xpath-es 0.0.0-stage → 1.0.0-rc.0
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.
- package/LICENSE +23 -0
- package/README.md +335 -2
- package/dist/ast.cjs +5 -0
- package/dist/ast.cjs.map +1 -0
- package/dist/ast.d.cts +79 -0
- package/dist/ast.d.ts +79 -0
- package/dist/ast.js +4 -0
- package/dist/ast.js.map +1 -0
- package/dist/clock.cjs +32 -0
- package/dist/clock.cjs.map +1 -0
- package/dist/clock.js +29 -0
- package/dist/clock.js.map +1 -0
- package/dist/compare.cjs +122 -0
- package/dist/compare.cjs.map +1 -0
- package/dist/compare.js +119 -0
- package/dist/compare.js.map +1 -0
- package/dist/compat.cjs +547 -0
- package/dist/compat.cjs.map +1 -0
- package/dist/compat.d.cts +194 -0
- package/dist/compat.d.ts +194 -0
- package/dist/compat.js +530 -0
- package/dist/compat.js.map +1 -0
- package/dist/compile.cjs +602 -0
- package/dist/compile.cjs.map +1 -0
- package/dist/compile.js +587 -0
- package/dist/compile.js.map +1 -0
- package/dist/dom.cjs +116 -0
- package/dist/dom.cjs.map +1 -0
- package/dist/dom.d.cts +100 -0
- package/dist/dom.d.ts +100 -0
- package/dist/dom.js +107 -0
- package/dist/dom.js.map +1 -0
- package/dist/errors.cjs +83 -0
- package/dist/errors.cjs.map +1 -0
- package/dist/errors.d.cts +39 -0
- package/dist/errors.d.ts +39 -0
- package/dist/errors.js +70 -0
- package/dist/errors.js.map +1 -0
- package/dist/functions.cjs +570 -0
- package/dist/functions.cjs.map +1 -0
- package/dist/functions.js +565 -0
- package/dist/functions.js.map +1 -0
- package/dist/index.cjs +39 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +7 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -0
- package/dist/lexer.cjs +264 -0
- package/dist/lexer.cjs.map +1 -0
- package/dist/lexer.js +261 -0
- package/dist/lexer.js.map +1 -0
- package/dist/order.cjs +351 -0
- package/dist/order.cjs.map +1 -0
- package/dist/order.js +345 -0
- package/dist/order.js.map +1 -0
- package/dist/parser.cjs +322 -0
- package/dist/parser.cjs.map +1 -0
- package/dist/parser.d.cts +9 -0
- package/dist/parser.d.ts +9 -0
- package/dist/parser.js +318 -0
- package/dist/parser.js.map +1 -0
- package/dist/program.cjs +180 -0
- package/dist/program.cjs.map +1 -0
- package/dist/program.d.cts +83 -0
- package/dist/program.d.ts +83 -0
- package/dist/program.js +174 -0
- package/dist/program.js.map +1 -0
- package/dist/ranked.cjs +192 -0
- package/dist/ranked.cjs.map +1 -0
- package/dist/ranked.js +187 -0
- package/dist/ranked.js.map +1 -0
- package/dist/runtime.cjs +596 -0
- package/dist/runtime.cjs.map +1 -0
- package/dist/runtime.js +585 -0
- package/dist/runtime.js.map +1 -0
- package/dist/steps.cjs +1137 -0
- package/dist/steps.cjs.map +1 -0
- package/dist/steps.js +1131 -0
- package/dist/steps.js.map +1 -0
- 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
|
-
#
|
|
1
|
+
# xpath-es
|
|
2
2
|
|
|
3
|
-
|
|
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
|
package/dist/ast.cjs.map
ADDED
|
@@ -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
package/dist/ast.js.map
ADDED
|
@@ -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
|