Skip to content

Commit 8bb82ca

Browse files
committed
fix #3748, fix #4293: lower css media range syntax
1 parent d8c3f87 commit 8bb82ca

File tree

7 files changed

+143
-17
lines changed

7 files changed

+143
-17
lines changed

CHANGELOG.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,28 @@
1414
console.log('size:', width + '\xD7' + height)
1515
```
1616

17+
* Lower CSS media query range syntax ([#3748](https://github.com/evanw/esbuild/issues/3748), [#4293](https://github.com/evanw/esbuild/issues/4293))
18+
19+
With this release, esbuild will now transform CSS media query range syntax into equivalent syntax using `min-`/`max-` prefixes for older browsers. For example, the following CSS:
20+
21+
```css
22+
@media (640px <= width <= 960px) {
23+
main {
24+
display: flex;
25+
}
26+
}
27+
```
28+
29+
will be transformed like this with a target such as `--target=chrome100` (or more specifically with `--supported:media-range=false` if desired):
30+
31+
```css
32+
@media (min-width: 640px) and (max-width: 960px) {
33+
main {
34+
display: flex;
35+
}
36+
}
37+
```
38+
1739
## 0.25.10
1840

1941
* Fix a panic in a minification edge case ([#4287](https://github.com/evanw/esbuild/issues/4287))

compat-table/src/caniuse.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ const jsFeatures: Record<string, JSFeature> = {
2929

3030
const cssFeatures: Record<string, CSSFeature> = {
3131
'css-matches-pseudo': 'IsPseudoClass',
32+
'css-media-range-syntax': 'MediaRange',
3233
}
3334

3435
const cssPrefixFeatures: Record<string, CSSProperty> = {

compat-table/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ export const cssFeatures = {
100100
InlineStyle: true,
101101
InsetProperty: true,
102102
IsPseudoClass: true,
103+
MediaRange: true,
103104
Modern_RGB_HSL: true,
104105
Nesting: true,
105106
RebeccaPurple: true,

internal/compat/css_table.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ const (
1818
InlineStyle
1919
InsetProperty
2020
IsPseudoClass
21+
MediaRange
2122
Modern_RGB_HSL
2223
Nesting
2324
RebeccaPurple
@@ -33,6 +34,7 @@ var StringToCSSFeature = map[string]CSSFeature{
3334
"inline-style": InlineStyle,
3435
"inset-property": InsetProperty,
3536
"is-pseudo-class": IsPseudoClass,
37+
"media-range": MediaRange,
3638
"modern-rgb-hsl": Modern_RGB_HSL,
3739
"nesting": Nesting,
3840
"rebecca-purple": RebeccaPurple,
@@ -111,6 +113,14 @@ var cssTable = map[CSSFeature]map[Engine][]versionRange{
111113
Opera: {{start: v{75, 0, 0}}},
112114
Safari: {{start: v{14, 0, 0}}},
113115
},
116+
MediaRange: {
117+
Chrome: {{start: v{104, 0, 0}}},
118+
Edge: {{start: v{104, 0, 0}}},
119+
Firefox: {{start: v{63, 0, 0}}},
120+
IOS: {{start: v{16, 4, 0}}},
121+
Opera: {{start: v{91, 0, 0}}},
122+
Safari: {{start: v{16, 4, 0}}},
123+
},
114124
Modern_RGB_HSL: {
115125
Chrome: {{start: v{66, 0, 0}}},
116126
Edge: {{start: v{79, 0, 0}}},

internal/css_ast/css_ast.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -973,6 +973,20 @@ func (cmp MQCmp) Flip() MQCmp {
973973
return cmp
974974
}
975975

976+
func (cmp MQCmp) Reverse() MQCmp {
977+
switch cmp {
978+
case MQCmpLt:
979+
return MQCmpGt
980+
case MQCmpLe:
981+
return MQCmpGe
982+
case MQCmpGt:
983+
return MQCmpLt
984+
case MQCmpGe:
985+
return MQCmpLe
986+
}
987+
return cmp
988+
}
989+
976990
type ComplexSelector struct {
977991
Selectors []CompoundSelector
978992
}

internal/css_parser/css_parser_media.go

Lines changed: 61 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package css_parser
33
import (
44
"strings"
55

6+
"github.com/evanw/esbuild/internal/compat"
67
"github.com/evanw/esbuild/internal/css_ast"
78
"github.com/evanw/esbuild/internal/css_lexer"
89
"github.com/evanw/esbuild/internal/logger"
@@ -191,73 +192,116 @@ func (p *parser) parseMediaInParens() (css_ast.MediaQuery, bool) {
191192
return css_ast.MediaQuery{}, false
192193
}
193194
tokens := p.convertTokens(p.tokens[start:end])
195+
loc := tokens[0].Loc
194196

195197
// Potentially pattern-match the tokens inside the parentheses
196198
if !isFunction && len(tokens) == 1 {
197199
if children := tokens[0].Children; children != nil {
198-
if mfPlain, ok := parsePlainOrBooleanMediaFeature(*children); ok {
199-
return mfPlain, true
200+
if term, ok := parsePlainOrBooleanMediaFeature(*children); ok {
201+
return css_ast.MediaQuery{Loc: loc, Data: term}, true
200202
}
201-
if mfRange, ok := parseRangeMediaFeature(*children); ok {
202-
return mfRange, true
203+
if term, ok := parseRangeMediaFeature(*children); ok {
204+
if p.options.unsupportedCSSFeatures.Has(compat.MediaRange) {
205+
var terms []css_ast.MediaQuery
206+
if term.BeforeCmp != css_ast.MQCmpNone {
207+
terms = append(terms, lowerMediaRange(term.NameLoc, term.Name, term.BeforeCmp.Reverse(), term.Before))
208+
}
209+
if term.AfterCmp != css_ast.MQCmpNone {
210+
terms = append(terms, lowerMediaRange(term.NameLoc, term.Name, term.AfterCmp, term.After))
211+
}
212+
if len(terms) == 1 {
213+
return terms[0], true
214+
} else {
215+
return css_ast.MediaQuery{Loc: loc, Data: &css_ast.MQBinary{Op: css_ast.MQBinaryOpAnd, Terms: terms}}, true
216+
}
217+
}
218+
return css_ast.MediaQuery{Loc: loc, Data: term}, true
203219
}
204220
}
205221
}
206-
return css_ast.MediaQuery{Loc: tokens[0].Loc, Data: &css_ast.MQGeneralEnclosed{Tokens: tokens}}, true
222+
return css_ast.MediaQuery{Loc: loc, Data: &css_ast.MQGeneralEnclosed{Tokens: tokens}}, true
223+
}
224+
225+
func lowerMediaRange(loc logger.Loc, name string, cmp css_ast.MQCmp, value []css_ast.Token) css_ast.MediaQuery {
226+
switch cmp {
227+
case css_ast.MQCmpLe:
228+
// "foo <= 123" => "max-foo: 123"
229+
return css_ast.MediaQuery{Loc: loc, Data: &css_ast.MQPlainOrBoolean{Name: "max-" + name, ValueOrNil: value}}
230+
231+
case css_ast.MQCmpGe:
232+
// "foo >= 123" => "min-foo: 123"
233+
return css_ast.MediaQuery{Loc: loc, Data: &css_ast.MQPlainOrBoolean{Name: "min-" + name, ValueOrNil: value}}
234+
235+
case css_ast.MQCmpLt:
236+
// "foo < 123" => "not (min-foo: 123)"
237+
return css_ast.MediaQuery{Loc: loc, Data: &css_ast.MQNot{
238+
Inner: css_ast.MediaQuery{Loc: loc, Data: &css_ast.MQPlainOrBoolean{Name: "min-" + name, ValueOrNil: value}},
239+
}}
240+
241+
case css_ast.MQCmpGt:
242+
// "foo > 123" => "not (max-foo: 123)"
243+
return css_ast.MediaQuery{Loc: loc, Data: &css_ast.MQNot{
244+
Inner: css_ast.MediaQuery{Loc: loc, Data: &css_ast.MQPlainOrBoolean{Name: "max-" + name, ValueOrNil: value}},
245+
}}
246+
247+
default:
248+
// "foo = 123" => "foo: 123"
249+
return css_ast.MediaQuery{Loc: loc, Data: &css_ast.MQPlainOrBoolean{Name: name, ValueOrNil: value}}
250+
}
207251
}
208252

209-
func parsePlainOrBooleanMediaFeature(tokens []css_ast.Token) (css_ast.MediaQuery, bool) {
253+
func parsePlainOrBooleanMediaFeature(tokens []css_ast.Token) (*css_ast.MQPlainOrBoolean, bool) {
210254
if len(tokens) == 1 && tokens[0].Kind == css_lexer.TIdent {
211-
return css_ast.MediaQuery{Loc: tokens[0].Loc, Data: &css_ast.MQPlainOrBoolean{Name: tokens[0].Text}}, true
255+
return &css_ast.MQPlainOrBoolean{Name: tokens[0].Text}, true
212256
}
213257
if len(tokens) >= 3 && tokens[0].Kind == css_lexer.TIdent && tokens[1].Kind == css_lexer.TColon {
214258
if value, rest := scanMediaValue(tokens[2:]); len(rest) == 0 {
215-
return css_ast.MediaQuery{Loc: tokens[0].Loc, Data: &css_ast.MQPlainOrBoolean{Name: tokens[0].Text, ValueOrNil: value}}, true
259+
return &css_ast.MQPlainOrBoolean{Name: tokens[0].Text, ValueOrNil: value}, true
216260
}
217261
}
218-
return css_ast.MediaQuery{}, false
262+
return nil, false
219263
}
220264

221-
func parseRangeMediaFeature(tokens []css_ast.Token) (css_ast.MediaQuery, bool) {
265+
func parseRangeMediaFeature(tokens []css_ast.Token) (*css_ast.MQRange, bool) {
222266
if first, tokens := scanMediaValue(tokens); len(first) > 0 {
223267
if firstCmp, tokens := scanMediaComparison(tokens); firstCmp != css_ast.MQCmpNone {
224268
if second, tokens := scanMediaValue(tokens); len(second) > 0 {
225269
if len(tokens) == 0 {
226270
if name, nameLoc, ok := isSingleIdent(first); ok {
227-
return css_ast.MediaQuery{Loc: first[0].Loc, Data: &css_ast.MQRange{
271+
return &css_ast.MQRange{
228272
Name: name,
229273
NameLoc: nameLoc,
230274
AfterCmp: firstCmp,
231275
After: second,
232-
}}, true
276+
}, true
233277
} else if name, nameLoc, ok := isSingleIdent(second); ok {
234-
return css_ast.MediaQuery{Loc: first[0].Loc, Data: &css_ast.MQRange{
278+
return &css_ast.MQRange{
235279
Before: first,
236280
BeforeCmp: firstCmp,
237281
Name: name,
238282
NameLoc: nameLoc,
239-
}}, true
283+
}, true
240284
}
241285
} else if name, nameLoc, ok := isSingleIdent(second); ok {
242286
if secondCmp, tokens := scanMediaComparison(tokens); secondCmp != css_ast.MQCmpNone {
243287
if f, s := firstCmp.Dir(), secondCmp.Dir(); (f < 0 && s < 0) || (f > 0 && s > 0) {
244288
if third, tokens := scanMediaValue(tokens); len(third) > 0 && len(tokens) == 0 {
245-
return css_ast.MediaQuery{Loc: first[0].Loc, Data: &css_ast.MQRange{
289+
return &css_ast.MQRange{
246290
Before: first,
247291
BeforeCmp: firstCmp,
248292
Name: name,
249293
NameLoc: nameLoc,
250294
AfterCmp: secondCmp,
251295
After: third,
252-
}}, true
296+
}, true
253297
}
254298
}
255299
}
256300
}
257301
}
258302
}
259303
}
260-
return css_ast.MediaQuery{}, false
304+
return nil, false
261305
}
262306

263307
func (p *parser) maybeSimplifyMediaNot(loc logger.Loc, inner css_ast.MediaQuery) css_ast.MediaQuery {

internal/css_parser/css_parser_test.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2625,6 +2625,40 @@ func TestMangleAtMedia(t *testing.T) {
26252625
expectPrintedMangle(t, "@media not (1px >= width) { a { color: red } }", "@media (1px < width) {\n a {\n color: red;\n }\n}\n", "")
26262626
}
26272627

2628+
func TestLowerAtMediaRange(t *testing.T) {
2629+
expectPrintedLower(t, "@media (width = 1px) { a { color: red } }", "@media (width: 1px) {\n a {\n color: red;\n }\n}\n", "")
2630+
2631+
expectPrintedLower(t, "@media (width < 1px) { a { color: red } }", "@media not (min-width: 1px) {\n a {\n color: red;\n }\n}\n", "")
2632+
expectPrintedLower(t, "@media (width <= 1px) { a { color: red } }", "@media (max-width: 1px) {\n a {\n color: red;\n }\n}\n", "")
2633+
expectPrintedLower(t, "@media (width > 1px) { a { color: red } }", "@media not (max-width: 1px) {\n a {\n color: red;\n }\n}\n", "")
2634+
expectPrintedLower(t, "@media (width >= 1px) { a { color: red } }", "@media (min-width: 1px) {\n a {\n color: red;\n }\n}\n", "")
2635+
2636+
expectPrintedLower(t, "@media (1px > width) { a { color: red } }", "@media not (min-width: 1px) {\n a {\n color: red;\n }\n}\n", "")
2637+
expectPrintedLower(t, "@media (1px >= width) { a { color: red } }", "@media (max-width: 1px) {\n a {\n color: red;\n }\n}\n", "")
2638+
expectPrintedLower(t, "@media (1px < width) { a { color: red } }", "@media not (max-width: 1px) {\n a {\n color: red;\n }\n}\n", "")
2639+
expectPrintedLower(t, "@media (1px <= width) { a { color: red } }", "@media (min-width: 1px) {\n a {\n color: red;\n }\n}\n", "")
2640+
2641+
expectPrintedLower(t, "@media (1px < width < 2px) { a { color: red } }", "@media (not (max-width: 1px)) and (not (min-width: 2px)) {\n a {\n color: red;\n }\n}\n", "")
2642+
expectPrintedLower(t, "@media (2px > width > 1px) { a { color: red } }", "@media (not (min-width: 2px)) and (not (max-width: 1px)) {\n a {\n color: red;\n }\n}\n", "")
2643+
expectPrintedLower(t, "@media (1px <= width <= 2px) { a { color: red } }", "@media (min-width: 1px) and (max-width: 2px) {\n a {\n color: red;\n }\n}\n", "")
2644+
expectPrintedLower(t, "@media (2px >= width >= 1px) { a { color: red } }", "@media (max-width: 2px) and (min-width: 1px) {\n a {\n color: red;\n }\n}\n", "")
2645+
2646+
expectPrintedLower(t, "@media (1px < width <= 2px) { a { color: red } }", "@media (not (max-width: 1px)) and (max-width: 2px) {\n a {\n color: red;\n }\n}\n", "")
2647+
expectPrintedLower(t, "@media (2px > width >= 1px) { a { color: red } }", "@media (not (min-width: 2px)) and (min-width: 1px) {\n a {\n color: red;\n }\n}\n", "")
2648+
expectPrintedLower(t, "@media (1px <= width < 2px) { a { color: red } }", "@media (min-width: 1px) and (not (min-width: 2px)) {\n a {\n color: red;\n }\n}\n", "")
2649+
expectPrintedLower(t, "@media (2px >= width > 1px) { a { color: red } }", "@media (max-width: 2px) and (not (max-width: 1px)) {\n a {\n color: red;\n }\n}\n", "")
2650+
2651+
expectPrintedLower(t, "@media not (1px < width < 2px) { a { color: red } }", "@media not ((not (max-width: 1px)) and (not (min-width: 2px))) {\n a {\n color: red;\n }\n}\n", "")
2652+
expectPrintedLower(t, "@media not (2px > width > 1px) { a { color: red } }", "@media not ((not (min-width: 2px)) and (not (max-width: 1px))) {\n a {\n color: red;\n }\n}\n", "")
2653+
expectPrintedLower(t, "@media not (1px <= width <= 2px) { a { color: red } }", "@media not ((min-width: 1px) and (max-width: 2px)) {\n a {\n color: red;\n }\n}\n", "")
2654+
expectPrintedLower(t, "@media not (2px >= width >= 1px) { a { color: red } }", "@media not ((max-width: 2px) and (min-width: 1px)) {\n a {\n color: red;\n }\n}\n", "")
2655+
2656+
expectPrintedLowerMangle(t, "@media not (1px < width < 2px) { a { color: red } }", "@media (max-width: 1px) or (min-width: 2px) {\n a {\n color: red;\n }\n}\n", "")
2657+
expectPrintedLowerMangle(t, "@media not (2px > width > 1px) { a { color: red } }", "@media (min-width: 2px) or (max-width: 1px) {\n a {\n color: red;\n }\n}\n", "")
2658+
expectPrintedLowerMangle(t, "@media not (1px <= width <= 2px) { a { color: red } }", "@media not ((min-width: 1px) and (max-width: 2px)) {\n a {\n color: red;\n }\n}\n", "")
2659+
expectPrintedLowerMangle(t, "@media not (2px >= width >= 1px) { a { color: red } }", "@media not ((max-width: 2px) and (min-width: 1px)) {\n a {\n color: red;\n }\n}\n", "")
2660+
}
2661+
26282662
func TestFontWeight(t *testing.T) {
26292663
expectPrintedMangle(t, "a { font-weight: normal }", "a {\n font-weight: 400;\n}\n", "")
26302664
expectPrintedMangle(t, "a { font-weight: bold }", "a {\n font-weight: 700;\n}\n", "")

0 commit comments

Comments
 (0)