mirror of
https://github.com/hanzoai/esbuild.git
synced 2026-09-24 16:58:50 +00:00
413 lines
12 KiB
Go
413 lines
12 KiB
Go
package css_parser
|
|
|
|
import (
|
|
"strings"
|
|
|
|
"github.com/evanw/esbuild/internal/compat"
|
|
"github.com/evanw/esbuild/internal/css_ast"
|
|
"github.com/evanw/esbuild/internal/css_lexer"
|
|
"github.com/evanw/esbuild/internal/logger"
|
|
)
|
|
|
|
// Reference: https://drafts.csswg.org/mediaqueries-4/
|
|
func (p *parser) parseMediaQueryListUntil(stop func(css_lexer.T) bool) []css_ast.MediaQuery {
|
|
var queries []css_ast.MediaQuery
|
|
p.eat(css_lexer.TWhitespace)
|
|
for !p.peek(css_lexer.TEndOfFile) && !stop(p.current().Kind) {
|
|
start := p.index
|
|
query, ok := p.parseMediaQuery()
|
|
if !ok {
|
|
// If parsing failed, parse an arbitrary sequence of tokens instead
|
|
p.index = start
|
|
loc := p.current().Range.Loc
|
|
for !p.peek(css_lexer.TEndOfFile) && !stop(p.current().Kind) && !p.peek(css_lexer.TComma) {
|
|
p.parseComponentValue()
|
|
}
|
|
tokens := p.convertTokens(p.tokens[start:p.index])
|
|
query = css_ast.MediaQuery{Loc: loc, Data: &css_ast.MQArbitraryTokens{Tokens: tokens}}
|
|
}
|
|
queries = append(queries, query)
|
|
p.eat(css_lexer.TWhitespace)
|
|
if !p.eat(css_lexer.TComma) {
|
|
break
|
|
}
|
|
p.eat(css_lexer.TWhitespace)
|
|
}
|
|
return queries
|
|
}
|
|
|
|
func (p *parser) parseMediaQuery() (css_ast.MediaQuery, bool) {
|
|
loc := p.current().Range.Loc
|
|
|
|
// Check for a media condition first
|
|
if p.looksLikeMediaCondition() {
|
|
return p.parseMediaCondition(mediaWithOr)
|
|
}
|
|
|
|
// Parse the media type and potentially the leading "not" or "only" keyword
|
|
mediaType := p.decoded()
|
|
if !p.peek(css_lexer.TIdent) {
|
|
p.expect(css_lexer.TIdent)
|
|
return css_ast.MediaQuery{}, false
|
|
}
|
|
op := css_ast.MQTypeOpNone
|
|
if strings.EqualFold(mediaType, "not") {
|
|
op = css_ast.MQTypeOpNot
|
|
} else if strings.EqualFold(mediaType, "only") {
|
|
op = css_ast.MQTypeOpOnly
|
|
}
|
|
if op != css_ast.MQTypeOpNone {
|
|
p.advance()
|
|
p.eat(css_lexer.TWhitespace)
|
|
mediaType = p.decoded()
|
|
if !p.peek(css_lexer.TIdent) {
|
|
p.expect(css_lexer.TIdent)
|
|
return css_ast.MediaQuery{}, false
|
|
}
|
|
}
|
|
|
|
// The <media-type> production does not include the keywords "only", "not", "and", "or", and "layer".
|
|
if strings.EqualFold(mediaType, "only") ||
|
|
strings.EqualFold(mediaType, "not") ||
|
|
strings.EqualFold(mediaType, "and") ||
|
|
strings.EqualFold(mediaType, "or") ||
|
|
strings.EqualFold(mediaType, "layer") {
|
|
p.unexpected()
|
|
return css_ast.MediaQuery{}, false
|
|
}
|
|
p.advance()
|
|
p.eat(css_lexer.TWhitespace)
|
|
|
|
// Potentially parse a chain of "and" operators
|
|
var andOrNull css_ast.MediaQuery
|
|
if p.peek(css_lexer.TIdent) && strings.EqualFold(p.decoded(), "and") {
|
|
p.advance()
|
|
p.eat(css_lexer.TWhitespace)
|
|
var ok bool
|
|
andOrNull, ok = p.parseMediaCondition(mediaWithoutOr)
|
|
if !ok {
|
|
return css_ast.MediaQuery{}, false
|
|
}
|
|
}
|
|
|
|
return css_ast.MediaQuery{Loc: loc, Data: &css_ast.MQType{Op: op, Type: mediaType, AndOrNull: andOrNull}}, true
|
|
}
|
|
|
|
func (p *parser) looksLikeMediaCondition() bool {
|
|
kind := p.current().Kind
|
|
return kind == css_lexer.TOpenParen || kind == css_lexer.TFunction ||
|
|
(kind == css_lexer.TIdent && strings.EqualFold(p.decoded(), "not") &&
|
|
p.next().Kind == css_lexer.TWhitespace &&
|
|
p.at(p.index+2).Kind == css_lexer.TOpenParen)
|
|
}
|
|
|
|
type mediaOr uint8
|
|
|
|
const (
|
|
mediaWithOr mediaOr = iota
|
|
mediaWithoutOr
|
|
)
|
|
|
|
func (p *parser) parseMediaCondition(or mediaOr) (css_ast.MediaQuery, bool) {
|
|
loc := p.current().Range.Loc
|
|
|
|
// Handle a leading "not"
|
|
if p.peek(css_lexer.TIdent) && strings.EqualFold(p.decoded(), "not") {
|
|
p.advance()
|
|
p.eat(css_lexer.TWhitespace)
|
|
if inner, ok := p.parseMediaInParens(); !ok {
|
|
return css_ast.MediaQuery{}, false
|
|
} else {
|
|
return p.maybeSimplifyMediaNot(loc, inner), true
|
|
}
|
|
}
|
|
|
|
// Parse the first term
|
|
first, ok := p.parseMediaInParens()
|
|
if !ok {
|
|
return css_ast.MediaQuery{}, false
|
|
}
|
|
p.eat(css_lexer.TWhitespace)
|
|
|
|
// Potentially parse a chain of "and" or "or" operators
|
|
if p.peek(css_lexer.TIdent) {
|
|
if keyword := p.decoded(); strings.EqualFold(keyword, "and") || (or == mediaWithOr && strings.EqualFold(keyword, "or")) {
|
|
op := css_ast.MQBinaryOpAnd
|
|
if len(keyword) == 2 {
|
|
op = css_ast.MQBinaryOpOr
|
|
}
|
|
inner := p.appendMediaTerm([]css_ast.MediaQuery{}, first, op)
|
|
for {
|
|
p.advance()
|
|
p.eat(css_lexer.TWhitespace)
|
|
next, ok := p.parseMediaInParens()
|
|
if !ok {
|
|
return css_ast.MediaQuery{}, false
|
|
}
|
|
inner = p.appendMediaTerm(inner, next, op)
|
|
p.eat(css_lexer.TWhitespace)
|
|
if !p.peek(css_lexer.TIdent) || !strings.EqualFold(p.decoded(), keyword) {
|
|
break
|
|
}
|
|
}
|
|
return css_ast.MediaQuery{Loc: loc, Data: &css_ast.MQBinary{Op: op, Terms: inner}}, true
|
|
}
|
|
}
|
|
|
|
return first, true
|
|
}
|
|
|
|
func (p *parser) appendMediaTerm(inner []css_ast.MediaQuery, term css_ast.MediaQuery, op css_ast.MQBinaryOp) []css_ast.MediaQuery {
|
|
// "(a and b) and c" => "a and b and c"
|
|
// "(a or b) or c" => "a or b or c"
|
|
if binary, ok := term.Data.(*css_ast.MQBinary); ok && binary.Op == op && p.options.minifySyntax {
|
|
return append(inner, binary.Terms...)
|
|
} else {
|
|
return append(inner, term)
|
|
}
|
|
}
|
|
|
|
func (p *parser) parseMediaInParens() (css_ast.MediaQuery, bool) {
|
|
p.eat(css_lexer.TWhitespace)
|
|
start := p.index
|
|
|
|
// Consume the opening token
|
|
isFunction := p.eat(css_lexer.TFunction)
|
|
if !isFunction && !p.expect(css_lexer.TOpenParen) {
|
|
return css_ast.MediaQuery{}, false
|
|
}
|
|
p.eat(css_lexer.TWhitespace)
|
|
|
|
// Handle a media condition
|
|
if !isFunction && p.looksLikeMediaCondition() {
|
|
if inner, ok := p.parseMediaCondition(mediaWithOr); !ok {
|
|
return css_ast.MediaQuery{}, false
|
|
} else {
|
|
p.eat(css_lexer.TWhitespace)
|
|
if !p.expect(css_lexer.TCloseParen) {
|
|
return css_ast.MediaQuery{}, false
|
|
}
|
|
return inner, ok
|
|
}
|
|
}
|
|
|
|
// Scan over the remaining tokens
|
|
for !p.peek(css_lexer.TCloseParen) && !p.peek(css_lexer.TEndOfFile) {
|
|
p.parseComponentValue()
|
|
}
|
|
end := p.index
|
|
if !p.expect(css_lexer.TCloseParen) {
|
|
return css_ast.MediaQuery{}, false
|
|
}
|
|
tokens := p.convertTokens(p.tokens[start:end])
|
|
loc := tokens[0].Loc
|
|
|
|
// Potentially pattern-match the tokens inside the parentheses
|
|
if !isFunction && len(tokens) == 1 {
|
|
if children := tokens[0].Children; children != nil {
|
|
if term, ok := parsePlainOrBooleanMediaFeature(*children); ok {
|
|
return css_ast.MediaQuery{Loc: loc, Data: term}, true
|
|
}
|
|
if term, ok := parseRangeMediaFeature(*children); ok {
|
|
if p.options.unsupportedCSSFeatures.Has(compat.MediaRange) {
|
|
var terms []css_ast.MediaQuery
|
|
if term.BeforeCmp != css_ast.MQCmpNone {
|
|
terms = append(terms, lowerMediaRange(term.NameLoc, term.Name, term.BeforeCmp.Reverse(), term.Before))
|
|
}
|
|
if term.AfterCmp != css_ast.MQCmpNone {
|
|
terms = append(terms, lowerMediaRange(term.NameLoc, term.Name, term.AfterCmp, term.After))
|
|
}
|
|
if len(terms) == 1 {
|
|
return terms[0], true
|
|
} else {
|
|
return css_ast.MediaQuery{Loc: loc, Data: &css_ast.MQBinary{Op: css_ast.MQBinaryOpAnd, Terms: terms}}, true
|
|
}
|
|
}
|
|
return css_ast.MediaQuery{Loc: loc, Data: term}, true
|
|
}
|
|
}
|
|
}
|
|
return css_ast.MediaQuery{Loc: loc, Data: &css_ast.MQArbitraryTokens{Tokens: tokens}}, true
|
|
}
|
|
|
|
func lowerMediaRange(loc logger.Loc, name string, cmp css_ast.MQCmp, value []css_ast.Token) css_ast.MediaQuery {
|
|
switch cmp {
|
|
case css_ast.MQCmpLe:
|
|
// "foo <= 123" => "max-foo: 123"
|
|
return css_ast.MediaQuery{Loc: loc, Data: &css_ast.MQPlainOrBoolean{Name: "max-" + name, ValueOrNil: value}}
|
|
|
|
case css_ast.MQCmpGe:
|
|
// "foo >= 123" => "min-foo: 123"
|
|
return css_ast.MediaQuery{Loc: loc, Data: &css_ast.MQPlainOrBoolean{Name: "min-" + name, ValueOrNil: value}}
|
|
|
|
case css_ast.MQCmpLt:
|
|
// "foo < 123" => "not (min-foo: 123)"
|
|
return css_ast.MediaQuery{Loc: loc, Data: &css_ast.MQNot{
|
|
Inner: css_ast.MediaQuery{Loc: loc, Data: &css_ast.MQPlainOrBoolean{Name: "min-" + name, ValueOrNil: value}},
|
|
}}
|
|
|
|
case css_ast.MQCmpGt:
|
|
// "foo > 123" => "not (max-foo: 123)"
|
|
return css_ast.MediaQuery{Loc: loc, Data: &css_ast.MQNot{
|
|
Inner: css_ast.MediaQuery{Loc: loc, Data: &css_ast.MQPlainOrBoolean{Name: "max-" + name, ValueOrNil: value}},
|
|
}}
|
|
|
|
default:
|
|
// "foo = 123" => "foo: 123"
|
|
return css_ast.MediaQuery{Loc: loc, Data: &css_ast.MQPlainOrBoolean{Name: name, ValueOrNil: value}}
|
|
}
|
|
}
|
|
|
|
func parsePlainOrBooleanMediaFeature(tokens []css_ast.Token) (*css_ast.MQPlainOrBoolean, bool) {
|
|
if len(tokens) == 1 && tokens[0].Kind == css_lexer.TIdent {
|
|
return &css_ast.MQPlainOrBoolean{Name: tokens[0].Text}, true
|
|
}
|
|
if len(tokens) >= 3 && tokens[0].Kind == css_lexer.TIdent && tokens[1].Kind == css_lexer.TColon {
|
|
if value, rest := scanMediaValue(tokens[2:]); len(rest) == 0 {
|
|
return &css_ast.MQPlainOrBoolean{Name: tokens[0].Text, ValueOrNil: value}, true
|
|
}
|
|
}
|
|
return nil, false
|
|
}
|
|
|
|
func parseRangeMediaFeature(tokens []css_ast.Token) (*css_ast.MQRange, bool) {
|
|
if first, tokens := scanMediaValue(tokens); len(first) > 0 {
|
|
if firstCmp, tokens := scanMediaComparison(tokens); firstCmp != css_ast.MQCmpNone {
|
|
if second, tokens := scanMediaValue(tokens); len(second) > 0 {
|
|
if len(tokens) == 0 {
|
|
if name, nameLoc, ok := isSingleIdent(first); ok {
|
|
return &css_ast.MQRange{
|
|
Name: name,
|
|
NameLoc: nameLoc,
|
|
AfterCmp: firstCmp,
|
|
After: second,
|
|
}, true
|
|
} else if name, nameLoc, ok := isSingleIdent(second); ok {
|
|
return &css_ast.MQRange{
|
|
Before: first,
|
|
BeforeCmp: firstCmp,
|
|
Name: name,
|
|
NameLoc: nameLoc,
|
|
}, true
|
|
}
|
|
} else if name, nameLoc, ok := isSingleIdent(second); ok {
|
|
if secondCmp, tokens := scanMediaComparison(tokens); secondCmp != css_ast.MQCmpNone {
|
|
if f, s := firstCmp.Dir(), secondCmp.Dir(); (f < 0 && s < 0) || (f > 0 && s > 0) {
|
|
if third, tokens := scanMediaValue(tokens); len(third) > 0 && len(tokens) == 0 {
|
|
return &css_ast.MQRange{
|
|
Before: first,
|
|
BeforeCmp: firstCmp,
|
|
Name: name,
|
|
NameLoc: nameLoc,
|
|
AfterCmp: secondCmp,
|
|
After: third,
|
|
}, true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return nil, false
|
|
}
|
|
|
|
func (p *parser) maybeSimplifyMediaNot(loc logger.Loc, inner css_ast.MediaQuery) css_ast.MediaQuery {
|
|
if p.options.minifySyntax {
|
|
switch data := inner.Data.(type) {
|
|
case *css_ast.MQNot:
|
|
// "not (not a)" => "a"
|
|
// "not (not (not a))" => "not a"
|
|
return data.Inner
|
|
|
|
case *css_ast.MQBinary:
|
|
// "not ((not a) and (not b))" => "a or b"
|
|
// "not ((not a) or (not b))" => "a and b"
|
|
terms := make([]css_ast.MediaQuery, 0, len(data.Terms))
|
|
for _, term := range data.Terms {
|
|
if not, ok := term.Data.(*css_ast.MQNot); ok {
|
|
terms = append(terms, not.Inner)
|
|
} else {
|
|
break
|
|
}
|
|
}
|
|
if len(terms) == len(data.Terms) {
|
|
data.Op ^= 1
|
|
data.Terms = terms
|
|
return inner
|
|
}
|
|
|
|
case *css_ast.MQRange:
|
|
if (data.BeforeCmp == css_ast.MQCmpNone && data.AfterCmp != css_ast.MQCmpEq) ||
|
|
(data.AfterCmp == css_ast.MQCmpNone && data.BeforeCmp != css_ast.MQCmpEq) {
|
|
data.BeforeCmp = data.BeforeCmp.Flip()
|
|
data.AfterCmp = data.AfterCmp.Flip()
|
|
return inner
|
|
}
|
|
}
|
|
}
|
|
return css_ast.MediaQuery{Loc: loc, Data: &css_ast.MQNot{Inner: inner}}
|
|
}
|
|
|
|
func isSingleIdent(tokens []css_ast.Token) (string, logger.Loc, bool) {
|
|
if len(tokens) == 1 && tokens[0].Kind == css_lexer.TIdent {
|
|
return tokens[0].Text, tokens[0].Loc, true
|
|
} else {
|
|
return "", logger.Loc{}, false
|
|
}
|
|
}
|
|
|
|
func scanMediaComparison(tokens []css_ast.Token) (css_ast.MQCmp, []css_ast.Token) {
|
|
if len(tokens) >= 1 {
|
|
switch tokens[0].Kind {
|
|
case css_lexer.TDelimEquals:
|
|
return css_ast.MQCmpEq, tokens[1:]
|
|
|
|
case css_lexer.TDelimLessThan:
|
|
// Handle "<=" or "<"
|
|
if len(tokens) >= 2 && tokens[1].Kind == css_lexer.TDelimEquals &&
|
|
((tokens[0].Whitespace&css_ast.WhitespaceAfter)|(tokens[1].Whitespace&css_ast.WhitespaceBefore)) == 0 {
|
|
return css_ast.MQCmpLe, tokens[2:]
|
|
}
|
|
return css_ast.MQCmpLt, tokens[1:]
|
|
|
|
case css_lexer.TDelimGreaterThan:
|
|
// Handle ">=" or ">"
|
|
if len(tokens) >= 2 && tokens[1].Kind == css_lexer.TDelimEquals &&
|
|
((tokens[0].Whitespace&css_ast.WhitespaceAfter)|(tokens[1].Whitespace&css_ast.WhitespaceBefore)) == 0 {
|
|
return css_ast.MQCmpGe, tokens[2:]
|
|
}
|
|
return css_ast.MQCmpGt, tokens[1:]
|
|
}
|
|
}
|
|
|
|
return css_ast.MQCmpNone, tokens
|
|
}
|
|
|
|
func scanMediaValue(tokens []css_ast.Token) ([]css_ast.Token, []css_ast.Token) {
|
|
n := 0
|
|
|
|
if len(tokens) >= 1 {
|
|
switch tokens[0].Kind {
|
|
case css_lexer.TDimension, css_lexer.TIdent:
|
|
n = 1
|
|
|
|
case css_lexer.TNumber:
|
|
// Potentially recognize a ratio which is "<number> / <number>"
|
|
if len(tokens) >= 3 && tokens[1].Kind == css_lexer.TDelimSlash && tokens[2].Kind == css_lexer.TNumber {
|
|
n = 3
|
|
} else {
|
|
n = 1
|
|
}
|
|
}
|
|
}
|
|
|
|
// Trim whitespace at the endpoints
|
|
if n > 0 {
|
|
tokens[0].Whitespace &= ^css_ast.WhitespaceBefore
|
|
tokens[n-1].Whitespace &= ^css_ast.WhitespaceAfter
|
|
}
|
|
|
|
return tokens[:n], tokens[n:]
|
|
}
|