@fuzdev/mdz 0.3.0 → 0.5.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 (81) hide show
  1. package/LICENSE +10 -17
  2. package/README.md +6 -3
  3. package/dist/Mdz.svelte +8 -7
  4. package/dist/Mdz.svelte.d.ts.map +1 -1
  5. package/dist/MdzNodeView.svelte +74 -30
  6. package/dist/MdzNodeView.svelte.d.ts +1 -2
  7. package/dist/MdzNodeView.svelte.d.ts.map +1 -1
  8. package/dist/MdzPrecompiled.svelte +3 -3
  9. package/dist/MdzPrecompiled.svelte.d.ts.map +1 -1
  10. package/dist/MdzRoot.svelte +3 -3
  11. package/dist/MdzRoot.svelte.d.ts.map +1 -1
  12. package/dist/MdzStream.svelte +3 -3
  13. package/dist/MdzStream.svelte.d.ts.map +1 -1
  14. package/dist/MdzStreamNodeView.svelte +79 -31
  15. package/dist/MdzStreamNodeView.svelte.d.ts +1 -2
  16. package/dist/MdzStreamNodeView.svelte.d.ts.map +1 -1
  17. package/dist/context_helpers.js +1 -1
  18. package/dist/mdz.d.ts +12 -1
  19. package/dist/mdz.d.ts.map +1 -1
  20. package/dist/mdz.js +1 -1
  21. package/dist/mdz_contexts.d.ts +33 -6
  22. package/dist/mdz_contexts.d.ts.map +1 -1
  23. package/dist/mdz_debug_work.d.ts.map +1 -1
  24. package/dist/mdz_helpers.d.ts +31 -3
  25. package/dist/mdz_helpers.d.ts.map +1 -1
  26. package/dist/mdz_helpers.js +104 -12
  27. package/dist/mdz_lexer.d.ts +3 -1
  28. package/dist/mdz_lexer.d.ts.map +1 -1
  29. package/dist/mdz_lexer.js +115 -31
  30. package/dist/mdz_opcodes.d.ts +3 -1
  31. package/dist/mdz_opcodes.d.ts.map +1 -1
  32. package/dist/mdz_opcodes_to_nodes.d.ts.map +1 -1
  33. package/dist/mdz_opcodes_to_nodes.js +25 -17
  34. package/dist/mdz_stream_parser.d.ts.map +1 -1
  35. package/dist/mdz_stream_parser.js +8 -8
  36. package/dist/mdz_stream_parser_block.js +5 -5
  37. package/dist/mdz_stream_parser_inline.js +4 -4
  38. package/dist/mdz_stream_parser_link.d.ts.map +1 -1
  39. package/dist/mdz_stream_parser_link.js +83 -11
  40. package/dist/mdz_stream_parser_list.d.ts.map +1 -1
  41. package/dist/mdz_stream_parser_list.js +13 -7
  42. package/dist/mdz_stream_parser_state.d.ts.map +1 -1
  43. package/dist/mdz_stream_parser_state.js +7 -7
  44. package/dist/mdz_stream_parser_table.d.ts.map +1 -1
  45. package/dist/mdz_stream_parser_table.js +16 -9
  46. package/dist/mdz_stream_parser_text.js +2 -2
  47. package/dist/mdz_stream_parser_url.js +7 -7
  48. package/dist/mdz_stream_state.svelte.d.ts +3 -1
  49. package/dist/mdz_stream_state.svelte.d.ts.map +1 -1
  50. package/dist/mdz_stream_state.svelte.js +4 -0
  51. package/dist/mdz_to_svelte.d.ts +17 -4
  52. package/dist/mdz_to_svelte.d.ts.map +1 -1
  53. package/dist/mdz_to_svelte.js +22 -9
  54. package/dist/mdz_token_parser.d.ts.map +1 -1
  55. package/dist/mdz_token_parser.js +25 -11
  56. package/dist/svelte_preprocess_mdz.d.ts +6 -3
  57. package/dist/svelte_preprocess_mdz.d.ts.map +1 -1
  58. package/dist/svelte_preprocess_mdz.js +11 -11
  59. package/package.json +25 -40
  60. package/src/lib/context_helpers.ts +2 -2
  61. package/src/lib/mdz.ts +15 -3
  62. package/src/lib/mdz_contexts.ts +41 -11
  63. package/src/lib/mdz_debug_work.ts +1 -1
  64. package/src/lib/mdz_helpers.ts +164 -43
  65. package/src/lib/mdz_lexer.ts +173 -80
  66. package/src/lib/mdz_opcodes.ts +3 -1
  67. package/src/lib/mdz_opcodes_to_nodes.ts +40 -30
  68. package/src/lib/mdz_stream_parser.ts +17 -17
  69. package/src/lib/mdz_stream_parser_block.ts +14 -14
  70. package/src/lib/mdz_stream_parser_inline.ts +18 -18
  71. package/src/lib/mdz_stream_parser_link.ts +88 -18
  72. package/src/lib/mdz_stream_parser_list.ts +31 -25
  73. package/src/lib/mdz_stream_parser_state.ts +27 -27
  74. package/src/lib/mdz_stream_parser_table.ts +38 -31
  75. package/src/lib/mdz_stream_parser_text.ts +3 -3
  76. package/src/lib/mdz_stream_parser_url.ts +11 -11
  77. package/src/lib/mdz_stream_state.svelte.ts +11 -8
  78. package/src/lib/mdz_to_svelte.ts +71 -27
  79. package/src/lib/mdz_token_parser.ts +52 -38
  80. package/src/lib/svelte_preprocess_mdz.ts +66 -60
  81. package/src/lib/tsdoc_mdz.ts +1 -1
