Files
webstudio/packages/css-data/bin/mdn-data.ts
T
Bogdan Chadkin 2af8d57b8e experimental: support transition-behavior property (#3650)
This is the new property and not supported by csstree out of the box. So
we try to parse it manually. Fixes a few invalid in webflow preset.
2024-06-30 15:15:11 +03:00

435 lines
11 KiB
TypeScript
Executable File

import { mkdirSync, writeFileSync } from "node:fs";
import { join, dirname } from "node:path";
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore @todo add missing type defitions for definitionSyntax, type DSNode, type CssNode
import { parse, definitionSyntax, type DSNode, type CssNode } from "css-tree";
import properties from "mdn-data/css/properties.json";
import syntaxes from "mdn-data/css/syntaxes.json";
import selectors from "mdn-data/css/selectors.json";
import data from "css-tree/dist/data";
import { camelCase } from "change-case";
import type {
KeywordValue,
StyleValue,
Unit,
UnitValue,
UnparsedValue,
} from "@webstudio-is/css-engine";
import * as customData from "../src/custom-data";
/**
* Store prefixed properties without change
* and convert to camel case only unprefixed properties
* @todo stop converting to camel case and use hyphenated format
*/
const normalizePropertyName = (property: string) => {
if (property.startsWith("-")) {
return property;
}
return camelCase(property);
};
const units: Record<customData.UnitGroup, Array<string>> = {
number: [],
// consider % as unit
percentage: ["%"],
...data.units,
};
type Property = keyof typeof properties;
type Value = (typeof properties)[Property];
const inheritValue = {
type: "keyword",
value: "inherit",
} as const;
const autoValue = {
type: "keyword",
value: "auto",
} as const;
// Normalize browser dependant properties.
const normalizedValues = {
"font-family": inheritValue,
"font-size": inheritValue,
"line-height": inheritValue,
// startOrNamelessValueIfLTRRightIfRTL
"text-align": { type: "keyword", value: "start" },
// canvastext
color: { type: "keyword", value: "black" },
"column-gap": {
type: "unit",
value: 0,
unit: "px",
},
"row-gap": {
type: "unit",
value: 0,
unit: "px",
},
"background-size": autoValue,
"text-size-adjust": autoValue,
} as const;
const beautifyKeyword = (_property: string, keyword: string) => {
/*
* The default value is `invert` or `currentcolor` for some css properties.
* But that isn't supported in all browsers, example outline-color.
* So, going with currentColor for consistency.
* https://developer.mozilla.org/en-US/docs/Web/CSS/outline-color#formal_definition
*/
if (keyword === "currentcolor" || keyword === "invertOrCurrentColor") {
return "currentColor";
}
return keyword;
};
const convertToStyleValue = (
node: CssNode,
property: string,
value: string,
unitGroups: Set<customData.UnitGroup>
): undefined | UnitValue | KeywordValue | UnparsedValue => {
if (node?.type === "Identifier") {
return {
type: "keyword",
value: beautifyKeyword(property, node.name),
};
}
if (node?.type === "Number") {
let unit: Unit = "number";
// set explicit unit when 0 initial is specified without unit
if (unitGroups.has("number") === false) {
if (unitGroups.has("length")) {
unit = "px";
} else {
throw Error(
`Cannot infer unit for "${value}" initial value of ${property} property`
);
}
}
return {
type: "unit",
unit,
value: Number(node.value),
};
}
if (node?.type === "Percentage") {
return {
type: "unit",
unit: "%",
value: Number(node.value),
};
}
if (node?.type === "Dimension") {
return {
type: "unit",
unit: node.unit as Unit,
value: Number(node.value),
};
}
};
const parseInitialValue = (
property: string,
value: string,
unitGroups: Set<customData.UnitGroup>
): StyleValue => {
// Our default values hardcoded because no single standard
if (property in normalizedValues) {
return normalizedValues[property as keyof typeof normalizedValues];
}
const ast = parse(value, { context: "value" });
if (ast.type !== "Value") {
throw Error(`Unknown parsed type ${ast.type}`);
}
// more than 2 values consider as keyword
if (ast.children.first !== ast.children.last) {
return {
type: "tuple",
value: ast.children.toArray().map((node) => {
const styleValue = convertToStyleValue(
node,
property,
value,
unitGroups
);
if (styleValue !== undefined) {
return styleValue;
}
throw Error(`Cannot find initial for ${property}`);
}),
};
}
const node = ast.children.first;
let styleValue: undefined | StyleValue;
if (node) {
styleValue = convertToStyleValue(node, property, value, unitGroups);
}
if (styleValue !== undefined) {
return styleValue;
}
throw Error(`Cannot find initial for ${property}`);
};
const walkSyntax = (
syntax: string,
enter: (node: DSNode) => void,
parsedSyntaxes = new Set<string>()
) => {
// fix cyclic syntaxes
if (parsedSyntaxes.has(syntax)) {
return;
}
parsedSyntaxes.add(syntax);
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore @todo add missing type defitions for definitionSyntax
const parsed = definitionSyntax.parse(syntax);
const walk = (node: DSNode) => {
if (node.type === "Group") {
for (const term of node.terms) {
// skip functions and their content as complex values
if (term.type === "Function") {
break;
}
walk(term);
}
return;
}
if (node.type === "Multiplier") {
walk(node.term);
return;
}
if (node.type === "Type") {
if (node.name === "deprecated-system-color") {
return;
}
const nestedSyntax = syntaxes[node.name as keyof typeof syntaxes]?.syntax;
if (nestedSyntax === undefined) {
enter(node);
} else {
// resolve nested syntaxes
walkSyntax(nestedSyntax, enter, parsedSyntaxes);
}
return;
}
if (node.type === "Property") {
// resolve other properties references
if (node.name in properties) {
walkSyntax(
properties[node.name as Property].syntax,
enter,
parsedSyntaxes
);
}
return;
}
enter(node);
};
walk(parsed);
};
type FilteredProperties = { [property in Property]: Value };
const experimentalProperties = [
"appearance",
"aspect-ratio",
"text-size-adjust",
"-webkit-line-clamp",
"background-position-x",
"background-position-y",
"-webkit-tap-highlight-color",
"-webkit-overflow-scrolling",
"transition-behavior",
];
const unsupportedProperties = [
"--*",
// shorthand properties
"all",
"font-synthesis",
"font-variant",
"overflow",
"white-space",
"text-wrap",
"background-position",
];
const animatableProperties: string[] = [];
const filteredProperties: FilteredProperties = (() => {
let property: Property;
const result = {} as FilteredProperties;
for (property in properties) {
const config = properties[property];
const isSupportedProperty =
// make sure the property standard and described in mdn
(config.status === "standard" && "mdn_url" in config) ||
experimentalProperties.includes(property);
const isShorthandProperty = Array.isArray(config.initial);
const isAnimatableProperty =
property.startsWith("-") === false &&
config.animationType !== "discrete" &&
config.animationType !== "notAnimatable";
if (unsupportedProperties.includes(property) || isShorthandProperty) {
continue;
}
if (isSupportedProperty) {
if (isAnimatableProperty) {
animatableProperties.push(property);
}
result[property as Property] = config;
}
}
return result;
})();
const propertiesData = {
...customData.propertiesData,
};
let property: Property;
for (property in filteredProperties) {
const config = filteredProperties[property];
// collect node types to improve parsing of css values
const unitGroups = new Set<customData.UnitGroup>();
const types = new Set<customData.RawPropertyData["types"][number]>();
walkSyntax(config.syntax, (node) => {
if (node.type === "Type") {
const name = node.name as customData.RawPropertyData["types"][number];
if (customData.valueTypes.includes(name) === false) {
throw Error(`Unknown value type "${node.name}"`);
}
types.add(name);
if (node.name === "integer" || node.name === "number") {
unitGroups.add("number");
return;
}
// type names match unit groups
if (node.name in units) {
unitGroups.add(node.name as customData.UnitGroup);
return;
}
}
});
if (Array.isArray(config.initial)) {
throw new Error(
`Property ${property} contains non string initial value ${config.initial.join(
", "
)}`
);
}
propertiesData[normalizePropertyName(property)] = {
unitGroups: Array.from(unitGroups),
inherited: config.inherited,
initial: parseInitialValue(property, config.initial, unitGroups),
types: Array.from(types),
};
}
const pseudoElements = Object.keys(selectors)
.filter((selector) => {
return selector.startsWith("::");
})
.map((selector) => selector.slice(2));
const targetDir = join(process.cwd(), process.argv.slice(2).pop() as string);
mkdirSync(targetDir, { recursive: true });
const writeToFile = (fileName: string, constant: string, data: unknown) => {
const autogeneratedHint = "// This file was generated by pnpm mdn-data\n";
const content =
autogeneratedHint +
`export const ${constant} = ` +
JSON.stringify(data, null, 2) +
" as const;";
writeFileSync(join(targetDir, fileName), content, "utf8");
};
// Non-standard properties are just missing in mdn data
const nonStandardValues = {
"background-clip": ["text"],
};
// https://www.w3.org/TR/css-values/#common-keywords
const commonKeywords = ["initial", "inherit", "unset"];
const keywordValues = (() => {
const result = { ...customData.keywordValues };
for (const property in filteredProperties) {
const keywords = new Set<string>();
walkSyntax(
filteredProperties[property as keyof typeof filteredProperties].syntax,
(node) => {
if (node.type === "Keyword") {
keywords.add(beautifyKeyword(property, node.name));
}
}
);
if (property in nonStandardValues) {
for (const nonStandartKeyword of nonStandardValues[
property as keyof typeof nonStandardValues
]) {
keywords.add(nonStandartKeyword);
}
}
for (const commonKeyword of commonKeywords) {
// Delete to add commonKeyword at the end of the set
keywords.delete(commonKeyword);
keywords.add(commonKeyword);
}
if (keywords.size !== 0) {
const key = normalizePropertyName(property);
result[key] = [...(result[key] ?? []), ...keywords];
}
}
return result;
})();
writeToFile("units.ts", "units", units);
writeToFile("properties.ts", "properties", propertiesData);
writeToFile("keyword-values.ts", "keywordValues", keywordValues);
writeToFile(
"animatable-properties.ts",
"animatableProperties",
animatableProperties
);
writeToFile("pseudo-elements.ts", "pseudoElements", pseudoElements);
let types = "";
const propertyLiterals = Object.keys(propertiesData).map((property) =>
JSON.stringify(property)
);
types += `export type Property = ${propertyLiterals.join(" | ")};\n\n`;
const unitLiterals = Object.values(units)
.flat()
.map((unit) => JSON.stringify(unit));
types += `export type Unit = ${unitLiterals.join(" | ")};\n`;
const typesFile = join(
process.cwd(),
"../css-engine/src/__generated__/types.ts"
);
mkdirSync(dirname(typesFile), { recursive: true });
writeFileSync(typesFile, types);