Skip to content

Commit e71fbd7

Browse files
committed
Compile media query range syntax to fallback when needed
1 parent 5f0236a commit e71fbd7

File tree

7 files changed

+303
-11
lines changed

7 files changed

+303
-11
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ A WIP CSS parser, transformer, and minifier written in Rust.
2828
- `clamp()` function
2929
- Alignment shorthands (e.g. `place-items`)
3030
- Two-value `overflow` shorthand
31+
- Media query range syntax (e.g. `@media (width <= 100px)` or `@media (100px < width < 500px)`)
3132
- **CSS modules** – TODO
3233

3334
## Benchmarks

build-prefixes.js

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,9 @@ let mdnFeatures = {
184184
placeSelf: mdn.css.properties['place-self'].__compat.support,
185185
placeContent: mdn.css.properties['place-content'].__compat.support,
186186
placeItems: mdn.css.properties['place-items'].__compat.support,
187-
overflowShorthand: mdn.css.properties['overflow'].multiple_keywords.__compat.support
187+
overflowShorthand: mdn.css.properties['overflow'].multiple_keywords.__compat.support,
188+
mediaRangeSyntax: mdn.css['at-rules'].media.range_syntax.__compat.support,
189+
mediaIntervalSyntax: {} // currently no browsers
188190
};
189191

