@fuzdev/mdz 0.1.0 → 0.3.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 (77) hide show
  1. package/README.md +4 -0
  2. package/dist/Mdz.svelte +15 -7
  3. package/dist/Mdz.svelte.d.ts +9 -2
  4. package/dist/Mdz.svelte.d.ts.map +1 -1
  5. package/dist/MdzNodeView.svelte +130 -95
  6. package/dist/MdzNodeView.svelte.d.ts +6 -2
  7. package/dist/MdzNodeView.svelte.d.ts.map +1 -1
  8. package/dist/MdzStream.svelte +3 -3
  9. package/dist/MdzStream.svelte.d.ts.map +1 -1
  10. package/dist/MdzStreamNodeView.svelte +129 -95
  11. package/dist/MdzStreamNodeView.svelte.d.ts +6 -2
  12. package/dist/MdzStreamNodeView.svelte.d.ts.map +1 -1
  13. package/dist/context_helpers.d.ts +1 -1
  14. package/dist/mdz.d.ts +59 -2
  15. package/dist/mdz.d.ts.map +1 -1
  16. package/dist/mdz.js +37 -1
  17. package/dist/mdz_debug_work.d.ts +38 -0
  18. package/dist/mdz_debug_work.d.ts.map +1 -0
  19. package/dist/mdz_debug_work.js +37 -0
  20. package/dist/mdz_helpers.d.ts +96 -1
  21. package/dist/mdz_helpers.d.ts.map +1 -1
  22. package/dist/mdz_helpers.js +192 -0
  23. package/dist/mdz_lexer.d.ts +47 -1
  24. package/dist/mdz_lexer.d.ts.map +1 -1
  25. package/dist/mdz_lexer.js +364 -15
  26. package/dist/mdz_opcodes.d.ts +6 -1
  27. package/dist/mdz_opcodes.d.ts.map +1 -1
  28. package/dist/mdz_opcodes_to_nodes.d.ts.map +1 -1
  29. package/dist/mdz_opcodes_to_nodes.js +26 -0
  30. package/dist/mdz_stream_parser.d.ts.map +1 -1
  31. package/dist/mdz_stream_parser.js +20 -1
  32. package/dist/mdz_stream_parser_inline.d.ts.map +1 -1
  33. package/dist/mdz_stream_parser_inline.js +19 -17
  34. package/dist/mdz_stream_parser_link.d.ts.map +1 -1
  35. package/dist/mdz_stream_parser_link.js +88 -56
  36. package/dist/mdz_stream_parser_list.d.ts.map +1 -1
  37. package/dist/mdz_stream_parser_list.js +32 -3
  38. package/dist/mdz_stream_parser_state.d.ts +41 -0
  39. package/dist/mdz_stream_parser_state.d.ts.map +1 -1
  40. package/dist/mdz_stream_parser_state.js +53 -3
  41. package/dist/mdz_stream_parser_table.d.ts +73 -0
  42. package/dist/mdz_stream_parser_table.d.ts.map +1 -0
  43. package/dist/mdz_stream_parser_table.js +362 -0
  44. package/dist/mdz_stream_parser_text.d.ts.map +1 -1
  45. package/dist/mdz_stream_parser_text.js +35 -24
  46. package/dist/mdz_stream_state.svelte.d.ts +5 -0
  47. package/dist/mdz_stream_state.svelte.d.ts.map +1 -1
  48. package/dist/mdz_stream_state.svelte.js +8 -0
  49. package/dist/mdz_to_svelte.d.ts.map +1 -1
  50. package/dist/mdz_to_svelte.js +51 -20
  51. package/dist/mdz_token_parser.d.ts +16 -0
  52. package/dist/mdz_token_parser.d.ts.map +1 -1
  53. package/dist/mdz_token_parser.js +112 -14
  54. package/dist/svelte_preprocess_mdz.js +33 -39
  55. package/package.json +11 -8
  56. package/src/lib/context_helpers.ts +1 -1
  57. package/src/lib/mdz.ts +79 -3
  58. package/src/lib/mdz_contexts.ts +1 -1
  59. package/src/lib/mdz_debug_work.ts +38 -0
  60. package/src/lib/mdz_helpers.ts +222 -1
  61. package/src/lib/mdz_lexer.ts +420 -21
  62. package/src/lib/mdz_opcodes.ts +10 -1
  63. package/src/lib/mdz_opcodes_to_nodes.ts +37 -3
  64. package/src/lib/mdz_stream_parser.ts +26 -9
  65. package/src/lib/mdz_stream_parser_block.ts +2 -2
  66. package/src/lib/mdz_stream_parser_inline.ts +25 -21
  67. package/src/lib/mdz_stream_parser_link.ts +92 -59
  68. package/src/lib/mdz_stream_parser_list.ts +34 -6
  69. package/src/lib/mdz_stream_parser_state.ts +87 -5
  70. package/src/lib/mdz_stream_parser_table.ts +420 -0
  71. package/src/lib/mdz_stream_parser_text.ts +40 -39
  72. package/src/lib/mdz_stream_parser_url.ts +2 -2
  73. package/src/lib/mdz_stream_state.svelte.ts +8 -1
  74. package/src/lib/mdz_to_svelte.ts +49 -23
  75. package/src/lib/mdz_token_parser.ts +135 -21
  76. package/src/lib/svelte_preprocess_mdz.ts +37 -38
  77. package/src/lib/tsdoc_mdz.ts +1 -1
