@rohal12/spindle 0.52.9 → 0.54.0
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 +5 -1
- package/dist/pkg/format.js +1 -1
- package/dist/pkg/headless.js +6484 -2558
- package/dist/pkg/story-variables.js +4009 -940
- package/dist/pkg/tooling.js +13 -1
- package/package.json +6 -1
- package/src/class-registry.ts +252 -250
- package/src/components/App.tsx +2 -0
- package/src/components/Passage.tsx +3 -3
- package/src/components/PassageDialog.tsx +2 -4
- package/src/components/RuntimeErrors.tsx +46 -0
- package/src/components/StoryInterface.tsx +2 -4
- package/src/components/macros/Computed.tsx +5 -2
- package/src/components/macros/Do.tsx +4 -2
- package/src/components/macros/Goto.tsx +13 -3
- package/src/components/macros/Include.tsx +2 -3
- package/src/components/macros/MacroError.tsx +14 -7
- package/src/components/macros/SaveManager.tsx +2 -2
- package/src/components/macros/VarDisplay.tsx +1 -1
- package/src/components/macros/Widget.tsx +5 -33
- package/src/index.tsx +57 -37
- package/src/interpolation.ts +4 -5
- package/src/markup/ast.ts +11 -226
- package/src/markup/code-attributes.ts +3 -3
- package/src/markup/code-end.ts +121 -0
- package/src/markup/parse.ts +118 -0
- package/src/markup/render.tsx +1 -1
- package/src/markup/spindle.d.peggy.ts +24 -0
- package/src/markup/spindle.peggy +463 -0
- package/src/markup/tokens.ts +95 -0
- package/src/markup/validate.ts +219 -0
- package/src/parser.ts +19 -4
- package/src/runtime-errors.ts +65 -0
- package/src/saves/format.ts +199 -0
- package/src/saves/save-manager.ts +45 -63
- package/src/saves/types.ts +71 -88
- package/src/store.ts +167 -87
- package/src/story-init.ts +3 -5
- package/src/story-variables.ts +18 -4
- package/src/structural.ts +266 -57
- package/src/styles.css +45 -0
- package/src/tooling.ts +50 -0
- package/src/types-drift-check.ts +28 -1
- package/src/utils/object-path.ts +13 -12
- package/src/utils/stable-key.ts +2 -1
- package/src/utils/value-kinds.ts +45 -0
- package/src/widgets/widget-def.ts +60 -0
- package/types/index.d.ts +32 -8
- package/types/tooling.d.ts +42 -0
- package/src/markup/tokenizer.ts +0 -1112
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// Kinds of story values that structural operations (clone, equality, merge)
|
|
2
|
+
// and property paths treat as a whole, by their content.
|
|
3
|
+
|
|
4
|
+
export const toStringTag = (value: object): string =>
|
|
5
|
+
Object.prototype.toString.call(value).slice(8, -1);
|
|
6
|
+
|
|
7
|
+
/** Temporal values are immutable: copies may share them. */
|
|
8
|
+
export const isTemporal = (value: object): boolean =>
|
|
9
|
+
toStringTag(value).startsWith('Temporal.');
|
|
10
|
+
|
|
11
|
+
/** new Number(), new String(), new Boolean(), Object(1n). */
|
|
12
|
+
export const isBoxed = (value: object): boolean =>
|
|
13
|
+
value instanceof Number ||
|
|
14
|
+
value instanceof String ||
|
|
15
|
+
value instanceof Boolean ||
|
|
16
|
+
toStringTag(value) === 'BigInt';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Values compared and copied as a whole, by their content: they have no
|
|
20
|
+
* own keys to merge by, or their keys are not the data (a property written
|
|
21
|
+
* into one would be dropped by clones and saves, and by Immer for Map and
|
|
22
|
+
* Set).
|
|
23
|
+
*/
|
|
24
|
+
export function isAtomic(value: object): boolean {
|
|
25
|
+
return (
|
|
26
|
+
value instanceof Date ||
|
|
27
|
+
value instanceof RegExp ||
|
|
28
|
+
value instanceof Map ||
|
|
29
|
+
value instanceof Set ||
|
|
30
|
+
ArrayBuffer.isView(value) ||
|
|
31
|
+
value instanceof ArrayBuffer ||
|
|
32
|
+
value instanceof URL ||
|
|
33
|
+
value instanceof URLSearchParams ||
|
|
34
|
+
value instanceof Error ||
|
|
35
|
+
isBoxed(value) ||
|
|
36
|
+
isTemporal(value)
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** "a Map", "an Error", ... for an atomic value (see isAtomic), else undefined. */
|
|
41
|
+
export function atomicName(value: object): string | undefined {
|
|
42
|
+
if (!isAtomic(value)) return undefined;
|
|
43
|
+
const tag = value instanceof Error ? 'Error' : toStringTag(value);
|
|
44
|
+
return `${/^[AEIOU]/.test(tag) ? 'an' : 'a'} ${tag}`;
|
|
45
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reading widget definitions without rendering anything: the `{widget}`
|
|
3
|
+
* macro, startup and markup validation (also in tooling) share these.
|
|
4
|
+
*/
|
|
5
|
+
import { parseMacroArgs } from '../components/macros/macro-args';
|
|
6
|
+
import type { MacroArgs } from '../registry';
|
|
7
|
+
|
|
8
|
+
/** A {widget} definition's name, then its `@` parameters. */
|
|
9
|
+
export const WIDGET_PARAMETERS = [
|
|
10
|
+
{ name: 'name', type: 'text', required: true },
|
|
11
|
+
{ name: 'parameters', type: 'expression' },
|
|
12
|
+
] as const;
|
|
13
|
+
|
|
14
|
+
export interface WidgetDef {
|
|
15
|
+
name: string;
|
|
16
|
+
params: string[];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* The widget a {widget} definition's arguments declare: its name and its
|
|
21
|
+
* parameters, the words after it that start with `@` (docs/widgets.md).
|
|
22
|
+
* Other words, such as `$name`, are not parameters.
|
|
23
|
+
*/
|
|
24
|
+
export function widgetDef({
|
|
25
|
+
name = '',
|
|
26
|
+
parameters = '',
|
|
27
|
+
}: MacroArgs<typeof WIDGET_PARAMETERS>): WidgetDef {
|
|
28
|
+
return {
|
|
29
|
+
name,
|
|
30
|
+
params: parameters.split(/\s+/).filter((word) => word.startsWith('@')),
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Read the arguments of a {widget} definition (see widgetDef). */
|
|
35
|
+
export function parseWidgetDef(rawArgs: string): WidgetDef {
|
|
36
|
+
return widgetDef(parseMacroArgs(rawArgs, WIDGET_PARAMETERS));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const BLOCK_WIDGET = /\{widget\s+["']?(\w+)["']?[^}]*\}([\s\S]*?)\{\/widget\}/g;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* The names of the block widgets (those whose body renders `{@children}`)
|
|
43
|
+
* that StoryInit and the passages tagged `widget` define. They must be
|
|
44
|
+
* known as block macros before any passage is parsed, so that passages
|
|
45
|
+
* invoking them nest their content whatever the passage order.
|
|
46
|
+
*/
|
|
47
|
+
export function blockWidgetNames(
|
|
48
|
+
passages: Iterable<{ name: string; tags?: string[]; content: string }>,
|
|
49
|
+
): string[] {
|
|
50
|
+
const names: string[] = [];
|
|
51
|
+
for (const passage of passages) {
|
|
52
|
+
if (passage.name !== 'StoryInit' && !passage.tags?.includes('widget')) {
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
for (const match of passage.content.matchAll(BLOCK_WIDGET)) {
|
|
56
|
+
if (/\{@children\}/.test(match[2]!)) names.push(match[1]!);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return names;
|
|
60
|
+
}
|
package/types/index.d.ts
CHANGED
|
@@ -575,7 +575,18 @@ export interface SaveMeta {
|
|
|
575
575
|
*/
|
|
576
576
|
export interface SaveRecord {
|
|
577
577
|
meta: SaveMeta;
|
|
578
|
-
payload:
|
|
578
|
+
/** The payload as stored: serialized in one piece, with its format version. */
|
|
579
|
+
payload: EncodedPayload;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
/**
|
|
583
|
+
* A save payload as stored. `data` is opaque serialized text; read it with
|
|
584
|
+
* the Story API (loading a save), not directly.
|
|
585
|
+
*/
|
|
586
|
+
export interface EncodedPayload {
|
|
587
|
+
/** The save format version `data` was written in. */
|
|
588
|
+
formatVersion: number;
|
|
589
|
+
data: string;
|
|
579
590
|
}
|
|
580
591
|
|
|
581
592
|
/**
|
|
@@ -584,7 +595,8 @@ export interface SaveRecord {
|
|
|
584
595
|
* @see {@link ../../src/saves/types.ts} for the implementation.
|
|
585
596
|
*/
|
|
586
597
|
export interface SaveExport {
|
|
587
|
-
version
|
|
598
|
+
/** The save format version of the export. */
|
|
599
|
+
formatVersion: number;
|
|
588
600
|
/** IFID of the story the save belongs to. Imports into other stories are rejected. */
|
|
589
601
|
ifid: string;
|
|
590
602
|
/** ISO 8601 timestamp of the export. */
|
|
@@ -614,13 +626,18 @@ export interface StoryAPI {
|
|
|
614
626
|
set(name: string, value: unknown): void;
|
|
615
627
|
set(vars: Record<string, unknown>): void;
|
|
616
628
|
|
|
617
|
-
/**
|
|
629
|
+
/**
|
|
630
|
+
* Navigate to a passage by name. Writes the session: if the variables hold
|
|
631
|
+
* a value a save cannot hold (a function, an instance of an unregistered
|
|
632
|
+
* class, a unique symbol), the navigation completes and then throws an
|
|
633
|
+
* error naming the variable.
|
|
634
|
+
*/
|
|
618
635
|
goto(passageName: string): void;
|
|
619
636
|
|
|
620
|
-
/** Go back one step in history. */
|
|
637
|
+
/** Go back one step in history. Throws like `goto()`. */
|
|
621
638
|
back(): void;
|
|
622
639
|
|
|
623
|
-
/** Go forward one step in history. */
|
|
640
|
+
/** Go forward one step in history. Throws like `goto()`. */
|
|
624
641
|
forward(): void;
|
|
625
642
|
|
|
626
643
|
/** Restart the story from the beginning. */
|
|
@@ -629,7 +646,9 @@ export interface StoryAPI {
|
|
|
629
646
|
/**
|
|
630
647
|
* Save the current state. Pass `slot` for a named save, `custom` for metadata.
|
|
631
648
|
* Resolves once the save is persisted (after `aftersave` handlers ran and
|
|
632
|
-
* `hasSave(slot)` is true); rejects if persisting fails
|
|
649
|
+
* `hasSave(slot)` is true); rejects if persisting fails, or if the state
|
|
650
|
+
* holds a value a save cannot hold (a function, an instance of an
|
|
651
|
+
* unregistered class, a unique symbol, a symbol key), naming it.
|
|
633
652
|
*/
|
|
634
653
|
save(slot?: string, custom?: Record<string, unknown>): Promise<void>;
|
|
635
654
|
|
|
@@ -638,7 +657,8 @@ export interface StoryAPI {
|
|
|
638
657
|
* playthrough, in call order: a save issued after the load belongs to it.
|
|
639
658
|
* Resolves once the loaded state is applied (immediately if the slot is
|
|
640
659
|
* empty, without loading if a restart was issued after the load); rejects
|
|
641
|
-
* if loading fails.
|
|
660
|
+
* if loading fails, e.g. for a save holding an instance of a class that is
|
|
661
|
+
* not registered, or one from an incompatible save format version.
|
|
642
662
|
*/
|
|
643
663
|
load(slot?: string): Promise<void>;
|
|
644
664
|
|
|
@@ -744,7 +764,11 @@ export interface StoryAPI {
|
|
|
744
764
|
/** Check whether any dialog is currently open. */
|
|
745
765
|
isDialogOpen(): boolean;
|
|
746
766
|
|
|
747
|
-
/**
|
|
767
|
+
/**
|
|
768
|
+
* Register a class so its instances keep their class through clones,
|
|
769
|
+
* history, saves and loads. A save refuses instances of classes that are
|
|
770
|
+
* not registered.
|
|
771
|
+
*/
|
|
748
772
|
registerClass(name: string, ctor: new (...args: any[]) => any): void;
|
|
749
773
|
|
|
750
774
|
/** Register a custom macro. */
|
package/types/tooling.d.ts
CHANGED
|
@@ -99,3 +99,45 @@ export declare function parseStoryVariables(
|
|
|
99
99
|
content: string,
|
|
100
100
|
sigil?: '$' | '%',
|
|
101
101
|
): Map<string, VariableSchema>;
|
|
102
|
+
|
|
103
|
+
/** A passage to validate. */
|
|
104
|
+
export interface MarkupPassage {
|
|
105
|
+
name: string;
|
|
106
|
+
content: string;
|
|
107
|
+
tags?: string[];
|
|
108
|
+
/**
|
|
109
|
+
* Passage attributes; `data-source-file` and `data-source-line` (the
|
|
110
|
+
* line of its `::` header) place errors in the source file.
|
|
111
|
+
*/
|
|
112
|
+
metadata?: Record<string, string>;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** An error in a passage's markup. */
|
|
116
|
+
export interface MarkupDiagnostic {
|
|
117
|
+
passage: string;
|
|
118
|
+
/** 1-based line within the passage's content. */
|
|
119
|
+
line: number;
|
|
120
|
+
/** 1-based column (UTF-16 code units). */
|
|
121
|
+
column: number;
|
|
122
|
+
message: string;
|
|
123
|
+
/** The source file and line, when the passage says where it came from. */
|
|
124
|
+
file?: string;
|
|
125
|
+
fileLine?: number;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Validate the markup of a story's passages as Spindle does when the story
|
|
130
|
+
* starts: malformed markup (unclosed or mismatched macros, tags, links,
|
|
131
|
+
* braces and attribute values) and unknown macros, checked against the
|
|
132
|
+
* built-in macros, those registered with `defineMacro` and the widgets the
|
|
133
|
+
* passages define. Spindle refuses to start a story with any of these.
|
|
134
|
+
*/
|
|
135
|
+
export declare function validateMarkup(
|
|
136
|
+
passages: Iterable<MarkupPassage>,
|
|
137
|
+
): MarkupDiagnostic[];
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* A diagnostic as one line of text, as Spindle shows it:
|
|
141
|
+
* `Passage "Start", line 3, column 5 (story.twee:12): Unclosed {if}: …`.
|
|
142
|
+
*/
|
|
143
|
+
export declare function formatDiagnostic(diagnostic: MarkupDiagnostic): string;
|