@rohal12/spindle 0.57.0 → 0.59.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/pkg/format.js +1 -1
- package/dist/pkg/headless.js +773 -493
- package/dist/pkg/macro-registry.json +29 -11
- package/dist/pkg/story-variables.js +804 -268
- package/dist/pkg/tooling.js +39 -2
- package/package.json +1 -1
- package/src/code-check.ts +215 -51
- 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/Include.tsx +2 -6
- package/src/components/macros/MacroLink.tsx +4 -1
- package/src/components/macros/Meter.tsx +1 -1
- package/src/components/macros/Radiobutton.tsx +2 -2
- package/src/components/macros/Watch.tsx +11 -6
- package/src/components/macros/include-args.ts +23 -0
- package/src/components/macros/input-macro.ts +4 -1
- package/src/components/macros/macro-args.ts +119 -46
- package/src/components/macros/option-utils.ts +10 -4
- package/src/expression.ts +4 -54
- package/src/index.tsx +2 -4
- package/src/js-lexer.ts +19 -0
- package/src/markup/grammar-shared.ts +8 -0
- package/src/markup/parse.ts +107 -2
- package/src/markup/spindle.d.peggy.ts +5 -2
- package/src/markup/spindle.peggy +27 -16
- package/src/markup/tokens.ts +3 -0
- package/src/markup/validate.ts +22 -12
- package/src/registry.ts +70 -6
- package/src/story-variables.ts +52 -10
- package/src/tooling.ts +63 -5
- package/src/transform.ts +65 -0
- package/src/widgets/widget-def.ts +2 -2
- package/types/index.d.ts +23 -0
- package/types/tooling.d.ts +337 -0
package/dist/pkg/tooling.js
CHANGED
|
@@ -2,9 +2,36 @@ import { readFileSync } from 'node:fs';
|
|
|
2
2
|
import { dirname, join } from 'node:path';
|
|
3
3
|
import { fileURLToPath } from 'node:url';
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
checkParameterTypes,
|
|
7
|
+
collectStoryPassageReferences,
|
|
8
|
+
validateStoryMarkup,
|
|
9
|
+
} from './story-variables.js';
|
|
6
10
|
|
|
7
|
-
export {
|
|
11
|
+
export {
|
|
12
|
+
parseStoryVariables,
|
|
13
|
+
formatDiagnostic,
|
|
14
|
+
findCodeEnd,
|
|
15
|
+
lexJs,
|
|
16
|
+
lexTemplate,
|
|
17
|
+
scanStringLiteral,
|
|
18
|
+
MarkupError,
|
|
19
|
+
parseSelectors,
|
|
20
|
+
tokenizeMarkup,
|
|
21
|
+
tokenizeMarkupTolerant,
|
|
22
|
+
transform,
|
|
23
|
+
passageTarget,
|
|
24
|
+
evaluatePassageName,
|
|
25
|
+
SIGIL_SCOPES,
|
|
26
|
+
isSigil,
|
|
27
|
+
endsWithOperator,
|
|
28
|
+
readQuoted,
|
|
29
|
+
splitArgs,
|
|
30
|
+
splitTopLevel,
|
|
31
|
+
stripLooseQuotes,
|
|
32
|
+
unescapeQuoted,
|
|
33
|
+
splitIncludeFlag,
|
|
34
|
+
} from './story-variables.js';
|
|
8
35
|
|
|
9
36
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
10
37
|
|
|
@@ -58,3 +85,13 @@ export function getMacroRegistry() {
|
|
|
58
85
|
export function validateMarkup(passages, options) {
|
|
59
86
|
return validateStoryMarkup(passages, metadata.values(), options);
|
|
60
87
|
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* The passages the markup of `source` names, with where each is written:
|
|
91
|
+
* `[[…]]` links and the passage of `{goto}`, `{include}`, `{link}`,
|
|
92
|
+
* `{watch}` and `{dialog}`, as the macros registered here declare them.
|
|
93
|
+
* Malformed tags are skipped, so half-typed markup reads.
|
|
94
|
+
*/
|
|
95
|
+
export function collectPassageReferences(source) {
|
|
96
|
+
return collectStoryPassageReferences(source, metadata.values());
|
|
97
|
+
}
|
package/package.json
CHANGED
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,12 +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,
|
|
37
|
+
passageTarget,
|
|
38
|
+
type ArgSpans,
|
|
39
|
+
type PassageTarget,
|
|
35
40
|
} from './components/macros/macro-args';
|
|
36
|
-
import {
|
|
41
|
+
import { subMacroParameters } from './components/macros/option-utils';
|
|
42
|
+
import { NameMap } from './utils/macro-names';
|
|
37
43
|
|
|
38
44
|
/** While a pass runs: the code it parsed so far, by goal and source. */
|
|
39
45
|
let parses: Map<string, ParsedCode | CodeSyntaxError> | null = null;
|
|
@@ -78,8 +84,15 @@ export interface CodePiece {
|
|
|
78
84
|
goal: JsGoal;
|
|
79
85
|
/** The markup it is in, for the error: `{print $a +}`. */
|
|
80
86
|
label: string;
|
|
81
|
-
/**
|
|
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
|
+
*/
|
|
82
91
|
passage?: boolean;
|
|
92
|
+
/** Whether it is the code in a quoted string, as in `{watch}`. */
|
|
93
|
+
inString?: boolean;
|
|
94
|
+
/** The macro it is the argument of, for a `passage` argument. */
|
|
95
|
+
macro?: string;
|
|
83
96
|
}
|
|
84
97
|
|
|
85
98
|
/** A passage name written out in markup, at `offset` in it. */
|
|
@@ -89,6 +102,10 @@ export interface PassagePiece {
|
|
|
89
102
|
offset: number;
|
|
90
103
|
/** The markup it is in, for the error: `[[Go->Hall]]`. */
|
|
91
104
|
label: string;
|
|
105
|
+
/** How much of the markup is the name, as written (quotes included). */
|
|
106
|
+
length: number;
|
|
107
|
+
/** The macro it is the argument of; `link` for `[[…]]` links. */
|
|
108
|
+
macro: string;
|
|
92
109
|
}
|
|
93
110
|
|
|
94
111
|
/** Macro arguments that don't have their parameters' forms. */
|
|
@@ -112,21 +129,79 @@ export interface TextPiece {
|
|
|
112
129
|
/** Macros whose whole argument text is one expression: branch conditions. */
|
|
113
130
|
const CONDITION_MACROS = new Set(['if', 'elseif', 'case']);
|
|
114
131
|
|
|
115
|
-
/** Code inside the quoted strings of a macro's arguments, by parameter. */
|
|
116
|
-
const CODE_IN_STRINGS: Record<string, Record<string, JsGoal>> = {
|
|
117
|
-
watch: { condition: 'expression', run: 'statements' },
|
|
118
|
-
};
|
|
119
|
-
|
|
120
|
-
/** The `string` parameters of a macro that name a passage. */
|
|
121
|
-
const PASSAGE_STRINGS: Record<string, readonly string[]> = {
|
|
122
|
-
watch: ['goto', 'dialog'],
|
|
123
|
-
};
|
|
124
|
-
|
|
125
132
|
/** Block macros whose body is the name of a passage. */
|
|
126
133
|
const PASSAGE_BODIES = new Set(['dialog']);
|
|
127
134
|
|
|
128
135
|
type Piece = CodePiece | TextPiece | PassagePiece | ArgumentErrorPiece;
|
|
129
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
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* `params` with each `string` and `text` parameter (options too) saying
|
|
151
|
+
* what it holds: what it declares, else `holds`.
|
|
152
|
+
*/
|
|
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
|
+
}
|
|
165
|
+
|
|
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
|
+
}
|
|
190
|
+
|
|
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
|
+
}
|
|
204
|
+
|
|
130
205
|
/** The goal of the code an argument of this type holds, if it is code. */
|
|
131
206
|
function codeGoal(param: ParameterDef): JsGoal | undefined {
|
|
132
207
|
if (param.type === 'expression' || param.type === 'passage') {
|
|
@@ -136,6 +211,15 @@ function codeGoal(param: ParameterDef): JsGoal | undefined {
|
|
|
136
211
|
return undefined;
|
|
137
212
|
}
|
|
138
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
|
+
|
|
139
223
|
/**
|
|
140
224
|
* The code and the markup-holding text in `tokens`, the tokens of `src`.
|
|
141
225
|
* `parametersOf` gives the declared parameters of a macro, if it has any.
|
|
@@ -143,7 +227,7 @@ function codeGoal(param: ParameterDef): JsGoal | undefined {
|
|
|
143
227
|
export function* codeAndText(
|
|
144
228
|
src: string,
|
|
145
229
|
tokens: readonly Token[],
|
|
146
|
-
parametersOf:
|
|
230
|
+
parametersOf: ParametersOf,
|
|
147
231
|
): Generator<Piece> {
|
|
148
232
|
/** The index of the closing tag of the `name` macro opened at `t`. */
|
|
149
233
|
const closeOf = (t: number, name: string) =>
|
|
@@ -160,7 +244,9 @@ export function* codeAndText(
|
|
|
160
244
|
yield {
|
|
161
245
|
kind: 'passage',
|
|
162
246
|
name: token.target,
|
|
163
|
-
offset:
|
|
247
|
+
offset: token.targetStart,
|
|
248
|
+
length: token.targetEnd - token.targetStart,
|
|
249
|
+
macro: 'link',
|
|
164
250
|
label: src.slice(token.start, token.end),
|
|
165
251
|
};
|
|
166
252
|
} else if (token.type === 'expression') {
|
|
@@ -172,9 +258,11 @@ export function* codeAndText(
|
|
|
172
258
|
label: `{${token.expression}}`,
|
|
173
259
|
};
|
|
174
260
|
} else if (token.type === 'html') {
|
|
261
|
+
// The values are the source text between their quotes, in order
|
|
262
|
+
let cursor = token.start;
|
|
175
263
|
for (const [name, value] of Object.entries(token.attributes)) {
|
|
176
|
-
|
|
177
|
-
|
|
264
|
+
const at = locate(src, value, cursor);
|
|
265
|
+
cursor = at + value.length;
|
|
178
266
|
if (!isCodeAttribute(name)) {
|
|
179
267
|
yield {
|
|
180
268
|
kind: 'text',
|
|
@@ -221,18 +309,22 @@ export function* codeAndText(
|
|
|
221
309
|
const body = tokens.slice(t + 1, close);
|
|
222
310
|
if (close > t + 1 && body.every((b) => b.type === 'text')) {
|
|
223
311
|
const text = src.slice(token.end, tokens[close]!.start);
|
|
224
|
-
const
|
|
312
|
+
const written = text.trim();
|
|
225
313
|
yield {
|
|
226
314
|
kind: 'passage',
|
|
227
|
-
name:
|
|
228
|
-
offset:
|
|
315
|
+
name: written.replace(/^["']|["']$/g, ''),
|
|
316
|
+
offset: token.end + text.indexOf(written),
|
|
317
|
+
length: written.length,
|
|
318
|
+
macro: name,
|
|
229
319
|
label: src.slice(token.start, token.end),
|
|
230
320
|
};
|
|
231
321
|
}
|
|
232
322
|
}
|
|
233
323
|
if (!args) continue;
|
|
234
324
|
const label = `{${token.name} ${args}}`;
|
|
235
|
-
|
|
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;
|
|
236
328
|
if (CONDITION_MACROS.has(name)) {
|
|
237
329
|
yield {
|
|
238
330
|
kind: 'code',
|
|
@@ -250,7 +342,7 @@ export function* codeAndText(
|
|
|
250
342
|
}
|
|
251
343
|
|
|
252
344
|
/** The code and text in the arguments `args` (at `offset`) of `macro`. */
|
|
253
|
-
function* argPieces(
|
|
345
|
+
export function* argPieces(
|
|
254
346
|
args: string,
|
|
255
347
|
offset: number,
|
|
256
348
|
params: readonly ParameterDef[],
|
|
@@ -258,16 +350,15 @@ function* argPieces(
|
|
|
258
350
|
label: string,
|
|
259
351
|
): Generator<Piece> {
|
|
260
352
|
let values: Record<string, unknown>;
|
|
353
|
+
const spans: ArgSpans = new Map();
|
|
261
354
|
try {
|
|
262
|
-
values = parseMacroArgs(args, params) as Record<string, unknown>;
|
|
355
|
+
values = parseMacroArgs(args, params, spans) as Record<string, unknown>;
|
|
263
356
|
} catch (error) {
|
|
264
357
|
if (!(error instanceof MacroArgumentError)) throw error;
|
|
265
358
|
yield { kind: 'argument-error', message: error.message, offset, label };
|
|
266
359
|
return;
|
|
267
360
|
}
|
|
268
|
-
const
|
|
269
|
-
const passageStrings = PASSAGE_STRINGS[macro.toLowerCase()] ?? [];
|
|
270
|
-
let cursor = 0;
|
|
361
|
+
const name = macro.toLowerCase();
|
|
271
362
|
/** The pieces of the parameters `list`, with their values in `from`. */
|
|
272
363
|
function* visit(
|
|
273
364
|
list: readonly ParameterDef[],
|
|
@@ -282,36 +373,63 @@ function* argPieces(
|
|
|
282
373
|
);
|
|
283
374
|
continue;
|
|
284
375
|
}
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
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
|
+
}
|
|
289
398
|
if (goal) {
|
|
290
399
|
const piece: CodePiece = {
|
|
291
400
|
kind: 'code',
|
|
292
401
|
code: value,
|
|
293
|
-
offset: offset +
|
|
402
|
+
offset: offset + start,
|
|
294
403
|
goal,
|
|
295
404
|
label,
|
|
296
405
|
};
|
|
297
|
-
if (param.type === 'passage')
|
|
406
|
+
if (param.type === 'passage') {
|
|
407
|
+
piece.passage = true;
|
|
408
|
+
piece.macro = name;
|
|
409
|
+
}
|
|
298
410
|
yield piece;
|
|
411
|
+
continue;
|
|
299
412
|
}
|
|
300
|
-
//
|
|
301
|
-
const
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
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') {
|
|
418
|
+
yield {
|
|
419
|
+
kind: 'code',
|
|
420
|
+
code: value,
|
|
421
|
+
offset: at,
|
|
422
|
+
goal: holds,
|
|
423
|
+
label,
|
|
424
|
+
inString: true,
|
|
425
|
+
};
|
|
426
|
+
} else if (holds === 'passage') {
|
|
427
|
+
yield passage(value);
|
|
428
|
+
} else if (holds === 'markup') {
|
|
311
429
|
yield {
|
|
312
430
|
kind: 'text',
|
|
313
431
|
text: value,
|
|
314
|
-
offset:
|
|
432
|
+
offset: at,
|
|
315
433
|
where: `In the ${param.name} of {${macro}}: `,
|
|
316
434
|
};
|
|
317
435
|
}
|
|
@@ -330,3 +448,49 @@ function locate(text: string, part: string, from: number): number {
|
|
|
330
448
|
const anywhere = text.indexOf(part);
|
|
331
449
|
return anywhere >= 0 ? anywhere : from;
|
|
332
450
|
}
|
|
451
|
+
|
|
452
|
+
/** A passage a piece of markup names, and where. */
|
|
453
|
+
export interface PassageReference {
|
|
454
|
+
/** The macro it is the argument of; `link` for `[[…]]` links. */
|
|
455
|
+
macro: string;
|
|
456
|
+
/** The name written out, or the expression whose value is the name. */
|
|
457
|
+
target: PassageTarget;
|
|
458
|
+
/** Where the name is written, as it is written (quotes included). */
|
|
459
|
+
start: number;
|
|
460
|
+
end: number;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/**
|
|
464
|
+
* The passages the markup `src` (with the `tokens` it tokenizes to) names:
|
|
465
|
+
* `[[…]]` links, the passage argument of `{goto}`, `{include}` and `{link}`
|
|
466
|
+
* (and of macros that declare one), the `goto` and `dialog` actions of
|
|
467
|
+
* `{watch}` (and the `string` and `text` arguments of macros that declare
|
|
468
|
+
* they hold one) and the body of `{dialog}`, in source order. A name written
|
|
469
|
+
* out is a literal; the others are expressions, which name a passage when
|
|
470
|
+
* they run.
|
|
471
|
+
*/
|
|
472
|
+
export function collectPassageReferences(
|
|
473
|
+
src: string,
|
|
474
|
+
tokens: readonly Token[],
|
|
475
|
+
parametersOf: ParametersOf,
|
|
476
|
+
): PassageReference[] {
|
|
477
|
+
const refs: PassageReference[] = [];
|
|
478
|
+
for (const piece of codeAndText(src, tokens, parametersOf)) {
|
|
479
|
+
if (piece.kind === 'passage') {
|
|
480
|
+
refs.push({
|
|
481
|
+
macro: piece.macro,
|
|
482
|
+
target: { kind: 'name', name: piece.name },
|
|
483
|
+
start: piece.offset,
|
|
484
|
+
end: piece.offset + piece.length,
|
|
485
|
+
});
|
|
486
|
+
} else if (piece.kind === 'code' && piece.passage) {
|
|
487
|
+
refs.push({
|
|
488
|
+
macro: piece.macro!,
|
|
489
|
+
target: { kind: 'expression', expression: piece.code },
|
|
490
|
+
start: piece.offset,
|
|
491
|
+
end: piece.offset + piece.code.length,
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
return refs;
|
|
496
|
+
}
|
|
@@ -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
|
|
|
@@ -4,18 +4,14 @@ import { parseMarkup } from '../../markup/parse';
|
|
|
4
4
|
import { NobrContext } from '../../markup/render';
|
|
5
5
|
import { defineMacro } from '../../define-macro';
|
|
6
6
|
import { evaluatePassageName } from './macro-args';
|
|
7
|
+
import { INCLUDE_PARAMETERS } from './include-args';
|
|
7
8
|
import { MacroError } from './MacroError';
|
|
8
9
|
|
|
9
10
|
defineMacro({
|
|
10
11
|
name: 'include',
|
|
11
12
|
interpolate: true,
|
|
12
13
|
merged: true,
|
|
13
|
-
|
|
14
|
-
// passage names and expressions containing the word stay intact (#201).
|
|
15
|
-
parameters: [
|
|
16
|
-
{ name: 'inline', type: 'flag' },
|
|
17
|
-
{ name: 'passage', type: 'passage', required: true },
|
|
18
|
-
],
|
|
14
|
+
parameters: INCLUDE_PARAMETERS,
|
|
19
15
|
render(_props, ctx) {
|
|
20
16
|
const { storyData } = useStoryFields('storyData');
|
|
21
17
|
|
|
@@ -12,7 +12,7 @@ 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) {
|
|
@@ -34,6 +34,8 @@ defineMacro({
|
|
|
34
34
|
const runBody = useDetachedBody();
|
|
35
35
|
|
|
36
36
|
const perform = () => {
|
|
37
|
+
// A link that failed to resolve is an error, not a control
|
|
38
|
+
if (failure) return;
|
|
37
39
|
runBody(children);
|
|
38
40
|
if (passage) {
|
|
39
41
|
useStoryStore.getState().navigate(passage);
|
|
@@ -51,6 +53,7 @@ defineMacro({
|
|
|
51
53
|
authorId: ctx.id,
|
|
52
54
|
label: display,
|
|
53
55
|
target: passage ?? undefined,
|
|
56
|
+
disabled: failure ? true : undefined,
|
|
54
57
|
perform,
|
|
55
58
|
});
|
|
56
59
|
|
|
@@ -40,7 +40,7 @@ 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 {
|
|
@@ -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
|
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { parseMacroArgs } from './macro-args';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The parameters of `{include}`. A standalone `inline` flag only counts
|
|
5
|
+
* outside quotes and brackets, so passage names and expressions containing
|
|
6
|
+
* the word stay intact (#201).
|
|
7
|
+
*/
|
|
8
|
+
export const INCLUDE_PARAMETERS = [
|
|
9
|
+
{ name: 'inline', type: 'flag' },
|
|
10
|
+
{ name: 'passage', type: 'passage', required: true },
|
|
11
|
+
] as const;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* The arguments of `{include}`: whether it has the `inline` flag, as its
|
|
15
|
+
* first or last word, and the passage expression that is left.
|
|
16
|
+
*/
|
|
17
|
+
export function splitIncludeFlag(rawArgs: string): {
|
|
18
|
+
inline: boolean;
|
|
19
|
+
passage: string | undefined;
|
|
20
|
+
} {
|
|
21
|
+
const { inline, passage } = parseMacroArgs(rawArgs, INCLUDE_PARAMETERS);
|
|
22
|
+
return { inline, passage };
|
|
23
|
+
}
|
|
@@ -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
|
|