@fuzdev/fuz_code 0.43.0 → 0.44.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.
@@ -1 +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
+ {"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;AAgBxE,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,iBA6DF,CAAC"}
@@ -3,10 +3,10 @@ import MagicString from 'magic-string';
3
3
  import { walk } from 'zimmerframe';
4
4
  import { should_exclude_path } from '@fuzdev/fuz_util/path.js';
5
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';
6
+ import { find_attribute, extract_static_string, try_extract_conditional_chain, build_static_bindings, resolve_component_names, handle_preprocess_error, } from '@fuzdev/fuz_util/svelte_preprocess_helpers.js';
7
7
  import { syntax_styler_global } from './syntax_styler_global.js';
8
8
  export const svelte_preprocess_fuz_code = (options = {}) => {
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;
9
+ const { exclude = [], syntax_styler = syntax_styler_global, cache = true, component_imports = ['@fuzdev/fuz_code/Code.svelte'], on_error = process.env.CI === 'true' ? 'throw' : 'log', } = options;
10
10
  // In-memory cache: content+lang hash → highlighted HTML
11
11
  const highlight_cache = new Map();
12
12
  return {
@@ -27,12 +27,14 @@ export const svelte_preprocess_fuz_code = (options = {}) => {
27
27
  if (code_names.size === 0) {
28
28
  return { code: content };
29
29
  }
30
+ const bindings = build_static_bindings(ast);
30
31
  // Find Code component usages with static content
31
32
  const transformations = find_code_usages(ast, syntax_styler, code_names, {
32
33
  cache: cache ? highlight_cache : null,
33
34
  on_error,
34
35
  filename,
35
36
  source: content,
37
+ bindings,
36
38
  });
37
39
  if (transformations.length === 0) {
38
40
  return { code: content };
@@ -61,7 +63,7 @@ const try_highlight = (text, lang, syntax_styler, options) => {
61
63
  options.cache?.set(cache_key, html);
62
64
  }
63
65
  catch (error) {
64
- handle_error(error, options);
66
+ handle_preprocess_error(error, '[fuz-code]', options.filename, options.on_error);
65
67
  return null;
66
68
  }
67
69
  }
@@ -95,13 +97,15 @@ const find_code_usages = (ast, syntax_styler, code_names, options) => {
95
97
  }
96
98
  // Resolve language - must be static and supported
97
99
  const lang_attr = find_attribute(node, 'lang');
98
- const lang_value = lang_attr ? extract_static_string(lang_attr.value) : 'svelte';
100
+ const lang_value = lang_attr
101
+ ? extract_static_string(lang_attr.value, options.bindings)
102
+ : 'svelte';
99
103
  if (lang_value === null)
100
104
  return;
101
105
  if (!syntax_styler.langs[lang_value])
102
106
  return;
103
107
  // Try simple static string
104
- const content_value = extract_static_string(content_attr.value);
108
+ const content_value = extract_static_string(content_attr.value, options.bindings);
105
109
  if (content_value !== null) {
106
110
  const html = try_highlight(content_value, lang_value, syntax_styler, options);
107
111
  if (html === null || html === content_value)
@@ -113,53 +117,43 @@ const find_code_usages = (ast, syntax_styler, code_names, options) => {
113
117
  });
114
118
  return;
115
119
  }
116
- // Try conditional expression with static string branches
117
- const conditional = try_extract_conditional(content_attr.value, options.source);
118
- if (conditional) {
119
- const html_a = try_highlight(conditional.consequent, lang_value, syntax_styler, options);
120
- const html_b = try_highlight(conditional.alternate, lang_value, syntax_styler, options);
121
- if (html_a === null || html_b === null)
122
- return;
123
- if (html_a === conditional.consequent && html_b === conditional.alternate)
120
+ // Try conditional chain (handles both simple and nested ternaries)
121
+ const chain = try_extract_conditional_chain(content_attr.value, options.source, options.bindings);
122
+ if (chain) {
123
+ // Highlight all branches
124
+ const highlighted = [];
125
+ let any_changed = false;
126
+ for (const branch of chain) {
127
+ const html = try_highlight(branch.value, lang_value, syntax_styler, options);
128
+ if (html === null)
129
+ return;
130
+ if (html !== branch.value)
131
+ any_changed = true;
132
+ highlighted.push({ html, original: branch.value });
133
+ }
134
+ if (!any_changed)
124
135
  return;
136
+ // Build nested ternary expression for dangerous_raw_html
137
+ // chain: [{test_source: 'a', value: ...}, {test_source: 'b', value: ...}, {test_source: null, value: ...}]
138
+ // → a ? 'html_a' : b ? 'html_b' : 'html_c'
139
+ let expr = '';
140
+ for (let i = 0; i < chain.length; i++) {
141
+ const branch = chain[i];
142
+ const html = highlighted[i].html;
143
+ if (branch.test_source !== null) {
144
+ expr += `${branch.test_source} ? '${escape_js_string(html)}' : `;
145
+ }
146
+ else {
147
+ expr += `'${escape_js_string(html)}'`;
148
+ }
149
+ }
125
150
  transformations.push({
126
151
  start: content_attr.start,
127
152
  end: content_attr.end,
128
- replacement: `dangerous_raw_html={${conditional.test_source} ? '${escape_js_string(html_a)}' : '${escape_js_string(html_b)}'}`,
153
+ replacement: `dangerous_raw_html={${expr}}`,
129
154
  });
130
155
  }
131
156
  },
132
157
  });
