mirror of
https://github.com/microsoft/playwright.git
synced 2025-06-26 21:40:17 +00:00
feat(selectors): introduce css parser (#4522)
It is not used for anything just yet.
This commit is contained in:
parent
84dc441a9e
commit
e98aceb981
238
src/server/common/cssParser.ts
Normal file
238
src/server/common/cssParser.ts
Normal file
@ -0,0 +1,238 @@
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
import * as css from './cssTokenizer';
|
||||
|
||||
// TODO: Consider giving more information, e.g. whether the argument was a quoted string or not.
|
||||
type ParsedSelectorLiteral = string;
|
||||
type ClauseCombinator = '' | '>' | '+' | '~';
|
||||
export type ParsedSelectorClause = ParsedSelectorLiteral | { css?: string, funcs: { name: string, args: ParsedSelectorList }[] };
|
||||
export type ParsedSelector = { clauses: { clause: ParsedSelectorClause, combinator: ClauseCombinator }[] };
|
||||
export type ParsedSelectorList = (ParsedSelectorLiteral | ParsedSelector)[];
|
||||
|
||||
export function parseCSS(selector: string): ParsedSelectorList {
|
||||
let tokens: css.CSSTokenInterface[];
|
||||
try {
|
||||
tokens = css.tokenize(selector);
|
||||
if (!(tokens[tokens.length - 1] instanceof css.EOFToken))
|
||||
tokens.push(new css.EOFToken());
|
||||
} catch (e) {
|
||||
const newMessage = e.message + ` while parsing selector "${selector}"`;
|
||||
const index = (e.stack || '').indexOf(e.message);
|
||||
if (index !== -1)
|
||||
e.stack = e.stack.substring(0, index) + newMessage + e.stack.substring(index + e.message.length);
|
||||
e.message = newMessage;
|
||||
throw e;
|
||||
}
|
||||
const unsupportedToken = tokens.find(token => {
|
||||
return (token instanceof css.AtKeywordToken) ||
|
||||
(token instanceof css.BadStringToken) ||
|
||||
(token instanceof css.BadURLToken) ||
|
||||
(token instanceof css.ColumnToken) ||
|
||||
(token instanceof css.CDOToken) ||
|
||||
(token instanceof css.CDCToken) ||
|
||||
(token instanceof css.SemicolonToken) ||
|
||||
// TODO: Consider using these for something, e.g. to escape complex strings.
|
||||
// For example :xpath{ (//div/bar[@attr="foo"])[2]/baz }
|
||||
// Or this way :xpath( {complex-xpath-goes-here("hello")} )
|
||||
(token instanceof css.OpenCurlyToken) ||
|
||||
(token instanceof css.CloseCurlyToken) ||
|
||||
// TODO: Consider treating these as strings?
|
||||
(token instanceof css.URLToken) ||
|
||||
(token instanceof css.PercentageToken);
|
||||
});
|
||||
if (unsupportedToken)
|
||||
throw new Error(`Unsupported token "${unsupportedToken.toSource()}" while parsing selector "${selector}"`);
|
||||
|
||||
let pos = 0;
|
||||
|
||||
function unexpected() {
|
||||
return new Error(`Unexpected token "${tokens[pos].toSource()}" while parsing selector "${selector}"`);
|
||||
}
|
||||
|
||||
function skipWhitespace() {
|
||||
while (tokens[pos] instanceof css.WhitespaceToken)
|
||||
pos++;
|
||||
}
|
||||
|
||||
function isIdent(p = pos) {
|
||||
return tokens[p] instanceof css.IdentToken;
|
||||
}
|
||||
|
||||
function isString(p = pos) {
|
||||
return tokens[p] instanceof css.StringToken;
|
||||
}
|
||||
|
||||
function isNumber(p = pos) {
|
||||
return tokens[p] instanceof css.NumberToken;
|
||||
}
|
||||
|
||||
function isComma(p = pos) {
|
||||
return tokens[p] instanceof css.CommaToken;
|
||||
}
|
||||
|
||||
function isCloseParen(p = pos) {
|
||||
return tokens[p] instanceof css.CloseParenToken;
|
||||
}
|
||||
|
||||
function isStar(p = pos) {
|
||||
return (tokens[p] instanceof css.DelimToken) && tokens[p].value === '*';
|
||||
}
|
||||
|
||||
function isEOF(p = pos) {
|
||||
return tokens[p] instanceof css.EOFToken;
|
||||
}
|
||||
|
||||
function isClauseCombinator(p = pos) {
|
||||
return (tokens[p] instanceof css.DelimToken) && (['>', '+', '~'].includes(tokens[p].value));
|
||||
}
|
||||
|
||||
function isSelectorClauseEnd(p = pos) {
|
||||
return isComma(p) || isCloseParen(p) || isEOF(p) || isClauseCombinator(p) || (tokens[p] instanceof css.WhitespaceToken);
|
||||
}
|
||||
|
||||
function consumeSelectorList(): ParsedSelectorList {
|
||||
const result = [consumeSelector()];
|
||||
while (true) {
|
||||
skipWhitespace();
|
||||
if (!isComma())
|
||||
break;
|
||||
pos++;
|
||||
result.push(consumeSelector());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function consumeSelector(): ParsedSelector | ParsedSelectorLiteral {
|
||||
skipWhitespace();
|
||||
const result = { clauses: [{ clause: consumeSelectorClause(), combinator: '' as ClauseCombinator }] };
|
||||
while (true) {
|
||||
skipWhitespace();
|
||||
if (isClauseCombinator()) {
|
||||
result.clauses[result.clauses.length - 1].combinator = tokens[pos++].value as ClauseCombinator;
|
||||
skipWhitespace();
|
||||
} else if (isSelectorClauseEnd()) {
|
||||
break;
|
||||
}
|
||||
result.clauses.push({ combinator: '', clause: consumeSelectorClause() });
|
||||
}
|
||||
if (result.clauses.length === 1 && typeof result.clauses[0].clause === 'string')
|
||||
return result.clauses[0].clause;
|
||||
return result;
|
||||
}
|
||||
|
||||
function consumeSelectorClause(): ParsedSelectorClause {
|
||||
// TODO: Consider symbols like `=`, `|=`, `~=`, `*=`, `/` and convert them to strings.
|
||||
if ((isNumber() || isString() || isStar() || isIdent()) && isSelectorClauseEnd(pos + 1))
|
||||
return isString() ? tokens[pos++].value : tokens[pos++].toSource();
|
||||
|
||||
let rawCSSString = '';
|
||||
const funcs: { name: string, args: ParsedSelectorList }[] = [];
|
||||
|
||||
while (!isSelectorClauseEnd()) {
|
||||
if (isIdent() || isStar()) {
|
||||
rawCSSString += tokens[pos++].toSource();
|
||||
} else if (tokens[pos] instanceof css.HashToken) {
|
||||
rawCSSString += tokens[pos++].toSource();
|
||||
} else if ((tokens[pos] instanceof css.DelimToken) && tokens[pos].value === '.') {
|
||||
pos++;
|
||||
if (isIdent())
|
||||
rawCSSString += '.' + tokens[pos++].toSource();
|
||||
else
|
||||
throw unexpected();
|
||||
} else if (tokens[pos] instanceof css.ColonToken) {
|
||||
pos++;
|
||||
if (isIdent()) {
|
||||
if (cssFilters.has(tokens[pos].value))
|
||||
rawCSSString += ':' + tokens[pos++].toSource();
|
||||
else
|
||||
funcs.push({ name: tokens[pos++].value, args: [] });
|
||||
} else if (tokens[pos] instanceof css.FunctionToken) {
|
||||
const name = tokens[pos++].value;
|
||||
if (cssFunctions.has(name))
|
||||
rawCSSString += `:${name}(${consumeCSSFunctionArgs()})`;
|
||||
else
|
||||
funcs.push({ name, args: consumeSelectorList() });
|
||||
skipWhitespace();
|
||||
if (!isCloseParen())
|
||||
throw unexpected();
|
||||
pos++;
|
||||
} else {
|
||||
throw unexpected();
|
||||
}
|
||||
} else if (tokens[pos] instanceof css.OpenSquareToken) {
|
||||
rawCSSString += '[';
|
||||
pos++;
|
||||
while (!(tokens[pos] instanceof css.CloseSquareToken) && !isEOF())
|
||||
rawCSSString += tokens[pos++].toSource();
|
||||
if (!(tokens[pos] instanceof css.CloseSquareToken))
|
||||
throw unexpected();
|
||||
rawCSSString += ']';
|
||||
pos++;
|
||||
} else {
|
||||
throw unexpected();
|
||||
}
|
||||
}
|
||||
if (!rawCSSString && !funcs.length)
|
||||
throw unexpected();
|
||||
return { css: rawCSSString || undefined, funcs };
|
||||
}
|
||||
|
||||
function consumeCSSFunctionArgs(): string {
|
||||
let s = '';
|
||||
while (!isCloseParen() && !isEOF())
|
||||
s += tokens[pos++].toSource();
|
||||
return s;
|
||||
}
|
||||
|
||||
const result = consumeSelectorList();
|
||||
if (!isEOF())
|
||||
throw new Error(`Error while parsing selector "${selector}"`);
|
||||
return result;
|
||||
}
|
||||
|
||||
export function serializeSelector(selectorList: ParsedSelectorList) {
|
||||
return selectorList.map(selector => {
|
||||
if (typeof selector === 'string')
|
||||
return selector;
|
||||
return selector.clauses.map(({ clause, combinator }) => {
|
||||
let s = '';
|
||||
if (typeof clause === 'string') {
|
||||
s = clause;
|
||||
} else {
|
||||
if (clause.css)
|
||||
s = clause.css;
|
||||
s = s + clause.funcs.map(func => `:${func.name}(${serializeSelector(func.args)})`).join('');
|
||||
}
|
||||
if (combinator)
|
||||
s += ' ' + combinator;
|
||||
return s;
|
||||
}).join(' ');
|
||||
}).join(', ');
|
||||
}
|
||||
|
||||
const cssFilters = new Set([
|
||||
'active', 'any-link', 'checked', 'blank', 'default', 'defined',
|
||||
'disabled', 'empty', 'enabled', 'first', 'first-child', 'first-of-type',
|
||||
'fullscreen', 'focus', 'focus-visible', 'focus-within', 'hover',
|
||||
'indeterminate', 'in-range', 'invalid', 'last-child', 'last-of-type',
|
||||
'link', 'only-child', 'only-of-type', 'optional', 'out-of-range', 'placeholder-shown',
|
||||
'read-only', 'read-write', 'required', 'root', 'target', 'valid', 'visited',
|
||||
]);
|
||||
|
||||
const cssFunctions = new Set([
|
||||
'dir', 'lang', 'nth-child', 'nth-last-child', 'nth-last-of-type', 'nth-of-type',
|
||||
]);
|
||||
54
src/server/common/cssTokenizer.d.ts
vendored
Normal file
54
src/server/common/cssTokenizer.d.ts
vendored
Normal file
@ -0,0 +1,54 @@
|
||||
/*
|
||||
* 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 {}
|
||||
950
src/server/common/cssTokenizer.js
Normal file
950
src/server/common/cssTokenizer.js
Normal file
@ -0,0 +1,950 @@
|
||||
/*
|
||||
* 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.
|
||||
// - Everything not related to tokenizing - below the first exports block - is removed.
|
||||
|
||||
// @ts-nocheck
|
||||
|
||||
(function (root, factory) {
|
||||
// Universal Module Definition (UMD) to support AMD, CommonJS/Node.js,
|
||||
// Rhino, and plain browser loading.
|
||||
if (typeof define === 'function' && define.amd) {
|
||||
define(['exports'], factory);
|
||||
} else if (typeof exports !== 'undefined') {
|
||||
factory(exports);
|
||||
} else {
|
||||
factory(root);
|
||||
}
|
||||
}(this, function (exports) {
|
||||
|
||||
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 CDCToken() { return this; }
|
||||
CDCToken.prototype = Object.create(CSSParserToken.prototype);
|
||||
CDCToken.prototype.tokenType = "CDC";
|
||||
CDCToken.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;
|
||||
|
||||
}));
|
||||
99
test/css-parser.spec.ts
Normal file
99
test/css-parser.spec.ts
Normal file
@ -0,0 +1,99 @@
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
import { it, expect } from './fixtures';
|
||||
import * as path from 'path';
|
||||
|
||||
const { parseCSS, serializeSelector: serialize } =
|
||||
require(path.join(__dirname, '..', 'lib', 'server', 'common', 'cssParser'));
|
||||
|
||||
it('should parse css', async () => {
|
||||
expect(serialize(parseCSS('div'))).toBe('div');
|
||||
expect(serialize(parseCSS('div.class'))).toBe('div.class');
|
||||
expect(serialize(parseCSS('.class'))).toBe('.class');
|
||||
expect(serialize(parseCSS('#id'))).toBe('#id');
|
||||
expect(serialize(parseCSS('.class#id'))).toBe('.class#id');
|
||||
expect(serialize(parseCSS('div#id.class'))).toBe('div#id.class');
|
||||
expect(serialize(parseCSS('*'))).toBe('*');
|
||||
expect(serialize(parseCSS('*div'))).toBe('*div');
|
||||
expect(serialize(parseCSS('div[attr *= foo i]'))).toBe('div[attr *= foo i]');
|
||||
expect(serialize(parseCSS('div[attr~="Bar baz" ]'))).toBe('div[attr~="Bar baz" ]');
|
||||
expect(serialize(parseCSS(`div [ foo = 'bar' s]`))).toBe(`div [ foo = "bar" s]`);
|
||||
|
||||
expect(serialize(parseCSS(':hover'))).toBe(':hover');
|
||||
expect(serialize(parseCSS('div:hover'))).toBe('div:hover');
|
||||
expect(serialize(parseCSS('#id:active:hover'))).toBe('#id:active:hover');
|
||||
expect(serialize(parseCSS(':dir(ltr)'))).toBe(':dir(ltr)');
|
||||
expect(serialize(parseCSS('#foo-bar.cls:nth-child(3n + 10)'))).toBe('#foo-bar.cls:nth-child(3n + 10)');
|
||||
expect(serialize(parseCSS(':lang(en)'))).toBe(':lang(en)');
|
||||
expect(serialize(parseCSS('*:hover'))).toBe('*:hover');
|
||||
|
||||
expect(serialize(parseCSS('div span'))).toBe('div span');
|
||||
expect(serialize(parseCSS('div>span'))).toBe('div > span');
|
||||
expect(serialize(parseCSS('div +span'))).toBe('div + span');
|
||||
expect(serialize(parseCSS('div~ span'))).toBe('div ~ span');
|
||||
expect(serialize(parseCSS('div >.class #id+ span'))).toBe('div > .class #id + span');
|
||||
expect(serialize(parseCSS('div>span+.class'))).toBe('div > span + .class');
|
||||
|
||||
expect(serialize(parseCSS('div:not(span)'))).toBe('div:not(span)');
|
||||
expect(serialize(parseCSS(':not(span)#id'))).toBe('#id:not(span)');
|
||||
expect(serialize(parseCSS('div:not(span):hover'))).toBe('div:hover:not(span)');
|
||||
expect(serialize(parseCSS('div:has(span):hover'))).toBe('div:hover:has(span)');
|
||||
expect(serialize(parseCSS('div:right-of(span):hover'))).toBe('div:hover:right-of(span)');
|
||||
expect(serialize(parseCSS(':right-of(span):react(foobar)'))).toBe(':right-of(span):react(foobar)');
|
||||
expect(serialize(parseCSS('div:is(span):hover'))).toBe('div:hover:is(span)');
|
||||
expect(serialize(parseCSS('div:scope:hover'))).toBe('div:hover:scope()');
|
||||
|
||||
expect(serialize(parseCSS(':text("foo")'))).toBe(':text(foo)');
|
||||
expect(serialize(parseCSS(':text("*")'))).toBe(':text(*)');
|
||||
expect(serialize(parseCSS(':text(*)'))).toBe(':text(*)');
|
||||
expect(serialize(parseCSS(':text("foo", normalize-space)'))).toBe(':text(foo, normalize-space)');
|
||||
expect(serialize(parseCSS(':index(3, div span)'))).toBe(':index(3, div span)');
|
||||
expect(serialize(parseCSS(':is(foo, bar>baz.cls+:not(qux))'))).toBe(':is(foo, bar > baz.cls + :not(qux))');
|
||||
// expect(serialize(parseCSS(':right-of(div, bar=50)'))).toBe(':right-of(div, bar=50)');
|
||||
});
|
||||
|
||||
it('should throw on malformed css', async () => {
|
||||
function expectError(selector: string) {
|
||||
let error = { message: '' };
|
||||
try {
|
||||
parseCSS(selector);
|
||||
} catch (e) {
|
||||
error = e;
|
||||
}
|
||||
expect(error.message).toContain(`while parsing selector "${selector}"`);
|
||||
}
|
||||
|
||||
expectError('');
|
||||
expectError('.');
|
||||
expectError('#');
|
||||
expectError('..');
|
||||
expectError('#.');
|
||||
expectError('.#');
|
||||
expectError('[attr=');
|
||||
expectError(':not(div');
|
||||
expectError('div)');
|
||||
expectError('()');
|
||||
expectError(':not(##)');
|
||||
expectError(':not()');
|
||||
expectError(':not(.)');
|
||||
expectError('div,');
|
||||
expectError(',div');
|
||||
expectError('div,,span');
|
||||
expectError('div > > span');
|
||||
expectError('div > > > > span');
|
||||
expectError('div >');
|
||||
});
|
||||
@ -131,3 +131,39 @@ it('should work with numerical id', async ({page, server}) => {
|
||||
const element = await page.$('#\\31\\32\\33');
|
||||
expect(element).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should work with :nth-child', async ({page, server}) => {
|
||||
await page.goto(server.PREFIX + '/deep-shadow.html');
|
||||
expect(await page.$$eval(`css=span:nth-child(odd)`, els => els.length)).toBe(3);
|
||||
expect(await page.$$eval(`css=span:nth-child(even)`, els => els.length)).toBe(1);
|
||||
expect(await page.$$eval(`css=span:nth-child(n+1)`, els => els.length)).toBe(4);
|
||||
expect(await page.$$eval(`css=span:nth-child(n+2)`, els => els.length)).toBe(1);
|
||||
expect(await page.$$eval(`css=span:nth-child(2n)`, els => els.length)).toBe(1);
|
||||
expect(await page.$$eval(`css=span:nth-child(2n+1)`, els => els.length)).toBe(3);
|
||||
expect(await page.$$eval(`css=span:nth-child(-n)`, els => els.length)).toBe(0);
|
||||
expect(await page.$$eval(`css=span:nth-child(-n+1)`, els => els.length)).toBe(3);
|
||||
expect(await page.$$eval(`css=span:nth-child(-n+2)`, els => els.length)).toBe(4);
|
||||
expect(await page.$$eval(`css=span:nth-child(23n+2)`, els => els.length)).toBe(1);
|
||||
});
|
||||
|
||||
it('should work with :not', async ({page, server}) => {
|
||||
await page.goto(server.PREFIX + '/deep-shadow.html');
|
||||
expect(await page.$$eval(`css=div:not(#root1)`, els => els.length)).toBe(2);
|
||||
expect(await page.$$eval(`css=body :not(span)`, els => els.length)).toBe(4);
|
||||
expect(await page.$$eval(`css=div > :not(span):not(div)`, els => els.length)).toBe(0);
|
||||
});
|
||||
|
||||
it('should work with spaces in :nth-child and :not', test => {
|
||||
test.fixme('Our selector parser is broken');
|
||||
}, async ({page, server}) => {
|
||||
await page.goto(server.PREFIX + '/deep-shadow.html');
|
||||
expect(await page.$$eval(`css=span:nth-child(23n +2)`, els => els.length)).toBe(1);
|
||||
expect(await page.$$eval(`css=span:nth-child(23n+ 2)`, els => els.length)).toBe(1);
|
||||
expect(await page.$$eval(`css=span:nth-child( 23n + 2 )`, els => els.length)).toBe(1);
|
||||
expect(await page.$$eval(`css=span:not(#root1 #target)`, els => els.length)).toBe(3);
|
||||
expect(await page.$$eval(`css=span:not(:not(#root1 #target))`, els => els.length)).toBe(1);
|
||||
expect(await page.$$eval(`css=span:not(span:not(#root1 #target))`, els => els.length)).toBe(1);
|
||||
expect(await page.$$eval(`css=div > :not(span)`, els => els.length)).toBe(2);
|
||||
expect(await page.$$eval(`css=body :not(span, div)`, els => els.length)).toBe(1);
|
||||
expect(await page.$$eval(`css=span, section:not(span, div)`, els => els.length)).toBe(5);
|
||||
});
|
||||
|
||||
@ -7,9 +7,10 @@
|
||||
"rootDir": "./src",
|
||||
"outDir": "./lib",
|
||||
"strict": true,
|
||||
"allowJs": true,
|
||||
"declaration": false
|
||||
},
|
||||
"compileOnSave": true,
|
||||
"include": ["src/**/*.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
"include": ["src/**/*.ts", "src/**/*.js"],
|
||||
"exclude": ["node_modules", "src/.eslintrc.js"]
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user