Files
webstudio/packages/css-data/src/parse-css-value.ts
T
Ivan Starkov 3c75b4e870 feat: Extend tailwind-to-webstudio conversion to support background properties (#2387)
## Description

1. Add support of simple tuple types to the parseCssValue
2. Add background properties conversion

## Steps for reproduction

see tests

## Code Review

- [ ] hi @kof, I need you to do
  - conceptual review (architecture, feature-correctness)
  - detailed review (read every line)
  - test it on preview

## Before requesting a review

- [ ] made a self-review
- [ ] added inline comments where things may be not obvious (the "why",
not "what")

## Before merging

- [ ] tested locally and on preview environment (preview dev login:
5de6)
- [ ] updated [test
cases](https://github.com/webstudio-is/webstudio/blob/main/apps/builder/docs/test-cases.md)
document
- [ ] added tests
- [ ] if any new env variables are added, added them to `.env.example`
and the `builder/env-check.js` if mandatory
2023-09-28 19:54:56 +04:00

184 lines
4.3 KiB
TypeScript

import { colord } from "colord";
import * as csstree from "css-tree";
import hyphenate from "hyphenate-style-name";
import warnOnce from "warn-once";
import {
TupleValue,
type StyleProperty,
type StyleValue,
type Unit,
} from "@webstudio-is/css-engine";
import { keywordValues } from "./__generated__/keyword-values";
import { units } from "./__generated__/units";
const cssTryParseValue = (input: string) => {
try {
const ast = csstree.parse(input, { context: "value" });
return ast;
} catch {
return undefined;
}
};
export const isValidDeclaration = (
property: string,
value: string
): boolean => {
const ast = cssTryParseValue(value);
if (ast == null) {
return false;
}
const cssPropertyName = hyphenate(property);
const matchResult = csstree.lexer.matchProperty(cssPropertyName, ast);
const isValidDeclaration = matchResult.matched != null;
// @todo remove after fix https://github.com/csstree/csstree/issues/246
if (isValidDeclaration && typeof CSSStyleValue !== "undefined") {
try {
CSSStyleValue.parse(cssPropertyName, value);
} catch {
warnOnce(
true,
`Css property "${property}" with value "${value}" is invalid according to CSSStyleValue.parse
but valid according to csstree.lexer.matchProperty.`
);
return false;
}
}
return isValidDeclaration;
};
export const parseCssValue = (
property: StyleProperty, // Handles only long-hand values.
input: string
): StyleValue => {
const invalidValue = {
type: "invalid",
value: input,
} as const;
if (input.length === 0) {
return invalidValue;
}
if (!isValidDeclaration(property, input)) {
return invalidValue;
}
const ast = cssTryParseValue(input);
if (ast == null) {
warnOnce(
true,
`Can't parse css property "${property}" with value "${input}"`
);
return invalidValue;
}
if (
ast != null &&
ast.type === "Value" &&
ast.children.first === ast.children.last
) {
// Try extract units from 1st children
const first = ast.children.first;
if (first?.type === "Number") {
return {
type: "unit",
unit: "number",
value: Number(first.value),
};
}
if (first?.type === "Dimension") {
const unit = first.unit as (typeof units)[keyof typeof units][number];
for (const unitGroup of Object.values(units)) {
if (unitGroup.includes(unit as never)) {
return {
type: "unit",
unit: unit as Unit,
value: Number(first.value),
};
}
}
return invalidValue;
}
if (first?.type === "Percentage") {
return {
type: "unit",
unit: "%",
value: Number(first.value),
};
}
if (first?.type === "Identifier") {
const values = keywordValues[
property as keyof typeof keywordValues
] as ReadonlyArray<string>;
const lettersRegex = /[^a-zA-Z]+/g;
const searchValues = values.map((value) =>
value.replace(lettersRegex, "").toLowerCase()
);
const keywordInput = input.replace(lettersRegex, "").toLowerCase();
const index = searchValues.indexOf(keywordInput);
if (index > -1) {
return {
type: "keyword",
value: values[index]!,
};
}
}
}
// Probably a color (we can use csstree.lexer.matchProperty(cssPropertyName, ast) to extract the type but this looks much simpler)
if (property.toLocaleLowerCase().includes("color")) {
const mayBeColor = colord(input);
if (mayBeColor.isValid()) {
const rgb = mayBeColor.toRgb();
return {
type: "rgb",
alpha: rgb.a,
r: rgb.r,
g: rgb.g,
b: rgb.b,
};
}
}
// Probably a tuple like background-position
if (ast != null && ast.type === "Value" && ast.children.size === 2) {
const tupleFirst = parseCssValue(
property,
csstree.generate(ast.children.first!)
);
const tupleLast = parseCssValue(
property,
csstree.generate(ast.children.last!)
);
const tupleResult = TupleValue.safeParse({
type: "tuple",
value: [tupleFirst, tupleLast],
});
if (tupleResult.success) {
return tupleResult.data;
}
}
return {
type: "unparsed",
value: input,
};
};