@@ -14,16 +14,16 @@
14
14
  * @module
15
15
  */
16
16
 
17
- import {DEV} from 'esm-env';
17
+ import { DEV } from 'esm-env';
18
18
 
19
19
  import {
20
20
  NEWLINE,
21
21
  PIPE,
22
22
  is_line_whitespace,
23
23
  mdz_split_table_row,
24
- mdz_parse_table_delimiter,
24
+ mdz_parse_table_delimiter
25
25
  } from './mdz_helpers.ts';
26
- import {MdzTableCellParser, type MdzNode, type MdzTableAlign} from './mdz.ts';
26
+ import { MdzTableCellParser, type MdzNode, type MdzTableAlign } from './mdz.ts';
27
27
  import {
28
28
  type MdzStreamParserState,
29
29
  type TryResult,
@@ -33,7 +33,7 @@ import {
33
33
  flush_text,
34
34
  offset,
35
35
  pop_stack_entry,
36
- push_stack_entry,
36
+ push_stack_entry
37
37
  } from './mdz_stream_parser_state.ts';
38
38
 
39
39
  /**
@@ -71,12 +71,12 @@ export const try_table_start = (state: MdzStreamParserState, forced: boolean): T
71
71
 
72
72
  const table_id = alloc_id(state);
73
73
  const table_start = offset(state, start);
74
- emit(state, {type: 'open', id: table_id, node_type: 'Table', start: table_start, align});
74
+ emit(state, { type: 'open', id: table_id, node_type: 'Table', start: table_start, align });
75
75
  push_stack_entry(state, table_id, 'Table', table_start);
76
76
  state.table = {
77
77
  id: table_id,
78
78
  item_indent: null,
79
- cell_parser: new MdzTableCellParser(state.buffer),
79
+ cell_parser: new MdzTableCellParser(state.buffer)
80
80
  };
81
81
 
82
82
  emit_table_row(state, header_cells, true, start, header_end);
@@ -152,7 +152,7 @@ export const process_table_line = (state: MdzStreamParserState, forced: boolean)
152
152
  const process_table_line_in_item = (
153
153
  state: MdzStreamParserState,
154
154
  forced: boolean,
155
- item_indent: number,
155
+ item_indent: number
156
156
  ): boolean => {
157
157
  if (state.pos >= state.buffer.length) {
158
158
  if (forced) {
@@ -204,9 +204,9 @@ const process_table_line_in_item = (
204
204
  */
205
205
  export const close_table = (state: MdzStreamParserState): void => {
206
206
  if (!state.table) return;
207
- const {item_indent} = state.table;
207
+ const { item_indent } = state.table;
208
208
  const entry = pop_stack_entry(state);
209
- emit(state, {type: 'close', id: entry.id, end: offset(state)});
209
+ emit(state, { type: 'close', id: entry.id, end: offset(state) });
210
210
  state.table = null;
211
211
  state.active_text_id = null;
212
212
  if (item_indent === null) {
@@ -220,7 +220,7 @@ export const close_table = (state: MdzStreamParserState): void => {
220
220
 
221
221
  /** A recognized in-item table head: the header row's cells + the delimiter alignment. */
222
222
  export interface TableInItemHead {
223
- header_cells: Array<{start: number; end: number}>;
223
+ header_cells: Array<{ start: number; end: number }>;
224
224
  header_end: number;
225
225
  align: Array<MdzTableAlign>;
226
226
  delim_end: number;
@@ -239,9 +239,9 @@ export const match_table_in_item_head = (
239
239
  state: MdzStreamParserState,
240
240
  header_first: number,
241
241
  marker_indent: number,
242
- forced: boolean,
242
+ forced: boolean
243
243
  ): TableInItemHead | 'pending' | null => {
244
- const {buffer} = state;
244
+ const { buffer } = state;
245
245
  const header_end = buffer.indexOf('\n', header_first);
246
246
  if (header_end === -1) return forced ? null : 'pending'; // header needs its newline + a delimiter beneath
247
247
  const header_cells = mdz_split_table_row(buffer, header_first, header_end);
@@ -258,7 +258,7 @@ export const match_table_in_item_head = (
258
258
  }
259
259
  const align = mdz_parse_table_delimiter(buffer, dj, delim_end);
260
260
  if (align === null || align.length !== header_cells.length) return null;
261
- return {header_cells, header_end, align, delim_end};
261
+ return { header_cells, header_end, align, delim_end };
262
262
  };
263
263
 
264
264
  /**
@@ -273,7 +273,7 @@ export const open_table_in_item = (
273
273
  state: MdzStreamParserState,
274
274
  header_first: number,
275
275
  marker_indent: number,
276
- head: TableInItemHead,
276
+ head: TableInItemHead
277
277
  ): void => {
278
278
  const table_id = alloc_id(state);
279
279
  const table_start = offset(state, header_first);
@@ -282,13 +282,13 @@ export const open_table_in_item = (
282
282
  id: table_id,
283
283
  node_type: 'Table',
284
284
  start: table_start,
285
- align: head.align,
285
+ align: head.align
286
286
  });
287
287
  push_stack_entry(state, table_id, 'Table', table_start);
288
288
  state.table = {
289
289
  id: table_id,
290
290
  item_indent: marker_indent,
291
- cell_parser: new MdzTableCellParser(state.buffer),
291
+ cell_parser: new MdzTableCellParser(state.buffer)
292
292
  };
293
293
  emit_table_row(state, head.header_cells, true, header_first, head.header_end);
294
294
  // leave the cursor on the delimiter's newline (or EOF) — the in-item body-row
@@ -301,14 +301,14 @@ export const open_table_in_item = (
301
301
  /** Emit one table row: open, each cell, close. */
302
302
  const emit_table_row = (
303
303
  state: MdzStreamParserState,
304
- cells: Array<{start: number; end: number}>,
304
+ cells: Array<{ start: number; end: number }>,
305
305
  header: boolean,
306
306
  row_start: number,
307
- row_end: number,
307
+ row_end: number
308
308
  ): void => {
309
309
  const row_id = alloc_id(state);
310
310
  const row_off = offset(state, row_start);
311
- emit(state, {type: 'open', id: row_id, node_type: 'TableRow', start: row_off, header});
311
+ emit(state, { type: 'open', id: row_id, node_type: 'TableRow', start: row_off, header });
312
312
  push_stack_entry(state, row_id, 'TableRow', row_off);
313
313
  // one lexer/parser shared across the whole table rather than allocated per
314
314
  // row (or per cell) — rebound per row because the buffer string changes
@@ -319,7 +319,7 @@ const emit_table_row = (
319
319
  emit_table_cell(state, cell_parser, cell.start, cell.end);
320
320
  }
321
321
  pop_stack_entry(state);
322
- emit(state, {type: 'close', id: row_id, end: offset(state, row_end)});
322
+ emit(state, { type: 'close', id: row_id, end: offset(state, row_end) });
323
323
  state.active_text_id = null;
324
324
  };
325
325
 
@@ -331,15 +331,15 @@ const emit_table_cell = (
331
331
  state: MdzStreamParserState,
332
332
  cell_parser: MdzTableCellParser,
333
333
  cell_start: number,
334
- cell_end: number,
334
+ cell_end: number
335
335
  ): void => {
336
336
  const cell_id = alloc_id(state);
337
337
  const cell_off = offset(state, cell_start);
338
- emit(state, {type: 'open', id: cell_id, node_type: 'TableCell', start: cell_off});
338
+ emit(state, { type: 'open', id: cell_id, node_type: 'TableCell', start: cell_off });
339
339
  push_stack_entry(state, cell_id, 'TableCell', cell_off);
340
340
  emit_inline_nodes(state, cell_parser.parse(cell_start, cell_end));
341
341
  pop_stack_entry(state);
342
- emit(state, {type: 'close', id: cell_id, end: offset(state, cell_end)});
342
+ emit(state, { type: 'close', id: cell_id, end: offset(state, cell_end) });
343
343
  state.active_text_id = null;
344
344
  };
345
345
 
@@ -357,7 +357,7 @@ const emit_inline_nodes = (state: MdzStreamParserState, nodes: Array<MdzNode>):
357
357
  content: node.content,
358
358
  text_type: 'Text',
359
359
  start: offset(state, node.start),
360
- end: offset(state, node.end),
360
+ end: offset(state, node.end)
361
361
  });
362
362
  break;
363
363
  case 'Code':
@@ -367,7 +367,7 @@ const emit_inline_nodes = (state: MdzStreamParserState, nodes: Array<MdzNode>):
367
367
  content: node.content,
368
368
  text_type: 'Code',
369
369
  start: offset(state, node.start),
370
- end: offset(state, node.end),
370
+ end: offset(state, node.end)
371
371
  });
372
372
  break;
373
373
  case 'Bold':
@@ -375,17 +375,17 @@ const emit_inline_nodes = (state: MdzStreamParserState, nodes: Array<MdzNode>):
375
375
  case 'Strikethrough': {
376
376
  const id = alloc_id(state);
377
377
  const s = offset(state, node.start);
378
- emit(state, {type: 'open', id, node_type: node.type, start: s});
378
+ emit(state, { type: 'open', id, node_type: node.type, start: s });
379
379
  push_stack_entry(state, id, node.type, s);
380
380
  emit_inline_nodes(state, node.children);
381
381
  pop_stack_entry(state);
382
- emit(state, {type: 'close', id, end: offset(state, node.end)});
382
+ emit(state, { type: 'close', id, end: offset(state, node.end) });
383
383
  break;
384
384
  }
385
385
  case 'Link': {
386
386
  const id = alloc_id(state);
387
387
  const s = offset(state, node.start);
388
- emit(state, {type: 'open', id, node_type: 'Link', start: s});
388
+ emit(state, { type: 'open', id, node_type: 'Link', start: s });
389
389
  push_stack_entry(state, id, 'Link', s);
390
390
  emit_inline_nodes(state, node.children);
391
391
  pop_stack_entry(state);
@@ -394,7 +394,7 @@ const emit_inline_nodes = (state: MdzStreamParserState, nodes: Array<MdzNode>):
394
394
  id,
395
395
  end: offset(state, node.end),
396
396
  reference: node.reference,
397
- link_type: node.link_type,
397
+ link_type: node.link_type
398
398
  });
