@rohal12/spindle 0.54.0 → 0.56.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.
@@ -1,13 +1,16 @@
1
1
  /**
2
2
  * Markup validation: parse every passage when the story starts and report
3
- * malformed markup and unknown macros with the passage, line and column
4
- * they are at, instead of failing when the passage renders. Tooling runs it
5
- * too (see src/tooling.ts).
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,18 @@ export interface MarkupValidationOptions {
49
52
  * others are still read for the widgets they define.
50
53
  */
51
54
  only?(passage: MarkupPassage): boolean;
55
+ /**
56
+ * Whether passage names written out must name one of the passages
57
+ * (default: true). Off when only part of a story is validated.
58
+ */
59
+ checkPassageNames?: boolean;
60
+ /**
61
+ * The declared parameters of a macro, whose `expression` and `statements`
62
+ * arguments are checked as code and whose `text` and `string` arguments
63
+ * as markup (see code-check.ts). Without them only the code of `{$…}`,
64
+ * `{do}`, branch conditions and attributes is checked.
65
+ */
66
+ parametersOf?(name: string): readonly ParameterDef[] | undefined;
52
67
  }
53
68
 
54
69
  /** Passages that hold no markup. */
@@ -83,8 +98,15 @@ function distance(a: string, b: string): number {
83
98
  return prev[b.length]!;
84
99
  }
85
100
 
86
- /** " Did you mean {name}?" for the closest known name, if one is close. */
87
- function suggestion(name: string, names: readonly string[]): string {
101
+ /**
102
+ * " Did you mean …?" with the known name closest to `name`, written by
103
+ * `quote`, if one is close.
104
+ */
105
+ function didYouMean(
106
+ name: string,
107
+ names: Iterable<string>,
108
+ quote: (name: string) => string,
109
+ ): string {
88
110
  const lower = name.toLowerCase();
89
111
  let best = '';
90
112
  let bestDistance = Math.max(2, Math.floor(lower.length / 3)) + 1;
@@ -95,9 +117,16 @@ function suggestion(name: string, names: readonly string[]): string {
95
117
  bestDistance = d;
96
118
  }
97
119
  }
98
- return best ? ` Did you mean {${best}}?` : '';
120
+ return best ? ` Did you mean ${quote(best)}?` : '';
99
121
  }
100
122
 
123
+ /** What a passage argument may be, after an error in one. */
124
+ const PASSAGE_ARGUMENT_HINT =
125
+ ' (a passage name is a quoted string or an expression)';
126
+
127
+ /** A passage argument that is one bare word (`{goto Kitchen}`). */
128
+ const BARE_WORD_RE = /^[A-Za-z][\w$]*$/;
129
+
101
130
  /** The name of the widget a `{widget}` with these arguments defines. */
