@fuzdev/fuz_ui 0.198.0 → 0.199.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 (108) hide show
  1. package/README.md +5 -3
  2. package/dist/ApiIndex.svelte +2 -2
  3. package/dist/ApiModule.svelte +1 -1
  4. package/dist/LibraryDetail.svelte +14 -16
  5. package/dist/LibraryDetail.svelte.d.ts.map +1 -1
  6. package/dist/LibrarySummary.svelte +12 -12
  7. package/dist/Mdz.svelte +2 -2
  8. package/dist/MdzNodeView.svelte +8 -6
  9. package/dist/MdzNodeView.svelte.d.ts.map +1 -1
  10. package/dist/MdzRoot.svelte +30 -0
  11. package/dist/MdzRoot.svelte.d.ts +12 -0
  12. package/dist/MdzRoot.svelte.d.ts.map +1 -0
  13. package/dist/MdzStream.svelte +32 -0
  14. package/dist/MdzStream.svelte.d.ts +12 -0
  15. package/dist/MdzStream.svelte.d.ts.map +1 -0
  16. package/dist/MdzStreamNodeView.svelte +106 -0
  17. package/dist/MdzStreamNodeView.svelte.d.ts +9 -0
  18. package/dist/MdzStreamNodeView.svelte.d.ts.map +1 -0
  19. package/dist/ProjectLinks.svelte +2 -0
  20. package/dist/ProjectLinks.svelte.d.ts.map +1 -1
  21. package/dist/declaration.svelte.js +2 -2
  22. package/dist/library.svelte.d.ts +9 -70
  23. package/dist/library.svelte.d.ts.map +1 -1
  24. package/dist/library.svelte.js +24 -18
  25. package/dist/library_helpers.d.ts +3 -3
  26. package/dist/library_helpers.js +3 -3
  27. package/dist/logos.d.ts +7 -0
  28. package/dist/logos.d.ts.map +1 -1
  29. package/dist/logos.js +9 -0
  30. package/dist/mdz.d.ts.map +1 -1
  31. package/dist/mdz.js +37 -29
  32. package/dist/mdz_components.d.ts +37 -23
  33. package/dist/mdz_components.d.ts.map +1 -1
  34. package/dist/mdz_components.js +16 -4
  35. package/dist/mdz_helpers.d.ts +46 -14
  36. package/dist/mdz_helpers.d.ts.map +1 -1
  37. package/dist/mdz_helpers.js +189 -56
  38. package/dist/mdz_lexer.d.ts.map +1 -1
  39. package/dist/mdz_lexer.js +18 -22
  40. package/dist/mdz_opcodes.d.ts +174 -0
  41. package/dist/mdz_opcodes.d.ts.map +1 -0
  42. package/dist/mdz_opcodes.js +14 -0
  43. package/dist/mdz_opcodes_to_nodes.d.ts +20 -0
  44. package/dist/mdz_opcodes_to_nodes.d.ts.map +1 -0
  45. package/dist/mdz_opcodes_to_nodes.js +332 -0
  46. package/dist/mdz_stream_parser.d.ts +80 -0
  47. package/dist/mdz_stream_parser.d.ts.map +1 -0
  48. package/dist/mdz_stream_parser.js +354 -0
  49. package/dist/mdz_stream_parser_block.d.ts +76 -0
  50. package/dist/mdz_stream_parser_block.d.ts.map +1 -0
  51. package/dist/mdz_stream_parser_block.js +458 -0
  52. package/dist/mdz_stream_parser_inline.d.ts +52 -0
  53. package/dist/mdz_stream_parser_inline.d.ts.map +1 -0
  54. package/dist/mdz_stream_parser_inline.js +378 -0
  55. package/dist/mdz_stream_parser_link.d.ts +22 -0
  56. package/dist/mdz_stream_parser_link.d.ts.map +1 -0
  57. package/dist/mdz_stream_parser_link.js +215 -0
  58. package/dist/mdz_stream_parser_state.d.ts +187 -0
  59. package/dist/mdz_stream_parser_state.d.ts.map +1 -0
  60. package/dist/mdz_stream_parser_state.js +398 -0
  61. package/dist/mdz_stream_parser_text.d.ts +14 -0
  62. package/dist/mdz_stream_parser_text.d.ts.map +1 -0
  63. package/dist/mdz_stream_parser_text.js +50 -0
  64. package/dist/mdz_stream_parser_url.d.ts +60 -0
  65. package/dist/mdz_stream_parser_url.d.ts.map +1 -0
  66. package/dist/mdz_stream_parser_url.js +307 -0
  67. package/dist/mdz_stream_state.svelte.d.ts +44 -0
  68. package/dist/mdz_stream_state.svelte.d.ts.map +1 -0
  69. package/dist/mdz_stream_state.svelte.js +357 -0
  70. package/dist/mdz_token_parser.d.ts.map +1 -1
  71. package/dist/mdz_token_parser.js +8 -39
  72. package/dist/module.svelte.js +1 -1
  73. package/dist/site.svelte.d.ts +13 -0
  74. package/dist/site.svelte.d.ts.map +1 -1
  75. package/dist/site.svelte.js +8 -2
  76. package/dist/tsdoc_mdz.d.ts +2 -2
  77. package/dist/tsdoc_mdz.js +2 -2
  78. package/dist/vite_plugin_pkg_json.d.ts +63 -0
  79. package/dist/vite_plugin_pkg_json.d.ts.map +1 -0
  80. package/dist/vite_plugin_pkg_json.js +126 -0
  81. package/package.json +6 -5
  82. package/src/lib/declaration.svelte.ts +2 -2
  83. package/src/lib/library.svelte.ts +37 -20
  84. package/src/lib/library_helpers.ts +3 -3
  85. package/src/lib/logos.ts +10 -0
  86. package/src/lib/mdz.ts +38 -29
  87. package/src/lib/mdz_components.ts +40 -19
  88. package/src/lib/mdz_helpers.ts +199 -56
  89. package/src/lib/mdz_lexer.ts +18 -20
  90. package/src/lib/mdz_opcodes.ts +205 -0
  91. package/src/lib/mdz_opcodes_to_nodes.ts +375 -0
  92. package/src/lib/mdz_stream_parser.ts +415 -0
  93. package/src/lib/mdz_stream_parser_block.ts +532 -0
  94. package/src/lib/mdz_stream_parser_inline.ts +414 -0
  95. package/src/lib/mdz_stream_parser_link.ts +271 -0
  96. package/src/lib/mdz_stream_parser_state.ts +539 -0
  97. package/src/lib/mdz_stream_parser_text.ts +77 -0
  98. package/src/lib/mdz_stream_parser_url.ts +365 -0
  99. package/src/lib/mdz_stream_state.svelte.ts +387 -0
  100. package/src/lib/mdz_token_parser.ts +13 -40
  101. package/src/lib/module.svelte.ts +1 -1
  102. package/src/lib/site.svelte.ts +17 -2
  103. package/src/lib/tsdoc_mdz.ts +2 -2
  104. package/src/lib/vite_plugin_pkg_json.ts +142 -0
  105. package/dist/package_helpers.d.ts +0 -150
  106. package/dist/package_helpers.d.ts.map +0 -1
  107. package/dist/package_helpers.js +0 -179
  108. package/src/lib/package_helpers.ts +0 -186
