123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305 |
- 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);
- };
|