@fuzdev/mdz 0.9.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/Mdz.svelte +37 -1
- package/dist/Mdz.svelte.d.ts +35 -0
- package/dist/Mdz.svelte.d.ts.map +1 -1
- package/dist/MdzNodeView.svelte +143 -27
- package/dist/MdzNodeView.svelte.d.ts +13 -1
- package/dist/MdzNodeView.svelte.d.ts.map +1 -1
- package/dist/MdzPrecompiled.svelte.d.ts.map +1 -1
- package/dist/MdzRoot.svelte.d.ts.map +1 -1
- package/dist/MdzStream.svelte.d.ts.map +1 -1
- package/dist/MdzStreamNodeView.svelte.d.ts.map +1 -1
- package/dist/context_helpers.d.ts.map +1 -1
- package/dist/mdz.d.ts +4 -1
- package/dist/mdz.d.ts.map +1 -1
- package/dist/mdz.js +4 -1
- package/dist/mdz_contexts.d.ts +14 -2
- package/dist/mdz_contexts.d.ts.map +1 -1
- package/dist/mdz_debug_work.d.ts.map +1 -1
- package/dist/mdz_helpers.d.ts +57 -7
- package/dist/mdz_helpers.d.ts.map +1 -1
- package/dist/mdz_helpers.js +113 -50
- package/dist/mdz_lexer.d.ts.map +1 -1
- package/dist/mdz_lexer.js +21 -17
- package/dist/mdz_opcodes.d.ts.map +1 -1
- package/dist/mdz_opcodes_to_nodes.d.ts.map +1 -1
- package/dist/mdz_stream_parser.d.ts +16 -3
- package/dist/mdz_stream_parser.d.ts.map +1 -1
- package/dist/mdz_stream_parser.js +16 -3
- package/dist/mdz_stream_parser_block.d.ts +2 -2
- package/dist/mdz_stream_parser_block.d.ts.map +1 -1
- package/dist/mdz_stream_parser_block.js +2 -2
- package/dist/mdz_stream_parser_inline.d.ts.map +1 -1
- package/dist/mdz_stream_parser_inline.js +4 -4
- package/dist/mdz_stream_parser_link.d.ts.map +1 -1
- package/dist/mdz_stream_parser_list.d.ts.map +1 -1
- package/dist/mdz_stream_parser_state.d.ts +32 -12
- package/dist/mdz_stream_parser_state.d.ts.map +1 -1
- package/dist/mdz_stream_parser_state.js +21 -14
- package/dist/mdz_stream_parser_table.d.ts.map +1 -1
- package/dist/mdz_stream_parser_text.d.ts.map +1 -1
- package/dist/mdz_stream_parser_text.js +7 -4
- package/dist/mdz_stream_parser_url.d.ts +3 -2
- package/dist/mdz_stream_parser_url.d.ts.map +1 -1
- package/dist/mdz_stream_parser_url.js +114 -42
- package/dist/mdz_stream_state.svelte.d.ts.map +1 -1
- package/dist/mdz_to_svelte.d.ts.map +1 -1
- package/dist/mdz_token_parser.d.ts.map +1 -1
- package/dist/svelte_preprocess_mdz.d.ts.map +1 -1
- package/dist/svelte_preprocess_mdz.js +8 -0
- package/dist/tsdoc_mdz.d.ts +8 -4
- package/dist/tsdoc_mdz.d.ts.map +1 -1
- package/dist/tsdoc_mdz.js +14 -4
- package/package.json +5 -5
- package/src/lib/mdz.ts +4 -1
- package/src/lib/mdz_contexts.ts +20 -2
- package/src/lib/mdz_helpers.ts +117 -49
- package/src/lib/mdz_lexer.ts +31 -14
- package/src/lib/mdz_stream_parser.ts +16 -3
- package/src/lib/mdz_stream_parser_block.ts +2 -2
- package/src/lib/mdz_stream_parser_inline.ts +4 -4
- package/src/lib/mdz_stream_parser_state.ts +34 -13
- package/src/lib/mdz_stream_parser_text.ts +13 -6
- package/src/lib/mdz_stream_parser_url.ts +130 -49
- package/src/lib/svelte_preprocess_mdz.ts +8 -0
- package/src/lib/tsdoc_mdz.ts +15 -4
package/src/lib/mdz_helpers.ts
CHANGED
|
@@ -528,6 +528,18 @@ export const is_word_char = (char_code: number): boolean =>
|
|
|
528
528
|
(char_code >= A_LOWER && char_code <= Z_LOWER) ||
|
|
529
529
|
(char_code >= ZERO && char_code <= NINE);
|
|
530
530
|
|
|
531
|
+
/**
|
|
532
|
+
* Whether a char next to an italic `_` blocks it from opening (the char
|
|
533
|
+
* before) or closing (the char after): a word char, or `_` itself — italic is
|
|
534
|
+
* self-opaque, so runs like `__init__` never pair. The one italic boundary
|
|
535
|
+
* predicate, shared by the lexer, the streaming parser, and the streaming
|
|
536
|
+
* auto-link span resolution.
|
|
537
|
+
*
|
|
538
|
+
* @nodocs
|
|
539
|
+
*/
|
|
540
|
+
export const is_italic_boundary_blocker = (char_code: number): boolean =>
|
|
541
|
+
is_word_char(char_code) || char_code === UNDERSCORE;
|
|
542
|
+
|
|
531
543
|
/**
|
|
532
544
|
* Lookup table for valid URI path characters per RFC 3986.
|
|
533
545
|
* Replaces a 16-comparison chain with a single array access.
|
|
@@ -587,7 +599,7 @@ export const TEXT_CLASS_STOP = 1;
|
|
|
587
599
|
export const TEXT_CLASS_NEWLINE = 2;
|
|
588
600
|
/** `h`/`H` — potential URL scheme start, needs the prefix probe. @nodocs */
|
|
589
601
|
export const TEXT_CLASS_URL = 4;
|
|
590
|
-
/** `/` and `.` — potential path start, needs the
|
|
602
|
+
/** `/` and `.` — potential path start, needs the `is_path_boundary` probe. @nodocs */
|
|
591
603
|
export const TEXT_CLASS_PATH = 8;
|
|
592
604
|
/** `\` — a literal-pipe escape candidate inside table cells only. @nodocs */
|
|
593
605
|
export const TEXT_CLASS_BACKSLASH = 16;
|
|
@@ -611,10 +623,23 @@ export const TEXT_SCAN_CLASS: Uint8Array = (() => {
|
|
|
611
623
|
return t;
|
|
612
624
|
})();
|
|
613
625
|
|
|
626
|
+
// trailing chars that never end a URL/path reference
|
|
627
|
+
const is_trailing_punctuation = (char_code: number): boolean =>
|
|
628
|
+
char_code === PERIOD ||
|
|
629
|
+
char_code === COMMA ||
|
|
630
|
+
char_code === SEMICOLON ||
|
|
631
|
+
char_code === COLON ||
|
|
632
|
+
char_code === EXCLAMATION ||
|
|
633
|
+
char_code === QUESTION ||
|
|
634
|
+
char_code === RIGHT_BRACKET;
|
|
635
|
+
|
|
614
636
|
/**
|
|
615
|
-
* Trim trailing
|
|
616
|
-
*
|
|
617
|
-
*
|
|
637
|
+
* Trim the trailing characters a URL/path scan consumed that read as
|
|
638
|
+
* surrounding prose rather than the reference, per GFM's autolink
|
|
639
|
+
* conventions, repeated until nothing more trims:
|
|
640
|
+
* - trailing punctuation `.,;:!?]`
|
|
641
|
+
* - an unbalanced trailing `)` (parens are valid in path components, so a
|
|
642
|
+
* balanced `(…)` stays)
|
|
618
643
|
*
|
|
619
644
|
* Note on `]`: the mdz parsers (`mdz.ts`, `mdz_stream_parser_url.ts`,
|
|
620
645
|
* `mdz_lexer.ts`) scan URL/path chars through `is_valid_path_char`, which
|
|
@@ -622,68 +647,111 @@ export const TEXT_SCAN_CLASS: Uint8Array = (() => {
|
|
|
622
647
|
* The `]` branch here is for external callers using this helper directly on
|
|
623
648
|
* arbitrary URL-ish input as part of the published `@fuzdev/mdz` surface.
|
|
624
649
|
*
|
|
625
|
-
*
|
|
650
|
+
* Tracks the end index and slices once, avoiding O(n²) string slicing.
|
|
626
651
|
*/
|
|
627
652
|
export const mdz_trim_url_punctuation = (url: string): string => {
|
|
628
653
|
let end = url.length;
|
|
654
|
+
// paren counts over the untrimmed prefix — counted once, on the first
|
|
655
|
+
// trailing `)`; the chars trimmed after that hold no parens except the
|
|
656
|
+
// trimmed `)` themselves, which decrement `close_count`
|
|
657
|
+
let open_count = -1;
|
|
658
|
+
let close_count = 0;
|
|
629
659
|
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
if (
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
) {
|
|
641
|
-
end--;
|
|
642
|
-
} else {
|
|
643
|
-
break;
|
|
660
|
+
for (;;) {
|
|
661
|
+
while (end > 0 && is_trailing_punctuation(url.charCodeAt(end - 1))) end--;
|
|
662
|
+
if (end === 0 || url.charCodeAt(end - 1) !== RIGHT_PAREN) break;
|
|
663
|
+
if (open_count === -1) {
|
|
664
|
+
open_count = 0;
|
|
665
|
+
for (let i = 0; i < end; i++) {
|
|
666
|
+
const char = url.charCodeAt(i);
|
|
667
|
+
if (char === LEFT_PAREN) open_count++;
|
|
668
|
+
else if (char === RIGHT_PAREN) close_count++;
|
|
669
|
+
}
|
|
644
670
|
}
|
|
671
|
+
if (close_count <= open_count) break; // balanced — the `)` belongs to the reference
|
|
672
|
+
end--;
|
|
673
|
+
close_count--;
|
|
645
674
|
}
|
|
646
675
|
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
// trailing-paren loop below can't trim anything.
|
|
650
|
-
if (end > 0 && url.charCodeAt(end - 1) === RIGHT_PAREN) {
|
|
651
|
-
let open_count = 0;
|
|
652
|
-
let close_count = 0;
|
|
653
|
-
for (let i = 0; i < end; i++) {
|
|
654
|
-
const char = url.charCodeAt(i);
|
|
655
|
-
if (char === LEFT_PAREN) open_count++;
|
|
656
|
-
if (char === RIGHT_PAREN) close_count++;
|
|
657
|
-
}
|
|
676
|
+
return end === url.length ? url : url.slice(0, end);
|
|
677
|
+
};
|
|
658
678
|
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
679
|
+
/**
|
|
680
|
+
* End of the URI-char run starting at `start` — where an auto-link scan
|
|
681
|
+
* stops, before any trailing trim. A path opened by `(` (`paren_opened`,
|
|
682
|
+
* see `is_path_boundary`) also ends before the `)` that matches that opener:
|
|
683
|
+
* a `(` inside the path raises the depth, so `(./Foo_(bar))` scans
|
|
684
|
+
* `./Foo_(bar)`.
|
|
685
|
+
*
|
|
686
|
+
* @nodocs
|
|
687
|
+
*/
|
|
688
|
+
export const scan_uri_chars_end = (text: string, start: number, paren_opened: boolean): number => {
|
|
689
|
+
let i = start;
|
|
690
|
+
let depth = 0;
|
|
691
|
+
while (i < text.length) {
|
|
692
|
+
const c = text.charCodeAt(i);
|
|
693
|
+
if (!is_valid_path_char(c)) break;
|
|
694
|
+
if (paren_opened) {
|
|
695
|
+
if (c === LEFT_PAREN) depth++;
|
|
696
|
+
else if (c === RIGHT_PAREN) {
|
|
697
|
+
if (depth === 0) break;
|
|
698
|
+
depth--;
|
|
667
699
|
}
|
|
668
700
|
}
|
|
701
|
+
i++;
|
|
669
702
|
}
|
|
670
|
-
|
|
671
|
-
// Return original string if no trimming, otherwise slice once
|
|
672
|
-
return end === url.length ? url : url.slice(0, end);
|
|
703
|
+
return i;
|
|
673
704
|
};
|
|
674
705
|
|
|
706
|
+
/**
|
|
707
|
+
* Whether an internal path reference is only its prefix (`/`, `./`, `../`)
|
|
708
|
+
* with nothing after it — such a reference never links, since a path is its
|
|
709
|
+
* prefix plus at least one URI char.
|
|
710
|
+
*
|
|
711
|
+
* @nodocs
|
|
712
|
+
*/
|
|
713
|
+
export const is_bare_path_prefix = (reference: string): boolean =>
|
|
714
|
+
reference === '/' || reference === './' || reference === '../';
|
|
715
|
+
|
|
716
|
+
// the char code `distance` chars before `index`, or `-1` past the start of input
|
|
717
|
+
const char_code_before = (text: string, index: number, distance: number): number =>
|
|
718
|
+
index >= distance ? text.charCodeAt(index - distance) : -1;
|
|
719
|
+
|
|
720
|
+
/**
|
|
721
|
+
* Whether an auto-detected path (`/…`, `./…`, `../…`) may start after the
|
|
722
|
+
* given characters: at the start of input, after a space, tab, or newline, or
|
|
723
|
+
* after an opening `(` that is itself neither glued to a word char nor the
|
|
724
|
+
* `](` of markdown link syntax. The paren case covers parenthesized
|
|
725
|
+
* references like `(./foo.md)` and `(see /docs)` — a path opened by `(` ends
|
|
726
|
+
* at its matching `)` (`scan_uri_chars_end`), and an unbalanced `)` after a
|
|
727
|
+
* whitespace-opened path is trimmed by `mdz_trim_url_punctuation` — while
|
|
728
|
+
* call-shaped `foo(./bar)` and a failed link's `[text](./foo bar)` stay
|
|
729
|
+
* literal. Every other preceding character (word chars, `[`, `{`, quotes,
|
|
730
|
+
* `*`, `_`, `~`, `/`, `.`) keeps a following path literal.
|
|
731
|
+
*
|
|
732
|
+
* @param prev - the char code directly before the path, or `-1` at the start of input
|
|
733
|
+
* @param before_prev - the char code before `prev`, or `-1` at the start of input
|
|
734
|
+
* @nodocs
|
|
735
|
+
*/
|
|
736
|
+
export const is_path_boundary = (prev: number, before_prev: number): boolean =>
|
|
737
|
+
prev === -1 ||
|
|
738
|
+
prev === SPACE ||
|
|
739
|
+
prev === NEWLINE ||
|
|
740
|
+
prev === TAB ||
|
|
741
|
+
(prev === LEFT_PAREN && before_prev !== RIGHT_BRACKET && !is_word_char(before_prev));
|
|
742
|
+
|
|
675
743
|
/**
|
|
676
744
|
* Check if position in text is the start of an absolute path (starts with `/`).
|
|
677
|
-
* Must
|
|
745
|
+
* Must sit at a path boundary (see `is_path_boundary`): start of input,
|
|
746
|
+
* whitespace, or a `(` not glued to a word or a `]`.
|
|
678
747
|
* Rejects `//` (comments/protocol-relative) and slash followed by whitespace.
|
|
679
748
|
*
|
|
680
749
|
* @nodocs
|
|
681
750
|
*/
|
|
682
751
|
export const is_at_absolute_path = (text: string, index: number): boolean => {
|
|
683
752
|
if (text.charCodeAt(index) !== SLASH) return false;
|
|
684
|
-
if (index
|
|
685
|
-
|
|
686
|
-
if (prev_char !== SPACE && prev_char !== NEWLINE && prev_char !== TAB) return false;
|
|
753
|
+
if (!is_path_boundary(char_code_before(text, index, 1), char_code_before(text, index, 2))) {
|
|
754
|
+
return false;
|
|
687
755
|
}
|
|
688
756
|
if (index + 1 >= text.length) return false;
|
|
689
757
|
const next_char = text.charCodeAt(index + 1);
|
|
@@ -692,16 +760,16 @@ export const is_at_absolute_path = (text: string, index: number): boolean => {
|
|
|
692
760
|
|
|
693
761
|
/**
|
|
694
762
|
* Check if position in text is the start of a relative path (`./` or `../`).
|
|
695
|
-
* Must
|
|
763
|
+
* Must sit at a path boundary (see `is_path_boundary`): start of input,
|
|
764
|
+
* whitespace, or a `(` not glued to a word or a `]`.
|
|
696
765
|
* Rejects prefix followed by whitespace, slash, or end of string.
|
|
697
766
|
*
|
|
698
767
|
* @nodocs
|
|
699
768
|
*/
|
|
700
769
|
export const is_at_relative_path = (text: string, index: number): boolean => {
|
|
701
770
|
if (text.charCodeAt(index) !== PERIOD) return false;
|
|
702
|
-
if (index
|
|
703
|
-
|
|
704
|
-
if (prev_char !== SPACE && prev_char !== NEWLINE && prev_char !== TAB) return false;
|
|
771
|
+
if (!is_path_boundary(char_code_before(text, index, 1), char_code_before(text, index, 2))) {
|
|
772
|
+
return false;
|
|
705
773
|
}
|
|
706
774
|
const remaining = text.length - index;
|
|
707
775
|
// Check for ../ (at least 4 chars: ../x)
|
package/src/lib/mdz_lexer.ts
CHANGED
|
@@ -14,6 +14,9 @@ import {
|
|
|
14
14
|
is_tag_name_char,
|
|
15
15
|
is_word_char,
|
|
16
16
|
is_valid_path_char,
|
|
17
|
+
is_bare_path_prefix,
|
|
18
|
+
is_italic_boundary_blocker,
|
|
19
|
+
scan_uri_chars_end,
|
|
17
20
|
mdz_trim_url_punctuation,
|
|
18
21
|
BACKTICK,
|
|
19
22
|
ASTERISK,
|
|
@@ -2133,6 +2136,13 @@ export class MdzLexer {
|
|
|
2133
2136
|
reference = mdz_trim_url_punctuation(reference);
|
|
2134
2137
|
this.#index = start + reference.length;
|
|
2135
2138
|
|
|
2139
|
+
// a bare scheme (`https://`) left after trimming isn't a URL — literal text,
|
|
2140
|
+
// matching the streaming parser's completion check
|
|
2141
|
+
if (reference.length <= match_url_prefix_case_insensitive(reference, 0)) {
|
|
2142
|
+
this.#emit_text(reference, start);
|
|
2143
|
+
return;
|
|
2144
|
+
}
|
|
2145
|
+
|
|
2136
2146
|
this.#tokens.push({
|
|
2137
2147
|
type: 'autolink',
|
|
2138
2148
|
reference,
|
|
@@ -2145,19 +2155,24 @@ export class MdzLexer {
|
|
|
2145
2155
|
#tokenize_auto_link_internal(): void {
|
|
2146
2156
|
const start = this.#index;
|
|
2147
2157
|
|
|
2148
|
-
//
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
}
|
|
2158
|
+
// collect path characters — a path opened by `(` ends at its matching `)`
|
|
2159
|
+
// (the probes only admit a `(` before a path at a valid boundary)
|
|
2160
|
+
this.#index = scan_uri_chars_end(
|
|
2161
|
+
this.#text,
|
|
2162
|
+
start,
|
|
2163
|
+
start > 0 && this.#text.charCodeAt(start - 1) === LEFT_PAREN
|
|
2164
|
+
);
|
|
2156
2165
|
|
|
2157
2166
|
let reference = this.#text.slice(start, this.#index);
|
|
2158
2167
|
reference = mdz_trim_url_punctuation(reference);
|
|
2159
2168
|
this.#index = start + reference.length;
|
|
2160
2169
|
|
|
2170
|
+
// a bare prefix (`./`, `/`) left after trimming isn't a path — literal text
|
|
2171
|
+
if (is_bare_path_prefix(reference)) {
|
|
2172
|
+
this.#emit_text(reference, start);
|
|
2173
|
+
return;
|
|
2174
|
+
}
|
|
2175
|
+
|
|
2161
2176
|
this.#tokens.push({
|
|
2162
2177
|
type: 'autolink',
|
|
2163
2178
|
reference,
|
|
@@ -2240,13 +2255,15 @@ export class MdzLexer {
|
|
|
2240
2255
|
* transparent (so `_~~x~~_` nests).
|
|
2241
2256
|
*/
|
|
2242
2257
|
#is_at_word_boundary(index: number, check_before: boolean, check_after: boolean): boolean {
|
|
2243
|
-
if (check_before && index > 0) {
|
|
2244
|
-
|
|
2245
|
-
if (is_word_char(prev) || prev === UNDERSCORE) return false;
|
|
2258
|
+
if (check_before && index > 0 && is_italic_boundary_blocker(this.#text.charCodeAt(index - 1))) {
|
|
2259
|
+
return false;
|
|
2246
2260
|
}
|
|
2247
|
-
if (
|
|
2248
|
-
|
|
2249
|
-
|
|
2261
|
+
if (
|
|
2262
|
+
check_after &&
|
|
2263
|
+
index < this.#text.length &&
|
|
2264
|
+
is_italic_boundary_blocker(this.#text.charCodeAt(index))
|
|
2265
|
+
) {
|
|
2266
|
+
return false;
|
|
2250
2267
|
}
|
|
2251
2268
|
return true;
|
|
2252
2269
|
}
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* corrections are bounded, explicit opcodes (`revert`, `wrap`, `trim_text`).
|
|
8
8
|
*
|
|
9
9
|
* The design is derived from
|
|
10
|
-
*
|
|
10
|
+
* [@pngwn.at](https://bsky.app/profile/pngwn.at/post/3mi527zntb22n)'s
|
|
11
11
|
* ideas: restrict the syntax so streaming is tractable, render
|
|
12
12
|
* optimistically and correct when wrong, emit serializable opcodes to avoid
|
|
13
13
|
* re-parsing, and keep the opcodes target-agnostic so any renderer can
|
|
@@ -138,9 +138,22 @@ import { consume_delimiter_as_text, consume_text_run } from './mdz_stream_parser
|
|
|
138
138
|
* Italic is the only wedge (the only delimiter whose one-shot form rejects
|
|
139
139
|
* on a failed first closer); see `try_code`'s hold and `code_search_limit`.
|
|
140
140
|
* - **Optimistic inline code unclosed at EOF** — a `` ` `` that opened
|
|
141
|
-
* optimistically consumes its tail as raw code text, so formatting
|
|
142
|
-
* never
|
|
141
|
+
* optimistically consumes its tail as raw code text, so formatting and
|
|
142
|
+
* auto-links inside it never form (`x **b** z` and `(./a.md)` after an
|
|
143
|
+
* unclosed backtick stay flat where one-shot parses the bold and the link);
|
|
143
144
|
* the parser never re-parses, so the EOF revert can only flatten it to text.
|
|
145
|
+
* - **A span crossing a markdown link's `]`** — the one-shot parse pairs a
|
|
146
|
+
* `**`/`_`/`~~` opener with the first occurrence of its delimiter even across
|
|
147
|
+
* a `[text](url)`'s bracket, because markdown links aren't bounded by an
|
|
148
|
+
* enclosing span. With the opener before the link and that occurrence in its
|
|
149
|
+
* display text, the link forms and the opener stays literal
|
|
150
|
+
* (`**x [a b** c](/u) y**`; also when the occurrence sits in an auto-link in
|
|
151
|
+
* the display text, `_x [a https://b_.c **d** e](/u) y_`); with the opener in
|
|
152
|
+
* the display text and the closer past `]`, a link that then fails re-lexes
|
|
153
|
+
* from `[` and the span forms (`[_]_`). Streaming decides the span before it
|
|
154
|
+
* knows whether the link completes, so it closes at the bracketed
|
|
155
|
+
* occurrence, or reverts the span with the failed link; mirroring the
|
|
156
|
+
* one-shot parse would need a hold to the link's `)` or a re-parse.
|
|
144
157
|
* - **Link/tag opens at EOF** — `finish()` doesn't open links or tags, so a held
|
|
145
158
|
* tail containing complete `[text](url)` / `<Tag>…</Tag>` syntax parses flat.
|
|
146
159
|
* - **Block elements interrupt optimistic inlines** — at column 0 a
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Block-element handlers for the streaming mdz parser.
|
|
3
3
|
*
|
|
4
|
-
* Each `try_*` returns
|
|
5
|
-
*
|
|
4
|
+
* Each `try_*` returns `TryResult` so the orchestrator in
|
|
5
|
+
* `mdz_stream_parser.ts` can fall through or wait for more input.
|
|
6
6
|
*
|
|
7
7
|
* @module
|
|
8
8
|
*/
|
|
@@ -10,8 +10,8 @@ import {
|
|
|
10
10
|
NEWLINE,
|
|
11
11
|
RIGHT_ANGLE,
|
|
12
12
|
UNDERSCORE,
|
|
13
|
+
is_italic_boundary_blocker,
|
|
13
14
|
is_line_whitespace,
|
|
14
|
-
is_word_char,
|
|
15
15
|
mdz_blockquote_line_has_content,
|
|
16
16
|
mdz_match_blockquote_prefix
|
|
17
17
|
} from './mdz_helpers.ts';
|
|
@@ -190,7 +190,7 @@ export const try_double_delimiter = (
|
|
|
190
190
|
export const try_italic = (state: MdzStreamParserState, forced = false): boolean => {
|
|
191
191
|
// opening boundary: reject after a word char, or after the delimiter
|
|
192
192
|
// itself — `_` is self-opaque, so runs like `__init__` stay literal
|
|
193
|
-
if (state.prev_char !== -1 && (
|
|
193
|
+
if (state.prev_char !== -1 && is_italic_boundary_blocker(state.prev_char)) {
|
|
194
194
|
consume_delimiter_as_text(state, '_');
|
|
195
195
|
return true;
|
|
196
196
|
}
|
|
@@ -248,7 +248,7 @@ export const check_close_word_boundary = (state: MdzStreamParserState): boolean
|
|
|
248
248
|
const next_pos = state.pos + 1;
|
|
249
249
|
if (next_pos < state.buffer.length) {
|
|
250
250
|
const next = state.buffer.charCodeAt(next_pos);
|
|
251
|
-
return !(
|
|
251
|
+
return !is_italic_boundary_blocker(next);
|
|
252
252
|
}
|
|
253
253
|
return true;
|
|
254
254
|
};
|
|
@@ -327,7 +327,7 @@ export const scan_closer_boundary = (
|
|
|
327
327
|
const after_pos = delim_pos + 1;
|
|
328
328
|
if (after_pos < state.buffer.length) {
|
|
329
329
|
const after = state.buffer.charCodeAt(after_pos);
|
|
330
|
-
return
|
|
330
|
+
return is_italic_boundary_blocker(after) ? 'rejected' : 'confirmed';
|
|
331
331
|
}
|
|
332
332
|
// closer at end of buffer — treat as confirmed, consistent with
|
|
333
333
|
// check_close_word_boundary which also assumes boundary at buffer end
|
|
@@ -167,6 +167,12 @@ export interface PendingUrl {
|
|
|
167
167
|
/** Prefix match tracking — only used when `!confirmed`. */
|
|
168
168
|
viable_https: boolean;
|
|
169
169
|
viable_http: boolean;
|
|
170
|
+
/**
|
|
171
|
+
* Paren depth inside a path opened by `(` — the path ends at a `)` at
|
|
172
|
+
* depth zero — or `-1` for every other URL/path, which scan to their
|
|
173
|
+
* terminator.
|
|
174
|
+
*/
|
|
175
|
+
paren_depth: number;
|
|
170
176
|
}
|
|
171
177
|
|
|
172
178
|
/**
|
|
@@ -185,6 +191,14 @@ export interface MdzStreamParserState {
|
|
|
185
191
|
stack: Array<StackEntry>;
|
|
186
192
|
column: number;
|
|
187
193
|
prev_char: number;
|
|
194
|
+
/**
|
|
195
|
+
* The char before `prev_char` — the second char of lookbehind the path
|
|
196
|
+
* boundary needs (`is_path_boundary`: a `(` opens a path only when not
|
|
197
|
+
* glued to a word or a `]`). Only `consume_text_run` maintains it, and it's read only
|
|
198
|
+
* when `prev_char` is `(`, which only a text run leaves behind ahead of a
|
|
199
|
+
* path-start char; elsewhere it may be stale.
|
|
200
|
+
*/
|
|
201
|
+
before_prev_char: number;
|
|
188
202
|
active_text_id: MdzNodeId | null;
|
|
189
203
|
accumulated_text: string;
|
|
190
204
|
accumulated_text_start: number;
|
|
@@ -331,6 +345,7 @@ export const create_state = (): MdzStreamParserState => ({
|
|
|
331
345
|
stack: [],
|
|
332
346
|
column: 0,
|
|
333
347
|
prev_char: -1,
|
|
348
|
+
before_prev_char: -1,
|
|
334
349
|
active_text_id: null,
|
|
335
350
|
accumulated_text: '',
|
|
336
351
|
accumulated_text_start: 0,
|
|
@@ -776,18 +791,24 @@ export const revert_above = (state: MdzStreamParserState, target_idx: number): v
|
|
|
776
791
|
};
|
|
777
792
|
|
|
778
793
|
/**
|
|
779
|
-
* Revert
|
|
780
|
-
*
|
|
781
|
-
*
|
|
782
|
-
*
|
|
783
|
-
*
|
|
784
|
-
*
|
|
794
|
+
* Revert one open frame, possibly **mid-stack**, without reverting the frames
|
|
795
|
+
* above it. Two callers:
|
|
796
|
+
*
|
|
797
|
+
* - an open `Italic` whose first closer candidate failed its boundary check
|
|
798
|
+
* (italic is the only inline with closer-boundary rules) — one-shot parsing
|
|
799
|
+
* greedy-rejects the opener at the first invalid closer, so once a failed
|
|
800
|
+
* candidate is seen the optimistic open is known wrong, and reverting
|
|
801
|
+
* immediately keeps chunked output identical to the one-shot parse
|
|
802
|
+
* - the auto-link span resolution (`revert_spans_closed_inside` in
|
|
803
|
+
* `mdz_stream_parser_url.ts`), which reverts `Bold`, `Italic`,
|
|
804
|
+
* `Strikethrough`, `Element`, and `Component` frames whose span the one-shot
|
|
805
|
+
* parse leaves literal, one frame at a time and possibly beneath a `Link`
|
|
806
|
+
* frame that stays open
|
|
785
807
|
*
|
|
786
|
-
*
|
|
787
|
-
*
|
|
788
|
-
*
|
|
789
|
-
*
|
|
790
|
-
* node is still its parent's last child; see `mdz_opcodes_to_nodes` and
|
|
808
|
+
* Containers opened above the reverted frame stay open, and the consumers'
|
|
809
|
+
* revert handlers re-parent them identically for every frame type — the
|
|
810
|
+
* reverted node is still its parent's last child, and both handlers work on
|
|
811
|
+
* node ids and children only (see `mdz_opcodes_to_nodes` and
|
|
791
812
|
* `MdzStreamState`).
|
|
792
813
|
*
|
|
793
814
|
* Bypasses `emit` for the opcode push: `emit`'s heading-segment handling
|
|
@@ -813,8 +834,8 @@ export const revert_failed_close = (state: MdzStreamParserState, stack_idx: numb
|
|
|
813
834
|
if (remaining === 0) counts.delete(entry.tag_name);
|
|
814
835
|
else counts.set(entry.tag_name, remaining);
|
|
815
836
|
}
|
|
816
|
-
//
|
|
817
|
-
//
|
|
837
|
+
// Element/Component frames count toward the link/tag nesting depth (Link
|
|
838
|
+
// frames are never reverted here today, but keep the counter honest)
|
|
818
839
|
if (entry.node_type === 'Link' || entry.tag_name !== undefined) state.open_link_tag_depth--;
|
|
819
840
|
state.opcodes.push({
|
|
820
841
|
type: 'revert',
|
|
@@ -6,14 +6,12 @@
|
|
|
6
6
|
|
|
7
7
|
import {
|
|
8
8
|
HTTPS_PREFIX_LENGTH,
|
|
9
|
-
NEWLINE,
|
|
10
|
-
SPACE,
|
|
11
|
-
TAB,
|
|
12
9
|
TEXT_CLASS_NEWLINE,
|
|
13
10
|
TEXT_CLASS_PATH,
|
|
14
11
|
TEXT_CLASS_STOP,
|
|
15
12
|
TEXT_CLASS_URL,
|
|
16
13
|
TEXT_SCAN_CLASS,
|
|
14
|
+
is_path_boundary,
|
|
17
15
|
is_word_char,
|
|
18
16
|
match_url_prefix_case_insensitive
|
|
19
17
|
} from './mdz_helpers.ts';
|
|
@@ -82,15 +80,24 @@ export const consume_text_run = (state: MdzStreamParserState): void => {
|
|
|
82
80
|
break;
|
|
83
81
|
}
|
|
84
82
|
} else if ((char_class & TEXT_CLASS_PATH) !== 0) {
|
|
85
|
-
// path (`/` or `.`): only break at a plausible path start
|
|
86
|
-
|
|
87
|
-
if (
|
|
83
|
+
// path (`/` or `.`): only break at a plausible path start — the run
|
|
84
|
+
// start's lookbehind is the incoming `prev_char`
|
|
85
|
+
if (
|
|
86
|
+
is_path_boundary(
|
|
87
|
+
state.buffer.charCodeAt(state.pos - 1),
|
|
88
|
+
state.pos - 2 >= start ? state.buffer.charCodeAt(state.pos - 2) : state.prev_char
|
|
89
|
+
)
|
|
90
|
+
) {
|
|
91
|
+
break;
|
|
92
|
+
}
|
|
88
93
|
}
|
|
89
94
|
// backslash (or a non-starting path char) — ordinary text
|
|
90
95
|
state.pos++;
|
|
91
96
|
}
|
|
92
97
|
if (mdz_debug_work.enabled) mdz_debug_work.total += state.pos - start;
|
|
93
98
|
accumulate_text(state, state.buffer.slice(start, state.pos), offset(state, start));
|
|
99
|
+
state.before_prev_char =
|
|
100
|
+
state.pos - 2 >= start ? state.buffer.charCodeAt(state.pos - 2) : state.prev_char;
|
|
94
101
|
state.prev_char = state.buffer.charCodeAt(state.pos - 1);
|
|
95
102
|
state.column += state.pos - start;
|
|
96
103
|
};
|