@@ -0,0 +1,378 @@
1
+ /**
2
+ * Inline-formatting handlers (bold, italic, strikethrough, inline code)
3
+ * for the streaming mdz parser.
4
+ *
5
+ * @module
6
+ */
7
+ import { ASTERISK, BACKTICK, NEWLINE, TILDE, UNDERSCORE, is_word_char } from './mdz_helpers.js';
8
+ import { accumulate_text, alloc_id, emit, ensure_paragraph, flush_text, offset, push_stack_entry, revert_above, } from './mdz_stream_parser_state.js';
9
+ // -- Bold --
10
+ export const try_bold = (state) => {
11
+ // we know we're at ** and there's no open Bold
12
+ if (state.pos + 1 >= state.buffer.length)
13
+ return false; // need more input
14
+ // need at least one char after ** to avoid empty bold
15
+ if (state.pos + 2 >= state.buffer.length)
16
+ return false;
17
+ flush_text(state);
18
+ ensure_paragraph(state);
19
+ const id = alloc_id(state);
20
+ const bold_start = offset(state);
21
+ emit(state, { type: 'open', id, node_type: 'Bold', start: bold_start });
22
+ push_stack_entry(state, id, 'Bold', bold_start, true, '**');
23
+ state.active_text_id = null;
24
+ state.pos += 2;
25
+ state.column += 2;
26
+ state.prev_char = ASTERISK;
27
+ return true;
28
+ };
29
+ /**
30
+ * Close a `Bold` container at the given stack index. The caller in
31
+ * `process_inline` already located the open via `find_open`, so we trust the
32
+ * index rather than re-walking the stack.
33
+ */
34
+ export const close_bold = (state, stack_idx) => {
35
+ flush_text(state);
36
+ // revert anything between the bold and the top of the stack
37
+ revert_above(state, stack_idx);
38
+ const entry = state.stack.pop();
39
+ // check for empty container — revert to literal text instead of closing
40
+ if (!entry.has_children) {
41
+ emit(state, {
42
+ type: 'revert',
43
+ id: entry.id,
44
+ replacement_text: entry.delimiter,
45
+ start: entry.start,
46
+ });
47
+ accumulate_text(state, '**', offset(state));
48
+ }
49
+ else {
50
+ emit(state, { type: 'close', id: entry.id, end: offset(state) + 2 });
51
+ }
52
+ state.active_text_id = null;
53
+ state.pos += 2;
54
+ state.column += 2;
55
+ state.prev_char = ASTERISK;
56
+ return true;
57
+ };
58
+ // -- Italic --
59
+ export const try_italic = (state, forced = false) => {
60
+ // check opening word boundary
61
+ if (state.prev_char !== -1 && is_word_char(state.prev_char)) {
62
+ // not at word boundary — emit as text
63
+ ensure_paragraph(state);
64
+ accumulate_text(state, '_', offset(state));
65
+ state.prev_char = UNDERSCORE;
66
+ state.column++;
67
+ state.pos++;
68
+ return true;
69
+ }
70
+ // need at least one char after _
71
+ if (state.pos + 1 >= state.buffer.length) {
72
+ if (forced) {
73
+ // at EOF with only _ left — treat as text
74
+ ensure_paragraph(state);
75
+ accumulate_text(state, '_', offset(state));
76
+ state.prev_char = UNDERSCORE;
77
+ state.column++;
78
+ state.pos++;
79
+ return true;
80
+ }
81
+ return false;
82
+ }
83
+ // Italic is non-optimistic: require confirmed closer in the buffer.
84
+ // This avoids flashing italic on common `_` usage (snake_case, identifiers).
85
+ // When the closer isn't visible yet (`pending`), we treat `_` as text rather
86
+ // than holding — holding would block all subsequent content from being
87
+ // parsed in normal mode, and at EOF the parser can't distinguish "still
88
+ // streaming" from "really done."
89
+ //
90
+ // Consequence: italic cannot span chunk boundaries when the closing `_`
91
+ // arrives in a later chunk. The one-shot path (the correctness gate) is
92
+ // always correct because the full buffer is visible. See the
93
+ // `deliberate-divergence` block in `mdz_parser_parity.test.ts` for an
94
+ // asserted gap.
95
+ const closer = scan_closer_boundary(state, '_');
96
+ if (closer !== 'confirmed') {
97
+ ensure_paragraph(state);
98
+ accumulate_text(state, '_', offset(state));
99
+ state.prev_char = UNDERSCORE;
100
+ state.column++;
101
+ state.pos++;
102
+ return true;
103
+ }
104
+ flush_text(state);
105
+ ensure_paragraph(state);
106
+ const id = alloc_id(state);
107
+ const italic_start = offset(state);
108
+ emit(state, { type: 'open', id, node_type: 'Italic', start: italic_start });
109
+ push_stack_entry(state, id, 'Italic', italic_start, true, '_');
110
+ state.active_text_id = null;
111
+ state.pos++;
112
+ state.column++;
113
+ state.prev_char = UNDERSCORE;
114
+ return true;
115
+ };
116
+ // -- Strikethrough --
117
+ export const try_strikethrough = (state) => {
118
+ // check opening word boundary
119
+ if (state.prev_char !== -1 && is_word_char(state.prev_char)) {
120
+ ensure_paragraph(state);
121
+ accumulate_text(state, '~', offset(state));
122
+ state.prev_char = TILDE;
123
+ state.column++;
124
+ state.pos++;
125
+ return true;
126
+ }
127
+ if (state.pos + 1 >= state.buffer.length)
128
+ return false;
129
+ // strikethrough is optimistic: 'pending' proceeds, only 'rejected' aborts
130
+ if (scan_closer_boundary(state, '~') === 'rejected') {
131
+ ensure_paragraph(state);
132
+ accumulate_text(state, '~', offset(state));
133
+ state.prev_char = TILDE;
134
+ state.column++;
135
+ state.pos++;
136
+ return true;
137
+ }
138
+ flush_text(state);
139
+ ensure_paragraph(state);
140
+ const id = alloc_id(state);
141
+ const strike_start = offset(state);
142
+ emit(state, { type: 'open', id, node_type: 'Strikethrough', start: strike_start });
143
+ push_stack_entry(state, id, 'Strikethrough', strike_start, true, '~');
144
+ state.active_text_id = null;
145
+ state.pos++;
146
+ state.column++;
147
+ state.prev_char = TILDE;
148
+ return true;
149
+ };
150
+ // -- Single delimiter close (italic/strikethrough) --
151
+ export const check_close_word_boundary = (state) => {
152
+ const next_pos = state.pos + 1;
153
+ if (next_pos < state.buffer.length) {
154
+ return !is_word_char(state.buffer.charCodeAt(next_pos));
155
+ }
156
+ // at end of buffer — we'll treat as boundary for now (finish() handles unclosed)
157
+ return true;
158
+ };
159
+ export const close_single_delimiter = (state, stack_idx) => {
160
+ flush_text(state);
161
+ revert_above(state, stack_idx);
162
+ const entry = state.stack.pop();
163
+ // check for empty container — revert to literal text instead of closing
164
+ if (!entry.has_children) {
165
+ emit(state, {
166
+ type: 'revert',
167
+ id: entry.id,
168
+ replacement_text: entry.delimiter,
169
+ start: entry.start,
170
+ });
171
+ accumulate_text(state, entry.delimiter, offset(state));
172
+ }
173
+ else {
174
+ emit(state, { type: 'close', id: entry.id, end: offset(state) + 1 });
175
+ }
176
+ state.active_text_id = null;
177
+ state.pos++;
178
+ state.column++;
179
+ state.prev_char = state.buffer.charCodeAt(state.pos - 1);
180
+ return true;
181
+ };
182
+ /**
183
+ * Scan for the first potential closing delimiter and check its word boundary.
184
+ * Returns a tri-state:
185
+ * - `'confirmed'`: closer found in buffer with valid word boundary
186
+ * - `'rejected'`: closer found but with invalid word boundary (greedy first-match fails)
187
+ * - `'pending'`: no closer in buffer yet, or can't determine boundary
188
+ *
189
+ * Optimistic constructs (strikethrough) treat `'pending'` as proceed.
190
+ * Non-optimistic constructs (italic) treat `'pending'` as text.
191
+ *
192
+ * Uses native `indexOf` instead of a manual char loop for faster scanning.
193
+ */
194
+ export const scan_closer_boundary = (state, delimiter) => {
195
+ const search_start = state.pos + 1;
196
+ const delim_pos = state.buffer.indexOf(delimiter, search_start);
197
+ if (delim_pos === -1)
198
+ return 'pending'; // no closer in buffer
199
+ // check for paragraph break before the delimiter
200
+ const para_pos = state.buffer.indexOf('\n\n', search_start);
201
+ if (para_pos !== -1 && para_pos < delim_pos)
202
+ return 'pending'; // paragraph break first
203
+ // check word boundary after the closer
204
+ const after_pos = delim_pos + 1;
205
+ if (after_pos < state.buffer.length) {
206
+ return is_word_char(state.buffer.charCodeAt(after_pos)) ? 'rejected' : 'confirmed';
207
+ }
208
+ // closer at end of buffer — treat as confirmed, consistent with
209
+ // check_close_word_boundary which also assumes boundary at buffer end
210
+ return 'confirmed';
211
+ };
212
+ // -- Inline code --
213
+ export const try_code = (state) => {
214
+ const start = state.pos;
215
+ let i = start + 1; // past opening backtick
216
+ // compute search boundary: don't scan past open formatting delimiters
217
+ const search_limit = code_search_limit(state, start + 1);
218
+ // scan for closing backtick (must be before newline and search boundary)
219
+ while (i < state.buffer.length && i < search_limit) {
220
+ const c = state.buffer.charCodeAt(i);
221
+ if (c === BACKTICK) {
222
+ // found close
223
+ const content = state.buffer.slice(start + 1, i);
224
+ if (content.length === 0) {
225
+ // empty code — treat as text
226
+ ensure_paragraph(state);
227
+ accumulate_text(state, '``', offset(state, start));
228
+ state.pos = i + 1;
229
+ state.column += 2;
230
+ state.prev_char = BACKTICK;
231
+ return true;
232
+ }
233
+ flush_text(state);
234
+ ensure_paragraph(state);
235
+ const id = alloc_id(state);
236
+ emit(state, {
237
+ type: 'text',
238
+ id,
239
+ content,
240
+ text_type: 'Code',
241
+ start: offset(state, start),
242
+ end: offset(state, i + 1),
243
+ });
244
+ state.active_text_id = null;
245
+ state.pos = i + 1;
246
+ state.column += i + 1 - start;
247
+ state.prev_char = BACKTICK;
248
+ return true;
249
+ }
250
+ if (c === NEWLINE) {
251
+ // inline code can't span lines — treat opening backtick as text
252
+ ensure_paragraph(state);
253
+ accumulate_text(state, '`', offset(state, start));
254
+ state.pos = start + 1;
255
+ state.column++;
256
+ state.prev_char = BACKTICK;
257
+ return true;
258
+ }
259
+ i++;
260
+ }
261
+ // hit search limit (open formatting delimiter boundary) — treat backtick as text
262
+ if (i < state.buffer.length && i >= search_limit) {
263
+ ensure_paragraph(state);
264
+ accumulate_text(state, '`', offset(state, start));
265
+ state.pos = start + 1;
266
+ state.column++;
267
+ state.prev_char = BACKTICK;
268
+ return true;
269
+ }
270
+ // buffer ended without close or newline.
271
+ // If formatting is on the stack, hold — the formatting delimiter might appear
272
+ // in the next chunk between the backtick and its closer, and going optimistic
273
+ // would make the code span consume the formatting closer as raw text.
274
+ if (has_inline_formatting_on_stack(state)) {
275
+ return false; // hold for more input
276
+ }
277
+ // no formatting on stack — open Code optimistically.
278
+ // Content streams via text/append_text inside the container.
279
+ // Closes on matching backtick, reverts on newline.
280
+ flush_text(state);
281
+ ensure_paragraph(state);
282
+ const id = alloc_id(state);
283
+ const code_start = offset(state, start);
284
+ emit(state, { type: 'open', id, node_type: 'Code', start: code_start });
285
+ push_stack_entry(state, id, 'Code', code_start, true, '`');
286
+ state.in_code = true;
287
+ state.active_text_id = null;
288
+ state.pos = start + 1; // past opening backtick
289
+ state.column++;
290
+ state.prev_char = BACKTICK;
291
+ return true;
292
+ };
293
+ /**
294
+ * Compute search limit for inline code scanning.
295
+ * Finds the closest closing delimiter for any open formatting on the stack.
296
+ * This prevents backtick scanning from crossing bold/italic/etc boundaries.
297
+ */
298
+ export const code_search_limit = (state, from) => {
299
+ let limit = state.buffer.length;
300
+ for (let s = state.stack.length - 1; s >= 0; s--) {
301
+ const entry = state.stack[s];
302
+ if (entry.node_type === 'Paragraph' || entry.node_type === 'Heading')
303
+ break;
304
+ // find the closing delimiter in the buffer
305
+ const delimiter = entry.delimiter;
306
+ if (!delimiter)
307
+ continue;
308
+ const idx = state.buffer.indexOf(delimiter, from);
309
+ if (idx !== -1 && idx < limit) {
310
+ limit = idx;
311
+ }
312
+ }
313
+ return limit;
314
+ };
315
+ /**
316
+ * Check if any inline formatting container (Bold, Italic, Strikethrough, Link)
317
+ * is on the stack above the current block boundary.
318
+ * Used to decide whether backtick can go optimistic — when formatting is open,
319
+ * the code span might cross the formatting boundary, so we hold instead.
320
+ */
321
+ export const has_inline_formatting_on_stack = (state) => {
322
+ for (let s = state.stack.length - 1; s >= 0; s--) {
323
+ const entry = state.stack[s];
324
+ if (entry.node_type === 'Paragraph' || entry.node_type === 'Heading')
325
+ return false;
326
+ if (entry.delimiter)
327
+ return true;
328
+ }
329
+ return false;
330
+ };
331
+ /**
332
+ * Process content inside an optimistic inline Code container.
333
+ * Scans raw text (no inline formatting) until closing backtick or newline.
334
+ * Backtick closes (or reverts if empty). Newline reverts (code can't span lines).
335
+ */
336
+ export const process_code_content = (state) => {
337
+ const char_code = state.buffer.charCodeAt(state.pos);
338
+ if (char_code === BACKTICK) {
339
+ flush_text(state);
340
+ const entry = state.stack.pop();
341
+ if (!entry.has_children) {
342
+ // empty code `` — revert opener, accumulate closer as text
343
+ emit(state, { type: 'revert', id: entry.id, replacement_text: '`', start: entry.start });
344
+ accumulate_text(state, '`', offset(state));
345
+ }
346
+ else {
347
+ emit(state, { type: 'close', id: entry.id, end: offset(state) + 1 });
348
+ }
349
+ state.in_code = false;
350
+ state.active_text_id = null;
351
+ state.pos++;
352
+ state.column++;
353
+ state.prev_char = BACKTICK;
354
+ return;
355
+ }
356
+ if (char_code === NEWLINE) {
357
+ // code spans can't cross lines — revert
358
+ flush_text(state);
359
+ const entry = state.stack.pop();
360
+ emit(state, { type: 'revert', id: entry.id, replacement_text: '`', start: entry.start });
361
+ state.in_code = false;
362
+ state.active_text_id = null;
363
+ // don't consume newline — outer loop handles it
364
+ return;
365
+ }
366
+ // scan raw text until backtick or newline (paragraph already ensured at code open)
367
+ const start = state.pos;
368
+ state.pos++;
369
+ while (state.pos < state.buffer.length) {
370
+ const c = state.buffer.charCodeAt(state.pos);
371
+ if (c === BACKTICK || c === NEWLINE)
372
+ break;
373
+ state.pos++;
374
+ }
375
+ accumulate_text(state, state.buffer.slice(start, state.pos), offset(state, start));
376
+ state.prev_char = state.buffer.charCodeAt(state.pos - 1);
377
+ state.column += state.pos - start;
378
+ };
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Markdown link `[text](url)` handlers and HTML-style tag handlers
3
+ * (`<tag>`, `<Tag />`, `</tag>`) for the streaming mdz parser.
4
+ *
5
+ * @module
6
+ */
7
+ import { type MdzStreamParserState, type TryResult } from './mdz_stream_parser_state.js';
8
+ export declare const try_link_open: (state: MdzStreamParserState) => boolean;
9
+ /**
10
+ * Revert an optimistic Link container to its literal `[` and consume the
11
+ * `]` at `bracket_pos` as text. Used whenever a `]` doesn't form a valid
12
+ * `[text](url)` — missing `(`, empty url, invalid url char, etc.
13
+ */
14
+ export declare const abort_link_to_text: (state: MdzStreamParserState, link_stack_idx: number, bracket_pos: number) => void;
15
+ /**
16
+ * Try to complete a link after seeing `]`.
17
+ * Returns false if more input is needed (leaves pos at `]`).
18
+ */
19
+ export declare const try_complete_link: (state: MdzStreamParserState, link_stack_idx: number) => boolean;
20
+ export declare const try_tag_open: (state: MdzStreamParserState) => TryResult;
21
+ export declare const try_close_tag: (state: MdzStreamParserState) => TryResult;
22
+ //# sourceMappingURL=mdz_stream_parser_link.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mdz_stream_parser_link.d.ts","sourceRoot":"../src/lib/","sources":["../src/lib/mdz_stream_parser_link.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAoBH,OAAO,EACN,KAAK,oBAAoB,EACzB,KAAK,SAAS,EASd,MAAM,8BAA8B,CAAC;AAItC,eAAO,MAAM,aAAa,GAAI,OAAO,oBAAoB,KAAG,OAa3D,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,kBAAkB,GAC9B,OAAO,oBAAoB,EAC3B,gBAAgB,MAAM,EACtB,aAAa,MAAM,KACjB,IAUF,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,iBAAiB,GAAI,OAAO,oBAAoB,EAAE,gBAAgB,MAAM,KAAG,OAwEvF,CAAC;AAIF,eAAO,MAAM,YAAY,GAAI,OAAO,oBAAoB,KAAG,SAsE1D,CAAC;AAEF,eAAO,MAAM,aAAa,GAAI,OAAO,oBAAoB,KAAG,SA0C3D,CAAC"}
@@ -0,0 +1,215 @@
1
+ /**
2
+ * Markdown link `[text](url)` handlers and HTML-style tag handlers
3
+ * (`<tag>`, `<Tag />`, `</tag>`) for the streaming mdz parser.
4
+ *
5
+ * @module
6
+ */
7
+ import { A_UPPER, LEFT_BRACKET, LEFT_PAREN, NEWLINE, RIGHT_ANGLE, RIGHT_BRACKET, RIGHT_PAREN, SLASH, SPACE, Z_UPPER, is_letter, is_tag_name_char, is_valid_path_char, mdz_is_safe_reference, mdz_is_url, } from './mdz_helpers.js';
8
+ import { accumulate_text, alloc_id, emit, ensure_paragraph, flush_text, offset, push_stack_entry, revert_above, } from './mdz_stream_parser_state.js';
9
+ // -- Links --
10
+ export const try_link_open = (state) => {
11
+ flush_text(state);
12
+ ensure_paragraph(state);
13
+ const id = alloc_id(state);
14
+ const link_start = offset(state);
15
+ emit(state, { type: 'open', id, node_type: 'Link', start: link_start });
16
+ push_stack_entry(state, id, 'Link', link_start, true, '[');
17
+ state.active_text_id = null;
18
+ state.pos++;
19
+ state.column++;
20
+ state.prev_char = LEFT_BRACKET;
21
+ return true;
22
+ };
23
+ /**
24
+ * Revert an optimistic Link container to its literal `[` and consume the
25
+ * `]` at `bracket_pos` as text. Used whenever a `]` doesn't form a valid
26
+ * `[text](url)` — missing `(`, empty url, invalid url char, etc.
27
+ */
28
+ export const abort_link_to_text = (state, link_stack_idx, bracket_pos) => {
29
+ flush_text(state);
30
+ revert_above(state, link_stack_idx);
31
+ const entry = state.stack.pop();
32
+ emit(state, { type: 'revert', id: entry.id, replacement_text: '[', start: entry.start });
33
+ state.active_text_id = null;
34
+ accumulate_text(state, ']', offset(state, bracket_pos));
35
+ state.pos = bracket_pos + 1;
36
+ state.column++;
37
+ state.prev_char = RIGHT_BRACKET;
38
+ };
39
+ /**
40
+ * Try to complete a link after seeing `]`.
41
+ * Returns false if more input is needed (leaves pos at `]`).
42
+ */
43
+ export const try_complete_link = (state, link_stack_idx) => {
44
+ const bracket_pos = state.pos;
45
+ // need at least one char after ]
46
+ if (bracket_pos + 1 >= state.buffer.length)
47
+ return false;
48
+ const after_bracket = state.buffer.charCodeAt(bracket_pos + 1);
49
+ if (after_bracket !== LEFT_PAREN) {
50
+ abort_link_to_text(state, link_stack_idx, bracket_pos);
51
+ return true;
52
+ }
53
+ // found ]( — scan for )
54
+ let i = bracket_pos + 2; // past ](
55
+ while (i < state.buffer.length) {
56
+ const c = state.buffer.charCodeAt(i);
57
+ if (c === RIGHT_PAREN) {
58
+ const reference = state.buffer.slice(bracket_pos + 2, i);
59
+ // validate reference: non-empty and only valid path chars
60
+ if (!reference.trim()) {
61
+ abort_link_to_text(state, link_stack_idx, bracket_pos);
62
+ return true;
63
+ }
64
+ let valid = true;
65
+ for (let j = 0; j < reference.length; j++) {
66
+ if (!is_valid_path_char(reference.charCodeAt(j))) {
67
+ valid = false;
68
+ break;
69
+ }
70
+ }
71
+ if (!valid) {
72
+ abort_link_to_text(state, link_stack_idx, bracket_pos);
73
+ return true;
74
+ }
75
+ // Reject unsafe protocols (javascript:, data:, etc.) at parse time.
76
+ // `is_valid_path_char` permits `:` so we have to filter explicitly.
77
+ if (!mdz_is_safe_reference(reference)) {
78
+ abort_link_to_text(state, link_stack_idx, bracket_pos);
79
+ return true;
80
+ }
81
+ // success — close the link with reference
82
+ flush_text(state);
83
+ revert_above(state, link_stack_idx);
84
+ const entry = state.stack.pop();
85
+ const link_type = mdz_is_url(reference) ? 'external' : 'internal';
86
+ emit(state, {
87
+ type: 'close',
88
+ id: entry.id,
89
+ end: offset(state, i + 1),
90
+ reference,
91
+ link_type,
92
+ });
93
+ state.active_text_id = null;
94
+ state.pos = i + 1;
95
+ state.column += i + 1 - bracket_pos;
96
+ state.prev_char = RIGHT_PAREN;
97
+ return true;
98
+ }
99
+ if (c === NEWLINE || c === SPACE) {
100
+ // invalid character in URL (simplified check)
101
+ break;
102
+ }
103
+ i++;
104
+ }
105
+ // didn't find ) — need more input or invalid
106
+ if (i >= state.buffer.length)
107
+ return false; // need more input
108
+ // found invalid char before ) — revert
109
+ abort_link_to_text(state, link_stack_idx, bracket_pos);
110
+ return true;
111
+ };
112
+ // -- Tags --
113
+ export const try_tag_open = (state) => {
114
+ const start = state.pos;
115
+ let i = start + 1; // past <
116
+ // tag name must start with letter
117
+ if (i >= state.buffer.length)
118
+ return 'not_match'; // treat as text via fallthrough
119
+ if (!is_letter(state.buffer.charCodeAt(i))) {
120
+ return 'not_match';
121
+ }
122
+ // collect tag name
123
+ const name_start = i;
124
+ while (i < state.buffer.length && is_tag_name_char(state.buffer.charCodeAt(i))) {
125
+ i++;
126
+ }
127
+ if (i >= state.buffer.length)
128
+ return 'need_more';
129
+ const name = state.buffer.slice(name_start, i);
130
+ // skip whitespace
131
+ while (i < state.buffer.length && state.buffer.charCodeAt(i) === SPACE) {
132
+ i++;
133
+ }
134
+ if (i >= state.buffer.length)
135
+ return 'need_more';
136
+ // check for self-closing />
137
+ if (state.buffer.charCodeAt(i) === SLASH &&
138
+ i + 1 < state.buffer.length &&
139
+ state.buffer.charCodeAt(i + 1) === RIGHT_ANGLE) {
140
+ const first_char = name.charCodeAt(0);
141
+ const is_component = first_char >= A_UPPER && first_char <= Z_UPPER;
142
+ const node_type = is_component ? 'Component' : 'Element';
143
+ flush_text(state);
144
+ ensure_paragraph(state);
145
+ const id = alloc_id(state);
146
+ const tag_start = offset(state, start);
147
+ emit(state, { type: 'open', id, node_type, start: tag_start, name });
148
+ emit(state, { type: 'close', id, end: offset(state, i + 2) });
149
+ state.active_text_id = null;
150
+ state.pos = i + 2;
151
+ state.column += i + 2 - start;
152
+ state.prev_char = RIGHT_ANGLE;
153
+ return 'consumed';
154
+ }
155
+ // check for >
156
+ if (state.buffer.charCodeAt(i) !== RIGHT_ANGLE) {
157
+ return 'not_match';
158
+ }
159
+ i++; // past >
160
+ const first_char = name.charCodeAt(0);
161
+ const is_component = first_char >= A_UPPER && first_char <= Z_UPPER;
162
+ const node_type = is_component ? 'Component' : 'Element';
163
+ flush_text(state);
164
+ ensure_paragraph(state);
165
+ const id = alloc_id(state);
166
+ const tag_start = offset(state, start);
167
+ const delimiter = state.buffer.slice(start, i); // e.g., "<Alert>"
168
+ emit(state, { type: 'open', id, node_type, start: tag_start, name });
169
+ push_stack_entry(state, id, node_type, tag_start, true, delimiter, name);
170
+ state.active_text_id = null;
171
+ state.pos = i;
172
+ state.column += i - start;
173
+ state.prev_char = RIGHT_ANGLE;
174
+ return 'consumed';
175
+ };
176
+ export const try_close_tag = (state) => {
177
+ // we're at </
178
+ let i = state.pos + 2; // past </
179
+ if (i >= state.buffer.length)
180
+ return 'need_more';
181
+ // collect tag name
182
+ const name_start = i;
183
+ while (i < state.buffer.length && is_tag_name_char(state.buffer.charCodeAt(i))) {
184
+ i++;
185
+ }
186
+ if (i >= state.buffer.length)
187
+ return 'need_more';
188
+ if (state.buffer.charCodeAt(i) !== RIGHT_ANGLE)
189
+ return 'not_match';
190
+ const name = state.buffer.slice(name_start, i);
191
+ i++; // past >
192
+ // find matching open tag on stack
193
+ let found_idx = -1;
194
+ for (let j = state.stack.length - 1; j >= 0; j--) {
195
+ const entry = state.stack[j];
196
+ if ((entry.node_type === 'Element' || entry.node_type === 'Component') &&
197
+ entry.tag_name === name) {
198
+ found_idx = j;
199
+ break;
200
+ }
201
+ if (entry.node_type === 'Paragraph' || entry.node_type === 'Heading')
202
+ break;
203
+ }
204
+ if (found_idx === -1)
205
+ return 'not_match';
206
+ flush_text(state);
207
+ revert_above(state, found_idx);
208
+ const entry = state.stack.pop();
209
+ emit(state, { type: 'close', id: entry.id, end: offset(state, i) });
210
+ state.active_text_id = null;
211
+ state.column += i - state.pos;
212
+ state.pos = i;
213
+ state.prev_char = RIGHT_ANGLE;
214
+ return 'consumed';
215
+ };