399
399
  break;
400
400
  }
@@ -402,11 +402,18 @@ const emit_inline_nodes = (state: MdzStreamParserState, nodes: Array<MdzNode>):
402
402
  case 'Component': {
403
403
  const id = alloc_id(state);
404
404
  const s = offset(state, node.start);
405
- emit(state, {type: 'open', id, node_type: node.type, start: s, name: node.name});
405
+ emit(state, {
406
+ type: 'open',
407
+ id,
408
+ node_type: node.type,
409
+ start: s,
410
+ name: node.name,
411
+ attributes: node.attributes
412
+ });
406
413
  push_stack_entry(state, id, node.type, s, '', node.name);
407
414
  emit_inline_nodes(state, node.children);
408
415
  pop_stack_entry(state);
409
- emit(state, {type: 'close', id, end: offset(state, node.end)});
416
+ emit(state, { type: 'close', id, end: offset(state, node.end) });
410
417
  break;
411
418
  }
412
419
  // cells are inline-only (lexed via `lex_table_cell`); a block node here
@@ -15,15 +15,15 @@ import {
15
15
  TEXT_CLASS_URL,
16
16
  TEXT_SCAN_CLASS,
17
17
  is_word_char,
18
- match_url_prefix_case_insensitive,
18
+ match_url_prefix_case_insensitive
19
19
  } from './mdz_helpers.ts';
