From 928f7c8766aafcef4a0d3c335e08ea95ddcf1f7f Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Tue, 18 Oct 2022 15:44:48 -0400 Subject: [PATCH] chore: port css parser to ts (#18134) --- .../src/server/isomorphic/cssParser.ts | 12 +- .../src/server/isomorphic/cssTokenizer.d.ts | 54 - .../src/server/isomorphic/cssTokenizer.js | 939 ----------------- .../src/server/isomorphic/cssTokenizer.ts | 945 ++++++++++++++++++ 4 files changed, 951 insertions(+), 999 deletions(-) delete mode 100644 packages/playwright-core/src/server/isomorphic/cssTokenizer.d.ts delete mode 100644 packages/playwright-core/src/server/isomorphic/cssTokenizer.js create mode 100644 packages/playwright-core/src/server/isomorphic/cssTokenizer.ts diff --git a/packages/playwright-core/src/server/isomorphic/cssParser.ts b/packages/playwright-core/src/server/isomorphic/cssParser.ts index 976e5bb509..d5fb882310 100644 --- a/packages/playwright-core/src/server/isomorphic/cssParser.ts +++ b/packages/playwright-core/src/server/isomorphic/cssParser.ts @@ -111,7 +111,7 @@ export function parseCSS(selector: string, customNames: Set): { selector } function isClauseCombinator(p = pos) { - return (tokens[p] instanceof css.DelimToken) && (['>', '+', '~'].includes(tokens[p].value)); + return (tokens[p] instanceof css.DelimToken) && (['>', '+', '~'].includes(tokens[p].value as string)); } function isSelectorClauseEnd(p = pos) { @@ -133,9 +133,9 @@ export function parseCSS(selector: string, customNames: Set): { selector function consumeArgument(): CSSFunctionArgument { skipWhitespace(); if (isNumber()) - return tokens[pos++].value; + return tokens[pos++].value!; if (isString()) - return tokens[pos++].value; + return tokens[pos++].value!; return consumeComplexSelector(); } @@ -179,15 +179,15 @@ export function parseCSS(selector: string, customNames: Set): { selector } else if (tokens[pos] instanceof css.ColonToken) { pos++; if (isIdent()) { - if (!customNames.has(tokens[pos].value.toLowerCase())) { + if (!customNames.has((tokens[pos].value as string).toLowerCase())) { rawCSSString += ':' + tokens[pos++].toSource(); } else { - const name = tokens[pos++].value.toLowerCase(); + const name = (tokens[pos++].value as string).toLowerCase(); functions.push({ name, args: [] }); names.add(name); } } else if (tokens[pos] instanceof css.FunctionToken) { - const name = tokens[pos++].value.toLowerCase(); + const name = (tokens[pos++].value as string).toLowerCase(); if (!customNames.has(name)) { rawCSSString += `:${name}(${consumeBuiltinFunctionArguments()})`; } else { diff --git a/packages/playwright-core/src/server/isomorphic/cssTokenizer.d.ts b/packages/playwright-core/src/server/isomorphic/cssTokenizer.d.ts deleted file mode 100644 index 1678e3a24b..0000000000 --- a/packages/playwright-core/src/server/isomorphic/cssTokenizer.d.ts +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright (c) Microsoft Corporation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -export class CSSTokenInterface { - toSource(): string; - value: string; -} - -export function tokenize(selector: string): CSSTokenInterface[]; - -export class IdentToken extends CSSTokenInterface {} -export class FunctionToken extends CSSTokenInterface {} -export class AtKeywordToken extends CSSTokenInterface {} -export class HashToken extends CSSTokenInterface {} -export class StringToken extends CSSTokenInterface {} -export class BadStringToken extends CSSTokenInterface {} -export class URLToken extends CSSTokenInterface {} -export class BadURLToken extends CSSTokenInterface {} -export class DelimToken extends CSSTokenInterface {} -export class NumberToken extends CSSTokenInterface {} -export class PercentageToken extends CSSTokenInterface {} -export class DimensionToken extends CSSTokenInterface {} -export class IncludeMatchToken extends CSSTokenInterface {} -export class DashMatchToken extends CSSTokenInterface {} -export class PrefixMatchToken extends CSSTokenInterface {} -export class SuffixMatchToken extends CSSTokenInterface {} -export class SubstringMatchToken extends CSSTokenInterface {} -export class ColumnToken extends CSSTokenInterface {} -export class WhitespaceToken extends CSSTokenInterface {} -export class CDOToken extends CSSTokenInterface {} -export class CDCToken extends CSSTokenInterface {} -export class ColonToken extends CSSTokenInterface {} -export class SemicolonToken extends CSSTokenInterface {} -export class CommaToken extends CSSTokenInterface {} -export class OpenParenToken extends CSSTokenInterface {} -export class CloseParenToken extends CSSTokenInterface {} -export class OpenSquareToken extends CSSTokenInterface {} -export class CloseSquareToken extends CSSTokenInterface {} -export class OpenCurlyToken extends CSSTokenInterface {} -export class CloseCurlyToken extends CSSTokenInterface {} -export class EOFToken extends CSSTokenInterface {} diff --git a/packages/playwright-core/src/server/isomorphic/cssTokenizer.js b/packages/playwright-core/src/server/isomorphic/cssTokenizer.js deleted file mode 100644 index 164a7262f0..0000000000 --- a/packages/playwright-core/src/server/isomorphic/cssTokenizer.js +++ /dev/null @@ -1,939 +0,0 @@ -/* - * Original at https://github.com/tabatkins/parse-css - * licensed under http://creativecommons.org/publicdomain/zero/1.0/ - * - * Modifications copyright (c) Microsoft Corporation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -// Changes from https://github.com/tabatkins/parse-css -// - Tabs are replaced with two spaces. -// - Universal Module Definition wrapper is removed. -// - Everything not related to tokenizing - below the first exports block - is removed. - -// @ts-nocheck - - -var between = function (num, first, last) { return num >= first && num <= last; } -function digit(code) { return between(code, 0x30,0x39); } -function hexdigit(code) { return digit(code) || between(code, 0x41,0x46) || between(code, 0x61,0x66); } -function uppercaseletter(code) { return between(code, 0x41,0x5a); } -function lowercaseletter(code) { return between(code, 0x61,0x7a); } -function letter(code) { return uppercaseletter(code) || lowercaseletter(code); } -function nonascii(code) { return code >= 0x80; } -function namestartchar(code) { return letter(code) || nonascii(code) || code == 0x5f; } -function namechar(code) { return namestartchar(code) || digit(code) || code == 0x2d; } -function nonprintable(code) { return between(code, 0,8) || code == 0xb || between(code, 0xe,0x1f) || code == 0x7f; } -function newline(code) { return code == 0xa; } -function whitespace(code) { return newline(code) || code == 9 || code == 0x20; } -function badescape(code) { return newline(code) || isNaN(code); } - -var maximumallowedcodepoint = 0x10ffff; - -var InvalidCharacterError = function(message) { - this.message = message; -}; -InvalidCharacterError.prototype = new Error; -InvalidCharacterError.prototype.name = 'InvalidCharacterError'; - -function preprocess(str) { - // Turn a string into an array of code points, - // following the preprocessing cleanup rules. - var codepoints = []; - for(var i = 0; i < str.length; i++) { - var code = str.charCodeAt(i); - if(code == 0xd && str.charCodeAt(i+1) == 0xa) { - code = 0xa; i++; - } - if(code == 0xd || code == 0xc) code = 0xa; - if(code == 0x0) code = 0xfffd; - if(between(code, 0xd800, 0xdbff) && between(str.charCodeAt(i+1), 0xdc00, 0xdfff)) { - // Decode a surrogate pair into an astral codepoint. - var lead = code - 0xd800; - var trail = str.charCodeAt(i+1) - 0xdc00; - code = Math.pow(2, 16) + lead * Math.pow(2, 10) + trail; - i++; - } - codepoints.push(code); - } - return codepoints; -} - -function stringFromCode(code) { - if(code <= 0xffff) return String.fromCharCode(code); - // Otherwise, encode astral char as surrogate pair. - code -= Math.pow(2, 16); - var lead = Math.floor(code/Math.pow(2, 10)) + 0xd800; - var trail = code % Math.pow(2, 10) + 0xdc00; - return String.fromCharCode(lead) + String.fromCharCode(trail); -} - -function tokenize(str) { - str = preprocess(str); - var i = -1; - var tokens = []; - var code; - - // Line number information. - var line = 0; - var column = 0; - // The only use of lastLineLength is in reconsume(). - var lastLineLength = 0; - var incrLineno = function() { - line += 1; - lastLineLength = column; - column = 0; - }; - var locStart = {line:line, column:column}; - - var codepoint = function(i) { - if(i >= str.length) { - return -1; - } - return str[i]; - } - var next = function(num) { - if(num === undefined) - num = 1; - if(num > 3) - throw "Spec Error: no more than three codepoints of lookahead."; - return codepoint(i+num); - }; - var consume = function(num) { - if(num === undefined) - num = 1; - i += num; - code = codepoint(i); - if(newline(code)) incrLineno(); - else column += num; - //console.log('Consume '+i+' '+String.fromCharCode(code) + ' 0x' + code.toString(16)); - return true; - }; - var reconsume = function() { - i -= 1; - if (newline(code)) { - line -= 1; - column = lastLineLength; - } else { - column -= 1; - } - locStart.line = line; - locStart.column = column; - return true; - }; - var eof = function(codepoint) { - if(codepoint === undefined) codepoint = code; - return codepoint == -1; - }; - var donothing = function() {}; - var parseerror = function() { console.log("Parse error at index " + i + ", processing codepoint 0x" + code.toString(16) + ".");return true; }; - - var consumeAToken = function() { - consumeComments(); - consume(); - if(whitespace(code)) { - while(whitespace(next())) consume(); - return new WhitespaceToken; - } - else if(code == 0x22) return consumeAStringToken(); - else if(code == 0x23) { - if(namechar(next()) || areAValidEscape(next(1), next(2))) { - var token = new HashToken(); - if(wouldStartAnIdentifier(next(1), next(2), next(3))) token.type = "id"; - token.value = consumeAName(); - return token; - } else { - return new DelimToken(code); - } - } - else if(code == 0x24) { - if(next() == 0x3d) { - consume(); - return new SuffixMatchToken(); - } else { - return new DelimToken(code); - } - } - else if(code == 0x27) return consumeAStringToken(); - else if(code == 0x28) return new OpenParenToken(); - else if(code == 0x29) return new CloseParenToken(); - else if(code == 0x2a) { - if(next() == 0x3d) { - consume(); - return new SubstringMatchToken(); - } else { - return new DelimToken(code); - } - } - else if(code == 0x2b) { - if(startsWithANumber()) { - reconsume(); - return consumeANumericToken(); - } else { - return new DelimToken(code); - } - } - else if(code == 0x2c) return new CommaToken(); - else if(code == 0x2d) { - if(startsWithANumber()) { - reconsume(); - return consumeANumericToken(); - } else if(next(1) == 0x2d && next(2) == 0x3e) { - consume(2); - return new CDCToken(); - } else if(startsWithAnIdentifier()) { - reconsume(); - return consumeAnIdentlikeToken(); - } else { - return new DelimToken(code); - } - } - else if(code == 0x2e) { - if(startsWithANumber()) { - reconsume(); - return consumeANumericToken(); - } else { - return new DelimToken(code); - } - } - else if(code == 0x3a) return new ColonToken; - else if(code == 0x3b) return new SemicolonToken; - else if(code == 0x3c) { - if(next(1) == 0x21 && next(2) == 0x2d && next(3) == 0x2d) { - consume(3); - return new CDOToken(); - } else { - return new DelimToken(code); - } - } - else if(code == 0x40) { - if(wouldStartAnIdentifier(next(1), next(2), next(3))) { - return new AtKeywordToken(consumeAName()); - } else { - return new DelimToken(code); - } - } - else if(code == 0x5b) return new OpenSquareToken(); - else if(code == 0x5c) { - if(startsWithAValidEscape()) { - reconsume(); - return consumeAnIdentlikeToken(); - } else { - parseerror(); - return new DelimToken(code); - } - } - else if(code == 0x5d) return new CloseSquareToken(); - else if(code == 0x5e) { - if(next() == 0x3d) { - consume(); - return new PrefixMatchToken(); - } else { - return new DelimToken(code); - } - } - else if(code == 0x7b) return new OpenCurlyToken(); - else if(code == 0x7c) { - if(next() == 0x3d) { - consume(); - return new DashMatchToken(); - } else if(next() == 0x7c) { - consume(); - return new ColumnToken(); - } else { - return new DelimToken(code); - } - } - else if(code == 0x7d) return new CloseCurlyToken(); - else if(code == 0x7e) { - if(next() == 0x3d) { - consume(); - return new IncludeMatchToken(); - } else { - return new DelimToken(code); - } - } - else if(digit(code)) { - reconsume(); - return consumeANumericToken(); - } - else if(namestartchar(code)) { - reconsume(); - return consumeAnIdentlikeToken(); - } - else if(eof()) return new EOFToken(); - else return new DelimToken(code); - }; - - var consumeComments = function() { - while(next(1) == 0x2f && next(2) == 0x2a) { - consume(2); - while(true) { - consume(); - if(code == 0x2a && next() == 0x2f) { - consume(); - break; - } else if(eof()) { - parseerror(); - return; - } - } - } - }; - - var consumeANumericToken = function() { - var num = consumeANumber(); - if(wouldStartAnIdentifier(next(1), next(2), next(3))) { - var token = new DimensionToken(); - token.value = num.value; - token.repr = num.repr; - token.type = num.type; - token.unit = consumeAName(); - return token; - } else if(next() == 0x25) { - consume(); - var token = new PercentageToken(); - token.value = num.value; - token.repr = num.repr; - return token; - } else { - var token = new NumberToken(); - token.value = num.value; - token.repr = num.repr; - token.type = num.type; - return token; - } - }; - - var consumeAnIdentlikeToken = function() { - var str = consumeAName(); - if(str.toLowerCase() == "url" && next() == 0x28) { - consume(); - while(whitespace(next(1)) && whitespace(next(2))) consume(); - if(next() == 0x22 || next() == 0x27) { - return new FunctionToken(str); - } else if(whitespace(next()) && (next(2) == 0x22 || next(2) == 0x27)) { - return new FunctionToken(str); - } else { - return consumeAURLToken(); - } - } else if(next() == 0x28) { - consume(); - return new FunctionToken(str); - } else { - return new IdentToken(str); - } - }; - - var consumeAStringToken = function(endingCodePoint) { - if(endingCodePoint === undefined) endingCodePoint = code; - var string = ""; - while(consume()) { - if(code == endingCodePoint || eof()) { - return new StringToken(string); - } else if(newline(code)) { - parseerror(); - reconsume(); - return new BadStringToken(); - } else if(code == 0x5c) { - if(eof(next())) { - donothing(); - } else if(newline(next())) { - consume(); - } else { - string += stringFromCode(consumeEscape()) - } - } else { - string += stringFromCode(code); - } - } - }; - - var consumeAURLToken = function() { - var token = new URLToken(""); - while(whitespace(next())) consume(); - if(eof(next())) return token; - while(consume()) { - if(code == 0x29 || eof()) { - return token; - } else if(whitespace(code)) { - while(whitespace(next())) consume(); - if(next() == 0x29 || eof(next())) { - consume(); - return token; - } else { - consumeTheRemnantsOfABadURL(); - return new BadURLToken(); - } - } else if(code == 0x22 || code == 0x27 || code == 0x28 || nonprintable(code)) { - parseerror(); - consumeTheRemnantsOfABadURL(); - return new BadURLToken(); - } else if(code == 0x5c) { - if(startsWithAValidEscape()) { - token.value += stringFromCode(consumeEscape()); - } else { - parseerror(); - consumeTheRemnantsOfABadURL(); - return new BadURLToken(); - } - } else { - token.value += stringFromCode(code); - } - } - }; - - var consumeEscape = function() { - // Assume the the current character is the \ - // and the next code point is not a newline. - consume(); - if(hexdigit(code)) { - // Consume 1-6 hex digits - var digits = [code]; - for(var total = 0; total < 5; total++) { - if(hexdigit(next())) { - consume(); - digits.push(code); - } else { - break; - } - } - if(whitespace(next())) consume(); - var value = parseInt(digits.map(function(x){return String.fromCharCode(x);}).join(''), 16); - if( value > maximumallowedcodepoint ) value = 0xfffd; - return value; - } else if(eof()) { - return 0xfffd; - } else { - return code; - } - }; - - var areAValidEscape = function(c1, c2) { - if(c1 != 0x5c) return false; - if(newline(c2)) return false; - return true; - }; - var startsWithAValidEscape = function() { - return areAValidEscape(code, next()); - }; - - var wouldStartAnIdentifier = function(c1, c2, c3) { - if(c1 == 0x2d) { - return namestartchar(c2) || c2 == 0x2d || areAValidEscape(c2, c3); - } else if(namestartchar(c1)) { - return true; - } else if(c1 == 0x5c) { - return areAValidEscape(c1, c2); - } else { - return false; - } - }; - var startsWithAnIdentifier = function() { - return wouldStartAnIdentifier(code, next(1), next(2)); - }; - - var wouldStartANumber = function(c1, c2, c3) { - if(c1 == 0x2b || c1 == 0x2d) { - if(digit(c2)) return true; - if(c2 == 0x2e && digit(c3)) return true; - return false; - } else if(c1 == 0x2e) { - if(digit(c2)) return true; - return false; - } else if(digit(c1)) { - return true; - } else { - return false; - } - }; - var startsWithANumber = function() { - return wouldStartANumber(code, next(1), next(2)); - }; - - var consumeAName = function() { - var result = ""; - while(consume()) { - if(namechar(code)) { - result += stringFromCode(code); - } else if(startsWithAValidEscape()) { - result += stringFromCode(consumeEscape()); - } else { - reconsume(); - return result; - } - } - }; - - var consumeANumber = function() { - var repr = []; - var type = "integer"; - if(next() == 0x2b || next() == 0x2d) { - consume(); - repr += stringFromCode(code); - } - while(digit(next())) { - consume(); - repr += stringFromCode(code); - } - if(next(1) == 0x2e && digit(next(2))) { - consume(); - repr += stringFromCode(code); - consume(); - repr += stringFromCode(code); - type = "number"; - while(digit(next())) { - consume(); - repr += stringFromCode(code); - } - } - var c1 = next(1), c2 = next(2), c3 = next(3); - if((c1 == 0x45 || c1 == 0x65) && digit(c2)) { - consume(); - repr += stringFromCode(code); - consume(); - repr += stringFromCode(code); - type = "number"; - while(digit(next())) { - consume(); - repr += stringFromCode(code); - } - } else if((c1 == 0x45 || c1 == 0x65) && (c2 == 0x2b || c2 == 0x2d) && digit(c3)) { - consume(); - repr += stringFromCode(code); - consume(); - repr += stringFromCode(code); - consume(); - repr += stringFromCode(code); - type = "number"; - while(digit(next())) { - consume(); - repr += stringFromCode(code); - } - } - var value = convertAStringToANumber(repr); - return {type:type, value:value, repr:repr}; - }; - - var convertAStringToANumber = function(string) { - // CSS's number rules are identical to JS, afaik. - return +string; - }; - - var consumeTheRemnantsOfABadURL = function() { - while(consume()) { - if(code == 0x29 || eof()) { - return; - } else if(startsWithAValidEscape()) { - consumeEscape(); - donothing(); - } else { - donothing(); - } - } - }; - - - - var iterationCount = 0; - while(!eof(next())) { - tokens.push(consumeAToken()); - iterationCount++; - if(iterationCount > str.length*2) return "I'm infinite-looping!"; - } - return tokens; -} - -function CSSParserToken() { throw "Abstract Base Class"; } -CSSParserToken.prototype.toJSON = function() { - return {token: this.tokenType}; -} -CSSParserToken.prototype.toString = function() { return this.tokenType; } -CSSParserToken.prototype.toSource = function() { return ''+this; } - -function BadStringToken() { return this; } -BadStringToken.prototype = Object.create(CSSParserToken.prototype); -BadStringToken.prototype.tokenType = "BADSTRING"; - -function BadURLToken() { return this; } -BadURLToken.prototype = Object.create(CSSParserToken.prototype); -BadURLToken.prototype.tokenType = "BADURL"; - -function WhitespaceToken() { return this; } -WhitespaceToken.prototype = Object.create(CSSParserToken.prototype); -WhitespaceToken.prototype.tokenType = "WHITESPACE"; -WhitespaceToken.prototype.toString = function() { return "WS"; } -WhitespaceToken.prototype.toSource = function() { return " "; } - -function CDOToken() { return this; } -CDOToken.prototype = Object.create(CSSParserToken.prototype); -CDOToken.prototype.tokenType = "CDO"; -CDOToken.prototype.toSource = function() { return ""; } - -function ColonToken() { return this; } -ColonToken.prototype = Object.create(CSSParserToken.prototype); -ColonToken.prototype.tokenType = ":"; - -function SemicolonToken() { return this; } -SemicolonToken.prototype = Object.create(CSSParserToken.prototype); -SemicolonToken.prototype.tokenType = ";"; - -function CommaToken() { return this; } -CommaToken.prototype = Object.create(CSSParserToken.prototype); -CommaToken.prototype.tokenType = ","; - -function GroupingToken() { throw "Abstract Base Class"; } -GroupingToken.prototype = Object.create(CSSParserToken.prototype); - -function OpenCurlyToken() { this.value = "{"; this.mirror = "}"; return this; } -OpenCurlyToken.prototype = Object.create(GroupingToken.prototype); -OpenCurlyToken.prototype.tokenType = "{"; - -function CloseCurlyToken() { this.value = "}"; this.mirror = "{"; return this; } -CloseCurlyToken.prototype = Object.create(GroupingToken.prototype); -CloseCurlyToken.prototype.tokenType = "}"; - -function OpenSquareToken() { this.value = "["; this.mirror = "]"; return this; } -OpenSquareToken.prototype = Object.create(GroupingToken.prototype); -OpenSquareToken.prototype.tokenType = "["; - -function CloseSquareToken() { this.value = "]"; this.mirror = "["; return this; } -CloseSquareToken.prototype = Object.create(GroupingToken.prototype); -CloseSquareToken.prototype.tokenType = "]"; - -function OpenParenToken() { this.value = "("; this.mirror = ")"; return this; } -OpenParenToken.prototype = Object.create(GroupingToken.prototype); -OpenParenToken.prototype.tokenType = "("; - -function CloseParenToken() { this.value = ")"; this.mirror = "("; return this; } -CloseParenToken.prototype = Object.create(GroupingToken.prototype); -CloseParenToken.prototype.tokenType = ")"; - -function IncludeMatchToken() { return this; } -IncludeMatchToken.prototype = Object.create(CSSParserToken.prototype); -IncludeMatchToken.prototype.tokenType = "~="; - -function DashMatchToken() { return this; } -DashMatchToken.prototype = Object.create(CSSParserToken.prototype); -DashMatchToken.prototype.tokenType = "|="; - -function PrefixMatchToken() { return this; } -PrefixMatchToken.prototype = Object.create(CSSParserToken.prototype); -PrefixMatchToken.prototype.tokenType = "^="; - -function SuffixMatchToken() { return this; } -SuffixMatchToken.prototype = Object.create(CSSParserToken.prototype); -SuffixMatchToken.prototype.tokenType = "$="; - -function SubstringMatchToken() { return this; } -SubstringMatchToken.prototype = Object.create(CSSParserToken.prototype); -SubstringMatchToken.prototype.tokenType = "*="; - -function ColumnToken() { return this; } -ColumnToken.prototype = Object.create(CSSParserToken.prototype); -ColumnToken.prototype.tokenType = "||"; - -function EOFToken() { return this; } -EOFToken.prototype = Object.create(CSSParserToken.prototype); -EOFToken.prototype.tokenType = "EOF"; -EOFToken.prototype.toSource = function() { return ""; } - -function DelimToken(code) { - this.value = stringFromCode(code); - return this; -} -DelimToken.prototype = Object.create(CSSParserToken.prototype); -DelimToken.prototype.tokenType = "DELIM"; -DelimToken.prototype.toString = function() { return "DELIM("+this.value+")"; } -DelimToken.prototype.toJSON = function() { - var json = this.constructor.prototype.constructor.prototype.toJSON.call(this); - json.value = this.value; - return json; -} -DelimToken.prototype.toSource = function() { - if(this.value == "\\") - return "\\\n"; - else - return this.value; -} - -function StringValuedToken() { throw "Abstract Base Class"; } -StringValuedToken.prototype = Object.create(CSSParserToken.prototype); -StringValuedToken.prototype.ASCIIMatch = function(str) { - return this.value.toLowerCase() == str.toLowerCase(); -} -StringValuedToken.prototype.toJSON = function() { - var json = this.constructor.prototype.constructor.prototype.toJSON.call(this); - json.value = this.value; - return json; -} - -function IdentToken(val) { - this.value = val; -} -IdentToken.prototype = Object.create(StringValuedToken.prototype); -IdentToken.prototype.tokenType = "IDENT"; -IdentToken.prototype.toString = function() { return "IDENT("+this.value+")"; } -IdentToken.prototype.toSource = function() { - return escapeIdent(this.value); -} - -function FunctionToken(val) { - this.value = val; - this.mirror = ")"; -} -FunctionToken.prototype = Object.create(StringValuedToken.prototype); -FunctionToken.prototype.tokenType = "FUNCTION"; -FunctionToken.prototype.toString = function() { return "FUNCTION("+this.value+")"; } -FunctionToken.prototype.toSource = function() { - return escapeIdent(this.value) + "("; -} - -function AtKeywordToken(val) { - this.value = val; -} -AtKeywordToken.prototype = Object.create(StringValuedToken.prototype); -AtKeywordToken.prototype.tokenType = "AT-KEYWORD"; -AtKeywordToken.prototype.toString = function() { return "AT("+this.value+")"; } -AtKeywordToken.prototype.toSource = function() { - return "@" + escapeIdent(this.value); -} - -function HashToken(val) { - this.value = val; - this.type = "unrestricted"; -} -HashToken.prototype = Object.create(StringValuedToken.prototype); -HashToken.prototype.tokenType = "HASH"; -HashToken.prototype.toString = function() { return "HASH("+this.value+")"; } -HashToken.prototype.toJSON = function() { - var json = this.constructor.prototype.constructor.prototype.toJSON.call(this); - json.value = this.value; - json.type = this.type; - return json; -} -HashToken.prototype.toSource = function() { - if(this.type == "id") { - return "#" + escapeIdent(this.value); - } else { - return "#" + escapeHash(this.value); - } -} - -function StringToken(val) { - this.value = val; -} -StringToken.prototype = Object.create(StringValuedToken.prototype); -StringToken.prototype.tokenType = "STRING"; -StringToken.prototype.toString = function() { - return '"' + escapeString(this.value) + '"'; -} - -function URLToken(val) { - this.value = val; -} -URLToken.prototype = Object.create(StringValuedToken.prototype); -URLToken.prototype.tokenType = "URL"; -URLToken.prototype.toString = function() { return "URL("+this.value+")"; } -URLToken.prototype.toSource = function() { - return 'url("' + escapeString(this.value) + '")'; -} - -function NumberToken() { - this.value = null; - this.type = "integer"; - this.repr = ""; -} -NumberToken.prototype = Object.create(CSSParserToken.prototype); -NumberToken.prototype.tokenType = "NUMBER"; -NumberToken.prototype.toString = function() { - if(this.type == "integer") - return "INT("+this.value+")"; - return "NUMBER("+this.value+")"; -} -NumberToken.prototype.toJSON = function() { - var json = this.constructor.prototype.constructor.prototype.toJSON.call(this); - json.value = this.value; - json.type = this.type; - json.repr = this.repr; - return json; -} -NumberToken.prototype.toSource = function() { return this.repr; }; - -function PercentageToken() { - this.value = null; - this.repr = ""; -} -PercentageToken.prototype = Object.create(CSSParserToken.prototype); -PercentageToken.prototype.tokenType = "PERCENTAGE"; -PercentageToken.prototype.toString = function() { return "PERCENTAGE("+this.value+")"; } -PercentageToken.prototype.toJSON = function() { - var json = this.constructor.prototype.constructor.prototype.toJSON.call(this); - json.value = this.value; - json.repr = this.repr; - return json; -} -PercentageToken.prototype.toSource = function() { return this.repr + "%"; } - -function DimensionToken() { - this.value = null; - this.type = "integer"; - this.repr = ""; - this.unit = ""; -} -DimensionToken.prototype = Object.create(CSSParserToken.prototype); -DimensionToken.prototype.tokenType = "DIMENSION"; -DimensionToken.prototype.toString = function() { return "DIM("+this.value+","+this.unit+")"; } -DimensionToken.prototype.toJSON = function() { - var json = this.constructor.prototype.constructor.prototype.toJSON.call(this); - json.value = this.value; - json.type = this.type; - json.repr = this.repr; - json.unit = this.unit; - return json; -} -DimensionToken.prototype.toSource = function() { - var source = this.repr; - var unit = escapeIdent(this.unit); - if(unit[0].toLowerCase() == "e" && (unit[1] == "-" || between(unit.charCodeAt(1), 0x30, 0x39))) { - // Unit is ambiguous with scinot - // Remove the leading "e", replace with escape. - unit = "\\65 " + unit.slice(1, unit.length); - } - return source+unit; -} - -function escapeIdent(string) { - string = ''+string; - var result = ''; - var firstcode = string.charCodeAt(0); - for(var i = 0; i < string.length; i++) { - var code = string.charCodeAt(i); - if(code == 0x0) { - throw new InvalidCharacterError('Invalid character: the input contains U+0000.'); - } - - if( - between(code, 0x1, 0x1f) || code == 0x7f || - (i == 0 && between(code, 0x30, 0x39)) || - (i == 1 && between(code, 0x30, 0x39) && firstcode == 0x2d) - ) { - result += '\\' + code.toString(16) + ' '; - } else if( - code >= 0x80 || - code == 0x2d || - code == 0x5f || - between(code, 0x30, 0x39) || - between(code, 0x41, 0x5a) || - between(code, 0x61, 0x7a) - ) { - result += string[i]; - } else { - result += '\\' + string[i]; - } - } - return result; -} - -function escapeHash(string) { - // Escapes the contents of "unrestricted"-type hash tokens. - // Won't preserve the ID-ness of "id"-type hash tokens; - // use escapeIdent() for that. - string = ''+string; - var result = ''; - var firstcode = string.charCodeAt(0); - for(var i = 0; i < string.length; i++) { - var code = string.charCodeAt(i); - if(code == 0x0) { - throw new InvalidCharacterError('Invalid character: the input contains U+0000.'); - } - - if( - code >= 0x80 || - code == 0x2d || - code == 0x5f || - between(code, 0x30, 0x39) || - between(code, 0x41, 0x5a) || - between(code, 0x61, 0x7a) - ) { - result += string[i]; - } else { - result += '\\' + code.toString(16) + ' '; - } - } - return result; -} - -function escapeString(string) { - string = ''+string; - var result = ''; - for(var i = 0; i < string.length; i++) { - var code = string.charCodeAt(i); - - if(code == 0x0) { - throw new InvalidCharacterError('Invalid character: the input contains U+0000.'); - } - - if(between(code, 0x1, 0x1f) || code == 0x7f) { - result += '\\' + code.toString(16) + ' '; - } else if(code == 0x22 || code == 0x5c) { - result += '\\' + string[i]; - } else { - result += string[i]; - } - } - return result; -} - -// Exportation. -exports.tokenize = tokenize; -exports.IdentToken = IdentToken; -exports.FunctionToken = FunctionToken; -exports.AtKeywordToken = AtKeywordToken; -exports.HashToken = HashToken; -exports.StringToken = StringToken; -exports.BadStringToken = BadStringToken; -exports.URLToken = URLToken; -exports.BadURLToken = BadURLToken; -exports.DelimToken = DelimToken; -exports.NumberToken = NumberToken; -exports.PercentageToken = PercentageToken; -exports.DimensionToken = DimensionToken; -exports.IncludeMatchToken = IncludeMatchToken; -exports.DashMatchToken = DashMatchToken; -exports.PrefixMatchToken = PrefixMatchToken; -exports.SuffixMatchToken = SuffixMatchToken; -exports.SubstringMatchToken = SubstringMatchToken; -exports.ColumnToken = ColumnToken; -exports.WhitespaceToken = WhitespaceToken; -exports.CDOToken = CDOToken; -exports.CDCToken = CDCToken; -exports.ColonToken = ColonToken; -exports.SemicolonToken = SemicolonToken; -exports.CommaToken = CommaToken; -exports.OpenParenToken = OpenParenToken; -exports.CloseParenToken = CloseParenToken; -exports.OpenSquareToken = OpenSquareToken; -exports.CloseSquareToken = CloseSquareToken; -exports.OpenCurlyToken = OpenCurlyToken; -exports.CloseCurlyToken = CloseCurlyToken; -exports.EOFToken = EOFToken; -exports.CSSParserToken = CSSParserToken; -exports.GroupingToken = GroupingToken; - diff --git a/packages/playwright-core/src/server/isomorphic/cssTokenizer.ts b/packages/playwright-core/src/server/isomorphic/cssTokenizer.ts new file mode 100644 index 0000000000..7d6d86f8b5 --- /dev/null +++ b/packages/playwright-core/src/server/isomorphic/cssTokenizer.ts @@ -0,0 +1,945 @@ +/* eslint-disable notice/notice */ + +/* + * The code in this file is licensed under the CC0 license. + * http://creativecommons.org/publicdomain/zero/1.0/ + * It is free to use for any purpose. No attribution, permission, or reproduction of this license is required. + */ + +// Original at https://github.com/tabatkins/parse-css +// Changes: +// - JS is replaced with TS. +// - Universal Module Definition wrapper is removed. +// - Everything not related to tokenizing - below the first exports block - is removed. + +export interface CSSTokenInterface { + toSource(): string; + value: string | number | undefined; +} + +const between = function(num: number, first: number, last: number) { return num >= first && num <= last; }; +function digit(code: number) { return between(code, 0x30, 0x39); } +function hexdigit(code: number) { return digit(code) || between(code, 0x41, 0x46) || between(code, 0x61, 0x66); } +function uppercaseletter(code: number) { return between(code, 0x41, 0x5a); } +function lowercaseletter(code: number) { return between(code, 0x61, 0x7a); } +function letter(code: number) { return uppercaseletter(code) || lowercaseletter(code); } +function nonascii(code: number) { return code >= 0x80; } +function namestartchar(code: number) { return letter(code) || nonascii(code) || code === 0x5f; } +function namechar(code: number) { return namestartchar(code) || digit(code) || code === 0x2d; } +function nonprintable(code: number) { return between(code, 0, 8) || code === 0xb || between(code, 0xe, 0x1f) || code === 0x7f; } +function newline(code: number) { return code === 0xa; } +function whitespace(code: number) { return newline(code) || code === 9 || code === 0x20; } + +const maximumallowedcodepoint = 0x10ffff; + +export class InvalidCharacterError extends Error { + constructor(message: string) { + super(message); + this.name = 'InvalidCharacterError'; + } +} + +function preprocess(str: string): number[] { + // Turn a string into an array of code points, + // following the preprocessing cleanup rules. + const codepoints = []; + for (let i = 0; i < str.length; i++) { + let code = str.charCodeAt(i); + if (code === 0xd && str.charCodeAt(i + 1) === 0xa) { + code = 0xa; i++; + } + if (code === 0xd || code === 0xc) code = 0xa; + if (code === 0x0) code = 0xfffd; + if (between(code, 0xd800, 0xdbff) && between(str.charCodeAt(i + 1), 0xdc00, 0xdfff)) { + // Decode a surrogate pair into an astral codepoint. + const lead = code - 0xd800; + const trail = str.charCodeAt(i + 1) - 0xdc00; + code = Math.pow(2, 16) + lead * Math.pow(2, 10) + trail; + i++; + } + codepoints.push(code); + } + return codepoints; +} + +function stringFromCode(code: number) { + if (code <= 0xffff) return String.fromCharCode(code); + // Otherwise, encode astral char as surrogate pair. + code -= Math.pow(2, 16); + const lead = Math.floor(code / Math.pow(2, 10)) + 0xd800; + const trail = code % Math.pow(2, 10) + 0xdc00; + return String.fromCharCode(lead) + String.fromCharCode(trail); +} + +export function tokenize(str1: string): CSSTokenInterface[] { + const str = preprocess(str1); + let i = -1; + const tokens: CSSTokenInterface[] = []; + let code: number; + + // Line number information. + let line = 0; + let column = 0; + // The only use of lastLineLength is in reconsume(). + let lastLineLength = 0; + const incrLineno = function() { + line += 1; + lastLineLength = column; + column = 0; + }; + const locStart = { line: line, column: column }; + + const codepoint = function(i: number): number { + if (i >= str.length) + return -1; + + return str[i]; + }; + const next = function(num?: number) { + if (num === undefined) + num = 1; + if (num > 3) + throw 'Spec Error: no more than three codepoints of lookahead.'; + return codepoint(i + num); + }; + const consume = function(num?: number): boolean { + if (num === undefined) + num = 1; + i += num; + code = codepoint(i); + if (newline(code)) incrLineno(); + else column += num; + // console.log('Consume '+i+' '+String.fromCharCode(code) + ' 0x' + code.toString(16)); + return true; + }; + const reconsume = function() { + i -= 1; + if (newline(code)) { + line -= 1; + column = lastLineLength; + } else { + column -= 1; + } + locStart.line = line; + locStart.column = column; + return true; + }; + const eof = function(codepoint?: number): boolean { + if (codepoint === undefined) codepoint = code; + return codepoint === -1; + }; + const donothing = function() { }; + const parseerror = function() { + // eslint-disable-next-line no-console + console.log('Parse error at index ' + i + ', processing codepoint 0x' + code.toString(16) + '.'); return true; + }; + + const consumeAToken = function(): CSSTokenInterface { + consumeComments(); + consume(); + if (whitespace(code)) { + while (whitespace(next())) consume(); + return new WhitespaceToken(); + } else if (code === 0x22) {return consumeAStringToken();} else if (code === 0x23) { + if (namechar(next()) || areAValidEscape(next(1), next(2))) { + const token = new HashToken(''); + if (wouldStartAnIdentifier(next(1), next(2), next(3))) token.type = 'id'; + token.value = consumeAName(); + return token; + } else { + return new DelimToken(code); + } + } else if (code === 0x24) { + if (next() === 0x3d) { + consume(); + return new SuffixMatchToken(); + } else { + return new DelimToken(code); + } + } else if (code === 0x27) {return consumeAStringToken();} else if (code === 0x28) {return new OpenParenToken();} else if (code === 0x29) {return new CloseParenToken();} else if (code === 0x2a) { + if (next() === 0x3d) { + consume(); + return new SubstringMatchToken(); + } else { + return new DelimToken(code); + } + } else if (code === 0x2b) { + if (startsWithANumber()) { + reconsume(); + return consumeANumericToken(); + } else { + return new DelimToken(code); + } + } else if (code === 0x2c) {return new CommaToken();} else if (code === 0x2d) { + if (startsWithANumber()) { + reconsume(); + return consumeANumericToken(); + } else if (next(1) === 0x2d && next(2) === 0x3e) { + consume(2); + return new CDCToken(); + } else if (startsWithAnIdentifier()) { + reconsume(); + return consumeAnIdentlikeToken(); + } else { + return new DelimToken(code); + } + } else if (code === 0x2e) { + if (startsWithANumber()) { + reconsume(); + return consumeANumericToken(); + } else { + return new DelimToken(code); + } + } else if (code === 0x3a) {return new ColonToken();} else if (code === 0x3b) {return new SemicolonToken();} else if (code === 0x3c) { + if (next(1) === 0x21 && next(2) === 0x2d && next(3) === 0x2d) { + consume(3); + return new CDOToken(); + } else { + return new DelimToken(code); + } + } else if (code === 0x40) { + if (wouldStartAnIdentifier(next(1), next(2), next(3))) + return new AtKeywordToken(consumeAName()); + else + return new DelimToken(code); + + } else if (code === 0x5b) {return new OpenSquareToken();} else if (code === 0x5c) { + if (startsWithAValidEscape()) { + reconsume(); + return consumeAnIdentlikeToken(); + } else { + parseerror(); + return new DelimToken(code); + } + } else if (code === 0x5d) {return new CloseSquareToken();} else if (code === 0x5e) { + if (next() === 0x3d) { + consume(); + return new PrefixMatchToken(); + } else { + return new DelimToken(code); + } + } else if (code === 0x7b) {return new OpenCurlyToken();} else if (code === 0x7c) { + if (next() === 0x3d) { + consume(); + return new DashMatchToken(); + } else if (next() === 0x7c) { + consume(); + return new ColumnToken(); + } else { + return new DelimToken(code); + } + } else if (code === 0x7d) {return new CloseCurlyToken();} else if (code === 0x7e) { + if (next() === 0x3d) { + consume(); + return new IncludeMatchToken(); + } else { + return new DelimToken(code); + } + } else if (digit(code)) { + reconsume(); + return consumeANumericToken(); + } else if (namestartchar(code)) { + reconsume(); + return consumeAnIdentlikeToken(); + } else if (eof()) {return new EOFToken();} else {return new DelimToken(code);} + }; + + const consumeComments = function() { + while (next(1) === 0x2f && next(2) === 0x2a) { + consume(2); + while (true) { + consume(); + if (code === 0x2a && next() === 0x2f) { + consume(); + break; + } else if (eof()) { + parseerror(); + return; + } + } + } + }; + + const consumeANumericToken = function() { + const num = consumeANumber(); + if (wouldStartAnIdentifier(next(1), next(2), next(3))) { + const token = new DimensionToken(); + token.value = num.value; + token.repr = num.repr; + token.type = num.type; + token.unit = consumeAName(); + return token; + } else if (next() === 0x25) { + consume(); + const token = new PercentageToken(); + token.value = num.value; + token.repr = num.repr; + return token; + } else { + const token = new NumberToken(); + token.value = num.value; + token.repr = num.repr; + token.type = num.type; + return token; + } + }; + + const consumeAnIdentlikeToken = function(): CSSTokenInterface { + const str = consumeAName(); + if (str.toLowerCase() === 'url' && next() === 0x28) { + consume(); + while (whitespace(next(1)) && whitespace(next(2))) consume(); + if (next() === 0x22 || next() === 0x27) + return new FunctionToken(str); + else if (whitespace(next()) && (next(2) === 0x22 || next(2) === 0x27)) + return new FunctionToken(str); + else + return consumeAURLToken(); + + } else if (next() === 0x28) { + consume(); + return new FunctionToken(str); + } else { + return new IdentToken(str); + } + }; + + const consumeAStringToken = function(endingCodePoint?: number): CSSParserToken { + if (endingCodePoint === undefined) endingCodePoint = code; + let string = ''; + while (consume()) { + if (code === endingCodePoint || eof()) { + return new StringToken(string); + } else if (newline(code)) { + parseerror(); + reconsume(); + return new BadStringToken(); + } else if (code === 0x5c) { + if (eof(next())) + donothing(); + else if (newline(next())) + consume(); + else + string += stringFromCode(consumeEscape()); + + } else { + string += stringFromCode(code); + } + } + throw new Error('Internal error'); + }; + + const consumeAURLToken = function(): CSSTokenInterface { + const token = new URLToken(''); + while (whitespace(next())) consume(); + if (eof(next())) return token; + while (consume()) { + if (code === 0x29 || eof()) { + return token; + } else if (whitespace(code)) { + while (whitespace(next())) consume(); + if (next() === 0x29 || eof(next())) { + consume(); + return token; + } else { + consumeTheRemnantsOfABadURL(); + return new BadURLToken(); + } + } else if (code === 0x22 || code === 0x27 || code === 0x28 || nonprintable(code)) { + parseerror(); + consumeTheRemnantsOfABadURL(); + return new BadURLToken(); + } else if (code === 0x5c) { + if (startsWithAValidEscape()) { + token.value += stringFromCode(consumeEscape()); + } else { + parseerror(); + consumeTheRemnantsOfABadURL(); + return new BadURLToken(); + } + } else { + token.value += stringFromCode(code); + } + } + throw new Error('Internal error'); + }; + + const consumeEscape = function() { + // Assume the the current character is the \ + // and the next code point is not a newline. + consume(); + if (hexdigit(code)) { + // Consume 1-6 hex digits + const digits = [code]; + for (let total = 0; total < 5; total++) { + if (hexdigit(next())) { + consume(); + digits.push(code); + } else { + break; + } + } + if (whitespace(next())) consume(); + let value = parseInt(digits.map(function(x) { return String.fromCharCode(x); }).join(''), 16); + if (value > maximumallowedcodepoint) value = 0xfffd; + return value; + } else if (eof()) { + return 0xfffd; + } else { + return code; + } + }; + + const areAValidEscape = function(c1: number, c2: number) { + if (c1 !== 0x5c) return false; + if (newline(c2)) return false; + return true; + }; + const startsWithAValidEscape = function() { + return areAValidEscape(code, next()); + }; + + const wouldStartAnIdentifier = function(c1: number, c2: number, c3: number) { + if (c1 === 0x2d) + return namestartchar(c2) || c2 === 0x2d || areAValidEscape(c2, c3); + else if (namestartchar(c1)) + return true; + else if (c1 === 0x5c) + return areAValidEscape(c1, c2); + else + return false; + + }; + const startsWithAnIdentifier = function() { + return wouldStartAnIdentifier(code, next(1), next(2)); + }; + + const wouldStartANumber = function(c1: number, c2: number, c3: number) { + if (c1 === 0x2b || c1 === 0x2d) { + if (digit(c2)) return true; + if (c2 === 0x2e && digit(c3)) return true; + return false; + } else if (c1 === 0x2e) { + if (digit(c2)) return true; + return false; + } else if (digit(c1)) { + return true; + } else { + return false; + } + }; + const startsWithANumber = function() { + return wouldStartANumber(code, next(1), next(2)); + }; + + const consumeAName = function(): string { + let result = ''; + while (consume()) { + if (namechar(code)) { + result += stringFromCode(code); + } else if (startsWithAValidEscape()) { + result += stringFromCode(consumeEscape()); + } else { + reconsume(); + return result; + } + } + throw new Error('Internal parse error'); + }; + + const consumeANumber = function() { + let repr = ''; + let type = 'integer'; + if (next() === 0x2b || next() === 0x2d) { + consume(); + repr += stringFromCode(code); + } + while (digit(next())) { + consume(); + repr += stringFromCode(code); + } + if (next(1) === 0x2e && digit(next(2))) { + consume(); + repr += stringFromCode(code); + consume(); + repr += stringFromCode(code); + type = 'number'; + while (digit(next())) { + consume(); + repr += stringFromCode(code); + } + } + const c1 = next(1), c2 = next(2), c3 = next(3); + if ((c1 === 0x45 || c1 === 0x65) && digit(c2)) { + consume(); + repr += stringFromCode(code); + consume(); + repr += stringFromCode(code); + type = 'number'; + while (digit(next())) { + consume(); + repr += stringFromCode(code); + } + } else if ((c1 === 0x45 || c1 === 0x65) && (c2 === 0x2b || c2 === 0x2d) && digit(c3)) { + consume(); + repr += stringFromCode(code); + consume(); + repr += stringFromCode(code); + consume(); + repr += stringFromCode(code); + type = 'number'; + while (digit(next())) { + consume(); + repr += stringFromCode(code); + } + } + const value = convertAStringToANumber(repr); + return { type: type, value: value, repr: repr }; + }; + + const convertAStringToANumber = function(string: string): number { + // CSS's number rules are identical to JS, afaik. + return +string; + }; + + const consumeTheRemnantsOfABadURL = function() { + while (consume()) { + if (code === 0x29 || eof()) { + return; + } else if (startsWithAValidEscape()) { + consumeEscape(); + donothing(); + } else { + donothing(); + } + } + }; + + let iterationCount = 0; + while (!eof(next())) { + tokens.push(consumeAToken()); + iterationCount++; + if (iterationCount > str.length * 2) throw new Error("I'm infinite-looping!"); + } + return tokens; +} + +export class CSSParserToken implements CSSTokenInterface { + tokenType = ''; + value: string | number | undefined; + toJSON(): any { + return { token: this.tokenType }; + } + toString() { return this.tokenType; } + toSource() { return '' + this; } +} + +export class BadStringToken extends CSSParserToken { + override tokenType = 'BADSTRING'; +} + +export class BadURLToken extends CSSParserToken { + override tokenType = 'BADURL'; +} + +export class WhitespaceToken extends CSSParserToken { + override tokenType = 'WHITESPACE'; + override toString() { return 'WS'; } + override toSource() { return ' '; } +} + +export class CDOToken extends CSSParserToken { + override tokenType = 'CDO'; + override toSource() { return ''; } +} + +export class ColonToken extends CSSParserToken { + override tokenType = ':'; +} + +export class SemicolonToken extends CSSParserToken { + override tokenType = ';'; +} + +export class CommaToken extends CSSParserToken { + override tokenType = ','; +} + +export class GroupingToken extends CSSParserToken { + override value = ''; + mirror = ''; +} + +export class OpenCurlyToken extends GroupingToken { + override tokenType = '{'; + constructor() { + super(); + this.value = '{'; + this.mirror = '}'; + } +} + +export class CloseCurlyToken extends GroupingToken { + override tokenType = '}'; + constructor() { + super(); + this.value = '}'; + this.mirror = '{'; + } +} + +export class OpenSquareToken extends GroupingToken { + override tokenType = '['; + constructor() { + super(); + this.value = '['; + this.mirror = ']'; + } +} + +export class CloseSquareToken extends GroupingToken { + override tokenType = ']'; + constructor() { + super(); + this.value = ']'; + this.mirror = '['; + } +} + +export class OpenParenToken extends GroupingToken { + override tokenType = '('; + constructor() { + super(); + this.value = '('; + this.mirror = ')'; + } +} + +export class CloseParenToken extends GroupingToken { + override tokenType = ')'; + constructor() { + super(); + this.value = ')'; + this.mirror = '('; + } +} + +export class IncludeMatchToken extends CSSParserToken { + override tokenType = '~='; +} + +export class DashMatchToken extends CSSParserToken { + override tokenType = '|='; +} + +export class PrefixMatchToken extends CSSParserToken { + override tokenType = '^='; +} + +export class SuffixMatchToken extends CSSParserToken { + override tokenType = '$='; +} + +export class SubstringMatchToken extends CSSParserToken { + override tokenType = '*='; +} + +export class ColumnToken extends CSSParserToken { + override tokenType = '||'; +} + +export class EOFToken extends CSSParserToken { + override tokenType = 'EOF'; + override toSource() { return ''; } +} + +export class DelimToken extends CSSParserToken { + override tokenType = 'DELIM'; + override value: string = ''; + + constructor(code: number) { + super(); + this.value = stringFromCode(code); + } + + override toString() { return 'DELIM(' + this.value + ')'; } + + override toJSON() { + const json = this.constructor.prototype.constructor.prototype.toJSON.call(this); + json.value = this.value; + return json; + } + + override toSource() { + if (this.value === '\\') + return '\\\n'; + else + return this.value; + } +} + +export abstract class StringValuedToken extends CSSParserToken { + override value: string = ''; + ASCIIMatch(str: string) { + return this.value.toLowerCase() === str.toLowerCase(); + } + + override toJSON() { + const json = this.constructor.prototype.constructor.prototype.toJSON.call(this); + json.value = this.value; + return json; + } +} + +export class IdentToken extends StringValuedToken { + constructor(val: string) { + super(); + this.value = val; + } + + override tokenType = 'IDENT'; + override toString() { return 'IDENT(' + this.value + ')'; } + override toSource() { + return escapeIdent(this.value); + } +} + +export class FunctionToken extends StringValuedToken { + override tokenType = 'FUNCTION'; + mirror: string; + constructor(val: string) { + super(); + this.value = val; + this.mirror = ')'; + } + + override toString() { return 'FUNCTION(' + this.value + ')'; } + + override toSource() { + return escapeIdent(this.value) + '('; + } +} + +export class AtKeywordToken extends StringValuedToken { + override tokenType = 'AT-KEYWORD'; + constructor(val: string) { + super(); + this.value = val; + } + override toString() { return 'AT(' + this.value + ')'; } + override toSource() { + return '@' + escapeIdent(this.value); + } +} + +export class HashToken extends StringValuedToken { + override tokenType = 'HASH'; + type: string; + constructor(val: string) { + super(); + this.value = val; + this.type = 'unrestricted'; + } + + override toString() { return 'HASH(' + this.value + ')'; } + + override toJSON() { + const json = this.constructor.prototype.constructor.prototype.toJSON.call(this); + json.value = this.value; + json.type = this.type; + return json; + } + + override toSource() { + if (this.type === 'id') + return '#' + escapeIdent(this.value); + else + return '#' + escapeHash(this.value); + + } +} + +export class StringToken extends StringValuedToken { + override tokenType = 'STRING'; + constructor(val: string) { + super(); + this.value = val; + } + + override toString() { + return '"' + escapeString(this.value) + '"'; + } +} + +export class URLToken extends StringValuedToken { + override tokenType = 'URL'; + constructor(val: string) { + super(); + this.value = val; + } + override toString() { return 'URL(' + this.value + ')'; } + override toSource() { + return 'url("' + escapeString(this.value) + '")'; + } +} + +export class NumberToken extends CSSParserToken { + override tokenType = 'NUMBER'; + type: string; + repr: string; + + constructor() { + super(); + this.type = 'integer'; + this.repr = ''; + } + + override toString() { + if (this.type === 'integer') + return 'INT(' + this.value + ')'; + return 'NUMBER(' + this.value + ')'; + } + override toJSON() { + const json = super.toJSON(); + json.value = this.value; + json.type = this.type; + json.repr = this.repr; + return json; + } + override toSource() { return this.repr; } +} + + +export class PercentageToken extends CSSParserToken { + override tokenType = 'PERCENTAGE'; + repr: string; + constructor() { + super(); + this.repr = ''; + } + override toString() { return 'PERCENTAGE(' + this.value + ')'; } + override toJSON() { + const json = this.constructor.prototype.constructor.prototype.toJSON.call(this); + json.value = this.value; + json.repr = this.repr; + return json; + } + override toSource() { return this.repr + '%'; } +} + +export class DimensionToken extends CSSParserToken { + override tokenType = 'DIMENSION'; + type: string; + repr: string; + unit: string; + + constructor() { + super(); + this.type = 'integer'; + this.repr = ''; + this.unit = ''; + } + + override toString() { return 'DIM(' + this.value + ',' + this.unit + ')'; } + override toJSON() { + const json = this.constructor.prototype.constructor.prototype.toJSON.call(this); + json.value = this.value; + json.type = this.type; + json.repr = this.repr; + json.unit = this.unit; + return json; + } + override toSource() { + const source = this.repr; + let unit = escapeIdent(this.unit); + if (unit[0].toLowerCase() === 'e' && (unit[1] === '-' || between(unit.charCodeAt(1), 0x30, 0x39))) { + // Unit is ambiguous with scinot + // Remove the leading "e", replace with escape. + unit = '\\65 ' + unit.slice(1, unit.length); + } + return source + unit; + } +} + +function escapeIdent(string: string) { + string = '' + string; + let result = ''; + const firstcode = string.charCodeAt(0); + for (let i = 0; i < string.length; i++) { + const code = string.charCodeAt(i); + if (code === 0x0) + throw new InvalidCharacterError('Invalid character: the input contains U+0000.'); + + if ( + between(code, 0x1, 0x1f) || code === 0x7f || + (i === 0 && between(code, 0x30, 0x39)) || + (i === 1 && between(code, 0x30, 0x39) && firstcode === 0x2d) + ) + result += '\\' + code.toString(16) + ' '; + else if ( + code >= 0x80 || + code === 0x2d || + code === 0x5f || + between(code, 0x30, 0x39) || + between(code, 0x41, 0x5a) || + between(code, 0x61, 0x7a) + ) + result += string[i]; + else + result += '\\' + string[i]; + + } + return result; +} + +function escapeHash(string: string) { + // Escapes the contents of "unrestricted"-type hash tokens. + // Won't preserve the ID-ness of "id"-type hash tokens; + // use escapeIdent() for that. + string = '' + string; + let result = ''; + for (let i = 0; i < string.length; i++) { + const code = string.charCodeAt(i); + if (code === 0x0) + throw new InvalidCharacterError('Invalid character: the input contains U+0000.'); + + if ( + code >= 0x80 || + code === 0x2d || + code === 0x5f || + between(code, 0x30, 0x39) || + between(code, 0x41, 0x5a) || + between(code, 0x61, 0x7a) + ) + result += string[i]; + else + result += '\\' + code.toString(16) + ' '; + + } + return result; +} + +function escapeString(string: string) { + string = '' + string; + let result = ''; + for (let i = 0; i < string.length; i++) { + const code = string.charCodeAt(i); + + if (code === 0x0) + throw new InvalidCharacterError('Invalid character: the input contains U+0000.'); + + if (between(code, 0x1, 0x1f) || code === 0x7f) + result += '\\' + code.toString(16) + ' '; + else if (code === 0x22 || code === 0x5c) + result += '\\' + string[i]; + else + result += string[i]; + + } + return result; +}