@fuzdev/fuz_ui 0.203.1 → 0.205.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.
Files changed (137) hide show
  1. package/dist/Alert.svelte +31 -17
  2. package/dist/Alert.svelte.d.ts +8 -2
  3. package/dist/Alert.svelte.d.ts.map +1 -1
  4. package/dist/ApiModule.svelte +14 -1
  5. package/dist/ApiModule.svelte.d.ts.map +1 -1
  6. package/dist/Card.svelte +28 -10
  7. package/dist/Card.svelte.d.ts +7 -2
  8. package/dist/Card.svelte.d.ts.map +1 -1
  9. package/dist/CopyToClipboard.svelte +5 -1
  10. package/dist/CopyToClipboard.svelte.d.ts.map +1 -1
  11. package/dist/DeclarationDetail.svelte +15 -4
  12. package/dist/DeclarationDetail.svelte.d.ts.map +1 -1
  13. package/dist/DocsLink.svelte +15 -4
  14. package/dist/DocsLink.svelte.d.ts +8 -1
  15. package/dist/DocsLink.svelte.d.ts.map +1 -1
  16. package/dist/DocsMenu.svelte +1 -1
  17. package/dist/DocsPageLinks.svelte +1 -1
  18. package/dist/EcosystemLinks.svelte +5 -2
  19. package/dist/EcosystemLinks.svelte.d.ts +2 -1
  20. package/dist/EcosystemLinks.svelte.d.ts.map +1 -1
  21. package/dist/EcosystemLinksPanel.svelte +4 -2
  22. package/dist/EcosystemLinksPanel.svelte.d.ts +2 -1
  23. package/dist/EcosystemLinksPanel.svelte.d.ts.map +1 -1
  24. package/dist/Hashlink.svelte +1 -1
  25. package/dist/ImgOrSvg.svelte +6 -5
  26. package/dist/ImgOrSvg.svelte.d.ts +2 -1
  27. package/dist/ImgOrSvg.svelte.d.ts.map +1 -1
  28. package/dist/LibraryDetail.svelte +5 -2
  29. package/dist/LibraryDetail.svelte.d.ts +3 -1
  30. package/dist/LibraryDetail.svelte.d.ts.map +1 -1
  31. package/dist/LibrarySummary.svelte +4 -2
  32. package/dist/LibrarySummary.svelte.d.ts +2 -1
  33. package/dist/LibrarySummary.svelte.d.ts.map +1 -1
  34. package/dist/PasteFromClipboard.svelte +3 -1
  35. package/dist/PasteFromClipboard.svelte.d.ts.map +1 -1
  36. package/dist/PendingButton.svelte +0 -2
  37. package/dist/PendingButton.svelte.d.ts.map +1 -1
  38. package/dist/StyleVariableButton.svelte +5 -1
  39. package/dist/StyleVariableButton.svelte.d.ts.map +1 -1
  40. package/dist/Svg.svelte +1 -1
  41. package/dist/TomeContent.svelte +5 -2
  42. package/dist/TomeContent.svelte.d.ts +2 -1
  43. package/dist/TomeContent.svelte.d.ts.map +1 -1
  44. package/dist/TypeLink.svelte +13 -4
  45. package/dist/TypeLink.svelte.d.ts +7 -1
  46. package/dist/TypeLink.svelte.d.ts.map +1 -1
  47. package/package.json +12 -36
  48. package/dist/Mdz.svelte +0 -34
  49. package/dist/Mdz.svelte.d.ts +0 -11
  50. package/dist/Mdz.svelte.d.ts.map +0 -1
  51. package/dist/MdzNodeView.svelte +0 -107
  52. package/dist/MdzNodeView.svelte.d.ts +0 -9
  53. package/dist/MdzNodeView.svelte.d.ts.map +0 -1
  54. package/dist/MdzPrecompiled.svelte +0 -30
  55. package/dist/MdzPrecompiled.svelte.d.ts +0 -11
  56. package/dist/MdzPrecompiled.svelte.d.ts.map +0 -1
  57. package/dist/MdzRoot.svelte +0 -30
  58. package/dist/MdzRoot.svelte.d.ts +0 -12
  59. package/dist/MdzRoot.svelte.d.ts.map +0 -1
  60. package/dist/MdzStream.svelte +0 -32
  61. package/dist/MdzStream.svelte.d.ts +0 -12
  62. package/dist/MdzStream.svelte.d.ts.map +0 -1
  63. package/dist/MdzStreamNodeView.svelte +0 -106
  64. package/dist/MdzStreamNodeView.svelte.d.ts +0 -9
  65. package/dist/MdzStreamNodeView.svelte.d.ts.map +0 -1
  66. package/dist/mdz.d.ts +0 -112
  67. package/dist/mdz.d.ts.map +0 -1
  68. package/dist/mdz.js +0 -1186
  69. package/dist/mdz_components.d.ts +0 -63
  70. package/dist/mdz_components.d.ts.map +0 -1
  71. package/dist/mdz_components.js +0 -32
  72. package/dist/mdz_helpers.d.ts +0 -164
  73. package/dist/mdz_helpers.d.ts.map +0 -1
  74. package/dist/mdz_helpers.js +0 -424
  75. package/dist/mdz_lexer.d.ts +0 -93
  76. package/dist/mdz_lexer.d.ts.map +0 -1
  77. package/dist/mdz_lexer.js +0 -732
  78. package/dist/mdz_opcodes.d.ts +0 -174
  79. package/dist/mdz_opcodes.d.ts.map +0 -1
  80. package/dist/mdz_opcodes.js +0 -14
  81. package/dist/mdz_opcodes_to_nodes.d.ts +0 -20
  82. package/dist/mdz_opcodes_to_nodes.d.ts.map +0 -1
  83. package/dist/mdz_opcodes_to_nodes.js +0 -332
  84. package/dist/mdz_stream_parser.d.ts +0 -80
  85. package/dist/mdz_stream_parser.d.ts.map +0 -1
  86. package/dist/mdz_stream_parser.js +0 -354
  87. package/dist/mdz_stream_parser_block.d.ts +0 -76
  88. package/dist/mdz_stream_parser_block.d.ts.map +0 -1
  89. package/dist/mdz_stream_parser_block.js +0 -458
  90. package/dist/mdz_stream_parser_inline.d.ts +0 -52
  91. package/dist/mdz_stream_parser_inline.d.ts.map +0 -1
  92. package/dist/mdz_stream_parser_inline.js +0 -378
  93. package/dist/mdz_stream_parser_link.d.ts +0 -22
  94. package/dist/mdz_stream_parser_link.d.ts.map +0 -1
  95. package/dist/mdz_stream_parser_link.js +0 -215
  96. package/dist/mdz_stream_parser_state.d.ts +0 -187
  97. package/dist/mdz_stream_parser_state.d.ts.map +0 -1
  98. package/dist/mdz_stream_parser_state.js +0 -398
  99. package/dist/mdz_stream_parser_text.d.ts +0 -14
  100. package/dist/mdz_stream_parser_text.d.ts.map +0 -1
  101. package/dist/mdz_stream_parser_text.js +0 -50
  102. package/dist/mdz_stream_parser_url.d.ts +0 -60
  103. package/dist/mdz_stream_parser_url.d.ts.map +0 -1
  104. package/dist/mdz_stream_parser_url.js +0 -307
  105. package/dist/mdz_stream_state.svelte.d.ts +0 -44
  106. package/dist/mdz_stream_state.svelte.d.ts.map +0 -1
  107. package/dist/mdz_stream_state.svelte.js +0 -357
  108. package/dist/mdz_to_svelte.d.ts +0 -41
  109. package/dist/mdz_to_svelte.d.ts.map +0 -1
  110. package/dist/mdz_to_svelte.js +0 -100
  111. package/dist/mdz_token_parser.d.ts +0 -14
  112. package/dist/mdz_token_parser.d.ts.map +0 -1
  113. package/dist/mdz_token_parser.js +0 -344
  114. package/dist/svelte_preprocess_mdz.d.ts +0 -65
  115. package/dist/svelte_preprocess_mdz.d.ts.map +0 -1
  116. package/dist/svelte_preprocess_mdz.js +0 -529
  117. package/dist/tsdoc_mdz.d.ts +0 -45
  118. package/dist/tsdoc_mdz.d.ts.map +0 -1
  119. package/dist/tsdoc_mdz.js +0 -88
  120. package/src/lib/mdz.ts +0 -1532
  121. package/src/lib/mdz_components.ts +0 -64
  122. package/src/lib/mdz_helpers.ts +0 -449
  123. package/src/lib/mdz_lexer.ts +0 -1012
  124. package/src/lib/mdz_opcodes.ts +0 -205
  125. package/src/lib/mdz_opcodes_to_nodes.ts +0 -375
  126. package/src/lib/mdz_stream_parser.ts +0 -415
  127. package/src/lib/mdz_stream_parser_block.ts +0 -532
  128. package/src/lib/mdz_stream_parser_inline.ts +0 -414
  129. package/src/lib/mdz_stream_parser_link.ts +0 -271
  130. package/src/lib/mdz_stream_parser_state.ts +0 -539
  131. package/src/lib/mdz_stream_parser_text.ts +0 -77
  132. package/src/lib/mdz_stream_parser_url.ts +0 -365
  133. package/src/lib/mdz_stream_state.svelte.ts +0 -387
  134. package/src/lib/mdz_to_svelte.ts +0 -141
  135. package/src/lib/mdz_token_parser.ts +0 -434
  136. package/src/lib/svelte_preprocess_mdz.ts +0 -742
  137. package/src/lib/tsdoc_mdz.ts +0 -97
