@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.
- package/README.md +55 -0
- package/dist/types/component/index.d.ts +80 -0
- package/dist/types/component/view.d.ts +49 -0
- package/dist/types/component/viewmodel.d.ts +7 -0
- package/dist/types/engines.d.ts +10 -0
- package/dist/types/examples.d.ts +9 -0
- package/dist/types/format.d.ts +15 -0
- package/dist/types/index.d.ts +272 -0
- package/docs/PLAY-FORMAT.md +156 -0
- package/package.json +64 -0
- package/src/component/index.js +53 -0
- package/src/component/view.js +318 -0
- package/src/component/viewmodel.js +181 -0
- package/src/engines.js +516 -0
- package/src/examples.js +546 -0
- package/src/format.js +20 -0
- package/src/index.js +157 -0
- package/styles/play.css +176 -0
package/README.md
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# @jarenjs/play
|
|
2
|
+
|
|
3
|
+
**A JSON-engine playground — understand an engine before you compose it.**
|
|
4
|
+
|
|
5
|
+
Pick an engine (JSONPath, JSON Pointer, JSON Patch, `$query`, JSLT,
|
|
6
|
+
markdown, mermaid, …), feed it a source input and one or more datasets from
|
|
7
|
+
a curated example library, and watch it run. Where `@jarenjs/studio` is for
|
|
8
|
+
building an application out of many files, `@jarenjs/play` is for
|
|
9
|
+
learning one engine standalone — a reference bench you experiment on first.
|
|
10
|
+
|
|
11
|
+
Two layers, the suite's convention:
|
|
12
|
+
|
|
13
|
+
- **the engine** (`@jarenjs/play`) — headless: an engine registry, a
|
|
14
|
+
curated example library, and a pure `runExample(engineId, source, data)
|
|
15
|
+
→ { ok, panels, timing, error }`. Wraps the real shipped compilers; knows
|
|
16
|
+
nothing of the DOM.
|
|
17
|
+
- **the component** (`@jarenjs/play/component`) — the playground UI, a
|
|
18
|
+
`createPlayComponent()` factory (like `@jarenjs/calc`): a JSLT view
|
|
19
|
+
(example rail, source editors, a dataset switcher, the run stage) the
|
|
20
|
+
host composes.
|
|
21
|
+
|
|
22
|
+
Play is a **student tool**: it opens calm — one clean result per run — and
|
|
23
|
+
drills deeper on demand. An engine's rich explainers (match cards, the
|
|
24
|
+
compiled program, a geometry-free AST, a canonical round-trip) are `deep`
|
|
25
|
+
result panels behind a quiet **"Explain ▸"** depth toggle: revealed beside
|
|
26
|
+
the answer on desktop, as a full-pane swap with a ← back on a phone.
|
|
27
|
+
|
|
28
|
+
## The model
|
|
29
|
+
|
|
30
|
+
An engine's inputs are heterogeneous — a selector needs one JSON document,
|
|
31
|
+
a patch needs a target, markdown needs no data at all. Each engine is a
|
|
32
|
+
**descriptor** (the panes it consumes + a pure `run`); each **example**
|
|
33
|
+
presets those panes plus a **list of datasets**:
|
|
34
|
+
|
|
35
|
+
```js
|
|
36
|
+
{ engine: 'path',
|
|
37
|
+
source: { selector: '$.store.book[*].title' },
|
|
38
|
+
datasets: [ { label: 'store', data: { data: '{ … }' } },
|
|
39
|
+
{ label: 'catalog', data: { data: '{ … }' } } ] }
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
The datasets list length is the whole story: **0** → the engine takes no
|
|
43
|
+
data (markdown/mermaid); **1** → one dataset; **N** → a switcher that runs
|
|
44
|
+
the *same* source over each shape. Adding an engine is a descriptor plus
|
|
45
|
+
examples — the picker, panes and switcher all derive from the descriptor.
|
|
46
|
+
See [docs/PLAY-FORMAT.md](docs/PLAY-FORMAT.md).
|
|
47
|
+
|
|
48
|
+
## Install
|
|
49
|
+
|
|
50
|
+
```
|
|
51
|
+
npm install @jarenjs/play
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Zero third-party runtime dependencies — only other `@jarenjs/*` packages.
|
|
55
|
+
Node ≥ 24, ESM.
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file The playground COMPONENT — `createPlayComponent(options)`, the
|
|
3
|
+
* suite's factory convention (like `@jarenjs/calc` / `@jarenjs/studio`). It
|
|
4
|
+
* hands the host the JSLT view (`rules` + `mode` + `modes`), the pure
|
|
5
|
+
* derivation (`viewModel`), and the headless engine surface its run loop
|
|
6
|
+
* binds (`runExample`, the registry, the example library). The reducer
|
|
7
|
+
* `play/*` actions and the debounced live-run are wired at the host.
|
|
8
|
+
*/
|
|
9
|
+
import { ENGINES, EXAMPLES, engineIds, runExample } from '../index.js';
|
|
10
|
+
import { playViewModel } from './viewmodel.js';
|
|
11
|
+
import { playRules, playModes, PLAY_MODE, PLAY_BASE } from './view.js';
|
|
12
|
+
/**
|
|
13
|
+
* Build the playground component.
|
|
14
|
+
*
|
|
15
|
+
* The three host seams are configured ONCE here and become defaults for
|
|
16
|
+
* every `runExample` call the returned component makes; a per-call option
|
|
17
|
+
* of the same name still wins, so a caller can vary one run. Configuring
|
|
18
|
+
* them at the factory and having them silently ignored is the trap this
|
|
19
|
+
* shape exists to close — a host that registered its operator packs here
|
|
20
|
+
* would otherwise watch `$mean` and `$npv` go missing at run time.
|
|
21
|
+
*
|
|
22
|
+
* @param {{ operators?: { toOptions: () => any },
|
|
23
|
+
* renderers?: Record<string, Function>, validate?: Function }} [options]
|
|
24
|
+
* `operators` reaches the query/jslt engines, `renderers` the visual
|
|
25
|
+
* engines (markdown/mermaid/charts/mdx), `validate` the JSON Schema one
|
|
26
|
+
*/
|
|
27
|
+
export declare function createPlayComponent(options?: {
|
|
28
|
+
operators?: {
|
|
29
|
+
toOptions: () => any;
|
|
30
|
+
};
|
|
31
|
+
renderers?: Record<string, Function>;
|
|
32
|
+
validate?: Function;
|
|
33
|
+
}): {
|
|
34
|
+
mode: string;
|
|
35
|
+
rules: ({
|
|
36
|
+
match: string;
|
|
37
|
+
mode: string;
|
|
38
|
+
body: {
|
|
39
|
+
$if: (string | (string | {
|
|
40
|
+
class: string;
|
|
41
|
+
})[] | {
|
|
42
|
+
$if: (string | (string | (string | {}[] | {}[] | {
|
|
43
|
+
class: string;
|
|
44
|
+
})[] | {
|
|
45
|
+
class: string;
|
|
46
|
+
})[] | {
|
|
47
|
+
$if: (string | (string | {
|
|
48
|
+
class: string;
|
|
49
|
+
})[] | {
|
|
50
|
+
$if: (string | (string | {
|
|
51
|
+
$apply: string;
|
|
52
|
+
}[] | {
|
|
53
|
+
class: string;
|
|
54
|
+
})[] | (string | {}[] | {
|
|
55
|
+
class: string;
|
|
56
|
+
})[])[];
|
|
57
|
+
})[];
|
|
58
|
+
})[];
|
|
59
|
+
})[];
|
|
60
|
+
};
|
|
61
|
+
} | {
|
|
62
|
+
match: string;
|
|
63
|
+
mode: string;
|
|
64
|
+
body: {}[];
|
|
65
|
+
})[];
|
|
66
|
+
modes: Readonly<{
|
|
67
|
+
play: {
|
|
68
|
+
unmatched: string;
|
|
69
|
+
};
|
|
70
|
+
}>;
|
|
71
|
+
viewModel: typeof playViewModel;
|
|
72
|
+
engines: Readonly<Record<string, import("../index.js").EngineDescriptor>>;
|
|
73
|
+
examples: readonly import("../index.js").PlayExample[];
|
|
74
|
+
engineIds: typeof engineIds;
|
|
75
|
+
runExample: (engineId: any, source: any, data: any, perCall?: {}) => import("../index.js").PlayResult;
|
|
76
|
+
operators: {
|
|
77
|
+
toOptions: () => any;
|
|
78
|
+
} | undefined;
|
|
79
|
+
};
|
|
80
|
+
export { playViewModel, playRules, playModes, PLAY_MODE, PLAY_BASE, ENGINES, EXAMPLES, engineIds, runExample, };
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file The playground as a JSLT view — the chrome is a document, rendered
|
|
3
|
+
* by the same engine as the rest of the suite. One `play` mode, rules
|
|
4
|
+
* matched by their ABSOLUTE slice path (`$.ui.play`), `$apply` and body
|
|
5
|
+
* references RELATIVE to the matched node. The host mounts the view model
|
|
6
|
+
* at `$.ui.play`. No imperative islands — the whole playground is data.
|
|
7
|
+
*/
|
|
8
|
+
/** The one mode this view uses. */
|
|
9
|
+
export declare const PLAY_MODE = "play";
|
|
10
|
+
/** The slice the host mounts the view model at. */
|
|
11
|
+
export declare const PLAY_BASE = "$.ui.play";
|
|
12
|
+
/** The modes the host merges into the site stylesheet. */
|
|
13
|
+
export declare const playModes: Readonly<{
|
|
14
|
+
play: {
|
|
15
|
+
unmatched: string;
|
|
16
|
+
};
|
|
17
|
+
}>;
|
|
18
|
+
/** The playground's JSLT rules — spread into the site stylesheet. */
|
|
19
|
+
export declare const playRules: ({
|
|
20
|
+
match: string;
|
|
21
|
+
mode: string;
|
|
22
|
+
body: {
|
|
23
|
+
$if: (string | (string | {
|
|
24
|
+
class: string;
|
|
25
|
+
})[] | {
|
|
26
|
+
$if: (string | (string | (string | {}[] | {}[] | {
|
|
27
|
+
class: string;
|
|
28
|
+
})[] | {
|
|
29
|
+
class: string;
|
|
30
|
+
})[] | {
|
|
31
|
+
$if: (string | (string | {
|
|
32
|
+
class: string;
|
|
33
|
+
})[] | {
|
|
34
|
+
$if: (string | (string | {
|
|
35
|
+
$apply: string;
|
|
36
|
+
}[] | {
|
|
37
|
+
class: string;
|
|
38
|
+
})[] | (string | {}[] | {
|
|
39
|
+
class: string;
|
|
40
|
+
})[])[];
|
|
41
|
+
})[];
|
|
42
|
+
})[];
|
|
43
|
+
})[];
|
|
44
|
+
};
|
|
45
|
+
} | {
|
|
46
|
+
match: string;
|
|
47
|
+
mode: string;
|
|
48
|
+
body: {}[];
|
|
49
|
+
})[];
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file The playground's engine descriptors — each wraps a real shipped
|
|
3
|
+
* `@jarenjs` compiler into a pure `run(source, data, options) → PlayResult`.
|
|
4
|
+
* `sourcePanes` are the engine input(s); `dataPanes` are the JSON it runs
|
|
5
|
+
* against (empty for source-only engines, which land in a later order).
|
|
6
|
+
* Registered operators reach the `query`/`jslt` engines through
|
|
7
|
+
* `options.operators` (a `.toOptions()` registry) — the host opt-in.
|
|
8
|
+
*/
|
|
9
|
+
/** @type {import('./index.js').EngineDescriptor[]} */
|
|
10
|
+
export declare const ENGINE_LIST: import('./index.js').EngineDescriptor[];
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file The playground's curated example library — the canonical home for
|
|
3
|
+
* the suite's engine examples (folded from the website + more). Each
|
|
4
|
+
* example presets its engine's source pane(s) and a LIST of datasets: one
|
|
5
|
+
* dataset shows a single run; several turn on a switcher, so the same
|
|
6
|
+
* source runs over each shape (see the `-shapes` / `-inputs` examples).
|
|
7
|
+
*/
|
|
8
|
+
/** @type {import('./index.js').PlayExample[]} */
|
|
9
|
+
export declare const EXAMPLE_LIST: import('./index.js').PlayExample[];
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Display formatting shared by the engine layer (the deep "how it
|
|
3
|
+
* ran" stat cards) and the component layer (the stage's timing line).
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* A measured duration as a stage label. Anything that is not a number —
|
|
7
|
+
* including the `null` an engine reports for a phase it does not have, or
|
|
8
|
+
* one the host never measured — is an em dash rather than a fabricated
|
|
9
|
+
* `0`, which would read as "ran in no time" instead of "not measured".
|
|
10
|
+
* Sub-hundredth-millisecond work prints as a floor: two decimals is the
|
|
11
|
+
* resolution the cards claim, and `0.00 ms` would over-claim it.
|
|
12
|
+
* @param {unknown} ms
|
|
13
|
+
* @returns {string}
|
|
14
|
+
*/
|
|
15
|
+
export declare function formatMs(ms: unknown): string;
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file `@jarenjs/play` — a JSON-engine playground. Pick an engine, feed
|
|
3
|
+
* it a source input and one or more datasets, and watch it run — a way to
|
|
4
|
+
* understand an engine standalone before composing it in the studio.
|
|
5
|
+
*
|
|
6
|
+
* The model (PLAY-FORMAT.md): each engine is a DESCRIPTOR — the panes it
|
|
7
|
+
* consumes (`sourcePanes` = the input; `dataPanes` = the JSON it runs
|
|
8
|
+
* against, possibly none) plus a pure `run`. Each EXAMPLE presets those
|
|
9
|
+
* source panes plus a LIST of datasets: 0 = the engine takes no data
|
|
10
|
+
* (markdown/mermaid), 1 = one dataset, N = a dataset switcher (the same
|
|
11
|
+
* source over several shapes). The engine registry and the example library
|
|
12
|
+
* fill in over the next orders; this is the headless core the component
|
|
13
|
+
* renders.
|
|
14
|
+
*/
|
|
15
|
+
export type EnginePane = {
|
|
16
|
+
/**
|
|
17
|
+
* - the pane's id (e.g. 'selector', 'data')
|
|
18
|
+
*/
|
|
19
|
+
key: string;
|
|
20
|
+
label: string;
|
|
21
|
+
control?: 'code' | 'text';
|
|
22
|
+
};
|
|
23
|
+
export type OptionPane = {
|
|
24
|
+
key: string;
|
|
25
|
+
label: string;
|
|
26
|
+
choices: Array<{
|
|
27
|
+
value: string;
|
|
28
|
+
label: string;
|
|
29
|
+
}>;
|
|
30
|
+
/**
|
|
31
|
+
* - the value used until the user picks another
|
|
32
|
+
*/
|
|
33
|
+
default: string;
|
|
34
|
+
};
|
|
35
|
+
export type Panel = {
|
|
36
|
+
/**
|
|
37
|
+
* - unique within the result (the tab key)
|
|
38
|
+
*/
|
|
39
|
+
id: string;
|
|
40
|
+
/**
|
|
41
|
+
* - the tab label (defaults to `id`)
|
|
42
|
+
*/
|
|
43
|
+
label?: string;
|
|
44
|
+
kind: 'code' | 'view' | 'table' | 'note' | 'cards';
|
|
45
|
+
/**
|
|
46
|
+
* - `deep` panels are the engine's rich
|
|
47
|
+
* explainers, hidden until the student drills in via the depth toggle;
|
|
48
|
+
* defaults to `simple` (the calm default view)
|
|
49
|
+
*/
|
|
50
|
+
depth?: 'simple' | 'deep';
|
|
51
|
+
/**
|
|
52
|
+
* - `code` / `note`: the text body
|
|
53
|
+
*/
|
|
54
|
+
text?: string;
|
|
55
|
+
/**
|
|
56
|
+
* - `view`: a host-rendered vnode, spliced verbatim
|
|
57
|
+
*/
|
|
58
|
+
vnode?: any;
|
|
59
|
+
/**
|
|
60
|
+
* - `table`: the header labels
|
|
61
|
+
*/
|
|
62
|
+
columns?: string[];
|
|
63
|
+
/**
|
|
64
|
+
* - `table`: cells, row-major
|
|
65
|
+
*/
|
|
66
|
+
rows?: Array<Array<any>>;
|
|
67
|
+
/**
|
|
68
|
+
* - `note`: the callout tone
|
|
69
|
+
*/
|
|
70
|
+
tone?: 'ok' | 'warn' | 'info';
|
|
71
|
+
/**
|
|
72
|
+
* `cards`: a row of stat cards (matches, compile/run timings, …)
|
|
73
|
+
*/
|
|
74
|
+
items?: Array<{
|
|
75
|
+
title: string;
|
|
76
|
+
value: string;
|
|
77
|
+
note?: string;
|
|
78
|
+
}>;
|
|
79
|
+
};
|
|
80
|
+
export type PlayResult = {
|
|
81
|
+
ok: boolean;
|
|
82
|
+
timing: {
|
|
83
|
+
compileMs: number;
|
|
84
|
+
runMs: number;
|
|
85
|
+
} | null;
|
|
86
|
+
error: {
|
|
87
|
+
message: string;
|
|
88
|
+
code?: string;
|
|
89
|
+
path?: string;
|
|
90
|
+
} | null;
|
|
91
|
+
/**
|
|
92
|
+
* - the result screens (`[]` on error); a single
|
|
93
|
+
* `code` panel for most engines, several for the richer ones
|
|
94
|
+
*/
|
|
95
|
+
panels: Panel[];
|
|
96
|
+
};
|
|
97
|
+
export type EngineDescriptor = {
|
|
98
|
+
id: string;
|
|
99
|
+
label: string;
|
|
100
|
+
/**
|
|
101
|
+
* - one line describing the engine
|
|
102
|
+
*/
|
|
103
|
+
lead?: string;
|
|
104
|
+
/**
|
|
105
|
+
* - the engine INPUT pane(s)
|
|
106
|
+
*/
|
|
107
|
+
sourcePanes: EnginePane[];
|
|
108
|
+
/**
|
|
109
|
+
* - the JSON it runs against (may be [])
|
|
110
|
+
*/
|
|
111
|
+
dataPanes: EnginePane[];
|
|
112
|
+
/**
|
|
113
|
+
* - live mode selects (may be absent)
|
|
114
|
+
*/
|
|
115
|
+
optionPanes?: OptionPane[];
|
|
116
|
+
run: (source: Record<string, string>, data: Record<string, string>, options?: RunOptions) => PlayResult;
|
|
117
|
+
};
|
|
118
|
+
export type RunOptions = {
|
|
119
|
+
/**
|
|
120
|
+
* - a host operator registry ({ toOptions() }) for query/jslt/jtlt
|
|
121
|
+
*/
|
|
122
|
+
operators?: any;
|
|
123
|
+
/**
|
|
124
|
+
* - the current option-pane values
|
|
125
|
+
*/
|
|
126
|
+
config?: Record<string, string>;
|
|
127
|
+
/**
|
|
128
|
+
* host-injected vnode renderers keyed by engine id — the visual engines
|
|
129
|
+
* (markdown/mermaid/charts) delegate their rendering here (the hybrid seam),
|
|
130
|
+
* so the package owns the descriptors + examples but stays dependency-light.
|
|
131
|
+
* A renderer receives the source text plus its second argument — the
|
|
132
|
+
* option-pane config for the visual engines, the PARSED data document for
|
|
133
|
+
* `mdx` — and returns the preview vnode, or `{ vnode, deep }` where `deep`
|
|
134
|
+
* is extra `Panel`s (AST, canonical round-trip) the host derives — shown
|
|
135
|
+
* only behind the depth toggle
|
|
136
|
+
*/
|
|
137
|
+
renderers?: Record<string, (source: string, config?: any) => any>;
|
|
138
|
+
/**
|
|
139
|
+
* host-injected JSON Schema validator (the same seam) — the `validate`
|
|
140
|
+
* engine delegates here so
|
|
141
|
+
*/
|
|
142
|
+
validate?: (schemaText: string, data: any, locale: string) => {
|
|
143
|
+
schemaError: string | null;
|
|
144
|
+
draft: string;
|
|
145
|
+
compileMs: number | null;
|
|
146
|
+
validateMs: number | null;
|
|
147
|
+
valid: boolean | null;
|
|
148
|
+
errors: any[];
|
|
149
|
+
};
|
|
150
|
+
};
|
|
151
|
+
export type PlayExample = {
|
|
152
|
+
id: string;
|
|
153
|
+
label: string;
|
|
154
|
+
/**
|
|
155
|
+
* - an engine id
|
|
156
|
+
*/
|
|
157
|
+
engine: string;
|
|
158
|
+
/**
|
|
159
|
+
* - presets the source pane(s)
|
|
160
|
+
*/
|
|
161
|
+
source: Record<string, string>;
|
|
162
|
+
/**
|
|
163
|
+
* the JSON to run against — `[]` for a source-only engine (josl/csv/md),
|
|
164
|
+
* one for a single run, several for a switcher
|
|
165
|
+
*/
|
|
166
|
+
datasets: Array<{
|
|
167
|
+
label: string;
|
|
168
|
+
data: Record<string, string>;
|
|
169
|
+
}>;
|
|
170
|
+
/**
|
|
171
|
+
* - presets option-pane values
|
|
172
|
+
*/
|
|
173
|
+
config?: Record<string, string>;
|
|
174
|
+
};
|
|
175
|
+
/**
|
|
176
|
+
* @typedef {Object} EnginePane
|
|
177
|
+
* @property {string} key - the pane's id (e.g. 'selector', 'data')
|
|
178
|
+
* @property {string} label
|
|
179
|
+
* @property {'code' | 'text'} [control]
|
|
180
|
+
*/
|
|
181
|
+
/**
|
|
182
|
+
* @typedef {Object} OptionPane
|
|
183
|
+
* A live select above the editors — a mode an engine runs in (JOSL vs TOML,
|
|
184
|
+
* CSV strict vs repair). Its value lives in the host's `config` slice.
|
|
185
|
+
* @property {string} key
|
|
186
|
+
* @property {string} label
|
|
187
|
+
* @property {Array<{ value: string, label: string }>} choices
|
|
188
|
+
* @property {string} default - the value used until the user picks another
|
|
189
|
+
*/
|
|
190
|
+
/**
|
|
191
|
+
* @typedef {Object} Panel
|
|
192
|
+
* A single result SCREEN. Every ok run yields at least one; a multi-screen
|
|
193
|
+
* engine (CSV: a summary note, the parsed table, the CSV round-trip) yields
|
|
194
|
+
* several, which the component shows behind a tab strip.
|
|
195
|
+
* @property {string} id - unique within the result (the tab key)
|
|
196
|
+
* @property {string} [label] - the tab label (defaults to `id`)
|
|
197
|
+
* @property {'code' | 'view' | 'table' | 'note' | 'cards'} kind
|
|
198
|
+
* @property {'simple' | 'deep'} [depth] - `deep` panels are the engine's rich
|
|
199
|
+
* explainers, hidden until the student drills in via the depth toggle;
|
|
200
|
+
* defaults to `simple` (the calm default view)
|
|
201
|
+
* @property {string} [text] - `code` / `note`: the text body
|
|
202
|
+
* @property {any} [vnode] - `view`: a host-rendered vnode, spliced verbatim
|
|
203
|
+
* @property {string[]} [columns] - `table`: the header labels
|
|
204
|
+
* @property {Array<Array<any>>} [rows] - `table`: cells, row-major
|
|
205
|
+
* @property {'ok' | 'warn' | 'info'} [tone] - `note`: the callout tone
|
|
206
|
+
* @property {Array<{ title: string, value: string, note?: string }>} [items]
|
|
207
|
+
* `cards`: a row of stat cards (matches, compile/run timings, …)
|
|
208
|
+
*/
|
|
209
|
+
/**
|
|
210
|
+
* @typedef {Object} PlayResult
|
|
211
|
+
* @property {boolean} ok
|
|
212
|
+
* @property {{ compileMs: number, runMs: number } | null} timing
|
|
213
|
+
* @property {{ message: string, code?: string, path?: string } | null} error
|
|
214
|
+
* @property {Panel[]} panels - the result screens (`[]` on error); a single
|
|
215
|
+
* `code` panel for most engines, several for the richer ones
|
|
216
|
+
*/
|
|
217
|
+
/**
|
|
218
|
+
* @typedef {Object} EngineDescriptor
|
|
219
|
+
* @property {string} id
|
|
220
|
+
* @property {string} label
|
|
221
|
+
* @property {string} [lead] - one line describing the engine
|
|
222
|
+
* @property {EnginePane[]} sourcePanes - the engine INPUT pane(s)
|
|
223
|
+
* @property {EnginePane[]} dataPanes - the JSON it runs against (may be [])
|
|
224
|
+
* @property {OptionPane[]} [optionPanes] - live mode selects (may be absent)
|
|
225
|
+
* @property {(source: Record<string, string>, data: Record<string, string>, options?: RunOptions) => PlayResult} run
|
|
226
|
+
*/
|
|
227
|
+
/**
|
|
228
|
+
* @typedef {Object} RunOptions
|
|
229
|
+
* @property {any} [operators] - a host operator registry ({ toOptions() }) for query/jslt/jtlt
|
|
230
|
+
* @property {Record<string, string>} [config] - the current option-pane values
|
|
231
|
+
* @property {Record<string, (source: string, config?: any) => any>} [renderers]
|
|
232
|
+
* host-injected vnode renderers keyed by engine id — the visual engines
|
|
233
|
+
* (markdown/mermaid/charts) delegate their rendering here (the hybrid seam),
|
|
234
|
+
* so the package owns the descriptors + examples but stays dependency-light.
|
|
235
|
+
* A renderer receives the source text plus its second argument — the
|
|
236
|
+
* option-pane config for the visual engines, the PARSED data document for
|
|
237
|
+
* `mdx` — and returns the preview vnode, or `{ vnode, deep }` where `deep`
|
|
238
|
+
* is extra `Panel`s (AST, canonical round-trip) the host derives — shown
|
|
239
|
+
* only behind the depth toggle
|
|
240
|
+
* @property {(schemaText: string, data: any, locale: string) => { schemaError: string|null, draft: string, compileMs: number|null, validateMs: number|null, valid: boolean|null, errors: any[] }} [validate]
|
|
241
|
+
* host-injected JSON Schema validator (the same seam) — the `validate`
|
|
242
|
+
* engine delegates here so @jarenjs/validate + the locale packs stay in the host
|
|
243
|
+
*/
|
|
244
|
+
/**
|
|
245
|
+
* @typedef {Object} PlayExample
|
|
246
|
+
* @property {string} id
|
|
247
|
+
* @property {string} label
|
|
248
|
+
* @property {string} engine - an engine id
|
|
249
|
+
* @property {Record<string, string>} source - presets the source pane(s)
|
|
250
|
+
* @property {Array<{ label: string, data: Record<string, string> }>} datasets
|
|
251
|
+
* the JSON to run against — `[]` for a source-only engine (josl/csv/md),
|
|
252
|
+
* one for a single run, several for a switcher
|
|
253
|
+
* @property {Record<string, string>} [config] - presets option-pane values
|
|
254
|
+
*/
|
|
255
|
+
/** The registered engines, by id. */
|
|
256
|
+
export declare const ENGINES: Readonly<Record<string, EngineDescriptor>>;
|
|
257
|
+
/** The ids of the registered engines, in registration order. */
|
|
258
|
+
export declare function engineIds(): string[];
|
|
259
|
+
/** The curated example library (the canonical home for the suite's engine
|
|
260
|
+
* examples). */
|
|
261
|
+
export declare const EXAMPLES: readonly PlayExample[];
|
|
262
|
+
/**
|
|
263
|
+
* Run one engine over a source + data. An unknown engine (or a throwing
|
|
264
|
+
* runner) yields an error Result — this never throws.
|
|
265
|
+
* @param {string} engineId
|
|
266
|
+
* @param {Record<string, string>} source
|
|
267
|
+
* @param {Record<string, string>} data
|
|
268
|
+
* @param {RunOptions} [options] - operators (query/jslt), the option-pane
|
|
269
|
+
* config, and host renderers (markdown/mermaid/charts)
|
|
270
|
+
* @returns {PlayResult}
|
|
271
|
+
*/
|
|
272
|
+
export declare function runExample(engineId: string, source: Record<string, string>, data: Record<string, string>, options?: RunOptions): PlayResult;
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
# PLAY-FORMAT — the engine descriptor and example contract
|
|
2
|
+
|
|
3
|
+
`@jarenjs/play` models every engine the same way, so the playground UI
|
|
4
|
+
(the example picker, the source panes, the dataset switcher, the run stage)
|
|
5
|
+
is engine-agnostic and adding an engine is data, not UI code.
|
|
6
|
+
|
|
7
|
+
## §1 The engine descriptor
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
EngineDescriptor = {
|
|
11
|
+
id: string, // 'path' | 'pointer' | 'query' | 'jslt' | …
|
|
12
|
+
label: string,
|
|
13
|
+
lead?: string, // one line describing the engine
|
|
14
|
+
sourcePanes: EnginePane[], // the engine INPUT (usually one)
|
|
15
|
+
dataPanes: EnginePane[], // the JSON it runs against (may be empty)
|
|
16
|
+
run: (source, data) => PlayResult,
|
|
17
|
+
}
|
|
18
|
+
EnginePane = { key: string, label: string, control?: 'code' | 'text' }
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
- `sourcePanes` are the engine's input(s): a JSONPath `selector`, a JSLT
|
|
22
|
+
`stylesheet`, a JSON `patch`, a query + its `externals`, a markdown
|
|
23
|
+
`source`. Editable in the playground.
|
|
24
|
+
- `dataPanes` are the JSON the engine runs against: usually one `data`; a
|
|
25
|
+
patch's is its `target`; markdown / mermaid / charts have **none** — the
|
|
26
|
+
source is everything.
|
|
27
|
+
- `run(source, data)` is PURE and NEVER throws: it wraps the real shipped
|
|
28
|
+
compiler and returns a `PlayResult`. `source` / `data` are maps keyed
|
|
29
|
+
by the pane `key`s, holding the raw text.
|
|
30
|
+
|
|
31
|
+
## §2 The result
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
PlayResult = {
|
|
35
|
+
ok: boolean,
|
|
36
|
+
// EITHER half may be null: an engine with no compile step (patch merge
|
|
37
|
+
// and diff) and a phase the host did not report are both "no number",
|
|
38
|
+
// and the stage omits that clause rather than printing `0 ms` — which
|
|
39
|
+
// read as "it was free". Only measured phases are ever shown.
|
|
40
|
+
timing: { compileMs: number | null, runMs: number | null } | null,
|
|
41
|
+
error: { message: string, code?, path? } | null,
|
|
42
|
+
panels: Panel[], // the result SCREENS ([] on error)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
Panel = {
|
|
46
|
+
id: string, // unique in the result (the tab key)
|
|
47
|
+
label?: string, // the tab label (defaults to id)
|
|
48
|
+
kind: 'code' | 'view' | 'table' | 'note' | 'cards',
|
|
49
|
+
depth?: 'simple' | 'deep', // 'deep' → hidden behind the depth toggle
|
|
50
|
+
// per kind:
|
|
51
|
+
text?: string, // code / note body
|
|
52
|
+
vnode?: any, // view: a host-rendered vnode, spliced verbatim
|
|
53
|
+
columns?: string[], rows?: any[][], // table
|
|
54
|
+
tone?: 'ok' | 'warn' | 'info', // note callout tone
|
|
55
|
+
items?: Array<{ title, value, note? }>, // cards: a row of stat cards
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Every ok run yields **at least one** panel — most engines a single `code`
|
|
60
|
+
panel. The `simple` panels are the calm default: the component shows one
|
|
61
|
+
inline, or — for more than one — a tab strip above the active panel body.
|
|
62
|
+
The `deep` panels are the engine's rich explainers (match cards, a
|
|
63
|
+
geometry-free AST, a compiled program, a canonical round-trip): they stay
|
|
64
|
+
hidden until the learner opens the **depth toggle** ("Explain ▸"), which
|
|
65
|
+
reveals them as their own tab row beside the answer on desktop and as a
|
|
66
|
+
full-pane swap (with a ← back) on a phone. **CSV** is the richest example:
|
|
67
|
+
a `note` summary is the calm answer; the parsed `table`, the dialect and
|
|
68
|
+
repairs tables and the round-trip `code` ride behind the toggle. The
|
|
69
|
+
playground owns this small render vocabulary (code block, spliced vnode,
|
|
70
|
+
table, callout, stat cards, coded error line), so it never depends on a
|
|
71
|
+
host's node helpers.
|
|
72
|
+
|
|
73
|
+
## §3 The example, and the dataset problem
|
|
74
|
+
|
|
75
|
+
```
|
|
76
|
+
PlayExample = {
|
|
77
|
+
id: string,
|
|
78
|
+
label: string,
|
|
79
|
+
engine: string, // an engine id
|
|
80
|
+
source: Record<paneKey, string>, // presets the source pane(s)
|
|
81
|
+
datasets: Array<{ label: string, data: Record<paneKey, string> }>,
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
An example presets the source, and carries a **list** of named datasets.
|
|
86
|
+
The list length is the answer to "one data or many":
|
|
87
|
+
|
|
88
|
+
- **0 datasets** — the engine takes no data (markdown / mermaid): no data
|
|
89
|
+
pane, no switcher.
|
|
90
|
+
- **1 dataset** — one data pane, no switcher.
|
|
91
|
+
- **N datasets** — a **dataset switcher** appears: run the same source over
|
|
92
|
+
dataset A vs. B vs. C without touching the source (e.g. one JSONPath
|
|
93
|
+
selector across three document shapes). Switching keeps the source;
|
|
94
|
+
editing a dataset re-runs.
|
|
95
|
+
|
|
96
|
+
## §4 Extending
|
|
97
|
+
|
|
98
|
+
A new engine is one `EngineDescriptor` (registered in `ENGINES`) plus its
|
|
99
|
+
`PlayExample`s (added to `EXAMPLES`). No UI changes: the picker groups
|
|
100
|
+
examples by engine, the panes render from `sourcePanes`/`dataPanes`, and
|
|
101
|
+
the switcher appears whenever an example has ≥ 2 datasets.
|
|
102
|
+
|
|
103
|
+
## §5 Host-injected seams (the dependency-light contract)
|
|
104
|
+
|
|
105
|
+
Engines whose real work is heavy or opinionated keep the package free of that
|
|
106
|
+
weight by delegating to a host-injected function on `RunOptions`, the same
|
|
107
|
+
shape for each:
|
|
108
|
+
|
|
109
|
+
- `renderers[id]` — the visual engines (markdown / mermaid / charts / mdx)
|
|
110
|
+
hand their source to a host renderer that returns a spliced `view` vnode —
|
|
111
|
+
or `{ vnode, deep, compileMs?, runMs? }`, where `deep` is extra panels only
|
|
112
|
+
the host can derive (the JSON AST, the canonical round-trip), shown behind
|
|
113
|
+
the depth toggle — so the package never imports `@jarenjs/md`, `/mermaid`
|
|
114
|
+
or `/charts`. The renderer's second argument is the option-pane config,
|
|
115
|
+
except `mdx`, which receives the PARSED data document (markdown × data is
|
|
116
|
+
a two-input engine). A renderer that reports `compileMs`/`runMs` is
|
|
117
|
+
believed; one that does not leaves the package holding a single
|
|
118
|
+
wall-clock number for the whole call, which it attributes to the RUN and
|
|
119
|
+
leaves the compile `null` — it never invents a figure it did not measure.
|
|
120
|
+
- `validate` — the JSON Schema engine hands `(schemaText, data, locale)` to a
|
|
121
|
+
host validator that returns `{ valid, errors, draft, compileMs, validateMs,
|
|
122
|
+
schemaError }` (errors already localized), so the compiled validator and the
|
|
123
|
+
`@jarenjs/locales` packs stay in the host.
|
|
124
|
+
- `operators` — a registry threaded to the `query`/`jslt`/`jtlt` engines.
|
|
125
|
+
|
|
126
|
+
A missing seam is an honest error Result (`PLAY_NO_RENDERER` /
|
|
127
|
+
`PLAY_NO_VALIDATOR`), never a throw. Read-only result panels are
|
|
128
|
+
self-contained; interactive panels (a generated form) are host-wired.
|
|
129
|
+
|
|
130
|
+
## §6 The session file
|
|
131
|
+
|
|
132
|
+
A session is saveable, shareable and — because a share link has a length
|
|
133
|
+
ceiling — **downloadable**. The file is a small self-describing envelope:
|
|
134
|
+
|
|
135
|
+
```
|
|
136
|
+
PlaySessionFile = {
|
|
137
|
+
$play: '0.1',
|
|
138
|
+
name: string, // the session title
|
|
139
|
+
session: { engine, exampleId, source, data, config },
|
|
140
|
+
}
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Two rules make the round trip safe. Reading is **liberal**: a bare
|
|
144
|
+
`session` object loads too, because that is what a share token decodes to
|
|
145
|
+
and what a hand-written file is likely to be. Reading is also
|
|
146
|
+
**untrusting**: every field goes through the same coercion a share token
|
|
147
|
+
does, so a foreign or hostile document lands as safe defaults rather than
|
|
148
|
+
reaching an engine, and a document naming no engine is refused outright —
|
|
149
|
+
the session in progress survives a bad file instead of being replaced by
|
|
150
|
+
it.
|
|
151
|
+
|
|
152
|
+
The host supplies the two capabilities (a download and a file picker); a
|
|
153
|
+
host that has neither still runs the surface, and the affordances say so
|
|
154
|
+
rather than failing silently. This is the path that makes the oversized-
|
|
155
|
+
share refusal honest: the link is declined, and the same session is
|
|
156
|
+
offered as a file the import side can read back.
|