Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 31 additions & 31 deletions docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,21 +11,21 @@ Query contexts default to the factory document when omitted. `closest()`, `first

| Method | Result |
| --- | --- |
| [`byClass(cls, context)`](../src/nwsapi.mts#L2972) | Returns elements with the class name. |
| [`byId(id, context)`](../src/nwsapi.mts#L2970) | Returns elements with the ID. Duplicate IDs are allowed by default. |
| [`byTag(tag, context)`](../src/nwsapi.mts#L2971) | Returns elements with the tag name. Use `*` for all elements. |
| [`closest(selectors, element, callback)`](../src/nwsapi.mts#L2978) | Returns the nearest match, starting with the element, or `null`. |
| [`compile(selector, mode, callback)`](../src/nwsapi.mts#L2980) | Compiles a selector into a resolver function. This is an advanced API. |
| [`configure(option, clear)`](../src/nwsapi.mts#L2981) | Reads or changes options. Pass `true` as the second argument to clear compiled selectors. |
| [`emit(message, proto)`](../src/nwsapi.mts#L2983) | Reports an error using the configured error policy. |
| [`first(selectors, context, callback)`](../src/nwsapi.mts#L2974) | Returns the first matching descendant, or `null`. |
| [`install(all)`](../src/nwsapi.mts#L2989) | Replaces native selector methods. Pass `true` to also replace collection methods. |
| [`match(selectors, element, callback)`](../src/nwsapi.mts#L2975) | Returns whether the element matches. |
| [`registerCombinator(combinator, resolver)`](../src/nwsapi.mts#L2996) | Adds a relationship between elements using trusted resolver code. |
| [`registerOperator(operator, resolver)`](../src/nwsapi.mts#L3021) | Adds an attribute operator using a resolver with `p1`, `p2`, and `p3` fields. |
| [`registerSelector(name, rexp, func)`](../src/nwsapi.mts#L3043) | Adds a selector pattern and a compiler callback that returns `source` and `status`. |
| [`select(selectors, context, callback)`](../src/nwsapi.mts#L2976) | Returns an array of matching descendants, or an empty array. |
| [`uninstall()`](../src/nwsapi.mts#L2990) | Restores the native methods saved by `install()`. |
| [`byClass(cls, context)`](../src/nwsapi.mts#L2982) | Returns elements with the class name. |
| [`byId(id, context)`](../src/nwsapi.mts#L2980) | Returns elements with the ID. Duplicate IDs are allowed by default. |
| [`byTag(tag, context)`](../src/nwsapi.mts#L2981) | Returns elements with the tag name. Use `*` for all elements. |
| [`closest(selectors, element, callback)`](../src/nwsapi.mts#L2988) | Returns the nearest match, starting with the element, or `null`. |
| [`compile(selector, mode, callback)`](../src/nwsapi.mts#L2990) | Compiles a selector into a resolver function. This is an advanced API. |
| [`configure(option, clear)`](../src/nwsapi.mts#L2991) | Reads or changes options. Pass `true` as the second argument to clear compiled selectors. |
| [`emit(message, proto)`](../src/nwsapi.mts#L2993) | Reports an error using the configured error policy. |
| [`first(selectors, context, callback)`](../src/nwsapi.mts#L2984) | Returns the first matching descendant, or `null`. |
| [`install(all)`](../src/nwsapi.mts#L2999) | Replaces native selector methods. Pass `true` to also replace collection methods. |
| [`match(selectors, element, callback)`](../src/nwsapi.mts#L2985) | Returns whether the element matches. |
| [`registerCombinator(combinator, resolver)`](../src/nwsapi.mts#L3006) | Adds a relationship between elements using trusted resolver code. |
| [`registerOperator(operator, resolver)`](../src/nwsapi.mts#L3031) | Adds an attribute operator using a resolver with `p1`, `p2`, and `p3` fields. |
| [`registerSelector(name, rexp, func)`](../src/nwsapi.mts#L3053) | Adds a selector pattern and a compiler callback that returns `source` and `status`. |
| [`select(selectors, context, callback)`](../src/nwsapi.mts#L2986) | Returns an array of matching descendants, or an empty array. |
| [`uninstall()`](../src/nwsapi.mts#L3000) | Restores the native methods saved by `install()`. |

<details>
<summary>Configuration</summary>
Expand Down Expand Up @@ -58,22 +58,22 @@ These exports support extensions and debugging. Prefer query methods and `config

