@rohal12/spindle 0.51.3 → 0.52.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 (66) hide show
  1. package/dist/pkg/format.js +1 -1
  2. package/dist/pkg/headless.js +4833 -1603
  3. package/dist/pkg/macro-registry.json +7 -7
  4. package/dist/pkg/story-variables.js +1658 -189
  5. package/package.json +5 -2
  6. package/src/automation/runner.ts +2 -1
  7. package/src/class-registry.ts +277 -90
  8. package/src/components/Passage.tsx +2 -2
  9. package/src/components/PassageDialog.tsx +2 -5
  10. package/src/components/StoryInterface.tsx +2 -4
  11. package/src/components/macros/Button.tsx +9 -32
  12. package/src/components/macros/Checkbox.tsx +10 -4
  13. package/src/components/macros/Computed.tsx +19 -13
  14. package/src/components/macros/Dialog.tsx +4 -1
  15. package/src/components/macros/For.tsx +33 -4
  16. package/src/components/macros/If.tsx +8 -0
  17. package/src/components/macros/Include.tsx +15 -24
  18. package/src/components/macros/MacroError.tsx +2 -1
  19. package/src/components/macros/MacroLink.tsx +14 -46
  20. package/src/components/macros/Meter.tsx +26 -40
  21. package/src/components/macros/Nobr.tsx +1 -0
  22. package/src/components/macros/PassageDisplay.tsx +3 -0
  23. package/src/components/macros/Print.tsx +4 -0
  24. package/src/components/macros/Radiobutton.tsx +27 -2
  25. package/src/components/macros/SaveManager.tsx +39 -14
  26. package/src/components/macros/Span.tsx +1 -0
  27. package/src/components/macros/StoryTitle.tsx +1 -0
  28. package/src/components/macros/Switch.tsx +13 -0
  29. package/src/components/macros/Unset.tsx +30 -10
  30. package/src/components/macros/VarDisplay.tsx +21 -4
  31. package/src/components/macros/Watch.tsx +88 -40
  32. package/src/components/macros/Widget.tsx +20 -1
  33. package/src/components/macros/WidgetInvocation.tsx +20 -158
  34. package/src/components/macros/arg-utils.ts +226 -0
  35. package/src/components/macros/detached-body.tsx +68 -0
  36. package/src/components/macros/option-utils.ts +10 -5
  37. package/src/define-macro.ts +44 -38
  38. package/src/execute-mutation.ts +499 -28
  39. package/src/expression.ts +88 -272
  40. package/src/hooks/use-action.ts +18 -3
  41. package/src/hooks/use-interpolate.ts +36 -5
  42. package/src/index.tsx +10 -1
  43. package/src/interpolation.ts +394 -96
  44. package/src/js-lexer.ts +1460 -0
  45. package/src/markup/code-attributes.ts +64 -0
  46. package/src/markup/markdown.ts +188 -9
  47. package/src/markup/render.tsx +552 -113
  48. package/src/markup/tokenizer.ts +601 -119
  49. package/src/prng.ts +8 -8
  50. package/src/registry.ts +35 -0
  51. package/src/saves/save-manager.ts +368 -153
  52. package/src/saves/storage.ts +24 -7
  53. package/src/saves/types.ts +20 -6
  54. package/src/store.ts +549 -137
  55. package/src/story-api.ts +46 -81
  56. package/src/story-init.ts +1 -1
  57. package/src/story-variables.ts +98 -102
  58. package/src/triggers.ts +6 -2
  59. package/src/utils/error-message.ts +12 -0
  60. package/src/utils/live-locals.ts +10 -3
  61. package/src/utils/namespace.ts +71 -0
  62. package/src/utils/object-path.ts +194 -0
  63. package/src/utils/stable-key.ts +82 -0
  64. package/src/widgets/widget-registry.ts +9 -0
  65. package/types/index.d.ts +43 -7
  66. package/types/tooling.d.ts +1 -0
@@ -15,13 +15,17 @@ import {
15
15
  LocalsUpdateContext,
16
16
  LocalsValuesContext,
17
17
  NobrContext,
18
+ InlineContext,
19
+ RawTextContext,
18
20
  renderNodes as _renderNodes,
19
21
  renderInlineNodes,
20
22
  } from './markup/render';
21
23
  import type { ASTNode } from './markup/ast';
22
- import { executeMutation } from './execute-mutation';
24
+ import { executeMutation, readState } from './execute-mutation';
23
25
  import { evaluate } from './expression';
24
26
  import { useStoryStore } from './store';
27
+ import { getByPath, setByPath } from './utils/object-path';
28
+ import { RESERVED_NAME } from './utils/namespace';
25
29
  import { useAction } from './hooks/use-action';
26
30
  import type { UseActionOptions } from './hooks/use-action';
27
31
  import { collectText } from './utils/extract-text';
@@ -29,12 +33,15 @@ import { currentSourceLocation } from './utils/source-location';
29
33
  import { parseVarArgs, extractOptions } from './components/macros/option-utils';
30
34
  import {
31
35
  registerMacro,
36
+ registerMacroText,
32
37
  registerSubMacro,
33
38
  registerMacroMetadata,
34
39
  } from './registry';
