@fuzdev/mdz 0.1.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/LICENSE +21 -0
- package/README.md +32 -0
- package/dist/Mdz.svelte +39 -0
- package/dist/Mdz.svelte.d.ts +17 -0
- package/dist/Mdz.svelte.d.ts.map +1 -0
- package/dist/MdzNodeView.svelte +140 -0
- package/dist/MdzNodeView.svelte.d.ts +9 -0
- package/dist/MdzNodeView.svelte.d.ts.map +1 -0
- package/dist/MdzPrecompiled.svelte +33 -0
- package/dist/MdzPrecompiled.svelte.d.ts +17 -0
- package/dist/MdzPrecompiled.svelte.d.ts.map +1 -0
- package/dist/MdzRoot.svelte +40 -0
- package/dist/MdzRoot.svelte.d.ts +14 -0
- package/dist/MdzRoot.svelte.d.ts.map +1 -0
- package/dist/MdzStream.svelte +37 -0
- package/dist/MdzStream.svelte.d.ts +18 -0
- package/dist/MdzStream.svelte.d.ts.map +1 -0
- package/dist/MdzStreamNodeView.svelte +150 -0
- package/dist/MdzStreamNodeView.svelte.d.ts +9 -0
- package/dist/MdzStreamNodeView.svelte.d.ts.map +1 -0
- package/dist/context_helpers.d.ts +23 -0
- package/dist/context_helpers.d.ts.map +1 -0
- package/dist/context_helpers.js +19 -0
- package/dist/mdz.d.ts +152 -0
- package/dist/mdz.d.ts.map +1 -0
- package/dist/mdz.js +61 -0
- package/dist/mdz_contexts.d.ts +106 -0
- package/dist/mdz_contexts.d.ts.map +1 -0
- package/dist/mdz_contexts.js +42 -0
- package/dist/mdz_helpers.d.ts +321 -0
- package/dist/mdz_helpers.d.ts.map +1 -0
- package/dist/mdz_helpers.js +668 -0
- package/dist/mdz_lexer.d.ts +123 -0
- package/dist/mdz_lexer.d.ts.map +1 -0
- package/dist/mdz_lexer.js +1405 -0
- package/dist/mdz_opcodes.d.ts +193 -0
- package/dist/mdz_opcodes.d.ts.map +1 -0
- package/dist/mdz_opcodes.js +14 -0
- package/dist/mdz_opcodes_to_nodes.d.ts +26 -0
- package/dist/mdz_opcodes_to_nodes.d.ts.map +1 -0
- package/dist/mdz_opcodes_to_nodes.js +410 -0
- package/dist/mdz_stream_parser.d.ts +109 -0
- package/dist/mdz_stream_parser.d.ts.map +1 -0
- package/dist/mdz_stream_parser.js +699 -0
- package/dist/mdz_stream_parser_block.d.ts +99 -0
- package/dist/mdz_stream_parser_block.d.ts.map +1 -0
- package/dist/mdz_stream_parser_block.js +437 -0
- package/dist/mdz_stream_parser_inline.d.ts +98 -0
- package/dist/mdz_stream_parser_inline.d.ts.map +1 -0
- package/dist/mdz_stream_parser_inline.js +481 -0
- package/dist/mdz_stream_parser_link.d.ts +29 -0
- package/dist/mdz_stream_parser_link.d.ts.map +1 -0
- package/dist/mdz_stream_parser_link.js +227 -0
- package/dist/mdz_stream_parser_list.d.ts +106 -0
- package/dist/mdz_stream_parser_list.d.ts.map +1 -0
- package/dist/mdz_stream_parser_list.js +438 -0
- package/dist/mdz_stream_parser_state.d.ts +476 -0
- package/dist/mdz_stream_parser_state.d.ts.map +1 -0
- package/dist/mdz_stream_parser_state.js +651 -0
- package/dist/mdz_stream_parser_text.d.ts +25 -0
- package/dist/mdz_stream_parser_text.d.ts.map +1 -0
- package/dist/mdz_stream_parser_text.js +67 -0
- package/dist/mdz_stream_parser_url.d.ts +76 -0
- package/dist/mdz_stream_parser_url.d.ts.map +1 -0
- package/dist/mdz_stream_parser_url.js +328 -0
- package/dist/mdz_stream_state.svelte.d.ts +53 -0
- package/dist/mdz_stream_state.svelte.d.ts.map +1 -0
- package/dist/mdz_stream_state.svelte.js +360 -0
- package/dist/mdz_to_svelte.d.ts +71 -0
- package/dist/mdz_to_svelte.d.ts.map +1 -0
- package/dist/mdz_to_svelte.js +127 -0
- package/dist/mdz_token_parser.d.ts +20 -0
- package/dist/mdz_token_parser.d.ts.map +1 -0
- package/dist/mdz_token_parser.js +449 -0
- package/dist/svelte_preprocess_mdz.d.ts +80 -0
- package/dist/svelte_preprocess_mdz.d.ts.map +1 -0
- package/dist/svelte_preprocess_mdz.js +543 -0
- package/dist/tsdoc_mdz.d.ts +45 -0
- package/dist/tsdoc_mdz.d.ts.map +1 -0
- package/dist/tsdoc_mdz.js +88 -0
- package/package.json +131 -0
- package/src/lib/context_helpers.ts +52 -0
- package/src/lib/mdz.ts +190 -0
- package/src/lib/mdz_contexts.ts +99 -0
- package/src/lib/mdz_helpers.ts +727 -0
- package/src/lib/mdz_lexer.ts +1761 -0
- package/src/lib/mdz_opcodes.ts +227 -0
- package/src/lib/mdz_opcodes_to_nodes.ts +470 -0
- package/src/lib/mdz_stream_parser.ts +766 -0
- package/src/lib/mdz_stream_parser_block.ts +490 -0
- package/src/lib/mdz_stream_parser_inline.ts +527 -0
- package/src/lib/mdz_stream_parser_link.ts +283 -0
- package/src/lib/mdz_stream_parser_list.ts +525 -0
- package/src/lib/mdz_stream_parser_state.ts +957 -0
- package/src/lib/mdz_stream_parser_text.ts +95 -0
- package/src/lib/mdz_stream_parser_url.ts +386 -0
- package/src/lib/mdz_stream_state.svelte.ts +383 -0
- package/src/lib/mdz_to_svelte.ts +213 -0
- package/src/lib/mdz_token_parser.ts +544 -0
- package/src/lib/svelte_preprocess_mdz.ts +777 -0
- package/src/lib/tsdoc_mdz.ts +97 -0
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Opcode types for the mdz streaming parser.
|
|
3
|
+
*
|
|
4
|
+
* Opcodes are serializable rendering instructions emitted by `MdzStreamParser`.
|
|
5
|
+
* They tell a renderer what to do next — open a container, append text, close it,
|
|
6
|
+
* or revert an optimistic assumption. Target-agnostic: works for HTML, Svelte, PDF, etc.
|
|
7
|
+
*
|
|
8
|
+
* The parser makes optimistic assumptions about ambiguous syntax (e.g., `**` is probably bold)
|
|
9
|
+
* and emits `revert` opcodes to correct when wrong. This enables true streaming rendering
|
|
10
|
+
* without ever re-parsing.
|
|
11
|
+
*
|
|
12
|
+
* @module
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Unique monotonic identifier for each node created by the parser.
|
|
17
|
+
* IDs are never reused within a parser instance.
|
|
18
|
+
*/
|
|
19
|
+
export type MdzNodeId = number;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Node types that can be opened as containers.
|
|
23
|
+
*
|
|
24
|
+
* `Code` appears here *and* in `MdzNodeTypeText`: inline code resolved within
|
|
25
|
+
* one chunk emits as a single `text` opcode, while a backtick with no closer
|
|
26
|
+
* visible yet opens an optimistic `Code` container whose content streams as
|
|
27
|
+
* text children (collapsed back to `content` by consumers at `close`).
|
|
28
|
+
*/
|
|
29
|
+
export type MdzNodeTypeContainer =
|
|
30
|
+
| 'Paragraph'
|
|
31
|
+
| 'Bold'
|
|
32
|
+
| 'Italic'
|
|
33
|
+
| 'Strikethrough'
|
|
34
|
+
| 'Link'
|
|
35
|
+
| 'Heading'
|
|
36
|
+
| 'List'
|
|
37
|
+
| 'ListItem'
|
|
38
|
+
| 'Blockquote'
|
|
39
|
+
| 'Element'
|
|
40
|
+
| 'Component'
|
|
41
|
+
| 'Codeblock'
|
|
42
|
+
| 'Code';
|
|
43
|
+
|
|
44
|
+
/** Node types for self-contained leaf elements. */
|
|
45
|
+
export type MdzNodeTypeVoid = 'Hr';
|
|
46
|
+
|
|
47
|
+
/** Discriminant for leaf text nodes (see `MdzNodeTypeContainer` for `Code`'s dual form). */
|
|
48
|
+
export type MdzNodeTypeText = 'Text' | 'Code';
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Open a container node. The renderer starts a new element/wrapper.
|
|
52
|
+
* Children are subsequent opcodes until the matching `close`.
|
|
53
|
+
*/
|
|
54
|
+
export interface MdzOpcodeOpen {
|
|
55
|
+
type: 'open';
|
|
56
|
+
id: MdzNodeId;
|
|
57
|
+
node_type: MdzNodeTypeContainer;
|
|
58
|
+
/** Byte offset in the full input where the opening delimiter begins. */
|
|
59
|
+
start: number;
|
|
60
|
+
/** Heading level (1-6). Present when `node_type` is `'Heading'`. */
|
|
61
|
+
level?: 1 | 2 | 3 | 4 | 5 | 6;
|
|
62
|
+
/** Tag name. Present when `node_type` is `'Element'` or `'Component'`. */
|
|
63
|
+
name?: string;
|
|
64
|
+
/** Language hint. Present when `node_type` is `'Codeblock'`. */
|
|
65
|
+
lang?: string | null;
|
|
66
|
+
/** Whether the list is ordered. Present when `node_type` is `'List'`. */
|
|
67
|
+
ordered?: boolean;
|
|
68
|
+
/** First item's authored number — the `<ol start>` attribute. Present for ordered `'List'`. */
|
|
69
|
+
start_number?: number;
|
|
70
|
+
/** Authored item number. Present when `node_type` is `'ListItem'` in an ordered list. */
|
|
71
|
+
number?: number;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Close a previously opened container node.
|
|
76
|
+
* Carries deferred metadata that wasn't known at open time.
|
|
77
|
+
*/
|
|
78
|
+
export interface MdzOpcodeClose {
|
|
79
|
+
type: 'close';
|
|
80
|
+
id: MdzNodeId;
|
|
81
|
+
/** Byte offset in the full input immediately after the closing delimiter. */
|
|
82
|
+
end: number;
|
|
83
|
+
/** Link URL/path, resolved when `](url)` completes. */
|
|
84
|
+
reference?: string;
|
|
85
|
+
/** Link type, resolved alongside `reference`. */
|
|
86
|
+
link_type?: 'external' | 'internal';
|
|
87
|
+
/** Heading slug, computed from full heading content. */
|
|
88
|
+
heading_id?: string;
|
|
89
|
+
/**
|
|
90
|
+
* If true, consumer drops this node and its descendants from the tree.
|
|
91
|
+
* Used for whitespace-only paragraphs that match nothing in `mdz_parse`'s
|
|
92
|
+
* output — the streaming parser emits open/text speculatively, then
|
|
93
|
+
* retroactively drops the empty wrapper at close.
|
|
94
|
+
*/
|
|
95
|
+
discard?: boolean;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Create a leaf text or code node.
|
|
100
|
+
* The parent is implicit — the innermost open container on the renderer's stack.
|
|
101
|
+
*/
|
|
102
|
+
export interface MdzOpcodeText {
|
|
103
|
+
type: 'text';
|
|
104
|
+
id: MdzNodeId;
|
|
105
|
+
content: string;
|
|
106
|
+
text_type: MdzNodeTypeText;
|
|
107
|
+
/** Byte offset where this node begins (for Code, the opening backtick). */
|
|
108
|
+
start: number;
|
|
109
|
+
/** Byte offset immediately after this node ends (for Code, after the closing backtick). */
|
|
110
|
+
end: number;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Append content to an existing text node.
|
|
115
|
+
* Streaming optimization — avoids creating a new node per chunk
|
|
116
|
+
* during plain text runs.
|
|
117
|
+
*/
|
|
118
|
+
export interface MdzOpcodeAppendText {
|
|
119
|
+
type: 'append_text';
|
|
120
|
+
id: MdzNodeId;
|
|
121
|
+
content: string;
|
|
122
|
+
/**
|
|
123
|
+
* Byte offset immediately after the appended content in the full input.
|
|
124
|
+
* When absent, consumers extend the node's `end` by `content.length`;
|
|
125
|
+
* present when the two differ — list continuation lines strip structural
|
|
126
|
+
* indent from content, so the source span outgrows the content length.
|
|
127
|
+
*/
|
|
128
|
+
end?: number;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Trim `count` characters from the end of an existing text node.
|
|
133
|
+
* If trimming empties the node, the consumer removes it from its parent.
|
|
134
|
+
*
|
|
135
|
+
* Used by paragraph/codeblock close to drop the trailing newline that
|
|
136
|
+
* separates inline content from the block boundary. Emitted instead of
|
|
137
|
+
* retroactively mutating the prior `text`/`append_text` opcode, so the
|
|
138
|
+
* opcode stream is append-only.
|
|
139
|
+
*/
|
|
140
|
+
export interface MdzOpcodeTrimText {
|
|
141
|
+
type: 'trim_text';
|
|
142
|
+
id: MdzNodeId;
|
|
143
|
+
count: number;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Create a self-contained leaf node (e.g., horizontal rule).
|
|
148
|
+
* Inserted as a child of the innermost open container, or at root level.
|
|
149
|
+
*/
|
|
150
|
+
export interface MdzOpcodeVoid {
|
|
151
|
+
type: 'void';
|
|
152
|
+
id: MdzNodeId;
|
|
153
|
+
node_type: MdzNodeTypeVoid;
|
|
154
|
+
/** Byte offset in the full input where this element begins. */
|
|
155
|
+
start: number;
|
|
156
|
+
/** Byte offset immediately after this element ends. */
|
|
157
|
+
end: number;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Undo an optimistic open. Removes the container wrapper,
|
|
162
|
+
* inserts `replacement_text` as literal text at the container's position,
|
|
163
|
+
* and re-parents the container's children to the grandparent.
|
|
164
|
+
*
|
|
165
|
+
* Only inline containers revert — block structure (paragraphs, headings,
|
|
166
|
+
* codeblocks, lists, blockquotes) is decided from bounded input before
|
|
167
|
+
* opening, so block opens are never speculative.
|
|
168
|
+
*/
|
|
169
|
+
export interface MdzOpcodeRevert {
|
|
170
|
+
type: 'revert';
|
|
171
|
+
id: MdzNodeId;
|
|
172
|
+
/** The delimiter text to emit as literal content (e.g., `"**"`, `"["`, `"<Tag>"`). */
|
|
173
|
+
replacement_text: string;
|
|
174
|
+
/** Byte offset of the original opening delimiter in the full input. */
|
|
175
|
+
start: number;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Retroactively wrap an existing text node in a container.
|
|
180
|
+
* Used for text-first auto-links: URL/path text streams as plain text,
|
|
181
|
+
* then gets wrapped in a Link when the URL boundary is found.
|
|
182
|
+
*
|
|
183
|
+
* Specific to text-first auto-detected content — not a general
|
|
184
|
+
* retroactive-container mechanism. Delimiter-paired inlines (bold, italic,
|
|
185
|
+
* strikethrough) use optimistic `open` + `revert` instead: their opening
|
|
186
|
+
* delimiter is an explicit open event, and the delimiters are discarded
|
|
187
|
+
* rather than retained as content.
|
|
188
|
+
*
|
|
189
|
+
* When `trim_end` is set, trailing characters (punctuation) are trimmed
|
|
190
|
+
* from the target text node and placed in a new sibling Text node after
|
|
191
|
+
* the Link wrapper, identified by `trim_id`.
|
|
192
|
+
*/
|
|
193
|
+
export interface MdzOpcodeWrap {
|
|
194
|
+
type: 'wrap';
|
|
195
|
+
/** ID for the new Link container node. */
|
|
196
|
+
id: MdzNodeId;
|
|
197
|
+
/** Container type to wrap in (always `'Link'` for now). */
|
|
198
|
+
node_type: 'Link';
|
|
199
|
+
/** ID of the existing text node to wrap. */
|
|
200
|
+
target_id: MdzNodeId;
|
|
201
|
+
/** Resolved URL or path reference. */
|
|
202
|
+
reference: string;
|
|
203
|
+
/** Whether the link is external (URL) or internal (path). */
|
|
204
|
+
link_type: 'external' | 'internal';
|
|
205
|
+
/** Byte offset where the URL/path begins. */
|
|
206
|
+
start: number;
|
|
207
|
+
/** Byte offset immediately after the URL/path (before any trimmed punctuation). */
|
|
208
|
+
end: number;
|
|
209
|
+
/** Number of trailing chars to trim from target and place after the link. */
|
|
210
|
+
trim_end?: number;
|
|
211
|
+
/** ID for the trimmed-text sibling node. Required when `trim_end` > 0. */
|
|
212
|
+
trim_id?: MdzNodeId;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/** All node types that can appear in the mdz tree. */
|
|
216
|
+
export type MdzNodeType = MdzNodeTypeContainer | MdzNodeTypeVoid | MdzNodeTypeText;
|
|
217
|
+
|
|
218
|
+
/** Discriminated union of all mdz opcodes. */
|
|
219
|
+
export type MdzOpcode =
|
|
220
|
+
| MdzOpcodeOpen
|
|
221
|
+
| MdzOpcodeClose
|
|
222
|
+
| MdzOpcodeText
|
|
223
|
+
| MdzOpcodeAppendText
|
|
224
|
+
| MdzOpcodeTrimText
|
|
225
|
+
| MdzOpcodeVoid
|
|
226
|
+
| MdzOpcodeRevert
|
|
227
|
+
| MdzOpcodeWrap;
|
|
@@ -0,0 +1,470 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Converts a stream of mdz opcodes to the `MdzNode[]` tree representation.
|
|
3
|
+
*
|
|
4
|
+
* Stack-based replay over a single flat children buffer: leaves push to the
|
|
5
|
+
* shared buffer, and each `open` frame records where its children begin
|
|
6
|
+
* (`children_start`). `close` slices the frame's range out into the built
|
|
7
|
+
* node; `revert` dissolves the frame boundary in place — its accumulated
|
|
8
|
+
* children become the parent's without copying — and merges the replacement
|
|
9
|
+
* delimiter text into an adjacent text node. Adjacent text left behind by
|
|
10
|
+
* reverts is coalesced once per container by `mdz_merge_adjacent_text` at
|
|
11
|
+
* close, so cascading reverts of deeply nested unclosed containers cost O(n)
|
|
12
|
+
* total instead of re-parenting (O(n²)) at every level.
|
|
13
|
+
*
|
|
14
|
+
* The output is identical to `mdz_parse()` from `mdz.ts`, enabling
|
|
15
|
+
* validation of the streaming parser against the existing fixture suite.
|
|
16
|
+
*
|
|
17
|
+
* @module
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import {DEV} from 'esm-env';
|
|
21
|
+
|
|
22
|
+
import type {
|
|
23
|
+
MdzNode,
|
|
24
|
+
MdzNodeText,
|
|
25
|
+
MdzNodeLink,
|
|
26
|
+
MdzNodeHeading,
|
|
27
|
+
MdzNodeCode,
|
|
28
|
+
MdzNodeListItem,
|
|
29
|
+
} from './mdz.js';
|
|
30
|
+
import type {MdzOpcode, MdzNodeId} from './mdz_opcodes.js';
|
|
31
|
+
import {mdz_extract_single_tag, mdz_heading_id, mdz_merge_adjacent_text} from './mdz_helpers.js';
|
|
32
|
+
|
|
33
|
+
interface StackFrame {
|
|
34
|
+
id: MdzNodeId;
|
|
35
|
+
node_type: string;
|
|
36
|
+
/** Index into the shared flat children buffer where this frame's children begin. */
|
|
37
|
+
children_start: number;
|
|
38
|
+
start: number;
|
|
39
|
+
// metadata from open opcode
|
|
40
|
+
level?: number;
|
|
41
|
+
name?: string;
|
|
42
|
+
lang?: string | null;
|
|
43
|
+
ordered?: boolean;
|
|
44
|
+
start_number?: number;
|
|
45
|
+
number?: number;
|
|
46
|
+
// metadata from close opcode (deferred)
|
|
47
|
+
end?: number;
|
|
48
|
+
reference?: string;
|
|
49
|
+
link_type?: 'external' | 'internal';
|
|
50
|
+
heading_id?: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Convert an array of mdz opcodes to the `MdzNode[]` tree.
|
|
55
|
+
* Produces output identical to `mdz_parse()`.
|
|
56
|
+
*/
|
|
57
|
+
export const mdz_opcodes_to_nodes = (opcodes: Array<MdzOpcode>): Array<MdzNode> => {
|
|
58
|
+
// flat children buffer shared by all open frames — see module docs.
|
|
59
|
+
// once every frame is closed it holds exactly the top-level nodes.
|
|
60
|
+
const children: Array<MdzNode> = [];
|
|
61
|
+
const stack: Array<StackFrame> = [];
|
|
62
|
+
// index node IDs to their text content (for append_text, trim_text, and wrap)
|
|
63
|
+
const text_nodes = new Map<MdzNodeId, MdzNodeText | MdzNodeCode>();
|
|
64
|
+
|
|
65
|
+
for (const op of opcodes) {
|
|
66
|
+
switch (op.type) {
|
|
67
|
+
case 'open': {
|
|
68
|
+
stack.push({
|
|
69
|
+
id: op.id,
|
|
70
|
+
node_type: op.node_type,
|
|
71
|
+
children_start: children.length,
|
|
72
|
+
start: op.start,
|
|
73
|
+
level: op.level,
|
|
74
|
+
name: op.name,
|
|
75
|
+
lang: op.lang,
|
|
76
|
+
ordered: op.ordered,
|
|
77
|
+
start_number: op.start_number,
|
|
78
|
+
number: op.number,
|
|
79
|
+
});
|
|
80
|
+
break;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
case 'close': {
|
|
84
|
+
if (DEV && stack.length === 0) {
|
|
85
|
+
throw new Error(`mdz_opcodes_to_nodes: close for id ${op.id} but stack is empty`);
|
|
86
|
+
}
|
|
87
|
+
const frame = stack.pop();
|
|
88
|
+
if (!frame) break;
|
|
89
|
+
|
|
90
|
+
// discard signal: the parser determined this node and its descendants
|
|
91
|
+
// should not appear in the tree (e.g. whitespace-only paragraph)
|
|
92
|
+
if (op.discard) {
|
|
93
|
+
children.length = frame.children_start;
|
|
94
|
+
break;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// apply deferred metadata
|
|
98
|
+
frame.end = op.end;
|
|
99
|
+
if (op.reference !== undefined) frame.reference = op.reference;
|
|
100
|
+
if (op.link_type !== undefined) frame.link_type = op.link_type;
|
|
101
|
+
if (op.heading_id !== undefined) frame.heading_id = op.heading_id;
|
|
102
|
+
|
|
103
|
+
const node = build_node(frame, children.splice(frame.children_start));
|
|
104
|
+
if (node) children.push(node);
|
|
105
|
+
break;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
case 'text': {
|
|
109
|
+
const node: MdzNodeText | MdzNodeCode =
|
|
110
|
+
op.text_type === 'Code'
|
|
111
|
+
? {type: 'Code', content: op.content, start: op.start, end: op.end}
|
|
112
|
+
: {type: 'Text', content: op.content, start: op.start, end: op.end};
|
|
113
|
+
text_nodes.set(op.id, node);
|
|
114
|
+
children.push(node);
|
|
115
|
+
break;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
case 'append_text': {
|
|
119
|
+
if (DEV && !text_nodes.has(op.id)) {
|
|
120
|
+
throw new Error(`mdz_opcodes_to_nodes: append_text for unknown id ${op.id}`);
|
|
121
|
+
}
|
|
122
|
+
const existing = text_nodes.get(op.id);
|
|
123
|
+
if (existing) {
|
|
124
|
+
existing.content += op.content;
|
|
125
|
+
// an explicit end covers stripped structural indent (list
|
|
126
|
+
// continuation lines), where content is shorter than its span
|
|
127
|
+
existing.end = op.end ?? existing.end + op.content.length;
|
|
128
|
+
}
|
|
129
|
+
break;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
case 'trim_text': {
|
|
133
|
+
if (DEV && !text_nodes.has(op.id)) {
|
|
134
|
+
throw new Error(`mdz_opcodes_to_nodes: trim_text for unknown id ${op.id}`);
|
|
135
|
+
}
|
|
136
|
+
const existing = text_nodes.get(op.id);
|
|
137
|
+
if (existing) {
|
|
138
|
+
existing.content = existing.content.slice(0, existing.content.length - op.count);
|
|
139
|
+
existing.end -= op.count;
|
|
140
|
+
if (existing.content.length === 0) {
|
|
141
|
+
// trim targets the most recent text run, so the node is near the
|
|
142
|
+
// end of the buffer — backward identity scan
|
|
143
|
+
for (let i = children.length - 1; i >= 0; i--) {
|
|
144
|
+
if (children[i] === existing) {
|
|
145
|
+
children.splice(i, 1);
|
|
146
|
+
break;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
text_nodes.delete(op.id);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
break;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
case 'void': {
|
|
156
|
+
children.push({type: 'Hr', start: op.start, end: op.end});
|
|
157
|
+
break;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
case 'revert': {
|
|
161
|
+
// find and remove the reverted node's frame from the stack.
|
|
162
|
+
// fast path: most reverts are top-down (revert_above, via
|
|
163
|
+
// close_paragraph/close_heading); a failed-closer revert
|
|
164
|
+
// (revert_failed_close) targets a mid-stack frame,
|
|
165
|
+
// dissolving it while the frames above stay open.
|
|
166
|
+
let reverted: StackFrame | null = null;
|
|
167
|
+
let reverted_idx = -1;
|
|
168
|
+
if (stack.length > 0 && stack[stack.length - 1]!.id === op.id) {
|
|
169
|
+
reverted_idx = stack.length - 1;
|
|
170
|
+
reverted = stack.pop()!;
|
|
171
|
+
} else {
|
|
172
|
+
for (let i = stack.length - 1; i >= 0; i--) {
|
|
173
|
+
if (stack[i]!.id === op.id) {
|
|
174
|
+
reverted_idx = i;
|
|
175
|
+
reverted = stack.splice(i, 1)[0]!;
|
|
176
|
+
break;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (DEV && !reverted) {
|
|
182
|
+
throw new Error(`mdz_opcodes_to_nodes: revert for id ${op.id} but not found on stack`);
|
|
183
|
+
}
|
|
184
|
+
if (!reverted) break;
|
|
185
|
+
|
|
186
|
+
if (op.replacement_text) {
|
|
187
|
+
// the frame dissolves — its children already sit in the buffer
|
|
188
|
+
// and now belong to the parent; only the replacement delimiter
|
|
189
|
+
// text needs to materialize at the frame's open position.
|
|
190
|
+
insert_replacement(children, stack, reverted_idx, reverted.children_start, op);
|
|
191
|
+
}
|
|
192
|
+
break;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
case 'wrap': {
|
|
196
|
+
const text_node = text_nodes.get(op.target_id);
|
|
197
|
+
if (!text_node) break;
|
|
198
|
+
|
|
199
|
+
// wraps always target a text node in the still-open container, near
|
|
200
|
+
// the end of the buffer — backward identity scan
|
|
201
|
+
let idx = -1;
|
|
202
|
+
for (let i = children.length - 1; i >= 0; i--) {
|
|
203
|
+
if (children[i] === text_node) {
|
|
204
|
+
idx = i;
|
|
205
|
+
break;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
if (idx === -1) break;
|
|
209
|
+
|
|
210
|
+
// handle trailing punctuation trim
|
|
211
|
+
let trimmed_node: MdzNodeText | null = null;
|
|
212
|
+
if (op.trim_end && op.trim_end > 0 && op.trim_id != null) {
|
|
213
|
+
const trimmed_content = text_node.content.slice(text_node.content.length - op.trim_end);
|
|
214
|
+
text_node.content = text_node.content.slice(0, text_node.content.length - op.trim_end);
|
|
215
|
+
text_node.end -= op.trim_end;
|
|
216
|
+
trimmed_node = {
|
|
217
|
+
type: 'Text',
|
|
218
|
+
content: trimmed_content,
|
|
219
|
+
start: text_node.end,
|
|
220
|
+
end: text_node.end + trimmed_content.length,
|
|
221
|
+
};
|
|
222
|
+
text_nodes.set(op.trim_id, trimmed_node);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// create Link wrapping the text node
|
|
226
|
+
const link: MdzNodeLink = {
|
|
227
|
+
type: 'Link',
|
|
228
|
+
reference: op.reference,
|
|
229
|
+
children: [text_node],
|
|
230
|
+
link_type: op.link_type,
|
|
231
|
+
start: op.start,
|
|
232
|
+
end: op.end,
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
// replace text node with [Link, trimmed?] in the buffer
|
|
236
|
+
if (trimmed_node) {
|
|
237
|
+
children.splice(idx, 1, link as MdzNode, trimmed_node as MdzNode);
|
|
238
|
+
} else {
|
|
239
|
+
children[idx] = link;
|
|
240
|
+
}
|
|
241
|
+
break;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
return children;
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Materialize an inline revert's replacement text at `at` (the dissolved
|
|
251
|
+
* frame's `children_start`), merging into an adjacent Text node when possible
|
|
252
|
+
* so the hot path — cascading reverts of nested unclosed containers — never
|
|
253
|
+
* shifts buffer elements:
|
|
254
|
+
*
|
|
255
|
+
* 1. append to the preceding sibling when it's a Text within the parent's range
|
|
256
|
+
* 2. else prepend into the frame's first child when it's a Text (top-of-stack
|
|
257
|
+
* reverts only — for a mid-stack revert the node at `at` may belong to a
|
|
258
|
+
* still-open inner frame)
|
|
259
|
+
* 3. else splice in a standalone Text node
|
|
260
|
+
*
|
|
261
|
+
* Mutating the adjacent nodes is safe: a `revert` seals the parser's active
|
|
262
|
+
* text run (no later `append_text`/`trim_text` targets them), and pending
|
|
263
|
+
* auto-link wraps always complete before an enclosing container reverts.
|
|
264
|
+
* Adjacent Text nodes this leaves behind are coalesced by
|
|
265
|
+
* `mdz_merge_adjacent_text` when the enclosing container closes.
|
|
266
|
+
*
|
|
267
|
+
* @mutates `children` and possibly the `children_start` of open frames on `stack`
|
|
268
|
+
*/
|
|
269
|
+
const insert_replacement = (
|
|
270
|
+
children: Array<MdzNode>,
|
|
271
|
+
stack: Array<StackFrame>,
|
|
272
|
+
reverted_idx: number,
|
|
273
|
+
at: number,
|
|
274
|
+
op: {replacement_text?: string; start: number},
|
|
275
|
+
): void => {
|
|
276
|
+
const text = op.replacement_text!;
|
|
277
|
+
const parent_start = reverted_idx > 0 ? stack[reverted_idx - 1]!.children_start : 0;
|
|
278
|
+
// 1. append to the preceding sibling within the parent's range
|
|
279
|
+
if (at > parent_start) {
|
|
280
|
+
const prev = children[at - 1]!;
|
|
281
|
+
if (prev.type === 'Text') {
|
|
282
|
+
prev.content += text;
|
|
283
|
+
prev.end = op.start + text.length;
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
// after pop/splice, the frame was top of stack iff its index equals the new length
|
|
288
|
+
const was_top = reverted_idx === stack.length;
|
|
289
|
+
// 2. prepend into the frame's first child
|
|
290
|
+
if (was_top && at < children.length) {
|
|
291
|
+
const first = children[at]!;
|
|
292
|
+
if (first.type === 'Text') {
|
|
293
|
+
first.content = text + first.content;
|
|
294
|
+
first.start = op.start;
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
// 3. standalone Text node
|
|
299
|
+
const node: MdzNode = {type: 'Text', content: text, start: op.start, end: op.start + text.length};
|
|
300
|
+
if (at >= children.length) {
|
|
301
|
+
children.push(node);
|
|
302
|
+
} else {
|
|
303
|
+
children.splice(at, 0, node);
|
|
304
|
+
}
|
|
305
|
+
// shift the ranges of open frames above the reverted one (they opened
|
|
306
|
+
// later, so their children_start is at or after the insertion point) —
|
|
307
|
+
// also when the insert was an append: a mid-stack revert can land at the
|
|
308
|
+
// buffer end while childless frames above share the same children_start,
|
|
309
|
+
// and without the shift the replacement text falls inside their ranges
|
|
310
|
+
for (let i = reverted_idx; i < stack.length; i++) {
|
|
311
|
+
stack[i]!.children_start++;
|
|
312
|
+
}
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Build an `MdzNode` from a completed stack frame and its children
|
|
317
|
+
* (sliced out of the shared flat buffer by the caller).
|
|
318
|
+
*/
|
|
319
|
+
const build_node = (frame: StackFrame, frame_children: Array<MdzNode>): MdzNode | null => {
|
|
320
|
+
switch (frame.node_type) {
|
|
321
|
+
case 'Paragraph': {
|
|
322
|
+
const children = mdz_merge_adjacent_text(frame_children);
|
|
323
|
+
if (children.length === 0) return null;
|
|
324
|
+
|
|
325
|
+
// extract single tag (MDX convention)
|
|
326
|
+
const single_tag = mdz_extract_single_tag(children);
|
|
327
|
+
if (single_tag) return single_tag;
|
|
328
|
+
|
|
329
|
+
return {
|
|
330
|
+
type: 'Paragraph',
|
|
331
|
+
children,
|
|
332
|
+
start: children[0]!.start,
|
|
333
|
+
end: children[children.length - 1]!.end,
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
case 'Heading': {
|
|
338
|
+
const children = mdz_merge_adjacent_text(frame_children);
|
|
339
|
+
const id = frame.heading_id ?? mdz_heading_id(children);
|
|
340
|
+
return {
|
|
341
|
+
type: 'Heading',
|
|
342
|
+
level: frame.level ?? 1,
|
|
343
|
+
id,
|
|
344
|
+
children,
|
|
345
|
+
start: frame.start,
|
|
346
|
+
end: frame.end!,
|
|
347
|
+
} as MdzNodeHeading;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
case 'Bold':
|
|
351
|
+
return {
|
|
352
|
+
type: 'Bold',
|
|
353
|
+
children: mdz_merge_adjacent_text(frame_children),
|
|
354
|
+
start: frame.start,
|
|
355
|
+
end: frame.end!,
|
|
356
|
+
};
|
|
357
|
+
|
|
358
|
+
case 'Italic':
|
|
359
|
+
return {
|
|
360
|
+
type: 'Italic',
|
|
361
|
+
children: mdz_merge_adjacent_text(frame_children),
|
|
362
|
+
start: frame.start,
|
|
363
|
+
end: frame.end!,
|
|
364
|
+
};
|
|
365
|
+
|
|
366
|
+
case 'Strikethrough':
|
|
367
|
+
return {
|
|
368
|
+
type: 'Strikethrough',
|
|
369
|
+
children: mdz_merge_adjacent_text(frame_children),
|
|
370
|
+
start: frame.start,
|
|
371
|
+
end: frame.end!,
|
|
372
|
+
};
|
|
373
|
+
|
|
374
|
+
case 'Link':
|
|
375
|
+
return {
|
|
376
|
+
type: 'Link',
|
|
377
|
+
reference: frame.reference ?? '',
|
|
378
|
+
children: mdz_merge_adjacent_text(frame_children),
|
|
379
|
+
link_type: frame.link_type ?? 'internal',
|
|
380
|
+
start: frame.start,
|
|
381
|
+
end: frame.end!,
|
|
382
|
+
};
|
|
383
|
+
|
|
384
|
+
case 'List': {
|
|
385
|
+
// items are the only possible children; `end` derives from the last
|
|
386
|
+
// one, matching the sync token parser (the close offset is cosmetic)
|
|
387
|
+
const children = frame_children as Array<MdzNodeListItem>;
|
|
388
|
+
const end = children.length > 0 ? children[children.length - 1]!.end : frame.end!;
|
|
389
|
+
if (frame.ordered) {
|
|
390
|
+
return {
|
|
391
|
+
type: 'List',
|
|
392
|
+
ordered: true,
|
|
393
|
+
start_number: frame.start_number ?? 1,
|
|
394
|
+
children,
|
|
395
|
+
start: frame.start,
|
|
396
|
+
end,
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
return {type: 'List', ordered: false, children, start: frame.start, end};
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
case 'ListItem': {
|
|
403
|
+
const children = mdz_merge_adjacent_text(frame_children);
|
|
404
|
+
// childless means an empty item — its close carried the marker end
|
|
405
|
+
const end = children.length > 0 ? children[children.length - 1]!.end : frame.end!;
|
|
406
|
+
if (frame.number !== undefined) {
|
|
407
|
+
return {type: 'ListItem', number: frame.number, children, start: frame.start, end};
|
|
408
|
+
}
|
|
409
|
+
return {type: 'ListItem', children, start: frame.start, end};
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
case 'Blockquote': {
|
|
413
|
+
// block children only — `end` derives from the last one, matching the
|
|
414
|
+
// sync token parser (the close offset is cosmetic)
|
|
415
|
+
const end =
|
|
416
|
+
frame_children.length > 0 ? frame_children[frame_children.length - 1]!.end : frame.end!;
|
|
417
|
+
return {type: 'Blockquote', children: frame_children, start: frame.start, end};
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
case 'Code': {
|
|
421
|
+
// optimistic inline code container — concatenate children text into leaf MdzNodeCode
|
|
422
|
+
let content = '';
|
|
423
|
+
for (const c of frame_children) {
|
|
424
|
+
if (c.type === 'Text') content += c.content;
|
|
425
|
+
}
|
|
426
|
+
return {
|
|
427
|
+
type: 'Code',
|
|
428
|
+
content,
|
|
429
|
+
start: frame.start,
|
|
430
|
+
end: frame.end!,
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
case 'Codeblock': {
|
|
435
|
+
// code block content is in children as text
|
|
436
|
+
let content = '';
|
|
437
|
+
for (const c of frame_children) {
|
|
438
|
+
if (c.type === 'Text') content += c.content;
|
|
439
|
+
}
|
|
440
|
+
return {
|
|
441
|
+
type: 'Codeblock',
|
|
442
|
+
lang: frame.lang ?? null,
|
|
443
|
+
content,
|
|
444
|
+
start: frame.start,
|
|
445
|
+
end: frame.end!,
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
case 'Element':
|
|
450
|
+
return {
|
|
451
|
+
type: 'Element',
|
|
452
|
+
name: frame.name ?? '',
|
|
453
|
+
children: mdz_merge_adjacent_text(frame_children),
|
|
454
|
+
start: frame.start,
|
|
455
|
+
end: frame.end!,
|
|
456
|
+
};
|
|
457
|
+
|
|
458
|
+
case 'Component':
|
|
459
|
+
return {
|
|
460
|
+
type: 'Component',
|
|
461
|
+
name: frame.name ?? '',
|
|
462
|
+
children: mdz_merge_adjacent_text(frame_children),
|
|
463
|
+
start: frame.start,
|
|
464
|
+
end: frame.end!,
|
|
465
|
+
};
|
|
466
|
+
|
|
467
|
+
default:
|
|
468
|
+
return null;
|
|
469
|
+
}
|
|
470
|
+
};
|