mirror of
https://github.com/hanzoai/esbuild.git
synced 2026-09-24 16:58:50 +00:00
2530 lines
76 KiB
Go
2530 lines
76 KiB
Go
package css_parser
|
||
|
||
import (
|
||
"fmt"
|
||
"strings"
|
||
|
||
"github.com/evanw/esbuild/internal/ast"
|
||
"github.com/evanw/esbuild/internal/compat"
|
||
"github.com/evanw/esbuild/internal/config"
|
||
"github.com/evanw/esbuild/internal/css_ast"
|
||
"github.com/evanw/esbuild/internal/css_lexer"
|
||
"github.com/evanw/esbuild/internal/logger"
|
||
)
|
||
|
||
// This is mostly a normal CSS parser with one exception: the addition of
|
||
// support for parsing https://drafts.csswg.org/css-nesting-1/.
|
||
|
||
type parser struct {
|
||
log logger.Log
|
||
source logger.Source
|
||
tokens []css_lexer.Token
|
||
allComments []logger.Range
|
||
legalComments []css_lexer.Comment
|
||
stack []css_lexer.T
|
||
importRecords []ast.ImportRecord
|
||
symbols []ast.Symbol
|
||
composes map[ast.Ref]*css_ast.Composes
|
||
localSymbols []ast.LocRef
|
||
localScope map[string]ast.LocRef
|
||
globalScope map[string]ast.LocRef
|
||
nestingWarnings map[logger.Loc]struct{}
|
||
tracker logger.LineColumnTracker
|
||
enclosingAtMedia [][]css_ast.MediaQuery
|
||
layersPreImport [][]string
|
||
layersPostImport [][]string
|
||
enclosingLayer []string
|
||
anonLayerCount int
|
||
index int
|
||
legalCommentIndex int
|
||
inSelectorSubtree int
|
||
prevError logger.Loc
|
||
options Options
|
||
nestingIsPresent bool
|
||
makeLocalSymbols bool
|
||
hasSeenAtImport bool
|
||
}
|
||
|
||
type Options struct {
|
||
cssPrefixData map[css_ast.D]compat.CSSPrefix
|
||
|
||
// This is an embedded struct. Always access these directly instead of off
|
||
// the name "optionsThatSupportStructuralEquality". This is only grouped like
|
||
// this to make the equality comparison easier and safer (and hopefully faster).
|
||
optionsThatSupportStructuralEquality
|
||
}
|
||
|
||
type symbolMode uint8
|
||
|
||
const (
|
||
symbolModeDisabled symbolMode = iota
|
||
symbolModeGlobal
|
||
symbolModeLocal
|
||
)
|
||
|
||
type optionsThatSupportStructuralEquality struct {
|
||
originalTargetEnv string
|
||
unsupportedCSSFeatures compat.CSSFeature
|
||
minifySyntax bool
|
||
minifyWhitespace bool
|
||
minifyIdentifiers bool
|
||
symbolMode symbolMode
|
||
}
|
||
|
||
func OptionsFromConfig(loader config.Loader, options *config.Options) Options {
|
||
var symbolMode symbolMode
|
||
switch loader {
|
||
case config.LoaderGlobalCSS:
|
||
symbolMode = symbolModeGlobal
|
||
case config.LoaderLocalCSS:
|
||
symbolMode = symbolModeLocal
|
||
}
|
||
|
||
return Options{
|
||
cssPrefixData: options.CSSPrefixData,
|
||
|
||
optionsThatSupportStructuralEquality: optionsThatSupportStructuralEquality{
|
||
minifySyntax: options.MinifySyntax,
|
||
minifyWhitespace: options.MinifyWhitespace,
|
||
minifyIdentifiers: options.MinifyIdentifiers,
|
||
unsupportedCSSFeatures: options.UnsupportedCSSFeatures,
|
||
originalTargetEnv: options.OriginalTargetEnv,
|
||
symbolMode: symbolMode,
|
||
},
|
||
}
|
||
}
|
||
|
||
func (a *Options) Equal(b *Options) bool {
|
||
// Compare "optionsThatSupportStructuralEquality"
|
||
if a.optionsThatSupportStructuralEquality != b.optionsThatSupportStructuralEquality {
|
||
return false
|
||
}
|
||
|
||
// Compare "cssPrefixData"
|
||
if len(a.cssPrefixData) != len(b.cssPrefixData) {
|
||
return false
|
||
}
|
||
for k, va := range a.cssPrefixData {
|
||
vb, ok := b.cssPrefixData[k]
|
||
if !ok || va != vb {
|
||
return false
|
||
}
|
||
}
|
||
for k := range b.cssPrefixData {
|
||
if _, ok := b.cssPrefixData[k]; !ok {
|
||
return false
|
||
}
|
||
}
|
||
|
||
return true
|
||
}
|
||
|
||
func Parse(log logger.Log, source logger.Source, options Options) css_ast.AST {
|
||
result := css_lexer.Tokenize(log, source, css_lexer.Options{
|
||
RecordAllComments: options.minifyIdentifiers,
|
||
})
|
||
p := parser{
|
||
log: log,
|
||
source: source,
|
||
tracker: logger.MakeLineColumnTracker(&source),
|
||
options: options,
|
||
tokens: result.Tokens,
|
||
allComments: result.AllComments,
|
||
legalComments: result.LegalComments,
|
||
prevError: logger.Loc{Start: -1},
|
||
composes: make(map[ast.Ref]*css_ast.Composes),
|
||
localScope: make(map[string]ast.LocRef),
|
||
globalScope: make(map[string]ast.LocRef),
|
||
makeLocalSymbols: options.symbolMode == symbolModeLocal,
|
||
}
|
||
rules := p.parseListOfRules(ruleContext{
|
||
isTopLevel: true,
|
||
parseSelectors: true,
|
||
})
|
||
p.expect(css_lexer.TEndOfFile)
|
||
return css_ast.AST{
|
||
Rules: rules,
|
||
CharFreq: p.computeCharacterFrequency(),
|
||
Symbols: p.symbols,
|
||
ImportRecords: p.importRecords,
|
||
ApproximateLineCount: result.ApproximateLineCount,
|
||
SourceMapComment: result.SourceMapComment,
|
||
LocalSymbols: p.localSymbols,
|
||
LocalScope: p.localScope,
|
||
GlobalScope: p.globalScope,
|
||
Composes: p.composes,
|
||
LayersPreImport: p.layersPreImport,
|
||
LayersPostImport: p.layersPostImport,
|
||
}
|
||
}
|
||
|
||
// Compute a character frequency histogram for everything that's not a bound
|
||
// symbol. This is used to modify how minified names are generated for slightly
|
||
// better gzip compression. Even though it's a very small win, we still do it
|
||
// because it's simple to do and very cheap to compute.
|
||
func (p *parser) computeCharacterFrequency() *ast.CharFreq {
|
||
if !p.options.minifyIdentifiers {
|
||
return nil
|
||
}
|
||
|
||
// Add everything in the file to the histogram
|
||
charFreq := &ast.CharFreq{}
|
||
charFreq.Scan(p.source.Contents, 1)
|
||
|
||
// Subtract out all comments
|
||
for _, commentRange := range p.allComments {
|
||
charFreq.Scan(p.source.TextForRange(commentRange), -1)
|
||
}
|
||
|
||
// Subtract out all import paths
|
||
for _, record := range p.importRecords {
|
||
if !record.SourceIndex.IsValid() {
|
||
charFreq.Scan(record.Path.Text, -1)
|
||
}
|
||
}
|
||
|
||
// Subtract out all symbols that will be minified
|
||
for _, symbol := range p.symbols {
|
||
if symbol.Kind == ast.SymbolLocalCSS {
|
||
charFreq.Scan(symbol.OriginalName, -int32(symbol.UseCountEstimate))
|
||
}
|
||
}
|
||
|
||
return charFreq
|
||
}
|
||
|
||
func (p *parser) advance() {
|
||
if p.index < len(p.tokens) {
|
||
p.index++
|
||
}
|
||
}
|
||
|
||
func (p *parser) at(index int) css_lexer.Token {
|
||
if index < len(p.tokens) {
|
||
return p.tokens[index]
|
||
}
|
||
return css_lexer.Token{
|
||
Kind: css_lexer.TEndOfFile,
|
||
Range: logger.Range{Loc: logger.Loc{Start: int32(len(p.source.Contents))}},
|
||
}
|
||
}
|
||
|
||
func (p *parser) current() css_lexer.Token {
|
||
return p.at(p.index)
|
||
}
|
||
|
||
func (p *parser) next() css_lexer.Token {
|
||
return p.at(p.index + 1)
|
||
}
|
||
|
||
func (p *parser) raw() string {
|
||
t := p.current()
|
||
return p.source.Contents[t.Range.Loc.Start:t.Range.End()]
|
||
}
|
||
|
||
func (p *parser) decoded() string {
|
||
return p.current().DecodedText(p.source.Contents)
|
||
}
|
||
|
||
func (p *parser) peek(kind css_lexer.T) bool {
|
||
return kind == p.current().Kind
|
||
}
|
||
|
||
func (p *parser) eat(kind css_lexer.T) bool {
|
||
if p.peek(kind) {
|
||
p.advance()
|
||
return true
|
||
}
|
||
return false
|
||
}
|
||
|
||
func (p *parser) expect(kind css_lexer.T) bool {
|
||
return p.expectWithMatchingLoc(kind, logger.Loc{Start: -1})
|
||
}
|
||
|
||
func (p *parser) expectWithMatchingLoc(kind css_lexer.T, matchingLoc logger.Loc) bool {
|
||
if p.eat(kind) {
|
||
return true
|
||
}
|
||
t := p.current()
|
||
if (t.Flags & css_lexer.DidWarnAboutSingleLineComment) != 0 {
|
||
return false
|
||
}
|
||
|
||
var text string
|
||
var suggestion string
|
||
var notes []logger.MsgData
|
||
|
||
expected := kind.String()
|
||
if strings.HasPrefix(expected, "\"") && strings.HasSuffix(expected, "\"") {
|
||
suggestion = expected[1 : len(expected)-1]
|
||
}
|
||
|
||
if (kind == css_lexer.TSemicolon || kind == css_lexer.TColon) && p.index > 0 && p.at(p.index-1).Kind == css_lexer.TWhitespace {
|
||
// Have a nice error message for forgetting a trailing semicolon or colon
|
||
text = fmt.Sprintf("Expected %s", expected)
|
||
t = p.at(p.index - 1)
|
||
} else if (kind == css_lexer.TCloseBrace || kind == css_lexer.TCloseBracket || kind == css_lexer.TCloseParen) &&
|
||
matchingLoc.Start != -1 && int(matchingLoc.Start)+1 <= len(p.source.Contents) {
|
||
// Have a nice error message for forgetting a closing brace/bracket/parenthesis
|
||
c := p.source.Contents[matchingLoc.Start : matchingLoc.Start+1]
|
||
text = fmt.Sprintf("Expected %s to go with %q", expected, c)
|
||
notes = append(notes, p.tracker.MsgData(logger.Range{Loc: matchingLoc, Len: 1}, fmt.Sprintf("The unbalanced %q is here:", c)))
|
||
} else {
|
||
switch t.Kind {
|
||
case css_lexer.TEndOfFile, css_lexer.TWhitespace:
|
||
text = fmt.Sprintf("Expected %s but found %s", expected, t.Kind.String())
|
||
t.Range.Len = 0
|
||
case css_lexer.TBadURL, css_lexer.TUnterminatedString:
|
||
text = fmt.Sprintf("Expected %s but found %s", expected, t.Kind.String())
|
||
default:
|
||
text = fmt.Sprintf("Expected %s but found %q", expected, p.raw())
|
||
}
|
||
}
|
||
|
||
if t.Range.Loc.Start > p.prevError.Start {
|
||
data := p.tracker.MsgData(t.Range, text)
|
||
data.Location.Suggestion = suggestion
|
||
p.log.AddMsgID(logger.MsgID_CSS_CSSSyntaxError, logger.Msg{Kind: logger.Warning, Data: data, Notes: notes})
|
||
p.prevError = t.Range.Loc
|
||
}
|
||
return false
|
||
}
|
||
|
||
func (p *parser) unexpected() {
|
||
if t := p.current(); t.Range.Loc.Start > p.prevError.Start && (t.Flags&css_lexer.DidWarnAboutSingleLineComment) == 0 {
|
||
var text string
|
||
switch t.Kind {
|
||
case css_lexer.TEndOfFile, css_lexer.TWhitespace:
|
||
text = fmt.Sprintf("Unexpected %s", t.Kind.String())
|
||
t.Range.Len = 0
|
||
case css_lexer.TBadURL, css_lexer.TUnterminatedString:
|
||
text = fmt.Sprintf("Unexpected %s", t.Kind.String())
|
||
default:
|
||
text = fmt.Sprintf("Unexpected %q", p.raw())
|
||
}
|
||
p.log.AddID(logger.MsgID_CSS_CSSSyntaxError, logger.Warning, &p.tracker, t.Range, text)
|
||
p.prevError = t.Range.Loc
|
||
}
|
||
}
|
||
|
||
func (p *parser) symbolForName(loc logger.Loc, name string) ast.LocRef {
|
||
var kind ast.SymbolKind
|
||
var scope map[string]ast.LocRef
|
||
|
||
if p.makeLocalSymbols {
|
||
kind = ast.SymbolLocalCSS
|
||
scope = p.localScope
|
||
} else {
|
||
kind = ast.SymbolGlobalCSS
|
||
scope = p.globalScope
|
||
}
|
||
|
||
entry, ok := scope[name]
|
||
if !ok {
|
||
entry = ast.LocRef{
|
||
Loc: loc,
|
||
Ref: ast.Ref{
|
||
SourceIndex: p.source.Index,
|
||
InnerIndex: uint32(len(p.symbols)),
|
||
},
|
||
}
|
||
p.symbols = append(p.symbols, ast.Symbol{
|
||
Kind: kind,
|
||
OriginalName: name,
|
||
Link: ast.InvalidRef,
|
||
})
|
||
scope[name] = entry
|
||
if kind == ast.SymbolLocalCSS {
|
||
p.localSymbols = append(p.localSymbols, entry)
|
||
}
|
||
}
|
||
|
||
p.symbols[entry.Ref.InnerIndex].UseCountEstimate++
|
||
return entry
|
||
}
|
||
|
||
func (p *parser) recordAtLayerRule(layers [][]string) {
|
||
if p.anonLayerCount > 0 {
|
||
return
|
||
}
|
||
|
||
for _, layer := range layers {
|
||
if len(p.enclosingLayer) > 0 {
|
||
clone := make([]string, 0, len(p.enclosingLayer)+len(layer))
|
||
layer = append(append(clone, p.enclosingLayer...), layer...)
|
||
}
|
||
p.layersPostImport = append(p.layersPostImport, layer)
|
||
}
|
||
}
|
||
|
||
type ruleContext struct {
|
||
isTopLevel bool
|
||
parseSelectors bool
|
||
}
|
||
|
||
func (p *parser) parseListOfRules(context ruleContext) []css_ast.Rule {
|
||
atRuleContext := atRuleContext{}
|
||
if context.isTopLevel {
|
||
atRuleContext.charsetValidity = atRuleValid
|
||
atRuleContext.importValidity = atRuleValid
|
||
atRuleContext.isTopLevel = true
|
||
}
|
||
rules := []css_ast.Rule{}
|
||
didFindAtImport := false
|
||
|
||
loop:
|
||
for {
|
||
if context.isTopLevel {
|
||
p.nestingIsPresent = false
|
||
}
|
||
|
||
// If there are any legal comments immediately before the current token,
|
||
// turn them all into comment rules and append them to the current rule list
|
||
for p.legalCommentIndex < len(p.legalComments) {
|
||
comment := p.legalComments[p.legalCommentIndex]
|
||
if comment.TokenIndexAfter > uint32(p.index) {
|
||
break
|
||
}
|
||
if comment.TokenIndexAfter == uint32(p.index) {
|
||
rules = append(rules, css_ast.Rule{Loc: comment.Loc, Data: &css_ast.RComment{Text: comment.Text}})
|
||
}
|
||
p.legalCommentIndex++
|
||
}
|
||
|
||
switch p.current().Kind {
|
||
case css_lexer.TEndOfFile:
|
||
break loop
|
||
|
||
case css_lexer.TCloseBrace:
|
||
if !context.isTopLevel {
|
||
break loop
|
||
}
|
||
|
||
case css_lexer.TWhitespace:
|
||
p.advance()
|
||
continue
|
||
|
||
case css_lexer.TAtKeyword:
|
||
rule := p.parseAtRule(atRuleContext)
|
||
|
||
// Disallow "@charset" and "@import" after other rules
|
||
if context.isTopLevel {
|
||
switch r := rule.Data.(type) {
|
||
case *css_ast.RAtCharset:
|
||
// This doesn't invalidate anything because it always comes first
|
||
|
||
case *css_ast.RAtImport:
|
||
didFindAtImport = true
|
||
if atRuleContext.charsetValidity == atRuleValid {
|
||
atRuleContext.afterLoc = rule.Loc
|
||
atRuleContext.charsetValidity = atRuleInvalidAfter
|
||
}
|
||
|
||
case *css_ast.RAtLayer:
|
||
if atRuleContext.charsetValidity == atRuleValid {
|
||
atRuleContext.afterLoc = rule.Loc
|
||
atRuleContext.charsetValidity = atRuleInvalidAfter
|
||
}
|
||
|
||
// From the specification: "Note: No @layer rules are allowed between
|
||
// @import and @namespace rules. Any @layer rule that comes after an
|
||
// @import or @namespace rule will cause any subsequent @import or
|
||
// @namespace rules to be ignored."
|
||
if atRuleContext.importValidity == atRuleValid && (r.Rules != nil || didFindAtImport) {
|
||
atRuleContext.afterLoc = rule.Loc
|
||
atRuleContext.charsetValidity = atRuleInvalidAfter
|
||
atRuleContext.importValidity = atRuleInvalidAfter
|
||
}
|
||
|
||
default:
|
||
if atRuleContext.importValidity == atRuleValid {
|
||
atRuleContext.afterLoc = rule.Loc
|
||
atRuleContext.charsetValidity = atRuleInvalidAfter
|
||
atRuleContext.importValidity = atRuleInvalidAfter
|
||
}
|
||
}
|
||
}
|
||
|
||
// Lower CSS nesting if it's not supported (but only at the top level)
|
||
if p.nestingIsPresent && p.options.unsupportedCSSFeatures.Has(compat.Nesting) && context.isTopLevel {
|
||
rules = p.lowerNestingInRule(rule, rules)
|
||
} else {
|
||
rules = append(rules, rule)
|
||
}
|
||
continue
|
||
|
||
case css_lexer.TCDO, css_lexer.TCDC:
|
||
if context.isTopLevel {
|
||
p.advance()
|
||
continue
|
||
}
|
||
}
|
||
|
||
if atRuleContext.importValidity == atRuleValid {
|
||
atRuleContext.afterLoc = p.current().Range.Loc
|
||
atRuleContext.charsetValidity = atRuleInvalidAfter
|
||
atRuleContext.importValidity = atRuleInvalidAfter
|
||
}
|
||
|
||
// Note: CSS recently changed to parse and discard declarations
|
||
// here instead of treating them as the start of a qualified rule.
|
||
// See also: https://github.com/w3c/csswg-drafts/issues/8834
|
||
if !context.isTopLevel {
|
||
if scan, index := p.scanForEndOfRule(); scan == endOfRuleSemicolon {
|
||
tokens := p.convertTokens(p.tokens[p.index:index])
|
||
rules = append(rules, css_ast.Rule{Loc: p.current().Range.Loc, Data: &css_ast.RBadDeclaration{Tokens: tokens}})
|
||
p.index = index + 1
|
||
continue
|
||
}
|
||
}
|
||
|
||
var rule css_ast.Rule
|
||
if context.parseSelectors {
|
||
rule = p.parseSelectorRule(context.isTopLevel, parseSelectorOpts{})
|
||
} else {
|
||
rule = p.parseQualifiedRule(parseQualifiedRuleOpts{isTopLevel: context.isTopLevel})
|
||
}
|
||
|
||
// Lower CSS nesting if it's not supported (but only at the top level)
|
||
if p.nestingIsPresent && p.options.unsupportedCSSFeatures.Has(compat.Nesting) && context.isTopLevel {
|
||
rules = p.lowerNestingInRule(rule, rules)
|
||
} else {
|
||
rules = append(rules, rule)
|
||
}
|
||
}
|
||
|
||
if p.options.minifySyntax {
|
||
rules = p.mangleRules(rules, context.isTopLevel)
|
||
}
|
||
return rules
|
||
}
|
||
|
||
type listOfDeclarationsOpts struct {
|
||
composesContext *composesContext
|
||
canInlineNoOpNesting bool
|
||
}
|
||
|
||
func (p *parser) parseListOfDeclarations(opts listOfDeclarationsOpts) (list []css_ast.Rule) {
|
||
list = []css_ast.Rule{}
|
||
foundNesting := false
|
||
|
||
for {
|
||
switch p.current().Kind {
|
||
case css_lexer.TWhitespace, css_lexer.TSemicolon:
|
||
p.advance()
|
||
|
||
case css_lexer.TEndOfFile, css_lexer.TCloseBrace:
|
||
list = p.processDeclarations(list, opts.composesContext)
|
||
if p.options.minifySyntax {
|
||
list = p.mangleRules(list, false /* isTopLevel */)
|
||
|
||
// Pull out all unnecessarily-nested declarations and stick them at the end
|
||
if opts.canInlineNoOpNesting {
|
||
// "a { & { x: y } }" => "a { x: y }"
|
||
// "a { & { b: c } d: e }" => "a { d: e; b: c }"
|
||
if foundNesting {
|
||
var inlineDecls []css_ast.Rule
|
||
n := 0
|
||
for _, rule := range list {
|
||
if rule, ok := rule.Data.(*css_ast.RSelector); ok && len(rule.Selectors) == 1 {
|
||
if sel := rule.Selectors[0]; len(sel.Selectors) == 1 && sel.Selectors[0].IsSingleAmpersand() {
|
||
inlineDecls = append(inlineDecls, rule.Rules...)
|
||
continue
|
||
}
|
||
}
|
||
list[n] = rule
|
||
n++
|
||
}
|
||
list = append(list[:n], inlineDecls...)
|
||
}
|
||
} else {
|
||
// "a, b::before { & { x: y } }" => "a, b::before { & { x: y } }"
|
||
}
|
||
}
|
||
return
|
||
|
||
case css_lexer.TAtKeyword:
|
||
if p.inSelectorSubtree > 0 {
|
||
p.nestingIsPresent = true
|
||
}
|
||
list = append(list, p.parseAtRule(atRuleContext{
|
||
isDeclarationList: true,
|
||
canInlineNoOpNesting: opts.canInlineNoOpNesting,
|
||
}))
|
||
|
||
// Reference: https://drafts.csswg.org/css-nesting-1/
|
||
default:
|
||
if scan, _ := p.scanForEndOfRule(); scan == endOfRuleOpenBrace {
|
||
p.nestingIsPresent = true
|
||
foundNesting = true
|
||
rule := p.parseSelectorRule(false, parseSelectorOpts{
|
||
isDeclarationContext: true,
|
||
composesContext: opts.composesContext,
|
||
})
|
||
|
||
// If this rule was a single ":global" or ":local", inline it here. This
|
||
// is handled differently than a bare "&" with normal CSS nesting because
|
||
// that would be inlined at the end of the parent rule's body instead,
|
||
// which is probably unexpected (e.g. it would trip people up when trying
|
||
// to write rules in a specific order).
|
||
if sel, ok := rule.Data.(*css_ast.RSelector); ok && len(sel.Selectors) == 1 {
|
||
if first := sel.Selectors[0]; len(first.Selectors) == 1 {
|
||
if first := first.Selectors[0]; first.WasEmptyFromLocalOrGlobal && first.IsSingleAmpersand() {
|
||
list = append(list, sel.Rules...)
|
||
continue
|
||
}
|
||
}
|
||
}
|
||
|
||
list = append(list, rule)
|
||
} else {
|
||
list = append(list, p.parseDeclaration())
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
func (p *parser) mangleRules(rules []css_ast.Rule, isTopLevel bool) []css_ast.Rule {
|
||
// Remove empty rules
|
||
mangledRules := make([]css_ast.Rule, 0, len(rules))
|
||
var prevNonComment css_ast.R
|
||
next:
|
||
for _, rule := range rules {
|
||
nextNonComment := rule.Data
|
||
|
||
switch r := rule.Data.(type) {
|
||
case *css_ast.RAtKeyframes:
|
||
// Do not remove empty "@keyframe foo {}" rules. Even empty rules still
|
||
// dispatch JavaScript animation events, so removing them changes
|
||
// behavior: https://bugzilla.mozilla.org/show_bug.cgi?id=1004377.
|
||
|
||
case *css_ast.RAtLayer:
|
||
if len(r.Rules) == 0 && len(r.Names) > 0 {
|
||
// Do not remove empty "@layer foo {}" rules. The specification says:
|
||
// "Cascade layers are sorted by the order in which they first are
|
||
// declared, with nested layers grouped within their parent layers
|
||
// before any unlayered rules." So removing empty rules could change
|
||
// the order in which they are first declared, and is therefore invalid.
|
||
//
|
||
// We can turn "@layer foo {}" into "@layer foo;" to be shorter. But
|
||
// don't collapse anonymous "@layer {}" into "@layer;" because that is
|
||
// a syntax error.
|
||
r.Rules = nil
|
||
} else if len(r.Rules) == 1 && len(r.Names) == 1 {
|
||
// Only collapse layers if each layer has exactly one name
|
||
if r2, ok := r.Rules[0].Data.(*css_ast.RAtLayer); ok && len(r2.Names) == 1 {
|
||
// "@layer a { @layer b {} }" => "@layer a.b;"
|
||
// "@layer a { @layer b { c {} } }" => "@layer a.b { c {} }"
|
||
r.Names[0] = append(r.Names[0], r2.Names[0]...)
|
||
r.Rules = r2.Rules
|
||
}
|
||
}
|
||
|
||
case *css_ast.RKnownAt:
|
||
if len(r.Rules) == 0 && atKnownRuleCanBeRemovedIfEmpty[r.AtToken] {
|
||
continue
|
||
}
|
||
|
||
case *css_ast.RAtMedia:
|
||
if len(r.Rules) == 0 {
|
||
continue
|
||
}
|
||
|
||
// Unwrap "@media" rules that duplicate conditions from a parent "@media"
|
||
// rule. This is unlikely to be authored manually but can be automatically
|
||
// generated when using a CSS framework such as Tailwind.
|
||
//
|
||
// @media (min-width: 1024px) {
|
||
// .md\:class {
|
||
// color: red;
|
||
// }
|
||
// @media (min-width: 1024px) {
|
||
// .md\:class {
|
||
// color: red;
|
||
// }
|
||
// }
|
||
// }
|
||
//
|
||
// This converts that code into the following:
|
||
//
|
||
// @media (min-width: 1024px) {
|
||
// .md\:class {
|
||
// color: red;
|
||
// }
|
||
// .md\:class {
|
||
// color: red;
|
||
// }
|
||
// }
|
||
//
|
||
// Which can then be mangled further.
|
||
for _, queries := range p.enclosingAtMedia {
|
||
if css_ast.MediaQueriesEqual(r.Queries, queries, nil) {
|
||
mangledRules = append(mangledRules, r.Rules...)
|
||
continue next
|
||
}
|
||
}
|
||
|
||
case *css_ast.RSelector:
|
||
if len(r.Rules) == 0 {
|
||
continue
|
||
}
|
||
|
||
// Merge adjacent selectors with the same content
|
||
// "a { color: red; } b { color: red; }" => "a, b { color: red; }"
|
||
if prevNonComment != nil {
|
||
if r, ok := rule.Data.(*css_ast.RSelector); ok {
|
||
if prev, ok := prevNonComment.(*css_ast.RSelector); ok && css_ast.RulesEqual(r.Rules, prev.Rules, nil) &&
|
||
isSafeSelectors(r.Selectors) && isSafeSelectors(prev.Selectors) {
|
||
nextSelector:
|
||
for _, sel := range r.Selectors {
|
||
for _, prevSel := range prev.Selectors {
|
||
if sel.Equal(prevSel, nil) {
|
||
// Don't add duplicate selectors more than once
|
||
continue nextSelector
|
||
}
|
||
}
|
||
prev.Selectors = append(prev.Selectors, sel)
|
||
}
|
||
continue
|
||
}
|
||
}
|
||
}
|
||
|
||
case *css_ast.RComment:
|
||
nextNonComment = nil
|
||
}
|
||
|
||
if nextNonComment != nil {
|
||
prevNonComment = nextNonComment
|
||
}
|
||
|
||
mangledRules = append(mangledRules, rule)
|
||
}
|
||
|
||
// Mangle non-top-level rules using a back-to-front pass. Top-level rules
|
||
// will be mangled by the linker instead for cross-file rule mangling.
|
||
if !isTopLevel {
|
||
remover := MakeDeadRuleMangler(ast.SymbolMap{})
|
||
mangledRules = remover.RemoveDeadRulesInPlace(p.source.Index, mangledRules, p.importRecords)
|
||
}
|
||
|
||
return mangledRules
|
||
}
|
||
|
||
type ruleEntry struct {
|
||
data css_ast.R
|
||
callCounter uint32
|
||
}
|
||
|
||
type hashEntry struct {
|
||
rules []ruleEntry
|
||
}
|
||
|
||
type callEntry struct {
|
||
importRecords []ast.ImportRecord
|
||
sourceIndex uint32
|
||
}
|
||
|
||
type DeadRuleRemover struct {
|
||
entries map[uint32]hashEntry
|
||
calls []callEntry
|
||
check css_ast.CrossFileEqualityCheck
|
||
}
|
||
|
||
func MakeDeadRuleMangler(symbols ast.SymbolMap) DeadRuleRemover {
|
||
return DeadRuleRemover{
|
||
entries: make(map[uint32]hashEntry),
|
||
check: css_ast.CrossFileEqualityCheck{Symbols: symbols},
|
||
}
|
||
}
|
||
|
||
func (remover *DeadRuleRemover) RemoveDeadRulesInPlace(sourceIndex uint32, rules []css_ast.Rule, importRecords []ast.ImportRecord) []css_ast.Rule {
|
||
// The caller may call this function multiple times, each with a different
|
||
// set of import records. Remember each set of import records for equality
|
||
// checks later.
|
||
callCounter := uint32(len(remover.calls))
|
||
remover.calls = append(remover.calls, callEntry{importRecords, sourceIndex})
|
||
|
||
// Remove duplicate rules, scanning from the back so we keep the last
|
||
// duplicate. Note that the linker calls this, so we do not want to do
|
||
// anything that modifies the rules themselves. One reason is that ASTs
|
||
// are immutable at the linking stage. Another reason is that merging
|
||
// CSS ASTs from separate files will mess up source maps because a single
|
||
// AST cannot simultaneously represent offsets from multiple files.
|
||
n := len(rules)
|
||
start := n
|
||
skipRule:
|
||
for i := n - 1; i >= 0; i-- {
|
||
rule := rules[i]
|
||
|
||
// Remove rules with selectors that don't apply to anything (e.g. ":is()")
|
||
if r, ok := rule.Data.(*css_ast.RSelector); ok && allSelectorsAreDead(r.Selectors) {
|
||
continue skipRule
|
||
}
|
||
|
||
// For duplicate rules, omit all but the last copy
|
||
if hash, ok := rule.Data.Hash(); ok {
|
||
entry := remover.entries[hash]
|
||
for _, current := range entry.rules {
|
||
var check *css_ast.CrossFileEqualityCheck
|
||
|
||
// If this rule was from another file, then pass along both arrays
|
||
// of import records so that the equality check for "url()" tokens
|
||
// can use them to check for equality.
|
||
if current.callCounter != callCounter {
|
||
// Reuse the same memory allocation
|
||
check = &remover.check
|
||
call := remover.calls[current.callCounter]
|
||
check.ImportRecordsA = importRecords
|
||
check.ImportRecordsB = call.importRecords
|
||
check.SourceIndexA = sourceIndex
|
||
check.SourceIndexB = call.sourceIndex
|
||
}
|
||
|
||
if rule.Data.Equal(current.data, check) {
|
||
continue skipRule
|
||
}
|
||
}
|
||
entry.rules = append(entry.rules, ruleEntry{
|
||
data: rule.Data,
|
||
callCounter: callCounter,
|
||
})
|
||
remover.entries[hash] = entry
|
||
}
|
||
|
||
start--
|
||
rules[start] = rule
|
||
}
|
||
|
||
return rules[start:]
|
||
}
|
||
|
||
func containsDeadSelectors(selectors []css_ast.CompoundSelector) bool {
|
||
for _, sel := range selectors {
|
||
for _, ss := range sel.SubclassSelectors {
|
||
if pseudo, ok := ss.Data.(*css_ast.SSPseudoClassWithSelectorList); ok && len(pseudo.Selectors) == 0 &&
|
||
(pseudo.Kind == css_ast.PseudoClassIs || pseudo.Kind == css_ast.PseudoClassWhere) {
|
||
// ":is()" and ":where()" never match anything when empty
|
||
return true
|
||
}
|
||
}
|
||
}
|
||
return false
|
||
}
|
||
|
||
func allSelectorsAreDead(selectors []css_ast.ComplexSelector) bool {
|
||
for _, sel := range selectors {
|
||
if !containsDeadSelectors(sel.Selectors) {
|
||
return false
|
||
}
|
||
}
|
||
return true
|
||
}
|
||
|
||
// Reference: https://developer.mozilla.org/en-US/docs/Web/HTML/Element
|
||
var nonDeprecatedElementsSupportedByIE7 = map[string]bool{
|
||
"a": true,
|
||
"abbr": true,
|
||
"address": true,
|
||
"area": true,
|
||
"b": true,
|
||
"base": true,
|
||
"blockquote": true,
|
||
"body": true,
|
||
"br": true,
|
||
"button": true,
|
||
"caption": true,
|
||
"cite": true,
|
||
"code": true,
|
||
"col": true,
|
||
"colgroup": true,
|
||
"dd": true,
|
||
"del": true,
|
||
"dfn": true,
|
||
"div": true,
|
||
"dl": true,
|
||
"dt": true,
|
||
"em": true,
|
||
"embed": true,
|
||
"fieldset": true,
|
||
"form": true,
|
||
"h1": true,
|
||
"h2": true,
|
||
"h3": true,
|
||
"h4": true,
|
||
"h5": true,
|
||
"h6": true,
|
||
"head": true,
|
||
"hr": true,
|
||
"html": true,
|
||
"i": true,
|
||
"iframe": true,
|
||
"img": true,
|
||
"input": true,
|
||
"ins": true,
|
||
"kbd": true,
|
||
"label": true,
|
||
"legend": true,
|
||
"li": true,
|
||
"link": true,
|
||
"map": true,
|
||
"menu": true,
|
||
"meta": true,
|
||
"noscript": true,
|
||
"object": true,
|
||
"ol": true,
|
||
"optgroup": true,
|
||
"option": true,
|
||
"p": true,
|
||
"param": true,
|
||
"pre": true,
|
||
"q": true,
|
||
"ruby": true,
|
||
"s": true,
|
||
"samp": true,
|
||
"script": true,
|
||
"select": true,
|
||
"small": true,
|
||
"span": true,
|
||
"strong": true,
|
||
"style": true,
|
||
"sub": true,
|
||
"sup": true,
|
||
"table": true,
|
||
"tbody": true,
|
||
"td": true,
|
||
"textarea": true,
|
||
"tfoot": true,
|
||
"th": true,
|
||
"thead": true,
|
||
"title": true,
|
||
"tr": true,
|
||
"u": true,
|
||
"ul": true,
|
||
"var": true,
|
||
}
|
||
|
||
// This only returns true if all of these selectors are considered "safe" which
|
||
// means that they are very likely to work in any browser a user might reasonably
|
||
// be using. We do NOT want to merge adjacent qualified rules with the same body
|
||
// if any of the selectors are unsafe, since then browsers which don't support
|
||
// that particular feature would ignore the entire merged qualified rule:
|
||
//
|
||
// Input:
|
||
// a { color: red }
|
||
// b { color: red }
|
||
// input::-moz-placeholder { color: red }
|
||
//
|
||
// Valid output:
|
||
// a, b { color: red }
|
||
// input::-moz-placeholder { color: red }
|
||
//
|
||
// Invalid output:
|
||
// a, b, input::-moz-placeholder { color: red }
|
||
//
|
||
// This considers IE 7 and above to be a browser that a user could possibly use.
|
||
// Versions of IE less than 6 are not considered.
|
||
func isSafeSelectors(complexSelectors []css_ast.ComplexSelector) bool {
|
||
for _, complex := range complexSelectors {
|
||
for _, compound := range complex.Selectors {
|
||
if len(compound.NestingSelectorLocs) > 0 {
|
||
// Bail because this is an extension: https://drafts.csswg.org/css-nesting-1/
|
||
return false
|
||
}
|
||
|
||
if compound.Combinator.Byte != 0 {
|
||
// "Before Internet Explorer 10, the combinator only works in standards mode"
|
||
// Reference: https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Selectors
|
||
return false
|
||
}
|
||
|
||
if compound.TypeSelector != nil {
|
||
if compound.TypeSelector.NamespacePrefix != nil {
|
||
// Bail if we hit a namespace, which doesn't work in IE before version 9
|
||
// Reference: https://developer.mozilla.org/en-US/docs/Web/CSS/Type_selectors
|
||
return false
|
||
}
|
||
|
||
if compound.TypeSelector.Name.Kind == css_lexer.TIdent && !nonDeprecatedElementsSupportedByIE7[compound.TypeSelector.Name.Text] {
|
||
// Bail if this element is either deprecated or not supported in IE 7
|
||
return false
|
||
}
|
||
}
|
||
|
||
for _, ss := range compound.SubclassSelectors {
|
||
switch s := ss.Data.(type) {
|
||
case *css_ast.SSAttribute:
|
||
if s.MatcherModifier != 0 {
|
||
// Bail if we hit a case modifier, which doesn't work in IE at all
|
||
// Reference: https://developer.mozilla.org/en-US/docs/Web/CSS/Attribute_selectors
|
||
return false
|
||
}
|
||
|
||
case *css_ast.SSPseudoClass:
|
||
// Bail if this pseudo class doesn't match a hard-coded list that's
|
||
// known to work everywhere. For example, ":focus" doesn't work in IE 7.
|
||
// Reference: https://developer.mozilla.org/en-US/docs/Web/CSS/Pseudo-classes
|
||
if s.Args == nil && !s.IsElement {
|
||
switch s.Name {
|
||
case "active", "first-child", "hover", "link", "visited":
|
||
continue
|
||
}
|
||
}
|
||
return false
|
||
|
||
case *css_ast.SSPseudoClassWithSelectorList:
|
||
// These definitely don't work in IE 7
|
||
return false
|
||
}
|
||
}
|
||
}
|
||
}
|
||
return true
|
||
}
|
||
|
||
func (p *parser) parseURLOrString() (string, logger.Range, bool) {
|
||
t := p.current()
|
||
switch t.Kind {
|
||
case css_lexer.TString:
|
||
text := p.decoded()
|
||
p.advance()
|
||
return text, t.Range, true
|
||
|
||
case css_lexer.TURL:
|
||
text := p.decoded()
|
||
p.advance()
|
||
return text, t.Range, true
|
||
|
||
case css_lexer.TFunction:
|
||
if strings.EqualFold(p.decoded(), "url") {
|
||
matchingLoc := logger.Loc{Start: p.current().Range.End() - 1}
|
||
i := p.index + 1
|
||
|
||
// Skip over whitespace
|
||
for p.at(i).Kind == css_lexer.TWhitespace {
|
||
i++
|
||
}
|
||
|
||
// Consume a string
|
||
if p.at(i).Kind == css_lexer.TString {
|
||
stringIndex := i
|
||
i++
|
||
|
||
// Skip over whitespace
|
||
for p.at(i).Kind == css_lexer.TWhitespace {
|
||
i++
|
||
}
|
||
|
||
// Consume a closing parenthesis
|
||
if close := p.at(i).Kind; close == css_lexer.TCloseParen || close == css_lexer.TEndOfFile {
|
||
t := p.at(stringIndex)
|
||
text := t.DecodedText(p.source.Contents)
|
||
p.index = i
|
||
p.expectWithMatchingLoc(css_lexer.TCloseParen, matchingLoc)
|
||
return text, t.Range, true
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
return "", logger.Range{}, false
|
||
}
|
||
|
||
func (p *parser) expectURLOrString() (url string, r logger.Range, ok bool) {
|
||
url, r, ok = p.parseURLOrString()
|
||
if !ok {
|
||
p.expect(css_lexer.TURL)
|
||
}
|
||
return
|
||
}
|
||
|
||
type atRuleKind uint8
|
||
|
||
const (
|
||
atRuleUnknown atRuleKind = iota
|
||
atRuleDeclarations
|
||
atRuleInheritContext
|
||
atRuleQualifiedOrEmpty
|
||
atRuleEmpty
|
||
)
|
||
|
||
var specialAtRules = map[string]atRuleKind{
|
||
"media": atRuleInheritContext,
|
||
"supports": atRuleInheritContext,
|
||
|
||
"font-face": atRuleDeclarations,
|
||
"page": atRuleDeclarations,
|
||
|
||
// These go inside "@page": https://www.w3.org/TR/css-page-3/#syntax-page-selector
|
||
"bottom-center": atRuleDeclarations,
|
||
"bottom-left-corner": atRuleDeclarations,
|
||
"bottom-left": atRuleDeclarations,
|
||
"bottom-right-corner": atRuleDeclarations,
|
||
"bottom-right": atRuleDeclarations,
|
||
"left-bottom": atRuleDeclarations,
|
||
"left-middle": atRuleDeclarations,
|
||
"left-top": atRuleDeclarations,
|
||
"right-bottom": atRuleDeclarations,
|
||
"right-middle": atRuleDeclarations,
|
||
"right-top": atRuleDeclarations,
|
||
"top-center": atRuleDeclarations,
|
||
"top-left-corner": atRuleDeclarations,
|
||
"top-left": atRuleDeclarations,
|
||
"top-right-corner": atRuleDeclarations,
|
||
"top-right": atRuleDeclarations,
|
||
|
||
// These properties are very deprecated and appear to only be useful for
|
||
// mobile versions of internet explorer (which may no longer exist?), but
|
||
// they are used by the https://ant.design/ design system so we recognize
|
||
// them to avoid the warning.
|
||
//
|
||
// Documentation: https://developer.mozilla.org/en-US/docs/Web/CSS/@viewport
|
||
// Discussion: https://github.com/w3c/csswg-drafts/issues/4766
|
||
//
|
||
"viewport": atRuleDeclarations,
|
||
"-ms-viewport": atRuleDeclarations,
|
||
|
||
// This feature has been removed from the web because it's actively harmful.
|
||
// However, there is one exception where "@-moz-document url-prefix() {" is
|
||
// accepted by Firefox to basically be an "if Firefox" conditional rule.
|
||
//
|
||
// Documentation: https://developer.mozilla.org/en-US/docs/Web/CSS/@document
|
||
// Discussion: https://bugzilla.mozilla.org/show_bug.cgi?id=1035091
|
||
//
|
||
"document": atRuleInheritContext,
|
||
"-moz-document": atRuleInheritContext,
|
||
|
||
// This is a new feature that changes how the CSS rule cascade works. It can
|
||
// end in either a "{}" block or a ";" rule terminator so we need this special
|
||
// case to support both.
|
||
//
|
||
// Documentation: https://developer.mozilla.org/en-US/docs/Web/CSS/@layer
|
||
// Motivation: https://developer.chrome.com/blog/cascade-layers/
|
||
//
|
||
"layer": atRuleQualifiedOrEmpty,
|
||
|
||
// Reference: https://drafts.csswg.org/css-cascade-6/#scoped-styles
|
||
"scope": atRuleInheritContext,
|
||
|
||
// Reference: https://drafts.csswg.org/css-fonts-4/#font-palette-values
|
||
"font-palette-values": atRuleDeclarations,
|
||
|
||
// Documentation: https://developer.mozilla.org/en-US/docs/Web/CSS/@counter-style
|
||
// Reference: https://drafts.csswg.org/css-counter-styles/#the-counter-style-rule
|
||
"counter-style": atRuleDeclarations,
|
||
|
||
// Documentation: https://developer.mozilla.org/en-US/docs/Web/CSS/@font-feature-values
|
||
// Reference: https://drafts.csswg.org/css-fonts/#font-feature-values
|
||
"font-feature-values": atRuleDeclarations,
|
||
"annotation": atRuleDeclarations,
|
||
"character-variant": atRuleDeclarations,
|
||
"historical-forms": atRuleDeclarations,
|
||
"ornaments": atRuleDeclarations,
|
||
"styleset": atRuleDeclarations,
|
||
"stylistic": atRuleDeclarations,
|
||
"swash": atRuleDeclarations,
|
||
|
||
// Container Queries
|
||
// Reference: https://drafts.csswg.org/css-contain-3/#container-rule
|
||
"container": atRuleInheritContext,
|
||
|
||
// Defining before-change style: the @starting-style rule
|
||
// Reference: https://drafts.csswg.org/css-transitions-2/#defining-before-change-style-the-starting-style-rule
|
||
"starting-style": atRuleInheritContext,
|
||
|
||
// Anchor Positioning
|
||
// Reference: https://drafts.csswg.org/css-anchor-position-1/#at-ruledef-position-try
|
||
"position-try": atRuleDeclarations,
|
||
|
||
// @view-transition
|
||
// Reference: https://drafts.csswg.org/css-view-transitions-2/#view-transition-rule
|
||
"view-transition": atRuleDeclarations,
|
||
}
|
||
|
||
var atKnownRuleCanBeRemovedIfEmpty = map[string]bool{
|
||
"media": true,
|
||
"supports": true,
|
||
"font-face": true,
|
||
"page": true,
|
||
|
||
// https://www.w3.org/TR/css-page-3/#syntax-page-selector
|
||
"bottom-center": true,
|
||
"bottom-left-corner": true,
|
||
"bottom-left": true,
|
||
"bottom-right-corner": true,
|
||
"bottom-right": true,
|
||
"left-bottom": true,
|
||
"left-middle": true,
|
||
"left-top": true,
|
||
"right-bottom": true,
|
||
"right-middle": true,
|
||
"right-top": true,
|
||
"top-center": true,
|
||
"top-left-corner": true,
|
||
"top-left": true,
|
||
"top-right-corner": true,
|
||
"top-right": true,
|
||
|
||
// https://drafts.csswg.org/css-cascade-6/#scoped-styles
|
||
"scope": true,
|
||
|
||
// https://drafts.csswg.org/css-fonts-4/#font-palette-values
|
||
"font-palette-values": true,
|
||
|
||
// https://drafts.csswg.org/css-contain-3/#container-rule
|
||
"container": true,
|
||
}
|
||
|
||
type atRuleValidity uint8
|
||
|
||
const (
|
||
atRuleInvalid atRuleValidity = iota
|
||
atRuleValid
|
||
atRuleInvalidAfter
|
||
)
|
||
|
||
type atRuleContext struct {
|
||
afterLoc logger.Loc
|
||
charsetValidity atRuleValidity
|
||
importValidity atRuleValidity
|
||
canInlineNoOpNesting bool
|
||
isDeclarationList bool
|
||
isTopLevel bool
|
||
}
|
||
|
||
func (p *parser) parseAtRule(context atRuleContext) css_ast.Rule {
|
||
// Parse the name
|
||
atToken := p.decoded()
|
||
atRange := p.current().Range
|
||
lowerAtToken := strings.ToLower(atToken)
|
||
kind := specialAtRules[lowerAtToken]
|
||
p.advance()
|
||
|
||
// Parse the prelude
|
||
preludeStart := p.index
|
||
abortRuleParser:
|
||
switch lowerAtToken {
|
||
case "charset":
|
||
switch context.charsetValidity {
|
||
case atRuleInvalid:
|
||
p.log.AddID(logger.MsgID_CSS_InvalidAtCharset, logger.Warning, &p.tracker, atRange, "\"@charset\" must be the first rule in the file")
|
||
|
||
case atRuleInvalidAfter:
|
||
p.log.AddIDWithNotes(logger.MsgID_CSS_InvalidAtCharset, logger.Warning, &p.tracker, atRange,
|
||
"\"@charset\" must be the first rule in the file",
|
||
[]logger.MsgData{p.tracker.MsgData(logger.Range{Loc: context.afterLoc},
|
||
"This rule cannot come before a \"@charset\" rule")})
|
||
|
||
case atRuleValid:
|
||
kind = atRuleEmpty
|
||
p.expect(css_lexer.TWhitespace)
|
||
if p.peek(css_lexer.TString) {
|
||
encoding := p.decoded()
|
||
if !strings.EqualFold(encoding, "UTF-8") {
|
||
p.log.AddID(logger.MsgID_CSS_UnsupportedAtCharset, logger.Warning, &p.tracker, p.current().Range,
|
||
fmt.Sprintf("\"UTF-8\" will be used instead of unsupported charset %q", encoding))
|
||
}
|
||
p.advance()
|
||
p.expect(css_lexer.TSemicolon)
|
||
return css_ast.Rule{Loc: atRange.Loc, Data: &css_ast.RAtCharset{Encoding: encoding}}
|
||
}
|
||
p.expect(css_lexer.TString)
|
||
}
|
||
|
||
case "import":
|
||
switch context.importValidity {
|
||
case atRuleInvalid:
|
||
p.log.AddID(logger.MsgID_CSS_InvalidAtImport, logger.Warning, &p.tracker, atRange, "\"@import\" is only valid at the top level")
|
||
|
||
case atRuleInvalidAfter:
|
||
p.log.AddIDWithNotes(logger.MsgID_CSS_InvalidAtImport, logger.Warning, &p.tracker, atRange,
|
||
"All \"@import\" rules must come first",
|
||
[]logger.MsgData{p.tracker.MsgData(logger.Range{Loc: context.afterLoc},
|
||
"This rule cannot come before an \"@import\" rule")})
|
||
|
||
case atRuleValid:
|
||
kind = atRuleEmpty
|
||
p.eat(css_lexer.TWhitespace)
|
||
if path, r, ok := p.expectURLOrString(); ok {
|
||
var conditions css_ast.ImportConditions
|
||
importConditionsStart := p.index
|
||
|
||
// Parse the optional "layer()"
|
||
p.eat(css_lexer.TWhitespace)
|
||
if (p.peek(css_lexer.TIdent) || p.peek(css_lexer.TFunction)) && strings.EqualFold(p.decoded(), "layer") {
|
||
p.parseComponentValue()
|
||
conditions.Layers = p.convertTokens(p.tokens[importConditionsStart:p.index])
|
||
importConditionsStart = p.index
|
||
|
||
// Remove leading and trailing whitespace
|
||
if len(conditions.Layers) > 0 {
|
||
conditions.Layers[0].Whitespace &= ^(css_ast.WhitespaceBefore | css_ast.WhitespaceAfter)
|
||
}
|
||
}
|
||
|
||
// Parse the optional "supports()"
|
||
p.eat(css_lexer.TWhitespace)
|
||
if p.peek(css_lexer.TFunction) && strings.EqualFold(p.decoded(), "supports") {
|
||
p.parseComponentValue()
|
||
conditions.Supports = p.convertTokens(p.tokens[importConditionsStart:p.index])
|
||
importConditionsStart = p.index
|
||
|
||
// Remove leading and trailing whitespace
|
||
if len(conditions.Supports) > 0 {
|
||
conditions.Supports[0].Whitespace &= ^(css_ast.WhitespaceBefore | css_ast.WhitespaceAfter)
|
||
}
|
||
}
|
||
|
||
// Parse the optional media query list
|
||
conditions.Queries = p.parseMediaQueryListUntil(func(kind css_lexer.T) bool {
|
||
return kind == css_lexer.TSemicolon || kind == css_lexer.TOpenBrace ||
|
||
kind == css_lexer.TCloseBrace || kind == css_lexer.TEndOfFile
|
||
})
|
||
if p.peek(css_lexer.TOpenBrace) {
|
||
break // Avoid parsing an invalid "@import" rule
|
||
}
|
||
|
||
// Check whether any import conditions are present
|
||
var importConditions *css_ast.ImportConditions
|
||
if len(conditions.Layers) > 0 || len(conditions.Supports) > 0 || len(conditions.Queries) > 0 {
|
||
importConditions = &conditions
|
||
}
|
||
|
||
p.expect(css_lexer.TSemicolon)
|
||
importRecordIndex := uint32(len(p.importRecords))
|
||
p.importRecords = append(p.importRecords, ast.ImportRecord{
|
||
Kind: ast.ImportAt,
|
||
Path: logger.Path{Text: path},
|
||
Range: r,
|
||
})
|
||
|
||
// Fill in the pre-import layers once we see the first "@import"
|
||
if !p.hasSeenAtImport {
|
||
p.hasSeenAtImport = true
|
||
p.layersPreImport = p.layersPostImport
|
||
p.layersPostImport = nil
|
||
}
|
||
|
||
return css_ast.Rule{Loc: atRange.Loc, Data: &css_ast.RAtImport{
|
||
ImportRecordIndex: importRecordIndex,
|
||
ImportConditions: importConditions,
|
||
}}
|
||
}
|
||
}
|
||
|
||
case "keyframes", "-webkit-keyframes", "-moz-keyframes", "-ms-keyframes", "-o-keyframes":
|
||
p.eat(css_lexer.TWhitespace)
|
||
nameLoc := p.current().Range.Loc
|
||
var name string
|
||
|
||
if p.peek(css_lexer.TIdent) {
|
||
name = p.decoded()
|
||
if isInvalidAnimationName(name) {
|
||
msg := logger.Msg{
|
||
ID: logger.MsgID_CSS_CSSSyntaxError,
|
||
Kind: logger.Warning,
|
||
Data: p.tracker.MsgData(p.current().Range, fmt.Sprintf("Cannot use %q as a name for \"@keyframes\" without quotes", name)),
|
||
Notes: []logger.MsgData{{Text: fmt.Sprintf("You can put %q in quotes to prevent it from becoming a CSS keyword.", name)}},
|
||
}
|
||
msg.Data.Location.Suggestion = fmt.Sprintf("%q", name)
|
||
p.log.AddMsg(msg)
|
||
break
|
||
}
|
||
p.advance()
|
||
} else if p.peek(css_lexer.TString) {
|
||
// Note: Strings as names is allowed in the CSS specification and works in
|
||
// Firefox and Safari but Chrome has strangely decided to deliberately not
|
||
// support this. We always turn all string names into identifiers to avoid
|
||
// them silently breaking in Chrome.
|
||
name = p.decoded()
|
||
p.advance()
|
||
if !p.makeLocalSymbols && isInvalidAnimationName(name) {
|
||
break
|
||
}
|
||
} else if !p.expect(css_lexer.TIdent) {
|
||
break
|
||
}
|
||
|
||
p.eat(css_lexer.TWhitespace)
|
||
blockStart := p.index
|
||
|
||
matchingLoc := p.current().Range.Loc
|
||
if p.expect(css_lexer.TOpenBrace) {
|
||
var blocks []css_ast.KeyframeBlock
|
||
|
||
badSyntax:
|
||
for {
|
||
switch p.current().Kind {
|
||
case css_lexer.TWhitespace:
|
||
p.advance()
|
||
continue
|
||
|
||
case css_lexer.TCloseBrace:
|
||
closeBraceLoc := p.current().Range.Loc
|
||
p.advance()
|
||
return css_ast.Rule{Loc: atRange.Loc, Data: &css_ast.RAtKeyframes{
|
||
AtToken: atToken,
|
||
Name: p.symbolForName(nameLoc, name),
|
||
Blocks: blocks,
|
||
CloseBraceLoc: closeBraceLoc,
|
||
}}
|
||
|
||
case css_lexer.TEndOfFile:
|
||
break badSyntax
|
||
|
||
case css_lexer.TOpenBrace:
|
||
p.expect(css_lexer.TPercentage)
|
||
break badSyntax
|
||
|
||
default:
|
||
var selectors []string
|
||
var firstSelectorLoc logger.Loc
|
||
|
||
selectors:
|
||
for {
|
||
t := p.current()
|
||
switch t.Kind {
|
||
case css_lexer.TWhitespace:
|
||
p.advance()
|
||
continue
|
||
|
||
case css_lexer.TOpenBrace:
|
||
blockMatchingLoc := p.current().Range.Loc
|
||
p.advance()
|
||
rules := p.parseListOfDeclarations(listOfDeclarationsOpts{})
|
||
closeBraceLoc := p.current().Range.Loc
|
||
if !p.expectWithMatchingLoc(css_lexer.TCloseBrace, blockMatchingLoc) {
|
||
closeBraceLoc = logger.Loc{}
|
||
}
|
||
|
||
// "@keyframes { from {} to { color: red } }" => "@keyframes { to { color: red } }"
|
||
if !p.options.minifySyntax || len(rules) > 0 {
|
||
blocks = append(blocks, css_ast.KeyframeBlock{
|
||
Selectors: selectors,
|
||
Rules: rules,
|
||
Loc: firstSelectorLoc,
|
||
CloseBraceLoc: closeBraceLoc,
|
||
})
|
||
}
|
||
break selectors
|
||
|
||
case css_lexer.TCloseBrace, css_lexer.TEndOfFile:
|
||
p.expect(css_lexer.TOpenBrace)
|
||
break badSyntax
|
||
|
||
case css_lexer.TIdent, css_lexer.TPercentage:
|
||
if firstSelectorLoc.Start == 0 {
|
||
firstSelectorLoc = p.current().Range.Loc
|
||
}
|
||
text := p.decoded()
|
||
if t.Kind == css_lexer.TIdent {
|
||
if strings.EqualFold(text, "from") {
|
||
if p.options.minifySyntax {
|
||
text = "0%" // "0%" is equivalent to but shorter than "from"
|
||
}
|
||
} else if !strings.EqualFold(text, "to") {
|
||
p.expect(css_lexer.TPercentage)
|
||
}
|
||
} else if p.options.minifySyntax && text == "100%" {
|
||
text = "to" // "to" is equivalent to but shorter than "100%"
|
||
}
|
||
selectors = append(selectors, text)
|
||
p.advance()
|
||
|
||
// Keyframe selectors are comma-separated
|
||
p.eat(css_lexer.TWhitespace)
|
||
if p.eat(css_lexer.TComma) {
|
||
p.eat(css_lexer.TWhitespace)
|
||
if k := p.current().Kind; k != css_lexer.TIdent && k != css_lexer.TPercentage {
|
||
p.expect(css_lexer.TPercentage)
|
||
break badSyntax
|
||
}
|
||
} else if k := p.current().Kind; k != css_lexer.TOpenBrace && k != css_lexer.TCloseBrace && k != css_lexer.TEndOfFile {
|
||
p.expect(css_lexer.TComma)
|
||
break badSyntax
|
||
}
|
||
|
||
default:
|
||
p.expect(css_lexer.TPercentage)
|
||
break badSyntax
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// Otherwise, finish parsing the body and return an unknown rule
|
||
for !p.peek(css_lexer.TCloseBrace) && !p.peek(css_lexer.TEndOfFile) {
|
||
p.parseComponentValue()
|
||
}
|
||
p.expectWithMatchingLoc(css_lexer.TCloseBrace, matchingLoc)
|
||
prelude := p.convertTokens(p.tokens[preludeStart:blockStart])
|
||
block, _ := p.convertTokensHelper(p.tokens[blockStart:p.index], css_lexer.TEndOfFile, convertTokensOpts{allowImports: true})
|
||
return css_ast.Rule{Loc: atRange.Loc, Data: &css_ast.RUnknownAt{AtToken: atToken, Prelude: prelude, Block: block}}
|
||
}
|
||
|
||
case "layer":
|
||
// Reference: https://developer.mozilla.org/en-US/docs/Web/CSS/@layer
|
||
|
||
// Read the layer name list
|
||
var names [][]string
|
||
p.eat(css_lexer.TWhitespace)
|
||
if p.peek(css_lexer.TIdent) {
|
||
for {
|
||
ident, ok := p.expectValidLayerNameIdent()
|
||
if !ok {
|
||
break abortRuleParser
|
||
}
|
||
name := []string{ident}
|
||
for {
|
||
p.eat(css_lexer.TWhitespace)
|
||
if !p.eat(css_lexer.TDelimDot) {
|
||
break
|
||
}
|
||
p.eat(css_lexer.TWhitespace)
|
||
ident, ok := p.expectValidLayerNameIdent()
|
||
if !ok {
|
||
break abortRuleParser
|
||
}
|
||
name = append(name, ident)
|
||
}
|
||
names = append(names, name)
|
||
p.eat(css_lexer.TWhitespace)
|
||
if !p.eat(css_lexer.TComma) {
|
||
break
|
||
}
|
||
p.eat(css_lexer.TWhitespace)
|
||
}
|
||
}
|
||
|
||
// Read the optional block
|
||
matchingLoc := p.current().Range.Loc
|
||
if len(names) <= 1 && p.eat(css_lexer.TOpenBrace) {
|
||
p.recordAtLayerRule(names)
|
||
oldEnclosingLayer := p.enclosingLayer
|
||
if len(names) == 1 {
|
||
p.enclosingLayer = append(p.enclosingLayer, names[0]...)
|
||
} else {
|
||
p.anonLayerCount++
|
||
}
|
||
|
||
// Parse the block for this rule
|
||
var rules []css_ast.Rule
|
||
if context.isDeclarationList {
|
||
rules = p.parseListOfDeclarations(listOfDeclarationsOpts{
|
||
canInlineNoOpNesting: context.canInlineNoOpNesting,
|
||
})
|
||
} else {
|
||
rules = p.parseListOfRules(ruleContext{
|
||
parseSelectors: true,
|
||
})
|
||
}
|
||
|
||
if len(names) != 1 {
|
||
p.anonLayerCount--
|
||
}
|
||
p.enclosingLayer = oldEnclosingLayer
|
||
closeBraceLoc := p.current().Range.Loc
|
||
if !p.expectWithMatchingLoc(css_lexer.TCloseBrace, matchingLoc) {
|
||
closeBraceLoc = logger.Loc{}
|
||
}
|
||
return css_ast.Rule{Loc: atRange.Loc, Data: &css_ast.RAtLayer{Names: names, Rules: rules, CloseBraceLoc: closeBraceLoc}}
|
||
}
|
||
|
||
// Handle lack of a block
|
||
if len(names) >= 1 && p.eat(css_lexer.TSemicolon) {
|
||
p.recordAtLayerRule(names)
|
||
return css_ast.Rule{Loc: atRange.Loc, Data: &css_ast.RAtLayer{Names: names}}
|
||
}
|
||
|
||
// Otherwise there's some kind of syntax error
|
||
switch p.current().Kind {
|
||
case css_lexer.TEndOfFile:
|
||
p.expect(css_lexer.TSemicolon)
|
||
p.recordAtLayerRule(names)
|
||
return css_ast.Rule{Loc: atRange.Loc, Data: &css_ast.RAtLayer{Names: names}}
|
||
|
||
case css_lexer.TCloseBrace:
|
||
p.expect(css_lexer.TSemicolon)
|
||
if !context.isTopLevel {
|
||
p.recordAtLayerRule(names)
|
||
return css_ast.Rule{Loc: atRange.Loc, Data: &css_ast.RAtLayer{Names: names}}
|
||
}
|
||
|
||
case css_lexer.TOpenBrace:
|
||
p.expect(css_lexer.TSemicolon)
|
||
|
||
default:
|
||
p.unexpected()
|
||
}
|
||
|
||
case "media":
|
||
queries := p.parseMediaQueryListUntil(func(kind css_lexer.T) bool {
|
||
return kind == css_lexer.TOpenBrace
|
||
})
|
||
|
||
// Expect a block after the query
|
||
matchingLoc := p.current().Range.Loc
|
||
if !p.expect(css_lexer.TOpenBrace) {
|
||
break
|
||
}
|
||
|
||
// Push the "@media" conditions
|
||
p.enclosingAtMedia = append(p.enclosingAtMedia, queries)
|
||
|
||
// Parse the block for this rule
|
||
var rules []css_ast.Rule
|
||
if context.isDeclarationList {
|
||
rules = p.parseListOfDeclarations(listOfDeclarationsOpts{
|
||
canInlineNoOpNesting: context.canInlineNoOpNesting,
|
||
})
|
||
} else {
|
||
rules = p.parseListOfRules(ruleContext{
|
||
parseSelectors: true,
|
||
})
|
||
}
|
||
|
||
// Pop the "@media" conditions
|
||
p.enclosingAtMedia = p.enclosingAtMedia[:len(p.enclosingAtMedia)-1]
|
||
|
||
closeBraceLoc := p.current().Range.Loc
|
||
if !p.expectWithMatchingLoc(css_lexer.TCloseBrace, matchingLoc) {
|
||
closeBraceLoc = logger.Loc{}
|
||
}
|
||
|
||
return css_ast.Rule{Loc: atRange.Loc, Data: &css_ast.RAtMedia{Queries: queries, Rules: rules, CloseBraceLoc: closeBraceLoc}}
|
||
|
||
case "scope":
|
||
var ok bool
|
||
|
||
// Parse the start limit
|
||
var start []css_ast.ComplexSelector
|
||
p.eat(css_lexer.TWhitespace)
|
||
if p.eat(css_lexer.TOpenParen) {
|
||
if start, ok = p.parseSelectorList(parseSelectorOpts{stopOnCloseParen: true}); !ok || !p.expect(css_lexer.TCloseParen) {
|
||
break
|
||
}
|
||
p.eat(css_lexer.TWhitespace)
|
||
}
|
||
|
||
// Parse the end limit
|
||
var end []css_ast.ComplexSelector
|
||
if strings.EqualFold(p.decoded(), "to") && p.eat(css_lexer.TIdent) {
|
||
p.eat(css_lexer.TWhitespace)
|
||
if !p.expect(css_lexer.TOpenParen) {
|
||
break
|
||
}
|
||
if end, ok = p.parseSelectorList(parseSelectorOpts{stopOnCloseParen: true}); !ok || !p.expect(css_lexer.TCloseParen) {
|
||
break
|
||
}
|
||
}
|
||
p.eat(css_lexer.TWhitespace)
|
||
|
||
// Expect a block after the query
|
||
matchingLoc := p.current().Range.Loc
|
||
if !p.expect(css_lexer.TOpenBrace) {
|
||
break
|
||
}
|
||
|
||
// Parse the block for this rule
|
||
var rules []css_ast.Rule
|
||
if context.isDeclarationList {
|
||
rules = p.parseListOfDeclarations(listOfDeclarationsOpts{
|
||
canInlineNoOpNesting: context.canInlineNoOpNesting,
|
||
})
|
||
} else {
|
||
rules = p.parseListOfRules(ruleContext{
|
||
parseSelectors: true,
|
||
})
|
||
}
|
||
|
||
closeBraceLoc := p.current().Range.Loc
|
||
if !p.expectWithMatchingLoc(css_lexer.TCloseBrace, matchingLoc) {
|
||
closeBraceLoc = logger.Loc{}
|
||
}
|
||
|
||
return css_ast.Rule{Loc: atRange.Loc, Data: &css_ast.RAtScope{Start: start, End: end, Rules: rules, CloseBraceLoc: closeBraceLoc}}
|
||
|
||
default:
|
||
if kind == atRuleUnknown && lowerAtToken == "namespace" {
|
||
// CSS namespaces are a weird feature that appears to only really be
|
||
// useful for styling XML. And the world has moved on from XHTML to
|
||
// HTML5 so pretty much no one uses CSS namespaces anymore. They are
|
||
// also complicated to support in a bundler because CSS namespaces are
|
||
// file-scoped, which means:
|
||
//
|
||
// * Default namespaces can be different in different files, in which
|
||
// case some default namespaces would have to be converted to prefixed
|
||
// namespaces to avoid collisions.
|
||
//
|
||
// * Prefixed namespaces from different files can use the same name, in
|
||
// which case some prefixed namespaces would need to be renamed to
|
||
// avoid collisions.
|
||
//
|
||
// Instead of implementing all of that for an extremely obscure feature,
|
||
// CSS namespaces are just explicitly not supported.
|
||
p.log.AddID(logger.MsgID_CSS_UnsupportedAtNamespace, logger.Warning, &p.tracker, atRange, "\"@namespace\" rules are not supported")
|
||
}
|
||
}
|
||
|
||
// Parse an unknown prelude
|
||
p.index = preludeStart
|
||
prelude:
|
||
for {
|
||
switch p.current().Kind {
|
||
case css_lexer.TOpenBrace, css_lexer.TEndOfFile:
|
||
break prelude
|
||
|
||
case css_lexer.TSemicolon, css_lexer.TCloseBrace:
|
||
prelude := p.convertTokens(p.tokens[preludeStart:p.index])
|
||
|
||
switch kind {
|
||
case atRuleQualifiedOrEmpty:
|
||
// Parse a known at rule below
|
||
break prelude
|
||
|
||
case atRuleEmpty, atRuleUnknown:
|
||
// Parse an unknown at rule
|
||
p.expect(css_lexer.TSemicolon)
|
||
return css_ast.Rule{Loc: atRange.Loc, Data: &css_ast.RUnknownAt{AtToken: atToken, Prelude: prelude}}
|
||
|
||
default:
|
||
// Report an error for rules that should have blocks
|
||
p.expect(css_lexer.TOpenBrace)
|
||
p.eat(css_lexer.TSemicolon)
|
||
return css_ast.Rule{Loc: atRange.Loc, Data: &css_ast.RUnknownAt{AtToken: atToken, Prelude: prelude}}
|
||
}
|
||
|
||
default:
|
||
p.parseComponentValue()
|
||
}
|
||
}
|
||
prelude := p.convertTokens(p.tokens[preludeStart:p.index])
|
||
blockStart := p.index
|
||
|
||
switch kind {
|
||
case atRuleEmpty:
|
||
// Report an error for rules that shouldn't have blocks
|
||
p.expect(css_lexer.TSemicolon)
|
||
p.parseBlock(css_lexer.TOpenBrace, css_lexer.TCloseBrace)
|
||
block := p.convertTokens(p.tokens[blockStart:p.index])
|
||
return css_ast.Rule{Loc: atRange.Loc, Data: &css_ast.RUnknownAt{AtToken: atToken, Prelude: prelude, Block: block}}
|
||
|
||
case atRuleDeclarations:
|
||
// Parse known rules whose blocks always consist of declarations
|
||
matchingLoc := p.current().Range.Loc
|
||
p.expect(css_lexer.TOpenBrace)
|
||
rules := p.parseListOfDeclarations(listOfDeclarationsOpts{})
|
||
closeBraceLoc := p.current().Range.Loc
|
||
if !p.expectWithMatchingLoc(css_lexer.TCloseBrace, matchingLoc) {
|
||
closeBraceLoc = logger.Loc{}
|
||
}
|
||
|
||
// Handle local names for "@counter-style"
|
||
if len(prelude) == 1 && lowerAtToken == "counter-style" {
|
||
if t := &prelude[0]; t.Kind == css_lexer.TIdent {
|
||
t.Kind = css_lexer.TSymbol
|
||
t.PayloadIndex = p.symbolForName(t.Loc, t.Text).Ref.InnerIndex
|
||
}
|
||
}
|
||
|
||
return css_ast.Rule{Loc: atRange.Loc, Data: &css_ast.RKnownAt{AtToken: atToken, Prelude: prelude, Rules: rules, CloseBraceLoc: closeBraceLoc}}
|
||
|
||
case atRuleInheritContext:
|
||
// Parse known rules whose blocks consist of whatever the current context is
|
||
matchingLoc := p.current().Range.Loc
|
||
p.expect(css_lexer.TOpenBrace)
|
||
|
||
// Parse the block for this rule
|
||
var rules []css_ast.Rule
|
||
if context.isDeclarationList {
|
||
rules = p.parseListOfDeclarations(listOfDeclarationsOpts{
|
||
canInlineNoOpNesting: context.canInlineNoOpNesting,
|
||
})
|
||
} else {
|
||
rules = p.parseListOfRules(ruleContext{
|
||
parseSelectors: true,
|
||
})
|
||
}
|
||
|
||
closeBraceLoc := p.current().Range.Loc
|
||
if !p.expectWithMatchingLoc(css_lexer.TCloseBrace, matchingLoc) {
|
||
closeBraceLoc = logger.Loc{}
|
||
}
|
||
|
||
// Handle local names for "@container"
|
||
if len(prelude) >= 1 && lowerAtToken == "container" {
|
||
if t := &prelude[0]; t.Kind == css_lexer.TIdent && !strings.EqualFold(t.Text, "not") {
|
||
t.Kind = css_lexer.TSymbol
|
||
t.PayloadIndex = p.symbolForName(t.Loc, t.Text).Ref.InnerIndex
|
||
}
|
||
}
|
||
|
||
return css_ast.Rule{Loc: atRange.Loc, Data: &css_ast.RKnownAt{AtToken: atToken, Prelude: prelude, Rules: rules, CloseBraceLoc: closeBraceLoc}}
|
||
|
||
case atRuleQualifiedOrEmpty:
|
||
matchingLoc := p.current().Range.Loc
|
||
if p.eat(css_lexer.TOpenBrace) {
|
||
rules := p.parseListOfRules(ruleContext{
|
||
parseSelectors: true,
|
||
})
|
||
closeBraceLoc := p.current().Range.Loc
|
||
if !p.expectWithMatchingLoc(css_lexer.TCloseBrace, matchingLoc) {
|
||
closeBraceLoc = logger.Loc{}
|
||
}
|
||
return css_ast.Rule{Loc: atRange.Loc, Data: &css_ast.RKnownAt{AtToken: atToken, Prelude: prelude, Rules: rules, CloseBraceLoc: closeBraceLoc}}
|
||
}
|
||
p.expect(css_lexer.TSemicolon)
|
||
return css_ast.Rule{Loc: atRange.Loc, Data: &css_ast.RKnownAt{AtToken: atToken, Prelude: prelude}}
|
||
|
||
default:
|
||
// Otherwise, parse an unknown rule
|
||
p.parseBlock(css_lexer.TOpenBrace, css_lexer.TCloseBrace)
|
||
block, _ := p.convertTokensHelper(p.tokens[blockStart:p.index], css_lexer.TEndOfFile, convertTokensOpts{allowImports: true})
|
||
return css_ast.Rule{Loc: atRange.Loc, Data: &css_ast.RUnknownAt{AtToken: atToken, Prelude: prelude, Block: block}}
|
||
}
|
||
}
|
||
|
||
func (p *parser) expectValidLayerNameIdent() (string, bool) {
|
||
r := p.current().Range
|
||
text := p.decoded()
|
||
if !p.expect(css_lexer.TIdent) {
|
||
return "", false
|
||
}
|
||
switch text {
|
||
case "initial", "inherit", "unset":
|
||
p.log.AddID(logger.MsgID_CSS_InvalidAtLayer, logger.Warning, &p.tracker, r, fmt.Sprintf("%q cannot be used as a layer name", text))
|
||
p.prevError = r.Loc
|
||
return "", false
|
||
}
|
||
return text, true
|
||
}
|
||
|
||
func (p *parser) convertTokens(tokens []css_lexer.Token) []css_ast.Token {
|
||
result, _ := p.convertTokensHelper(tokens, css_lexer.TEndOfFile, convertTokensOpts{})
|
||
return result
|
||
}
|
||
|
||
type convertTokensOpts struct {
|
||
allowImports bool
|
||
verbatimWhitespace bool
|
||
isInsideCalcFunction bool
|
||
}
|
||
|
||
func (p *parser) convertTokensHelper(tokens []css_lexer.Token, close css_lexer.T, opts convertTokensOpts) ([]css_ast.Token, []css_lexer.Token) {
|
||
result := []css_ast.Token{}
|
||
var nextWhitespace css_ast.WhitespaceFlags
|
||
|
||
// Enable verbatim whitespace mode when the first two non-whitespace tokens
|
||
// are a CSS variable name followed by a colon. This is because it could be
|
||
// a form of CSS variable usage, and removing whitespace could potentially
|
||
// break this usage. For example, the following CSS is ignored by Chrome if
|
||
// the whitespace isn't preserved:
|
||
//
|
||
// @supports (--foo: ) {
|
||
// html { background: green; }
|
||
// }
|
||
//
|
||
// Strangely whitespace removal doesn't cause the declaration to be ignored
|
||
// in Firefox or Safari, so there's definitely a browser bug somewhere.
|
||
if !opts.verbatimWhitespace {
|
||
for i, t := range tokens {
|
||
if t.Kind == css_lexer.TWhitespace {
|
||
continue
|
||
}
|
||
if t.Kind == css_lexer.TIdent && strings.HasPrefix(t.DecodedText(p.source.Contents), "--") {
|
||
for _, t := range tokens[i+1:] {
|
||
if t.Kind == css_lexer.TWhitespace {
|
||
continue
|
||
}
|
||
if t.Kind == css_lexer.TColon {
|
||
opts.verbatimWhitespace = true
|
||
}
|
||
break
|
||
}
|
||
}
|
||
break
|
||
}
|
||
}
|
||
|
||
loop:
|
||
for len(tokens) > 0 {
|
||
t := tokens[0]
|
||
tokens = tokens[1:]
|
||
if t.Kind == close {
|
||
break loop
|
||
}
|
||
token := css_ast.Token{
|
||
Loc: t.Range.Loc,
|
||
Kind: t.Kind,
|
||
Text: t.DecodedText(p.source.Contents),
|
||
Whitespace: nextWhitespace,
|
||
}
|
||
nextWhitespace = 0
|
||
|
||
// Warn about invalid "+" and "-" operators that break the containing "calc()"
|
||
if opts.isInsideCalcFunction && t.Kind.IsNumeric() && len(result) > 0 && result[len(result)-1].Kind.IsNumeric() &&
|
||
(strings.HasPrefix(token.Text, "+") || strings.HasPrefix(token.Text, "-")) {
|
||
// "calc(1+2)" and "calc(1-2)" are invalid
|
||
p.log.AddID(logger.MsgID_CSS_InvalidCalc, logger.Warning, &p.tracker, logger.Range{Loc: t.Range.Loc, Len: 1},
|
||
fmt.Sprintf("The %q operator only works if there is whitespace on both sides", token.Text[:1]))
|
||
}
|
||
|
||
switch t.Kind {
|
||
case css_lexer.TWhitespace:
|
||
if last := len(result) - 1; last >= 0 {
|
||
result[last].Whitespace |= css_ast.WhitespaceAfter
|
||
}
|
||
nextWhitespace = css_ast.WhitespaceBefore
|
||
continue
|
||
|
||
case css_lexer.TDelimPlus, css_lexer.TDelimMinus:
|
||
// Warn about invalid "+" and "-" operators that break the containing "calc()"
|
||
if opts.isInsideCalcFunction && len(tokens) > 0 {
|
||
if len(result) == 0 || result[len(result)-1].Kind == css_lexer.TComma {
|
||
// "calc(-(1 + 2))" is invalid
|
||
p.log.AddID(logger.MsgID_CSS_InvalidCalc, logger.Warning, &p.tracker, t.Range,
|
||
fmt.Sprintf("%q can only be used as an infix operator, not a prefix operator", token.Text))
|
||
} else if token.Whitespace != css_ast.WhitespaceBefore || tokens[0].Kind != css_lexer.TWhitespace {
|
||
// "calc(1- 2)" and "calc(1 -(2))" are invalid
|
||
p.log.AddID(logger.MsgID_CSS_InvalidCalc, logger.Warning, &p.tracker, t.Range,
|
||
fmt.Sprintf("The %q operator only works if there is whitespace on both sides", token.Text))
|
||
}
|
||
}
|
||
|
||
case css_lexer.TNumber:
|
||
if p.options.minifySyntax {
|
||
if text, ok := mangleNumber(token.Text); ok {
|
||
token.Text = text
|
||
}
|
||
}
|
||
|
||
case css_lexer.TPercentage:
|
||
if p.options.minifySyntax {
|
||
if text, ok := mangleNumber(token.PercentageValue()); ok {
|
||
token.Text = text + "%"
|
||
}
|
||
}
|
||
|
||
case css_lexer.TDimension:
|
||
token.UnitOffset = t.UnitOffset
|
||
|
||
if p.options.minifySyntax {
|
||
if text, ok := mangleNumber(token.DimensionValue()); ok {
|
||
token.Text = text + token.DimensionUnit()
|
||
token.UnitOffset = uint16(len(text))
|
||
}
|
||
|
||
if value, unit, ok := mangleDimension(token.DimensionValue(), token.DimensionUnit()); ok {
|
||
token.Text = value + unit
|
||
token.UnitOffset = uint16(len(value))
|
||
}
|
||
}
|
||
|
||
case css_lexer.TURL:
|
||
token.PayloadIndex = uint32(len(p.importRecords))
|
||
var flags ast.ImportRecordFlags
|
||
if !opts.allowImports {
|
||
flags |= ast.IsUnused
|
||
}
|
||
p.importRecords = append(p.importRecords, ast.ImportRecord{
|
||
Kind: ast.ImportURL,
|
||
Path: logger.Path{Text: token.Text},
|
||
Range: t.Range,
|
||
Flags: flags,
|
||
})
|
||
token.Text = ""
|
||
|
||
case css_lexer.TFunction:
|
||
var nested []css_ast.Token
|
||
original := tokens
|
||
nestedOpts := opts
|
||
if strings.EqualFold(token.Text, "var") {
|
||
// CSS variables require verbatim whitespace for correctness
|
||
nestedOpts.verbatimWhitespace = true
|
||
}
|
||
if strings.EqualFold(token.Text, "calc") {
|
||
nestedOpts.isInsideCalcFunction = true
|
||
}
|
||
nested, tokens = p.convertTokensHelper(tokens, css_lexer.TCloseParen, nestedOpts)
|
||
token.Children = &nested
|
||
|
||
// Apply "calc" simplification rules when minifying
|
||
if p.options.minifySyntax && strings.EqualFold(token.Text, "calc") {
|
||
token = p.tryToReduceCalcExpression(token)
|
||
}
|
||
|
||
// Treat a URL function call with a string just like a URL token
|
||
if strings.EqualFold(token.Text, "url") && len(nested) == 1 && nested[0].Kind == css_lexer.TString {
|
||
token.Kind = css_lexer.TURL
|
||
token.Text = ""
|
||
token.Children = nil
|
||
token.PayloadIndex = uint32(len(p.importRecords))
|
||
var flags ast.ImportRecordFlags
|
||
if !opts.allowImports {
|
||
flags |= ast.IsUnused
|
||
}
|
||
p.importRecords = append(p.importRecords, ast.ImportRecord{
|
||
Kind: ast.ImportURL,
|
||
Path: logger.Path{Text: nested[0].Text},
|
||
Range: original[0].Range,
|
||
Flags: flags,
|
||
})
|
||
}
|
||
|
||
case css_lexer.TOpenParen:
|
||
var nested []css_ast.Token
|
||
nested, tokens = p.convertTokensHelper(tokens, css_lexer.TCloseParen, opts)
|
||
token.Children = &nested
|
||
|
||
case css_lexer.TOpenBrace:
|
||
var nested []css_ast.Token
|
||
nested, tokens = p.convertTokensHelper(tokens, css_lexer.TCloseBrace, opts)
|
||
|
||
// Pretty-printing: insert leading and trailing whitespace when not minifying
|
||
if !opts.verbatimWhitespace && !p.options.minifyWhitespace && len(nested) > 0 {
|
||
nested[0].Whitespace |= css_ast.WhitespaceBefore
|
||
nested[len(nested)-1].Whitespace |= css_ast.WhitespaceAfter
|
||
}
|
||
|
||
token.Children = &nested
|
||
|
||
case css_lexer.TOpenBracket:
|
||
var nested []css_ast.Token
|
||
nested, tokens = p.convertTokensHelper(tokens, css_lexer.TCloseBracket, opts)
|
||
token.Children = &nested
|
||
}
|
||
|
||
result = append(result, token)
|
||
}
|
||
|
||
if !opts.verbatimWhitespace {
|
||
for i := range result {
|
||
token := &result[i]
|
||
|
||
// Always remove leading and trailing whitespace
|
||
if i == 0 {
|
||
token.Whitespace &= ^css_ast.WhitespaceBefore
|
||
}
|
||
if i+1 == len(result) {
|
||
token.Whitespace &= ^css_ast.WhitespaceAfter
|
||
}
|
||
|
||
switch token.Kind {
|
||
case css_lexer.TComma:
|
||
// Assume that whitespace can always be removed before a comma
|
||
token.Whitespace &= ^css_ast.WhitespaceBefore
|
||
if i > 0 {
|
||
result[i-1].Whitespace &= ^css_ast.WhitespaceAfter
|
||
}
|
||
|
||
// Assume whitespace can always be added after a comma
|
||
if p.options.minifyWhitespace {
|
||
token.Whitespace &= ^css_ast.WhitespaceAfter
|
||
if i+1 < len(result) {
|
||
result[i+1].Whitespace &= ^css_ast.WhitespaceBefore
|
||
}
|
||
} else {
|
||
token.Whitespace |= css_ast.WhitespaceAfter
|
||
if i+1 < len(result) {
|
||
result[i+1].Whitespace |= css_ast.WhitespaceBefore
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// Insert an explicit whitespace token if we're in verbatim mode and all
|
||
// tokens were whitespace. In this case there is no token to attach the
|
||
// whitespace before/after flags so this is the only way to represent this.
|
||
// This is the only case where this function generates an explicit whitespace
|
||
// token. It represents whitespace as flags in all other cases.
|
||
if opts.verbatimWhitespace && len(result) == 0 && nextWhitespace == css_ast.WhitespaceBefore {
|
||
result = append(result, css_ast.Token{
|
||
Kind: css_lexer.TWhitespace,
|
||
})
|
||
}
|
||
|
||
return result, tokens
|
||
}
|
||
|
||
func shiftDot(text string, dotOffset int) (string, bool) {
|
||
// This doesn't handle numbers with exponents
|
||
if strings.ContainsAny(text, "eE") {
|
||
return "", false
|
||
}
|
||
|
||
// Handle a leading sign
|
||
sign := ""
|
||
if len(text) > 0 && (text[0] == '-' || text[0] == '+') {
|
||
sign = text[:1]
|
||
text = text[1:]
|
||
}
|
||
|
||
// Remove the dot
|
||
dot := strings.IndexByte(text, '.')
|
||
if dot == -1 {
|
||
dot = len(text)
|
||
} else {
|
||
text = text[:dot] + text[dot+1:]
|
||
}
|
||
|
||
// Move the dot
|
||
dot += dotOffset
|
||
|
||
// Remove any leading zeros before the dot
|
||
for len(text) > 0 && dot > 0 && text[0] == '0' {
|
||
text = text[1:]
|
||
dot--
|
||
}
|
||
|
||
// Remove any trailing zeros after the dot
|
||
for len(text) > 0 && len(text) > dot && text[len(text)-1] == '0' {
|
||
text = text[:len(text)-1]
|
||
}
|
||
|
||
// Does this number have no fractional component?
|
||
if dot >= len(text) {
|
||
trailingZeros := strings.Repeat("0", dot-len(text))
|
||
return fmt.Sprintf("%s%s%s", sign, text, trailingZeros), true
|
||
}
|
||
|
||
// Potentially add leading zeros
|
||
if dot < 0 {
|
||
text = strings.Repeat("0", -dot) + text
|
||
dot = 0
|
||
}
|
||
|
||
// Insert the dot again
|
||
return fmt.Sprintf("%s%s.%s", sign, text[:dot], text[dot:]), true
|
||
}
|
||
|
||
func mangleDimension(value string, unit string) (string, string, bool) {
|
||
const msLen = 2
|
||
const sLen = 1
|
||
|
||
// Mangle times: https://developer.mozilla.org/en-US/docs/Web/CSS/time
|
||
if strings.EqualFold(unit, "ms") {
|
||
if shifted, ok := shiftDot(value, -3); ok && len(shifted)+sLen < len(value)+msLen {
|
||
// Convert "ms" to "s" if shorter
|
||
return shifted, "s", true
|
||
}
|
||
}
|
||
if strings.EqualFold(unit, "s") {
|
||
if shifted, ok := shiftDot(value, 3); ok && len(shifted)+msLen < len(value)+sLen {
|
||
// Convert "s" to "ms" if shorter
|
||
return shifted, "ms", true
|
||
}
|
||
}
|
||
|
||
return "", "", false
|
||
}
|
||
|
||
func mangleNumber(t string) (string, bool) {
|
||
original := t
|
||
|
||
if dot := strings.IndexByte(t, '.'); dot != -1 {
|
||
// Remove trailing zeros
|
||
for len(t) > 0 && t[len(t)-1] == '0' {
|
||
t = t[:len(t)-1]
|
||
}
|
||
|
||
// Remove the decimal point if it's unnecessary
|
||
if dot+1 == len(t) {
|
||
t = t[:dot]
|
||
if t == "" || t == "+" || t == "-" {
|
||
t += "0"
|
||
}
|
||
} else {
|
||
// Remove a leading zero
|
||
if len(t) >= 3 && t[0] == '0' && t[1] == '.' && t[2] >= '0' && t[2] <= '9' {
|
||
t = t[1:]
|
||
} else if len(t) >= 4 && (t[0] == '+' || t[0] == '-') && t[1] == '0' && t[2] == '.' && t[3] >= '0' && t[3] <= '9' {
|
||
t = t[0:1] + t[2:]
|
||
}
|
||
}
|
||
}
|
||
|
||
return t, t != original
|
||
}
|
||
|
||
func (p *parser) parseSelectorRule(isTopLevel bool, opts parseSelectorOpts) css_ast.Rule {
|
||
// Save and restore the local symbol state in case there are any bare
|
||
// ":global" or ":local" annotations. The effect of these should be scoped
|
||
// to within the selector rule.
|
||
local := p.makeLocalSymbols
|
||
preludeStart := p.index
|
||
|
||
// Try parsing the prelude as a selector list
|
||
if list, ok := p.parseSelectorList(opts); ok {
|
||
canInlineNoOpNesting := true
|
||
for _, sel := range list {
|
||
// We cannot transform the CSS "a, b::before { & { color: red } }" into
|
||
// "a, b::before { color: red }" because it's basically equivalent to
|
||
// ":is(a, b::before) { color: red }" which only applies to "a", not to
|
||
// "b::before" because pseudo-elements are not valid within :is():
|
||
// https://www.w3.org/TR/selectors-4/#matches-pseudo. This restriction
|
||
// may be relaxed in the future, but this restriction hash shipped so
|
||
// we're stuck with it: https://github.com/w3c/csswg-drafts/issues/7433.
|
||
if sel.UsesPseudoElement() {
|
||
canInlineNoOpNesting = false
|
||
break
|
||
}
|
||
}
|
||
selector := css_ast.RSelector{Selectors: list}
|
||
matchingLoc := p.current().Range.Loc
|
||
if p.expect(css_lexer.TOpenBrace) {
|
||
p.inSelectorSubtree++
|
||
declOpts := listOfDeclarationsOpts{
|
||
canInlineNoOpNesting: canInlineNoOpNesting,
|
||
}
|
||
|
||
// Prepare for "composes" declarations
|
||
if opts.composesContext != nil && len(list) == 1 && len(list[0].Selectors) == 1 && list[0].Selectors[0].IsSingleAmpersand() {
|
||
// Support code like this:
|
||
//
|
||
// .foo {
|
||
// :local { composes: bar }
|
||
// :global { composes: baz }
|
||
// }
|
||
//
|
||
declOpts.composesContext = opts.composesContext
|
||
} else {
|
||
composesContext := composesContext{parentRange: list[0].Selectors[0].Range()}
|
||
if opts.composesContext != nil {
|
||
composesContext.problemRange = opts.composesContext.parentRange
|
||
}
|
||
for _, sel := range list {
|
||
first := sel.Selectors[0]
|
||
if first.Combinator.Byte != 0 {
|
||
composesContext.problemRange = logger.Range{Loc: first.Combinator.Loc, Len: 1}
|
||
} else if first.TypeSelector != nil {
|
||
composesContext.problemRange = first.TypeSelector.Range()
|
||
} else if len(first.NestingSelectorLocs) > 0 {
|
||
composesContext.problemRange = logger.Range{Loc: first.NestingSelectorLocs[0], Len: 1}
|
||
} else {
|
||
for i, ss := range first.SubclassSelectors {
|
||
class, ok := ss.Data.(*css_ast.SSClass)
|
||
if i > 0 || !ok {
|
||
composesContext.problemRange = ss.Range
|
||
} else {
|
||
composesContext.parentRefs = append(composesContext.parentRefs, class.Name.Ref)
|
||
}
|
||
}
|
||
}
|
||
if composesContext.problemRange.Len > 0 {
|
||
break
|
||
}
|
||
if len(sel.Selectors) > 1 {
|
||
composesContext.problemRange = sel.Selectors[1].Range()
|
||
break
|
||
}
|
||
}
|
||
declOpts.composesContext = &composesContext
|
||
}
|
||
|
||
selector.Rules = p.parseListOfDeclarations(declOpts)
|
||
p.inSelectorSubtree--
|
||
closeBraceLoc := p.current().Range.Loc
|
||
if p.expectWithMatchingLoc(css_lexer.TCloseBrace, matchingLoc) {
|
||
selector.CloseBraceLoc = closeBraceLoc
|
||
}
|
||
p.makeLocalSymbols = local
|
||
return css_ast.Rule{Loc: p.tokens[preludeStart].Range.Loc, Data: &selector}
|
||
}
|
||
}
|
||
|
||
p.makeLocalSymbols = local
|
||
p.index = preludeStart
|
||
|
||
// Otherwise, parse a generic qualified rule
|
||
return p.parseQualifiedRule(parseQualifiedRuleOpts{
|
||
isAlreadyInvalid: true,
|
||
isTopLevel: isTopLevel,
|
||
isDeclarationContext: opts.isDeclarationContext,
|
||
})
|
||
}
|
||
|
||
type parseQualifiedRuleOpts struct {
|
||
isAlreadyInvalid bool
|
||
isTopLevel bool
|
||
isDeclarationContext bool
|
||
}
|
||
|
||
func (p *parser) parseQualifiedRule(opts parseQualifiedRuleOpts) css_ast.Rule {
|
||
preludeStart := p.index
|
||
preludeLoc := p.current().Range.Loc
|
||
|
||
loop:
|
||
for {
|
||
switch p.current().Kind {
|
||
case css_lexer.TOpenBrace, css_lexer.TEndOfFile:
|
||
break loop
|
||
|
||
case css_lexer.TCloseBrace:
|
||
if !opts.isTopLevel {
|
||
break loop
|
||
}
|
||
|
||
case css_lexer.TSemicolon:
|
||
if opts.isDeclarationContext {
|
||
return css_ast.Rule{Loc: preludeLoc, Data: &css_ast.RBadDeclaration{
|
||
Tokens: p.convertTokens(p.tokens[preludeStart:p.index]),
|
||
}}
|
||
}
|
||
}
|
||
|
||
p.parseComponentValue()
|
||
}
|
||
|
||
qualified := css_ast.RQualified{
|
||
Prelude: p.convertTokens(p.tokens[preludeStart:p.index]),
|
||
}
|
||
|
||
matchingLoc := p.current().Range.Loc
|
||
if p.eat(css_lexer.TOpenBrace) {
|
||
qualified.Rules = p.parseListOfDeclarations(listOfDeclarationsOpts{})
|
||
closeBraceLoc := p.current().Range.Loc
|
||
if p.expectWithMatchingLoc(css_lexer.TCloseBrace, matchingLoc) {
|
||
qualified.CloseBraceLoc = closeBraceLoc
|
||
}
|
||
} else if !opts.isAlreadyInvalid {
|
||
p.expect(css_lexer.TOpenBrace)
|
||
}
|
||
|
||
return css_ast.Rule{Loc: preludeLoc, Data: &qualified}
|
||
}
|
||
|
||
type endOfRuleScan uint8
|
||
|
||
const (
|
||
endOfRuleUnknown endOfRuleScan = iota
|
||
endOfRuleSemicolon
|
||
endOfRuleOpenBrace
|
||
)
|
||
|
||
// Note: This was a late change to the CSS nesting syntax.
|
||
// See also: https://github.com/w3c/csswg-drafts/issues/7961
|
||
func (p *parser) scanForEndOfRule() (endOfRuleScan, int) {
|
||
var initialStack [4]css_lexer.T
|
||
stack := initialStack[:0]
|
||
|
||
// Small state machine to recognize "--foo:"
|
||
const (
|
||
customPropIdent = iota
|
||
customPropColon
|
||
customPropAccept
|
||
customPropReject
|
||
)
|
||
customProp := customPropIdent
|
||
|
||
for i, t := range p.tokens[p.index:] {
|
||
// People sometimes use CSS custom properties to store JSON. This is
|
||
// problematic because the "{...}" braces look like a CSS block unless
|
||
// this edge case is special-cased. See the (quite long) discussion on
|
||
// https://github.com/w3c/csswg-drafts/issues/7961 for more information.
|
||
//
|
||
// I couldn't find any actual specification for how this is supposed to
|
||
// work. Just a lot of discussion on GitHub. I did find this text, from
|
||
// https://drafts.csswg.org/css-syntax/#consume-a-declaration:
|
||
//
|
||
// Otherwise, if decl’s value contains a top-level simple block with
|
||
// an associated token of <{-token>, and also contains any other non-
|
||
// <whitespace-token> value, return nothing. (That is, a top-level {}-
|
||
// block is only allowed as the entire value of a non-custom property.)
|
||
//
|
||
// However, current browsers appear to accept custom properties with a
|
||
// {}-block contained anywhere inside them, not just as the entire value.
|
||
// So that's what I'm attempting to do here.
|
||
if customProp < customPropAccept {
|
||
switch customProp {
|
||
case customPropIdent:
|
||
if t.Kind == css_lexer.TIdent && strings.HasPrefix(p.source.Contents[t.Range.Loc.Start:], "--") {
|
||
customProp = customPropColon
|
||
} else if t.Kind != css_lexer.TWhitespace {
|
||
customProp = customPropReject
|
||
}
|
||
case customPropColon:
|
||
if t.Kind == css_lexer.TColon {
|
||
customProp = customPropAccept
|
||
} else if t.Kind != css_lexer.TWhitespace {
|
||
customProp = customPropReject
|
||
}
|
||
}
|
||
}
|
||
|
||
switch t.Kind {
|
||
case css_lexer.TSemicolon:
|
||
if len(stack) == 0 {
|
||
return endOfRuleSemicolon, p.index + i
|
||
}
|
||
|
||
case css_lexer.TFunction, css_lexer.TOpenParen:
|
||
stack = append(stack, css_lexer.TCloseParen)
|
||
|
||
case css_lexer.TOpenBracket:
|
||
stack = append(stack, css_lexer.TCloseBracket)
|
||
|
||
case css_lexer.TOpenBrace:
|
||
if len(stack) == 0 && customProp != customPropAccept {
|
||
return endOfRuleOpenBrace, p.index + i
|
||
}
|
||
stack = append(stack, css_lexer.TCloseBrace)
|
||
|
||
case css_lexer.TCloseParen, css_lexer.TCloseBracket:
|
||
if n := len(stack); n > 0 && t.Kind == stack[n-1] {
|
||
stack = stack[:n-1]
|
||
}
|
||
|
||
case css_lexer.TCloseBrace:
|
||
if n := len(stack); n > 0 && t.Kind == stack[n-1] {
|
||
stack = stack[:n-1]
|
||
} else {
|
||
return endOfRuleUnknown, -1
|
||
}
|
||
}
|
||
}
|
||
|
||
return endOfRuleUnknown, -1
|
||
}
|
||
|
||
func (p *parser) parseDeclaration() css_ast.Rule {
|
||
// Parse the key
|
||
keyStart := p.index
|
||
keyRange := p.tokens[keyStart].Range
|
||
keyIsIdent := p.expect(css_lexer.TIdent)
|
||
ok := false
|
||
if keyIsIdent {
|
||
p.eat(css_lexer.TWhitespace)
|
||
ok = p.eat(css_lexer.TColon)
|
||
}
|
||
|
||
// Parse the value
|
||
valueStart := p.index
|
||
stop:
|
||
for {
|
||
switch p.current().Kind {
|
||
case css_lexer.TEndOfFile, css_lexer.TSemicolon, css_lexer.TCloseBrace:
|
||
break stop
|
||
|
||
default:
|
||
p.parseComponentValue()
|
||
}
|
||
}
|
||
|
||
// Stop now if this is not a valid declaration
|
||
if !ok {
|
||
if keyIsIdent {
|
||
if end := keyRange.End(); end > p.prevError.Start {
|
||
p.prevError.Start = end
|
||
data := p.tracker.MsgData(logger.Range{Loc: logger.Loc{Start: end}}, "Expected \":\"")
|
||
data.Location.Suggestion = ":"
|
||
p.log.AddMsgID(logger.MsgID_CSS_CSSSyntaxError, logger.Msg{
|
||
Kind: logger.Warning,
|
||
Data: data,
|
||
})
|
||
}
|
||
}
|
||
|
||
return css_ast.Rule{Loc: keyRange.Loc, Data: &css_ast.RBadDeclaration{
|
||
Tokens: p.convertTokens(p.tokens[keyStart:p.index]),
|
||
}}
|
||
}
|
||
|
||
keyToken := p.tokens[keyStart]
|
||
keyText := keyToken.DecodedText(p.source.Contents)
|
||
value := p.tokens[valueStart:p.index]
|
||
verbatimWhitespace := strings.HasPrefix(keyText, "--")
|
||
|
||
// Remove trailing "!important"
|
||
important := false
|
||
i := len(value) - 1
|
||
if i >= 0 && value[i].Kind == css_lexer.TWhitespace {
|
||
i--
|
||
}
|
||
if i >= 0 && value[i].Kind == css_lexer.TIdent && strings.EqualFold(value[i].DecodedText(p.source.Contents), "important") {
|
||
i--
|
||
if i >= 0 && value[i].Kind == css_lexer.TWhitespace {
|
||
i--
|
||
}
|
||
if i >= 0 && value[i].Kind == css_lexer.TDelimExclamation {
|
||
value = value[:i]
|
||
important = true
|
||
}
|
||
}
|
||
|
||
result, _ := p.convertTokensHelper(value, css_lexer.TEndOfFile, convertTokensOpts{
|
||
allowImports: true,
|
||
|
||
// CSS variables require verbatim whitespace for correctness
|
||
verbatimWhitespace: verbatimWhitespace,
|
||
})
|
||
|
||
// Insert or remove whitespace before the first token
|
||
if !verbatimWhitespace && len(result) > 0 {
|
||
if p.options.minifyWhitespace {
|
||
result[0].Whitespace &= ^css_ast.WhitespaceBefore
|
||
} else {
|
||
result[0].Whitespace |= css_ast.WhitespaceBefore
|
||
}
|
||
}
|
||
|
||
lowerKeyText := strings.ToLower(keyText)
|
||
key := css_ast.KnownDeclarations[lowerKeyText]
|
||
|
||
// Attempt to point out trivial typos
|
||
if key == css_ast.DUnknown {
|
||
if corrected, ok := css_ast.MaybeCorrectDeclarationTypo(lowerKeyText); ok {
|
||
data := p.tracker.MsgData(keyToken.Range, fmt.Sprintf("%q is not a known CSS property", keyText))
|
||
data.Location.Suggestion = corrected
|
||
p.log.AddMsgID(logger.MsgID_CSS_UnsupportedCSSProperty, logger.Msg{Kind: logger.Warning, Data: data,
|
||
Notes: []logger.MsgData{{Text: fmt.Sprintf("Did you mean %q instead?", corrected)}}})
|
||
}
|
||
}
|
||
|
||
return css_ast.Rule{Loc: keyRange.Loc, Data: &css_ast.RDeclaration{
|
||
Key: key,
|
||
KeyText: keyText,
|
||
KeyRange: keyToken.Range,
|
||
Value: result,
|
||
Important: important,
|
||
}}
|
||
}
|
||
|
||
func (p *parser) parseComponentValue() {
|
||
switch p.current().Kind {
|
||
case css_lexer.TFunction:
|
||
p.parseBlock(css_lexer.TFunction, css_lexer.TCloseParen)
|
||
|
||
case css_lexer.TOpenParen:
|
||
p.parseBlock(css_lexer.TOpenParen, css_lexer.TCloseParen)
|
||
|
||
case css_lexer.TOpenBrace:
|
||
p.parseBlock(css_lexer.TOpenBrace, css_lexer.TCloseBrace)
|
||
|
||
case css_lexer.TOpenBracket:
|
||
p.parseBlock(css_lexer.TOpenBracket, css_lexer.TCloseBracket)
|
||
|
||
case css_lexer.TEndOfFile:
|
||
p.unexpected()
|
||
|
||
default:
|
||
p.advance()
|
||
}
|
||
}
|
||
|
||
func (p *parser) parseBlock(open css_lexer.T, close css_lexer.T) {
|
||
current := p.current()
|
||
matchingStart := current.Range.End() - 1
|
||
if p.expect(open) {
|
||
for !p.eat(close) {
|
||
if p.peek(css_lexer.TEndOfFile) {
|
||
p.expectWithMatchingLoc(close, logger.Loc{Start: matchingStart})
|
||
return
|
||
}
|
||
|
||
p.parseComponentValue()
|
||
}
|
||
}
|
||
}
|