@jarenjs/play 0.34.0 → 0.34.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -1
- package/dist/types/index.d.ts +73 -6
- package/docs/PLAY-FORMAT.md +46 -1
- package/package.json +5 -5
- package/src/component/view.js +23 -6
- package/src/component/viewmodel.js +61 -3
- package/src/engines.js +97 -45
- package/src/index.js +28 -1
- package/styles/play.css +6 -0
package/README.md
CHANGED
|
@@ -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
|
|
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
|
|
@@ -206,11 +247,37 @@ export type PlayExample = {
|
|
|
206
247
|
* @property {Array<{ title: string, value: string, note?: string }>} [items]
|
|
207
248
|
* `cards`: a row of stat cards (matches, compile/run timings, …)
|
|
208
249
|
*/
|
|
250
|
+
/**
|
|
251
|
+
* The error half of a Result: what the compiler said, plus WHERE — in the
|
|
252
|
+
* format's own fields, so the view can point at the pane and the location
|
|
253
|
+
* rather than only quoting the message. Every location field is present
|
|
254
|
+
* exactly when the compiler stated it (a field is never fabricated), and
|
|
255
|
+
* `pane` names the editor the location points into; a location into a
|
|
256
|
+
* document the learner never typed (XQuery's generated query document) has
|
|
257
|
+
* no pane. See PLAY-FORMAT §2.
|
|
258
|
+
* @typedef {Object} PlayError
|
|
259
|
+
* @property {string} message - the compiler's message, verbatim (a coded
|
|
260
|
+
* error composes `code: reason at path` itself)
|
|
261
|
+
* @property {string} [code] - the stable diagnosis code, when there is one
|
|
262
|
+
* @property {string} [pane] - the pane KEY (source or data) the error is
|
|
263
|
+
* about: the source pane for a compile or run failure, the data pane
|
|
264
|
+
* whose JSON did not parse
|
|
265
|
+
* @property {string} [path] - JSON Pointer into that pane's DOCUMENT (a
|
|
266
|
+
* coded error's `docPath`: the failing patch operation, the query
|
|
267
|
+
* construct, the stylesheet rule)
|
|
268
|
+
* @property {string} [dataPath] - JSON Pointer into the DATA the document
|
|
269
|
+
* was applied to, when a runtime error blames both (a patch operation
|
|
270
|
+
* AND the target location it failed at)
|
|
271
|
+
* @property {number} [position] - 0-based offset into that pane's TEXT
|
|
272
|
+
* (the syntax family: a JSONPath selector, a JSON Pointer, an XQuery)
|
|
273
|
+
* @property {number} [line] - 1-based line (the JOSL / CSV family)
|
|
274
|
+
* @property {number} [column] - 1-based column (with `line`)
|
|
275
|
+
*/
|
|
209
276
|
/**
|
|
210
277
|
* @typedef {Object} PlayResult
|
|
211
278
|
* @property {boolean} ok
|
|
212
279
|
* @property {{ compileMs: number, runMs: number } | null} timing
|
|
213
|
-
* @property {
|
|
280
|
+
* @property {PlayError | null} error - null on an ok run
|
|
214
281
|
* @property {Panel[]} panels - the result screens (`[]` on error); a single
|
|
215
282
|
* `code` panel for most engines, several for the richer ones
|
|
216
283
|
*/
|
package/docs/PLAY-FORMAT.md
CHANGED
|
@@ -38,10 +38,25 @@ PlayResult = {
|
|
|
38
38
|
// and the stage omits that clause rather than printing `0 ms` — which
|
|
39
39
|
// read as "it was free". Only measured phases are ever shown.
|
|
40
40
|
timing: { compileMs: number | null, runMs: number | null } | null,
|
|
41
|
-
error:
|
|
41
|
+
error: PlayError | null,
|
|
42
42
|
panels: Panel[], // the result SCREENS ([] on error)
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
+
PlayError = {
|
|
46
|
+
message: string, // what the compiler said, verbatim
|
|
47
|
+
code?: string, // its stable diagnosis code, when it has one
|
|
48
|
+
// WHERE — the location half, in the format's own fields. A field is
|
|
49
|
+
// present exactly when the compiler stated it; nothing is fabricated.
|
|
50
|
+
pane?: string, // the pane KEY the error is about: the source pane
|
|
51
|
+
// for a compile or run failure, the data pane
|
|
52
|
+
// whose JSON did not parse
|
|
53
|
+
path?: string, // JSON Pointer into that pane's DOCUMENT
|
|
54
|
+
dataPath?: string, // JSON Pointer into the DATA the document ran over
|
|
55
|
+
position?: number, // 0-based offset into that pane's TEXT
|
|
56
|
+
line?: number, // 1-based line …
|
|
57
|
+
column?: number, // … and column
|
|
58
|
+
}
|
|
59
|
+
|
|
45
60
|
Panel = {
|
|
46
61
|
id: string, // unique in the result (the tab key)
|
|
47
62
|
label?: string, // the tab label (defaults to id)
|
|
@@ -56,6 +71,36 @@ Panel = {
|
|
|
56
71
|
}
|
|
57
72
|
```
|
|
58
73
|
|
|
74
|
+
An error says **where**, because for a learner the location is the lesson:
|
|
75
|
+
which token of the selector, which operation of the patch, which rule of
|
|
76
|
+
the stylesheet. The compilers already carry it, in three shapes, and the
|
|
77
|
+
error copies whichever its compiler stated: the **coded family** (patch,
|
|
78
|
+
`$query`, JSLT, JTLT — `@jarenjs/core`'s `CodedError`) states `path`, a
|
|
79
|
+
JSON Pointer into the document being compiled or run (`/rules/0/match`,
|
|
80
|
+
`/$idiv`, `/0/path`), and a patch *runtime* error adds `dataPath`, the
|
|
81
|
+
target location its operation failed at — two facts, both kept; the
|
|
82
|
+
**syntax family** (JSONPath, JSON Pointer, XQuery) states `position`, a
|
|
83
|
+
0-based offset into the source text; the **line/column family** (JOSL,
|
|
84
|
+
CSV) states 1-based `line` and `column`. `pane` names the editor the
|
|
85
|
+
location points into, and it is the whole location for a pane whose JSON
|
|
86
|
+
does not parse: the host's `JSON.parse` states its offset only inside
|
|
87
|
+
engine-specific message text, never as a field, so the pane is claimed and
|
|
88
|
+
nothing finer. Two honest gaps: a missing host seam (`PLAY_NO_RENDERER`,
|
|
89
|
+
`PLAY_NO_VALIDATOR`) is nobody's pane, and XQuery compiles a *generated*
|
|
90
|
+
query document, so a location after its parse phase is a pointer into a
|
|
91
|
+
document the learner never typed — stated, with no pane. `message` stays
|
|
92
|
+
verbatim (a coded error composes `code: reason at path` itself), so a
|
|
93
|
+
host with no structured reader still gets everything.
|
|
94
|
+
|
|
95
|
+
The component lands it in two places: the editor the error is about is
|
|
96
|
+
marked `aria-invalid` (the state the renderer owns; the ring is styled off
|
|
97
|
+
it) and its label carries the phrase — `at /rules/0/match`, `at position
|
|
98
|
+
7`, `at line 2, column 5` — while the stage's error line shows the code
|
|
99
|
+
chip, the message with its own code prefix dropped rather than read twice,
|
|
100
|
+
and the pane's label. A data pane a runtime error merely failed *at* is
|
|
101
|
+
not marked invalid (the op was wrong, not the target) but shows the
|
|
102
|
+
`dataPath` phrase, because that is where the reader looks next.
|
|
103
|
+
|
|
59
104
|
Every ok run yields **at least one** panel — most engines a single `code`
|
|
60
105
|
panel. The `simple` panels are the calm default: the component shows one
|
|
61
106
|
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.34.
|
|
4
|
+
"version": "0.34.2",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.js",
|
|
7
7
|
"types": "./dist/types/index.d.ts",
|
|
@@ -56,9 +56,9 @@
|
|
|
56
56
|
"prepack": "npm run build:types"
|
|
57
57
|
},
|
|
58
58
|
"dependencies": {
|
|
59
|
-
"@jarenjs/core": "^0.34.
|
|
60
|
-
"@jarenjs/josl": "^0.34.
|
|
61
|
-
"@jarenjs/json": "^0.34.
|
|
62
|
-
"@jarenjs/validate": "^0.34.
|
|
59
|
+
"@jarenjs/core": "^0.34.2",
|
|
60
|
+
"@jarenjs/josl": "^0.34.2",
|
|
61
|
+
"@jarenjs/json": "^0.34.2",
|
|
62
|
+
"@jarenjs/validate": "^0.34.2"
|
|
63
63
|
}
|
|
64
64
|
}
|
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
|
@@ -27,10 +27,18 @@ const fmt = (v) => (v === undefined ? '(no result)' : JSON.stringify(v, null, 2)
|
|
|
27
27
|
const msg = (err) => String(/** @type {any} */ (err)?.message ?? err);
|
|
28
28
|
const code = (err) => /** @type {any} */ (err)?.code;
|
|
29
29
|
|
|
30
|
-
/**
|
|
31
|
-
|
|
30
|
+
/**
|
|
31
|
+
* Parse a pane's JSON text; `{ value }` or `{ error, pane }`. `pane` is the
|
|
32
|
+
* pane KEY (what the result's error location names); `label` is the word
|
|
33
|
+
* the message uses for it, which differs where the pane's label does (the
|
|
34
|
+
* patch engine's `data` pane is its "target"). The host's `JSON.parse`
|
|
35
|
+
* states the offending offset only inside its engine-specific message
|
|
36
|
+
* text, never as a field — so a parse failure locates the PANE and
|
|
37
|
+
* nothing finer.
|
|
38
|
+
*/
|
|
39
|
+
function parseJson(text, pane, label = pane) {
|
|
32
40
|
try { return { value: JSON.parse((text ?? 'null') === '' ? 'null' : text) }; }
|
|
33
|
-
catch (err) { return { error: `${label}: ${msg(err)}
|
|
41
|
+
catch (err) { return { error: `${label}: ${msg(err)}`, pane }; }
|
|
34
42
|
}
|
|
35
43
|
|
|
36
44
|
/** A single-`code`-panel Result — the shape most engines return. */
|
|
@@ -85,7 +93,7 @@ function visual(id, label, lead, opts = {}) {
|
|
|
85
93
|
const t0 = now();
|
|
86
94
|
let view;
|
|
87
95
|
try { view = render(source.source ?? '', options?.config); }
|
|
88
|
-
catch (err) { return fail(msg(err), code(err)); }
|
|
96
|
+
catch (err) { return fail(msg(err), code(err), locate(err, 'source')); }
|
|
89
97
|
const t1 = now();
|
|
90
98
|
return okPanels(renderedPanels(view), ...renderTiming(view, t1 - t0));
|
|
91
99
|
},
|
|
@@ -118,8 +126,50 @@ function renderedPanels(view) {
|
|
|
118
126
|
: [];
|
|
119
127
|
return [{ id: 'preview', label: 'Preview', kind: 'view', vnode }, ...deep];
|
|
120
128
|
}
|
|
121
|
-
/**
|
|
122
|
-
|
|
129
|
+
/**
|
|
130
|
+
* An error Result. `where` is the location half of the format's error —
|
|
131
|
+
* `pane` plus whichever of `path`/`dataPath`/`position`/`line`/`column`
|
|
132
|
+
* the compiler stated (see {@link locate}); only stated fields land on
|
|
133
|
+
* the error, so a field's presence means the compiler said it.
|
|
134
|
+
* @returns {import('./index.js').PlayResult}
|
|
135
|
+
*/
|
|
136
|
+
const fail = (message, c, where) => ({
|
|
137
|
+
ok: false, timing: null,
|
|
138
|
+
error: { message, ...(c === undefined ? {} : { code: c }), ...where },
|
|
139
|
+
panels: [],
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
/** The error Result of a pane whose JSON did not parse — locates the pane. */
|
|
143
|
+
const failParse = (d) => fail(d.error, undefined, { pane: d.pane });
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* The location an engine error carries, in the format's own fields — one
|
|
147
|
+
* adapter over the three shapes the compilers use. The coded family
|
|
148
|
+
* (`@jarenjs/core` `CodedError`: patch, query, JSLT, JTLT) states a
|
|
149
|
+
* `docPath`, a JSON Pointer into the document being compiled or run, and
|
|
150
|
+
* a patch runtime error adds the `dataPath` its operation failed at in the
|
|
151
|
+
* target; the syntax family (JSONPath, JSON Pointer, XQuery) states a
|
|
152
|
+
* 0-based `position` into the source text; the line/column family (JOSL,
|
|
153
|
+
* CSV) states 1-based `line`/`column`. Only what the error carries is
|
|
154
|
+
* copied. `pane` names the editor the phase was working on — the source
|
|
155
|
+
* pane for compile AND run (a runtime location points into the compiled
|
|
156
|
+
* document, which is that pane's text) — and is omitted where the
|
|
157
|
+
* location is into a document the learner never typed (XQuery compiles a
|
|
158
|
+
* GENERATED query document, and a location in it has no pane).
|
|
159
|
+
* @param {unknown} err
|
|
160
|
+
* @param {string} [pane] the pane key the location points into
|
|
161
|
+
*/
|
|
162
|
+
function locate(err, pane) {
|
|
163
|
+
const e = /** @type {any} */ (err);
|
|
164
|
+
/** @type {{ pane?: string, path?: string, dataPath?: string, position?: number, line?: number, column?: number }} */
|
|
165
|
+
const where = pane === undefined ? {} : { pane };
|
|
166
|
+
if (typeof e?.docPath === 'string') where.path = e.docPath;
|
|
167
|
+
if (typeof e?.dataPath === 'string') where.dataPath = e.dataPath;
|
|
168
|
+
if (typeof e?.position === 'number') where.position = e.position;
|
|
169
|
+
if (typeof e?.line === 'number') where.line = e.line;
|
|
170
|
+
if (typeof e?.column === 'number') where.column = e.column;
|
|
171
|
+
return where;
|
|
172
|
+
}
|
|
123
173
|
|
|
124
174
|
/** The compile options the query/jslt engines run with (registry-aware). */
|
|
125
175
|
function compileOptions(options) {
|
|
@@ -135,14 +185,14 @@ export const ENGINE_LIST = [
|
|
|
135
185
|
sourcePanes: [{ key: 'selector', label: 'Selector', control: 'text' }],
|
|
136
186
|
dataPanes: [{ key: 'data', label: 'Data' }],
|
|
137
187
|
run(source, data) {
|
|
138
|
-
const d = parseJson(data.data, 'data'); if (d.error) return
|
|
188
|
+
const d = parseJson(data.data, 'data'); if (d.error) return failParse(d);
|
|
139
189
|
let compiled; const t0 = now();
|
|
140
190
|
try { compiled = compileJSONPath(source.selector ?? ''); }
|
|
141
|
-
catch (err) { return fail(msg(err), code(err)); }
|
|
191
|
+
catch (err) { return fail(msg(err), code(err), locate(err, 'selector')); }
|
|
142
192
|
const t1 = now();
|
|
143
193
|
let nodes;
|
|
144
194
|
try { nodes = compiled.nodes(d.value); }
|
|
145
|
-
catch (err) { return fail(msg(err), code(err)); }
|
|
195
|
+
catch (err) { return fail(msg(err), code(err), locate(err, 'selector')); }
|
|
146
196
|
const t2 = now();
|
|
147
197
|
return ok(fmt(nodes.map((n) => n.value)), t1 - t0, t2 - t1, [
|
|
148
198
|
deepCards('how', 'How it matched', [
|
|
@@ -164,16 +214,16 @@ export const ENGINE_LIST = [
|
|
|
164
214
|
],
|
|
165
215
|
dataPanes: [{ key: 'data', label: 'Data' }],
|
|
166
216
|
run(source, data) {
|
|
167
|
-
const d = parseJson(data.data, 'data'); if (d.error) return
|
|
217
|
+
const d = parseJson(data.data, 'data'); if (d.error) return failParse(d);
|
|
168
218
|
const pointer = source.pointer ?? '';
|
|
169
219
|
const relative = /^\d/.test(pointer);
|
|
170
220
|
let getter; const t0 = now();
|
|
171
221
|
try { getter = relative ? compileRelativeJSONPointer(pointer) : compileJSONPointer(pointer); }
|
|
172
|
-
catch (err) { return fail(msg(err), code(err)); }
|
|
222
|
+
catch (err) { return fail(msg(err), code(err), locate(err, 'pointer')); }
|
|
173
223
|
const t1 = now();
|
|
174
224
|
let value;
|
|
175
225
|
try { value = relative ? getter(d.value, source.location ?? '') : getter(d.value); }
|
|
176
|
-
catch (err) { return fail(msg(err), code(err)); }
|
|
226
|
+
catch (err) { return fail(msg(err), code(err), locate(err, 'pointer')); }
|
|
177
227
|
const t2 = now();
|
|
178
228
|
if (value === JSONPOINTER_NOTHING) return ok('(nothing — the pointer addresses no value)', t1 - t0, t2 - t1);
|
|
179
229
|
return ok(fmt(value), t1 - t0, t2 - t1);
|
|
@@ -193,13 +243,13 @@ export const ENGINE_LIST = [
|
|
|
193
243
|
}],
|
|
194
244
|
run(source, data, options) {
|
|
195
245
|
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
|
|
246
|
+
const target = parseJson(data.data, 'data', 'target'); if (target.error) return failParse(target);
|
|
247
|
+
const patch = parseJson(source.patch, 'patch'); if (patch.error) return failParse(patch);
|
|
198
248
|
if (mode === 'merge') {
|
|
199
249
|
// RFC 7396: null members delete; an unchanged document is the INPUT
|
|
200
250
|
let out; const t0 = now();
|
|
201
251
|
try { out = applyMergePatch(target.value, patch.value); }
|
|
202
|
-
catch (err) { return fail(msg(err), code(err)); }
|
|
252
|
+
catch (err) { return fail(msg(err), code(err), locate(err, 'patch')); }
|
|
203
253
|
const t1 = now();
|
|
204
254
|
return ok(fmt(out), null, t1 - t0, [
|
|
205
255
|
deepCards('how', 'How it merged', [
|
|
@@ -215,7 +265,7 @@ export const ENGINE_LIST = [
|
|
|
215
265
|
jsonPatch = createJSONPatch(target.value, patch.value);
|
|
216
266
|
mergePatch = createMergePatch(target.value, patch.value);
|
|
217
267
|
}
|
|
218
|
-
catch (err) { return fail(msg(err), code(err)); }
|
|
268
|
+
catch (err) { return fail(msg(err), code(err), locate(err, 'patch')); }
|
|
219
269
|
const t1 = now();
|
|
220
270
|
return okPanels([
|
|
221
271
|
{ id: 'out', label: `JSON Patch (${jsonPatch.length} ops)`, kind: 'code', text: fmt(jsonPatch) },
|
|
@@ -224,11 +274,11 @@ export const ENGINE_LIST = [
|
|
|
224
274
|
}
|
|
225
275
|
let apply; const t0 = now();
|
|
226
276
|
try { apply = compileJSONPatch(patch.value, { changes: true }); }
|
|
227
|
-
catch (err) { return fail(msg(err), code(err)); }
|
|
277
|
+
catch (err) { return fail(msg(err), code(err), locate(err, 'patch')); }
|
|
228
278
|
const t1 = now();
|
|
229
279
|
let run;
|
|
230
280
|
try { run = apply(target.value); }
|
|
231
|
-
catch (err) { return fail(msg(err), code(err)); }
|
|
281
|
+
catch (err) { return fail(msg(err), code(err), locate(err, 'patch')); }
|
|
232
282
|
const t2 = now();
|
|
233
283
|
return ok(fmt(run.doc), t1 - t0, t2 - t1, [
|
|
234
284
|
deepCode('changes', 'What changed', fmt(run.changes)),
|
|
@@ -240,19 +290,19 @@ export const ENGINE_LIST = [
|
|
|
240
290
|
sourcePanes: [{ key: 'query', label: 'Query', control: 'code' }, { key: 'externals', label: 'Externals', control: 'code' }],
|
|
241
291
|
dataPanes: [{ key: 'data', label: 'Data' }],
|
|
242
292
|
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
|
|
293
|
+
const d = parseJson(data.data, 'data'); if (d.error) return failParse(d);
|
|
294
|
+
const q = parseJson(source.query, 'query'); if (q.error) return failParse(q);
|
|
245
295
|
let externals = {};
|
|
246
296
|
if (source.externals !== undefined && String(source.externals).trim() !== '') {
|
|
247
|
-
const e = parseJson(source.externals, 'externals'); if (e.error) return
|
|
297
|
+
const e = parseJson(source.externals, 'externals'); if (e.error) return failParse(e); externals = e.value;
|
|
248
298
|
}
|
|
249
299
|
let fn; const t0 = now();
|
|
250
300
|
try { fn = compileJsonQuery(q.value, compileOptions(options)); }
|
|
251
|
-
catch (err) { return fail(msg(err), code(err)); }
|
|
301
|
+
catch (err) { return fail(msg(err), code(err), locate(err, 'query')); }
|
|
252
302
|
const t1 = now();
|
|
253
303
|
let out;
|
|
254
304
|
try { out = fn(d.value, externals); }
|
|
255
|
-
catch (err) { return fail(msg(err), code(err)); }
|
|
305
|
+
catch (err) { return fail(msg(err), code(err), locate(err, 'query')); }
|
|
256
306
|
const t2 = now();
|
|
257
307
|
const items = out === undefined ? 0 : Array.isArray(out) ? out.length : 1;
|
|
258
308
|
return ok(fmt(out), t1 - t0, t2 - t1, [
|
|
@@ -269,15 +319,15 @@ export const ENGINE_LIST = [
|
|
|
269
319
|
sourcePanes: [{ key: 'stylesheet', label: 'Stylesheet', control: 'code' }],
|
|
270
320
|
dataPanes: [{ key: 'data', label: 'Data' }],
|
|
271
321
|
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
|
|
322
|
+
const d = parseJson(data.data, 'data'); if (d.error) return failParse(d);
|
|
323
|
+
const s = parseJson(source.stylesheet, 'stylesheet'); if (s.error) return failParse(s);
|
|
274
324
|
let compiled; const t0 = now();
|
|
275
325
|
try { compiled = compileJsltStylesheet(s.value, compileOptions(options)); }
|
|
276
|
-
catch (err) { return fail(msg(err), code(err)); }
|
|
326
|
+
catch (err) { return fail(msg(err), code(err), locate(err, 'stylesheet')); }
|
|
277
327
|
const t1 = now();
|
|
278
328
|
let out;
|
|
279
329
|
try { out = compiled(d.value); }
|
|
280
|
-
catch (err) { return fail(msg(err), code(err)); }
|
|
330
|
+
catch (err) { return fail(msg(err), code(err), locate(err, 'stylesheet')); }
|
|
281
331
|
const t2 = now();
|
|
282
332
|
// the identity lesson: a rule that changes nothing hands the INPUT back
|
|
283
333
|
// (shared, copy-on-write) — worth teaching, so the deep card says which
|
|
@@ -295,15 +345,15 @@ export const ENGINE_LIST = [
|
|
|
295
345
|
sourcePanes: [{ key: 'template', label: 'Template', control: 'code' }],
|
|
296
346
|
dataPanes: [{ key: 'data', label: 'Data' }],
|
|
297
347
|
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
|
|
348
|
+
const d = parseJson(data.data, 'data'); if (d.error) return failParse(d);
|
|
349
|
+
const t = parseJson(source.template, 'template'); if (t.error) return failParse(t);
|
|
300
350
|
let render; const t0 = now();
|
|
301
351
|
try { render = compileJtltStylesheet(t.value, compileOptions(options)); }
|
|
302
|
-
catch (err) { return fail(msg(err), code(err)); }
|
|
352
|
+
catch (err) { return fail(msg(err), code(err), locate(err, 'template')); }
|
|
303
353
|
const t1 = now();
|
|
304
354
|
let out;
|
|
305
355
|
try { out = render(d.value); }
|
|
306
|
-
catch (err) { return fail(msg(err), code(err)); }
|
|
356
|
+
catch (err) { return fail(msg(err), code(err), locate(err, 'template')); }
|
|
307
357
|
const t2 = now();
|
|
308
358
|
// JTLT emits TEXT (markdown / xml / source) — show it verbatim, not fmt'd
|
|
309
359
|
return ok(out === '' ? '(empty)' : out, t1 - t0, t2 - t1, [
|
|
@@ -317,20 +367,22 @@ export const ENGINE_LIST = [
|
|
|
317
367
|
sourcePanes: [{ key: 'text', label: 'XQuery', control: 'code' }],
|
|
318
368
|
dataPanes: [{ key: 'data', label: 'Data' }],
|
|
319
369
|
run(source, data, options) {
|
|
320
|
-
const d = parseJson(data.data, 'data'); if (d.error) return
|
|
370
|
+
const d = parseJson(data.data, 'data'); if (d.error) return failParse(d);
|
|
321
371
|
let doc, fn; const t0 = now();
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
}
|
|
326
|
-
catch (err) { return fail(msg(err), code(err)); }
|
|
372
|
+
// two phases: the text parses (a syntax error locates a position in
|
|
373
|
+
// the XQuery pane), then the GENERATED query document compiles and
|
|
374
|
+
// runs — a location in that document has no pane the learner typed
|
|
375
|
+
try { doc = parseXQuery(source.text ?? ''); }
|
|
376
|
+
catch (err) { return fail(msg(err), code(err), locate(err, 'text')); }
|
|
377
|
+
try { fn = compileJsonQuery(doc, compileOptions(options)); }
|
|
378
|
+
catch (err) { return fail(msg(err), code(err), locate(err)); }
|
|
327
379
|
const t1 = now();
|
|
328
380
|
let out;
|
|
329
381
|
try {
|
|
330
382
|
const externals = fn.externals.includes('doc') ? { doc: d.value } : {};
|
|
331
383
|
out = fn(d.value, externals);
|
|
332
384
|
}
|
|
333
|
-
catch (err) { return fail(msg(err), code(err)); }
|
|
385
|
+
catch (err) { return fail(msg(err), code(err), locate(err)); }
|
|
334
386
|
const t2 = now();
|
|
335
387
|
return ok(out === undefined ? '(empty sequence)' : fmt(out), t1 - t0, t2 - t1, [
|
|
336
388
|
// the machinery: the XQuery text parses to a runnable query DOCUMENT
|
|
@@ -355,7 +407,7 @@ export const ENGINE_LIST = [
|
|
|
355
407
|
// parser streams document-order events as it reads — capture (capped)
|
|
356
408
|
// for the "how it streamed" drill-down
|
|
357
409
|
try { parsed = parseJosl(source.text ?? '', { mode, onEvent: (e) => { if (events.length < 200) events.push(e); } }); }
|
|
358
|
-
catch (err) { return fail(msg(err), code(err)); }
|
|
410
|
+
catch (err) { return fail(msg(err), code(err), locate(err, 'text')); }
|
|
359
411
|
const t1 = now();
|
|
360
412
|
const out = stringifyJsonx(parsed, { indent: 2 });
|
|
361
413
|
const t2 = now();
|
|
@@ -391,7 +443,7 @@ export const ENGINE_LIST = [
|
|
|
391
443
|
// strict mode THROWS on the first RFC 4180 violation (with a code);
|
|
392
444
|
// repair mode reads anyway and lists every fix — the lesson of the tab
|
|
393
445
|
try { doc = parseCsvDocument(source.text ?? '', opts); }
|
|
394
|
-
catch (err) { return fail(msg(err), code(err)); }
|
|
446
|
+
catch (err) { return fail(msg(err), code(err), locate(err, 'text')); }
|
|
395
447
|
const t1 = now();
|
|
396
448
|
// one calm SCREEN (the summary note) plus the drill-down: the parsed
|
|
397
449
|
// records, the sniffed dialect, the repairs, and the CSV round-trip
|
|
@@ -461,12 +513,12 @@ export const ENGINE_LIST = [
|
|
|
461
513
|
if (typeof validate !== 'function') {
|
|
462
514
|
return fail('the JSON Schema engine validates in the host — inject options.validate', 'PLAY_NO_VALIDATOR');
|
|
463
515
|
}
|
|
464
|
-
const d = parseJson(data.data, 'data'); if (d.error) return
|
|
516
|
+
const d = parseJson(data.data, 'data'); if (d.error) return failParse(d);
|
|
465
517
|
const locale = options?.config?.locale ?? 'en';
|
|
466
518
|
let report;
|
|
467
519
|
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');
|
|
520
|
+
catch (err) { return fail(msg(err), code(err), locate(err)); }
|
|
521
|
+
if (report.schemaError) return fail(report.schemaError, 'SCHEMA', { pane: 'schema' });
|
|
470
522
|
const errs = report.errors ?? [];
|
|
471
523
|
const summary = (report.valid ? '✓ valid' : `✗ ${errs.length} error${errs.length === 1 ? '' : 's'}`)
|
|
472
524
|
+ ` · ${report.draft} · compiled ${formatMs(report.compileMs)} · validated ${formatMs(report.validateMs)}`;
|
|
@@ -494,11 +546,11 @@ export const ENGINE_LIST = [
|
|
|
494
546
|
if (typeof render !== 'function') {
|
|
495
547
|
return fail('the MDX engine renders in the host — inject options.renderers.mdx', 'PLAY_NO_RENDERER');
|
|
496
548
|
}
|
|
497
|
-
const d = parseJson(data.data, 'data'); if (d.error) return
|
|
549
|
+
const d = parseJson(data.data, 'data'); if (d.error) return failParse(d);
|
|
498
550
|
const t0 = now();
|
|
499
551
|
let view;
|
|
500
552
|
try { view = render(source.source ?? '', d.value); }
|
|
501
|
-
catch (err) { return fail(msg(err), code(err)); }
|
|
553
|
+
catch (err) { return fail(msg(err), code(err), locate(err, 'source')); }
|
|
502
554
|
const t1 = now();
|
|
503
555
|
return okPanels(renderedPanels(view), ...renderTiming(view, t1 - t0));
|
|
504
556
|
},
|
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
|
*/
|
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; }
|