@jarenjs/play 0.34.0 → 0.43.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -3
- package/dist/types/component/index.d.ts +1 -1
- package/dist/types/index.d.ts +91 -11
- package/docs/PLAY-FORMAT.md +54 -2
- package/package.json +6 -5
- package/src/component/view.js +23 -6
- package/src/component/viewmodel.js +61 -3
- package/src/engines.js +168 -45
- package/src/examples.js +42 -0
- package/src/index.js +45 -6
- package/styles/play.css +6 -0
package/README.md
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
**A JSON-engine playground — understand an engine before you compose it.**
|
|
4
4
|
|
|
5
5
|
Pick an engine (JSONPath, JSON Pointer, JSON Patch, `$query`, JSLT,
|
|
6
|
-
markdown, mermaid, …), feed it a source input and
|
|
7
|
-
a curated example library, and watch it run. Where `@jarenjs/studio` is for
|
|
6
|
+
markdown, mermaid, a `$contract` document, …), feed it a source input and
|
|
7
|
+
one or more datasets from a curated example library, and watch it run. Where `@jarenjs/studio` is for
|
|
8
8
|
building an application out of many files, `@jarenjs/play` is for
|
|
9
9
|
learning one engine standalone — a reference bench you experiment on first.
|
|
10
10
|
|
|
@@ -23,7 +23,13 @@ Play is a **student tool**: it opens calm — one clean result per run — and
|
|
|
23
23
|
drills deeper on demand. An engine's rich explainers (match cards, the
|
|
24
24
|
compiled program, a geometry-free AST, a canonical round-trip) are `deep`
|
|
25
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.
|
|
26
|
+
the answer on desktop, as a full-pane swap with a ← back on a phone. And
|
|
27
|
+
when a run fails, the error says **where**: the editor it is about is
|
|
28
|
+
marked invalid and its label carries the compiler's own location — the
|
|
29
|
+
rule of the stylesheet (`at /rules/0/match`), the token of the selector
|
|
30
|
+
(`at position 7`), the line of the document (`at line 2, column 5`) — so
|
|
31
|
+
the learner is pointed at the text to fix, not just told it is wrong
|
|
32
|
+
([PLAY-FORMAT §2](./docs/PLAY-FORMAT.md#2-the-result)).
|
|
27
33
|
|
|
28
34
|
## The model
|
|
29
35
|
|
|
@@ -72,7 +72,7 @@ export declare function createPlayComponent(options?: {
|
|
|
72
72
|
engines: Readonly<Record<string, import("../index.js").EngineDescriptor>>;
|
|
73
73
|
examples: readonly import("../index.js").PlayExample[];
|
|
74
74
|
engineIds: typeof engineIds;
|
|
75
|
-
runExample: (engineId: any, source: any, data: any, perCall?: {}) => import("../index.js").PlayResult
|
|
75
|
+
runExample: (engineId: any, source: any, data: any, perCall?: {}) => import("../index.js").PlayResult | Promise<import("../index.js").PlayResult>;
|
|
76
76
|
operators: {
|
|
77
77
|
toOptions: () => any;
|
|
78
78
|
} | undefined;
|
package/dist/types/index.d.ts
CHANGED
|
@@ -77,17 +77,58 @@ export type Panel = {
|
|
|
77
77
|
note?: string;
|
|
78
78
|
}>;
|
|
79
79
|
};
|
|
80
|
+
export type PlayError = {
|
|
81
|
+
/**
|
|
82
|
+
* - the compiler's message, verbatim (a coded
|
|
83
|
+
* error composes `code: reason at path` itself)
|
|
84
|
+
*/
|
|
85
|
+
message: string;
|
|
86
|
+
/**
|
|
87
|
+
* - the stable diagnosis code, when there is one
|
|
88
|
+
*/
|
|
89
|
+
code?: string;
|
|
90
|
+
/**
|
|
91
|
+
* - the pane KEY (source or data) the error is
|
|
92
|
+
* about: the source pane for a compile or run failure, the data pane
|
|
93
|
+
* whose JSON did not parse
|
|
94
|
+
*/
|
|
95
|
+
pane?: string;
|
|
96
|
+
/**
|
|
97
|
+
* - JSON Pointer into that pane's DOCUMENT (a
|
|
98
|
+
* coded error's `docPath`: the failing patch operation, the query
|
|
99
|
+
* construct, the stylesheet rule)
|
|
100
|
+
*/
|
|
101
|
+
path?: string;
|
|
102
|
+
/**
|
|
103
|
+
* - JSON Pointer into the DATA the document
|
|
104
|
+
* was applied to, when a runtime error blames both (a patch operation
|
|
105
|
+
* AND the target location it failed at)
|
|
106
|
+
*/
|
|
107
|
+
dataPath?: string;
|
|
108
|
+
/**
|
|
109
|
+
* - 0-based offset into that pane's TEXT
|
|
110
|
+
* (the syntax family: a JSONPath selector, a JSON Pointer, an XQuery)
|
|
111
|
+
*/
|
|
112
|
+
position?: number;
|
|
113
|
+
/**
|
|
114
|
+
* - 1-based line (the JOSL / CSV family)
|
|
115
|
+
*/
|
|
116
|
+
line?: number;
|
|
117
|
+
/**
|
|
118
|
+
* - 1-based column (with `line`)
|
|
119
|
+
*/
|
|
120
|
+
column?: number;
|
|
121
|
+
};
|
|
80
122
|
export type PlayResult = {
|
|
81
123
|
ok: boolean;
|
|
82
124
|
timing: {
|
|
83
125
|
compileMs: number;
|
|
84
126
|
runMs: number;
|
|
85
127
|
} | null;
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
} | null;
|
|
128
|
+
/**
|
|
129
|
+
* - null on an ok run
|
|
130
|
+
*/
|
|
131
|
+
error: PlayError | null;
|
|
91
132
|
/**
|
|
92
133
|
* - the result screens (`[]` on error); a single
|
|
93
134
|
* `code` panel for most engines, several for the richer ones
|
|
@@ -113,7 +154,13 @@ export type EngineDescriptor = {
|
|
|
113
154
|
* - live mode selects (may be absent)
|
|
114
155
|
*/
|
|
115
156
|
optionPanes?: OptionPane[];
|
|
116
|
-
|
|
157
|
+
/**
|
|
158
|
+
* most engines answer synchronously; an engine whose run resolves real
|
|
159
|
+
* promises (the contract engine's dispatch panel) answers a thenable
|
|
160
|
+
* Result, which `runExample` passes through with the same never-throw
|
|
161
|
+
* contract (a rejection settles into an error Result)
|
|
162
|
+
*/
|
|
163
|
+
run: (source: Record<string, string>, data: Record<string, string>, options?: RunOptions) => PlayResult | Promise<PlayResult>;
|
|
117
164
|
};
|
|
118
165
|
export type RunOptions = {
|
|
119
166
|
/**
|
|
@@ -206,11 +253,37 @@ export type PlayExample = {
|
|
|
206
253
|
* @property {Array<{ title: string, value: string, note?: string }>} [items]
|
|
207
254
|
* `cards`: a row of stat cards (matches, compile/run timings, …)
|
|
208
255
|
*/
|
|
256
|
+
/**
|
|
257
|
+
* The error half of a Result: what the compiler said, plus WHERE — in the
|
|
258
|
+
* format's own fields, so the view can point at the pane and the location
|
|
259
|
+
* rather than only quoting the message. Every location field is present
|
|
260
|
+
* exactly when the compiler stated it (a field is never fabricated), and
|
|
261
|
+
* `pane` names the editor the location points into; a location into a
|
|
262
|
+
* document the learner never typed (XQuery's generated query document) has
|
|
263
|
+
* no pane. See PLAY-FORMAT §2.
|
|
264
|
+
* @typedef {Object} PlayError
|
|
265
|
+
* @property {string} message - the compiler's message, verbatim (a coded
|
|
266
|
+
* error composes `code: reason at path` itself)
|
|
267
|
+
* @property {string} [code] - the stable diagnosis code, when there is one
|
|
268
|
+
* @property {string} [pane] - the pane KEY (source or data) the error is
|
|
269
|
+
* about: the source pane for a compile or run failure, the data pane
|
|
270
|
+
* whose JSON did not parse
|
|
271
|
+
* @property {string} [path] - JSON Pointer into that pane's DOCUMENT (a
|
|
272
|
+
* coded error's `docPath`: the failing patch operation, the query
|
|
273
|
+
* construct, the stylesheet rule)
|
|
274
|
+
* @property {string} [dataPath] - JSON Pointer into the DATA the document
|
|
275
|
+
* was applied to, when a runtime error blames both (a patch operation
|
|
276
|
+
* AND the target location it failed at)
|
|
277
|
+
* @property {number} [position] - 0-based offset into that pane's TEXT
|
|
278
|
+
* (the syntax family: a JSONPath selector, a JSON Pointer, an XQuery)
|
|
279
|
+
* @property {number} [line] - 1-based line (the JOSL / CSV family)
|
|
280
|
+
* @property {number} [column] - 1-based column (with `line`)
|
|
281
|
+
*/
|
|
209
282
|
/**
|
|
210
283
|
* @typedef {Object} PlayResult
|
|
211
284
|
* @property {boolean} ok
|
|
212
285
|
* @property {{ compileMs: number, runMs: number } | null} timing
|
|
213
|
-
* @property {
|
|
286
|
+
* @property {PlayError | null} error - null on an ok run
|
|
214
287
|
* @property {Panel[]} panels - the result screens (`[]` on error); a single
|
|
215
288
|
* `code` panel for most engines, several for the richer ones
|
|
216
289
|
*/
|
|
@@ -222,7 +295,11 @@ export type PlayExample = {
|
|
|
222
295
|
* @property {EnginePane[]} sourcePanes - the engine INPUT pane(s)
|
|
223
296
|
* @property {EnginePane[]} dataPanes - the JSON it runs against (may be [])
|
|
224
297
|
* @property {OptionPane[]} [optionPanes] - live mode selects (may be absent)
|
|
225
|
-
* @property {(source: Record<string, string>, data: Record<string, string>, options?: RunOptions) => PlayResult} run
|
|
298
|
+
* @property {(source: Record<string, string>, data: Record<string, string>, options?: RunOptions) => PlayResult | Promise<PlayResult>} run
|
|
299
|
+
* most engines answer synchronously; an engine whose run resolves real
|
|
300
|
+
* promises (the contract engine's dispatch panel) answers a thenable
|
|
301
|
+
* Result, which `runExample` passes through with the same never-throw
|
|
302
|
+
* contract (a rejection settles into an error Result)
|
|
226
303
|
*/
|
|
227
304
|
/**
|
|
228
305
|
* @typedef {Object} RunOptions
|
|
@@ -261,12 +338,15 @@ export declare function engineIds(): string[];
|
|
|
261
338
|
export declare const EXAMPLES: readonly PlayExample[];
|
|
262
339
|
/**
|
|
263
340
|
* Run one engine over a source + data. An unknown engine (or a throwing
|
|
264
|
-
* runner) yields an error Result — this never throws.
|
|
341
|
+
* runner) yields an error Result — this never throws. A synchronous
|
|
342
|
+
* engine answers a Result; an async engine (the contract dispatch)
|
|
343
|
+
* answers a Promise of one that never rejects — a host that must know
|
|
344
|
+
* which awaits `Promise.resolve(runExample(…))`.
|
|
265
345
|
* @param {string} engineId
|
|
266
346
|
* @param {Record<string, string>} source
|
|
267
347
|
* @param {Record<string, string>} data
|
|
268
348
|
* @param {RunOptions} [options] - operators (query/jslt), the option-pane
|
|
269
349
|
* config, and host renderers (markdown/mermaid/charts)
|
|
270
|
-
* @returns {PlayResult}
|
|
350
|
+
* @returns {PlayResult | Promise<PlayResult>}
|
|
271
351
|
*/
|
|
272
|
-
export declare function runExample(engineId: string, source: Record<string, string>, data: Record<string, string>, options?: RunOptions): PlayResult
|
|
352
|
+
export declare function runExample(engineId: string, source: Record<string, string>, data: Record<string, string>, options?: RunOptions): PlayResult | Promise<PlayResult>;
|
package/docs/PLAY-FORMAT.md
CHANGED
|
@@ -13,7 +13,7 @@ EngineDescriptor = {
|
|
|
13
13
|
lead?: string, // one line describing the engine
|
|
14
14
|
sourcePanes: EnginePane[], // the engine INPUT (usually one)
|
|
15
15
|
dataPanes: EnginePane[], // the JSON it runs against (may be empty)
|
|
16
|
-
run: (source, data) => PlayResult
|
|
16
|
+
run: (source, data) => PlayResult | Promise<PlayResult>,
|
|
17
17
|
}
|
|
18
18
|
EnginePane = { key: string, label: string, control?: 'code' | 'text' }
|
|
19
19
|
```
|
|
@@ -27,6 +27,13 @@ EnginePane = { key: string, label: string, control?: 'code' | 'text' }
|
|
|
27
27
|
- `run(source, data)` is PURE and NEVER throws: it wraps the real shipped
|
|
28
28
|
compiler and returns a `PlayResult`. `source` / `data` are maps keyed
|
|
29
29
|
by the pane `key`s, holding the raw text.
|
|
30
|
+
- Most engines answer synchronously. An engine whose run resolves real
|
|
31
|
+
promises (the contract engine's dispatch panel invokes a local client)
|
|
32
|
+
answers a **thenable** `PlayResult`; `runExample` passes it through
|
|
33
|
+
under the same never-throw contract — a rejection settles into an
|
|
34
|
+
error Result, so `await Promise.resolve(runExample(…))` is total for
|
|
35
|
+
every engine. A host that renders live must drop a settled result an
|
|
36
|
+
even newer run superseded (the website keys runs by a sequence).
|
|
30
37
|
|
|
31
38
|
## §2 The result
|
|
32
39
|
|
|
@@ -38,10 +45,25 @@ PlayResult = {
|
|
|
38
45
|
// and the stage omits that clause rather than printing `0 ms` — which
|
|
39
46
|
// read as "it was free". Only measured phases are ever shown.
|
|
40
47
|
timing: { compileMs: number | null, runMs: number | null } | null,
|
|
41
|
-
error:
|
|
48
|
+
error: PlayError | null,
|
|
42
49
|
panels: Panel[], // the result SCREENS ([] on error)
|
|
43
50
|
}
|
|
44
51
|
|
|
52
|
+
PlayError = {
|
|
53
|
+
message: string, // what the compiler said, verbatim
|
|
54
|
+
code?: string, // its stable diagnosis code, when it has one
|
|
55
|
+
// WHERE — the location half, in the format's own fields. A field is
|
|
56
|
+
// present exactly when the compiler stated it; nothing is fabricated.
|
|
57
|
+
pane?: string, // the pane KEY the error is about: the source pane
|
|
58
|
+
// for a compile or run failure, the data pane
|
|
59
|
+
// whose JSON did not parse
|
|
60
|
+
path?: string, // JSON Pointer into that pane's DOCUMENT
|
|
61
|
+
dataPath?: string, // JSON Pointer into the DATA the document ran over
|
|
62
|
+
position?: number, // 0-based offset into that pane's TEXT
|
|
63
|
+
line?: number, // 1-based line …
|
|
64
|
+
column?: number, // … and column
|
|
65
|
+
}
|
|
66
|
+
|
|
45
67
|
Panel = {
|
|
46
68
|
id: string, // unique in the result (the tab key)
|
|
47
69
|
label?: string, // the tab label (defaults to id)
|
|
@@ -56,6 +78,36 @@ Panel = {
|
|
|
56
78
|
}
|
|
57
79
|
```
|
|
58
80
|
|
|
81
|
+
An error says **where**, because for a learner the location is the lesson:
|
|
82
|
+
which token of the selector, which operation of the patch, which rule of
|
|
83
|
+
the stylesheet. The compilers already carry it, in three shapes, and the
|
|
84
|
+
error copies whichever its compiler stated: the **coded family** (patch,
|
|
85
|
+
`$query`, JSLT, JTLT — `@jarenjs/core`'s `CodedError`) states `path`, a
|
|
86
|
+
JSON Pointer into the document being compiled or run (`/rules/0/match`,
|
|
87
|
+
`/$idiv`, `/0/path`), and a patch *runtime* error adds `dataPath`, the
|
|
88
|
+
target location its operation failed at — two facts, both kept; the
|
|
89
|
+
**syntax family** (JSONPath, JSON Pointer, XQuery) states `position`, a
|
|
90
|
+
0-based offset into the source text; the **line/column family** (JOSL,
|
|
91
|
+
CSV) states 1-based `line` and `column`. `pane` names the editor the
|
|
92
|
+
location points into, and it is the whole location for a pane whose JSON
|
|
93
|
+
does not parse: the host's `JSON.parse` states its offset only inside
|
|
94
|
+
engine-specific message text, never as a field, so the pane is claimed and
|
|
95
|
+
nothing finer. Two honest gaps: a missing host seam (`PLAY_NO_RENDERER`,
|
|
96
|
+
`PLAY_NO_VALIDATOR`) is nobody's pane, and XQuery compiles a *generated*
|
|
97
|
+
query document, so a location after its parse phase is a pointer into a
|
|
98
|
+
document the learner never typed — stated, with no pane. `message` stays
|
|
99
|
+
verbatim (a coded error composes `code: reason at path` itself), so a
|
|
100
|
+
host with no structured reader still gets everything.
|
|
101
|
+
|
|
102
|
+
The component lands it in two places: the editor the error is about is
|
|
103
|
+
marked `aria-invalid` (the state the renderer owns; the ring is styled off
|
|
104
|
+
it) and its label carries the phrase — `at /rules/0/match`, `at position
|
|
105
|
+
7`, `at line 2, column 5` — while the stage's error line shows the code
|
|
106
|
+
chip, the message with its own code prefix dropped rather than read twice,
|
|
107
|
+
and the pane's label. A data pane a runtime error merely failed *at* is
|
|
108
|
+
not marked invalid (the op was wrong, not the target) but shows the
|
|
109
|
+
`dataPath` phrase, because that is where the reader looks next.
|
|
110
|
+
|
|
59
111
|
Every ok run yields **at least one** panel — most engines a single `code`
|
|
60
112
|
panel. The `simple` panels are the calm default: the component shows one
|
|
61
113
|
inline, or — for more than one — a tab strip above the active panel body.
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jarenjs/play",
|
|
3
3
|
"private": false,
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.43.1",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.js",
|
|
7
7
|
"types": "./dist/types/index.d.ts",
|
|
@@ -56,9 +56,10 @@
|
|
|
56
56
|
"prepack": "npm run build:types"
|
|
57
57
|
},
|
|
58
58
|
"dependencies": {
|
|
59
|
-
"@jarenjs/
|
|
60
|
-
"@jarenjs/
|
|
61
|
-
"@jarenjs/
|
|
62
|
-
"@jarenjs/
|
|
59
|
+
"@jarenjs/contract": "^0.43.1",
|
|
60
|
+
"@jarenjs/core": "^0.43.1",
|
|
61
|
+
"@jarenjs/josl": "^0.43.1",
|
|
62
|
+
"@jarenjs/json": "^0.43.1",
|
|
63
|
+
"@jarenjs/validate": "^0.43.1"
|
|
63
64
|
}
|
|
64
65
|
}
|
package/src/component/view.js
CHANGED
|
@@ -172,7 +172,15 @@ const shell = {
|
|
|
172
172
|
], ''] },
|
|
173
173
|
], ''] },
|
|
174
174
|
],
|
|
175
|
-
|
|
175
|
+
// the error line: the code chip (only when there is one), the message
|
|
176
|
+
// (its own code prefix dropped), and the editor the error is about —
|
|
177
|
+
// the pane itself is flagged and carries the location, see
|
|
178
|
+
// sourcePane / dataPane
|
|
179
|
+
['p', { class: 'error-line' },
|
|
180
|
+
{ $if: ['$.result.error.code', [['strong', {}, '$.result.error.code'], ' '], ''] },
|
|
181
|
+
'$.result.error.text',
|
|
182
|
+
{ $if: ['$.result.error.paneLabel',
|
|
183
|
+
['span', { class: 'jplay-error-pane' }, ' — ', '$.result.error.paneLabel'], ''] }]] },
|
|
176
184
|
['p', { class: 'muted jplay-hint' }, 'Pick an example, or edit the source or data — it runs live.']] },
|
|
177
185
|
],
|
|
178
186
|
],
|
|
@@ -197,15 +205,24 @@ const railExample = {
|
|
|
197
205
|
}, '$.label'],
|
|
198
206
|
};
|
|
199
207
|
|
|
208
|
+
/** A pane label: the name, plus the last error's location in this pane
|
|
209
|
+
* (`at /rules/3`, `at position 7`, `at line 2, column 5`) when it has one. */
|
|
210
|
+
const paneLabel = ['span', { class: 'jplay-pane-label muted' }, '$.label',
|
|
211
|
+
{ $if: ['$.where', ['span', { class: 'jplay-pane-where' }, ' · ', '$.where'], ''] }];
|
|
212
|
+
|
|
213
|
+
/** The editor the last error is about says so — `aria-invalid` is the
|
|
214
|
+
* state the renderer owns (the ring is styled off it), absent otherwise. */
|
|
215
|
+
const invalid = { $if: ['$.invalid', 'true'] };
|
|
216
|
+
|
|
200
217
|
/** One source editor (a `text` control renders an input, else a textarea). */
|
|
201
218
|
const sourcePane = {
|
|
202
219
|
match: `${PLAY_BASE}.sourcePanes[*]`, mode: PLAY_MODE,
|
|
203
220
|
body: ['label', { class: 'jplay-pane' },
|
|
204
|
-
|
|
221
|
+
paneLabel,
|
|
205
222
|
{ $if: [{ $eq: ['$.control', 'text'] },
|
|
206
|
-
['input', { type: 'text', class: 'editor line', spellcheck: 'false', value: '$.value',
|
|
223
|
+
['input', { type: 'text', class: 'editor line', spellcheck: 'false', value: '$.value', 'aria-invalid': invalid,
|
|
207
224
|
on: { input: { action: 'play/source', with: { key: '$.key' } } } }],
|
|
208
|
-
['textarea', { class: 'editor', rows: 6, spellcheck: 'false', value: '$.value',
|
|
225
|
+
['textarea', { class: 'editor', rows: 6, spellcheck: 'false', value: '$.value', 'aria-invalid': invalid,
|
|
209
226
|
on: { input: { action: 'play/source', with: { key: '$.key' } } } }]] },
|
|
210
227
|
],
|
|
211
228
|
};
|
|
@@ -214,8 +231,8 @@ const sourcePane = {
|
|
|
214
231
|
const dataPane = {
|
|
215
232
|
match: `${PLAY_BASE}.dataPanes[*]`, mode: PLAY_MODE,
|
|
216
233
|
body: ['label', { class: 'jplay-pane' },
|
|
217
|
-
|
|
218
|
-
['textarea', { class: 'editor', rows: 8, spellcheck: 'false', value: '$.value',
|
|
234
|
+
paneLabel,
|
|
235
|
+
['textarea', { class: 'editor', rows: 8, spellcheck: 'false', value: '$.value', 'aria-invalid': invalid,
|
|
219
236
|
on: { input: { action: 'play/data', with: { key: '$.key' } } } }],
|
|
220
237
|
],
|
|
221
238
|
};
|
|
@@ -27,6 +27,59 @@ function formatTiming(timing) {
|
|
|
27
27
|
return parts.length === 0 ? null : parts.join(' · ');
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
/**
|
|
31
|
+
* The location an error states, as the phrase a pane label carries — one
|
|
32
|
+
* per family, in the wording the compilers' own messages use: a JSON
|
|
33
|
+
* Pointer (`at /rules/3`; the root pointer `''` reads "at the root"), a
|
|
34
|
+
* 0-based source offset (`at position 7`), or a 1-based line and column
|
|
35
|
+
* (`at line 2, column 5`). Empty when the error states no location.
|
|
36
|
+
*/
|
|
37
|
+
function formatWhere(err) {
|
|
38
|
+
if (!err) return '';
|
|
39
|
+
const parts = [];
|
|
40
|
+
if (typeof err.path === 'string') parts.push(`at ${err.path === '' ? 'the root' : err.path}`);
|
|
41
|
+
if (typeof err.position === 'number') parts.push(`at position ${err.position}`);
|
|
42
|
+
if (typeof err.line === 'number') {
|
|
43
|
+
parts.push(`at line ${err.line}${typeof err.column === 'number' ? `, column ${err.column}` : ''}`);
|
|
44
|
+
}
|
|
45
|
+
return parts.join(', ');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* The error's claim on one editor pane. The pane the error names is
|
|
50
|
+
* INVALID (its text is what failed — the compile, the run, or a JSON parse)
|
|
51
|
+
* and carries the location phrase; a data pane is not invalid when a
|
|
52
|
+
* runtime error merely failed AT a location in it (a patch op removing a
|
|
53
|
+
* path the target lacks blames the op, not the target), yet that location
|
|
54
|
+
* is the lesson, so the data pane carries it as `where` without the flag.
|
|
55
|
+
* @returns {{ invalid: boolean, where: string }}
|
|
56
|
+
*/
|
|
57
|
+
function paneError(err, key, isData) {
|
|
58
|
+
if (!err) return { invalid: false, where: '' };
|
|
59
|
+
if (err.pane === key) return { invalid: true, where: formatWhere(err) };
|
|
60
|
+
if (isData && typeof err.dataPath === 'string') {
|
|
61
|
+
return { invalid: false, where: `at ${err.dataPath === '' ? 'the root' : err.dataPath}` };
|
|
62
|
+
}
|
|
63
|
+
return { invalid: false, where: '' };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* The stage's error line. `code` is shown as its own chip, so a message
|
|
68
|
+
* that begins with that same code (the coded family composes
|
|
69
|
+
* `code: reason at path`) drops the prefix rather than reading it twice;
|
|
70
|
+
* `paneLabel` names the editor the error is about (the pane's LABEL, for
|
|
71
|
+
* a reader), empty when no pane owns it.
|
|
72
|
+
*/
|
|
73
|
+
function deriveError(err, panes) {
|
|
74
|
+
if (!err) return null;
|
|
75
|
+
const code = err.code ?? '';
|
|
76
|
+
const message = err.message ?? '';
|
|
77
|
+
const prefix = `${code}: `;
|
|
78
|
+
const text = code !== '' && message.startsWith(prefix) ? message.slice(prefix.length) : message;
|
|
79
|
+
const pane = typeof err.pane === 'string' ? panes.find((p) => p.key === err.pane) : undefined;
|
|
80
|
+
return { code, message, text, paneLabel: pane?.label ?? '' };
|
|
81
|
+
}
|
|
82
|
+
|
|
30
83
|
/**
|
|
31
84
|
* Shape one panel for the view: kind flags for the `$if` dispatch plus the
|
|
32
85
|
* per-kind content (a `table` becomes column/cell records the JSLT can walk).
|
|
@@ -62,7 +115,7 @@ function shapePanel(p) {
|
|
|
62
115
|
* else the first — so a fresh result with different screens never strands
|
|
63
116
|
* the view on a tab that is gone.
|
|
64
117
|
*/
|
|
65
|
-
function deriveResult(r, wantedId, deepWanted, deepPick) {
|
|
118
|
+
function deriveResult(r, wantedId, deepWanted, deepPick, panes) {
|
|
66
119
|
const all = Array.isArray(r.panels) ? r.panels : [];
|
|
67
120
|
const panels = all.filter((p) => p.depth !== 'deep');
|
|
68
121
|
const deep = all.filter((p) => p.depth === 'deep');
|
|
@@ -75,7 +128,7 @@ function deriveResult(r, wantedId, deepWanted, deepPick) {
|
|
|
75
128
|
return {
|
|
76
129
|
ran: true,
|
|
77
130
|
ok: r.ok === true,
|
|
78
|
-
error: r.error
|
|
131
|
+
error: deriveError(r.error, panes),
|
|
79
132
|
timing: formatTiming(r.timing),
|
|
80
133
|
tabbed: panels.length > 1,
|
|
81
134
|
tabs: panels.map((p) => ({ id: p.id, label: p.label ?? p.id, active: p.id === activeId })),
|
|
@@ -113,11 +166,16 @@ export function playViewModel(state) {
|
|
|
113
166
|
examples: exs.map((ex) => ({ id: ex.id, label: ex.label, active: ex.id === s.exampleId })),
|
|
114
167
|
}));
|
|
115
168
|
|
|
169
|
+
// the editors, each carrying the last error's claim on it (an invalid
|
|
170
|
+
// flag + a location phrase) so the pane the learner has to fix says so
|
|
171
|
+
const err = s.result?.error ?? null;
|
|
116
172
|
const sourcePanes = (engine?.sourcePanes ?? []).map((p) => ({
|
|
117
173
|
key: p.key, label: p.label, control: p.control ?? 'code', value: s.source?.[p.key] ?? '',
|
|
174
|
+
...paneError(err, p.key, false),
|
|
118
175
|
}));
|
|
119
176
|
const dataPanes = (engine?.dataPanes ?? []).map((p) => ({
|
|
120
177
|
key: p.key, label: p.label, value: s.data?.[p.key] ?? '',
|
|
178
|
+
...paneError(err, p.key, true),
|
|
121
179
|
}));
|
|
122
180
|
|
|
123
181
|
// option panes: live mode selects (josl dialect, csv repair/headers/…);
|
|
@@ -133,7 +191,7 @@ export function playViewModel(state) {
|
|
|
133
191
|
const datasets = (active?.datasets ?? []).map((ds, i) => ({ index: i, label: ds.label, active: i === datasetIndex }));
|
|
134
192
|
|
|
135
193
|
const r = s.result ?? null;
|
|
136
|
-
const result = r === null ? { ran: false } : deriveResult(r, s.panel, s.deep, s.deepPick);
|
|
194
|
+
const result = r === null ? { ran: false } : deriveResult(r, s.panel, s.deep, s.deepPick, [...sourcePanes, ...dataPanes]);
|
|
137
195
|
|
|
138
196
|
// the IDE half: the saveable-session chrome
|
|
139
197
|
const names = Array.isArray(s.names) ? s.names : [];
|
package/src/engines.js
CHANGED
|
@@ -18,6 +18,9 @@ import { parseXQuery } from '@jarenjs/json/xquery';
|
|
|
18
18
|
import { parseJosl, stringifyJosl, stringifyJsonx } from '@jarenjs/josl';
|
|
19
19
|
import { parseCsvDocument, stringifyCsv, sniffCsvDialect } from '@jarenjs/josl/csv';
|
|
20
20
|
import { createTypeTestCompiler } from '@jarenjs/validate/query';
|
|
21
|
+
import { compileContract } from '@jarenjs/contract';
|
|
22
|
+
import { openLocalClient } from '@jarenjs/contract/local';
|
|
23
|
+
import { toOpenApi, toTypeScript } from '@jarenjs/contract/project';
|
|
21
24
|
import { formatMs } from './format.js';
|
|
22
25
|
|
|
23
26
|
const compileTypeTest = createTypeTestCompiler();
|
|
@@ -27,10 +30,18 @@ const fmt = (v) => (v === undefined ? '(no result)' : JSON.stringify(v, null, 2)
|
|
|
27
30
|
const msg = (err) => String(/** @type {any} */ (err)?.message ?? err);
|
|
28
31
|
const code = (err) => /** @type {any} */ (err)?.code;
|
|
29
32
|
|
|
30
|
-
/**
|
|
31
|
-
|
|
33
|
+
/**
|
|
34
|
+
* Parse a pane's JSON text; `{ value }` or `{ error, pane }`. `pane` is the
|
|
35
|
+
* pane KEY (what the result's error location names); `label` is the word
|
|
36
|
+
* the message uses for it, which differs where the pane's label does (the
|
|
37
|
+
* patch engine's `data` pane is its "target"). The host's `JSON.parse`
|
|
38
|
+
* states the offending offset only inside its engine-specific message
|
|
39
|
+
* text, never as a field — so a parse failure locates the PANE and
|
|
40
|
+
* nothing finer.
|
|
41
|
+
*/
|
|
42
|
+
function parseJson(text, pane, label = pane) {
|
|
32
43
|
try { return { value: JSON.parse((text ?? 'null') === '' ? 'null' : text) }; }
|
|
33
|
-
catch (err) { return { error: `${label}: ${msg(err)}
|
|
44
|
+
catch (err) { return { error: `${label}: ${msg(err)}`, pane }; }
|
|
34
45
|
}
|
|
35
46
|
|
|
36
47
|
/** A single-`code`-panel Result — the shape most engines return. */
|
|
@@ -85,7 +96,7 @@ function visual(id, label, lead, opts = {}) {
|
|
|
85
96
|
const t0 = now();
|
|
86
97
|
let view;
|
|
87
98
|
try { view = render(source.source ?? '', options?.config); }
|
|
88
|
-
catch (err) { return fail(msg(err), code(err)); }
|
|
99
|
+
catch (err) { return fail(msg(err), code(err), locate(err, 'source')); }
|
|
89
100
|
const t1 = now();
|
|
90
101
|
return okPanels(renderedPanels(view), ...renderTiming(view, t1 - t0));
|
|
91
102
|
},
|
|
@@ -118,8 +129,50 @@ function renderedPanels(view) {
|
|
|
118
129
|
: [];
|
|
119
130
|
return [{ id: 'preview', label: 'Preview', kind: 'view', vnode }, ...deep];
|
|
120
131
|
}
|
|
121
|
-
/**
|
|
122
|
-
|
|
132
|
+
/**
|
|
133
|
+
* An error Result. `where` is the location half of the format's error —
|
|
134
|
+
* `pane` plus whichever of `path`/`dataPath`/`position`/`line`/`column`
|
|
135
|
+
* the compiler stated (see {@link locate}); only stated fields land on
|
|
136
|
+
* the error, so a field's presence means the compiler said it.
|
|
137
|
+
* @returns {import('./index.js').PlayResult}
|
|
138
|
+
*/
|
|
139
|
+
const fail = (message, c, where) => ({
|
|
140
|
+
ok: false, timing: null,
|
|
141
|
+
error: { message, ...(c === undefined ? {} : { code: c }), ...where },
|
|
142
|
+
panels: [],
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
/** The error Result of a pane whose JSON did not parse — locates the pane. */
|
|
146
|
+
const failParse = (d) => fail(d.error, undefined, { pane: d.pane });
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* The location an engine error carries, in the format's own fields — one
|
|
150
|
+
* adapter over the three shapes the compilers use. The coded family
|
|
151
|
+
* (`@jarenjs/core` `CodedError`: patch, query, JSLT, JTLT) states a
|
|
152
|
+
* `docPath`, a JSON Pointer into the document being compiled or run, and
|
|
153
|
+
* a patch runtime error adds the `dataPath` its operation failed at in the
|
|
154
|
+
* target; the syntax family (JSONPath, JSON Pointer, XQuery) states a
|
|
155
|
+
* 0-based `position` into the source text; the line/column family (JOSL,
|
|
156
|
+
* CSV) states 1-based `line`/`column`. Only what the error carries is
|
|
157
|
+
* copied. `pane` names the editor the phase was working on — the source
|
|
158
|
+
* pane for compile AND run (a runtime location points into the compiled
|
|
159
|
+
* document, which is that pane's text) — and is omitted where the
|
|
160
|
+
* location is into a document the learner never typed (XQuery compiles a
|
|
161
|
+
* GENERATED query document, and a location in it has no pane).
|
|
162
|
+
* @param {unknown} err
|
|
163
|
+
* @param {string} [pane] the pane key the location points into
|
|
164
|
+
*/
|
|
165
|
+
function locate(err, pane) {
|
|
166
|
+
const e = /** @type {any} */ (err);
|
|
167
|
+
/** @type {{ pane?: string, path?: string, dataPath?: string, position?: number, line?: number, column?: number }} */
|
|
168
|
+
const where = pane === undefined ? {} : { pane };
|
|
169
|
+
if (typeof e?.docPath === 'string') where.path = e.docPath;
|
|
170
|
+
if (typeof e?.dataPath === 'string') where.dataPath = e.dataPath;
|
|
171
|
+
if (typeof e?.position === 'number') where.position = e.position;
|
|
172
|
+
if (typeof e?.line === 'number') where.line = e.line;
|
|
173
|
+
if (typeof e?.column === 'number') where.column = e.column;
|
|
174
|
+
return where;
|
|
175
|
+
}
|
|
123
176
|
|
|
124
177
|
/** The compile options the query/jslt engines run with (registry-aware). */
|
|
125
178
|
function compileOptions(options) {
|
|
@@ -135,14 +188,14 @@ export const ENGINE_LIST = [
|
|
|
135
188
|
sourcePanes: [{ key: 'selector', label: 'Selector', control: 'text' }],
|
|
136
189
|
dataPanes: [{ key: 'data', label: 'Data' }],
|
|
137
190
|
run(source, data) {
|
|
138
|
-
const d = parseJson(data.data, 'data'); if (d.error) return
|
|
191
|
+
const d = parseJson(data.data, 'data'); if (d.error) return failParse(d);
|
|
139
192
|
let compiled; const t0 = now();
|
|
140
193
|
try { compiled = compileJSONPath(source.selector ?? ''); }
|
|
141
|
-
catch (err) { return fail(msg(err), code(err)); }
|
|
194
|
+
catch (err) { return fail(msg(err), code(err), locate(err, 'selector')); }
|
|
142
195
|
const t1 = now();
|
|
143
196
|
let nodes;
|
|
144
197
|
try { nodes = compiled.nodes(d.value); }
|
|
145
|
-
catch (err) { return fail(msg(err), code(err)); }
|
|
198
|
+
catch (err) { return fail(msg(err), code(err), locate(err, 'selector')); }
|
|
146
199
|
const t2 = now();
|
|
147
200
|
return ok(fmt(nodes.map((n) => n.value)), t1 - t0, t2 - t1, [
|
|
148
201
|
deepCards('how', 'How it matched', [
|
|
@@ -164,16 +217,16 @@ export const ENGINE_LIST = [
|
|
|
164
217
|
],
|
|
165
218
|
dataPanes: [{ key: 'data', label: 'Data' }],
|
|
166
219
|
run(source, data) {
|
|
167
|
-
const d = parseJson(data.data, 'data'); if (d.error) return
|
|
220
|
+
const d = parseJson(data.data, 'data'); if (d.error) return failParse(d);
|
|
168
221
|
const pointer = source.pointer ?? '';
|
|
169
222
|
const relative = /^\d/.test(pointer);
|
|
170
223
|
let getter; const t0 = now();
|
|
171
224
|
try { getter = relative ? compileRelativeJSONPointer(pointer) : compileJSONPointer(pointer); }
|
|
172
|
-
catch (err) { return fail(msg(err), code(err)); }
|
|
225
|
+
catch (err) { return fail(msg(err), code(err), locate(err, 'pointer')); }
|
|
173
226
|
const t1 = now();
|
|
174
227
|
let value;
|
|
175
228
|
try { value = relative ? getter(d.value, source.location ?? '') : getter(d.value); }
|
|
176
|
-
catch (err) { return fail(msg(err), code(err)); }
|
|
229
|
+
catch (err) { return fail(msg(err), code(err), locate(err, 'pointer')); }
|
|
177
230
|
const t2 = now();
|
|
178
231
|
if (value === JSONPOINTER_NOTHING) return ok('(nothing — the pointer addresses no value)', t1 - t0, t2 - t1);
|
|
179
232
|
return ok(fmt(value), t1 - t0, t2 - t1);
|
|
@@ -193,13 +246,13 @@ export const ENGINE_LIST = [
|
|
|
193
246
|
}],
|
|
194
247
|
run(source, data, options) {
|
|
195
248
|
const mode = options?.config?.mode ?? 'patch';
|
|
196
|
-
const target = parseJson(data.data, 'target'); if (target.error) return
|
|
197
|
-
const patch = parseJson(source.patch, 'patch'); if (patch.error) return
|
|
249
|
+
const target = parseJson(data.data, 'data', 'target'); if (target.error) return failParse(target);
|
|
250
|
+
const patch = parseJson(source.patch, 'patch'); if (patch.error) return failParse(patch);
|
|
198
251
|
if (mode === 'merge') {
|
|
199
252
|
// RFC 7396: null members delete; an unchanged document is the INPUT
|
|
200
253
|
let out; const t0 = now();
|
|
201
254
|
try { out = applyMergePatch(target.value, patch.value); }
|
|
202
|
-
catch (err) { return fail(msg(err), code(err)); }
|
|
255
|
+
catch (err) { return fail(msg(err), code(err), locate(err, 'patch')); }
|
|
203
256
|
const t1 = now();
|
|
204
257
|
return ok(fmt(out), null, t1 - t0, [
|
|
205
258
|
deepCards('how', 'How it merged', [
|
|
@@ -215,7 +268,7 @@ export const ENGINE_LIST = [
|
|
|
215
268
|
jsonPatch = createJSONPatch(target.value, patch.value);
|
|
216
269
|
mergePatch = createMergePatch(target.value, patch.value);
|
|
217
270
|
}
|
|
218
|
-
catch (err) { return fail(msg(err), code(err)); }
|
|
271
|
+
catch (err) { return fail(msg(err), code(err), locate(err, 'patch')); }
|
|
219
272
|
const t1 = now();
|
|
220
273
|
return okPanels([
|
|
221
274
|
{ id: 'out', label: `JSON Patch (${jsonPatch.length} ops)`, kind: 'code', text: fmt(jsonPatch) },
|
|
@@ -224,11 +277,11 @@ export const ENGINE_LIST = [
|
|
|
224
277
|
}
|
|
225
278
|
let apply; const t0 = now();
|
|
226
279
|
try { apply = compileJSONPatch(patch.value, { changes: true }); }
|
|
227
|
-
catch (err) { return fail(msg(err), code(err)); }
|
|
280
|
+
catch (err) { return fail(msg(err), code(err), locate(err, 'patch')); }
|
|
228
281
|
const t1 = now();
|
|
229
282
|
let run;
|
|
230
283
|
try { run = apply(target.value); }
|
|
231
|
-
catch (err) { return fail(msg(err), code(err)); }
|
|
284
|
+
catch (err) { return fail(msg(err), code(err), locate(err, 'patch')); }
|
|
232
285
|
const t2 = now();
|
|
233
286
|
return ok(fmt(run.doc), t1 - t0, t2 - t1, [
|
|
234
287
|
deepCode('changes', 'What changed', fmt(run.changes)),
|
|
@@ -240,19 +293,19 @@ export const ENGINE_LIST = [
|
|
|
240
293
|
sourcePanes: [{ key: 'query', label: 'Query', control: 'code' }, { key: 'externals', label: 'Externals', control: 'code' }],
|
|
241
294
|
dataPanes: [{ key: 'data', label: 'Data' }],
|
|
242
295
|
run(source, data, options) {
|
|
243
|
-
const d = parseJson(data.data, 'data'); if (d.error) return
|
|
244
|
-
const q = parseJson(source.query, 'query'); if (q.error) return
|
|
296
|
+
const d = parseJson(data.data, 'data'); if (d.error) return failParse(d);
|
|
297
|
+
const q = parseJson(source.query, 'query'); if (q.error) return failParse(q);
|
|
245
298
|
let externals = {};
|
|
246
299
|
if (source.externals !== undefined && String(source.externals).trim() !== '') {
|
|
247
|
-
const e = parseJson(source.externals, 'externals'); if (e.error) return
|
|
300
|
+
const e = parseJson(source.externals, 'externals'); if (e.error) return failParse(e); externals = e.value;
|
|
248
301
|
}
|
|
249
302
|
let fn; const t0 = now();
|
|
250
303
|
try { fn = compileJsonQuery(q.value, compileOptions(options)); }
|
|
251
|
-
catch (err) { return fail(msg(err), code(err)); }
|
|
304
|
+
catch (err) { return fail(msg(err), code(err), locate(err, 'query')); }
|
|
252
305
|
const t1 = now();
|
|
253
306
|
let out;
|
|
254
307
|
try { out = fn(d.value, externals); }
|
|
255
|
-
catch (err) { return fail(msg(err), code(err)); }
|
|
308
|
+
catch (err) { return fail(msg(err), code(err), locate(err, 'query')); }
|
|
256
309
|
const t2 = now();
|
|
257
310
|
const items = out === undefined ? 0 : Array.isArray(out) ? out.length : 1;
|
|
258
311
|
return ok(fmt(out), t1 - t0, t2 - t1, [
|
|
@@ -269,15 +322,15 @@ export const ENGINE_LIST = [
|
|
|
269
322
|
sourcePanes: [{ key: 'stylesheet', label: 'Stylesheet', control: 'code' }],
|
|
270
323
|
dataPanes: [{ key: 'data', label: 'Data' }],
|
|
271
324
|
run(source, data, options) {
|
|
272
|
-
const d = parseJson(data.data, 'data'); if (d.error) return
|
|
273
|
-
const s = parseJson(source.stylesheet, 'stylesheet'); if (s.error) return
|
|
325
|
+
const d = parseJson(data.data, 'data'); if (d.error) return failParse(d);
|
|
326
|
+
const s = parseJson(source.stylesheet, 'stylesheet'); if (s.error) return failParse(s);
|
|
274
327
|
let compiled; const t0 = now();
|
|
275
328
|
try { compiled = compileJsltStylesheet(s.value, compileOptions(options)); }
|
|
276
|
-
catch (err) { return fail(msg(err), code(err)); }
|
|
329
|
+
catch (err) { return fail(msg(err), code(err), locate(err, 'stylesheet')); }
|
|
277
330
|
const t1 = now();
|
|
278
331
|
let out;
|
|
279
332
|
try { out = compiled(d.value); }
|
|
280
|
-
catch (err) { return fail(msg(err), code(err)); }
|
|
333
|
+
catch (err) { return fail(msg(err), code(err), locate(err, 'stylesheet')); }
|
|
281
334
|
const t2 = now();
|
|
282
335
|
// the identity lesson: a rule that changes nothing hands the INPUT back
|
|
283
336
|
// (shared, copy-on-write) — worth teaching, so the deep card says which
|
|
@@ -295,15 +348,15 @@ export const ENGINE_LIST = [
|
|
|
295
348
|
sourcePanes: [{ key: 'template', label: 'Template', control: 'code' }],
|
|
296
349
|
dataPanes: [{ key: 'data', label: 'Data' }],
|
|
297
350
|
run(source, data, options) {
|
|
298
|
-
const d = parseJson(data.data, 'data'); if (d.error) return
|
|
299
|
-
const t = parseJson(source.template, 'template'); if (t.error) return
|
|
351
|
+
const d = parseJson(data.data, 'data'); if (d.error) return failParse(d);
|
|
352
|
+
const t = parseJson(source.template, 'template'); if (t.error) return failParse(t);
|
|
300
353
|
let render; const t0 = now();
|
|
301
354
|
try { render = compileJtltStylesheet(t.value, compileOptions(options)); }
|
|
302
|
-
catch (err) { return fail(msg(err), code(err)); }
|
|
355
|
+
catch (err) { return fail(msg(err), code(err), locate(err, 'template')); }
|
|
303
356
|
const t1 = now();
|
|
304
357
|
let out;
|
|
305
358
|
try { out = render(d.value); }
|
|
306
|
-
catch (err) { return fail(msg(err), code(err)); }
|
|
359
|
+
catch (err) { return fail(msg(err), code(err), locate(err, 'template')); }
|
|
307
360
|
const t2 = now();
|
|
308
361
|
// JTLT emits TEXT (markdown / xml / source) — show it verbatim, not fmt'd
|
|
309
362
|
return ok(out === '' ? '(empty)' : out, t1 - t0, t2 - t1, [
|
|
@@ -317,20 +370,22 @@ export const ENGINE_LIST = [
|
|
|
317
370
|
sourcePanes: [{ key: 'text', label: 'XQuery', control: 'code' }],
|
|
318
371
|
dataPanes: [{ key: 'data', label: 'Data' }],
|
|
319
372
|
run(source, data, options) {
|
|
320
|
-
const d = parseJson(data.data, 'data'); if (d.error) return
|
|
373
|
+
const d = parseJson(data.data, 'data'); if (d.error) return failParse(d);
|
|
321
374
|
let doc, fn; const t0 = now();
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
}
|
|
326
|
-
catch (err) { return fail(msg(err), code(err)); }
|
|
375
|
+
// two phases: the text parses (a syntax error locates a position in
|
|
376
|
+
// the XQuery pane), then the GENERATED query document compiles and
|
|
377
|
+
// runs — a location in that document has no pane the learner typed
|
|
378
|
+
try { doc = parseXQuery(source.text ?? ''); }
|
|
379
|
+
catch (err) { return fail(msg(err), code(err), locate(err, 'text')); }
|
|
380
|
+
try { fn = compileJsonQuery(doc, compileOptions(options)); }
|
|
381
|
+
catch (err) { return fail(msg(err), code(err), locate(err)); }
|
|
327
382
|
const t1 = now();
|
|
328
383
|
let out;
|
|
329
384
|
try {
|
|
330
385
|
const externals = fn.externals.includes('doc') ? { doc: d.value } : {};
|
|
331
386
|
out = fn(d.value, externals);
|
|
332
387
|
}
|
|
333
|
-
catch (err) { return fail(msg(err), code(err)); }
|
|
388
|
+
catch (err) { return fail(msg(err), code(err), locate(err)); }
|
|
334
389
|
const t2 = now();
|
|
335
390
|
return ok(out === undefined ? '(empty sequence)' : fmt(out), t1 - t0, t2 - t1, [
|
|
336
391
|
// the machinery: the XQuery text parses to a runnable query DOCUMENT
|
|
@@ -355,7 +410,7 @@ export const ENGINE_LIST = [
|
|
|
355
410
|
// parser streams document-order events as it reads — capture (capped)
|
|
356
411
|
// for the "how it streamed" drill-down
|
|
357
412
|
try { parsed = parseJosl(source.text ?? '', { mode, onEvent: (e) => { if (events.length < 200) events.push(e); } }); }
|
|
358
|
-
catch (err) { return fail(msg(err), code(err)); }
|
|
413
|
+
catch (err) { return fail(msg(err), code(err), locate(err, 'text')); }
|
|
359
414
|
const t1 = now();
|
|
360
415
|
const out = stringifyJsonx(parsed, { indent: 2 });
|
|
361
416
|
const t2 = now();
|
|
@@ -391,7 +446,7 @@ export const ENGINE_LIST = [
|
|
|
391
446
|
// strict mode THROWS on the first RFC 4180 violation (with a code);
|
|
392
447
|
// repair mode reads anyway and lists every fix — the lesson of the tab
|
|
393
448
|
try { doc = parseCsvDocument(source.text ?? '', opts); }
|
|
394
|
-
catch (err) { return fail(msg(err), code(err)); }
|
|
449
|
+
catch (err) { return fail(msg(err), code(err), locate(err, 'text')); }
|
|
395
450
|
const t1 = now();
|
|
396
451
|
// one calm SCREEN (the summary note) plus the drill-down: the parsed
|
|
397
452
|
// records, the sniffed dialect, the repairs, and the CSV round-trip
|
|
@@ -461,12 +516,12 @@ export const ENGINE_LIST = [
|
|
|
461
516
|
if (typeof validate !== 'function') {
|
|
462
517
|
return fail('the JSON Schema engine validates in the host — inject options.validate', 'PLAY_NO_VALIDATOR');
|
|
463
518
|
}
|
|
464
|
-
const d = parseJson(data.data, 'data'); if (d.error) return
|
|
519
|
+
const d = parseJson(data.data, 'data'); if (d.error) return failParse(d);
|
|
465
520
|
const locale = options?.config?.locale ?? 'en';
|
|
466
521
|
let report;
|
|
467
522
|
try { report = validate(source.schema ?? '', d.value, locale); }
|
|
468
|
-
catch (err) { return fail(msg(err), code(err)); }
|
|
469
|
-
if (report.schemaError) return fail(report.schemaError, 'SCHEMA');
|
|
523
|
+
catch (err) { return fail(msg(err), code(err), locate(err)); }
|
|
524
|
+
if (report.schemaError) return fail(report.schemaError, 'SCHEMA', { pane: 'schema' });
|
|
470
525
|
const errs = report.errors ?? [];
|
|
471
526
|
const summary = (report.valid ? '✓ valid' : `✗ ${errs.length} error${errs.length === 1 ? '' : 's'}`)
|
|
472
527
|
+ ` · ${report.draft} · compiled ${formatMs(report.compileMs)} · validated ${formatMs(report.validateMs)}`;
|
|
@@ -494,15 +549,83 @@ export const ENGINE_LIST = [
|
|
|
494
549
|
if (typeof render !== 'function') {
|
|
495
550
|
return fail('the MDX engine renders in the host — inject options.renderers.mdx', 'PLAY_NO_RENDERER');
|
|
496
551
|
}
|
|
497
|
-
const d = parseJson(data.data, 'data'); if (d.error) return
|
|
552
|
+
const d = parseJson(data.data, 'data'); if (d.error) return failParse(d);
|
|
498
553
|
const t0 = now();
|
|
499
554
|
let view;
|
|
500
555
|
try { view = render(source.source ?? '', d.value); }
|
|
501
|
-
catch (err) { return fail(msg(err), code(err)); }
|
|
556
|
+
catch (err) { return fail(msg(err), code(err), locate(err, 'source')); }
|
|
502
557
|
const t1 = now();
|
|
503
558
|
return okPanels(renderedPanels(view), ...renderTiming(view, t1 - t0));
|
|
504
559
|
},
|
|
505
560
|
},
|
|
561
|
+
{
|
|
562
|
+
// the one ASYNC engine: the dispatch panel resolves a real client
|
|
563
|
+
// outcome, so run() returns a Promise — runExample and the hosts
|
|
564
|
+
// pass a thenable Result through (PLAY-FORMAT §2)
|
|
565
|
+
id: 'contract', label: 'Contract', lead: 'A $contract document — operations two ends may exchange, compiled, projected and dispatched in-process.',
|
|
566
|
+
sourcePanes: [{ key: 'document', label: '$contract document', control: 'code' }],
|
|
567
|
+
dataPanes: [{ key: 'call', label: 'Dispatch — { "op", "input" }' }],
|
|
568
|
+
async run(source, data) {
|
|
569
|
+
const d = parseJson(source.document, 'document'); if (d.error) return failParse(d);
|
|
570
|
+
let contract; const t0 = now();
|
|
571
|
+
// a compile refusal is the engine's lesson: a stable JC00xx code
|
|
572
|
+
// with the JSON Pointer of the member at fault
|
|
573
|
+
try { contract = compileContract(d.value); }
|
|
574
|
+
catch (err) { return fail(msg(err), code(err), locate(err, 'document')); }
|
|
575
|
+
const t1 = now();
|
|
576
|
+
let openapi, types;
|
|
577
|
+
try {
|
|
578
|
+
openapi = toOpenApi(contract, {
|
|
579
|
+
info: {
|
|
580
|
+
title: typeof d.value?.id === 'string' ? d.value.id : 'contract',
|
|
581
|
+
version: typeof d.value?.version === 'string' ? d.value.version : '0',
|
|
582
|
+
},
|
|
583
|
+
});
|
|
584
|
+
types = toTypeScript(contract);
|
|
585
|
+
}
|
|
586
|
+
catch (err) { return fail(msg(err), code(err), locate(err, 'document')); }
|
|
587
|
+
// the dispatch pane: an op + input run against trivial echo
|
|
588
|
+
// handlers over the local binding — the whole pipeline with no
|
|
589
|
+
// network. Output validation is DECLARED off (a capability the
|
|
590
|
+
// binding states), so any echo crosses; input validation still
|
|
591
|
+
// refuses a bad input with its outcome, which is the lesson.
|
|
592
|
+
/** @type {import('./index.js').Panel} */
|
|
593
|
+
let dispatch = {
|
|
594
|
+
id: 'dispatch', label: 'Dispatch', kind: 'note', tone: 'info',
|
|
595
|
+
text: 'Type { "op": "<operation id>", "input": { … } } in the Dispatch pane to run an operation against echo handlers (validateOutput declared \'never\').',
|
|
596
|
+
};
|
|
597
|
+
if (String(data.call ?? '').trim() !== '') {
|
|
598
|
+
const call = parseJson(data.call, 'call'); if (call.error) return failParse(call);
|
|
599
|
+
const op = call.value !== null && typeof call.value === 'object' ? call.value.op : undefined;
|
|
600
|
+
if (typeof op !== 'string') {
|
|
601
|
+
dispatch = { id: 'dispatch', label: 'Dispatch', kind: 'note', tone: 'warn', text: 'the call must be an object naming an "op" (and optionally an "input")' };
|
|
602
|
+
}
|
|
603
|
+
else {
|
|
604
|
+
/** @type {Record<string, (input: any) => any>} */
|
|
605
|
+
const handlers = {};
|
|
606
|
+
for (const id of Object.keys(contract.operations)) handlers[id] = (input) => input;
|
|
607
|
+
const client = openLocalClient(contract, handlers, { validateOutput: 'never' });
|
|
608
|
+
try {
|
|
609
|
+
const outcome = await client.invoke(op, call.value.input ?? {});
|
|
610
|
+
dispatch = { id: 'dispatch', label: 'Dispatch (echo)', kind: 'code', text: fmt(outcome) };
|
|
611
|
+
}
|
|
612
|
+
catch (err) {
|
|
613
|
+
// a host mistake (JC1005: unknown, opaque or subscribe
|
|
614
|
+
// operation) is the panel's answer, not an engine failure
|
|
615
|
+
dispatch = { id: 'dispatch', label: 'Dispatch', kind: 'note', tone: 'warn', text: msg(err) };
|
|
616
|
+
}
|
|
617
|
+
finally { client.close(); }
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
const t2 = now();
|
|
621
|
+
return okPanels([
|
|
622
|
+
{ id: 'describe', label: 'describe()', kind: 'code', text: fmt(contract.describe()) },
|
|
623
|
+
{ id: 'openapi', label: 'OpenAPI', kind: 'code', text: fmt(openapi.document) },
|
|
624
|
+
{ id: 'types', label: 'TypeScript', kind: 'code', text: types },
|
|
625
|
+
dispatch,
|
|
626
|
+
], t1 - t0, t2 - t1);
|
|
627
|
+
},
|
|
628
|
+
},
|
|
506
629
|
// ——— the visual engines: descriptor + examples here, rendering delegated ———
|
|
507
630
|
visual('markdown', 'Markdown', 'CommonMark + GFM + frontmatter → a JSON AST, rendered live.', { sourceLabel: 'Markdown' }),
|
|
508
631
|
visual('mermaid', 'Mermaid', 'Diagrams-as-code → a geometry-free AST → pure-vnode SVG.', { sourceLabel: 'Mermaid' }),
|
package/src/examples.js
CHANGED
|
@@ -543,4 +543,46 @@ is a fixed point.
|
|
|
543
543
|
name: { type: 'string', minLength: 2 }, email: { type: 'string', format: 'email' }, age: { type: 'integer', minimum: 13 },
|
|
544
544
|
}, required: ['name', 'email'] }) },
|
|
545
545
|
datasets: [{ label: 'invalid', data: { data: j({ name: 'A', email: 'not-an-email', age: 7 }) } }] },
|
|
546
|
+
|
|
547
|
+
// ——— Contract ($contract document → describe / OpenAPI / TypeScript / dispatch) ———
|
|
548
|
+
{ id: 'contract-shop', label: 'A shop contract', engine: 'contract',
|
|
549
|
+
source: { document: j({ $contract: '0.1', id: 'shop', version: '1',
|
|
550
|
+
$defs: { Product: { type: 'object', required: ['id', 'name', 'price'], properties: {
|
|
551
|
+
id: { type: 'integer' }, name: { type: 'string', minLength: 1 }, price: { type: 'number', minimum: 0 } } } },
|
|
552
|
+
operations: {
|
|
553
|
+
'catalog.load': { kind: 'read',
|
|
554
|
+
input: { type: 'object', properties: { since: { type: 'string', format: 'date-time' } } },
|
|
555
|
+
output: { type: 'array', items: { $ref: '#/$defs/Product' } },
|
|
556
|
+
http: { method: 'GET', path: '/api/catalog' },
|
|
557
|
+
doc: 'The whole catalog.' },
|
|
558
|
+
'product.save': { kind: 'command',
|
|
559
|
+
input: { type: 'object', required: ['id', 'product'], properties: {
|
|
560
|
+
id: { type: 'integer' }, product: { $ref: '#/$defs/Product' } } },
|
|
561
|
+
output: { $ref: '#/$defs/Product' },
|
|
562
|
+
errors: { conflict: { status: 409 } },
|
|
563
|
+
policy: { idempotency: 'optional' },
|
|
564
|
+
http: { method: 'PUT', path: '/api/products/{id}' } },
|
|
565
|
+
} }) },
|
|
566
|
+
datasets: [
|
|
567
|
+
{ label: 'a valid save', data: { call: j({ op: 'product.save', input: { id: 7, product: { id: 7, name: 'Duck', price: 9.99 } } }) } },
|
|
568
|
+
{ label: 'an invalid input', data: { call: j({ op: 'product.save', input: { id: 'seven' } }) } },
|
|
569
|
+
{ label: 'no dispatch', data: { call: '' } },
|
|
570
|
+
] },
|
|
571
|
+
{ id: 'contract-minimal', label: 'One operation, no http', engine: 'contract',
|
|
572
|
+
source: { document: j({ $contract: '0.1', id: 'echo', operations: {
|
|
573
|
+
'echo.say': { kind: 'command',
|
|
574
|
+
input: { type: 'object', required: ['text'], properties: { text: { type: 'string' } } },
|
|
575
|
+
output: true,
|
|
576
|
+
doc: 'No http member: the binding defaults to POST /echo.say.' },
|
|
577
|
+
} }) },
|
|
578
|
+
datasets: [{ label: 'say something', data: { call: j({ op: 'echo.say', input: { text: 'hello' } }) } }] },
|
|
579
|
+
{ id: 'contract-broken', label: 'A refusal, with its docPath', engine: 'contract',
|
|
580
|
+
source: { document: j({ $contract: '0.1', id: 'broken', operations: {
|
|
581
|
+
'catalog.load': { kind: 'read',
|
|
582
|
+
input: { type: 'object', properties: { since: { type: 'string' } } },
|
|
583
|
+
// no output member: JC00xx at compile, never at request time —
|
|
584
|
+
// the error names /operations/catalog.load with its code
|
|
585
|
+
http: { method: 'GET', path: '/api/catalog' } },
|
|
586
|
+
} }) },
|
|
587
|
+
datasets: [{ label: 'nothing to dispatch', data: { call: '' } }] },
|
|
546
588
|
];
|
package/src/index.js
CHANGED
|
@@ -54,11 +54,38 @@ import { EXAMPLE_LIST } from './examples.js';
|
|
|
54
54
|
* `cards`: a row of stat cards (matches, compile/run timings, …)
|
|
55
55
|
*/
|
|
56
56
|
|
|
57
|
+
/**
|
|
58
|
+
* The error half of a Result: what the compiler said, plus WHERE — in the
|
|
59
|
+
* format's own fields, so the view can point at the pane and the location
|
|
60
|
+
* rather than only quoting the message. Every location field is present
|
|
61
|
+
* exactly when the compiler stated it (a field is never fabricated), and
|
|
62
|
+
* `pane` names the editor the location points into; a location into a
|
|
63
|
+
* document the learner never typed (XQuery's generated query document) has
|
|
64
|
+
* no pane. See PLAY-FORMAT §2.
|
|
65
|
+
* @typedef {Object} PlayError
|
|
66
|
+
* @property {string} message - the compiler's message, verbatim (a coded
|
|
67
|
+
* error composes `code: reason at path` itself)
|
|
68
|
+
* @property {string} [code] - the stable diagnosis code, when there is one
|
|
69
|
+
* @property {string} [pane] - the pane KEY (source or data) the error is
|
|
70
|
+
* about: the source pane for a compile or run failure, the data pane
|
|
71
|
+
* whose JSON did not parse
|
|
72
|
+
* @property {string} [path] - JSON Pointer into that pane's DOCUMENT (a
|
|
73
|
+
* coded error's `docPath`: the failing patch operation, the query
|
|
74
|
+
* construct, the stylesheet rule)
|
|
75
|
+
* @property {string} [dataPath] - JSON Pointer into the DATA the document
|
|
76
|
+
* was applied to, when a runtime error blames both (a patch operation
|
|
77
|
+
* AND the target location it failed at)
|
|
78
|
+
* @property {number} [position] - 0-based offset into that pane's TEXT
|
|
79
|
+
* (the syntax family: a JSONPath selector, a JSON Pointer, an XQuery)
|
|
80
|
+
* @property {number} [line] - 1-based line (the JOSL / CSV family)
|
|
81
|
+
* @property {number} [column] - 1-based column (with `line`)
|
|
82
|
+
*/
|
|
83
|
+
|
|
57
84
|
/**
|
|
58
85
|
* @typedef {Object} PlayResult
|
|
59
86
|
* @property {boolean} ok
|
|
60
87
|
* @property {{ compileMs: number, runMs: number } | null} timing
|
|
61
|
-
* @property {
|
|
88
|
+
* @property {PlayError | null} error - null on an ok run
|
|
62
89
|
* @property {Panel[]} panels - the result screens (`[]` on error); a single
|
|
63
90
|
* `code` panel for most engines, several for the richer ones
|
|
64
91
|
*/
|
|
@@ -71,7 +98,11 @@ import { EXAMPLE_LIST } from './examples.js';
|
|
|
71
98
|
* @property {EnginePane[]} sourcePanes - the engine INPUT pane(s)
|
|
72
99
|
* @property {EnginePane[]} dataPanes - the JSON it runs against (may be [])
|
|
73
100
|
* @property {OptionPane[]} [optionPanes] - live mode selects (may be absent)
|
|
74
|
-
* @property {(source: Record<string, string>, data: Record<string, string>, options?: RunOptions) => PlayResult} run
|
|
101
|
+
* @property {(source: Record<string, string>, data: Record<string, string>, options?: RunOptions) => PlayResult | Promise<PlayResult>} run
|
|
102
|
+
* most engines answer synchronously; an engine whose run resolves real
|
|
103
|
+
* promises (the contract engine's dispatch panel) answers a thenable
|
|
104
|
+
* Result, which `runExample` passes through with the same never-throw
|
|
105
|
+
* contract (a rejection settles into an error Result)
|
|
75
106
|
*/
|
|
76
107
|
|
|
77
108
|
/**
|
|
@@ -135,23 +166,31 @@ function withConfig(engine, options) {
|
|
|
135
166
|
|
|
136
167
|
/**
|
|
137
168
|
* Run one engine over a source + data. An unknown engine (or a throwing
|
|
138
|
-
* runner) yields an error Result — this never throws.
|
|
169
|
+
* runner) yields an error Result — this never throws. A synchronous
|
|
170
|
+
* engine answers a Result; an async engine (the contract dispatch)
|
|
171
|
+
* answers a Promise of one that never rejects — a host that must know
|
|
172
|
+
* which awaits `Promise.resolve(runExample(…))`.
|
|
139
173
|
* @param {string} engineId
|
|
140
174
|
* @param {Record<string, string>} source
|
|
141
175
|
* @param {Record<string, string>} data
|
|
142
176
|
* @param {RunOptions} [options] - operators (query/jslt), the option-pane
|
|
143
177
|
* config, and host renderers (markdown/mermaid/charts)
|
|
144
|
-
* @returns {PlayResult}
|
|
178
|
+
* @returns {PlayResult | Promise<PlayResult>}
|
|
145
179
|
*/
|
|
146
180
|
export function runExample(engineId, source, data, options = {}) {
|
|
147
181
|
const engine = ENGINES[engineId];
|
|
148
182
|
if (engine === undefined) {
|
|
149
183
|
return { ok: false, timing: null, error: { message: `unknown engine: ${engineId}` }, panels: [] };
|
|
150
184
|
}
|
|
185
|
+
const failed = (/** @type {unknown} */ err) =>
|
|
186
|
+
/** @type {PlayResult} */ ({ ok: false, timing: null, error: { message: String(/** @type {any} */ (err)?.message ?? err) }, panels: [] });
|
|
151
187
|
try {
|
|
152
|
-
|
|
188
|
+
const result = engine.run(source ?? {}, data ?? {}, withConfig(engine, options));
|
|
189
|
+
return result !== null && typeof result === 'object' && typeof (/** @type {any} */ (result).then) === 'function'
|
|
190
|
+
? /** @type {Promise<PlayResult>} */ (result).then((r) => r, failed)
|
|
191
|
+
: result;
|
|
153
192
|
}
|
|
154
193
|
catch (err) {
|
|
155
|
-
return
|
|
194
|
+
return failed(err);
|
|
156
195
|
}
|
|
157
196
|
}
|
package/styles/play.css
CHANGED
|
@@ -70,6 +70,12 @@
|
|
|
70
70
|
.jplay-lead { font-size: 0.8rem; }
|
|
71
71
|
.jplay-pane { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
|
|
72
72
|
.jplay-pane-label { font-size: 0.7rem; font-family: var(--mono, monospace); }
|
|
73
|
+
/* the last error's claim on a pane: the location rides on the label, and the
|
|
74
|
+
editor it is about carries aria-invalid — the state the renderer owns —
|
|
75
|
+
which is what the ring is styled off (the --fail family, no new hue) */
|
|
76
|
+
.jplay-pane-where { color: var(--fail, #dc2626); }
|
|
77
|
+
.jplay .editor[aria-invalid='true'] { border-color: var(--fail, #dc2626); }
|
|
78
|
+
.jplay-error-pane { color: var(--muted, #64748b); }
|
|
73
79
|
.jplay-datasets { align-self: flex-start; }
|
|
74
80
|
/* the validate engine's JSON ↔ form toggle + the generated-form container */
|
|
75
81
|
.jplay-dataview { align-self: flex-start; }
|