@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
|
@@ -27,6 +27,10 @@ export class MdzStreamNode {
|
|
|
27
27
|
ordered;
|
|
28
28
|
start_number;
|
|
29
29
|
number;
|
|
30
|
+
/** Per-column alignment (Table nodes). */
|
|
31
|
+
align;
|
|
32
|
+
/** Whether this is the header row (TableRow nodes). */
|
|
33
|
+
header;
|
|
30
34
|
constructor(id, type) {
|
|
31
35
|
this.id = id;
|
|
32
36
|
this.type = type;
|
|
@@ -96,6 +100,10 @@ export class MdzStreamState {
|
|
|
96
100
|
node.start_number = opcode.start_number;
|
|
97
101
|
if (opcode.number !== undefined)
|
|
98
102
|
node.number = opcode.number;
|
|
103
|
+
if (opcode.align !== undefined)
|
|
104
|
+
node.align = opcode.align;
|
|
105
|
+
if (opcode.header !== undefined)
|
|
106
|
+
node.header = opcode.header;
|
|
99
107
|
this.#attach(node);
|
|
100
108
|
this.#stack.push(opcode.id);
|
|
101
109
|
break;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mdz_to_svelte.d.ts","sourceRoot":"../src/lib/","sources":["../src/lib/mdz_to_svelte.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAMH,OAAO,KAAK,EAAC,OAAO,
|
|
1
|
+
{"version":3,"file":"mdz_to_svelte.d.ts","sourceRoot":"../src/lib/","sources":["../src/lib/mdz_to_svelte.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAMH,OAAO,KAAK,EAAC,OAAO,EAAkB,MAAM,UAAU,CAAC;AAGvD;;GAEG;AACH,MAAM,WAAW,iBAAiB;IACjC,sCAAsC;IACtC,MAAM,EAAE,MAAM,CAAC;IAEf,yDAAyD;IACzD,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,SAAS,GAAG,OAAO,CAAA;KAAC,CAAC,CAAC;IAEhE,yEAAyE;IACzE,qBAAqB,EAAE,OAAO,CAAC;CAC/B;AAMD;;GAEG;AACH,MAAM,WAAW,kBAAkB;IAClC;;;OAGG;IACH,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAEnC;;;;;OAKG;IACH,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAElC;;;;OAIG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;IAEd;;;;;OAKG;IACH,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAE/B;;;;OAIG;IACH,0BAA0B,CAAC,EAAE,MAAM,CAAC;CACpC;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,aAAa,GACzB,OAAO,KAAK,CAAC,OAAO,CAAC,EACrB,SAAS,kBAAkB,KACzB,iBAsJF,CAAC"}
|
package/dist/mdz_to_svelte.js
CHANGED
|
@@ -7,10 +7,10 @@
|
|
|
7
7
|
*
|
|
8
8
|
* @module
|
|
9
9
|
*/
|
|
10
|
-
import { UnreachableError } from '@fuzdev/fuz_util/error.
|
|
11
|
-
import { escape_svelte_text } from '@fuzdev/fuz_util/svelte_preprocess_helpers.
|
|
12
|
-
import { escape_js_string } from '@fuzdev/fuz_util/string.
|
|
13
|
-
import {
|
|
10
|
+
import { UnreachableError } from '@fuzdev/fuz_util/error.ts';
|
|
11
|
+
import { escape_svelte_text } from '@fuzdev/fuz_util/svelte_preprocess_helpers.ts';
|
|
12
|
+
import { escape_js_string } from '@fuzdev/fuz_util/string.ts';
|
|
13
|
+
import { mdz_is_void_element, mdz_classify_link } from './mdz_helpers.js';
|
|
14
14
|
/** Derives an import's local identifier from a `.svelte` import path's basename. */
|
|
15
15
|
const component_local_name = (import_path) => import_path.slice(import_path.lastIndexOf('/') + 1).replace(/\.svelte$/, '');
|
|
16
16
|
/**
|
|
@@ -58,24 +58,22 @@ export const mdz_to_svelte = (nodes, options) => {
|
|
|
58
58
|
return `<s>${render_nodes(node.children)}</s>`;
|
|
59
59
|
case 'Link': {
|
|
60
60
|
const children_markup = render_nodes(node.children);
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
// Bare references (e.g. `foo`) aren't absolute — resolve() throws on them
|
|
71
|
-
!node.reference.startsWith('/')) {
|
|
72
|
-
return `<a href={'${escape_js_string(node.reference)}'}>${children_markup}</a>`;
|
|
73
|
-
}
|
|
61
|
+
// safety gate + resolve-vs-raw classification shared with the runtime
|
|
62
|
+
// views via `mdz_classify_link`, so the three renderers can't drift
|
|
63
|
+
// (an unsafe `javascript:`/`data:` ref renders as plain children, no
|
|
64
|
+
// `<a>` — defense in depth, since `mdz_to_svelte` is a public entry
|
|
65
|
+
// callable with hand-built trees that never went through `mdz_parse`)
|
|
66
|
+
const link = mdz_classify_link(node.reference, node.link_type, base);
|
|
67
|
+
if (link.kind === 'unsafe')
|
|
68
|
+
return children_markup;
|
|
69
|
+
if (link.kind === 'resolve') {
|
|
74
70
|
imports.set('resolve', { path: '$app/paths', kind: 'named' });
|
|
75
|
-
return `<a href={resolve('${escape_js_string(
|
|
71
|
+
return `<a href={resolve('${escape_js_string(link.href)}')}>${children_markup}</a>`;
|
|
72
|
+
}
|
|
73
|
+
if (link.kind === 'external') {
|
|
74
|
+
return `<a href={'${escape_js_string(link.href)}'} target="_blank" rel="noopener">${children_markup}</a>`;
|
|
76
75
|
}
|
|
77
|
-
|
|
78
|
-
return `<a href={'${escape_js_string(node.reference)}'} target="_blank" rel="noopener">${children_markup}</a>`;
|
|
76
|
+
return `<a href={'${escape_js_string(link.href)}'}>${children_markup}</a>`;
|
|
79
77
|
}
|
|
80
78
|
case 'Paragraph':
|
|
81
79
|
return `<p>${render_nodes(node.children)}</p>`;
|
|
@@ -90,6 +88,39 @@ export const mdz_to_svelte = (nodes, options) => {
|
|
|
90
88
|
return `<li>${render_nodes(node.children)}</li>`;
|
|
91
89
|
case 'Blockquote':
|
|
92
90
|
return `<blockquote>${render_nodes(node.children)}</blockquote>`;
|
|
91
|
+
case 'Table': {
|
|
92
|
+
const { align } = node;
|
|
93
|
+
const render_row = (row, tag) => {
|
|
94
|
+
let cells = '';
|
|
95
|
+
// normalize to the column count: pad short rows, ignore extra cells
|
|
96
|
+
for (let i = 0; i < align.length; i++) {
|
|
97
|
+
const a = align[i];
|
|
98
|
+
const style = a ? ` style="text-align:${a}"` : '';
|
|
99
|
+
const cell = row.children[i];
|
|
100
|
+
cells += `<${tag}${style}>${cell ? render_nodes(cell.children) : ''}</${tag}>`;
|
|
101
|
+
}
|
|
102
|
+
return `<tr>${cells}</tr>`;
|
|
103
|
+
};
|
|
104
|
+
// by grammar the header row is always first (and the only one) —
|
|
105
|
+
// read it by index and skip it in the body, mirroring the node
|
|
106
|
+
// views (a `.filter()` pair re-partitions every row; see
|
|
107
|
+
// `MdzNodeView.svelte`'s "Table rendering de-quadratified" note)
|
|
108
|
+
let out = '<table>';
|
|
109
|
+
const header_row = node.children[0]?.header ? node.children[0] : undefined;
|
|
110
|
+
if (header_row)
|
|
111
|
+
out += '<thead>' + render_row(header_row, 'th') + '</thead>';
|
|
112
|
+
let body = '';
|
|
113
|
+
for (const row of node.children)
|
|
114
|
+
if (!row.header)
|
|
115
|
+
body += render_row(row, 'td');
|
|
116
|
+
if (body)
|
|
117
|
+
out += '<tbody>' + body + '</tbody>';
|
|
118
|
+
return out + '</table>';
|
|
119
|
+
}
|
|
120
|
+
case 'TableRow':
|
|
121
|
+
case 'TableCell':
|
|
122
|
+
// rendered within their `Table`; never reached through `render_nodes`
|
|
123
|
+
throw new Error(`mdz: ${node.type} must render within a Table`);
|
|
93
124
|
case 'Hr':
|
|
94
125
|
return '<hr />';
|
|
95
126
|
case 'Heading': {
|
|
@@ -15,6 +15,22 @@ import { type MdzToken } from './mdz_lexer.js';
|
|
|
15
15
|
export declare class MdzTokenParser {
|
|
16
16
|
#private;
|
|
17
17
|
constructor(tokens: Array<MdzToken>);
|
|
18
|
+
/**
|
|
19
|
+
* Rebind to a new token stream and rewind, so one parser instance can be
|
|
20
|
+
* reused across inputs — the streaming table path reuses one across a row's
|
|
21
|
+
* cells via `MdzTableCellParser`.
|
|
22
|
+
*
|
|
23
|
+
* @nodocs
|
|
24
|
+
*/
|
|
25
|
+
reset(tokens: Array<MdzToken>): void;
|
|
18
26
|
parse(): Array<MdzNode>;
|
|
27
|
+
/**
|
|
28
|
+
* Parse the entire token stream as a single inline run — used for table
|
|
29
|
+
* cells, whose tokens come from `MdzLexer.lex_table_cell`. Adjacent text
|
|
30
|
+
* merges as it accumulates, matching `#parse_table_cell`.
|
|
31
|
+
*
|
|
32
|
+
* @nodocs
|
|
33
|
+
*/
|
|
34
|
+
parse_inline_run(): Array<MdzNode>;
|
|
19
35
|
}
|
|
20
36
|
//# sourceMappingURL=mdz_token_parser.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mdz_token_parser.d.ts","sourceRoot":"../src/lib/","sources":["../src/lib/mdz_token_parser.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EACX,OAAO,
|
|
1
|
+
{"version":3,"file":"mdz_token_parser.d.ts","sourceRoot":"../src/lib/","sources":["../src/lib/mdz_token_parser.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EACX,OAAO,EAeP,MAAM,UAAU,CAAC;AAQlB,OAAO,EACN,KAAK,QAAQ,EAYb,MAAM,gBAAgB,CAAC;AAiBxB;;;GAGG;AACH,qBAAa,cAAc;;gBAId,MAAM,EAAE,KAAK,CAAC,QAAQ,CAAC;IAInC;;;;;;OAMG;IACH,KAAK,CAAC,MAAM,EAAE,KAAK,CAAC,QAAQ,CAAC,GAAG,IAAI;IAKpC,KAAK,IAAI,KAAK,CAAC,OAAO,CAAC;IAIvB;;;;;;OAMG;IACH,gBAAgB,IAAI,KAAK,CAAC,OAAO,CAAC;CA+iBlC"}
|
package/dist/mdz_token_parser.js
CHANGED
|
@@ -8,6 +8,19 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { has_non_whitespace, mdz_extract_single_tag, mdz_heading_id, mdz_merge_adjacent_text, mdz_push_merging_text, } from './mdz_helpers.js';
|
|
10
10
|
import {} from './mdz_lexer.js';
|
|
11
|
+
/**
|
|
12
|
+
* Whether a token type ends the current inline context — the block-boundary
|
|
13
|
+
* set shared by the delimiter-pair and link children loops (an unclosed
|
|
14
|
+
* inline never swallows block structure).
|
|
15
|
+
*/
|
|
16
|
+
const token_is_block_boundary = (type) => type === 'paragraph_break' ||
|
|
17
|
+
type === 'heading_start' ||
|
|
18
|
+
type === 'hr' ||
|
|
19
|
+
type === 'codeblock' ||
|
|
20
|
+
type === 'list_open' ||
|
|
21
|
+
type === 'list_item_close' ||
|
|
22
|
+
type === 'blockquote_open' ||
|
|
23
|
+
type === 'table_open';
|
|
11
24
|
/**
|
|
12
25
|
* Builds an `MdzNode[]` tree from a lexed `MdzToken[]` stream.
|
|
13
26
|
* Used by `mdz_parse`, which should be preferred for simple usage.
|
|
@@ -18,9 +31,36 @@ export class MdzTokenParser {
|
|
|
18
31
|
constructor(tokens) {
|
|
19
32
|
this.#tokens = tokens;
|
|
20
33
|
}
|
|
34
|
+
/**
|
|
35
|
+
* Rebind to a new token stream and rewind, so one parser instance can be
|
|
36
|
+
* reused across inputs — the streaming table path reuses one across a row's
|
|
37
|
+
* cells via `MdzTableCellParser`.
|
|
38
|
+
*
|
|
39
|
+
* @nodocs
|
|
40
|
+
*/
|
|
41
|
+
reset(tokens) {
|
|
42
|
+
this.#tokens = tokens;
|
|
43
|
+
this.#index = 0;
|
|
44
|
+
}
|
|
21
45
|
parse() {
|
|
22
46
|
return this.#parse_block_stream(false);
|
|
23
47
|
}
|
|
48
|
+
/**
|
|
49
|
+
* Parse the entire token stream as a single inline run — used for table
|
|
50
|
+
* cells, whose tokens come from `MdzLexer.lex_table_cell`. Adjacent text
|
|
51
|
+
* merges as it accumulates, matching `#parse_table_cell`.
|
|
52
|
+
*
|
|
53
|
+
* @nodocs
|
|
54
|
+
*/
|
|
55
|
+
parse_inline_run() {
|
|
56
|
+
const children = [];
|
|
57
|
+
while (this.#index < this.#tokens.length) {
|
|
58
|
+
const node = this.#parse_inline();
|
|
59
|
+
if (node)
|
|
60
|
+
mdz_push_merging_text(children, node);
|
|
61
|
+
}
|
|
62
|
+
return children;
|
|
63
|
+
}
|
|
24
64
|
/**
|
|
25
65
|
* Parse a run of block-level tokens into nodes — the document root, or a
|
|
26
66
|
* blockquote's contents (a quote's content is a mini-document, so both
|
|
@@ -80,6 +120,13 @@ export class MdzTokenParser {
|
|
|
80
120
|
nodes.push(this.#parse_blockquote());
|
|
81
121
|
continue;
|
|
82
122
|
}
|
|
123
|
+
if (token.type === 'table_open') {
|
|
124
|
+
const flushed = this.#flush_paragraph(paragraph_children, true);
|
|
125
|
+
if (flushed)
|
|
126
|
+
nodes.push(flushed);
|
|
127
|
+
nodes.push(this.#parse_table());
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
83
130
|
if (token.type === 'paragraph_break') {
|
|
84
131
|
const flushed = this.#flush_paragraph(paragraph_children, true);
|
|
85
132
|
if (flushed)
|
|
@@ -107,6 +154,63 @@ export class MdzTokenParser {
|
|
|
107
154
|
const end = children.length > 0 ? children[children.length - 1].end : open.end;
|
|
108
155
|
return { type: 'Blockquote', children, start: open.start, end };
|
|
109
156
|
}
|
|
157
|
+
#parse_table() {
|
|
158
|
+
const open = this.#tokens[this.#index];
|
|
159
|
+
this.#index++;
|
|
160
|
+
const rows = [];
|
|
161
|
+
while (this.#index < this.#tokens.length) {
|
|
162
|
+
const t = this.#tokens[this.#index];
|
|
163
|
+
if (t.type === 'table_row_open') {
|
|
164
|
+
rows.push(this.#parse_table_row());
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
if (t.type === 'table_close') {
|
|
168
|
+
this.#index++;
|
|
169
|
+
break;
|
|
170
|
+
}
|
|
171
|
+
this.#index++; // skip anything unexpected (defensive)
|
|
172
|
+
}
|
|
173
|
+
const end = rows.length > 0 ? rows[rows.length - 1].end : open.end;
|
|
174
|
+
return { type: 'Table', align: open.align, children: rows, start: open.start, end };
|
|
175
|
+
}
|
|
176
|
+
#parse_table_row() {
|
|
177
|
+
const open = this.#tokens[this.#index];
|
|
178
|
+
const header = open.header;
|
|
179
|
+
this.#index++;
|
|
180
|
+
const cells = [];
|
|
181
|
+
while (this.#index < this.#tokens.length) {
|
|
182
|
+
const t = this.#tokens[this.#index];
|
|
183
|
+
if (t.type === 'table_cell_open') {
|
|
184
|
+
cells.push(this.#parse_table_cell());
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
if (t.type === 'table_row_close') {
|
|
188
|
+
this.#index++;
|
|
189
|
+
return { type: 'TableRow', header, children: cells, start: open.start, end: t.end };
|
|
190
|
+
}
|
|
191
|
+
this.#index++; // skip anything unexpected (defensive)
|
|
192
|
+
}
|
|
193
|
+
const end = cells.length > 0 ? cells[cells.length - 1].end : open.end;
|
|
194
|
+
return { type: 'TableRow', header, children: cells, start: open.start, end };
|
|
195
|
+
}
|
|
196
|
+
#parse_table_cell() {
|
|
197
|
+
const open = this.#tokens[this.#index];
|
|
198
|
+
const start = open.start;
|
|
199
|
+
this.#index++;
|
|
200
|
+
const children = [];
|
|
201
|
+
while (this.#index < this.#tokens.length) {
|
|
202
|
+
const t = this.#tokens[this.#index];
|
|
203
|
+
if (t.type === 'table_cell_close') {
|
|
204
|
+
this.#index++;
|
|
205
|
+
return { type: 'TableCell', children, start, end: t.end };
|
|
206
|
+
}
|
|
207
|
+
const node = this.#parse_inline();
|
|
208
|
+
if (node)
|
|
209
|
+
mdz_push_merging_text(children, node);
|
|
210
|
+
}
|
|
211
|
+
const end = children.length > 0 ? children[children.length - 1].end : open.end;
|
|
212
|
+
return { type: 'TableCell', children, start, end };
|
|
213
|
+
}
|
|
110
214
|
#parse_heading() {
|
|
111
215
|
const token = this.#tokens[this.#index];
|
|
112
216
|
const start = token.start;
|
|
@@ -188,6 +292,12 @@ export class MdzTokenParser {
|
|
|
188
292
|
children.push(this.#parse_blockquote());
|
|
189
293
|
continue;
|
|
190
294
|
}
|
|
295
|
+
if (t.type === 'table_open') {
|
|
296
|
+
flush();
|
|
297
|
+
in_paragraph = false;
|
|
298
|
+
children.push(this.#parse_table());
|
|
299
|
+
continue;
|
|
300
|
+
}
|
|
191
301
|
if (t.type === 'codeblock') {
|
|
192
302
|
flush();
|
|
193
303
|
in_paragraph = false;
|
|
@@ -301,13 +411,7 @@ export class MdzTokenParser {
|
|
|
301
411
|
this.#index++;
|
|
302
412
|
return { type: node_type, children, start, end: t.end };
|
|
303
413
|
}
|
|
304
|
-
if (t.type
|
|
305
|
-
t.type === 'heading_start' ||
|
|
306
|
-
t.type === 'hr' ||
|
|
307
|
-
t.type === 'codeblock' ||
|
|
308
|
-
t.type === 'list_open' ||
|
|
309
|
-
t.type === 'list_item_close' ||
|
|
310
|
-
t.type === 'blockquote_open') {
|
|
414
|
+
if (token_is_block_boundary(t.type)) {
|
|
311
415
|
break;
|
|
312
416
|
}
|
|
313
417
|
const node = this.#parse_inline();
|
|
@@ -342,13 +446,7 @@ export class MdzTokenParser {
|
|
|
342
446
|
// No link_ref - treat as text
|
|
343
447
|
return { type: 'Text', content: '[', start, end: start + 1 };
|
|
344
448
|
}
|
|
345
|
-
if (t.type
|
|
346
|
-
t.type === 'heading_start' ||
|
|
347
|
-
t.type === 'hr' ||
|
|
348
|
-
t.type === 'codeblock' ||
|
|
349
|
-
t.type === 'list_open' ||
|
|
350
|
-
t.type === 'list_item_close' ||
|
|
351
|
-
t.type === 'blockquote_open') {
|
|
449
|
+
if (token_is_block_boundary(t.type)) {
|
|
352
450
|
break;
|
|
353
451
|
}
|
|
354
452
|
const node = this.#parse_inline();
|
|
@@ -16,8 +16,8 @@
|
|
|
16
16
|
import { parse } from 'svelte/compiler';
|
|
17
17
|
import MagicString from 'magic-string';
|
|
18
18
|
import { walk } from 'zimmerframe';
|
|
19
|
-
import { should_exclude_path } from '@fuzdev/fuz_util/path.
|
|
20
|
-
import { find_attribute, extract_static_string, try_extract_conditional_chain, build_static_bindings, resolve_component_names, has_identifier_in_tree, find_import_insert_position, generate_import_lines, remove_variable_declaration, remove_import_declaration, remove_import_specifier, handle_preprocess_error, } from '@fuzdev/fuz_util/svelte_preprocess_helpers.
|
|
19
|
+
import { should_exclude_path } from '@fuzdev/fuz_util/path.ts';
|
|
20
|
+
import { find_attribute, extract_static_string, try_extract_conditional_chain, build_static_bindings, resolve_component_names, has_identifier_in_tree, find_import_insert_position, generate_import_lines, remove_variable_declaration, remove_import_declaration, remove_import_specifier, handle_preprocess_error, } from '@fuzdev/fuz_util/svelte_preprocess_helpers.ts';
|
|
21
21
|
import { mdz_parse } from './mdz.js';
|
|
22
22
|
import { mdz_to_svelte } from './mdz_to_svelte.js';
|
|
23
23
|
const PRECOMPILED_NAME = 'MdzPrecompiled';
|
|
@@ -138,6 +138,29 @@ const collect_consumed_bindings = (value, bindings) => {
|
|
|
138
138
|
collect_from_expr(value.expression);
|
|
139
139
|
return consumed;
|
|
140
140
|
};
|
|
141
|
+
/**
|
|
142
|
+
* Parse a static mdz string and render it to Svelte markup. Returns `null` to
|
|
143
|
+
* leave the usage for the runtime — either the content raised a parse error
|
|
144
|
+
* (reported via `on_error`) or it contains unconfigured tags (which the
|
|
145
|
+
* precompiler emits nothing for; the runtime renders the fallback chrome).
|
|
146
|
+
*/
|
|
147
|
+
const render_static_mdz = (content, base, context) => {
|
|
148
|
+
let result;
|
|
149
|
+
try {
|
|
150
|
+
result = mdz_to_svelte(mdz_parse(content), {
|
|
151
|
+
components: context.components,
|
|
152
|
+
elements: context.elements,
|
|
153
|
+
base,
|
|
154
|
+
code_component_import: context.code_component_import,
|
|
155
|
+
codeblock_component_import: context.codeblock_component_import,
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
catch (error) {
|
|
159
|
+
handle_preprocess_error(error, '[fuz-mdz]', context.filename, context.on_error);
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
return result.has_unconfigured_tags ? null : result;
|
|
163
|
+
};
|
|
141
164
|
/**
|
|
142
165
|
* Walks the AST to find `Mdz` component usages with static `content` props
|
|
143
166
|
* and generates transformations to replace them with `MdzPrecompiled` children.
|
|
@@ -181,25 +204,9 @@ const find_mdz_usages = (ast, mdz_names, context) => {
|
|
|
181
204
|
// Extract static string value
|
|
182
205
|
const content_value = extract_static_string(content_attr.value, context.bindings);
|
|
183
206
|
if (content_value !== null) {
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
const nodes = mdz_parse(content_value);
|
|
188
|
-
result = mdz_to_svelte(nodes, {
|
|
189
|
-
components: context.components,
|
|
190
|
-
elements: context.elements,
|
|
191
|
-
base,
|
|
192
|
-
code_component_import: context.code_component_import,
|
|
193
|
-
codeblock_component_import: context.codeblock_component_import,
|
|
194
|
-
});
|
|
195
|
-
}
|
|
196
|
-
catch (error) {
|
|
197
|
-
handle_preprocess_error(error, '[fuz-mdz]', context.filename, context.on_error);
|
|
198
|
-
return;
|
|
199
|
-
}
|
|
200
|
-
// If content has unconfigured tags, skip this usage (fall back to runtime)
|
|
201
|
-
if (result.has_unconfigured_tags)
|
|
202
|
-
return;
|
|
207
|
+
const result = render_static_mdz(content_value, base, context);
|
|
208
|
+
if (result === null)
|
|
209
|
+
return; // parse error or unconfigured tags — leave for runtime
|
|
203
210
|
const consumed = collect_consumed_bindings(content_attr.value, context.bindings);
|
|
204
211
|
const replacement = build_replacement(node, exclude_attrs, result.markup, context.source);
|
|
205
212
|
transformed_usages.set(node.name, (transformed_usages.get(node.name) ?? 0) + 1);
|
|
@@ -219,24 +226,11 @@ const find_mdz_usages = (ast, mdz_names, context) => {
|
|
|
219
226
|
return;
|
|
220
227
|
// Parse and render each branch
|
|
221
228
|
const branch_results = [];
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
elements: context.elements,
|
|
228
|
-
base,
|
|
229
|
-
code_component_import: context.code_component_import,
|
|
230
|
-
codeblock_component_import: context.codeblock_component_import,
|
|
231
|
-
});
|
|
232
|
-
if (result.has_unconfigured_tags)
|
|
233
|
-
return;
|
|
234
|
-
branch_results.push({ markup: result.markup, imports: result.imports });
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
catch (error) {
|
|
238
|
-
handle_preprocess_error(error, '[fuz-mdz]', context.filename, context.on_error);
|
|
239
|
-
return;
|
|
229
|
+
for (const branch of chain) {
|
|
230
|
+
const result = render_static_mdz(branch.value, base, context);
|
|
231
|
+
if (result === null)
|
|
232
|
+
return; // parse error or unconfigured tags — leave for runtime
|
|
233
|
+
branch_results.push({ markup: result.markup, imports: result.imports });
|
|
240
234
|
}
|
|
241
235
|
// Build {#if}/{:else if}/{:else} markup
|
|
242
236
|
let children_markup = '';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fuzdev/mdz",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.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": "📝",
|
|
@@ -30,7 +30,10 @@
|
|
|
30
30
|
"deploy": "gro deploy",
|
|
31
31
|
"benchmark": "gro run src/benchmarks/mdz.benchmark.ts",
|
|
32
32
|
"benchmark:save": "gro run src/benchmarks/mdz.benchmark.ts --save",
|
|
33
|
-
"benchmark:clean": "rm -f src/benchmarks/baseline.json"
|
|
33
|
+
"benchmark:clean": "rm -f src/benchmarks/baseline.json",
|
|
34
|
+
"benchmark:render": "BENCHMARK_RENDER=1 gro test render.benchmark",
|
|
35
|
+
"benchmark:render:save": "BENCHMARK_RENDER=save gro test render.benchmark",
|
|
36
|
+
"benchmark:render:clean": "rm -f src/benchmarks/render/baseline.json"
|
|
34
37
|
},
|
|
35
38
|
"type": "module",
|
|
36
39
|
"engines": {
|
|
@@ -57,15 +60,15 @@
|
|
|
57
60
|
},
|
|
58
61
|
"devDependencies": {
|
|
59
62
|
"@fuzdev/blake3_wasm": "^0.1.1",
|
|
60
|
-
"@fuzdev/fuz_code": "^0.46.
|
|
61
|
-
"@fuzdev/fuz_css": "^0.
|
|
62
|
-
"@fuzdev/fuz_ui": "^0.
|
|
63
|
+
"@fuzdev/fuz_code": "^0.46.1",
|
|
64
|
+
"@fuzdev/fuz_css": "^0.63.2",
|
|
65
|
+
"@fuzdev/fuz_ui": "^0.205.1",
|
|
63
66
|
"@fuzdev/fuz_util": "^0.65.1",
|
|
64
|
-
"@fuzdev/gro": "^0.
|
|
67
|
+
"@fuzdev/gro": "^0.205.1",
|
|
65
68
|
"@ryanatkn/eslint-config": "^0.12.1",
|
|
66
69
|
"@sveltejs/acorn-typescript": "^1.0.10",
|
|
67
70
|
"@sveltejs/adapter-static": "^3.0.10",
|
|
68
|
-
"@sveltejs/kit": "
|
|
71
|
+
"@sveltejs/kit": "2.64.0",
|
|
69
72
|
"@sveltejs/package": "^2.5.7",
|
|
70
73
|
"@sveltejs/vite-plugin-svelte": "^6.2.4",
|
|
71
74
|
"@types/estree": "^1.0.8",
|
|
@@ -83,7 +86,7 @@
|
|
|
83
86
|
"tslib": "^2.8.1",
|
|
84
87
|
"typescript": "^5.9.3",
|
|
85
88
|
"typescript-eslint": "^8.48.1",
|
|
86
|
-
"vite": "^7.3.
|
|
89
|
+
"vite": "^7.3.6",
|
|
87
90
|
"vitest": "^4.0.15"
|
|
88
91
|
},
|
|
89
92
|
"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,46 @@ 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
|
+
/**
|
|
93
|
+
* Rebind to a new buffer so one instance serves a whole table — the
|
|
94
|
+
* streaming path rebinds per row (the buffer string changes across feeds;
|
|
95
|
+
* the lexer's search memo survives when it doesn't).
|
|
96
|
+
*/
|
|
97
|
+
rebind(text: string): void {
|
|
98
|
+
this.#lexer.rebind(text);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
parse(cell_start: number, cell_end: number): Array<MdzNode> {
|
|
102
|
+
this.#parser.reset(this.#lexer.lex_table_cell(cell_start, cell_end));
|
|
103
|
+
return this.#parser.parse_inline_run();
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
67
107
|
export type MdzNode =
|
|
68
108
|
| MdzNodeText
|
|
69
109
|
| MdzNodeCode
|
|
@@ -78,6 +118,9 @@ export type MdzNode =
|
|
|
78
118
|
| MdzNodeList
|
|
79
119
|
| MdzNodeListItem
|
|
80
120
|
| MdzNodeBlockquote
|
|
121
|
+
| MdzNodeTable
|
|
122
|
+
| MdzNodeTableRow
|
|
123
|
+
| MdzNodeTableCell
|
|
81
124
|
| MdzNodeElement
|
|
82
125
|
| MdzNodeComponent;
|
|
83
126
|
|
|
@@ -175,6 +218,39 @@ export interface MdzNodeBlockquote extends MdzNodeBase {
|
|
|
175
218
|
children: Array<MdzNode>;
|
|
176
219
|
}
|
|
177
220
|
|
|
221
|
+
/** Per-column text alignment from the delimiter row's colons; `null` is the default (no alignment). */
|
|
222
|
+
export type MdzTableAlign = 'left' | 'center' | 'right' | null;
|
|
223
|
+
|
|
224
|
+
export interface MdzNodeTable extends MdzNodeBase {
|
|
225
|
+
type: 'Table';
|
|
226
|
+
/**
|
|
227
|
+
* Per-column alignment, length equal to the delimiter row's column count.
|
|
228
|
+
* Rendered as `text-align` on each cell; tooling can read it to re-theme
|
|
229
|
+
* (e.g. a CSP-strict consumer that avoids inline styles).
|
|
230
|
+
*/
|
|
231
|
+
align: Array<MdzTableAlign>;
|
|
232
|
+
/** The header row first, then any body rows. */
|
|
233
|
+
children: Array<MdzNodeTableRow>;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export interface MdzNodeTableRow extends MdzNodeBase {
|
|
237
|
+
type: 'TableRow';
|
|
238
|
+
/** `true` for the header row (renders `thead`/`th`); `false` for body rows (`tbody`/`td`). */
|
|
239
|
+
header: boolean;
|
|
240
|
+
/**
|
|
241
|
+
* The row's authored cells, preserved as written. The renderer normalizes to
|
|
242
|
+
* the column count: short rows are padded with empty cells, cells past the
|
|
243
|
+
* column count are ignored (their content survives here for tooling).
|
|
244
|
+
*/
|
|
245
|
+
children: Array<MdzNodeTableCell>;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export interface MdzNodeTableCell extends MdzNodeBase {
|
|
249
|
+
type: 'TableCell';
|
|
250
|
+
/** Inline content only (text, bold, italic, strikethrough, code, links). */
|
|
251
|
+
children: Array<MdzNode>;
|
|
252
|
+
}
|
|
253
|
+
|
|
178
254
|
export interface MdzNodeElement extends MdzNodeBase {
|
|
179
255
|
type: 'Element';
|
|
180
256
|
/** HTML element name (e.g. `div`, `span`, `aside`). */
|
package/src/lib/mdz_contexts.ts
CHANGED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministic scan-work counter for the complexity-ratio guards
|
|
3
|
+
* (`mdz_scaling_ratio.test.ts`). Off by default; when `enabled`, the parser hot
|
|
4
|
+
* paths accumulate the number of characters scanned (and tag-stack frames
|
|
5
|
+
* walked) into `total`. Unlike wall-clock timing this is a pure function of
|
|
6
|
+
* input + algorithm — invariant across machines, runs, and thermal state — so a
|
|
7
|
+
* super-linear regression shows up as a work ratio > ~linear regardless of the
|
|
8
|
+
* ~2× machine variance that makes absolute timing unusable here. Its sole
|
|
9
|
+
* consumer is `mdz_scaling_ratio.test.ts`.
|
|
10
|
+
*
|
|
11
|
+
* Coverage is the dominant re-scan choke points — `#index_of` + `#tokenize_text`
|
|
12
|
+
* (sync) and the streaming memo scans, `consume_text_run`, and `try_close_tag`'s
|
|
13
|
+
* stack walk — not every scan. A super-linear regression isolated to an
|
|
14
|
+
* *uncounted* probe (e.g. `#has_paragraph_break_between`, the tag-name collection
|
|
15
|
+
* loop, the URL/path probes) wouldn't move the ratio; extend the instrumentation
|
|
16
|
+
* if hot cost ever migrates there.
|
|
17
|
+
*
|
|
18
|
+
* Zero store when disabled: each instrumented site guards on `enabled` (a single
|
|
19
|
+
* branch-predicted property load, the same category as the shipped `DEV`
|
|
20
|
+
* checks). It is deliberately NOT the `esm-env` `DEV` flag — `DEV` is true under
|
|
21
|
+
* every `gro test`/`gro dev` run, which would leave the counter live in ordinary
|
|
22
|
+
* development; this flag is flipped true only by the ratio test.
|
|
23
|
+
*
|
|
24
|
+
* @nodocs
|
|
25
|
+
*/
|
|
26
|
+
export const mdz_debug_work: {enabled: boolean; total: number} = {enabled: false, total: 0};
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Reset the counter and set whether the hot paths accumulate into it. Tests call
|
|
30
|
+
* `mdz_debug_work_reset()` immediately before a parse and read
|
|
31
|
+
* `mdz_debug_work.total` immediately after; pass `false` to disable and zero it.
|
|
32
|
+
*
|
|
33
|
+
* @nodocs
|
|
34
|
+
*/
|
|
35
|
+
export const mdz_debug_work_reset = (enabled = true): void => {
|
|
36
|
+
mdz_debug_work.enabled = enabled;
|
|
37
|
+
mdz_debug_work.total = 0;
|
|
38
|
+
};
|