Initial commit: Crypto trader application

This commit is contained in:
2025-12-25 20:20:40 -05:00
commit 07a04c1bb8
47895 changed files with 2042266 additions and 0 deletions

21
frontend/node_modules/@asamuzakjp/dom-selector/LICENSE generated vendored Normal file
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.

View File

@@ -0,0 +1,324 @@
# 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.
## Install
```console
npm i @asamuzakjp/dom-selector
```
## Usage
```javascript
import { DOMSelector } from '@asamuzakjp/dom-selector';
import { JSDOM } from 'jsdom';
const { window } = new JSDOM();
const {
closest, matches, querySelector, querySelectorAll
} = new DOMSelector(window);
```
<!-- Generated by documentation.js. Update this documentation by updating the source code. -->
### matches(selector, node, opt)
matches - equivalent to [Element.matches()][64]
#### Parameters
- `selector` **[string][59]** CSS selector
- `node` **[object][60]** Element node
- `opt` **[object][60]?** options
- `opt.noexcept` **[boolean][61]?** no exception
- `opt.warn` **[boolean][61]?** console warn e.g. unsupported pseudo-class
Returns **[boolean][61]** `true` if matched, `false` otherwise
### closest(selector, node, opt)
closest - equivalent to [Element.closest()][65]
#### Parameters
- `selector` **[string][59]** CSS selector
- `node` **[object][60]** Element node
- `opt` **[object][60]?** options
- `opt.noexcept` **[boolean][61]?** no exception
- `opt.warn` **[boolean][61]?** console warn e.g. unsupported pseudo-class
Returns **[object][60]?** matched node
### querySelector(selector, node, opt)
querySelector - equivalent to [Document.querySelector()][66], [DocumentFragment.querySelector()][67] and [Element.querySelector()][68]
#### Parameters
- `selector` **[string][59]** CSS selector
- `node` **[object][60]** Document, DocumentFragment or Element node
- `opt` **[object][60]?** options
- `opt.noexcept` **[boolean][61]?** no exception
- `opt.warn` **[boolean][61]?** console warn e.g. unsupported pseudo-class
Returns **[object][60]?** matched node
### querySelectorAll(selector, node, opt)
querySelectorAll - equivalent to [Document.querySelectorAll()][69], [DocumentFragment.querySelectorAll()][70] and [Element.querySelectorAll()][71]
**NOTE**: returns Array, not NodeList
#### Parameters
- `selector` **[string][59]** CSS selector
- `node` **[object][60]** Document, DocumentFragment or Element node
- `opt` **[object][60]?** options
- `opt.noexcept` **[boolean][61]?** no exception
- `opt.warn` **[boolean][61]?** console warn e.g. unsupported pseudo-class
Returns **[Array][62]&lt;([object][60] \| [undefined][63])>** array of matched nodes
## Monkey patch jsdom
``` javascript
import { DOMSelector } from '@asamuzakjp/dom-selector';
import { JSDOM } from 'jsdom';
const dom = new JSDOM('', {
runScripts: 'dangerously',
url: 'http://localhost/',
beforeParse: window => {
const domSelector = new DOMSelector(window);
const matches = domSelector.matches.bind(domSelector);
window.Element.prototype.matches = function (...args) {
if (!args.length) {
throw new window.TypeError('1 argument required, but only 0 present.');
}
const [selector] = args;
return matches(selector, this);
};
const closest = domSelector.closest.bind(domSelector);
window.Element.prototype.closest = function (...args) {
if (!args.length) {
throw new window.TypeError('1 argument required, but only 0 present.');
}
const [selector] = args;
return closest(selector, this);
};
const querySelector = domSelector.querySelector.bind(domSelector);
window.Document.prototype.querySelector = function (...args) {
if (!args.length) {
throw new window.TypeError('1 argument required, but only 0 present.');
}
const [selector] = args;
return querySelector(selector, this);
};
window.DocumentFragment.prototype.querySelector = function (...args) {
if (!args.length) {
throw new window.TypeError('1 argument required, but only 0 present.');
}
const [selector] = args;
return querySelector(selector, this);
};
window.Element.prototype.querySelector = function (...args) {
if (!args.length) {
throw new window.TypeError('1 argument required, but only 0 present.');
}
const [selector] = args;
return querySelector(selector, this);
};
const querySelectorAll = domSelector.querySelectorAll.bind(domSelector);
window.Document.prototype.querySelectorAll = function (...args) {
if (!args.length) {
throw new window.TypeError('1 argument required, but only 0 present.');
}
const [selector] = args;
return querySelectorAll(selector, this);
};
window.DocumentFragment.prototype.querySelectorAll = function (...args) {
if (!args.length) {
throw new window.TypeError('1 argument required, but only 0 present.');
}
const [selector] = args;
return querySelectorAll(selector, this);
};
window.Element.prototype.querySelectorAll = function (...args) {
if (!args.length) {
throw new window.TypeError('1 argument required, but only 0 present.');
}
const [selector] = args;
return querySelectorAll(selector, this);
};
}
});
```
## Supported CSS selectors
|Pattern|Supported|Note|
|:--------|:-------:|:--------|
|\*|✓| |
|E|✓| |
|ns\|E|✓| |
|\*\|E|✓| |
|\|E|✓| |
|E&nbsp;F|✓| |
|E > F|✓| |
|E + F|✓| |
|E ~ F|✓| |
|F \|\| E|Unsupported| |
|E.warning|✓| |
|E#myid|✓| |
|E\[foo\]|✓| |
|E\[foo="bar"\]|✓| |
|E\[foo="bar"&nbsp;i\]|✓| |
|E\[foo="bar"&nbsp;s\]|✓| |
|E\[foo~="bar"\]|✓| |
|E\[foo^="bar"\]|✓| |
|E\[foo$="bar"\]|✓| |
|E\[foo*="bar"\]|✓| |
|E\[foo\|="en"\]|✓| |
|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&#8209;link|✓| |
|E:link|✓| |
|E:visited|✓|Returns `false` or `null` to prevent fingerprinting.|
|E:local&#8209;link|✓| |
|E:target|✓| |
|E:target&#8209;within|✓| |
|E:scope|✓| |
|E:hover|✓| |
|E:active|✓| |
|E:focus|✓| |
|E:focus&#8209;visible|✓| |
|E:focus&#8209;within|✓| |
|E:current|Unsupported| |
|E:current(s)|Unsupported| |
|E:past|Unsupported| |
|E:future|Unsupported| |
|E:open<br>E:closed|Partially supported|Matching with &lt;select&gt;, e.g. `select:open`, is not supported.|
|E:popover-open|✓| |
|E:enabled<br>E:disabled|✓| |
|E:read&#8209;write<br>E:read&#8209;only|✓| |
|E:placeholder&#8209;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&#8209;valid<br>E:user&#8209;invalid|Unsupported| |
|E:root|✓| |
|E:empty|✓| |
|E:nth&#8209;child(n&nbsp;[of&nbsp;S]?)|✓| |
|E:nth&#8209;last&#8209;child(n&nbsp;[of&nbsp;S]?)|✓| |
|E:first&#8209;child|✓| |
|E:last&#8209;child|✓| |
|E:only&#8209;child|✓| |
|E:nth&#8209;of&#8209;type(n)|✓| |
|E:nth&#8209;last&#8209;of&#8209;type(n)|✓| |
|E:first&#8209;of&#8209;type|✓| |
|E:last&#8209;of&#8209;type|✓| |
|E:only&#8209;of&#8209;type|✓| |
|E:nth&#8209;col(n)|Unsupported| |
|E:nth&#8209;last&#8209;col(n)|Unsupported| |
|CE:state(v)|✓|*1|
|:host|✓| |
|:host(s)|✓| |
|:host(:state(v))|✓|*1|
|:host:has(rs1, rs2, ...)|✓| |
|:host(s):has(rs1, rs2, ...)|✓| |
|:host&#8209;context(s)|✓| |
|:host&#8209;context(s):has(rs1, rs2, ...)|✓| |
|&amp;|✓|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/)
[1]: #matches
[2]: #parameters
[3]: #closest
[4]: #parameters-1
[5]: #queryselector
[6]: #parameters-2
[7]: #queryselectorall
[8]: #parameters-3
[59]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String
[60]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object
[61]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean
[62]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array
[63]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/undefined
[64]: https://developer.mozilla.org/docs/Web/API/Element/matches
[65]: https://developer.mozilla.org/docs/Web/API/Element/closest
[66]: https://developer.mozilla.org/docs/Web/API/Document/querySelector
[67]: https://developer.mozilla.org/docs/Web/API/DocumentFragment/querySelector
[68]: https://developer.mozilla.org/docs/Web/API/Element/querySelector
[69]: https://developer.mozilla.org/docs/Web/API/Document/querySelectorAll
[70]: https://developer.mozilla.org/docs/Web/API/DocumentFragment/querySelectorAll
[71]: https://developer.mozilla.org/docs/Web/API/Element/querySelectorAll

