@rohal12/spindle 0.54.0 → 0.55.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 +6295 -1088
- package/dist/pkg/macro-registry.json +4 -4
- package/dist/pkg/story-variables.js +6262 -1059
- package/dist/pkg/tooling.js +3 -1
- package/package.json +2 -1
- package/src/code-check.ts +314 -0
- package/src/components/macros/Goto.tsx +19 -8
- package/src/components/macros/Include.tsx +21 -8
- package/src/components/macros/Set.tsx +1 -1
- package/src/components/macros/macro-args.ts +16 -7
- package/src/define-macro.ts +2 -0
- package/src/expression.ts +25 -20
- package/src/index.tsx +9 -3
- package/src/js-lexer.ts +981 -1342
- package/src/markup/validate.ts +90 -28
- package/src/registry.ts +49 -14
- package/src/runtime-errors.ts +10 -0
- package/src/store.ts +8 -2
- package/src/story-variables.ts +32 -5
- package/src/tooling.ts +9 -1
- package/src/widgets/widget-def.ts +1 -1
- package/types/index.d.ts +7 -3
- package/types/tooling.d.ts +14 -6
package/src/markup/validate.ts
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Markup validation: parse every passage when the story starts and report
|
|
3
|
-
* malformed markup
|
|
4
|
-
*
|
|
5
|
-
*
|
|
3
|
+
* malformed markup, unknown macros, syntax errors in the code it runs and
|
|
4
|
+
* passage names that name no passage (see code-check.ts) with the passage,
|
|
5
|
+
* line and column they are at, instead of failing when the passage renders.
|
|
6
|
+
* Tooling runs it too (see src/tooling.ts).
|
|
6
7
|
*/
|
|
7
8
|
import { lineColumn, MarkupError, parseMarkup, tokenizeMarkup } from './parse';
|
|
8
9
|
import type { Token } from './tokens';
|
|
9
|
-
import { isCodeAttribute } from './code-attributes';
|
|
10
10
|
import { parseWidgetDef } from '../widgets/widget-def';
|
|
11
|
+
import { codeAndText, parseOrError, withParseCache } from '../code-check';
|
|
12
|
+
import { CodeSyntaxError } from '../js-lexer';
|
|
13
|
+
import type { ParameterDef } from '../registry';
|
|
11
14
|
|
|
12
15
|
/** A passage to validate. */
|
|
13
16
|
export interface MarkupPassage {
|
|
@@ -49,6 +52,13 @@ export interface MarkupValidationOptions {
|
|
|
49
52
|
* others are still read for the widgets they define.
|
|
50
53
|
*/
|
|
51
54
|
only?(passage: MarkupPassage): boolean;
|
|
55
|
+
/**
|
|
56
|
+
* The declared parameters of a macro, whose `expression` and `statements`
|
|
57
|
+
* arguments are checked as code and whose `text` and `string` arguments
|
|
58
|
+
* as markup (see code-check.ts). Without them only the code of `{$…}`,
|
|
59
|
+
* `{do}`, branch conditions and attributes is checked.
|
|
60
|
+
*/
|
|
61
|
+
parametersOf?(name: string): readonly ParameterDef[] | undefined;
|
|
52
62
|
}
|
|
53
63
|
|
|
54
64
|
/** Passages that hold no markup. */
|
|
@@ -83,8 +93,15 @@ function distance(a: string, b: string): number {
|
|
|
83
93
|
return prev[b.length]!;
|
|
84
94
|
}
|
|
85
95
|
|
|
86
|
-
/**
|
|
87
|
-
|
|
96
|
+
/**
|
|
97
|
+
* " Did you mean …?" with the known name closest to `name`, written by
|
|
98
|
+
* `quote`, if one is close.
|
|
99
|
+
*/
|
|
100
|
+
function didYouMean(
|
|
101
|
+
name: string,
|
|
102
|
+
names: Iterable<string>,
|
|
103
|
+
quote: (name: string) => string,
|
|
104
|
+
): string {
|
|
88
105
|
const lower = name.toLowerCase();
|
|
89
106
|
let best = '';
|
|
90
107
|
let bestDistance = Math.max(2, Math.floor(lower.length / 3)) + 1;
|
|
@@ -95,9 +112,16 @@ function suggestion(name: string, names: readonly string[]): string {
|
|
|
95
112
|
bestDistance = d;
|
|
96
113
|
}
|
|
97
114
|
}
|
|
98
|
-
return best ? ` Did you mean
|
|
115
|
+
return best ? ` Did you mean ${quote(best)}?` : '';
|
|
99
116
|
}
|
|
100
117
|
|
|
118
|
+
/** What a passage argument may be, after an error in one. */
|
|
119
|
+
const PASSAGE_ARGUMENT_HINT =
|
|
120
|
+
' (a passage name is a quoted string or an expression)';
|
|
121
|
+
|
|
122
|
+
/** A passage argument that is one bare word (`{goto Kitchen}`). */
|
|
123
|
+
const BARE_WORD_RE = /^[A-Za-z][\w$]*$/;
|
|
124
|
+
|
|
101
125
|
/** The name of the widget a `{widget}` with these arguments defines. */
|
|
102
126
|
function widgetName(rawArgs: string): string | undefined {
|
|
103
127
|
try {
|
|
@@ -107,7 +131,10 @@ function widgetName(rawArgs: string): string | undefined {
|
|
|
107
131
|
}
|
|
108
132
|
}
|
|
109
133
|
|
|
110
|
-
/**
|
|
134
|
+
/**
|
|
135
|
+
* Validate the markup of every passage. Passage names written out (links,
|
|
136
|
+
* quoted `passage` arguments) must name one of `passages`.
|
|
137
|
+
*/
|
|
111
138
|
export function validateMarkup(
|
|
112
139
|
passages: Iterable<MarkupPassage>,
|
|
113
140
|
options: MarkupValidationOptions,
|
|
@@ -136,7 +163,9 @@ export function validateMarkup(
|
|
|
136
163
|
// Parse every passage first: the widgets they define are known macros.
|
|
137
164
|
const parsed: [MarkupPassage, Token[]][] = [];
|
|
138
165
|
const widgets = new Set<string>();
|
|
166
|
+
const passageNames = new Set<string>();
|
|
139
167
|
for (const passage of passages) {
|
|
168
|
+
passageNames.add(passage.name);
|
|
140
169
|
if (NOT_MARKUP.has(passage.name)) continue;
|
|
141
170
|
if (passage.tags?.some((tag) => NOT_MARKUP_TAGS.includes(tag))) continue;
|
|
142
171
|
const reported = !options.only || options.only(passage);
|
|
@@ -181,39 +210,72 @@ export function validateMarkup(
|
|
|
181
210
|
report(
|
|
182
211
|
passage,
|
|
183
212
|
base + token.start,
|
|
184
|
-
`${where}Unknown macro {${token.name}}.${
|
|
213
|
+
`${where}Unknown macro {${token.name}}.${didYouMean(token.name, names, (n) => `{${n}}`)}`,
|
|
185
214
|
);
|
|
186
215
|
}
|
|
187
216
|
};
|
|
188
217
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
218
|
+
const parametersOf = options.parametersOf ?? (() => undefined);
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Check `tokens`, the tokens of `src`, which starts at `base` in the
|
|
222
|
+
* passage: their macros, the code in them and the markup in their text.
|
|
223
|
+
*/
|
|
224
|
+
const checkTokens = (
|
|
225
|
+
passage: MarkupPassage,
|
|
226
|
+
src: string,
|
|
227
|
+
tokens: Token[],
|
|
228
|
+
base: number,
|
|
229
|
+
where: string,
|
|
230
|
+
) => {
|
|
231
|
+
checkMacros(passage, tokens, base, where);
|
|
232
|
+
for (const piece of codeAndText(src, tokens, parametersOf)) {
|
|
233
|
+
const at = base + piece.offset;
|
|
234
|
+
if (piece.kind === 'passage') {
|
|
235
|
+
if (!passageNames.has(piece.name)) {
|
|
236
|
+
const hint = didYouMean(piece.name, passageNames, JSON.stringify);
|
|
237
|
+
report(
|
|
202
238
|
passage,
|
|
203
|
-
tokenizeMarkup(value, { text: true }),
|
|
204
239
|
at,
|
|
205
|
-
where
|
|
240
|
+
`${where}No passage named ${JSON.stringify(piece.name)} in ${piece.label}.${hint}`,
|
|
206
241
|
);
|
|
207
|
-
}
|
|
208
|
-
|
|
242
|
+
}
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
if (piece.kind === 'code') {
|
|
246
|
+
const result = parseOrError(piece.code, piece.goal);
|
|
247
|
+
const code = piece.code.trim();
|
|
248
|
+
if (result instanceof CodeSyntaxError) {
|
|
249
|
+
const reason = result.reasonIn(passage.content, at);
|
|
209
250
|
report(
|
|
210
251
|
passage,
|
|
211
|
-
|
|
212
|
-
where
|
|
252
|
+
at + result.pos,
|
|
253
|
+
`${where}${reason} in ${piece.label}` +
|
|
254
|
+
(piece.passage ? PASSAGE_ARGUMENT_HINT : ''),
|
|
255
|
+
);
|
|
256
|
+
} else if (piece.passage && BARE_WORD_RE.test(code)) {
|
|
257
|
+
report(
|
|
258
|
+
passage,
|
|
259
|
+
at,
|
|
260
|
+
`${where}Unquoted passage name in ${piece.label}: write ${JSON.stringify(code)}${PASSAGE_ARGUMENT_HINT}`,
|
|
213
261
|
);
|
|
214
262
|
}
|
|
263
|
+
continue;
|
|
264
|
+
}
|
|
265
|
+
if (!piece.text.includes('{')) continue;
|
|
266
|
+
try {
|
|
267
|
+
parseMarkup(piece.text, { text: true, hooks });
|
|
268
|
+
const inner = tokenizeMarkup(piece.text, { text: true });
|
|
269
|
+
checkTokens(passage, piece.text, inner, at, piece.where);
|
|
270
|
+
} catch (err) {
|
|
271
|
+
if (!(err instanceof MarkupError)) throw err;
|
|
272
|
+
report(passage, at + err.offset, piece.where + err.reason);
|
|
215
273
|
}
|
|
216
274
|
}
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
for (const [passage, tokens] of parsed) {
|
|
278
|
+
withParseCache(() => checkTokens(passage, passage.content, tokens, 0, ''));
|
|
217
279
|
}
|
|
218
280
|
return diagnostics;
|
|
219
281
|
}
|
package/src/registry.ts
CHANGED
|
@@ -69,9 +69,12 @@ export function isSubMacro(name: string): boolean {
|
|
|
69
69
|
}
|
|
70
70
|
|
|
71
71
|
/**
|
|
72
|
-
* How a macro argument is read (see components/macros/macro-args.ts).
|
|
72
|
+
* How a macro argument is read (see components/macros/macro-args.ts). Every
|
|
73
|
+
* declared parameter names one: there is no default.
|
|
73
74
|
* Quoted strings accept `\"`, `\'` and `\\` escapes.
|
|
74
|
-
* - `expression`: code, as written
|
|
75
|
+
* - `expression`: code, as written.
|
|
76
|
+
* - `statements`: code run as statements (`{set}`), as written.
|
|
77
|
+
* - `passage`: a passage name: a quoted string or an expression, as written.
|
|
75
78
|
* - `variable`: a variable reference such as `$name` or `"$name"`, as written.
|
|
76
79
|
* - `string`: one quoted string; anything else leaves the argument unset.
|
|
77
80
|
* - `text`: one quoted string, or text with any loose quotes stripped.
|
|
@@ -84,27 +87,59 @@ export function isSubMacro(name: string): boolean {
|
|
|
84
87
|
* - `options`: keywords, the names of its `parameters`, each followed by a
|
|
85
88
|
* quoted string or a number unless it is a flag.
|
|
86
89
|
*/
|
|
87
|
-
export
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
90
|
+
export const PARAMETER_TYPES = [
|
|
91
|
+
'expression',
|
|
92
|
+
'statements',
|
|
93
|
+
'passage',
|
|
94
|
+
'variable',
|
|
95
|
+
'string',
|
|
96
|
+
'text',
|
|
97
|
+
'names',
|
|
98
|
+
'delay',
|
|
99
|
+
'number',
|
|
100
|
+
'flag',
|
|
101
|
+
'separator',
|
|
102
|
+
'options',
|
|
103
|
+
] as const;
|
|
104
|
+
|
|
105
|
+
export type ParameterType = (typeof PARAMETER_TYPES)[number];
|
|
98
106
|
|
|
99
107
|
export interface ParameterDef {
|
|
100
108
|
name: string;
|
|
101
109
|
required?: boolean;
|
|
102
110
|
description?: string;
|
|
103
|
-
type
|
|
111
|
+
type: ParameterType;
|
|
104
112
|
/** The options of an `options` parameter. */
|
|
105
113
|
parameters?: readonly ParameterDef[];
|
|
106
114
|
}
|
|
107
115
|
|
|
116
|
+
/**
|
|
117
|
+
* Throw if a parameter `macro` declares (or an option of one) has no type,
|
|
118
|
+
* or one that isn't a ParameterType: arguments are read by their type, and
|
|
119
|
+
* there is no default.
|
|
120
|
+
*/
|
|
121
|
+
export function checkParameterTypes(
|
|
122
|
+
macro: string,
|
|
123
|
+
parameters: readonly ParameterDef[],
|
|
124
|
+
): void {
|
|
125
|
+
for (const param of parameters) {
|
|
126
|
+
const type: unknown = param.type;
|
|
127
|
+
if (!PARAMETER_TYPES.includes(type as ParameterType)) {
|
|
128
|
+
const problem =
|
|
129
|
+
type === undefined
|
|
130
|
+
? 'has no type'
|
|
131
|
+
: `has the unknown type ${JSON.stringify(type)}`;
|
|
132
|
+
throw new Error(
|
|
133
|
+
`spindle: The parameter "${param.name}" of the macro {${macro}} ${problem}. ` +
|
|
134
|
+
`Give it one of the types ${PARAMETER_TYPES.join(', ')} ` +
|
|
135
|
+
'(see docs/custom-macros.md#parameter-types), ' +
|
|
136
|
+
'or declare no parameters and read props.rawArgs.',
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
if (param.parameters) checkParameterTypes(macro, param.parameters);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
108
143
|
type ArgValue<T, D> = T extends 'flag' | 'separator'
|
|
109
144
|
? boolean
|
|
110
145
|
: T extends 'names'
|
package/src/runtime-errors.ts
CHANGED
|
@@ -41,6 +41,16 @@ export function showRuntimeError(context: string, error: unknown): void {
|
|
|
41
41
|
);
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
/**
|
|
45
|
+
* The error for `name`, a passage name that names no passage, used in the
|
|
46
|
+
* passage `from`.
|
|
47
|
+
*/
|
|
48
|
+
export function noPassageError(name: string, from: string): Error {
|
|
49
|
+
return new Error(
|
|
50
|
+
`No passage named ${JSON.stringify(name)} (in passage ${JSON.stringify(from)})`,
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
44
54
|
/** Remove the shown error `id`. */
|
|
45
55
|
export function dismissRuntimeError(id: number): void {
|
|
46
56
|
update(errors.filter((e) => e.id !== id));
|
package/src/store.ts
CHANGED
|
@@ -45,7 +45,7 @@ import {
|
|
|
45
45
|
|
|
46
46
|
import { deepClone, mergeKeys, mergesWith, shareEqual } from './structural';
|
|
47
47
|
import { shallowCopy } from './utils/object-path';
|
|
48
|
-
import { showRuntimeError } from './runtime-errors';
|
|
48
|
+
import { noPassageError, showRuntimeError } from './runtime-errors';
|
|
49
49
|
import {
|
|
50
50
|
snapshotPRNG,
|
|
51
51
|
restorePRNG,
|
|
@@ -278,6 +278,9 @@ function persistSession(get: () => StoryState): void {
|
|
|
278
278
|
const SESSION_ERROR_CONTEXT =
|
|
279
279
|
'The game could not be saved for a page reload; a reload goes back to the last passage it could save:';
|
|
280
280
|
|
|
281
|
+
/** What the page shows before an error naming a missing passage. */
|
|
282
|
+
const NAVIGATION_ERROR_CONTEXT = 'The story could not go to another passage:';
|
|
283
|
+
|
|
281
284
|
/**
|
|
282
285
|
* Write the session, then run `after` (the rest of the operation: its
|
|
283
286
|
* events and queued navigations), then throw the session's error if writing
|
|
@@ -1145,7 +1148,10 @@ export const useStoryStore = create<StoryState>()(
|
|
|
1145
1148
|
}
|
|
1146
1149
|
|
|
1147
1150
|
if (!storyData.passages.has(passageName)) {
|
|
1148
|
-
|
|
1151
|
+
// Shown on the page too: code (Story.goto()) can name any passage
|
|
1152
|
+
const error = noPassageError(passageName, get().currentPassage);
|
|
1153
|
+
console.error(`spindle: ${error.message}`);
|
|
1154
|
+
showRuntimeError(NAVIGATION_ERROR_CONTEXT, error);
|
|
1149
1155
|
return;
|
|
1150
1156
|
}
|
|
1151
1157
|
|
package/src/story-variables.ts
CHANGED
|
@@ -3,7 +3,13 @@ import type { Token } from './markup/tokens';
|
|
|
3
3
|
import { MarkupError, tokenizeMarkup } from './markup/parse';
|
|
4
4
|
import { isCodeAttribute, splitSigilTemplate } from './markup/code-attributes';
|
|
5
5
|
import { errorMessage } from './utils/error-message';
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
CodeSyntaxError,
|
|
8
|
+
lexJs,
|
|
9
|
+
scanStringLiteral,
|
|
10
|
+
type JsGoal,
|
|
11
|
+
} from './js-lexer';
|
|
12
|
+
import { parseOrError, withParseCache } from './code-check';
|
|
7
13
|
import { createNamespace, variableNameError } from './utils/namespace';
|
|
8
14
|
|
|
9
15
|
/**
|
|
@@ -245,6 +251,25 @@ function scanCode(
|
|
|
245
251
|
code: string,
|
|
246
252
|
onRef: RefCallback,
|
|
247
253
|
goal: JsGoal = 'expression',
|
|
254
|
+
): void {
|
|
255
|
+
const parsed = parseOrError(code, goal);
|
|
256
|
+
if (parsed instanceof CodeSyntaxError) {
|
|
257
|
+
scanCodeLeniently(code, onRef, goal);
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
for (const ref of parsed.refs) {
|
|
261
|
+
if (ref.sigil !== '$') continue;
|
|
262
|
+
VAR_PATH_RE.lastIndex = ref.start;
|
|
263
|
+
onRef(VAR_PATH_RE.exec(code)![1]!);
|
|
264
|
+
}
|
|
265
|
+
for (const text of parsed.strings) scanInterpolations(text, onRef);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/** `scanCode` for code acorn can't parse: lexed leniently. */
|
|
269
|
+
function scanCodeLeniently(
|
|
270
|
+
code: string,
|
|
271
|
+
onRef: RefCallback,
|
|
272
|
+
goal: JsGoal,
|
|
248
273
|
): void {
|
|
249
274
|
/** Open template literals: their text so far, null in an interpolation. */
|
|
250
275
|
const templates: { nesting: number; text: string | null }[] = [];
|
|
@@ -295,7 +320,9 @@ function collectPassageRefs(
|
|
|
295
320
|
storeVarMacros: ReadonlySet<string>,
|
|
296
321
|
onRef: RefCallback,
|
|
297
322
|
): void {
|
|
298
|
-
|
|
323
|
+
withParseCache(() =>
|
|
324
|
+
collectTokenRefs(content, tokensOf(content, false), storeVarMacros, onRef),
|
|
325
|
+
);
|
|
299
326
|
}
|
|
300
327
|
|
|
301
328
|
/** Report the `$var` references in the tokens of `content`. */
|
|
@@ -345,8 +372,9 @@ function collectTokenRefs(
|
|
|
345
372
|
}
|
|
346
373
|
|
|
347
374
|
/**
|
|
348
|
-
*
|
|
349
|
-
* Returns
|
|
375
|
+
* Check the `$var` references of all passages against the schema, at story
|
|
376
|
+
* start. Returns the error messages (empty = valid). (The syntax of their
|
|
377
|
+
* code is checked with their markup: see markup/validate.ts.)
|
|
350
378
|
*
|
|
351
379
|
* `storeVarMacros` lists the input macros whose first argument names a bound
|
|
352
380
|
* variable; it defaults to the built-in ones.
|
|
@@ -366,7 +394,6 @@ export function validatePassages(
|
|
|
366
394
|
if (name === 'StoryVariables' || name === 'StoryTransients') continue;
|
|
367
395
|
|
|
368
396
|
const forLocals = extractForLocals(passage.content);
|
|
369
|
-
|
|
370
397
|
collectPassageRefs(passage.content, storeVarSet, (ref) => {
|
|
371
398
|
const error = validateRef(ref, schema, forLocals);
|
|
372
399
|
if (error) {
|
package/src/tooling.ts
CHANGED
|
@@ -11,8 +11,10 @@ import {
|
|
|
11
11
|
} from './markup/validate';
|
|
12
12
|
import { isBlockMacro } from './markup/ast';
|
|
13
13
|
import { blockWidgetNames } from './widgets/widget-def';
|
|
14
|
+
import type { ParameterDef } from './registry';
|
|
14
15
|
|
|
15
16
|
export { parseStoryVariables } from './story-variables';
|
|
17
|
+
export { checkParameterTypes } from './registry';
|
|
16
18
|
export { formatDiagnostic } from './markup/validate';
|
|
17
19
|
export type { MarkupDiagnostic, MarkupPassage } from './markup/validate';
|
|
18
20
|
|
|
@@ -21,12 +23,15 @@ export interface ToolingMacro {
|
|
|
21
23
|
name: string;
|
|
22
24
|
block: boolean;
|
|
23
25
|
subMacros: string[];
|
|
26
|
+
/** Its declared parameters: their types tell which arguments are code. */
|
|
27
|
+
parameters?: readonly ParameterDef[];
|
|
24
28
|
}
|
|
25
29
|
|
|
26
30
|
/**
|
|
27
31
|
* Validate the markup of a story's passages as Spindle does when the story
|
|
28
32
|
* starts, against the given macros (built-in and user-defined): malformed
|
|
29
|
-
* markup and
|
|
33
|
+
* markup, unknown macros and syntax errors in code, with their passage, line
|
|
34
|
+
* and column.
|
|
30
35
|
*/
|
|
31
36
|
export function validateStoryMarkup(
|
|
32
37
|
passages: Iterable<MarkupPassage>,
|
|
@@ -34,16 +39,19 @@ export function validateStoryMarkup(
|
|
|
34
39
|
): MarkupDiagnostic[] {
|
|
35
40
|
const list = [...passages];
|
|
36
41
|
const known = new Set<string>();
|
|
42
|
+
const parameters = new Map<string, readonly ParameterDef[] | undefined>();
|
|
37
43
|
const blocks = new Set(blockWidgetNames(list).map((n) => n.toLowerCase()));
|
|
38
44
|
for (const macro of macros) {
|
|
39
45
|
const name = macro.name.toLowerCase();
|
|
40
46
|
known.add(name);
|
|
47
|
+
parameters.set(name, macro.parameters);
|
|
41
48
|
for (const sub of macro.subMacros) known.add(sub.toLowerCase());
|
|
42
49
|
if (macro.block) blocks.add(name);
|
|
43
50
|
}
|
|
44
51
|
return validateMarkup(list, {
|
|
45
52
|
isKnownMacro: (name) => known.has(name),
|
|
46
53
|
macroNames: known,
|
|
54
|
+
parametersOf: (name) => parameters.get(name),
|
|
47
55
|
isBlockMacro: (name) =>
|
|
48
56
|
blocks.has(name.toLowerCase()) || isBlockMacro(name),
|
|
49
57
|
});
|
|
@@ -8,7 +8,7 @@ import type { MacroArgs } from '../registry';
|
|
|
8
8
|
/** A {widget} definition's name, then its `@` parameters. */
|
|
9
9
|
export const WIDGET_PARAMETERS = [
|
|
10
10
|
{ name: 'name', type: 'text', required: true },
|
|
11
|
-
{ name: 'parameters', type: '
|
|
11
|
+
{ name: 'parameters', type: 'text' },
|
|
12
12
|
] as const;
|
|
13
13
|
|
|
14
14
|
export interface WidgetDef {
|
package/types/index.d.ts
CHANGED
|
@@ -242,7 +242,9 @@ export interface StorageQuota {
|
|
|
242
242
|
/**
|
|
243
243
|
* How a macro argument is read into `ctx.args`. Quoted strings accept `\"`,
|
|
244
244
|
* `\'` and `\\` escapes.
|
|
245
|
-
* - `expression`: code, as written
|
|
245
|
+
* - `expression`: code, as written.
|
|
246
|
+
* - `statements`: code run as statements (`{set}`), as written.
|
|
247
|
+
* - `passage`: a passage name: a quoted string or an expression, as written.
|
|
246
248
|
* - `variable`: a variable reference such as `$name` or `"$name"`, as written.
|
|
247
249
|
* - `string`: one quoted string; anything else leaves the argument unset.
|
|
248
250
|
* - `text`: one quoted string, or text with any loose quotes stripped.
|
|
@@ -258,6 +260,8 @@ export interface StorageQuota {
|
|
|
258
260
|
*/
|
|
259
261
|
export type ParameterType =
|
|
260
262
|
| 'expression'
|
|
263
|
+
| 'statements'
|
|
264
|
+
| 'passage'
|
|
261
265
|
| 'variable'
|
|
262
266
|
| 'string'
|
|
263
267
|
| 'text'
|
|
@@ -276,8 +280,8 @@ export interface ParameterDef {
|
|
|
276
280
|
name: string;
|
|
277
281
|
required?: boolean;
|
|
278
282
|
description?: string;
|
|
279
|
-
/** How the argument is read
|
|
280
|
-
type
|
|
283
|
+
/** How the argument is read: required, there is no default. */
|
|
284
|
+
type: ParameterType;
|
|
281
285
|
/** The options of an `options` parameter. */
|
|
282
286
|
parameters?: readonly ParameterDef[];
|
|
283
287
|
}
|
package/types/tooling.d.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* How a macro argument is read into `ctx.args`. Quoted strings accept `\"`,
|
|
3
3
|
* `\'` and `\\` escapes.
|
|
4
|
-
* - `expression`: code, as written
|
|
4
|
+
* - `expression`: code, as written.
|
|
5
|
+
* - `statements`: code run as statements (`{set}`), as written.
|
|
6
|
+
* - `passage`: a passage name: a quoted string or an expression, as written.
|
|
5
7
|
* - `variable`: a variable reference such as `$name` or `"$name"`, as written.
|
|
6
8
|
* - `string`: one quoted string; anything else leaves the argument unset.
|
|
7
9
|
* - `text`: one quoted string, or text with any loose quotes stripped.
|
|
@@ -16,6 +18,8 @@
|
|
|
16
18
|
*/
|
|
17
19
|
export type ParameterType =
|
|
18
20
|
| 'expression'
|
|
21
|
+
| 'statements'
|
|
22
|
+
| 'passage'
|
|
19
23
|
| 'variable'
|
|
20
24
|
| 'string'
|
|
21
25
|
| 'text'
|
|
@@ -30,8 +34,8 @@ export interface ParameterDef {
|
|
|
30
34
|
name: string;
|
|
31
35
|
required?: boolean;
|
|
32
36
|
description?: string;
|
|
33
|
-
/** How the argument is read
|
|
34
|
-
type
|
|
37
|
+
/** How the argument is read: required, there is no default. */
|
|
38
|
+
type: ParameterType;
|
|
35
39
|
/** The options of an `options` parameter. */
|
|
36
40
|
parameters?: readonly ParameterDef[];
|
|
37
41
|
}
|
|
@@ -128,9 +132,13 @@ export interface MarkupDiagnostic {
|
|
|
128
132
|
/**
|
|
129
133
|
* Validate the markup of a story's passages as Spindle does when the story
|
|
130
134
|
* starts: malformed markup (unclosed or mismatched macros, tags, links,
|
|
131
|
-
* braces and attribute values)
|
|
132
|
-
*
|
|
133
|
-
*
|
|
135
|
+
* braces and attribute values), unknown macros and syntax errors in the
|
|
136
|
+
* code passages run (`{$…}` expressions, `{do}` bodies, conditions and the
|
|
137
|
+
* `expression`/`statements`/`passage` arguments of macros), checked against
|
|
138
|
+
* the built-in macros, those registered with `defineMacro` and the widgets
|
|
139
|
+
* the passages define, and passage names that name none of `passages`
|
|
140
|
+
* (links, quoted `passage` arguments, `{link}`, `{dialog}`, `{watch}`).
|
|
141
|
+
* Spindle refuses to start a story with any of these.
|
|
134
142
|
*/
|
|
135
143
|
export declare function validateMarkup(
|
|
136
144
|
passages: Iterable<MarkupPassage>,
|