module.exports = function(css) { var TokenType = require('../token-types'); var tokens = [], urlMode = false, blockMode = 0, c, // current character cn, // next character pos = 0, tn = 0, ln = 1, col = 1; var Punctuation = { ' ': TokenType.Space, '\n': TokenType.Newline, '\r': TokenType.Newline, '\t': TokenType.Tab, '!': TokenType.ExclamationMark, '"': TokenType.QuotationMark, '#': TokenType.NumberSign, '$': TokenType.DollarSign, '%': TokenType.PercentSign, '&': TokenType.Ampersand, '\'': TokenType.Apostrophe, '(': TokenType.LeftParenthesis, ')': TokenType.RightParenthesis, '*': TokenType.Asterisk, '+': TokenType.PlusSign, ',': TokenType.Comma, '-': TokenType.HyphenMinus, '.': TokenType.FullStop, '/': TokenType.Solidus, ':': TokenType.Colon, ';': TokenType.Semicolon, '<': TokenType.LessThanSign, '=': TokenType.EqualsSign, '==': TokenType.EqualitySign, '!=': TokenType.InequalitySign, '>': TokenType.GreaterThanSign, '?': TokenType.QuestionMark, '@': TokenType.CommercialAt, '[': TokenType.LeftSquareBracket, ']': TokenType.RightSquareBracket, '^': TokenType.CircumflexAccent, '_': TokenType.LowLine, '{': TokenType.LeftCurlyBracket, '|': TokenType.VerticalLine, '}': TokenType.RightCurlyBracket, '~': TokenType.Tilde }; /** * Add a token to the token list * @param {string} type * @param {string} value */ function pushToken(type, value, column) { tokens.push({ tn: tn++, ln: ln, col: column, type: type, value: value }); } /** * Check if a character is a decimal digit * @param {string} c Character * @returns {boolean} */ function isDecimalDigit(c) { return '0123456789'.indexOf(c) >= 0; } /** * Parse spaces * @param {string} css Unparsed part of CSS string */ function parseSpaces(css) { var start = pos; // Read the string until we meet a non-space character: for (; pos < css.length; pos++) { if (css.charAt(pos) !== ' ') break; } // Add a substring containing only spaces to tokens: pushToken(TokenType.Space, css.substring(start, pos--), col); col += pos - start; } /** * Parse a string within quotes * @param {string} css Unparsed part of CSS string * @param {string} q Quote (either `'` or `"`) */ function parseString(css, q) { var start = pos; // Read the string until we meet a matching quote: for (pos++; pos < css.length; pos++) { // Skip escaped quotes: if (css.charAt(pos) === '\\') pos++; else if (css.charAt(pos) === q) break; } // Add the string (including quotes) to tokens: pushToken(q === '"' ? TokenType.StringDQ : TokenType.StringSQ, css.substring(start, pos + 1), col); col += pos - start; } /** * Parse numbers * @param {string} css Unparsed part of CSS string */ function parseDecimalNumber(css) { var start = pos; // Read the string until we meet a character that's not a digit: for (; pos < css.length; pos++) { if (!isDecimalDigit(css.charAt(pos))) break; } // Add the number to tokens: pushToken(TokenType.DecimalNumber, css.substring(start, pos--), col); col += pos - start; } /** * Parse identifier * @param {string} css Unparsed part of CSS string */ function parseIdentifier(css) { var start = pos; // Skip all opening slashes: while (css.charAt(pos) === '/') pos++; // Read the string until we meet a punctuation mark: for (; pos < css.length; pos++) { // Skip all '\': if (css.charAt(pos) === '\\') pos++; else if (css.charAt(pos) in Punctuation) break; } var ident = css.substring(start, pos--); // Enter url mode if parsed substring is `url`: urlMode = urlMode || ident === 'url'; // Add identifier to tokens: pushToken(TokenType.Identifier, ident, col); col += pos - start; } /** * Parse equality sign * @param {string} sass Unparsed part of SASS string */ function parseEquality(css) { pushToken(TokenType.EqualitySign, '==', col); pos++; col++; } /** * Parse inequality sign * @param {string} sass Unparsed part of SASS string */ function parseInequality(css) { pushToken(TokenType.InequalitySign, '!=', col); pos++; col++; } /** * Parse a multiline comment * @param {string} css Unparsed part of CSS string */ function parseMLComment(css) { var start = pos; // Read the string until we meet `*/`. // Since we already know first 2 characters (`/*`), start reading // from `pos + 2`: for (pos += 2; pos < css.length; pos++) { if (css.charAt(pos) === '*' && css.charAt(pos + 1) === '/') { pos++; break; } } // Add full comment (including `/*` and `*/`) to the list of tokens: var comment = css.substring(start, pos + 1); pushToken(TokenType.CommentML, comment, col); var newlines = comment.split('\n'); if (newlines.length > 1) { ln += newlines.length - 1; col = newlines[newlines.length - 1].length; } else { col += (pos - start); } } /** * Parse a single line comment * @param {string} css Unparsed part of CSS string */ function parseSLComment(css) { var start = pos; // Read the string until we meet line break. // Since we already know first 2 characters (`//`), start reading // from `pos + 2`: for (pos += 2; pos < css.length; pos++) { if (css.charAt(pos) === '\n' || css.charAt(pos) === '\r') { break; } } // Add comment (including `//` and line break) to the list of tokens: pushToken(TokenType.CommentSL, css.substring(start, pos--), col); col += pos - start; } /** * Convert a CSS string to a list of tokens * @param {string} css CSS string * @returns {Array} List of tokens * @private */ function getTokens(css) { // Parse string, character by character: for (pos = 0; pos < css.length; col++, pos++) { c = css.charAt(pos); cn = css.charAt(pos + 1); // If we meet `/*`, it's a start of a multiline comment. // Parse following characters as a multiline comment: if (c === '/' && cn === '*') { parseMLComment(css); } // If we meet `//` and it is not a part of url: else if (!urlMode && c === '/' && cn === '/') { // If we're currently inside a block, treat `//` as a start // of identifier. Else treat `//` as a start of a single-line // comment: parseSLComment(css); } // If current character is a double or single quote, it's a start // of a string: else if (c === '"' || c === "'") { parseString(css, c); } // If current character is a space: else if (c === ' ') { parseSpaces(css); } // If current character is `=`, it must be combined with next `=` else if (c === '=' && cn === '=') { parseEquality(css); } // If we meet `!=`, this must be inequality else if (c === '!' && cn === '=') { parseInequality(css); } // If current character is a punctuation mark: else if (c in Punctuation) { // Add it to the list of tokens: pushToken(Punctuation[c], c, col); if (c === '\n' || c === '\r') { ln++; col = 0; } // Go to next line if (c === ')') urlMode = false; // exit url mode if (c === '{') blockMode++; // enter a block if (c === '}') blockMode--; // exit a block } // If current character is a decimal digit: else if (isDecimalDigit(c)) { parseDecimalNumber(css); } // If current character is anything else: else { parseIdentifier(css); } } return tokens; } return getTokens(css); };