@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.
- package/LICENSE +21 -0
- package/README.md +32 -0
- package/dist/Mdz.svelte +39 -0
- package/dist/Mdz.svelte.d.ts +17 -0
- package/dist/Mdz.svelte.d.ts.map +1 -0
- package/dist/MdzNodeView.svelte +140 -0
- package/dist/MdzNodeView.svelte.d.ts +9 -0
- package/dist/MdzNodeView.svelte.d.ts.map +1 -0
- package/dist/MdzPrecompiled.svelte +33 -0
- package/dist/MdzPrecompiled.svelte.d.ts +17 -0
- package/dist/MdzPrecompiled.svelte.d.ts.map +1 -0
- package/dist/MdzRoot.svelte +40 -0
- package/dist/MdzRoot.svelte.d.ts +14 -0
- package/dist/MdzRoot.svelte.d.ts.map +1 -0
- package/dist/MdzStream.svelte +37 -0
- package/dist/MdzStream.svelte.d.ts +18 -0
- package/dist/MdzStream.svelte.d.ts.map +1 -0
- package/dist/MdzStreamNodeView.svelte +150 -0
- package/dist/MdzStreamNodeView.svelte.d.ts +9 -0
- package/dist/MdzStreamNodeView.svelte.d.ts.map +1 -0
- package/dist/context_helpers.d.ts +23 -0
- package/dist/context_helpers.d.ts.map +1 -0
- package/dist/context_helpers.js +19 -0
- package/dist/mdz.d.ts +152 -0
- package/dist/mdz.d.ts.map +1 -0
- package/dist/mdz.js +61 -0
- package/dist/mdz_contexts.d.ts +106 -0
- package/dist/mdz_contexts.d.ts.map +1 -0
- package/dist/mdz_contexts.js +42 -0
- package/dist/mdz_helpers.d.ts +321 -0
- package/dist/mdz_helpers.d.ts.map +1 -0
- package/dist/mdz_helpers.js +668 -0
- package/dist/mdz_lexer.d.ts +123 -0
- package/dist/mdz_lexer.d.ts.map +1 -0
- package/dist/mdz_lexer.js +1405 -0
- package/dist/mdz_opcodes.d.ts +193 -0
- package/dist/mdz_opcodes.d.ts.map +1 -0
- package/dist/mdz_opcodes.js +14 -0
- package/dist/mdz_opcodes_to_nodes.d.ts +26 -0
- package/dist/mdz_opcodes_to_nodes.d.ts.map +1 -0
- package/dist/mdz_opcodes_to_nodes.js +410 -0
- package/dist/mdz_stream_parser.d.ts +109 -0
- package/dist/mdz_stream_parser.d.ts.map +1 -0
- package/dist/mdz_stream_parser.js +699 -0
- package/dist/mdz_stream_parser_block.d.ts +99 -0
- package/dist/mdz_stream_parser_block.d.ts.map +1 -0
- package/dist/mdz_stream_parser_block.js +437 -0
- package/dist/mdz_stream_parser_inline.d.ts +98 -0
- package/dist/mdz_stream_parser_inline.d.ts.map +1 -0
- package/dist/mdz_stream_parser_inline.js +481 -0
- package/dist/mdz_stream_parser_link.d.ts +29 -0
- package/dist/mdz_stream_parser_link.d.ts.map +1 -0
- package/dist/mdz_stream_parser_link.js +227 -0
- package/dist/mdz_stream_parser_list.d.ts +106 -0
- package/dist/mdz_stream_parser_list.d.ts.map +1 -0
- package/dist/mdz_stream_parser_list.js +438 -0
- package/dist/mdz_stream_parser_state.d.ts +476 -0
- package/dist/mdz_stream_parser_state.d.ts.map +1 -0
- package/dist/mdz_stream_parser_state.js +651 -0
- package/dist/mdz_stream_parser_text.d.ts +25 -0
- package/dist/mdz_stream_parser_text.d.ts.map +1 -0
- package/dist/mdz_stream_parser_text.js +67 -0
- package/dist/mdz_stream_parser_url.d.ts +76 -0
- package/dist/mdz_stream_parser_url.d.ts.map +1 -0
- package/dist/mdz_stream_parser_url.js +328 -0
- package/dist/mdz_stream_state.svelte.d.ts +53 -0
- package/dist/mdz_stream_state.svelte.d.ts.map +1 -0
- package/dist/mdz_stream_state.svelte.js +360 -0
- package/dist/mdz_to_svelte.d.ts +71 -0
- package/dist/mdz_to_svelte.d.ts.map +1 -0
- package/dist/mdz_to_svelte.js +127 -0
- package/dist/mdz_token_parser.d.ts +20 -0
- package/dist/mdz_token_parser.d.ts.map +1 -0
- package/dist/mdz_token_parser.js +449 -0
- package/dist/svelte_preprocess_mdz.d.ts +80 -0
- package/dist/svelte_preprocess_mdz.d.ts.map +1 -0
- package/dist/svelte_preprocess_mdz.js +543 -0
- package/dist/tsdoc_mdz.d.ts +45 -0
- package/dist/tsdoc_mdz.d.ts.map +1 -0
- package/dist/tsdoc_mdz.js +88 -0
- package/package.json +131 -0
- package/src/lib/context_helpers.ts +52 -0
- package/src/lib/mdz.ts +190 -0
- package/src/lib/mdz_contexts.ts +99 -0
- package/src/lib/mdz_helpers.ts +727 -0
- package/src/lib/mdz_lexer.ts +1761 -0
- package/src/lib/mdz_opcodes.ts +227 -0
- package/src/lib/mdz_opcodes_to_nodes.ts +470 -0
- package/src/lib/mdz_stream_parser.ts +766 -0
- package/src/lib/mdz_stream_parser_block.ts +490 -0
- package/src/lib/mdz_stream_parser_inline.ts +527 -0
- package/src/lib/mdz_stream_parser_link.ts +283 -0
- package/src/lib/mdz_stream_parser_list.ts +525 -0
- package/src/lib/mdz_stream_parser_state.ts +957 -0
- package/src/lib/mdz_stream_parser_text.ts +95 -0
- package/src/lib/mdz_stream_parser_url.ts +386 -0
- package/src/lib/mdz_stream_state.svelte.ts +383 -0
- package/src/lib/mdz_to_svelte.ts +213 -0
- package/src/lib/mdz_token_parser.ts +544 -0
- package/src/lib/svelte_preprocess_mdz.ts +777 -0
- 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
|
+
};
|