Initial commit

This commit is contained in:
mats committed 2026-09-22 08:41:20 +02:00
commit 89053153cc
8152 files changed
+2086461

No files matched your search

+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2023 asamuzaK (Kazz)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+259
View File
@@ -0,0 +1,259 @@
# DOM Selector
[![build](https://github.com/asamuzaK/domSelector/actions/workflows/node.js.yml/badge.svg)](https://github.com/asamuzaK/domSelector/actions/workflows/node.js.yml)
[![CodeQL](https://github.com/asamuzaK/domSelector/actions/workflows/github-code-scanning/codeql/badge.svg)](https://github.com/asamuzaK/domSelector/actions/workflows/github-code-scanning/codeql)
[![npm (scoped)](https://img.shields.io/npm/v/@asamuzakjp/dom-selector)](https://www.npmjs.com/package/@asamuzakjp/dom-selector)
A CSS selector engine built for strict specification compliance.
## Features
* **Strict Specification Compliance**: Strictly adheres to modern web standards. It accurately parses, evaluates, and extracts elements across complex combinations of pseudo-classes and HTML attributes. Features comprehensive support for CSS Selectors Level 4 (e.g., `:is()`, `:not()`, `:where()`, `:has()`) and Shadow DOM pseudo-classes (`:host`, `:host-context`).
* **Utility Functions**: Provides utility methods alongside standard querying, such as `check()` for AST evaluation and `extractSubjects()` for extracting subject keys from selectors.
* **jsdom's Default Engine**: Adopted as the CSS selector engine for [jsdom](https://github.com/jsdom/jsdom).
## Install
``` console
npm i @asamuzakjp/dom-selector
```
## Usage
``` javascript
import { DOMSelector } from '@asamuzakjp/dom-selector';
import { JSDOM } from 'jsdom';
const { window } = new JSDOM();
// Destructuring methods (all methods are bound to the instance)
const {
check, closest, extractSubjects, matches, querySelector, querySelectorAll, supports
} = new DOMSelector(window);
```
## API
### `new DOMSelector(window, document?, opt?)`
Creates an instance of the DOMSelector.
* `window` **{Window}** The window object.
* `document` **{Document}?** The document object. Defaults to window.document.
* `opt` **{object}?** Options:
* `opt.cacheSize` **{number}?** Maximum number of items to store in the internal cache. Default is 2048.
### `matches(selector, node, opt?)`
Equivalent to [Element.matches()](https://developer.mozilla.org/docs/Web/API/Element/matches).
* `selector` **{string}** CSS selector.
* `node` **{Element}** Element node.
* `opt` **{object}?** Options:
* `opt.noexcept` **{boolean}?** Do not throw exceptions.
* `opt.warn` **{boolean}?** Console warn (e.g. unsupported pseudo-class).
* **Returns** **{boolean}** `true` if matched, `false` otherwise.
### `closest(selector, node, opt?)`
Equivalent to [Element.closest()](https://developer.mozilla.org/docs/Web/API/Element/closest).
* `selector` **{string}** CSS selector.
* `node` **{Element}** Element node.
* `opt` **{object}?** Options:
* `opt.noexcept` **{boolean}?** Do not throw exceptions.
* `opt.warn` **{boolean}?** Console warn (e.g. unsupported pseudo-class).
* **Returns** **{Element | null}** The matched ancestor node or `null`.
### `querySelector(selector, node, opt?)`
Equivalent to [Document.querySelector()](https://developer.mozilla.org/docs/Web/API/Document/querySelector), [DocumentFragment.querySelector()](https://developer.mozilla.org/docs/Web/API/DocumentFragment/querySelector) and [Element.querySelector()](https://developer.mozilla.org/docs/Web/API/Element/querySelector).
* `selector` **{string}** CSS selector.
* `node` **{Document | DocumentFragment | Element}** Node to find within.
* `opt` **{object}?** Options:
* `opt.noexcept` **{boolean}?** Do not throw exceptions.
* `opt.warn` **{boolean}?** Console warn (e.g. unsupported pseudo-class).
* **Returns** **{Element | null}** The matched node or `null`.
### `querySelectorAll(selector, node, opt?)`
Equivalent to [Document.querySelectorAll()](https://developer.mozilla.org/docs/Web/API/Document/querySelectorAll), [DocumentFragment.querySelectorAll()](https://developer.mozilla.org/docs/Web/API/DocumentFragment/querySelectorAll) and [Element.querySelectorAll()](https://developer.mozilla.org/docs/Web/API/Element/querySelectorAll).
**NOTE**: Returns a standard `Array`, not a `NodeList`.
* `selector` **{string}** CSS selector.
* `node` **{Document | DocumentFragment | Element}** Node to find within.
* `opt` **{object}?** Options:
* `opt.noexcept` **{boolean}?** Do not throw exceptions.
* `opt.warn` **{boolean}?** Console warn (e.g. unsupported pseudo-class).
* **Returns** **{Array}** Array of matched nodes.
### `check(selector, node, opt?)`
Checks if an element matches a CSS selector and returns additional abstract syntax tree (AST) information.
**NOTE**: Any pseudo-elements in the selector are excluded from the matching evaluation.
* `selector` **{string}** CSS selector.
* `node` **{Element}** Element node.
* `opt` **{object}?** Options:
* `opt.noexcept` **{boolean}?** Do not throw exceptions.
* `opt.warn` **{boolean}?** Console warn (e.g. unsupported pseudo-class).
* **Returns** **{object}** An object containing the following properties:
* `match` **{boolean}** `true` if the element matches the selector, `false` otherwise.
* `pseudoElement` **{string | null}** The pseudo-element extracted from the selector, if any.
* `ast` **{object | null}** The parsed AST object.
### `extractSubjects(selector, caseSensitive?)`
Parses a selector and extracts the rightmost subject keys (Id, Class, Tag).
* `selector` **{string}** CSS selector.
* `caseSensitive` **{boolean}?** `true` if the tag key should be case sensitive. Defaults to `false`.
* **Returns** **{Array\<{id: string|null, className: string|null, tag: string|null}\>}** An array of extracted keys.
### `supports(selector)`
Checks if the given CSS selector is supported by this engine.
See the table below for the full list of supported selectors.
* `selector` **{string}** CSS selector.
* **Returns** **{boolean}** `true` if the selector is supported, `false` otherwise.
### `clear(clearAll?)`
Clears the internal caches to free up memory.
* `clearAll` **{boolean}?** Whether to clear all caches. If false, only cached matching results are cleared. Defaults to `false`.
* **Returns** **{void}**
## Supported CSS selectors
| Pattern | Supported | Note |
| :--- | :---: | :--- |
| `*` | ✓ | |
| `E` | ✓ | |
| <code>ns\|E</code> | ✓ | |
| <code>*\|E</code> | ✓ | |
| <code>\|E</code> | ✓ | |
| `E F` | ✓ | |
| `E > F` | ✓ | |
| `E + F` | ✓ | |
| `E ~ F` | ✓ | |
| <code>F \|\| E</code> | Unsupported | |
| `E.warning` | ✓ | |
| `E#myid` | ✓ | |
| `E[foo]` | ✓ | |
| `E[foo="bar"]` | ✓ | |
| `E[foo="bar" i]` | ✓ | |
| `E[foo="bar" s]` | ✓ | |
| `E[foo~="bar"]` | ✓ | |
| `E[foo^="bar"]` | ✓ | |
| `E[foo$="bar"]` | ✓ | |
| `E[foo*="bar"]` | ✓ | |
| <code>E[foo\|="en"]</code> | ✓ | |
| `E:is(s1, s2, …)` | ✓ | |
| `E:not(s1, s2, …)` | ✓ | |
| `E:where(s1, s2, …)` | ✓ | |
| `E:has(rs1, rs2, …)` | ✓ | |
| `E:defined` | Partially supported | Matching with MathML is not yet supported. |
| `E:dir(ltr)` | ✓ | |
| `E:lang(en)` | ✓ | |
| `E:any-link` | ✓ | |
| `E:link` | ✓ | |
| `E:visited` | ✓ | Returns `false` or `null` to prevent fingerprinting. |
| `E:local-link` | ✓ | |
| `E:target` | ✓ | |
| `E:target-within` | | Removed from spec. |
| `E:scope` | ✓ | |
| `E:hover` | ✓ | |
| `E:active` | ✓ | |
| `E:focus` | ✓ | |
| `E:focus-visible` | ✓ | |
| `E:focus-within` | ✓ | |
| `E:current` | Unsupported | |
| `E:current(s)` | Unsupported | |
| `E:past` | Unsupported | |
| `E:future` | Unsupported | |
| `E:open` | Partially supported | Matching with `<select>`, e.g. `select:open`, or `<input type="color">` are not supported. |
| `E:popover-open` | Unsupported | |
| `E:enabled`<br>`E:disabled` | ✓ | |
| `E:read-write`<br>`E:read-only` | ✓ | |
| `E:placeholder-shown` | ✓ | |
| `E:default` | ✓ | |
| `E:checked` | ✓ | |
| `E:indeterminate` | ✓ | |
| `E:blank` | Unsupported | |
| `E:valid`<br>`E:invalid` | ✓ | |
| `E:in-range`<br>`E:out-of-range` | ✓ | |
| `E:required`<br>`E:optional` | ✓ | |
| `E:user-valid`<br>`E:user-invalid` | Unsupported | |
| `E:root` | ✓ | |
| `E:empty` | ✓ | |
| `E:nth-child(n [of S]?)` | ✓ | |
| `E:nth-last-child(n [of S]?)` | ✓ | |
| `E:first-child` | ✓ | |
| `E:last-child` | ✓ | |
| `E:only-child` | ✓ | |
| `E:nth-of-type(n)` | ✓ | |
| `E:nth-last-of-type(n)` | ✓ | |
| `E:first-of-type` | ✓ | |
| `E:last-of-type` | ✓ | |
| `E:only-of-type` | ✓ | |
| `E:nth-col(n)` | Unsupported | |
| `E:nth-last-col(n)` | Unsupported | |
| `CE:state(v)` | ✓ | \*1 |
| `:host` | ✓ | |
| `:host(s)` | ✓ | |
| `:host(:state(v))` | ✓ | \*1 |
| `:host:has(rs1, rs2, ...)` | ✓ | |
| `:host(s):has(rs1, rs2, ...)` | ✓ | |
| `:host-context(s)` | ✓ | |
| `:host-context(s):has(rs1, rs2, ...)` | ✓ | |
| `&` | ✓ | Only supports outermost `&`, i.e. equivalent to `:scope` |
\*1: `ElementInternals.states`, i.e. `CustomStateSet`, is not implemented in jsdom, so you need to apply a patch in the custom element constructor.
``` javascript
class LabeledCheckbox extends window.HTMLElement {
#internals;
constructor() {
super();
this.#internals = this.attachInternals();
// patch CustomStateSet
if (!this.#internals.states) {
this.#internals.states = new Set();
}
this.addEventListener('click', this._onClick.bind(this));
}
get checked() {
return this.#internals.states.has('checked');
}
set checked(flag) {
if (flag) {
this.#internals.states.add('checked');
} else {
this.#internals.states.delete('checked');
}
}
_onClick(event) {
this.checked = !this.checked;
}
}
```
## Performance
See [benchmark](https://github.com/asamuzaK/domSelector/actions/workflows/benchmark.yml) for the latest results.
## Acknowledgments
The following resources have been of great help in the development of the DOM Selector.
* [CSSTree](https://github.com/csstree/csstree)
* [selery](https://github.com/danburzo/selery)
* [jsdom](https://github.com/jsdom/jsdom)
* [nwsapi](https://github.com/dperini/nwsapi)
---
Copyright (c) 2023 [asamuzaK (Kazz)](https://github.com/asamuzaK/)
+92
View File
@@ -0,0 +1,92 @@
{
"name": "@asamuzakjp/dom-selector",
"description": "A CSS selector engine.",
"author": "asamuzaK",
"license": "MIT",
"homepage": "https://github.com/asamuzaK/domSelector#readme",
"bugs": {
"url": "https://github.com/asamuzaK/domSelector/issues"
},
"repository": {
"type": "git",
"url": "git+https://github.com/asamuzaK/domSelector.git"
},
"files": [
"src",
"types"
],
"type": "module",
"types": "./types/index.d.ts",
"exports": {
".": {
"types": "./types/index.d.ts",
"default": "./src/index.js"
},
"./package.json": "./package.json"
},
"dependencies": {
"bidi-js": "^1.0.3",
"css-tree": "^3.2.1",
"is-potential-custom-element-name": "^1.0.1",
"lru-cache": "^11.5.2"
},
"devDependencies": {
"@types/css-tree": "^2.3.11",
"@types/node": "^26.1.1",
"c8": "^12.0.0",
"chai": "^6.2.2",
"commander": "^15.0.0",
"eslint": "^9.39.5",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-jsdoc": "^63.2.1",
"eslint-plugin-prettier": "^5.5.6",
"eslint-plugin-regexp": "^3.1.1",
"eslint-plugin-unicorn": "^65.0.1",
"globals": "^17.7.0",
"jsdom": "^29.1.1",
"mitata": "^1.0.34",
"mocha": "^11.7.6",
"neostandard": "^0.13.0",
"prettier": "^3.9.6",
"sinon": "^22.1.0",
"tinybench": "^6.1.2",
"typescript": "^7.0.2",
"wpt-runner": "^7.0.0"
},
"overrides": {
"c8": {
"yargs": "^18.0.0"
},
"eslint": {
"brace-expansion": "^1.1.16",
"js-yaml": "^4.3.0"
},
"jsdom": "$jsdom",
"mocha": {
"diff": "^8.0.3",
"js-yaml": "^4.3.0"
},
"serialize-javascript": "^7.0.4"
},
"scripts": {
"bench": "node --expose-gc benchmark/bench.js",
"bench:cache": "node --expose-gc benchmark/bench-cache.js",
"bench:nwsapi": "node --expose-gc benchmark/bench-nwsapi.js",
"bench:ps-form": "node --expose-gc benchmark/bench-default.js && node --expose-gc benchmark/bench-indeterminate.js && node --expose-gc benchmark/bench-valid-invalid.js",
"bench:ps-has": "node --expose-gc benchmark/bench-has.js",
"bench:ps-nth": "node --expose-gc benchmark/bench-nth-child.js && node --expose-gc benchmark/bench-nth-of-type.js",
"bench:ps-traverse": "node --expose-gc benchmark/bench-dir.js && node --expose-gc benchmark/bench-lang.js",
"bench:tl": "node --expose-gc benchmark/bench-testing-library.js",
"bench:universal": "node --expose-gc benchmark/bench-universal.js",
"build": "npm run tsc && npm run lint && npm test",
"lint": "eslint --fix .",
"test": "c8 --reporter=text mocha --exit test/**/*.test.js",
"test:wpt": "node test/wpt/wpt-runner.js",
"tsc": "node scripts/index clean --dir=types -i && npx tsc",
"update:wpt": "git submodule update --init --recursive --remote"
},
"engines": {
"node": "^22.13.0 || >=24.0.0"
},
"version": "8.3.2"
}
+379
View File
@@ -0,0 +1,379 @@
/*!
* DOM Selector - A CSS selector engine.
* @license MIT
* @copyright asamuzaK (Kazz)
* @see {@link https://github.com/asamuzaK/domSelector/blob/main/LICENSE}
*/
/* import */
import { LRUCache } from 'lru-cache';
import { Finder } from './js/finder.js';
import { Nwsapi } from './js/nwsapi.js';
import { extractSubjectsAst, parseSelector } from './js/parser.js';
import {
extractSubjectsRegExp,
filterSelector,
isSupportedAST
} from './js/selector.js';
import { collectAllDescendants, getType } from './js/utility.js';
/* constants */
import {
DOCUMENT_NODE,
ELEMENT_NODE,
TARGET_ALL,
TARGET_FIRST,
TARGET_LINEAL,
TARGET_SELF
} from './js/constant.js';
const CACHE_SIZE = 4096;
/* regexp */
const REG_SELECTOR = /[[\]():\\"'`]/;
const REG_UNIVERSAL = /^(?:\*\|)?\*$/;
/**
* @typedef {object} CheckResult
* @property {boolean} match - The match result.
* @property {string?} pseudoElement - The pseudo-element, if any.
* @property {object?} ast - The AST object.
*/
/* DOMSelector */
export class DOMSelector {
/* private fields */
#window;
#document;
#finder;
#idlUtils;
#nwsapi;
#cache;
/**
* Creates an instance of DOMSelector.
* @param {Window} window - The window object.
* @param {Document} document - The document object.
* @param {object} [opt] - Options.
*/
constructor(window, document, opt = {}) {
const { cacheSize, idlUtils } = opt;
this.#window = window;
this.#document = document ?? window.document;
this.#idlUtils = idlUtils;
this.#cache = new LRUCache({
max: cacheSize ?? CACHE_SIZE
});
this.#finder = new Finder(this.#window);
this.#nwsapi = new Nwsapi(this.#window, this.#document, cacheSize);
}
/**
* Wraps the node for IDL internal implementation if idlUtils is present.
* @private
* @param {Document|DocumentFragment|Element} node - The raw node.
* @returns {object} The wrapped or raw node.
*/
#wrapNode = node =>
this.#idlUtils ? this.#idlUtils.wrapperForImpl(node) : node;
/**
* Validates a node and returns an Error if invalid.
* @private
* @param {Document|DocumentFragment|Element} node - The node to check.
* @param {boolean} [element] - `true` if the node must be an Element.
* @returns {TypeError|null} Returns a TypeError if invalid, otherwise null.
*/
#validateNodeType = (node, element = false) => {
if (!node?.nodeType) {
return new this.#window.TypeError(`Unexpected type ${getType(node)}`);
}
if (element && node.nodeType !== ELEMENT_NODE) {
return new this.#window.TypeError(`Unexpected node ${node.nodeName}`);
}
return null;
};
/**
* Executes Nwsapi matching logic with caching and error wrapping.
* @private
* @param {string} selector - The CSS selector to match against.
* @param {Document|Element} node - The target node to check.
* @param {number} targetType - The target constant indicating the scope (e.g., TARGET_SELF).
* @param {function(object): (Array<Element>|Element|boolean|null)} callback - The callback function that executes the specific nwsapi method.
* @param {boolean} [isCheck] - True if is check method.
* @returns {{success: boolean, result: Array<Element>|Element|boolean|null}} An object indicating whether the execution succeeded and its result.
*/
#tryNwsapi = (selector, node, targetType, callback, isCheck = false) => {
const document = node.ownerDocument;
if (
node.isConnected &&
document === this.#document &&
document.contentType === 'text/html' &&
document.documentElement
) {
const cacheKey = `${isCheck ? 'check' : targetType}_${selector}`;
let filterMatches = this.#cache.get(cacheKey);
if (filterMatches === undefined) {
filterMatches = filterSelector(selector, targetType);
this.#cache.set(cacheKey, filterMatches);
}
if (filterMatches) {
try {
return { success: true, result: callback(node) };
} catch {
// fall through
}
}
}
return { success: false, result: null };
};
/**
* Encapsulates Finder traversal logic and error handling.
* @private
* @param {string} selector - The CSS selector to match against.
* @param {Document|DocumentFragment|Element} node - The node from which to start searching.
* @param {object} opt - Optional parameters.
* @param {number} targetType - The target constant indicating the scope (e.g., TARGET_FIRST, TARGET_ALL).
* @returns {Set<Element>|Array<Element>|Element|boolean|null} The search results from Finder, or the error handling return value.
*/
#findNodes = (selector, node, opt, targetType) => {
try {
return this.#finder.setup(selector, node, opt).find(targetType);
} catch (e) {
return this.#finder.onError(e, opt);
}
};
/**
* Clears the internal caches.
* @param {boolean} [clearAll] - Whether to clear all caches. If false,
* only cached matching results are cleared.
* @returns {void}
*/
clear = (clearAll = false) => {
if (clearAll) {
this.#cache.clear();
}
this.#finder.clearResults(true);
this.#nwsapi.clear(clearAll);
};
/**
* Parses a selector and extracts the rightmost subject keys (Id, Class, Tag).
* @param {string} selector - The CSS selector to parse.
* @param {boolean} caseSensitive - True if tag key should be case sensitive.
* @returns {Array<{id: string|null, className: string|null, tag: string|null}>} The list of extracted keys for each selector group.
*/
extractSubjects = (selector, caseSensitive = false) => {
if (!selector || typeof selector !== 'string') {
return [{ id: null, className: null, tag: null }];
}
const cacheKey = `extract_${selector}_${caseSensitive}`;
let subjects = this.#cache.get(cacheKey);
if (subjects !== undefined) {
return subjects;
}
subjects = [];
if (!REG_SELECTOR.test(selector)) {
subjects = extractSubjectsRegExp(selector, caseSensitive);
} else {
try {
const ast = parseSelector(selector);
subjects = extractSubjectsAst(ast);
} catch {
// fall through
}
}
if (!subjects.length) {
subjects.push({ id: null, className: null, tag: null });
}
this.#cache.set(cacheKey, subjects);
return subjects;
};
/**
* Checks if the given CSS selector is supported by this engine.
* @param {string} selector - The CSS selector to check.
* @returns {boolean} `true` if the selector is supported, `false` otherwise.
*/
supports = selector => {
if (typeof selector !== 'string') {
return false;
}
const cacheKey = `supports_${selector}`;
let isSupported = this.#cache.get(cacheKey);
if (isSupported !== undefined) {
return isSupported;
}
if (filterSelector(selector, TARGET_SELF)) {
isSupported = true;
} else {
try {
const ast = parseSelector(selector);
isSupported = isSupportedAST(ast);
} catch {
isSupported = false;
}
}
this.#cache.set(cacheKey, isSupported);
return isSupported;
};
/**
* Checks if an element matches a CSS selector.
* @param {string} selector - The CSS selector to check against.
* @param {Element} node - The element node to check.
* @param {object} [opt] - Optional parameters.
* @returns {CheckResult} An object containing the check result.
*/
check = (selector, node, opt = {}) => {
node = this.#wrapNode(node);
const error = this.#validateNodeType(node, true);
if (error) {
return this.#finder.onError(error, opt);
}
if (REG_UNIVERSAL.test(selector)) {
return {
ast: parseSelector(selector),
match: true,
pseudoElement: null
};
}
const nwsapiRes = this.#tryNwsapi(
selector,
node,
TARGET_SELF,
n => this.#nwsapi.match(selector, n),
true
);
if (nwsapiRes.success) {
let ast = null;
if (nwsapiRes.result) {
const astCacheKey = `check_ast_${selector}`;
ast = this.#cache.get(astCacheKey);
if (ast === undefined) {
ast = parseSelector(selector);
this.#cache.set(astCacheKey, ast);
}
}
return {
match: nwsapiRes.result,
ast,
pseudoElement: null
};
}
const options = { ...opt, check: true, noexcept: true, warn: false };
return this.#finder.setup(selector, node, options).find(TARGET_SELF);
};
/**
* Returns true if the element matches the selector.
* @param {string} selector - The CSS selector to match against.
* @param {Element} node - The element node to test.
* @param {object} [opt] - Optional parameters.
* @returns {boolean} True if the element matches, false otherwise.
*/
matches = (selector, node, opt = {}) => {
node = this.#wrapNode(node);
const error = this.#validateNodeType(node, true);
if (error) {
return this.#finder.onError(error, opt);
}
if (REG_UNIVERSAL.test(selector)) {
return true;
}
const nwsapiRes = this.#tryNwsapi(selector, node, TARGET_SELF, n =>
this.#nwsapi.match(selector, n)
);
if (nwsapiRes.success) {
return nwsapiRes.result;
}
const nodes = this.#findNodes(selector, node, opt, TARGET_SELF);
return !!(nodes && nodes.size > 0);
};
/**
* Traverses up the DOM tree to find the first node that matches the selector.
* @param {string} selector - The CSS selector to match against.
* @param {Element} node - The element from which to start traversing.
* @param {object} [opt] - Optional parameters.
* @returns {?Element} The first matching ancestor element, or `null`.
*/
closest = (selector, node, opt = {}) => {
node = this.#wrapNode(node);
const error = this.#validateNodeType(node, true);
if (error) {
return this.#finder.onError(error, opt);
}
if (REG_UNIVERSAL.test(selector)) {
return node;
}
const nwsapiRes = this.#tryNwsapi(selector, node, TARGET_LINEAL, n =>
this.#nwsapi.closest(selector, n)
);
if (nwsapiRes.success) {
return nwsapiRes.result;
}
const nodes = this.#findNodes(selector, node, opt, TARGET_LINEAL);
if (nodes && nodes.size) {
let refNode = node;
while (refNode) {
if (nodes.has(refNode)) {
return refNode;
}
refNode = refNode.parentNode;
}
}
return null;
};
/**
* Returns the first element within the subtree that matches the selector.
* @param {string} selector - The CSS selector to match.
* @param {Document|DocumentFragment|Element} node - The node to find within.
* @param {object} [opt] - Optional parameters.
* @returns {?Element} The first matching element, or `null`.
*/
querySelector = (selector, node, opt = {}) => {
node = this.#wrapNode(node);
const error = this.#validateNodeType(node);
if (error) {
return this.#finder.onError(error, opt);
}
if (REG_UNIVERSAL.test(selector)) {
return node.firstElementChild;
}
const nodes = this.#findNodes(selector, node, opt, TARGET_FIRST);
if (nodes && nodes.size) {
return nodes.values().next().value;
}
return null;
};
/**
* Returns an array of elements within the subtree that match the selector.
* Note: This method returns an Array, not a NodeList.
* @param {string} selector - The CSS selector to match.
* @param {Document|DocumentFragment|Element} node - The node to find within.
* @param {object} [opt] - Optional parameters.
* @returns {Array<Element>} An array of elements, or an empty array.
*/
querySelectorAll = (selector, node, opt = {}) => {
node = this.#wrapNode(node);
const error = this.#validateNodeType(node);
if (error) {
return this.#finder.onError(error, opt);
}
if (REG_UNIVERSAL.test(selector)) {
const document =
node.nodeType === DOCUMENT_NODE ? node : node.ownerDocument;
return collectAllDescendants(node, document);
}
const nodes = this.#findNodes(selector, node, opt, TARGET_ALL);
if (nodes && nodes.size) {
return [...nodes];
}
return [];
};
}
+286
View File
@@ -0,0 +1,286 @@
/**
* constant.js
*/
/* string */
export const ATRULE = 'Atrule';
export const ATTR_SELECTOR = 'AttributeSelector';
export const CLASS_SELECTOR = 'ClassSelector';
export const COMBINATOR = 'Combinator';
export const DIR_NEXT = 'next';
export const DIR_PREV = 'prev';
export const IDENT = 'Identifier';
export const ID_SELECTOR = 'IdSelector';
export const NEST_SELECTOR = 'NestingSelector';
export const NOT_SUPPORTED_ERR = 'NotSupportedError';
export const NTH = 'Nth';
export const OPERATOR = 'Operator';
export const PS_CLASS_SELECTOR = 'PseudoClassSelector';
export const PS_ELEMENT_SELECTOR = 'PseudoElementSelector';
export const RULE = 'Rule';
export const SCOPE = 'Scope';
export const SELECTOR = 'Selector';
export const SELECTOR_LIST = 'SelectorList';
export const STRING = 'String';
export const SYNTAX_ERR = 'SyntaxError';
export const TARGET_ALL = 'all';
export const TARGET_FIRST = 'first';
export const TARGET_LINEAL = 'lineal';
export const TARGET_SELF = 'self';
export const TYPE_SELECTOR = 'TypeSelector';
/* numeric */
export const BIT_01 = 1;
export const BIT_02 = 2;
export const BIT_04 = 4;
export const BIT_08 = 8;
export const BIT_16 = 0x10;
export const BIT_32 = 0x20;
export const BIT_FFFF = 0xffff;
export const DUO = 2;
export const HEX = 16;
export const TYPE_FROM = 8;
export const TYPE_TO = -1;
/* Node */
export const ELEMENT_NODE = 1;
export const TEXT_NODE = 3;
export const DOCUMENT_NODE = 9;
export const DOCUMENT_FRAGMENT_NODE = 11;
export const DOCUMENT_POSITION_PRECEDING = 2;
export const DOCUMENT_POSITION_CONTAINS = 8;
export const DOCUMENT_POSITION_CONTAINED_BY = 0x10;
/* NodeFilter */
export const FILTER_ACCEPT = 1;
export const FILTER_REJECT = 2;
export const FILTER_SKIP = 3;
export const SHOW_ALL = 0xffffffff;
export const SHOW_CONTAINER = 0x501;
export const SHOW_DOCUMENT = 0x100;
export const SHOW_DOCUMENT_FRAGMENT = 0x400;
export const SHOW_ELEMENT = 1;
/* selectors */
export const ALPHA_NUM = '[A-Z\\d]+';
export const CHILD_IDX = '(?:first|last|only)-(?:child|of-type)';
export const DIGIT = '(?:0|[1-9]\\d*)';
export const LANG_PART = `(?:-${ALPHA_NUM})*`;
export const PSEUDO_CLASS = `(?:any-)?link|${CHILD_IDX}|checked|empty|indeterminate|read-(?:only|write)|target`;
export const ANB = `[+-]?(?:${DIGIT}n?|n)|(?:[+-]?${DIGIT})?n\\s*[+-]\\s*${DIGIT}`;
// combinators
export const COMBO = '\\s?[\\s>~+]\\s?';
export const DESCEND = '\\s?[\\s>]\\s?';
export const SIBLING = '\\s?[+~]\\s?';
// LOGIC_IS: :is()
export const LOGIC_IS = `:is\\(\\s*[^)]+\\s*\\)`;
// N_TH: excludes An+B with selector list, e.g. :nth-child(2n+1 of .foo)
export const N_TH = `nth-(?:last-)?(?:child|of-type)\\(\\s*(?:even|odd|${ANB})\\s*\\)`;
// ATTR_TYPE: excludes [foo|=bar]
export const ATTR_TYPE = '\\[[^|\\]]+\\]';
// SUB_TYPE: attr, id, class, pseudo-class
export const SUB_TYPE = `${ATTR_TYPE}|[#.:][\\w-]+`;
export const SUB_TYPE_WO_PSEUDO = `${ATTR_TYPE}|[#.][\\w-]+`;
// TAG_TYPE: *, tag
export const TAG_TYPE_WO_UNIVERSAL = '[A-Za-z][\\w-]*';
export const TAG_TYPE = `\\*|${TAG_TYPE_WO_UNIVERSAL}`;
export const TAG_TYPE_I = '\\*|[A-Z][\\w-]*';
export const COMPOUND = `(?:${TAG_TYPE}|(?:${TAG_TYPE})?(?:${SUB_TYPE})+)`;
export const COMPOUND_L = `(?:${TAG_TYPE}|(?:${TAG_TYPE})?(?:${SUB_TYPE}|${LOGIC_IS})+)`;
export const COMPOUND_I = `(?:${TAG_TYPE_I}|(?:${TAG_TYPE_I})?(?:${SUB_TYPE})+)`;
export const COMPOUND_WO_PSEUDO = `(?:${TAG_TYPE}|(?:${TAG_TYPE})?(?:${SUB_TYPE_WO_PSEUDO})+)`;
export const COMPLEX = `${COMPOUND}(?:${COMBO}${COMPOUND})*`;
export const COMPLEX_L = `${COMPOUND_L}(?:${COMBO}${COMPOUND_L})*`;
export const HAS_COMPOUND = `has\\([\\s>]?\\s*${COMPOUND_WO_PSEUDO}\\s*\\)`;
export const LOGIC_COMPOUND = `(?:is|not)\\(\\s*${COMPOUND_L}(?:\\s*,\\s*${COMPOUND_L})*\\s*\\)`;
export const LOGIC_COMPLEX = `(?:is|not)\\(\\s*${COMPLEX_L}(?:\\s*,\\s*${COMPLEX_L})*\\s*\\)`;
/* forms and input types */
export const FORM_PARTS = Object.freeze([
'button',
'input',
'select',
'textarea'
]);
export const INPUT_BUTTON = Object.freeze(['button', 'reset', 'submit']);
export const INPUT_CHECK = Object.freeze(['checkbox', 'radio']);
export const INPUT_DATE = Object.freeze([
'date',
'datetime-local',
'month',
'time',
'week'
]);
export const INPUT_TEXT = Object.freeze([
'email',
'password',
'search',
'tel',
'text',
'url'
]);
export const INPUT_EDIT = Object.freeze([
...INPUT_DATE,
...INPUT_TEXT,
'number'
]);
export const INPUT_LTR = Object.freeze([
...INPUT_CHECK,
'color',
'date',
'image',
'number',
'range',
'time'
]);
/* Attribute names that must treet values as ASCII case-insensitive */
export const ATTR_VALUE_I = Object.freeze([
'accept',
'accept-charset',
'align',
'alink',
'axis',
'bgcolor',
'charset',
'checked',
'clear',
'codetype',
'color',
'compact',
'declare',
'defer',
'dir',
'direction',
'disabled',
'enctype',
'face',
'frame',
'hreflang',
'http-equiv',
'lang',
'language',
'link',
'media',
'method',
'multiple',
'nohref',
'noresize',
'noshade',
'nowrap',
'readonly',
'rel',
'rev',
'rules',
'scope',
'scrolling',
'selected',
'shape',
'target',
'text',
'type',
'valign',
'valuetype',
'vlink'
]);
/* logical combination pseudo-classes */
export const KEYS_LOGICAL = new Set(['has', 'is', 'not', 'where']);
/* lists of supported / unsupported pseudo-classes and pseudo-elements */
export const KEYS_PS_CLASS_SUPPORTED = new Set([
'active',
'any-link',
'checked',
'closed',
'default',
'defined',
'dir',
'disabled',
'empty',
'enabled',
'first-child',
'first-of-type',
'focus',
'focus-visible',
'focus-within',
'has',
'host',
'host-context',
'hover',
'in-range',
'indeterminate',
'invalid',
'is',
'lang',
'last-child',
'last-of-type',
'link',
'local-link',
'not',
'nth-child',
'nth-last-child',
'nth-last-of-type',
'nth-of-type',
'only-child',
'only-of-type',
'open',
'optional',
'out-of-range',
'placeholder-shown',
'read-only',
'read-write',
'required',
'root',
'scope',
'state',
'target',
'target-within',
'valid',
'visited',
'where'
]);
export const KEYS_PS_CLASS_UNSUPPORTED = new Set([
'autofill',
'blank',
'buffering',
'contains',
'current',
'fullscreen',
'future',
'has-slotted',
'heading',
'modal',
'muted',
'nth-col',
'nth-last-col',
'past',
'paused',
'picture-in-picture',
'playing',
'popover-open',
'seeking',
'stalled',
'user-invalid',
'user-valid',
'volume-locked',
'-webkit-autofill'
]);
export const KEYS_PS_ELEMENT_UNSUPPORTED = new Set([
'after',
'backdrop',
'before',
'cue',
'cue-region',
'file-selector-button',
'first-letter',
'first-line',
'marker',
'part',
'placeholder',
'selection',
'slotted',
'target-text'
]);
File diff suppressed because it is too large. Load diff
+154
View File
@@ -0,0 +1,154 @@
/**
* event.js
*/
/* constants */
const KEYS_MODIFIER = new Set([
'Alt',
'AltGraph',
'CapsLock',
'Control',
'Fn',
'FnLock',
'Hyper',
'Meta',
'NumLock',
'ScrollLock',
'Shift',
'Super',
'Symbol',
'SymbolLock'
]);
/**
* EventHandler
* Tracks window events to record recent user interactions.
*/
export class EventHandler {
/* private fields */
#window;
#event;
#eventHandlers;
#fucusEvent;
#lastFocusVisible;
#cleanupFunctions;
/**
* @param {object} window - The window object.
*/
constructor(window) {
this.#window = window;
this.#event = null;
this.#fucusEvent = null;
this.#lastFocusVisible = null;
this.#eventHandlers = [
{
keys: ['focus', 'focusin'],
handler: this.handleFocusEvent
},
{
keys: ['keydown', 'keyup'],
handler: this.handleKeyboardEvent
},
{
keys: ['mouseover', 'mousedown', 'mouseup', 'click', 'mouseout'],
handler: this.handleMouseEvent
}
];
this.registerEventListeners();
}
/**
* Gets the most recent non-modifier keyboard or mouse event.
* @returns {Event|null} The current event object.
*/
get currentEvent() {
return this.#event;
}
/**
* Gets the most recent focus event.
* @returns {Event|null} The current focus event object.
*/
get currentFocus() {
return this.#fucusEvent;
}
/**
* Gets the node that last had visible focus.
* @returns {Node|null} The last focused node.
*/
get lastFocusVisible() {
return this.#lastFocusVisible;
}
/**
* Sets the node that last had visible focus.
* @param {Node} node - The node to set as the last visible focus.
*/
set lastFocusVisible(node) {
this.#lastFocusVisible = node;
}
/**
* Handles focus events and updates the current focus state.
* @param {Event} evt - The focus event.
*/
handleFocusEvent = evt => {
this.#fucusEvent = evt;
};
/**
* Handles keyboard events and updates the current event state.
* @param {KeyboardEvent} evt - The keyboard event.
*/
handleKeyboardEvent = evt => {
const { key } = evt;
if (!KEYS_MODIFIER.has(key)) {
this.#event = evt;
}
};
/**
* Handles mouse events and updates the current event state.
* @param {MouseEvent} evt - The mouse event.
*/
handleMouseEvent = evt => {
this.#event = evt;
};
/**
* Registers all predefined event listeners on the window object.
*/
registerEventListeners = () => {
if (!this.#cleanupFunctions) {
this.#cleanupFunctions = [];
}
for (const { keys, handler } of this.#eventHandlers) {
for (const key of keys) {
this.#window.addEventListener(key, handler, {
capture: true,
passive: true
});
this.#cleanupFunctions.push(() => {
this.#window.removeEventListener(key, handler, { capture: true });
});
}
}
};
/**
* Removes all registered event listeners and resets all internal states.
*/
destroy = () => {
if (this.#cleanupFunctions) {
for (const cleanup of this.#cleanupFunctions) {
cleanup();
}
}
this.#cleanupFunctions = null;
this.#event = null;
this.#fucusEvent = null;
this.#lastFocusVisible = null;
};
}
File diff suppressed because it is too large. Load diff
+101
View File
@@ -0,0 +1,101 @@
/**
* mapper.js
*/
import { SelectorProcessor } from './processor.js';
import { parseSelector, walkAST } from './parser.js';
import { createHasValidator } from './selector.js';
/**
* Mapper
* Maps CSS selectors to parsed ASTs and sets up node arrays.
*/
export class Mapper {
#context;
#processor;
/**
* @param {import('./finder.js').Finder} context - The Finder instance.
*/
constructor(context) {
this.#context = context;
this.#processor = new SelectorProcessor(context);
}
/**
* Gets the corresponding AST and empty nodes array for the given selector.
* @param {string} selector - The CSS selector string.
* @returns {Array} An array containing the AST, empty nodes array, and selector AST.
*/
correspond(selector) {
const ctx = this.#context;
const nodes = [];
let ast = null;
// Check cache.
if (ctx.documentCache.has(ctx.document)) {
const cachedItem = ctx.documentCache.get(ctx.document);
if (cachedItem && cachedItem.has(selector)) {
const item = cachedItem.get(selector);
ast = item.ast;
ctx.invalidate = item.invalidate;
ctx.selectorAST = item.selectorAST;
}
}
// Clear flags for reuse if cache hit.
if (ast) {
const l = ast.length;
for (let i = 0; i < l; i++) {
ast[i].dir = null;
ast[i].filtered = false;
ast[i].find = false;
nodes[i] = [];
}
return [ast, nodes, ctx.selectorAST];
}
// Parse selector and build metadata.
const selectorAST = parseSelector(selector);
const { branches, info } = walkAST(
selectorAST,
true,
createHasValidator(ctx.window)
);
const {
hasHasPseudoFunc,
hasLogicalPseudoFunc,
hasNestingSelector,
hasNthChildOfSelector,
hasStatePseudoClass,
hasUnsupportedPseudoClass
} = info;
// Determine invalidation flags.
ctx.invalidate =
hasHasPseudoFunc ||
hasNestingSelector ||
hasStatePseudoClass ||
hasUnsupportedPseudoClass ||
!!(hasLogicalPseudoFunc && hasNthChildOfSelector);
// Process branches.
const processed = this.#processor.process(branches, selector);
ast = processed.ast;
const descendant = processed.descendant;
// Store in cache.
let cachedItem;
if (ctx.documentCache.has(ctx.document)) {
cachedItem = ctx.documentCache.get(ctx.document);
} else {
cachedItem = new Map();
}
cachedItem.set(selector, {
ast,
descendant,
invalidate: ctx.invalidate,
selectorAST
});
ctx.documentCache.set(ctx.document, cachedItem);
// Initialize nodes.
const l = ast.length;
for (let i = 0; i < l; i++) {
nodes[i] = [];
}
return [ast, nodes, selectorAST];
}
}
+768
View File
@@ -0,0 +1,768 @@
/**
* matcher.js
*/
/* import */
import { generateCSS, parseAstName, unescapeSelector } from './parser.js';
import {
generateException,
getDirectionality,
getLanguageAttribute,
getType,
isContentEditable,
isNamespaceDeclared
} from './utility.js';
/* constants */
import {
ALPHA_NUM,
ATTR_VALUE_I,
INPUT_EDIT,
LANG_PART,
NOT_SUPPORTED_ERR,
PS_ELEMENT_SELECTOR,
STRING,
SYNTAX_ERR
} from './constant.js';
const KEYS_ATTR_VALUE_I = new Set(ATTR_VALUE_I);
const KEYS_INPUT_EDIT = new Set(INPUT_EDIT);
/* regexp */
const REG_LANG_VALID = new RegExp(`^(?:\\*-)?${ALPHA_NUM}${LANG_PART}$`, 'i');
/* cache */
const astMetaCache = new WeakMap();
/**
* Validates a pseudo-element selector.
* @param {string} astName - The name of the pseudo-element from the AST.
* @param {string} astType - The type of the selector from the AST.
* @param {object} [opt] - Optional parameters.
* @param {boolean} [opt.forgive] - If true, ignores unknown pseudo-elements.
* @param {object} [opt.globalObject] - The global object.
* @param {boolean} [opt.warn] - If true, throws an error for unsupported ones.
* @throws {DOMException} If the selector is invalid or unsupported.
* @returns {void}
*/
export const matchPseudoElementSelector = (
astName,
astType,
{ forgive, globalObject, warn } = {}
) => {
if (astType !== PS_ELEMENT_SELECTOR) {
// Ensure the AST node is a pseudo-element selector.
throw new TypeError(`Unexpected ast type ${getType(astType)}`);
}
switch (astName) {
case 'after':
case 'backdrop':
case 'before':
case 'cue':
case 'cue-region':
case 'first-letter':
case 'first-line':
case 'file-selector-button':
case 'marker':
case 'placeholder':
case 'selection':
case 'target-text': {
// Warn if the pseudo-element is known but unsupported.
if (warn) {
throw generateException(
`Unsupported pseudo-element ::${astName}`,
NOT_SUPPORTED_ERR,
globalObject
);
}
break;
}
case 'part':
case 'slotted': {
// Warn if the functional pseudo-element is known but unsupported.
if (warn) {
throw generateException(
`Unsupported pseudo-element ::${astName}()`,
NOT_SUPPORTED_ERR,
globalObject
);
}
break;
}
default: {
// Handle vendor-prefixed unknown pseudo-elements.
if (astName.startsWith('-webkit-')) {
if (warn) {
throw generateException(
`Unsupported pseudo-element ::${astName}`,
NOT_SUPPORTED_ERR,
globalObject
);
}
} else if (!forgive) {
// Throw an error for unknown pseudo-elements if not forgiven.
throw generateException(
`Unknown pseudo-element ::${astName}`,
SYNTAX_ERR,
globalObject
);
}
}
}
};
/**
* Matches the :dir() pseudo-class against an element's directionality.
* @param {object} ast - The AST object for the pseudo-class.
* @param {object} node - The element node to match against.
* @param {WeakMap} [dirCache] - Cache for directionality.
* @throws {TypeError} If the AST does not contain a valid direction value.
* @returns {boolean} - True if matches, otherwise false.
*/
export const matchDirectionPseudoClass = (
ast,
node,
dirCache = new WeakMap()
) => {
const { name } = ast;
if (!name) {
const type = name === '' ? '(empty String)' : getType(name);
throw new TypeError(`Unexpected ast type ${type}`);
}
const dir = getDirectionality(node, dirCache);
return name === dir;
};
/**
* Matches the :lang() pseudo-class against an element's language attribute.
* @param {object} ast - The AST object for the pseudo-class child.
* @param {object} node - The element node to match against.
* @param {WeakMap} [langCache] - Cache for language attributes.
* @throws {TypeError} If the AST does not contain a valid language value.
* @returns {boolean} - True if matches, otherwise false.
*/
export const matchLanguagePseudoClass = (
ast,
node,
langCache = new WeakMap()
) => {
const elementLang = getLanguageAttribute(node, langCache);
if (elementLang === null) {
return false;
}
let meta = astMetaCache.get(ast);
if (meta === undefined) {
meta = {};
astMetaCache.set(ast, meta);
}
if (meta.langRegex !== undefined) {
if (meta.langPattern === '*') {
return elementLang !== '';
}
if (meta.langRegex === null) {
return false;
}
return meta.langRegex.test(elementLang);
}
const { name, type, value } = ast;
const langPattern =
type === STRING && value !== undefined ? value : unescapeSelector(name);
meta.langPattern = langPattern;
if (typeof langPattern !== 'string') {
meta.langRegex = null;
return false;
}
if (langPattern === '*') {
meta.langRegex = null;
return elementLang !== '';
}
if (!REG_LANG_VALID.test(langPattern)) {
meta.langRegex = null;
return false;
}
const regexStr = langPattern
.split('-')
.map((part, index) => {
const core = part === '*' ? ALPHA_NUM : part;
return (index === 0 ? core : `-${core}`) + LANG_PART;
})
.join('');
const matcherRegex = new RegExp(`^${regexStr}$`, 'i');
meta.langRegex = matcherRegex;
return matcherRegex.test(elementLang);
};
/**
* Matches the :checked pseudo-class.
* @param {object} node - The Element node.
* @returns {boolean} True if matches, otherwise false.
*/
export const matchCheckedPseudoClass = node => {
const { localName } = node;
if (localName === 'option') {
return node.selected;
}
if (localName === 'input') {
const attrType = node.getAttribute('type');
if (node.checked) {
return attrType === 'checkbox' || attrType === 'radio';
}
}
return false;
};
/**
* Matches the :any-link and :link pseudo-classes.
* @param {object} node - The Element node.
* @returns {boolean} True if matches, otherwise false.
*/
export const matchLinkPseudoClass = node => {
const { localName } = node;
if (localName === 'a' || localName === 'area') {
return node.hasAttribute('href');
}
return false;
};
/**
* Matches the :open pseudo-class.
* @param {object} node - The Element node.
* @returns {boolean} True if matches, otherwise false.
*/
export const matchOpenPseudoClass = node => {
const { localName } = node;
if (localName === 'details' || localName === 'dialog') {
return node.hasAttribute('open');
}
return false;
};
/**
* Matches the :placeholder-shown pseudo-class.
* @param {object} node - The Element node.
* @param {Set.<string>} keys - A set of input type keys.
* @returns {boolean} True if matches, otherwise false.
*/
export const matchPlaceholderShownPseudoClass = (node, keys) => {
let placeholder;
if (node.placeholder) {
placeholder = node.placeholder;
} else if (node.hasAttribute('placeholder')) {
placeholder = node.getAttribute('placeholder');
}
if (typeof placeholder === 'string' && !/[\r\n]/.test(placeholder)) {
let targetNode;
const { localName } = node;
if (localName === 'textarea') {
targetNode = node;
} else if (localName === 'input') {
if (node.hasAttribute('type')) {
if (keys.has(node.getAttribute('type'))) {
targetNode = node;
}
} else {
targetNode = node;
}
}
if (targetNode) {
return node.value === '';
}
}
return false;
};
/**
* Matches the :in-range and :out-of-range pseudo-classes.
* @param {string} astName - The name of the pseudo-class.
* @param {object} node - The Element node.
* @param {Set.<string>} keys - A set of input type keys.
* @returns {boolean} True if matches, otherwise false.
*/
export const matchRangePseudoClass = (astName, node, keys) => {
const { localName } = node;
const attrType = node.getAttribute('type');
if (
localName === 'input' &&
!(node.readOnly || node.hasAttribute('readonly')) &&
!(node.disabled || node.hasAttribute('disabled')) &&
keys.has(attrType)
) {
const flowed = node.validity.rangeUnderflow || node.validity.rangeOverflow;
if (astName === 'out-of-range') {
return flowed;
}
if (flowed) {
return false;
}
return (
node.hasAttribute('min') ||
node.hasAttribute('max') ||
attrType === 'range'
);
}
return false;
};
/**
* Match the :read-only and :read-write pseudo-classes
* @param {string} astName - pseudo-class name
* @param {object} node - Element node
* @returns {boolean} - True if matches, otherwise false.
*/
export const matchReadOnlyPseudoClass = (astName, node) => {
const { localName } = node;
let isReadOnly = false;
switch (localName) {
case 'textarea':
case 'input': {
const isEditableInput = !node.type || KEYS_INPUT_EDIT.has(node.type);
if (localName === 'textarea' || isEditableInput) {
isReadOnly =
node.readOnly ||
node.hasAttribute('readonly') ||
node.disabled ||
node.hasAttribute('disabled');
} else {
// Non-editable input types are always read-only
isReadOnly = true;
}
break;
}
default: {
isReadOnly = !isContentEditable(node);
}
}
if (astName === 'read-only') {
return isReadOnly;
}
return !isReadOnly;
};
/**
* Matches the :required and :optional pseudo-classes.
* @param {string} astName - The name of the pseudo-class.
* @param {object} node - The Element node.
* @param {Set.<string>} keys - A set of input type keys.
* @returns {boolean} True if matches, otherwise false.
*/
export const matchRequiredPseudoClass = (astName, node, keys) => {
const { localName } = node;
let required = false;
if (localName === 'select' || localName === 'textarea') {
if (node.required || node.hasAttribute('required')) {
required = true;
}
} else if (localName === 'input') {
if (node.hasAttribute('type')) {
const attrType = node.getAttribute('type');
if (keys.has(attrType)) {
if (node.required || node.hasAttribute('required')) {
required = true;
}
}
} else if (node.required || node.hasAttribute('required')) {
required = true;
}
} else {
return false;
}
if (astName === 'optional') {
return !required;
}
return required;
};
/**
* Matches an attribute selector against an element.
* @param {object} ast - The AST for the attribute selector.
* @param {object} node - The element node to match against.
* @param {object} [opt] - Optional parameters.
* @param {boolean} [opt.check] - True if running in an internal check.
* @param {boolean} [opt.forgive] - True to forgive certain syntax errors.
* @param {object} [opt.globalObject] - The global object.
* @returns {boolean} - True if the attribute selector matches, otherwise false.
*/
export const matchAttributeSelector = (
ast,
node,
{ check, forgive, globalObject } = {}
) => {
const {
flags: astFlags,
matcher: astMatcher,
name: astName,
value: astValue
} = ast;
if (typeof astFlags === 'string' && !/^[is]$/i.test(astFlags) && !forgive) {
const css = generateCSS(ast);
throw generateException(
`Invalid selector ${css}`,
SYNTAX_ERR,
globalObject
);
}
const isHTML = node.ownerDocument.contentType === 'text/html';
let meta = astMetaCache.get(ast);
if (meta === undefined) {
meta = {
attrValues: new Set()
};
astMetaCache.set(ast, meta);
}
if (astMatcher === null && !astFlags && typeof astName?.name === 'string') {
if (meta.astName === undefined) {
const rawName = unescapeSelector(astName.name);
meta.astName = rawName;
meta.hasPipe = rawName.indexOf('|') > -1;
}
if (!meta.hasPipe) {
if (
meta.astName === 'lang' &&
node.hasAttributeNS('http://www.w3.org/XML/1998/namespace', 'lang')
) {
return false;
}
if (node.hasAttribute(meta.astName)) {
return true;
}
const attrs = node.attributes;
if (!attrs || attrs.length === 0) {
return false;
}
const checkName = isHTML ? meta.astName.toLowerCase() : meta.astName;
for (let i = 0, len = attrs.length; i < len; i++) {
let itemName = attrs[i].name;
if (isHTML) {
itemName = itemName.toLowerCase();
}
const colonIdx = itemName.indexOf(':');
if (colonIdx > -1) {
const itemLocalName = itemName
.substring(colonIdx + 1)
.replace(/^:/, '');
if (checkName === itemLocalName) {
return true;
}
} else if (checkName === itemName) {
return true;
}
}
return false;
}
}
const { attributes } = node;
if (!attributes || !attributes.length) {
return false;
}
if (meta.caseInsensitive === undefined) {
let caseInsensitive = false;
if (typeof astFlags === 'string') {
caseInsensitive = /^i$/i.test(astFlags);
} else if (isHTML) {
caseInsensitive = KEYS_ATTR_VALUE_I.has(astName.name);
}
meta.caseInsensitive = caseInsensitive;
let astAttrName = unescapeSelector(astName.name);
meta.hasPipeInName = astAttrName.indexOf('|') > -1;
if ((isHTML && !meta.hasPipeInName) || caseInsensitive) {
astAttrName = astAttrName.toLowerCase();
}
meta.astAttrName = astAttrName;
if (meta.hasPipeInName) {
const { prefix, localName } = parseAstName(astAttrName);
meta.astPrefix = prefix;
meta.astLocalName = localName;
}
const { name: astIdentValue, value: astStringValue } = astValue ?? {};
let attrValue;
if (astIdentValue) {
attrValue = unescapeSelector(astIdentValue);
} else if (astStringValue !== undefined) {
attrValue = astStringValue;
}
if (caseInsensitive && typeof attrValue === 'string') {
attrValue = attrValue.toLowerCase();
}
meta.cachedAttrValue = attrValue;
if (astMatcher === '~=' && attrValue && typeof attrValue === 'string') {
meta.tildeTarget = /\s/.test(attrValue) ? null : ` ${attrValue} `;
}
}
const caseInsensitive = meta.caseInsensitive;
const astAttrName = meta.astAttrName;
const attrValue = meta.cachedAttrValue;
const attrValues = meta.attrValues;
if (attrValues.size) {
attrValues.clear();
}
if (meta.hasPipeInName) {
const astPrefix = meta.astPrefix;
const astLocalName = meta.astLocalName;
for (let i = 0, len = attributes.length; i < len; i++) {
const item = attributes[i];
let itemName = item.name;
let itemValue = item.value;
if (caseInsensitive) {
itemName = itemName.toLowerCase();
itemValue = itemValue.toLowerCase();
}
const colonIdx = itemName.indexOf(':');
switch (astPrefix) {
case '': {
if (astLocalName === itemName) {
attrValues.add(itemValue);
}
break;
}
case '*': {
if (colonIdx > -1) {
const itemLocalName = itemName
.substring(colonIdx + 1)
.replace(/^:/, '');
if (itemLocalName === astLocalName) {
attrValues.add(itemValue);
}
} else if (astLocalName === itemName) {
attrValues.add(itemValue);
}
break;
}
default: {
if (!check) {
if (forgive) {
return false;
}
const css = generateCSS(ast);
throw generateException(
`Invalid selector ${css}`,
SYNTAX_ERR,
globalObject
);
}
if (colonIdx > -1) {
const itemPrefix = itemName.substring(0, colonIdx);
const itemLocalName = itemName
.substring(colonIdx + 1)
.replace(/^:/, '');
if (itemPrefix === 'xml' && itemLocalName === 'lang') {
continue;
}
if (astPrefix === itemPrefix && astLocalName === itemLocalName) {
if (isNamespaceDeclared(astPrefix, node)) {
attrValues.add(itemValue);
}
}
}
}
}
}
} else {
for (let i = 0, len = attributes.length; i < len; i++) {
const item = attributes[i];
const origName = item.name;
if (
item.namespaceURI === 'http://www.w3.org/XML/1998/namespace' &&
item.localName === 'lang'
) {
continue;
}
const colonIdx = origName.indexOf(':');
if (colonIdx > -1) {
const itemPrefix = origName.substring(0, colonIdx);
const itemLocalName = origName
.substring(colonIdx + 1)
.replace(/^:/, '');
if (itemPrefix === 'xml' && itemLocalName === 'lang') {
continue;
}
}
let isMatch = false;
if (origName === astAttrName) {
isMatch = true;
} else if (caseInsensitive && origName.toLowerCase() === astAttrName) {
isMatch = true;
}
if (isMatch) {
attrValues.add(caseInsensitive ? item.value.toLowerCase() : item.value);
} else if (colonIdx > -1) {
const itemLocalName = origName
.substring(colonIdx + 1)
.replace(/^:/, '');
let isLocalMatch = false;
if (itemLocalName === astAttrName) {
isLocalMatch = true;
} else if (
caseInsensitive &&
itemLocalName.toLowerCase() === astAttrName
) {
isLocalMatch = true;
}
if (isLocalMatch) {
attrValues.add(
caseInsensitive ? item.value.toLowerCase() : item.value
);
}
}
}
}
if (!attrValues.size) {
return false;
}
switch (astMatcher) {
case '=': {
return typeof attrValue === 'string' && attrValues.has(attrValue);
}
case '~=': {
if (meta.tildeTarget === null || !attrValue) {
return false;
}
const target = meta.tildeTarget;
for (const value of attrValues) {
if (` ${value.replace(/[\t\r\n\f]/g, ' ')} `.includes(target)) {
return true;
}
}
return false;
}
case '|=': {
if (!attrValue || typeof attrValue !== 'string') {
return false;
}
const prefix = `${attrValue}-`;
for (const value of attrValues) {
if (value === attrValue || value.startsWith(prefix)) {
return true;
}
}
return false;
}
case '^=': {
if (!attrValue || typeof attrValue !== 'string') {
return false;
}
for (const value of attrValues) {
if (value.startsWith(attrValue)) {
return true;
}
}
return false;
}
case '$=': {
if (!attrValue || typeof attrValue !== 'string') {
return false;
}
for (const value of attrValues) {
if (value.endsWith(attrValue)) {
return true;
}
}
return false;
}
case '*=': {
if (!attrValue || typeof attrValue !== 'string') {
return false;
}
for (const value of attrValues) {
if (value.includes(attrValue)) {
return true;
}
}
return false;
}
case null:
default: {
return true;
}
}
};
/**
* Matches a type selector against an element.
* @param {object} ast - The AST for the type selector.
* @param {object} node - The element node to match against.
* @param {object} [opt] - Optional parameters.
* @param {boolean} [opt.check] - True if running in an internal check.
* @param {boolean} [opt.forgive] - True to forgive undeclared namespace.
* @param {object} [opt.globalObject] - The global object.
* @returns {boolean} - True if the type selector matches, otherwise false.
*/
export const matchTypeSelector = (
ast,
node,
{ check, forgive, globalObject } = {}
) => {
const { localName, namespaceURI, prefix } = node;
let meta = astMetaCache.get(ast);
if (meta === undefined) {
meta = {};
astMetaCache.set(ast, meta);
}
if (meta.astName === undefined) {
const astName = unescapeSelector(ast.name);
const { prefix: parsedPrefix, localName: parsedLocalName } = parseAstName(
astName,
node
);
meta.astName = astName;
meta.astPrefix = parsedPrefix;
meta.astPrefixLowerCased = parsedPrefix.toLowerCase();
meta.astLocalName = parsedLocalName;
meta.astLocalNameLowerCased = parsedLocalName.toLowerCase();
meta.hasPipe = astName.includes('|');
}
const isHTML =
node.ownerDocument.contentType === 'text/html' &&
(!namespaceURI || namespaceURI === 'http://www.w3.org/1999/xhtml');
if (isHTML && localName === meta.astLocalName && !meta.hasPipe) {
return true;
}
let nodePrefix;
let nodeLocalName;
const colonIdx = localName.indexOf(':');
if (colonIdx > -1) {
nodePrefix = localName.substring(0, colonIdx);
nodeLocalName = localName.substring(colonIdx + 1);
} else {
nodePrefix = prefix || '';
nodeLocalName = localName;
}
const astPrefix = isHTML ? meta.astPrefixLowerCased : meta.astPrefix;
const astLocalName = isHTML ? meta.astLocalNameLowerCased : meta.astLocalName;
const isUniversal = astLocalName === '*';
switch (astPrefix) {
case '': {
return (
!nodePrefix &&
!namespaceURI &&
(isUniversal || astLocalName === nodeLocalName)
);
}
case '*': {
return isUniversal || astLocalName === nodeLocalName;
}
default: {
if (!check) {
if (forgive) {
return false;
}
const css = generateCSS(ast);
throw generateException(
`Invalid selector ${css}`,
SYNTAX_ERR,
globalObject
);
}
const astNS = node.lookupNamespaceURI(astPrefix);
const nodeNS = node.lookupNamespaceURI(nodePrefix);
if (astNS === nodeNS && astPrefix === nodePrefix) {
return isUniversal || astLocalName === nodeLocalName;
} else if (!forgive && !astNS) {
throw generateException(
`Undeclared namespace ${astPrefix}`,
SYNTAX_ERR,
globalObject
);
}
return false;
}
}
};
File diff suppressed because it is too large. Load diff
+504
View File
@@ -0,0 +1,504 @@
/**
* parser.js
*/
/* import */
import * as cssTree from 'css-tree';
import { getType } from './utility.js';
/* constants */
import {
ATTR_SELECTOR,
BIT_01,
BIT_02,
BIT_04,
BIT_08,
BIT_16,
BIT_32,
BIT_FFFF,
CLASS_SELECTOR,
COMBINATOR,
DUO,
HEX,
ID_SELECTOR,
KEYS_LOGICAL,
KEYS_PS_CLASS_SUPPORTED,
NEST_SELECTOR,
NTH,
PS_CLASS_SELECTOR,
PS_ELEMENT_SELECTOR,
SELECTOR,
SYNTAX_ERR,
TYPE_SELECTOR
} from './constant.js';
const AST_SORT_ORDER = new Map([
[PS_ELEMENT_SELECTOR, BIT_01],
[ID_SELECTOR, BIT_02],
[CLASS_SELECTOR, BIT_04],
[TYPE_SELECTOR, BIT_08],
[ATTR_SELECTOR, BIT_16],
[PS_CLASS_SELECTOR, BIT_32]
]);
const KEYS_PS_NTH = new Set([
'first-child',
'first-of-type',
'last-child',
'last-of-type',
'only-child',
'only-of-type'
]);
const KEYS_PS_CLASS_STATE = new Set([
'checked',
'closed',
'disabled',
'empty',
'enabled',
'in-range',
'indeterminate',
'invalid',
'open',
'out-of-range',
'placeholder-shown',
'read-only',
'read-write',
'valid',
...KEYS_PS_NTH
]);
const KEYS_SHADOW_HOST = new Set(['host', 'host-context']);
const U_FFFD = '\uFFFD';
/* regexp */
const REG_EMPTY_PS_FUNC =
/(?<=:(?:dir|has|host(?:-context)?|is|lang|not|nth-(?:last-)?(?:child|of-type)|where))\(\s+\)/g;
const REG_SHADOW_PS_ELEMENT = /^part|slotted$/;
/**
* Unescapes a CSS selector string.
* @param {string} selector - The CSS selector to unescape.
* @returns {string} The unescaped selector string.
*/
export const unescapeSelector = (selector = '') => {
if (typeof selector === 'string' && selector.indexOf('\\', 0) >= 0) {
const arr = selector.split('\\');
const selectorItems = [arr[0]];
const l = arr.length;
for (let i = 1; i < l; i++) {
const item = arr[i];
if (item === '' && i === l - 1) {
selectorItems.push(U_FFFD);
} else if (item === '') {
// Empty segment at non-last position means \\ (escaped backslash)
selectorItems.push('\\');
// skip the next segment which is the remainder after \\
i++;
if (i < l) {
selectorItems.push(arr[i]);
}
} else {
const hexExists = /^([\da-f]{1,6}\s?)/i.exec(item);
if (hexExists) {
const [, hex] = hexExists;
let str;
try {
const low = parseInt('D800', HEX);
const high = parseInt('DFFF', HEX);
const deci = parseInt(hex, HEX);
if (deci === 0 || (deci >= low && deci <= high)) {
str = U_FFFD;
} else {
str = String.fromCodePoint(deci);
}
} catch (e) {
str = U_FFFD;
}
let postStr = '';
if (item.length > hex.length) {
postStr = item.substring(hex.length);
}
selectorItems.push(`${str}${postStr}`);
} else if (/^[\n\r\f]/.test(item)) {
// whitespace
selectorItems.push(`\\${item}`);
} else {
selectorItems.push(item);
}
}
}
return selectorItems.join('');
}
return selector;
};
/**
* Preprocesses a selector string according to the specification.
* @see https://drafts.csswg.org/css-syntax-3/#input-preprocessing
* @param {string} value - The value to preprocess.
* @returns {string} The preprocessed selector string.
*/
export const preprocess = value => {
// Non-string values will be converted to string.
if (typeof value !== 'string') {
if (value === undefined || value === null) {
return getType(value).toLowerCase();
} else if (Array.isArray(value)) {
return value.join(',');
} else if (Object.hasOwn(value, 'toString')) {
return value.toString();
} else {
throw new DOMException(`Invalid selector ${value}`, SYNTAX_ERR);
}
}
let selector = value;
let index = 0;
while (index >= 0) {
// @see https://drafts.csswg.org/selectors/#id-selectors
index = selector.indexOf('#', index);
if (index < 0) {
break;
}
const preHash = selector.substring(0, index + 1);
let postHash = selector.substring(index + 1);
const codePoint = postHash.codePointAt(0);
if (codePoint > BIT_FFFF) {
const str = `\\${codePoint.toString(HEX)} `;
if (postHash.length === DUO) {
postHash = str;
} else {
postHash = `${str}${postHash.substring(DUO)}`;
}
}
selector = `${preHash}${postHash}`;
index++;
}
selector = selector
.replace(/\f|\r\n?/g, '\n')
.replace(/[\0\uD800-\uDFFF]|\\$/g, U_FFFD);
if (selector === '&') {
return '';
}
return selector;
};
/**
* Creates an Abstract Syntax Tree (AST) from a CSS selector string.
* @param {string} sel - The CSS selector string.
* @param {string} context - Parse what part of CSS.
* @returns {object} The parsed AST object.
*/
export const parseSelector = (sel, context = 'selectorList') => {
const selector = preprocess(sel);
// invalid selectors
if (/^$|^\s*>|,\s*$/.test(selector)) {
throw new DOMException(`Invalid selector ${selector}`, SYNTAX_ERR);
}
try {
return cssTree.parse(selector, { context });
} catch (e) {
const { message } = e;
if (
/^(?:"\]"|Attribute selector [()\s,=~^$*|]+) is expected$/.test(
message
) &&
!selector.endsWith(']')
) {
const index = selector.lastIndexOf('[');
const selPart = selector.substring(index);
if (selPart.includes('"')) {
const quotes = selPart.match(/"/g).length;
if (quotes % 2) {
return parseSelector(`${selector}"]`);
}
return parseSelector(`${selector}]`);
}
return parseSelector(`${selector}]`);
} else if (message === '")" is expected') {
// workaround for https://github.com/csstree/csstree/issues/283
if (REG_EMPTY_PS_FUNC.test(selector)) {
return parseSelector(`${selector.replaceAll(REG_EMPTY_PS_FUNC, '()')}`);
} else if (!selector.endsWith(')')) {
return parseSelector(`${selector})`);
} else {
throw new DOMException(`Invalid selector ${selector}`, SYNTAX_ERR);
}
} else {
throw new DOMException(`Invalid selector ${selector}`, SYNTAX_ERR);
}
}
};
/**
* Walks the provided AST to collect selector branches and gather information
* about its contents.
* @param {object} ast - The AST to traverse.
* @param {boolean} toObject - True if converts ast to object, false otherwise.
* @param {function(object): void} [callback] - Optional callback for each node.
* @returns {{branches: Array<object>, info: object}} An object containing the selector branches and info.
*/
export const walkAST = (ast = {}, toObject = false, callback = null) => {
const branches = new Set();
const info = {
hasForgivenPseudoFunc: false,
hasHasPseudoFunc: false,
hasLogicalPseudoFunc: false,
hasNestedSelector: false,
hasNestingSelector: false,
hasNotPseudoFunc: false,
hasNthChildOfSelector: false,
hasStatePseudoClass: false,
hasUnsupportedPseudoClass: false
};
const opt = {
enter(node) {
if (typeof callback === 'function') {
callback(node);
}
switch (node.type) {
case CLASS_SELECTOR: {
if (/^-?\d/.test(node.name)) {
throw new DOMException(
`Invalid selector .${node.name}`,
SYNTAX_ERR
);
}
break;
}
case ID_SELECTOR: {
if (/^-?\d/.test(node.name)) {
throw new DOMException(
`Invalid selector #${node.name}`,
SYNTAX_ERR
);
}
break;
}
case PS_CLASS_SELECTOR: {
const name = node.name.toLowerCase();
if (KEYS_LOGICAL.has(name)) {
info.hasNestedSelector = true;
info.hasLogicalPseudoFunc = true;
if (name === 'has') {
info.hasHasPseudoFunc = true;
} else if (name === 'not') {
info.hasNotPseudoFunc = true;
} else {
info.hasForgivenPseudoFunc = true;
}
} else if (KEYS_PS_CLASS_STATE.has(name)) {
info.hasStatePseudoClass = true;
} else if (
KEYS_SHADOW_HOST.has(name) &&
Array.isArray(node.children) &&
node.children.length
) {
info.hasNestedSelector = true;
} else if (!KEYS_PS_CLASS_SUPPORTED.has(name)) {
info.hasUnsupportedPseudoClass = true;
}
break;
}
case PS_ELEMENT_SELECTOR: {
if (REG_SHADOW_PS_ELEMENT.test(node.name)) {
info.hasNestedSelector = true;
}
break;
}
case NTH: {
if (node.selector) {
info.hasNestedSelector = true;
info.hasNthChildOfSelector = true;
}
break;
}
case SELECTOR: {
branches.add(node.children);
break;
}
case NEST_SELECTOR: {
info.hasNestingSelector = true;
break;
}
default:
}
}
};
const clonedAst = cssTree.clone(ast);
cssTree.walk(toObject ? cssTree.toPlainObject(clonedAst) : clonedAst, opt);
if (info.hasNestedSelector === true) {
cssTree.findAll(clonedAst, (node, item, list) => {
if (list) {
if (node.type === PS_CLASS_SELECTOR && KEYS_LOGICAL.has(node.name)) {
const itemList = list.filter(i => {
const { name, type } = i;
return type === PS_CLASS_SELECTOR && KEYS_LOGICAL.has(name);
});
for (const { children } of itemList) {
// SelectorList
for (const { children: grandChildren } of children) {
// Selector
for (const { children: greatGrandChildren } of grandChildren) {
if (branches.has(greatGrandChildren)) {
branches.delete(greatGrandChildren);
}
}
}
}
} else if (
node.type === PS_CLASS_SELECTOR &&
KEYS_SHADOW_HOST.has(node.name) &&
Array.isArray(node.children) &&
node.children.length
) {
const itemList = list.filter(i => {
const { children, name, type } = i;
const res =
type === PS_CLASS_SELECTOR &&
KEYS_SHADOW_HOST.has(name) &&
Array.isArray(children) &&
children.length;
return res;
});
for (const { children } of itemList) {
// Selector
for (const { children: grandChildren } of children) {
if (branches.has(grandChildren)) {
branches.delete(grandChildren);
}
}
}
} else if (
node.type === PS_ELEMENT_SELECTOR &&
REG_SHADOW_PS_ELEMENT.test(node.name)
) {
const itemList = list.filter(i => {
const { name, type } = i;
const res =
type === PS_ELEMENT_SELECTOR && REG_SHADOW_PS_ELEMENT.test(name);
return res;
});
for (const { children } of itemList) {
// Selector
for (const { children: grandChildren } of children) {
if (branches.has(grandChildren)) {
branches.delete(grandChildren);
}
}
}
} else if (node.type === NTH && node.selector) {
const itemList = list.filter(i => {
const { selector, type } = i;
const res = type === NTH && selector;
return res;
});
for (const { selector } of itemList) {
const { children } = selector;
// Selector
for (const { children: grandChildren } of children) {
if (branches.has(grandChildren)) {
branches.delete(grandChildren);
}
}
}
}
}
});
}
return {
info,
branches: [...branches]
};
};
/**
* Comparison function for sorting AST nodes based on specificity.
* @param {object} a - The first AST node.
* @param {object} b - The second AST node.
* @returns {number} -1, 0 or 1, depending on the sort order.
*/
export const compareASTNodes = (a, b) => {
const bitA = AST_SORT_ORDER.get(a.type);
const bitB = AST_SORT_ORDER.get(b.type);
if (bitA === bitB) {
return 0;
} else if (bitA > bitB) {
return 1;
} else {
return -1;
}
};
/**
* Sorts a collection of AST nodes based on CSS specificity rules.
* @param {Array<object>} asts - A collection of AST nodes to sort.
* @returns {Array<object>} A new array containing the sorted AST nodes.
*/
export const sortAST = asts => {
const arr = [...asts];
if (arr.length > 1) {
arr.sort(compareASTNodes);
}
return arr;
};
/**
* Parses a type selector's name, which may include a namespace prefix.
* @param {string} selector - The type selector name (e.g., 'ns|E' or 'E').
* @returns {{prefix: string, localName: string}} An object with `prefix` and
* `localName` properties.
*/
export const parseAstName = selector => {
let prefix;
let localName;
if (selector && typeof selector === 'string') {
if (selector.indexOf('|') > -1) {
[prefix, localName] = selector.split('|');
} else {
prefix = '*';
localName = selector;
}
} else {
throw new DOMException(`Invalid selector ${selector}`, SYNTAX_ERR);
}
return {
prefix,
localName
};
};
/**
* Extracts the rightmost subject keys (id, class, tag) from a CSS selector AST.
* @param {object} ast - The AST representation of the CSS selector.
* @returns {Array<{id: string|null, className: string|null, tag: string|null}>} The list of extracted keys for each selector group.
*/
export const extractSubjectsAst = ast => {
const subjects = [];
if (ast?.type === 'SelectorList') {
for (const selectorNode of ast.children) {
let idKey = null;
let classKey = null;
let tagKey = null;
let current = selectorNode.children.tail;
while (current) {
const node = current.data;
if (node.type === COMBINATOR) {
break;
}
if (node.type === ID_SELECTOR) {
idKey = idKey ?? unescapeSelector(node.name);
} else if (node.type === CLASS_SELECTOR) {
classKey = classKey ?? unescapeSelector(node.name);
} else if (node.type === TYPE_SELECTOR) {
const { localName } = parseAstName(unescapeSelector(node.name));
if (localName !== '*') {
tagKey = tagKey ?? localName.toLowerCase();
}
}
current = current.prev;
}
subjects.push({ id: idKey, className: classKey, tag: tagKey });
}
}
return subjects;
};
/* Re-exported from css-tree. */
export { find as findAST, generate as generateCSS } from 'css-tree';
+86
View File
@@ -0,0 +1,86 @@
/**
* processor.js
*/
/* import */
import { sortAST, unescapeSelector } from './parser.js';
import { isInvalidCombinator } from './selector.js';
import { generateException } from './utility.js';
/* constants */
import { COMBINATOR, SYNTAX_ERR } from './constant.js';
/**
* SelectorProcessor
* Processes raw selector branches into an internal AST format.
*/
export class SelectorProcessor {
#context;
/**
* @param {import('./finder.js').Finder} context - The Finder instance.
*/
constructor(context) {
this.#context = context;
}
/**
* Processes selector branches into the internal AST.
* @param {Array.<object>} branches - The raw selector branches.
* @param {string} selector - The original CSS selector string.
* @returns {object} An object containing the ast and a descendant flag.
*/
process(branches, selector) {
let descendant = false;
const ast = [];
for (const items of branches) {
const branch = [];
let prevType = null;
const itemsLen = items.length;
if (itemsLen) {
const leaves = new Set();
for (let j = 0; j < itemsLen; j++) {
const item = items[j];
const isLast = j === itemsLen - 1;
// Validate combinator syntax.
if (isInvalidCombinator(item.type, prevType, isLast)) {
const msg = `Invalid selector ${selector}`;
this.#context.onError(
generateException(msg, SYNTAX_ERR, this.#context.window)
);
return { ast: [], descendant: false, invalidate: false };
}
if (item.type === COMBINATOR) {
// Flag if the branch contains descendant or child combinators.
if (item.name === ' ' || item.name === '>') {
descendant = true;
}
branch.push({ combo: item, leaves: sortAST(leaves) });
leaves.clear();
} else {
// Unescape selector names and detect namespaces.
if (item.name && typeof item.name === 'string') {
const unescapedName = unescapeSelector(item.name);
if (unescapedName !== item.name) {
item.name = unescapedName;
}
if (/[|:]/.test(unescapedName)) {
item.namespace = true;
}
}
leaves.add(item);
}
prevType = item.type;
// Handle the trailing items after the loop finishes.
if (isLast) {
branch.push({ combo: null, leaves: sortAST(leaves) });
leaves.clear();
}
}
}
// Initialize the structure required for node collection.
ast.push({ branch, dir: null, filtered: false, find: false });
}
return { ast, descendant };
}
}
+334
View File
@@ -0,0 +1,334 @@
/**
* selector.js
*/
/* import */
import * as cssTree from 'css-tree';
import { generateException } from './utility.js';
/* constants */
import {
ATTR_TYPE,
COMBINATOR,
COMBO,
COMPOUND_I,
HAS_COMPOUND,
KEYS_LOGICAL,
KEYS_PS_CLASS_SUPPORTED,
LOGIC_COMPLEX,
LOGIC_COMPOUND,
N_TH,
PSEUDO_CLASS,
PS_CLASS_SELECTOR,
PS_ELEMENT_SELECTOR,
SELECTOR,
SYNTAX_ERR,
TAG_TYPE_WO_UNIVERSAL,
TARGET_ALL
} from './constant.js';
/* regexp */
const REG_EXCLUDE_BASIC =
/[|\\]|::|[^\u0021-\u007F\s]|\[\s*[\w$*=^|~-]+(?:(?:"[\w$*=^|~\s'-]+"|'[\w$*=^|~\s"-]+')?(?:\s+[\w$*=^|~-]+)+|"[^"\]]{1,255}|'[^'\]]{1,255})\s*\]|:(?:is|where)\(\s*\)|\[\s*data-/;
const REG_EXCLUDE_QSA = new RegExp(
`(?:^(?:[A-Z]|\\.)[\\w-]*$|${COMPOUND_I}${COMBO}${COMPOUND_I})`,
'i'
);
const REG_COMPLEX = new RegExp(`${COMPOUND_I}${COMBO}${COMPOUND_I}`, 'i');
const REG_LOGIC_COMPLEX = new RegExp(
`:(?!${PSEUDO_CLASS}|${N_TH}|${LOGIC_COMPLEX})`
);
const REG_LOGIC_COMPOUND = new RegExp(
`:(?!${PSEUDO_CLASS}|${N_TH}|${LOGIC_COMPOUND})`
);
const REG_LOGIC_HAS_COMPOUND = new RegExp(
`:(?!${PSEUDO_CLASS}|${N_TH}|${LOGIC_COMPOUND}|${HAS_COMPOUND})`
);
const REG_END_WITH_HAS = new RegExp(`:${HAS_COMPOUND}$`);
const REG_WO_LOGICAL = new RegExp(`:(?!${PSEUDO_CLASS}|${N_TH})`);
const REG_COMBO = new RegExp(COMBO);
const REG_ID = /#(\D[^#.*]+)/g;
const REG_CLASS = /\.(\D[^#.*]+)/g;
const REG_TAG = /^([^#.]+)/;
const REG_INVALID_SYNTAX =
/[+~>]\s*[+~>]|^\s*[+~>]|[+~>]\s*$|^\s*,|,\s*,|,\s*$/;
const REG_TEST_LIB = new RegExp(
`^(?:[*]?${ATTR_TYPE}(?:\\s*,\\s*${TAG_TYPE_WO_UNIVERSAL}${COMBO}${TAG_TYPE_WO_UNIVERSAL})?)$`
);
/**
* Find a nested :has() pseudo-class.
* @param {object} leaf - The AST leaf to check.
* @returns {?object} The leaf if it's :has, otherwise null.
*/
export const findNestedHas = leaf => leaf.name === 'has';
/**
* Find a logical pseudo-class that contains a nested :has().
* @param {object} leaf - The AST leaf to check.
* @returns {?object} The leaf if it matches, otherwise null.
*/
export const findLogicalWithNestedHas = leaf => {
if (KEYS_LOGICAL.has(leaf.name) && cssTree.find(leaf, findNestedHas)) {
return leaf;
}
return null;
};
/**
* Validates nesting restrictions within :has() arguments.
* @param {Array<object>} astChildren - The AST nodes representing the :has() arguments.
* @returns {boolean} False if there's an invalid nesting constraint violation.
*/
export const validateHasNesting = astChildren => {
const l = astChildren.length;
for (let i = 0; i < l; i++) {
const item = cssTree.find(astChildren[i], findLogicalWithNestedHas);
if (item) {
// If nested :has() is wrapped inside :is() or :where(), it is forgiven.
if (item.name !== 'is' && item.name !== 'where') {
return false;
}
}
}
return true;
};
/**
* Creates a callback function to validate :has() nesting during AST walk.
* @param {object} globalObj - The global window object.
* @returns {function(object): void} The callback function for walkAST.
*/
export const createHasValidator = globalObj => node => {
if (
node.type === PS_CLASS_SELECTOR &&
node.name.toLowerCase() === 'has' &&
!validateHasNesting(Array.from(node.children || []))
) {
const css = cssTree.generate(node);
throw generateException(
`Disallowed nested :has() pseudo-class: ${css}`,
SYNTAX_ERR,
globalObj
);
}
};
/**
* Check if a combinator node is invalid (leading, trailing, or consecutive).
* @param {string} type - The current node type.
* @param {string|null} prevType - The previous node type.
* @param {boolean} isLast - Whether the current node is the last in the list.
* @returns {boolean} True if the combinator is invalid.
*/
export const isInvalidCombinator = (type, prevType, isLast) =>
type === COMBINATOR &&
(prevType === null || prevType === COMBINATOR || isLast);
/**
* Checks if a given AST is supported by the DOMSelector engine.
* @param {object} ast - The AST to validate.
* @returns {boolean} True if the selector is fully supported.
*/
export const isSupportedAST = ast => {
let isSupported = true;
const walk = (
node,
context = { insideHas: false, insideForgiving: false }
) => {
if (!isSupported || !node) {
return;
}
const nextContext = { ...context };
if (node.type === PS_ELEMENT_SELECTOR) {
isSupported = false;
return;
} else if (node.type === PS_CLASS_SELECTOR) {
let name = node.name;
if (name && typeof name === 'string') {
name = name.toLowerCase();
}
if (!KEYS_PS_CLASS_SUPPORTED.has(name)) {
isSupported = false;
return;
}
if (name === 'has') {
if (context.insideHas && !context.insideForgiving) {
isSupported = false;
return;
}
nextContext.insideHas = true;
} else if (name === 'is' || name === 'where') {
nextContext.insideForgiving = true;
}
}
if (node.children) {
let prevType = null;
if (node.children.head !== undefined) {
let current = node.children.head;
while (current) {
if (!current.data) {
current = current.next;
continue;
}
const childType = current.data.type;
if (
node.type === SELECTOR &&
isInvalidCombinator(childType, prevType, !current.next)
) {
if (!(prevType === null && context.insideHas)) {
isSupported = false;
return;
}
}
prevType = childType;
walk(current.data, nextContext);
if (!isSupported) {
return;
}
current = current.next;
}
} else if (Array.isArray(node.children)) {
const l = node.children.length;
for (let i = 0; i < l; i++) {
const child = node.children[i];
if (!child) {
continue;
}
const childType = child.type;
if (
node.type === SELECTOR &&
isInvalidCombinator(childType, prevType, i === l - 1)
) {
if (!(prevType === null && context.insideHas)) {
isSupported = false;
return;
}
}
prevType = childType;
walk(child, nextContext);
if (!isSupported) {
return;
}
}
}
}
if (node.selector) {
walk(node.selector, nextContext);
}
};
walk(ast);
return isSupported;
};
/**
* Extracts the rightmost subject keys (id, class, tag) from a selector.
* @param {string} selector - The CSS selector string to parse.
* @param {boolean} caseSensitive - True if the tag should be case-sensitive.
* @returns {Array<{id: string|null, className: string|null, tag: string|null}>} The list of extracted keys for each selector group.
*/
export const extractSubjectsRegExp = (selector, caseSensitive) => {
const subjects = [];
const groups = selector.split(',');
for (let i = 0; i < groups.length; i++) {
const group = groups[i].trim();
if (!group) {
continue;
}
const compounds = group.split(REG_COMBO);
const rightmost = compounds[compounds.length - 1];
let idKey = null;
let classKey = null;
let tagKey = null;
if (rightmost) {
const idMatch = rightmost.match(REG_ID);
if (idMatch) {
idKey = idMatch[idMatch.length - 1].slice(1);
}
const classMatch = rightmost.match(REG_CLASS);
if (classMatch) {
classKey = classMatch[classMatch.length - 1].slice(1);
}
const tagMatch = rightmost.match(REG_TAG);
if (tagMatch) {
const tag = tagMatch[1];
if (tag !== '*') {
tagKey = caseSensitive ? tag : tag.toLowerCase();
}
}
}
subjects.push({ id: idKey, className: classKey, tag: tagKey });
}
return subjects;
};
/**
* Filter a selector for use with nwsapi.
* @param {string} selector - The selector string.
* @param {string} target - The target type.
* @returns {boolean} - True if the selector is valid for nwsapi.
*/
export const filterSelector = (selector, target) => {
// Basic validation and fast-fail for null/undefined/non-string values.
if (
!selector ||
typeof selector !== 'string' ||
/null|undefined/.test(selector)
) {
return false;
}
// Validate syntax.
if (REG_INVALID_SYNTAX.test(selector)) {
return false;
}
// Target-specific early exits.
if (
target === TARGET_ALL &&
REG_EXCLUDE_QSA.test(selector) &&
!REG_TEST_LIB.test(selector)
) {
return false;
}
// Exclude various complex or unsupported selectors early.
// i.e. non-ASCII, escaped selectors, namespaced selectors, pseudo-elements.
if (
selector.includes('/') ||
selector.includes('&') ||
REG_EXCLUDE_BASIC.test(selector)
) {
return false;
}
// Validate attribute selector integrity.
if (selector.includes('[')) {
const index = selector.lastIndexOf('[');
if (selector.indexOf(']', index) === -1) {
return false;
}
}
// Logic for pseudo-classes.
if (selector.includes(':')) {
// Determine if the selector has complex logical structures.
const isComplex =
target === TARGET_ALL ? false : REG_COMPLEX.test(selector);
// Handle :has() specifically.
if (selector.includes(':has(')) {
if (!isComplex || REG_LOGIC_HAS_COMPOUND.test(selector)) {
return false;
}
return REG_END_WITH_HAS.test(selector);
}
// Handle :is() and :not().
if (/(?:is|not)\(/.test(selector)) {
if (isComplex) {
return !REG_LOGIC_COMPLEX.test(selector);
} else {
return !REG_LOGIC_COMPOUND.test(selector);
}
}
// Default check for other pseudo-classes against known list.
if (REG_WO_LOGICAL.test(selector)) {
return false;
}
}
return true;
};
+949
View File
@@ -0,0 +1,949 @@
/**
* utility.js
*/
/* import */
import bidiFactory from 'bidi-js';
import isCustomElementName from 'is-potential-custom-element-name';
/* constants */
import {
CLASS_SELECTOR,
DIR_NEXT,
DIR_PREV,
DOCUMENT_FRAGMENT_NODE,
DOCUMENT_NODE,
DOCUMENT_POSITION_CONTAINS,
DOCUMENT_POSITION_PRECEDING,
ELEMENT_NODE,
ID_SELECTOR,
INPUT_BUTTON,
INPUT_EDIT,
INPUT_LTR,
INPUT_TEXT,
PS_ELEMENT_SELECTOR,
SHOW_ELEMENT,
TARGET_FIRST,
TEXT_NODE,
TYPE_FROM,
TYPE_SELECTOR,
TYPE_TO
} from './constant.js';
const KEYS_DIR_AUTO = new Set([...INPUT_BUTTON, ...INPUT_TEXT, 'hidden']);
const KEYS_DIR_LTR = new Set(INPUT_LTR);
const KEYS_INPUT_EDIT = new Set(INPUT_EDIT);
const KEYS_NODE_DIR_EXCLUDE = new Set(['bdi', 'script', 'style', 'textarea']);
const KEYS_NODE_FOCUSABLE = new Set(['button', 'select', 'textarea']);
const KEYS_NODE_FOCUSABLE_SVG = new Set([
'clipPath',
'defs',
'desc',
'linearGradient',
'marker',
'mask',
'metadata',
'pattern',
'radialGradient',
'script',
'style',
'symbol',
'title'
]);
/* regexp */
const REG_IS_HTML = /^(?:application\/xhtml\+x|text\/ht)ml$/;
const REG_IS_XML =
/^(?:application\/(?:[\w\-.]+\+)?|image\/[\w\-.]+\+|text\/)xml$/;
/**
* Get type of an object.
* @param {object} o - Object to check.
* @returns {string} - Type of the object.
*/
export const getType = o =>
Object.prototype.toString.call(o).slice(TYPE_FROM, TYPE_TO);
/**
* Verify array contents.
* @param {Array} arr - The array.
* @param {string} type - Expected type, e.g. 'String'.
* @throws {TypeError} - Throws if array or its items are of unexpected type.
* @returns {Array} - The verified array.
*/
export const verifyArray = (arr, type) => {
if (!Array.isArray(arr)) {
throw new TypeError(`Unexpected type ${getType(arr)}`);
}
if (typeof type !== 'string') {
throw new TypeError(`Unexpected type ${getType(type)}`);
}
for (const item of arr) {
if (getType(item) !== type) {
throw new TypeError(`Unexpected type ${getType(item)}`);
}
}
return arr;
};
/**
* Generate a DOMException.
* @param {string} msg - The error message.
* @param {string} name - The error name.
* @param {object} globalObject - The global object (e.g., window).
* @returns {DOMException} The generated DOMException object.
*/
export const generateException = (msg, name, globalObject = globalThis) => {
return new globalObject.DOMException(msg, name);
};
/**
* Resolve content document, root node, and check if it's in a shadow DOM.
* @param {object} node - Document, DocumentFragment, or Element node.
* @returns {Array.<object|boolean>} - [document, root, isInShadow].
*/
export const resolveContent = node => {
if (!node?.nodeType) {
throw new TypeError(`Unexpected type ${getType(node)}`);
}
let document;
let root;
let shadow;
switch (node.nodeType) {
case DOCUMENT_NODE: {
document = node;
root = node;
break;
}
case DOCUMENT_FRAGMENT_NODE: {
const { host, mode, ownerDocument } = node;
document = ownerDocument;
root = node;
shadow = host && (mode === 'close' || mode === 'open');
break;
}
case ELEMENT_NODE: {
document = node.ownerDocument;
let refNode = node;
while (refNode) {
const { host, mode, nodeType, parentNode } = refNode;
if (nodeType === DOCUMENT_FRAGMENT_NODE) {
shadow = host && (mode === 'close' || mode === 'open');
break;
} else if (parentNode) {
refNode = parentNode;
} else {
break;
}
}
root = refNode;
break;
}
default: {
throw new TypeError(`Unexpected node ${node.nodeName}`);
}
}
return [document, root, !!shadow];
};
/**
* Traverse node tree with a TreeWalker.
* @param {object} node - The target node.
* @param {object} walker - The TreeWalker instance.
* @param {boolean} [force] - Traverse only to the next node.
* @returns {?object} - The current node if found, otherwise null.
*/
export const traverseNode = (node, walker, force = false) => {
if (!node?.nodeType) {
throw new TypeError(`Unexpected type ${getType(node)}`);
}
if (!walker) {
return null;
}
let refNode = walker.currentNode;
if (refNode === node) {
return refNode;
} else if (force || refNode.contains(node)) {
refNode = walker.nextNode();
while (refNode) {
if (refNode === node) {
break;
}
refNode = walker.nextNode();
}
return refNode;
} else {
if (refNode !== walker.root) {
let bool;
while (refNode) {
if (refNode === node) {
bool = true;
break;
} else if (refNode === walker.root || refNode.contains(node)) {
break;
}
refNode = walker.parentNode();
}
if (bool) {
return refNode;
}
}
if (node.nodeType === ELEMENT_NODE) {
let bool;
while (refNode) {
if (refNode === node) {
bool = true;
break;
}
refNode = walker.nextNode();
}
if (bool) {
return refNode;
}
}
}
return null;
};
/**
* Check if a node is a custom element.
* @param {object} node - The Element node.
* @param {object} [opt] - Options.
* @param {boolean} [opt.formAssociated] - True if the node is form associated.
* @returns {boolean} - True if it's a custom element.
*/
export const isCustomElement = (node, { formAssociated } = {}) => {
if (!node?.nodeType) {
throw new TypeError(`Unexpected type ${getType(node)}`);
}
if (node.nodeType !== ELEMENT_NODE) {
return false;
}
const { localName, ownerDocument } = node;
const window = ownerDocument.defaultView;
let elmConstructor;
const attr = node.getAttribute('is');
if (attr) {
elmConstructor =
isCustomElementName(attr) && window.customElements.get(attr);
} else {
elmConstructor =
isCustomElementName(localName) && window.customElements.get(localName);
}
if (elmConstructor) {
if (formAssociated) {
return !!elmConstructor.formAssociated;
}
return true;
}
return false;
};
/**
* Get slotted text content.
* @param {object} node - The Element node (likely a <slot>).
* @returns {?string} - The text content.
*/
export const getSlottedTextContent = node => {
if (!node?.nodeType) {
throw new TypeError(`Unexpected type ${getType(node)}`);
}
if (typeof node.assignedNodes !== 'function') {
return null;
}
const nodes = node.assignedNodes();
if (nodes.length) {
let text = '';
const l = nodes.length;
for (let i = 0; i < l; i++) {
const item = nodes[i];
text = item.textContent.trim();
if (text) {
break;
}
}
return text;
}
return node.textContent.trim();
};
/**
* Get directionality of a node.
* @see https://html.spec.whatwg.org/multipage/dom.html#the-dir-attribute
* @param {object} node - The Element node.
* @param {WeakMap} [dirCache] - Cache for directionality.
* @returns {?string} - 'ltr' or 'rtl'.
*/
export const getDirectionality = (node, dirCache = new WeakMap()) => {
if (!node?.nodeType) {
throw new TypeError(`Unexpected type ${getType(node)}`);
}
if (node.nodeType !== ELEMENT_NODE) {
return null;
}
if (dirCache.has(node)) {
return dirCache.get(node);
}
const { dir: dirAttr, localName, parentNode } = node;
const { getEmbeddingLevels } = bidiFactory();
let result = 'ltr';
if (dirAttr === 'ltr' || dirAttr === 'rtl') {
result = dirAttr;
} else if (dirAttr === 'auto') {
let text = '';
switch (localName) {
case 'input': {
if (!node.type || KEYS_DIR_AUTO.has(node.type)) {
text = node.value;
} else if (KEYS_DIR_LTR.has(node.type)) {
result = 'ltr';
text = null; // Flag to skip text evaluation
}
break;
}
case 'slot': {
text = getSlottedTextContent(node);
break;
}
case 'textarea': {
text = node.value;
break;
}
default: {
const items = [].slice.call(node.childNodes);
for (const item of items) {
const {
dir: itemDir,
localName: itemLocalName,
nodeType: itemNodeType,
textContent: itemTextContent
} = item;
if (itemNodeType === TEXT_NODE) {
text = itemTextContent.trim();
} else if (
itemNodeType === ELEMENT_NODE &&
!KEYS_NODE_DIR_EXCLUDE.has(itemLocalName) &&
(!itemDir || (itemDir !== 'ltr' && itemDir !== 'rtl'))
) {
if (itemLocalName === 'slot') {
text = getSlottedTextContent(item);
} else {
text = itemTextContent.trim();
}
}
if (text) {
break;
}
}
}
}
if (text !== null) {
if (text) {
const {
paragraphs: [{ level }]
} = getEmbeddingLevels(text);
if (level % 2 === 1) {
result = 'rtl';
}
} else if (parentNode) {
const { nodeType: parentNodeType } = parentNode;
if (parentNodeType === ELEMENT_NODE) {
result = getDirectionality(parentNode, dirCache);
}
}
}
} else if (localName === 'input' && node.type === 'tel') {
result = 'ltr';
} else if (localName === 'bdi') {
const text = node.textContent.trim();
if (text) {
const {
paragraphs: [{ level }]
} = getEmbeddingLevels(text);
if (level % 2 === 1) {
result = 'rtl';
}
}
} else if (parentNode) {
if (localName === 'slot') {
const text = getSlottedTextContent(node);
if (text) {
const {
paragraphs: [{ level }]
} = getEmbeddingLevels(text);
if (level % 2 === 1) {
result = 'rtl';
} else {
result = 'ltr';
}
} else {
const { nodeType: parentNodeType } = parentNode;
if (parentNodeType === ELEMENT_NODE) {
result = getDirectionality(parentNode, dirCache);
}
}
} else {
const { nodeType: parentNodeType } = parentNode;
if (parentNodeType === ELEMENT_NODE) {
result = getDirectionality(parentNode, dirCache);
}
}
}
dirCache.set(node, result);
return result;
};
/**
* Get language attribute of a node.
* @param {object} node - The Element node.
* @param {WeakMap} [langCache] - Cache for language attributes.
* @returns {?string} - Language attribute value.
*/
export const getLanguageAttribute = (node, langCache = new WeakMap()) => {
if (!node?.nodeType) {
throw new TypeError(`Unexpected type ${getType(node)}`);
}
if (node.nodeType !== ELEMENT_NODE) {
return null;
}
if (langCache.has(node)) {
return langCache.get(node);
}
const { contentType } = node.ownerDocument;
const isHtml = REG_IS_HTML.test(contentType);
const isXml = REG_IS_XML.test(contentType);
let isShadow = false;
let result;
const visited = [];
// Traverse up from the current node to the root.
let current = node;
while (current) {
if (current.nodeType === ELEMENT_NODE && langCache.has(current)) {
result = langCache.get(current);
break;
}
if (current.nodeType === ELEMENT_NODE) {
visited.push(current);
}
// Check if the current node is an element.
switch (current.nodeType) {
case ELEMENT_NODE: {
// Check for and return the language attribute if present.
if (isHtml && current.hasAttribute('lang')) {
result = current.getAttribute('lang');
} else if (isXml && current.hasAttribute('xml:lang')) {
result = current.getAttribute('xml:lang');
}
break;
}
case DOCUMENT_FRAGMENT_NODE: {
// Continue traversal if the current node is a shadow root.
if (current.host) {
isShadow = true;
}
break;
}
case DOCUMENT_NODE:
default: {
// Stop if we reach the root document node.
result = null;
}
}
if (result !== undefined) {
break;
}
if (isShadow) {
current = current.host;
isShadow = false;
} else if (current.parentNode) {
current = current.parentNode;
} else {
break;
}
}
if (result === undefined) {
result = null;
}
for (const visitedNode of visited) {
langCache.set(visitedNode, result);
}
return result;
};
/**
* Check if content is editable.
* NOTE: Not implemented in jsdom https://github.com/jsdom/jsdom/issues/1670
* @param {object} node - The Element node.
* @returns {boolean} - True if content is editable.
*/
export const isContentEditable = node => {
if (!node?.nodeType) {
throw new TypeError(`Unexpected type ${getType(node)}`);
}
if (node.nodeType !== ELEMENT_NODE) {
return false;
}
if (typeof node.isContentEditable === 'boolean') {
return node.isContentEditable;
} else if (node.ownerDocument.designMode === 'on') {
return true;
} else {
let attr;
if (node.hasAttribute('contenteditable')) {
attr = node.getAttribute('contenteditable');
} else {
attr = 'inherit';
}
switch (attr) {
case '':
case 'true': {
return true;
}
case 'plaintext-only': {
// FIXME:
// @see https://github.com/w3c/editing/issues/470
// @see https://github.com/whatwg/html/issues/10651
return true;
}
case 'false': {
return false;
}
default: {
if (node?.parentNode?.nodeType === ELEMENT_NODE) {
return isContentEditable(node.parentNode);
}
return false;
}
}
}
};
/**
* Check if a node is visible.
* @param {object} node - The Element node.
* @returns {boolean} - True if the node is visible.
*/
export const isVisible = node => {
if (node?.nodeType !== ELEMENT_NODE) {
return false;
}
// TODO: switch to node.checkVisibility().
const window = node.ownerDocument.defaultView;
const { display, visibility } = window.getComputedStyle(node);
return display !== 'none' && visibility === 'visible';
};
/**
* Check if focus is visible on the node.
* @param {object} node - The Element node.
* @returns {boolean} - True if focus is visible.
*/
export const isFocusVisible = node => {
if (node?.nodeType !== ELEMENT_NODE) {
return false;
}
const { localName, type } = node;
switch (localName) {
case 'input': {
if (!type || KEYS_INPUT_EDIT.has(type)) {
return true;
}
return false;
}
case 'textarea': {
return true;
}
default: {
return isContentEditable(node);
}
}
};
/**
* Check if an area is focusable.
* @param {object} node - The Element node.
* @returns {boolean} - True if the area is focusable.
*/
export const isFocusableArea = node => {
if (node?.nodeType !== ELEMENT_NODE) {
return false;
}
if (!node.isConnected) {
return false;
}
const window = node.ownerDocument.defaultView;
if (node instanceof window.HTMLElement) {
if (Number.isInteger(parseInt(node.getAttribute('tabindex')))) {
return true;
}
if (isContentEditable(node)) {
return true;
}
const { localName, parentNode } = node;
switch (localName) {
case 'a': {
if (node.href || node.hasAttribute('href')) {
return true;
}
return false;
}
case 'iframe': {
return true;
}
case 'input': {
if (
node.disabled ||
node.hasAttribute('disabled') ||
node.hidden ||
node.hasAttribute('hidden')
) {
return false;
}
return true;
}
case 'summary': {
if (parentNode.localName === 'details') {
let child = parentNode.firstElementChild;
let bool = false;
while (child) {
if (child.localName === 'summary') {
bool = child === node;
break;
}
child = child.nextElementSibling;
}
return bool;
}
return false;
}
default: {
if (
KEYS_NODE_FOCUSABLE.has(localName) &&
!(node.disabled || node.hasAttribute('disabled'))
) {
return true;
}
}
}
} else if (node instanceof window.SVGElement) {
if (Number.isInteger(parseInt(node.getAttributeNS(null, 'tabindex')))) {
const ns = 'http://www.w3.org/2000/svg';
let bool;
let refNode = node;
while (refNode.namespaceURI === ns) {
bool = KEYS_NODE_FOCUSABLE_SVG.has(refNode.localName);
if (bool) {
break;
}
if (refNode?.parentNode?.namespaceURI === ns) {
refNode = refNode.parentNode;
} else {
break;
}
}
if (bool) {
return false;
}
return true;
}
if (
node.localName === 'a' &&
(node.href || node.hasAttributeNS(null, 'href'))
) {
return true;
}
}
return false;
};
/**
* Check if a node is focusable.
* NOTE: Not applied, needs fix in jsdom itself.
* @see https://github.com/whatwg/html/pull/8392
* @see https://phabricator.services.mozilla.com/D156219
* @see https://github.com/jsdom/jsdom/issues/3029
* @see https://github.com/jsdom/jsdom/issues/3464
* @param {object} node - The Element node.
* @returns {boolean} - True if the node is focusable.
*/
export const isFocusable = node => {
if (node?.nodeType !== ELEMENT_NODE) {
return false;
}
const window = node.ownerDocument.defaultView;
let refNode = node;
let res = true;
while (refNode) {
if (refNode.disabled || refNode.hasAttribute('disabled')) {
res = false;
break;
}
if (refNode.hidden || refNode.hasAttribute('hidden')) {
res = false;
}
const { contentVisibility, display, visibility } =
window.getComputedStyle(refNode);
if (
display === 'none' ||
visibility !== 'visible' ||
(contentVisibility === 'hidden' && refNode !== node)
) {
res = false;
} else {
res = true;
}
if (res && refNode?.parentNode?.nodeType === ELEMENT_NODE) {
refNode = refNode.parentNode;
} else {
break;
}
}
return res;
};
/**
* Get namespace URI.
* @param {string} ns - The namespace prefix.
* @param {object} node - The Element node.
* @returns {?string} - The namespace URI.
*/
export const getNamespaceURI = (ns, node) => {
if (typeof ns !== 'string') {
throw new TypeError(`Unexpected type ${getType(ns)}`);
} else if (!node?.nodeType) {
throw new TypeError(`Unexpected type ${getType(node)}`);
}
if (!ns || node.nodeType !== ELEMENT_NODE) {
return null;
}
const { attributes } = node;
let res;
for (const attr of attributes) {
const { name, namespaceURI, prefix, value } = attr;
if (name === `xmlns:${ns}`) {
res = value;
} else if (prefix === ns) {
res = namespaceURI;
}
if (res) {
break;
}
}
return res ?? null;
};
/**
* Check if a namespace is declared.
* @param {string} ns - The namespace.
* @param {object} node - The Element node.
* @returns {boolean} - True if the namespace is declared.
*/
export const isNamespaceDeclared = (ns = '', node = {}) => {
if (!ns || typeof ns !== 'string' || node?.nodeType !== ELEMENT_NODE) {
return false;
}
if (node.lookupNamespaceURI(ns)) {
return true;
}
const root = node.ownerDocument.documentElement;
let parent = node;
let res;
while (parent) {
res = getNamespaceURI(ns, parent);
if (res || parent === root) {
break;
}
parent = parent.parentNode;
}
return !!res;
};
/**
* Check if nodeA precedes and/or contains nodeB.
* @param {object} nodeA - The first Element node.
* @param {object} nodeB - The second Element node.
* @returns {boolean} - True if nodeA precedes nodeB.
*/
export const isPreceding = (nodeA, nodeB) => {
if (!nodeA?.nodeType) {
throw new TypeError(`Unexpected type ${getType(nodeA)}`);
} else if (!nodeB?.nodeType) {
throw new TypeError(`Unexpected type ${getType(nodeB)}`);
}
if (nodeA.nodeType !== ELEMENT_NODE || nodeB.nodeType !== ELEMENT_NODE) {
return false;
}
const posBit = nodeB.compareDocumentPosition(nodeA);
const res =
posBit & DOCUMENT_POSITION_PRECEDING || posBit & DOCUMENT_POSITION_CONTAINS;
return !!res;
};
/**
* Comparison function for sorting nodes based on document position.
* @param {object} a - The first node.
* @param {object} b - The second node.
* @returns {number} - Sort order.
*/
export const compareNodes = (a, b) => {
if (isPreceding(b, a)) {
return 1;
}
return -1;
};
/**
* Sort a collection of nodes.
* @param {Array.<object>|Set.<object>} nodes - Collection of nodes.
* @returns {Array.<object>} - Collection of sorted nodes.
*/
export const sortNodes = (nodes = []) => {
const arr = [...nodes];
if (arr.length > 1) {
arr.sort(compareNodes);
}
return arr;
};
/**
* Traverses AST nodes to find the most optimal seed selector
* (ID > Class > Tag).
* @param {Array} nodes - AST nodes to traverse.
* @param {object} [state] - The current state of the search.
* @returns {object} The search state containing the best seed.
*/
export const findBestSeed = (nodes, state = { seed: null, priority: 0 }) => {
for (const node of nodes) {
if (state.priority === 3) {
return state;
}
if (Array.isArray(node)) {
findBestSeed(node, state);
} else if (node && typeof node === 'object') {
// ID Selector (Fastest: getElementById)
if (node.type === ID_SELECTOR) {
state.seed = { type: 'id', value: node.name };
state.priority = 3;
return state;
} else if (node.type === CLASS_SELECTOR && state.priority < 2) {
// Class Selector (Faster: getElementsByClassName)
state.seed = { type: 'class', value: node.name };
state.priority = 2;
} else if (
node.type === TYPE_SELECTOR &&
state.priority < 1 &&
node.name !== '*'
) {
// Type/Tag Selector (Excludes universal '*')
state.seed = { type: 'tag', value: node.name };
state.priority = 1;
}
if (node.children) {
findBestSeed(node.children, state);
}
}
}
return state;
};
/**
* Traces the DOM tree upwards and sideways from a seed element,
* populating the allowlist with safe paths for :has() evaluation.
* @param {object} current - The starting seed element.
* @param {WeakSet} list - The WeakSet to populate.
* @param {Set} visitedAncestors - The Set to track visited nodes.
* @returns {void}
*/
export const populateHasAllowlist = (current, list, visitedAncestors) => {
list.add(current);
while (
current &&
(current.nodeType === ELEMENT_NODE ||
current.nodeType === DOCUMENT_FRAGMENT_NODE)
) {
if (visitedAncestors.has(current)) {
break;
}
visitedAncestors.add(current);
let sibling = current.previousElementSibling;
while (sibling) {
list.add(sibling);
sibling = sibling.previousElementSibling;
}
sibling = current.nextElementSibling;
while (sibling) {
list.add(sibling);
sibling = sibling.nextElementSibling;
}
current = current.parentNode;
if (current) {
list.add(current);
}
}
};
/**
* Collects all descendant elements of a given node using a TreeWalker.
* @param {Document|DocumentFragment|Element} node - The node to start from.
* @param {Document} document - The Document used to create the TreeWalker.
* @returns {Array<Element>} An array containing all descendant elements.
*/
export const collectAllDescendants = (node, document) => {
if (!node?.nodeType) {
throw new TypeError(`Unexpected type ${getType(node)}`);
}
if (document?.nodeType !== DOCUMENT_NODE) {
throw new TypeError(`Unexpected type ${getType(document)}`);
}
const walker = document.createTreeWalker(node, SHOW_ELEMENT);
const descendants = [];
let refNode = walker.nextNode();
while (refNode) {
descendants.push(refNode);
refNode = walker.nextNode();
}
return descendants;
};
/**
* Gets the traversal direction and starting twig.
* @param {Array.<object>} branch - The selector branch.
* @param {string} targetType - The target type.
* @param {boolean} hasScope - True if selector includes ':scope'.
* @param {boolean} scoped - True if traversal is scoped within target node.
* @returns {object} Object containing dir and twig properties.
*/
export const getTraversalStrategy = (branch, targetType, hasScope, scoped) => {
const branchLen = branch.length;
const firstTwig = branch[0];
if (branchLen === 1) {
return { dir: DIR_PREV, twig: firstTwig };
}
const lastTwig = branch[branchLen - 1];
const {
leaves: [{ name: firstName, type: firstType }]
} = firstTwig;
const {
leaves: [{ name: lastName, type: lastType }]
} = lastTwig;
const lastCombo = branch[branchLen - 2].combo?.name;
if (
hasScope ||
lastType === PS_ELEMENT_SELECTOR ||
lastType === ID_SELECTOR ||
lastCombo === '+' ||
lastCombo === '~'
) {
return { dir: DIR_PREV, twig: lastTwig };
} else if (firstType === ID_SELECTOR) {
return { dir: DIR_NEXT, twig: firstTwig };
} else if (firstName === '*' && firstType === TYPE_SELECTOR) {
return { dir: DIR_PREV, twig: lastTwig };
} else if (lastName === '*' && lastType === TYPE_SELECTOR) {
return { dir: DIR_NEXT, twig: firstTwig };
} else if (branchLen === 1 || branchLen === 2) {
return { dir: DIR_PREV, twig: lastTwig };
} else if (branchLen > 2 && scoped && targetType === TARGET_FIRST) {
return { dir: DIR_PREV, twig: lastTwig };
}
return { dir: DIR_NEXT, twig: firstTwig };
};
+27
View File
@@ -0,0 +1,27 @@
/*!
* DOM Selector - A CSS selector engine.
* @license MIT
* @copyright asamuzaK (Kazz)
* @see {@link https://github.com/asamuzaK/domSelector/blob/main/LICENSE}
*/
export type CheckResult = {
match: boolean;
pseudoElement: string | null;
ast: object | null;
};
export declare class DOMSelector {
#private;
constructor(window: Window, document: Document, opt?: object);
clear: (clearAll?: boolean) => void;
extractSubjects: (selector: string, caseSensitive?: boolean) => Array<{
id: string | null;
className: string | null;
tag: string | null;
}>;
supports: (selector: string) => boolean;
check: (selector: string, node: Element, opt?: object) => CheckResult;
matches: (selector: string, node: Element, opt?: object) => boolean;
closest: (selector: string, node: Element, opt?: object) => Element | null;
querySelector: (selector: string, node: Document | DocumentFragment | Element, opt?: object) => Element | null;
querySelectorAll: (selector: string, node: Document | DocumentFragment | Element, opt?: object) => Array<Element>;
}
+89
View File
@@ -0,0 +1,89 @@
export declare const ATRULE = "Atrule";
export declare const ATTR_SELECTOR = "AttributeSelector";
export declare const CLASS_SELECTOR = "ClassSelector";
export declare const COMBINATOR = "Combinator";
export declare const DIR_NEXT = "next";
export declare const DIR_PREV = "prev";
export declare const IDENT = "Identifier";
export declare const ID_SELECTOR = "IdSelector";
export declare const NEST_SELECTOR = "NestingSelector";
export declare const NOT_SUPPORTED_ERR = "NotSupportedError";
export declare const NTH = "Nth";
export declare const OPERATOR = "Operator";
export declare const PS_CLASS_SELECTOR = "PseudoClassSelector";
export declare const PS_ELEMENT_SELECTOR = "PseudoElementSelector";
export declare const RULE = "Rule";
export declare const SCOPE = "Scope";
export declare const SELECTOR = "Selector";
export declare const SELECTOR_LIST = "SelectorList";
export declare const STRING = "String";
export declare const SYNTAX_ERR = "SyntaxError";
export declare const TARGET_ALL = "all";
export declare const TARGET_FIRST = "first";
export declare const TARGET_LINEAL = "lineal";
export declare const TARGET_SELF = "self";
export declare const TYPE_SELECTOR = "TypeSelector";
export declare const BIT_01 = 1;
export declare const BIT_02 = 2;
export declare const BIT_04 = 4;
export declare const BIT_08 = 8;
export declare const BIT_16 = 16;
export declare const BIT_32 = 32;
export declare const BIT_FFFF = 65535;
export declare const DUO = 2;
export declare const HEX = 16;
export declare const TYPE_FROM = 8;
export declare const TYPE_TO = -1;
export declare const ELEMENT_NODE = 1;
export declare const TEXT_NODE = 3;
export declare const DOCUMENT_NODE = 9;
export declare const DOCUMENT_FRAGMENT_NODE = 11;
export declare const DOCUMENT_POSITION_PRECEDING = 2;
export declare const DOCUMENT_POSITION_CONTAINS = 8;
export declare const DOCUMENT_POSITION_CONTAINED_BY = 16;
export declare const FILTER_ACCEPT = 1;
export declare const FILTER_REJECT = 2;
export declare const FILTER_SKIP = 3;
export declare const SHOW_ALL = 4294967295;
export declare const SHOW_CONTAINER = 1281;
export declare const SHOW_DOCUMENT = 256;
export declare const SHOW_DOCUMENT_FRAGMENT = 1024;
export declare const SHOW_ELEMENT = 1;
export declare const ALPHA_NUM = "[A-Z\\d]+";
export declare const CHILD_IDX = "(?:first|last|only)-(?:child|of-type)";
export declare const DIGIT = "(?:0|[1-9]\\d*)";
export declare const LANG_PART = "(?:-[A-Z\\d]+)*";
export declare const PSEUDO_CLASS = "(?:any-)?link|(?:first|last|only)-(?:child|of-type)|checked|empty|indeterminate|read-(?:only|write)|target";
export declare const ANB = "[+-]?(?:(?:0|[1-9]\\d*)n?|n)|(?:[+-]?(?:0|[1-9]\\d*))?n\\s*[+-]\\s*(?:0|[1-9]\\d*)";
export declare const COMBO = "\\s?[\\s>~+]\\s?";
export declare const DESCEND = "\\s?[\\s>]\\s?";
export declare const SIBLING = "\\s?[+~]\\s?";
export declare const LOGIC_IS = ":is\\(\\s*[^)]+\\s*\\)";
export declare const N_TH = "nth-(?:last-)?(?:child|of-type)\\(\\s*(?:even|odd|[+-]?(?:(?:0|[1-9]\\d*)n?|n)|(?:[+-]?(?:0|[1-9]\\d*))?n\\s*[+-]\\s*(?:0|[1-9]\\d*))\\s*\\)";
export declare const ATTR_TYPE = "\\[[^|\\]]+\\]";
export declare const SUB_TYPE = "\\[[^|\\]]+\\]|[#.:][\\w-]+";
export declare const SUB_TYPE_WO_PSEUDO = "\\[[^|\\]]+\\]|[#.][\\w-]+";
export declare const TAG_TYPE_WO_UNIVERSAL = "[A-Za-z][\\w-]*";
export declare const TAG_TYPE = "\\*|[A-Za-z][\\w-]*";
export declare const TAG_TYPE_I = "\\*|[A-Z][\\w-]*";
export declare const COMPOUND = "(?:\\*|[A-Za-z][\\w-]*|(?:\\*|[A-Za-z][\\w-]*)?(?:\\[[^|\\]]+\\]|[#.:][\\w-]+)+)";
export declare const COMPOUND_L = "(?:\\*|[A-Za-z][\\w-]*|(?:\\*|[A-Za-z][\\w-]*)?(?:\\[[^|\\]]+\\]|[#.:][\\w-]+|:is\\(\\s*[^)]+\\s*\\))+)";
export declare const COMPOUND_I = "(?:\\*|[A-Z][\\w-]*|(?:\\*|[A-Z][\\w-]*)?(?:\\[[^|\\]]+\\]|[#.:][\\w-]+)+)";
export declare const COMPOUND_WO_PSEUDO = "(?:\\*|[A-Za-z][\\w-]*|(?:\\*|[A-Za-z][\\w-]*)?(?:\\[[^|\\]]+\\]|[#.][\\w-]+)+)";
export declare const COMPLEX = "(?:\\*|[A-Za-z][\\w-]*|(?:\\*|[A-Za-z][\\w-]*)?(?:\\[[^|\\]]+\\]|[#.:][\\w-]+)+)(?:\\s?[\\s>~+]\\s?(?:\\*|[A-Za-z][\\w-]*|(?:\\*|[A-Za-z][\\w-]*)?(?:\\[[^|\\]]+\\]|[#.:][\\w-]+)+))*";
export declare const COMPLEX_L = "(?:\\*|[A-Za-z][\\w-]*|(?:\\*|[A-Za-z][\\w-]*)?(?:\\[[^|\\]]+\\]|[#.:][\\w-]+|:is\\(\\s*[^)]+\\s*\\))+)(?:\\s?[\\s>~+]\\s?(?:\\*|[A-Za-z][\\w-]*|(?:\\*|[A-Za-z][\\w-]*)?(?:\\[[^|\\]]+\\]|[#.:][\\w-]+|:is\\(\\s*[^)]+\\s*\\))+))*";
export declare const HAS_COMPOUND = "has\\([\\s>]?\\s*(?:\\*|[A-Za-z][\\w-]*|(?:\\*|[A-Za-z][\\w-]*)?(?:\\[[^|\\]]+\\]|[#.][\\w-]+)+)\\s*\\)";
export declare const LOGIC_COMPOUND = "(?:is|not)\\(\\s*(?:\\*|[A-Za-z][\\w-]*|(?:\\*|[A-Za-z][\\w-]*)?(?:\\[[^|\\]]+\\]|[#.:][\\w-]+|:is\\(\\s*[^)]+\\s*\\))+)(?:\\s*,\\s*(?:\\*|[A-Za-z][\\w-]*|(?:\\*|[A-Za-z][\\w-]*)?(?:\\[[^|\\]]+\\]|[#.:][\\w-]+|:is\\(\\s*[^)]+\\s*\\))+))*\\s*\\)";
export declare const LOGIC_COMPLEX = "(?:is|not)\\(\\s*(?:\\*|[A-Za-z][\\w-]*|(?:\\*|[A-Za-z][\\w-]*)?(?:\\[[^|\\]]+\\]|[#.:][\\w-]+|:is\\(\\s*[^)]+\\s*\\))+)(?:\\s?[\\s>~+]\\s?(?:\\*|[A-Za-z][\\w-]*|(?:\\*|[A-Za-z][\\w-]*)?(?:\\[[^|\\]]+\\]|[#.:][\\w-]+|:is\\(\\s*[^)]+\\s*\\))+))*(?:\\s*,\\s*(?:\\*|[A-Za-z][\\w-]*|(?:\\*|[A-Za-z][\\w-]*)?(?:\\[[^|\\]]+\\]|[#.:][\\w-]+|:is\\(\\s*[^)]+\\s*\\))+)(?:\\s?[\\s>~+]\\s?(?:\\*|[A-Za-z][\\w-]*|(?:\\*|[A-Za-z][\\w-]*)?(?:\\[[^|\\]]+\\]|[#.:][\\w-]+|:is\\(\\s*[^)]+\\s*\\))+))*)*\\s*\\)";
export declare const FORM_PARTS: readonly string[];
export declare const INPUT_BUTTON: readonly string[];
export declare const INPUT_CHECK: readonly string[];
export declare const INPUT_DATE: readonly string[];
export declare const INPUT_TEXT: readonly string[];
export declare const INPUT_EDIT: readonly string[];
export declare const INPUT_LTR: readonly string[];
export declare const ATTR_VALUE_I: readonly string[];
export declare const KEYS_LOGICAL: Set<string>;
export declare const KEYS_PS_CLASS_SUPPORTED: Set<string>;
export declare const KEYS_PS_CLASS_UNSUPPORTED: Set<string>;
export declare const KEYS_PS_ELEMENT_UNSUPPORTED: Set<string>;
+41
View File
@@ -0,0 +1,41 @@
export declare class Evaluator {
#private;
window: object;
documentCache: WeakMap<WeakKey, any>;
check: boolean | undefined;
noexcept: boolean | undefined;
warn: boolean | undefined;
matchOpts: {
warn: boolean;
} | undefined;
node: object | undefined;
pseudoElements: any[] | undefined;
invalidate: boolean | undefined;
constructor(window: object);
setup(selector: string, node: object, opt?: {
check?: boolean;
noexcept?: boolean;
warn?: boolean;
}): object;
onError: (e: Error, opt?: {
noexcept?: boolean;
}) => void;
clearResults: (all?: boolean) => void;
matchSelector: (ast: object, node: object, opt: object) => boolean;
matchLeaves: (leaves: Array<object>, node: object, opt: object) => boolean;
getFilterLeaves: (leaves: Array<object>) => Array<object>;
evaluateShadowHost: (ast: object, node: object) => boolean;
matchPseudoClassSelector: (ast: object, node: object, opt?: {
forgive?: boolean;
warn?: boolean;
}) => Set<object> | boolean;
createTreeWalker: (node: object, opt?: {
force?: boolean;
whatToShow?: number;
}) => object;
yieldCombinatorMatches(twig: object, node: object, opt?: {
dir?: string;
}): Generator<any, void, unknown>;
yieldTraverseAllDescendants(baseNode: object, leaves: Array<object>, opt: object): Generator<any, void, unknown>;
yieldFindDescendantNodes(leaves: Array<object>, baseNode: object, opt: object): Generator<any, void, unknown>;
}
+13
View File
@@ -0,0 +1,13 @@
export declare class EventHandler {
#private;
constructor(window: object);
get currentEvent(): Event | null;
get currentFocus(): Event | null;
get lastFocusVisible(): Node | null;
set lastFocusVisible(node: Node);
handleFocusEvent: (evt: Event) => void;
handleKeyboardEvent: (evt: KeyboardEvent) => void;
handleMouseEvent: (evt: MouseEvent) => void;
registerEventListeners: () => void;
destroy: () => void;
}
+10
View File
@@ -0,0 +1,10 @@
import { Evaluator } from './evaluator.js';
export declare class Finder extends Evaluator {
#private;
setup(selector: string, node: object, opt?: {
check?: boolean;
noexcept?: boolean;
warn?: boolean;
}): object;
find: (targetType: string) => Set<object> | object;
}
+5
View File
@@ -0,0 +1,5 @@
export declare class Mapper {
#private;
constructor(context: import('./finder.js').Finder);
correspond(selector: string): any[];
}
+24
View File
@@ -0,0 +1,24 @@
export declare const matchPseudoElementSelector: (astName: string, astType: string, { forgive, globalObject, warn }?: {
forgive?: boolean;
globalObject?: object;
warn?: boolean;
}) => void;
export declare const matchDirectionPseudoClass: (ast: object, node: object, dirCache?: WeakMap<any, any>) => boolean;
export declare const matchLanguagePseudoClass: (ast: object, node: object, langCache?: WeakMap<any, any>) => boolean;
export declare const matchCheckedPseudoClass: (node: object) => boolean;
export declare const matchLinkPseudoClass: (node: object) => boolean;
export declare const matchOpenPseudoClass: (node: object) => boolean;
export declare const matchPlaceholderShownPseudoClass: (node: object, keys: Set<string>) => boolean;
export declare const matchRangePseudoClass: (astName: string, node: object, keys: Set<string>) => boolean;
export declare const matchReadOnlyPseudoClass: (astName: string, node: object) => boolean;
export declare const matchRequiredPseudoClass: (astName: string, node: object, keys: Set<string>) => boolean;
export declare const matchAttributeSelector: (ast: object, node: object, { check, forgive, globalObject }?: {
check?: boolean;
forgive?: boolean;
globalObject?: object;
}) => boolean;
export declare const matchTypeSelector: (ast: object, node: object, { check, forgive, globalObject }?: {
check?: boolean;
forgive?: boolean;
globalObject?: object;
}) => boolean;
+63
View File
@@ -0,0 +1,63 @@
export declare const concatCall: (nodes: Array<Element> | NodeList, callback?: ((element: Element) => boolean | void) | undefined) => Array<Element>;
export declare const isHTML: (node: Element | Document) => boolean;
export declare const isTarget: (node: Element) => boolean;
export declare const isIndeterminate: (node: Element) => boolean;
export declare const optimize: (selector: string, tokens: Array<string>) => string;
export declare const matchAssert: (factory: Array<(c: Element, f?: ((element: Element) => boolean | void), x?: null, r?: boolean) => boolean>, element: Element, callback?: ((element: Element) => boolean | void) | undefined) => boolean;
export declare const solveNth: (element: Element, dir: boolean | number, state: object, isOfType: boolean) => number;
export declare class Nwsapi {
#private;
hasDupes: boolean | undefined;
constructor(window: object, document: object, cacheSize?: number);
private #documentOrder;
private #unique;
byId(id: string, context: Element | Document): Array<Element>;
byTag(tag: string, context: Element | Document | DocumentFragment): Array<Element>;
byClass(cls: string, context: Element | Document | DocumentFragment): Array<Element>;
private #emit;
private #switchContext;
compileSelector(expression: string, source: string, mode: boolean): string;
compileUniversal(selector: string, source: string): {
source: string;
selector: string;
};
compileId(selector: string, source: string): {
source: string;
selector: string;
};
compileClass(selector: string, source: string): {
source: string;
selector: string;
};
compileTag(selector: string, source: string): {
source: string;
selector: string;
};
compileAttribute(selector: string, source: string): {
source: string;
selector: string;
};
compileCombinator(symbol: string, selector: string, source: string): {
source: string;
selector: string;
};
compilePseudo(selector: string, source: string, selectorString: string): {
source: string;
selector: string;
};
compilePseudoStructural(match: Array<string>, source: string): string;
compilePseudoTreeStruct(match: Array<string>, source: string, selectorString: string): string;
compilePseudoLogical(match: Array<string>, source: string): string;
compilePseudoLocation(match: Array<string>, source: string): string;
compilePseudoInputState(match: Array<string>, source: string): string;
compilePseudoInputValue(match: Array<string>, source: string): string;
compile(selector: string, mode: boolean): (c: Element | Element[] | NodeList, f?: ((element: Element) => boolean | void), x?: Element | Document | null, r?: boolean | Element[]) => boolean | Element[];
collect(selectors: Array<string>, context: Element | Document, callback?: ((element: Element) => boolean | void) | undefined): object;
private #matchCollect;
private #parseSelector;
match(selectors: string, element: Element, callback?: ((element: Element) => boolean | void) | undefined): boolean;
closest(selectors: string, element: Element, callback?: ((element: Element) => boolean | void) | undefined): Element | null;
select(selectors: string, context: Element | Document, callback?: ((element: Element) => boolean | void) | undefined): Array<Element>;
first(selectors: string, context: Element | Document, callback?: ((element: Element) => boolean | void) | undefined): Element | null;
clear(clearAll?: boolean): void;
}
+19
View File
@@ -0,0 +1,19 @@
export declare const unescapeSelector: (selector?: string) => string;
export declare const preprocess: (value: string) => string;
export declare const parseSelector: (sel: string, context?: string) => object;
export declare const walkAST: (ast?: object, toObject?: boolean, callback?: Function) => {
branches: Array<object>;
info: object;
};
export declare const compareASTNodes: (a: object, b: object) => number;
export declare const sortAST: (asts: Array<object>) => Array<object>;
export declare const parseAstName: (selector: string) => {
prefix: string;
localName: string;
};
export declare const extractSubjectsAst: (ast: object) => Array<{
id: string | null;
className: string | null;
tag: string | null;
}>;
export { find as findAST, generate as generateCSS } from 'css-tree';
@@ -0,0 +1,5 @@
export declare class SelectorProcessor {
#private;
constructor(context: import('./finder.js').Finder);
process(branches: Array<object>, selector: string): object;
}
+12
View File
@@ -0,0 +1,12 @@
export declare const findNestedHas: (leaf: object) => object | null;
export declare const findLogicalWithNestedHas: (leaf: object) => object | null;
export declare const validateHasNesting: (astChildren: Array<object>) => boolean;
export declare const createHasValidator: (globalObj: object) => Function;
export declare const isInvalidCombinator: (type: string, prevType: string | null, isLast: boolean) => boolean;
export declare const isSupportedAST: (ast: object) => boolean;
export declare const extractSubjectsRegExp: (selector: string, caseSensitive: boolean) => Array<{
id: string | null;
className: string | null;
tag: string | null;
}>;
export declare const filterSelector: (selector: string, target: string) => boolean;
+25
View File
@@ -0,0 +1,25 @@
export declare const getType: (o: object) => string;
export declare const verifyArray: (arr: any[], type: string) => any[];
export declare const generateException: (msg: string, name: string, globalObject?: object) => DOMException;
export declare const resolveContent: (node: object) => Array<object | boolean>;
export declare const traverseNode: (node: object, walker: object, force?: boolean) => object | null;
export declare const isCustomElement: (node: object, { formAssociated }?: {
formAssociated?: boolean;
}) => boolean;
export declare const getSlottedTextContent: (node: object) => string | null;
export declare const getDirectionality: (node: object, dirCache?: WeakMap<any, any>) => string | null;
export declare const getLanguageAttribute: (node: object, langCache?: WeakMap<any, any>) => string | null;
export declare const isContentEditable: (node: object) => boolean;
export declare const isVisible: (node: object) => boolean;
export declare const isFocusVisible: (node: object) => boolean;
export declare const isFocusableArea: (node: object) => boolean;
export declare const isFocusable: (node: object) => boolean;
export declare const getNamespaceURI: (ns: string, node: object) => string | null;
export declare const isNamespaceDeclared: (ns?: string, node?: object) => boolean;
export declare const isPreceding: (nodeA: object, nodeB: object) => boolean;
export declare const compareNodes: (a: object, b: object) => number;
export declare const sortNodes: (nodes?: Array<object> | Set<object>) => Array<object>;
export declare const findBestSeed: (nodes: any[], state?: object) => object;
export declare const populateHasAllowlist: (current: object, list: WeakSet<any>, visitedAncestors: Set<any>) => void;
export declare const collectAllDescendants: (node: Document | DocumentFragment | Element, document: Document) => Array<Element>;
export declare const getTraversalStrategy: (branch: Array<object>, targetType: string, hasScope: boolean, scoped: boolean) => object;