@fuzdev/mdz 0.1.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 (101) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +32 -0
  3. package/dist/Mdz.svelte +39 -0
  4. package/dist/Mdz.svelte.d.ts +17 -0
  5. package/dist/Mdz.svelte.d.ts.map +1 -0
  6. package/dist/MdzNodeView.svelte +140 -0
  7. package/dist/MdzNodeView.svelte.d.ts +9 -0
  8. package/dist/MdzNodeView.svelte.d.ts.map +1 -0
  9. package/dist/MdzPrecompiled.svelte +33 -0
  10. package/dist/MdzPrecompiled.svelte.d.ts +17 -0
  11. package/dist/MdzPrecompiled.svelte.d.ts.map +1 -0
  12. package/dist/MdzRoot.svelte +40 -0
  13. package/dist/MdzRoot.svelte.d.ts +14 -0
  14. package/dist/MdzRoot.svelte.d.ts.map +1 -0
  15. package/dist/MdzStream.svelte +37 -0
  16. package/dist/MdzStream.svelte.d.ts +18 -0
  17. package/dist/MdzStream.svelte.d.ts.map +1 -0
  18. package/dist/MdzStreamNodeView.svelte +150 -0
  19. package/dist/MdzStreamNodeView.svelte.d.ts +9 -0
  20. package/dist/MdzStreamNodeView.svelte.d.ts.map +1 -0
  21. package/dist/context_helpers.d.ts +23 -0
  22. package/dist/context_helpers.d.ts.map +1 -0
  23. package/dist/context_helpers.js +19 -0
  24. package/dist/mdz.d.ts +152 -0
  25. package/dist/mdz.d.ts.map +1 -0
  26. package/dist/mdz.js +61 -0
  27. package/dist/mdz_contexts.d.ts +106 -0
  28. package/dist/mdz_contexts.d.ts.map +1 -0
  29. package/dist/mdz_contexts.js +42 -0
  30. package/dist/mdz_helpers.d.ts +321 -0
  31. package/dist/mdz_helpers.d.ts.map +1 -0
  32. package/dist/mdz_helpers.js +668 -0
  33. package/dist/mdz_lexer.d.ts +123 -0
  34. package/dist/mdz_lexer.d.ts.map +1 -0
  35. package/dist/mdz_lexer.js +1405 -0
  36. package/dist/mdz_opcodes.d.ts +193 -0
  37. package/dist/mdz_opcodes.d.ts.map +1 -0
  38. package/dist/mdz_opcodes.js +14 -0
  39. package/dist/mdz_opcodes_to_nodes.d.ts +26 -0
  40. package/dist/mdz_opcodes_to_nodes.d.ts.map +1 -0
  41. package/dist/mdz_opcodes_to_nodes.js +410 -0
  42. package/dist/mdz_stream_parser.d.ts +109 -0
  43. package/dist/mdz_stream_parser.d.ts.map +1 -0
  44. package/dist/mdz_stream_parser.js +699 -0
  45. package/dist/mdz_stream_parser_block.d.ts +99 -0
  46. package/dist/mdz_stream_parser_block.d.ts.map +1 -0
  47. package/dist/mdz_stream_parser_block.js +437 -0
  48. package/dist/mdz_stream_parser_inline.d.ts +98 -0
  49. package/dist/mdz_stream_parser_inline.d.ts.map +1 -0
  50. package/dist/mdz_stream_parser_inline.js +481 -0
  51. package/dist/mdz_stream_parser_link.d.ts +29 -0
  52. package/dist/mdz_stream_parser_link.d.ts.map +1 -0
  53. package/dist/mdz_stream_parser_link.js +227 -0
  54. package/dist/mdz_stream_parser_list.d.ts +106 -0
  55. package/dist/mdz_stream_parser_list.d.ts.map +1 -0
  56. package/dist/mdz_stream_parser_list.js +438 -0
  57. package/dist/mdz_stream_parser_state.d.ts +476 -0
  58. package/dist/mdz_stream_parser_state.d.ts.map +1 -0
  59. package/dist/mdz_stream_parser_state.js +651 -0
  60. package/dist/mdz_stream_parser_text.d.ts +25 -0
  61. package/dist/mdz_stream_parser_text.d.ts.map +1 -0
  62. package/dist/mdz_stream_parser_text.js +67 -0
  63. package/dist/mdz_stream_parser_url.d.ts +76 -0
  64. package/dist/mdz_stream_parser_url.d.ts.map +1 -0
  65. package/dist/mdz_stream_parser_url.js +328 -0
  66. package/dist/mdz_stream_state.svelte.d.ts +53 -0
  67. package/dist/mdz_stream_state.svelte.d.ts.map +1 -0
  68. package/dist/mdz_stream_state.svelte.js +360 -0
  69. package/dist/mdz_to_svelte.d.ts +71 -0
  70. package/dist/mdz_to_svelte.d.ts.map +1 -0
  71. package/dist/mdz_to_svelte.js +127 -0
  72. package/dist/mdz_token_parser.d.ts +20 -0
  73. package/dist/mdz_token_parser.d.ts.map +1 -0
  74. package/dist/mdz_token_parser.js +449 -0
  75. package/dist/svelte_preprocess_mdz.d.ts +80 -0
  76. package/dist/svelte_preprocess_mdz.d.ts.map +1 -0
  77. package/dist/svelte_preprocess_mdz.js +543 -0
  78. package/dist/tsdoc_mdz.d.ts +45 -0
  79. package/dist/tsdoc_mdz.d.ts.map +1 -0
  80. package/dist/tsdoc_mdz.js +88 -0
  81. package/package.json +131 -0
  82. package/src/lib/context_helpers.ts +52 -0
  83. package/src/lib/mdz.ts +190 -0
  84. package/src/lib/mdz_contexts.ts +99 -0
  85. package/src/lib/mdz_helpers.ts +727 -0
  86. package/src/lib/mdz_lexer.ts +1761 -0
  87. package/src/lib/mdz_opcodes.ts +227 -0
  88. package/src/lib/mdz_opcodes_to_nodes.ts +470 -0
  89. package/src/lib/mdz_stream_parser.ts +766 -0
  90. package/src/lib/mdz_stream_parser_block.ts +490 -0
  91. package/src/lib/mdz_stream_parser_inline.ts +527 -0
  92. package/src/lib/mdz_stream_parser_link.ts +283 -0
  93. package/src/lib/mdz_stream_parser_list.ts +525 -0
  94. package/src/lib/mdz_stream_parser_state.ts +957 -0
  95. package/src/lib/mdz_stream_parser_text.ts +95 -0
  96. package/src/lib/mdz_stream_parser_url.ts +386 -0
  97. package/src/lib/mdz_stream_state.svelte.ts +383 -0
  98. package/src/lib/mdz_to_svelte.ts +213 -0
  99. package/src/lib/mdz_token_parser.ts +544 -0
  100. package/src/lib/svelte_preprocess_mdz.ts +777 -0
  101. package/src/lib/tsdoc_mdz.ts +97 -0
