@rohal12/spindle 0.52.5 → 0.52.6
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 +651 -544
- package/dist/pkg/macro-registry.json +255 -0
- package/package.json +1 -1
- package/src/components/macros/Button.tsx +2 -4
- package/src/components/macros/Checkbox.tsx +6 -13
- package/src/components/macros/Computed.tsx +38 -55
- package/src/components/macros/Cycle.tsx +1 -0
- package/src/components/macros/Dialog.tsx +7 -8
- package/src/components/macros/Do.tsx +2 -1
- package/src/components/macros/For.tsx +27 -78
- package/src/components/macros/Goto.tsx +6 -9
- package/src/components/macros/Include.tsx +10 -57
- package/src/components/macros/Listbox.tsx +1 -0
- package/src/components/macros/MacroError.tsx +37 -0
- package/src/components/macros/MacroLink.tsx +9 -24
- package/src/components/macros/Meter.tsx +19 -37
- package/src/components/macros/Numberbox.tsx +5 -32
- package/src/components/macros/PassageDisplay.tsx +37 -47
- package/src/components/macros/Print.tsx +1 -0
- package/src/components/macros/Radiobutton.tsx +8 -38
- package/src/components/macros/Repeat.tsx +3 -3
- package/src/components/macros/Set.tsx +3 -29
- package/src/components/macros/Switch.tsx +36 -37
- package/src/components/macros/Textarea.tsx +2 -28
- package/src/components/macros/Textbox.tsx +2 -29
- package/src/components/macros/Type.tsx +3 -3
- package/src/components/macros/Unset.tsx +17 -40
- package/src/components/macros/Watch.tsx +23 -91
- package/src/components/macros/WidgetInvocation.tsx +9 -71
- package/src/components/macros/detached-body.tsx +2 -2
- package/src/components/macros/input-macro.ts +48 -0
- package/src/components/macros/locals-scope.tsx +60 -0
- package/src/components/macros/macro-args.ts +280 -0
- package/src/components/macros/option-utils.ts +7 -12
- package/src/define-macro.ts +47 -30
- package/src/hooks/use-render-options.ts +26 -0
- package/src/markup/render.tsx +2 -4
- package/src/registry.ts +48 -0
- package/types/index.d.ts +69 -6
- package/types/tooling.d.ts +33 -1
package/dist/pkg/headless.js
CHANGED
|
@@ -3679,11 +3679,29 @@ function ExprDisplay({ expression, className, id }) {
|
|
|
3679
3679
|
return /* @__PURE__ */ jsx(Fragment$1, { children: display });
|
|
3680
3680
|
}
|
|
3681
3681
|
//#endregion
|
|
3682
|
-
//#region src/
|
|
3683
|
-
|
|
3684
|
-
|
|
3685
|
-
|
|
3686
|
-
|
|
3682
|
+
//#region src/hooks/use-render-options.ts
|
|
3683
|
+
/**
|
|
3684
|
+
* The renderNodes() options of the enclosing content: nobr, inline and raw
|
|
3685
|
+
* rendering and the locals in scope. A macro or widget body rendered with
|
|
3686
|
+
* them renders as the content around it does.
|
|
3687
|
+
*/
|
|
3688
|
+
function useRenderOptions() {
|
|
3689
|
+
return {
|
|
3690
|
+
nobr: useContext(NobrContext),
|
|
3691
|
+
inline: useContext(InlineContext),
|
|
3692
|
+
raw: useContext(RawTextContext),
|
|
3693
|
+
locals: useContext(LocalsValuesContext)
|
|
3694
|
+
};
|
|
3695
|
+
}
|
|
3696
|
+
//#endregion
|
|
3697
|
+
//#region src/components/macros/locals-scope.tsx
|
|
3698
|
+
/**
|
|
3699
|
+
* Render `body` in a new locals scope: the locals of `parentValues`, with
|
|
3700
|
+
* `ownKeys` (an iteration's loop variables, a widget's parameters) on top.
|
|
3701
|
+
* Locals the body assigns are kept in this scope.
|
|
3702
|
+
*/
|
|
3703
|
+
function LocalsScope({ body, parentValues, ownKeys }) {
|
|
3704
|
+
const options = useRenderOptions();
|
|
3687
3705
|
const [localMutations, setLocalMutations] = useState({});
|
|
3688
3706
|
const localState = useMemo(() => createNamespace(parentValues, ownKeys, localMutations), [
|
|
3689
3707
|
parentValues,
|
|
@@ -3710,29 +3728,22 @@ function WidgetBody({ body, parentValues, ownKeys }) {
|
|
|
3710
3728
|
children: /* @__PURE__ */ jsx(LocalsValuesContext.Provider, {
|
|
3711
3729
|
value: localState,
|
|
3712
3730
|
children: renderNodes(body, {
|
|
3713
|
-
|
|
3714
|
-
inline,
|
|
3715
|
-
raw,
|
|
3731
|
+
...options,
|
|
3716
3732
|
locals: localState
|
|
3717
3733
|
})
|
|
3718
3734
|
})
|
|
3719
3735
|
});
|
|
3720
3736
|
}
|
|
3737
|
+
//#endregion
|
|
3738
|
+
//#region src/components/macros/WidgetInvocation.tsx
|
|
3721
3739
|
function WidgetInvocation({ body, params, rawArgs, invocationChildren }) {
|
|
3722
|
-
const
|
|
3723
|
-
const
|
|
3724
|
-
const inline = useContext(InlineContext);
|
|
3725
|
-
const raw = useContext(RawTextContext);
|
|
3740
|
+
const renderOptions = useRenderOptions();
|
|
3741
|
+
const parentValues = renderOptions.locals;
|
|
3726
3742
|
const [mergedVars, mergedTemps, mergedLocals, mergedTrans] = useMergedLocals();
|
|
3727
3743
|
const childrenValue = invocationChildren?.length ? invocationChildren : null;
|
|
3728
3744
|
if (params.length === 0) return /* @__PURE__ */ jsx(WidgetChildrenContext.Provider, {
|
|
3729
3745
|
value: childrenValue,
|
|
3730
|
-
children: renderNodes(body,
|
|
3731
|
-
nobr,
|
|
3732
|
-
inline,
|
|
3733
|
-
raw,
|
|
3734
|
-
locals: parentValues
|
|
3735
|
-
})
|
|
3746
|
+
children: renderNodes(body, renderOptions)
|
|
3736
3747
|
});
|
|
3737
3748
|
const argExprs = rawArgs ? splitArgs(rawArgs) : [];
|
|
3738
3749
|
const values = [];
|
|
@@ -3753,7 +3764,7 @@ function WidgetInvocation({ body, params, rawArgs, invocationChildren }) {
|
|
|
3753
3764
|
}, values);
|
|
3754
3765
|
return /* @__PURE__ */ jsx(WidgetChildrenContext.Provider, {
|
|
3755
3766
|
value: childrenValue,
|
|
3756
|
-
children: /* @__PURE__ */ jsx(
|
|
3767
|
+
children: /* @__PURE__ */ jsx(LocalsScope, {
|
|
3757
3768
|
body,
|
|
3758
3769
|
parentValues,
|
|
3759
3770
|
ownKeys
|
|
@@ -3958,6 +3969,32 @@ function MacroError({ macro, error }) {
|
|
|
3958
3969
|
children: `{${macro} error${currentSourceLocation()}: ${errorMessage(error)}}`
|
|
3959
3970
|
});
|
|
3960
3971
|
}
|
|
3972
|
+
/** Log an error in the macro `{label}` (its name, or name and arguments). */
|
|
3973
|
+
function logMacroError(label, error) {
|
|
3974
|
+
console.error(`spindle: Error in {${label}}${currentSourceLocation()}:`, error);
|
|
3975
|
+
}
|
|
3976
|
+
/**
|
|
3977
|
+
* Run `effect` once, during the macro's first render. If it throws, the
|
|
3978
|
+
* error is logged under `{macro rawArgs}` and shown in place of the macro
|
|
3979
|
+
* from then on; otherwise the macro renders nothing.
|
|
3980
|
+
*/
|
|
3981
|
+
function useRunOnce(macro, rawArgs, effect) {
|
|
3982
|
+
const failure = useRef();
|
|
3983
|
+
if (failure.current === void 0) {
|
|
3984
|
+
failure.current = null;
|
|
3985
|
+
try {
|
|
3986
|
+
effect();
|
|
3987
|
+
} catch (error) {
|
|
3988
|
+
failure.current = { error };
|
|
3989
|
+
logMacroError(`${macro} ${rawArgs}`, error);
|
|
3990
|
+
}
|
|
3991
|
+
}
|
|
3992
|
+
if (!failure.current) return null;
|
|
3993
|
+
return /* @__PURE__ */ jsx(MacroError, {
|
|
3994
|
+
macro,
|
|
3995
|
+
error: failure.current.error
|
|
3996
|
+
});
|
|
3997
|
+
}
|
|
3961
3998
|
//#endregion
|
|
3962
3999
|
//#region src/markup/render.tsx
|
|
3963
4000
|
var defaultUpdater = {
|
|
@@ -4327,11 +4364,8 @@ function resolveAttributeValue(name, value, scope, errors) {
|
|
|
4327
4364
|
}
|
|
4328
4365
|
function HtmlNodeRenderer({ node }) {
|
|
4329
4366
|
const scope = useTextScope();
|
|
4330
|
-
const nobr =
|
|
4331
|
-
const locals = useContext(LocalsValuesContext);
|
|
4332
|
-
const inRaw = useContext(RawTextContext);
|
|
4367
|
+
const { nobr, locals, raw: inRaw, inline: parentInline } = useRenderOptions();
|
|
4333
4368
|
const inSvg = useContext(SvgContext);
|
|
4334
|
-
const parentInline = useContext(InlineContext);
|
|
4335
4369
|
const tag = node.tag.toLowerCase();
|
|
4336
4370
|
const isSvgRoot = tag === "svg";
|
|
4337
4371
|
const isRawRoot = !inRaw && (isSvgRoot || PREFORMATTED_ELEMENTS.has(tag));
|
|
@@ -7494,18 +7528,236 @@ function collectText(nodes) {
|
|
|
7494
7528
|
return nodes.map((n) => n.type === "text" ? n.value : "").join("");
|
|
7495
7529
|
}
|
|
7496
7530
|
//#endregion
|
|
7531
|
+
//#region src/utils/parse-delay.ts
|
|
7532
|
+
/**
|
|
7533
|
+
* Parse a delay string into milliseconds.
|
|
7534
|
+
* Supports: "2s" → 2000, "500ms" → 500, bare number → ms
|
|
7535
|
+
*/
|
|
7536
|
+
function parseDelay(str) {
|
|
7537
|
+
const trimmed = str.trim();
|
|
7538
|
+
if (trimmed.endsWith("ms")) return parseFloat(trimmed.slice(0, -2));
|
|
7539
|
+
if (trimmed.endsWith("s")) return parseFloat(trimmed.slice(0, -1)) * 1e3;
|
|
7540
|
+
return parseFloat(trimmed);
|
|
7541
|
+
}
|
|
7542
|
+
//#endregion
|
|
7543
|
+
//#region src/components/macros/macro-args.ts
|
|
7544
|
+
/**
|
|
7545
|
+
* The macro argument layer: reads a macro's raw argument text into the
|
|
7546
|
+
* parameters its definition declares (see ParameterDef), so every macro
|
|
7547
|
+
* splits and unquotes its arguments the same way. defineMacro() passes the
|
|
7548
|
+
* result to the macro as `ctx.args`.
|
|
7549
|
+
*
|
|
7550
|
+
* Arguments are read in groups, split at `separator` parameters. A group's
|
|
7551
|
+
* `flag` parameters are taken off its start or end first. Its last
|
|
7552
|
+
* `expression` or `text` parameter (without one, its last parameter) reads
|
|
7553
|
+
* the text the others leave; each parameter before it reads one term from
|
|
7554
|
+
* the start, and each after it one term from the end. Terms are separated by
|
|
7555
|
+
* whitespace outside literals and brackets (see arg-utils.ts), and a term
|
|
7556
|
+
* that follows an operator belongs to the expression before it. A parameter
|
|
7557
|
+
* with nothing to read is unset (a flag or separator is false, options are
|
|
7558
|
+
* empty).
|
|
7559
|
+
*/
|
|
7560
|
+
/** The terms of `src`: the runs of text between its depth-0 whitespace. */
|
|
7561
|
+
function terms(src) {
|
|
7562
|
+
const spans = [];
|
|
7563
|
+
let start = 0;
|
|
7564
|
+
for (const i of [...topLevelIndices(src, isWhitespace), src.length]) {
|
|
7565
|
+
if (i > start) spans.push([start, i]);
|
|
7566
|
+
start = i + 1;
|
|
7567
|
+
}
|
|
7568
|
+
return spans;
|
|
7569
|
+
}
|
|
7570
|
+
var slice = (src, [start, end]) => src.slice(start, end);
|
|
7571
|
+
/**
|
|
7572
|
+
* The value of a `text` argument: one quoted string, or the text with any
|
|
7573
|
+
* loose quotes stripped (`"Red` → `Red`).
|
|
7574
|
+
*/
|
|
7575
|
+
function readText(src) {
|
|
7576
|
+
return readWholeQuoted(src) ?? stripLooseQuotes(src);
|
|
7577
|
+
}
|
|
7578
|
+
function readValue(param, src) {
|
|
7579
|
+
switch (param.type) {
|
|
7580
|
+
case "string": return readWholeQuoted(src) ?? void 0;
|
|
7581
|
+
case "text": return readText(src);
|
|
7582
|
+
case "names": return src.split(",").map((name) => name.trim());
|
|
7583
|
+
case "delay": return parseDelay(src);
|
|
7584
|
+
case "number": return Number(src);
|
|
7585
|
+
case "options": return readOptions(param.parameters ?? [], src);
|
|
7586
|
+
default: return src;
|
|
7587
|
+
}
|
|
7588
|
+
}
|
|
7589
|
+
function unset(param) {
|
|
7590
|
+
if (param.type === "flag" || param.type === "separator") return false;
|
|
7591
|
+
return param.type === "options" ? {} : void 0;
|
|
7592
|
+
}
|
|
7593
|
+
var WORD_RE = /\w+/y;
|
|
7594
|
+
var DIGITS_RE = /\d+/y;
|
|
7595
|
+
/** Match the sticky regex `re` at `pos`, returning the matched text. */
|
|
7596
|
+
function matchAt(re, src, pos) {
|
|
7597
|
+
re.lastIndex = pos;
|
|
7598
|
+
return re.exec(src)?.[0] ?? null;
|
|
7599
|
+
}
|
|
7600
|
+
/**
|
|
7601
|
+
* Read keyword options (`goto "X" priority 5 once`). A keyword takes the
|
|
7602
|
+
* quoted string or digit run after it as its value; keywords that aren't
|
|
7603
|
+
* declared, and their values, are skipped.
|
|
7604
|
+
*/
|
|
7605
|
+
function readOptions(params, src) {
|
|
7606
|
+
const options = {};
|
|
7607
|
+
let i = 0;
|
|
7608
|
+
while (i < src.length) {
|
|
7609
|
+
const key = matchAt(WORD_RE, src, i);
|
|
7610
|
+
if (!key) {
|
|
7611
|
+
i = readQuoted(src, i)?.end ?? i + 1;
|
|
7612
|
+
continue;
|
|
7613
|
+
}
|
|
7614
|
+
i += key.length;
|
|
7615
|
+
let val;
|
|
7616
|
+
let j = i;
|
|
7617
|
+
while (j < src.length && isWhitespace(src[j])) j++;
|
|
7618
|
+
if (j > i) {
|
|
7619
|
+
const quoted = readQuoted(src, j);
|
|
7620
|
+
const digits = quoted ? null : matchAt(DIGITS_RE, src, j);
|
|
7621
|
+
if (quoted) {
|
|
7622
|
+
val = quoted.value;
|
|
7623
|
+
i = quoted.end;
|
|
7624
|
+
} else if (digits) {
|
|
7625
|
+
val = digits;
|
|
7626
|
+
i = j + digits.length;
|
|
7627
|
+
}
|
|
7628
|
+
}
|
|
7629
|
+
const param = params.find((p) => p.name === key);
|
|
7630
|
+
if (param?.type === "flag") options[key] = true;
|
|
7631
|
+
else if (param?.type === "number") options[key] = Number(val ?? 0);
|
|
7632
|
+
else if (param) options[key] = val;
|
|
7633
|
+
}
|
|
7634
|
+
return options;
|
|
7635
|
+
}
|
|
7636
|
+
/** `inline %name` reads a transient variable; `inline % 2` is modulo. */
|
|
7637
|
+
var LEADING_OPERATOR_RE = /^(?:[-+*/&|^=<>?:,.]|%(?![A-Za-z_]))/;
|
|
7638
|
+
/**
|
|
7639
|
+
* The rest of `src` without the flag `word` as its last or first term, or
|
|
7640
|
+
* `null` if it has no such flag. Next to a binary operator the word is an
|
|
7641
|
+
* operand (`"a" + inline`), not a flag; the closing `/` of a regex literal
|
|
7642
|
+
* is not one.
|
|
7643
|
+
*/
|
|
7644
|
+
function takeFlag(src, word) {
|
|
7645
|
+
const spans = terms(src);
|
|
7646
|
+
if (spans.length < 2) return null;
|
|
7647
|
+
if (slice(src, spans[spans.length - 1]) === word) {
|
|
7648
|
+
const rest = src.slice(0, spans[spans.length - 2][1]);
|
|
7649
|
+
if (!endsWithOperator(rest)) return rest;
|
|
7650
|
+
}
|
|
7651
|
+
if (slice(src, spans[0]) === word) {
|
|
7652
|
+
const rest = src.slice(spans[1][0]);
|
|
7653
|
+
if (!LEADING_OPERATOR_RE.test(rest)) return rest;
|
|
7654
|
+
}
|
|
7655
|
+
return null;
|
|
7656
|
+
}
|
|
7657
|
+
/**
|
|
7658
|
+
* Where the separator `word` splits `src`, as the end of the text before it
|
|
7659
|
+
* and the start of the text after it, or `null` if it doesn't. A word is a
|
|
7660
|
+
* term with terms on both sides; `=` is an assignment at depth 0, not part
|
|
7661
|
+
* of `==` or `!=`.
|
|
7662
|
+
*/
|
|
7663
|
+
function findSeparator(src, word) {
|
|
7664
|
+
if (word === "=") {
|
|
7665
|
+
const indices = topLevelIndices(src, (ch) => ch === "=");
|
|
7666
|
+
for (let k = 0; k < indices.length; k++) {
|
|
7667
|
+
const i = indices[k];
|
|
7668
|
+
if (src[i + 1] === "=") {
|
|
7669
|
+
k++;
|
|
7670
|
+
continue;
|
|
7671
|
+
}
|
|
7672
|
+
if (src[i - 1] !== "!") return [i, i + 1];
|
|
7673
|
+
}
|
|
7674
|
+
return null;
|
|
7675
|
+
}
|
|
7676
|
+
const spans = terms(src);
|
|
7677
|
+
for (let k = 1; k < spans.length - 1; k++) if (slice(src, spans[k]) === word) return [spans[k - 1][1], spans[k + 1][0]];
|
|
7678
|
+
return null;
|
|
7679
|
+
}
|
|
7680
|
+
var takesRest = (param) => param.type === void 0 || param.type === "expression" || param.type === "text";
|
|
7681
|
+
/** Read the parameters of one group from `src` (`null`: no text) into `args`. */
|
|
7682
|
+
function readGroup(params, src, args) {
|
|
7683
|
+
let text = src?.trim() ?? "";
|
|
7684
|
+
for (const flag of params.filter((p) => p.type === "flag")) {
|
|
7685
|
+
const rest = src === null ? null : takeFlag(text, flag.name);
|
|
7686
|
+
args[flag.name] = rest !== null;
|
|
7687
|
+
text = rest ?? text;
|
|
7688
|
+
}
|
|
7689
|
+
const positional = params.filter((p) => p.type !== "flag");
|
|
7690
|
+
if (positional.length === 0) return;
|
|
7691
|
+
const lastText = positional.findIndex((p, i) => takesRest(p) && !positional.slice(i + 1).some(takesRest));
|
|
7692
|
+
const restIndex = lastText < 0 ? positional.length - 1 : lastText;
|
|
7693
|
+
const read = (param, value) => value === "" ? unset(param) : readValue(param, value);
|
|
7694
|
+
if (positional.length === 1) {
|
|
7695
|
+
args[positional[0].name] = read(positional[0], text);
|
|
7696
|
+
return;
|
|
7697
|
+
}
|
|
7698
|
+
const spans = terms(text);
|
|
7699
|
+
let from = 0;
|
|
7700
|
+
let to = spans.length;
|
|
7701
|
+
for (const param of positional.slice(0, restIndex)) args[param.name] = from < to ? read(param, slice(text, spans[from++])) : unset(param);
|
|
7702
|
+
for (const param of positional.slice(restIndex + 1).reverse()) {
|
|
7703
|
+
const value = from < to && !endsWithOperator(text.slice(0, spans[to - 1][0])) ? read(param, slice(text, spans[to - 1])) : void 0;
|
|
7704
|
+
args[param.name] = value ?? unset(param);
|
|
7705
|
+
if (value !== void 0) to--;
|
|
7706
|
+
}
|
|
7707
|
+
const rest = positional[restIndex];
|
|
7708
|
+
args[rest.name] = from < to ? read(rest, text.slice(spans[from][0], spans[to - 1][1])) : unset(rest);
|
|
7709
|
+
}
|
|
7710
|
+
/** Read `rawArgs` into the declared `parameters` (see above). */
|
|
7711
|
+
function parseMacroArgs(rawArgs, parameters) {
|
|
7712
|
+
const args = {};
|
|
7713
|
+
let rest = rawArgs.trim();
|
|
7714
|
+
let groupStart = 0;
|
|
7715
|
+
parameters.forEach((separator, i) => {
|
|
7716
|
+
if (separator.type !== "separator") return;
|
|
7717
|
+
const at = rest === null ? null : findSeparator(rest, separator.name);
|
|
7718
|
+
args[separator.name] = at !== null;
|
|
7719
|
+
const before = at ? rest.slice(0, at[0]) : rest;
|
|
7720
|
+
readGroup(parameters.slice(groupStart, i), before, args);
|
|
7721
|
+
rest = at ? rest.slice(at[1]) : null;
|
|
7722
|
+
groupStart = i + 1;
|
|
7723
|
+
});
|
|
7724
|
+
readGroup(parameters.slice(groupStart), rest, args);
|
|
7725
|
+
return args;
|
|
7726
|
+
}
|
|
7727
|
+
/**
|
|
7728
|
+
* The passage a passage-name argument names: the value of its expression
|
|
7729
|
+
* or, when that can't be evaluated (`{goto Bob's room}`), its text.
|
|
7730
|
+
*/
|
|
7731
|
+
function evaluatePassageName(expr, evaluate) {
|
|
7732
|
+
try {
|
|
7733
|
+
return String(evaluate(expr ?? ""));
|
|
7734
|
+
} catch {
|
|
7735
|
+
return readText(expr ?? "");
|
|
7736
|
+
}
|
|
7737
|
+
}
|
|
7738
|
+
/**
|
|
7739
|
+
* The variable a `storeVar` macro binds: its first term, as written. Custom
|
|
7740
|
+
* macros may bind one without declaring parameters.
|
|
7741
|
+
*/
|
|
7742
|
+
function readBoundVariable(rawArgs) {
|
|
7743
|
+
const src = rawArgs.trim();
|
|
7744
|
+
const first = terms(src)[0];
|
|
7745
|
+
return first ? slice(src, first) : "";
|
|
7746
|
+
}
|
|
7747
|
+
//#endregion
|
|
7497
7748
|
//#region src/components/macros/option-utils.ts
|
|
7749
|
+
/** `$var "placeholder"`, the arguments of the input macros. */
|
|
7498
7750
|
function parseVarArgs(rawArgs) {
|
|
7499
|
-
const
|
|
7500
|
-
|
|
7501
|
-
|
|
7502
|
-
|
|
7503
|
-
|
|
7504
|
-
|
|
7505
|
-
|
|
7751
|
+
const { variable = "", placeholder = "" } = parseMacroArgs(rawArgs, [{
|
|
7752
|
+
name: "variable",
|
|
7753
|
+
type: "variable"
|
|
7754
|
+
}, {
|
|
7755
|
+
name: "placeholder",
|
|
7756
|
+
type: "string"
|
|
7757
|
+
}]);
|
|
7506
7758
|
return {
|
|
7507
|
-
varName,
|
|
7508
|
-
placeholder
|
|
7759
|
+
varName: variable.replace(/["']/g, ""),
|
|
7760
|
+
placeholder
|
|
7509
7761
|
};
|
|
7510
7762
|
}
|
|
7511
7763
|
/**
|
|
@@ -7536,6 +7788,7 @@ var sharedHooks = {
|
|
|
7536
7788
|
useContext
|
|
7537
7789
|
};
|
|
7538
7790
|
function defineMacro(config, source = "builtin") {
|
|
7791
|
+
const parameters = config.parameters ?? [];
|
|
7539
7792
|
function Wrapper(props) {
|
|
7540
7793
|
let className = props.className;
|
|
7541
7794
|
let id = props.id;
|
|
@@ -7546,18 +7799,13 @@ function defineMacro(config, source = "builtin") {
|
|
|
7546
7799
|
id = resolve(id);
|
|
7547
7800
|
}
|
|
7548
7801
|
const { update, getValues } = useContext(LocalsUpdateContext);
|
|
7549
|
-
const
|
|
7550
|
-
const inline = useContext(InlineContext);
|
|
7551
|
-
const raw = useContext(RawTextContext);
|
|
7552
|
-
const localsValues = useContext(LocalsValuesContext);
|
|
7802
|
+
const renderOptions = useRenderOptions();
|
|
7553
7803
|
const renderNodes$1 = (nodes, options) => renderNodes(nodes, {
|
|
7554
|
-
|
|
7555
|
-
inline,
|
|
7556
|
-
raw,
|
|
7557
|
-
locals: localsValues,
|
|
7804
|
+
...renderOptions,
|
|
7558
7805
|
...options
|
|
7559
7806
|
});
|
|
7560
7807
|
const ctx = {
|
|
7808
|
+
args: parseMacroArgs(props.rawArgs, parameters),
|
|
7561
7809
|
collectText,
|
|
7562
7810
|
sourceLocation: currentSourceLocation,
|
|
7563
7811
|
parseVarArgs,
|
|
@@ -7588,7 +7836,7 @@ function defineMacro(config, source = "builtin") {
|
|
|
7588
7836
|
ctx.evaluate = (expr) => evaluate(expr, merged[0], merged[1], merged[2], merged[3]);
|
|
7589
7837
|
}
|
|
7590
7838
|
if (config.storeVar) {
|
|
7591
|
-
const firstToken = props.rawArgs
|
|
7839
|
+
const firstToken = readBoundVariable(props.rawArgs).replace(/["']/g, "");
|
|
7592
7840
|
if (firstToken.startsWith("%")) return h("span", { class: "error" }, `{${config.name}}: transient variables (%${firstToken.slice(1)}) cannot be bound to input macros`);
|
|
7593
7841
|
const varExpr = firstToken.replace(/["']/g, "").replace(/^\$/, "");
|
|
7594
7842
|
const segments = varExpr.split(".");
|
|
@@ -7605,7 +7853,11 @@ function defineMacro(config, source = "builtin") {
|
|
|
7605
7853
|
return config.render(props, ctx);
|
|
7606
7854
|
}
|
|
7607
7855
|
registerMacro(config.name, Wrapper);
|
|
7608
|
-
|
|
7856
|
+
const text = config.text;
|
|
7857
|
+
registerMacroText(config.name, text && ((props, ctx) => text(props, {
|
|
7858
|
+
...ctx,
|
|
7859
|
+
args: parseMacroArgs(props.rawArgs, parameters)
|
|
7860
|
+
})));
|
|
7609
7861
|
const isBlock = config.block === true || config.block !== false && (config.subMacros?.length ?? 0) > 0;
|
|
7610
7862
|
registerMacroMetadata(config.name, {
|
|
7611
7863
|
name: config.name,
|
|
@@ -7615,7 +7867,7 @@ function defineMacro(config, source = "builtin") {
|
|
|
7615
7867
|
interpolate: config.interpolate,
|
|
7616
7868
|
merged: config.merged,
|
|
7617
7869
|
description: config.description,
|
|
7618
|
-
parameters: config.parameters,
|
|
7870
|
+
parameters: config.parameters && [...config.parameters],
|
|
7619
7871
|
source
|
|
7620
7872
|
});
|
|
7621
7873
|
if (config.subMacros) for (const sub of config.subMacros) registerSubMacro(sub);
|
|
@@ -8288,29 +8540,24 @@ defineMacro({
|
|
|
8288
8540
|
//#region src/components/macros/Set.tsx
|
|
8289
8541
|
defineMacro({
|
|
8290
8542
|
name: "set",
|
|
8543
|
+
parameters: [{
|
|
8544
|
+
name: "code",
|
|
8545
|
+
type: "expression",
|
|
8546
|
+
required: true
|
|
8547
|
+
}],
|
|
8291
8548
|
render({ rawArgs }, ctx) {
|
|
8292
|
-
|
|
8293
|
-
const failure = ctx.hooks.useRef(null);
|
|
8294
|
-
if (!ran.current) {
|
|
8295
|
-
ran.current = true;
|
|
8296
|
-
try {
|
|
8297
|
-
ctx.mutate(rawArgs);
|
|
8298
|
-
} catch (err) {
|
|
8299
|
-
failure.current = { error: err };
|
|
8300
|
-
console.error(`spindle: Error in {set ${rawArgs}}${ctx.sourceLocation()}:`, err);
|
|
8301
|
-
}
|
|
8302
|
-
}
|
|
8303
|
-
if (failure.current) return /* @__PURE__ */ jsx(MacroError, {
|
|
8304
|
-
macro: "set",
|
|
8305
|
-
error: failure.current.error
|
|
8306
|
-
});
|
|
8307
|
-
return null;
|
|
8549
|
+
return useRunOnce("set", rawArgs, () => ctx.mutate(rawArgs));
|
|
8308
8550
|
}
|
|
8309
8551
|
});
|
|
8310
8552
|
//#endregion
|
|
8311
8553
|
//#region src/components/macros/Print.tsx
|
|
8312
8554
|
defineMacro({
|
|
8313
8555
|
name: "print",
|
|
8556
|
+
parameters: [{
|
|
8557
|
+
name: "expression",
|
|
8558
|
+
type: "expression",
|
|
8559
|
+
required: true
|
|
8560
|
+
}],
|
|
8314
8561
|
interpolate: true,
|
|
8315
8562
|
merged: true,
|
|
8316
8563
|
render({ rawArgs }, ctx) {
|
|
@@ -8338,30 +8585,16 @@ defineMacro({
|
|
|
8338
8585
|
* applied, even `undefined`; a first evaluation that throws leaves it unset.
|
|
8339
8586
|
*/
|
|
8340
8587
|
var UNSET = Symbol("unset");
|
|
8341
|
-
|
|
8342
|
-
|
|
8343
|
-
|
|
8344
|
-
|
|
8345
|
-
|
|
8346
|
-
|
|
8347
|
-
|
|
8348
|
-
|
|
8349
|
-
|
|
8350
|
-
|
|
8351
|
-
continue;
|
|
8352
|
-
}
|
|
8353
|
-
if (i > 0 && trimmed[i - 1] === "!") continue;
|
|
8354
|
-
const target = trimmed.slice(0, i).trim();
|
|
8355
|
-
const expr = trimmed.slice(i + 1).trim();
|
|
8356
|
-
if (!target.match(/^[$_@]\w+$/)) throw new Error(`{computed}: target must be $name, _name, or @name, got "${target}"`);
|
|
8357
|
-
checkVariableName(target.slice(1), target);
|
|
8358
|
-
return {
|
|
8359
|
-
target,
|
|
8360
|
-
expr
|
|
8361
|
-
};
|
|
8362
|
-
}
|
|
8363
|
-
}
|
|
8364
|
-
throw new Error(`{computed}: expected "target = expression", got "${rawArgs}"`);
|
|
8588
|
+
/** Check the arguments of "target = expression". */
|
|
8589
|
+
function assignmentArgs(rawArgs, args) {
|
|
8590
|
+
if (!args["="]) throw new Error(`{computed}: expected "target = expression", got "${rawArgs}"`);
|
|
8591
|
+
const { target = "", expression: expr = "" } = args;
|
|
8592
|
+
if (!target.match(/^[$_@]\w+$/)) throw new Error(`{computed}: target must be $name, _name, or @name, got "${target}"`);
|
|
8593
|
+
checkVariableName(target.slice(1), target);
|
|
8594
|
+
return {
|
|
8595
|
+
target,
|
|
8596
|
+
expr
|
|
8597
|
+
};
|
|
8365
8598
|
}
|
|
8366
8599
|
/**
|
|
8367
8600
|
* Evaluate `expr` against the current values — the store's state and the
|
|
@@ -8378,7 +8611,7 @@ function computeAndApply(expr, name, isTemp, isLocal, getLocals, rawArgs, prevRe
|
|
|
8378
8611
|
const { variables, temporary, transient } = readState();
|
|
8379
8612
|
newValue = evaluate(expr, variables, temporary, getLocals(), transient);
|
|
8380
8613
|
} catch (err) {
|
|
8381
|
-
|
|
8614
|
+
logMacroError(`computed ${rawArgs}`, err);
|
|
8382
8615
|
return;
|
|
8383
8616
|
}
|
|
8384
8617
|
if (!deepEqual(prevRef.current, newValue)) {
|
|
@@ -8386,7 +8619,7 @@ function computeAndApply(expr, name, isTemp, isLocal, getLocals, rawArgs, prevRe
|
|
|
8386
8619
|
if (isLocal) try {
|
|
8387
8620
|
localsUpdate(name, newValue);
|
|
8388
8621
|
} catch (err) {
|
|
8389
|
-
|
|
8622
|
+
logMacroError(`computed ${rawArgs}`, err);
|
|
8390
8623
|
}
|
|
8391
8624
|
else {
|
|
8392
8625
|
const state = useStoryStore.getState();
|
|
@@ -8398,12 +8631,28 @@ function computeAndApply(expr, name, isTemp, isLocal, getLocals, rawArgs, prevRe
|
|
|
8398
8631
|
defineMacro({
|
|
8399
8632
|
name: "computed",
|
|
8400
8633
|
merged: true,
|
|
8634
|
+
parameters: [
|
|
8635
|
+
{
|
|
8636
|
+
name: "target",
|
|
8637
|
+
type: "variable",
|
|
8638
|
+
required: true
|
|
8639
|
+
},
|
|
8640
|
+
{
|
|
8641
|
+
name: "=",
|
|
8642
|
+
type: "separator"
|
|
8643
|
+
},
|
|
8644
|
+
{
|
|
8645
|
+
name: "expression",
|
|
8646
|
+
type: "expression",
|
|
8647
|
+
required: true
|
|
8648
|
+
}
|
|
8649
|
+
],
|
|
8401
8650
|
render({ rawArgs }, ctx) {
|
|
8402
8651
|
const [mergedVars, mergedTemps, mergedLocals, mergedTrans] = ctx.merged;
|
|
8403
8652
|
let target;
|
|
8404
8653
|
let expr;
|
|
8405
8654
|
try {
|
|
8406
|
-
({target, expr} =
|
|
8655
|
+
({target, expr} = assignmentArgs(rawArgs, ctx.args));
|
|
8407
8656
|
} catch (err) {
|
|
8408
8657
|
return /* @__PURE__ */ jsx(MacroError, {
|
|
8409
8658
|
macro: "computed",
|
|
@@ -8415,14 +8664,13 @@ defineMacro({
|
|
|
8415
8664
|
const name = target.slice(1);
|
|
8416
8665
|
const localsUpdate = isLocal ? ctx.update : null;
|
|
8417
8666
|
const prevOutput = ctx.hooks.useRef(UNSET);
|
|
8667
|
+
const compute = () => computeAndApply(expr, name, isTemp, isLocal, ctx.getValues, rawArgs, prevOutput, localsUpdate);
|
|
8418
8668
|
const ran = ctx.hooks.useRef(false);
|
|
8419
8669
|
if (!ran.current) {
|
|
8420
8670
|
ran.current = true;
|
|
8421
|
-
|
|
8671
|
+
compute();
|
|
8422
8672
|
}
|
|
8423
|
-
ctx.hooks.useLayoutEffect(
|
|
8424
|
-
computeAndApply(expr, name, isTemp, isLocal, ctx.getValues, rawArgs, prevOutput, localsUpdate);
|
|
8425
|
-
}, [
|
|
8673
|
+
ctx.hooks.useLayoutEffect(compute, [
|
|
8426
8674
|
mergedVars,
|
|
8427
8675
|
mergedTemps,
|
|
8428
8676
|
mergedLocals,
|
|
@@ -8433,26 +8681,13 @@ defineMacro({
|
|
|
8433
8681
|
});
|
|
8434
8682
|
//#endregion
|
|
8435
8683
|
//#region src/components/macros/Meter.tsx
|
|
8436
|
-
/**
|
|
8437
|
-
|
|
8438
|
-
|
|
8439
|
-
* trailing standalone `"…"` or `'…'` string is the label mode, with `\"`,
|
|
8440
|
-
* `\'` and `\\` escapes. A string after an operator (`$a ?? "5"`) is an
|
|
8441
|
-
* operand of the max expression, not the label.
|
|
8442
|
-
*/
|
|
8443
|
-
function parseMeterArgs(rawArgs) {
|
|
8444
|
-
const tokens = splitTopLevel(rawArgs.trim(), isWhitespace).filter(Boolean);
|
|
8445
|
-
let labelMode = "";
|
|
8446
|
-
const label = tokens.length >= 2 && !endsWithOperator(tokens.slice(0, -1).join(" ")) ? readWholeQuoted(tokens[tokens.length - 1]) : null;
|
|
8447
|
-
if (label !== null) {
|
|
8448
|
-
labelMode = label;
|
|
8449
|
-
tokens.pop();
|
|
8450
|
-
}
|
|
8451
|
-
if (tokens.length < 2) throw new Error("meter requires two arguments: {meter currentExpr maxExpr}");
|
|
8684
|
+
/** Check the arguments of `{meter currentExpr maxExpr ["label"]}`. */
|
|
8685
|
+
function meterArgs(args) {
|
|
8686
|
+
if (args.current === void 0 || args.max === void 0) throw new Error("meter requires two arguments: {meter currentExpr maxExpr}");
|
|
8452
8687
|
return {
|
|
8453
|
-
currentExpr:
|
|
8454
|
-
maxExpr:
|
|
8455
|
-
labelMode
|
|
8688
|
+
currentExpr: args.current,
|
|
8689
|
+
maxExpr: args.max,
|
|
8690
|
+
labelMode: args.label ?? ""
|
|
8456
8691
|
};
|
|
8457
8692
|
}
|
|
8458
8693
|
function formatLabel(current, max, labelMode) {
|
|
@@ -8465,9 +8700,25 @@ defineMacro({
|
|
|
8465
8700
|
name: "meter",
|
|
8466
8701
|
interpolate: true,
|
|
8467
8702
|
merged: true,
|
|
8468
|
-
|
|
8703
|
+
parameters: [
|
|
8704
|
+
{
|
|
8705
|
+
name: "current",
|
|
8706
|
+
type: "expression",
|
|
8707
|
+
required: true
|
|
8708
|
+
},
|
|
8709
|
+
{
|
|
8710
|
+
name: "max",
|
|
8711
|
+
type: "expression",
|
|
8712
|
+
required: true
|
|
8713
|
+
},
|
|
8714
|
+
{
|
|
8715
|
+
name: "label",
|
|
8716
|
+
type: "string"
|
|
8717
|
+
}
|
|
8718
|
+
],
|
|
8719
|
+
render(_props, ctx) {
|
|
8469
8720
|
try {
|
|
8470
|
-
const { currentExpr, maxExpr, labelMode } =
|
|
8721
|
+
const { currentExpr, maxExpr, labelMode } = meterArgs(ctx.args);
|
|
8471
8722
|
const current = Number(ctx.evaluate(currentExpr));
|
|
8472
8723
|
const max = Number(ctx.evaluate(maxExpr));
|
|
8473
8724
|
const pct = max === 0 ? 0 : Math.max(0, Math.min(100, current / max * 100));
|
|
@@ -8583,16 +8834,12 @@ function keyOf(val, ancestors) {
|
|
|
8583
8834
|
//#endregion
|
|
8584
8835
|
//#region src/components/macros/For.tsx
|
|
8585
8836
|
/**
|
|
8586
|
-
*
|
|
8837
|
+
* Check the arguments of "@item, @i of $list" or "@item of $list".
|
|
8587
8838
|
*/
|
|
8588
|
-
function
|
|
8589
|
-
|
|
8590
|
-
|
|
8591
|
-
const
|
|
8592
|
-
const listExpr = rawArgs.slice(ofIdx + 4).trim();
|
|
8593
|
-
const vars = varsPart.split(",").map((v) => v.trim());
|
|
8594
|
-
const itemVar = vars[0];
|
|
8595
|
-
const indexVar = vars.length > 1 ? vars[1] : null;
|
|
8839
|
+
function loopArgs(rawArgs, args) {
|
|
8840
|
+
if (!args.of) throw new Error(`{for} requires "of" keyword: {for ${rawArgs}}`);
|
|
8841
|
+
const [itemVar = "", indexVar = null] = args.variables ?? [];
|
|
8842
|
+
const listExpr = args.list ?? "";
|
|
8596
8843
|
if (!itemVar.startsWith("@")) throw new Error(`{for} loop variable must use @ prefix: got "${itemVar}"`);
|
|
8597
8844
|
if (indexVar && !indexVar.startsWith("@")) throw new Error(`{for} index variable must use @ prefix: got "${indexVar}"`);
|
|
8598
8845
|
checkVariableName(itemVar.slice(1), itemVar);
|
|
@@ -8604,50 +8851,18 @@ function parseForArgs(rawArgs) {
|
|
|
8604
8851
|
};
|
|
8605
8852
|
}
|
|
8606
8853
|
function ForIteration({ parentValues, itemVar, itemValue, indexVar, indexValue, children }) {
|
|
8607
|
-
|
|
8608
|
-
|
|
8609
|
-
[itemVar]: itemValue,
|
|
8610
|
-
...indexVar ? { [indexVar]: indexValue } : void 0
|
|
8611
|
-
}), [
|
|
8612
|
-
itemVar,
|
|
8613
|
-
itemValue,
|
|
8614
|
-
indexVar,
|
|
8615
|
-
indexValue
|
|
8616
|
-
]);
|
|
8617
|
-
const localState = useMemo(() => createNamespace(parentValues, ownKeys, localMutations), [
|
|
8854
|
+
return /* @__PURE__ */ jsx(LocalsScope, {
|
|
8855
|
+
body: children,
|
|
8618
8856
|
parentValues,
|
|
8619
|
-
ownKeys
|
|
8620
|
-
|
|
8621
|
-
|
|
8622
|
-
|
|
8623
|
-
|
|
8624
|
-
|
|
8625
|
-
|
|
8626
|
-
|
|
8627
|
-
|
|
8628
|
-
setLocalMutations((prev) => ({
|
|
8629
|
-
...prev,
|
|
8630
|
-
[key]: value
|
|
8631
|
-
}));
|
|
8632
|
-
}, []);
|
|
8633
|
-
const updater = useMemo(() => ({
|
|
8634
|
-
update,
|
|
8635
|
-
getValues
|
|
8636
|
-
}), [update, getValues]);
|
|
8637
|
-
const nobr = useContext(NobrContext);
|
|
8638
|
-
const inline = useContext(InlineContext);
|
|
8639
|
-
const raw = useContext(RawTextContext);
|
|
8640
|
-
return /* @__PURE__ */ jsx(LocalsUpdateContext.Provider, {
|
|
8641
|
-
value: updater,
|
|
8642
|
-
children: /* @__PURE__ */ jsx(LocalsValuesContext.Provider, {
|
|
8643
|
-
value: localState,
|
|
8644
|
-
children: renderNodes(children, {
|
|
8645
|
-
nobr,
|
|
8646
|
-
inline,
|
|
8647
|
-
raw,
|
|
8648
|
-
locals: localState
|
|
8649
|
-
})
|
|
8650
|
-
})
|
|
8857
|
+
ownKeys: useMemo(() => ({
|
|
8858
|
+
[itemVar]: itemValue,
|
|
8859
|
+
...indexVar ? { [indexVar]: indexValue } : void 0
|
|
8860
|
+
}), [
|
|
8861
|
+
itemVar,
|
|
8862
|
+
itemValue,
|
|
8863
|
+
indexVar,
|
|
8864
|
+
indexValue
|
|
8865
|
+
])
|
|
8651
8866
|
});
|
|
8652
8867
|
}
|
|
8653
8868
|
defineMacro({
|
|
@@ -8655,21 +8870,29 @@ defineMacro({
|
|
|
8655
8870
|
block: true,
|
|
8656
8871
|
interpolate: true,
|
|
8657
8872
|
merged: true,
|
|
8873
|
+
parameters: [
|
|
8874
|
+
{
|
|
8875
|
+
name: "variables",
|
|
8876
|
+
type: "names",
|
|
8877
|
+
required: true
|
|
8878
|
+
},
|
|
8879
|
+
{
|
|
8880
|
+
name: "of",
|
|
8881
|
+
type: "separator"
|
|
8882
|
+
},
|
|
8883
|
+
{
|
|
8884
|
+
name: "list",
|
|
8885
|
+
type: "expression",
|
|
8886
|
+
required: true
|
|
8887
|
+
}
|
|
8888
|
+
],
|
|
8658
8889
|
render({ rawArgs, children = [] }, ctx) {
|
|
8659
8890
|
const parentValues = useContext(LocalsValuesContext);
|
|
8660
|
-
let
|
|
8661
|
-
try {
|
|
8662
|
-
parsed = parseForArgs(rawArgs);
|
|
8663
|
-
} catch (err) {
|
|
8664
|
-
return /* @__PURE__ */ jsx(MacroError, {
|
|
8665
|
-
macro: "for",
|
|
8666
|
-
error: err
|
|
8667
|
-
});
|
|
8668
|
-
}
|
|
8669
|
-
const { itemVar, indexVar, listExpr } = parsed;
|
|
8891
|
+
let loop;
|
|
8670
8892
|
let list;
|
|
8671
8893
|
try {
|
|
8672
|
-
|
|
8894
|
+
loop = loopArgs(rawArgs, ctx.args);
|
|
8895
|
+
const result = ctx.evaluate(loop.listExpr);
|
|
8673
8896
|
if (!Array.isArray(result)) return /* @__PURE__ */ jsx("span", {
|
|
8674
8897
|
class: "error",
|
|
8675
8898
|
children: `{for error: expression did not evaluate to an array}`
|
|
@@ -8681,6 +8904,7 @@ defineMacro({
|
|
|
8681
8904
|
error: err
|
|
8682
8905
|
});
|
|
8683
8906
|
}
|
|
8907
|
+
const { itemVar, indexVar } = loop;
|
|
8684
8908
|
const content = list.map((item, i) => /* @__PURE__ */ jsx(ForIteration, {
|
|
8685
8909
|
parentValues,
|
|
8686
8910
|
itemVar,
|
|
@@ -8692,7 +8916,7 @@ defineMacro({
|
|
|
8692
8916
|
return ctx.wrap(content);
|
|
8693
8917
|
},
|
|
8694
8918
|
text({ rawArgs, children = [] }, ctx) {
|
|
8695
|
-
const { itemVar, indexVar, listExpr } =
|
|
8919
|
+
const { itemVar, indexVar, listExpr } = loopArgs(rawArgs, ctx.args);
|
|
8696
8920
|
const list = ctx.evaluate(listExpr);
|
|
8697
8921
|
if (!Array.isArray(list)) throw new Error("expression did not evaluate to an array");
|
|
8698
8922
|
return list.map((item, i) => ctx.renderText(children, {
|
|
@@ -8712,7 +8936,7 @@ defineMacro({
|
|
|
8712
8936
|
try {
|
|
8713
8937
|
ctx.mutate(code);
|
|
8714
8938
|
} catch (err) {
|
|
8715
|
-
|
|
8939
|
+
logMacroError("do", err);
|
|
8716
8940
|
}
|
|
8717
8941
|
}, []);
|
|
8718
8942
|
return null;
|
|
@@ -8743,27 +8967,20 @@ function liveLocalsView(getValues) {
|
|
|
8743
8967
|
});
|
|
8744
8968
|
}
|
|
8745
8969
|
//#endregion
|
|
8746
|
-
//#region src/utils/parse-delay.ts
|
|
8747
|
-
/**
|
|
8748
|
-
* Parse a delay string into milliseconds.
|
|
8749
|
-
* Supports: "2s" → 2000, "500ms" → 500, bare number → ms
|
|
8750
|
-
*/
|
|
8751
|
-
function parseDelay(str) {
|
|
8752
|
-
const trimmed = str.trim();
|
|
8753
|
-
if (trimmed.endsWith("ms")) return parseFloat(trimmed.slice(0, -2));
|
|
8754
|
-
if (trimmed.endsWith("s")) return parseFloat(trimmed.slice(0, -1)) * 1e3;
|
|
8755
|
-
return parseFloat(trimmed);
|
|
8756
|
-
}
|
|
8757
|
-
//#endregion
|
|
8758
8970
|
//#region src/components/macros/Repeat.tsx
|
|
8759
8971
|
var RepeatContext = createContext({ stop: () => {} });
|
|
8760
8972
|
defineMacro({
|
|
8761
8973
|
name: "repeat",
|
|
8762
8974
|
block: true,
|
|
8763
8975
|
interpolate: true,
|
|
8764
|
-
|
|
8976
|
+
parameters: [{
|
|
8977
|
+
name: "delay",
|
|
8978
|
+
type: "delay",
|
|
8979
|
+
required: true
|
|
8980
|
+
}],
|
|
8981
|
+
render({ children = [] }, ctx) {
|
|
8765
8982
|
const { useState, useEffect, useCallback } = ctx.hooks;
|
|
8766
|
-
const delay =
|
|
8983
|
+
const delay = ctx.args.delay ?? 0;
|
|
8767
8984
|
const [count, setCount] = useState(0);
|
|
8768
8985
|
const [stopped, setStopped] = useState(false);
|
|
8769
8986
|
const stop = useCallback(() => setStopped(true), []);
|
|
@@ -8810,8 +9027,7 @@ defineMacro({
|
|
|
8810
9027
|
*/
|
|
8811
9028
|
function useDetachedBody() {
|
|
8812
9029
|
const updater = useContext(LocalsUpdateContext);
|
|
8813
|
-
const nobr =
|
|
8814
|
-
const inline = useContext(InlineContext);
|
|
9030
|
+
const { nobr, inline } = useRenderOptions();
|
|
8815
9031
|
const widgetChildren = useContext(WidgetChildrenContext);
|
|
8816
9032
|
const repeat = useContext(RepeatContext);
|
|
8817
9033
|
const closeDialog = useContext(DialogCloseContext);
|
|
@@ -8854,9 +9070,13 @@ defineMacro({
|
|
|
8854
9070
|
name: "button",
|
|
8855
9071
|
block: true,
|
|
8856
9072
|
interpolate: true,
|
|
9073
|
+
parameters: [{
|
|
9074
|
+
name: "label",
|
|
9075
|
+
type: "text",
|
|
9076
|
+
required: true
|
|
9077
|
+
}],
|
|
8857
9078
|
render({ rawArgs, children = [] }, ctx) {
|
|
8858
|
-
const
|
|
8859
|
-
const label = ctx.resolve?.(text) ?? rawArgs;
|
|
9079
|
+
const label = ctx.resolve?.(ctx.args.label ?? "") ?? rawArgs;
|
|
8860
9080
|
const runBody = useDetachedBody();
|
|
8861
9081
|
const handleClick = () => runBody(children);
|
|
8862
9082
|
ctx.useAction({
|
|
@@ -8876,30 +9096,22 @@ defineMacro({
|
|
|
8876
9096
|
});
|
|
8877
9097
|
//#endregion
|
|
8878
9098
|
//#region src/components/macros/MacroLink.tsx
|
|
8879
|
-
function parseLinkArgs(rawArgs) {
|
|
8880
|
-
const parts = [];
|
|
8881
|
-
const re = /(["'])((?:\\[^]|(?!\1)[^\\])*)\1/g;
|
|
8882
|
-
let m;
|
|
8883
|
-
while ((m = re.exec(rawArgs)) !== null) parts.push(unescapeQuoted(m[2]));
|
|
8884
|
-
if (parts.length >= 2) return {
|
|
8885
|
-
display: parts[0],
|
|
8886
|
-
passage: parts[1]
|
|
8887
|
-
};
|
|
8888
|
-
if (parts.length === 1) return {
|
|
8889
|
-
display: parts[0],
|
|
8890
|
-
passage: null
|
|
8891
|
-
};
|
|
8892
|
-
return {
|
|
8893
|
-
display: rawArgs.trim(),
|
|
8894
|
-
passage: null
|
|
8895
|
-
};
|
|
8896
|
-
}
|
|
8897
9099
|
defineMacro({
|
|
8898
9100
|
name: "link",
|
|
8899
9101
|
block: true,
|
|
8900
9102
|
interpolate: true,
|
|
9103
|
+
parameters: [{
|
|
9104
|
+
name: "text",
|
|
9105
|
+
type: "string",
|
|
9106
|
+
required: true
|
|
9107
|
+
}, {
|
|
9108
|
+
name: "passage",
|
|
9109
|
+
type: "string"
|
|
9110
|
+
}],
|
|
8901
9111
|
render({ rawArgs, children = [] }, ctx) {
|
|
8902
|
-
const {
|
|
9112
|
+
const { text } = ctx.args;
|
|
9113
|
+
const display = text ?? rawArgs.trim();
|
|
9114
|
+
const passage = text === void 0 ? null : ctx.args.passage ?? null;
|
|
8903
9115
|
const runBody = useDetachedBody();
|
|
8904
9116
|
const perform = () => {
|
|
8905
9117
|
runBody(children);
|
|
@@ -8928,9 +9140,27 @@ defineMacro({
|
|
|
8928
9140
|
});
|
|
8929
9141
|
//#endregion
|
|
8930
9142
|
//#region src/components/macros/Switch.tsx
|
|
9143
|
+
/**
|
|
9144
|
+
* The first {case} branch whose value equals `value`, else the {default}
|
|
9145
|
+
* branch, else null. Throws what a case expression throws.
|
|
9146
|
+
*/
|
|
9147
|
+
function selectBranch(value, branches, evaluate) {
|
|
9148
|
+
let defaultBranch = null;
|
|
9149
|
+
for (let i = 1; i < branches.length; i++) {
|
|
9150
|
+
const branch = branches[i];
|
|
9151
|
+
if (branch.rawArgs === "") defaultBranch = branch;
|
|
9152
|
+
else if (value === evaluate(branch.rawArgs)) return branch;
|
|
9153
|
+
}
|
|
9154
|
+
return defaultBranch;
|
|
9155
|
+
}
|
|
8931
9156
|
defineMacro({
|
|
8932
9157
|
name: "switch",
|
|
8933
9158
|
subMacros: ["case", "default"],
|
|
9159
|
+
parameters: [{
|
|
9160
|
+
name: "expression",
|
|
9161
|
+
type: "expression",
|
|
9162
|
+
required: true
|
|
9163
|
+
}],
|
|
8934
9164
|
merged: true,
|
|
8935
9165
|
render({ rawArgs, branches = [] }, ctx) {
|
|
8936
9166
|
let switchValue;
|
|
@@ -8942,35 +9172,20 @@ defineMacro({
|
|
|
8942
9172
|
error: err
|
|
8943
9173
|
});
|
|
8944
9174
|
}
|
|
8945
|
-
let
|
|
8946
|
-
|
|
8947
|
-
|
|
8948
|
-
|
|
8949
|
-
|
|
8950
|
-
|
|
8951
|
-
|
|
8952
|
-
|
|
8953
|
-
const caseValue = ctx.evaluate(branch.rawArgs);
|
|
8954
|
-
if (switchValue === caseValue) return /* @__PURE__ */ jsx(Fragment$1, { children: ctx.renderNodes(branch.children) });
|
|
8955
|
-
} catch (err) {
|
|
8956
|
-
return /* @__PURE__ */ jsx(MacroError, {
|
|
8957
|
-
macro: "case",
|
|
8958
|
-
error: err
|
|
8959
|
-
});
|
|
8960
|
-
}
|
|
9175
|
+
let branch;
|
|
9176
|
+
try {
|
|
9177
|
+
branch = selectBranch(switchValue, branches, ctx.evaluate);
|
|
9178
|
+
} catch (err) {
|
|
9179
|
+
return /* @__PURE__ */ jsx(MacroError, {
|
|
9180
|
+
macro: "case",
|
|
9181
|
+
error: err
|
|
9182
|
+
});
|
|
8961
9183
|
}
|
|
8962
|
-
|
|
8963
|
-
return null;
|
|
9184
|
+
return branch && /* @__PURE__ */ jsx(Fragment$1, { children: ctx.renderNodes(branch.children) });
|
|
8964
9185
|
},
|
|
8965
9186
|
text({ rawArgs, branches = [] }, ctx) {
|
|
8966
|
-
const
|
|
8967
|
-
|
|
8968
|
-
for (let i = 1; i < branches.length; i++) {
|
|
8969
|
-
const branch = branches[i];
|
|
8970
|
-
if (branch.rawArgs === "") defaultBranch = branch;
|
|
8971
|
-
else if (switchValue === ctx.evaluate(branch.rawArgs)) return ctx.renderText(branch.children);
|
|
8972
|
-
}
|
|
8973
|
-
return defaultBranch ? ctx.renderText(defaultBranch.children) : "";
|
|
9187
|
+
const branch = selectBranch(ctx.evaluate(rawArgs), branches, ctx.evaluate);
|
|
9188
|
+
return branch ? ctx.renderText(branch.children) : "";
|
|
8974
9189
|
}
|
|
8975
9190
|
});
|
|
8976
9191
|
//#endregion
|
|
@@ -9030,9 +9245,14 @@ defineMacro({
|
|
|
9030
9245
|
name: "type",
|
|
9031
9246
|
block: true,
|
|
9032
9247
|
interpolate: true,
|
|
9033
|
-
|
|
9248
|
+
parameters: [{
|
|
9249
|
+
name: "delay",
|
|
9250
|
+
type: "delay",
|
|
9251
|
+
required: true
|
|
9252
|
+
}],
|
|
9253
|
+
render({ children = [] }, ctx) {
|
|
9034
9254
|
const { useState, useEffect, useRef } = ctx.hooks;
|
|
9035
|
-
const speed =
|
|
9255
|
+
const speed = ctx.args.delay ?? 0;
|
|
9036
9256
|
const containerRef = useRef(null);
|
|
9037
9257
|
const [totalChars, setTotalChars] = useState(0);
|
|
9038
9258
|
const [visibleChars, setVisibleChars] = useState(0);
|
|
@@ -9213,11 +9433,18 @@ defineMacro({
|
|
|
9213
9433
|
name: "dialog",
|
|
9214
9434
|
block: true,
|
|
9215
9435
|
interpolate: true,
|
|
9216
|
-
|
|
9436
|
+
parameters: [{
|
|
9437
|
+
name: "label",
|
|
9438
|
+
type: "text",
|
|
9439
|
+
required: true
|
|
9440
|
+
}, {
|
|
9441
|
+
name: "noclose",
|
|
9442
|
+
type: "flag"
|
|
9443
|
+
}],
|
|
9444
|
+
render({ children = [] }, ctx) {
|
|
9217
9445
|
const [open, setOpen] = ctx.hooks.useState(false);
|
|
9218
|
-
const noclose =
|
|
9219
|
-
const
|
|
9220
|
-
const label = ctx.resolve?.(readWholeQuoted(labelRaw) ?? labelRaw.replace(/^["']|["']$/g, "")) ?? labelRaw;
|
|
9446
|
+
const { noclose } = ctx.args;
|
|
9447
|
+
const label = ctx.resolve(ctx.args.label ?? "");
|
|
9221
9448
|
const passageName = ctx.collectText(children).trim().replace(/^["']|["']$/g, "");
|
|
9222
9449
|
return /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("button", {
|
|
9223
9450
|
id: ctx.id,
|
|
@@ -9624,59 +9851,22 @@ defineMacro({
|
|
|
9624
9851
|
});
|
|
9625
9852
|
//#endregion
|
|
9626
9853
|
//#region src/components/macros/Include.tsx
|
|
9627
|
-
var FLAG = "inline";
|
|
9628
|
-
/**
|
|
9629
|
-
* Split a standalone `inline` flag off the start or end of the include
|
|
9630
|
-
* arguments. The flag only counts outside quotes, brackets, parentheses and
|
|
9631
|
-
* braces, separated by whitespace from the target expression, so passage
|
|
9632
|
-
* names and expressions containing the word stay intact (#201).
|
|
9633
|
-
*/
|
|
9634
|
-
function parseIncludeArgs(rawArgs) {
|
|
9635
|
-
const trimmed = rawArgs.trim();
|
|
9636
|
-
const spaces = topLevelIndices(trimmed, isWhitespace);
|
|
9637
|
-
let first = null;
|
|
9638
|
-
let last = null;
|
|
9639
|
-
if (spaces.length > 0) {
|
|
9640
|
-
let s = 0;
|
|
9641
|
-
while (s + 1 < spaces.length && spaces[s + 1] === spaces[s] + 1) s++;
|
|
9642
|
-
first = [spaces[0], spaces[s] + 1];
|
|
9643
|
-
let e = spaces.length - 1;
|
|
9644
|
-
while (e > 0 && spaces[e - 1] === spaces[e] - 1) e--;
|
|
9645
|
-
last = [spaces[e], spaces[spaces.length - 1] + 1];
|
|
9646
|
-
}
|
|
9647
|
-
if (last && last[1] === trimmed.length - 6 && trimmed.endsWith(FLAG)) {
|
|
9648
|
-
const rest = trimmed.slice(0, last[0]);
|
|
9649
|
-
if (!endsWithOperator(rest)) return {
|
|
9650
|
-
nameExpr: rest,
|
|
9651
|
-
inline: true
|
|
9652
|
-
};
|
|
9653
|
-
}
|
|
9654
|
-
if (first && first[0] === 6 && trimmed.startsWith(FLAG)) {
|
|
9655
|
-
const rest = trimmed.slice(first[1]);
|
|
9656
|
-
if (!/^(?:[-+*/&|^=<>?:,.]|%(?![A-Za-z_]))/.test(rest)) return {
|
|
9657
|
-
nameExpr: rest,
|
|
9658
|
-
inline: true
|
|
9659
|
-
};
|
|
9660
|
-
}
|
|
9661
|
-
return {
|
|
9662
|
-
nameExpr: trimmed,
|
|
9663
|
-
inline: false
|
|
9664
|
-
};
|
|
9665
|
-
}
|
|
9666
9854
|
defineMacro({
|
|
9667
9855
|
name: "include",
|
|
9668
9856
|
interpolate: true,
|
|
9669
9857
|
merged: true,
|
|
9670
|
-
|
|
9858
|
+
parameters: [{
|
|
9859
|
+
name: "inline",
|
|
9860
|
+
type: "flag"
|
|
9861
|
+
}, {
|
|
9862
|
+
name: "passage",
|
|
9863
|
+
type: "expression",
|
|
9864
|
+
required: true
|
|
9865
|
+
}],
|
|
9866
|
+
render(_props, ctx) {
|
|
9671
9867
|
const storyData = useStoryStore((s) => s.storyData);
|
|
9672
|
-
const {
|
|
9673
|
-
|
|
9674
|
-
try {
|
|
9675
|
-
const result = ctx.evaluate(nameExpr);
|
|
9676
|
-
passageName = String(result);
|
|
9677
|
-
} catch {
|
|
9678
|
-
passageName = nameExpr.replace(/^["']|["']$/g, "");
|
|
9679
|
-
}
|
|
9868
|
+
const { inline } = ctx.args;
|
|
9869
|
+
const passageName = evaluatePassageName(ctx.args.passage, ctx.evaluate);
|
|
9680
9870
|
const passage = storyData?.passages.get(passageName);
|
|
9681
9871
|
const ast = ctx.hooks.useMemo(() => passage ? buildAST(tokenize(passage.content)) : null, [passage]);
|
|
9682
9872
|
if (!storyData) return null;
|
|
@@ -9706,16 +9896,14 @@ defineMacro({
|
|
|
9706
9896
|
defineMacro({
|
|
9707
9897
|
name: "goto",
|
|
9708
9898
|
merged: true,
|
|
9709
|
-
|
|
9899
|
+
parameters: [{
|
|
9900
|
+
name: "passage",
|
|
9901
|
+
type: "expression",
|
|
9902
|
+
required: true
|
|
9903
|
+
}],
|
|
9904
|
+
render(_props, ctx) {
|
|
9710
9905
|
ctx.hooks.useLayoutEffect(() => {
|
|
9711
|
-
|
|
9712
|
-
try {
|
|
9713
|
-
const result = ctx.evaluate(rawArgs);
|
|
9714
|
-
passageName = String(result);
|
|
9715
|
-
} catch {
|
|
9716
|
-
passageName = rawArgs.replace(/^["']|["']$/g, "");
|
|
9717
|
-
}
|
|
9718
|
-
useStoryStore.getState().navigate(passageName);
|
|
9906
|
+
useStoryStore.getState().navigate(evaluatePassageName(ctx.args.passage, ctx.evaluate));
|
|
9719
9907
|
}, []);
|
|
9720
9908
|
return null;
|
|
9721
9909
|
}
|
|
@@ -9724,124 +9912,90 @@ defineMacro({
|
|
|
9724
9912
|
//#region src/components/macros/Unset.tsx
|
|
9725
9913
|
defineMacro({
|
|
9726
9914
|
name: "unset",
|
|
9915
|
+
parameters: [{
|
|
9916
|
+
name: "variable",
|
|
9917
|
+
type: "variable",
|
|
9918
|
+
required: true
|
|
9919
|
+
}],
|
|
9727
9920
|
render({ rawArgs }, ctx) {
|
|
9728
|
-
|
|
9729
|
-
const failure = ctx.hooks.useRef(null);
|
|
9730
|
-
if (!ran.current) {
|
|
9731
|
-
ran.current = true;
|
|
9921
|
+
return useRunOnce("unset", rawArgs, () => {
|
|
9732
9922
|
const state = useStoryStore.getState();
|
|
9733
|
-
const name =
|
|
9734
|
-
|
|
9735
|
-
|
|
9736
|
-
|
|
9737
|
-
|
|
9738
|
-
|
|
9739
|
-
else throw new Error(`{unset} expects a variable ($name, _name, %name, or @name), got "${name}"`);
|
|
9740
|
-
} catch (err) {
|
|
9741
|
-
failure.current = { error: err };
|
|
9742
|
-
console.error(`spindle: Error in {unset ${rawArgs}}${ctx.sourceLocation()}:`, err);
|
|
9743
|
-
}
|
|
9744
|
-
}
|
|
9745
|
-
if (failure.current) return /* @__PURE__ */ jsx(MacroError, {
|
|
9746
|
-
macro: "unset",
|
|
9747
|
-
error: failure.current.error
|
|
9923
|
+
const name = ctx.args.variable ?? "";
|
|
9924
|
+
if (name.startsWith("$")) state.deleteVariable(name.slice(1));
|
|
9925
|
+
else if (name.startsWith("_")) state.deleteTemporary(name.slice(1));
|
|
9926
|
+
else if (name.startsWith("%")) state.deleteTransient(name.slice(1));
|
|
9927
|
+
else if (name.startsWith("@")) ctx.update(name.slice(1), void 0);
|
|
9928
|
+
else throw new Error(`{unset} expects a variable ($name, _name, %name, or @name), got "${name}"`);
|
|
9748
9929
|
});
|
|
9749
|
-
return null;
|
|
9750
9930
|
}
|
|
9751
9931
|
});
|
|
9752
9932
|
//#endregion
|
|
9933
|
+
//#region src/components/macros/input-macro.ts
|
|
9934
|
+
/**
|
|
9935
|
+
* Define a text input macro, `{name $var "placeholder"}`: an `<input>` of
|
|
9936
|
+
* `type` (or, without one, a `<textarea>`) bound to the story variable.
|
|
9937
|
+
* `toValue` turns what the reader types, or an automation action's value
|
|
9938
|
+
* (`undefined` when it has none), into the variable's value: by default,
|
|
9939
|
+
* the text.
|
|
9940
|
+
*/
|
|
9941
|
+
function defineInputMacro(name, type, toValue = (input) => input !== void 0 ? String(input) : "") {
|
|
9942
|
+
defineMacro({
|
|
9943
|
+
name,
|
|
9944
|
+
storeVar: true,
|
|
9945
|
+
parameters: [{
|
|
9946
|
+
name: "variable",
|
|
9947
|
+
type: "variable",
|
|
9948
|
+
required: true
|
|
9949
|
+
}, {
|
|
9950
|
+
name: "placeholder",
|
|
9951
|
+
type: "string"
|
|
9952
|
+
}],
|
|
9953
|
+
render(_props, ctx) {
|
|
9954
|
+
const placeholder = ctx.args.placeholder ?? "";
|
|
9955
|
+
ctx.useAction({
|
|
9956
|
+
type: name,
|
|
9957
|
+
key: `$${ctx.varName}`,
|
|
9958
|
+
authorId: ctx.id,
|
|
9959
|
+
label: placeholder || ctx.varName,
|
|
9960
|
+
variable: ctx.varName,
|
|
9961
|
+
value: ctx.value,
|
|
9962
|
+
perform: (v) => ctx.setValue(toValue(v))
|
|
9963
|
+
});
|
|
9964
|
+
return h(type ? "input" : "textarea", {
|
|
9965
|
+
type,
|
|
9966
|
+
id: ctx.id,
|
|
9967
|
+
class: ctx.cls,
|
|
9968
|
+
value: ctx.value == null ? "" : String(ctx.value),
|
|
9969
|
+
placeholder,
|
|
9970
|
+
onInput: (e) => ctx.setValue(toValue(e.target.value))
|
|
9971
|
+
});
|
|
9972
|
+
}
|
|
9973
|
+
});
|
|
9974
|
+
}
|
|
9975
|
+
//#endregion
|
|
9753
9976
|
//#region src/components/macros/Textbox.tsx
|
|
9754
|
-
|
|
9755
|
-
name: "textbox",
|
|
9756
|
-
storeVar: true,
|
|
9757
|
-
render({ rawArgs }, ctx) {
|
|
9758
|
-
const { placeholder } = ctx.parseVarArgs(rawArgs);
|
|
9759
|
-
ctx.useAction({
|
|
9760
|
-
type: "textbox",
|
|
9761
|
-
key: `$${ctx.varName}`,
|
|
9762
|
-
authorId: ctx.id,
|
|
9763
|
-
label: placeholder || ctx.varName,
|
|
9764
|
-
variable: ctx.varName,
|
|
9765
|
-
value: ctx.value,
|
|
9766
|
-
perform: (v) => ctx.setValue(v !== void 0 ? String(v) : "")
|
|
9767
|
-
});
|
|
9768
|
-
return /* @__PURE__ */ jsx("input", {
|
|
9769
|
-
type: "text",
|
|
9770
|
-
id: ctx.id,
|
|
9771
|
-
class: ctx.cls,
|
|
9772
|
-
value: ctx.value == null ? "" : String(ctx.value),
|
|
9773
|
-
placeholder,
|
|
9774
|
-
onInput: (e) => ctx.setValue(e.target.value)
|
|
9775
|
-
});
|
|
9776
|
-
}
|
|
9777
|
-
});
|
|
9977
|
+
defineInputMacro("textbox", "text");
|
|
9778
9978
|
//#endregion
|
|
9779
9979
|
//#region src/components/macros/Numberbox.tsx
|
|
9780
|
-
|
|
9781
|
-
name: "numberbox",
|
|
9782
|
-
storeVar: true,
|
|
9783
|
-
render({ rawArgs }, ctx) {
|
|
9784
|
-
const { placeholder } = ctx.parseVarArgs(rawArgs);
|
|
9785
|
-
ctx.useAction({
|
|
9786
|
-
type: "numberbox",
|
|
9787
|
-
key: `$${ctx.varName}`,
|
|
9788
|
-
authorId: ctx.id,
|
|
9789
|
-
label: placeholder || ctx.varName,
|
|
9790
|
-
variable: ctx.varName,
|
|
9791
|
-
value: ctx.value,
|
|
9792
|
-
perform: (v) => ctx.setValue(v !== void 0 ? Number(v) : 0)
|
|
9793
|
-
});
|
|
9794
|
-
return /* @__PURE__ */ jsx("input", {
|
|
9795
|
-
type: "number",
|
|
9796
|
-
id: ctx.id,
|
|
9797
|
-
class: ctx.cls,
|
|
9798
|
-
value: ctx.value == null ? "" : String(ctx.value),
|
|
9799
|
-
placeholder,
|
|
9800
|
-
onInput: (e) => {
|
|
9801
|
-
const val = e.target.value;
|
|
9802
|
-
ctx.setValue(val === "" ? 0 : Number(val));
|
|
9803
|
-
}
|
|
9804
|
-
});
|
|
9805
|
-
}
|
|
9806
|
-
});
|
|
9980
|
+
defineInputMacro("numberbox", "number", (v) => v !== void 0 ? Number(v) : 0);
|
|
9807
9981
|
//#endregion
|
|
9808
9982
|
//#region src/components/macros/Textarea.tsx
|
|
9809
|
-
|
|
9810
|
-
name: "textarea",
|
|
9811
|
-
storeVar: true,
|
|
9812
|
-
render({ rawArgs }, ctx) {
|
|
9813
|
-
const { placeholder } = ctx.parseVarArgs(rawArgs);
|
|
9814
|
-
ctx.useAction({
|
|
9815
|
-
type: "textarea",
|
|
9816
|
-
key: `$${ctx.varName}`,
|
|
9817
|
-
authorId: ctx.id,
|
|
9818
|
-
label: placeholder || ctx.varName,
|
|
9819
|
-
variable: ctx.varName,
|
|
9820
|
-
value: ctx.value,
|
|
9821
|
-
perform: (v) => ctx.setValue(v !== void 0 ? String(v) : "")
|
|
9822
|
-
});
|
|
9823
|
-
return /* @__PURE__ */ jsx("textarea", {
|
|
9824
|
-
id: ctx.id,
|
|
9825
|
-
class: ctx.cls,
|
|
9826
|
-
value: ctx.value == null ? "" : String(ctx.value),
|
|
9827
|
-
placeholder,
|
|
9828
|
-
onInput: (e) => ctx.setValue(e.target.value)
|
|
9829
|
-
});
|
|
9830
|
-
}
|
|
9831
|
-
});
|
|
9983
|
+
defineInputMacro("textarea");
|
|
9832
9984
|
//#endregion
|
|
9833
9985
|
//#region src/components/macros/Checkbox.tsx
|
|
9834
|
-
function parseCheckboxLabel(rawArgs) {
|
|
9835
|
-
const match = rawArgs.match(/^\s*["']?\$?[\w.]+["']?\s+(.+)$/s);
|
|
9836
|
-
if (!match) return "";
|
|
9837
|
-
const rest = match[1].trimEnd() || match[1];
|
|
9838
|
-
return readWholeQuoted(rest) ?? stripLooseQuotes(rest);
|
|
9839
|
-
}
|
|
9840
9986
|
defineMacro({
|
|
9841
9987
|
name: "checkbox",
|
|
9842
9988
|
storeVar: true,
|
|
9843
|
-
|
|
9844
|
-
|
|
9989
|
+
parameters: [{
|
|
9990
|
+
name: "variable",
|
|
9991
|
+
type: "variable",
|
|
9992
|
+
required: true
|
|
9993
|
+
}, {
|
|
9994
|
+
name: "label",
|
|
9995
|
+
type: "text"
|
|
9996
|
+
}],
|
|
9997
|
+
render(_props, ctx) {
|
|
9998
|
+
const label = ctx.args.label ?? "";
|
|
9845
9999
|
ctx.useAction({
|
|
9846
10000
|
type: "checkbox",
|
|
9847
10001
|
key: `$${ctx.varName}`,
|
|
@@ -9864,38 +10018,28 @@ defineMacro({
|
|
|
9864
10018
|
});
|
|
9865
10019
|
//#endregion
|
|
9866
10020
|
//#region src/components/macros/Radiobutton.tsx
|
|
9867
|
-
function parseRadioArgs(rawArgs) {
|
|
9868
|
-
const head = rawArgs.match(/^\s*["']?\$?[\w.]+["']?\s+/);
|
|
9869
|
-
if (head) {
|
|
9870
|
-
const rest = rawArgs.slice(head[0].length).trim();
|
|
9871
|
-
const value = readQuoted(rest, 0);
|
|
9872
|
-
if (value && (value.end === rest.length || isWhitespace(rest[value.end]))) {
|
|
9873
|
-
const labelRaw = rest.slice(value.end).trim();
|
|
9874
|
-
const label = readWholeQuoted(labelRaw) ?? stripLooseQuotes(labelRaw);
|
|
9875
|
-
return {
|
|
9876
|
-
value: value.value,
|
|
9877
|
-
label
|
|
9878
|
-
};
|
|
9879
|
-
}
|
|
9880
|
-
}
|
|
9881
|
-
const match = rawArgs.match(/^\s*["']?\$?[\w.]+["']?\s+["'](.+?)["']\s+["']?(.+?)["']?\s*$/s);
|
|
9882
|
-
if (!match) {
|
|
9883
|
-
const parts = rawArgs.trim().split(/\s+/).slice(1);
|
|
9884
|
-
return {
|
|
9885
|
-
value: parts[0] ?? "",
|
|
9886
|
-
label: parts.slice(1).join(" ")
|
|
9887
|
-
};
|
|
9888
|
-
}
|
|
9889
|
-
return {
|
|
9890
|
-
value: match[1],
|
|
9891
|
-
label: match[2]
|
|
9892
|
-
};
|
|
9893
|
-
}
|
|
9894
10021
|
defineMacro({
|
|
9895
10022
|
name: "radiobutton",
|
|
9896
10023
|
storeVar: true,
|
|
9897
|
-
|
|
9898
|
-
|
|
10024
|
+
parameters: [
|
|
10025
|
+
{
|
|
10026
|
+
name: "variable",
|
|
10027
|
+
type: "variable",
|
|
10028
|
+
required: true
|
|
10029
|
+
},
|
|
10030
|
+
{
|
|
10031
|
+
name: "value",
|
|
10032
|
+
type: "text",
|
|
10033
|
+
required: true
|
|
10034
|
+
},
|
|
10035
|
+
{
|
|
10036
|
+
name: "label",
|
|
10037
|
+
type: "text"
|
|
10038
|
+
}
|
|
10039
|
+
],
|
|
10040
|
+
render(_props, ctx) {
|
|
10041
|
+
const radioValue = ctx.args.value ?? "";
|
|
10042
|
+
const label = ctx.args.label ?? "";
|
|
9899
10043
|
ctx.useAction({
|
|
9900
10044
|
type: "radiobutton",
|
|
9901
10045
|
key: `$${ctx.varName}:${radioValue}`,
|
|
@@ -9923,6 +10067,11 @@ defineMacro({
|
|
|
9923
10067
|
name: "listbox",
|
|
9924
10068
|
subMacros: ["option"],
|
|
9925
10069
|
storeVar: true,
|
|
10070
|
+
parameters: [{
|
|
10071
|
+
name: "variable",
|
|
10072
|
+
type: "variable",
|
|
10073
|
+
required: true
|
|
10074
|
+
}],
|
|
9926
10075
|
render({ children = [] }, ctx) {
|
|
9927
10076
|
const options = ctx.extractOptions(children);
|
|
9928
10077
|
ctx.useAction({
|
|
@@ -9955,6 +10104,11 @@ defineMacro({
|
|
|
9955
10104
|
name: "cycle",
|
|
9956
10105
|
block: true,
|
|
9957
10106
|
storeVar: true,
|
|
10107
|
+
parameters: [{
|
|
10108
|
+
name: "variable",
|
|
10109
|
+
type: "variable",
|
|
10110
|
+
required: true
|
|
10111
|
+
}],
|
|
9958
10112
|
render({ children = [] }, ctx) {
|
|
9959
10113
|
const options = ctx.extractOptions(children);
|
|
9960
10114
|
const handleClick = () => {
|
|
@@ -10191,6 +10345,16 @@ function setCSSProperties(el, config) {
|
|
|
10191
10345
|
el.style.setProperty("--passage-pause", `${config.pause / 1e3}s`);
|
|
10192
10346
|
}
|
|
10193
10347
|
/**
|
|
10348
|
+
* Set up `containerEl` for an outgoing transition and snapshot the passage
|
|
10349
|
+
* it shows, or return null if there is no container or passage.
|
|
10350
|
+
*/
|
|
10351
|
+
function snapshotPassage(containerEl, config) {
|
|
10352
|
+
if (!containerEl) return null;
|
|
10353
|
+
setCSSProperties(containerEl, config);
|
|
10354
|
+
const passageEl = containerEl.querySelector(".passage");
|
|
10355
|
+
return passageEl ? createSnapshot(passageEl, config.type) : null;
|
|
10356
|
+
}
|
|
10357
|
+
/**
|
|
10194
10358
|
* Remove all snapshot elements and crossfading class from a container.
|
|
10195
10359
|
*/
|
|
10196
10360
|
function cleanupSnapshots(containerEl) {
|
|
@@ -10258,18 +10422,12 @@ defineMacro({
|
|
|
10258
10422
|
setDisplayedPassage(currentPassage);
|
|
10259
10423
|
return;
|
|
10260
10424
|
}
|
|
10425
|
+
const snapshot = config.type === "fade-through" || config.type === "crossfade" ? snapshotPassage(containerEl, config) : null;
|
|
10426
|
+
if (!containerEl || !snapshot) {
|
|
10427
|
+
setDisplayedPassage(currentPassage);
|
|
10428
|
+
return;
|
|
10429
|
+
}
|
|
10261
10430
|
if (config.type === "fade-through") {
|
|
10262
|
-
if (!containerEl) {
|
|
10263
|
-
setDisplayedPassage(currentPassage);
|
|
10264
|
-
return;
|
|
10265
|
-
}
|
|
10266
|
-
setCSSProperties(containerEl, config);
|
|
10267
|
-
const passageEl = containerEl.querySelector(".passage");
|
|
10268
|
-
if (!passageEl) {
|
|
10269
|
-
setDisplayedPassage(currentPassage);
|
|
10270
|
-
return;
|
|
10271
|
-
}
|
|
10272
|
-
const snapshot = createSnapshot(passageEl, config.type);
|
|
10273
10431
|
containerEl.appendChild(snapshot);
|
|
10274
10432
|
setDisplayedPassage("");
|
|
10275
10433
|
const incomingDelay = config.duration + config.pause;
|
|
@@ -10282,27 +10440,12 @@ defineMacro({
|
|
|
10282
10440
|
}, cleanupDelay)];
|
|
10283
10441
|
return;
|
|
10284
10442
|
}
|
|
10285
|
-
|
|
10286
|
-
|
|
10287
|
-
setDisplayedPassage(currentPassage);
|
|
10288
|
-
return;
|
|
10289
|
-
}
|
|
10290
|
-
setCSSProperties(containerEl, config);
|
|
10291
|
-
const passageEl = containerEl.querySelector(".passage");
|
|
10292
|
-
if (!passageEl) {
|
|
10293
|
-
setDisplayedPassage(currentPassage);
|
|
10294
|
-
return;
|
|
10295
|
-
}
|
|
10296
|
-
const snapshot = createSnapshot(passageEl, config.type);
|
|
10297
|
-
containerEl.classList.add("passage-container--crossfading");
|
|
10298
|
-
containerEl.appendChild(snapshot);
|
|
10299
|
-
setDisplayedPassage(currentPassage);
|
|
10300
|
-
timeoutsRef.current = [setTimeout(() => {
|
|
10301
|
-
cleanupSnapshots(containerEl);
|
|
10302
|
-
}, config.duration)];
|
|
10303
|
-
return;
|
|
10304
|
-
}
|
|
10443
|
+
containerEl.classList.add("passage-container--crossfading");
|
|
10444
|
+
containerEl.appendChild(snapshot);
|
|
10305
10445
|
setDisplayedPassage(currentPassage);
|
|
10446
|
+
timeoutsRef.current = [setTimeout(() => {
|
|
10447
|
+
cleanupSnapshots(containerEl);
|
|
10448
|
+
}, config.duration)];
|
|
10306
10449
|
}, [navigationId]);
|
|
10307
10450
|
const effectivePassage = renderDeferred ? storyData?.passages.get("StoryLoading") ?? null : displayedPassage ? storyData?.passages.get(displayedPassage) : null;
|
|
10308
10451
|
if (!effectivePassage && displayedPassage && !renderDeferred) return /* @__PURE__ */ jsxs("div", {
|
|
@@ -10334,87 +10477,46 @@ defineMacro({
|
|
|
10334
10477
|
});
|
|
10335
10478
|
//#endregion
|
|
10336
10479
|
//#region src/components/macros/Watch.tsx
|
|
10337
|
-
var WORD_RE = /\w+/y;
|
|
10338
|
-
var DIGITS_RE = /\d+/y;
|
|
10339
|
-
/** Match the sticky regex `re` at `pos`, returning the matched text. */
|
|
10340
|
-
function matchAt(re, src, pos) {
|
|
10341
|
-
re.lastIndex = pos;
|
|
10342
|
-
return re.exec(src)?.[0] ?? null;
|
|
10343
|
-
}
|
|
10344
|
-
/**
|
|
10345
|
-
* Parse `{watch "condition" keyword value …}`: a quoted condition followed
|
|
10346
|
-
* by keyword options (`goto "X"`, `dialog "X"`, `run "X"`, `name "X"`,
|
|
10347
|
-
* `priority N`, `once`). Quoted strings accept `\"`, `\'` and `\\` escapes.
|
|
10348
|
-
* Returns `null` when the arguments do not start with a quoted condition.
|
|
10349
|
-
*/
|
|
10350
|
-
function parseWatchArgs(rawArgs) {
|
|
10351
|
-
const raw = rawArgs.trim();
|
|
10352
|
-
const cond = readQuoted(raw, 0);
|
|
10353
|
-
if (!cond) return null;
|
|
10354
|
-
const options = {};
|
|
10355
|
-
let i = cond.end;
|
|
10356
|
-
while (i < raw.length) {
|
|
10357
|
-
const key = matchAt(WORD_RE, raw, i);
|
|
10358
|
-
if (!key) {
|
|
10359
|
-
i = readQuoted(raw, i)?.end ?? i + 1;
|
|
10360
|
-
continue;
|
|
10361
|
-
}
|
|
10362
|
-
i += key.length;
|
|
10363
|
-
let val;
|
|
10364
|
-
let j = i;
|
|
10365
|
-
while (j < raw.length && isWhitespace(raw[j])) j++;
|
|
10366
|
-
if (j > i) {
|
|
10367
|
-
const quoted = readQuoted(raw, j);
|
|
10368
|
-
const digits = quoted ? null : matchAt(DIGITS_RE, raw, j);
|
|
10369
|
-
if (quoted) {
|
|
10370
|
-
val = quoted.value;
|
|
10371
|
-
i = quoted.end;
|
|
10372
|
-
} else if (digits) {
|
|
10373
|
-
val = digits;
|
|
10374
|
-
i = j + digits.length;
|
|
10375
|
-
}
|
|
10376
|
-
}
|
|
10377
|
-
switch (key) {
|
|
10378
|
-
case "goto":
|
|
10379
|
-
options.goto = val;
|
|
10380
|
-
break;
|
|
10381
|
-
case "dialog":
|
|
10382
|
-
options.dialog = val;
|
|
10383
|
-
break;
|
|
10384
|
-
case "run":
|
|
10385
|
-
options.run = val;
|
|
10386
|
-
break;
|
|
10387
|
-
case "name":
|
|
10388
|
-
options.name = val;
|
|
10389
|
-
break;
|
|
10390
|
-
case "priority":
|
|
10391
|
-
options.priority = val ? Number(val) : 0;
|
|
10392
|
-
break;
|
|
10393
|
-
case "once":
|
|
10394
|
-
options.once = true;
|
|
10395
|
-
break;
|
|
10396
|
-
}
|
|
10397
|
-
}
|
|
10398
|
-
return {
|
|
10399
|
-
condition: cond.value,
|
|
10400
|
-
options
|
|
10401
|
-
};
|
|
10402
|
-
}
|
|
10403
|
-
/**
|
|
10404
|
-
* Parse the watcher name of `{unwatch "name"}`: a quoted string, with the
|
|
10405
|
-
* same escapes as `{watch}`, or a bare name.
|
|
10406
|
-
*/
|
|
10407
|
-
function parseUnwatchName(rawArgs) {
|
|
10408
|
-
const raw = rawArgs.trim();
|
|
10409
|
-
return readWholeQuoted(raw) ?? raw.replace(/^['"]|['"]$/g, "");
|
|
10410
|
-
}
|
|
10411
10480
|
defineMacro({
|
|
10412
10481
|
name: "watch",
|
|
10413
|
-
|
|
10482
|
+
parameters: [{
|
|
10483
|
+
name: "condition",
|
|
10484
|
+
type: "string",
|
|
10485
|
+
required: true
|
|
10486
|
+
}, {
|
|
10487
|
+
name: "options",
|
|
10488
|
+
type: "options",
|
|
10489
|
+
parameters: [
|
|
10490
|
+
{
|
|
10491
|
+
name: "goto",
|
|
10492
|
+
type: "string"
|
|
10493
|
+
},
|
|
10494
|
+
{
|
|
10495
|
+
name: "dialog",
|
|
10496
|
+
type: "string"
|
|
10497
|
+
},
|
|
10498
|
+
{
|
|
10499
|
+
name: "run",
|
|
10500
|
+
type: "string"
|
|
10501
|
+
},
|
|
10502
|
+
{
|
|
10503
|
+
name: "name",
|
|
10504
|
+
type: "string"
|
|
10505
|
+
},
|
|
10506
|
+
{
|
|
10507
|
+
name: "priority",
|
|
10508
|
+
type: "number"
|
|
10509
|
+
},
|
|
10510
|
+
{
|
|
10511
|
+
name: "once",
|
|
10512
|
+
type: "flag"
|
|
10513
|
+
}
|
|
10514
|
+
]
|
|
10515
|
+
}],
|
|
10516
|
+
render(_props, ctx) {
|
|
10414
10517
|
const { hooks } = ctx;
|
|
10415
|
-
const
|
|
10416
|
-
if (
|
|
10417
|
-
const { condition, options } = parsed;
|
|
10518
|
+
const { condition, options } = ctx.args;
|
|
10519
|
+
if (condition === void 0) return null;
|
|
10418
10520
|
const registered = hooks.useRef(false);
|
|
10419
10521
|
if (!registered.current) {
|
|
10420
10522
|
registered.current = true;
|
|
@@ -10425,8 +10527,13 @@ defineMacro({
|
|
|
10425
10527
|
});
|
|
10426
10528
|
defineMacro({
|
|
10427
10529
|
name: "unwatch",
|
|
10428
|
-
|
|
10429
|
-
|
|
10530
|
+
parameters: [{
|
|
10531
|
+
name: "name",
|
|
10532
|
+
type: "text",
|
|
10533
|
+
required: true
|
|
10534
|
+
}],
|
|
10535
|
+
render(_props, ctx) {
|
|
10536
|
+
const name = ctx.args.name ?? "";
|
|
10430
10537
|
const ran = ctx.hooks.useRef(false);
|
|
10431
10538
|
if (!ran.current) {
|
|
10432
10539
|
ran.current = true;
|