@fuzdev/fuz_ui 0.198.1 → 0.200.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 (113) hide show
  1. package/dist/ApiDeclarationList.svelte +2 -1
  2. package/dist/ApiDeclarationList.svelte.d.ts.map +1 -1
  3. package/dist/ApiIndex.svelte +2 -2
  4. package/dist/ApiModule.svelte +3 -3
  5. package/dist/DeclarationDetail.svelte +145 -90
  6. package/dist/DeclarationDetail.svelte.d.ts.map +1 -1
  7. package/dist/LibraryDetail.svelte +14 -16
  8. package/dist/LibraryDetail.svelte.d.ts.map +1 -1
  9. package/dist/LibrarySummary.svelte +12 -12
  10. package/dist/Mdz.svelte +2 -2
  11. package/dist/MdzNodeView.svelte +8 -6
  12. package/dist/MdzNodeView.svelte.d.ts.map +1 -1
  13. package/dist/MdzRoot.svelte +30 -0
  14. package/dist/MdzRoot.svelte.d.ts +12 -0
  15. package/dist/MdzRoot.svelte.d.ts.map +1 -0
  16. package/dist/MdzStream.svelte +32 -0
  17. package/dist/MdzStream.svelte.d.ts +12 -0
  18. package/dist/MdzStream.svelte.d.ts.map +1 -0
  19. package/dist/MdzStreamNodeView.svelte +106 -0
  20. package/dist/MdzStreamNodeView.svelte.d.ts +9 -0
  21. package/dist/MdzStreamNodeView.svelte.d.ts.map +1 -0
  22. package/dist/ProjectLinks.svelte +1 -1
  23. package/dist/api_search.svelte.d.ts.map +1 -1
  24. package/dist/api_search.svelte.js +4 -2
  25. package/dist/declaration.svelte.d.ts +11 -0
  26. package/dist/declaration.svelte.d.ts.map +1 -1
  27. package/dist/declaration.svelte.js +15 -3
  28. package/dist/library.svelte.d.ts +9 -70
  29. package/dist/library.svelte.d.ts.map +1 -1
  30. package/dist/library.svelte.js +24 -18
  31. package/dist/library_helpers.d.ts +3 -3
  32. package/dist/library_helpers.js +3 -3
  33. package/dist/mdz.d.ts.map +1 -1
  34. package/dist/mdz.js +37 -29
  35. package/dist/mdz_components.d.ts +37 -23
  36. package/dist/mdz_components.d.ts.map +1 -1
  37. package/dist/mdz_components.js +16 -4
  38. package/dist/mdz_helpers.d.ts +46 -14
  39. package/dist/mdz_helpers.d.ts.map +1 -1
  40. package/dist/mdz_helpers.js +189 -56
  41. package/dist/mdz_lexer.d.ts.map +1 -1
  42. package/dist/mdz_lexer.js +18 -22
  43. package/dist/mdz_opcodes.d.ts +174 -0
  44. package/dist/mdz_opcodes.d.ts.map +1 -0
  45. package/dist/mdz_opcodes.js +14 -0
  46. package/dist/mdz_opcodes_to_nodes.d.ts +20 -0
  47. package/dist/mdz_opcodes_to_nodes.d.ts.map +1 -0
  48. package/dist/mdz_opcodes_to_nodes.js +332 -0
  49. package/dist/mdz_stream_parser.d.ts +80 -0
  50. package/dist/mdz_stream_parser.d.ts.map +1 -0
  51. package/dist/mdz_stream_parser.js +354 -0
  52. package/dist/mdz_stream_parser_block.d.ts +76 -0
  53. package/dist/mdz_stream_parser_block.d.ts.map +1 -0
  54. package/dist/mdz_stream_parser_block.js +458 -0
  55. package/dist/mdz_stream_parser_inline.d.ts +52 -0
  56. package/dist/mdz_stream_parser_inline.d.ts.map +1 -0
  57. package/dist/mdz_stream_parser_inline.js +378 -0
  58. package/dist/mdz_stream_parser_link.d.ts +22 -0
  59. package/dist/mdz_stream_parser_link.d.ts.map +1 -0
  60. package/dist/mdz_stream_parser_link.js +215 -0
  61. package/dist/mdz_stream_parser_state.d.ts +187 -0
  62. package/dist/mdz_stream_parser_state.d.ts.map +1 -0
  63. package/dist/mdz_stream_parser_state.js +398 -0
  64. package/dist/mdz_stream_parser_text.d.ts +14 -0
  65. package/dist/mdz_stream_parser_text.d.ts.map +1 -0
  66. package/dist/mdz_stream_parser_text.js +50 -0
  67. package/dist/mdz_stream_parser_url.d.ts +60 -0
  68. package/dist/mdz_stream_parser_url.d.ts.map +1 -0
  69. package/dist/mdz_stream_parser_url.js +307 -0
  70. package/dist/mdz_stream_state.svelte.d.ts +44 -0
  71. package/dist/mdz_stream_state.svelte.d.ts.map +1 -0
  72. package/dist/mdz_stream_state.svelte.js +357 -0
  73. package/dist/mdz_token_parser.d.ts.map +1 -1
  74. package/dist/mdz_token_parser.js +8 -39
  75. package/dist/module.svelte.d.ts +2 -0
  76. package/dist/module.svelte.d.ts.map +1 -1
  77. package/dist/module.svelte.js +3 -1
  78. package/dist/site.svelte.d.ts +13 -0
  79. package/dist/site.svelte.d.ts.map +1 -1
  80. package/dist/site.svelte.js +8 -2
  81. package/dist/tsdoc_mdz.d.ts +2 -2
  82. package/dist/tsdoc_mdz.js +2 -2
  83. package/dist/vite_plugin_pkg_json.d.ts +63 -0
  84. package/dist/vite_plugin_pkg_json.d.ts.map +1 -0
  85. package/dist/vite_plugin_pkg_json.js +126 -0
  86. package/package.json +8 -7
  87. package/src/lib/api_search.svelte.ts +4 -2
  88. package/src/lib/declaration.svelte.ts +18 -3
  89. package/src/lib/library.svelte.ts +37 -20
  90. package/src/lib/library_helpers.ts +3 -3
  91. package/src/lib/mdz.ts +38 -29
  92. package/src/lib/mdz_components.ts +40 -19
  93. package/src/lib/mdz_helpers.ts +199 -56
  94. package/src/lib/mdz_lexer.ts +18 -20
  95. package/src/lib/mdz_opcodes.ts +205 -0
  96. package/src/lib/mdz_opcodes_to_nodes.ts +375 -0
  97. package/src/lib/mdz_stream_parser.ts +415 -0
  98. package/src/lib/mdz_stream_parser_block.ts +532 -0
  99. package/src/lib/mdz_stream_parser_inline.ts +414 -0
  100. package/src/lib/mdz_stream_parser_link.ts +271 -0
  101. package/src/lib/mdz_stream_parser_state.ts +539 -0
  102. package/src/lib/mdz_stream_parser_text.ts +77 -0
  103. package/src/lib/mdz_stream_parser_url.ts +365 -0
  104. package/src/lib/mdz_stream_state.svelte.ts +387 -0
  105. package/src/lib/mdz_token_parser.ts +13 -40
  106. package/src/lib/module.svelte.ts +5 -1
  107. package/src/lib/site.svelte.ts +17 -2
  108. package/src/lib/tsdoc_mdz.ts +2 -2
  109. package/src/lib/vite_plugin_pkg_json.ts +142 -0
  110. package/dist/package_helpers.d.ts +0 -150
  111. package/dist/package_helpers.d.ts.map +0 -1
  112. package/dist/package_helpers.js +0 -179
  113. package/src/lib/package_helpers.ts +0 -186
