@rohal12/spindle 0.59.17 → 0.59.18
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 +7218 -7070
- package/dist/pkg/story-variables.js +221 -147
- package/package.json +1 -1
- package/src/components/StoryInterface.tsx +17 -6
- package/src/components/macros/Watch.tsx +11 -0
- package/src/markup/ast.ts +2 -0
- package/src/markup/render.tsx +9 -1
- package/src/markup/spindle.peggy +6 -4
- package/src/saves/format.ts +3 -1
- package/src/saves/types.ts +5 -0
- package/src/store.ts +25 -2
- package/src/structural.ts +1 -3
- package/src/tooling.ts +5 -2
- package/src/triggers.ts +48 -2
- package/src/utils/stable-key.ts +57 -3
- package/src/utils/value-kinds.ts +3 -0
- package/src/widgets/ast-scanner.ts +76 -6
- package/src/widgets/widget-def.ts +8 -4
|
@@ -151,7 +151,11 @@ function lexeme(lex, loc, fields) {
|
|
|
151
151
|
/** The AST node of a lexeme that stands alone (not a macro or element). */
|
|
152
152
|
function nodeOf(x) {
|
|
153
153
|
switch (x.lex) {
|
|
154
|
-
case "text": return {
|
|
154
|
+
case "text": return x.comment ? {
|
|
155
|
+
type: "text",
|
|
156
|
+
value: x.value,
|
|
157
|
+
comment: true
|
|
158
|
+
} : {
|
|
155
159
|
type: "text",
|
|
156
160
|
value: x.value
|
|
157
161
|
};
|
|
@@ -223,7 +227,7 @@ function tokenOf(x) {
|
|
|
223
227
|
}
|
|
224
228
|
/**
|
|
225
229
|
* Flatten items (lexemes, nodes, arrays of them, nulls) and join adjacent
|
|
226
|
-
* text, except escaped braces, which stay a text item of their own.
|
|
230
|
+
* text, except escaped braces and comments, which stay a text item of their own.
|
|
227
231
|
*/
|
|
228
232
|
function joinText(items, convert) {
|
|
229
233
|
const out = [];
|
|
@@ -231,7 +235,7 @@ function joinText(items, convert) {
|
|
|
231
235
|
const add = (item) => {
|
|
232
236
|
if (item === null || item === void 0) return;
|
|
233
237
|
if (Array.isArray(item)) return item.forEach(add);
|
|
234
|
-
if (item.lex === "text" && !item.escape && last && last.lex === "text" && !last.escape && last.end === item.start) {
|
|
238
|
+
if (item.lex === "text" && !item.escape && !item.comment && last && last.lex === "text" && !last.escape && !last.comment && last.end === item.start) {
|
|
235
239
|
last.value += item.value;
|
|
236
240
|
last.end = item.end;
|
|
237
241
|
return;
|
|
@@ -725,7 +729,9 @@ function peg$parse(input, options) {
|
|
|
725
729
|
return lexeme("text", location(), { value });
|
|
726
730
|
}
|
|
727
731
|
function peg$f41(value) {
|
|
728
|
-
|
|
732
|
+
const node = lexeme("text", location(), { value });
|
|
733
|
+
node.comment = true;
|
|
734
|
+
return node;
|
|
729
735
|
}
|
|
730
736
|
function peg$f42(bs) {
|
|
731
737
|
return bs.length % 2 === 1;
|
|
@@ -10586,70 +10592,6 @@ function evaluatePassageName(expr, evaluate, { storyData, currentPassage }) {
|
|
|
10586
10592
|
if (storyData && !storyData.passages.has(name)) throw noPassageError(name, currentPassage);
|
|
10587
10593
|
return name;
|
|
10588
10594
|
}
|
|
10589
|
-
//#endregion
|
|
10590
|
-
//#region src/widgets/widget-def.ts
|
|
10591
|
-
/**
|
|
10592
|
-
* Reading widget definitions without rendering anything: the `{widget}`
|
|
10593
|
-
* macro, startup and markup validation (also in tooling) share these.
|
|
10594
|
-
*/
|
|
10595
|
-
/** A {widget} definition's name, then its `@` parameters. */
|
|
10596
|
-
var WIDGET_PARAMETERS = [{
|
|
10597
|
-
name: "name",
|
|
10598
|
-
type: "text",
|
|
10599
|
-
holds: "text",
|
|
10600
|
-
required: true
|
|
10601
|
-
}, {
|
|
10602
|
-
name: "parameters",
|
|
10603
|
-
type: "text",
|
|
10604
|
-
holds: "text"
|
|
10605
|
-
}];
|
|
10606
|
-
/**
|
|
10607
|
-
* The widget a {widget} definition's arguments declare: its name and its
|
|
10608
|
-
* parameters, the words after it that start with `@` (docs/widgets.md).
|
|
10609
|
-
* Other words, such as `$name`, are not parameters.
|
|
10610
|
-
*/
|
|
10611
|
-
function widgetDef({ name = "", parameters = "" }) {
|
|
10612
|
-
return {
|
|
10613
|
-
name,
|
|
10614
|
-
params: parameters.split(/\s+/).filter((word) => word.startsWith("@"))
|
|
10615
|
-
};
|
|
10616
|
-
}
|
|
10617
|
-
/** Read the arguments of a {widget} definition (see widgetDef). */
|
|
10618
|
-
function parseWidgetDef(rawArgs) {
|
|
10619
|
-
return widgetDef(parseMacroArgs(rawArgs, WIDGET_PARAMETERS));
|
|
10620
|
-
}
|
|
10621
|
-
/**
|
|
10622
|
-
* The names of the block widgets (those whose body renders `{@children}`)
|
|
10623
|
-
* that StoryInit and the passages tagged `widget` define. They must be
|
|
10624
|
-
* known as block macros before any passage is parsed, so that passages
|
|
10625
|
-
* invoking them nest their content whatever the passage order.
|
|
10626
|
-
*
|
|
10627
|
-
* The passages are read as flat tokens, which need no block macros to be
|
|
10628
|
-
* known, so a `{@children}` in an HTML comment or in a `{do}` body is text
|
|
10629
|
-
* here as it is to the parser, and does not make a block widget (#387).
|
|
10630
|
-
*/
|
|
10631
|
-
function blockWidgetNames(passages) {
|
|
10632
|
-
const names = [];
|
|
10633
|
-
for (const passage of passages) {
|
|
10634
|
-
if (passage.name !== "StoryInit" && !passage.tags?.includes("widget")) continue;
|
|
10635
|
-
const open = [];
|
|
10636
|
-
for (const token of tokenizeMarkupTolerant(passage.content).tokens) if (token.type === "macro" && token.name.toLowerCase() === "widget") {
|
|
10637
|
-
if (!token.isClose) {
|
|
10638
|
-
open.push({
|
|
10639
|
-
rawArgs: token.rawArgs,
|
|
10640
|
-
isBlock: false
|
|
10641
|
-
});
|
|
10642
|
-
continue;
|
|
10643
|
-
}
|
|
10644
|
-
const def = open.pop();
|
|
10645
|
-
if (!def?.isBlock) continue;
|
|
10646
|
-
try {
|
|
10647
|
-
names.push(parseWidgetDef(def.rawArgs).name);
|
|
10648
|
-
} catch {}
|
|
10649
|
-
} else if (token.type === "variable" && token.scope === "local" && token.name === "children" && open.length > 0) open[open.length - 1].isBlock = true;
|
|
10650
|
-
}
|
|
10651
|
-
return names;
|
|
10652
|
-
}
|
|
10653
10595
|
/** The parameters of the built-in sub-macros that take arguments. */
|
|
10654
10596
|
var SUB_MACRO_PARAMETERS = new Map([["option", [{
|
|
10655
10597
|
name: "value",
|
|
@@ -10983,6 +10925,214 @@ function collectPassageReferences(src, tokens, parametersOf) {
|
|
|
10983
10925
|
});
|
|
10984
10926
|
return refs;
|
|
10985
10927
|
}
|
|
10928
|
+
new NameMap();
|
|
10929
|
+
new NameMap();
|
|
10930
|
+
new NameSet();
|
|
10931
|
+
/**
|
|
10932
|
+
* How a macro argument is read (see components/macros/macro-args.ts). Every
|
|
10933
|
+
* declared parameter names one: there is no default.
|
|
10934
|
+
* Quoted strings accept `\"`, `\'` and `\\` escapes.
|
|
10935
|
+
* - `expression`: code, as written.
|
|
10936
|
+
* - `statements`: code run as statements (`{set}`), as written.
|
|
10937
|
+
* - `passage`: a passage name: a quoted string or an expression, as written.
|
|
10938
|
+
* - `variable`: a variable reference such as `$name` or `"$name"`, as written.
|
|
10939
|
+
* - `string`: one quoted string; anything else leaves the argument unset.
|
|
10940
|
+
* - `text`: one quoted string, or text with any loose quotes stripped.
|
|
10941
|
+
* - `names`: a comma-separated list of names (`@item, @i`).
|
|
10942
|
+
* - `delay`: a duration (`2s`, `500ms`, `300`) in milliseconds.
|
|
10943
|
+
* - `number`: a number.
|
|
10944
|
+
* - `flag`: a keyword, the parameter's name, at the start or end; a boolean.
|
|
10945
|
+
* - `separator`: a word (`of`) or `=` separating the parameters before it
|
|
10946
|
+
* from those after it; a boolean.
|
|
10947
|
+
* - `options`: keywords, the names of its `parameters`, each followed by a
|
|
10948
|
+
* quoted string or a number unless it is a flag.
|
|
10949
|
+
*/
|
|
10950
|
+
var PARAMETER_TYPES = [
|
|
10951
|
+
"expression",
|
|
10952
|
+
"statements",
|
|
10953
|
+
"passage",
|
|
10954
|
+
"variable",
|
|
10955
|
+
"string",
|
|
10956
|
+
"text",
|
|
10957
|
+
"names",
|
|
10958
|
+
"delay",
|
|
10959
|
+
"number",
|
|
10960
|
+
"flag",
|
|
10961
|
+
"separator",
|
|
10962
|
+
"options"
|
|
10963
|
+
];
|
|
10964
|
+
/**
|
|
10965
|
+
* What the value of a `string` or `text` argument holds, for the check at
|
|
10966
|
+
* story start and for tooling (see code-check.ts):
|
|
10967
|
+
* - `markup`: markup the macro renders (`{button}`'s label): its markup
|
|
10968
|
+
* is checked.
|
|
10969
|
+
* - `text`: plain text the macro uses as written (`{checkbox}`'s label).
|
|
10970
|
+
* - `passage`: a passage name (`{watch}`'s `goto`): the passage must exist.
|
|
10971
|
+
* - `expression`, `statements`: code (`{watch}`'s condition and `run`): it
|
|
10972
|
+
* is checked as code, and its variable references against the schema.
|
|
10973
|
+
*
|
|
10974
|
+
* Without it, the argument of a macro with `interpolate` holds `markup`,
|
|
10975
|
+
* any other `text`.
|
|
10976
|
+
*/
|
|
10977
|
+
var STRING_HOLDS = [
|
|
10978
|
+
"markup",
|
|
10979
|
+
"text",
|
|
10980
|
+
"passage",
|
|
10981
|
+
"expression",
|
|
10982
|
+
"statements"
|
|
10983
|
+
];
|
|
10984
|
+
/**
|
|
10985
|
+
* Throw if a parameter `macro` declares (or an option of one) has no type,
|
|
10986
|
+
* or one that isn't a ParameterType: arguments are read by their type, and
|
|
10987
|
+
* there is no default. Throw too if it declares what it `holds` wrongly.
|
|
10988
|
+
*/
|
|
10989
|
+
function checkParameterTypes(macro, parameters) {
|
|
10990
|
+
const holdsFix = `Give a \`string\` or \`text\` parameter one of ${STRING_HOLDS.join(", ")} to hold (see docs/custom-macros.md#what-a-string-holds).`;
|
|
10991
|
+
for (const param of parameters) {
|
|
10992
|
+
const type = param.type;
|
|
10993
|
+
if (!PARAMETER_TYPES.includes(type)) throw parameterError(macro, param, type === void 0 ? "has no type" : `has the unknown type ${JSON.stringify(type)}`, `Give it one of the types ${PARAMETER_TYPES.join(", ")} (see docs/custom-macros.md#parameter-types), or declare no parameters and read props.rawArgs.`);
|
|
10994
|
+
const holds = param.holds;
|
|
10995
|
+
if (holds !== void 0 && type !== "string" && type !== "text") throw parameterError(macro, param, `is of the type ${type}, which holds what it is`, holdsFix);
|
|
10996
|
+
if (holds !== void 0 && !STRING_HOLDS.includes(holds)) throw parameterError(macro, param, `holds the unknown ${JSON.stringify(holds)}`, holdsFix);
|
|
10997
|
+
if (param.parameters) checkParameterTypes(macro, param.parameters);
|
|
10998
|
+
}
|
|
10999
|
+
}
|
|
11000
|
+
/** The error for the parameter `param` of `macro`: what is wrong, and the fix. */
|
|
11001
|
+
function parameterError(macro, param, problem, fix) {
|
|
11002
|
+
return /* @__PURE__ */ new Error(`spindle: The parameter "${param.name}" of the macro {${macro}} ${problem}. ${fix}`);
|
|
11003
|
+
}
|
|
11004
|
+
var metadataRegistry = new NameMap();
|
|
11005
|
+
function getMacroRegistry() {
|
|
11006
|
+
return Array.from(metadataRegistry.values());
|
|
11007
|
+
}
|
|
11008
|
+
//#endregion
|
|
11009
|
+
//#region src/widgets/ast-scanner.ts
|
|
11010
|
+
/** The declared parameters of the macros registered so far. */
|
|
11011
|
+
var registeredParameters = (name) => parameterLookup(getMacroRegistry())(name);
|
|
11012
|
+
/**
|
|
11013
|
+
* Whether the text of an attribute value or macro label renders a
|
|
11014
|
+
* `{@children}`: it is markup, parsed as the renderer parses it.
|
|
11015
|
+
*/
|
|
11016
|
+
function textContainsChildren(text, parametersOf) {
|
|
11017
|
+
if (!text.includes("{")) return false;
|
|
11018
|
+
let nodes;
|
|
11019
|
+
try {
|
|
11020
|
+
nodes = parseMarkup(text, { text: true });
|
|
11021
|
+
} catch {
|
|
11022
|
+
return false;
|
|
11023
|
+
}
|
|
11024
|
+
return astContainsChildren(nodes, parametersOf);
|
|
11025
|
+
}
|
|
11026
|
+
/**
|
|
11027
|
+
* Whether a tag renders a `{@children}` in an attribute value or a macro
|
|
11028
|
+
* argument that holds markup (`<span title="{@children}">`,
|
|
11029
|
+
* `{button "{@children}"}`). Literal arguments and code are not markup.
|
|
11030
|
+
*/
|
|
11031
|
+
function tokenTextContainsChildren(token, parametersOf = registeredParameters) {
|
|
11032
|
+
for (const piece of codeAndText("", [token], parametersOf)) if (piece.kind === "text" && textContainsChildren(piece.text, parametersOf)) return true;
|
|
11033
|
+
return false;
|
|
11034
|
+
}
|
|
11035
|
+
/**
|
|
11036
|
+
* Recursively scan an AST node array for a VariableNode
|
|
11037
|
+
* with scope 'local' and name 'children' (@children), in the content and in
|
|
11038
|
+
* the attributes and labels that render markup.
|
|
11039
|
+
*/
|
|
11040
|
+
function astContainsChildren(nodes, parametersOf = registeredParameters) {
|
|
11041
|
+
for (const node of nodes) {
|
|
11042
|
+
if (node.type === "variable" && node.scope === "local" && node.name === "children") return true;
|
|
11043
|
+
if (node.type === "html") {
|
|
11044
|
+
const span = {
|
|
11045
|
+
start: 0,
|
|
11046
|
+
end: 0
|
|
11047
|
+
};
|
|
11048
|
+
if (tokenTextContainsChildren({
|
|
11049
|
+
isClose: false,
|
|
11050
|
+
isSelfClose: false,
|
|
11051
|
+
...node,
|
|
11052
|
+
...span
|
|
11053
|
+
}, parametersOf) || astContainsChildren(node.children, parametersOf)) return true;
|
|
11054
|
+
}
|
|
11055
|
+
if (node.type === "macro") {
|
|
11056
|
+
const span = {
|
|
11057
|
+
start: 0,
|
|
11058
|
+
end: 0
|
|
11059
|
+
};
|
|
11060
|
+
if (tokenTextContainsChildren({
|
|
11061
|
+
isClose: false,
|
|
11062
|
+
...node,
|
|
11063
|
+
...span
|
|
11064
|
+
}, parametersOf) || astContainsChildren(node.children, parametersOf)) return true;
|
|
11065
|
+
if (node.branches) {
|
|
11066
|
+
for (const branch of node.branches) if (astContainsChildren(branch.children, parametersOf)) return true;
|
|
11067
|
+
}
|
|
11068
|
+
}
|
|
11069
|
+
}
|
|
11070
|
+
return false;
|
|
11071
|
+
}
|
|
11072
|
+
//#endregion
|
|
11073
|
+
//#region src/widgets/widget-def.ts
|
|
11074
|
+
/**
|
|
11075
|
+
* Reading widget definitions without rendering anything: the `{widget}`
|
|
11076
|
+
* macro, startup and markup validation (also in tooling) share these.
|
|
11077
|
+
*/
|
|
11078
|
+
/** A {widget} definition's name, then its `@` parameters. */
|
|
11079
|
+
var WIDGET_PARAMETERS = [{
|
|
11080
|
+
name: "name",
|
|
11081
|
+
type: "text",
|
|
11082
|
+
holds: "text",
|
|
11083
|
+
required: true
|
|
11084
|
+
}, {
|
|
11085
|
+
name: "parameters",
|
|
11086
|
+
type: "text",
|
|
11087
|
+
holds: "text"
|
|
11088
|
+
}];
|
|
11089
|
+
/**
|
|
11090
|
+
* The widget a {widget} definition's arguments declare: its name and its
|
|
11091
|
+
* parameters, the words after it that start with `@` (docs/widgets.md).
|
|
11092
|
+
* Other words, such as `$name`, are not parameters.
|
|
11093
|
+
*/
|
|
11094
|
+
function widgetDef({ name = "", parameters = "" }) {
|
|
11095
|
+
return {
|
|
11096
|
+
name,
|
|
11097
|
+
params: parameters.split(/\s+/).filter((word) => word.startsWith("@"))
|
|
11098
|
+
};
|
|
11099
|
+
}
|
|
11100
|
+
/** Read the arguments of a {widget} definition (see widgetDef). */
|
|
11101
|
+
function parseWidgetDef(rawArgs) {
|
|
11102
|
+
return widgetDef(parseMacroArgs(rawArgs, WIDGET_PARAMETERS));
|
|
11103
|
+
}
|
|
11104
|
+
/**
|
|
11105
|
+
* The names of the block widgets (those whose body renders `{@children}`)
|
|
11106
|
+
* that StoryInit and the passages tagged `widget` define. They must be
|
|
11107
|
+
* known as block macros before any passage is parsed, so that passages
|
|
11108
|
+
* invoking them nest their content whatever the passage order.
|
|
11109
|
+
*
|
|
11110
|
+
* The passages are read as flat tokens, which need no block macros to be
|
|
11111
|
+
* known, so a `{@children}` in an HTML comment or in a `{do}` body is text
|
|
11112
|
+
* here as it is to the parser, and does not make a block widget (#387).
|
|
11113
|
+
*/
|
|
11114
|
+
function blockWidgetNames(passages, parametersOf = registeredParameters) {
|
|
11115
|
+
const names = [];
|
|
11116
|
+
for (const passage of passages) {
|
|
11117
|
+
if (passage.name !== "StoryInit" && !passage.tags?.includes("widget")) continue;
|
|
11118
|
+
const open = [];
|
|
11119
|
+
for (const token of tokenizeMarkupTolerant(passage.content).tokens) if (token.type === "macro" && token.name.toLowerCase() === "widget") {
|
|
11120
|
+
if (!token.isClose) {
|
|
11121
|
+
open.push({
|
|
11122
|
+
rawArgs: token.rawArgs,
|
|
11123
|
+
isBlock: false
|
|
11124
|
+
});
|
|
11125
|
+
continue;
|
|
11126
|
+
}
|
|
11127
|
+
const def = open.pop();
|
|
11128
|
+
if (!def?.isBlock) continue;
|
|
11129
|
+
try {
|
|
11130
|
+
names.push(parseWidgetDef(def.rawArgs).name);
|
|
11131
|
+
} catch {}
|
|
11132
|
+
} else if (open.length > 0 && (token.type === "variable" && token.scope === "local" && token.name === "children" || tokenTextContainsChildren(token, parametersOf))) open[open.length - 1].isBlock = true;
|
|
11133
|
+
}
|
|
11134
|
+
return names;
|
|
11135
|
+
}
|
|
10986
11136
|
//#endregion
|
|
10987
11137
|
//#region src/markup/validate.ts
|
|
10988
11138
|
/**
|
|
@@ -11157,83 +11307,6 @@ function validateMarkup(passages, options) {
|
|
|
11157
11307
|
for (const [passage, tokens] of parsed) withParseCache(() => checkTokens(passage, passage.content, tokens, 0, ""));
|
|
11158
11308
|
return diagnostics;
|
|
11159
11309
|
}
|
|
11160
|
-
new NameMap();
|
|
11161
|
-
new NameMap();
|
|
11162
|
-
new NameSet();
|
|
11163
|
-
/**
|
|
11164
|
-
* How a macro argument is read (see components/macros/macro-args.ts). Every
|
|
11165
|
-
* declared parameter names one: there is no default.
|
|
11166
|
-
* Quoted strings accept `\"`, `\'` and `\\` escapes.
|
|
11167
|
-
* - `expression`: code, as written.
|
|
11168
|
-
* - `statements`: code run as statements (`{set}`), as written.
|
|
11169
|
-
* - `passage`: a passage name: a quoted string or an expression, as written.
|
|
11170
|
-
* - `variable`: a variable reference such as `$name` or `"$name"`, as written.
|
|
11171
|
-
* - `string`: one quoted string; anything else leaves the argument unset.
|
|
11172
|
-
* - `text`: one quoted string, or text with any loose quotes stripped.
|
|
11173
|
-
* - `names`: a comma-separated list of names (`@item, @i`).
|
|
11174
|
-
* - `delay`: a duration (`2s`, `500ms`, `300`) in milliseconds.
|
|
11175
|
-
* - `number`: a number.
|
|
11176
|
-
* - `flag`: a keyword, the parameter's name, at the start or end; a boolean.
|
|
11177
|
-
* - `separator`: a word (`of`) or `=` separating the parameters before it
|
|
11178
|
-
* from those after it; a boolean.
|
|
11179
|
-
* - `options`: keywords, the names of its `parameters`, each followed by a
|
|
11180
|
-
* quoted string or a number unless it is a flag.
|
|
11181
|
-
*/
|
|
11182
|
-
var PARAMETER_TYPES = [
|
|
11183
|
-
"expression",
|
|
11184
|
-
"statements",
|
|
11185
|
-
"passage",
|
|
11186
|
-
"variable",
|
|
11187
|
-
"string",
|
|
11188
|
-
"text",
|
|
11189
|
-
"names",
|
|
11190
|
-
"delay",
|
|
11191
|
-
"number",
|
|
11192
|
-
"flag",
|
|
11193
|
-
"separator",
|
|
11194
|
-
"options"
|
|
11195
|
-
];
|
|
11196
|
-
/**
|
|
11197
|
-
* What the value of a `string` or `text` argument holds, for the check at
|
|
11198
|
-
* story start and for tooling (see code-check.ts):
|
|
11199
|
-
* - `markup`: markup the macro renders (`{button}`'s label): its markup
|
|
11200
|
-
* is checked.
|
|
11201
|
-
* - `text`: plain text the macro uses as written (`{checkbox}`'s label).
|
|
11202
|
-
* - `passage`: a passage name (`{watch}`'s `goto`): the passage must exist.
|
|
11203
|
-
* - `expression`, `statements`: code (`{watch}`'s condition and `run`): it
|
|
11204
|
-
* is checked as code, and its variable references against the schema.
|
|
11205
|
-
*
|
|
11206
|
-
* Without it, the argument of a macro with `interpolate` holds `markup`,
|
|
11207
|
-
* any other `text`.
|
|
11208
|
-
*/
|
|
11209
|
-
var STRING_HOLDS = [
|
|
11210
|
-
"markup",
|
|
11211
|
-
"text",
|
|
11212
|
-
"passage",
|
|
11213
|
-
"expression",
|
|
11214
|
-
"statements"
|
|
11215
|
-
];
|
|
11216
|
-
/**
|
|
11217
|
-
* Throw if a parameter `macro` declares (or an option of one) has no type,
|
|
11218
|
-
* or one that isn't a ParameterType: arguments are read by their type, and
|
|
11219
|
-
* there is no default. Throw too if it declares what it `holds` wrongly.
|
|
11220
|
-
*/
|
|
11221
|
-
function checkParameterTypes(macro, parameters) {
|
|
11222
|
-
const holdsFix = `Give a \`string\` or \`text\` parameter one of ${STRING_HOLDS.join(", ")} to hold (see docs/custom-macros.md#what-a-string-holds).`;
|
|
11223
|
-
for (const param of parameters) {
|
|
11224
|
-
const type = param.type;
|
|
11225
|
-
if (!PARAMETER_TYPES.includes(type)) throw parameterError(macro, param, type === void 0 ? "has no type" : `has the unknown type ${JSON.stringify(type)}`, `Give it one of the types ${PARAMETER_TYPES.join(", ")} (see docs/custom-macros.md#parameter-types), or declare no parameters and read props.rawArgs.`);
|
|
11226
|
-
const holds = param.holds;
|
|
11227
|
-
if (holds !== void 0 && type !== "string" && type !== "text") throw parameterError(macro, param, `is of the type ${type}, which holds what it is`, holdsFix);
|
|
11228
|
-
if (holds !== void 0 && !STRING_HOLDS.includes(holds)) throw parameterError(macro, param, `holds the unknown ${JSON.stringify(holds)}`, holdsFix);
|
|
11229
|
-
if (param.parameters) checkParameterTypes(macro, param.parameters);
|
|
11230
|
-
}
|
|
11231
|
-
}
|
|
11232
|
-
/** The error for the parameter `param` of `macro`: what is wrong, and the fix. */
|
|
11233
|
-
function parameterError(macro, param, problem, fix) {
|
|
11234
|
-
return /* @__PURE__ */ new Error(`spindle: The parameter "${param.name}" of the macro {${macro}} ${problem}. ${fix}`);
|
|
11235
|
-
}
|
|
11236
|
-
new NameMap();
|
|
11237
11310
|
//#endregion
|
|
11238
11311
|
//#region src/utils/namespace.ts
|
|
11239
11312
|
/** The variable name no namespace can hold. */
|
|
@@ -11430,7 +11503,8 @@ function validateStoryMarkup(passages, macros, options = {}) {
|
|
|
11430
11503
|
const list = [...passages];
|
|
11431
11504
|
const all = [...macros];
|
|
11432
11505
|
const known = /* @__PURE__ */ new Set();
|
|
11433
|
-
const
|
|
11506
|
+
const parametersOf = parameterLookup(all);
|
|
11507
|
+
const blocks = new Set(blockWidgetNames(list, parametersOf).map((n) => n.toLowerCase()));
|
|
11434
11508
|
for (const macro of all) {
|
|
11435
11509
|
const name = macro.name.toLowerCase();
|
|
11436
11510
|
known.add(name);
|
|
@@ -11440,7 +11514,7 @@ function validateStoryMarkup(passages, macros, options = {}) {
|
|
|
11440
11514
|
return validateMarkup(list, {
|
|
11441
11515
|
isKnownMacro: (name) => known.has(name),
|
|
11442
11516
|
macroNames: known,
|
|
11443
|
-
parametersOf
|
|
11517
|
+
parametersOf,
|
|
11444
11518
|
isBlockMacro: (name) => blocks.has(name.toLowerCase()) || isBlockMacro(name),
|
|
11445
11519
|
checkPassageNames: options.checkPassageNames
|
|
11446
11520
|
});
|
package/package.json
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
|
-
import { useMemo } from 'preact/hooks';
|
|
1
|
+
import { useLayoutEffect, useMemo } from 'preact/hooks';
|
|
2
2
|
import { useStoryFields } from '../hooks/use-story-fields';
|
|
3
3
|
import { parseMarkup } from '../markup/parse';
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
renderInlineNodes,
|
|
6
|
+
InterfaceContext,
|
|
7
|
+
NobrContext,
|
|
8
|
+
} from '../markup/render';
|
|
9
|
+
import { declareInterfaceMounted } from '../triggers';
|
|
5
10
|
import { errorMessage } from '../utils/error-message';
|
|
6
11
|
|
|
7
12
|
const DEFAULT_MARKUP =
|
|
@@ -26,9 +31,15 @@ export function StoryInterface() {
|
|
|
26
31
|
}
|
|
27
32
|
}, [markup]);
|
|
28
33
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
34
|
+
useLayoutEffect(declareInterfaceMounted, []);
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<InterfaceContext.Provider value={true}>
|
|
38
|
+
{nobr ? (
|
|
39
|
+
<NobrContext.Provider value={true}>{rendered}</NobrContext.Provider>
|
|
40
|
+
) : (
|
|
41
|
+
rendered
|
|
42
|
+
)}
|
|
43
|
+
</InterfaceContext.Provider>
|
|
33
44
|
);
|
|
34
45
|
}
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { defineMacro } from '../../define-macro';
|
|
2
2
|
import { FrozenStateContext } from '../../hooks/use-story-fields';
|
|
3
3
|
import { useStoryStore } from '../../store';
|
|
4
|
+
import { InterfaceContext } from '../../markup/render';
|
|
4
5
|
import {
|
|
5
6
|
addMacroTrigger,
|
|
7
|
+
declareInterfaceWatch,
|
|
6
8
|
removeTrigger,
|
|
7
9
|
subscribeTriggerResets,
|
|
8
10
|
triggerResets,
|
|
@@ -50,6 +52,15 @@ defineMacro({
|
|
|
50
52
|
() => subscribeTriggerResets(() => rerender((n) => n + 1)),
|
|
51
53
|
[],
|
|
52
54
|
);
|
|
55
|
+
// The interface's watchers are re-registered by a load of a save made
|
|
56
|
+
// before the interface mounted (#419)
|
|
57
|
+
const inInterface = hooks.useContext(InterfaceContext);
|
|
58
|
+
const declaredKey = JSON.stringify([condition, options]);
|
|
59
|
+
hooks.useLayoutEffect(
|
|
60
|
+
() =>
|
|
61
|
+
inInterface ? declareInterfaceWatch(condition, options) : undefined,
|
|
62
|
+
[inInterface, declaredKey],
|
|
63
|
+
);
|
|
53
64
|
const registered = hooks.useRef<number | null>(null);
|
|
54
65
|
if (
|
|
55
66
|
registered.current === null ||
|
package/src/markup/ast.ts
CHANGED
|
@@ -8,6 +8,8 @@ import { NameSet } from '../utils/macro-names';
|
|
|
8
8
|
export interface TextNode {
|
|
9
9
|
type: 'text';
|
|
10
10
|
value: string;
|
|
11
|
+
/** A closed HTML comment: markdown drops it, and so does raw rendering. */
|
|
12
|
+
comment?: true;
|
|
11
13
|
}
|
|
12
14
|
|
|
13
15
|
export interface VariableNode extends Selectors {
|
package/src/markup/render.tsx
CHANGED
|
@@ -44,6 +44,8 @@ export const LocalsValuesContext =
|
|
|
44
44
|
createContext<Record<string, unknown>>(EMPTY_NAMESPACE);
|
|
45
45
|
export const LocalsUpdateContext = createContext<LocalsUpdater>(defaultUpdater);
|
|
46
46
|
export const NobrContext = createContext(false);
|
|
47
|
+
/** Whether the markup is the story interface, which stays mounted (#419). */
|
|
48
|
+
export const InterfaceContext = createContext(false);
|
|
47
49
|
/**
|
|
48
50
|
* True while rendering inside an inline HTML element (e.g. `<span>`), where
|
|
49
51
|
* block-level markdown and `<p>` wrappers would produce invalid HTML (#220).
|
|
@@ -710,6 +712,12 @@ function HtmlNodeRenderer({ node }: { node: HtmlNode }) {
|
|
|
710
712
|
}
|
|
711
713
|
}
|
|
712
714
|
}
|
|
715
|
+
// The content of a foreignObject is HTML again, with HTML attributes
|
|
716
|
+
if (inSvg && tag === 'foreignobject' && children) {
|
|
717
|
+
children = (
|
|
718
|
+
<SvgContext.Provider value={false}>{children}</SvgContext.Provider>
|
|
719
|
+
);
|
|
720
|
+
}
|
|
713
721
|
let element = h(node.tag, props, children);
|
|
714
722
|
if (isSvgRoot && !inSvg) {
|
|
715
723
|
element = <SvgContext.Provider value={true}>{element}</SvgContext.Provider>;
|
|
@@ -806,7 +814,7 @@ function renderMacro(node: MacroNode, key: string) {
|
|
|
806
814
|
*/
|
|
807
815
|
function renderSingleNode(node: ASTNode): preact.ComponentChildren {
|
|
808
816
|
// Authored text outside markdown still has its character references decoded
|
|
809
|
-
if (node.type === 'text') return decodeText(node.value);
|
|
817
|
+
if (node.type === 'text') return node.comment ? null : decodeText(node.value);
|
|
810
818
|
const key = nodeKey(node);
|
|
811
819
|
switch (node.type) {
|
|
812
820
|
case 'variable':
|
package/src/markup/spindle.peggy
CHANGED
|
@@ -104,7 +104,7 @@ function lexeme(lex, loc, fields) {
|
|
|
104
104
|
function nodeOf(x) {
|
|
105
105
|
switch (x.lex) {
|
|
106
106
|
case 'text':
|
|
107
|
-
return { type: 'text', value: x.value };
|
|
107
|
+
return x.comment ? { type: 'text', value: x.value, comment: true } : { type: 'text', value: x.value };
|
|
108
108
|
case 'link':
|
|
109
109
|
return withSelectors({ type: 'macro', name: 'link', rawArgs: quoteArg(x.display) + ' ' + quoteArg(x.target), children: [] }, x);
|
|
110
110
|
case 'variable':
|
|
@@ -137,7 +137,7 @@ function tokenOf(x) {
|
|
|
137
137
|
|
|
138
138
|
/**
|
|
139
139
|
* Flatten items (lexemes, nodes, arrays of them, nulls) and join adjacent
|
|
140
|
-
* text, except escaped braces, which stay a text item of their own.
|
|
140
|
+
* text, except escaped braces and comments, which stay a text item of their own.
|
|
141
141
|
*/
|
|
142
142
|
function joinText(items, convert) {
|
|
143
143
|
const out = [];
|
|
@@ -145,7 +145,7 @@ function joinText(items, convert) {
|
|
|
145
145
|
const add = (item) => {
|
|
146
146
|
if (item === null || item === undefined) return;
|
|
147
147
|
if (Array.isArray(item)) return item.forEach(add);
|
|
148
|
-
if (item.lex === 'text' && !item.escape && last && last.lex === 'text' && !last.escape && last.end === item.start) {
|
|
148
|
+
if (item.lex === 'text' && !item.escape && !item.comment && last && last.lex === 'text' && !last.escape && !last.comment && last.end === item.start) {
|
|
149
149
|
last.value += item.value;
|
|
150
150
|
last.end = item.end;
|
|
151
151
|
return;
|
|
@@ -332,7 +332,9 @@ TextRun
|
|
|
332
332
|
*/
|
|
333
333
|
Comment
|
|
334
334
|
= value:$("<!--" (">" / "->" / (!"-->" .)* "-->")) {
|
|
335
|
-
|
|
335
|
+
const node = lexeme('text', location(), { value });
|
|
336
|
+
node.comment = true;
|
|
337
|
+
return node;
|
|
336
338
|
}
|
|
337
339
|
|
|
338
340
|
/**
|
package/src/saves/format.ts
CHANGED
|
@@ -175,7 +175,9 @@ function isPayload(value: unknown): value is Record<string, unknown> {
|
|
|
175
175
|
isCountsMap(value.visitCounts) &&
|
|
176
176
|
isCountsMap(value.renderCounts) &&
|
|
177
177
|
isOptionalPRNGSnapshot(value.prng) &&
|
|
178
|
-
isWatchers(value.watchers)
|
|
178
|
+
isWatchers(value.watchers) &&
|
|
179
|
+
(value.interfaceWatchers === undefined ||
|
|
180
|
+
typeof value.interfaceWatchers === 'boolean')
|
|
179
181
|
);
|
|
180
182
|
}
|
|
181
183
|
|
package/src/saves/types.ts
CHANGED
|
@@ -28,6 +28,11 @@ export interface SavePayload {
|
|
|
28
28
|
prng?: PRNGSnapshot | null;
|
|
29
29
|
/** The {watch} macro watchers registered (absent in older saves). */
|
|
30
30
|
watchers?: SavedWatcher[];
|
|
31
|
+
/**
|
|
32
|
+
* Whether the story interface had mounted when the save was made, so that
|
|
33
|
+
* `watchers` includes its {watch} macros (absent in older saves).
|
|
34
|
+
*/
|
|
35
|
+
interfaceWatchers?: boolean;
|
|
31
36
|
}
|
|
32
37
|
|
|
33
38
|
export interface SaveMeta {
|