@fuzdev/mdz 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 (101) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +32 -0
  3. package/dist/Mdz.svelte +39 -0
  4. package/dist/Mdz.svelte.d.ts +17 -0
  5. package/dist/Mdz.svelte.d.ts.map +1 -0
  6. package/dist/MdzNodeView.svelte +140 -0
  7. package/dist/MdzNodeView.svelte.d.ts +9 -0
  8. package/dist/MdzNodeView.svelte.d.ts.map +1 -0
  9. package/dist/MdzPrecompiled.svelte +33 -0
  10. package/dist/MdzPrecompiled.svelte.d.ts +17 -0
  11. package/dist/MdzPrecompiled.svelte.d.ts.map +1 -0
  12. package/dist/MdzRoot.svelte +40 -0
  13. package/dist/MdzRoot.svelte.d.ts +14 -0
  14. package/dist/MdzRoot.svelte.d.ts.map +1 -0
  15. package/dist/MdzStream.svelte +37 -0
  16. package/dist/MdzStream.svelte.d.ts +18 -0
  17. package/dist/MdzStream.svelte.d.ts.map +1 -0
  18. package/dist/MdzStreamNodeView.svelte +150 -0
  19. package/dist/MdzStreamNodeView.svelte.d.ts +9 -0
  20. package/dist/MdzStreamNodeView.svelte.d.ts.map +1 -0
  21. package/dist/context_helpers.d.ts +23 -0
  22. package/dist/context_helpers.d.ts.map +1 -0
  23. package/dist/context_helpers.js +19 -0
  24. package/dist/mdz.d.ts +152 -0
  25. package/dist/mdz.d.ts.map +1 -0
  26. package/dist/mdz.js +61 -0
  27. package/dist/mdz_contexts.d.ts +106 -0
  28. package/dist/mdz_contexts.d.ts.map +1 -0
  29. package/dist/mdz_contexts.js +42 -0
  30. package/dist/mdz_helpers.d.ts +321 -0
  31. package/dist/mdz_helpers.d.ts.map +1 -0
  32. package/dist/mdz_helpers.js +668 -0
  33. package/dist/mdz_lexer.d.ts +123 -0
  34. package/dist/mdz_lexer.d.ts.map +1 -0
  35. package/dist/mdz_lexer.js +1405 -0
  36. package/dist/mdz_opcodes.d.ts +193 -0
  37. package/dist/mdz_opcodes.d.ts.map +1 -0
  38. package/dist/mdz_opcodes.js +14 -0
  39. package/dist/mdz_opcodes_to_nodes.d.ts +26 -0
  40. package/dist/mdz_opcodes_to_nodes.d.ts.map +1 -0
  41. package/dist/mdz_opcodes_to_nodes.js +410 -0
  42. package/dist/mdz_stream_parser.d.ts +109 -0
  43. package/dist/mdz_stream_parser.d.ts.map +1 -0
  44. package/dist/mdz_stream_parser.js +699 -0
  45. package/dist/mdz_stream_parser_block.d.ts +99 -0
  46. package/dist/mdz_stream_parser_block.d.ts.map +1 -0
  47. package/dist/mdz_stream_parser_block.js +437 -0
  48. package/dist/mdz_stream_parser_inline.d.ts +98 -0
  49. package/dist/mdz_stream_parser_inline.d.ts.map +1 -0
  50. package/dist/mdz_stream_parser_inline.js +481 -0
  51. package/dist/mdz_stream_parser_link.d.ts +29 -0
  52. package/dist/mdz_stream_parser_link.d.ts.map +1 -0
  53. package/dist/mdz_stream_parser_link.js +227 -0
  54. package/dist/mdz_stream_parser_list.d.ts +106 -0
  55. package/dist/mdz_stream_parser_list.d.ts.map +1 -0
  56. package/dist/mdz_stream_parser_list.js +438 -0
  57. package/dist/mdz_stream_parser_state.d.ts +476 -0
  58. package/dist/mdz_stream_parser_state.d.ts.map +1 -0
  59. package/dist/mdz_stream_parser_state.js +651 -0
  60. package/dist/mdz_stream_parser_text.d.ts +25 -0
  61. package/dist/mdz_stream_parser_text.d.ts.map +1 -0
  62. package/dist/mdz_stream_parser_text.js +67 -0
  63. package/dist/mdz_stream_parser_url.d.ts +76 -0
  64. package/dist/mdz_stream_parser_url.d.ts.map +1 -0
  65. package/dist/mdz_stream_parser_url.js +328 -0
  66. package/dist/mdz_stream_state.svelte.d.ts +53 -0
  67. package/dist/mdz_stream_state.svelte.d.ts.map +1 -0
  68. package/dist/mdz_stream_state.svelte.js +360 -0
  69. package/dist/mdz_to_svelte.d.ts +71 -0
  70. package/dist/mdz_to_svelte.d.ts.map +1 -0
  71. package/dist/mdz_to_svelte.js +127 -0
  72. package/dist/mdz_token_parser.d.ts +20 -0
  73. package/dist/mdz_token_parser.d.ts.map +1 -0
  74. package/dist/mdz_token_parser.js +449 -0
  75. package/dist/svelte_preprocess_mdz.d.ts +80 -0
  76. package/dist/svelte_preprocess_mdz.d.ts.map +1 -0
  77. package/dist/svelte_preprocess_mdz.js +543 -0
  78. package/dist/tsdoc_mdz.d.ts +45 -0
  79. package/dist/tsdoc_mdz.d.ts.map +1 -0
  80. package/dist/tsdoc_mdz.js +88 -0
  81. package/package.json +131 -0
  82. package/src/lib/context_helpers.ts +52 -0
  83. package/src/lib/mdz.ts +190 -0
  84. package/src/lib/mdz_contexts.ts +99 -0
  85. package/src/lib/mdz_helpers.ts +727 -0
  86. package/src/lib/mdz_lexer.ts +1761 -0
  87. package/src/lib/mdz_opcodes.ts +227 -0
  88. package/src/lib/mdz_opcodes_to_nodes.ts +470 -0
  89. package/src/lib/mdz_stream_parser.ts +766 -0
  90. package/src/lib/mdz_stream_parser_block.ts +490 -0
  91. package/src/lib/mdz_stream_parser_inline.ts +527 -0
  92. package/src/lib/mdz_stream_parser_link.ts +283 -0
  93. package/src/lib/mdz_stream_parser_list.ts +525 -0
  94. package/src/lib/mdz_stream_parser_state.ts +957 -0
  95. package/src/lib/mdz_stream_parser_text.ts +95 -0
  96. package/src/lib/mdz_stream_parser_url.ts +386 -0
  97. package/src/lib/mdz_stream_state.svelte.ts +383 -0
  98. package/src/lib/mdz_to_svelte.ts +213 -0
  99. package/src/lib/mdz_token_parser.ts +544 -0
  100. package/src/lib/svelte_preprocess_mdz.ts +777 -0
  101. package/src/lib/tsdoc_mdz.ts +97 -0