@@ -0,0 +1,527 @@
1
+ /**
2
+ * Inline-formatting handlers (bold, italic, strikethrough, inline code)
3
+ * for the streaming mdz parser.
4
+ *
5
+ * @module
6
+ */
7
+
8
+ import {
9
+ BACKTICK,
10
+ NEWLINE,
11
+ RIGHT_ANGLE,
12
+ UNDERSCORE,
13
+ is_line_whitespace,
14
+ is_word_char,
15
+ mdz_blockquote_line_has_content,
16
+ mdz_match_blockquote_prefix,
17
+ } from './mdz_helpers.js';
18
+ import {
19
+ type MdzStreamParserState,
20
+ accumulate_text,
21
+ alloc_id,
22
+ buffer_index_of,
23
+ emit,
24
+ ensure_paragraph,
25
+ flush_text,
26
+ is_run_boundary,
27
+ offset,
28
+ pop_stack_entry,
29
+ push_stack_entry,
30
+ revert_above,
31
+ } from './mdz_stream_parser_state.js';
32
+ import {match_fence_opener, match_list_marker} from './mdz_stream_parser_list.js';
33
+ import {consume_delimiter_as_text} from './mdz_stream_parser_text.js';
34
+
35
+ /**
36
+ * Whether `buffer` contains a paragraph break — a newline followed (across
37
+ * optional line whitespace) by another newline — anywhere in `[from, to)`,
38
+ * both newlines inside the window. The streaming analogue of the lexer's
39
+ * `#has_paragraph_break_between`; callers bound `to` to a found delimiter,
40
+ * keeping the probe amortized (see `scan_closer_boundary`). A break can't
41
+ * straddle `to`: the delimiter there is never whitespace, so a whitespace
42
+ * run starting inside the window ends inside it.
43
+ */
44
+ const has_paragraph_break_between = (buffer: string, from: number, to: number): boolean => {
45
+ for (let i = from; i < to - 1; i++) {
46
+ if (buffer.charCodeAt(i) === NEWLINE) {
47
+ let j = i + 1;
48
+ while (j < to && is_line_whitespace(buffer.charCodeAt(j))) {
49
+ j++;
50
+ }
51
+ if (j < to && buffer.charCodeAt(j) === NEWLINE) {
52
+ return true;
53
+ }
54
+ i = j - 1; // skip the scanned whitespace run (loop increment lands on j)
55
+ }
56
+ }
57
+ return false;
58
+ };
59
+
60
+ /**
61
+ * Whether the current inline run ends anywhere in `[from, to)` — a paragraph
62
+ * break, or (inside an open list) a line that classifies as list structure
63
+ * rather than continuation: blank, at or above the deepest marker's indent,
64
+ * a nesting marker (subject to the ordered interrupt guard, which makes a
65
+ * deeper non-`1.` ordered marker continuation text), a fence opener line, or
66
+ * a quote opener line.
67
+ * The streaming counterpart of the sync lexer's `#max_search_index` run
68
+ * bound: a closer candidate past a run boundary can never close.
69
+ *
70
+ * Same amortization as the paragraph-break probe — callers bound `to` to a
71
+ * found delimiter, so consecutive scans cover disjoint windows, and the
72
+ * per-line classification reads only bounded prefixes. A line that can't yet
73
+ * classify (cut off at the buffer end) reports no boundary: while streaming
74
+ * the optimistic open it permits is corrected by the run-close revert, and
75
+ * in forced mode the buffer is complete so every line classifies.
76
+ */
77
+ const has_run_boundary_between = (
78
+ state: MdzStreamParserState,
79
+ from: number,
80
+ to: number,
81
+ forced: boolean,
82
+ ): boolean => {
83
+ const {buffer} = state;
84
+ if (state.list_levels.length === 0) {
85
+ return has_paragraph_break_between(buffer, from, to);
86
+ }
87
+ const deepest_indent = state.list_levels[state.list_levels.length - 1]!.indent;
88
+ let i = buffer.indexOf('\n', from);
89
+ while (i !== -1 && i < to) {
90
+ const line_start = i + 1;
91
+ let j = line_start;
92
+ while (j < buffer.length && is_line_whitespace(buffer.charCodeAt(j))) {
93
+ j++;
94
+ }
95
+ if (j >= buffer.length) return false; // unresolved line (only reachable while streaming)
96
+ if (buffer.charCodeAt(j) === NEWLINE) return true; // blank line ends the run
97
+ if (j - line_start <= deepest_indent) return true; // sibling, dedent, or list end
98
+ const marker = match_list_marker(state, j, forced);
99
+ if (marker === 'pending') return false;
100
+ if (marker !== null && !marker.empty && (!marker.ordered || marker.number === 1)) {
101
+ return true; // a nested list opens
102
+ }
103
+ if (buffer.charCodeAt(j) === BACKTICK) {
104
+ const fence = match_fence_opener(state, j, forced);
105
+ if (fence === 'pending') return false;
106
+ if (fence !== null) return true; // a fence opener interrupts the run
107
+ }
108
+ if (buffer.charCodeAt(j) === RIGHT_ANGLE) {
109
+ const prefix = mdz_match_blockquote_prefix(buffer, j, forced);
110
+ if (prefix === 'pending') return false;
111
+ if (prefix !== null) {
112
+ const has_content = mdz_blockquote_line_has_content(buffer, prefix.content_start, forced);
113
+ if (has_content === 'pending') return false;
114
+ if (has_content) return true; // a quote opener interrupts the run
115
+ }
116
+ }
117
+ i = buffer.indexOf('\n', j); // continuation — classify the next line
118
+ }
119
+ return false;
120
+ };
121
+
122
+ // -- Bold / strikethrough (doubled-delimiter inlines) --
123
+
124
+ /**
125
+ * Whether a closing doubled delimiter exists in the remaining buffer with no
126
+ * run boundary before it — the one-shot parse's open condition (the lexer's
127
+ * `#tokenize_double_delimiter` under its `#max_search_index` bound). Only
128
+ * consulted in forced mode, where the buffer is complete so a missing closer
129
+ * is definitive. The boundary probe is bounded to the found-closer window,
130
+ * same as `scan_closer_boundary`.
131
+ */
132
+ const has_double_closer = (state: MdzStreamParserState, delimiter: '**' | '~~'): boolean => {
133
+ const search_start = state.pos + 2;
134
+ const buffer = state.buffer;
135
+ const close_index = buffer.indexOf(delimiter, search_start);
136
+ if (close_index === -1) return false;
137
+ return !has_run_boundary_between(state, search_start, close_index, true);
138
+ };
139
+
140
+ /**
141
+ * Try to open a doubled-delimiter inline (`**` bold, `~~` strikethrough) —
142
+ * the streaming analogue of the lexer's `#tokenize_double_delimiter`. No
143
+ * word-boundary checks, matching the one-shot parse: doubled delimiters are
144
+ * unambiguous enough to pair anywhere, and the first closer wins.
145
+ *
146
+ * @nodocs
147
+ */
148
+ export const try_double_delimiter = (
149
+ state: MdzStreamParserState,
150
+ delimiter: '**' | '~~',
151
+ node_type: 'Bold' | 'Strikethrough',
152
+ forced = false,
153
+ ): boolean => {
154
+ // we know we're at the doubled delimiter (the caller checked both chars)
155
+ // and there's no matching open container
156
+ if (forced) {
157
+ // at EOF the closer scan has full information: open only when the
158
+ // one-shot parse would, otherwise the delimiter is literal text
159
+ if (!has_double_closer(state, delimiter)) {
160
+ consume_delimiter_as_text(state, delimiter);
161
+ return true;
162
+ }
163
+ } else if (state.pos + 2 >= state.buffer.length) {
164
+ // need at least one char after the delimiter to avoid an empty container
165
+ return false;
166
+ }
167
+
168
+ flush_text(state);
169
+ ensure_paragraph(state);
170
+
171
+ const id = alloc_id(state);
172
+ const start = offset(state);
173
+ emit(state, {type: 'open', id, node_type, start});
174
+ push_stack_entry(state, id, node_type, start, delimiter);
175
+ state.active_text_id = null;
176
+ state.pos += 2;
177
+ state.column += 2;
178
+ state.prev_char = delimiter.charCodeAt(1);
179
+ return true;
180
+ };
181
+
182
+ // -- Italic (single-delimiter inline) --
183
+
184
+ /**
185
+ * Try to open an italic span (`_`) — the streaming analogue of the lexer's
186
+ * `#tokenize_italic`.
187
+ *
188
+ * @nodocs
189
+ */
190
+ export const try_italic = (state: MdzStreamParserState, forced = false): boolean => {
191
+ // opening boundary: reject after a word char, or after the delimiter
192
+ // itself — `_` is self-opaque, so runs like `__init__` stay literal
193
+ if (state.prev_char !== -1 && (is_word_char(state.prev_char) || state.prev_char === UNDERSCORE)) {
194
+ consume_delimiter_as_text(state, '_');
195
+ return true;
196
+ }
197
+
198
+ // need at least one char after the delimiter
199
+ if (state.pos + 1 >= state.buffer.length) {
200
+ if (forced) {
201
+ // at EOF with only the delimiter left — treat as text
202
+ consume_delimiter_as_text(state, '_');
203
+ return true;
204
+ }
205
+ return false;
206
+ }
207
+
208
+ // greedy first-closer check: a visible closer with an invalid boundary
209
+ // kills the open ('rejected', matching the one-shot parse). No closer in
210
+ // the buffer ('pending') opens optimistically while streaming — the revert
211
+ // machinery corrects at the first failed closer or run close — but
212
+ // at EOF (forced) nothing can ever close it, so degrade to text.
213
+ const closer = scan_closer_boundary(state, forced);
214
+ if (closer === 'rejected' || (forced && closer === 'pending')) {
215
+ consume_delimiter_as_text(state, '_');
216
+ return true;
217
+ }
218
+
219
+ flush_text(state);
220
+ ensure_paragraph(state);
221
+
222
+ const id = alloc_id(state);
223
+ const start = offset(state);
224
+ emit(state, {type: 'open', id, node_type: 'Italic', start});
225
+ push_stack_entry(state, id, 'Italic', start, '_');
226
+ state.active_text_id = null;
227
+ state.pos++;
228
+ state.column++;
229
+ state.prev_char = UNDERSCORE;
230
+ return true;
231
+ };
232
+
233
+ // -- Delimiter close (bold/italic/strikethrough) --
234
+
235
+ /**
236
+ * Whether the char after a closing italic candidate permits the close (only
237
+ * `_` has close-boundary rules — `**`/`~~` close anywhere). Word chars and
238
+ * `_` itself (self-opaque — `_x__` never closes on the first `_` of a run)
239
+ * fail the boundary.
240
+ *
241
+ * At end of buffer the boundary is treated as valid — only reachable in
242
+ * forced mode, where end-of-input is a true boundary; while streaming,
243
+ * `process_inline` holds the closer decision until the next char arrives.
244
+ *
245
+ * @nodocs
246
+ */
247
+ export const check_close_word_boundary = (state: MdzStreamParserState): boolean => {
248
+ const next_pos = state.pos + 1;
249
+ if (next_pos < state.buffer.length) {
250
+ const next = state.buffer.charCodeAt(next_pos);
251
+ return !(is_word_char(next) || next === UNDERSCORE);
252
+ }
253
+ return true;
254
+ };
255
+
256
+ /**
257
+ * Close an open delimiter-paired container (`Bold`, `Italic`, `Strikethrough`)
258
+ * at the given stack index, consuming the closing delimiter — read from the
259
+ * entry, so `**`/`~~` advance two chars and `_` one. The caller in
260
+ * `process_inline` already located the open via `find_open`, so we trust the
261
+ * index rather than re-walking the stack.
262
+ *
263
+ * @nodocs
264
+ */
265
+ export const close_delimiter = (state: MdzStreamParserState, stack_idx: number): boolean => {
266
+ flush_text(state);
267
+ // revert anything between the container and the top of the stack
268
+ revert_above(state, stack_idx);
269
+ const entry = pop_stack_entry(state);
270
+ const {delimiter} = entry;
271
+ // check for empty container — revert to literal text instead of closing
272
+ if (!entry.has_children) {
273
+ emit(state, {
274
+ type: 'revert',
275
+ id: entry.id,
276
+ replacement_text: delimiter,
277
+ start: entry.start,
278
+ });
279
+ accumulate_text(state, delimiter, offset(state));
280
+ } else {
281
+ emit(state, {type: 'close', id: entry.id, end: offset(state) + delimiter.length});
282
+ }
283
+ state.active_text_id = null;
284
+ state.pos += delimiter.length;
285
+ state.column += delimiter.length;
286
+ state.prev_char = delimiter.charCodeAt(delimiter.length - 1);
287
+ return true;
288
+ };
289
+
290
+ /**
291
+ * Scan for the first potential closing `_` and check its word boundary (only
292
+ * italic has closer-boundary rules). Returns a tri-state:
293
+ * - `'confirmed'`: closer found in buffer with valid word boundary
294
+ * - `'rejected'`: closer found but with invalid word boundary (greedy first-match fails)
295
+ * - `'pending'`: no closer in buffer yet, or can't determine boundary
296
+ *
297
+ * The boundary check is self-opaque: a candidate followed by `_` itself is
298
+ * `'rejected'`, so underscore runs (`__init__`) degrade to literal text.
299
+ * While streaming, `'pending'` opens optimistically (the revert machinery
300
+ * corrects later); in forced mode it degrades to text.
301
+ *
302
+ * The delimiter search uses raw `indexOf` and stays amortized-linear without
303
+ * a memo: candidates are occurrences of the searched character itself, so a
304
+ * scan ends either at the next candidate (consecutive scans cover disjoint
305
+ * ranges) or at the end of the buffer with no candidates left to rescan it.
306
+ * The run-boundary probe (paragraph breaks, and list structure inside open
307
+ * lists) is bounded to the same window — only a boundary *before* the found
308
+ * delimiter matters — inheriting that amortization; an unbounded probe would
309
+ * rescan to EOF per candidate on single-paragraph input, going quadratic.
310
+ *
311
+ * @nodocs
312
+ */
313
+ export const scan_closer_boundary = (
314
+ state: MdzStreamParserState,
315
+ forced = false,
316
+ ): 'confirmed' | 'rejected' | 'pending' => {
317
+ const search_start = state.pos + 1;
318
+ const buffer = state.buffer;
319
+ const delim_pos = buffer.indexOf('_', search_start);
320
+ if (delim_pos === -1) return 'pending'; // no closer in buffer
321
+ // check for a run boundary before the delimiter (delimiters are never \n,
322
+ // so a relevant boundary lies entirely within [search_start, delim_pos))
323
+ if (has_run_boundary_between(state, search_start, delim_pos, forced)) {
324
+ return 'pending'; // the run ends first
325
+ }
326
+ // check word boundary after the closer (the delimiter itself is self-opaque)
327
+ const after_pos = delim_pos + 1;
328
+ if (after_pos < state.buffer.length) {
329
+ const after = state.buffer.charCodeAt(after_pos);
330
+ return is_word_char(after) || after === UNDERSCORE ? 'rejected' : 'confirmed';
331
+ }
332
+ // closer at end of buffer — treat as confirmed, consistent with
333
+ // check_close_word_boundary which also assumes boundary at buffer end
334
+ return 'confirmed';
335
+ };
336
+
337
+ // -- Inline code --
338
+
339
+ /** @nodocs */
340
+ export const try_code = (state: MdzStreamParserState, forced = false): boolean => {
341
+ const start = state.pos;
342
+ let i = start + 1; // past opening backtick
343
+
344
+ // compute search boundary: don't scan past open formatting delimiters
345
+ const search_limit = code_search_limit(state, start + 1);
346
+
347
+ // scan for closing backtick (must be before newline and search boundary)
348
+ while (i < state.buffer.length && i < search_limit) {
349
+ const c = state.buffer.charCodeAt(i);
350
+ if (c === BACKTICK) {
351
+ // found close
352
+ const content = state.buffer.slice(start + 1, i);
353
+ if (content.length === 0) {
354
+ // empty code — treat as text
355
+ consume_delimiter_as_text(state, '``');
356
+ return true;
357
+ }
358
+ flush_text(state);
359
+ ensure_paragraph(state);
360
+ const id = alloc_id(state);
361
+ emit(state, {
362
+ type: 'text',
363
+ id,
364
+ content,
365
+ text_type: 'Code',
366
+ start: offset(state, start),
367
+ end: offset(state, i + 1),
368
+ });
369
+ state.active_text_id = null;
370
+ state.pos = i + 1;
371
+ state.column += i + 1 - start;
372
+ state.prev_char = BACKTICK;
373
+ return true;
374
+ }
375
+ if (c === NEWLINE) {
376
+ // inline code can't span lines — treat opening backtick as text
377
+ consume_delimiter_as_text(state, '`');
378
+ return true;
379
+ }
380
+ i++;
381
+ }
382
+
383
+ // hit search limit (open formatting delimiter boundary) — treat backtick as
384
+ // text. This is the documented backtick-adjacent divergence point: when the
385
+ // bounding frame is a wrongly-optimistic italic (its failed closer not yet
386
+ // processed), the one-shot parse rejects that italic and scans unbounded —
387
+ // see the `MdzStreamParser` TSDoc and the parity suite's expected-divergence
388
+ // battery
389
+ if (i < state.buffer.length && i >= search_limit) {
390
+ consume_delimiter_as_text(state, '`');
391
+ return true;
392
+ }
393
+
394
+ // buffer ended without close or newline.
395
+ // At EOF the buffer is complete, so no closer can ever arrive — the
396
+ // backtick is literal text, matching the one-shot parse's unclosed
397
+ // fallback (an optimistic open here would strand an unclosable Code).
398
+ if (forced) {
399
+ consume_delimiter_as_text(state, '`');
400
+ return true;
401
+ }
402
+ // If formatting is on the stack, hold — the formatting delimiter might appear
403
+ // in the next chunk between the backtick and its closer, and going optimistic
404
+ // would make the code span consume the formatting closer as raw text.
405
+ if (has_inline_formatting_on_stack(state)) {
406
+ return false; // hold for more input
407
+ }
408
+
409
+ // no formatting on stack — open Code optimistically.
410
+ // Content streams via text/append_text inside the container.
411
+ // Closes on matching backtick, reverts on newline.
412
+ flush_text(state);
413
+ ensure_paragraph(state);
414
+ const id = alloc_id(state);
415
+ const code_start = offset(state, start);
416
+ emit(state, {type: 'open', id, node_type: 'Code', start: code_start});
417
+ push_stack_entry(state, id, 'Code', code_start, '`');
418
+ state.in_code = true;
419
+ state.active_text_id = null;
420
+ state.pos = start + 1; // past opening backtick
421
+ state.column++;
422
+ state.prev_char = BACKTICK;
423
+ return true;
424
+ };
425
+
426
+ /**
427
+ * Compute search limit for inline code scanning.
428
+ * Finds the closest closing delimiter for any open formatting on the stack.
429
+ * This prevents backtick scanning from crossing bold/italic/etc boundaries.
430
+ *
431
+ * @nodocs
432
+ */
433
+ export const code_search_limit = (state: MdzStreamParserState, from: number): number => {
434
+ let limit = state.buffer.length;
435
+ for (let s = state.stack.length - 1; s >= 0; s--) {
436
+ const entry = state.stack[s]!;
437
+ if (is_run_boundary(entry.node_type)) break;
438
+ const delimiter = entry.delimiter;
439
+ if (!delimiter) continue;
440
+ // an open Link contributes no bound: the sync lexer never narrows its
441
+ // scan bound to a link's `]` (the link children loop checks `]` only
442
+ // between children), so a code span may swallow `]` and the link still
443
+ // completes on a later one — in "[a `b ] c`](/u)" the `]` sits inside
444
+ // the code span in both parsers
445
+ if (entry.node_type === 'Link') continue;
446
+ // for tags the stored delimiter is the opening form (kept for revert
447
+ // replacement text) — the scan must stop at the closing form, so the
448
+ // code span can't swallow the enclosing tag's closer (parity with the
449
+ // sync lexer's per-iteration `#max_search_index` narrowing)
450
+ const needle = entry.tag_name === undefined ? delimiter : `</${entry.tag_name}>`;
451
+ // find the closing delimiter in the buffer (memoized — see `buffer_index_of`)
452
+ const idx = buffer_index_of(state, needle, from);
453
+ if (idx !== -1 && idx < limit) {
454
+ limit = idx;
455
+ }
456
+ }
457
+ return limit;
458
+ };
459
+
460
+ /**
461
+ * Check if any inline formatting container (Bold, Italic, Strikethrough, Link)
462
+ * is on the stack above the current block boundary.
463
+ * Used to decide whether backtick can go optimistic — when formatting is open,
464
+ * the code span might cross the formatting boundary, so we hold instead.
465
+ *
466
+ * @nodocs
467
+ */
468
+ export const has_inline_formatting_on_stack = (state: MdzStreamParserState): boolean => {
469
+ for (let s = state.stack.length - 1; s >= 0; s--) {
470
+ const entry = state.stack[s]!;
471
+ if (is_run_boundary(entry.node_type)) return false;
472
+ if (entry.delimiter) return true;
473
+ }
474
+ return false;
475
+ };
476
+
477
+ /**
478
+ * Process content inside an optimistic inline Code container.
479
+ * Scans raw text (no inline formatting) until closing backtick or newline.
480
+ * Backtick closes (or reverts if empty). Newline reverts (code can't span lines).
481
+ *
482
+ * @nodocs
483
+ */
484
+ export const process_code_content = (state: MdzStreamParserState): void => {
485
+ const char_code = state.buffer.charCodeAt(state.pos);
486
+
487
+ if (char_code === BACKTICK) {
488
+ flush_text(state);
489
+ const entry = pop_stack_entry(state);
490
+ if (!entry.has_children) {
491
+ // empty code `` — revert opener, accumulate closer as text
492
+ emit(state, {type: 'revert', id: entry.id, replacement_text: '`', start: entry.start});
493
+ accumulate_text(state, '`', offset(state));
494
+ } else {
495
+ emit(state, {type: 'close', id: entry.id, end: offset(state) + 1});
496
+ }
497
+ state.in_code = false;
498
+ state.active_text_id = null;
499
+ state.pos++;
500
+ state.column++;
501
+ state.prev_char = BACKTICK;
502
+ return;
503
+ }
504
+
505
+ if (char_code === NEWLINE) {
506
+ // code spans can't cross lines — revert
507
+ flush_text(state);
508
+ const entry = pop_stack_entry(state);
509
+ emit(state, {type: 'revert', id: entry.id, replacement_text: '`', start: entry.start});
510
+ state.in_code = false;
511
+ state.active_text_id = null;
512
+ // don't consume newline — outer loop handles it
513
+ return;
514
+ }
515
+
516
+ // scan raw text until backtick or newline (paragraph already ensured at code open)
517
+ const start = state.pos;
518
+ state.pos++;
519
+ while (state.pos < state.buffer.length) {
520
+ const c = state.buffer.charCodeAt(state.pos);
521
+ if (c === BACKTICK || c === NEWLINE) break;
522
+ state.pos++;
523
+ }
524
+ accumulate_text(state, state.buffer.slice(start, state.pos), offset(state, start));
525
+ state.prev_char = state.buffer.charCodeAt(state.pos - 1);
526
+ state.column += state.pos - start;
527
+ };