@rohal12/spindle 0.1.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 (65) hide show
  1. package/README.md +66 -0
  2. package/dist/pkg/format.js +1 -0
  3. package/dist/pkg/index.js +12 -0
  4. package/dist/pkg/types/globals.d.ts +18 -0
  5. package/dist/pkg/types/index.d.ts +158 -0
  6. package/package.json +71 -0
  7. package/src/components/App.tsx +53 -0
  8. package/src/components/Passage.tsx +36 -0
  9. package/src/components/PassageLink.tsx +35 -0
  10. package/src/components/SaveLoadDialog.tsx +403 -0
  11. package/src/components/SettingsDialog.tsx +106 -0
  12. package/src/components/StoryInterface.tsx +31 -0
  13. package/src/components/macros/Back.tsx +23 -0
  14. package/src/components/macros/Button.tsx +49 -0
  15. package/src/components/macros/Checkbox.tsx +41 -0
  16. package/src/components/macros/Computed.tsx +100 -0
  17. package/src/components/macros/Cycle.tsx +39 -0
  18. package/src/components/macros/Do.tsx +46 -0
  19. package/src/components/macros/For.tsx +113 -0
  20. package/src/components/macros/Forward.tsx +25 -0
  21. package/src/components/macros/Goto.tsx +23 -0
  22. package/src/components/macros/If.tsx +63 -0
  23. package/src/components/macros/Include.tsx +52 -0
  24. package/src/components/macros/Listbox.tsx +42 -0
  25. package/src/components/macros/MacroLink.tsx +107 -0
  26. package/src/components/macros/Numberbox.tsx +43 -0
  27. package/src/components/macros/Print.tsx +48 -0
  28. package/src/components/macros/QuickLoad.tsx +33 -0
  29. package/src/components/macros/QuickSave.tsx +22 -0
  30. package/src/components/macros/Radiobutton.tsx +59 -0
  31. package/src/components/macros/Repeat.tsx +53 -0
  32. package/src/components/macros/Restart.tsx +27 -0
  33. package/src/components/macros/Saves.tsx +25 -0
  34. package/src/components/macros/Set.tsx +36 -0
  35. package/src/components/macros/SettingsButton.tsx +29 -0
  36. package/src/components/macros/Stop.tsx +12 -0
  37. package/src/components/macros/StoryTitle.tsx +20 -0
  38. package/src/components/macros/Switch.tsx +69 -0
  39. package/src/components/macros/Textarea.tsx +41 -0
  40. package/src/components/macros/Textbox.tsx +40 -0
  41. package/src/components/macros/Timed.tsx +63 -0
  42. package/src/components/macros/Type.tsx +83 -0
  43. package/src/components/macros/Unset.tsx +25 -0
  44. package/src/components/macros/VarDisplay.tsx +44 -0
  45. package/src/components/macros/Widget.tsx +18 -0
  46. package/src/components/macros/option-utils.ts +14 -0
  47. package/src/expression.ts +93 -0
  48. package/src/index.tsx +120 -0
  49. package/src/markup/ast.ts +284 -0
  50. package/src/markup/markdown.ts +21 -0
  51. package/src/markup/render.tsx +537 -0
  52. package/src/markup/tokenizer.ts +581 -0
  53. package/src/parser.ts +72 -0
  54. package/src/registry.ts +21 -0
  55. package/src/saves/idb.ts +165 -0
  56. package/src/saves/save-manager.ts +317 -0
  57. package/src/saves/types.ts +40 -0
  58. package/src/settings.ts +96 -0
  59. package/src/store.ts +317 -0
  60. package/src/story-api.ts +129 -0
  61. package/src/story-init.ts +67 -0
  62. package/src/story-variables.ts +166 -0
  63. package/src/styles.css +780 -0
  64. package/src/utils/parse-delay.ts +14 -0
  65. package/src/widgets/widget-registry.ts +15 -0
