@webstudio-is/css-data 0.65.0 → 0.67.0

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.
Files changed (47) hide show
  1. package/lib/__generated__/keyword-values.js +2 -2
  2. package/lib/__generated__/properties.js +2 -2
  3. package/lib/cjs/__generated__/keyword-values.js +2 -2
  4. package/lib/cjs/__generated__/properties.js +2 -2
  5. package/lib/cjs/html.js +1 -3
  6. package/lib/cjs/index.js +3 -0
  7. package/lib/cjs/parse-css-value.js +152 -0
  8. package/lib/cjs/parse-css.js +125 -0
  9. package/lib/cjs/property-parsers/background.js +105 -0
  10. package/lib/cjs/property-parsers/index.js +18 -0
  11. package/lib/cjs/property-parsers/parsers.js +24 -0
  12. package/lib/cjs/property-parsers/to-longhand.js +24 -0
  13. package/lib/html.js +1 -3
  14. package/lib/index.js +3 -0
  15. package/lib/parse-css-value.js +122 -0
  16. package/lib/parse-css.js +95 -0
  17. package/lib/property-parsers/background.js +75 -0
  18. package/lib/property-parsers/index.js +1 -0
  19. package/lib/property-parsers/parsers.js +4 -0
  20. package/lib/property-parsers/to-longhand.js +4 -0
  21. package/lib/types/src/__generated__/keyword-values.d.ts +1 -1
  22. package/lib/types/src/__generated__/properties.d.ts +2 -2
  23. package/lib/types/src/index.d.ts +5 -2
  24. package/lib/types/src/parse-css-value.d.ts +3 -0
  25. package/lib/types/src/parse-css-value.test.d.ts +1 -0
  26. package/lib/types/src/parse-css.d.ts +9 -0
  27. package/lib/types/src/parse-css.test.d.ts +1 -0
  28. package/lib/types/src/property-parsers/background.d.ts +11 -0
  29. package/lib/types/src/property-parsers/background.test.d.ts +1 -0
  30. package/lib/types/src/property-parsers/index.d.ts +1 -0
  31. package/lib/types/src/property-parsers/parsers.d.ts +1 -0
  32. package/lib/types/src/property-parsers/to-longhand.d.ts +1 -0
  33. package/package.json +12 -4
  34. package/src/__generated__/keyword-values.ts +2 -2
  35. package/src/__generated__/properties.ts +2 -2
  36. package/src/html.ts +1 -3
  37. package/src/index.ts +6 -0
  38. package/src/parse-css-value.test.ts +136 -0
  39. package/src/parse-css-value.ts +157 -0
  40. package/src/parse-css.test.ts +101 -0
  41. package/src/parse-css.ts +124 -0
  42. package/src/property-parsers/README.md +11 -0
  43. package/src/property-parsers/background.test.ts +184 -0
  44. package/src/property-parsers/background.ts +132 -0
  45. package/src/property-parsers/index.ts +1 -0
  46. package/src/property-parsers/parsers.ts +4 -0
  47. package/src/property-parsers/to-longhand.ts +4 -0