View File

@@ -0,0 +1,55 @@
# Blue Oak Model License
Version 1.0.0
## Purpose
This license gives everyone as much permission to work with
this software as possible, while protecting contributors
from liability.
## Acceptance
In order to receive this license, you must agree to its
rules. The rules of this license are both obligations
under that agreement and conditions to your license.
You must not do anything with this software that triggers
a rule that you cannot or will not follow.
## Copyright
Each contributor licenses you to do everything with this
software that would otherwise infringe that contributor's
copyright in it.
## Notices
You must ensure that everyone who gets a copy of
any part of this software from you, with or without
changes, also gets the text of this license or a link to
<https://blueoakcouncil.org/license/1.0.0>.
## Excuse
If anyone notifies you in writing that you have not
complied with [Notices](#notices), you can keep your
license by taking all practical steps to comply within 30
days after the notice. If you do not do so, your license
ends immediately.
## Patent
Each contributor licenses you to do everything with this
software that would otherwise infringe any patent claims
they can license or become able to license.
## Reliability
No contributor can revoke this license.
## No Liability
***As far as the law allows, this software comes as is,
without any warranty or condition, and no contributor
will be liable to anyone for any damages related to this
software or this license, under any kind of legal claim.***

View File

@@ -0,0 +1,334 @@
# lru-cache
A cache object that deletes the least-recently-used items.
Specify a max number of the most recently used items that you
want to keep, and this cache will keep that many of the most
recently accessed items.
This is not primarily a TTL cache, and does not make strong TTL
guarantees. There is no preemptive pruning of expired items by
default, but you _may_ set a TTL on the cache or on a single
`set`. If you do so, it will treat expired items as missing, and
delete them when fetched. If you are more interested in TTL
caching than LRU caching, check out
[@isaacs/ttlcache](http://npm.im/@isaacs/ttlcache).
As of version 7, this is one of the most performant LRU
implementations available in JavaScript, and supports a wide
diversity of use cases. However, note that using some of the
features will necessarily impact performance, by causing the
cache to have to do more work. See the "Performance" section
below.
## Installation
```bash
npm install lru-cache --save
```
## Usage
```js
// hybrid module, either works
import { LRUCache } from 'lru-cache'
// or:
const { LRUCache } = require('lru-cache')
// or in minified form for web browsers:
import { LRUCache } from 'http://unpkg.com/lru-cache@9/dist/mjs/index.min.mjs'
// At least one of 'max', 'ttl', or 'maxSize' is required, to prevent
// unsafe unbounded storage.
//
// In most cases, it's best to specify a max for performance, so all
// the required memory allocation is done up-front.
//
// All the other options are optional, see the sections below for
// documentation on what each one does. Most of them can be
// overridden for specific items in get()/set()
const options = {
max: 500,
// for use with tracking overall storage size
maxSize: 5000,
sizeCalculation: (value, key) => {
return 1
},
// for use when you need to clean up something when objects
// are evicted from the cache
dispose: (value, key, reason) => {
freeFromMemoryOrWhatever(value)
},
// for use when you need to know that an item is being inserted
// note that this does NOT allow you to prevent the insertion,
// it just allows you to know about it.
onInsert: (value, key, reason) => {
logInsertionOrWhatever(key, value)
},
// how long to live in ms
ttl: 1000 * 60 * 5,
// return stale items before removing from cache?
allowStale: false,
updateAgeOnGet: false,
updateAgeOnHas: false,
// async method to use for cache.fetch(), for
// stale-while-revalidate type of behavior
fetchMethod: async (key, staleValue, { options, signal, context }) => {},
}
const cache = new LRUCache(options)
cache.set('key', 'value')
cache.get('key') // "value"
// non-string keys ARE fully supported
// but note that it must be THE SAME object, not
// just a JSON-equivalent object.
var someObject = { a: 1 }
cache.set(someObject, 'a value')
// Object keys are not toString()-ed
cache.set('[object Object]', 'a different value')
assert.equal(cache.get(someObject), 'a value')
// A similar object with same keys/values won't work,
// because it's a different object identity
assert.equal(cache.get({ a: 1 }), undefined)
cache.clear() // empty the cache
```
If you put more stuff in the cache, then less recently used items
will fall out. That's what an LRU cache is.
For full description of the API and all options, please see [the
LRUCache typedocs](https://isaacs.github.io/node-lru-cache/)
## Storage Bounds Safety
This implementation aims to be as flexible as possible, within
the limits of safe memory consumption and optimal performance.
At initial object creation, storage is allocated for `max` items.
If `max` is set to zero, then some performance is lost, and item
count is unbounded. Either `maxSize` or `ttl` _must_ be set if
`max` is not specified.
If `maxSize` is set, then this creates a safe limit on the
maximum storage consumed, but without the performance benefits of
pre-allocation. When `maxSize` is set, every item _must_ provide
a size, either via the `sizeCalculation` method provided to the
constructor, or via a `size` or `sizeCalculation` option provided
to `cache.set()`. The size of every item _must_ be a positive
integer.
If neither `max` nor `maxSize` are set, then `ttl` tracking must
be enabled. Note that, even when tracking item `ttl`, items are
_not_ preemptively deleted when they become stale, unless
`ttlAutopurge` is enabled. Instead, they are only purged the
next time the key is requested. Thus, if `ttlAutopurge`, `max`,
and `maxSize` are all not set, then the cache will potentially
grow unbounded.
In this case, a warning is printed to standard error. Future
versions may require the use of `ttlAutopurge` if `max` and
`maxSize` are not specified.
If you truly wish to use a cache that is bound _only_ by TTL
expiration, consider using a `Map` object, and calling
`setTimeout` to delete entries when they expire. It will perform
much better than an LRU cache.
Here is an implementation you may use, under the same
[license](./LICENSE) as this package:
```js
// a storage-unbounded ttl cache that is not an lru-cache
const cache = {
data: new Map(),
timers: new Map(),
set: (k, v, ttl) => {
if (cache.timers.has(k)) {
clearTimeout(cache.timers.get(k))
}
cache.timers.set(
k,
setTimeout(() => cache.delete(k), ttl),
)
cache.data.set(k, v)
},
get: k => cache.data.get(k),
has: k => cache.data.has(k),
delete: k => {
if (cache.timers.has(k)) {
clearTimeout(cache.timers.get(k))
}
cache.timers.delete(k)
return cache.data.delete(k)
},
clear: () => {
cache.data.clear()
for (const v of cache.timers.values()) {
clearTimeout(v)
}
cache.timers.clear()
},
}
```
If that isn't to your liking, check out
[@isaacs/ttlcache](http://npm.im/@isaacs/ttlcache).
## Storing Undefined Values
This cache never stores undefined values, as `undefined` is used
internally in a few places to indicate that a key is not in the
cache.
You may call `cache.set(key, undefined)`, but this is just
an alias for `cache.delete(key)`. Note that this has the effect
that `cache.has(key)` will return _false_ after setting it to
undefined.
```js
cache.set(myKey, undefined)
cache.has(myKey) // false!
```
If you need to track `undefined` values, and still note that the
key is in the cache, an easy workaround is to use a sigil object
of your own.
```js
import { LRUCache } from 'lru-cache'
const undefinedValue = Symbol('undefined')
const cache = new LRUCache(...)
const mySet = (key, value) =>
cache.set(key, value === undefined ? undefinedValue : value)
const myGet = (key, value) => {
const v = cache.get(key)
return v === undefinedValue ? undefined : v
}
```
## Performance
As of January 2022, version 7 of this library is one of the most
performant LRU cache implementations in JavaScript.
Benchmarks can be extremely difficult to get right. In
particular, the performance of set/get/delete operations on
objects will vary _wildly_ depending on the type of key used. V8
is highly optimized for objects with keys that are short strings,
especially integer numeric strings. Thus any benchmark which
tests _solely_ using numbers as keys will tend to find that an
object-based approach performs the best.
Note that coercing _anything_ to strings to use as object keys is
unsafe, unless you can be 100% certain that no other type of
value will be used. For example:
```js
const myCache = {}
const set = (k, v) => (myCache[k] = v)
const get = k => myCache[k]
set({}, 'please hang onto this for me')
set('[object Object]', 'oopsie')
```
Also beware of "Just So" stories regarding performance. Garbage
collection of large (especially: deep) object graphs can be
incredibly costly, with several "tipping points" where it
increases exponentially. As a result, putting that off until
later can make it much worse, and less predictable. If a library
performs well, but only in a scenario where the object graph is
kept shallow, then that won't help you if you are using large
objects as keys.
In general, when attempting to use a library to improve
performance (such as a cache like this one), it's best to choose
an option that will perform well in the sorts of scenarios where
you'll actually use it.
This library is optimized for repeated gets and minimizing
eviction time, since that is the expected need of a LRU. Set
operations are somewhat slower on average than a few other
options, in part because of that optimization. It is assumed
that you'll be caching some costly operation, ideally as rarely
as possible, so optimizing set over get would be unwise.
If performance matters to you:
1. If it's at all possible to use small integer values as keys,
and you can guarantee that no other types of values will be
used as keys, then do that, and use a cache such as
[lru-fast](https://npmjs.com/package/lru-fast), or
[mnemonist's
LRUCache](https://yomguithereal.github.io/mnemonist/lru-cache)
which uses an Object as its data store.
2. Failing that, if at all possible, use short non-numeric
strings (ie, less than 256 characters) as your keys, and use
[mnemonist's
LRUCache](https://yomguithereal.github.io/mnemonist/lru-cache).
3. If the types of your keys will be anything else, especially
long strings, strings that look like floats, objects, or some
mix of types, or if you aren't sure, then this library will
work well for you.
If you do not need the features that this library provides
(like asynchronous fetching, a variety of TTL staleness
options, and so on), then [mnemonist's
LRUMap](https://yomguithereal.github.io/mnemonist/lru-map) is
a very good option, and just slightly faster than this module
(since it does considerably less).
4. Do not use a `dispose` function, size tracking, or especially
ttl behavior, unless absolutely needed. These features are
convenient, and necessary in some use cases, and every attempt
has been made to make the performance impact minimal, but it
isn't nothing.
## Breaking Changes in Version 7
This library changed to a different algorithm and internal data
structure in version 7, yielding significantly better
performance, albeit with some subtle changes as a result.
If you were relying on the internals of LRUCache in version 6 or
before, it probably will not work in version 7 and above.
## Breaking Changes in Version 8
- The `fetchContext` option was renamed to `context`, and may no
longer be set on the cache instance itself.
- Rewritten in TypeScript, so pretty much all the types moved
around a lot.
- The AbortController/AbortSignal polyfill was removed. For this
reason, **Node version 16.14.0 or higher is now required**.
- Internal properties were moved to actual private class
properties.
- Keys and values must not be `null` or `undefined`.
- Minified export available at `'lru-cache/min'`, for both CJS
and MJS builds.
## Breaking Changes in Version 9
- Named export only, no default export.
- AbortController polyfill returned, albeit with a warning when
used.
## Breaking Changes in Version 10
- `cache.fetch()` return type is now `Promise<V | undefined>`
instead of `Promise<V | void>`. This is an irrelevant change
practically speaking, but can require changes for TypeScript
users.
For more info, see the [change log](CHANGELOG.md).

View File

@@ -0,0 +1,101 @@
{
"name": "lru-cache",
"description": "A cache object that deletes the least-recently-used items.",
"version": "11.2.4",
"author": "Isaac Z. Schlueter <i@izs.me>",
"keywords": [
"mru",
"lru",
"cache"
],
"sideEffects": false,
"scripts": {
"build": "npm run prepare",
"prepare": "tshy && bash fixup.sh",
"pretest": "npm run prepare",
"presnap": "npm run prepare",
"test": "tap",
"snap": "tap",
"preversion": "npm test",
"postversion": "npm publish",
"prepublishOnly": "git push origin --follow-tags",
"format": "prettier --write .",
"typedoc": "typedoc --tsconfig ./.tshy/esm.json ./src/*.ts",
"benchmark-results-typedoc": "bash scripts/benchmark-results-typedoc.sh",
"prebenchmark": "npm run prepare",
"benchmark": "make -C benchmark",
"preprofile": "npm run prepare",
"profile": "make -C benchmark profile"
},
"main": "./dist/commonjs/index.js",
"types": "./dist/commonjs/index.d.ts",
"tshy": {
"exports": {
".": "./src/index.ts",
"./min": {
"import": {
"types": "./dist/esm/index.d.ts",
"default": "./dist/esm/index.min.js"
},
"require": {
"types": "./dist/commonjs/index.d.ts",
"default": "./dist/commonjs/index.min.js"
}
}
}
},
"repository": {
"type": "git",
"url": "git://github.com/isaacs/node-lru-cache.git"
},
"devDependencies": {
"@types/node": "^24.3.0",
"benchmark": "^2.1.4",
"esbuild": "^0.25.9",
"marked": "^4.2.12",
"mkdirp": "^3.0.1",
"prettier": "^3.6.2",
"tap": "^21.1.0",
"tshy": "^3.0.2",
"typedoc": "^0.28.12"
},
"license": "BlueOak-1.0.0",
"files": [
"dist"
],
"engines": {
"node": "20 || >=22"
},
"tap": {
"node-arg": [
"--expose-gc"
],
"plugin": [
"@tapjs/clock"
]
},
"exports": {
".": {
"import": {
"types": "./dist/esm/index.d.ts",
"default": "./dist/esm/index.js"
},
"require": {
"types": "./dist/commonjs/index.d.ts",
"default": "./dist/commonjs/index.js"
}
},
"./min": {
"import": {
"types": "./dist/esm/index.d.ts",
"default": "./dist/esm/index.min.js"
},
"require": {
"types": "./dist/commonjs/index.d.ts",
"default": "./dist/commonjs/index.min.js"
}
}
},
"type": "module",
"module": "./dist/esm/index.js"
}

View File

@@ -0,0 +1,81 @@
{
"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": [
"dist",
"src",
"types"
],
"type": "module",
"exports": {
"import": {
"types": "./types/index.d.ts",
"default": "./src/index.js"
},
"require": {
"types": "./dist/cjs/index.d.cts",
"default": "./dist/cjs/index.cjs"
},
"default": {
"types": "./dist/cjs/types/index.d.cts",
"default": "./dist/cjs/index.cjs"
}
},
"types": "types/index.d.ts",
"dependencies": {
"@asamuzakjp/nwsapi": "^2.3.9",
"bidi-js": "^1.0.3",
"css-tree": "^3.1.0",
"is-potential-custom-element-name": "^1.0.1",
"lru-cache": "^11.2.4"
},
"devDependencies": {
"@types/css-tree": "^2.3.11",
"benchmark": "^2.1.4",
"c8": "^10.1.3",
"chai": "^6.2.1",
"commander": "^14.0.2",
"esbuild": "^0.27.1",
"eslint": "^9.39.1",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-jsdoc": "^61.4.1",
"eslint-plugin-prettier": "^5.5.4",
"eslint-plugin-regexp": "^2.10.0",
"eslint-plugin-unicorn": "^62.0.0",
"globals": "^16.5.0",
"jsdom": "^27.2.0",
"mocha": "^11.7.5",
"neostandard": "^0.12.2",
"prettier": "^3.7.4",
"sinon": "^21.0.0",
"tsup": "^8.5.1",
"typescript": "^5.9.3",
"wpt-runner": "^6.1.0"
},
"overrides": {
"jsdom": "$jsdom"
},
"scripts": {
"bench": "node benchmark/bench.js",
"bench:sizzle": "node benchmark/bench-sizzle.js",
"build": "npm run tsc && npm run lint && npm test && npm run bundle && npm run test:cjs",
"bundle": "tsup src/index.js --format=cjs --platform=node --outDir=dist/cjs/ --sourcemap --dts",
"lint": "eslint --fix .",
"test": "c8 --reporter=text mocha --parallel --exit test/**/*.test.js",
"test:cjs": "mocha --exit test/index.test.cjs",
"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"
},
"version": "6.7.6"
}

View File

@@ -0,0 +1,353 @@
/*!
* 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 { filterSelector, getType, initNwsapi } from './js/utility.js';
/* constants */
import {
DOCUMENT_NODE,
DOCUMENT_FRAGMENT_NODE,
ELEMENT_NODE,
TARGET_ALL,
TARGET_FIRST,
TARGET_LINEAL,
TARGET_SELF
} from './js/constant.js';
const MAX_CACHE = 1024;
/**
* @typedef {object} CheckResult
* @property {boolean} match - The match result.
* @property {string?} pseudoElement - The pseudo-element, if any.
*/
/* 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 { idlUtils } = opt;
this.#window = window;
this.#document = document ?? window.document;
this.#finder = new Finder(window);
this.#idlUtils = idlUtils;
this.#nwsapi = initNwsapi(window, document);
this.#cache = new LRUCache({
max: MAX_CACHE
});
}
/**
* Clears the internal cache of finder results.
* @returns {void}
*/
clear = () => {
this.#finder.clearResults(true);
};
/**
* 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 = {}) => {
if (!node?.nodeType) {
const e = new this.#window.TypeError(`Unexpected type ${getType(node)}`);
return this.#finder.onError(e, opt);
} else if (node.nodeType !== ELEMENT_NODE) {
const e = new this.#window.TypeError(`Unexpected node ${node.nodeName}`);
return this.#finder.onError(e, opt);
}
const document = node.ownerDocument;
if (
document === this.#document &&
document.contentType === 'text/html' &&
document.documentElement &&
node.parentNode
) {
const cacheKey = `check_${selector}`;
let filterMatches = false;
if (this.#cache.has(cacheKey)) {
filterMatches = this.#cache.get(cacheKey);
} else {
filterMatches = filterSelector(selector, TARGET_SELF);
this.#cache.set(cacheKey, filterMatches);
}
if (filterMatches) {
try {
const n = this.#idlUtils ? this.#idlUtils.wrapperForImpl(node) : node;
const match = this.#nwsapi.match(selector, n);
return {
match,
pseudoElement: null
};
} catch (e) {
// fall through
}
}
}
let res;
try {
if (this.#idlUtils) {
node = this.#idlUtils.wrapperForImpl(node);
}
opt.check = true;
opt.noexept = true;
opt.warn = false;
this.#finder.setup(selector, node, opt);
res = this.#finder.find(TARGET_SELF);
} catch (e) {
this.#finder.onError(e, opt);
}
return res;
};
/**
* 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, or `false` otherwise.
*/
matches = (selector, node, opt = {}) => {
if (!node?.nodeType) {
const e = new this.#window.TypeError(`Unexpected type ${getType(node)}`);
return this.#finder.onError(e, opt);
} else if (node.nodeType !== ELEMENT_NODE) {
const e = new this.#window.TypeError(`Unexpected node ${node.nodeName}`);
return this.#finder.onError(e, opt);
}
const document = node.ownerDocument;
if (
document === this.#document &&
document.contentType === 'text/html' &&
document.documentElement &&
node.parentNode
) {
const cacheKey = `matches_${selector}`;
let filterMatches = false;
if (this.#cache.has(cacheKey)) {
filterMatches = this.#cache.get(cacheKey);
} else {
filterMatches = filterSelector(selector, TARGET_SELF);
this.#cache.set(cacheKey, filterMatches);
}
if (filterMatches) {
try {
const n = this.#idlUtils ? this.#idlUtils.wrapperForImpl(node) : node;
const res = this.#nwsapi.match(selector, n);
return res;
} catch (e) {
// fall through
}
}
}
let res;
try {
if (this.#idlUtils) {
node = this.#idlUtils.wrapperForImpl(node);
}
this.#finder.setup(selector, node, opt);
const nodes = this.#finder.find(TARGET_SELF);
res = nodes.size;
} catch (e) {
this.#finder.onError(e, opt);
}
return !!res;
};
/**
* 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 = {}) => {
if (!node?.nodeType) {
const e = new this.#window.TypeError(`Unexpected type ${getType(node)}`);
return this.#finder.onError(e, opt);
} else if (node.nodeType !== ELEMENT_NODE) {
const e = new this.#window.TypeError(`Unexpected node ${node.nodeName}`);
return this.#finder.onError(e, opt);
}
const document = node.ownerDocument;
if (
document === this.#document &&
document.contentType === 'text/html' &&
document.documentElement &&
node.parentNode
) {
const cacheKey = `closest_${selector}`;
let filterMatches = false;
if (this.#cache.has(cacheKey)) {
filterMatches = this.#cache.get(cacheKey);
} else {
filterMatches = filterSelector(selector, TARGET_LINEAL);
this.#cache.set(cacheKey, filterMatches);
}
if (filterMatches) {
try {
const n = this.#idlUtils ? this.#idlUtils.wrapperForImpl(node) : node;
const res = this.#nwsapi.closest(selector, n);
return res;
} catch (e) {
// fall through
}
}
}
let res;
try {
if (this.#idlUtils) {
node = this.#idlUtils.wrapperForImpl(node);
}
this.#finder.setup(selector, node, opt);
const nodes = this.#finder.find(TARGET_LINEAL);
if (nodes.size) {
let refNode = node;
while (refNode) {
if (nodes.has(refNode)) {
res = refNode;
break;
}
refNode = refNode.parentNode;
}
}
} catch (e) {
this.#finder.onError(e, opt);
}
return res ?? 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 = {}) => {
if (!node?.nodeType) {
const e = new this.#window.TypeError(`Unexpected type ${getType(node)}`);
return this.#finder.onError(e, opt);
}
/*
const document =
node.nodeType === DOCUMENT_NODE ? node : node.ownerDocument;
if (
document === this.#document &&
document.contentType === 'text/html' &&
document.documentElement &&
(node.nodeType !== DOCUMENT_FRAGMENT_NODE || !node.host)
) {
const cacheKey = `querySelector_${selector}`;
let filterMatches = false;
if (this.#cache.has(cacheKey)) {
filterMatches = this.#cache.get(cacheKey);
} else {
filterMatches = filterSelector(selector, TARGET_FIRST);
this.#cache.set(cacheKey, filterMatches);
}
if (filterMatches) {
try {
const n = this.#idlUtils ? this.#idlUtils.wrapperForImpl(node) : node;
const res = this.#nwsapi.first(selector, n);
return res;
} catch (e) {
// fall through
}
}
}
*/
let res;
try {
if (this.#idlUtils) {
node = this.#idlUtils.wrapperForImpl(node);
}
this.#finder.setup(selector, node, opt);
const nodes = this.#finder.find(TARGET_FIRST);
if (nodes.size) {
[res] = [...nodes];
}
} catch (e) {
this.#finder.onError(e, opt);
}
return res ?? 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 = {}) => {
if (!node?.nodeType) {
const e = new this.#window.TypeError(`Unexpected type ${getType(node)}`);
return this.#finder.onError(e, opt);
}
const document =
node.nodeType === DOCUMENT_NODE ? node : node.ownerDocument;
if (
document === this.#document &&
document.contentType === 'text/html' &&
document.documentElement &&
(node.nodeType !== DOCUMENT_FRAGMENT_NODE || !node.host)
) {
const cacheKey = `querySelectorAll_${selector}`;
let filterMatches = false;
if (this.#cache.has(cacheKey)) {
filterMatches = this.#cache.get(cacheKey);
} else {
filterMatches = filterSelector(selector, TARGET_ALL);
this.#cache.set(cacheKey, filterMatches);
}
if (filterMatches) {
try {
const n = this.#idlUtils ? this.#idlUtils.wrapperForImpl(node) : node;
const res = this.#nwsapi.select(selector, n);
return res;
} catch (e) {
// fall through
}
}
}
let res;
try {
if (this.#idlUtils) {
node = this.#idlUtils.wrapperForImpl(node);
}
this.#finder.setup(selector, node, opt);
const nodes = this.#finder.find(TARGET_ALL);
if (nodes.size) {
res = [...nodes];
}
} catch (e) {
this.#finder.onError(e, opt);
}
return res ?? [];
};
}

View File

@@ -0,0 +1,129 @@
/**
* constant.js
*/
/* string */
export const ATRULE = 'Atrule';
export const ATTR_SELECTOR = 'AttributeSelector';
export const CLASS_SELECTOR = 'ClassSelector';
export const COMBINATOR = 'Combinator';
export const IDENT = 'Identifier';
export const ID_SELECTOR = 'IdSelector';
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 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*\\)`;
// SUB_TYPE: attr, id, class, pseudo-class, note that [foo|=bar] is excluded
export const SUB_TYPE = '\\[[^|\\]]+\\]|[#.:][\\w-]+';
export const SUB_TYPE_WO_PSEUDO = '\\[[^|\\]]+\\]|[#.][\\w-]+';
// TAG_TYPE: *, tag
export const TAG_TYPE = '\\*|[A-Za-z][\\w-]*';
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'
]);
/* logical combination pseudo-classes */
export const KEYS_LOGICAL = new Set(['has', 'is', 'not', 'where']);

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,587 @@
/**
* matcher.js
*/
/* import */
import { generateCSS, parseAstName, unescapeSelector } from './parser.js';
import {
generateException,
getDirectionality,
getLanguageAttribute,
getType,
isContentEditable,
isCustomElement,
isNamespaceDeclared
} from './utility.js';
/* constants */
import {
ALPHA_NUM,
FORM_PARTS,
IDENT,
INPUT_EDIT,
LANG_PART,
NOT_SUPPORTED_ERR,
PS_ELEMENT_SELECTOR,
STRING,
SYNTAX_ERR
} from './constant.js';
const KEYS_FORM_PS_DISABLED = new Set([
...FORM_PARTS,
'fieldset',
'optgroup',
'option'
]);
const KEYS_INPUT_EDIT = new Set(INPUT_EDIT);
const REG_LANG_VALID = new RegExp(`^(?:\\*-)?${ALPHA_NUM}${LANG_PART}$`, 'i');
const REG_TAG_NAME = /[A-Z][\\w-]*/i;
/**
* 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 {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, opt = {}) => {
const { forgive, globalObject, warn } = opt;
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 or unknown pseudo-elements.
if (astName.startsWith('-webkit-')) {
if (warn) {
throw generateException(
`Unsupported pseudo-element ::${astName}`,
NOT_SUPPORTED_ERR,
globalObject
);
}
// Throw an error for unknown pseudo-elements if not forgiven.
} else if (!forgive) {
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.
* @throws {TypeError} If the AST does not contain a valid direction value.
* @returns {boolean} - True if the directionality matches, otherwise false.
*/
export const matchDirectionPseudoClass = (ast, node) => {
const { name } = ast;
// The :dir() pseudo-class requires a direction argument (e.g., "ltr").
if (!name) {
const type = name === '' ? '(empty String)' : getType(name);
throw new TypeError(`Unexpected ast type ${type}`);
}
// Get the computed directionality of the element.
const dir = getDirectionality(node);
// Compare the expected direction with the element's actual direction.
return name === dir;
};
/**
* Matches the :lang() pseudo-class against an element's language.
* @see https://datatracker.ietf.org/doc/html/rfc4647#section-3.3.1
* @param {object} ast - The AST object for the pseudo-class.
* @param {object} node - The element node to match against.
* @returns {boolean} - True if the language matches, otherwise false.
*/
export const matchLanguagePseudoClass = (ast, node) => {
const { name, type, value } = ast;
let langPattern;
// Determine the language pattern from the AST.
if (type === STRING && value) {
langPattern = value;
} else if (type === IDENT && name) {
langPattern = unescapeSelector(name);
}
// If no valid language pattern is provided, it cannot match.
if (typeof langPattern !== 'string') {
return false;
}
// Get the effective language attribute for the current node.
const elementLang = getLanguageAttribute(node);
// If the element has no language, it cannot match a specific pattern.
if (elementLang === null) {
return false;
}
// Handle the universal selector '*' for :lang.
if (langPattern === '*') {
// It matches any language unless attribute is not empty.
return elementLang !== '';
}
// Validate the provided language pattern structure.
if (!REG_LANG_VALID.test(langPattern)) {
return false;
}
// Build a regex for extended language range matching.
let matcherRegex;
if (langPattern.indexOf('-') > -1) {
// Handle complex patterns with wildcards and sub-tags (e.g., '*-US').
const [langMain, langSub, ...langRest] = langPattern.split('-');
const extendedMain =
langMain === '*' ? `${ALPHA_NUM}${LANG_PART}` : `${langMain}${LANG_PART}`;
const extendedSub = `-${langSub}${LANG_PART}`;
let extendedRest = '';
// Use a standard for loop for performance as per the rules.
for (let i = 0; i < langRest.length; i++) {
extendedRest += `-${langRest[i]}${LANG_PART}`;
}
matcherRegex = new RegExp(
`^${extendedMain}${extendedSub}${extendedRest}$`,
'i'
);
} else {
// Handle simple language patterns (e.g., 'en').
matcherRegex = new RegExp(`^${langPattern}${LANG_PART}$`, 'i');
}
// Test the element's language against the constructed regex.
return matcherRegex.test(elementLang);
};
/**
* Matches the :disabled and :enabled pseudo-classes.
* @param {string} astName - pseudo-class name
* @param {object} node - Element node
* @returns {boolean} - True if matched
*/
export const matchDisabledPseudoClass = (astName, node) => {
const { localName, parentNode } = node;
if (
!KEYS_FORM_PS_DISABLED.has(localName) &&
!isCustomElement(node, { formAssociated: true })
) {
return false;
}
let isDisabled = false;
if (node.disabled || node.hasAttribute('disabled')) {
isDisabled = true;
} else if (localName === 'option') {
if (
parentNode &&
parentNode.localName === 'optgroup' &&
(parentNode.disabled || parentNode.hasAttribute('disabled'))
) {
isDisabled = true;
}
} else if (localName !== 'optgroup') {
let current = parentNode;
while (current) {
if (
current.localName === 'fieldset' &&
(current.disabled || current.hasAttribute('disabled'))
) {
// The first <legend> in a disabled <fieldset> is not disabled.
let legend;
let element = current.firstElementChild;
while (element) {
if (element.localName === 'legend') {
legend = element;
break;
}
element = element.nextElementSibling;
}
if (!legend || !legend.contains(node)) {
isDisabled = true;
}
// Found the containing fieldset, stop searching up.
break;
}
current = current.parentNode;
}
}
if (astName === 'disabled') {
return isDisabled;
}
return !isDisabled;
};
/**
* Match the :read-only and :read-write pseudo-classes
* @param {string} astName - pseudo-class name
* @param {object} node - Element node
* @returns {boolean} - True if matched
*/
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 an attribute selector against an element.
* This function handles various attribute matchers like '=', '~=', '^=', etc.,
* and considers namespaces and case sensitivity based on document type.
* @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.
* @returns {boolean} - True if the attribute selector matches, otherwise false.
*/
export const matchAttributeSelector = (ast, node, opt = {}) => {
const {
flags: astFlags,
matcher: astMatcher,
name: astName,
value: astValue
} = ast;
const { check, forgive, globalObject } = opt;
// Validate selector flags ('i' or 's').
if (typeof astFlags === 'string' && !/^[is]$/i.test(astFlags) && !forgive) {
const css = generateCSS(ast);
throw generateException(
`Invalid selector ${css}`,
SYNTAX_ERR,
globalObject
);
}
const { attributes } = node;
// An element with no attributes cannot match.
if (!attributes || !attributes.length) {
return false;
}
// Determine case sensitivity based on document type and flags.
const contentType = node.ownerDocument.contentType;
let caseInsensitive;
if (contentType === 'text/html') {
if (typeof astFlags === 'string' && /^s$/i.test(astFlags)) {
caseInsensitive = false;
} else {
caseInsensitive = true;
}
} else if (typeof astFlags === 'string' && /^i$/i.test(astFlags)) {
caseInsensitive = true;
} else {
caseInsensitive = false;
}
// Prepare the attribute name from the selector for matching.
let astAttrName = unescapeSelector(astName.name);
if (caseInsensitive) {
astAttrName = astAttrName.toLowerCase();
}
// A set to store the values of attributes whose names match.
const attrValues = new Set();
// Handle namespaced attribute names (e.g., [*|attr], [ns|attr]).
if (astAttrName.indexOf('|') > -1) {
const { prefix: astPrefix, localName: astLocalName } =
parseAstName(astAttrName);
for (const item of attributes) {
let { name: itemName, value: itemValue } = item;
if (caseInsensitive) {
itemName = itemName.toLowerCase();
itemValue = itemValue.toLowerCase();
}
switch (astPrefix) {
case '': {
if (astLocalName === itemName) {
attrValues.add(itemValue);
}
break;
}
case '*': {
if (itemName.indexOf(':') > -1) {
const [, ...restItemName] = itemName.split(':');
const itemLocalName = restItemName.join(':').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 (itemName.indexOf(':') > -1) {
const [itemPrefix, ...restItemName] = itemName.split(':');
const itemLocalName = restItemName.join(':').replace(/^:/, '');
// Ignore the 'xml:lang' attribute.
if (itemPrefix === 'xml' && itemLocalName === 'lang') {
continue;
} else if (
astPrefix === itemPrefix &&
astLocalName === itemLocalName
) {
const namespaceDeclared = isNamespaceDeclared(astPrefix, node);
if (namespaceDeclared) {
attrValues.add(itemValue);
}
}
}
}
}
}
// Handle non-namespaced attribute names.
} else {
for (let { name: itemName, value: itemValue } of attributes) {
if (caseInsensitive) {
itemName = itemName.toLowerCase();
itemValue = itemValue.toLowerCase();
}
if (itemName.indexOf(':') > -1) {
const [itemPrefix, ...restItemName] = itemName.split(':');
const itemLocalName = restItemName.join(':').replace(/^:/, '');
// The attribute is starting with ':'.
if (!itemPrefix && astAttrName === `:${itemLocalName}`) {
attrValues.add(itemValue);
// Ignore the 'xml:lang' attribute.
} else if (itemPrefix === 'xml' && itemLocalName === 'lang') {
continue;
} else if (astAttrName === itemLocalName) {
attrValues.add(itemValue);
}
} else if (astAttrName === itemName) {
attrValues.add(itemValue);
}
}
}
if (!attrValues.size) {
return false;
}
// Prepare the value from the selector's RHS for comparison.
const { name: astIdentValue, value: astStringValue } = astValue ?? {};
let attrValue;
if (astIdentValue) {
if (caseInsensitive) {
attrValue = astIdentValue.toLowerCase();
} else {
attrValue = astIdentValue;
}
} else if (astStringValue) {
if (caseInsensitive) {
attrValue = astStringValue.toLowerCase();
} else {
attrValue = astStringValue;
}
} else if (astStringValue === '') {
attrValue = astStringValue;
}
// Perform the final match based on the specified matcher.
switch (astMatcher) {
case '=': {
return typeof attrValue === 'string' && attrValues.has(attrValue);
}
case '~=': {
if (attrValue && typeof attrValue === 'string') {
for (const value of attrValues) {
const item = new Set(value.split(/\s+/));
if (item.has(attrValue)) {
return true;
}
}
}
return false;
}
case '|=': {
if (attrValue && typeof attrValue === 'string') {
for (const value of attrValues) {
if (value === attrValue || value.startsWith(`${attrValue}-`)) {
return true;
}
}
}
return false;
}
case '^=': {
if (attrValue && typeof attrValue === 'string') {
for (const value of attrValues) {
if (value.startsWith(`${attrValue}`)) {
return true;
}
}
}
return false;
}
case '$=': {
if (attrValue && typeof attrValue === 'string') {
for (const value of attrValues) {
if (value.endsWith(`${attrValue}`)) {
return true;
}
}
}
return false;
}
case '*=': {
if (attrValue && typeof attrValue === 'string') {
for (const value of attrValues) {
if (value.includes(`${attrValue}`)) {
return true;
}
}
}
return false;
}
case null:
default: {
// This case handles attribute existence checks (e.g., '[disabled]').
return true;
}
}
};
/**
* match type selector
* @param {object} ast - AST
* @param {object} node - Element node
* @param {object} [opt] - options
* @param {boolean} [opt.check] - running in internal check()
* @param {boolean} [opt.forgive] - forgive undeclared namespace
* @returns {boolean} - result
*/
export const matchTypeSelector = (ast, node, opt = {}) => {
const astName = unescapeSelector(ast.name);
const { localName, namespaceURI, prefix } = node;
const { check, forgive, globalObject } = opt;
let { prefix: astPrefix, localName: astLocalName } = parseAstName(
astName,
node
);
if (
node.ownerDocument.contentType === 'text/html' &&
(!namespaceURI || namespaceURI === 'http://www.w3.org/1999/xhtml') &&
REG_TAG_NAME.test(localName)
) {
astPrefix = astPrefix.toLowerCase();
astLocalName = astLocalName.toLowerCase();
}
let nodePrefix;
let nodeLocalName;
// just in case that the namespaced content is parsed as text/html
if (localName.indexOf(':') > -1) {
[nodePrefix, nodeLocalName] = localName.split(':');
} else {
nodePrefix = prefix || '';
nodeLocalName = localName;
}
switch (astPrefix) {
case '': {
if (
!nodePrefix &&
!namespaceURI &&
(astLocalName === '*' || astLocalName === nodeLocalName)
) {
return true;
}
return false;
}
case '*': {
if (astLocalName === '*' || astLocalName === nodeLocalName) {
return true;
}
return false;
}
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) {
if (astLocalName === '*' || astLocalName === nodeLocalName) {
return true;
}
return false;
} else if (!forgive && !astNS) {
throw generateException(
`Undeclared namespace ${astPrefix}`,
SYNTAX_ERR,
globalObject
);
}
return false;
}
}
};

View File

@@ -0,0 +1,431 @@
/**
* 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,
DUO,
HEX,
ID_SELECTOR,
KEYS_LOGICAL,
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_CLASS_STATE = new Set([
'checked',
'closed',
'disabled',
'empty',
'enabled',
'in-range',
'indeterminate',
'invalid',
'open',
'out-of-range',
'placeholder-shown',
'read-only',
'read-write',
'valid'
]);
const KEYS_SHADOW_HOST = new Set(['host', 'host-context']);
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$/;
const U_FFFD = '\uFFFD';
/**
* 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 {
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}`);
// whitespace
} else if (/^[\n\r\f]/.test(item)) {
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++;
}
return selector
.replace(/\f|\r\n?/g, '\n')
.replace(/[\0\uD800-\uDFFF]|\\$/g, U_FFFD)
.replace(/\x26/g, ':scope');
};
/**
* Creates an Abstract Syntax Tree (AST) from a CSS selector string.
* @param {string} sel - The CSS selector string.
* @returns {object} The parsed AST object.
*/
export const parseSelector = sel => {
const selector = preprocess(sel);
// invalid selectors
if (/^$|^\s*>|,\s*$/.test(selector)) {
throw new DOMException(`Invalid selector ${selector}`, SYNTAX_ERR);
}
try {
const ast = cssTree.parse(selector, {
context: 'selectorList',
parseCustomProperty: true
});
return cssTree.toPlainObject(ast);
} 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.
* @returns {{branches: Array<object>, info: object}} An object containing the selector branches and info.
*/
export const walkAST = (ast = {}) => {
const branches = new Set();
const info = {
hasForgivenPseudoFunc: false,
hasHasPseudoFunc: false,
hasLogicalPseudoFunc: false,
hasNotPseudoFunc: false,
hasNthChildOfSelector: false,
hasNestedSelector: false,
hasStatePseudoClass: false
};
const opt = {
enter(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: {
if (KEYS_LOGICAL.has(node.name)) {
info.hasNestedSelector = true;
info.hasLogicalPseudoFunc = true;
if (node.name === 'has') {
info.hasHasPseudoFunc = true;
} else if (node.name === 'not') {
info.hasNotPseudoFunc = true;
} else {
info.hasForgivenPseudoFunc = true;
}
} else if (KEYS_PS_CLASS_STATE.has(node.name)) {
info.hasStatePseudoClass = true;
} else if (
KEYS_SHADOW_HOST.has(node.name) &&
Array.isArray(node.children) &&
node.children.length
) {
info.hasNestedSelector = 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;
}
default:
}
}
};
cssTree.walk(ast, opt);
if (info.hasNestedSelector === true) {
cssTree.findAll(ast, (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
};
};
/* Re-exported from css-tree. */
export { find as findAST, generate as generateCSS } from 'css-tree';

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,14 @@
export class DOMSelector {
constructor(window: Window, document: Document, opt?: object);
clear: () => void;
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>;
#private;
}
export type CheckResult = {
match: boolean;
pseudoElement: string | null;
};

View File

@@ -0,0 +1,77 @@
export const ATRULE: "Atrule";
export const ATTR_SELECTOR: "AttributeSelector";
export const CLASS_SELECTOR: "ClassSelector";
export const COMBINATOR: "Combinator";
export const IDENT: "Identifier";
export const ID_SELECTOR: "IdSelector";
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";
export const BIT_01: 1;
export const BIT_02: 2;
export const BIT_04: 4;
export const BIT_08: 8;
export const BIT_16: 16;
export const BIT_32: 32;
export const BIT_FFFF: 65535;
export const DUO: 2;
export const HEX: 16;
export const TYPE_FROM: 8;
export const TYPE_TO: -1;
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: 16;
export const SHOW_ALL: 4294967295;
export const SHOW_CONTAINER: 1281;
export const SHOW_DOCUMENT: 256;
export const SHOW_DOCUMENT_FRAGMENT: 1024;
export const SHOW_ELEMENT: 1;
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: "(?:-[A-Z\\d]+)*";
export const PSEUDO_CLASS: "(?:any-)?link|(?:first|last|only)-(?:child|of-type)|checked|empty|indeterminate|read-(?:only|write)|target";
export const ANB: "[+-]?(?:(?:0|[1-9]\\d*)n?|n)|(?:[+-]?(?:0|[1-9]\\d*))?n\\s*[+-]\\s*(?:0|[1-9]\\d*)";
export const COMBO: "\\s?[\\s>~+]\\s?";
export const DESCEND: "\\s?[\\s>]\\s?";
export const SIBLING: "\\s?[+~]\\s?";
export const LOGIC_IS: ":is\\(\\s*[^)]+\\s*\\)";
export 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 const SUB_TYPE: "\\[[^|\\]]+\\]|[#.:][\\w-]+";
export const SUB_TYPE_WO_PSEUDO: "\\[[^|\\]]+\\]|[#.][\\w-]+";
export const TAG_TYPE: "\\*|[A-Za-z][\\w-]*";
export const TAG_TYPE_I: "\\*|[A-Z][\\w-]*";
export const COMPOUND: "(?:\\*|[A-Za-z][\\w-]*|(?:\\*|[A-Za-z][\\w-]*)?(?:\\[[^|\\]]+\\]|[#.:][\\w-]+)+)";
export const COMPOUND_L: "(?:\\*|[A-Za-z][\\w-]*|(?:\\*|[A-Za-z][\\w-]*)?(?:\\[[^|\\]]+\\]|[#.:][\\w-]+|:is\\(\\s*[^)]+\\s*\\))+)";
export const COMPOUND_I: "(?:\\*|[A-Z][\\w-]*|(?:\\*|[A-Z][\\w-]*)?(?:\\[[^|\\]]+\\]|[#.:][\\w-]+)+)";
export const COMPOUND_WO_PSEUDO: "(?:\\*|[A-Za-z][\\w-]*|(?:\\*|[A-Za-z][\\w-]*)?(?:\\[[^|\\]]+\\]|[#.][\\w-]+)+)";
export const COMPLEX: "(?:\\*|[A-Za-z][\\w-]*|(?:\\*|[A-Za-z][\\w-]*)?(?:\\[[^|\\]]+\\]|[#.:][\\w-]+)+)(?:\\s?[\\s>~+]\\s?(?:\\*|[A-Za-z][\\w-]*|(?:\\*|[A-Za-z][\\w-]*)?(?:\\[[^|\\]]+\\]|[#.:][\\w-]+)+))*";
export 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 const HAS_COMPOUND: "has\\([\\s>]?\\s*(?:\\*|[A-Za-z][\\w-]*|(?:\\*|[A-Za-z][\\w-]*)?(?:\\[[^|\\]]+\\]|[#.][\\w-]+)+)\\s*\\)";
export 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 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 const FORM_PARTS: readonly string[];
export const INPUT_BUTTON: readonly string[];
export const INPUT_CHECK: readonly string[];
export const INPUT_DATE: readonly string[];
export const INPUT_TEXT: readonly string[];
export const INPUT_EDIT: readonly string[];
export const INPUT_LTR: readonly string[];
export const KEYS_LOGICAL: Set<string>;

View File

@@ -0,0 +1,64 @@
export class Finder {
constructor(window: object);
onError: (e: Error, opt?: {
noexcept?: boolean;
}) => void;
setup: (selector: string, node: object, opt?: {
check?: boolean;
noexcept?: boolean;
warn?: boolean;
}) => object;
clearResults: (all?: boolean) => void;
private _handleFocusEvent;
private _handleKeyboardEvent;
private _handleMouseEvent;
private _registerEventListeners;
private _processSelectorBranches;
private _correspond;
private _createTreeWalker;
private _getSelectorBranches;
private _getFilteredChildren;
private _collectNthChild;
private _collectNthOfType;
private _matchAnPlusB;
private _matchHasPseudoFunc;
private _evaluateHasPseudo;
private _matchLogicalPseudoFunc;
private _matchPseudoClassSelector;
private _evaluateHostPseudo;
private _evaluateHostContextPseudo;
private _matchShadowHostPseudoClass;
private _matchSelectorForElement;
private _matchSelectorForShadowRoot;
private _matchSelector;
private _matchLeaves;
private _traverseAllDescendants;
private _findDescendantNodes;
private _matchDescendantCombinator;
private _matchChildCombinator;
private _matchAdjacentSiblingCombinator;
private _matchGeneralSiblingCombinator;
private _matchCombinator;
private _traverseAndCollectNodes;
private _findPrecede;
private _findNodeWalker;
private _matchSelf;
private _findLineal;
private _findEntryNodesForPseudoElement;
private _findEntryNodesForId;
private _findEntryNodesForClass;
private _findEntryNodesForType;
private _findEntryNodesForOther;
private _findEntryNodes;
private _determineTraversalStrategy;
private _processPendingItems;
private _collectNodes;
private _getCombinedNodes;
private _matchNodeNext;
private _matchNodePrev;
private _processComplexBranchAll;
private _findChildNodeContainedByNode;
private _processComplexBranchFirst;
find: (targetType: string) => Set<object>;
#private;
}

View File

@@ -0,0 +1,16 @@
export function matchPseudoElementSelector(astName: string, astType: string, opt?: {
forgive?: boolean;
warn?: boolean;
}): void;
export function matchDirectionPseudoClass(ast: object, node: object): boolean;
export function matchLanguagePseudoClass(ast: object, node: object): boolean;
export function matchDisabledPseudoClass(astName: string, node: object): boolean;
export function matchReadOnlyPseudoClass(astName: string, node: object): boolean;
export function matchAttributeSelector(ast: object, node: object, opt?: {
check?: boolean;
forgive?: boolean;
}): boolean;
export function matchTypeSelector(ast: object, node: object, opt?: {
check?: boolean;
forgive?: boolean;
}): boolean;

View File

@@ -0,0 +1,14 @@
export function unescapeSelector(selector?: string): string;
export function preprocess(value: string): string;
export function parseSelector(sel: string): object;
export function walkAST(ast?: object): {
branches: Array<object>;
info: object;
};
export function compareASTNodes(a: object, b: object): number;
export function sortAST(asts: Array<object>): Array<object>;
export function parseAstName(selector: string): {
prefix: string;
localName: string;
};
export { find as findAST, generate as generateCSS } from "css-tree";

View File

@@ -0,0 +1,30 @@
export function getType(o: object): string;
export function verifyArray(arr: any[], type: string): any[];
export function generateException(msg: string, name: string, globalObject?: object): DOMException;
export function findNestedHas(leaf: object): object | null;
export function findLogicalWithNestedHas(leaf: object): object | null;
export function filterNodesByAnB(nodes: Array<object>, anb: {
a: number;
b: number;
reverse?: boolean;
}): Array<object>;
export function resolveContent(node: object): Array<object | boolean>;
export function traverseNode(node: object, walker: object, force?: boolean): object | null;
export function isCustomElement(node: object, opt?: object): boolean;
export function getSlottedTextContent(node: object): string | null;
export function getDirectionality(node: object): string | null;
export function getLanguageAttribute(node: object): string | null;
export function isContentEditable(node: object): boolean;
export function isVisible(node: object): boolean;
export function isFocusVisible(node: object): boolean;
export function isFocusableArea(node: object): boolean;
export function isFocusable(node: object): boolean;
export function getNamespaceURI(ns: string, node: object): string | null;
export function isNamespaceDeclared(ns?: string, node?: object): boolean;
export function isPreceding(nodeA: object, nodeB: object): boolean;
export function compareNodes(a: object, b: object): number;
export function sortNodes(nodes?: Array<object> | Set<object>): Array<object>;
export function concatNestedSelectors(selectors: Array<Array<string>>): string;
export function extractNestedSelectors(css: string): Array<Array<string>>;
export function initNwsapi(window: object, document: object): object;
export function filterSelector(selector: string, target: string): boolean;