@@ -27,6 +27,10 @@ export class MdzStreamNode {
27
27
  ordered;
28
28
  start_number;
29
29
  number;
30
+ /** Per-column alignment (Table nodes). */
31
+ align;
32
+ /** Whether this is the header row (TableRow nodes). */
33
+ header;
30
34
  constructor(id, type) {
31
35
  this.id = id;
32
36
  this.type = type;
@@ -96,6 +100,10 @@ export class MdzStreamState {
96
100
  node.start_number = opcode.start_number;
97
101
  if (opcode.number !== undefined)
98
102
  node.number = opcode.number;
103
+ if (opcode.align !== undefined)
104
+ node.align = opcode.align;
105
+ if (opcode.header !== undefined)
106
+ node.header = opcode.header;
99
107
  this.#attach(node);
100
108
  this.#stack.push(opcode.id);
101
109
  break;
@@ -1 +1 @@
1
- {"version":3,"file":"mdz_to_svelte.d.ts","sourceRoot":"../src/lib/","sources":["../src/lib/mdz_to_svelte.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAMH,OAAO,KAAK,EAAC,OAAO,EAAC,MAAM,UAAU,CAAC;AAGtC;;GAEG;AACH,MAAM,WAAW,iBAAiB;IACjC,sCAAsC;IACtC,MAAM,EAAE,MAAM,CAAC;IAEf,yDAAyD;IACzD,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,SAAS,GAAG,OAAO,CAAA;KAAC,CAAC,CAAC;IAEhE,yEAAyE;IACzE,qBAAqB,EAAE,OAAO,CAAC;CAC/B;AAMD;;GAEG;AACH,MAAM,WAAW,kBAAkB;IAClC;;;OAGG;IACH,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAEnC;;;;;OAKG;IACH,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAElC;;;;OAIG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;IAEd;;;;;OAKG;IACH,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAE/B;;;;OAIG;IACH,0BAA0B,CAAC,EAAE,MAAM,CAAC;CACpC;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,aAAa,GACzB,OAAO,KAAK,CAAC,OAAO,CAAC,EACrB,SAAS,kBAAkB,KACzB,iBA4HF,CAAC"}
1
+ {"version":3,"file":"mdz_to_svelte.d.ts","sourceRoot":"../src/lib/","sources":["../src/lib/mdz_to_svelte.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAMH,OAAO,KAAK,EAAC,OAAO,EAAkB,MAAM,UAAU,CAAC;AAGvD;;GAEG;AACH,MAAM,WAAW,iBAAiB;IACjC,sCAAsC;IACtC,MAAM,EAAE,MAAM,CAAC;IAEf,yDAAyD;IACzD,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,SAAS,GAAG,OAAO,CAAA;KAAC,CAAC,CAAC;IAEhE,yEAAyE;IACzE,qBAAqB,EAAE,OAAO,CAAC;CAC/B;AAMD;;GAEG;AACH,MAAM,WAAW,kBAAkB;IAClC;;;OAGG;IACH,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAEnC;;;;;OAKG;IACH,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAElC;;;;OAIG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;IAEd;;;;;OAKG;IACH,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAE/B;;;;OAIG;IACH,0BAA0B,CAAC,EAAE,MAAM,CAAC;CACpC;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,aAAa,GACzB,OAAO,KAAK,CAAC,OAAO,CAAC,EACrB,SAAS,kBAAkB,KACzB,iBAsJF,CAAC"}
@@ -7,10 +7,10 @@
7
7
  *
8
8
  * @module
9
9
  */
10
- import { UnreachableError } from '@fuzdev/fuz_util/error.js';
11
- import { escape_svelte_text } from '@fuzdev/fuz_util/svelte_preprocess_helpers.js';
12
- import { escape_js_string } from '@fuzdev/fuz_util/string.js';
13
- import { mdz_resolve_relative_path, mdz_is_void_element } from './mdz_helpers.js';
10
+ import { UnreachableError } from '@fuzdev/fuz_util/error.ts';
11
+ import { escape_svelte_text } from '@fuzdev/fuz_util/svelte_preprocess_helpers.ts';
12
+ import { escape_js_string } from '@fuzdev/fuz_util/string.ts';
13
+ import { mdz_is_void_element, mdz_classify_link } from './mdz_helpers.js';
14
14
  /** Derives an import's local identifier from a `.svelte` import path's basename. */
15
15
  const component_local_name = (import_path) => import_path.slice(import_path.lastIndexOf('/') + 1).replace(/\.svelte$/, '');
16
16
  /**
@@ -58,24 +58,22 @@ export const mdz_to_svelte = (nodes, options) => {
58
58
  return `<s>${render_nodes(node.children)}</s>`;
59
59
  case 'Link': {
60
60
  const children_markup = render_nodes(node.children);
61
- if (node.link_type === 'internal') {
62
- if (node.reference.startsWith('.') && base) {
63
- const resolved = mdz_resolve_relative_path(node.reference, base);
64
- imports.set('resolve', { path: '$app/paths', kind: 'named' });
65
- return `<a href={resolve('${escape_js_string(resolved)}')}>${children_markup}</a>`;
66
- }
67
- if (node.reference.startsWith('#') ||
68
- node.reference.startsWith('?') ||
69
- node.reference.startsWith('.') ||
70
- // Bare references (e.g. `foo`) aren't absolute — resolve() throws on them
71
- !node.reference.startsWith('/')) {
72
- return `<a href={'${escape_js_string(node.reference)}'}>${children_markup}</a>`;
73
- }
61
+ // safety gate + resolve-vs-raw classification shared with the runtime
62
+ // views via `mdz_classify_link`, so the three renderers can't drift
63
+ // (an unsafe `javascript:`/`data:` ref renders as plain children, no
64
+ // `<a>` — defense in depth, since `mdz_to_svelte` is a public entry
65
+ // callable with hand-built trees that never went through `mdz_parse`)
66
+ const link = mdz_classify_link(node.reference, node.link_type, base);
67
+ if (link.kind === 'unsafe')
68
+ return children_markup;
69
+ if (link.kind === 'resolve') {
74
70
  imports.set('resolve', { path: '$app/paths', kind: 'named' });
75
- return `<a href={resolve('${escape_js_string(node.reference)}')}>${children_markup}</a>`;
71
+ return `<a href={resolve('${escape_js_string(link.href)}')}>${children_markup}</a>`;
72
+ }
73
+ if (link.kind === 'external') {
74
+ return `<a href={'${escape_js_string(link.href)}'} target="_blank" rel="noopener">${children_markup}</a>`;
76
75
  }
77
- // External link — matches MdzNodeView: target="_blank" rel="noopener"
78
- return `<a href={'${escape_js_string(node.reference)}'} target="_blank" rel="noopener">${children_markup}</a>`;
76
+ return `<a href={'${escape_js_string(link.href)}'}>${children_markup}</a>`;
79
77
  }
80
78
  case 'Paragraph':
81
79
  return `<p>${render_nodes(node.children)}</p>`;
@@ -90,6 +88,39 @@ export const mdz_to_svelte = (nodes, options) => {
90
88
  return `<li>${render_nodes(node.children)}</li>`;
91
89
  case 'Blockquote':
92
90
  return `<blockquote>${render_nodes(node.children)}</blockquote>`;
91
+ case 'Table': {
92
+ const { align } = node;
93
+ const render_row = (row, tag) => {
94
+ let cells = '';
95
+ // normalize to the column count: pad short rows, ignore extra cells
96
+ for (let i = 0; i < align.length; i++) {
97
+ const a = align[i];
98
+ const style = a ? ` style="text-align:${a}"` : '';
99
+ const cell = row.children[i];
100
+ cells += `<${tag}${style}>${cell ? render_nodes(cell.children) : ''}</${tag}>`;
101
+ }
102
+ return `<tr>${cells}</tr>`;
103
+ };
104
+ // by grammar the header row is always first (and the only one) —
105
+ // read it by index and skip it in the body, mirroring the node
106
+ // views (a `.filter()` pair re-partitions every row; see
107
+ // `MdzNodeView.svelte`'s "Table rendering de-quadratified" note)
108
+ let out = '<table>';
109
+ const header_row = node.children[0]?.header ? node.children[0] : undefined;
110
+ if (header_row)
111
+ out += '<thead>' + render_row(header_row, 'th') + '</thead>';
112
+ let body = '';
113
+ for (const row of node.children)
114
+ if (!row.header)
115
+ body += render_row(row, 'td');
116
+ if (body)
117
+ out += '<tbody>' + body + '</tbody>';
118
+ return out + '</table>';
119
+ }
120
+ case 'TableRow':
121
+ case 'TableCell':
122
+ // rendered within their `Table`; never reached through `render_nodes`
123
+ throw new Error(`mdz: ${node.type} must render within a Table`);
93
124
  case 'Hr':
94
125
  return '<hr />';
95
126
  case 'Heading': {
@@ -15,6 +15,22 @@ import { type MdzToken } from './mdz_lexer.js';
15
15
  export declare class MdzTokenParser {
16
16
  #private;
17
17
  constructor(tokens: Array<MdzToken>);
18
+ /**
19
+ * Rebind to a new token stream and rewind, so one parser instance can be
20
+ * reused across inputs — the streaming table path reuses one across a row's
21
+ * cells via `MdzTableCellParser`.
22
+ *
23
+ * @nodocs
24
+ */
25
+ reset(tokens: Array<MdzToken>): void;
18
26
  parse(): Array<MdzNode>;
27
+ /**
28
+ * Parse the entire token stream as a single inline run — used for table
29
+ * cells, whose tokens come from `MdzLexer.lex_table_cell`. Adjacent text
30
+ * merges as it accumulates, matching `#parse_table_cell`.
31
+ *
32
+ * @nodocs
33
+ */
34
+ parse_inline_run(): Array<MdzNode>;
19
35
  }
20
36
  //# sourceMappingURL=mdz_token_parser.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"mdz_token_parser.d.ts","sourceRoot":"../src/lib/","sources":["../src/lib/mdz_token_parser.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EACX,OAAO,EAYP,MAAM,UAAU,CAAC;AAQlB,OAAO,EACN,KAAK,QAAQ,EASb,MAAM,gBAAgB,CAAC;AAExB;;;GAGG;AACH,qBAAa,cAAc;;gBAId,MAAM,EAAE,KAAK,CAAC,QAAQ,CAAC;IAInC,KAAK,IAAI,KAAK,CAAC,OAAO,CAAC;CAyevB"}
1
+ {"version":3,"file":"mdz_token_parser.d.ts","sourceRoot":"../src/lib/","sources":["../src/lib/mdz_token_parser.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EACX,OAAO,EAeP,MAAM,UAAU,CAAC;AAQlB,OAAO,EACN,KAAK,QAAQ,EAYb,MAAM,gBAAgB,CAAC;AAiBxB;;;GAGG;AACH,qBAAa,cAAc;;gBAId,MAAM,EAAE,KAAK,CAAC,QAAQ,CAAC;IAInC;;;;;;OAMG;IACH,KAAK,CAAC,MAAM,EAAE,KAAK,CAAC,QAAQ,CAAC,GAAG,IAAI;IAKpC,KAAK,IAAI,KAAK,CAAC,OAAO,CAAC;IAIvB;;;;;;OAMG;IACH,gBAAgB,IAAI,KAAK,CAAC,OAAO,CAAC;CA+iBlC"}
@@ -8,6 +8,19 @@
8
8
  */
9
9
  import { has_non_whitespace, mdz_extract_single_tag, mdz_heading_id, mdz_merge_adjacent_text, mdz_push_merging_text, } from './mdz_helpers.js';
10
10
  import {} from './mdz_lexer.js';
11
+ /**
12
+ * Whether a token type ends the current inline context — the block-boundary
13
+ * set shared by the delimiter-pair and link children loops (an unclosed
14
+ * inline never swallows block structure).
15
+ */
16
+ const token_is_block_boundary = (type) => type === 'paragraph_break' ||
17
+ type === 'heading_start' ||
18
+ type === 'hr' ||
19
+ type === 'codeblock' ||
20
+ type === 'list_open' ||
21
+ type === 'list_item_close' ||
22
+ type === 'blockquote_open' ||
23
+ type === 'table_open';
11
24
  /**
12
25
  * Builds an `MdzNode[]` tree from a lexed `MdzToken[]` stream.
13
26
  * Used by `mdz_parse`, which should be preferred for simple usage.
@@ -18,9 +31,36 @@ export class MdzTokenParser {
18
31
  constructor(tokens) {
19
32
  this.#tokens = tokens;
20
33
  }
34
+ /**
35
+ * Rebind to a new token stream and rewind, so one parser instance can be
36
+ * reused across inputs — the streaming table path reuses one across a row's
37
+ * cells via `MdzTableCellParser`.
38
+ *
39
+ * @nodocs
40
+ */
41
+ reset(tokens) {
42
+ this.#tokens = tokens;
43
+ this.#index = 0;
44
+ }
21
45
  parse() {
22
46
  return this.#parse_block_stream(false);
23
47
  }
48
+ /**
49
+ * Parse the entire token stream as a single inline run — used for table
50
+ * cells, whose tokens come from `MdzLexer.lex_table_cell`. Adjacent text
51
+ * merges as it accumulates, matching `#parse_table_cell`.
52
+ *
53
+ * @nodocs
54
+ */
55
+ parse_inline_run() {
56
+ const children = [];
57
+ while (this.#index < this.#tokens.length) {
58
+ const node = this.#parse_inline();
59
+ if (node)
60
+ mdz_push_merging_text(children, node);
61
+ }
62
+ return children;
63
+ }
24
64
  /**
25
65
  * Parse a run of block-level tokens into nodes — the document root, or a
26
66
  * blockquote's contents (a quote's content is a mini-document, so both
@@ -80,6 +120,13 @@ export class MdzTokenParser {
80
120
  nodes.push(this.#parse_blockquote());
81
121
  continue;
82
122
  }
123
+ if (token.type === 'table_open') {
124
+ const flushed = this.#flush_paragraph(paragraph_children, true);
125
+ if (flushed)
126
+ nodes.push(flushed);
127
+ nodes.push(this.#parse_table());
128
+ continue;
129
+ }
83
130
  if (token.type === 'paragraph_break') {
84
131
  const flushed = this.#flush_paragraph(paragraph_children, true);
85
132
  if (flushed)
@@ -107,6 +154,63 @@ export class MdzTokenParser {
107
154
  const end = children.length > 0 ? children[children.length - 1].end : open.end;
108
155
  return { type: 'Blockquote', children, start: open.start, end };
109
156
  }
157
+ #parse_table() {
158
+ const open = this.#tokens[this.#index];
159
+ this.#index++;
160
+ const rows = [];
161
+ while (this.#index < this.#tokens.length) {
162
+ const t = this.#tokens[this.#index];
163
+ if (t.type === 'table_row_open') {
164
+ rows.push(this.#parse_table_row());
165
+ continue;
166
+ }
167
+ if (t.type === 'table_close') {
168
+ this.#index++;
169
+ break;
170
+ }
171
+ this.#index++; // skip anything unexpected (defensive)
172
+ }
173
+ const end = rows.length > 0 ? rows[rows.length - 1].end : open.end;
174
+ return { type: 'Table', align: open.align, children: rows, start: open.start, end };
175
+ }
176
+ #parse_table_row() {
177
+ const open = this.#tokens[this.#index];
178
+ const header = open.header;
179
+ this.#index++;
180
+ const cells = [];
181
+ while (this.#index < this.#tokens.length) {
182
+ const t = this.#tokens[this.#index];
183
+ if (t.type === 'table_cell_open') {
184
+ cells.push(this.#parse_table_cell());
185
+ continue;
186
+ }
187
+ if (t.type === 'table_row_close') {
188
+ this.#index++;
189
+ return { type: 'TableRow', header, children: cells, start: open.start, end: t.end };
190
+ }
191
+ this.#index++; // skip anything unexpected (defensive)
192
+ }
193
+ const end = cells.length > 0 ? cells[cells.length - 1].end : open.end;
194
+ return { type: 'TableRow', header, children: cells, start: open.start, end };
195
+ }
196
+ #parse_table_cell() {
197
+ const open = this.#tokens[this.#index];
198
+ const start = open.start;
199
+ this.#index++;
200
+ const children = [];
201
+ while (this.#index < this.#tokens.length) {
202
+ const t = this.#tokens[this.#index];
203
+ if (t.type === 'table_cell_close') {
204
+ this.#index++;
205
+ return { type: 'TableCell', children, start, end: t.end };
206
+ }
207
+ const node = this.#parse_inline();
208
+ if (node)
209
+ mdz_push_merging_text(children, node);
210
+ }
211
+ const end = children.length > 0 ? children[children.length - 1].end : open.end;
212
+ return { type: 'TableCell', children, start, end };
213
+ }
110
214
  #parse_heading() {
111
215
  const token = this.#tokens[this.#index];
112
216
  const start = token.start;
@@ -188,6 +292,12 @@ export class MdzTokenParser {
188
292
  children.push(this.#parse_blockquote());
189
293
  continue;
190
294
  }
295
+ if (t.type === 'table_open') {
296
+ flush();
297
+ in_paragraph = false;
298
+ children.push(this.#parse_table());
299
+ continue;
300
+ }
191
301
  if (t.type === 'codeblock') {
192
302
  flush();
193
303
  in_paragraph = false;
@@ -301,13 +411,7 @@ export class MdzTokenParser {
301
411
  this.#index++;
302
412
  return { type: node_type, children, start, end: t.end };
303
413
  }
304
- if (t.type === 'paragraph_break' ||
305
- t.type === 'heading_start' ||
306
- t.type === 'hr' ||
307
- t.type === 'codeblock' ||
308
- t.type === 'list_open' ||
309
- t.type === 'list_item_close' ||
310
- t.type === 'blockquote_open') {
414
+ if (token_is_block_boundary(t.type)) {
311
415
  break;
312
416
  }
313
417
  const node = this.#parse_inline();
@@ -342,13 +446,7 @@ export class MdzTokenParser {
342
446
  // No link_ref - treat as text
343
447
  return { type: 'Text', content: '[', start, end: start + 1 };
344
448
  }
345
- if (t.type === 'paragraph_break' ||
346
- t.type === 'heading_start' ||
347
- t.type === 'hr' ||
348
- t.type === 'codeblock' ||
349
- t.type === 'list_open' ||
350
- t.type === 'list_item_close' ||
351
- t.type === 'blockquote_open') {
449
+ if (token_is_block_boundary(t.type)) {
352
450
  break;
353
451
  }
354
452
  const node = this.#parse_inline();
@@ -16,8 +16,8 @@
16
16
  import { parse } from 'svelte/compiler';
17
17
  import MagicString from 'magic-string';
18
18
  import { walk } from 'zimmerframe';
19
- import { should_exclude_path } from '@fuzdev/fuz_util/path.js';
20
- import { find_attribute, extract_static_string, try_extract_conditional_chain, build_static_bindings, resolve_component_names, has_identifier_in_tree, find_import_insert_position, generate_import_lines, remove_variable_declaration, remove_import_declaration, remove_import_specifier, handle_preprocess_error, } from '@fuzdev/fuz_util/svelte_preprocess_helpers.js';
19
+ import { should_exclude_path } from '@fuzdev/fuz_util/path.ts';
20
+ import { find_attribute, extract_static_string, try_extract_conditional_chain, build_static_bindings, resolve_component_names, has_identifier_in_tree, find_import_insert_position, generate_import_lines, remove_variable_declaration, remove_import_declaration, remove_import_specifier, handle_preprocess_error, } from '@fuzdev/fuz_util/svelte_preprocess_helpers.ts';
21
21
  import { mdz_parse } from './mdz.js';
22
22
  import { mdz_to_svelte } from './mdz_to_svelte.js';
23
23
  const PRECOMPILED_NAME = 'MdzPrecompiled';
@@ -138,6 +138,29 @@ const collect_consumed_bindings = (value, bindings) => {
138
138
  collect_from_expr(value.expression);
139
139
  return consumed;
140
140
  };
141
+ /**
142
+ * Parse a static mdz string and render it to Svelte markup. Returns `null` to
143
+ * leave the usage for the runtime — either the content raised a parse error
144
+ * (reported via `on_error`) or it contains unconfigured tags (which the
145
+ * precompiler emits nothing for; the runtime renders the fallback chrome).
146
+ */
147
+ const render_static_mdz = (content, base, context) => {
148
+ let result;
149
+ try {
150
+ result = mdz_to_svelte(mdz_parse(content), {
151
+ components: context.components,
152
+ elements: context.elements,
153
+ base,
154
+ code_component_import: context.code_component_import,
155
+ codeblock_component_import: context.codeblock_component_import,
156
+ });
157
+ }
158
+ catch (error) {
159
+ handle_preprocess_error(error, '[fuz-mdz]', context.filename, context.on_error);
160
+ return null;
161
+ }
162
+ return result.has_unconfigured_tags ? null : result;
163
+ };
141
164
  /**
142
165
  * Walks the AST to find `Mdz` component usages with static `content` props
143
166
  * and generates transformations to replace them with `MdzPrecompiled` children.
@@ -181,25 +204,9 @@ const find_mdz_usages = (ast, mdz_names, context) => {
181
204
  // Extract static string value
182
205
  const content_value = extract_static_string(content_attr.value, context.bindings);
183
206
  if (content_value !== null) {
184
- // Parse mdz content and render to Svelte markup
185
- let result;
186
- try {
187
- const nodes = mdz_parse(content_value);
188
- result = mdz_to_svelte(nodes, {
189
- components: context.components,
190
- elements: context.elements,
191
- base,
192
- code_component_import: context.code_component_import,
193
- codeblock_component_import: context.codeblock_component_import,
194
- });
195
- }
196
- catch (error) {
197
- handle_preprocess_error(error, '[fuz-mdz]', context.filename, context.on_error);
198
- return;
199
- }
200
- // If content has unconfigured tags, skip this usage (fall back to runtime)
201
- if (result.has_unconfigured_tags)
202
- return;
207
+ const result = render_static_mdz(content_value, base, context);
208
+ if (result === null)
209
+ return; // parse error or unconfigured tags — leave for runtime
203
210
  const consumed = collect_consumed_bindings(content_attr.value, context.bindings);
204
211
  const replacement = build_replacement(node, exclude_attrs, result.markup, context.source);
205
212
  transformed_usages.set(node.name, (transformed_usages.get(node.name) ?? 0) + 1);
@@ -219,24 +226,11 @@ const find_mdz_usages = (ast, mdz_names, context) => {
219
226
  return;
220
227
  // Parse and render each branch
221
228
  const branch_results = [];
222
- try {
223
- for (const branch of chain) {
224
- const nodes = mdz_parse(branch.value);
225
- const result = mdz_to_svelte(nodes, {
226
- components: context.components,
227
- elements: context.elements,
228
- base,
229
- code_component_import: context.code_component_import,
230
- codeblock_component_import: context.codeblock_component_import,
231
- });
232
- if (result.has_unconfigured_tags)
233
- return;
234
- branch_results.push({ markup: result.markup, imports: result.imports });
235
- }
236
- }
237
- catch (error) {
238
- handle_preprocess_error(error, '[fuz-mdz]', context.filename, context.on_error);
239
- return;
229
+ for (const branch of chain) {
230
+ const result = render_static_mdz(branch.value, base, context);
231
+ if (result === null)
232
+ return; // parse error or unconfigured tags — leave for runtime
233
+ branch_results.push({ markup: result.markup, imports: result.imports });
240
234
  }
241
235
  // Build {#if}/{:else if}/{:else} markup
242
236
  let children_markup = '';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fuzdev/mdz",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "description": "a strict markdown dialect built for streaming, Svelte authoring, docs websites, and untrusted content",
5
5
  "tagline": "markdown for fuz",
6
6
  "glyph": "📝",
@@ -30,7 +30,10 @@
30
30
  "deploy": "gro deploy",
31
31
  "benchmark": "gro run src/benchmarks/mdz.benchmark.ts",
32
32
  "benchmark:save": "gro run src/benchmarks/mdz.benchmark.ts --save",
33
- "benchmark:clean": "rm -f src/benchmarks/baseline.json"
33
+ "benchmark:clean": "rm -f src/benchmarks/baseline.json",
34
+ "benchmark:render": "BENCHMARK_RENDER=1 gro test render.benchmark",
35
+ "benchmark:render:save": "BENCHMARK_RENDER=save gro test render.benchmark",
36
+ "benchmark:render:clean": "rm -f src/benchmarks/render/baseline.json"
34
37
  },
35
38
  "type": "module",
36
39
  "engines": {
@@ -57,15 +60,15 @@
57
60
  },
58
61
  "devDependencies": {
59
62
  "@fuzdev/blake3_wasm": "^0.1.1",
60
- "@fuzdev/fuz_code": "^0.46.0",
61
- "@fuzdev/fuz_css": "^0.62.0",
62
- "@fuzdev/fuz_ui": "^0.204.0",
63
+ "@fuzdev/fuz_code": "^0.46.1",
64
+ "@fuzdev/fuz_css": "^0.63.2",
65
+ "@fuzdev/fuz_ui": "^0.205.1",
63
66
  "@fuzdev/fuz_util": "^0.65.1",
64
- "@fuzdev/gro": "^0.204.0",
67
+ "@fuzdev/gro": "^0.205.1",
65
68
  "@ryanatkn/eslint-config": "^0.12.1",
66
69
  "@sveltejs/acorn-typescript": "^1.0.10",
67
70
  "@sveltejs/adapter-static": "^3.0.10",
68
- "@sveltejs/kit": "^2.65.0",
71
+ "@sveltejs/kit": "2.64.0",
69
72
  "@sveltejs/package": "^2.5.7",
70
73
  "@sveltejs/vite-plugin-svelte": "^6.2.4",
71
74
  "@types/estree": "^1.0.8",
@@ -83,7 +86,7 @@
83
86
  "tslib": "^2.8.1",
84
87
  "typescript": "^5.9.3",
85
88
  "typescript-eslint": "^8.48.1",
86
- "vite": "^7.3.1",
89
+ "vite": "^7.3.6",
87
90
  "vitest": "^4.0.15"
88
91
  },
89
92
  "prettier": {
@@ -14,7 +14,7 @@ import {getContext, setContext} from 'svelte';
14
14
  * If a `fallback` is provided, the `value` argument to `set` is optional
15
15
  * and `get_maybe` is omitted from the type.
16
16
  *
17
- * Vendored from `@fuzdev/fuz_ui/context_helpers.js` — fuz_ui sits above mdz
17
+ * Vendored from `@fuzdev/fuz_ui/context_helpers.ts` — fuz_ui sits above mdz
18
18
  * in the dependency graph, so mdz keeps its own copy for `mdz_contexts.ts`.
19
19
  * Consumers should import fuz_ui's.
20
20
  *
package/src/lib/mdz.ts CHANGED
@@ -19,7 +19,7 @@
19
19
  * - inline code in backticks (creates `Code` nodes; auto-linking to identifiers/modules
20
20
  * is handled by the rendering layer via `MdzNodeView.svelte`)
21
21
  * - paragraph breaks (blank lines)
22
- * - block elements: headings, horizontal rules, lists, blockquotes, code blocks
22
+ * - block elements: headings, horizontal rules, lists, blockquotes, code blocks, tables
23
23
  * - HTML elements and Svelte components (opt-in via context)
24
24
  *
25
25
  * Whitespace: the parser preserves whitespace in text node content (single
@@ -50,8 +50,8 @@
50
50
  * @module
51
51
  */
52
52
 
53
- import {MdzLexer} from './mdz_lexer.js';
54
- import {MdzTokenParser} from './mdz_token_parser.js';
53
+ import {MdzLexer} from './mdz_lexer.ts';
54
+ import {MdzTokenParser} from './mdz_token_parser.ts';
55
55
 
56
56
  // TODO design incremental parsing or some system that preserves Svelte components across re-renders when possible
57
57
 
@@ -64,6 +64,46 @@ import {MdzTokenParser} from './mdz_token_parser.js';
64
64
  export const mdz_parse = (text: string): Array<MdzNode> =>
65
65
  new MdzTokenParser(new MdzLexer(text).tokenize()).parse();
66
66
 
67
+ /**
68
+ * Parses table cells' inline content for the streaming parser, sharing one
69
+ * `MdzLexer` + `MdzTokenParser` across a row's cells rather than allocating a
70
+ * fresh pair (and search-memo `Map`) per cell. Bind to the buffer holding the
71
+ * row, then call `parse` once per cell `[cell_start, cell_end)` span; each
72
+ * call yields inline `MdzNode`s honoring code-span protection and the `\|`
73
+ * escape, matching the sync reference exactly — it *is* the sync lexer/parser,
74
+ * reset per cell, mirroring how the sync lexer already reuses one instance
75
+ * across a table's cells.
76
+ *
77
+ * The sync pipeline never needs this seam: its `MdzLexer` tokenizes a table's
78
+ * cells in place. Only the streaming path, which parses each fully-buffered
79
+ * cell out of band, would otherwise allocate per cell.
80
+ *
81
+ * @nodocs
82
+ */
83
+ export class MdzTableCellParser {
84
+ #lexer: MdzLexer;
85
+ #parser: MdzTokenParser = new MdzTokenParser([]);
86
+
87
+ /** @param text - the buffer the cell spans index into (stable across a row). */
88
+ constructor(text: string) {
89
+ this.#lexer = new MdzLexer(text);
90
+ }
91
+
92
+ /**
93
+ * Rebind to a new buffer so one instance serves a whole table — the
94
+ * streaming path rebinds per row (the buffer string changes across feeds;
95
+ * the lexer's search memo survives when it doesn't).
96
+ */
97
+ rebind(text: string): void {
98
+ this.#lexer.rebind(text);
99
+ }
100
+
101
+ parse(cell_start: number, cell_end: number): Array<MdzNode> {
102
+ this.#parser.reset(this.#lexer.lex_table_cell(cell_start, cell_end));
103
+ return this.#parser.parse_inline_run();
104
+ }
105
+ }
106
+
67
107
  export type MdzNode =
68
108
  | MdzNodeText
69
109
  | MdzNodeCode
@@ -78,6 +118,9 @@ export type MdzNode =
78
118
  | MdzNodeList
79
119
  | MdzNodeListItem
80
120
  | MdzNodeBlockquote
121
+ | MdzNodeTable
122
+ | MdzNodeTableRow
123
+ | MdzNodeTableCell
81
124
  | MdzNodeElement
82
125
  | MdzNodeComponent;
83
126
 
@@ -175,6 +218,39 @@ export interface MdzNodeBlockquote extends MdzNodeBase {
175
218
  children: Array<MdzNode>;
176
219
  }
177
220
 
221
+ /** Per-column text alignment from the delimiter row's colons; `null` is the default (no alignment). */
222
+ export type MdzTableAlign = 'left' | 'center' | 'right' | null;
223
+
224
+ export interface MdzNodeTable extends MdzNodeBase {
225
+ type: 'Table';
226
+ /**
227
+ * Per-column alignment, length equal to the delimiter row's column count.
228
+ * Rendered as `text-align` on each cell; tooling can read it to re-theme
229
+ * (e.g. a CSP-strict consumer that avoids inline styles).
230
+ */
231
+ align: Array<MdzTableAlign>;
232
+ /** The header row first, then any body rows. */
233
+ children: Array<MdzNodeTableRow>;
234
+ }
235
+
236
+ export interface MdzNodeTableRow extends MdzNodeBase {
237
+ type: 'TableRow';
238
+ /** `true` for the header row (renders `thead`/`th`); `false` for body rows (`tbody`/`td`). */
239
+ header: boolean;
240
+ /**
241
+ * The row's authored cells, preserved as written. The renderer normalizes to
242
+ * the column count: short rows are padded with empty cells, cells past the
243
+ * column count are ignored (their content survives here for tooling).
244
+ */
245
+ children: Array<MdzNodeTableCell>;
246
+ }
247
+
248
+ export interface MdzNodeTableCell extends MdzNodeBase {
249
+ type: 'TableCell';
250
+ /** Inline content only (text, bold, italic, strikethrough, code, links). */
251
+ children: Array<MdzNode>;
252
+ }
253
+
178
254
  export interface MdzNodeElement extends MdzNodeBase {
179
255
  type: 'Element';
180
256
  /** HTML element name (e.g. `div`, `span`, `aside`). */
@@ -1,6 +1,6 @@
1
1
  import type {Component} from 'svelte';
2
2
 
3
- import {create_context} from './context_helpers.js';
3
+ import {create_context} from './context_helpers.ts';
4
4
 
5
5
  /**
6
6
  * Component registry for custom Svelte components that can be used in mdz content.
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Deterministic scan-work counter for the complexity-ratio guards
3
+ * (`mdz_scaling_ratio.test.ts`). Off by default; when `enabled`, the parser hot
4
+ * paths accumulate the number of characters scanned (and tag-stack frames
5
+ * walked) into `total`. Unlike wall-clock timing this is a pure function of
6
+ * input + algorithm — invariant across machines, runs, and thermal state — so a
7
+ * super-linear regression shows up as a work ratio > ~linear regardless of the
8
+ * ~2× machine variance that makes absolute timing unusable here. Its sole
9
+ * consumer is `mdz_scaling_ratio.test.ts`.
10
+ *
11
+ * Coverage is the dominant re-scan choke points — `#index_of` + `#tokenize_text`
12
+ * (sync) and the streaming memo scans, `consume_text_run`, and `try_close_tag`'s
13
+ * stack walk — not every scan. A super-linear regression isolated to an
14
+ * *uncounted* probe (e.g. `#has_paragraph_break_between`, the tag-name collection
15
+ * loop, the URL/path probes) wouldn't move the ratio; extend the instrumentation
16
+ * if hot cost ever migrates there.
17
+ *
18
+ * Zero store when disabled: each instrumented site guards on `enabled` (a single
19
+ * branch-predicted property load, the same category as the shipped `DEV`
20
+ * checks). It is deliberately NOT the `esm-env` `DEV` flag — `DEV` is true under
21
+ * every `gro test`/`gro dev` run, which would leave the counter live in ordinary
22
+ * development; this flag is flipped true only by the ratio test.
23
+ *
24
+ * @nodocs
25
+ */
26
+ export const mdz_debug_work: {enabled: boolean; total: number} = {enabled: false, total: 0};
27
+
28
+ /**
29
+ * Reset the counter and set whether the hot paths accumulate into it. Tests call
30
+ * `mdz_debug_work_reset()` immediately before a parse and read
31
+ * `mdz_debug_work.total` immediately after; pass `false` to disable and zero it.
32
+ *
33
+ * @nodocs
34
+ */
35
+ export const mdz_debug_work_reset = (enabled = true): void => {
36
+ mdz_debug_work.enabled = enabled;
37
+ mdz_debug_work.total = 0;
38
+ };