@rohal12/spindle 0.52.6 → 0.52.8
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/README.md +26 -0
- package/dist/pkg/format.js +1 -1
- package/dist/pkg/headless.js +764 -735
- package/dist/pkg/macro-registry.json +18 -0
- package/dist/pkg/story-variables.js +54 -32
- package/package.json +1 -1
- package/src/action-registry.ts +9 -0
- package/src/class-registry.ts +1 -5
- package/src/components/Passage.tsx +35 -39
- package/src/components/PassageDialog.tsx +2 -2
- package/src/components/StoryInterface.tsx +2 -2
- package/src/components/macros/Checkbox.tsx +3 -8
- package/src/components/macros/Computed.tsx +1 -1
- package/src/components/macros/Cycle.tsx +3 -6
- package/src/components/macros/ExprDisplay.tsx +12 -29
- package/src/components/macros/If.tsx +35 -41
- package/src/components/macros/Include.tsx +2 -1
- package/src/components/macros/Listbox.tsx +5 -7
- package/src/components/macros/Print.tsx +3 -5
- package/src/components/macros/Radiobutton.tsx +3 -5
- package/src/components/macros/Repeat.tsx +2 -1
- package/src/components/macros/Switch.tsx +6 -13
- package/src/components/macros/Timed.tsx +14 -18
- package/src/components/macros/Type.tsx +2 -1
- package/src/components/macros/VarDisplay.tsx +12 -32
- package/src/components/macros/Widget.tsx +49 -20
- package/src/components/macros/branches.ts +18 -0
- package/src/components/macros/display.ts +19 -0
- package/src/components/macros/input-macro.ts +33 -11
- package/src/components/macros/macro-args.ts +7 -0
- package/src/components/macros/option-utils.ts +10 -7
- package/src/define-macro.ts +4 -6
- package/src/expression.ts +58 -40
- package/src/hooks/use-action.ts +7 -15
- package/src/index.tsx +5 -15
- package/src/interpolation.ts +34 -41
- package/src/js-lexer.ts +80 -45
- package/src/markup/ast.ts +4 -3
- package/src/markup/render.tsx +4 -12
- package/src/registry.ts +13 -12
- package/src/saves/save-manager.ts +18 -13
- package/src/saves/storage.ts +19 -20
- package/src/story-api.ts +30 -73
- package/src/structural.ts +1 -18
- package/src/types-drift-check.ts +37 -50
- package/src/utils/macro-names.ts +40 -0
- package/src/utils/namespace.ts +17 -0
- package/src/widgets/widget-registry.ts +5 -4
|
@@ -182,6 +182,13 @@
|
|
|
182
182
|
"next"
|
|
183
183
|
],
|
|
184
184
|
"interpolate": true,
|
|
185
|
+
"parameters": [
|
|
186
|
+
{
|
|
187
|
+
"name": "delay",
|
|
188
|
+
"type": "delay",
|
|
189
|
+
"required": true
|
|
190
|
+
}
|
|
191
|
+
],
|
|
185
192
|
"source": "builtin"
|
|
186
193
|
},
|
|
187
194
|
{
|
|
@@ -208,6 +215,17 @@
|
|
|
208
215
|
"name": "widget",
|
|
209
216
|
"block": true,
|
|
210
217
|
"subMacros": [],
|
|
218
|
+
"parameters": [
|
|
219
|
+
{
|
|
220
|
+
"name": "name",
|
|
221
|
+
"type": "text",
|
|
222
|
+
"required": true
|
|
223
|
+
},
|
|
224
|
+
{
|
|
225
|
+
"name": "parameters",
|
|
226
|
+
"type": "expression"
|
|
227
|
+
}
|
|
228
|
+
],
|
|
211
229
|
"source": "builtin"
|
|
212
230
|
},
|
|
213
231
|
{
|
|
@@ -248,6 +248,13 @@ var UNCLOSED = -1;
|
|
|
248
248
|
var MALFORMED = -2;
|
|
249
249
|
/** A frame result: the scan stops at index `s` inside it (`STOPPED - s`). */
|
|
250
250
|
var STOPPED = -3;
|
|
251
|
+
/**
|
|
252
|
+
* The result of a frame that closes at `end`. `bodyStarted`: the code inside
|
|
253
|
+
* started a function or class body. That body replaced the one to come, and
|
|
254
|
+
* is gone once the frame is closed, so after the frame no body is to come —
|
|
255
|
+
* a scan that skips the frame must know, as must the frames around it.
|
|
256
|
+
*/
|
|
257
|
+
var frameEnd = (end, bodyStarted) => end * 2 + +bodyStarted;
|
|
251
258
|
/** The closers whose search for their frame a frame result may depend on. */
|
|
252
259
|
var CLOSERS = [
|
|
253
260
|
")",
|
|
@@ -480,14 +487,30 @@ function scan(src, handlers, start, nesting, outer, ctx) {
|
|
|
480
487
|
]] : void 0;
|
|
481
488
|
/**
|
|
482
489
|
* How many function or class bodies to come the scan has started, and with
|
|
483
|
-
* `parens`, that count when each open frame opened. Code in a
|
|
484
|
-
*
|
|
485
|
-
* follow once the frame is closed: none is to come then.
|
|
490
|
+
* `braces` or `parens`, that count when each open frame opened. Code in a
|
|
491
|
+
* frame that starts one replaces the one to come, and the new one can't
|
|
492
|
+
* follow once the frame is closed: none is to come then. A scan skipping
|
|
493
|
+
* the frame learns this from its result (`frameEnd`, `resume`), and counts
|
|
494
|
+
* the body as started too, for the frames around it to record.
|
|
486
495
|
*/
|
|
487
496
|
let bodyStarts = 0;
|
|
488
|
-
const startsAt = parens ? [0] : void 0;
|
|
497
|
+
const startsAt = braces || parens ? [0] : void 0;
|
|
489
498
|
/** A `(…)` or `[…]` frame result found in `parens`, to skip to. */
|
|
490
499
|
let parenTo;
|
|
500
|
+
/** The result of the frame at index `k`, closing at `i`. */
|
|
501
|
+
const closedAt = (k) => frameEnd(i, startsAt !== void 0 && bodyStarts !== startsAt[k]);
|
|
502
|
+
/**
|
|
503
|
+
* Go on after a frame an earlier scan lexed, whose result (`frameEnd`) is
|
|
504
|
+
* `result`: returns the index it closes at. A body started in it is gone
|
|
505
|
+
* after it, with the one it replaced.
|
|
506
|
+
*/
|
|
507
|
+
function resume(result) {
|
|
508
|
+
if (result % 2 === 1) {
|
|
509
|
+
pendingBody = void 0;
|
|
510
|
+
bodyStarts++;
|
|
511
|
+
}
|
|
512
|
+
return Math.floor(result / 2);
|
|
513
|
+
}
|
|
491
514
|
/** The closer `c` looked for its frame and found index `k`. */
|
|
492
515
|
function lookedFor(c, k) {
|
|
493
516
|
if (!lowest) return;
|
|
@@ -635,8 +658,8 @@ function scan(src, handlers, start, nesting, outer, ctx) {
|
|
|
635
658
|
if (lowest) {
|
|
636
659
|
for (let j = frames.length - 1; j >= k; j--) foldLowest(j);
|
|
637
660
|
lowest.length = k;
|
|
638
|
-
startsAt.length = k;
|
|
639
661
|
}
|
|
662
|
+
if (startsAt) startsAt.length = k;
|
|
640
663
|
frames.length = k;
|
|
641
664
|
if (stackIds) stackIds.length = k;
|
|
642
665
|
for (const list of Object.values(closers)) while (list.length && list[list.length - 1] >= k) list.pop();
|
|
@@ -713,6 +736,18 @@ function scan(src, handlers, start, nesting, outer, ctx) {
|
|
|
713
736
|
stmtNext = f.kind === "block";
|
|
714
737
|
keyNext = f.kind === "object" || f.kind === "class";
|
|
715
738
|
}
|
|
739
|
+
/**
|
|
740
|
+
* Go past frame `f` if an earlier scan lexed it: to just after it, or to
|
|
741
|
+
* the end of the source when it never closes. Whether it did, or
|
|
742
|
+
* MALFORMED when that scan found the code malformed.
|
|
743
|
+
*/
|
|
744
|
+
function skipLexed(f) {
|
|
745
|
+
const end = known(f);
|
|
746
|
+
if (end === void 0) return false;
|
|
747
|
+
if (end === MALFORMED) return MALFORMED;
|
|
748
|
+
i = end === UNCLOSED ? src.length : resume(end) + 1;
|
|
749
|
+
return true;
|
|
750
|
+
}
|
|
716
751
|
function openBrace() {
|
|
717
752
|
let f;
|
|
718
753
|
if (pendingBody?.depth === frames.length) {
|
|
@@ -756,10 +791,10 @@ function scan(src, handlers, start, nesting, outer, ctx) {
|
|
|
756
791
|
return;
|
|
757
792
|
}
|
|
758
793
|
const closed = frames[k];
|
|
794
|
+
const result = closedAt(k);
|
|
759
795
|
if (lowest && c !== "}") {
|
|
760
796
|
for (let j = frames.length - 1; j > k; j--) foldLowest(j);
|
|
761
|
-
|
|
762
|
-
recordParen(k, i * 2 + +bodyStarted);
|
|
797
|
+
recordParen(k, result);
|
|
763
798
|
}
|
|
764
799
|
truncate(k);
|
|
765
800
|
if (c === "]" && !ctx.lookahead) ctx.brackets.set(closed.open, i);
|
|
@@ -768,7 +803,7 @@ function scan(src, handlers, start, nesting, outer, ctx) {
|
|
|
768
803
|
stmtNext = closed.header;
|
|
769
804
|
} else if (c === "]") endPunct(c, false);
|
|
770
805
|
else {
|
|
771
|
-
record(closed,
|
|
806
|
+
record(closed, result);
|
|
772
807
|
afterBrace(closed);
|
|
773
808
|
}
|
|
774
809
|
}
|
|
@@ -870,23 +905,16 @@ function scan(src, handlers, start, nesting, outer, ctx) {
|
|
|
870
905
|
} else if (ch === "`") {
|
|
871
906
|
literal(ch, i);
|
|
872
907
|
if (frames.length === 1) return i + 1;
|
|
873
|
-
record(top(),
|
|
908
|
+
record(top(), closedAt(frames.length - 1));
|
|
874
909
|
i++;
|
|
875
910
|
truncate(frames.length - 1);
|
|
876
911
|
endOperand();
|
|
877
912
|
} else if (ch === "$" && src.charAt(i + 1) === "{") {
|
|
878
913
|
const f = frame("expr", "}", i);
|
|
879
914
|
f.interpolation = true;
|
|
880
|
-
const
|
|
881
|
-
if (
|
|
882
|
-
if (
|
|
883
|
-
i = src.length;
|
|
884
|
-
break;
|
|
885
|
-
}
|
|
886
|
-
if (end !== void 0) {
|
|
887
|
-
i = end + 1;
|
|
888
|
-
continue;
|
|
889
|
-
}
|
|
915
|
+
const skipped = skipLexed(f);
|
|
916
|
+
if (skipped === MALFORMED) return malformed();
|
|
917
|
+
if (skipped) continue;
|
|
890
918
|
literal("${", i);
|
|
891
919
|
i += 2;
|
|
892
920
|
nesting++;
|
|
@@ -913,14 +941,9 @@ function scan(src, handlers, start, nesting, outer, ctx) {
|
|
|
913
941
|
if (ch === "`") {
|
|
914
942
|
endWord();
|
|
915
943
|
const f = frame("template", "`", i);
|
|
916
|
-
const
|
|
917
|
-
if (
|
|
918
|
-
if (
|
|
919
|
-
i = src.length;
|
|
920
|
-
break;
|
|
921
|
-
}
|
|
922
|
-
if (end !== void 0) {
|
|
923
|
-
i = end + 1;
|
|
944
|
+
const skipped = skipLexed(f);
|
|
945
|
+
if (skipped === MALFORMED) return malformed();
|
|
946
|
+
if (skipped) {
|
|
924
947
|
endOperand();
|
|
925
948
|
continue;
|
|
926
949
|
}
|
|
@@ -939,7 +962,7 @@ function scan(src, handlers, start, nesting, outer, ctx) {
|
|
|
939
962
|
lookedFor("}", k);
|
|
940
963
|
if (frames[k].interpolation) {
|
|
941
964
|
endWord();
|
|
942
|
-
record(frames[k],
|
|
965
|
+
record(frames[k], closedAt(k));
|
|
943
966
|
truncate(k);
|
|
944
967
|
nesting--;
|
|
945
968
|
literal(ch, i);
|
|
@@ -1021,7 +1044,7 @@ function scan(src, handlers, start, nesting, outer, ctx) {
|
|
|
1021
1044
|
continue;
|
|
1022
1045
|
}
|
|
1023
1046
|
}
|
|
1024
|
-
if (ch === outer.closer && frames.length === 1) break;
|
|
1047
|
+
if (ch === outer.closer && (frames.length === 1 || ch === "]" && innermost("]") === 0 && innermost("}") === 0)) break;
|
|
1025
1048
|
trackCode(ch);
|
|
1026
1049
|
if (skipTo) {
|
|
1027
1050
|
const { frame: skipped, end } = skipTo;
|
|
@@ -1031,7 +1054,7 @@ function scan(src, handlers, start, nesting, outer, ctx) {
|
|
|
1031
1054
|
i = src.length;
|
|
1032
1055
|
break;
|
|
1033
1056
|
}
|
|
1034
|
-
i = end;
|
|
1057
|
+
i = resume(end);
|
|
1035
1058
|
afterBrace(skipped);
|
|
1036
1059
|
i++;
|
|
1037
1060
|
continue;
|
|
@@ -1049,8 +1072,7 @@ function scan(src, handlers, start, nesting, outer, ctx) {
|
|
|
1049
1072
|
strict.stopped = true;
|
|
1050
1073
|
return STOPPED - result;
|
|
1051
1074
|
}
|
|
1052
|
-
i =
|
|
1053
|
-
if (result % 2 === 1) pendingBody = void 0;
|
|
1075
|
+
i = resume(result);
|
|
1054
1076
|
if (skipped.closer === ")") {
|
|
1055
1077
|
endPunct(")", skipped.header);
|
|
1056
1078
|
stmtNext = skipped.header;
|
package/package.json
CHANGED
package/src/action-registry.ts
CHANGED
|
@@ -29,6 +29,15 @@ export interface StoryAction {
|
|
|
29
29
|
perform: (value?: unknown) => void;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
/** The fields of a StoryAction that are left out when unset. */
|
|
33
|
+
export const OPTIONAL_ACTION_FIELDS = [
|
|
34
|
+
'target',
|
|
35
|
+
'variable',
|
|
36
|
+
'options',
|
|
37
|
+
'value',
|
|
38
|
+
'disabled',
|
|
39
|
+
] as const;
|
|
40
|
+
|
|
32
41
|
/** A mounted control's hold on its action ID. */
|
|
33
42
|
export interface ActionRegistration {
|
|
34
43
|
/**
|
package/src/class-registry.ts
CHANGED
|
@@ -1,10 +1,6 @@
|
|
|
1
1
|
// Class registry for preserving class instances across clone/save/load cycles.
|
|
2
2
|
|
|
3
|
-
import { setOwn } from './
|
|
4
|
-
import { hasOwn } from './utils/namespace';
|
|
5
|
-
|
|
6
|
-
// Copying and comparing story values live with the structural diff
|
|
7
|
-
export { deepClone, deepEqual, type DeepCloneOptions } from './structural';
|
|
3
|
+
import { hasOwn, setOwn } from './utils/namespace';
|
|
8
4
|
|
|
9
5
|
type Constructor = new (...args: any[]) => any;
|
|
10
6
|
|
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
import { tokenize } from '../markup/tokenizer';
|
|
9
9
|
import { buildAST, type ASTNode } from '../markup/ast';
|
|
10
10
|
import { renderNodes, NobrContext } from '../markup/render';
|
|
11
|
-
import {
|
|
11
|
+
import { useStoryFields } from '../hooks/use-story-fields';
|
|
12
12
|
import type { Passage as PassageData } from '../parser';
|
|
13
13
|
import { sourceLocationOf } from '../utils/source-location';
|
|
14
14
|
import { emitFromRender } from '../event-emitter';
|
|
@@ -40,6 +40,21 @@ export function renderPassageContent(passage: PassageData) {
|
|
|
40
40
|
return renderNodes(ast, nobr ? { nobr: true } : undefined);
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
/**
|
|
44
|
+
* The content of a special passage shown with every passage (PassageHeader,
|
|
45
|
+
* PassageFooter, PassageDone), or null. One that fails to render is logged
|
|
46
|
+
* and left out, so it can't take the passage down with it.
|
|
47
|
+
*/
|
|
48
|
+
function renderSpecialPassage(passage: PassageData | undefined) {
|
|
49
|
+
if (!passage) return null;
|
|
50
|
+
try {
|
|
51
|
+
return renderPassageContent(passage);
|
|
52
|
+
} catch (err) {
|
|
53
|
+
console.error(`spindle: Error in ${passage.name}:`, err);
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
43
58
|
interface PassageProps {
|
|
44
59
|
passage: PassageData;
|
|
45
60
|
dataTransition?: string;
|
|
@@ -59,7 +74,7 @@ export function Passage({
|
|
|
59
74
|
dataTransition,
|
|
60
75
|
navigationId,
|
|
61
76
|
}: PassageProps) {
|
|
62
|
-
const storyData =
|
|
77
|
+
const { storyData } = useStoryFields('storyData');
|
|
63
78
|
const isCodePassage = CODE_PASSAGES.has(passage.name);
|
|
64
79
|
const [doneReady, setDoneReady] = useState(false);
|
|
65
80
|
|
|
@@ -76,35 +91,21 @@ export function Passage({
|
|
|
76
91
|
}
|
|
77
92
|
}, [passage.content, passage.name]);
|
|
78
93
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
: storyData?.passages.get(
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
const donePassage = isCodePassage
|
|
86
|
-
? undefined
|
|
87
|
-
: storyData?.passages.get('PassageDone');
|
|
88
|
-
|
|
89
|
-
const headerContent = useMemo(() => {
|
|
90
|
-
if (!headerPassage) return null;
|
|
91
|
-
try {
|
|
92
|
-
return renderPassageContent(headerPassage);
|
|
93
|
-
} catch (err) {
|
|
94
|
-
console.error('spindle: Error in PassageHeader:', err);
|
|
95
|
-
return null;
|
|
96
|
-
}
|
|
97
|
-
}, [headerPassage?.content]);
|
|
94
|
+
// Code passages are shown without the special passages
|
|
95
|
+
const special = (name: string) =>
|
|
96
|
+
isCodePassage ? undefined : storyData?.passages.get(name);
|
|
97
|
+
const headerPassage = special('PassageHeader');
|
|
98
|
+
const footerPassage = special('PassageFooter');
|
|
99
|
+
const donePassage = special('PassageDone');
|
|
98
100
|
|
|
99
|
-
const
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
}, [footerPassage?.content]);
|
|
101
|
+
const headerContent = useMemo(
|
|
102
|
+
() => renderSpecialPassage(headerPassage),
|
|
103
|
+
[headerPassage?.content],
|
|
104
|
+
);
|
|
105
|
+
const footerContent = useMemo(
|
|
106
|
+
() => renderSpecialPassage(footerPassage),
|
|
107
|
+
[footerPassage?.content],
|
|
108
|
+
);
|
|
108
109
|
|
|
109
110
|
// Defer PassageDone to after DOM commit
|
|
110
111
|
useEffect(() => {
|
|
@@ -112,15 +113,10 @@ export function Passage({
|
|
|
112
113
|
return () => setDoneReady(false);
|
|
113
114
|
}, [passage.name]);
|
|
114
115
|
|
|
115
|
-
const doneContent = useMemo(
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
} catch (err) {
|
|
120
|
-
console.error('spindle: Error in PassageDone:', err);
|
|
121
|
-
return null;
|
|
122
|
-
}
|
|
123
|
-
}, [doneReady, donePassage?.content]);
|
|
116
|
+
const doneContent = useMemo(
|
|
117
|
+
() => (doneReady ? renderSpecialPassage(donePassage) : null),
|
|
118
|
+
[doneReady, donePassage?.content],
|
|
119
|
+
);
|
|
124
120
|
|
|
125
121
|
// Signal that this passage's DOM is committed (after descendants' layout
|
|
126
122
|
// effects, before paint). Runs once per mount: PassageDisplay keys the
|
|
@@ -3,7 +3,7 @@ import { useCallback, useLayoutEffect, useMemo, useRef } from 'preact/hooks';
|
|
|
3
3
|
import { tokenize } from '../markup/tokenizer';
|
|
4
4
|
import { buildAST } from '../markup/ast';
|
|
5
5
|
import { renderNodes, NobrContext } from '../markup/render';
|
|
6
|
-
import {
|
|
6
|
+
import { useStoryFields } from '../hooks/use-story-fields';
|
|
7
7
|
import { emitFromRender } from '../event-emitter';
|
|
8
8
|
import { useModalFocus } from '../hooks/use-modal-focus';
|
|
9
9
|
import { errorMessage } from '../utils/error-message';
|
|
@@ -38,7 +38,7 @@ export function PassageDialog({
|
|
|
38
38
|
onCloseRef.current = onClose;
|
|
39
39
|
const stableOnClose = useCallback(() => onCloseRef.current(), []);
|
|
40
40
|
|
|
41
|
-
const storyData =
|
|
41
|
+
const { storyData } = useStoryFields('storyData');
|
|
42
42
|
|
|
43
43
|
const passage = passageName
|
|
44
44
|
? storyData?.passages.get(passageName)
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useMemo } from 'preact/hooks';
|
|
2
|
-
import {
|
|
2
|
+
import { useStoryFields } from '../hooks/use-story-fields';
|
|
3
3
|
import { tokenize } from '../markup/tokenizer';
|
|
4
4
|
import { buildAST } from '../markup/ast';
|
|
5
5
|
import { renderInlineNodes, NobrContext } from '../markup/render';
|
|
@@ -9,7 +9,7 @@ const DEFAULT_MARKUP =
|
|
|
9
9
|
'<header class="story-menubar">{story-title}{back}{forward}{restart}{quicksave}{quickload}{saves}{settings}</header>\n{passage}';
|
|
10
10
|
|
|
11
11
|
export function StoryInterface() {
|
|
12
|
-
const storyData =
|
|
12
|
+
const { storyData } = useStoryFields('storyData');
|
|
13
13
|
|
|
14
14
|
const overridePassage = storyData?.passages.get('StoryInterface');
|
|
15
15
|
const markup =
|
|
@@ -1,21 +1,16 @@
|
|
|
1
1
|
import { defineMacro } from '../../define-macro';
|
|
2
|
+
import { VARIABLE_PARAMETER, useVariableAction } from './input-macro';
|
|
2
3
|
|
|
3
4
|
defineMacro({
|
|
4
5
|
name: 'checkbox',
|
|
5
6
|
storeVar: true,
|
|
6
|
-
parameters: [
|
|
7
|
-
{ name: 'variable', type: 'variable', required: true },
|
|
8
|
-
{ name: 'label', type: 'text' },
|
|
9
|
-
],
|
|
7
|
+
parameters: [VARIABLE_PARAMETER, { name: 'label', type: 'text' }],
|
|
10
8
|
render(_props, ctx) {
|
|
11
9
|
const label = ctx.args.label ?? '';
|
|
12
10
|
|
|
13
|
-
ctx
|
|
11
|
+
useVariableAction(ctx, {
|
|
14
12
|
type: 'checkbox',
|
|
15
|
-
key: `$${ctx.varName}`,
|
|
16
|
-
authorId: ctx.id,
|
|
17
13
|
label: label || ctx.varName || '',
|
|
18
|
-
variable: ctx.varName,
|
|
19
14
|
value: !!ctx.value,
|
|
20
15
|
perform: (v) => ctx.setValue!(v !== undefined ? !!v : !ctx.value),
|
|
21
16
|
});
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { useStoryStore } from '../../store';
|
|
2
2
|
import { evaluate } from '../../expression';
|
|
3
3
|
import { readState } from '../../execute-mutation';
|
|
4
|
-
import { deepEqual } from '../../
|
|
4
|
+
import { deepEqual } from '../../structural';
|
|
5
5
|
import { defineMacro } from '../../define-macro';
|
|
6
6
|
import { MacroError, logMacroError } from './MacroError';
|
|
7
7
|
import { checkVariableName } from '../../utils/namespace';
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { defineMacro } from '../../define-macro';
|
|
2
|
+
import { VARIABLE_PARAMETER, useVariableAction } from './input-macro';
|
|
2
3
|
|
|
3
4
|
defineMacro({
|
|
4
5
|
name: 'cycle',
|
|
5
6
|
block: true,
|
|
6
7
|
storeVar: true,
|
|
7
|
-
parameters: [
|
|
8
|
+
parameters: [VARIABLE_PARAMETER],
|
|
8
9
|
render({ children = [] }, ctx) {
|
|
9
10
|
const options = ctx.extractOptions(children);
|
|
10
11
|
|
|
@@ -16,14 +17,10 @@ defineMacro({
|
|
|
16
17
|
ctx.setValue!(options[nextIndex]);
|
|
17
18
|
};
|
|
18
19
|
|
|
19
|
-
ctx
|
|
20
|
+
useVariableAction(ctx, {
|
|
20
21
|
type: 'cycle',
|
|
21
|
-
key: `$${ctx.varName}`,
|
|
22
|
-
authorId: ctx.id,
|
|
23
22
|
label: ctx.value == null ? options[0] || '' : String(ctx.value),
|
|
24
|
-
variable: ctx.varName,
|
|
25
23
|
options,
|
|
26
|
-
value: ctx.value,
|
|
27
24
|
perform: (v) => {
|
|
28
25
|
if (v !== undefined) {
|
|
29
26
|
ctx.setValue!(v);
|
|
@@ -1,41 +1,24 @@
|
|
|
1
1
|
import { evaluate } from '../../expression';
|
|
2
2
|
import { useInterpolate } from '../../hooks/use-interpolate';
|
|
3
3
|
import { useMergedLocals } from '../../hooks/use-merged-locals';
|
|
4
|
+
import { display, wrapContent } from './display';
|
|
5
|
+
import type { ExpressionNode } from '../../markup/ast';
|
|
4
6
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
id?: string;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export function ExprDisplay({ expression, className, id }: ExprDisplayProps) {
|
|
7
|
+
/** An expression display: `{$hp + 1}`. */
|
|
8
|
+
export function ExprDisplay({ node }: { node: ExpressionNode }) {
|
|
9
|
+
const { expression } = node;
|
|
12
10
|
const resolve = useInterpolate();
|
|
13
|
-
className = resolve(className);
|
|
14
|
-
id = resolve(id);
|
|
11
|
+
const className = resolve(node.className);
|
|
12
|
+
const id = resolve(node.id);
|
|
15
13
|
const [variables, temporary, localsValues, transient] = useMergedLocals();
|
|
16
14
|
|
|
17
|
-
let
|
|
15
|
+
let text: string;
|
|
18
16
|
try {
|
|
19
|
-
|
|
20
|
-
expression,
|
|
21
|
-
variables,
|
|
22
|
-
temporary,
|
|
23
|
-
localsValues,
|
|
24
|
-
transient,
|
|
17
|
+
text = display(
|
|
18
|
+
evaluate(expression, variables, temporary, localsValues, transient),
|
|
25
19
|
);
|
|
26
|
-
display = value == null ? '' : String(value);
|
|
27
20
|
} catch {
|
|
28
|
-
|
|
21
|
+
text = `{error: ${expression}}`;
|
|
29
22
|
}
|
|
30
|
-
|
|
31
|
-
if (className || id)
|
|
32
|
-
return (
|
|
33
|
-
<span
|
|
34
|
-
id={id}
|
|
35
|
-
class={className}
|
|
36
|
-
>
|
|
37
|
-
{display}
|
|
38
|
-
</span>
|
|
39
|
-
);
|
|
40
|
-
return <>{display}</>;
|
|
23
|
+
return wrapContent(className, id, text);
|
|
41
24
|
}
|
|
@@ -1,6 +1,23 @@
|
|
|
1
1
|
import { defineMacro } from '../../define-macro';
|
|
2
2
|
import { MacroError } from './MacroError';
|
|
3
3
|
import type { Branch } from '../../markup/ast';
|
|
4
|
+
import { selectBranch } from './branches';
|
|
5
|
+
import { wrapContent } from './display';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The {if}/{elseif}/{else} branch whose condition holds, else the {else}
|
|
9
|
+
* branch, else null: the chain ends at its first bare branch. Throws what a
|
|
10
|
+
* condition throws.
|
|
11
|
+
*/
|
|
12
|
+
function selectIfBranch(
|
|
13
|
+
branches: Branch[],
|
|
14
|
+
evaluate: (expr: string) => unknown,
|
|
15
|
+
): Branch | null {
|
|
16
|
+
const end = branches.findIndex((branch) => branch.rawArgs === '');
|
|
17
|
+
return selectBranch(end < 0 ? branches : branches.slice(0, end + 1), (expr) =>
|
|
18
|
+
Boolean(evaluate(expr)),
|
|
19
|
+
);
|
|
20
|
+
}
|
|
4
21
|
|
|
5
22
|
defineMacro({
|
|
6
23
|
name: 'if',
|
|
@@ -8,49 +25,26 @@ defineMacro({
|
|
|
8
25
|
interpolate: true,
|
|
9
26
|
merged: true,
|
|
10
27
|
render({ branches = [] }, ctx) {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
{children}
|
|
22
|
-
</span>
|
|
23
|
-
);
|
|
24
|
-
return <>{children}</>;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
for (const branch of branches) {
|
|
28
|
-
if (branch.rawArgs === '') {
|
|
29
|
-
return renderBranch(branch);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
try {
|
|
33
|
-
if (ctx.evaluate!(branch.rawArgs)) {
|
|
34
|
-
return renderBranch(branch);
|
|
35
|
-
}
|
|
36
|
-
} catch (err) {
|
|
37
|
-
return (
|
|
38
|
-
<MacroError
|
|
39
|
-
macro="if"
|
|
40
|
-
error={err}
|
|
41
|
-
/>
|
|
42
|
-
);
|
|
43
|
-
}
|
|
28
|
+
let branch: Branch | null;
|
|
29
|
+
try {
|
|
30
|
+
branch = selectIfBranch(branches, ctx.evaluate!);
|
|
31
|
+
} catch (err) {
|
|
32
|
+
return (
|
|
33
|
+
<MacroError
|
|
34
|
+
macro="if"
|
|
35
|
+
error={err}
|
|
36
|
+
/>
|
|
37
|
+
);
|
|
44
38
|
}
|
|
45
|
-
|
|
46
|
-
return
|
|
39
|
+
if (!branch) return null;
|
|
40
|
+
return wrapContent(
|
|
41
|
+
ctx.resolve!(branch.className),
|
|
42
|
+
ctx.resolve!(branch.id),
|
|
43
|
+
ctx.renderNodes(branch.children),
|
|
44
|
+
);
|
|
47
45
|
},
|
|
48
46
|
text({ branches = [] }, ctx) {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
return ctx.renderText(branch.children);
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
return '';
|
|
47
|
+
const branch = selectIfBranch(branches, ctx.evaluate);
|
|
48
|
+
return branch ? ctx.renderText(branch.children) : '';
|
|
55
49
|
},
|
|
56
50
|
});
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { useStoryStore } from '../../store';
|
|
2
|
+
import { useStoryFields } from '../../hooks/use-story-fields';
|
|
2
3
|
import { tokenize } from '../../markup/tokenizer';
|
|
3
4
|
import { buildAST } from '../../markup/ast';
|
|
4
5
|
import { NobrContext } from '../../markup/render';
|
|
@@ -16,7 +17,7 @@ defineMacro({
|
|
|
16
17
|
{ name: 'passage', type: 'expression', required: true },
|
|
17
18
|
],
|
|
18
19
|
render(_props, ctx) {
|
|
19
|
-
const storyData =
|
|
20
|
+
const { storyData } = useStoryFields('storyData');
|
|
20
21
|
|
|
21
22
|
const { inline } = ctx.args;
|
|
22
23
|
const passageName = evaluatePassageName(ctx.args.passage, ctx.evaluate!);
|
|
@@ -1,21 +1,19 @@
|
|
|
1
1
|
import { defineMacro } from '../../define-macro';
|
|
2
|
+
import { VARIABLE_PARAMETER, useVariableAction } from './input-macro';
|
|
3
|
+
import { display } from './display';
|
|
2
4
|
|
|
3
5
|
defineMacro({
|
|
4
6
|
name: 'listbox',
|
|
5
7
|
subMacros: ['option'],
|
|
6
8
|
storeVar: true,
|
|
7
|
-
parameters: [
|
|
9
|
+
parameters: [VARIABLE_PARAMETER],
|
|
8
10
|
render({ children = [] }, ctx) {
|
|
9
11
|
const options = ctx.extractOptions(children);
|
|
10
12
|
|
|
11
|
-
ctx
|
|
13
|
+
useVariableAction(ctx, {
|
|
12
14
|
type: 'listbox',
|
|
13
|
-
key: `$${ctx.varName}`,
|
|
14
|
-
authorId: ctx.id,
|
|
15
15
|
label: ctx.varName!,
|
|
16
|
-
variable: ctx.varName,
|
|
17
16
|
options,
|
|
18
|
-
value: ctx.value,
|
|
19
17
|
perform: (v) => {
|
|
20
18
|
if (v !== undefined) ctx.setValue!(String(v));
|
|
21
19
|
},
|
|
@@ -25,7 +23,7 @@ defineMacro({
|
|
|
25
23
|
<select
|
|
26
24
|
id={ctx.id}
|
|
27
25
|
class={ctx.cls}
|
|
28
|
-
value={
|
|
26
|
+
value={display(ctx.value)}
|
|
29
27
|
onChange={(e) => ctx.setValue!((e.target as HTMLSelectElement).value)}
|
|
30
28
|
>
|
|
31
29
|
{options.map((opt) => (
|