@rohal12/spindle 0.56.0 → 0.57.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.
@@ -16468,9 +16468,23 @@ var slice = (src, [start, end]) => src.slice(start, end);
16468
16468
  function readText(src) {
16469
16469
  return readWholeQuoted(src) ?? stripLooseQuotes(src);
16470
16470
  }
16471
+ /**
16472
+ * An argument that doesn't have its parameter's form: a `string` parameter
16473
+ * given anything but one quoted string.
16474
+ */
16475
+ var MacroArgumentError = class extends Error {
16476
+ constructor(param, src) {
16477
+ super(`The ${param.name} must be a quoted string ("…" or '…'), not ${src}`);
16478
+ this.name = "MacroArgumentError";
16479
+ }
16480
+ };
16471
16481
  function readValue(param, src) {
16472
16482
  switch (param.type) {
16473
- case "string": return readWholeQuoted(src) ?? void 0;
16483
+ case "string": {
16484
+ const value = readWholeQuoted(src);
16485
+ if (value === null) throw new MacroArgumentError(param, src);
16486
+ return value;
16487
+ }
16474
16488
  case "text": return readText(src);
16475
16489
  case "names": return src.split(",").map((name) => name.trim());
16476
16490
  case "delay": return parseDelay(src);
@@ -16592,8 +16606,16 @@ function readGroup(params, src, args) {
16592
16606
  let from = 0;
16593
16607
  let to = spans.length;
16594
16608
  for (const param of positional.slice(0, restIndex)) args[param.name] = from < to ? read(param, slice(text, spans[from++])) : unset(param);
16609
+ const readTrailing = (param, value) => {
16610
+ try {
16611
+ return read(param, value);
16612
+ } catch (error) {
16613
+ if (error instanceof MacroArgumentError) return void 0;
16614
+ throw error;
16615
+ }
16616
+ };
16595
16617
  for (const param of positional.slice(restIndex + 1).reverse()) {
16596
- const value = from < to && !endsWithOperator(text.slice(0, spans[to - 1][0])) ? read(param, slice(text, spans[to - 1])) : void 0;
16618
+ const value = from < to && !endsWithOperator(text.slice(0, spans[to - 1][0])) ? readTrailing(param, slice(text, spans[to - 1])) : void 0;
16597
16619
  args[param.name] = value ?? unset(param);
16598
16620
  if (value !== void 0) to--;
16599
16621
  }
@@ -16662,8 +16684,11 @@ function parseVarArgs(rawArgs) {
16662
16684
  /** The parameters of an `{option}` sub-macro: its value. */
16663
16685
  var OPTION_PARAMETERS = [{
16664
16686
  name: "value",
16665
- type: "string"
16687
+ type: "string",
16688
+ required: true
16666
16689
  }];
16690
+ /** The parameters of the built-in sub-macros that take arguments. */
16691
+ var SUB_MACRO_PARAMETERS = { option: OPTION_PARAMETERS };
16667
16692
  /**
16668
16693
  * Walk AST children to find {option} macro nodes, returning their values.
16669
16694
  */
@@ -16671,7 +16696,7 @@ function extractOptions(children) {
16671
16696
  const options = [];
16672
16697
  for (const node of children) if (node.type === "macro" && node.name === "option") {
16673
16698
  const { value } = parseMacroArgs(node.rawArgs, OPTION_PARAMETERS);
16674
- options.push(value ?? node.rawArgs.trim().replace(/^(["'])(.+)\1$/s, "$2"));
16699
+ options.push(value ?? "");
16675
16700
  }
16676
16701
  return options;
16677
16702
  }
@@ -16708,8 +16733,18 @@ function defineMacro(config, source = "builtin") {
16708
16733
  ...renderOptions,
16709
16734
  ...options
16710
16735
  });
16736
+ let args;
16737
+ try {
16738
+ args = parseMacroArgs(props.rawArgs, parameters);
16739
+ } catch (error) {
16740
+ if (error instanceof MacroArgumentError) return h(MacroError, {
16741
+ macro: config.name,
16742
+ error
16743
+ });
16744
+ throw error;
16745
+ }
16711
16746
  const ctx = {
16712
- args: parseMacroArgs(props.rawArgs, parameters),
16747
+ args,
16713
16748
  collectText,
16714
16749
  sourceLocation: currentSourceLocation,
16715
16750
  parseVarArgs,
@@ -16748,7 +16783,15 @@ function defineMacro(config, source = "builtin") {
16748
16783
  });
16749
16784
  };
16750
16785
  }
16751
- return config.render(props, ctx);
16786
+ try {
16787
+ return config.render(props, ctx);
16788
+ } catch (error) {
16789
+ if (error instanceof MacroArgumentError) return h(MacroError, {
16790
+ macro: config.name,
16791
+ error
16792
+ });
16793
+ throw error;
16794
+ }
16752
16795
  }
16753
16796
  registerMacro(config.name, Wrapper);
16754
16797
  const text = config.text;
@@ -17118,10 +17161,7 @@ var CODE_IN_STRINGS = { watch: {
17118
17161
  run: "statements"
17119
17162
  } };
17120
17163
  /** The `string` parameters of a macro that name a passage. */
17121
- var PASSAGE_STRINGS = {
17122
- link: ["passage"],
17123
- watch: ["goto", "dialog"]
17124
- };
17164
+ var PASSAGE_STRINGS = { watch: ["goto", "dialog"] };
17125
17165
  /** Block macros whose body is the name of a passage. */
17126
17166
  var PASSAGE_BODIES = new Set(["dialog"]);
17127
17167
  /** The goal of the code an argument of this type holds, if it is code. */
@@ -17225,7 +17265,19 @@ function* codeAndText(src, tokens, parametersOf) {
17225
17265
  }
17226
17266
  /** The code and text in the arguments `args` (at `offset`) of `macro`. */
17227
17267
  function* argPieces(args, offset, params, macro, label) {
17228
- const values = parseMacroArgs(args, params);
17268
+ let values;
17269
+ try {
17270
+ values = parseMacroArgs(args, params);
17271
+ } catch (error) {
17272
+ if (!(error instanceof MacroArgumentError)) throw error;
17273
+ yield {
17274
+ kind: "argument-error",
17275
+ message: error.message,
17276
+ offset,
17277
+ label
17278
+ };
17279
+ return;
17280
+ }
17229
17281
  const inStrings = CODE_IN_STRINGS[macro.toLowerCase()] ?? {};
17230
17282
  const passageStrings = PASSAGE_STRINGS[macro.toLowerCase()] ?? [];
17231
17283
  let cursor = 0;
@@ -17725,7 +17777,7 @@ function validateMarkup(passages, options) {
17725
17777
  report(passage, base + token.start, `${where}Unknown macro {${token.name}}.${didYouMean(token.name, names, (n) => `{${n}}`)}`);
17726
17778
  }
17727
17779
  };
17728
- const parametersOf = options.parametersOf ?? (() => void 0);
17780
+ const parametersOf = (name) => options.parametersOf?.(name) ?? SUB_MACRO_PARAMETERS[name.toLowerCase()];
17729
17781
  /**
17730
17782
  * Check `tokens`, the tokens of `src`, which starts at `base` in the
17731
17783
  * passage: their macros, the code in them and the markup in their text.
@@ -17734,6 +17786,10 @@ function validateMarkup(passages, options) {
17734
17786
  checkMacros(passage, tokens, base, where);
17735
17787
  for (const piece of codeAndText(src, tokens, parametersOf)) {
17736
17788
  const at = base + piece.offset;
17789
+ if (piece.kind === "argument-error") {
17790
+ report(passage, at, `${where}${piece.message} in ${piece.label}`);
17791
+ continue;
17792
+ }
17737
17793
  if (piece.kind === "passage") {
17738
17794
  if (options.checkPassageNames !== false && !passageNames.has(piece.name)) {
17739
17795
  const hint = didYouMean(piece.name, passageNames, JSON.stringify);
@@ -18403,18 +18459,24 @@ defineMacro({
18403
18459
  name: "link",
18404
18460
  block: true,
18405
18461
  interpolate: true,
18462
+ merged: true,
18406
18463
  parameters: [{
18407
18464
  name: "text",
18408
18465
  type: "string",
18409
18466
  required: true
18410
18467
  }, {
18411
18468
  name: "passage",
18412
- type: "string"
18469
+ type: "passage"
18413
18470
  }],
18414
- render({ rawArgs, children = [] }, ctx) {
18415
- const { text } = ctx.args;
18416
- const display = text ?? rawArgs.trim();
18417
- const passage = text === void 0 ? null : ctx.args.passage ?? null;
18471
+ render({ children = [] }, ctx) {
18472
+ const display = ctx.args.text ?? "";
18473
+ let passage = null;
18474
+ let failure;
18475
+ if (ctx.args.passage !== void 0) try {
18476
+ passage = evaluatePassageName(ctx.args.passage, ctx.evaluate, useStoryStore.getState());
18477
+ } catch (error) {
18478
+ failure = { error };
18479
+ }
18418
18480
  const runBody = useDetachedBody();
18419
18481
  const perform = () => {
18420
18482
  runBody(children);
@@ -18432,6 +18494,10 @@ defineMacro({
18432
18494
  target: passage ?? void 0,
18433
18495
  perform
18434
18496
  });
18497
+ if (failure) return /* @__PURE__ */ jsx(MacroError, {
18498
+ macro: "link",
18499
+ error: failure.error
18500
+ });
18435
18501
  return /* @__PURE__ */ jsx("a", {
18436
18502
  id: ctx.id,
18437
18503
  class: ctx.cls,
@@ -145,6 +145,7 @@
145
145
  "block": true,
146
146
  "subMacros": [],
147
147
  "interpolate": true,
148
+ "merged": true,
148
149
  "parameters": [
149
150
  {
150
151
  "name": "text",
@@ -153,7 +154,7 @@
153
154
  },
154
155
  {
155
156
  "name": "passage",
156
- "type": "string"
157
+ "type": "passage"
157
158
  }
158
159
  ],
159
160
  "source": "builtin"
@@ -9756,9 +9756,23 @@ var slice = (src, [start, end]) => src.slice(start, end);
9756
9756
  function readText(src) {
9757
9757
  return readWholeQuoted(src) ?? stripLooseQuotes(src);
9758
9758
  }
9759
+ /**
9760
+ * An argument that doesn't have its parameter's form: a `string` parameter
9761
+ * given anything but one quoted string.
9762
+ */
9763
+ var MacroArgumentError = class extends Error {
9764
+ constructor(param, src) {
9765
+ super(`The ${param.name} must be a quoted string ("…" or '…'), not ${src}`);
9766
+ this.name = "MacroArgumentError";
9767
+ }
9768
+ };
9759
9769
  function readValue(param, src) {
9760
9770
  switch (param.type) {
9761
- case "string": return readWholeQuoted(src) ?? void 0;
9771
+ case "string": {
9772
+ const value = readWholeQuoted(src);
9773
+ if (value === null) throw new MacroArgumentError(param, src);
9774
+ return value;
9775
+ }
9762
9776
  case "text": return readText(src);
9763
9777
  case "names": return src.split(",").map((name) => name.trim());
9764
9778
  case "delay": return parseDelay(src);
@@ -9880,8 +9894,16 @@ function readGroup(params, src, args) {
9880
9894
  let from = 0;
9881
9895
  let to = spans.length;
9882
9896
  for (const param of positional.slice(0, restIndex)) args[param.name] = from < to ? read(param, slice(text, spans[from++])) : unset(param);
9897
+ const readTrailing = (param, value) => {
9898
+ try {
9899
+ return read(param, value);
9900
+ } catch (error) {
9901
+ if (error instanceof MacroArgumentError) return void 0;
9902
+ throw error;
9903
+ }
9904
+ };
9883
9905
  for (const param of positional.slice(restIndex + 1).reverse()) {
9884
- const value = from < to && !endsWithOperator(text.slice(0, spans[to - 1][0])) ? read(param, slice(text, spans[to - 1])) : void 0;
9906
+ const value = from < to && !endsWithOperator(text.slice(0, spans[to - 1][0])) ? readTrailing(param, slice(text, spans[to - 1])) : void 0;
9885
9907
  args[param.name] = value ?? unset(param);
9886
9908
  if (value !== void 0) to--;
9887
9909
  }
@@ -9993,10 +10015,7 @@ var CODE_IN_STRINGS = { watch: {
9993
10015
  run: "statements"
9994
10016
  } };
9995
10017
  /** The `string` parameters of a macro that name a passage. */
9996
- var PASSAGE_STRINGS = {
9997
- link: ["passage"],
9998
- watch: ["goto", "dialog"]
9999
- };
10018
+ var PASSAGE_STRINGS = { watch: ["goto", "dialog"] };
10000
10019
  /** Block macros whose body is the name of a passage. */
10001
10020
  var PASSAGE_BODIES = new Set(["dialog"]);
10002
10021
  /** The goal of the code an argument of this type holds, if it is code. */
@@ -10100,7 +10119,19 @@ function* codeAndText(src, tokens, parametersOf) {
10100
10119
  }
10101
10120
  /** The code and text in the arguments `args` (at `offset`) of `macro`. */
10102
10121
  function* argPieces(args, offset, params, macro, label) {
10103
- const values = parseMacroArgs(args, params);
10122
+ let values;
10123
+ try {
10124
+ values = parseMacroArgs(args, params);
10125
+ } catch (error) {
10126
+ if (!(error instanceof MacroArgumentError)) throw error;
10127
+ yield {
10128
+ kind: "argument-error",
10129
+ message: error.message,
10130
+ offset,
10131
+ label
10132
+ };
10133
+ return;
10134
+ }
10104
10135
  const inStrings = CODE_IN_STRINGS[macro.toLowerCase()] ?? {};
10105
10136
  const passageStrings = PASSAGE_STRINGS[macro.toLowerCase()] ?? [];
10106
10137
  let cursor = 0;
@@ -10154,6 +10185,12 @@ function locate(text, part, from) {
10154
10185
  const anywhere = text.indexOf(part);
10155
10186
  return anywhere >= 0 ? anywhere : from;
10156
10187
  }
10188
+ /** The parameters of the built-in sub-macros that take arguments. */
10189
+ var SUB_MACRO_PARAMETERS = { option: [{
10190
+ name: "value",
10191
+ type: "string",
10192
+ required: true
10193
+ }] };
10157
10194
  //#endregion
10158
10195
  //#region src/markup/validate.ts
10159
10196
  /**
@@ -10275,7 +10312,7 @@ function validateMarkup(passages, options) {
10275
10312
  report(passage, base + token.start, `${where}Unknown macro {${token.name}}.${didYouMean(token.name, names, (n) => `{${n}}`)}`);
10276
10313
  }
10277
10314
  };
10278
- const parametersOf = options.parametersOf ?? (() => void 0);
10315
+ const parametersOf = (name) => options.parametersOf?.(name) ?? SUB_MACRO_PARAMETERS[name.toLowerCase()];
10279
10316
  /**
10280
10317
  * Check `tokens`, the tokens of `src`, which starts at `base` in the
10281
10318
  * passage: their macros, the code in them and the markup in their text.
@@ -10284,6 +10321,10 @@ function validateMarkup(passages, options) {
10284
10321
  checkMacros(passage, tokens, base, where);
10285
10322
  for (const piece of codeAndText(src, tokens, parametersOf)) {
10286
10323
  const at = base + piece.offset;
10324
+ if (piece.kind === "argument-error") {
10325
+ report(passage, at, `${where}${piece.message} in ${piece.label}`);
10326
+ continue;
10327
+ }
10287
10328
  if (piece.kind === "passage") {
10288
10329
  if (options.checkPassageNames !== false && !passageNames.has(piece.name)) {
10289
10330
  const hint = didYouMean(piece.name, passageNames, JSON.stringify);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rohal12/spindle",
3
- "version": "0.56.0",
3
+ "version": "0.57.0",
4
4
  "type": "module",
5
5
  "description": "A Preact-based story format for Twine 2.",
6
6
  "license": "Unlicense",
package/src/code-check.ts CHANGED
@@ -29,7 +29,10 @@ import { isCodeAttribute, splitSigilTemplate } from './markup/code-attributes';
29
29
  import { CodeSyntaxError, parseCode, type JsGoal } from './js-lexer';
30
30
  import type { ParsedCode } from './js-lexer';
31
31
  import type { ParameterDef } from './registry';
32
- import { parseMacroArgs } from './components/macros/macro-args';
32
+ import {
33
+ MacroArgumentError,
34
+ parseMacroArgs,
35
+ } from './components/macros/macro-args';
33
36
  import { readWholeQuoted } from './components/macros/arg-utils';
34
37
 
35
38
  /** While a pass runs: the code it parsed so far, by goal and source. */
@@ -88,6 +91,15 @@ export interface PassagePiece {
88
91
  label: string;
89
92
  }
90
93
 
94
+ /** Macro arguments that don't have their parameters' forms. */
95
+ export interface ArgumentErrorPiece {
96
+ kind: 'argument-error';
97
+ message: string;
98
+ offset: number;
99
+ /** The markup it is in, for the error: `{link Go}`. */
100
+ label: string;
101
+ }
102
+
91
103
  /** Text in markup that may hold markup of its own, at `offset` in it. */
92
104
  export interface TextPiece {
93
105
  kind: 'text';
@@ -107,14 +119,13 @@ const CODE_IN_STRINGS: Record<string, Record<string, JsGoal>> = {
107
119
 
108
120
  /** The `string` parameters of a macro that name a passage. */
109
121
  const PASSAGE_STRINGS: Record<string, readonly string[]> = {
110
- link: ['passage'],
111
122
  watch: ['goto', 'dialog'],
112
123
  };
113
124
 
114
125
  /** Block macros whose body is the name of a passage. */
115
126
  const PASSAGE_BODIES = new Set(['dialog']);
116
127
 
117
- type Piece = CodePiece | TextPiece | PassagePiece;
128
+ type Piece = CodePiece | TextPiece | PassagePiece | ArgumentErrorPiece;
118
129
 
119
130
  /** The goal of the code an argument of this type holds, if it is code. */
120
131
  function codeGoal(param: ParameterDef): JsGoal | undefined {
@@ -246,7 +257,14 @@ function* argPieces(
246
257
  macro: string,
247
258
  label: string,
248
259
  ): Generator<Piece> {
249
- const values = parseMacroArgs(args, params) as Record<string, unknown>;
260
+ let values: Record<string, unknown>;
261
+ try {
262
+ values = parseMacroArgs(args, params) as Record<string, unknown>;
263
+ } catch (error) {
264
+ if (!(error instanceof MacroArgumentError)) throw error;
265
+ yield { kind: 'argument-error', message: error.message, offset, label };
266
+ return;
267
+ }
250
268
  const inStrings = CODE_IN_STRINGS[macro.toLowerCase()] ?? {};
251
269
  const passageStrings = PASSAGE_STRINGS[macro.toLowerCase()] ?? [];
252
270
  let cursor = 0;
@@ -1,21 +1,36 @@
1
1
  import { useStoryStore } from '../../store';
2
2
  import { defineMacro } from '../../define-macro';
3
3
  import { useDetachedBody } from './detached-body';
4
+ import { evaluatePassageName } from './macro-args';
5
+ import { MacroError } from './MacroError';
4
6
 
5
7
  defineMacro({
6
8
  name: 'link',
7
9
  block: true,
8
10
  interpolate: true,
9
- // {link "text" "Passage"} or {link "text"}; unquoted, the whole arguments
10
- // are the text.
11
+ merged: true,
12
+ // {link "text" passage} or {link "text"}; the passage is a quoted name or
13
+ // an expression, as in {goto}.
11
14
  parameters: [
12
15
  { name: 'text', type: 'string', required: true },
13
- { name: 'passage', type: 'string' },
16
+ { name: 'passage', type: 'passage' },
14
17
  ],
15
- render({ rawArgs, children = [] }, ctx) {
16
- const { text } = ctx.args;
17
- const display = text ?? rawArgs.trim();
18
- const passage = text === undefined ? null : (ctx.args.passage ?? null);
18
+ render({ children = [] }, ctx) {
19
+ const display = ctx.args.text ?? '';
20
+ let passage: string | null = null;
21
+ // Boxed: anything can be thrown, including null and other falsy values
22
+ let failure: { error: unknown } | undefined;
23
+ if (ctx.args.passage !== undefined) {
24
+ try {
25
+ passage = evaluatePassageName(
26
+ ctx.args.passage,
27
+ ctx.evaluate!,
28
+ useStoryStore.getState(),
29
+ );
30
+ } catch (error) {
31
+ failure = { error };
32
+ }
33
+ }
19
34
  const runBody = useDetachedBody();
20
35
 
21
36
  const perform = () => {
@@ -39,6 +54,14 @@ defineMacro({
39
54
  perform,
40
55
  });
41
56
 
57
+ if (failure) {
58
+ return (
59
+ <MacroError
60
+ macro="link"
61
+ error={failure.error}
62
+ />
63
+ );
64
+ }
42
65
  return (
43
66
  <a
44
67
  id={ctx.id}
@@ -50,10 +50,24 @@ export function readText(src: string): string {
50
50
  return readWholeQuoted(src) ?? stripLooseQuotes(src);
51
51
  }
52
52
 
53
+ /**
54
+ * An argument that doesn't have its parameter's form: a `string` parameter
55
+ * given anything but one quoted string.
56
+ */
57
+ export class MacroArgumentError extends Error {
58
+ constructor(param: ParameterDef, src: string) {
59
+ super(`The ${param.name} must be a quoted string ("…" or '…'), not ${src}`);
60
+ this.name = 'MacroArgumentError';
61
+ }
62
+ }
63
+
53
64
  function readValue(param: ParameterDef, src: string): unknown {
54
65
  switch (param.type) {
55
- case 'string':
56
- return readWholeQuoted(src) ?? undefined;
66
+ case 'string': {
67
+ const value = readWholeQuoted(src);
68
+ if (value === null) throw new MacroArgumentError(param, src);
69
+ return value;
70
+ }
57
71
  case 'text':
58
72
  return readText(src);
59
73
  case 'names':
@@ -220,10 +234,20 @@ function readGroup(
220
234
  args[param.name] =
221
235
  from < to ? read(param, slice(text, spans[from++]!)) : unset(param);
222
236
  }
237
+ // Optional ones at the end: a term that doesn't have their form (a
238
+ // `string` that isn't quoted) belongs to the rest, as in {meter $hp 100}
239
+ const readTrailing = (param: ParameterDef, value: string) => {
240
+ try {
241
+ return read(param, value);
242
+ } catch (error) {
243
+ if (error instanceof MacroArgumentError) return undefined;
244
+ throw error;
245
+ }
246
+ };
223
247
  for (const param of positional.slice(restIndex + 1).reverse()) {
224
248
  const value =
225
249
  from < to && !endsWithOperator(text.slice(0, spans[to - 1]![0]))
226
- ? read(param, slice(text, spans[to - 1]!))
250
+ ? readTrailing(param, slice(text, spans[to - 1]!))
227
251
  : undefined;
228
252
  args[param.name] = value ?? unset(param);
229
253
  if (value !== undefined) to--;
@@ -1,5 +1,6 @@
1
1
  import type { ASTNode } from '../../markup/ast';
2
2
  import { parseMacroArgs } from './macro-args';
3
+ import type { ParameterDef } from '../../registry';
3
4
 
4
5
  /** `$var "placeholder"`, the arguments of the input macros. */
5
6
  export function parseVarArgs(rawArgs: string): {
@@ -14,7 +15,14 @@ export function parseVarArgs(rawArgs: string): {
14
15
  }
15
16
 
16
17
  /** The parameters of an `{option}` sub-macro: its value. */
17
- const OPTION_PARAMETERS = [{ name: 'value', type: 'string' }] as const;
18
+ const OPTION_PARAMETERS = [
19
+ { name: 'value', type: 'string', required: true },
20
+ ] as const;
21
+
22
+ /** The parameters of the built-in sub-macros that take arguments. */
23
+ export const SUB_MACRO_PARAMETERS: Record<string, readonly ParameterDef[]> = {
24
+ option: OPTION_PARAMETERS,
25
+ };
18
26
 
19
27
  /**
20
28
  * Walk AST children to find {option} macro nodes, returning their values.
@@ -23,12 +31,9 @@ export function extractOptions(children: ASTNode[]): string[] {
23
31
  const options: string[] = [];
24
32
  for (const node of children) {
25
33
  if (node.type === 'macro' && node.name === 'option') {
26
- // {option "Long Sword"} gives `Long Sword`. A value that isn't one
27
- // quoted string loses a matching pair of quotes around it, if any.
34
+ // {option "Long Sword"} gives `Long Sword`
28
35
  const { value } = parseMacroArgs(node.rawArgs, OPTION_PARAMETERS);
29
- options.push(
30
- value ?? node.rawArgs.trim().replace(/^(["'])(.+)\1$/s, '$2'),
31
- );
36
+ options.push(value ?? '');
32
37
  }
33
38
  }
34
39
  return options;
@@ -30,9 +30,11 @@ import { currentSourceLocation } from './utils/source-location';
30
30
  import { parseVarArgs, extractOptions } from './components/macros/option-utils';
31
31
  import { wrapContent } from './components/macros/display';
32
32
  import {
33
+ MacroArgumentError,
33
34
  parseMacroArgs,
34
35
  readBoundVariable,
35
36
  } from './components/macros/macro-args';
37
+ import { MacroError } from './components/macros/MacroError';
36
38
  import {
37
39
  checkParameterTypes,
38
40
  registerMacro,
@@ -162,8 +164,18 @@ export function defineMacro<const P extends readonly ParameterDef[] = []>(
162
164
  raw?: boolean;
163
165
  },
164
166
  ) => _renderNodes(nodes, { ...renderOptions, ...options });
167
+ let args: MacroArgs<P>;
168
+ try {
169
+ args = parseMacroArgs(props.rawArgs, parameters) as MacroArgs<P>;
170
+ } catch (error) {
171
+ // Shown in place, as other macro errors are
172
+ if (error instanceof MacroArgumentError) {
173
+ return h(MacroError, { macro: config.name, error });
174
+ }
175
+ throw error;
176
+ }
165
177
  const ctx: MacroContext<MacroArgs<P>> = {
166
- args: parseMacroArgs(props.rawArgs, parameters) as MacroArgs<P>,
178
+ args,
167
179
  collectText,
168
180
  sourceLocation: currentSourceLocation,
169
181
  parseVarArgs,
@@ -224,7 +236,15 @@ export function defineMacro<const P extends readonly ParameterDef[] = []>(
224
236
  };
225
237
  }
226
238
 
227
- return config.render(props, ctx);
239
+ try {
240
+ return config.render(props, ctx);
241
+ } catch (error) {
242
+ // An argument read while rendering, e.g. a sub-macro's ({option})
243
+ if (error instanceof MacroArgumentError) {
244
+ return h(MacroError, { macro: config.name, error });
245
+ }
246
+ throw error;
247
+ }
228
248
  }
229
249
 
230
250
  registerMacro(config.name, Wrapper);
@@ -11,6 +11,7 @@ import { parseWidgetDef } from '../widgets/widget-def';
11
11
  import { codeAndText, parseOrError, withParseCache } from '../code-check';
12
12
  import { CodeSyntaxError } from '../js-lexer';
13
13
  import type { ParameterDef } from '../registry';
14
+ import { SUB_MACRO_PARAMETERS } from '../components/macros/option-utils';
14
15
 
15
16
  /** A passage to validate. */
16
17
  export interface MarkupPassage {
@@ -220,7 +221,8 @@ export function validateMarkup(
220
221
  }
221
222
  };
222
223
 
223
- const parametersOf = options.parametersOf ?? (() => undefined);
224
+ const parametersOf = (name: string) =>
225
+ options.parametersOf?.(name) ?? SUB_MACRO_PARAMETERS[name.toLowerCase()];
224
226
 
225
227
  /**
226
228
  * Check `tokens`, the tokens of `src`, which starts at `base` in the
@@ -236,6 +238,10 @@ export function validateMarkup(
236
238
  checkMacros(passage, tokens, base, where);
237
239
  for (const piece of codeAndText(src, tokens, parametersOf)) {
238
240
  const at = base + piece.offset;
241
+ if (piece.kind === 'argument-error') {
242
+ report(passage, at, `${where}${piece.message} in ${piece.label}`);
243
+ continue;
244
+ }
239
245
  if (piece.kind === 'passage') {
240
246
  if (
241
247
  options.checkPassageNames !== false &&