| Member | Purpose |
| --- | --- |
| [`CFG`](../src/nwsapi.mts#L2958) | Contains the compiler syntax settings. |
| [`Config`](../src/nwsapi.mts#L2984) | Contains the active options. Use `configure()` to change them. |
| [`M_BODY`](../src/nwsapi.mts#L2961) | Contains the matching resolver body template. |
| [`M_TEST`](../src/nwsapi.mts#L2965) | Contains the matching resolver test template. |
| [`matchLambdas`](../src/nwsapi.mts#L2950) | Caches compiled matching functions, not DOM results. |
| [`matchResolvers`](../src/nwsapi.mts#L2953) | Caches matching plans, not DOM results. |
| [`N_BODY`](../src/nwsapi.mts#L2962) | Exposes the matching resolver body template. |
| [`N_TEST`](../src/nwsapi.mts#L2966) | Contains the alternate resolver test template. |
| [`Operators`](../src/nwsapi.mts#L2992) | Contains registered attribute operators. |
| [`S_BODY`](../src/nwsapi.mts#L2960) | Contains the selection resolver body template. |
| [`S_TEST`](../src/nwsapi.mts#L2964) | Contains the selection resolver test template. |
| [`selectLambdas`](../src/nwsapi.mts#L2951) | Caches compiled selection functions, not DOM results. |
| [`Selectors`](../src/nwsapi.mts#L2993) | Contains registered selector extensions. |
| [`selectResolvers`](../src/nwsapi.mts#L2954) | Caches selection plans, not DOM results. |
| [`Snapshot`](../src/nwsapi.mts#L2985) | Contains the document state and helpers used by compiled selectors. |
| [`Version`](../src/nwsapi.mts#L2987) | Contains the engine version string. |
| [`CFG`](../src/nwsapi.mts#L2968) | Contains the compiler syntax settings. |
| [`Config`](../src/nwsapi.mts#L2994) | Contains the active options. Use `configure()` to change them. |
| [`M_BODY`](../src/nwsapi.mts#L2971) | Contains the matching resolver body template. |
| [`M_TEST`](../src/nwsapi.mts#L2975) | Contains the matching resolver test template. |
| [`matchLambdas`](../src/nwsapi.mts#L2960) | Caches compiled matching functions, not DOM results. |
| [`matchResolvers`](../src/nwsapi.mts#L2963) | Caches matching plans, not DOM results. |
| [`N_BODY`](../src/nwsapi.mts#L2972) | Exposes the matching resolver body template. |
| [`N_TEST`](../src/nwsapi.mts#L2976) | Contains the alternate resolver test template. |
| [`Operators`](../src/nwsapi.mts#L3002) | Contains registered attribute operators. |
| [`S_BODY`](../src/nwsapi.mts#L2970) | Contains the selection resolver body template. |
| [`S_TEST`](../src/nwsapi.mts#L2974) | Contains the selection resolver test template. |
| [`selectLambdas`](../src/nwsapi.mts#L2961) | Caches compiled selection functions, not DOM results. |
| [`Selectors`](../src/nwsapi.mts#L3003) | Contains registered selector extensions. |
| [`selectResolvers`](../src/nwsapi.mts#L2964) | Caches selection plans, not DOM results. |
| [`Snapshot`](../src/nwsapi.mts#L2995) | Contains the document state and helpers used by compiled selectors. |
| [`Version`](../src/nwsapi.mts#L2997) | Contains the engine version string. |

</details>

Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@
"upstream:verify": "node scripts/run.mts scripts/git-partial-submodule.mts verify",
"test:attributes": "node scripts/run.mts node_modules/vitest/vitest.mjs run --config .config/vitest.config.mts test/attribute-parse-error.test.mts",
"test:attributes:browser": "NWSAPI_BROWSER=1 node scripts/run.mts node_modules/vitest/vitest.mjs run --config .config/vitest.config.mts test/attribute-strings-browser.test.mts",
"test:browser": "NWSAPI_BROWSER=1 node scripts/run.mts node_modules/vitest/vitest.mjs run --config .config/vitest.config.mts"
"test:browser": "NWSAPI_BROWSER=1 node scripts/run.mts node_modules/vitest/vitest.mjs run --config .config/vitest.config.mts",
"test:optimizer": "node scripts/run.mts node_modules/vitest/vitest.mjs run --config .config/vitest.config.mts test/optimizer-nesting.test.mts"
}
}
14 changes: 12 additions & 2 deletions src/nwsapi.mts
Original file line number Diff line number Diff line change
Expand Up @@ -1212,7 +1212,8 @@
// pairs, coloring breakage and other editors highlightning problems.
//

var // non-ascii chars
var parenthesized,
// non-ascii chars
noascii = '[^\\x00-\\x9f]',
// unicode chars
unicode = '\\\\[0-9a-fA-F]{1,6}',
Expand Down Expand Up @@ -1348,6 +1349,15 @@
// deepest localName in selector strings and then
// use it to retrieve all possible matching nodes
// that will be filtered by compiled resolvers
// The parenthesized part has to tolerate nesting. Written as
// '\x28[^\x29]+' it stops at the first ')', so a final compound
// holding a nested functional pseudo-class matches nothing at all, and
// a selector the optimizer cannot read is answered by testing every
// element in the context instead of the elements of one tag or class.
parenthesized = '\\x28[^\\x28\\x29]*(?:\\x29|$)'
parenthesized = '\\x28(?:[^\\x28\\x29]|' + parenthesized + ')*(?:\\x29|$)'
parenthesized = '\\x28(?:[^\\x28\\x29]|' + parenthesized + ')*(?:\\x29|$)'

reOptimizer = RegExp(
'(?:([.:#*]?)' +
'(' +
Expand All @@ -1356,7 +1366,7 @@
'(?:' +
':[-\\w]+|' +
'\\[[^\\]]+(?:\\]|$)|' +
'\\x28[^\\x29]+(?:\\x29|$)' +
parenthesized +
')*)$',
)

Expand Down
11 changes: 11 additions & 0 deletions test/optimizer-nesting.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Nested optimizer regression

Run with Node.js ≥ 22:

```sh
pnpm install
pnpm run test:optimizer
```

The tests check strict factory initialization and nested selector results,
including cached queries. jsdom supplies an independent reference engine.
47 changes: 47 additions & 0 deletions test/optimizer-nesting.test.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
const __dirname = import.meta.dirname
import { createRequire } from 'node:module'
const require = createRequire(import.meta.url)
const assert = require('node:assert/strict')
const { readFileSync } = require('node:fs')
const { join } = require('node:path')
import { test } from 'vitest'
const vm = require('node:vm')
const { JSDOM } = require('jsdom')
const source = readFileSync(join(__dirname, '../src/nwsapi.js'), 'utf8')

test('strict factory initialization does not leak parser variables', () => {
const context = {
module: { exports: {} as (host: unknown) => unknown },
exports: {},
}
vm.runInNewContext('"use strict";\n' + source, context)
const { window } = new JSDOM('<!doctype html><div></div>')
try {
assert.doesNotThrow(() => context.module.exports(window))
assert.equal(Object.hasOwn(context, 'parenthesized'), false)
} finally {
window.close()
}
})

for (const selector of [
'div:not(:nth-of-type(2n))',
'div:not(:nth-child(3))',
'div:is(.a):not(:where(.b))',
'div:not(:not(:not(span)))',
'div:has(:is(.a .b))',
]) {
test(selector, () => {
const { window } = new JSDOM(
'<!doctype html><div id=a class=a><span class=a><i class=b></i></span></div><div id=b class=b></div><div id=c class=a></div>',
)
try {
const nw = require('../src/nwsapi')(window)
const expected = [...window.document.querySelectorAll(selector)]
assert.deepEqual(nw.select(selector), expected)
assert.deepEqual(nw.select(selector), expected, 'cached selection')
} finally {
window.close()
}
})
}