@fuzdev/fuz_ui 0.198.0 → 0.199.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/README.md +5 -3
- package/dist/ApiIndex.svelte +2 -2
- package/dist/ApiModule.svelte +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 +2 -0
- package/dist/ProjectLinks.svelte.d.ts.map +1 -1
- package/dist/declaration.svelte.js +2 -2
- 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/logos.d.ts +7 -0
- package/dist/logos.d.ts.map +1 -1
- package/dist/logos.js +9 -0
- 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.js +1 -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 +6 -5
- package/src/lib/declaration.svelte.ts +2 -2
- package/src/lib/library.svelte.ts +37 -20
- package/src/lib/library_helpers.ts +3 -3
- package/src/lib/logos.ts +10 -0
- 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 +1 -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,415 @@
|
|
|
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
|
+
*
|
|
8
|
+
* The design was independently arrived at but shares goals with
|
|
9
|
+
* {@link https://bsky.app/profile/pngwn.at/post/3mi527zntb22n @pngwn.at}'s
|
|
10
|
+
* Penguin-Flavoured Markdown (PFM): restrict the syntax so streaming is tractable,
|
|
11
|
+
* render optimistically and correct when wrong, emit serializable opcodes
|
|
12
|
+
* to avoid re-parsing, and keep the opcodes target-agnostic so any renderer
|
|
13
|
+
* can consume them. mdz diverges in one respect: the Svelte consumer
|
|
14
|
+
* (`MdzStreamState`) does build a reactive tree from opcodes — the platform
|
|
15
|
+
* dictates this — but mutations are fine-grained via `$state`, not diffed.
|
|
16
|
+
*
|
|
17
|
+
* The parser is split across sibling modules: this file holds the public
|
|
18
|
+
* `MdzStreamParser` class and the `process_loop` / `process_inline`
|
|
19
|
+
* orchestrators. Per-category handlers (block / inline / link / url / text)
|
|
20
|
+
* live in `mdz_stream_parser_*.ts` as free functions taking the shared
|
|
21
|
+
* `MdzStreamParserState` as first argument.
|
|
22
|
+
*
|
|
23
|
+
* Usage:
|
|
24
|
+
* ```ts
|
|
25
|
+
* const parser = new MdzStreamParser();
|
|
26
|
+
* parser.feed('hello **bold');
|
|
27
|
+
* const ops1 = parser.take_opcodes(); // open Paragraph, text "hello ", open Bold, text "bold"
|
|
28
|
+
* parser.feed('** world');
|
|
29
|
+
* const ops2 = parser.take_opcodes(); // close Bold, text " world"
|
|
30
|
+
* parser.finish();
|
|
31
|
+
* const ops3 = parser.take_opcodes(); // close Paragraph
|
|
32
|
+
* ```
|
|
33
|
+
*
|
|
34
|
+
* @module
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
import type {MdzOpcode} from './mdz_opcodes.js';
|
|
38
|
+
import {
|
|
39
|
+
BACKTICK,
|
|
40
|
+
ASTERISK,
|
|
41
|
+
UNDERSCORE,
|
|
42
|
+
TILDE,
|
|
43
|
+
NEWLINE,
|
|
44
|
+
HYPHEN,
|
|
45
|
+
HASH,
|
|
46
|
+
LEFT_ANGLE,
|
|
47
|
+
SLASH,
|
|
48
|
+
LEFT_BRACKET,
|
|
49
|
+
RIGHT_BRACKET,
|
|
50
|
+
H_LOWER,
|
|
51
|
+
H_UPPER,
|
|
52
|
+
PERIOD,
|
|
53
|
+
is_word_char,
|
|
54
|
+
} from './mdz_helpers.js';
|
|
55
|
+
import {
|
|
56
|
+
type MdzStreamParserState,
|
|
57
|
+
accumulate_text,
|
|
58
|
+
close_codeblock_at_eof,
|
|
59
|
+
close_heading,
|
|
60
|
+
close_paragraph,
|
|
61
|
+
create_state,
|
|
62
|
+
ensure_paragraph,
|
|
63
|
+
find_open,
|
|
64
|
+
flush_text,
|
|
65
|
+
handle_paragraph_break,
|
|
66
|
+
offset,
|
|
67
|
+
} from './mdz_stream_parser_state.js';
|
|
68
|
+
import {
|
|
69
|
+
process_codeblock,
|
|
70
|
+
process_codeblock_forced,
|
|
71
|
+
try_codeblock_open,
|
|
72
|
+
try_heading,
|
|
73
|
+
try_hr,
|
|
74
|
+
} from './mdz_stream_parser_block.js';
|
|
75
|
+
import {
|
|
76
|
+
check_close_word_boundary,
|
|
77
|
+
close_bold,
|
|
78
|
+
close_single_delimiter,
|
|
79
|
+
process_code_content,
|
|
80
|
+
try_bold,
|
|
81
|
+
try_code,
|
|
82
|
+
try_italic,
|
|
83
|
+
try_strikethrough,
|
|
84
|
+
} from './mdz_stream_parser_inline.js';
|
|
85
|
+
import {
|
|
86
|
+
try_close_tag,
|
|
87
|
+
try_complete_link,
|
|
88
|
+
try_link_open,
|
|
89
|
+
try_tag_open,
|
|
90
|
+
} from './mdz_stream_parser_link.js';
|
|
91
|
+
import {
|
|
92
|
+
complete_pending_url,
|
|
93
|
+
process_url_content,
|
|
94
|
+
start_speculative_url,
|
|
95
|
+
try_auto_path_absolute,
|
|
96
|
+
try_auto_path_relative,
|
|
97
|
+
try_auto_url_forced,
|
|
98
|
+
} from './mdz_stream_parser_url.js';
|
|
99
|
+
import {consume_text_run} from './mdz_stream_parser_text.js';
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Streaming opcode parser for mdz content.
|
|
103
|
+
* Feed chunks via `feed()`, retrieve opcodes via `take_opcodes()`, call `finish()` at end.
|
|
104
|
+
*
|
|
105
|
+
* The opcode sequence is not deterministic across chunk boundaries — the same input
|
|
106
|
+
* fed in different chunk sizes may produce different `text`/`append_text` splits and
|
|
107
|
+
* different optimistic/revert sequences. The final tree (via `mdz_opcodes_to_nodes`)
|
|
108
|
+
* matches the one-shot result for all input except one case: italic (`_..._`) where
|
|
109
|
+
* the opening and closing delimiters straddle a chunk boundary. Italic is
|
|
110
|
+
* non-optimistic — it requires a confirmed closer in the buffer — so when the
|
|
111
|
+
* closer arrives only in a later chunk, the opening `_` has already been emitted
|
|
112
|
+
* as text and italic cannot apply retroactively. See `try_italic` in
|
|
113
|
+
* `mdz_stream_parser_inline.ts`.
|
|
114
|
+
*/
|
|
115
|
+
export class MdzStreamParser {
|
|
116
|
+
#state: MdzStreamParserState = create_state();
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Feed a chunk of text to the parser.
|
|
120
|
+
* Opcodes are accumulated and retrieved via `take_opcodes()`.
|
|
121
|
+
*/
|
|
122
|
+
feed(chunk: string): void {
|
|
123
|
+
const state = this.#state;
|
|
124
|
+
state.buffer += chunk;
|
|
125
|
+
process_loop(state, false);
|
|
126
|
+
// flush any accumulated text so the renderer shows it immediately
|
|
127
|
+
flush_text(state);
|
|
128
|
+
// drain processed bytes from the buffer
|
|
129
|
+
if (state.pos > 0) {
|
|
130
|
+
state.base_offset += state.pos;
|
|
131
|
+
state.buffer = state.buffer.slice(state.pos);
|
|
132
|
+
state.pos = 0;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Signal end of input. Resolves all pending state: closes open blocks,
|
|
138
|
+
* reverts unclosed optimistic opens, trims trailing newlines.
|
|
139
|
+
*
|
|
140
|
+
* Trailing-newline trimming is handled in one place: `trim_trailing_newline()`
|
|
141
|
+
* called at the top of `close_paragraph()` and `close_codeblock_at_eof()`,
|
|
142
|
+
* before either function reverts its inner stack. The trim sees the
|
|
143
|
+
* just-flushed text node's `last_text_id` (or a still-accumulated `\n`) and
|
|
144
|
+
* emits a `trim_text` opcode. Revert opcodes only fire after.
|
|
145
|
+
*
|
|
146
|
+
* Optimistic-container revert is handled by `close_paragraph` and
|
|
147
|
+
* `close_heading` (each pops its own inner stack), so no separate
|
|
148
|
+
* `revert_all_optimistic` is needed — optimistic containers can only exist
|
|
149
|
+
* inside an open Paragraph or Heading (parser invariant).
|
|
150
|
+
*/
|
|
151
|
+
finish(): void {
|
|
152
|
+
const state = this.#state;
|
|
153
|
+
// process any remaining buffer — `forced=true` treats ambiguous trailing
|
|
154
|
+
// bytes as concrete content rather than waiting for more input
|
|
155
|
+
if (state.buffer.length > 0) {
|
|
156
|
+
process_loop(state, true);
|
|
157
|
+
}
|
|
158
|
+
// finalize pending URL if any (text-first auto-links)
|
|
159
|
+
if (state.pending_url !== null) {
|
|
160
|
+
flush_text(state);
|
|
161
|
+
complete_pending_url(state);
|
|
162
|
+
}
|
|
163
|
+
flush_text(state);
|
|
164
|
+
// close heading if open — internally reverts its inner optimistic stack
|
|
165
|
+
if (state.in_heading) {
|
|
166
|
+
close_heading(state);
|
|
167
|
+
}
|
|
168
|
+
// close paragraph if open — internally trims trailing \n, reverts its
|
|
169
|
+
// inner optimistic stack, then pops
|
|
170
|
+
close_paragraph(state);
|
|
171
|
+
// close codeblock if open (unclosed at EOF) — internally trims trailing \n
|
|
172
|
+
if (state.codeblock) {
|
|
173
|
+
close_codeblock_at_eof(state);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Drain and return all accumulated opcodes.
|
|
179
|
+
* Destructive — empties the internal queue. The returned array is owned by the caller.
|
|
180
|
+
*/
|
|
181
|
+
take_opcodes(): Array<MdzOpcode> {
|
|
182
|
+
const ops = this.#state.opcodes;
|
|
183
|
+
this.#state.opcodes = [];
|
|
184
|
+
return ops;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// -- Processing loop --
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Core processing loop. When `forced` is false (normal streaming), returns
|
|
192
|
+
* when more input is needed. When `forced` is true (EOF), treats buffer end
|
|
193
|
+
* as content end and never waits for more input.
|
|
194
|
+
*/
|
|
195
|
+
const process_loop = (state: MdzStreamParserState, forced: boolean): void => {
|
|
196
|
+
// absorb leading newlines at start of input or left over from a block element
|
|
197
|
+
// close in a prior chunk. Only clear the flag once a non-newline char arrives —
|
|
198
|
+
// otherwise an all-newline chunk would prematurely drop the flag and let the
|
|
199
|
+
// next chunk's leading content stick to a `\n` text node.
|
|
200
|
+
if (state.skip_leading_newlines) {
|
|
201
|
+
while (state.pos < state.buffer.length && state.buffer.charCodeAt(state.pos) === NEWLINE) {
|
|
202
|
+
state.pos++;
|
|
203
|
+
}
|
|
204
|
+
if (state.pos < state.buffer.length) {
|
|
205
|
+
state.skip_leading_newlines = false;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
while (state.pos < state.buffer.length) {
|
|
209
|
+
// codeblock mode
|
|
210
|
+
if (state.codeblock) {
|
|
211
|
+
if (forced) {
|
|
212
|
+
process_codeblock_forced(state);
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
if (!process_codeblock(state)) return; // need more input
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// optimistic inline code mode — raw scanning for close/revert
|
|
220
|
+
if (state.in_code) {
|
|
221
|
+
process_code_content(state);
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// text-first URL/path scanning mode
|
|
226
|
+
if (state.pending_url !== null) {
|
|
227
|
+
process_url_content(state, forced);
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const char_code = state.buffer.charCodeAt(state.pos);
|
|
232
|
+
|
|
233
|
+
// newline handling
|
|
234
|
+
if (char_code === NEWLINE) {
|
|
235
|
+
if (state.in_heading) {
|
|
236
|
+
// newline ends heading
|
|
237
|
+
flush_text(state);
|
|
238
|
+
close_heading(state);
|
|
239
|
+
state.pos++;
|
|
240
|
+
// absorb consecutive newlines after heading
|
|
241
|
+
while (state.pos < state.buffer.length && state.buffer.charCodeAt(state.pos) === NEWLINE) {
|
|
242
|
+
state.pos++;
|
|
243
|
+
}
|
|
244
|
+
if (state.pos >= state.buffer.length) {
|
|
245
|
+
state.skip_leading_newlines = true;
|
|
246
|
+
}
|
|
247
|
+
state.column = 0;
|
|
248
|
+
state.prev_char = NEWLINE;
|
|
249
|
+
continue;
|
|
250
|
+
}
|
|
251
|
+
// hold trailing \n when streaming (could be start of \n\n)
|
|
252
|
+
if (!forced && state.pos + 1 >= state.buffer.length) return;
|
|
253
|
+
// check for paragraph break (\n\n)
|
|
254
|
+
if (
|
|
255
|
+
state.pos + 1 < state.buffer.length &&
|
|
256
|
+
state.buffer.charCodeAt(state.pos + 1) === NEWLINE
|
|
257
|
+
) {
|
|
258
|
+
handle_paragraph_break(state);
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
// single newline: accumulate as text
|
|
262
|
+
accumulate_text(state, '\n', offset(state));
|
|
263
|
+
state.pos++;
|
|
264
|
+
state.column = 0;
|
|
265
|
+
state.prev_char = NEWLINE;
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// block elements at column 0
|
|
270
|
+
if (state.column === 0 && !state.in_heading) {
|
|
271
|
+
if (char_code === HASH) {
|
|
272
|
+
const r = try_heading(state);
|
|
273
|
+
if (r === 'consumed') continue;
|
|
274
|
+
if (!forced && r === 'need_more') return;
|
|
275
|
+
} else if (char_code === HYPHEN) {
|
|
276
|
+
const r = try_hr(state, forced);
|
|
277
|
+
if (r === 'consumed') continue;
|
|
278
|
+
if (!forced && r === 'need_more') return;
|
|
279
|
+
} else if (char_code === BACKTICK) {
|
|
280
|
+
const r = try_codeblock_open(state);
|
|
281
|
+
if (r === 'consumed') continue;
|
|
282
|
+
if (!forced && r === 'need_more') return;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// inline processing
|
|
287
|
+
if (!process_inline(state, forced)) return; // need more input
|
|
288
|
+
}
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
// -- Inline processing --
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Process one inline element. Returns false if more input is needed.
|
|
295
|
+
* When `forced` is true (EOF processing), skips optimistic opening constructs
|
|
296
|
+
* (no new bold/code/link/tag opens) but still handles italic (non-optimistic,
|
|
297
|
+
* safe to try), all closing constructs, and auto-links.
|
|
298
|
+
* Never returns false in forced mode.
|
|
299
|
+
*/
|
|
300
|
+
const process_inline = (state: MdzStreamParserState, forced: boolean): boolean => {
|
|
301
|
+
const char_code = state.buffer.charCodeAt(state.pos);
|
|
302
|
+
|
|
303
|
+
// check for closing delimiters first (if matching open exists)
|
|
304
|
+
if (
|
|
305
|
+
char_code === ASTERISK &&
|
|
306
|
+
state.pos + 1 < state.buffer.length &&
|
|
307
|
+
state.buffer.charCodeAt(state.pos + 1) === ASTERISK
|
|
308
|
+
) {
|
|
309
|
+
const bold_idx = find_open(state, 'Bold');
|
|
310
|
+
if (bold_idx !== -1) {
|
|
311
|
+
return close_bold(state, bold_idx);
|
|
312
|
+
}
|
|
313
|
+
if (!forced) return try_bold(state);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
if (char_code === ASTERISK && !forced) {
|
|
317
|
+
// could be start of ** — need next char to decide
|
|
318
|
+
if (state.pos + 1 >= state.buffer.length) return false; // hold for next chunk
|
|
319
|
+
// single asterisk is text (next char is not *)
|
|
320
|
+
ensure_paragraph(state);
|
|
321
|
+
accumulate_text(state, '*', offset(state));
|
|
322
|
+
state.prev_char = ASTERISK;
|
|
323
|
+
state.column++;
|
|
324
|
+
state.pos++;
|
|
325
|
+
return true;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
if (char_code === UNDERSCORE) {
|
|
329
|
+
const open_idx = find_open(state, 'Italic');
|
|
330
|
+
if (open_idx !== -1 && check_close_word_boundary(state)) {
|
|
331
|
+
return close_single_delimiter(state, open_idx);
|
|
332
|
+
}
|
|
333
|
+
// Italic is non-optimistic — only opens when closer is confirmed in
|
|
334
|
+
// the buffer, so it's safe (and necessary) to try in forced mode too.
|
|
335
|
+
return try_italic(state, forced);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
if (char_code === TILDE) {
|
|
339
|
+
const open_idx = find_open(state, 'Strikethrough');
|
|
340
|
+
if (open_idx !== -1 && check_close_word_boundary(state)) {
|
|
341
|
+
return close_single_delimiter(state, open_idx);
|
|
342
|
+
}
|
|
343
|
+
if (!forced) return try_strikethrough(state);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
if (char_code === BACKTICK && !forced) {
|
|
347
|
+
return try_code(state);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
if (char_code === LEFT_BRACKET && !forced) {
|
|
351
|
+
return try_link_open(state);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
if (char_code === RIGHT_BRACKET && !forced) {
|
|
355
|
+
const link_idx = find_open(state, 'Link');
|
|
356
|
+
if (link_idx !== -1) {
|
|
357
|
+
return try_complete_link(state, link_idx);
|
|
358
|
+
}
|
|
359
|
+
// no open link — treat as text
|
|
360
|
+
ensure_paragraph(state);
|
|
361
|
+
accumulate_text(state, ']', offset(state));
|
|
362
|
+
state.prev_char = RIGHT_BRACKET;
|
|
363
|
+
state.column++;
|
|
364
|
+
state.pos++;
|
|
365
|
+
return true;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
if (char_code === LEFT_ANGLE) {
|
|
369
|
+
// closing tags are handled even in forced mode
|
|
370
|
+
if (state.pos + 1 < state.buffer.length && state.buffer.charCodeAt(state.pos + 1) === SLASH) {
|
|
371
|
+
const result = try_close_tag(state);
|
|
372
|
+
if (result === 'consumed') return true;
|
|
373
|
+
// in forced mode, need_more falls through to text
|
|
374
|
+
if (!forced && result === 'need_more') return false;
|
|
375
|
+
// not_match — fall through
|
|
376
|
+
}
|
|
377
|
+
if (!forced) {
|
|
378
|
+
const tag_result = try_tag_open(state);
|
|
379
|
+
if (tag_result === 'consumed') return true;
|
|
380
|
+
if (tag_result === 'need_more') return false;
|
|
381
|
+
// not_match — fall through
|
|
382
|
+
}
|
|
383
|
+
// not a valid tag — fall through to text
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// auto-detected URLs (text-first speculative prefix matching).
|
|
387
|
+
// Fires on `h` and `H` — scheme matching is case-insensitive (RFC 3986).
|
|
388
|
+
if (char_code === H_LOWER || char_code === H_UPPER) {
|
|
389
|
+
if (forced) {
|
|
390
|
+
const result = try_auto_url_forced(state);
|
|
391
|
+
if (result === 'consumed') return true;
|
|
392
|
+
if (result === 'need_more') return false;
|
|
393
|
+
} else if (state.prev_char === -1 || !is_word_char(state.prev_char)) {
|
|
394
|
+
// word boundary — start speculative URL prefix matching
|
|
395
|
+
start_speculative_url(state);
|
|
396
|
+
return true;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// auto-detected paths
|
|
401
|
+
if (char_code === SLASH) {
|
|
402
|
+
const result = try_auto_path_absolute(state, forced);
|
|
403
|
+
if (result === 'consumed') return true;
|
|
404
|
+
if (result === 'need_more') return false;
|
|
405
|
+
}
|
|
406
|
+
if (char_code === PERIOD) {
|
|
407
|
+
const result = try_auto_path_relative(state, forced);
|
|
408
|
+
if (result === 'consumed') return true;
|
|
409
|
+
if (result === 'need_more') return false;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// plain text
|
|
413
|
+
consume_text_run(state);
|
|
414
|
+
return true;
|
|
415
|
+
};
|