20
20
  import {
21
21
  type MdzStreamParserState,
22
22
  accumulate_text,
23
23
  ensure_paragraph,
24
- offset,
24
+ offset
25
25
  } from './mdz_stream_parser_state.ts';
26
- import {mdz_debug_work} from './mdz_debug_work.ts';
26
+ import { mdz_debug_work } from './mdz_debug_work.ts';
27
27
 
28
28
  /**
29
29
  * Consume a delimiter at the current position as literal text — the shared
@@ -23,7 +23,7 @@ import {
23
23
  is_valid_path_char,
24
24
  is_word_char,
25
25
  match_url_prefix_case_insensitive,
26
- mdz_trim_url_punctuation,
26
+ mdz_trim_url_punctuation
27
27
  } from './mdz_helpers.ts';
28
28
  import {
29
29
  type MdzStreamParserState,
@@ -33,7 +33,7 @@ import {
33
33
  emit,
34
34
  ensure_paragraph,
35
35
  flush_text,
36
- offset,
36
+ offset
37
37
  } from './mdz_stream_parser_state.ts';
38
38
 
39
39
  const HTTPS_PREFIX = 'https://';
@@ -136,7 +136,7 @@ export const try_auto_path_relative = (state: MdzStreamParserState, forced = fal
136
136
  */