@@ -0,0 +1,95 @@
1
+ import * as csstree from "css-tree";
2
+ import { parseCssValue as parseCssValueLonghand } from "./parse-css-value";
3
+ import * as parsers from "./property-parsers/parsers";
4
+ import * as toLonghand from "./property-parsers/to-longhand";
5
+ import { StyleValue } from "./schema";
6
+ const parseCssValue = function parseCssValue2(property, value) {
7
+ const unwrap = toLonghand[property];
8
+ if (typeof unwrap === "function") {
9
+ const longhands = unwrap(value);
10
+ return Object.fromEntries(
11
+ Object.entries(longhands).map(([property2, value2]) => {
12
+ const valueParser = parsers[property2];
13
+ if (typeof valueParser === "function") {
14
+ return [property2, valueParser(value2)];
15
+ }
16
+ if (Array.isArray(value2)) {
17
+ return [
18
+ property2,
19
+ {
20
+ type: "invalid",
21
+ value: value2.join("")
22
+ }
23
+ ];
24
+ }
25
+ if (!value2) {
26
+ return [property2, { type: "invalid", value: "" }];
27
+ }
28
+ return [
29
+ property2,
30
+ parseCssValueLonghand(property2, value2)
31
+ ];
32
+ })
33
+ );
34
+ }
35
+ return {
36
+ [property]: parseCssValueLonghand(property, value)
37
+ };
38
+ };
39
+ const parseCss = function cssToWS(css) {
40
+ const ast = csstree.parse(css);
41
+ let selectors = [];
42
+ const styles = {};
43
+ csstree.walk(ast, (node, item) => {
44
+ if (node.type === "SelectorList") {
45
+ selectors = [];
46
+ }
47
+ if (node.type === "ClassSelector") {
48
+ if (!item.prev && !item.next) {
49
+ selectors.push(node.name);
50
+ }
51
+ return;
52
+ }
53
+ if (node.type === "Declaration") {
54
+ const stringValue = csstree.generate(node.value);
55
+ const parsedCss = parseCssValue(
56
+ node.property,
57
+ stringValue
58
+ );
59
+ Object.entries(parsedCss).forEach(
60
+ ([property, value]) => {
61
+ try {
62
+ StyleValue.parse(value);
63
+ selectors.forEach((selector) => {
64
+ if (Array.isArray(styles[selector])) {
65
+ styles[selector].push({
66
+ property,
67
+ value
68
+ });
69
+ } else {
70
+ styles[selector] = [{ property, value }];
71
+ }
72
+ });
73
+ } catch (error) {
74
+ if (true) {
75
+ console.warn(
76
+ true,
77
+ `Declaration parsing for \`${selectors.join(", ")}.${node.property}: ${stringValue}\` failed:
78
+
79
+ ${JSON.stringify(
80
+ parsedCss,
81
+ null,
82
+ 2
83
+ )}`
84
+ );
85
+ }
86
+ }
87
+ }
88
+ );
89
+ }
90
+ });
91
+ return styles;
92
+ };
93
+ export {
94
+ parseCss
95
+ };
@@ -0,0 +1,75 @@
1
+ import * as csstree from "css-tree";
2
+ import { parseCssValue } from "../parse-css-value";
3
+ const gradientNames = [
4
+ "conic-gradient",
5
+ "linear-gradient",
6
+ "radial-gradient",
7
+ "repeating-conic-gradient",
8
+ "repeating-linear-gradient",
9
+ "repeating-radial-gradient"
10
+ ];
11
+ const parseBackground = (background) => {
12
+ const { backgroundImage, backgroundColor: backgroundColorRaw } = backgroundToLonghand(background);
13
+ const backgroundColor = backgroundColorRaw ? parseCssValue("backgroundColor", backgroundColorRaw) : void 0;
14
+ return {
15
+ backgroundImage: parseBackgroundImage(backgroundImage),
16
+ backgroundColor: backgroundColor && backgroundColor.type === "rgb" ? backgroundColor : void 0
17
+ };
18
+ };
19
+ const backgroundToLonghand = (background) => {
20
+ const layers = [];
21
+ let tokenStream = background.trim();
22
+ tokenStream = tokenStream.endsWith(";") ? tokenStream.slice(0, -1) : tokenStream;
23
+ const cleanupKeywords = ["background:", "background-image:"];
24
+ for (const cleanupKeyword of cleanupKeywords) {
25
+ tokenStream = tokenStream.startsWith(cleanupKeyword) ? tokenStream.slice(cleanupKeyword.length).trim() : tokenStream;
26
+ }
27
+ const cssAst = csstree.parse(tokenStream, { context: "value" });
28
+ let backgroundColorRaw;
29
+ let nestingLevel = 0;
30
+ csstree.walk(cssAst, {
31
+ enter: (node, item, list) => {
32
+ if (node.type === "Function") {
33
+ if (gradientNames.includes(node.name)) {
34
+ layers.push(csstree.generate(node));
35
+ }
36
+ if (item.next === null && nestingLevel === 0) {
37
+ backgroundColorRaw = csstree.generate(node);
38
+ }
39
+ nestingLevel++;
40
+ }
41
+ if (node.type === "Hash" && item.next === null && nestingLevel === 0) {
42
+ backgroundColorRaw = csstree.generate(node);
43
+ }
44
+ },
45
+ leave: (node, item, list) => {
46
+ if (node.type === "Function") {
47
+ nestingLevel--;
48
+ }
49
+ }
50
+ });
51
+ return {
52
+ backgroundImage: layers,
53
+ backgroundColor: backgroundColorRaw
54
+ };
55
+ };
56
+ const parseBackgroundImage = (layers) => {
57
+ const backgroundImages = [];
58
+ for (const layer of layers) {
59
+ if (gradientNames.some((gradientName) => layer.startsWith(gradientName)) === false) {
60
+ break;
61
+ }
62
+ const layerStyle = parseCssValue("backgroundImage", layer);
63
+ if (layerStyle.type !== "unparsed") {
64
+ break;
65
+ }
66
+ backgroundImages.push(layerStyle);
67
+ }
68
+ return backgroundImages.length > 0 ? { type: "layers", value: backgroundImages } : { type: "invalid", value: layers.join(",") };
69
+ };
70
+ export {
71
+ backgroundToLonghand,
72
+ gradientNames,
73
+ parseBackground,
74
+ parseBackgroundImage
75
+ };
@@ -0,0 +1 @@
1
+ export * from "./background";
@@ -0,0 +1,4 @@
1
+ import { parseBackgroundImage } from "./background";
2
+ export {
3
+ parseBackgroundImage as backgroundImage
4
+ };
@@ -0,0 +1,4 @@
1
+ import { backgroundToLonghand } from "./background";
2
+ export {
3
+ backgroundToLonghand as background
4
+ };
@@ -139,7 +139,7 @@ export declare const keywordValues: {
139
139
  readonly fontVariantLigatures: readonly ["normal", "none", "common-ligatures", "no-common-ligatures", "discretionary-ligatures", "no-discretionary-ligatures", "historical-ligatures", "no-historical-ligatures", "contextual", "no-contextual", "initial", "inherit", "unset"];
140
140
  readonly fontVariantNumeric: readonly ["normal", "lining-nums", "oldstyle-nums", "proportional-nums", "tabular-nums", "diagonal-fractions", "stacked-fractions", "ordinal", "slashed-zero", "initial", "inherit", "unset"];
141
141
  readonly fontVariantPosition: readonly ["normal", "sub", "super", "initial", "inherit", "unset"];
142
- readonly fontWeight: readonly ["400", "700", "bolder", "lighter", "initial", "inherit", "unset"];
142
+ readonly fontWeight: readonly ["normal", "bold", "bolder", "lighter", "initial", "inherit", "unset"];
143
143
  readonly forcedColorAdjust: readonly ["auto", "none", "initial", "inherit", "unset"];
144
144
  readonly gridAutoColumns: readonly ["min-content", "max-content", "auto", "initial", "inherit", "unset"];
145
145
  readonly gridAutoFlow: readonly ["row", "column", "dense", "initial", "inherit", "unset"];
@@ -1438,7 +1438,7 @@ export declare const properties: {
1438
1438
  readonly inherited: true;
1439
1439
  readonly initial: {
1440
1440
  readonly type: "keyword";
1441
- readonly value: "400";
1441
+ readonly value: "normal";
1442
1442
  };
1443
1443
  readonly popularity: 0.88598106;
1444
1444
  readonly appliesTo: "allElements";
@@ -2355,7 +2355,7 @@ export declare const properties: {
2355
2355
  readonly inherited: false;
2356
2356
  readonly initial: {
2357
2357
  readonly type: "keyword";
2358
- readonly value: "invertOrCurrentColor";
2358
+ readonly value: "currentColor";
2359
2359
  };
2360
2360
  readonly popularity: 0.09538741;
2361
2361
  readonly appliesTo: "allElements";
@@ -3,6 +3,9 @@ export declare const html: Html;
3
3
  export * from "./__generated__/keyword-values";
4
4
  export * from "./__generated__/units";
5
5
  export * from "./schema";
6
+ export * from "./property-parsers/index";
7
+ export * from "./parse-css-value";
8
+ export * from "./parse-css";
6
9
  export declare const properties: import("type-fest/source/writable-deep").WritableObjectDeep<{
7
10
  readonly accentColor: {
8
11
  readonly unitGroups: readonly [];
@@ -1443,7 +1446,7 @@ export declare const properties: import("type-fest/source/writable-deep").Writab
1443
1446
  readonly inherited: true;
1444
1447
  readonly initial: {
1445
1448
  readonly type: "keyword";
1446
- readonly value: "400";
1449
+ readonly value: "normal";
1447
1450
  };
1448
1451
  readonly popularity: 0.88598106;
1449
1452
  readonly appliesTo: "allElements";
@@ -2360,7 +2363,7 @@ export declare const properties: import("type-fest/source/writable-deep").Writab
2360
2363
  readonly inherited: false;
2361
2364
  readonly initial: {
2362
2365
  readonly type: "keyword";
2363
- readonly value: "invertOrCurrentColor";
2366
+ readonly value: "currentColor";
2364
2367
  };
2365
2368
  readonly popularity: 0.09538741;
2366
2369
  readonly appliesTo: "allElements";
@@ -0,0 +1,3 @@
1
+ import type { StyleProperty, StyleValue } from "./schema";
2
+ export declare const isValidDeclaration: (property: string, value: string) => boolean;
3
+ export declare const parseCssValue: (property: StyleProperty, input: string) => StyleValue;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,9 @@
1
+ import { StyleValue, type StyleProperty } from "./schema";
2
+ type Selector = string;
3
+ type Style = {
4
+ property: StyleProperty;
5
+ value: StyleValue;
6
+ };
7
+ export type Styles = Record<Selector, Style[]>;
8
+ export declare const parseCss: (css: string) => Styles;
9
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,11 @@
1
+ import type { InvalidValue, LayersValue, RgbValue } from "../schema";
2
+ export declare const gradientNames: string[];
3
+ export declare const parseBackground: (background: string) => {
4
+ backgroundImage: LayersValue | InvalidValue;
5
+ backgroundColor: RgbValue | undefined;
6
+ };
7
+ export declare const backgroundToLonghand: (background: string) => {
8
+ backgroundImage: string[];
9
+ backgroundColor: string | undefined;
10
+ };
11
+ export declare const parseBackgroundImage: (layers: string[]) => LayersValue | InvalidValue;
@@ -0,0 +1 @@
1
+ export * from "./background";
@@ -0,0 +1 @@
1
+ export { parseBackgroundImage as backgroundImage } from "./background";
@@ -0,0 +1 @@
1
+ export { backgroundToLonghand as background } from "./background";
package/package.json CHANGED
@@ -1,21 +1,22 @@
1
1
  {
2
2
  "name": "@webstudio-is/css-data",
3
- "version": "0.65.0",
3
+ "version": "0.67.0",
4
4
  "description": "CSS Data",
5
5
  "author": "Webstudio <github@webstudio.is>",
6
6
  "homepage": "https://webstudio.is",
7
7
  "type": "module",
8
8
  "devDependencies": {
9
- "@types/css-tree": "^2.0.0",
9
+ "@types/css-tree": "^2.3.1",
10
10
  "camelcase": "^6.3.0",
11
- "css-tree": "^2.3.1",
12
11
  "html-tags": "^3.2.0",
12
+ "jest": "^29.3.1",
13
13
  "mdn-data": "2.0.30",
14
14
  "tsx": "^3.12.6",
15
15
  "type-fest": "^3.7.1",
16
16
  "typescript": "5.0.3",
17
17
  "zod": "^3.19.1",
18
18
  "@webstudio-is/scripts": "^0.0.0",
19
+ "@webstudio-is/jest-config": "^1.0.6",
19
20
  "@webstudio-is/tsconfig": "^1.0.6"
20
21
  },
21
22
  "peerDependencies": {
@@ -35,6 +36,12 @@
35
36
  "license": "MIT",
36
37
  "private": false,
37
38
  "sideEffects": false,
39
+ "dependencies": {
40
+ "colord": "^2.9.3",
41
+ "hyphenate-style-name": "^1.0.4",
42
+ "warn-once": "^0.1.1",
43
+ "css-tree": "^2.3.1"
44
+ },
38
45
  "scripts": {
39
46
  "typecheck": "tsc --noEmit --emitDeclarationOnly false",
40
47
  "checks": "pnpm typecheck && pnpm lint",
@@ -42,6 +49,7 @@
42
49
  "build": "build-package",
43
50
  "build:mdn-data": "tsx ./bin/mdn-data.ts ./src/__generated__ && prettier --write \"./src/__generated__/**/*.ts\"",
44
51
  "dts": "tsc --declarationDir lib/types",
45
- "lint": "eslint ./src --ext .ts,.tsx --max-warnings 0"
52
+ "lint": "eslint ./src --ext .ts,.tsx --max-warnings 0",
53
+ "test": "NODE_OPTIONS=--experimental-vm-modules jest"
46
54
  }
47
55
  }
@@ -3836,8 +3836,8 @@ export const keywordValues = {
3836
3836
  "unset",
3837
3837
  ],
3838
3838
  fontWeight: [
3839
- "400",
3840
- "700",
3839
+ "normal",
3840
+ "bold",
3841
3841
  "bolder",
3842
3842
  "lighter",
3843
3843
  "initial",
@@ -1445,7 +1445,7 @@ export const properties = {
1445
1445
  inherited: true,
1446
1446
  initial: {
1447
1447
  type: "keyword",
1448
- value: "400",
1448
+ value: "normal",
1449
1449
  },
1450
1450
  popularity: 0.88598106,
1451
1451
  appliesTo: "allElements",
@@ -2365,7 +2365,7 @@ export const properties = {
2365
2365
  inherited: false,
2366
2366
  initial: {
2367
2367
  type: "keyword",
2368
- value: "invertOrCurrentColor",
2368
+ value: "currentColor",
2369
2369
  },
2370
2370
  popularity: 0.09538741,
2371
2371
  appliesTo: "allElements",
package/src/html.ts CHANGED
@@ -48,9 +48,7 @@ const pl40px: Styles[number] = {
48
48
 
49
49
  const fontWeightBold: Styles[number] = {
50
50
  property: "fontWeight",
51
- // in browsers defined as bold
52
- // though builder accepts only numeric values
53
- value: { type: "keyword", value: "700" },
51
+ value: { type: "keyword", value: "bold" },
54
52
  };
55
53
 
56
54
  const fontStyleItalic: Styles[number] = {
package/src/index.ts CHANGED
@@ -7,6 +7,12 @@ export * from "./__generated__/keyword-values";
7
7
  export * from "./__generated__/units";
8
8
  export * from "./schema";
9
9
 
10
+ // longhand property parsers
11
+ export * from "./property-parsers/index";
12
+ // shorthand property parsers
13
+ export * from "./parse-css-value";
14
+ export * from "./parse-css";
15
+
10
16
  import { properties as generatedProperties } from "./__generated__/properties";
11
17
 
12
18
  // convert to writable to avoid conflicts with schema type
@@ -0,0 +1,136 @@
1
+ import { describe, test, expect } from "@jest/globals";
2
+ import { parseCssValue } from "./parse-css-value";
3
+
4
+ describe("Parse CSS value", () => {
5
+ describe("number value", () => {
6
+ test("unitless", () => {
7
+ expect(parseCssValue("lineHeight", "10")).toEqual({
8
+ type: "unit",
9
+ unit: "number",
10
+ value: 10,
11
+ });
12
+ });
13
+ });
14
+
15
+ describe("unit value", () => {
16
+ test("with unit", () => {
17
+ expect(parseCssValue("width", "10px")).toEqual({
18
+ type: "unit",
19
+ unit: "px",
20
+ value: 10,
21
+ });
22
+ });
23
+
24
+ test("empty input", () => {
25
+ expect(parseCssValue("width", "")).toEqual({
26
+ type: "invalid",
27
+ value: "",
28
+ });
29
+ });
30
+ });
31
+
32
+ describe("keyword value", () => {
33
+ test("keyword", () => {
34
+ expect(parseCssValue("width", "auto")).toEqual({
35
+ type: "keyword",
36
+ value: "auto",
37
+ });
38
+ });
39
+
40
+ test("keyword display block", () => {
41
+ expect(parseCssValue("display", "block")).toEqual({
42
+ type: "keyword",
43
+ value: "block",
44
+ });
45
+ });
46
+
47
+ test("keyword with unit", () => {
48
+ expect(parseCssValue("width", "autopx")).toEqual({
49
+ type: "invalid",
50
+ value: "autopx",
51
+ });
52
+ });
53
+
54
+ test("invalid", () => {
55
+ // This will return px as a fallback unit, as number is not valid for width.
56
+ expect(parseCssValue("width", "10")).toEqual({
57
+ type: "invalid",
58
+ value: "10",
59
+ });
60
+
61
+ // This will return number unit, as number is valid for aspectRatio.
62
+ expect(parseCssValue("aspectRatio", "10")).toEqual({
63
+ type: "unit",
64
+ unit: "number",
65
+ value: 10,
66
+ });
67
+ });
68
+ });
69
+
70
+ describe("Unparesd valid values", () => {
71
+ test("Simple valid function values", () => {
72
+ expect(parseCssValue("filter", "blur(4px)")).toEqual({
73
+ type: "unparsed",
74
+ value: "blur(4px)",
75
+ });
76
+
77
+ expect(parseCssValue("width", "calc(4px + 16em)")).toEqual({
78
+ type: "unparsed",
79
+ value: "calc(4px + 16em)",
80
+ });
81
+ });
82
+
83
+ test("Multiple valid function values", () => {
84
+ expect(
85
+ parseCssValue(
86
+ "filter",
87
+ "blur(4px) drop-shadow(16px 16px 20px blue) opacity(25%)"
88
+ )
89
+ ).toEqual({
90
+ type: "unparsed",
91
+ value: "blur(4px) drop-shadow(16px 16px 20px blue) opacity(25%)",
92
+ });
93
+
94
+ expect(parseCssValue("filter", "blur(calc(4px + 16em))")).toEqual({
95
+ type: "unparsed",
96
+ value: "blur(calc(4px + 16em))",
97
+ });
98
+ });
99
+
100
+ test("Invalid function values", () => {
101
+ expect(parseCssValue("width", "blur(4)")).toEqual({
102
+ type: "invalid",
103
+ value: "blur(4)",
104
+ });
105
+ });
106
+ });
107
+
108
+ describe("Colors", () => {
109
+ test("Color rgba values", () => {
110
+ expect(parseCssValue("backgroundColor", "rgba(0,0,0,0)")).toEqual({
111
+ type: "rgb",
112
+ alpha: 0,
113
+ b: 0,
114
+ g: 0,
115
+ r: 0,
116
+ });
117
+ });
118
+
119
+ test("Color rgba values", () => {
120
+ expect(parseCssValue("backgroundColor", "#00220011")).toEqual({
121
+ type: "rgb",
122
+ alpha: 0.07,
123
+ b: 0,
124
+ g: 34,
125
+ r: 0,
126
+ });
127
+ });
128
+
129
+ test("Color rgba values", () => {
130
+ expect(parseCssValue("color", "red")).toEqual({
131
+ type: "keyword",
132
+ value: "red",
133
+ });
134
+ });
135
+ });
136
+ });