clava 0.2.0 → 0.2.2
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/CHANGELOG.md +12 -0
- package/dist/index.d.ts +0 -10
- package/dist/index.js +608 -392
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/perfs/component.bench.ts +233 -0
- package/src/index.ts +758 -620
- package/src/utils.ts +146 -34
- package/tests/_utils.ts +2 -2
- package/tests/extend-test.ts +48 -0
- package/tests/language-service-test.ts +3 -0
package/src/utils.ts
CHANGED
|
@@ -8,6 +8,14 @@ import type {
|
|
|
8
8
|
export const MODES = ["jsx", "html", "htmlObj"] as const;
|
|
9
9
|
export type Mode = (typeof MODES)[number];
|
|
10
10
|
|
|
11
|
+
// eslint-disable-next-line @typescript-eslint/unbound-method
|
|
12
|
+
const hasOwn = Object.prototype.hasOwnProperty;
|
|
13
|
+
|
|
14
|
+
function isAsciiLetter(code: number) {
|
|
15
|
+
if (code >= 65 && code <= 90) return true;
|
|
16
|
+
return code >= 97 && code <= 122;
|
|
17
|
+
}
|
|
18
|
+
|
|
11
19
|
/**
|
|
12
20
|
* Returns the appropriate class property name based on the mode.
|
|
13
21
|
* @example
|
|
@@ -26,10 +34,38 @@ export function getClassPropertyName(mode: Mode) {
|
|
|
26
34
|
*/
|
|
27
35
|
export function hyphenToCamel(str: string) {
|
|
28
36
|
// CSS custom properties (variables) should not be converted
|
|
29
|
-
if (str.
|
|
37
|
+
if (str.length >= 2 && str.charCodeAt(0) === 45 && str.charCodeAt(1) === 45) {
|
|
30
38
|
return str;
|
|
31
39
|
}
|
|
32
|
-
|
|
40
|
+
// Fast path: no hyphen -> return as-is
|
|
41
|
+
let hyphenIndex = str.indexOf("-");
|
|
42
|
+
if (hyphenIndex === -1) return str;
|
|
43
|
+
|
|
44
|
+
let result = "";
|
|
45
|
+
let lastIndex = 0;
|
|
46
|
+
while (hyphenIndex !== -1) {
|
|
47
|
+
result += str.slice(lastIndex, hyphenIndex);
|
|
48
|
+
|
|
49
|
+
const nextIndex = hyphenIndex + 1;
|
|
50
|
+
if (nextIndex >= str.length) {
|
|
51
|
+
result += "-";
|
|
52
|
+
lastIndex = nextIndex;
|
|
53
|
+
break;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const code = str.charCodeAt(nextIndex);
|
|
57
|
+
if (isAsciiLetter(code)) {
|
|
58
|
+
result += str[nextIndex].toUpperCase();
|
|
59
|
+
lastIndex = nextIndex + 1;
|
|
60
|
+
} else {
|
|
61
|
+
result += "-";
|
|
62
|
+
lastIndex = nextIndex;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
hyphenIndex = str.indexOf("-", lastIndex);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return result + str.slice(lastIndex);
|
|
33
69
|
}
|
|
34
70
|
|
|
35
71
|
/**
|
|
@@ -40,10 +76,23 @@ export function hyphenToCamel(str: string) {
|
|
|
40
76
|
*/
|
|
41
77
|
export function camelToHyphen(str: string) {
|
|
42
78
|
// CSS custom properties (variables) should not be converted
|
|
43
|
-
if (str.
|
|
79
|
+
if (str.length >= 2 && str.charCodeAt(0) === 45 && str.charCodeAt(1) === 45) {
|
|
44
80
|
return str;
|
|
45
81
|
}
|
|
46
|
-
|
|
82
|
+
|
|
83
|
+
let result = "";
|
|
84
|
+
let lastIndex = 0;
|
|
85
|
+
for (let i = 0; i < str.length; i++) {
|
|
86
|
+
const code = str.charCodeAt(i);
|
|
87
|
+
if (code < 65 || code > 90) continue;
|
|
88
|
+
result += str.slice(lastIndex, i);
|
|
89
|
+
result += "-";
|
|
90
|
+
result += str[i].toLowerCase();
|
|
91
|
+
lastIndex = i + 1;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (lastIndex === 0) return str;
|
|
95
|
+
return result + str.slice(lastIndex);
|
|
47
96
|
}
|
|
48
97
|
|
|
49
98
|
/**
|
|
@@ -67,20 +116,60 @@ export function htmlStyleToStyleValue(styleString: string) {
|
|
|
67
116
|
if (!styleString) return {};
|
|
68
117
|
|
|
69
118
|
const result: StyleValue = {};
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
119
|
+
const len = styleString.length;
|
|
120
|
+
let i = 0;
|
|
121
|
+
while (i < len) {
|
|
122
|
+
// Skip leading whitespace and stray semicolons
|
|
123
|
+
while (i < len) {
|
|
124
|
+
const c = styleString.charCodeAt(i);
|
|
125
|
+
if (c !== 32 && c !== 9 && c !== 10 && c !== 13 && c !== 59) break;
|
|
126
|
+
i++;
|
|
127
|
+
}
|
|
128
|
+
if (i >= len) break;
|
|
129
|
+
// Read property name until ':' or ';'
|
|
130
|
+
const propStart = i;
|
|
131
|
+
while (i < len) {
|
|
132
|
+
const c = styleString.charCodeAt(i);
|
|
133
|
+
if (c === 58 || c === 59) break;
|
|
134
|
+
i++;
|
|
135
|
+
}
|
|
136
|
+
if (i >= len || styleString.charCodeAt(i) === 59) {
|
|
137
|
+
// No colon found - skip this declaration
|
|
138
|
+
if (i < len) i++; // skip ';'
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
let propEnd = i;
|
|
142
|
+
// Trim trailing whitespace from property name
|
|
143
|
+
while (propEnd > propStart) {
|
|
144
|
+
const c = styleString.charCodeAt(propEnd - 1);
|
|
145
|
+
if (c !== 32 && c !== 9 && c !== 10 && c !== 13) break;
|
|
146
|
+
propEnd--;
|
|
147
|
+
}
|
|
148
|
+
if (propEnd === propStart) {
|
|
149
|
+
// Empty property - skip
|
|
150
|
+
while (i < len && styleString.charCodeAt(i) !== 59) i++;
|
|
151
|
+
if (i < len) i++;
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
const property = styleString.slice(propStart, propEnd);
|
|
155
|
+
i++; // skip ':'
|
|
156
|
+
// Skip whitespace before value
|
|
157
|
+
while (i < len) {
|
|
158
|
+
const c = styleString.charCodeAt(i);
|
|
159
|
+
if (c !== 32 && c !== 9 && c !== 10 && c !== 13) break;
|
|
160
|
+
i++;
|
|
161
|
+
}
|
|
162
|
+
const valStart = i;
|
|
163
|
+
while (i < len && styleString.charCodeAt(i) !== 59) i++;
|
|
164
|
+
let valEnd = i;
|
|
165
|
+
while (valEnd > valStart) {
|
|
166
|
+
const c = styleString.charCodeAt(valEnd - 1);
|
|
167
|
+
if (c !== 32 && c !== 9 && c !== 10 && c !== 13) break;
|
|
168
|
+
valEnd--;
|
|
169
|
+
}
|
|
170
|
+
if (i < len) i++; // skip ';'
|
|
171
|
+
if (valEnd === valStart) continue;
|
|
172
|
+
const value = styleString.slice(valStart, valEnd);
|
|
84
173
|
// CSS property names and values are dynamic - cast required for index access
|
|
85
174
|
(result as Record<string, string>)[hyphenToCamel(property)] = value;
|
|
86
175
|
}
|
|
@@ -96,11 +185,14 @@ export function htmlStyleToStyleValue(styleString: string) {
|
|
|
96
185
|
*/
|
|
97
186
|
export function htmlObjStyleToStyleValue(style: HTMLCSSProperties) {
|
|
98
187
|
const result: StyleValue = {};
|
|
99
|
-
for (const
|
|
188
|
+
for (const key in style) {
|
|
189
|
+
if (!hasOwn.call(style, key)) continue;
|
|
190
|
+
const value = (style as Record<string, unknown>)[key];
|
|
100
191
|
if (value == null) continue;
|
|
101
192
|
// CSS property names and values are dynamic - cast required for index access
|
|
102
|
-
(result as Record<string, string>)[hyphenToCamel(key)] =
|
|
103
|
-
|
|
193
|
+
(result as Record<string, string>)[hyphenToCamel(key)] = parseLengthValue(
|
|
194
|
+
value as string | number,
|
|
195
|
+
);
|
|
104
196
|
}
|
|
105
197
|
return result;
|
|
106
198
|
}
|
|
@@ -113,10 +205,14 @@ export function htmlObjStyleToStyleValue(style: HTMLCSSProperties) {
|
|
|
113
205
|
*/
|
|
114
206
|
export function jsxStyleToStyleValue(style: JSXCSSProperties) {
|
|
115
207
|
const result: StyleValue = {};
|
|
116
|
-
for (const
|
|
208
|
+
for (const key in style) {
|
|
209
|
+
if (!hasOwn.call(style, key)) continue;
|
|
210
|
+
const value = (style as Record<string, unknown>)[key];
|
|
117
211
|
if (value == null) continue;
|
|
118
212
|
// CSS property names and values are dynamic - cast required for index access
|
|
119
|
-
(result as Record<string, string>)[key] = parseLengthValue(
|
|
213
|
+
(result as Record<string, string>)[key] = parseLengthValue(
|
|
214
|
+
value as string | number,
|
|
215
|
+
);
|
|
120
216
|
}
|
|
121
217
|
return result;
|
|
122
218
|
}
|
|
@@ -128,13 +224,18 @@ export function jsxStyleToStyleValue(style: JSXCSSProperties) {
|
|
|
128
224
|
* // "background-color: red; font-size: 16px;"
|
|
129
225
|
*/
|
|
130
226
|
export function styleValueToHTMLStyle(style: StyleValue): string {
|
|
131
|
-
|
|
132
|
-
for (const
|
|
227
|
+
let result = "";
|
|
228
|
+
for (const key in style) {
|
|
229
|
+
if (!hasOwn.call(style, key)) continue;
|
|
230
|
+
const value = (style as Record<string, unknown>)[key];
|
|
133
231
|
if (value == null) continue;
|
|
134
|
-
|
|
232
|
+
if (result) result += "; ";
|
|
233
|
+
result += camelToHyphen(key);
|
|
234
|
+
result += ": ";
|
|
235
|
+
result += value as string | number;
|
|
135
236
|
}
|
|
136
|
-
if (!
|
|
137
|
-
return `${
|
|
237
|
+
if (!result) return "";
|
|
238
|
+
return `${result};`;
|
|
138
239
|
}
|
|
139
240
|
|
|
140
241
|
/**
|
|
@@ -145,10 +246,11 @@ export function styleValueToHTMLStyle(style: StyleValue): string {
|
|
|
145
246
|
*/
|
|
146
247
|
export function styleValueToHTMLObjStyle(style: StyleValue) {
|
|
147
248
|
const result: CSS.PropertiesHyphen = {};
|
|
148
|
-
for (const
|
|
249
|
+
for (const key in style) {
|
|
250
|
+
if (!hasOwn.call(style, key)) continue;
|
|
251
|
+
const value = (style as Record<string, unknown>)[key];
|
|
149
252
|
if (value == null) continue;
|
|
150
|
-
|
|
151
|
-
result[property] = value;
|
|
253
|
+
(result as Record<string, unknown>)[camelToHyphen(key)] = value;
|
|
152
254
|
}
|
|
153
255
|
return result;
|
|
154
256
|
}
|
|
@@ -172,7 +274,17 @@ export function styleValueToJSXStyle(style: StyleValue) {
|
|
|
172
274
|
export function isHTMLObjStyle(
|
|
173
275
|
style: CSS.Properties<any> | CSS.PropertiesHyphen<any>,
|
|
174
276
|
): style is CSS.PropertiesHyphen {
|
|
175
|
-
|
|
176
|
-
(
|
|
177
|
-
|
|
277
|
+
for (const key in style) {
|
|
278
|
+
if (!hasOwn.call(style, key)) continue;
|
|
279
|
+
// Quick exclusion of CSS custom properties (--foo)
|
|
280
|
+
if (
|
|
281
|
+
key.length >= 2 &&
|
|
282
|
+
key.charCodeAt(0) === 45 &&
|
|
283
|
+
key.charCodeAt(1) === 45
|
|
284
|
+
) {
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
if (key.indexOf("-") !== -1) return true;
|
|
288
|
+
}
|
|
289
|
+
return false;
|
|
178
290
|
}
|
package/tests/_utils.ts
CHANGED
|
@@ -71,8 +71,8 @@ export function getConfigDescription(config: Config) {
|
|
|
71
71
|
return "custom";
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
-
export function createCVFromConfig
|
|
75
|
-
config:
|
|
74
|
+
export function createCVFromConfig(
|
|
75
|
+
config: Config,
|
|
76
76
|
): ReturnType<typeof create>["cv"] {
|
|
77
77
|
const transformClass = getConfigTransformClass(config);
|
|
78
78
|
const hasTransform = "transformClass" in config && config.transformClass;
|
package/tests/extend-test.ts
CHANGED
|
@@ -249,5 +249,53 @@ for (const config of Object.values(CONFIGS)) {
|
|
|
249
249
|
const props = component();
|
|
250
250
|
expect(getStyleClass(props)).toEqual({ class: cls("lg") });
|
|
251
251
|
});
|
|
252
|
+
|
|
253
|
+
test("extend jsx modal component preserves style", () => {
|
|
254
|
+
const base = cv({
|
|
255
|
+
class: "base",
|
|
256
|
+
style: { backgroundColor: "red" },
|
|
257
|
+
});
|
|
258
|
+
const component = getModeComponent(
|
|
259
|
+
mode,
|
|
260
|
+
cv({ extend: [base.jsx], class: "extended" }),
|
|
261
|
+
);
|
|
262
|
+
const props = component();
|
|
263
|
+
expect(getStyleClass(props)).toEqual({
|
|
264
|
+
class: cls("base extended"),
|
|
265
|
+
backgroundColor: "red",
|
|
266
|
+
});
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
test("extend html modal component preserves style", () => {
|
|
270
|
+
const base = cv({
|
|
271
|
+
class: "base",
|
|
272
|
+
style: { backgroundColor: "red" },
|
|
273
|
+
});
|
|
274
|
+
const component = getModeComponent(
|
|
275
|
+
mode,
|
|
276
|
+
cv({ extend: [base.html], class: "extended" }),
|
|
277
|
+
);
|
|
278
|
+
const props = component();
|
|
279
|
+
expect(getStyleClass(props)).toEqual({
|
|
280
|
+
class: cls("base extended"),
|
|
281
|
+
backgroundColor: "red",
|
|
282
|
+
});
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
test("extend htmlObj modal component preserves style", () => {
|
|
286
|
+
const base = cv({
|
|
287
|
+
class: "base",
|
|
288
|
+
style: { backgroundColor: "red" },
|
|
289
|
+
});
|
|
290
|
+
const component = getModeComponent(
|
|
291
|
+
mode,
|
|
292
|
+
cv({ extend: [base.htmlObj], class: "extended" }),
|
|
293
|
+
);
|
|
294
|
+
const props = component();
|
|
295
|
+
expect(getStyleClass(props)).toEqual({
|
|
296
|
+
class: cls("base extended"),
|
|
297
|
+
backgroundColor: "red",
|
|
298
|
+
});
|
|
299
|
+
});
|
|
252
300
|
});
|
|
253
301
|
}
|
|
@@ -27,6 +27,9 @@ function createCompilerOptions(): ts.CompilerOptions {
|
|
|
27
27
|
target: ts.ScriptTarget.ES2020,
|
|
28
28
|
module: ts.ModuleKind.NodeNext,
|
|
29
29
|
moduleResolution: ts.ModuleResolutionKind.NodeNext,
|
|
30
|
+
// Match the repo's Node-aware TS environment so language-service
|
|
31
|
+
// navigation in the fixture reflects real editor behavior.
|
|
32
|
+
types: ["node"],
|
|
30
33
|
allowImportingTsExtensions: true,
|
|
31
34
|
strict: true,
|
|
32
35
|
skipLibCheck: true,
|