@rohal12/spindle 0.52.7 → 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 +710 -703
- package/dist/pkg/macro-registry.json +18 -0
- 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/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
|
@@ -3,13 +3,9 @@ import { useContext } from 'preact/hooks';
|
|
|
3
3
|
import { LocalsValuesContext } from '../../markup/render';
|
|
4
4
|
import { useInterpolate } from '../../hooks/use-interpolate';
|
|
5
5
|
import { ownValue, variableNameError } from '../../utils/namespace';
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
temporary: '_',
|
|
10
|
-
local: '@',
|
|
11
|
-
transient: '%',
|
|
12
|
-
} as const;
|
|
6
|
+
import { SCOPE_SIGILS } from '../../markup/tokenizer';
|
|
7
|
+
import type { VariableNode } from '../../markup/ast';
|
|
8
|
+
import { display, wrapContent } from './display';
|
|
13
9
|
|
|
14
10
|
const STORE_NAMESPACES = {
|
|
15
11
|
variable: 'variables',
|
|
@@ -17,17 +13,12 @@ const STORE_NAMESPACES = {
|
|
|
17
13
|
transient: 'transient',
|
|
18
14
|
} as const;
|
|
19
15
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
className?: string;
|
|
24
|
-
id?: string;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export function VarDisplay({ name, scope, className, id }: VarDisplayProps) {
|
|
16
|
+
/** A `{$name}` (`{_name}`, `{@name}`, `{%name}`) variable display. */
|
|
17
|
+
export function VarDisplay({ node }: { node: VariableNode }) {
|
|
18
|
+
const { name, scope } = node;
|
|
28
19
|
const resolve = useInterpolate();
|
|
29
|
-
className = resolve(className);
|
|
30
|
-
id = resolve(id);
|
|
20
|
+
const className = resolve(node.className);
|
|
21
|
+
const id = resolve(node.id);
|
|
31
22
|
const localsValues = useContext(LocalsValuesContext);
|
|
32
23
|
const parts = name.split('.');
|
|
33
24
|
const root = parts[0]!;
|
|
@@ -36,11 +27,10 @@ export function VarDisplay({ name, scope, className, id }: VarDisplayProps) {
|
|
|
36
27
|
scope === 'local' ? undefined : ownValue(s[STORE_NAMESPACES[scope]], root),
|
|
37
28
|
);
|
|
38
29
|
|
|
39
|
-
const
|
|
30
|
+
const sigil = SCOPE_SIGILS[scope];
|
|
31
|
+
const nameError = variableNameError(root, sigil + root);
|
|
40
32
|
if (nameError) {
|
|
41
|
-
return
|
|
42
|
-
<span class="error">{`{${SIGILS[scope]}${name} error: ${nameError}}`}</span>
|
|
43
|
-
);
|
|
33
|
+
return <span class="error">{`{${sigil}${name} error: ${nameError}}`}</span>;
|
|
44
34
|
}
|
|
45
35
|
|
|
46
36
|
let value: unknown;
|
|
@@ -61,15 +51,5 @@ export function VarDisplay({ name, scope, className, id }: VarDisplayProps) {
|
|
|
61
51
|
value = (value as Record<string, unknown>)[part];
|
|
62
52
|
}
|
|
63
53
|
|
|
64
|
-
|
|
65
|
-
if (className || id)
|
|
66
|
-
return (
|
|
67
|
-
<span
|
|
68
|
-
id={id}
|
|
69
|
-
class={className}
|
|
70
|
-
>
|
|
71
|
-
{display}
|
|
72
|
-
</span>
|
|
73
|
-
);
|
|
74
|
-
return <>{display}</>;
|
|
54
|
+
return wrapContent(className, id, display(value));
|
|
75
55
|
}
|
|
@@ -1,35 +1,69 @@
|
|
|
1
1
|
import { registerWidget } from '../../widgets/widget-registry';
|
|
2
2
|
import { astContainsChildren } from '../../widgets/ast-scanner';
|
|
3
|
-
import { registerBlockMacro } from '../../markup/ast';
|
|
3
|
+
import { registerBlockMacro, type ASTNode } from '../../markup/ast';
|
|
4
4
|
import { defineMacro } from '../../define-macro';
|
|
5
5
|
import { checkVariableName } from '../../utils/namespace';
|
|
6
6
|
import { MacroError } from './MacroError';
|
|
7
|
+
import { parseMacroArgs } from './macro-args';
|
|
8
|
+
import type { MacroArgs } from '../../registry';
|
|
9
|
+
|
|
10
|
+
/** A {widget} definition's name, then its `@` parameters. */
|
|
11
|
+
const WIDGET_PARAMETERS = [
|
|
12
|
+
{ name: 'name', type: 'text', required: true },
|
|
13
|
+
{ name: 'parameters', type: 'expression' },
|
|
14
|
+
] as const;
|
|
15
|
+
|
|
16
|
+
interface WidgetDef {
|
|
17
|
+
name: string;
|
|
18
|
+
params: string[];
|
|
19
|
+
}
|
|
7
20
|
|
|
8
21
|
/**
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
22
|
+
* The widget a {widget} definition's arguments declare: its name and its
|
|
23
|
+
* parameters, the words after it that start with `@` (docs/widgets.md).
|
|
24
|
+
* Other words, such as `$name`, are not parameters.
|
|
12
25
|
*/
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
const [first, ...rest] = rawArgs.trim().split(/\s+/);
|
|
26
|
+
function widgetDef({
|
|
27
|
+
name = '',
|
|
28
|
+
parameters = '',
|
|
29
|
+
}: MacroArgs<typeof WIDGET_PARAMETERS>): WidgetDef {
|
|
18
30
|
return {
|
|
19
|
-
name
|
|
20
|
-
params:
|
|
31
|
+
name,
|
|
32
|
+
params: parameters.split(/\s+/).filter((word) => word.startsWith('@')),
|
|
21
33
|
};
|
|
22
34
|
}
|
|
23
35
|
|
|
36
|
+
/** Read the arguments of a {widget} definition (see widgetDef). */
|
|
37
|
+
export function parseWidgetDef(rawArgs: string): WidgetDef {
|
|
38
|
+
return widgetDef(parseMacroArgs(rawArgs, WIDGET_PARAMETERS));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Register the widget a definition declares, with its body. Widgets whose
|
|
43
|
+
* body renders {@children} take a closing tag, so they are registered as
|
|
44
|
+
* block macros too: passages parsed later nest their content. A parameter
|
|
45
|
+
* that no namespace can hold throws (see registerWidget), registering
|
|
46
|
+
* nothing.
|
|
47
|
+
*/
|
|
48
|
+
export function registerWidgetDef(
|
|
49
|
+
{ name, params }: WidgetDef,
|
|
50
|
+
body: ASTNode[],
|
|
51
|
+
): void {
|
|
52
|
+
const isBlock = astContainsChildren(body);
|
|
53
|
+
registerWidget(name, body, params, isBlock);
|
|
54
|
+
if (isBlock) registerBlockMacro(name);
|
|
55
|
+
}
|
|
56
|
+
|
|
24
57
|
defineMacro({
|
|
25
58
|
name: 'widget',
|
|
26
59
|
block: true,
|
|
27
|
-
|
|
28
|
-
|
|
60
|
+
parameters: WIDGET_PARAMETERS,
|
|
61
|
+
render({ children = [] }, ctx) {
|
|
62
|
+
let parsed: WidgetDef | undefined;
|
|
29
63
|
let error: unknown;
|
|
30
64
|
try {
|
|
31
65
|
// Refuse `@` parameters that no namespace can hold
|
|
32
|
-
const def =
|
|
66
|
+
const def = widgetDef(ctx.args);
|
|
33
67
|
for (const param of def.params) checkVariableName(param.slice(1), param);
|
|
34
68
|
parsed = def;
|
|
35
69
|
} catch (err) {
|
|
@@ -41,12 +75,7 @@ defineMacro({
|
|
|
41
75
|
const paramsKey = params.join(',');
|
|
42
76
|
|
|
43
77
|
ctx.hooks.useLayoutEffect(() => {
|
|
44
|
-
if (
|
|
45
|
-
// Widgets whose body renders {@children} take a closing tag; register
|
|
46
|
-
// them as block macros so passages parsed later nest their content.
|
|
47
|
-
const isBlock = astContainsChildren(children);
|
|
48
|
-
registerWidget(name, children, params, isBlock);
|
|
49
|
-
if (isBlock) registerBlockMacro(name);
|
|
78
|
+
if (parsed) registerWidgetDef(parsed, children);
|
|
50
79
|
}, [name, childrenKey, paramsKey]);
|
|
51
80
|
|
|
52
81
|
if (error) {
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { Branch } from '../../markup/ast';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The branch a conditional block ({if}, {switch}) shows: the first of
|
|
5
|
+
* `branches` whose argument `matches`, else the last bare one ({else},
|
|
6
|
+
* {default}), else null. Throws what `matches` throws.
|
|
7
|
+
*/
|
|
8
|
+
export function selectBranch(
|
|
9
|
+
branches: Branch[],
|
|
10
|
+
matches: (rawArgs: string) => boolean,
|
|
11
|
+
): Branch | null {
|
|
12
|
+
let fallback: Branch | null = null;
|
|
13
|
+
for (const branch of branches) {
|
|
14
|
+
if (branch.rawArgs === '') fallback = branch;
|
|
15
|
+
else if (matches(branch.rawArgs)) return branch;
|
|
16
|
+
}
|
|
17
|
+
return fallback;
|
|
18
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { h, Fragment, type ComponentChildren, type VNode } from 'preact';
|
|
2
|
+
|
|
3
|
+
/** The text a value is shown as: nothing for null and undefined. */
|
|
4
|
+
export function display(value: unknown): string {
|
|
5
|
+
return value == null ? '' : String(value);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* `content` in a `<span>` carrying `className` and `id`, or in a fragment if
|
|
10
|
+
* it has neither.
|
|
11
|
+
*/
|
|
12
|
+
export function wrapContent(
|
|
13
|
+
className: string | undefined,
|
|
14
|
+
id: string | undefined,
|
|
15
|
+
content: ComponentChildren,
|
|
16
|
+
): VNode<any> {
|
|
17
|
+
if (className || id) return h('span', { id, class: className }, content);
|
|
18
|
+
return h(Fragment, null, content);
|
|
19
|
+
}
|
|
@@ -1,5 +1,34 @@
|
|
|
1
1
|
import { h } from 'preact';
|
|
2
|
-
import { defineMacro } from '../../define-macro';
|
|
2
|
+
import { defineMacro, type MacroContext } from '../../define-macro';
|
|
3
|
+
import type { UseActionOptions } from '../../hooks/use-action';
|
|
4
|
+
import { display } from './display';
|
|
5
|
+
|
|
6
|
+
/** The story variable a `storeVar` macro binds: its first parameter. */
|
|
7
|
+
export const VARIABLE_PARAMETER = {
|
|
8
|
+
name: 'variable',
|
|
9
|
+
type: 'variable',
|
|
10
|
+
required: true,
|
|
11
|
+
} as const;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Register the automation action of a macro bound to a story variable: keyed
|
|
15
|
+
* by the variable (or `key`), with the variable's value unless `action`
|
|
16
|
+
* gives another.
|
|
17
|
+
*/
|
|
18
|
+
export function useVariableAction(
|
|
19
|
+
ctx: Pick<MacroContext, 'useAction' | 'id' | 'varName' | 'value'>,
|
|
20
|
+
action: Omit<UseActionOptions, 'key' | 'authorId' | 'variable'> & {
|
|
21
|
+
key?: string;
|
|
22
|
+
},
|
|
23
|
+
): string {
|
|
24
|
+
return ctx.useAction({
|
|
25
|
+
key: `$${ctx.varName}`,
|
|
26
|
+
authorId: ctx.id,
|
|
27
|
+
variable: ctx.varName,
|
|
28
|
+
value: ctx.value,
|
|
29
|
+
...action,
|
|
30
|
+
});
|
|
31
|
+
}
|
|
3
32
|
|
|
4
33
|
/**
|
|
5
34
|
* Define a text input macro, `{name $var "placeholder"}`: an `<input>` of
|
|
@@ -17,20 +46,13 @@ export function defineInputMacro(
|
|
|
17
46
|
defineMacro({
|
|
18
47
|
name,
|
|
19
48
|
storeVar: true,
|
|
20
|
-
parameters: [
|
|
21
|
-
{ name: 'variable', type: 'variable', required: true },
|
|
22
|
-
{ name: 'placeholder', type: 'string' },
|
|
23
|
-
],
|
|
49
|
+
parameters: [VARIABLE_PARAMETER, { name: 'placeholder', type: 'string' }],
|
|
24
50
|
render(_props, ctx) {
|
|
25
51
|
const placeholder = ctx.args.placeholder ?? '';
|
|
26
52
|
|
|
27
|
-
ctx
|
|
53
|
+
useVariableAction(ctx, {
|
|
28
54
|
type: name,
|
|
29
|
-
key: `$${ctx.varName}`,
|
|
30
|
-
authorId: ctx.id,
|
|
31
55
|
label: placeholder || ctx.varName!,
|
|
32
|
-
variable: ctx.varName,
|
|
33
|
-
value: ctx.value,
|
|
34
56
|
perform: (v) => ctx.setValue!(toValue(v)),
|
|
35
57
|
});
|
|
36
58
|
|
|
@@ -38,7 +60,7 @@ export function defineInputMacro(
|
|
|
38
60
|
type,
|
|
39
61
|
id: ctx.id,
|
|
40
62
|
class: ctx.cls,
|
|
41
|
-
value:
|
|
63
|
+
value: display(ctx.value),
|
|
42
64
|
placeholder,
|
|
43
65
|
onInput: (e: Event) =>
|
|
44
66
|
ctx.setValue!(toValue((e.target as HTMLInputElement).value)),
|
|
@@ -232,6 +232,13 @@ function readGroup(
|
|
|
232
232
|
: unset(rest);
|
|
233
233
|
}
|
|
234
234
|
|
|
235
|
+
/** The duration a timing macro ({repeat}, {type}, {timed}/{next}) waits. */
|
|
236
|
+
export const DELAY_PARAMETER = {
|
|
237
|
+
name: 'delay',
|
|
238
|
+
type: 'delay',
|
|
239
|
+
required: true,
|
|
240
|
+
} as const;
|
|
241
|
+
|
|
235
242
|
/** Read `rawArgs` into the declared `parameters` (see above). */
|
|
236
243
|
export function parseMacroArgs<const P extends readonly ParameterDef[]>(
|
|
237
244
|
rawArgs: string,
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import type { ASTNode } from '../../markup/ast';
|
|
2
|
-
import { readWholeQuoted } from './arg-utils';
|
|
3
2
|
import { parseMacroArgs } from './macro-args';
|
|
4
3
|
|
|
5
4
|
/** `$var "placeholder"`, the arguments of the input macros. */
|
|
@@ -14,18 +13,22 @@ export function parseVarArgs(rawArgs: string): {
|
|
|
14
13
|
return { varName: variable.replace(/["']/g, ''), placeholder };
|
|
15
14
|
}
|
|
16
15
|
|
|
16
|
+
/** The parameters of an `{option}` sub-macro: its value. */
|
|
17
|
+
const OPTION_PARAMETERS = [{ name: 'value', type: 'string' }] as const;
|
|
18
|
+
|
|
17
19
|
/**
|
|
18
|
-
* Walk AST children to find {option} macro nodes, returning their
|
|
20
|
+
* Walk AST children to find {option} macro nodes, returning their values.
|
|
19
21
|
*/
|
|
20
22
|
export function extractOptions(children: ASTNode[]): string[] {
|
|
21
23
|
const options: string[] = [];
|
|
22
24
|
for (const node of children) {
|
|
23
25
|
if (node.type === 'macro' && node.name === 'option') {
|
|
24
|
-
|
|
25
|
-
//
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
26
|
+
// {option "Long Sword"} gives `Long Sword`. A value that isn't one
|
|
27
|
+
// quoted string loses a matching pair of quotes around it, if any.
|
|
28
|
+
const { value } = parseMacroArgs(node.rawArgs, OPTION_PARAMETERS);
|
|
29
|
+
options.push(
|
|
30
|
+
value ?? node.rawArgs.trim().replace(/^(["'])(.+)\1$/s, '$2'),
|
|
31
|
+
);
|
|
29
32
|
}
|
|
30
33
|
}
|
|
31
34
|
return options;
|
package/src/define-macro.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { h
|
|
1
|
+
import { h } from 'preact';
|
|
2
2
|
import type { VNode, ComponentChildren } from 'preact';
|
|
3
3
|
import {
|
|
4
4
|
useContext,
|
|
@@ -28,6 +28,7 @@ import type { UseActionOptions } from './hooks/use-action';
|
|
|
28
28
|
import { collectText } from './utils/extract-text';
|
|
29
29
|
import { currentSourceLocation } from './utils/source-location';
|
|
30
30
|
import { parseVarArgs, extractOptions } from './components/macros/option-utils';
|
|
31
|
+
import { wrapContent } from './components/macros/display';
|
|
31
32
|
import {
|
|
32
33
|
parseMacroArgs,
|
|
33
34
|
readBoundVariable,
|
|
@@ -177,11 +178,8 @@ export function defineMacro<const P extends readonly ParameterDef[] = []>(
|
|
|
177
178
|
mutate: (code: string) => executeMutation(code, getValues(), update),
|
|
178
179
|
update,
|
|
179
180
|
getValues,
|
|
180
|
-
wrap: (content: ComponentChildren): VNode<any> =>
|
|
181
|
-
|
|
182
|
-
return h('span', { id, class: className }, content);
|
|
183
|
-
return h(Fragment, null, content);
|
|
184
|
-
},
|
|
181
|
+
wrap: (content: ComponentChildren): VNode<any> =>
|
|
182
|
+
wrapContent(className, id, content),
|
|
185
183
|
};
|
|
186
184
|
|
|
187
185
|
if (config.merged) {
|
package/src/expression.ts
CHANGED
|
@@ -132,7 +132,7 @@ export function previousPassage(): Passage | undefined {
|
|
|
132
132
|
* visited()/rendered() and their has-forms, counting in the counters
|
|
133
133
|
* `counts()` returns. A missing name means the current passage.
|
|
134
134
|
*/
|
|
135
|
-
|
|
135
|
+
function countQueries(counts: () => Counts) {
|
|
136
136
|
const count = (name?: string): number =>
|
|
137
137
|
countOf(counts(), name ?? useStoryStore.getState().currentPassage);
|
|
138
138
|
return {
|
|
@@ -143,6 +143,31 @@ export function countQueries(counts: () => Counts) {
|
|
|
143
143
|
};
|
|
144
144
|
}
|
|
145
145
|
|
|
146
|
+
/**
|
|
147
|
+
* The history functions of passage code and the story API: the current and
|
|
148
|
+
* previous passage, and visited()/rendered() with their has-forms, counting
|
|
149
|
+
* in the counters `visitCounts()` and `renderCounts()` return.
|
|
150
|
+
*/
|
|
151
|
+
export function historyQueries(
|
|
152
|
+
visitCounts: () => Counts,
|
|
153
|
+
renderCounts: () => Counts,
|
|
154
|
+
) {
|
|
155
|
+
const visits = countQueries(visitCounts);
|
|
156
|
+
const renders = countQueries(renderCounts);
|
|
157
|
+
return {
|
|
158
|
+
currentPassage,
|
|
159
|
+
previousPassage,
|
|
160
|
+
visited: visits.count,
|
|
161
|
+
hasVisited: visits.has,
|
|
162
|
+
hasVisitedAny: visits.any,
|
|
163
|
+
hasVisitedAll: visits.all,
|
|
164
|
+
rendered: renders.count,
|
|
165
|
+
hasRendered: renders.has,
|
|
166
|
+
hasRenderedAny: renders.any,
|
|
167
|
+
hasRenderedAll: renders.all,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
146
171
|
let cachedFns: ExpressionFns | null = null;
|
|
147
172
|
let cachedVisitCounts: Counts | null = null;
|
|
148
173
|
let cachedRenderCounts: Counts | null = null;
|
|
@@ -159,19 +184,11 @@ export function buildExpressionFns() {
|
|
|
159
184
|
return cachedFns;
|
|
160
185
|
}
|
|
161
186
|
|
|
162
|
-
const visits = countQueries(() => visitCounts);
|
|
163
|
-
const renders = countQueries(() => renderCounts);
|
|
164
187
|
cachedFns = {
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
hasVisitedAny: visits.any,
|
|
170
|
-
hasVisitedAll: visits.all,
|
|
171
|
-
rendered: renders.count,
|
|
172
|
-
hasRendered: renders.has,
|
|
173
|
-
hasRenderedAny: renders.any,
|
|
174
|
-
hasRenderedAll: renders.all,
|
|
188
|
+
...historyQueries(
|
|
189
|
+
() => visitCounts,
|
|
190
|
+
() => renderCounts,
|
|
191
|
+
),
|
|
175
192
|
random,
|
|
176
193
|
randomInt,
|
|
177
194
|
};
|
|
@@ -195,21 +212,24 @@ function namespaceArg(ns: Namespace): Namespace {
|
|
|
195
212
|
return ns;
|
|
196
213
|
}
|
|
197
214
|
|
|
198
|
-
/**
|
|
199
|
-
|
|
200
|
-
* e.g. evaluate("$health + 10", variables, temporary) → number
|
|
201
|
-
*/
|
|
202
|
-
export function evaluate(
|
|
203
|
-
expr: string,
|
|
215
|
+
/** The namespaces code runs in, as evaluate() and execute() take them. */
|
|
216
|
+
type Namespaces = [
|
|
204
217
|
variables: Namespace,
|
|
205
218
|
temporary: Namespace,
|
|
206
|
-
locals
|
|
207
|
-
transient
|
|
219
|
+
locals?: Namespace,
|
|
220
|
+
transient?: Namespace,
|
|
221
|
+
];
|
|
222
|
+
|
|
223
|
+
/** Run compiled code in `namespaces`. */
|
|
224
|
+
function run(
|
|
225
|
+
fn: CompiledExpression,
|
|
226
|
+
...[
|
|
227
|
+
variables,
|
|
228
|
+
temporary,
|
|
229
|
+
locals = EMPTY_NAMESPACE,
|
|
230
|
+
transient = EMPTY_NAMESPACE,
|
|
231
|
+
]: Namespaces
|
|
208
232
|
): unknown {
|
|
209
|
-
const transformed = transform(expr);
|
|
210
|
-
// The line break keeps a trailing `// comment` from swallowing the `)`.
|
|
211
|
-
const body = `return (${transformed}\n);`;
|
|
212
|
-
const fn = getOrCompile(body, body);
|
|
213
233
|
return fn(
|
|
214
234
|
namespaceArg(variables),
|
|
215
235
|
namespaceArg(temporary),
|
|
@@ -219,26 +239,24 @@ export function evaluate(
|
|
|
219
239
|
);
|
|
220
240
|
}
|
|
221
241
|
|
|
242
|
+
/**
|
|
243
|
+
* Evaluate an expression and return its value.
|
|
244
|
+
* e.g. evaluate("$health + 10", variables, temporary) → number
|
|
245
|
+
*/
|
|
246
|
+
export function evaluate(expr: string, ...namespaces: Namespaces): unknown {
|
|
247
|
+
const transformed = transform(expr);
|
|
248
|
+
// The line break keeps a trailing `// comment` from swallowing the `)`.
|
|
249
|
+
const body = `return (${transformed}\n);`;
|
|
250
|
+
return run(getOrCompile(body, body), ...namespaces);
|
|
251
|
+
}
|
|
252
|
+
|
|
222
253
|
/**
|
|
223
254
|
* Execute statements (no return value).
|
|
224
255
|
* e.g. execute("$health = 100; $name = 'Hero'", variables, temporary)
|
|
225
256
|
*/
|
|
226
|
-
export function execute(
|
|
227
|
-
code: string,
|
|
228
|
-
variables: Namespace,
|
|
229
|
-
temporary: Namespace,
|
|
230
|
-
locals: Namespace = EMPTY_NAMESPACE,
|
|
231
|
-
transient: Namespace = EMPTY_NAMESPACE,
|
|
232
|
-
): void {
|
|
257
|
+
export function execute(code: string, ...namespaces: Namespaces): void {
|
|
233
258
|
const transformed = transform(code, 'statements');
|
|
234
|
-
|
|
235
|
-
fn(
|
|
236
|
-
namespaceArg(variables),
|
|
237
|
-
namespaceArg(temporary),
|
|
238
|
-
namespaceArg(locals),
|
|
239
|
-
buildExpressionFns(),
|
|
240
|
-
namespaceArg(transient),
|
|
241
|
-
);
|
|
259
|
+
run(getOrCompile('exec:' + transformed, transformed), ...namespaces);
|
|
242
260
|
}
|
|
243
261
|
|
|
244
262
|
/**
|
package/src/hooks/use-action.ts
CHANGED
|
@@ -2,22 +2,15 @@ import { useLayoutEffect, useRef } from 'preact/hooks';
|
|
|
2
2
|
import {
|
|
3
3
|
registerAction,
|
|
4
4
|
generateActionId,
|
|
5
|
+
OPTIONAL_ACTION_FIELDS,
|
|
5
6
|
type ActionRegistration,
|
|
6
|
-
type ActionType,
|
|
7
7
|
type StoryAction,
|
|
8
8
|
} from '../action-registry';
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
/** The action to register, and what its ID is generated from. */
|
|
11
|
+
export interface UseActionOptions extends Omit<StoryAction, 'id'> {
|
|
12
12
|
key: string;
|
|
13
13
|
authorId?: string;
|
|
14
|
-
label: string;
|
|
15
|
-
target?: string;
|
|
16
|
-
variable?: string;
|
|
17
|
-
options?: string[];
|
|
18
|
-
value?: unknown;
|
|
19
|
-
disabled?: boolean;
|
|
20
|
-
perform: (value?: unknown) => void;
|
|
21
14
|
}
|
|
22
15
|
|
|
23
16
|
export function useAction(opts: UseActionOptions): string {
|
|
@@ -39,11 +32,10 @@ export function useAction(opts: UseActionOptions): string {
|
|
|
39
32
|
label: opts.label,
|
|
40
33
|
perform: (...args) => performRef.current(...args),
|
|
41
34
|
};
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
if (opts.disabled !== undefined) action.disabled = opts.disabled;
|
|
35
|
+
for (const field of OPTIONAL_ACTION_FIELDS) {
|
|
36
|
+
if (opts[field] !== undefined)
|
|
37
|
+
Object.assign(action, { [field]: opts[field] });
|
|
38
|
+
}
|
|
47
39
|
return action;
|
|
48
40
|
};
|
|
49
41
|
|
package/src/index.tsx
CHANGED
|
@@ -19,9 +19,7 @@ import {
|
|
|
19
19
|
import { getMacroRegistry } from './registry';
|
|
20
20
|
import { tokenize } from './markup/tokenizer';
|
|
21
21
|
import { buildAST, registerBlockMacro } from './markup/ast';
|
|
22
|
-
import {
|
|
23
|
-
import { astContainsChildren } from './widgets/ast-scanner';
|
|
24
|
-
import { parseWidgetDef } from './components/macros/Widget';
|
|
22
|
+
import { parseWidgetDef, registerWidgetDef } from './components/macros/Widget';
|
|
25
23
|
import { errorMessage } from './utils/error-message';
|
|
26
24
|
import type { ASTNode } from './markup/ast';
|
|
27
25
|
import './macros/register-builtins';
|
|
@@ -174,23 +172,15 @@ export function boot() {
|
|
|
174
172
|
const widgetAST = buildAST(widgetTokens);
|
|
175
173
|
for (const node of widgetAST) {
|
|
176
174
|
if (node.type === 'macro' && node.name === 'widget' && node.rawArgs) {
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
'$_@',
|
|
180
|
-
);
|
|
181
|
-
const children = node.children as ASTNode[];
|
|
182
|
-
const isBlock = astContainsChildren(children);
|
|
175
|
+
// Read and registered as the {widget} macro does
|
|
176
|
+
const def = parseWidgetDef(node.rawArgs);
|
|
183
177
|
try {
|
|
184
|
-
|
|
178
|
+
registerWidgetDef(def, node.children as ASTNode[]);
|
|
185
179
|
} catch (err) {
|
|
186
180
|
// As the {widget} macro refuses it: the others still register
|
|
187
181
|
console.error(
|
|
188
|
-
`spindle: widget "${
|
|
182
|
+
`spindle: widget "${def.name}" in passage "${passage.name}" was not registered: ${errorMessage(err)}`,
|
|
189
183
|
);
|
|
190
|
-
continue;
|
|
191
|
-
}
|
|
192
|
-
if (isBlock) {
|
|
193
|
-
registerBlockMacro(widgetName);
|
|
194
184
|
}
|
|
195
185
|
}
|
|
196
186
|
}
|