Skip to content

Commit 676e71c

Browse files
committed
improve svelte_preprocess_fuz_code to handle more cases
1 parent 70d5a83 commit 676e71c

File tree

6 files changed

+137
-62
lines changed

6 files changed

+137
-62
lines changed

.changeset/funny-numbers-remain.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@fuzdev/fuz_code': patch
3+
---
4+
5+
improve `svelte_preprocess_fuz_code` to handle more cases

package-lock.json

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@
6060
"@changesets/changelog-git": "^0.2.1",
6161
"@fuzdev/fuz_css": "^0.47.0",
6262
"@fuzdev/fuz_ui": "^0.181.1",
63-
"@fuzdev/fuz_util": "^0.49.2",
63+
"@fuzdev/fuz_util": "^0.50.0",
6464
"@ryanatkn/eslint-config": "^0.9.0",
6565
"@ryanatkn/gro": "^0.191.0",
6666
"@sveltejs/adapter-static": "^3.0.10",

src/lib/svelte_preprocess_fuz_code.ts

Lines changed: 32 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,11 @@ import {should_exclude_path} from '@fuzdev/fuz_util/path.js';
55
import {escape_js_string} from '@fuzdev/fuz_util/string.js';
66
import {
77
find_attribute,
8-
evaluate_static_expr,
98
extract_static_string,
9+
try_extract_conditional_chain,
1010
build_static_bindings,
1111
resolve_component_names,
12+
handle_preprocess_error,
1213
type ResolvedComponentImport,
1314
} from '@fuzdev/fuz_util/svelte_preprocess_helpers.js';
1415

@@ -136,7 +137,7 @@ const try_highlight = (
136137
html = syntax_styler.stylize(text, lang);
137138
options.cache?.set(cache_key, html);
138139
} catch (error) {
139-
handle_error(error, options);
140+
handle_preprocess_error(error, '[fuz-code]', options.filename, options.on_error);
140141
return null;
141142
}
142143
}
@@ -200,69 +201,46 @@ const find_code_usages = (
200201
return;
201202
}
202203

203-
// Try conditional expression with static string branches
204-
const conditional = try_extract_conditional(
204+
// Try conditional chain (handles both simple and nested ternaries)
205+
const chain = try_extract_conditional_chain(
205206
content_attr.value,
206207
options.source,
207208
options.bindings,
208209
);
209-
if (conditional) {
210-
const html_a = try_highlight(conditional.consequent, lang_value, syntax_styler, options);
211-
const html_b = try_highlight(conditional.alternate, lang_value, syntax_styler, options);
212-
if (html_a === null || html_b === null) return;
213-
if (html_a === conditional.consequent && html_b === conditional.alternate) return;
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+
214236
transformations.push({
215237
start: content_attr.start,
216238
end: content_attr.end,
217-
replacement: `dangerous_raw_html={${conditional.test_source} ? '${escape_js_string(html_a)}' : '${escape_js_string(html_b)}'}`,
239+
replacement: `dangerous_raw_html={${expr}}`,
218240
});
219241
}
220242
},
221243
});
222244

223245
return transformations;
224246
};
225-
226-
type AttributeValue = AST.Attribute['value'];
227-
228-
interface ConditionalStaticStrings {
229-
test_source: string;
230-
consequent: string;
231-
alternate: string;
232-
}
233-
234-
/**
235-
* Try to extract a conditional expression where both branches are static strings.
236-
* Returns the condition source text and both branch values, or `null` if not applicable.
237-
*/
238-
const try_extract_conditional = (
239-
value: AttributeValue,
240-
source: string,
241-
bindings: ReadonlyMap<string, string>,
242-
): ConditionalStaticStrings | null => {
243-
if (value === true || Array.isArray(value)) return null;
244-
const expr = value.expression;
245-
if (expr.type !== 'ConditionalExpression') return null;
246-
247-
const consequent = evaluate_static_expr(expr.consequent, bindings);
248-
if (consequent === null) return null;
249-
const alternate = evaluate_static_expr(expr.alternate, bindings);
250-
if (alternate === null) return null;
251-
252-
const test = expr.test as any;
253-
const test_source = source.slice(test.start, test.end);
254-
return {test_source, consequent, alternate};
255-
};
256-
257-
/**
258-
* Handle errors during highlighting.
259-
*/
260-
const handle_error = (error: unknown, options: FindCodeUsagesOptions): void => {
261-
const message = `[fuz-code] Highlighting failed${options.filename ? ` in ${options.filename}` : ''}: ${error instanceof Error ? error.message : String(error)}`;
262-
263-
if (options.on_error === 'throw') {
264-
throw new Error(message);
265-
}
266-
// eslint-disable-next-line no-console
267-
console.error(message);
268-
};

