Skip to content

Commit 6e68cfa

Browse files
TrySoundistarkov
authored andcommitted
refactor: output hyphenated properties from css parser (#4900)
We are going to switch to hyphenated properties in styles. Here refactored css parser to output hyphenated property instead of camel case and added camelCaseProperty utility which does the opposite of hyphenateProperty.
1 parent 80237ee commit 6e68cfa

File tree

19 files changed

+617
-324
lines changed

19 files changed

+617
-324
lines changed

apps/builder/app/builder/features/style-panel/sections/advanced/parse-style-input.ts

Lines changed: 26 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
11
import {
2+
type ParsedStyleDecl,
23
properties,
34
parseCss,
4-
type ParsedStyleDecl,
5+
camelCaseProperty,
56
} from "@webstudio-is/css-data";
6-
import { type StyleProperty } from "@webstudio-is/css-engine";
7-
import { camelCase } from "change-case";
7+
import type { CssProperty, StyleProperty } from "@webstudio-is/css-engine";
88
import { lexer } from "css-tree";
99

10+
type StyleDecl = Omit<ParsedStyleDecl, "property"> & {
11+
property: StyleProperty;
12+
};
13+
1014
/**
1115
* Does several attempts to parse:
1216
* - Custom property "--foo"
@@ -16,7 +20,7 @@ import { lexer } from "css-tree";
1620
* - Property and value: color: red
1721
* - Multiple properties: color: red; background: blue
1822
*/
19-
export const parseStyleInput = (css: string): Array<ParsedStyleDecl> => {
23+
export const parseStyleInput = (css: string): Array<StyleDecl> => {
2024
css = css.trim();
2125
// Is it a custom property "--foo"?
2226
if (css.startsWith("--") && lexer.match("<custom-ident>", css).matched) {
@@ -30,12 +34,11 @@ export const parseStyleInput = (css: string): Array<ParsedStyleDecl> => {
3034
}
3135

3236
// Is it a known regular property?
33-
const camelCasedProperty = camelCase(css);
34-
if (camelCasedProperty in properties) {
37+
if (camelCaseProperty(css as CssProperty) in properties) {
3538
return [
3639
{
3740
selector: "selector",
38-
property: css as StyleProperty,
41+
property: camelCaseProperty(css as CssProperty),
3942
value: { type: "unset", value: "" },
4043
},
4144
];
@@ -52,20 +55,29 @@ export const parseStyleInput = (css: string): Array<ParsedStyleDecl> => {
5255
];
5356
}
5457

55-
const styles = parseCss(`selector{${css}}`);
58+
const hyphenatedStyles = parseCss(`selector{${css}}`);
59+
const newStyles: StyleDecl[] = [];
5660

57-
for (const style of styles) {
61+
for (const { property, ...styleDecl } of hyphenatedStyles) {
5862
// somethingunknown: red; -> --somethingunknown: red;
5963
if (
6064
// Note: currently in tests it returns unparsed, but in the client it returns invalid,
6165
// because we use native APIs when available in parseCss.
62-
style.value.type === "invalid" ||
63-
(style.value.type === "unparsed" &&
64-
style.property.startsWith("--") === false)
66+
styleDecl.value.type === "invalid" ||
67+
(styleDecl.value.type === "unparsed" &&
68+
property.startsWith("--") === false)
6569
) {
66-
style.property = `--${style.property}`;
70+
newStyles.push({
71+
...styleDecl,
72+
property: `--${property}`,
73+
});
74+
} else {
75+
newStyles.push({
76+
...styleDecl,
77+
property: camelCaseProperty(property),
78+
});
6779
}
6880
}
6981

70-
return styles;
82+
return newStyles;
7183
};

apps/builder/app/builder/features/style-panel/shared/css-fragment.tsx

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,9 @@ import {
77
completionKeymap,
88
type CompletionSource,
99
} from "@codemirror/autocomplete";
10-
import { parseCss } from "@webstudio-is/css-data";
10+
import { camelCaseProperty, parseCss } from "@webstudio-is/css-data";
1111
import { css as style } from "@webstudio-is/design-system";
12+
import type { StyleProperty, StyleValue } from "@webstudio-is/css-engine";
1213
import {
1314
EditorContent,
1415
EditorDialog,
@@ -18,7 +19,10 @@ import {
1819
} from "~/builder/shared/code-editor-base";
1920
import { $availableVariables } from "./model";
2021

21-
export const parseCssFragment = (css: string, fallbacks: string[]) => {
22+
export const parseCssFragment = (
23+
css: string,
24+
fallbacks: string[]
25+
): Map<StyleProperty, StyleValue> => {
2226
let parsed = parseCss(`.styles{${css}}`);
2327
if (parsed.length === 0) {
2428
for (const fallbackProperty of fallbacks) {
@@ -30,7 +34,10 @@ export const parseCssFragment = (css: string, fallbacks: string[]) => {
3034
}
3135
}
3236
return new Map(
33-
parsed.map((styleDecl) => [styleDecl.property, styleDecl.value])
37+
parsed.map((styleDecl) => [
38+
camelCaseProperty(styleDecl.property),
39+
styleDecl.value,
40+
])
3441
);
3542
};
3643

apps/builder/app/shared/copy-paste/plugin-webflow/styles.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,11 @@ import type { WfAsset, WfElementNode, WfNode, WfStyle } from "./schema";
77
import { nanoid } from "nanoid";
88
import { $styleSources } from "~/shared/nano-states";
99
import {
10+
camelCaseProperty,
1011
parseCss,
1112
pseudoElements,
1213
type ParsedStyleDecl,
1314
} from "@webstudio-is/css-data";
14-
import { kebabCase } from "change-case";
1515
import { equalMedia, hyphenateProperty } from "@webstudio-is/css-engine";
1616
import type { WfStylePresets } from "./style-presets-overrides";
1717
import { builderApi } from "~/shared/builder-api";
@@ -103,7 +103,7 @@ const replaceAtImages = (
103103
};
104104

105105
const processStyles = (parsedStyles: ParsedStyleDecl[]) => {
106-
const styles = new Map();
106+
const styles = new Map<string, ParsedStyleDecl>();
107107
for (const parsedStyleDecl of parsedStyles) {
108108
const { breakpoint, selector, state, property } = parsedStyleDecl;
109109
const key = `${breakpoint}:${selector}:${state}:${property}`;
@@ -113,7 +113,7 @@ const processStyles = (parsedStyles: ParsedStyleDecl[]) => {
113113
const { breakpoint, selector, state, property } = parsedStyleDecl;
114114
const key = `${breakpoint}:${selector}:${state}:${property}`;
115115
styles.set(key, parsedStyleDecl);
116-
if (property === "backgroundClip") {
116+
if (property === "background-clip") {
117117
const colorKey = `${breakpoint}:${selector}:${state}:color`;
118118
styles.delete(colorKey);
119119
styles.set(colorKey, {
@@ -197,12 +197,12 @@ const addNodeStyles = ({
197197
fragment.styles.push({
198198
styleSourceId,
199199
breakpointId: breakpoint.id,
200-
property: style.property,
200+
property: camelCaseProperty(style.property),
201201
value: style.value,
202202
state: style.state,
203203
});
204204
if (style.value.type === "invalid") {
205-
const error = `Invalid style value: Local "${kebabCase(style.property)}: ${style.value.value}"`;
205+
const error = `Invalid style value: Local "${hyphenateProperty(style.property)}: ${style.value.value}"`;
206206
toast.error(error);
207207
console.error(error);
208208
}

apps/builder/app/shared/style-object-model.test.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import {
1010
getStyleDeclKey,
1111
} from "@webstudio-is/sdk";
1212
import { $, renderData } from "@webstudio-is/template";
13-
import { parseCss } from "@webstudio-is/css-data";
13+
import { camelCaseProperty, parseCss } from "@webstudio-is/css-data";
1414
import type { StyleValue } from "@webstudio-is/css-engine";
1515
import {
1616
type StyleObjectModel,
@@ -54,7 +54,7 @@ const createModel = ({
5454
styleSourceId: selector,
5555
breakpointId: breakpoint ?? "base",
5656
state,
57-
property,
57+
property: camelCaseProperty(property),
5858
value,
5959
};
6060
styles.set(getStyleDeclKey(styleDecl), styleDecl);

packages/ai/src/chains/operations/edit-styles.server.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1-
import { parseTailwindToWebstudio } from "@webstudio-is/css-data";
1+
import {
2+
camelCaseProperty,
3+
parseTailwindToWebstudio,
4+
} from "@webstudio-is/css-data";
25
import type { aiOperation, wsOperation } from "./edit-styles";
36

47
export { name } from "./edit-styles";
@@ -11,10 +14,14 @@ export const aiOperationToWs = async (
1114
if (operation.className === "") {
1215
throw new Error(`Operation ${operation.operation} className is empty`);
1316
}
14-
const styles = await parseTailwindToWebstudio(operation.className);
17+
const hyphenatedStyles = await parseTailwindToWebstudio(operation.className);
18+
const newStyles = hyphenatedStyles.map(({ property, ...styleDecl }) => ({
19+
...styleDecl,
20+
property: camelCaseProperty(property),
21+
}));
1522
return {
1623
operation: "applyStyles",
1724
instanceIds: operation.wsIds,
18-
styles: styles,
25+
styles: newStyles,
1926
};
2027
};

packages/css-data/bin/css-to-ws.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import { parseArgs, type ParseArgsConfig } from "node:util";
33
import * as path from "node:path";
44
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
5-
import { parseCss } from "../src/parse-css";
5+
import { camelCaseProperty, parseCss } from "../src/parse-css";
66

77
const cliOptions = {
88
allowPositionals: true,
@@ -34,7 +34,13 @@ const objectGroupBy = <Item>(list: Item[], by: (item: Item) => string) => {
3434
};
3535

3636
const css = readFileSync(sourcePath, "utf8");
37-
const parsed = parseCss(css);
37+
const parsed = parseCss(css).map(({ property, ...styleDecl }) => ({
38+
selector: styleDecl.selector,
39+
breakpoint: styleDecl.breakpoint,
40+
state: styleDecl.state,
41+
property: camelCaseProperty(property),
42+
value: styleDecl.value,
43+
}));
3844
const records = objectGroupBy(parsed, (item) => item.selector);
3945
mkdirSync(path.dirname(destinationPath), { recursive: true });
4046
const code = `/* eslint-disable */

packages/css-data/bin/html.css.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
22
import type { StyleValue } from "@webstudio-is/css-engine";
3-
import { parseCss } from "../src/parse-css";
3+
import { camelCaseProperty, parseCss } from "../src/parse-css";
44

55
const css = readFileSync("./src/html.css", "utf8");
66
const parsed = parseCss(css);
77
const result: [string, StyleValue][] = [];
88
for (const styleDecl of parsed) {
9-
result.push([`${styleDecl.selector}:${styleDecl.property}`, styleDecl.value]);
9+
result.push([
10+
`${styleDecl.selector}:${camelCaseProperty(styleDecl.property)}`,
11+
styleDecl.value,
12+
]);
1013
}
1114
let code = "";
1215
code += `import type { HtmlTags } from "html-tags";\n`;

packages/css-data/bin/mdn-data.ts

Lines changed: 20 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -7,28 +7,18 @@ import properties from "mdn-data/css/properties.json";
77
import syntaxes from "mdn-data/css/syntaxes.json";
88
import selectors from "mdn-data/css/selectors.json";
99
import data from "css-tree/dist/data";
10-
import { camelCase } from "change-case";
11-
import type {
12-
KeywordValue,
13-
StyleValue,
14-
Unit,
15-
UnitValue,
16-
UnparsedValue,
17-
FontFamilyValue,
10+
import {
11+
type KeywordValue,
12+
type StyleValue,
13+
type Unit,
14+
type UnitValue,
15+
type UnparsedValue,
16+
type FontFamilyValue,
17+
hyphenateProperty,
18+
type CssProperty,
1819
} from "@webstudio-is/css-engine";
1920
import * as customData from "../src/custom-data";
20-
21-
/**
22-
* Store prefixed properties without change
23-
* and convert to camel case only unprefixed properties
24-
* @todo stop converting to camel case and use hyphenated format
25-
*/
26-
const normalizePropertyName = (property: string) => {
27-
if (property.startsWith("-")) {
28-
return property;
29-
}
30-
return camelCase(property);
31-
};
21+
import { camelCaseProperty } from "../src/parse-css";
3222

3323
const units: Record<string, Array<string>> = {
3424
number: [],
@@ -233,7 +223,7 @@ const walkSyntax = (
233223
walk(parsed);
234224
};
235225

236-
type FilteredProperties = { [property in Property]: Value };
226+
type FilteredProperties = { [property: string]: Value };
237227

238228
const experimentalProperties = [
239229
"appearance",
@@ -299,7 +289,7 @@ const propertiesData = {
299289
...customData.propertiesData,
300290
};
301291

302-
let property: Property;
292+
let property: string;
303293
for (property in filteredProperties) {
304294
const config = filteredProperties[property];
305295
const unitGroups = new Set<string>();
@@ -326,7 +316,7 @@ for (property in filteredProperties) {
326316
);
327317
}
328318

329-
propertiesData[normalizePropertyName(property)] = {
319+
propertiesData[camelCaseProperty(property as CssProperty)] = {
330320
unitGroups: Array.from(unitGroups),
331321
inherited: config.inherited,
332322
initial: parseInitialValue(property, config.initial, unitGroups),
@@ -367,7 +357,7 @@ const keywordValues = (() => {
367357
const result = { ...customData.keywordValues };
368358

369359
for (const property in filteredProperties) {
370-
const key = normalizePropertyName(property);
360+
const key = camelCaseProperty(property as CssProperty);
371361
// prevent merging with custom keywords
372362
if (result[key]) {
373363
continue;
@@ -416,10 +406,14 @@ writeToFile("pseudo-elements.ts", "pseudoElements", pseudoElements);
416406

417407
let types = "";
418408

419-
const propertyLiterals = Object.keys(propertiesData).map((property) =>
409+
const camelCasedProperties = Object.keys(propertiesData).map((property) =>
420410
JSON.stringify(property)
421411
);
422-
types += `export type Property = ${propertyLiterals.join(" | ")};\n\n`;
412+
types += `export type CamelCasedProperty = ${camelCasedProperties.join(" | ")};\n\n`;
413+
const hyphenatedProperties = Object.keys(propertiesData).map((property) =>
414+
JSON.stringify(hyphenateProperty(property))
415+
);
416+
types += `export type HyphenatedProperty = ${hyphenatedProperties.join(" | ")};\n\n`;
423417

424418
const unitLiterals = Object.values(units)
425419
.flat()

0 commit comments

Comments
 (0)