@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.
- package/dist/pkg/format.js +1 -1
- package/dist/pkg/headless.js +83 -17
- package/dist/pkg/macro-registry.json +2 -1
- package/dist/pkg/story-variables.js +49 -8
- package/package.json +1 -1
- package/src/code-check.ts +22 -4
- package/src/components/macros/MacroLink.tsx +30 -7
- package/src/components/macros/macro-args.ts +27 -3
- package/src/components/macros/option-utils.ts +11 -6
- package/src/define-macro.ts +22 -2
- package/src/markup/validate.ts +7 -1
package/dist/pkg/headless.js
CHANGED
|
@@ -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":
|
|
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])) ?
|
|
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 ??
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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 ?? (
|
|
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: "
|
|
18469
|
+
type: "passage"
|
|
18413
18470
|
}],
|
|
18414
|
-
render({
|
|
18415
|
-
const
|
|
18416
|
-
|
|
18417
|
-
|
|
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": "
|
|
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":
|
|
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])) ?
|
|
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
|
-
|
|
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 ?? (
|
|
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
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 {
|
|
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
|
-
|
|
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
|
-
|
|
10
|
-
//
|
|
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: '
|
|
16
|
+
{ name: 'passage', type: 'passage' },
|
|
14
17
|
],
|
|
15
|
-
render({
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
|
|
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
|
-
?
|
|
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 = [
|
|
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
|
|
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;
|
package/src/define-macro.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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);
|
package/src/markup/validate.ts
CHANGED
|
@@ -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 =
|
|
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 &&
|