@fuzdev/mdz 0.4.0 → 0.5.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 +7 -7
- 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
|
@@ -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,EAAE,OAAO,EAAmB,MAAM,UAAU,CAAC;
|
|
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,EAAE,OAAO,EAAmB,MAAM,UAAU,CAAC;AACzD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAe3D;;;GAGG;AACH,MAAM,MAAM,kBAAkB,GAAG,MAAM,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,iBAAiB,CAAA;CAAE,CAAC;AAUtF;;GAEG;AACH,MAAM,WAAW,iBAAiB;IACjC,sCAAsC;IACtC,MAAM,EAAE,MAAM,CAAC;IAEf,yDAAyD;IACzD,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,SAAS,GAAG,OAAO,CAAA;KAAE,CAAC,CAAC;IAElE,yEAAyE;IACzE,qBAAqB,EAAE,OAAO,CAAC;CAC/B;AAMD;;GAEG;AACH,MAAM,WAAW,kBAAkB;IAClC;;;;;;OAMG;IACH,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,kBAAkB,CAAC,CAAC;IAE/C;;;;;OAKG;IACH,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAElC;;;;;OAKG;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,iBAmKF,CAAC"}
|
package/dist/mdz_to_svelte.js
CHANGED
|
@@ -10,7 +10,14 @@
|
|
|
10
10
|
import { UnreachableError } from '@fuzdev/fuz_util/error.ts';
|
|
11
11
|
import { escape_svelte_text } from '@fuzdev/fuz_util/svelte_preprocess_helpers.ts';
|
|
12
12
|
import { escape_js_string } from '@fuzdev/fuz_util/string.ts';
|
|
13
|
-
import { mdz_is_void_element, mdz_classify_link } from './mdz_helpers.js';
|
|
13
|
+
import { mdz_is_void_element, mdz_classify_link, mdz_filter_element_attributes, mdz_filter_component_attributes, ALLOWED_ELEMENT_ATTRIBUTES } from './mdz_helpers.js';
|
|
14
|
+
const format_attributes = (entries) => {
|
|
15
|
+
let out = '';
|
|
16
|
+
for (const [name, value] of entries) {
|
|
17
|
+
out += value === true ? ` ${name}={true}` : ` ${name}={'${escape_js_string(value)}'}`;
|
|
18
|
+
}
|
|
19
|
+
return out;
|
|
20
|
+
};
|
|
14
21
|
/** Derives an import's local identifier from a `.svelte` import path's basename. */
|
|
15
22
|
const component_local_name = (import_path) => import_path.slice(import_path.lastIndexOf('/') + 1).replace(/\.svelte$/, '');
|
|
16
23
|
/**
|
|
@@ -68,7 +75,7 @@ export const mdz_to_svelte = (nodes, options) => {
|
|
|
68
75
|
return children_markup;
|
|
69
76
|
if (link.kind === 'resolve') {
|
|
70
77
|
imports.set('resolve', { path: '$app/paths', kind: 'named' });
|
|
71
|
-
return `<a href={resolve('${escape_js_string(link.
|
|
78
|
+
return `<a href={resolve('${escape_js_string(link.path)}')}>${children_markup}</a>`;
|
|
72
79
|
}
|
|
73
80
|
if (link.kind === 'external') {
|
|
74
81
|
return `<a href={'${escape_js_string(link.href)}'} target="_blank" rel="noopener">${children_markup}</a>`;
|
|
@@ -130,24 +137,30 @@ export const mdz_to_svelte = (nodes, options) => {
|
|
|
130
137
|
return `<h${node.level}${id_attr}>${render_nodes(node.children)}</h${node.level}>`;
|
|
131
138
|
}
|
|
132
139
|
case 'Element': {
|
|
133
|
-
if (elements[node.name]
|
|
140
|
+
if (elements[node.name] !== true) {
|
|
134
141
|
has_unconfigured_tags = true;
|
|
135
142
|
return '';
|
|
136
143
|
}
|
|
144
|
+
// filter via the SAME helper the node views use, so precompiled and
|
|
145
|
+
// runtime output stay identical (its DEV-warn fires at build time too)
|
|
146
|
+
const attrs_str = format_attributes(Object.entries(mdz_filter_element_attributes(node.name, node.attributes, ALLOWED_ELEMENT_ATTRIBUTES)));
|
|
137
147
|
if (mdz_is_void_element(node.name)) {
|
|
138
148
|
// void elements cannot have content — any parsed children are dropped
|
|
139
|
-
return `<${node.name} />`;
|
|
149
|
+
return `<${node.name}${attrs_str} />`;
|
|
140
150
|
}
|
|
141
|
-
return `<${node.name}>${render_nodes(node.children)}</${node.name}>`;
|
|
151
|
+
return `<${node.name}${attrs_str}>${render_nodes(node.children)}</${node.name}>`;
|
|
142
152
|
}
|
|
143
153
|
case 'Component': {
|
|
144
|
-
const
|
|
145
|
-
if (!
|
|
154
|
+
const entry = components[node.name];
|
|
155
|
+
if (!entry) {
|
|
146
156
|
has_unconfigured_tags = true;
|
|
147
157
|
return '';
|
|
148
158
|
}
|
|
149
|
-
|
|
150
|
-
|
|
159
|
+
const { path, props } = typeof entry === 'string' ? { path: entry, props: undefined } : entry;
|
|
160
|
+
imports.set(node.name, { path, kind: 'default' });
|
|
161
|
+
// filter via the SAME helper the node views use
|
|
162
|
+
const attrs_str = format_attributes(Object.entries(mdz_filter_component_attributes(node.name, node.attributes, props)));
|
|
163
|
+
return `<${node.name}${attrs_str}>${render_nodes(node.children)}</${node.name}>`;
|
|
151
164
|
}
|
|
152
165
|
default:
|
|
153
166
|
throw new UnreachableError(node);
|
|
@@ -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,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;
|
|
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;CA6jBlC"}
|
package/dist/mdz_token_parser.js
CHANGED
|
@@ -478,7 +478,14 @@ export class MdzTokenParser {
|
|
|
478
478
|
const t = this.#tokens[this.#index];
|
|
479
479
|
if (t.type === 'tag_close' && t.name === tag_name) {
|
|
480
480
|
this.#index++;
|
|
481
|
-
return {
|
|
481
|
+
return {
|
|
482
|
+
type: node_type,
|
|
483
|
+
name: tag_name,
|
|
484
|
+
attributes: open_token.attributes,
|
|
485
|
+
children,
|
|
486
|
+
start,
|
|
487
|
+
end: t.end
|
|
488
|
+
};
|
|
482
489
|
}
|
|
483
490
|
const node = this.#parse_inline();
|
|
484
491
|
if (node)
|
|
@@ -491,7 +498,14 @@ export class MdzTokenParser {
|
|
|
491
498
|
const token = this.#tokens[this.#index];
|
|
492
499
|
const node_type = token.is_component ? 'Component' : 'Element';
|
|
493
500
|
this.#index++;
|
|
494
|
-
return {
|
|
501
|
+
return {
|
|
502
|
+
type: node_type,
|
|
503
|
+
name: token.name,
|
|
504
|
+
attributes: token.attributes,
|
|
505
|
+
children: [],
|
|
506
|
+
start: token.start,
|
|
507
|
+
end: token.end
|
|
508
|
+
};
|
|
495
509
|
}
|
|
496
510
|
// -- Paragraph flushing --
|
|
497
511
|
#flush_paragraph(paragraph_children, trim_trailing = false) {
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
* @module
|
|
15
15
|
*/
|
|
16
16
|
import { type PreprocessorGroup } from 'svelte/compiler';
|
|
17
|
+
import { type MdzComponentImport } from './mdz_to_svelte.js';
|
|
17
18
|
/**
|
|
18
19
|
* Options for `svelte_preprocess_mdz`.
|
|
19
20
|
*/
|
|
@@ -23,12 +24,14 @@ export interface SveltePreprocessMdzOptions {
|
|
|
23
24
|
/**
|
|
24
25
|
* Component import mapping for mdz content.
|
|
25
26
|
* Key: component name as used in mdz (e.g., 'Alert').
|
|
26
|
-
* Value: import path (e.g., '#lib/Alert.svelte')
|
|
27
|
+
* Value: import path (e.g., '#lib/Alert.svelte'), which receives no attributes,
|
|
28
|
+
* or `{path, props}` naming the attributes passed as props
|
|
29
|
+
* (e.g., `{path: '#lib/Alert.svelte', props: ['status']}`, or `props: 'all'`).
|
|
27
30
|
*
|
|
28
31
|
* If mdz content references a component not in this map,
|
|
29
32
|
* that `Mdz` usage is skipped (left as runtime).
|
|
30
33
|
*/
|
|
31
|
-
components?: Record<string,
|
|
34
|
+
components?: Record<string, MdzComponentImport>;
|
|
32
35
|
/**
|
|
33
36
|
* Allowed HTML elements in mdz content (e.g., `{aside: true, details: true}`).
|
|
34
37
|
* Values are boolean placeholders for future per-element configuration,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"svelte_preprocess_mdz.d.ts","sourceRoot":"../src/lib/","sources":["../src/lib/svelte_preprocess_mdz.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAGH,OAAO,EAAS,KAAK,iBAAiB,EAAY,MAAM,iBAAiB,CAAC;
|
|
1
|
+
{"version":3,"file":"svelte_preprocess_mdz.d.ts","sourceRoot":"../src/lib/","sources":["../src/lib/svelte_preprocess_mdz.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAGH,OAAO,EAAS,KAAK,iBAAiB,EAAY,MAAM,iBAAiB,CAAC;AAsB1E,OAAO,EAAiB,KAAK,kBAAkB,EAA0B,MAAM,oBAAoB,CAAC;AASpG;;GAEG;AACH,MAAM,WAAW,0BAA0B;IAC1C,gCAAgC;IAChC,OAAO,CAAC,EAAE,KAAK,CAAC,MAAM,GAAG,MAAM,CAAC,CAAC;IAEjC;;;;;;;;;OASG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,kBAAkB,CAAC,CAAC;IAEhD;;;;;;OAMG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAEnC;;;;;OAKG;IACH,qBAAqB,CAAC,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;IAEtC;;;;OAIG;IACH,yBAAyB,CAAC,EAAE,MAAM,CAAC;IAEnC;;;;;OAKG;IACH,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAE/B;;;;OAIG;IACH,0BAA0B,CAAC,EAAE,MAAM,CAAC;IAEpC;;;;OAIG;IACH,QAAQ,CAAC,EAAE,KAAK,GAAG,OAAO,CAAC;CAC3B;AAID;;;;;GAKG;AACH,eAAO,MAAM,qBAAqB,GACjC,UAAS,0BAA+B,KACtC,iBA6FF,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fuzdev/mdz",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.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": "📝",
|
|
7
7
|
"logo": "logo.svg",
|
|
8
8
|
"logo_alt": "a friendly pixelated spider facing you",
|
|
9
|
-
"license": "
|
|
9
|
+
"license": "0BSD",
|
|
10
10
|
"homepage": "https://mdz.fuz.dev/",
|
|
11
11
|
"author": {
|
|
12
12
|
"name": "Ryan Atkinson",
|
|
@@ -64,12 +64,12 @@
|
|
|
64
64
|
},
|
|
65
65
|
"devDependencies": {
|
|
66
66
|
"@fuzdev/blake3-wasm": "^0.2.0",
|
|
67
|
-
"@fuzdev/fuz_code": "^0.
|
|
67
|
+
"@fuzdev/fuz_code": "^0.52.1",
|
|
68
68
|
"@fuzdev/fuz_css": "^0.66.1",
|
|
69
|
-
"@fuzdev/fuz_ui": "^0.
|
|
69
|
+
"@fuzdev/fuz_ui": "^0.213.0",
|
|
70
70
|
"@fuzdev/fuz_util": "^0.72.0",
|
|
71
|
-
"@fuzdev/gro": "^0.
|
|
72
|
-
"@ryanatkn/eslint-config": "^0.
|
|
71
|
+
"@fuzdev/gro": "^0.216.0",
|
|
72
|
+
"@ryanatkn/eslint-config": "^0.13.0",
|
|
73
73
|
"@sveltejs/acorn-typescript": "^1.0.10",
|
|
74
74
|
"@sveltejs/adapter-static": "^4.0.0",
|
|
75
75
|
"@sveltejs/kit": "^3.0.1",
|
|
@@ -83,7 +83,7 @@
|
|
|
83
83
|
"esm-env": "^1.2.2",
|
|
84
84
|
"svelte": "^5.57.2",
|
|
85
85
|
"svelte-check": "^4.7.6",
|
|
86
|
-
"svelte-docinfo": "^0.
|
|
86
|
+
"svelte-docinfo": "^0.9.0",
|
|
87
87
|
"svelte2tsx": "^0.7.56",
|
|
88
88
|
"tslib": "^2.8.1",
|
|
89
89
|
"typescript": "^6.0.3",
|
package/src/lib/mdz.ts
CHANGED
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
* ## Status
|
|
46
46
|
*
|
|
47
47
|
* Pre-stable — breaking changes are expected as the dialect matures.
|
|
48
|
-
* Next:
|
|
48
|
+
* Next: editing.
|
|
49
49
|
*
|
|
50
50
|
* @module
|
|
51
51
|
*/
|
|
@@ -251,10 +251,20 @@ export interface MdzNodeTableCell extends MdzNodeBase {
|
|
|
251
251
|
children: Array<MdzNode>;
|
|
252
252
|
}
|
|
253
253
|
|
|
254
|
+
export interface MdzAttribute {
|
|
255
|
+
name: string;
|
|
256
|
+
/** `true` for bare boolean shorthand (`disabled` ≡ `disabled={true}`). */
|
|
257
|
+
value: string | true;
|
|
258
|
+
start: number;
|
|
259
|
+
end: number;
|
|
260
|
+
}
|
|
261
|
+
|
|
254
262
|
export interface MdzNodeElement extends MdzNodeBase {
|
|
255
263
|
type: 'Element';
|
|
256
264
|
/** HTML element name (e.g. `div`, `span`, `aside`). */
|
|
257
265
|
name: string;
|
|
266
|
+
/** Parsed tag attributes; always present, possibly empty. */
|
|
267
|
+
attributes: Array<MdzAttribute>;
|
|
258
268
|
children: Array<MdzNode>;
|
|
259
269
|
}
|
|
260
270
|
|
|
@@ -262,5 +272,7 @@ export interface MdzNodeComponent extends MdzNodeBase {
|
|
|
262
272
|
type: 'Component';
|
|
263
273
|
/** Svelte component name (e.g. `Alert`, `Card`). */
|
|
264
274
|
name: string;
|
|
275
|
+
/** Parsed tag attributes; always present, possibly empty. */
|
|
276
|
+
attributes: Array<MdzAttribute>;
|
|
265
277
|
children: Array<MdzNode>;
|
|
266
278
|
}
|
package/src/lib/mdz_contexts.ts
CHANGED
|
@@ -2,21 +2,51 @@ import type { Component } from 'svelte';
|
|
|
2
2
|
|
|
3
3
|
import { create_context } from './context_helpers.ts';
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* The attributes a registered component receives as props: a list of allowed
|
|
7
|
+
* names, or `'all'` to pass every attribute through. Attributes outside the
|
|
8
|
+
* list are dropped (DEV-warned).
|
|
9
|
+
*/
|
|
10
|
+
export type MdzComponentProps = ReadonlyArray<string> | 'all';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* A component registry entry that allows attributes through as props.
|
|
14
|
+
*/
|
|
15
|
+
export interface MdzComponentConfig {
|
|
16
|
+
component: Component<any, any>;
|
|
17
|
+
/** Attribute names passed as props, or `'all'`; omitted passes none. */
|
|
18
|
+
props?: MdzComponentProps;
|
|
19
|
+
}
|
|
20
|
+
|
|
5
21
|
/**
|
|
6
22
|
* Component registry for custom Svelte components that can be used in mdz content.
|
|
7
23
|
*
|
|
8
|
-
*
|
|
24
|
+
* A bare component entry renders the tag with no props — its attributes are
|
|
25
|
+
* dropped. A `MdzComponentConfig` entry names the attributes that pass through
|
|
26
|
+
* as props, so a component only receives what it's prepared for:
|
|
9
27
|
*
|
|
10
|
-
*
|
|
28
|
+
* ```ts
|
|
29
|
+
* new Map([
|
|
30
|
+
* ['Alert', {component: Alert, props: ['status']}], // `<Alert status="error">`
|
|
31
|
+
* ['Note', Note], // `<Note>`, attributes dropped
|
|
32
|
+
* ]);
|
|
33
|
+
* ```
|
|
34
|
+
*
|
|
35
|
+
* Attribute values are author-controlled strings (or `true` for bare names),
|
|
36
|
+
* so with untrusted content, allow only props the component treats safely.
|
|
37
|
+
* `props: 'all'` passes everything — a component that spreads rest props onto
|
|
38
|
+
* an element then forwards `style`, `href`, and the like.
|
|
11
39
|
*/
|
|
12
|
-
|
|
40
|
+
// props pass through untyped (`string | true`); a typed registry is a separate concern
|
|
41
|
+
export type MdzComponents = Map<string, Component<any, any> | MdzComponentConfig>;
|
|
13
42
|
|
|
14
43
|
/**
|
|
15
44
|
* Element registry for HTML elements that can be used in mdz content.
|
|
16
45
|
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
46
|
+
* Register an element by mapping its name to `true` (e.g. `['aside', true]`),
|
|
47
|
+
* which allows `<aside>...</aside>` in mdz content. Only `true` enables an
|
|
48
|
+
* element — the renderers treat any other value (including `false`) as
|
|
49
|
+
* unregistered, so the tag falls back to literal text.
|
|
20
50
|
*/
|
|
21
51
|
export type MdzElements = Map<string, boolean>;
|
|
22
52
|
|
package/src/lib/mdz_helpers.ts
CHANGED
|
@@ -7,13 +7,18 @@
|
|
|
7
7
|
* @module
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
+
import { DEV } from 'esm-env';
|
|
11
|
+
import type { Component } from 'svelte';
|
|
12
|
+
|
|
10
13
|
import type {
|
|
14
|
+
MdzAttribute,
|
|
11
15
|
MdzNode,
|
|
12
16
|
MdzNodeText,
|
|
13
17
|
MdzNodeComponent,
|
|
14
18
|
MdzNodeElement,
|
|
15
19
|
MdzTableAlign
|
|
16
20
|
} from './mdz.ts';
|
|
21
|
+
import type { MdzComponentConfig, MdzComponentProps } from './mdz_contexts.ts';
|
|
17
22
|
|
|
18
23
|
// Character codes for performance
|
|
19
24
|
/** @nodocs */
|
|
@@ -76,6 +81,8 @@ export const AMPERSAND = 38; // &
|
|
|
76
81
|
/** @nodocs */
|
|
77
82
|
export const APOSTROPHE = 39; // '
|
|
78
83
|
/** @nodocs */
|
|
84
|
+
export const DOUBLE_QUOTE = 34; // "
|
|
85
|
+
/** @nodocs */
|
|
79
86
|
export const PLUS = 43; // +
|
|
80
87
|
/** @nodocs */
|
|
81
88
|
export const EQUALS = 61; // =
|
|
@@ -787,6 +794,93 @@ const VOID_ELEMENTS = new Set([
|
|
|
787
794
|
// implicitly self-closing at parse time, or rejecting the open/close form
|
|
788
795
|
export const mdz_is_void_element = (name: string): boolean => VOID_ELEMENTS.has(name);
|
|
789
796
|
|
|
797
|
+
/**
|
|
798
|
+
* The closed, global set of attribute names allowed on rendered HTML elements.
|
|
799
|
+
* Every name is inert on any element (no URL, no script, no focus or DOM
|
|
800
|
+
* clobbering), so element safety is independent of which element names a
|
|
801
|
+
* consumer registers. Anything outside this set is dropped by
|
|
802
|
+
* `mdz_filter_element_attributes`.
|
|
803
|
+
*/
|
|
804
|
+
export const ALLOWED_ELEMENT_ATTRIBUTES: ReadonlySet<string> = new Set([
|
|
805
|
+
'class',
|
|
806
|
+
'title',
|
|
807
|
+
'lang',
|
|
808
|
+
'dir',
|
|
809
|
+
'role',
|
|
810
|
+
'aria-label',
|
|
811
|
+
'aria-hidden',
|
|
812
|
+
'aria-describedby',
|
|
813
|
+
'aria-labelledby'
|
|
814
|
+
]);
|
|
815
|
+
|
|
816
|
+
// filters an element's parsed attributes against the allowlist, returning a
|
|
817
|
+
// name→value object ready to spread onto `<svelte:element {...}>`. Disallowed
|
|
818
|
+
// names are dropped (DEV-warned). Used identically by MdzNodeView,
|
|
819
|
+
// MdzStreamNodeView, and mdz_to_svelte, so precompiled and runtime output match.
|
|
820
|
+
export const mdz_filter_element_attributes = (
|
|
821
|
+
element_name: string,
|
|
822
|
+
attributes: Array<MdzAttribute>,
|
|
823
|
+
allowlist: ReadonlySet<string>
|
|
824
|
+
): Record<string, string | true> => {
|
|
825
|
+
const filtered: Record<string, string | true> = {};
|
|
826
|
+
for (const attr of attributes) {
|
|
827
|
+
if (allowlist.has(attr.name)) {
|
|
828
|
+
filtered[attr.name] = attr.value;
|
|
829
|
+
} else if (DEV) {
|
|
830
|
+
console.warn(
|
|
831
|
+
`mdz: dropped disallowed attribute \`${attr.name}\` on <${element_name}> — not in the element attribute allowlist`
|
|
832
|
+
);
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
return filtered;
|
|
836
|
+
};
|
|
837
|
+
|
|
838
|
+
// filters a component's parsed attributes against its registered `props`,
|
|
839
|
+
// returning a name→value props object spread onto `<Component {...props}>`.
|
|
840
|
+
// Omitted `props` passes nothing, `'all'` passes everything; dropped names are
|
|
841
|
+
// DEV-warned. Used identically by MdzNodeView, MdzStreamNodeView, and
|
|
842
|
+
// mdz_to_svelte, so precompiled and runtime output match.
|
|
843
|
+
export const mdz_filter_component_attributes = (
|
|
844
|
+
component_name: string,
|
|
845
|
+
attributes: Array<MdzAttribute>,
|
|
846
|
+
props: MdzComponentProps | undefined
|
|
847
|
+
): Record<string, string | true> => {
|
|
848
|
+
const filtered: Record<string, string | true> = {};
|
|
849
|
+
for (const attr of attributes) {
|
|
850
|
+
if (props === 'all' || props?.includes(attr.name)) {
|
|
851
|
+
filtered[attr.name] = attr.value;
|
|
852
|
+
} else if (DEV) {
|
|
853
|
+
console.warn(
|
|
854
|
+
`mdz: dropped attribute \`${attr.name}\` on <${component_name}> — not in its registered props`
|
|
855
|
+
);
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
return filtered;
|
|
859
|
+
};
|
|
860
|
+
|
|
861
|
+
// normalizes a component registry entry to its config form
|
|
862
|
+
export const mdz_component_config = (
|
|
863
|
+
entry: Component<any, any> | MdzComponentConfig
|
|
864
|
+
): MdzComponentConfig => (typeof entry === 'function' ? { component: entry } : entry);
|
|
865
|
+
|
|
866
|
+
// reconstructs authored attribute text (` name="value"` / ` name`) for the
|
|
867
|
+
// unregistered-tag literal placeholder. The quote char is chosen so a value
|
|
868
|
+
// containing one quote type stays balanced — a parsed value holds at most one
|
|
869
|
+
// (a `"`-delimited value can't contain `"`, a `'`-delimited one can't contain
|
|
870
|
+
// `'`). The caller interpolates the result as Svelte text (auto-escaped).
|
|
871
|
+
export const mdz_format_unregistered_attributes = (attributes: Array<MdzAttribute>): string => {
|
|
872
|
+
let text = '';
|
|
873
|
+
for (const attr of attributes) {
|
|
874
|
+
if (attr.value === true) {
|
|
875
|
+
text += ` ${attr.name}`;
|
|
876
|
+
} else {
|
|
877
|
+
const quote = attr.value.includes('"') ? "'" : '"';
|
|
878
|
+
text += ` ${attr.name}=${quote}${attr.value}${quote}`;
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
return text;
|
|
882
|
+
};
|
|
883
|
+
|
|
790
884
|
/**
|
|
791
885
|
* Resolves a relative path (`./` or `../`) against a base path.
|
|
792
886
|
* The base is treated as a directory regardless of trailing slash
|
|
@@ -817,6 +911,20 @@ export const mdz_resolve_relative_path = (reference: string, base: string): stri
|
|
|
817
911
|
return segments.join('/');
|
|
818
912
|
};
|
|
819
913
|
|
|
914
|
+
// `/a` → `a`, `//a` → `a` — the pathname form `resolve()` takes; `\` counts as a
|
|
915
|
+
// slash too, and tab/LF/CR are dropped, since browsers read `/\host` and
|
|
916
|
+
// `/<tab>/host` as protocol-relative
|
|
917
|
+
const strip_leading_slashes = (path: string): string => {
|
|
918
|
+
let i = 0;
|
|
919
|
+
for (;;) {
|
|
920
|
+
const c = path.charCodeAt(i);
|
|
921
|
+
if (c !== SLASH && c !== BACKSLASH && c !== TAB && c !== NEWLINE && c !== CARRIAGE_RETURN)
|
|
922
|
+
break;
|
|
923
|
+
i++;
|
|
924
|
+
}
|
|
925
|
+
return path.slice(i);
|
|
926
|
+
};
|
|
927
|
+
|
|
820
928
|
/**
|
|
821
929
|
* How a `Link` node's `reference` should render, shared by all three renderers
|
|
822
930
|
* (`MdzNodeView`, `MdzStreamNodeView`, `mdz_to_svelte`) so the safety gate and
|
|
@@ -826,8 +934,13 @@ export const mdz_resolve_relative_path = (reference: string, base: string): stri
|
|
|
826
934
|
export type MdzLinkRender =
|
|
827
935
|
/** Unsafe protocol — render children only, no `<a>`. */
|
|
828
936
|
| { kind: 'unsafe' }
|
|
829
|
-
/**
|
|
830
|
-
|
|
937
|
+
/**
|
|
938
|
+
* Absolute path — render `resolve(path)` (needs `$app/paths`). `path` is the
|
|
939
|
+
* reference minus its leading slash, SvelteKit's pathname form, which only
|
|
940
|
+
* prepends the base path (or `#` under hash routing; a relative `./`/`../`
|
|
941
|
+
* prefix under `paths.relative` SSR) and keeps the rest verbatim.
|
|
942
|
+
*/
|
|
943
|
+
| { kind: 'resolve'; path: string }
|
|
831
944
|
/** Internal fragment/query/relative/bare ref — raw `href`, no `resolve()`. */
|
|
832
945
|
| { kind: 'internal'; href: string }
|
|
833
946
|
/** External link — raw `href` plus `target="_blank" rel="noopener"`. */
|
|
@@ -836,6 +949,13 @@ export type MdzLinkRender =
|
|
|
836
949
|
/**
|
|
837
950
|
* Classify a `Link` reference into how it should render. Pure — no rendering,
|
|
838
951
|
* no context; `base` is the resolved base path (or `undefined`).
|
|
952
|
+
*
|
|
953
|
+
* Absolute references are authored URLs, not SvelteKit route ids: they classify
|
|
954
|
+
* as `resolve` with the base-relative pathname, never a leading-slash route id,
|
|
955
|
+
* whose resolution would drop `(group)` and empty segments, decode `[x+nn]`
|
|
956
|
+
* escapes, and throw on `[param]` segments. A leading `/` followed by any `/`
|
|
957
|
+
* or `\` run collapses to one `/`, so `//host` and `/\host` stay under the base
|
|
958
|
+
* rather than becoming protocol-relative.
|
|
839
959
|
* @nodocs
|
|
840
960
|
*/
|
|
841
961
|
export const mdz_classify_link = (
|
|
@@ -846,19 +966,14 @@ export const mdz_classify_link = (
|
|
|
846
966
|
if (!mdz_is_safe_reference(reference)) return { kind: 'unsafe' };
|
|
847
967
|
if (link_type === 'internal') {
|
|
848
968
|
if (reference.startsWith('.') && base) {
|
|
849
|
-
return {
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
if (
|
|
854
|
-
reference.startsWith('#') ||
|
|
855
|
-
reference.startsWith('?') ||
|
|
856
|
-
reference.startsWith('.') ||
|
|
857
|
-
!reference.startsWith('/')
|
|
858
|
-
) {
|
|
859
|
-
return { kind: 'internal', href: reference };
|
|
969
|
+
return {
|
|
970
|
+
kind: 'resolve',
|
|
971
|
+
path: strip_leading_slashes(mdz_resolve_relative_path(reference, base))
|
|
972
|
+
};
|
|
860
973
|
}
|
|
861
|
-
|
|
974
|
+
// fragment/query/relative/bare — not under the base, so a raw `href`
|
|
975
|
+
if (!reference.startsWith('/')) return { kind: 'internal', href: reference };
|
|
976
|
+
return { kind: 'resolve', path: strip_leading_slashes(reference) };
|
|
862
977
|
}
|
|
863
978
|
return { kind: 'external', href: reference };
|
|
864
979
|
};
|
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
|
}
|