@rohal12/spindle 0.7.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/pkg/format.js +1 -1
- package/package.json +3 -1
- package/src/components/macros/Button.tsx +13 -2
- package/src/components/macros/Computed.tsx +2 -2
- package/src/components/macros/Do.tsx +12 -2
- package/src/components/macros/For.tsx +52 -12
- package/src/components/macros/Goto.tsx +5 -3
- package/src/components/macros/If.tsx +7 -2
- package/src/components/macros/Include.tsx +3 -3
- package/src/components/macros/MacroLink.tsx +20 -5
- package/src/components/macros/Meter.tsx +7 -3
- package/src/components/macros/Print.tsx +2 -2
- package/src/components/macros/Set.tsx +16 -3
- package/src/components/macros/Switch.tsx +11 -4
- package/src/components/macros/VarDisplay.tsx +14 -6
- package/src/components/macros/Widget.tsx +1 -3
- package/src/components/macros/WidgetInvocation.tsx +39 -9
- package/src/expression.ts +10 -4
- package/src/hooks/use-merged-locals.ts +12 -10
- package/src/markup/ast.ts +1 -1
- package/src/markup/render.tsx +11 -1
- package/src/markup/tokenizer.ts +55 -1
- package/src/story-variables.ts +5 -5
package/src/expression.ts
CHANGED
|
@@ -21,6 +21,7 @@ interface ExpressionFns {
|
|
|
21
21
|
type CompiledExpression = (
|
|
22
22
|
variables: Record<string, unknown>,
|
|
23
23
|
temporary: Record<string, unknown>,
|
|
24
|
+
locals: Record<string, unknown>,
|
|
24
25
|
__fns: ExpressionFns,
|
|
25
26
|
) => unknown;
|
|
26
27
|
|
|
@@ -34,11 +35,13 @@ const fnCache = new Map<string, CompiledExpression>();
|
|
|
34
35
|
*/
|
|
35
36
|
const VAR_RE = /\$(\w+)/g;
|
|
36
37
|
const TEMP_RE = /\b_(\w+)/g;
|
|
38
|
+
const LOCAL_RE = /@(\w+)/g;
|
|
37
39
|
|
|
38
40
|
function transform(expr: string): string {
|
|
39
41
|
return expr
|
|
40
42
|
.replace(VAR_RE, 'variables["$1"]')
|
|
41
|
-
.replace(TEMP_RE, 'temporary["$1"]')
|
|
43
|
+
.replace(TEMP_RE, 'temporary["$1"]')
|
|
44
|
+
.replace(LOCAL_RE, 'locals["$1"]');
|
|
42
45
|
}
|
|
43
46
|
|
|
44
47
|
const preamble =
|
|
@@ -55,6 +58,7 @@ function getOrCompile(key: string, body: string): CompiledExpression {
|
|
|
55
58
|
const fn = new Function(
|
|
56
59
|
'variables',
|
|
57
60
|
'temporary',
|
|
61
|
+
'locals',
|
|
58
62
|
'__fns',
|
|
59
63
|
preamble + body,
|
|
60
64
|
) as CompiledExpression;
|
|
@@ -136,11 +140,12 @@ export function evaluate(
|
|
|
136
140
|
expr: string,
|
|
137
141
|
variables: Record<string, unknown>,
|
|
138
142
|
temporary: Record<string, unknown>,
|
|
143
|
+
locals: Record<string, unknown> = {},
|
|
139
144
|
): unknown {
|
|
140
145
|
const transformed = transform(expr);
|
|
141
146
|
const body = `return (${transformed});`;
|
|
142
147
|
const fn = getOrCompile(body, body);
|
|
143
|
-
return fn(variables, temporary, buildExpressionFns());
|
|
148
|
+
return fn(variables, temporary, locals, buildExpressionFns());
|
|
144
149
|
}
|
|
145
150
|
|
|
146
151
|
/**
|
|
@@ -151,15 +156,16 @@ export function execute(
|
|
|
151
156
|
code: string,
|
|
152
157
|
variables: Record<string, unknown>,
|
|
153
158
|
temporary: Record<string, unknown>,
|
|
159
|
+
locals: Record<string, unknown> = {},
|
|
154
160
|
): void {
|
|
155
161
|
const transformed = transform(code);
|
|
156
162
|
const fn = getOrCompile('exec:' + transformed, transformed);
|
|
157
|
-
fn(variables, temporary, buildExpressionFns());
|
|
163
|
+
fn(variables, temporary, locals, buildExpressionFns());
|
|
158
164
|
}
|
|
159
165
|
|
|
160
166
|
/**
|
|
161
167
|
* Convenience: evaluate using store state directly.
|
|
162
168
|
*/
|
|
163
169
|
export function evaluateWithState(expr: string, state: StoryState): unknown {
|
|
164
|
-
return evaluate(expr, state.variables, state.temporary);
|
|
170
|
+
return evaluate(expr, state.variables, state.temporary, {});
|
|
165
171
|
}
|
|
@@ -3,24 +3,26 @@ import { useStoryStore } from '../store';
|
|
|
3
3
|
import { LocalsContext } from '../markup/render';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
*
|
|
7
|
-
* Locals
|
|
6
|
+
* Return store variables, temporary, and @-prefixed locals from context.
|
|
7
|
+
* Locals use `@` prefix keys internally; the returned locals dict has
|
|
8
|
+
* the prefix stripped so it can be passed directly to evaluate/execute.
|
|
8
9
|
*/
|
|
9
10
|
export function useMergedLocals(): readonly [
|
|
10
11
|
Record<string, unknown>,
|
|
11
12
|
Record<string, unknown>,
|
|
13
|
+
Record<string, unknown>,
|
|
12
14
|
] {
|
|
13
15
|
const variables = useStoryStore((s) => s.variables);
|
|
14
16
|
const temporary = useStoryStore((s) => s.temporary);
|
|
15
|
-
const
|
|
17
|
+
const scope = useContext(LocalsContext);
|
|
16
18
|
|
|
17
19
|
return useMemo(() => {
|
|
18
|
-
const
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
20
|
+
const locals: Record<string, unknown> = {};
|
|
21
|
+
for (const [key, val] of Object.entries(scope.values)) {
|
|
22
|
+
if (key.startsWith('@')) {
|
|
23
|
+
locals[key.slice(1)] = val;
|
|
24
|
+
}
|
|
23
25
|
}
|
|
24
|
-
return [
|
|
25
|
-
}, [variables, temporary,
|
|
26
|
+
return [variables, temporary, locals] as const;
|
|
27
|
+
}, [variables, temporary, scope.values]);
|
|
26
28
|
}
|
package/src/markup/ast.ts
CHANGED
package/src/markup/render.tsx
CHANGED
|
@@ -42,7 +42,17 @@ import { markdownToHtml } from './markdown';
|
|
|
42
42
|
import { h } from 'preact';
|
|
43
43
|
import type { ASTNode, Branch, MacroNode } from './ast';
|
|
44
44
|
|
|
45
|
-
export
|
|
45
|
+
export interface LocalsScope {
|
|
46
|
+
values: Record<string, unknown>;
|
|
47
|
+
update: (key: string, value: unknown) => void;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const defaultLocalsScope: LocalsScope = {
|
|
51
|
+
values: {},
|
|
52
|
+
update: () => {},
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export const LocalsContext = createContext<LocalsScope>(defaultLocalsScope);
|
|
46
56
|
|
|
47
57
|
const EMPTY_BRANCHES: Branch[] = [];
|
|
48
58
|
|
package/src/markup/tokenizer.ts
CHANGED
|
@@ -29,7 +29,7 @@ export interface MacroToken {
|
|
|
29
29
|
export interface VariableToken {
|
|
30
30
|
type: 'variable';
|
|
31
31
|
name: string;
|
|
32
|
-
scope: 'variable' | 'temporary';
|
|
32
|
+
scope: 'variable' | 'temporary' | 'local';
|
|
33
33
|
className?: string;
|
|
34
34
|
id?: string;
|
|
35
35
|
start: number;
|
|
@@ -419,6 +419,34 @@ export function tokenize(input: string): Token[] {
|
|
|
419
419
|
continue;
|
|
420
420
|
}
|
|
421
421
|
|
|
422
|
+
if (charAfter === '@') {
|
|
423
|
+
// {.class#id @local.field}
|
|
424
|
+
i = afterSelectors + 1;
|
|
425
|
+
const nameStart = i;
|
|
426
|
+
while (i < input.length && /[\w.]/.test(input[i]!)) i++;
|
|
427
|
+
const name = input.slice(nameStart, i);
|
|
428
|
+
|
|
429
|
+
if (input[i] === '}') {
|
|
430
|
+
i++; // skip }
|
|
431
|
+
const token: VariableToken = {
|
|
432
|
+
type: 'variable',
|
|
433
|
+
name,
|
|
434
|
+
scope: 'local',
|
|
435
|
+
start,
|
|
436
|
+
end: i,
|
|
437
|
+
};
|
|
438
|
+
if (className) token.className = className;
|
|
439
|
+
if (id) token.id = id;
|
|
440
|
+
tokens.push(token);
|
|
441
|
+
textStart = i;
|
|
442
|
+
continue;
|
|
443
|
+
}
|
|
444
|
+
// Not valid — treat as text
|
|
445
|
+
i = start + 1;
|
|
446
|
+
textStart = start;
|
|
447
|
+
continue;
|
|
448
|
+
}
|
|
449
|
+
|
|
422
450
|
if (charAfter !== undefined && /[a-zA-Z]/.test(charAfter)) {
|
|
423
451
|
// {.class#id macroName args}
|
|
424
452
|
i = afterSelectors;
|
|
@@ -515,6 +543,32 @@ export function tokenize(input: string): Token[] {
|
|
|
515
543
|
continue;
|
|
516
544
|
}
|
|
517
545
|
|
|
546
|
+
// {@local.field}
|
|
547
|
+
if (nextChar === '@') {
|
|
548
|
+
flushText(i);
|
|
549
|
+
i += 2;
|
|
550
|
+
const nameStart = i;
|
|
551
|
+
while (i < input.length && /[\w.]/.test(input[i]!)) i++;
|
|
552
|
+
const name = input.slice(nameStart, i);
|
|
553
|
+
|
|
554
|
+
if (input[i] === '}') {
|
|
555
|
+
i++; // skip }
|
|
556
|
+
tokens.push({
|
|
557
|
+
type: 'variable',
|
|
558
|
+
name,
|
|
559
|
+
scope: 'local',
|
|
560
|
+
start,
|
|
561
|
+
end: i,
|
|
562
|
+
});
|
|
563
|
+
textStart = i;
|
|
564
|
+
continue;
|
|
565
|
+
}
|
|
566
|
+
// Not a valid local token — treat as text
|
|
567
|
+
i = start + 1;
|
|
568
|
+
textStart = start;
|
|
569
|
+
continue;
|
|
570
|
+
}
|
|
571
|
+
|
|
518
572
|
// {macro ...} or {/macro} — but not bare { that's just text
|
|
519
573
|
// Must start with a letter or /
|
|
520
574
|
if (
|
package/src/story-variables.ts
CHANGED
|
@@ -14,7 +14,7 @@ export interface VariableSchema extends FieldSchema {
|
|
|
14
14
|
|
|
15
15
|
const DECLARATION_RE = /^\$(\w+)\s*=\s*(.+)$/;
|
|
16
16
|
const VAR_REF_RE = /\$(\w+(?:\.\w+)*)/g;
|
|
17
|
-
const FOR_LOCAL_RE = /\{for\s
|
|
17
|
+
const FOR_LOCAL_RE = /\{for\s+@(\w+)(?:\s*,\s*@(\w+))?\s+of\b/g;
|
|
18
18
|
|
|
19
19
|
const VALID_VAR_TYPES = new Set<string>(['number', 'string', 'boolean']);
|
|
20
20
|
|
|
@@ -77,16 +77,16 @@ export function parseStoryVariables(
|
|
|
77
77
|
|
|
78
78
|
/**
|
|
79
79
|
* Extract for-loop local variable names from passage content.
|
|
80
|
-
* `{for
|
|
81
|
-
* `{for
|
|
80
|
+
* `{for @item of ...}` → "item"
|
|
81
|
+
* `{for @index, @item of ...}` → "index", "item"
|
|
82
82
|
*/
|
|
83
83
|
function extractForLocals(content: string): Set<string> {
|
|
84
84
|
const locals = new Set<string>();
|
|
85
85
|
let match: RegExpExecArray | null;
|
|
86
86
|
FOR_LOCAL_RE.lastIndex = 0;
|
|
87
87
|
while ((match = FOR_LOCAL_RE.exec(content)) !== null) {
|
|
88
|
-
locals.add(match[1]
|
|
89
|
-
if (match[2]) locals.add(match[2]
|
|
88
|
+
locals.add(match[1]!);
|
|
89
|
+
if (match[2]) locals.add(match[2]!);
|
|
90
90
|
}
|
|
91
91
|
return locals;
|
|
92
92
|
}
|