@webstudio-is/css-data 0.66.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 (36) hide show
  1. package/lib/cjs/index.js +3 -0
  2. package/lib/cjs/parse-css-value.js +152 -0
  3. package/lib/cjs/parse-css.js +125 -0
  4. package/lib/cjs/property-parsers/background.js +105 -0
  5. package/lib/cjs/property-parsers/index.js +18 -0
  6. package/lib/cjs/property-parsers/parsers.js +24 -0
  7. package/lib/cjs/property-parsers/to-longhand.js +24 -0
  8. package/lib/index.js +3 -0
  9. package/lib/parse-css-value.js +122 -0
  10. package/lib/parse-css.js +95 -0
  11. package/lib/property-parsers/background.js +75 -0
  12. package/lib/property-parsers/index.js +1 -0
  13. package/lib/property-parsers/parsers.js +4 -0
  14. package/lib/property-parsers/to-longhand.js +4 -0
  15. package/lib/types/src/index.d.ts +3 -0
  16. package/lib/types/src/parse-css-value.d.ts +3 -0
  17. package/lib/types/src/parse-css-value.test.d.ts +1 -0
  18. package/lib/types/src/parse-css.d.ts +9 -0
  19. package/lib/types/src/parse-css.test.d.ts +1 -0
  20. package/lib/types/src/property-parsers/background.d.ts +11 -0
  21. package/lib/types/src/property-parsers/background.test.d.ts +1 -0
  22. package/lib/types/src/property-parsers/index.d.ts +1 -0
  23. package/lib/types/src/property-parsers/parsers.d.ts +1 -0
  24. package/lib/types/src/property-parsers/to-longhand.d.ts +1 -0
  25. package/package.json +12 -4
  26. package/src/index.ts +6 -0
  27. package/src/parse-css-value.test.ts +136 -0
  28. package/src/parse-css-value.ts +157 -0
  29. package/src/parse-css.test.ts +101 -0
  30. package/src/parse-css.ts +124 -0
  31. package/src/property-parsers/README.md +11 -0
  32. package/src/property-parsers/background.test.ts +184 -0
  33. package/src/property-parsers/background.ts +132 -0
  34. package/src/property-parsers/index.ts +1 -0
  35. package/src/property-parsers/parsers.ts +4 -0
  36. package/src/property-parsers/to-longhand.ts +4 -0
