@fuzdev/fuz_ui 0.181.1 → 0.182.1

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.
@@ -0,0 +1,127 @@
1
+ /**
2
+ * Converts parsed `MdzNode` arrays to Svelte markup strings.
3
+ *
4
+ * Used by the `svelte_preprocess_mdz` preprocessor to expand static `<Mdz content="...">` usages
5
+ * into pre-rendered Svelte markup at build time. The output for each node type matches what
6
+ * `MdzNodeView.svelte` renders at runtime, so precompiled and runtime rendering are identical.
7
+ *
8
+ * @module
9
+ */
10
+
11
+ import {UnreachableError} from '@fuzdev/fuz_util/error.js';
12
+ import {escape_svelte_text} from '@fuzdev/fuz_util/svelte_preprocess_helpers.js';
13
+ import {escape_js_string} from '@fuzdev/fuz_util/string.js';
14
+
15
+ import type {MdzNode} from './mdz.js';
16
+
17
+ /**
18
+ * Result of converting `MdzNode` arrays to Svelte markup.
19
+ */
20
+ export interface MdzToSvelteResult {
21
+ /** Generated Svelte markup string. */
22
+ markup: string;
23
+
24
+ /** Required imports: `Map<local_name, {path, kind}>`. */
25
+ imports: Map<string, {path: string; kind: 'default' | 'named'}>;
26
+
27
+ /** Whether content references unconfigured Component or Element tags. */
28
+ has_unconfigured_tags: boolean;
29
+ }
30
+
31
+ /**
32
+ * Converts an array of `MdzNode` to a Svelte markup string.
33
+ *
34
+ * Each node type produces output matching what `MdzNodeView.svelte` renders at runtime.
35
+ * Collects required imports and flags unconfigured component/element references.
36
+ *
37
+ * @param nodes Parsed mdz nodes to render.
38
+ * @param components Component name to import path mapping (e.g., `{Alert: '$lib/Alert.svelte'}`).
39
+ * If content references a component not in this map, `has_unconfigured_tags` is set.
40
+ * @param elements Allowed HTML element names (e.g., `new Set(['aside', 'details'])`).
41
+ * If content references an element not in this set, `has_unconfigured_tags` is set.
42
+ */
43
+ export const mdz_to_svelte = (
44
+ nodes: Array<MdzNode>,
45
+ components: Record<string, string>,
46
+ elements: ReadonlySet<string>,
47
+ ): MdzToSvelteResult => {
48
+ const imports: Map<string, {path: string; kind: 'default' | 'named'}> = new Map();
49
+ let has_unconfigured_tags = false;
50
+
51
+ const render_nodes = (children: Array<MdzNode>): string => {
52
+ return children.map((child) => render_node(child)).join('');
53
+ };
54
+
55
+ const render_node = (node: MdzNode): string => {
56
+ switch (node.type) {
57
+ case 'Text':
58
+ return escape_svelte_text(node.content);
59
+
60
+ case 'Code':
61
+ imports.set('DocsLink', {path: '@fuzdev/fuz_ui/DocsLink.svelte', kind: 'default'});
62
+ return `<DocsLink reference={'${escape_js_string(node.content)}'} />`;
63
+
64
+ case 'Codeblock': {
65
+ imports.set('Code', {path: '@fuzdev/fuz_code/Code.svelte', kind: 'default'});
66
+ const lang_attr =
67
+ node.lang === null ? 'lang={null}' : `lang={'${escape_js_string(node.lang)}'}`;
68
+ return `<Code ${lang_attr} content={'${escape_js_string(node.content)}'} />`;
69
+ }
70
+
71
+ case 'Bold':
72
+ return `<strong>${render_nodes(node.children)}</strong>`;
73
+
74
+ case 'Italic':
75
+ return `<em>${render_nodes(node.children)}</em>`;
76
+
77
+ case 'Strikethrough':
78
+ return `<s>${render_nodes(node.children)}</s>`;
79
+
80
+ case 'Link': {
81
+ const children_markup = render_nodes(node.children);
82
+ if (node.link_type === 'internal') {
83
+ if (node.reference.startsWith('#') || node.reference.startsWith('?')) {
84
+ return `<a href={'${escape_js_string(node.reference)}'}>${children_markup}</a>`;
85
+ }
86
+ imports.set('resolve', {path: '$app/paths', kind: 'named'});
87
+ return `<a href={resolve('${escape_js_string(node.reference)}')}>${children_markup}</a>`;
88
+ }
89
+ // External link — matches MdzNodeView: target="_blank" rel="noopener"
90
+ return `<a href={'${escape_js_string(node.reference)}'} target="_blank" rel="noopener">${children_markup}</a>`;
91
+ }
92
+
93
+ case 'Paragraph':
94
+ return `<p>${render_nodes(node.children)}</p>`;
95
+
96
+ case 'Hr':
97
+ return '<hr />';
98
+
99
+ case 'Heading':
100
+ return `<h${node.level}>${render_nodes(node.children)}</h${node.level}>`;
101
+
102
+ case 'Element': {
103
+ if (!elements.has(node.name)) {
104
+ has_unconfigured_tags = true;
105
+ return '';
106
+ }
107
+ return `<${node.name}>${render_nodes(node.children)}</${node.name}>`;
108
+ }
109
+
110
+ case 'Component': {
111
+ const import_path = components[node.name];
112
+ if (!import_path) {
113
+ has_unconfigured_tags = true;
114
+ return '';
115
+ }
116
+ imports.set(node.name, {path: import_path, kind: 'default'});
117
+ return `<${node.name}>${render_nodes(node.children)}</${node.name}>`;
118
+ }
119
+
120
+ default:
121
+ throw new UnreachableError(node);
122
+ }
123
+ };
124
+
125
+ const markup = render_nodes(nodes);
126
+ return {markup, imports, has_unconfigured_tags};
127
+ };
@@ -52,10 +52,12 @@ export interface SourceFileInfo {
52
52
  * handles nested directories without special heuristics.
53
53
  *
54
54
  * @example
55
+ * ```ts
55
56
  * const options = module_create_source_options(process.cwd(), {
56
57
  * source_paths: ['src/lib', 'src/routes'],
57
58
  * source_root: 'src',
58
59
  * });
60
+ * ```
59
61
  */
60
62
  export interface ModuleSourceOptions {
61
63
  /**
@@ -64,7 +66,10 @@ export interface ModuleSourceOptions {
64
66
  * All `source_paths` are relative to this. Typically `process.cwd()` when
65
67
  * running from the project root via Gro, Vite, or other build tools.
66
68
  *
67
- * @example '/home/user/my-project'
69
+ * @example
70
+ * ```ts
71
+ * '/home/user/my-project'
72
+ * ```
68
73
  */
69
74
  project_root: string;
70
75
  /**
@@ -73,8 +78,14 @@ export interface ModuleSourceOptions {
73
78
  * Paths should not have leading or trailing slashes - they are added
74
79
  * internally for correct matching.
75
80
  *
76
- * @example ['src/lib'] - single source directory
77
- * @example ['src/lib', 'src/routes'] - multiple directories
81
+ * @example
82
+ * ```ts
83
+ * ['src/lib'] // single source directory
84
+ * ```
85
+ * @example
86
+ * ```ts
87
+ * ['src/lib', 'src/routes'] // multiple directories
88
+ * ```
78
89
  */
79
90
  source_paths: Array<string>;
80
91
  /**
@@ -84,8 +95,14 @@ export interface ModuleSourceOptions {
84
95
  * - Single `source_path`: defaults to that path
85
96
  * - Multiple `source_paths`: required (no auto-derivation)
86
97
  *
87
- * @example 'src/lib' - module paths like 'foo.ts', 'utils/bar.ts'
88
- * @example 'src' - module paths like 'lib/foo.ts', 'routes/page.svelte'
98
+ * @example
99
+ * ```ts
100
+ * 'src/lib' // module paths like 'foo.ts', 'utils/bar.ts'
101
+ * ```
102
+ * @example
103
+ * ```ts
104
+ * 'src' // module paths like 'lib/foo.ts', 'routes/page.svelte'
105
+ * ```
89
106
  */
90
107
  source_root?: string;
91
108
  /** Patterns to exclude (matched against full path). */
@@ -100,20 +117,24 @@ export interface ModuleSourceOptions {
100
117
  * @default Uses file extension: `.svelte` → svelte, `.ts`/`.js` → typescript
101
118
  *
102
119
  * @example
120
+ * ```ts
103
121
  * // Add MDsveX support
104
122
  * get_analyzer: (path) => {
105
123
  * if (path.endsWith('.svelte') || path.endsWith('.svx')) return 'svelte';
106
124
  * if (path.endsWith('.ts') || path.endsWith('.js')) return 'typescript';
107
125
  * return null;
108
126
  * }
127
+ * ```
109
128
  *
110
129
  * @example
130
+ * ```ts
111
131
  * // Include .d.ts files
112
132
  * get_analyzer: (path) => {
113
133
  * if (path.endsWith('.svelte')) return 'svelte';
114
134
  * if (path.endsWith('.ts') || path.endsWith('.d.ts') || path.endsWith('.js')) return 'typescript';
115
135
  * return null;
116
136
  * }
137
+ * ```
117
138
  */
118
139
  get_analyzer: (path: string) => AnalyzerType | null;
119
140
  }
@@ -157,21 +178,27 @@ export const MODULE_SOURCE_PARTIAL: ModuleSourcePartial = {
157
178
  * @param overrides Optional overrides for default options
158
179
  *
159
180
  * @example
181
+ * ```ts
160
182
  * // Standard SvelteKit library
161
183
  * const options = module_create_source_options(process.cwd());
184
+ * ```
162
185
  *
163
186
  * @example
187
+ * ```ts
164
188
  * // Multiple source directories
165
189
  * const options = module_create_source_options(process.cwd(), {
166
190
  * source_paths: ['src/lib', 'src/routes'],
167
191
  * source_root: 'src',
168
192
  * });
193
+ * ```
169
194
  *
170
195
  * @example
196
+ * ```ts
171
197
  * // Custom exclusions
172
198
  * const options = module_create_source_options(process.cwd(), {
173
199
  * exclude_patterns: [/\.test\.ts$/, /\.internal\.ts$/],
174
200
  * });
201
+ * ```
175
202
  */
176
203
  export const module_create_source_options = (
177
204
  project_root: string,
@@ -195,14 +222,17 @@ export const module_create_source_options = (
195
222
  * @throws Error if validation fails
196
223
  *
197
224
  * @example
225
+ * ```ts
198
226
  * // Valid - single source path (source_root auto-derived)
199
227
  * module_validate_source_options({
200
228
  * project_root: '/home/user/project',
201
229
  * source_paths: ['src/lib'],
202
230
  * ...
203
231
  * });
232
+ * ```
204
233
  *
205
234
  * @example
235
+ * ```ts
206
236
  * // Valid - multiple source paths with explicit source_root
207
237
  * module_validate_source_options({
208
238
  * project_root: '/home/user/project',
@@ -210,14 +240,17 @@ export const module_create_source_options = (
210
240
  * source_root: 'src',
211
241
  * ...
212
242
  * });
243
+ * ```
213
244
  *
214
245
  * @example
246
+ * ```ts
215
247
  * // Invalid - multiple source paths without source_root
216
248
  * module_validate_source_options({
217
249
  * project_root: '/home/user/project',
218
250
  * source_paths: ['src/lib', 'src/routes'], // throws
219
251
  * ...
220
252
  * });
253
+ * ```
221
254
  */
222
255
  export const module_validate_source_options = (options: ModuleSourceOptions): void => {
223
256
  const {project_root, source_paths, source_root} = options;
@@ -319,17 +352,21 @@ export const module_get_source_root = (options: ModuleSourceOptions): string =>
319
352
  * @param options Module source options for path extraction
320
353
  *
321
354
  * @example
355
+ * ```ts
322
356
  * const options = module_create_source_options('/home/user/project');
323
357
  * module_extract_path('/home/user/project/src/lib/foo.ts', options) // => 'foo.ts'
324
358
  * module_extract_path('/home/user/project/src/lib/nested/bar.svelte', options) // => 'nested/bar.svelte'
359
+ * ```
325
360
  *
326
361
  * @example
362
+ * ```ts
327
363
  * const options = module_create_source_options('/home/user/project', {
328
364
  * source_paths: ['src/lib', 'src/routes'],
329
365
  * source_root: 'src',
330
366
  * });
331
367
  * module_extract_path('/home/user/project/src/lib/foo.ts', options) // => 'lib/foo.ts'
332
368
  * module_extract_path('/home/user/project/src/routes/page.svelte', options) // => 'routes/page.svelte'
369
+ * ```
333
370
  */
334
371
  export const module_extract_path = (source_id: string, options: ModuleSourceOptions): string => {
335
372
  const effective_root = module_get_source_root(options);
@@ -347,8 +384,10 @@ export const module_extract_path = (source_id: string, options: ModuleSourceOpti
347
384
  * Extract component name from a Svelte module path.
348
385
  *
349
386
  * @example
387
+ * ```ts
350
388
  * module_get_component_name('Alert.svelte') // => 'Alert'
351
389
  * module_get_component_name('components/Button.svelte') // => 'Button'
390
+ * ```
352
391
  */
353
392
  export const module_get_component_name = (module_path: string): string =>
354
393
  module_path.replace(/^.*\//, '').replace(/\.svelte$/, '');
@@ -357,7 +396,9 @@ export const module_get_component_name = (module_path: string): string =>
357
396
  * Convert module path to module key format (with ./ prefix).
358
397
  *
359
398
  * @example
399
+ * ```ts
360
400
  * module_get_key('foo.ts') // => './foo.ts'
401
+ * ```
361
402
  */
362
403
  export const module_get_key = (module_path: string): string => `./${module_path}`;
363
404
 
@@ -394,10 +435,12 @@ export const module_is_test = (path: string): boolean => path.endsWith('.test.ts
394
435
  * @returns True if the path is an analyzable source file
395
436
  *
396
437
  * @example
438
+ * ```ts
397
439
  * const options = module_create_source_options('/home/user/project');
398
440
  * module_is_source('/home/user/project/src/lib/foo.ts', options) // => true
399
441
  * module_is_source('/home/user/project/src/lib/foo.test.ts', options) // => false (excluded)
400
442
  * module_is_source('/home/user/project/src/fixtures/mini/src/lib/bar.ts', options) // => false (wrong prefix)
443
+ * ```
401
444
  */
402
445
  export const module_is_source = (path: string, options: ModuleSourceOptions): boolean => {
403
446
  // Check exclusion patterns first (fast regex check)
@@ -33,12 +33,16 @@ import type {PackageJson} from '@fuzdev/fuz_util/package_json.js';
33
33
  * @returns Full GitHub URL to the file on the main branch
34
34
  *
35
35
  * @example
36
+ * ```ts
36
37
  * url_github_file('https://github.com/foo/bar', 'src/index.ts')
37
38
  * // => 'https://github.com/foo/bar/blob/main/src/index.ts'
39
+ * ```
38
40
  *
39
41
  * @example
42
+ * ```ts
40
43
  * url_github_file('https://github.com/foo/bar', './src/index.ts', 42)
41
44
  * // => 'https://github.com/foo/bar/blob/main/src/index.ts#L42'
45
+ * ```
42
46
  */
43
47
  export const url_github_file = (repo_url: string, file_path: string, line?: number): string => {
44
48
  const clean_path = file_path.replace(/^\.\//, '');
@@ -54,8 +58,10 @@ export const url_github_file = (repo_url: string, file_path: string, line?: numb
54
58
  * @returns Organization URL, or null if repo_url doesn't end with repo_name
55
59
  *
56
60
  * @example
61
+ * ```ts
57
62
  * url_github_org('https://github.com/fuzdev/fuz_ui', 'fuz_ui')
58
63
  * // => 'https://github.com/fuzdev'
64
+ * ```
59
65
  */
60
66
  export const url_github_org = (repo_url: string, repo_name: string): string | null => {
61
67
  return repo_url.endsWith('/' + repo_name) ? strip_end(repo_url, '/' + repo_name) : null;
@@ -68,12 +74,16 @@ export const url_github_org = (repo_url: string, repo_name: string): string | nu
68
74
  * @returns Owner name, or null if not a valid GitHub URL
69
75
  *
70
76
  * @example
77
+ * ```ts
71
78
  * repo_url_github_owner('https://github.com/fuzdev/fuz_ui')
72
79
  * // => 'fuzdev'
80
+ * ```
73
81
  *
74
82
  * @example
83
+ * ```ts
75
84
  * repo_url_github_owner('https://gitlab.com/foo/bar')
76
85
  * // => null (not a GitHub URL)
86
+ * ```
77
87
  */
78
88
  export const repo_url_github_owner = (repo_url: string): string | null => {
79
89
  const stripped = strip_start(repo_url, 'https://github.com/');
@@ -89,8 +99,10 @@ export const repo_url_github_owner = (repo_url: string): string | null => {
89
99
  * @returns Full npm package page URL
90
100
  *
91
101
  * @example
102
+ * ```ts
92
103
  * url_npm_package('@fuzdev/fuz_ui')
93
104
  * // => 'https://www.npmjs.com/package/@fuzdev/fuz_ui'
105
+ * ```
94
106
  */
95
107
  export const url_npm_package = (package_name: string): string =>
96
108
  'https://www.npmjs.com/package/' + package_name;
@@ -118,12 +130,16 @@ export const package_is_published = (package_json: PackageJson): boolean => {
118
130
  * @throws Error if scoped package name is malformed
119
131
  *
120
132
  * @example
133
+ * ```ts
121
134
  * repo_name_parse('@fuzdev/fuz_ui')
122
135
  * // => 'fuz_ui'
136
+ * ```
123
137
  *
124
138
  * @example
139
+ * ```ts
125
140
  * repo_name_parse('lodash')
126
141
  * // => 'lodash'
142
+ * ```
127
143
  */
128
144
  export const repo_name_parse = (name: string): string => {
129
145
  if (name[0] === '@') {
@@ -146,16 +162,22 @@ export const repo_name_parse = (name: string): string => {
146
162
  * @returns Clean repository URL, or null if not provided
147
163
  *
148
164
  * @example
165
+ * ```ts
149
166
  * repo_url_parse('https://github.com/foo/bar')
150
167
  * // => 'https://github.com/foo/bar'
168
+ * ```
151
169
  *
152
170
  * @example
171
+ * ```ts
153
172
  * repo_url_parse({url: 'git+https://github.com/foo/bar.git'})
154
173
  * // => 'https://github.com/foo/bar'
174
+ * ```
155
175
  *
156
176
  * @example
177
+ * ```ts
157
178
  * repo_url_parse(undefined)
158
179
  * // => null
180
+ * ```
159
181
  */
160
182
  export const repo_url_parse = (repository: PackageJson['repository']): string | null => {
161
183
  if (!repository) return null;
@@ -172,8 +194,10 @@ export const repo_url_parse = (repository: PackageJson['repository']): string |
172
194
  * @returns Full URL to the .well-known file
173
195
  *
174
196
  * @example
197
+ * ```ts
175
198
  * url_well_known('https://fuz.dev', 'package.json')
176
199
  * // => 'https://fuz.dev/.well-known/package.json'
200
+ * ```
177
201
  */
178
202
  export const url_well_known = (homepage_url: string, filename: string): string => {
179
203
  return `${ensure_end(homepage_url, '/')}.well-known/${filename}`;