Primeiro commit do projeto Angular
This commit is contained in:
+11
@@ -0,0 +1,11 @@
|
||||
Copyright (c) Felix Böhm
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
|
||||
|
||||
Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
|
||||
|
||||
Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
|
||||
|
||||
THIS IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS,
|
||||
EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
export * from "./types.js";
|
||||
export { isTraversal, parse } from "./parse.js";
|
||||
export { stringify } from "./stringify.js";
|
||||
//# sourceMappingURL=index.d.ts.map
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAC;AAC3B,OAAO,EAAE,WAAW,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAChD,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC"}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
"use strict";
|
||||
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
var desc = Object.getOwnPropertyDescriptor(m, k);
|
||||
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
||||
desc = { enumerable: true, get: function() { return m[k]; } };
|
||||
}
|
||||
Object.defineProperty(o, k2, desc);
|
||||
}) : (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
o[k2] = m[k];
|
||||
}));
|
||||
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
||||
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.stringify = exports.parse = exports.isTraversal = void 0;
|
||||
__exportStar(require("./types.js"), exports);
|
||||
var parse_js_1 = require("./parse.js");
|
||||
Object.defineProperty(exports, "isTraversal", { enumerable: true, get: function () { return parse_js_1.isTraversal; } });
|
||||
Object.defineProperty(exports, "parse", { enumerable: true, get: function () { return parse_js_1.parse; } });
|
||||
var stringify_js_1 = require("./stringify.js");
|
||||
Object.defineProperty(exports, "stringify", { enumerable: true, get: function () { return stringify_js_1.stringify; } });
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"type": "commonjs"
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
import { type Selector, type Traversal } from "./types.js";
|
||||
/**
|
||||
* Checks whether a specific selector is a traversal.
|
||||
* This is useful eg. in swapping the order of elements that
|
||||
* are not traversals.
|
||||
*
|
||||
* @param selector Selector to check.
|
||||
*/
|
||||
export declare function isTraversal(selector: Selector): selector is Traversal;
|
||||
/**
|
||||
* Parses `selector`.
|
||||
*
|
||||
* @param selector Selector to parse.
|
||||
* @returns Returns a two-dimensional array.
|
||||
* The first dimension represents selectors separated by commas (eg. `sub1, sub2`),
|
||||
* the second contains the relevant tokens for that selector.
|
||||
*/
|
||||
export declare function parse(selector: string): Selector[][];
|
||||
//# sourceMappingURL=parse.d.ts.map
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"parse.d.ts","sourceRoot":"","sources":["../../src/parse.ts"],"names":[],"mappings":"AAAA,OAAO,EACH,KAAK,QAAQ,EAGb,KAAK,SAAS,EAIjB,MAAM,YAAY,CAAC;AAyEpB;;;;;;GAMG;AACH,wBAAgB,WAAW,CAAC,QAAQ,EAAE,QAAQ,GAAG,QAAQ,IAAI,SAAS,CAcrE;AAuCD;;;;;;;GAOG;AACH,wBAAgB,KAAK,CAAC,QAAQ,EAAE,MAAM,GAAG,QAAQ,EAAE,EAAE,CAUpD"}
|
||||
+489
@@ -0,0 +1,489 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.isTraversal = isTraversal;
|
||||
exports.parse = parse;
|
||||
const types_js_1 = require("./types.js");
|
||||
const reName = /^[^#\\]?(?:\\(?:[\da-f]{1,6}\s?|.)|[\w\u00B0-\uFFFF-])+/;
|
||||
const reEscape = /\\([\da-f]{1,6}\s?|(\s)|.)/gi;
|
||||
var CharCode;
|
||||
(function (CharCode) {
|
||||
CharCode[CharCode["LeftParenthesis"] = 40] = "LeftParenthesis";
|
||||
CharCode[CharCode["RightParenthesis"] = 41] = "RightParenthesis";
|
||||
CharCode[CharCode["LeftSquareBracket"] = 91] = "LeftSquareBracket";
|
||||
CharCode[CharCode["RightSquareBracket"] = 93] = "RightSquareBracket";
|
||||
CharCode[CharCode["Comma"] = 44] = "Comma";
|
||||
CharCode[CharCode["Period"] = 46] = "Period";
|
||||
CharCode[CharCode["Colon"] = 58] = "Colon";
|
||||
CharCode[CharCode["SingleQuote"] = 39] = "SingleQuote";
|
||||
CharCode[CharCode["DoubleQuote"] = 34] = "DoubleQuote";
|
||||
CharCode[CharCode["Plus"] = 43] = "Plus";
|
||||
CharCode[CharCode["Tilde"] = 126] = "Tilde";
|
||||
CharCode[CharCode["QuestionMark"] = 63] = "QuestionMark";
|
||||
CharCode[CharCode["ExclamationMark"] = 33] = "ExclamationMark";
|
||||
CharCode[CharCode["Slash"] = 47] = "Slash";
|
||||
CharCode[CharCode["Equal"] = 61] = "Equal";
|
||||
CharCode[CharCode["Dollar"] = 36] = "Dollar";
|
||||
CharCode[CharCode["Pipe"] = 124] = "Pipe";
|
||||
CharCode[CharCode["Circumflex"] = 94] = "Circumflex";
|
||||
CharCode[CharCode["Asterisk"] = 42] = "Asterisk";
|
||||
CharCode[CharCode["GreaterThan"] = 62] = "GreaterThan";
|
||||
CharCode[CharCode["LessThan"] = 60] = "LessThan";
|
||||
CharCode[CharCode["Hash"] = 35] = "Hash";
|
||||
CharCode[CharCode["LowerI"] = 105] = "LowerI";
|
||||
CharCode[CharCode["LowerS"] = 115] = "LowerS";
|
||||
CharCode[CharCode["BackSlash"] = 92] = "BackSlash";
|
||||
// Whitespace
|
||||
CharCode[CharCode["Space"] = 32] = "Space";
|
||||
CharCode[CharCode["Tab"] = 9] = "Tab";
|
||||
CharCode[CharCode["NewLine"] = 10] = "NewLine";
|
||||
CharCode[CharCode["FormFeed"] = 12] = "FormFeed";
|
||||
CharCode[CharCode["CarriageReturn"] = 13] = "CarriageReturn";
|
||||
})(CharCode || (CharCode = {}));
|
||||
const actionTypes = new Map([
|
||||
[CharCode.Tilde, types_js_1.AttributeAction.Element],
|
||||
[CharCode.Circumflex, types_js_1.AttributeAction.Start],
|
||||
[CharCode.Dollar, types_js_1.AttributeAction.End],
|
||||
[CharCode.Asterisk, types_js_1.AttributeAction.Any],
|
||||
[CharCode.ExclamationMark, types_js_1.AttributeAction.Not],
|
||||
[CharCode.Pipe, types_js_1.AttributeAction.Hyphen],
|
||||
]);
|
||||
// Pseudos, whose data property is parsed as well.
|
||||
const unpackPseudos = new Set([
|
||||
"has",
|
||||
"not",
|
||||
"matches",
|
||||
"is",
|
||||
"where",
|
||||
"host",
|
||||
"host-context",
|
||||
]);
|
||||
/**
|
||||
* Pseudo elements defined in CSS Level 1 and CSS Level 2 can be written with
|
||||
* a single colon; eg. :before will turn into ::before.
|
||||
*
|
||||
* @see {@link https://www.w3.org/TR/2018/WD-selectors-4-20181121/#pseudo-element-syntax}
|
||||
*/
|
||||
const pseudosToPseudoElements = new Set([
|
||||
"before",
|
||||
"after",
|
||||
"first-line",
|
||||
"first-letter",
|
||||
]);
|
||||
/**
|
||||
* Checks whether a specific selector is a traversal.
|
||||
* This is useful eg. in swapping the order of elements that
|
||||
* are not traversals.
|
||||
*
|
||||
* @param selector Selector to check.
|
||||
*/
|
||||
function isTraversal(selector) {
|
||||
switch (selector.type) {
|
||||
case types_js_1.SelectorType.Adjacent:
|
||||
case types_js_1.SelectorType.Child:
|
||||
case types_js_1.SelectorType.Descendant:
|
||||
case types_js_1.SelectorType.Parent:
|
||||
case types_js_1.SelectorType.Sibling:
|
||||
case types_js_1.SelectorType.ColumnCombinator: {
|
||||
return true;
|
||||
}
|
||||
default: {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
const stripQuotesFromPseudos = new Set(["contains", "icontains"]);
|
||||
// Unescape function taken from https://github.com/jquery/sizzle/blob/master/src/sizzle.js#L152
|
||||
function funescape(_, escaped, escapedWhitespace) {
|
||||
const high = Number.parseInt(escaped, 16) - 65536;
|
||||
// NaN means non-codepoint
|
||||
return high !== high || escapedWhitespace
|
||||
? escaped
|
||||
: high < 0
|
||||
? // BMP codepoint
|
||||
String.fromCharCode(high + 65536)
|
||||
: // Supplemental Plane codepoint (surrogate pair)
|
||||
String.fromCharCode((high >> 10) | 55296, (high & 1023) | 56320);
|
||||
}
|
||||
function unescapeCSS(cssString) {
|
||||
return cssString.replace(reEscape, funescape);
|
||||
}
|
||||
function isQuote(c) {
|
||||
return c === CharCode.SingleQuote || c === CharCode.DoubleQuote;
|
||||
}
|
||||
function isWhitespace(c) {
|
||||
return (c === CharCode.Space ||
|
||||
c === CharCode.Tab ||
|
||||
c === CharCode.NewLine ||
|
||||
c === CharCode.FormFeed ||
|
||||
c === CharCode.CarriageReturn);
|
||||
}
|
||||
/**
|
||||
* Parses `selector`.
|
||||
*
|
||||
* @param selector Selector to parse.
|
||||
* @returns Returns a two-dimensional array.
|
||||
* The first dimension represents selectors separated by commas (eg. `sub1, sub2`),
|
||||
* the second contains the relevant tokens for that selector.
|
||||
*/
|
||||
function parse(selector) {
|
||||
const subselects = [];
|
||||
const endIndex = parseSelector(subselects, `${selector}`, 0);
|
||||
if (endIndex < selector.length) {
|
||||
throw new Error(`Unmatched selector: ${selector.slice(endIndex)}`);
|
||||
}
|
||||
return subselects;
|
||||
}
|
||||
function parseSelector(subselects, selector, selectorIndex) {
|
||||
let tokens = [];
|
||||
function getName(offset) {
|
||||
const match = selector.slice(selectorIndex + offset).match(reName);
|
||||
if (!match) {
|
||||
throw new Error(`Expected name, found ${selector.slice(selectorIndex)}`);
|
||||
}
|
||||
const [name] = match;
|
||||
selectorIndex += offset + name.length;
|
||||
return unescapeCSS(name);
|
||||
}
|
||||
function stripWhitespace(offset) {
|
||||
selectorIndex += offset;
|
||||
while (selectorIndex < selector.length &&
|
||||
isWhitespace(selector.charCodeAt(selectorIndex))) {
|
||||
selectorIndex++;
|
||||
}
|
||||
}
|
||||
function readValueWithParenthesis() {
|
||||
selectorIndex += 1;
|
||||
const start = selectorIndex;
|
||||
for (let counter = 1; selectorIndex < selector.length; selectorIndex++) {
|
||||
switch (selector.charCodeAt(selectorIndex)) {
|
||||
case CharCode.BackSlash: {
|
||||
// Skip next character
|
||||
selectorIndex += 1;
|
||||
break;
|
||||
}
|
||||
case CharCode.LeftParenthesis: {
|
||||
counter += 1;
|
||||
break;
|
||||
}
|
||||
case CharCode.RightParenthesis: {
|
||||
counter -= 1;
|
||||
if (counter === 0) {
|
||||
return unescapeCSS(selector.slice(start, selectorIndex++));
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
throw new Error("Parenthesis not matched");
|
||||
}
|
||||
function ensureNotTraversal() {
|
||||
if (tokens.length > 0 && isTraversal(tokens[tokens.length - 1])) {
|
||||
throw new Error("Did not expect successive traversals.");
|
||||
}
|
||||
}
|
||||
function addTraversal(type) {
|
||||
if (tokens.length > 0 &&
|
||||
tokens[tokens.length - 1].type === types_js_1.SelectorType.Descendant) {
|
||||
tokens[tokens.length - 1].type = type;
|
||||
return;
|
||||
}
|
||||
ensureNotTraversal();
|
||||
tokens.push({ type });
|
||||
}
|
||||
function addSpecialAttribute(name, action) {
|
||||
tokens.push({
|
||||
type: types_js_1.SelectorType.Attribute,
|
||||
name,
|
||||
action,
|
||||
value: getName(1),
|
||||
namespace: null,
|
||||
ignoreCase: "quirks",
|
||||
});
|
||||
}
|
||||
/**
|
||||
* We have finished parsing the current part of the selector.
|
||||
*
|
||||
* Remove descendant tokens at the end if they exist,
|
||||
* and return the last index, so that parsing can be
|
||||
* picked up from here.
|
||||
*/
|
||||
function finalizeSubselector() {
|
||||
if (tokens.length > 0 &&
|
||||
tokens[tokens.length - 1].type === types_js_1.SelectorType.Descendant) {
|
||||
tokens.pop();
|
||||
}
|
||||
if (tokens.length === 0) {
|
||||
throw new Error("Empty sub-selector");
|
||||
}
|
||||
subselects.push(tokens);
|
||||
}
|
||||
stripWhitespace(0);
|
||||
if (selector.length === selectorIndex) {
|
||||
return selectorIndex;
|
||||
}
|
||||
loop: while (selectorIndex < selector.length) {
|
||||
const firstChar = selector.charCodeAt(selectorIndex);
|
||||
switch (firstChar) {
|
||||
// Whitespace
|
||||
case CharCode.Space:
|
||||
case CharCode.Tab:
|
||||
case CharCode.NewLine:
|
||||
case CharCode.FormFeed:
|
||||
case CharCode.CarriageReturn: {
|
||||
if (tokens.length === 0 ||
|
||||
tokens[0].type !== types_js_1.SelectorType.Descendant) {
|
||||
ensureNotTraversal();
|
||||
tokens.push({ type: types_js_1.SelectorType.Descendant });
|
||||
}
|
||||
stripWhitespace(1);
|
||||
break;
|
||||
}
|
||||
// Traversals
|
||||
case CharCode.GreaterThan: {
|
||||
addTraversal(types_js_1.SelectorType.Child);
|
||||
stripWhitespace(1);
|
||||
break;
|
||||
}
|
||||
case CharCode.LessThan: {
|
||||
addTraversal(types_js_1.SelectorType.Parent);
|
||||
stripWhitespace(1);
|
||||
break;
|
||||
}
|
||||
case CharCode.Tilde: {
|
||||
addTraversal(types_js_1.SelectorType.Sibling);
|
||||
stripWhitespace(1);
|
||||
break;
|
||||
}
|
||||
case CharCode.Plus: {
|
||||
addTraversal(types_js_1.SelectorType.Adjacent);
|
||||
stripWhitespace(1);
|
||||
break;
|
||||
}
|
||||
// Special attribute selectors: .class, #id
|
||||
case CharCode.Period: {
|
||||
addSpecialAttribute("class", types_js_1.AttributeAction.Element);
|
||||
break;
|
||||
}
|
||||
case CharCode.Hash: {
|
||||
addSpecialAttribute("id", types_js_1.AttributeAction.Equals);
|
||||
break;
|
||||
}
|
||||
case CharCode.LeftSquareBracket: {
|
||||
stripWhitespace(1);
|
||||
// Determine attribute name and namespace
|
||||
let name;
|
||||
let namespace = null;
|
||||
if (selector.charCodeAt(selectorIndex) === CharCode.Pipe) {
|
||||
// Equivalent to no namespace
|
||||
name = getName(1);
|
||||
}
|
||||
else if (selector.startsWith("*|", selectorIndex)) {
|
||||
namespace = "*";
|
||||
name = getName(2);
|
||||
}
|
||||
else {
|
||||
name = getName(0);
|
||||
if (selector.charCodeAt(selectorIndex) === CharCode.Pipe &&
|
||||
selector.charCodeAt(selectorIndex + 1) !==
|
||||
CharCode.Equal) {
|
||||
namespace = name;
|
||||
name = getName(1);
|
||||
}
|
||||
}
|
||||
stripWhitespace(0);
|
||||
// Determine comparison operation
|
||||
let action = types_js_1.AttributeAction.Exists;
|
||||
const possibleAction = actionTypes.get(selector.charCodeAt(selectorIndex));
|
||||
if (possibleAction) {
|
||||
action = possibleAction;
|
||||
if (selector.charCodeAt(selectorIndex + 1) !==
|
||||
CharCode.Equal) {
|
||||
throw new Error("Expected `=`");
|
||||
}
|
||||
stripWhitespace(2);
|
||||
}
|
||||
else if (selector.charCodeAt(selectorIndex) === CharCode.Equal) {
|
||||
action = types_js_1.AttributeAction.Equals;
|
||||
stripWhitespace(1);
|
||||
}
|
||||
// Determine value
|
||||
let value = "";
|
||||
let ignoreCase = null;
|
||||
if (action !== "exists") {
|
||||
if (isQuote(selector.charCodeAt(selectorIndex))) {
|
||||
const quote = selector.charCodeAt(selectorIndex);
|
||||
selectorIndex += 1;
|
||||
const sectionStart = selectorIndex;
|
||||
while (selectorIndex < selector.length &&
|
||||
selector.charCodeAt(selectorIndex) !== quote) {
|
||||
selectorIndex +=
|
||||
// Skip next character if it is escaped
|
||||
selector.charCodeAt(selectorIndex) ===
|
||||
CharCode.BackSlash
|
||||
? 2
|
||||
: 1;
|
||||
}
|
||||
if (selector.charCodeAt(selectorIndex) !== quote) {
|
||||
throw new Error("Attribute value didn't end");
|
||||
}
|
||||
value = unescapeCSS(selector.slice(sectionStart, selectorIndex));
|
||||
selectorIndex += 1;
|
||||
}
|
||||
else {
|
||||
const valueStart = selectorIndex;
|
||||
while (selectorIndex < selector.length &&
|
||||
!isWhitespace(selector.charCodeAt(selectorIndex)) &&
|
||||
selector.charCodeAt(selectorIndex) !==
|
||||
CharCode.RightSquareBracket) {
|
||||
selectorIndex +=
|
||||
// Skip next character if it is escaped
|
||||
selector.charCodeAt(selectorIndex) ===
|
||||
CharCode.BackSlash
|
||||
? 2
|
||||
: 1;
|
||||
}
|
||||
value = unescapeCSS(selector.slice(valueStart, selectorIndex));
|
||||
}
|
||||
stripWhitespace(0);
|
||||
// See if we have a force ignore flag
|
||||
switch (selector.charCodeAt(selectorIndex) | 0x20) {
|
||||
// If the forceIgnore flag is set (either `i` or `s`), use that value
|
||||
case CharCode.LowerI: {
|
||||
ignoreCase = true;
|
||||
stripWhitespace(1);
|
||||
break;
|
||||
}
|
||||
case CharCode.LowerS: {
|
||||
ignoreCase = false;
|
||||
stripWhitespace(1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (selector.charCodeAt(selectorIndex) !==
|
||||
CharCode.RightSquareBracket) {
|
||||
throw new Error("Attribute selector didn't terminate");
|
||||
}
|
||||
selectorIndex += 1;
|
||||
const attributeSelector = {
|
||||
type: types_js_1.SelectorType.Attribute,
|
||||
name,
|
||||
action,
|
||||
value,
|
||||
namespace,
|
||||
ignoreCase,
|
||||
};
|
||||
tokens.push(attributeSelector);
|
||||
break;
|
||||
}
|
||||
case CharCode.Colon: {
|
||||
if (selector.charCodeAt(selectorIndex + 1) === CharCode.Colon) {
|
||||
tokens.push({
|
||||
type: types_js_1.SelectorType.PseudoElement,
|
||||
name: getName(2).toLowerCase(),
|
||||
data: selector.charCodeAt(selectorIndex) ===
|
||||
CharCode.LeftParenthesis
|
||||
? readValueWithParenthesis()
|
||||
: null,
|
||||
});
|
||||
break;
|
||||
}
|
||||
const name = getName(1).toLowerCase();
|
||||
if (pseudosToPseudoElements.has(name)) {
|
||||
tokens.push({
|
||||
type: types_js_1.SelectorType.PseudoElement,
|
||||
name,
|
||||
data: null,
|
||||
});
|
||||
break;
|
||||
}
|
||||
let data = null;
|
||||
if (selector.charCodeAt(selectorIndex) ===
|
||||
CharCode.LeftParenthesis) {
|
||||
if (unpackPseudos.has(name)) {
|
||||
if (isQuote(selector.charCodeAt(selectorIndex + 1))) {
|
||||
throw new Error(`Pseudo-selector ${name} cannot be quoted`);
|
||||
}
|
||||
data = [];
|
||||
selectorIndex = parseSelector(data, selector, selectorIndex + 1);
|
||||
if (selector.charCodeAt(selectorIndex) !==
|
||||
CharCode.RightParenthesis) {
|
||||
throw new Error(`Missing closing parenthesis in :${name} (${selector})`);
|
||||
}
|
||||
selectorIndex += 1;
|
||||
}
|
||||
else {
|
||||
data = readValueWithParenthesis();
|
||||
if (stripQuotesFromPseudos.has(name)) {
|
||||
const quot = data.charCodeAt(0);
|
||||
if (quot === data.charCodeAt(data.length - 1) &&
|
||||
isQuote(quot)) {
|
||||
data = data.slice(1, -1);
|
||||
}
|
||||
}
|
||||
data = unescapeCSS(data);
|
||||
}
|
||||
}
|
||||
tokens.push({ type: types_js_1.SelectorType.Pseudo, name, data });
|
||||
break;
|
||||
}
|
||||
case CharCode.Comma: {
|
||||
finalizeSubselector();
|
||||
tokens = [];
|
||||
stripWhitespace(1);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
if (selector.startsWith("/*", selectorIndex)) {
|
||||
const endIndex = selector.indexOf("*/", selectorIndex + 2);
|
||||
if (endIndex < 0) {
|
||||
throw new Error("Comment was not terminated");
|
||||
}
|
||||
selectorIndex = endIndex + 2;
|
||||
// Remove leading whitespace
|
||||
if (tokens.length === 0) {
|
||||
stripWhitespace(0);
|
||||
}
|
||||
break;
|
||||
}
|
||||
let namespace = null;
|
||||
let name;
|
||||
if (firstChar === CharCode.Asterisk) {
|
||||
selectorIndex += 1;
|
||||
name = "*";
|
||||
}
|
||||
else if (firstChar === CharCode.Pipe) {
|
||||
name = "";
|
||||
if (selector.charCodeAt(selectorIndex + 1) === CharCode.Pipe) {
|
||||
addTraversal(types_js_1.SelectorType.ColumnCombinator);
|
||||
stripWhitespace(2);
|
||||
break;
|
||||
}
|
||||
}
|
||||
else if (reName.test(selector.slice(selectorIndex))) {
|
||||
name = getName(0);
|
||||
}
|
||||
else {
|
||||
break loop;
|
||||
}
|
||||
if (selector.charCodeAt(selectorIndex) === CharCode.Pipe &&
|
||||
selector.charCodeAt(selectorIndex + 1) !== CharCode.Pipe) {
|
||||
namespace = name;
|
||||
if (selector.charCodeAt(selectorIndex + 1) ===
|
||||
CharCode.Asterisk) {
|
||||
name = "*";
|
||||
selectorIndex += 2;
|
||||
}
|
||||
else {
|
||||
name = getName(1);
|
||||
}
|
||||
}
|
||||
tokens.push(name === "*"
|
||||
? { type: types_js_1.SelectorType.Universal, namespace }
|
||||
: { type: types_js_1.SelectorType.Tag, name, namespace });
|
||||
}
|
||||
}
|
||||
}
|
||||
finalizeSubselector();
|
||||
return selectorIndex;
|
||||
}
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
import { type Selector } from "./types.js";
|
||||
/**
|
||||
* Turns `selector` back into a string.
|
||||
*
|
||||
* @param selector Selector to stringify.
|
||||
*/
|
||||
export declare function stringify(selector: Selector[][]): string;
|
||||
//# sourceMappingURL=stringify.d.ts.map
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"stringify.d.ts","sourceRoot":"","sources":["../../src/stringify.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,QAAQ,EAAiC,MAAM,YAAY,CAAC;AA8B1E;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,QAAQ,EAAE,QAAQ,EAAE,EAAE,GAAG,MAAM,CAUxD"}
|
||||
+150
@@ -0,0 +1,150 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.stringify = stringify;
|
||||
const types_js_1 = require("./types.js");
|
||||
const attribValueChars = ["\\", '"'];
|
||||
const pseudoValueChars = [...attribValueChars, "(", ")"];
|
||||
const charsToEscapeInAttributeValue = new Set(attribValueChars.map((c) => c.charCodeAt(0)));
|
||||
const charsToEscapeInPseudoValue = new Set(pseudoValueChars.map((c) => c.charCodeAt(0)));
|
||||
const charsToEscapeInName = new Set([
|
||||
...pseudoValueChars,
|
||||
"~",
|
||||
"^",
|
||||
"$",
|
||||
"*",
|
||||
"+",
|
||||
"!",
|
||||
"|",
|
||||
":",
|
||||
"[",
|
||||
"]",
|
||||
" ",
|
||||
".",
|
||||
"%",
|
||||
].map((c) => c.charCodeAt(0)));
|
||||
/**
|
||||
* Turns `selector` back into a string.
|
||||
*
|
||||
* @param selector Selector to stringify.
|
||||
*/
|
||||
function stringify(selector) {
|
||||
return selector
|
||||
.map((token) => token
|
||||
.map((token, index, array) => stringifyToken(token, index, array))
|
||||
.join(""))
|
||||
.join(", ");
|
||||
}
|
||||
function stringifyToken(token, index, array) {
|
||||
switch (token.type) {
|
||||
// Simple types
|
||||
case types_js_1.SelectorType.Child: {
|
||||
return index === 0 ? "> " : " > ";
|
||||
}
|
||||
case types_js_1.SelectorType.Parent: {
|
||||
return index === 0 ? "< " : " < ";
|
||||
}
|
||||
case types_js_1.SelectorType.Sibling: {
|
||||
return index === 0 ? "~ " : " ~ ";
|
||||
}
|
||||
case types_js_1.SelectorType.Adjacent: {
|
||||
return index === 0 ? "+ " : " + ";
|
||||
}
|
||||
case types_js_1.SelectorType.Descendant: {
|
||||
return " ";
|
||||
}
|
||||
case types_js_1.SelectorType.ColumnCombinator: {
|
||||
return index === 0 ? "|| " : " || ";
|
||||
}
|
||||
case types_js_1.SelectorType.Universal: {
|
||||
// Return an empty string if the selector isn't needed.
|
||||
return token.namespace === "*" &&
|
||||
index + 1 < array.length &&
|
||||
"name" in array[index + 1]
|
||||
? ""
|
||||
: `${getNamespace(token.namespace)}*`;
|
||||
}
|
||||
case types_js_1.SelectorType.Tag: {
|
||||
return getNamespacedName(token);
|
||||
}
|
||||
case types_js_1.SelectorType.PseudoElement: {
|
||||
return `::${escapeName(token.name, charsToEscapeInName)}${token.data === null
|
||||
? ""
|
||||
: `(${escapeName(token.data, charsToEscapeInPseudoValue)})`}`;
|
||||
}
|
||||
case types_js_1.SelectorType.Pseudo: {
|
||||
return `:${escapeName(token.name, charsToEscapeInName)}${token.data === null
|
||||
? ""
|
||||
: `(${typeof token.data === "string"
|
||||
? escapeName(token.data, charsToEscapeInPseudoValue)
|
||||
: stringify(token.data)})`}`;
|
||||
}
|
||||
case types_js_1.SelectorType.Attribute: {
|
||||
if (token.name === "id" &&
|
||||
token.action === types_js_1.AttributeAction.Equals &&
|
||||
token.ignoreCase === "quirks" &&
|
||||
!token.namespace) {
|
||||
return `#${escapeName(token.value, charsToEscapeInName)}`;
|
||||
}
|
||||
if (token.name === "class" &&
|
||||
token.action === types_js_1.AttributeAction.Element &&
|
||||
token.ignoreCase === "quirks" &&
|
||||
!token.namespace) {
|
||||
return `.${escapeName(token.value, charsToEscapeInName)}`;
|
||||
}
|
||||
const name = getNamespacedName(token);
|
||||
if (token.action === types_js_1.AttributeAction.Exists) {
|
||||
return `[${name}]`;
|
||||
}
|
||||
return `[${name}${getActionValue(token.action)}="${escapeName(token.value, charsToEscapeInAttributeValue)}"${token.ignoreCase === null ? "" : token.ignoreCase ? " i" : " s"}]`;
|
||||
}
|
||||
}
|
||||
}
|
||||
function getActionValue(action) {
|
||||
switch (action) {
|
||||
case types_js_1.AttributeAction.Equals: {
|
||||
return "";
|
||||
}
|
||||
case types_js_1.AttributeAction.Element: {
|
||||
return "~";
|
||||
}
|
||||
case types_js_1.AttributeAction.Start: {
|
||||
return "^";
|
||||
}
|
||||
case types_js_1.AttributeAction.End: {
|
||||
return "$";
|
||||
}
|
||||
case types_js_1.AttributeAction.Any: {
|
||||
return "*";
|
||||
}
|
||||
case types_js_1.AttributeAction.Not: {
|
||||
return "!";
|
||||
}
|
||||
case types_js_1.AttributeAction.Hyphen: {
|
||||
return "|";
|
||||
}
|
||||
default: {
|
||||
throw new Error("Shouldn't be here");
|
||||
}
|
||||
}
|
||||
}
|
||||
function getNamespacedName(token) {
|
||||
return `${getNamespace(token.namespace)}${escapeName(token.name, charsToEscapeInName)}`;
|
||||
}
|
||||
function getNamespace(namespace) {
|
||||
return namespace === null
|
||||
? ""
|
||||
: `${namespace === "*"
|
||||
? "*"
|
||||
: escapeName(namespace, charsToEscapeInName)}|`;
|
||||
}
|
||||
function escapeName(name, charsToEscape) {
|
||||
let lastIndex = 0;
|
||||
let escapedName = "";
|
||||
for (let index = 0; index < name.length; index++) {
|
||||
if (charsToEscape.has(name.charCodeAt(index))) {
|
||||
escapedName += `${name.slice(lastIndex, index)}\\${name.charAt(index)}`;
|
||||
lastIndex = index + 1;
|
||||
}
|
||||
}
|
||||
return escapedName.length > 0 ? escapedName + name.slice(lastIndex) : name;
|
||||
}
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
export type Selector = PseudoSelector | PseudoElement | AttributeSelector | TagSelector | UniversalSelector | Traversal;
|
||||
export declare enum SelectorType {
|
||||
Attribute = "attribute",
|
||||
Pseudo = "pseudo",
|
||||
PseudoElement = "pseudo-element",
|
||||
Tag = "tag",
|
||||
Universal = "universal",
|
||||
Adjacent = "adjacent",
|
||||
Child = "child",
|
||||
Descendant = "descendant",
|
||||
Parent = "parent",
|
||||
Sibling = "sibling",
|
||||
ColumnCombinator = "column-combinator"
|
||||
}
|
||||
/**
|
||||
* Modes for ignore case.
|
||||
*
|
||||
* This could be updated to an enum, and the object is
|
||||
* the current stand-in that will allow code to be updated
|
||||
* without big changes.
|
||||
*/
|
||||
export declare const IgnoreCaseMode: {
|
||||
readonly Unknown: null;
|
||||
readonly QuirksMode: "quirks";
|
||||
readonly IgnoreCase: true;
|
||||
readonly CaseSensitive: false;
|
||||
};
|
||||
export interface AttributeSelector {
|
||||
type: SelectorType.Attribute;
|
||||
name: string;
|
||||
action: AttributeAction;
|
||||
value: string;
|
||||
ignoreCase: "quirks" | boolean | null;
|
||||
namespace: string | null;
|
||||
}
|
||||
export type DataType = Selector[][] | null | string;
|
||||
export interface PseudoSelector {
|
||||
type: SelectorType.Pseudo;
|
||||
name: string;
|
||||
data: DataType;
|
||||
}
|
||||
export interface PseudoElement {
|
||||
type: SelectorType.PseudoElement;
|
||||
name: string;
|
||||
data: string | null;
|
||||
}
|
||||
export interface TagSelector {
|
||||
type: SelectorType.Tag;
|
||||
name: string;
|
||||
namespace: string | null;
|
||||
}
|
||||
export interface UniversalSelector {
|
||||
type: SelectorType.Universal;
|
||||
namespace: string | null;
|
||||
}
|
||||
export interface Traversal {
|
||||
type: TraversalType;
|
||||
}
|
||||
export declare enum AttributeAction {
|
||||
Any = "any",
|
||||
Element = "element",
|
||||
End = "end",
|
||||
Equals = "equals",
|
||||
Exists = "exists",
|
||||
Hyphen = "hyphen",
|
||||
Not = "not",
|
||||
Start = "start"
|
||||
}
|
||||
export type TraversalType = SelectorType.Adjacent | SelectorType.Child | SelectorType.Descendant | SelectorType.Parent | SelectorType.Sibling | SelectorType.ColumnCombinator;
|
||||
//# sourceMappingURL=types.d.ts.map
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,QAAQ,GACd,cAAc,GACd,aAAa,GACb,iBAAiB,GACjB,WAAW,GACX,iBAAiB,GACjB,SAAS,CAAC;AAEhB,oBAAY,YAAY;IACpB,SAAS,cAAc;IACvB,MAAM,WAAW;IACjB,aAAa,mBAAmB;IAChC,GAAG,QAAQ;IACX,SAAS,cAAc;IAGvB,QAAQ,aAAa;IACrB,KAAK,UAAU;IACf,UAAU,eAAe;IACzB,MAAM,WAAW;IACjB,OAAO,YAAY;IACnB,gBAAgB,sBAAsB;CACzC;AAED;;;;;;GAMG;AACH,eAAO,MAAM,cAAc;;;;;CAKjB,CAAC;AAEX,MAAM,WAAW,iBAAiB;IAC9B,IAAI,EAAE,YAAY,CAAC,SAAS,CAAC;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,eAAe,CAAC;IACxB,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,QAAQ,GAAG,OAAO,GAAG,IAAI,CAAC;IACtC,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;CAC5B;AAED,MAAM,MAAM,QAAQ,GAAG,QAAQ,EAAE,EAAE,GAAG,IAAI,GAAG,MAAM,CAAC;AAEpD,MAAM,WAAW,cAAc;IAC3B,IAAI,EAAE,YAAY,CAAC,MAAM,CAAC;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,QAAQ,CAAC;CAClB;AAED,MAAM,WAAW,aAAa;IAC1B,IAAI,EAAE,YAAY,CAAC,aAAa,CAAC;IACjC,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;CACvB;AAED,MAAM,WAAW,WAAW;IACxB,IAAI,EAAE,YAAY,CAAC,GAAG,CAAC;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;CAC5B;AAED,MAAM,WAAW,iBAAiB;IAC9B,IAAI,EAAE,YAAY,CAAC,SAAS,CAAC;IAC7B,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;CAC5B;AAED,MAAM,WAAW,SAAS;IACtB,IAAI,EAAE,aAAa,CAAC;CACvB;AAED,oBAAY,eAAe;IACvB,GAAG,QAAQ;IACX,OAAO,YAAY;IACnB,GAAG,QAAQ;IACX,MAAM,WAAW;IACjB,MAAM,WAAW;IACjB,MAAM,WAAW;IACjB,GAAG,QAAQ;IACX,KAAK,UAAU;CAClB;AAED,MAAM,MAAM,aAAa,GACnB,YAAY,CAAC,QAAQ,GACrB,YAAY,CAAC,KAAK,GAClB,YAAY,CAAC,UAAU,GACvB,YAAY,CAAC,MAAM,GACnB,YAAY,CAAC,OAAO,GACpB,YAAY,CAAC,gBAAgB,CAAC"}
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.AttributeAction = exports.IgnoreCaseMode = exports.SelectorType = void 0;
|
||||
var SelectorType;
|
||||
(function (SelectorType) {
|
||||
SelectorType["Attribute"] = "attribute";
|
||||
SelectorType["Pseudo"] = "pseudo";
|
||||
SelectorType["PseudoElement"] = "pseudo-element";
|
||||
SelectorType["Tag"] = "tag";
|
||||
SelectorType["Universal"] = "universal";
|
||||
// Traversals
|
||||
SelectorType["Adjacent"] = "adjacent";
|
||||
SelectorType["Child"] = "child";
|
||||
SelectorType["Descendant"] = "descendant";
|
||||
SelectorType["Parent"] = "parent";
|
||||
SelectorType["Sibling"] = "sibling";
|
||||
SelectorType["ColumnCombinator"] = "column-combinator";
|
||||
})(SelectorType || (exports.SelectorType = SelectorType = {}));
|
||||
/**
|
||||
* Modes for ignore case.
|
||||
*
|
||||
* This could be updated to an enum, and the object is
|
||||
* the current stand-in that will allow code to be updated
|
||||
* without big changes.
|
||||
*/
|
||||
exports.IgnoreCaseMode = {
|
||||
Unknown: null,
|
||||
QuirksMode: "quirks",
|
||||
IgnoreCase: true,
|
||||
CaseSensitive: false,
|
||||
};
|
||||
var AttributeAction;
|
||||
(function (AttributeAction) {
|
||||
AttributeAction["Any"] = "any";
|
||||
AttributeAction["Element"] = "element";
|
||||
AttributeAction["End"] = "end";
|
||||
AttributeAction["Equals"] = "equals";
|
||||
AttributeAction["Exists"] = "exists";
|
||||
AttributeAction["Hyphen"] = "hyphen";
|
||||
AttributeAction["Not"] = "not";
|
||||
AttributeAction["Start"] = "start";
|
||||
})(AttributeAction || (exports.AttributeAction = AttributeAction = {}));
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
export * from "./types.js";
|
||||
export { isTraversal, parse } from "./parse.js";
|
||||
export { stringify } from "./stringify.js";
|
||||
//# sourceMappingURL=index.d.ts.map
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAC;AAC3B,OAAO,EAAE,WAAW,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAChD,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC"}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
export * from "./types.js";
|
||||
export { isTraversal, parse } from "./parse.js";
|
||||
export { stringify } from "./stringify.js";
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"type": "module"
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
import { type Selector, type Traversal } from "./types.js";
|
||||
/**
|
||||
* Checks whether a specific selector is a traversal.
|
||||
* This is useful eg. in swapping the order of elements that
|
||||
* are not traversals.
|
||||
*
|
||||
* @param selector Selector to check.
|
||||
*/
|
||||
export declare function isTraversal(selector: Selector): selector is Traversal;
|
||||
/**
|
||||
* Parses `selector`.
|
||||
*
|
||||
* @param selector Selector to parse.
|
||||
* @returns Returns a two-dimensional array.
|
||||
* The first dimension represents selectors separated by commas (eg. `sub1, sub2`),
|
||||
* the second contains the relevant tokens for that selector.
|
||||
*/
|
||||
export declare function parse(selector: string): Selector[][];
|
||||
//# sourceMappingURL=parse.d.ts.map
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"parse.d.ts","sourceRoot":"","sources":["../../src/parse.ts"],"names":[],"mappings":"AAAA,OAAO,EACH,KAAK,QAAQ,EAGb,KAAK,SAAS,EAIjB,MAAM,YAAY,CAAC;AAyEpB;;;;;;GAMG;AACH,wBAAgB,WAAW,CAAC,QAAQ,EAAE,QAAQ,GAAG,QAAQ,IAAI,SAAS,CAcrE;AAuCD;;;;;;;GAOG;AACH,wBAAgB,KAAK,CAAC,QAAQ,EAAE,MAAM,GAAG,QAAQ,EAAE,EAAE,CAUpD"}
|
||||
+485
@@ -0,0 +1,485 @@
|
||||
import { SelectorType, AttributeAction, } from "./types.js";
|
||||
const reName = /^[^#\\]?(?:\\(?:[\da-f]{1,6}\s?|.)|[\w\u00B0-\uFFFF-])+/;
|
||||
const reEscape = /\\([\da-f]{1,6}\s?|(\s)|.)/gi;
|
||||
var CharCode;
|
||||
(function (CharCode) {
|
||||
CharCode[CharCode["LeftParenthesis"] = 40] = "LeftParenthesis";
|
||||
CharCode[CharCode["RightParenthesis"] = 41] = "RightParenthesis";
|
||||
CharCode[CharCode["LeftSquareBracket"] = 91] = "LeftSquareBracket";
|
||||
CharCode[CharCode["RightSquareBracket"] = 93] = "RightSquareBracket";
|
||||
CharCode[CharCode["Comma"] = 44] = "Comma";
|
||||
CharCode[CharCode["Period"] = 46] = "Period";
|
||||
CharCode[CharCode["Colon"] = 58] = "Colon";
|
||||
CharCode[CharCode["SingleQuote"] = 39] = "SingleQuote";
|
||||
CharCode[CharCode["DoubleQuote"] = 34] = "DoubleQuote";
|
||||
CharCode[CharCode["Plus"] = 43] = "Plus";
|
||||
CharCode[CharCode["Tilde"] = 126] = "Tilde";
|
||||
CharCode[CharCode["QuestionMark"] = 63] = "QuestionMark";
|
||||
CharCode[CharCode["ExclamationMark"] = 33] = "ExclamationMark";
|
||||
CharCode[CharCode["Slash"] = 47] = "Slash";
|
||||
CharCode[CharCode["Equal"] = 61] = "Equal";
|
||||
CharCode[CharCode["Dollar"] = 36] = "Dollar";
|
||||
CharCode[CharCode["Pipe"] = 124] = "Pipe";
|
||||
CharCode[CharCode["Circumflex"] = 94] = "Circumflex";
|
||||
CharCode[CharCode["Asterisk"] = 42] = "Asterisk";
|
||||
CharCode[CharCode["GreaterThan"] = 62] = "GreaterThan";
|
||||
CharCode[CharCode["LessThan"] = 60] = "LessThan";
|
||||
CharCode[CharCode["Hash"] = 35] = "Hash";
|
||||
CharCode[CharCode["LowerI"] = 105] = "LowerI";
|
||||
CharCode[CharCode["LowerS"] = 115] = "LowerS";
|
||||
CharCode[CharCode["BackSlash"] = 92] = "BackSlash";
|
||||
// Whitespace
|
||||
CharCode[CharCode["Space"] = 32] = "Space";
|
||||
CharCode[CharCode["Tab"] = 9] = "Tab";
|
||||
CharCode[CharCode["NewLine"] = 10] = "NewLine";
|
||||
CharCode[CharCode["FormFeed"] = 12] = "FormFeed";
|
||||
CharCode[CharCode["CarriageReturn"] = 13] = "CarriageReturn";
|
||||
})(CharCode || (CharCode = {}));
|
||||
const actionTypes = new Map([
|
||||
[CharCode.Tilde, AttributeAction.Element],
|
||||
[CharCode.Circumflex, AttributeAction.Start],
|
||||
[CharCode.Dollar, AttributeAction.End],
|
||||
[CharCode.Asterisk, AttributeAction.Any],
|
||||
[CharCode.ExclamationMark, AttributeAction.Not],
|
||||
[CharCode.Pipe, AttributeAction.Hyphen],
|
||||
]);
|
||||
// Pseudos, whose data property is parsed as well.
|
||||
const unpackPseudos = new Set([
|
||||
"has",
|
||||
"not",
|
||||
"matches",
|
||||
"is",
|
||||
"where",
|
||||
"host",
|
||||
"host-context",
|
||||
]);
|
||||
/**
|
||||
* Pseudo elements defined in CSS Level 1 and CSS Level 2 can be written with
|
||||
* a single colon; eg. :before will turn into ::before.
|
||||
*
|
||||
* @see {@link https://www.w3.org/TR/2018/WD-selectors-4-20181121/#pseudo-element-syntax}
|
||||
*/
|
||||
const pseudosToPseudoElements = new Set([
|
||||
"before",
|
||||
"after",
|
||||
"first-line",
|
||||
"first-letter",
|
||||
]);
|
||||
/**
|
||||
* Checks whether a specific selector is a traversal.
|
||||
* This is useful eg. in swapping the order of elements that
|
||||
* are not traversals.
|
||||
*
|
||||
* @param selector Selector to check.
|
||||
*/
|
||||
export function isTraversal(selector) {
|
||||
switch (selector.type) {
|
||||
case SelectorType.Adjacent:
|
||||
case SelectorType.Child:
|
||||
case SelectorType.Descendant:
|
||||
case SelectorType.Parent:
|
||||
case SelectorType.Sibling:
|
||||
case SelectorType.ColumnCombinator: {
|
||||
return true;
|
||||
}
|
||||
default: {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
const stripQuotesFromPseudos = new Set(["contains", "icontains"]);
|
||||
// Unescape function taken from https://github.com/jquery/sizzle/blob/master/src/sizzle.js#L152
|
||||
function funescape(_, escaped, escapedWhitespace) {
|
||||
const high = Number.parseInt(escaped, 16) - 65536;
|
||||
// NaN means non-codepoint
|
||||
return high !== high || escapedWhitespace
|
||||
? escaped
|
||||
: high < 0
|
||||
? // BMP codepoint
|
||||
String.fromCharCode(high + 65536)
|
||||
: // Supplemental Plane codepoint (surrogate pair)
|
||||
String.fromCharCode((high >> 10) | 55296, (high & 1023) | 56320);
|
||||
}
|
||||
function unescapeCSS(cssString) {
|
||||
return cssString.replace(reEscape, funescape);
|
||||
}
|
||||
function isQuote(c) {
|
||||
return c === CharCode.SingleQuote || c === CharCode.DoubleQuote;
|
||||
}
|
||||
function isWhitespace(c) {
|
||||
return (c === CharCode.Space ||
|
||||
c === CharCode.Tab ||
|
||||
c === CharCode.NewLine ||
|
||||
c === CharCode.FormFeed ||
|
||||
c === CharCode.CarriageReturn);
|
||||
}
|
||||
/**
|
||||
* Parses `selector`.
|
||||
*
|
||||
* @param selector Selector to parse.
|
||||
* @returns Returns a two-dimensional array.
|
||||
* The first dimension represents selectors separated by commas (eg. `sub1, sub2`),
|
||||
* the second contains the relevant tokens for that selector.
|
||||
*/
|
||||
export function parse(selector) {
|
||||
const subselects = [];
|
||||
const endIndex = parseSelector(subselects, `${selector}`, 0);
|
||||
if (endIndex < selector.length) {
|
||||
throw new Error(`Unmatched selector: ${selector.slice(endIndex)}`);
|
||||
}
|
||||
return subselects;
|
||||
}
|
||||
function parseSelector(subselects, selector, selectorIndex) {
|
||||
let tokens = [];
|
||||
function getName(offset) {
|
||||
const match = selector.slice(selectorIndex + offset).match(reName);
|
||||
if (!match) {
|
||||
throw new Error(`Expected name, found ${selector.slice(selectorIndex)}`);
|
||||
}
|
||||
const [name] = match;
|
||||
selectorIndex += offset + name.length;
|
||||
return unescapeCSS(name);
|
||||
}
|
||||
function stripWhitespace(offset) {
|
||||
selectorIndex += offset;
|
||||
while (selectorIndex < selector.length &&
|
||||
isWhitespace(selector.charCodeAt(selectorIndex))) {
|
||||
selectorIndex++;
|
||||
}
|
||||
}
|
||||
function readValueWithParenthesis() {
|
||||
selectorIndex += 1;
|
||||
const start = selectorIndex;
|
||||
for (let counter = 1; selectorIndex < selector.length; selectorIndex++) {
|
||||
switch (selector.charCodeAt(selectorIndex)) {
|
||||
case CharCode.BackSlash: {
|
||||
// Skip next character
|
||||
selectorIndex += 1;
|
||||
break;
|
||||
}
|
||||
case CharCode.LeftParenthesis: {
|
||||
counter += 1;
|
||||
break;
|
||||
}
|
||||
case CharCode.RightParenthesis: {
|
||||
counter -= 1;
|
||||
if (counter === 0) {
|
||||
return unescapeCSS(selector.slice(start, selectorIndex++));
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
throw new Error("Parenthesis not matched");
|
||||
}
|
||||
function ensureNotTraversal() {
|
||||
if (tokens.length > 0 && isTraversal(tokens[tokens.length - 1])) {
|
||||
throw new Error("Did not expect successive traversals.");
|
||||
}
|
||||
}
|
||||
function addTraversal(type) {
|
||||
if (tokens.length > 0 &&
|
||||
tokens[tokens.length - 1].type === SelectorType.Descendant) {
|
||||
tokens[tokens.length - 1].type = type;
|
||||
return;
|
||||
}
|
||||
ensureNotTraversal();
|
||||
tokens.push({ type });
|
||||
}
|
||||
function addSpecialAttribute(name, action) {
|
||||
tokens.push({
|
||||
type: SelectorType.Attribute,
|
||||
name,
|
||||
action,
|
||||
value: getName(1),
|
||||
namespace: null,
|
||||
ignoreCase: "quirks",
|
||||
});
|
||||
}
|
||||
/**
|
||||
* We have finished parsing the current part of the selector.
|
||||
*
|
||||
* Remove descendant tokens at the end if they exist,
|
||||
* and return the last index, so that parsing can be
|
||||
* picked up from here.
|
||||
*/
|
||||
function finalizeSubselector() {
|
||||
if (tokens.length > 0 &&
|
||||
tokens[tokens.length - 1].type === SelectorType.Descendant) {
|
||||
tokens.pop();
|
||||
}
|
||||
if (tokens.length === 0) {
|
||||
throw new Error("Empty sub-selector");
|
||||
}
|
||||
subselects.push(tokens);
|
||||
}
|
||||
stripWhitespace(0);
|
||||
if (selector.length === selectorIndex) {
|
||||
return selectorIndex;
|
||||
}
|
||||
loop: while (selectorIndex < selector.length) {
|
||||
const firstChar = selector.charCodeAt(selectorIndex);
|
||||
switch (firstChar) {
|
||||
// Whitespace
|
||||
case CharCode.Space:
|
||||
case CharCode.Tab:
|
||||
case CharCode.NewLine:
|
||||
case CharCode.FormFeed:
|
||||
case CharCode.CarriageReturn: {
|
||||
if (tokens.length === 0 ||
|
||||
tokens[0].type !== SelectorType.Descendant) {
|
||||
ensureNotTraversal();
|
||||
tokens.push({ type: SelectorType.Descendant });
|
||||
}
|
||||
stripWhitespace(1);
|
||||
break;
|
||||
}
|
||||
// Traversals
|
||||
case CharCode.GreaterThan: {
|
||||
addTraversal(SelectorType.Child);
|
||||
stripWhitespace(1);
|
||||
break;
|
||||
}
|
||||
case CharCode.LessThan: {
|
||||
addTraversal(SelectorType.Parent);
|
||||
stripWhitespace(1);
|
||||
break;
|
||||
}
|
||||
case CharCode.Tilde: {
|
||||
addTraversal(SelectorType.Sibling);
|
||||
stripWhitespace(1);
|
||||
break;
|
||||
}
|
||||
case CharCode.Plus: {
|
||||
addTraversal(SelectorType.Adjacent);
|
||||
stripWhitespace(1);
|
||||
break;
|
||||
}
|
||||
// Special attribute selectors: .class, #id
|
||||
case CharCode.Period: {
|
||||
addSpecialAttribute("class", AttributeAction.Element);
|
||||
break;
|
||||
}
|
||||
case CharCode.Hash: {
|
||||
addSpecialAttribute("id", AttributeAction.Equals);
|
||||
break;
|
||||
}
|
||||
case CharCode.LeftSquareBracket: {
|
||||
stripWhitespace(1);
|
||||
// Determine attribute name and namespace
|
||||
let name;
|
||||
let namespace = null;
|
||||
if (selector.charCodeAt(selectorIndex) === CharCode.Pipe) {
|
||||
// Equivalent to no namespace
|
||||
name = getName(1);
|
||||
}
|
||||
else if (selector.startsWith("*|", selectorIndex)) {
|
||||
namespace = "*";
|
||||
name = getName(2);
|
||||
}
|
||||
else {
|
||||
name = getName(0);
|
||||
if (selector.charCodeAt(selectorIndex) === CharCode.Pipe &&
|
||||
selector.charCodeAt(selectorIndex + 1) !==
|
||||
CharCode.Equal) {
|
||||
namespace = name;
|
||||
name = getName(1);
|
||||
}
|
||||
}
|
||||
stripWhitespace(0);
|
||||
// Determine comparison operation
|
||||
let action = AttributeAction.Exists;
|
||||
const possibleAction = actionTypes.get(selector.charCodeAt(selectorIndex));
|
||||
if (possibleAction) {
|
||||
action = possibleAction;
|
||||
if (selector.charCodeAt(selectorIndex + 1) !==
|
||||
CharCode.Equal) {
|
||||
throw new Error("Expected `=`");
|
||||
}
|
||||
stripWhitespace(2);
|
||||
}
|
||||
else if (selector.charCodeAt(selectorIndex) === CharCode.Equal) {
|
||||
action = AttributeAction.Equals;
|
||||
stripWhitespace(1);
|
||||
}
|
||||
// Determine value
|
||||
let value = "";
|
||||
let ignoreCase = null;
|
||||
if (action !== "exists") {
|
||||
if (isQuote(selector.charCodeAt(selectorIndex))) {
|
||||
const quote = selector.charCodeAt(selectorIndex);
|
||||
selectorIndex += 1;
|
||||
const sectionStart = selectorIndex;
|
||||
while (selectorIndex < selector.length &&
|
||||
selector.charCodeAt(selectorIndex) !== quote) {
|
||||
selectorIndex +=
|
||||
// Skip next character if it is escaped
|
||||
selector.charCodeAt(selectorIndex) ===
|
||||
CharCode.BackSlash
|
||||
? 2
|
||||
: 1;
|
||||
}
|
||||
if (selector.charCodeAt(selectorIndex) !== quote) {
|
||||
throw new Error("Attribute value didn't end");
|
||||
}
|
||||
value = unescapeCSS(selector.slice(sectionStart, selectorIndex));
|
||||
selectorIndex += 1;
|
||||
}
|
||||
else {
|
||||
const valueStart = selectorIndex;
|
||||
while (selectorIndex < selector.length &&
|
||||
!isWhitespace(selector.charCodeAt(selectorIndex)) &&
|
||||
selector.charCodeAt(selectorIndex) !==
|
||||
CharCode.RightSquareBracket) {
|
||||
selectorIndex +=
|
||||
// Skip next character if it is escaped
|
||||
selector.charCodeAt(selectorIndex) ===
|
||||
CharCode.BackSlash
|
||||
? 2
|
||||
: 1;
|
||||
}
|
||||
value = unescapeCSS(selector.slice(valueStart, selectorIndex));
|
||||
}
|
||||
stripWhitespace(0);
|
||||
// See if we have a force ignore flag
|
||||
switch (selector.charCodeAt(selectorIndex) | 0x20) {
|
||||
// If the forceIgnore flag is set (either `i` or `s`), use that value
|
||||
case CharCode.LowerI: {
|
||||
ignoreCase = true;
|
||||
stripWhitespace(1);
|
||||
break;
|
||||
}
|
||||
case CharCode.LowerS: {
|
||||
ignoreCase = false;
|
||||
stripWhitespace(1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (selector.charCodeAt(selectorIndex) !==
|
||||
CharCode.RightSquareBracket) {
|
||||
throw new Error("Attribute selector didn't terminate");
|
||||
}
|
||||
selectorIndex += 1;
|
||||
const attributeSelector = {
|
||||
type: SelectorType.Attribute,
|
||||
name,
|
||||
action,
|
||||
value,
|
||||
namespace,
|
||||
ignoreCase,
|
||||
};
|
||||
tokens.push(attributeSelector);
|
||||
break;
|
||||
}
|
||||
case CharCode.Colon: {
|
||||
if (selector.charCodeAt(selectorIndex + 1) === CharCode.Colon) {
|
||||
tokens.push({
|
||||
type: SelectorType.PseudoElement,
|
||||
name: getName(2).toLowerCase(),
|
||||
data: selector.charCodeAt(selectorIndex) ===
|
||||
CharCode.LeftParenthesis
|
||||
? readValueWithParenthesis()
|
||||
: null,
|
||||
});
|
||||
break;
|
||||
}
|
||||
const name = getName(1).toLowerCase();
|
||||
if (pseudosToPseudoElements.has(name)) {
|
||||
tokens.push({
|
||||
type: SelectorType.PseudoElement,
|
||||
name,
|
||||
data: null,
|
||||
});
|
||||
break;
|
||||
}
|
||||
let data = null;
|
||||
if (selector.charCodeAt(selectorIndex) ===
|
||||
CharCode.LeftParenthesis) {
|
||||
if (unpackPseudos.has(name)) {
|
||||
if (isQuote(selector.charCodeAt(selectorIndex + 1))) {
|
||||
throw new Error(`Pseudo-selector ${name} cannot be quoted`);
|
||||
}
|
||||
data = [];
|
||||
selectorIndex = parseSelector(data, selector, selectorIndex + 1);
|
||||
if (selector.charCodeAt(selectorIndex) !==
|
||||
CharCode.RightParenthesis) {
|
||||
throw new Error(`Missing closing parenthesis in :${name} (${selector})`);
|
||||
}
|
||||
selectorIndex += 1;
|
||||
}
|
||||
else {
|
||||
data = readValueWithParenthesis();
|
||||
if (stripQuotesFromPseudos.has(name)) {
|
||||
const quot = data.charCodeAt(0);
|
||||
if (quot === data.charCodeAt(data.length - 1) &&
|
||||
isQuote(quot)) {
|
||||
data = data.slice(1, -1);
|
||||
}
|
||||
}
|
||||
data = unescapeCSS(data);
|
||||
}
|
||||
}
|
||||
tokens.push({ type: SelectorType.Pseudo, name, data });
|
||||
break;
|
||||
}
|
||||
case CharCode.Comma: {
|
||||
finalizeSubselector();
|
||||
tokens = [];
|
||||
stripWhitespace(1);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
if (selector.startsWith("/*", selectorIndex)) {
|
||||
const endIndex = selector.indexOf("*/", selectorIndex + 2);
|
||||
if (endIndex < 0) {
|
||||
throw new Error("Comment was not terminated");
|
||||
}
|
||||
selectorIndex = endIndex + 2;
|
||||
// Remove leading whitespace
|
||||
if (tokens.length === 0) {
|
||||
stripWhitespace(0);
|
||||
}
|
||||
break;
|
||||
}
|
||||
let namespace = null;
|
||||
let name;
|
||||
if (firstChar === CharCode.Asterisk) {
|
||||
selectorIndex += 1;
|
||||
name = "*";
|
||||
}
|
||||
else if (firstChar === CharCode.Pipe) {
|
||||
name = "";
|
||||
if (selector.charCodeAt(selectorIndex + 1) === CharCode.Pipe) {
|
||||
addTraversal(SelectorType.ColumnCombinator);
|
||||
stripWhitespace(2);
|
||||
break;
|
||||
}
|
||||
}
|
||||
else if (reName.test(selector.slice(selectorIndex))) {
|
||||
name = getName(0);
|
||||
}
|
||||
else {
|
||||
break loop;
|
||||
}
|
||||
if (selector.charCodeAt(selectorIndex) === CharCode.Pipe &&
|
||||
selector.charCodeAt(selectorIndex + 1) !== CharCode.Pipe) {
|
||||
namespace = name;
|
||||
if (selector.charCodeAt(selectorIndex + 1) ===
|
||||
CharCode.Asterisk) {
|
||||
name = "*";
|
||||
selectorIndex += 2;
|
||||
}
|
||||
else {
|
||||
name = getName(1);
|
||||
}
|
||||
}
|
||||
tokens.push(name === "*"
|
||||
? { type: SelectorType.Universal, namespace }
|
||||
: { type: SelectorType.Tag, name, namespace });
|
||||
}
|
||||
}
|
||||
}
|
||||
finalizeSubselector();
|
||||
return selectorIndex;
|
||||
}
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
import { type Selector } from "./types.js";
|
||||
/**
|
||||
* Turns `selector` back into a string.
|
||||
*
|
||||
* @param selector Selector to stringify.
|
||||
*/
|
||||
export declare function stringify(selector: Selector[][]): string;
|
||||
//# sourceMappingURL=stringify.d.ts.map
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"stringify.d.ts","sourceRoot":"","sources":["../../src/stringify.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,QAAQ,EAAiC,MAAM,YAAY,CAAC;AA8B1E;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,QAAQ,EAAE,QAAQ,EAAE,EAAE,GAAG,MAAM,CAUxD"}
|
||||
+147
@@ -0,0 +1,147 @@
|
||||
import { SelectorType, AttributeAction } from "./types.js";
|
||||
const attribValueChars = ["\\", '"'];
|
||||
const pseudoValueChars = [...attribValueChars, "(", ")"];
|
||||
const charsToEscapeInAttributeValue = new Set(attribValueChars.map((c) => c.charCodeAt(0)));
|
||||
const charsToEscapeInPseudoValue = new Set(pseudoValueChars.map((c) => c.charCodeAt(0)));
|
||||
const charsToEscapeInName = new Set([
|
||||
...pseudoValueChars,
|
||||
"~",
|
||||
"^",
|
||||
"$",
|
||||
"*",
|
||||
"+",
|
||||
"!",
|
||||
"|",
|
||||
":",
|
||||
"[",
|
||||
"]",
|
||||
" ",
|
||||
".",
|
||||
"%",
|
||||
].map((c) => c.charCodeAt(0)));
|
||||
/**
|
||||
* Turns `selector` back into a string.
|
||||
*
|
||||
* @param selector Selector to stringify.
|
||||
*/
|
||||
export function stringify(selector) {
|
||||
return selector
|
||||
.map((token) => token
|
||||
.map((token, index, array) => stringifyToken(token, index, array))
|
||||
.join(""))
|
||||
.join(", ");
|
||||
}
|
||||
function stringifyToken(token, index, array) {
|
||||
switch (token.type) {
|
||||
// Simple types
|
||||
case SelectorType.Child: {
|
||||
return index === 0 ? "> " : " > ";
|
||||
}
|
||||
case SelectorType.Parent: {
|
||||
return index === 0 ? "< " : " < ";
|
||||
}
|
||||
case SelectorType.Sibling: {
|
||||
return index === 0 ? "~ " : " ~ ";
|
||||
}
|
||||
case SelectorType.Adjacent: {
|
||||
return index === 0 ? "+ " : " + ";
|
||||
}
|
||||
case SelectorType.Descendant: {
|
||||
return " ";
|
||||
}
|
||||
case SelectorType.ColumnCombinator: {
|
||||
return index === 0 ? "|| " : " || ";
|
||||
}
|
||||
case SelectorType.Universal: {
|
||||
// Return an empty string if the selector isn't needed.
|
||||
return token.namespace === "*" &&
|
||||
index + 1 < array.length &&
|
||||
"name" in array[index + 1]
|
||||
? ""
|
||||
: `${getNamespace(token.namespace)}*`;
|
||||
}
|
||||
case SelectorType.Tag: {
|
||||
return getNamespacedName(token);
|
||||
}
|
||||
case SelectorType.PseudoElement: {
|
||||
return `::${escapeName(token.name, charsToEscapeInName)}${token.data === null
|
||||
? ""
|
||||
: `(${escapeName(token.data, charsToEscapeInPseudoValue)})`}`;
|
||||
}
|
||||
case SelectorType.Pseudo: {
|
||||
return `:${escapeName(token.name, charsToEscapeInName)}${token.data === null
|
||||
? ""
|
||||
: `(${typeof token.data === "string"
|
||||
? escapeName(token.data, charsToEscapeInPseudoValue)
|
||||
: stringify(token.data)})`}`;
|
||||
}
|
||||
case SelectorType.Attribute: {
|
||||
if (token.name === "id" &&
|
||||
token.action === AttributeAction.Equals &&
|
||||
token.ignoreCase === "quirks" &&
|
||||
!token.namespace) {
|
||||
return `#${escapeName(token.value, charsToEscapeInName)}`;
|
||||
}
|
||||
if (token.name === "class" &&
|
||||
token.action === AttributeAction.Element &&
|
||||
token.ignoreCase === "quirks" &&
|
||||
!token.namespace) {
|
||||
return `.${escapeName(token.value, charsToEscapeInName)}`;
|
||||
}
|
||||
const name = getNamespacedName(token);
|
||||
if (token.action === AttributeAction.Exists) {
|
||||
return `[${name}]`;
|
||||
}
|
||||
return `[${name}${getActionValue(token.action)}="${escapeName(token.value, charsToEscapeInAttributeValue)}"${token.ignoreCase === null ? "" : token.ignoreCase ? " i" : " s"}]`;
|
||||
}
|
||||
}
|
||||
}
|
||||
function getActionValue(action) {
|
||||
switch (action) {
|
||||
case AttributeAction.Equals: {
|
||||
return "";
|
||||
}
|
||||
case AttributeAction.Element: {
|
||||
return "~";
|
||||
}
|
||||
case AttributeAction.Start: {
|
||||
return "^";
|
||||
}
|
||||
case AttributeAction.End: {
|
||||
return "$";
|
||||
}
|
||||
case AttributeAction.Any: {
|
||||
return "*";
|
||||
}
|
||||
case AttributeAction.Not: {
|
||||
return "!";
|
||||
}
|
||||
case AttributeAction.Hyphen: {
|
||||
return "|";
|
||||
}
|
||||
default: {
|
||||
throw new Error("Shouldn't be here");
|
||||
}
|
||||
}
|
||||
}
|
||||
function getNamespacedName(token) {
|
||||
return `${getNamespace(token.namespace)}${escapeName(token.name, charsToEscapeInName)}`;
|
||||
}
|
||||
function getNamespace(namespace) {
|
||||
return namespace === null
|
||||
? ""
|
||||
: `${namespace === "*"
|
||||
? "*"
|
||||
: escapeName(namespace, charsToEscapeInName)}|`;
|
||||
}
|
||||
function escapeName(name, charsToEscape) {
|
||||
let lastIndex = 0;
|
||||
let escapedName = "";
|
||||
for (let index = 0; index < name.length; index++) {
|
||||
if (charsToEscape.has(name.charCodeAt(index))) {
|
||||
escapedName += `${name.slice(lastIndex, index)}\\${name.charAt(index)}`;
|
||||
lastIndex = index + 1;
|
||||
}
|
||||
}
|
||||
return escapedName.length > 0 ? escapedName + name.slice(lastIndex) : name;
|
||||
}
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
export type Selector = PseudoSelector | PseudoElement | AttributeSelector | TagSelector | UniversalSelector | Traversal;
|
||||
export declare enum SelectorType {
|
||||
Attribute = "attribute",
|
||||
Pseudo = "pseudo",
|
||||
PseudoElement = "pseudo-element",
|
||||
Tag = "tag",
|
||||
Universal = "universal",
|
||||
Adjacent = "adjacent",
|
||||
Child = "child",
|
||||
Descendant = "descendant",
|
||||
Parent = "parent",
|
||||
Sibling = "sibling",
|
||||
ColumnCombinator = "column-combinator"
|
||||
}
|
||||
/**
|
||||
* Modes for ignore case.
|
||||
*
|
||||
* This could be updated to an enum, and the object is
|
||||
* the current stand-in that will allow code to be updated
|
||||
* without big changes.
|
||||
*/
|
||||
export declare const IgnoreCaseMode: {
|
||||
readonly Unknown: null;
|
||||
readonly QuirksMode: "quirks";
|
||||
readonly IgnoreCase: true;
|
||||
readonly CaseSensitive: false;
|
||||
};
|
||||
export interface AttributeSelector {
|
||||
type: SelectorType.Attribute;
|
||||
name: string;
|
||||
action: AttributeAction;
|
||||
value: string;
|
||||
ignoreCase: "quirks" | boolean | null;
|
||||
namespace: string | null;
|
||||
}
|
||||
export type DataType = Selector[][] | null | string;
|
||||
export interface PseudoSelector {
|
||||
type: SelectorType.Pseudo;
|
||||
name: string;
|
||||
data: DataType;
|
||||
}
|
||||
export interface PseudoElement {
|
||||
type: SelectorType.PseudoElement;
|
||||
name: string;
|
||||
data: string | null;
|
||||
}
|
||||
export interface TagSelector {
|
||||
type: SelectorType.Tag;
|
||||
name: string;
|
||||
namespace: string | null;
|
||||
}
|
||||
export interface UniversalSelector {
|
||||
type: SelectorType.Universal;
|
||||
namespace: string | null;
|
||||
}
|
||||
export interface Traversal {
|
||||
type: TraversalType;
|
||||
}
|
||||
export declare enum AttributeAction {
|
||||
Any = "any",
|
||||
Element = "element",
|
||||
End = "end",
|
||||
Equals = "equals",
|
||||
Exists = "exists",
|
||||
Hyphen = "hyphen",
|
||||
Not = "not",
|
||||
Start = "start"
|
||||
}
|
||||
export type TraversalType = SelectorType.Adjacent | SelectorType.Child | SelectorType.Descendant | SelectorType.Parent | SelectorType.Sibling | SelectorType.ColumnCombinator;
|
||||
//# sourceMappingURL=types.d.ts.map
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,QAAQ,GACd,cAAc,GACd,aAAa,GACb,iBAAiB,GACjB,WAAW,GACX,iBAAiB,GACjB,SAAS,CAAC;AAEhB,oBAAY,YAAY;IACpB,SAAS,cAAc;IACvB,MAAM,WAAW;IACjB,aAAa,mBAAmB;IAChC,GAAG,QAAQ;IACX,SAAS,cAAc;IAGvB,QAAQ,aAAa;IACrB,KAAK,UAAU;IACf,UAAU,eAAe;IACzB,MAAM,WAAW;IACjB,OAAO,YAAY;IACnB,gBAAgB,sBAAsB;CACzC;AAED;;;;;;GAMG;AACH,eAAO,MAAM,cAAc;;;;;CAKjB,CAAC;AAEX,MAAM,WAAW,iBAAiB;IAC9B,IAAI,EAAE,YAAY,CAAC,SAAS,CAAC;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,eAAe,CAAC;IACxB,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,QAAQ,GAAG,OAAO,GAAG,IAAI,CAAC;IACtC,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;CAC5B;AAED,MAAM,MAAM,QAAQ,GAAG,QAAQ,EAAE,EAAE,GAAG,IAAI,GAAG,MAAM,CAAC;AAEpD,MAAM,WAAW,cAAc;IAC3B,IAAI,EAAE,YAAY,CAAC,MAAM,CAAC;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,QAAQ,CAAC;CAClB;AAED,MAAM,WAAW,aAAa;IAC1B,IAAI,EAAE,YAAY,CAAC,aAAa,CAAC;IACjC,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;CACvB;AAED,MAAM,WAAW,WAAW;IACxB,IAAI,EAAE,YAAY,CAAC,GAAG,CAAC;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;CAC5B;AAED,MAAM,WAAW,iBAAiB;IAC9B,IAAI,EAAE,YAAY,CAAC,SAAS,CAAC;IAC7B,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;CAC5B;AAED,MAAM,WAAW,SAAS;IACtB,IAAI,EAAE,aAAa,CAAC;CACvB;AAED,oBAAY,eAAe;IACvB,GAAG,QAAQ;IACX,OAAO,YAAY;IACnB,GAAG,QAAQ;IACX,MAAM,WAAW;IACjB,MAAM,WAAW;IACjB,MAAM,WAAW;IACjB,GAAG,QAAQ;IACX,KAAK,UAAU;CAClB;AAED,MAAM,MAAM,aAAa,GACnB,YAAY,CAAC,QAAQ,GACrB,YAAY,CAAC,KAAK,GAClB,YAAY,CAAC,UAAU,GACvB,YAAY,CAAC,MAAM,GACnB,YAAY,CAAC,OAAO,GACpB,YAAY,CAAC,gBAAgB,CAAC"}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
export var SelectorType;
|
||||
(function (SelectorType) {
|
||||
SelectorType["Attribute"] = "attribute";
|
||||
SelectorType["Pseudo"] = "pseudo";
|
||||
SelectorType["PseudoElement"] = "pseudo-element";
|
||||
SelectorType["Tag"] = "tag";
|
||||
SelectorType["Universal"] = "universal";
|
||||
// Traversals
|
||||
SelectorType["Adjacent"] = "adjacent";
|
||||
SelectorType["Child"] = "child";
|
||||
SelectorType["Descendant"] = "descendant";
|
||||
SelectorType["Parent"] = "parent";
|
||||
SelectorType["Sibling"] = "sibling";
|
||||
SelectorType["ColumnCombinator"] = "column-combinator";
|
||||
})(SelectorType || (SelectorType = {}));
|
||||
/**
|
||||
* Modes for ignore case.
|
||||
*
|
||||
* This could be updated to an enum, and the object is
|
||||
* the current stand-in that will allow code to be updated
|
||||
* without big changes.
|
||||
*/
|
||||
export const IgnoreCaseMode = {
|
||||
Unknown: null,
|
||||
QuirksMode: "quirks",
|
||||
IgnoreCase: true,
|
||||
CaseSensitive: false,
|
||||
};
|
||||
export var AttributeAction;
|
||||
(function (AttributeAction) {
|
||||
AttributeAction["Any"] = "any";
|
||||
AttributeAction["Element"] = "element";
|
||||
AttributeAction["End"] = "end";
|
||||
AttributeAction["Equals"] = "equals";
|
||||
AttributeAction["Exists"] = "exists";
|
||||
AttributeAction["Hyphen"] = "hyphen";
|
||||
AttributeAction["Not"] = "not";
|
||||
AttributeAction["Start"] = "start";
|
||||
})(AttributeAction || (AttributeAction = {}));
|
||||
+81
@@ -0,0 +1,81 @@
|
||||
{
|
||||
"name": "css-what",
|
||||
"version": "7.0.0",
|
||||
"description": "a CSS selector parser",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/fb55/css-what"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/fb55"
|
||||
},
|
||||
"license": "BSD-2-Clause",
|
||||
"author": "Felix Böhm <me@feedic.com> (http://feedic.com)",
|
||||
"sideEffects": false,
|
||||
"type": "module",
|
||||
"exports": {
|
||||
"./package.json": "./package.json",
|
||||
".": {
|
||||
"import": {
|
||||
"types": "./dist/esm/index.d.ts",
|
||||
"default": "./dist/esm/index.js"
|
||||
},
|
||||
"require": {
|
||||
"types": "./dist/commonjs/index.d.ts",
|
||||
"default": "./dist/commonjs/index.js"
|
||||
}
|
||||
}
|
||||
},
|
||||
"main": "./dist/commonjs/index.js",
|
||||
"module": "./dist/esm/index.js",
|
||||
"types": "./dist/commonjs/index.d.ts",
|
||||
"files": [
|
||||
"dist",
|
||||
"src"
|
||||
],
|
||||
"scripts": {
|
||||
"format": "npm run format:es && npm run format:prettier",
|
||||
"format:es": "npm run lint:es -- --fix",
|
||||
"format:prettier": "npm run prettier -- --write",
|
||||
"lint": "npm run lint:tsc && npm run lint:es && npm run lint:prettier",
|
||||
"lint:es": "eslint src",
|
||||
"lint:prettier": "npm run prettier -- --check",
|
||||
"lint:tsc": "tsc --noEmit",
|
||||
"prepublishOnly": "tshy",
|
||||
"prettier": "prettier '**/*.{ts,md,json,yml}'",
|
||||
"test": "npm run test:vi && npm run lint",
|
||||
"test:vi": "vitest run"
|
||||
},
|
||||
"prettier": {
|
||||
"tabWidth": 4
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.15.30",
|
||||
"@typescript-eslint/eslint-plugin": "^7.18.0",
|
||||
"@typescript-eslint/parser": "^7.18.0",
|
||||
"@vitest/coverage-v8": "^3.2.4",
|
||||
"eslint": "^8.57.1",
|
||||
"eslint-config-prettier": "^10.1.5",
|
||||
"eslint-plugin-n": "^17.20.0",
|
||||
"eslint-plugin-unicorn": "^55.0.0",
|
||||
"prettier": "^3.6.2",
|
||||
"tshy": "^3.0.2",
|
||||
"typescript": "^5.8.3",
|
||||
"vitest": "^3.2.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
},
|
||||
"tshy": {
|
||||
"exclude": [
|
||||
"**/*.spec.ts",
|
||||
"**/__fixtures__/*",
|
||||
"**/__tests__/*",
|
||||
"**/__snapshots__/*"
|
||||
],
|
||||
"exports": {
|
||||
"./package.json": "./package.json",
|
||||
".": "./src/index.ts"
|
||||
}
|
||||
}
|
||||
}
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
# css-what
|
||||
|
||||
[](https://github.com/fb55/css-what/actions/workflows/nodejs-test.yml)
|
||||
[](https://coveralls.io/github/fb55/css-what?branch=master)
|
||||
|
||||
A CSS selector parser.
|
||||
|
||||
## Example
|
||||
|
||||
```js
|
||||
import * as CSSwhat from "css-what";
|
||||
|
||||
CSSwhat.parse("foo[bar]:baz")
|
||||
|
||||
~> [
|
||||
[
|
||||
{ type: "tag", name: "foo" },
|
||||
{
|
||||
type: "attribute",
|
||||
name: "bar",
|
||||
action: "exists",
|
||||
value: "",
|
||||
ignoreCase: null
|
||||
},
|
||||
{ type: "pseudo", name: "baz", data: null }
|
||||
]
|
||||
]
|
||||
```
|
||||
|
||||
## API
|
||||
|
||||
**`CSSwhat.parse(selector)` - Parses `selector`.**
|
||||
|
||||
The function returns a two-dimensional array. The first array represents selectors separated by commas (eg. `sub1, sub2`), the second contains the relevant tokens for that selector. Possible token types are:
|
||||
|
||||
| name | properties | example | output |
|
||||
| ------------------- | --------------------------------------- | ------------- | ---------------------------------------------------------------------------------------- |
|
||||
| `tag` | `name` | `div` | `{ type: 'tag', name: 'div' }` |
|
||||
| `universal` | - | `*` | `{ type: 'universal' }` |
|
||||
| `pseudo` | `name`, `data` | `:name(data)` | `{ type: 'pseudo', name: 'name', data: 'data' }` |
|
||||
| `pseudo` | `name`, `data` | `:name` | `{ type: 'pseudo', name: 'name', data: null }` |
|
||||
| `pseudo-element` | `name` | `::name` | `{ type: 'pseudo-element', name: 'name' }` |
|
||||
| `attribute` | `name`, `action`, `value`, `ignoreCase` | `[attr]` | `{ type: 'attribute', name: 'attr', action: 'exists', value: '', ignoreCase: false }` |
|
||||
| `attribute` | `name`, `action`, `value`, `ignoreCase` | `[attr=val]` | `{ type: 'attribute', name: 'attr', action: 'equals', value: 'val', ignoreCase: false }` |
|
||||
| `attribute` | `name`, `action`, `value`, `ignoreCase` | `[attr^=val]` | `{ type: 'attribute', name: 'attr', action: 'start', value: 'val', ignoreCase: false }` |
|
||||
| `attribute` | `name`, `action`, `value`, `ignoreCase` | `[attr$=val]` | `{ type: 'attribute', name: 'attr', action: 'end', value: 'val', ignoreCase: false }` |
|
||||
| `child` | - | `>` | `{ type: 'child' }` |
|
||||
| `parent` | - | `<` | `{ type: 'parent' }` |
|
||||
| `sibling` | - | `~` | `{ type: 'sibling' }` |
|
||||
| `adjacent` | - | `+` | `{ type: 'adjacent' }` |
|
||||
| `descendant` | - | | `{ type: 'descendant' }` |
|
||||
| `column-combinator` | - | `\|\|` | `{ type: 'column-combinator' }` |
|
||||
|
||||
**`CSSwhat.stringify(selector)` - Turns `selector` back into a string.**
|
||||
|
||||
---
|
||||
|
||||
License: BSD-2-Clause
|
||||
|
||||
## Security contact information
|
||||
|
||||
To report a security vulnerability, please use the [Tidelift security contact](https://tidelift.com/security).
|
||||
Tidelift will coordinate the fix and disclosure.
|
||||
|
||||
## `css-what` for enterprise
|
||||
|
||||
Available as part of the Tidelift Subscription
|
||||
|
||||
The maintainers of `css-what` and thousands of other packages are working with Tidelift to deliver commercial support and maintenance for the open source dependencies you use to build your applications. Save time, reduce risk, and improve code health, while paying the maintainers of the exact dependencies you use. [Learn more.](https://tidelift.com/subscription/pkg/npm-css-what?utm_source=npm-css-what&utm_medium=referral&utm_campaign=enterprise&utm_term=repo)
|
||||
+17296
File diff suppressed because it is too large
Load Diff
+1028
File diff suppressed because it is too large
Load Diff
+3
@@ -0,0 +1,3 @@
|
||||
export * from "./types.js";
|
||||
export { isTraversal, parse } from "./parse.js";
|
||||
export { stringify } from "./stringify.js";
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { parse } from "./parse.js";
|
||||
import { tests } from "./__fixtures__/tests.js";
|
||||
|
||||
const broken = [
|
||||
"[",
|
||||
"(",
|
||||
"{",
|
||||
"()",
|
||||
"<>",
|
||||
"{}",
|
||||
",",
|
||||
",a",
|
||||
"a,",
|
||||
"[id=012345678901234567890123456789",
|
||||
"input[name=foo b]",
|
||||
"input[name!foo]",
|
||||
"input[name|]",
|
||||
"input[name=']",
|
||||
"input[name=foo[baz]]",
|
||||
':has("p")',
|
||||
":has(p",
|
||||
":foo(p()",
|
||||
"#",
|
||||
"##foo",
|
||||
"/*",
|
||||
];
|
||||
|
||||
describe("Parse", () => {
|
||||
describe("Own tests", () => {
|
||||
for (const [selector, expected, message] of tests) {
|
||||
it(message, () => expect(parse(selector)).toStrictEqual(expected));
|
||||
}
|
||||
});
|
||||
|
||||
describe("Collected selectors (qwery, sizzle, nwmatcher)", () => {
|
||||
const out = JSON.parse(
|
||||
readFileSync(`${__dirname}/__fixtures__/out.json`, "utf8"),
|
||||
);
|
||||
for (const s of Object.keys(out)) {
|
||||
it(s, () => {
|
||||
expect(parse(s)).toStrictEqual(out[s]);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe("Broken selectors", () => {
|
||||
for (const selector of broken) {
|
||||
it(`should not parse — ${selector}`, () => {
|
||||
expect(() => parse(selector)).toThrow(Error);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
it("should ignore comments", () => {
|
||||
expect(parse("/* comment1 */ /**/ foo /*comment2*/")).toEqual([
|
||||
[{ name: "foo", namespace: null, type: "tag" }],
|
||||
]);
|
||||
|
||||
expect(() => parse("/*/")).toThrowError("Comment was not terminated");
|
||||
});
|
||||
|
||||
it("should support legacy pseudo-elements with single colon", () => {
|
||||
expect(parse(":before")).toEqual([
|
||||
[{ name: "before", data: null, type: "pseudo-element" }],
|
||||
]);
|
||||
});
|
||||
});
|
||||
+634
@@ -0,0 +1,634 @@
|
||||
import {
|
||||
type Selector,
|
||||
SelectorType,
|
||||
type AttributeSelector,
|
||||
type Traversal,
|
||||
AttributeAction,
|
||||
type TraversalType,
|
||||
type DataType,
|
||||
} from "./types.js";
|
||||
|
||||
const reName = /^[^#\\]?(?:\\(?:[\da-f]{1,6}\s?|.)|[\w\u00B0-\uFFFF-])+/;
|
||||
const reEscape = /\\([\da-f]{1,6}\s?|(\s)|.)/gi;
|
||||
|
||||
const enum CharCode {
|
||||
LeftParenthesis = 40,
|
||||
RightParenthesis = 41,
|
||||
LeftSquareBracket = 91,
|
||||
RightSquareBracket = 93,
|
||||
Comma = 44,
|
||||
Period = 46,
|
||||
Colon = 58,
|
||||
SingleQuote = 39,
|
||||
DoubleQuote = 34,
|
||||
Plus = 43,
|
||||
Tilde = 126,
|
||||
QuestionMark = 63,
|
||||
ExclamationMark = 33,
|
||||
Slash = 47,
|
||||
Equal = 61,
|
||||
Dollar = 36,
|
||||
Pipe = 124,
|
||||
Circumflex = 94,
|
||||
Asterisk = 42,
|
||||
GreaterThan = 62,
|
||||
LessThan = 60,
|
||||
Hash = 35,
|
||||
LowerI = 105,
|
||||
LowerS = 115,
|
||||
BackSlash = 92,
|
||||
|
||||
// Whitespace
|
||||
Space = 32,
|
||||
Tab = 9,
|
||||
NewLine = 10,
|
||||
FormFeed = 12,
|
||||
CarriageReturn = 13,
|
||||
}
|
||||
|
||||
const actionTypes = new Map<number, AttributeAction>([
|
||||
[CharCode.Tilde, AttributeAction.Element],
|
||||
[CharCode.Circumflex, AttributeAction.Start],
|
||||
[CharCode.Dollar, AttributeAction.End],
|
||||
[CharCode.Asterisk, AttributeAction.Any],
|
||||
[CharCode.ExclamationMark, AttributeAction.Not],
|
||||
[CharCode.Pipe, AttributeAction.Hyphen],
|
||||
]);
|
||||
|
||||
// Pseudos, whose data property is parsed as well.
|
||||
const unpackPseudos = new Set([
|
||||
"has",
|
||||
"not",
|
||||
"matches",
|
||||
"is",
|
||||
"where",
|
||||
"host",
|
||||
"host-context",
|
||||
]);
|
||||
|
||||
/**
|
||||
* Pseudo elements defined in CSS Level 1 and CSS Level 2 can be written with
|
||||
* a single colon; eg. :before will turn into ::before.
|
||||
*
|
||||
* @see {@link https://www.w3.org/TR/2018/WD-selectors-4-20181121/#pseudo-element-syntax}
|
||||
*/
|
||||
const pseudosToPseudoElements = new Set([
|
||||
"before",
|
||||
"after",
|
||||
"first-line",
|
||||
"first-letter",
|
||||
]);
|
||||
|
||||
/**
|
||||
* Checks whether a specific selector is a traversal.
|
||||
* This is useful eg. in swapping the order of elements that
|
||||
* are not traversals.
|
||||
*
|
||||
* @param selector Selector to check.
|
||||
*/
|
||||
export function isTraversal(selector: Selector): selector is Traversal {
|
||||
switch (selector.type) {
|
||||
case SelectorType.Adjacent:
|
||||
case SelectorType.Child:
|
||||
case SelectorType.Descendant:
|
||||
case SelectorType.Parent:
|
||||
case SelectorType.Sibling:
|
||||
case SelectorType.ColumnCombinator: {
|
||||
return true;
|
||||
}
|
||||
default: {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const stripQuotesFromPseudos = new Set(["contains", "icontains"]);
|
||||
|
||||
// Unescape function taken from https://github.com/jquery/sizzle/blob/master/src/sizzle.js#L152
|
||||
function funescape(_: string, escaped: string, escapedWhitespace?: string) {
|
||||
const high = Number.parseInt(escaped, 16) - 0x1_00_00;
|
||||
|
||||
// NaN means non-codepoint
|
||||
return high !== high || escapedWhitespace
|
||||
? escaped
|
||||
: high < 0
|
||||
? // BMP codepoint
|
||||
String.fromCharCode(high + 0x1_00_00)
|
||||
: // Supplemental Plane codepoint (surrogate pair)
|
||||
String.fromCharCode(
|
||||
(high >> 10) | 0xd8_00,
|
||||
(high & 0x3_ff) | 0xdc_00,
|
||||
);
|
||||
}
|
||||
|
||||
function unescapeCSS(cssString: string) {
|
||||
return cssString.replace(reEscape, funescape);
|
||||
}
|
||||
|
||||
function isQuote(c: number): boolean {
|
||||
return c === CharCode.SingleQuote || c === CharCode.DoubleQuote;
|
||||
}
|
||||
|
||||
function isWhitespace(c: number): boolean {
|
||||
return (
|
||||
c === CharCode.Space ||
|
||||
c === CharCode.Tab ||
|
||||
c === CharCode.NewLine ||
|
||||
c === CharCode.FormFeed ||
|
||||
c === CharCode.CarriageReturn
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses `selector`.
|
||||
*
|
||||
* @param selector Selector to parse.
|
||||
* @returns Returns a two-dimensional array.
|
||||
* The first dimension represents selectors separated by commas (eg. `sub1, sub2`),
|
||||
* the second contains the relevant tokens for that selector.
|
||||
*/
|
||||
export function parse(selector: string): Selector[][] {
|
||||
const subselects: Selector[][] = [];
|
||||
|
||||
const endIndex = parseSelector(subselects, `${selector}`, 0);
|
||||
|
||||
if (endIndex < selector.length) {
|
||||
throw new Error(`Unmatched selector: ${selector.slice(endIndex)}`);
|
||||
}
|
||||
|
||||
return subselects;
|
||||
}
|
||||
|
||||
function parseSelector(
|
||||
subselects: Selector[][],
|
||||
selector: string,
|
||||
selectorIndex: number,
|
||||
): number {
|
||||
let tokens: Selector[] = [];
|
||||
|
||||
function getName(offset: number): string {
|
||||
const match = selector.slice(selectorIndex + offset).match(reName);
|
||||
|
||||
if (!match) {
|
||||
throw new Error(
|
||||
`Expected name, found ${selector.slice(selectorIndex)}`,
|
||||
);
|
||||
}
|
||||
|
||||
const [name] = match;
|
||||
selectorIndex += offset + name.length;
|
||||
return unescapeCSS(name);
|
||||
}
|
||||
|
||||
function stripWhitespace(offset: number) {
|
||||
selectorIndex += offset;
|
||||
|
||||
while (
|
||||
selectorIndex < selector.length &&
|
||||
isWhitespace(selector.charCodeAt(selectorIndex))
|
||||
) {
|
||||
selectorIndex++;
|
||||
}
|
||||
}
|
||||
|
||||
function readValueWithParenthesis(): string {
|
||||
selectorIndex += 1;
|
||||
const start = selectorIndex;
|
||||
|
||||
for (
|
||||
let counter = 1;
|
||||
selectorIndex < selector.length;
|
||||
selectorIndex++
|
||||
) {
|
||||
switch (selector.charCodeAt(selectorIndex)) {
|
||||
case CharCode.BackSlash: {
|
||||
// Skip next character
|
||||
selectorIndex += 1;
|
||||
break;
|
||||
}
|
||||
case CharCode.LeftParenthesis: {
|
||||
counter += 1;
|
||||
break;
|
||||
}
|
||||
case CharCode.RightParenthesis: {
|
||||
counter -= 1;
|
||||
|
||||
if (counter === 0) {
|
||||
return unescapeCSS(
|
||||
selector.slice(start, selectorIndex++),
|
||||
);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error("Parenthesis not matched");
|
||||
}
|
||||
|
||||
function ensureNotTraversal() {
|
||||
if (tokens.length > 0 && isTraversal(tokens[tokens.length - 1])) {
|
||||
throw new Error("Did not expect successive traversals.");
|
||||
}
|
||||
}
|
||||
|
||||
function addTraversal(type: TraversalType) {
|
||||
if (
|
||||
tokens.length > 0 &&
|
||||
tokens[tokens.length - 1].type === SelectorType.Descendant
|
||||
) {
|
||||
tokens[tokens.length - 1].type = type;
|
||||
return;
|
||||
}
|
||||
|
||||
ensureNotTraversal();
|
||||
|
||||
tokens.push({ type });
|
||||
}
|
||||
|
||||
function addSpecialAttribute(name: string, action: AttributeAction) {
|
||||
tokens.push({
|
||||
type: SelectorType.Attribute,
|
||||
name,
|
||||
action,
|
||||
value: getName(1),
|
||||
namespace: null,
|
||||
ignoreCase: "quirks",
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* We have finished parsing the current part of the selector.
|
||||
*
|
||||
* Remove descendant tokens at the end if they exist,
|
||||
* and return the last index, so that parsing can be
|
||||
* picked up from here.
|
||||
*/
|
||||
function finalizeSubselector() {
|
||||
if (
|
||||
tokens.length > 0 &&
|
||||
tokens[tokens.length - 1].type === SelectorType.Descendant
|
||||
) {
|
||||
tokens.pop();
|
||||
}
|
||||
|
||||
if (tokens.length === 0) {
|
||||
throw new Error("Empty sub-selector");
|
||||
}
|
||||
|
||||
subselects.push(tokens);
|
||||
}
|
||||
|
||||
stripWhitespace(0);
|
||||
|
||||
if (selector.length === selectorIndex) {
|
||||
return selectorIndex;
|
||||
}
|
||||
|
||||
loop: while (selectorIndex < selector.length) {
|
||||
const firstChar = selector.charCodeAt(selectorIndex);
|
||||
|
||||
switch (firstChar) {
|
||||
// Whitespace
|
||||
case CharCode.Space:
|
||||
case CharCode.Tab:
|
||||
case CharCode.NewLine:
|
||||
case CharCode.FormFeed:
|
||||
case CharCode.CarriageReturn: {
|
||||
if (
|
||||
tokens.length === 0 ||
|
||||
tokens[0].type !== SelectorType.Descendant
|
||||
) {
|
||||
ensureNotTraversal();
|
||||
tokens.push({ type: SelectorType.Descendant });
|
||||
}
|
||||
|
||||
stripWhitespace(1);
|
||||
break;
|
||||
}
|
||||
// Traversals
|
||||
case CharCode.GreaterThan: {
|
||||
addTraversal(SelectorType.Child);
|
||||
stripWhitespace(1);
|
||||
break;
|
||||
}
|
||||
case CharCode.LessThan: {
|
||||
addTraversal(SelectorType.Parent);
|
||||
stripWhitespace(1);
|
||||
break;
|
||||
}
|
||||
case CharCode.Tilde: {
|
||||
addTraversal(SelectorType.Sibling);
|
||||
stripWhitespace(1);
|
||||
break;
|
||||
}
|
||||
case CharCode.Plus: {
|
||||
addTraversal(SelectorType.Adjacent);
|
||||
stripWhitespace(1);
|
||||
break;
|
||||
}
|
||||
// Special attribute selectors: .class, #id
|
||||
case CharCode.Period: {
|
||||
addSpecialAttribute("class", AttributeAction.Element);
|
||||
break;
|
||||
}
|
||||
case CharCode.Hash: {
|
||||
addSpecialAttribute("id", AttributeAction.Equals);
|
||||
break;
|
||||
}
|
||||
case CharCode.LeftSquareBracket: {
|
||||
stripWhitespace(1);
|
||||
|
||||
// Determine attribute name and namespace
|
||||
|
||||
let name: string;
|
||||
let namespace: string | null = null;
|
||||
|
||||
if (selector.charCodeAt(selectorIndex) === CharCode.Pipe) {
|
||||
// Equivalent to no namespace
|
||||
name = getName(1);
|
||||
} else if (selector.startsWith("*|", selectorIndex)) {
|
||||
namespace = "*";
|
||||
name = getName(2);
|
||||
} else {
|
||||
name = getName(0);
|
||||
|
||||
if (
|
||||
selector.charCodeAt(selectorIndex) === CharCode.Pipe &&
|
||||
selector.charCodeAt(selectorIndex + 1) !==
|
||||
CharCode.Equal
|
||||
) {
|
||||
namespace = name;
|
||||
name = getName(1);
|
||||
}
|
||||
}
|
||||
|
||||
stripWhitespace(0);
|
||||
|
||||
// Determine comparison operation
|
||||
|
||||
let action: AttributeAction = AttributeAction.Exists;
|
||||
const possibleAction = actionTypes.get(
|
||||
selector.charCodeAt(selectorIndex),
|
||||
);
|
||||
|
||||
if (possibleAction) {
|
||||
action = possibleAction;
|
||||
|
||||
if (
|
||||
selector.charCodeAt(selectorIndex + 1) !==
|
||||
CharCode.Equal
|
||||
) {
|
||||
throw new Error("Expected `=`");
|
||||
}
|
||||
|
||||
stripWhitespace(2);
|
||||
} else if (
|
||||
selector.charCodeAt(selectorIndex) === CharCode.Equal
|
||||
) {
|
||||
action = AttributeAction.Equals;
|
||||
stripWhitespace(1);
|
||||
}
|
||||
|
||||
// Determine value
|
||||
|
||||
let value = "";
|
||||
let ignoreCase: boolean | null = null;
|
||||
|
||||
if (action !== "exists") {
|
||||
if (isQuote(selector.charCodeAt(selectorIndex))) {
|
||||
const quote = selector.charCodeAt(selectorIndex);
|
||||
selectorIndex += 1;
|
||||
const sectionStart = selectorIndex;
|
||||
while (
|
||||
selectorIndex < selector.length &&
|
||||
selector.charCodeAt(selectorIndex) !== quote
|
||||
) {
|
||||
selectorIndex +=
|
||||
// Skip next character if it is escaped
|
||||
selector.charCodeAt(selectorIndex) ===
|
||||
CharCode.BackSlash
|
||||
? 2
|
||||
: 1;
|
||||
}
|
||||
|
||||
if (selector.charCodeAt(selectorIndex) !== quote) {
|
||||
throw new Error("Attribute value didn't end");
|
||||
}
|
||||
|
||||
value = unescapeCSS(
|
||||
selector.slice(sectionStart, selectorIndex),
|
||||
);
|
||||
selectorIndex += 1;
|
||||
} else {
|
||||
const valueStart = selectorIndex;
|
||||
|
||||
while (
|
||||
selectorIndex < selector.length &&
|
||||
!isWhitespace(selector.charCodeAt(selectorIndex)) &&
|
||||
selector.charCodeAt(selectorIndex) !==
|
||||
CharCode.RightSquareBracket
|
||||
) {
|
||||
selectorIndex +=
|
||||
// Skip next character if it is escaped
|
||||
selector.charCodeAt(selectorIndex) ===
|
||||
CharCode.BackSlash
|
||||
? 2
|
||||
: 1;
|
||||
}
|
||||
|
||||
value = unescapeCSS(
|
||||
selector.slice(valueStart, selectorIndex),
|
||||
);
|
||||
}
|
||||
|
||||
stripWhitespace(0);
|
||||
|
||||
// See if we have a force ignore flag
|
||||
switch (selector.charCodeAt(selectorIndex) | 0x20) {
|
||||
// If the forceIgnore flag is set (either `i` or `s`), use that value
|
||||
case CharCode.LowerI: {
|
||||
ignoreCase = true;
|
||||
stripWhitespace(1);
|
||||
break;
|
||||
}
|
||||
case CharCode.LowerS: {
|
||||
ignoreCase = false;
|
||||
stripWhitespace(1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
selector.charCodeAt(selectorIndex) !==
|
||||
CharCode.RightSquareBracket
|
||||
) {
|
||||
throw new Error("Attribute selector didn't terminate");
|
||||
}
|
||||
|
||||
selectorIndex += 1;
|
||||
|
||||
const attributeSelector: AttributeSelector = {
|
||||
type: SelectorType.Attribute,
|
||||
name,
|
||||
action,
|
||||
value,
|
||||
namespace,
|
||||
ignoreCase,
|
||||
};
|
||||
|
||||
tokens.push(attributeSelector);
|
||||
break;
|
||||
}
|
||||
case CharCode.Colon: {
|
||||
if (selector.charCodeAt(selectorIndex + 1) === CharCode.Colon) {
|
||||
tokens.push({
|
||||
type: SelectorType.PseudoElement,
|
||||
name: getName(2).toLowerCase(),
|
||||
data:
|
||||
selector.charCodeAt(selectorIndex) ===
|
||||
CharCode.LeftParenthesis
|
||||
? readValueWithParenthesis()
|
||||
: null,
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
const name = getName(1).toLowerCase();
|
||||
|
||||
if (pseudosToPseudoElements.has(name)) {
|
||||
tokens.push({
|
||||
type: SelectorType.PseudoElement,
|
||||
name,
|
||||
data: null,
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
let data: DataType = null;
|
||||
|
||||
if (
|
||||
selector.charCodeAt(selectorIndex) ===
|
||||
CharCode.LeftParenthesis
|
||||
) {
|
||||
if (unpackPseudos.has(name)) {
|
||||
if (isQuote(selector.charCodeAt(selectorIndex + 1))) {
|
||||
throw new Error(
|
||||
`Pseudo-selector ${name} cannot be quoted`,
|
||||
);
|
||||
}
|
||||
|
||||
data = [];
|
||||
selectorIndex = parseSelector(
|
||||
data,
|
||||
selector,
|
||||
selectorIndex + 1,
|
||||
);
|
||||
|
||||
if (
|
||||
selector.charCodeAt(selectorIndex) !==
|
||||
CharCode.RightParenthesis
|
||||
) {
|
||||
throw new Error(
|
||||
`Missing closing parenthesis in :${name} (${selector})`,
|
||||
);
|
||||
}
|
||||
|
||||
selectorIndex += 1;
|
||||
} else {
|
||||
data = readValueWithParenthesis();
|
||||
|
||||
if (stripQuotesFromPseudos.has(name)) {
|
||||
const quot = data.charCodeAt(0);
|
||||
|
||||
if (
|
||||
quot === data.charCodeAt(data.length - 1) &&
|
||||
isQuote(quot)
|
||||
) {
|
||||
data = data.slice(1, -1);
|
||||
}
|
||||
}
|
||||
|
||||
data = unescapeCSS(data);
|
||||
}
|
||||
}
|
||||
|
||||
tokens.push({ type: SelectorType.Pseudo, name, data });
|
||||
break;
|
||||
}
|
||||
case CharCode.Comma: {
|
||||
finalizeSubselector();
|
||||
tokens = [];
|
||||
stripWhitespace(1);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
if (selector.startsWith("/*", selectorIndex)) {
|
||||
const endIndex = selector.indexOf("*/", selectorIndex + 2);
|
||||
|
||||
if (endIndex < 0) {
|
||||
throw new Error("Comment was not terminated");
|
||||
}
|
||||
|
||||
selectorIndex = endIndex + 2;
|
||||
|
||||
// Remove leading whitespace
|
||||
if (tokens.length === 0) {
|
||||
stripWhitespace(0);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
let namespace = null;
|
||||
let name: string;
|
||||
|
||||
if (firstChar === CharCode.Asterisk) {
|
||||
selectorIndex += 1;
|
||||
name = "*";
|
||||
} else if (firstChar === CharCode.Pipe) {
|
||||
name = "";
|
||||
|
||||
if (
|
||||
selector.charCodeAt(selectorIndex + 1) === CharCode.Pipe
|
||||
) {
|
||||
addTraversal(SelectorType.ColumnCombinator);
|
||||
stripWhitespace(2);
|
||||
break;
|
||||
}
|
||||
} else if (reName.test(selector.slice(selectorIndex))) {
|
||||
name = getName(0);
|
||||
} else {
|
||||
break loop;
|
||||
}
|
||||
|
||||
if (
|
||||
selector.charCodeAt(selectorIndex) === CharCode.Pipe &&
|
||||
selector.charCodeAt(selectorIndex + 1) !== CharCode.Pipe
|
||||
) {
|
||||
namespace = name;
|
||||
if (
|
||||
selector.charCodeAt(selectorIndex + 1) ===
|
||||
CharCode.Asterisk
|
||||
) {
|
||||
name = "*";
|
||||
selectorIndex += 2;
|
||||
} else {
|
||||
name = getName(1);
|
||||
}
|
||||
}
|
||||
|
||||
tokens.push(
|
||||
name === "*"
|
||||
? { type: SelectorType.Universal, namespace }
|
||||
: { type: SelectorType.Tag, name, namespace },
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
finalizeSubselector();
|
||||
return selectorIndex;
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { parse, stringify } from "./index.js";
|
||||
import { tests } from "./__fixtures__/tests.js";
|
||||
|
||||
describe("Stringify & re-parse", () => {
|
||||
describe("Own tests", () => {
|
||||
for (const [selector, expected, message] of tests) {
|
||||
it(`${message} (${selector})`, () => {
|
||||
expect(parse(stringify(expected))).toStrictEqual(expected);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
it("Collected Selectors (qwery, sizzle, nwmatcher)", () => {
|
||||
const out = JSON.parse(
|
||||
readFileSync(`${__dirname}/__fixtures__/out.json`, "utf8"),
|
||||
);
|
||||
for (const s of Object.keys(out)) {
|
||||
expect(parse(stringify(out[s]))).toStrictEqual(out[s]);
|
||||
}
|
||||
});
|
||||
});
|
||||
+204
@@ -0,0 +1,204 @@
|
||||
import { type Selector, SelectorType, AttributeAction } from "./types.js";
|
||||
|
||||
const attribValueChars = ["\\", '"'];
|
||||
const pseudoValueChars = [...attribValueChars, "(", ")"];
|
||||
|
||||
const charsToEscapeInAttributeValue = new Set(
|
||||
attribValueChars.map((c) => c.charCodeAt(0)),
|
||||
);
|
||||
const charsToEscapeInPseudoValue = new Set(
|
||||
pseudoValueChars.map((c) => c.charCodeAt(0)),
|
||||
);
|
||||
const charsToEscapeInName = new Set(
|
||||
[
|
||||
...pseudoValueChars,
|
||||
"~",
|
||||
"^",
|
||||
"$",
|
||||
"*",
|
||||
"+",
|
||||
"!",
|
||||
"|",
|
||||
":",
|
||||
"[",
|
||||
"]",
|
||||
" ",
|
||||
".",
|
||||
"%",
|
||||
].map((c) => c.charCodeAt(0)),
|
||||
);
|
||||
|
||||
/**
|
||||
* Turns `selector` back into a string.
|
||||
*
|
||||
* @param selector Selector to stringify.
|
||||
*/
|
||||
export function stringify(selector: Selector[][]): string {
|
||||
return selector
|
||||
.map((token) =>
|
||||
token
|
||||
.map((token, index, array) =>
|
||||
stringifyToken(token, index, array),
|
||||
)
|
||||
.join(""),
|
||||
)
|
||||
.join(", ");
|
||||
}
|
||||
|
||||
function stringifyToken(
|
||||
token: Selector,
|
||||
index: number,
|
||||
array: Selector[],
|
||||
): string {
|
||||
switch (token.type) {
|
||||
// Simple types
|
||||
case SelectorType.Child: {
|
||||
return index === 0 ? "> " : " > ";
|
||||
}
|
||||
case SelectorType.Parent: {
|
||||
return index === 0 ? "< " : " < ";
|
||||
}
|
||||
case SelectorType.Sibling: {
|
||||
return index === 0 ? "~ " : " ~ ";
|
||||
}
|
||||
case SelectorType.Adjacent: {
|
||||
return index === 0 ? "+ " : " + ";
|
||||
}
|
||||
case SelectorType.Descendant: {
|
||||
return " ";
|
||||
}
|
||||
case SelectorType.ColumnCombinator: {
|
||||
return index === 0 ? "|| " : " || ";
|
||||
}
|
||||
case SelectorType.Universal: {
|
||||
// Return an empty string if the selector isn't needed.
|
||||
return token.namespace === "*" &&
|
||||
index + 1 < array.length &&
|
||||
"name" in array[index + 1]
|
||||
? ""
|
||||
: `${getNamespace(token.namespace)}*`;
|
||||
}
|
||||
|
||||
case SelectorType.Tag: {
|
||||
return getNamespacedName(token);
|
||||
}
|
||||
|
||||
case SelectorType.PseudoElement: {
|
||||
return `::${escapeName(token.name, charsToEscapeInName)}${
|
||||
token.data === null
|
||||
? ""
|
||||
: `(${escapeName(token.data, charsToEscapeInPseudoValue)})`
|
||||
}`;
|
||||
}
|
||||
|
||||
case SelectorType.Pseudo: {
|
||||
return `:${escapeName(token.name, charsToEscapeInName)}${
|
||||
token.data === null
|
||||
? ""
|
||||
: `(${
|
||||
typeof token.data === "string"
|
||||
? escapeName(
|
||||
token.data,
|
||||
charsToEscapeInPseudoValue,
|
||||
)
|
||||
: stringify(token.data)
|
||||
})`
|
||||
}`;
|
||||
}
|
||||
|
||||
case SelectorType.Attribute: {
|
||||
if (
|
||||
token.name === "id" &&
|
||||
token.action === AttributeAction.Equals &&
|
||||
token.ignoreCase === "quirks" &&
|
||||
!token.namespace
|
||||
) {
|
||||
return `#${escapeName(token.value, charsToEscapeInName)}`;
|
||||
}
|
||||
if (
|
||||
token.name === "class" &&
|
||||
token.action === AttributeAction.Element &&
|
||||
token.ignoreCase === "quirks" &&
|
||||
!token.namespace
|
||||
) {
|
||||
return `.${escapeName(token.value, charsToEscapeInName)}`;
|
||||
}
|
||||
|
||||
const name = getNamespacedName(token);
|
||||
|
||||
if (token.action === AttributeAction.Exists) {
|
||||
return `[${name}]`;
|
||||
}
|
||||
|
||||
return `[${name}${getActionValue(token.action)}="${escapeName(
|
||||
token.value,
|
||||
charsToEscapeInAttributeValue,
|
||||
)}"${
|
||||
token.ignoreCase === null ? "" : token.ignoreCase ? " i" : " s"
|
||||
}]`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getActionValue(action: AttributeAction): string {
|
||||
switch (action) {
|
||||
case AttributeAction.Equals: {
|
||||
return "";
|
||||
}
|
||||
case AttributeAction.Element: {
|
||||
return "~";
|
||||
}
|
||||
case AttributeAction.Start: {
|
||||
return "^";
|
||||
}
|
||||
case AttributeAction.End: {
|
||||
return "$";
|
||||
}
|
||||
case AttributeAction.Any: {
|
||||
return "*";
|
||||
}
|
||||
case AttributeAction.Not: {
|
||||
return "!";
|
||||
}
|
||||
case AttributeAction.Hyphen: {
|
||||
return "|";
|
||||
}
|
||||
default: {
|
||||
throw new Error("Shouldn't be here");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getNamespacedName(token: {
|
||||
name: string;
|
||||
namespace: string | null;
|
||||
}): string {
|
||||
return `${getNamespace(token.namespace)}${escapeName(
|
||||
token.name,
|
||||
charsToEscapeInName,
|
||||
)}`;
|
||||
}
|
||||
|
||||
function getNamespace(namespace: string | null): string {
|
||||
return namespace === null
|
||||
? ""
|
||||
: `${
|
||||
namespace === "*"
|
||||
? "*"
|
||||
: escapeName(namespace, charsToEscapeInName)
|
||||
}|`;
|
||||
}
|
||||
|
||||
function escapeName(name: string, charsToEscape: Set<number>): string {
|
||||
let lastIndex = 0;
|
||||
let escapedName = "";
|
||||
|
||||
for (let index = 0; index < name.length; index++) {
|
||||
if (charsToEscape.has(name.charCodeAt(index))) {
|
||||
escapedName += `${name.slice(lastIndex, index)}\\${name.charAt(index)}`;
|
||||
lastIndex = index + 1;
|
||||
}
|
||||
}
|
||||
|
||||
return escapedName.length > 0 ? escapedName + name.slice(lastIndex) : name;
|
||||
}
|
||||
+94
@@ -0,0 +1,94 @@
|
||||
export type Selector =
|
||||
| PseudoSelector
|
||||
| PseudoElement
|
||||
| AttributeSelector
|
||||
| TagSelector
|
||||
| UniversalSelector
|
||||
| Traversal;
|
||||
|
||||
export enum SelectorType {
|
||||
Attribute = "attribute",
|
||||
Pseudo = "pseudo",
|
||||
PseudoElement = "pseudo-element",
|
||||
Tag = "tag",
|
||||
Universal = "universal",
|
||||
|
||||
// Traversals
|
||||
Adjacent = "adjacent",
|
||||
Child = "child",
|
||||
Descendant = "descendant",
|
||||
Parent = "parent",
|
||||
Sibling = "sibling",
|
||||
ColumnCombinator = "column-combinator",
|
||||
}
|
||||
|
||||
/**
|
||||
* Modes for ignore case.
|
||||
*
|
||||
* This could be updated to an enum, and the object is
|
||||
* the current stand-in that will allow code to be updated
|
||||
* without big changes.
|
||||
*/
|
||||
export const IgnoreCaseMode = {
|
||||
Unknown: null,
|
||||
QuirksMode: "quirks",
|
||||
IgnoreCase: true,
|
||||
CaseSensitive: false,
|
||||
} as const;
|
||||
|
||||
export interface AttributeSelector {
|
||||
type: SelectorType.Attribute;
|
||||
name: string;
|
||||
action: AttributeAction;
|
||||
value: string;
|
||||
ignoreCase: "quirks" | boolean | null;
|
||||
namespace: string | null;
|
||||
}
|
||||
|
||||
export type DataType = Selector[][] | null | string;
|
||||
|
||||
export interface PseudoSelector {
|
||||
type: SelectorType.Pseudo;
|
||||
name: string;
|
||||
data: DataType;
|
||||
}
|
||||
|
||||
export interface PseudoElement {
|
||||
type: SelectorType.PseudoElement;
|
||||
name: string;
|
||||
data: string | null;
|
||||
}
|
||||
|
||||
export interface TagSelector {
|
||||
type: SelectorType.Tag;
|
||||
name: string;
|
||||
namespace: string | null;
|
||||
}
|
||||
|
||||
export interface UniversalSelector {
|
||||
type: SelectorType.Universal;
|
||||
namespace: string | null;
|
||||
}
|
||||
|
||||
export interface Traversal {
|
||||
type: TraversalType;
|
||||
}
|
||||
|
||||
export enum AttributeAction {
|
||||
Any = "any",
|
||||
Element = "element",
|
||||
End = "end",
|
||||
Equals = "equals",
|
||||
Exists = "exists",
|
||||
Hyphen = "hyphen",
|
||||
Not = "not",
|
||||
Start = "start",
|
||||
}
|
||||
|
||||
export type TraversalType =
|
||||
| SelectorType.Adjacent
|
||||
| SelectorType.Child
|
||||
| SelectorType.Descendant
|
||||
| SelectorType.Parent
|
||||
| SelectorType.Sibling
|
||||
| SelectorType.ColumnCombinator;
|
||||
+185
@@ -0,0 +1,185 @@
|
||||
/**
|
||||
* @fileoverview CSS Selector parsing tests from WPT
|
||||
* @see https://github.com/web-platform-tests/wpt/tree/0bb883967c888261a8372923fd61eb5ad14305b2/css/selectors/parsing
|
||||
* @license BSD-3-Clause (https://github.com/web-platform-tests/wpt/blob/master/LICENSE.md)
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { parse, stringify } from "./index.js";
|
||||
|
||||
function test_valid_selector(
|
||||
selector: string,
|
||||
serialized: string | string[] = selector,
|
||||
) {
|
||||
const result = stringify(parse(selector));
|
||||
if (Array.isArray(serialized)) {
|
||||
// Should be a part of the array
|
||||
expect(serialized).toContain(result);
|
||||
} else {
|
||||
expect(result).toStrictEqual(serialized);
|
||||
}
|
||||
}
|
||||
|
||||
function test_invalid_selector(selector: string) {
|
||||
expect(() => parse(selector)).toThrow(Error);
|
||||
}
|
||||
|
||||
describe("Web Platform Tests", () => {
|
||||
it("Attribute selectors", () => {
|
||||
// Attribute presence and value selectors
|
||||
test_valid_selector("[att]");
|
||||
test_valid_selector("[att=val]", '[att="val"]');
|
||||
test_valid_selector("[att~=val]", '[att~="val"]');
|
||||
test_valid_selector("[att|=val]", '[att|="val"]');
|
||||
test_valid_selector("h1[title]");
|
||||
test_valid_selector("span[class='example']", 'span[class="example"]');
|
||||
test_valid_selector("a[hreflang=fr]", 'a[hreflang="fr"]');
|
||||
test_valid_selector("a[hreflang|='en']", 'a[hreflang|="en"]');
|
||||
|
||||
// Substring matching attribute selectors
|
||||
test_valid_selector("[att^=val]", '[att^="val"]');
|
||||
test_valid_selector("[att$=val]", '[att$="val"]');
|
||||
test_valid_selector("[att*=val]", '[att*="val"]');
|
||||
test_valid_selector('object[type^="image/"]');
|
||||
test_valid_selector('a[href$=".html"]');
|
||||
test_valid_selector('p[title*="hello"]');
|
||||
|
||||
// From Attribute selectors and namespaces examples in spec:
|
||||
test_valid_selector("[*|att]");
|
||||
test_valid_selector("[|att]", "[att]");
|
||||
});
|
||||
|
||||
it("Child combinators", () => {
|
||||
test_valid_selector("body > p");
|
||||
test_valid_selector("div ol>li p", "div ol > li p");
|
||||
});
|
||||
|
||||
it("Class selectors", () => {
|
||||
test_valid_selector("*.pastoral", ["*.pastoral", ".pastoral"]);
|
||||
test_valid_selector(".pastoral", ["*.pastoral", ".pastoral"]);
|
||||
test_valid_selector("h1.pastoral");
|
||||
test_valid_selector("p.pastoral.marine");
|
||||
});
|
||||
|
||||
it("Descendant combinator", () => {
|
||||
test_valid_selector("h1 em");
|
||||
test_valid_selector("div * p");
|
||||
test_valid_selector("div p *[href]", ["div p *[href]", "div p [href]"]);
|
||||
});
|
||||
|
||||
it(":focus-visible pseudo-class", () => {
|
||||
test_valid_selector(":focus-visible");
|
||||
test_valid_selector("a:focus-visible");
|
||||
test_valid_selector(":focus:not(:focus-visible)");
|
||||
});
|
||||
|
||||
it("The relational pseudo-class", () => {
|
||||
test_valid_selector(":has(a)");
|
||||
test_valid_selector(":has(#a)");
|
||||
test_valid_selector(":has(.a)");
|
||||
test_valid_selector(":has([a])");
|
||||
test_valid_selector(':has([a="b"])');
|
||||
test_valid_selector(':has([a|="b"])');
|
||||
test_valid_selector(":has(:hover)");
|
||||
test_valid_selector("*:has(.a)", ["*:has(.a)", ":has(.a)"]);
|
||||
test_valid_selector(".a:has(.b)");
|
||||
test_valid_selector(".a:has(> .b)");
|
||||
test_valid_selector(".a:has(~ .b)");
|
||||
test_valid_selector(".a:has(+ .b)");
|
||||
test_valid_selector(".a:has(.b) .c");
|
||||
test_valid_selector(".a .b:has(.c)");
|
||||
test_valid_selector(".a .b:has(.c .d)");
|
||||
test_valid_selector(".a .b:has(.c .d) .e");
|
||||
test_valid_selector(".a:has(.b:has(.c))");
|
||||
test_valid_selector(".a:has(.b:is(.c .d))");
|
||||
test_valid_selector(".a:has(.b:is(.c:has(.d) .e))");
|
||||
test_valid_selector(".a:is(.b:has(.c) .d)");
|
||||
test_valid_selector(".a:not(:has(.b))");
|
||||
test_valid_selector(".a:has(:not(.b))");
|
||||
test_valid_selector(".a:has(.b):has(.c)");
|
||||
test_valid_selector("*|*:has(*)", ":has(*)");
|
||||
test_valid_selector(":has(*|*)");
|
||||
test_invalid_selector(".a:has()");
|
||||
});
|
||||
|
||||
it("ID selectors", () => {
|
||||
test_valid_selector("h1#chapter1");
|
||||
test_valid_selector("#chapter1");
|
||||
test_valid_selector("*#z98y", ["*#z98y", "#z98y"]);
|
||||
});
|
||||
|
||||
it("The Matches-Any Pseudo-class: ':is()'", () => {
|
||||
test_valid_selector(
|
||||
":is(ul,ol,.list) > [hidden]",
|
||||
":is(ul, ol, .list) > [hidden]",
|
||||
);
|
||||
test_valid_selector(":is(:hover,:focus)", ":is(:hover, :focus)");
|
||||
test_valid_selector("a:is(:not(:hover))");
|
||||
|
||||
test_valid_selector(":is(#a)");
|
||||
test_valid_selector(".a.b ~ :is(.c.d ~ .e.f)");
|
||||
test_valid_selector(".a.b ~ .c.d:is(span.e + .f, .g.h > .i.j .k)");
|
||||
});
|
||||
|
||||
it("The negation pseudo-class", () => {
|
||||
test_valid_selector("button:not([disabled])");
|
||||
test_valid_selector("*:not(foo)", ["*:not(foo)", ":not(foo)"]);
|
||||
test_valid_selector(":not(:link):not(:visited)");
|
||||
test_valid_selector("*|*:not(*)", ":not(*)");
|
||||
test_valid_selector(":not(:hover)");
|
||||
test_valid_selector(":not(*|*)");
|
||||
test_valid_selector("foo:not(bar)");
|
||||
test_valid_selector(":not(:not(foo))");
|
||||
test_valid_selector(":not(.a .b)");
|
||||
test_valid_selector(":not(.a + .b)");
|
||||
test_valid_selector(":not(.a .b ~ c)");
|
||||
test_valid_selector(":not(span.a, div.b)");
|
||||
test_valid_selector(":not(.a .b ~ c, .d .e)");
|
||||
test_valid_selector(":not(:host)");
|
||||
test_valid_selector(":not(:host(.a))");
|
||||
test_valid_selector(":host(:not(.a))");
|
||||
test_valid_selector(":not(:host(:not(.a)))");
|
||||
test_valid_selector(
|
||||
":not([disabled][selected])",
|
||||
":not([disabled][selected])",
|
||||
);
|
||||
test_valid_selector(
|
||||
":not([disabled],[selected])",
|
||||
":not([disabled], [selected])",
|
||||
);
|
||||
|
||||
test_invalid_selector(":not()");
|
||||
test_invalid_selector(":not(:not())");
|
||||
});
|
||||
|
||||
it("Sibling combinators", () => {
|
||||
test_valid_selector("math + p");
|
||||
test_valid_selector("h1.opener + h2");
|
||||
test_valid_selector("h1 ~ pre");
|
||||
});
|
||||
|
||||
it("Universal selector", () => {
|
||||
test_valid_selector("*");
|
||||
test_valid_selector("div :first-child", [
|
||||
"div *:first-child",
|
||||
"div :first-child",
|
||||
]);
|
||||
test_valid_selector("div *:first-child", [
|
||||
"div *:first-child",
|
||||
"div :first-child",
|
||||
]);
|
||||
});
|
||||
|
||||
it("The Specificity-adjustment Pseudo-class: ':where()'", () => {
|
||||
test_valid_selector(
|
||||
":where(ul,ol,.list) > [hidden]",
|
||||
":where(ul, ol, .list) > [hidden]",
|
||||
);
|
||||
test_valid_selector(":where(:hover,:focus)", ":where(:hover, :focus)");
|
||||
test_valid_selector("a:where(:not(:hover))");
|
||||
|
||||
test_valid_selector(":where(#a)");
|
||||
test_valid_selector(".a.b ~ :where(.c.d ~ .e.f)");
|
||||
test_valid_selector(".a.b ~ .c.d:where(span.e + .f, .g.h > .i.j .k)");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user