@fuzdev/fuz_ui 0.183.2 → 0.185.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.
@@ -52,6 +52,15 @@ export declare class Declaration {
52
52
  description?: string | undefined;
53
53
  default_value?: string | undefined;
54
54
  bindable?: boolean | undefined;
55
+ examples?: string[] | undefined;
56
+ deprecated_message?: string | undefined;
57
+ see_also?: string[] | undefined;
58
+ throws?: {
59
+ [x: string]: unknown;
60
+ description: string;
61
+ type?: string | undefined;
62
+ }[] | undefined;
63
+ since?: string | undefined;
55
64
  }[] | undefined;
56
65
  return_type: string | undefined;
57
66
  return_description: string | undefined;
@@ -1 +1 @@
1
- {"version":3,"file":"declaration.svelte.d.ts","sourceRoot":"../src/lib/","sources":["../src/lib/declaration.svelte.ts"],"names":[],"mappings":"AAAA,OAAO,EACN,KAAK,eAAe,EAGpB,MAAM,iCAAiC,CAAC;AAEzC,OAAO,KAAK,EAAC,MAAM,EAAC,MAAM,oBAAoB,CAAC;AAG/C;;GAEG;AACH,qBAAa,WAAW;IACvB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAiB;IACxC,QAAQ,CAAC,gBAAgB,EAAE,eAAe,CAAiB;IAE3D,OAAO,wCAAiC;IAExC;;OAEG;IACH,WAAW,SAA8B;IAEzC,IAAI,SAAwC;IAC5C,IAAI,4FAAwC;IAE5C;;OAEG;IACH,UAAU,qBAQR;IAEF;;OAEG;IACH,OAAO,SAA0D;IAEjE;;OAEG;IACH,gBAAgB,SAMd;IAEF;;OAEG;IACH,YAAY,qBAIV;IAEF;;OAEG;IACH,YAAY,SAAiE;IAE7E,cAAc,qBAAkD;IAChE,WAAW,qBAA+C;IAC1D,kBAAkB,qBAAsD;IACxE,UAAU;;;;;;;oBAA8C;IACxD,KAAK;;;;;;;;oBAAyC;IAC9C,WAAW,qBAA+C;IAC1D,kBAAkB,qBAAsD;IACxE,cAAc;;;;;oBAAkD;IAChE,OAAO,uBAA2C;IAClD,UAAU,uBAA8C;IACxD,MAAM;;;;oBAA0C;IAChD,KAAK,qBAAyC;IAC9C,QAAQ,uBAA4C;IACpD,QAAQ,uBAA4C;IACpD,OAAO,EAAE,KAAK,CAAC,eAAe,CAAC,GAAG,SAAS,CAEzC;IACF,UAAU,EAAE,KAAK,CAAC,eAAe,CAAC,GAAG,SAAS,CAE5C;IAEF,YAAY,UAA2D;IACvE,aAAa,UAAuC;IACpD,iBAAiB,UAAgC;IACjD,cAAc,UAA+D;IAC7E,SAAS,UAAqD;IAC9D,YAAY,UAAuE;gBAEvE,MAAM,EAAE,MAAM,EAAE,gBAAgB,EAAE,eAAe;CAI7D"}
1
+ {"version":3,"file":"declaration.svelte.d.ts","sourceRoot":"../src/lib/","sources":["../src/lib/declaration.svelte.ts"],"names":[],"mappings":"AAAA,OAAO,EACN,KAAK,eAAe,EAGpB,MAAM,iCAAiC,CAAC;AAEzC,OAAO,KAAK,EAAC,MAAM,EAAC,MAAM,oBAAoB,CAAC;AAG/C;;GAEG;AACH,qBAAa,WAAW;IACvB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAiB;IACxC,QAAQ,CAAC,gBAAgB,EAAE,eAAe,CAAiB;IAE3D,OAAO,wCAAiC;IAExC;;OAEG;IACH,WAAW,SAA8B;IAEzC,IAAI,SAAwC;IAC5C,IAAI,4FAAwC;IAE5C;;OAEG;IACH,UAAU,qBAQR;IAEF;;OAEG;IACH,OAAO,SAAoF;IAE3F;;OAEG;IACH,gBAAgB,SAMd;IAEF;;OAEG;IACH,YAAY,qBAIV;IAEF;;OAEG;IACH,YAAY,SAAiE;IAE7E,cAAc,qBAAkD;IAChE,WAAW,qBAA+C;IAC1D,kBAAkB,qBAAsD;IACxE,UAAU;;;;;;;oBAA8C;IACxD,KAAK;;;;;;;;;;;;;;;;;oBAAyC;IAC9C,WAAW,qBAA+C;IAC1D,kBAAkB,qBAAsD;IACxE,cAAc;;;;;oBAAkD;IAChE,OAAO,uBAA2C;IAClD,UAAU,uBAA8C;IACxD,MAAM;;;;oBAA0C;IAChD,KAAK,qBAAyC;IAC9C,QAAQ,uBAA4C;IACpD,QAAQ,uBAA4C;IACpD,OAAO,EAAE,KAAK,CAAC,eAAe,CAAC,GAAG,SAAS,CAEzC;IACF,UAAU,EAAE,KAAK,CAAC,eAAe,CAAC,GAAG,SAAS,CAE5C;IAEF,YAAY,UAA2D;IACvE,aAAa,UAAuC;IACpD,iBAAiB,UAAgC;IACjD,cAAc,UAA+D;IAC7E,SAAS,UAAqD;IAC9D,YAAY,UAAuE;gBAEvE,MAAM,EAAE,MAAM,EAAE,gBAAgB,EAAE,eAAe;CAI7D"}
@@ -22,7 +22,7 @@ export class Declaration {
22
22
  /**
23
23
  * API documentation URL.
24
24
  */
25
- url_api = $derived(`/docs/api/${this.module_path}#${this.name}`);
25
+ url_api = $derived(`/docs/api${this.library.url_prefix}/${this.module_path}#${this.name}`);
26
26
  /**
27
27
  * Generated TypeScript import statement.
28
28
  */
@@ -1,6 +1,10 @@
1
1
  import type { LibraryJson } from '@fuzdev/fuz_util/library_json.js';
2
2
  import { Declaration } from './declaration.svelte.js';
3
3
  import { Module } from './module.svelte.js';
4
+ /**
5
+ * Normalizes a URL prefix: ensures leading `/`, strips trailing `/`, returns `''` for falsy and non-string values.
6
+ */
7
+ export declare const parse_library_url_prefix: (value: unknown) => string;
4
8
  /**
5
9
  * Rich runtime representation of a library.
6
10
  *
@@ -12,6 +16,12 @@ import { Module } from './module.svelte.js';
12
16
  */
13
17
  export declare class Library {
14
18
  readonly library_json: LibraryJson;
19
+ /**
20
+ * URL path prefix for multi-package documentation sites.
21
+ * Prepended to `/docs/api/` paths in `Module.url_api` and `Declaration.url_api`.
22
+ * Default `''` preserves single-package behavior.
23
+ */
24
+ readonly url_prefix: string;
15
25
  package_json: {
16
26
  [x: string]: unknown;
17
27
  name: string;
@@ -116,6 +126,7 @@ export declare class Library {
116
126
  type?: string | undefined;
117
127
  }[] | undefined;
118
128
  since?: string | undefined;
129
+ mutates?: Record<string, string> | undefined;
119
130
  extends?: string[] | undefined;
120
131
  implements?: string[] | undefined;
121
132
  members?: Record<string, unknown>[] | undefined;
@@ -128,11 +139,21 @@ export declare class Library {
128
139
  description?: string | undefined;
129
140
  default_value?: string | undefined;
130
141
  bindable?: boolean | undefined;
142
+ examples?: string[] | undefined;
143
+ deprecated_message?: string | undefined;
144
+ see_also?: string[] | undefined;
145
+ throws?: {
146
+ [x: string]: unknown;
147
+ description: string;
148
+ type?: string | undefined;
149
+ }[] | undefined;
150
+ since?: string | undefined;
131
151
  }[] | undefined;
132
152
  also_exported_from?: string[] | undefined;
133
153
  alias_of?: {
134
154
  module: string;
135
155
  name: string;
156
+ kind: "function" | "class" | "type" | "json" | "variable" | "constructor" | "component" | "css";
136
157
  } | undefined;
137
158
  }[] | undefined;
138
159
  module_comment?: string | undefined;
@@ -171,7 +192,7 @@ export declare class Library {
171
192
  * Declaration lookup map by name. Provides O(1) lookup.
172
193
  */
173
194
  declaration_map: Map<string, Declaration>;
174
- constructor(library_json: LibraryJson);
195
+ constructor(library_json: LibraryJson, url_prefix?: string);
175
196
  /**
176
197
  * Look up a declaration by name.
177
198
  */
@@ -1 +1 @@
1
- {"version":3,"file":"library.svelte.d.ts","sourceRoot":"../src/lib/","sources":["../src/lib/library.svelte.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,WAAW,EAAC,MAAM,kCAAkC,CAAC;AAGlE,OAAO,EAAC,WAAW,EAAC,MAAM,yBAAyB,CAAC;AACpD,OAAO,EAAC,MAAM,EAAC,MAAM,oBAAoB,CAAC;AAE1C;;;;;;;;GAQG;AACH,qBAAa,OAAO;IACnB,QAAQ,CAAC,YAAY,EAAE,WAAW,CAAiB;IAEnD,YAAY;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;MAA4C;IACxD,WAAW;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;MAA2C;IAEtD,IAAI,SAAoC;IACxC,SAAS,SAAyC;IAClD,QAAQ,wCAAwC;IAChD,UAAU,gBAA0C;IACpD,YAAY,+CAA4C;IACxD,QAAQ,+CAAwC;IAChD,QAAQ,SAAwC;IAChD,OAAO,+CAAuC;IAC9C,aAAa,+CAA6C;IAC1D,SAAS,UAAyC;IAElD;;OAEG;IACH,OAAO,gBAML;IAEF;;OAEG;IACH,OAAO,WAIL;IAEF;;OAEG;IACH,cAAc,WAA4E;IAE1F;;OAEG;IACH,YAAY,gBAAmE;IAE/E;;OAEG;IACH,eAAe,2BAAgE;gBAEnE,YAAY,EAAE,WAAW;IAIrC;;OAEG;IACH,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,WAAW,GAAG,SAAS;IAIzD;;OAEG;IACH,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO;IAItC;;OAEG;IACH,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;IAI/C;;OAEG;IACH,mBAAmB,CAAC,KAAK,EAAE,MAAM,GAAG,KAAK,CAAC,WAAW,CAAC;CAGtD;AAED,eAAO,MAAM,eAAe;;;;CAA4B,CAAC"}
1
+ {"version":3,"file":"library.svelte.d.ts","sourceRoot":"../src/lib/","sources":["../src/lib/library.svelte.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,WAAW,EAAC,MAAM,kCAAkC,CAAC;AAIlE,OAAO,EAAC,WAAW,EAAC,MAAM,yBAAyB,CAAC;AACpD,OAAO,EAAC,MAAM,EAAC,MAAM,oBAAoB,CAAC;AAE1C;;GAEG;AACH,eAAO,MAAM,wBAAwB,GAAI,OAAO,OAAO,KAAG,MAGzD,CAAC;AAEF;;;;;;;;GAQG;AACH,qBAAa,OAAO;IACnB,QAAQ,CAAC,YAAY,EAAE,WAAW,CAAiB;IAEnD;;;;OAIG;IACH,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAE5B,YAAY;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;MAA4C;IACxD,WAAW;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;MAA2C;IAEtD,IAAI,SAAoC;IACxC,SAAS,SAAyC;IAClD,QAAQ,wCAAwC;IAChD,UAAU,gBAA0C;IACpD,YAAY,+CAA4C;IACxD,QAAQ,+CAAwC;IAChD,QAAQ,SAAwC;IAChD,OAAO,+CAAuC;IAC9C,aAAa,+CAA6C;IAC1D,SAAS,UAAyC;IAElD;;OAEG;IACH,OAAO,gBAML;IAEF;;OAEG;IACH,OAAO,WAIL;IAEF;;OAEG;IACH,cAAc,WAA4E;IAE1F;;OAEG;IACH,YAAY,gBAAmE;IAE/E;;OAEG;IACH,eAAe,2BAAgE;gBAEnE,YAAY,EAAE,WAAW,EAAE,UAAU,SAAK;IAKtD;;OAEG;IACH,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,WAAW,GAAG,SAAS;IAIzD;;OAEG;IACH,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO;IAItC;;OAEG;IACH,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;IAI/C;;OAEG;IACH,mBAAmB,CAAC,KAAK,EAAE,MAAM,GAAG,KAAK,CAAC,WAAW,CAAC;CAGtD;AAED,eAAO,MAAM,eAAe;;;;CAA4B,CAAC"}
@@ -1,6 +1,15 @@
1
+ import { ensure_start, strip_end } from '@fuzdev/fuz_util/string.js';
1
2
  import { create_context } from './context_helpers.js';
2
3
  import { Declaration } from './declaration.svelte.js';
3
4
  import { Module } from './module.svelte.js';
5
+ /**
6
+ * Normalizes a URL prefix: ensures leading `/`, strips trailing `/`, returns `''` for falsy and non-string values.
7
+ */
8
+ export const parse_library_url_prefix = (value) => {
9
+ if (!value || typeof value !== 'string')
10
+ return '';
11
+ return ensure_start(strip_end(value, '/'), '/');
12
+ };
4
13
  /**
5
14
  * Rich runtime representation of a library.
6
15
  *
@@ -12,6 +21,12 @@ import { Module } from './module.svelte.js';
12
21
  */
13
22
  export class Library {
14
23
  library_json = $state.raw();
24
+ /**
25
+ * URL path prefix for multi-package documentation sites.
26
+ * Prepended to `/docs/api/` paths in `Module.url_api` and `Declaration.url_api`.
27
+ * Default `''` preserves single-package behavior.
28
+ */
29
+ url_prefix;
15
30
  package_json = $derived(this.library_json.package_json);
16
31
  source_json = $derived(this.library_json.source_json);
17
32
  name = $derived(this.library_json.name);
@@ -50,8 +65,9 @@ export class Library {
50
65
  * Declaration lookup map by name. Provides O(1) lookup.
51
66
  */
52
67
  declaration_map = $derived(new Map(this.declarations.map((d) => [d.name, d])));
53
- constructor(library_json) {
68
+ constructor(library_json, url_prefix = '') {
54
69
  this.library_json = library_json;
70
+ this.url_prefix = parse_library_url_prefix(url_prefix);
55
71
  }
56
72
  /**
57
73
  * Look up a declaration by name.
package/dist/mdz.d.ts CHANGED
@@ -16,8 +16,8 @@
16
16
  *
17
17
  * ## Design philosophy
18
18
  *
19
- * - **False negatives over false positives**: Strict syntax prevents accidentally
20
- * interpreting plain text as formatting. When in doubt, treat as plain text.
19
+ * - **False negatives over false positives**: When in doubt, treat as plain text.
20
+ * Block elements can interrupt paragraphs without blank lines; inline formatting is strict.
21
21
  * - **One way to do things**: Single unambiguous syntax per feature. No alternatives.
22
22
  * - **Explicit over implicit**: Clear delimiters and column-0 requirements avoid ambiguity.
23
23
  * - **Simple over complete**: Prefer simple parsing rules over complex edge case handling.
@@ -102,6 +102,9 @@ export declare class MdzParser {
102
102
  /**
103
103
  * Main parse method. Returns flat array of nodes,
104
104
  * with paragraph nodes wrapping content between double newlines.
105
+ *
106
+ * Block elements (headings, HR, codeblocks) are detected at every column-0
107
+ * position — they can interrupt paragraphs without requiring blank lines.
105
108
  */
106
109
  parse(): Array<MdzNode>;
107
110
  }
package/dist/mdz.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"mdz.d.ts","sourceRoot":"../src/lib/","sources":["../src/lib/mdz.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AAIH;;GAEG;AACH,eAAO,MAAM,SAAS,GAAI,MAAM,MAAM,KAAG,KAAK,CAAC,OAAO,CAAgC,CAAC;AAEvF,MAAM,MAAM,OAAO,GAChB,WAAW,GACX,WAAW,GACX,gBAAgB,GAChB,WAAW,GACX,aAAa,GACb,oBAAoB,GACpB,WAAW,GACX,gBAAgB,GAChB,SAAS,GACT,cAAc,GACd,cAAc,GACd,gBAAgB,CAAC;AAEpB,MAAM,WAAW,WAAW;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;CACZ;AAED,MAAM,WAAW,WAAY,SAAQ,WAAW;IAC/C,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,WAAY,SAAQ,WAAW;IAC/C,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,gBAAiB,SAAQ,WAAW;IACpD,IAAI,EAAE,WAAW,CAAC;IAClB,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,WAAY,SAAQ,WAAW;IAC/C,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,KAAK,CAAC,OAAO,CAAC,CAAC;CACzB;AAED,MAAM,WAAW,aAAc,SAAQ,WAAW;IACjD,IAAI,EAAE,QAAQ,CAAC;IACf,QAAQ,EAAE,KAAK,CAAC,OAAO,CAAC,CAAC;CACzB;AAED,MAAM,WAAW,oBAAqB,SAAQ,WAAW;IACxD,IAAI,EAAE,eAAe,CAAC;IACtB,QAAQ,EAAE,KAAK,CAAC,OAAO,CAAC,CAAC;CACzB;AAED,MAAM,WAAW,WAAY,SAAQ,WAAW;IAC/C,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,KAAK,CAAC,OAAO,CAAC,CAAC;IACzB,SAAS,EAAE,UAAU,GAAG,UAAU,CAAC;CACnC;AAED,MAAM,WAAW,gBAAiB,SAAQ,WAAW;IACpD,IAAI,EAAE,WAAW,CAAC;IAClB,QAAQ,EAAE,KAAK,CAAC,OAAO,CAAC,CAAC;CACzB;AAED,MAAM,WAAW,SAAU,SAAQ,WAAW;IAC7C,IAAI,EAAE,IAAI,CAAC;CACX;AAED,MAAM,WAAW,cAAe,SAAQ,WAAW;IAClD,IAAI,EAAE,SAAS,CAAC;IAChB,KAAK,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IAC7B,QAAQ,EAAE,KAAK,CAAC,OAAO,CAAC,CAAC;CACzB;AAED,MAAM,WAAW,cAAe,SAAQ,WAAW;IAClD,IAAI,EAAE,SAAS,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,KAAK,CAAC,OAAO,CAAC,CAAC;CACzB;AAED,MAAM,WAAW,gBAAiB,SAAQ,WAAW;IACpD,IAAI,EAAE,WAAW,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,KAAK,CAAC,OAAO,CAAC,CAAC;CACzB;AA+CD;;;;GAIG;AACH,qBAAa,SAAS;;gBAQT,QAAQ,EAAE,MAAM;IAI5B;;;OAGG;IACH,KAAK,IAAI,KAAK,CAAC,OAAO,CAAC;CA8lDvB;AAQD,eAAO,MAAM,UAAU,GAAI,GAAG,MAAM,KAAG,OAA8B,CAAC"}
1
+ {"version":3,"file":"mdz.d.ts","sourceRoot":"../src/lib/","sources":["../src/lib/mdz.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AAIH;;GAEG;AACH,eAAO,MAAM,SAAS,GAAI,MAAM,MAAM,KAAG,KAAK,CAAC,OAAO,CAAgC,CAAC;AAEvF,MAAM,MAAM,OAAO,GAChB,WAAW,GACX,WAAW,GACX,gBAAgB,GAChB,WAAW,GACX,aAAa,GACb,oBAAoB,GACpB,WAAW,GACX,gBAAgB,GAChB,SAAS,GACT,cAAc,GACd,cAAc,GACd,gBAAgB,CAAC;AAEpB,MAAM,WAAW,WAAW;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;CACZ;AAED,MAAM,WAAW,WAAY,SAAQ,WAAW;IAC/C,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,WAAY,SAAQ,WAAW;IAC/C,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,gBAAiB,SAAQ,WAAW;IACpD,IAAI,EAAE,WAAW,CAAC;IAClB,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,WAAY,SAAQ,WAAW;IAC/C,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,KAAK,CAAC,OAAO,CAAC,CAAC;CACzB;AAED,MAAM,WAAW,aAAc,SAAQ,WAAW;IACjD,IAAI,EAAE,QAAQ,CAAC;IACf,QAAQ,EAAE,KAAK,CAAC,OAAO,CAAC,CAAC;CACzB;AAED,MAAM,WAAW,oBAAqB,SAAQ,WAAW;IACxD,IAAI,EAAE,eAAe,CAAC;IACtB,QAAQ,EAAE,KAAK,CAAC,OAAO,CAAC,CAAC;CACzB;AAED,MAAM,WAAW,WAAY,SAAQ,WAAW;IAC/C,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,KAAK,CAAC,OAAO,CAAC,CAAC;IACzB,SAAS,EAAE,UAAU,GAAG,UAAU,CAAC;CACnC;AAED,MAAM,WAAW,gBAAiB,SAAQ,WAAW;IACpD,IAAI,EAAE,WAAW,CAAC;IAClB,QAAQ,EAAE,KAAK,CAAC,OAAO,CAAC,CAAC;CACzB;AAED,MAAM,WAAW,SAAU,SAAQ,WAAW;IAC7C,IAAI,EAAE,IAAI,CAAC;CACX;AAED,MAAM,WAAW,cAAe,SAAQ,WAAW;IAClD,IAAI,EAAE,SAAS,CAAC;IAChB,KAAK,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IAC7B,QAAQ,EAAE,KAAK,CAAC,OAAO,CAAC,CAAC;CACzB;AAED,MAAM,WAAW,cAAe,SAAQ,WAAW;IAClD,IAAI,EAAE,SAAS,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,KAAK,CAAC,OAAO,CAAC,CAAC;CACzB;AAED,MAAM,WAAW,gBAAiB,SAAQ,WAAW;IACpD,IAAI,EAAE,WAAW,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,KAAK,CAAC,OAAO,CAAC,CAAC;CACzB;AA+CD;;;;GAIG;AACH,qBAAa,SAAS;;gBAQT,QAAQ,EAAE,MAAM;IAI5B;;;;;;OAMG;IACH,KAAK,IAAI,KAAK,CAAC,OAAO,CAAC;CAslDvB;AAQD,eAAO,MAAM,UAAU,GAAI,GAAG,MAAM,KAAG,OAA8B,CAAC"}
package/dist/mdz.js CHANGED
@@ -16,8 +16,8 @@
16
16
  *
17
17
  * ## Design philosophy
18
18
  *
19
- * - **False negatives over false positives**: Strict syntax prevents accidentally
20
- * interpreting plain text as formatting. When in doubt, treat as plain text.
19
+ * - **False negatives over false positives**: When in doubt, treat as plain text.
20
+ * Block elements can interrupt paragraphs without blank lines; inline formatting is strict.
21
21
  * - **One way to do things**: Single unambiguous syntax per feature. No alternatives.
22
22
  * - **Explicit over implicit**: Clear delimiters and column-0 requirements avoid ambiguity.
23
23
  * - **Simple over complete**: Prefer simple parsing rules over complex edge case handling.
@@ -95,88 +95,121 @@ export class MdzParser {
95
95
  /**
96
96
  * Main parse method. Returns flat array of nodes,
97
97
  * with paragraph nodes wrapping content between double newlines.
98
+ *
99
+ * Block elements (headings, HR, codeblocks) are detected at every column-0
100
+ * position — they can interrupt paragraphs without requiring blank lines.
98
101
  */
99
102
  parse() {
100
103
  this.#nodes.length = 0;
101
104
  const root_nodes = [];
102
105
  const paragraph_children = [];
103
- // Check for block element at document start
104
- const start_block = this.#try_parse_block_element();
105
- if (start_block) {
106
- root_nodes.push(start_block);
107
- }
106
+ // Skip leading newlines
107
+ this.#skip_newlines();
108
108
  while (this.#index < this.#template.length) {
109
+ // Peek for block element (read-only match), flush paragraph first, then parse.
110
+ // Flush must happen before parse because parse modifies accumulation state.
111
+ const block_type = this.#peek_block_element();
112
+ if (block_type) {
113
+ const flushed = this.#flush_paragraph(paragraph_children, true);
114
+ if (flushed)
115
+ root_nodes.push(flushed);
116
+ if (block_type === 'heading')
117
+ root_nodes.push(this.#parse_heading());
118
+ else if (block_type === 'hr')
119
+ root_nodes.push(this.#parse_hr());
120
+ else
121
+ root_nodes.push(this.#parse_code_block());
122
+ this.#skip_newlines();
123
+ continue;
124
+ }
109
125
  // Check for paragraph break (double newline)
110
126
  if (this.#is_at_paragraph_break()) {
111
- this.#flush_text();
112
- // Move flushed nodes to paragraph_children
113
- if (this.#nodes.length > 0) {
114
- paragraph_children.push(...this.#nodes);
115
- this.#nodes.length = 0;
116
- }
117
- // Wrap accumulated nodes in paragraph (or add single tag directly)
118
- if (paragraph_children.length > 0) {
119
- if (this.#is_single_tag(paragraph_children)) {
120
- // Single tag (component/element) - add directly without paragraph wrapper (MDX convention)
121
- const tag = paragraph_children.find((n) => n.type === 'Component' || n.type === 'Element');
122
- root_nodes.push(tag);
123
- }
124
- else {
125
- // Regular paragraph
126
- root_nodes.push({
127
- type: 'Paragraph',
128
- children: paragraph_children.slice(),
129
- start: paragraph_children[0].start,
130
- end: paragraph_children[paragraph_children.length - 1].end,
131
- });
132
- }
133
- paragraph_children.length = 0;
134
- }
135
- // Consume the paragraph break
136
- this.#eat('\n\n');
137
- // Check for block element after paragraph break
138
- const block = this.#try_parse_block_element();
139
- if (block) {
140
- root_nodes.push(block);
141
- }
127
+ const flushed = this.#flush_paragraph(paragraph_children, true);
128
+ if (flushed)
129
+ root_nodes.push(flushed);
130
+ this.#skip_newlines();
131
+ continue;
132
+ }
133
+ // Parse inline content
134
+ const node = this.#parse_node();
135
+ if (node.type === 'Text') {
136
+ this.#accumulate_text(node.content, node.start);
142
137
  }
143
138
  else {
144
- const node = this.#parse_node();
145
- if (node.type === 'Text') {
146
- this.#accumulate_text(node.content, node.start);
147
- }
148
- else {
149
- this.#flush_text();
150
- this.#nodes.push(node);
151
- }
152
- if (this.#nodes.length > 0) {
153
- paragraph_children.push(...this.#nodes);
154
- this.#nodes.length = 0;
155
- }
139
+ this.#flush_text();
140
+ this.#nodes.push(node);
141
+ }
142
+ if (this.#nodes.length > 0) {
143
+ paragraph_children.push(...this.#nodes);
144
+ this.#nodes.length = 0;
156
145
  }
157
146
  }
147
+ // Flush remaining content as final paragraph
148
+ const final_paragraph = this.#flush_paragraph(paragraph_children, true);
149
+ if (final_paragraph)
150
+ root_nodes.push(final_paragraph);
151
+ return root_nodes;
152
+ }
153
+ /**
154
+ * Flush accumulated inline content as a paragraph node (or single tag).
155
+ * When `trim_trailing` is true, trims trailing newlines from the last text node
156
+ * (the line break before a block element or paragraph break).
157
+ *
158
+ * @mutates paragraph_children - trims last text node, removes whitespace-only nodes, clears array
159
+ */
160
+ #flush_paragraph(paragraph_children, trim_trailing = false) {
158
161
  this.#flush_text();
159
162
  if (this.#nodes.length > 0) {
160
163
  paragraph_children.push(...this.#nodes);
161
- }
162
- // Wrap remaining nodes in final paragraph if any (or add single tag directly)
163
- if (paragraph_children.length > 0) {
164
- if (this.#is_single_tag(paragraph_children)) {
165
- // Single tag (component/element) - add directly without paragraph wrapper (MDX convention)
166
- const tag = paragraph_children.find((n) => n.type === 'Component' || n.type === 'Element');
167
- root_nodes.push(tag);
164
+ this.#nodes.length = 0;
165
+ }
166
+ if (paragraph_children.length === 0) {
167
+ return null;
168
+ }
169
+ if (trim_trailing) {
170
+ // Trim trailing newlines from the last text node (line break before block element)
171
+ const last = paragraph_children[paragraph_children.length - 1];
172
+ if (last.type === 'Text') {
173
+ const trimmed = last.content.replace(/\n+$/, '');
174
+ if (trimmed) {
175
+ last.content = trimmed;
176
+ last.end = last.start + trimmed.length;
177
+ }
178
+ else {
179
+ paragraph_children.pop();
180
+ }
168
181
  }
169
- else {
170
- // Regular paragraph
171
- root_nodes.push({
172
- type: 'Paragraph',
173
- children: paragraph_children,
174
- start: paragraph_children[0].start,
175
- end: paragraph_children[paragraph_children.length - 1].end,
176
- });
182
+ // Skip whitespace-only paragraphs (e.g. newlines between consecutive blocks)
183
+ const has_content = paragraph_children.some((n) => n.type !== 'Text' || n.content.trim().length > 0);
184
+ if (!has_content) {
185
+ paragraph_children.length = 0;
186
+ return null;
177
187
  }
178
188
  }
179
- return root_nodes;
189
+ // Single tag (component/element) - add directly without paragraph wrapper (MDX convention)
190
+ const single_tag = this.#extract_single_tag(paragraph_children);
191
+ if (single_tag) {
192
+ paragraph_children.length = 0;
193
+ return single_tag;
194
+ }
195
+ // Regular paragraph
196
+ const result = {
197
+ type: 'Paragraph',
198
+ children: paragraph_children.slice(),
199
+ start: paragraph_children[0].start,
200
+ end: paragraph_children[paragraph_children.length - 1].end,
201
+ };
202
+ paragraph_children.length = 0;
203
+ return result;
204
+ }
205
+ /**
206
+ * Consume consecutive newline characters.
207
+ */
208
+ #skip_newlines() {
209
+ while (this.#index < this.#template.length &&
210
+ this.#template.charCodeAt(this.#index) === NEWLINE) {
211
+ this.#index++;
212
+ }
180
213
  }
181
214
  /**
182
215
  * Accumulate text for later flushing (performance optimization).
@@ -667,50 +700,42 @@ export class MdzParser {
667
700
  };
668
701
  }
669
702
  /**
670
- * Check if nodes represent a single tag (component or element) with only whitespace text nodes.
671
- * Used to determine if paragraph wrapping should be skipped (MDX convention).
672
- * Returns true if there's exactly one Component/Element node and all other nodes are whitespace-only Text nodes.
703
+ * Extract a single tag (component or element) if it's the only non-whitespace content.
704
+ * Returns the tag node if paragraph wrapping should be skipped (MDX convention),
705
+ * or null if the content should be wrapped in a paragraph.
673
706
  */
674
- #is_single_tag(nodes) {
675
- let found_tag = false;
707
+ #extract_single_tag(nodes) {
708
+ let tag = null;
676
709
  for (const node of nodes) {
677
710
  if (node.type === 'Component' || node.type === 'Element') {
678
- if (found_tag)
679
- return false; // Multiple tags
680
- found_tag = true;
711
+ if (tag)
712
+ return null; // Multiple tags
713
+ tag = node;
681
714
  }
682
715
  else if (node.type === 'Text') {
683
716
  // Allow only whitespace-only text nodes
684
717
  if (node.content.trim() !== '')
685
- return false;
718
+ return null;
686
719
  }
687
720
  else {
688
721
  // Any other node type means not a single tag
689
- return false;
722
+ return null;
690
723
  }
691
724
  }
692
- return found_tag;
725
+ return tag;
693
726
  }
694
727
  /**
695
- * Try to parse a block element (heading, hr, or codeblock) at current position.
696
- * Returns the parsed block node if a match is found, null otherwise.
697
- *
698
- * Block elements must:
699
- * - Start at column 0 (no leading whitespace)
700
- * - Be followed by blank line or EOF
701
- *
702
- * This helper eliminates duplication between document start and post-paragraph-break parsing.
728
+ * Read-only check if current position matches a block element.
729
+ * Does not modify parser state used to peek before flushing paragraph.
730
+ * Returns which block type matched, or null if none.
703
731
  */
704
- #try_parse_block_element() {
705
- if (this.#match_heading()) {
706
- return this.#parse_heading();
707
- }
708
- else if (this.#match_hr()) {
709
- return this.#parse_hr();
710
- }
711
- else if (this.#match_code_block()) {
712
- return this.#parse_code_block();
713
- }
732
+ #peek_block_element() {
733
+ if (this.#match_heading())
734
+ return 'heading';
735
+ if (this.#match_hr())
736
+ return 'hr';
737
+ if (this.#match_code_block())
738
+ return 'codeblock';
714
739
  return null;
715
740
  }
716
741
  /**
@@ -1037,6 +1062,20 @@ export class MdzParser {
1037
1062
  if (this.#is_at_paragraph_break()) {
1038
1063
  break;
1039
1064
  }
1065
+ // When next line could start a block element, consume the newline and stop.
1066
+ // The main loop will try block detection at the next character.
1067
+ // Consuming the newline here avoids a 3-iteration detour (break before \n,
1068
+ // fail peek at \n, safety-increment, then succeed peek at block char).
1069
+ if (char_code === NEWLINE) {
1070
+ const next_i = this.#index + 1;
1071
+ if (next_i < this.#template.length) {
1072
+ const next_char = this.#template.charCodeAt(next_i);
1073
+ if (next_char === HASH || next_char === HYPHEN || next_char === BACKTICK) {
1074
+ this.#index++; // consume the newline
1075
+ break;
1076
+ }
1077
+ }
1078
+ }
1040
1079
  // Check for URL or internal path mid-text
1041
1080
  if (this.#is_at_url() || this.#is_at_internal_path()) {
1042
1081
  break;
@@ -1127,18 +1166,15 @@ export class MdzParser {
1127
1166
  }
1128
1167
  /**
1129
1168
  * Check if current position matches a horizontal rule.
1130
- * HR must be exactly `---` at column 0, followed by blank line or EOF.
1169
+ * HR must be exactly `---` at column 0, followed by newline or EOF.
1131
1170
  *
1132
- * Blank line requirement rationale:
1133
- * Prevents block elements from accidentally consuming following content.
1134
- * Without this, `---` followed by regular text would create an hr and treat
1135
- * the next line as a new paragraph, which could be surprising. The blank line
1136
- * makes block element boundaries explicit and predictable.
1171
+ * mdz has no setext headings, so `---` after a paragraph is unambiguous
1172
+ * (always an HR, unlike CommonMark where it becomes a setext heading).
1137
1173
  */
1138
1174
  #match_hr() {
1139
1175
  let i = this.#index;
1140
- // Must start at column 0 (no leading whitespace)
1141
- if (i < this.#template.length && this.#template.charCodeAt(i) === SPACE) {
1176
+ // Must start at column 0 (beginning of input or after newline)
1177
+ if (i > 0 && this.#template.charCodeAt(i - 1) !== NEWLINE) {
1142
1178
  return false;
1143
1179
  }
1144
1180
  // Must have exactly three hyphens
@@ -1153,15 +1189,7 @@ export class MdzParser {
1153
1189
  while (i < this.#template.length) {
1154
1190
  const char_code = this.#template.charCodeAt(i);
1155
1191
  if (char_code === NEWLINE) {
1156
- // Found newline - check if followed by another newline (blank line) or EOF
1157
- const next_i = i + 1;
1158
- if (next_i >= this.#template.length) {
1159
- return true; // hr followed by newline + EOF
1160
- }
1161
- if (this.#template.charCodeAt(next_i) === NEWLINE) {
1162
- return true; // hr followed by blank line
1163
- }
1164
- return false; // hr followed by single newline + content
1192
+ return true;
1165
1193
  }
1166
1194
  if (char_code !== SPACE) {
1167
1195
  return false; // Non-whitespace after ---, not an hr
@@ -1194,17 +1222,12 @@ export class MdzParser {
1194
1222
  /**
1195
1223
  * Check if current position matches a heading.
1196
1224
  * Heading must be 1-6 hashes at column 0, followed by space and content,
1197
- * followed by blank line or EOF.
1198
- *
1199
- * Blank line requirement rationale:
1200
- * Ensures headings are visually and semantically separate from following content.
1201
- * Without this, `# Heading\nText` would be ambiguous - is the text part of the
1202
- * heading or a new paragraph? The blank line makes document structure explicit.
1225
+ * followed by newline or EOF.
1203
1226
  */
1204
1227
  #match_heading() {
1205
1228
  let i = this.#index;
1206
- // Must start at column 0 (no leading whitespace)
1207
- if (i < this.#template.length && this.#template.charCodeAt(i) === SPACE) {
1229
+ // Must start at column 0 (beginning of input or after newline)
1230
+ if (i > 0 && this.#template.charCodeAt(i - 1) !== NEWLINE) {
1208
1231
  return false;
1209
1232
  }
1210
1233
  // Count hashes (must be 1-6)
@@ -1235,23 +1258,8 @@ export class MdzParser {
1235
1258
  if (!has_content) {
1236
1259
  return false; // heading with only whitespace, treat as plain text
1237
1260
  }
1238
- // At this point we're at newline or EOF
1239
- // Check for blank line after (newline + newline) or EOF
1240
- if (i >= this.#template.length) {
1241
- return true; // heading at EOF
1242
- }
1243
- // Must have newline
1244
- if (this.#template.charCodeAt(i) !== NEWLINE) {
1245
- return false;
1246
- }
1247
- const next_i = i + 1;
1248
- if (next_i >= this.#template.length) {
1249
- return true; // heading followed by newline + EOF
1250
- }
1251
- if (this.#template.charCodeAt(next_i) === NEWLINE) {
1252
- return true; // heading followed by blank line
1253
- }
1254
- return false; // heading followed by single newline + content
1261
+ // At newline or EOF both are valid
1262
+ return true;
1255
1263
  }
1256
1264
  /**
1257
1265
  * Parse heading: `# Heading text`
@@ -1312,19 +1320,13 @@ export class MdzParser {
1312
1320
  }
1313
1321
  /**
1314
1322
  * Check if current position matches a code block.
1315
- * Code block must be 3+ backticks at column 0, followed by blank line or EOF.
1323
+ * Code block must be 3+ backticks at column 0, closing fence followed by newline or EOF.
1316
1324
  * Empty code blocks (no content) are treated as invalid.
1317
- *
1318
- * Blank line requirement rationale:
1319
- * Separates code blocks from following content to prevent ambiguity.
1320
- * Codeblocks are distinct semantic units that should be visually isolated.
1321
- * The blank line makes it explicit where the code block ends and regular
1322
- * content begins, following the "explicit over implicit" design principle.
1323
1325
  */
1324
1326
  #match_code_block() {
1325
1327
  let i = this.#index;
1326
- // Must start at column 0 (no leading whitespace)
1327
- if (i < this.#template.length && this.#template.charCodeAt(i) === SPACE) {
1328
+ // Must start at column 0 (beginning of input or after newline)
1329
+ if (i > 0 && this.#template.charCodeAt(i - 1) !== NEWLINE) {
1328
1330
  return false;
1329
1331
  }
1330
1332
  // Must have at least three backticks
@@ -1386,15 +1388,7 @@ export class MdzParser {
1386
1388
  // closing fence has non-whitespace after it on same line - not a code block
1387
1389
  return false;
1388
1390
  }
1389
- // Check if followed by blank line or EOF
1390
- const next_j = j + 1;
1391
- if (next_j >= this.#template.length) {
1392
- return true; // code block followed by newline + EOF
1393
- }
1394
- if (this.#template.charCodeAt(next_j) === NEWLINE) {
1395
- return true; // code block followed by blank line
1396
- }
1397
- return false; // code block followed by single newline + content
1391
+ return true; // code block followed by newline or EOF
1398
1392
  }
1399
1393
  }
1400
1394
  i++;
@@ -1 +1 @@
1
- {"version":3,"file":"module.svelte.d.ts","sourceRoot":"../src/lib/","sources":["../src/lib/module.svelte.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,iCAAiC,CAAC;AAEhE,OAAO,EAAC,WAAW,EAAC,MAAM,yBAAyB,CAAC;AACpD,OAAO,KAAK,EAAC,OAAO,EAAC,MAAM,qBAAqB,CAAC;AAGjD;;GAEG;AACH,qBAAa,MAAM;IAClB,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAiB;IAC1C,QAAQ,CAAC,WAAW,EAAE,UAAU,CAAiB;IAEjD;;OAEG;IACH,IAAI,SAAmC;IAEvC;;OAEG;IACH,WAAW,SAA8B;IAEzC,cAAc,qBAA6C;IAE3D;;OAEG;IACH,YAAY,gBAMV;IAEF;;OAEG;IACH,OAAO,SAAsC;IAE7C;;OAEG;IACH,UAAU,qBAIR;IAEF,gBAAgB,EAAE,OAAO,CAEvB;IAEF,kBAAkB,EAAE,OAAO,CAA+C;IAE1E;;OAEG;IACH,YAAY,uBAA2C;IAEvD;;OAEG;IACH,UAAU,uBAAyC;gBAEvC,OAAO,EAAE,OAAO,EAAE,WAAW,EAAE,UAAU;IAKrD;;OAEG;IACH,uBAAuB,CAAC,IAAI,EAAE,MAAM,GAAG,WAAW,GAAG,SAAS;CAG9D"}
1
+ {"version":3,"file":"module.svelte.d.ts","sourceRoot":"../src/lib/","sources":["../src/lib/module.svelte.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,iCAAiC,CAAC;AAEhE,OAAO,EAAC,WAAW,EAAC,MAAM,yBAAyB,CAAC;AACpD,OAAO,KAAK,EAAC,OAAO,EAAC,MAAM,qBAAqB,CAAC;AAGjD;;GAEG;AACH,qBAAa,MAAM;IAClB,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAiB;IAC1C,QAAQ,CAAC,WAAW,EAAE,UAAU,CAAiB;IAEjD;;OAEG;IACH,IAAI,SAAmC;IAEvC;;OAEG;IACH,WAAW,SAA8B;IAEzC,cAAc,qBAA6C;IAE3D;;OAEG;IACH,YAAY,gBAMV;IAEF;;OAEG;IACH,OAAO,SAAgE;IAEvE;;OAEG;IACH,UAAU,qBAIR;IAEF,gBAAgB,EAAE,OAAO,CAEvB;IAEF,kBAAkB,EAAE,OAAO,CAA+C;IAE1E;;OAEG;IACH,YAAY,uBAA2C;IAEvD;;OAEG;IACH,UAAU,uBAAyC;gBAEvC,OAAO,EAAE,OAAO,EAAE,WAAW,EAAE,UAAU;IAKrD;;OAEG;IACH,uBAAuB,CAAC,IAAI,EAAE,MAAM,GAAG,WAAW,GAAG,SAAS;CAG9D"}
@@ -26,7 +26,7 @@ export class Module {
26
26
  /**
27
27
  * API documentation URL for this module.
28
28
  */
29
- url_api = $derived(`/docs/api/${this.path}`);
29
+ url_api = $derived(`/docs/api${this.library.url_prefix}/${this.path}`);
30
30
  /**
31
31
  * GitHub source URL.
32
32
  */
package/package.json CHANGED
@@ -1,12 +1,11 @@
1
1
  {
2
2
  "name": "@fuzdev/fuz_ui",
3
- "version": "0.183.2",
3
+ "version": "0.185.0",
4
4
  "description": "Svelte UI library",
5
5
  "motto": "friendly user zystem",
6
6
  "glyph": "🧶",
7
7
  "logo": "logo.svg",
8
8
  "logo_alt": "a friendly brown spider facing you",
9
- "public": true,
10
9
  "license": "MIT",
11
10
  "homepage": "https://ui.fuz.dev/",
12
11
  "author": {
@@ -35,10 +34,10 @@
35
34
  "node": ">=22.15"
36
35
  },
37
36
  "peerDependencies": {
38
- "@fuzdev/fuz_code": ">=0.45.0",
39
- "@fuzdev/fuz_css": ">=0.47.0",
40
- "@fuzdev/fuz_util": ">=0.50.1",
41
- "@fuzdev/gro": ">=0.192.0",
37
+ "@fuzdev/fuz_code": ">=0.45.1",
38
+ "@fuzdev/fuz_css": ">=0.53.0",
39
+ "@fuzdev/fuz_util": ">=0.52.0",
40
+ "@fuzdev/gro": ">=0.195.0",
42
41
  "@jridgewell/trace-mapping": "^0.3",
43
42
  "@sveltejs/kit": "^2.47.3",
44
43
  "@types/estree": "^1",
@@ -76,9 +75,9 @@
76
75
  "devDependencies": {
77
76
  "@changesets/changelog-git": "^0.2.1",
78
77
  "@fuzdev/fuz_code": "^0.45.1",
79
- "@fuzdev/fuz_css": "^0.51.0",
80
- "@fuzdev/fuz_util": "^0.50.1",
81
- "@fuzdev/gro": "^0.194.0",
78
+ "@fuzdev/fuz_css": "^0.53.0",
79
+ "@fuzdev/fuz_util": "^0.52.0",
80
+ "@fuzdev/gro": "^0.195.0",
82
81
  "@jridgewell/trace-mapping": "^0.3.31",
83
82
  "@ryanatkn/eslint-config": "^0.9.0",
84
83
  "@sveltejs/adapter-static": "^3.0.10",
@@ -40,7 +40,7 @@ export class Declaration {
40
40
  /**
41
41
  * API documentation URL.
42
42
  */
43
- url_api = $derived(`/docs/api/${this.module_path}#${this.name}`);
43
+ url_api = $derived(`/docs/api${this.library.url_prefix}/${this.module_path}#${this.name}`);
44
44
 
45
45
  /**
46
46
  * Generated TypeScript import statement.
@@ -1,9 +1,18 @@
1
1
  import type {LibraryJson} from '@fuzdev/fuz_util/library_json.js';
2
+ import {ensure_start, strip_end} from '@fuzdev/fuz_util/string.js';
2
3
 
3
4
  import {create_context} from './context_helpers.js';
4
5
  import {Declaration} from './declaration.svelte.js';
5
6
  import {Module} from './module.svelte.js';
6
7
 
8
+ /**
9
+ * Normalizes a URL prefix: ensures leading `/`, strips trailing `/`, returns `''` for falsy and non-string values.
10
+ */
11
+ export const parse_library_url_prefix = (value: unknown): string => {
12
+ if (!value || typeof value !== 'string') return '';
13
+ return ensure_start(strip_end(value, '/'), '/');
14
+ };
15
+
7
16
  /**
8
17
  * Rich runtime representation of a library.
9
18
  *
@@ -16,6 +25,13 @@ import {Module} from './module.svelte.js';
16
25
  export class Library {
17
26
  readonly library_json: LibraryJson = $state.raw()!;
18
27
 
28
+ /**
29
+ * URL path prefix for multi-package documentation sites.
30
+ * Prepended to `/docs/api/` paths in `Module.url_api` and `Declaration.url_api`.
31
+ * Default `''` preserves single-package behavior.
32
+ */
33
+ readonly url_prefix: string;
34
+
19
35
  package_json = $derived(this.library_json.package_json);
20
36
  source_json = $derived(this.library_json.source_json);
21
37
 
@@ -65,8 +81,9 @@ export class Library {
65
81
  */
66
82
  declaration_map = $derived(new Map(this.declarations.map((d) => [d.name, d])));
67
83
 
68
- constructor(library_json: LibraryJson) {
84
+ constructor(library_json: LibraryJson, url_prefix = '') {
69
85
  this.library_json = library_json;
86
+ this.url_prefix = parse_library_url_prefix(url_prefix);
70
87
  }
71
88
 
72
89
  /**
package/src/lib/mdz.ts CHANGED
@@ -16,8 +16,8 @@
16
16
  *
17
17
  * ## Design philosophy
18
18
  *
19
- * - **False negatives over false positives**: Strict syntax prevents accidentally
20
- * interpreting plain text as formatting. When in doubt, treat as plain text.
19
+ * - **False negatives over false positives**: When in doubt, treat as plain text.
20
+ * Block elements can interrupt paragraphs without blank lines; inline formatting is strict.
21
21
  * - **One way to do things**: Single unambiguous syntax per feature. No alternatives.
22
22
  * - **Explicit over implicit**: Clear delimiters and column-0 requirements avoid ambiguity.
23
23
  * - **Simple over complete**: Prefer simple parsing rules over complex edge case handling.
@@ -186,92 +186,130 @@ export class MdzParser {
186
186
  /**
187
187
  * Main parse method. Returns flat array of nodes,
188
188
  * with paragraph nodes wrapping content between double newlines.
189
+ *
190
+ * Block elements (headings, HR, codeblocks) are detected at every column-0
191
+ * position — they can interrupt paragraphs without requiring blank lines.
189
192
  */
190
193
  parse(): Array<MdzNode> {
191
194
  this.#nodes.length = 0;
192
195
  const root_nodes: Array<MdzNode> = [];
193
196
  const paragraph_children: Array<MdzNode> = [];
194
197
 
195
- // Check for block element at document start
196
- const start_block = this.#try_parse_block_element();
197
- if (start_block) {
198
- root_nodes.push(start_block);
199
- }
198
+ // Skip leading newlines
199
+ this.#skip_newlines();
200
200
 
201
201
  while (this.#index < this.#template.length) {
202
+ // Peek for block element (read-only match), flush paragraph first, then parse.
203
+ // Flush must happen before parse because parse modifies accumulation state.
204
+ const block_type = this.#peek_block_element();
205
+ if (block_type) {
206
+ const flushed = this.#flush_paragraph(paragraph_children, true);
207
+ if (flushed) root_nodes.push(flushed);
208
+ if (block_type === 'heading') root_nodes.push(this.#parse_heading());
209
+ else if (block_type === 'hr') root_nodes.push(this.#parse_hr());
210
+ else root_nodes.push(this.#parse_code_block());
211
+ this.#skip_newlines();
212
+ continue;
213
+ }
214
+
202
215
  // Check for paragraph break (double newline)
203
216
  if (this.#is_at_paragraph_break()) {
204
- this.#flush_text();
205
- // Move flushed nodes to paragraph_children
206
- if (this.#nodes.length > 0) {
207
- paragraph_children.push(...this.#nodes);
208
- this.#nodes.length = 0;
209
- }
210
- // Wrap accumulated nodes in paragraph (or add single tag directly)
211
- if (paragraph_children.length > 0) {
212
- if (this.#is_single_tag(paragraph_children)) {
213
- // Single tag (component/element) - add directly without paragraph wrapper (MDX convention)
214
- const tag = paragraph_children.find(
215
- (n) => n.type === 'Component' || n.type === 'Element',
216
- )!;
217
- root_nodes.push(tag);
218
- } else {
219
- // Regular paragraph
220
- root_nodes.push({
221
- type: 'Paragraph',
222
- children: paragraph_children.slice(),
223
- start: paragraph_children[0]!.start,
224
- end: paragraph_children[paragraph_children.length - 1]!.end,
225
- });
226
- }
227
- paragraph_children.length = 0;
228
- }
229
- // Consume the paragraph break
230
- this.#eat('\n\n');
217
+ const flushed = this.#flush_paragraph(paragraph_children, true);
218
+ if (flushed) root_nodes.push(flushed);
219
+ this.#skip_newlines();
220
+ continue;
221
+ }
231
222
 
232
- // Check for block element after paragraph break
233
- const block = this.#try_parse_block_element();
234
- if (block) {
235
- root_nodes.push(block);
236
- }
223
+ // Parse inline content
224
+ const node = this.#parse_node();
225
+ if (node.type === 'Text') {
226
+ this.#accumulate_text(node.content, node.start);
237
227
  } else {
238
- const node = this.#parse_node();
239
- if (node.type === 'Text') {
240
- this.#accumulate_text(node.content, node.start);
241
- } else {
242
- this.#flush_text();
243
- this.#nodes.push(node);
244
- }
245
- if (this.#nodes.length > 0) {
246
- paragraph_children.push(...this.#nodes);
247
- this.#nodes.length = 0;
248
- }
228
+ this.#flush_text();
229
+ this.#nodes.push(node);
230
+ }
231
+ if (this.#nodes.length > 0) {
232
+ paragraph_children.push(...this.#nodes);
233
+ this.#nodes.length = 0;
249
234
  }
250
235
  }
251
236
 
237
+ // Flush remaining content as final paragraph
238
+ const final_paragraph = this.#flush_paragraph(paragraph_children, true);
239
+ if (final_paragraph) root_nodes.push(final_paragraph);
240
+
241
+ return root_nodes;
242
+ }
243
+
244
+ /**
245
+ * Flush accumulated inline content as a paragraph node (or single tag).
246
+ * When `trim_trailing` is true, trims trailing newlines from the last text node
247
+ * (the line break before a block element or paragraph break).
248
+ *
249
+ * @mutates paragraph_children - trims last text node, removes whitespace-only nodes, clears array
250
+ */
251
+ #flush_paragraph(paragraph_children: Array<MdzNode>, trim_trailing = false): MdzNode | null {
252
252
  this.#flush_text();
253
253
  if (this.#nodes.length > 0) {
254
254
  paragraph_children.push(...this.#nodes);
255
+ this.#nodes.length = 0;
255
256
  }
256
257
 
257
- // Wrap remaining nodes in final paragraph if any (or add single tag directly)
258
- if (paragraph_children.length > 0) {
259
- if (this.#is_single_tag(paragraph_children)) {
260
- // Single tag (component/element) - add directly without paragraph wrapper (MDX convention)
261
- const tag = paragraph_children.find((n) => n.type === 'Component' || n.type === 'Element')!;
262
- root_nodes.push(tag);
263
- } else {
264
- // Regular paragraph
265
- root_nodes.push({
266
- type: 'Paragraph',
267
- children: paragraph_children,
268
- start: paragraph_children[0]!.start,
269
- end: paragraph_children[paragraph_children.length - 1]!.end,
270
- });
258
+ if (paragraph_children.length === 0) {
259
+ return null;
260
+ }
261
+
262
+ if (trim_trailing) {
263
+ // Trim trailing newlines from the last text node (line break before block element)
264
+ const last = paragraph_children[paragraph_children.length - 1]!;
265
+ if (last.type === 'Text') {
266
+ const trimmed = last.content.replace(/\n+$/, '');
267
+ if (trimmed) {
268
+ last.content = trimmed;
269
+ last.end = last.start + trimmed.length;
270
+ } else {
271
+ paragraph_children.pop();
272
+ }
273
+ }
274
+
275
+ // Skip whitespace-only paragraphs (e.g. newlines between consecutive blocks)
276
+ const has_content = paragraph_children.some(
277
+ (n) => n.type !== 'Text' || n.content.trim().length > 0,
278
+ );
279
+ if (!has_content) {
280
+ paragraph_children.length = 0;
281
+ return null;
271
282
  }
272
283
  }
273
284
 
274
- return root_nodes;
285
+ // Single tag (component/element) - add directly without paragraph wrapper (MDX convention)
286
+ const single_tag = this.#extract_single_tag(paragraph_children);
287
+ if (single_tag) {
288
+ paragraph_children.length = 0;
289
+ return single_tag;
290
+ }
291
+
292
+ // Regular paragraph
293
+ const result: MdzParagraphNode = {
294
+ type: 'Paragraph',
295
+ children: paragraph_children.slice(),
296
+ start: paragraph_children[0]!.start,
297
+ end: paragraph_children[paragraph_children.length - 1]!.end,
298
+ };
299
+ paragraph_children.length = 0;
300
+ return result;
301
+ }
302
+
303
+ /**
304
+ * Consume consecutive newline characters.
305
+ */
306
+ #skip_newlines(): void {
307
+ while (
308
+ this.#index < this.#template.length &&
309
+ this.#template.charCodeAt(this.#index) === NEWLINE
310
+ ) {
311
+ this.#index++;
312
+ }
275
313
  }
276
314
 
277
315
  /**
@@ -870,47 +908,38 @@ export class MdzParser {
870
908
  }
871
909
 
872
910
  /**
873
- * Check if nodes represent a single tag (component or element) with only whitespace text nodes.
874
- * Used to determine if paragraph wrapping should be skipped (MDX convention).
875
- * Returns true if there's exactly one Component/Element node and all other nodes are whitespace-only Text nodes.
911
+ * Extract a single tag (component or element) if it's the only non-whitespace content.
912
+ * Returns the tag node if paragraph wrapping should be skipped (MDX convention),
913
+ * or null if the content should be wrapped in a paragraph.
876
914
  */
877
- #is_single_tag(nodes: Array<MdzNode>): boolean {
878
- let found_tag = false;
915
+ #extract_single_tag(nodes: Array<MdzNode>): MdzComponentNode | MdzElementNode | null {
916
+ let tag: MdzComponentNode | MdzElementNode | null = null;
879
917
 
880
918
  for (const node of nodes) {
881
919
  if (node.type === 'Component' || node.type === 'Element') {
882
- if (found_tag) return false; // Multiple tags
883
- found_tag = true;
920
+ if (tag) return null; // Multiple tags
921
+ tag = node;
884
922
  } else if (node.type === 'Text') {
885
923
  // Allow only whitespace-only text nodes
886
- if (node.content.trim() !== '') return false;
924
+ if (node.content.trim() !== '') return null;
887
925
  } else {
888
926
  // Any other node type means not a single tag
889
- return false;
927
+ return null;
890
928
  }
891
929
  }
892
930
 
893
- return found_tag;
931
+ return tag;
894
932
  }
895
933
 
896
934
  /**
897
- * Try to parse a block element (heading, hr, or codeblock) at current position.
898
- * Returns the parsed block node if a match is found, null otherwise.
899
- *
900
- * Block elements must:
901
- * - Start at column 0 (no leading whitespace)
902
- * - Be followed by blank line or EOF
903
- *
904
- * This helper eliminates duplication between document start and post-paragraph-break parsing.
935
+ * Read-only check if current position matches a block element.
936
+ * Does not modify parser state used to peek before flushing paragraph.
937
+ * Returns which block type matched, or null if none.
905
938
  */
906
- #try_parse_block_element(): MdzHeadingNode | MdzHrNode | MdzCodeblockNode | null {
907
- if (this.#match_heading()) {
908
- return this.#parse_heading();
909
- } else if (this.#match_hr()) {
910
- return this.#parse_hr();
911
- } else if (this.#match_code_block()) {
912
- return this.#parse_code_block();
913
- }
939
+ #peek_block_element(): 'heading' | 'hr' | 'codeblock' | null {
940
+ if (this.#match_heading()) return 'heading';
941
+ if (this.#match_hr()) return 'hr';
942
+ if (this.#match_code_block()) return 'codeblock';
914
943
  return null;
915
944
  }
916
945
 
@@ -1288,6 +1317,21 @@ export class MdzParser {
1288
1317
  break;
1289
1318
  }
1290
1319
 
1320
+ // When next line could start a block element, consume the newline and stop.
1321
+ // The main loop will try block detection at the next character.
1322
+ // Consuming the newline here avoids a 3-iteration detour (break before \n,
1323
+ // fail peek at \n, safety-increment, then succeed peek at block char).
1324
+ if (char_code === NEWLINE) {
1325
+ const next_i = this.#index + 1;
1326
+ if (next_i < this.#template.length) {
1327
+ const next_char = this.#template.charCodeAt(next_i);
1328
+ if (next_char === HASH || next_char === HYPHEN || next_char === BACKTICK) {
1329
+ this.#index++; // consume the newline
1330
+ break;
1331
+ }
1332
+ }
1333
+ }
1334
+
1291
1335
  // Check for URL or internal path mid-text
1292
1336
  if (this.#is_at_url() || this.#is_at_internal_path()) {
1293
1337
  break;
@@ -1395,19 +1439,16 @@ export class MdzParser {
1395
1439
 
1396
1440
  /**
1397
1441
  * Check if current position matches a horizontal rule.
1398
- * HR must be exactly `---` at column 0, followed by blank line or EOF.
1442
+ * HR must be exactly `---` at column 0, followed by newline or EOF.
1399
1443
  *
1400
- * Blank line requirement rationale:
1401
- * Prevents block elements from accidentally consuming following content.
1402
- * Without this, `---` followed by regular text would create an hr and treat
1403
- * the next line as a new paragraph, which could be surprising. The blank line
1404
- * makes block element boundaries explicit and predictable.
1444
+ * mdz has no setext headings, so `---` after a paragraph is unambiguous
1445
+ * (always an HR, unlike CommonMark where it becomes a setext heading).
1405
1446
  */
1406
1447
  #match_hr(): boolean {
1407
1448
  let i = this.#index;
1408
1449
 
1409
- // Must start at column 0 (no leading whitespace)
1410
- if (i < this.#template.length && this.#template.charCodeAt(i) === SPACE) {
1450
+ // Must start at column 0 (beginning of input or after newline)
1451
+ if (i > 0 && this.#template.charCodeAt(i - 1) !== NEWLINE) {
1411
1452
  return false;
1412
1453
  }
1413
1454
 
@@ -1426,15 +1467,7 @@ export class MdzParser {
1426
1467
  while (i < this.#template.length) {
1427
1468
  const char_code = this.#template.charCodeAt(i);
1428
1469
  if (char_code === NEWLINE) {
1429
- // Found newline - check if followed by another newline (blank line) or EOF
1430
- const next_i = i + 1;
1431
- if (next_i >= this.#template.length) {
1432
- return true; // hr followed by newline + EOF
1433
- }
1434
- if (this.#template.charCodeAt(next_i) === NEWLINE) {
1435
- return true; // hr followed by blank line
1436
- }
1437
- return false; // hr followed by single newline + content
1470
+ return true;
1438
1471
  }
1439
1472
  if (char_code !== SPACE) {
1440
1473
  return false; // Non-whitespace after ---, not an hr
@@ -1476,18 +1509,13 @@ export class MdzParser {
1476
1509
  /**
1477
1510
  * Check if current position matches a heading.
1478
1511
  * Heading must be 1-6 hashes at column 0, followed by space and content,
1479
- * followed by blank line or EOF.
1480
- *
1481
- * Blank line requirement rationale:
1482
- * Ensures headings are visually and semantically separate from following content.
1483
- * Without this, `# Heading\nText` would be ambiguous - is the text part of the
1484
- * heading or a new paragraph? The blank line makes document structure explicit.
1512
+ * followed by newline or EOF.
1485
1513
  */
1486
1514
  #match_heading(): boolean {
1487
1515
  let i = this.#index;
1488
1516
 
1489
- // Must start at column 0 (no leading whitespace)
1490
- if (i < this.#template.length && this.#template.charCodeAt(i) === SPACE) {
1517
+ // Must start at column 0 (beginning of input or after newline)
1518
+ if (i > 0 && this.#template.charCodeAt(i - 1) !== NEWLINE) {
1491
1519
  return false;
1492
1520
  }
1493
1521
 
@@ -1526,27 +1554,8 @@ export class MdzParser {
1526
1554
  return false; // heading with only whitespace, treat as plain text
1527
1555
  }
1528
1556
 
1529
- // At this point we're at newline or EOF
1530
- // Check for blank line after (newline + newline) or EOF
1531
- if (i >= this.#template.length) {
1532
- return true; // heading at EOF
1533
- }
1534
-
1535
- // Must have newline
1536
- if (this.#template.charCodeAt(i) !== NEWLINE) {
1537
- return false;
1538
- }
1539
-
1540
- const next_i = i + 1;
1541
- if (next_i >= this.#template.length) {
1542
- return true; // heading followed by newline + EOF
1543
- }
1544
-
1545
- if (this.#template.charCodeAt(next_i) === NEWLINE) {
1546
- return true; // heading followed by blank line
1547
- }
1548
-
1549
- return false; // heading followed by single newline + content
1557
+ // At newline or EOF both are valid
1558
+ return true;
1550
1559
  }
1551
1560
 
1552
1561
  /**
@@ -1616,20 +1625,14 @@ export class MdzParser {
1616
1625
 
1617
1626
  /**
1618
1627
  * Check if current position matches a code block.
1619
- * Code block must be 3+ backticks at column 0, followed by blank line or EOF.
1628
+ * Code block must be 3+ backticks at column 0, closing fence followed by newline or EOF.
1620
1629
  * Empty code blocks (no content) are treated as invalid.
1621
- *
1622
- * Blank line requirement rationale:
1623
- * Separates code blocks from following content to prevent ambiguity.
1624
- * Codeblocks are distinct semantic units that should be visually isolated.
1625
- * The blank line makes it explicit where the code block ends and regular
1626
- * content begins, following the "explicit over implicit" design principle.
1627
1630
  */
1628
1631
  #match_code_block(): boolean {
1629
1632
  let i = this.#index;
1630
1633
 
1631
- // Must start at column 0 (no leading whitespace)
1632
- if (i < this.#template.length && this.#template.charCodeAt(i) === SPACE) {
1634
+ // Must start at column 0 (beginning of input or after newline)
1635
+ if (i > 0 && this.#template.charCodeAt(i - 1) !== NEWLINE) {
1633
1636
  return false;
1634
1637
  }
1635
1638
 
@@ -1704,15 +1707,7 @@ export class MdzParser {
1704
1707
  return false;
1705
1708
  }
1706
1709
 
1707
- // Check if followed by blank line or EOF
1708
- const next_j = j + 1;
1709
- if (next_j >= this.#template.length) {
1710
- return true; // code block followed by newline + EOF
1711
- }
1712
- if (this.#template.charCodeAt(next_j) === NEWLINE) {
1713
- return true; // code block followed by blank line
1714
- }
1715
- return false; // code block followed by single newline + content
1710
+ return true; // code block followed by newline or EOF
1716
1711
  }
1717
1712
  }
1718
1713
  i++;
@@ -37,7 +37,7 @@ export class Module {
37
37
  /**
38
38
  * API documentation URL for this module.
39
39
  */
40
- url_api = $derived(`/docs/api/${this.path}`);
40
+ url_api = $derived(`/docs/api${this.library.url_prefix}/${this.path}`);
41
41
 
42
42
  /**
43
43
  * GitHub source URL.