xpath-es 0.0.0-stage → 1.0.0-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/LICENSE +23 -0
  2. package/README.md +335 -2
  3. package/dist/ast.cjs +5 -0
  4. package/dist/ast.cjs.map +1 -0
  5. package/dist/ast.d.cts +79 -0
  6. package/dist/ast.d.ts +79 -0
  7. package/dist/ast.js +4 -0
  8. package/dist/ast.js.map +1 -0
  9. package/dist/clock.cjs +32 -0
  10. package/dist/clock.cjs.map +1 -0
  11. package/dist/clock.js +29 -0
  12. package/dist/clock.js.map +1 -0
  13. package/dist/compare.cjs +122 -0
  14. package/dist/compare.cjs.map +1 -0
  15. package/dist/compare.js +119 -0
  16. package/dist/compare.js.map +1 -0
  17. package/dist/compat.cjs +547 -0
  18. package/dist/compat.cjs.map +1 -0
  19. package/dist/compat.d.cts +194 -0
  20. package/dist/compat.d.ts +194 -0
  21. package/dist/compat.js +530 -0
  22. package/dist/compat.js.map +1 -0
  23. package/dist/compile.cjs +602 -0
  24. package/dist/compile.cjs.map +1 -0
  25. package/dist/compile.js +587 -0
  26. package/dist/compile.js.map +1 -0
  27. package/dist/dom.cjs +116 -0
  28. package/dist/dom.cjs.map +1 -0
  29. package/dist/dom.d.cts +100 -0
  30. package/dist/dom.d.ts +100 -0
  31. package/dist/dom.js +107 -0
  32. package/dist/dom.js.map +1 -0
  33. package/dist/errors.cjs +83 -0
  34. package/dist/errors.cjs.map +1 -0
  35. package/dist/errors.d.cts +39 -0
  36. package/dist/errors.d.ts +39 -0
  37. package/dist/errors.js +70 -0
  38. package/dist/errors.js.map +1 -0
  39. package/dist/functions.cjs +570 -0
  40. package/dist/functions.cjs.map +1 -0
  41. package/dist/functions.js +565 -0
  42. package/dist/functions.js.map +1 -0
  43. package/dist/index.cjs +39 -0
  44. package/dist/index.cjs.map +1 -0
  45. package/dist/index.d.cts +7 -0
  46. package/dist/index.d.ts +7 -0
  47. package/dist/index.js +7 -0
  48. package/dist/index.js.map +1 -0
  49. package/dist/lexer.cjs +264 -0
  50. package/dist/lexer.cjs.map +1 -0
  51. package/dist/lexer.js +261 -0
  52. package/dist/lexer.js.map +1 -0
  53. package/dist/order.cjs +351 -0
  54. package/dist/order.cjs.map +1 -0
  55. package/dist/order.js +345 -0
  56. package/dist/order.js.map +1 -0
  57. package/dist/parser.cjs +322 -0
  58. package/dist/parser.cjs.map +1 -0
  59. package/dist/parser.d.cts +9 -0
  60. package/dist/parser.d.ts +9 -0
  61. package/dist/parser.js +318 -0
  62. package/dist/parser.js.map +1 -0
  63. package/dist/program.cjs +180 -0
  64. package/dist/program.cjs.map +1 -0
  65. package/dist/program.d.cts +83 -0
  66. package/dist/program.d.ts +83 -0
  67. package/dist/program.js +174 -0
  68. package/dist/program.js.map +1 -0
  69. package/dist/ranked.cjs +192 -0
  70. package/dist/ranked.cjs.map +1 -0
  71. package/dist/ranked.js +187 -0
  72. package/dist/ranked.js.map +1 -0
  73. package/dist/runtime.cjs +596 -0
  74. package/dist/runtime.cjs.map +1 -0
  75. package/dist/runtime.js +585 -0
  76. package/dist/runtime.js.map +1 -0
  77. package/dist/steps.cjs +1137 -0
  78. package/dist/steps.cjs.map +1 -0
  79. package/dist/steps.js +1131 -0
  80. package/dist/steps.js.map +1 -0
  81. package/package.json +135 -5
