@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.
Files changed (47) hide show
  1. package/LICENSE +10 -17
  2. package/README.md +2 -3
  3. package/dist/MdzNodeView.svelte +59 -23
  4. package/dist/MdzNodeView.svelte.d.ts.map +1 -1
  5. package/dist/MdzStreamNodeView.svelte +63 -23
  6. package/dist/MdzStreamNodeView.svelte.d.ts.map +1 -1
  7. package/dist/mdz.d.ts +12 -1
  8. package/dist/mdz.d.ts.map +1 -1
  9. package/dist/mdz.js +1 -1
  10. package/dist/mdz_contexts.d.ts +33 -6
  11. package/dist/mdz_contexts.d.ts.map +1 -1
  12. package/dist/mdz_helpers.d.ts +31 -3
  13. package/dist/mdz_helpers.d.ts.map +1 -1
  14. package/dist/mdz_helpers.js +102 -10
  15. package/dist/mdz_lexer.d.ts +3 -1
  16. package/dist/mdz_lexer.d.ts.map +1 -1
  17. package/dist/mdz_lexer.js +89 -15
  18. package/dist/mdz_opcodes.d.ts +3 -1
  19. package/dist/mdz_opcodes.d.ts.map +1 -1
  20. package/dist/mdz_opcodes_to_nodes.d.ts.map +1 -1
  21. package/dist/mdz_opcodes_to_nodes.js +3 -0
  22. package/dist/mdz_stream_parser_link.d.ts.map +1 -1
  23. package/dist/mdz_stream_parser_link.js +81 -9
  24. package/dist/mdz_stream_parser_table.js +8 -1
  25. package/dist/mdz_stream_state.svelte.d.ts +3 -1
  26. package/dist/mdz_stream_state.svelte.d.ts.map +1 -1
  27. package/dist/mdz_stream_state.svelte.js +4 -0
  28. package/dist/mdz_to_svelte.d.ts +17 -4
  29. package/dist/mdz_to_svelte.d.ts.map +1 -1
  30. package/dist/mdz_to_svelte.js +22 -9
  31. package/dist/mdz_token_parser.d.ts.map +1 -1
  32. package/dist/mdz_token_parser.js +16 -2
  33. package/dist/svelte_preprocess_mdz.d.ts +5 -2
  34. package/dist/svelte_preprocess_mdz.d.ts.map +1 -1
  35. package/package.json +11 -10
  36. package/src/lib/mdz.ts +13 -1
  37. package/src/lib/mdz_contexts.ts +36 -6
  38. package/src/lib/mdz_helpers.ts +129 -14
  39. package/src/lib/mdz_lexer.ts +98 -15
  40. package/src/lib/mdz_opcodes.ts +3 -1
  41. package/src/lib/mdz_opcodes_to_nodes.ts +5 -0
  42. package/src/lib/mdz_stream_parser_link.ts +77 -7
  43. package/src/lib/mdz_stream_parser_table.ts +8 -1
  44. package/src/lib/mdz_stream_state.svelte.ts +4 -1
  45. package/src/lib/mdz_to_svelte.ts +57 -13
  46. package/src/lib/mdz_token_parser.ts +16 -2
  47. 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;AAGzD;;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;;;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"}
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"}
@@ -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.href)}')}>${children_markup}</a>`;
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] === undefined) {
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 import_path = components[node.name];
145
- if (!import_path) {
154
+ const entry = components[node.name];
155
+ if (!entry) {
146
156
  has_unconfigured_tags = true;
147
157
  return '';
148
158
  }
149
- imports.set(node.name, { path: import_path, kind: 'default' });
150
- return `<${node.name}>${render_nodes(node.children)}</${node.name}>`;
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;CA+iBlC"}
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"}
@@ -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 { type: node_type, name: tag_name, children, start, end: t.end };
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 { type: node_type, name: token.name, children: [], start: token.start, end: token.end };
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, 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;AA+B1E;;GAEG;AACH,MAAM,WAAW,0BAA0B;IAC1C,gCAAgC;IAChC,OAAO,CAAC,EAAE,KAAK,CAAC,MAAM,GAAG,MAAM,CAAC,CAAC;IAEjC;;;;;;;OAOG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAEpC;;;;;;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"}
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.4.0",
3
+ "version": "0.6.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": "MIT",
9
+ "license": "0BSD",
10
10
  "homepage": "https://mdz.fuz.dev/",
11
11
  "author": {
12
12
  "name": "Ryan Atkinson",
@@ -44,7 +44,7 @@
44
44
  "node": ">=24.14"
45
45
  },
46
46
  "peerDependencies": {
47
- "@fuzdev/fuz_util": ">=0.65.1",
47
+ "@fuzdev/fuz_util": ">=0.77.0",
48
48
  "@sveltejs/kit": "^3",
49
49
  "@types/estree": "^1",
50
50
  "esm-env": "^1",
@@ -64,12 +64,13 @@
64
64
  },
65
65
  "devDependencies": {
66
66
  "@fuzdev/blake3-wasm": "^0.2.0",
67
- "@fuzdev/fuz_code": "^0.50.0",
68
- "@fuzdev/fuz_css": "^0.66.1",
69
- "@fuzdev/fuz_ui": "^0.212.0",
70
- "@fuzdev/fuz_util": "^0.72.0",
71
- "@fuzdev/gro": "^0.214.0",
72
- "@ryanatkn/eslint-config": "^0.12.2",
67
+ "@fuzdev/fuz_code": "^0.53.0",
68
+ "@fuzdev/fuz_css": "^0.70.0",
69
+ "@fuzdev/fuz_schema": "^0.5.0",
70
+ "@fuzdev/fuz_ui": "^0.216.1",
71
+ "@fuzdev/fuz_util": "^0.77.0",
72
+ "@fuzdev/gro": "^0.219.0",
73
+ "@ryanatkn/eslint-config": "^0.13.0",
73
74
  "@sveltejs/acorn-typescript": "^1.0.10",
74
75
  "@sveltejs/adapter-static": "^4.0.0",
75
76
  "@sveltejs/kit": "^3.0.1",
@@ -83,7 +84,7 @@
83
84
  "esm-env": "^1.2.2",
84
85
  "svelte": "^5.57.2",
85
86
  "svelte-check": "^4.7.6",
86
- "svelte-docinfo": "^0.8.0",
87
+ "svelte-docinfo": "^0.13.0",
87
88
  "svelte2tsx": "^0.7.56",
88
89
  "tslib": "^2.8.1",
89
90
  "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: attributes, editing.
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
  }
@@ -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
- * For example, registering 'Alert' allows using `<Alert>...</Alert>` in mdz content.
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
- * The Map values are Svelte components.
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
- export type MdzComponents = Map<string, Component<any, any>>; // TODO support params
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
- * For example, registering 'div' allows using `<div>...</div>` in mdz content.
18
- *
19
- * The Map values are boolean placeholders for future configuration options.
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
 
@@ -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
- /** Route/relative path — wrap in `resolve()` (needs `$app/paths`). */
830
- | { kind: 'resolve'; href: string }
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 { kind: 'resolve', href: mdz_resolve_relative_path(reference, base) };
850
- }
851
- // fragment/query/relative/bare — `resolve()` only accepts absolute paths or
852
- // route ids and throws on anything else
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
- return { kind: 'resolve', href: reference };
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
  };