diff --git a/assets/repo/coverage.svg b/assets/repo/coverage.svg index d968ea56..30521399 100644 --- a/assets/repo/coverage.svg +++ b/assets/repo/coverage.svg @@ -1 +1 @@ -coverage: 82%coverage82% +coverage: 83%coverage83% diff --git a/docs/api.md b/docs/api.md index 235f2ff4..a3f4f051 100644 --- a/docs/api.md +++ b/docs/api.md @@ -11,21 +11,21 @@ Query contexts default to the factory document when omitted. `closest()`, `first | Method | Result | | --- | --- | -| [`byClass(cls, context)`](../src/nwsapi.mts#L3864) | Returns elements with the class name. | -| [`byId(id, context)`](../src/nwsapi.mts#L3862) | Returns elements with the ID. Duplicate IDs are allowed by default. | -| [`byTag(tag, context)`](../src/nwsapi.mts#L3863) | Returns elements with the tag name. Use `*` for all elements. | -| [`closest(selectors, element, callback)`](../src/nwsapi.mts#L3870) | Returns the nearest match, starting with the element, or `null`. | -| [`compile(selector, mode, callback, relative)`](../src/nwsapi.mts#L3872) | Compiles a selector into a resolver function. This is an advanced API. | -| [`configure(option, clear)`](../src/nwsapi.mts#L3873) | Reads or changes options. Pass `true` as the second argument to clear compiled selectors. | -| [`emit(message, proto)`](../src/nwsapi.mts#L3875) | Reports an error using the configured error policy. | -| [`first(selectors, context, callback)`](../src/nwsapi.mts#L3866) | Returns the first matching descendant, or `null`. | -| [`install(all)`](../src/nwsapi.mts#L3881) | Replaces native selector methods. Pass `true` to also replace collection methods. | -| [`match(selectors, element, callback)`](../src/nwsapi.mts#L3867) | Returns whether the element matches. | -| [`registerCombinator(combinator, resolver)`](../src/nwsapi.mts#L3888) | Adds a relationship between elements using trusted resolver code. | -| [`registerOperator(operator, resolver)`](../src/nwsapi.mts#L3913) | Adds an attribute operator using a resolver with `p1`, `p2`, and `p3` fields. | -| [`registerSelector(name, rexp, func)`](../src/nwsapi.mts#L3935) | Adds a selector pattern and a compiler callback that returns `source` and `status`. | -| [`select(selectors, context, callback)`](../src/nwsapi.mts#L3868) | Returns an array of matching descendants, or an empty array. | -| [`uninstall()`](../src/nwsapi.mts#L3882) | Restores the native methods saved by `install()`. | +| [`byClass(cls, context)`](../src/nwsapi.mts#L3865) | Returns elements with the class name. | +| [`byId(id, context)`](../src/nwsapi.mts#L3863) | Returns elements with the ID. Duplicate IDs are allowed by default. | +| [`byTag(tag, context)`](../src/nwsapi.mts#L3864) | Returns elements with the tag name. Use `*` for all elements. | +| [`closest(selectors, element, callback)`](../src/nwsapi.mts#L3871) | Returns the nearest match, starting with the element, or `null`. | +| [`compile(selector, mode, callback, relative)`](../src/nwsapi.mts#L3873) | Compiles a selector into a resolver function. This is an advanced API. | +| [`configure(option, clear)`](../src/nwsapi.mts#L3874) | Reads or changes options. Pass `true` as the second argument to clear compiled selectors. | +| [`emit(message, proto)`](../src/nwsapi.mts#L3876) | Reports an error using the configured error policy. | +| [`first(selectors, context, callback)`](../src/nwsapi.mts#L3867) | Returns the first matching descendant, or `null`. | +| [`install(all)`](../src/nwsapi.mts#L3882) | Replaces native selector methods. Pass `true` to also replace collection methods. | +| [`match(selectors, element, callback)`](../src/nwsapi.mts#L3868) | Returns whether the element matches. | +| [`registerCombinator(combinator, resolver)`](../src/nwsapi.mts#L3889) | Adds a relationship between elements using trusted resolver code. | +| [`registerOperator(operator, resolver)`](../src/nwsapi.mts#L3914) | Adds an attribute operator using a resolver with `p1`, `p2`, and `p3` fields. | +| [`registerSelector(name, rexp, func)`](../src/nwsapi.mts#L3936) | Adds a selector pattern and a compiler callback that returns `source` and `status`. | +| [`select(selectors, context, callback)`](../src/nwsapi.mts#L3869) | Returns an array of matching descendants, or an empty array. | +| [`uninstall()`](../src/nwsapi.mts#L3883) | Restores the native methods saved by `install()`. |
Configuration @@ -60,22 +60,22 @@ These exports support extensions and debugging. Prefer query methods and `config | Member | Purpose | | --- | --- | -| [`CFG`](../src/nwsapi.mts#L3850) | Contains the compiler syntax settings. | -| [`Config`](../src/nwsapi.mts#L3876) | Contains the active options. Use `configure()` to change them. | -| [`M_BODY`](../src/nwsapi.mts#L3853) | Contains the matching resolver body template. | -| [`M_TEST`](../src/nwsapi.mts#L3857) | Contains the matching resolver test template. | -| [`matchLambdas`](../src/nwsapi.mts#L3842) | Caches compiled matching functions, not DOM results. | -| [`matchResolvers`](../src/nwsapi.mts#L3845) | Caches matching plans, not DOM results. | -| [`N_BODY`](../src/nwsapi.mts#L3854) | Exposes the matching resolver body template. | -| [`N_TEST`](../src/nwsapi.mts#L3858) | Contains the alternate resolver test template. | -| [`Operators`](../src/nwsapi.mts#L3884) | Contains registered attribute operators. | -| [`S_BODY`](../src/nwsapi.mts#L3852) | Contains the selection resolver body template. | -| [`S_TEST`](../src/nwsapi.mts#L3856) | Contains the selection resolver test template. | -| [`selectLambdas`](../src/nwsapi.mts#L3843) | Caches compiled selection functions, not DOM results. | -| [`Selectors`](../src/nwsapi.mts#L3885) | Contains registered selector extensions. | -| [`selectResolvers`](../src/nwsapi.mts#L3846) | Caches selection plans, not DOM results. | -| [`Snapshot`](../src/nwsapi.mts#L3877) | Contains the document state and helpers used by compiled selectors. | -| [`Version`](../src/nwsapi.mts#L3879) | Contains the engine version string. | +| [`CFG`](../src/nwsapi.mts#L3851) | Contains the compiler syntax settings. | +| [`Config`](../src/nwsapi.mts#L3877) | Contains the active options. Use `configure()` to change them. | +| [`M_BODY`](../src/nwsapi.mts#L3854) | Contains the matching resolver body template. | +| [`M_TEST`](../src/nwsapi.mts#L3858) | Contains the matching resolver test template. | +| [`matchLambdas`](../src/nwsapi.mts#L3843) | Caches compiled matching functions, not DOM results. | +| [`matchResolvers`](../src/nwsapi.mts#L3846) | Caches matching plans, not DOM results. | +| [`N_BODY`](../src/nwsapi.mts#L3855) | Exposes the matching resolver body template. | +| [`N_TEST`](../src/nwsapi.mts#L3859) | Contains the alternate resolver test template. | +| [`Operators`](../src/nwsapi.mts#L3885) | Contains registered attribute operators. | +| [`S_BODY`](../src/nwsapi.mts#L3853) | Contains the selection resolver body template. | +| [`S_TEST`](../src/nwsapi.mts#L3857) | Contains the selection resolver test template. | +| [`selectLambdas`](../src/nwsapi.mts#L3844) | Caches compiled selection functions, not DOM results. | +| [`Selectors`](../src/nwsapi.mts#L3886) | Contains registered selector extensions. | +| [`selectResolvers`](../src/nwsapi.mts#L3847) | Caches selection plans, not DOM results. | +| [`Snapshot`](../src/nwsapi.mts#L3878) | Contains the document state and helpers used by compiled selectors. | +| [`Version`](../src/nwsapi.mts#L3880) | Contains the engine version string. |
diff --git a/docs/legacy.md b/docs/legacy.md index 149961de..1907df39 100644 --- a/docs/legacy.md +++ b/docs/legacy.md @@ -63,21 +63,6 @@ accessors when needed. Their reference assertions pass without expected-failure markers. Relative `:has()` queries also use legacy parent traversal for sibling arguments. Other changes from #167 remain outside this extraction. -### Combining the extracted PRs - -Keep these integration changes when their prerequisites land: - -- With #194, retain one `classOf` implementation and route its fallback through - `attrOf(e, 'class')`. The modern read table should call the `classOf` helper; - the legacy table should call `legacyClassOf`. Expose those helpers under - distinct Snapshot keys so one does not overwrite the other. The legacy - pseudo-class rewrite must select `legacyClassOf` too. Use `read.id('e')` - for the ID comparison so legacy forms retain their attribute fallback. - -These are integration recipes, not claims that the prerequisites are included -in this standalone branch. Validate the combined tree with both the original -legacy assertions and each prerequisite's focused tests. - ## Validation Use Node.js 26 and pnpm ≥ 12.3.4. diff --git a/src/nwsapi.mts b/src/nwsapi.mts index 6369e164..bac5d4e0 100644 --- a/src/nwsapi.mts +++ b/src/nwsapi.mts @@ -1290,7 +1290,7 @@ return v + '.id' }, cls: function (v) { - return v + '.getAttribute("class")' + return 's.classOf(' + v + ')' }, up: function (v) { return v + '.parentElement' @@ -1316,7 +1316,7 @@ return helper('hId', 'idOf') + '(' + v + ')' }, cls: function (v) { - return helper('hCls', 'classOf') + '(' + v + ')' + return helper('hCls', 'legacyClassOf') + '(' + v + ')' }, up: function (v) { return helper('hUp', 'upOf') + '(' + v + ')' @@ -1337,7 +1337,7 @@ helpReads = function (code) { var reads = { localName: ['hTag', 'tagOf'], - className: ['hCls', 'classOf'], + className: ['hCls', 'legacyClassOf'], id: ['hId', 'idOf'], parentElement: ['hUp', 'upOf'], nextElementSibling: ['hNext', 'nextOf'], @@ -2278,18 +2278,17 @@ // id resolver case '#': match = selector.match(Patterns.id) - match[1] = escapeIdentifier(match[1]).replace( - REX.RegExpChar, - '\\$&', + // an exact comparison, which is what the selector asks for. + // escapeIdentifier turns the CSS escapes into JavaScript ones, so + // only the quote is escaped after it. + expr = escapeIdentifier(match[1]).replace( + /\\.|\x22/g, + function (part) { + return part == '"' ? '\\"' : part + }, ) source = - 'if((/^' + - match[1] + - '$/.test(' + - read.attr('e', 'id') + - '))){' + - source + - '}' + 'if((' + read.id('e') + '=="' + expr + '")){' + source + '}' break // class name resolver @@ -3753,11 +3752,12 @@ selectResolvers = createCache(), // passed to resolvers Snapshot: { + classOf: typeof classOf attrOf: typeof legacyAttrOf hasAttrOf: typeof legacyHasAttrOf tagOf: typeof legacyTagOf idOf: typeof legacyIdOf - classOf: typeof legacyClassOf + legacyClassOf: typeof legacyClassOf upOf: typeof legacyUpOf nextOf: typeof legacyNextOf prevOf: typeof legacyPrevOf @@ -3802,7 +3802,7 @@ hasAttrOf: legacyHasAttrOf, tagOf: legacyTagOf, idOf: legacyIdOf, - classOf: legacyClassOf, + legacyClassOf: legacyClassOf, upOf: legacyUpOf, nextOf: legacyNextOf, prevOf: legacyPrevOf, @@ -3828,6 +3828,7 @@ isDisabled: isDisabled, isModal: isModal, isFullscreen: isFullscreen, + classOf: classOf, isPictureInPicture: isPictureInPicture, isPopoverOpen: isPopoverOpen, isFocusable: isFocusable, diff --git a/test/repo/e2e/upstream/fixtures/property-reads.html b/test/repo/e2e/upstream/fixtures/property-reads.html new file mode 100644 index 00000000..6d2bd789 --- /dev/null +++ b/test/repo/e2e/upstream/fixtures/property-reads.html @@ -0,0 +1,30 @@ + + +Reflected class and ID reads + + + + diff --git a/test/repo/e2e/upstream/manifest.mts b/test/repo/e2e/upstream/manifest.mts index 5d728af7..d3b7b184 100644 --- a/test/repo/e2e/upstream/manifest.mts +++ b/test/repo/e2e/upstream/manifest.mts @@ -36,6 +36,11 @@ export const manifest: Array<{ install?: boolean legacyMap?: boolean }> = [ + { + path: '/_repo/test/repo/e2e/upstream/fixtures/property-reads.html', + note: 'local WPT regression: reflected classes, SVG base values, attribute fallbacks, and escaped IDs', + install: false, + }, { path: '/_repo/test/repo/e2e/upstream/fixtures/attribute-equality.html', note: 'local WPT regression: exact attribute values, escapes, case rules, and custom operators', diff --git a/test/repo/unit/property-reads.test.mts b/test/repo/unit/property-reads.test.mts new file mode 100644 index 00000000..a25470f7 --- /dev/null +++ b/test/repo/unit/property-reads.test.mts @@ -0,0 +1,53 @@ +import { JSDOM } from 'jsdom' +import { expect, test } from 'vitest' +import factory from '../../../src/nwsapi.js' + +test('class matching uses HTML and SVG reflections before reading attributes', t => { + const { window } = new JSDOM('') + t.onTestFinished(() => window.close()) + const engine = factory(window) + const resolver = engine.compile('.item', false)! + for (const className of ['item', { baseVal: 'item' }]) { + const target = { + className, + getAttribute() { + throw Error('Unnecessary attribute read') + }, + } + expect(resolver(target, undefined, undefined, false)).toBe(true) + } + let reads = 0 + const target = { + getAttribute(name: string) { + expect(name).toBe('class') + reads++ + return 'item' + }, + } + expect(resolver(target, undefined, undefined, false)).toBe(true) + expect(reads).toBe(1) +}) + +test('ID comparisons preserve escaped punctuation without attribute calls', t => { + const { window } = new JSDOM('') + t.onTestFinished(() => window.close()) + const engine = factory(window) + for (const [id, selector] of [ + ['a.b', '#a\\.b'], + ['a$b', '#a\\$b'], + ['a"b', '#a\\22 b'], + ['a"b', '#a\\"b'], + ['a\\b', '#a\\5c b'], + ]) { + const target = { + id, + getAttribute() { + throw Error('Unnecessary attribute read') + }, + } + const resolver = engine.compile(selector, false)! + expect(resolver(target, undefined, undefined, false)).toBe(true) + target.id += 'suffix' + expect(resolver(target, undefined, undefined, false)).toBe(false) + } +})