@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,490 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Block-element handlers for the streaming mdz parser.
|
|
3
|
+
*
|
|
4
|
+
* Each `try_*` returns {@link TryResult} so the orchestrator in
|
|
5
|
+
* {@link ./mdz_stream_parser.ts} can fall through or wait for more input.
|
|
6
|
+
*
|
|
7
|
+
* @module
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
BACKTICK,
|
|
12
|
+
HASH,
|
|
13
|
+
HR_HYPHEN_COUNT,
|
|
14
|
+
HYPHEN,
|
|
15
|
+
MAX_HEADING_LEVEL,
|
|
16
|
+
MIN_CODEBLOCK_BACKTICKS,
|
|
17
|
+
NEWLINE,
|
|
18
|
+
SPACE,
|
|
19
|
+
is_line_whitespace,
|
|
20
|
+
} from './mdz_helpers.js';
|
|
21
|
+
import {
|
|
22
|
+
type CodeblockState,
|
|
23
|
+
type MdzStreamParserState,
|
|
24
|
+
type TryResult,
|
|
25
|
+
alloc_id,
|
|
26
|
+
close_paragraph,
|
|
27
|
+
emit,
|
|
28
|
+
flush_text,
|
|
29
|
+
offset,
|
|
30
|
+
push_stack_entry,
|
|
31
|
+
} from './mdz_stream_parser_state.js';
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Try to parse a heading at column 0.
|
|
35
|
+
*
|
|
36
|
+
* @nodocs
|
|
37
|
+
*/
|
|
38
|
+
export const try_heading = (state: MdzStreamParserState): TryResult => {
|
|
39
|
+
const start = state.pos;
|
|
40
|
+
let i = start;
|
|
41
|
+
|
|
42
|
+
// count hashes
|
|
43
|
+
let hash_count = 0;
|
|
44
|
+
while (
|
|
45
|
+
i < state.buffer.length &&
|
|
46
|
+
state.buffer.charCodeAt(i) === HASH &&
|
|
47
|
+
hash_count <= MAX_HEADING_LEVEL
|
|
48
|
+
) {
|
|
49
|
+
hash_count++;
|
|
50
|
+
i++;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (hash_count === 0 || hash_count > MAX_HEADING_LEVEL) return 'not_match';
|
|
54
|
+
|
|
55
|
+
// need at least one more char to check for space
|
|
56
|
+
if (i >= state.buffer.length) return 'need_more';
|
|
57
|
+
|
|
58
|
+
// must have space after hashes
|
|
59
|
+
if (state.buffer.charCodeAt(i) !== SPACE) return 'not_match';
|
|
60
|
+
i++; // consume space
|
|
61
|
+
|
|
62
|
+
// must have at least one non-whitespace char before the newline (mirrors
|
|
63
|
+
// the sync lexer's has-content probe) — hold until it can be decided. A
|
|
64
|
+
// line-bounded hold: the heading opens the moment its first content char
|
|
65
|
+
// arrives. In forced mode the orchestrator treats `need_more` as text.
|
|
66
|
+
let j = i;
|
|
67
|
+
while (j < state.buffer.length && is_line_whitespace(state.buffer.charCodeAt(j))) {
|
|
68
|
+
j++;
|
|
69
|
+
}
|
|
70
|
+
if (j >= state.buffer.length) return 'need_more';
|
|
71
|
+
if (state.buffer.charCodeAt(j) === NEWLINE) return 'not_match'; // no content — literal text
|
|
72
|
+
|
|
73
|
+
// close any open paragraph first
|
|
74
|
+
flush_text(state);
|
|
75
|
+
close_paragraph(state);
|
|
76
|
+
|
|
77
|
+
const id = alloc_id(state);
|
|
78
|
+
const heading_start = offset(state, start);
|
|
79
|
+
emit(state, {
|
|
80
|
+
type: 'open',
|
|
81
|
+
id,
|
|
82
|
+
node_type: 'Heading',
|
|
83
|
+
start: heading_start,
|
|
84
|
+
level: hash_count as 1 | 2 | 3 | 4 | 5 | 6,
|
|
85
|
+
});
|
|
86
|
+
push_stack_entry(state, id, 'Heading', heading_start);
|
|
87
|
+
state.in_heading = true;
|
|
88
|
+
state.heading_text_parts = [''];
|
|
89
|
+
state.pos = i;
|
|
90
|
+
state.column = i - start;
|
|
91
|
+
state.prev_char = SPACE;
|
|
92
|
+
return 'consumed';
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Try to parse a horizontal rule at column 0.
|
|
97
|
+
* When `forced` is true, treats buffer end as EOF (no more input coming).
|
|
98
|
+
*
|
|
99
|
+
* @nodocs
|
|
100
|
+
*/
|
|
101
|
+
export const try_hr = (state: MdzStreamParserState, forced = false): TryResult => {
|
|
102
|
+
const start = state.pos;
|
|
103
|
+
let i = start;
|
|
104
|
+
|
|
105
|
+
// must have exactly 3 hyphens
|
|
106
|
+
if (i + HR_HYPHEN_COUNT > state.buffer.length) return 'need_more';
|
|
107
|
+
for (let j = 0; j < HR_HYPHEN_COUNT; j++) {
|
|
108
|
+
if (state.buffer.charCodeAt(i + j) !== HYPHEN) return 'not_match';
|
|
109
|
+
}
|
|
110
|
+
i += HR_HYPHEN_COUNT;
|
|
111
|
+
|
|
112
|
+
// after hyphens, only line whitespace allowed before newline/EOF
|
|
113
|
+
while (i < state.buffer.length) {
|
|
114
|
+
const c = state.buffer.charCodeAt(i);
|
|
115
|
+
if (c === NEWLINE) break;
|
|
116
|
+
if (!is_line_whitespace(c)) return 'not_match';
|
|
117
|
+
i++;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// if buffer ends without newline/EOF confirmation, need more input (unless forced)
|
|
121
|
+
if (i >= state.buffer.length && !forced) {
|
|
122
|
+
return 'need_more';
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
flush_text(state);
|
|
126
|
+
close_paragraph(state);
|
|
127
|
+
|
|
128
|
+
const id = alloc_id(state);
|
|
129
|
+
emit(state, {
|
|
130
|
+
type: 'void',
|
|
131
|
+
id,
|
|
132
|
+
node_type: 'Hr',
|
|
133
|
+
start: offset(state, start),
|
|
134
|
+
end: offset(state, i),
|
|
135
|
+
});
|
|
136
|
+
// `i` sits at the line's newline, or at the buffer end — in forced mode
|
|
137
|
+
// always the buffer end (a newline would have resolved this line during
|
|
138
|
+
// `feed`, so a forced HR is only ever the document's final line). Trailing
|
|
139
|
+
// newline and blank lines absorb at the process-loop top.
|
|
140
|
+
state.pos = i;
|
|
141
|
+
state.skip_blank_lines = true;
|
|
142
|
+
state.column = 0;
|
|
143
|
+
state.prev_char = NEWLINE;
|
|
144
|
+
return 'consumed';
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Try to open a code block at column 0. A complete opener line is decisive —
|
|
149
|
+
* raw mode is absolute, so fence-ness never depends on a closer arriving:
|
|
150
|
+
* an unclosed fence consumes to EOF as content. Any open paragraph closes
|
|
151
|
+
* first (fences interrupt paragraphs, matching the sync parser). The only
|
|
152
|
+
* hold is the opener line itself; an opener cut off at EOF without its
|
|
153
|
+
* newline stays text (the orchestrator treats `need_more` as text in forced
|
|
154
|
+
* mode).
|
|
155
|
+
*
|
|
156
|
+
* @nodocs
|
|
157
|
+
*/
|
|
158
|
+
export const try_codeblock_open = (state: MdzStreamParserState): TryResult => {
|
|
159
|
+
const start = state.pos;
|
|
160
|
+
let i = start;
|
|
161
|
+
|
|
162
|
+
// count backticks
|
|
163
|
+
let backtick_count = 0;
|
|
164
|
+
while (i < state.buffer.length && state.buffer.charCodeAt(i) === BACKTICK) {
|
|
165
|
+
backtick_count++;
|
|
166
|
+
i++;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (backtick_count < MIN_CODEBLOCK_BACKTICKS) {
|
|
170
|
+
// if buffer ended mid-backtick-sequence, need more input
|
|
171
|
+
if (i >= state.buffer.length) return 'need_more';
|
|
172
|
+
// definitely not enough backticks
|
|
173
|
+
return 'not_match';
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// find end of opening fence line (need newline)
|
|
177
|
+
const lang_start = i;
|
|
178
|
+
while (i < state.buffer.length) {
|
|
179
|
+
const c = state.buffer.charCodeAt(i);
|
|
180
|
+
if (c === NEWLINE || is_line_whitespace(c)) break;
|
|
181
|
+
i++;
|
|
182
|
+
}
|
|
183
|
+
const lang = i > lang_start ? state.buffer.slice(lang_start, i) : null;
|
|
184
|
+
|
|
185
|
+
// skip trailing whitespace on opening fence line
|
|
186
|
+
while (i < state.buffer.length && is_line_whitespace(state.buffer.charCodeAt(i))) {
|
|
187
|
+
i++;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// must find newline
|
|
191
|
+
if (i >= state.buffer.length) return 'need_more';
|
|
192
|
+
if (state.buffer.charCodeAt(i) !== NEWLINE) return 'not_match';
|
|
193
|
+
i++; // consume newline
|
|
194
|
+
|
|
195
|
+
flush_text(state);
|
|
196
|
+
close_paragraph(state);
|
|
197
|
+
|
|
198
|
+
const id = alloc_id(state);
|
|
199
|
+
const cb_start = offset(state, start);
|
|
200
|
+
emit(state, {type: 'open', id, node_type: 'Codeblock', start: cb_start, lang});
|
|
201
|
+
state.codeblock = {id, backtick_count, text_id: null, fence_indent: 0, start: cb_start};
|
|
202
|
+
state.pos = i;
|
|
203
|
+
state.column = 0;
|
|
204
|
+
state.prev_char = NEWLINE;
|
|
205
|
+
return 'consumed';
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Emit codeblock content as `text` (first chunk) or `append_text` (subsequent
|
|
210
|
+
* chunks), updating `cb.text_id` on first emit. `content_start` is the local
|
|
211
|
+
* buffer position where the content slice began — only used to compute the
|
|
212
|
+
* global byte offset for the first `text` opcode.
|
|
213
|
+
*
|
|
214
|
+
* @nodocs
|
|
215
|
+
*/
|
|
216
|
+
export const emit_codeblock_text = (
|
|
217
|
+
state: MdzStreamParserState,
|
|
218
|
+
cb: CodeblockState,
|
|
219
|
+
content: string,
|
|
220
|
+
content_start: number,
|
|
221
|
+
): void => {
|
|
222
|
+
if (cb.text_id !== null) {
|
|
223
|
+
emit(state, {type: 'append_text', id: cb.text_id, content});
|
|
224
|
+
} else {
|
|
225
|
+
const id = alloc_id(state);
|
|
226
|
+
const text_start = offset(state, content_start);
|
|
227
|
+
emit(state, {
|
|
228
|
+
type: 'text',
|
|
229
|
+
id,
|
|
230
|
+
content,
|
|
231
|
+
text_type: 'Text',
|
|
232
|
+
start: text_start,
|
|
233
|
+
end: text_start + content.length,
|
|
234
|
+
});
|
|
235
|
+
cb.text_id = id;
|
|
236
|
+
}
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Drain the remaining buffer as codeblock content at EOF, looking for a closing
|
|
241
|
+
* fence whose terminator is EOF rather than `\n`. Matches the sync lexer's
|
|
242
|
+
* `#match_fence`, which allows a fence at the end of the document.
|
|
243
|
+
*
|
|
244
|
+
* If a fence is found, the codeblock closes normally. Otherwise the remaining
|
|
245
|
+
* buffer becomes content and `close_codeblock_at_eof` (in
|
|
246
|
+
* `mdz_stream_parser_state.ts`) emits the plain close after process completes
|
|
247
|
+
* (an unclosed fence consumes to EOF — never reverted).
|
|
248
|
+
*
|
|
249
|
+
* @nodocs
|
|
250
|
+
*/
|
|
251
|
+
export const process_codeblock_forced = (state: MdzStreamParserState): void => {
|
|
252
|
+
const cb = state.codeblock!;
|
|
253
|
+
const start = state.pos;
|
|
254
|
+
|
|
255
|
+
while (state.pos < state.buffer.length) {
|
|
256
|
+
if (state.column === 0) {
|
|
257
|
+
const fence_match = match_codeblock_close(state, cb.backtick_count, true);
|
|
258
|
+
if (fence_match !== -1) {
|
|
259
|
+
// emit content up to the closing fence (excluding trailing newline before fence)
|
|
260
|
+
let content_end = state.pos;
|
|
261
|
+
if (content_end > start && state.buffer.charCodeAt(content_end - 1) === NEWLINE) {
|
|
262
|
+
content_end--;
|
|
263
|
+
}
|
|
264
|
+
const content = state.buffer.slice(start, content_end);
|
|
265
|
+
if (content.length > 0) {
|
|
266
|
+
emit_codeblock_text(state, cb, content, start);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// the closer here is always EOF-terminated (`fence_match` is the
|
|
270
|
+
// buffer end): a newline-terminated closer resolves during `feed`,
|
|
271
|
+
// whose hold retains at most a trailing newline plus a partial
|
|
272
|
+
// fence line. So — like the non-forced path's `fence_match - 1` —
|
|
273
|
+
// the close `end` excludes any line terminator
|
|
274
|
+
emit(state, {type: 'close', id: cb.id, end: offset(state, fence_match)});
|
|
275
|
+
state.pos = fence_match;
|
|
276
|
+
state.codeblock = null;
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const c = state.buffer.charCodeAt(state.pos);
|
|
282
|
+
if (c === NEWLINE) {
|
|
283
|
+
state.column = 0;
|
|
284
|
+
} else {
|
|
285
|
+
state.column++;
|
|
286
|
+
}
|
|
287
|
+
state.pos++;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// no closing fence — dump the rest as codeblock content; caller's
|
|
291
|
+
// `close_codeblock_at_eof` then closes the codeblock at EOF
|
|
292
|
+
const remaining = state.buffer.slice(start, state.pos);
|
|
293
|
+
if (remaining.length > 0) {
|
|
294
|
+
emit_codeblock_text(state, cb, remaining, start);
|
|
295
|
+
}
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Process bytes while in codeblock mode.
|
|
300
|
+
* Returns true if processing should continue, false if more input needed.
|
|
301
|
+
*
|
|
302
|
+
* @nodocs
|
|
303
|
+
*/
|
|
304
|
+
export const process_codeblock = (state: MdzStreamParserState): boolean => {
|
|
305
|
+
const cb = state.codeblock!;
|
|
306
|
+
const start = state.pos;
|
|
307
|
+
|
|
308
|
+
while (state.pos < state.buffer.length) {
|
|
309
|
+
// check for closing fence at column 0
|
|
310
|
+
if (state.column === 0) {
|
|
311
|
+
const fence_match = match_codeblock_close(state, cb.backtick_count);
|
|
312
|
+
if (fence_match === -2) {
|
|
313
|
+
// need more input — hold from this position (don't consume potential fence chars)
|
|
314
|
+
break;
|
|
315
|
+
}
|
|
316
|
+
if (fence_match !== -1) {
|
|
317
|
+
// emit content up to the closing fence (excluding trailing newline before fence)
|
|
318
|
+
let content_end = state.pos;
|
|
319
|
+
if (content_end > start && state.buffer.charCodeAt(content_end - 1) === NEWLINE) {
|
|
320
|
+
content_end--;
|
|
321
|
+
}
|
|
322
|
+
const content = state.buffer.slice(start, content_end);
|
|
323
|
+
if (content.length > 0) {
|
|
324
|
+
emit_codeblock_text(state, cb, content, start);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// codeblock end = position of newline after closing fence (exclusive)
|
|
328
|
+
emit(state, {type: 'close', id: cb.id, end: offset(state, fence_match - 1)});
|
|
329
|
+
state.pos = fence_match;
|
|
330
|
+
state.codeblock = null;
|
|
331
|
+
state.column = 0;
|
|
332
|
+
state.prev_char = NEWLINE;
|
|
333
|
+
// trailing newlines and blank lines absorb at the process-loop top
|
|
334
|
+
state.skip_blank_lines = true;
|
|
335
|
+
return true;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// not a closing fence — advance
|
|
340
|
+
const c = state.buffer.charCodeAt(state.pos);
|
|
341
|
+
if (c === NEWLINE) {
|
|
342
|
+
state.column = 0;
|
|
343
|
+
} else {
|
|
344
|
+
state.column++;
|
|
345
|
+
}
|
|
346
|
+
state.pos++;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// buffer exhausted or broke for potential fence — emit content so far, hold trailing data.
|
|
350
|
+
// Hold trailing \n (could be before a closing fence in next chunk), and also hold
|
|
351
|
+
// \n + backticks when we broke mid-fence-check (the \n precedes the backticks and
|
|
352
|
+
// would be stripped by fence close, so must not be emitted as content yet).
|
|
353
|
+
let emit_end = state.pos;
|
|
354
|
+
if (emit_end > start && state.buffer.charCodeAt(emit_end - 1) === NEWLINE) {
|
|
355
|
+
emit_end--;
|
|
356
|
+
state.pos = emit_end; // leave \n (+ any held backticks after it) in buffer
|
|
357
|
+
}
|
|
358
|
+
const content = state.buffer.slice(start, emit_end);
|
|
359
|
+
if (content.length > 0) {
|
|
360
|
+
emit_codeblock_text(state, cb, content, start);
|
|
361
|
+
}
|
|
362
|
+
return false; // need more input
|
|
363
|
+
};
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Process buffered lines while inside an in-item code block
|
|
367
|
+
* (`fence_indent > 0`). Operates on complete lines only — a partial line at
|
|
368
|
+
* the buffer end holds until its newline arrives (line-bounded, like the
|
|
369
|
+
* opener hold), keeping the per-line indent strip and closer probe
|
|
370
|
+
* deterministic across chunk boundaries. Content lines strip
|
|
371
|
+
* `min(fence_indent, line_indent)` and emit joined by `'\n'` separators
|
|
372
|
+
* (first line bare, later lines prefixed), so emitted content never carries
|
|
373
|
+
* the region's final newline — `close_codeblock_at_eof` therefore skips its
|
|
374
|
+
* trailing-newline trim for in-item fences.
|
|
375
|
+
*
|
|
376
|
+
* A closing fence — any indent <= the opener's, a run of >= the opener's
|
|
377
|
+
* backticks, only whitespace to the line end — closes the block with `pos`
|
|
378
|
+
* left at its terminating newline, so the list line classifier dispatches
|
|
379
|
+
* what follows (mirroring the sync lexer's loop shape). In forced mode EOF
|
|
380
|
+
* terminates closer and content lines.
|
|
381
|
+
*
|
|
382
|
+
* Returns `false` when more input is needed; never in forced mode.
|
|
383
|
+
*
|
|
384
|
+
* @nodocs
|
|
385
|
+
*/
|
|
386
|
+
export const process_codeblock_in_item = (
|
|
387
|
+
state: MdzStreamParserState,
|
|
388
|
+
forced: boolean,
|
|
389
|
+
): boolean => {
|
|
390
|
+
const cb = state.codeblock!;
|
|
391
|
+
while (state.pos < state.buffer.length) {
|
|
392
|
+
const line_start = state.pos;
|
|
393
|
+
let j = line_start;
|
|
394
|
+
while (j < state.buffer.length && is_line_whitespace(state.buffer.charCodeAt(j))) {
|
|
395
|
+
j++;
|
|
396
|
+
}
|
|
397
|
+
if (j >= state.buffer.length && !forced) return false; // partial line — hold
|
|
398
|
+
const line_indent = j - line_start;
|
|
399
|
+
// closer probe
|
|
400
|
+
if (
|
|
401
|
+
line_indent <= cb.fence_indent &&
|
|
402
|
+
j < state.buffer.length &&
|
|
403
|
+
state.buffer.charCodeAt(j) === BACKTICK
|
|
404
|
+
) {
|
|
405
|
+
let k = j;
|
|
406
|
+
let count = 0;
|
|
407
|
+
while (k < state.buffer.length && state.buffer.charCodeAt(k) === BACKTICK) {
|
|
408
|
+
count++;
|
|
409
|
+
k++;
|
|
410
|
+
}
|
|
411
|
+
if (k >= state.buffer.length && !forced) return false; // run could still grow
|
|
412
|
+
if (count >= cb.backtick_count) {
|
|
413
|
+
let m = k;
|
|
414
|
+
while (m < state.buffer.length && is_line_whitespace(state.buffer.charCodeAt(m))) {
|
|
415
|
+
m++;
|
|
416
|
+
}
|
|
417
|
+
if (m >= state.buffer.length && !forced) return false; // trailing content could disqualify
|
|
418
|
+
if (m >= state.buffer.length || state.buffer.charCodeAt(m) === NEWLINE) {
|
|
419
|
+
emit(state, {type: 'close', id: cb.id, end: offset(state, m)});
|
|
420
|
+
state.codeblock = null;
|
|
421
|
+
state.pos = m; // at the closer's newline (or EOF)
|
|
422
|
+
state.column = m - line_start;
|
|
423
|
+
return true;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
// not a closer — the line is content
|
|
427
|
+
}
|
|
428
|
+
// content line — emit it whole once its newline arrives (or at EOF when forced)
|
|
429
|
+
const nl = state.buffer.indexOf('\n', j);
|
|
430
|
+
if (nl === -1 && !forced) return false; // hold the partial line
|
|
431
|
+
const line_end = nl === -1 ? state.buffer.length : nl;
|
|
432
|
+
const strip = line_start + Math.min(cb.fence_indent, line_indent);
|
|
433
|
+
const content = state.buffer.slice(strip, line_end);
|
|
434
|
+
emit_codeblock_text(state, cb, cb.text_id !== null ? '\n' + content : content, strip);
|
|
435
|
+
state.pos = nl === -1 ? line_end : line_end + 1;
|
|
436
|
+
state.column = 0;
|
|
437
|
+
state.prev_char = NEWLINE;
|
|
438
|
+
}
|
|
439
|
+
return true;
|
|
440
|
+
};
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* Check if the buffer at current position has a codeblock closing fence —
|
|
444
|
+
* a run of at least `backtick_count` backticks with only whitespace to end
|
|
445
|
+
* of line. Returns the position after the fence (including trailing newline)
|
|
446
|
+
* on success, `-1` when definitely not a fence, or `-2` when more input is
|
|
447
|
+
* needed to decide.
|
|
448
|
+
*
|
|
449
|
+
* When `forced` is true, EOF after the fence counts as a valid terminator
|
|
450
|
+
* (matches `mdz_parse`'s handling of code blocks at end of input).
|
|
451
|
+
*
|
|
452
|
+
* @nodocs
|
|
453
|
+
*/
|
|
454
|
+
export const match_codeblock_close = (
|
|
455
|
+
state: MdzStreamParserState,
|
|
456
|
+
backtick_count: number,
|
|
457
|
+
forced: boolean = false,
|
|
458
|
+
): number => {
|
|
459
|
+
let i = state.pos;
|
|
460
|
+
|
|
461
|
+
// count backticks
|
|
462
|
+
let count = 0;
|
|
463
|
+
while (i < state.buffer.length && state.buffer.charCodeAt(i) === BACKTICK) {
|
|
464
|
+
count++;
|
|
465
|
+
i++;
|
|
466
|
+
}
|
|
467
|
+
if (count < backtick_count) {
|
|
468
|
+
// buffer ended mid-backtick-sequence — could still grow into a fence
|
|
469
|
+
if (!forced && i >= state.buffer.length) return -2;
|
|
470
|
+
return -1;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// trailing whitespace allowed
|
|
474
|
+
while (i < state.buffer.length && is_line_whitespace(state.buffer.charCodeAt(i))) {
|
|
475
|
+
i++;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// must be followed by newline or EOF
|
|
479
|
+
if (i < state.buffer.length && state.buffer.charCodeAt(i) === NEWLINE) {
|
|
480
|
+
return i + 1;
|
|
481
|
+
}
|
|
482
|
+
if (i >= state.buffer.length) {
|
|
483
|
+
// in forced mode, EOF terminates the fence
|
|
484
|
+
if (forced) return i;
|
|
485
|
+
// enough backticks but buffer ended — the run could still grow, or
|
|
486
|
+
// whitespace/newline/disqualifying content could follow
|
|
487
|
+
return -2;
|
|
488
|
+
}
|
|
489
|
+
return -1;
|
|
490
|
+
};
|