@webstudio-is/css-engine 0.53.0 → 0.55.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.
- package/lib/cjs/core/compare-media.js +3 -0
- package/lib/cjs/core/css-engine.js +2 -2
- package/lib/cjs/core/find-applicable-media.js +33 -0
- package/lib/cjs/core/index.js +1 -0
- package/lib/cjs/core/rules.js +10 -5
- package/lib/cjs/core/to-value.js +26 -19
- package/lib/core/compare-media.js +3 -0
- package/lib/core/css-engine.js +2 -2
- package/lib/core/find-applicable-media.js +13 -0
- package/lib/core/index.js +1 -0
- package/lib/core/rules.js +10 -5
- package/lib/core/to-value.js +26 -19
- package/lib/types/core/css-engine.d.ts +2 -1
- package/lib/types/core/find-applicable-media.d.ts +2 -0
- package/lib/types/core/find-applicable-media.test.d.ts +1 -0
- package/lib/types/core/index.d.ts +1 -0
- package/lib/types/core/rules.d.ts +4 -2
- package/lib/types/core/to-value.d.ts +2 -5
- package/package.json +5 -4
- package/src/core/compare-media.test.ts +2 -0
- package/src/core/compare-media.ts +11 -0
- package/src/core/css-engine.test.ts +58 -0
- package/src/core/css-engine.ts +7 -2
- package/src/core/find-applicable-media.test.ts +43 -0
- package/src/core/find-applicable-media.ts +20 -0
- package/src/core/index.ts +1 -0
- package/src/core/match-media.ts +1 -0
- package/src/core/rules.ts +14 -4
- package/src/core/to-value.test.ts +64 -29
- package/src/core/to-value.ts +30 -24
|
@@ -22,6 +22,9 @@ __export(compare_media_exports, {
|
|
|
22
22
|
});
|
|
23
23
|
module.exports = __toCommonJS(compare_media_exports);
|
|
24
24
|
const compareMedia = (optionA, optionB) => {
|
|
25
|
+
if (optionA?.minWidth === void 0 && optionA?.maxWidth !== void 0 || optionB?.minWidth === void 0 && optionB?.maxWidth !== void 0) {
|
|
26
|
+
return 1;
|
|
27
|
+
}
|
|
25
28
|
if (optionA?.minWidth !== void 0 && optionB?.minWidth !== void 0) {
|
|
26
29
|
return optionA.minWidth - optionB.minWidth;
|
|
27
30
|
}
|
|
@@ -69,10 +69,10 @@ class CssEngine {
|
|
|
69
69
|
}
|
|
70
70
|
return mediaRule;
|
|
71
71
|
}
|
|
72
|
-
addStyleRule(selectorText, rule) {
|
|
72
|
+
addStyleRule(selectorText, rule, transformValue) {
|
|
73
73
|
const mediaRule = this.addMediaRule(rule.breakpoint || defaultMediaRuleId);
|
|
74
74
|
__privateSet(this, _isDirty, true);
|
|
75
|
-
const styleRule = new import_rules.StyleRule(selectorText, rule.style);
|
|
75
|
+
const styleRule = new import_rules.StyleRule(selectorText, rule.style, transformValue);
|
|
76
76
|
styleRule.onChange = __privateGet(this, _onChangeRule);
|
|
77
77
|
if (mediaRule === void 0) {
|
|
78
78
|
throw new Error("No media rule found");
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
var find_applicable_media_exports = {};
|
|
20
|
+
__export(find_applicable_media_exports, {
|
|
21
|
+
findApplicableMedia: () => findApplicableMedia
|
|
22
|
+
});
|
|
23
|
+
module.exports = __toCommonJS(find_applicable_media_exports);
|
|
24
|
+
var import_compare_media = require("./compare-media");
|
|
25
|
+
var import_match_media = require("./match-media");
|
|
26
|
+
const findApplicableMedia = (media, width) => {
|
|
27
|
+
const sortedMedia = media.sort(import_compare_media.compareMedia).reverse();
|
|
28
|
+
for (const options of sortedMedia) {
|
|
29
|
+
if ((0, import_match_media.matchMedia)(options, width)) {
|
|
30
|
+
return options;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
};
|
package/lib/cjs/core/index.js
CHANGED
|
@@ -28,3 +28,4 @@ __reExport(core_exports, require("./to-value"), module.exports);
|
|
|
28
28
|
__reExport(core_exports, require("./match-media"), module.exports);
|
|
29
29
|
__reExport(core_exports, require("./equal-media"), module.exports);
|
|
30
30
|
__reExport(core_exports, require("./compare-media"), module.exports);
|
|
31
|
+
__reExport(core_exports, require("./find-applicable-media"), module.exports);
|
package/lib/cjs/core/rules.js
CHANGED
|
@@ -44,12 +44,14 @@ __export(rules_exports, {
|
|
|
44
44
|
module.exports = __toCommonJS(rules_exports);
|
|
45
45
|
var import_to_value = require("./to-value");
|
|
46
46
|
var import_to_property = require("./to-property");
|
|
47
|
-
var _styleMap, _isDirty, _string, _onChange, _mediaType;
|
|
47
|
+
var _styleMap, _isDirty, _string, _transformValue, _onChange, _mediaType;
|
|
48
48
|
class StylePropertyMap {
|
|
49
|
-
constructor() {
|
|
49
|
+
constructor(transformValue) {
|
|
50
50
|
__privateAdd(this, _styleMap, /* @__PURE__ */ new Map());
|
|
51
51
|
__privateAdd(this, _isDirty, false);
|
|
52
52
|
__privateAdd(this, _string, "");
|
|
53
|
+
__privateAdd(this, _transformValue, void 0);
|
|
54
|
+
__privateSet(this, _transformValue, transformValue);
|
|
53
55
|
}
|
|
54
56
|
set(property, value) {
|
|
55
57
|
__privateGet(this, _styleMap).set(property, value);
|
|
@@ -81,7 +83,9 @@ class StylePropertyMap {
|
|
|
81
83
|
if (value === void 0) {
|
|
82
84
|
continue;
|
|
83
85
|
}
|
|
84
|
-
block.push(
|
|
86
|
+
block.push(
|
|
87
|
+
`${(0, import_to_property.toProperty)(property)}: ${(0, import_to_value.toValue)(value, __privateGet(this, _transformValue))}`
|
|
88
|
+
);
|
|
85
89
|
}
|
|
86
90
|
__privateSet(this, _string, block.join("; "));
|
|
87
91
|
__privateSet(this, _isDirty, false);
|
|
@@ -91,12 +95,13 @@ class StylePropertyMap {
|
|
|
91
95
|
_styleMap = new WeakMap();
|
|
92
96
|
_isDirty = new WeakMap();
|
|
93
97
|
_string = new WeakMap();
|
|
98
|
+
_transformValue = new WeakMap();
|
|
94
99
|
class StyleRule {
|
|
95
|
-
constructor(selectorText, style) {
|
|
100
|
+
constructor(selectorText, style, transformValue) {
|
|
96
101
|
__privateAdd(this, _onChange, () => {
|
|
97
102
|
this.onChange?.();
|
|
98
103
|
});
|
|
99
|
-
this.styleMap = new StylePropertyMap();
|
|
104
|
+
this.styleMap = new StylePropertyMap(transformValue);
|
|
100
105
|
this.selectorText = selectorText;
|
|
101
106
|
let property;
|
|
102
107
|
for (property in style) {
|
package/lib/cjs/core/to-value.js
CHANGED
|
@@ -22,34 +22,41 @@ __export(to_value_exports, {
|
|
|
22
22
|
});
|
|
23
23
|
module.exports = __toCommonJS(to_value_exports);
|
|
24
24
|
var import_fonts = require("@webstudio-is/fonts");
|
|
25
|
-
const defaultOptions = {
|
|
26
|
-
withFallback: true
|
|
27
|
-
};
|
|
28
25
|
const assertUnreachable = (_arg, errorMessage) => {
|
|
29
26
|
throw new Error(errorMessage);
|
|
30
27
|
};
|
|
31
|
-
const
|
|
32
|
-
if (
|
|
28
|
+
const fallbackTransform = (styleValue) => {
|
|
29
|
+
if (styleValue.type === "fontFamily") {
|
|
30
|
+
const firstFontFamily = styleValue.value[0];
|
|
31
|
+
const fallbacks = import_fonts.SYSTEM_FONTS.get(firstFontFamily);
|
|
32
|
+
const fontFamily = [...styleValue.value];
|
|
33
|
+
if (Array.isArray(fallbacks)) {
|
|
34
|
+
fontFamily.push(...fallbacks);
|
|
35
|
+
} else {
|
|
36
|
+
fontFamily.push(import_fonts.DEFAULT_FONT_FALLBACK);
|
|
37
|
+
}
|
|
38
|
+
return {
|
|
39
|
+
type: "fontFamily",
|
|
40
|
+
value: fontFamily
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
const toValue = (styleValue, transformValue) => {
|
|
45
|
+
if (styleValue === void 0) {
|
|
33
46
|
return "";
|
|
34
47
|
}
|
|
48
|
+
const transformedValue = transformValue?.(styleValue) ?? fallbackTransform(styleValue);
|
|
49
|
+
const value = transformedValue ?? styleValue;
|
|
35
50
|
if (value.type === "unit") {
|
|
36
51
|
return value.value + (value.unit === "number" ? "" : value.unit);
|
|
37
52
|
}
|
|
38
53
|
if (value.type === "fontFamily") {
|
|
39
|
-
|
|
40
|
-
return value.value[0];
|
|
41
|
-
}
|
|
42
|
-
const family = value.value[0];
|
|
43
|
-
const fallbacks = import_fonts.SYSTEM_FONTS.get(family);
|
|
44
|
-
if (Array.isArray(fallbacks)) {
|
|
45
|
-
return [...value.value, ...fallbacks].join(", ");
|
|
46
|
-
}
|
|
47
|
-
return [...value.value, import_fonts.DEFAULT_FONT_FALLBACK].join(", ");
|
|
54
|
+
return value.value.join(", ");
|
|
48
55
|
}
|
|
49
56
|
if (value.type === "var") {
|
|
50
57
|
const fallbacks = [];
|
|
51
58
|
for (const fallback of value.fallbacks) {
|
|
52
|
-
fallbacks.push(toValue(fallback,
|
|
59
|
+
fallbacks.push(toValue(fallback, transformValue));
|
|
53
60
|
}
|
|
54
61
|
const fallbacksString = fallbacks.length > 0 ? `, ${fallbacks.join(", ")}` : "";
|
|
55
62
|
return `var(--${value.value}${fallbacksString})`;
|
|
@@ -67,10 +74,10 @@ const toValue = (value, options = defaultOptions) => {
|
|
|
67
74
|
return `rgba(${value.r}, ${value.g}, ${value.b}, ${value.alpha})`;
|
|
68
75
|
}
|
|
69
76
|
if (value.type === "image") {
|
|
70
|
-
if (value.hidden) {
|
|
77
|
+
if (value.hidden || value.value.type !== "url") {
|
|
71
78
|
return "none";
|
|
72
79
|
}
|
|
73
|
-
return `url(${value.value.
|
|
80
|
+
return `url(${value.value.url})`;
|
|
74
81
|
}
|
|
75
82
|
if (value.type === "unparsed") {
|
|
76
83
|
if (value.hidden) {
|
|
@@ -79,10 +86,10 @@ const toValue = (value, options = defaultOptions) => {
|
|
|
79
86
|
return value.value;
|
|
80
87
|
}
|
|
81
88
|
if (value.type === "layers") {
|
|
82
|
-
return value.value.map((value2) => toValue(value2,
|
|
89
|
+
return value.value.map((value2) => toValue(value2, transformValue)).join(",");
|
|
83
90
|
}
|
|
84
91
|
if (value.type === "tuple") {
|
|
85
|
-
return value.value.map((value2) => toValue(value2,
|
|
92
|
+
return value.value.map((value2) => toValue(value2, transformValue)).join(" ");
|
|
86
93
|
}
|
|
87
94
|
assertUnreachable(value, `Unknown value type`);
|
|
88
95
|
throw new Error("Unknown value type");
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
const compareMedia = (optionA, optionB) => {
|
|
2
|
+
if (optionA?.minWidth === void 0 && optionA?.maxWidth !== void 0 || optionB?.minWidth === void 0 && optionB?.maxWidth !== void 0) {
|
|
3
|
+
return 1;
|
|
4
|
+
}
|
|
2
5
|
if (optionA?.minWidth !== void 0 && optionB?.minWidth !== void 0) {
|
|
3
6
|
return optionA.minWidth - optionB.minWidth;
|
|
4
7
|
}
|
package/lib/core/css-engine.js
CHANGED
|
@@ -51,10 +51,10 @@ class CssEngine {
|
|
|
51
51
|
}
|
|
52
52
|
return mediaRule;
|
|
53
53
|
}
|
|
54
|
-
addStyleRule(selectorText, rule) {
|
|
54
|
+
addStyleRule(selectorText, rule, transformValue) {
|
|
55
55
|
const mediaRule = this.addMediaRule(rule.breakpoint || defaultMediaRuleId);
|
|
56
56
|
__privateSet(this, _isDirty, true);
|
|
57
|
-
const styleRule = new StyleRule(selectorText, rule.style);
|
|
57
|
+
const styleRule = new StyleRule(selectorText, rule.style, transformValue);
|
|
58
58
|
styleRule.onChange = __privateGet(this, _onChangeRule);
|
|
59
59
|
if (mediaRule === void 0) {
|
|
60
60
|
throw new Error("No media rule found");
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { compareMedia } from "./compare-media";
|
|
2
|
+
import { matchMedia } from "./match-media";
|
|
3
|
+
const findApplicableMedia = (media, width) => {
|
|
4
|
+
const sortedMedia = media.sort(compareMedia).reverse();
|
|
5
|
+
for (const options of sortedMedia) {
|
|
6
|
+
if (matchMedia(options, width)) {
|
|
7
|
+
return options;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
};
|
|
11
|
+
export {
|
|
12
|
+
findApplicableMedia
|
|
13
|
+
};
|
package/lib/core/index.js
CHANGED
package/lib/core/rules.js
CHANGED
|
@@ -16,14 +16,16 @@ var __privateSet = (obj, member, value, setter) => {
|
|
|
16
16
|
setter ? setter.call(obj, value) : member.set(obj, value);
|
|
17
17
|
return value;
|
|
18
18
|
};
|
|
19
|
-
var _styleMap, _isDirty, _string, _onChange, _mediaType;
|
|
19
|
+
var _styleMap, _isDirty, _string, _transformValue, _onChange, _mediaType;
|
|
20
20
|
import { toValue } from "./to-value";
|
|
21
21
|
import { toProperty } from "./to-property";
|
|
22
22
|
class StylePropertyMap {
|
|
23
|
-
constructor() {
|
|
23
|
+
constructor(transformValue) {
|
|
24
24
|
__privateAdd(this, _styleMap, /* @__PURE__ */ new Map());
|
|
25
25
|
__privateAdd(this, _isDirty, false);
|
|
26
26
|
__privateAdd(this, _string, "");
|
|
27
|
+
__privateAdd(this, _transformValue, void 0);
|
|
28
|
+
__privateSet(this, _transformValue, transformValue);
|
|
27
29
|
}
|
|
28
30
|
set(property, value) {
|
|
29
31
|
__privateGet(this, _styleMap).set(property, value);
|
|
@@ -55,7 +57,9 @@ class StylePropertyMap {
|
|
|
55
57
|
if (value === void 0) {
|
|
56
58
|
continue;
|
|
57
59
|
}
|
|
58
|
-
block.push(
|
|
60
|
+
block.push(
|
|
61
|
+
`${toProperty(property)}: ${toValue(value, __privateGet(this, _transformValue))}`
|
|
62
|
+
);
|
|
59
63
|
}
|
|
60
64
|
__privateSet(this, _string, block.join("; "));
|
|
61
65
|
__privateSet(this, _isDirty, false);
|
|
@@ -65,12 +69,13 @@ class StylePropertyMap {
|
|
|
65
69
|
_styleMap = new WeakMap();
|
|
66
70
|
_isDirty = new WeakMap();
|
|
67
71
|
_string = new WeakMap();
|
|
72
|
+
_transformValue = new WeakMap();
|
|
68
73
|
class StyleRule {
|
|
69
|
-
constructor(selectorText, style) {
|
|
74
|
+
constructor(selectorText, style, transformValue) {
|
|
70
75
|
__privateAdd(this, _onChange, () => {
|
|
71
76
|
this.onChange?.();
|
|
72
77
|
});
|
|
73
|
-
this.styleMap = new StylePropertyMap();
|
|
78
|
+
this.styleMap = new StylePropertyMap(transformValue);
|
|
74
79
|
this.selectorText = selectorText;
|
|
75
80
|
let property;
|
|
76
81
|
for (property in style) {
|
package/lib/core/to-value.js
CHANGED
|
@@ -1,32 +1,39 @@
|
|
|
1
1
|
import { DEFAULT_FONT_FALLBACK, SYSTEM_FONTS } from "@webstudio-is/fonts";
|
|
2
|
-
const defaultOptions = {
|
|
3
|
-
withFallback: true
|
|
4
|
-
};
|
|
5
2
|
const assertUnreachable = (_arg, errorMessage) => {
|
|
6
3
|
throw new Error(errorMessage);
|
|
7
4
|
};
|
|
8
|
-
const
|
|
9
|
-
if (
|
|
5
|
+
const fallbackTransform = (styleValue) => {
|
|
6
|
+
if (styleValue.type === "fontFamily") {
|
|
7
|
+
const firstFontFamily = styleValue.value[0];
|
|
8
|
+
const fallbacks = SYSTEM_FONTS.get(firstFontFamily);
|
|
9
|
+
const fontFamily = [...styleValue.value];
|
|
10
|
+
if (Array.isArray(fallbacks)) {
|
|
11
|
+
fontFamily.push(...fallbacks);
|
|
12
|
+
} else {
|
|
13
|
+
fontFamily.push(DEFAULT_FONT_FALLBACK);
|
|
14
|
+
}
|
|
15
|
+
return {
|
|
16
|
+
type: "fontFamily",
|
|
17
|
+
value: fontFamily
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
const toValue = (styleValue, transformValue) => {
|
|
22
|
+
if (styleValue === void 0) {
|
|
10
23
|
return "";
|
|
11
24
|
}
|
|
25
|
+
const transformedValue = transformValue?.(styleValue) ?? fallbackTransform(styleValue);
|
|
26
|
+
const value = transformedValue ?? styleValue;
|
|
12
27
|
if (value.type === "unit") {
|
|
13
28
|
return value.value + (value.unit === "number" ? "" : value.unit);
|
|
14
29
|
}
|
|
15
30
|
if (value.type === "fontFamily") {
|
|
16
|
-
|
|
17
|
-
return value.value[0];
|
|
18
|
-
}
|
|
19
|
-
const family = value.value[0];
|
|
20
|
-
const fallbacks = SYSTEM_FONTS.get(family);
|
|
21
|
-
if (Array.isArray(fallbacks)) {
|
|
22
|
-
return [...value.value, ...fallbacks].join(", ");
|
|
23
|
-
}
|
|
24
|
-
return [...value.value, DEFAULT_FONT_FALLBACK].join(", ");
|
|
31
|
+
return value.value.join(", ");
|
|
25
32
|
}
|
|
26
33
|
if (value.type === "var") {
|
|
27
34
|
const fallbacks = [];
|
|
28
35
|
for (const fallback of value.fallbacks) {
|
|
29
|
-
fallbacks.push(toValue(fallback,
|
|
36
|
+
fallbacks.push(toValue(fallback, transformValue));
|
|
30
37
|
}
|
|
31
38
|
const fallbacksString = fallbacks.length > 0 ? `, ${fallbacks.join(", ")}` : "";
|
|
32
39
|
return `var(--${value.value}${fallbacksString})`;
|
|
@@ -44,10 +51,10 @@ const toValue = (value, options = defaultOptions) => {
|
|
|
44
51
|
return `rgba(${value.r}, ${value.g}, ${value.b}, ${value.alpha})`;
|
|
45
52
|
}
|
|
46
53
|
if (value.type === "image") {
|
|
47
|
-
if (value.hidden) {
|
|
54
|
+
if (value.hidden || value.value.type !== "url") {
|
|
48
55
|
return "none";
|
|
49
56
|
}
|
|
50
|
-
return `url(${value.value.
|
|
57
|
+
return `url(${value.value.url})`;
|
|
51
58
|
}
|
|
52
59
|
if (value.type === "unparsed") {
|
|
53
60
|
if (value.hidden) {
|
|
@@ -56,10 +63,10 @@ const toValue = (value, options = defaultOptions) => {
|
|
|
56
63
|
return value.value;
|
|
57
64
|
}
|
|
58
65
|
if (value.type === "layers") {
|
|
59
|
-
return value.value.map((value2) => toValue(value2,
|
|
66
|
+
return value.value.map((value2) => toValue(value2, transformValue)).join(",");
|
|
60
67
|
}
|
|
61
68
|
if (value.type === "tuple") {
|
|
62
|
-
return value.value.map((value2) => toValue(value2,
|
|
69
|
+
return value.value.map((value2) => toValue(value2, transformValue)).join(" ");
|
|
63
70
|
}
|
|
64
71
|
assertUnreachable(value, `Unknown value type`);
|
|
65
72
|
throw new Error("Unknown value type");
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { CssRule } from "@webstudio-is/css-data";
|
|
2
2
|
import { MediaRule, PlaintextRule, StyleRule, type FontFaceOptions, type MediaRuleOptions } from "./rules";
|
|
3
|
+
import type { TransformValue } from "./to-value";
|
|
3
4
|
export type CssEngineOptions = {
|
|
4
5
|
name?: string;
|
|
5
6
|
};
|
|
@@ -7,7 +8,7 @@ export declare class CssEngine {
|
|
|
7
8
|
#private;
|
|
8
9
|
constructor({ name }: CssEngineOptions);
|
|
9
10
|
addMediaRule(id: string, options?: MediaRuleOptions): MediaRule;
|
|
10
|
-
addStyleRule(selectorText: string, rule: CssRule): StyleRule;
|
|
11
|
+
addStyleRule(selectorText: string, rule: CssRule, transformValue?: TransformValue): StyleRule;
|
|
11
12
|
addPlaintextRule(cssText: string): PlaintextRule | Map<string, PlaintextRule>;
|
|
12
13
|
addFontFaceRule(options: FontFaceOptions): number;
|
|
13
14
|
clear(): void;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import type { Style, StyleProperty, StyleValue } from "@webstudio-is/css-data";
|
|
2
|
+
import { type TransformValue } from "./to-value";
|
|
2
3
|
declare class StylePropertyMap {
|
|
3
4
|
#private;
|
|
4
5
|
onChange?: () => void;
|
|
6
|
+
constructor(transformValue?: TransformValue);
|
|
5
7
|
set(property: StyleProperty, value?: StyleValue): void;
|
|
6
8
|
has(property: StyleProperty): boolean;
|
|
7
|
-
keys(): IterableIterator<"color" | "left" | "right" | "top" | "bottom" | "contain" | "clip" | "content" |
|
|
9
|
+
keys(): IterableIterator<"color" | "left" | "right" | "top" | "bottom" | "contain" | "clip" | "content" | `--${string}` | "accentColor" | "alignContent" | "alignItems" | "alignSelf" | "alignTracks" | "animationComposition" | "animationDelay" | "animationDirection" | "animationDuration" | "animationFillMode" | "animationIterationCount" | "animationName" | "animationPlayState" | "animationTimingFunction" | "animationTimeline" | "appearance" | "aspectRatio" | "backdropFilter" | "backfaceVisibility" | "backgroundAttachment" | "backgroundBlendMode" | "backgroundClip" | "backgroundColor" | "backgroundImage" | "backgroundOrigin" | "backgroundPosition" | "backgroundPositionX" | "backgroundPositionY" | "backgroundRepeat" | "backgroundSize" | "blockOverflow" | "blockSize" | "borderBlockColor" | "borderBlockStyle" | "borderBlockWidth" | "borderBlockEndColor" | "borderBlockEndStyle" | "borderBlockEndWidth" | "borderBlockStartColor" | "borderBlockStartStyle" | "borderBlockStartWidth" | "borderBottomColor" | "borderBottomLeftRadius" | "borderBottomRightRadius" | "borderBottomStyle" | "borderBottomWidth" | "borderCollapse" | "borderEndEndRadius" | "borderEndStartRadius" | "borderImageOutset" | "borderImageRepeat" | "borderImageSlice" | "borderImageSource" | "borderImageWidth" | "borderInlineColor" | "borderInlineStyle" | "borderInlineWidth" | "borderInlineEndColor" | "borderInlineEndStyle" | "borderInlineEndWidth" | "borderInlineStartColor" | "borderInlineStartStyle" | "borderInlineStartWidth" | "borderLeftColor" | "borderLeftStyle" | "borderLeftWidth" | "borderRightColor" | "borderRightStyle" | "borderRightWidth" | "borderSpacing" | "borderStartEndRadius" | "borderStartStartRadius" | "borderTopColor" | "borderTopLeftRadius" | "borderTopRightRadius" | "borderTopStyle" | "borderTopWidth" | "boxDecorationBreak" | "boxShadow" | "boxSizing" | "breakAfter" | "breakBefore" | "breakInside" | "captionSide" | "caretColor" | "caretShape" | "clear" | "clipPath" | "printColorAdjust" | "colorScheme" | "columnCount" | "columnFill" | "columnGap" | "columnRuleColor" | "columnRuleStyle" | "columnRuleWidth" | "columnSpan" | "columnWidth" | "containIntrinsicBlockSize" | "containIntrinsicHeight" | "containIntrinsicInlineSize" | "containIntrinsicWidth" | "contentVisibility" | "counterIncrement" | "counterReset" | "counterSet" | "cursor" | "direction" | "display" | "emptyCells" | "filter" | "flexBasis" | "flexDirection" | "flexGrow" | "flexShrink" | "flexWrap" | "float" | "fontFamily" | "fontFeatureSettings" | "fontKerning" | "fontLanguageOverride" | "fontOpticalSizing" | "fontVariationSettings" | "fontSize" | "fontSizeAdjust" | "fontStretch" | "fontStyle" | "fontSynthesis" | "fontVariant" | "fontVariantAlternates" | "fontVariantCaps" | "fontVariantEastAsian" | "fontVariantLigatures" | "fontVariantNumeric" | "fontVariantPosition" | "fontWeight" | "forcedColorAdjust" | "gridAutoColumns" | "gridAutoFlow" | "gridAutoRows" | "gridColumnEnd" | "gridColumnStart" | "gridRowEnd" | "gridRowStart" | "gridTemplateAreas" | "gridTemplateColumns" | "gridTemplateRows" | "hangingPunctuation" | "height" | "hyphenateCharacter" | "hyphens" | "imageOrientation" | "imageRendering" | "imageResolution" | "initialLetter" | "initialLetterAlign" | "inlineSize" | "inputSecurity" | "insetBlockEnd" | "insetBlockStart" | "insetInlineEnd" | "insetInlineStart" | "isolation" | "justifyContent" | "justifyItems" | "justifySelf" | "justifyTracks" | "letterSpacing" | "lineBreak" | "lineClamp" | "lineHeight" | "lineHeightStep" | "listStyleImage" | "listStylePosition" | "listStyleType" | "marginBlockEnd" | "marginBlockStart" | "marginBottom" | "marginInlineEnd" | "marginInlineStart" | "marginLeft" | "marginRight" | "marginTop" | "marginTrim" | "maskBorderMode" | "maskBorderOutset" | "maskBorderRepeat" | "maskBorderSlice" | "maskBorderSource" | "maskBorderWidth" | "maskClip" | "maskComposite" | "maskImage" | "maskMode" | "maskOrigin" | "maskPosition" | "maskRepeat" | "maskSize" | "maskType" | "masonryAutoFlow" | "mathDepth" | "mathShift" | "mathStyle" | "maxBlockSize" | "maxHeight" | "maxInlineSize" | "maxLines" | "maxWidth" | "minBlockSize" | "minHeight" | "minInlineSize" | "minWidth" | "mixBlendMode" | "objectFit" | "objectPosition" | "offsetAnchor" | "offsetDistance" | "offsetPath" | "offsetPosition" | "offsetRotate" | "opacity" | "order" | "orphans" | "outlineColor" | "outlineOffset" | "outlineStyle" | "outlineWidth" | "overflow" | "overflowAnchor" | "overflowBlock" | "overflowClipMargin" | "overflowInline" | "overflowWrap" | "overflowX" | "overflowY" | "overscrollBehavior" | "overscrollBehaviorBlock" | "overscrollBehaviorInline" | "overscrollBehaviorX" | "overscrollBehaviorY" | "paddingBlockEnd" | "paddingBlockStart" | "paddingBottom" | "paddingInlineEnd" | "paddingInlineStart" | "paddingLeft" | "paddingRight" | "paddingTop" | "pageBreakAfter" | "pageBreakBefore" | "pageBreakInside" | "paintOrder" | "perspective" | "perspectiveOrigin" | "pointerEvents" | "position" | "quotes" | "resize" | "rotate" | "rowGap" | "rubyAlign" | "rubyMerge" | "rubyPosition" | "scale" | "scrollbarColor" | "scrollbarGutter" | "scrollbarWidth" | "scrollBehavior" | "scrollMarginBlockStart" | "scrollMarginBlockEnd" | "scrollMarginBottom" | "scrollMarginInlineStart" | "scrollMarginInlineEnd" | "scrollMarginLeft" | "scrollMarginRight" | "scrollMarginTop" | "scrollPaddingBlockStart" | "scrollPaddingBlockEnd" | "scrollPaddingBottom" | "scrollPaddingInlineStart" | "scrollPaddingInlineEnd" | "scrollPaddingLeft" | "scrollPaddingRight" | "scrollPaddingTop" | "scrollSnapAlign" | "scrollSnapStop" | "scrollSnapType" | "scrollTimelineAxis" | "scrollTimelineName" | "shapeImageThreshold" | "shapeMargin" | "shapeOutside" | "tabSize" | "tableLayout" | "textAlign" | "textAlignLast" | "textCombineUpright" | "textDecorationColor" | "textDecorationLine" | "textDecorationSkip" | "textDecorationSkipInk" | "textDecorationStyle" | "textDecorationThickness" | "textEmphasisColor" | "textEmphasisPosition" | "textEmphasisStyle" | "textIndent" | "textJustify" | "textOrientation" | "textOverflow" | "textRendering" | "textShadow" | "textSizeAdjust" | "textTransform" | "textUnderlineOffset" | "textUnderlinePosition" | "touchAction" | "transform" | "transformBox" | "transformOrigin" | "transformStyle" | "transitionDelay" | "transitionDuration" | "transitionProperty" | "transitionTimingFunction" | "translate" | "unicodeBidi" | "userSelect" | "verticalAlign" | "visibility" | "whiteSpace" | "widows" | "width" | "willChange" | "wordBreak" | "wordSpacing" | "wordWrap" | "writingMode" | "zIndex">;
|
|
8
10
|
delete(property: StyleProperty): void;
|
|
9
11
|
clear(): void;
|
|
10
12
|
toString(): string;
|
|
@@ -14,7 +16,7 @@ export declare class StyleRule {
|
|
|
14
16
|
styleMap: StylePropertyMap;
|
|
15
17
|
selectorText: string;
|
|
16
18
|
onChange?: () => void;
|
|
17
|
-
constructor(selectorText: string, style: Style);
|
|
19
|
+
constructor(selectorText: string, style: Style, transformValue?: TransformValue);
|
|
18
20
|
get cssText(): string;
|
|
19
21
|
}
|
|
20
22
|
export type MediaRuleOptions = {
|
|
@@ -1,6 +1,3 @@
|
|
|
1
1
|
import type { StyleValue } from "@webstudio-is/css-data";
|
|
2
|
-
type
|
|
3
|
-
|
|
4
|
-
};
|
|
5
|
-
export declare const toValue: (value?: StyleValue, options?: ToCssOptions) => string;
|
|
6
|
-
export {};
|
|
2
|
+
export type TransformValue = (styleValue: StyleValue) => undefined | StyleValue;
|
|
3
|
+
export declare const toValue: (styleValue: undefined | StyleValue, transformValue?: TransformValue) => string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@webstudio-is/css-engine",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.55.0",
|
|
4
4
|
"description": "CSS Renderer for Webstudio",
|
|
5
5
|
"author": "Webstudio <github@webstudio.is>",
|
|
6
6
|
"homepage": "https://webstudio.is",
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"hyphenate-style-name": "^1.0.4",
|
|
10
10
|
"react": "^17.0.2",
|
|
11
11
|
"react-dom": "^17.0.2",
|
|
12
|
-
"@webstudio-is/fonts": "^0.
|
|
12
|
+
"@webstudio-is/fonts": "^0.55.0"
|
|
13
13
|
},
|
|
14
14
|
"devDependencies": {
|
|
15
15
|
"@jest/globals": "^29.3.1",
|
|
@@ -19,11 +19,12 @@
|
|
|
19
19
|
"@types/react-dom": "^17.0.9",
|
|
20
20
|
"jest": "^29.3.1",
|
|
21
21
|
"typescript": "5.0.3",
|
|
22
|
-
"@webstudio-is/
|
|
22
|
+
"@webstudio-is/asset-uploader": "^0.55.0",
|
|
23
|
+
"@webstudio-is/css-data": "^0.55.0",
|
|
23
24
|
"@webstudio-is/jest-config": "^1.0.2",
|
|
24
25
|
"@webstudio-is/scripts": "^0.0.0",
|
|
25
26
|
"@webstudio-is/storybook-config": "^0.0.0",
|
|
26
|
-
"@webstudio-is/tsconfig": "^1.0.
|
|
27
|
+
"@webstudio-is/tsconfig": "^1.0.3"
|
|
27
28
|
},
|
|
28
29
|
"exports": {
|
|
29
30
|
"source": "./src/index.ts",
|
|
@@ -20,6 +20,7 @@ describe("Compare media", () => {
|
|
|
20
20
|
|
|
21
21
|
test("webflow", () => {
|
|
22
22
|
const initial = [
|
|
23
|
+
{},
|
|
23
24
|
{ maxWidth: 991 },
|
|
24
25
|
{ maxWidth: 767 },
|
|
25
26
|
{ maxWidth: 479 },
|
|
@@ -28,6 +29,7 @@ describe("Compare media", () => {
|
|
|
28
29
|
{ minWidth: 1920 },
|
|
29
30
|
];
|
|
30
31
|
const sorted = [
|
|
32
|
+
{},
|
|
31
33
|
{ maxWidth: 991 },
|
|
32
34
|
{ maxWidth: 767 },
|
|
33
35
|
{ maxWidth: 479 },
|
|
@@ -8,11 +8,22 @@ export const compareMedia = (
|
|
|
8
8
|
optionA: MediaRuleOptions,
|
|
9
9
|
optionB: MediaRuleOptions
|
|
10
10
|
) => {
|
|
11
|
+
// Ensures a media with no min/max is always first
|
|
12
|
+
if (
|
|
13
|
+
(optionA?.minWidth === undefined && optionA?.maxWidth !== undefined) ||
|
|
14
|
+
(optionB?.minWidth === undefined && optionB?.maxWidth !== undefined)
|
|
15
|
+
) {
|
|
16
|
+
return 1;
|
|
17
|
+
}
|
|
18
|
+
// Both are defined by minWidth, put the bigger one first
|
|
11
19
|
if (optionA?.minWidth !== undefined && optionB?.minWidth !== undefined) {
|
|
12
20
|
return optionA.minWidth - optionB.minWidth;
|
|
13
21
|
}
|
|
22
|
+
// Both are defined by maxWidth, put the smaller one first
|
|
14
23
|
if (optionA?.maxWidth !== undefined && optionB?.maxWidth !== undefined) {
|
|
15
24
|
return optionB.maxWidth - optionA.maxWidth;
|
|
16
25
|
}
|
|
26
|
+
|
|
27
|
+
// Media with maxWith should render before minWith just to have the same sorting visually in the UI as in CSSOM.
|
|
17
28
|
return "minWidth" in optionA ? 1 : -1;
|
|
18
29
|
};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { describe, beforeEach, test, expect } from "@jest/globals";
|
|
2
|
+
import type { Assets } from "@webstudio-is/asset-uploader";
|
|
2
3
|
import { CssEngine } from "./css-engine";
|
|
3
4
|
|
|
4
5
|
const style0 = {
|
|
@@ -349,4 +350,61 @@ describe("CssEngine", () => {
|
|
|
349
350
|
}"
|
|
350
351
|
`);
|
|
351
352
|
});
|
|
353
|
+
|
|
354
|
+
test("render images with injected asset url", () => {
|
|
355
|
+
const assets: Assets = new Map([
|
|
356
|
+
[
|
|
357
|
+
"1234",
|
|
358
|
+
{
|
|
359
|
+
type: "image",
|
|
360
|
+
path: "foo.png",
|
|
361
|
+
id: "1234567890",
|
|
362
|
+
projectId: "",
|
|
363
|
+
format: "",
|
|
364
|
+
size: 1212,
|
|
365
|
+
name: "img",
|
|
366
|
+
description: "",
|
|
367
|
+
location: "REMOTE",
|
|
368
|
+
createdAt: "",
|
|
369
|
+
meta: { width: 1, height: 2 },
|
|
370
|
+
},
|
|
371
|
+
],
|
|
372
|
+
]);
|
|
373
|
+
const rule = engine.addStyleRule(
|
|
374
|
+
".c",
|
|
375
|
+
{
|
|
376
|
+
style: {
|
|
377
|
+
backgroundImage: {
|
|
378
|
+
type: "image",
|
|
379
|
+
value: {
|
|
380
|
+
type: "asset",
|
|
381
|
+
value: "1234",
|
|
382
|
+
},
|
|
383
|
+
},
|
|
384
|
+
},
|
|
385
|
+
breakpoint: "0",
|
|
386
|
+
},
|
|
387
|
+
(styleValue) => {
|
|
388
|
+
if (styleValue.type === "image" && styleValue.value.type === "asset") {
|
|
389
|
+
const asset = assets.get(styleValue.value.value);
|
|
390
|
+
if (asset === undefined) {
|
|
391
|
+
return { type: "keyword", value: "none" };
|
|
392
|
+
}
|
|
393
|
+
return {
|
|
394
|
+
type: "image",
|
|
395
|
+
value: {
|
|
396
|
+
type: "url",
|
|
397
|
+
url: asset.path,
|
|
398
|
+
},
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
);
|
|
403
|
+
rule.styleMap.delete("display");
|
|
404
|
+
expect(engine.cssText).toMatchInlineSnapshot(`
|
|
405
|
+
"@media all {
|
|
406
|
+
.c { background-image: url(foo.png) }
|
|
407
|
+
}"
|
|
408
|
+
`);
|
|
409
|
+
});
|
|
352
410
|
});
|
package/src/core/css-engine.ts
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
import { compareMedia } from "./compare-media";
|
|
11
11
|
import { StyleElement } from "./style-element";
|
|
12
12
|
import { StyleSheet } from "./style-sheet";
|
|
13
|
+
import type { TransformValue } from "./to-value";
|
|
13
14
|
|
|
14
15
|
const defaultMediaRuleId = "__default-media-rule__";
|
|
15
16
|
|
|
@@ -36,10 +37,14 @@ export class CssEngine {
|
|
|
36
37
|
}
|
|
37
38
|
return mediaRule;
|
|
38
39
|
}
|
|
39
|
-
addStyleRule(
|
|
40
|
+
addStyleRule(
|
|
41
|
+
selectorText: string,
|
|
42
|
+
rule: CssRule,
|
|
43
|
+
transformValue?: TransformValue
|
|
44
|
+
) {
|
|
40
45
|
const mediaRule = this.addMediaRule(rule.breakpoint || defaultMediaRuleId);
|
|
41
46
|
this.#isDirty = true;
|
|
42
|
-
const styleRule = new StyleRule(selectorText, rule.style);
|
|
47
|
+
const styleRule = new StyleRule(selectorText, rule.style, transformValue);
|
|
43
48
|
styleRule.onChange = this.#onChangeRule;
|
|
44
49
|
if (mediaRule === undefined) {
|
|
45
50
|
// Should be impossible to reach.
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { describe, test, expect } from "@jest/globals";
|
|
2
|
+
import { findApplicableMedia } from "./find-applicable-media";
|
|
3
|
+
|
|
4
|
+
const media = [
|
|
5
|
+
{},
|
|
6
|
+
{ maxWidth: 991 },
|
|
7
|
+
{ maxWidth: 767 },
|
|
8
|
+
{ maxWidth: 479 },
|
|
9
|
+
{ minWidth: 1280 },
|
|
10
|
+
{ minWidth: 1440 },
|
|
11
|
+
{ minWidth: 1920 },
|
|
12
|
+
];
|
|
13
|
+
|
|
14
|
+
describe("Find applicable media", () => {
|
|
15
|
+
test("200", () => {
|
|
16
|
+
expect(findApplicableMedia([...media], 200)).toStrictEqual({
|
|
17
|
+
maxWidth: 479,
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
test("479", () => {
|
|
21
|
+
expect(findApplicableMedia([...media], 479)).toStrictEqual({
|
|
22
|
+
maxWidth: 479,
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
test("480", () => {
|
|
26
|
+
expect(findApplicableMedia([...media], 480)).toStrictEqual({
|
|
27
|
+
maxWidth: 767,
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
test("1279", () => {
|
|
31
|
+
expect(findApplicableMedia([...media], 1279)).toStrictEqual({});
|
|
32
|
+
});
|
|
33
|
+
test("1280", () => {
|
|
34
|
+
expect(findApplicableMedia([...media], 1280)).toStrictEqual({
|
|
35
|
+
minWidth: 1280,
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
test("1440", () => {
|
|
39
|
+
expect(findApplicableMedia([...media], 1440)).toStrictEqual({
|
|
40
|
+
minWidth: 1440,
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { compareMedia } from "./compare-media";
|
|
2
|
+
import { matchMedia } from "./match-media";
|
|
3
|
+
import type { MediaRuleOptions } from "./rules";
|
|
4
|
+
|
|
5
|
+
// Find media rule that matches the given width when rendered.
|
|
6
|
+
export const findApplicableMedia = <Media extends MediaRuleOptions>(
|
|
7
|
+
media: Array<Media>,
|
|
8
|
+
width: number
|
|
9
|
+
) => {
|
|
10
|
+
const sortedMedia = media
|
|
11
|
+
.sort(compareMedia)
|
|
12
|
+
// Reverse order is needed because the last rule in CSSOM has higher source order specificity.
|
|
13
|
+
.reverse();
|
|
14
|
+
|
|
15
|
+
for (const options of sortedMedia) {
|
|
16
|
+
if (matchMedia(options, width)) {
|
|
17
|
+
return options;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
};
|
package/src/core/index.ts
CHANGED
package/src/core/match-media.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { MediaRuleOptions } from "./rules";
|
|
2
2
|
|
|
3
|
+
// This will match a breakpoint that has no min/max.
|
|
3
4
|
export const matchMedia = (options: MediaRuleOptions, width: number) => {
|
|
4
5
|
const minWidth = options.minWidth ?? Number.MIN_SAFE_INTEGER;
|
|
5
6
|
const maxWidth = options.maxWidth ?? Number.MAX_SAFE_INTEGER;
|
package/src/core/rules.ts
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
import type { Style, StyleProperty, StyleValue } from "@webstudio-is/css-data";
|
|
2
|
-
import { toValue } from "./to-value";
|
|
2
|
+
import { toValue, type TransformValue } from "./to-value";
|
|
3
3
|
import { toProperty } from "./to-property";
|
|
4
4
|
|
|
5
5
|
class StylePropertyMap {
|
|
6
6
|
#styleMap: Map<StyleProperty, StyleValue | undefined> = new Map();
|
|
7
7
|
#isDirty = false;
|
|
8
8
|
#string = "";
|
|
9
|
+
#transformValue?: TransformValue;
|
|
9
10
|
onChange?: () => void;
|
|
11
|
+
constructor(transformValue?: TransformValue) {
|
|
12
|
+
this.#transformValue = transformValue;
|
|
13
|
+
}
|
|
10
14
|
set(property: StyleProperty, value?: StyleValue) {
|
|
11
15
|
this.#styleMap.set(property, value);
|
|
12
16
|
this.#isDirty = true;
|
|
@@ -37,7 +41,9 @@ class StylePropertyMap {
|
|
|
37
41
|
if (value === undefined) {
|
|
38
42
|
continue;
|
|
39
43
|
}
|
|
40
|
-
block.push(
|
|
44
|
+
block.push(
|
|
45
|
+
`${toProperty(property)}: ${toValue(value, this.#transformValue)}`
|
|
46
|
+
);
|
|
41
47
|
}
|
|
42
48
|
this.#string = block.join("; ");
|
|
43
49
|
this.#isDirty = false;
|
|
@@ -49,8 +55,12 @@ export class StyleRule {
|
|
|
49
55
|
styleMap;
|
|
50
56
|
selectorText;
|
|
51
57
|
onChange?: () => void;
|
|
52
|
-
constructor(
|
|
53
|
-
|
|
58
|
+
constructor(
|
|
59
|
+
selectorText: string,
|
|
60
|
+
style: Style,
|
|
61
|
+
transformValue?: TransformValue
|
|
62
|
+
) {
|
|
63
|
+
this.styleMap = new StylePropertyMap(transformValue);
|
|
54
64
|
this.selectorText = selectorText;
|
|
55
65
|
let property: StyleProperty;
|
|
56
66
|
for (property in style) {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { describe, test, expect } from "@jest/globals";
|
|
2
|
+
import type { Assets } from "@webstudio-is/asset-uploader";
|
|
2
3
|
import { toValue } from "./to-value";
|
|
3
4
|
|
|
4
5
|
describe("Convert WS CSS Values to native CSS strings", () => {
|
|
@@ -54,51 +55,85 @@ describe("Convert WS CSS Values to native CSS strings", () => {
|
|
|
54
55
|
expect(value).toBe("Courier New, monospace");
|
|
55
56
|
});
|
|
56
57
|
|
|
57
|
-
test("
|
|
58
|
+
test("Transform font family value to override default fallback", () => {
|
|
58
59
|
const value = toValue(
|
|
59
60
|
{
|
|
60
61
|
type: "fontFamily",
|
|
61
62
|
value: ["Courier New"],
|
|
62
63
|
},
|
|
63
|
-
|
|
64
|
+
(styleValue) => {
|
|
65
|
+
if (styleValue.type === "fontFamily") {
|
|
66
|
+
return {
|
|
67
|
+
type: "fontFamily",
|
|
68
|
+
value: [styleValue.value[0]],
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
}
|
|
64
72
|
);
|
|
65
73
|
expect(value).toBe("Courier New");
|
|
66
74
|
});
|
|
67
75
|
|
|
68
76
|
test("array", () => {
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
{
|
|
73
|
-
type: "keyword",
|
|
74
|
-
value: "auto",
|
|
75
|
-
},
|
|
76
|
-
{ type: "unit", value: 10, unit: "px" },
|
|
77
|
-
{ type: "unparsed", value: "calc(10px)" },
|
|
77
|
+
const assets: Assets = new Map([
|
|
78
|
+
[
|
|
79
|
+
"1234567890",
|
|
78
80
|
{
|
|
79
81
|
type: "image",
|
|
80
|
-
|
|
81
|
-
type: "asset",
|
|
82
|
-
value: {
|
|
83
|
-
type: "image",
|
|
84
|
-
path: "foo.png",
|
|
82
|
+
path: "foo.png",
|
|
85
83
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
},
|
|
96
|
-
},
|
|
84
|
+
id: "1234567890",
|
|
85
|
+
projectId: "",
|
|
86
|
+
format: "",
|
|
87
|
+
size: 1212,
|
|
88
|
+
name: "img",
|
|
89
|
+
description: "",
|
|
90
|
+
location: "REMOTE",
|
|
91
|
+
createdAt: "",
|
|
92
|
+
meta: { width: 1, height: 2 },
|
|
97
93
|
},
|
|
98
94
|
],
|
|
99
|
-
|
|
95
|
+
]);
|
|
96
|
+
|
|
97
|
+
const value = toValue(
|
|
98
|
+
{
|
|
99
|
+
type: "layers",
|
|
100
|
+
value: [
|
|
101
|
+
{
|
|
102
|
+
type: "keyword",
|
|
103
|
+
value: "auto",
|
|
104
|
+
},
|
|
105
|
+
{ type: "unit", value: 10, unit: "px" },
|
|
106
|
+
{ type: "unparsed", value: "calc(10px)" },
|
|
107
|
+
{
|
|
108
|
+
type: "image",
|
|
109
|
+
value: {
|
|
110
|
+
type: "asset",
|
|
111
|
+
value: "1234567890",
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
],
|
|
115
|
+
},
|
|
116
|
+
(styleValue) => {
|
|
117
|
+
if (styleValue.type === "image" && styleValue.value.type === "asset") {
|
|
118
|
+
const asset = assets.get(styleValue.value.value);
|
|
119
|
+
if (asset === undefined) {
|
|
120
|
+
return {
|
|
121
|
+
type: "keyword",
|
|
122
|
+
value: "none",
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
return {
|
|
126
|
+
type: "image",
|
|
127
|
+
value: {
|
|
128
|
+
type: "url",
|
|
129
|
+
url: asset.path,
|
|
130
|
+
},
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
);
|
|
100
135
|
|
|
101
|
-
expect(value).toBe("auto,10px,calc(10px),url(foo.png)
|
|
136
|
+
expect(value).toBe("auto,10px,calc(10px),url(foo.png)");
|
|
102
137
|
});
|
|
103
138
|
|
|
104
139
|
test("tuple", () => {
|
package/src/core/to-value.ts
CHANGED
|
@@ -1,44 +1,50 @@
|
|
|
1
1
|
import type { StyleValue } from "@webstudio-is/css-data";
|
|
2
2
|
import { DEFAULT_FONT_FALLBACK, SYSTEM_FONTS } from "@webstudio-is/fonts";
|
|
3
3
|
|
|
4
|
-
type
|
|
5
|
-
withFallback: boolean;
|
|
6
|
-
};
|
|
7
|
-
|
|
8
|
-
const defaultOptions = {
|
|
9
|
-
withFallback: true,
|
|
10
|
-
};
|
|
4
|
+
export type TransformValue = (styleValue: StyleValue) => undefined | StyleValue;
|
|
11
5
|
|
|
12
6
|
// exhaustive check, should never happen in runtime as ts would give error
|
|
13
7
|
const assertUnreachable = (_arg: never, errorMessage: string) => {
|
|
14
8
|
throw new Error(errorMessage);
|
|
15
9
|
};
|
|
16
10
|
|
|
11
|
+
const fallbackTransform: TransformValue = (styleValue) => {
|
|
12
|
+
if (styleValue.type === "fontFamily") {
|
|
13
|
+
const firstFontFamily = styleValue.value[0];
|
|
14
|
+
const fallbacks = SYSTEM_FONTS.get(firstFontFamily);
|
|
15
|
+
const fontFamily: string[] = [...styleValue.value];
|
|
16
|
+
if (Array.isArray(fallbacks)) {
|
|
17
|
+
fontFamily.push(...fallbacks);
|
|
18
|
+
} else {
|
|
19
|
+
fontFamily.push(DEFAULT_FONT_FALLBACK);
|
|
20
|
+
}
|
|
21
|
+
return {
|
|
22
|
+
type: "fontFamily",
|
|
23
|
+
value: fontFamily,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
|
|
17
28
|
export const toValue = (
|
|
18
|
-
|
|
19
|
-
|
|
29
|
+
styleValue: undefined | StyleValue,
|
|
30
|
+
transformValue?: TransformValue
|
|
20
31
|
): string => {
|
|
21
|
-
if (
|
|
32
|
+
if (styleValue === undefined) {
|
|
22
33
|
return "";
|
|
23
34
|
}
|
|
35
|
+
const transformedValue =
|
|
36
|
+
transformValue?.(styleValue) ?? fallbackTransform(styleValue);
|
|
37
|
+
const value = transformedValue ?? styleValue;
|
|
24
38
|
if (value.type === "unit") {
|
|
25
39
|
return value.value + (value.unit === "number" ? "" : value.unit);
|
|
26
40
|
}
|
|
27
41
|
if (value.type === "fontFamily") {
|
|
28
|
-
|
|
29
|
-
return value.value[0];
|
|
30
|
-
}
|
|
31
|
-
const family = value.value[0];
|
|
32
|
-
const fallbacks = SYSTEM_FONTS.get(family);
|
|
33
|
-
if (Array.isArray(fallbacks)) {
|
|
34
|
-
return [...value.value, ...fallbacks].join(", ");
|
|
35
|
-
}
|
|
36
|
-
return [...value.value, DEFAULT_FONT_FALLBACK].join(", ");
|
|
42
|
+
return value.value.join(", ");
|
|
37
43
|
}
|
|
38
44
|
if (value.type === "var") {
|
|
39
45
|
const fallbacks = [];
|
|
40
46
|
for (const fallback of value.fallbacks) {
|
|
41
|
-
fallbacks.push(toValue(fallback,
|
|
47
|
+
fallbacks.push(toValue(fallback, transformValue));
|
|
42
48
|
}
|
|
43
49
|
const fallbacksString =
|
|
44
50
|
fallbacks.length > 0 ? `, ${fallbacks.join(", ")}` : "";
|
|
@@ -62,7 +68,7 @@ export const toValue = (
|
|
|
62
68
|
}
|
|
63
69
|
|
|
64
70
|
if (value.type === "image") {
|
|
65
|
-
if (value.hidden) {
|
|
71
|
+
if (value.hidden || value.value.type !== "url") {
|
|
66
72
|
// We assume that property is background-image and use this to hide background layers
|
|
67
73
|
// In the future we might want to have a more generic way to hide values
|
|
68
74
|
// i.e. have knowledge about property-name, as none is property specific
|
|
@@ -70,7 +76,7 @@ export const toValue = (
|
|
|
70
76
|
}
|
|
71
77
|
|
|
72
78
|
// @todo image-set
|
|
73
|
-
return `url(${value.value.
|
|
79
|
+
return `url(${value.value.url})`;
|
|
74
80
|
}
|
|
75
81
|
|
|
76
82
|
if (value.type === "unparsed") {
|
|
@@ -85,11 +91,11 @@ export const toValue = (
|
|
|
85
91
|
}
|
|
86
92
|
|
|
87
93
|
if (value.type === "layers") {
|
|
88
|
-
return value.value.map((value) => toValue(value,
|
|
94
|
+
return value.value.map((value) => toValue(value, transformValue)).join(",");
|
|
89
95
|
}
|
|
90
96
|
|
|
91
97
|
if (value.type === "tuple") {
|
|
92
|
-
return value.value.map((value) => toValue(value,
|
|
98
|
+
return value.value.map((value) => toValue(value, transformValue)).join(" ");
|
|
93
99
|
}
|
|
94
100
|
|
|
95
101
|
// Will give ts error in case of missing type
|