@fuzdev/mdz 0.4.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +10 -17
- package/README.md +2 -3
- package/dist/MdzNodeView.svelte +59 -23
- package/dist/MdzNodeView.svelte.d.ts.map +1 -1
- package/dist/MdzStreamNodeView.svelte +63 -23
- package/dist/MdzStreamNodeView.svelte.d.ts.map +1 -1
- package/dist/mdz.d.ts +12 -1
- package/dist/mdz.d.ts.map +1 -1
- package/dist/mdz.js +1 -1
- package/dist/mdz_contexts.d.ts +33 -6
- package/dist/mdz_contexts.d.ts.map +1 -1
- package/dist/mdz_helpers.d.ts +31 -3
- package/dist/mdz_helpers.d.ts.map +1 -1
- package/dist/mdz_helpers.js +102 -10
- package/dist/mdz_lexer.d.ts +3 -1
- package/dist/mdz_lexer.d.ts.map +1 -1
- package/dist/mdz_lexer.js +89 -15
- package/dist/mdz_opcodes.d.ts +3 -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 +3 -0
- package/dist/mdz_stream_parser_link.d.ts.map +1 -1
- package/dist/mdz_stream_parser_link.js +81 -9
- package/dist/mdz_stream_parser_table.js +8 -1
- package/dist/mdz_stream_state.svelte.d.ts +3 -1
- package/dist/mdz_stream_state.svelte.d.ts.map +1 -1
- package/dist/mdz_stream_state.svelte.js +4 -0
- package/dist/mdz_to_svelte.d.ts +17 -4
- package/dist/mdz_to_svelte.d.ts.map +1 -1
- package/dist/mdz_to_svelte.js +22 -9
- package/dist/mdz_token_parser.d.ts.map +1 -1
- package/dist/mdz_token_parser.js +16 -2
- package/dist/svelte_preprocess_mdz.d.ts +5 -2
- package/dist/svelte_preprocess_mdz.d.ts.map +1 -1
- package/package.json +11 -10
- package/src/lib/mdz.ts +13 -1
- package/src/lib/mdz_contexts.ts +36 -6
- package/src/lib/mdz_helpers.ts +129 -14
- package/src/lib/mdz_lexer.ts +98 -15
- package/src/lib/mdz_opcodes.ts +3 -1
- package/src/lib/mdz_opcodes_to_nodes.ts +5 -0
- package/src/lib/mdz_stream_parser_link.ts +77 -7
- package/src/lib/mdz_stream_parser_table.ts +8 -1
- package/src/lib/mdz_stream_state.svelte.ts +4 -1
- package/src/lib/mdz_to_svelte.ts +57 -13
- package/src/lib/mdz_token_parser.ts +16 -2
- package/src/lib/svelte_preprocess_mdz.ts +6 -4
package/src/lib/mdz_lexer.ts
CHANGED
|
@@ -26,6 +26,9 @@ import {
|
|
|
26
26
|
LEFT_ANGLE,
|
|
27
27
|
RIGHT_ANGLE,
|
|
28
28
|
SLASH,
|
|
29
|
+
EQUALS,
|
|
30
|
+
APOSTROPHE,
|
|
31
|
+
DOUBLE_QUOTE,
|
|
29
32
|
PERIOD,
|
|
30
33
|
LEFT_BRACKET,
|
|
31
34
|
LEFT_PAREN,
|
|
@@ -58,7 +61,7 @@ import {
|
|
|
58
61
|
TEXT_SCAN_CLASS
|
|
59
62
|
} from './mdz_helpers.ts';
|
|
60
63
|
|
|
61
|
-
import type { MdzTableAlign } from './mdz.ts';
|
|
64
|
+
import type { MdzAttribute, MdzTableAlign } from './mdz.ts';
|
|
62
65
|
import { mdz_debug_work } from './mdz_debug_work.ts';
|
|
63
66
|
|
|
64
67
|
//
|
|
@@ -177,12 +180,14 @@ export interface MdzTokenTagOpen extends MdzTokenBase {
|
|
|
177
180
|
type: 'tag_open';
|
|
178
181
|
name: string;
|
|
179
182
|
is_component: boolean;
|
|
183
|
+
attributes: Array<MdzAttribute>;
|
|
180
184
|
}
|
|
181
185
|
|
|
182
186
|
export interface MdzTokenTagSelfClose extends MdzTokenBase {
|
|
183
187
|
type: 'tag_self_close';
|
|
184
188
|
name: string;
|
|
185
189
|
is_component: boolean;
|
|
190
|
+
attributes: Array<MdzAttribute>;
|
|
186
191
|
}
|
|
187
192
|
|
|
188
193
|
export interface MdzTokenTagClose extends MdzTokenBase {
|
|
@@ -1794,9 +1799,78 @@ export class MdzLexer {
|
|
|
1794
1799
|
const first_char_code = tag_name.charCodeAt(0);
|
|
1795
1800
|
const is_component = first_char_code >= A_UPPER && first_char_code <= Z_UPPER;
|
|
1796
1801
|
|
|
1797
|
-
//
|
|
1798
|
-
|
|
1799
|
-
|
|
1802
|
+
// Attribute loop: consume space-separated attributes until `/>` or `>`.
|
|
1803
|
+
// Registry-blind — every syntactically valid attribute is parsed here;
|
|
1804
|
+
// policy (allowlist filtering) happens at render/build time. The scan is
|
|
1805
|
+
// a single forward pass bounded by the line: any newline (or a missing
|
|
1806
|
+
// separator, bad value, or duplicate name) rejects the whole tag to
|
|
1807
|
+
// literal text via the shared `#reject_tag` bailout.
|
|
1808
|
+
const attributes: Array<MdzAttribute> = [];
|
|
1809
|
+
const seen = new Set<string>(); // O(1) duplicate-name detection
|
|
1810
|
+
for (;;) {
|
|
1811
|
+
// skip a run of literal spaces only (tabs/newlines are not SPACE, so
|
|
1812
|
+
// they fall through to the reject path — the single-line, space-only rule)
|
|
1813
|
+
let had_space = false;
|
|
1814
|
+
while (this.#index < this.#text.length && this.#text.charCodeAt(this.#index) === SPACE) {
|
|
1815
|
+
this.#index++;
|
|
1816
|
+
had_space = true;
|
|
1817
|
+
}
|
|
1818
|
+
const c = this.#index < this.#text.length ? this.#text.charCodeAt(this.#index) : -1;
|
|
1819
|
+
// `/>` or `>` ends the open tag — break to the existing self-close / commit
|
|
1820
|
+
if (c === SLASH || c === RIGHT_ANGLE) break;
|
|
1821
|
+
// otherwise an attribute name must follow, space-separated from what
|
|
1822
|
+
// precedes it (`<a x="1"y="2">` rejects — `y` abuts the closing quote)
|
|
1823
|
+
if (c === -1 || !had_space || !is_letter(c)) {
|
|
1824
|
+
this.#reject_tag(start, bound);
|
|
1825
|
+
return;
|
|
1826
|
+
}
|
|
1827
|
+
// collect the attribute name (same grammar as tag names)
|
|
1828
|
+
const attr_start = this.#index;
|
|
1829
|
+
this.#index++; // first char already passed is_letter
|
|
1830
|
+
while (
|
|
1831
|
+
this.#index < this.#text.length &&
|
|
1832
|
+
is_tag_name_char(this.#text.charCodeAt(this.#index))
|
|
1833
|
+
) {
|
|
1834
|
+
this.#index++;
|
|
1835
|
+
}
|
|
1836
|
+
const name = this.#text.slice(attr_start, this.#index);
|
|
1837
|
+
// duplicate names are ambiguous (Svelte compile-errors) → literal tag
|
|
1838
|
+
if (seen.has(name)) {
|
|
1839
|
+
this.#reject_tag(start, bound);
|
|
1840
|
+
return;
|
|
1841
|
+
}
|
|
1842
|
+
seen.add(name);
|
|
1843
|
+
let value: string | true = true; // bare name is boolean `true`
|
|
1844
|
+
if (this.#index < this.#text.length && this.#text.charCodeAt(this.#index) === EQUALS) {
|
|
1845
|
+
this.#index++; // consume =
|
|
1846
|
+
// the value must be immediately quoted — no space after `=`, no
|
|
1847
|
+
// `{` brace literal, no unquoted value (this one check covers all)
|
|
1848
|
+
const quote = this.#index < this.#text.length ? this.#text.charCodeAt(this.#index) : -1;
|
|
1849
|
+
if (quote !== DOUBLE_QUOTE && quote !== APOSTROPHE) {
|
|
1850
|
+
this.#reject_tag(start, bound);
|
|
1851
|
+
return;
|
|
1852
|
+
}
|
|
1853
|
+
this.#index++; // consume opening quote
|
|
1854
|
+
const value_start = this.#index;
|
|
1855
|
+
// scan to the matching quote, bounded by newline / text end — NOT
|
|
1856
|
+
// by `#max_search_index` (the open-tag scan never is); a `>` inside
|
|
1857
|
+
// the value is content, consumed atomically before `>` is sought
|
|
1858
|
+
while (
|
|
1859
|
+
this.#index < this.#text.length &&
|
|
1860
|
+
this.#text.charCodeAt(this.#index) !== quote &&
|
|
1861
|
+
this.#text.charCodeAt(this.#index) !== NEWLINE
|
|
1862
|
+
) {
|
|
1863
|
+
this.#index++;
|
|
1864
|
+
}
|
|
1865
|
+
if (this.#index >= this.#text.length || this.#text.charCodeAt(this.#index) === NEWLINE) {
|
|
1866
|
+
// unterminated quote (or newline inside the value) → literal tag
|
|
1867
|
+
this.#reject_tag(start, bound);
|
|
1868
|
+
return;
|
|
1869
|
+
}
|
|
1870
|
+
value = this.#text.slice(value_start, this.#index);
|
|
1871
|
+
this.#index++; // consume closing quote
|
|
1872
|
+
}
|
|
1873
|
+
attributes.push({ name, value, start: attr_start, end: this.#index });
|
|
1800
1874
|
}
|
|
1801
1875
|
|
|
1802
1876
|
// Check for self-closing />
|
|
@@ -1810,6 +1884,7 @@ export class MdzLexer {
|
|
|
1810
1884
|
type: 'tag_self_close',
|
|
1811
1885
|
name: tag_name,
|
|
1812
1886
|
is_component,
|
|
1887
|
+
attributes,
|
|
1813
1888
|
start,
|
|
1814
1889
|
end: this.#index
|
|
1815
1890
|
});
|
|
@@ -1818,9 +1893,7 @@ export class MdzLexer {
|
|
|
1818
1893
|
|
|
1819
1894
|
// Check for >
|
|
1820
1895
|
if (this.#index >= this.#text.length || this.#text.charCodeAt(this.#index) !== RIGHT_ANGLE) {
|
|
1821
|
-
this.#
|
|
1822
|
-
this.#index = start + 1;
|
|
1823
|
-
this.#emit_text('<', start);
|
|
1896
|
+
this.#reject_tag(start, bound);
|
|
1824
1897
|
return;
|
|
1825
1898
|
}
|
|
1826
1899
|
this.#index++; // consume >
|
|
@@ -1837,21 +1910,24 @@ export class MdzLexer {
|
|
|
1837
1910
|
const search_limit = Math.min(this.#max_search_index, this.#text.length);
|
|
1838
1911
|
const closing_tag_pos = this.#index_of(closing_tag, this.#index);
|
|
1839
1912
|
if (closing_tag_pos === -1 || closing_tag_pos > search_limit) {
|
|
1840
|
-
this.#
|
|
1841
|
-
this.#index = start + 1;
|
|
1842
|
-
this.#emit_text('<', start);
|
|
1913
|
+
this.#reject_tag(start, bound);
|
|
1843
1914
|
return;
|
|
1844
1915
|
}
|
|
1845
1916
|
if (this.#has_paragraph_break_between(this.#index, closing_tag_pos)) {
|
|
1846
|
-
this.#
|
|
1847
|
-
this.#index = start + 1;
|
|
1848
|
-
this.#emit_text('<', start);
|
|
1917
|
+
this.#reject_tag(start, bound);
|
|
1849
1918
|
return;
|
|
1850
1919
|
}
|
|
1851
1920
|
|
|
1852
1921
|
// Emit tag_open
|
|
1853
1922
|
const open_token_index = this.#tokens.length;
|
|
1854
|
-
this.#tokens.push({
|
|
1923
|
+
this.#tokens.push({
|
|
1924
|
+
type: 'tag_open',
|
|
1925
|
+
name: tag_name,
|
|
1926
|
+
is_component,
|
|
1927
|
+
attributes,
|
|
1928
|
+
start,
|
|
1929
|
+
end: this.#index
|
|
1930
|
+
});
|
|
1855
1931
|
|
|
1856
1932
|
// Tokenize children until the closing tag. Each iteration narrows
|
|
1857
1933
|
// `#max_search_index` to the next closer so a child's delimiter scan
|
|
@@ -1892,8 +1968,15 @@ export class MdzLexer {
|
|
|
1892
1968
|
// (the `#revert_tokens_from_link_open` pattern; the old fallthrough
|
|
1893
1969
|
// left a dangling `tag_open` whose parser fallback discarded content)
|
|
1894
1970
|
this.#max_search_index = saved_max;
|
|
1895
|
-
this.#failed_tag.add(`${start}:${bound}`);
|
|
1896
1971
|
this.#tokens.splice(open_token_index);
|
|
1972
|
+
this.#reject_tag(start, bound);
|
|
1973
|
+
}
|
|
1974
|
+
|
|
1975
|
+
// a tag open at `start` that can't commit: memoizes the failure for this
|
|
1976
|
+
// `bound` so re-entries bail fast, then emits the `<` as text and resumes
|
|
1977
|
+
// scanning after it — the rest re-parses as ordinary content
|
|
1978
|
+
#reject_tag(start: number, bound: number): void {
|
|
1979
|
+
this.#failed_tag.add(`${start}:${bound}`);
|
|
1897
1980
|
this.#index = start + 1;
|
|
1898
1981
|
this.#emit_text('<', start);
|
|
1899
1982
|
}
|
package/src/lib/mdz_opcodes.ts
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* @module
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
-
import type { MdzTableAlign } from './mdz.ts';
|
|
15
|
+
import type { MdzAttribute, MdzTableAlign } from './mdz.ts';
|
|
16
16
|
|
|
17
17
|
/**
|
|
18
18
|
* Unique monotonic identifier for each node created by the parser.
|
|
@@ -66,6 +66,8 @@ export interface MdzOpcodeOpen {
|
|
|
66
66
|
level?: 1 | 2 | 3 | 4 | 5 | 6;
|
|
67
67
|
/** Tag name. Present when `node_type` is `'Element'` or `'Component'`. */
|
|
68
68
|
name?: string;
|
|
69
|
+
/** Parsed attributes. Present (possibly empty) when `node_type` is `'Element'` or `'Component'`. */
|
|
70
|
+
attributes?: Array<MdzAttribute>;
|
|
69
71
|
/** Language hint. Present when `node_type` is `'Codeblock'`. */
|
|
70
72
|
lang?: string | null;
|
|
71
73
|
/** Whether the list is ordered. Present when `node_type` is `'List'`. */
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
import { DEV } from 'esm-env';
|
|
21
21
|
|
|
22
22
|
import type {
|
|
23
|
+
MdzAttribute,
|
|
23
24
|
MdzNode,
|
|
24
25
|
MdzNodeText,
|
|
25
26
|
MdzNodeLink,
|
|
@@ -42,6 +43,7 @@ interface StackFrame {
|
|
|
42
43
|
// metadata from open opcode
|
|
43
44
|
level?: number;
|
|
44
45
|
name?: string;
|
|
46
|
+
attributes?: Array<MdzAttribute>;
|
|
45
47
|
lang?: string | null;
|
|
46
48
|
ordered?: boolean;
|
|
47
49
|
start_number?: number;
|
|
@@ -77,6 +79,7 @@ export const mdz_opcodes_to_nodes = (opcodes: Array<MdzOpcode>): Array<MdzNode>
|
|
|
77
79
|
start: op.start,
|
|
78
80
|
level: op.level,
|
|
79
81
|
name: op.name,
|
|
82
|
+
attributes: op.attributes,
|
|
80
83
|
lang: op.lang,
|
|
81
84
|
ordered: op.ordered,
|
|
82
85
|
start_number: op.start_number,
|
|
@@ -489,6 +492,7 @@ const build_node = (frame: StackFrame, frame_children: Array<MdzNode>): MdzNode
|
|
|
489
492
|
return {
|
|
490
493
|
type: 'Element',
|
|
491
494
|
name: frame.name ?? '',
|
|
495
|
+
attributes: frame.attributes ?? [],
|
|
492
496
|
children: mdz_merge_adjacent_text(frame_children),
|
|
493
497
|
start: frame.start,
|
|
494
498
|
end: frame.end!
|
|
@@ -498,6 +502,7 @@ const build_node = (frame: StackFrame, frame_children: Array<MdzNode>): MdzNode
|
|
|
498
502
|
return {
|
|
499
503
|
type: 'Component',
|
|
500
504
|
name: frame.name ?? '',
|
|
505
|
+
attributes: frame.attributes ?? [],
|
|
501
506
|
children: mdz_merge_adjacent_text(frame_children),
|
|
502
507
|
start: frame.start,
|
|
503
508
|
end: frame.end!
|
|
@@ -6,8 +6,12 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import type { MdzNodeTypeContainer } from './mdz_opcodes.ts';
|
|
9
|
+
import type { MdzAttribute } from './mdz.ts';
|
|
9
10
|
import {
|
|
10
11
|
A_UPPER,
|
|
12
|
+
APOSTROPHE,
|
|
13
|
+
DOUBLE_QUOTE,
|
|
14
|
+
EQUALS,
|
|
11
15
|
LEFT_BRACKET,
|
|
12
16
|
LEFT_PAREN,
|
|
13
17
|
MAX_INLINE_NESTING_DEPTH,
|
|
@@ -211,11 +215,77 @@ export const try_tag_open = (state: MdzStreamParserState): TryResult => {
|
|
|
211
215
|
|
|
212
216
|
const name = state.buffer.slice(name_start, i);
|
|
213
217
|
|
|
214
|
-
//
|
|
215
|
-
|
|
216
|
-
|
|
218
|
+
// Attribute loop: consume space-separated attributes until `/>` or `>`.
|
|
219
|
+
// This widens the held-tag-open rescan surface (the TODO above) with the
|
|
220
|
+
// attribute/value text — still line-bounded, since a newline anywhere aborts
|
|
221
|
+
// the whole tag (`not_match`). Every buffer-end point holds (`need_more`) so
|
|
222
|
+
// the same tree is produced regardless of where feeds split. The quoted-value
|
|
223
|
+
// terminator search reuses `buffer_index_of` (memoized, append-aware) so a
|
|
224
|
+
// long streamed value doesn't rescan quadratically across held retries,
|
|
225
|
+
// matching `try_complete_link`.
|
|
226
|
+
const attributes: Array<MdzAttribute> = [];
|
|
227
|
+
const seen = new Set<string>();
|
|
228
|
+
for (;;) {
|
|
229
|
+
// skip a run of literal spaces only (tabs/newlines are not SPACE)
|
|
230
|
+
let had_space = false;
|
|
231
|
+
while (i < state.buffer.length && state.buffer.charCodeAt(i) === SPACE) {
|
|
232
|
+
i++;
|
|
233
|
+
had_space = true;
|
|
234
|
+
}
|
|
235
|
+
if (i >= state.buffer.length) return 'need_more';
|
|
236
|
+
|
|
237
|
+
const c = state.buffer.charCodeAt(i);
|
|
238
|
+
// `/>` or `>` ends the open tag — break to the existing self-close / commit
|
|
239
|
+
if (c === SLASH || c === RIGHT_ANGLE) break;
|
|
240
|
+
// otherwise an attribute name must follow, space-separated from what
|
|
241
|
+
// precedes it (`<a x="1"y="2">` — `y` abuts the closing quote — reverts)
|
|
242
|
+
if (!is_letter(c)) return 'not_match';
|
|
243
|
+
if (!had_space) return 'not_match';
|
|
244
|
+
|
|
245
|
+
// collect the attribute name (same grammar as tag names)
|
|
246
|
+
const attr_start = i;
|
|
247
|
+
i++; // first char already passed is_letter
|
|
248
|
+
while (i < state.buffer.length && is_tag_name_char(state.buffer.charCodeAt(i))) {
|
|
249
|
+
i++;
|
|
250
|
+
}
|
|
251
|
+
if (i >= state.buffer.length) return 'need_more'; // mid-name — could still grow
|
|
252
|
+
const attr_name = state.buffer.slice(attr_start, i);
|
|
253
|
+
// duplicate names are ambiguous (Svelte compile-errors) → literal tag
|
|
254
|
+
if (seen.has(attr_name)) return 'not_match';
|
|
255
|
+
seen.add(attr_name);
|
|
256
|
+
|
|
257
|
+
let value: string | true = true; // bare name is boolean `true`
|
|
258
|
+
if (state.buffer.charCodeAt(i) === EQUALS) {
|
|
259
|
+
i++; // consume =
|
|
260
|
+
if (i >= state.buffer.length) return 'need_more'; // nothing after `=` yet
|
|
261
|
+
// the value must be immediately quoted — no space, no `{`, no unquoted
|
|
262
|
+
const quote_code = state.buffer.charCodeAt(i);
|
|
263
|
+
if (quote_code !== DOUBLE_QUOTE && quote_code !== APOSTROPHE) return 'not_match';
|
|
264
|
+
i++; // consume opening quote
|
|
265
|
+
const value_start = i;
|
|
266
|
+
// find the matching quote and the next newline via the memoized,
|
|
267
|
+
// append-aware search; a `>` inside the value is content, consumed
|
|
268
|
+
// atomically here so a `</Name>`-shaped substring never reaches
|
|
269
|
+
// `try_close_tag`
|
|
270
|
+
const quote_str = quote_code === DOUBLE_QUOTE ? '"' : "'";
|
|
271
|
+
const q = buffer_index_of(state, quote_str, value_start);
|
|
272
|
+
const n = buffer_index_of(state, '\n', value_start);
|
|
273
|
+
if (q !== -1 && (n === -1 || q < n)) {
|
|
274
|
+
value = state.buffer.slice(value_start, q);
|
|
275
|
+
i = q + 1; // past the closing quote
|
|
276
|
+
} else if (n !== -1 && (q === -1 || n < q)) {
|
|
277
|
+
return 'not_match'; // newline inside the value → literal tag
|
|
278
|
+
} else {
|
|
279
|
+
return 'need_more'; // neither found yet — unterminated so far, hold
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
attributes.push({
|
|
283
|
+
name: attr_name,
|
|
284
|
+
value,
|
|
285
|
+
start: offset(state, attr_start),
|
|
286
|
+
end: offset(state, i)
|
|
287
|
+
});
|
|
217
288
|
}
|
|
218
|
-
if (i >= state.buffer.length) return 'need_more';
|
|
219
289
|
|
|
220
290
|
// check for self-closing />
|
|
221
291
|
if (state.buffer.charCodeAt(i) === SLASH && i + 1 >= state.buffer.length) {
|
|
@@ -230,7 +300,7 @@ export const try_tag_open = (state: MdzStreamParserState): TryResult => {
|
|
|
230
300
|
ensure_paragraph(state);
|
|
231
301
|
const id = alloc_id(state);
|
|
232
302
|
const tag_start = offset(state, start);
|
|
233
|
-
emit(state, { type: 'open', id, node_type, start: tag_start, name });
|
|
303
|
+
emit(state, { type: 'open', id, node_type, start: tag_start, name, attributes });
|
|
234
304
|
emit(state, { type: 'close', id, end: offset(state, i + 2) });
|
|
235
305
|
state.active_text_id = null;
|
|
236
306
|
state.pos = i + 2;
|
|
@@ -253,8 +323,8 @@ export const try_tag_open = (state: MdzStreamParserState): TryResult => {
|
|
|
253
323
|
ensure_paragraph(state);
|
|
254
324
|
const id = alloc_id(state);
|
|
255
325
|
const tag_start = offset(state, start);
|
|
256
|
-
const delimiter = state.buffer.slice(start, i); // e.g., "<Alert>"
|
|
257
|
-
emit(state, { type: 'open', id, node_type, start: tag_start, name });
|
|
326
|
+
const delimiter = state.buffer.slice(start, i); // e.g., "<Alert>" or "<Alert x="1">"
|
|
327
|
+
emit(state, { type: 'open', id, node_type, start: tag_start, name, attributes });
|
|
258
328
|
push_stack_entry(state, id, node_type, tag_start, delimiter, name);
|
|
259
329
|
state.active_text_id = null;
|
|
260
330
|
state.pos = i;
|
|
@@ -402,7 +402,14 @@ const emit_inline_nodes = (state: MdzStreamParserState, nodes: Array<MdzNode>):
|
|
|
402
402
|
case 'Component': {
|
|
403
403
|
const id = alloc_id(state);
|
|
404
404
|
const s = offset(state, node.start);
|
|
405
|
-
emit(state, {
|
|
405
|
+
emit(state, {
|
|
406
|
+
type: 'open',
|
|
407
|
+
id,
|
|
408
|
+
node_type: node.type,
|
|
409
|
+
start: s,
|
|
410
|
+
name: node.name,
|
|
411
|
+
attributes: node.attributes
|
|
412
|
+
});
|
|
406
413
|
push_stack_entry(state, id, node.type, s, '', node.name);
|
|
407
414
|
emit_inline_nodes(state, node.children);
|
|
408
415
|
pop_stack_entry(state);
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
import { DEV } from 'esm-env';
|
|
12
12
|
|
|
13
13
|
import type { MdzNodeId, MdzNodeType, MdzOpcode } from './mdz_opcodes.ts';
|
|
14
|
-
import type { MdzTableAlign } from './mdz.ts';
|
|
14
|
+
import type { MdzAttribute, MdzTableAlign } from './mdz.ts';
|
|
15
15
|
|
|
16
16
|
/**
|
|
17
17
|
* A reactive node in the stream renderer tree.
|
|
@@ -27,6 +27,8 @@ export class MdzStreamNode {
|
|
|
27
27
|
reference?: string = $state();
|
|
28
28
|
link_type?: 'external' | 'internal' = $state();
|
|
29
29
|
name?: string;
|
|
30
|
+
/** Parsed tag attributes (Element/Component nodes); known at open, never mutated, so plain not `$state`. */
|
|
31
|
+
attributes?: Array<MdzAttribute>;
|
|
30
32
|
lang?: string | null;
|
|
31
33
|
heading_id?: string = $state();
|
|
32
34
|
ordered?: boolean;
|
|
@@ -105,6 +107,7 @@ export class MdzStreamState {
|
|
|
105
107
|
const node = new MdzStreamNode(opcode.id, opcode.node_type);
|
|
106
108
|
if (opcode.level !== undefined) node.level = opcode.level;
|
|
107
109
|
if (opcode.name !== undefined) node.name = opcode.name;
|
|
110
|
+
if (opcode.attributes !== undefined) node.attributes = opcode.attributes;
|
|
108
111
|
if (opcode.lang !== undefined) node.lang = opcode.lang;
|
|
109
112
|
if (opcode.ordered !== undefined) node.ordered = opcode.ordered;
|
|
110
113
|
if (opcode.start_number !== undefined) node.start_number = opcode.start_number;
|
package/src/lib/mdz_to_svelte.ts
CHANGED
|
@@ -13,7 +13,34 @@ import { escape_svelte_text } from '@fuzdev/fuz_util/svelte_preprocess_helpers.t
|
|
|
13
13
|
import { escape_js_string } from '@fuzdev/fuz_util/string.ts';
|
|
14
14
|
|
|
15
15
|
import type { MdzNode, MdzNodeTableRow } from './mdz.ts';
|
|
16
|
-
import {
|
|
16
|
+
import type { MdzComponentProps } from './mdz_contexts.ts';
|
|
17
|
+
import {
|
|
18
|
+
mdz_is_void_element,
|
|
19
|
+
mdz_classify_link,
|
|
20
|
+
mdz_filter_element_attributes,
|
|
21
|
+
mdz_filter_component_attributes,
|
|
22
|
+
ALLOWED_ELEMENT_ATTRIBUTES
|
|
23
|
+
} from './mdz_helpers.ts';
|
|
24
|
+
|
|
25
|
+
// emits parsed attributes as Svelte markup, matching the file's `name={'...'}`
|
|
26
|
+
// idiom for string attrs. Booleans emit as `name={true}`, not bare — a bare
|
|
27
|
+
// attribute compiles to `name=""`, while the runtime views spread `true`, which
|
|
28
|
+
// Svelte renders per attribute (`aria-hidden="true"`); the expression form
|
|
29
|
+
// keeps precompiled and runtime output identical. Each entry gets a leading
|
|
30
|
+
// space, so the result splices straight after the tag name.
|
|
31
|
+
/**
|
|
32
|
+
* A build-time component registry entry: an import path, or an import path
|
|
33
|
+
* plus the attribute names passed as props (see `MdzComponentProps`).
|
|
34
|
+
*/
|
|
35
|
+
export type MdzComponentImport = string | { path: string; props?: MdzComponentProps };
|
|
36
|
+
|
|
37
|
+
const format_attributes = (entries: Array<[string, string | true]>): string => {
|
|
38
|
+
let out = '';
|
|
39
|
+
for (const [name, value] of entries) {
|
|
40
|
+
out += value === true ? ` ${name}={true}` : ` ${name}={'${escape_js_string(value)}'}`;
|
|
41
|
+
}
|
|
42
|
+
return out;
|
|
43
|
+
};
|
|
17
44
|
|
|
18
45
|
/**
|
|
19
46
|
* Result of converting `MdzNode` arrays to Svelte markup.
|
|
@@ -38,10 +65,13 @@ const component_local_name = (import_path: string): string =>
|
|
|
38
65
|
*/
|
|
39
66
|
export interface MdzToSvelteOptions {
|
|
40
67
|
/**
|
|
41
|
-
* Component name to import
|
|
68
|
+
* Component name to import mapping, mirroring the runtime `MdzComponents`
|
|
69
|
+
* registry: a bare import path receives no attributes
|
|
70
|
+
* (`{Note: '#lib/Note.svelte'}`), and the config form names the attributes
|
|
71
|
+
* passed as props (`{Alert: {path: '#lib/Alert.svelte', props: ['status']}}`).
|
|
42
72
|
* If content references a component not in this map, `has_unconfigured_tags` is set.
|
|
43
73
|
*/
|
|
44
|
-
components: Record<string,
|
|
74
|
+
components: Record<string, MdzComponentImport>;
|
|
45
75
|
|
|
46
76
|
/**
|
|
47
77
|
* Allowed HTML elements (e.g., `{aside: true, details: true}`).
|
|
@@ -53,8 +83,9 @@ export interface MdzToSvelteOptions {
|
|
|
53
83
|
|
|
54
84
|
/**
|
|
55
85
|
* Base path for resolving relative links (e.g., `'/docs/usage/'`).
|
|
56
|
-
* When provided, relative references (`./`, `../`) are resolved
|
|
57
|
-
*
|
|
86
|
+
* When provided, relative references (`./`, `../`) are resolved against it and,
|
|
87
|
+
* like absolute references, passed through `resolve()` in its pathname form.
|
|
88
|
+
* Trailing slash recommended.
|
|
58
89
|
*/
|
|
59
90
|
base?: string;
|
|
60
91
|
|
|
@@ -140,7 +171,7 @@ export const mdz_to_svelte = (
|
|
|
140
171
|
if (link.kind === 'unsafe') return children_markup;
|
|
141
172
|
if (link.kind === 'resolve') {
|
|
142
173
|
imports.set('resolve', { path: '$app/paths', kind: 'named' });
|
|
143
|
-
return `<a href={resolve('${escape_js_string(link.
|
|
174
|
+
return `<a href={resolve('${escape_js_string(link.path)}')}>${children_markup}</a>`;
|
|
144
175
|
}
|
|
145
176
|
if (link.kind === 'external') {
|
|
146
177
|
return `<a href={'${escape_js_string(link.href)}'} target="_blank" rel="noopener">${children_markup}</a>`;
|
|
@@ -208,25 +239,38 @@ export const mdz_to_svelte = (
|
|
|
208
239
|
}
|
|
209
240
|
|
|
210
241
|
case 'Element': {
|
|
211
|
-
if (elements[node.name]
|
|
242
|
+
if (elements[node.name] !== true) {
|
|
212
243
|
has_unconfigured_tags = true;
|
|
213
244
|
return '';
|
|
214
245
|
}
|
|
246
|
+
// filter via the SAME helper the node views use, so precompiled and
|
|
247
|
+
// runtime output stay identical (its DEV-warn fires at build time too)
|
|
248
|
+
const attrs_str = format_attributes(
|
|
249
|
+
Object.entries(
|
|
250
|
+
mdz_filter_element_attributes(node.name, node.attributes, ALLOWED_ELEMENT_ATTRIBUTES)
|
|
251
|
+
)
|
|
252
|
+
);
|
|
215
253
|
if (mdz_is_void_element(node.name)) {
|
|
216
254
|
// void elements cannot have content — any parsed children are dropped
|
|
217
|
-
return `<${node.name} />`;
|
|
255
|
+
return `<${node.name}${attrs_str} />`;
|
|
218
256
|
}
|
|
219
|
-
return `<${node.name}>${render_nodes(node.children)}</${node.name}>`;
|
|
257
|
+
return `<${node.name}${attrs_str}>${render_nodes(node.children)}</${node.name}>`;
|
|
220
258
|
}
|
|
221
259
|
|
|
222
260
|
case 'Component': {
|
|
223
|
-
const
|
|
224
|
-
if (!
|
|
261
|
+
const entry = components[node.name];
|
|
262
|
+
if (!entry) {
|
|
225
263
|
has_unconfigured_tags = true;
|
|
226
264
|
return '';
|
|
227
265
|
}
|
|
228
|
-
|
|
229
|
-
|
|
266
|
+
const { path, props } =
|
|
267
|
+
typeof entry === 'string' ? { path: entry, props: undefined } : entry;
|
|
268
|
+
imports.set(node.name, { path, kind: 'default' });
|
|
269
|
+
// filter via the SAME helper the node views use
|
|
270
|
+
const attrs_str = format_attributes(
|
|
271
|
+
Object.entries(mdz_filter_component_attributes(node.name, node.attributes, props))
|
|
272
|
+
);
|
|
273
|
+
return `<${node.name}${attrs_str}>${render_nodes(node.children)}</${node.name}>`;
|
|
230
274
|
}
|
|
231
275
|
|
|
232
276
|
default:
|
|
@@ -581,7 +581,14 @@ export class MdzTokenParser {
|
|
|
581
581
|
const t = this.#tokens[this.#index]!;
|
|
582
582
|
if (t.type === 'tag_close' && t.name === tag_name) {
|
|
583
583
|
this.#index++;
|
|
584
|
-
return {
|
|
584
|
+
return {
|
|
585
|
+
type: node_type,
|
|
586
|
+
name: tag_name,
|
|
587
|
+
attributes: open_token.attributes,
|
|
588
|
+
children,
|
|
589
|
+
start,
|
|
590
|
+
end: t.end
|
|
591
|
+
};
|
|
585
592
|
}
|
|
586
593
|
const node = this.#parse_inline();
|
|
587
594
|
if (node) mdz_push_merging_text(children, node);
|
|
@@ -595,7 +602,14 @@ export class MdzTokenParser {
|
|
|
595
602
|
const token = this.#tokens[this.#index]! as MdzTokenTagSelfClose;
|
|
596
603
|
const node_type: 'Component' | 'Element' = token.is_component ? 'Component' : 'Element';
|
|
597
604
|
this.#index++;
|
|
598
|
-
return {
|
|
605
|
+
return {
|
|
606
|
+
type: node_type,
|
|
607
|
+
name: token.name,
|
|
608
|
+
attributes: token.attributes,
|
|
609
|
+
children: [],
|
|
610
|
+
start: token.start,
|
|
611
|
+
end: token.end
|
|
612
|
+
};
|
|
599
613
|
}
|
|
600
614
|
|
|
601
615
|
// -- Paragraph flushing --
|
|
@@ -37,7 +37,7 @@ import {
|
|
|
37
37
|
} from '@fuzdev/fuz_util/svelte_preprocess_helpers.ts';
|
|
38
38
|
|
|
39
39
|
import { mdz_parse } from './mdz.ts';
|
|
40
|
-
import { mdz_to_svelte, type MdzToSvelteResult } from './mdz_to_svelte.ts';
|
|
40
|
+
import { mdz_to_svelte, type MdzComponentImport, type MdzToSvelteResult } from './mdz_to_svelte.ts';
|
|
41
41
|
|
|
42
42
|
/**
|
|
43
43
|
* An estree `ImportDeclaration` augmented with Svelte's position data.
|
|
@@ -56,12 +56,14 @@ export interface SveltePreprocessMdzOptions {
|
|
|
56
56
|
/**
|
|
57
57
|
* Component import mapping for mdz content.
|
|
58
58
|
* Key: component name as used in mdz (e.g., 'Alert').
|
|
59
|
-
* Value: import path (e.g., '#lib/Alert.svelte')
|
|
59
|
+
* Value: import path (e.g., '#lib/Alert.svelte'), which receives no attributes,
|
|
60
|
+
* or `{path, props}` naming the attributes passed as props
|
|
61
|
+
* (e.g., `{path: '#lib/Alert.svelte', props: ['status']}`, or `props: 'all'`).
|
|
60
62
|
*
|
|
61
63
|
* If mdz content references a component not in this map,
|
|
62
64
|
* that `Mdz` usage is skipped (left as runtime).
|
|
63
65
|
*/
|
|
64
|
-
components?: Record<string,
|
|
66
|
+
components?: Record<string, MdzComponentImport>;
|
|
65
67
|
|
|
66
68
|
/**
|
|
67
69
|
* Allowed HTML elements in mdz content (e.g., `{aside: true, details: true}`).
|
|
@@ -235,7 +237,7 @@ interface FindMdzUsagesResult {
|
|
|
235
237
|
}
|
|
236
238
|
|
|
237
239
|
interface FindMdzUsagesContext {
|
|
238
|
-
components: Record<string,
|
|
240
|
+
components: Record<string, MdzComponentImport>;
|
|
239
241
|
elements: Record<string, boolean>;
|
|
240
242
|
filename: string | undefined;
|
|
241
243
|
source: string;
|