@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.
Files changed (50) hide show
  1. package/README.md +5 -1
  2. package/dist/pkg/format.js +1 -1
  3. package/dist/pkg/headless.js +6484 -2558
  4. package/dist/pkg/story-variables.js +4009 -940
  5. package/dist/pkg/tooling.js +13 -1
  6. package/package.json +6 -1
  7. package/src/class-registry.ts +252 -250
  8. package/src/components/App.tsx +2 -0
  9. package/src/components/Passage.tsx +3 -3
  10. package/src/components/PassageDialog.tsx +2 -4
  11. package/src/components/RuntimeErrors.tsx +46 -0
  12. package/src/components/StoryInterface.tsx +2 -4
  13. package/src/components/macros/Computed.tsx +5 -2
  14. package/src/components/macros/Do.tsx +4 -2
  15. package/src/components/macros/Goto.tsx +13 -3
  16. package/src/components/macros/Include.tsx +2 -3
  17. package/src/components/macros/MacroError.tsx +14 -7
  18. package/src/components/macros/SaveManager.tsx +2 -2
  19. package/src/components/macros/VarDisplay.tsx +1 -1
  20. package/src/components/macros/Widget.tsx +5 -33
  21. package/src/index.tsx +57 -37
  22. package/src/interpolation.ts +4 -5
  23. package/src/markup/ast.ts +11 -226
  24. package/src/markup/code-attributes.ts +3 -3
  25. package/src/markup/code-end.ts +121 -0
  26. package/src/markup/parse.ts +118 -0
  27. package/src/markup/render.tsx +1 -1
  28. package/src/markup/spindle.d.peggy.ts +24 -0
  29. package/src/markup/spindle.peggy +463 -0
  30. package/src/markup/tokens.ts +95 -0
  31. package/src/markup/validate.ts +219 -0
  32. package/src/parser.ts +19 -4
  33. package/src/runtime-errors.ts +65 -0
  34. package/src/saves/format.ts +199 -0
  35. package/src/saves/save-manager.ts +45 -63
  36. package/src/saves/types.ts +71 -88
  37. package/src/store.ts +167 -87
  38. package/src/story-init.ts +3 -5
  39. package/src/story-variables.ts +18 -4
  40. package/src/structural.ts +266 -57
  41. package/src/styles.css +45 -0
  42. package/src/tooling.ts +50 -0
  43. package/src/types-drift-check.ts +28 -1
  44. package/src/utils/object-path.ts +13 -12
  45. package/src/utils/stable-key.ts +2 -1
  46. package/src/utils/value-kinds.ts +45 -0
  47. package/src/widgets/widget-def.ts +60 -0
  48. package/types/index.d.ts +32 -8
  49. package/types/tooling.d.ts +42 -0
  50. 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: SavePayload;
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: 1;
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
- /** Navigate to a passage by name. */
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
- /** Register a class constructor for use in story expressions. */
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. */
@@ -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;