@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,766 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Streaming opcode parser for mdz.
|
|
3
|
+
*
|
|
4
|
+
* Fed chunks of text (e.g., from LLM output), emits opcodes as rendering
|
|
5
|
+
* instructions. Makes optimistic assumptions about ambiguous syntax and
|
|
6
|
+
* emits `revert` opcodes to correct when wrong. Never re-parses —
|
|
7
|
+
* corrections are bounded, explicit opcodes (`revert`, `wrap`, `trim_text`).
|
|
8
|
+
*
|
|
9
|
+
* The design is derived from
|
|
10
|
+
* {@link https://bsky.app/profile/pngwn.at/post/3mi527zntb22n @pngwn.at}'s
|
|
11
|
+
* ideas: restrict the syntax so streaming is tractable, render
|
|
12
|
+
* optimistically and correct when wrong, emit serializable opcodes to avoid
|
|
13
|
+
* re-parsing, and keep the opcodes target-agnostic so any renderer can
|
|
14
|
+
* consume them. mdz diverges in one respect: the Svelte consumer
|
|
15
|
+
* (`MdzStreamState`) does build a reactive tree from opcodes — the platform
|
|
16
|
+
* dictates this — but mutations are fine-grained via `$state`, not diffed.
|
|
17
|
+
*
|
|
18
|
+
* The parser is split across sibling modules: this file holds the public
|
|
19
|
+
* `MdzStreamParser` class and the `process_loop` / `process_inline`
|
|
20
|
+
* orchestrators. Per-category handlers (block / inline / link / url / text)
|
|
21
|
+
* live in `mdz_stream_parser_*.ts` as free functions taking the shared
|
|
22
|
+
* `MdzStreamParserState` as first argument.
|
|
23
|
+
*
|
|
24
|
+
* Usage:
|
|
25
|
+
* ```ts
|
|
26
|
+
* const parser = new MdzStreamParser();
|
|
27
|
+
* parser.feed('hello **bold');
|
|
28
|
+
* const ops1 = parser.take_opcodes(); // open Paragraph, text "hello ", open Bold, text "bold"
|
|
29
|
+
* parser.feed('** world');
|
|
30
|
+
* const ops2 = parser.take_opcodes(); // close Bold, text " world"
|
|
31
|
+
* parser.finish();
|
|
32
|
+
* const ops3 = parser.take_opcodes(); // close Paragraph
|
|
33
|
+
* ```
|
|
34
|
+
*
|
|
35
|
+
* @module
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
import type {MdzOpcode} from './mdz_opcodes.js';
|
|
39
|
+
import {
|
|
40
|
+
BACKTICK,
|
|
41
|
+
ASTERISK,
|
|
42
|
+
UNDERSCORE,
|
|
43
|
+
TILDE,
|
|
44
|
+
NEWLINE,
|
|
45
|
+
HYPHEN,
|
|
46
|
+
HASH,
|
|
47
|
+
LEFT_ANGLE,
|
|
48
|
+
RIGHT_ANGLE,
|
|
49
|
+
SLASH,
|
|
50
|
+
LEFT_BRACKET,
|
|
51
|
+
RIGHT_BRACKET,
|
|
52
|
+
H_LOWER,
|
|
53
|
+
H_UPPER,
|
|
54
|
+
PERIOD,
|
|
55
|
+
SPACE,
|
|
56
|
+
ZERO,
|
|
57
|
+
NINE,
|
|
58
|
+
is_word_char,
|
|
59
|
+
is_line_whitespace,
|
|
60
|
+
mdz_match_blockquote_prefix,
|
|
61
|
+
mdz_blockquote_line_has_content,
|
|
62
|
+
mdz_blockquote_strip_width,
|
|
63
|
+
mdz_remap_segments,
|
|
64
|
+
} from './mdz_helpers.js';
|
|
65
|
+
import {
|
|
66
|
+
type MdzStreamParserState,
|
|
67
|
+
type TryResult,
|
|
68
|
+
accumulate_text,
|
|
69
|
+
alloc_id,
|
|
70
|
+
close_all_list_levels,
|
|
71
|
+
close_codeblock_at_eof,
|
|
72
|
+
close_heading,
|
|
73
|
+
close_paragraph,
|
|
74
|
+
create_state,
|
|
75
|
+
emit,
|
|
76
|
+
find_open,
|
|
77
|
+
flush_text,
|
|
78
|
+
handle_paragraph_break,
|
|
79
|
+
offset,
|
|
80
|
+
pop_stack_entry,
|
|
81
|
+
push_stack_entry,
|
|
82
|
+
revert_failed_close,
|
|
83
|
+
} from './mdz_stream_parser_state.js';
|
|
84
|
+
import {
|
|
85
|
+
process_codeblock,
|
|
86
|
+
process_codeblock_forced,
|
|
87
|
+
process_codeblock_in_item,
|
|
88
|
+
try_codeblock_open,
|
|
89
|
+
try_heading,
|
|
90
|
+
try_hr,
|
|
91
|
+
} from './mdz_stream_parser_block.js';
|
|
92
|
+
import {process_list_line, try_list_start} from './mdz_stream_parser_list.js';
|
|
93
|
+
import {
|
|
94
|
+
check_close_word_boundary,
|
|
95
|
+
close_delimiter,
|
|
96
|
+
process_code_content,
|
|
97
|
+
try_code,
|
|
98
|
+
try_double_delimiter,
|
|
99
|
+
try_italic,
|
|
100
|
+
} from './mdz_stream_parser_inline.js';
|
|
101
|
+
import {
|
|
102
|
+
try_close_tag,
|
|
103
|
+
try_complete_link,
|
|
104
|
+
try_link_open,
|
|
105
|
+
try_tag_open,
|
|
106
|
+
} from './mdz_stream_parser_link.js';
|
|
107
|
+
import {
|
|
108
|
+
complete_pending_url,
|
|
109
|
+
process_url_content,
|
|
110
|
+
start_speculative_url,
|
|
111
|
+
try_auto_path_absolute,
|
|
112
|
+
try_auto_path_relative,
|
|
113
|
+
try_auto_url_forced,
|
|
114
|
+
} from './mdz_stream_parser_url.js';
|
|
115
|
+
import {consume_delimiter_as_text, consume_text_run} from './mdz_stream_parser_text.js';
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Streaming opcode parser for mdz content.
|
|
119
|
+
* Feed chunks via `feed()`, retrieve opcodes via `take_opcodes()`, call `finish()` at end.
|
|
120
|
+
*
|
|
121
|
+
* The opcode sequence is not deterministic across chunk boundaries — the same input
|
|
122
|
+
* fed in different chunk sizes may produce different `text`/`append_text` splits and
|
|
123
|
+
* different optimistic/revert sequences. The final tree (via `mdz_opcodes_to_nodes`)
|
|
124
|
+
* matches the one-shot result: optimistic opens are corrected by `revert` opcodes
|
|
125
|
+
* at the first failed closer, at run close (paragraph, heading, or list-item
|
|
126
|
+
* run), or at EOF, and at EOF the delimiter-paired opens
|
|
127
|
+
* (bold/italic/strikethrough) and inline-code candidates are gated on a closer
|
|
128
|
+
* scan of the complete final buffer, so held tails parse like the one-shot
|
|
129
|
+
* parse. The residual divergence classes are:
|
|
130
|
+
*
|
|
131
|
+
* - **Backtick-adjacent chunking** — an inline-code candidate held across
|
|
132
|
+
* chunks can make its text-vs-code decision bounded by a wrongly-optimistic
|
|
133
|
+
* italic (opened before its failed closer was visible), where the one-shot
|
|
134
|
+
* parse greedy-rejects that italic and scans unbounded — `` _`__` ``
|
|
135
|
+
* chunked at the italic stays flat text where one-shot parses the code span.
|
|
136
|
+
* Italic is the only wedge (the only delimiter whose one-shot form rejects
|
|
137
|
+
* on a failed first closer); see `try_code`'s hold and `code_search_limit`.
|
|
138
|
+
* - **Optimistic inline code unclosed at EOF** — a `` ` `` that opened
|
|
139
|
+
* optimistically consumes its tail as raw code text, so formatting inside it
|
|
140
|
+
* never forms (`` h `x **b** z `` stays flat where one-shot parses the bold);
|
|
141
|
+
* the parser never re-parses, so the EOF revert can only flatten it to text.
|
|
142
|
+
* - **Link/tag opens at EOF** — `finish()` doesn't open links or tags, so a held
|
|
143
|
+
* tail containing complete `[text](url)` / `<Tag>…</Tag>` syntax parses flat.
|
|
144
|
+
* - **Block elements interrupt optimistic inlines** — at column 0 a
|
|
145
|
+
* heading/HR/fence/list/blockquote line interrupts the open paragraph even when an
|
|
146
|
+
* optimistic inline container spans it (`**a\n# h\nb**` parses the heading
|
|
147
|
+
* here; the one-shot parse, knowing the closer exists, swallows the line as
|
|
148
|
+
* bold text). Inherent: the swallow is only correct when a closer eventually
|
|
149
|
+
* arrives, which streaming can't know — interrupting matches the one-shot
|
|
150
|
+
* parse on the no-closer flip side (`**a\n# h`) and renders blocks promptly.
|
|
151
|
+
*
|
|
152
|
+
* Lifecycle: one parser instance per stream — `feed()` any number of times,
|
|
153
|
+
* `finish()` exactly once, then a final `take_opcodes()`. There is no
|
|
154
|
+
* `reset()`; calling `feed()` or `finish()` after `finish()` is undefined.
|
|
155
|
+
* To restart, construct a new parser (and a new consumer — see
|
|
156
|
+
* `MdzStreamState`).
|
|
157
|
+
*/
|
|
158
|
+
export class MdzStreamParser {
|
|
159
|
+
#state: MdzStreamParserState = create_state();
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Feed a chunk of text to the parser.
|
|
163
|
+
* Opcodes are accumulated and retrieved via `take_opcodes()`.
|
|
164
|
+
*/
|
|
165
|
+
feed(chunk: string): void {
|
|
166
|
+
feed_state(this.#state, chunk);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Signal end of input. Resolves all pending state: closes open blocks,
|
|
171
|
+
* reverts unclosed optimistic opens, trims trailing newlines.
|
|
172
|
+
*
|
|
173
|
+
* Trailing-newline trimming is handled in one place: `trim_trailing_newline()`
|
|
174
|
+
* called at the top of `close_paragraph()` and `close_codeblock_at_eof()`,
|
|
175
|
+
* before either function reverts its inner stack. The trim sees the
|
|
176
|
+
* just-flushed text node's `last_text_id` (or a still-accumulated `\n`) and
|
|
177
|
+
* emits a `trim_text` opcode. Revert opcodes only fire after.
|
|
178
|
+
*
|
|
179
|
+
* Optimistic-container revert is handled by `close_paragraph` and
|
|
180
|
+
* `close_heading` (each reverts everything above its own block frame via
|
|
181
|
+
* `revert_above`), so no separate revert pass is needed — optimistic
|
|
182
|
+
* containers can only exist inside an open Paragraph or Heading (parser
|
|
183
|
+
* invariant).
|
|
184
|
+
*/
|
|
185
|
+
finish(): void {
|
|
186
|
+
finish_state(this.#state);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Drain and return all accumulated opcodes.
|
|
191
|
+
* Destructive — empties the internal queue. The returned array is owned by the caller.
|
|
192
|
+
*/
|
|
193
|
+
take_opcodes(): Array<MdzOpcode> {
|
|
194
|
+
const ops = this.#state.opcodes;
|
|
195
|
+
this.#state.opcodes = [];
|
|
196
|
+
return ops;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// -- Drive functions --
|
|
201
|
+
// The bodies of `feed`/`finish` as free functions over the state, so the
|
|
202
|
+
// blockquote machinery can drive its nested inner parser through the same
|
|
203
|
+
// code paths (nested parsing is recursive, which keeps these and
|
|
204
|
+
// `process_loop` in one module).
|
|
205
|
+
|
|
206
|
+
/** Feed a chunk to a parser state — the body of `MdzStreamParser#feed`. */
|
|
207
|
+
const feed_state = (state: MdzStreamParserState, chunk: string): void => {
|
|
208
|
+
state.buffer += chunk;
|
|
209
|
+
process_loop(state, false);
|
|
210
|
+
// flush any accumulated text so the renderer shows it immediately
|
|
211
|
+
flush_text(state);
|
|
212
|
+
// drain processed bytes from the buffer
|
|
213
|
+
if (state.pos > 0) {
|
|
214
|
+
state.base_offset += state.pos;
|
|
215
|
+
state.buffer = state.buffer.slice(state.pos);
|
|
216
|
+
state.pos = 0;
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
/** Resolve all pending state at end of input — the body of `MdzStreamParser#finish`. */
|
|
221
|
+
const finish_state = (state: MdzStreamParserState): void => {
|
|
222
|
+
// process any remaining buffer — `forced=true` treats ambiguous trailing
|
|
223
|
+
// bytes as concrete content rather than waiting for more input
|
|
224
|
+
if (state.buffer.length > 0) {
|
|
225
|
+
process_loop(state, true);
|
|
226
|
+
}
|
|
227
|
+
// close an open blockquote — finishes its nested inner document (an
|
|
228
|
+
// unclosed fence inside ends at the quote's EOF, which is this) and
|
|
229
|
+
// cascades recursively through deeper quotes
|
|
230
|
+
if (state.blockquote) {
|
|
231
|
+
finish_blockquote(state);
|
|
232
|
+
}
|
|
233
|
+
// finalize pending URL if any (text-first auto-links)
|
|
234
|
+
if (state.pending_url !== null) {
|
|
235
|
+
flush_text(state);
|
|
236
|
+
complete_pending_url(state);
|
|
237
|
+
}
|
|
238
|
+
flush_text(state);
|
|
239
|
+
// close heading if open — internally reverts its inner optimistic stack
|
|
240
|
+
if (state.in_heading) {
|
|
241
|
+
close_heading(state);
|
|
242
|
+
}
|
|
243
|
+
// close paragraph if open (a top-level paragraph or a list item's
|
|
244
|
+
// Paragraph block child) — internally trims trailing \n, reverts its
|
|
245
|
+
// inner optimistic stack, then pops
|
|
246
|
+
close_paragraph(state);
|
|
247
|
+
// close codeblock if open — raw mode is absolute, so an unclosed fence
|
|
248
|
+
// consumed to EOF and gets a plain close (top-level trims trailing \n)
|
|
249
|
+
if (state.codeblock) {
|
|
250
|
+
close_codeblock_at_eof(state);
|
|
251
|
+
}
|
|
252
|
+
// EOF ends any open list: seal the item run, close items and lists
|
|
253
|
+
close_all_list_levels(state);
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
// -- Blockquotes --
|
|
257
|
+
// A quote's content is a mini-document behind the per-line prefix, parsed by
|
|
258
|
+
// a nested parser state: the outer parser classifies each line's prefix
|
|
259
|
+
// (line-bounded holds), strips one level, and feeds the remainder — including
|
|
260
|
+
// each line's terminating newline — to the inner document, forwarding its
|
|
261
|
+
// opcodes with positions remapped to source offsets. Deeper prefixes nest by
|
|
262
|
+
// recursion (the inner parser opens its own quote), so the outer tracks at
|
|
263
|
+
// most one quote. The id counter is shared, so ids pass through unmapped.
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Open a blockquote at `state.pos` (the validated prefix's first `>`).
|
|
267
|
+
* `item_indent` is -1 for top-level quotes or the attach item's marker
|
|
268
|
+
* indent for in-item quotes (block children of a list item).
|
|
269
|
+
*/
|
|
270
|
+
const open_blockquote = (state: MdzStreamParserState, item_indent: number): void => {
|
|
271
|
+
const id = alloc_id(state);
|
|
272
|
+
const start = offset(state);
|
|
273
|
+
emit(state, {type: 'open', id, node_type: 'Blockquote', start});
|
|
274
|
+
push_stack_entry(state, id, 'Blockquote', start);
|
|
275
|
+
const inner = create_state();
|
|
276
|
+
inner.next_id = state.next_id;
|
|
277
|
+
const strip = state.pos + mdz_blockquote_strip_width(state.buffer, state.pos);
|
|
278
|
+
state.blockquote = {
|
|
279
|
+
id,
|
|
280
|
+
inner,
|
|
281
|
+
segments: [{inner_start: 0, source_start: offset(state, strip)}],
|
|
282
|
+
inner_len: 0,
|
|
283
|
+
text_inner_ends: new Map(),
|
|
284
|
+
item_indent,
|
|
285
|
+
};
|
|
286
|
+
state.pos = strip;
|
|
287
|
+
state.prev_char = SPACE;
|
|
288
|
+
state.active_text_id = null;
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
/** Feed a slice of quote content to the inner document and forward its opcodes. */
|
|
292
|
+
const feed_blockquote_inner = (state: MdzStreamParserState, chunk: string): void => {
|
|
293
|
+
const blockquote = state.blockquote!;
|
|
294
|
+
feed_state(blockquote.inner, chunk);
|
|
295
|
+
blockquote.inner_len += chunk.length;
|
|
296
|
+
forward_inner_opcodes(state);
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Drain the inner document's opcodes into the outer stream, remapping
|
|
301
|
+
* positions from inner-document offsets to source offsets. Ids pass through
|
|
302
|
+
* unmapped (the counter is shared). `append_text` always gains an explicit
|
|
303
|
+
* remapped `end` — the implied extend-by-content-length is wrong across the
|
|
304
|
+
* prefix gap at line boundaries — via per-node inner-end tracking.
|
|
305
|
+
*/
|
|
306
|
+
const forward_inner_opcodes = (state: MdzStreamParserState): void => {
|
|
307
|
+
const blockquote = state.blockquote!;
|
|
308
|
+
const ops = blockquote.inner.opcodes;
|
|
309
|
+
if (ops.length === 0) return;
|
|
310
|
+
blockquote.inner.opcodes = [];
|
|
311
|
+
const {segments, text_inner_ends} = blockquote;
|
|
312
|
+
for (const op of ops) {
|
|
313
|
+
switch (op.type) {
|
|
314
|
+
case 'open':
|
|
315
|
+
case 'revert':
|
|
316
|
+
op.start = mdz_remap_segments(segments, op.start);
|
|
317
|
+
break;
|
|
318
|
+
case 'close':
|
|
319
|
+
op.end = mdz_remap_segments(segments, op.end);
|
|
320
|
+
break;
|
|
321
|
+
case 'text':
|
|
322
|
+
text_inner_ends.set(op.id, op.end);
|
|
323
|
+
op.start = mdz_remap_segments(segments, op.start);
|
|
324
|
+
op.end = mdz_remap_segments(segments, op.end);
|
|
325
|
+
break;
|
|
326
|
+
case 'append_text': {
|
|
327
|
+
const inner_end = op.end ?? text_inner_ends.get(op.id)! + op.content.length;
|
|
328
|
+
text_inner_ends.set(op.id, inner_end);
|
|
329
|
+
op.end = mdz_remap_segments(segments, inner_end);
|
|
330
|
+
break;
|
|
331
|
+
}
|
|
332
|
+
case 'trim_text': {
|
|
333
|
+
const inner_end = text_inner_ends.get(op.id);
|
|
334
|
+
if (inner_end !== undefined) text_inner_ends.set(op.id, inner_end - op.count);
|
|
335
|
+
break;
|
|
336
|
+
}
|
|
337
|
+
case 'void':
|
|
338
|
+
op.start = mdz_remap_segments(segments, op.start);
|
|
339
|
+
op.end = mdz_remap_segments(segments, op.end);
|
|
340
|
+
break;
|
|
341
|
+
case 'wrap':
|
|
342
|
+
op.start = mdz_remap_segments(segments, op.start);
|
|
343
|
+
op.end = mdz_remap_segments(segments, op.end);
|
|
344
|
+
break;
|
|
345
|
+
}
|
|
346
|
+
emit(state, op);
|
|
347
|
+
}
|
|
348
|
+
};
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Close the open blockquote: finish the inner document (its EOF — unclosed
|
|
352
|
+
* fences and open structure inside resolve here, recursing through deeper
|
|
353
|
+
* quotes), forward the final opcodes, sync the shared id counter back, and
|
|
354
|
+
* close the Blockquote frame.
|
|
355
|
+
*/
|
|
356
|
+
const finish_blockquote = (state: MdzStreamParserState): void => {
|
|
357
|
+
const blockquote = state.blockquote!;
|
|
358
|
+
finish_state(blockquote.inner);
|
|
359
|
+
forward_inner_opcodes(state);
|
|
360
|
+
state.next_id = blockquote.inner.next_id;
|
|
361
|
+
state.blockquote = null;
|
|
362
|
+
const entry = pop_stack_entry(state);
|
|
363
|
+
emit(state, {type: 'close', id: entry.id, end: offset(state)});
|
|
364
|
+
state.active_text_id = null;
|
|
365
|
+
};
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Try to open a blockquote at a column-0 `>` — a valid prefix with
|
|
369
|
+
* non-whitespace content after it on the same line (no empty start,
|
|
370
|
+
* mirroring lists; the hold is line-bounded). Interrupts a current
|
|
371
|
+
* paragraph, like every block construct.
|
|
372
|
+
*/
|
|
373
|
+
const try_blockquote_start = (state: MdzStreamParserState, forced: boolean): TryResult => {
|
|
374
|
+
const prefix = mdz_match_blockquote_prefix(state.buffer, state.pos, forced);
|
|
375
|
+
if (prefix === 'pending') return 'need_more';
|
|
376
|
+
if (prefix === null) return 'not_match';
|
|
377
|
+
const has_content = mdz_blockquote_line_has_content(state.buffer, prefix.content_start, forced);
|
|
378
|
+
if (has_content === 'pending') return 'need_more';
|
|
379
|
+
if (!has_content) return 'not_match';
|
|
380
|
+
flush_text(state);
|
|
381
|
+
close_paragraph(state);
|
|
382
|
+
open_blockquote(state, -1);
|
|
383
|
+
return 'consumed';
|
|
384
|
+
};
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* Process input while a quote is open: pass content through to the inner
|
|
388
|
+
* document mid-line, and classify each following line's prefix at its
|
|
389
|
+
* newline — continue (feed the held newline, strip one level), or end the
|
|
390
|
+
* quote (blank line, missing or mis-positioned prefix, EOF). Returns false
|
|
391
|
+
* when the buffer ends in an unresolved line and more input is needed; all
|
|
392
|
+
* holds are line-bounded.
|
|
393
|
+
*/
|
|
394
|
+
const process_blockquote = (state: MdzStreamParserState, forced: boolean): boolean => {
|
|
395
|
+
const blockquote = state.blockquote!;
|
|
396
|
+
const {buffer} = state;
|
|
397
|
+
// mid-line: feed content through to the line's newline (or buffer end —
|
|
398
|
+
// content streams as it arrives; `pos` is the fed watermark)
|
|
399
|
+
if (state.pos < buffer.length && buffer.charCodeAt(state.pos) !== NEWLINE) {
|
|
400
|
+
let i = state.pos;
|
|
401
|
+
while (i < buffer.length && buffer.charCodeAt(i) !== NEWLINE) {
|
|
402
|
+
i++;
|
|
403
|
+
}
|
|
404
|
+
feed_blockquote_inner(state, buffer.slice(state.pos, i));
|
|
405
|
+
state.pos = i;
|
|
406
|
+
}
|
|
407
|
+
if (state.pos >= buffer.length) return true; // everything fed; more may arrive
|
|
408
|
+
|
|
409
|
+
// at the line's newline: the next line decides continue-vs-end before the
|
|
410
|
+
// held newline feeds (it joins the inner document either way — quote lines
|
|
411
|
+
// are newline-terminated — but a continuation also needs its strip point)
|
|
412
|
+
const nl_pos = state.pos;
|
|
413
|
+
const line_start = nl_pos + 1;
|
|
414
|
+
let first = line_start;
|
|
415
|
+
if (blockquote.item_indent >= 0) {
|
|
416
|
+
while (first < buffer.length && is_line_whitespace(buffer.charCodeAt(first))) {
|
|
417
|
+
first++;
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
if (first >= buffer.length && !forced) return false; // unresolved line — hold
|
|
421
|
+
const indent = first - line_start;
|
|
422
|
+
if (
|
|
423
|
+
first < buffer.length &&
|
|
424
|
+
buffer.charCodeAt(first) === RIGHT_ANGLE &&
|
|
425
|
+
(blockquote.item_indent < 0 ? indent === 0 : indent > blockquote.item_indent)
|
|
426
|
+
) {
|
|
427
|
+
const prefix = mdz_match_blockquote_prefix(buffer, first, forced);
|
|
428
|
+
if (prefix === 'pending') return false;
|
|
429
|
+
if (prefix !== null) {
|
|
430
|
+
// the quote continues: feed the held newline, then resume past the
|
|
431
|
+
// stripped prefix level (a bare prefix line contributes only its
|
|
432
|
+
// newline — the next iteration classifies again from there)
|
|
433
|
+
feed_blockquote_inner(state, '\n');
|
|
434
|
+
const strip = first + mdz_blockquote_strip_width(buffer, first);
|
|
435
|
+
blockquote.segments.push({
|
|
436
|
+
inner_start: blockquote.inner_len,
|
|
437
|
+
source_start: offset(state, strip),
|
|
438
|
+
});
|
|
439
|
+
state.pos = strip;
|
|
440
|
+
state.column = strip - line_start;
|
|
441
|
+
state.prev_char = NEWLINE;
|
|
442
|
+
return true;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
// the line doesn't continue the quote — it ends here. The last quote
|
|
446
|
+
// line's newline joins the inner document; the ending line re-dispatches
|
|
447
|
+
// outside: the list classifier owns it inside an open list (blank
|
|
448
|
+
// containment, markers, list end), the main loop otherwise.
|
|
449
|
+
feed_blockquote_inner(state, '\n');
|
|
450
|
+
finish_blockquote(state);
|
|
451
|
+
if (state.list_levels.length === 0) {
|
|
452
|
+
state.pos = line_start;
|
|
453
|
+
state.column = 0;
|
|
454
|
+
state.prev_char = NEWLINE;
|
|
455
|
+
state.skip_blank_lines = true;
|
|
456
|
+
}
|
|
457
|
+
return true;
|
|
458
|
+
};
|
|
459
|
+
|
|
460
|
+
// -- Processing loop --
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* Absorb the blank region at the current position: newlines and
|
|
464
|
+
* whitespace-only lines. Runs while `skip_blank_lines` is set — at document
|
|
465
|
+
* start, after block-element closes, and after paragraph breaks. Clears the
|
|
466
|
+
* flag when a content line is reached (its leading whitespace stays in the
|
|
467
|
+
* text); leaves it set when the buffer is consumed entirely, carrying the
|
|
468
|
+
* absorb across chunks.
|
|
469
|
+
*
|
|
470
|
+
* Returns `false` when the buffer ends in an unresolved partial line
|
|
471
|
+
* (whitespace so far — could still gain content) and more input is needed;
|
|
472
|
+
* `state.pos` stays at the partial line's start. In forced mode a trailing
|
|
473
|
+
* whitespace run is a blank line at EOF and is consumed.
|
|
474
|
+
*/
|
|
475
|
+
const skip_blank_region = (state: MdzStreamParserState, forced: boolean): boolean => {
|
|
476
|
+
while (state.pos < state.buffer.length) {
|
|
477
|
+
const char_code = state.buffer.charCodeAt(state.pos);
|
|
478
|
+
if (char_code === NEWLINE) {
|
|
479
|
+
state.pos++;
|
|
480
|
+
state.column = 0;
|
|
481
|
+
state.prev_char = NEWLINE;
|
|
482
|
+
continue;
|
|
483
|
+
}
|
|
484
|
+
if (!is_line_whitespace(char_code)) {
|
|
485
|
+
state.skip_blank_lines = false;
|
|
486
|
+
return true;
|
|
487
|
+
}
|
|
488
|
+
// whitespace at line start — blank only if the rest of the line is whitespace
|
|
489
|
+
let i = state.pos;
|
|
490
|
+
while (i < state.buffer.length && is_line_whitespace(state.buffer.charCodeAt(i))) {
|
|
491
|
+
i++;
|
|
492
|
+
}
|
|
493
|
+
if (i >= state.buffer.length) {
|
|
494
|
+
if (forced) {
|
|
495
|
+
// trailing whitespace-only line at EOF
|
|
496
|
+
state.pos = i;
|
|
497
|
+
return true;
|
|
498
|
+
}
|
|
499
|
+
return false; // partial line — need more input to classify it
|
|
500
|
+
}
|
|
501
|
+
if (state.buffer.charCodeAt(i) !== NEWLINE) {
|
|
502
|
+
// line has content — its leading whitespace is text
|
|
503
|
+
state.skip_blank_lines = false;
|
|
504
|
+
return true;
|
|
505
|
+
}
|
|
506
|
+
state.pos = i; // blank line — newline consumed next iteration
|
|
507
|
+
}
|
|
508
|
+
return true; // consumed everything; flag stays set for the next chunk
|
|
509
|
+
};
|
|
510
|
+
|
|
511
|
+
/**
|
|
512
|
+
* Core processing loop. When `forced` is false (normal streaming), returns
|
|
513
|
+
* when more input is needed. When `forced` is true (EOF), treats buffer end
|
|
514
|
+
* as content end and never waits for more input.
|
|
515
|
+
*/
|
|
516
|
+
const process_loop = (state: MdzStreamParserState, forced: boolean): void => {
|
|
517
|
+
while (state.pos < state.buffer.length) {
|
|
518
|
+
// absorb the blank region at start of input or after a block boundary
|
|
519
|
+
// (paragraph break, heading/HR/codeblock close in this or a prior chunk)
|
|
520
|
+
if (state.skip_blank_lines) {
|
|
521
|
+
if (!skip_blank_region(state, forced)) return; // need more input
|
|
522
|
+
continue;
|
|
523
|
+
}
|
|
524
|
+
// open blockquote — all input routes through the quote (prefix
|
|
525
|
+
// classification + nested-parser feeding) until a line breaks it
|
|
526
|
+
if (state.blockquote) {
|
|
527
|
+
if (!process_blockquote(state, forced)) return; // need more input
|
|
528
|
+
continue;
|
|
529
|
+
}
|
|
530
|
+
// codeblock mode — in-item fences (`fence_indent > 0`) stream complete
|
|
531
|
+
// lines with per-line indent stripping; top-level fences keep the raw
|
|
532
|
+
// char-walk fast path
|
|
533
|
+
if (state.codeblock) {
|
|
534
|
+
if (state.codeblock.fence_indent > 0) {
|
|
535
|
+
if (!process_codeblock_in_item(state, forced)) return; // need more input
|
|
536
|
+
continue;
|
|
537
|
+
}
|
|
538
|
+
if (forced) {
|
|
539
|
+
// consumes to the buffer end by construction — content runs to
|
|
540
|
+
// EOF, or an EOF-terminated closer ends the block (see
|
|
541
|
+
// `process_codeblock_forced`) — so nothing is left to process
|
|
542
|
+
process_codeblock_forced(state);
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
if (!process_codeblock(state)) return; // need more input
|
|
546
|
+
continue;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
// optimistic inline code mode — raw scanning for close/revert
|
|
550
|
+
if (state.in_code) {
|
|
551
|
+
process_code_content(state);
|
|
552
|
+
continue;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// text-first URL/path scanning mode
|
|
556
|
+
if (state.pending_url !== null) {
|
|
557
|
+
process_url_content(state, forced);
|
|
558
|
+
continue;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
const char_code = state.buffer.charCodeAt(state.pos);
|
|
562
|
+
|
|
563
|
+
// newline handling
|
|
564
|
+
if (char_code === NEWLINE) {
|
|
565
|
+
if (state.in_heading) {
|
|
566
|
+
// newline ends heading; trailing blanks absorb at the loop top
|
|
567
|
+
flush_text(state);
|
|
568
|
+
close_heading(state);
|
|
569
|
+
state.pos++;
|
|
570
|
+
state.skip_blank_lines = true;
|
|
571
|
+
state.column = 0;
|
|
572
|
+
state.prev_char = NEWLINE;
|
|
573
|
+
continue;
|
|
574
|
+
}
|
|
575
|
+
// inside an open list, every line classifies through the list line
|
|
576
|
+
// classifier (continuation / marker / blank containment / list end);
|
|
577
|
+
// a deeper quote-opener line signals back so the quote opens here
|
|
578
|
+
if (state.list_levels.length > 0) {
|
|
579
|
+
const r = process_list_line(state, forced);
|
|
580
|
+
if (r === 'blockquote') {
|
|
581
|
+
open_blockquote(state, state.list_levels[state.list_levels.length - 1]!.indent);
|
|
582
|
+
continue;
|
|
583
|
+
}
|
|
584
|
+
if (!r) return; // need more input
|
|
585
|
+
continue;
|
|
586
|
+
}
|
|
587
|
+
// scan past line whitespace: a following blank line is a paragraph break
|
|
588
|
+
let after = state.pos + 1;
|
|
589
|
+
while (after < state.buffer.length && is_line_whitespace(state.buffer.charCodeAt(after))) {
|
|
590
|
+
after++;
|
|
591
|
+
}
|
|
592
|
+
if (after >= state.buffer.length && !forced) {
|
|
593
|
+
// hold trailing `\n` + whitespace — could become a blank line
|
|
594
|
+
return;
|
|
595
|
+
}
|
|
596
|
+
if (after < state.buffer.length && state.buffer.charCodeAt(after) === NEWLINE) {
|
|
597
|
+
handle_paragraph_break(state);
|
|
598
|
+
continue;
|
|
599
|
+
}
|
|
600
|
+
// single newline: accumulate as text (any scanned whitespace is the
|
|
601
|
+
// next line's leading content and accumulates via the normal text path)
|
|
602
|
+
accumulate_text(state, '\n', offset(state));
|
|
603
|
+
state.pos++;
|
|
604
|
+
state.column = 0;
|
|
605
|
+
state.prev_char = NEWLINE;
|
|
606
|
+
continue;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
// block elements at column 0
|
|
610
|
+
if (state.column === 0 && !state.in_heading) {
|
|
611
|
+
if (char_code === HASH) {
|
|
612
|
+
const r = try_heading(state);
|
|
613
|
+
if (r === 'consumed') continue;
|
|
614
|
+
if (!forced && r === 'need_more') return;
|
|
615
|
+
} else if (char_code === HYPHEN) {
|
|
616
|
+
const r = try_hr(state, forced);
|
|
617
|
+
if (r === 'consumed') continue;
|
|
618
|
+
if (!forced && r === 'need_more') return;
|
|
619
|
+
// not an HR — could start an unordered list
|
|
620
|
+
const lr = try_list_start(state, forced);
|
|
621
|
+
if (lr === 'consumed') continue;
|
|
622
|
+
if (!forced && lr === 'need_more') return;
|
|
623
|
+
} else if (char_code === BACKTICK) {
|
|
624
|
+
const r = try_codeblock_open(state);
|
|
625
|
+
if (r === 'consumed') continue;
|
|
626
|
+
if (!forced && r === 'need_more') return;
|
|
627
|
+
} else if (char_code >= ZERO && char_code <= NINE) {
|
|
628
|
+
const r = try_list_start(state, forced);
|
|
629
|
+
if (r === 'consumed') continue;
|
|
630
|
+
if (!forced && r === 'need_more') return;
|
|
631
|
+
} else if (char_code === RIGHT_ANGLE) {
|
|
632
|
+
const r = try_blockquote_start(state, forced);
|
|
633
|
+
if (r === 'consumed') continue;
|
|
634
|
+
if (!forced && r === 'need_more') return;
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
// inline processing
|
|
639
|
+
if (!process_inline(state, forced)) return; // need more input
|
|
640
|
+
}
|
|
641
|
+
};
|
|
642
|
+
|
|
643
|
+
// -- Inline processing --
|
|
644
|
+
|
|
645
|
+
/**
|
|
646
|
+
* Process one inline element. Returns false if more input is needed.
|
|
647
|
+
* When `forced` is true (EOF processing), open decisions run against the
|
|
648
|
+
* complete final buffer instead of waiting for more input: bold, italic,
|
|
649
|
+
* and strikethrough open only when their confirmed closer is already
|
|
650
|
+
* present, and an inline-code candidate degrades to literal text when its
|
|
651
|
+
* closer isn't (at EOF it never can be). Link and tag opens are skipped
|
|
652
|
+
* entirely — held `[text](url)` / `<Tag>` tails parse flat (an accepted
|
|
653
|
+
* divergence; see the `MdzStreamParser` class TSDoc). All closing
|
|
654
|
+
* constructs and auto-links still run.
|
|
655
|
+
* Never returns false in forced mode.
|
|
656
|
+
*/
|
|
657
|
+
const process_inline = (state: MdzStreamParserState, forced: boolean): boolean => {
|
|
658
|
+
const char_code = state.buffer.charCodeAt(state.pos);
|
|
659
|
+
|
|
660
|
+
// check for closing delimiters first (if matching open exists)
|
|
661
|
+
if (char_code === ASTERISK || char_code === TILDE) {
|
|
662
|
+
if (
|
|
663
|
+
state.pos + 1 < state.buffer.length &&
|
|
664
|
+
state.buffer.charCodeAt(state.pos + 1) === char_code
|
|
665
|
+
) {
|
|
666
|
+
const node_type = char_code === ASTERISK ? 'Bold' : 'Strikethrough';
|
|
667
|
+
const open_idx = find_open(state, node_type);
|
|
668
|
+
if (open_idx !== -1) {
|
|
669
|
+
return close_delimiter(state, open_idx);
|
|
670
|
+
}
|
|
671
|
+
return try_double_delimiter(state, char_code === ASTERISK ? '**' : '~~', node_type, forced);
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
if (!forced) {
|
|
675
|
+
// could be start of the doubled delimiter — need next char to decide
|
|
676
|
+
if (state.pos + 1 >= state.buffer.length) return false; // hold for next chunk
|
|
677
|
+
// single char is text (next char doesn't double it)
|
|
678
|
+
consume_delimiter_as_text(state, char_code === ASTERISK ? '*' : '~');
|
|
679
|
+
return true;
|
|
680
|
+
}
|
|
681
|
+
// forced with a lone delimiter char: falls through to the text handler
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
if (char_code === UNDERSCORE) {
|
|
685
|
+
const open_idx = find_open(state, 'Italic');
|
|
686
|
+
if (open_idx !== -1) {
|
|
687
|
+
// hold: the char after a potential closer decides its boundary
|
|
688
|
+
if (!forced && state.pos + 1 >= state.buffer.length) return false;
|
|
689
|
+
if (check_close_word_boundary(state)) {
|
|
690
|
+
return close_delimiter(state, open_idx);
|
|
691
|
+
}
|
|
692
|
+
// failed closer: the one-shot parse greedy-rejects the opener at
|
|
693
|
+
// the first invalid closer, so the optimistic open is known wrong
|
|
694
|
+
revert_failed_close(state, open_idx);
|
|
695
|
+
// fall through: this same delimiter may be a fresh opener, matching
|
|
696
|
+
// the one-shot parse, which continues after the rejected opener
|
|
697
|
+
}
|
|
698
|
+
return try_italic(state, forced);
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
if (char_code === BACKTICK) {
|
|
702
|
+
return try_code(state, forced);
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
if (char_code === LEFT_BRACKET && !forced) {
|
|
706
|
+
return try_link_open(state);
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
if (char_code === RIGHT_BRACKET && !forced) {
|
|
710
|
+
const link_idx = find_open(state, 'Link');
|
|
711
|
+
if (link_idx !== -1) {
|
|
712
|
+
return try_complete_link(state, link_idx);
|
|
713
|
+
}
|
|
714
|
+
// no open link — treat as text
|
|
715
|
+
consume_delimiter_as_text(state, ']');
|
|
716
|
+
return true;
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
if (char_code === LEFT_ANGLE) {
|
|
720
|
+
// closing tags are handled even in forced mode
|
|
721
|
+
if (state.pos + 1 < state.buffer.length && state.buffer.charCodeAt(state.pos + 1) === SLASH) {
|
|
722
|
+
const result = try_close_tag(state);
|
|
723
|
+
if (result === 'consumed') return true;
|
|
724
|
+
// in forced mode, need_more falls through to text
|
|
725
|
+
if (!forced && result === 'need_more') return false;
|
|
726
|
+
// not_match — fall through
|
|
727
|
+
}
|
|
728
|
+
if (!forced) {
|
|
729
|
+
const tag_result = try_tag_open(state);
|
|
730
|
+
if (tag_result === 'consumed') return true;
|
|
731
|
+
if (tag_result === 'need_more') return false;
|
|
732
|
+
// not_match — fall through
|
|
733
|
+
}
|
|
734
|
+
// not a valid tag — fall through to text
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
// auto-detected URLs (text-first speculative prefix matching).
|
|
738
|
+
// Fires on `h` and `H` — scheme matching is case-insensitive (RFC 3986).
|
|
739
|
+
if (char_code === H_LOWER || char_code === H_UPPER) {
|
|
740
|
+
if (forced) {
|
|
741
|
+
const result = try_auto_url_forced(state);
|
|
742
|
+
if (result === 'consumed') return true;
|
|
743
|
+
if (result === 'need_more') return false;
|
|
744
|
+
} else if (state.prev_char === -1 || !is_word_char(state.prev_char)) {
|
|
745
|
+
// word boundary — start speculative URL prefix matching
|
|
746
|
+
start_speculative_url(state);
|
|
747
|
+
return true;
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
// auto-detected paths
|
|
752
|
+
if (char_code === SLASH) {
|
|
753
|
+
const result = try_auto_path_absolute(state, forced);
|
|
754
|
+
if (result === 'consumed') return true;
|
|
755
|
+
if (result === 'need_more') return false;
|
|
756
|
+
}
|
|
757
|
+
if (char_code === PERIOD) {
|
|
758
|
+
const result = try_auto_path_relative(state, forced);
|
|
759
|
+
if (result === 'consumed') return true;
|
|
760
|
+
if (result === 'need_more') return false;
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
// plain text
|
|
764
|
+
consume_text_run(state);
|
|
765
|
+
return true;
|
|
766
|
+
};
|