update css-what from 2.1.3 to v6.1.0

Signed-off-by: lixingchi1 <lixingchi1@huawei.com>
This commit is contained in:
lixingchi1
2022-09-07 15:35:20 +08:00
parent 3640fff512
commit 6645b076bf
18 changed files with 28752 additions and 380 deletions
+72
View File
@@ -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
}
}
]
}
+3
View File
@@ -0,0 +1,3 @@
node_modules/
coverage/
lib/
-274
View File
@@ -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);
}
+9068
View File
File diff suppressed because it is too large Load Diff
+55 -73
View File
@@ -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"
}
+51 -33
View File
@@ -1,51 +1,69 @@
# css-what [![Build Status](https://secure.travis-ci.org/fb55/css-what.svg?branch=master)](http://travis-ci.org/fb55/css-what)
# css-what
a CSS selector parser
[![Build Status](https://img.shields.io/github/workflow/status/fb55/css-what/Node.js%20CI/master)](https://github.com/fb55/css-what/actions/workflows/nodejs-test.yml)
[![Coverage](https://img.shields.io/coveralls/github/fb55/css-what/master)](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
+3
View File
@@ -0,0 +1,3 @@
export * from "./types";
export { isTraversal, parse } from "./parse";
export { stringify } from "./stringify";
+64
View File
@@ -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
View File
@@ -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;
}
+22
View File
@@ -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]);
}
});
});
+179
View File
@@ -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;
}
+94
View File
@@ -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
View File
@@ -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)");
});
});
+9
View File
@@ -0,0 +1,9 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"target": "ES2019",
"module": "es2015",
"outDir": "lib/es",
"moduleResolution": "node"
}
}
+5
View File
@@ -0,0 +1,5 @@
{
"extends": "./tsconfig.json",
"include": ["src"],
"exclude": []
}
+34
View File
@@ -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__/*"
]
}