mirror of
https://github.com/openharmony/third_party_css-what.git
synced 2026-07-01 03:23:11 -04:00
update css-what from 2.1.3 to v6.1.0
Signed-off-by: lixingchi1 <lixingchi1@huawei.com>
This commit is contained in:
@@ -0,0 +1,72 @@
|
||||
{
|
||||
"extends": ["eslint:recommended", "plugin:node/recommended", "prettier"],
|
||||
"env": {
|
||||
"node": true,
|
||||
"es6": true
|
||||
},
|
||||
"rules": {
|
||||
"eqeqeq": [2, "smart"],
|
||||
"no-caller": 2,
|
||||
"dot-notation": 2,
|
||||
"no-var": 2,
|
||||
"prefer-const": 2,
|
||||
"prefer-arrow-callback": [2, { "allowNamedFunctions": true }],
|
||||
"arrow-body-style": [2, "as-needed"],
|
||||
"object-shorthand": 2,
|
||||
"prefer-template": 2,
|
||||
"one-var": [2, "never"],
|
||||
"prefer-destructuring": [2, { "object": true }],
|
||||
"capitalized-comments": 2,
|
||||
"multiline-comment-style": [2, "starred-block"],
|
||||
"spaced-comment": 2,
|
||||
"yoda": [2, "never"],
|
||||
"curly": [2, "multi-line"],
|
||||
"no-else-return": 2,
|
||||
|
||||
"node/no-unsupported-features/es-syntax": [
|
||||
2,
|
||||
{ "ignores": ["modules"] }
|
||||
]
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"files": "*.ts",
|
||||
"extends": [
|
||||
"plugin:@typescript-eslint/eslint-recommended",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"prettier"
|
||||
],
|
||||
"parserOptions": {
|
||||
"sourceType": "module",
|
||||
"project": "./tsconfig.eslint.json"
|
||||
},
|
||||
"settings": {
|
||||
"node": {
|
||||
"tryExtensions": [".js", ".json", ".node", ".ts"]
|
||||
}
|
||||
},
|
||||
"rules": {
|
||||
"@typescript-eslint/prefer-for-of": 0,
|
||||
"@typescript-eslint/member-ordering": 0,
|
||||
"@typescript-eslint/explicit-function-return-type": 0,
|
||||
"@typescript-eslint/no-unused-vars": 0,
|
||||
"@typescript-eslint/no-use-before-define": [
|
||||
2,
|
||||
{ "functions": false }
|
||||
],
|
||||
"@typescript-eslint/consistent-type-definitions": [
|
||||
2,
|
||||
"interface"
|
||||
],
|
||||
"@typescript-eslint/prefer-function-type": 2,
|
||||
"@typescript-eslint/no-unnecessary-type-arguments": 2,
|
||||
"@typescript-eslint/prefer-string-starts-ends-with": 2,
|
||||
"@typescript-eslint/prefer-readonly": 2,
|
||||
"@typescript-eslint/prefer-includes": 2,
|
||||
"@typescript-eslint/no-unnecessary-condition": 2,
|
||||
"@typescript-eslint/switch-exhaustiveness-check": 2,
|
||||
"@typescript-eslint/prefer-nullish-coalescing": 2
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
node_modules/
|
||||
coverage/
|
||||
lib/
|
||||
@@ -1,274 +0,0 @@
|
||||
"use strict";
|
||||
|
||||
module.exports = parse;
|
||||
|
||||
var re_name = /^(?:\\.|[\w\-\u00b0-\uFFFF])+/,
|
||||
re_escape = /\\([\da-f]{1,6}\s?|(\s)|.)/ig,
|
||||
//modified version of https://github.com/jquery/sizzle/blob/master/src/sizzle.js#L87
|
||||
re_attr = /^\s*((?:\\.|[\w\u00b0-\uFFFF\-])+)\s*(?:(\S?)=\s*(?:(['"])([^]*?)\3|(#?(?:\\.|[\w\u00b0-\uFFFF\-])*)|)|)\s*(i)?\]/;
|
||||
|
||||
var actionTypes = {
|
||||
__proto__: null,
|
||||
"undefined": "exists",
|
||||
"": "equals",
|
||||
"~": "element",
|
||||
"^": "start",
|
||||
"$": "end",
|
||||
"*": "any",
|
||||
"!": "not",
|
||||
"|": "hyphen"
|
||||
};
|
||||
|
||||
var simpleSelectors = {
|
||||
__proto__: null,
|
||||
">": "child",
|
||||
"<": "parent",
|
||||
"~": "sibling",
|
||||
"+": "adjacent"
|
||||
};
|
||||
|
||||
var attribSelectors = {
|
||||
__proto__: null,
|
||||
"#": ["id", "equals"],
|
||||
".": ["class", "element"]
|
||||
};
|
||||
|
||||
//pseudos, whose data-property is parsed as well
|
||||
var unpackPseudos = {
|
||||
__proto__: null,
|
||||
"has": true,
|
||||
"not": true,
|
||||
"matches": true
|
||||
};
|
||||
|
||||
var stripQuotesFromPseudos = {
|
||||
__proto__: null,
|
||||
"contains": true,
|
||||
"icontains": true
|
||||
};
|
||||
|
||||
var quotes = {
|
||||
__proto__: null,
|
||||
"\"": true,
|
||||
"'": true
|
||||
};
|
||||
|
||||
//unescape function taken from https://github.com/jquery/sizzle/blob/master/src/sizzle.js#L139
|
||||
function funescape( _, escaped, escapedWhitespace ) {
|
||||
var high = "0x" + escaped - 0x10000;
|
||||
// NaN means non-codepoint
|
||||
// Support: Firefox
|
||||
// Workaround erroneous numeric interpretation of +"0x"
|
||||
return high !== high || escapedWhitespace ?
|
||||
escaped :
|
||||
// BMP codepoint
|
||||
high < 0 ?
|
||||
String.fromCharCode( high + 0x10000 ) :
|
||||
// Supplemental Plane codepoint (surrogate pair)
|
||||
String.fromCharCode( high >> 10 | 0xD800, high & 0x3FF | 0xDC00 );
|
||||
}
|
||||
|
||||
function unescapeCSS(str){
|
||||
return str.replace(re_escape, funescape);
|
||||
}
|
||||
|
||||
function isWhitespace(c){
|
||||
return c === " " || c === "\n" || c === "\t" || c === "\f" || c === "\r";
|
||||
}
|
||||
|
||||
function parse(selector, options){
|
||||
var subselects = [];
|
||||
|
||||
selector = parseSelector(subselects, selector + "", options);
|
||||
|
||||
if(selector !== ""){
|
||||
throw new SyntaxError("Unmatched selector: " + selector);
|
||||
}
|
||||
|
||||
return subselects;
|
||||
}
|
||||
|
||||
function parseSelector(subselects, selector, options){
|
||||
var tokens = [],
|
||||
sawWS = false,
|
||||
data, firstChar, name, quot;
|
||||
|
||||
function getName(){
|
||||
var sub = selector.match(re_name)[0];
|
||||
selector = selector.substr(sub.length);
|
||||
return unescapeCSS(sub);
|
||||
}
|
||||
|
||||
function stripWhitespace(start){
|
||||
while(isWhitespace(selector.charAt(start))) start++;
|
||||
selector = selector.substr(start);
|
||||
}
|
||||
|
||||
function isEscaped(pos) {
|
||||
var slashCount = 0;
|
||||
|
||||
while (selector.charAt(--pos) === "\\") slashCount++;
|
||||
return (slashCount & 1) === 1;
|
||||
}
|
||||
|
||||
stripWhitespace(0);
|
||||
|
||||
while(selector !== ""){
|
||||
firstChar = selector.charAt(0);
|
||||
|
||||
if(isWhitespace(firstChar)){
|
||||
sawWS = true;
|
||||
stripWhitespace(1);
|
||||
} else if(firstChar in simpleSelectors){
|
||||
tokens.push({type: simpleSelectors[firstChar]});
|
||||
sawWS = false;
|
||||
|
||||
stripWhitespace(1);
|
||||
} else if(firstChar === ","){
|
||||
if(tokens.length === 0){
|
||||
throw new SyntaxError("empty sub-selector");
|
||||
}
|
||||
subselects.push(tokens);
|
||||
tokens = [];
|
||||
sawWS = false;
|
||||
stripWhitespace(1);
|
||||
} else {
|
||||
if(sawWS){
|
||||
if(tokens.length > 0){
|
||||
tokens.push({type: "descendant"});
|
||||
}
|
||||
sawWS = false;
|
||||
}
|
||||
|
||||
if(firstChar === "*"){
|
||||
selector = selector.substr(1);
|
||||
tokens.push({type: "universal"});
|
||||
} else if(firstChar in attribSelectors){
|
||||
selector = selector.substr(1);
|
||||
tokens.push({
|
||||
type: "attribute",
|
||||
name: attribSelectors[firstChar][0],
|
||||
action: attribSelectors[firstChar][1],
|
||||
value: getName(),
|
||||
ignoreCase: false
|
||||
});
|
||||
} else if(firstChar === "["){
|
||||
selector = selector.substr(1);
|
||||
data = selector.match(re_attr);
|
||||
if(!data){
|
||||
throw new SyntaxError("Malformed attribute selector: " + selector);
|
||||
}
|
||||
selector = selector.substr(data[0].length);
|
||||
name = unescapeCSS(data[1]);
|
||||
|
||||
if(
|
||||
!options || (
|
||||
"lowerCaseAttributeNames" in options ?
|
||||
options.lowerCaseAttributeNames :
|
||||
!options.xmlMode
|
||||
)
|
||||
){
|
||||
name = name.toLowerCase();
|
||||
}
|
||||
|
||||
tokens.push({
|
||||
type: "attribute",
|
||||
name: name,
|
||||
action: actionTypes[data[2]],
|
||||
value: unescapeCSS(data[4] || data[5] || ""),
|
||||
ignoreCase: !!data[6]
|
||||
});
|
||||
|
||||
} else if(firstChar === ":"){
|
||||
if(selector.charAt(1) === ":"){
|
||||
selector = selector.substr(2);
|
||||
tokens.push({type: "pseudo-element", name: getName().toLowerCase()});
|
||||
continue;
|
||||
}
|
||||
|
||||
selector = selector.substr(1);
|
||||
|
||||
name = getName().toLowerCase();
|
||||
data = null;
|
||||
|
||||
if(selector.charAt(0) === "("){
|
||||
if(name in unpackPseudos){
|
||||
quot = selector.charAt(1);
|
||||
var quoted = quot in quotes;
|
||||
|
||||
selector = selector.substr(quoted + 1);
|
||||
|
||||
data = [];
|
||||
selector = parseSelector(data, selector, options);
|
||||
|
||||
if(quoted){
|
||||
if(selector.charAt(0) !== quot){
|
||||
throw new SyntaxError("unmatched quotes in :" + name);
|
||||
} else {
|
||||
selector = selector.substr(1);
|
||||
}
|
||||
}
|
||||
|
||||
if(selector.charAt(0) !== ")"){
|
||||
throw new SyntaxError("missing closing parenthesis in :" + name + " " + selector);
|
||||
}
|
||||
|
||||
selector = selector.substr(1);
|
||||
} else {
|
||||
var pos = 1, counter = 1;
|
||||
|
||||
for(; counter > 0 && pos < selector.length; pos++){
|
||||
if(selector.charAt(pos) === "(" && !isEscaped(pos)) counter++;
|
||||
else if(selector.charAt(pos) === ")" && !isEscaped(pos)) counter--;
|
||||
}
|
||||
|
||||
if(counter){
|
||||
throw new SyntaxError("parenthesis not matched");
|
||||
}
|
||||
|
||||
data = selector.substr(1, pos - 2);
|
||||
selector = selector.substr(pos);
|
||||
|
||||
if(name in stripQuotesFromPseudos){
|
||||
quot = data.charAt(0);
|
||||
|
||||
if(quot === data.slice(-1) && quot in quotes){
|
||||
data = data.slice(1, -1);
|
||||
}
|
||||
|
||||
data = unescapeCSS(data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tokens.push({type: "pseudo", name: name, data: data});
|
||||
} else if(re_name.test(selector)){
|
||||
name = getName();
|
||||
|
||||
if(!options || ("lowerCaseTags" in options ? options.lowerCaseTags : !options.xmlMode)){
|
||||
name = name.toLowerCase();
|
||||
}
|
||||
|
||||
tokens.push({type: "tag", name: name});
|
||||
} else {
|
||||
if(tokens.length && tokens[tokens.length - 1].type === "descendant"){
|
||||
tokens.pop();
|
||||
}
|
||||
addToken(subselects, tokens);
|
||||
return selector;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
addToken(subselects, tokens);
|
||||
|
||||
return selector;
|
||||
}
|
||||
|
||||
function addToken(subselects, tokens){
|
||||
if(subselects.length > 0 && tokens.length === 0){
|
||||
throw new SyntaxError("empty sub-selector");
|
||||
}
|
||||
|
||||
subselects.push(tokens);
|
||||
}
|
||||
Generated
+9068
File diff suppressed because it is too large
Load Diff
+55
-73
@@ -1,77 +1,59 @@
|
||||
{
|
||||
"_from": "css-what@^2.1.3",
|
||||
"_id": "css-what@2.1.3",
|
||||
"_inBundle": false,
|
||||
"_integrity": "sha512-a+EPoD+uZiNfh+5fxw2nO9QwFa6nJe2Or35fGY6Ipw1R3R4AGz1d1TEZrCegvw2YTmZ0jXirGYlzxxpYSHwpEg==",
|
||||
"_location": "/css-what",
|
||||
"_phantomChildren": {},
|
||||
"_requested": {
|
||||
"type": "range",
|
||||
"registry": true,
|
||||
"raw": "css-what@^2.1.3",
|
||||
"author": "Felix Böhm <me@feedic.com> (http://feedic.com)",
|
||||
"name": "css-what",
|
||||
"escapedName": "css-what",
|
||||
"rawSpec": "^2.1.3",
|
||||
"saveSpec": null,
|
||||
"fetchSpec": "^2.1.3"
|
||||
},
|
||||
"_requiredBy": [
|
||||
"/"
|
||||
],
|
||||
"_resolved": "https://registry.npmjs.org/css-what/-/css-what-2.1.3.tgz",
|
||||
"_shasum": "a6d7604573365fe74686c3f311c56513d88285f2",
|
||||
"_spec": "css-what@^2.1.3",
|
||||
"_where": "/home/c00321158/hmf-ace-ohos/third_party/jsframework",
|
||||
"author": {
|
||||
"name": "Felix Böhm",
|
||||
"email": "me@feedic.com",
|
||||
"url": "http://feedic.com"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/fb55/css-what/issues"
|
||||
},
|
||||
"bundleDependencies": false,
|
||||
"dependencies": {},
|
||||
"deprecated": false,
|
||||
"description": "a CSS selector parser",
|
||||
"devDependencies": {
|
||||
"jshint": "2"
|
||||
},
|
||||
"engines": {
|
||||
"node": "*"
|
||||
},
|
||||
"files": [
|
||||
"index.js"
|
||||
],
|
||||
"homepage": "https://github.com/fb55/css-what#readme",
|
||||
"jshintConfig": {
|
||||
"eqeqeq": true,
|
||||
"freeze": true,
|
||||
"latedef": "nofunc",
|
||||
"noarg": true,
|
||||
"nonbsp": true,
|
||||
"undef": true,
|
||||
"unused": true,
|
||||
"eqnull": true,
|
||||
"proto": true,
|
||||
"node": true,
|
||||
"globals": {
|
||||
"describe": true,
|
||||
"it": true
|
||||
"description": "a CSS selector parser",
|
||||
"version": "6.1.0",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/fb55"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/fb55/css-what"
|
||||
},
|
||||
"main": "lib/commonjs/index.js",
|
||||
"module": "lib/es/index.js",
|
||||
"types": "lib/es/index.d.ts",
|
||||
"sideEffects": false,
|
||||
"files": [
|
||||
"lib/**/*"
|
||||
],
|
||||
"scripts": {
|
||||
"test": "npm run test:jest && npm run lint",
|
||||
"test:jest": "jest",
|
||||
"lint": "npm run lint:es && npm run lint:prettier",
|
||||
"lint:es": "eslint src",
|
||||
"lint:prettier": "npm run prettier -- --check",
|
||||
"format": "npm run format:es && npm run format:prettier",
|
||||
"format:es": "npm run lint:es -- --fix",
|
||||
"format:prettier": "npm run prettier -- --write",
|
||||
"prettier": "prettier '**/*.{ts,md,json,yml}'",
|
||||
"build": "tsc && tsc -p tsconfig.es.json",
|
||||
"prepare": "npm run build"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jest": "^27.4.1",
|
||||
"@types/node": "^17.0.23",
|
||||
"@typescript-eslint/eslint-plugin": "^5.17.0",
|
||||
"@typescript-eslint/parser": "^5.17.0",
|
||||
"eslint": "^8.12.0",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"eslint-plugin-node": "^11.1.0",
|
||||
"jest": "^27.5.1",
|
||||
"prettier": "^2.6.1",
|
||||
"ts-jest": "^27.1.4",
|
||||
"typescript": "^4.6.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
},
|
||||
"license": "BSD-2-Clause",
|
||||
"jest": {
|
||||
"preset": "ts-jest",
|
||||
"roots": [
|
||||
"src"
|
||||
]
|
||||
},
|
||||
"prettier": {
|
||||
"tabWidth": 4
|
||||
}
|
||||
},
|
||||
"license": "BSD-2-Clause",
|
||||
"main": "./index.js",
|
||||
"name": "css-what",
|
||||
"optionalDependencies": {},
|
||||
"prettier": {
|
||||
"tabWidth": 4
|
||||
},
|
||||
"repository": {
|
||||
"url": "git+https://github.com/fb55/css-what.git"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "node tests/test.js && jshint *.js"
|
||||
},
|
||||
"version": "2.1.3"
|
||||
}
|
||||
|
||||
@@ -1,51 +1,69 @@
|
||||
# css-what [](http://travis-ci.org/fb55/css-what)
|
||||
# css-what
|
||||
|
||||
a CSS selector parser
|
||||
[](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
|
||||
require('css-what')('foo[bar]:baz')
|
||||
import * as CSSwhat from "css-what";
|
||||
|
||||
~> [ [ { type: 'tag', name: 'foo' },
|
||||
{ type: 'attribute',
|
||||
name: 'bar',
|
||||
action: 'exists',
|
||||
value: '',
|
||||
ignoreCase: false },
|
||||
{ type: 'pseudo',
|
||||
name: 'baz',
|
||||
data: null } ] ]
|
||||
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(selector, options)` - Parses `str`, with the passed `options`.__
|
||||
**`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 | attributes | 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' }`
|
||||
| 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' }` |
|
||||
|
||||
|
||||
__Options:__
|
||||
|
||||
- `xmlMode`: When enabled, tag names will be case-sensitive (meaning they won't be lowercased).
|
||||
**`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)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,3 @@
|
||||
export * from "./types";
|
||||
export { isTraversal, parse } from "./parse";
|
||||
export { stringify } from "./stringify";
|
||||
@@ -0,0 +1,64 @@
|
||||
import { readFileSync } from "fs";
|
||||
import { parse } from ".";
|
||||
import { tests } from "./__fixtures__/tests";
|
||||
|
||||
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) {
|
||||
test(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)) {
|
||||
test(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");
|
||||
});
|
||||
});
|
||||
+603
@@ -0,0 +1,603 @@
|
||||
import {
|
||||
Selector,
|
||||
SelectorType,
|
||||
AttributeSelector,
|
||||
Traversal,
|
||||
AttributeAction,
|
||||
TraversalType,
|
||||
DataType,
|
||||
} from "./types";
|
||||
|
||||
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,
|
||||
Star = 42,
|
||||
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",
|
||||
]);
|
||||
|
||||
/**
|
||||
* 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 = parseInt(escaped, 16) - 0x10000;
|
||||
|
||||
// NaN means non-codepoint
|
||||
return high !== high || escapedWhitespace
|
||||
? escaped
|
||||
: high < 0
|
||||
? // BMP codepoint
|
||||
String.fromCharCode(high + 0x10000)
|
||||
: // Supplemental Plane codepoint (surrogate pair)
|
||||
String.fromCharCode((high >> 10) | 0xd800, (high & 0x3ff) | 0xdc00);
|
||||
}
|
||||
|
||||
function unescapeCSS(str: string) {
|
||||
return str.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`, optionally with the passed `options`.
|
||||
*
|
||||
* @param selector Selector to parse.
|
||||
* @param options Options for parsing.
|
||||
* @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;
|
||||
let counter = 1;
|
||||
|
||||
for (
|
||||
;
|
||||
counter > 0 && selectorIndex < selector.length;
|
||||
selectorIndex++
|
||||
) {
|
||||
if (
|
||||
selector.charCodeAt(selectorIndex) ===
|
||||
CharCode.LeftParenthesis &&
|
||||
!isEscaped(selectorIndex)
|
||||
) {
|
||||
counter++;
|
||||
} else if (
|
||||
selector.charCodeAt(selectorIndex) ===
|
||||
CharCode.RightParenthesis &&
|
||||
!isEscaped(selectorIndex)
|
||||
) {
|
||||
counter--;
|
||||
}
|
||||
}
|
||||
|
||||
if (counter) {
|
||||
throw new Error("Parenthesis not matched");
|
||||
}
|
||||
|
||||
return unescapeCSS(selector.slice(start, selectorIndex - 1));
|
||||
}
|
||||
|
||||
function isEscaped(pos: number): boolean {
|
||||
let slashCount = 0;
|
||||
|
||||
while (selector.charCodeAt(--pos) === CharCode.BackSlash) slashCount++;
|
||||
return (slashCount & 1) === 1;
|
||||
}
|
||||
|
||||
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 &&
|
||||
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);
|
||||
let sectionEnd = selectorIndex + 1;
|
||||
while (
|
||||
sectionEnd < selector.length &&
|
||||
(selector.charCodeAt(sectionEnd) !== quote ||
|
||||
isEscaped(sectionEnd))
|
||||
) {
|
||||
sectionEnd += 1;
|
||||
}
|
||||
|
||||
if (selector.charCodeAt(sectionEnd) !== quote) {
|
||||
throw new Error("Attribute value didn't end");
|
||||
}
|
||||
|
||||
value = unescapeCSS(
|
||||
selector.slice(selectorIndex + 1, sectionEnd)
|
||||
);
|
||||
selectorIndex = sectionEnd + 1;
|
||||
} else {
|
||||
const valueStart = selectorIndex;
|
||||
|
||||
while (
|
||||
selectorIndex < selector.length &&
|
||||
((!isWhitespace(
|
||||
selector.charCodeAt(selectorIndex)
|
||||
) &&
|
||||
selector.charCodeAt(selectorIndex) !==
|
||||
CharCode.RightSquareBracket) ||
|
||||
isEscaped(selectorIndex))
|
||||
) {
|
||||
selectorIndex += 1;
|
||||
}
|
||||
|
||||
value = unescapeCSS(
|
||||
selector.slice(valueStart, selectorIndex)
|
||||
);
|
||||
}
|
||||
|
||||
stripWhitespace(0);
|
||||
|
||||
// See if we have a force ignore flag
|
||||
|
||||
const forceIgnore =
|
||||
selector.charCodeAt(selectorIndex) | 0x20;
|
||||
|
||||
// If the forceIgnore flag is set (either `i` or `s`), use that value
|
||||
if (forceIgnore === CharCode.LowerS) {
|
||||
ignoreCase = false;
|
||||
stripWhitespace(1);
|
||||
} else if (forceIgnore === CharCode.LowerI) {
|
||||
ignoreCase = true;
|
||||
stripWhitespace(1);
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
const name = getName(1).toLowerCase();
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { readFileSync } from "fs";
|
||||
import { parse, stringify } from ".";
|
||||
import { tests } from "./__fixtures__/tests";
|
||||
|
||||
describe("Stringify & re-parse", () => {
|
||||
describe("Own tests", () => {
|
||||
for (const [selector, expected, message] of tests) {
|
||||
test(`${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]);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,179 @@
|
||||
import { Selector, SelectorType, AttributeAction } from "./types";
|
||||
|
||||
const attribValChars = ["\\", '"'];
|
||||
const pseudoValChars = [...attribValChars, "(", ")"];
|
||||
|
||||
const charsToEscapeInAttributeValue = new Set(
|
||||
attribValChars.map((c) => c.charCodeAt(0))
|
||||
);
|
||||
const charsToEscapeInPseudoValue = new Set(
|
||||
pseudoValChars.map((c) => c.charCodeAt(0))
|
||||
);
|
||||
const charsToEscapeInName = new Set(
|
||||
[
|
||||
...pseudoValChars,
|
||||
"~",
|
||||
"^",
|
||||
"$",
|
||||
"*",
|
||||
"+",
|
||||
"!",
|
||||
"|",
|
||||
":",
|
||||
"[",
|
||||
"]",
|
||||
" ",
|
||||
".",
|
||||
].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(stringifyToken).join(""))
|
||||
.join(", ");
|
||||
}
|
||||
|
||||
function stringifyToken(
|
||||
token: Selector,
|
||||
index: number,
|
||||
arr: 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 < arr.length &&
|
||||
"name" in arr[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 "|";
|
||||
case AttributeAction.Exists:
|
||||
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(str: string, charsToEscape: Set<number>): string {
|
||||
let lastIdx = 0;
|
||||
let ret = "";
|
||||
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
if (charsToEscape.has(str.charCodeAt(i))) {
|
||||
ret += `${str.slice(lastIdx, i)}\\${str.charAt(i)}`;
|
||||
lastIdx = i + 1;
|
||||
}
|
||||
}
|
||||
|
||||
return ret.length > 0 ? ret + str.slice(lastIdx) : str;
|
||||
}
|
||||
@@ -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;
|
||||
+184
@@ -0,0 +1,184 @@
|
||||
/**
|
||||
* @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 { parse, stringify } from ".";
|
||||
|
||||
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)");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"target": "ES2019",
|
||||
"module": "es2015",
|
||||
"outDir": "lib/es",
|
||||
"moduleResolution": "node"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"include": ["src"],
|
||||
"exclude": []
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
/* Basic Options */
|
||||
"target": "es5" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */,
|
||||
"module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */,
|
||||
// "lib": [], /* Specify library files to be included in the compilation. */
|
||||
"declaration": true /* Generates corresponding '.d.ts' file. */,
|
||||
"declarationMap": true /* Generates a sourcemap for each corresponding '.d.ts' file. */,
|
||||
// "sourceMap": true, /* Generates corresponding '.map' file. */
|
||||
"outDir": "lib/commonjs" /* Redirect output structure to the directory. */,
|
||||
// "importHelpers": true, /* Import emit helpers from 'tslib'. */
|
||||
|
||||
/* Strict Type-Checking Options */
|
||||
"strict": true /* Enable all strict type-checking options. */,
|
||||
|
||||
/* Additional Checks */
|
||||
"noUnusedLocals": true /* Report errors on unused locals. */,
|
||||
"noUnusedParameters": true /* Report errors on unused parameters. */,
|
||||
"noImplicitReturns": true /* Report error when not all code paths in function return a value. */,
|
||||
"noFallthroughCasesInSwitch": true /* Report errors for fallthrough cases in switch statement. */,
|
||||
|
||||
/* Module Resolution Options */
|
||||
// "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
|
||||
"esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */,
|
||||
"resolveJsonModule": true
|
||||
},
|
||||
"include": ["src"],
|
||||
"exclude": [
|
||||
"**/*.spec.ts",
|
||||
"**/__fixtures__/*",
|
||||
"**/__tests__/*",
|
||||
"**/__snapshots__/*"
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user