src/routes/library.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@
7171
"@changesets/changelog-git": "^0.2.1",
7272
"@fuzdev/fuz_css": "^0.47.0",
7373
"@fuzdev/fuz_ui": "^0.181.1",
74-
"@fuzdev/fuz_util": "^0.49.2",
74+
"@fuzdev/fuz_util": "^0.50.0",
7575
"@ryanatkn/eslint-config": "^0.9.0",
7676
"@ryanatkn/gro": "^0.191.0",
7777
"@sveltejs/adapter-static": "^3.0.10",
@@ -647,7 +647,7 @@
647647
{
648648
"name": "PreprocessFuzCodeOptions",
649649
"kind": "type",
650-
"source_line": 18,
650+
"source_line": 19,
651651
"type_signature": "PreprocessFuzCodeOptions",
652652
"properties": [
653653
{
@@ -685,7 +685,7 @@
685685
{
686686
"name": "svelte_preprocess_fuz_code",
687687
"kind": "function",
688-
"source_line": 43,
688+
"source_line": 44,
689689
"type_signature": "(options?: PreprocessFuzCodeOptions): PreprocessorGroup",
690690
"return_type": "PreprocessorGroup",
691691
"parameters": [

src/test/svelte_preprocess_fuz_code.test.ts

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,98 @@ const y = 2;" lang="ts" />`;
260260
const ast = parse(result, {filename: 'Test.svelte', modern: true});
261261
expect(ast.fragment.nodes.length).toBeGreaterThan(0);
262262
});
263+
264+
test('transforms nested ternary (3 branches)', async () => {
265+
const input = `<script lang="ts">
266+
import Code from '@fuzdev/fuz_code/Code.svelte';
267+
let a = true;
268+
let b = false;
269+
</script>
270+
271+
<Code content={a ? 'const x = 1;' : b ? 'let y = 2;' : 'var z = 3;'} lang="ts" />`;
272+
const result = await run(input);
273+
274+
expect(result).toContain('dangerous_raw_html=');
275+
expect(result).toContain('a ?');
276+
expect(result).toContain('b ?');
277+
expect(result).toContain('token_keyword');
278+
});
279+
280+
test('nested ternary produces correct HTML for all branches', async () => {
281+
const input = `<script lang="ts">
282+
import Code from '@fuzdev/fuz_code/Code.svelte';
283+
let a = true;
284+
let b = false;
285+
</script>
286+
287+
<Code content={a ? 'const x = 1;' : b ? 'let y = 2;' : 'var z = 3;'} lang="ts" />`;
288+
const result = await run(input);
289+
290+
// Extract the nested ternary expression from: dangerous_raw_html={a ? '...' : b ? '...' : '...'}
291+
const match =
292+
/dangerous_raw_html=\{(\w+) \? '((?:[^'\\]|\\.)*)' : (\w+) \? '((?:[^'\\]|\\.)*)' : '((?:[^'\\]|\\.)*)'\}/.exec(
293+
result,
294+
);
295+
expect(match).toBeTruthy();
296+
expect(match![1]).toBe('a');
297+
expect(match![3]).toBe('b');
298+
299+
const unescape = (s: string) =>
300+
s.replace(/\\'/g, "'").replace(/\\n/g, '\n').replace(/\\r/g, '\r').replace(/\\\\/g, '\\');
301+
302+
expect(unescape(match![2]!)).toBe(syntax_styler_global.stylize('const x = 1;', 'ts'));
303+
expect(unescape(match![4]!)).toBe(syntax_styler_global.stylize('let y = 2;', 'ts'));
304+
expect(unescape(match![5]!)).toBe(syntax_styler_global.stylize('var z = 3;', 'ts'));
305+
});
306+
307+
test('skips nested ternary with dynamic branch', async () => {
308+
const input = `<script lang="ts">
309+
import Code from '@fuzdev/fuz_code/Code.svelte';
310+
let a = true;
311+
let b = false;
312+
let code = 'x';
313+
</script>
314+
315+
<Code content={a ? 'const x = 1;' : b ? code : 'var z = 3;'} lang="ts" />`;
316+
const result = await run(input);
317+
318+
expect(result).not.toContain('dangerous_raw_html');
319+
});
320+
321+
test('nested ternary output is parseable by Svelte compiler', async () => {
322+
const input = `<script lang="ts">
323+
import Code from '@fuzdev/fuz_code/Code.svelte';
324+
let a = true;
325+
let b = false;
326+
</script>
327+
328+
<Code content={a ? 'const x = 1;' : b ? 'let y = 2;' : 'var z = 3;'} lang="ts" />`;
329+
const result = await run(input);
330+
331+
const ast = parse(result, {filename: 'Test.svelte', modern: true});
332+
expect(ast.fragment.nodes.length).toBeGreaterThan(0);
333+
});
334+
335+
test('transforms 4-branch nested ternary', async () => {
336+
const input = `<script lang="ts">
337+
import Code from '@fuzdev/fuz_code/Code.svelte';
338+
let a = true;
339+
let b = false;
340+
let c = false;
341+
</script>
342+
343+
<Code content={a ? 'const w = 1;' : b ? 'let x = 2;' : c ? 'var y = 3;' : 'type Z = 4;'} lang="ts" />`;
344+
const result = await run(input);
345+
346+
expect(result).toContain('dangerous_raw_html=');
347+
expect(result).toContain('a ?');
348+
expect(result).toContain('b ?');
349+
expect(result).toContain('c ?');
350+
expect(result).toContain('token_keyword');
351+
352+
const ast = parse(result, {filename: 'Test.svelte', modern: true});
353+
expect(ast.fragment.nodes.length).toBeGreaterThan(0);
354+
});
263355
});
264356

265357
describe('const variable tracing', () => {

0 commit comments

Comments
 (0)