133
158
  return transformations;
134
159
  };
135
- /**
136
- * Try to extract a conditional expression where both branches are static strings.
137
- * Returns the condition source text and both branch values, or `null` if not applicable.
138
- */
139
- const try_extract_conditional = (value, source) => {
140
- if (value === true || Array.isArray(value))
141
- return null;
142
- const expr = value.expression;
143
- if (expr.type !== 'ConditionalExpression')
144
- return null;
145
- const consequent = evaluate_static_expr(expr.consequent);
146
- if (consequent === null)
147
- return null;
148
- const alternate = evaluate_static_expr(expr.alternate);
149
- if (alternate === null)
150
- return null;
151
- const test = expr.test;
152
- const test_source = source.slice(test.start, test.end);
153
- return { test_source, consequent, alternate };
154
- };
155
- /**
156
- * Handle errors during highlighting.
157
- */
158
- const handle_error = (error, options) => {
159
- const message = `[fuz-code] Highlighting failed${options.filename ? ` in ${options.filename}` : ''}: ${error instanceof Error ? error.message : String(error)}`;
160
- if (options.on_error === 'throw') {
161
- throw new Error(message);
162
- }
163
- // eslint-disable-next-line no-console
164
- console.error(message);
165
- };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fuzdev/fuz_code",
3
- "version": "0.43.0",
3
+ "version": "0.44.1",
4
4
  "description": "syntax styling utilities and components for TypeScript, Svelte, and Markdown",
5
5
  "glyph": "🎨",
6
6
  "logo": "logo.svg",
@@ -32,8 +32,8 @@
32
32
  "node": ">=22.15"
33
33
  },
