@fuzdev/mdz 0.1.0 → 0.2.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/MdzNodeView.svelte +30 -1
- package/dist/MdzNodeView.svelte.d.ts.map +1 -1
- package/dist/MdzStreamNodeView.svelte +31 -0
- package/dist/MdzStreamNodeView.svelte.d.ts.map +1 -1
- package/dist/context_helpers.d.ts +1 -1
- package/dist/mdz.d.ts +53 -2
- package/dist/mdz.d.ts.map +1 -1
- package/dist/mdz.js +29 -1
- package/dist/mdz_helpers.d.ts +35 -1
- package/dist/mdz_helpers.d.ts.map +1 -1
- package/dist/mdz_helpers.js +120 -0
- package/dist/mdz_lexer.d.ts +35 -1
- package/dist/mdz_lexer.d.ts.map +1 -1
- package/dist/mdz_lexer.js +211 -2
- 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_list.d.ts.map +1 -1
- package/dist/mdz_stream_parser_list.js +22 -1
- package/dist/mdz_stream_parser_state.d.ts +12 -0
- package/dist/mdz_stream_parser_state.d.ts.map +1 -1
- package/dist/mdz_stream_parser_state.js +1 -0
- package/dist/mdz_stream_parser_table.d.ts +72 -0
- package/dist/mdz_stream_parser_table.d.ts.map +1 -0
- package/dist/mdz_stream_parser_table.js +351 -0
- 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 +37 -3
- 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 +101 -2
- package/dist/svelte_preprocess_mdz.js +2 -2
- package/package.json +8 -8
- package/src/lib/context_helpers.ts +1 -1
- package/src/lib/mdz.ts +70 -3
- package/src/lib/mdz_contexts.ts +1 -1
- package/src/lib/mdz_helpers.ts +125 -1
- package/src/lib/mdz_lexer.ts +262 -3
- 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 +4 -4
- package/src/lib/mdz_stream_parser_link.ts +3 -3
- package/src/lib/mdz_stream_parser_list.ts +24 -2
- package/src/lib/mdz_stream_parser_state.ts +12 -2
- package/src/lib/mdz_stream_parser_table.ts +409 -0
- package/src/lib/mdz_stream_parser_text.ts +2 -2
- 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 +39 -5
- package/src/lib/mdz_token_parser.ts +122 -5
- package/src/lib/svelte_preprocess_mdz.ts +4 -4
- package/src/lib/tsdoc_mdz.ts +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fuzdev/mdz",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "a strict markdown dialect built for streaming, Svelte authoring, docs websites, and untrusted content",
|
|
5
5
|
"tagline": "markdown for fuz",
|
|
6
6
|
"glyph": "📝",
|
|
@@ -57,17 +57,17 @@
|
|
|
57
57
|
},
|
|
58
58
|
"devDependencies": {
|
|
59
59
|
"@fuzdev/blake3_wasm": "^0.1.1",
|
|
60
|
-
"@fuzdev/fuz_code": "^0.46.
|
|
61
|
-
"@fuzdev/fuz_css": "^0.
|
|
62
|
-
"@fuzdev/fuz_ui": "^0.
|
|
60
|
+
"@fuzdev/fuz_code": "^0.46.1",
|
|
61
|
+
"@fuzdev/fuz_css": "^0.63.2",
|
|
62
|
+
"@fuzdev/fuz_ui": "^0.205.1",
|
|
63
63
|
"@fuzdev/fuz_util": "^0.65.1",
|
|
64
|
-
"@fuzdev/gro": "^0.
|
|
64
|
+
"@fuzdev/gro": "^0.205.1",
|
|
65
65
|
"@ryanatkn/eslint-config": "^0.12.1",
|
|
66
66
|
"@sveltejs/acorn-typescript": "^1.0.10",
|
|
67
67
|
"@sveltejs/adapter-static": "^3.0.10",
|
|
68
|
-
"@sveltejs/kit": "
|
|
68
|
+
"@sveltejs/kit": "2.64.0",
|
|
69
69
|
"@sveltejs/package": "^2.5.7",
|
|
70
|
-
"@sveltejs/vite-plugin-svelte": "^
|
|
70
|
+
"@sveltejs/vite-plugin-svelte": "^7.1.2",
|
|
71
71
|
"@types/estree": "^1.0.8",
|
|
72
72
|
"@types/node": "^24.12.4",
|
|
73
73
|
"@webref/css": "^8.2.0",
|
|
@@ -83,7 +83,7 @@
|
|
|
83
83
|
"tslib": "^2.8.1",
|
|
84
84
|
"typescript": "^5.9.3",
|
|
85
85
|
"typescript-eslint": "^8.48.1",
|
|
86
|
-
"vite": "^
|
|
86
|
+
"vite": "^8.0.16",
|
|
87
87
|
"vitest": "^4.0.15"
|
|
88
88
|
},
|
|
89
89
|
"prettier": {
|
|
@@ -14,7 +14,7 @@ import {getContext, setContext} from 'svelte';
|
|
|
14
14
|
* If a `fallback` is provided, the `value` argument to `set` is optional
|
|
15
15
|
* and `get_maybe` is omitted from the type.
|
|
16
16
|
*
|
|
17
|
-
* Vendored from `@fuzdev/fuz_ui/context_helpers.
|
|
17
|
+
* Vendored from `@fuzdev/fuz_ui/context_helpers.ts` — fuz_ui sits above mdz
|
|
18
18
|
* in the dependency graph, so mdz keeps its own copy for `mdz_contexts.ts`.
|
|
19
19
|
* Consumers should import fuz_ui's.
|
|
20
20
|
*
|
package/src/lib/mdz.ts
CHANGED
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
* - inline code in backticks (creates `Code` nodes; auto-linking to identifiers/modules
|
|
20
20
|
* is handled by the rendering layer via `MdzNodeView.svelte`)
|
|
21
21
|
* - paragraph breaks (blank lines)
|
|
22
|
-
* - block elements: headings, horizontal rules, lists, blockquotes, code blocks
|
|
22
|
+
* - block elements: headings, horizontal rules, lists, blockquotes, code blocks, tables
|
|
23
23
|
* - HTML elements and Svelte components (opt-in via context)
|
|
24
24
|
*
|
|
25
25
|
* Whitespace: the parser preserves whitespace in text node content (single
|
|
@@ -50,8 +50,8 @@
|
|
|
50
50
|
* @module
|
|
51
51
|
*/
|
|
52
52
|
|
|
53
|
-
import {MdzLexer} from './mdz_lexer.
|
|
54
|
-
import {MdzTokenParser} from './mdz_token_parser.
|
|
53
|
+
import {MdzLexer} from './mdz_lexer.ts';
|
|
54
|
+
import {MdzTokenParser} from './mdz_token_parser.ts';
|
|
55
55
|
|
|
56
56
|
// TODO design incremental parsing or some system that preserves Svelte components across re-renders when possible
|
|
57
57
|
|
|
@@ -64,6 +64,37 @@ import {MdzTokenParser} from './mdz_token_parser.js';
|
|
|
64
64
|
export const mdz_parse = (text: string): Array<MdzNode> =>
|
|
65
65
|
new MdzTokenParser(new MdzLexer(text).tokenize()).parse();
|
|
66
66
|
|
|
67
|
+
/**
|
|
68
|
+
* Parses table cells' inline content for the streaming parser, sharing one
|
|
69
|
+
* `MdzLexer` + `MdzTokenParser` across a row's cells rather than allocating a
|
|
70
|
+
* fresh pair (and search-memo `Map`) per cell. Bind to the buffer holding the
|
|
71
|
+
* row, then call `parse` once per cell `[cell_start, cell_end)` span; each
|
|
72
|
+
* call yields inline `MdzNode`s honoring code-span protection and the `\|`
|
|
73
|
+
* escape, matching the sync reference exactly — it *is* the sync lexer/parser,
|
|
74
|
+
* reset per cell, mirroring how the sync lexer already reuses one instance
|
|
75
|
+
* across a table's cells.
|
|
76
|
+
*
|
|
77
|
+
* The sync pipeline never needs this seam: its `MdzLexer` tokenizes a table's
|
|
78
|
+
* cells in place. Only the streaming path, which parses each fully-buffered
|
|
79
|
+
* cell out of band, would otherwise allocate per cell.
|
|
80
|
+
*
|
|
81
|
+
* @nodocs
|
|
82
|
+
*/
|
|
83
|
+
export class MdzTableCellParser {
|
|
84
|
+
#lexer: MdzLexer;
|
|
85
|
+
#parser: MdzTokenParser = new MdzTokenParser([]);
|
|
86
|
+
|
|
87
|
+
/** @param text - the buffer the cell spans index into (stable across a row). */
|
|
88
|
+
constructor(text: string) {
|
|
89
|
+
this.#lexer = new MdzLexer(text);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
parse(cell_start: number, cell_end: number): Array<MdzNode> {
|
|
93
|
+
this.#parser.reset(this.#lexer.lex_table_cell(cell_start, cell_end));
|
|
94
|
+
return this.#parser.parse_inline_run();
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
67
98
|
export type MdzNode =
|
|
68
99
|
| MdzNodeText
|
|
69
100
|
| MdzNodeCode
|
|
@@ -78,6 +109,9 @@ export type MdzNode =
|
|
|
78
109
|
| MdzNodeList
|
|
79
110
|
| MdzNodeListItem
|
|
80
111
|
| MdzNodeBlockquote
|
|
112
|
+
| MdzNodeTable
|
|
113
|
+
| MdzNodeTableRow
|
|
114
|
+
| MdzNodeTableCell
|
|
81
115
|
| MdzNodeElement
|
|
82
116
|
| MdzNodeComponent;
|
|
83
117
|
|
|
@@ -175,6 +209,39 @@ export interface MdzNodeBlockquote extends MdzNodeBase {
|
|
|
175
209
|
children: Array<MdzNode>;
|
|
176
210
|
}
|
|
177
211
|
|
|
212
|
+
/** Per-column text alignment from the delimiter row's colons; `null` is the default (no alignment). */
|
|
213
|
+
export type MdzTableAlign = 'left' | 'center' | 'right' | null;
|
|
214
|
+
|
|
215
|
+
export interface MdzNodeTable extends MdzNodeBase {
|
|
216
|
+
type: 'Table';
|
|
217
|
+
/**
|
|
218
|
+
* Per-column alignment, length equal to the delimiter row's column count.
|
|
219
|
+
* Rendered as `text-align` on each cell; tooling can read it to re-theme
|
|
220
|
+
* (e.g. a CSP-strict consumer that avoids inline styles).
|
|
221
|
+
*/
|
|
222
|
+
align: Array<MdzTableAlign>;
|
|
223
|
+
/** The header row first, then any body rows. */
|
|
224
|
+
children: Array<MdzNodeTableRow>;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export interface MdzNodeTableRow extends MdzNodeBase {
|
|
228
|
+
type: 'TableRow';
|
|
229
|
+
/** `true` for the header row (renders `thead`/`th`); `false` for body rows (`tbody`/`td`). */
|
|
230
|
+
header: boolean;
|
|
231
|
+
/**
|
|
232
|
+
* The row's authored cells, preserved as written. The renderer normalizes to
|
|
233
|
+
* the column count: short rows are padded with empty cells, cells past the
|
|
234
|
+
* column count are ignored (their content survives here for tooling).
|
|
235
|
+
*/
|
|
236
|
+
children: Array<MdzNodeTableCell>;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
export interface MdzNodeTableCell extends MdzNodeBase {
|
|
240
|
+
type: 'TableCell';
|
|
241
|
+
/** Inline content only (text, bold, italic, strikethrough, code, links). */
|
|
242
|
+
children: Array<MdzNode>;
|
|
243
|
+
}
|
|
244
|
+
|
|
178
245
|
export interface MdzNodeElement extends MdzNodeBase {
|
|
179
246
|
type: 'Element';
|
|
180
247
|
/** HTML element name (e.g. `div`, `span`, `aside`). */
|
package/src/lib/mdz_contexts.ts
CHANGED
package/src/lib/mdz_helpers.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* @module
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import type {MdzNode, MdzNodeText, MdzNodeComponent, MdzNodeElement} from './mdz.
|
|
10
|
+
import type {MdzNode, MdzNodeText, MdzNodeComponent, MdzNodeElement, MdzTableAlign} from './mdz.ts';
|
|
11
11
|
|
|
12
12
|
// Character codes for performance
|
|
13
13
|
/** @nodocs */
|
|
@@ -47,6 +47,10 @@ export const RIGHT_PAREN = 41; // )
|
|
|
47
47
|
/** @nodocs */
|
|
48
48
|
export const COLON = 58; // :
|
|
49
49
|
/** @nodocs */
|
|
50
|
+
export const PIPE = 124; // |
|
|
51
|
+
/** @nodocs */
|
|
52
|
+
export const BACKSLASH = 92; // \
|
|
53
|
+
/** @nodocs */
|
|
50
54
|
export const PERIOD = 46; // .
|
|
51
55
|
/** @nodocs */
|
|
52
56
|
export const COMMA = 44; // ,
|
|
@@ -287,6 +291,126 @@ export const mdz_remap_segments = (
|
|
|
287
291
|
return seg.source_start + (p - seg.inner_start);
|
|
288
292
|
};
|
|
289
293
|
|
|
294
|
+
// -- Table helpers --
|
|
295
|
+
|
|
296
|
+
/** Trim line-whitespace from both ends of a `[start, end)` span. */
|
|
297
|
+
const trim_table_cell = (
|
|
298
|
+
text: string,
|
|
299
|
+
start: number,
|
|
300
|
+
end: number,
|
|
301
|
+
): {start: number; end: number} => {
|
|
302
|
+
let s = start;
|
|
303
|
+
let e = end;
|
|
304
|
+
while (s < e && is_line_whitespace(text.charCodeAt(s))) s++;
|
|
305
|
+
while (e > s && is_line_whitespace(text.charCodeAt(e - 1))) e--;
|
|
306
|
+
return {start: s, end: e};
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Split a table row line `[line_start, line_end)` into its cell-content spans.
|
|
311
|
+
*
|
|
312
|
+
* A valid row starts with `|` and ends — after optional trailing whitespace —
|
|
313
|
+
* with `|`, yielding at least one cell. Returns each cell's trimmed
|
|
314
|
+
* `[start, end)` source span, or `null` when the line is not a valid pipe row
|
|
315
|
+
* (no leading `|`, no trailing `|`, or zero cells).
|
|
316
|
+
*
|
|
317
|
+
* Pipe-finding honors two protections so the cell content can hold a literal
|
|
318
|
+
* `|`: an inline-code span (a `` ` `` to its matching `` ` `` on the same line —
|
|
319
|
+
* an unclosed `` ` `` is literal and protects nothing) shields the pipes inside
|
|
320
|
+
* it, and a `\|` escape is consumed as a literal pipe. The escape is
|
|
321
|
+
* table-cell-scoped — the caller's inline tokenizer performs the matching
|
|
322
|
+
* `\|` → `|` substitution; this scanner only declines to split there.
|
|
323
|
+
*
|
|
324
|
+
* @nodocs
|
|
325
|
+
*/
|
|
326
|
+
export const mdz_split_table_row = (
|
|
327
|
+
text: string,
|
|
328
|
+
line_start: number,
|
|
329
|
+
line_end: number,
|
|
330
|
+
): Array<{start: number; end: number}> | null => {
|
|
331
|
+
if (line_start >= line_end || text.charCodeAt(line_start) !== PIPE) return null;
|
|
332
|
+
const cells: Array<{start: number; end: number}> = [];
|
|
333
|
+
let i = line_start + 1; // past the leading pipe
|
|
334
|
+
let cell_start = i;
|
|
335
|
+
while (i < line_end) {
|
|
336
|
+
const char_code = text.charCodeAt(i);
|
|
337
|
+
if (char_code === BACKTICK) {
|
|
338
|
+
// inline-code span: skip to the matching backtick; unclosed → literal
|
|
339
|
+
let close = -1;
|
|
340
|
+
for (let k = i + 1; k < line_end; k++) {
|
|
341
|
+
if (text.charCodeAt(k) === BACKTICK) {
|
|
342
|
+
close = k;
|
|
343
|
+
break;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
if (close === -1) {
|
|
347
|
+
i++;
|
|
348
|
+
} else {
|
|
349
|
+
i = close + 1;
|
|
350
|
+
}
|
|
351
|
+
continue;
|
|
352
|
+
}
|
|
353
|
+
if (char_code === BACKSLASH) {
|
|
354
|
+
// `\|` is a literal pipe, never a delimiter
|
|
355
|
+
i += i + 1 < line_end && text.charCodeAt(i + 1) === PIPE ? 2 : 1;
|
|
356
|
+
continue;
|
|
357
|
+
}
|
|
358
|
+
if (char_code === PIPE) {
|
|
359
|
+
cells.push(trim_table_cell(text, cell_start, i));
|
|
360
|
+
i++;
|
|
361
|
+
cell_start = i;
|
|
362
|
+
continue;
|
|
363
|
+
}
|
|
364
|
+
i++;
|
|
365
|
+
}
|
|
366
|
+
// the trailing pipe is required: only whitespace may follow the last one
|
|
367
|
+
for (let k = cell_start; k < line_end; k++) {
|
|
368
|
+
if (!is_line_whitespace(text.charCodeAt(k))) return null;
|
|
369
|
+
}
|
|
370
|
+
if (cells.length === 0) return null;
|
|
371
|
+
return cells;
|
|
372
|
+
};
|
|
373
|
+
|
|
374
|
+
/** Classify a trimmed delimiter cell `[start, end)`; `false` when it is not a valid delimiter cell. */
|
|
375
|
+
const table_delimiter_align = (text: string, start: number, end: number): MdzTableAlign | false => {
|
|
376
|
+
let i = start;
|
|
377
|
+
let j = end;
|
|
378
|
+
if (i >= j) return false;
|
|
379
|
+
const left = text.charCodeAt(i) === COLON;
|
|
380
|
+
if (left) i++;
|
|
381
|
+
const right = j > i && text.charCodeAt(j - 1) === COLON;
|
|
382
|
+
if (right) j--;
|
|
383
|
+
if (i >= j) return false; // needs at least one dash
|
|
384
|
+
for (let k = i; k < j; k++) {
|
|
385
|
+
if (text.charCodeAt(k) !== HYPHEN) return false;
|
|
386
|
+
}
|
|
387
|
+
return left && right ? 'center' : left ? 'left' : right ? 'right' : null;
|
|
388
|
+
};
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Parse a table delimiter row (`| --- | :-: | ---: |`) into its per-column
|
|
392
|
+
* alignment, or `null` when the line is not a valid delimiter row. Each cell
|
|
393
|
+
* must be an optional leading `:`, one or more `-`, and an optional trailing
|
|
394
|
+
* `:`. Splits with `mdz_split_table_row` so the column count matches the header.
|
|
395
|
+
*
|
|
396
|
+
* @nodocs
|
|
397
|
+
*/
|
|
398
|
+
export const mdz_parse_table_delimiter = (
|
|
399
|
+
text: string,
|
|
400
|
+
line_start: number,
|
|
401
|
+
line_end: number,
|
|
402
|
+
): Array<MdzTableAlign> | null => {
|
|
403
|
+
const cells = mdz_split_table_row(text, line_start, line_end);
|
|
404
|
+
if (cells === null) return null;
|
|
405
|
+
const align: Array<MdzTableAlign> = [];
|
|
406
|
+
for (const cell of cells) {
|
|
407
|
+
const a = table_delimiter_align(text, cell.start, cell.end);
|
|
408
|
+
if (a === false) return null;
|
|
409
|
+
align.push(a);
|
|
410
|
+
}
|
|
411
|
+
return align;
|
|
412
|
+
};
|
|
413
|
+
|
|
290
414
|
/**
|
|
291
415
|
* Lowercase an ASCII letter code; passes other chars through.
|
|
292
416
|
* Mirrors the `[A-Z] → [a-z]` shift without touching non-ASCII.
|
package/src/lib/mdz_lexer.ts
CHANGED
|
@@ -47,7 +47,13 @@ import {
|
|
|
47
47
|
mdz_blockquote_line_has_content,
|
|
48
48
|
mdz_blockquote_strip_width,
|
|
49
49
|
mdz_remap_segments,
|
|
50
|
-
|
|
50
|
+
mdz_split_table_row,
|
|
51
|
+
mdz_parse_table_delimiter,
|
|
52
|
+
PIPE,
|
|
53
|
+
BACKSLASH,
|
|
54
|
+
} from './mdz_helpers.ts';
|
|
55
|
+
|
|
56
|
+
import type {MdzTableAlign} from './mdz.ts';
|
|
51
57
|
|
|
52
58
|
//
|
|
53
59
|
// Token types
|
|
@@ -84,7 +90,13 @@ export type MdzToken =
|
|
|
84
90
|
| MdzTokenListItemOpen
|
|
85
91
|
| MdzTokenListItemClose
|
|
86
92
|
| MdzTokenBlockquoteOpen
|
|
87
|
-
| MdzTokenBlockquoteClose
|
|
93
|
+
| MdzTokenBlockquoteClose
|
|
94
|
+
| MdzTokenTableOpen
|
|
95
|
+
| MdzTokenTableClose
|
|
96
|
+
| MdzTokenTableRowOpen
|
|
97
|
+
| MdzTokenTableRowClose
|
|
98
|
+
| MdzTokenTableCellOpen
|
|
99
|
+
| MdzTokenTableCellClose;
|
|
88
100
|
|
|
89
101
|
export interface MdzTokenText extends MdzTokenBase {
|
|
90
102
|
type: 'text';
|
|
@@ -207,6 +219,34 @@ export interface MdzTokenBlockquoteClose extends MdzTokenBase {
|
|
|
207
219
|
type: 'blockquote_close';
|
|
208
220
|
}
|
|
209
221
|
|
|
222
|
+
export interface MdzTokenTableOpen extends MdzTokenBase {
|
|
223
|
+
type: 'table_open';
|
|
224
|
+
/** Per-column alignment from the delimiter row; length is the column count. */
|
|
225
|
+
align: Array<MdzTableAlign>;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export interface MdzTokenTableClose extends MdzTokenBase {
|
|
229
|
+
type: 'table_close';
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export interface MdzTokenTableRowOpen extends MdzTokenBase {
|
|
233
|
+
type: 'table_row_open';
|
|
234
|
+
/** `true` for the header row, `false` for body rows. */
|
|
235
|
+
header: boolean;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
export interface MdzTokenTableRowClose extends MdzTokenBase {
|
|
239
|
+
type: 'table_row_close';
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export interface MdzTokenTableCellOpen extends MdzTokenBase {
|
|
243
|
+
type: 'table_cell_open';
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
export interface MdzTokenTableCellClose extends MdzTokenBase {
|
|
247
|
+
type: 'table_cell_close';
|
|
248
|
+
}
|
|
249
|
+
|
|
210
250
|
/**
|
|
211
251
|
* A matched list marker: `- ` (unordered) or `N. ` (ordered, 1-9 digits).
|
|
212
252
|
* `empty` markers have nothing but whitespace after the marker — valid only
|
|
@@ -258,6 +298,12 @@ export class MdzLexer {
|
|
|
258
298
|
* scanner pre-validated every line in the run as continuation).
|
|
259
299
|
*/
|
|
260
300
|
#list_run_strip: boolean = false;
|
|
301
|
+
/**
|
|
302
|
+
* Whether `#tokenize_text` is inside a table cell: `#max_search_index` is a
|
|
303
|
+
* hard stop (the cell's trimmed end, mid-line with no newline to halt on)
|
|
304
|
+
* and `\|` unescapes to a literal pipe.
|
|
305
|
+
*/
|
|
306
|
+
#in_table_cell: boolean = false;
|
|
261
307
|
|
|
262
308
|
constructor(text: string) {
|
|
263
309
|
this.#text = text;
|
|
@@ -300,6 +346,7 @@ export class MdzLexer {
|
|
|
300
346
|
if (this.#tokenize_codeblock()) continue;
|
|
301
347
|
if (this.#tokenize_list()) continue;
|
|
302
348
|
if (this.#tokenize_blockquote()) continue;
|
|
349
|
+
if (this.#tokenize_table()) continue;
|
|
303
350
|
}
|
|
304
351
|
|
|
305
352
|
// Check for paragraph break
|
|
@@ -317,6 +364,22 @@ export class MdzLexer {
|
|
|
317
364
|
return this.#tokens;
|
|
318
365
|
}
|
|
319
366
|
|
|
367
|
+
/**
|
|
368
|
+
* Tokenize a table cell's inline content `[cell_start, cell_end)` as a
|
|
369
|
+
* bounded inline run, with no wrapping cell tokens (the bare form of
|
|
370
|
+
* `#emit_table_cell`, sharing `#tokenize_cell_inline`). The streaming parser
|
|
371
|
+
* reuses it across a row's cells via `MdzTableCellParser`: the token buffer
|
|
372
|
+
* resets per call while the search memo — valid over the immutable bound
|
|
373
|
+
* text — carries across cells (the same reuse the sync lexer does in place).
|
|
374
|
+
*
|
|
375
|
+
* @nodocs
|
|
376
|
+
*/
|
|
377
|
+
lex_table_cell(cell_start: number, cell_end: number): Array<MdzToken> {
|
|
378
|
+
this.#tokens = [];
|
|
379
|
+
this.#tokenize_cell_inline(cell_start, cell_end);
|
|
380
|
+
return this.#tokens;
|
|
381
|
+
}
|
|
382
|
+
|
|
320
383
|
#is_at_column_0(): boolean {
|
|
321
384
|
return this.#index === 0 || this.#text.charCodeAt(this.#index - 1) === NEWLINE;
|
|
322
385
|
}
|
|
@@ -604,6 +667,163 @@ export class MdzLexer {
|
|
|
604
667
|
this.#index = last_line_end;
|
|
605
668
|
}
|
|
606
669
|
|
|
670
|
+
// -- Table tokenizers --
|
|
671
|
+
|
|
672
|
+
/**
|
|
673
|
+
* Tokenize a table starting at a column-0 pipe row, emitting
|
|
674
|
+
* `table_open`/`table_row_open`/`table_cell_open` … structure tokens (the
|
|
675
|
+
* header row's cells first, then each body row's). Returns false with no
|
|
676
|
+
* side effects when the current line isn't a table: not a `| … |` row, or
|
|
677
|
+
* not followed by a delimiter row whose column count matches the header. The
|
|
678
|
+
* delimiter row's colons set `align`; body rows continue until a blank line,
|
|
679
|
+
* a non-pipe-row line, or EOF.
|
|
680
|
+
*/
|
|
681
|
+
#tokenize_table(): boolean {
|
|
682
|
+
const table_start = this.#index;
|
|
683
|
+
let header_end = this.#index_of('\n', this.#index);
|
|
684
|
+
if (header_end === -1) header_end = this.#text.length;
|
|
685
|
+
const header_cells = mdz_split_table_row(this.#text, this.#index, header_end);
|
|
686
|
+
if (header_cells === null) return false;
|
|
687
|
+
if (header_end >= this.#text.length) return false; // a header needs a delimiter row beneath it
|
|
688
|
+
const delim_start = header_end + 1;
|
|
689
|
+
let delim_end = this.#index_of('\n', delim_start);
|
|
690
|
+
if (delim_end === -1) delim_end = this.#text.length;
|
|
691
|
+
const align = mdz_parse_table_delimiter(this.#text, delim_start, delim_end);
|
|
692
|
+
if (align === null || align.length !== header_cells.length) return false;
|
|
693
|
+
|
|
694
|
+
// committed — emit the table; the delimiter row is structural (not emitted)
|
|
695
|
+
this.#tokens.push({type: 'table_open', align, start: table_start, end: header_end});
|
|
696
|
+
this.#emit_table_row(header_cells, true, table_start, header_end);
|
|
697
|
+
|
|
698
|
+
this.#index = delim_end;
|
|
699
|
+
let table_end = delim_end;
|
|
700
|
+
|
|
701
|
+
// body rows: consecutive valid pipe rows, until a blank/non-row line or EOF
|
|
702
|
+
while (this.#index < this.#text.length) {
|
|
703
|
+
const row_start = this.#index + 1; // past the newline at #index
|
|
704
|
+
let row_end = this.#index_of('\n', row_start);
|
|
705
|
+
if (row_end === -1) row_end = this.#text.length;
|
|
706
|
+
const cells = mdz_split_table_row(this.#text, row_start, row_end);
|
|
707
|
+
if (cells === null) break;
|
|
708
|
+
this.#emit_table_row(cells, false, row_start, row_end);
|
|
709
|
+
this.#index = row_end;
|
|
710
|
+
table_end = row_end;
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
this.#tokens.push({type: 'table_close', start: table_end, end: table_end});
|
|
714
|
+
this.#skip_blank_lines();
|
|
715
|
+
return true;
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
/** Emit one table row's open / cell / close tokens. */
|
|
719
|
+
#emit_table_row(
|
|
720
|
+
cells: Array<{start: number; end: number}>,
|
|
721
|
+
header: boolean,
|
|
722
|
+
row_start: number,
|
|
723
|
+
row_end: number,
|
|
724
|
+
): void {
|
|
725
|
+
this.#tokens.push({type: 'table_row_open', header, start: row_start, end: row_start});
|
|
726
|
+
for (const cell of cells) {
|
|
727
|
+
this.#emit_table_cell(cell.start, cell.end);
|
|
728
|
+
}
|
|
729
|
+
this.#tokens.push({type: 'table_row_close', start: row_end, end: row_end});
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
/**
|
|
733
|
+
* Emit one cell's `table_cell_open`, inline-content tokens, and
|
|
734
|
+
* `table_cell_close`. Inline tokenization is bounded to the cell's trimmed
|
|
735
|
+
* span via `#max_search_index`, with `#in_table_cell` set so `#tokenize_text`
|
|
736
|
+
* stops at that bound and unescapes `\|`.
|
|
737
|
+
*/
|
|
738
|
+
#emit_table_cell(cell_start: number, cell_end: number): void {
|
|
739
|
+
this.#tokens.push({type: 'table_cell_open', start: cell_start, end: cell_start});
|
|
740
|
+
this.#tokenize_cell_inline(cell_start, cell_end);
|
|
741
|
+
this.#tokens.push({type: 'table_cell_close', start: cell_end, end: cell_end});
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
/**
|
|
745
|
+
* Tokenize a cell's trimmed inline span `[cell_start, cell_end)` into the
|
|
746
|
+
* current token buffer, bounded by `#max_search_index` with `#in_table_cell`
|
|
747
|
+
* set so `#tokenize_text` stops at the bound and unescapes `\|`. Shared by
|
|
748
|
+
* `#emit_table_cell` (wrapped in cell tokens) and `lex_table_cell` (bare).
|
|
749
|
+
*/
|
|
750
|
+
#tokenize_cell_inline(cell_start: number, cell_end: number): void {
|
|
751
|
+
const saved_max = this.#max_search_index;
|
|
752
|
+
this.#max_search_index = cell_end;
|
|
753
|
+
this.#in_table_cell = true;
|
|
754
|
+
this.#index = cell_start;
|
|
755
|
+
while (this.#index < cell_end) {
|
|
756
|
+
this.#tokenize_inline();
|
|
757
|
+
}
|
|
758
|
+
this.#in_table_cell = false;
|
|
759
|
+
this.#max_search_index = saved_max;
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
/**
|
|
763
|
+
* Recognize an in-item table whose header row's leading `|` is at `first`
|
|
764
|
+
* (the line's indent already skipped): a pipe row whose next line is a
|
|
765
|
+
* delimiter row, both indented past `marker_indent` (so both stay inside the
|
|
766
|
+
* item). Returns the parsed header/delimiter, or null. Pure — no emission.
|
|
767
|
+
*/
|
|
768
|
+
#match_item_table_head(
|
|
769
|
+
first: number,
|
|
770
|
+
marker_indent: number,
|
|
771
|
+
): {
|
|
772
|
+
header_cells: Array<{start: number; end: number}>;
|
|
773
|
+
header_end: number;
|
|
774
|
+
align: Array<MdzTableAlign>;
|
|
775
|
+
delim_end: number;
|
|
776
|
+
} | null {
|
|
777
|
+
const header_end = this.#index_of('\n', first);
|
|
778
|
+
if (header_end === -1) return null; // a header needs a delimiter line beneath it
|
|
779
|
+
const header_cells = mdz_split_table_row(this.#text, first, header_end);
|
|
780
|
+
if (header_cells === null) return null;
|
|
781
|
+
const delim_line_start = header_end + 1;
|
|
782
|
+
let dj = delim_line_start;
|
|
783
|
+
while (dj < this.#text.length && is_line_whitespace(this.#text.charCodeAt(dj))) dj++;
|
|
784
|
+
if (dj - delim_line_start <= marker_indent) return null; // delimiter dedented out of the item
|
|
785
|
+
let delim_end = this.#index_of('\n', dj);
|
|
786
|
+
if (delim_end === -1) delim_end = this.#text.length;
|
|
787
|
+
const align = mdz_parse_table_delimiter(this.#text, dj, delim_end);
|
|
788
|
+
if (align === null || align.length !== header_cells.length) return null;
|
|
789
|
+
return {header_cells, header_end, align, delim_end};
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
/** Whether a table opens at `first` as a block child of an item at `marker_indent`. */
|
|
793
|
+
#is_item_table_start(first: number, marker_indent: number): boolean {
|
|
794
|
+
return this.#match_item_table_head(first, marker_indent) !== null;
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
/**
|
|
798
|
+
* Tokenize an in-item table at `first` (a recognized table start). Mirrors
|
|
799
|
+
* `#tokenize_table` but per row skips the line's indent to the `|` and ends
|
|
800
|
+
* when a row dedents to `marker_indent` or shallower, leaving `#index` at the
|
|
801
|
+
* table's terminating newline so the list loop classifies what follows (no
|
|
802
|
+
* `#skip_blank_lines` — the list owns blank-line containment).
|
|
803
|
+
*/
|
|
804
|
+
#tokenize_item_table(first: number, marker_indent: number): void {
|
|
805
|
+
const head = this.#match_item_table_head(first, marker_indent)!;
|
|
806
|
+
this.#tokens.push({type: 'table_open', align: head.align, start: first, end: head.header_end});
|
|
807
|
+
this.#emit_table_row(head.header_cells, true, first, head.header_end);
|
|
808
|
+
this.#index = head.delim_end;
|
|
809
|
+
let table_end = head.delim_end;
|
|
810
|
+
while (this.#index < this.#text.length) {
|
|
811
|
+
const row_line_start = this.#index + 1; // past the newline at #index
|
|
812
|
+
let rj = row_line_start;
|
|
813
|
+
while (rj < this.#text.length && is_line_whitespace(this.#text.charCodeAt(rj))) rj++;
|
|
814
|
+
if (rj - row_line_start <= marker_indent) break; // dedent ends the table
|
|
815
|
+
if (this.#text.charCodeAt(rj) !== PIPE) break;
|
|
816
|
+
let row_end = this.#index_of('\n', rj);
|
|
817
|
+
if (row_end === -1) row_end = this.#text.length;
|
|
818
|
+
const cells = mdz_split_table_row(this.#text, rj, row_end);
|
|
819
|
+
if (cells === null) break;
|
|
820
|
+
this.#emit_table_row(cells, false, rj, row_end);
|
|
821
|
+
this.#index = row_end;
|
|
822
|
+
table_end = row_end;
|
|
823
|
+
}
|
|
824
|
+
this.#tokens.push({type: 'table_close', start: table_end, end: table_end});
|
|
825
|
+
}
|
|
826
|
+
|
|
607
827
|
// -- List tokenizers --
|
|
608
828
|
|
|
609
829
|
/**
|
|
@@ -739,6 +959,23 @@ export class MdzLexer {
|
|
|
739
959
|
}
|
|
740
960
|
}
|
|
741
961
|
|
|
962
|
+
// a deeper pipe-row line whose next line is a delimiter becomes a
|
|
963
|
+
// table child of the item it would otherwise continue (the fence/quote
|
|
964
|
+
// precedent, but a two-line lookahead)
|
|
965
|
+
if (indent > 0 && this.#text.charCodeAt(first) === PIPE) {
|
|
966
|
+
const target_idx = this.#list_continuation_target(levels, indent);
|
|
967
|
+
if (target_idx !== -1) {
|
|
968
|
+
const marker_indent = levels[target_idx]!.indent;
|
|
969
|
+
if (this.#is_item_table_start(first, marker_indent)) {
|
|
970
|
+
while (levels.length - 1 > target_idx) {
|
|
971
|
+
this.#list_close(levels);
|
|
972
|
+
}
|
|
973
|
+
this.#tokenize_item_table(first, marker_indent);
|
|
974
|
+
continue;
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
|
|
742
979
|
// any other line deeper than an open item's marker opens a new
|
|
743
980
|
// `Paragraph` block child of that item (the item's inline run is never
|
|
744
981
|
// current here — a blank, block child, or dedent intervened)
|
|
@@ -947,6 +1184,9 @@ export class MdzLexer {
|
|
|
947
1184
|
if (this.#text.charCodeAt(j) === RIGHT_ANGLE && this.#is_blockquote_opener(j)) {
|
|
948
1185
|
return nl; // a quote opener line interrupts the run
|
|
949
1186
|
}
|
|
1187
|
+
if (this.#text.charCodeAt(j) === PIPE && this.#is_item_table_start(j, deepest_indent)) {
|
|
1188
|
+
return nl; // a table opener (pipe row + delimiter) interrupts the run
|
|
1189
|
+
}
|
|
950
1190
|
i = line_start; // continuation — keep scanning
|
|
951
1191
|
}
|
|
952
1192
|
}
|
|
@@ -1543,6 +1783,24 @@ export class MdzLexer {
|
|
|
1543
1783
|
while (this.#index < this.#text.length) {
|
|
1544
1784
|
const char_code = this.#text.charCodeAt(this.#index);
|
|
1545
1785
|
|
|
1786
|
+
// in a table cell: the trimmed cell end is a hard stop (no newline to
|
|
1787
|
+
// halt on mid-line), and `\|` is a literal pipe (the only mdz escape,
|
|
1788
|
+
// scoped here)
|
|
1789
|
+
if (this.#in_table_cell) {
|
|
1790
|
+
if (this.#index >= this.#max_search_index) break;
|
|
1791
|
+
if (
|
|
1792
|
+
char_code === BACKSLASH &&
|
|
1793
|
+
this.#index + 1 < this.#max_search_index &&
|
|
1794
|
+
this.#text.charCodeAt(this.#index + 1) === PIPE
|
|
1795
|
+
) {
|
|
1796
|
+
(parts ??= []).push(this.#text.slice(part_start, this.#index));
|
|
1797
|
+
parts.push('|');
|
|
1798
|
+
this.#index += 2;
|
|
1799
|
+
part_start = this.#index;
|
|
1800
|
+
continue;
|
|
1801
|
+
}
|
|
1802
|
+
}
|
|
1803
|
+
|
|
1546
1804
|
// Stop at special characters
|
|
1547
1805
|
if (
|
|
1548
1806
|
char_code === BACKTICK ||
|
|
@@ -1588,7 +1846,8 @@ export class MdzLexer {
|
|
|
1588
1846
|
next_char === HYPHEN ||
|
|
1589
1847
|
next_char === BACKTICK ||
|
|
1590
1848
|
next_char === ONE ||
|
|
1591
|
-
next_char === RIGHT_ANGLE
|
|
1849
|
+
next_char === RIGHT_ANGLE ||
|
|
1850
|
+
next_char === PIPE
|
|
1592
1851
|
) {
|
|
1593
1852
|
this.#index++; // consume the newline
|
|
1594
1853
|
break;
|
package/src/lib/mdz_opcodes.ts
CHANGED
|
@@ -12,6 +12,8 @@
|
|
|
12
12
|
* @module
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
+
import type {MdzTableAlign} from './mdz.ts';
|
|
16
|
+
|
|
15
17
|
/**
|
|
16
18
|
* Unique monotonic identifier for each node created by the parser.
|
|
17
19
|
* IDs are never reused within a parser instance.
|
|
@@ -39,7 +41,10 @@ export type MdzNodeTypeContainer =
|
|
|
39
41
|
| 'Element'
|
|
40
42
|
| 'Component'
|
|
41
43
|
| 'Codeblock'
|
|
42
|
-
| 'Code'
|
|
44
|
+
| 'Code'
|
|
45
|
+
| 'Table'
|
|
46
|
+
| 'TableRow'
|
|
47
|
+
| 'TableCell';
|
|
43
48
|
|
|
44
49
|
/** Node types for self-contained leaf elements. */
|
|
45
50
|
export type MdzNodeTypeVoid = 'Hr';
|
|
@@ -69,6 +74,10 @@ export interface MdzOpcodeOpen {
|
|
|
69
74
|
start_number?: number;
|
|
70
75
|
/** Authored item number. Present when `node_type` is `'ListItem'` in an ordered list. */
|
|
71
76
|
number?: number;
|
|
77
|
+
/** Per-column alignment from the delimiter row. Present when `node_type` is `'Table'`. */
|
|
78
|
+
align?: Array<MdzTableAlign>;
|
|
79
|
+
/** Whether this is the header row (`thead`/`th`). Present when `node_type` is `'TableRow'`. */
|
|
80
|
+
header?: boolean;
|
|
72
81
|
}
|
|
73
82
|
|
|
74
83
|
/**
|