@fuzdev/fuz_code 0.41.1 → 0.43.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/README.md CHANGED
@@ -175,7 +175,7 @@ Experimental modules:
175
175
 
176
176
  ## License [🐦](https://wikipedia.org/wiki/Free_and_open-source_software)
177
177
 
178
- based on [Prism](https://github.com/PrismJS/prism)
178
+ based on [Prism](https://github.com/PrismJS/prism) ([prismjs.com](https://prismjs.com/))
179
179
  by [Lea Verou](https://lea.verou.me/)
180
180
 
181
181
  the [Svelte grammar](src/lib/grammar_svelte.ts)
package/dist/Code.svelte CHANGED
@@ -27,7 +27,7 @@
27
27
  | {
28
28
  content?: undefined;
29
29
  /**
30
- * Pre-highlighted HTML from the `svelte_preprocess_code_static` preprocessor.
30
+ * Pre-highlighted HTML from the `svelte_preprocess_fuz_code` preprocessor.
31
31
  * When provided, skips runtime syntax highlighting entirely.
32
32
  *
33
33
  * Named `dangerous_raw_html` to signal that it bypasses sanitization,
@@ -8,7 +8,7 @@ type $$ComponentProps = SvelteHTMLElements['code'] & ({
8
8
  } | {
9
9
  content?: undefined;
10
10
  /**
11
- * Pre-highlighted HTML from the `svelte_preprocess_code_static` preprocessor.
11
+ * Pre-highlighted HTML from the `svelte_preprocess_fuz_code` preprocessor.
12
12
  * When provided, skips runtime syntax highlighting entirely.
13
13
  *
14
14
  * Named `dangerous_raw_html` to signal that it bypasses sanitization,
@@ -1,6 +1,6 @@
1
1
  import { type PreprocessorGroup } from 'svelte/compiler';
2
2
  import type { SyntaxStyler } from './syntax_styler.js';
3
- export interface PreprocessCodeStaticOptions {
3
+ export interface PreprocessFuzCodeOptions {
4
4
  /** File patterns to exclude. */
5
5
  exclude?: Array<string | RegExp>;
6
6
  /** Custom syntax styler. @default syntax_styler_global */
@@ -20,5 +20,5 @@ export interface PreprocessCodeStaticOptions {
20
20
  */
21
21
  on_error?: 'log' | 'throw';
22
22
  }
23
- export declare const svelte_preprocess_code_static: (options?: PreprocessCodeStaticOptions) => PreprocessorGroup;
24
- //# sourceMappingURL=svelte_preprocess_code_static.d.ts.map
23
+ export declare const svelte_preprocess_fuz_code: (options?: PreprocessFuzCodeOptions) => PreprocessorGroup;
24
+ //# sourceMappingURL=svelte_preprocess_fuz_code.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"svelte_preprocess_fuz_code.d.ts","sourceRoot":"../src/lib/","sources":["../src/lib/svelte_preprocess_fuz_code.ts"],"names":[],"mappings":"AAAA,OAAO,EAAQ,KAAK,iBAAiB,EAAW,MAAM,iBAAiB,CAAC;AAcxE,OAAO,KAAK,EAAC,YAAY,EAAC,MAAM,oBAAoB,CAAC;AAErD,MAAM,WAAW,wBAAwB;IACxC,gCAAgC;IAChC,OAAO,CAAC,EAAE,KAAK,CAAC,MAAM,GAAG,MAAM,CAAC,CAAC;IAEjC,0DAA0D;IAC1D,aAAa,CAAC,EAAE,YAAY,CAAC;IAE7B,8CAA8C;IAC9C,KAAK,CAAC,EAAE,OAAO,CAAC;IAEhB;;;;;OAKG;IACH,iBAAiB,CAAC,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;IAElC;;;OAGG;IACH,QAAQ,CAAC,EAAE,KAAK,GAAG,OAAO,CAAC;CAC3B;AAED,eAAO,MAAM,0BAA0B,GACtC,UAAS,wBAA6B,KACpC,iBA0DF,CAAC"}
@@ -1,16 +1,19 @@
1
1
  import { parse } from 'svelte/compiler';
2
2
  import MagicString from 'magic-string';
3
3
  import { walk } from 'zimmerframe';
4
+ import { should_exclude_path } from '@fuzdev/fuz_util/path.js';
5
+ import { escape_js_string } from '@fuzdev/fuz_util/string.js';
6
+ import { find_attribute, evaluate_static_expr, extract_static_string, resolve_component_names, } from '@fuzdev/fuz_util/svelte_preprocess_helpers.js';
4
7
  import { syntax_styler_global } from './syntax_styler_global.js';
5
- export const svelte_preprocess_code_static = (options = {}) => {
8
+ export const svelte_preprocess_fuz_code = (options = {}) => {
6
9
  const { exclude = [], syntax_styler = syntax_styler_global, cache = true, component_imports = ['@fuzdev/fuz_code/Code.svelte'], on_error = process.env.CI ? 'throw' : 'log', } = options;
7
10
  // In-memory cache: content+lang hash → highlighted HTML
8
11
  const highlight_cache = new Map();
9
12
  return {
10
- name: 'code-static',
13
+ name: 'fuz-code',
11
14
  markup: ({ content, filename }) => {
12
15
  // Skip excluded files
13
- if (should_exclude(filename, exclude)) {
16
+ if (should_exclude_path(filename, exclude)) {
14
17
  return { code: content };
15
18
  }
16
19
  // Quick check: does file import from a known Code component source?
@@ -20,7 +23,7 @@ export const svelte_preprocess_code_static = (options = {}) => {
20
23
  const s = new MagicString(content);
21
24
  const ast = parse(content, { filename, modern: true });
22
25
  // Resolve which local names map to the Code component
23
- const code_names = resolve_code_names(ast, component_imports);
26
+ const code_names = resolve_component_names(ast, component_imports);
24
27
  if (code_names.size === 0) {
25
28
  return { code: content };
26
29
  }
@@ -45,39 +48,6 @@ export const svelte_preprocess_code_static = (options = {}) => {
45
48
  },
46
49
  };
47
50
  };
48
- /**
49
- * Check if a filename matches any exclusion pattern.
50
- */
51
- const should_exclude = (filename, exclude) => {
52
- if (!filename || exclude.length === 0)
53
- return false;
54
- return exclude.some((pattern) => typeof pattern === 'string' ? filename.includes(pattern) : pattern.test(filename));
55
- };
56
- /**
57
- * Scans import declarations to find local names that import from known Code component sources.
58
- * Handles default imports, named imports, and aliased imports.
59
- * Checks both instance (`<script>`) and module (`<script module>`) scripts.
60
- */
61
- const resolve_code_names = (ast, component_imports) => {
62
- const names = new Set();
63
- for (const script of [ast.instance, ast.module]) {
64
- if (!script)
65
- continue;
66
- for (const node of script.content.body) {
67
- if (node.type !== 'ImportDeclaration')
68
- continue;
69
- if (!component_imports.includes(node.source.value))
70
- continue;
71
- for (const specifier of node.specifiers) {
72
- // default import: `import Code from '...'`
73
- // aliased: `import Highlighter from '...'`
74
- // named: `import { default as Code } from '...'`
75
- names.add(specifier.local.name);
76
- }
77
- }
78
- }
79
- return names;
80
- };
81
51
  /**
82
52
  * Attempt to highlight content, using cache if available.
83
53
  * Returns the highlighted HTML, or `null` on error.
@@ -111,6 +81,9 @@ const find_code_usages = (ast, syntax_styler, code_names, options) => {
111
81
  context.next();
112
82
  if (!code_names.has(node.name))
113
83
  return;
84
+ // Skip if spread attributes present — can't determine content statically
85
+ if (node.attributes.some((attr) => attr.type === 'SpreadAttribute'))
86
+ return;
114
87
  const content_attr = find_attribute(node, 'content');
115
88
  if (!content_attr)
116
89
  return;
@@ -159,62 +132,6 @@ const find_code_usages = (ast, syntax_styler, code_names, options) => {
159
132
  });
160
133
  return transformations;
161
134
  };
162
- /**
163
- * Find an attribute by name on a component node.
164
- */
165
- const find_attribute = (node, name) => {
166
- for (const attr of node.attributes) {
167
- if (attr.type === 'Attribute' && attr.name === name) {
168
- return attr;
169
- }
170
- }
171
- return undefined;
172
- };
173
- /**
174
- * Recursively evaluate an expression AST node to a static string value.
175
- * Handles string literals, template literals (no interpolation), and string concatenation.
176
- * Returns `null` for dynamic or non-string expressions.
177
- */
178
- const evaluate_static_expr = (expr) => {
179
- if (expr.type === 'Literal' && typeof expr.value === 'string')
180
- return expr.value;
181
- if (expr.type === 'TemplateLiteral' && expr.expressions.length === 0) {
182
- return expr.quasis.map((q) => q.value.cooked ?? q.value.raw).join('');
183
- }
184
- if (expr.type === 'BinaryExpression' && expr.operator === '+') {
185
- const left = evaluate_static_expr(expr.left);
186
- if (left === null)
187
- return null;
188
- const right = evaluate_static_expr(expr.right);
189
- if (right === null)
190
- return null;
191
- return left + right;
192
- }
193
- return null;
194
- };
195
- /**
196
- * Extract the string value from a static attribute value.
197
- * Returns `null` for dynamic, non-string, or null literal values.
198
- */
199
- const extract_static_string = (value) => {
200
- // Boolean attribute
201
- if (value === true)
202
- return null;
203
- // Plain attribute: content="text"
204
- if (Array.isArray(value)) {
205
- const first = value[0];
206
- if (value.length === 1 && first?.type === 'Text') {
207
- return first.data;
208
- }
209
- return null;
210
- }
211
- // ExpressionTag
212
- const expr = value.expression;
213
- // Null literal
214
- if (expr.type === 'Literal' && expr.value === null)
215
- return null;
216
- return evaluate_static_expr(expr);
217
- };
218
135
  /**
219
136
  * Try to extract a conditional expression where both branches are static strings.
220
137
  * Returns the condition source text and both branch values, or `null` if not applicable.
@@ -235,23 +152,11 @@ const try_extract_conditional = (value, source) => {
235
152
  const test_source = source.slice(test.start, test.end);
236
153
  return { test_source, consequent, alternate };
237
154
  };
238
- /**
239
- * Escapes a string for use inside a single-quoted JS string literal.
240
- * Single quotes are used because `stylize()` output contains double quotes
241
- * on every token span, so wrapping with single quotes avoids escaping those.
242
- */
243
- const escape_js_string = (html) => {
244
- return html
245
- .replace(/\\/g, '\\\\') // backslashes first
246
- .replace(/'/g, "\\'") // single quotes
247
- .replace(/\n/g, '\\n') // newlines
248
- .replace(/\r/g, '\\r'); // carriage returns
249
- };
250
155
  /**
251
156
  * Handle errors during highlighting.
252
157
  */
253
158
  const handle_error = (error, options) => {
254
- const message = `[code-static] Highlighting failed${options.filename ? ` in ${options.filename}` : ''}: ${error instanceof Error ? error.message : String(error)}`;
159
+ const message = `[fuz-code] Highlighting failed${options.filename ? ` in ${options.filename}` : ''}: ${error instanceof Error ? error.message : String(error)}`;
255
160
  if (options.on_error === 'throw') {
256
161
  throw new Error(message);
257
162
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fuzdev/fuz_code",
3
- "version": "0.41.1",
3
+ "version": "0.43.0",
4
4
  "description": "syntax styling utilities and components for TypeScript, Svelte, and Markdown",
5
5
  "glyph": "🎨",
6
6
  "logo": "logo.svg",
@@ -33,6 +33,8 @@
33
33
  },
34
34
  "peerDependencies": {
35
35
  "@fuzdev/fuz_css": ">=0.44.1",
36
+ "@fuzdev/fuz_util": ">=0.49.0",
37
+ "esm-env": "^1",
36
38
  "magic-string": "^0.30",
37
39
  "svelte": "^5",
38
40
  "zimmerframe": "^1"
@@ -41,6 +43,9 @@
41
43
  "@fuzdev/fuz_css": {
42
44
  "optional": true
43
45
  },
46
+ "@fuzdev/fuz_util": {
47
+ "optional": true
48
+ },
44
49
  "magic-string": {
45
50
  "optional": true
46
51
  },
@@ -55,7 +60,7 @@
55
60
  "@changesets/changelog-git": "^0.2.1",
56
61
  "@fuzdev/fuz_css": "^0.47.0",
57
62
  "@fuzdev/fuz_ui": "^0.181.1",
58
- "@fuzdev/fuz_util": "^0.48.3",
63
+ "@fuzdev/fuz_util": "^0.49.0",
59
64
  "@ryanatkn/eslint-config": "^0.9.0",
60
65
  "@ryanatkn/gro": "^0.190.0",
61
66
  "@sveltejs/adapter-static": "^3.0.10",
@@ -1,11 +1,20 @@
1
1
  import {parse, type PreprocessorGroup, type AST} from 'svelte/compiler';
2
2
  import MagicString from 'magic-string';
3
3
  import {walk} from 'zimmerframe';
4
+ import {should_exclude_path} from '@fuzdev/fuz_util/path.js';
5
+ import {escape_js_string} from '@fuzdev/fuz_util/string.js';
6
+ import {
7
+ find_attribute,
8
+ evaluate_static_expr,
9
+ extract_static_string,
10
+ resolve_component_names,
11
+ type ResolvedComponentImport,
12
+ } from '@fuzdev/fuz_util/svelte_preprocess_helpers.js';
4
13
 
5
14
  import {syntax_styler_global} from './syntax_styler_global.js';
6
15
  import type {SyntaxStyler} from './syntax_styler.js';
7
16
 
8
- export interface PreprocessCodeStaticOptions {
17
+ export interface PreprocessFuzCodeOptions {
9
18
  /** File patterns to exclude. */
10
19
  exclude?: Array<string | RegExp>;
11
20
 
@@ -30,8 +39,8 @@ export interface PreprocessCodeStaticOptions {
30
39
  on_error?: 'log' | 'throw';
31
40
  }
32
41
 
33
- export const svelte_preprocess_code_static = (
34
- options: PreprocessCodeStaticOptions = {},
42
+ export const svelte_preprocess_fuz_code = (
43
+ options: PreprocessFuzCodeOptions = {},
35
44
  ): PreprocessorGroup => {
36
45
  const {
37
46
  exclude = [],
@@ -45,11 +54,11 @@ export const svelte_preprocess_code_static = (
45
54
  const highlight_cache: Map<string, string> = new Map();
46
55
 
47
56
  return {
48
- name: 'code-static',
57
+ name: 'fuz-code',
49
58
 
50
59
  markup: ({content, filename}) => {
51
60
  // Skip excluded files
52
- if (should_exclude(filename, exclude)) {
61
+ if (should_exclude_path(filename, exclude)) {
53
62
  return {code: content};
54
63
  }
55
64
 
@@ -62,7 +71,7 @@ export const svelte_preprocess_code_static = (
62
71
  const ast = parse(content, {filename, modern: true});
63
72
 
64
73
  // Resolve which local names map to the Code component
65
- const code_names = resolve_code_names(ast, component_imports);
74
+ const code_names = resolve_component_names(ast, component_imports);
66
75
  if (code_names.size === 0) {
67
76
  return {code: content};
68
77
  }
@@ -92,43 +101,6 @@ export const svelte_preprocess_code_static = (
92
101
  };
93
102
  };
94
103
 
95
- /**
96
- * Check if a filename matches any exclusion pattern.
97
- */
98
- const should_exclude = (filename: string | undefined, exclude: Array<string | RegExp>): boolean => {
99
- if (!filename || exclude.length === 0) return false;
100
- return exclude.some((pattern) =>
101
- typeof pattern === 'string' ? filename.includes(pattern) : pattern.test(filename),
102
- );
103
- };
104
-
105
- /**
106
- * Scans import declarations to find local names that import from known Code component sources.
107
- * Handles default imports, named imports, and aliased imports.
108
- * Checks both instance (`<script>`) and module (`<script module>`) scripts.
109
- */
110
- const resolve_code_names = (ast: AST.Root, component_imports: Array<string>): Set<string> => {
111
- const names: Set<string> = new Set();
112
-
113
- for (const script of [ast.instance, ast.module]) {
114
- if (!script) continue;
115
-
116
- for (const node of script.content.body) {
117
- if (node.type !== 'ImportDeclaration') continue;
118
- if (!component_imports.includes(node.source.value as string)) continue;
119
-
120
- for (const specifier of node.specifiers) {
121
- // default import: `import Code from '...'`
122
- // aliased: `import Highlighter from '...'`
123
- // named: `import { default as Code } from '...'`
124
- names.add(specifier.local.name);
125
- }
126
- }
127
- }
128
-
129
- return names;
130
- };
131
-
132
104
  interface Transformation {
133
105
  start: number;
134
106
  end: number;
@@ -173,7 +145,7 @@ const try_highlight = (
173
145
  const find_code_usages = (
174
146
  ast: AST.Root,
175
147
  syntax_styler: SyntaxStyler,
176
- code_names: Set<string>,
148
+ code_names: Map<string, ResolvedComponentImport>,
177
149
  options: FindCodeUsagesOptions,
178
150
  ): Array<Transformation> => {
179
151
  const transformations: Array<Transformation> = [];
@@ -187,6 +159,9 @@ const find_code_usages = (
187
159
 
188
160
  if (!code_names.has(node.name)) return;
189
161
 
162
+ // Skip if spread attributes present — can't determine content statically
163
+ if (node.attributes.some((attr: any) => attr.type === 'SpreadAttribute')) return;
164
+
190
165
  const content_attr = find_attribute(node, 'content');
191
166
  if (!content_attr) return;
192
167
 
@@ -237,64 +212,8 @@ const find_code_usages = (
237
212
  return transformations;
238
213
  };
239
214
 
240
- /**
241
- * Find an attribute by name on a component node.
242
- */
243
- const find_attribute = (node: AST.Component, name: string): AST.Attribute | undefined => {
244
- for (const attr of node.attributes) {
245
- if (attr.type === 'Attribute' && attr.name === name) {
246
- return attr;
247
- }
248
- }
249
- return undefined;
250
- };
251
-
252
215
  type Attribute_Value = AST.Attribute['value'];
253
216
 
254
- /**
255
- * Recursively evaluate an expression AST node to a static string value.
256
- * Handles string literals, template literals (no interpolation), and string concatenation.
257
- * Returns `null` for dynamic or non-string expressions.
258
- */
259
- const evaluate_static_expr = (expr: any): string | null => {
260
- if (expr.type === 'Literal' && typeof expr.value === 'string') return expr.value;
261
- if (expr.type === 'TemplateLiteral' && expr.expressions.length === 0) {
262
- return expr.quasis.map((q: any) => q.value.cooked ?? q.value.raw).join('');
263
- }
264
- if (expr.type === 'BinaryExpression' && expr.operator === '+') {
265
- const left = evaluate_static_expr(expr.left);
266
- if (left === null) return null;
267
- const right = evaluate_static_expr(expr.right);
268
- if (right === null) return null;
269
- return left + right;
270
- }
271
- return null;
272
- };
273
-
274
- /**
275
- * Extract the string value from a static attribute value.
276
- * Returns `null` for dynamic, non-string, or null literal values.
277
- */
278
- const extract_static_string = (value: Attribute_Value): string | null => {
279
- // Boolean attribute
280
- if (value === true) return null;
281
-
282
- // Plain attribute: content="text"
283
- if (Array.isArray(value)) {
284
- const first = value[0];
285
- if (value.length === 1 && first?.type === 'Text') {
286
- return first.data;
287
- }
288
- return null;
289
- }
290
-
291
- // ExpressionTag
292
- const expr = value.expression;
293
- // Null literal
294
- if (expr.type === 'Literal' && expr.value === null) return null;
295
- return evaluate_static_expr(expr);
296
- };
297
-
298
217
  interface ConditionalStaticStrings {
299
218
  test_source: string;
300
219
  consequent: string;
@@ -323,24 +242,11 @@ const try_extract_conditional = (
323
242
  return {test_source, consequent, alternate};
324
243
  };
325
244
 
326
- /**
327
- * Escapes a string for use inside a single-quoted JS string literal.
328
- * Single quotes are used because `stylize()` output contains double quotes
329
- * on every token span, so wrapping with single quotes avoids escaping those.
330
- */
331
- const escape_js_string = (html: string): string => {
332
- return html
333
- .replace(/\\/g, '\\\\') // backslashes first
334
- .replace(/'/g, "\\'") // single quotes
335
- .replace(/\n/g, '\\n') // newlines
336
- .replace(/\r/g, '\\r'); // carriage returns
337
- };
338
-
339
245
  /**
340
246
  * Handle errors during highlighting.
341
247
  */
342
248
  const handle_error = (error: unknown, options: FindCodeUsagesOptions): void => {
343
- const message = `[code-static] Highlighting failed${options.filename ? ` in ${options.filename}` : ''}: ${error instanceof Error ? error.message : String(error)}`;
249
+ const message = `[fuz-code] Highlighting failed${options.filename ? ` in ${options.filename}` : ''}: ${error instanceof Error ? error.message : String(error)}`;
344
250
 
345
251
  if (options.on_error === 'throw') {
346
252
  throw new Error(message);
@@ -1 +0,0 @@
1
- {"version":3,"file":"svelte_preprocess_code_static.d.ts","sourceRoot":"../src/lib/","sources":["../src/lib/svelte_preprocess_code_static.ts"],"names":[],"mappings":"AAAA,OAAO,EAAQ,KAAK,iBAAiB,EAAW,MAAM,iBAAiB,CAAC;AAKxE,OAAO,KAAK,EAAC,YAAY,EAAC,MAAM,oBAAoB,CAAC;AAErD,MAAM,WAAW,2BAA2B;IAC3C,gCAAgC;IAChC,OAAO,CAAC,EAAE,KAAK,CAAC,MAAM,GAAG,MAAM,CAAC,CAAC;IAEjC,0DAA0D;IAC1D,aAAa,CAAC,EAAE,YAAY,CAAC;IAE7B,8CAA8C;IAC9C,KAAK,CAAC,EAAE,OAAO,CAAC;IAEhB;;;;;OAKG;IACH,iBAAiB,CAAC,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;IAElC;;;OAGG;IACH,QAAQ,CAAC,EAAE,KAAK,GAAG,OAAO,CAAC;CAC3B;AAED,eAAO,MAAM,6BAA6B,GACzC,UAAS,2BAAgC,KACvC,iBA0DF,CAAC"}