@webstudio-is/css-engine 0.4.0 → 0.15.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/css-engine.cjs +5 -3
- package/lib/cjs/core/rules.cjs +20 -14
- package/lib/cjs/core/to-value.cjs +19 -2
- package/lib/core/css-engine.js +5 -3
- package/lib/core/rules.js +20 -14
- package/lib/core/to-value.js +19 -2
- package/package.json +1 -1
- package/src/core/css-engine.test.ts +87 -12
- package/src/core/css-engine.ts +7 -3
- package/src/core/rules.ts +32 -11
- package/src/core/to-value.ts +30 -2
|
@@ -81,8 +81,9 @@ class CssEngine {
|
|
|
81
81
|
}
|
|
82
82
|
addPlaintextRule(cssText) {
|
|
83
83
|
const rule = __privateGet(this, _plainRules).get(cssText);
|
|
84
|
-
if (rule !== void 0)
|
|
84
|
+
if (rule !== void 0) {
|
|
85
85
|
return rule;
|
|
86
|
+
}
|
|
86
87
|
__privateSet(this, _isDirty, true);
|
|
87
88
|
return __privateGet(this, _plainRules).set(cssText, new import_rules.PlaintextRule(cssText));
|
|
88
89
|
}
|
|
@@ -113,10 +114,11 @@ class CssEngine {
|
|
|
113
114
|
for (const plaintextRule of __privateGet(this, _plainRules).values()) {
|
|
114
115
|
css.push(plaintextRule.cssText);
|
|
115
116
|
}
|
|
116
|
-
for (const mediaRule of __privateGet(this, _mediaRules).values()) {
|
|
117
|
+
for (const mediaRule of import_rules.MediaRule.sort(__privateGet(this, _mediaRules).values())) {
|
|
117
118
|
const { cssText } = mediaRule;
|
|
118
|
-
if (cssText !== "")
|
|
119
|
+
if (cssText !== "") {
|
|
119
120
|
css.push(cssText);
|
|
121
|
+
}
|
|
120
122
|
}
|
|
121
123
|
__privateSet(this, _cssText, css.join("\n"));
|
|
122
124
|
return __privateGet(this, _cssText);
|
package/lib/cjs/core/rules.cjs
CHANGED
|
@@ -50,7 +50,7 @@ __export(rules_exports, {
|
|
|
50
50
|
module.exports = __toCommonJS(rules_exports);
|
|
51
51
|
var import_hyphenate_style_name = __toESM(require("hyphenate-style-name"), 1);
|
|
52
52
|
var import_to_value = require("./to-value");
|
|
53
|
-
var _styleMap, _isDirty, _string, _onChange,
|
|
53
|
+
var _styleMap, _isDirty, _string, _onChange, _mediaType;
|
|
54
54
|
class StylePropertyMap {
|
|
55
55
|
constructor() {
|
|
56
56
|
__privateAdd(this, _styleMap, /* @__PURE__ */ new Map());
|
|
@@ -76,8 +76,9 @@ class StylePropertyMap {
|
|
|
76
76
|
}
|
|
77
77
|
const block = [];
|
|
78
78
|
for (const [property, value] of __privateGet(this, _styleMap)) {
|
|
79
|
-
if (value === void 0)
|
|
79
|
+
if (value === void 0) {
|
|
80
80
|
continue;
|
|
81
|
+
}
|
|
81
82
|
block.push(`${(0, import_hyphenate_style_name.default)(property)}: ${(0, import_to_value.toValue)(value)}`);
|
|
82
83
|
}
|
|
83
84
|
__privateSet(this, _string, block.join("; "));
|
|
@@ -108,31 +109,39 @@ class StyleRule {
|
|
|
108
109
|
_onChange = new WeakMap();
|
|
109
110
|
class MediaRule {
|
|
110
111
|
constructor(options = {}) {
|
|
111
|
-
__privateAdd(this, _options, void 0);
|
|
112
112
|
this.rules = [];
|
|
113
113
|
__privateAdd(this, _mediaType, void 0);
|
|
114
|
-
|
|
114
|
+
this.options = options;
|
|
115
115
|
__privateSet(this, _mediaType, options.mediaType ?? "all");
|
|
116
116
|
}
|
|
117
|
+
static sort(mediaRules) {
|
|
118
|
+
return Array.from(mediaRules).sort((ruleA, ruleB) => {
|
|
119
|
+
return (ruleA.options.minWidth ?? -Number.MAX_SAFE_INTEGER) - (ruleB.options.minWidth ?? -Number.MAX_SAFE_INTEGER);
|
|
120
|
+
});
|
|
121
|
+
}
|
|
117
122
|
insertRule(rule) {
|
|
118
123
|
this.rules.push(rule);
|
|
119
124
|
return rule;
|
|
120
125
|
}
|
|
121
126
|
get cssText() {
|
|
122
|
-
if (this.rules.length === 0)
|
|
127
|
+
if (this.rules.length === 0) {
|
|
123
128
|
return "";
|
|
129
|
+
}
|
|
124
130
|
const rules = [];
|
|
125
131
|
for (const rule of this.rules) {
|
|
126
132
|
rules.push(` ${rule.cssText}`);
|
|
127
133
|
}
|
|
128
134
|
let conditionText = "";
|
|
129
|
-
const { minWidth, maxWidth } =
|
|
130
|
-
if (minWidth !== void 0)
|
|
135
|
+
const { minWidth, maxWidth } = this.options;
|
|
136
|
+
if (minWidth !== void 0) {
|
|
131
137
|
conditionText = `min-width: ${minWidth}px`;
|
|
132
|
-
|
|
138
|
+
}
|
|
139
|
+
if (maxWidth !== void 0) {
|
|
133
140
|
conditionText = `max-width: ${maxWidth}px`;
|
|
134
|
-
|
|
141
|
+
}
|
|
142
|
+
if (conditionText) {
|
|
135
143
|
conditionText = `and (${conditionText}) `;
|
|
144
|
+
}
|
|
136
145
|
return `@media ${__privateGet(this, _mediaType)} ${conditionText}{
|
|
137
146
|
${rules.join(
|
|
138
147
|
"\n"
|
|
@@ -140,7 +149,6 @@ ${rules.join(
|
|
|
140
149
|
}`;
|
|
141
150
|
}
|
|
142
151
|
}
|
|
143
|
-
_options = new WeakMap();
|
|
144
152
|
_mediaType = new WeakMap();
|
|
145
153
|
class PlaintextRule {
|
|
146
154
|
constructor(cssText) {
|
|
@@ -150,14 +158,12 @@ class PlaintextRule {
|
|
|
150
158
|
}
|
|
151
159
|
class FontFaceRule {
|
|
152
160
|
constructor(options) {
|
|
153
|
-
|
|
154
|
-
__privateSet(this, _options2, options);
|
|
161
|
+
this.options = options;
|
|
155
162
|
}
|
|
156
163
|
get cssText() {
|
|
157
|
-
const { fontFamily, fontStyle, fontWeight, fontDisplay, src } =
|
|
164
|
+
const { fontFamily, fontStyle, fontWeight, fontDisplay, src } = this.options;
|
|
158
165
|
return `@font-face {
|
|
159
166
|
font-family: ${fontFamily}; font-style: ${fontStyle}; font-weight: ${fontWeight}; font-display: ${fontDisplay}; src: ${src};
|
|
160
167
|
}`;
|
|
161
168
|
}
|
|
162
169
|
}
|
|
163
|
-
_options2 = new WeakMap();
|
|
@@ -25,9 +25,13 @@ var import_fonts = require("@webstudio-is/fonts");
|
|
|
25
25
|
const defaultOptions = {
|
|
26
26
|
withFallback: true
|
|
27
27
|
};
|
|
28
|
+
const assertUnreachable = (_arg, errorMessage) => {
|
|
29
|
+
throw new Error(errorMessage);
|
|
30
|
+
};
|
|
28
31
|
const toValue = (value, options = defaultOptions) => {
|
|
29
|
-
if (value === void 0)
|
|
32
|
+
if (value === void 0) {
|
|
30
33
|
return "";
|
|
34
|
+
}
|
|
31
35
|
if (value.type === "unit") {
|
|
32
36
|
return value.value + (value.unit === "number" ? "" : value.unit);
|
|
33
37
|
}
|
|
@@ -50,5 +54,18 @@ const toValue = (value, options = defaultOptions) => {
|
|
|
50
54
|
const fallbacksString = fallbacks.length > 0 ? `, ${fallbacks.join(", ")}` : "";
|
|
51
55
|
return `var(--${value.value}${fallbacksString})`;
|
|
52
56
|
}
|
|
53
|
-
|
|
57
|
+
if (value.type === "keyword") {
|
|
58
|
+
return value.value;
|
|
59
|
+
}
|
|
60
|
+
if (value.type === "invalid") {
|
|
61
|
+
return value.value;
|
|
62
|
+
}
|
|
63
|
+
if (value.type === "unset") {
|
|
64
|
+
return value.value;
|
|
65
|
+
}
|
|
66
|
+
if (value.type === "rgb") {
|
|
67
|
+
return `rgba(${value.r}, ${value.g}, ${value.b}, ${value.alpha})`;
|
|
68
|
+
}
|
|
69
|
+
assertUnreachable(value, `Unknown value type`);
|
|
70
|
+
throw new Error("Unknown value type");
|
|
54
71
|
};
|
package/lib/core/css-engine.js
CHANGED
|
@@ -63,8 +63,9 @@ class CssEngine {
|
|
|
63
63
|
}
|
|
64
64
|
addPlaintextRule(cssText) {
|
|
65
65
|
const rule = __privateGet(this, _plainRules).get(cssText);
|
|
66
|
-
if (rule !== void 0)
|
|
66
|
+
if (rule !== void 0) {
|
|
67
67
|
return rule;
|
|
68
|
+
}
|
|
68
69
|
__privateSet(this, _isDirty, true);
|
|
69
70
|
return __privateGet(this, _plainRules).set(cssText, new PlaintextRule(cssText));
|
|
70
71
|
}
|
|
@@ -95,10 +96,11 @@ class CssEngine {
|
|
|
95
96
|
for (const plaintextRule of __privateGet(this, _plainRules).values()) {
|
|
96
97
|
css.push(plaintextRule.cssText);
|
|
97
98
|
}
|
|
98
|
-
for (const mediaRule of __privateGet(this, _mediaRules).values()) {
|
|
99
|
+
for (const mediaRule of MediaRule.sort(__privateGet(this, _mediaRules).values())) {
|
|
99
100
|
const { cssText } = mediaRule;
|
|
100
|
-
if (cssText !== "")
|
|
101
|
+
if (cssText !== "") {
|
|
101
102
|
css.push(cssText);
|
|
103
|
+
}
|
|
102
104
|
}
|
|
103
105
|
__privateSet(this, _cssText, css.join("\n"));
|
|
104
106
|
return __privateGet(this, _cssText);
|
package/lib/core/rules.js
CHANGED
|
@@ -16,7 +16,7 @@ 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,
|
|
19
|
+
var _styleMap, _isDirty, _string, _onChange, _mediaType;
|
|
20
20
|
import hyphenate from "hyphenate-style-name";
|
|
21
21
|
import { toValue } from "./to-value";
|
|
22
22
|
class StylePropertyMap {
|
|
@@ -44,8 +44,9 @@ class StylePropertyMap {
|
|
|
44
44
|
}
|
|
45
45
|
const block = [];
|
|
46
46
|
for (const [property, value] of __privateGet(this, _styleMap)) {
|
|
47
|
-
if (value === void 0)
|
|
47
|
+
if (value === void 0) {
|
|
48
48
|
continue;
|
|
49
|
+
}
|
|
49
50
|
block.push(`${hyphenate(property)}: ${toValue(value)}`);
|
|
50
51
|
}
|
|
51
52
|
__privateSet(this, _string, block.join("; "));
|
|
@@ -76,31 +77,39 @@ class StyleRule {
|
|
|
76
77
|
_onChange = new WeakMap();
|
|
77
78
|
class MediaRule {
|
|
78
79
|
constructor(options = {}) {
|
|
79
|
-
__privateAdd(this, _options, void 0);
|
|
80
80
|
this.rules = [];
|
|
81
81
|
__privateAdd(this, _mediaType, void 0);
|
|
82
|
-
|
|
82
|
+
this.options = options;
|
|
83
83
|
__privateSet(this, _mediaType, options.mediaType ?? "all");
|
|
84
84
|
}
|
|
85
|
+
static sort(mediaRules) {
|
|
86
|
+
return Array.from(mediaRules).sort((ruleA, ruleB) => {
|
|
87
|
+
return (ruleA.options.minWidth ?? -Number.MAX_SAFE_INTEGER) - (ruleB.options.minWidth ?? -Number.MAX_SAFE_INTEGER);
|
|
88
|
+
});
|
|
89
|
+
}
|
|
85
90
|
insertRule(rule) {
|
|
86
91
|
this.rules.push(rule);
|
|
87
92
|
return rule;
|
|
88
93
|
}
|
|
89
94
|
get cssText() {
|
|
90
|
-
if (this.rules.length === 0)
|
|
95
|
+
if (this.rules.length === 0) {
|
|
91
96
|
return "";
|
|
97
|
+
}
|
|
92
98
|
const rules = [];
|
|
93
99
|
for (const rule of this.rules) {
|
|
94
100
|
rules.push(` ${rule.cssText}`);
|
|
95
101
|
}
|
|
96
102
|
let conditionText = "";
|
|
97
|
-
const { minWidth, maxWidth } =
|
|
98
|
-
if (minWidth !== void 0)
|
|
103
|
+
const { minWidth, maxWidth } = this.options;
|
|
104
|
+
if (minWidth !== void 0) {
|
|
99
105
|
conditionText = `min-width: ${minWidth}px`;
|
|
100
|
-
|
|
106
|
+
}
|
|
107
|
+
if (maxWidth !== void 0) {
|
|
101
108
|
conditionText = `max-width: ${maxWidth}px`;
|
|
102
|
-
|
|
109
|
+
}
|
|
110
|
+
if (conditionText) {
|
|
103
111
|
conditionText = `and (${conditionText}) `;
|
|
112
|
+
}
|
|
104
113
|
return `@media ${__privateGet(this, _mediaType)} ${conditionText}{
|
|
105
114
|
${rules.join(
|
|
106
115
|
"\n"
|
|
@@ -108,7 +117,6 @@ ${rules.join(
|
|
|
108
117
|
}`;
|
|
109
118
|
}
|
|
110
119
|
}
|
|
111
|
-
_options = new WeakMap();
|
|
112
120
|
_mediaType = new WeakMap();
|
|
113
121
|
class PlaintextRule {
|
|
114
122
|
constructor(cssText) {
|
|
@@ -118,17 +126,15 @@ class PlaintextRule {
|
|
|
118
126
|
}
|
|
119
127
|
class FontFaceRule {
|
|
120
128
|
constructor(options) {
|
|
121
|
-
|
|
122
|
-
__privateSet(this, _options2, options);
|
|
129
|
+
this.options = options;
|
|
123
130
|
}
|
|
124
131
|
get cssText() {
|
|
125
|
-
const { fontFamily, fontStyle, fontWeight, fontDisplay, src } =
|
|
132
|
+
const { fontFamily, fontStyle, fontWeight, fontDisplay, src } = this.options;
|
|
126
133
|
return `@font-face {
|
|
127
134
|
font-family: ${fontFamily}; font-style: ${fontStyle}; font-weight: ${fontWeight}; font-display: ${fontDisplay}; src: ${src};
|
|
128
135
|
}`;
|
|
129
136
|
}
|
|
130
137
|
}
|
|
131
|
-
_options2 = new WeakMap();
|
|
132
138
|
export {
|
|
133
139
|
FontFaceRule,
|
|
134
140
|
MediaRule,
|
package/lib/core/to-value.js
CHANGED
|
@@ -2,9 +2,13 @@ import { DEFAULT_FONT_FALLBACK, SYSTEM_FONTS } from "@webstudio-is/fonts";
|
|
|
2
2
|
const defaultOptions = {
|
|
3
3
|
withFallback: true
|
|
4
4
|
};
|
|
5
|
+
const assertUnreachable = (_arg, errorMessage) => {
|
|
6
|
+
throw new Error(errorMessage);
|
|
7
|
+
};
|
|
5
8
|
const toValue = (value, options = defaultOptions) => {
|
|
6
|
-
if (value === void 0)
|
|
9
|
+
if (value === void 0) {
|
|
7
10
|
return "";
|
|
11
|
+
}
|
|
8
12
|
if (value.type === "unit") {
|
|
9
13
|
return value.value + (value.unit === "number" ? "" : value.unit);
|
|
10
14
|
}
|
|
@@ -27,7 +31,20 @@ const toValue = (value, options = defaultOptions) => {
|
|
|
27
31
|
const fallbacksString = fallbacks.length > 0 ? `, ${fallbacks.join(", ")}` : "";
|
|
28
32
|
return `var(--${value.value}${fallbacksString})`;
|
|
29
33
|
}
|
|
30
|
-
|
|
34
|
+
if (value.type === "keyword") {
|
|
35
|
+
return value.value;
|
|
36
|
+
}
|
|
37
|
+
if (value.type === "invalid") {
|
|
38
|
+
return value.value;
|
|
39
|
+
}
|
|
40
|
+
if (value.type === "unset") {
|
|
41
|
+
return value.value;
|
|
42
|
+
}
|
|
43
|
+
if (value.type === "rgb") {
|
|
44
|
+
return `rgba(${value.r}, ${value.g}, ${value.b}, ${value.alpha})`;
|
|
45
|
+
}
|
|
46
|
+
assertUnreachable(value, `Unknown value type`);
|
|
47
|
+
throw new Error("Unknown value type");
|
|
31
48
|
};
|
|
32
49
|
export {
|
|
33
50
|
toValue
|
package/package.json
CHANGED
|
@@ -5,16 +5,25 @@ const style0 = {
|
|
|
5
5
|
} as const;
|
|
6
6
|
|
|
7
7
|
const mediaRuleOptions0 = { minWidth: 0 } as const;
|
|
8
|
-
const
|
|
8
|
+
const mediaId0 = "0";
|
|
9
|
+
|
|
10
|
+
const style1 = {
|
|
11
|
+
display: { type: "keyword", value: "flex" },
|
|
12
|
+
} as const;
|
|
13
|
+
|
|
14
|
+
const mediaRuleOptions1 = { minWidth: 300 } as const;
|
|
15
|
+
const mediaId1 = "1";
|
|
9
16
|
|
|
10
17
|
describe("CssEngine", () => {
|
|
11
18
|
let engine: CssEngine;
|
|
12
19
|
|
|
13
|
-
|
|
20
|
+
const reset = () => {
|
|
14
21
|
engine = new CssEngine();
|
|
15
|
-
}
|
|
22
|
+
};
|
|
16
23
|
|
|
17
|
-
|
|
24
|
+
beforeEach(reset);
|
|
25
|
+
|
|
26
|
+
test("use default media rule when there is no matching one registered", () => {
|
|
18
27
|
engine.addStyleRule(".c", {
|
|
19
28
|
style: style0,
|
|
20
29
|
breakpoint: "x",
|
|
@@ -35,10 +44,10 @@ describe("CssEngine", () => {
|
|
|
35
44
|
}"
|
|
36
45
|
`);
|
|
37
46
|
|
|
38
|
-
engine.addMediaRule(
|
|
47
|
+
engine.addMediaRule(mediaId0, mediaRuleOptions0);
|
|
39
48
|
engine.addStyleRule(".c1", {
|
|
40
49
|
style: { color: { type: "keyword", value: "blue" } },
|
|
41
|
-
breakpoint:
|
|
50
|
+
breakpoint: mediaId0,
|
|
42
51
|
});
|
|
43
52
|
// Default media query should allways be the first to have the lowest source order specificity
|
|
44
53
|
expect(engine.cssText).toMatchInlineSnapshot(`
|
|
@@ -52,8 +61,74 @@ describe("CssEngine", () => {
|
|
|
52
61
|
`);
|
|
53
62
|
});
|
|
54
63
|
|
|
64
|
+
test("sort media queries based on lower min-width", () => {
|
|
65
|
+
engine.addMediaRule(mediaId1, mediaRuleOptions1);
|
|
66
|
+
engine.addStyleRule(".c2", {
|
|
67
|
+
style: style1,
|
|
68
|
+
breakpoint: mediaId1,
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
engine.addMediaRule(mediaId0, mediaRuleOptions0);
|
|
72
|
+
engine.addStyleRule(".c1", {
|
|
73
|
+
style: style0,
|
|
74
|
+
breakpoint: mediaId0,
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
engine.addStyleRule(".c3", {
|
|
78
|
+
style: style0,
|
|
79
|
+
breakpoint: "x",
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
// Default media query should allways be the first to have the lowest source order specificity
|
|
83
|
+
expect(engine.cssText).toMatchInlineSnapshot(`
|
|
84
|
+
"@media all {
|
|
85
|
+
.c3 { display: block }
|
|
86
|
+
}
|
|
87
|
+
@media all and (min-width: 0px) {
|
|
88
|
+
.c1 { display: block }
|
|
89
|
+
}
|
|
90
|
+
@media all and (min-width: 300px) {
|
|
91
|
+
.c2 { display: flex }
|
|
92
|
+
}"
|
|
93
|
+
`);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test("keep the sort order when minWidth is not defined", () => {
|
|
97
|
+
engine.addStyleRule(".c0", {
|
|
98
|
+
style: style0,
|
|
99
|
+
breakpoint: "x",
|
|
100
|
+
});
|
|
101
|
+
engine.addStyleRule(".c1", {
|
|
102
|
+
style: style1,
|
|
103
|
+
breakpoint: "x",
|
|
104
|
+
});
|
|
105
|
+
expect(engine.cssText).toMatchInlineSnapshot(`
|
|
106
|
+
"@media all {
|
|
107
|
+
.c0 { display: block }
|
|
108
|
+
.c1 { display: flex }
|
|
109
|
+
}"
|
|
110
|
+
`);
|
|
111
|
+
|
|
112
|
+
reset();
|
|
113
|
+
|
|
114
|
+
engine.addStyleRule(".c1", {
|
|
115
|
+
style: style1,
|
|
116
|
+
breakpoint: "x",
|
|
117
|
+
});
|
|
118
|
+
engine.addStyleRule(".c0", {
|
|
119
|
+
style: style0,
|
|
120
|
+
breakpoint: "x",
|
|
121
|
+
});
|
|
122
|
+
expect(engine.cssText).toMatchInlineSnapshot(`
|
|
123
|
+
"@media all {
|
|
124
|
+
.c1 { display: flex }
|
|
125
|
+
.c0 { display: block }
|
|
126
|
+
}"
|
|
127
|
+
`);
|
|
128
|
+
});
|
|
129
|
+
|
|
55
130
|
test("rule with multiple properties", () => {
|
|
56
|
-
engine.addMediaRule(
|
|
131
|
+
engine.addMediaRule(mediaId0, mediaRuleOptions0);
|
|
57
132
|
engine.addStyleRule(".c", {
|
|
58
133
|
style: {
|
|
59
134
|
...style0,
|
|
@@ -69,7 +144,7 @@ describe("CssEngine", () => {
|
|
|
69
144
|
});
|
|
70
145
|
|
|
71
146
|
test("hyphenate property", () => {
|
|
72
|
-
engine.addMediaRule(
|
|
147
|
+
engine.addMediaRule(mediaId0, mediaRuleOptions0);
|
|
73
148
|
engine.addStyleRule(".c", {
|
|
74
149
|
style: {
|
|
75
150
|
backgroundColor: { type: "keyword", value: "red" },
|
|
@@ -84,7 +159,7 @@ describe("CssEngine", () => {
|
|
|
84
159
|
});
|
|
85
160
|
|
|
86
161
|
test("add rule", () => {
|
|
87
|
-
engine.addMediaRule(
|
|
162
|
+
engine.addMediaRule(mediaId0, mediaRuleOptions0);
|
|
88
163
|
const rule1 = engine.addStyleRule(".c", {
|
|
89
164
|
style: {
|
|
90
165
|
...style0,
|
|
@@ -116,7 +191,7 @@ describe("CssEngine", () => {
|
|
|
116
191
|
});
|
|
117
192
|
|
|
118
193
|
test("update rule", () => {
|
|
119
|
-
engine.addMediaRule(
|
|
194
|
+
engine.addMediaRule(mediaId0, mediaRuleOptions0);
|
|
120
195
|
const rule = engine.addStyleRule(".c", {
|
|
121
196
|
style: {
|
|
122
197
|
...style0,
|
|
@@ -147,7 +222,7 @@ describe("CssEngine", () => {
|
|
|
147
222
|
});
|
|
148
223
|
|
|
149
224
|
test("don't override media queries", () => {
|
|
150
|
-
engine.addMediaRule(
|
|
225
|
+
engine.addMediaRule(mediaId0, mediaRuleOptions0);
|
|
151
226
|
engine.addStyleRule(".c", {
|
|
152
227
|
style: style0,
|
|
153
228
|
breakpoint: "0",
|
|
@@ -157,7 +232,7 @@ describe("CssEngine", () => {
|
|
|
157
232
|
.c { display: block }
|
|
158
233
|
}"
|
|
159
234
|
`);
|
|
160
|
-
engine.addMediaRule(
|
|
235
|
+
engine.addMediaRule(mediaId0, mediaRuleOptions0);
|
|
161
236
|
expect(engine.cssText).toMatchInlineSnapshot(`
|
|
162
237
|
"@media all and (min-width: 0px) {
|
|
163
238
|
.c { display: block }
|
package/src/core/css-engine.ts
CHANGED
|
@@ -47,7 +47,9 @@ export class CssEngine {
|
|
|
47
47
|
}
|
|
48
48
|
addPlaintextRule(cssText: string) {
|
|
49
49
|
const rule = this.#plainRules.get(cssText);
|
|
50
|
-
if (rule !== undefined)
|
|
50
|
+
if (rule !== undefined) {
|
|
51
|
+
return rule;
|
|
52
|
+
}
|
|
51
53
|
this.#isDirty = true;
|
|
52
54
|
return this.#plainRules.set(cssText, new PlaintextRule(cssText));
|
|
53
55
|
}
|
|
@@ -80,9 +82,11 @@ export class CssEngine {
|
|
|
80
82
|
for (const plaintextRule of this.#plainRules.values()) {
|
|
81
83
|
css.push(plaintextRule.cssText);
|
|
82
84
|
}
|
|
83
|
-
for (const mediaRule of this.#mediaRules.values()) {
|
|
85
|
+
for (const mediaRule of MediaRule.sort(this.#mediaRules.values())) {
|
|
84
86
|
const { cssText } = mediaRule;
|
|
85
|
-
if (cssText !== "")
|
|
87
|
+
if (cssText !== "") {
|
|
88
|
+
css.push(cssText);
|
|
89
|
+
}
|
|
86
90
|
}
|
|
87
91
|
this.#cssText = css.join("\n");
|
|
88
92
|
return this.#cssText;
|
package/src/core/rules.ts
CHANGED
|
@@ -26,7 +26,9 @@ class StylePropertyMap {
|
|
|
26
26
|
}
|
|
27
27
|
const block: Array<string> = [];
|
|
28
28
|
for (const [property, value] of this.#styleMap) {
|
|
29
|
-
if (value === undefined)
|
|
29
|
+
if (value === undefined) {
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
30
32
|
block.push(`${hyphenate(property)}: ${toValue(value)}`);
|
|
31
33
|
}
|
|
32
34
|
this.#string = block.join("; ");
|
|
@@ -63,11 +65,22 @@ export type MediaRuleOptions = {
|
|
|
63
65
|
};
|
|
64
66
|
|
|
65
67
|
export class MediaRule {
|
|
66
|
-
|
|
68
|
+
// Sort media rules by minWidth.
|
|
69
|
+
// Needed to ensure that more specific media rules are inserted after less specific ones.
|
|
70
|
+
// So that they get a higher specificity.
|
|
71
|
+
static sort(mediaRules: Iterable<MediaRule>) {
|
|
72
|
+
return Array.from(mediaRules).sort((ruleA, ruleB) => {
|
|
73
|
+
return (
|
|
74
|
+
(ruleA.options.minWidth ?? -Number.MAX_SAFE_INTEGER) -
|
|
75
|
+
(ruleB.options.minWidth ?? -Number.MAX_SAFE_INTEGER)
|
|
76
|
+
);
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
options: MediaRuleOptions;
|
|
67
80
|
rules: Array<StyleRule | PlaintextRule> = [];
|
|
68
81
|
#mediaType;
|
|
69
82
|
constructor(options: MediaRuleOptions = {}) {
|
|
70
|
-
this
|
|
83
|
+
this.options = options;
|
|
71
84
|
this.#mediaType = options.mediaType ?? "all";
|
|
72
85
|
}
|
|
73
86
|
insertRule(rule: StyleRule | PlaintextRule) {
|
|
@@ -75,16 +88,24 @@ export class MediaRule {
|
|
|
75
88
|
return rule;
|
|
76
89
|
}
|
|
77
90
|
get cssText() {
|
|
78
|
-
if (this.rules.length === 0)
|
|
91
|
+
if (this.rules.length === 0) {
|
|
92
|
+
return "";
|
|
93
|
+
}
|
|
79
94
|
const rules = [];
|
|
80
95
|
for (const rule of this.rules) {
|
|
81
96
|
rules.push(` ${rule.cssText}`);
|
|
82
97
|
}
|
|
83
98
|
let conditionText = "";
|
|
84
|
-
const { minWidth, maxWidth } = this
|
|
85
|
-
if (minWidth !== undefined)
|
|
86
|
-
|
|
87
|
-
|
|
99
|
+
const { minWidth, maxWidth } = this.options;
|
|
100
|
+
if (minWidth !== undefined) {
|
|
101
|
+
conditionText = `min-width: ${minWidth}px`;
|
|
102
|
+
}
|
|
103
|
+
if (maxWidth !== undefined) {
|
|
104
|
+
conditionText = `max-width: ${maxWidth}px`;
|
|
105
|
+
}
|
|
106
|
+
if (conditionText) {
|
|
107
|
+
conditionText = `and (${conditionText}) `;
|
|
108
|
+
}
|
|
88
109
|
return `@media ${this.#mediaType} ${conditionText}{\n${rules.join(
|
|
89
110
|
"\n"
|
|
90
111
|
)}\n}`;
|
|
@@ -108,13 +129,13 @@ export type FontFaceOptions = {
|
|
|
108
129
|
};
|
|
109
130
|
|
|
110
131
|
export class FontFaceRule {
|
|
111
|
-
|
|
132
|
+
options: FontFaceOptions;
|
|
112
133
|
constructor(options: FontFaceOptions) {
|
|
113
|
-
this
|
|
134
|
+
this.options = options;
|
|
114
135
|
}
|
|
115
136
|
get cssText() {
|
|
116
137
|
const { fontFamily, fontStyle, fontWeight, fontDisplay, src } =
|
|
117
|
-
this
|
|
138
|
+
this.options;
|
|
118
139
|
return `@font-face {\n font-family: ${fontFamily}; font-style: ${fontStyle}; font-weight: ${fontWeight}; font-display: ${fontDisplay}; src: ${src};\n}`;
|
|
119
140
|
}
|
|
120
141
|
}
|
package/src/core/to-value.ts
CHANGED
|
@@ -9,11 +9,18 @@ const defaultOptions = {
|
|
|
9
9
|
withFallback: true,
|
|
10
10
|
};
|
|
11
11
|
|
|
12
|
+
// exhaustive check, should never happen in runtime as ts would give error
|
|
13
|
+
const assertUnreachable = (_arg: never, errorMessage: string) => {
|
|
14
|
+
throw new Error(errorMessage);
|
|
15
|
+
};
|
|
16
|
+
|
|
12
17
|
export const toValue = (
|
|
13
18
|
value?: StyleValue,
|
|
14
19
|
options: ToCssOptions = defaultOptions
|
|
15
20
|
): string => {
|
|
16
|
-
if (value === undefined)
|
|
21
|
+
if (value === undefined) {
|
|
22
|
+
return "";
|
|
23
|
+
}
|
|
17
24
|
if (value.type === "unit") {
|
|
18
25
|
return value.value + (value.unit === "number" ? "" : value.unit);
|
|
19
26
|
}
|
|
@@ -37,5 +44,26 @@ export const toValue = (
|
|
|
37
44
|
fallbacks.length > 0 ? `, ${fallbacks.join(", ")}` : "";
|
|
38
45
|
return `var(--${value.value}${fallbacksString})`;
|
|
39
46
|
}
|
|
40
|
-
|
|
47
|
+
|
|
48
|
+
if (value.type === "keyword") {
|
|
49
|
+
return value.value;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (value.type === "invalid") {
|
|
53
|
+
return value.value;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (value.type === "unset") {
|
|
57
|
+
return value.value;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (value.type === "rgb") {
|
|
61
|
+
return `rgba(${value.r}, ${value.g}, ${value.b}, ${value.alpha})`;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Will give ts error in case of missing type
|
|
65
|
+
assertUnreachable(value, `Unknown value type`);
|
|
66
|
+
|
|
67
|
+
// Will never happen
|
|
68
|
+
throw new Error("Unknown value type");
|
|
41
69
|
};
|