clava 0.2.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/utils.ts CHANGED
@@ -8,6 +8,9 @@ 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
+
11
14
  /**
12
15
  * Returns the appropriate class property name based on the mode.
13
16
  * @example
@@ -26,9 +29,11 @@ export function getClassPropertyName(mode: Mode) {
26
29
  */
27
30
  export function hyphenToCamel(str: string) {
28
31
  // CSS custom properties (variables) should not be converted
29
- if (str.startsWith("--")) {
32
+ if (str.length >= 2 && str.charCodeAt(0) === 45 && str.charCodeAt(1) === 45) {
30
33
  return str;
31
34
  }
35
+ // Fast path: no hyphen -> return as-is
36
+ if (str.indexOf("-") === -1) return str;
32
37
  return str.replace(/-([a-z])/gi, (_, letter) => letter.toUpperCase());
33
38
  }
34
39
 
@@ -40,7 +45,7 @@ export function hyphenToCamel(str: string) {
40
45
  */
41
46
  export function camelToHyphen(str: string) {
42
47
  // CSS custom properties (variables) should not be converted
43
- if (str.startsWith("--")) {
48
+ if (str.length >= 2 && str.charCodeAt(0) === 45 && str.charCodeAt(1) === 45) {
44
49
  return str;
45
50
  }
46
51
  return str.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`);
@@ -67,20 +72,60 @@ export function htmlStyleToStyleValue(styleString: string) {
67
72
  if (!styleString) return {};
68
73
 
69
74
  const result: StyleValue = {};
70
- const declarations = styleString.split(";");
71
-
72
- for (const declaration of declarations) {
73
- const trimmed = declaration.trim();
74
- if (!trimmed) continue;
75
-
76
- const colonIndex = trimmed.indexOf(":");
77
- if (colonIndex === -1) continue;
78
-
79
- const property = trimmed.slice(0, colonIndex).trim();
80
- const value = trimmed.slice(colonIndex + 1).trim();
81
- if (!property) continue;
82
- if (!value) continue;
83
-
75
+ const len = styleString.length;
76
+ let i = 0;
77
+ while (i < len) {
78
+ // Skip leading whitespace and stray semicolons
79
+ while (i < len) {
80
+ const c = styleString.charCodeAt(i);
81
+ if (c !== 32 && c !== 9 && c !== 10 && c !== 13 && c !== 59) break;
82
+ i++;
83
+ }
84
+ if (i >= len) break;
85
+ // Read property name until ':' or ';'
86
+ const propStart = i;
87
+ while (i < len) {
88
+ const c = styleString.charCodeAt(i);
89
+ if (c === 58 || c === 59) break;
90
+ i++;
91
+ }
92
+ if (i >= len || styleString.charCodeAt(i) === 59) {
93
+ // No colon found - skip this declaration
94
+ if (i < len) i++; // skip ';'
95
+ continue;
96
+ }
97
+ let propEnd = i;
98
+ // Trim trailing whitespace from property name
99
+ while (propEnd > propStart) {
100
+ const c = styleString.charCodeAt(propEnd - 1);
101
+ if (c !== 32 && c !== 9 && c !== 10 && c !== 13) break;
102
+ propEnd--;
103
+ }
104
+ if (propEnd === propStart) {
105
+ // Empty property - skip
106
+ while (i < len && styleString.charCodeAt(i) !== 59) i++;
107
+ if (i < len) i++;
108
+ continue;
109
+ }
110
+ const property = styleString.slice(propStart, propEnd);
111
+ i++; // skip ':'
112
+ // Skip whitespace before value
113
+ while (i < len) {
114
+ const c = styleString.charCodeAt(i);
115
+ if (c !== 32 && c !== 9 && c !== 10 && c !== 13) break;
116
+ i++;
117
+ }
118
+ const valStart = i;
119
+ while (i < len && styleString.charCodeAt(i) !== 59) i++;
120
+ let valEnd = i;
121
+ while (valEnd > valStart) {
122
+ const c = styleString.charCodeAt(valEnd - 1);
123
+ if (c !== 32 && c !== 9 && c !== 10 && c !== 13) break;
124
+ valEnd--;
125
+ }
126
+ if (i < len) i++; // skip ';'
127
+ if (valEnd === valStart) continue;
128
+ const value = styleString.slice(valStart, valEnd);
84
129
  // CSS property names and values are dynamic - cast required for index access
85
130
  (result as Record<string, string>)[hyphenToCamel(property)] = value;
86
131
  }
@@ -96,11 +141,14 @@ export function htmlStyleToStyleValue(styleString: string) {
96
141
  */
97
142
  export function htmlObjStyleToStyleValue(style: HTMLCSSProperties) {
98
143
  const result: StyleValue = {};
99
- for (const [key, value] of Object.entries(style)) {
144
+ for (const key in style) {
145
+ if (!hasOwn.call(style, key)) continue;
146
+ const value = (style as Record<string, unknown>)[key];
100
147
  if (value == null) continue;
101
148
  // CSS property names and values are dynamic - cast required for index access
102
- (result as Record<string, string>)[hyphenToCamel(key)] =
103
- parseLengthValue(value);
149
+ (result as Record<string, string>)[hyphenToCamel(key)] = parseLengthValue(
150
+ value as string | number,
151
+ );
104
152
  }
105
153
  return result;
106
154
  }
@@ -113,10 +161,14 @@ export function htmlObjStyleToStyleValue(style: HTMLCSSProperties) {
113
161
  */
114
162
  export function jsxStyleToStyleValue(style: JSXCSSProperties) {
115
163
  const result: StyleValue = {};
116
- for (const [key, value] of Object.entries(style)) {
164
+ for (const key in style) {
165
+ if (!hasOwn.call(style, key)) continue;
166
+ const value = (style as Record<string, unknown>)[key];
117
167
  if (value == null) continue;
118
168
  // CSS property names and values are dynamic - cast required for index access
119
- (result as Record<string, string>)[key] = parseLengthValue(value);
169
+ (result as Record<string, string>)[key] = parseLengthValue(
170
+ value as string | number,
171
+ );
120
172
  }
121
173
  return result;
122
174
  }
@@ -128,13 +180,18 @@ export function jsxStyleToStyleValue(style: JSXCSSProperties) {
128
180
  * // "background-color: red; font-size: 16px;"
129
181
  */
130
182
  export function styleValueToHTMLStyle(style: StyleValue): string {
131
- const parts: string[] = [];
132
- for (const [key, value] of Object.entries(style)) {
183
+ let result = "";
184
+ for (const key in style) {
185
+ if (!hasOwn.call(style, key)) continue;
186
+ const value = (style as Record<string, unknown>)[key];
133
187
  if (value == null) continue;
134
- parts.push(`${camelToHyphen(key)}: ${value}`);
188
+ if (result) result += "; ";
189
+ result += camelToHyphen(key);
190
+ result += ": ";
191
+ result += value as string | number;
135
192
  }
136
- if (!parts.length) return "";
137
- return `${parts.join("; ")};`;
193
+ if (!result) return "";
194
+ return `${result};`;
138
195
  }
139
196
 
140
197
  /**
@@ -145,10 +202,11 @@ export function styleValueToHTMLStyle(style: StyleValue): string {
145
202
  */
146
203
  export function styleValueToHTMLObjStyle(style: StyleValue) {
147
204
  const result: CSS.PropertiesHyphen = {};
148
- for (const [key, value] of Object.entries(style)) {
205
+ for (const key in style) {
206
+ if (!hasOwn.call(style, key)) continue;
207
+ const value = (style as Record<string, unknown>)[key];
149
208
  if (value == null) continue;
150
- const property = camelToHyphen(key) as keyof HTMLCSSProperties;
151
- result[property] = value;
209
+ (result as Record<string, unknown>)[camelToHyphen(key)] = value;
152
210
  }
153
211
  return result;
154
212
  }
@@ -172,7 +230,17 @@ export function styleValueToJSXStyle(style: StyleValue) {
172
230
  export function isHTMLObjStyle(
173
231
  style: CSS.Properties<any> | CSS.PropertiesHyphen<any>,
174
232
  ): style is CSS.PropertiesHyphen {
175
- return Object.keys(style).some(
176
- (key) => key.includes("-") && !key.startsWith("--"),
177
- );
233
+ for (const key in style) {
234
+ if (!hasOwn.call(style, key)) continue;
235
+ // Quick exclusion of CSS custom properties (--foo)
236
+ if (
237
+ key.length >= 2 &&
238
+ key.charCodeAt(0) === 45 &&
239
+ key.charCodeAt(1) === 45
240
+ ) {
241
+ continue;
242
+ }
243
+ if (key.indexOf("-") !== -1) return true;
244
+ }
245
+ return false;
178
246
  }
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<T extends Config>(
75
- config: T,
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;
@@ -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,