package/LICENSE ADDED
@@ -0,0 +1,23 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2018 Cameron McCormack
4
+ Copyright (c) 2026 Bugsee
5
+
6
+ Permission is hereby granted, free of charge, to any person obtaining
7
+ a copy of this software and associated documentation files (the
8
+ "Software"), to deal in the Software without restriction, including
9
+ without limitation the rights to use, copy, modify, merge, publish,
10
+ distribute, sublicense, and/or sell copies of the Software, and to
11
+ permit persons to whom the Software is furnished to do so, subject to
12
+ the following conditions:
13
+
14
+ The above copyright notice and this permission notice shall be
15
+ included in all copies or substantial portions of the Software.
16
+
17
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
18
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
19
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
20
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
21
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
22
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
23
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,336 @@
1
- # Temporary Holding Version
1
+ # xpath-es
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ A modern, strictly typed XPath 1.0 engine for TypeScript and JavaScript, **release candidate** (`1.0.0-rc.0`, published under the npm dist-tag `next`).
4
+
5
+ The goal is an engine for libraries that evaluate XPath on untrusted XML, such as XML-signature and SAML stacks: the
6
+ Recommendation's answers by default (`strict` mode, with the deviations listed below), the answers of the widely used
7
+ [`xpath`](https://github.com/goto100/xpath) package on request (`upstream` mode), and bounded work on hostile input: every
8
+ evaluation has a work budget and a time limit, and an input that would need more is refused with a typed error. Some
9
+ expressions are inherently quadratic on some documents (see [Limits](#limits)); the engine refuses those rather than
10
+ running them. The API may still change before 1.0.0.
11
+
12
+ ## Status
13
+
14
+ This repository holds four things:
15
+
16
+ 1. **The engine** (`src/`, TypeScript, strict, no `any`): a lexer, a parser to an immutable AST, a closure compiler
17
+ and an interpreter, node-sets as sorted arrays with a one-pass document-order numbering, lazy axes, a work budget and
18
+ a wall-clock limit on every evaluation, typed errors (`XPathError` and its subclasses), and `compat.ts`, an entry
19
+ with upstream `xpath`'s API (`select`, `select1`, `parse`, `evaluate`, `useNamespaces`, the value classes and the
20
+ `is*` guards). `src/index.ts` exports both. **It is a release candidate and not yet adopted by anything**; the API may
21
+ change before 1.0.0.
22
+ 2. **A differential harness** (`test/`) that compares any candidate XPath engine with upstream `xpath` 0.0.36, the
23
+ oracle: random documents, a grammar-based expression fuzzer, mutation scenarios, and a corpus of the expressions
24
+ real XML-signature and SAML libraries evaluate.
25
+ 3. **A hostile-document suite** (`test/hostile.ts`, `bench/`) that runs shapes which make a naive engine superlinear
26
+ and fails a candidate that is too slow or gives a different answer.
27
+
28
+ ### What gates, and where the engine deliberately differs from upstream
29
+
30
+ **Supported DOMs:** `@xmldom/xmldom` >= 0.8.15 is gated (0.8.15 is its LTS and what the SAML libraries use; the 0.9
31
+ line is gated with the split below, tested with the latest 0.9.x); jsdom, happy-dom, linkedom and the browsers
32
+ (Chromium, Firefox, WebKit) are compared or smoke-tested. Older xmldom (the unscoped `xmldom` package, 0.6 and
33
+ earlier) is not supported or tested: it is unmaintained and has known vulnerabilities. The engine does not check which
34
+ DOM it is given; it takes any DOM-shaped nodes.
35
+
36
+ | DOM | Status | Gate |
37
+ |---|---|---|
38
+ | `@xmldom/xmldom` 0.8.15 (what the SAML libraries use) | **gated** | zero differences from upstream, in every scenario and for random expressions, except class 5 below (`CLASSIFY=ns-duplicate`) |
39
+ | `@xmldom/xmldom` 0.9 | **gated, split** | `CLASSIFY=all`: no difference where upstream answers and the engine answers, except class 5; every other difference must fall in a documented class, and an unclassified difference fails |
40
+ | jsdom, happy-dom, linkedom | compared, **not gating** | the nightly run reports the differences; it fails only on a crash |
41
+
42
+ The documented classes (xmldom 0.9):
43
+
44
+ - **namespace nodes** and **a context inside a removed subtree**: upstream throws
45
+ (`Cannot read properties of null (reading 'childNodes')`), because it orders nodes with the DOM's own
46
+ `compareDocumentPosition`, which xmldom 0.9 cannot evaluate for a detached node or for upstream's synthetic namespace
47
+ nodes. The engine numbers the tree itself and answers.
48
+ - **a node-set spanning several trees**: upstream returns an arbitrary order; the engine throws `XPathOrderError`
49
+ (a safe failure).
50
+ - **class 5, the namespace axis over duplicate contexts** (xmldom 0.8 and 0.9): upstream makes a new namespace-node
51
+ object on every evaluation, so a context reached twice yields the same namespace node twice, and its node-set (or a
52
+ `count()` of it) holds duplicates; the engine, which removes duplicates between steps, returns it once. A node-set
53
+ holds each node once, so the engine is right by the Recommendation. The check is rigorous: the expression must
54
+ contain `namespace::`, both engines must answer, and the engine's answer must equal that of upstream with its
55
+ namespace nodes interned per (element, prefix) (`test/support/interned-oracle.ts`, upstream's own source with two
56
+ creation sites changed), for node-sets and for scalars alike. No consumer (samlify, xml-crypto, xml-encryption)
57
+ uses the namespace axis.
58
+
59
+ Why the other DOMs do not gate: upstream delegates ordering to the DOM there and inherits its defects (linkedom's
60
+ `compareDocumentPosition` is not a document order, happy-dom and jsdom misorder or drop attributes). The engine
61
+ returns true document order, and throws `XPathOrderError` for node-sets of several trees.
62
+
63
+ Other deliberate differences from upstream, in either mode:
64
+
65
+ - Error *messages* differ where upstream throws an accidental `TypeError` or `ReferenceError`; the engine throws typed
66
+ errors, and the fuzzers compare the *category* of two errors (syntax, unresolvable name, type, no document order,
67
+ limit), not the text.
68
+ - **Class 6, the limits.** The engine stops an evaluation at its default work budget or time limit (below); upstream
69
+ would have gone on. When that is the only difference, the fuzzers re-run the engine with the limits off and require
70
+ upstream's answer exactly (`test/support/classify.ts`, `isLimitPolicy`). The expressions this happens to are
71
+ quadratic or worse on a document with a parent of thousands of children.
72
+ - Plain-object resolver maps (`namespaces`, `functions`, `variables`) are read by own property only: a prefix called
73
+ `constructor`, or a function called `toString`, is not a member of `Object.prototype`.
74
+ - In upstream mode a namespace resolver (a function) is asked at every name test, as upstream asks it, but it must not
75
+ change the document; a plain map (`useNamespaces`) is data and costs nothing. In strict mode each prefix is asked once
76
+ per evaluation, before anything is walked (section 1: the declarations of the expression context are fixed), so a
77
+ resolver never runs during the walk and the one-pass axes stay on. Custom functions and variable resolvers may: the engine drops the numbering and the sibling ranks it made
78
+ when one has run, and orders a handful of nodes locally in the meantime (`FEW_NODES` in `src/order.ts`).
79
+ - Document-only subexpressions (no context node, no position, no variable, no function the engine does not know) are
80
+ evaluated once per document: `//a[//b[//c]]` computes `//b[//c]` once, not once per `a`.
81
+
82
+ ## Modes: upstream and strict
83
+
84
+ `new XPathProgram(source, { mode })`, `select(expression, node, false, { mode })`, `parse(expression, { mode })` and the
85
+ `mode` option of the evaluator take `'strict'` (the default) or `'upstream'`.
86
+
87
+ - **`upstream`** (opt in with `mode: 'upstream'`) is the adoption gate: the node-sets and the values existing consumers (samlify, xml-crypto,
88
+ xml-encryption) have always seen from `xpath` 0.0.36, quirks included.
89
+ - **`strict`** (the default) follows the Recommendation (16 November 1999), except where [the list below](#where-strict-mode-differs-from-the-recommendation)
90
+ says otherwise. Its answers are checked against the text of the Recommendation (`test/unit/strict-spec.test.ts`,
91
+ `test/unit/strict-conformance.test.ts`), against an independent reference evaluator written from the Recommendation
92
+ that shares no code with the engine (`test/strict-oracle.ts`, random documents and location paths, on xmldom 0.8 and
93
+ 0.9 in CI and on jsdom, happy-dom and linkedom in the nightly run), never against upstream; and
94
+ `test/mode-agreement.ts` checks that on every random expression that uses none of the deviating features the two
95
+ modes agree, so strict mode does not regress where upstream is right.
96
+
97
+ ### Known deviations from XPath 1.0 inherited from upstream (`upstream` mode)
98
+
99
+ Each one is pinned in both modes in `test/unit/modes.test.ts`.
100
+
101
+ | | `upstream` mode | `strict` mode (default) |
102
+ |---|---|---|
103
+ | `following::` (2.2) | includes the context node's descendants and, when it has children, leaves out its following siblings; `/r/a[1]/following::e` finds nothing | not the descendants; the following siblings of the node and of each ancestor with their subtrees (for an attribute or namespace node, its element's children too) |
104
+ | `preceding::` (2.2) | includes the ancestors and the document node | excludes ancestors; an attribute or namespace node stands where its element stands |
105
+ | `id()` (4.1) | asks the DOM's `getElementById` for each token (a node-set argument is joined as the nodes' serialisations: `id(//@ref)` names nothing) | the tokens of each node's string-value; at most one element per distinct token, the first in document order, from one index per evaluation |
106
+ | name tests (2.3) | case-insensitive wherever the DOM answers `implementation.hasFeature('HTML', '2.0')` with true, which xmldom, jsdom and browsers do for every document: `//E` finds `<e/>` in an XML document | case-sensitive in an XML document; case-insensitive in an HTML document (`contentType` `text/html`) |
107
+ | `@*`, `attribute::*` (5.3) | include `xmlns` / `xmlns:p` declarations | namespace declarations are not attributes |
108
+ | `lang()` (4.3) | the context node's own `xml:lang` on a DOM that answers `''` for a missing attribute (xmldom), the ancestors' otherwise; case-sensitive | the nearest ancestor-or-self `xml:lang`, ignoring case, with `-` sublanguages |
109
+ | `number()` of a node-set (4.4) | through JavaScript's `Number()`: an empty node-set is 0, `0x10` is 16 | the string-value of the first node, converted by the grammar: an empty node-set is NaN |
110
+ | string to number (4.4) | `1.` is NaN; white space is JavaScript's (a no-break space is trimmed) | the grammar of 4.4: `1.` and `.5` are numbers, `+1`, `1e3`, `0x10`, a lone `.` are NaN; only space, tab, CR and LF around it |
111
+ | number to string (4.2) | small numbers are written by padding digits and the sign goes wrong: `string(-0.00000001)` is `0.0000000-1` | no exponent, the sign kept: `-0.00000001`; both zeros print `0` |
112
+ | `substring()` (4.2) | JavaScript's `substring()` on rounded bounds: reversed bounds swap, NaN is 0: `substring("12345", 3, -1)` is `2`, `substring("12345", 0 div 0)` is `12345` | positions `round(start)` up to `round(start) + round(length)`, NaN gives the empty string, infinities clamp |
113
+ | `string-length()`, `substring()` (4.2) | UTF-16 code units | characters |
114
+ | namespace axis (5.4) | each evaluation makes new node objects, so a context reached twice yields a namespace node twice (class 5); `xmlns=""` makes a node; a namespace node has no parent | one node per element and prefix, held once; no node for `xmlns=""`; the parent is the element |
115
+ | axis names, NUL (3.7) | an unknown axis name selects nothing (`foo::bar` is `[]`); a NUL ends the expression | a syntax error |
116
+ | an unresolvable prefix (2.3) | an error only when a node test meets a node: `//e[count(following-sibling::p:x)=0]` is fine when `e` has no following sibling | an error whenever the expression is evaluated |
117
+ | the `xml` prefix (2.3) | unresolved unless the resolver knows it | bound to `http://www.w3.org/XML/1998/namespace` without a declaration: `//@xml:lang` works with no resolver |
118
+ | principal node type (2.3) | `*` and a name test accept attributes, elements and namespace nodes on every axis: `@x/self::*` finds `@x`, `count(//@x/descendant-or-self::*)` counts the attribute | the principal node type of the axis: attributes on `attribute::`, namespace nodes on `namespace::`, elements on every other |
119
+ | `node()` (2.3) | rejects namespace nodes and a document fragment | accepts every node of the data model: namespace nodes, a document fragment (the root of a detached tree) |
120
+ | namespace axis order (5.4) | two inherited bindings cannot be ordered: `//b/namespace::*`, `//namespace::*` and `count(//b/namespace::*[name()="xml"])` throw `XPathOrderError` (class 5) | in document order: `xml`, the element's own declarations, then those inherited from its parent outwards; an element, then its namespace nodes, then its attributes |
121
+ | a boolean and a node-set (3.4) | `false() < //b` compares the boolean, as a number, with each node's number (a boolean on the right converts the node-set to a boolean) | both orders convert the node-set to a boolean |
122
+ | `local-name()` (4.1) | falls back to the node's name: `#text`, `#document`, `#comment` | empty for a node without an expanded name; only an element, attribute, processing instruction or namespace node has one |
123
+ | white space and numbers (3.7) | none between a name and `(` or `::` (`count (//a)` is a syntax error), `1.` is not a number, `,` and `$` do not make `*`, `div` an operand (`concat("x", *)`, `$div`) | all of them are lexed as the grammar says |
124
+ | text nodes (5.7) | one node per DOM Text or CDATASection node: `<a>x<![CDATA[cd]]>w</a>` has three `text()` nodes, and `string(//a/text())` is `x` | one text node per run of adjacent Text and CDATASection siblings, represented by the first DOM node of the run, whose string-value is the whole run: one `text()` node, `string(//a/text())` is `xcdw` |
125
+ | the XML declaration (5) | some DOMs (xmldom) keep it as a processing instruction child of the document: `count(/processing-instruction())` is 1 for `<?xml version="1.0"?><a/>` | not a node |
126
+ | `/` (2) | the owner document, even of a detached element | the root of the tree that contains the context node: for a detached element or a document fragment, its topmost ancestor |
127
+ | `translate()`, `contains()`, `starts-with()`, `substring-before()`, `substring-after()` (4.2) | UTF-16 code units: `translate("😀", "😁", "x")` is `x` followed by a lone surrogate | characters; a match cannot begin or end inside a surrogate pair |
128
+
129
+ **`id()` cannot detect duplicate IDs in either mode.** When two elements have the same ID, `id()` answers the first in
130
+ the order the DOM looks up (document order for the DOMs tested); the Recommendation leaves duplicates undefined, and
131
+ Blink, WebKit and Gecko answer the first as well. The ID attributes are `id` and, in strict mode, `xml:id`.
132
+
133
+ Strict is the default, so code written against `xpath` that relied on a quirk above must ask for `mode: 'upstream'`.
134
+
135
+ ### Where strict mode differs from the Recommendation
136
+
137
+ Strict mode is the Recommendation except for these, each a choice or a limit and none an accident:
138
+
139
+ - **An HTML document** (`contentType` `text/html`) matches names without regard to case, as HTML does; an XML document never does.
140
+ - **Text nodes are represented by DOM nodes.** `text()` yields one node per run of adjacent Text and CDATASection siblings, and
141
+ that node is the first DOM node of the run, so identity, document order and node-set semantics stay those of the DOM. The
142
+ string-value (`string()`, `=`, `contains()`, `sum()`) is the whole run. Code that takes the selected DOM node and reads its own
143
+ `nodeValue` or `data` sees the first piece only. This matters for signed values: a NameID `victim@example.test` with a CDATA
144
+ tail `.evil.test` canonicalises to the same bytes as the same text without CDATA; strict mode reads it in full through
145
+ `string()`, where `upstream` mode's `string(//NameID/text())` is only the first piece.
146
+ - **The order of namespace nodes** within an element is not specified by the Recommendation; strict mode orders them as the
147
+ table says.
148
+ - **DocumentType, Entity, EntityReference and Notation nodes** are not nodes of the model: a node test never selects them.
149
+ - **`id()`** answers one element per token, the first in document order; duplicate IDs are not detected. The ID attributes are
150
+ `id` and `xml:id`.
151
+ - **Limits** (below) refuse an evaluation that needs more than the work budget or the time limit, which the Recommendation
152
+ does not know of.
153
+ - **Predicates other than `[k]` and `[last()]` after `following::` or `preceding::`** are evaluated for each context node, as
154
+ the Recommendation defines them; they are not made linear (below).
155
+
156
+ **A difference between the modes on the signature path.** A SAML consumer looks for the element that carries the signed ID with
157
+ an expression such as `//*[@*[local-name(.)='ID']='<id>']`. In `upstream` mode (and in `xpath` itself) an element that only
158
+ declares `xmlns:ID="<id>"` also matches, because upstream lists namespace declarations as attributes; in strict mode it does
159
+ not (section 5.3: a declaration is not an attribute). A response that carries such a decoy next to the signed element is
160
+ therefore rejected by a consumer in `upstream` mode (two matches) and accepted in strict mode (one match, the signed
161
+ element). `test/unit/strict-conformance.test.ts` pins both behaviours.
162
+
163
+ Cost: strict `following::` and `preceding::` are linear over a node-set of contexts when there is no positional predicate and
164
+ when `[k]` or `[last()]` is the first predicate; other positional predicates are not. The bound holds for an evaluation
165
+ in which no variable or function callback is in use (`variables` or `functions` supplied and used by the expression, whatever
166
+ the value): with one, each context node is evaluated on its own and the work budget can refuse the expression. Without a positional predicate they
167
+ are one pass over the tree (`preceding::` of a set is everything before the last context that is not an ancestor of it).
168
+ With `[k]` or `[last()]` as the first predicate they are one pass over the tree, then a lookup per context node by
169
+ document-order number (a binary search; the ancestors of each context are kept on a stack while the contexts are taken
170
+ in document order), so that the position stays relative to each context's own axis. `//b/preceding::*` over 3,000 `b`
171
+ elements takes 21,000 units (it took 9 million), `//a/following::b[1]` 63,000. A predicate that is positional in another
172
+ way (`[position() < 3]`, `[last() - 1]`, a positional predicate that is not the first) is still evaluated per context node
173
+ and costs a walk of the axis for each, so it can reach the budget over thousands of contexts; raise the budget for such a call.
174
+
175
+ ## Limits
176
+
177
+ Every evaluation has a work budget and a time limit, so that an expression on a hostile document stops with a typed
178
+ error (`XPathBudgetError`, code `BUDGET`; `XPathTimeoutError`, code `TIMEOUT`; `XPathLimitError`, code `LIMIT`) instead
179
+ of running for minutes. A *unit* is a node visited, a node-set member sorted, one evaluation of a predicate, an operand of a boolean, arithmetic or
180
+ comparison chain, a function call or a variable, or (for string functions) sixteen characters scanned.
181
+
182
+ | | default | measured |
183
+ |---|---|---|
184
+ | `DEFAULT_BUDGET` | 5,000,000 units | the most one evaluation of a signed SAML login response took was **14,232** units, for a response with a 2,000-value group list (7 units per value, about 1 ms); the hostile documents of the gate (256 KiB each) take at most **328,000** units (18 ms). The default is 350 times the first and 15 times the second. A hostile expression is stopped after at most 5,000,000 units: 49 ms for `//*` with 20,000 predicates `[1=1]` on 100,000 nodes (it ran 25 s before predicate evaluations were counted), up to the time limit where a unit costs more |
185
+ | `DEFAULT_TIMEOUT_MS` | 1,000 ms | the backstop for work the units do not model; the clock is read every 4,096 units and every 512 charges, so work that charges little cannot keep the limit from being looked at |
186
+ | namespace nodes | 262,144 per evaluation | a document nested 8,000 levels deep with a new prefix at every level made 32 million nodes (4 GB) before |
187
+ | `id()` | 10,000 distinct tokens per call | 40,000 tokens took 39 s before |
188
+ | strings | 16,777,216 characters per string | nested `concat()` doubled a string per level |
189
+
190
+ They apply to every entry: `XPathProgram.run`, `select`, `select1`, `selectWithResolver`, `useNamespaces` (as
191
+ `{ budget, timeoutMs }`, also as defaults: `useNamespaces(map, { budget })`) and the evaluator object. `0` turns a limit
192
+ off, `Infinity` works as well; anything that is not a number of at least 0 (a string such as `'0'`, `NaN`, `null`, a
193
+ negative number) is refused with `XPathContextError`, because it would otherwise turn the limit off silently.
194
+ `bench/limits.ts` prints the measurements. A caller whose legitimate documents are larger raises the limits.
195
+
196
+ The parse cache of the compat entry is a byte-bounded LRU (an estimate of 16 MiB retained in all, at 512 bytes per character
197
+ of source, and 512 programs) and does not keep an expression longer than 2,048 characters.
198
+
199
+ Some expressions stay expensive on some documents, and the limits refuse them: `//a[count(preceding::a)=1]` over 50,000
200
+ siblings, a union of 20,000 named steps, and a predicate such as `following::*[position() < 3]` over thousands of contexts.
201
+ Raise the budget for a call that is meant to do that much work.
202
+
203
+ ## Errors
204
+
205
+ Errors are typed (`XPathError` and its subclasses, each with a stable `code`). The package ships as ES modules and as
206
+ CommonJS, so a program that loads both has two copies of the classes and `instanceof` fails across them. Use
207
+ `isXPathError(value)`, which recognises an error from either copy (by a brand registered with `Symbol.for`), and the
208
+ `code` property, which is the same string in both.
209
+
210
+ ## Running it
211
+
212
+ Node >= 22.18 (the TypeScript sources run directly, by type stripping; `npm run build` makes the plain JavaScript in
213
+ `dist/`, see [Build and compatibility](#build-and-compatibility)).
214
+
215
+ The one dependency that is a release tarball (`xpath-fork`, a dev dependency) needs `allow-remote=root` under npm 12 (EALLOWREMOTE
216
+ otherwise); the committed `.npmrc` sets it, and npm 10 and 11 ignore it. The integrity of the tarball is pinned in
217
+ `package-lock.json`.
218
+
219
+ ```sh
220
+ npm ci
221
+ npm run check # tsc (TypeScript 7, every strict flag), oxlint (type-aware), oxfmt --check
222
+ npm run test:unit # unit tests of the engine (node:test)
223
+ npm run build # dist/: ES modules and CommonJS with declarations, same compiler and strict options
224
+ npm run test:harness # the harness tests itself on candidates whose verdict is known
225
+ npm run test:differential # the engine against upstream xpath (documents, scenarios, random expressions)
226
+ npm run test:differential:fork # the bugsee/xpath fork against upstream (its expression fuzzer: test:fuzz-expr:fork)
227
+ npm run test:hostile
228
+ npm run test:oracle # strict mode against the independent reference evaluator (test/strict-oracle.ts)
229
+ npm run fixtures # builds the signed SAML responses the benchmarks use (results/, not committed)
230
+ npm run bench:hostile # numbers, per engine
231
+ npm run bench:consumers
232
+ npm run bench:e2e # samlify + xml-crypto, with the candidate swapped in for xpath, and a shadow mode
233
+ ```
234
+
235
+ ### Choosing the candidate
236
+
237
+ `CANDIDATE` names the engine under test: a package name resolved from `node_modules`, or a path (starting with `.`
238
+ or absolute) to a CommonJS or ES module that exports the legacy `xpath` API (`select`, `select1`,
239
+ `selectWithResolver`, `parse(...)`, `evaluate`, `XNodeSet`). `CANDIDATE=./src/index.ts` judges this engine, and
240
+ `CANDIDATE=./test/support/candidate-slow.ts` the same engine with its fast paths switched off. The default is
241
+ `xpath-fork`, the [bugsee/xpath](https://github.com/bugsee/xpath) release tarball, which is a dev dependency.
242
+
243
+ ```sh
244
+ CANDIDATE=./path/to/engine.js node test/fuzz.ts xmldom08 300 1 all
245
+ CANDIDATE=xpath node test/mutation.ts # the oracle against itself: must show no differences
246
+ ```
247
+
248
+ ### Scripts
249
+
250
+ | script | what it does |
251
+ |---|---|
252
+ | `test/fuzz.ts <dom> <documents> [seed] [fuzz\|all]` | random documents and the scenarios static, mutated, detached, fragment, two documents, pair ordering; `all` adds the whole corpus. Fails on any difference in members, order or attribute/namespace nodes (`TOLERATE=order,mixed` lets kinds through; `CLASSIFY=all` is the split gate for xmldom 0.9 and `CLASSIFY=ns-duplicate` allows class 5 only, above) |
253
+ | `test/fuzz-expr.ts <dom> <documents> <expressions> [seed] [ns] [limit ms]` | random expressions from a grammar on random documents; an evaluation that does not return in time is counted, with the engine that was running |
254
+ | `test/mutation.ts` | the document changes while a result is held, or while an expression is being evaluated by a custom function |
255
+ | `test/hostile.ts` | the hostile gate, below |
256
+ | `test/hostile-digests.ts` | records what the oracle answers on each hostile case (`baselines/oracle-digests.json`) |
257
+ | `test/hostile-strict-digests.ts` | records the Recommendation's answer, from plain DOM code, on the hostile cases where strict mode differs from upstream by design (`baselines/strict-digests.json`, used when `CANDIDATE_MODE=strict`) |
258
+ | `test/strict-axes-fuzz.ts <documents> <expressions> [seed] [dom]` | strict `following::`/`preceding::` fast paths against the strict slow path (`XPathProgram.forceSlowPath`), on random node-set contexts with positional predicates |
259
+ | `test/strict-oracle.ts [dom] <documents> <expressions> [seed] [ns]` | strict mode against an independent reference evaluator written from the Recommendation (the data model, the axes, node tests, positions, unions), on random documents and location paths; `STRICT_ORACLE_MODE=upstream` is the self-test |
260
+ | `test/harness-selftest.ts` | a candidate that reverses node-sets must be caught by the fuzzer, a quadratic one by the gate |
261
+
262
+ The DOM matrix for `<dom>`: `xmldom08` (`@xmldom/xmldom` 0.8.15, the default), `xmldom09` (`@xmldom/xmldom` 0.9.x), `jsdom`,
263
+ `happy` (happy-dom) and `linkedom`, all dev dependencies with exact versions.
264
+
265
+ ### The hostile gate and baselines
266
+
267
+ Every case of `bench/hostile-cases.ts` runs on the candidate in its own process, killed after the bound
268
+ (`HOSTILE_BOUND_MS`, default 10 000 ms, loose on purpose: good engines take milliseconds and quadratic ones minutes),
269
+ and must give the oracle's answer where `baselines/oracle-digests.json` knows it (upstream itself cannot finish some
270
+ cases, which are then judged on time only).
271
+
272
+ A candidate with known-slow shapes lists them by case name in `baselines/<candidate>.json` (or the file `BASELINE`
273
+ names). A listed case that is too slow does not fail the run and is reported; one that is within the bound is reported
274
+ so the baseline can shrink when that is stable. A wrong result or an error is never baselined. With no baseline file, or
275
+ `HOSTILE_STRICT=1`, a candidate is judged strictly. `baselines/xpath-fork.json` records what the fork is known to
276
+ be slow on.
277
+
278
+ ## Build and compatibility
279
+
280
+ `npm run build` compiles `src/` to plain JavaScript with the same compiler and the same strict options as the type
281
+ check (every strict flag, no `any`). This is what the package ships.
282
+
283
+ ```
284
+ dist/index.js index.d.ts ES modules (and one .js / .d.ts per source module, flat in dist/)
285
+ dist/index.cjs index.d.cts CommonJS (the same modules as .cjs / .d.cts)
286
+ dist/*.js.map dist/*.cjs.map source maps with the sources inlined
287
+ ```
288
+
289
+ - **ES2022, not down-levelled.** Classes, `#private` members, class fields and optional chaining are emitted as
290
+ written. The emitted files contain no TypeScript syntax and do not need Node's type stripping. The sources are
291
+ compiled against the ES2022 library only (no Node and no DOM typings), so they cannot use a newer API, and nothing
292
+ from Node's typings reaches the `.d.ts` files.
293
+ - **Node >= 20** and recent browsers (`engines.node` is `>=20`). Developing in this repository, which runs the
294
+ TypeScript sources and tests directly, needs Node >= 22.18.
295
+ - **`package.json`:** an `exports` map (`types` / `import` / `require`, so a TypeScript consumer gets `index.d.ts` or
296
+ `index.d.cts`), `main`, `module`, `types`, `files` (only `dist/`, the licence and this readme are packed) and
297
+ `"sideEffects": false`.
298
+ - **Browser-safe.** The code imports nothing from Node (no `node:` modules, no packages); the ES module entry is the
299
+ browser entry. The time limit reads `globalThis.performance.now()` and falls back to `Date.now()` where there is
300
+ no `performance` object.
301
+ - **Declarations** are emitted without declaration maps: the maps would point at sources that are not shipped.
302
+
303
+ What checks it, on the built output rather than the sources (`npm run build`, then):
304
+
305
+ | check | command |
306
+ |---|---|
307
+ | exports through `import` and `require`, the same names from both, the time limit without `performance`; the emitted files parse as ES2022 (acorn), keep their classes, `#private` members and `?.`, contain no TypeScript and no Node import; the declarations have no Node reference; `npm pack --dry-run` content | `npm run test:dist` |
308
+ | a TypeScript consumer (ES2022 + DOM libraries, no Node typings) compiles against the declarations for both conditions | `npm run test:dist:types` |
309
+ | unit tests, a reduced differential gate (xmldom 0.8 and 0.9, mutation, random expressions) and the strict hostile gate, with the candidate pointed at `dist/index.cjs`, on the tests compiled to JavaScript (`npm run build:tests`) | `npm run test:built` |
310
+ | real browsers (Playwright: Chromium, Firefox, WebKit): expressions on `DOMParser` documents including namespaces, an HTML document and the budget and time-limit errors, and the same expressions through upstream `xpath` compared in the page | `npx playwright install chromium && npm run test:browser` |
311
+
312
+ CI runs the first three on Node 20, 22 and 24 and the browser check on all three engines (WebKit is reported but does
313
+ not block while the runners cannot install its system libraries from apt). WebKit is Playwright's build, not Safari
314
+ itself, and no real mobile browser is tested.
315
+
316
+ ## Licence, credits and references
317
+
318
+ MIT, see `LICENSE`.
319
+
320
+ `xpath-es` is a new code base with no upstream, written from the W3C specification, and it exists because of
321
+ [`xpath`](https://github.com/goto100/xpath), whose behaviour it is measured against and whose tests and quirks it
322
+ learns from. `xpath` was originally written by **Cameron McCormack** (copyright 2018, MIT, preserved in `LICENSE`),
323
+ with contributions from **Yaron Naveh**, **goto100**, **Thomas Weinert**, **Jimmy Rishe** and
324
+ [others](https://github.com/goto100/xpath/graphs/contributors). Thank you.
325
+
326
+ - Design ideas (lazy sort flags, context-sensitivity analysis, reverse-axis storage, predicate merging) were studied in
327
+ the XPath implementations of WebKit, Blink and Gecko; no code from those projects is included.
328
+ - W3C, *XML Path Language (XPath) Version 1.0*, W3C Recommendation, 16 November 1999:
329
+ <https://www.w3.org/TR/1999/REC-xpath-19991116/>
330
+ - `xpath` 0.0.36, the oracle (dev dependency): <https://github.com/goto100/xpath>, MIT.
331
+ - The [bugsee/xpath](https://github.com/bugsee/xpath) fork, judged by this harness (dev dependency): MIT, same
332
+ copyright holders.
333
+
334
+ Other dev dependencies keep their own licences: `@xmldom/xmldom`/`xmldom` (MIT), `jsdom` (MIT), `happy-dom` (MIT),
335
+ `linkedom` (ISC), `samlify` (MIT), `xml-crypto` (MIT), `@authenio/xml-encryption` (MIT), `node-forge` (BSD-3-Clause
336
+ or GPL-2.0, used here under BSD-3-Clause), TypeScript (Apache-2.0), oxlint and oxfmt (MIT).
package/dist/ast.cjs ADDED
@@ -0,0 +1,5 @@
1
+ "use strict";
2
+ // Immutable AST. Every node is a member of a discriminated union on `kind`; every array is readonly.
3
+ // W3C XPath 1.0 (16 Nov 1999) section 3 (expressions) and section 2 (location paths).
4
+ Object.defineProperty(exports, "__esModule", { value: true });
5
+ //# sourceMappingURL=ast.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ast.cjs","sourceRoot":"","sources":["../src/ast.ts"],"names":[],"mappings":";AAAA,qGAAqG;AACrG,sFAAsF","sourcesContent":["// Immutable AST. Every node is a member of a discriminated union on `kind`; every array is readonly.\n// W3C XPath 1.0 (16 Nov 1999) section 3 (expressions) and section 2 (location paths).\n\nexport type Axis =\n | 'ancestor'\n | 'ancestor-or-self'\n | 'attribute'\n | 'child'\n | 'descendant'\n | 'descendant-or-self'\n | 'following'\n | 'following-sibling'\n | 'namespace'\n | 'parent'\n | 'preceding'\n | 'preceding-sibling'\n | 'self'\n /** An AxisName token that names no axis. Upstream accepts it and the step selects nothing (quirk Q-AXIS). */\n | 'unknown';\n\nexport type NodeTest =\n | { readonly kind: 'any' }\n | { readonly kind: 'prefixAny'; readonly prefix: string }\n | { readonly kind: 'name'; readonly prefix: string | null; readonly localName: string; readonly name: string }\n | { readonly kind: 'text' }\n | { readonly kind: 'comment' }\n | { readonly kind: 'node' }\n | { readonly kind: 'pi' }\n | { readonly kind: 'piTarget'; readonly target: string }\n /** A NodeType token upstream's grammar accepts but does not know; matches nothing. */\n | { readonly kind: 'never' };\n\nexport interface Step {\n readonly axis: Axis;\n readonly test: NodeTest;\n readonly predicates: readonly Expr[];\n}\n\nexport type BinaryOp = '=' | '!=' | '<' | '<=' | '>' | '>=' | '+' | '-' | '*' | 'div' | 'mod';\n\nexport interface ChainLink {\n readonly op: BinaryOp;\n readonly operand: Expr;\n}\n\nexport type Expr =\n | { readonly kind: 'or'; readonly operands: readonly Expr[] }\n | { readonly kind: 'and'; readonly operands: readonly Expr[] }\n /** A left-associative run of binary operators of one precedence level: `a - b + c`. Flat, so a long run is not deep. */\n | { readonly kind: 'chain'; readonly first: Expr; readonly rest: readonly ChainLink[] }\n | { readonly kind: 'negate'; readonly operand: Expr }\n | { readonly kind: 'union'; readonly operands: readonly Expr[] }\n | {\n readonly kind: 'path';\n /** The FilterExpr the path starts from, or null for a plain location path. */\n readonly filter: Expr | null;\n readonly filterPredicates: readonly Expr[];\n readonly absolute: boolean;\n readonly steps: readonly Step[];\n }\n | { readonly kind: 'literal'; readonly value: string }\n | { readonly kind: 'number'; readonly value: number }\n | { readonly kind: 'call'; readonly name: string; readonly args: readonly Expr[] }\n | { readonly kind: 'variable'; readonly name: string };\n\nexport type StaticType = 'boolean' | 'number' | 'string' | 'node-set' | 'unknown';\n"]}
package/dist/ast.d.cts ADDED
@@ -0,0 +1,79 @@
1
+ export type Axis = 'ancestor' | 'ancestor-or-self' | 'attribute' | 'child' | 'descendant' | 'descendant-or-self' | 'following' | 'following-sibling' | 'namespace' | 'parent' | 'preceding' | 'preceding-sibling' | 'self'
2
+ /** An AxisName token that names no axis. Upstream accepts it and the step selects nothing (quirk Q-AXIS). */
3
+ | 'unknown';
4
+ export type NodeTest = {
5
+ readonly kind: 'any';
6
+ } | {
7
+ readonly kind: 'prefixAny';
8
+ readonly prefix: string;
9
+ } | {
10
+ readonly kind: 'name';
11
+ readonly prefix: string | null;
12
+ readonly localName: string;
13
+ readonly name: string;
14
+ } | {
15
+ readonly kind: 'text';
16
+ } | {
17
+ readonly kind: 'comment';
18
+ } | {
19
+ readonly kind: 'node';
20
+ } | {
21
+ readonly kind: 'pi';
22
+ } | {
23
+ readonly kind: 'piTarget';
24
+ readonly target: string;
25
+ }
26
+ /** A NodeType token upstream's grammar accepts but does not know; matches nothing. */
27
+ | {
28
+ readonly kind: 'never';
29
+ };
30
+ export interface Step {
31
+ readonly axis: Axis;
32
+ readonly test: NodeTest;
33
+ readonly predicates: readonly Expr[];
34
+ }
35
+ export type BinaryOp = '=' | '!=' | '<' | '<=' | '>' | '>=' | '+' | '-' | '*' | 'div' | 'mod';
36
+ export interface ChainLink {
37
+ readonly op: BinaryOp;
38
+ readonly operand: Expr;
39
+ }
40
+ export type Expr = {
41
+ readonly kind: 'or';
42
+ readonly operands: readonly Expr[];
43
+ } | {
44
+ readonly kind: 'and';
45
+ readonly operands: readonly Expr[];
46
+ }
47
+ /** A left-associative run of binary operators of one precedence level: `a - b + c`. Flat, so a long run is not deep. */
48
+ | {
49
+ readonly kind: 'chain';
50
+ readonly first: Expr;
51
+ readonly rest: readonly ChainLink[];
52
+ } | {
53
+ readonly kind: 'negate';
54
+ readonly operand: Expr;
55
+ } | {
56
+ readonly kind: 'union';
57
+ readonly operands: readonly Expr[];
58
+ } | {
59
+ readonly kind: 'path';
60
+ /** The FilterExpr the path starts from, or null for a plain location path. */
61
+ readonly filter: Expr | null;
62
+ readonly filterPredicates: readonly Expr[];
63
+ readonly absolute: boolean;
64
+ readonly steps: readonly Step[];
65
+ } | {
66
+ readonly kind: 'literal';
67
+ readonly value: string;
68
+ } | {
69
+ readonly kind: 'number';
70
+ readonly value: number;
71
+ } | {
72
+ readonly kind: 'call';
73
+ readonly name: string;
74
+ readonly args: readonly Expr[];
75
+ } | {
76
+ readonly kind: 'variable';
77
+ readonly name: string;
78
+ };
79
+ export type StaticType = 'boolean' | 'number' | 'string' | 'node-set' | 'unknown';
package/dist/ast.d.ts ADDED
@@ -0,0 +1,79 @@
1
+ export type Axis = 'ancestor' | 'ancestor-or-self' | 'attribute' | 'child' | 'descendant' | 'descendant-or-self' | 'following' | 'following-sibling' | 'namespace' | 'parent' | 'preceding' | 'preceding-sibling' | 'self'
2
+ /** An AxisName token that names no axis. Upstream accepts it and the step selects nothing (quirk Q-AXIS). */
3
+ | 'unknown';
4
+ export type NodeTest = {
5
+ readonly kind: 'any';
6
+ } | {
7
+ readonly kind: 'prefixAny';
8
+ readonly prefix: string;
9
+ } | {
10
+ readonly kind: 'name';
11
+ readonly prefix: string | null;
12
+ readonly localName: string;
13
+ readonly name: string;
14
+ } | {
15
+ readonly kind: 'text';
16
+ } | {
17
+ readonly kind: 'comment';
18
+ } | {
19
+ readonly kind: 'node';
20
+ } | {
21
+ readonly kind: 'pi';
22
+ } | {
23
+ readonly kind: 'piTarget';
24
+ readonly target: string;
25
+ }
26
+ /** A NodeType token upstream's grammar accepts but does not know; matches nothing. */
27
+ | {
28
+ readonly kind: 'never';
29
+ };
30
+ export interface Step {
31
+ readonly axis: Axis;
32
+ readonly test: NodeTest;
33
+ readonly predicates: readonly Expr[];
34
+ }
35
+ export type BinaryOp = '=' | '!=' | '<' | '<=' | '>' | '>=' | '+' | '-' | '*' | 'div' | 'mod';
36
+ export interface ChainLink {
37
+ readonly op: BinaryOp;
38
+ readonly operand: Expr;
39
+ }
40
+ export type Expr = {
41
+ readonly kind: 'or';
42
+ readonly operands: readonly Expr[];
43
+ } | {
44
+ readonly kind: 'and';
45
+ readonly operands: readonly Expr[];
46
+ }
47
+ /** A left-associative run of binary operators of one precedence level: `a - b + c`. Flat, so a long run is not deep. */
48
+ | {
49
+ readonly kind: 'chain';
50
+ readonly first: Expr;
51
+ readonly rest: readonly ChainLink[];
52
+ } | {
53
+ readonly kind: 'negate';
54
+ readonly operand: Expr;
55
+ } | {
56
+ readonly kind: 'union';
57
+ readonly operands: readonly Expr[];
58
+ } | {
59
+ readonly kind: 'path';
60
+ /** The FilterExpr the path starts from, or null for a plain location path. */
61
+ readonly filter: Expr | null;
62
+ readonly filterPredicates: readonly Expr[];
63
+ readonly absolute: boolean;
64
+ readonly steps: readonly Step[];
65
+ } | {
66
+ readonly kind: 'literal';
67
+ readonly value: string;
68
+ } | {
69
+ readonly kind: 'number';
70
+ readonly value: number;
71
+ } | {
72
+ readonly kind: 'call';
73
+ readonly name: string;
74
+ readonly args: readonly Expr[];
75
+ } | {
76
+ readonly kind: 'variable';
77
+ readonly name: string;
78
+ };
79
+ export type StaticType = 'boolean' | 'number' | 'string' | 'node-set' | 'unknown';
package/dist/ast.js ADDED
@@ -0,0 +1,4 @@
1
+ // Immutable AST. Every node is a member of a discriminated union on `kind`; every array is readonly.
2
+ // W3C XPath 1.0 (16 Nov 1999) section 3 (expressions) and section 2 (location paths).
3
+ export {};
4
+ //# sourceMappingURL=ast.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ast.js","sourceRoot":"","sources":["../src/ast.ts"],"names":[],"mappings":"AAAA,qGAAqG;AACrG,sFAAsF","sourcesContent":["// Immutable AST. Every node is a member of a discriminated union on `kind`; every array is readonly.\n// W3C XPath 1.0 (16 Nov 1999) section 3 (expressions) and section 2 (location paths).\n\nexport type Axis =\n | 'ancestor'\n | 'ancestor-or-self'\n | 'attribute'\n | 'child'\n | 'descendant'\n | 'descendant-or-self'\n | 'following'\n | 'following-sibling'\n | 'namespace'\n | 'parent'\n | 'preceding'\n | 'preceding-sibling'\n | 'self'\n /** An AxisName token that names no axis. Upstream accepts it and the step selects nothing (quirk Q-AXIS). */\n | 'unknown';\n\nexport type NodeTest =\n | { readonly kind: 'any' }\n | { readonly kind: 'prefixAny'; readonly prefix: string }\n | { readonly kind: 'name'; readonly prefix: string | null; readonly localName: string; readonly name: string }\n | { readonly kind: 'text' }\n | { readonly kind: 'comment' }\n | { readonly kind: 'node' }\n | { readonly kind: 'pi' }\n | { readonly kind: 'piTarget'; readonly target: string }\n /** A NodeType token upstream's grammar accepts but does not know; matches nothing. */\n | { readonly kind: 'never' };\n\nexport interface Step {\n readonly axis: Axis;\n readonly test: NodeTest;\n readonly predicates: readonly Expr[];\n}\n\nexport type BinaryOp = '=' | '!=' | '<' | '<=' | '>' | '>=' | '+' | '-' | '*' | 'div' | 'mod';\n\nexport interface ChainLink {\n readonly op: BinaryOp;\n readonly operand: Expr;\n}\n\nexport type Expr =\n | { readonly kind: 'or'; readonly operands: readonly Expr[] }\n | { readonly kind: 'and'; readonly operands: readonly Expr[] }\n /** A left-associative run of binary operators of one precedence level: `a - b + c`. Flat, so a long run is not deep. */\n | { readonly kind: 'chain'; readonly first: Expr; readonly rest: readonly ChainLink[] }\n | { readonly kind: 'negate'; readonly operand: Expr }\n | { readonly kind: 'union'; readonly operands: readonly Expr[] }\n | {\n readonly kind: 'path';\n /** The FilterExpr the path starts from, or null for a plain location path. */\n readonly filter: Expr | null;\n readonly filterPredicates: readonly Expr[];\n readonly absolute: boolean;\n readonly steps: readonly Step[];\n }\n | { readonly kind: 'literal'; readonly value: string }\n | { readonly kind: 'number'; readonly value: number }\n | { readonly kind: 'call'; readonly name: string; readonly args: readonly Expr[] }\n | { readonly kind: 'variable'; readonly name: string };\n\nexport type StaticType = 'boolean' | 'number' | 'string' | 'node-set' | 'unknown';\n"]}
package/dist/clock.cjs ADDED
@@ -0,0 +1,32 @@
1
+ "use strict";
2
+ // The clock for the evaluation time limit: milliseconds since the Unix epoch, like `Date.now()`, but taken from the
3
+ // high-resolution monotonic timer where there is one.
4
+ //
5
+ // `performance.now()` alone is an offset from `performance.timeOrigin` (a value such as 86423.4), not an epoch
6
+ // timestamp, so the origin is added: `performance.timeOrigin + performance.now()` (a value such as 1794411376976.4).
7
+ // Where there is no performance object with a numeric `timeOrigin`, `Date.now()` is used instead. Both choices give
8
+ // epoch milliseconds, so every reading means the same thing whichever clock answers; the choice is made once, when the
9
+ // module loads, and never changes per reading.
10
+ //
11
+ // `performance` is not in the ES2022 language library and Node's typings must not leak into the public declarations,
12
+ // so the object is looked up on `globalThis` once, here, and narrowed by a type guard.
13
+ Object.defineProperty(exports, "__esModule", { value: true });
14
+ exports.now = void 0;
15
+ function isTimer(value) {
16
+ return (typeof value === 'object' &&
17
+ value !== null &&
18
+ 'now' in value &&
19
+ typeof value.now === 'function' &&
20
+ 'timeOrigin' in value &&
21
+ typeof value.timeOrigin === 'number');
22
+ }
23
+ function chooseClock() {
24
+ const performanceObject = Reflect.get(globalThis, 'performance');
25
+ if (!isTimer(performanceObject))
26
+ return () => Date.now();
27
+ const origin = performanceObject.timeOrigin;
28
+ return () => origin + performanceObject.now();
29
+ }
30
+ /** Milliseconds since the Unix epoch (high resolution where `performance` is available). */
31
+ exports.now = chooseClock();
32
+ //# sourceMappingURL=clock.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"clock.cjs","sourceRoot":"","sources":["../src/clock.ts"],"names":[],"mappings":";AAAA,oHAAoH;AACpH,sDAAsD;AACtD,EAAE;AACF,+GAA+G;AAC/G,qHAAqH;AACrH,oHAAoH;AACpH,uHAAuH;AACvH,+CAA+C;AAC/C,EAAE;AACF,qHAAqH;AACrH,uFAAuF;;;AASvF,SAAS,OAAO,CAAC,KAAc;IAC3B,OAAO,CACH,OAAO,KAAK,KAAK,QAAQ;QACzB,KAAK,KAAK,IAAI;QACd,KAAK,IAAI,KAAK;QACd,OAAO,KAAK,CAAC,GAAG,KAAK,UAAU;QAC/B,YAAY,IAAI,KAAK;QACrB,OAAO,KAAK,CAAC,UAAU,KAAK,QAAQ,CACvC,CAAC;AACN,CAAC;AAED,SAAS,WAAW;IAChB,MAAM,iBAAiB,GAAY,OAAO,CAAC,GAAG,CAAC,UAAU,EAAE,aAAa,CAAC,CAAC;IAC1E,IAAI,CAAC,OAAO,CAAC,iBAAiB,CAAC;QAAE,OAAO,GAAW,EAAE,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC;IACjE,MAAM,MAAM,GAAG,iBAAiB,CAAC,UAAU,CAAC;IAC5C,OAAO,GAAW,EAAE,CAAC,MAAM,GAAG,iBAAiB,CAAC,GAAG,EAAE,CAAC;AAC1D,CAAC;AAED,4FAA4F;AAC/E,QAAA,GAAG,GAAU,WAAW,EAAE,CAAC","sourcesContent":["// The clock for the evaluation time limit: milliseconds since the Unix epoch, like `Date.now()`, but taken from the\n// high-resolution monotonic timer where there is one.\n//\n// `performance.now()` alone is an offset from `performance.timeOrigin` (a value such as 86423.4), not an epoch\n// timestamp, so the origin is added: `performance.timeOrigin + performance.now()` (a value such as 1794411376976.4).\n// Where there is no performance object with a numeric `timeOrigin`, `Date.now()` is used instead. Both choices give\n// epoch milliseconds, so every reading means the same thing whichever clock answers; the choice is made once, when the\n// module loads, and never changes per reading.\n//\n// `performance` is not in the ES2022 language library and Node's typings must not leak into the public declarations,\n// so the object is looked up on `globalThis` once, here, and narrowed by a type guard.\n\nexport type Clock = () => number;\n\ninterface Timer {\n now(): number;\n readonly timeOrigin: number;\n}\n\nfunction isTimer(value: unknown): value is Timer {\n return (\n typeof value === 'object' &&\n value !== null &&\n 'now' in value &&\n typeof value.now === 'function' &&\n 'timeOrigin' in value &&\n typeof value.timeOrigin === 'number'\n );\n}\n\nfunction chooseClock(): Clock {\n const performanceObject: unknown = Reflect.get(globalThis, 'performance');\n if (!isTimer(performanceObject)) return (): number => Date.now();\n const origin = performanceObject.timeOrigin;\n return (): number => origin + performanceObject.now();\n}\n\n/** Milliseconds since the Unix epoch (high resolution where `performance` is available). */\nexport const now: Clock = chooseClock();\n"]}
package/dist/clock.js ADDED
@@ -0,0 +1,29 @@
1
+ // The clock for the evaluation time limit: milliseconds since the Unix epoch, like `Date.now()`, but taken from the
2
+ // high-resolution monotonic timer where there is one.
3
+ //
4
+ // `performance.now()` alone is an offset from `performance.timeOrigin` (a value such as 86423.4), not an epoch
5
+ // timestamp, so the origin is added: `performance.timeOrigin + performance.now()` (a value such as 1794411376976.4).
6
+ // Where there is no performance object with a numeric `timeOrigin`, `Date.now()` is used instead. Both choices give
7
+ // epoch milliseconds, so every reading means the same thing whichever clock answers; the choice is made once, when the
8
+ // module loads, and never changes per reading.
9
+ //
10
+ // `performance` is not in the ES2022 language library and Node's typings must not leak into the public declarations,
11
+ // so the object is looked up on `globalThis` once, here, and narrowed by a type guard.
12
+ function isTimer(value) {
13
+ return (typeof value === 'object' &&
14
+ value !== null &&
15
+ 'now' in value &&
16
+ typeof value.now === 'function' &&
17
+ 'timeOrigin' in value &&
18
+ typeof value.timeOrigin === 'number');
19
+ }
20
+ function chooseClock() {
21
+ const performanceObject = Reflect.get(globalThis, 'performance');
22
+ if (!isTimer(performanceObject))
23
+ return () => Date.now();
24
+ const origin = performanceObject.timeOrigin;
25
+ return () => origin + performanceObject.now();
26
+ }
27
+ /** Milliseconds since the Unix epoch (high resolution where `performance` is available). */
28
+ export const now = chooseClock();
29
+ //# sourceMappingURL=clock.js.map