@fuzdev/mdz 0.2.0 → 0.4.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 +4 -0
- package/dist/Mdz.svelte +18 -9
- package/dist/Mdz.svelte.d.ts +9 -2
- package/dist/Mdz.svelte.d.ts.map +1 -1
- package/dist/MdzNodeView.svelte +129 -115
- package/dist/MdzNodeView.svelte.d.ts +4 -1
- package/dist/MdzNodeView.svelte.d.ts.map +1 -1
- package/dist/MdzPrecompiled.svelte +3 -3
- package/dist/MdzPrecompiled.svelte.d.ts.map +1 -1
- package/dist/MdzRoot.svelte +3 -3
- package/dist/MdzRoot.svelte.d.ts.map +1 -1
- package/dist/MdzStream.svelte +6 -6
- package/dist/MdzStream.svelte.d.ts.map +1 -1
- package/dist/MdzStreamNodeView.svelte +129 -118
- package/dist/MdzStreamNodeView.svelte.d.ts +4 -1
- package/dist/MdzStreamNodeView.svelte.d.ts.map +1 -1
- package/dist/context_helpers.js +1 -1
- package/dist/mdz.d.ts +6 -0
- package/dist/mdz.d.ts.map +1 -1
- package/dist/mdz.js +8 -0
- package/dist/mdz_contexts.d.ts.map +1 -1
- package/dist/mdz_debug_work.d.ts +38 -0
- package/dist/mdz_debug_work.d.ts.map +1 -0
- package/dist/mdz_debug_work.js +37 -0
- package/dist/mdz_helpers.d.ts +61 -0
- package/dist/mdz_helpers.d.ts.map +1 -1
- package/dist/mdz_helpers.js +74 -2
- package/dist/mdz_lexer.d.ts +12 -0
- package/dist/mdz_lexer.d.ts.map +1 -1
- package/dist/mdz_lexer.js +190 -40
- package/dist/mdz_opcodes.d.ts.map +1 -1
- package/dist/mdz_opcodes_to_nodes.d.ts.map +1 -1
- package/dist/mdz_opcodes_to_nodes.js +22 -17
- package/dist/mdz_stream_parser.d.ts.map +1 -1
- package/dist/mdz_stream_parser.js +8 -8
- package/dist/mdz_stream_parser_block.js +5 -5
- package/dist/mdz_stream_parser_inline.d.ts.map +1 -1
- package/dist/mdz_stream_parser_inline.js +22 -20
- package/dist/mdz_stream_parser_link.d.ts.map +1 -1
- package/dist/mdz_stream_parser_link.js +88 -56
- package/dist/mdz_stream_parser_list.d.ts.map +1 -1
- package/dist/mdz_stream_parser_list.js +23 -9
- package/dist/mdz_stream_parser_state.d.ts +29 -0
- package/dist/mdz_stream_parser_state.d.ts.map +1 -1
- package/dist/mdz_stream_parser_state.js +59 -10
- package/dist/mdz_stream_parser_table.d.ts +3 -2
- package/dist/mdz_stream_parser_table.d.ts.map +1 -1
- package/dist/mdz_stream_parser_table.js +24 -13
- package/dist/mdz_stream_parser_text.d.ts.map +1 -1
- package/dist/mdz_stream_parser_text.js +36 -25
- package/dist/mdz_stream_parser_url.js +7 -7
- package/dist/mdz_stream_state.svelte.d.ts.map +1 -1
- package/dist/mdz_to_svelte.d.ts +1 -1
- package/dist/mdz_to_svelte.d.ts.map +1 -1
- package/dist/mdz_to_svelte.js +28 -31
- package/dist/mdz_token_parser.d.ts.map +1 -1
- package/dist/mdz_token_parser.js +24 -25
- package/dist/svelte_preprocess_mdz.d.ts +2 -2
- package/dist/svelte_preprocess_mdz.d.ts.map +1 -1
- package/dist/svelte_preprocess_mdz.js +41 -47
- package/package.json +28 -40
- package/src/lib/context_helpers.ts +2 -2
- package/src/lib/mdz.ts +11 -2
- package/src/lib/mdz_contexts.ts +5 -5
- package/src/lib/mdz_debug_work.ts +38 -0
- package/src/lib/mdz_helpers.ts +126 -23
- package/src/lib/mdz_lexer.ts +243 -93
- package/src/lib/mdz_opcodes.ts +1 -1
- package/src/lib/mdz_opcodes_to_nodes.ts +35 -30
- package/src/lib/mdz_stream_parser.ts +17 -17
- package/src/lib/mdz_stream_parser_block.ts +14 -14
- package/src/lib/mdz_stream_parser_inline.ts +38 -34
- package/src/lib/mdz_stream_parser_link.ts +99 -66
- package/src/lib/mdz_stream_parser_list.ts +41 -29
- package/src/lib/mdz_stream_parser_state.ts +100 -28
- package/src/lib/mdz_stream_parser_table.ts +47 -36
- package/src/lib/mdz_stream_parser_text.ts +40 -39
- package/src/lib/mdz_stream_parser_url.ts +11 -11
- package/src/lib/mdz_stream_state.svelte.ts +8 -8
- package/src/lib/mdz_to_svelte.ts +38 -46
- package/src/lib/mdz_token_parser.ts +55 -58
- package/src/lib/svelte_preprocess_mdz.ts +92 -89
- package/src/lib/tsdoc_mdz.ts +1 -1
|
@@ -8,8 +8,10 @@
|
|
|
8
8
|
* @module
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import type {MdzNodeTypeContainer, MdzNodeId, MdzOpcode} from './mdz_opcodes.ts';
|
|
12
|
-
import {
|
|
11
|
+
import type { MdzNodeTypeContainer, MdzNodeId, MdzOpcode } from './mdz_opcodes.ts';
|
|
12
|
+
import type { MdzTableCellParser } from './mdz.ts';
|
|
13
|
+
import { NEWLINE, has_non_whitespace, mdz_heading_id_from_text } from './mdz_helpers.ts';
|
|
14
|
+
import { mdz_debug_work } from './mdz_debug_work.ts';
|
|
13
15
|
|
|
14
16
|
/**
|
|
15
17
|
* Tri-state result for `try_*` parser handlers.
|
|
@@ -112,7 +114,7 @@ export interface MdzBlockquoteState {
|
|
|
112
114
|
* offsets — one per quote line; the line's content and terminating
|
|
113
115
|
* newline are contiguous in both coordinate spaces.
|
|
114
116
|
*/
|
|
115
|
-
segments: Array<{inner_start: number; source_start: number}>;
|
|
117
|
+
segments: Array<{ inner_start: number; source_start: number }>;
|
|
116
118
|
/** Total bytes fed to the inner document so far. */
|
|
117
119
|
inner_len: number;
|
|
118
120
|
/**
|
|
@@ -214,8 +216,9 @@ export interface MdzStreamParserState {
|
|
|
214
216
|
* is `null` for a top-level table (column-0 rows) or the enclosing list item's
|
|
215
217
|
* marker indent for a table nested as a block child (rows skip the indent and
|
|
216
218
|
* a dedent to that indent ends the table, returning control to the list).
|
|
219
|
+
* `cell_parser` is the table's shared cell parser, rebound per row.
|
|
217
220
|
*/
|
|
218
|
-
table: {id: MdzNodeId; item_indent: number | null} | null;
|
|
221
|
+
table: { id: MdzNodeId; item_indent: number | null; cell_parser: MdzTableCellParser } | null;
|
|
219
222
|
/**
|
|
220
223
|
* Whether the innermost open ListItem's first inline run is current.
|
|
221
224
|
* Content then flows directly into the ListItem frame — the sync AST
|
|
@@ -243,6 +246,32 @@ export interface MdzStreamParserState {
|
|
|
243
246
|
search_memo: Map<string, BufferSearchMemo> | null;
|
|
244
247
|
/** Open-container counts for `find_open`'s O(1) "nothing open" early exit. */
|
|
245
248
|
open_counts: OpenInlineCounts;
|
|
249
|
+
/**
|
|
250
|
+
* Count of open `Link`/`Element`/`Component` frames in the current run — the
|
|
251
|
+
* streaming analogue of the sync lexer's `#inline_depth`. Kept as an O(1)
|
|
252
|
+
* counter (not a stack walk, which would reintroduce the deep-stack
|
|
253
|
+
* quadratic `open_counts` exists to avoid) so `try_link_open`/`try_tag_open`
|
|
254
|
+
* can cap nesting at `MAX_INLINE_NESTING_DEPTH` — rendering a deeper `[`/`<`
|
|
255
|
+
* literal, matching the sync cap on pure/symmetric nesting. It counts
|
|
256
|
+
* **optimistic** opens, though, so it diverges from the sync `#inline_depth`
|
|
257
|
+
* (which the sync tag path only bumps after a closer precheck) on an
|
|
258
|
+
* asymmetric cap-saturating prefix — 100+ *unclosed* `<a>` before a valid
|
|
259
|
+
* `[x](/u)` saturates this counter and suppresses the trailing link that sync
|
|
260
|
+
* still forms. Adversarial-only, documented not fixed (streaming can't
|
|
261
|
+
* precheck closers): see the flip-at-cap case in `mdz_nesting_cap.test.ts`.
|
|
262
|
+
* Run-scoped for free like `open_counts`: a
|
|
263
|
+
* run close reverts every inline frame above it, decrementing back to zero.
|
|
264
|
+
*/
|
|
265
|
+
open_link_tag_depth: number;
|
|
266
|
+
/**
|
|
267
|
+
* Per-name counts of open Element/Component frames, the tag analogue of
|
|
268
|
+
* `open_counts`: `try_close_tag` bails in O(1) when the closing name isn't
|
|
269
|
+
* open, instead of walking a stack that grows one frame per leaked unclosed
|
|
270
|
+
* `<Tag>`. Lazily created on the first tag open — tag-free inputs never
|
|
271
|
+
* allocate the map. Run-scoped like `open_counts`: run closes revert every
|
|
272
|
+
* inline frame above them, decrementing the counts back to zero.
|
|
273
|
+
*/
|
|
274
|
+
open_tag_counts: Map<string, number> | null;
|
|
246
275
|
/** Whether we're inside a heading (newline ends it). */
|
|
247
276
|
in_heading: boolean;
|
|
248
277
|
/** Whether we're inside an optimistic inline Code container. */
|
|
@@ -314,7 +343,9 @@ export const create_state = (): MdzStreamParserState => ({
|
|
|
314
343
|
last_empty_item_end: 0,
|
|
315
344
|
base_offset: 0,
|
|
316
345
|
search_memo: null,
|
|
317
|
-
open_counts: {Bold: 0, Italic: 0, Strikethrough: 0, Link: 0},
|
|
346
|
+
open_counts: { Bold: 0, Italic: 0, Strikethrough: 0, Link: 0 },
|
|
347
|
+
open_link_tag_depth: 0,
|
|
348
|
+
open_tag_counts: null,
|
|
318
349
|
in_heading: false,
|
|
319
350
|
in_code: false,
|
|
320
351
|
in_paragraph: false,
|
|
@@ -326,7 +357,7 @@ export const create_state = (): MdzStreamParserState => ({
|
|
|
326
357
|
paragraph_stack_idx: -1,
|
|
327
358
|
last_text_id: null,
|
|
328
359
|
last_text_ended_with_newline: false,
|
|
329
|
-
last_text_was_singleton_newline: false
|
|
360
|
+
last_text_was_singleton_newline: false
|
|
330
361
|
});
|
|
331
362
|
|
|
332
363
|
/** @nodocs */
|
|
@@ -346,7 +377,7 @@ export const push_stack_entry = (
|
|
|
346
377
|
node_type: MdzNodeTypeContainer,
|
|
347
378
|
start: number,
|
|
348
379
|
delimiter: string = '',
|
|
349
|
-
tag_name?: string
|
|
380
|
+
tag_name?: string
|
|
350
381
|
): void => {
|
|
351
382
|
state.stack.push({
|
|
352
383
|
id,
|
|
@@ -355,13 +386,20 @@ export const push_stack_entry = (
|
|
|
355
386
|
tag_name,
|
|
356
387
|
has_children: false,
|
|
357
388
|
has_non_whitespace_content: false,
|
|
358
|
-
start
|
|
389
|
+
start
|
|
359
390
|
});
|
|
360
391
|
if (node_type === 'Paragraph') {
|
|
361
392
|
state.paragraph_stack_idx = state.stack.length - 1;
|
|
362
393
|
} else {
|
|
363
394
|
bump_open_count(state, node_type, 1);
|
|
364
395
|
}
|
|
396
|
+
if (tag_name !== undefined) {
|
|
397
|
+
const counts = (state.open_tag_counts ??= new Map());
|
|
398
|
+
counts.set(tag_name, (counts.get(tag_name) ?? 0) + 1);
|
|
399
|
+
}
|
|
400
|
+
// Link/Element/Component contribute to the nesting-depth cap (tag_name is
|
|
401
|
+
// set for Element/Component only, so `Link || tag_name` is exactly the three)
|
|
402
|
+
if (node_type === 'Link' || tag_name !== undefined) state.open_link_tag_depth++;
|
|
365
403
|
};
|
|
366
404
|
|
|
367
405
|
/**
|
|
@@ -374,7 +412,7 @@ export const push_stack_entry = (
|
|
|
374
412
|
const bump_open_count = (
|
|
375
413
|
state: MdzStreamParserState,
|
|
376
414
|
node_type: MdzNodeTypeContainer,
|
|
377
|
-
delta: number
|
|
415
|
+
delta: number
|
|
378
416
|
): void => {
|
|
379
417
|
const counts = state.open_counts;
|
|
380
418
|
if (node_type === 'Bold') counts.Bold += delta;
|
|
@@ -394,6 +432,16 @@ const bump_open_count = (
|
|
|
394
432
|
export const pop_stack_entry = (state: MdzStreamParserState): StackEntry => {
|
|
395
433
|
const entry = state.stack.pop()!;
|
|
396
434
|
bump_open_count(state, entry.node_type, -1);
|
|
435
|
+
if (entry.tag_name !== undefined) {
|
|
436
|
+
const counts = state.open_tag_counts!;
|
|
437
|
+
// drop zero-count keys so a long stream of many distinct tag names can't
|
|
438
|
+
// grow the map unboundedly — the `!counts.get(name)` bail in `try_close_tag`
|
|
439
|
+
// treats a missing key and a 0 count identically, so this is behavior-neutral
|
|
440
|
+
const remaining = counts.get(entry.tag_name)! - 1;
|
|
441
|
+
if (remaining === 0) counts.delete(entry.tag_name);
|
|
442
|
+
else counts.set(entry.tag_name, remaining);
|
|
443
|
+
}
|
|
444
|
+
if (entry.node_type === 'Link' || entry.tag_name !== undefined) state.open_link_tag_depth--;
|
|
397
445
|
return entry;
|
|
398
446
|
};
|
|
399
447
|
|
|
@@ -454,7 +502,7 @@ const memo_index_of = (
|
|
|
454
502
|
state: MdzStreamParserState,
|
|
455
503
|
memo: BufferSearchMemo,
|
|
456
504
|
needle: string,
|
|
457
|
-
from: number
|
|
505
|
+
from: number
|
|
458
506
|
): number => {
|
|
459
507
|
const global_from = state.base_offset + from;
|
|
460
508
|
const global_end = state.base_offset + state.buffer.length;
|
|
@@ -465,7 +513,11 @@ const memo_index_of = (
|
|
|
465
513
|
// rescan only the appended tail, overlapping the old boundary so a
|
|
466
514
|
// straddling needle isn't missed
|
|
467
515
|
const rescan_from = Math.max(global_from, memo.searched_to - (needle.length - 1));
|
|
468
|
-
const
|
|
516
|
+
const local_from = rescan_from - state.base_offset;
|
|
517
|
+
const result = state.buffer.indexOf(needle, local_from);
|
|
518
|
+
if (mdz_debug_work.enabled) {
|
|
519
|
+
mdz_debug_work.total += (result === -1 ? state.buffer.length : result) - local_from;
|
|
520
|
+
}
|
|
469
521
|
memo.from = global_from;
|
|
470
522
|
memo.result = result === -1 ? -1 : state.base_offset + result;
|
|
471
523
|
memo.searched_to = global_end;
|
|
@@ -474,6 +526,9 @@ const memo_index_of = (
|
|
|
474
526
|
// found result already behind `from` — fall through to a fresh scan
|
|
475
527
|
}
|
|
476
528
|
const result = state.buffer.indexOf(needle, from);
|
|
529
|
+
if (mdz_debug_work.enabled) {
|
|
530
|
+
mdz_debug_work.total += (result === -1 ? state.buffer.length : result) - from;
|
|
531
|
+
}
|
|
477
532
|
memo.from = global_from;
|
|
478
533
|
memo.result = result === -1 ? -1 : state.base_offset + result;
|
|
479
534
|
memo.searched_to = global_end;
|
|
@@ -493,13 +548,13 @@ const memo_index_of = (
|
|
|
493
548
|
export const buffer_index_of = (
|
|
494
549
|
state: MdzStreamParserState,
|
|
495
550
|
needle: string,
|
|
496
|
-
from: number
|
|
551
|
+
from: number
|
|
497
552
|
): number => {
|
|
498
553
|
const map = (state.search_memo ??= new Map());
|
|
499
554
|
let memo = map.get(needle);
|
|
500
555
|
if (memo === undefined) {
|
|
501
556
|
// "scanned [0, 0), found nothing" — memo_index_of handles the full scan
|
|
502
|
-
memo = {from: 0, result: -1, searched_to: 0};
|
|
557
|
+
memo = { from: 0, result: -1, searched_to: 0 };
|
|
503
558
|
map.set(needle, memo);
|
|
504
559
|
}
|
|
505
560
|
return memo_index_of(state, memo, needle, from);
|
|
@@ -519,8 +574,13 @@ export const emit = (state: MdzStreamParserState, op: MdzOpcode): void => {
|
|
|
519
574
|
if (top) {
|
|
520
575
|
top.has_children = true;
|
|
521
576
|
// propagate non-whitespace marker to the nearest Paragraph so
|
|
522
|
-
// whitespace-only paragraphs can be discarded at close
|
|
523
|
-
|
|
577
|
+
// whitespace-only paragraphs can be discarded at close — the O(1)
|
|
578
|
+
// no-paragraph check runs first so codeblock/table-cell emissions
|
|
579
|
+
// (never inside a Paragraph) skip the content scan entirely
|
|
580
|
+
if (
|
|
581
|
+
op.type === 'void' ||
|
|
582
|
+
(state.paragraph_stack_idx !== -1 && has_non_whitespace(op.content))
|
|
583
|
+
) {
|
|
524
584
|
mark_paragraph_non_whitespace(state);
|
|
525
585
|
}
|
|
526
586
|
}
|
|
@@ -582,7 +642,7 @@ export const emit = (state: MdzStreamParserState, op: MdzOpcode): void => {
|
|
|
582
642
|
export const accumulate_text = (
|
|
583
643
|
state: MdzStreamParserState,
|
|
584
644
|
text: string,
|
|
585
|
-
start_offset: number
|
|
645
|
+
start_offset: number
|
|
586
646
|
): void => {
|
|
587
647
|
if (state.accumulated_text.length === 0) {
|
|
588
648
|
state.accumulated_text_start = start_offset;
|
|
@@ -618,7 +678,7 @@ export const flush_text = (state: MdzStreamParserState): void => {
|
|
|
618
678
|
type: 'append_text',
|
|
619
679
|
id: state.active_text_id,
|
|
620
680
|
content: state.accumulated_text,
|
|
621
|
-
end: state.accumulated_text_end
|
|
681
|
+
end: state.accumulated_text_end
|
|
622
682
|
});
|
|
623
683
|
} else {
|
|
624
684
|
ensure_paragraph(state);
|
|
@@ -630,7 +690,7 @@ export const flush_text = (state: MdzStreamParserState): void => {
|
|
|
630
690
|
content: state.accumulated_text,
|
|
631
691
|
text_type: 'Text',
|
|
632
692
|
start,
|
|
633
|
-
end: state.accumulated_text_end
|
|
693
|
+
end: state.accumulated_text_end
|
|
634
694
|
});
|
|
635
695
|
state.active_text_id = id;
|
|
636
696
|
}
|
|
@@ -649,7 +709,7 @@ export const ensure_paragraph = (state: MdzStreamParserState): void => {
|
|
|
649
709
|
if (state.in_heading || state.in_paragraph || state.in_list_item_run) return;
|
|
650
710
|
const id = alloc_id(state);
|
|
651
711
|
const start = offset(state);
|
|
652
|
-
emit(state, {type: 'open', id, node_type: 'Paragraph', start});
|
|
712
|
+
emit(state, { type: 'open', id, node_type: 'Paragraph', start });
|
|
653
713
|
push_stack_entry(state, id, 'Paragraph', start);
|
|
654
714
|
state.in_paragraph = true;
|
|
655
715
|
};
|
|
@@ -709,7 +769,7 @@ export const revert_above = (state: MdzStreamParserState, target_idx: number): v
|
|
|
709
769
|
type: 'revert',
|
|
710
770
|
id: entry.id,
|
|
711
771
|
replacement_text: entry.delimiter,
|
|
712
|
-
start: entry.start
|
|
772
|
+
start: entry.start
|
|
713
773
|
});
|
|
714
774
|
}
|
|
715
775
|
state.active_text_id = null;
|
|
@@ -744,11 +804,23 @@ export const revert_failed_close = (state: MdzStreamParserState, stack_idx: numb
|
|
|
744
804
|
const frames_above = state.stack.length - 1 - stack_idx;
|
|
745
805
|
state.stack.splice(stack_idx, 1);
|
|
746
806
|
bump_open_count(state, entry.node_type, -1);
|
|
807
|
+
if (entry.tag_name !== undefined) {
|
|
808
|
+
const counts = state.open_tag_counts!;
|
|
809
|
+
// drop zero-count keys so a long stream of many distinct tag names can't
|
|
810
|
+
// grow the map unboundedly — the `!counts.get(name)` bail in `try_close_tag`
|
|
811
|
+
// treats a missing key and a 0 count identically, so this is behavior-neutral
|
|
812
|
+
const remaining = counts.get(entry.tag_name)! - 1;
|
|
813
|
+
if (remaining === 0) counts.delete(entry.tag_name);
|
|
814
|
+
else counts.set(entry.tag_name, remaining);
|
|
815
|
+
}
|
|
816
|
+
// only ever reverts an Italic today, but keep the depth counter honest if a
|
|
817
|
+
// Link/Element/Component is ever failed-closed mid-stack
|
|
818
|
+
if (entry.node_type === 'Link' || entry.tag_name !== undefined) state.open_link_tag_depth--;
|
|
747
819
|
state.opcodes.push({
|
|
748
820
|
type: 'revert',
|
|
749
821
|
id: entry.id,
|
|
750
822
|
replacement_text: entry.delimiter,
|
|
751
|
-
start: entry.start
|
|
823
|
+
start: entry.start
|
|
752
824
|
});
|
|
753
825
|
// structural opcode seals the prior text run (mirrors `emit`)
|
|
754
826
|
seal_text_run(state);
|
|
@@ -780,7 +852,7 @@ export const trim_trailing_newline = (state: MdzStreamParserState): void => {
|
|
|
780
852
|
}
|
|
781
853
|
if (state.last_text_id === null || !state.last_text_ended_with_newline) return;
|
|
782
854
|
// emit trim opcode; consumer adjusts content and removes empty nodes
|
|
783
|
-
emit(state, {type: 'trim_text', id: state.last_text_id, count: 1});
|
|
855
|
+
emit(state, { type: 'trim_text', id: state.last_text_id, count: 1 });
|
|
784
856
|
// if this trim empties a singleton text node, clear active_text_id so
|
|
785
857
|
// subsequent content doesn't merge into a deleted node via append_text
|
|
786
858
|
if (state.last_text_was_singleton_newline) {
|
|
@@ -807,8 +879,8 @@ export const close_paragraph = (state: MdzStreamParserState): void => {
|
|
|
807
879
|
emit(
|
|
808
880
|
state,
|
|
809
881
|
discard
|
|
810
|
-
? {type: 'close', id: entry.id, end: offset(state), discard: true}
|
|
811
|
-
: {type: 'close', id: entry.id, end: offset(state)}
|
|
882
|
+
? { type: 'close', id: entry.id, end: offset(state), discard: true }
|
|
883
|
+
: { type: 'close', id: entry.id, end: offset(state) }
|
|
812
884
|
);
|
|
813
885
|
state.active_text_id = null;
|
|
814
886
|
state.in_paragraph = false;
|
|
@@ -841,7 +913,7 @@ export const close_heading = (state: MdzStreamParserState): void => {
|
|
|
841
913
|
const heading_id = mdz_heading_id_from_text(heading_text);
|
|
842
914
|
// the heading is now the top of the stack
|
|
843
915
|
const entry = pop_stack_entry(state);
|
|
844
|
-
emit(state, {type: 'close', id: entry.id, end: offset(state), heading_id});
|
|
916
|
+
emit(state, { type: 'close', id: entry.id, end: offset(state), heading_id });
|
|
845
917
|
state.active_text_id = null;
|
|
846
918
|
};
|
|
847
919
|
|
|
@@ -861,7 +933,7 @@ export const close_codeblock_at_eof = (state: MdzStreamParserState): void => {
|
|
|
861
933
|
if (state.codeblock.fence_indent === 0) {
|
|
862
934
|
trim_trailing_newline(state);
|
|
863
935
|
}
|
|
864
|
-
emit(state, {type: 'close', id: state.codeblock.id, end: offset(state)});
|
|
936
|
+
emit(state, { type: 'close', id: state.codeblock.id, end: offset(state) });
|
|
865
937
|
state.codeblock = null;
|
|
866
938
|
};
|
|
867
939
|
|
|
@@ -915,7 +987,7 @@ export const close_list_item = (state: MdzStreamParserState): void => {
|
|
|
915
987
|
emit(state, {
|
|
916
988
|
type: 'close',
|
|
917
989
|
id: entry.id,
|
|
918
|
-
end: entry.has_children ? offset(state) : state.last_empty_item_end
|
|
990
|
+
end: entry.has_children ? offset(state) : state.last_empty_item_end
|
|
919
991
|
});
|
|
920
992
|
state.active_text_id = null;
|
|
921
993
|
};
|
|
@@ -928,7 +1000,7 @@ export const close_list_item = (state: MdzStreamParserState): void => {
|
|
|
928
1000
|
export const close_list_level = (state: MdzStreamParserState): void => {
|
|
929
1001
|
close_list_item(state);
|
|
930
1002
|
const entry = pop_stack_entry(state);
|
|
931
|
-
emit(state, {type: 'close', id: entry.id, end: offset(state)});
|
|
1003
|
+
emit(state, { type: 'close', id: entry.id, end: offset(state) });
|
|
932
1004
|
state.list_levels.pop();
|
|
933
1005
|
};
|
|
934
1006
|
|
|
@@ -7,22 +7,23 @@
|
|
|
7
7
|
* whole header emits at once. Body rows then stream one per line. Because a row
|
|
8
8
|
* is always fully buffered before it emits (no within-row streaming), each
|
|
9
9
|
* cell's inline content is parsed with the sync reference (`MdzTableCellParser`,
|
|
10
|
-
* one `MdzLexer`/`MdzTokenParser` shared across the
|
|
11
|
-
* as opcodes — guaranteeing sync↔stream parity
|
|
10
|
+
* one `MdzLexer`/`MdzTokenParser` shared across the whole table's cells,
|
|
11
|
+
* rebound per row) and replayed as opcodes — guaranteeing sync↔stream parity
|
|
12
|
+
* by construction.
|
|
12
13
|
*
|
|
13
14
|
* @module
|
|
14
15
|
*/
|
|
15
16
|
|
|
16
|
-
import {DEV} from 'esm-env';
|
|
17
|
+
import { DEV } from 'esm-env';
|
|
17
18
|
|
|
18
19
|
import {
|
|
19
20
|
NEWLINE,
|
|
20
21
|
PIPE,
|
|
21
22
|
is_line_whitespace,
|
|
22
23
|
mdz_split_table_row,
|
|
23
|
-
mdz_parse_table_delimiter
|
|
24
|
+
mdz_parse_table_delimiter
|
|
24
25
|
} from './mdz_helpers.ts';
|
|
25
|
-
import {MdzTableCellParser, type MdzNode, type MdzTableAlign} from './mdz.ts';
|
|
26
|
+
import { MdzTableCellParser, type MdzNode, type MdzTableAlign } from './mdz.ts';
|
|
26
27
|
import {
|
|
27
28
|
type MdzStreamParserState,
|
|
28
29
|
type TryResult,
|
|
@@ -32,7 +33,7 @@ import {
|
|
|
32
33
|
flush_text,
|
|
33
34
|
offset,
|
|
34
35
|
pop_stack_entry,
|
|
35
|
-
push_stack_entry
|
|
36
|
+
push_stack_entry
|
|
36
37
|
} from './mdz_stream_parser_state.ts';
|
|
37
38
|
|
|
38
39
|
/**
|
|
@@ -70,9 +71,13 @@ export const try_table_start = (state: MdzStreamParserState, forced: boolean): T
|
|
|
70
71
|
|
|
71
72
|
const table_id = alloc_id(state);
|
|
72
73
|
const table_start = offset(state, start);
|
|
73
|
-
emit(state, {type: 'open', id: table_id, node_type: 'Table', start: table_start, align});
|
|
74
|
+
emit(state, { type: 'open', id: table_id, node_type: 'Table', start: table_start, align });
|
|
74
75
|
push_stack_entry(state, table_id, 'Table', table_start);
|
|
75
|
-
state.table = {
|
|
76
|
+
state.table = {
|
|
77
|
+
id: table_id,
|
|
78
|
+
item_indent: null,
|
|
79
|
+
cell_parser: new MdzTableCellParser(state.buffer)
|
|
80
|
+
};
|
|
76
81
|
|
|
77
82
|
emit_table_row(state, header_cells, true, start, header_end);
|
|
78
83
|
|
|
@@ -147,7 +152,7 @@ export const process_table_line = (state: MdzStreamParserState, forced: boolean)
|
|
|
147
152
|
const process_table_line_in_item = (
|
|
148
153
|
state: MdzStreamParserState,
|
|
149
154
|
forced: boolean,
|
|
150
|
-
item_indent: number
|
|
155
|
+
item_indent: number
|
|
151
156
|
): boolean => {
|
|
152
157
|
if (state.pos >= state.buffer.length) {
|
|
153
158
|
if (forced) {
|
|
@@ -199,9 +204,9 @@ const process_table_line_in_item = (
|
|
|
199
204
|
*/
|
|
200
205
|
export const close_table = (state: MdzStreamParserState): void => {
|
|
201
206
|
if (!state.table) return;
|
|
202
|
-
const {item_indent} = state.table;
|
|
207
|
+
const { item_indent } = state.table;
|
|
203
208
|
const entry = pop_stack_entry(state);
|
|
204
|
-
emit(state, {type: 'close', id: entry.id, end: offset(state)});
|
|
209
|
+
emit(state, { type: 'close', id: entry.id, end: offset(state) });
|
|
205
210
|
state.table = null;
|
|
206
211
|
state.active_text_id = null;
|
|
207
212
|
if (item_indent === null) {
|
|
@@ -215,7 +220,7 @@ export const close_table = (state: MdzStreamParserState): void => {
|
|
|
215
220
|
|
|
216
221
|
/** A recognized in-item table head: the header row's cells + the delimiter alignment. */
|
|
217
222
|
export interface TableInItemHead {
|
|
218
|
-
header_cells: Array<{start: number; end: number}>;
|
|
223
|
+
header_cells: Array<{ start: number; end: number }>;
|
|
219
224
|
header_end: number;
|
|
220
225
|
align: Array<MdzTableAlign>;
|
|
221
226
|
delim_end: number;
|
|
@@ -234,9 +239,9 @@ export const match_table_in_item_head = (
|
|
|
234
239
|
state: MdzStreamParserState,
|
|
235
240
|
header_first: number,
|
|
236
241
|
marker_indent: number,
|
|
237
|
-
forced: boolean
|
|
242
|
+
forced: boolean
|
|
238
243
|
): TableInItemHead | 'pending' | null => {
|
|
239
|
-
const {buffer} = state;
|
|
244
|
+
const { buffer } = state;
|
|
240
245
|
const header_end = buffer.indexOf('\n', header_first);
|
|
241
246
|
if (header_end === -1) return forced ? null : 'pending'; // header needs its newline + a delimiter beneath
|
|
242
247
|
const header_cells = mdz_split_table_row(buffer, header_first, header_end);
|
|
@@ -253,7 +258,7 @@ export const match_table_in_item_head = (
|
|
|
253
258
|
}
|
|
254
259
|
const align = mdz_parse_table_delimiter(buffer, dj, delim_end);
|
|
255
260
|
if (align === null || align.length !== header_cells.length) return null;
|
|
256
|
-
return {header_cells, header_end, align, delim_end};
|
|
261
|
+
return { header_cells, header_end, align, delim_end };
|
|
257
262
|
};
|
|
258
263
|
|
|
259
264
|
/**
|
|
@@ -268,7 +273,7 @@ export const open_table_in_item = (
|
|
|
268
273
|
state: MdzStreamParserState,
|
|
269
274
|
header_first: number,
|
|
270
275
|
marker_indent: number,
|
|
271
|
-
head: TableInItemHead
|
|
276
|
+
head: TableInItemHead
|
|
272
277
|
): void => {
|
|
273
278
|
const table_id = alloc_id(state);
|
|
274
279
|
const table_start = offset(state, header_first);
|
|
@@ -277,10 +282,14 @@ export const open_table_in_item = (
|
|
|
277
282
|
id: table_id,
|
|
278
283
|
node_type: 'Table',
|
|
279
284
|
start: table_start,
|
|
280
|
-
align: head.align
|
|
285
|
+
align: head.align
|
|
281
286
|
});
|
|
282
287
|
push_stack_entry(state, table_id, 'Table', table_start);
|
|
283
|
-
state.table = {
|
|
288
|
+
state.table = {
|
|
289
|
+
id: table_id,
|
|
290
|
+
item_indent: marker_indent,
|
|
291
|
+
cell_parser: new MdzTableCellParser(state.buffer)
|
|
292
|
+
};
|
|
284
293
|
emit_table_row(state, head.header_cells, true, header_first, head.header_end);
|
|
285
294
|
// leave the cursor on the delimiter's newline (or EOF) — the in-item body-row
|
|
286
295
|
// handler reads from the line after it and hands back on this same convention
|
|
@@ -292,23 +301,25 @@ export const open_table_in_item = (
|
|
|
292
301
|
/** Emit one table row: open, each cell, close. */
|
|
293
302
|
const emit_table_row = (
|
|
294
303
|
state: MdzStreamParserState,
|
|
295
|
-
cells: Array<{start: number; end: number}>,
|
|
304
|
+
cells: Array<{ start: number; end: number }>,
|
|
296
305
|
header: boolean,
|
|
297
306
|
row_start: number,
|
|
298
|
-
row_end: number
|
|
307
|
+
row_end: number
|
|
299
308
|
): void => {
|
|
300
309
|
const row_id = alloc_id(state);
|
|
301
310
|
const row_off = offset(state, row_start);
|
|
302
|
-
emit(state, {type: 'open', id: row_id, node_type: 'TableRow', start: row_off, header});
|
|
311
|
+
emit(state, { type: 'open', id: row_id, node_type: 'TableRow', start: row_off, header });
|
|
303
312
|
push_stack_entry(state, row_id, 'TableRow', row_off);
|
|
304
|
-
// one lexer/parser shared across the
|
|
305
|
-
//
|
|
306
|
-
|
|
313
|
+
// one lexer/parser shared across the whole table rather than allocated per
|
|
314
|
+
// row (or per cell) — rebound per row because the buffer string changes
|
|
315
|
+
// across feeds; the lexer's search memo survives when it doesn't
|
|
316
|
+
const cell_parser = state.table!.cell_parser;
|
|
317
|
+
cell_parser.rebind(state.buffer);
|
|
307
318
|
for (const cell of cells) {
|
|
308
319
|
emit_table_cell(state, cell_parser, cell.start, cell.end);
|
|
309
320
|
}
|
|
310
321
|
pop_stack_entry(state);
|
|
311
|
-
emit(state, {type: 'close', id: row_id, end: offset(state, row_end)});
|
|
322
|
+
emit(state, { type: 'close', id: row_id, end: offset(state, row_end) });
|
|
312
323
|
state.active_text_id = null;
|
|
313
324
|
};
|
|
314
325
|
|
|
@@ -320,15 +331,15 @@ const emit_table_cell = (
|
|
|
320
331
|
state: MdzStreamParserState,
|
|
321
332
|
cell_parser: MdzTableCellParser,
|
|
322
333
|
cell_start: number,
|
|
323
|
-
cell_end: number
|
|
334
|
+
cell_end: number
|
|
324
335
|
): void => {
|
|
325
336
|
const cell_id = alloc_id(state);
|
|
326
337
|
const cell_off = offset(state, cell_start);
|
|
327
|
-
emit(state, {type: 'open', id: cell_id, node_type: 'TableCell', start: cell_off});
|
|
338
|
+
emit(state, { type: 'open', id: cell_id, node_type: 'TableCell', start: cell_off });
|
|
328
339
|
push_stack_entry(state, cell_id, 'TableCell', cell_off);
|
|
329
340
|
emit_inline_nodes(state, cell_parser.parse(cell_start, cell_end));
|
|
330
341
|
pop_stack_entry(state);
|
|
331
|
-
emit(state, {type: 'close', id: cell_id, end: offset(state, cell_end)});
|
|
342
|
+
emit(state, { type: 'close', id: cell_id, end: offset(state, cell_end) });
|
|
332
343
|
state.active_text_id = null;
|
|
333
344
|
};
|
|
334
345
|
|
|
@@ -346,7 +357,7 @@ const emit_inline_nodes = (state: MdzStreamParserState, nodes: Array<MdzNode>):
|
|
|
346
357
|
content: node.content,
|
|
347
358
|
text_type: 'Text',
|
|
348
359
|
start: offset(state, node.start),
|
|
349
|
-
end: offset(state, node.end)
|
|
360
|
+
end: offset(state, node.end)
|
|
350
361
|
});
|
|
351
362
|
break;
|
|
352
363
|
case 'Code':
|
|
@@ -356,7 +367,7 @@ const emit_inline_nodes = (state: MdzStreamParserState, nodes: Array<MdzNode>):
|
|
|
356
367
|
content: node.content,
|
|
357
368
|
text_type: 'Code',
|
|
358
369
|
start: offset(state, node.start),
|
|
359
|
-
end: offset(state, node.end)
|
|
370
|
+
end: offset(state, node.end)
|
|
360
371
|
});
|
|
361
372
|
break;
|
|
362
373
|
case 'Bold':
|
|
@@ -364,17 +375,17 @@ const emit_inline_nodes = (state: MdzStreamParserState, nodes: Array<MdzNode>):
|
|
|
364
375
|
case 'Strikethrough': {
|
|
365
376
|
const id = alloc_id(state);
|
|
366
377
|
const s = offset(state, node.start);
|
|
367
|
-
emit(state, {type: 'open', id, node_type: node.type, start: s});
|
|
378
|
+
emit(state, { type: 'open', id, node_type: node.type, start: s });
|
|
368
379
|
push_stack_entry(state, id, node.type, s);
|
|
369
380
|
emit_inline_nodes(state, node.children);
|
|
370
381
|
pop_stack_entry(state);
|
|
371
|
-
emit(state, {type: 'close', id, end: offset(state, node.end)});
|
|
382
|
+
emit(state, { type: 'close', id, end: offset(state, node.end) });
|
|
372
383
|
break;
|
|
373
384
|
}
|
|
374
385
|
case 'Link': {
|
|
375
386
|
const id = alloc_id(state);
|
|
376
387
|
const s = offset(state, node.start);
|
|
377
|
-
emit(state, {type: 'open', id, node_type: 'Link', start: s});
|
|
388
|
+
emit(state, { type: 'open', id, node_type: 'Link', start: s });
|
|
378
389
|
push_stack_entry(state, id, 'Link', s);
|
|
379
390
|
emit_inline_nodes(state, node.children);
|
|
380
391
|
pop_stack_entry(state);
|
|
@@ -383,7 +394,7 @@ const emit_inline_nodes = (state: MdzStreamParserState, nodes: Array<MdzNode>):
|
|
|
383
394
|
id,
|
|
384
395
|
end: offset(state, node.end),
|
|
385
396
|
reference: node.reference,
|
|
386
|
-
link_type: node.link_type
|
|
397
|
+
link_type: node.link_type
|
|
387
398
|
});
|
|
388
399
|
break;
|
|
389
400
|
}
|
|
@@ -391,11 +402,11 @@ const emit_inline_nodes = (state: MdzStreamParserState, nodes: Array<MdzNode>):
|
|
|
391
402
|
case 'Component': {
|
|
392
403
|
const id = alloc_id(state);
|
|
393
404
|
const s = offset(state, node.start);
|
|
394
|
-
emit(state, {type: 'open', id, node_type: node.type, start: s, name: node.name});
|
|
405
|
+
emit(state, { type: 'open', id, node_type: node.type, start: s, name: node.name });
|
|
395
406
|
push_stack_entry(state, id, node.type, s, '', node.name);
|
|
396
407
|
emit_inline_nodes(state, node.children);
|
|
397
408
|
pop_stack_entry(state);
|
|
398
|
-
emit(state, {type: 'close', id, end: offset(state, node.end)});
|
|
409
|
+
emit(state, { type: 'close', id, end: offset(state, node.end) });
|
|
399
410
|
break;
|
|
400
411
|
}
|
|
401
412
|
// cells are inline-only (lexed via `lex_table_cell`); a block node here
|
|
@@ -5,30 +5,25 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import {
|
|
8
|
-
ASTERISK,
|
|
9
|
-
BACKTICK,
|
|
10
8
|
HTTPS_PREFIX_LENGTH,
|
|
11
|
-
H_LOWER,
|
|
12
|
-
H_UPPER,
|
|
13
|
-
LEFT_ANGLE,
|
|
14
|
-
LEFT_BRACKET,
|
|
15
9
|
NEWLINE,
|
|
16
|
-
PERIOD,
|
|
17
|
-
RIGHT_BRACKET,
|
|
18
|
-
SLASH,
|
|
19
10
|
SPACE,
|
|
20
11
|
TAB,
|
|
21
|
-
|
|
22
|
-
|
|
12
|
+
TEXT_CLASS_NEWLINE,
|
|
13
|
+
TEXT_CLASS_PATH,
|
|
14
|
+
TEXT_CLASS_STOP,
|
|
15
|
+
TEXT_CLASS_URL,
|
|
16
|
+
TEXT_SCAN_CLASS,
|
|
23
17
|
is_word_char,
|
|
24
|
-
match_url_prefix_case_insensitive
|
|
18
|
+
match_url_prefix_case_insensitive
|
|
25
19
|
} from './mdz_helpers.ts';
|
|
26
20
|
import {
|
|
27
21
|
type MdzStreamParserState,
|
|
28
22
|
accumulate_text,
|
|
29
23
|
ensure_paragraph,
|
|
30
|
-
offset
|
|
24
|
+
offset
|
|
31
25
|
} from './mdz_stream_parser_state.ts';
|
|
26
|
+
import { mdz_debug_work } from './mdz_debug_work.ts';
|
|
32
27
|
|
|
33
28
|
/**
|
|
34
29
|
* Consume a delimiter at the current position as literal text — the shared
|
|
@@ -60,35 +55,41 @@ export const consume_text_run = (state: MdzStreamParserState): void => {
|
|
|
60
55
|
state.pos++;
|
|
61
56
|
while (state.pos < state.buffer.length) {
|
|
62
57
|
const c = state.buffer.charCodeAt(state.pos);
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
// URL/path: only break when actually at a URL or path start.
|
|
73
|
-
// Most h/H/./slash in prose are not URLs/paths — continuing the scan
|
|
74
|
-
// avoids the full dispatch cycle through process_loop → process_inline.
|
|
75
|
-
// For `h`/`H` at a word boundary, also break when there isn't enough
|
|
76
|
-
// buffer left to confirm `https://` / `http://` — process_inline's
|
|
77
|
-
// speculator then carries the prefix match across chunk boundaries.
|
|
78
|
-
// Scheme matching is case-insensitive (RFC 3986).
|
|
79
|
-
((c === H_LOWER || c === H_UPPER) &&
|
|
80
|
-
(match_url_prefix_case_insensitive(state.buffer, state.pos) > 0 ||
|
|
81
|
-
(state.buffer.length - state.pos < HTTPS_PREFIX_LENGTH &&
|
|
82
|
-
!is_word_char(state.buffer.charCodeAt(state.pos - 1))))) ||
|
|
83
|
-
((c === SLASH || c === PERIOD) &&
|
|
84
|
-
(state.buffer.charCodeAt(state.pos - 1) === SPACE ||
|
|
85
|
-
state.buffer.charCodeAt(state.pos - 1) === NEWLINE ||
|
|
86
|
-
state.buffer.charCodeAt(state.pos - 1) === TAB))
|
|
87
|
-
) {
|
|
88
|
-
break;
|
|
58
|
+
// one table load classifies the char; zero (the common case) continues
|
|
59
|
+
// the run without any further checks. `TEXT_SCAN_CLASS` is shared with
|
|
60
|
+
// the sync lexer's `#tokenize_text` (`mdz_lexer.ts`); the dispatch bodies
|
|
61
|
+
// intentionally differ — that one handles `\|` escapes and list-run
|
|
62
|
+
// strip, this one URL speculation across chunk boundaries.
|
|
63
|
+
const char_class = c < 128 ? TEXT_SCAN_CLASS[c]! : 0;
|
|
64
|
+
if (char_class === 0) {
|
|
65
|
+
state.pos++;
|
|
66
|
+
continue;
|
|
89
67
|
}
|
|
68
|
+
if ((char_class & (TEXT_CLASS_STOP | TEXT_CLASS_NEWLINE)) !== 0) break;
|
|
69
|
+
if ((char_class & TEXT_CLASS_URL) !== 0) {
|
|
70
|
+
// URL: only break when actually at a URL start. Most h/H in prose are
|
|
71
|
+
// not URLs — continuing the scan avoids the full dispatch cycle
|
|
72
|
+
// through process_loop → process_inline. At a word boundary, also
|
|
73
|
+
// break when there isn't enough buffer left to confirm `https://` /
|
|
74
|
+
// `http://` — process_inline's speculator then carries the prefix
|
|
75
|
+
// match across chunk boundaries. Scheme matching is case-insensitive
|
|
76
|
+
// (RFC 3986).
|
|
77
|
+
if (
|
|
78
|
+
match_url_prefix_case_insensitive(state.buffer, state.pos) > 0 ||
|
|
79
|
+
(state.buffer.length - state.pos < HTTPS_PREFIX_LENGTH &&
|
|
80
|
+
!is_word_char(state.buffer.charCodeAt(state.pos - 1)))
|
|
81
|
+
) {
|
|
82
|
+
break;
|
|
83
|
+
}
|
|
84
|
+
} else if ((char_class & TEXT_CLASS_PATH) !== 0) {
|
|
85
|
+
// path (`/` or `.`): only break at a plausible path start
|
|
86
|
+
const prev = state.buffer.charCodeAt(state.pos - 1);
|
|
87
|
+
if (prev === SPACE || prev === NEWLINE || prev === TAB) break;
|
|
88
|
+
}
|
|
89
|
+
// backslash (or a non-starting path char) — ordinary text
|
|
90
90
|
state.pos++;
|
|
91
91
|
}
|
|
92
|
+
if (mdz_debug_work.enabled) mdz_debug_work.total += state.pos - start;
|
|
92
93
|
accumulate_text(state, state.buffer.slice(start, state.pos), offset(state, start));
|
|
93
94
|
state.prev_char = state.buffer.charCodeAt(state.pos - 1);
|
|
94
95
|
state.column += state.pos - start;
|