@fuzdev/fuz_util 0.48.4 → 0.49.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/dist/diff.js +2 -2
- package/dist/path.d.ts +11 -0
- package/dist/path.d.ts.map +1 -1
- package/dist/path.js +15 -0
- package/dist/string.d.ts +8 -1
- package/dist/string.d.ts.map +1 -1
- package/dist/string.js +25 -4
- package/dist/svelte_preprocess_helpers.d.ts +134 -0
- package/dist/svelte_preprocess_helpers.d.ts.map +1 -0
- package/dist/svelte_preprocess_helpers.js +243 -0
- package/package.json +10 -1
- package/src/lib/diff.ts +2 -2
- package/src/lib/path.ts +20 -0
- package/src/lib/string.ts +27 -4
- package/src/lib/svelte_preprocess_helpers.ts +270 -0
package/dist/diff.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* @module
|
|
5
5
|
*/
|
|
6
|
-
import {
|
|
6
|
+
import { string_is_binary } from './string.js';
|
|
7
7
|
/**
|
|
8
8
|
* Generate a line-based diff between two strings using LCS algorithm.
|
|
9
9
|
*
|
|
@@ -176,7 +176,7 @@ export const format_diff = (diff, current_path, desired_path, options = {}) => {
|
|
|
176
176
|
*/
|
|
177
177
|
export const generate_diff = (current, desired, path, options = {}) => {
|
|
178
178
|
// Skip binary files
|
|
179
|
-
if (
|
|
179
|
+
if (string_is_binary(current) || string_is_binary(desired)) {
|
|
180
180
|
return null;
|
|
181
181
|
}
|
|
182
182
|
const diff = diff_lines(current, desired);
|
package/dist/path.d.ts
CHANGED
|
@@ -53,6 +53,17 @@ export type PathPiece = {
|
|
|
53
53
|
* @todo maybe rethink this API, it's a bit weird, but fits the usage in `ui/Breadcrumbs.svelte`
|
|
54
54
|
*/
|
|
55
55
|
export declare const parse_path_pieces: (raw_path: string) => Array<PathPiece>;
|
|
56
|
+
/**
|
|
57
|
+
* Checks if a filename matches any exclusion pattern.
|
|
58
|
+
*
|
|
59
|
+
* Returns `false` when `filename` is `undefined`, empty string, or `exclude` is empty.
|
|
60
|
+
* String patterns use substring matching. RegExp patterns use `.test()`.
|
|
61
|
+
*
|
|
62
|
+
* @param filename The file path to check, or `undefined` for virtual files.
|
|
63
|
+
* @param exclude Array of string or RegExp exclusion patterns.
|
|
64
|
+
* @returns `true` if the file should be excluded from processing.
|
|
65
|
+
*/
|
|
66
|
+
export declare const should_exclude_path: (filename: string | undefined, exclude: Array<string | RegExp>) => boolean;
|
|
56
67
|
/**
|
|
57
68
|
* Converts a string into a URL-compatible slug.
|
|
58
69
|
* @param str the string to convert
|
package/dist/path.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"path.d.ts","sourceRoot":"../src/lib/","sources":["../src/lib/path.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,QAAQ,EAAC,MAAM,YAAY,CAAC;AAEzC;;GAEG;AACH,MAAM,MAAM,MAAM,GAAG,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;AAEhD;;GAEG;AACH,MAAM,WAAW,QAAQ;IACxB,EAAE,EAAE,MAAM,CAAC;IACX,YAAY,EAAE,OAAO,CAAC;CACtB;AAED;;GAEG;AACH,MAAM,WAAW,YAAa,SAAQ,QAAQ;IAC7C,IAAI,EAAE,MAAM,CAAC;CACb;AAED;;GAEG;AACH,MAAM,MAAM,UAAU,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,YAAY,EAAE,OAAO,KAAK,OAAO,CAAC;AAE1E;;GAEG;AACH,MAAM,MAAM,UAAU,GAAG,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC;AAEnD;;GAEG;AACH,eAAO,MAAM,YAAY,GAAI,aAAa,MAAM,GAAG,GAAG,KAAG,MACgC,CAAC;AAE1F;;GAEG;AACH,eAAO,MAAM,gBAAgB,GAAI,MAAM,MAAM,KAAG,KAAK,CAAC,MAAM,CAU3D,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,mBAAmB,GAAI,MAAM,MAAM,KAAG,KAAK,CAAC,MAAM,CACH,CAAC;AAE7D;;GAEG;AACH,MAAM,MAAM,SAAS,GAClB;IACA,IAAI,EAAE,OAAO,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;CACZ,GACD;IACA,IAAI,EAAE,WAAW,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;CACZ,CAAC;AAEL;;;GAGG;AACH,eAAO,MAAM,iBAAiB,GAAI,UAAU,MAAM,KAAG,KAAK,CAAC,SAAS,CAgBnE,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,OAAO,GAAI,KAAK,MAAM,EAAE,gCAA6B,KAAG,MAYpE,CAAC"}
|
|
1
|
+
{"version":3,"file":"path.d.ts","sourceRoot":"../src/lib/","sources":["../src/lib/path.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,QAAQ,EAAC,MAAM,YAAY,CAAC;AAEzC;;GAEG;AACH,MAAM,MAAM,MAAM,GAAG,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;AAEhD;;GAEG;AACH,MAAM,WAAW,QAAQ;IACxB,EAAE,EAAE,MAAM,CAAC;IACX,YAAY,EAAE,OAAO,CAAC;CACtB;AAED;;GAEG;AACH,MAAM,WAAW,YAAa,SAAQ,QAAQ;IAC7C,IAAI,EAAE,MAAM,CAAC;CACb;AAED;;GAEG;AACH,MAAM,MAAM,UAAU,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,YAAY,EAAE,OAAO,KAAK,OAAO,CAAC;AAE1E;;GAEG;AACH,MAAM,MAAM,UAAU,GAAG,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC;AAEnD;;GAEG;AACH,eAAO,MAAM,YAAY,GAAI,aAAa,MAAM,GAAG,GAAG,KAAG,MACgC,CAAC;AAE1F;;GAEG;AACH,eAAO,MAAM,gBAAgB,GAAI,MAAM,MAAM,KAAG,KAAK,CAAC,MAAM,CAU3D,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,mBAAmB,GAAI,MAAM,MAAM,KAAG,KAAK,CAAC,MAAM,CACH,CAAC;AAE7D;;GAEG;AACH,MAAM,MAAM,SAAS,GAClB;IACA,IAAI,EAAE,OAAO,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;CACZ,GACD;IACA,IAAI,EAAE,WAAW,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;CACZ,CAAC;AAEL;;;GAGG;AACH,eAAO,MAAM,iBAAiB,GAAI,UAAU,MAAM,KAAG,KAAK,CAAC,SAAS,CAgBnE,CAAC;AAEF;;;;;;;;;GASG;AACH,eAAO,MAAM,mBAAmB,GAC/B,UAAU,MAAM,GAAG,SAAS,EAC5B,SAAS,KAAK,CAAC,MAAM,GAAG,MAAM,CAAC,KAC7B,OAKF,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,OAAO,GAAI,KAAK,MAAM,EAAE,gCAA6B,KAAG,MAYpE,CAAC"}
|
package/dist/path.js
CHANGED
|
@@ -42,6 +42,21 @@ export const parse_path_pieces = (raw_path) => {
|
|
|
42
42
|
}
|
|
43
43
|
return pieces;
|
|
44
44
|
};
|
|
45
|
+
/**
|
|
46
|
+
* Checks if a filename matches any exclusion pattern.
|
|
47
|
+
*
|
|
48
|
+
* Returns `false` when `filename` is `undefined`, empty string, or `exclude` is empty.
|
|
49
|
+
* String patterns use substring matching. RegExp patterns use `.test()`.
|
|
50
|
+
*
|
|
51
|
+
* @param filename The file path to check, or `undefined` for virtual files.
|
|
52
|
+
* @param exclude Array of string or RegExp exclusion patterns.
|
|
53
|
+
* @returns `true` if the file should be excluded from processing.
|
|
54
|
+
*/
|
|
55
|
+
export const should_exclude_path = (filename, exclude) => {
|
|
56
|
+
if (!filename || exclude.length === 0)
|
|
57
|
+
return false;
|
|
58
|
+
return exclude.some((pattern) => typeof pattern === 'string' ? filename.includes(pattern) : pattern.test(filename));
|
|
59
|
+
};
|
|
45
60
|
/**
|
|
46
61
|
* Converts a string into a URL-compatible slug.
|
|
47
62
|
* @param str the string to convert
|
package/dist/string.d.ts
CHANGED
|
@@ -70,6 +70,13 @@ export declare const pad_width: (str: string, target_width: number, align?: "lef
|
|
|
70
70
|
* @returns The edit distance between the strings
|
|
71
71
|
*/
|
|
72
72
|
export declare const levenshtein_distance: (a: string, b: string) => number;
|
|
73
|
+
/**
|
|
74
|
+
* Escapes a string for use inside a single-quoted JS string literal.
|
|
75
|
+
*
|
|
76
|
+
* Uses a single-pass regex replacement to escape backslashes, single quotes,
|
|
77
|
+
* newlines, carriage returns, and Unicode line/paragraph separators.
|
|
78
|
+
*/
|
|
79
|
+
export declare const escape_js_string: (value: string) => string;
|
|
73
80
|
/**
|
|
74
81
|
* Check if content appears to be binary.
|
|
75
82
|
*
|
|
@@ -78,5 +85,5 @@ export declare const levenshtein_distance: (a: string, b: string) => number;
|
|
|
78
85
|
* @param content - Content to check.
|
|
79
86
|
* @returns True if content appears to be binary.
|
|
80
87
|
*/
|
|
81
|
-
export declare const
|
|
88
|
+
export declare const string_is_binary: (content: string) => boolean;
|
|
82
89
|
//# sourceMappingURL=string.d.ts.map
|
package/dist/string.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"string.d.ts","sourceRoot":"../src/lib/","sources":["../src/lib/string.ts"],"names":[],"mappings":"AAEA;;GAEG;AACH,eAAO,MAAM,QAAQ,GAAI,KAAK,MAAM,EAAE,WAAW,MAAM,EAAE,eAAc,KAAG,MAMzE,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,WAAW,GAAI,QAAQ,MAAM,EAAE,UAAU,MAAM,KAAG,MAG9D,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,SAAS,GAAI,QAAQ,MAAM,EAAE,UAAU,MAAM,KAAG,MAG5D,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,WAAW,GAAI,QAAQ,MAAM,EAAE,UAAU,MAAM,KAAG,MAK9D,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,YAAY,GAAI,QAAQ,MAAM,EAAE,UAAU,MAAM,KAAG,MAK/D,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,YAAY,GAAI,QAAQ,MAAM,EAAE,SAAS,MAAM,KAAG,MAG9D,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,UAAU,GAAI,QAAQ,MAAM,EAAE,SAAS,MAAM,KAAG,MAG5D,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,QAAQ,GAAI,KAAK,MAAM,KAAG,MAK1B,CAAC;AAEd;;GAEG;AACH,eAAO,MAAM,MAAM,GAAI,OAAO,MAAM,GAAG,SAAS,GAAG,IAAI,EAAE,eAAY,KAAG,MAC9C,CAAC;AAE3B;;GAEG;AACH,eAAO,MAAM,eAAe,GAAI,KAAK,MAAM,KAAG,MACI,CAAC;AAEnD;;GAEG;AACH,eAAO,MAAM,UAAU,GAAI,KAAK,MAAM,KAAG,MAAsD,CAAC;AAEhG;;;;GAIG;AACH,eAAO,MAAM,SAAS,GAAI,OAAO,OAAO,KAAG,MACwC,CAAC;AAEpF;;;;;;;GAOG;AACH,eAAO,MAAM,oBAAoB,GAAI,KAAK,MAAM,KAAG,MAuClD,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,SAAS,GACrB,KAAK,MAAM,EACX,cAAc,MAAM,EACpB,QAAO,MAAM,GAAG,OAAgB,KAC9B,MAQF,CAAC;AAEF;;;;;;;GAOG;AACH,eAAO,MAAM,oBAAoB,GAAI,GAAG,MAAM,EAAE,GAAG,MAAM,KAAG,MAmC3D,CAAC;AAEF;;;;;;;GAOG;AACH,eAAO,MAAM,
|
|
1
|
+
{"version":3,"file":"string.d.ts","sourceRoot":"../src/lib/","sources":["../src/lib/string.ts"],"names":[],"mappings":"AAEA;;GAEG;AACH,eAAO,MAAM,QAAQ,GAAI,KAAK,MAAM,EAAE,WAAW,MAAM,EAAE,eAAc,KAAG,MAMzE,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,WAAW,GAAI,QAAQ,MAAM,EAAE,UAAU,MAAM,KAAG,MAG9D,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,SAAS,GAAI,QAAQ,MAAM,EAAE,UAAU,MAAM,KAAG,MAG5D,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,WAAW,GAAI,QAAQ,MAAM,EAAE,UAAU,MAAM,KAAG,MAK9D,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,YAAY,GAAI,QAAQ,MAAM,EAAE,UAAU,MAAM,KAAG,MAK/D,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,YAAY,GAAI,QAAQ,MAAM,EAAE,SAAS,MAAM,KAAG,MAG9D,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,UAAU,GAAI,QAAQ,MAAM,EAAE,SAAS,MAAM,KAAG,MAG5D,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,QAAQ,GAAI,KAAK,MAAM,KAAG,MAK1B,CAAC;AAEd;;GAEG;AACH,eAAO,MAAM,MAAM,GAAI,OAAO,MAAM,GAAG,SAAS,GAAG,IAAI,EAAE,eAAY,KAAG,MAC9C,CAAC;AAE3B;;GAEG;AACH,eAAO,MAAM,eAAe,GAAI,KAAK,MAAM,KAAG,MACI,CAAC;AAEnD;;GAEG;AACH,eAAO,MAAM,UAAU,GAAI,KAAK,MAAM,KAAG,MAAsD,CAAC;AAEhG;;;;GAIG;AACH,eAAO,MAAM,SAAS,GAAI,OAAO,OAAO,KAAG,MACwC,CAAC;AAEpF;;;;;;;GAOG;AACH,eAAO,MAAM,oBAAoB,GAAI,KAAK,MAAM,KAAG,MAuClD,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,SAAS,GACrB,KAAK,MAAM,EACX,cAAc,MAAM,EACpB,QAAO,MAAM,GAAG,OAAgB,KAC9B,MAQF,CAAC;AAEF;;;;;;;GAOG;AACH,eAAO,MAAM,oBAAoB,GAAI,GAAG,MAAM,EAAE,GAAG,MAAM,KAAG,MAmC3D,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,gBAAgB,GAAI,OAAO,MAAM,KAAG,MAkB9C,CAAC;AAEJ;;;;;;;GAOG;AACH,eAAO,MAAM,gBAAgB,GAAI,SAAS,MAAM,KAAG,OAAgD,CAAC"}
|
package/dist/string.js
CHANGED
|
@@ -191,6 +191,30 @@ export const levenshtein_distance = (a, b) => {
|
|
|
191
191
|
}
|
|
192
192
|
return prev[short_len];
|
|
193
193
|
};
|
|
194
|
+
/**
|
|
195
|
+
* Escapes a string for use inside a single-quoted JS string literal.
|
|
196
|
+
*
|
|
197
|
+
* Uses a single-pass regex replacement to escape backslashes, single quotes,
|
|
198
|
+
* newlines, carriage returns, and Unicode line/paragraph separators.
|
|
199
|
+
*/
|
|
200
|
+
export const escape_js_string = (value) => value.replace(/[\\'\n\r\u2028\u2029]/g, (ch) => {
|
|
201
|
+
switch (ch) {
|
|
202
|
+
case '\\':
|
|
203
|
+
return '\\\\';
|
|
204
|
+
case "'":
|
|
205
|
+
return "\\'";
|
|
206
|
+
case '\n':
|
|
207
|
+
return '\\n';
|
|
208
|
+
case '\r':
|
|
209
|
+
return '\\r';
|
|
210
|
+
case '\u2028':
|
|
211
|
+
return '\\u2028';
|
|
212
|
+
case '\u2029':
|
|
213
|
+
return '\\u2029';
|
|
214
|
+
default:
|
|
215
|
+
return ch;
|
|
216
|
+
}
|
|
217
|
+
});
|
|
194
218
|
/**
|
|
195
219
|
* Check if content appears to be binary.
|
|
196
220
|
*
|
|
@@ -199,7 +223,4 @@ export const levenshtein_distance = (a, b) => {
|
|
|
199
223
|
* @param content - Content to check.
|
|
200
224
|
* @returns True if content appears to be binary.
|
|
201
225
|
*/
|
|
202
|
-
export const
|
|
203
|
-
const sample = content.slice(0, 8192);
|
|
204
|
-
return sample.includes('\0');
|
|
205
|
-
};
|
|
226
|
+
export const string_is_binary = (content) => content.slice(0, 8192).includes('\0');
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared helper functions for Svelte preprocessors.
|
|
3
|
+
*
|
|
4
|
+
* Provides AST utilities for detecting static content, resolving imports,
|
|
5
|
+
* managing import statements, and escaping strings for Svelte templates.
|
|
6
|
+
* Used by `svelte_preprocess_mdz` in fuz_ui, `svelte_preprocess_fuz_code`
|
|
7
|
+
* in fuz_code, and potentially other Svelte preprocessors.
|
|
8
|
+
*
|
|
9
|
+
* Uses `import type` from `svelte/compiler` for AST types only — no runtime
|
|
10
|
+
* Svelte dependency. Consumers must have `svelte` installed for type resolution.
|
|
11
|
+
*
|
|
12
|
+
* @module
|
|
13
|
+
*/
|
|
14
|
+
import type { Expression, ImportDeclaration, ImportDefaultSpecifier, ImportSpecifier } from 'estree';
|
|
15
|
+
import type { AST } from 'svelte/compiler';
|
|
16
|
+
/** Import metadata for a single import specifier. */
|
|
17
|
+
export interface PreprocessImportInfo {
|
|
18
|
+
/** The module path to import from. */
|
|
19
|
+
path: string;
|
|
20
|
+
/** Whether this is a default or named import. */
|
|
21
|
+
kind: 'default' | 'named';
|
|
22
|
+
}
|
|
23
|
+
/** Information about a resolved component import. */
|
|
24
|
+
export interface ResolvedComponentImport {
|
|
25
|
+
/** The `ImportDeclaration` AST node that provides this name. */
|
|
26
|
+
import_node: ImportDeclaration;
|
|
27
|
+
/** The specific import specifier for this name. */
|
|
28
|
+
specifier: ImportSpecifier | ImportDefaultSpecifier;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Finds an attribute by name on a component AST node.
|
|
32
|
+
*
|
|
33
|
+
* Iterates the node's `attributes` array and returns the first `Attribute`
|
|
34
|
+
* node whose `name` matches. Skips `SpreadAttribute`, directive, and other node types.
|
|
35
|
+
*
|
|
36
|
+
* @param node The component AST node to search.
|
|
37
|
+
* @param name The attribute name to find.
|
|
38
|
+
* @returns The matching `Attribute` node, or `undefined` if not found.
|
|
39
|
+
*/
|
|
40
|
+
export declare const find_attribute: (node: AST.Component, name: string) => AST.Attribute | undefined;
|
|
41
|
+
/**
|
|
42
|
+
* Recursively evaluates an expression AST node to a static string value.
|
|
43
|
+
*
|
|
44
|
+
* Handles string `Literal`, `TemplateLiteral` without interpolation, and
|
|
45
|
+
* `BinaryExpression` with the `+` operator (string concatenation).
|
|
46
|
+
* Returns `null` for dynamic expressions, non-string literals, or unsupported node types.
|
|
47
|
+
*
|
|
48
|
+
* @param expr An ESTree expression AST node.
|
|
49
|
+
* @returns The resolved static string, or `null` if the expression is dynamic.
|
|
50
|
+
*/
|
|
51
|
+
export declare const evaluate_static_expr: (expr: Expression) => string | null;
|
|
52
|
+
/**
|
|
53
|
+
* Extracts a static string value from a Svelte attribute value AST node.
|
|
54
|
+
*
|
|
55
|
+
* Handles three forms:
|
|
56
|
+
* - Boolean `true` (bare attribute like `inline`) -- returns `null`.
|
|
57
|
+
* - Array with a single `Text` node (quoted attribute like `content="text"`) --
|
|
58
|
+
* returns the text data.
|
|
59
|
+
* - `ExpressionTag` (expression like `content={'text'}`) -- delegates to `evaluate_static_expr`.
|
|
60
|
+
*
|
|
61
|
+
* Returns `null` for null literals, mixed arrays, dynamic expressions, and non-string values.
|
|
62
|
+
*
|
|
63
|
+
* @param value The attribute value from `AST.Attribute['value']`.
|
|
64
|
+
* @returns The resolved static string, or `null` if the value is dynamic.
|
|
65
|
+
*/
|
|
66
|
+
export declare const extract_static_string: (value: AST.Attribute["value"]) => string | null;
|
|
67
|
+
/**
|
|
68
|
+
* Resolves local names that import from specified source paths.
|
|
69
|
+
*
|
|
70
|
+
* Scans `ImportDeclaration` nodes in both the instance and module scripts.
|
|
71
|
+
* Handles default, named, and aliased imports. Skips namespace imports.
|
|
72
|
+
* Returns import node references alongside names to support import removal.
|
|
73
|
+
*
|
|
74
|
+
* @param ast The parsed Svelte AST root node.
|
|
75
|
+
* @param component_imports Array of import source paths to match against.
|
|
76
|
+
* @returns Map of local names to their resolved import info.
|
|
77
|
+
*/
|
|
78
|
+
export declare const resolve_component_names: (ast: AST.Root, component_imports: Array<string>) => Map<string, ResolvedComponentImport>;
|
|
79
|
+
/**
|
|
80
|
+
* Finds the position to insert new import statements within a script block.
|
|
81
|
+
*
|
|
82
|
+
* Returns the end position of the last `ImportDeclaration`, or the start
|
|
83
|
+
* of the script body content if no imports exist.
|
|
84
|
+
*
|
|
85
|
+
* @param script The parsed `AST.Script` node.
|
|
86
|
+
* @returns The character position where new imports should be inserted.
|
|
87
|
+
*/
|
|
88
|
+
export declare const find_import_insert_position: (script: AST.Script) => number;
|
|
89
|
+
/**
|
|
90
|
+
* Generates indented import statement lines from an import map.
|
|
91
|
+
*
|
|
92
|
+
* Default imports produce `import Name from 'path';` lines.
|
|
93
|
+
* Named imports are grouped by path into `import {a, b} from 'path';` lines.
|
|
94
|
+
*
|
|
95
|
+
* @param imports Map of local names to their import info.
|
|
96
|
+
* @param indent Indentation prefix for each line. @default '\t'
|
|
97
|
+
* @returns A string of newline-separated import statements.
|
|
98
|
+
*/
|
|
99
|
+
export declare const generate_import_lines: (imports: Map<string, PreprocessImportInfo>, indent?: string) => string;
|
|
100
|
+
/**
|
|
101
|
+
* Checks if an identifier with the given name appears anywhere in an AST subtree.
|
|
102
|
+
*
|
|
103
|
+
* Recursively walks all object and array properties of the tree, matching
|
|
104
|
+
* ESTree `Identifier` nodes (`{type: 'Identifier', name}`). Nodes in the
|
|
105
|
+
* `skip` set are excluded from traversal — used to skip `ImportDeclaration`
|
|
106
|
+
* nodes so the import's own specifier identifier doesn't false-positive.
|
|
107
|
+
*
|
|
108
|
+
* Safe for Svelte template ASTs: `Component.name` is a plain string property
|
|
109
|
+
* (not an `Identifier` node), so `<Mdz>` tags do not produce false matches.
|
|
110
|
+
*
|
|
111
|
+
* @param node The AST subtree to search.
|
|
112
|
+
* @param name The identifier name to look for.
|
|
113
|
+
* @param skip Set of AST nodes to skip during traversal.
|
|
114
|
+
* @returns `true` if a matching `Identifier` node is found.
|
|
115
|
+
*/
|
|
116
|
+
export declare const has_identifier_in_tree: (node: unknown, name: string, skip?: Set<unknown>) => boolean;
|
|
117
|
+
/**
|
|
118
|
+
* Escapes text for safe embedding in Svelte template markup.
|
|
119
|
+
*
|
|
120
|
+
* Uses a single-pass regex replacement to avoid corruption that occurs with sequential
|
|
121
|
+
* `.replace()` calls (where the second replace matches characters introduced by the first).
|
|
122
|
+
*
|
|
123
|
+
* Escapes four characters:
|
|
124
|
+
* - `{` → `{'{'}` and `}` → `{'}'}` — prevents Svelte expression interpretation
|
|
125
|
+
* - `<` → `<` — prevents HTML/Svelte tag interpretation
|
|
126
|
+
* - `&` → `&` — prevents HTML entity interpretation
|
|
127
|
+
*
|
|
128
|
+
* The `&` escaping is necessary because runtime `MdzNodeView.svelte` renders text
|
|
129
|
+
* with `{node.content}` (a Svelte expression), which auto-escapes `&` to `&`.
|
|
130
|
+
* The preprocessor emits raw template text where `&` is NOT auto-escaped, so
|
|
131
|
+
* manual escaping is required to match the runtime behavior.
|
|
132
|
+
*/
|
|
133
|
+
export declare const escape_svelte_text: (text: string) => string;
|
|
134
|
+
//# sourceMappingURL=svelte_preprocess_helpers.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"svelte_preprocess_helpers.d.ts","sourceRoot":"../src/lib/","sources":["../src/lib/svelte_preprocess_helpers.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,EAAC,UAAU,EAAE,iBAAiB,EAAE,sBAAsB,EAAE,eAAe,EAAC,MAAM,QAAQ,CAAC;AACnG,OAAO,KAAK,EAAC,GAAG,EAAC,MAAM,iBAAiB,CAAC;AAEzC,qDAAqD;AACrD,MAAM,WAAW,oBAAoB;IACpC,sCAAsC;IACtC,IAAI,EAAE,MAAM,CAAC;IACb,iDAAiD;IACjD,IAAI,EAAE,SAAS,GAAG,OAAO,CAAC;CAC1B;AAED,qDAAqD;AACrD,MAAM,WAAW,uBAAuB;IACvC,gEAAgE;IAChE,WAAW,EAAE,iBAAiB,CAAC;IAC/B,mDAAmD;IACnD,SAAS,EAAE,eAAe,GAAG,sBAAsB,CAAC;CACpD;AAED;;;;;;;;;GASG;AACH,eAAO,MAAM,cAAc,GAAI,MAAM,GAAG,CAAC,SAAS,EAAE,MAAM,MAAM,KAAG,GAAG,CAAC,SAAS,GAAG,SAOlF,CAAC;AAEF;;;;;;;;;GASG;AACH,eAAO,MAAM,oBAAoB,GAAI,MAAM,UAAU,KAAG,MAAM,GAAG,IAchE,CAAC;AAEF;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,qBAAqB,GAAI,OAAO,GAAG,CAAC,SAAS,CAAC,OAAO,CAAC,KAAG,MAAM,GAAG,IAkB9E,CAAC;AAEF;;;;;;;;;;GAUG;AACH,eAAO,MAAM,uBAAuB,GACnC,KAAK,GAAG,CAAC,IAAI,EACb,mBAAmB,KAAK,CAAC,MAAM,CAAC,KAC9B,GAAG,CAAC,MAAM,EAAE,uBAAuB,CAcrC,CAAC;AAEF;;;;;;;;GAQG;AACH,eAAO,MAAM,2BAA2B,GAAI,QAAQ,GAAG,CAAC,MAAM,KAAG,MAYhE,CAAC;AAEF;;;;;;;;;GASG;AACH,eAAO,MAAM,qBAAqB,GACjC,SAAS,GAAG,CAAC,MAAM,EAAE,oBAAoB,CAAC,EAC1C,SAAQ,MAAa,KACnB,MAyBF,CAAC;AAEF;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,sBAAsB,GAClC,MAAM,OAAO,EACb,MAAM,MAAM,EACZ,OAAO,GAAG,CAAC,OAAO,CAAC,KACjB,OAYF,CAAC;AAEF;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,kBAAkB,GAAI,MAAM,MAAM,KAAG,MAc/C,CAAC"}
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared helper functions for Svelte preprocessors.
|
|
3
|
+
*
|
|
4
|
+
* Provides AST utilities for detecting static content, resolving imports,
|
|
5
|
+
* managing import statements, and escaping strings for Svelte templates.
|
|
6
|
+
* Used by `svelte_preprocess_mdz` in fuz_ui, `svelte_preprocess_fuz_code`
|
|
7
|
+
* in fuz_code, and potentially other Svelte preprocessors.
|
|
8
|
+
*
|
|
9
|
+
* Uses `import type` from `svelte/compiler` for AST types only — no runtime
|
|
10
|
+
* Svelte dependency. Consumers must have `svelte` installed for type resolution.
|
|
11
|
+
*
|
|
12
|
+
* @module
|
|
13
|
+
*/
|
|
14
|
+
/**
|
|
15
|
+
* Finds an attribute by name on a component AST node.
|
|
16
|
+
*
|
|
17
|
+
* Iterates the node's `attributes` array and returns the first `Attribute`
|
|
18
|
+
* node whose `name` matches. Skips `SpreadAttribute`, directive, and other node types.
|
|
19
|
+
*
|
|
20
|
+
* @param node The component AST node to search.
|
|
21
|
+
* @param name The attribute name to find.
|
|
22
|
+
* @returns The matching `Attribute` node, or `undefined` if not found.
|
|
23
|
+
*/
|
|
24
|
+
export const find_attribute = (node, name) => {
|
|
25
|
+
for (const attr of node.attributes) {
|
|
26
|
+
if (attr.type === 'Attribute' && attr.name === name) {
|
|
27
|
+
return attr;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return undefined;
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* Recursively evaluates an expression AST node to a static string value.
|
|
34
|
+
*
|
|
35
|
+
* Handles string `Literal`, `TemplateLiteral` without interpolation, and
|
|
36
|
+
* `BinaryExpression` with the `+` operator (string concatenation).
|
|
37
|
+
* Returns `null` for dynamic expressions, non-string literals, or unsupported node types.
|
|
38
|
+
*
|
|
39
|
+
* @param expr An ESTree expression AST node.
|
|
40
|
+
* @returns The resolved static string, or `null` if the expression is dynamic.
|
|
41
|
+
*/
|
|
42
|
+
export const evaluate_static_expr = (expr) => {
|
|
43
|
+
if (expr.type === 'Literal' && typeof expr.value === 'string')
|
|
44
|
+
return expr.value;
|
|
45
|
+
if (expr.type === 'TemplateLiteral' && expr.expressions.length === 0) {
|
|
46
|
+
return expr.quasis.map((q) => q.value.cooked ?? q.value.raw).join('');
|
|
47
|
+
}
|
|
48
|
+
if (expr.type === 'BinaryExpression' && expr.operator === '+') {
|
|
49
|
+
if (expr.left.type === 'PrivateIdentifier')
|
|
50
|
+
return null;
|
|
51
|
+
const left = evaluate_static_expr(expr.left);
|
|
52
|
+
if (left === null)
|
|
53
|
+
return null;
|
|
54
|
+
const right = evaluate_static_expr(expr.right);
|
|
55
|
+
if (right === null)
|
|
56
|
+
return null;
|
|
57
|
+
return left + right;
|
|
58
|
+
}
|
|
59
|
+
return null;
|
|
60
|
+
};
|
|
61
|
+
/**
|
|
62
|
+
* Extracts a static string value from a Svelte attribute value AST node.
|
|
63
|
+
*
|
|
64
|
+
* Handles three forms:
|
|
65
|
+
* - Boolean `true` (bare attribute like `inline`) -- returns `null`.
|
|
66
|
+
* - Array with a single `Text` node (quoted attribute like `content="text"`) --
|
|
67
|
+
* returns the text data.
|
|
68
|
+
* - `ExpressionTag` (expression like `content={'text'}`) -- delegates to `evaluate_static_expr`.
|
|
69
|
+
*
|
|
70
|
+
* Returns `null` for null literals, mixed arrays, dynamic expressions, and non-string values.
|
|
71
|
+
*
|
|
72
|
+
* @param value The attribute value from `AST.Attribute['value']`.
|
|
73
|
+
* @returns The resolved static string, or `null` if the value is dynamic.
|
|
74
|
+
*/
|
|
75
|
+
export const extract_static_string = (value) => {
|
|
76
|
+
// Boolean attribute (e.g., <Mdz inline />)
|
|
77
|
+
if (value === true)
|
|
78
|
+
return null;
|
|
79
|
+
// Plain attribute: content="text"
|
|
80
|
+
if (Array.isArray(value)) {
|
|
81
|
+
const first = value[0];
|
|
82
|
+
if (value.length === 1 && first?.type === 'Text') {
|
|
83
|
+
return first.data;
|
|
84
|
+
}
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
// ExpressionTag: content={expr}
|
|
88
|
+
const expr = value.expression;
|
|
89
|
+
// Null literal
|
|
90
|
+
if (expr.type === 'Literal' && expr.value === null)
|
|
91
|
+
return null;
|
|
92
|
+
return evaluate_static_expr(expr);
|
|
93
|
+
};
|
|
94
|
+
/**
|
|
95
|
+
* Resolves local names that import from specified source paths.
|
|
96
|
+
*
|
|
97
|
+
* Scans `ImportDeclaration` nodes in both the instance and module scripts.
|
|
98
|
+
* Handles default, named, and aliased imports. Skips namespace imports.
|
|
99
|
+
* Returns import node references alongside names to support import removal.
|
|
100
|
+
*
|
|
101
|
+
* @param ast The parsed Svelte AST root node.
|
|
102
|
+
* @param component_imports Array of import source paths to match against.
|
|
103
|
+
* @returns Map of local names to their resolved import info.
|
|
104
|
+
*/
|
|
105
|
+
export const resolve_component_names = (ast, component_imports) => {
|
|
106
|
+
const names = new Map();
|
|
107
|
+
for (const script of [ast.instance, ast.module]) {
|
|
108
|
+
if (!script)
|
|
109
|
+
continue;
|
|
110
|
+
for (const node of script.content.body) {
|
|
111
|
+
if (node.type !== 'ImportDeclaration')
|
|
112
|
+
continue;
|
|
113
|
+
if (!component_imports.includes(node.source.value))
|
|
114
|
+
continue;
|
|
115
|
+
for (const specifier of node.specifiers) {
|
|
116
|
+
if (specifier.type === 'ImportNamespaceSpecifier')
|
|
117
|
+
continue;
|
|
118
|
+
names.set(specifier.local.name, { import_node: node, specifier });
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return names;
|
|
123
|
+
};
|
|
124
|
+
/**
|
|
125
|
+
* Finds the position to insert new import statements within a script block.
|
|
126
|
+
*
|
|
127
|
+
* Returns the end position of the last `ImportDeclaration`, or the start
|
|
128
|
+
* of the script body content if no imports exist.
|
|
129
|
+
*
|
|
130
|
+
* @param script The parsed `AST.Script` node.
|
|
131
|
+
* @returns The character position where new imports should be inserted.
|
|
132
|
+
*/
|
|
133
|
+
export const find_import_insert_position = (script) => {
|
|
134
|
+
let last_import_end = -1;
|
|
135
|
+
for (const node of script.content.body) {
|
|
136
|
+
if (node.type === 'ImportDeclaration') {
|
|
137
|
+
// Svelte's parser always provides position data on AST nodes
|
|
138
|
+
last_import_end = node.end;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
if (last_import_end !== -1) {
|
|
142
|
+
return last_import_end;
|
|
143
|
+
}
|
|
144
|
+
return script.content.start;
|
|
145
|
+
};
|
|
146
|
+
/**
|
|
147
|
+
* Generates indented import statement lines from an import map.
|
|
148
|
+
*
|
|
149
|
+
* Default imports produce `import Name from 'path';` lines.
|
|
150
|
+
* Named imports are grouped by path into `import {a, b} from 'path';` lines.
|
|
151
|
+
*
|
|
152
|
+
* @param imports Map of local names to their import info.
|
|
153
|
+
* @param indent Indentation prefix for each line. @default '\t'
|
|
154
|
+
* @returns A string of newline-separated import statements.
|
|
155
|
+
*/
|
|
156
|
+
export const generate_import_lines = (imports, indent = '\t') => {
|
|
157
|
+
const default_imports = [];
|
|
158
|
+
const named_by_path = new Map();
|
|
159
|
+
for (const [name, { path, kind }] of imports) {
|
|
160
|
+
if (kind === 'default') {
|
|
161
|
+
default_imports.push([name, path]);
|
|
162
|
+
}
|
|
163
|
+
else {
|
|
164
|
+
let names = named_by_path.get(path);
|
|
165
|
+
if (!names) {
|
|
166
|
+
names = [];
|
|
167
|
+
named_by_path.set(path, names);
|
|
168
|
+
}
|
|
169
|
+
names.push(name);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
const lines = [];
|
|
173
|
+
for (const [name, path] of default_imports) {
|
|
174
|
+
lines.push(`${indent}import ${name} from '${path}';`);
|
|
175
|
+
}
|
|
176
|
+
for (const [path, names] of named_by_path) {
|
|
177
|
+
lines.push(`${indent}import {${names.join(', ')}} from '${path}';`);
|
|
178
|
+
}
|
|
179
|
+
return lines.join('\n');
|
|
180
|
+
};
|
|
181
|
+
/**
|
|
182
|
+
* Checks if an identifier with the given name appears anywhere in an AST subtree.
|
|
183
|
+
*
|
|
184
|
+
* Recursively walks all object and array properties of the tree, matching
|
|
185
|
+
* ESTree `Identifier` nodes (`{type: 'Identifier', name}`). Nodes in the
|
|
186
|
+
* `skip` set are excluded from traversal — used to skip `ImportDeclaration`
|
|
187
|
+
* nodes so the import's own specifier identifier doesn't false-positive.
|
|
188
|
+
*
|
|
189
|
+
* Safe for Svelte template ASTs: `Component.name` is a plain string property
|
|
190
|
+
* (not an `Identifier` node), so `<Mdz>` tags do not produce false matches.
|
|
191
|
+
*
|
|
192
|
+
* @param node The AST subtree to search.
|
|
193
|
+
* @param name The identifier name to look for.
|
|
194
|
+
* @param skip Set of AST nodes to skip during traversal.
|
|
195
|
+
* @returns `true` if a matching `Identifier` node is found.
|
|
196
|
+
*/
|
|
197
|
+
export const has_identifier_in_tree = (node, name, skip) => {
|
|
198
|
+
if (node === null || node === undefined || typeof node !== 'object')
|
|
199
|
+
return false;
|
|
200
|
+
if (skip?.has(node))
|
|
201
|
+
return false;
|
|
202
|
+
if (Array.isArray(node)) {
|
|
203
|
+
return node.some((child) => has_identifier_in_tree(child, name, skip));
|
|
204
|
+
}
|
|
205
|
+
const record = node;
|
|
206
|
+
if (record.type === 'Identifier' && record.name === name)
|
|
207
|
+
return true;
|
|
208
|
+
for (const key of Object.keys(record)) {
|
|
209
|
+
if (has_identifier_in_tree(record[key], name, skip))
|
|
210
|
+
return true;
|
|
211
|
+
}
|
|
212
|
+
return false;
|
|
213
|
+
};
|
|
214
|
+
/**
|
|
215
|
+
* Escapes text for safe embedding in Svelte template markup.
|
|
216
|
+
*
|
|
217
|
+
* Uses a single-pass regex replacement to avoid corruption that occurs with sequential
|
|
218
|
+
* `.replace()` calls (where the second replace matches characters introduced by the first).
|
|
219
|
+
*
|
|
220
|
+
* Escapes four characters:
|
|
221
|
+
* - `{` → `{'{'}` and `}` → `{'}'}` — prevents Svelte expression interpretation
|
|
222
|
+
* - `<` → `<` — prevents HTML/Svelte tag interpretation
|
|
223
|
+
* - `&` → `&` — prevents HTML entity interpretation
|
|
224
|
+
*
|
|
225
|
+
* The `&` escaping is necessary because runtime `MdzNodeView.svelte` renders text
|
|
226
|
+
* with `{node.content}` (a Svelte expression), which auto-escapes `&` to `&`.
|
|
227
|
+
* The preprocessor emits raw template text where `&` is NOT auto-escaped, so
|
|
228
|
+
* manual escaping is required to match the runtime behavior.
|
|
229
|
+
*/
|
|
230
|
+
export const escape_svelte_text = (text) => text.replace(/[{}<&]/g, (ch) => {
|
|
231
|
+
switch (ch) {
|
|
232
|
+
case '{':
|
|
233
|
+
return "{'{'}";
|
|
234
|
+
case '}':
|
|
235
|
+
return "{'}'}";
|
|
236
|
+
case '<':
|
|
237
|
+
return '<';
|
|
238
|
+
case '&':
|
|
239
|
+
return '&';
|
|
240
|
+
default:
|
|
241
|
+
return ch;
|
|
242
|
+
}
|
|
243
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fuzdev/fuz_util",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.49.0",
|
|
4
4
|
"description": "utility belt for JS",
|
|
5
5
|
"glyph": "🦕",
|
|
6
6
|
"logo": "logo.svg",
|
|
@@ -44,8 +44,10 @@
|
|
|
44
44
|
"web"
|
|
45
45
|
],
|
|
46
46
|
"peerDependencies": {
|
|
47
|
+
"@types/estree": "^1",
|
|
47
48
|
"@types/node": "^24",
|
|
48
49
|
"esm-env": "^1.2.2",
|
|
50
|
+
"svelte": "^5",
|
|
49
51
|
"zod": "^4.0.14"
|
|
50
52
|
},
|
|
51
53
|
"peerDependenciesMeta": {
|
|
@@ -55,6 +57,12 @@
|
|
|
55
57
|
"esm-env": {
|
|
56
58
|
"optional": true
|
|
57
59
|
},
|
|
60
|
+
"@types/estree": {
|
|
61
|
+
"optional": true
|
|
62
|
+
},
|
|
63
|
+
"svelte": {
|
|
64
|
+
"optional": true
|
|
65
|
+
},
|
|
58
66
|
"zod": {
|
|
59
67
|
"optional": true
|
|
60
68
|
}
|
|
@@ -70,6 +78,7 @@
|
|
|
70
78
|
"@sveltejs/kit": "^2.50.1",
|
|
71
79
|
"@sveltejs/package": "^2.5.7",
|
|
72
80
|
"@sveltejs/vite-plugin-svelte": "^6.2.4",
|
|
81
|
+
"@types/estree": "^1.0.8",
|
|
73
82
|
"@types/node": "^24.10.1",
|
|
74
83
|
"@webref/css": "^8.2.0",
|
|
75
84
|
"dequal": "^2.0.3",
|
package/src/lib/diff.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* @module
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import {
|
|
7
|
+
import {string_is_binary} from './string.js';
|
|
8
8
|
|
|
9
9
|
/** Line diff result */
|
|
10
10
|
export interface DiffLine {
|
|
@@ -224,7 +224,7 @@ export const generate_diff = (
|
|
|
224
224
|
options: FormatDiffOptions = {},
|
|
225
225
|
): string | null => {
|
|
226
226
|
// Skip binary files
|
|
227
|
-
if (
|
|
227
|
+
if (string_is_binary(current) || string_is_binary(desired)) {
|
|
228
228
|
return null;
|
|
229
229
|
}
|
|
230
230
|
|
package/src/lib/path.ts
CHANGED
|
@@ -94,6 +94,26 @@ export const parse_path_pieces = (raw_path: string): Array<PathPiece> => {
|
|
|
94
94
|
return pieces;
|
|
95
95
|
};
|
|
96
96
|
|
|
97
|
+
/**
|
|
98
|
+
* Checks if a filename matches any exclusion pattern.
|
|
99
|
+
*
|
|
100
|
+
* Returns `false` when `filename` is `undefined`, empty string, or `exclude` is empty.
|
|
101
|
+
* String patterns use substring matching. RegExp patterns use `.test()`.
|
|
102
|
+
*
|
|
103
|
+
* @param filename The file path to check, or `undefined` for virtual files.
|
|
104
|
+
* @param exclude Array of string or RegExp exclusion patterns.
|
|
105
|
+
* @returns `true` if the file should be excluded from processing.
|
|
106
|
+
*/
|
|
107
|
+
export const should_exclude_path = (
|
|
108
|
+
filename: string | undefined,
|
|
109
|
+
exclude: Array<string | RegExp>,
|
|
110
|
+
): boolean => {
|
|
111
|
+
if (!filename || exclude.length === 0) return false;
|
|
112
|
+
return exclude.some((pattern) =>
|
|
113
|
+
typeof pattern === 'string' ? filename.includes(pattern) : pattern.test(filename),
|
|
114
|
+
);
|
|
115
|
+
};
|
|
116
|
+
|
|
97
117
|
/**
|
|
98
118
|
* Converts a string into a URL-compatible slug.
|
|
99
119
|
* @param str the string to convert
|
package/src/lib/string.ts
CHANGED
|
@@ -209,6 +209,32 @@ export const levenshtein_distance = (a: string, b: string): number => {
|
|
|
209
209
|
return prev[short_len]!;
|
|
210
210
|
};
|
|
211
211
|
|
|
212
|
+
/**
|
|
213
|
+
* Escapes a string for use inside a single-quoted JS string literal.
|
|
214
|
+
*
|
|
215
|
+
* Uses a single-pass regex replacement to escape backslashes, single quotes,
|
|
216
|
+
* newlines, carriage returns, and Unicode line/paragraph separators.
|
|
217
|
+
*/
|
|
218
|
+
export const escape_js_string = (value: string): string =>
|
|
219
|
+
value.replace(/[\\'\n\r\u2028\u2029]/g, (ch) => {
|
|
220
|
+
switch (ch) {
|
|
221
|
+
case '\\':
|
|
222
|
+
return '\\\\';
|
|
223
|
+
case "'":
|
|
224
|
+
return "\\'";
|
|
225
|
+
case '\n':
|
|
226
|
+
return '\\n';
|
|
227
|
+
case '\r':
|
|
228
|
+
return '\\r';
|
|
229
|
+
case '\u2028':
|
|
230
|
+
return '\\u2028';
|
|
231
|
+
case '\u2029':
|
|
232
|
+
return '\\u2029';
|
|
233
|
+
default:
|
|
234
|
+
return ch;
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
|
|
212
238
|
/**
|
|
213
239
|
* Check if content appears to be binary.
|
|
214
240
|
*
|
|
@@ -217,7 +243,4 @@ export const levenshtein_distance = (a: string, b: string): number => {
|
|
|
217
243
|
* @param content - Content to check.
|
|
218
244
|
* @returns True if content appears to be binary.
|
|
219
245
|
*/
|
|
220
|
-
export const
|
|
221
|
-
const sample = content.slice(0, 8192);
|
|
222
|
-
return sample.includes('\0');
|
|
223
|
-
};
|
|
246
|
+
export const string_is_binary = (content: string): boolean => content.slice(0, 8192).includes('\0');
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared helper functions for Svelte preprocessors.
|
|
3
|
+
*
|
|
4
|
+
* Provides AST utilities for detecting static content, resolving imports,
|
|
5
|
+
* managing import statements, and escaping strings for Svelte templates.
|
|
6
|
+
* Used by `svelte_preprocess_mdz` in fuz_ui, `svelte_preprocess_fuz_code`
|
|
7
|
+
* in fuz_code, and potentially other Svelte preprocessors.
|
|
8
|
+
*
|
|
9
|
+
* Uses `import type` from `svelte/compiler` for AST types only — no runtime
|
|
10
|
+
* Svelte dependency. Consumers must have `svelte` installed for type resolution.
|
|
11
|
+
*
|
|
12
|
+
* @module
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type {Expression, ImportDeclaration, ImportDefaultSpecifier, ImportSpecifier} from 'estree';
|
|
16
|
+
import type {AST} from 'svelte/compiler';
|
|
17
|
+
|
|
18
|
+
/** Import metadata for a single import specifier. */
|
|
19
|
+
export interface PreprocessImportInfo {
|
|
20
|
+
/** The module path to import from. */
|
|
21
|
+
path: string;
|
|
22
|
+
/** Whether this is a default or named import. */
|
|
23
|
+
kind: 'default' | 'named';
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Information about a resolved component import. */
|
|
27
|
+
export interface ResolvedComponentImport {
|
|
28
|
+
/** The `ImportDeclaration` AST node that provides this name. */
|
|
29
|
+
import_node: ImportDeclaration;
|
|
30
|
+
/** The specific import specifier for this name. */
|
|
31
|
+
specifier: ImportSpecifier | ImportDefaultSpecifier;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Finds an attribute by name on a component AST node.
|
|
36
|
+
*
|
|
37
|
+
* Iterates the node's `attributes` array and returns the first `Attribute`
|
|
38
|
+
* node whose `name` matches. Skips `SpreadAttribute`, directive, and other node types.
|
|
39
|
+
*
|
|
40
|
+
* @param node The component AST node to search.
|
|
41
|
+
* @param name The attribute name to find.
|
|
42
|
+
* @returns The matching `Attribute` node, or `undefined` if not found.
|
|
43
|
+
*/
|
|
44
|
+
export const find_attribute = (node: AST.Component, name: string): AST.Attribute | undefined => {
|
|
45
|
+
for (const attr of node.attributes) {
|
|
46
|
+
if (attr.type === 'Attribute' && attr.name === name) {
|
|
47
|
+
return attr;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return undefined;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Recursively evaluates an expression AST node to a static string value.
|
|
55
|
+
*
|
|
56
|
+
* Handles string `Literal`, `TemplateLiteral` without interpolation, and
|
|
57
|
+
* `BinaryExpression` with the `+` operator (string concatenation).
|
|
58
|
+
* Returns `null` for dynamic expressions, non-string literals, or unsupported node types.
|
|
59
|
+
*
|
|
60
|
+
* @param expr An ESTree expression AST node.
|
|
61
|
+
* @returns The resolved static string, or `null` if the expression is dynamic.
|
|
62
|
+
*/
|
|
63
|
+
export const evaluate_static_expr = (expr: Expression): string | null => {
|
|
64
|
+
if (expr.type === 'Literal' && typeof expr.value === 'string') return expr.value;
|
|
65
|
+
if (expr.type === 'TemplateLiteral' && expr.expressions.length === 0) {
|
|
66
|
+
return expr.quasis.map((q) => q.value.cooked ?? q.value.raw).join('');
|
|
67
|
+
}
|
|
68
|
+
if (expr.type === 'BinaryExpression' && expr.operator === '+') {
|
|
69
|
+
if (expr.left.type === 'PrivateIdentifier') return null;
|
|
70
|
+
const left = evaluate_static_expr(expr.left);
|
|
71
|
+
if (left === null) return null;
|
|
72
|
+
const right = evaluate_static_expr(expr.right);
|
|
73
|
+
if (right === null) return null;
|
|
74
|
+
return left + right;
|
|
75
|
+
}
|
|
76
|
+
return null;
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Extracts a static string value from a Svelte attribute value AST node.
|
|
81
|
+
*
|
|
82
|
+
* Handles three forms:
|
|
83
|
+
* - Boolean `true` (bare attribute like `inline`) -- returns `null`.
|
|
84
|
+
* - Array with a single `Text` node (quoted attribute like `content="text"`) --
|
|
85
|
+
* returns the text data.
|
|
86
|
+
* - `ExpressionTag` (expression like `content={'text'}`) -- delegates to `evaluate_static_expr`.
|
|
87
|
+
*
|
|
88
|
+
* Returns `null` for null literals, mixed arrays, dynamic expressions, and non-string values.
|
|
89
|
+
*
|
|
90
|
+
* @param value The attribute value from `AST.Attribute['value']`.
|
|
91
|
+
* @returns The resolved static string, or `null` if the value is dynamic.
|
|
92
|
+
*/
|
|
93
|
+
export const extract_static_string = (value: AST.Attribute['value']): string | null => {
|
|
94
|
+
// Boolean attribute (e.g., <Mdz inline />)
|
|
95
|
+
if (value === true) return null;
|
|
96
|
+
|
|
97
|
+
// Plain attribute: content="text"
|
|
98
|
+
if (Array.isArray(value)) {
|
|
99
|
+
const first = value[0];
|
|
100
|
+
if (value.length === 1 && first?.type === 'Text') {
|
|
101
|
+
return first.data;
|
|
102
|
+
}
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// ExpressionTag: content={expr}
|
|
107
|
+
const expr = value.expression;
|
|
108
|
+
// Null literal
|
|
109
|
+
if (expr.type === 'Literal' && expr.value === null) return null;
|
|
110
|
+
return evaluate_static_expr(expr);
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Resolves local names that import from specified source paths.
|
|
115
|
+
*
|
|
116
|
+
* Scans `ImportDeclaration` nodes in both the instance and module scripts.
|
|
117
|
+
* Handles default, named, and aliased imports. Skips namespace imports.
|
|
118
|
+
* Returns import node references alongside names to support import removal.
|
|
119
|
+
*
|
|
120
|
+
* @param ast The parsed Svelte AST root node.
|
|
121
|
+
* @param component_imports Array of import source paths to match against.
|
|
122
|
+
* @returns Map of local names to their resolved import info.
|
|
123
|
+
*/
|
|
124
|
+
export const resolve_component_names = (
|
|
125
|
+
ast: AST.Root,
|
|
126
|
+
component_imports: Array<string>,
|
|
127
|
+
): Map<string, ResolvedComponentImport> => {
|
|
128
|
+
const names: Map<string, ResolvedComponentImport> = new Map();
|
|
129
|
+
for (const script of [ast.instance, ast.module]) {
|
|
130
|
+
if (!script) continue;
|
|
131
|
+
for (const node of script.content.body) {
|
|
132
|
+
if (node.type !== 'ImportDeclaration') continue;
|
|
133
|
+
if (!component_imports.includes(node.source.value as string)) continue;
|
|
134
|
+
for (const specifier of node.specifiers) {
|
|
135
|
+
if (specifier.type === 'ImportNamespaceSpecifier') continue;
|
|
136
|
+
names.set(specifier.local.name, {import_node: node, specifier});
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return names;
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Finds the position to insert new import statements within a script block.
|
|
145
|
+
*
|
|
146
|
+
* Returns the end position of the last `ImportDeclaration`, or the start
|
|
147
|
+
* of the script body content if no imports exist.
|
|
148
|
+
*
|
|
149
|
+
* @param script The parsed `AST.Script` node.
|
|
150
|
+
* @returns The character position where new imports should be inserted.
|
|
151
|
+
*/
|
|
152
|
+
export const find_import_insert_position = (script: AST.Script): number => {
|
|
153
|
+
let last_import_end = -1;
|
|
154
|
+
for (const node of script.content.body) {
|
|
155
|
+
if (node.type === 'ImportDeclaration') {
|
|
156
|
+
// Svelte's parser always provides position data on AST nodes
|
|
157
|
+
last_import_end = (node as unknown as AST.BaseNode).end;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
if (last_import_end !== -1) {
|
|
161
|
+
return last_import_end;
|
|
162
|
+
}
|
|
163
|
+
return (script.content as unknown as AST.BaseNode).start;
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Generates indented import statement lines from an import map.
|
|
168
|
+
*
|
|
169
|
+
* Default imports produce `import Name from 'path';` lines.
|
|
170
|
+
* Named imports are grouped by path into `import {a, b} from 'path';` lines.
|
|
171
|
+
*
|
|
172
|
+
* @param imports Map of local names to their import info.
|
|
173
|
+
* @param indent Indentation prefix for each line. @default '\t'
|
|
174
|
+
* @returns A string of newline-separated import statements.
|
|
175
|
+
*/
|
|
176
|
+
export const generate_import_lines = (
|
|
177
|
+
imports: Map<string, PreprocessImportInfo>,
|
|
178
|
+
indent: string = '\t',
|
|
179
|
+
): string => {
|
|
180
|
+
const default_imports: Array<[string, string]> = [];
|
|
181
|
+
const named_by_path: Map<string, Array<string>> = new Map();
|
|
182
|
+
|
|
183
|
+
for (const [name, {path, kind}] of imports) {
|
|
184
|
+
if (kind === 'default') {
|
|
185
|
+
default_imports.push([name, path]);
|
|
186
|
+
} else {
|
|
187
|
+
let names = named_by_path.get(path);
|
|
188
|
+
if (!names) {
|
|
189
|
+
names = [];
|
|
190
|
+
named_by_path.set(path, names);
|
|
191
|
+
}
|
|
192
|
+
names.push(name);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const lines: Array<string> = [];
|
|
197
|
+
for (const [name, path] of default_imports) {
|
|
198
|
+
lines.push(`${indent}import ${name} from '${path}';`);
|
|
199
|
+
}
|
|
200
|
+
for (const [path, names] of named_by_path) {
|
|
201
|
+
lines.push(`${indent}import {${names.join(', ')}} from '${path}';`);
|
|
202
|
+
}
|
|
203
|
+
return lines.join('\n');
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Checks if an identifier with the given name appears anywhere in an AST subtree.
|
|
208
|
+
*
|
|
209
|
+
* Recursively walks all object and array properties of the tree, matching
|
|
210
|
+
* ESTree `Identifier` nodes (`{type: 'Identifier', name}`). Nodes in the
|
|
211
|
+
* `skip` set are excluded from traversal — used to skip `ImportDeclaration`
|
|
212
|
+
* nodes so the import's own specifier identifier doesn't false-positive.
|
|
213
|
+
*
|
|
214
|
+
* Safe for Svelte template ASTs: `Component.name` is a plain string property
|
|
215
|
+
* (not an `Identifier` node), so `<Mdz>` tags do not produce false matches.
|
|
216
|
+
*
|
|
217
|
+
* @param node The AST subtree to search.
|
|
218
|
+
* @param name The identifier name to look for.
|
|
219
|
+
* @param skip Set of AST nodes to skip during traversal.
|
|
220
|
+
* @returns `true` if a matching `Identifier` node is found.
|
|
221
|
+
*/
|
|
222
|
+
export const has_identifier_in_tree = (
|
|
223
|
+
node: unknown,
|
|
224
|
+
name: string,
|
|
225
|
+
skip?: Set<unknown>,
|
|
226
|
+
): boolean => {
|
|
227
|
+
if (node === null || node === undefined || typeof node !== 'object') return false;
|
|
228
|
+
if (skip?.has(node)) return false;
|
|
229
|
+
if (Array.isArray(node)) {
|
|
230
|
+
return node.some((child) => has_identifier_in_tree(child, name, skip));
|
|
231
|
+
}
|
|
232
|
+
const record = node as Record<string, unknown>;
|
|
233
|
+
if (record.type === 'Identifier' && record.name === name) return true;
|
|
234
|
+
for (const key of Object.keys(record)) {
|
|
235
|
+
if (has_identifier_in_tree(record[key], name, skip)) return true;
|
|
236
|
+
}
|
|
237
|
+
return false;
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Escapes text for safe embedding in Svelte template markup.
|
|
242
|
+
*
|
|
243
|
+
* Uses a single-pass regex replacement to avoid corruption that occurs with sequential
|
|
244
|
+
* `.replace()` calls (where the second replace matches characters introduced by the first).
|
|
245
|
+
*
|
|
246
|
+
* Escapes four characters:
|
|
247
|
+
* - `{` → `{'{'}` and `}` → `{'}'}` — prevents Svelte expression interpretation
|
|
248
|
+
* - `<` → `<` — prevents HTML/Svelte tag interpretation
|
|
249
|
+
* - `&` → `&` — prevents HTML entity interpretation
|
|
250
|
+
*
|
|
251
|
+
* The `&` escaping is necessary because runtime `MdzNodeView.svelte` renders text
|
|
252
|
+
* with `{node.content}` (a Svelte expression), which auto-escapes `&` to `&`.
|
|
253
|
+
* The preprocessor emits raw template text where `&` is NOT auto-escaped, so
|
|
254
|
+
* manual escaping is required to match the runtime behavior.
|
|
255
|
+
*/
|
|
256
|
+
export const escape_svelte_text = (text: string): string =>
|
|
257
|
+
text.replace(/[{}<&]/g, (ch) => {
|
|
258
|
+
switch (ch) {
|
|
259
|
+
case '{':
|
|
260
|
+
return "{'{'}";
|
|
261
|
+
case '}':
|
|
262
|
+
return "{'}'}";
|
|
263
|
+
case '<':
|
|
264
|
+
return '<';
|
|
265
|
+
case '&':
|
|
266
|
+
return '&';
|
|
267
|
+
default:
|
|
268
|
+
return ch;
|
|
269
|
+
}
|
|
270
|
+
});
|