785 lines
26 KiB
TypeScript
785 lines
26 KiB
TypeScript
// https://www.w3.org/TR/css-syntax-3
|
|
|
|
import {fromCodePoint, toCodePoints} from 'css-line-break';
|
|
|
|
export enum TokenType {
|
|
STRING_TOKEN,
|
|
BAD_STRING_TOKEN,
|
|
LEFT_PARENTHESIS_TOKEN,
|
|
RIGHT_PARENTHESIS_TOKEN,
|
|
COMMA_TOKEN,
|
|
HASH_TOKEN,
|
|
DELIM_TOKEN,
|
|
AT_KEYWORD_TOKEN,
|
|
PREFIX_MATCH_TOKEN,
|
|
DASH_MATCH_TOKEN,
|
|
INCLUDE_MATCH_TOKEN,
|
|
LEFT_CURLY_BRACKET_TOKEN,
|
|
RIGHT_CURLY_BRACKET_TOKEN,
|
|
SUFFIX_MATCH_TOKEN,
|
|
SUBSTRING_MATCH_TOKEN,
|
|
DIMENSION_TOKEN,
|
|
PERCENTAGE_TOKEN,
|
|
NUMBER_TOKEN,
|
|
FUNCTION,
|
|
FUNCTION_TOKEN,
|
|
IDENT_TOKEN,
|
|
COLUMN_TOKEN,
|
|
URL_TOKEN,
|
|
BAD_URL_TOKEN,
|
|
CDC_TOKEN,
|
|
CDO_TOKEN,
|
|
COLON_TOKEN,
|
|
SEMICOLON_TOKEN,
|
|
LEFT_SQUARE_BRACKET_TOKEN,
|
|
RIGHT_SQUARE_BRACKET_TOKEN,
|
|
UNICODE_RANGE_TOKEN,
|
|
WHITESPACE_TOKEN,
|
|
EOF_TOKEN
|
|
}
|
|
|
|
interface IToken {
|
|
type: TokenType;
|
|
}
|
|
|
|
export interface Token extends IToken {
|
|
type:
|
|
| TokenType.BAD_URL_TOKEN
|
|
| TokenType.BAD_STRING_TOKEN
|
|
| TokenType.LEFT_PARENTHESIS_TOKEN
|
|
| TokenType.RIGHT_PARENTHESIS_TOKEN
|
|
| TokenType.COMMA_TOKEN
|
|
| TokenType.SUBSTRING_MATCH_TOKEN
|
|
| TokenType.PREFIX_MATCH_TOKEN
|
|
| TokenType.SUFFIX_MATCH_TOKEN
|
|
| TokenType.COLON_TOKEN
|
|
| TokenType.SEMICOLON_TOKEN
|
|
| TokenType.LEFT_SQUARE_BRACKET_TOKEN
|
|
| TokenType.RIGHT_SQUARE_BRACKET_TOKEN
|
|
| TokenType.LEFT_CURLY_BRACKET_TOKEN
|
|
| TokenType.RIGHT_CURLY_BRACKET_TOKEN
|
|
| TokenType.DASH_MATCH_TOKEN
|
|
| TokenType.INCLUDE_MATCH_TOKEN
|
|
| TokenType.COLUMN_TOKEN
|
|
| TokenType.WHITESPACE_TOKEN
|
|
| TokenType.CDC_TOKEN
|
|
| TokenType.CDO_TOKEN
|
|
| TokenType.EOF_TOKEN;
|
|
}
|
|
|
|
export interface StringValueToken extends IToken {
|
|
type:
|
|
| TokenType.STRING_TOKEN
|
|
| TokenType.DELIM_TOKEN
|
|
| TokenType.FUNCTION_TOKEN
|
|
| TokenType.IDENT_TOKEN
|
|
| TokenType.URL_TOKEN
|
|
| TokenType.AT_KEYWORD_TOKEN;
|
|
value: string;
|
|
}
|
|
|
|
export interface HashToken extends IToken {
|
|
type: TokenType.HASH_TOKEN;
|
|
flags: number;
|
|
value: string;
|
|
}
|
|
|
|
export interface NumberValueToken extends IToken {
|
|
type: TokenType.PERCENTAGE_TOKEN | TokenType.NUMBER_TOKEN;
|
|
flags: number;
|
|
number: number;
|
|
}
|
|
|
|
export interface DimensionToken extends IToken {
|
|
type: TokenType.DIMENSION_TOKEN;
|
|
flags: number;
|
|
unit: string;
|
|
number: number;
|
|
}
|
|
|
|
export interface UnicodeRangeToken extends IToken {
|
|
type: TokenType.UNICODE_RANGE_TOKEN;
|
|
start: number;
|
|
end: number;
|
|
}
|
|
|
|
export type CSSToken = Token | StringValueToken | NumberValueToken | DimensionToken | UnicodeRangeToken | HashToken;
|
|
|
|
export const FLAG_UNRESTRICTED = 1 << 0;
|
|
export const FLAG_ID = 1 << 1;
|
|
export const FLAG_INTEGER = 1 << 2;
|
|
export const FLAG_NUMBER = 1 << 3;
|
|
|
|
const LINE_FEED = 0x000a;
|
|
const SOLIDUS = 0x002f;
|
|
const REVERSE_SOLIDUS = 0x005c;
|
|
const CHARACTER_TABULATION = 0x0009;
|
|
const SPACE = 0x0020;
|
|
const QUOTATION_MARK = 0x0022;
|
|
const EQUALS_SIGN = 0x003d;
|
|
const NUMBER_SIGN = 0x0023;
|
|
const DOLLAR_SIGN = 0x0024;
|
|
const PERCENTAGE_SIGN = 0x0025;
|
|
const APOSTROPHE = 0x0027;
|
|
const LEFT_PARENTHESIS = 0x0028;
|
|
const RIGHT_PARENTHESIS = 0x0029;
|
|
const LOW_LINE = 0x005f;
|
|
const HYPHEN_MINUS = 0x002d;
|
|
const EXCLAMATION_MARK = 0x0021;
|
|
const LESS_THAN_SIGN = 0x003c;
|
|
const GREATER_THAN_SIGN = 0x003e;
|
|
const COMMERCIAL_AT = 0x0040;
|
|
const LEFT_SQUARE_BRACKET = 0x005b;
|
|
const RIGHT_SQUARE_BRACKET = 0x005d;
|
|
const CIRCUMFLEX_ACCENT = 0x003d;
|
|
const LEFT_CURLY_BRACKET = 0x007b;
|
|
const QUESTION_MARK = 0x003f;
|
|
const RIGHT_CURLY_BRACKET = 0x007d;
|
|
const VERTICAL_LINE = 0x007c;
|
|
const TILDE = 0x007e;
|
|
const CONTROL = 0x0080;
|
|
const REPLACEMENT_CHARACTER = 0xfffd;
|
|
const ASTERISK = 0x002a;
|
|
const PLUS_SIGN = 0x002b;
|
|
const COMMA = 0x002c;
|
|
const COLON = 0x003a;
|
|
const SEMICOLON = 0x003b;
|
|
const FULL_STOP = 0x002e;
|
|
const NULL = 0x0000;
|
|
const BACKSPACE = 0x0008;
|
|
const LINE_TABULATION = 0x000b;
|
|
const SHIFT_OUT = 0x000e;
|
|
const INFORMATION_SEPARATOR_ONE = 0x001f;
|
|
const DELETE = 0x007f;
|
|
const EOF = -1;
|
|
const ZERO = 0x0030;
|
|
const a = 0x0061;
|
|
const e = 0x0065;
|
|
const f = 0x0066;
|
|
const u = 0x0075;
|
|
const z = 0x007a;
|
|
const A = 0x0041;
|
|
const E = 0x0045;
|
|
const F = 0x0046;
|
|
const U = 0x0055;
|
|
const Z = 0x005a;
|
|
|
|
const isDigit = (codePoint: number) => codePoint >= ZERO && codePoint <= 0x0039;
|
|
const isSurrogateCodePoint = (codePoint: number) => codePoint >= 0xd800 && codePoint <= 0xdfff;
|
|
const isHex = (codePoint: number) =>
|
|
isDigit(codePoint) || (codePoint >= A && codePoint <= F) || (codePoint >= a && codePoint <= f);
|
|
const isLowerCaseLetter = (codePoint: number) => codePoint >= a && codePoint <= z;
|
|
const isUpperCaseLetter = (codePoint: number) => codePoint >= A && codePoint <= Z;
|
|
const isLetter = (codePoint: number) => isLowerCaseLetter(codePoint) || isUpperCaseLetter(codePoint);
|
|
const isNonASCIICodePoint = (codePoint: number) => codePoint >= CONTROL;
|
|
const isWhiteSpace = (codePoint: number): boolean =>
|
|
codePoint === LINE_FEED || codePoint === CHARACTER_TABULATION || codePoint === SPACE;
|
|
const isNameStartCodePoint = (codePoint: number): boolean =>
|
|
isLetter(codePoint) || isNonASCIICodePoint(codePoint) || codePoint === LOW_LINE;
|
|
const isNameCodePoint = (codePoint: number): boolean =>
|
|
isNameStartCodePoint(codePoint) || isDigit(codePoint) || codePoint === HYPHEN_MINUS;
|
|
const isNonPrintableCodePoint = (codePoint: number): boolean => {
|
|
return (
|
|
(codePoint >= NULL && codePoint <= BACKSPACE) ||
|
|
codePoint === LINE_TABULATION ||
|
|
(codePoint >= SHIFT_OUT && codePoint <= INFORMATION_SEPARATOR_ONE) ||
|
|
codePoint === DELETE
|
|
);
|
|
};
|
|
const isValidEscape = (c1: number, c2: number): boolean => {
|
|
if (c1 !== REVERSE_SOLIDUS) {
|
|
return false;
|
|
}
|
|
|
|
return c2 !== LINE_FEED;
|
|
};
|
|
const isIdentifierStart = (c1: number, c2: number, c3: number): boolean => {
|
|
if (c1 === HYPHEN_MINUS) {
|
|
return isNameStartCodePoint(c2) || isValidEscape(c2, c3);
|
|
} else if (isNameStartCodePoint(c1)) {
|
|
return true;
|
|
} else if (c1 === REVERSE_SOLIDUS && isValidEscape(c1, c2)) {
|
|
return true;
|
|
}
|
|
return false;
|
|
};
|
|
|
|
const isNumberStart = (c1: number, c2: number, c3: number): boolean => {
|
|
if (c1 === PLUS_SIGN || c1 === HYPHEN_MINUS) {
|
|
if (isDigit(c2)) {
|
|
return true;
|
|
}
|
|
|
|
return c2 === FULL_STOP && isDigit(c3);
|
|
}
|
|
|
|
if (c1 === FULL_STOP) {
|
|
return isDigit(c2);
|
|
}
|
|
|
|
return isDigit(c1);
|
|
};
|
|
|
|
const stringToNumber = (codePoints: number[]): number => {
|
|
let c = 0;
|
|
let sign = 1;
|
|
if (codePoints[c] === PLUS_SIGN || codePoints[c] === HYPHEN_MINUS) {
|
|
if (codePoints[c] === HYPHEN_MINUS) {
|
|
sign = -1;
|
|
}
|
|
c++;
|
|
}
|
|
|
|
const integers = [];
|
|
|
|
while (isDigit(codePoints[c])) {
|
|
integers.push(codePoints[c++]);
|
|
}
|
|
|
|
const int = integers.length ? parseInt(fromCodePoint(...integers), 10) : 0;
|
|
|
|
if (codePoints[c] === FULL_STOP) {
|
|
c++;
|
|
}
|
|
|
|
const fraction = [];
|
|
while (isDigit(codePoints[c])) {
|
|
fraction.push(codePoints[c++]);
|
|
}
|
|
|
|
const fracd = fraction.length;
|
|
const frac = fracd ? parseInt(fromCodePoint(...fraction), 10) : 0;
|
|
|
|
if (codePoints[c] === E || codePoints[c] === e) {
|
|
c++;
|
|
}
|
|
|
|
let expsign = 1;
|
|
|
|
if (codePoints[c] === PLUS_SIGN || codePoints[c] === HYPHEN_MINUS) {
|
|
if (codePoints[c] === HYPHEN_MINUS) {
|
|
expsign = -1;
|
|
}
|
|
c++;
|
|
}
|
|
|
|
const exponent = [];
|
|
|
|
while (isDigit(codePoints[c])) {
|
|
exponent.push(codePoints[c++]);
|
|
}
|
|
|
|
const exp = exponent.length ? parseInt(fromCodePoint(...exponent), 10) : 0;
|
|
|
|
return sign * (int + frac * Math.pow(10, -fracd)) * Math.pow(10, expsign * exp);
|
|
};
|
|
|
|
const LEFT_PARENTHESIS_TOKEN: Token = {
|
|
type: TokenType.LEFT_PARENTHESIS_TOKEN
|
|
};
|
|
const RIGHT_PARENTHESIS_TOKEN: Token = {
|
|
type: TokenType.RIGHT_PARENTHESIS_TOKEN
|
|
};
|
|
const COMMA_TOKEN: Token = {type: TokenType.COMMA_TOKEN};
|
|
const SUFFIX_MATCH_TOKEN: Token = {type: TokenType.SUFFIX_MATCH_TOKEN};
|
|
const PREFIX_MATCH_TOKEN: Token = {type: TokenType.PREFIX_MATCH_TOKEN};
|
|
const COLUMN_TOKEN: Token = {type: TokenType.COLUMN_TOKEN};
|
|
const DASH_MATCH_TOKEN: Token = {type: TokenType.DASH_MATCH_TOKEN};
|
|
const INCLUDE_MATCH_TOKEN: Token = {type: TokenType.INCLUDE_MATCH_TOKEN};
|
|
const LEFT_CURLY_BRACKET_TOKEN: Token = {
|
|
type: TokenType.LEFT_CURLY_BRACKET_TOKEN
|
|
};
|
|
const RIGHT_CURLY_BRACKET_TOKEN: Token = {
|
|
type: TokenType.RIGHT_CURLY_BRACKET_TOKEN
|
|
};
|
|
const SUBSTRING_MATCH_TOKEN: Token = {type: TokenType.SUBSTRING_MATCH_TOKEN};
|
|
const BAD_URL_TOKEN: Token = {type: TokenType.BAD_URL_TOKEN};
|
|
const BAD_STRING_TOKEN: Token = {type: TokenType.BAD_STRING_TOKEN};
|
|
const CDO_TOKEN: Token = {type: TokenType.CDO_TOKEN};
|
|
const CDC_TOKEN: Token = {type: TokenType.CDC_TOKEN};
|
|
const COLON_TOKEN: Token = {type: TokenType.COLON_TOKEN};
|
|
const SEMICOLON_TOKEN: Token = {type: TokenType.SEMICOLON_TOKEN};
|
|
const LEFT_SQUARE_BRACKET_TOKEN: Token = {
|
|
type: TokenType.LEFT_SQUARE_BRACKET_TOKEN
|
|
};
|
|
const RIGHT_SQUARE_BRACKET_TOKEN: Token = {
|
|
type: TokenType.RIGHT_SQUARE_BRACKET_TOKEN
|
|
};
|
|
const WHITESPACE_TOKEN: Token = {type: TokenType.WHITESPACE_TOKEN};
|
|
export const EOF_TOKEN: Token = {type: TokenType.EOF_TOKEN};
|
|
|
|
export class Tokenizer {
|
|
private _value: number[];
|
|
|
|
constructor() {
|
|
this._value = [];
|
|
}
|
|
|
|
write(chunk: string) {
|
|
this._value.push(...toCodePoints(chunk));
|
|
}
|
|
|
|
read(): CSSToken[] {
|
|
const tokens = [];
|
|
let token = this.consumeToken();
|
|
while (token !== EOF_TOKEN) {
|
|
tokens.push(token);
|
|
token = this.consumeToken();
|
|
}
|
|
return tokens;
|
|
}
|
|
|
|
private consumeToken(): CSSToken {
|
|
const codePoint = this.consumeCodePoint();
|
|
|
|
switch (codePoint) {
|
|
case QUOTATION_MARK:
|
|
return this.consumeStringToken(QUOTATION_MARK);
|
|
case NUMBER_SIGN:
|
|
const c1 = this.peekCodePoint(0);
|
|
const c2 = this.peekCodePoint(1);
|
|
const c3 = this.peekCodePoint(2);
|
|
if (isNameCodePoint(c1) || isValidEscape(c2, c3)) {
|
|
const flags = isIdentifierStart(c1, c2, c3) ? FLAG_ID : FLAG_UNRESTRICTED;
|
|
const value = this.consumeName();
|
|
|
|
return {type: TokenType.HASH_TOKEN, value, flags};
|
|
}
|
|
break;
|
|
case DOLLAR_SIGN:
|
|
if (this.peekCodePoint(0) === EQUALS_SIGN) {
|
|
this.consumeCodePoint();
|
|
return SUFFIX_MATCH_TOKEN;
|
|
}
|
|
break;
|
|
case APOSTROPHE:
|
|
return this.consumeStringToken(APOSTROPHE);
|
|
case LEFT_PARENTHESIS:
|
|
return LEFT_PARENTHESIS_TOKEN;
|
|
case RIGHT_PARENTHESIS:
|
|
return RIGHT_PARENTHESIS_TOKEN;
|
|
case ASTERISK:
|
|
if (this.peekCodePoint(0) === EQUALS_SIGN) {
|
|
this.consumeCodePoint();
|
|
return SUBSTRING_MATCH_TOKEN;
|
|
}
|
|
break;
|
|
case PLUS_SIGN:
|
|
if (isNumberStart(codePoint, this.peekCodePoint(0), this.peekCodePoint(1))) {
|
|
this.reconsumeCodePoint(codePoint);
|
|
return this.consumeNumericToken();
|
|
}
|
|
break;
|
|
break;
|
|
case COMMA:
|
|
return COMMA_TOKEN;
|
|
case HYPHEN_MINUS:
|
|
const e1 = codePoint;
|
|
const e2 = this.peekCodePoint(0);
|
|
const e3 = this.peekCodePoint(1);
|
|
|
|
if (isNumberStart(e1, e2, e3)) {
|
|
this.reconsumeCodePoint(codePoint);
|
|
return this.consumeNumericToken();
|
|
}
|
|
|
|
if (isIdentifierStart(e1, e2, e3)) {
|
|
this.reconsumeCodePoint(codePoint);
|
|
return this.consumeIdentLikeToken();
|
|
}
|
|
|
|
if (e2 === HYPHEN_MINUS && e3 === GREATER_THAN_SIGN) {
|
|
this.consumeCodePoint();
|
|
this.consumeCodePoint();
|
|
return CDC_TOKEN;
|
|
}
|
|
break;
|
|
|
|
case FULL_STOP:
|
|
if (isNumberStart(codePoint, this.peekCodePoint(0), this.peekCodePoint(1))) {
|
|
this.reconsumeCodePoint(codePoint);
|
|
return this.consumeNumericToken();
|
|
}
|
|
break;
|
|
case SOLIDUS:
|
|
if (this.peekCodePoint(0) === ASTERISK) {
|
|
this.consumeCodePoint();
|
|
while (true) {
|
|
let c = this.consumeCodePoint();
|
|
if (c === ASTERISK) {
|
|
c = this.consumeCodePoint();
|
|
if (c === SOLIDUS) {
|
|
return this.consumeToken();
|
|
}
|
|
}
|
|
if (c === EOF) {
|
|
return this.consumeToken();
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
case COLON:
|
|
return COLON_TOKEN;
|
|
case SEMICOLON:
|
|
return SEMICOLON_TOKEN;
|
|
case LESS_THAN_SIGN:
|
|
if (
|
|
this.peekCodePoint(0) === EXCLAMATION_MARK &&
|
|
this.peekCodePoint(1) === HYPHEN_MINUS &&
|
|
this.peekCodePoint(2) === HYPHEN_MINUS
|
|
) {
|
|
this.consumeCodePoint();
|
|
this.consumeCodePoint();
|
|
return CDO_TOKEN;
|
|
}
|
|
break;
|
|
case COMMERCIAL_AT:
|
|
const a1 = this.peekCodePoint(0);
|
|
const a2 = this.peekCodePoint(1);
|
|
const a3 = this.peekCodePoint(2);
|
|
if (isIdentifierStart(a1, a2, a3)) {
|
|
const value = this.consumeName();
|
|
return {type: TokenType.AT_KEYWORD_TOKEN, value};
|
|
}
|
|
break;
|
|
case LEFT_SQUARE_BRACKET:
|
|
return LEFT_SQUARE_BRACKET_TOKEN;
|
|
case REVERSE_SOLIDUS:
|
|
if (isValidEscape(codePoint, this.peekCodePoint(0))) {
|
|
this.reconsumeCodePoint(codePoint);
|
|
return this.consumeIdentLikeToken();
|
|
}
|
|
break;
|
|
case RIGHT_SQUARE_BRACKET:
|
|
return RIGHT_SQUARE_BRACKET_TOKEN;
|
|
case CIRCUMFLEX_ACCENT:
|
|
if (this.peekCodePoint(0) === EQUALS_SIGN) {
|
|
this.consumeCodePoint();
|
|
return PREFIX_MATCH_TOKEN;
|
|
}
|
|
break;
|
|
case LEFT_CURLY_BRACKET:
|
|
return LEFT_CURLY_BRACKET_TOKEN;
|
|
case RIGHT_CURLY_BRACKET:
|
|
return RIGHT_CURLY_BRACKET_TOKEN;
|
|
case u:
|
|
case U:
|
|
const u1 = this.peekCodePoint(0);
|
|
const u2 = this.peekCodePoint(1);
|
|
if (u1 === PLUS_SIGN && (isHex(u2) || u2 === QUESTION_MARK)) {
|
|
this.consumeCodePoint();
|
|
this.consumeUnicodeRangeToken();
|
|
}
|
|
this.reconsumeCodePoint(codePoint);
|
|
return this.consumeIdentLikeToken();
|
|
break;
|
|
case VERTICAL_LINE:
|
|
if (this.peekCodePoint(0) === EQUALS_SIGN) {
|
|
this.consumeCodePoint();
|
|
return DASH_MATCH_TOKEN;
|
|
}
|
|
if (this.peekCodePoint(0) === VERTICAL_LINE) {
|
|
this.consumeCodePoint();
|
|
return COLUMN_TOKEN;
|
|
}
|
|
break;
|
|
case TILDE:
|
|
if (this.peekCodePoint(0) === EQUALS_SIGN) {
|
|
this.consumeCodePoint();
|
|
return INCLUDE_MATCH_TOKEN;
|
|
}
|
|
break;
|
|
case EOF:
|
|
return EOF_TOKEN;
|
|
}
|
|
|
|
if (isWhiteSpace(codePoint)) {
|
|
this.consumeWhiteSpace();
|
|
return WHITESPACE_TOKEN;
|
|
}
|
|
|
|
if (isDigit(codePoint)) {
|
|
this.reconsumeCodePoint(codePoint);
|
|
return this.consumeNumericToken();
|
|
}
|
|
|
|
if (isNameStartCodePoint(codePoint)) {
|
|
this.reconsumeCodePoint(codePoint);
|
|
return this.consumeIdentLikeToken();
|
|
}
|
|
|
|
return {type: TokenType.DELIM_TOKEN, value: fromCodePoint(codePoint)};
|
|
}
|
|
|
|
private consumeCodePoint(): number {
|
|
const value = this._value.shift();
|
|
|
|
return typeof value === 'undefined' ? -1 : value;
|
|
}
|
|
|
|
private reconsumeCodePoint(codePoint: number) {
|
|
this._value.unshift(codePoint);
|
|
}
|
|
|
|
private peekCodePoint(delta: number): number {
|
|
if (delta >= this._value.length) {
|
|
return -1;
|
|
}
|
|
|
|
return this._value[delta];
|
|
}
|
|
|
|
private consumeUnicodeRangeToken(): UnicodeRangeToken {
|
|
const digits = [];
|
|
let codePoint = this.consumeCodePoint();
|
|
while (isHex(codePoint) && digits.length < 6) {
|
|
digits.push(codePoint);
|
|
codePoint = this.consumeCodePoint();
|
|
}
|
|
let questionMarks = false;
|
|
while (codePoint === QUESTION_MARK && digits.length < 6) {
|
|
digits.push(codePoint);
|
|
codePoint = this.consumeCodePoint();
|
|
questionMarks = true;
|
|
}
|
|
|
|
if (questionMarks) {
|
|
const start = parseInt(fromCodePoint(...digits.map(digit => (digit === QUESTION_MARK ? ZERO : digit))), 16);
|
|
const end = parseInt(fromCodePoint(...digits.map(digit => (digit === QUESTION_MARK ? F : digit))), 16);
|
|
return {type: TokenType.UNICODE_RANGE_TOKEN, start, end};
|
|
}
|
|
|
|
const start = parseInt(fromCodePoint(...digits), 16);
|
|
if (this.peekCodePoint(0) === HYPHEN_MINUS && isHex(this.peekCodePoint(1))) {
|
|
this.consumeCodePoint();
|
|
codePoint = this.consumeCodePoint();
|
|
const endDigits = [];
|
|
while (isHex(codePoint) && endDigits.length < 6) {
|
|
endDigits.push(codePoint);
|
|
codePoint = this.consumeCodePoint();
|
|
}
|
|
const end = parseInt(fromCodePoint(...endDigits), 16);
|
|
|
|
return {type: TokenType.UNICODE_RANGE_TOKEN, start, end};
|
|
} else {
|
|
return {type: TokenType.UNICODE_RANGE_TOKEN, start, end: start};
|
|
}
|
|
}
|
|
|
|
private consumeIdentLikeToken(): StringValueToken | Token {
|
|
const value = this.consumeName();
|
|
if (value.toLowerCase() === 'url' && this.peekCodePoint(0) === LEFT_PARENTHESIS) {
|
|
this.consumeCodePoint();
|
|
return this.consumeUrlToken();
|
|
} else if (this.peekCodePoint(0) === LEFT_PARENTHESIS) {
|
|
this.consumeCodePoint();
|
|
return {type: TokenType.FUNCTION_TOKEN, value};
|
|
}
|
|
|
|
return {type: TokenType.IDENT_TOKEN, value};
|
|
}
|
|
|
|
private consumeUrlToken(): StringValueToken | Token {
|
|
const value = [];
|
|
this.consumeWhiteSpace();
|
|
|
|
if (this.peekCodePoint(0) === EOF) {
|
|
return {type: TokenType.URL_TOKEN, value: ''};
|
|
}
|
|
|
|
const next = this.peekCodePoint(0);
|
|
if (next === APOSTROPHE || next === QUOTATION_MARK) {
|
|
const stringToken = this.consumeStringToken(this.consumeCodePoint());
|
|
if (stringToken.type === TokenType.STRING_TOKEN) {
|
|
this.consumeWhiteSpace();
|
|
|
|
if (this.peekCodePoint(0) === EOF || this.peekCodePoint(0) === RIGHT_PARENTHESIS) {
|
|
this.consumeCodePoint();
|
|
return {type: TokenType.URL_TOKEN, value: stringToken.value};
|
|
}
|
|
}
|
|
|
|
this.consumeBadUrlRemnants();
|
|
return BAD_URL_TOKEN;
|
|
}
|
|
|
|
while (true) {
|
|
const codePoint = this.consumeCodePoint();
|
|
if (codePoint === EOF || codePoint === RIGHT_PARENTHESIS) {
|
|
return {type: TokenType.URL_TOKEN, value: fromCodePoint(...value)};
|
|
} else if (isWhiteSpace(codePoint)) {
|
|
this.consumeWhiteSpace();
|
|
if (this.peekCodePoint(0) === EOF || this.peekCodePoint(0) === RIGHT_PARENTHESIS) {
|
|
this.consumeCodePoint();
|
|
return {type: TokenType.URL_TOKEN, value: fromCodePoint(...value)};
|
|
}
|
|
this.consumeBadUrlRemnants();
|
|
return BAD_URL_TOKEN;
|
|
} else if (
|
|
codePoint === QUOTATION_MARK ||
|
|
codePoint === APOSTROPHE ||
|
|
codePoint === LEFT_PARENTHESIS ||
|
|
isNonPrintableCodePoint(codePoint)
|
|
) {
|
|
this.consumeBadUrlRemnants();
|
|
return BAD_URL_TOKEN;
|
|
} else if (codePoint === REVERSE_SOLIDUS) {
|
|
if (isValidEscape(codePoint, this.peekCodePoint(0))) {
|
|
value.push(this.consumeEscapedCodePoint());
|
|
} else {
|
|
this.consumeBadUrlRemnants();
|
|
return BAD_URL_TOKEN;
|
|
}
|
|
} else {
|
|
value.push(codePoint);
|
|
}
|
|
}
|
|
}
|
|
|
|
private consumeWhiteSpace(): void {
|
|
while (isWhiteSpace(this.peekCodePoint(0))) {
|
|
this.consumeCodePoint();
|
|
}
|
|
}
|
|
|
|
private consumeBadUrlRemnants(): void {
|
|
while (true) {
|
|
let codePoint = this.consumeCodePoint();
|
|
if (codePoint === RIGHT_PARENTHESIS || codePoint === EOF) {
|
|
return;
|
|
}
|
|
|
|
if (isValidEscape(codePoint, this.peekCodePoint(0))) {
|
|
this.consumeEscapedCodePoint();
|
|
}
|
|
}
|
|
}
|
|
|
|
private consumeStringToken(endingCodePoint: number): StringValueToken | Token {
|
|
let value = '';
|
|
|
|
do {
|
|
const codePoint = this.consumeCodePoint();
|
|
if (codePoint === EOF || codePoint === endingCodePoint) {
|
|
return {type: TokenType.STRING_TOKEN, value};
|
|
}
|
|
|
|
if (codePoint === LINE_FEED) {
|
|
this.reconsumeCodePoint(codePoint);
|
|
return BAD_STRING_TOKEN;
|
|
}
|
|
|
|
if (codePoint === REVERSE_SOLIDUS) {
|
|
const next = this.peekCodePoint(0);
|
|
if (next !== EOF) {
|
|
if (next === LINE_FEED) {
|
|
this.consumeCodePoint();
|
|
} else if (isValidEscape(codePoint, next)) {
|
|
value += fromCodePoint(this.consumeEscapedCodePoint());
|
|
}
|
|
}
|
|
} else {
|
|
value += fromCodePoint(codePoint);
|
|
}
|
|
} while (true);
|
|
}
|
|
|
|
private consumeNumber() {
|
|
let repr = [];
|
|
let type = FLAG_INTEGER;
|
|
let c1 = this.peekCodePoint(0);
|
|
if (c1 === PLUS_SIGN || c1 === HYPHEN_MINUS) {
|
|
repr.push(this.consumeCodePoint());
|
|
}
|
|
|
|
while (isDigit(this.peekCodePoint(0))) {
|
|
repr.push(this.consumeCodePoint());
|
|
}
|
|
c1 = this.peekCodePoint(0);
|
|
let c2 = this.peekCodePoint(1);
|
|
if (c1 === FULL_STOP && isDigit(c2)) {
|
|
repr.push(this.consumeCodePoint(), this.consumeCodePoint());
|
|
type = FLAG_NUMBER;
|
|
while (isDigit(this.peekCodePoint(0))) {
|
|
repr.push(this.consumeCodePoint());
|
|
}
|
|
}
|
|
|
|
c1 = this.peekCodePoint(0);
|
|
c2 = this.peekCodePoint(1);
|
|
let c3 = this.peekCodePoint(2);
|
|
if ((c1 === E || c1 === e) && (((c2 === PLUS_SIGN || c2 === HYPHEN_MINUS) && isDigit(c3)) || isDigit(c2))) {
|
|
repr.push(this.consumeCodePoint(), this.consumeCodePoint());
|
|
type = FLAG_NUMBER;
|
|
while (isDigit(this.peekCodePoint(0))) {
|
|
repr.push(this.consumeCodePoint());
|
|
}
|
|
}
|
|
|
|
return [stringToNumber(repr), type];
|
|
}
|
|
|
|
private consumeNumericToken(): NumberValueToken | DimensionToken {
|
|
const [number, flags] = this.consumeNumber();
|
|
const c1 = this.peekCodePoint(0);
|
|
const c2 = this.peekCodePoint(1);
|
|
const c3 = this.peekCodePoint(2);
|
|
|
|
if (isIdentifierStart(c1, c2, c3)) {
|
|
let unit = this.consumeName();
|
|
return {type: TokenType.DIMENSION_TOKEN, number, flags, unit};
|
|
}
|
|
|
|
if (c1 === PERCENTAGE_SIGN) {
|
|
this.consumeCodePoint();
|
|
return {type: TokenType.PERCENTAGE_TOKEN, number, flags};
|
|
}
|
|
|
|
return {type: TokenType.NUMBER_TOKEN, number, flags};
|
|
}
|
|
|
|
private consumeEscapedCodePoint(): number {
|
|
const codePoint = this.consumeCodePoint();
|
|
|
|
if (isHex(codePoint)) {
|
|
let hex = fromCodePoint(codePoint);
|
|
while (isHex(this.peekCodePoint(0)) && hex.length < 6) {
|
|
hex += fromCodePoint(this.consumeCodePoint());
|
|
}
|
|
|
|
if (isWhiteSpace(this.peekCodePoint(0))) {
|
|
this.consumeCodePoint();
|
|
}
|
|
|
|
const hexCodePoint = parseInt(hex, 16);
|
|
|
|
if (hexCodePoint === 0 || isSurrogateCodePoint(hexCodePoint) || hexCodePoint > 0x10ffff) {
|
|
return REPLACEMENT_CHARACTER;
|
|
}
|
|
|
|
return hexCodePoint;
|
|
}
|
|
|
|
if (codePoint === EOF) {
|
|
return REPLACEMENT_CHARACTER;
|
|
}
|
|
|
|
return codePoint;
|
|
}
|
|
|
|
private consumeName(): string {
|
|
let result = '';
|
|
while (true) {
|
|
const codePoint = this.consumeCodePoint();
|
|
if (isNameCodePoint(codePoint)) {
|
|
result += fromCodePoint(codePoint);
|
|
} else if (isValidEscape(codePoint, this.peekCodePoint(0))) {
|
|
result += fromCodePoint(this.consumeEscapedCodePoint());
|
|
} else {
|
|
this.reconsumeCodePoint(codePoint);
|
|
return result;
|
|
}
|
|
}
|
|
}
|
|
}
|