@@ -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
+ };
@@ -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 [];
@@ -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.66.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
  }
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
+ });
@@ -0,0 +1,157 @@
1
+ import { colord } from "colord";
2
+ import * as csstree from "css-tree";
3
+ import hyphenate from "hyphenate-style-name";
4
+ import warnOnce from "warn-once";
5
+ import { keywordValues } from "./__generated__/keyword-values";
6
+ import { units } from "./__generated__/units";
7
+ import type { StyleProperty, StyleValue, Unit } from "./schema";
8
+
9
+ const cssTryParseValue = (input: string) => {
10
+ try {
11
+ const ast = csstree.parse(input, { context: "value" });
12
+ return ast;
13
+ } catch {
14
+ return undefined;
15
+ }
16
+ };
17
+
18
+ export const isValidDeclaration = (
19
+ property: string,
20
+ value: string
21
+ ): boolean => {
22
+ const ast = cssTryParseValue(value);
23
+
24
+ if (ast == null) {
25
+ return false;
26
+ }
27
+
28
+ const cssPropertyName = hyphenate(property);
29
+
30
+ const matchResult = csstree.lexer.matchProperty(cssPropertyName, ast);
31
+
32
+ const isValidDeclaration = matchResult.matched != null;
33
+
34
+ // @todo remove after fix https://github.com/csstree/csstree/issues/246
35
+ if (isValidDeclaration && typeof CSSStyleValue !== "undefined") {
36
+ try {
37
+ CSSStyleValue.parse(cssPropertyName, value);
38
+ } catch {
39
+ warnOnce(
40
+ true,
41
+ `Css property "${property}" with value "${value}" is invalid according to CSSStyleValue.parse
42
+ but valid according to csstree.lexer.matchProperty.`
43
+ );
44
+ return false;
45
+ }
46
+ }
47
+
48
+ return isValidDeclaration;
49
+ };
50
+
51
+ export const parseCssValue = (
52
+ property: StyleProperty, // Handles only long-hand values.
53
+ input: string
54
+ ): StyleValue => {
55
+ const invalidValue = {
56
+ type: "invalid",
57
+ value: input,
58
+ } as const;
59
+
60
+ if (input.length === 0) {
61
+ return invalidValue;
62
+ }
63
+
64
+ if (!isValidDeclaration(property, input)) {
65
+ return invalidValue;
66
+ }
67
+
68
+ const ast = cssTryParseValue(input);
69
+
70
+ if (ast == null) {
71
+ warnOnce(
72
+ true,
73
+ `Can't parse css property "${property}" with value "${input}"`
74
+ );
75
+ return invalidValue;
76
+ }
77
+
78
+ if (
79
+ ast != null &&
80
+ ast.type === "Value" &&
81
+ ast.children.first === ast.children.last
82
+ ) {
83
+ // Try extract units from 1st children
84
+ const first = ast.children.first;
85
+
86
+ if (first?.type === "Number") {
87
+ return {
88
+ type: "unit",
89
+ unit: "number",
90
+ value: Number(first.value),
91
+ };
92
+ }
93
+
94
+ if (first?.type === "Dimension") {
95
+ const unit = first.unit as (typeof units)[keyof typeof units][number];
96
+
97
+ for (const unitGroup of Object.values(units)) {
98
+ if (unitGroup.includes(unit as never)) {
99
+ return {
100
+ type: "unit",
101
+ unit: unit as Unit,
102
+ value: Number(first.value),
103
+ };
104
+ }
105
+ }
106
+ return invalidValue;
107
+ }
108
+
109
+ if (first?.type === "Percentage") {
110
+ return {
111
+ type: "unit",
112
+ unit: "%",
113
+ value: Number(first.value),
114
+ };
115
+ }
116
+
117
+ if (first?.type === "Identifier") {
118
+ const values = keywordValues[
119
+ property as keyof typeof keywordValues
120
+ ] as ReadonlyArray<string>;
121
+ const lettersRegex = /[^a-zA-Z]+/g;
122
+ const searchValues = values.map((value) =>
123
+ value.replace(lettersRegex, "").toLowerCase()
124
+ );
125
+ const keywordInput = input.replace(lettersRegex, "").toLowerCase();
126
+
127
+ const index = searchValues.indexOf(keywordInput);
128
+
129
+ if (index > -1) {
130
+ return {
131
+ type: "keyword",
132
+ value: values[index],
133
+ };
134
+ }
135
+ }
136
+ }
137
+
138
+ // Probably a color (we can use csstree.lexer.matchProperty(cssPropertyName, ast) to extract the type but this looks much simpler)
139
+ if (property.toLocaleLowerCase().includes("color")) {
140
+ const mayBeColor = colord(input);
141
+ if (mayBeColor.isValid()) {
142
+ const rgb = mayBeColor.toRgb();
143
+ return {
144
+ type: "rgb",
145
+ alpha: rgb.a,
146
+ r: rgb.r,
147
+ g: rgb.g,
148
+ b: rgb.b,
149
+ };
150
+ }
151
+ }
152
+
153
+ return {
154
+ type: "unparsed",
155
+ value: input,
156
+ };
157
+ };
@@ -0,0 +1,101 @@
1
+ import { describe, expect, test } from "@jest/globals";
2
+ import { parseCss } from "./parse-css";
3
+
4
+ describe("Parse CSS", () => {
5
+ test("one class selector rules", () => {
6
+ expect(parseCss(`.test { color: #ff0000 }`)).toMatchInlineSnapshot(`
7
+ {
8
+ "test": [
9
+ {
10
+ "property": "color",
11
+ "value": {
12
+ "alpha": 1,
13
+ "b": 0,
14
+ "g": 0,
15
+ "r": 255,
16
+ "type": "rgb",
17
+ },
18
+ },
19
+ ],
20
+ }
21
+ `);
22
+ });
23
+
24
+ test("parses supported shorthand values", () => {
25
+ expect(
26
+ parseCss(
27
+ `.test { background: #ff0000 linear-gradient(180deg, #11181C 0%, rgba(17, 24, 28, 0) 36.09%), #EBFFFC; }`
28
+ ).test
29
+ ).toMatchInlineSnapshot(`
30
+ [
31
+ {
32
+ "property": "backgroundImage",
33
+ "value": {
34
+ "type": "layers",
35
+ "value": [
36
+ {
37
+ "type": "unparsed",
38
+ "value": "linear-gradient(180deg,#11181C 0%,rgba(17,24,28,0) 36.09%)",
39
+ },
40
+ ],
41
+ },
42
+ },
43
+ {
44
+ "property": "backgroundColor",
45
+ "value": {
46
+ "alpha": 1,
47
+ "b": 252,
48
+ "g": 255,
49
+ "r": 235,
50
+ "type": "rgb",
51
+ },
52
+ },
53
+ ]
54
+ `);
55
+ });
56
+
57
+ test("parses unsupported shorthand values", () => {
58
+ expect(parseCss(`.test { padding: 4px }`).test[0]).toMatchInlineSnapshot(`
59
+ {
60
+ "property": "padding",
61
+ "value": {
62
+ "type": "unit",
63
+ "unit": "px",
64
+ "value": 4,
65
+ },
66
+ }
67
+ `);
68
+ });
69
+
70
+ test("complex selector rules", () => {
71
+ expect(parseCss(`.test, a, .test2, .test:hover { color: #ff0000 }`))
72
+ .toMatchInlineSnapshot(`
73
+ {
74
+ "test": [
75
+ {
76
+ "property": "color",
77
+ "value": {
78
+ "alpha": 1,
79
+ "b": 0,
80
+ "g": 0,
81
+ "r": 255,
82
+ "type": "rgb",
83
+ },
84
+ },
85
+ ],
86
+ "test2": [
87
+ {
88
+ "property": "color",
89
+ "value": {
90
+ "alpha": 1,
91
+ "b": 0,
92
+ "g": 0,
93
+ "r": 255,
94
+ "type": "rgb",
95
+ },
96
+ },
97
+ ],
98
+ }
99
+ `);
100
+ });
101
+ });