@rohal12/spindle 0.52.7 → 0.52.9
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 +986 -854
- package/dist/pkg/macro-registry.json +18 -0
- package/package.json +1 -1
- package/src/action-registry.ts +9 -0
- package/src/class-registry.ts +1 -5
- package/src/components/Passage.tsx +35 -39
- package/src/components/PassageDialog.tsx +2 -2
- package/src/components/StoryInterface.tsx +2 -2
- package/src/components/macros/Checkbox.tsx +3 -8
- package/src/components/macros/Computed.tsx +1 -1
- package/src/components/macros/Cycle.tsx +3 -6
- package/src/components/macros/ExprDisplay.tsx +12 -29
- package/src/components/macros/If.tsx +35 -41
- package/src/components/macros/Include.tsx +2 -1
- package/src/components/macros/Listbox.tsx +5 -7
- package/src/components/macros/Print.tsx +3 -5
- package/src/components/macros/Radiobutton.tsx +3 -5
- package/src/components/macros/Repeat.tsx +2 -1
- package/src/components/macros/Switch.tsx +6 -13
- package/src/components/macros/Timed.tsx +14 -18
- package/src/components/macros/Type.tsx +2 -1
- package/src/components/macros/VarDisplay.tsx +12 -32
- package/src/components/macros/Widget.tsx +49 -20
- package/src/components/macros/branches.ts +18 -0
- package/src/components/macros/display.ts +19 -0
- package/src/components/macros/input-macro.ts +33 -11
- package/src/components/macros/macro-args.ts +7 -0
- package/src/components/macros/option-utils.ts +10 -7
- package/src/define-macro.ts +4 -6
- package/src/expression.ts +58 -40
- package/src/hooks/use-action.ts +7 -15
- package/src/index.tsx +5 -15
- package/src/interpolation.ts +34 -41
- package/src/markup/ast.ts +4 -3
- package/src/markup/render.tsx +121 -65
- package/src/parser.ts +119 -13
- 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
package/src/registry.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ComponentType } from 'preact';
|
|
2
2
|
import type { ASTNode, Branch } from './markup/ast';
|
|
3
|
+
import { NameMap, NameSet } from './utils/macro-names';
|
|
3
4
|
|
|
4
5
|
export interface MacroProps {
|
|
5
6
|
rawArgs: string;
|
|
@@ -9,17 +10,17 @@ export interface MacroProps {
|
|
|
9
10
|
branches?: Branch[];
|
|
10
11
|
}
|
|
11
12
|
|
|
12
|
-
const registry = new
|
|
13
|
+
const registry = new NameMap<ComponentType<MacroProps>>();
|
|
13
14
|
|
|
14
15
|
export function registerMacro(
|
|
15
16
|
name: string,
|
|
16
17
|
component: ComponentType<MacroProps>,
|
|
17
18
|
): void {
|
|
18
|
-
registry.set(name
|
|
19
|
+
registry.set(name, component);
|
|
19
20
|
}
|
|
20
21
|
|
|
21
22
|
export function getMacro(name: string): ComponentType<MacroProps> | undefined {
|
|
22
|
-
return registry.get(name
|
|
23
|
+
return registry.get(name);
|
|
23
24
|
}
|
|
24
25
|
|
|
25
26
|
/**
|
|
@@ -42,29 +43,29 @@ export type MacroTextRenderer = (
|
|
|
42
43
|
ctx: MacroTextContext,
|
|
43
44
|
) => string;
|
|
44
45
|
|
|
45
|
-
const textRegistry = new
|
|
46
|
+
const textRegistry = new NameMap<MacroTextRenderer>();
|
|
46
47
|
|
|
47
48
|
/** Register (or, with undefined, remove) the text form of a macro. */
|
|
48
49
|
export function registerMacroText(
|
|
49
50
|
name: string,
|
|
50
51
|
text: MacroTextRenderer | undefined,
|
|
51
52
|
): void {
|
|
52
|
-
if (text) textRegistry.set(name
|
|
53
|
-
else textRegistry.delete(name
|
|
53
|
+
if (text) textRegistry.set(name, text);
|
|
54
|
+
else textRegistry.delete(name);
|
|
54
55
|
}
|
|
55
56
|
|
|
56
57
|
export function getMacroText(name: string): MacroTextRenderer | undefined {
|
|
57
|
-
return textRegistry.get(name
|
|
58
|
+
return textRegistry.get(name);
|
|
58
59
|
}
|
|
59
60
|
|
|
60
|
-
const subMacros = new
|
|
61
|
+
const subMacros = new NameSet();
|
|
61
62
|
|
|
62
63
|
export function registerSubMacro(name: string): void {
|
|
63
|
-
subMacros.add(name
|
|
64
|
+
subMacros.add(name);
|
|
64
65
|
}
|
|
65
66
|
|
|
66
67
|
export function isSubMacro(name: string): boolean {
|
|
67
|
-
return subMacros.has(name
|
|
68
|
+
return subMacros.has(name);
|
|
68
69
|
}
|
|
69
70
|
|
|
70
71
|
/**
|
|
@@ -133,13 +134,13 @@ export interface MacroMetadata {
|
|
|
133
134
|
parameters?: ParameterDef[];
|
|
134
135
|
}
|
|
135
136
|
|
|
136
|
-
const metadataRegistry = new
|
|
137
|
+
const metadataRegistry = new NameMap<MacroMetadata>();
|
|
137
138
|
|
|
138
139
|
export function registerMacroMetadata(
|
|
139
140
|
name: string,
|
|
140
141
|
metadata: MacroMetadata,
|
|
141
142
|
): void {
|
|
142
|
-
metadataRegistry.set(name
|
|
143
|
+
metadataRegistry.set(name, metadata);
|
|
143
144
|
}
|
|
144
145
|
|
|
145
146
|
export function getMacroRegistry(): MacroMetadata[] {
|
|
@@ -9,7 +9,8 @@ import type {
|
|
|
9
9
|
} from './types';
|
|
10
10
|
import { estimatePayloadBytes, isSaveExport, isSavePayload } from './types';
|
|
11
11
|
import { getBackend, resetBackend } from './storage';
|
|
12
|
-
import {
|
|
12
|
+
import { serialize, deserialize } from '../class-registry';
|
|
13
|
+
import { deepClone } from '../structural';
|
|
13
14
|
import { emit } from '../event-emitter';
|
|
14
15
|
import { withoutDraws } from '../prng';
|
|
15
16
|
|
|
@@ -101,6 +102,18 @@ function queued<A extends unknown[], R>(
|
|
|
101
102
|
return (...args) => inOrder(() => op(...args));
|
|
102
103
|
}
|
|
103
104
|
|
|
105
|
+
/** An operation passing the save `saveId`, if it is stored, to `read`. */
|
|
106
|
+
const readingSave = <R>(read: (record: SaveRecord | undefined) => R) =>
|
|
107
|
+
queued(async (saveId: string) =>
|
|
108
|
+
read(await (await getBackend()).getSave(saveId)),
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
/** An operation passing the save a slot holds, if any, to `read`. */
|
|
112
|
+
const readingSlot = <R>(read: (record: SaveRecord | undefined) => R) =>
|
|
113
|
+
queued(async (ifid: string, slot?: string) =>
|
|
114
|
+
read(await slotRecord(ifid, slot)),
|
|
115
|
+
);
|
|
116
|
+
|
|
104
117
|
// --- Playthroughs ---
|
|
105
118
|
|
|
106
119
|
/**
|
|
@@ -371,9 +384,7 @@ export function deserializePayload(payload: SavePayload): SavePayload {
|
|
|
371
384
|
const livePayload = (record: SaveRecord | undefined) =>
|
|
372
385
|
record && deserializePayload(record.payload);
|
|
373
386
|
|
|
374
|
-
export const loadSave =
|
|
375
|
-
livePayload(await (await getBackend()).getSave(saveId)),
|
|
376
|
-
);
|
|
387
|
+
export const loadSave = readingSave(livePayload);
|
|
377
388
|
|
|
378
389
|
/**
|
|
379
390
|
* Delete a save record. If the default slot or a named slot holds it, that
|
|
@@ -623,9 +634,7 @@ async function hasQuickSaveNow(ifid: string, slot?: string): Promise<boolean> {
|
|
|
623
634
|
return (await slotRecord(ifid, slot)) !== undefined;
|
|
624
635
|
}
|
|
625
636
|
|
|
626
|
-
export const loadQuickSave =
|
|
627
|
-
livePayload(await slotRecord(ifid, slot)),
|
|
628
|
-
);
|
|
637
|
+
export const loadQuickSave = readingSlot(livePayload);
|
|
629
638
|
|
|
630
639
|
/**
|
|
631
640
|
* Read the save held in a slot (the default slot when `slot` is omitted) for
|
|
@@ -753,9 +762,7 @@ function toExport(record: SaveRecord | undefined): SaveExport | undefined {
|
|
|
753
762
|
};
|
|
754
763
|
}
|
|
755
764
|
|
|
756
|
-
export const exportSave =
|
|
757
|
-
toExport(await (await getBackend()).getSave(saveId)),
|
|
758
|
-
);
|
|
765
|
+
export const exportSave = readingSave(toExport);
|
|
759
766
|
|
|
760
767
|
/**
|
|
761
768
|
* Validate an export against the running story and build the record to store:
|
|
@@ -812,9 +819,7 @@ export function importSave(
|
|
|
812
819
|
* Export the save held in a slot (default autosave slot when `slot` is omitted).
|
|
813
820
|
* Returns undefined if the slot is empty.
|
|
814
821
|
*/
|
|
815
|
-
export const exportSlotSave =
|
|
816
|
-
toExport(await slotRecord(ifid, slot)),
|
|
817
|
-
);
|
|
822
|
+
export const exportSlotSave = readingSlot(toExport);
|
|
818
823
|
|
|
819
824
|
/**
|
|
820
825
|
* Import an exported save into a slot (default autosave slot when `slot` is
|
package/src/saves/storage.ts
CHANGED
|
@@ -256,6 +256,19 @@ export function createLocalStorageBackend(): StorageBackend {
|
|
|
256
256
|
const IDB_DB_NAME = 'spindle';
|
|
257
257
|
const IDB_DB_VERSION = 1;
|
|
258
258
|
|
|
259
|
+
/** Each object store's key path, and the key path of each of its indexes. */
|
|
260
|
+
const IDB_STORES: Record<
|
|
261
|
+
TableName,
|
|
262
|
+
{ keyPath: string; indexes: Record<string, string> }
|
|
263
|
+
> = {
|
|
264
|
+
saves: {
|
|
265
|
+
keyPath: 'meta.id',
|
|
266
|
+
indexes: { ifid: 'meta.ifid', playthroughId: 'meta.playthroughId' },
|
|
267
|
+
},
|
|
268
|
+
playthroughs: { keyPath: 'id', indexes: { ifid: 'ifid' } },
|
|
269
|
+
meta: { keyPath: 'key', indexes: {} },
|
|
270
|
+
};
|
|
271
|
+
|
|
259
272
|
function createIDBBackend(): StorageBackend {
|
|
260
273
|
let dbPromise: Promise<IDBDatabase> | null = null;
|
|
261
274
|
|
|
@@ -267,26 +280,12 @@ function createIDBBackend(): StorageBackend {
|
|
|
267
280
|
|
|
268
281
|
req.onupgradeneeded = () => {
|
|
269
282
|
const db = req.result;
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
const
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
savesStore.createIndex('playthroughId', 'meta.playthroughId', {
|
|
277
|
-
unique: false,
|
|
278
|
-
});
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
if (!db.objectStoreNames.contains('playthroughs')) {
|
|
282
|
-
const ptStore = db.createObjectStore('playthroughs', {
|
|
283
|
-
keyPath: 'id',
|
|
284
|
-
});
|
|
285
|
-
ptStore.createIndex('ifid', 'ifid', { unique: false });
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
if (!db.objectStoreNames.contains('meta')) {
|
|
289
|
-
db.createObjectStore('meta', { keyPath: 'key' });
|
|
283
|
+
for (const [name, { keyPath, indexes }] of Object.entries(IDB_STORES)) {
|
|
284
|
+
if (db.objectStoreNames.contains(name)) continue;
|
|
285
|
+
const store = db.createObjectStore(name, { keyPath });
|
|
286
|
+
for (const [index, path] of Object.entries(indexes)) {
|
|
287
|
+
store.createIndex(index, path, { unique: false });
|
|
288
|
+
}
|
|
290
289
|
}
|
|
291
290
|
};
|
|
292
291
|
|
package/src/story-api.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useStoryStore, trackRuntimeUnsub } from './store';
|
|
2
|
-
import type { VariableNamespaces } from './store';
|
|
2
|
+
import type { StoryState, VariableNamespaces } from './store';
|
|
3
3
|
import {
|
|
4
4
|
on as emitterOn,
|
|
5
5
|
emit,
|
|
@@ -24,7 +24,7 @@ import { registerClass } from './class-registry';
|
|
|
24
24
|
import { frozenCopy, getActiveMutationScope } from './execute-mutation';
|
|
25
25
|
import { getByPath, setByPath } from './utils/object-path';
|
|
26
26
|
import { changedNames, checkVariableName, ownValue } from './utils/namespace';
|
|
27
|
-
import {
|
|
27
|
+
import { historyQueries } from './expression';
|
|
28
28
|
import { defineMacro } from './define-macro';
|
|
29
29
|
import type { MacroDefinition } from './define-macro';
|
|
30
30
|
import { getMacroRegistry as _getMacroRegistry } from './registry';
|
|
@@ -259,9 +259,15 @@ function setOne(draft: VariableNamespaces, name: string, value: unknown): void {
|
|
|
259
259
|
}
|
|
260
260
|
}
|
|
261
261
|
|
|
262
|
+
/** A method calling the store action `name`, as the store holds it then. */
|
|
263
|
+
function storeAction<K extends keyof StoryState>(name: K): StoryState[K] {
|
|
264
|
+
return ((...args: unknown[]) =>
|
|
265
|
+
(useStoryStore.getState()[name] as (...args: unknown[]) => unknown)(
|
|
266
|
+
...args,
|
|
267
|
+
)) as StoryState[K];
|
|
268
|
+
}
|
|
269
|
+
|
|
262
270
|
function createStoryAPI(): StoryAPI {
|
|
263
|
-
const visits = countQueries(() => useStoryStore.getState().visitCounts);
|
|
264
|
-
const renders = countQueries(() => useStoryStore.getState().renderCounts);
|
|
265
271
|
return {
|
|
266
272
|
get(name: string): unknown {
|
|
267
273
|
const { isTransient, key } = parseName(name);
|
|
@@ -300,64 +306,23 @@ function createStoryAPI(): StoryAPI {
|
|
|
300
306
|
// Called from running mutation code, these store actions commit the
|
|
301
307
|
// code's writes so far before they record, replace or save state, and
|
|
302
308
|
// the code goes on from the state they leave (see storyStateGuard).
|
|
303
|
-
goto
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
return useStoryStore.getState().save(slot, custom);
|
|
321
|
-
},
|
|
322
|
-
|
|
323
|
-
load(slot?: string): Promise<void> {
|
|
324
|
-
return useStoryStore.getState().load(slot);
|
|
325
|
-
},
|
|
326
|
-
|
|
327
|
-
hasSave(slot?: string): boolean {
|
|
328
|
-
return useStoryStore.getState().hasSave(slot);
|
|
329
|
-
},
|
|
330
|
-
|
|
331
|
-
getSaveInfo(slot?: string): Promise<SaveInfo | null> {
|
|
332
|
-
return useStoryStore.getState().getSaveInfo(slot);
|
|
333
|
-
},
|
|
334
|
-
|
|
335
|
-
listSaves(): Promise<SaveInfo[]> {
|
|
336
|
-
return useStoryStore.getState().listSaves();
|
|
337
|
-
},
|
|
338
|
-
|
|
339
|
-
deleteSave(slot?: string): Promise<void> {
|
|
340
|
-
return useStoryStore.getState().deleteSave(slot);
|
|
341
|
-
},
|
|
342
|
-
|
|
343
|
-
exportSave(slot?: string): Promise<SaveExport | null> {
|
|
344
|
-
return useStoryStore.getState().exportSave(slot);
|
|
345
|
-
},
|
|
346
|
-
|
|
347
|
-
importSave(data: unknown, slot?: string): Promise<SaveInfo> {
|
|
348
|
-
return useStoryStore.getState().importSave(data, slot);
|
|
349
|
-
},
|
|
350
|
-
|
|
351
|
-
visited: visits.count,
|
|
352
|
-
hasVisited: visits.has,
|
|
353
|
-
hasVisitedAny: visits.any,
|
|
354
|
-
hasVisitedAll: visits.all,
|
|
355
|
-
rendered: renders.count,
|
|
356
|
-
hasRendered: renders.has,
|
|
357
|
-
hasRenderedAny: renders.any,
|
|
358
|
-
hasRenderedAll: renders.all,
|
|
359
|
-
currentPassage,
|
|
360
|
-
previousPassage,
|
|
309
|
+
goto: storeAction('navigate'),
|
|
310
|
+
back: storeAction('goBack'),
|
|
311
|
+
forward: storeAction('goForward'),
|
|
312
|
+
restart: storeAction('restart'),
|
|
313
|
+
save: storeAction('save'),
|
|
314
|
+
load: storeAction('load'),
|
|
315
|
+
hasSave: storeAction('hasSave'),
|
|
316
|
+
getSaveInfo: storeAction('getSaveInfo'),
|
|
317
|
+
listSaves: storeAction('listSaves'),
|
|
318
|
+
deleteSave: storeAction('deleteSave'),
|
|
319
|
+
exportSave: storeAction('exportSave'),
|
|
320
|
+
importSave: storeAction('importSave'),
|
|
321
|
+
|
|
322
|
+
...historyQueries(
|
|
323
|
+
() => useStoryStore.getState().visitCounts,
|
|
324
|
+
() => useStoryStore.getState().renderCounts,
|
|
325
|
+
),
|
|
361
326
|
|
|
362
327
|
get title(): string {
|
|
363
328
|
return useStoryStore.getState().storyData?.name || '';
|
|
@@ -416,17 +381,9 @@ function createStoryAPI(): StoryAPI {
|
|
|
416
381
|
return { usage: 0, quota: 0, estimateSupported: false };
|
|
417
382
|
},
|
|
418
383
|
|
|
419
|
-
clearGameData()
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
clearAllData(): Promise<void> {
|
|
424
|
-
return useStoryStore.getState().clearAllData();
|
|
425
|
-
},
|
|
426
|
-
|
|
427
|
-
deletePlaythrough(playthroughId: string): Promise<void> {
|
|
428
|
-
return useStoryStore.getState().deletePlaythrough(playthroughId);
|
|
429
|
-
},
|
|
384
|
+
clearGameData: storeAction('clearGameData'),
|
|
385
|
+
clearAllData: storeAction('clearAllData'),
|
|
386
|
+
deletePlaythrough: storeAction('deletePlaythrough'),
|
|
430
387
|
|
|
431
388
|
get backend() {
|
|
432
389
|
return getBackendType();
|
package/src/structural.ts
CHANGED
|
@@ -4,26 +4,9 @@
|
|
|
4
4
|
// what a change is.
|
|
5
5
|
|
|
6
6
|
import { registeredClassName } from './class-registry';
|
|
7
|
-
import { hasOwn } from './utils/namespace';
|
|
7
|
+
import { hasOwn, setOwn } from './utils/namespace';
|
|
8
8
|
import { deleteByPath, getByPath, setByPath } from './utils/object-path';
|
|
9
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
10
|
// --- Deep Clone ---
|
|
28
11
|
|
|
29
12
|
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
package/src/types-drift-check.ts
CHANGED
|
@@ -1,62 +1,61 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Compile-time check: the hand-written types
|
|
3
|
-
* with the source
|
|
4
|
-
* the published types have drifted from the implementation.
|
|
2
|
+
* Compile-time check: the hand-written published types (types/*.d.ts) must
|
|
3
|
+
* stay in sync with the source they describe. If this file fails to
|
|
4
|
+
* compile, the published types have drifted from the implementation.
|
|
5
5
|
*
|
|
6
6
|
* Run: npx tsc --noEmit
|
|
7
7
|
*/
|
|
8
8
|
import type { StoryAPI as SourceAPI } from './story-api';
|
|
9
|
-
import type { StoryAPI as PublishedAPI } from '../types/index';
|
|
10
|
-
|
|
11
|
-
// Both directions — if either fails, the types have drifted.
|
|
12
|
-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
13
|
-
const _sourceToPublished: PublishedAPI = {} as SourceAPI;
|
|
14
|
-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
15
|
-
const _publishedToSource: SourceAPI = {} as PublishedAPI;
|
|
16
|
-
|
|
17
|
-
// Tooling entry point (`@rohal12/spindle/tooling`): types/tooling.d.ts must
|
|
18
|
-
// match the parser that dist/pkg/tooling.js re-exports.
|
|
19
9
|
import type { parseStoryVariables as SourceParse } from './story-variables';
|
|
20
|
-
import type { parseStoryVariables as PublishedParse } from '../types/tooling';
|
|
21
|
-
|
|
22
|
-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
23
|
-
const _parseSourceToPublished: typeof PublishedParse = {} as typeof SourceParse;
|
|
24
|
-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
25
|
-
const _parsePublishedToSource: typeof SourceParse = {} as typeof PublishedParse;
|
|
26
|
-
|
|
27
|
-
// Custom macro API: the published MacroContext/MacroDefinition must match the
|
|
28
|
-
// ones defineMacro() actually passes and accepts.
|
|
29
10
|
import type {
|
|
30
11
|
MacroContext as SourceMacroContext,
|
|
31
12
|
MacroDefinition as SourceMacroDefinition,
|
|
32
13
|
} from './define-macro';
|
|
33
14
|
import type { MacroProps as SourceMacroProps } from './registry';
|
|
34
15
|
import type { ASTNode as SourceASTNode } from './markup/ast';
|
|
16
|
+
import type { bootStory as SourceBootStory } from './headless';
|
|
35
17
|
import type {
|
|
18
|
+
StoryAPI as PublishedAPI,
|
|
36
19
|
MacroContext as PublishedMacroContext,
|
|
37
20
|
MacroDefinition as PublishedMacroDefinition,
|
|
38
21
|
MacroProps as PublishedMacroProps,
|
|
39
22
|
ASTNode as PublishedASTNode,
|
|
40
23
|
} from '../types/index';
|
|
24
|
+
import type { parseStoryVariables as PublishedParse } from '../types/tooling';
|
|
25
|
+
import type { bootStory as PublishedBootStory } from '../types/headless';
|
|
41
26
|
|
|
27
|
+
/** What the source declares, by published name. */
|
|
28
|
+
interface Source {
|
|
29
|
+
StoryAPI: SourceAPI;
|
|
30
|
+
// Tooling entry point (`@rohal12/spindle/tooling`): the parser that
|
|
31
|
+
// dist/pkg/tooling.js re-exports.
|
|
32
|
+
parseStoryVariables: typeof SourceParse;
|
|
33
|
+
// Custom macro API: the MacroContext/MacroDefinition defineMacro()
|
|
34
|
+
// actually passes and accepts.
|
|
35
|
+
MacroContext: SourceMacroContext;
|
|
36
|
+
MacroDefinition: SourceMacroDefinition;
|
|
37
|
+
MacroProps: SourceMacroProps;
|
|
38
|
+
ASTNode: SourceASTNode;
|
|
39
|
+
// Headless entry point (`@rohal12/spindle/headless`).
|
|
40
|
+
bootStory: typeof SourceBootStory;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** What types/index.d.ts, tooling.d.ts and headless.d.ts publish. */
|
|
44
|
+
interface Published {
|
|
45
|
+
StoryAPI: PublishedAPI;
|
|
46
|
+
parseStoryVariables: typeof PublishedParse;
|
|
47
|
+
MacroContext: PublishedMacroContext;
|
|
48
|
+
MacroDefinition: PublishedMacroDefinition;
|
|
49
|
+
MacroProps: PublishedMacroProps;
|
|
50
|
+
ASTNode: PublishedASTNode;
|
|
51
|
+
bootStory: typeof PublishedBootStory;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Both directions — if either fails, the types have drifted.
|
|
42
55
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
43
|
-
const
|
|
44
|
-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
45
|
-
const _ctxPublishedToSource: SourceMacroContext = {} as PublishedMacroContext;
|
|
46
|
-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
47
|
-
const _defSourceToPublished: PublishedMacroDefinition =
|
|
48
|
-
{} as SourceMacroDefinition;
|
|
49
|
-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
50
|
-
const _defPublishedToSource: SourceMacroDefinition =
|
|
51
|
-
{} as PublishedMacroDefinition;
|
|
52
|
-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
53
|
-
const _propsSourceToPublished: PublishedMacroProps = {} as SourceMacroProps;
|
|
54
|
-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
55
|
-
const _propsPublishedToSource: SourceMacroProps = {} as PublishedMacroProps;
|
|
56
|
-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
57
|
-
const _astSourceToPublished: PublishedASTNode = {} as SourceASTNode;
|
|
56
|
+
const _sourceToPublished: Published = {} as Source;
|
|
58
57
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
59
|
-
const
|
|
58
|
+
const _publishedToSource: Source = {} as Published;
|
|
60
59
|
|
|
61
60
|
// A typical custom macro written against the published types must type-check,
|
|
62
61
|
// and misuse of the hooks must not (no `any` leaking through).
|
|
@@ -91,15 +90,3 @@ const _exampleMacro: PublishedMacroDefinition = {
|
|
|
91
90
|
);
|
|
92
91
|
},
|
|
93
92
|
};
|
|
94
|
-
|
|
95
|
-
// Headless entry point (`@rohal12/spindle/headless`): types/headless.d.ts must
|
|
96
|
-
// match src/headless.ts.
|
|
97
|
-
import type { bootStory as SourceBootStory } from './headless';
|
|
98
|
-
import type { bootStory as PublishedBootStory } from '../types/headless';
|
|
99
|
-
|
|
100
|
-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
101
|
-
const _bootSourceToPublished: typeof PublishedBootStory =
|
|
102
|
-
{} as typeof SourceBootStory;
|
|
103
|
-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
104
|
-
const _bootPublishedToSource: typeof SourceBootStory =
|
|
105
|
-
{} as typeof PublishedBootStory;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Macro and widget names are case-insensitive: `{StatusBar}` invokes the
|
|
3
|
+
* widget defined as `statusbar`. A NameMap keys its entries by the lowercased
|
|
4
|
+
* name.
|
|
5
|
+
*/
|
|
6
|
+
export class NameMap<V> extends Map<string, V> {
|
|
7
|
+
override get(name: string): V | undefined {
|
|
8
|
+
return super.get(name.toLowerCase());
|
|
9
|
+
}
|
|
10
|
+
override set(name: string, value: V): this {
|
|
11
|
+
return super.set(name.toLowerCase(), value);
|
|
12
|
+
}
|
|
13
|
+
override has(name: string): boolean {
|
|
14
|
+
return super.has(name.toLowerCase());
|
|
15
|
+
}
|
|
16
|
+
override delete(name: string): boolean {
|
|
17
|
+
return super.delete(name.toLowerCase());
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** A set of macro or widget names (see NameMap). */
|
|
22
|
+
export class NameSet {
|
|
23
|
+
private readonly names = new NameMap<true>();
|
|
24
|
+
|
|
25
|
+
constructor(names: Iterable<string> = []) {
|
|
26
|
+
for (const name of names) this.add(name);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
add(name: string): void {
|
|
30
|
+
this.names.set(name, true);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
has(name: string): boolean {
|
|
34
|
+
return this.names.has(name);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
delete(name: string): void {
|
|
38
|
+
this.names.delete(name);
|
|
39
|
+
}
|
|
40
|
+
}
|
package/src/utils/namespace.ts
CHANGED
|
@@ -48,6 +48,23 @@ export const ownValue = (
|
|
|
48
48
|
key: string,
|
|
49
49
|
): unknown => (ns && hasOwn(ns, key) ? (ns as Namespace)[key] : undefined);
|
|
50
50
|
|
|
51
|
+
/**
|
|
52
|
+
* Set an own enumerable property. A "__proto__" key is defined as an own
|
|
53
|
+
* property (as JSON.parse does) instead of replacing the prototype.
|
|
54
|
+
*/
|
|
55
|
+
export function setOwn(target: object, key: string, value: unknown): void {
|
|
56
|
+
if (key === '__proto__') {
|
|
57
|
+
Object.defineProperty(target, key, {
|
|
58
|
+
value,
|
|
59
|
+
enumerable: true,
|
|
60
|
+
writable: true,
|
|
61
|
+
configurable: true,
|
|
62
|
+
});
|
|
63
|
+
} else {
|
|
64
|
+
(target as Record<string, unknown>)[key] = value;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
51
68
|
/**
|
|
52
69
|
* A record with no prototype holding the own entries of `sources`, or only
|
|
53
70
|
* those whose value passes `keep`.
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ASTNode } from '../markup/ast';
|
|
2
2
|
import { checkVariableName } from '../utils/namespace';
|
|
3
|
+
import { NameMap } from '../utils/macro-names';
|
|
3
4
|
|
|
4
5
|
interface WidgetEntry {
|
|
5
6
|
body: ASTNode[];
|
|
@@ -7,7 +8,7 @@ interface WidgetEntry {
|
|
|
7
8
|
isBlock: boolean;
|
|
8
9
|
}
|
|
9
10
|
|
|
10
|
-
const widgets = new
|
|
11
|
+
const widgets = new NameMap<WidgetEntry>();
|
|
11
12
|
|
|
12
13
|
/**
|
|
13
14
|
* Register a widget. Its `@` parameters become locals of its body, so one
|
|
@@ -24,7 +25,7 @@ export function registerWidget(
|
|
|
24
25
|
for (const param of filteredParams) {
|
|
25
26
|
if (param.startsWith('@')) checkVariableName(param.slice(1), param);
|
|
26
27
|
}
|
|
27
|
-
widgets.set(name
|
|
28
|
+
widgets.set(name, {
|
|
28
29
|
body: bodyAST,
|
|
29
30
|
params: filteredParams,
|
|
30
31
|
isBlock,
|
|
@@ -32,11 +33,11 @@ export function registerWidget(
|
|
|
32
33
|
}
|
|
33
34
|
|
|
34
35
|
export function getWidget(name: string): WidgetEntry | undefined {
|
|
35
|
-
return widgets.get(name
|
|
36
|
+
return widgets.get(name);
|
|
36
37
|
}
|
|
37
38
|
|
|
38
39
|
export function isBlockWidget(name: string): boolean {
|
|
39
|
-
return widgets.get(name
|
|
40
|
+
return widgets.get(name)?.isBlock ?? false;
|
|
40
41
|
}
|
|
41
42
|
|
|
42
43
|
export function clearWidgets(): void {
|