@rohal12/spindle 0.52.4 → 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 +1502 -1521
- package/dist/pkg/macro-registry.json +255 -0
- package/package.json +1 -1
- package/src/class-registry.ts +6 -182
- 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/execute-mutation.ts +17 -97
- package/src/hooks/use-render-options.ts +26 -0
- package/src/markup/render.tsx +2 -4
- package/src/registry.ts +48 -0
- package/src/saves/save-manager.ts +190 -259
- package/src/saves/storage.ts +229 -402
- package/src/store.ts +244 -310
- package/src/structural.ts +388 -0
- package/types/index.d.ts +69 -6
- package/types/tooling.d.ts +33 -1
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
// Structural operations on story values: copying, comparing, and finding
|
|
2
|
+
// and merging the differences between two versions of a value. Mutation
|
|
3
|
+
// commits, history and save hooks all go through these, so they agree on
|
|
4
|
+
// what a change is.
|
|
5
|
+
|
|
6
|
+
import { registeredClassName } from './class-registry';
|
|
7
|
+
import { hasOwn } from './utils/namespace';
|
|
8
|
+
import { deleteByPath, getByPath, setByPath } from './utils/object-path';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Set an own enumerable property. A "__proto__" key is defined as an own
|
|
12
|
+
* property (as JSON.parse does) instead of replacing the prototype.
|
|
13
|
+
*/
|
|
14
|
+
export function setOwn(target: object, key: string, value: unknown): void {
|
|
15
|
+
if (key === '__proto__') {
|
|
16
|
+
Object.defineProperty(target, key, {
|
|
17
|
+
value,
|
|
18
|
+
enumerable: true,
|
|
19
|
+
writable: true,
|
|
20
|
+
configurable: true,
|
|
21
|
+
});
|
|
22
|
+
} else {
|
|
23
|
+
(target as Record<string, unknown>)[key] = value;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// --- Deep Clone ---
|
|
28
|
+
|
|
29
|
+
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
30
|
+
if (typeof value !== 'object' || value === null) return false;
|
|
31
|
+
const proto = Object.getPrototypeOf(value);
|
|
32
|
+
return proto === Object.prototype || proto === null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface DeepCloneOptions {
|
|
36
|
+
/**
|
|
37
|
+
* Return instances of unregistered classes (DOM nodes, promises, other
|
|
38
|
+
* library objects) by reference instead of copying their own keys into a
|
|
39
|
+
* plain object, which would lose their prototype and identity.
|
|
40
|
+
*/
|
|
41
|
+
keepUnregistered?: boolean;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function deepClone<T>(value: T, options: DeepCloneOptions = {}): T {
|
|
45
|
+
const seen = new Map<object, object>();
|
|
46
|
+
|
|
47
|
+
function clone(val: unknown): unknown {
|
|
48
|
+
if (val === null || typeof val !== 'object') return val;
|
|
49
|
+
|
|
50
|
+
const obj = val as object;
|
|
51
|
+
if (seen.has(obj)) return seen.get(obj);
|
|
52
|
+
|
|
53
|
+
if (val instanceof Date) return new Date(val.getTime()) as unknown;
|
|
54
|
+
if (val instanceof RegExp)
|
|
55
|
+
return new RegExp(val.source, val.flags) as unknown;
|
|
56
|
+
|
|
57
|
+
if (Array.isArray(val)) {
|
|
58
|
+
const arr: unknown[] = [];
|
|
59
|
+
seen.set(obj, arr);
|
|
60
|
+
for (let i = 0; i < val.length; i++) {
|
|
61
|
+
arr[i] = clone(val[i]);
|
|
62
|
+
}
|
|
63
|
+
return arr;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (val instanceof Map) {
|
|
67
|
+
const copy = new Map();
|
|
68
|
+
seen.set(obj, copy);
|
|
69
|
+
for (const [k, v] of val) {
|
|
70
|
+
copy.set(clone(k), clone(v));
|
|
71
|
+
}
|
|
72
|
+
return copy;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (val instanceof Set) {
|
|
76
|
+
const copy = new Set();
|
|
77
|
+
seen.set(obj, copy);
|
|
78
|
+
for (const v of val) {
|
|
79
|
+
copy.add(clone(v));
|
|
80
|
+
}
|
|
81
|
+
return copy;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// A registered class instance keeps its class, and a plain object its
|
|
85
|
+
// prototype (which may be null). An instance of an unregistered class
|
|
86
|
+
// is treated as plain, unless it is kept by reference.
|
|
87
|
+
const keepsProto =
|
|
88
|
+
registeredClassName(obj) !== undefined || isPlainObject(val);
|
|
89
|
+
if (!keepsProto && options.keepUnregistered) return val;
|
|
90
|
+
const copy = (
|
|
91
|
+
keepsProto
|
|
92
|
+
? Object.create(Object.getPrototypeOf(obj) as object | null)
|
|
93
|
+
: {}
|
|
94
|
+
) as Record<string, unknown>;
|
|
95
|
+
seen.set(obj, copy);
|
|
96
|
+
for (const key of Object.keys(obj)) {
|
|
97
|
+
setOwn(copy, key, clone((obj as Record<string, unknown>)[key]));
|
|
98
|
+
}
|
|
99
|
+
return copy;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return clone(value) as T;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// --- Deep Equal ---
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Structural equality over the value types deepClone() supports: primitives,
|
|
109
|
+
* arrays, plain objects, class instances, Date, RegExp, Map and Set (nested
|
|
110
|
+
* at any depth). Map and Set entries are compared in insertion order. Arrays
|
|
111
|
+
* are compared by length and index, so a hole equals an undefined element
|
|
112
|
+
* (deepClone() and save/load turn holes into undefined elements).
|
|
113
|
+
*/
|
|
114
|
+
export function deepEqual(a: unknown, b: unknown): boolean {
|
|
115
|
+
return equal(a, b, new Map());
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* `assumed` holds the pairs already being compared: meeting one again (a
|
|
120
|
+
* cycle) assumes it equal. Every object may pair with several others, since
|
|
121
|
+
* cycles of different lengths can still unfold to the same value.
|
|
122
|
+
*/
|
|
123
|
+
function equal(
|
|
124
|
+
a: unknown,
|
|
125
|
+
b: unknown,
|
|
126
|
+
assumed: Map<object, Set<object>>,
|
|
127
|
+
): boolean {
|
|
128
|
+
if (Object.is(a, b)) return true;
|
|
129
|
+
if (
|
|
130
|
+
a === null ||
|
|
131
|
+
b === null ||
|
|
132
|
+
typeof a !== 'object' ||
|
|
133
|
+
typeof b !== 'object'
|
|
134
|
+
) {
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
if (Object.getPrototypeOf(a) !== Object.getPrototypeOf(b)) return false;
|
|
138
|
+
const pairs = assumed.get(a);
|
|
139
|
+
if (pairs?.has(b)) return true;
|
|
140
|
+
if (pairs) pairs.add(b);
|
|
141
|
+
else assumed.set(a, new Set([b]));
|
|
142
|
+
|
|
143
|
+
if (a instanceof Date) return Object.is(a.getTime(), (b as Date).getTime());
|
|
144
|
+
if (a instanceof RegExp) return String(a) === String(b);
|
|
145
|
+
if (a instanceof Map || a instanceof Set) {
|
|
146
|
+
const bc = b as Map<unknown, unknown> | Set<unknown>;
|
|
147
|
+
if (a.size !== bc.size) return false;
|
|
148
|
+
const ai = a.entries();
|
|
149
|
+
const bi = bc.entries();
|
|
150
|
+
for (
|
|
151
|
+
let x = ai.next(), y = bi.next();
|
|
152
|
+
!x.done;
|
|
153
|
+
x = ai.next(), y = bi.next()
|
|
154
|
+
) {
|
|
155
|
+
if (!equal(x.value, y.value, assumed)) return false;
|
|
156
|
+
}
|
|
157
|
+
return true;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (Array.isArray(a)) {
|
|
161
|
+
const bc = b as unknown[];
|
|
162
|
+
if (a.length !== bc.length) return false;
|
|
163
|
+
for (let i = 0; i < a.length; i++) {
|
|
164
|
+
if (!equal(a[i], bc[i], assumed)) return false;
|
|
165
|
+
}
|
|
166
|
+
return true;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const ao = a as Record<string, unknown>;
|
|
170
|
+
const bo = b as Record<string, unknown>;
|
|
171
|
+
const keys = Object.keys(ao);
|
|
172
|
+
if (keys.length !== Object.keys(bo).length) return false;
|
|
173
|
+
for (const key of keys) {
|
|
174
|
+
if (!hasOwn(bo, key) || !equal(ao[key], bo[key], assumed)) return false;
|
|
175
|
+
}
|
|
176
|
+
return true;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// --- Structural diff ---
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Objects merged property by property: plain objects and (registered)
|
|
183
|
+
* class instances. Arrays, Map, Set, Date and RegExp are values that are
|
|
184
|
+
* replaced as a whole, since their elements have no stable identity to
|
|
185
|
+
* merge by (a shift moves every index).
|
|
186
|
+
*/
|
|
187
|
+
export function isMergeable(value: unknown): value is Record<string, unknown> {
|
|
188
|
+
return (
|
|
189
|
+
typeof value === 'object' &&
|
|
190
|
+
value !== null &&
|
|
191
|
+
!Array.isArray(value) &&
|
|
192
|
+
!(value instanceof Map) &&
|
|
193
|
+
!(value instanceof Set) &&
|
|
194
|
+
!(value instanceof Date) &&
|
|
195
|
+
!(value instanceof RegExp)
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Whether changes between `a` and `b` merge key by key: both are objects of
|
|
201
|
+
* one class (see isMergeable), or, with `arrays`, both are arrays.
|
|
202
|
+
*/
|
|
203
|
+
export function mergesWith(
|
|
204
|
+
a: unknown,
|
|
205
|
+
b: unknown,
|
|
206
|
+
arrays: boolean,
|
|
207
|
+
): a is Record<string, unknown> {
|
|
208
|
+
if (arrays && (Array.isArray(a) || Array.isArray(b))) {
|
|
209
|
+
return Array.isArray(a) && Array.isArray(b);
|
|
210
|
+
}
|
|
211
|
+
return (
|
|
212
|
+
isMergeable(a) &&
|
|
213
|
+
isMergeable(b) &&
|
|
214
|
+
Object.getPrototypeOf(a) === Object.getPrototypeOf(b)
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/** How a value differs from an earlier version at one key. */
|
|
219
|
+
export type KeyChange =
|
|
220
|
+
| { key: string; kind: 'added' | 'changed'; after: unknown }
|
|
221
|
+
| { key: string; kind: 'deleted' }
|
|
222
|
+
| {
|
|
223
|
+
key: string;
|
|
224
|
+
kind: 'nested';
|
|
225
|
+
before: Record<string, unknown>;
|
|
226
|
+
after: Record<string, unknown>;
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* The keys at which `after` differs from `before`, two values that merge
|
|
231
|
+
* (see mergesWith): keys added, deleted, or holding another value. Store
|
|
232
|
+
* updates are immutable, so a value that kept its identity is no change,
|
|
233
|
+
* and one rebuilt with equal content (mutation code commits whole values)
|
|
234
|
+
* is none either. Values that merge are reported as 'nested', to be
|
|
235
|
+
* compared key by key; they may still be equal. With `arrays`, arrays
|
|
236
|
+
* merge too: elements at the indices both hold are compared, and the
|
|
237
|
+
* elements one has beyond the other's length are added or deleted.
|
|
238
|
+
*/
|
|
239
|
+
export function keyChanges(
|
|
240
|
+
before: Record<string, unknown>,
|
|
241
|
+
after: Record<string, unknown>,
|
|
242
|
+
arrays: boolean,
|
|
243
|
+
): KeyChange[] {
|
|
244
|
+
const changes: KeyChange[] = [];
|
|
245
|
+
const compare = (key: string, b: unknown, a: unknown): void => {
|
|
246
|
+
if (Object.is(b, a)) return;
|
|
247
|
+
if (mergesWith(b, a, arrays)) {
|
|
248
|
+
changes.push({
|
|
249
|
+
key,
|
|
250
|
+
kind: 'nested',
|
|
251
|
+
before: b,
|
|
252
|
+
after: a as Record<string, unknown>,
|
|
253
|
+
});
|
|
254
|
+
} else if (!deepEqual(b, a)) {
|
|
255
|
+
changes.push({ key, kind: 'changed', after: a });
|
|
256
|
+
}
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
if (Array.isArray(after)) {
|
|
260
|
+
const b = before as unknown as unknown[];
|
|
261
|
+
const a = after as unknown[];
|
|
262
|
+
for (let i = 0; i < Math.min(b.length, a.length); i++) {
|
|
263
|
+
compare(String(i), b[i], a[i]);
|
|
264
|
+
}
|
|
265
|
+
for (let i = b.length; i < a.length; i++) {
|
|
266
|
+
changes.push({ key: String(i), kind: 'added', after: a[i] });
|
|
267
|
+
}
|
|
268
|
+
for (let i = a.length; i < b.length; i++) {
|
|
269
|
+
changes.push({ key: String(i), kind: 'deleted' });
|
|
270
|
+
}
|
|
271
|
+
return changes;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
for (const key of Object.keys(after)) {
|
|
275
|
+
if (hasOwn(before, key)) compare(key, before[key], after[key]);
|
|
276
|
+
else changes.push({ key, kind: 'added', after: after[key] });
|
|
277
|
+
}
|
|
278
|
+
for (const key of Object.keys(before)) {
|
|
279
|
+
if (!hasOwn(after, key)) changes.push({ key, kind: 'deleted' });
|
|
280
|
+
}
|
|
281
|
+
return changes;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/** A write to one property path of an object, or its deletion. */
|
|
285
|
+
export type PathChange =
|
|
286
|
+
| { path: string[]; deleted: false; value: unknown }
|
|
287
|
+
| { path: string[]; deleted: true };
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* The property paths where `after` differs from `before`, two objects of
|
|
291
|
+
* one class: objects that merge (see isMergeable) are compared property by
|
|
292
|
+
* property, any other value as a whole.
|
|
293
|
+
*/
|
|
294
|
+
export function diffPaths(
|
|
295
|
+
before: Record<string, unknown>,
|
|
296
|
+
after: Record<string, unknown>,
|
|
297
|
+
): PathChange[] {
|
|
298
|
+
const changes: PathChange[] = [];
|
|
299
|
+
const ancestors = new Set<object>();
|
|
300
|
+
(function walk(
|
|
301
|
+
b: Record<string, unknown>,
|
|
302
|
+
a: Record<string, unknown>,
|
|
303
|
+
path: string[],
|
|
304
|
+
): void {
|
|
305
|
+
// Stop at cycles; shared (non-cyclic) references are visited per path.
|
|
306
|
+
if (ancestors.has(a)) return;
|
|
307
|
+
ancestors.add(a);
|
|
308
|
+
for (const change of keyChanges(b, a, false)) {
|
|
309
|
+
const at = [...path, change.key];
|
|
310
|
+
if (change.kind === 'nested') {
|
|
311
|
+
walk(change.before, change.after, at);
|
|
312
|
+
} else {
|
|
313
|
+
changes.push(
|
|
314
|
+
change.kind === 'deleted'
|
|
315
|
+
? { path: at, deleted: true }
|
|
316
|
+
: { path: at, deleted: false, value: change.after },
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
ancestors.delete(a);
|
|
321
|
+
})(before, after, []);
|
|
322
|
+
return changes;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/** Whether `target` already holds what `change` would write. */
|
|
326
|
+
export function isApplied(
|
|
327
|
+
target: Record<string, unknown>,
|
|
328
|
+
change: PathChange,
|
|
329
|
+
): boolean {
|
|
330
|
+
const parent = getByPath(target, change.path.slice(0, -1));
|
|
331
|
+
if (parent === null || typeof parent !== 'object') return change.deleted;
|
|
332
|
+
const key = change.path[change.path.length - 1]!;
|
|
333
|
+
if (!hasOwn(parent, key)) return change.deleted;
|
|
334
|
+
return (
|
|
335
|
+
!change.deleted &&
|
|
336
|
+
deepEqual((parent as Record<string, unknown>)[key], change.value)
|
|
337
|
+
);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
export function applyChange(
|
|
341
|
+
target: Record<string, unknown>,
|
|
342
|
+
change: PathChange,
|
|
343
|
+
): void {
|
|
344
|
+
if (change.deleted) deleteByPath(target, change.path);
|
|
345
|
+
else setByPath(target, change.path, change.value);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Write how `after` differs from `before` (see keyChanges; arrays merge by
|
|
350
|
+
* index) into `target`, another version of the same object or array.
|
|
351
|
+
* Values that changed are written whole, as deep copies; for values that
|
|
352
|
+
* merge, `mergeNested` may merge them into `target` instead, returning
|
|
353
|
+
* whether it did. Into an array, elements removed from the end are removed
|
|
354
|
+
* at the same indices and elements added are appended, and changes at
|
|
355
|
+
* indices `target` lacks are dropped: where `target` was resized, its
|
|
356
|
+
* indices do not line up with `before`'s.
|
|
357
|
+
*/
|
|
358
|
+
export function mergeKeys(
|
|
359
|
+
target: Record<string, unknown>,
|
|
360
|
+
before: Record<string, unknown>,
|
|
361
|
+
after: Record<string, unknown>,
|
|
362
|
+
mergeNested?: (
|
|
363
|
+
key: string,
|
|
364
|
+
before: Record<string, unknown>,
|
|
365
|
+
after: Record<string, unknown>,
|
|
366
|
+
) => boolean,
|
|
367
|
+
): void {
|
|
368
|
+
const list = Array.isArray(target) ? (target as unknown[]) : undefined;
|
|
369
|
+
for (const change of keyChanges(before, after, true)) {
|
|
370
|
+
const { key } = change;
|
|
371
|
+
if (change.kind === 'deleted') {
|
|
372
|
+
if (list) list.length = Math.min(list.length, Number(key));
|
|
373
|
+
else delete target[key];
|
|
374
|
+
} else if (list && change.kind === 'added') {
|
|
375
|
+
list.push(deepClone(change.after));
|
|
376
|
+
} else if (list && Number(key) >= list.length) {
|
|
377
|
+
continue;
|
|
378
|
+
} else if (
|
|
379
|
+
change.kind !== 'nested' ||
|
|
380
|
+
!(
|
|
381
|
+
mergeNested?.(key, change.before, change.after) ||
|
|
382
|
+
deepEqual(change.before, change.after)
|
|
383
|
+
)
|
|
384
|
+
) {
|
|
385
|
+
target[key] = deepClone(change.after);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
}
|
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
|
}
|