@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.
- package/README.md +66 -0
- package/dist/pkg/format.js +1 -0
- package/dist/pkg/index.js +12 -0
- package/dist/pkg/types/globals.d.ts +18 -0
- package/dist/pkg/types/index.d.ts +158 -0
- package/package.json +71 -0
- package/src/components/App.tsx +53 -0
- package/src/components/Passage.tsx +36 -0
- package/src/components/PassageLink.tsx +35 -0
- package/src/components/SaveLoadDialog.tsx +403 -0
- package/src/components/SettingsDialog.tsx +106 -0
- package/src/components/StoryInterface.tsx +31 -0
- package/src/components/macros/Back.tsx +23 -0
- package/src/components/macros/Button.tsx +49 -0
- package/src/components/macros/Checkbox.tsx +41 -0
- package/src/components/macros/Computed.tsx +100 -0
- package/src/components/macros/Cycle.tsx +39 -0
- package/src/components/macros/Do.tsx +46 -0
- package/src/components/macros/For.tsx +113 -0
- package/src/components/macros/Forward.tsx +25 -0
- package/src/components/macros/Goto.tsx +23 -0
- package/src/components/macros/If.tsx +63 -0
- package/src/components/macros/Include.tsx +52 -0
- package/src/components/macros/Listbox.tsx +42 -0
- package/src/components/macros/MacroLink.tsx +107 -0
- package/src/components/macros/Numberbox.tsx +43 -0
- package/src/components/macros/Print.tsx +48 -0
- package/src/components/macros/QuickLoad.tsx +33 -0
- package/src/components/macros/QuickSave.tsx +22 -0
- package/src/components/macros/Radiobutton.tsx +59 -0
- package/src/components/macros/Repeat.tsx +53 -0
- package/src/components/macros/Restart.tsx +27 -0
- package/src/components/macros/Saves.tsx +25 -0
- package/src/components/macros/Set.tsx +36 -0
- package/src/components/macros/SettingsButton.tsx +29 -0
- package/src/components/macros/Stop.tsx +12 -0
- package/src/components/macros/StoryTitle.tsx +20 -0
- package/src/components/macros/Switch.tsx +69 -0
- package/src/components/macros/Textarea.tsx +41 -0
- package/src/components/macros/Textbox.tsx +40 -0
- package/src/components/macros/Timed.tsx +63 -0
- package/src/components/macros/Type.tsx +83 -0
- package/src/components/macros/Unset.tsx +25 -0
- package/src/components/macros/VarDisplay.tsx +44 -0
- package/src/components/macros/Widget.tsx +18 -0
- package/src/components/macros/option-utils.ts +14 -0
- package/src/expression.ts +93 -0
- package/src/index.tsx +120 -0
- package/src/markup/ast.ts +284 -0
- package/src/markup/markdown.ts +21 -0
- package/src/markup/render.tsx +537 -0
- package/src/markup/tokenizer.ts +581 -0
- package/src/parser.ts +72 -0
- package/src/registry.ts +21 -0
- package/src/saves/idb.ts +165 -0
- package/src/saves/save-manager.ts +317 -0
- package/src/saves/types.ts +40 -0
- package/src/settings.ts +96 -0
- package/src/store.ts +317 -0
- package/src/story-api.ts +129 -0
- package/src/story-init.ts +67 -0
- package/src/story-variables.ts +166 -0
- package/src/styles.css +780 -0
- package/src/utils/parse-delay.ts +14 -0
- 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
|
+
}
|