102
131
  function widgetName(rawArgs: string): string | undefined {
103
132
  try {
@@ -107,7 +136,10 @@ function widgetName(rawArgs: string): string | undefined {
107
136
  }
108
137
  }
109
138
 
110
- /** Validate the markup of every passage. */
139
+ /**
140
+ * Validate the markup of every passage. Passage names written out (links,
141
+ * quoted `passage` arguments) must name one of `passages`.
142
+ */
111
143
  export function validateMarkup(
112
144
  passages: Iterable<MarkupPassage>,
113
145
  options: MarkupValidationOptions,
@@ -136,7 +168,9 @@ export function validateMarkup(
136
168
  // Parse every passage first: the widgets they define are known macros.
137
169
  const parsed: [MarkupPassage, Token[]][] = [];
138
170
  const widgets = new Set<string>();
171
+ const passageNames = new Set<string>();
139
172
  for (const passage of passages) {
173
+ passageNames.add(passage.name);
140
174
  if (NOT_MARKUP.has(passage.name)) continue;
141
175
  if (passage.tags?.some((tag) => NOT_MARKUP_TAGS.includes(tag))) continue;
142
176
  const reported = !options.only || options.only(passage);
@@ -181,39 +215,75 @@ export function validateMarkup(
181
215
  report(
182
216
  passage,
183
217
  base + token.start,
184
- `${where}Unknown macro {${token.name}}.${suggestion(token.name, names)}`,
218
+ `${where}Unknown macro {${token.name}}.${didYouMean(token.name, names, (n) => `{${n}}`)}`,
185
219
  );
186
220
  }
187
221
  };
188
222
 
189
- for (const [passage, tokens] of parsed) {
190
- checkMacros(passage, tokens, 0, '');
191
- for (const token of tokens) {
192
- if (token.type !== 'html') continue;
193
- for (const [attr, value] of Object.entries(token.attributes)) {
194
- if (isCodeAttribute(attr) || !value.includes('{')) continue;
195
- // The value is the source text between its quotes
196
- const found = passage.content.indexOf(value, token.start);
197
- const at = found === -1 ? token.start : found;
198
- const where = `In the ${attr} attribute of <${token.tag}>: `;
199
- try {
200
- parseMarkup(value, { text: true, hooks });
201
- checkMacros(
223
+ const parametersOf = options.parametersOf ?? (() => undefined);
224
+
225
+ /**
226
+ * Check `tokens`, the tokens of `src`, which starts at `base` in the
227
+ * passage: their macros, the code in them and the markup in their text.
228
+ */
229
+ const checkTokens = (
230
+ passage: MarkupPassage,
231
+ src: string,
232
+ tokens: Token[],
233
+ base: number,
234
+ where: string,
235
+ ) => {
236
+ checkMacros(passage, tokens, base, where);
237
+ for (const piece of codeAndText(src, tokens, parametersOf)) {
238
+ const at = base + piece.offset;
239
+ if (piece.kind === 'passage') {
240
+ if (
241
+ options.checkPassageNames !== false &&
242
+ !passageNames.has(piece.name)
243
+ ) {
244
+ const hint = didYouMean(piece.name, passageNames, JSON.stringify);
245
+ report(
202
246
  passage,
203
- tokenizeMarkup(value, { text: true }),
204
247
  at,
205
- where,
248
+ `${where}No passage named ${JSON.stringify(piece.name)} in ${piece.label}.${hint}`,
249
+ );
250
+ }
251
+ continue;
252
+ }
253
+ if (piece.kind === 'code') {
254
+ const result = parseOrError(piece.code, piece.goal);
255
+ const code = piece.code.trim();
256
+ if (result instanceof CodeSyntaxError) {
257
+ const reason = result.reasonIn(passage.content, at);
258
+ report(
259
+ passage,
260
+ at + result.pos,
261
+ `${where}${reason} in ${piece.label}` +
262
+ (piece.passage ? PASSAGE_ARGUMENT_HINT : ''),
206
263
  );
207
- } catch (err) {
208
- if (!(err instanceof MarkupError)) throw err;
264
+ } else if (piece.passage && BARE_WORD_RE.test(code)) {
209
265
  report(
210
266
  passage,
211
- found === -1 ? at : at + err.offset,
212
- where + err.reason,
267
+ at,
268
+ `${where}Unquoted passage name in ${piece.label}: write ${JSON.stringify(code)}${PASSAGE_ARGUMENT_HINT}`,
213
269
  );
214
270
  }
271
+ continue;
272
+ }
273
+ if (!piece.text.includes('{')) continue;
274
+ try {
275
+ parseMarkup(piece.text, { text: true, hooks });
276
+ const inner = tokenizeMarkup(piece.text, { text: true });
277
+ checkTokens(passage, piece.text, inner, at, piece.where);
278
+ } catch (err) {
279
+ if (!(err instanceof MarkupError)) throw err;
280
+ report(passage, at + err.offset, piece.where + err.reason);
215
281
  }
216
282
  }
283
+ };
284
+
285
+ for (const [passage, tokens] of parsed) {
286
+ withParseCache(() => checkTokens(passage, passage.content, tokens, 0, ''));
217
287
  }
218
288
  return diagnostics;
219
289
  }
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 (the default).
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 type ParameterType =
88
- | 'expression'
89
- | 'variable'
90
- | 'string'
91
- | 'text'
92
- | 'names'
93
- | 'delay'
94
- | 'number'
95
- | 'flag'
96
- | 'separator'
97
- | 'options';
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?: ParameterType;
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'
@@ -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
- console.error(`spindle: Passage "${passageName}" not found.`);
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
 
@@ -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 { lexJs, scanStringLiteral, type JsGoal } from './js-lexer';
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
- collectTokenRefs(content, tokensOf(content, false), storeVarMacros, onRef);
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
- * Scan all passages for $var references, check against schema.
349
- * Returns list of error messages (empty = valid).
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,40 +11,60 @@ 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
 
19
21
  /** What tooling knows about a macro (see MacroMetadata). */
22
+ /** Options for validating markup from tooling. */
23
+ export interface ValidateMarkupOptions {
24
+ /**
25
+ * Whether passage names written out (links, quoted `passage` arguments)
26
+ * must name one of the passages given (default: true). Turn it off to
27
+ * validate only part of a story.
28
+ */
29
+ checkPassageNames?: boolean;
30
+ }
31
+
20
32
  export interface ToolingMacro {
21
33
  name: string;
22
34
  block: boolean;
23
35
  subMacros: string[];
36
+ /** Its declared parameters: their types tell which arguments are code. */
37
+ parameters?: readonly ParameterDef[];
24
38
  }
25
39
 
26
40
  /**
27
41
  * Validate the markup of a story's passages as Spindle does when the story
28
42
  * starts, against the given macros (built-in and user-defined): malformed
29
- * markup and unknown macros, with their passage, line and column.
43
+ * markup, unknown macros and syntax errors in code, with their passage, line
44
+ * and column.
30
45
  */
31
46
  export function validateStoryMarkup(
32
47
  passages: Iterable<MarkupPassage>,
33
48
  macros: Iterable<ToolingMacro>,
49
+ options: ValidateMarkupOptions = {},
34
50
  ): MarkupDiagnostic[] {
35
51
  const list = [...passages];
36
52
  const known = new Set<string>();
53
+ const parameters = new Map<string, readonly ParameterDef[] | undefined>();
37
54
  const blocks = new Set(blockWidgetNames(list).map((n) => n.toLowerCase()));
38
55
  for (const macro of macros) {
39
56
  const name = macro.name.toLowerCase();
40
57
  known.add(name);
58
+ parameters.set(name, macro.parameters);
41
59
  for (const sub of macro.subMacros) known.add(sub.toLowerCase());
42
60
  if (macro.block) blocks.add(name);
43
61
  }
44
62
  return validateMarkup(list, {
45
63
  isKnownMacro: (name) => known.has(name),
46
64
  macroNames: known,
65
+ parametersOf: (name) => parameters.get(name),
47
66
  isBlockMacro: (name) =>
48
67
  blocks.has(name.toLowerCase()) || isBlockMacro(name),
68
+ checkPassageNames: options.checkPassageNames,
49
69
  });
50
70
  }
@@ -115,5 +115,5 @@ const _exampleMacro: PublishedMacroDefinition = {
115
115
  // metadata.
116
116
  declare const validateStoryMarkup: typeof SourceValidateStoryMarkup;
117
117
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
118
- const _validateMarkup: typeof PublishedValidateMarkup = (passages) =>
119
- validateStoryMarkup(passages, []);
118
+ const _validateMarkup: typeof PublishedValidateMarkup = (passages, options) =>
119
+ validateStoryMarkup(passages, [], options);
@@ -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: 'expression' },
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 (the default).
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 (default `expression`). */
280
- type?: ParameterType;
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
  }
@@ -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 (the default).
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 (default `expression`). */
34
- type?: ParameterType;
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,14 +132,29 @@ 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) and unknown macros, checked against the
132
- * built-in macros, those registered with `defineMacro` and the widgets the
133
- * passages define. Spindle refuses to start a story with any of these.
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>,
145
+ options?: ValidateMarkupOptions,
137
146
  ): MarkupDiagnostic[];
138
147
 
148
+ /** Options for {@link validateMarkup}. */
149
+ export interface ValidateMarkupOptions {
150
+ /**
151
+ * Whether passage names written out (links, quoted `passage` arguments)
152
+ * must name one of `passages` (default: true). Turn it off to validate
153
+ * only part of a story.
154
+ */
155
+ checkPassageNames?: boolean;
156
+ }
157
+
139
158
  /**
140
159
  * A diagnostic as one line of text, as Spindle shows it:
141
160
  * `Passage "Start", line 3, column 5 (story.twee:12): Unclosed {if}: …`.