@rohal12/spindle 0.52.5 → 0.52.7
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 +705 -576
- package/dist/pkg/macro-registry.json +255 -0
- package/dist/pkg/story-variables.js +54 -32
- 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/js-lexer.ts +80 -45
- 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
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/js-lexer.ts
CHANGED
|
@@ -397,6 +397,14 @@ const MALFORMED = -2;
|
|
|
397
397
|
/** A frame result: the scan stops at index `s` inside it (`STOPPED - s`). */
|
|
398
398
|
const STOPPED = -3;
|
|
399
399
|
|
|
400
|
+
/**
|
|
401
|
+
* The result of a frame that closes at `end`. `bodyStarted`: the code inside
|
|
402
|
+
* started a function or class body. That body replaced the one to come, and
|
|
403
|
+
* is gone once the frame is closed, so after the frame no body is to come —
|
|
404
|
+
* a scan that skips the frame must know, as must the frames around it.
|
|
405
|
+
*/
|
|
406
|
+
const frameEnd = (end: number, bodyStarted: boolean) => end * 2 + +bodyStarted;
|
|
407
|
+
|
|
400
408
|
/** The closers whose search for their frame a frame result may depend on. */
|
|
401
409
|
const CLOSERS = [')', ']', '}'] as const;
|
|
402
410
|
const FRAME_KINDS: readonly Frame['kind'][] = [
|
|
@@ -423,11 +431,13 @@ function frameKey(f: Frame): number | undefined {
|
|
|
423
431
|
*/
|
|
424
432
|
export interface JsScanCache {
|
|
425
433
|
/**
|
|
426
|
-
* Where each `{…}` frame, template literal or `${…}` interpolation closes
|
|
427
|
-
*
|
|
428
|
-
* code inside
|
|
429
|
-
*
|
|
430
|
-
*
|
|
434
|
+
* Where each `{…}` frame, template literal or `${…}` interpolation closes,
|
|
435
|
+
* as a frame result (`frameEnd`): its `}` or closing backtick, and whether
|
|
436
|
+
* the code inside started a function or class body; or `UNCLOSED` or
|
|
437
|
+
* `MALFORMED`. The code inside such a frame lexes the same whatever
|
|
438
|
+
* surrounds it, given where it opens and its kind — a stray `)` or `]`
|
|
439
|
+
* inside never closes it — so a later scan entering the same frame skips
|
|
440
|
+
* to its end.
|
|
431
441
|
*/
|
|
432
442
|
braces: Map<number, number>;
|
|
433
443
|
/** Look-ahead bracket matches (`ScanContext.brackets`). */
|
|
@@ -457,10 +467,8 @@ export interface JsScanCache {
|
|
|
457
467
|
stacks: Map<string, number>;
|
|
458
468
|
/**
|
|
459
469
|
* How `(…)` and `[…]` frames end, by the kind of scan and then by the frame
|
|
460
|
-
* (`parenKey`):
|
|
461
|
-
*
|
|
462
|
-
* can't follow once the frame is closed), `end * 2` otherwise, or
|
|
463
|
-
* `UNCLOSED`, `MALFORMED` or `STOPPED - s`. Unlike braces, a stray closer inside may close a frame
|
|
470
|
+
* (`parenKey`): a frame result (`frameEnd`), or `UNCLOSED`, `MALFORMED` or
|
|
471
|
+
* `STOPPED - s`. Unlike braces, a stray closer inside may close a frame
|
|
464
472
|
* around them, so the code inside lexes the same only around frames for
|
|
465
473
|
* which the closers it met find nothing to close; the key says which
|
|
466
474
|
* closers met none.
|
|
@@ -748,15 +756,34 @@ function scan(
|
|
|
748
756
|
: undefined;
|
|
749
757
|
/**
|
|
750
758
|
* How many function or class bodies to come the scan has started, and with
|
|
751
|
-
* `parens`, that count when each open frame opened. Code in a
|
|
752
|
-
*
|
|
753
|
-
* follow once the frame is closed: none is to come then.
|
|
759
|
+
* `braces` or `parens`, that count when each open frame opened. Code in a
|
|
760
|
+
* frame that starts one replaces the one to come, and the new one can't
|
|
761
|
+
* follow once the frame is closed: none is to come then. A scan skipping
|
|
762
|
+
* the frame learns this from its result (`frameEnd`, `resume`), and counts
|
|
763
|
+
* the body as started too, for the frames around it to record.
|
|
754
764
|
*/
|
|
755
765
|
let bodyStarts = 0;
|
|
756
|
-
const startsAt: number[] | undefined = parens ? [0] : undefined;
|
|
766
|
+
const startsAt: number[] | undefined = braces || parens ? [0] : undefined;
|
|
757
767
|
/** A `(…)` or `[…]` frame result found in `parens`, to skip to. */
|
|
758
768
|
let parenTo: { frame: Frame; result: number } | undefined;
|
|
759
769
|
|
|
770
|
+
/** The result of the frame at index `k`, closing at `i`. */
|
|
771
|
+
const closedAt = (k: number) =>
|
|
772
|
+
frameEnd(i, startsAt !== undefined && bodyStarts !== startsAt[k]);
|
|
773
|
+
|
|
774
|
+
/**
|
|
775
|
+
* Go on after a frame an earlier scan lexed, whose result (`frameEnd`) is
|
|
776
|
+
* `result`: returns the index it closes at. A body started in it is gone
|
|
777
|
+
* after it, with the one it replaced.
|
|
778
|
+
*/
|
|
779
|
+
function resume(result: number): number {
|
|
780
|
+
if (result % 2 === 1) {
|
|
781
|
+
pendingBody = undefined;
|
|
782
|
+
bodyStarts++;
|
|
783
|
+
}
|
|
784
|
+
return Math.floor(result / 2);
|
|
785
|
+
}
|
|
786
|
+
|
|
760
787
|
/** The closer `c` looked for its frame and found index `k`. */
|
|
761
788
|
function lookedFor(c: (typeof CLOSERS)[number], k: number) {
|
|
762
789
|
if (!lowest) return;
|
|
@@ -933,8 +960,8 @@ function scan(
|
|
|
933
960
|
if (lowest) {
|
|
934
961
|
for (let j = frames.length - 1; j >= k; j--) foldLowest(j);
|
|
935
962
|
lowest.length = k;
|
|
936
|
-
startsAt!.length = k;
|
|
937
963
|
}
|
|
964
|
+
if (startsAt) startsAt.length = k;
|
|
938
965
|
frames.length = k;
|
|
939
966
|
if (stackIds) stackIds.length = k;
|
|
940
967
|
for (const list of Object.values(closers)) {
|
|
@@ -1034,6 +1061,19 @@ function scan(
|
|
|
1034
1061
|
keyNext = f.kind === 'object' || f.kind === 'class';
|
|
1035
1062
|
}
|
|
1036
1063
|
|
|
1064
|
+
/**
|
|
1065
|
+
* Go past frame `f` if an earlier scan lexed it: to just after it, or to
|
|
1066
|
+
* the end of the source when it never closes. Whether it did, or
|
|
1067
|
+
* MALFORMED when that scan found the code malformed.
|
|
1068
|
+
*/
|
|
1069
|
+
function skipLexed(f: Frame): boolean | typeof MALFORMED {
|
|
1070
|
+
const end = known(f);
|
|
1071
|
+
if (end === undefined) return false;
|
|
1072
|
+
if (end === MALFORMED) return MALFORMED;
|
|
1073
|
+
i = end === UNCLOSED ? src.length : resume(end) + 1;
|
|
1074
|
+
return true;
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1037
1077
|
function openBrace() {
|
|
1038
1078
|
let f: Frame;
|
|
1039
1079
|
if (pendingBody?.depth === frames.length) {
|
|
@@ -1079,12 +1119,11 @@ function scan(
|
|
|
1079
1119
|
return;
|
|
1080
1120
|
}
|
|
1081
1121
|
const closed = frames[k]!;
|
|
1122
|
+
const result = closedAt(k);
|
|
1082
1123
|
if (lowest && c !== '}') {
|
|
1083
1124
|
// Its entry complete, with those of the frames still open in it
|
|
1084
1125
|
for (let j = frames.length - 1; j > k; j--) foldLowest(j);
|
|
1085
|
-
|
|
1086
|
-
const bodyStarted = bodyStarts !== startsAt![k];
|
|
1087
|
-
recordParen(k, i * 2 + +bodyStarted);
|
|
1126
|
+
recordParen(k, result);
|
|
1088
1127
|
}
|
|
1089
1128
|
truncate(k);
|
|
1090
1129
|
if (c === ']' && !ctx.lookahead) ctx.brackets.set(closed.open, i);
|
|
@@ -1095,7 +1134,7 @@ function scan(
|
|
|
1095
1134
|
} else if (c === ']') {
|
|
1096
1135
|
endPunct(c, false);
|
|
1097
1136
|
} else {
|
|
1098
|
-
record(closed,
|
|
1137
|
+
record(closed, result);
|
|
1099
1138
|
afterBrace(closed);
|
|
1100
1139
|
}
|
|
1101
1140
|
}
|
|
@@ -1214,24 +1253,17 @@ function scan(
|
|
|
1214
1253
|
} else if (ch === '`') {
|
|
1215
1254
|
literal(ch, i);
|
|
1216
1255
|
if (frames.length === 1) return i + 1; // the end of `lexTemplate`
|
|
1217
|
-
record(top(),
|
|
1256
|
+
record(top(), closedAt(frames.length - 1));
|
|
1218
1257
|
i++;
|
|
1219
1258
|
truncate(frames.length - 1);
|
|
1220
1259
|
endOperand();
|
|
1221
1260
|
} else if (ch === '$' && src.charAt(i + 1) === '{') {
|
|
1222
1261
|
const f = frame('expr', '}', i);
|
|
1223
1262
|
f.interpolation = true;
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
if (
|
|
1227
|
-
|
|
1228
|
-
break;
|
|
1229
|
-
}
|
|
1230
|
-
if (end !== undefined) {
|
|
1231
|
-
// An interpolation an earlier scan lexed: on with the text after it
|
|
1232
|
-
i = end + 1;
|
|
1233
|
-
continue;
|
|
1234
|
-
}
|
|
1263
|
+
// An interpolation an earlier scan lexed: on with the text after it
|
|
1264
|
+
const skipped = skipLexed(f);
|
|
1265
|
+
if (skipped === MALFORMED) return malformed();
|
|
1266
|
+
if (skipped) continue;
|
|
1235
1267
|
literal('${', i);
|
|
1236
1268
|
i += 2;
|
|
1237
1269
|
nesting++;
|
|
@@ -1261,15 +1293,10 @@ function scan(
|
|
|
1261
1293
|
if (ch === '`') {
|
|
1262
1294
|
endWord();
|
|
1263
1295
|
const f = frame('template', '`', i);
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
if (
|
|
1267
|
-
|
|
1268
|
-
break;
|
|
1269
|
-
}
|
|
1270
|
-
if (end !== undefined) {
|
|
1271
|
-
// A template literal an earlier scan lexed: on after it
|
|
1272
|
-
i = end + 1;
|
|
1296
|
+
// A template literal an earlier scan lexed: on after it
|
|
1297
|
+
const skipped = skipLexed(f);
|
|
1298
|
+
if (skipped === MALFORMED) return malformed();
|
|
1299
|
+
if (skipped) {
|
|
1273
1300
|
endOperand();
|
|
1274
1301
|
continue;
|
|
1275
1302
|
}
|
|
@@ -1291,7 +1318,7 @@ function scan(
|
|
|
1291
1318
|
lookedFor('}', k);
|
|
1292
1319
|
if (frames[k]!.interpolation) {
|
|
1293
1320
|
endWord();
|
|
1294
|
-
record(frames[k]!,
|
|
1321
|
+
record(frames[k]!, closedAt(k));
|
|
1295
1322
|
truncate(k);
|
|
1296
1323
|
nesting--;
|
|
1297
1324
|
literal(ch, i);
|
|
@@ -1399,8 +1426,17 @@ function scan(
|
|
|
1399
1426
|
}
|
|
1400
1427
|
}
|
|
1401
1428
|
|
|
1402
|
-
// End of an `outer` square bracket
|
|
1403
|
-
|
|
1429
|
+
// End of an `outer` square bracket: a `]` that closes no `[` within the
|
|
1430
|
+
// innermost braces closes it, over any `(` left open in it, as it closes
|
|
1431
|
+
// a nested `[`. Look-ahead scans record where the `[`s nested in theirs
|
|
1432
|
+
// end (`ctx.brackets`), so one starting at a `[` must find the same.
|
|
1433
|
+
if (
|
|
1434
|
+
ch === outer.closer &&
|
|
1435
|
+
(frames.length === 1 ||
|
|
1436
|
+
(ch === ']' && innermost(']') === 0 && innermost('}') === 0))
|
|
1437
|
+
) {
|
|
1438
|
+
break;
|
|
1439
|
+
}
|
|
1404
1440
|
|
|
1405
1441
|
// Regular code character
|
|
1406
1442
|
trackCode(ch);
|
|
@@ -1413,7 +1449,7 @@ function scan(
|
|
|
1413
1449
|
i = src.length;
|
|
1414
1450
|
break;
|
|
1415
1451
|
}
|
|
1416
|
-
i = end;
|
|
1452
|
+
i = resume(end);
|
|
1417
1453
|
afterBrace(skipped);
|
|
1418
1454
|
i++;
|
|
1419
1455
|
continue;
|
|
@@ -1432,8 +1468,7 @@ function scan(
|
|
|
1432
1468
|
strict!.stopped = true;
|
|
1433
1469
|
return STOPPED - result;
|
|
1434
1470
|
}
|
|
1435
|
-
i =
|
|
1436
|
-
if (result % 2 === 1) pendingBody = undefined;
|
|
1471
|
+
i = resume(result);
|
|
1437
1472
|
if (skipped.closer === ')') {
|
|
1438
1473
|
endPunct(')', skipped.header);
|
|
1439
1474
|
stmtNext = skipped.header;
|
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;
|
package/types/index.d.ts
CHANGED
|
@@ -239,6 +239,35 @@ export interface StorageQuota {
|
|
|
239
239
|
estimateSupported: boolean;
|
|
240
240
|
}
|
|
241
241
|
|
|
242
|
+
/**
|
|
243
|
+
* How a macro argument is read into `ctx.args`. Quoted strings accept `\"`,
|
|
244
|
+
* `\'` and `\\` escapes.
|
|
245
|
+
* - `expression`: code, as written (the default).
|
|
246
|
+
* - `variable`: a variable reference such as `$name` or `"$name"`, as written.
|
|
247
|
+
* - `string`: one quoted string; anything else leaves the argument unset.
|
|
248
|
+
* - `text`: one quoted string, or text with any loose quotes stripped.
|
|
249
|
+
* - `names`: a comma-separated list of names (`@item, @i`).
|
|
250
|
+
* - `delay`: a duration (`2s`, `500ms`, `300`) in milliseconds.
|
|
251
|
+
* - `number`: a number.
|
|
252
|
+
* - `flag`: a keyword, the parameter's name, at the start or end; a boolean.
|
|
253
|
+
* - `separator`: a word (`of`) or `=` separating the parameters before it
|
|
254
|
+
* from those after it; a boolean.
|
|
255
|
+
* - `options`: keywords, the names of its `parameters`, each followed by a
|
|
256
|
+
* quoted string or a number unless it is a flag.
|
|
257
|
+
* @see {@link ../../src/registry.ts} for the implementation.
|
|
258
|
+
*/
|
|
259
|
+
export type ParameterType =
|
|
260
|
+
| 'expression'
|
|
261
|
+
| 'variable'
|
|
262
|
+
| 'string'
|
|
263
|
+
| 'text'
|
|
264
|
+
| 'names'
|
|
265
|
+
| 'delay'
|
|
266
|
+
| 'number'
|
|
267
|
+
| 'flag'
|
|
268
|
+
| 'separator'
|
|
269
|
+
| 'options';
|
|
270
|
+
|
|
242
271
|
/**
|
|
243
272
|
* Parameter metadata for a macro definition.
|
|
244
273
|
* @see {@link ../../src/registry.ts} for the implementation.
|
|
@@ -247,8 +276,32 @@ export interface ParameterDef {
|
|
|
247
276
|
name: string;
|
|
248
277
|
required?: boolean;
|
|
249
278
|
description?: string;
|
|
279
|
+
/** How the argument is read (default `expression`). */
|
|
280
|
+
type?: ParameterType;
|
|
281
|
+
/** The options of an `options` parameter. */
|
|
282
|
+
parameters?: readonly ParameterDef[];
|
|
250
283
|
}
|
|
251
284
|
|
|
285
|
+
type ArgValue<T, D> = T extends 'flag' | 'separator'
|
|
286
|
+
? boolean
|
|
287
|
+
: T extends 'names'
|
|
288
|
+
? string[] | undefined
|
|
289
|
+
: T extends 'delay' | 'number'
|
|
290
|
+
? number | undefined
|
|
291
|
+
: T extends 'options'
|
|
292
|
+
? D extends { parameters: infer Q extends readonly ParameterDef[] }
|
|
293
|
+
? Partial<MacroArgs<Q>>
|
|
294
|
+
: Partial<MacroArgs>
|
|
295
|
+
: string | undefined;
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* A macro's arguments (`ctx.args`), by parameter name.
|
|
299
|
+
* @see {@link ../../src/registry.ts} for the implementation.
|
|
300
|
+
*/
|
|
301
|
+
export type MacroArgs<P extends readonly ParameterDef[] = ParameterDef[]> = {
|
|
302
|
+
[D in P[number] as D['name']]: ArgValue<D['type'], D>;
|
|
303
|
+
};
|
|
304
|
+
|
|
252
305
|
/**
|
|
253
306
|
* Metadata about a registered macro, returned by `Story.getMacroRegistry()`.
|
|
254
307
|
* @see {@link ../../src/registry.ts} for the implementation.
|
|
@@ -363,7 +416,9 @@ export interface UseActionOptions {
|
|
|
363
416
|
* this package), so `ctx.hooks.useState<T>()`, `ctx.h()` etc. are fully typed.
|
|
364
417
|
* @see {@link ../../src/define-macro.ts} for the implementation.
|
|
365
418
|
*/
|
|
366
|
-
export interface MacroContext {
|
|
419
|
+
export interface MacroContext<A = MacroArgs> {
|
|
420
|
+
/** The arguments, read into the macro's declared `parameters`. */
|
|
421
|
+
args: A;
|
|
367
422
|
className?: string;
|
|
368
423
|
id?: string;
|
|
369
424
|
resolve?: (s: string | undefined) => string | undefined;
|
|
@@ -437,7 +492,9 @@ export interface MacroTextContext {
|
|
|
437
492
|
* Configuration object for `Story.defineMacro()`.
|
|
438
493
|
* @see {@link ../../src/define-macro.ts} for the implementation.
|
|
439
494
|
*/
|
|
440
|
-
export interface MacroDefinition
|
|
495
|
+
export interface MacroDefinition<
|
|
496
|
+
P extends readonly ParameterDef[] = ParameterDef[],
|
|
497
|
+
> {
|
|
441
498
|
name: string;
|
|
442
499
|
/** Sub-macro names (e.g. `['option']`); a non-empty list makes the macro a block macro. */
|
|
443
500
|
subMacros?: string[];
|
|
@@ -451,15 +508,21 @@ export interface MacroDefinition {
|
|
|
451
508
|
storeVar?: boolean;
|
|
452
509
|
/** Tooling hint: one-line description shown by editors. */
|
|
453
510
|
description?: string;
|
|
454
|
-
/**
|
|
455
|
-
parameters?:
|
|
456
|
-
render: (
|
|
511
|
+
/** The parameters: read into `ctx.args`, and shown by tooling. */
|
|
512
|
+
parameters?: P;
|
|
513
|
+
render: (
|
|
514
|
+
props: MacroProps,
|
|
515
|
+
ctx: MacroContext<MacroArgs<P>>,
|
|
516
|
+
) => ComponentChildren;
|
|
457
517
|
/**
|
|
458
518
|
* The macro's text form, used where markup becomes a string: HTML
|
|
459
519
|
* attribute values, image alt text and link titles, macro labels. Without
|
|
460
520
|
* one, the macro can't be used there and is reported as an error.
|
|
461
521
|
*/
|
|
462
|
-
text?: (
|
|
522
|
+
text?: (
|
|
523
|
+
props: MacroProps,
|
|
524
|
+
ctx: MacroTextContext & { args: MacroArgs<P> },
|
|
525
|
+
) => string;
|
|
463
526
|
}
|
|
464
527
|
|
|
465
528
|
/**
|
package/types/tooling.d.ts
CHANGED
|
@@ -1,7 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How a macro argument is read into `ctx.args`. Quoted strings accept `\"`,
|
|
3
|
+
* `\'` and `\\` escapes.
|
|
4
|
+
* - `expression`: code, as written (the default).
|
|
5
|
+
* - `variable`: a variable reference such as `$name` or `"$name"`, as written.
|
|
6
|
+
* - `string`: one quoted string; anything else leaves the argument unset.
|
|
7
|
+
* - `text`: one quoted string, or text with any loose quotes stripped.
|
|
8
|
+
* - `names`: a comma-separated list of names (`@item, @i`).
|
|
9
|
+
* - `delay`: a duration (`2s`, `500ms`, `300`) in milliseconds.
|
|
10
|
+
* - `number`: a number.
|
|
11
|
+
* - `flag`: a keyword, the parameter's name, at the start or end; a boolean.
|
|
12
|
+
* - `separator`: a word (`of`) or `=` separating the parameters before it
|
|
13
|
+
* from those after it; a boolean.
|
|
14
|
+
* - `options`: keywords, the names of its `parameters`, each followed by a
|
|
15
|
+
* quoted string or a number unless it is a flag.
|
|
16
|
+
*/
|
|
17
|
+
export type ParameterType =
|
|
18
|
+
| 'expression'
|
|
19
|
+
| 'variable'
|
|
20
|
+
| 'string'
|
|
21
|
+
| 'text'
|
|
22
|
+
| 'names'
|
|
23
|
+
| 'delay'
|
|
24
|
+
| 'number'
|
|
25
|
+
| 'flag'
|
|
26
|
+
| 'separator'
|
|
27
|
+
| 'options';
|
|
28
|
+
|
|
1
29
|
export interface ParameterDef {
|
|
2
30
|
name: string;
|
|
3
31
|
required?: boolean;
|
|
4
32
|
description?: string;
|
|
33
|
+
/** How the argument is read (default `expression`). */
|
|
34
|
+
type?: ParameterType;
|
|
35
|
+
/** The options of an `options` parameter. */
|
|
36
|
+
parameters?: readonly ParameterDef[];
|
|
5
37
|
}
|
|
6
38
|
|
|
7
39
|
export interface MacroMetadata {
|
|
@@ -24,7 +56,7 @@ export interface MacroDefinition {
|
|
|
24
56
|
merged?: boolean;
|
|
25
57
|
storeVar?: boolean;
|
|
26
58
|
description?: string;
|
|
27
|
-
parameters?: ParameterDef[];
|
|
59
|
+
parameters?: readonly ParameterDef[];
|
|
28
60
|
render: (...args: any[]) => any;
|
|
29
61
|
text?: (...args: any[]) => string;
|
|
30
62
|
}
|