@rohal12/spindle 0.58.0 → 0.59.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/dist/pkg/format.js +1 -1
- package/dist/pkg/headless.js +428 -277
- package/dist/pkg/macro-registry.json +29 -11
- package/dist/pkg/story-variables.js +330 -207
- package/package.json +1 -1
- package/src/automation/runner.ts +2 -1
- package/src/code-check.ts +158 -83
- package/src/components/macros/Button.tsx +3 -1
- package/src/components/macros/Checkbox.tsx +4 -1
- package/src/components/macros/Dialog.tsx +1 -1
- package/src/components/macros/MacroLink.tsx +3 -2
- package/src/components/macros/Meter.tsx +6 -2
- package/src/components/macros/Radiobutton.tsx +2 -2
- package/src/components/macros/Watch.tsx +11 -6
- package/src/components/macros/arg-utils.ts +0 -37
- package/src/components/macros/input-macro.ts +4 -1
- package/src/components/macros/macro-args.ts +93 -52
- package/src/components/macros/option-utils.ts +1 -1
- package/src/index.tsx +2 -4
- package/src/js-lexer.ts +19 -0
- package/src/markup/parse.ts +22 -4
- package/src/markup/spindle.peggy +21 -9
- package/src/markup/tokens.ts +3 -0
- package/src/markup/validate.ts +13 -10
- package/src/registry.ts +63 -6
- package/src/saves/storage.ts +14 -1
- package/src/store.ts +29 -7
- package/src/story-variables.ts +31 -65
- package/src/structural.ts +67 -21
- package/src/tooling.ts +15 -16
- package/src/widgets/widget-def.ts +2 -2
- package/types/index.d.ts +23 -0
- package/types/tooling.d.ts +28 -2
package/package.json
CHANGED
package/src/automation/runner.ts
CHANGED
|
@@ -8,6 +8,7 @@ import type {
|
|
|
8
8
|
import type { StoryAPI } from '../story-api';
|
|
9
9
|
import type { StoryAction } from '../action-registry';
|
|
10
10
|
import { errorMessage } from '../utils/error-message';
|
|
11
|
+
import { deepEqual } from '../structural';
|
|
11
12
|
|
|
12
13
|
export interface RunOptions {
|
|
13
14
|
onStep?: (stepIndex: number, step: AutomationStep) => void;
|
|
@@ -88,7 +89,7 @@ async function executeStep(
|
|
|
88
89
|
if (assert.variables) {
|
|
89
90
|
for (const [key, expected] of Object.entries(assert.variables)) {
|
|
90
91
|
const actual = story.get(key);
|
|
91
|
-
if (actual
|
|
92
|
+
if (!deepEqual(actual, expected)) {
|
|
92
93
|
errors.push({
|
|
93
94
|
step: index,
|
|
94
95
|
message: `Variable "${key}": expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`,
|
package/src/code-check.ts
CHANGED
|
@@ -8,18 +8,20 @@
|
|
|
8
8
|
* - `{$…}` expressions, and `{do}` bodies (statements);
|
|
9
9
|
* - the conditions of `{if}`, `{elseif}` and `{case}`;
|
|
10
10
|
* - macro arguments whose declared parameter type is `expression`,
|
|
11
|
-
* `statements` or `passage`, built-in and custom macros alike
|
|
12
|
-
*
|
|
11
|
+
* `statements` or `passage`, built-in and custom macros alike, and the
|
|
12
|
+
* `string` and `text` arguments that hold code (ParameterDef.holds: the
|
|
13
|
+
* condition and `run` action of `{watch}`);
|
|
13
14
|
* - the `{$…}` references in attributes holding code (`onclick`).
|
|
14
15
|
*
|
|
15
|
-
* Text that may hold markup is found in
|
|
16
|
-
*
|
|
17
|
-
* markup in turn.
|
|
16
|
+
* Text that may hold markup is found in the `string` and `text` arguments
|
|
17
|
+
* that hold markup (labels such as `{button}`'s) and HTML attribute values;
|
|
18
|
+
* the check reads its markup in turn.
|
|
18
19
|
*
|
|
19
20
|
* Passage names written out are found in links (`[[Go->Hall]]`), in
|
|
20
21
|
* `passage` arguments that are one quoted string (`{goto "Hall"}`), in the
|
|
21
|
-
*
|
|
22
|
-
* body of `{dialog}`; the check
|
|
22
|
+
* `string` and `text` arguments that hold a passage name (the `goto` and
|
|
23
|
+
* `dialog` actions of `{watch}`) and in the body of `{dialog}`; the check
|
|
24
|
+
* looks each one up.
|
|
23
25
|
*
|
|
24
26
|
* The arguments of macros that declare no parameters may be anything:
|
|
25
27
|
* they are not checked.
|
|
@@ -28,13 +30,16 @@ import type { Token } from './markup/tokens';
|
|
|
28
30
|
import { isCodeAttribute, splitSigilTemplate } from './markup/code-attributes';
|
|
29
31
|
import { CodeSyntaxError, parseCode, type JsGoal } from './js-lexer';
|
|
30
32
|
import type { ParsedCode } from './js-lexer';
|
|
31
|
-
import type { ParameterDef } from './registry';
|
|
33
|
+
import type { ParameterDef, StringHolds } from './registry';
|
|
32
34
|
import {
|
|
33
35
|
MacroArgumentError,
|
|
34
36
|
parseMacroArgs,
|
|
35
37
|
passageTarget,
|
|
38
|
+
type ArgSpans,
|
|
36
39
|
type PassageTarget,
|
|
37
40
|
} from './components/macros/macro-args';
|
|
41
|
+
import { subMacroParameters } from './components/macros/option-utils';
|
|
42
|
+
import { NameMap } from './utils/macro-names';
|
|
38
43
|
|
|
39
44
|
/** While a pass runs: the code it parsed so far, by goal and source. */
|
|
40
45
|
let parses: Map<string, ParsedCode | CodeSyntaxError> | null = null;
|
|
@@ -79,7 +84,10 @@ export interface CodePiece {
|
|
|
79
84
|
goal: JsGoal;
|
|
80
85
|
/** The markup it is in, for the error: `{print $a +}`. */
|
|
81
86
|
label: string;
|
|
82
|
-
/**
|
|
87
|
+
/**
|
|
88
|
+
* Whether it names a passage: a `passage` argument that is an
|
|
89
|
+
* expression (one that is a string literal is a PassagePiece).
|
|
90
|
+
*/
|
|
83
91
|
passage?: boolean;
|
|
84
92
|
/** Whether it is the code in a quoted string, as in `{watch}`. */
|
|
85
93
|
inString?: boolean;
|
|
@@ -94,7 +102,7 @@ export interface PassagePiece {
|
|
|
94
102
|
offset: number;
|
|
95
103
|
/** The markup it is in, for the error: `[[Go->Hall]]`. */
|
|
96
104
|
label: string;
|
|
97
|
-
/** How much of the markup is the name, as written. */
|
|
105
|
+
/** How much of the markup is the name, as written (quotes included). */
|
|
98
106
|
length: number;
|
|
99
107
|
/** The macro it is the argument of; `link` for `[[…]]` links. */
|
|
100
108
|
macro: string;
|
|
@@ -121,30 +129,78 @@ export interface TextPiece {
|
|
|
121
129
|
/** Macros whose whole argument text is one expression: branch conditions. */
|
|
122
130
|
const CONDITION_MACROS = new Set(['if', 'elseif', 'case']);
|
|
123
131
|
|
|
124
|
-
/**
|
|
125
|
-
const
|
|
126
|
-
['watch', { condition: 'expression', run: 'statements' }],
|
|
127
|
-
]);
|
|
132
|
+
/** Block macros whose body is the name of a passage. */
|
|
133
|
+
const PASSAGE_BODIES = new Set(['dialog']);
|
|
128
134
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
135
|
+
type Piece = CodePiece | TextPiece | PassagePiece | ArgumentErrorPiece;
|
|
136
|
+
|
|
137
|
+
/** The declared parameters of a macro, if it has any. */
|
|
138
|
+
export type ParametersOf = (
|
|
139
|
+
macro: string,
|
|
140
|
+
) => readonly ParameterDef[] | undefined;
|
|
141
|
+
|
|
142
|
+
/** What the argument check needs to know of a macro (see MacroMetadata). */
|
|
143
|
+
export interface MacroParameters {
|
|
144
|
+
name: string;
|
|
145
|
+
parameters?: readonly ParameterDef[];
|
|
146
|
+
interpolate?: boolean;
|
|
147
|
+
}
|
|
133
148
|
|
|
134
149
|
/**
|
|
135
|
-
*
|
|
136
|
-
*
|
|
150
|
+
* `params` with each `string` and `text` parameter (options too) saying
|
|
151
|
+
* what it holds: what it declares, else `holds`.
|
|
137
152
|
*/
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
153
|
+
function withHolds(
|
|
154
|
+
params: readonly ParameterDef[],
|
|
155
|
+
holds: StringHolds,
|
|
156
|
+
): readonly ParameterDef[] {
|
|
157
|
+
return params.map((param) => {
|
|
158
|
+
if (param.parameters) {
|
|
159
|
+
return { ...param, parameters: withHolds(param.parameters, holds) };
|
|
160
|
+
}
|
|
161
|
+
const isString = param.type === 'string' || param.type === 'text';
|
|
162
|
+
return isString && !param.holds ? { ...param, holds } : param;
|
|
163
|
+
});
|
|
164
|
+
}
|
|
143
165
|
|
|
144
|
-
/**
|
|
145
|
-
|
|
166
|
+
/**
|
|
167
|
+
* The declared parameters of `macros`, by name in any case, and of the
|
|
168
|
+
* built-in sub-macros (`{option}`): what the story-start check, reference
|
|
169
|
+
* collection and tooling read arguments by. Each `string` and `text`
|
|
170
|
+
* parameter says what it holds: what it declares, else `markup` for a macro
|
|
171
|
+
* with `interpolate` (which can resolve markup) and `text` for any other.
|
|
172
|
+
*/
|
|
173
|
+
export function parameterLookup(
|
|
174
|
+
macros: Iterable<MacroParameters>,
|
|
175
|
+
): ParametersOf {
|
|
176
|
+
const parameters = new NameMap<readonly ParameterDef[]>();
|
|
177
|
+
for (const { name, parameters: params, interpolate } of macros) {
|
|
178
|
+
if (params)
|
|
179
|
+
parameters.set(name, withHolds(params, holdsByDefault(interpolate)));
|
|
180
|
+
}
|
|
181
|
+
return (name) => {
|
|
182
|
+
let params = parameters.get(name);
|
|
183
|
+
if (!params) {
|
|
184
|
+
const sub = subMacroParameters(name);
|
|
185
|
+
if (sub) parameters.set(name, (params = withHolds(sub, 'text')));
|
|
186
|
+
}
|
|
187
|
+
return params;
|
|
188
|
+
};
|
|
189
|
+
}
|
|
146
190
|
|
|
147
|
-
|
|
191
|
+
/** What a `string` or `text` argument holds when its parameter doesn't say. */
|
|
192
|
+
const holdsByDefault = (interpolate: boolean | undefined): StringHolds =>
|
|
193
|
+
interpolate ? 'markup' : 'text';
|
|
194
|
+
|
|
195
|
+
/** Whether a parameter in `params` (options too) holds code in a string. */
|
|
196
|
+
export function holdsCode(params: readonly ParameterDef[]): boolean {
|
|
197
|
+
return params.some(
|
|
198
|
+
(p) =>
|
|
199
|
+
p.holds === 'expression' ||
|
|
200
|
+
p.holds === 'statements' ||
|
|
201
|
+
(!!p.parameters && holdsCode(p.parameters)),
|
|
202
|
+
);
|
|
203
|
+
}
|
|
148
204
|
|
|
149
205
|
/** The goal of the code an argument of this type holds, if it is code. */
|
|
150
206
|
function codeGoal(param: ParameterDef): JsGoal | undefined {
|
|
@@ -155,6 +211,15 @@ function codeGoal(param: ParameterDef): JsGoal | undefined {
|
|
|
155
211
|
return undefined;
|
|
156
212
|
}
|
|
157
213
|
|
|
214
|
+
/**
|
|
215
|
+
* What a `string` or `text` argument holds: what its parameter says (see
|
|
216
|
+
* parameterLookup), else markup, which is checked.
|
|
217
|
+
*/
|
|
218
|
+
function stringHolds(param: ParameterDef): StringHolds | undefined {
|
|
219
|
+
if (param.type !== 'string' && param.type !== 'text') return undefined;
|
|
220
|
+
return param.holds ?? 'markup';
|
|
221
|
+
}
|
|
222
|
+
|
|
158
223
|
/**
|
|
159
224
|
* The code and the markup-holding text in `tokens`, the tokens of `src`.
|
|
160
225
|
* `parametersOf` gives the declared parameters of a macro, if it has any.
|
|
@@ -162,7 +227,7 @@ function codeGoal(param: ParameterDef): JsGoal | undefined {
|
|
|
162
227
|
export function* codeAndText(
|
|
163
228
|
src: string,
|
|
164
229
|
tokens: readonly Token[],
|
|
165
|
-
parametersOf:
|
|
230
|
+
parametersOf: ParametersOf,
|
|
166
231
|
): Generator<Piece> {
|
|
167
232
|
/** The index of the closing tag of the `name` macro opened at `t`. */
|
|
168
233
|
const closeOf = (t: number, name: string) =>
|
|
@@ -179,8 +244,8 @@ export function* codeAndText(
|
|
|
179
244
|
yield {
|
|
180
245
|
kind: 'passage',
|
|
181
246
|
name: token.target,
|
|
182
|
-
offset:
|
|
183
|
-
length: token.
|
|
247
|
+
offset: token.targetStart,
|
|
248
|
+
length: token.targetEnd - token.targetStart,
|
|
184
249
|
macro: 'link',
|
|
185
250
|
label: src.slice(token.start, token.end),
|
|
186
251
|
};
|
|
@@ -193,9 +258,11 @@ export function* codeAndText(
|
|
|
193
258
|
label: `{${token.expression}}`,
|
|
194
259
|
};
|
|
195
260
|
} else if (token.type === 'html') {
|
|
261
|
+
// The values are the source text between their quotes, in order
|
|
262
|
+
let cursor = token.start;
|
|
196
263
|
for (const [name, value] of Object.entries(token.attributes)) {
|
|
197
|
-
|
|
198
|
-
|
|
264
|
+
const at = locate(src, value, cursor);
|
|
265
|
+
cursor = at + value.length;
|
|
199
266
|
if (!isCodeAttribute(name)) {
|
|
200
267
|
yield {
|
|
201
268
|
kind: 'text',
|
|
@@ -242,12 +309,12 @@ export function* codeAndText(
|
|
|
242
309
|
const body = tokens.slice(t + 1, close);
|
|
243
310
|
if (close > t + 1 && body.every((b) => b.type === 'text')) {
|
|
244
311
|
const text = src.slice(token.end, tokens[close]!.start);
|
|
245
|
-
const
|
|
312
|
+
const written = text.trim();
|
|
246
313
|
yield {
|
|
247
314
|
kind: 'passage',
|
|
248
|
-
name:
|
|
249
|
-
offset:
|
|
250
|
-
length:
|
|
315
|
+
name: written.replace(/^["']|["']$/g, ''),
|
|
316
|
+
offset: token.end + text.indexOf(written),
|
|
317
|
+
length: written.length,
|
|
251
318
|
macro: name,
|
|
252
319
|
label: src.slice(token.start, token.end),
|
|
253
320
|
};
|
|
@@ -255,7 +322,9 @@ export function* codeAndText(
|
|
|
255
322
|
}
|
|
256
323
|
if (!args) continue;
|
|
257
324
|
const label = `{${token.name} ${args}}`;
|
|
258
|
-
|
|
325
|
+
// The arguments end the tag, but for the whitespace before its }
|
|
326
|
+
const tag = src.slice(token.start, token.end - 1);
|
|
327
|
+
const argsAt = token.start + tag.trimEnd().length - args.length;
|
|
259
328
|
if (CONDITION_MACROS.has(name)) {
|
|
260
329
|
yield {
|
|
261
330
|
kind: 'code',
|
|
@@ -281,18 +350,15 @@ export function* argPieces(
|
|
|
281
350
|
label: string,
|
|
282
351
|
): Generator<Piece> {
|
|
283
352
|
let values: Record<string, unknown>;
|
|
353
|
+
const spans: ArgSpans = new Map();
|
|
284
354
|
try {
|
|
285
|
-
values = parseMacroArgs(args, params) as Record<string, unknown>;
|
|
355
|
+
values = parseMacroArgs(args, params, spans) as Record<string, unknown>;
|
|
286
356
|
} catch (error) {
|
|
287
357
|
if (!(error instanceof MacroArgumentError)) throw error;
|
|
288
358
|
yield { kind: 'argument-error', message: error.message, offset, label };
|
|
289
359
|
return;
|
|
290
360
|
}
|
|
291
|
-
const
|
|
292
|
-
const inStrings = CODE_IN_STRINGS.get(key) ?? {};
|
|
293
|
-
const passageStrings = PASSAGE_STRINGS.get(key) ?? [];
|
|
294
|
-
const literalStrings = LITERAL_STRINGS.get(key) ?? [];
|
|
295
|
-
let cursor = 0;
|
|
361
|
+
const name = macro.toLowerCase();
|
|
296
362
|
/** The pieces of the parameters `list`, with their values in `from`. */
|
|
297
363
|
function* visit(
|
|
298
364
|
list: readonly ParameterDef[],
|
|
@@ -307,58 +373,70 @@ export function* argPieces(
|
|
|
307
373
|
);
|
|
308
374
|
continue;
|
|
309
375
|
}
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
const
|
|
376
|
+
const span = spans.get(param);
|
|
377
|
+
if (typeof value !== 'string' || !value.trim() || !span) continue;
|
|
378
|
+
const [start, end] = span;
|
|
379
|
+
const written = args.slice(start, end);
|
|
380
|
+
/** The name as written (quotes included), at the argument. */
|
|
381
|
+
const passage = (passageName: string): PassagePiece => ({
|
|
382
|
+
kind: 'passage',
|
|
383
|
+
name: passageName,
|
|
384
|
+
offset: offset + start,
|
|
385
|
+
length: end - start,
|
|
386
|
+
macro: name,
|
|
387
|
+
label,
|
|
388
|
+
});
|
|
389
|
+
const goal = codeGoal(param);
|
|
390
|
+
if (param.type === 'passage') {
|
|
391
|
+
// A name written out, else an expression naming one
|
|
392
|
+
const target = passageTarget(value);
|
|
393
|
+
if (target.kind === 'name') {
|
|
394
|
+
yield passage(target.name);
|
|
395
|
+
continue;
|
|
396
|
+
}
|
|
397
|
+
}
|
|
314
398
|
if (goal) {
|
|
315
399
|
const piece: CodePiece = {
|
|
316
400
|
kind: 'code',
|
|
317
401
|
code: value,
|
|
318
|
-
offset: offset +
|
|
402
|
+
offset: offset + start,
|
|
319
403
|
goal,
|
|
320
404
|
label,
|
|
321
405
|
};
|
|
322
406
|
if (param.type === 'passage') {
|
|
323
407
|
piece.passage = true;
|
|
324
|
-
piece.macro =
|
|
408
|
+
piece.macro = name;
|
|
325
409
|
}
|
|
326
|
-
if (!codeGoal(param)) piece.inString = true;
|
|
327
410
|
yield piece;
|
|
411
|
+
continue;
|
|
328
412
|
}
|
|
329
|
-
//
|
|
330
|
-
const
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
: passageStrings.includes(param.name)
|
|
335
|
-
? ({ kind: 'name', name: value } as const)
|
|
336
|
-
: null;
|
|
337
|
-
if (target?.kind === 'name') {
|
|
413
|
+
// The value of a string is inside its quotes
|
|
414
|
+
const at =
|
|
415
|
+
offset + start + (value !== written && /^["']/.test(written) ? 1 : 0);
|
|
416
|
+
const holds = stringHolds(param);
|
|
417
|
+
if (holds === 'expression' || holds === 'statements') {
|
|
338
418
|
yield {
|
|
339
|
-
kind: '
|
|
340
|
-
|
|
341
|
-
offset:
|
|
342
|
-
|
|
343
|
-
macro: macro.toLowerCase(),
|
|
419
|
+
kind: 'code',
|
|
420
|
+
code: value,
|
|
421
|
+
offset: at,
|
|
422
|
+
goal: holds,
|
|
344
423
|
label,
|
|
424
|
+
inString: true,
|
|
345
425
|
};
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
(param.type === 'text' || param.type === 'string') &&
|
|
350
|
-
!literalStrings.includes(param.name)
|
|
351
|
-
) {
|
|
426
|
+
} else if (holds === 'passage') {
|
|
427
|
+
yield passage(value);
|
|
428
|
+
} else if (holds === 'markup') {
|
|
352
429
|
yield {
|
|
353
430
|
kind: 'text',
|
|
354
431
|
text: value,
|
|
355
|
-
offset:
|
|
432
|
+
offset: at,
|
|
356
433
|
where: `In the ${param.name} of {${macro}}: `,
|
|
357
434
|
};
|
|
358
435
|
}
|
|
359
436
|
}
|
|
360
437
|
}
|
|
361
|
-
|
|
438
|
+
// Options are visited in metadata order, which is not the order written
|
|
439
|
+
yield* [...visit(params, values)].sort((a, b) => a.offset - b.offset);
|
|
362
440
|
}
|
|
363
441
|
|
|
364
442
|
/**
|
|
@@ -387,14 +465,15 @@ export interface PassageReference {
|
|
|
387
465
|
* The passages the markup `src` (with the `tokens` it tokenizes to) names:
|
|
388
466
|
* `[[…]]` links, the passage argument of `{goto}`, `{include}` and `{link}`
|
|
389
467
|
* (and of macros that declare one), the `goto` and `dialog` actions of
|
|
390
|
-
* `{watch}` and the
|
|
391
|
-
*
|
|
392
|
-
*
|
|
468
|
+
* `{watch}` (and the `string` and `text` arguments of macros that declare
|
|
469
|
+
* they hold one) and the body of `{dialog}`, in source order. A name written
|
|
470
|
+
* out is a literal; the others are expressions, which name a passage when
|
|
471
|
+
* they run.
|
|
393
472
|
*/
|
|
394
473
|
export function collectPassageReferences(
|
|
395
474
|
src: string,
|
|
396
475
|
tokens: readonly Token[],
|
|
397
|
-
parametersOf:
|
|
476
|
+
parametersOf: ParametersOf,
|
|
398
477
|
): PassageReference[] {
|
|
399
478
|
const refs: PassageReference[] = [];
|
|
400
479
|
for (const piece of codeAndText(src, tokens, parametersOf)) {
|
|
@@ -408,15 +487,11 @@ export function collectPassageReferences(
|
|
|
408
487
|
} else if (piece.kind === 'code' && piece.passage) {
|
|
409
488
|
refs.push({
|
|
410
489
|
macro: piece.macro!,
|
|
411
|
-
target:
|
|
490
|
+
target: { kind: 'expression', expression: piece.code },
|
|
412
491
|
start: piece.offset,
|
|
413
492
|
end: piece.offset + piece.code.length,
|
|
414
493
|
});
|
|
415
494
|
}
|
|
416
495
|
}
|
|
417
|
-
return refs
|
|
418
|
-
(ref, i) =>
|
|
419
|
-
refs.findIndex((r) => r.start === ref.start && r.macro === ref.macro) ===
|
|
420
|
-
i,
|
|
421
|
-
);
|
|
496
|
+
return refs;
|
|
422
497
|
}
|
|
@@ -5,7 +5,9 @@ defineMacro({
|
|
|
5
5
|
name: 'button',
|
|
6
6
|
block: true,
|
|
7
7
|
interpolate: true,
|
|
8
|
-
parameters: [
|
|
8
|
+
parameters: [
|
|
9
|
+
{ name: 'label', type: 'text', holds: 'markup', required: true },
|
|
10
|
+
],
|
|
9
11
|
render({ rawArgs, children = [] }, ctx) {
|
|
10
12
|
const label = ctx.resolve?.(ctx.args.label ?? '') ?? rawArgs;
|
|
11
13
|
const runBody = useDetachedBody();
|
|
@@ -4,7 +4,10 @@ import { VARIABLE_PARAMETER, useVariableAction } from './input-macro';
|
|
|
4
4
|
defineMacro({
|
|
5
5
|
name: 'checkbox',
|
|
6
6
|
storeVar: true,
|
|
7
|
-
parameters: [
|
|
7
|
+
parameters: [
|
|
8
|
+
VARIABLE_PARAMETER,
|
|
9
|
+
{ name: 'label', type: 'text', holds: 'text' },
|
|
10
|
+
],
|
|
8
11
|
render(_props, ctx) {
|
|
9
12
|
const label = ctx.args.label ?? '';
|
|
10
13
|
|
|
@@ -12,11 +12,12 @@ defineMacro({
|
|
|
12
12
|
// {link "text" passage} or {link "text"}; the passage is a quoted name or
|
|
13
13
|
// an expression, as in {goto}.
|
|
14
14
|
parameters: [
|
|
15
|
-
{ name: 'text', type: 'string', required: true },
|
|
15
|
+
{ name: 'text', type: 'string', holds: 'markup', required: true },
|
|
16
16
|
{ name: 'passage', type: 'passage' },
|
|
17
17
|
],
|
|
18
18
|
render({ children = [] }, ctx) {
|
|
19
|
-
|
|
19
|
+
// The label is markup: resolved once, for the element and its action
|
|
20
|
+
const display = ctx.resolve?.(ctx.args.text ?? '') ?? '';
|
|
20
21
|
let passage: string | null = null;
|
|
21
22
|
// Boxed: anything can be thrown, including null and other falsy values
|
|
22
23
|
let failure: { error: unknown } | undefined;
|
|
@@ -40,11 +40,15 @@ defineMacro({
|
|
|
40
40
|
parameters: [
|
|
41
41
|
{ name: 'current', type: 'expression', required: true },
|
|
42
42
|
{ name: 'max', type: 'expression', required: true },
|
|
43
|
-
{ name: 'label', type: 'string' },
|
|
43
|
+
{ name: 'label', type: 'string', holds: 'markup' },
|
|
44
44
|
],
|
|
45
45
|
render(_props, ctx) {
|
|
46
46
|
try {
|
|
47
|
-
const { currentExpr, maxExpr, labelMode } = meterArgs(
|
|
47
|
+
const { currentExpr, maxExpr, labelMode } = meterArgs({
|
|
48
|
+
...ctx.args,
|
|
49
|
+
// The label is markup: resolved before reading none, % or a unit
|
|
50
|
+
label: ctx.resolve?.(ctx.args.label),
|
|
51
|
+
});
|
|
48
52
|
const current = Number(ctx.evaluate!(currentExpr));
|
|
49
53
|
const max = Number(ctx.evaluate!(maxExpr));
|
|
50
54
|
const pct =
|
|
@@ -6,8 +6,8 @@ defineMacro({
|
|
|
6
6
|
storeVar: true,
|
|
7
7
|
parameters: [
|
|
8
8
|
VARIABLE_PARAMETER,
|
|
9
|
-
{ name: 'value', type: 'text', required: true },
|
|
10
|
-
{ name: 'label', type: 'text' },
|
|
9
|
+
{ name: 'value', type: 'text', holds: 'text', required: true },
|
|
10
|
+
{ name: 'label', type: 'text', holds: 'text' },
|
|
11
11
|
],
|
|
12
12
|
render(_props, ctx) {
|
|
13
13
|
const radioValue = ctx.args.value ?? '';
|
|
@@ -5,15 +5,20 @@ defineMacro({
|
|
|
5
5
|
name: 'watch',
|
|
6
6
|
// {watch "condition" goto "X" dialog "X" run "X" name "X" priority N once}
|
|
7
7
|
parameters: [
|
|
8
|
-
{
|
|
8
|
+
{
|
|
9
|
+
name: 'condition',
|
|
10
|
+
type: 'string',
|
|
11
|
+
holds: 'expression',
|
|
12
|
+
required: true,
|
|
13
|
+
},
|
|
9
14
|
{
|
|
10
15
|
name: 'options',
|
|
11
16
|
type: 'options',
|
|
12
17
|
parameters: [
|
|
13
|
-
{ name: 'goto', type: 'string' },
|
|
14
|
-
{ name: 'dialog', type: 'string' },
|
|
15
|
-
{ name: 'run', type: 'string' },
|
|
16
|
-
{ name: 'name', type: 'string' },
|
|
18
|
+
{ name: 'goto', type: 'string', holds: 'passage' },
|
|
19
|
+
{ name: 'dialog', type: 'string', holds: 'passage' },
|
|
20
|
+
{ name: 'run', type: 'string', holds: 'statements' },
|
|
21
|
+
{ name: 'name', type: 'string', holds: 'text' },
|
|
17
22
|
{ name: 'priority', type: 'number' },
|
|
18
23
|
{ name: 'once', type: 'flag' },
|
|
19
24
|
],
|
|
@@ -41,7 +46,7 @@ defineMacro({
|
|
|
41
46
|
|
|
42
47
|
defineMacro({
|
|
43
48
|
name: 'unwatch',
|
|
44
|
-
parameters: [{ name: 'name', type: 'text', required: true }],
|
|
49
|
+
parameters: [{ name: 'name', type: 'text', holds: 'text', required: true }],
|
|
45
50
|
render(_props, ctx) {
|
|
46
51
|
const name = ctx.args.name ?? '';
|
|
47
52
|
|
|
@@ -69,43 +69,6 @@ export function readWholeQuoted(src: string): string | null {
|
|
|
69
69
|
return quoted && quoted.end === src.length ? quoted.value : null;
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
-
const JS_SIMPLE_ESCAPES: Record<string, string> = {
|
|
73
|
-
n: '\n',
|
|
74
|
-
t: '\t',
|
|
75
|
-
r: '\r',
|
|
76
|
-
b: '\b',
|
|
77
|
-
f: '\f',
|
|
78
|
-
v: '\v',
|
|
79
|
-
'0': '\0',
|
|
80
|
-
};
|
|
81
|
-
|
|
82
|
-
/** Undo every escape of a JavaScript string literal's body. */
|
|
83
|
-
function decodeJsEscapes(body: string): string {
|
|
84
|
-
return body.replace(
|
|
85
|
-
/\\(?:u\{([0-9a-fA-F]+)\}|u([0-9a-fA-F]{4})|x([0-9a-fA-F]{2})|(\r\n|[\n\r\u2028\u2029])|([\s\S]))/g,
|
|
86
|
-
(_, braced, unicode, hex, lineBreak, other: string | undefined) => {
|
|
87
|
-
if (lineBreak !== undefined) return '';
|
|
88
|
-
const code = braced ?? unicode ?? hex;
|
|
89
|
-
if (code !== undefined) {
|
|
90
|
-
const point = parseInt(code, 16);
|
|
91
|
-
return point <= 0x10ffff ? String.fromCodePoint(point) : '';
|
|
92
|
-
}
|
|
93
|
-
return JS_SIMPLE_ESCAPES[other!] ?? other!;
|
|
94
|
-
},
|
|
95
|
-
);
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
/**
|
|
99
|
-
* The value of `src` when it is exactly one `"…"` or `'…'` string, read as
|
|
100
|
-
* the JavaScript literal it is (`"\u0048all"` is `Hall`), otherwise `null`.
|
|
101
|
-
* `readWholeQuoted` keeps other escapes as written, for labels and code.
|
|
102
|
-
*/
|
|
103
|
-
export function readWholeJsString(src: string): string | null {
|
|
104
|
-
const quoted = readQuoted(src, 0);
|
|
105
|
-
if (!quoted || quoted.end !== src.length) return null;
|
|
106
|
-
return decodeJsEscapes(src.slice(1, -1));
|
|
107
|
-
}
|
|
108
|
-
|
|
109
72
|
/**
|
|
110
73
|
* Strip an optional quote from each end of an unquoted-or-loosely-quoted
|
|
111
74
|
* argument (`"Red`, `Red'`), leaving at least one character. This is the
|
|
@@ -46,7 +46,10 @@ export function defineInputMacro(
|
|
|
46
46
|
defineMacro({
|
|
47
47
|
name,
|
|
48
48
|
storeVar: true,
|
|
49
|
-
parameters: [
|
|
49
|
+
parameters: [
|
|
50
|
+
VARIABLE_PARAMETER,
|
|
51
|
+
{ name: 'placeholder', type: 'string', holds: 'text' },
|
|
52
|
+
],
|
|
50
53
|
render(_props, ctx) {
|
|
51
54
|
const placeholder = ctx.args.placeholder ?? '';
|
|
52
55
|
|