34
34
  "peerDependencies": {
35
- "@fuzdev/fuz_css": ">=0.44.1",
36
- "@fuzdev/fuz_util": ">=0.49.0",
35
+ "@fuzdev/fuz_css": ">=0.47.0",
36
+ "@fuzdev/fuz_util": ">=0.49.2",
37
37
  "esm-env": "^1",
38
38
  "magic-string": "^0.30",
39
39
  "svelte": "^5",
@@ -60,9 +60,9 @@
60
60
  "@changesets/changelog-git": "^0.2.1",
61
61
  "@fuzdev/fuz_css": "^0.47.0",
62
62
  "@fuzdev/fuz_ui": "^0.181.1",
63
- "@fuzdev/fuz_util": "^0.49.0",
63
+ "@fuzdev/fuz_util": "^0.50.0",
64
64
  "@ryanatkn/eslint-config": "^0.9.0",
65
- "@ryanatkn/gro": "^0.190.0",
65
+ "@ryanatkn/gro": "^0.191.0",
66
66
  "@sveltejs/adapter-static": "^3.0.10",
67
67
  "@sveltejs/kit": "^2.50.1",
68
68
  "@sveltejs/package": "^2.5.7",
@@ -5,9 +5,11 @@ import {should_exclude_path} from '@fuzdev/fuz_util/path.js';
5
5
  import {escape_js_string} from '@fuzdev/fuz_util/string.js';
6
6
  import {
7
7
  find_attribute,
8
- evaluate_static_expr,
9
8
  extract_static_string,
9
+ try_extract_conditional_chain,
10
+ build_static_bindings,
10
11
  resolve_component_names,
12
+ handle_preprocess_error,
11
13
  type ResolvedComponentImport,
12
14
  } from '@fuzdev/fuz_util/svelte_preprocess_helpers.js';
13
15
 
@@ -47,7 +49,7 @@ export const svelte_preprocess_fuz_code = (
47
49
  syntax_styler = syntax_styler_global,
48
50
  cache = true,
49
51
  component_imports = ['@fuzdev/fuz_code/Code.svelte'],
50
- on_error = process.env.CI ? 'throw' : 'log',
52
+ on_error = process.env.CI === 'true' ? 'throw' : 'log',
51
53
  } = options;
52
54
 
53
55
  // In-memory cache: content+lang hash → highlighted HTML
@@ -76,12 +78,15 @@ export const svelte_preprocess_fuz_code = (
76
78
  return {code: content};
77
79
  }
78
80
 
81
+ const bindings = build_static_bindings(ast);
82
+
79
83
  // Find Code component usages with static content
80
84
  const transformations = find_code_usages(ast, syntax_styler, code_names, {
81
85
  cache: cache ? highlight_cache : null,
82
86
  on_error,
83
87
  filename,
84
88
  source: content,
89
+ bindings,
85
90
  });
86
91
 
87
92
  if (transformations.length === 0) {
@@ -112,6 +117,7 @@ interface FindCodeUsagesOptions {
112
117
  on_error: 'log' | 'throw';
113
118
  filename: string | undefined;
114
119
  source: string;
120
+ bindings: ReadonlyMap<string, string>;
115
121
  }
116
122
 
117
123
  /**
@@ -131,7 +137,7 @@ const try_highlight = (
131
137
  html = syntax_styler.stylize(text, lang);
132
138
  options.cache?.set(cache_key, html);
133
139
  } catch (error) {
134
- handle_error(error, options);
140
+ handle_preprocess_error(error, '[fuz-code]', options.filename, options.on_error);
135
141
  return null;
136
142
  }
137
143
  }
@@ -160,7 +166,7 @@ const find_code_usages = (
160
166
  if (!code_names.has(node.name)) return;
161
167
 
162
168
  // Skip if spread attributes present — can't determine content statically
163
- if (node.attributes.some((attr: any) => attr.type === 'SpreadAttribute')) return;
169
+ if (node.attributes.some((attr) => attr.type === 'SpreadAttribute')) return;
164
170
 
165
171
  const content_attr = find_attribute(node, 'content');
166
172
  if (!content_attr) return;
@@ -176,12 +182,14 @@ const find_code_usages = (
176
182
 
177
183
  // Resolve language - must be static and supported
178
184
  const lang_attr = find_attribute(node, 'lang');
179
- const lang_value = lang_attr ? extract_static_string(lang_attr.value) : 'svelte';
185
+ const lang_value = lang_attr
186
+ ? extract_static_string(lang_attr.value, options.bindings)
187
+ : 'svelte';
180
188
  if (lang_value === null) return;
181
189
  if (!syntax_styler.langs[lang_value]) return;
182
190
 
183
191
  // Try simple static string
184
- const content_value = extract_static_string(content_attr.value);
192
+ const content_value = extract_static_string(content_attr.value, options.bindings);
185
193
  if (content_value !== null) {
186
194
  const html = try_highlight(content_value, lang_value, syntax_styler, options);
187
195
  if (html === null || html === content_value) return;
@@ -193,17 +201,42 @@ const find_code_usages = (
193
201
  return;
194
202
  }
195
203
 
196
- // Try conditional expression with static string branches
197
- const conditional = try_extract_conditional(content_attr.value, options.source);
198
- if (conditional) {
199
- const html_a = try_highlight(conditional.consequent, lang_value, syntax_styler, options);
200
- const html_b = try_highlight(conditional.alternate, lang_value, syntax_styler, options);
201
- if (html_a === null || html_b === null) return;
202
- if (html_a === conditional.consequent && html_b === conditional.alternate) return;
204
+ // Try conditional chain (handles both simple and nested ternaries)
205
+ const chain = try_extract_conditional_chain(
206
+ content_attr.value,
207
+ options.source,
208
+ options.bindings,
209
+ );
210
+ if (chain) {
211
+ // Highlight all branches
212
+ const highlighted: Array<{html: string; original: string}> = [];
213
+ let any_changed = false;
214
+ for (const branch of chain) {
215
+ const html = try_highlight(branch.value, lang_value, syntax_styler, options);
216
+ if (html === null) return;
217
+ if (html !== branch.value) any_changed = true;
218
+ highlighted.push({html, original: branch.value});
219
+ }
220
+ if (!any_changed) return;
221
+
222
+ // Build nested ternary expression for dangerous_raw_html
223
+ // chain: [{test_source: 'a', value: ...}, {test_source: 'b', value: ...}, {test_source: null, value: ...}]
224
+ // → a ? 'html_a' : b ? 'html_b' : 'html_c'
225
+ let expr = '';
226
+ for (let i = 0; i < chain.length; i++) {
227
+ const branch = chain[i]!;
228
+ const html = highlighted[i]!.html;
229
+ if (branch.test_source !== null) {
230
+ expr += `${branch.test_source} ? '${escape_js_string(html)}' : `;
231
+ } else {
232
+ expr += `'${escape_js_string(html)}'`;
233
+ }
234
+ }
235
+
203
236
  transformations.push({
204
237
  start: content_attr.start,
205
238
  end: content_attr.end,
206
- replacement: `dangerous_raw_html={${conditional.test_source} ? '${escape_js_string(html_a)}' : '${escape_js_string(html_b)}'}`,
239
+ replacement: `dangerous_raw_html={${expr}}`,
207
240
  });
208
241
  }
209
242
  },
@@ -211,46 +244,3 @@ const find_code_usages = (
211
244
 
212
245
  return transformations;
213
246
  };
214
-
215
- type Attribute_Value = AST.Attribute['value'];
216
-
217
- interface ConditionalStaticStrings {
218
- test_source: string;
219
- consequent: string;
220
- alternate: string;
221
- }
222
-
223
- /**
224
- * Try to extract a conditional expression where both branches are static strings.
225
- * Returns the condition source text and both branch values, or `null` if not applicable.
226
- */
227
- const try_extract_conditional = (
228
- value: Attribute_Value,
229
- source: string,
230
- ): ConditionalStaticStrings | null => {
231
- if (value === true || Array.isArray(value)) return null;
232
- const expr = value.expression;
233
- if (expr.type !== 'ConditionalExpression') return null;
234
-
235
- const consequent = evaluate_static_expr(expr.consequent);
236
- if (consequent === null) return null;
237
- const alternate = evaluate_static_expr(expr.alternate);
238
- if (alternate === null) return null;
239
-
240
- const test = expr.test as any;
241
- const test_source = source.slice(test.start, test.end);
242
- return {test_source, consequent, alternate};
243
- };
244
-
245
- /**
246
- * Handle errors during highlighting.
247
- */
248
- const handle_error = (error: unknown, options: FindCodeUsagesOptions): void => {
249
- const message = `[fuz-code] Highlighting failed${options.filename ? ` in ${options.filename}` : ''}: ${error instanceof Error ? error.message : String(error)}`;
250
-
251
- if (options.on_error === 'throw') {
252
- throw new Error(message);
253
- }
254
- // eslint-disable-next-line no-console
255
- console.error(message);
256
- };