@@ -0,0 +1,383 @@
1
+ /**
2
+ * Reactive state for the mdz streaming renderer.
3
+ *
4
+ * Maintains a reactive tree of `MdzStreamNode` objects that Svelte 5 can
5
+ * efficiently update. Opcodes from `MdzStreamParser` are applied via
6
+ * `apply()` or `apply_batch()`, mutating the tree in place.
7
+ *
8
+ * @module
9
+ */
10
+
11
+ import {DEV} from 'esm-env';
12
+
13
+ import type {MdzNodeId, MdzNodeType, MdzOpcode} from './mdz_opcodes.js';
14
+
15
+ /**
16
+ * A reactive node in the stream renderer tree.
17
+ * Fields are `$state` so Svelte re-renders only what changes.
18
+ */
19
+ export class MdzStreamNode {
20
+ id: MdzNodeId;
21
+ type: MdzNodeType;
22
+ content: string = $state('');
23
+ children: Array<MdzStreamNode> = $state([]);
24
+ // metadata
25
+ level?: 1 | 2 | 3 | 4 | 5 | 6;
26
+ reference?: string = $state();
27
+ link_type?: 'external' | 'internal' = $state();
28
+ name?: string;
29
+ lang?: string | null;
30
+ heading_id?: string = $state();
31
+ ordered?: boolean;
32
+ start_number?: number;
33
+ number?: number;
34
+
35
+ constructor(id: MdzNodeId, type: MdzNodeType) {
36
+ this.id = id;
37
+ this.type = type;
38
+ }
39
+ }
40
+
41
+ /**
42
+ * Reactive state manager for streaming mdz content.
43
+ * Apply opcodes to incrementally build and update the render tree.
44
+ *
45
+ * Lifecycle: one state instance per stream, paired with one `MdzStreamParser`.
46
+ * Opcodes must be applied exactly once, in emission order — `take_opcodes()`
47
+ * batches can be split or concatenated freely, but never reordered, skipped,
48
+ * or replayed. There is no `reset()`; to restart a stream, construct a new
49
+ * parser and a new state.
50
+ */
51
+ export class MdzStreamState {
52
+ root: Array<MdzStreamNode> = $state([]);
53
+
54
+ /** Node lookup by ID. */
55
+ #nodes = new Map<MdzNodeId, MdzStreamNode>();
56
+
57
+ /** Stack of open container IDs (for knowing current parent). */
58
+ #stack: Array<MdzNodeId> = [];
59
+
60
+ /**
61
+ * Children array of the container at `stack_idx - 1` — the parent of a
62
+ * node sitting at `stack_idx` on the stack — or `root` when the node is
63
+ * top-level. There is no parent map: a stack-resident node's parent is
64
+ * the stack entry below it, and every other opcode target (text being
65
+ * trimmed, wrap targets) lives in the innermost open container, so the
66
+ * stack answers all parent lookups. See the matching invariants in
67
+ * `mdz_opcodes_to_nodes`.
68
+ */
69
+ #parent_children_at(stack_idx: number): Array<MdzStreamNode> {
70
+ if (stack_idx <= 0) return this.root;
71
+ const parent = this.#nodes.get(this.#stack[stack_idx - 1]!);
72
+ if (DEV && !parent) {
73
+ throw new Error(
74
+ `MdzStreamState: parent id ${this.#stack[stack_idx - 1]} on stack but missing from nodes`,
75
+ );
76
+ }
77
+ return parent ? parent.children : this.root;
78
+ }
79
+
80
+ /**
81
+ * Register a new node and attach it to its current parent (innermost open
82
+ * container, or root). Shared by `open`, `text`, and `void` paths.
83
+ */
84
+ #attach(node: MdzStreamNode): void {
85
+ this.#nodes.set(node.id, node);
86
+ this.#parent_children_at(this.#stack.length).push(node);
87
+ }
88
+
89
+ /**
90
+ * Apply a single opcode.
91
+ */
92
+ apply(opcode: MdzOpcode): void {
93
+ switch (opcode.type) {
94
+ case 'open': {
95
+ if (DEV && this.#nodes.has(opcode.id)) {
96
+ throw new Error(
97
+ `MdzStreamState: duplicate open for id ${opcode.id} (node_type '${opcode.node_type}')`,
98
+ );
99
+ }
100
+ const node = new MdzStreamNode(opcode.id, opcode.node_type);
101
+ if (opcode.level !== undefined) node.level = opcode.level;
102
+ if (opcode.name !== undefined) node.name = opcode.name;
103
+ if (opcode.lang !== undefined) node.lang = opcode.lang;
104
+ if (opcode.ordered !== undefined) node.ordered = opcode.ordered;
105
+ if (opcode.start_number !== undefined) node.start_number = opcode.start_number;
106
+ if (opcode.number !== undefined) node.number = opcode.number;
107
+ this.#attach(node);
108
+ this.#stack.push(opcode.id);
109
+ break;
110
+ }
111
+
112
+ case 'close': {
113
+ const stack_idx = this.#stack.lastIndexOf(opcode.id);
114
+ if (DEV) {
115
+ if (!this.#nodes.has(opcode.id)) {
116
+ throw new Error(`MdzStreamState: close for unknown id ${opcode.id}`);
117
+ }
118
+ if (stack_idx === -1) {
119
+ throw new Error(`MdzStreamState: close for id ${opcode.id} which is not on the stack`);
120
+ }
121
+ }
122
+ const node = this.#nodes.get(opcode.id);
123
+ if (node) {
124
+ if (opcode.discard) {
125
+ // drop the node and its descendants from its parent's children —
126
+ // it's the parent's last child (anything attached after it
127
+ // opened went inside it), with a defensive backward scan
128
+ if (stack_idx !== -1) {
129
+ const parent_children = this.#parent_children_at(stack_idx);
130
+ for (let i = parent_children.length - 1; i >= 0; i--) {
131
+ if (parent_children[i] === node) {
132
+ parent_children.splice(i, 1);
133
+ break;
134
+ }
135
+ }
136
+ }
137
+ } else {
138
+ // apply deferred metadata
139
+ if (opcode.reference !== undefined) node.reference = opcode.reference;
140
+ if (opcode.link_type !== undefined) node.link_type = opcode.link_type;
141
+ if (opcode.heading_id !== undefined) node.heading_id = opcode.heading_id;
142
+ // Code/Codeblock: collapse text children into `content` so renderers
143
+ // can read it directly without walking children every render.
144
+ if (node.type === 'Code' || node.type === 'Codeblock') {
145
+ let content = '';
146
+ for (const child of node.children) content += child.content;
147
+ node.content = content;
148
+ }
149
+ }
150
+ }
151
+ // pop from stack
152
+ if (stack_idx !== -1) this.#stack.splice(stack_idx, 1);
153
+ // cleanup: closed nodes have no further opcode interactions.
154
+ // children are retained in the render tree via children arrays
155
+ // (discarded nodes' subtrees also need full cleanup, which the
156
+ // traversal in `#cleanup_node` already handles).
157
+ this.#cleanup_node(opcode.id);
158
+ break;
159
+ }
160
+
161
+ case 'text': {
162
+ if (DEV && this.#nodes.has(opcode.id)) {
163
+ throw new Error(
164
+ `MdzStreamState: duplicate text for id ${opcode.id} (text_type '${opcode.text_type}')`,
165
+ );
166
+ }
167
+ const node = new MdzStreamNode(opcode.id, opcode.text_type);
168
+ node.content = opcode.content;
169
+ this.#attach(node);
170
+ break;
171
+ }
172
+
173
+ case 'append_text': {
174
+ if (DEV && !this.#nodes.has(opcode.id)) {
175
+ throw new Error(`MdzStreamState: append_text for unknown id ${opcode.id}`);
176
+ }
177
+ const node = this.#nodes.get(opcode.id);
178
+ if (node) {
179
+ node.content += opcode.content;
180
+ }
181
+ break;
182
+ }
183
+
184
+ case 'trim_text': {
185
+ if (DEV && !this.#nodes.has(opcode.id)) {
186
+ throw new Error(`MdzStreamState: trim_text for unknown id ${opcode.id}`);
187
+ }
188
+ const node = this.#nodes.get(opcode.id);
189
+ if (node) {
190
+ node.content = node.content.slice(0, node.content.length - opcode.count);
191
+ if (node.content.length === 0) {
192
+ // trim targets the most recent text run with no structural
193
+ // opcode since, so the node is the last child of the innermost
194
+ // open container — backward scan from the end
195
+ const parent_children = this.#parent_children_at(this.#stack.length);
196
+ for (let i = parent_children.length - 1; i >= 0; i--) {
197
+ if (parent_children[i] === node) {
198
+ parent_children.splice(i, 1);
199
+ break;
200
+ }
201
+ }
202
+ this.#nodes.delete(opcode.id);
203
+ }
204
+ }
205
+ break;
206
+ }
207
+
208
+ case 'void': {
209
+ if (DEV && this.#nodes.has(opcode.id)) {
210
+ throw new Error(
211
+ `MdzStreamState: duplicate void for id ${opcode.id} (node_type '${opcode.node_type}')`,
212
+ );
213
+ }
214
+ const node = new MdzStreamNode(opcode.id, opcode.node_type);
215
+ this.#attach(node);
216
+ break;
217
+ }
218
+
219
+ case 'revert': {
220
+ // the reverted node is the last child of its parent even for a
221
+ // mid-stack revert (a failed-closer revert with containers still
222
+ // open above it) — anything attached after it opened went inside
223
+ // it, so the pop+append fast path below covers both cases.
224
+ // lastIndexOf finds the node's own stack position; its parent is
225
+ // the entry below that position.
226
+ const stack_idx = this.#stack.lastIndexOf(opcode.id);
227
+ if (DEV) {
228
+ if (!this.#nodes.has(opcode.id)) {
229
+ throw new Error(`MdzStreamState: revert for unknown id ${opcode.id}`);
230
+ }
231
+ if (stack_idx === -1) {
232
+ throw new Error(`MdzStreamState: revert for id ${opcode.id} which is not on the stack`);
233
+ }
234
+ }
235
+ const node = this.#nodes.get(opcode.id);
236
+ if (!node || stack_idx === -1) break;
237
+
238
+ const parent_children = this.#parent_children_at(stack_idx);
239
+
240
+ // find the node — last child of its parent, backward scan is defensive
241
+ let node_idx = -1;
242
+ for (let i = parent_children.length - 1; i >= 0; i--) {
243
+ if (parent_children[i] === node) {
244
+ node_idx = i;
245
+ break;
246
+ }
247
+ }
248
+ if (node_idx === -1) {
249
+ if (DEV) {
250
+ throw new Error(
251
+ `MdzStreamState: revert for id ${opcode.id} but node not found in parent's children`,
252
+ );
253
+ }
254
+ break;
255
+ }
256
+
257
+ // remove reverted node from stack before any pushes
258
+ this.#stack.splice(stack_idx, 1);
259
+
260
+ // replacement literal delimiter text. Reuses the reverted node's id
261
+ // (unique — the node is deleted below) for a stable keyed-each key;
262
+ // deliberately not registered in #nodes (no opcode ever targets it).
263
+ let replacement_text_node: MdzStreamNode | null = null;
264
+ if (opcode.replacement_text) {
265
+ replacement_text_node = new MdzStreamNode(opcode.id, 'Text');
266
+ replacement_text_node.content = opcode.replacement_text;
267
+ }
268
+
269
+ if (node_idx === parent_children.length - 1) {
270
+ // inline revert: the node's children become the parent's, in place
271
+ // at the end — pop + append, never a spread (a spread of the
272
+ // accumulated children would hit the argument-count limit on
273
+ // pathological inputs)
274
+ parent_children.pop();
275
+ if (replacement_text_node) parent_children.push(replacement_text_node);
276
+ for (const child of node.children) {
277
+ parent_children.push(child);
278
+ }
279
+ } else {
280
+ // defensive mid-array fallback — unreachable for parser-produced
281
+ // streams, where the reverted node is always the last child
282
+ const replacement_nodes: Array<MdzStreamNode> = replacement_text_node
283
+ ? [replacement_text_node, ...node.children]
284
+ : [...node.children];
285
+ parent_children.splice(node_idx, 1, ...replacement_nodes);
286
+ }
287
+
288
+ this.#nodes.delete(opcode.id);
289
+ break;
290
+ }
291
+
292
+ case 'wrap': {
293
+ if (DEV) {
294
+ if (!this.#nodes.has(opcode.target_id)) {
295
+ throw new Error(`MdzStreamState: wrap target_id ${opcode.target_id} not in nodes`);
296
+ }
297
+ if (this.#nodes.has(opcode.id)) {
298
+ throw new Error(`MdzStreamState: wrap id ${opcode.id} already exists in nodes`);
299
+ }
300
+ }
301
+ const target = this.#nodes.get(opcode.target_id);
302
+ if (!target) break;
303
+
304
+ // wraps target a text node in the still-open innermost container,
305
+ // emitted before any structural opcode could move it — so the
306
+ // target sits at/near the end of the container's children
307
+ const parent_children = this.#parent_children_at(this.#stack.length);
308
+
309
+ // find target — backward scan from the end
310
+ let target_idx = -1;
311
+ for (let i = parent_children.length - 1; i >= 0; i--) {
312
+ if (parent_children[i] === target) {
313
+ target_idx = i;
314
+ break;
315
+ }
316
+ }
317
+ if (target_idx === -1) break;
318
+
319
+ // handle trailing punctuation trim
320
+ let trimmed_node: MdzStreamNode | null = null;
321
+ if (opcode.trim_end && opcode.trim_end > 0 && opcode.trim_id != null) {
322
+ const trimmed_content = target.content.slice(target.content.length - opcode.trim_end);
323
+ target.content = target.content.slice(0, target.content.length - opcode.trim_end);
324
+ trimmed_node = new MdzStreamNode(opcode.trim_id, 'Text');
325
+ trimmed_node.content = trimmed_content;
326
+ this.#nodes.set(opcode.trim_id, trimmed_node);
327
+ }
328
+
329
+ // create Link wrapper containing target
330
+ const link = new MdzStreamNode(opcode.id, opcode.node_type);
331
+ link.reference = opcode.reference;
332
+ link.link_type = opcode.link_type;
333
+ link.children.push(target);
334
+ this.#nodes.set(opcode.id, link);
335
+
336
+ // replace target with [Link, trimmed?] in parent's children
337
+ if (trimmed_node) {
338
+ parent_children.splice(target_idx, 1, link, trimmed_node);
339
+ } else {
340
+ parent_children[target_idx] = link;
341
+ }
342
+
343
+ // cleanup: Link is complete (no close opcode coming).
344
+ // trim_id (if any) stays in maps — cleaned when parent closes.
345
+ this.#cleanup_node(opcode.id);
346
+ break;
347
+ }
348
+ }
349
+ }
350
+
351
+ /**
352
+ * Remove a node and its descendants from the `#nodes` lookup. Iterative to avoid
353
+ * stack overflow on deep trees.
354
+ *
355
+ * Parser invariant: once a container closes, no future opcode targets that
356
+ * subtree (no `append_text` to a closed Text, no nested `wrap`/`revert`).
357
+ * Children stay reachable via `parent.children` arrays — only the lookup
358
+ * map is pruned. If this invariant is violated, the misbehaving opcode
359
+ * triggers a DEV throw in `apply()` and silently no-ops in production.
360
+ */
361
+ #cleanup_node(id: MdzNodeId): void {
362
+ const queue: Array<MdzNodeId> = [id];
363
+ while (queue.length > 0) {
364
+ const current_id = queue.pop()!;
365
+ const node = this.#nodes.get(current_id);
366
+ if (node) {
367
+ for (const child of node.children) {
368
+ queue.push(child.id);
369
+ }
370
+ }
371
+ this.#nodes.delete(current_id);
372
+ }
373
+ }
374
+
375
+ /**
376
+ * Apply a batch of opcodes.
377
+ */
378
+ apply_batch(opcodes: Array<MdzOpcode>): void {
379
+ for (const op of opcodes) {
380
+ this.apply(op);
381
+ }
382
+ }
383
+ }
@@ -0,0 +1,213 @@
1
+ /**
2
+ * Converts parsed `MdzNode` arrays to Svelte markup strings.
3
+ *
4
+ * Used by the `svelte_preprocess_mdz` preprocessor to expand static `<Mdz content="...">` usages
5
+ * into pre-rendered Svelte markup at build time. The output for each node type matches what
6
+ * `MdzNodeView.svelte` renders at runtime, so precompiled and runtime rendering are identical.
7
+ *
8
+ * @module
9
+ */
10
+
11
+ import {UnreachableError} from '@fuzdev/fuz_util/error.js';
12
+ import {escape_svelte_text} from '@fuzdev/fuz_util/svelte_preprocess_helpers.js';
13
+ import {escape_js_string} from '@fuzdev/fuz_util/string.js';
14
+
15
+ import type {MdzNode} from './mdz.js';
16
+ import {mdz_resolve_relative_path, mdz_is_void_element} from './mdz_helpers.js';
17
+
18
+ /**
19
+ * Result of converting `MdzNode` arrays to Svelte markup.
20
+ */
21
+ export interface MdzToSvelteResult {
22
+ /** Generated Svelte markup string. */
23
+ markup: string;
24
+
25
+ /** Required imports: `Map<local_name, {path, kind}>`. */
26
+ imports: Map<string, {path: string; kind: 'default' | 'named'}>;
27
+
28
+ /** Whether content references unconfigured Component or Element tags. */
29
+ has_unconfigured_tags: boolean;
30
+ }
31
+
32
+ /** Derives an import's local identifier from a `.svelte` import path's basename. */
33
+ const component_local_name = (import_path: string): string =>
34
+ import_path.slice(import_path.lastIndexOf('/') + 1).replace(/\.svelte$/, '');
35
+
36
+ /**
37
+ * Options for `mdz_to_svelte`.
38
+ */
39
+ export interface MdzToSvelteOptions {
40
+ /**
41
+ * Component name to import path mapping (e.g., `{Alert: '$lib/Alert.svelte'}`).
42
+ * If content references a component not in this map, `has_unconfigured_tags` is set.
43
+ */
44
+ components: Record<string, string>;
45
+
46
+ /**
47
+ * Allowed HTML elements (e.g., `{aside: true, details: true}`).
48
+ * Values are boolean placeholders for future per-element configuration,
49
+ * mirroring the runtime `MdzElements` registry.
50
+ * If content references an element not in this record, `has_unconfigured_tags` is set.
51
+ */
52
+ elements: Record<string, boolean>;
53
+
54
+ /**
55
+ * Base path for resolving relative links (e.g., `'/docs/usage/'`).
56
+ * When provided, relative references (`./`, `../`) are resolved to absolute paths
57
+ * and passed through `resolve()`. Trailing slash recommended.
58
+ */
59
+ base?: string;
60
+
61
+ /**
62
+ * Import path for the inline `code` component (receives `reference`).
63
+ * When omitted, inline code renders as a plain `<code>` element — matching
64
+ * `MdzNodeView.svelte`'s runtime default, so precompiled and runtime output
65
+ * stay identical per configuration.
66
+ */
67
+ code_component_import?: string;
68
+
69
+ /**
70
+ * Import path for the codeblock component (receives `lang` and `content`).
71
+ * When omitted, code blocks render as a plain `<pre><code>` element —
72
+ * matching `MdzNodeView.svelte`'s runtime default.
73
+ */
74
+ codeblock_component_import?: string;
75
+ }
76
+
77
+ /**
78
+ * Converts an array of `MdzNode` to a Svelte markup string.
79
+ *
80
+ * Each node type produces output matching what `MdzNodeView.svelte` renders at runtime.
81
+ * Collects required imports and flags unconfigured component/element references.
82
+ *
83
+ * @param nodes - parsed mdz nodes to render
84
+ * @param options - rendering configuration matching the runtime context surface
85
+ */
86
+ export const mdz_to_svelte = (
87
+ nodes: Array<MdzNode>,
88
+ options: MdzToSvelteOptions,
89
+ ): MdzToSvelteResult => {
90
+ const {components, elements, base, code_component_import, codeblock_component_import} = options;
91
+ const imports: Map<string, {path: string; kind: 'default' | 'named'}> = new Map();
92
+ let has_unconfigured_tags = false;
93
+
94
+ const render_nodes = (children: Array<MdzNode>): string => {
95
+ return children.map((child) => render_node(child)).join('');
96
+ };
97
+
98
+ const render_node = (node: MdzNode): string => {
99
+ switch (node.type) {
100
+ case 'Text':
101
+ return escape_svelte_text(node.content);
102
+
103
+ case 'Code': {
104
+ if (!code_component_import) {
105
+ return `<code>${escape_svelte_text(node.content)}</code>`;
106
+ }
107
+ const name = component_local_name(code_component_import);
108
+ imports.set(name, {path: code_component_import, kind: 'default'});
109
+ return `<${name} reference={'${escape_js_string(node.content)}'} />`;
110
+ }
111
+
112
+ case 'Codeblock': {
113
+ const lang_attr =
114
+ node.lang === null ? 'lang={null}' : `lang={'${escape_js_string(node.lang)}'}`;
115
+ if (!codeblock_component_import) {
116
+ return `<pre><code>${escape_svelte_text(node.content)}</code></pre>`;
117
+ }
118
+ const name = component_local_name(codeblock_component_import);
119
+ imports.set(name, {path: codeblock_component_import, kind: 'default'});
120
+ return `<${name} ${lang_attr} content={'${escape_js_string(node.content)}'} />`;
121
+ }
122
+
123
+ case 'Bold':
124
+ return `<strong>${render_nodes(node.children)}</strong>`;
125
+
126
+ case 'Italic':
127
+ return `<em>${render_nodes(node.children)}</em>`;
128
+
129
+ case 'Strikethrough':
130
+ return `<s>${render_nodes(node.children)}</s>`;
131
+
132
+ case 'Link': {
133
+ const children_markup = render_nodes(node.children);
134
+ if (node.link_type === 'internal') {
135
+ if (node.reference.startsWith('.') && base) {
136
+ const resolved = mdz_resolve_relative_path(node.reference, base);
137
+ imports.set('resolve', {path: '$app/paths', kind: 'named'});
138
+ return `<a href={resolve('${escape_js_string(resolved)}')}>${children_markup}</a>`;
139
+ }
140
+ if (
141
+ node.reference.startsWith('#') ||
142
+ node.reference.startsWith('?') ||
143
+ node.reference.startsWith('.') ||
144
+ // Bare references (e.g. `foo`) aren't absolute — resolve() throws on them
145
+ !node.reference.startsWith('/')
146
+ ) {
147
+ return `<a href={'${escape_js_string(node.reference)}'}>${children_markup}</a>`;
148
+ }
149
+ imports.set('resolve', {path: '$app/paths', kind: 'named'});
150
+ return `<a href={resolve('${escape_js_string(node.reference)}')}>${children_markup}</a>`;
151
+ }
152
+ // External link — matches MdzNodeView: target="_blank" rel="noopener"
153
+ return `<a href={'${escape_js_string(node.reference)}'} target="_blank" rel="noopener">${children_markup}</a>`;
154
+ }
155
+
156
+ case 'Paragraph':
157
+ return `<p>${render_nodes(node.children)}</p>`;
158
+
159
+ case 'List': {
160
+ const tag = node.ordered ? 'ol' : 'ul';
161
+ const start_attr =
162
+ node.ordered && node.start_number !== undefined && node.start_number !== 1
163
+ ? ` start={${node.start_number}}`
164
+ : '';
165
+ return `<${tag}${start_attr}>${render_nodes(node.children)}</${tag}>`;
166
+ }
167
+
168
+ case 'ListItem':
169
+ return `<li>${render_nodes(node.children)}</li>`;
170
+
171
+ case 'Blockquote':
172
+ return `<blockquote>${render_nodes(node.children)}</blockquote>`;
173
+
174
+ case 'Hr':
175
+ return '<hr />';
176
+
177
+ case 'Heading': {
178
+ // matches MdzNodeView's `id={node.id || undefined}` — the slugified
179
+ // fragment anchor, omitted when the text has no sluggable characters
180
+ const id_attr = node.id ? ` id={'${escape_js_string(node.id)}'}` : '';
181
+ return `<h${node.level}${id_attr}>${render_nodes(node.children)}</h${node.level}>`;
182
+ }
183
+
184
+ case 'Element': {
185
+ if (elements[node.name] === undefined) {
186
+ has_unconfigured_tags = true;
187
+ return '';
188
+ }
189
+ if (mdz_is_void_element(node.name)) {
190
+ // void elements cannot have content — any parsed children are dropped
191
+ return `<${node.name} />`;
192
+ }
193
+ return `<${node.name}>${render_nodes(node.children)}</${node.name}>`;
194
+ }
195
+
196
+ case 'Component': {
197
+ const import_path = components[node.name];
198
+ if (!import_path) {
199
+ has_unconfigured_tags = true;
200
+ return '';
201
+ }
202
+ imports.set(node.name, {path: import_path, kind: 'default'});
203
+ return `<${node.name}>${render_nodes(node.children)}</${node.name}>`;
204
+ }
205
+
206
+ default:
207
+ throw new UnreachableError(node);
208
+ }
209
+ };
210
+
211
+ const markup = render_nodes(nodes);
212
+ return {markup, imports, has_unconfigured_tags};
213
+ };