@@ -0,0 +1,537 @@
1
+ import { createContext } from 'preact';
2
+ import { PassageLink } from '../components/PassageLink';
3
+ import { VarDisplay } from '../components/macros/VarDisplay';
4
+ import { Set } from '../components/macros/Set';
5
+ import { Print } from '../components/macros/Print';
6
+ import { If } from '../components/macros/If';
7
+ import { For } from '../components/macros/For';
8
+ import { Do } from '../components/macros/Do';
9
+ import { Button } from '../components/macros/Button';
10
+ import { StoryTitle } from '../components/macros/StoryTitle';
11
+ import { Restart } from '../components/macros/Restart';
12
+ import { Back } from '../components/macros/Back';
13
+ import { Forward } from '../components/macros/Forward';
14
+ import { QuickSave } from '../components/macros/QuickSave';
15
+ import { QuickLoad } from '../components/macros/QuickLoad';
16
+ import { SettingsButton } from '../components/macros/SettingsButton';
17
+ import { Saves } from '../components/macros/Saves';
18
+ import { Include } from '../components/macros/Include';
19
+ import { Goto } from '../components/macros/Goto';
20
+ import { Unset } from '../components/macros/Unset';
21
+ import { Textbox } from '../components/macros/Textbox';
22
+ import { Numberbox } from '../components/macros/Numberbox';
23
+ import { Textarea } from '../components/macros/Textarea';
24
+ import { Checkbox } from '../components/macros/Checkbox';
25
+ import { Radiobutton } from '../components/macros/Radiobutton';
26
+ import { Listbox } from '../components/macros/Listbox';
27
+ import { Cycle } from '../components/macros/Cycle';
28
+ import { MacroLink } from '../components/macros/MacroLink';
29
+ import { Switch } from '../components/macros/Switch';
30
+ import { Timed } from '../components/macros/Timed';
31
+ import { Repeat } from '../components/macros/Repeat';
32
+ import { Stop } from '../components/macros/Stop';
33
+ import { Type } from '../components/macros/Type';
34
+ import { Widget } from '../components/macros/Widget';
35
+ import { Computed } from '../components/macros/Computed';
36
+ import { getWidget } from '../widgets/widget-registry';
37
+ import { getMacro } from '../registry';
38
+ import { markdownToHtml } from './markdown';
39
+ import { h } from 'preact';
40
+ import type { ASTNode, MacroNode } from './ast';
41
+
42
+ export const LocalsContext = createContext<Record<string, unknown>>({});
43
+
44
+ /**
45
+ * Convert an HTML string (from micromark) to Preact VNodes,
46
+ * replacing <span data-tw="N"> placeholder elements with pre-rendered components.
47
+ */
48
+ function htmlToPreact(
49
+ html: string,
50
+ components: preact.ComponentChildren[],
51
+ ): preact.ComponentChildren {
52
+ const temp = document.createElement('div');
53
+ temp.innerHTML = html.trim();
54
+ const children = Array.from(temp.childNodes).map((child, i) =>
55
+ convertDomNode(child, i, components),
56
+ );
57
+ return <>{children}</>;
58
+ }
59
+
60
+ function convertDomNode(
61
+ node: Node,
62
+ key: number,
63
+ components: preact.ComponentChildren[],
64
+ ): preact.ComponentChildren {
65
+ if (node.nodeType === Node.TEXT_NODE) {
66
+ return node.textContent;
67
+ }
68
+ if (node.nodeType === Node.ELEMENT_NODE) {
69
+ const el = node as Element;
70
+ const tag = el.tagName.toLowerCase();
71
+
72
+ // Check if it's a placeholder for a Twine component
73
+ const twIdx = el.getAttribute('data-tw');
74
+ if (twIdx != null) {
75
+ return components[parseInt(twIdx, 10)];
76
+ }
77
+
78
+ // Convert attributes
79
+ const props: Record<string, any> = { key };
80
+ for (const attr of Array.from(el.attributes)) {
81
+ props[attr.name] = attr.value;
82
+ }
83
+
84
+ // Convert children recursively
85
+ const children = Array.from(el.childNodes).map((child, i) =>
86
+ convertDomNode(child, i, components),
87
+ );
88
+
89
+ return h(tag, props, ...children);
90
+ }
91
+ return null;
92
+ }
93
+
94
+ function renderMacro(node: MacroNode, key: number) {
95
+ switch (node.name) {
96
+ case 'set':
97
+ return (
98
+ <Set
99
+ key={key}
100
+ rawArgs={node.rawArgs}
101
+ />
102
+ );
103
+
104
+ case 'computed':
105
+ return (
106
+ <Computed
107
+ key={key}
108
+ rawArgs={node.rawArgs}
109
+ />
110
+ );
111
+
112
+ case 'print':
113
+ return (
114
+ <Print
115
+ key={key}
116
+ rawArgs={node.rawArgs}
117
+ className={node.className}
118
+ id={node.id}
119
+ />
120
+ );
121
+
122
+ case 'if':
123
+ return (
124
+ <If
125
+ key={key}
126
+ branches={node.branches!}
127
+ />
128
+ );
129
+
130
+ case 'for':
131
+ return (
132
+ <For
133
+ key={key}
134
+ rawArgs={node.rawArgs}
135
+ children={node.children}
136
+ className={node.className}
137
+ id={node.id}
138
+ />
139
+ );
140
+
141
+ case 'do':
142
+ return (
143
+ <Do
144
+ key={key}
145
+ children={node.children}
146
+ />
147
+ );
148
+
149
+ case 'button':
150
+ return (
151
+ <Button
152
+ key={key}
153
+ rawArgs={node.rawArgs}
154
+ children={node.children}
155
+ className={node.className}
156
+ id={node.id}
157
+ />
158
+ );
159
+
160
+ case 'story-title':
161
+ return (
162
+ <StoryTitle
163
+ key={key}
164
+ className={node.className}
165
+ id={node.id}
166
+ />
167
+ );
168
+
169
+ case 'back':
170
+ return (
171
+ <Back
172
+ key={key}
173
+ className={node.className}
174
+ id={node.id}
175
+ />
176
+ );
177
+
178
+ case 'forward':
179
+ return (
180
+ <Forward
181
+ key={key}
182
+ className={node.className}
183
+ id={node.id}
184
+ />
185
+ );
186
+
187
+ case 'restart':
188
+ return (
189
+ <Restart
190
+ key={key}
191
+ className={node.className}
192
+ id={node.id}
193
+ />
194
+ );
195
+
196
+ case 'quicksave':
197
+ return (
198
+ <QuickSave
199
+ key={key}
200
+ className={node.className}
201
+ id={node.id}
202
+ />
203
+ );
204
+
205
+ case 'quickload':
206
+ return (
207
+ <QuickLoad
208
+ key={key}
209
+ className={node.className}
210
+ id={node.id}
211
+ />
212
+ );
213
+
214
+ case 'settings':
215
+ return (
216
+ <SettingsButton
217
+ key={key}
218
+ className={node.className}
219
+ id={node.id}
220
+ />
221
+ );
222
+
223
+ case 'saves':
224
+ return (
225
+ <Saves
226
+ key={key}
227
+ className={node.className}
228
+ id={node.id}
229
+ />
230
+ );
231
+
232
+ case 'include':
233
+ return (
234
+ <Include
235
+ key={key}
236
+ rawArgs={node.rawArgs}
237
+ className={node.className}
238
+ id={node.id}
239
+ />
240
+ );
241
+
242
+ case 'goto':
243
+ return (
244
+ <Goto
245
+ key={key}
246
+ rawArgs={node.rawArgs}
247
+ />
248
+ );
249
+
250
+ case 'unset':
251
+ return (
252
+ <Unset
253
+ key={key}
254
+ rawArgs={node.rawArgs}
255
+ />
256
+ );
257
+
258
+ case 'textbox':
259
+ return (
260
+ <Textbox
261
+ key={key}
262
+ rawArgs={node.rawArgs}
263
+ className={node.className}
264
+ id={node.id}
265
+ />
266
+ );
267
+
268
+ case 'numberbox':
269
+ return (
270
+ <Numberbox
271
+ key={key}
272
+ rawArgs={node.rawArgs}
273
+ className={node.className}
274
+ id={node.id}
275
+ />
276
+ );
277
+
278
+ case 'textarea':
279
+ return (
280
+ <Textarea
281
+ key={key}
282
+ rawArgs={node.rawArgs}
283
+ className={node.className}
284
+ id={node.id}
285
+ />
286
+ );
287
+
288
+ case 'checkbox':
289
+ return (
290
+ <Checkbox
291
+ key={key}
292
+ rawArgs={node.rawArgs}
293
+ className={node.className}
294
+ id={node.id}
295
+ />
296
+ );
297
+
298
+ case 'radiobutton':
299
+ return (
300
+ <Radiobutton
301
+ key={key}
302
+ rawArgs={node.rawArgs}
303
+ className={node.className}
304
+ id={node.id}
305
+ />
306
+ );
307
+
308
+ case 'listbox':
309
+ return (
310
+ <Listbox
311
+ key={key}
312
+ rawArgs={node.rawArgs}
313
+ children={node.children}
314
+ className={node.className}
315
+ id={node.id}
316
+ />
317
+ );
318
+
319
+ case 'cycle':
320
+ return (
321
+ <Cycle
322
+ key={key}
323
+ rawArgs={node.rawArgs}
324
+ children={node.children}
325
+ className={node.className}
326
+ id={node.id}
327
+ />
328
+ );
329
+
330
+ case 'link':
331
+ return (
332
+ <MacroLink
333
+ key={key}
334
+ rawArgs={node.rawArgs}
335
+ children={node.children}
336
+ className={node.className}
337
+ id={node.id}
338
+ />
339
+ );
340
+
341
+ case 'switch':
342
+ return (
343
+ <Switch
344
+ key={key}
345
+ rawArgs={node.rawArgs}
346
+ branches={node.branches!}
347
+ />
348
+ );
349
+
350
+ case 'timed':
351
+ return (
352
+ <Timed
353
+ key={key}
354
+ rawArgs={node.rawArgs}
355
+ children={node.children}
356
+ branches={node.branches!}
357
+ className={node.className}
358
+ id={node.id}
359
+ />
360
+ );
361
+
362
+ case 'repeat':
363
+ return (
364
+ <Repeat
365
+ key={key}
366
+ rawArgs={node.rawArgs}
367
+ children={node.children}
368
+ className={node.className}
369
+ id={node.id}
370
+ />
371
+ );
372
+
373
+ case 'stop':
374
+ return (
375
+ <Stop key={key} />
376
+ );
377
+
378
+ case 'type':
379
+ return (
380
+ <Type
381
+ key={key}
382
+ rawArgs={node.rawArgs}
383
+ children={node.children}
384
+ className={node.className}
385
+ id={node.id}
386
+ />
387
+ );
388
+
389
+ case 'widget':
390
+ return (
391
+ <Widget
392
+ key={key}
393
+ rawArgs={node.rawArgs}
394
+ children={node.children}
395
+ />
396
+ );
397
+
398
+ // {option}, {case}, {default}, {next} are handled by parent components
399
+ case 'option':
400
+ case 'case':
401
+ case 'default':
402
+ case 'next':
403
+ return null;
404
+
405
+ default: {
406
+ // Check widget registry for user-defined widgets
407
+ const widgetAST = getWidget(node.name);
408
+ if (widgetAST) {
409
+ return <>{renderNodes(widgetAST)}</>;
410
+ }
411
+
412
+ // Check component registry for custom macros
413
+ const Component = getMacro(node.name);
414
+ if (Component) {
415
+ return (
416
+ <Component
417
+ key={key}
418
+ rawArgs={node.rawArgs}
419
+ className={node.className}
420
+ id={node.id}
421
+ >
422
+ {renderNodes(node.children)}
423
+ </Component>
424
+ );
425
+ }
426
+
427
+ return (
428
+ <span
429
+ key={key}
430
+ class="error"
431
+ >
432
+ {`{unknown macro: ${node.name}}`}
433
+ </span>
434
+ );
435
+ }
436
+ }
437
+ }
438
+
439
+ /**
440
+ * Render a non-text AST node to a Preact element.
441
+ */
442
+ function renderSingleNode(
443
+ node: ASTNode,
444
+ key: number,
445
+ ): preact.ComponentChildren {
446
+ switch (node.type) {
447
+ case 'text':
448
+ return node.value;
449
+
450
+ case 'link':
451
+ return (
452
+ <PassageLink
453
+ key={key}
454
+ target={node.target}
455
+ className={node.className}
456
+ id={node.id}
457
+ >
458
+ {node.display}
459
+ </PassageLink>
460
+ );
461
+
462
+ case 'variable':
463
+ return (
464
+ <VarDisplay
465
+ key={key}
466
+ name={node.name}
467
+ scope={node.scope}
468
+ className={node.className}
469
+ id={node.id}
470
+ />
471
+ );
472
+
473
+ case 'macro':
474
+ return renderMacro(node, key);
475
+
476
+ case 'html':
477
+ return h(
478
+ node.tag,
479
+ { key, ...node.attributes },
480
+ node.children.length > 0 ? renderNodes(node.children) : undefined,
481
+ );
482
+ }
483
+ }
484
+
485
+ /**
486
+ * Render AST nodes without markdown processing.
487
+ * Used for inline containers (button labels, link text) where block-level
488
+ * markdown (lists, headers) would misinterpret content like "-" or "+".
489
+ */
490
+ export function renderInlineNodes(
491
+ nodes: ASTNode[],
492
+ ): preact.ComponentChildren {
493
+ if (nodes.length === 0) return null;
494
+ return nodes.map((node, i) => renderSingleNode(node, i));
495
+ }
496
+
497
+ /**
498
+ * Render AST nodes with full CommonMark markdown support.
499
+ *
500
+ * Combines all nodes into a single markdown document, using <tw-N> placeholder
501
+ * elements for non-text nodes (variables, macros, links, HTML). This allows
502
+ * markdown syntax to span across Twine tokens — e.g., markdown tables can
503
+ * contain {$variables} and {macros} in their cells.
504
+ *
505
+ * After micromark processes the combined string, the HTML is parsed back into
506
+ * Preact VNodes with placeholders replaced by the real rendered components.
507
+ */
508
+ export function renderNodes(nodes: ASTNode[]): preact.ComponentChildren {
509
+ if (nodes.length === 0) return null;
510
+
511
+ // If there's no text at all, render nodes directly without markdown
512
+ const hasText = nodes.some((n) => n.type === 'text');
513
+ if (!hasText) {
514
+ return nodes.map((node, i) => renderSingleNode(node, i));
515
+ }
516
+
517
+ // Build combined markdown string with placeholders for non-text nodes
518
+ const components: preact.ComponentChildren[] = [];
519
+ let combined = '';
520
+
521
+ for (let i = 0; i < nodes.length; i++) {
522
+ const node = nodes[i];
523
+ if (node.type === 'text') {
524
+ combined += node.value;
525
+ } else {
526
+ const phIdx = components.length;
527
+ components.push(renderSingleNode(node, i));
528
+ combined += `<span data-tw="${phIdx}"></span>`;
529
+ }
530
+ }
531
+
532
+ // Run combined text through markdown
533
+ const html = markdownToHtml(combined);
534
+
535
+ // Convert HTML to Preact VNodes, replacing placeholders with components
536
+ return htmlToPreact(html, components);
537
+ }