Skip to content

Commit 334524c

Browse files
committed
Refactor color fallback generation
Fixes parcel-bundler#108
1 parent 5079e35 commit 334524c

File tree

5 files changed

+217
-50
lines changed

5 files changed

+217
-50
lines changed

scripts/build-prefixes.js

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -180,9 +180,7 @@ let mdnFeatures = {
180180
logicalSize: mdn.css.properties['inline-size'].__compat.support,
181181
logicalTextAlign: mdn.css.properties['text-align']['flow_relative_values_start_and_end'].__compat.support,
182182
labColors: mdn.css.types.color.lab.__compat.support,
183-
lchColors: mdn.css.types.color.lch.__compat.support,
184183
oklabColors: {},
185-
oklchColors: {},
186184
colorFunction: mdn.css.types.color.color.__compat.support
187185
};
188186

src/compat.rs

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,6 @@ pub enum Feature {
3636
FormValidation,
3737
Fullscreen,
3838
LabColors,
39-
LchColors,
4039
LogicalBorderRadius,
4140
LogicalBorders,
4241
LogicalInset,
@@ -47,7 +46,6 @@ pub enum Feature {
4746
MediaIntervalSyntax,
4847
MediaRangeSyntax,
4948
OklabColors,
50-
OklchColors,
5149
OverflowShorthand,
5250
P3Colors,
5351
PlaceContent,
@@ -1157,8 +1155,7 @@ impl Feature {
11571155
Feature::CssNesting |
11581156
Feature::CustomMediaQueries |
11591157
Feature::MediaIntervalSyntax |
1160-
Feature::OklabColors |
1161-
Feature::OklchColors => {
1158+
Feature::OklabColors => {
11621159
return false
11631160
}
11641161
Feature::DoublePositionGradients => {
@@ -1615,7 +1612,6 @@ impl Feature {
16151612
}
16161613
}
16171614
Feature::LabColors |
1618-
Feature::LchColors |
16191615
Feature::ColorFunction => {
16201616
if let Some(version) = browsers.safari {
16211617
if version < 983040 {

src/lib.rs

Lines changed: 144 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8707,7 +8707,7 @@ mod tests {
87078707
indoc! { r#"
87088708
.foo {
87098709
background-color: #494745;
8710-
background-color: oklab(40% .001236 .0039);
8710+
background-color: lab(30.4045% .415295 1.4957);
87118711
}
87128712
"#},
87138713
Browsers {
@@ -8721,7 +8721,7 @@ mod tests {
87218721
indoc! { r#"
87228722
.foo {
87238723
background-color: #7e250f;
8724-
background-color: oklch(40% .126874 34.5686);
8724+
background-color: lab(29.2661% 38.2437 35.3889);
87258725
}
87268726
"#},
87278727
Browsers {
@@ -8833,6 +8833,63 @@ mod tests {
88338833
}
88348834
);
88358835

8836+
prefix_test(
8837+
".foo { background-color: color(display-p3 0.43313 0.50108 0.37950); }",
8838+
indoc! { r#"
8839+
.foo {
8840+
background-color: #6a805d;
8841+
background-color: color(display-p3 .43313 .50108 .3795);
8842+
}
8843+
"#},
8844+
Browsers {
8845+
chrome: Some(90 << 16),
8846+
safari: Some(14 << 16),
8847+
..Browsers::default()
8848+
}
8849+
);
8850+
8851+
prefix_test(
8852+
".foo { background-color: color(display-p3 0.43313 0.50108 0.37950); }",
8853+
indoc! { r#"
8854+
.foo {
8855+
background-color: color(display-p3 .43313 .50108 .3795);
8856+
}
8857+
"#},
8858+
Browsers {
8859+
safari: Some(14 << 16),
8860+
..Browsers::default()
8861+
}
8862+
);
8863+
8864+
prefix_test(
8865+
".foo { background-color: color(display-p3 0.43313 0.50108 0.37950); }",
8866+
indoc! { r#"
8867+
.foo {
8868+
background-color: #6a805d;
8869+
background-color: color(display-p3 .43313 .50108 .3795);
8870+
}
8871+
"#},
8872+
Browsers {
8873+
chrome: Some(90 << 16),
8874+
safari: Some(15 << 16),
8875+
..Browsers::default()
8876+
}
8877+
);
8878+
8879+
prefix_test(
8880+
".foo { background-color: color(display-p3 0.43313 0.50108 0.37950); }",
8881+
indoc! { r#"
8882+
.foo {
8883+
background-color: #6a805d;
8884+
background-color: color(display-p3 .43313 .50108 .3795);
8885+
}
8886+
"#},
8887+
Browsers {
8888+
chrome: Some(90 << 16),
8889+
..Browsers::default()
8890+
}
8891+
);
8892+
88368893
prefix_test(
88378894
".foo { background-color: color(a98-rgb 0.44091 0.49971 0.37408); }",
88388895
indoc! { r#"
@@ -8847,6 +8904,19 @@ mod tests {
88478904
}
88488905
);
88498906

8907+
prefix_test(
8908+
".foo { background-color: color(a98-rgb 0.44091 0.49971 0.37408); }",
8909+
indoc! { r#"
8910+
.foo {
8911+
background-color: color(a98-rgb .44091 .49971 .37408);
8912+
}
8913+
"#},
8914+
Browsers {
8915+
safari: Some(15 << 16),
8916+
..Browsers::default()
8917+
}
8918+
);
8919+
88508920
prefix_test(
88518921
".foo { background-color: color(prophoto-rgb 0.36589 0.41717 0.31333); }",
88528922
indoc! { r#"
@@ -10126,6 +10196,78 @@ mod tests {
1012610196
..Browsers::default()
1012710197
});
1012810198

10199+
prefix_test(r#"
10200+
.foo {
10201+
--foo: color(display-p3 0 1 0);
10202+
}
10203+
"#, indoc! {r#"
10204+
.foo {
10205+
--foo: #00f942;
10206+
}
10207+
10208+
@supports (color: color(display-p3 0 0 0)) {
10209+
.foo {
10210+
--foo: color(display-p3 0 1 0);
10211+
}
10212+
}
10213+
"#}, Browsers {
10214+
safari: Some(14 << 16),
10215+
chrome: Some(90 << 16),
10216+
..Browsers::default()
10217+
});
10218+
10219+
prefix_test(r#"
10220+
.foo {
10221+
--foo: color(display-p3 0 1 0);
10222+
}
10223+
"#, indoc! {r#"
10224+
.foo {
10225+
--foo: color(display-p3 0 1 0);
10226+
}
10227+
"#}, Browsers {
10228+
safari: Some(14 << 16),
10229+
..Browsers::default()
10230+
});
10231+
10232+
prefix_test(r#"
10233+
.foo {
10234+
--foo: color(display-p3 0 1 0);
10235+
}
10236+
"#, indoc! {r#"
10237+
.foo {
10238+
--foo: #00f942;
10239+
}
10240+
10241+
@supports (color: color(display-p3 0 0 0)) {
10242+
.foo {
10243+
--foo: color(display-p3 0 1 0);
10244+
}
10245+
}
10246+
"#}, Browsers {
10247+
safari: Some(15 << 16),
10248+
chrome: Some(90 << 16),
10249+
..Browsers::default()
10250+
});
10251+
10252+
prefix_test(r#"
10253+
.foo {
10254+
--foo: color(display-p3 0 1 0);
10255+
}
10256+
"#, indoc! {r#"
10257+
.foo {
10258+
--foo: #00f942;
10259+
}
10260+
10261+
@supports (color: color(display-p3 0 0 0)) {
10262+
.foo {
10263+
--foo: color(display-p3 0 1 0);
10264+
}
10265+
}
10266+
"#}, Browsers {
10267+
chrome: Some(90 << 16),
10268+
..Browsers::default()
10269+
});
10270+
1012910271
prefix_test(r#"
1013010272
.foo {
1013110273
text-decoration: underline;

src/properties/custom.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -509,7 +509,7 @@ impl<'i> TokenList<'i> {
509509
let mut fallbacks = ColorFallbackKind::empty();
510510
for token in &self.0 {
511511
if let TokenOrValue::Color(color) = token {
512-
fallbacks |= color.get_necessary_fallbacks(targets);
512+
fallbacks |= color.get_possible_fallbacks(targets);
513513
}
514514
}
515515

@@ -530,6 +530,8 @@ impl<'i> TokenList<'i> {
530530
}
531531

532532
pub(crate) fn get_fallbacks(&mut self, targets: Browsers) -> Vec<(SupportsCondition<'i>, Self)> {
533+
// Get the full list of possible fallbacks, and remove the lowest one, which will replace
534+
// the original declaration. The remaining fallbacks need to be added as @supports rules.
533535
let mut fallbacks = self.get_necessary_fallbacks(targets);
534536
let lowest_fallback = fallbacks.lowest();
535537
fallbacks.remove(lowest_fallback);
@@ -539,7 +541,7 @@ impl<'i> TokenList<'i> {
539541
res.push((ColorFallbackKind::P3.supports_condition(), self.get_fallback(ColorFallbackKind::P3)));
540542
}
541543

542-
if fallbacks.contains(ColorFallbackKind::LAB) || (!lowest_fallback.is_empty() && lowest_fallback != ColorFallbackKind::LAB) {
544+
if fallbacks.contains(ColorFallbackKind::LAB) {
543545
res.push((ColorFallbackKind::LAB.supports_condition(), self.get_fallback(ColorFallbackKind::LAB)));
544546
}
545547

src/values/color.rs

Lines changed: 68 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ bitflags! {
5656
const RGB = 0b01;
5757
const P3 = 0b10;
5858
const LAB = 0b100;
59+
const OKLAB = 0b1000;
5960
}
6061
}
6162

@@ -93,6 +94,24 @@ impl ColorFallbackKind {
9394
*self & ColorFallbackKind::from_bits_truncate(self.bits().wrapping_neg())
9495
}
9596

97+
pub fn highest(&self) -> ColorFallbackKind {
98+
// This finds the highest set bit.
99+
if self.is_empty() {
100+
return ColorFallbackKind::empty();
101+
}
102+
103+
let zeros = 7 - self.bits().leading_zeros();
104+
ColorFallbackKind::from_bits_truncate(1 << zeros)
105+
}
106+
107+
pub fn and_below(&self) -> ColorFallbackKind {
108+
if self.is_empty() {
109+
return ColorFallbackKind::empty();
110+
}
111+
112+
*self | ColorFallbackKind::from_bits_truncate(self.bits() - 1)
113+
}
114+
96115
pub fn supports_condition<'i>(&self) -> SupportsCondition<'i> {
97116
let s = match *self {
98117
ColorFallbackKind::P3 => "color: color(display-p3 0 0 0)",
@@ -125,58 +144,68 @@ impl CssColor {
125144
P3::from(self).into()
126145
}
127146

128-
pub fn get_necessary_fallbacks(&self, targets: Browsers) -> ColorFallbackKind {
129-
let feature = match self {
130-
CssColor::CurrentColor | CssColor::RGBA(_) => return ColorFallbackKind::empty(),
147+
pub(crate) fn get_possible_fallbacks(&self, targets: Browsers) -> ColorFallbackKind {
148+
// Fallbacks occur in levels: Oklab -> Lab -> P3 -> RGB. We start with all levels
149+
// below and including the authored color space, and remove the ones that aren't
150+
// compatible with our browser targets.
151+
let mut fallbacks = match self {
152+
CssColor::CurrentColor | CssColor::RGBA(_) | CssColor::Float(..) => return ColorFallbackKind::empty(),
131153
CssColor::LAB(lab) => {
132154
match &**lab {
133-
LABColor::LAB(..) => Feature::LabColors,
134-
LABColor::LCH(..) => Feature::LchColors,
135-
LABColor::OKLAB(..) => Feature::OklabColors,
136-
LABColor::OKLCH(..) => Feature::OklchColors
155+
LABColor::LAB(..) | LABColor::LCH(..) => ColorFallbackKind::LAB.and_below(),
156+
LABColor::OKLAB(..) | LABColor::OKLCH(..) => ColorFallbackKind::OKLAB.and_below(),
137157
}
138158
},
139-
CssColor::Predefined(..) => Feature::ColorFunction,
140-
CssColor::Float(..) => return ColorFallbackKind::empty()
141-
};
159+
CssColor::Predefined(predefined) => {
160+
match &**predefined {
161+
PredefinedColor::DisplayP3(..) => ColorFallbackKind::P3.and_below(),
162+
_ => {
163+
if Feature::ColorFunction.is_compatible(targets) {
164+
return ColorFallbackKind::empty();
165+
}
142166

143-
let mut fallbacks = ColorFallbackKind::empty();
144-
if !feature.is_compatible(targets) {
145-
// Convert to lab if compatible. This should not affect interpolation
146-
// since this is always done in oklab by default, except for legacy
147-
// srgb syntaxes. See https://www.w3.org/TR/css-color-4/#interpolation-space.
148-
// If lab is not compatible with all targets, try P3 as some browsers
149-
// implemented this before other color spaces. Finally, fall back to
150-
// legacy sRGB if none of these are fully compatible with the targets.
151-
if Feature::LabColors.is_compatible(targets) {
152-
fallbacks |= ColorFallbackKind::LAB;
153-
} else if Feature::P3Colors.is_compatible(targets) {
154-
fallbacks |= ColorFallbackKind::P3;
155-
if feature == Feature::OklabColors || feature == Feature::OklchColors {
156-
fallbacks |= ColorFallbackKind::LAB;
157-
}
158-
} else {
159-
fallbacks |= ColorFallbackKind::RGB;
160-
161-
// Also include either lab or P3 if partially compatible, as these
162-
// support a much wider color gamut than sRGB. LAB will replace the
163-
// original color (e.g. oklab), whereas P3 will be in addition.
164-
if Feature::LabColors.is_partially_compatible(targets) {
165-
fallbacks |= ColorFallbackKind::LAB;
166-
} else if Feature::P3Colors.is_partially_compatible(targets) {
167-
fallbacks |= ColorFallbackKind::P3;
168-
169-
// Convert oklab to lab because it has better compatibility.
170-
if feature == Feature::OklabColors || feature == Feature::OklchColors {
171-
fallbacks |= ColorFallbackKind::LAB;
167+
ColorFallbackKind::LAB.and_below()
172168
}
173169
}
174170
}
171+
};
172+
173+
if fallbacks.contains(ColorFallbackKind::OKLAB) {
174+
if Feature::OklabColors.is_compatible(targets) {
175+
fallbacks.remove(ColorFallbackKind::LAB.and_below());
176+
}
177+
}
178+
179+
if fallbacks.contains(ColorFallbackKind::LAB) {
180+
if Feature::LabColors.is_compatible(targets) {
181+
fallbacks.remove(ColorFallbackKind::P3.and_below());
182+
} else if Feature::LabColors.is_partially_compatible(targets) {
183+
// We don't need P3 if Lab is supported by some of our targets.
184+
// No browser implements Lab but not P3.
185+
fallbacks.remove(ColorFallbackKind::P3);
186+
}
187+
}
188+
189+
if fallbacks.contains(ColorFallbackKind::P3) {
190+
if Feature::P3Colors.is_compatible(targets) {
191+
fallbacks.remove(ColorFallbackKind::RGB);
192+
} else if fallbacks.highest() != ColorFallbackKind::P3 && !Feature::P3Colors.is_partially_compatible(targets) {
193+
// Remove P3 if it isn't supported by any targets, and wasn't the
194+
// original authored color.
195+
fallbacks.remove(ColorFallbackKind::P3);
196+
}
175197
}
176198

177199
fallbacks
178200
}
179201

202+
pub fn get_necessary_fallbacks(&self, targets: Browsers) -> ColorFallbackKind {
203+
// Get the full set of possible fallbacks, and remove the highest one, which
204+
// will replace the original declaration. The remaining fallbacks need to be added.
205+
let fallbacks = self.get_possible_fallbacks(targets);
206+
fallbacks - fallbacks.highest()
207+
}
208+
180209
pub(crate) fn get_fallback(&self, kind: ColorFallbackKind) -> CssColor {
181210
if matches!(self, CssColor::RGBA(_)) {
182211
return self.clone();

0 commit comments

Comments
 (0)