@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
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { defineMacro } from '../../define-macro';
|
|
2
2
|
import { MacroError } from './MacroError';
|
|
3
|
+
import { display } from './display';
|
|
3
4
|
|
|
4
5
|
defineMacro({
|
|
5
6
|
name: 'print',
|
|
@@ -8,9 +9,7 @@ defineMacro({
|
|
|
8
9
|
merged: true,
|
|
9
10
|
render({ rawArgs }, ctx) {
|
|
10
11
|
try {
|
|
11
|
-
|
|
12
|
-
const display = result == null ? '' : String(result);
|
|
13
|
-
return ctx.wrap(display);
|
|
12
|
+
return ctx.wrap(display(ctx.evaluate!(rawArgs)));
|
|
14
13
|
} catch (err) {
|
|
15
14
|
return (
|
|
16
15
|
<MacroError
|
|
@@ -21,7 +20,6 @@ defineMacro({
|
|
|
21
20
|
}
|
|
22
21
|
},
|
|
23
22
|
text({ rawArgs }, ctx) {
|
|
24
|
-
|
|
25
|
-
return result == null ? '' : String(result);
|
|
23
|
+
return display(ctx.evaluate(rawArgs));
|
|
26
24
|
},
|
|
27
25
|
});
|
|
@@ -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: 'radiobutton',
|
|
5
6
|
storeVar: true,
|
|
6
7
|
parameters: [
|
|
7
|
-
|
|
8
|
+
VARIABLE_PARAMETER,
|
|
8
9
|
{ name: 'value', type: 'text', required: true },
|
|
9
10
|
{ name: 'label', type: 'text' },
|
|
10
11
|
],
|
|
@@ -12,13 +13,10 @@ defineMacro({
|
|
|
12
13
|
const radioValue = ctx.args.value ?? '';
|
|
13
14
|
const label = ctx.args.label ?? '';
|
|
14
15
|
|
|
15
|
-
ctx
|
|
16
|
+
useVariableAction(ctx, {
|
|
16
17
|
type: 'radiobutton',
|
|
17
18
|
key: `$${ctx.varName}:${radioValue}`,
|
|
18
|
-
authorId: ctx.id,
|
|
19
19
|
label: label || radioValue,
|
|
20
|
-
variable: ctx.varName,
|
|
21
|
-
value: ctx.value,
|
|
22
20
|
perform: () => ctx.setValue!(radioValue),
|
|
23
21
|
});
|
|
24
22
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createContext } from 'preact';
|
|
2
2
|
import { defineMacro } from '../../define-macro';
|
|
3
|
+
import { DELAY_PARAMETER } from './macro-args';
|
|
3
4
|
|
|
4
5
|
export const RepeatContext = createContext<{ stop: () => void }>({
|
|
5
6
|
stop: () => {},
|
|
@@ -9,7 +10,7 @@ defineMacro({
|
|
|
9
10
|
name: 'repeat',
|
|
10
11
|
block: true,
|
|
11
12
|
interpolate: true,
|
|
12
|
-
parameters: [
|
|
13
|
+
parameters: [DELAY_PARAMETER],
|
|
13
14
|
render({ children = [] }, ctx) {
|
|
14
15
|
const { useState, useEffect, useCallback } = ctx.hooks;
|
|
15
16
|
|
|
@@ -1,26 +1,19 @@
|
|
|
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';
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* The first {case} branch whose value equals `value`, else the {default}
|
|
7
8
|
* branch, else null. Throws what a case expression throws.
|
|
8
9
|
*/
|
|
9
|
-
function
|
|
10
|
+
function selectCase(
|
|
10
11
|
value: unknown,
|
|
11
12
|
branches: Branch[],
|
|
12
13
|
evaluate: (expr: string) => unknown,
|
|
13
14
|
): Branch | null {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
const branch = branches[i]!;
|
|
17
|
-
if (branch.rawArgs === '') {
|
|
18
|
-
defaultBranch = branch;
|
|
19
|
-
} else if (value === evaluate(branch.rawArgs)) {
|
|
20
|
-
return branch;
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
return defaultBranch;
|
|
15
|
+
// The first branch holds the content before the first {case}
|
|
16
|
+
return selectBranch(branches.slice(1), (expr) => value === evaluate(expr));
|
|
24
17
|
}
|
|
25
18
|
|
|
26
19
|
defineMacro({
|
|
@@ -43,7 +36,7 @@ defineMacro({
|
|
|
43
36
|
|
|
44
37
|
let branch: Branch | null;
|
|
45
38
|
try {
|
|
46
|
-
branch =
|
|
39
|
+
branch = selectCase(switchValue, branches, ctx.evaluate!);
|
|
47
40
|
} catch (err) {
|
|
48
41
|
return (
|
|
49
42
|
<MacroError
|
|
@@ -56,7 +49,7 @@ defineMacro({
|
|
|
56
49
|
return branch && <>{ctx.renderNodes(branch.children)}</>;
|
|
57
50
|
},
|
|
58
51
|
text({ rawArgs, branches = [] }, ctx) {
|
|
59
|
-
const branch =
|
|
52
|
+
const branch = selectCase(ctx.evaluate(rawArgs), branches, ctx.evaluate);
|
|
60
53
|
return branch ? ctx.renderText(branch.children) : '';
|
|
61
54
|
},
|
|
62
55
|
});
|
|
@@ -1,16 +1,21 @@
|
|
|
1
|
-
import { parseDelay } from '../../utils/parse-delay';
|
|
2
1
|
import { defineMacro } from '../../define-macro';
|
|
2
|
+
import { DELAY_PARAMETER, parseMacroArgs } from './macro-args';
|
|
3
|
+
import { wrapContent } from './display';
|
|
4
|
+
|
|
5
|
+
/** The parameters of {timed} and of each {next}: the delay before it shows. */
|
|
6
|
+
const TIMED_PARAMETERS = [DELAY_PARAMETER] as const;
|
|
3
7
|
|
|
4
8
|
defineMacro({
|
|
5
9
|
name: 'timed',
|
|
6
10
|
subMacros: ['next'],
|
|
7
11
|
interpolate: true,
|
|
12
|
+
parameters: TIMED_PARAMETERS,
|
|
8
13
|
render({ branches = [] }, ctx) {
|
|
9
14
|
const { useState, useEffect, useMemo } = ctx.hooks;
|
|
10
15
|
|
|
11
16
|
const sections = useMemo(() => {
|
|
12
17
|
return branches.map((branch) => ({
|
|
13
|
-
delay: branch.rawArgs
|
|
18
|
+
delay: parseMacroArgs(branch.rawArgs, TIMED_PARAMETERS).delay ?? 0,
|
|
14
19
|
nodes: branch.children,
|
|
15
20
|
className: branch.className,
|
|
16
21
|
id: branch.id,
|
|
@@ -35,21 +40,12 @@ defineMacro({
|
|
|
35
40
|
if (visibleIndex < 0) return ctx.wrap(null);
|
|
36
41
|
|
|
37
42
|
const section = sections[visibleIndex]!;
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
id={sectionId}
|
|
46
|
-
class={sectionClass}
|
|
47
|
-
>
|
|
48
|
-
{content}
|
|
49
|
-
</span>
|
|
50
|
-
) : (
|
|
51
|
-
content
|
|
52
|
-
);
|
|
53
|
-
return ctx.wrap(inner);
|
|
43
|
+
return ctx.wrap(
|
|
44
|
+
wrapContent(
|
|
45
|
+
ctx.resolve!(section.className),
|
|
46
|
+
ctx.resolve!(section.id),
|
|
47
|
+
ctx.renderNodes(section.nodes),
|
|
48
|
+
),
|
|
49
|
+
);
|
|
54
50
|
},
|
|
55
51
|
});
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { defineMacro } from '../../define-macro';
|
|
2
|
+
import { DELAY_PARAMETER } from './macro-args';
|
|
2
3
|
|
|
3
4
|
defineMacro({
|
|
4
5
|
name: 'type',
|
|
5
6
|
block: true,
|
|
6
7
|
interpolate: true,
|
|
7
|
-
parameters: [
|
|
8
|
+
parameters: [DELAY_PARAMETER],
|
|
8
9
|
render({ children = [] }, ctx) {
|
|
9
10
|
const { useState, useEffect, useRef } = ctx.hooks;
|
|
10
11
|
const speed = ctx.args.delay ?? 0;
|
|
@@ -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) {
|