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#L3747) | Returns elements with the class name. |
| [`byId(id, context)`](../src/nwsapi.mts#L3745) | Returns elements with the ID. Duplicate IDs are allowed by default. |
| [`byTag(tag, context)`](../src/nwsapi.mts#L3746) | Returns elements with the tag name. Use `*` for all elements. |
| [`closest(selectors, element, callback)`](../src/nwsapi.mts#L3753) | Returns the nearest match, starting with the element, or `null`. |
| [`compile(selector, mode, callback, relative)`](../src/nwsapi.mts#L3755) | Compiles a selector into a resolver function. This is an advanced API. |
| [`configure(option, clear)`](../src/nwsapi.mts#L3756) | Reads or changes options. Pass `true` as the second argument to clear compiled selectors. |
| [`emit(message, proto)`](../src/nwsapi.mts#L3758) | Reports an error using the configured error policy. |
| [`first(selectors, context, callback)`](../src/nwsapi.mts#L3749) | Returns the first matching descendant, or `null`. |
| [`install(all)`](../src/nwsapi.mts#L3764) | Replaces native selector methods. Pass `true` to also replace collection methods. |
| [`match(selectors, element, callback)`](../src/nwsapi.mts#L3750) | Returns whether the element matches. |
| [`registerCombinator(combinator, resolver)`](../src/nwsapi.mts#L3771) | Adds a relationship between elements using trusted resolver code. |
| [`registerOperator(operator, resolver)`](../src/nwsapi.mts#L3796) | Adds an attribute operator using a resolver with `p1`, `p2`, and `p3` fields. |
| [`registerSelector(name, rexp, func)`](../src/nwsapi.mts#L3818) | Adds a selector pattern and a compiler callback that returns `source` and `status`. |
| [`select(selectors, context, callback)`](../src/nwsapi.mts#L3751) | Returns an array of matching descendants, or an empty array. |
| [`uninstall()`](../src/nwsapi.mts#L3765) | Restores the native methods saved by `install()`. |
| [`byClass(cls, context)`](../src/nwsapi.mts#L3774) | Returns elements with the class name. |
| [`byId(id, context)`](../src/nwsapi.mts#L3772) | Returns elements with the ID. Duplicate IDs are allowed by default. |
| [`byTag(tag, context)`](../src/nwsapi.mts#L3773) | Returns elements with the tag name. Use `*` for all elements. |
| [`closest(selectors, element, callback)`](../src/nwsapi.mts#L3780) | Returns the nearest match, starting with the element, or `null`. |
| [`compile(selector, mode, callback, relative)`](../src/nwsapi.mts#L3782) | Compiles a selector into a resolver function. This is an advanced API. |
| [`configure(option, clear)`](../src/nwsapi.mts#L3783) | Reads or changes options. Pass `true` as the second argument to clear compiled selectors. |
| [`emit(message, proto)`](../src/nwsapi.mts#L3785) | Reports an error using the configured error policy. |
| [`first(selectors, context, callback)`](../src/nwsapi.mts#L3776) | Returns the first matching descendant, or `null`. |
| [`install(all)`](../src/nwsapi.mts#L3791) | Replaces native selector methods. Pass `true` to also replace collection methods. |
| [`match(selectors, element, callback)`](../src/nwsapi.mts#L3777) | Returns whether the element matches. |
| [`registerCombinator(combinator, resolver)`](../src/nwsapi.mts#L3798) | Adds a relationship between elements using trusted resolver code. |
| [`registerOperator(operator, resolver)`](../src/nwsapi.mts#L3823) | Adds an attribute operator using a resolver with `p1`, `p2`, and `p3` fields. |
| [`registerSelector(name, rexp, func)`](../src/nwsapi.mts#L3845) | Adds a selector pattern and a compiler callback that returns `source` and `status`. |
| [`select(selectors, context, callback)`](../src/nwsapi.mts#L3778) | Returns an array of matching descendants, or an empty array. |
| [`uninstall()`](../src/nwsapi.mts#L3792) | Restores the native methods saved by `install()`. |

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

| Member | Purpose |
| --- | --- |
| [`CFG`](../src/nwsapi.mts#L3733) | Contains the compiler syntax settings. |
| [`Config`](../src/nwsapi.mts#L3759) | Contains the active options. Use `configure()` to change them. |
| [`M_BODY`](../src/nwsapi.mts#L3736) | Contains the matching resolver body template. |
| [`M_TEST`](../src/nwsapi.mts#L3740) | Contains the matching resolver test template. |
| [`matchLambdas`](../src/nwsapi.mts#L3725) | Caches compiled matching functions, not DOM results. |
| [`matchResolvers`](../src/nwsapi.mts#L3728) | Caches matching plans, not DOM results. |
| [`N_BODY`](../src/nwsapi.mts#L3737) | Exposes the matching resolver body template. |
| [`N_TEST`](../src/nwsapi.mts#L3741) | Contains the alternate resolver test template. |
| [`Operators`](../src/nwsapi.mts#L3767) | Contains registered attribute operators. |
| [`S_BODY`](../src/nwsapi.mts#L3735) | Contains the selection resolver body template. |
| [`S_TEST`](../src/nwsapi.mts#L3739) | Contains the selection resolver test template. |
| [`selectLambdas`](../src/nwsapi.mts#L3726) | Caches compiled selection functions, not DOM results. |
| [`Selectors`](../src/nwsapi.mts#L3768) | Contains registered selector extensions. |
| [`selectResolvers`](../src/nwsapi.mts#L3729) | Caches selection plans, not DOM results. |
| [`Snapshot`](../src/nwsapi.mts#L3760) | Contains the document state and helpers used by compiled selectors. |
| [`Version`](../src/nwsapi.mts#L3762) | Contains the engine version string. |
| [`CFG`](../src/nwsapi.mts#L3760) | Contains the compiler syntax settings. |
| [`Config`](../src/nwsapi.mts#L3786) | Contains the active options. Use `configure()` to change them. |
| [`M_BODY`](../src/nwsapi.mts#L3763) | Contains the matching resolver body template. |
| [`M_TEST`](../src/nwsapi.mts#L3767) | Contains the matching resolver test template. |
| [`matchLambdas`](../src/nwsapi.mts#L3752) | Caches compiled matching functions, not DOM results. |
| [`matchResolvers`](../src/nwsapi.mts#L3755) | Caches matching plans, not DOM results. |
| [`N_BODY`](../src/nwsapi.mts#L3764) | Exposes the matching resolver body template. |
| [`N_TEST`](../src/nwsapi.mts#L3768) | Contains the alternate resolver test template. |
| [`Operators`](../src/nwsapi.mts#L3794) | Contains registered attribute operators. |
| [`S_BODY`](../src/nwsapi.mts#L3762) | Contains the selection resolver body template. |
| [`S_TEST`](../src/nwsapi.mts#L3766) | Contains the selection resolver test template. |
| [`selectLambdas`](../src/nwsapi.mts#L3753) | Caches compiled selection functions, not DOM results. |
| [`Selectors`](../src/nwsapi.mts#L3795) | Contains registered selector extensions. |
| [`selectResolvers`](../src/nwsapi.mts#L3756) | Caches selection plans, not DOM results. |
| [`Snapshot`](../src/nwsapi.mts#L3787) | Contains the document state and helpers used by compiled selectors. |
| [`Version`](../src/nwsapi.mts#L3789) | Contains the engine version string. |

</details>

Expand Down
29 changes: 28 additions & 1 deletion src/nwsapi.mts
Original file line number Diff line number Diff line change
Expand Up @@ -2179,7 +2179,7 @@

loop += mode || mode === null ? '{' + source + '}' : source

if (mode || (mode === null && selector.includes(':nth'))) {
if (mode || selector.includes(':nth')) {
loop += reNthElem.test(selector) ? 's.nthElement(null, 2);' : ''
loop += reNthType.test(selector) ? 's.nthOfType(null, 2);' : ''
}
Expand Down Expand Up @@ -2587,6 +2587,33 @@
: 'n>' + (b - 1)
: 'false'
}
// A constant index needs no index. nth(Element|OfType)
// builds the sibling list of the parent to number the
// element within it, which is the right trade for an an+b
// form that has to know where the element sits, and pure
// overhead for ':nth-child(3)', which only has to know
// whether three steps back runs out of siblings.
//
// Only for the -child forms: of-type has to compare the
// name of every sibling it steps over, and reading
// localName through the host on each one costs more than
// the list it avoids.
if (test == 'n==' + a && a >= 1 && !expr) {
test = type ? 'next' : 'previous'
source =
'n=1,o=e;' +
'while(n<=' +
a +
'&&(o=o.' +
test +
'ElementSibling))++n;' +
'if(n==' +
a +
'){' +
source +
'}'
break
}
expr = expr ? 'OfType' : 'Element'
type = type ? 'true' : 'false'
source =
Expand Down
35 changes: 35 additions & 0 deletions test/repo/e2e/upstream/fixtures/nth-constant.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<!doctype html>
<meta charset="utf-8">
<title>Constant sibling indexes</title>
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<body>
<script>
for (const pseudo of ['nth-child', 'nth-last-child', 'nth-of-type', 'nth-last-of-type']) {
test(t => {
const engine = NW.Dom;
const parent = document.createElement('main');
parent.innerHTML = '<i></i>text<!-- gap --><b></b><i></i><i></i>';
document.body.appendChild(parent);
t.add_cleanup(() => parent.remove());
for (let round = 0; round < 3; round++) {
for (const index of ['0', '-1', '1', '2', '3', '4', '5', '0n+3', '2n', 'n+3']) {
const selector = `:${pseudo}(${index})`;
// Zero is never a valid sibling index. Chromium currently returns
// every child for :nth-child(0), even without NWSAPI installed.
const expected = index === '0' ? [] : Array.from(parent.querySelectorAll(selector));
assert_array_equals(engine.select(selector, parent), expected, selector);
assert_equals(engine.first(selector, parent), expected[0] || null, selector);
for (const element of parent.children) {
assert_equals(engine.match(selector, element), expected.includes(element), selector);
}
}
parent.prepend(document.createElement('i'));
parent.lastElementChild.remove();
}
const orphan = document.createElement('i');
assert_true(engine.match(`:${pseudo}(1)`, orphan));
assert_false(engine.match(`:${pseudo}(2)`, orphan));
}, `${pseudo} handles constant indexes, formulas, detached nodes, and mutations`);
}
</script>
5 changes: 5 additions & 0 deletions test/repo/e2e/upstream/manifest.mts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,11 @@ export const manifest: Array<{
install?: boolean
legacyMap?: boolean
}> = [
{
path: '/_repo/test/repo/e2e/upstream/fixtures/nth-constant.html',
note: 'local WPT regression: constant sibling indexes agree with native queries after mutation',
install: false,
},
{
path: '/_repo/test/repo/e2e/upstream/fixtures/legacy-dom.html',
note: 'local WPT regression: legacy host reads agree with native selectors and refresh after mutations',
Expand Down
71 changes: 71 additions & 0 deletions test/repo/unit/nth-constant.test.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { JSDOM } from 'jsdom'
import { expect, test } from 'vitest'
import factory from '../../../src/nwsapi.js'

for (const pseudo of [
'nth-child',
'nth-last-child',
'nth-of-type',
'nth-last-of-type',
]) {
test(`${pseudo} preserves constant and formula results after mutations`, t => {
const { window } = new JSDOM(
'<main><i></i>text<!-- gap --><b></b><i></i><i></i></main>',
{
url: 'https://example.test/',
},
)
t.onTestFinished(() => window.close())
const { document } = window
const engine = factory(window)
const parent = document.querySelector('main')!
for (let round = 0; round < 3; round++) {
for (const index of [
'0',
'-1',
'1',
'2',
'3',
'4',
'5',
'0n+3',
'2n',
'n+3',
]) {
const selector = `:${pseudo}(${index})`
const elements = [...parent.children]
const expected = elements.filter(element => {
const siblings = pseudo.includes('of-type')
? elements.filter(
sibling => sibling.localName === element.localName,
)
: elements
const position = pseudo.includes('last')
? siblings.length - siblings.indexOf(element)
: siblings.indexOf(element) + 1
return index === '2n'
? position % 2 === 0
: index === 'n+3'
? position >= 3
: position === (index === '0n+3' ? 3 : Number(index))
})
expect(
engine.select(selector, parent),
`${selector}, round ${round}`,
).toEqual(expected)
expect(engine.first(selector, parent)).toBe(expected[0] ?? null)
for (const element of parent.children) {
expect(engine.match(selector, element)).toBe(
expected.includes(element),
)
}
}
parent.prepend(document.createElement('i'))
parent.lastElementChild!.remove()
parent.remove()
}
const orphan = document.createElement('i')
expect(engine.match(`:${pseudo}(1)`, orphan)).toBe(true)
expect(engine.match(`:${pseudo}(2)`, orphan)).toBe(false)
})
}