/* * The MIT License (MIT) * Copyright (C) 2016 by Roman Dvornov * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ // derived from https://github.com/csstree/csstree this.cssTree = this.cssTree || (() => { function createItem(data) { return { prev: null, next: null, data: data }; } function allocateCursor(node, prev, next) { let cursor; if (cursors !== null) { cursor = cursors; cursors = cursors.cursor; cursor.prev = prev; cursor.next = next; cursor.cursor = node.cursor; } else { cursor = { prev: prev, next: next, cursor: node.cursor }; } node.cursor = cursor; return cursor; } function releaseCursor(node) { const cursor = node.cursor; node.cursor = cursor.cursor; cursor.prev = null; cursor.next = null; cursor.cursor = cursors; cursors = cursor; } let cursors = null; function List() { this.cursor = null; this.head = null; this.tail = null; } List.createItem = createItem; List.prototype.createItem = createItem; List.prototype.updateCursors = function (prevOld, prevNew, nextOld, nextNew) { let cursor = this.cursor; while (cursor !== null) { if (cursor.prev === prevOld) { cursor.prev = prevNew; } if (cursor.next === nextOld) { cursor.next = nextNew; } cursor = cursor.cursor; } }; List.prototype.getSize = function () { let size = 0; let cursor = this.head; while (cursor) { size++; cursor = cursor.next; } return size; }; List.prototype.fromArray = function (array) { let cursor = null; this.head = null; for (let i = 0; i < array.length; i++) { const item = createItem(array[i]); if (cursor !== null) { cursor.next = item; } else { this.head = item; } item.prev = cursor; cursor = item; } this.tail = cursor; return this; }; List.prototype.toArray = function () { let cursor = this.head; const result = []; while (cursor) { result.push(cursor.data); cursor = cursor.next; } return result; }; List.prototype.toJSON = List.prototype.toArray; List.prototype.isEmpty = function () { return this.head === null; }; List.prototype.first = function () { return this.head && this.head.data; }; List.prototype.last = function () { return this.tail && this.tail.data; }; List.prototype.each = function (fn, context) { let item; if (context === undefined) { context = this; } // push cursor const cursor = allocateCursor(this, null, this.head); while (cursor.next !== null) { item = cursor.next; cursor.next = item.next; fn.call(context, item.data, item, this); } // pop cursor releaseCursor(this); }; List.prototype.forEach = List.prototype.each; List.prototype.eachRight = function (fn, context) { let item; if (context === undefined) { context = this; } // push cursor const cursor = allocateCursor(this, this.tail, null); while (cursor.prev !== null) { item = cursor.prev; cursor.prev = item.prev; fn.call(context, item.data, item, this); } // pop cursor releaseCursor(this); }; List.prototype.forEachRight = List.prototype.eachRight; List.prototype.nextUntil = function (start, fn, context) { if (start === null) { return; } let item; if (context === undefined) { context = this; } // push cursor const cursor = allocateCursor(this, null, start); while (cursor.next !== null) { item = cursor.next; cursor.next = item.next; if (fn.call(context, item.data, item, this)) { break; } } // pop cursor releaseCursor(this); }; List.prototype.prevUntil = function (start, fn, context) { if (start === null) { return; } let item; if (context === undefined) { context = this; } // push cursor const cursor = allocateCursor(this, start, null); while (cursor.prev !== null) { item = cursor.prev; cursor.prev = item.prev; if (fn.call(context, item.data, item, this)) { break; } } // pop cursor releaseCursor(this); }; List.prototype.some = function (fn, context) { let cursor = this.head; if (context === undefined) { context = this; } while (cursor !== null) { if (fn.call(context, cursor.data, cursor, this)) { return true; } cursor = cursor.next; } return false; }; List.prototype.map = function (fn, context) { const result = new List(); let cursor = this.head; if (context === undefined) { context = this; } while (cursor !== null) { result.appendData(fn.call(context, cursor.data, cursor, this)); cursor = cursor.next; } return result; }; List.prototype.filter = function (fn, context) { const result = new List(); let cursor = this.head; if (context === undefined) { context = this; } while (cursor !== null) { if (fn.call(context, cursor.data, cursor, this)) { result.appendData(cursor.data); } cursor = cursor.next; } return result; }; List.prototype.clear = function () { this.head = null; this.tail = null; }; List.prototype.copy = function () { const result = new List(); let cursor = this.head; while (cursor !== null) { result.insert(createItem(cursor.data)); cursor = cursor.next; } return result; }; List.prototype.prepend = function (item) { // head // ^ // item this.updateCursors(null, item, this.head, item); // insert to the beginning of the list if (this.head !== null) { // new item <- first item this.head.prev = item; // new item -> first item item.next = this.head; } else { // if list has no head, then it also has no tail // in this case tail points to the new item this.tail = item; } // head always points to new item this.head = item; return this; }; List.prototype.prependData = function (data) { return this.prepend(createItem(data)); }; List.prototype.append = function (item) { return this.insert(item); }; List.prototype.appendData = function (data) { return this.insert(createItem(data)); }; List.prototype.insert = function (item, before) { if (before !== undefined && before !== null) { // prev before // ^ // item this.updateCursors(before.prev, item, before, item); if (before.prev === null) { // insert to the beginning of list if (this.head !== before) { throw new Error("before doesn\"t belong to list"); } // since head points to before therefore list doesn"t empty // no need to check tail this.head = item; before.prev = item; item.next = before; this.updateCursors(null, item); } else { // insert between two items before.prev.next = item; item.prev = before.prev; before.prev = item; item.next = before; } } else { // tail // ^ // item this.updateCursors(this.tail, item, null, item); // insert to the ending of the list if (this.tail !== null) { // last item -> new item this.tail.next = item; // last item <- new item item.prev = this.tail; } else { // if list has no tail, then it also has no head // in this case head points to new item this.head = item; } // tail always points to new item this.tail = item; } return this; }; List.prototype.insertData = function (data, before) { return this.insert(createItem(data), before); }; List.prototype.remove = function (item) { // item // ^ // prev next this.updateCursors(item, item.prev, item, item.next); if (item.prev !== null) { item.prev.next = item.next; } else { if (this.head !== item) { throw new Error("item doesn\"t belong to list"); } this.head = item.next; } if (item.next !== null) { item.next.prev = item.prev; } else { if (this.tail !== item) { throw new Error("item doesn\"t belong to list"); } this.tail = item.prev; } item.prev = null; item.next = null; return item; }; List.prototype.push = function (data) { this.insert(createItem(data)); }; List.prototype.pop = function () { if (this.tail !== null) { return this.remove(this.tail); } }; List.prototype.unshift = function (data) { this.prepend(createItem(data)); }; List.prototype.shift = function () { if (this.head !== null) { return this.remove(this.head); } }; List.prototype.prependList = function (list) { return this.insertList(list, this.head); }; List.prototype.appendList = function (list) { return this.insertList(list); }; List.prototype.insertList = function (list, before) { // ignore empty lists if (list.head === null) { return this; } if (before !== undefined && before !== null) { this.updateCursors(before.prev, list.tail, before, list.head); // insert in the middle of dist list if (before.prev !== null) { // before.prev <-> list.head before.prev.next = list.head; list.head.prev = before.prev; } else { this.head = list.head; } before.prev = list.tail; list.tail.next = before; } else { this.updateCursors(this.tail, list.tail, null, list.head); // insert to end of the list if (this.tail !== null) { // if destination list has a tail, then it also has a head, // but head doesn"t change // dest tail -> source head this.tail.next = list.head; // dest tail <- source head list.head.prev = this.tail; } else { // if list has no a tail, then it also has no a head // in this case points head to new item this.head = list.head; } // tail always start point to new item this.tail = list.tail; } list.head = null; list.tail = null; return this; }; List.prototype.replace = function (oldItem, newItemOrList) { if ("head" in newItemOrList) { this.insertList(newItemOrList, oldItem); } else { this.insert(newItemOrList, oldItem); } this.remove(oldItem); }; // --- function createCustomError(name, message) { // use Object.create(), because some VMs prevent setting line/column otherwise // (iOS Safari 10 even throws an exception) const error = Object.create(SyntaxError.prototype); const errorStack = new Error(); error.name = name; error.message = message; Object.defineProperty(error, "stack", { get: function () { return (errorStack.stack || "").replace(/^(.+\n){1,3}/, name + ": " + message + "\n"); } }); return error; } // --- const MAX_LINE_LENGTH = 100; const OFFSET_CORRECTION = 60; const TAB_REPLACEMENT = " "; function sourceFragment(error, extraLines) { function processLines(start, end) { return lines.slice(start, end).map(function (line, idx) { let num = String(start + idx + 1); while (num.length < maxNumLength) { num = " " + num; } return num + " |" + line; }).join("\n"); } const lines = error.source.split(/\r\n?|\n|\f/); let line = error.line; let column = error.column; const startLine = Math.max(1, line - extraLines) - 1; const endLine = Math.min(line + extraLines, lines.length + 1); const maxNumLength = Math.max(4, String(endLine).length) + 1; let cutLeft = 0; // column correction according to replaced tab before column column += (TAB_REPLACEMENT.length - 1) * (lines[line - 1].substr(0, column - 1).match(/\t/g) || []).length; if (column > MAX_LINE_LENGTH) { cutLeft = column - OFFSET_CORRECTION + 3; column = OFFSET_CORRECTION - 2; } for (let i = startLine; i <= endLine; i++) { if (i >= 0 && i < lines.length) { lines[i] = lines[i].replace(/\t/g, TAB_REPLACEMENT); lines[i] = (cutLeft > 0 && lines[i].length > cutLeft ? "\u2026" : "") + lines[i].substr(cutLeft, MAX_LINE_LENGTH - 2) + (lines[i].length > cutLeft + MAX_LINE_LENGTH - 1 ? "\u2026" : ""); } } return [ processLines(startLine, line), new Array(column + maxNumLength + 2).join("-") + "^", processLines(line, endLine) ].filter(Boolean).join("\n"); } function CssSyntaxError(message, source, offset, line, column) { const error = createCustomError("CssSyntaxError", message); error.source = source; error.offset = offset; error.line = line; error.column = column; error.sourceFragment = function (extraLines) { return sourceFragment(error, isNaN(extraLines) ? 0 : extraLines); }; Object.defineProperty(error, "formattedMessage", { get: function () { return ( "Parse error: " + error.message + "\n" + sourceFragment(error, 2) ); } }); // for backward capability error.parseError = { offset: offset, line: line, column: column }; return error; } // --- // token types (note: value shouldn't intersect with used char codes) const WHITESPACE = 1; const IDENTIFIER = 2; const NUMBER = 3; const STRING = 4; const COMMENT = 5; const PUNCTUATOR = 6; const CDO = 7; const CDC = 8; const ATKEYWORD = 14; const FUNCTION = 15; const URL = 16; const RAW = 17; const TAB = 9; const NEW_LINE = 10; const F = 12; const R = 13; const SPACE = 32; const TYPE = { WhiteSpace: WHITESPACE, Identifier: IDENTIFIER, Number: NUMBER, String: STRING, Comment: COMMENT, Punctuator: PUNCTUATOR, CDO: CDO, CDC: CDC, AtKeyword: ATKEYWORD, Function: FUNCTION, Url: URL, Raw: RAW, ExclamationMark: 33, // ! QuotationMark: 34, // " NumberSign: 35, // # DollarSign: 36, // $ PercentSign: 37, // % Ampersand: 38, // & Apostrophe: 39, // ' LeftParenthesis: 40, // ( RightParenthesis: 41, // ) Asterisk: 42, // * PlusSign: 43, // + Comma: 44, // , HyphenMinus: 45, // - FullStop: 46, // . Solidus: 47, // / Colon: 58, // : Semicolon: 59, // ; LessThanSign: 60, // < EqualsSign: 61, // = GreaterThanSign: 62, // > QuestionMark: 63, // ? CommercialAt: 64, // @ LeftSquareBracket: 91, // [ Backslash: 92, // \ RightSquareBracket: 93, // ] CircumflexAccent: 94, // ^ LowLine: 95, // _ GraveAccent: 96, // ` LeftCurlyBracket: 123, // { VerticalLine: 124, // | RightCurlyBracket: 125, // } Tilde: 126 // ~ }; const NAME = Object.keys(TYPE).reduce(function (result, key) { result[TYPE[key]] = key; return result; }, {}); // https://drafts.csswg.org/css-syntax/#tokenizer-definitions // > non-ASCII code point // > A code point with a value equal to or greater than U+0080 // > name-start code point // > A letter, a non-ASCII code point, or U+005F LOW LINE (_). // > name code point // > A name-start code point, a digit, or U+002D HYPHEN-MINUS (-) // That means only ASCII code points has a special meaning and we a maps for 0..127 codes only const SafeUint32Array = typeof Uint32Array !== "undefined" ? Uint32Array : Array; // fallback on Array when TypedArray is not supported const SYMBOL_TYPE = new SafeUint32Array(0x80); const PUNCTUATION = new SafeUint32Array(0x80); const STOP_URL_RAW = new SafeUint32Array(0x80); for (let i = 0; i < SYMBOL_TYPE.length; i++) { SYMBOL_TYPE[i] = IDENTIFIER; } // fill categories [ TYPE.ExclamationMark, // ! TYPE.QuotationMark, // " TYPE.NumberSign, // # TYPE.DollarSign, // $ TYPE.PercentSign, // % TYPE.Ampersand, // & TYPE.Apostrophe, // ' TYPE.LeftParenthesis, // ( TYPE.RightParenthesis, // ) TYPE.Asterisk, // * TYPE.PlusSign, // + TYPE.Comma, // , TYPE.HyphenMinus, // - TYPE.FullStop, // . TYPE.Solidus, // / TYPE.Colon, // : TYPE.Semicolon, // ; TYPE.LessThanSign, // < TYPE.EqualsSign, // = TYPE.GreaterThanSign, // > TYPE.QuestionMark, // ? TYPE.CommercialAt, // @ TYPE.LeftSquareBracket, // [ // TYPE.Backslash, // \ TYPE.RightSquareBracket, // ] TYPE.CircumflexAccent, // ^ // TYPE.LowLine, // _ TYPE.GraveAccent, // ` TYPE.LeftCurlyBracket, // { TYPE.VerticalLine, // | TYPE.RightCurlyBracket, // } TYPE.Tilde // ~ ].forEach(function (key) { SYMBOL_TYPE[Number(key)] = PUNCTUATOR; PUNCTUATION[Number(key)] = PUNCTUATOR; }); for (let i = 48; i <= 57; i++) { SYMBOL_TYPE[i] = NUMBER; } SYMBOL_TYPE[SPACE] = WHITESPACE; SYMBOL_TYPE[TAB] = WHITESPACE; SYMBOL_TYPE[NEW_LINE] = WHITESPACE; SYMBOL_TYPE[R] = WHITESPACE; SYMBOL_TYPE[F] = WHITESPACE; SYMBOL_TYPE[TYPE.Apostrophe] = STRING; SYMBOL_TYPE[TYPE.QuotationMark] = STRING; STOP_URL_RAW[SPACE] = 1; STOP_URL_RAW[TAB] = 1; STOP_URL_RAW[NEW_LINE] = 1; STOP_URL_RAW[R] = 1; STOP_URL_RAW[F] = 1; STOP_URL_RAW[TYPE.Apostrophe] = 1; STOP_URL_RAW[TYPE.QuotationMark] = 1; STOP_URL_RAW[TYPE.LeftParenthesis] = 1; STOP_URL_RAW[TYPE.RightParenthesis] = 1; // whitespace is punctuation ... PUNCTUATION[SPACE] = PUNCTUATOR; PUNCTUATION[TAB] = PUNCTUATOR; PUNCTUATION[NEW_LINE] = PUNCTUATOR; PUNCTUATION[R] = PUNCTUATOR; PUNCTUATION[F] = PUNCTUATOR; // ... hyper minus is not PUNCTUATION[TYPE.HyphenMinus] = 0; const constants = { TYPE: TYPE, NAME: NAME, SYMBOL_TYPE: SYMBOL_TYPE, PUNCTUATION: PUNCTUATION, STOP_URL_RAW: STOP_URL_RAW }; // --- const BACK_SLASH = 92; const E = 101; // 'e'.charCodeAt(0) function firstCharOffset(source) { // detect BOM (https://en.wikipedia.org/wiki/Byte_order_mark) if (source.charCodeAt(0) === 0xFEFF || // UTF-16BE source.charCodeAt(0) === 0xFFFE) { // UTF-16LE return 1; } return 0; } function isHex(code) { return (code >= 48 && code <= 57) || // 0 .. 9 (code >= 65 && code <= 70) || // A .. F (code >= 97 && code <= 102); // a .. f } function isNumber(code) { return code >= 48 && code <= 57; } function isWhiteSpace(code) { return code === SPACE || code === TAB || isNewline(code); } function isNewline(code) { return code === R || code === NEW_LINE || code === F; } function getNewlineLength(source, offset, code) { if (isNewline(code)) { if (code === R && offset + 1 < source.length && source.charCodeAt(offset + 1) === NEW_LINE) { return 2; } return 1; } return 0; } function cmpChar(testStr, offset, referenceCode) { let code = testStr.charCodeAt(offset); // code.toLowerCase() for A..Z if (code >= 65 && code <= 90) { code = code | 32; } return code === referenceCode; } function cmpStr(testStr, start, end, referenceStr) { if (end - start !== referenceStr.length) { return false; } if (start < 0 || end > testStr.length) { return false; } for (let i = start; i < end; i++) { let testCode = testStr.charCodeAt(i); const refCode = referenceStr.charCodeAt(i - start); // testCode.toLowerCase() for A..Z if (testCode >= 65 && testCode <= 90) { testCode = testCode | 32; } if (testCode !== refCode) { return false; } } return true; } function findWhiteSpaceStart(source, offset) { while (offset >= 0 && isWhiteSpace(source.charCodeAt(offset))) { offset--; } return offset + 1; } function findWhiteSpaceEnd(source, offset) { while (offset < source.length && isWhiteSpace(source.charCodeAt(offset))) { offset++; } return offset; } function findCommentEnd(source, offset) { const commentEnd = source.indexOf("*/", offset); if (commentEnd === -1) { return source.length; } return commentEnd + 2; } function findStringEnd(source, offset, quote) { for (; offset < source.length; offset++) { const code = source.charCodeAt(offset); // TODO: bad string if (code === BACK_SLASH) { offset++; } else if (code === quote) { offset++; break; } } return offset; } function findDecimalNumberEnd(source, offset) { while (offset < source.length && isNumber(source.charCodeAt(offset))) { offset++; } return offset; } function findNumberEnd(source, offset, allowFraction) { let code; offset = findDecimalNumberEnd(source, offset); // fraction: .\d+ if (allowFraction && offset + 1 < source.length && source.charCodeAt(offset) === FULLSTOP) { code = source.charCodeAt(offset + 1); if (isNumber(code)) { offset = findDecimalNumberEnd(source, offset + 1); } } // exponent: e[+-]\d+ if (offset + 1 < source.length) { if ((source.charCodeAt(offset) | 32) === E) { // case insensitive check for `e` code = source.charCodeAt(offset + 1); if (code === PLUSSIGN || code === HYPHENMINUS) { if (offset + 2 < source.length) { code = source.charCodeAt(offset + 2); } } if (isNumber(code)) { offset = findDecimalNumberEnd(source, offset + 2); } } } return offset; } // skip escaped unicode sequence that can ends with space // [0-9a-f]{1,6}(\r\n|[ \n\r\t\f])? function findEscapeEnd(source, offset) { for (let i = 0; i < 7 && offset + i < source.length; i++) { const code = source.charCodeAt(offset + i); if (i !== 6 && isHex(code)) { continue; } if (i > 0) { offset += i - 1 + getNewlineLength(source, offset + i, code); if (code === SPACE || code === TAB) { offset++; } } break; } return offset; } function findIdentifierEnd(source, offset) { for (; offset < source.length; offset++) { const code = source.charCodeAt(offset); if (code === BACK_SLASH) { offset = findEscapeEnd(source, offset + 1); } else if (code < 0x80 && PUNCTUATION[code] === PUNCTUATOR) { break; } } return offset; } function findUrlRawEnd(source, offset) { for (; offset < source.length; offset++) { const code = source.charCodeAt(offset); if (code === BACK_SLASH) { offset = findEscapeEnd(source, offset + 1); } else if (code < 0x80 && STOP_URL_RAW[code] === 1) { break; } } return offset; } const utils = { firstCharOffset: firstCharOffset, isHex: isHex, isNumber: isNumber, isWhiteSpace: isWhiteSpace, isNewline: isNewline, getNewlineLength: getNewlineLength, cmpChar: cmpChar, cmpStr: cmpStr, findWhiteSpaceStart: findWhiteSpaceStart, findWhiteSpaceEnd: findWhiteSpaceEnd, findCommentEnd: findCommentEnd, findStringEnd: findStringEnd, findDecimalNumberEnd: findDecimalNumberEnd, findNumberEnd: findNumberEnd, findEscapeEnd: findEscapeEnd, findIdentifierEnd: findIdentifierEnd, findUrlRawEnd: findUrlRawEnd }; // --- const STAR = TYPE.Asterisk; const SLASH = TYPE.Solidus; const FULLSTOP = TYPE.FullStop; const PLUSSIGN = TYPE.PlusSign; const HYPHENMINUS = TYPE.HyphenMinus; const GREATERTHANSIGN = TYPE.GreaterThanSign; const LESSTHANSIGN = TYPE.LessThanSign; const EXCLAMATIONMARK = TYPE.ExclamationMark; const COMMERCIALAT = TYPE.CommercialAt; const QUOTATIONMARK = TYPE.QuotationMark; const APOSTROPHE = TYPE.Apostrophe; const LEFTPARENTHESIS = TYPE.LeftParenthesis; const RIGHTPARENTHESIS = TYPE.RightParenthesis; const LEFTCURLYBRACKET = TYPE.LeftCurlyBracket; const RIGHTCURLYBRACKET = TYPE.RightCurlyBracket; const LEFTSQUAREBRACKET = TYPE.LeftSquareBracket; const RIGHTSQUAREBRACKET = TYPE.RightSquareBracket; const NUMBERSIGN = TYPE.NumberSign; const COMMA = TYPE.Comma; const SOLIDUS = TYPE.Solidus; const ASTERISK = TYPE.Asterisk; const PERCENTSIGN = TYPE.PercentSign; const BACKSLASH = TYPE.Backslash; const VERTICALLINE = TYPE.VerticalLine; const TILDE = TYPE.Tilde; const SEMICOLON = TYPE.Semicolon; const COLON = TYPE.Colon; const DOLLARSIGN = TYPE.DollarSign; const EQUALSSIGN = TYPE.EqualsSign; const CIRCUMFLEXACCENT = TYPE.CircumflexAccent; const TYPE_CDC = TYPE.CDC; const TYPE_CDO = TYPE.CDO; const QUESTIONMARK = TYPE.QuestionMark; const NULL = 0; const MIN_BUFFER_SIZE = 16 * 1024; const OFFSET_MASK = 0x00FFFFFF; const TYPE_SHIFT = 24; function computeLinesAndColumns(tokenizer, source) { const sourceLength = source.length; const start = firstCharOffset(source); let lines = tokenizer.lines; let line = tokenizer.startLine; let columns = tokenizer.columns; let column = tokenizer.startColumn; if (lines === null || lines.length < sourceLength + 1) { lines = new SafeUint32Array(Math.max(sourceLength + 1024, MIN_BUFFER_SIZE)); columns = new SafeUint32Array(lines.length); } let i; for (i = start; i < sourceLength; i++) { const code = source.charCodeAt(i); lines[i] = line; columns[i] = column++; if (code === NEW_LINE || code === R || code === F) { if (code === R && i + 1 < sourceLength && source.charCodeAt(i + 1) === NEW_LINE) { i++; lines[i] = line; columns[i] = column; } line++; column = 1; } } lines[i] = line; columns[i] = column; tokenizer.linesAnsColumnsComputed = true; tokenizer.lines = lines; tokenizer.columns = columns; } function tokenLayout(tokenizer, source, startPos) { const sourceLength = source.length; let offsetAndType = tokenizer.offsetAndType; let balance = tokenizer.balance; let tokenCount = 0; let prevType = 0; let offset = startPos; let anchor = 0; let balanceCloseCode = 0; let balanceStart = 0; let balancePrev = 0; if (offsetAndType === null || offsetAndType.length < sourceLength + 1) { offsetAndType = new SafeUint32Array(sourceLength + 1024); balance = new SafeUint32Array(sourceLength + 1024); } while (offset < sourceLength) { let code = source.charCodeAt(offset); let type = code < 0x80 ? SYMBOL_TYPE[code] : IDENTIFIER; balance[tokenCount] = sourceLength; switch (type) { case WHITESPACE: offset = findWhiteSpaceEnd(source, offset + 1); break; case PUNCTUATOR: switch (code) { case balanceCloseCode: balancePrev = balanceStart & OFFSET_MASK; balanceStart = balance[balancePrev]; balanceCloseCode = balanceStart >> TYPE_SHIFT; balance[tokenCount] = balancePrev; balance[balancePrev++] = tokenCount; for (; balancePrev < tokenCount; balancePrev++) { if (balance[balancePrev] === sourceLength) { balance[balancePrev] = tokenCount; } } break; case LEFTSQUAREBRACKET: balance[tokenCount] = balanceStart; balanceCloseCode = RIGHTSQUAREBRACKET; balanceStart = (balanceCloseCode << TYPE_SHIFT) | tokenCount; break; case LEFTCURLYBRACKET: balance[tokenCount] = balanceStart; balanceCloseCode = RIGHTCURLYBRACKET; balanceStart = (balanceCloseCode << TYPE_SHIFT) | tokenCount; break; case LEFTPARENTHESIS: balance[tokenCount] = balanceStart; balanceCloseCode = RIGHTPARENTHESIS; balanceStart = (balanceCloseCode << TYPE_SHIFT) | tokenCount; break; } // /* if (code === STAR && prevType === SLASH) { type = COMMENT; offset = findCommentEnd(source, offset + 1); tokenCount--; // rewrite prev token break; } // edge case for -.123 and +.123 if (code === FULLSTOP && (prevType === PLUSSIGN || prevType === HYPHENMINUS)) { if (offset + 1 < sourceLength && isNumber(source.charCodeAt(offset + 1))) { type = NUMBER; offset = findNumberEnd(source, offset + 2, false); tokenCount--; // rewrite prev token break; } } // if (code === HYPHENMINUS && prevType === HYPHENMINUS) { if (offset + 1 < sourceLength && source.charCodeAt(offset + 1) === GREATERTHANSIGN) { type = CDC; offset = offset + 2; tokenCount--; // rewrite prev token break; } } // ident( if (code === LEFTPARENTHESIS && prevType === IDENTIFIER) { offset = offset + 1; tokenCount--; // rewrite prev token balance[tokenCount] = balance[tokenCount + 1]; balanceStart--; // 4 char length identifier and equal to `url(` (case insensitive) if (offset - anchor === 4 && cmpStr(source, anchor, offset, "url(")) { // special case for url() because it can contain any symbols sequence with few exceptions anchor = findWhiteSpaceEnd(source, offset); code = source.charCodeAt(anchor); if (code !== LEFTPARENTHESIS && code !== RIGHTPARENTHESIS && code !== QUOTATIONMARK && code !== APOSTROPHE) { // url( offsetAndType[tokenCount++] = (URL << TYPE_SHIFT) | offset; balance[tokenCount] = sourceLength; // ws* if (anchor !== offset) { offsetAndType[tokenCount++] = (WHITESPACE << TYPE_SHIFT) | anchor; balance[tokenCount] = sourceLength; } // raw type = RAW; offset = findUrlRawEnd(source, anchor); } else { type = URL; } } else { type = FUNCTION; } break; } type = code; offset = offset + 1; break; case NUMBER: offset = findNumberEnd(source, offset + 1, prevType !== FULLSTOP); // merge number with a preceding dot, dash or plus if (prevType === FULLSTOP || prevType === HYPHENMINUS || prevType === PLUSSIGN) { tokenCount--; // rewrite prev token } break; case STRING: offset = findStringEnd(source, offset + 1, code); break; default: anchor = offset; offset = findIdentifierEnd(source, offset); // merge identifier with a preceding dash if (prevType === HYPHENMINUS) { // rewrite prev token tokenCount--; // restore prev prev token type // for case @-prefix-ident prevType = tokenCount === 0 ? 0 : offsetAndType[tokenCount - 1] >> TYPE_SHIFT; } if (prevType === COMMERCIALAT) { // rewrite prev token and change type to tokenCount--; type = ATKEYWORD; } } offsetAndType[tokenCount++] = (type << TYPE_SHIFT) | offset; prevType = type; } // finalize arrays offsetAndType[tokenCount] = offset; balance[tokenCount] = sourceLength; balance[sourceLength] = sourceLength; // prevents false positive balance match with any token while (balanceStart !== 0) { balancePrev = balanceStart & OFFSET_MASK; balanceStart = balance[balancePrev]; balance[balancePrev] = sourceLength; } tokenizer.offsetAndType = offsetAndType; tokenizer.tokenCount = tokenCount; tokenizer.balance = balance; } // // tokenizer // function Tokenizer(source, startOffset, startLine, startColumn) { this.offsetAndType = null; this.balance = null; this.lines = null; this.columns = null; this.setSource(source, startOffset, startLine, startColumn); } Tokenizer.prototype = { setSource: function (source, startOffset, startLine, startColumn) { const safeSource = String(source || ""); const start = firstCharOffset(safeSource); this.source = safeSource; this.firstCharOffset = start; this.startOffset = typeof startOffset === "undefined" ? 0 : startOffset; this.startLine = typeof startLine === "undefined" ? 1 : startLine; this.startColumn = typeof startColumn === "undefined" ? 1 : startColumn; this.linesAnsColumnsComputed = false; this.eof = false; this.currentToken = -1; this.tokenType = 0; this.tokenStart = start; this.tokenEnd = start; tokenLayout(this, safeSource, start); this.next(); }, lookupType: function (offset) { offset += this.currentToken; if (offset < this.tokenCount) { return this.offsetAndType[offset] >> TYPE_SHIFT; } return NULL; }, lookupNonWSType: function (offset) { offset += this.currentToken; for (let type; offset < this.tokenCount; offset++) { type = this.offsetAndType[offset] >> TYPE_SHIFT; if (type !== WHITESPACE) { return type; } } return NULL; }, lookupValue: function (offset, referenceStr) { offset += this.currentToken; if (offset < this.tokenCount) { return cmpStr( this.source, this.offsetAndType[offset - 1] & OFFSET_MASK, this.offsetAndType[offset] & OFFSET_MASK, referenceStr ); } return false; }, getTokenStart: function (tokenNum) { if (tokenNum === this.currentToken) { return this.tokenStart; } if (tokenNum > 0) { return tokenNum < this.tokenCount ? this.offsetAndType[tokenNum - 1] & OFFSET_MASK : this.offsetAndType[this.tokenCount] & OFFSET_MASK; } return this.firstCharOffset; }, getOffsetExcludeWS: function () { if (this.currentToken > 0) { if ((this.offsetAndType[this.currentToken - 1] >> TYPE_SHIFT) === WHITESPACE) { return this.currentToken > 1 ? this.offsetAndType[this.currentToken - 2] & OFFSET_MASK : this.firstCharOffset; } } return this.tokenStart; }, getRawLength: function (startToken, endTokenType1, endTokenType2, includeTokenType2) { let cursor = startToken; let balanceEnd; loop: for (; cursor < this.tokenCount; cursor++) { balanceEnd = this.balance[cursor]; // belance end points to offset before start if (balanceEnd < startToken) { break loop; } // check token is stop type switch (this.offsetAndType[cursor] >> TYPE_SHIFT) { case endTokenType1: break loop; case endTokenType2: if (includeTokenType2) { cursor++; } break loop; default: // fast forward to the end of balanced block if (this.balance[balanceEnd] === cursor) { cursor = balanceEnd; } } } return cursor - this.currentToken; }, isBalanceEdge: function (pos) { const balanceStart = this.balance[this.currentToken]; return balanceStart < pos; }, getTokenValue: function () { return this.source.substring(this.tokenStart, this.tokenEnd); }, substrToCursor: function (start) { return this.source.substring(start, this.tokenStart); }, skipWS: function () { let skipTokenCount = 0; for (let i = this.currentToken; i < this.tokenCount; i++ , skipTokenCount++) { if ((this.offsetAndType[i] >> TYPE_SHIFT) !== WHITESPACE) { break; } } if (skipTokenCount > 0) { this.skip(skipTokenCount); } }, skipSC: function () { while (this.tokenType === WHITESPACE || this.tokenType === COMMENT) { this.next(); } }, skip: function (tokenCount) { let next = this.currentToken + tokenCount; if (next < this.tokenCount) { this.currentToken = next; this.tokenStart = this.offsetAndType[next - 1] & OFFSET_MASK; next = this.offsetAndType[next]; this.tokenType = next >> TYPE_SHIFT; this.tokenEnd = next & OFFSET_MASK; } else { this.currentToken = this.tokenCount; this.next(); } }, next: function () { let next = this.currentToken + 1; if (next < this.tokenCount) { this.currentToken = next; this.tokenStart = this.tokenEnd; next = this.offsetAndType[next]; this.tokenType = next >> TYPE_SHIFT; this.tokenEnd = next & OFFSET_MASK; } else { this.currentToken = this.tokenCount; this.eof = true; this.tokenType = NULL; this.tokenStart = this.tokenEnd = this.source.length; } }, eat: function (tokenType) { if (this.tokenType !== tokenType) { let offset = this.tokenStart; let message = NAME[tokenType] + " is expected"; // tweak message and offset if (tokenType === IDENTIFIER) { // when identifier is expected but there is a function or url if (this.tokenType === FUNCTION || this.tokenType === URL) { offset = this.tokenEnd - 1; message += " but function found"; } } else { // when test type is part of another token show error for current position + 1 // e.g. eat(HYPHENMINUS) will fail on "-foo", but pointing on "-" is odd if (this.source.charCodeAt(this.tokenStart) === tokenType) { offset = offset + 1; } } this.error(message, offset); } this.next(); }, eatNonWS: function (tokenType) { this.skipWS(); this.eat(tokenType); }, consume: function (tokenType) { const value = this.getTokenValue(); this.eat(tokenType); return value; }, consumeFunctionName: function () { const name = this.source.substring(this.tokenStart, this.tokenEnd - 1); this.eat(FUNCTION); return name; }, consumeNonWS: function (tokenType) { this.skipWS(); return this.consume(tokenType); }, expectIdentifier: function (name) { if (this.tokenType !== IDENTIFIER || cmpStr(this.source, this.tokenStart, this.tokenEnd, name) === false) { this.error("Identifier `" + name + "` is expected"); } this.next(); }, getLocation: function (offset, filename) { if (!this.linesAnsColumnsComputed) { computeLinesAndColumns(this, this.source); } return { source: filename, offset: this.startOffset + offset, line: this.lines[offset], column: this.columns[offset] }; }, getLocationRange: function (start, end, filename) { if (!this.linesAnsColumnsComputed) { computeLinesAndColumns(this, this.source); } return { source: filename, start: { offset: this.startOffset + start, line: this.lines[start], column: this.columns[start] }, end: { offset: this.startOffset + end, line: this.lines[end], column: this.columns[end] } }; }, error: function (message, offset) { const location = typeof offset !== "undefined" && offset < this.source.length ? this.getLocation(offset) : this.eof ? this.getLocation(findWhiteSpaceStart(this.source, this.source.length - 1)) : this.getLocation(this.tokenStart); throw new CssSyntaxError( message || "Unexpected input", this.source, location.offset, location.line, location.column ); }, dump: function () { let offset = 0; return Array.prototype.slice.call(this.offsetAndType, 0, this.tokenCount).map(function (item, idx) { const start = offset; const end = item & OFFSET_MASK; offset = end; return { idx: idx, type: NAME[item >> TYPE_SHIFT], chunk: this.source.substring(start, end), balance: this.balance[idx] }; }, this); } }; // extend with error class Tokenizer.CssSyntaxError = CssSyntaxError; // extend tokenizer with constants Object.keys(constants).forEach(function (key) { Tokenizer[key] = constants[key]; }); // extend tokenizer with static methods from utils Object.keys(utils).forEach(function (key) { Tokenizer[key] = utils[key]; }); // warm up tokenizer to elimitate code branches that never execute // fix soft deoptimizations (insufficient type feedback) new Tokenizer("\n\r\r\n\f//\"\"''/*\r\n\f*/1a;.\\31\t+2{url(a);func();+1.2e3 -.4e-5 .6e+7}").getLocation(); // --- const sequence = function readSequence(recognizer) { const children = this.createList(); let child = null; const context = { recognizer: recognizer, space: null, ignoreWS: false, ignoreWSAfter: false }; this.scanner.skipSC(); while (!this.scanner.eof) { switch (this.scanner.tokenType) { case COMMENT: this.scanner.next(); continue; case WHITESPACE: if (context.ignoreWS) { this.scanner.next(); } else { context.space = this.WhiteSpace(); } continue; } child = recognizer.getNode.call(this, context); if (child === undefined) { break; } if (context.space !== null) { children.push(context.space); context.space = null; } children.push(child); if (context.ignoreWSAfter) { context.ignoreWSAfter = false; context.ignoreWS = true; } else { context.ignoreWS = false; } } return children; }; // --- const noop = function () { }; function createParseContext(name) { return function () { return this[name](); }; } function processConfig(config) { const parserConfig = { context: {}, scope: {}, atrule: {}, pseudo: {} }; if (config.parseContext) { for (let name in config.parseContext) { switch (typeof config.parseContext[name]) { case "function": parserConfig.context[name] = config.parseContext[name]; break; case "string": parserConfig.context[name] = createParseContext(config.parseContext[name]); break; } } } if (config.scope) { for (let name in config.scope) { parserConfig.scope[name] = config.scope[name]; } } if (config.atrule) { for (let name in config.atrule) { const atrule = config.atrule[name]; if (atrule.parse) { parserConfig.atrule[name] = atrule.parse; } } } if (config.pseudo) { for (let name in config.pseudo) { const pseudo = config.pseudo[name]; if (pseudo.parse) { parserConfig.pseudo[name] = pseudo.parse; } } } if (config.node) { for (let name in config.node) { parserConfig[name] = config.node[name].parse; } } return parserConfig; } function createParser(config) { const parser = { scanner: new Tokenizer(), filename: "", needPositions: false, onParseError: noop, onParseErrorThrow: false, parseAtrulePrelude: true, parseRulePrelude: true, parseValue: true, parseCustomProperty: false, readSequence: sequence, createList: function () { return new List(); }, createSingleNodeList: function (node) { return new List().appendData(node); }, getFirstListNode: function (list) { return list && list.first(); }, getLastListNode: function (list) { return list.last(); }, parseWithFallback: function (consumer, fallback) { const startToken = this.scanner.currentToken; try { return consumer.call(this); } catch (e) { if (this.onParseErrorThrow) { throw e; } const fallbackNode = fallback.call(this, startToken); this.onParseErrorThrow = true; this.onParseError(e, fallbackNode); this.onParseErrorThrow = false; return fallbackNode; } }, getLocation: function (start, end) { if (this.needPositions) { return this.scanner.getLocationRange( start, end, this.filename ); } return null; }, getLocationFromList: function (list) { if (this.needPositions) { const head = this.getFirstListNode(list); const tail = this.getLastListNode(list); return this.scanner.getLocationRange( head !== null ? head.loc.start.offset - this.scanner.startOffset : this.scanner.tokenStart, tail !== null ? tail.loc.end.offset - this.scanner.startOffset : this.scanner.tokenStart, this.filename ); } return null; } }; config = processConfig(config || {}); for (let key in config) { parser[key] = config[key]; } return function (source, options) { options = options || {}; const context = options.context || "default"; let ast; parser.scanner.setSource(source, options.offset, options.line, options.column); parser.filename = options.filename || ""; parser.needPositions = Boolean(options.positions); parser.onParseError = typeof options.onParseError === "function" ? options.onParseError : noop; parser.onParseErrorThrow = false; parser.parseAtrulePrelude = "parseAtrulePrelude" in options ? Boolean(options.parseAtrulePrelude) : true; parser.parseRulePrelude = "parseRulePrelude" in options ? Boolean(options.parseRulePrelude) : true; parser.parseValue = "parseValue" in options ? Boolean(options.parseValue) : true; parser.parseCustomProperty = "parseCustomProperty" in options ? Boolean(options.parseCustomProperty) : false; if (!parser.context.hasOwnProperty(context)) { throw new Error("Unknown context `" + context + "`"); } ast = parser.context[context].call(parser, options); if (!parser.scanner.eof) { parser.scanner.error(); } return ast; }; } // --- const U = 117; // 'u'.charCodeAt(0) const getNode = function defaultRecognizer(context) { switch (this.scanner.tokenType) { case NUMBERSIGN: return this.HexColor(); case COMMA: context.space = null; context.ignoreWSAfter = true; return this.Operator(); case SOLIDUS: case ASTERISK: case PLUSSIGN: case HYPHENMINUS: return this.Operator(); case LEFTPARENTHESIS: return this.Parentheses(this.readSequence, context.recognizer); case LEFTSQUAREBRACKET: return this.Brackets(this.readSequence, context.recognizer); case STRING: return this.String(); case NUMBER: switch (this.scanner.lookupType(1)) { case PERCENTSIGN: return this.Percentage(); case IDENTIFIER: // edge case: number with folowing \0 and \9 hack shouldn"t to be a Dimension if (cmpChar(this.scanner.source, this.scanner.tokenEnd, BACKSLASH)) { return this.Number(); } else { return this.Dimension(); } default: return this.Number(); } case FUNCTION: return this.Function(this.readSequence, context.recognizer); case URL: return this.Url(); case IDENTIFIER: // check for unicode range, it should start with u+ or U+ if (cmpChar(this.scanner.source, this.scanner.tokenStart, U) && cmpChar(this.scanner.source, this.scanner.tokenStart + 1, PLUSSIGN)) { return this.UnicodeRange(); } else { return this.Identifier(); } } }; // --- const AtrulePrelude = { getNode: getNode }; // --- function Selector_getNode(context) { switch (this.scanner.tokenType) { case PLUSSIGN: case GREATERTHANSIGN: case TILDE: context.space = null; context.ignoreWSAfter = true; return this.Combinator(); case SOLIDUS: // /deep/ return this.Combinator(); case FULLSTOP: return this.ClassSelector(); case LEFTSQUAREBRACKET: return this.AttributeSelector(); case NUMBERSIGN: return this.IdSelector(); case COLON: if (this.scanner.lookupType(1) === COLON) { return this.PseudoElementSelector(); } else { return this.PseudoClassSelector(); } case IDENTIFIER: case ASTERISK: case VERTICALLINE: return this.TypeSelector(); case NUMBER: return this.Percentage(); } } const Selector = { getNode: Selector_getNode }; // --- const Value_getNode = function defaultRecognizer(context) { switch (this.scanner.tokenType) { case NUMBERSIGN: return this.HexColor(); case COMMA: context.space = null; context.ignoreWSAfter = true; return this.Operator(); case SOLIDUS: case ASTERISK: case PLUSSIGN: case HYPHENMINUS: return this.Operator(); case LEFTPARENTHESIS: return this.Parentheses(this.readSequence, context.recognizer); case LEFTSQUAREBRACKET: return this.Brackets(this.readSequence, context.recognizer); case STRING: return this.String(); case NUMBER: switch (this.scanner.lookupType(1)) { case PERCENTSIGN: return this.Percentage(); case IDENTIFIER: // edge case: number with folowing \0 and \9 hack shouldn't to be a Dimension if (cmpChar(this.scanner.source, this.scanner.tokenEnd, BACKSLASH)) { return this.Number(); } else { return this.Dimension(); } default: return this.Number(); } case FUNCTION: return this.Function(this.readSequence, context.recognizer); case URL: return this.Url(); case IDENTIFIER: // check for unicode range, it should start with u+ or U+ if (cmpChar(this.scanner.source, this.scanner.tokenStart, U) && cmpChar(this.scanner.source, this.scanner.tokenStart + 1, PLUSSIGN)) { return this.UnicodeRange(); } else { return this.Identifier(); } } }; // --- // https://drafts.csswg.org/css-images-4/#element-notation // https://developer.mozilla.org/en-US/docs/Web/CSS/element const Value_Element = function () { this.scanner.skipSC(); const children = this.createSingleNodeList( this.IdSelector() ); this.scanner.skipSC(); return children; }; // --- // legacy IE function // expression '(' raw ')' const Value_expression = function () { return this.createSingleNodeList( this.Raw(this.scanner.currentToken, 0, 0, false, false) ); }; // --- // let '(' ident (',' ? )? ')' const Value_var = function () { const children = this.createList(); this.scanner.skipSC(); const identStart = this.scanner.tokenStart; this.scanner.eat(HYPHENMINUS); if (this.scanner.source.charCodeAt(this.scanner.tokenStart) !== HYPHENMINUS) { this.scanner.error("HyphenMinus is expected"); } this.scanner.eat(IDENTIFIER); children.push({ type: "Identifier", loc: this.getLocation(identStart, this.scanner.tokenStart), name: this.scanner.substrToCursor(identStart) }); this.scanner.skipSC(); if (this.scanner.tokenType === COMMA) { children.push(this.Operator()); children.push(this.parseCustomProperty ? this.Value(null) : this.Raw(this.scanner.currentToken, EXCLAMATIONMARK, SEMICOLON, false, false) ); } return children; }; // --- const Value = { getNode: Value_getNode, "-moz-element": Value_Element, "element": Value_Element, "expression": Value_expression, "let": Value_var }; // --- const scope = { AtrulePrelude: AtrulePrelude, Selector: Selector, Value: Value }; // --- const fontFace = { parse: { prelude: null, block: function () { return this.Block(true); } } }; // --- const _import = { parse: { prelude: function () { const children = this.createList(); this.scanner.skipSC(); switch (this.scanner.tokenType) { case STRING: children.push(this.String()); break; case URL: children.push(this.Url()); break; default: this.scanner.error("String or url() is expected"); } if (this.scanner.lookupNonWSType(0) === IDENTIFIER || this.scanner.lookupNonWSType(0) === LEFTPARENTHESIS) { children.push(this.WhiteSpace()); children.push(this.MediaQueryList()); } return children; }, block: null } }; // --- const media = { parse: { prelude: function () { return this.createSingleNodeList( this.MediaQueryList() ); }, block: function () { return this.Block(false); } } }; // --- const page = { parse: { prelude: function () { return this.createSingleNodeList( this.SelectorList() ); }, block: function () { return this.Block(true); } } }; // --- function supports_consumeRaw() { return this.createSingleNodeList( this.Raw(this.scanner.currentToken, 0, 0, false, false) ); } function parentheses() { let index = 0; this.scanner.skipSC(); // TODO: make it simplier if (this.scanner.tokenType === IDENTIFIER) { index = 1; } else if (this.scanner.tokenType === HYPHENMINUS && this.scanner.lookupType(1) === IDENTIFIER) { index = 2; } if (index !== 0 && this.scanner.lookupNonWSType(index) === COLON) { return this.createSingleNodeList( this.Declaration() ); } return readSequence.call(this); } function readSequence() { const children = this.createList(); let space = null; let child; this.scanner.skipSC(); scan: while (!this.scanner.eof) { switch (this.scanner.tokenType) { case WHITESPACE: space = this.WhiteSpace(); continue; case COMMENT: this.scanner.next(); continue; case FUNCTION: child = this.Function(supports_consumeRaw, this.scope.AtrulePrelude); break; case IDENTIFIER: child = this.Identifier(); break; case LEFTPARENTHESIS: child = this.Parentheses(parentheses, this.scope.AtrulePrelude); break; default: break scan; } if (space !== null) { children.push(space); space = null; } children.push(child); } return children; } const supports = { parse: { prelude: function () { const children = readSequence.call(this); if (this.getFirstListNode(children) === null) { this.scanner.error("Condition is expected"); } return children; }, block: function () { return this.Block(false); } } }; // --- const atrule = { "font-face": fontFace, "import": _import, media: media, page: page, supports: supports }; // --- const dir = { parse: function () { return this.createSingleNodeList( this.Identifier() ); } }; // --- const has = { parse: function () { return this.createSingleNodeList( this.SelectorList() ); } }; // --- const lang = { parse: function () { return this.createSingleNodeList( this.Identifier() ); } }; // --- const matches = { parse: function selectorList() { return this.createSingleNodeList( this.SelectorList() ); } }; const not = matches; // --- const ALLOW_OF_CLAUSE = true; const nthChild = { parse: function nthWithOfClause() { return this.createSingleNodeList( this.Nth(ALLOW_OF_CLAUSE) ); } }; const nthLastChild = nthChild; // --- const DISALLOW_OF_CLAUSE = false; const nthLastOfType = { parse: function nth() { return this.createSingleNodeList( this.Nth(DISALLOW_OF_CLAUSE) ); } }; const nthOfType = nthLastOfType; // --- const slotted = { parse: function compoundSelector() { return this.createSingleNodeList( this.Selector() ); } }; // --- const pseudo = { dir: dir, has: has, lang: lang, matches: matches, not: not, "nth-child": nthChild, "nth-last-child": nthLastChild, "nth-last-of-type": nthLastOfType, "nth-of-type": nthOfType, slotted: slotted }; // --- const AnPlusB_N = 110; // 'n'.charCodeAt(0) const DISALLOW_SIGN = true; const ALLOW_SIGN = false; function checkTokenIsInteger(scanner, disallowSign) { let pos = scanner.tokenStart; if (scanner.source.charCodeAt(pos) === PLUSSIGN || scanner.source.charCodeAt(pos) === HYPHENMINUS) { if (disallowSign) { scanner.error(); } pos++; } for (; pos < scanner.tokenEnd; pos++) { if (!isNumber(scanner.source.charCodeAt(pos))) { scanner.error("Unexpected input", pos); } } } // An+B microsyntax https://www.w3.org/TR/css-syntax-3/#anb const AnPlusB = { name: "AnPlusB", structure: { a: [String, null], b: [String, null] }, parse: function () { const start = this.scanner.tokenStart; let end = start; let prefix = ""; let a = null; let b = null; if (this.scanner.tokenType === NUMBER || this.scanner.tokenType === PLUSSIGN) { checkTokenIsInteger(this.scanner, ALLOW_SIGN); prefix = this.scanner.getTokenValue(); this.scanner.next(); end = this.scanner.tokenStart; } if (this.scanner.tokenType === IDENTIFIER) { let bStart = this.scanner.tokenStart; if (cmpChar(this.scanner.source, bStart, HYPHENMINUS)) { if (prefix === "") { prefix = "-"; bStart++; } else { this.scanner.error("Unexpected hyphen minus"); } } if (!cmpChar(this.scanner.source, bStart, AnPlusB_N)) { this.scanner.error(); } a = prefix === "" ? "1" : prefix === "+" ? "+1" : prefix === "-" ? "-1" : prefix; const len = this.scanner.tokenEnd - bStart; if (len > 1) { // ..n-.. if (this.scanner.source.charCodeAt(bStart + 1) !== HYPHENMINUS) { this.scanner.error("Unexpected input", bStart + 1); } if (len > 2) { // ..n-{number}.. this.scanner.tokenStart = bStart + 2; } else { // ..n- {number} this.scanner.next(); this.scanner.skipSC(); } checkTokenIsInteger(this.scanner, DISALLOW_SIGN); b = "-" + this.scanner.getTokenValue(); this.scanner.next(); end = this.scanner.tokenStart; } else { prefix = ""; this.scanner.next(); end = this.scanner.tokenStart; this.scanner.skipSC(); if (this.scanner.tokenType === HYPHENMINUS || this.scanner.tokenType === PLUSSIGN) { prefix = this.scanner.getTokenValue(); this.scanner.next(); this.scanner.skipSC(); } if (this.scanner.tokenType === NUMBER) { checkTokenIsInteger(this.scanner, prefix !== ""); if (!isNumber(this.scanner.source.charCodeAt(this.scanner.tokenStart))) { prefix = this.scanner.source.charAt(this.scanner.tokenStart); this.scanner.tokenStart++; } if (prefix === "") { // should be an operator before number this.scanner.error(); } else if (prefix === "+") { // plus is using by default prefix = ""; } b = prefix + this.scanner.getTokenValue(); this.scanner.next(); end = this.scanner.tokenStart; } else { if (prefix) { this.scanner.eat(NUMBER); } } } } else { if (prefix === "" || prefix === "+") { // no number this.scanner.error( "Number or identifier is expected", this.scanner.tokenStart + ( this.scanner.tokenType === PLUSSIGN || this.scanner.tokenType === HYPHENMINUS ) ); } b = prefix; } return { type: "AnPlusB", loc: this.getLocation(start, end), a: a, b: b }; }, generate: function (node) { const a = node.a !== null && node.a !== undefined; let b = node.b !== null && node.b !== undefined; if (a) { this.chunk( node.a === "+1" ? "+n" : node.a === "1" ? "n" : node.a === "-1" ? "-n" : node.a + "n" ); if (b) { b = String(node.b); if (b.charAt(0) === "-" || b.charAt(0) === "+") { this.chunk(b.charAt(0)); this.chunk(b.substr(1)); } else { this.chunk("+"); this.chunk(b); } } } else { this.chunk(String(node.b)); } } }; // --- function Atrule_consumeRaw(startToken) { return this.Raw(startToken, SEMICOLON, LEFTCURLYBRACKET, false, true); } function isDeclarationBlockAtrule() { for (let offset = 1, type; type = this.scanner.lookupType(offset); offset++) { // eslint-disable-line no-cond-assign if (type === RIGHTCURLYBRACKET) { return true; } if (type === LEFTCURLYBRACKET || type === ATKEYWORD) { return false; } } return false; } const Atrule = { name: "Atrule", structure: { name: String, prelude: ["AtrulePrelude", "Raw", null], block: ["Block", null] }, parse: function () { const start = this.scanner.tokenStart; let name; let nameLowerCase; let prelude = null; let block = null; this.scanner.eat(ATKEYWORD); name = this.scanner.substrToCursor(start + 1); nameLowerCase = name.toLowerCase(); this.scanner.skipSC(); // parse prelude if (this.scanner.eof === false && this.scanner.tokenType !== LEFTCURLYBRACKET && this.scanner.tokenType !== SEMICOLON) { if (this.parseAtrulePrelude) { prelude = this.parseWithFallback(this.AtrulePrelude.bind(this, name), Atrule_consumeRaw); // turn empty AtrulePrelude into null if (prelude.type === "AtrulePrelude" && prelude.children.head === null) { prelude = null; } } else { prelude = Atrule_consumeRaw.call(this, this.scanner.currentToken); } this.scanner.skipSC(); } switch (this.scanner.tokenType) { case SEMICOLON: this.scanner.next(); break; case LEFTCURLYBRACKET: if (this.atrule.hasOwnProperty(nameLowerCase) && typeof this.atrule[nameLowerCase].block === "function") { block = this.atrule[nameLowerCase].block.call(this); } else { // TODO: should consume block content as Raw? block = this.Block(isDeclarationBlockAtrule.call(this)); } break; } return { type: "Atrule", loc: this.getLocation(start, this.scanner.tokenStart), name: name, prelude: prelude, block: block }; }, generate: function (node) { this.chunk("@"); this.chunk(node.name); if (node.prelude !== null) { this.chunk(" "); this.node(node.prelude); } if (node.block) { this.node(node.block); } else { this.chunk(";"); } }, walkContext: "atrule" }; // --- const Syntax_AtrulePrelude = { name: "AtrulePrelude", structure: { children: [[]] }, parse: function (name) { let children = null; if (name !== null) { name = name.toLowerCase(); } this.scanner.skipSC(); if (this.atrule.hasOwnProperty(name) && typeof this.atrule[name].prelude === "function") { // custom consumer children = this.atrule[name].prelude.call(this); } else { // default consumer children = this.readSequence(this.scope.AtrulePrelude); } this.scanner.skipSC(); if (this.scanner.eof !== true && this.scanner.tokenType !== LEFTCURLYBRACKET && this.scanner.tokenType !== SEMICOLON) { this.scanner.error("Semicolon or block is expected"); } if (children === null) { children = this.createList(); } return { type: "AtrulePrelude", loc: this.getLocationFromList(children), children: children }; }, generate: function (node) { this.children(node); }, walkContext: "atrulePrelude" }; // --- function getAttributeName() { if (this.scanner.eof) { this.scanner.error("Unexpected end of input"); } const start = this.scanner.tokenStart; let expectIdentifier = false; let checkColon = true; if (this.scanner.tokenType === ASTERISK) { expectIdentifier = true; checkColon = false; this.scanner.next(); } else if (this.scanner.tokenType !== VERTICALLINE) { this.scanner.eat(IDENTIFIER); } if (this.scanner.tokenType === VERTICALLINE) { if (this.scanner.lookupType(1) !== EQUALSSIGN) { this.scanner.next(); this.scanner.eat(IDENTIFIER); } else if (expectIdentifier) { this.scanner.error("Identifier is expected", this.scanner.tokenEnd); } } else if (expectIdentifier) { this.scanner.error("Vertical line is expected"); } if (checkColon && this.scanner.tokenType === COLON) { this.scanner.next(); this.scanner.eat(IDENTIFIER); } return { type: "Identifier", loc: this.getLocation(start, this.scanner.tokenStart), name: this.scanner.substrToCursor(start) }; } function getOperator() { const start = this.scanner.tokenStart; const tokenType = this.scanner.tokenType; if (tokenType !== EQUALSSIGN && // = tokenType !== TILDE && // ~= tokenType !== CIRCUMFLEXACCENT && // ^= tokenType !== DOLLARSIGN && // $= tokenType !== ASTERISK && // *= tokenType !== VERTICALLINE // |= ) { this.scanner.error("Attribute selector (=, ~=, ^=, $=, *=, |=) is expected"); } if (tokenType === EQUALSSIGN) { this.scanner.next(); } else { this.scanner.next(); this.scanner.eat(EQUALSSIGN); } return this.scanner.substrToCursor(start); } // "[" S* attrib_name "]" // "[" S* attrib_name S* attrib_matcher S* [ IDENT | STRING ] S* attrib_flags? S* "]" const AttributeSelector = { name: "AttributeSelector", structure: { name: "Identifier", matcher: [String, null], value: ["String", "Identifier", null], flags: [String, null] }, parse: function () { const start = this.scanner.tokenStart; let name; let matcher = null; let value = null; let flags = null; this.scanner.eat(LEFTSQUAREBRACKET); this.scanner.skipSC(); name = getAttributeName.call(this); this.scanner.skipSC(); if (this.scanner.tokenType !== RIGHTSQUAREBRACKET) { // avoid case `[name i]` if (this.scanner.tokenType !== IDENTIFIER) { matcher = getOperator.call(this); this.scanner.skipSC(); value = this.scanner.tokenType === STRING ? this.String() : this.Identifier(); this.scanner.skipSC(); } // attribute flags if (this.scanner.tokenType === IDENTIFIER) { flags = this.scanner.getTokenValue(); this.scanner.next(); this.scanner.skipSC(); } } this.scanner.eat(RIGHTSQUAREBRACKET); return { type: "AttributeSelector", loc: this.getLocation(start, this.scanner.tokenStart), name: name, matcher: matcher, value: value, flags: flags }; }, generate: function (node) { let flagsPrefix = " "; this.chunk("["); this.node(node.name); if (node.matcher !== null) { this.chunk(node.matcher); if (node.value !== null) { this.node(node.value); // space between string and flags is not required if (node.value.type === "String") { flagsPrefix = ""; } } } if (node.flags !== null) { this.chunk(flagsPrefix); this.chunk(node.flags); } this.chunk("]"); } }; // --- function Block_consumeRaw(startToken) { return this.Raw(startToken, 0, 0, false, true); } function consumeRule() { return this.parseWithFallback(this.Rule, Block_consumeRaw); } function consumeRawDeclaration(startToken) { return this.Raw(startToken, 0, SEMICOLON, true, true); } function consumeDeclaration() { if (this.scanner.tokenType === SEMICOLON) { return consumeRawDeclaration.call(this, this.scanner.currentToken); } const node = this.parseWithFallback(this.Declaration, consumeRawDeclaration); if (this.scanner.tokenType === SEMICOLON) { this.scanner.next(); } return node; } const Block = { name: "Block", structure: { children: [[ "Atrule", "Rule", "Declaration" ]] }, parse: function (isDeclaration) { const consumer = isDeclaration ? consumeDeclaration : consumeRule; const start = this.scanner.tokenStart; const children = this.createList(); this.scanner.eat(LEFTCURLYBRACKET); scan: while (!this.scanner.eof) { switch (this.scanner.tokenType) { case RIGHTCURLYBRACKET: break scan; case WHITESPACE: case COMMENT: this.scanner.next(); break; case ATKEYWORD: children.push(this.parseWithFallback(this.Atrule, Block_consumeRaw)); break; default: children.push(consumer.call(this)); } } if (!this.scanner.eof) { this.scanner.eat(RIGHTCURLYBRACKET); } return { type: "Block", loc: this.getLocation(start, this.scanner.tokenStart), children: children }; }, generate: function (node) { this.chunk("{"); this.children(node, function (prev) { if (prev.type === "Declaration") { this.chunk(";"); } }); this.chunk("}"); }, walkContext: "block" }; // --- const Brackets = { name: "Brackets", structure: { children: [[]] }, parse: function (readSequence, recognizer) { const start = this.scanner.tokenStart; let children = null; this.scanner.eat(LEFTSQUAREBRACKET); children = readSequence.call(this, recognizer); if (!this.scanner.eof) { this.scanner.eat(RIGHTSQUAREBRACKET); } return { type: "Brackets", loc: this.getLocation(start, this.scanner.tokenStart), children: children }; }, generate: function (node) { this.chunk("["); this.children(node); this.chunk("]"); } }; // --- const Syntax_CDC = { name: "CDC", structure: [], parse: function () { const start = this.scanner.tokenStart; this.scanner.eat(TYPE_CDC); // --> return { type: "CDC", loc: this.getLocation(start, this.scanner.tokenStart) }; }, generate: function () { this.chunk("-->"); } }; // --- const Syntax_CDO = { name: "CDO", structure: [], parse: function () { const start = this.scanner.tokenStart; this.scanner.eat(TYPE_CDO); // child = this.CDC(); break; // CSS Syntax Module Level 3 // ยง2.2 Error handling // At the "top level" of a stylesheet, an starts an at-rule. case ATKEYWORD: child = this.parseWithFallback(this.Atrule, StyleSheet_consumeRaw); break; // Anything else starts a qualified rule ... default: child = this.parseWithFallback(this.Rule, StyleSheet_consumeRaw); } children.push(child); } return { type: "StyleSheet", loc: this.getLocation(start, this.scanner.tokenStart), children: children }; }, generate: function (node) { this.children(node); }, walkContext: "stylesheet" }; // --- function eatIdentifierOrAsterisk() { if (this.scanner.tokenType !== IDENTIFIER && this.scanner.tokenType !== ASTERISK) { this.scanner.error("Identifier or asterisk is expected"); } this.scanner.next(); } // ident // ident|ident // ident|* // * // *|ident // *|* // |ident // |* const TypeSelector = { name: "TypeSelector", structure: { name: String }, parse: function () { const start = this.scanner.tokenStart; if (this.scanner.tokenType === VERTICALLINE) { this.scanner.next(); eatIdentifierOrAsterisk.call(this); } else { eatIdentifierOrAsterisk.call(this); if (this.scanner.tokenType === VERTICALLINE) { this.scanner.next(); eatIdentifierOrAsterisk.call(this); } } return { type: "TypeSelector", loc: this.getLocation(start, this.scanner.tokenStart), name: this.scanner.substrToCursor(start) }; }, generate: function (node) { this.chunk(node.name); } }; // --- function scanUnicodeNumber(scanner) { for (let pos = scanner.tokenStart + 1; pos < scanner.tokenEnd; pos++) { const code = scanner.source.charCodeAt(pos); // break on fullstop or hyperminus/plussign after exponent if (code === FULLSTOP || code === PLUSSIGN) { // break token, exclude symbol scanner.tokenStart = pos; return false; } } return true; } // https://drafts.csswg.org/css-syntax-3/#urange function scanUnicodeRange(scanner) { const hexStart = scanner.tokenStart + 1; // skip + let hexLength = 0; scan: { if (scanner.tokenType === NUMBER) { if (scanner.source.charCodeAt(scanner.tokenStart) !== FULLSTOP && scanUnicodeNumber(scanner)) { scanner.next(); } else if (scanner.source.charCodeAt(scanner.tokenStart) !== HYPHENMINUS) { break scan; } } else { scanner.next(); // PLUSSIGN } if (scanner.tokenType === HYPHENMINUS) { scanner.next(); } if (scanner.tokenType === NUMBER) { scanner.next(); } if (scanner.tokenType === IDENTIFIER) { scanner.next(); } if (scanner.tokenStart === hexStart) { scanner.error("Unexpected input", hexStart); } } // validate for U+x{1,6} or U+x{1,6}-x{1,6} // where x is [0-9a-fA-F] let i; let wasHyphenMinus = false; for (i = hexStart; i < scanner.tokenStart; i++) { const code = scanner.source.charCodeAt(i); if (isHex(code) === false && (code !== HYPHENMINUS || wasHyphenMinus)) { scanner.error("Unexpected input", i); } if (code === HYPHENMINUS) { // hex sequence shouldn"t be an empty if (hexLength === 0) { scanner.error("Unexpected input", i); } wasHyphenMinus = true; hexLength = 0; } else { hexLength++; // too long hex sequence if (hexLength > 6) { scanner.error("Too long hex sequence", i); } } } // check we have a non-zero sequence if (hexLength === 0) { scanner.error("Unexpected input", i - 1); } // U+abc??? if (!wasHyphenMinus) { // consume as many U+003F QUESTION MARK (?) code points as possible for (; hexLength < 6 && !scanner.eof; scanner.next()) { if (scanner.tokenType !== QUESTIONMARK) { break; } hexLength++; } } } const UnicodeRange = { name: "UnicodeRange", structure: { value: String }, parse: function () { const start = this.scanner.tokenStart; this.scanner.next(); // U or u scanUnicodeRange(this.scanner); return { type: "UnicodeRange", loc: this.getLocation(start, this.scanner.tokenStart), value: this.scanner.substrToCursor(start) }; }, generate: function (node) { this.chunk(node.value); } }; // --- // url "(" S* (string | raw) S* ")" const Url = { name: "Url", structure: { value: ["String", "Raw"] }, parse: function () { const start = this.scanner.tokenStart; let value; this.scanner.eat(URL); this.scanner.skipSC(); switch (this.scanner.tokenType) { case STRING: value = this.String(); break; case RAW: value = this.Raw(this.scanner.currentToken, 0, RAW, true, false); break; default: this.scanner.error("String or Raw is expected"); } this.scanner.skipSC(); this.scanner.eat(RIGHTPARENTHESIS); return { type: "Url", loc: this.getLocation(start, this.scanner.tokenStart), value: value }; }, generate: function (node) { this.chunk("url"); this.chunk("("); this.node(node.value); this.chunk(")"); } }; // --- const Syntax_Value = { name: "Value", structure: { children: [[]] }, parse: function () { const start = this.scanner.tokenStart; const children = this.readSequence(this.scope.Value); return { type: "Value", loc: this.getLocation(start, this.scanner.tokenStart), children: children }; }, generate: function (node) { this.children(node); } }; // --- const WhiteSpace_SPACE = Object.freeze({ type: "WhiteSpace", loc: null, value: " " }); const WhiteSpace = { name: "WhiteSpace", structure: { value: String }, parse: function () { this.scanner.eat(WHITESPACE); return WhiteSpace_SPACE; // return { // type: "WhiteSpace", // loc: this.getLocation(this.scanner.tokenStart, this.scanner.tokenEnd), // value: this.scanner.consume(WHITESPACE) // }; }, generate: function (node) { this.chunk(node.value); } }; // --- function processChildren(node, delimeter) { const list = node.children; let prev = null; if (typeof delimeter !== "function") { list.forEach(this.node, this); } else { list.forEach(function (node) { if (prev !== null) { delimeter.call(this, prev); } this.node(node); prev = node; }, this); } } function createGenerator(config) { function processNode(node) { if (hasOwnProperty.call(types, node.type)) { types[node.type].call(this, node); } else { throw new Error("Unknown node type: " + node.type); } } const types = {}; if (config.node) { for (const name in config.node) { types[name] = config.node[name].generate; } } return function (node, options) { let buffer = ""; let handlers = { children: processChildren, node: processNode, chunk: function (chunk) { buffer += chunk; }, result: function () { return buffer; } }; if (options) { if (typeof options.decorator === "function") { handlers = options.decorator(handlers); } } handlers.node(node); return handlers.result(); }; } // --- const node = { AnPlusB: AnPlusB, Atrule: Atrule, AtrulePrelude: Syntax_AtrulePrelude, AttributeSelector: AttributeSelector, Block: Block, Brackets: Brackets, CDC: Syntax_CDC, CDO: Syntax_CDO, ClassSelector: ClassSelector, Combinator: Combinator, Comment: Syntax_Comment, Declaration: Declaration, DeclarationList: DeclarationList, Dimension: Dimension, Function: Syntax_Function, HexColor: HexColor, Identifier: Identifier, IdSelector: IdSelector, MediaFeature: MediaFeature, MediaQuery: MediaQuery, MediaQueryList: MediaQueryList, Nth: Nth, Number: Syntax_Number, Operator: Operator, Parentheses: Parentheses, Percentage: Percentage, PseudoClassSelector: PseudoClassSelector, PseudoElementSelector: PseudoElementSelector, Ratio: Ratio, Raw: Raw, Rule: Rule, Selector: Syntax_Selector, SelectorList: SelectorList, String: Syntax_String, StyleSheet: Syntax_StyleSheet, TypeSelector: TypeSelector, UnicodeRange: UnicodeRange, Url: Url, Value: Syntax_Value, WhiteSpace: WhiteSpace }; // --- const config = { parseContext: { default: "StyleSheet", stylesheet: "StyleSheet", atrule: "Atrule", atrulePrelude: function (options) { return this.AtrulePrelude(options.atrule ? String(options.atrule) : null); }, mediaQueryList: "MediaQueryList", mediaQuery: "MediaQuery", rule: "Rule", selectorList: "SelectorList", selector: "Selector", block: function () { return this.Block(true); }, declarationList: "DeclarationList", declaration: "Declaration", value: "Value" }, scope: scope, atrule: atrule, pseudo: pseudo, node: node }; return { parse: createParser(config), generate: createGenerator(config) }; })();