@@ -0,0 +1,126 @@
1
+ /**
2
+ * Vite plugin serving a curated, publish-safe `package.json` as the virtual
3
+ * module `virtual:pkg.json`.
4
+ *
5
+ * Consumers' `src/routes/library.ts` (and root layouts) need package identity —
6
+ * name, version, the Fuz extension fields — in the client. Importing the root
7
+ * `package.json` directly inlines the *whole* file into the client bundle
8
+ * (`scripts`, `dependencies`, `devDependencies`, private config) and trips
9
+ * SvelteKit's `server.fs.allow` on cold HMR reloads. This plugin reads the
10
+ * project's `package.json` at build time, strips it to `pkg_json_keys`, and
11
+ * serves only that subset. Consumers combine it with `virtual:svelte-docinfo`'s
12
+ * analyzed `modules` via `library_json_from_modules` to build a `LibraryJson`
13
+ * (see `src/routes/library.ts` for the canonical pattern).
14
+ *
15
+ * The `.json` suffix on the virtual id is load-bearing, mirroring how
16
+ * `vite_plugin_fuz_css`'s `virtual:fuz.css` relies on `.css`: `load()` returns
17
+ * raw JSON text and Vite's built-in `vite:json` plugin transforms it into an ES
18
+ * module (default export plus named exports), so consumers write
19
+ * `import package_json from 'virtual:pkg.json'`. Do *not* return a JS module
20
+ * here — the `.json` id would double-transform it.
21
+ *
22
+ * ```ts
23
+ * // vite.config.ts
24
+ * import {vite_plugin_pkg_json} from './vite_plugin_pkg_json.js';
25
+ * export default defineConfig({plugins: [vite_plugin_pkg_json(), sveltekit()]});
26
+ * ```
27
+ *
28
+ * The kept field set defaults to `pkg_json_keys`. To expose extra publish-safe
29
+ * fields, pass a wider `keys` list — typically composed from the default with a
30
+ * spread (`` keys: [...pkg_json_keys, 'keywords'] ``). Because `library_json_from_modules`
31
+ * re-strips at runtime, the *same* list must reach that call (and the consumer's
32
+ * `virtual:pkg.json` ambient type) for the extras to survive end to end — share
33
+ * one const across all three sites:
34
+ *
35
+ * ```ts
36
+ * // src/routes/pkg_json_keys.ts
37
+ * import {pkg_json_keys} from '@fuzdev/fuz_util/pkg_json.js';
38
+ * export const pkg_json_keys_custom = [...pkg_json_keys, 'keywords'] as const;
39
+ *
40
+ * // vite.config.ts → vite_plugin_pkg_json({keys: pkg_json_keys_custom})
41
+ * // src/routes/library.ts → library_json_from_modules(pkg_json, modules, pkg_json_keys_custom)
42
+ * ```
43
+ *
44
+ * @module
45
+ */
46
+ import { readFileSync } from 'node:fs';
47
+ import { join } from 'node:path';
48
+ import { pkg_json_from_package_json, pkg_json_keys } from '@fuzdev/fuz_util/pkg_json.js';
49
+ const VIRTUAL_ID = 'virtual:pkg.json';
50
+ /**
51
+ * Resolved id, `\0`-prefixed — the conventional Rollup marker for "this id is
52
+ * mine," hiding the virtual module from other plugins and the dev module graph.
53
+ *
54
+ * This deliberately diverges from `vite_plugin_fuz_css`, which avoids the `\0`
55
+ * on its resolved id: SvelteKit's dev FOUC prevention inlines `virtual:fuz.css`
56
+ * into the SSR'd `<head>` via URL lookup and can't resolve the `\0`-encoded
57
+ * form, so the page would flash unstyled. JSON is never inlined into `<head>`,
58
+ * so that constraint doesn't apply here and the `\0` is free to do its normal
59
+ * job. The trailing `.json` survives the prefix, so Vite's built-in `vite:json`
60
+ * plugin still matches and transforms the loaded text (see the module comment).
61
+ */
62
+ const RESOLVED_VIRTUAL_ID = '\0virtual:pkg.json';
63
+ /**
64
+ * Creates the `virtual:pkg.json` plugin. Zero-config for canonical fuz usage —
65
+ * the publish-safe field set defaults to `pkg_json_keys`; widen it via `keys`.
66
+ */
67
+ export const vite_plugin_pkg_json = (options = {}) => {
68
+ const { keys = pkg_json_keys } = options;
69
+ let root = '';
70
+ let is_dev = false;
71
+ /** Curated JSON cached for build; left `null` in dev so `load` re-reads. */
72
+ let cached = null;
73
+ /**
74
+ * Reads `package.json`, strips it to the configured `keys` subset via
75
+ * `pkg_json_from_package_json` (the same strip the runtime uses, so the two
76
+ * can't drift), and returns the curated JSON text. Routes failures through the
77
+ * plugin context (`ctx.error`/`ctx.warn`) so a missing or malformed file
78
+ * surfaces as a named, actionable diagnostic rather than a raw
79
+ * `readFileSync`/`JSON.parse` stack trace.
80
+ */
81
+ const read_curated = (ctx) => {
82
+ const package_json_path = join(root, 'package.json');
83
+ ctx.addWatchFile(package_json_path); // re-emit when package.json changes
84
+ let raw;
85
+ try {
86
+ raw = JSON.parse(readFileSync(package_json_path, 'utf8'));
87
+ }
88
+ catch (err) {
89
+ // ctx.error throws, so this never returns
90
+ return ctx.error(`vite_plugin_pkg_json: failed to read or parse ${package_json_path}: ${err.message}`);
91
+ }
92
+ if (raw.name === undefined) {
93
+ ctx.warn(`vite_plugin_pkg_json: ${package_json_path} has no "name" field`);
94
+ }
95
+ return JSON.stringify(pkg_json_from_package_json(raw, keys));
96
+ };
97
+ return {
98
+ name: 'vite-plugin-pkg-json',
99
+ // Resolve the virtual id before other plugins claim it.
100
+ enforce: 'pre',
101
+ configResolved(config) {
102
+ root = config.root;
103
+ is_dev = config.command === 'serve';
104
+ },
105
+ buildStart() {
106
+ // Read package.json once up front so a missing or malformed file fails
107
+ // immediately — at build start, or at dev-server startup — rather than
108
+ // only when something first imports the virtual module. In build the
109
+ // result is cached for `load`; in dev `load` re-reads each time so edits
110
+ // to package.json propagate through the `addWatchFile`-driven reload.
111
+ const curated = read_curated(this);
112
+ if (!is_dev)
113
+ cached = curated;
114
+ },
115
+ resolveId(id) {
116
+ if (id === VIRTUAL_ID)
117
+ return RESOLVED_VIRTUAL_ID;
118
+ return undefined;
119
+ },
120
+ load(id) {
121
+ if (id !== RESOLVED_VIRTUAL_ID)
122
+ return undefined;
123
+ return cached ?? read_curated(this);
124
+ },
125
+ };
126
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fuzdev/fuz_ui",
3
- "version": "0.198.1",
3
+ "version": "0.200.0",
4
4
  "description": "Svelte UI library",
5
5
  "tagline": "friendly user zystem",
6
6
  "glyph": "🧶",
@@ -29,7 +29,8 @@
29
29
  "preview": "vite preview",
30
30
  "deploy": "gro deploy",
31
31
  "benchmark": "gro run src/benchmarks/mdz.benchmark.ts",
32
- "benchmark:save": "gro run src/benchmarks/mdz.benchmark.ts --save"
32
+ "benchmark:save": "gro run src/benchmarks/mdz.benchmark.ts --save",
33
+ "benchmark:clean": "rm -f src/benchmarks/baseline.json"
33
34
  },
