@webstudio-is/css-engine 0.4.1 → 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.
@@ -114,7 +114,7 @@ class CssEngine {
114
114
  for (const plaintextRule of __privateGet(this, _plainRules).values()) {
115
115
  css.push(plaintextRule.cssText);
116
116
  }
117
- for (const mediaRule of __privateGet(this, _mediaRules).values()) {
117
+ for (const mediaRule of import_rules.MediaRule.sort(__privateGet(this, _mediaRules).values())) {
118
118
  const { cssText } = mediaRule;
119
119
  if (cssText !== "") {
120
120
  css.push(cssText);
@@ -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, _options, _mediaType, _options2;
53
+ var _styleMap, _isDirty, _string, _onChange, _mediaType;
54
54
  class StylePropertyMap {
55
55
  constructor() {
56
56
  __privateAdd(this, _styleMap, /* @__PURE__ */ new Map());
@@ -109,12 +109,16 @@ class StyleRule {
109
109
  _onChange = new WeakMap();
110
110
  class MediaRule {
111
111
  constructor(options = {}) {
112
- __privateAdd(this, _options, void 0);
113
112
  this.rules = [];
114
113
  __privateAdd(this, _mediaType, void 0);
115
- __privateSet(this, _options, options);
114
+ this.options = options;
116
115
  __privateSet(this, _mediaType, options.mediaType ?? "all");
117
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
+ }
118
122
  insertRule(rule) {
119
123
  this.rules.push(rule);
120
124
  return rule;
@@ -128,7 +132,7 @@ class MediaRule {
128
132
  rules.push(` ${rule.cssText}`);
129
133
  }
130
134
  let conditionText = "";
131
- const { minWidth, maxWidth } = __privateGet(this, _options);
135
+ const { minWidth, maxWidth } = this.options;
132
136
  if (minWidth !== void 0) {
133
137
  conditionText = `min-width: ${minWidth}px`;
134
138
  }
@@ -145,7 +149,6 @@ ${rules.join(
145
149
  }`;
146
150
  }
147
151
  }
148
- _options = new WeakMap();
149
152
  _mediaType = new WeakMap();
150
153
  class PlaintextRule {
151
154
  constructor(cssText) {
@@ -155,14 +158,12 @@ class PlaintextRule {
155
158
  }
156
159
  class FontFaceRule {
157
160
  constructor(options) {
158
- __privateAdd(this, _options2, void 0);
159
- __privateSet(this, _options2, options);
161
+ this.options = options;
160
162
  }
161
163
  get cssText() {
162
- const { fontFamily, fontStyle, fontWeight, fontDisplay, src } = __privateGet(this, _options2);
164
+ const { fontFamily, fontStyle, fontWeight, fontDisplay, src } = this.options;
163
165
  return `@font-face {
164
166
  font-family: ${fontFamily}; font-style: ${fontStyle}; font-weight: ${fontWeight}; font-display: ${fontDisplay}; src: ${src};
165
167
  }`;
166
168
  }
167
169
  }
168
- _options2 = new WeakMap();
@@ -25,6 +25,9 @@ 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
32
  if (value === void 0) {
30
33
  return "";
@@ -51,5 +54,18 @@ const toValue = (value, options = defaultOptions) => {
51
54
  const fallbacksString = fallbacks.length > 0 ? `, ${fallbacks.join(", ")}` : "";
52
55
  return `var(--${value.value}${fallbacksString})`;
53
56
  }
54
- return value.value;
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");
55
71
  };
@@ -96,7 +96,7 @@ class CssEngine {
96
96
  for (const plaintextRule of __privateGet(this, _plainRules).values()) {
97
97
  css.push(plaintextRule.cssText);
98
98
  }
99
- for (const mediaRule of __privateGet(this, _mediaRules).values()) {
99
+ for (const mediaRule of MediaRule.sort(__privateGet(this, _mediaRules).values())) {
100
100
  const { cssText } = mediaRule;
101
101
  if (cssText !== "") {
102
102
  css.push(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, _options, _mediaType, _options2;
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 {
@@ -77,12 +77,16 @@ class StyleRule {
77
77
  _onChange = new WeakMap();
78
78
  class MediaRule {
79
79
  constructor(options = {}) {
80
- __privateAdd(this, _options, void 0);
81
80
  this.rules = [];
82
81
  __privateAdd(this, _mediaType, void 0);
83
- __privateSet(this, _options, options);
82
+ this.options = options;
84
83
  __privateSet(this, _mediaType, options.mediaType ?? "all");
85
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
+ }
86
90
  insertRule(rule) {
87
91
  this.rules.push(rule);
88
92
  return rule;
@@ -96,7 +100,7 @@ class MediaRule {
96
100
  rules.push(` ${rule.cssText}`);
97
101
  }
98
102
  let conditionText = "";
99
- const { minWidth, maxWidth } = __privateGet(this, _options);
103
+ const { minWidth, maxWidth } = this.options;
100
104
  if (minWidth !== void 0) {
101
105
  conditionText = `min-width: ${minWidth}px`;
102
106
  }
@@ -113,7 +117,6 @@ ${rules.join(
113
117
  }`;
114
118
  }
115
119
  }
116
- _options = new WeakMap();
117
120
  _mediaType = new WeakMap();
118
121
  class PlaintextRule {
119
122
  constructor(cssText) {
@@ -123,17 +126,15 @@ class PlaintextRule {
123
126
  }
124
127
  class FontFaceRule {
125
128
  constructor(options) {
126
- __privateAdd(this, _options2, void 0);
127
- __privateSet(this, _options2, options);
129
+ this.options = options;
128
130
  }
129
131
  get cssText() {
130
- const { fontFamily, fontStyle, fontWeight, fontDisplay, src } = __privateGet(this, _options2);
132
+ const { fontFamily, fontStyle, fontWeight, fontDisplay, src } = this.options;
131
133
  return `@font-face {
132
134
  font-family: ${fontFamily}; font-style: ${fontStyle}; font-weight: ${fontWeight}; font-display: ${fontDisplay}; src: ${src};
133
135
  }`;
134
136
  }
135
137
  }
136
- _options2 = new WeakMap();
137
138
  export {
138
139
  FontFaceRule,
139
140
  MediaRule,
@@ -2,6 +2,9 @@ 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
9
  if (value === void 0) {
7
10
  return "";
@@ -28,7 +31,20 @@ const toValue = (value, options = defaultOptions) => {
28
31
  const fallbacksString = fallbacks.length > 0 ? `, ${fallbacks.join(", ")}` : "";
29
32
  return `var(--${value.value}${fallbacksString})`;
30
33
  }
31
- return value.value;
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");
32
48
  };
33
49
  export {
34
50
  toValue
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webstudio-is/css-engine",
3
- "version": "0.4.1",
3
+ "version": "0.15.0",
4
4
  "description": "CSS Renderer for Webstudio",
5
5
  "author": "Webstudio <github@webstudio.is>",
6
6
  "homepage": "https://webstudio.is",
@@ -5,16 +5,25 @@ const style0 = {
5
5
  } as const;
6
6
 
7
7
  const mediaRuleOptions0 = { minWidth: 0 } as const;
8
- const mediaId = "0";
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
- beforeEach(() => {
20
+ const reset = () => {
14
21
  engine = new CssEngine();
15
- });
22
+ };
16
23
 
17
- test("use default media rule when there is no matching one registrered", () => {
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(mediaId, mediaRuleOptions0);
47
+ engine.addMediaRule(mediaId0, mediaRuleOptions0);
39
48
  engine.addStyleRule(".c1", {
40
49
  style: { color: { type: "keyword", value: "blue" } },
41
- breakpoint: mediaId,
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(mediaId, mediaRuleOptions0);
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(mediaId, mediaRuleOptions0);
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(mediaId, mediaRuleOptions0);
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(mediaId, mediaRuleOptions0);
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(mediaId, mediaRuleOptions0);
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(mediaId, mediaRuleOptions0);
235
+ engine.addMediaRule(mediaId0, mediaRuleOptions0);
161
236
  expect(engine.cssText).toMatchInlineSnapshot(`
162
237
  "@media all and (min-width: 0px) {
163
238
  .c { display: block }
@@ -82,7 +82,7 @@ export class CssEngine {
82
82
  for (const plaintextRule of this.#plainRules.values()) {
83
83
  css.push(plaintextRule.cssText);
84
84
  }
85
- for (const mediaRule of this.#mediaRules.values()) {
85
+ for (const mediaRule of MediaRule.sort(this.#mediaRules.values())) {
86
86
  const { cssText } = mediaRule;
87
87
  if (cssText !== "") {
88
88
  css.push(cssText);
package/src/core/rules.ts CHANGED
@@ -65,11 +65,22 @@ export type MediaRuleOptions = {
65
65
  };
66
66
 
67
67
  export class MediaRule {
68
- #options: MediaRuleOptions;
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;
69
80
  rules: Array<StyleRule | PlaintextRule> = [];
70
81
  #mediaType;
71
82
  constructor(options: MediaRuleOptions = {}) {
72
- this.#options = options;
83
+ this.options = options;
73
84
  this.#mediaType = options.mediaType ?? "all";
74
85
  }
75
86
  insertRule(rule: StyleRule | PlaintextRule) {
@@ -85,7 +96,7 @@ export class MediaRule {
85
96
  rules.push(` ${rule.cssText}`);
86
97
  }
87
98
  let conditionText = "";
88
- const { minWidth, maxWidth } = this.#options;
99
+ const { minWidth, maxWidth } = this.options;
89
100
  if (minWidth !== undefined) {
90
101
  conditionText = `min-width: ${minWidth}px`;
91
102
  }
@@ -118,13 +129,13 @@ export type FontFaceOptions = {
118
129
  };
119
130
 
120
131
  export class FontFaceRule {
121
- #options: FontFaceOptions;
132
+ options: FontFaceOptions;
122
133
  constructor(options: FontFaceOptions) {
123
- this.#options = options;
134
+ this.options = options;
124
135
  }
125
136
  get cssText() {
126
137
  const { fontFamily, fontStyle, fontWeight, fontDisplay, src } =
127
- this.#options;
138
+ this.options;
128
139
  return `@font-face {\n font-family: ${fontFamily}; font-style: ${fontStyle}; font-weight: ${fontWeight}; font-display: ${fontDisplay}; src: ${src};\n}`;
129
140
  }
130
141
  }
@@ -9,6 +9,11 @@ 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
@@ -39,5 +44,26 @@ export const toValue = (
39
44
  fallbacks.length > 0 ? `, ${fallbacks.join(", ")}` : "";
40
45
  return `var(--${value.value}${fallbacksString})`;
41
46
  }
42
- return value.value;
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");
43
69
  };