@jarenjs/play 0.34.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.
@@ -0,0 +1,546 @@
1
+ //@ts-check
2
+ /**
3
+ * @file The playground's curated example library — the canonical home for
4
+ * the suite's engine examples (folded from the website + more). Each
5
+ * example presets its engine's source pane(s) and a LIST of datasets: one
6
+ * dataset shows a single run; several turn on a switcher, so the same
7
+ * source runs over each shape (see the `-shapes` / `-inputs` examples).
8
+ */
9
+
10
+ const j = (v) => JSON.stringify(v, null, 2);
11
+
12
+ /** A small bookstore, shared by several path/pointer/query examples. */
13
+ const BOOKSTORE = {
14
+ store: {
15
+ book: [
16
+ { category: 'reference', author: 'Nigel Rees', title: 'Sayings of the Century', price: 8.95 },
17
+ { category: 'fiction', author: 'Evelyn Waugh', title: 'Sword of Honour', price: 12.99 },
18
+ { category: 'fiction', author: 'Herman Melville', title: 'Moby Dick', isbn: '0-553-21311-3', price: 8.99 },
19
+ { category: 'fiction', author: 'J. R. R. Tolkien', title: 'The Lord of the Rings', isbn: '0-395-19395-8', price: 22.99 },
20
+ ],
21
+ bicycle: { color: 'red', price: 399 },
22
+ },
23
+ ratings: [
24
+ { isbn: '0-553-21311-3', stars: 4 },
25
+ { isbn: '0-395-19395-8', stars: 5 },
26
+ ],
27
+ };
28
+ const bookstore = { label: 'bookstore', data: { data: j(BOOKSTORE) } };
29
+
30
+ /** @type {import('./index.js').PlayExample[]} */
31
+ export const EXAMPLE_LIST = [
32
+ // ——— JSONPath ———
33
+ { id: 'path-authors', label: 'All authors', engine: 'path',
34
+ source: { selector: '$.store.book[*].author' }, datasets: [bookstore] },
35
+ { id: 'path-cheap', label: 'Filter: books under 10', engine: 'path',
36
+ source: { selector: '$.store.book[?@.price < 10].title' }, datasets: [bookstore] },
37
+ { id: 'path-prices', label: 'Every price, anywhere', engine: 'path',
38
+ source: { selector: '$..price' }, datasets: [bookstore] },
39
+ { id: 'path-shapes', label: 'One selector, three shapes', engine: 'path',
40
+ source: { selector: '$..name' },
41
+ datasets: [
42
+ { label: 'flat', data: { data: j({ name: 'root', child: { name: 'inner' } }) } },
43
+ { label: 'array', data: { data: j({ people: [{ name: 'Ada' }, { name: 'Alan' }] }) } },
44
+ { label: 'deep', data: { data: j({ a: { b: { c: { name: 'buried' } } } }) } },
45
+ ] },
46
+ { id: 'path-slice', label: 'Slice: first two', engine: 'path',
47
+ source: { selector: '$.store.book[0:2].title' }, datasets: [bookstore] },
48
+ { id: 'path-regex', label: 'Regex filter (I-Regexp)', engine: 'path',
49
+ source: { selector: "$.store.book[?match(@.author, '.*Tolkien')]" }, datasets: [bookstore] },
50
+ { id: 'path-last', label: 'Last element', engine: 'path',
51
+ source: { selector: '$.store.book[-1].title' }, datasets: [bookstore] },
52
+
53
+ // ——— JSON Pointer ———
54
+ { id: 'pointer-nested', label: 'A nested member', engine: 'pointer',
55
+ source: { pointer: '/store/book/1/title', location: '' }, datasets: [bookstore] },
56
+ { id: 'pointer-escaped', label: 'Escaped keys (~0 ~1)', engine: 'pointer',
57
+ source: { pointer: '/a~1b/m~0n', location: '' },
58
+ datasets: [{ label: 'tricky keys', data: { data: j({ 'a/b': { 'm~n': 'you found me' }, plain: 1 }) } }] },
59
+ { id: 'pointer-miss', label: 'A miss is NOTHING, not an error', engine: 'pointer',
60
+ source: { pointer: '/store/book/9/title', location: '' }, datasets: [bookstore] },
61
+ { id: 'pointer-relative', label: 'Relative: sibling', engine: 'pointer',
62
+ source: { pointer: '1/price', location: '/store/book/0/title' }, datasets: [bookstore] },
63
+ { id: 'pointer-relative-key', label: 'Relative: key name (0#)', engine: 'pointer',
64
+ source: { pointer: '0#', location: '/store/book/0/title' }, datasets: [bookstore] },
65
+
66
+ // ——— JSON Patch ———
67
+ { id: 'patch-basics', label: 'RFC 6902 basics', engine: 'patch',
68
+ source: { patch: j([{ op: 'replace', path: '/baz', value: 'boo' }, { op: 'add', path: '/hello', value: ['world'] }, { op: 'remove', path: '/foo' }]) },
69
+ datasets: [{ label: 'doc', data: { data: j({ baz: 'qux', foo: 'bar', numbers: [1, 2, 3] }) } }] },
70
+ { id: 'patch-append', label: 'Append to an array (the - token)', engine: 'patch',
71
+ source: { patch: j([{ op: 'add', path: '/numbers/-', value: 4 }]) },
72
+ datasets: [{ label: 'doc', data: { data: j({ numbers: [1, 2, 3] }) } }] },
73
+ { id: 'patch-move', label: 'Move a member', engine: 'patch',
74
+ source: { patch: j([{ op: 'move', from: '/a', path: '/b' }]) },
75
+ datasets: [{ label: 'doc', data: { data: j({ a: 1, keep: true }) } }] },
76
+ { id: 'patch-test', label: 'Guarded update (test op)', engine: 'patch',
77
+ source: { patch: j([
78
+ { op: 'test', path: '/version', value: 5 },
79
+ { op: 'replace', path: '/user/name', value: 'Bob' },
80
+ { op: 'add', path: '/user/tags/-', value: 'admin' },
81
+ { op: 'replace', path: '/version', value: 6 },
82
+ ]) },
83
+ datasets: [{ label: 'doc', data: { data: j({ version: 5, user: { name: 'Alice', tags: ['reader'] } }) } }] },
84
+ { id: 'patch-merge', label: 'Merge (RFC 7396)', engine: 'patch', config: { mode: 'merge' },
85
+ source: { patch: j({ age: 31, address: { zip: '10999' }, temp: null, newField: 'hello' }) },
86
+ datasets: [{ label: 'doc', data: { data: j({ name: 'Alice', age: 30, address: { city: 'Berlin', zip: '10115' }, temp: 'delete-me' }) } }] },
87
+ { id: 'patch-diff', label: 'Diff two documents', engine: 'patch', config: { mode: 'diff' },
88
+ source: { patch: j({ user: { name: 'Bob', tags: ['reader', 'admin'] }, version: 6 }) },
89
+ datasets: [{ label: 'original', data: { data: j({ user: { name: 'Alice', tags: ['reader'] }, version: 5 }) } }] },
90
+
91
+ // ——— $query ———
92
+ { id: 'query-filter', label: 'Filter + order', engine: 'query',
93
+ source: { query: j({ $for: { b: '$.store.book[*]' }, $where: { $lt: ['$b.price', 10] }, $orderby: '$b.price', $return: { title: '$b.title', price: '$b.price' } }), externals: '' },
94
+ datasets: [bookstore] },
95
+ { id: 'query-join', label: 'Join books and ratings on isbn', engine: 'query',
96
+ source: { query: j({ $for: { b: '$.store.book[*]', r: '$.ratings[*]' }, $where: { $eq: ['$b.isbn', '$r.isbn'] }, $orderby: '$b.price', $return: { title: '$b.title', stars: '$r.stars' } }), externals: '' },
97
+ datasets: [bookstore] },
98
+ { id: 'query-group', label: 'Group + aggregate', engine: 'query',
99
+ source: { query: j({ $for: { b: '$.store.book[*]' }, $groupby: { genre: '$b.category' }, $return: { genre: '$genre', count: { $count: '$b' }, avg: { $avg: '$b.price' } } }), externals: '' },
100
+ datasets: [bookstore] },
101
+ { id: 'query-fold', label: '$fold: a running total', engine: 'query',
102
+ source: { query: j({ $fold: { total: 0 }, $for: { b: '$.store.book[*]' }, $where: { $lt: ['$b.price', 10] }, $return: { $add: ['$total', '$b.price'] } }), externals: '' },
103
+ datasets: [bookstore] },
104
+ { id: 'query-external', label: 'External parameter', engine: 'query',
105
+ source: { query: j({ $for: { b: '$.store.book[*]' }, $where: { $ge: ['$b.price', '$minPrice'] }, $return: '$b.title' }), externals: j({ minPrice: 10 }) },
106
+ datasets: [bookstore] },
107
+ { id: 'query-valid', label: '$valid: schema as type test', engine: 'query',
108
+ source: { query: j({ $for: { b: '$.store.book[*]' }, $where: { $valid: ['$b', { type: 'object', required: ['isbn'], properties: { price: { maximum: 25 } } }] }, $return: '$b.title' }), externals: '' },
109
+ datasets: [bookstore] },
110
+ { id: 'query-mean', label: 'Aggregate with the registered $mean', engine: 'query',
111
+ source: { query: j({ average: { $mean: '$.readings[*]' } }), externals: '' },
112
+ datasets: [{ label: 'readings', data: { data: j({ readings: [10, 12, 14, 20, 8, 6, 30] }) } }] },
113
+
114
+ // ——— JSLT ———
115
+ { id: 'jslt-identity', label: 'Identity (proof of no change)', engine: 'jslt',
116
+ source: { stylesheet: j([]) }, datasets: [bookstore] },
117
+ { id: 'jslt-vat', label: 'Surgical: VAT on every price', engine: 'jslt',
118
+ source: { stylesheet: j([{ match: '$..price', body: { $mul: ['$', 1.21] } }]) },
119
+ datasets: [bookstore] },
120
+ { id: 'jslt-reshape', label: 'Reshape a document', engine: 'jslt',
121
+ source: { stylesheet: j({ $jslt: '0.1', rules: [{ match: '$', body: { shopColour: '$.store.bicycle.color', firstTitle: '$.store.book[0].title' } }] }) },
122
+ datasets: [bookstore] },
123
+ { id: 'jslt-schema-match', label: 'Schema match: annotate books', engine: 'jslt',
124
+ source: { stylesheet: j({ $jslt: '0.1', unmatched: 'fresh', rules: [
125
+ { match: { schema: { type: 'object', required: ['title', 'author', 'price'] } },
126
+ body: { title: '$.title', price: '$.price', label: { $concat: ['$.title', ' by ', '$.author'] } } },
127
+ ] }) },
128
+ datasets: [bookstore] },
129
+ { id: 'jslt-inputs', label: 'One stylesheet, two inputs', engine: 'jslt',
130
+ source: { stylesheet: j({ $jslt: '0.1', rules: [{ match: '$', body: { greeting: { $concat: ['Hello, ', '$.name'] } } }] }) },
131
+ datasets: [
132
+ { label: 'Ada', data: { data: j({ name: 'Ada Lovelace' }) } },
133
+ { label: 'Alan', data: { data: j({ name: 'Alan Turing' }) } },
134
+ ] },
135
+
136
+ // ——— JTLT (JSLT's text front-end) ———
137
+ { id: 'jtlt-md', label: 'Render a Markdown book list', engine: 'jtlt',
138
+ source: { template: j([
139
+ { match: '$', body: ['# Books\n\n', { $apply: '$.store.book[*]' }] },
140
+ { match: '$.store.book[*]', body: ['- **', '$.title', '** — ', '$.price', '\n'] },
141
+ ]) },
142
+ datasets: [bookstore] },
143
+ { id: 'jtlt-modes', label: 'Two modes: a TOC and the body', engine: 'jtlt',
144
+ source: { template: j({ $jtlt: '0.1', rules: [
145
+ { match: '$', body: ['TOC\n', { $apply: ['$.sections[*]', 'toc'] }, '\n', { $apply: '$.sections[*]' }] },
146
+ { mode: 'toc', match: '$.sections[*]', body: ['- ', '$.heading', '\n'] },
147
+ { match: '$.sections[*]', body: ['== ', '$.heading', ' ==\n', '$.text', '\n\n'] },
148
+ ] }) },
149
+ datasets: [{ label: 'doc', data: { data: j({ sections: [
150
+ { heading: 'Introduction', text: 'Start here.' },
151
+ { heading: 'Usage', text: 'Then this.' },
152
+ ] }) } }] },
153
+ { id: 'jtlt-xml', label: 'XML: escaping vs $raw', engine: 'jtlt',
154
+ source: { template: j({ $jtlt: '0.1', output: 'xml', rules: [
155
+ { match: '$', body: ['<notes>\n', { $apply: '$.notes[*]' }, '</notes>'] },
156
+ { match: '$.notes[*]', body: [' <note title="', '$.title', '">', { $raw: '$.markup' }, '</note>\n'] },
157
+ ] }) },
158
+ datasets: [{ label: 'notes', data: { data: j({ notes: [
159
+ { title: 'Q&A', markup: '<b>escaped attribute, raw body</b>' },
160
+ { title: "Rock 'n' roll", markup: '<i>quotes too</i>' },
161
+ ] }) } }] },
162
+ { id: 'jtlt-codegen', label: 'Codegen with $json', engine: 'jtlt',
163
+ source: { template: j([{ match: '$', body: ['export const config = ', { $json: '$' }, ';\n'] }]) },
164
+ datasets: [{ label: 'config', data: { data: j({ threshold: 10, labels: ['alpha', 'beta'] }) } }] },
165
+ { id: 'jtlt-sql', label: 'SQL DDL — SQLite', engine: 'jtlt',
166
+ source: { template: j({ $jtlt: '0.1', rules: [
167
+ { match: '$', body: ['-- SQLite schema: ', '$.database', '\n\n', { $apply: '$.tables[*]' }] },
168
+ { match: '$.tables[*]', body: ['CREATE TABLE ', '$.name', ' (\n', { $apply: '$.columns[*]' }, ' PRIMARY KEY (', '$.columns[?@.pk].name', ')\n);\n\n'] },
169
+ { match: '$.tables[*].columns[*]', body: [' ', '$.name', ' ', { $apply: ['$', 'type'] },
170
+ { $if: [{ $eq: ['$.nullable', false] }, ' NOT NULL', ''] },
171
+ { $if: [{ $eq: ['$.unique', true] }, ' UNIQUE', ''] },
172
+ { $if: ['$.references', { $concat: [' REFERENCES ', '$.references'] }, ''] },
173
+ ',\n'] },
174
+ { mode: 'type', match: { schema: { required: ['type'], properties: { type: { const: 'int' } } } }, body: ['INTEGER'] },
175
+ { mode: 'type', match: { schema: { required: ['type'], properties: { type: { const: 'decimal' } } } }, body: ['NUMERIC'] },
176
+ { mode: 'type', body: ['TEXT'] },
177
+ ] }) },
178
+ datasets: [{ label: 'shop', data: { data: j({ database: 'shop', tables: [
179
+ { name: 'customers', columns: [
180
+ { name: 'id', type: 'int', pk: true },
181
+ { name: 'email', type: 'string', maxLength: 254, nullable: false, unique: true },
182
+ { name: 'joined_on', type: 'date', nullable: false },
183
+ { name: 'notes', type: 'text' },
184
+ ] },
185
+ { name: 'orders', columns: [
186
+ { name: 'id', type: 'int', pk: true },
187
+ { name: 'customer_id', type: 'int', nullable: false, references: 'customers (id)' },
188
+ { name: 'total', type: 'decimal', nullable: false },
189
+ { name: 'placed_at', type: 'datetime', nullable: false },
190
+ ] },
191
+ ] }) } }] },
192
+
193
+ { id: 'jtlt-sql-pg', label: 'SQL DDL — PostgreSQL', engine: 'jtlt',
194
+ source: { template: j({ $jtlt: '0.1', rules: [
195
+ { match: '$', body: ['-- PostgreSQL schema: ', '$.database', '\n\n', { $apply: '$.tables[*]' }] },
196
+ { match: '$.tables[*]', body: ['CREATE TABLE ', '$.name', ' (\n', { $apply: '$.columns[*]' }, ' PRIMARY KEY (', '$.columns[?@.pk].name', ')\n);\n\n'] },
197
+ { match: '$.tables[*].columns[*]', body: [' ', '$.name', ' ', { $apply: ['$', 'type'] },
198
+ { $if: [{ $eq: ['$.nullable', false] }, ' NOT NULL', ''] },
199
+ { $if: [{ $eq: ['$.unique', true] }, ' UNIQUE', ''] },
200
+ { $if: ['$.references', { $concat: [' REFERENCES ', '$.references'] }, ''] },
201
+ ',\n'] },
202
+ // priority rules: the pk / maxLength overrides beat the plain types
203
+ { mode: 'type', priority: 1, match: { schema: { required: ['pk'], properties: { pk: { const: true } } } }, body: ['integer GENERATED ALWAYS AS IDENTITY'] },
204
+ { mode: 'type', priority: 1, match: { schema: { required: ['maxLength'] } }, body: [{ $concat: ['varchar(', '$.maxLength', ')'] }] },
205
+ { mode: 'type', match: { schema: { required: ['type'], properties: { type: { const: 'int' } } } }, body: ['integer'] },
206
+ { mode: 'type', match: { schema: { required: ['type'], properties: { type: { const: 'decimal' } } } }, body: ['numeric(12,2)'] },
207
+ { mode: 'type', match: { schema: { required: ['type'], properties: { type: { const: 'date' } } } }, body: ['date'] },
208
+ { mode: 'type', match: { schema: { required: ['type'], properties: { type: { const: 'datetime' } } } }, body: ['timestamptz'] },
209
+ { mode: 'type', body: ['text'] },
210
+ ] }) },
211
+ datasets: [{ label: 'shop', data: { data: j({ database: 'shop', tables: [
212
+ { name: 'customers', columns: [
213
+ { name: 'id', type: 'int', pk: true },
214
+ { name: 'email', type: 'string', maxLength: 254, nullable: false, unique: true },
215
+ { name: 'joined_on', type: 'date', nullable: false },
216
+ { name: 'notes', type: 'text' },
217
+ ] },
218
+ { name: 'orders', columns: [
219
+ { name: 'id', type: 'int', pk: true },
220
+ { name: 'customer_id', type: 'int', nullable: false, references: 'customers (id)' },
221
+ { name: 'total', type: 'decimal', nullable: false },
222
+ { name: 'placed_at', type: 'datetime', nullable: false },
223
+ ] },
224
+ ] }) } }] },
225
+
226
+ // ——— XQuery (the text subset → a query document, run over $doc) ———
227
+ { id: 'xquery-flwor', label: 'FLWOR: cheap books', engine: 'xquery',
228
+ source: { text: 'for $b in $doc?store?book?*\nwhere $b?price < 10\norder by $b?price\nreturn map { "title": $b?title, "price": $b?price }' },
229
+ datasets: [bookstore] },
230
+ { id: 'xquery-join', label: 'Join books and ratings on isbn', engine: 'xquery',
231
+ source: { text: 'for $b in $doc?store?book?*, $r in $doc?ratings?*\nwhere $b?isbn = $r?isbn\norder by $b?price\nreturn map { "title": $b?title, "stars": $r?stars }' },
232
+ datasets: [bookstore] },
233
+ { id: 'xquery-group', label: 'Group by category', engine: 'xquery',
234
+ source: { text: 'for $b in $doc?store?book?*\ngroup by $genre := $b?category\nreturn map { "genre": $genre, "count": count($b), "avg": avg($b?price) }' },
235
+ datasets: [bookstore] },
236
+ { id: 'xquery-quantifier', label: 'Quantifier: any 5-star?', engine: 'xquery',
237
+ source: { text: 'some $r in $doc?ratings?* satisfies $r?stars >= 5' },
238
+ datasets: [bookstore] },
239
+
240
+ // ——— JOSL (source-only: no data pane; the mode select is the toggle) ———
241
+ { id: 'josl-citizens', label: 'First-class citizens', engine: 'josl', config: { mode: 'josl' }, datasets: [],
242
+ source: { text: `# JOSL: TOML 1.0 + JavaScript's obvious types
243
+ title = "kitchen sink"
244
+ middle-name = null # TOML has no null; JOSL does (flip to TOML → error)
245
+ big = 9007199254740993 # promotes to bigint, losslessly
246
+ mask = 0xffn # bigint literal, any radix
247
+ match = /^ok[!.]?$/i # a real RegExp, validated at parse time
248
+ when = 2026-07-18T12:00:00Z # offset date-time -> Date
249
+ day = 2026-07-18 # local date -> LocalDate
250
+
251
+ [server]
252
+ host = "localhost"
253
+ ports = [ 8080, 8443 ]
254
+ ` } },
255
+ { id: 'josl-records', label: 'Record stream [[]]', engine: 'josl', config: { mode: 'josl' }, datasets: [],
256
+ source: { text: `# The most common LLM output shape: a list of records.
257
+ # Each [[]] completes the previous record -- streamable.
258
+ [[]]
259
+ name = "first"
260
+ score = 0.92
261
+ [meta]
262
+ source = "model-a"
263
+
264
+ [[]]
265
+ name = "second"
266
+ score = 0.87
267
+ tags = [ "draft" ]
268
+ ` } },
269
+ { id: 'josl-toml', label: 'Strict TOML 1.0', engine: 'josl', config: { mode: 'toml' }, datasets: [],
270
+ source: { text: `# mode: toml -- the same engine, extensions rejected.
271
+ # This dialect passes the complete official toml-test 1.0.0 suite.
272
+ title = "TOML Example"
273
+
274
+ [owner]
275
+ name = "Tom Preston-Werner"
276
+ dob = 1979-05-27T07:32:00-08:00
277
+
278
+ [[products]]
279
+ name = "Hammer"
280
+ sku = 738594937
281
+ ` } },
282
+
283
+ // ——— CSV (source-only: strict vs repair is the lesson, live) ———
284
+ { id: 'csv-rfc', label: 'RFC 4180', engine: 'csv', config: { repair: 'strict', headers: 'true', delimiter: 'auto', typed: 'off' }, datasets: [],
285
+ source: { text: `id,name,note
286
+ 1,Ada,"quoted, with a comma"
287
+ 2,Grace,"a doubled "" quote"
288
+ 3,Alan,"a field that spans
289
+ two lines"
290
+ ` } },
291
+ { id: 'csv-repair', label: 'Damaged → self-healing', engine: 'csv', config: { repair: 'repair', headers: 'true', delimiter: 'auto', typed: 'off' }, datasets: [],
292
+ source: { text: `id,name,note
293
+ 1,Ada,"never closed
294
+ 2,Grace,"ok"trailing text
295
+ 3,Alan,"he said "hi" today"
296
+ 4,Edsger
297
+ 5,Barbara,extra,column
298
+ ` } },
299
+ { id: 'csv-dialect', label: 'European dialect (sniffed)', engine: 'csv', config: { repair: 'strict', headers: 'auto', delimiter: 'auto', typed: 'on' }, datasets: [],
300
+ source: { text: `stad;inwoners;opgericht
301
+ Amsterdam;921402;1275-10-27
302
+ Rotterdam;651446;1340-06-07
303
+ Den Haag;548320;1248-01-01
304
+ ` } },
305
+ { id: 'csv-typed', label: 'Typed values', engine: 'csv', config: { repair: 'strict', headers: 'true', delimiter: ',', typed: 'on' }, datasets: [],
306
+ source: { text: `sku,price,postcode,huge,active,when
307
+ A-1,19.95,01234,123456789012345678901234567890,true,2026-07-27
308
+ A-2,4.5,00042,7,false,2026-07-27T08:30:00Z
309
+ ` } },
310
+
311
+ // ——— MDX (markdown × data: interpolation + sections over the data pane) ———
312
+ { id: 'mdx-invoice', label: 'An invoice from data', engine: 'mdx',
313
+ source: { source: `# Invoice {$.number}
314
+
315
+ Billed to **{$.customer.name}** ({$.customer.email}).
316
+
317
+ {#each $.lines as line}
318
+
319
+ **{$line.description}** — {$line.amount}
320
+
321
+ {/each}
322
+
323
+ Total: **{$.total}**
324
+
325
+ {#if $.paid}
326
+
327
+ Paid — thank you!
328
+
329
+ {/if}
330
+ ` },
331
+ datasets: [
332
+ { label: 'paid', data: { data: j({ number: 'INV-7', customer: { name: 'Ada', email: 'ada@example.com' },
333
+ lines: [{ description: 'Rubber duck', amount: 9.99 }, { description: 'Duck house', amount: 40.01 }],
334
+ total: 50, paid: true }) } },
335
+ { label: 'unpaid', data: { data: j({ number: 'INV-8', customer: { name: 'Alan', email: 'alan@example.com' },
336
+ lines: [{ description: 'Enigma manual', amount: 120 }],
337
+ total: 120, paid: false }) } },
338
+ ] },
339
+ { id: 'mdx-digest', label: 'Frontmatter binds as externals', engine: 'mdx',
340
+ source: { source: `---
341
+ title: The weekly digest
342
+ ---
343
+ # {$title}
344
+
345
+ {#each $.stories as story}
346
+ ## {$story.headline}
347
+
348
+ {$story.summary}
349
+
350
+ {/each}
351
+ ` },
352
+ datasets: [{ label: 'stories', data: { data: j({ stories: [
353
+ { headline: 'Play retires the playground', summary: 'One curated surface, calm by default.' },
354
+ { headline: 'Markdown meets data', summary: 'The same document, rendered per reader.' },
355
+ ] }) } }] },
356
+
357
+ { id: 'mdx-comment', label: 'The comment spelling', engine: 'mdx',
358
+ source: { source: `# Release <!--mdx:$.version-->0.0.0<!--/mdx-->
359
+
360
+ \`{$.path}\` is terser, and it is the right read in a document that is
361
+ always rendered against data. But it shows as literal gibberish anywhere
362
+ the transform has not run — so a document that is ALSO read raw uses the
363
+ comment spelling instead:
364
+
365
+ - shipped <!--mdx:$.date-->a while ago<!--/mdx-->
366
+ - <!--mdx:$.packages-->some<!--/mdx--> packages, one version
367
+
368
+ Same expression, same compiler, same cache. The markers survive the pass,
369
+ so the value can be re-derived; every other renderer drops them and shows
370
+ the baked text. An interpolated value lands in a TEXT node either way —
371
+ it is never re-read as markdown, which is what makes the pass safe over
372
+ data you did not write: <!--mdx:$.note-->none<!--/mdx-->
373
+ ` },
374
+ datasets: [{ label: 'release', data: { data: j({
375
+ version: '0.31.3', date: '2026-08-11', packages: 21,
376
+ note: '**not bold**, <script>not script</script>',
377
+ }) } }] },
378
+
379
+ // ——— Markdown (visual: rendered by a host renderer, source-only) ———
380
+ { id: 'md-tour', label: 'GFM tour', engine: 'markdown', datasets: [],
381
+ source: { source: `---
382
+ title: The inverse of JTLT
383
+ tags: [markdown, jaren]
384
+ ---
385
+ # Markdown, as JSON
386
+
387
+ Parse **CommonMark** with *GFM* extensions into a plain JSON AST —
388
+ then query it, transform it with JSLT, and render it as vnodes.
389
+
390
+ - [x] tables, strikethrough, task lists
391
+ - [x] footnotes[^1] and bare links like www.commonmark.org
392
+ - [ ] your ~~regex~~ hand-rolled parser
393
+
394
+ [^1]: Collected out of the flow and rendered at the end, GitHub-style.
395
+
396
+ | engine | output |
397
+ | :----- | -----: |
398
+ | JTLT | Markdown |
399
+ | @jarenjs/md | JSON |
400
+
401
+ \`\`\`js
402
+ const doc = parseMarkdown(source);
403
+ doc.ast[0].type; // 'heading'
404
+ \`\`\`
405
+
406
+ > One suite, one philosophy: parse once, run a specialized closure.
407
+ ` } },
408
+ { id: 'md-frontmatter', label: 'Frontmatter flavours', engine: 'markdown', datasets: [],
409
+ source: { source: `+++
410
+ title = "TOML up top"
411
+ weight = 3
412
+ +++
413
+ The same document works with \`---\` YAML, \`---json\`, a leading
414
+ \`{\` JSON object, or \`+++\` TOML — all normalize to plain JSON on
415
+ \`doc.frontmatter\`, and bind as JSLT externals.
416
+ ` } },
417
+ { id: 'md-anchors', label: 'Anchors, footnotes and bare links', engine: 'markdown', datasets: [],
418
+ source: { source: `# Everything linkable
419
+
420
+ Every heading gets a GitHub-compatible \`id\`, so [jump to the notes](#the-notes)
421
+ lands where you expect — the same slug GitHub mints, so one committed README
422
+ anchors identically here, on GitHub and in an editor preview.
423
+
424
+ Bare addresses become links without \`<>\`: visit www.commonmark.org/help,
425
+ read https://spec.commonmark.org/ or mail spec@commonmark.test. Trailing
426
+ punctuation stays out of the link — see www.commonmark.org/a.b. — and a
427
+ citation[^why] carries its own return path.
428
+
429
+ ## The notes
430
+
431
+ Footnotes are collected out of the flow and rendered once, at the end, in
432
+ first-reference order. An uncited definition renders nothing at all.
433
+
434
+ [^why]: Cite it twice[^why] and it grows a second back-reference.
435
+ ` } },
436
+ { id: 'md-directives', label: 'Directives: a number a machine derives', engine: 'markdown', datasets: [],
437
+ source: { source: `# Comment-carried data
438
+
439
+ Jaren is <!--bm:jsonpath.ctsRatio-->23.1<!--/bm-->x faster on the CTS mean.
440
+
441
+ Every markdown renderer on earth drops HTML comments, so that line reads as
442
+ plain, correct, static text — here, on GitHub and on npm. A directive-aware
443
+ consumer reads the marker instead and re-derives the value; \`bake()\` writes
444
+ the fresh one back into the source, so a re-derivation is a reviewable diff
445
+ rather than a number that quietly stopped being true.
446
+
447
+ This repository's own published figures work exactly this way.
448
+
449
+ <!--bm:example-->
450
+ A block directive wraps whole blocks — a table, a list, anything the
451
+ resolver produces.
452
+ <!--/bm-->
453
+
454
+ One rule: an inline marker must not begin a line. A comment at the start of
455
+ a line opens an HTML block and swallows the rest of that line.
456
+ ` } },
457
+ { id: 'md-roundtrip', label: 'Round trip is a fixed point', engine: 'markdown', datasets: [],
458
+ source: { source: `# Canonical form
459
+
460
+ Re-parsing \`toMarkdown(doc)\` yields a **deep-equal** AST: printing
461
+ is a fixed point.
462
+
463
+ 1. parse
464
+ 2. print
465
+ 3. parse again
466
+ ` } },
467
+
468
+ // ——— Mermaid (visual: geometry-free AST → pure-vnode SVG) ———
469
+ { id: 'mermaid-flow', label: 'Flowchart', engine: 'mermaid', datasets: [],
470
+ source: { source: `flowchart TD
471
+ A[Start] --> B{Is it working?}
472
+ B -->|Yes| C[Ship it]
473
+ B -->|No| D[Debug]
474
+ D --> B
475
+ C --> E((Done))` } },
476
+ { id: 'mermaid-seq', label: 'Sequence', engine: 'mermaid', datasets: [],
477
+ source: { source: `sequenceDiagram
478
+ participant A as Alice
479
+ participant B as Bob
480
+ A->>+B: Authenticate
481
+ B-->>-A: Token
482
+ note over A,B: handshake complete` } },
483
+ { id: 'mermaid-state', label: 'State diagram', engine: 'mermaid', datasets: [],
484
+ source: { source: `stateDiagram-v2
485
+ [*] --> Idle
486
+ Idle --> Running : start
487
+ Running --> Idle : stop
488
+ Running --> [*]` } },
489
+ { id: 'mermaid-pie', label: 'Pie', engine: 'mermaid', datasets: [],
490
+ source: { source: `pie showData
491
+ title Time spent
492
+ "Parsing" : 20
493
+ "Layout" : 35
494
+ "Rendering" : 45` } },
495
+
496
+ // ——— Charts (visual: JSON/JSONX/JOSL definition → SVG; static only) ———
497
+ { id: 'charts-pie', label: 'Pie', engine: 'charts', config: { format: 'json' }, datasets: [],
498
+ source: { source: j({ type: 'pie', title: 'Suite time by package', slices: [
499
+ { label: 'validate', value: 42 }, { label: 'json', value: 25 }, { label: 'view', value: 18 }, { label: 'md', value: 15 },
500
+ ] }) } },
501
+ { id: 'charts-bar', label: 'Grouped bars', engine: 'charts', config: { format: 'json' }, datasets: [],
502
+ source: { source: j({ type: 'bar', title: 'Parse profile — ms per document', valLabel: 'ms/op',
503
+ categories: ['~2 kB', '~40 kB', '~90 kB'],
504
+ series: [{ name: 'jaren', values: [0.11, 2.3, 10.7] }, { name: 'rival', values: [0.43, 6.1, 38.4] }],
505
+ }) } },
506
+ { id: 'charts-heatmap', label: 'Heatmap (log)', engine: 'charts', config: { format: 'json' }, datasets: [],
507
+ source: { source: j({ type: 'heatmap', title: 'Speed ratio by scenario × scale (log)', log: true,
508
+ xLabels: ['4 books', '100 books', '1000 books'], yLabels: ['singular', 'filter', 'join'],
509
+ values: [[220, 80, 12], [90, 30, 6], [15, 4, 1.2]],
510
+ }) } },
511
+ { id: 'charts-sankey', label: 'Sankey', engine: 'charts', config: { format: 'json' }, datasets: [],
512
+ source: { source: j({ type: 'sankey', title: 'Where visits go', links: [
513
+ { source: 'search', target: 'home', value: 40 }, { source: 'social', target: 'home', value: 15 },
514
+ { source: 'home', target: 'docs', value: 30 }, { source: 'home', target: 'play', value: 20 },
515
+ { source: 'docs', target: 'github', value: 8 },
516
+ ] }) } },
517
+
518
+ // ——— JSON Schema validation (the validate engine, host-delegated) ———
519
+ { id: 'validate-user', label: 'User', engine: 'validate', config: { locale: 'en' },
520
+ source: { schema: j({ type: 'object', title: 'User', properties: {
521
+ name: { type: 'string', minLength: 2 }, email: { type: 'string', format: 'email' },
522
+ age: { type: 'integer', minimum: 13 }, newsletter: { type: 'boolean' }, plan: { enum: ['free', 'pro'] },
523
+ tags: { type: 'array', default: [], items: { type: 'string' } },
524
+ }, required: ['name', 'email'] }) },
525
+ datasets: [{ label: 'valid', data: { data: j({ name: 'Ada', email: 'ada@example.com', age: 36, newsletter: true, plan: 'pro', tags: ['compiler'] }) } }] },
526
+ { id: 'validate-conditional', label: 'Conditional (if/then/else)', engine: 'validate', config: { locale: 'en' },
527
+ source: { schema: j({ $schema: 'https://json-schema.org/draft/2020-12/schema', type: 'object', title: 'Payment',
528
+ properties: { method: { type: 'string', enum: ['card', 'iban'] }, cardNumber: { type: 'string', pattern: '^\\d{16}$' }, iban: { type: 'string', format: 'iban' } },
529
+ required: ['method'], if: { properties: { method: { const: 'card' } }, required: ['method'] }, then: { required: ['cardNumber'] }, else: { required: ['iban'] } }) },
530
+ datasets: [{ label: 'a card payment', data: { data: j({ method: 'card', cardNumber: '4111111111111111' }) } }] },
531
+ { id: 'validate-query', label: 'Cross-field ($query)', engine: 'validate', config: { locale: 'en' },
532
+ source: { schema: j({ type: 'object', title: 'Invoice',
533
+ description: 'The $query keyword embeds a Jaren JSON Query as a cross-field assertion — the class of constraint (sums, ordering) JSON Schema is notoriously bad at.',
534
+ properties: {
535
+ lines: { type: 'array', minItems: 1, items: { type: 'object',
536
+ properties: { description: { type: 'string', minLength: 1 }, amount: { type: 'number' } }, required: ['description', 'amount'] } },
537
+ total: { type: 'number', description: 'Must equal the sum of the line amounts' },
538
+ }, required: ['lines', 'total'], $query: { $eq: ['$.total', { $sum: '$.lines[*].amount' }] } }) },
539
+ datasets: [{ label: 'balanced', data: { data: j({ lines: [
540
+ { description: 'Rubber duck', amount: 9.99 }, { description: 'Duck house', amount: 40.01 }], total: 50 }) } }] },
541
+ { id: 'validate-invalid', label: 'Invalid data (see the errors)', engine: 'validate', config: { locale: 'en' },
542
+ source: { schema: j({ type: 'object', title: 'User', properties: {
543
+ name: { type: 'string', minLength: 2 }, email: { type: 'string', format: 'email' }, age: { type: 'integer', minimum: 13 },
544
+ }, required: ['name', 'email'] }) },
545
+ datasets: [{ label: 'invalid', data: { data: j({ name: 'A', email: 'not-an-email', age: 7 }) } }] },
546
+ ];
package/src/format.js ADDED
@@ -0,0 +1,20 @@
1
+ //@ts-check
2
+ /**
3
+ * @file Display formatting shared by the engine layer (the deep "how it
4
+ * ran" stat cards) and the component layer (the stage's timing line).
5
+ */
6
+
7
+ /**
8
+ * A measured duration as a stage label. Anything that is not a number —
9
+ * including the `null` an engine reports for a phase it does not have, or
10
+ * one the host never measured — is an em dash rather than a fabricated
11
+ * `0`, which would read as "ran in no time" instead of "not measured".
12
+ * Sub-hundredth-millisecond work prints as a floor: two decimals is the
13
+ * resolution the cards claim, and `0.00 ms` would over-claim it.
14
+ * @param {unknown} ms
15
+ * @returns {string}
16
+ */
17
+ export function formatMs(ms) {
18
+ if (typeof ms !== 'number') return '—';
19
+ return ms < 0.01 ? '<0.01 ms' : `${ms.toFixed(2)} ms`;
20
+ }