190192
for (let feature in mdnFeatures) {
@@ -298,7 +300,7 @@ impl Feature {
298300
pub fn is_compatible(&self, browsers: Browsers) -> bool {
299301
match self {
300302
${[...compat].map(([features, browsers]) =>
301-
`${features.map(name => `Feature::${enumify(name)}`).join(' |\n ')} => {
303+
`${features.map(name => `Feature::${enumify(name)}`).join(' |\n ')} => {` + (Object.entries(browsers).length === 0 ? '}' : `
302304
${Object.entries(browsers).map(([browser, min]) =>
303305
`if let Some(version) = browsers.${browser} {
304306
if version >= ${min} {
@@ -307,7 +309,7 @@ impl Feature {
307309
}`
308310
).join('\n ')}
309311
}`
310-
).join('\n ')}
312+
)).join('\n ')}
311313
}
312314
false
313315
}

src/compat.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ pub enum Feature {
3131
DoublePositionGradients,
3232
FormValidation,
3333
Fullscreen,
34+
MediaIntervalSyntax,
35+
MediaRangeSyntax,
3436
OverflowShorthand,
3537
PlaceContent,
3638
PlaceItems,
@@ -1275,6 +1277,14 @@ impl Feature {
12751277
}
12761278
}
12771279
}
1280+
Feature::MediaRangeSyntax => {
1281+
if let Some(version) = browsers.firefox {
1282+
if version >= 4128768 {
1283+
return true
1284+
}
1285+
}
1286+
}
1287+
Feature::MediaIntervalSyntax => {}
12781288
}
12791289
false
12801290
}

src/lib.rs

Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2542,6 +2542,197 @@ mod tests {
25422542
minify_test("@media (example, all,), speech { .foo { color: chartreuse }}", "@media speech{.foo{color:#7fff00}}");
25432543
minify_test("@media &test, speech { .foo { color: chartreuse }}", "@media speech{.foo{color:#7fff00}}");
25442544
minify_test("@media &test { .foo { color: chartreuse }}", "@media not all{.foo{color:#7fff00}}");
2545+
minify_test("@media (min-width: calc(200px + 40px)) { .foo { color: chartreuse }}", "@media (min-width:240px){.foo{color:#7fff00}}");
2546+
minify_test("@media (min-width: calc(1em + 5px)) { .foo { color: chartreuse }}", "@media (min-width:calc(1em + 5px)){.foo{color:#7fff00}}");
2547+
2548+
prefix_test(
2549+
r#"
2550+
@media (width >= 240px) {
2551+
.foo {
2552+
color: chartreuse;
2553+
}
2554+
}
2555+
"#,
2556+
indoc! { r#"
2557+
@media (min-width: 240px) {
2558+
.foo {
2559+
color: #7fff00;
2560+
}
2561+
}
2562+
"#},
2563+
Browsers {
2564+
firefox: Some(60 << 16),
2565+
..Browsers::default()
2566+
}
2567+
);
2568+
2569+
prefix_test(
2570+
r#"
2571+
@media (width >= 240px) {
2572+
.foo {
2573+
color: chartreuse;
2574+
}
2575+
}
2576+
"#,
2577+
indoc! { r#"
2578+
@media (width >= 240px) {
2579+
.foo {
2580+
color: #7fff00;
2581+
}
2582+
}
2583+
"#},
2584+
Browsers {
2585+
firefox: Some(64 << 16),
2586+
..Browsers::default()
2587+
}
2588+
);
2589+
2590+
prefix_test(
2591+
r#"
2592+
@media (width > 240px) {
2593+
.foo {
2594+
color: chartreuse;
2595+
}
2596+
}
2597+
"#,
2598+
indoc! { r#"
2599+
@media (min-width: 240.001px) {
2600+
.foo {
2601+
color: #7fff00;
2602+
}
2603+
}
2604+
"#},
2605+
Browsers {
2606+
firefox: Some(60 << 16),
2607+
..Browsers::default()
2608+
}
2609+
);
2610+
2611+
prefix_test(
2612+
r#"
2613+
@media (width <= 240px) {
2614+
.foo {
2615+
color: chartreuse;
2616+
}
2617+
}
2618+
"#,
2619+
indoc! { r#"
2620+
@media (max-width: 240px) {
2621+
.foo {
2622+
color: #7fff00;
2623+
}
2624+
}
2625+
"#},
2626+
Browsers {
2627+
firefox: Some(60 << 16),
2628+
..Browsers::default()
2629+
}
2630+
);
2631+
2632+
prefix_test(
2633+
r#"
2634+
@media (width <= 240px) {
2635+
.foo {
2636+
color: chartreuse;
2637+
}
2638+
}
2639+
"#,
2640+
indoc! { r#"
2641+
@media (width <= 240px) {
2642+
.foo {
2643+
color: #7fff00;
2644+
}
2645+
}
2646+
"#},
2647+
Browsers {
2648+
firefox: Some(64 << 16),
2649+
..Browsers::default()
2650+
}
2651+
);
2652+
2653+
prefix_test(
2654+
r#"
2655+
@media (width < 240px) {
2656+
.foo {
2657+
color: chartreuse;
2658+
}
2659+
}
2660+
"#,
2661+
indoc! { r#"
2662+
@media (max-width: 239.999px) {
2663+
.foo {
2664+
color: #7fff00;
2665+
}
2666+
}
2667+
"#},
2668+
Browsers {
2669+
firefox: Some(60 << 16),
2670+
..Browsers::default()
2671+
}
2672+
);
2673+
2674+
prefix_test(
2675+
r#"
2676+
@media (100px <= width <= 200px) {
2677+
.foo {
2678+
color: chartreuse;
2679+
}
2680+
}
2681+
"#,
2682+
indoc! { r#"
2683+
@media (min-width: 100px) and (max-width: 200px) {
2684+
.foo {
2685+
color: #7fff00;
2686+
}
2687+
}
2688+
"#},
2689+
Browsers {
2690+
firefox: Some(85 << 16),
2691+
..Browsers::default()
2692+
}
2693+
);
2694+
2695+
prefix_test(
2696+
r#"
2697+
@media (100px < width < 200px) {
2698+
.foo {
2699+
color: chartreuse;
2700+
}
2701+
}
2702+
"#,
2703+
indoc! { r#"
2704+
@media (min-width: 100.001px) and (max-width: 199.999px) {
2705+
.foo {
2706+
color: #7fff00;
2707+
}
2708+
}
2709+
"#},
2710+
Browsers {
2711+
firefox: Some(85 << 16),
2712+
..Browsers::default()
2713+
}
2714+
);
2715+
2716+
prefix_test(
2717+
r#"
2718+
@media (200px >= width >= 100px) {
2719+
.foo {
2720+
color: chartreuse;
2721+
}
2722+
}
2723+
"#,
2724+
indoc! { r#"
2725+
@media (max-width: 200px) and (min-width: 100px) {
2726+
.foo {
2727+
color: #7fff00;
2728+
}
2729+
}
2730+
"#},
2731+
Browsers {
2732+
firefox: Some(85 << 16),
2733+
..Browsers::default()
2734+
}
2735+
);
25452736
}
25462737

25472738
#[test]

src/media_query.rs

Lines changed: 76 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ use crate::values::{
77
resolution::Resolution,
88
ratio::Ratio
99
};
10+
use crate::compat::Feature;
1011

1112
/// A type that encapsulates a media query list.
1213
#[derive(Clone, Debug, PartialEq)]
@@ -323,6 +324,18 @@ impl ToCss for MediaFeatureComparison {
323324
}
324325
}
325326

327+
impl MediaFeatureComparison {
328+
fn opposite(&self) -> MediaFeatureComparison {
329+
match self {
330+
MediaFeatureComparison::GreaterThan => MediaFeatureComparison::LessThan,
331+
MediaFeatureComparison::GreaterThanEqual => MediaFeatureComparison::LessThanEqual,
332+
MediaFeatureComparison::LessThan => MediaFeatureComparison::GreaterThan,
333+
MediaFeatureComparison::LessThanEqual => MediaFeatureComparison::GreaterThanEqual,
334+
MediaFeatureComparison::Equal => MediaFeatureComparison::Equal
335+
}
336+
}
337+
}
338+
326339
/// https://drafts.csswg.org/mediaqueries/#typedef-media-feature
327340
#[derive(Clone, Debug, PartialEq)]
328341
pub enum MediaFeature {
@@ -414,14 +427,7 @@ impl MediaFeature {
414427
end_operator
415428
})
416429
} else {
417-
// Flip operator.
418-
let operator = match operator.unwrap() {
419-
MediaFeatureComparison::GreaterThan => MediaFeatureComparison::LessThan,
420-
MediaFeatureComparison::GreaterThanEqual => MediaFeatureComparison::LessThanEqual,
421-
MediaFeatureComparison::LessThan => MediaFeatureComparison::GreaterThan,
422-
MediaFeatureComparison::LessThanEqual => MediaFeatureComparison::GreaterThanEqual,
423-
MediaFeatureComparison::Equal => MediaFeatureComparison::Equal
424-
};
430+
let operator = operator.unwrap().opposite();
425431
Ok(MediaFeature::Range {
426432
name,
427433
operator,
@@ -445,11 +451,26 @@ impl ToCss for MediaFeature {
445451
value.to_css(dest)?;
446452
}
447453
MediaFeature::Range { name, operator, value } => {
454+
// If range syntax is unsupported, use min/max prefix if possible.
455+
if let Some(targets) = dest.targets {
456+
if !Feature::MediaRangeSyntax.is_compatible(targets) {
457+
return write_min_max(operator, name, value, dest)
458+
}
459+
}
460+
448461
serialize_identifier(name, dest)?;
449462
operator.to_css(dest)?;
450463
value.to_css(dest)?;
451464
}
452465
MediaFeature::Interval { name, start, start_operator, end, end_operator } => {
466+
if let Some(targets) = dest.targets {
467+
if !Feature::MediaIntervalSyntax.is_compatible(targets) {
468+
write_min_max(&start_operator.opposite(), name, start, dest)?;
469+
dest.write_str(" and (")?;
470+
return write_min_max(end_operator, name, end, dest)
471+
}
472+
}
473+
453474
start.to_css(dest)?;
454475
start_operator.to_css(dest)?;
455476
serialize_identifier(name, dest)?;
@@ -462,6 +483,39 @@ impl ToCss for MediaFeature {
462483
}
463484
}
464485

486+
#[inline]
487+
fn write_min_max<W>(operator: &MediaFeatureComparison, name: &str, value: &MediaFeatureValue, dest: &mut Printer<W>) -> std::fmt::Result where W: std::fmt::Write {
488+
let prefix = match operator {
489+
MediaFeatureComparison::GreaterThan |
490+
MediaFeatureComparison::GreaterThanEqual => Some("min-"),
491+
MediaFeatureComparison::LessThan |
492+
MediaFeatureComparison::LessThanEqual => Some("max-"),
493+
MediaFeatureComparison::Equal => None
494+
};
495+
496+
if let Some(prefix) = prefix {
497+
dest.write_str(prefix)?;
498+
}
499+
500+
serialize_identifier(name, dest)?;
501+
dest.delim(':', false)?;
502+
503+
let adjusted = match operator {
504+
MediaFeatureComparison::GreaterThan => Some(value.clone() + 0.001),
505+
MediaFeatureComparison::LessThan => Some(value.clone() + -0.001),
506+
_ => None
507+
};
508+
509+
if let Some(value) = adjusted {
510+
value.to_css(dest)?;
511+
} else {
512+
value.to_css(dest)?;
513+
}
514+
515+
dest.write_char(')')?;
516+
Ok(())
517+
}
518+
465519
#[derive(Clone, Debug, PartialEq)]
466520
pub enum MediaFeatureValue {
467521
Length(Length),
@@ -510,6 +564,20 @@ impl ToCss for MediaFeatureValue {
510564
}
511565
}
512566

567+
impl std::ops::Add<f32> for MediaFeatureValue {
568+
type Output = Self;
569+
570+
fn add(self, other: f32) -> MediaFeatureValue {
571+
match self {
572+
MediaFeatureValue::Length(len) => MediaFeatureValue::Length(len + Length::px(other)),
573+
MediaFeatureValue::Number(num) => MediaFeatureValue::Number(num + other),
574+
MediaFeatureValue::Resolution(res) => MediaFeatureValue::Resolution(res + other),
575+
MediaFeatureValue::Ratio(ratio) => MediaFeatureValue::Ratio(ratio + other),
576+
MediaFeatureValue::Ident(id) => MediaFeatureValue::Ident(id)
577+
}
578+
}
579+
}
580+
513581
/// Consumes an operation or a colon, or returns an error.
514582
fn consume_operation_or_colon<'i, 't>(input: &mut Parser<'i, 't>, allow_colon: bool) -> Result<Option<MediaFeatureComparison>, ParseError<'i, ()>> {
515583
let location = input.current_source_location();

0 commit comments

Comments
 (0)