@fuzdev/fuz_ui 0.198.1 → 0.200.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/dist/ApiDeclarationList.svelte +2 -1
- package/dist/ApiDeclarationList.svelte.d.ts.map +1 -1
- package/dist/ApiIndex.svelte +2 -2
- package/dist/ApiModule.svelte +3 -3
- package/dist/DeclarationDetail.svelte +145 -90
- package/dist/DeclarationDetail.svelte.d.ts.map +1 -1
- package/dist/LibraryDetail.svelte +14 -16
- package/dist/LibraryDetail.svelte.d.ts.map +1 -1
- package/dist/LibrarySummary.svelte +12 -12
- package/dist/Mdz.svelte +2 -2
- package/dist/MdzNodeView.svelte +8 -6
- package/dist/MdzNodeView.svelte.d.ts.map +1 -1
- package/dist/MdzRoot.svelte +30 -0
- package/dist/MdzRoot.svelte.d.ts +12 -0
- package/dist/MdzRoot.svelte.d.ts.map +1 -0
- package/dist/MdzStream.svelte +32 -0
- package/dist/MdzStream.svelte.d.ts +12 -0
- package/dist/MdzStream.svelte.d.ts.map +1 -0
- package/dist/MdzStreamNodeView.svelte +106 -0
- package/dist/MdzStreamNodeView.svelte.d.ts +9 -0
- package/dist/MdzStreamNodeView.svelte.d.ts.map +1 -0
- package/dist/ProjectLinks.svelte +1 -1
- package/dist/api_search.svelte.d.ts.map +1 -1
- package/dist/api_search.svelte.js +4 -2
- package/dist/declaration.svelte.d.ts +11 -0
- package/dist/declaration.svelte.d.ts.map +1 -1
- package/dist/declaration.svelte.js +15 -3
- package/dist/library.svelte.d.ts +9 -70
- package/dist/library.svelte.d.ts.map +1 -1
- package/dist/library.svelte.js +24 -18
- package/dist/library_helpers.d.ts +3 -3
- package/dist/library_helpers.js +3 -3
- package/dist/mdz.d.ts.map +1 -1
- package/dist/mdz.js +37 -29
- package/dist/mdz_components.d.ts +37 -23
- package/dist/mdz_components.d.ts.map +1 -1
- package/dist/mdz_components.js +16 -4
- package/dist/mdz_helpers.d.ts +46 -14
- package/dist/mdz_helpers.d.ts.map +1 -1
- package/dist/mdz_helpers.js +189 -56
- package/dist/mdz_lexer.d.ts.map +1 -1
- package/dist/mdz_lexer.js +18 -22
- package/dist/mdz_opcodes.d.ts +174 -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 +20 -0
- package/dist/mdz_opcodes_to_nodes.d.ts.map +1 -0
- package/dist/mdz_opcodes_to_nodes.js +332 -0
- package/dist/mdz_stream_parser.d.ts +80 -0
- package/dist/mdz_stream_parser.d.ts.map +1 -0
- package/dist/mdz_stream_parser.js +354 -0
- package/dist/mdz_stream_parser_block.d.ts +76 -0
- package/dist/mdz_stream_parser_block.d.ts.map +1 -0
- package/dist/mdz_stream_parser_block.js +458 -0
- package/dist/mdz_stream_parser_inline.d.ts +52 -0
- package/dist/mdz_stream_parser_inline.d.ts.map +1 -0
- package/dist/mdz_stream_parser_inline.js +378 -0
- package/dist/mdz_stream_parser_link.d.ts +22 -0
- package/dist/mdz_stream_parser_link.d.ts.map +1 -0
- package/dist/mdz_stream_parser_link.js +215 -0
- package/dist/mdz_stream_parser_state.d.ts +187 -0
- package/dist/mdz_stream_parser_state.d.ts.map +1 -0
- package/dist/mdz_stream_parser_state.js +398 -0
- package/dist/mdz_stream_parser_text.d.ts +14 -0
- package/dist/mdz_stream_parser_text.d.ts.map +1 -0
- package/dist/mdz_stream_parser_text.js +50 -0
- package/dist/mdz_stream_parser_url.d.ts +60 -0
- package/dist/mdz_stream_parser_url.d.ts.map +1 -0
- package/dist/mdz_stream_parser_url.js +307 -0
- package/dist/mdz_stream_state.svelte.d.ts +44 -0
- package/dist/mdz_stream_state.svelte.d.ts.map +1 -0
- package/dist/mdz_stream_state.svelte.js +357 -0
- package/dist/mdz_token_parser.d.ts.map +1 -1
- package/dist/mdz_token_parser.js +8 -39
- package/dist/module.svelte.d.ts +2 -0
- package/dist/module.svelte.d.ts.map +1 -1
- package/dist/module.svelte.js +3 -1
- package/dist/site.svelte.d.ts +13 -0
- package/dist/site.svelte.d.ts.map +1 -1
- package/dist/site.svelte.js +8 -2
- package/dist/tsdoc_mdz.d.ts +2 -2
- package/dist/tsdoc_mdz.js +2 -2
- package/dist/vite_plugin_pkg_json.d.ts +63 -0
- package/dist/vite_plugin_pkg_json.d.ts.map +1 -0
- package/dist/vite_plugin_pkg_json.js +126 -0
- package/package.json +8 -7
- package/src/lib/api_search.svelte.ts +4 -2
- package/src/lib/declaration.svelte.ts +18 -3
- package/src/lib/library.svelte.ts +37 -20
- package/src/lib/library_helpers.ts +3 -3
- package/src/lib/mdz.ts +38 -29
- package/src/lib/mdz_components.ts +40 -19
- package/src/lib/mdz_helpers.ts +199 -56
- package/src/lib/mdz_lexer.ts +18 -20
- package/src/lib/mdz_opcodes.ts +205 -0
- package/src/lib/mdz_opcodes_to_nodes.ts +375 -0
- package/src/lib/mdz_stream_parser.ts +415 -0
- package/src/lib/mdz_stream_parser_block.ts +532 -0
- package/src/lib/mdz_stream_parser_inline.ts +414 -0
- package/src/lib/mdz_stream_parser_link.ts +271 -0
- package/src/lib/mdz_stream_parser_state.ts +539 -0
- package/src/lib/mdz_stream_parser_text.ts +77 -0
- package/src/lib/mdz_stream_parser_url.ts +365 -0
- package/src/lib/mdz_stream_state.svelte.ts +387 -0
- package/src/lib/mdz_token_parser.ts +13 -40
- package/src/lib/module.svelte.ts +5 -1
- package/src/lib/site.svelte.ts +17 -2
- package/src/lib/tsdoc_mdz.ts +2 -2
- package/src/lib/vite_plugin_pkg_json.ts +142 -0
- package/dist/package_helpers.d.ts +0 -150
- package/dist/package_helpers.d.ts.map +0 -1
- package/dist/package_helpers.js +0 -179
- package/src/lib/package_helpers.ts +0 -186
|
@@ -0,0 +1,532 @@
|
|
|
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
|
+
} from './mdz_helpers.js';
|
|
20
|
+
import {
|
|
21
|
+
type CodeblockState,
|
|
22
|
+
type MdzStreamParserState,
|
|
23
|
+
type TryResult,
|
|
24
|
+
accumulate_text,
|
|
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
|
+
* Returns true if consumed, false if needs more input, null if definitely not a heading.
|
|
36
|
+
*/
|
|
37
|
+
export const try_heading = (state: MdzStreamParserState): TryResult => {
|
|
38
|
+
const start = state.pos;
|
|
39
|
+
let i = start;
|
|
40
|
+
|
|
41
|
+
// count hashes
|
|
42
|
+
let hash_count = 0;
|
|
43
|
+
while (
|
|
44
|
+
i < state.buffer.length &&
|
|
45
|
+
state.buffer.charCodeAt(i) === HASH &&
|
|
46
|
+
hash_count <= MAX_HEADING_LEVEL
|
|
47
|
+
) {
|
|
48
|
+
hash_count++;
|
|
49
|
+
i++;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (hash_count === 0 || hash_count > MAX_HEADING_LEVEL) return 'not_match';
|
|
53
|
+
|
|
54
|
+
// need at least one more char to check for space
|
|
55
|
+
if (i >= state.buffer.length) return 'need_more';
|
|
56
|
+
|
|
57
|
+
// must have space after hashes
|
|
58
|
+
if (state.buffer.charCodeAt(i) !== SPACE) return 'not_match';
|
|
59
|
+
i++; // consume space
|
|
60
|
+
|
|
61
|
+
// must have at least one non-whitespace char (but we might not have it yet)
|
|
62
|
+
// for streaming, we'll open the heading optimistically when we see `# `
|
|
63
|
+
// if the content turns out empty, that's an edge case we can handle on close
|
|
64
|
+
|
|
65
|
+
// close any open paragraph first
|
|
66
|
+
flush_text(state);
|
|
67
|
+
close_paragraph(state);
|
|
68
|
+
|
|
69
|
+
const id = alloc_id(state);
|
|
70
|
+
const heading_start = offset(state, start);
|
|
71
|
+
emit(state, {
|
|
72
|
+
type: 'open',
|
|
73
|
+
id,
|
|
74
|
+
node_type: 'Heading',
|
|
75
|
+
start: heading_start,
|
|
76
|
+
level: hash_count as 1 | 2 | 3 | 4 | 5 | 6,
|
|
77
|
+
});
|
|
78
|
+
push_stack_entry(state, id, 'Heading', heading_start);
|
|
79
|
+
state.in_heading = true;
|
|
80
|
+
state.heading_text_parts = [''];
|
|
81
|
+
state.pos = i;
|
|
82
|
+
state.column = i - start;
|
|
83
|
+
state.prev_char = SPACE;
|
|
84
|
+
return 'consumed';
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Try to parse a horizontal rule at column 0.
|
|
89
|
+
* Returns true if consumed, false if needs more input, null if definitely not an HR.
|
|
90
|
+
* When `forced` is true, treats buffer end as EOF (no more input coming).
|
|
91
|
+
*/
|
|
92
|
+
export const try_hr = (state: MdzStreamParserState, forced = false): TryResult => {
|
|
93
|
+
const start = state.pos;
|
|
94
|
+
let i = start;
|
|
95
|
+
|
|
96
|
+
// must have exactly 3 hyphens
|
|
97
|
+
if (i + HR_HYPHEN_COUNT > state.buffer.length) return 'need_more';
|
|
98
|
+
for (let j = 0; j < HR_HYPHEN_COUNT; j++) {
|
|
99
|
+
if (state.buffer.charCodeAt(i + j) !== HYPHEN) return 'not_match';
|
|
100
|
+
}
|
|
101
|
+
i += HR_HYPHEN_COUNT;
|
|
102
|
+
|
|
103
|
+
// after hyphens, only spaces allowed before newline/EOF
|
|
104
|
+
while (i < state.buffer.length) {
|
|
105
|
+
const c = state.buffer.charCodeAt(i);
|
|
106
|
+
if (c === NEWLINE) break;
|
|
107
|
+
if (c !== SPACE) return 'not_match';
|
|
108
|
+
i++;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// if buffer ends without newline/EOF confirmation, need more input (unless forced)
|
|
112
|
+
if (i >= state.buffer.length && !forced) {
|
|
113
|
+
return 'need_more';
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
flush_text(state);
|
|
117
|
+
close_paragraph(state);
|
|
118
|
+
|
|
119
|
+
const id = alloc_id(state);
|
|
120
|
+
emit(state, {
|
|
121
|
+
type: 'void',
|
|
122
|
+
id,
|
|
123
|
+
node_type: 'Hr',
|
|
124
|
+
start: offset(state, start),
|
|
125
|
+
end: offset(state, i),
|
|
126
|
+
});
|
|
127
|
+
if (forced) {
|
|
128
|
+
state.pos = state.buffer.length;
|
|
129
|
+
} else {
|
|
130
|
+
state.pos = i;
|
|
131
|
+
if (i < state.buffer.length && state.buffer.charCodeAt(i) === NEWLINE) {
|
|
132
|
+
state.pos++; // consume the newline
|
|
133
|
+
}
|
|
134
|
+
// skip additional newlines
|
|
135
|
+
while (state.pos < state.buffer.length && state.buffer.charCodeAt(state.pos) === NEWLINE) {
|
|
136
|
+
state.pos++;
|
|
137
|
+
}
|
|
138
|
+
if (state.pos >= state.buffer.length) {
|
|
139
|
+
state.skip_leading_newlines = true;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
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.
|
|
149
|
+
*
|
|
150
|
+
* Opens the codeblock optimistically when possible. When the current paragraph
|
|
151
|
+
* has no prior content, opens immediately on `\`\`\`lang\n` without lookahead,
|
|
152
|
+
* enabling streaming. When the paragraph has content (text before the fence on
|
|
153
|
+
* a prior line), falls back to lookahead via `find_closing_fence` to avoid
|
|
154
|
+
* splitting the paragraph on revert.
|
|
155
|
+
*/
|
|
156
|
+
export const try_codeblock_open = (state: MdzStreamParserState): TryResult => {
|
|
157
|
+
const start = state.pos;
|
|
158
|
+
let i = start;
|
|
159
|
+
|
|
160
|
+
// count backticks
|
|
161
|
+
let backtick_count = 0;
|
|
162
|
+
while (i < state.buffer.length && state.buffer.charCodeAt(i) === BACKTICK) {
|
|
163
|
+
backtick_count++;
|
|
164
|
+
i++;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (backtick_count < MIN_CODEBLOCK_BACKTICKS) {
|
|
168
|
+
// if buffer ended mid-backtick-sequence, need more input
|
|
169
|
+
if (i >= state.buffer.length) return 'need_more';
|
|
170
|
+
// definitely not enough backticks
|
|
171
|
+
return 'not_match';
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// find end of opening fence line (need newline)
|
|
175
|
+
const lang_start = i;
|
|
176
|
+
while (i < state.buffer.length) {
|
|
177
|
+
const c = state.buffer.charCodeAt(i);
|
|
178
|
+
if (c === NEWLINE) break;
|
|
179
|
+
if (c === SPACE) break;
|
|
180
|
+
i++;
|
|
181
|
+
}
|
|
182
|
+
const lang = i > lang_start ? state.buffer.slice(lang_start, i) : null;
|
|
183
|
+
|
|
184
|
+
// skip trailing spaces on opening fence line
|
|
185
|
+
while (i < state.buffer.length && state.buffer.charCodeAt(i) === SPACE) {
|
|
186
|
+
i++;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// must find newline
|
|
190
|
+
if (i >= state.buffer.length) return 'need_more';
|
|
191
|
+
if (state.buffer.charCodeAt(i) !== NEWLINE) return 'not_match';
|
|
192
|
+
i++; // consume newline
|
|
193
|
+
|
|
194
|
+
// when the current paragraph has content, closing it would split text on
|
|
195
|
+
// revert — fall back to lookahead to avoid that
|
|
196
|
+
if (state.in_paragraph && (state.accumulated_text.length > 0 || state.active_text_id !== null)) {
|
|
197
|
+
const closing_result = find_closing_fence(state, i, backtick_count);
|
|
198
|
+
if (closing_result === 'not_found') return 'need_more';
|
|
199
|
+
if (closing_result === 'invalid') return 'not_match';
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// build delimiter for potential revert (e.g. "```ts\n" or "```\n")
|
|
203
|
+
const delimiter = state.buffer.slice(start, i);
|
|
204
|
+
|
|
205
|
+
flush_text(state);
|
|
206
|
+
close_paragraph(state);
|
|
207
|
+
|
|
208
|
+
const id = alloc_id(state);
|
|
209
|
+
const cb_start = offset(state, start);
|
|
210
|
+
emit(state, {type: 'open', id, node_type: 'Codeblock', start: cb_start, lang});
|
|
211
|
+
state.codeblock = {id, backtick_count, text_id: null, delimiter, start: cb_start};
|
|
212
|
+
state.pos = i;
|
|
213
|
+
state.column = 0;
|
|
214
|
+
state.prev_char = NEWLINE;
|
|
215
|
+
return 'consumed';
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Scan buffer from `start` for a valid closing fence with `backtick_count` backticks.
|
|
220
|
+
* Used as a fallback when the current paragraph has prior content (to avoid
|
|
221
|
+
* splitting the paragraph on revert). Returns 'found' if a valid closing fence
|
|
222
|
+
* exists, 'not_found' if the buffer ends before we can determine, 'invalid' if
|
|
223
|
+
* we can see the full remaining content and there's no valid closing fence.
|
|
224
|
+
*/
|
|
225
|
+
export const find_closing_fence = (
|
|
226
|
+
state: MdzStreamParserState,
|
|
227
|
+
start: number,
|
|
228
|
+
backtick_count: number,
|
|
229
|
+
): 'found' | 'not_found' | 'invalid' => {
|
|
230
|
+
let i = start;
|
|
231
|
+
const content_start = start;
|
|
232
|
+
let at_col0 = true;
|
|
233
|
+
|
|
234
|
+
while (i < state.buffer.length) {
|
|
235
|
+
if (at_col0 && state.buffer.charCodeAt(i) === BACKTICK) {
|
|
236
|
+
// count backticks
|
|
237
|
+
let count = 0;
|
|
238
|
+
const fence_start = i;
|
|
239
|
+
while (i < state.buffer.length && state.buffer.charCodeAt(i) === BACKTICK) {
|
|
240
|
+
count++;
|
|
241
|
+
i++;
|
|
242
|
+
}
|
|
243
|
+
if (count === backtick_count) {
|
|
244
|
+
// skip trailing spaces
|
|
245
|
+
while (i < state.buffer.length && state.buffer.charCodeAt(i) === SPACE) {
|
|
246
|
+
i++;
|
|
247
|
+
}
|
|
248
|
+
// must be followed by newline or EOF
|
|
249
|
+
if (i >= state.buffer.length || state.buffer.charCodeAt(i) === NEWLINE) {
|
|
250
|
+
// check for empty content
|
|
251
|
+
const content = state.buffer.slice(content_start, fence_start);
|
|
252
|
+
const final_content = content.endsWith('\n') ? content.slice(0, -1) : content;
|
|
253
|
+
if (final_content.length === 0) return 'invalid'; // empty codeblock
|
|
254
|
+
return 'found';
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
// not a valid fence — continue scanning
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (state.buffer.charCodeAt(i) === NEWLINE) {
|
|
262
|
+
at_col0 = true;
|
|
263
|
+
} else {
|
|
264
|
+
at_col0 = false;
|
|
265
|
+
}
|
|
266
|
+
i++;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// reached end of buffer without finding closing fence
|
|
270
|
+
return 'not_found';
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Emit codeblock content as `text` (first chunk) or `append_text` (subsequent
|
|
275
|
+
* chunks), updating `cb.text_id` on first emit. `content_start` is the local
|
|
276
|
+
* buffer position where the content slice began — only used to compute the
|
|
277
|
+
* global byte offset for the first `text` opcode.
|
|
278
|
+
*/
|
|
279
|
+
export const emit_codeblock_text = (
|
|
280
|
+
state: MdzStreamParserState,
|
|
281
|
+
cb: CodeblockState,
|
|
282
|
+
content: string,
|
|
283
|
+
content_start: number,
|
|
284
|
+
): void => {
|
|
285
|
+
if (cb.text_id !== null) {
|
|
286
|
+
emit(state, {type: 'append_text', id: cb.text_id, content});
|
|
287
|
+
} else {
|
|
288
|
+
const id = alloc_id(state);
|
|
289
|
+
const text_start = offset(state, content_start);
|
|
290
|
+
emit(state, {
|
|
291
|
+
type: 'text',
|
|
292
|
+
id,
|
|
293
|
+
content,
|
|
294
|
+
text_type: 'Text',
|
|
295
|
+
start: text_start,
|
|
296
|
+
end: text_start + content.length,
|
|
297
|
+
});
|
|
298
|
+
cb.text_id = id;
|
|
299
|
+
}
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Drain the remaining buffer as codeblock content at EOF, looking for a closing
|
|
304
|
+
* fence whose terminator is EOF rather than `\n`. Matches `mdz_parse`'s
|
|
305
|
+
* `#match_code_block`, which allows a fence at the end of the document.
|
|
306
|
+
*
|
|
307
|
+
* If a fence is found, the codeblock closes normally. Otherwise the remaining
|
|
308
|
+
* buffer becomes content and `close_codeblock_at_eof` (in state.ts) handles
|
|
309
|
+
* the revert-and-wrap dance after process completes.
|
|
310
|
+
*/
|
|
311
|
+
export const process_codeblock_forced = (state: MdzStreamParserState): void => {
|
|
312
|
+
const cb = state.codeblock!;
|
|
313
|
+
const start = state.pos;
|
|
314
|
+
|
|
315
|
+
while (state.pos < state.buffer.length) {
|
|
316
|
+
if (state.column === 0) {
|
|
317
|
+
const fence_match = match_codeblock_close(state, cb.backtick_count, true);
|
|
318
|
+
if (fence_match !== -1) {
|
|
319
|
+
// emit content up to the closing fence (excluding trailing newline before fence)
|
|
320
|
+
let content_end = state.pos;
|
|
321
|
+
if (content_end > start && state.buffer.charCodeAt(content_end - 1) === NEWLINE) {
|
|
322
|
+
content_end--;
|
|
323
|
+
}
|
|
324
|
+
const content = state.buffer.slice(start, content_end);
|
|
325
|
+
if (content.length > 0) {
|
|
326
|
+
emit_codeblock_text(state, cb, content, start);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// empty codeblock — revert (matches process_codeblock behavior)
|
|
330
|
+
if (cb.text_id === null && content.length === 0) {
|
|
331
|
+
revert_empty_codeblock(state, cb);
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
emit(state, {type: 'close', id: cb.id, end: offset(state, fence_match)});
|
|
336
|
+
state.pos = fence_match;
|
|
337
|
+
state.codeblock = null;
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
const c = state.buffer.charCodeAt(state.pos);
|
|
343
|
+
if (c === NEWLINE) {
|
|
344
|
+
state.column = 0;
|
|
345
|
+
} else {
|
|
346
|
+
state.column++;
|
|
347
|
+
}
|
|
348
|
+
state.pos++;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// no closing fence — dump the rest as codeblock content; caller's
|
|
352
|
+
// `close_codeblock_at_eof` then reverts the (now-unclosed) codeblock.
|
|
353
|
+
const remaining = state.buffer.slice(start, state.pos);
|
|
354
|
+
if (remaining.length > 0) {
|
|
355
|
+
emit_codeblock_text(state, cb, remaining, start);
|
|
356
|
+
}
|
|
357
|
+
};
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Process bytes while in codeblock mode.
|
|
361
|
+
* Returns true if processing should continue, false if more input needed.
|
|
362
|
+
*/
|
|
363
|
+
export const process_codeblock = (state: MdzStreamParserState): boolean => {
|
|
364
|
+
const cb = state.codeblock!;
|
|
365
|
+
const start = state.pos;
|
|
366
|
+
|
|
367
|
+
while (state.pos < state.buffer.length) {
|
|
368
|
+
// check for closing fence at column 0
|
|
369
|
+
if (state.column === 0) {
|
|
370
|
+
const fence_match = match_codeblock_close(state, cb.backtick_count);
|
|
371
|
+
if (fence_match === -2) {
|
|
372
|
+
// need more input — hold from this position (don't consume potential fence chars)
|
|
373
|
+
break;
|
|
374
|
+
}
|
|
375
|
+
if (fence_match !== -1) {
|
|
376
|
+
// emit content up to the closing fence (excluding trailing newline before fence)
|
|
377
|
+
let content_end = state.pos;
|
|
378
|
+
if (content_end > start && state.buffer.charCodeAt(content_end - 1) === NEWLINE) {
|
|
379
|
+
content_end--;
|
|
380
|
+
}
|
|
381
|
+
const content = state.buffer.slice(start, content_end);
|
|
382
|
+
if (content.length > 0) {
|
|
383
|
+
emit_codeblock_text(state, cb, content, start);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// empty codeblock: no content was ever emitted → invalid, revert
|
|
387
|
+
if (cb.text_id === null && content.length === 0) {
|
|
388
|
+
return revert_empty_codeblock(state, cb);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// codeblock end = position of newline after closing fence (exclusive)
|
|
392
|
+
emit(state, {type: 'close', id: cb.id, end: offset(state, fence_match - 1)});
|
|
393
|
+
state.pos = fence_match;
|
|
394
|
+
state.codeblock = null;
|
|
395
|
+
state.column = 0;
|
|
396
|
+
state.prev_char = NEWLINE;
|
|
397
|
+
// skip newlines after codeblock
|
|
398
|
+
while (state.pos < state.buffer.length && state.buffer.charCodeAt(state.pos) === NEWLINE) {
|
|
399
|
+
state.pos++;
|
|
400
|
+
}
|
|
401
|
+
// if buffer ended before absorbing all trailing newlines, continue in next chunk
|
|
402
|
+
if (state.pos >= state.buffer.length) {
|
|
403
|
+
state.skip_leading_newlines = true;
|
|
404
|
+
}
|
|
405
|
+
return true;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// not a closing fence — advance
|
|
410
|
+
const c = state.buffer.charCodeAt(state.pos);
|
|
411
|
+
if (c === NEWLINE) {
|
|
412
|
+
state.column = 0;
|
|
413
|
+
} else {
|
|
414
|
+
state.column++;
|
|
415
|
+
}
|
|
416
|
+
state.pos++;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// buffer exhausted or broke for potential fence — emit content so far, hold trailing data.
|
|
420
|
+
// Hold trailing \n (could be before a closing fence in next chunk), and also hold
|
|
421
|
+
// \n + backticks when we broke mid-fence-check (the \n precedes the backticks and
|
|
422
|
+
// would be stripped by fence close, so must not be emitted as content yet).
|
|
423
|
+
let emit_end = state.pos;
|
|
424
|
+
if (emit_end > start && state.buffer.charCodeAt(emit_end - 1) === NEWLINE) {
|
|
425
|
+
emit_end--;
|
|
426
|
+
state.pos = emit_end; // leave \n (+ any held backticks after it) in buffer
|
|
427
|
+
}
|
|
428
|
+
const content = state.buffer.slice(start, emit_end);
|
|
429
|
+
if (content.length > 0) {
|
|
430
|
+
emit_codeblock_text(state, cb, content, start);
|
|
431
|
+
}
|
|
432
|
+
return false; // need more input
|
|
433
|
+
};
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* Check if the buffer at current position has a codeblock closing fence.
|
|
437
|
+
* Returns the position after the fence (including trailing newline) on success,
|
|
438
|
+
* `-1` when definitely not a fence, or `-2` when more input is needed to decide.
|
|
439
|
+
*
|
|
440
|
+
* When `forced` is true, EOF after the fence counts as a valid terminator
|
|
441
|
+
* (matches `mdz_parse`'s handling of code blocks at end of input).
|
|
442
|
+
*/
|
|
443
|
+
export const match_codeblock_close = (
|
|
444
|
+
state: MdzStreamParserState,
|
|
445
|
+
backtick_count: number,
|
|
446
|
+
forced: boolean = false,
|
|
447
|
+
): number => {
|
|
448
|
+
let i = state.pos;
|
|
449
|
+
|
|
450
|
+
// count backticks
|
|
451
|
+
let count = 0;
|
|
452
|
+
while (i < state.buffer.length && state.buffer.charCodeAt(i) === BACKTICK) {
|
|
453
|
+
count++;
|
|
454
|
+
i++;
|
|
455
|
+
}
|
|
456
|
+
if (count !== backtick_count) {
|
|
457
|
+
// buffer ended mid-backtick-sequence — could still become a fence
|
|
458
|
+
if (!forced && i >= state.buffer.length && count < backtick_count) return -2;
|
|
459
|
+
return -1;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// trailing spaces allowed
|
|
463
|
+
while (i < state.buffer.length && state.buffer.charCodeAt(i) === SPACE) {
|
|
464
|
+
i++;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// must be followed by newline or EOF
|
|
468
|
+
if (i < state.buffer.length && state.buffer.charCodeAt(i) === NEWLINE) {
|
|
469
|
+
return i + 1;
|
|
470
|
+
}
|
|
471
|
+
if (i >= state.buffer.length) {
|
|
472
|
+
// in forced mode, EOF terminates the fence
|
|
473
|
+
if (forced) return i;
|
|
474
|
+
// exact backtick count but buffer ended (could be spaces or newline coming)
|
|
475
|
+
return -2;
|
|
476
|
+
}
|
|
477
|
+
return -1;
|
|
478
|
+
};
|
|
479
|
+
|
|
480
|
+
/**
|
|
481
|
+
* Revert an empty codeblock (closing fence found but no content was emitted).
|
|
482
|
+
* Wraps the opening fence delimiter in a new paragraph and accumulates the
|
|
483
|
+
* closing fence text for normal paragraph processing.
|
|
484
|
+
*/
|
|
485
|
+
export const revert_empty_codeblock = (
|
|
486
|
+
state: MdzStreamParserState,
|
|
487
|
+
cb: CodeblockState,
|
|
488
|
+
): boolean => {
|
|
489
|
+
const wrap_id = alloc_id(state);
|
|
490
|
+
emit(state, {
|
|
491
|
+
type: 'revert',
|
|
492
|
+
id: cb.id,
|
|
493
|
+
replacement_text: cb.delimiter,
|
|
494
|
+
start: cb.start,
|
|
495
|
+
wrap_node_type: 'Paragraph',
|
|
496
|
+
wrap_id,
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
// enter paragraph mode with the wrapper
|
|
500
|
+
push_stack_entry(state, wrap_id, 'Paragraph', cb.start);
|
|
501
|
+
state.in_paragraph = true;
|
|
502
|
+
state.active_text_id = null;
|
|
503
|
+
state.codeblock = null;
|
|
504
|
+
|
|
505
|
+
// accumulate the closing fence text (backticks + spaces, without trailing newline)
|
|
506
|
+
const fence_start = state.pos;
|
|
507
|
+
let fence_content_end = fence_start;
|
|
508
|
+
while (
|
|
509
|
+
fence_content_end < state.buffer.length &&
|
|
510
|
+
state.buffer.charCodeAt(fence_content_end) === BACKTICK
|
|
511
|
+
) {
|
|
512
|
+
fence_content_end++;
|
|
513
|
+
}
|
|
514
|
+
while (
|
|
515
|
+
fence_content_end < state.buffer.length &&
|
|
516
|
+
state.buffer.charCodeAt(fence_content_end) === SPACE
|
|
517
|
+
) {
|
|
518
|
+
fence_content_end++;
|
|
519
|
+
}
|
|
520
|
+
accumulate_text(
|
|
521
|
+
state,
|
|
522
|
+
state.buffer.slice(fence_start, fence_content_end),
|
|
523
|
+
offset(state, fence_start),
|
|
524
|
+
);
|
|
525
|
+
|
|
526
|
+
// position past the closing fence content, leaving any trailing newline for normal processing
|
|
527
|
+
state.pos = fence_content_end;
|
|
528
|
+
state.column = fence_content_end - fence_start;
|
|
529
|
+
state.prev_char =
|
|
530
|
+
fence_content_end > fence_start ? state.buffer.charCodeAt(fence_content_end - 1) : NEWLINE;
|
|
531
|
+
return true;
|
|
532
|
+
};
|