@webstudio-is/css-engine 0.52.0 → 0.54.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.
@@ -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
+ };
@@ -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);
@@ -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(`${(0, import_to_property.toProperty)(property)}: ${(0, import_to_value.toValue)(value)}`);
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) {
@@ -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 toValue = (value, options = defaultOptions) => {
32
- if (value === void 0) {
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
- if (options.withFallback === false) {
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, options));
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.value.path}) /* id=${value.value.value.id} */`;
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, options)).join(",");
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, options)).join(" ");
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
  }
@@ -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
@@ -4,6 +4,7 @@ export * from "./to-value";
4
4
  export * from "./match-media";
5
5
  export * from "./equal-media";
6
6
  export * from "./compare-media";
7
+ export * from "./find-applicable-media";
7
8
  export {
8
9
  CssEngine
9
10
  };
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(`${toProperty(property)}: ${toValue(value)}`);
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) {
@@ -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 toValue = (value, options = defaultOptions) => {
9
- if (value === void 0) {
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
- if (options.withFallback === false) {
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, options));
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.value.path}) /* id=${value.value.value.id} */`;
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, options)).join(",");
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, options)).join(" ");
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,2 @@
1
+ import type { MediaRuleOptions } from "./rules";
2
+ export declare const findApplicableMedia: <Media extends MediaRuleOptions>(media: Media[], width: number) => Media | undefined;
@@ -0,0 +1 @@
1
+ export {};
@@ -5,3 +5,4 @@ export * from "./to-value";
5
5
  export * from "./match-media";
6
6
  export * from "./equal-media";
7
7
  export * from "./compare-media";
8
+ export * from "./find-applicable-media";
@@ -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" | "filter" | "float" | "fontFamily" | "width" | "height" | `--${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" | "flexBasis" | "flexDirection" | "flexGrow" | "flexShrink" | "flexWrap" | "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" | "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" | "willChange" | "wordBreak" | "wordSpacing" | "wordWrap" | "writingMode" | "zIndex">;
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 ToCssOptions = {
3
- withFallback: boolean;
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.52.0",
3
+ "version": "0.54.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.52.0"
12
+ "@webstudio-is/fonts": "^0.54.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/css-data": "^0.52.0",
22
+ "@webstudio-is/asset-uploader": "^0.54.0",
23
+ "@webstudio-is/css-data": "^0.54.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.1"
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
  });
@@ -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(selectorText: string, rule: CssRule) {
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
@@ -11,3 +11,4 @@ export * from "./to-value";
11
11
  export * from "./match-media";
12
12
  export * from "./equal-media";
13
13
  export * from "./compare-media";
14
+ export * from "./find-applicable-media";
@@ -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(`${toProperty(property)}: ${toValue(value)}`);
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(selectorText: string, style: Style) {
53
- this.styleMap = new StylePropertyMap();
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("withFallback=false", () => {
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
- { withFallback: false }
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 value = toValue({
70
- type: "layers",
71
- value: [
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
- value: {
81
- type: "asset",
82
- value: {
83
- type: "image",
84
- path: "foo.png",
82
+ path: "foo.png",
85
83
 
86
- id: "1234567890",
87
- projectId: "",
88
- format: "",
89
- size: 1212,
90
- name: "img",
91
- description: "",
92
- location: "REMOTE",
93
- createdAt: "",
94
- meta: { width: 1, height: 2 },
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) /* id=1234567890 */");
136
+ expect(value).toBe("auto,10px,calc(10px),url(foo.png)");
102
137
  });
103
138
 
104
139
  test("tuple", () => {
@@ -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 ToCssOptions = {
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
- value?: StyleValue,
19
- options: ToCssOptions = defaultOptions
29
+ styleValue: undefined | StyleValue,
30
+ transformValue?: TransformValue
20
31
  ): string => {
21
- if (value === undefined) {
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
- if (options.withFallback === false) {
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, options));
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.value.path}) /* id=${value.value.value.id} */`;
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, options)).join(",");
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, options)).join(" ");
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