@vectojs/markdown 0.7.0 → 0.9.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/Markdown.d.ts +175 -9
- package/dist/MarkdownWorkerSource.d.ts +1 -1
- package/dist/frontMatter.d.ts +69 -0
- package/dist/incrementalLex.d.ts +172 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +456 -35
- package/dist/index.mjs +453 -34
- package/package.json +2 -2
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Incremental block lexing for a growing (append-only) Markdown source.
|
|
3
|
+
*
|
|
4
|
+
* ## The problem this solves
|
|
5
|
+
*
|
|
6
|
+
* `marked` has no incremental lexing API, so the obvious streaming strategy is
|
|
7
|
+
* to re-lex the whole accumulated document on every chunk. That is O(n²) over a
|
|
8
|
+
* stream, and measured in `comparisons/stream-markdown-smd` it dominates
|
|
9
|
+
* everything else: a 25 070-char document delivered in 784 chunks cost 434 ms in
|
|
10
|
+
* Chrome 150 while lexing the finished document **once** cost 0.975 ms. The
|
|
11
|
+
* parser is linear; the strategy was quadratic.
|
|
12
|
+
*
|
|
13
|
+
* This module keeps a **stable block boundary** — a character offset before
|
|
14
|
+
* which the token list can no longer change — and re-lexes only the text after
|
|
15
|
+
* it, splicing the result onto the already-stable token prefix. The cost per
|
|
16
|
+
* chunk becomes O(unstable tail) instead of O(document).
|
|
17
|
+
*
|
|
18
|
+
* ## The correctness contract
|
|
19
|
+
*
|
|
20
|
+
* {@link lexFull} and {@link lexAppend} must return a token list **deeply
|
|
21
|
+
* identical** to `marked.lexer(source)` for the same source. Speed is
|
|
22
|
+
* secondary: a boundary chosen one line too early silently corrupts the token
|
|
23
|
+
* stream, which is a far worse failure than being slow. `incrementalLex.test.ts`
|
|
24
|
+
* enforces this by streaming a corpus one character at a time and comparing
|
|
25
|
+
* against a full lex at **every** intermediate length.
|
|
26
|
+
*
|
|
27
|
+
* ## Why the boundary rule is what it is
|
|
28
|
+
*
|
|
29
|
+
* The rule: cut immediately after a `space` token that has **at least one token
|
|
30
|
+
* following it**, and never when a link reference definition exists.
|
|
31
|
+
*
|
|
32
|
+
* Three properties of `marked`'s block lexer (18.0.7) make that safe, and each
|
|
33
|
+
* was measured exhaustively rather than reasoned about:
|
|
34
|
+
*
|
|
35
|
+
* 1. **A pushed `space` token always means a blank line.** A lone `\n` is merged
|
|
36
|
+
* into the preceding token's `raw` instead of being pushed, so a `space`
|
|
37
|
+
* token in the list is a real block separator — never a single line ending.
|
|
38
|
+
* 2. **For every built-in rule, only the token adjacent to the end of the source
|
|
39
|
+
* can still change.** With a token following the `space`, the construct
|
|
40
|
+
* before that `space` is committed. This is what rules out the interesting
|
|
41
|
+
* failures: an indented code block or a loose list *can* absorb a blank line
|
|
42
|
+
* and keep going, and a `paragraph` can still acquire a setext underline —
|
|
43
|
+
* but only while it is the last thing in the source. A brute-force sweep over
|
|
44
|
+
* 14 documents × every prefix length × every cut index found the
|
|
45
|
+
* `nFollow >= 1` form safe for every predecessor type (`blockquote`, `code`,
|
|
46
|
+
* `heading`, `hr`, `html`, `list`, `paragraph`, `table`) and the
|
|
47
|
+
* `nFollow == 0` form unsafe for `code`, `list` and `paragraph` — hence the
|
|
48
|
+
* one-token lag.
|
|
49
|
+
*
|
|
50
|
+
* Two things break that property, and both were found by fuzzing rather than
|
|
51
|
+
* by reading the rules:
|
|
52
|
+
*
|
|
53
|
+
* **A `list` reaches past a blank line to absorb a following list item.**
|
|
54
|
+
* Measured: `'1. ordered\n2. second\n\n\n1.'` is a *single* `list` token with
|
|
55
|
+
* `loose: true`, where a splice at the blank line yields
|
|
56
|
+
* `[list(loose:false), space, list]`. Three tokens against one, and the
|
|
57
|
+
* `loose` flag differs, which changes rendering. So a cut whose next token is
|
|
58
|
+
* a `list` is only taken once a *further* token exists after it — at which
|
|
59
|
+
* point the list can no longer grow. This is {@link cutIsSettled}.
|
|
60
|
+
*
|
|
61
|
+
* **Our own `blockMath` extension breaks locality in both directions**, which
|
|
62
|
+
* is why {@link hasBlockMathOpener} degrades outright:
|
|
63
|
+
* - *Forward*: the tokenizer is `/^ {0,3}\$\$([\s\S]+?)\$\$[ \t]*(?:\n|$)/`
|
|
64
|
+
* and `[\s\S]+?` crosses blank lines, so an unterminated `$$` reaches
|
|
65
|
+
* arbitrarily far ahead. Measured: `'$$\nopen\n\npara\n'` lexes to
|
|
66
|
+
* `[paragraph, space, paragraph]`, and appending `'\n$$\n'` collapses all
|
|
67
|
+
* three into one `blockMath` token.
|
|
68
|
+
* - *Backward*: `blockTokens` clips the text handed to the paragraph
|
|
69
|
+
* tokenizer whenever an extension's `startBlock` hook reports a position,
|
|
70
|
+
* and sets a flag that merges the **next** paragraph into the clipped one.
|
|
71
|
+
* Since `blockMath` supplies `start()`, a `$$` anywhere ahead retroactively
|
|
72
|
+
* re-groups paragraphs already emitted. Measured on
|
|
73
|
+
* `'Term\n: definition-ish\n| partial | table |\n| --- |\n\nAfter.\n'`
|
|
74
|
+
* plus a trailing `'\n$$\nx\n'`: without the extension it is
|
|
75
|
+
* `paragraph, paragraph`; with it registered the two become **one** merged
|
|
76
|
+
* paragraph. Capping the boundary cannot fix a backwards reach, so any
|
|
77
|
+
* line-start `$$` degrades the instance instead. That is only correct
|
|
78
|
+
* because `start()` returns `undefined` when no line-start `$$` exists, so
|
|
79
|
+
* with none present the clip never fires and the merge is impossible.
|
|
80
|
+
*
|
|
81
|
+
* Any future block-level extension that supplies `start()` or whose tokenizer
|
|
82
|
+
* can span a blank line needs the same treatment.
|
|
83
|
+
* 3. **Link reference definitions break prefix reuse entirely.** `marked`
|
|
84
|
+
* collects every `def` while block-lexing and only then resolves reflinks
|
|
85
|
+
* across the *whole* document, so a definition arriving late retroactively
|
|
86
|
+
* changes inline tokens that are already emitted, and one inside the stable
|
|
87
|
+
* prefix is invisible to a suffix lex. Both directions are unfixable by
|
|
88
|
+
* boundary placement, so an instance that sees any definition degrades to
|
|
89
|
+
* full lexing permanently. The sweep confirms it: `def` was the one
|
|
90
|
+
* predecessor type unsafe at `nFollow >= 1` (21/21).
|
|
91
|
+
*
|
|
92
|
+
* Degrading is always available and always correct, so a carriage return takes
|
|
93
|
+
* it: `marked` normalises CR internally, which desyncs every raw-length offset
|
|
94
|
+
* from the source those offsets are supposed to index.
|
|
95
|
+
*
|
|
96
|
+
* ## Why the boundary is verified rather than trusted
|
|
97
|
+
*
|
|
98
|
+
* Offsets are derived by summing `raw` lengths, which assumes `raw` strings tile
|
|
99
|
+
* their source. They usually do, but not always: measured against marked 18.0.7,
|
|
100
|
+
* a source ending in a bare list marker (`"- a\n- "`) lexes to raw `"- a\n-\n"`,
|
|
101
|
+
* because the list tokenizer trims the final item and re-adds a newline. So
|
|
102
|
+
* every advance is **verified** — the text being declared stable must equal the
|
|
103
|
+
* concatenated `raw` of the tokens covering it — and an advance that fails
|
|
104
|
+
* verification is simply not taken. That case is transient (the next chunk
|
|
105
|
+
* completes the item and it tiles again), so declining costs one chunk of window
|
|
106
|
+
* growth, where degrading would have cost the whole rest of the stream.
|
|
107
|
+
*/
|
|
108
|
+
import { type TokensList } from 'marked';
|
|
109
|
+
/**
|
|
110
|
+
* Everything needed to extend a lex without redoing it.
|
|
111
|
+
*
|
|
112
|
+
* `source` and `tail` are both carried because they serve different masters:
|
|
113
|
+
* `source` is what a full lex needs if this instance ever degrades (and what the
|
|
114
|
+
* caller reconciles its own length check against), while `tail` is the unstable
|
|
115
|
+
* suffix that is actually re-lexed each chunk. Keeping `tail` separately is what
|
|
116
|
+
* makes the per-chunk string work O(tail) — deriving it as
|
|
117
|
+
* `source.slice(stableOffset)` each time would force the engine to flatten the
|
|
118
|
+
* concatenation rope, putting an O(document) memcpy back into the hot path.
|
|
119
|
+
*/
|
|
120
|
+
export interface IncrementalLexCache {
|
|
121
|
+
/** Full accumulated source these tokens describe. */
|
|
122
|
+
readonly source: string;
|
|
123
|
+
/** `source.slice(stableOffset)` — the part still subject to change. */
|
|
124
|
+
readonly tail: string;
|
|
125
|
+
/** Complete token list for `source`. */
|
|
126
|
+
readonly tokens: TokensList;
|
|
127
|
+
/** Number of leading tokens that can no longer change. */
|
|
128
|
+
readonly stableCount: number;
|
|
129
|
+
/** Character offset in `source` at which the stable prefix ends. */
|
|
130
|
+
readonly stableOffset: number;
|
|
131
|
+
/** Once set, this instance always full-lexes. Never clears. */
|
|
132
|
+
readonly degraded: boolean;
|
|
133
|
+
/** Why it degraded, for tests and diagnostics. `null` while incremental. */
|
|
134
|
+
readonly degradedReason: DegradeReason | null;
|
|
135
|
+
}
|
|
136
|
+
/** Why an instance gave up on incremental lexing. */
|
|
137
|
+
export type DegradeReason =
|
|
138
|
+
/** A link reference definition exists; see the module comment. */
|
|
139
|
+
'link-definition'
|
|
140
|
+
/** A carriage return desyncs `raw`-length offsets from source offsets. */
|
|
141
|
+
| 'carriage-return'
|
|
142
|
+
/** A line-start `$$` lets `blockMath` reach outside its own token. */
|
|
143
|
+
| 'block-math';
|
|
144
|
+
export interface IncrementalLexResult {
|
|
145
|
+
/** Deeply identical to `marked.lexer(source)`. */
|
|
146
|
+
readonly tokens: TokensList;
|
|
147
|
+
readonly cache: IncrementalLexCache;
|
|
148
|
+
/**
|
|
149
|
+
* Characters actually handed to `marked.lexer()`. Equal to `source.length` for
|
|
150
|
+
* a full lex and to the unstable tail otherwise — so the ratio against the
|
|
151
|
+
* document length is the direct measure of what the boundary saved.
|
|
152
|
+
*/
|
|
153
|
+
readonly charsLexed: number;
|
|
154
|
+
/** Leading tokens taken from the cache rather than re-lexed. */
|
|
155
|
+
readonly reusedTokens: number;
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Lex `source` from scratch and prepare to extend it incrementally.
|
|
159
|
+
*
|
|
160
|
+
* Used for the first request for an instance, and for anything that is not an
|
|
161
|
+
* append: a `setContent()`, or a resync.
|
|
162
|
+
*/
|
|
163
|
+
export declare function lexFull(source: string): IncrementalLexResult;
|
|
164
|
+
/**
|
|
165
|
+
* Extend a previous lex with appended text, re-lexing only the unstable tail.
|
|
166
|
+
*
|
|
167
|
+
* The caller must guarantee `append` extends exactly `prev.source`. This does not
|
|
168
|
+
* verify that, because the verification would be an O(document) comparison per
|
|
169
|
+
* chunk and would defeat the purpose; the worker enforces it structurally by
|
|
170
|
+
* owning the cache and by checking the caller's `expectedLength` first.
|
|
171
|
+
*/
|
|
172
|
+
export declare function lexAppend(prev: IncrementalLexCache, append: string): IncrementalLexResult;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,2 +1,4 @@
|
|
|
1
1
|
export * from './Markdown';
|
|
2
|
+
export { parseFrontMatterFields, scanFrontMatter } from './frontMatter';
|
|
3
|
+
export type { FrontMatterScan } from './frontMatter';
|
|
2
4
|
export type { IncompleteMarkdownMode, StreamController, StreamControllerOptions, StreamControllerState, StreamPacingOptions, } from './StreamController';
|