@jarenjs/json 0.9.2
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/ARCHITECTURE.md +175 -0
- package/LICENSE +21 -0
- package/README.md +471 -0
- package/dist/types/basic.d.ts +32 -0
- package/dist/types/index.d.ts +4 -0
- package/dist/types/jslt/dispatch.d.ts +11 -0
- package/dist/types/jslt/errors.d.ts +18 -0
- package/dist/types/jslt/index.d.ts +53 -0
- package/dist/types/jslt/stylesheet.d.ts +8 -0
- package/dist/types/jtlt/desugar.d.ts +19 -0
- package/dist/types/jtlt/errors.d.ts +18 -0
- package/dist/types/jtlt/index.d.ts +57 -0
- package/dist/types/jtlt/template.d.ts +8 -0
- package/dist/types/jtlt/writer.d.ts +6 -0
- package/dist/types/path.d.ts +235 -0
- package/dist/types/pointer.d.ts +114 -0
- package/dist/types/query/compile.d.ts +21 -0
- package/dist/types/query/errors.d.ts +18 -0
- package/dist/types/query/index.d.ts +70 -0
- package/dist/types/query/normalize.d.ts +68 -0
- package/dist/types/query/operators.d.ts +424 -0
- package/dist/types/query/runtime.d.ts +93 -0
- package/dist/types/segments.d.ts +62 -0
- package/dist/types/xquery/index.d.ts +19 -0
- package/dist/types/xquery/parse.d.ts +20 -0
- package/docs/JSLT-FORMAT.md +861 -0
- package/docs/JSLT-PRELUDE.md +159 -0
- package/docs/JTLT-FORMAT.md +659 -0
- package/docs/QUERY-FORMAT.md +1221 -0
- package/docs/XQUERY-FRONTEND.md +321 -0
- package/package.json +81 -0
- package/schemas/jaren-jslt.draft-07.schema.json +776 -0
- package/schemas/jaren-jslt.schema.json +776 -0
- package/schemas/jaren-query.draft-07.schema.json +613 -0
- package/schemas/jaren-query.schema.json +375 -0
- package/src/basic.js +300 -0
- package/src/index.js +4 -0
- package/src/jslt/dispatch.js +934 -0
- package/src/jslt/errors.js +34 -0
- package/src/jslt/index.js +121 -0
- package/src/jslt/stylesheet.js +234 -0
- package/src/jtlt/desugar.js +231 -0
- package/src/jtlt/errors.js +34 -0
- package/src/jtlt/index.js +155 -0
- package/src/jtlt/template.js +130 -0
- package/src/jtlt/writer.js +110 -0
- package/src/path.js +977 -0
- package/src/pointer.js +453 -0
- package/src/query/compile.js +817 -0
- package/src/query/errors.js +33 -0
- package/src/query/index.js +150 -0
- package/src/query/normalize.js +1047 -0
- package/src/query/operators.js +1253 -0
- package/src/query/runtime.js +233 -0
- package/src/segments.js +627 -0
- package/src/xquery/index.js +35 -0
- package/src/xquery/parse.js +1647 -0
package/README.md
ADDED
|
@@ -0,0 +1,471 @@
|
|
|
1
|
+
# @jarenjs/json
|
|
2
|
+
|
|
3
|
+
The JSON addressing, query, and stylesheet standards of [Jaren](https://github.com/jklarenbeek/jarenjs), compiled: JSON Pointer ([RFC 6901](https://datatracker.ietf.org/doc/html/rfc6901)), a JSONPath engine ([RFC 9535](https://www.rfc-editor.org/rfc/rfc9535.html)) that passes the complete official compliance suite, the **Jaren JSON Query format** — a declarative query-and-transformation language with XQuery 3.1 semantics whose queries are themselves JSON documents — and **JSLT**, a recursive template-dispatch layer over that same stack. Everything follows the same architecture: parse and decide once, then run a specialized closure. No `eval`, no `new Function`, CSP-safe, zero runtime dependencies beyond the [`@jarenjs/core`](../core) foundation.
|
|
4
|
+
|
|
5
|
+
None of it depends on JSON Schema: every module can be used standalone in any JavaScript project.
|
|
6
|
+
|
|
7
|
+
## Modules
|
|
8
|
+
|
|
9
|
+
| Import | Contents |
|
|
10
|
+
|---|---|
|
|
11
|
+
| `@jarenjs/json` | everything below |
|
|
12
|
+
| `@jarenjs/json/basic` | JSON, JSON Pointer and JSONPath string validation |
|
|
13
|
+
| `@jarenjs/json/pointer` | the JSON Pointer and Relative JSON Pointer compiler |
|
|
14
|
+
| `@jarenjs/json/path` | the JSONPath compiler |
|
|
15
|
+
| `@jarenjs/json/query` | the Jaren JSON Query engine |
|
|
16
|
+
| `@jarenjs/json/jslt` | the Jaren JSLT stylesheet compiler and dispatcher |
|
|
17
|
+
| `@jarenjs/json/jtlt` | the Jaren JTLT template compiler — JSON to text/XML |
|
|
18
|
+
| `@jarenjs/json/xquery` | the XQuery text front-end for the query engine |
|
|
19
|
+
|
|
20
|
+
## JSON utilities
|
|
21
|
+
|
|
22
|
+
`@jarenjs/json` implements the JSON addressing standards next to each other:
|
|
23
|
+
|
|
24
|
+
| Standard | Functions |
|
|
25
|
+
|---|---|
|
|
26
|
+
| JSON validation | `isValidJSON`, `isValidJSONCheap` (a fast "definitely not JSON" pre-test) |
|
|
27
|
+
| JSON Pointer ([RFC 6901](https://datatracker.ietf.org/doc/html/rfc6901)) | `compileJSONPointer`, `parseJSONPointer`, `isValidJSONPointer`, `isValidJSONPointerUriFragment` |
|
|
28
|
+
| Relative JSON Pointer | `compileRelativeJSONPointer`, `parseRelativeJSONPointer`, `compileDataRef`, `isValidRelativeJSONPointer` |
|
|
29
|
+
| JSONPath ([RFC 9535](https://www.rfc-editor.org/rfc/rfc9535.html)) | `compileJSONPath`, `queryJSONPath`, `parseJSONPath`, `isValidJSONPathStrict` |
|
|
30
|
+
| Jaren JSON Query | `compileJsonQuery`, `queryJson`, `JsonQueryCompileError`, `JsonQueryRuntimeError` |
|
|
31
|
+
| Jaren JSLT | `compileJsltStylesheet`, `transformJson`, `JsltCompileError`, `JsltRuntimeError` |
|
|
32
|
+
| Jaren JTLT | `compileJtltStylesheet`, `renderText`, `JtltCompileError`, `JtltRuntimeError` |
|
|
33
|
+
|
|
34
|
+
### JSON Pointer
|
|
35
|
+
|
|
36
|
+
Pointers address a single location. Like every other engine in this package they are compiled, not interpreted: `compileJSONPointer(pointer)` parses once (strict RFC 6901, throwing a `JSONPointerSyntaxError` with `source` and `position` on bad input), pre-decodes every member name, pre-parses every array index, and returns a getter specialized by segment count. Resolving allocates nothing and returns `JSONPOINTER_NOTHING` — the same sentinel as `JSONPATH_NOTHING` — when the pointer addresses no location:
|
|
37
|
+
|
|
38
|
+
```javascript
|
|
39
|
+
import { compileJSONPointer, JSONPOINTER_NOTHING } from '@jarenjs/json';
|
|
40
|
+
|
|
41
|
+
const doc = { limits: { min: 2, max: 9 }, value: 5 };
|
|
42
|
+
|
|
43
|
+
const getMin = compileJSONPointer('/limits/min');
|
|
44
|
+
getMin(doc); // 2
|
|
45
|
+
getMin({}); // JSONPOINTER_NOTHING
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Relative pointers resolve from a location inside the document, given as an RFC 6901 pointer string. The relative part (level count, `#` form, trailing segments) compiles once; per call only the location varies. This is the hot path of the validator's `data`/`$data` keywords, where the ref is a schema constant known at schema-compile time:
|
|
49
|
+
|
|
50
|
+
```javascript
|
|
51
|
+
import { compileRelativeJSONPointer } from '@jarenjs/json';
|
|
52
|
+
|
|
53
|
+
const getLimits = compileRelativeJSONPointer('1/limits');
|
|
54
|
+
getLimits(doc, '/value'); // { min: 2, max: 9 }
|
|
55
|
+
|
|
56
|
+
const getName = compileRelativeJSONPointer('0#');
|
|
57
|
+
getName(doc, '/limits/min'); // 'min' (the member name of the location)
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
`compileDataRef(ref)` compiles the union the validator accepts — `''` for the data root, a leading `/` for an absolute pointer, a leading digit for a relative one — deciding the dispatch once at compile time. On a realistic `$data` workload the compiled resolvers are 4–20x faster than the interpretive resolver they replaced, and beat the `jsonpointer` npm package on every scenario (`npm run benchmark:jsonpointer`, 2026-07-17: absolute pointers 12–16x, relative pointers 4–16x, `compileDataRef` dispatch 7–20x).
|
|
61
|
+
|
|
62
|
+
## The JSONPath compiler
|
|
63
|
+
|
|
64
|
+
JSONPath selects *many* locations: a query like `$.store.book[?@.price < 10].title` describes a whole nodelist. The `path.js` module implements the complete RFC 9535 specification as a two-stage compiler, mirroring the philosophy of the schema validator: **parse and decide everything once, then run a specialized function**.
|
|
65
|
+
|
|
66
|
+
```javascript
|
|
67
|
+
import { compileJSONPath } from '@jarenjs/json';
|
|
68
|
+
|
|
69
|
+
const query = compileJSONPath('$.store.book[?@.price < 10].title');
|
|
70
|
+
|
|
71
|
+
query(data); // ['Sayings of the Century', 'Moby Dick']
|
|
72
|
+
query(other); // compiled once, reusable on any document
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### Compiled query API
|
|
76
|
+
|
|
77
|
+
`compileJSONPath(source)` throws a `JSONPathSyntaxError` (with `source` and `position` properties) on invalid queries and returns a function with helpers attached:
|
|
78
|
+
|
|
79
|
+
| Call | Returns |
|
|
80
|
+
|---|---|
|
|
81
|
+
| `query(data)` / `query.values(data)` | array of matched values, in document order |
|
|
82
|
+
| `query.first(data)` | the first matched value, or `undefined` |
|
|
83
|
+
| `query.exists(data)` | `true` when at least one node matches |
|
|
84
|
+
| `query.nodes(data)` | array of `{ path, value }` pairs with normalized paths |
|
|
85
|
+
| `query.paths(data)` | array of normalized paths (RFC 9535 §2.7), e.g. `$['store']['book'][0]['title']` |
|
|
86
|
+
| `query.source` | the original query string |
|
|
87
|
+
| `query.ast` | the parsed query AST (deeply frozen) |
|
|
88
|
+
|
|
89
|
+
For one-off queries there is `queryJSONPath(source, data)`, which keeps a cache of compiled queries (512 entries, FIFO), and `isValidJSONPathStrict(source)` for a boolean grammar check — this is what the `json-path` format in [`@jarenjs/formats`](../formats) uses.
|
|
90
|
+
|
|
91
|
+
### Supported syntax
|
|
92
|
+
|
|
93
|
+
Everything in RFC 9535, with no extensions and no omissions:
|
|
94
|
+
|
|
95
|
+
| Construct | Example |
|
|
96
|
+
|---|---|
|
|
97
|
+
| root / current node | `$` — `@` inside filter expressions |
|
|
98
|
+
| name selector | `$.store`, `$['two words']`, `$["é"]` |
|
|
99
|
+
| wildcard | `$.store.*`, `$[*]` |
|
|
100
|
+
| index (negative from end) | `$[0]`, `$[-1]` |
|
|
101
|
+
| array slice | `$[1:3]`, `$[5:]`, `$[::2]`, `$[::-1]` |
|
|
102
|
+
| child segment, multi-selector | `$[0, 3]`, `$['a', 'b', *]` |
|
|
103
|
+
| descendant segment | `$..author`, `$..[0]`, `$..*` |
|
|
104
|
+
| filter selector | `$[?@.price < 10]`, `$[?(@.a && !@.b)]` |
|
|
105
|
+
| comparisons | `== != < <= > >=` with `Nothing`-aware semantics |
|
|
106
|
+
| logical operators | `&&`, `\|\|`, `!`, parentheses |
|
|
107
|
+
| function extensions | `length()`, `count()`, `match()`, `search()`, `value()` |
|
|
108
|
+
|
|
109
|
+
The parser is strict about everything the RFC is strict about: leading zeros, `-0`, integer bounds (±2⁵³−1), whitespace placement, lone surrogates, string escape rules, and the *well-typedness* of function expressions — `$[?length(@)]` (a value used as a test) and `$[?@[*] == 1]` (a non-singular query in a comparison) are compile-time errors, as the spec demands.
|
|
110
|
+
|
|
111
|
+
### Filter semantics worth knowing
|
|
112
|
+
|
|
113
|
+
- **`Nothing` is not `null`.** A missing member and a member whose value is `null` are different things: `$[?@.a == null]` only matches nodes where `a` exists *and* is `null`. Two missing values compare equal (`$[?@.absent1 == @.absent2]` is `true`). The sentinel is exported as `JSONPATH_NOTHING` for advanced integrations.
|
|
114
|
+
- **`==` is deep structural equality**; numbers compare mathematically (`1 == 1.0`).
|
|
115
|
+
- **`<` orders numbers and strings only**, and strings are ordered by Unicode scalar value (code points), not by UTF-16 code units.
|
|
116
|
+
- **`match()`/`search()` take I-Regexp** ([RFC 9485](https://www.rfc-editor.org/rfc/rfc9485.html)) patterns, validated against the complete I-Regexp grammar — lookarounds, backreferences, lazy quantifiers and multi-character escapes like `\d` make the function yield `false`, as the spec requires. Literal patterns are compiled to a `RegExp` once at query-compile time; dynamic patterns get a per-callsite cache. Unescaped `^`/`$` behave as anchors, matching the RFC's own ECMAScript translation and the official compliance test suite.
|
|
117
|
+
|
|
118
|
+
### Conformance and performance
|
|
119
|
+
|
|
120
|
+
Conformance: **all 703 tests** of the official [JSONPath Compliance Test Suite](https://github.com/jsonpath-standard/jsonpath-compliance-test-suite) pass, including the normalized-path assertions (the suite is a git submodule at `benchmark/jsonpath-suite/`).
|
|
121
|
+
|
|
122
|
+
Measured with `node benchmark/jsonpath.js --profile --scale -i 3000` (2026-07-17, Node v24.14.0, 1000-item synthetic document, vs [json-p3](https://www.npmjs.com/package/json-p3) 2.2.2):
|
|
123
|
+
|
|
124
|
+
| Query shape | Jaren | vs json-p3 |
|
|
125
|
+
|---|---|---|
|
|
126
|
+
| singular `$.items[42].name` | 189 ns (~5M queries/s) | 4.9x |
|
|
127
|
+
| wildcard `$.items[*].id` | 31.2 µs | 12.1x |
|
|
128
|
+
| slice `$.items[100:200].id` | 4.4 µs | 9.5x |
|
|
129
|
+
| filter `$.items[?@.price < 10].name` | 38.9 µs | 8.7x |
|
|
130
|
+
| regexp filter `$.items[?match(@.name, "item-1.*")].id` | 38.8 µs | 14.3x |
|
|
131
|
+
| descendant `$..value` | 210.0 µs | 81.8x |
|
|
132
|
+
| mean over all 456 CTS queries | 151 ns | 18.7x |
|
|
133
|
+
| compile | ~2.4 µs per query | — |
|
|
134
|
+
|
|
135
|
+
## The Jaren JSON Query language
|
|
136
|
+
|
|
137
|
+
JSONPath answers "which nodes?"; it cannot join, group, aggregate or reshape. The Jaren JSON Query format is the next layer: a query-and-transformation language with the semantics of **XQuery 3.1** — sequences, FLWOR, effective boolean value, existential comparisons — and a surface syntax that is **JSON itself**, the way an XSLT stylesheet is an XML document. Navigation leaves are RFC 9535 JSONPath strings.
|
|
138
|
+
|
|
139
|
+
The language contract is the specification in [docs/QUERY-FORMAT.md](./docs/QUERY-FORMAT.md); this section is the tour.
|
|
140
|
+
|
|
141
|
+
```javascript
|
|
142
|
+
import { compileJsonQuery } from '@jarenjs/json';
|
|
143
|
+
|
|
144
|
+
const query = compileJsonQuery({
|
|
145
|
+
$for: { b: '$.store.book[*]' },
|
|
146
|
+
$where: { $lt: ['$b.price', 10] },
|
|
147
|
+
$orderby: '$b.price',
|
|
148
|
+
$return: { title: '$b.title', price: '$b.price' },
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
query(data);
|
|
152
|
+
// [ { title: 'Sayings of the Century', price: 8.95 },
|
|
153
|
+
// { title: 'Moby Dick', price: 8.99 } ]
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### Three rules
|
|
157
|
+
|
|
158
|
+
Every JSON value is an expression under three rules — there is no rule four:
|
|
159
|
+
|
|
160
|
+
1. **Objects** partition by their keys. All keys `$`-prefixed → an **operator phrase** from a closed vocabulary (`{"$lt": [a, b]}`, the FLWOR phrase, ...). No key `$`-prefixed → a **map constructor**: keys are literal member names, values are evaluated. Mixed → compile error.
|
|
161
|
+
2. **Strings** starting with `$` are **queries**: a complete RFC 9535 JSONPath (`"$.store.book[*]"`), or a variable-rooted path (`"$b.title"`). `"$$x"` escapes to the literal string `"$x"`; any other string is itself.
|
|
162
|
+
3. **Scalars** (`42`, `true`, `null`) are literals; **arrays** are array constructors whose element sequences flatten, XQuery-style.
|
|
163
|
+
|
|
164
|
+
Escape hatches: `{"$const": v}` quotes any value verbatim; `{"$map": [[keyExpr, valExpr], ...]}` constructs objects with computed keys. That is the entire encoding.
|
|
165
|
+
|
|
166
|
+
### The degenerate query is a JSONPath string
|
|
167
|
+
|
|
168
|
+
A bare JSONPath string is a complete query document, so the on-ramp from "I use JSONPath" to "I need a join" is a one-liner at each step:
|
|
169
|
+
|
|
170
|
+
```javascript
|
|
171
|
+
import { queryJson } from '@jarenjs/json';
|
|
172
|
+
|
|
173
|
+
queryJson('$.store.book[?@.price < 10].title', data);
|
|
174
|
+
// [ 'Sayings of the Century', 'Moby Dick' ]
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
### Joins
|
|
178
|
+
|
|
179
|
+
Cross-variable predicates — the thing path filters fundamentally cannot express — are a `$where` away (this is the spec's example A.3):
|
|
180
|
+
|
|
181
|
+
```javascript
|
|
182
|
+
queryJson({
|
|
183
|
+
$for: { b: '$.store.book[*]', r: '$.ratings[*]' },
|
|
184
|
+
$where: { $eq: ['$b.isbn', '$r.isbn'] },
|
|
185
|
+
$orderby: '$b.price',
|
|
186
|
+
$return: { title: '$b.title', stars: '$r.stars' },
|
|
187
|
+
}, data);
|
|
188
|
+
// [ { title: 'Moby Dick', stars: 4 },
|
|
189
|
+
// { title: 'The Lord of the Rings', stars: 5 } ]
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
Books without an `isbn` drop out on their own: `$b.isbn` evaluates to the empty sequence and an existential `$eq` over an empty sequence is `false`.
|
|
193
|
+
|
|
194
|
+
### Grouping
|
|
195
|
+
|
|
196
|
+
XQuery 3.1 `group by` semantics: grouping keys become singleton variables, every other variable rebinds to the sequence of its values across the group (spec example A.4):
|
|
197
|
+
|
|
198
|
+
```javascript
|
|
199
|
+
queryJson({
|
|
200
|
+
$for: { b: '$.store.book[*]' },
|
|
201
|
+
$groupby: { genre: '$b.category' },
|
|
202
|
+
$return: { genre: '$genre', count: { $count: '$b' }, avg: { $avg: '$b.price' } },
|
|
203
|
+
}, data);
|
|
204
|
+
// [ { genre: 'reference', count: 1, avg: 8.95 },
|
|
205
|
+
// { genre: 'fiction', count: 3, avg: 14.99 } ]
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
### The FLWOR phrase
|
|
209
|
+
|
|
210
|
+
One phrase carries the whole pipeline. Clauses apply in **fixed semantic order regardless of JSON key order** (JSON key order is not interoperable, so it never carries meaning here); exotic interleavings nest phrases:
|
|
211
|
+
|
|
212
|
+
| Key | Value | Presence |
|
|
213
|
+
|---|---|---|
|
|
214
|
+
| `$for` | iteration bindings `{ name: source, ... }`, positional form `{ "$in": expr, "$at": "i" }` | at least one of `$for`/`$let` |
|
|
215
|
+
| `$let` | sequence bindings (no iteration) | at least one of `$for`/`$let` |
|
|
216
|
+
| `$where` | tuple filter (effective boolean value) | optional |
|
|
217
|
+
| `$groupby` | grouping-key bindings | optional |
|
|
218
|
+
| `$orderby` | key spec or list: `{ "$key": e, "$dir": "desc", "$empty": "greatest" }` | optional |
|
|
219
|
+
| `$count` | variable name for the 0-based tuple index | optional |
|
|
220
|
+
| `$return` | the result expression per surviving tuple | required |
|
|
221
|
+
|
|
222
|
+
Semantic order: `$for → $let → $where → $groupby → $orderby → $count → $return`. Quantifiers are their own two-key phrases: `{"$some": bindings, "$satisfies": expr}` / `{"$every": ...}`, with short-circuit evaluation.
|
|
223
|
+
|
|
224
|
+
The operator library (58 operators: comparisons, IEEE-double arithmetic, logic, strings with I-Regexp `$match`/`$search`/`$replace`, aggregates, sequence tools like `$distinct`/`$subsequence`/`$range`, type predicates and casts, `$coalesce`) is cataloged in [QUERY-FORMAT.md §8](./docs/QUERY-FORMAT.md#8-operators).
|
|
225
|
+
|
|
226
|
+
### External parameters
|
|
227
|
+
|
|
228
|
+
A variable no phrase binds is an **external**: use is the declaration. The compiled query exposes the collected names and takes bindings at call time:
|
|
229
|
+
|
|
230
|
+
```javascript
|
|
231
|
+
const cheaper = compileJsonQuery({
|
|
232
|
+
$for: { b: '$.store.book[*]' },
|
|
233
|
+
$where: { $le: ['$b.price', '$maxPrice'] },
|
|
234
|
+
$return: '$b.title',
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
cheaper.externals; // ['maxPrice']
|
|
238
|
+
cheaper(data, { maxPrice: 9 }); // [ 'Sayings of the Century', 'Moby Dick' ]
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
The compiled function also carries `query.first(data, externals)`, `query.exists(data, externals)` and `query.doc` (a frozen copy of the query document). Results come back as plain JSON: `undefined` for the empty sequence, the item for a singleton, an array for anything longer. `queryJson(doc, data, externals)` is the cached one-call form.
|
|
242
|
+
|
|
243
|
+
### XQuery semantics, stated deviations
|
|
244
|
+
|
|
245
|
+
Wherever this format does not explicitly deviate, **XQuery 3.1 defines the behavior** — sequences flatten, comparisons are existential, `$where` takes the effective boolean value, `$groupby` is XQuery `group by`. The deviations are few, numbered, and normative ([QUERY-FORMAT.md §11](./docs/QUERY-FORMAT.md#11-deviations-from-xquery-31)): D1 all numbers are IEEE doubles, D2 `$eq` is deep structural JSON equality, D3 the EBV of an object/array is `true`, D4 `$for` unpacks array items one level, D5 regexes are I-Regexp not XSD, D6 all positions are 0-based, D7 fixed clause order.
|
|
246
|
+
|
|
247
|
+
> **The two filter dialects.** A filter inside a path string (`"$.a[?@.b == @.c]"`) keeps **RFC 9535 semantics**, where two missing members compare equal (`Nothing == Nothing` is true). The same comparison at query level (`{"$eq": ["$x.b", "$x.c"]}`) keeps **XQuery semantics**, where a comparison over empty sequences is `false`. Both dialects are conformant to their own standard; the spec documents the split in [§5.2](./docs/QUERY-FORMAT.md#52-the-two-filter-dialects) and query variables are deliberately not visible inside path filters — a predicate that needs two variables belongs in `$where`.
|
|
248
|
+
|
|
249
|
+
### Errors carry a docPath
|
|
250
|
+
|
|
251
|
+
Compile errors (`JsonQueryCompileError`, codes `JQ0xxx`) and runtime errors (`JsonQueryRuntimeError`, codes `JQ2xxx`) both carry a stable `code` and a `docPath` — an RFC 6901 JSON Pointer **into the query document** locating the offending construct (e.g. `/$where/$eq/1`). Unknown operators come with a "did you mean" suggestion.
|
|
252
|
+
|
|
253
|
+
### The schema twins
|
|
254
|
+
|
|
255
|
+
The complete structural grammar of the language is published as JSON Schema, twice:
|
|
256
|
+
|
|
257
|
+
- [`schemas/jaren-query.schema.json`](./schemas/jaren-query.schema.json) — canonical, draft 2020-12;
|
|
258
|
+
- [`schemas/jaren-query.draft-07.schema.json`](./schemas/jaren-query.draft-07.schema.json) — a mechanically derived draft-07 twin.
|
|
259
|
+
|
|
260
|
+
Both exist because the schema is authored in a draft-neutral keyword subset (no `$ref` siblings, no `unevaluated*`, no tuples), so ecosystems pinned to draft-07 — several structured-output stacks among them — get an identical grammar for one `$defs`→`definitions` rename. Every fixture in the test suite validates under both drafts.
|
|
261
|
+
|
|
262
|
+
## Generating queries with LLMs
|
|
263
|
+
|
|
264
|
+
A query language whose entire grammar is one JSON Schema is a natural fit for **constrained decoding** — the structured-output mode of every major LLM API. Hand the schema to the provider and the model cannot emit an unknown operator, a three-argument `$eq`, or a mixed `$`/plain-key object; what remains is validated and compiled in two lines, and every failure carries a `docPath` you can feed back to the model for repair:
|
|
265
|
+
|
|
266
|
+
```javascript
|
|
267
|
+
import { readFile } from 'node:fs/promises';
|
|
268
|
+
import { JarenValidator } from '@jarenjs/validate';
|
|
269
|
+
import { compileJsonQuery } from '@jarenjs/json';
|
|
270
|
+
|
|
271
|
+
const schema = JSON.parse(await readFile('packages/json/schemas/jaren-query.schema.json', 'utf8'));
|
|
272
|
+
|
|
273
|
+
const text = await generate(prompt, schema); // your provider's structured-output call
|
|
274
|
+
|
|
275
|
+
const isQueryDoc = new JarenValidator().compile(schema);
|
|
276
|
+
const doc = JSON.parse(text);
|
|
277
|
+
if (!isQueryDoc(doc)) throw new Error('model escaped the schema');
|
|
278
|
+
const query = compileJsonQuery(doc); // JsonQueryCompileError.docPath on the residue
|
|
279
|
+
query(data); // JsonQueryRuntimeError.docPath at runtime
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
The validate step is not redundant: provider structured-output implementations support varying JSON Schema subsets regardless of the draft they declare (`patternProperties` and `propertyNames`, which the schema uses for binding names and map constructors, are not universally enforced). Validating locally catches whatever the provider's subset let through. A simplified "LLM profile" of the schema — trading some precision for the lowest-common-denominator subset — is a possible follow-up; it does not exist today.
|
|
283
|
+
|
|
284
|
+
Query documents are plain JSON, so they travel through the rest of an LLM toolchain as-is: function-call arguments, retrieval filters, audit logs, replay.
|
|
285
|
+
|
|
286
|
+
## The XQuery text front-end
|
|
287
|
+
|
|
288
|
+
`@jarenjs/json/xquery` ships `parseXQuery(text)`, a parser for a defined subset of XQuery 3.1 *text* syntax that emits Jaren JSON Query documents — a human authoring syntax and the compatibility bridge that makes W3C QT3 test material runnable against the engine (see `benchmark/qt3-runner.js`, `npm run benchmark:qt3`). It is not a second engine: the output is always a query document, and where the two languages disagree, the JSON format wins. Subset boundaries, the 1-based/0-based adjustment rules and the function mapping table are documented in [docs/XQUERY-FRONTEND.md](./docs/XQUERY-FRONTEND.md).
|
|
289
|
+
|
|
290
|
+
```javascript
|
|
291
|
+
import { compileXQuery } from '@jarenjs/json/xquery';
|
|
292
|
+
|
|
293
|
+
const q = compileXQuery('for $b in $doc?store?book?* where $b?price lt 10 return $b?title');
|
|
294
|
+
q(null, { doc: data }); // [ 'Sayings of the Century', 'Moby Dick' ]
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
## Query benchmark
|
|
298
|
+
|
|
299
|
+
`npm run benchmark:jsonquery` runs the scenario matrix against [fontoxpath](https://www.npmjs.com/package/fontoxpath) (a real XQuery 3.1 engine in JavaScript — the closest honest comparison) and [jsonata](https://www.npmjs.com/package/jsonata) (the popular practical alternative), asserting result equivalence on every document before timing anything. Each engine runs the same scenario written idiomatically in its own language (`benchmark/adaptors/jsonquery/`).
|
|
300
|
+
|
|
301
|
+
Measured with `npm run benchmark:jsonquery:profile` (2026-07-17, Node v24.14.0; ratios are that engine's time over Jaren's):
|
|
302
|
+
|
|
303
|
+
| Scenario | Jaren | fontoxpath 3.34 | jsonata 2.2 |
|
|
304
|
+
|---|---|---|---|
|
|
305
|
+
| **4-book bookstore** | | | |
|
|
306
|
+
| singular access `$b.title` | 382 ns (2.6M/s) | 7.3 µs (19x) | 5.7 µs (15x) |
|
|
307
|
+
| filter + project (spec A.2) | 1.9 µs (527k/s) | 36.1 µs (19x) | 27.0 µs (14x) |
|
|
308
|
+
| join (spec A.3) | 5.6 µs (178k/s) | 77.5 µs (14x) | 111.7 µs (20x) |
|
|
309
|
+
| group + aggregate (spec A.4) | 4.3 µs (234k/s) | n/a | 47.7 µs (11x) |
|
|
310
|
+
| deep reshape | 2.6 µs (393k/s) | 190.5 µs (75x) | 82.9 µs (33x) |
|
|
311
|
+
| **10,000-book bookstore** | | | |
|
|
312
|
+
| singular access | 299 ns (3.3M/s) | 16.6 µs (56x) | 4.2 µs (14x) |
|
|
313
|
+
| filter + project | 1.5 ms | 328.3 ms (215x) | 68.0 ms (45x) |
|
|
314
|
+
| join (measured at 1,000 books) | 27.8 ms | 1.57 s (57x) | 1.75 s (63x) |
|
|
315
|
+
| group + aggregate | 2.5 ms | n/a | 39.9 ms (16x) |
|
|
316
|
+
| deep reshape | 3.6 ms | 422.0 ms (118x) | 119.0 ms (33x) |
|
|
317
|
+
| **compile, µs per query** | 24.3 µs | 460.7 µs (19x) | 66.1 µs (2.7x) |
|
|
318
|
+
|
|
319
|
+
Honest caveats — what each competitor is optimized for:
|
|
320
|
+
|
|
321
|
+
- **fontoxpath** is an XML-first XPath/XQuery engine; JSON rides on XDM maps and arrays. The benchmark pre-converts each document to XDM *once, outside the timed loop* (per-call conversion would cost ~12 ms alone at 10k books), and fontoxpath has no public compile-only API, so its compile number is fresh-source evaluation minus cached re-evaluation. It does not implement `group by`. Its engineering effort goes into DOM navigation, buckets and XQuery Update — not JSON throughput.
|
|
322
|
+
- **jsonata** is a tree-walking interpreter whose `evaluate()` is async since 2.x; its numbers include that promise overhead because its API imposes it. It is optimized for expressiveness and embeddability, not raw speed.
|
|
323
|
+
- **Jaren**'s compile number includes `JSON.parse` of the query text, since the competitors parse text too.
|
|
324
|
+
- The join is a naive O(books × ratings) nested loop in **all three** engines (Jaren's hash-join optimizer is roadmap); it is measured at 1,000 books.
|
|
325
|
+
|
|
326
|
+
## JSLT — declarative JSON transformation
|
|
327
|
+
|
|
328
|
+
JSLT adds XSLT-style recursive template dispatch without adding another data language: **match = JSONPath, type = JSON Schema, produce = Jaren queries**. A rule can select by position, by shape, or by both; a schema is therefore a pattern, compiled through the same type-test hook as `$valid`/`$assert`/`$as`. The normative contract is [JSLT-FORMAT.md](./docs/JSLT-FORMAT.md).
|
|
329
|
+
|
|
330
|
+
The small-but-important use case is a surgical override. Unmatched containers recurse into their children and return the original object when every child is unchanged, so unrelated subtrees remain shared:
|
|
331
|
+
|
|
332
|
+
```javascript
|
|
333
|
+
import { compileJsltStylesheet } from '@jarenjs/json/jslt';
|
|
334
|
+
|
|
335
|
+
const applyVat = compileJsltStylesheet([
|
|
336
|
+
{ match: '$..price', body: { $mul: ['$', 1.21] } },
|
|
337
|
+
]);
|
|
338
|
+
|
|
339
|
+
const input = {
|
|
340
|
+
catalog: { books: [{ title: 'A', price: 10 }] },
|
|
341
|
+
meta: { publisher: 'N' },
|
|
342
|
+
};
|
|
343
|
+
const output = applyVat(input);
|
|
344
|
+
// output.catalog.books[0].price === 12.1
|
|
345
|
+
// output.meta === input.meta
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
Modes walk the same source through independent rule sets. Here the root applies the section list once as a table of contents and once as rendered body content:
|
|
349
|
+
|
|
350
|
+
```javascript
|
|
351
|
+
const renderGuide = compileJsltStylesheet({
|
|
352
|
+
$jslt: '0.1',
|
|
353
|
+
rules: [
|
|
354
|
+
{
|
|
355
|
+
match: '$',
|
|
356
|
+
body: {
|
|
357
|
+
toc: [{ $apply: ['$.sections[*]', 'toc'] }],
|
|
358
|
+
body: [{ $apply: ['$.sections[*]', 'render'] }],
|
|
359
|
+
},
|
|
360
|
+
},
|
|
361
|
+
{
|
|
362
|
+
mode: 'toc',
|
|
363
|
+
match: '$.sections[*]',
|
|
364
|
+
body: { ref: '$.id', label: '$.heading' },
|
|
365
|
+
},
|
|
366
|
+
{
|
|
367
|
+
mode: 'render',
|
|
368
|
+
match: '$.sections[*]',
|
|
369
|
+
body: { anchor: '$.id', text: '$.text' },
|
|
370
|
+
},
|
|
371
|
+
],
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
renderGuide({
|
|
375
|
+
sections: [{ id: 'intro', heading: 'Introduction', text: 'Start here.' }],
|
|
376
|
+
});
|
|
377
|
+
// { toc: [{ ref: 'intro', label: 'Introduction' }],
|
|
378
|
+
// body: [{ anchor: 'intro', text: 'Start here.' }] }
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
The brackets around each `$apply` are deliberate: an object member holds one value, while `$apply` returns a sequence. `children: [{ $apply: '$.children[*]' }]` uses the query array-constructor rule to collect that sequence into an array; the bare member form fails when two or more children are produced.
|
|
382
|
+
|
|
383
|
+
The default unmatched disposition is `share`: unchanged containers retain `===` identity with the input. Use envelope-level `"unmatched": "fresh"` when the caller needs an independently mutable tree; use `"error"` for exhaustive dispatch. Rule-body outputs and path results still follow query-engine sharing semantics.
|
|
384
|
+
|
|
385
|
+
Shape matches and schema operators need the validator bridge at application wiring time—the JSON package itself remains validator-independent:
|
|
386
|
+
|
|
387
|
+
```javascript
|
|
388
|
+
import { createTypeTestCompiler } from '@jarenjs/validate/query';
|
|
389
|
+
|
|
390
|
+
const annotateBooks = compileJsltStylesheet([
|
|
391
|
+
{
|
|
392
|
+
match: {
|
|
393
|
+
schema: { type: 'object', required: ['title', 'author'] },
|
|
394
|
+
},
|
|
395
|
+
body: {
|
|
396
|
+
title: '$.title',
|
|
397
|
+
byline: { $concat: ['$.title', ' by ', '$.author'] },
|
|
398
|
+
},
|
|
399
|
+
},
|
|
400
|
+
], { compileTypeTest: createTypeTestCompiler() });
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
The complete stylesheet grammar is published for validators and LLM constrained decoding as [`jaren-jslt.schema.json`](./schemas/jaren-jslt.schema.json) (draft 2020-12) and its mechanically derived [`jaren-jslt.draft-07.schema.json`](./schemas/jaren-jslt.draft-07.schema.json) twin. Rule-body definitions are mechanically copied from the query artifact and extended only with `$apply`, so the query vocabulary stays closed. Provider structured-output implementations still support uneven schema subsets; validate the generated document locally before compiling it, as described in [JSLT-FORMAT Appendix B](./docs/JSLT-FORMAT.md#appendix-b-llm-structured-output-non-normative).
|
|
404
|
+
|
|
405
|
+
### JSLT benchmark
|
|
406
|
+
|
|
407
|
+
`npm run benchmark:jslt` asserts result equivalence before timing Jaren against a hand-written recursive JavaScript transform and JSONata's transform operator. Measured with `npm run benchmark:jslt:profile` (2026-07-17, Node v24.14.0; competitor ratios are competitor time over Jaren):
|
|
408
|
+
|
|
409
|
+
| Scenario | Jaren JSLT | native JS | jsonata 2.2 |
|
|
410
|
+
|---|---:|---:|---:|
|
|
411
|
+
| **4-book bookstore** | | | |
|
|
412
|
+
| identity (`share`) | 81 ns | 2.04 µs (25.3x) | 14.60 µs (180x) |
|
|
413
|
+
| surgical prices | 9.15 µs | 1.09 µs (0.12x) | 90.13 µs (9.8x) |
|
|
414
|
+
| reshape + modes | 8.70 µs | 431 ns (0.050x) | n/a |
|
|
415
|
+
| fresh schema annotation | 3.50 µs | 911 ns (0.26x) | 136.92 µs (39.1x) |
|
|
416
|
+
| **10,000-book bookstore** | | | |
|
|
417
|
+
| identity (`share`) | 24 ns | 3.41 ms (143,482x) | 13.52 ms (569,663x) |
|
|
418
|
+
| surgical prices | 23.17 ms | 2.00 ms (0.086x) | 152.48 ms (6.6x) |
|
|
419
|
+
| reshape + modes | 20.01 ms | 137.36 µs (0.007x) | n/a |
|
|
420
|
+
| fresh schema annotation | 6.40 ms | 1.73 ms (0.27x) | 277.33 ms (43.4x) |
|
|
421
|
+
| **document-independent** | | | |
|
|
422
|
+
| compile, per stylesheet | 33.16 µs | n/a | 70.50 µs (2.1x) |
|
|
423
|
+
|
|
424
|
+
The identity row is the sharing fast path: Jaren returns the input reference in O(1), while native and JSONata deep-copy. On actual transformations, hand-written JavaScript is 3.4–143x faster because it is bespoke code with no matcher, rank table, mode, schema, or error machinery—the honest cost of the abstraction. Jaren is 6.6–45x faster than JSONata where the transform operator can express the scenario; reshape+modes is `n/a`, not silently replaced by a different JSONata feature. JSONata 2.x timings include its required promise overhead and transform-copy cost. Scaled prices are rounded to cents because JSONata's copy normalizes long binary decimal tails and these scenarios do not sort. fontoxpath is excluded because it has XPath/XQuery but no XSLT dispatcher; Saxon-JS is excluded as a heavyweight SEF/XSLT toolchain for this benchmark workspace.
|
|
425
|
+
|
|
426
|
+
## JTLT — template-driven text output
|
|
427
|
+
|
|
428
|
+
JSLT transforms JSON into JSON. JTLT points the same dispatcher at **text**: a template is a JSLT-shaped rule document whose bodies are *segment lists* — literal text, interpolated queries, and `$apply` splices — and whose result is a string. It is the T4/XSLT-`method="text"` analogue of this stack, and like the XQuery module it is a **front-end, not a second engine**: `compileJtltStylesheet` desugars the template into an ordinary JSLT 0.1 stylesheet (inspectable as `render.stylesheet`) and serializes the dispatched result, so dispatch, modes, conflict resolution and schema matching are inherited, not reimplemented. The normative contract is [JTLT-FORMAT.md](./docs/JTLT-FORMAT.md); this section is the tour.
|
|
429
|
+
|
|
430
|
+
```javascript
|
|
431
|
+
import { compileJtltStylesheet } from '@jarenjs/json/jtlt';
|
|
432
|
+
|
|
433
|
+
const listBooks = compileJtltStylesheet([
|
|
434
|
+
{ match: '$', body: ['# Books\n', { $apply: '$.store.book[*]' }] },
|
|
435
|
+
{ match: '$.store.book[*]', body: ['- ', '$.title', ' (', '$.price', ')\n'] },
|
|
436
|
+
]);
|
|
437
|
+
|
|
438
|
+
listBooks(data);
|
|
439
|
+
// '# Books\n' +
|
|
440
|
+
// '- Sayings of the Century (8.95)\n' +
|
|
441
|
+
// '- Sword of Honour (12.99)\n' + ...
|
|
442
|
+
```
|
|
443
|
+
|
|
444
|
+
Segments follow the query format's own string rules: a string is literal text unless it starts with `$` (a query expression, interpolated), and `"$$x"` escapes the literal text `"$x"`. Objects are operator phrases evaluated as expressions; three forms are special at segment level: `{ "$apply": ... }` splices the dispatched output of other rules in place, `{ "$raw": e }` interpolates without escaping, and `{ "$json": e }` embeds data as `JSON.stringify` text. Sequence-valued interpolations join with a single space (the XSLT `value-of` separator default), the empty sequence renders nothing, and interpolating an object or array is a runtime error that names the offending segment — dispatch into containers with `$apply` instead.
|
|
445
|
+
|
|
446
|
+
The envelope's `output` member selects the serialization method. `"text"` (the default) writes everything raw; `"xml"` escapes interpolated data while literal template text stays raw markup — the XSLT/T4 contract exactly:
|
|
447
|
+
|
|
448
|
+
```javascript
|
|
449
|
+
const toXml = compileJtltStylesheet({
|
|
450
|
+
$jtlt: '0.1',
|
|
451
|
+
output: 'xml',
|
|
452
|
+
rules: [
|
|
453
|
+
{ match: '$', body: ['<books>', { $apply: '$.store.book[*]' }, '</books>'] },
|
|
454
|
+
{ match: '$.store.book[*]', body: ['<book title="', '$.title', '"/>'] },
|
|
455
|
+
],
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
toXml(data);
|
|
459
|
+
// '<books><book title="Sayings of the Century"/>...</books>'
|
|
460
|
+
// interpolated data is XML-escaped; literal markup passes through raw
|
|
461
|
+
```
|
|
462
|
+
|
|
463
|
+
Unmatched nodes follow the XSLT built-in template rules, restated for JSON: containers apply templates to every child in document order, atoms emit their (method-escaped) string value — so `{ $apply: '$.title' }` doubles as a value-of with rule-override capability. A matchless rule replaces that default; `priority` conflicts resolve exactly as in JSLT, with the band at and below `-1e307` reserved for the built-ins. Modes, schema matches (via `options.compileTypeTest`), user externals and the reserved `$root`/`$path` parameters all work as in JSLT. Compile and runtime errors carry stable `TL`-prefixed codes and a `docPath` into the **template** document (engine errors are remapped from the compiled stylesheet back to the author's source). `renderText(template, data, externals?)` is the cached one-call form.
|
|
464
|
+
|
|
465
|
+
## Roadmap
|
|
466
|
+
|
|
467
|
+
This package's roadmap lives in the repository-wide [ROADMAP](../../ROADMAP.md), under its `@jarenjs/json` sections: the query filter optimizer and hash joins, lazy sequences, the JSLT single-walk matcher, write operations and JSON Patch, custom JSONPath function extensions, canonical JSON, XQuery front-end `xs:*` casts, and more. Recently landed from that list: JSON Schema as the query type system (`$valid`/`$assert`/`$as`), the inverse [`$query` keyword](../validate/README.md) in the validator, compiled JSON Pointers, and the complete JSLT template layer.
|
|
468
|
+
|
|
469
|
+
## Development
|
|
470
|
+
|
|
471
|
+
Unit tests live in `test/json/` at the repository root (`npm run test:json`); the JSONPath tests are built from the RFC's own examples, the query and JSLT tests from their normative fixtures (each schema corpus validates against both artifact drafts), and every example in this README runs in `test/json/readme-examples.test.js`. This package's internals are described in its own [ARCHITECTURE](./ARCHITECTURE.md) document. Benchmarks (all documented in the [benchmark workspace README](../../benchmark/README.md)): `benchmark/jsonpath.js` (JSONPath compliance + performance), `benchmark/jsonpointer.js` (compiled pointers vs the interpretive resolver and the `jsonpointer` npm package), `benchmark/jsonquery.js` (query engine vs fontoxpath/jsonata), `benchmark/jslt.js` (stylesheet engine vs native JS/JSONata), `benchmark/qt3-runner.js` (W3C QT3 scorecard through the XQuery front-end). See the repository [README](../../README.md) and [ARCHITECTURE](../../ARCHITECTURE.md) for the monorepo picture.
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export declare function isValidJSONCheap(data: any): boolean;
|
|
2
|
+
export declare function isValidJSON(data: any): boolean;
|
|
3
|
+
export declare function isValidJSONPointer(str: any): boolean;
|
|
4
|
+
export declare function isValidJSONPointerUriFragment(str: any): boolean;
|
|
5
|
+
export declare function isValidRelativeJSONPointer(str: any): boolean;
|
|
6
|
+
/**
|
|
7
|
+
* Validates a JSONPath expression string per RFC 9535.
|
|
8
|
+
*
|
|
9
|
+
* JSONPath syntax includes:
|
|
10
|
+
* - $ - root node selector
|
|
11
|
+
* - @ - current node selector (used in filter expressions)
|
|
12
|
+
* - .name - dot notation for child member
|
|
13
|
+
* - ['name'] or ["name"] - bracket notation for child member
|
|
14
|
+
* - [index] - array index
|
|
15
|
+
* - [*] - wildcard selector
|
|
16
|
+
* - [start:end:step] - array slice
|
|
17
|
+
* - [?expression] - filter expression
|
|
18
|
+
* - .. - recursive descent
|
|
19
|
+
*
|
|
20
|
+
* @param {string} str - The JSONPath expression to validate
|
|
21
|
+
* @returns {boolean} - True if the string is a valid JSONPath expression
|
|
22
|
+
* @example
|
|
23
|
+
* isValidJSONPath('$.store.book[0].title'); // true
|
|
24
|
+
* isValidJSONPath('$..name'); // true
|
|
25
|
+
* isValidJSONPath('$[*]'); // true
|
|
26
|
+
* isValidJSONPath('$[?(@.price < 10)]'); // true
|
|
27
|
+
* isValidJSONPath('$.store.book[0:5]'); // true
|
|
28
|
+
* isValidJSONPath('@.name'); // true (current node selector)
|
|
29
|
+
* isValidJSONPath('store'); // false (must start with $ or @)
|
|
30
|
+
* isValidJSONPath(''); // false (empty string)
|
|
31
|
+
*/
|
|
32
|
+
export declare function isValidJSONPath(str: string): boolean;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compile a normalized stylesheet model into the reusable transformation
|
|
3
|
+
* evaluator and its user-external metadata.
|
|
4
|
+
* @param {object} model - result of normalizeJsltStylesheet
|
|
5
|
+
* @param {object} [options] - compile options
|
|
6
|
+
* @returns {{evaluate: Function, externals: readonly string[]}}
|
|
7
|
+
*/
|
|
8
|
+
export declare function compileJsltDispatch(model: object, options?: object): {
|
|
9
|
+
evaluate: Function;
|
|
10
|
+
externals: readonly string[];
|
|
11
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Error thrown when a JSLT stylesheet is rejected at compile time
|
|
3
|
+
* (`JT0xxx` codes).
|
|
4
|
+
*/
|
|
5
|
+
export declare class JsltCompileError extends Error {
|
|
6
|
+
code: any;
|
|
7
|
+
docPath: any;
|
|
8
|
+
constructor(code: any, message: any, docPath: any, cause?: undefined);
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Error thrown when evaluating a compiled JSLT stylesheet fails
|
|
12
|
+
* (`JT2xxx` codes).
|
|
13
|
+
*/
|
|
14
|
+
export declare class JsltRuntimeError extends Error {
|
|
15
|
+
code: any;
|
|
16
|
+
docPath: any;
|
|
17
|
+
constructor(code: any, message: any, docPath: any, cause?: undefined);
|
|
18
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
export { JsltCompileError, JsltRuntimeError } from './errors.js';
|
|
2
|
+
/**
|
|
3
|
+
* Compile a Jaren JSLT 0.1 stylesheet into a reusable transformation.
|
|
4
|
+
*
|
|
5
|
+
* The returned function maps the internal sequence result to plain JSON:
|
|
6
|
+
* `undefined` for the empty sequence, the item itself for a singleton,
|
|
7
|
+
* and an array of items for a longer sequence. Metadata:
|
|
8
|
+
*
|
|
9
|
+
* - `transform.externals` - user parameter names in first-appearance order
|
|
10
|
+
* (`root` and `path` are engine-bound and excluded)
|
|
11
|
+
* - `transform.doc` - an independent, deeply frozen stylesheet copy
|
|
12
|
+
*
|
|
13
|
+
* @param {any} doc - a bare rule array or `{"$jslt":"0.1","rules":[]}`
|
|
14
|
+
* stylesheet envelope
|
|
15
|
+
* @param {object} [options] - compile options
|
|
16
|
+
* @param {(schemaJson: any, docPath: string) => ((value: any) => boolean)}
|
|
17
|
+
* [options.compileTypeTest] - validator-agnostic hook compiling schema
|
|
18
|
+
* match conditions and schema literals inside query bodies
|
|
19
|
+
* @param {number} [options.maxDepth=1024] - maximum dispatch nesting depth
|
|
20
|
+
* @returns {function} reusable `transform(data, externals?)` function
|
|
21
|
+
* @throws {import('./errors.js').JsltCompileError} when compilation fails
|
|
22
|
+
* @example
|
|
23
|
+
* const transform = compileJsltStylesheet([
|
|
24
|
+
* { match: '$..price', body: { $mul: ['$', 1.21] } }
|
|
25
|
+
* ]);
|
|
26
|
+
* transform({ item: { price: 10 } });
|
|
27
|
+
* // { item: { price: 12.1 } }
|
|
28
|
+
*/
|
|
29
|
+
export declare function compileJsltStylesheet(doc: any, options?: {
|
|
30
|
+
compileTypeTest?: (schemaJson: any, docPath: string) => ((value: any) => boolean);
|
|
31
|
+
maxDepth?: number;
|
|
32
|
+
}): Function;
|
|
33
|
+
/**
|
|
34
|
+
* Transform a JSON value with a JSLT stylesheet in one call.
|
|
35
|
+
* Object/array stylesheet documents are compiled once and cached by
|
|
36
|
+
* identity in a WeakMap.
|
|
37
|
+
* @param {any} stylesheet - JSLT stylesheet document
|
|
38
|
+
* @param {any} data - input JSON value
|
|
39
|
+
* @param {object} [externals] - user parameter bindings
|
|
40
|
+
* @param {object} [options] - compile options used on a cache miss
|
|
41
|
+
* @param {(schemaJson: any, docPath: string) => ((value: any) => boolean)}
|
|
42
|
+
* [options.compileTypeTest] - schema type-test compiler
|
|
43
|
+
* @param {number} [options.maxDepth=1024] - maximum dispatch nesting depth
|
|
44
|
+
* @returns {any} `undefined`, one JSON item, or an array of result items
|
|
45
|
+
* @throws {import('./errors.js').JsltCompileError} when compilation fails
|
|
46
|
+
* @throws {import('./errors.js').JsltRuntimeError} when dispatch fails
|
|
47
|
+
* @example
|
|
48
|
+
* transformJson([], { value: 1 }); // returns the input object by reference
|
|
49
|
+
*/
|
|
50
|
+
export declare function transformJson(stylesheet: any, data: any, externals?: object, options?: {
|
|
51
|
+
compileTypeTest?: (schemaJson: any, docPath: string) => ((value: any) => boolean);
|
|
52
|
+
maxDepth?: number;
|
|
53
|
+
}): any;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Normalize a frozen JSLT stylesheet document into a frozen, closure-free
|
|
3
|
+
* model with ranked per-mode rule arrays.
|
|
4
|
+
* @param {any} doc - deeply frozen stylesheet document
|
|
5
|
+
* @returns {object} frozen stylesheet model
|
|
6
|
+
* @throws {JsltCompileError} on JT0001-JT0004 shape errors
|
|
7
|
+
*/
|
|
8
|
+
export declare function normalizeJsltStylesheet(doc: any): object;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Desugar a normalized template model into a JSLT 0.1 stylesheet
|
|
3
|
+
* document. User rules keep their index; built-in rules are appended.
|
|
4
|
+
* @param {object} model - result of normalizeJtltTemplate
|
|
5
|
+
* @returns {object} a JSLT stylesheet envelope
|
|
6
|
+
* @throws {JtltCompileError} on TL0004 segment errors
|
|
7
|
+
*/
|
|
8
|
+
export declare function desugarTemplate(model: object): object;
|
|
9
|
+
/**
|
|
10
|
+
* Translate a docPath into the desugared JSLT stylesheet back into the
|
|
11
|
+
* author's template document. Best-effort: paths that cannot be walked
|
|
12
|
+
* (built-in rules, envelope members the template does not have) map to
|
|
13
|
+
* '' - the whole document - and `render.stylesheet` stays available for
|
|
14
|
+
* inspection.
|
|
15
|
+
* @param {object} model - result of normalizeJtltTemplate
|
|
16
|
+
* @param {string} docPath - pointer into the desugared stylesheet
|
|
17
|
+
* @returns {string} pointer into the template document
|
|
18
|
+
*/
|
|
19
|
+
export declare function remapDocPath(model: object, docPath: string): string;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Error thrown when a JTLT template is rejected at compile time
|
|
3
|
+
* (`TL0xxx` codes).
|
|
4
|
+
*/
|
|
5
|
+
export declare class JtltCompileError extends Error {
|
|
6
|
+
code: any;
|
|
7
|
+
docPath: any;
|
|
8
|
+
constructor(code: any, message: any, docPath: any, cause?: undefined);
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Error thrown when rendering with a compiled JTLT template fails
|
|
12
|
+
* (`TL2xxx` codes).
|
|
13
|
+
*/
|
|
14
|
+
export declare class JtltRuntimeError extends Error {
|
|
15
|
+
code: any;
|
|
16
|
+
docPath: any;
|
|
17
|
+
constructor(code: any, message: any, docPath: any, cause?: undefined);
|
|
18
|
+
}
|