@fuzdev/mdz 0.1.0 → 0.3.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 +15 -7
- package/dist/Mdz.svelte.d.ts +9 -2
- package/dist/Mdz.svelte.d.ts.map +1 -1
- package/dist/MdzNodeView.svelte +130 -95
- package/dist/MdzNodeView.svelte.d.ts +6 -2
- package/dist/MdzNodeView.svelte.d.ts.map +1 -1
- package/dist/MdzStream.svelte +3 -3
- package/dist/MdzStream.svelte.d.ts.map +1 -1
- package/dist/MdzStreamNodeView.svelte +129 -95
- package/dist/MdzStreamNodeView.svelte.d.ts +6 -2
- package/dist/MdzStreamNodeView.svelte.d.ts.map +1 -1
- package/dist/context_helpers.d.ts +1 -1
- package/dist/mdz.d.ts +59 -2
- package/dist/mdz.d.ts.map +1 -1
- package/dist/mdz.js +37 -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 +96 -1
- package/dist/mdz_helpers.d.ts.map +1 -1
- package/dist/mdz_helpers.js +192 -0
- package/dist/mdz_lexer.d.ts +47 -1
- package/dist/mdz_lexer.d.ts.map +1 -1
- package/dist/mdz_lexer.js +364 -15
- package/dist/mdz_opcodes.d.ts +6 -1
- 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 +26 -0
- package/dist/mdz_stream_parser.d.ts.map +1 -1
- package/dist/mdz_stream_parser.js +20 -1
- package/dist/mdz_stream_parser_inline.d.ts.map +1 -1
- package/dist/mdz_stream_parser_inline.js +19 -17
- 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 +32 -3
- package/dist/mdz_stream_parser_state.d.ts +41 -0
- package/dist/mdz_stream_parser_state.d.ts.map +1 -1
- package/dist/mdz_stream_parser_state.js +53 -3
- package/dist/mdz_stream_parser_table.d.ts +73 -0
- package/dist/mdz_stream_parser_table.d.ts.map +1 -0
- package/dist/mdz_stream_parser_table.js +362 -0
- package/dist/mdz_stream_parser_text.d.ts.map +1 -1
- package/dist/mdz_stream_parser_text.js +35 -24
- package/dist/mdz_stream_state.svelte.d.ts +5 -0
- package/dist/mdz_stream_state.svelte.d.ts.map +1 -1
- package/dist/mdz_stream_state.svelte.js +8 -0
- package/dist/mdz_to_svelte.d.ts.map +1 -1
- package/dist/mdz_to_svelte.js +51 -20
- package/dist/mdz_token_parser.d.ts +16 -0
- package/dist/mdz_token_parser.d.ts.map +1 -1
- package/dist/mdz_token_parser.js +112 -14
- package/dist/svelte_preprocess_mdz.js +33 -39
- package/package.json +11 -8
- package/src/lib/context_helpers.ts +1 -1
- package/src/lib/mdz.ts +79 -3
- package/src/lib/mdz_contexts.ts +1 -1
- package/src/lib/mdz_debug_work.ts +38 -0
- package/src/lib/mdz_helpers.ts +222 -1
- package/src/lib/mdz_lexer.ts +420 -21
- package/src/lib/mdz_opcodes.ts +10 -1
- package/src/lib/mdz_opcodes_to_nodes.ts +37 -3
- package/src/lib/mdz_stream_parser.ts +26 -9
- package/src/lib/mdz_stream_parser_block.ts +2 -2
- package/src/lib/mdz_stream_parser_inline.ts +25 -21
- package/src/lib/mdz_stream_parser_link.ts +92 -59
- package/src/lib/mdz_stream_parser_list.ts +34 -6
- package/src/lib/mdz_stream_parser_state.ts +87 -5
- package/src/lib/mdz_stream_parser_table.ts +420 -0
- package/src/lib/mdz_stream_parser_text.ts +40 -39
- package/src/lib/mdz_stream_parser_url.ts +2 -2
- package/src/lib/mdz_stream_state.svelte.ts +8 -1
- package/src/lib/mdz_to_svelte.ts +49 -23
- package/src/lib/mdz_token_parser.ts +135 -21
- package/src/lib/svelte_preprocess_mdz.ts +37 -38
- package/src/lib/tsdoc_mdz.ts +1 -1
package/dist/mdz_lexer.d.ts
CHANGED
|
@@ -6,11 +6,12 @@
|
|
|
6
6
|
*
|
|
7
7
|
* @module
|
|
8
8
|
*/
|
|
9
|
+
import type { MdzTableAlign } from './mdz.js';
|
|
9
10
|
export interface MdzTokenBase {
|
|
10
11
|
start: number;
|
|
11
12
|
end: number;
|
|
12
13
|
}
|
|
13
|
-
export type MdzToken = MdzTokenText | MdzTokenCode | MdzTokenCodeblock | MdzTokenBoldOpen | MdzTokenBoldClose | MdzTokenItalicOpen | MdzTokenItalicClose | MdzTokenStrikethroughOpen | MdzTokenStrikethroughClose | MdzTokenLinkTextOpen | MdzTokenLinkTextClose | MdzTokenLinkRef | MdzTokenAutolink | MdzTokenHeadingStart | MdzTokenHr | MdzTokenTagOpen | MdzTokenTagSelfClose | MdzTokenTagClose | MdzTokenHeadingEnd | MdzTokenParagraphBreak | MdzTokenListOpen | MdzTokenListClose | MdzTokenListItemOpen | MdzTokenListItemClose | MdzTokenBlockquoteOpen | MdzTokenBlockquoteClose;
|
|
14
|
+
export type MdzToken = MdzTokenText | MdzTokenCode | MdzTokenCodeblock | MdzTokenBoldOpen | MdzTokenBoldClose | MdzTokenItalicOpen | MdzTokenItalicClose | MdzTokenStrikethroughOpen | MdzTokenStrikethroughClose | MdzTokenLinkTextOpen | MdzTokenLinkTextClose | MdzTokenLinkRef | MdzTokenAutolink | MdzTokenHeadingStart | MdzTokenHr | MdzTokenTagOpen | MdzTokenTagSelfClose | MdzTokenTagClose | MdzTokenHeadingEnd | MdzTokenParagraphBreak | MdzTokenListOpen | MdzTokenListClose | MdzTokenListItemOpen | MdzTokenListItemClose | MdzTokenBlockquoteOpen | MdzTokenBlockquoteClose | MdzTokenTableOpen | MdzTokenTableClose | MdzTokenTableRowOpen | MdzTokenTableRowClose | MdzTokenTableCellOpen | MdzTokenTableCellClose;
|
|
14
15
|
export interface MdzTokenText extends MdzTokenBase {
|
|
15
16
|
type: 'text';
|
|
16
17
|
content: string;
|
|
@@ -106,6 +107,28 @@ export interface MdzTokenBlockquoteOpen extends MdzTokenBase {
|
|
|
106
107
|
export interface MdzTokenBlockquoteClose extends MdzTokenBase {
|
|
107
108
|
type: 'blockquote_close';
|
|
108
109
|
}
|
|
110
|
+
export interface MdzTokenTableOpen extends MdzTokenBase {
|
|
111
|
+
type: 'table_open';
|
|
112
|
+
/** Per-column alignment from the delimiter row; length is the column count. */
|
|
113
|
+
align: Array<MdzTableAlign>;
|
|
114
|
+
}
|
|
115
|
+
export interface MdzTokenTableClose extends MdzTokenBase {
|
|
116
|
+
type: 'table_close';
|
|
117
|
+
}
|
|
118
|
+
export interface MdzTokenTableRowOpen extends MdzTokenBase {
|
|
119
|
+
type: 'table_row_open';
|
|
120
|
+
/** `true` for the header row, `false` for body rows. */
|
|
121
|
+
header: boolean;
|
|
122
|
+
}
|
|
123
|
+
export interface MdzTokenTableRowClose extends MdzTokenBase {
|
|
124
|
+
type: 'table_row_close';
|
|
125
|
+
}
|
|
126
|
+
export interface MdzTokenTableCellOpen extends MdzTokenBase {
|
|
127
|
+
type: 'table_cell_open';
|
|
128
|
+
}
|
|
129
|
+
export interface MdzTokenTableCellClose extends MdzTokenBase {
|
|
130
|
+
type: 'table_cell_close';
|
|
131
|
+
}
|
|
109
132
|
/**
|
|
110
133
|
* Tokenizes mdz input into a flat `MdzToken` stream for `MdzTokenParser`.
|
|
111
134
|
*
|
|
@@ -118,6 +141,29 @@ export interface MdzTokenBlockquoteClose extends MdzTokenBase {
|
|
|
118
141
|
export declare class MdzLexer {
|
|
119
142
|
#private;
|
|
120
143
|
constructor(text: string);
|
|
144
|
+
/**
|
|
145
|
+
* Rebind to a new text, so one lexer instance can be reused across inputs —
|
|
146
|
+
* the streaming table path reuses one across a table's rows via
|
|
147
|
+
* `MdzTableCellParser` instead of allocating a lexer (and its memo `Map`)
|
|
148
|
+
* per row. The search memos survive when the text is identical (they're
|
|
149
|
+
* valid over the immutable text — the common case for rows parsed from one
|
|
150
|
+
* buffered chunk) and clear otherwise. Per-parse cursor state is reset by
|
|
151
|
+
* `lex_table_cell` itself.
|
|
152
|
+
*
|
|
153
|
+
* @nodocs
|
|
154
|
+
*/
|
|
155
|
+
rebind(text: string): void;
|
|
121
156
|
tokenize(): Array<MdzToken>;
|
|
157
|
+
/**
|
|
158
|
+
* Tokenize a table cell's inline content `[cell_start, cell_end)` as a
|
|
159
|
+
* bounded inline run, with no wrapping cell tokens (the bare form of
|
|
160
|
+
* `#emit_table_cell`, sharing `#tokenize_cell_inline`). The streaming parser
|
|
161
|
+
* reuses it across a row's cells via `MdzTableCellParser`: the token buffer
|
|
162
|
+
* resets per call while the search memo — valid over the immutable bound
|
|
163
|
+
* text — carries across cells (the same reuse the sync lexer does in place).
|
|
164
|
+
*
|
|
165
|
+
* @nodocs
|
|
166
|
+
*/
|
|
167
|
+
lex_table_cell(cell_start: number, cell_end: number): Array<MdzToken>;
|
|
122
168
|
}
|
|
123
169
|
//# sourceMappingURL=mdz_lexer.d.ts.map
|
package/dist/mdz_lexer.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mdz_lexer.d.ts","sourceRoot":"../src/lib/","sources":["../src/lib/mdz_lexer.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;
|
|
1
|
+
{"version":3,"file":"mdz_lexer.d.ts","sourceRoot":"../src/lib/","sources":["../src/lib/mdz_lexer.ts"],"names":[],"mappings":"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"}
|
package/dist/mdz_lexer.js
CHANGED
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
*
|
|
7
7
|
* @module
|
|
8
8
|
*/
|
|
9
|
-
import { mdz_is_url, mdz_is_safe_reference, is_letter, is_tag_name_char, is_word_char, is_valid_path_char, mdz_trim_url_punctuation, BACKTICK, ASTERISK, UNDERSCORE, TILDE, NEWLINE, HYPHEN, HASH, SPACE, LEFT_ANGLE, RIGHT_ANGLE, SLASH, PERIOD, LEFT_BRACKET, LEFT_PAREN, RIGHT_BRACKET, A_UPPER, Z_UPPER, ZERO, ONE, NINE, HR_HYPHEN_COUNT, MIN_CODEBLOCK_BACKTICKS, MAX_HEADING_LEVEL, MAX_LIST_NUMBER_DIGITS, match_url_prefix_case_insensitive, is_at_absolute_path, is_at_relative_path, is_line_whitespace, mdz_match_blockquote_prefix, mdz_blockquote_line_has_content, mdz_blockquote_strip_width, mdz_remap_segments, } from './mdz_helpers.js';
|
|
9
|
+
import { mdz_is_url, mdz_is_safe_reference, is_letter, is_tag_name_char, is_word_char, is_valid_path_char, mdz_trim_url_punctuation, BACKTICK, ASTERISK, UNDERSCORE, TILDE, NEWLINE, HYPHEN, HASH, SPACE, LEFT_ANGLE, RIGHT_ANGLE, SLASH, PERIOD, LEFT_BRACKET, LEFT_PAREN, RIGHT_BRACKET, A_UPPER, Z_UPPER, ZERO, ONE, NINE, HR_HYPHEN_COUNT, MIN_CODEBLOCK_BACKTICKS, MAX_HEADING_LEVEL, MAX_INLINE_NESTING_DEPTH, MAX_LIST_NUMBER_DIGITS, match_url_prefix_case_insensitive, is_at_absolute_path, is_at_relative_path, is_line_whitespace, mdz_match_blockquote_prefix, mdz_blockquote_line_has_content, mdz_blockquote_strip_width, mdz_remap_segments, mdz_split_table_row, mdz_parse_table_delimiter, PIPE, TEXT_CLASS_BACKSLASH, TEXT_CLASS_NEWLINE, TEXT_CLASS_STOP, TEXT_CLASS_URL, TEXT_SCAN_CLASS, } from './mdz_helpers.js';
|
|
10
|
+
import { mdz_debug_work } from './mdz_debug_work.js';
|
|
10
11
|
//
|
|
11
12
|
// Lexer
|
|
12
13
|
//
|
|
@@ -33,9 +34,73 @@ export class MdzLexer {
|
|
|
33
34
|
* scanner pre-validated every line in the run as continuation).
|
|
34
35
|
*/
|
|
35
36
|
#list_run_strip = false;
|
|
37
|
+
/**
|
|
38
|
+
* Whether `#tokenize_text` is inside a table cell: `#max_search_index` is a
|
|
39
|
+
* hard stop (the cell's trimmed end, mid-line with no newline to halt on)
|
|
40
|
+
* and `\|` unescapes to a literal pipe.
|
|
41
|
+
*/
|
|
42
|
+
#in_table_cell = false;
|
|
43
|
+
/**
|
|
44
|
+
* Start offsets of markdown-link opens (`[`) that definitively failed — so
|
|
45
|
+
* a re-lex from before the `[` short-circuits to literal text instead of
|
|
46
|
+
* re-attempting (and re-descending into) the same failing link. Without it,
|
|
47
|
+
* the revert-and-re-tokenize failure path is exponential on nested `[`
|
|
48
|
+
* (a ~30-byte `[`×n input hangs). A link's outcome (does a valid `](ref)`
|
|
49
|
+
* follow?) is independent of `#max_search_index`, so `start` alone keys it.
|
|
50
|
+
* The outcome is a pure function of the immutable text, so the memo is valid
|
|
51
|
+
* for the whole lex and across table-cell ranges; cleared on `rebind`.
|
|
52
|
+
*/
|
|
53
|
+
#failed_link_starts = new Set();
|
|
54
|
+
/**
|
|
55
|
+
* The tag analogue of `#failed_link_starts`, keyed `${start}:${bound}`. A
|
|
56
|
+
* tag's closer search respects `#max_search_index`, so the same `start` can
|
|
57
|
+
* fail under a narrow bound yet succeed under the wider one a re-lex uses —
|
|
58
|
+
* the bound is part of the key.
|
|
59
|
+
*/
|
|
60
|
+
#failed_tag = new Set();
|
|
61
|
+
/**
|
|
62
|
+
* Current link/tag nesting depth — the number of open
|
|
63
|
+
* `link_text_open`/`tag_open` containers being recursed into. Bumped around
|
|
64
|
+
* `#tokenize_markdown_link`/`#tokenize_tag`'s children loops (not by
|
|
65
|
+
* delimiters, which can't nest deeply — see `MAX_INLINE_NESTING_DEPTH`). At
|
|
66
|
+
* the cap a further `[`/`<` renders literal instead of opening, bounding
|
|
67
|
+
* recursion depth (no stack overflow) and, with the failure memos, the
|
|
68
|
+
* revert re-scan (linear, not quadratic) on adversarial nesting.
|
|
69
|
+
*/
|
|
70
|
+
#inline_depth = 0;
|
|
71
|
+
/**
|
|
72
|
+
* Start offsets of `[`/`<` openers suppressed by the nesting-depth cap
|
|
73
|
+
* (`#inline_depth >= MAX_INLINE_NESTING_DEPTH`) — position-keyed like
|
|
74
|
+
* `#failed_link_starts` so a re-lex from a shallower depth short-circuits to
|
|
75
|
+
* literal text rather than re-opening the construct (which would reintroduce
|
|
76
|
+
* the quadratic the cap removes). A suppression is bound-independent, so
|
|
77
|
+
* `start` alone keys it; cleared on `rebind`.
|
|
78
|
+
*/
|
|
79
|
+
#depth_capped = new Set();
|
|
36
80
|
constructor(text) {
|
|
37
81
|
this.#text = text;
|
|
38
82
|
}
|
|
83
|
+
/**
|
|
84
|
+
* Rebind to a new text, so one lexer instance can be reused across inputs —
|
|
85
|
+
* the streaming table path reuses one across a table's rows via
|
|
86
|
+
* `MdzTableCellParser` instead of allocating a lexer (and its memo `Map`)
|
|
87
|
+
* per row. The search memos survive when the text is identical (they're
|
|
88
|
+
* valid over the immutable text — the common case for rows parsed from one
|
|
89
|
+
* buffered chunk) and clear otherwise. Per-parse cursor state is reset by
|
|
90
|
+
* `lex_table_cell` itself.
|
|
91
|
+
*
|
|
92
|
+
* @nodocs
|
|
93
|
+
*/
|
|
94
|
+
rebind(text) {
|
|
95
|
+
if (this.#text === text)
|
|
96
|
+
return;
|
|
97
|
+
this.#text = text;
|
|
98
|
+
this.#search_memo.clear();
|
|
99
|
+
this.#break_memo = null;
|
|
100
|
+
this.#failed_link_starts.clear();
|
|
101
|
+
this.#failed_tag.clear();
|
|
102
|
+
this.#depth_capped.clear();
|
|
103
|
+
}
|
|
39
104
|
/**
|
|
40
105
|
* Memoized `String#indexOf` over the immutable text.
|
|
41
106
|
*
|
|
@@ -52,6 +117,10 @@ export class MdzLexer {
|
|
|
52
117
|
return memo.result;
|
|
53
118
|
}
|
|
54
119
|
const result = this.#text.indexOf(needle, from);
|
|
120
|
+
if (mdz_debug_work.enabled) {
|
|
121
|
+
// scan span: `indexOf` examined [from, result] (found) or [from, EOF)
|
|
122
|
+
mdz_debug_work.total += (result === -1 ? this.#text.length : result) - from;
|
|
123
|
+
}
|
|
55
124
|
if (memo === undefined) {
|
|
56
125
|
this.#search_memo.set(needle, { from, result });
|
|
57
126
|
}
|
|
@@ -77,6 +146,8 @@ export class MdzLexer {
|
|
|
77
146
|
continue;
|
|
78
147
|
if (this.#tokenize_blockquote())
|
|
79
148
|
continue;
|
|
149
|
+
if (this.#tokenize_table())
|
|
150
|
+
continue;
|
|
80
151
|
}
|
|
81
152
|
// Check for paragraph break
|
|
82
153
|
if (this.#is_at_paragraph_break()) {
|
|
@@ -90,6 +161,24 @@ export class MdzLexer {
|
|
|
90
161
|
}
|
|
91
162
|
return this.#tokens;
|
|
92
163
|
}
|
|
164
|
+
/**
|
|
165
|
+
* Tokenize a table cell's inline content `[cell_start, cell_end)` as a
|
|
166
|
+
* bounded inline run, with no wrapping cell tokens (the bare form of
|
|
167
|
+
* `#emit_table_cell`, sharing `#tokenize_cell_inline`). The streaming parser
|
|
168
|
+
* reuses it across a row's cells via `MdzTableCellParser`: the token buffer
|
|
169
|
+
* resets per call while the search memo — valid over the immutable bound
|
|
170
|
+
* text — carries across cells (the same reuse the sync lexer does in place).
|
|
171
|
+
*
|
|
172
|
+
* @nodocs
|
|
173
|
+
*/
|
|
174
|
+
lex_table_cell(cell_start, cell_end) {
|
|
175
|
+
this.#tokens = [];
|
|
176
|
+
// depth is balanced back to 0 per cell, but pin it so a future early-return
|
|
177
|
+
// between an `#inline_depth++` and its decrement can't leak into the next cell
|
|
178
|
+
this.#inline_depth = 0;
|
|
179
|
+
this.#tokenize_cell_inline(cell_start, cell_end);
|
|
180
|
+
return this.#tokens;
|
|
181
|
+
}
|
|
93
182
|
#is_at_column_0() {
|
|
94
183
|
return this.#index === 0 || this.#text.charCodeAt(this.#index - 1) === NEWLINE;
|
|
95
184
|
}
|
|
@@ -350,6 +439,156 @@ export class MdzLexer {
|
|
|
350
439
|
this.#tokens.push({ type: 'blockquote_close', start: last_line_end, end: last_line_end });
|
|
351
440
|
this.#index = last_line_end;
|
|
352
441
|
}
|
|
442
|
+
// -- Table tokenizers --
|
|
443
|
+
/**
|
|
444
|
+
* Tokenize a table starting at a column-0 pipe row, emitting
|
|
445
|
+
* `table_open`/`table_row_open`/`table_cell_open` … structure tokens (the
|
|
446
|
+
* header row's cells first, then each body row's). Returns false with no
|
|
447
|
+
* side effects when the current line isn't a table: not a `| … |` row, or
|
|
448
|
+
* not followed by a delimiter row whose column count matches the header. The
|
|
449
|
+
* delimiter row's colons set `align`; body rows continue until a blank line,
|
|
450
|
+
* a non-pipe-row line, or EOF.
|
|
451
|
+
*/
|
|
452
|
+
#tokenize_table() {
|
|
453
|
+
const table_start = this.#index;
|
|
454
|
+
let header_end = this.#index_of('\n', this.#index);
|
|
455
|
+
if (header_end === -1)
|
|
456
|
+
header_end = this.#text.length;
|
|
457
|
+
const header_cells = mdz_split_table_row(this.#text, this.#index, header_end);
|
|
458
|
+
if (header_cells === null)
|
|
459
|
+
return false;
|
|
460
|
+
if (header_end >= this.#text.length)
|
|
461
|
+
return false; // a header needs a delimiter row beneath it
|
|
462
|
+
const delim_start = header_end + 1;
|
|
463
|
+
let delim_end = this.#index_of('\n', delim_start);
|
|
464
|
+
if (delim_end === -1)
|
|
465
|
+
delim_end = this.#text.length;
|
|
466
|
+
const align = mdz_parse_table_delimiter(this.#text, delim_start, delim_end);
|
|
467
|
+
if (align === null || align.length !== header_cells.length)
|
|
468
|
+
return false;
|
|
469
|
+
// committed — emit the table; the delimiter row is structural (not emitted)
|
|
470
|
+
this.#tokens.push({ type: 'table_open', align, start: table_start, end: header_end });
|
|
471
|
+
this.#emit_table_row(header_cells, true, table_start, header_end);
|
|
472
|
+
this.#index = delim_end;
|
|
473
|
+
let table_end = delim_end;
|
|
474
|
+
// body rows: consecutive valid pipe rows, until a blank/non-row line or EOF
|
|
475
|
+
while (this.#index < this.#text.length) {
|
|
476
|
+
const row_start = this.#index + 1; // past the newline at #index
|
|
477
|
+
let row_end = this.#index_of('\n', row_start);
|
|
478
|
+
if (row_end === -1)
|
|
479
|
+
row_end = this.#text.length;
|
|
480
|
+
const cells = mdz_split_table_row(this.#text, row_start, row_end);
|
|
481
|
+
if (cells === null)
|
|
482
|
+
break;
|
|
483
|
+
this.#emit_table_row(cells, false, row_start, row_end);
|
|
484
|
+
this.#index = row_end;
|
|
485
|
+
table_end = row_end;
|
|
486
|
+
}
|
|
487
|
+
this.#tokens.push({ type: 'table_close', start: table_end, end: table_end });
|
|
488
|
+
this.#skip_blank_lines();
|
|
489
|
+
return true;
|
|
490
|
+
}
|
|
491
|
+
/** Emit one table row's open / cell / close tokens. */
|
|
492
|
+
#emit_table_row(cells, header, row_start, row_end) {
|
|
493
|
+
this.#tokens.push({ type: 'table_row_open', header, start: row_start, end: row_start });
|
|
494
|
+
for (const cell of cells) {
|
|
495
|
+
this.#emit_table_cell(cell.start, cell.end);
|
|
496
|
+
}
|
|
497
|
+
this.#tokens.push({ type: 'table_row_close', start: row_end, end: row_end });
|
|
498
|
+
}
|
|
499
|
+
/**
|
|
500
|
+
* Emit one cell's `table_cell_open`, inline-content tokens, and
|
|
501
|
+
* `table_cell_close`. Inline tokenization is bounded to the cell's trimmed
|
|
502
|
+
* span via `#max_search_index`, with `#in_table_cell` set so `#tokenize_text`
|
|
503
|
+
* stops at that bound and unescapes `\|`.
|
|
504
|
+
*/
|
|
505
|
+
#emit_table_cell(cell_start, cell_end) {
|
|
506
|
+
this.#tokens.push({ type: 'table_cell_open', start: cell_start, end: cell_start });
|
|
507
|
+
this.#tokenize_cell_inline(cell_start, cell_end);
|
|
508
|
+
this.#tokens.push({ type: 'table_cell_close', start: cell_end, end: cell_end });
|
|
509
|
+
}
|
|
510
|
+
/**
|
|
511
|
+
* Tokenize a cell's trimmed inline span `[cell_start, cell_end)` into the
|
|
512
|
+
* current token buffer, bounded by `#max_search_index` with `#in_table_cell`
|
|
513
|
+
* set so `#tokenize_text` stops at the bound and unescapes `\|`. Shared by
|
|
514
|
+
* `#emit_table_cell` (wrapped in cell tokens) and `lex_table_cell` (bare).
|
|
515
|
+
*/
|
|
516
|
+
#tokenize_cell_inline(cell_start, cell_end) {
|
|
517
|
+
const saved_max = this.#max_search_index;
|
|
518
|
+
this.#max_search_index = cell_end;
|
|
519
|
+
this.#in_table_cell = true;
|
|
520
|
+
this.#index = cell_start;
|
|
521
|
+
while (this.#index < cell_end) {
|
|
522
|
+
this.#tokenize_inline();
|
|
523
|
+
}
|
|
524
|
+
this.#in_table_cell = false;
|
|
525
|
+
this.#max_search_index = saved_max;
|
|
526
|
+
}
|
|
527
|
+
/**
|
|
528
|
+
* Recognize an in-item table whose header row's leading `|` is at `first`
|
|
529
|
+
* (the line's indent already skipped): a pipe row whose next line is a
|
|
530
|
+
* delimiter row, both indented past `marker_indent` (so both stay inside the
|
|
531
|
+
* item). Returns the parsed header/delimiter, or null. Pure — no emission.
|
|
532
|
+
*/
|
|
533
|
+
#match_item_table_head(first, marker_indent) {
|
|
534
|
+
const header_end = this.#index_of('\n', first);
|
|
535
|
+
if (header_end === -1)
|
|
536
|
+
return null; // a header needs a delimiter line beneath it
|
|
537
|
+
const header_cells = mdz_split_table_row(this.#text, first, header_end);
|
|
538
|
+
if (header_cells === null)
|
|
539
|
+
return null;
|
|
540
|
+
const delim_line_start = header_end + 1;
|
|
541
|
+
let dj = delim_line_start;
|
|
542
|
+
while (dj < this.#text.length && is_line_whitespace(this.#text.charCodeAt(dj)))
|
|
543
|
+
dj++;
|
|
544
|
+
if (dj - delim_line_start <= marker_indent)
|
|
545
|
+
return null; // delimiter dedented out of the item
|
|
546
|
+
let delim_end = this.#index_of('\n', dj);
|
|
547
|
+
if (delim_end === -1)
|
|
548
|
+
delim_end = this.#text.length;
|
|
549
|
+
const align = mdz_parse_table_delimiter(this.#text, dj, delim_end);
|
|
550
|
+
if (align === null || align.length !== header_cells.length)
|
|
551
|
+
return null;
|
|
552
|
+
return { header_cells, header_end, align, delim_end };
|
|
553
|
+
}
|
|
554
|
+
/** Whether a table opens at `first` as a block child of an item at `marker_indent`. */
|
|
555
|
+
#is_item_table_start(first, marker_indent) {
|
|
556
|
+
return this.#match_item_table_head(first, marker_indent) !== null;
|
|
557
|
+
}
|
|
558
|
+
/**
|
|
559
|
+
* Tokenize an in-item table at `first` (a recognized table start). Mirrors
|
|
560
|
+
* `#tokenize_table` but per row skips the line's indent to the `|` and ends
|
|
561
|
+
* when a row dedents to `marker_indent` or shallower, leaving `#index` at the
|
|
562
|
+
* table's terminating newline so the list loop classifies what follows (no
|
|
563
|
+
* `#skip_blank_lines` — the list owns blank-line containment).
|
|
564
|
+
*/
|
|
565
|
+
#tokenize_item_table(first, marker_indent) {
|
|
566
|
+
const head = this.#match_item_table_head(first, marker_indent);
|
|
567
|
+
this.#tokens.push({ type: 'table_open', align: head.align, start: first, end: head.header_end });
|
|
568
|
+
this.#emit_table_row(head.header_cells, true, first, head.header_end);
|
|
569
|
+
this.#index = head.delim_end;
|
|
570
|
+
let table_end = head.delim_end;
|
|
571
|
+
while (this.#index < this.#text.length) {
|
|
572
|
+
const row_line_start = this.#index + 1; // past the newline at #index
|
|
573
|
+
let rj = row_line_start;
|
|
574
|
+
while (rj < this.#text.length && is_line_whitespace(this.#text.charCodeAt(rj)))
|
|
575
|
+
rj++;
|
|
576
|
+
if (rj - row_line_start <= marker_indent)
|
|
577
|
+
break; // dedent ends the table
|
|
578
|
+
if (this.#text.charCodeAt(rj) !== PIPE)
|
|
579
|
+
break;
|
|
580
|
+
let row_end = this.#index_of('\n', rj);
|
|
581
|
+
if (row_end === -1)
|
|
582
|
+
row_end = this.#text.length;
|
|
583
|
+
const cells = mdz_split_table_row(this.#text, rj, row_end);
|
|
584
|
+
if (cells === null)
|
|
585
|
+
break;
|
|
586
|
+
this.#emit_table_row(cells, false, rj, row_end);
|
|
587
|
+
this.#index = row_end;
|
|
588
|
+
table_end = row_end;
|
|
589
|
+
}
|
|
590
|
+
this.#tokens.push({ type: 'table_close', start: table_end, end: table_end });
|
|
591
|
+
}
|
|
353
592
|
// -- List tokenizers --
|
|
354
593
|
/**
|
|
355
594
|
* Tokenize an entire list starting at a column-0 marker line, emitting
|
|
@@ -478,6 +717,22 @@ export class MdzLexer {
|
|
|
478
717
|
continue;
|
|
479
718
|
}
|
|
480
719
|
}
|
|
720
|
+
// a deeper pipe-row line whose next line is a delimiter becomes a
|
|
721
|
+
// table child of the item it would otherwise continue (the fence/quote
|
|
722
|
+
// precedent, but a two-line lookahead)
|
|
723
|
+
if (indent > 0 && this.#text.charCodeAt(first) === PIPE) {
|
|
724
|
+
const target_idx = this.#list_continuation_target(levels, indent);
|
|
725
|
+
if (target_idx !== -1) {
|
|
726
|
+
const marker_indent = levels[target_idx].indent;
|
|
727
|
+
if (this.#is_item_table_start(first, marker_indent)) {
|
|
728
|
+
while (levels.length - 1 > target_idx) {
|
|
729
|
+
this.#list_close(levels);
|
|
730
|
+
}
|
|
731
|
+
this.#tokenize_item_table(first, marker_indent);
|
|
732
|
+
continue;
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
}
|
|
481
736
|
// any other line deeper than an open item's marker opens a new
|
|
482
737
|
// `Paragraph` block child of that item (the item's inline run is never
|
|
483
738
|
// current here — a blank, block child, or dedent intervened)
|
|
@@ -681,6 +936,9 @@ export class MdzLexer {
|
|
|
681
936
|
if (this.#text.charCodeAt(j) === RIGHT_ANGLE && this.#is_blockquote_opener(j)) {
|
|
682
937
|
return nl; // a quote opener line interrupts the run
|
|
683
938
|
}
|
|
939
|
+
if (this.#text.charCodeAt(j) === PIPE && this.#is_item_table_start(j, deepest_indent)) {
|
|
940
|
+
return nl; // a table opener (pipe row + delimiter) interrupts the run
|
|
941
|
+
}
|
|
684
942
|
i = line_start; // continuation — keep scanning
|
|
685
943
|
}
|
|
686
944
|
}
|
|
@@ -1002,12 +1260,30 @@ export class MdzLexer {
|
|
|
1002
1260
|
}
|
|
1003
1261
|
#tokenize_markdown_link() {
|
|
1004
1262
|
const start = this.#index;
|
|
1263
|
+
// a link open at this offset already failed, or was suppressed by the
|
|
1264
|
+
// nesting cap — emit `[` as text instead of re-descending into the same
|
|
1265
|
+
// failing children (exponential on nested `[`); the short-circuit
|
|
1266
|
+
// reproduces the revert's exact net effect (`[` text, cursor at start + 1)
|
|
1267
|
+
if (this.#failed_link_starts.has(start) || this.#depth_capped.has(start)) {
|
|
1268
|
+
this.#index = start + 1;
|
|
1269
|
+
this.#emit_text('[', start);
|
|
1270
|
+
return;
|
|
1271
|
+
}
|
|
1272
|
+
// nesting-depth cap: too deep to open — render literal and record the
|
|
1273
|
+
// suppression so a shallower re-lex short-circuits here too
|
|
1274
|
+
if (this.#inline_depth >= MAX_INLINE_NESTING_DEPTH) {
|
|
1275
|
+
this.#depth_capped.add(start);
|
|
1276
|
+
this.#index = start + 1;
|
|
1277
|
+
this.#emit_text('[', start);
|
|
1278
|
+
return;
|
|
1279
|
+
}
|
|
1005
1280
|
this.#index++; // consume [ (dispatch guarantees it)
|
|
1006
1281
|
// Emit link_text_open
|
|
1007
1282
|
this.#tokens.push({ type: 'link_text_open', start, end: this.#index });
|
|
1008
1283
|
// Tokenize children until ] — only `]` delimits link text; a bare `)`
|
|
1009
1284
|
// is ordinary content (matching the streaming parser, which has no `)`
|
|
1010
1285
|
// dispatch case — the reference scan finds its own `)` after `](`)
|
|
1286
|
+
this.#inline_depth++;
|
|
1011
1287
|
while (this.#index < this.#text.length) {
|
|
1012
1288
|
if (this.#text.charCodeAt(this.#index) === RIGHT_BRACKET)
|
|
1013
1289
|
break;
|
|
@@ -1015,6 +1291,7 @@ export class MdzLexer {
|
|
|
1015
1291
|
break;
|
|
1016
1292
|
this.#tokenize_inline();
|
|
1017
1293
|
}
|
|
1294
|
+
this.#inline_depth--;
|
|
1018
1295
|
// Check for ]
|
|
1019
1296
|
if (this.#index >= this.#text.length || this.#text.charCodeAt(this.#index) !== RIGHT_BRACKET) {
|
|
1020
1297
|
// Revert - remove link_text_open and all children tokens added
|
|
@@ -1071,6 +1348,13 @@ export class MdzLexer {
|
|
|
1071
1348
|
});
|
|
1072
1349
|
}
|
|
1073
1350
|
#revert_tokens_from_link_open(start) {
|
|
1351
|
+
// record the failure so a re-lex from before this `[` short-circuits.
|
|
1352
|
+
// Keyed by `start` alone — unlike `#failed_tag`'s `start:bound` — because a
|
|
1353
|
+
// link's outcome is bound-independent: the `]`-terminator scan (the children
|
|
1354
|
+
// loop) runs to `text.length` and never consults `#max_search_index`, so a
|
|
1355
|
+
// wider-bound re-lex can't move it. The bound reaches nested tags inside the
|
|
1356
|
+
// link text, but re-attempting those wider never flips the link's net result.
|
|
1357
|
+
this.#failed_link_starts.add(start);
|
|
1074
1358
|
// Find and remove link_text_open and all tokens after it
|
|
1075
1359
|
let open_idx = -1;
|
|
1076
1360
|
for (let i = this.#tokens.length - 1; i >= 0; i--) {
|
|
@@ -1087,6 +1371,30 @@ export class MdzLexer {
|
|
|
1087
1371
|
}
|
|
1088
1372
|
#tokenize_tag() {
|
|
1089
1373
|
const start = this.#index;
|
|
1374
|
+
// short-circuit a re-attempt of a tag open that already failed at this
|
|
1375
|
+
// offset+bound — re-descending into the same failing children is
|
|
1376
|
+
// exponential on nested same-name tags. Keyed by
|
|
1377
|
+
// `#max_search_index` (captured as `bound`) because the closer search is
|
|
1378
|
+
// bound-sensitive, so a wider-bound re-lex can legitimately succeed. The
|
|
1379
|
+
// key string is built only when the memo is non-empty (adversarial
|
|
1380
|
+
// input); prose never allocates it. The short-circuit reproduces a
|
|
1381
|
+
// revert's exact net effect (`<` text, cursor at start + 1).
|
|
1382
|
+
const bound = this.#max_search_index;
|
|
1383
|
+
if (this.#depth_capped.has(start) ||
|
|
1384
|
+
(this.#failed_tag.size !== 0 && this.#failed_tag.has(`${start}:${bound}`))) {
|
|
1385
|
+
this.#index = start + 1;
|
|
1386
|
+
this.#emit_text('<', start);
|
|
1387
|
+
return;
|
|
1388
|
+
}
|
|
1389
|
+
// nesting-depth cap: too deep to open a tag — render literal and record
|
|
1390
|
+
// the suppression (position-keyed) so a shallower re-lex short-circuits
|
|
1391
|
+
// here too, keeping the revert re-scan linear
|
|
1392
|
+
if (this.#inline_depth >= MAX_INLINE_NESTING_DEPTH) {
|
|
1393
|
+
this.#depth_capped.add(start);
|
|
1394
|
+
this.#index = start + 1;
|
|
1395
|
+
this.#emit_text('<', start);
|
|
1396
|
+
return;
|
|
1397
|
+
}
|
|
1090
1398
|
this.#index++; // consume <
|
|
1091
1399
|
// Tag name must start with a letter
|
|
1092
1400
|
if (this.#index >= this.#text.length || !is_letter(this.#text.charCodeAt(this.#index))) {
|
|
@@ -1123,6 +1431,7 @@ export class MdzLexer {
|
|
|
1123
1431
|
}
|
|
1124
1432
|
// Check for >
|
|
1125
1433
|
if (this.#index >= this.#text.length || this.#text.charCodeAt(this.#index) !== RIGHT_ANGLE) {
|
|
1434
|
+
this.#failed_tag.add(`${start}:${bound}`);
|
|
1126
1435
|
this.#index = start + 1;
|
|
1127
1436
|
this.#emit_text('<', start);
|
|
1128
1437
|
return;
|
|
@@ -1140,11 +1449,13 @@ export class MdzLexer {
|
|
|
1140
1449
|
const search_limit = Math.min(this.#max_search_index, this.#text.length);
|
|
1141
1450
|
const closing_tag_pos = this.#index_of(closing_tag, this.#index);
|
|
1142
1451
|
if (closing_tag_pos === -1 || closing_tag_pos > search_limit) {
|
|
1452
|
+
this.#failed_tag.add(`${start}:${bound}`);
|
|
1143
1453
|
this.#index = start + 1;
|
|
1144
1454
|
this.#emit_text('<', start);
|
|
1145
1455
|
return;
|
|
1146
1456
|
}
|
|
1147
1457
|
if (this.#has_paragraph_break_between(this.#index, closing_tag_pos)) {
|
|
1458
|
+
this.#failed_tag.add(`${start}:${bound}`);
|
|
1148
1459
|
this.#index = start + 1;
|
|
1149
1460
|
this.#emit_text('<', start);
|
|
1150
1461
|
return;
|
|
@@ -1158,8 +1469,10 @@ export class MdzLexer {
|
|
|
1158
1469
|
// the code span). Recomputed per iteration because a nested same-name
|
|
1159
1470
|
// tag legitimately consumes the nearest closer (`<b>x<b>y</b></b>`).
|
|
1160
1471
|
const saved_max = this.#max_search_index;
|
|
1472
|
+
this.#inline_depth++;
|
|
1161
1473
|
while (this.#index < this.#text.length) {
|
|
1162
1474
|
if (this.#match(closing_tag)) {
|
|
1475
|
+
this.#inline_depth--;
|
|
1163
1476
|
this.#max_search_index = saved_max;
|
|
1164
1477
|
const close_start = this.#index;
|
|
1165
1478
|
this.#index += closing_tag.length;
|
|
@@ -1180,11 +1493,13 @@ export class MdzLexer {
|
|
|
1180
1493
|
this.#max_search_index = next_close;
|
|
1181
1494
|
this.#tokenize_inline();
|
|
1182
1495
|
}
|
|
1496
|
+
this.#inline_depth--;
|
|
1183
1497
|
// No valid closer left — revert: drop the open token and all consumed
|
|
1184
1498
|
// children tokens, emit `<` as text, and re-lex from the next char
|
|
1185
1499
|
// (the `#revert_tokens_from_link_open` pattern; the old fallthrough
|
|
1186
1500
|
// left a dangling `tag_open` whose parser fallback discarded content)
|
|
1187
1501
|
this.#max_search_index = saved_max;
|
|
1502
|
+
this.#failed_tag.add(`${start}:${bound}`);
|
|
1188
1503
|
this.#tokens.splice(open_token_index);
|
|
1189
1504
|
this.#index = start + 1;
|
|
1190
1505
|
this.#emit_text('<', start);
|
|
@@ -1208,19 +1523,42 @@ export class MdzLexer {
|
|
|
1208
1523
|
// content: parts accumulate around each stripped region
|
|
1209
1524
|
let parts = null;
|
|
1210
1525
|
let part_start = start;
|
|
1211
|
-
|
|
1526
|
+
// in a table cell the trimmed cell end is a hard stop (no newline to
|
|
1527
|
+
// halt on mid-line) — hoisted to the loop bound
|
|
1528
|
+
const scan_end = this.#in_table_cell
|
|
1529
|
+
? Math.min(this.#max_search_index, this.#text.length)
|
|
1530
|
+
: this.#text.length;
|
|
1531
|
+
while (this.#index < scan_end) {
|
|
1212
1532
|
const char_code = this.#text.charCodeAt(this.#index);
|
|
1533
|
+
// one table load classifies the char; zero (the common case — the
|
|
1534
|
+
// char can't end or interrupt the run) skips the dispatch below.
|
|
1535
|
+
// `TEXT_SCAN_CLASS` is shared with the streaming scanner
|
|
1536
|
+
// (`consume_text_run` in `mdz_stream_parser_text.ts`); the two dispatch
|
|
1537
|
+
// bodies intentionally differ around it — this one handles `\|` escapes
|
|
1538
|
+
// and list-run strip, that one URL speculation across chunk boundaries.
|
|
1539
|
+
const char_class = char_code < 128 ? TEXT_SCAN_CLASS[char_code] : 0;
|
|
1540
|
+
if (char_class === 0) {
|
|
1541
|
+
this.#index++;
|
|
1542
|
+
continue;
|
|
1543
|
+
}
|
|
1213
1544
|
// Stop at special characters
|
|
1214
|
-
if (
|
|
1215
|
-
char_code === ASTERISK ||
|
|
1216
|
-
char_code === UNDERSCORE ||
|
|
1217
|
-
char_code === TILDE ||
|
|
1218
|
-
char_code === LEFT_BRACKET ||
|
|
1219
|
-
char_code === RIGHT_BRACKET ||
|
|
1220
|
-
char_code === LEFT_ANGLE) {
|
|
1545
|
+
if ((char_class & TEXT_CLASS_STOP) !== 0)
|
|
1221
1546
|
break;
|
|
1547
|
+
if ((char_class & TEXT_CLASS_BACKSLASH) !== 0) {
|
|
1548
|
+
// `\|` is a literal pipe — the only mdz escape, scoped to table cells
|
|
1549
|
+
if (this.#in_table_cell &&
|
|
1550
|
+
this.#index + 1 < this.#max_search_index &&
|
|
1551
|
+
this.#text.charCodeAt(this.#index + 1) === PIPE) {
|
|
1552
|
+
(parts ??= []).push(this.#text.slice(part_start, this.#index));
|
|
1553
|
+
parts.push('|');
|
|
1554
|
+
this.#index += 2;
|
|
1555
|
+
part_start = this.#index;
|
|
1556
|
+
continue;
|
|
1557
|
+
}
|
|
1558
|
+
this.#index++;
|
|
1559
|
+
continue;
|
|
1222
1560
|
}
|
|
1223
|
-
if (
|
|
1561
|
+
if ((char_class & TEXT_CLASS_NEWLINE) !== 0) {
|
|
1224
1562
|
if (this.#list_run_strip) {
|
|
1225
1563
|
if (this.#index >= this.#max_search_index)
|
|
1226
1564
|
break; // the run's terminator
|
|
@@ -1251,20 +1589,31 @@ export class MdzLexer {
|
|
|
1251
1589
|
next_char === HYPHEN ||
|
|
1252
1590
|
next_char === BACKTICK ||
|
|
1253
1591
|
next_char === ONE ||
|
|
1254
|
-
next_char === RIGHT_ANGLE
|
|
1592
|
+
next_char === RIGHT_ANGLE ||
|
|
1593
|
+
next_char === PIPE) {
|
|
1255
1594
|
this.#index++; // consume the newline
|
|
1256
1595
|
break;
|
|
1257
1596
|
}
|
|
1258
1597
|
}
|
|
1598
|
+
this.#index++; // single newline — ordinary text
|
|
1599
|
+
continue;
|
|
1600
|
+
}
|
|
1601
|
+
// URL or internal path mid-text (class guard avoids probing every char)
|
|
1602
|
+
if ((char_class & TEXT_CLASS_URL) !== 0) {
|
|
1603
|
+
if (this.#is_at_url())
|
|
1604
|
+
break;
|
|
1605
|
+
}
|
|
1606
|
+
else if (char_code === SLASH) {
|
|
1607
|
+
if (is_at_absolute_path(this.#text, this.#index))
|
|
1608
|
+
break;
|
|
1259
1609
|
}
|
|
1260
|
-
|
|
1261
|
-
if (((char_code === 104 /* h */ || char_code === 72) /* H */ && this.#is_at_url()) ||
|
|
1262
|
-
(char_code === SLASH && is_at_absolute_path(this.#text, this.#index)) ||
|
|
1263
|
-
(char_code === PERIOD && is_at_relative_path(this.#text, this.#index))) {
|
|
1610
|
+
else if (is_at_relative_path(this.#text, this.#index)) {
|
|
1264
1611
|
break;
|
|
1265
1612
|
}
|
|
1266
1613
|
this.#index++;
|
|
1267
1614
|
}
|
|
1615
|
+
if (mdz_debug_work.enabled)
|
|
1616
|
+
mdz_debug_work.total += this.#index - start;
|
|
1268
1617
|
// Ensure we always consume at least one character
|
|
1269
1618
|
if (this.#index === start && this.#index < this.#text.length) {
|
|
1270
1619
|
this.#index++;
|
package/dist/mdz_opcodes.d.ts
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
*
|
|
12
12
|
* @module
|
|
13
13
|
*/
|
|
14
|
+
import type { MdzTableAlign } from './mdz.js';
|
|
14
15
|
/**
|
|
15
16
|
* Unique monotonic identifier for each node created by the parser.
|
|
16
17
|
* IDs are never reused within a parser instance.
|
|
@@ -24,7 +25,7 @@ export type MdzNodeId = number;
|
|
|
24
25
|
* visible yet opens an optimistic `Code` container whose content streams as
|
|
25
26
|
* text children (collapsed back to `content` by consumers at `close`).
|
|
26
27
|
*/
|
|
27
|
-
export type MdzNodeTypeContainer = 'Paragraph' | 'Bold' | 'Italic' | 'Strikethrough' | 'Link' | 'Heading' | 'List' | 'ListItem' | 'Blockquote' | 'Element' | 'Component' | 'Codeblock' | 'Code';
|
|
28
|
+
export type MdzNodeTypeContainer = 'Paragraph' | 'Bold' | 'Italic' | 'Strikethrough' | 'Link' | 'Heading' | 'List' | 'ListItem' | 'Blockquote' | 'Element' | 'Component' | 'Codeblock' | 'Code' | 'Table' | 'TableRow' | 'TableCell';
|
|
28
29
|
/** Node types for self-contained leaf elements. */
|
|
29
30
|
export type MdzNodeTypeVoid = 'Hr';
|
|
30
31
|
/** Discriminant for leaf text nodes (see `MdzNodeTypeContainer` for `Code`'s dual form). */
|
|
@@ -51,6 +52,10 @@ export interface MdzOpcodeOpen {
|
|
|
51
52
|
start_number?: number;
|
|
52
53
|
/** Authored item number. Present when `node_type` is `'ListItem'` in an ordered list. */
|
|
53
54
|
number?: number;
|
|
55
|
+
/** Per-column alignment from the delimiter row. Present when `node_type` is `'Table'`. */
|
|
56
|
+
align?: Array<MdzTableAlign>;
|
|
57
|
+
/** Whether this is the header row (`thead`/`th`). Present when `node_type` is `'TableRow'`. */
|
|
58
|
+
header?: boolean;
|
|
54
59
|
}
|
|
55
60
|
/**
|
|
56
61
|
* Close a previously opened container node.
|