@fuzdev/fuz_ui 0.198.1 → 0.199.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ApiIndex.svelte +2 -2
- package/dist/ApiModule.svelte +1 -1
- package/dist/LibraryDetail.svelte +14 -16
- package/dist/LibraryDetail.svelte.d.ts.map +1 -1
- package/dist/LibrarySummary.svelte +12 -12
- package/dist/Mdz.svelte +2 -2
- package/dist/MdzNodeView.svelte +8 -6
- package/dist/MdzNodeView.svelte.d.ts.map +1 -1
- package/dist/MdzRoot.svelte +30 -0
- package/dist/MdzRoot.svelte.d.ts +12 -0
- package/dist/MdzRoot.svelte.d.ts.map +1 -0
- package/dist/MdzStream.svelte +32 -0
- package/dist/MdzStream.svelte.d.ts +12 -0
- package/dist/MdzStream.svelte.d.ts.map +1 -0
- package/dist/MdzStreamNodeView.svelte +106 -0
- package/dist/MdzStreamNodeView.svelte.d.ts +9 -0
- package/dist/MdzStreamNodeView.svelte.d.ts.map +1 -0
- package/dist/declaration.svelte.js +2 -2
- package/dist/library.svelte.d.ts +9 -70
- package/dist/library.svelte.d.ts.map +1 -1
- package/dist/library.svelte.js +24 -18
- package/dist/library_helpers.d.ts +3 -3
- package/dist/library_helpers.js +3 -3
- package/dist/mdz.d.ts.map +1 -1
- package/dist/mdz.js +37 -29
- package/dist/mdz_components.d.ts +37 -23
- package/dist/mdz_components.d.ts.map +1 -1
- package/dist/mdz_components.js +16 -4
- package/dist/mdz_helpers.d.ts +46 -14
- package/dist/mdz_helpers.d.ts.map +1 -1
- package/dist/mdz_helpers.js +189 -56
- package/dist/mdz_lexer.d.ts.map +1 -1
- package/dist/mdz_lexer.js +18 -22
- package/dist/mdz_opcodes.d.ts +174 -0
- package/dist/mdz_opcodes.d.ts.map +1 -0
- package/dist/mdz_opcodes.js +14 -0
- package/dist/mdz_opcodes_to_nodes.d.ts +20 -0
- package/dist/mdz_opcodes_to_nodes.d.ts.map +1 -0
- package/dist/mdz_opcodes_to_nodes.js +332 -0
- package/dist/mdz_stream_parser.d.ts +80 -0
- package/dist/mdz_stream_parser.d.ts.map +1 -0
- package/dist/mdz_stream_parser.js +354 -0
- package/dist/mdz_stream_parser_block.d.ts +76 -0
- package/dist/mdz_stream_parser_block.d.ts.map +1 -0
- package/dist/mdz_stream_parser_block.js +458 -0
- package/dist/mdz_stream_parser_inline.d.ts +52 -0
- package/dist/mdz_stream_parser_inline.d.ts.map +1 -0
- package/dist/mdz_stream_parser_inline.js +378 -0
- package/dist/mdz_stream_parser_link.d.ts +22 -0
- package/dist/mdz_stream_parser_link.d.ts.map +1 -0
- package/dist/mdz_stream_parser_link.js +215 -0
- package/dist/mdz_stream_parser_state.d.ts +187 -0
- package/dist/mdz_stream_parser_state.d.ts.map +1 -0
- package/dist/mdz_stream_parser_state.js +398 -0
- package/dist/mdz_stream_parser_text.d.ts +14 -0
- package/dist/mdz_stream_parser_text.d.ts.map +1 -0
- package/dist/mdz_stream_parser_text.js +50 -0
- package/dist/mdz_stream_parser_url.d.ts +60 -0
- package/dist/mdz_stream_parser_url.d.ts.map +1 -0
- package/dist/mdz_stream_parser_url.js +307 -0
- package/dist/mdz_stream_state.svelte.d.ts +44 -0
- package/dist/mdz_stream_state.svelte.d.ts.map +1 -0
- package/dist/mdz_stream_state.svelte.js +357 -0
- package/dist/mdz_token_parser.d.ts.map +1 -1
- package/dist/mdz_token_parser.js +8 -39
- package/dist/module.svelte.js +1 -1
- package/dist/site.svelte.d.ts +13 -0
- package/dist/site.svelte.d.ts.map +1 -1
- package/dist/site.svelte.js +8 -2
- package/dist/tsdoc_mdz.d.ts +2 -2
- package/dist/tsdoc_mdz.js +2 -2
- package/dist/vite_plugin_pkg_json.d.ts +63 -0
- package/dist/vite_plugin_pkg_json.d.ts.map +1 -0
- package/dist/vite_plugin_pkg_json.js +126 -0
- package/package.json +6 -5
- package/src/lib/declaration.svelte.ts +2 -2
- package/src/lib/library.svelte.ts +37 -20
- package/src/lib/library_helpers.ts +3 -3
- package/src/lib/mdz.ts +38 -29
- package/src/lib/mdz_components.ts +40 -19
- package/src/lib/mdz_helpers.ts +199 -56
- package/src/lib/mdz_lexer.ts +18 -20
- package/src/lib/mdz_opcodes.ts +205 -0
- package/src/lib/mdz_opcodes_to_nodes.ts +375 -0
- package/src/lib/mdz_stream_parser.ts +415 -0
- package/src/lib/mdz_stream_parser_block.ts +532 -0
- package/src/lib/mdz_stream_parser_inline.ts +414 -0
- package/src/lib/mdz_stream_parser_link.ts +271 -0
- package/src/lib/mdz_stream_parser_state.ts +539 -0
- package/src/lib/mdz_stream_parser_text.ts +77 -0
- package/src/lib/mdz_stream_parser_url.ts +365 -0
- package/src/lib/mdz_stream_state.svelte.ts +387 -0
- package/src/lib/mdz_token_parser.ts +13 -40
- package/src/lib/module.svelte.ts +1 -1
- package/src/lib/site.svelte.ts +17 -2
- package/src/lib/tsdoc_mdz.ts +2 -2
- package/src/lib/vite_plugin_pkg_json.ts +142 -0
- package/dist/package_helpers.d.ts +0 -150
- package/dist/package_helpers.d.ts.map +0 -1
- package/dist/package_helpers.js +0 -179
- 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.
|
|
3
|
+
"version": "0.199.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": {
|
|
@@ -83,8 +84,8 @@
|
|
|
83
84
|
"@fuzdev/blake3_wasm": "^0.1.1",
|
|
84
85
|
"@fuzdev/fuz_code": "^0.45.1",
|
|
85
86
|
"@fuzdev/fuz_css": "^0.61.1",
|
|
86
|
-
"@fuzdev/fuz_util": "^0.
|
|
87
|
-
"@fuzdev/gro": "^0.
|
|
87
|
+
"@fuzdev/fuz_util": "^0.65.1",
|
|
88
|
+
"@fuzdev/gro": "^0.203.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.
|
|
108
|
+
"svelte-docinfo": "^0.4.0",
|
|
108
109
|
"svelte2tsx": "^0.7.55",
|
|
109
110
|
"tslib": "^2.8.1",
|
|
110
111
|
"typescript": "^5.9.3",
|
|
@@ -9,7 +9,7 @@ import type {
|
|
|
9
9
|
import {generateImport, getDisplayName} from 'svelte-docinfo/declaration-helpers.js';
|
|
10
10
|
|
|
11
11
|
import type {Module} from './module.svelte.js';
|
|
12
|
-
import {url_github_file} from '
|
|
12
|
+
import {url_github_file} from '@fuzdev/fuz_util/package_helpers.js';
|
|
13
13
|
|
|
14
14
|
// The `virtual:svelte-docinfo` module is serialized with svelte-docinfo's
|
|
15
15
|
// `compactReplacer`, which strips empty default arrays — so the runtime data
|
|
@@ -74,7 +74,7 @@ export class Declaration {
|
|
|
74
74
|
generateImport(
|
|
75
75
|
this.declaration_json as DeclarationJson,
|
|
76
76
|
this.module_path,
|
|
77
|
-
this.library.
|
|
77
|
+
this.library.pkg_json.name,
|
|
78
78
|
),
|
|
79
79
|
);
|
|
80
80
|
|
|
@@ -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
|
|
47
|
+
readonly pkg_json = $derived(this.library_json.pkg_json);
|
|
38
48
|
readonly source_json = $derived(this.library_json.source_json);
|
|
39
49
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
readonly
|
|
43
|
-
readonly
|
|
44
|
-
|
|
45
|
-
readonly
|
|
46
|
-
readonly
|
|
47
|
-
readonly
|
|
48
|
-
readonly
|
|
49
|
-
readonly
|
|
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
|
|
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
|
|
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://
|
|
27
|
-
* url_to_root_relative('https://
|
|
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
|
-
|
|
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
|
-
|
|
913
|
-
|
|
914
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1133
|
-
nodes
|
|
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
|
-
*
|
|
7
|
-
*
|
|
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
|
|
21
|
+
export type MdzElements = Map<string, boolean>;
|
|
10
22
|
|
|
11
23
|
/**
|
|
12
|
-
* Context for providing
|
|
13
|
-
*
|
|
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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
*
|
|
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
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
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
|
|
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
|
+
};
|