/* MIT License http://www.opensource.org/licenses/mit-license.php Author Tobias Koppers @sokra */ "use strict"; /** * @typedef {object} CssTokenCallbacks * @property {(function(string, number, number, number, number): number)=} url * @property {(function(string, number, number): number)=} comment * @property {(function(string, number, number): number)=} string * @property {(function(string, number, number): number)=} leftParenthesis * @property {(function(string, number, number): number)=} rightParenthesis * @property {(function(string, number, number): number)=} function * @property {(function(string, number, number): number)=} colon * @property {(function(string, number, number): number)=} atKeyword * @property {(function(string, number, number): number)=} delim * @property {(function(string, number, number): number)=} identifier * @property {(function(string, number, number, boolean): number)=} hash * @property {(function(string, number, number): number)=} leftCurlyBracket * @property {(function(string, number, number): number)=} rightCurlyBracket * @property {(function(string, number, number): number)=} semicolon * @property {(function(string, number, number): number)=} comma */ /** @typedef {function(string, number, CssTokenCallbacks): number} CharHandler */ // spec: https://drafts.csswg.org/css-syntax/ const CC_LINE_FEED = "\n".charCodeAt(0); const CC_CARRIAGE_RETURN = "\r".charCodeAt(0); const CC_FORM_FEED = "\f".charCodeAt(0); const CC_TAB = "\t".charCodeAt(0); const CC_SPACE = " ".charCodeAt(0); const CC_SOLIDUS = "/".charCodeAt(0); const CC_REVERSE_SOLIDUS = "\\".charCodeAt(0); const CC_ASTERISK = "*".charCodeAt(0); const CC_LEFT_PARENTHESIS = "(".charCodeAt(0); const CC_RIGHT_PARENTHESIS = ")".charCodeAt(0); const CC_LEFT_CURLY = "{".charCodeAt(0); const CC_RIGHT_CURLY = "}".charCodeAt(0); const CC_LEFT_SQUARE = "[".charCodeAt(0); const CC_RIGHT_SQUARE = "]".charCodeAt(0); const CC_QUOTATION_MARK = '"'.charCodeAt(0); const CC_APOSTROPHE = "'".charCodeAt(0); const CC_FULL_STOP = ".".charCodeAt(0); const CC_COLON = ":".charCodeAt(0); const CC_SEMICOLON = ";".charCodeAt(0); const CC_COMMA = ",".charCodeAt(0); const CC_PERCENTAGE = "%".charCodeAt(0); const CC_AT_SIGN = "@".charCodeAt(0); const CC_LOW_LINE = "_".charCodeAt(0); const CC_LOWER_A = "a".charCodeAt(0); const CC_LOWER_F = "f".charCodeAt(0); const CC_LOWER_E = "e".charCodeAt(0); const CC_LOWER_U = "u".charCodeAt(0); const CC_LOWER_Z = "z".charCodeAt(0); const CC_UPPER_A = "A".charCodeAt(0); const CC_UPPER_F = "F".charCodeAt(0); const CC_UPPER_E = "E".charCodeAt(0); const CC_UPPER_U = "E".charCodeAt(0); const CC_UPPER_Z = "Z".charCodeAt(0); const CC_0 = "0".charCodeAt(0); const CC_9 = "9".charCodeAt(0); const CC_NUMBER_SIGN = "#".charCodeAt(0); const CC_PLUS_SIGN = "+".charCodeAt(0); const CC_HYPHEN_MINUS = "-".charCodeAt(0); const CC_LESS_THAN_SIGN = "<".charCodeAt(0); const CC_GREATER_THAN_SIGN = ">".charCodeAt(0); /** * @param {number} cc char code * @returns {boolean} true, if cc is a newline */ const _isNewLine = cc => cc === CC_LINE_FEED || cc === CC_CARRIAGE_RETURN || cc === CC_FORM_FEED; /** @type {CharHandler} */ const consumeSpace = (input, pos, _callbacks) => { // Consume as much whitespace as possible. while (_isWhiteSpace(input.charCodeAt(pos))) { pos++; } // Return a . return pos; }; /** * @param {number} cc char code * @returns {boolean} true, if cc is a newline */ const _isNewline = cc => cc === CC_LINE_FEED || cc === CC_CARRIAGE_RETURN || cc === CC_FORM_FEED; /** * @param {number} cc char code * @returns {boolean} true, if cc is a space (U+0009 CHARACTER TABULATION or U+0020 SPACE) */ const _isSpace = cc => cc === CC_TAB || cc === CC_SPACE; /** * @param {number} cc char code * @returns {boolean} true, if cc is a whitespace */ const _isWhiteSpace = cc => _isNewline(cc) || _isSpace(cc); /** * ident-start code point * * A letter, a non-ASCII code point, or U+005F LOW LINE (_). * @param {number} cc char code * @returns {boolean} true, if cc is a start code point of an identifier */ const isIdentStartCodePoint = cc => (cc >= CC_LOWER_A && cc <= CC_LOWER_Z) || (cc >= CC_UPPER_A && cc <= CC_UPPER_Z) || cc === CC_LOW_LINE || cc >= 0x80; /** @type {CharHandler} */ const consumeDelimToken = (input, pos, _callbacks) => // Return a with its value set to the current input code point. pos; /** @type {CharHandler} */ const consumeComments = (input, pos, callbacks) => { // This section describes how to consume comments from a stream of code points. It returns nothing. // If the next two input code point are U+002F SOLIDUS (/) followed by a U+002A ASTERISK (*), // consume them and all following code points up to and including the first U+002A ASTERISK (*) // followed by a U+002F SOLIDUS (/), or up to an EOF code point. // Return to the start of this step. while ( input.charCodeAt(pos) === CC_SOLIDUS && input.charCodeAt(pos + 1) === CC_ASTERISK ) { const start = pos; pos += 2; for (;;) { if (pos === input.length) { // If the preceding paragraph ended by consuming an EOF code point, this is a parse error. return pos; } if ( input.charCodeAt(pos) === CC_ASTERISK && input.charCodeAt(pos + 1) === CC_SOLIDUS ) { pos += 2; if (callbacks.comment) { pos = callbacks.comment(input, start, pos); } break; } pos++; } } return pos; }; /** * @param {number} cc char code * @returns {boolean} true, if cc is a hex digit */ const _isHexDigit = cc => _isDigit(cc) || (cc >= CC_UPPER_A && cc <= CC_UPPER_F) || (cc >= CC_LOWER_A && cc <= CC_LOWER_F); /** * @param {string} input input * @param {number} pos position * @returns {number} position */ const _consumeAnEscapedCodePoint = (input, pos) => { // This section describes how to consume an escaped code point. // It assumes that the U+005C REVERSE SOLIDUS (\) has already been consumed and that the next input code point has already been verified to be part of a valid escape. // It will return a code point. // Consume the next input code point. const cc = input.charCodeAt(pos); pos++; // EOF // This is a parse error. Return U+FFFD REPLACEMENT CHARACTER (�). if (pos === input.length) { return pos; } // hex digit // Consume as many hex digits as possible, but no more than 5. // Note that this means 1-6 hex digits have been consumed in total. // If the next input code point is whitespace, consume it as well. // Interpret the hex digits as a hexadecimal number. // If this number is zero, or is for a surrogate, or is greater than the maximum allowed code point, return U+FFFD REPLACEMENT CHARACTER (�). // Otherwise, return the code point with that value. if (_isHexDigit(cc)) { for (let i = 0; i < 5; i++) { if (_isHexDigit(input.charCodeAt(pos))) { pos++; } } if (_isWhiteSpace(input.charCodeAt(pos))) { pos++; } return pos; } // anything else // Return the current input code point. return pos; }; /** @type {CharHandler} */ const consumeAStringToken = (input, pos, callbacks) => { // This section describes how to consume a string token from a stream of code points. // It returns either a or . // // This algorithm may be called with an ending code point, which denotes the code point that ends the string. // If an ending code point is not specified, the current input code point is used. const start = pos - 1; const endingCodePoint = input.charCodeAt(pos - 1); // Initially create a with its value set to the empty string. // Repeatedly consume the next input code point from the stream: for (;;) { // EOF // This is a parse error. Return the . if (pos === input.length) { if (callbacks.string !== undefined) { return callbacks.string(input, start, pos); } return pos; } const cc = input.charCodeAt(pos); pos++; // ending code point // Return the . if (cc === endingCodePoint) { if (callbacks.string !== undefined) { return callbacks.string(input, start, pos); } return pos; } // newline // This is a parse error. // Reconsume the current input code point, create a , and return it. else if (_isNewLine(cc)) { pos--; // bad string return pos; } // U+005C REVERSE SOLIDUS (\) else if (cc === CC_REVERSE_SOLIDUS) { // If the next input code point is EOF, do nothing. if (pos === input.length) { return pos; } // Otherwise, if the next input code point is a newline, consume it. else if (_isNewLine(input.charCodeAt(pos))) { pos++; } // Otherwise, (the stream starts with a valid escape) consume an escaped code point and append the returned code point to the ’s value. else if (_ifTwoCodePointsAreValidEscape(input, pos)) { pos = _consumeAnEscapedCodePoint(input, pos); } } // anything else // Append the current input code point to the ’s value. else { // Append } } }; /** * @param {number} cc char code * @param {number} q char code * @returns {boolean} is non-ASCII code point */ const isNonASCIICodePoint = (cc, q) => // Simplify cc > 0x80; /** * @param {number} cc char code * @returns {boolean} is letter */ const isLetter = cc => (cc >= CC_LOWER_A && cc <= CC_LOWER_Z) || (cc >= CC_UPPER_A && cc <= CC_UPPER_Z); /** * @param {number} cc char code * @param {number} q char code * @returns {boolean} is identifier start code */ const _isIdentStartCodePoint = (cc, q) => isLetter(cc) || isNonASCIICodePoint(cc, q) || cc === CC_LOW_LINE; /** * @param {number} cc char code * @param {number} q char code * @returns {boolean} is identifier code */ const _isIdentCodePoint = (cc, q) => _isIdentStartCodePoint(cc, q) || _isDigit(cc) || cc === CC_HYPHEN_MINUS; /** * @param {number} cc char code * @returns {boolean} is digit */ const _isDigit = cc => cc >= CC_0 && cc <= CC_9; /** * @param {string} input input * @param {number} pos position * @param {number=} f first code point * @param {number=} s second code point * @returns {boolean} true if two code points are a valid escape */ const _ifTwoCodePointsAreValidEscape = (input, pos, f, s) => { // This section describes how to check if two code points are a valid escape. // The algorithm described here can be called explicitly with two code points, or can be called with the input stream itself. // In the latter case, the two code points in question are the current input code point and the next input code point, in that order. // Note: This algorithm will not consume any additional code point. const first = f || input.charCodeAt(pos - 1); const second = s || input.charCodeAt(pos); // If the first code point is not U+005C REVERSE SOLIDUS (\), return false. if (first !== CC_REVERSE_SOLIDUS) return false; // Otherwise, if the second code point is a newline, return false. if (_isNewLine(second)) return false; // Otherwise, return true. return true; }; /** * @param {string} input input * @param {number} pos position * @param {number=} f first * @param {number=} s second * @param {number=} t third * @returns {boolean} true, if input at pos starts an identifier */ const _ifThreeCodePointsWouldStartAnIdentSequence = (input, pos, f, s, t) => { // This section describes how to check if three code points would start an ident sequence. // The algorithm described here can be called explicitly with three code points, or can be called with the input stream itself. // In the latter case, the three code points in question are the current input code point and the next two input code points, in that order. // Note: This algorithm will not consume any additional code points. const first = f || input.charCodeAt(pos - 1); const second = s || input.charCodeAt(pos); const third = t || input.charCodeAt(pos + 1); // Look at the first code point: // U+002D HYPHEN-MINUS if (first === CC_HYPHEN_MINUS) { // If the second code point is an ident-start code point or a U+002D HYPHEN-MINUS // or a U+002D HYPHEN-MINUS, or the second and third code points are a valid escape, return true. if ( _isIdentStartCodePoint(second, pos) || second === CC_HYPHEN_MINUS || _ifTwoCodePointsAreValidEscape(input, pos, second, third) ) { return true; } return false; } // ident-start code point else if (_isIdentStartCodePoint(first, pos - 1)) { return true; } // U+005C REVERSE SOLIDUS (\) // If the first and second code points are a valid escape, return true. Otherwise, return false. else if (first === CC_REVERSE_SOLIDUS) { if (_ifTwoCodePointsAreValidEscape(input, pos, first, second)) { return true; } return false; } // anything else // Return false. return false; }; /** * @param {string} input input * @param {number} pos position * @param {number=} f first * @param {number=} s second * @param {number=} t third * @returns {boolean} true, if input at pos starts an identifier */ const _ifThreeCodePointsWouldStartANumber = (input, pos, f, s, t) => { // This section describes how to check if three code points would start a number. // The algorithm described here can be called explicitly with three code points, or can be called with the input stream itself. // In the latter case, the three code points in question are the current input code point and the next two input code points, in that order. // Note: This algorithm will not consume any additional code points. const first = f || input.charCodeAt(pos - 1); const second = s || input.charCodeAt(pos); const third = t || input.charCodeAt(pos); // Look at the first code point: // U+002B PLUS SIGN (+) // U+002D HYPHEN-MINUS (-) // // If the second code point is a digit, return true. // Otherwise, if the second code point is a U+002E FULL STOP (.) and the third code point is a digit, return true. // Otherwise, return false. if (first === CC_PLUS_SIGN || first === CC_HYPHEN_MINUS) { if (_isDigit(second)) { return true; } else if (second === CC_FULL_STOP && _isDigit(third)) { return true; } return false; } // U+002E FULL STOP (.) // If the second code point is a digit, return true. Otherwise, return false. else if (first === CC_FULL_STOP) { if (_isDigit(second)) { return true; } return false; } // digit // Return true. else if (_isDigit(first)) { return true; } // anything else // Return false. return false; }; /** @type {CharHandler} */ const consumeNumberSign = (input, pos, callbacks) => { // If the next input code point is an ident code point or the next two input code points are a valid escape, then: // - Create a . // - If the next 3 input code points would start an ident sequence, set the ’s type flag to "id". // - Consume an ident sequence, and set the ’s value to the returned string. // - Return the . const start = pos - 1; const first = input.charCodeAt(pos); const second = input.charCodeAt(pos + 1); if ( _isIdentCodePoint(first, pos - 1) || _ifTwoCodePointsAreValidEscape(input, pos, first, second) ) { const third = input.charCodeAt(pos + 2); let isId = false; if ( _ifThreeCodePointsWouldStartAnIdentSequence( input, pos, first, second, third ) ) { isId = true; } pos = _consumeAnIdentSequence(input, pos, callbacks); if (callbacks.hash !== undefined) { return callbacks.hash(input, start, pos, isId); } return pos; } // Otherwise, return a with its value set to the current input code point. return pos; }; /** @type {CharHandler} */ const consumeHyphenMinus = (input, pos, callbacks) => { // If the input stream starts with a number, reconsume the current input code point, consume a numeric token, and return it. if (_ifThreeCodePointsWouldStartANumber(input, pos)) { pos--; return consumeANumericToken(input, pos, callbacks); } // Otherwise, if the next 2 input code points are U+002D HYPHEN-MINUS U+003E GREATER-THAN SIGN (->), consume them and return a . else if ( input.charCodeAt(pos) === CC_HYPHEN_MINUS && input.charCodeAt(pos + 1) === CC_GREATER_THAN_SIGN ) { return pos + 2; } // Otherwise, if the input stream starts with an ident sequence, reconsume the current input code point, consume an ident-like token, and return it. else if (_ifThreeCodePointsWouldStartAnIdentSequence(input, pos)) { pos--; return consumeAnIdentLikeToken(input, pos, callbacks); } // Otherwise, return a with its value set to the current input code point. return pos; }; /** @type {CharHandler} */ const consumeFullStop = (input, pos, callbacks) => { const start = pos - 1; // If the input stream starts with a number, reconsume the current input code point, consume a numeric token, and return it. if (_ifThreeCodePointsWouldStartANumber(input, pos)) { pos--; return consumeANumericToken(input, pos, callbacks); } // Otherwise, return a with its value set to the current input code point. if (callbacks.delim !== undefined) { return callbacks.delim(input, start, pos); } return pos; }; /** @type {CharHandler} */ const consumePlusSign = (input, pos, callbacks) => { // If the input stream starts with a number, reconsume the current input code point, consume a numeric token, and return it. if (_ifThreeCodePointsWouldStartANumber(input, pos)) { pos--; return consumeANumericToken(input, pos, callbacks); } // Otherwise, return a with its value set to the current input code point. return pos; }; /** @type {CharHandler} */ const _consumeANumber = (input, pos) => { // This section describes how to consume a number from a stream of code points. // It returns a numeric value, and a type which is either "integer" or "number". // Execute the following steps in order: // Initially set type to "integer". Let repr be the empty string. // If the next input code point is U+002B PLUS SIGN (+) or U+002D HYPHEN-MINUS (-), consume it and append it to repr. if ( input.charCodeAt(pos) === CC_HYPHEN_MINUS || input.charCodeAt(pos) === CC_PLUS_SIGN ) { pos++; } // While the next input code point is a digit, consume it and append it to repr. while (_isDigit(input.charCodeAt(pos))) { pos++; } // If the next 2 input code points are U+002E FULL STOP (.) followed by a digit, then: // 1. Consume the next input code point and append it to number part. // 2. While the next input code point is a digit, consume it and append it to number part. // 3. Set type to "number". if ( input.charCodeAt(pos) === CC_FULL_STOP && _isDigit(input.charCodeAt(pos + 1)) ) { pos++; while (_isDigit(input.charCodeAt(pos))) { pos++; } } // If the next 2 or 3 input code points are U+0045 LATIN CAPITAL LETTER E (E) or U+0065 LATIN SMALL LETTER E (e), optionally followed by U+002D HYPHEN-MINUS (-) or U+002B PLUS SIGN (+), followed by a digit, then: // 1. Consume the next input code point. // 2. If the next input code point is "+" or "-", consume it and append it to exponent part. // 3. While the next input code point is a digit, consume it and append it to exponent part. // 4. Set type to "number". if ( (input.charCodeAt(pos) === CC_LOWER_E || input.charCodeAt(pos) === CC_UPPER_E) && (((input.charCodeAt(pos + 1) === CC_HYPHEN_MINUS || input.charCodeAt(pos + 1) === CC_PLUS_SIGN) && _isDigit(input.charCodeAt(pos + 2))) || _isDigit(input.charCodeAt(pos + 1))) ) { pos++; if ( input.charCodeAt(pos) === CC_PLUS_SIGN || input.charCodeAt(pos) === CC_HYPHEN_MINUS ) { pos++; } while (_isDigit(input.charCodeAt(pos))) { pos++; } } // Let value be the result of interpreting number part as a base-10 number. // If exponent part is non-empty, interpret it as a base-10 integer, then raise 10 to the power of the result, multiply it by value, and set value to that result. // Return value and type. return pos; }; /** @type {CharHandler} */ const consumeANumericToken = (input, pos, callbacks) => { // This section describes how to consume a numeric token from a stream of code points. // It returns either a , , or . // Consume a number and let number be the result. pos = _consumeANumber(input, pos, callbacks); // If the next 3 input code points would start an ident sequence, then: // // - Create a with the same value and type flag as number, and a unit set initially to the empty string. // - Consume an ident sequence. Set the ’s unit to the returned value. // - Return the . const first = input.charCodeAt(pos); const second = input.charCodeAt(pos + 1); const third = input.charCodeAt(pos + 2); if ( _ifThreeCodePointsWouldStartAnIdentSequence( input, pos, first, second, third ) ) { return _consumeAnIdentSequence(input, pos, callbacks); } // Otherwise, if the next input code point is U+0025 PERCENTAGE SIGN (%), consume it. // Create a with the same value as number, and return it. else if (first === CC_PERCENTAGE) { return pos + 1; } // Otherwise, create a with the same value and type flag as number, and return it. return pos; }; /** @type {CharHandler} */ const consumeColon = (input, pos, callbacks) => { // Return a . if (callbacks.colon !== undefined) { return callbacks.colon(input, pos - 1, pos); } return pos; }; /** @type {CharHandler} */ const consumeLeftParenthesis = (input, pos, callbacks) => { // Return a <(-token>. if (callbacks.leftParenthesis !== undefined) { return callbacks.leftParenthesis(input, pos - 1, pos); } return pos; }; /** @type {CharHandler} */ const consumeRightParenthesis = (input, pos, callbacks) => { // Return a <)-token>. if (callbacks.rightParenthesis !== undefined) { return callbacks.rightParenthesis(input, pos - 1, pos); } return pos; }; /** @type {CharHandler} */ const consumeLeftSquareBracket = (input, pos, callbacks) => // Return a <]-token>. pos; /** @type {CharHandler} */ const consumeRightSquareBracket = (input, pos, callbacks) => // Return a <]-token>. pos; /** @type {CharHandler} */ const consumeLeftCurlyBracket = (input, pos, callbacks) => { // Return a <{-token>. if (callbacks.leftCurlyBracket !== undefined) { return callbacks.leftCurlyBracket(input, pos - 1, pos); } return pos; }; /** @type {CharHandler} */ const consumeRightCurlyBracket = (input, pos, callbacks) => { // Return a <}-token>. if (callbacks.rightCurlyBracket !== undefined) { return callbacks.rightCurlyBracket(input, pos - 1, pos); } return pos; }; /** @type {CharHandler} */ const consumeSemicolon = (input, pos, callbacks) => { // Return a . if (callbacks.semicolon !== undefined) { return callbacks.semicolon(input, pos - 1, pos); } return pos; }; /** @type {CharHandler} */ const consumeComma = (input, pos, callbacks) => { // Return a . if (callbacks.comma !== undefined) { return callbacks.comma(input, pos - 1, pos); } return pos; }; /** @type {CharHandler} */ const _consumeAnIdentSequence = (input, pos) => { // This section describes how to consume an ident sequence from a stream of code points. // It returns a string containing the largest name that can be formed from adjacent code points in the stream, starting from the first. // Note: This algorithm does not do the verification of the first few code points that are necessary to ensure the returned code points would constitute an . // If that is the intended use, ensure that the stream starts with an ident sequence before calling this algorithm. // Let result initially be an empty string. // Repeatedly consume the next input code point from the stream: for (;;) { const cc = input.charCodeAt(pos); pos++; // ident code point // Append the code point to result. if (_isIdentCodePoint(cc, pos - 1)) { // Nothing } // the stream starts with a valid escape // Consume an escaped code point. Append the returned code point to result. else if (_ifTwoCodePointsAreValidEscape(input, pos)) { pos = _consumeAnEscapedCodePoint(input, pos); } // anything else // Reconsume the current input code point. Return result. else { return pos - 1; } } }; /** * @param {number} cc char code * @returns {boolean} true, when cc is the non-printable code point, otherwise false */ const _isNonPrintableCodePoint = cc => (cc >= 0x00 && cc <= 0x08) || cc === 0x0b || (cc >= 0x0e && cc <= 0x1f) || cc === 0x7f; /** * @param {string} input input * @param {number} pos position * @returns {number} position */ const consumeTheRemnantsOfABadUrl = (input, pos) => { // This section describes how to consume the remnants of a bad url from a stream of code points, // "cleaning up" after the tokenizer realizes that it’s in the middle of a rather than a . // It returns nothing; its sole use is to consume enough of the input stream to reach a recovery point where normal tokenizing can resume. // Repeatedly consume the next input code point from the stream: for (;;) { // EOF // Return. if (pos === input.length) { return pos; } const cc = input.charCodeAt(pos); pos++; // U+0029 RIGHT PARENTHESIS ()) // Return. if (cc === CC_RIGHT_PARENTHESIS) { return pos; } // the input stream starts with a valid escape // Consume an escaped code point. // This allows an escaped right parenthesis ("\)") to be encountered without ending the . // This is otherwise identical to the "anything else" clause. else if (_ifTwoCodePointsAreValidEscape(input, pos)) { pos = _consumeAnEscapedCodePoint(input, pos); } // anything else // Do nothing. else { // Do nothing. } } }; /** * @param {string} input input * @param {number} pos position * @param {number} fnStart start * @param {CssTokenCallbacks} callbacks callbacks * @returns {pos} pos */ const consumeAUrlToken = (input, pos, fnStart, callbacks) => { // This section describes how to consume a url token from a stream of code points. // It returns either a or a . // Note: This algorithm assumes that the initial "url(" has already been consumed. // This algorithm also assumes that it’s being called to consume an "unquoted" value, like url(foo). // A quoted value, like url("foo"), is parsed as a . // Consume an ident-like token automatically handles this distinction; this algorithm shouldn’t be called directly otherwise. // Initially create a with its value set to the empty string. // Consume as much whitespace as possible. while (_isWhiteSpace(input.charCodeAt(pos))) { pos++; } const contentStart = pos; // Repeatedly consume the next input code point from the stream: for (;;) { // EOF // This is a parse error. Return the . if (pos === input.length) { if (callbacks.url !== undefined) { return callbacks.url(input, fnStart, pos, contentStart, pos - 1); } return pos; } const cc = input.charCodeAt(pos); pos++; // U+0029 RIGHT PARENTHESIS ()) // Return the . if (cc === CC_RIGHT_PARENTHESIS) { if (callbacks.url !== undefined) { return callbacks.url(input, fnStart, pos, contentStart, pos - 1); } return pos; } // whitespace // Consume as much whitespace as possible. // If the next input code point is U+0029 RIGHT PARENTHESIS ()) or EOF, consume it and return the // (if EOF was encountered, this is a parse error); otherwise, consume the remnants of a bad url, create a , and return it. else if (_isWhiteSpace(cc)) { const end = pos - 1; while (_isWhiteSpace(input.charCodeAt(pos))) { pos++; } if (pos === input.length) { if (callbacks.url !== undefined) { return callbacks.url(input, fnStart, pos, contentStart, end); } return pos; } if (input.charCodeAt(pos) === CC_RIGHT_PARENTHESIS) { pos++; if (callbacks.url !== undefined) { return callbacks.url(input, fnStart, pos, contentStart, end); } return pos; } // Don't handle bad urls return consumeTheRemnantsOfABadUrl(input, pos); } // U+0022 QUOTATION MARK (") // U+0027 APOSTROPHE (') // U+0028 LEFT PARENTHESIS (() // non-printable code point // This is a parse error. Consume the remnants of a bad url, create a , and return it. else if ( cc === CC_QUOTATION_MARK || cc === CC_APOSTROPHE || cc === CC_LEFT_PARENTHESIS || _isNonPrintableCodePoint(cc) ) { // Don't handle bad urls return consumeTheRemnantsOfABadUrl(input, pos); } // // U+005C REVERSE SOLIDUS (\) // // If the stream starts with a valid escape, consume an escaped code point and append the returned code point to the ’s value. // // Otherwise, this is a parse error. Consume the remnants of a bad url, create a , and return it. else if (cc === CC_REVERSE_SOLIDUS) { if (_ifTwoCodePointsAreValidEscape(input, pos)) { pos = _consumeAnEscapedCodePoint(input, pos); } else { // Don't handle bad urls return consumeTheRemnantsOfABadUrl(input, pos); } } // anything else // Append the current input code point to the ’s value. else { // Nothing } } }; /** @type {CharHandler} */ const consumeAnIdentLikeToken = (input, pos, callbacks) => { const start = pos; // This section describes how to consume an ident-like token from a stream of code points. // It returns an , , , or . pos = _consumeAnIdentSequence(input, pos, callbacks); // If string’s value is an ASCII case-insensitive match for "url", and the next input code point is U+0028 LEFT PARENTHESIS ((), consume it. // While the next two input code points are whitespace, consume the next input code point. // If the next one or two input code points are U+0022 QUOTATION MARK ("), U+0027 APOSTROPHE ('), or whitespace followed by U+0022 QUOTATION MARK (") or U+0027 APOSTROPHE ('), then create a with its value set to string and return it. // Otherwise, consume a url token, and return it. if ( input.slice(start, pos).toLowerCase() === "url" && input.charCodeAt(pos) === CC_LEFT_PARENTHESIS ) { pos++; const end = pos; while ( _isWhiteSpace(input.charCodeAt(pos)) && _isWhiteSpace(input.charCodeAt(pos + 1)) ) { pos++; } if ( input.charCodeAt(pos) === CC_QUOTATION_MARK || input.charCodeAt(pos) === CC_APOSTROPHE || (_isWhiteSpace(input.charCodeAt(pos)) && (input.charCodeAt(pos + 1) === CC_QUOTATION_MARK || input.charCodeAt(pos + 1) === CC_APOSTROPHE)) ) { if (callbacks.function !== undefined) { return callbacks.function(input, start, end); } return pos; } return consumeAUrlToken(input, pos, start, callbacks); } // Otherwise, if the next input code point is U+0028 LEFT PARENTHESIS ((), consume it. // Create a with its value set to string and return it. if (input.charCodeAt(pos) === CC_LEFT_PARENTHESIS) { pos++; if (callbacks.function !== undefined) { return callbacks.function(input, start, pos); } return pos; } // Otherwise, create an with its value set to string and return it. if (callbacks.identifier !== undefined) { return callbacks.identifier(input, start, pos); } return pos; }; /** @type {CharHandler} */ const consumeLessThan = (input, pos, _callbacks) => { // If the next 3 input code points are U+0021 EXCLAMATION MARK U+002D HYPHEN-MINUS U+002D HYPHEN-MINUS (!--), consume them and return a . if (input.slice(pos, pos + 3) === "!--") { return pos + 3; } // Otherwise, return a with its value set to the current input code point. return pos; }; /** @type {CharHandler} */ const consumeCommercialAt = (input, pos, callbacks) => { const start = pos - 1; // If the next 3 input code points would start an ident sequence, consume an ident sequence, create an with its value set to the returned value, and return it. if ( _ifThreeCodePointsWouldStartAnIdentSequence( input, pos, input.charCodeAt(pos), input.charCodeAt(pos + 1), input.charCodeAt(pos + 2) ) ) { pos = _consumeAnIdentSequence(input, pos, callbacks); if (callbacks.atKeyword !== undefined) { pos = callbacks.atKeyword(input, start, pos); } return pos; } // Otherwise, return a with its value set to the current input code point. return pos; }; /** @type {CharHandler} */ const consumeReverseSolidus = (input, pos, callbacks) => { // If the input stream starts with a valid escape, reconsume the current input code point, consume an ident-like token, and return it. if (_ifTwoCodePointsAreValidEscape(input, pos)) { pos--; return consumeAnIdentLikeToken(input, pos, callbacks); } // Otherwise, this is a parse error. Return a with its value set to the current input code point. return pos; }; /** @type {CharHandler} */ const consumeAToken = (input, pos, callbacks) => { const cc = input.charCodeAt(pos - 1); // https://drafts.csswg.org/css-syntax/#consume-token switch (cc) { // whitespace case CC_LINE_FEED: case CC_CARRIAGE_RETURN: case CC_FORM_FEED: case CC_TAB: case CC_SPACE: return consumeSpace(input, pos, callbacks); // U+0022 QUOTATION MARK (") case CC_QUOTATION_MARK: return consumeAStringToken(input, pos, callbacks); // U+0023 NUMBER SIGN (#) case CC_NUMBER_SIGN: return consumeNumberSign(input, pos, callbacks); // U+0027 APOSTROPHE (') case CC_APOSTROPHE: return consumeAStringToken(input, pos, callbacks); // U+0028 LEFT PARENTHESIS (() case CC_LEFT_PARENTHESIS: return consumeLeftParenthesis(input, pos, callbacks); // U+0029 RIGHT PARENTHESIS ()) case CC_RIGHT_PARENTHESIS: return consumeRightParenthesis(input, pos, callbacks); // U+002B PLUS SIGN (+) case CC_PLUS_SIGN: return consumePlusSign(input, pos, callbacks); // U+002C COMMA (,) case CC_COMMA: return consumeComma(input, pos, callbacks); // U+002D HYPHEN-MINUS (-) case CC_HYPHEN_MINUS: return consumeHyphenMinus(input, pos, callbacks); // U+002E FULL STOP (.) case CC_FULL_STOP: return consumeFullStop(input, pos, callbacks); // U+003A COLON (:) case CC_COLON: return consumeColon(input, pos, callbacks); // U+003B SEMICOLON (;) case CC_SEMICOLON: return consumeSemicolon(input, pos, callbacks); // U+003C LESS-THAN SIGN (<) case CC_LESS_THAN_SIGN: return consumeLessThan(input, pos, callbacks); // U+0040 COMMERCIAL AT (@) case CC_AT_SIGN: return consumeCommercialAt(input, pos, callbacks); // U+005B LEFT SQUARE BRACKET ([) case CC_LEFT_SQUARE: return consumeLeftSquareBracket(input, pos, callbacks); // U+005C REVERSE SOLIDUS (\) case CC_REVERSE_SOLIDUS: return consumeReverseSolidus(input, pos, callbacks); // U+005D RIGHT SQUARE BRACKET (]) case CC_RIGHT_SQUARE: return consumeRightSquareBracket(input, pos, callbacks); // U+007B LEFT CURLY BRACKET ({) case CC_LEFT_CURLY: return consumeLeftCurlyBracket(input, pos, callbacks); // U+007D RIGHT CURLY BRACKET (}) case CC_RIGHT_CURLY: return consumeRightCurlyBracket(input, pos, callbacks); default: // digit // Reconsume the current input code point, consume a numeric token, and return it. if (_isDigit(cc)) { pos--; return consumeANumericToken(input, pos, callbacks); } else if (cc === CC_LOWER_U || cc === CC_UPPER_U) { // If unicode ranges allowed is true and the input stream would start a unicode-range, // reconsume the current input code point, consume a unicode-range token, and return it. // Skip now // if (_ifThreeCodePointsWouldStartAUnicodeRange(input, pos)) { // pos--; // return consumeAUnicodeRangeToken(input, pos, callbacks); // } // Otherwise, reconsume the current input code point, consume an ident-like token, and return it. pos--; return consumeAnIdentLikeToken(input, pos, callbacks); } // ident-start code point // Reconsume the current input code point, consume an ident-like token, and return it. else if (isIdentStartCodePoint(cc)) { pos--; return consumeAnIdentLikeToken(input, pos, callbacks); } // EOF, but we don't have it // anything else // Return a with its value set to the current input code point. return consumeDelimToken(input, pos, callbacks); } }; /** * @param {string} input input css * @param {CssTokenCallbacks} callbacks callbacks * @returns {void} */ module.exports = (input, callbacks) => { // This section describes how to consume a token from a stream of code points. It will return a single token of any type. let pos = 0; while (pos < input.length) { // Consume comments. pos = consumeComments(input, pos, callbacks); // Consume the next input code point. pos++; pos = consumeAToken(input, pos, callbacks); } }; module.exports.isIdentStartCodePoint = isIdentStartCodePoint; /** * @param {string} input input * @param {number} pos position * @returns {number} position after comments */ module.exports.eatComments = (input, pos) => { for (;;) { const originalPos = pos; pos = consumeComments(input, pos, {}); if (originalPos === pos) { break; } } return pos; }; /** * @param {string} input input * @param {number} pos position * @returns {number} position after whitespace */ module.exports.eatWhitespace = (input, pos) => { while (_isWhiteSpace(input.charCodeAt(pos))) { pos++; } return pos; }; /** * @param {string} input input * @param {number} pos position * @returns {number} position after whitespace and comments */ module.exports.eatWhitespaceAndComments = (input, pos) => { for (;;) { const originalPos = pos; pos = consumeComments(input, pos, {}); while (_isWhiteSpace(input.charCodeAt(pos))) { pos++; } if (originalPos === pos) { break; } } return pos; }; /** * @param {string} input input * @param {number} pos position * @returns {number} position after whitespace and comments */ module.exports.eatComments = (input, pos) => { for (;;) { const originalPos = pos; pos = consumeComments(input, pos, {}); if (originalPos === pos) { break; } } return pos; }; /** * @param {string} input input * @param {number} pos position * @returns {number} position after whitespace */ module.exports.eatWhiteLine = (input, pos) => { for (;;) { const cc = input.charCodeAt(pos); if (_isSpace(cc)) { pos++; continue; } if (_isNewLine(cc)) pos++; // For `\r\n` if (cc === CC_CARRIAGE_RETURN && input.charCodeAt(pos + 1) === CC_LINE_FEED) pos++; break; } return pos; }; /** * @param {string} input input * @param {number} pos position * @returns {[number, number] | undefined} positions of ident sequence */ module.exports.skipCommentsAndEatIdentSequence = (input, pos) => { pos = module.exports.eatComments(input, pos); const start = pos; if ( _ifThreeCodePointsWouldStartAnIdentSequence( input, pos, input.charCodeAt(pos), input.charCodeAt(pos + 1), input.charCodeAt(pos + 2) ) ) { return [start, _consumeAnIdentSequence(input, pos, {})]; } return undefined; }; /** * @param {string} input input * @param {number} pos position * @returns {[number, number] | undefined} positions of ident sequence */ module.exports.eatString = (input, pos) => { pos = module.exports.eatWhitespaceAndComments(input, pos); const start = pos; if ( input.charCodeAt(pos) === CC_QUOTATION_MARK || input.charCodeAt(pos) === CC_APOSTROPHE ) { return [start, consumeAStringToken(input, pos + 1, {})]; } return undefined; }; /** * @param {string} input input * @param {number} pos position * @param {CssTokenCallbacks} cbs callbacks * @returns {[number, number][]} positions of ident sequence */ module.exports.eatImageSetStrings = (input, pos, cbs) => { /** @type {[number, number][]} */ const result = []; let isFirst = true; let needStop = false; // We already in `func(` token let balanced = 1; /** @type {CssTokenCallbacks} */ const callbacks = { ...cbs, string: (_input, start, end) => { if (isFirst && balanced === 1) { result.push([start, end]); isFirst = false; } return end; }, comma: (_input, _start, end) => { if (balanced === 1) { isFirst = true; } return end; }, leftParenthesis: (input, start, end) => { balanced++; return end; }, function: (_input, start, end) => { balanced++; return end; }, rightParenthesis: (_input, _start, end) => { balanced--; if (balanced === 0) { needStop = true; } return end; } }; while (pos < input.length) { // Consume comments. pos = consumeComments(input, pos, callbacks); // Consume the next input code point. pos++; pos = consumeAToken(input, pos, callbacks); if (needStop) { break; } } return result; }; /** * @param {string} input input * @param {number} pos position * @param {CssTokenCallbacks} cbs callbacks * @returns {[[number, number, number, number] | undefined, [number, number] | undefined, [number, number] | undefined, [number, number] | undefined]} positions of top level tokens */ module.exports.eatImportTokens = (input, pos, cbs) => { const result = /** @type {[[number, number, number, number] | undefined, [number, number] | undefined, [number, number] | undefined, [number, number] | undefined]} */ (new Array(4)); /** @type {0 | 1 | 2 | undefined} */ let scope; let needStop = false; let balanced = 0; /** @type {CssTokenCallbacks} */ const callbacks = { ...cbs, url: (_input, start, end, contentStart, contentEnd) => { if ( result[0] === undefined && balanced === 0 && result[1] === undefined && result[2] === undefined && result[3] === undefined ) { result[0] = [start, end, contentStart, contentEnd]; scope = undefined; } return end; }, string: (_input, start, end) => { if ( balanced === 0 && result[0] === undefined && result[1] === undefined && result[2] === undefined && result[3] === undefined ) { result[0] = [start, end, start + 1, end - 1]; scope = undefined; } else if (result[0] !== undefined && scope === 0) { result[0][2] = start + 1; result[0][3] = end - 1; } return end; }, leftParenthesis: (_input, _start, end) => { balanced++; return end; }, rightParenthesis: (_input, _start, end) => { balanced--; if (balanced === 0 && scope !== undefined) { /** @type {[number, number]} */ (result[scope])[1] = end; scope = undefined; } return end; }, function: (input, start, end) => { if (balanced === 0) { const name = input .slice(start, end - 1) .replace(/\\/g, "") .toLowerCase(); if ( name === "url" && result[0] === undefined && result[1] === undefined && result[2] === undefined && result[3] === undefined ) { scope = 0; result[scope] = [start, end + 1, end + 1, end + 1]; } else if ( name === "layer" && result[1] === undefined && result[2] === undefined ) { scope = 1; result[scope] = [start, end]; } else if (name === "supports" && result[2] === undefined) { scope = 2; result[scope] = [start, end]; } else { scope = undefined; } } balanced++; return end; }, identifier: (input, start, end) => { if ( balanced === 0 && result[1] === undefined && result[2] === undefined ) { const name = input.slice(start, end).replace(/\\/g, "").toLowerCase(); if (name === "layer") { result[1] = [start, end]; scope = undefined; } } return end; }, semicolon: (_input, start, end) => { if (balanced === 0) { needStop = true; result[3] = [start, end]; } return end; } }; while (pos < input.length) { // Consume comments. pos = consumeComments(input, pos, callbacks); // Consume the next input code point. pos++; pos = consumeAToken(input, pos, callbacks); if (needStop) { break; } } return result; }; /** * @param {string} input input * @param {number} pos position * @returns {[number, number] | undefined} positions of ident sequence */ module.exports.eatIdentSequence = (input, pos) => { pos = module.exports.eatWhitespaceAndComments(input, pos); const start = pos; if ( _ifThreeCodePointsWouldStartAnIdentSequence( input, pos, input.charCodeAt(pos), input.charCodeAt(pos + 1), input.charCodeAt(pos + 2) ) ) { return [start, _consumeAnIdentSequence(input, pos, {})]; } return undefined; }; /** * @param {string} input input * @param {number} pos position * @returns {[number, number, boolean] | undefined} positions of ident sequence or string */ module.exports.eatIdentSequenceOrString = (input, pos) => { pos = module.exports.eatWhitespaceAndComments(input, pos); const start = pos; if ( input.charCodeAt(pos) === CC_QUOTATION_MARK || input.charCodeAt(pos) === CC_APOSTROPHE ) { return [start, consumeAStringToken(input, pos + 1, {}), false]; } else if ( _ifThreeCodePointsWouldStartAnIdentSequence( input, pos, input.charCodeAt(pos), input.charCodeAt(pos + 1), input.charCodeAt(pos + 2) ) ) { return [start, _consumeAnIdentSequence(input, pos, {}), true]; } return undefined; }; /** * @param {string} chars characters * @returns {(input: string, pos: number) => number} function to eat characters */ module.exports.eatUntil = chars => { const charCodes = Array.from({ length: chars.length }, (_, i) => chars.charCodeAt(i) ); const arr = Array.from( { length: charCodes.reduce((a, b) => Math.max(a, b), 0) + 1 }, () => false ); for (const cc of charCodes) { arr[cc] = true; } return (input, pos) => { for (;;) { const cc = input.charCodeAt(pos); if (cc < arr.length && arr[cc]) { return pos; } pos++; if (pos === input.length) return pos; } }; };