@@ -1,539 +0,0 @@
1
- /**
2
- * Streaming parser state and low-level operations.
3
- *
4
- * Free functions take a `MdzStreamParserState` first parameter so handlers can
5
- * live in separate sibling modules — JS private (`#`) fields can't cross
6
- * module boundaries.
7
- *
8
- * @module
9
- */
10
-
11
- import type {MdzContainerNodeType, MdzNodeId, MdzOpcode} from './mdz_opcodes.js';
12
- import {NEWLINE, mdz_heading_id_from_text} from './mdz_helpers.js';
13
-
14
- /**
15
- * Tri-state result for `try_*` parser handlers.
16
- * - `'consumed'`: input matched and the parser advanced
17
- * - `'need_more'`: input is potentially valid but more bytes are required to decide
18
- * - `'not_match'`: input definitely doesn't match — caller falls through to the next handler
19
- */
20
- export type TryResult = 'consumed' | 'need_more' | 'not_match';
21
-
22
- export interface StackEntry {
23
- id: MdzNodeId;
24
- node_type: MdzContainerNodeType;
25
- /** Whether this was opened speculatively (will be reverted if not closed). */
26
- optimistic: boolean;
27
- /** The opening delimiter text, used as `replacement_text` on revert. */
28
- delimiter: string;
29
- /** Tag name for Element/Component entries, `undefined` for all others. */
30
- tag_name: string | undefined;
31
- /** Whether any child content has been emitted inside this container. */
32
- has_children: boolean;
33
- /**
34
- * Whether any non-whitespace text or non-text child has been emitted
35
- * inside this container. Only tracked for `Paragraph` entries to detect
36
- * whitespace-only paragraphs that should be dropped at close.
37
- */
38
- has_non_whitespace_content: boolean;
39
- /** Global byte offset of the opening delimiter. */
40
- start: number;
41
- }
42
-
43
- export interface CodeblockState {
44
- id: MdzNodeId;
45
- backtick_count: number;
46
- text_id: MdzNodeId | null;
47
- /** The full opening fence line (e.g. "```ts\n"), used as `replacement_text` on revert. */
48
- delimiter: string;
49
- /** Global byte offset of the opening fence. */
50
- start: number;
51
- }
52
-
53
- /**
54
- * Pending auto-link URL/path state for text-first rendering.
55
- * When set, URL chars flow as visible text. On terminator, a `wrap` opcode
56
- * retroactively wraps the text node in a Link.
57
- *
58
- * For URLs, `confirmed` starts false during speculative prefix matching
59
- * (chars stream as text while we verify `https://` or `http://`).
60
- * For paths, `confirmed` starts true (prefix already validated by hold).
61
- */
62
- export interface PendingUrl {
63
- url_text: string;
64
- start: number;
65
- link_type: 'external' | 'internal';
66
- /** Whether the URL/path prefix has been fully confirmed. */
67
- confirmed: boolean;
68
- /** Prefix match tracking — only used when `!confirmed`. */
69
- viable_https: boolean;
70
- viable_http: boolean;
71
- }
72
-
73
- /**
74
- * Mutable state for the streaming parser. One instance per `MdzStreamParser`.
75
- * Handlers in sibling modules take this as their first parameter — the streaming
76
- * parser uses free functions, not class methods, so state crosses module
77
- * boundaries.
78
- */
79
- export interface MdzStreamParserState {
80
- buffer: string;
81
- pos: number;
82
- opcodes: Array<MdzOpcode>;
83
- next_id: MdzNodeId;
84
- stack: Array<StackEntry>;
85
- column: number;
86
- prev_char: number;
87
- active_text_id: MdzNodeId | null;
88
- accumulated_text: string;
89
- accumulated_text_start: number;
90
- codeblock: CodeblockState | null;
91
- /** Global byte offset of the start of `buffer`. */
92
- base_offset: number;
93
- /** Whether we're inside a heading (newline ends it). */
94
- in_heading: boolean;
95
- /** Whether we're inside an optimistic inline Code container. */
96
- in_code: boolean;
97
- /** Cached flag: whether a Paragraph is open on the stack. */
98
- in_paragraph: boolean;
99
- pending_url: PendingUrl | null;
100
- /**
101
- * Stack of text segments for heading ID computation.
102
- * Each open container inside a heading pushes a new segment.
103
- * On close: pop and append to parent (children's text is part of heading).
104
- * On revert: pop, prepend replacement_text, append to parent
105
- * (document order: delimiter text comes before children's text).
106
- */
107
- heading_text_parts: Array<string>;
108
- /**
109
- * When true, skip leading newlines at the start of the next processing pass.
110
- * Set after block element closes (codeblock, heading, HR) when trailing
111
- * newlines couldn't be absorbed within the same buffer — needed for
112
- * char-by-char streaming where post-block newlines arrive in later chunks.
113
- */
114
- skip_leading_newlines: boolean;
115
- /**
116
- * Stack index of the innermost open `Paragraph`, or `-1` when none.
117
- * Lets `mark_paragraph_non_whitespace` skip the stack walk that runs on
118
- * every non-whitespace emit. Updated only on Paragraph push/pop (block
119
- * boundaries — only one of `Paragraph`/`Heading` can be on the stack
120
- * at a time, so the cache is stable across inline pushes/pops).
121
- */
122
- paragraph_stack_idx: number;
123
- /**
124
- * Id of the most-recent `text`/`append_text` emission, used by
125
- * `trim_trailing_newline` to target the right opcode after `take_opcodes()`
126
- * has drained the queue. `null` after a structural emit (open/close/revert)
127
- * sealed the prior text run.
128
- */
129
- last_text_id: MdzNodeId | null;
130
- /** Whether the most recent text emission's content ended in `'\n'`. */
131
- last_text_ended_with_newline: boolean;
132
- /**
133
- * Whether the most recent text emission was a `text` opcode (not
134
- * `append_text`) with content exactly `'\n'` — trimming the trailing \n
135
- * would leave the text node empty, so `active_text_id` must be cleared
136
- * to keep subsequent content from merging into a deleted node.
137
- */
138
- last_text_was_singleton_newline: boolean;
139
- }
140
-
141
- export const create_state = (): MdzStreamParserState => ({
142
- buffer: '',
143
- pos: 0,
144
- opcodes: [],
145
- next_id: 0,
146
- stack: [],
147
- column: 0,
148
- prev_char: -1,
149
- active_text_id: null,
150
- accumulated_text: '',
151
- accumulated_text_start: 0,
152
- codeblock: null,
153
- base_offset: 0,
154
- in_heading: false,
155
- in_code: false,
156
- in_paragraph: false,
157
- pending_url: null,
158
- heading_text_parts: [],
159
- // matches mdz_parse: leading newlines before any block are skipped, not
160
- // preserved as paragraph content
161
- skip_leading_newlines: true,
162
- paragraph_stack_idx: -1,
163
- last_text_id: null,
164
- last_text_ended_with_newline: false,
165
- last_text_was_singleton_newline: false,
166
- });
167
-
168
- export const alloc_id = (state: MdzStreamParserState): MdzNodeId => state.next_id++;
169
-
170
- /**
171
- * Whether `text` contains any non-whitespace character. Hot path — called from
172
- * `emit` on every text emission. Char-loop checking common whitespace (space,
173
- * tab, newline, carriage return, form feed, vertical tab) avoids the regex
174
- * allocation that `/\S/.test()` incurs per call.
175
- *
176
- * Deliberately ASCII-only. `/\S/` and `String.prototype.trim()` (which
177
- * `mdz_parse` uses to detect whitespace-only paragraphs) recognize Unicode
178
- * whitespace too (U+00A0 NBSP, U+2028/U+2029, U+3000, etc.). A paragraph
179
- * containing only Unicode whitespace would be kept by the streaming parser
180
- * but dropped by `mdz_parse` — no fixture currently hits this.
181
- */
182
- const has_non_whitespace = (text: string): boolean => {
183
- for (let i = 0; i < text.length; i++) {
184
- const c = text.charCodeAt(i);
185
- // 0x09=tab, 0x0a=LF, 0x0b=VT, 0x0c=FF, 0x0d=CR, 0x20=space
186
- if (c !== 0x20 && (c < 0x09 || c > 0x0d)) return true;
187
- }
188
- return false;
189
- };
190
-
191
- /**
192
- * Push a new container frame onto the stack. Fills the boilerplate fields
193
- * (`has_children`, `has_non_whitespace_content`) so call sites only spell out
194
- * what varies per container type. Keeping the object literal in one place
195
- * also gives V8 a single monomorphic creation site for `StackEntry`.
196
- */
197
- export const push_stack_entry = (
198
- state: MdzStreamParserState,
199
- id: MdzNodeId,
200
- node_type: MdzContainerNodeType,
201
- start: number,
202
- optimistic: boolean = false,
203
- delimiter: string = '',
204
- tag_name?: string,
205
- ): void => {
206
- state.stack.push({
207
- id,
208
- node_type,
209
- optimistic,
210
- delimiter,
211
- tag_name,
212
- has_children: false,
213
- has_non_whitespace_content: false,
214
- start,
215
- });
216
- if (node_type === 'Paragraph') {
217
- state.paragraph_stack_idx = state.stack.length - 1;
218
- }
219
- };
220
-
221
- /**
222
- * Mark the innermost open `Paragraph` as having non-whitespace content,
223
- * via the `paragraph_stack_idx` cache (O(1) instead of an O(stack) walk).
224
- *
225
- * No-op when no Paragraph is open — that includes the Heading case, since
226
- * block boundaries are mutually exclusive (a Heading on stack means
227
- * `paragraph_stack_idx === -1`).
228
- */
229
- const mark_paragraph_non_whitespace = (state: MdzStreamParserState): void => {
230
- if (state.paragraph_stack_idx === -1) return;
231
- state.stack[state.paragraph_stack_idx]!.has_non_whitespace_content = true;
232
- };
233
-
234
- /** Global byte offset for a local buffer position. */
235
- export const offset = (state: MdzStreamParserState, pos: number = state.pos): number =>
236
- state.base_offset + pos;
237
-
238
- export const emit = (state: MdzStreamParserState, op: MdzOpcode): void => {
239
- state.opcodes.push(op);
240
- // mark the innermost container as having children
241
- if (op.type === 'text' || op.type === 'append_text' || op.type === 'void') {
242
- const top = state.stack[state.stack.length - 1];
243
- if (top) {
244
- top.has_children = true;
245
- // propagate non-whitespace marker to the nearest Paragraph so
246
- // whitespace-only paragraphs can be discarded at close
247
- if (op.type === 'void' || has_non_whitespace(op.content)) {
248
- mark_paragraph_non_whitespace(state);
249
- }
250
- }
251
- } else if (op.type === 'open') {
252
- // opening a child container counts as content for the parent
253
- const parent = state.stack[state.stack.length - 1];
254
- if (parent) {
255
- parent.has_children = true;
256
- mark_paragraph_non_whitespace(state);
257
- }
258
- } else if (op.type === 'wrap') {
259
- // retroactive Link wrap of a text node — the resulting Link is non-whitespace
260
- mark_paragraph_non_whitespace(state);
261
- }
262
- // track the most recent text run so `trim_trailing_newline` can target it
263
- // after `take_opcodes()` has drained the queue
264
- if (op.type === 'text') {
265
- state.last_text_id = op.id;
266
- state.last_text_ended_with_newline = op.content.endsWith('\n');
267
- state.last_text_was_singleton_newline = op.content === '\n';
268
- } else if (op.type === 'append_text') {
269
- state.last_text_id = op.id;
270
- state.last_text_ended_with_newline = op.content.endsWith('\n');
271
- // append_text builds on a prior text node; the underlying node can't be
272
- // singleton even if this appended chunk is just '\n'
273
- state.last_text_was_singleton_newline = false;
274
- } else if (op.type === 'open' || op.type === 'close' || op.type === 'revert') {
275
- // structural opcode seals the prior text run
276
- state.last_text_id = null;
277
- state.last_text_ended_with_newline = false;
278
- state.last_text_was_singleton_newline = false;
279
- }
280
- // track text content for heading ID computation
281
- if (state.in_heading && state.heading_text_parts.length > 0) {
282
- if (op.type === 'text' || op.type === 'append_text') {
283
- state.heading_text_parts[state.heading_text_parts.length - 1] += op.content;
284
- } else if (op.type === 'open') {
285
- // child container: push a new segment for its text content
286
- state.heading_text_parts.push('');
287
- } else if (op.type === 'close') {
288
- // normal close: pop child's text and merge into parent
289
- if (state.heading_text_parts.length > 1) {
290
- const child_text = state.heading_text_parts.pop()!;
291
- state.heading_text_parts[state.heading_text_parts.length - 1] += child_text;
292
- }
293
- } else if (op.type === 'revert') {
294
- // revert: pop child's text, prepend replacement (document order), merge into parent
295
- if (state.heading_text_parts.length > 1) {
296
- const child_text = state.heading_text_parts.pop()!;
297
- state.heading_text_parts[state.heading_text_parts.length - 1] +=
298
- (op.replacement_text || '') + child_text;
299
- }
300
- }
301
- }
302
- };
303
-
304
- /** Accumulate text, tracking the start offset for the first character. */
305
- export const accumulate_text = (
306
- state: MdzStreamParserState,
307
- text: string,
308
- start_offset: number,
309
- ): void => {
310
- if (state.accumulated_text.length === 0) {
311
- state.accumulated_text_start = start_offset;
312
- }
313
- state.accumulated_text += text;
314
- };
315
-
316
- /**
317
- * Flush accumulated text as a text or append_text opcode.
318
- */
319
- export const flush_text = (state: MdzStreamParserState): void => {
320
- if (state.accumulated_text.length === 0) return;
321
- if (state.active_text_id !== null) {
322
- emit(state, {
323
- type: 'append_text',
324
- id: state.active_text_id,
325
- content: state.accumulated_text,
326
- });
327
- } else {
328
- ensure_paragraph(state);
329
- const id = alloc_id(state);
330
- const start = state.accumulated_text_start;
331
- emit(state, {
332
- type: 'text',
333
- id,
334
- content: state.accumulated_text,
335
- text_type: 'Text',
336
- start,
337
- end: start + state.accumulated_text.length,
338
- });
339
- state.active_text_id = id;
340
- }
341
- state.accumulated_text = '';
342
- };
343
-
344
- export const ensure_paragraph = (state: MdzStreamParserState): void => {
345
- if (state.in_heading || state.in_paragraph) return;
346
- const id = alloc_id(state);
347
- const start = offset(state);
348
- emit(state, {type: 'open', id, node_type: 'Paragraph', start});
349
- push_stack_entry(state, id, 'Paragraph', start);
350
- state.in_paragraph = true;
351
- };
352
-
353
- /**
354
- * Find the innermost open container of a given type.
355
- * Returns stack index, or -1 if not found.
356
- * Does not cross block boundaries (Paragraph, Heading).
357
- */
358
- export const find_open = (state: MdzStreamParserState, type: MdzContainerNodeType): number => {
359
- for (let i = state.stack.length - 1; i >= 0; i--) {
360
- const entry = state.stack[i]!;
361
- if (entry.node_type === type) return i;
362
- // don't cross block boundaries
363
- if (entry.node_type === 'Paragraph' || entry.node_type === 'Heading') return -1;
364
- }
365
- return -1;
366
- };
367
-
368
- /**
369
- * Revert all stack entries above the given index.
370
- */
371
- export const revert_above = (state: MdzStreamParserState, target_idx: number): void => {
372
- while (state.stack.length - 1 > target_idx) {
373
- const entry = state.stack.pop()!;
374
- if (entry.node_type === 'Code') state.in_code = false;
375
- if (entry.optimistic) {
376
- emit(state, {
377
- type: 'revert',
378
- id: entry.id,
379
- replacement_text: entry.delimiter,
380
- start: entry.start,
381
- });
382
- } else {
383
- emit(state, {type: 'close', id: entry.id, end: offset(state)});
384
- }
385
- }
386
- state.active_text_id = null;
387
- };
388
-
389
- /**
390
- * Revert all optimistic inline containers in the current block context.
391
- */
392
- export const revert_all_optimistic = (state: MdzStreamParserState): void => {
393
- while (state.stack.length > 0) {
394
- const top = state.stack[state.stack.length - 1]!;
395
- if (top.node_type === 'Paragraph' || top.node_type === 'Heading') break;
396
- state.stack.pop();
397
- if (top.node_type === 'Code') state.in_code = false;
398
- if (top.optimistic) {
399
- emit(state, {
400
- type: 'revert',
401
- id: top.id,
402
- replacement_text: top.delimiter,
403
- start: top.start,
404
- });
405
- } else {
406
- emit(state, {type: 'close', id: top.id, end: offset(state)});
407
- }
408
- }
409
- state.active_text_id = null;
410
- };
411
-
412
- /**
413
- * Trim a trailing newline from paragraph content.
414
- * Checks unflushed accumulated text first; otherwise emits a `trim_text`
415
- * opcode targeting the most recent text/append_text via the tracking fields
416
- * on `state` (which survive `take_opcodes()` drains). The opcode stream
417
- * stays append-only — no retroactive opcode mutation.
418
- */
419
- export const trim_trailing_newline = (state: MdzStreamParserState): void => {
420
- if (state.accumulated_text.endsWith('\n')) {
421
- state.accumulated_text = state.accumulated_text.slice(0, -1);
422
- return;
423
- }
424
- if (state.last_text_id === null || !state.last_text_ended_with_newline) return;
425
- // emit trim opcode; consumer adjusts content and removes empty nodes
426
- emit(state, {type: 'trim_text', id: state.last_text_id, count: 1});
427
- // if this trim empties a singleton text node, clear active_text_id so
428
- // subsequent content doesn't merge into a deleted node via append_text
429
- if (state.last_text_was_singleton_newline) {
430
- state.active_text_id = null;
431
- }
432
- // after trim, the prior text no longer ends in \n — guard against re-trim
433
- state.last_text_ended_with_newline = false;
434
- state.last_text_was_singleton_newline = false;
435
- };
436
-
437
- export const close_paragraph = (state: MdzStreamParserState): void => {
438
- // find and close the paragraph on the stack
439
- for (let i = state.stack.length - 1; i >= 0; i--) {
440
- if (state.stack[i]!.node_type === 'Paragraph') {
441
- // trim trailing newline from paragraph content
442
- trim_trailing_newline(state);
443
- // revert everything above the paragraph
444
- while (state.stack.length - 1 > i) {
445
- const entry = state.stack.pop()!;
446
- if (entry.node_type === 'Code') state.in_code = false;
447
- emit(state, {
448
- type: 'revert',
449
- id: entry.id,
450
- replacement_text: entry.delimiter,
451
- start: entry.start,
452
- });
453
- }
454
- const entry = state.stack.pop()!;
455
- // drop whitespace-only paragraphs to match `mdz_parse`'s output;
456
- // consumers (`mdz_opcodes_to_nodes`, `MdzStreamState`) honor `discard`
457
- // by removing the node and its descendants from the tree
458
- const discard = !entry.has_non_whitespace_content;
459
- emit(
460
- state,
461
- discard
462
- ? {type: 'close', id: entry.id, end: offset(state), discard: true}
463
- : {type: 'close', id: entry.id, end: offset(state)},
464
- );
465
- state.active_text_id = null;
466
- state.in_paragraph = false;
467
- state.paragraph_stack_idx = -1;
468
- return;
469
- }
470
- }
471
- };
472
-
473
- export const close_heading = (state: MdzStreamParserState): void => {
474
- // revert optimistic containers inside the heading (before clearing in_heading,
475
- // so revert replacement text is captured in heading_text_parts)
476
- revert_all_optimistic(state);
477
- // compute heading ID from accumulated text before clearing heading state
478
- const heading_text = state.heading_text_parts.join('');
479
- state.heading_text_parts = [];
480
- state.in_heading = false;
481
- const heading_id = mdz_heading_id_from_text(heading_text);
482
- const end = offset(state);
483
- // find and close the heading
484
- for (let i = state.stack.length - 1; i >= 0; i--) {
485
- if (state.stack[i]!.node_type === 'Heading') {
486
- const entry = state.stack[i]!;
487
- state.stack.splice(i, 1);
488
- emit(state, {type: 'close', id: entry.id, end, heading_id});
489
- state.active_text_id = null;
490
- return;
491
- }
492
- }
493
- };
494
-
495
- /**
496
- * Close an unclosed codeblock at EOF by reverting it to a paragraph wrapper.
497
- *
498
- * Unlike `revert_empty_codeblock` (which pushes the wrapper onto the stack
499
- * because parsing continues), this is terminal — called only from `finish()`
500
- * after no further emits happen — so it skips the `push_stack_entry` /
501
- * `in_paragraph` / `paragraph_stack_idx` bookkeeping. The wrapper exists only
502
- * in the opcode stream; the parser state's stack stays untouched.
503
- */
504
- export const close_codeblock_at_eof = (state: MdzStreamParserState): void => {
505
- if (!state.codeblock) return;
506
- const cb = state.codeblock;
507
- // trim trailing newline from codeblock content (mirrors close_paragraph behavior)
508
- trim_trailing_newline(state);
509
- const wrap_id = alloc_id(state);
510
- emit(state, {
511
- type: 'revert',
512
- id: cb.id,
513
- replacement_text: cb.delimiter,
514
- start: cb.start,
515
- wrap_node_type: 'Paragraph',
516
- wrap_id,
517
- });
518
- emit(state, {type: 'close', id: wrap_id, end: offset(state)});
519
- state.codeblock = null;
520
- };
521
-
522
- export const handle_paragraph_break = (state: MdzStreamParserState): void => {
523
- flush_text(state);
524
- // revert all unclosed optimistic inline containers
525
- revert_all_optimistic(state);
526
- close_paragraph(state);
527
- // skip all consecutive newlines
528
- while (state.pos < state.buffer.length && state.buffer.charCodeAt(state.pos) === NEWLINE) {
529
- state.pos++;
530
- }
531
- // if we ran out of buffer while skipping, carry the absorb across chunks —
532
- // otherwise a `\n\n\n\n` split mid-run would leak a leading `\n` into the
533
- // next paragraph's text. Mirrors the heading-newline absorb in `process_inline`.
534
- if (state.pos >= state.buffer.length) {
535
- state.skip_leading_newlines = true;
536
- }
537
- state.column = 0;
538
- state.prev_char = NEWLINE;
539
- };
@@ -1,77 +0,0 @@
1
- /**
2
- * Plain-text run consumption for the streaming mdz parser.
3
- *
4
- * @module
5
- */
6
-
7
- import {
8
- ASTERISK,
9
- BACKTICK,
10
- HTTPS_PREFIX_LENGTH,
11
- H_LOWER,
12
- H_UPPER,
13
- LEFT_ANGLE,
14
- LEFT_BRACKET,
15
- NEWLINE,
16
- PERIOD,
17
- RIGHT_BRACKET,
18
- SLASH,
19
- SPACE,
20
- TAB,
21
- TILDE,
22
- UNDERSCORE,
23
- is_word_char,
24
- match_url_prefix_case_insensitive,
25
- } from './mdz_helpers.js';
26
- import {
27
- type MdzStreamParserState,
28
- accumulate_text,
29
- ensure_paragraph,
30
- offset,
31
- } from './mdz_stream_parser_state.js';
32
-
33
- /**
34
- * Consume a run of plain text characters. Scans ahead to the next
35
- * structurally interesting character and accumulates the whole run
36
- * as a single slice, avoiding per-character string concatenation
37
- * and dispatch overhead.
38
- */
39
- export const consume_text_run = (state: MdzStreamParserState): void => {
40
- ensure_paragraph(state);
41
- const start = state.pos;
42
- state.pos++;
43
- while (state.pos < state.buffer.length) {
44
- const c = state.buffer.charCodeAt(state.pos);
45
- if (
46
- c === BACKTICK ||
47
- c === ASTERISK ||
48
- c === UNDERSCORE ||
49
- c === TILDE ||
50
- c === LEFT_BRACKET ||
51
- c === RIGHT_BRACKET ||
52
- c === LEFT_ANGLE ||
53
- c === NEWLINE ||
54
- // URL/path: only break when actually at a URL or path start.
55
- // Most h/H/./slash in prose are not URLs/paths — continuing the scan
56
- // avoids the full dispatch cycle through process_loop → process_inline.
57
- // For `h`/`H` at a word boundary, also break when there isn't enough
58
- // buffer left to confirm `https://` / `http://` — process_inline's
59
- // speculator then carries the prefix match across chunk boundaries.
60
- // Scheme matching is case-insensitive (RFC 3986).
61
- ((c === H_LOWER || c === H_UPPER) &&
62
- (match_url_prefix_case_insensitive(state.buffer, state.pos) > 0 ||
63
- (state.buffer.length - state.pos < HTTPS_PREFIX_LENGTH &&
64
- !is_word_char(state.buffer.charCodeAt(state.pos - 1))))) ||
65
- ((c === SLASH || c === PERIOD) &&
66
- (state.buffer.charCodeAt(state.pos - 1) === SPACE ||
67
- state.buffer.charCodeAt(state.pos - 1) === NEWLINE ||
68
- state.buffer.charCodeAt(state.pos - 1) === TAB))
69
- ) {
70
- break;
71
- }
72
- state.pos++;
73
- }
74
- accumulate_text(state, state.buffer.slice(start, state.pos), offset(state, start));
75
- state.prev_char = state.buffer.charCodeAt(state.pos - 1);
76
- state.column += state.pos - start;
77
- };