@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
@@ -8,6 +8,7 @@
8
8
  * @module
9
9
  */
10
10
  import { NEWLINE, has_non_whitespace, mdz_heading_id_from_text } from './mdz_helpers.js';
11
+ import { mdz_debug_work } from './mdz_debug_work.js';
11
12
  /** @nodocs */
12
13
  export const create_state = () => ({
13
14
  buffer: '',
@@ -24,11 +25,14 @@ export const create_state = () => ({
24
25
  codeblock: null,
25
26
  list_levels: [],
26
27
  blockquote: null,
28
+ table: null,
27
29
  in_list_item_run: false,
28
30
  last_empty_item_end: 0,
29
31
  base_offset: 0,
30
32
  search_memo: null,
31
33
  open_counts: { Bold: 0, Italic: 0, Strikethrough: 0, Link: 0 },
34
+ open_link_tag_depth: 0,
35
+ open_tag_counts: null,
32
36
  in_heading: false,
33
37
  in_code: false,
34
38
  in_paragraph: false,
@@ -68,6 +72,14 @@ export const push_stack_entry = (state, id, node_type, start, delimiter = '', ta
68
72
  else {
69
73
  bump_open_count(state, node_type, 1);
70
74
  }
75
+ if (tag_name !== undefined) {
76
+ const counts = (state.open_tag_counts ??= new Map());
77
+ counts.set(tag_name, (counts.get(tag_name) ?? 0) + 1);
78
+ }
79
+ // Link/Element/Component contribute to the nesting-depth cap (tag_name is
80
+ // set for Element/Component only, so `Link || tag_name` is exactly the three)
81
+ if (node_type === 'Link' || tag_name !== undefined)
82
+ state.open_link_tag_depth++;
71
83
  };
72
84
  /**
73
85
  * Adjust the `open_counts` entry for a counted container type; no-op for
@@ -98,6 +110,19 @@ const bump_open_count = (state, node_type, delta) => {
98
110
  export const pop_stack_entry = (state) => {
99
111
  const entry = state.stack.pop();
100
112
  bump_open_count(state, entry.node_type, -1);
113
+ if (entry.tag_name !== undefined) {
114
+ const counts = state.open_tag_counts;
115
+ // drop zero-count keys so a long stream of many distinct tag names can't
116
+ // grow the map unboundedly — the `!counts.get(name)` bail in `try_close_tag`
117
+ // treats a missing key and a 0 count identically, so this is behavior-neutral
118
+ const remaining = counts.get(entry.tag_name) - 1;
119
+ if (remaining === 0)
120
+ counts.delete(entry.tag_name);
121
+ else
122
+ counts.set(entry.tag_name, remaining);
123
+ }
124
+ if (entry.node_type === 'Link' || entry.tag_name !== undefined)
125
+ state.open_link_tag_depth--;
101
126
  return entry;
102
127
  };
103
128
  /**
@@ -162,7 +187,11 @@ const memo_index_of = (state, memo, needle, from) => {
162
187
  // rescan only the appended tail, overlapping the old boundary so a
163
188
  // straddling needle isn't missed
164
189
  const rescan_from = Math.max(global_from, memo.searched_to - (needle.length - 1));
165
- const result = state.buffer.indexOf(needle, rescan_from - state.base_offset);
190
+ const local_from = rescan_from - state.base_offset;
191
+ const result = state.buffer.indexOf(needle, local_from);
192
+ if (mdz_debug_work.enabled) {
193
+ mdz_debug_work.total += (result === -1 ? state.buffer.length : result) - local_from;
194
+ }
166
195
  memo.from = global_from;
167
196
  memo.result = result === -1 ? -1 : state.base_offset + result;
168
197
  memo.searched_to = global_end;
@@ -171,6 +200,9 @@ const memo_index_of = (state, memo, needle, from) => {
171
200
  // found result already behind `from` — fall through to a fresh scan
172
201
  }
173
202
  const result = state.buffer.indexOf(needle, from);
203
+ if (mdz_debug_work.enabled) {
204
+ mdz_debug_work.total += (result === -1 ? state.buffer.length : result) - from;
205
+ }
174
206
  memo.from = global_from;
175
207
  memo.result = result === -1 ? -1 : state.base_offset + result;
176
208
  memo.searched_to = global_end;
@@ -210,8 +242,11 @@ export const emit = (state, op) => {
210
242
  if (top) {
211
243
  top.has_children = true;
212
244
  // propagate non-whitespace marker to the nearest Paragraph so
213
- // whitespace-only paragraphs can be discarded at close
214
- if (op.type === 'void' || has_non_whitespace(op.content)) {
245
+ // whitespace-only paragraphs can be discarded at close — the O(1)
246
+ // no-paragraph check runs first so codeblock/table-cell emissions
247
+ // (never inside a Paragraph) skip the content scan entirely
248
+ if (op.type === 'void' ||
249
+ (state.paragraph_stack_idx !== -1 && has_non_whitespace(op.content))) {
215
250
  mark_paragraph_non_whitespace(state);
216
251
  }
217
252
  }
@@ -435,6 +470,21 @@ export const revert_failed_close = (state, stack_idx) => {
435
470
  const frames_above = state.stack.length - 1 - stack_idx;
436
471
  state.stack.splice(stack_idx, 1);
437
472
  bump_open_count(state, entry.node_type, -1);
473
+ if (entry.tag_name !== undefined) {
474
+ const counts = state.open_tag_counts;
475
+ // drop zero-count keys so a long stream of many distinct tag names can't
476
+ // grow the map unboundedly — the `!counts.get(name)` bail in `try_close_tag`
477
+ // treats a missing key and a 0 count identically, so this is behavior-neutral
478
+ const remaining = counts.get(entry.tag_name) - 1;
479
+ if (remaining === 0)
480
+ counts.delete(entry.tag_name);
481
+ else
482
+ counts.set(entry.tag_name, remaining);
483
+ }
484
+ // only ever reverts an Italic today, but keep the depth counter honest if a
485
+ // Link/Element/Component is ever failed-closed mid-stack
486
+ if (entry.node_type === 'Link' || entry.tag_name !== undefined)
487
+ state.open_link_tag_depth--;
438
488
  state.opcodes.push({
439
489
  type: 'revert',
440
490
  id: entry.id,
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Table handlers for the streaming mdz parser.
3
+ *
4
+ * A table is recognized at a column-0 pipe row whose next line is a valid
5
+ * delimiter row with a matching column count — a bounded one-line lookahead:
6
+ * the header row holds (`need_more`) until its delimiter line arrives, then the
7
+ * whole header emits at once. Body rows then stream one per line. Because a row
8
+ * is always fully buffered before it emits (no within-row streaming), each
9
+ * cell's inline content is parsed with the sync reference (`MdzTableCellParser`,
10
+ * one `MdzLexer`/`MdzTokenParser` shared across the whole table's cells,
11
+ * rebound per row) and replayed as opcodes — guaranteeing sync↔stream parity
12
+ * by construction.
13
+ *
14
+ * @module
15
+ */
16
+ import { type MdzTableAlign } from './mdz.js';
17
+ import { type MdzStreamParserState, type TryResult } from './mdz_stream_parser_state.js';
18
+ /**
19
+ * Try to open a table at a column-0 pipe row. Returns `need_more` until both
20
+ * the header line and its delimiter line are buffered (or, in forced mode,
21
+ * resolvable), `not_match` when the two lines aren't a valid header+delimiter
22
+ * pair, and `consumed` once the table opens (closing any open paragraph first,
23
+ * emitting the header row, and leaving `state.pos` at the first body line).
24
+ *
25
+ * @nodocs
26
+ */
27
+ export declare const try_table_start: (state: MdzStreamParserState, forced: boolean) => TryResult;
28
+ /**
29
+ * Process the line at `state.pos` while a table is open: emit it as a body row,
30
+ * or close the table when it isn't a valid pipe row. Returns false (need more
31
+ * input) only when a pipe-row line is started but not yet complete. A non-pipe
32
+ * line (including a blank line) ends the table and is left for normal
33
+ * processing.
34
+ *
35
+ * @nodocs
36
+ */
37
+ export declare const process_table_line: (state: MdzStreamParserState, forced: boolean) => boolean;
38
+ /**
39
+ * Close the open table.
40
+ *
41
+ * @nodocs
42
+ */
43
+ export declare const close_table: (state: MdzStreamParserState) => void;
44
+ /** A recognized in-item table head: the header row's cells + the delimiter alignment. */
45
+ export interface TableInItemHead {
46
+ header_cells: Array<{
47
+ start: number;
48
+ end: number;
49
+ }>;
50
+ header_end: number;
51
+ align: Array<MdzTableAlign>;
52
+ delim_end: number;
53
+ }
54
+ /**
55
+ * Recognize a table opening as a block child of a list item: a pipe row at
56
+ * `header_first` (the line's indent already skipped) whose next line is a
57
+ * delimiter row, both indented past `marker_indent` (so both stay inside the
58
+ * item). Returns the parsed head, `'pending'` when the two-line lookahead isn't
59
+ * fully buffered, or `null` when it isn't a table.
60
+ *
61
+ * @nodocs
62
+ */
63
+ export declare const match_table_in_item_head: (state: MdzStreamParserState, header_first: number, marker_indent: number, forced: boolean) => TableInItemHead | "pending" | null;
64
+ /**
65
+ * Open a table as a block child of a list item at `marker_indent`, given a
66
+ * `head` from `match_table_in_item_head`. Emits the `Table` open + header row
67
+ * and enters table mode (with `item_indent`), leaving `state.pos` at the first
68
+ * body line. The caller closes the item's run and pops to the attach level first.
69
+ *
70
+ * @nodocs
71
+ */
72
+ export declare const open_table_in_item: (state: MdzStreamParserState, header_first: number, marker_indent: number, head: TableInItemHead) => void;
73
+ //# sourceMappingURL=mdz_stream_parser_table.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mdz_stream_parser_table.d.ts","sourceRoot":"../src/lib/","sources":["../src/lib/mdz_stream_parser_table.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAWH,OAAO,EAAmC,KAAK,aAAa,EAAC,MAAM,UAAU,CAAC;AAC9E,OAAO,EACN,KAAK,oBAAoB,EACzB,KAAK,SAAS,EAQd,MAAM,8BAA8B,CAAC;AAEtC;;;;;;;;GAQG;AACH,eAAO,MAAM,eAAe,GAAI,OAAO,oBAAoB,EAAE,QAAQ,OAAO,KAAG,SAyC9E,CAAC;AAEF;;;;;;;;GAQG;AACH,eAAO,MAAM,kBAAkB,GAAI,OAAO,oBAAoB,EAAE,QAAQ,OAAO,KAAG,OAyCjF,CAAC;AA2DF;;;;GAIG;AACH,eAAO,MAAM,WAAW,GAAI,OAAO,oBAAoB,KAAG,IAczD,CAAC;AAEF,yFAAyF;AACzF,MAAM,WAAW,eAAe;IAC/B,YAAY,EAAE,KAAK,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAC,CAAC,CAAC;IAClD,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,KAAK,CAAC,aAAa,CAAC,CAAC;IAC5B,SAAS,EAAE,MAAM,CAAC;CAClB;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,wBAAwB,GACpC,OAAO,oBAAoB,EAC3B,cAAc,MAAM,EACpB,eAAe,MAAM,EACrB,QAAQ,OAAO,KACb,eAAe,GAAG,SAAS,GAAG,IAmBhC,CAAC;AAEF;;;;;;;GAOG;AACH,eAAO,MAAM,kBAAkB,GAC9B,OAAO,oBAAoB,EAC3B,cAAc,MAAM,EACpB,eAAe,MAAM,EACrB,MAAM,eAAe,KACnB,IAsBF,CAAC"}
@@ -0,0 +1,362 @@
1
+ /**
2
+ * Table handlers for the streaming mdz parser.
3
+ *
4
+ * A table is recognized at a column-0 pipe row whose next line is a valid
5
+ * delimiter row with a matching column count — a bounded one-line lookahead:
6
+ * the header row holds (`need_more`) until its delimiter line arrives, then the
7
+ * whole header emits at once. Body rows then stream one per line. Because a row
8
+ * is always fully buffered before it emits (no within-row streaming), each
9
+ * cell's inline content is parsed with the sync reference (`MdzTableCellParser`,
10
+ * one `MdzLexer`/`MdzTokenParser` shared across the whole table's cells,
11
+ * rebound per row) and replayed as opcodes — guaranteeing sync↔stream parity
12
+ * by construction.
13
+ *
14
+ * @module
15
+ */
16
+ import { DEV } from 'esm-env';
17
+ import { NEWLINE, PIPE, is_line_whitespace, mdz_split_table_row, mdz_parse_table_delimiter, } from './mdz_helpers.js';
18
+ import { MdzTableCellParser } from './mdz.js';
19
+ import { alloc_id, close_paragraph, emit, flush_text, offset, pop_stack_entry, push_stack_entry, } from './mdz_stream_parser_state.js';
20
+ /**
21
+ * Try to open a table at a column-0 pipe row. Returns `need_more` until both
22
+ * the header line and its delimiter line are buffered (or, in forced mode,
23
+ * resolvable), `not_match` when the two lines aren't a valid header+delimiter
24
+ * pair, and `consumed` once the table opens (closing any open paragraph first,
25
+ * emitting the header row, and leaving `state.pos` at the first body line).
26
+ *
27
+ * @nodocs
28
+ */
29
+ export const try_table_start = (state, forced) => {
30
+ const start = state.pos;
31
+ const header_end = state.buffer.indexOf('\n', start);
32
+ // a table needs a header line plus a delimiter line — without the header's
33
+ // newline there's no room for a delimiter (at EOF it can never be a table)
34
+ if (header_end === -1)
35
+ return forced ? 'not_match' : 'need_more';
36
+ const header_cells = mdz_split_table_row(state.buffer, start, header_end);
37
+ if (header_cells === null)
38
+ return 'not_match';
39
+ const delim_start = header_end + 1;
40
+ let delim_end = state.buffer.indexOf('\n', delim_start);
41
+ if (delim_end === -1) {
42
+ if (!forced)
43
+ return 'need_more'; // delimiter line not complete yet
44
+ delim_end = state.buffer.length;
45
+ }
46
+ const align = mdz_parse_table_delimiter(state.buffer, delim_start, delim_end);
47
+ if (align === null || align.length !== header_cells.length)
48
+ return 'not_match';
49
+ // committed — the delimiter row is structural, never emitted
50
+ flush_text(state);
51
+ close_paragraph(state);
52
+ const table_id = alloc_id(state);
53
+ const table_start = offset(state, start);
54
+ emit(state, { type: 'open', id: table_id, node_type: 'Table', start: table_start, align });
55
+ push_stack_entry(state, table_id, 'Table', table_start);
56
+ state.table = {
57
+ id: table_id,
58
+ item_indent: null,
59
+ cell_parser: new MdzTableCellParser(state.buffer),
60
+ };
61
+ emit_table_row(state, header_cells, true, start, header_end);
62
+ // skip past the delimiter row to the first potential body line
63
+ state.pos = delim_end === state.buffer.length ? delim_end : delim_end + 1;
64
+ state.column = 0;
65
+ state.prev_char = NEWLINE;
66
+ return 'consumed';
67
+ };
68
+ /**
69
+ * Process the line at `state.pos` while a table is open: emit it as a body row,
70
+ * or close the table when it isn't a valid pipe row. Returns false (need more
71
+ * input) only when a pipe-row line is started but not yet complete. A non-pipe
72
+ * line (including a blank line) ends the table and is left for normal
73
+ * processing.
74
+ *
75
+ * @nodocs
76
+ */
77
+ export const process_table_line = (state, forced) => {
78
+ const item_indent = state.table.item_indent;
79
+ if (item_indent !== null) {
80
+ return process_table_line_in_item(state, forced, item_indent);
81
+ }
82
+ if (state.pos >= state.buffer.length) {
83
+ // the table may continue with more rows in a later chunk
84
+ if (forced) {
85
+ close_table(state);
86
+ return true;
87
+ }
88
+ return false;
89
+ }
90
+ const row_start = state.pos;
91
+ // a body row must start with `|` at column 0; anything else ends the table
92
+ // (decided without waiting for the line's newline)
93
+ if (state.buffer.charCodeAt(row_start) !== PIPE) {
94
+ close_table(state);
95
+ return true;
96
+ }
97
+ let row_end = state.buffer.indexOf('\n', row_start);
98
+ if (row_end === -1) {
99
+ if (!forced)
100
+ return false; // pipe-row line not complete — hold
101
+ row_end = state.buffer.length;
102
+ }
103
+ const cells = mdz_split_table_row(state.buffer, row_start, row_end);
104
+ if (cells === null) {
105
+ // starts with `|` but isn't a valid row (e.g. no trailing pipe) — table ends
106
+ close_table(state);
107
+ return true;
108
+ }
109
+ emit_table_row(state, cells, false, row_start, row_end);
110
+ state.pos = row_end === state.buffer.length ? row_end : row_end + 1;
111
+ state.column = 0;
112
+ state.prev_char = NEWLINE;
113
+ return true;
114
+ };
115
+ /**
116
+ * Process body rows of an in-item table. Unlike the top-level path this keeps
117
+ * `state.pos` on the newline that ends each row — a row boundary the buffer
118
+ * never compacts past, since it's the active cursor. So when a dedent (to the
119
+ * item's `marker_indent` or shallower), a blank line, or a non-pipe line ends
120
+ * the table, `close_table` leaves the cursor on that newline and the list's
121
+ * newline handler re-classifies the following line — the same control-return
122
+ * shape as the in-item codeblock, robust across chunk boundaries.
123
+ */
124
+ const process_table_line_in_item = (state, forced, item_indent) => {
125
+ if (state.pos >= state.buffer.length) {
126
+ if (forced) {
127
+ close_table(state);
128
+ return true;
129
+ }
130
+ return false;
131
+ }
132
+ // `state.pos` sits on the newline ending the previous row (or the delimiter);
133
+ // the candidate next row is the line after it
134
+ const line_start = state.pos + 1;
135
+ let j = line_start;
136
+ while (j < state.buffer.length && is_line_whitespace(state.buffer.charCodeAt(j))) {
137
+ j++;
138
+ }
139
+ if (j >= state.buffer.length) {
140
+ if (forced) {
141
+ close_table(state);
142
+ return true;
143
+ }
144
+ return false; // indent-only / incomplete line — hold
145
+ }
146
+ if (j - line_start <= item_indent || state.buffer.charCodeAt(j) !== PIPE) {
147
+ // a dedent to the marker (or shallower), a blank line, or a non-pipe line
148
+ // ends the table; the cursor stays on the row-boundary newline so the
149
+ // list resumes via its newline handler
150
+ close_table(state);
151
+ return true;
152
+ }
153
+ let row_end = state.buffer.indexOf('\n', j);
154
+ if (row_end === -1) {
155
+ if (!forced)
156
+ return false; // pipe-row line not complete — hold
157
+ row_end = state.buffer.length;
158
+ }
159
+ const cells = mdz_split_table_row(state.buffer, j, row_end);
160
+ if (cells === null) {
161
+ close_table(state);
162
+ return true;
163
+ }
164
+ emit_table_row(state, cells, false, j, row_end);
165
+ state.pos = row_end; // stay on this row's terminating newline (or EOF)
166
+ return true;
167
+ };
168
+ /**
169
+ * Close the open table.
170
+ *
171
+ * @nodocs
172
+ */
173
+ export const close_table = (state) => {
174
+ if (!state.table)
175
+ return;
176
+ const { item_indent } = state.table;
177
+ const entry = pop_stack_entry(state);
178
+ emit(state, { type: 'close', id: entry.id, end: offset(state) });
179
+ state.table = null;
180
+ state.active_text_id = null;
181
+ if (item_indent === null) {
182
+ // top-level: the trailing newline + any blank lines absorb at the loop top
183
+ state.skip_blank_lines = true;
184
+ }
185
+ // in-item: the cursor is left on a row-boundary newline (or EOF). The list's
186
+ // newline handler re-classifies the following line; at EOF, `finish` closes
187
+ // the list. No skip_blank_lines — the list owns blank-line containment.
188
+ };
189
+ /**
190
+ * Recognize a table opening as a block child of a list item: a pipe row at
191
+ * `header_first` (the line's indent already skipped) whose next line is a
192
+ * delimiter row, both indented past `marker_indent` (so both stay inside the
193
+ * item). Returns the parsed head, `'pending'` when the two-line lookahead isn't
194
+ * fully buffered, or `null` when it isn't a table.
195
+ *
196
+ * @nodocs
197
+ */
198
+ export const match_table_in_item_head = (state, header_first, marker_indent, forced) => {
199
+ const { buffer } = state;
200
+ const header_end = buffer.indexOf('\n', header_first);
201
+ if (header_end === -1)
202
+ return forced ? null : 'pending'; // header needs its newline + a delimiter beneath
203
+ const header_cells = mdz_split_table_row(buffer, header_first, header_end);
204
+ if (header_cells === null)
205
+ return null;
206
+ const delim_line_start = header_end + 1;
207
+ let dj = delim_line_start;
208
+ while (dj < buffer.length && is_line_whitespace(buffer.charCodeAt(dj)))
209
+ dj++;
210
+ if (dj >= buffer.length)
211
+ return forced ? null : 'pending'; // delimiter line not buffered yet
212
+ if (dj - delim_line_start <= marker_indent)
213
+ return null; // delimiter dedented out of the item
214
+ let delim_end = buffer.indexOf('\n', dj);
215
+ if (delim_end === -1) {
216
+ if (!forced)
217
+ return 'pending'; // delimiter line incomplete
218
+ delim_end = buffer.length;
219
+ }
220
+ const align = mdz_parse_table_delimiter(buffer, dj, delim_end);
221
+ if (align === null || align.length !== header_cells.length)
222
+ return null;
223
+ return { header_cells, header_end, align, delim_end };
224
+ };
225
+ /**
226
+ * Open a table as a block child of a list item at `marker_indent`, given a
227
+ * `head` from `match_table_in_item_head`. Emits the `Table` open + header row
228
+ * and enters table mode (with `item_indent`), leaving `state.pos` at the first
229
+ * body line. The caller closes the item's run and pops to the attach level first.
230
+ *
231
+ * @nodocs
232
+ */
233
+ export const open_table_in_item = (state, header_first, marker_indent, head) => {
234
+ const table_id = alloc_id(state);
235
+ const table_start = offset(state, header_first);
236
+ emit(state, {
237
+ type: 'open',
238
+ id: table_id,
239
+ node_type: 'Table',
240
+ start: table_start,
241
+ align: head.align,
242
+ });
243
+ push_stack_entry(state, table_id, 'Table', table_start);
244
+ state.table = {
245
+ id: table_id,
246
+ item_indent: marker_indent,
247
+ cell_parser: new MdzTableCellParser(state.buffer),
248
+ };
249
+ emit_table_row(state, head.header_cells, true, header_first, head.header_end);
250
+ // leave the cursor on the delimiter's newline (or EOF) — the in-item body-row
251
+ // handler reads from the line after it and hands back on this same convention
252
+ state.pos = head.delim_end;
253
+ state.column = 0;
254
+ state.prev_char = NEWLINE;
255
+ };
256
+ /** Emit one table row: open, each cell, close. */
257
+ const emit_table_row = (state, cells, header, row_start, row_end) => {
258
+ const row_id = alloc_id(state);
259
+ const row_off = offset(state, row_start);
260
+ emit(state, { type: 'open', id: row_id, node_type: 'TableRow', start: row_off, header });
261
+ push_stack_entry(state, row_id, 'TableRow', row_off);
262
+ // one lexer/parser shared across the whole table rather than allocated per
263
+ // row (or per cell) — rebound per row because the buffer string changes
264
+ // across feeds; the lexer's search memo survives when it doesn't
265
+ const cell_parser = state.table.cell_parser;
266
+ cell_parser.rebind(state.buffer);
267
+ for (const cell of cells) {
268
+ emit_table_cell(state, cell_parser, cell.start, cell.end);
269
+ }
270
+ pop_stack_entry(state);
271
+ emit(state, { type: 'close', id: row_id, end: offset(state, row_end) });
272
+ state.active_text_id = null;
273
+ };
274
+ /**
275
+ * Emit one cell: open, its inline content (parsed with the sync reference over
276
+ * the fully-buffered span via the row's shared `cell_parser`), close.
277
+ */
278
+ const emit_table_cell = (state, cell_parser, cell_start, cell_end) => {
279
+ const cell_id = alloc_id(state);
280
+ const cell_off = offset(state, cell_start);
281
+ emit(state, { type: 'open', id: cell_id, node_type: 'TableCell', start: cell_off });
282
+ push_stack_entry(state, cell_id, 'TableCell', cell_off);
283
+ emit_inline_nodes(state, cell_parser.parse(cell_start, cell_end));
284
+ pop_stack_entry(state);
285
+ emit(state, { type: 'close', id: cell_id, end: offset(state, cell_end) });
286
+ state.active_text_id = null;
287
+ };
288
+ /**
289
+ * Replay sync-parsed inline nodes as opcodes. Cell content is inline-only;
290
+ * positions are local to `state.buffer` and lifted to global via `offset`.
291
+ */
292
+ const emit_inline_nodes = (state, nodes) => {
293
+ for (const node of nodes) {
294
+ switch (node.type) {
295
+ case 'Text':
296
+ emit(state, {
297
+ type: 'text',
298
+ id: alloc_id(state),
299
+ content: node.content,
300
+ text_type: 'Text',
301
+ start: offset(state, node.start),
302
+ end: offset(state, node.end),
303
+ });
304
+ break;
305
+ case 'Code':
306
+ emit(state, {
307
+ type: 'text',
308
+ id: alloc_id(state),
309
+ content: node.content,
310
+ text_type: 'Code',
311
+ start: offset(state, node.start),
312
+ end: offset(state, node.end),
313
+ });
314
+ break;
315
+ case 'Bold':
316
+ case 'Italic':
317
+ case 'Strikethrough': {
318
+ const id = alloc_id(state);
319
+ const s = offset(state, node.start);
320
+ emit(state, { type: 'open', id, node_type: node.type, start: s });
321
+ push_stack_entry(state, id, node.type, s);
322
+ emit_inline_nodes(state, node.children);
323
+ pop_stack_entry(state);
324
+ emit(state, { type: 'close', id, end: offset(state, node.end) });
325
+ break;
326
+ }
327
+ case 'Link': {
328
+ const id = alloc_id(state);
329
+ const s = offset(state, node.start);
330
+ emit(state, { type: 'open', id, node_type: 'Link', start: s });
331
+ push_stack_entry(state, id, 'Link', s);
332
+ emit_inline_nodes(state, node.children);
333
+ pop_stack_entry(state);
334
+ emit(state, {
335
+ type: 'close',
336
+ id,
337
+ end: offset(state, node.end),
338
+ reference: node.reference,
339
+ link_type: node.link_type,
340
+ });
341
+ break;
342
+ }
343
+ case 'Element':
344
+ case 'Component': {
345
+ const id = alloc_id(state);
346
+ const s = offset(state, node.start);
347
+ emit(state, { type: 'open', id, node_type: node.type, start: s, name: node.name });
348
+ push_stack_entry(state, id, node.type, s, '', node.name);
349
+ emit_inline_nodes(state, node.children);
350
+ pop_stack_entry(state);
351
+ emit(state, { type: 'close', id, end: offset(state, node.end) });
352
+ break;
353
+ }
354
+ // cells are inline-only (lexed via `lex_table_cell`); a block node here
355
+ // would mean a sync/stream grammar drift — fail loud in dev
356
+ default:
357
+ if (DEV) {
358
+ throw new Error(`mdz table cell: unexpected inline node type '${node.type}'`);
359
+ }
360
+ }
361
+ }
362
+ };
@@ -1 +1 @@
1
- {"version":3,"file":"mdz_stream_parser_text.d.ts","sourceRoot":"../src/lib/","sources":["../src/lib/mdz_stream_parser_text.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAqBH,OAAO,EACN,KAAK,oBAAoB,EAIzB,MAAM,8BAA8B,CAAC;AAEtC;;;;;;;GAOG;AACH,eAAO,MAAM,yBAAyB,GAAI,OAAO,oBAAoB,EAAE,WAAW,MAAM,KAAG,IAM1F,CAAC;AAEF;;;;;;;GAOG;AACH,eAAO,MAAM,gBAAgB,GAAI,OAAO,oBAAoB,KAAG,IAsC9D,CAAC"}
1
+ {"version":3,"file":"mdz_stream_parser_text.d.ts","sourceRoot":"../src/lib/","sources":["../src/lib/mdz_stream_parser_text.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAeH,OAAO,EACN,KAAK,oBAAoB,EAIzB,MAAM,8BAA8B,CAAC;AAGtC;;;;;;;GAOG;AACH,eAAO,MAAM,yBAAyB,GAAI,OAAO,oBAAoB,EAAE,WAAW,MAAM,KAAG,IAM1F,CAAC;AAEF;;;;;;;GAOG;AACH,eAAO,MAAM,gBAAgB,GAAI,OAAO,oBAAoB,KAAG,IA4C9D,CAAC"}
@@ -3,8 +3,9 @@
3
3
  *
4
4
  * @module
5
5
  */
6
- import { ASTERISK, BACKTICK, HTTPS_PREFIX_LENGTH, H_LOWER, H_UPPER, LEFT_ANGLE, LEFT_BRACKET, NEWLINE, PERIOD, RIGHT_BRACKET, SLASH, SPACE, TAB, TILDE, UNDERSCORE, is_word_char, match_url_prefix_case_insensitive, } from './mdz_helpers.js';
6
+ import { HTTPS_PREFIX_LENGTH, NEWLINE, SPACE, TAB, TEXT_CLASS_NEWLINE, TEXT_CLASS_PATH, TEXT_CLASS_STOP, TEXT_CLASS_URL, TEXT_SCAN_CLASS, is_word_char, match_url_prefix_case_insensitive, } from './mdz_helpers.js';
7
7
  import { accumulate_text, ensure_paragraph, offset, } from './mdz_stream_parser_state.js';
8
+ import { mdz_debug_work } from './mdz_debug_work.js';
8
9
  /**
9
10
  * Consume a delimiter at the current position as literal text — the shared
10
11
  * tail of every "this delimiter doesn't open/close anything here" decision
@@ -34,33 +35,43 @@ export const consume_text_run = (state) => {
34
35
  state.pos++;
35
36
  while (state.pos < state.buffer.length) {
36
37
  const c = state.buffer.charCodeAt(state.pos);
37
- if (c === BACKTICK ||
38
- c === ASTERISK ||
39
- c === UNDERSCORE ||
40
- c === TILDE ||
41
- c === LEFT_BRACKET ||
42
- c === RIGHT_BRACKET ||
43
- c === LEFT_ANGLE ||
44
- c === NEWLINE ||
45
- // URL/path: only break when actually at a URL or path start.
46
- // Most h/H/./slash in prose are not URLs/paths — continuing the scan
47
- // avoids the full dispatch cycle through process_loop → process_inline.
48
- // For `h`/`H` at a word boundary, also break when there isn't enough
49
- // buffer left to confirm `https://` / `http://` — process_inline's
50
- // speculator then carries the prefix match across chunk boundaries.
51
- // Scheme matching is case-insensitive (RFC 3986).
52
- ((c === H_LOWER || c === H_UPPER) &&
53
- (match_url_prefix_case_insensitive(state.buffer, state.pos) > 0 ||
54
- (state.buffer.length - state.pos < HTTPS_PREFIX_LENGTH &&
55
- !is_word_char(state.buffer.charCodeAt(state.pos - 1))))) ||
56
- ((c === SLASH || c === PERIOD) &&
57
- (state.buffer.charCodeAt(state.pos - 1) === SPACE ||
58
- state.buffer.charCodeAt(state.pos - 1) === NEWLINE ||
59
- state.buffer.charCodeAt(state.pos - 1) === TAB))) {
38
+ // one table load classifies the char; zero (the common case) continues
39
+ // the run without any further checks. `TEXT_SCAN_CLASS` is shared with
40
+ // the sync lexer's `#tokenize_text` (`mdz_lexer.ts`); the dispatch bodies
41
+ // intentionally differ — that one handles `\|` escapes and list-run
42
+ // strip, this one URL speculation across chunk boundaries.
43
+ const char_class = c < 128 ? TEXT_SCAN_CLASS[c] : 0;
44
+ if (char_class === 0) {
45
+ state.pos++;
46
+ continue;
47
+ }
48
+ if ((char_class & (TEXT_CLASS_STOP | TEXT_CLASS_NEWLINE)) !== 0)
60
49
  break;
50
+ if ((char_class & TEXT_CLASS_URL) !== 0) {
51
+ // URL: only break when actually at a URL start. Most h/H in prose are
52
+ // not URLs — continuing the scan avoids the full dispatch cycle
53
+ // through process_loop → process_inline. At a word boundary, also
54
+ // break when there isn't enough buffer left to confirm `https://` /
55
+ // `http://` — process_inline's speculator then carries the prefix
56
+ // match across chunk boundaries. Scheme matching is case-insensitive
57
+ // (RFC 3986).
58
+ if (match_url_prefix_case_insensitive(state.buffer, state.pos) > 0 ||
59
+ (state.buffer.length - state.pos < HTTPS_PREFIX_LENGTH &&
60
+ !is_word_char(state.buffer.charCodeAt(state.pos - 1)))) {
61
+ break;
62
+ }
63
+ }
64
+ else if ((char_class & TEXT_CLASS_PATH) !== 0) {
65
+ // path (`/` or `.`): only break at a plausible path start
66
+ const prev = state.buffer.charCodeAt(state.pos - 1);
67
+ if (prev === SPACE || prev === NEWLINE || prev === TAB)
68
+ break;
61
69
  }
70
+ // backslash (or a non-starting path char) — ordinary text
62
71
  state.pos++;
63
72
  }
73
+ if (mdz_debug_work.enabled)
74
+ mdz_debug_work.total += state.pos - start;
64
75
  accumulate_text(state, state.buffer.slice(start, state.pos), offset(state, start));
65
76
  state.prev_char = state.buffer.charCodeAt(state.pos - 1);
66
77
  state.column += state.pos - start;
@@ -8,6 +8,7 @@
8
8
  * @module
9
9
  */
10
10
  import type { MdzNodeId, MdzNodeType, MdzOpcode } from './mdz_opcodes.js';
11
+ import type { MdzTableAlign } from './mdz.js';
11
12
  /**
12
13
  * A reactive node in the stream renderer tree.
13
14
  * Fields are `$state` so Svelte re-renders only what changes.
@@ -26,6 +27,10 @@ export declare class MdzStreamNode {
26
27
  ordered?: boolean;
27
28
  start_number?: number;
28
29
  number?: number;
30
+ /** Per-column alignment (Table nodes). */
31
+ align?: Array<MdzTableAlign>;
32
+ /** Whether this is the header row (TableRow nodes). */
33
+ header?: boolean;
29
34
  constructor(id: MdzNodeId, type: MdzNodeType);
30
35
  }
31
36
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"mdz_stream_state.svelte.d.ts","sourceRoot":"../src/lib/","sources":["../src/lib/mdz_stream_state.svelte.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAIH,OAAO,KAAK,EAAC,SAAS,EAAE,WAAW,EAAE,SAAS,EAAC,MAAM,kBAAkB,CAAC;AAExE;;;GAGG;AACH,qBAAa,aAAa;IACzB,EAAE,EAAE,SAAS,CAAC;IACd,IAAI,EAAE,WAAW,CAAC;IAClB,OAAO,EAAE,MAAM,CAAc;IAC7B,QAAQ,EAAE,KAAK,CAAC,aAAa,CAAC,CAAc;IAE5C,KAAK,CAAC,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IAC9B,SAAS,CAAC,EAAE,MAAM,CAAY;IAC9B,SAAS,CAAC,EAAE,UAAU,GAAG,UAAU,CAAY;IAC/C,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,CAAY;IAC/B,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,MAAM,CAAC,EAAE,MAAM,CAAC;gBAEJ,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,WAAW;CAI5C;AAED;;;;;;;;;GASG;AACH,qBAAa,cAAc;;IAC1B,IAAI,EAAE,KAAK,CAAC,aAAa,CAAC,CAAc;IAqCxC;;OAEG;IACH,KAAK,CAAC,MAAM,EAAE,SAAS,GAAG,IAAI;IA2R9B;;OAEG;IACH,WAAW,CAAC,OAAO,EAAE,KAAK,CAAC,SAAS,CAAC,GAAG,IAAI;CAK5C"}
1
+ {"version":3,"file":"mdz_stream_state.svelte.d.ts","sourceRoot":"../src/lib/","sources":["../src/lib/mdz_stream_state.svelte.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAIH,OAAO,KAAK,EAAC,SAAS,EAAE,WAAW,EAAE,SAAS,EAAC,MAAM,kBAAkB,CAAC;AACxE,OAAO,KAAK,EAAC,aAAa,EAAC,MAAM,UAAU,CAAC;AAE5C;;;GAGG;AACH,qBAAa,aAAa;IACzB,EAAE,EAAE,SAAS,CAAC;IACd,IAAI,EAAE,WAAW,CAAC;IAClB,OAAO,EAAE,MAAM,CAAc;IAC7B,QAAQ,EAAE,KAAK,CAAC,aAAa,CAAC,CAAc;IAE5C,KAAK,CAAC,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IAC9B,SAAS,CAAC,EAAE,MAAM,CAAY;IAC9B,SAAS,CAAC,EAAE,UAAU,GAAG,UAAU,CAAY;IAC/C,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,CAAY;IAC/B,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,0CAA0C;IAC1C,KAAK,CAAC,EAAE,KAAK,CAAC,aAAa,CAAC,CAAC;IAC7B,uDAAuD;IACvD,MAAM,CAAC,EAAE,OAAO,CAAC;gBAEL,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,WAAW;CAI5C;AAED;;;;;;;;;GASG;AACH,qBAAa,cAAc;;IAC1B,IAAI,EAAE,KAAK,CAAC,aAAa,CAAC,CAAc;IAqCxC;;OAEG;IACH,KAAK,CAAC,MAAM,EAAE,SAAS,GAAG,IAAI;IA6R9B;;OAEG;IACH,WAAW,CAAC,OAAO,EAAE,KAAK,CAAC,SAAS,CAAC,GAAG,IAAI;CAK5C"}