34
35
  "type": "module",
35
36
  "engines": {
@@ -81,10 +82,10 @@
81
82
  "devDependencies": {
82
83
  "@changesets/changelog-git": "^0.2.1",
83
84
  "@fuzdev/blake3_wasm": "^0.1.1",
84
- "@fuzdev/fuz_code": "^0.45.1",
85
- "@fuzdev/fuz_css": "^0.61.1",
86
- "@fuzdev/fuz_util": "^0.63.0",
87
- "@fuzdev/gro": "^0.200.0",
85
+ "@fuzdev/fuz_code": "^0.46.0",
86
+ "@fuzdev/fuz_css": "^0.62.0",
87
+ "@fuzdev/fuz_util": "^0.65.1",
88
+ "@fuzdev/gro": "^0.204.0",
88
89
  "@jridgewell/trace-mapping": "^0.3.31",
89
90
  "@ryanatkn/eslint-config": "^0.12.1",
90
91
  "@sveltejs/acorn-typescript": "^1.0.9",
@@ -104,7 +105,7 @@
104
105
  "prettier-plugin-svelte": "^3.5.1",
105
106
  "svelte": "^5.56.0",
106
107
  "svelte-check": "^4.4.8",
107
- "svelte-docinfo": "^0.2.1",
108
+ "svelte-docinfo": "^0.4.1",
108
109
  "svelte2tsx": "^0.7.55",
109
110
  "tslib": "^2.8.1",
110
111
  "typescript": "^5.9.3",
@@ -42,7 +42,8 @@ export const create_api_search = (library: Library): ApiSearchState => {
42
42
  const all_declarations = $derived(library.declarations);
43
43
  const filtered_declarations = $derived.by(() => {
44
44
  const items = query.trim() ? library.search_declarations(query) : all_declarations;
45
- return items.sort((a, b) => a.name.localeCompare(b.name));
45
+ // spread before sort — `items` may be the shared source array
46
+ return [...items].sort((a, b) => a.name.localeCompare(b.name));
46
47
  });
47
48
 
48
49
  return {
@@ -83,7 +84,8 @@ export const create_module_declaration_search = (
83
84
 
84
85
  const filtered = $derived.by(() => {
85
86
  const trimmed_query = query.trim();
86
- if (!trimmed_query) return all.sort((a, b) => a.name.localeCompare(b.name));
87
+ // spread before sort — `all` is the shared source array
88
+ if (!trimmed_query) return [...all].sort((a, b) => a.name.localeCompare(b.name));
87
89
 
88
90
  const terms = trimmed_query.toLowerCase().split(/\s+/);
89
91
 
@@ -5,11 +5,12 @@ import type {
5
5
  ParameterJsonInput,
6
6
  ComponentPropJsonInput,
7
7
  OverloadJsonInput,
8
+ Reactivity,
8
9
  } from 'svelte-docinfo/types.js';
9
10
  import {generateImport, getDisplayName} from 'svelte-docinfo/declaration-helpers.js';
10
11
 
11
12
  import type {Module} from './module.svelte.js';
12
- import {url_github_file} from './package_helpers.js';
13
+ import {url_github_file} from '@fuzdev/fuz_util/package_helpers.js';
13
14
 
14
15
  // The `virtual:svelte-docinfo` module is serialized with svelte-docinfo's
15
16
  // `compactReplacer`, which strips empty default arrays — so the runtime data
@@ -74,7 +75,7 @@ export class Declaration {
74
75
  generateImport(
75
76
  this.declaration_json as DeclarationJson,
76
77
  this.module_path,
77
- this.library.package_json.name,
78
+ this.library.pkg_json.name,
78
79
  ),
79
80
  );
80
81
 
@@ -141,13 +142,27 @@ export class Declaration {
141
142
  */
142
143
  alias_of = $derived(this.declaration_json.aliasOf);
143
144
 
145
+ /**
146
+ * Other modules that also export this declaration (re-export paths
147
+ * relative to src/lib). Absent when only exported from its defining module.
148
+ */
149
+ also_exported_from = $derived(field<Array<string>>(this.declaration_json, 'alsoExportedFrom'));
150
+
144
151
  /**
145
152
  * Mutation documentation from `@mutates` tags, mapping parameter names to descriptions.
146
153
  */
147
154
  mutates = $derived(this.declaration_json.mutates);
148
155
 
156
+ /**
157
+ * Svelte reactivity flavor (`$state`, `$state.raw`, `$derived`, `$derived.by`)
158
+ * when this variable is initialized with a value-producing rune.
159
+ * Present on `variable` kind only.
160
+ */
161
+ reactivity = $derived(field<Reactivity>(this.declaration_json, 'reactivity'));
162
+
149
163
  has_examples = $derived(this.examples.length > 0);
150
- is_deprecated = $derived(!!this.deprecated_message);
164
+ // presence, not truthiness — a bare `@deprecated` (no message text) arrives as `''`
165
+ is_deprecated = $derived(this.deprecated_message !== undefined);
151
166
  has_documentation = $derived(!!this.doc_comment);
152
167
  has_parameters = $derived(!!(this.parameters && this.parameters.length > 0));
153
168
  has_props = $derived(!!(this.props && this.props.length > 0));
@@ -1,9 +1,19 @@
1
1
  import type {LibraryJson} from '@fuzdev/fuz_util/library_json.js';
2
2
  import {ensure_start, strip_end} from '@fuzdev/fuz_util/string.js';
3
+ import type {Url} from '@fuzdev/fuz_util/url.js';
3
4
 
4
5
  import {create_context} from './context_helpers.js';
5
6
  import {Declaration} from './declaration.svelte.js';
6
7
  import {Module} from './module.svelte.js';
8
+ import {
9
+ package_is_published,
10
+ repo_name_parse,
11
+ repo_url_github_owner,
12
+ repo_url_parse,
13
+ url_github_file,
14
+ url_logo,
15
+ url_npm_package,
16
+ } from '@fuzdev/fuz_util/package_helpers.js';
7
17
 
8
18
  export const library_context = create_context<Library>();
9
19
 
@@ -34,37 +44,36 @@ export class Library {
34
44
  */
35
45
  readonly url_prefix: string;
36
46
 
37
- readonly package_json = $derived(this.library_json.package_json);
47
+ readonly pkg_json = $derived(this.library_json.pkg_json);
38
48
  readonly source_json = $derived(this.library_json.source_json);
39
49
 
40
- readonly name = $derived(this.library_json.name);
41
- readonly repo_name = $derived(this.library_json.repo_name);
42
- readonly repo_url = $derived(this.library_json.repo_url);
43
- readonly owner_name = $derived(this.library_json.owner_name);
44
- readonly homepage_url = $derived(this.library_json.homepage_url);
45
- readonly logo_url = $derived(this.library_json.logo_url);
46
- readonly logo_alt = $derived(this.library_json.logo_alt);
47
- readonly npm_url = $derived(this.library_json.npm_url);
48
- readonly changelog_url = $derived(this.library_json.changelog_url);
49
- readonly published = $derived(this.library_json.published);
50
+ // Everything below is derived from `pkg_json` — `LibraryJson` stores only the
51
+ // raw `pkg_json`/`source_json` pair, so derivation lives in exactly one place.
52
+ readonly name = $derived(this.pkg_json.name);
53
+ readonly repo_name = $derived(repo_name_parse(this.pkg_json.name));
54
+ /** Non-null — the constructor rejects a `pkg_json` without a parseable `repository`. */
55
+ readonly repo_url: Url = $derived(repo_url_parse(this.pkg_json.repository)!);
56
+ readonly owner_name = $derived(repo_url_github_owner(this.repo_url));
57
+ readonly homepage_url: Url | null = $derived(this.pkg_json.homepage ?? null);
58
+ readonly logo_url: Url | null = $derived(url_logo(this.homepage_url, this.pkg_json.logo));
59
+ readonly logo_alt = $derived(this.pkg_json.logo_alt ?? `logo for ${this.repo_name}`);
60
+ readonly published = $derived(package_is_published(this.pkg_json));
61
+ readonly npm_url: Url | null = $derived(this.published ? url_npm_package(this.name) : null);
62
+ readonly changelog_url: Url | null = $derived(
63
+ this.published ? url_github_file(this.repo_url, 'CHANGELOG.md') : null,
64
+ );
50
65
 
51
66
  /**
52
- * Organization URL (e.g., 'https://github.com/ryanatkn').
67
+ * Organization URL (e.g., 'https://github.com/ryanatkn'), built from `owner_name`.
53
68
  */
54
- readonly org_url = $derived(
55
- this.repo_url && this.repo_name
56
- ? this.repo_url.endsWith('/' + this.repo_name)
57
- ? this.repo_url.slice(0, -this.repo_name.length - 1)
58
- : null
59
- : null,
60
- );
69
+ readonly org_url = $derived(this.owner_name ? 'https://github.com/' + this.owner_name : null);
61
70
 
62
71
  /**
63
72
  * All modules as rich `Module` instances.
64
73
  */
65
74
  readonly modules = $derived(
66
75
  this.source_json.modules
67
- ? this.source_json.modules.map((module_json) => new Module(this, module_json as any)) // TODO: remove cast when fuz_util SourceJson uses svelte-docinfo types
76
+ ? this.source_json.modules.map((module_json) => new Module(this, module_json))
68
77
  : [],
69
78
  );
70
79
 
@@ -91,6 +100,14 @@ export class Library {
91
100
  readonly declaration_by_name = $derived(new Map(this.declarations.map((d) => [d.name, d])));
92
101
 
93
102
  constructor(library_json: LibraryJson, url_prefix = '') {
103
+ // `repo_url` is exposed non-null and several derived URLs depend on it, so
104
+ // fail loud here rather than letting a missing/unparseable repository surface
105
+ // as a downstream `null`.
106
+ if (repo_url_parse(library_json.pkg_json.repository) === null) {
107
+ throw Error(
108
+ `failed to construct Library - pkg_json for "${library_json.pkg_json.name}" has no parseable \`repository\``,
109
+ );
110
+ }
94
111
  this.library_json = library_json;
95
112
  this.url_prefix = parse_library_url_prefix(url_prefix);
96
113
  }
@@ -4,7 +4,7 @@
4
4
  * Runtime UI helpers for building URLs in the library documentation system.
5
5
  * These depend on fuz_ui's documentation paths and SvelteKit's runtime state.
6
6
  *
7
- * For generic package/repository URL helpers, see `package_helpers.ts`.
7
+ * For generic package/repository URL helpers, see `@fuzdev/fuz_util/package_helpers.js`.
8
8
  *
9
9
  * @module
10
10
  */
@@ -23,8 +23,8 @@ import {page} from '$app/state';
23
23
  *
24
24
  * @example
25
25
  * ```ts
26
- * // Assuming page.url.origin is 'https://example.com'
27
- * url_to_root_relative('https://example.com/docs/api')
26
+ * // Assuming page.url.origin is 'https://fuz.dev'
27
+ * url_to_root_relative('https://fuz.dev/docs/api')
28
28
  * // => '/docs/api'
29
29
  * ```
30
30
  */
package/src/lib/mdz.ts CHANGED
@@ -51,8 +51,7 @@ import {
51
51
  HR_HYPHEN_COUNT,
52
52
  MIN_CODEBLOCK_BACKTICKS,
53
53
  MAX_HEADING_LEVEL,
54
- HTTPS_PREFIX_LENGTH,
55
- HTTP_PREFIX_LENGTH,
54
+ match_url_prefix_case_insensitive,
56
55
  is_letter,
57
56
  is_tag_name_char,
58
57
  is_word_char,
@@ -64,6 +63,8 @@ import {
64
63
  extract_single_tag,
65
64
  mdz_heading_id,
66
65
  mdz_is_url,
66
+ mdz_is_safe_reference,
67
+ mdz_push_merging_text,
67
68
  } from './mdz_helpers.js';
68
69
 
69
70
  // TODO design incremental parsing or some system that preserves Svelte components across re-renders when possible
@@ -689,6 +690,19 @@ export class MdzParser {
689
690
  }
690
691
  }
691
692
 
693
+ // Reject unsafe protocols (javascript:, data:, etc.) — fall back to text.
694
+ // `is_valid_path_char` permits `:` so a malicious `[x](javascript:...)` would
695
+ // otherwise pass character validation and reach the renderer.
696
+ if (!mdz_is_safe_reference(reference)) {
697
+ this.#index = start + 1;
698
+ return {
699
+ type: 'Text',
700
+ content: '[',
701
+ start,
702
+ end: this.#index,
703
+ };
704
+ }
705
+
692
706
  this.#index = close_paren + 1;
693
707
 
694
708
  // Determine link type (external vs internal)
@@ -907,27 +921,26 @@ export class MdzParser {
907
921
 
908
922
  /**
909
923
  * Check if current position is the start of an external URL (`https://` or `http://`).
924
+ *
925
+ * Requires the preceding character (if any) to not be a word character. This
926
+ * matches the streaming parser and the spec's "false negatives over false
927
+ * positives" policy — `xhttps://...` is treated as plain text rather than
928
+ * `x` followed by a link.
929
+ *
930
+ * Scheme matching is case-insensitive (RFC 3986 §3.1); the original casing
931
+ * is preserved in the emitted reference.
910
932
  */
911
933
  #is_at_url(): boolean {
912
- if (this.#match('https://')) {
913
- // Check for protocol-only (e.g., just "https://")
914
- // Must have at least one non-whitespace character after protocol
915
- if (this.#index + HTTPS_PREFIX_LENGTH >= this.#template.length) {
916
- return false;
917
- }
918
- const next_char = this.#template.charCodeAt(this.#index + HTTPS_PREFIX_LENGTH);
919
- return next_char !== SPACE && next_char !== NEWLINE;
920
- }
921
- if (this.#match('http://')) {
922
- // Check for protocol-only (e.g., just "http://")
923
- // Must have at least one non-whitespace character after protocol
924
- if (this.#index + HTTP_PREFIX_LENGTH >= this.#template.length) {
925
- return false;
926
- }
927
- const next_char = this.#template.charCodeAt(this.#index + HTTP_PREFIX_LENGTH);
928
- return next_char !== SPACE && next_char !== NEWLINE;
934
+ // word boundary check: skip when preceded by [A-Za-z0-9]
935
+ if (this.#index > 0 && is_word_char(this.#template.charCodeAt(this.#index - 1))) {
936
+ return false;
929
937
  }
930
- return false;
938
+ const prefix_len = match_url_prefix_case_insensitive(this.#template, this.#index);
939
+ if (prefix_len === 0) return false;
940
+ // Must have at least one non-whitespace character after protocol
941
+ if (this.#index + prefix_len >= this.#template.length) return false;
942
+ const next_char = this.#template.charCodeAt(this.#index + prefix_len);
943
+ return next_char !== SPACE && next_char !== NEWLINE;
931
944
  }
932
945
 
933
946
  /**
@@ -937,12 +950,8 @@ export class MdzParser {
937
950
  #parse_auto_link_url(): MdzLinkNode {
938
951
  const start = this.#index;
939
952
 
940
- // Consume protocol
941
- if (this.#match('https://')) {
942
- this.#index += HTTPS_PREFIX_LENGTH;
943
- } else if (this.#match('http://')) {
944
- this.#index += HTTP_PREFIX_LENGTH;
945
- }
953
+ // Consume protocol (case-insensitive match; original casing preserved in reference)
954
+ this.#index += match_url_prefix_case_insensitive(this.#template, this.#index);
946
955
 
947
956
  // Collect URL characters using RFC 3986 whitelist
948
957
  // Stop at whitespace or any character invalid in URIs
@@ -1065,7 +1074,7 @@ export class MdzParser {
1065
1074
 
1066
1075
  // Check for URL or internal absolute/relative path mid-text (char code guard avoids startsWith on every char)
1067
1076
  if (
1068
- (char_code === 104 /* h */ && this.#is_at_url()) ||
1077
+ ((char_code === 104 /* h */ || char_code === 72) /* H */ && this.#is_at_url()) ||
1069
1078
  (char_code === SLASH && is_at_absolute_path(this.#template, this.#index)) ||
1070
1079
  (char_code === PERIOD && is_at_relative_path(this.#template, this.#index))
1071
1080
  ) {
@@ -1129,8 +1138,8 @@ export class MdzParser {
1129
1138
  break;
1130
1139
  }
1131
1140
 
1132
- const node = this.#parse_node();
1133
- nodes.push(node);
1141
+ // merge adjacent Text nodes (e.g., text + failed delimiter + text)
1142
+ mdz_push_merging_text(nodes, this.#parse_node());
1134
1143
  }
1135
1144
 
1136
1145
  // Restore previous boundary
@@ -3,17 +3,35 @@ import type {Component} from 'svelte';
3
3
  import {create_context} from './context_helpers.js';
4
4
 
5
5
  /**
6
- * Context for providing custom mdz components.
7
- * Must be set by the application using mdz.
6
+ * Component registry for custom Svelte components that can be used in mdz content.
7
+ *
8
+ * For example, registering 'Alert' allows using `<Alert>...</Alert>` in mdz content.
9
+ *
10
+ * The Map values are the Svelte component constructors.
11
+ */
12
+ export type MdzComponents = Map<string, Component<any, any>>; // TODO support params
13
+
14
+ /**
15
+ * Element registry for HTML elements that can be used in mdz content.
16
+ *
17
+ * For example, registering 'div' allows using `<div>...</div>` in mdz content.
18
+ *
19
+ * The Map values are boolean placeholders for future configuration options.
8
20
  */
9
- export const mdz_components_context = create_context<MdzComponents>();
21
+ export type MdzElements = Map<string, boolean>;
10
22
 
11
23
  /**
12
- * Context for providing allowed HTML elements.
13
- * Must be set by the application using mdz.
24
+ * Context for providing custom mdz components via a getter function.
25
+ * Set to a getter (e.g., `() => components`) so changes are reflected reactively.
26
+ */
27
+ export const mdz_components_context = create_context<() => MdzComponents | undefined>();
28
+
29
+ /**
30
+ * Context for providing allowed HTML elements via a getter function.
31
+ * Set to a getter (e.g., `() => elements`) so changes are reflected reactively.
14
32
  * By default, no HTML elements are allowed.
15
33
  */
16
- export const mdz_elements_context = create_context<MdzElements>();
34
+ export const mdz_elements_context = create_context<() => MdzElements | undefined>();
17
35
 
18
36
  /**
19
37
  * Context for providing a base path getter for resolving relative links in mdz content.
@@ -24,20 +42,23 @@ export const mdz_elements_context = create_context<MdzElements>();
24
42
  */
25
43
  export const mdz_base_context = create_context<() => string | undefined>();
26
44
 
27
- /**
28
- * Component registry for custom Svelte components that can be used in mdz content.
29
- *
30
- * For example, registering 'Alert' allows using `<Alert>...</Alert>` in mdz content.
31
- *
32
- * The Map values are the Svelte component constructors.
33
- */
34
- export type MdzComponents = Map<string, Component<any, any>>; // TODO support params
45
+ interface MdzGetterContext<T> {
46
+ get_maybe(): (() => T | undefined) | undefined;
47
+ set(value: () => T | undefined): unknown;
48
+ }
35
49
 
36
50
  /**
37
- * Element registry for HTML elements that can be used in mdz content.
51
+ * Set an mdz context to a getter that prefers the local `value_fn` and falls
52
+ * back to the ancestor's value when `value_fn()` returns `undefined`.
38
53
  *
39
- * For example, registering 'div' allows using `<div>...</div>` in mdz content.
40
- *
41
- * The Map values are boolean placeholders for future configuration options.
54
+ * Pass `value_fn` as a getter (not a snapshot) so prop changes remain reactive.
55
+ * The ancestor lookup happens once at component init — that's intentional, it
56
+ * captures the surrounding scope's context at mount time.
42
57
  */
43
- export type MdzElements = Map<string, boolean>;
58
+ export const set_mdz_context_with_fallback = <T>(
59
+ context: MdzGetterContext<T>,
60
+ value_fn: () => T | undefined,
61
+ ): void => {
62
+ const ancestor = context.get_maybe();
63
+ context.set(() => value_fn() ?? ancestor?.());
64
+ };