@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
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The macro argument layer: reads a macro's raw argument text into the
|
|
3
|
+
* parameters its definition declares (see ParameterDef), so every macro
|
|
4
|
+
* splits and unquotes its arguments the same way. defineMacro() passes the
|
|
5
|
+
* result to the macro as `ctx.args`.
|
|
6
|
+
*
|
|
7
|
+
* Arguments are read in groups, split at `separator` parameters. A group's
|
|
8
|
+
* `flag` parameters are taken off its start or end first. Its last
|
|
9
|
+
* `expression` or `text` parameter (without one, its last parameter) reads
|
|
10
|
+
* the text the others leave; each parameter before it reads one term from
|
|
11
|
+
* the start, and each after it one term from the end. Terms are separated by
|
|
12
|
+
* whitespace outside literals and brackets (see arg-utils.ts), and a term
|
|
13
|
+
* that follows an operator belongs to the expression before it. A parameter
|
|
14
|
+
* with nothing to read is unset (a flag or separator is false, options are
|
|
15
|
+
* empty).
|
|
16
|
+
*/
|
|
17
|
+
import { parseDelay } from '../../utils/parse-delay';
|
|
18
|
+
import type { MacroArgs, ParameterDef } from '../../registry';
|
|
19
|
+
import {
|
|
20
|
+
endsWithOperator,
|
|
21
|
+
isWhitespace,
|
|
22
|
+
readQuoted,
|
|
23
|
+
readWholeQuoted,
|
|
24
|
+
stripLooseQuotes,
|
|
25
|
+
topLevelIndices,
|
|
26
|
+
} from './arg-utils';
|
|
27
|
+
|
|
28
|
+
type Span = [start: number, end: number];
|
|
29
|
+
|
|
30
|
+
/** The terms of `src`: the runs of text between its depth-0 whitespace. */
|
|
31
|
+
function terms(src: string): Span[] {
|
|
32
|
+
const spans: Span[] = [];
|
|
33
|
+
let start = 0;
|
|
34
|
+
for (const i of [...topLevelIndices(src, isWhitespace), src.length]) {
|
|
35
|
+
if (i > start) spans.push([start, i]);
|
|
36
|
+
start = i + 1;
|
|
37
|
+
}
|
|
38
|
+
return spans;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const slice = (src: string, [start, end]: Span) => src.slice(start, end);
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* The value of a `text` argument: one quoted string, or the text with any
|
|
45
|
+
* loose quotes stripped (`"Red` → `Red`).
|
|
46
|
+
*/
|
|
47
|
+
export function readText(src: string): string {
|
|
48
|
+
return readWholeQuoted(src) ?? stripLooseQuotes(src);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function readValue(param: ParameterDef, src: string): unknown {
|
|
52
|
+
switch (param.type) {
|
|
53
|
+
case 'string':
|
|
54
|
+
return readWholeQuoted(src) ?? undefined;
|
|
55
|
+
case 'text':
|
|
56
|
+
return readText(src);
|
|
57
|
+
case 'names':
|
|
58
|
+
return src.split(',').map((name) => name.trim());
|
|
59
|
+
case 'delay':
|
|
60
|
+
return parseDelay(src);
|
|
61
|
+
case 'number':
|
|
62
|
+
return Number(src);
|
|
63
|
+
case 'options':
|
|
64
|
+
return readOptions(param.parameters ?? [], src);
|
|
65
|
+
default:
|
|
66
|
+
return src;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function unset(param: ParameterDef): unknown {
|
|
71
|
+
if (param.type === 'flag' || param.type === 'separator') return false;
|
|
72
|
+
return param.type === 'options' ? {} : undefined;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const WORD_RE = /\w+/y;
|
|
76
|
+
const DIGITS_RE = /\d+/y;
|
|
77
|
+
|
|
78
|
+
/** Match the sticky regex `re` at `pos`, returning the matched text. */
|
|
79
|
+
function matchAt(re: RegExp, src: string, pos: number): string | null {
|
|
80
|
+
re.lastIndex = pos;
|
|
81
|
+
return re.exec(src)?.[0] ?? null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Read keyword options (`goto "X" priority 5 once`). A keyword takes the
|
|
86
|
+
* quoted string or digit run after it as its value; keywords that aren't
|
|
87
|
+
* declared, and their values, are skipped.
|
|
88
|
+
*/
|
|
89
|
+
function readOptions(
|
|
90
|
+
params: readonly ParameterDef[],
|
|
91
|
+
src: string,
|
|
92
|
+
): Record<string, unknown> {
|
|
93
|
+
const options: Record<string, unknown> = {};
|
|
94
|
+
let i = 0;
|
|
95
|
+
while (i < src.length) {
|
|
96
|
+
const key = matchAt(WORD_RE, src, i);
|
|
97
|
+
if (!key) {
|
|
98
|
+
// Skip whitespace and stray characters, or a stray quoted string.
|
|
99
|
+
i = readQuoted(src, i)?.end ?? i + 1;
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
i += key.length;
|
|
103
|
+
|
|
104
|
+
// A value is a quoted string or a digit run, after whitespace.
|
|
105
|
+
let val: string | undefined;
|
|
106
|
+
let j = i;
|
|
107
|
+
while (j < src.length && isWhitespace(src[j]!)) j++;
|
|
108
|
+
if (j > i) {
|
|
109
|
+
const quoted = readQuoted(src, j);
|
|
110
|
+
const digits = quoted ? null : matchAt(DIGITS_RE, src, j);
|
|
111
|
+
if (quoted) {
|
|
112
|
+
val = quoted.value;
|
|
113
|
+
i = quoted.end;
|
|
114
|
+
} else if (digits) {
|
|
115
|
+
val = digits;
|
|
116
|
+
i = j + digits.length;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const param = params.find((p) => p.name === key);
|
|
121
|
+
if (param?.type === 'flag') options[key] = true;
|
|
122
|
+
else if (param?.type === 'number') options[key] = Number(val ?? 0);
|
|
123
|
+
else if (param) options[key] = val;
|
|
124
|
+
}
|
|
125
|
+
return options;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** `inline %name` reads a transient variable; `inline % 2` is modulo. */
|
|
129
|
+
const LEADING_OPERATOR_RE = /^(?:[-+*/&|^=<>?:,.]|%(?![A-Za-z_]))/;
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* The rest of `src` without the flag `word` as its last or first term, or
|
|
133
|
+
* `null` if it has no such flag. Next to a binary operator the word is an
|
|
134
|
+
* operand (`"a" + inline`), not a flag; the closing `/` of a regex literal
|
|
135
|
+
* is not one.
|
|
136
|
+
*/
|
|
137
|
+
function takeFlag(src: string, word: string): string | null {
|
|
138
|
+
const spans = terms(src);
|
|
139
|
+
if (spans.length < 2) return null;
|
|
140
|
+
if (slice(src, spans[spans.length - 1]!) === word) {
|
|
141
|
+
const rest = src.slice(0, spans[spans.length - 2]![1]);
|
|
142
|
+
if (!endsWithOperator(rest)) return rest;
|
|
143
|
+
}
|
|
144
|
+
if (slice(src, spans[0]!) === word) {
|
|
145
|
+
const rest = src.slice(spans[1]![0]);
|
|
146
|
+
if (!LEADING_OPERATOR_RE.test(rest)) return rest;
|
|
147
|
+
}
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Where the separator `word` splits `src`, as the end of the text before it
|
|
153
|
+
* and the start of the text after it, or `null` if it doesn't. A word is a
|
|
154
|
+
* term with terms on both sides; `=` is an assignment at depth 0, not part
|
|
155
|
+
* of `==` or `!=`.
|
|
156
|
+
*/
|
|
157
|
+
function findSeparator(src: string, word: string): Span | null {
|
|
158
|
+
if (word === '=') {
|
|
159
|
+
const indices = topLevelIndices(src, (ch) => ch === '=');
|
|
160
|
+
for (let k = 0; k < indices.length; k++) {
|
|
161
|
+
const i = indices[k]!;
|
|
162
|
+
if (src[i + 1] === '=') {
|
|
163
|
+
k++;
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
if (src[i - 1] !== '!') return [i, i + 1];
|
|
167
|
+
}
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
const spans = terms(src);
|
|
171
|
+
for (let k = 1; k < spans.length - 1; k++) {
|
|
172
|
+
if (slice(src, spans[k]!) === word) {
|
|
173
|
+
return [spans[k - 1]![1], spans[k + 1]![0]];
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const takesRest = (param: ParameterDef) =>
|
|
180
|
+
param.type === undefined ||
|
|
181
|
+
param.type === 'expression' ||
|
|
182
|
+
param.type === 'text';
|
|
183
|
+
|
|
184
|
+
/** Read the parameters of one group from `src` (`null`: no text) into `args`. */
|
|
185
|
+
function readGroup(
|
|
186
|
+
params: readonly ParameterDef[],
|
|
187
|
+
src: string | null,
|
|
188
|
+
args: Record<string, unknown>,
|
|
189
|
+
): void {
|
|
190
|
+
let text = src?.trim() ?? '';
|
|
191
|
+
for (const flag of params.filter((p) => p.type === 'flag')) {
|
|
192
|
+
const rest = src === null ? null : takeFlag(text, flag.name);
|
|
193
|
+
args[flag.name] = rest !== null;
|
|
194
|
+
text = rest ?? text;
|
|
195
|
+
}
|
|
196
|
+
const positional = params.filter((p) => p.type !== 'flag');
|
|
197
|
+
if (positional.length === 0) return;
|
|
198
|
+
|
|
199
|
+
// The last expression or text parameter, else the last one, reads the rest.
|
|
200
|
+
const lastText = positional.findIndex(
|
|
201
|
+
(p, i) => takesRest(p) && !positional.slice(i + 1).some(takesRest),
|
|
202
|
+
);
|
|
203
|
+
const restIndex = lastText < 0 ? positional.length - 1 : lastText;
|
|
204
|
+
const read = (param: ParameterDef, value: string) =>
|
|
205
|
+
value === '' ? unset(param) : readValue(param, value);
|
|
206
|
+
|
|
207
|
+
// One parameter reads the whole text: no need to split it.
|
|
208
|
+
if (positional.length === 1) {
|
|
209
|
+
args[positional[0]!.name] = read(positional[0]!, text);
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const spans = terms(text);
|
|
214
|
+
let from = 0;
|
|
215
|
+
let to = spans.length;
|
|
216
|
+
for (const param of positional.slice(0, restIndex)) {
|
|
217
|
+
args[param.name] =
|
|
218
|
+
from < to ? read(param, slice(text, spans[from++]!)) : unset(param);
|
|
219
|
+
}
|
|
220
|
+
for (const param of positional.slice(restIndex + 1).reverse()) {
|
|
221
|
+
const value =
|
|
222
|
+
from < to && !endsWithOperator(text.slice(0, spans[to - 1]![0]))
|
|
223
|
+
? read(param, slice(text, spans[to - 1]!))
|
|
224
|
+
: undefined;
|
|
225
|
+
args[param.name] = value ?? unset(param);
|
|
226
|
+
if (value !== undefined) to--;
|
|
227
|
+
}
|
|
228
|
+
const rest = positional[restIndex]!;
|
|
229
|
+
args[rest.name] =
|
|
230
|
+
from < to
|
|
231
|
+
? read(rest, text.slice(spans[from]![0], spans[to - 1]![1]))
|
|
232
|
+
: unset(rest);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/** Read `rawArgs` into the declared `parameters` (see above). */
|
|
236
|
+
export function parseMacroArgs<const P extends readonly ParameterDef[]>(
|
|
237
|
+
rawArgs: string,
|
|
238
|
+
parameters: P,
|
|
239
|
+
): MacroArgs<P> {
|
|
240
|
+
const args: Record<string, unknown> = {};
|
|
241
|
+
// The text after the last separator read, null once one is missing
|
|
242
|
+
let rest: string | null = rawArgs.trim();
|
|
243
|
+
let groupStart = 0;
|
|
244
|
+
parameters.forEach((separator, i) => {
|
|
245
|
+
if (separator.type !== 'separator') return;
|
|
246
|
+
const at = rest === null ? null : findSeparator(rest, separator.name);
|
|
247
|
+
args[separator.name] = at !== null;
|
|
248
|
+
const before = at ? rest!.slice(0, at[0]) : rest;
|
|
249
|
+
readGroup(parameters.slice(groupStart, i), before, args);
|
|
250
|
+
rest = at ? rest!.slice(at[1]) : null;
|
|
251
|
+
groupStart = i + 1;
|
|
252
|
+
});
|
|
253
|
+
readGroup(parameters.slice(groupStart), rest, args);
|
|
254
|
+
return args as MacroArgs<P>;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* The passage a passage-name argument names: the value of its expression
|
|
259
|
+
* or, when that can't be evaluated (`{goto Bob's room}`), its text.
|
|
260
|
+
*/
|
|
261
|
+
export function evaluatePassageName(
|
|
262
|
+
expr: string | undefined,
|
|
263
|
+
evaluate: (expr: string) => unknown,
|
|
264
|
+
): string {
|
|
265
|
+
try {
|
|
266
|
+
return String(evaluate(expr ?? ''));
|
|
267
|
+
} catch {
|
|
268
|
+
return readText(expr ?? '');
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* The variable a `storeVar` macro binds: its first term, as written. Custom
|
|
274
|
+
* macros may bind one without declaring parameters.
|
|
275
|
+
*/
|
|
276
|
+
export function readBoundVariable(rawArgs: string): string {
|
|
277
|
+
const src = rawArgs.trim();
|
|
278
|
+
const first = terms(src)[0];
|
|
279
|
+
return first ? slice(src, first) : '';
|
|
280
|
+
}
|
|
@@ -1,22 +1,17 @@
|
|
|
1
1
|
import type { ASTNode } from '../../markup/ast';
|
|
2
2
|
import { readWholeQuoted } from './arg-utils';
|
|
3
|
+
import { parseMacroArgs } from './macro-args';
|
|
3
4
|
|
|
5
|
+
/** `$var "placeholder"`, the arguments of the input macros. */
|
|
4
6
|
export function parseVarArgs(rawArgs: string): {
|
|
5
7
|
varName: string;
|
|
6
8
|
placeholder: string;
|
|
7
9
|
} {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
}
|
|
13
|
-
const varName = match[1]!.replace(/["']/g, '');
|
|
14
|
-
// A quoted placeholder accepts \" \' and \\ escapes.
|
|
15
|
-
const quoted = match[2];
|
|
16
|
-
const placeholder = quoted
|
|
17
|
-
? (readWholeQuoted(quoted) ?? quoted.slice(1, -1))
|
|
18
|
-
: '';
|
|
19
|
-
return { varName, placeholder };
|
|
10
|
+
const { variable = '', placeholder = '' } = parseMacroArgs(rawArgs, [
|
|
11
|
+
{ name: 'variable', type: 'variable' },
|
|
12
|
+
{ name: 'placeholder', type: 'string' },
|
|
13
|
+
]);
|
|
14
|
+
return { varName: variable.replace(/["']/g, ''), placeholder };
|
|
20
15
|
}
|
|
21
16
|
|
|
22
17
|
/**
|
package/src/define-macro.ts
CHANGED
|
@@ -11,12 +11,9 @@ import {
|
|
|
11
11
|
} from 'preact/hooks';
|
|
12
12
|
import { useInterpolate } from './hooks/use-interpolate';
|
|
13
13
|
import { useMergedLocals } from './hooks/use-merged-locals';
|
|
14
|
+
import { useRenderOptions } from './hooks/use-render-options';
|
|
14
15
|
import {
|
|
15
16
|
LocalsUpdateContext,
|
|
16
|
-
LocalsValuesContext,
|
|
17
|
-
NobrContext,
|
|
18
|
-
InlineContext,
|
|
19
|
-
RawTextContext,
|
|
20
17
|
renderNodes as _renderNodes,
|
|
21
18
|
renderInlineNodes,
|
|
22
19
|
} from './markup/render';
|
|
@@ -31,23 +28,34 @@ import type { UseActionOptions } from './hooks/use-action';
|
|
|
31
28
|
import { collectText } from './utils/extract-text';
|
|
32
29
|
import { currentSourceLocation } from './utils/source-location';
|
|
33
30
|
import { parseVarArgs, extractOptions } from './components/macros/option-utils';
|
|
31
|
+
import {
|
|
32
|
+
parseMacroArgs,
|
|
33
|
+
readBoundVariable,
|
|
34
|
+
} from './components/macros/macro-args';
|
|
34
35
|
import {
|
|
35
36
|
registerMacro,
|
|
36
37
|
registerMacroText,
|
|
37
38
|
registerSubMacro,
|
|
38
39
|
registerMacroMetadata,
|
|
39
40
|
} from './registry';
|
|
40
|
-
import type {
|
|
41
|
+
import type {
|
|
42
|
+
MacroArgs,
|
|
43
|
+
MacroProps,
|
|
44
|
+
MacroTextContext,
|
|
45
|
+
ParameterDef,
|
|
46
|
+
} from './registry';
|
|
41
47
|
import { registerBlockMacro } from './markup/ast';
|
|
42
48
|
|
|
43
|
-
export type { MacroTextContext };
|
|
49
|
+
export type { MacroArgs, MacroTextContext, ParameterDef };
|
|
44
50
|
|
|
45
51
|
export function macroClass(type: string, className?: string): string {
|
|
46
52
|
const base = `macro-${type}`;
|
|
47
53
|
return className ? `${base} ${className}` : base;
|
|
48
54
|
}
|
|
49
55
|
|
|
50
|
-
export interface MacroContext {
|
|
56
|
+
export interface MacroContext<A = MacroArgs> {
|
|
57
|
+
/** The arguments, read into the declared parameters (see ParameterDef). */
|
|
58
|
+
args: A;
|
|
51
59
|
className?: string;
|
|
52
60
|
id?: string;
|
|
53
61
|
resolve?: (s: string | undefined) => string | undefined;
|
|
@@ -86,7 +94,9 @@ export interface MacroContext {
|
|
|
86
94
|
};
|
|
87
95
|
}
|
|
88
96
|
|
|
89
|
-
export interface MacroDefinition
|
|
97
|
+
export interface MacroDefinition<
|
|
98
|
+
P extends readonly ParameterDef[] = ParameterDef[],
|
|
99
|
+
> {
|
|
90
100
|
name: string;
|
|
91
101
|
subMacros?: string[];
|
|
92
102
|
block?: boolean;
|
|
@@ -94,14 +104,20 @@ export interface MacroDefinition {
|
|
|
94
104
|
merged?: boolean;
|
|
95
105
|
storeVar?: boolean;
|
|
96
106
|
description?: string;
|
|
97
|
-
parameters?:
|
|
98
|
-
render: (
|
|
107
|
+
parameters?: P;
|
|
108
|
+
render: (
|
|
109
|
+
props: MacroProps,
|
|
110
|
+
ctx: MacroContext<MacroArgs<P>>,
|
|
111
|
+
) => ComponentChildren;
|
|
99
112
|
/**
|
|
100
113
|
* The macro's text form, used where markup becomes a string: HTML
|
|
101
114
|
* attribute values, image alt text and link titles, macro labels. Without
|
|
102
115
|
* one, the macro can't be used there and is reported as an error.
|
|
103
116
|
*/
|
|
104
|
-
text?: (
|
|
117
|
+
text?: (
|
|
118
|
+
props: MacroProps,
|
|
119
|
+
ctx: MacroTextContext & { args: MacroArgs<P> },
|
|
120
|
+
) => string;
|
|
105
121
|
}
|
|
106
122
|
|
|
107
123
|
const sharedHooks = {
|
|
@@ -114,10 +130,12 @@ const sharedHooks = {
|
|
|
114
130
|
useContext,
|
|
115
131
|
};
|
|
116
132
|
|
|
117
|
-
export function defineMacro(
|
|
118
|
-
config: MacroDefinition
|
|
133
|
+
export function defineMacro<const P extends readonly ParameterDef[] = []>(
|
|
134
|
+
config: MacroDefinition<P>,
|
|
119
135
|
source: 'builtin' | 'user' = 'builtin',
|
|
120
136
|
): void {
|
|
137
|
+
const parameters: readonly ParameterDef[] = config.parameters ?? [];
|
|
138
|
+
|
|
121
139
|
function Wrapper(props: MacroProps) {
|
|
122
140
|
// className/id resolved first (interpolate may transform them)
|
|
123
141
|
let className = props.className;
|
|
@@ -131,10 +149,7 @@ export function defineMacro(
|
|
|
131
149
|
|
|
132
150
|
// Always-on: cssClass + mutation
|
|
133
151
|
const { update, getValues } = useContext(LocalsUpdateContext);
|
|
134
|
-
const
|
|
135
|
-
const inline = useContext(InlineContext);
|
|
136
|
-
const raw = useContext(RawTextContext);
|
|
137
|
-
const localsValues = useContext(LocalsValuesContext);
|
|
152
|
+
const renderOptions = useRenderOptions();
|
|
138
153
|
const renderNodes = (
|
|
139
154
|
nodes: ASTNode[],
|
|
140
155
|
options?: {
|
|
@@ -143,15 +158,9 @@ export function defineMacro(
|
|
|
143
158
|
inline?: boolean;
|
|
144
159
|
raw?: boolean;
|
|
145
160
|
},
|
|
146
|
-
) =>
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
inline,
|
|
150
|
-
raw,
|
|
151
|
-
locals: localsValues,
|
|
152
|
-
...options,
|
|
153
|
-
});
|
|
154
|
-
const ctx: MacroContext = {
|
|
161
|
+
) => _renderNodes(nodes, { ...renderOptions, ...options });
|
|
162
|
+
const ctx: MacroContext<MacroArgs<P>> = {
|
|
163
|
+
args: parseMacroArgs(props.rawArgs, parameters) as MacroArgs<P>,
|
|
155
164
|
collectText,
|
|
156
165
|
sourceLocation: currentSourceLocation,
|
|
157
166
|
parseVarArgs,
|
|
@@ -183,8 +192,7 @@ export function defineMacro(
|
|
|
183
192
|
}
|
|
184
193
|
|
|
185
194
|
if (config.storeVar) {
|
|
186
|
-
const firstToken =
|
|
187
|
-
props.rawArgs.trim().split(/\s+/)[0]?.replace(/["']/g, '') ?? '';
|
|
195
|
+
const firstToken = readBoundVariable(props.rawArgs).replace(/["']/g, '');
|
|
188
196
|
|
|
189
197
|
if (firstToken.startsWith('%')) {
|
|
190
198
|
return h(
|
|
@@ -220,7 +228,16 @@ export function defineMacro(
|
|
|
220
228
|
}
|
|
221
229
|
|
|
222
230
|
registerMacro(config.name, Wrapper);
|
|
223
|
-
|
|
231
|
+
const text = config.text;
|
|
232
|
+
registerMacroText(
|
|
233
|
+
config.name,
|
|
234
|
+
text &&
|
|
235
|
+
((props, ctx) =>
|
|
236
|
+
text(props, {
|
|
237
|
+
...ctx,
|
|
238
|
+
args: parseMacroArgs(props.rawArgs, parameters) as MacroArgs<P>,
|
|
239
|
+
})),
|
|
240
|
+
);
|
|
224
241
|
|
|
225
242
|
// Store metadata for tooling API
|
|
226
243
|
const isBlock =
|
|
@@ -234,7 +251,7 @@ export function defineMacro(
|
|
|
234
251
|
interpolate: config.interpolate,
|
|
235
252
|
merged: config.merged,
|
|
236
253
|
description: config.description,
|
|
237
|
-
parameters: config.parameters,
|
|
254
|
+
parameters: config.parameters && [...config.parameters],
|
|
238
255
|
source,
|
|
239
256
|
});
|
|
240
257
|
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { useContext } from 'preact/hooks';
|
|
2
|
+
import {
|
|
3
|
+
InlineContext,
|
|
4
|
+
LocalsValuesContext,
|
|
5
|
+
NobrContext,
|
|
6
|
+
RawTextContext,
|
|
7
|
+
} from '../markup/render';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The renderNodes() options of the enclosing content: nobr, inline and raw
|
|
11
|
+
* rendering and the locals in scope. A macro or widget body rendered with
|
|
12
|
+
* them renders as the content around it does.
|
|
13
|
+
*/
|
|
14
|
+
export function useRenderOptions(): {
|
|
15
|
+
nobr: boolean;
|
|
16
|
+
inline: boolean;
|
|
17
|
+
raw: boolean;
|
|
18
|
+
locals: Record<string, unknown>;
|
|
19
|
+
} {
|
|
20
|
+
return {
|
|
21
|
+
nobr: useContext(NobrContext),
|
|
22
|
+
inline: useContext(InlineContext),
|
|
23
|
+
raw: useContext(RawTextContext),
|
|
24
|
+
locals: useContext(LocalsValuesContext),
|
|
25
|
+
};
|
|
26
|
+
}
|
package/src/markup/render.tsx
CHANGED
|
@@ -9,6 +9,7 @@ import { markdownToHtml } from './markdown';
|
|
|
9
9
|
import { h } from 'preact';
|
|
10
10
|
import type { ASTNode, HtmlNode, MacroNode } from './ast';
|
|
11
11
|
import { useTextScope } from '../hooks/use-interpolate';
|
|
12
|
+
import { useRenderOptions } from '../hooks/use-render-options';
|
|
12
13
|
import {
|
|
13
14
|
hasInterpolation,
|
|
14
15
|
interpolateCode,
|
|
@@ -524,11 +525,8 @@ function resolveAttributeValue(
|
|
|
524
525
|
|
|
525
526
|
function HtmlNodeRenderer({ node }: { node: HtmlNode }) {
|
|
526
527
|
const scope = useTextScope();
|
|
527
|
-
const nobr =
|
|
528
|
-
const locals = useContext(LocalsValuesContext);
|
|
529
|
-
const inRaw = useContext(RawTextContext);
|
|
528
|
+
const { nobr, locals, raw: inRaw, inline: parentInline } = useRenderOptions();
|
|
530
529
|
const inSvg = useContext(SvgContext);
|
|
531
|
-
const parentInline = useContext(InlineContext);
|
|
532
530
|
const tag = node.tag.toLowerCase();
|
|
533
531
|
const isSvgRoot = tag === 'svg';
|
|
534
532
|
const isRawRoot = !inRaw && (isSvgRoot || PREFORMATTED_ELEMENTS.has(tag));
|
package/src/registry.ts
CHANGED
|
@@ -67,12 +67,60 @@ export function isSubMacro(name: string): boolean {
|
|
|
67
67
|
return subMacros.has(name.toLowerCase());
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
+
/**
|
|
71
|
+
* How a macro argument is read (see components/macros/macro-args.ts).
|
|
72
|
+
* Quoted strings accept `\"`, `\'` and `\\` escapes.
|
|
73
|
+
* - `expression`: code, as written (the default).
|
|
74
|
+
* - `variable`: a variable reference such as `$name` or `"$name"`, as written.
|
|
75
|
+
* - `string`: one quoted string; anything else leaves the argument unset.
|
|
76
|
+
* - `text`: one quoted string, or text with any loose quotes stripped.
|
|
77
|
+
* - `names`: a comma-separated list of names (`@item, @i`).
|
|
78
|
+
* - `delay`: a duration (`2s`, `500ms`, `300`) in milliseconds.
|
|
79
|
+
* - `number`: a number.
|
|
80
|
+
* - `flag`: a keyword, the parameter's name, at the start or end; a boolean.
|
|
81
|
+
* - `separator`: a word (`of`) or `=` separating the parameters before it
|
|
82
|
+
* from those after it; a boolean.
|
|
83
|
+
* - `options`: keywords, the names of its `parameters`, each followed by a
|
|
84
|
+
* quoted string or a number unless it is a flag.
|
|
85
|
+
*/
|
|
86
|
+
export type ParameterType =
|
|
87
|
+
| 'expression'
|
|
88
|
+
| 'variable'
|
|
89
|
+
| 'string'
|
|
90
|
+
| 'text'
|
|
91
|
+
| 'names'
|
|
92
|
+
| 'delay'
|
|
93
|
+
| 'number'
|
|
94
|
+
| 'flag'
|
|
95
|
+
| 'separator'
|
|
96
|
+
| 'options';
|
|
97
|
+
|
|
70
98
|
export interface ParameterDef {
|
|
71
99
|
name: string;
|
|
72
100
|
required?: boolean;
|
|
73
101
|
description?: string;
|
|
102
|
+
type?: ParameterType;
|
|
103
|
+
/** The options of an `options` parameter. */
|
|
104
|
+
parameters?: readonly ParameterDef[];
|
|
74
105
|
}
|
|
75
106
|
|
|
107
|
+
type ArgValue<T, D> = T extends 'flag' | 'separator'
|
|
108
|
+
? boolean
|
|
109
|
+
: T extends 'names'
|
|
110
|
+
? string[] | undefined
|
|
111
|
+
: T extends 'delay' | 'number'
|
|
112
|
+
? number | undefined
|
|
113
|
+
: T extends 'options'
|
|
114
|
+
? D extends { parameters: infer Q extends readonly ParameterDef[] }
|
|
115
|
+
? Partial<MacroArgs<Q>>
|
|
116
|
+
: Partial<MacroArgs>
|
|
117
|
+
: string | undefined;
|
|
118
|
+
|
|
119
|
+
/** The arguments of a macro declaring `parameters`, by parameter name. */
|
|
120
|
+
export type MacroArgs<P extends readonly ParameterDef[] = ParameterDef[]> = {
|
|
121
|
+
[D in P[number] as D['name']]: ArgValue<D['type'], D>;
|
|
122
|
+
};
|
|
123
|
+
|
|
76
124
|
export interface MacroMetadata {
|
|
77
125
|
name: string;
|
|
78
126
|
block: boolean;
|