137
137
  export const start_pending_url = (
138
138
  state: MdzStreamParserState,
139
- link_type: 'external' | 'internal',
139
+ link_type: 'external' | 'internal'
140
140
  ): void => {
141
141
  flush_text(state);
142
142
  ensure_paragraph(state);
@@ -147,7 +147,7 @@ export const start_pending_url = (
147
147
  link_type,
148
148
  confirmed: true,
149
149
  viable_https: false,
150
- viable_http: false,
150
+ viable_http: false
151
151
  };
152
152
  // don't advance pos — URL chars start at current position, handled by process_url_content
153
153
  };
@@ -169,7 +169,7 @@ export const start_speculative_url = (state: MdzStreamParserState): void => {
169
169
  link_type: 'external',
170
170
  confirmed: false,
171
171
  viable_https: true,
172
- viable_http: true,
172
+ viable_http: true
173
173
  };
174
174
  };
175
175
 
@@ -313,7 +313,7 @@ export const complete_pending_url = (state: MdzStreamParserState): void => {
313
313
  start: pu.start,
314
314
  end: url_end,
315
315
  trim_end,
316
- trim_id,
316
+ trim_id
317
317
  });
318
318
  state.active_text_id = trim_id;
319
319
  } else {
@@ -325,7 +325,7 @@ export const complete_pending_url = (state: MdzStreamParserState): void => {
325
325
  reference,
326
326
  link_type: pu.link_type,
327
327
  start: pu.start,
328
- end: url_end,
328
+ end: url_end
329
329
  });
330
330
  state.active_text_id = null;
331
331
  }
