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
6 changes: 3 additions & 3 deletions .config/coverage.config.mts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
export const coverageThresholds = {
statements: 77,
branches: 65,
statements: 79,
branches: 70,
functions: 81,
lines: 75,
lines: 78,
} as const
1 change: 1 addition & 0 deletions .config/runtime.d.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export interface NwsapiEngine {
Config: Record<string, boolean>
matchLambdas: {
clear(): void
get(key: string): unknown
Expand Down
2 changes: 1 addition & 1 deletion assets/repo/coverage.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
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#L3133) | Returns elements with the class name. |
| [`byId(id, context)`](../src/nwsapi.mts#L3131) | Returns elements with the ID. Duplicate IDs are allowed by default. |
| [`byTag(tag, context)`](../src/nwsapi.mts#L3132) | Returns elements with the tag name. Use `*` for all elements. |
| [`closest(selectors, element, callback)`](../src/nwsapi.mts#L3139) | Returns the nearest match, starting with the element, or `null`. |
| [`compile(selector, mode, callback, relative)`](../src/nwsapi.mts#L3141) | Compiles a selector into a resolver function. This is an advanced API. |
| [`configure(option, clear)`](../src/nwsapi.mts#L3142) | Reads or changes options. Pass `true` as the second argument to clear compiled selectors. |
| [`emit(message, proto)`](../src/nwsapi.mts#L3144) | Reports an error using the configured error policy. |
| [`first(selectors, context, callback)`](../src/nwsapi.mts#L3135) | Returns the first matching descendant, or `null`. |
| [`install(all)`](../src/nwsapi.mts#L3150) | Replaces native selector methods. Pass `true` to also replace collection methods. |
| [`match(selectors, element, callback)`](../src/nwsapi.mts#L3136) | Returns whether the element matches. |
| [`registerCombinator(combinator, resolver)`](../src/nwsapi.mts#L3157) | Adds a relationship between elements using trusted resolver code. |
| [`registerOperator(operator, resolver)`](../src/nwsapi.mts#L3182) | Adds an attribute operator using a resolver with `p1`, `p2`, and `p3` fields. |
| [`registerSelector(name, rexp, func)`](../src/nwsapi.mts#L3204) | Adds a selector pattern and a compiler callback that returns `source` and `status`. |
| [`select(selectors, context, callback)`](../src/nwsapi.mts#L3137) | Returns an array of matching descendants, or an empty array. |
| [`uninstall()`](../src/nwsapi.mts#L3151) | Restores the native methods saved by `install()`. |
| [`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()`. |

<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#L3119) | Contains the compiler syntax settings. |
| [`Config`](../src/nwsapi.mts#L3145) | Contains the active options. Use `configure()` to change them. |
| [`M_BODY`](../src/nwsapi.mts#L3122) | Contains the matching resolver body template. |
| [`M_TEST`](../src/nwsapi.mts#L3126) | Contains the matching resolver test template. |
| [`matchLambdas`](../src/nwsapi.mts#L3111) | Caches compiled matching functions, not DOM results. |
| [`matchResolvers`](../src/nwsapi.mts#L3114) | Caches matching plans, not DOM results. |
| [`N_BODY`](../src/nwsapi.mts#L3123) | Exposes the matching resolver body template. |
| [`N_TEST`](../src/nwsapi.mts#L3127) | Contains the alternate resolver test template. |
| [`Operators`](../src/nwsapi.mts#L3153) | Contains registered attribute operators. |
| [`S_BODY`](../src/nwsapi.mts#L3121) | Contains the selection resolver body template. |
| [`S_TEST`](../src/nwsapi.mts#L3125) | Contains the selection resolver test template. |
| [`selectLambdas`](../src/nwsapi.mts#L3112) | Caches compiled selection functions, not DOM results. |
| [`Selectors`](../src/nwsapi.mts#L3154) | Contains registered selector extensions. |
| [`selectResolvers`](../src/nwsapi.mts#L3115) | Caches selection plans, not DOM results. |
| [`Snapshot`](../src/nwsapi.mts#L3146) | Contains the document state and helpers used by compiled selectors. |
| [`Version`](../src/nwsapi.mts#L3148) | Contains the engine version string. |
| [`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. |

</details>

Expand Down
104 changes: 104 additions & 0 deletions docs/legacy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
# Legacy DOM hosts

`LEGACY` enables DOM compatibility handling. It is not a JavaScript syntax
fallback, does not transpile this package, and does not supply missing built-ins.
An older browser must first be able to run the source and its dependencies.
See [runtime initialization](legacy-runtime.md) for optional built-in handling.

```js
NW.Dom.configure({ LEGACY: true });
```

Modern documents leave the flag off. Document setup enables it when the host
lacks `hasAttribute`, `getElementsByClassName`, `firstElementChild`, or a string
`localName`. This is a small capability check, not exhaustive detection of
every historical DOM quirk. Set the flag explicitly for subtler differences.

Document changes retain the flag. Changing it clears compiled resolvers and
query plans, because their host reads depend on the mode. Select legacy mode
before using an optional runtime feature that the environment lacks.

## Host reads

Compilation chooses a read table. Modern selections retain direct DOM reads;
single-node attribute matches guard the method. Legacy resolvers call helpers
through local aliases such as `hTag=s.tagOf`. Only used aliases are declared.
The pseudo-class rewrite preserves selector text inside generated string and
regexp literals; text such as `e.localName` is not itself a property read.

Legacy collection lookups filter non-elements. Fragment, ID, sibling, and
ancestor walks use node-level traversal when element-level APIs are absent.
Modern traversal keeps its direct reads. `:scope` no longer requires `classList`.

| Host behavior | Legacy handling |
| --- | --- |
| Attribute names differ from their reflected property names. | Try the markup name, then mappings such as `className`, `htmlFor`, and `colSpan`. |
| A missing attribute returns a property default. | Check the attribute node and its `specified` flag first. |
| URL reads return resolved values. | Probe the document for a raw read through the second argument, attribute node, or ordinary getter. |
| A boolean attribute returns a boolean. | Represent present `true` as an empty string and `false` as absent. The original spelling cannot be recovered. |
| `style` returns an object. | Read `style.cssText`; stringify other non-string values. |
| `hasAttribute` is absent. | Use attribute presence rather than a property default. |
| Wildcard tag collections contain comments. | Keep elements only. |
| Element traversal properties are absent. | Walk `firstChild`, `nextSibling`, `previousSibling`, and `parentNode`, skipping non-elements. |
| `localName` is absent. | Derive it from `nodeName`, removing a namespace prefix and lowercasing HTML names. |
| Class lookup is absent. | Scan descendants and test their class text. |
| `getAttributeNames` is absent. | Read specified names from the attribute collection. |
| `isConnected` is absent. | Walk to the root and check for a document. |
| A form control shadows the form's `id` property. | Read the form's ID attribute. |

The attribute handling follows David Mark's historical survey,
[A is for Attributes](https://web.archive.org/web/20091217095816/http://www.cinsoft.net/attributes.html).
The distinction remains useful when implementing custom DOM hosts; see
[attributes versus properties](https://jakearchibald.com/2024/attributes-vs-properties/).

## Limits

This extraction does not add browser features such as constraint validation
or media state. It does not claim compatibility with historical XML engines.
Use `select`, `match`, `first`, and `closest` when the host lacks the prototypes
needed by `install()`.

The `:disabled`, `:enabled`, `:required`, and `:defined` helpers use legacy
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.
- With #205, use the captured `sliceCall` in collection adapters. Keep legacy
element filtering and fallback traversal around those calls. The wider-arity
fallback in #187's `argsWith` should also use `sliceCall(args).concat(tail)`.

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.

```sh
pnpm install
pnpm run test:legacy
pnpm run test:node test/repo/unit/legacy-runtime.test.mts
pnpm run test:coverage
```

The Node tests use jsdom and a proxy host that hides modern APIs and simulates
attribute and collection quirks. The WPT runner uses the same host fixture in
Chromium to check native answers and DOM mutations. Coverage uses WPT for the
engine and Node for the adapter. These tests do not run historical browsers.

The suite retains the legacy tests from archived #167, including generated-code
inspection, mode changes, scoped queries, missing `WeakMap`, and non-element
collection entries. jsdom 30 supplies a separate selector implementation for
reference comparisons. Modern/legacy parity tests instead compare the same
engine in both modes and do not establish selector-spec correctness.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@
"test": "node scripts/repo/run.mts node_modules/vitest/vitest.mjs run --config .config/vitest.config.mts",
"type": "node scripts/repo/run.mts node_modules/typescript/bin/tsc --noEmit -p .config/tsconfig.check.json",
"test:node": "node scripts/repo/run.mts node_modules/vitest/vitest.mjs run --config .config/vitest.config.mts",
"test:legacy": "node scripts/repo/run.mts node_modules/vitest/vitest.mjs run --config .config/vitest.config.mts test/repo/unit/legacy-dom.test.mts",
"test:unit": "node scripts/repo/run.mts node_modules/vitest/vitest.mjs run --config .config/vitest.config.mts test/repo/unit",
"test:integration": "node scripts/repo/run.mts node_modules/vitest/vitest.mjs run --config .config/vitest.config.mts test/repo/integration",
"test:e2e": "pnpm run test:browser && pnpm run test:package && pnpm run test:upstream",
Expand Down
11 changes: 11 additions & 0 deletions scripts/repo/serve.mts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ import type { AddressInfo } from 'node:net'
import { createServer } from 'node:http'
import path from 'node:path'
import process from 'node:process'
import { stripTypeScriptTypes } from 'node:module'
import { readFile } from 'node:fs/promises'
import { REPO_ROOT as repoRoot } from './lib/paths.mts'
const docRoot = path.join(repoRoot, 'upstream', 'wpt')

Expand Down Expand Up @@ -125,6 +127,15 @@ async function serve(req: IncomingMessage, res: ServerResponse) {
}
info = await stat(filePath)
}
// Shared browser fixtures use the same TypeScript source as Node tests.
if (path.extname(filePath) === '.mts') {
const code = stripTypeScriptTypes(await readFile(filePath, 'utf8'))
send(res, 200, req.method === 'HEAD' ? '' : code, {
'content-type': 'text/javascript; charset=utf-8',
'cache-control': 'no-cache',
})
return
}
const type =
CONTENT_TYPES[path.extname(filePath).toLowerCase()] ||
'application/octet-stream'
Expand Down
Loading