@fuzdev/fuz_code 0.42.0 → 0.44.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)
|
|
@@ -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;
|
|
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;AAexE,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"}
|
|
@@ -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, build_static_bindings, resolve_component_names, } from '@fuzdev/fuz_util/svelte_preprocess_helpers.js';
|
|
4
7
|
import { syntax_styler_global } from './syntax_styler_global.js';
|
|
5
8
|
export const svelte_preprocess_fuz_code = (options = {}) => {
|
|
6
|
-
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;
|
|
7
10
|
// In-memory cache: content+lang hash → highlighted HTML
|
|
8
11
|
const highlight_cache = new Map();
|
|
9
12
|
return {
|
|
10
13
|
name: 'fuz-code',
|
|
11
14
|
markup: ({ content, filename }) => {
|
|
12
15
|
// Skip excluded files
|
|
13
|
-
if (
|
|
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,16 +23,18 @@ export const svelte_preprocess_fuz_code = (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 =
|
|
26
|
+
const code_names = resolve_component_names(ast, component_imports);
|
|
24
27
|
if (code_names.size === 0) {
|
|
25
28
|
return { code: content };
|
|
26
29
|
}
|
|
30
|
+
const bindings = build_static_bindings(ast);
|
|
27
31
|
// Find Code component usages with static content
|
|
28
32
|
const transformations = find_code_usages(ast, syntax_styler, code_names, {
|
|
29
33
|
cache: cache ? highlight_cache : null,
|
|
30
34
|
on_error,
|
|
31
35
|
filename,
|
|
32
36
|
source: content,
|
|
37
|
+
bindings,
|
|
33
38
|
});
|
|
34
39
|
if (transformations.length === 0) {
|
|
35
40
|
return { code: content };
|
|
@@ -45,39 +50,6 @@ export const svelte_preprocess_fuz_code = (options = {}) => {
|
|
|
45
50
|
},
|
|
46
51
|
};
|
|
47
52
|
};
|
|
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
53
|
/**
|
|
82
54
|
* Attempt to highlight content, using cache if available.
|
|
83
55
|
* Returns the highlighted HTML, or `null` on error.
|
|
@@ -111,6 +83,9 @@ const find_code_usages = (ast, syntax_styler, code_names, options) => {
|
|
|
111
83
|
context.next();
|
|
112
84
|
if (!code_names.has(node.name))
|
|
113
85
|
return;
|
|
86
|
+
// Skip if spread attributes present — can't determine content statically
|
|
87
|
+
if (node.attributes.some((attr) => attr.type === 'SpreadAttribute'))
|
|
88
|
+
return;
|
|
114
89
|
const content_attr = find_attribute(node, 'content');
|
|
115
90
|
if (!content_attr)
|
|
116
91
|
return;
|
|
@@ -122,13 +97,15 @@ const find_code_usages = (ast, syntax_styler, code_names, options) => {
|
|
|
122
97
|
}
|
|
123
98
|
// Resolve language - must be static and supported
|
|
124
99
|
const lang_attr = find_attribute(node, 'lang');
|
|
125
|
-
const lang_value = lang_attr
|
|
100
|
+
const lang_value = lang_attr
|
|
101
|
+
? extract_static_string(lang_attr.value, options.bindings)
|
|
102
|
+
: 'svelte';
|
|
126
103
|
if (lang_value === null)
|
|
127
104
|
return;
|
|
128
105
|
if (!syntax_styler.langs[lang_value])
|
|
129
106
|
return;
|
|
130
107
|
// Try simple static string
|
|
131
|
-
const content_value = extract_static_string(content_attr.value);
|
|
108
|
+
const content_value = extract_static_string(content_attr.value, options.bindings);
|
|
132
109
|
if (content_value !== null) {
|
|
133
110
|
const html = try_highlight(content_value, lang_value, syntax_styler, options);
|
|
134
111
|
if (html === null || html === content_value)
|
|
@@ -141,7 +118,7 @@ const find_code_usages = (ast, syntax_styler, code_names, options) => {
|
|
|
141
118
|
return;
|
|
142
119
|
}
|
|
143
120
|
// Try conditional expression with static string branches
|
|
144
|
-
const conditional = try_extract_conditional(content_attr.value, options.source);
|
|
121
|
+
const conditional = try_extract_conditional(content_attr.value, options.source, options.bindings);
|
|
145
122
|
if (conditional) {
|
|
146
123
|
const html_a = try_highlight(conditional.consequent, lang_value, syntax_styler, options);
|
|
147
124
|
const html_b = try_highlight(conditional.alternate, lang_value, syntax_styler, options);
|
|
@@ -159,94 +136,26 @@ const find_code_usages = (ast, syntax_styler, code_names, options) => {
|
|
|
159
136
|
});
|
|
160
137
|
return transformations;
|
|
161
138
|
};
|
|
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
139
|
/**
|
|
219
140
|
* Try to extract a conditional expression where both branches are static strings.
|
|
220
141
|
* Returns the condition source text and both branch values, or `null` if not applicable.
|
|
221
142
|
*/
|
|
222
|
-
const try_extract_conditional = (value, source) => {
|
|
143
|
+
const try_extract_conditional = (value, source, bindings) => {
|
|
223
144
|
if (value === true || Array.isArray(value))
|
|
224
145
|
return null;
|
|
225
146
|
const expr = value.expression;
|
|
226
147
|
if (expr.type !== 'ConditionalExpression')
|
|
227
148
|
return null;
|
|
228
|
-
const consequent = evaluate_static_expr(expr.consequent);
|
|
149
|
+
const consequent = evaluate_static_expr(expr.consequent, bindings);
|
|
229
150
|
if (consequent === null)
|
|
230
151
|
return null;
|
|
231
|
-
const alternate = evaluate_static_expr(expr.alternate);
|
|
152
|
+
const alternate = evaluate_static_expr(expr.alternate, bindings);
|
|
232
153
|
if (alternate === null)
|
|
233
154
|
return null;
|
|
234
155
|
const test = expr.test;
|
|
235
156
|
const test_source = source.slice(test.start, test.end);
|
|
236
157
|
return { test_source, consequent, alternate };
|
|
237
158
|
};
|
|
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
159
|
/**
|
|
251
160
|
* Handle errors during highlighting.
|
|
252
161
|
*/
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fuzdev/fuz_code",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.44.0",
|
|
4
4
|
"description": "syntax styling utilities and components for TypeScript, Svelte, and Markdown",
|
|
5
5
|
"glyph": "🎨",
|
|
6
6
|
"logo": "logo.svg",
|
|
@@ -32,7 +32,9 @@
|
|
|
32
32
|
"node": ">=22.15"
|
|
33
33
|
},
|
|
34
34
|
"peerDependencies": {
|
|
35
|
-
"@fuzdev/fuz_css": ">=0.
|
|
35
|
+
"@fuzdev/fuz_css": ">=0.47.0",
|
|
36
|
+
"@fuzdev/fuz_util": ">=0.49.2",
|
|
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,9 +60,9 @@
|
|
|
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.
|
|
63
|
+
"@fuzdev/fuz_util": "^0.49.2",
|
|
59
64
|
"@ryanatkn/eslint-config": "^0.9.0",
|
|
60
|
-
"@ryanatkn/gro": "^0.
|
|
65
|
+
"@ryanatkn/gro": "^0.191.0",
|
|
61
66
|
"@sveltejs/adapter-static": "^3.0.10",
|
|
62
67
|
"@sveltejs/kit": "^2.50.1",
|
|
63
68
|
"@sveltejs/package": "^2.5.7",
|
|
@@ -1,6 +1,16 @@
|
|
|
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
|
+
build_static_bindings,
|
|
11
|
+
resolve_component_names,
|
|
12
|
+
type ResolvedComponentImport,
|
|
13
|
+
} from '@fuzdev/fuz_util/svelte_preprocess_helpers.js';
|
|
4
14
|
|
|
5
15
|
import {syntax_styler_global} from './syntax_styler_global.js';
|
|
6
16
|
import type {SyntaxStyler} from './syntax_styler.js';
|
|
@@ -38,7 +48,7 @@ export const svelte_preprocess_fuz_code = (
|
|
|
38
48
|
syntax_styler = syntax_styler_global,
|
|
39
49
|
cache = true,
|
|
40
50
|
component_imports = ['@fuzdev/fuz_code/Code.svelte'],
|
|
41
|
-
on_error = process.env.CI ? 'throw' : 'log',
|
|
51
|
+
on_error = process.env.CI === 'true' ? 'throw' : 'log',
|
|
42
52
|
} = options;
|
|
43
53
|
|
|
44
54
|
// In-memory cache: content+lang hash → highlighted HTML
|
|
@@ -49,7 +59,7 @@ export const svelte_preprocess_fuz_code = (
|
|
|
49
59
|
|
|
50
60
|
markup: ({content, filename}) => {
|
|
51
61
|
// Skip excluded files
|
|
52
|
-
if (
|
|
62
|
+
if (should_exclude_path(filename, exclude)) {
|
|
53
63
|
return {code: content};
|
|
54
64
|
}
|
|
55
65
|
|
|
@@ -62,17 +72,20 @@ export const svelte_preprocess_fuz_code = (
|
|
|
62
72
|
const ast = parse(content, {filename, modern: true});
|
|
63
73
|
|
|
64
74
|
// Resolve which local names map to the Code component
|
|
65
|
-
const code_names =
|
|
75
|
+
const code_names = resolve_component_names(ast, component_imports);
|
|
66
76
|
if (code_names.size === 0) {
|
|
67
77
|
return {code: content};
|
|
68
78
|
}
|
|
69
79
|
|
|
80
|
+
const bindings = build_static_bindings(ast);
|
|
81
|
+
|
|
70
82
|
// Find Code component usages with static content
|
|
71
83
|
const transformations = find_code_usages(ast, syntax_styler, code_names, {
|
|
72
84
|
cache: cache ? highlight_cache : null,
|
|
73
85
|
on_error,
|
|
74
86
|
filename,
|
|
75
87
|
source: content,
|
|
88
|
+
bindings,
|
|
76
89
|
});
|
|
77
90
|
|
|
78
91
|
if (transformations.length === 0) {
|
|
@@ -92,43 +105,6 @@ export const svelte_preprocess_fuz_code = (
|
|
|
92
105
|
};
|
|
93
106
|
};
|
|
94
107
|
|
|
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
108
|
interface Transformation {
|
|
133
109
|
start: number;
|
|
134
110
|
end: number;
|
|
@@ -140,6 +116,7 @@ interface FindCodeUsagesOptions {
|
|
|
140
116
|
on_error: 'log' | 'throw';
|
|
141
117
|
filename: string | undefined;
|
|
142
118
|
source: string;
|
|
119
|
+
bindings: ReadonlyMap<string, string>;
|
|
143
120
|
}
|
|
144
121
|
|
|
145
122
|
/**
|
|
@@ -173,7 +150,7 @@ const try_highlight = (
|
|
|
173
150
|
const find_code_usages = (
|
|
174
151
|
ast: AST.Root,
|
|
175
152
|
syntax_styler: SyntaxStyler,
|
|
176
|
-
code_names:
|
|
153
|
+
code_names: Map<string, ResolvedComponentImport>,
|
|
177
154
|
options: FindCodeUsagesOptions,
|
|
178
155
|
): Array<Transformation> => {
|
|
179
156
|
const transformations: Array<Transformation> = [];
|
|
@@ -187,6 +164,9 @@ const find_code_usages = (
|
|
|
187
164
|
|
|
188
165
|
if (!code_names.has(node.name)) return;
|
|
189
166
|
|
|
167
|
+
// Skip if spread attributes present — can't determine content statically
|
|
168
|
+
if (node.attributes.some((attr: any) => attr.type === 'SpreadAttribute')) return;
|
|
169
|
+
|
|
190
170
|
const content_attr = find_attribute(node, 'content');
|
|
191
171
|
if (!content_attr) return;
|
|
192
172
|
|
|
@@ -201,12 +181,14 @@ const find_code_usages = (
|
|
|
201
181
|
|
|
202
182
|
// Resolve language - must be static and supported
|
|
203
183
|
const lang_attr = find_attribute(node, 'lang');
|
|
204
|
-
const lang_value = lang_attr
|
|
184
|
+
const lang_value = lang_attr
|
|
185
|
+
? extract_static_string(lang_attr.value, options.bindings)
|
|
186
|
+
: 'svelte';
|
|
205
187
|
if (lang_value === null) return;
|
|
206
188
|
if (!syntax_styler.langs[lang_value]) return;
|
|
207
189
|
|
|
208
190
|
// Try simple static string
|
|
209
|
-
const content_value = extract_static_string(content_attr.value);
|
|
191
|
+
const content_value = extract_static_string(content_attr.value, options.bindings);
|
|
210
192
|
if (content_value !== null) {
|
|
211
193
|
const html = try_highlight(content_value, lang_value, syntax_styler, options);
|
|
212
194
|
if (html === null || html === content_value) return;
|
|
@@ -219,7 +201,11 @@ const find_code_usages = (
|
|
|
219
201
|
}
|
|
220
202
|
|
|
221
203
|
// Try conditional expression with static string branches
|
|
222
|
-
const conditional = try_extract_conditional(
|
|
204
|
+
const conditional = try_extract_conditional(
|
|
205
|
+
content_attr.value,
|
|
206
|
+
options.source,
|
|
207
|
+
options.bindings,
|
|
208
|
+
);
|
|
223
209
|
if (conditional) {
|
|
224
210
|
const html_a = try_highlight(conditional.consequent, lang_value, syntax_styler, options);
|
|
225
211
|
const html_b = try_highlight(conditional.alternate, lang_value, syntax_styler, options);
|
|
@@ -237,63 +223,7 @@ const find_code_usages = (
|
|
|
237
223
|
return transformations;
|
|
238
224
|
};
|
|
239
225
|
|
|
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
|
-
type Attribute_Value = AST.Attribute['value'];
|
|
253
|
-
|
|
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
|
-
};
|
|
226
|
+
type AttributeValue = AST.Attribute['value'];
|
|
297
227
|
|
|
298
228
|
interface ConditionalStaticStrings {
|
|
299
229
|
test_source: string;
|
|
@@ -306,16 +236,17 @@ interface ConditionalStaticStrings {
|
|
|
306
236
|
* Returns the condition source text and both branch values, or `null` if not applicable.
|
|
307
237
|
*/
|
|
308
238
|
const try_extract_conditional = (
|
|
309
|
-
value:
|
|
239
|
+
value: AttributeValue,
|
|
310
240
|
source: string,
|
|
241
|
+
bindings: ReadonlyMap<string, string>,
|
|
311
242
|
): ConditionalStaticStrings | null => {
|
|
312
243
|
if (value === true || Array.isArray(value)) return null;
|
|
313
244
|
const expr = value.expression;
|
|
314
245
|
if (expr.type !== 'ConditionalExpression') return null;
|
|
315
246
|
|
|
316
|
-
const consequent = evaluate_static_expr(expr.consequent);
|
|
247
|
+
const consequent = evaluate_static_expr(expr.consequent, bindings);
|
|
317
248
|
if (consequent === null) return null;
|
|
318
|
-
const alternate = evaluate_static_expr(expr.alternate);
|
|
249
|
+
const alternate = evaluate_static_expr(expr.alternate, bindings);
|
|
319
250
|
if (alternate === null) return null;
|
|
320
251
|
|
|
321
252
|
const test = expr.test as any;
|
|
@@ -323,19 +254,6 @@ const try_extract_conditional = (
|
|
|
323
254
|
return {test_source, consequent, alternate};
|
|
324
255
|
};
|
|
325
256
|
|
|
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
257
|
/**
|
|
340
258
|
* Handle errors during highlighting.
|
|
341
259
|
*/
|