@@ -339,7 +339,7 @@ export const complete_pending_url = (state: MdzStreamParserState): void => {
339
339
  */
340
340
  export const scan_auto_link_forced = (
341
341
  state: MdzStreamParserState,
342
- link_type: 'external' | 'internal',
342
+ link_type: 'external' | 'internal'
343
343
  ): TryResult => {
344
344
  let i = state.pos;
345
345
  while (i < state.buffer.length) {
@@ -366,7 +366,7 @@ export const scan_auto_link_forced = (
366
366
  ensure_paragraph(state);
367
367
 
368
368
  const link_id = alloc_id(state);
369
- emit(state, {type: 'open', id: link_id, node_type: 'Link', start: link_start});
369
+ emit(state, { type: 'open', id: link_id, node_type: 'Link', start: link_start });
370
370
  const text_id = alloc_id(state);
371
371
  emit(state, {
372
372
  type: 'text',
@@ -374,9 +374,9 @@ export const scan_auto_link_forced = (
374
374
  content: reference,
375
375
  text_type: 'Text',
376
376
  start: link_start,
377
- end: link_end,
377
+ end: link_end
378
378
  });
379
- emit(state, {type: 'close', id: link_id, end: link_end, reference, link_type});
379
+ emit(state, { type: 'close', id: link_id, end: link_end, reference, link_type });
380
380
 
381
381
  state.active_text_id = null;
382
382
  state.column += end_pos - state.pos;
@@ -8,10 +8,10 @@
8
8
  * @module
9
9
  */
10
10
 
11
- import {DEV} from 'esm-env';
11
+ import { DEV } from 'esm-env';
12
12
 
13
- import type {MdzNodeId, MdzNodeType, MdzOpcode} from './mdz_opcodes.ts';
14
- import type {MdzTableAlign} from './mdz.ts';
13
+ import type { MdzNodeId, MdzNodeType, MdzOpcode } from './mdz_opcodes.ts';
14
+ import type { MdzAttribute, MdzTableAlign } from './mdz.ts';
15
15
 
16
16
  /**
17
17
  * A reactive node in the stream renderer tree.
@@ -27,6 +27,8 @@ export class MdzStreamNode {
27
27
  reference?: string = $state();
28
28
  link_type?: 'external' | 'internal' = $state();
29
29
  name?: string;
30
+ /** Parsed tag attributes (Element/Component nodes); known at open, never mutated, so plain not `$state`. */
31
+ attributes?: Array<MdzAttribute>;
30
32
  lang?: string | null;
31
33
  heading_id?: string = $state();
32
34
  ordered?: boolean;
@@ -76,7 +78,7 @@ export class MdzStreamState {
76
78
  const parent = this.#nodes.get(this.#stack[stack_idx - 1]!);
77
79
  if (DEV && !parent) {
78
80
  throw new Error(
79
- `MdzStreamState: parent id ${this.#stack[stack_idx - 1]} on stack but missing from nodes`,
81
+ `MdzStreamState: parent id ${this.#stack[stack_idx - 1]} on stack but missing from nodes`
80
82
  );
81
83
  }
82
84
  return parent ? parent.children : this.root;
@@ -99,12 +101,13 @@ export class MdzStreamState {
99
101
  case 'open': {
100
102
  if (DEV && this.#nodes.has(opcode.id)) {
101
103
  throw new Error(
102
- `MdzStreamState: duplicate open for id ${opcode.id} (node_type '${opcode.node_type}')`,
104
+ `MdzStreamState: duplicate open for id ${opcode.id} (node_type '${opcode.node_type}')`
103
105
  );
104
106
  }
105
107
  const node = new MdzStreamNode(opcode.id, opcode.node_type);
106
108
  if (opcode.level !== undefined) node.level = opcode.level;
107
109
  if (opcode.name !== undefined) node.name = opcode.name;
110
+ if (opcode.attributes !== undefined) node.attributes = opcode.attributes;
108
111
  if (opcode.lang !== undefined) node.lang = opcode.lang;
109
112
  if (opcode.ordered !== undefined) node.ordered = opcode.ordered;
110
113
  if (opcode.start_number !== undefined) node.start_number = opcode.start_number;
@@ -168,7 +171,7 @@ export class MdzStreamState {
168
171
  case 'text': {
169
172
  if (DEV && this.#nodes.has(opcode.id)) {
170
173
  throw new Error(
171
- `MdzStreamState: duplicate text for id ${opcode.id} (text_type '${opcode.text_type}')`,
174
+ `MdzStreamState: duplicate text for id ${opcode.id} (text_type '${opcode.text_type}')`
172
175
  );
173
176
  }
174
177
  const node = new MdzStreamNode(opcode.id, opcode.text_type);
@@ -215,7 +218,7 @@ export class MdzStreamState {
215
218
  case 'void': {
216
219
  if (DEV && this.#nodes.has(opcode.id)) {
217
220
  throw new Error(
218
- `MdzStreamState: duplicate void for id ${opcode.id} (node_type '${opcode.node_type}')`,
221
+ `MdzStreamState: duplicate void for id ${opcode.id} (node_type '${opcode.node_type}')`
219
222
  );
220
223
  }
221
224
  const node = new MdzStreamNode(opcode.id, opcode.node_type);
@@ -255,7 +258,7 @@ export class MdzStreamState {
255
258
  if (node_idx === -1) {
256
259
  if (DEV) {
257
260
  throw new Error(
258
- `MdzStreamState: revert for id ${opcode.id} but node not found in parent's children`,
261
+ `MdzStreamState: revert for id ${opcode.id} but node not found in parent's children`
259
262
  );
260
263
  }
261
264
  break;
@@ -8,12 +8,39 @@
8
8
  * @module
9
9
  */
10
10
 
11
- import {UnreachableError} from '@fuzdev/fuz_util/error.ts';
12
- import {escape_svelte_text} from '@fuzdev/fuz_util/svelte_preprocess_helpers.ts';
13
- import {escape_js_string} from '@fuzdev/fuz_util/string.ts';
14
-
15
- import type {MdzNode, MdzNodeTableRow} from './mdz.ts';
16
- import {mdz_is_void_element, mdz_classify_link} from './mdz_helpers.ts';
11
+ import { UnreachableError } from '@fuzdev/fuz_util/error.ts';
12
+ import { escape_svelte_text } from '@fuzdev/fuz_util/svelte_preprocess_helpers.ts';
13
+ import { escape_js_string } from '@fuzdev/fuz_util/string.ts';
14
+
15
+ import type { MdzNode, MdzNodeTableRow } from './mdz.ts';
16
+ import type { MdzComponentProps } from './mdz_contexts.ts';
17
+ import {
18
+ mdz_is_void_element,
19
+ mdz_classify_link,
20
+ mdz_filter_element_attributes,
21
+ mdz_filter_component_attributes,
22
+ ALLOWED_ELEMENT_ATTRIBUTES
23
+ } from './mdz_helpers.ts';
24
+
25
+ // emits parsed attributes as Svelte markup, matching the file's `name={'...'}`
26
+ // idiom for string attrs. Booleans emit as `name={true}`, not bare — a bare
27
+ // attribute compiles to `name=""`, while the runtime views spread `true`, which
28
+ // Svelte renders per attribute (`aria-hidden="true"`); the expression form
29
+ // keeps precompiled and runtime output identical. Each entry gets a leading
30
+ // space, so the result splices straight after the tag name.
31
+ /**
32
+ * A build-time component registry entry: an import path, or an import path
33
+ * plus the attribute names passed as props (see `MdzComponentProps`).
34
+ */
35
+ export type MdzComponentImport = string | { path: string; props?: MdzComponentProps };
36
+
37
+ const format_attributes = (entries: Array<[string, string | true]>): string => {
38
+ let out = '';
39
+ for (const [name, value] of entries) {
40
+ out += value === true ? ` ${name}={true}` : ` ${name}={'${escape_js_string(value)}'}`;
41
+ }
42
+ return out;
43
+ };
17
44
 
18
45
  /**
19
46
  * Result of converting `MdzNode` arrays to Svelte markup.
@@ -23,7 +50,7 @@ export interface MdzToSvelteResult {
23
50
  markup: string;
24
51
 
25
52
  /** Required imports: `Map<local_name, {path, kind}>`. */
26
- imports: Map<string, {path: string; kind: 'default' | 'named'}>;
53
+ imports: Map<string, { path: string; kind: 'default' | 'named' }>;
27
54
 
28
55
  /** Whether content references unconfigured Component or Element tags. */
29
56
  has_unconfigured_tags: boolean;
@@ -38,10 +65,13 @@ const component_local_name = (import_path: string): string =>
38
65
  */
39
66
  export interface MdzToSvelteOptions {
40
67
  /**
41
- * Component name to import path mapping (e.g., `{Alert: '$lib/Alert.svelte'}`).
68
+ * Component name to import mapping, mirroring the runtime `MdzComponents`
69
+ * registry: a bare import path receives no attributes
70
+ * (`{Note: '#lib/Note.svelte'}`), and the config form names the attributes
71
+ * passed as props (`{Alert: {path: '#lib/Alert.svelte', props: ['status']}}`).
42
72
  * If content references a component not in this map, `has_unconfigured_tags` is set.
43
73
  */
44
- components: Record<string, string>;
74
+ components: Record<string, MdzComponentImport>;
45
75
 
46
76
  /**
47
77
  * Allowed HTML elements (e.g., `{aside: true, details: true}`).
@@ -53,8 +83,9 @@ export interface MdzToSvelteOptions {
53
83
 
54
84
  /**
55
85
  * 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.
86
+ * When provided, relative references (`./`, `../`) are resolved against it and,
87
+ * like absolute references, passed through `resolve()` in its pathname form.
88
+ * Trailing slash recommended.
58
89
  */
59
90
  base?: string;
60
91
 
@@ -85,10 +116,10 @@ export interface MdzToSvelteOptions {
85
116
  */
86
117
  export const mdz_to_svelte = (
87
118
  nodes: Array<MdzNode>,
88
- options: MdzToSvelteOptions,
119
+ options: MdzToSvelteOptions
89
120
  ): 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();
121
+ const { components, elements, base, code_component_import, codeblock_component_import } = options;
122
+ const imports: Map<string, { path: string; kind: 'default' | 'named' }> = new Map();
92
123
  let has_unconfigured_tags = false;
93
124
 
94
125
  const render_nodes = (children: Array<MdzNode>): string => {
@@ -105,7 +136,7 @@ export const mdz_to_svelte = (
105
136
  return `<code>${escape_svelte_text(node.content)}</code>`;
106
137
  }
107
138
  const name = component_local_name(code_component_import);
108
- imports.set(name, {path: code_component_import, kind: 'default'});
139
+ imports.set(name, { path: code_component_import, kind: 'default' });
109
140
  return `<${name} reference={'${escape_js_string(node.content)}'} />`;
110
141
  }
111
142
 
@@ -116,7 +147,7 @@ export const mdz_to_svelte = (
116
147
  return `<pre><code>${escape_svelte_text(node.content)}</code></pre>`;
117
148
  }
118
149
  const name = component_local_name(codeblock_component_import);
119
- imports.set(name, {path: codeblock_component_import, kind: 'default'});
150
+ imports.set(name, { path: codeblock_component_import, kind: 'default' });
120
151
  return `<${name} ${lang_attr} content={'${escape_js_string(node.content)}'} />`;
121
152
  }
122
153
 
@@ -139,8 +170,8 @@ export const mdz_to_svelte = (
139
170
  const link = mdz_classify_link(node.reference, node.link_type, base);
140
171
  if (link.kind === 'unsafe') return children_markup;
141
172
  if (link.kind === 'resolve') {
142
- imports.set('resolve', {path: '$app/paths', kind: 'named'});
143
- return `<a href={resolve('${escape_js_string(link.href)}')}>${children_markup}</a>`;
173
+ imports.set('resolve', { path: '$app/paths', kind: 'named' });
174
+ return `<a href={resolve('${escape_js_string(link.path)}')}>${children_markup}</a>`;
144
175
  }
145
176
  if (link.kind === 'external') {
146
177
  return `<a href={'${escape_js_string(link.href)}'} target="_blank" rel="noopener">${children_markup}</a>`;
@@ -167,7 +198,7 @@ export const mdz_to_svelte = (
167
198
  return `<blockquote>${render_nodes(node.children)}</blockquote>`;
168
199
 
169
200
  case 'Table': {
170
- const {align} = node;
201
+ const { align } = node;
171
202
  const render_row = (row: MdzNodeTableRow, tag: 'th' | 'td'): string => {
172
203
  let cells = '';
173
204
  // normalize to the column count: pad short rows, ignore extra cells
@@ -208,25 +239,38 @@ export const mdz_to_svelte = (
208
239
  }
209
240
 
210
241
  case 'Element': {
211
- if (elements[node.name] === undefined) {
242
+ if (elements[node.name] !== true) {
212
243
  has_unconfigured_tags = true;
213
244
  return '';
214
245
  }
246
+ // filter via the SAME helper the node views use, so precompiled and
247
+ // runtime output stay identical (its DEV-warn fires at build time too)
248
+ const attrs_str = format_attributes(
249
+ Object.entries(
250
+ mdz_filter_element_attributes(node.name, node.attributes, ALLOWED_ELEMENT_ATTRIBUTES)
251
+ )
252
+ );
215
253
  if (mdz_is_void_element(node.name)) {
216
254
  // void elements cannot have content — any parsed children are dropped
217
- return `<${node.name} />`;
255
+ return `<${node.name}${attrs_str} />`;
218
256
  }
219
- return `<${node.name}>${render_nodes(node.children)}</${node.name}>`;
257
+ return `<${node.name}${attrs_str}>${render_nodes(node.children)}</${node.name}>`;
220
258
  }
221
259
 
222
260
  case 'Component': {
223
- const import_path = components[node.name];
224
- if (!import_path) {
261
+ const entry = components[node.name];
262
+ if (!entry) {
225
263
  has_unconfigured_tags = true;
226
264
  return '';
227
265
  }
228
- imports.set(node.name, {path: import_path, kind: 'default'});
229
- return `<${node.name}>${render_nodes(node.children)}</${node.name}>`;
266
+ const { path, props } =
267
+ typeof entry === 'string' ? { path: entry, props: undefined } : entry;
268
+ imports.set(node.name, { path, kind: 'default' });
269
+ // filter via the SAME helper the node views use
270
+ const attrs_str = format_attributes(
271
+ Object.entries(mdz_filter_component_attributes(node.name, node.attributes, props))
272
+ );
273
+ return `<${node.name}${attrs_str}>${render_nodes(node.children)}</${node.name}>`;
230
274
  }
231
275
 
232
276
  default:
@@ -235,5 +279,5 @@ export const mdz_to_svelte = (
235
279
  };
236
280
 
237
281
  const markup = render_nodes(nodes);
238
- return {markup, imports, has_unconfigured_tags};
282
+ return { markup, imports, has_unconfigured_tags };
239
283
  };