@fuzdev/mdz 0.4.0 → 0.6.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 (47) hide show
  1. package/LICENSE +10 -17
  2. package/README.md +2 -3
  3. package/dist/MdzNodeView.svelte +59 -23
  4. package/dist/MdzNodeView.svelte.d.ts.map +1 -1
  5. package/dist/MdzStreamNodeView.svelte +63 -23
  6. package/dist/MdzStreamNodeView.svelte.d.ts.map +1 -1
  7. package/dist/mdz.d.ts +12 -1
  8. package/dist/mdz.d.ts.map +1 -1
  9. package/dist/mdz.js +1 -1
  10. package/dist/mdz_contexts.d.ts +33 -6
  11. package/dist/mdz_contexts.d.ts.map +1 -1
  12. package/dist/mdz_helpers.d.ts +31 -3
  13. package/dist/mdz_helpers.d.ts.map +1 -1
  14. package/dist/mdz_helpers.js +102 -10
  15. package/dist/mdz_lexer.d.ts +3 -1
  16. package/dist/mdz_lexer.d.ts.map +1 -1
  17. package/dist/mdz_lexer.js +89 -15
  18. package/dist/mdz_opcodes.d.ts +3 -1
  19. package/dist/mdz_opcodes.d.ts.map +1 -1
  20. package/dist/mdz_opcodes_to_nodes.d.ts.map +1 -1
  21. package/dist/mdz_opcodes_to_nodes.js +3 -0
  22. package/dist/mdz_stream_parser_link.d.ts.map +1 -1
  23. package/dist/mdz_stream_parser_link.js +81 -9
  24. package/dist/mdz_stream_parser_table.js +8 -1
  25. package/dist/mdz_stream_state.svelte.d.ts +3 -1
  26. package/dist/mdz_stream_state.svelte.d.ts.map +1 -1
  27. package/dist/mdz_stream_state.svelte.js +4 -0
  28. package/dist/mdz_to_svelte.d.ts +17 -4
  29. package/dist/mdz_to_svelte.d.ts.map +1 -1
  30. package/dist/mdz_to_svelte.js +22 -9
  31. package/dist/mdz_token_parser.d.ts.map +1 -1
  32. package/dist/mdz_token_parser.js +16 -2
  33. package/dist/svelte_preprocess_mdz.d.ts +5 -2
  34. package/dist/svelte_preprocess_mdz.d.ts.map +1 -1
  35. package/package.json +11 -10
  36. package/src/lib/mdz.ts +13 -1
  37. package/src/lib/mdz_contexts.ts +36 -6
  38. package/src/lib/mdz_helpers.ts +129 -14
  39. package/src/lib/mdz_lexer.ts +98 -15
  40. package/src/lib/mdz_opcodes.ts +3 -1
  41. package/src/lib/mdz_opcodes_to_nodes.ts +5 -0
  42. package/src/lib/mdz_stream_parser_link.ts +77 -7
  43. package/src/lib/mdz_stream_parser_table.ts +8 -1
  44. package/src/lib/mdz_stream_state.svelte.ts +4 -1
  45. package/src/lib/mdz_to_svelte.ts +57 -13
  46. package/src/lib/mdz_token_parser.ts +16 -2
  47. package/src/lib/svelte_preprocess_mdz.ts +6 -4