35
- import type { MacroProps, ParameterDef } from './registry';
40
+ import type { MacroProps, MacroTextContext, ParameterDef } from './registry';
36
41
  import { registerBlockMacro } from './markup/ast';
37
42
 
43
+ export type { MacroTextContext };
44
+
38
45
  export function macroClass(type: string, className?: string): string {
39
46
  const base = `macro-${type}`;
40
47
  return className ? `${base} ${className}` : base;
@@ -89,6 +96,12 @@ export interface MacroDefinition {
89
96
  description?: string;
90
97
  parameters?: ParameterDef[];
91
98
  render: (props: MacroProps, ctx: MacroContext) => ComponentChildren;
99
+ /**
100
+ * The macro's text form, used where markup becomes a string: HTML
101
+ * attribute values, image alt text and link titles, macro labels. Without
102
+ * one, the macro can't be used there and is reported as an error.
103
+ */
104
+ text?: (props: MacroProps, ctx: MacroTextContext) => string;
92
105
  }
93
106
 
94
107
  const sharedHooks = {
@@ -101,33 +114,6 @@ const sharedHooks = {
101
114
  useContext,
102
115
  };
103
116
 
104
- /** Traverse a dot-path on an object, returning the nested value. */
105
- function getByPath(obj: Record<string, unknown>, segments: string[]): unknown {
106
- let current: unknown = obj;
107
- for (const seg of segments) {
108
- if (current == null || typeof current !== 'object') return undefined;
109
- current = (current as Record<string, unknown>)[seg];
110
- }
111
- return current;
112
- }
113
-
114
- /** Set a nested value by dot-path segments on an (Immer draft) object. */
115
- function setByPath(
116
- obj: Record<string, unknown>,
117
- segments: string[],
118
- value: unknown,
119
- ): void {
120
- let target: Record<string, unknown> = obj;
121
- for (let i = 0; i < segments.length - 1; i++) {
122
- const seg = segments[i]!;
123
- if (target[seg] == null || typeof target[seg] !== 'object') {
124
- target[seg] = {};
125
- }
126
- target = target[seg] as Record<string, unknown>;
127
- }
128
- target[segments[segments.length - 1]!] = value;
129
- }
130
-
131
117
  export function defineMacro(
132
118
  config: MacroDefinition,
133
119
  source: 'builtin' | 'user' = 'builtin',
@@ -146,11 +132,25 @@ export function defineMacro(
146
132
  // Always-on: cssClass + mutation
147
133
  const { update, getValues } = useContext(LocalsUpdateContext);
148
134
  const nobr = useContext(NobrContext);
135
+ const inline = useContext(InlineContext);
136
+ const raw = useContext(RawTextContext);
149
137
  const localsValues = useContext(LocalsValuesContext);
150
138
  const renderNodes = (
151
139
  nodes: ASTNode[],
152
- options?: { nobr?: boolean; locals?: Record<string, unknown> },
153
- ) => _renderNodes(nodes, { nobr, locals: localsValues, ...options });
140
+ options?: {
141
+ nobr?: boolean;
142
+ locals?: Record<string, unknown>;
143
+ inline?: boolean;
144
+ raw?: boolean;
145
+ },
146
+ ) =>
147
+ _renderNodes(nodes, {
148
+ nobr,
149
+ inline,
150
+ raw,
151
+ locals: localsValues,
152
+ ...options,
153
+ });
154
154
  const ctx: MacroContext = {
155
155
  collectText,
156
156
  sourceLocation: currentSourceLocation,
@@ -196,17 +196,22 @@ export function defineMacro(
196
196
 
197
197
  const varExpr = firstToken.replace(/["']/g, '').replace(/^\$/, '');
198
198
  const segments = varExpr.split('.');
199
+ if (segments.includes(RESERVED_NAME)) {
200
+ return h(
201
+ 'span',
202
+ { class: 'error' },
203
+ `{${config.name}}: "$${varExpr}" cannot be bound: ${RESERVED_NAME} is reserved`,
204
+ );
205
+ }
199
206
  ctx.varName = varExpr;
200
207
  ctx.value = useStoryStore((s) => getByPath(s.variables, segments));
201
- ctx.getValue = () =>
202
- getByPath(useStoryStore.getState().variables, segments);
208
+ // In program order, also when mutation code performs the input
209
+ ctx.getValue = () => getByPath(readState().variables, segments);
203
210
  ctx.setValue = (value: unknown) => {
204
211
  useStoryStore.setState((state) => {
205
- setByPath(
206
- state.variables as Record<string, unknown>,
207
- segments,
208
- value,
209
- );
212
+ setByPath(state.variables, segments, value, {
213
+ createMissing: true,
214
+ });
210
215
  });
211
216
  };
212
217
  }
@@ -215,6 +220,7 @@ export function defineMacro(
215
220
  }
216
221
 
217
222
  registerMacro(config.name, Wrapper);
223
+ registerMacroText(config.name, config.text);
218
224
 
219
225
  // Store metadata for tooling API
220
226
  const isBlock =