@@ -26,6 +26,9 @@ import {
26
26
  LEFT_ANGLE,
27
27
  RIGHT_ANGLE,
28
28
  SLASH,
29
+ EQUALS,
30
+ APOSTROPHE,
31
+ DOUBLE_QUOTE,
29
32
  PERIOD,
30
33
  LEFT_BRACKET,
31
34
  LEFT_PAREN,
@@ -58,7 +61,7 @@ import {
58
61
  TEXT_SCAN_CLASS
59
62
  } from './mdz_helpers.ts';
60
63
 
61
- import type { MdzTableAlign } from './mdz.ts';
64
+ import type { MdzAttribute, MdzTableAlign } from './mdz.ts';
62
65
  import { mdz_debug_work } from './mdz_debug_work.ts';
63
66
 
64
67
  //
@@ -177,12 +180,14 @@ export interface MdzTokenTagOpen extends MdzTokenBase {
177
180
  type: 'tag_open';
178
181
  name: string;
179
182
  is_component: boolean;
183
+ attributes: Array<MdzAttribute>;
180
184
  }
181
185
 
182
186
  export interface MdzTokenTagSelfClose extends MdzTokenBase {
183
187
  type: 'tag_self_close';
184
188
  name: string;
185
189
  is_component: boolean;
190
+ attributes: Array<MdzAttribute>;
186
191
  }
187
192
 
188
193
  export interface MdzTokenTagClose extends MdzTokenBase {
@@ -1794,9 +1799,78 @@ export class MdzLexer {
1794
1799
  const first_char_code = tag_name.charCodeAt(0);
1795
1800
  const is_component = first_char_code >= A_UPPER && first_char_code <= Z_UPPER;
1796
1801
 
1797
- // Skip whitespace
1798
- while (this.#index < this.#text.length && this.#text.charCodeAt(this.#index) === SPACE) {
1799
- this.#index++;
1802
+ // Attribute loop: consume space-separated attributes until `/>` or `>`.
1803
+ // Registry-blind — every syntactically valid attribute is parsed here;
1804
+ // policy (allowlist filtering) happens at render/build time. The scan is
1805
+ // a single forward pass bounded by the line: any newline (or a missing
1806
+ // separator, bad value, or duplicate name) rejects the whole tag to
1807
+ // literal text via the shared `#reject_tag` bailout.
1808
+ const attributes: Array<MdzAttribute> = [];
1809
+ const seen = new Set<string>(); // O(1) duplicate-name detection
1810
+ for (;;) {
1811
+ // skip a run of literal spaces only (tabs/newlines are not SPACE, so
1812
+ // they fall through to the reject path — the single-line, space-only rule)
1813
+ let had_space = false;
1814
+ while (this.#index < this.#text.length && this.#text.charCodeAt(this.#index) === SPACE) {
1815
+ this.#index++;
1816
+ had_space = true;
1817
+ }
1818
+ const c = this.#index < this.#text.length ? this.#text.charCodeAt(this.#index) : -1;
1819
+ // `/>` or `>` ends the open tag — break to the existing self-close / commit
1820
+ if (c === SLASH || c === RIGHT_ANGLE) break;
1821
+ // otherwise an attribute name must follow, space-separated from what
1822
+ // precedes it (`<a x="1"y="2">` rejects — `y` abuts the closing quote)
1823
+ if (c === -1 || !had_space || !is_letter(c)) {
1824
+ this.#reject_tag(start, bound);
1825
+ return;
1826
+ }
1827
+ // collect the attribute name (same grammar as tag names)
1828
+ const attr_start = this.#index;
1829
+ this.#index++; // first char already passed is_letter
1830
+ while (
1831
+ this.#index < this.#text.length &&
1832
+ is_tag_name_char(this.#text.charCodeAt(this.#index))
1833
+ ) {
1834
+ this.#index++;
1835
+ }
1836
+ const name = this.#text.slice(attr_start, this.#index);
1837
+ // duplicate names are ambiguous (Svelte compile-errors) → literal tag
1838
+ if (seen.has(name)) {
1839
+ this.#reject_tag(start, bound);
1840
+ return;
1841
+ }
1842
+ seen.add(name);
1843
+ let value: string | true = true; // bare name is boolean `true`
1844
+ if (this.#index < this.#text.length && this.#text.charCodeAt(this.#index) === EQUALS) {
1845
+ this.#index++; // consume =
1846
+ // the value must be immediately quoted — no space after `=`, no
1847
+ // `{` brace literal, no unquoted value (this one check covers all)
1848
+ const quote = this.#index < this.#text.length ? this.#text.charCodeAt(this.#index) : -1;
1849
+ if (quote !== DOUBLE_QUOTE && quote !== APOSTROPHE) {
1850
+ this.#reject_tag(start, bound);
1851
+ return;
1852
+ }
1853
+ this.#index++; // consume opening quote
1854
+ const value_start = this.#index;
1855
+ // scan to the matching quote, bounded by newline / text end — NOT
1856
+ // by `#max_search_index` (the open-tag scan never is); a `>` inside
1857
+ // the value is content, consumed atomically before `>` is sought
1858
+ while (
1859
+ this.#index < this.#text.length &&
1860
+ this.#text.charCodeAt(this.#index) !== quote &&
1861
+ this.#text.charCodeAt(this.#index) !== NEWLINE
1862
+ ) {
1863
+ this.#index++;
1864
+ }
1865
+ if (this.#index >= this.#text.length || this.#text.charCodeAt(this.#index) === NEWLINE) {
1866
+ // unterminated quote (or newline inside the value) → literal tag
1867
+ this.#reject_tag(start, bound);
1868
+ return;
1869
+ }
1870
+ value = this.#text.slice(value_start, this.#index);
1871
+ this.#index++; // consume closing quote
1872
+ }
1873
+ attributes.push({ name, value, start: attr_start, end: this.#index });
1800
1874
  }
1801
1875
 
1802
1876
  // Check for self-closing />
@@ -1810,6 +1884,7 @@ export class MdzLexer {
1810
1884
  type: 'tag_self_close',
1811
1885
  name: tag_name,
1812
1886
  is_component,
1887
+ attributes,
1813
1888
  start,
1814
1889
  end: this.#index
1815
1890
  });
@@ -1818,9 +1893,7 @@ export class MdzLexer {
1818
1893
 
1819
1894
  // Check for >
1820
1895
  if (this.#index >= this.#text.length || this.#text.charCodeAt(this.#index) !== RIGHT_ANGLE) {
1821
- this.#failed_tag.add(`${start}:${bound}`);
1822
- this.#index = start + 1;
1823
- this.#emit_text('<', start);
1896
+ this.#reject_tag(start, bound);
1824
1897
  return;
1825
1898
  }
1826
1899
  this.#index++; // consume >
@@ -1837,21 +1910,24 @@ export class MdzLexer {
1837
1910
  const search_limit = Math.min(this.#max_search_index, this.#text.length);
1838
1911
  const closing_tag_pos = this.#index_of(closing_tag, this.#index);
1839
1912
  if (closing_tag_pos === -1 || closing_tag_pos > search_limit) {
1840
- this.#failed_tag.add(`${start}:${bound}`);
1841
- this.#index = start + 1;
1842
- this.#emit_text('<', start);
1913
+ this.#reject_tag(start, bound);
1843
1914
  return;
1844
1915
  }
1845
1916
  if (this.#has_paragraph_break_between(this.#index, closing_tag_pos)) {
1846
- this.#failed_tag.add(`${start}:${bound}`);
1847
- this.#index = start + 1;
1848
- this.#emit_text('<', start);
1917
+ this.#reject_tag(start, bound);
1849
1918
  return;
1850
1919
  }
1851
1920
 
1852
1921
  // Emit tag_open
1853
1922
  const open_token_index = this.#tokens.length;
1854
- this.#tokens.push({ type: 'tag_open', name: tag_name, is_component, start, end: this.#index });
1923
+ this.#tokens.push({
1924
+ type: 'tag_open',
1925
+ name: tag_name,
1926
+ is_component,
1927
+ attributes,
1928
+ start,
1929
+ end: this.#index
1930
+ });
1855
1931
 
1856
1932
  // Tokenize children until the closing tag. Each iteration narrows
1857
1933
  // `#max_search_index` to the next closer so a child's delimiter scan
@@ -1892,8 +1968,15 @@ export class MdzLexer {
1892
1968
  // (the `#revert_tokens_from_link_open` pattern; the old fallthrough
1893
1969
  // left a dangling `tag_open` whose parser fallback discarded content)
1894
1970
  this.#max_search_index = saved_max;
1895
- this.#failed_tag.add(`${start}:${bound}`);
1896
1971
  this.#tokens.splice(open_token_index);
1972
+ this.#reject_tag(start, bound);
1973
+ }
1974
+
1975
+ // a tag open at `start` that can't commit: memoizes the failure for this
1976
+ // `bound` so re-entries bail fast, then emits the `<` as text and resumes
1977
+ // scanning after it — the rest re-parses as ordinary content
1978
+ #reject_tag(start: number, bound: number): void {
1979
+ this.#failed_tag.add(`${start}:${bound}`);
1897
1980
  this.#index = start + 1;
1898
1981
  this.#emit_text('<', start);
1899
1982
  }
@@ -12,7 +12,7 @@
12
12
  * @module
13
13
  */
14
14
 
15
- import type { MdzTableAlign } from './mdz.ts';
15
+ import type { MdzAttribute, MdzTableAlign } from './mdz.ts';
16
16
 
17
17
  /**
18
18
  * Unique monotonic identifier for each node created by the parser.
@@ -66,6 +66,8 @@ export interface MdzOpcodeOpen {
66
66
  level?: 1 | 2 | 3 | 4 | 5 | 6;
67
67
  /** Tag name. Present when `node_type` is `'Element'` or `'Component'`. */
68
68
  name?: string;
69
+ /** Parsed attributes. Present (possibly empty) when `node_type` is `'Element'` or `'Component'`. */
70
+ attributes?: Array<MdzAttribute>;
69
71
  /** Language hint. Present when `node_type` is `'Codeblock'`. */
70
72
  lang?: string | null;
71
73
  /** Whether the list is ordered. Present when `node_type` is `'List'`. */
@@ -20,6 +20,7 @@
20
20
  import { DEV } from 'esm-env';
21
21
 
22
22
  import type {
23
+ MdzAttribute,
23
24
  MdzNode,
24
25
  MdzNodeText,
25
26
  MdzNodeLink,
@@ -42,6 +43,7 @@ interface StackFrame {
42
43
  // metadata from open opcode
43
44
  level?: number;
44
45
  name?: string;
46
+ attributes?: Array<MdzAttribute>;
45
47
  lang?: string | null;
46
48
  ordered?: boolean;
47
49
  start_number?: number;
@@ -77,6 +79,7 @@ export const mdz_opcodes_to_nodes = (opcodes: Array<MdzOpcode>): Array<MdzNode>
77
79
  start: op.start,
78
80
  level: op.level,
79
81
  name: op.name,
82
+ attributes: op.attributes,
80
83
  lang: op.lang,
81
84
  ordered: op.ordered,
82
85
  start_number: op.start_number,
@@ -489,6 +492,7 @@ const build_node = (frame: StackFrame, frame_children: Array<MdzNode>): MdzNode
489
492
  return {
490
493
  type: 'Element',
491
494
  name: frame.name ?? '',
495
+ attributes: frame.attributes ?? [],
492
496
  children: mdz_merge_adjacent_text(frame_children),
493
497
  start: frame.start,
494
498
  end: frame.end!
@@ -498,6 +502,7 @@ const build_node = (frame: StackFrame, frame_children: Array<MdzNode>): MdzNode
498
502
  return {
499
503
  type: 'Component',
500
504
  name: frame.name ?? '',
505
+ attributes: frame.attributes ?? [],
501
506
  children: mdz_merge_adjacent_text(frame_children),
502
507
  start: frame.start,
503
508
  end: frame.end!
@@ -6,8 +6,12 @@
6
6
  */
7
7
 
8
8
  import type { MdzNodeTypeContainer } from './mdz_opcodes.ts';
9
+ import type { MdzAttribute } from './mdz.ts';
9
10
  import {
10
11
  A_UPPER,
12
+ APOSTROPHE,
13
+ DOUBLE_QUOTE,
14
+ EQUALS,
11
15
  LEFT_BRACKET,
12
16
  LEFT_PAREN,
13
17
  MAX_INLINE_NESTING_DEPTH,
@@ -211,11 +215,77 @@ export const try_tag_open = (state: MdzStreamParserState): TryResult => {
211
215
 
212
216
  const name = state.buffer.slice(name_start, i);
213
217
 
214
- // skip whitespace
215
- while (i < state.buffer.length && state.buffer.charCodeAt(i) === SPACE) {
216
- i++;
218
+ // Attribute loop: consume space-separated attributes until `/>` or `>`.
219
+ // This widens the held-tag-open rescan surface (the TODO above) with the
220
+ // attribute/value text — still line-bounded, since a newline anywhere aborts
221
+ // the whole tag (`not_match`). Every buffer-end point holds (`need_more`) so
222
+ // the same tree is produced regardless of where feeds split. The quoted-value
223
+ // terminator search reuses `buffer_index_of` (memoized, append-aware) so a
224
+ // long streamed value doesn't rescan quadratically across held retries,
225
+ // matching `try_complete_link`.
226
+ const attributes: Array<MdzAttribute> = [];
227
+ const seen = new Set<string>();
228
+ for (;;) {
229
+ // skip a run of literal spaces only (tabs/newlines are not SPACE)
230
+ let had_space = false;
231
+ while (i < state.buffer.length && state.buffer.charCodeAt(i) === SPACE) {
232
+ i++;
233
+ had_space = true;
234
+ }
235
+ if (i >= state.buffer.length) return 'need_more';
236
+
237
+ const c = state.buffer.charCodeAt(i);
238
+ // `/>` or `>` ends the open tag — break to the existing self-close / commit
239
+ if (c === SLASH || c === RIGHT_ANGLE) break;
240
+ // otherwise an attribute name must follow, space-separated from what
241
+ // precedes it (`<a x="1"y="2">` — `y` abuts the closing quote — reverts)
242
+ if (!is_letter(c)) return 'not_match';
243
+ if (!had_space) return 'not_match';
244
+
245
+ // collect the attribute name (same grammar as tag names)
246
+ const attr_start = i;
247
+ i++; // first char already passed is_letter
248
+ while (i < state.buffer.length && is_tag_name_char(state.buffer.charCodeAt(i))) {
249
+ i++;
250
+ }
251
+ if (i >= state.buffer.length) return 'need_more'; // mid-name — could still grow
252
+ const attr_name = state.buffer.slice(attr_start, i);
253
+ // duplicate names are ambiguous (Svelte compile-errors) → literal tag
254
+ if (seen.has(attr_name)) return 'not_match';
255
+ seen.add(attr_name);
256
+
257
+ let value: string | true = true; // bare name is boolean `true`
258
+ if (state.buffer.charCodeAt(i) === EQUALS) {
259
+ i++; // consume =
260
+ if (i >= state.buffer.length) return 'need_more'; // nothing after `=` yet
261
+ // the value must be immediately quoted — no space, no `{`, no unquoted
262
+ const quote_code = state.buffer.charCodeAt(i);
263
+ if (quote_code !== DOUBLE_QUOTE && quote_code !== APOSTROPHE) return 'not_match';
264
+ i++; // consume opening quote
265
+ const value_start = i;
266
+ // find the matching quote and the next newline via the memoized,
267
+ // append-aware search; a `>` inside the value is content, consumed
268
+ // atomically here so a `</Name>`-shaped substring never reaches
269
+ // `try_close_tag`
270
+ const quote_str = quote_code === DOUBLE_QUOTE ? '"' : "'";
271
+ const q = buffer_index_of(state, quote_str, value_start);
272
+ const n = buffer_index_of(state, '\n', value_start);
273
+ if (q !== -1 && (n === -1 || q < n)) {
274
+ value = state.buffer.slice(value_start, q);
275
+ i = q + 1; // past the closing quote
276
+ } else if (n !== -1 && (q === -1 || n < q)) {
277
+ return 'not_match'; // newline inside the value → literal tag
278
+ } else {
279
+ return 'need_more'; // neither found yet — unterminated so far, hold
280
+ }
281
+ }
282
+ attributes.push({
283
+ name: attr_name,
284
+ value,
285
+ start: offset(state, attr_start),
286
+ end: offset(state, i)
287
+ });
217
288
  }
218
- if (i >= state.buffer.length) return 'need_more';
219
289
 
220
290
  // check for self-closing />
221
291
  if (state.buffer.charCodeAt(i) === SLASH && i + 1 >= state.buffer.length) {
@@ -230,7 +300,7 @@ export const try_tag_open = (state: MdzStreamParserState): TryResult => {
230
300
  ensure_paragraph(state);
231
301
  const id = alloc_id(state);
232
302
  const tag_start = offset(state, start);
233
- emit(state, { type: 'open', id, node_type, start: tag_start, name });
303
+ emit(state, { type: 'open', id, node_type, start: tag_start, name, attributes });
234
304
  emit(state, { type: 'close', id, end: offset(state, i + 2) });
235
305
  state.active_text_id = null;
236
306
  state.pos = i + 2;
@@ -253,8 +323,8 @@ export const try_tag_open = (state: MdzStreamParserState): TryResult => {
253
323
  ensure_paragraph(state);
254
324
  const id = alloc_id(state);
255
325
  const tag_start = offset(state, start);
256
- const delimiter = state.buffer.slice(start, i); // e.g., "<Alert>"
257
- emit(state, { type: 'open', id, node_type, start: tag_start, name });
326
+ const delimiter = state.buffer.slice(start, i); // e.g., "<Alert>" or "<Alert x="1">"
327
+ emit(state, { type: 'open', id, node_type, start: tag_start, name, attributes });
258
328
  push_stack_entry(state, id, node_type, tag_start, delimiter, name);
259
329
  state.active_text_id = null;
260
330
  state.pos = i;
@@ -402,7 +402,14 @@ 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);
@@ -11,7 +11,7 @@
11
11
  import { DEV } from 'esm-env';
12
12
 
13
13
  import type { MdzNodeId, MdzNodeType, MdzOpcode } from './mdz_opcodes.ts';
14
- import type { MdzTableAlign } from './mdz.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;
@@ -105,6 +107,7 @@ export class MdzStreamState {
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;
@@ -13,7 +13,34 @@ import { escape_svelte_text } from '@fuzdev/fuz_util/svelte_preprocess_helpers.t
13
13
  import { escape_js_string } from '@fuzdev/fuz_util/string.ts';
14
14
 
15
15
  import type { MdzNode, MdzNodeTableRow } from './mdz.ts';
16
- import { mdz_is_void_element, mdz_classify_link } from './mdz_helpers.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.
@@ -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
 
@@ -140,7 +171,7 @@ export const mdz_to_svelte = (
140
171
  if (link.kind === 'unsafe') return children_markup;
141
172
  if (link.kind === 'resolve') {
142
173
  imports.set('resolve', { path: '$app/paths', kind: 'named' });
143
- return `<a href={resolve('${escape_js_string(link.href)}')}>${children_markup}</a>`;
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>`;
@@ -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:
@@ -581,7 +581,14 @@ export class MdzTokenParser {
581
581
  const t = this.#tokens[this.#index]!;
582
582
  if (t.type === 'tag_close' && t.name === tag_name) {
583
583
  this.#index++;
584
- return { type: node_type, name: tag_name, children, start, end: t.end };
584
+ return {
585
+ type: node_type,
586
+ name: tag_name,
587
+ attributes: open_token.attributes,
588
+ children,
589
+ start,
590
+ end: t.end
591
+ };
585
592
  }
586
593
  const node = this.#parse_inline();
587
594
  if (node) mdz_push_merging_text(children, node);
@@ -595,7 +602,14 @@ export class MdzTokenParser {
595
602
  const token = this.#tokens[this.#index]! as MdzTokenTagSelfClose;
596
603
  const node_type: 'Component' | 'Element' = token.is_component ? 'Component' : 'Element';
597
604
  this.#index++;
598
- return { type: node_type, name: token.name, children: [], start: token.start, end: token.end };
605
+ return {
606
+ type: node_type,
607
+ name: token.name,
608
+ attributes: token.attributes,
609
+ children: [],
610
+ start: token.start,
611
+ end: token.end
612
+ };
599
613
  }
600
614
 
601
615
  // -- Paragraph flushing --
@@ -37,7 +37,7 @@ import {
37
37
  } from '@fuzdev/fuz_util/svelte_preprocess_helpers.ts';
38
38
 
39
39
  import { mdz_parse } from './mdz.ts';
40
- import { mdz_to_svelte, type MdzToSvelteResult } from './mdz_to_svelte.ts';
40
+ import { mdz_to_svelte, type MdzComponentImport, type MdzToSvelteResult } from './mdz_to_svelte.ts';
41
41
 
42
42
  /**
43
43
  * An estree `ImportDeclaration` augmented with Svelte's position data.
@@ -56,12 +56,14 @@ export interface SveltePreprocessMdzOptions {
56
56
  /**
57
57
  * Component import mapping for mdz content.
58
58
  * Key: component name as used in mdz (e.g., 'Alert').
59
- * Value: import path (e.g., '#lib/Alert.svelte').
59
+ * Value: import path (e.g., '#lib/Alert.svelte'), which receives no attributes,
60
+ * or `{path, props}` naming the attributes passed as props
61
+ * (e.g., `{path: '#lib/Alert.svelte', props: ['status']}`, or `props: 'all'`).
60
62
  *
61
63
  * If mdz content references a component not in this map,
62
64
  * that `Mdz` usage is skipped (left as runtime).
63
65
  */
64
- components?: Record<string, string>;
66
+ components?: Record<string, MdzComponentImport>;
65
67
 
66
68
  /**
67
69
  * Allowed HTML elements in mdz content (e.g., `{aside: true, details: true}`).
@@ -235,7 +237,7 @@ interface FindMdzUsagesResult {
235
237
  }
236
238
 
237
239
  interface FindMdzUsagesContext {
238
- components: Record<string, string>;
240
+ components: Record<string, MdzComponentImport>;
239
241
  elements: Record<string, boolean>;
240
242
  filename: string | undefined;
241
243
  source: string;