mirror of
https://github.com/zhigang1992/esbuild.git
synced 2026-01-12 22:46:54 +08:00
798 lines
20 KiB
Go
798 lines
20 KiB
Go
package css_parser
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/evanw/esbuild/internal/ast"
|
|
"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
|
|
options config.Options
|
|
tokens []css_lexer.Token
|
|
stack []css_lexer.T
|
|
index int
|
|
end int
|
|
prevError logger.Loc
|
|
importRecords []ast.ImportRecord
|
|
}
|
|
|
|
func Parse(log logger.Log, source logger.Source, options config.Options) css_ast.AST {
|
|
p := parser{
|
|
log: log,
|
|
source: source,
|
|
options: options,
|
|
tokens: css_lexer.Tokenize(log, source),
|
|
prevError: logger.Loc{Start: -1},
|
|
}
|
|
p.end = len(p.tokens)
|
|
tree := css_ast.AST{}
|
|
tree.Rules = p.parseListOfRules(ruleContext{
|
|
isTopLevel: true,
|
|
parseSelectors: true,
|
|
})
|
|
tree.ImportRecords = p.importRecords
|
|
p.expect(css_lexer.TEndOfFile)
|
|
return tree
|
|
}
|
|
|
|
func (p *parser) advance() {
|
|
if p.index < p.end {
|
|
p.index++
|
|
}
|
|
}
|
|
|
|
func (p *parser) at(index int) css_lexer.Token {
|
|
if index < p.end {
|
|
return p.tokens[index]
|
|
}
|
|
if p.end < len(p.tokens) {
|
|
return css_lexer.Token{
|
|
Kind: css_lexer.TEndOfFile,
|
|
Range: logger.Range{Loc: p.tokens[p.end].Range.Loc},
|
|
}
|
|
}
|
|
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 {
|
|
if p.eat(kind) {
|
|
return true
|
|
}
|
|
t := p.current()
|
|
var text string
|
|
if kind == css_lexer.TSemicolon && p.index > 0 && p.at(p.index-1).Kind == css_lexer.TWhitespace {
|
|
// Have a nice error message for forgetting a trailing semicolon
|
|
text = fmt.Sprintf("Expected \";\"")
|
|
t = p.at(p.index - 1)
|
|
} else {
|
|
switch t.Kind {
|
|
case css_lexer.TEndOfFile, css_lexer.TWhitespace:
|
|
text = fmt.Sprintf("Expected %s but found %s", kind.String(), t.Kind.String())
|
|
t.Range.Len = 0
|
|
case css_lexer.TBadURL, css_lexer.TBadString:
|
|
text = fmt.Sprintf("Expected %s but found %s", kind.String(), t.Kind.String())
|
|
default:
|
|
text = fmt.Sprintf("Expected %s but found %q", kind.String(), p.raw())
|
|
}
|
|
}
|
|
if t.Range.Loc.Start > p.prevError.Start {
|
|
p.log.AddRangeWarning(&p.source, t.Range, text)
|
|
p.prevError = t.Range.Loc
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (p *parser) unexpected() {
|
|
if t := p.current(); t.Range.Loc.Start > p.prevError.Start {
|
|
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.TBadString:
|
|
text = fmt.Sprintf("Unexpected %s", t.Kind.String())
|
|
default:
|
|
text = fmt.Sprintf("Unexpected %q", p.raw())
|
|
}
|
|
p.log.AddRangeWarning(&p.source, t.Range, text)
|
|
p.prevError = t.Range.Loc
|
|
}
|
|
}
|
|
|
|
type ruleContext struct {
|
|
isTopLevel bool
|
|
parseSelectors bool
|
|
}
|
|
|
|
func (p *parser) parseListOfRules(context ruleContext) []css_ast.R {
|
|
rules := []css_ast.R{}
|
|
for {
|
|
switch p.current().Kind {
|
|
case css_lexer.TEndOfFile, css_lexer.TCloseBrace:
|
|
return rules
|
|
|
|
case css_lexer.TWhitespace:
|
|
p.advance()
|
|
continue
|
|
|
|
case css_lexer.TAtKeyword:
|
|
rules = append(rules, p.parseAtRule(atRuleContext{}))
|
|
continue
|
|
|
|
case css_lexer.TCDO, css_lexer.TCDC:
|
|
if context.isTopLevel {
|
|
p.advance()
|
|
continue
|
|
}
|
|
}
|
|
|
|
if context.parseSelectors {
|
|
rules = append(rules, p.parseSelectorRule())
|
|
} else {
|
|
rules = append(rules, p.parseQualifiedRuleFrom(p.index, false /* isAlreadyInvalid */))
|
|
}
|
|
}
|
|
}
|
|
|
|
func (p *parser) parseListOfDeclarations() (list []css_ast.R) {
|
|
for {
|
|
switch p.current().Kind {
|
|
case css_lexer.TWhitespace, css_lexer.TSemicolon:
|
|
p.advance()
|
|
|
|
case css_lexer.TEndOfFile, css_lexer.TCloseBrace:
|
|
p.processDeclarations(list)
|
|
return
|
|
|
|
case css_lexer.TAtKeyword:
|
|
list = append(list, p.parseAtRule(atRuleContext{
|
|
isDeclarationList: true,
|
|
}))
|
|
|
|
case css_lexer.TDelimAmpersand:
|
|
// Reference: https://drafts.csswg.org/css-nesting-1/
|
|
list = append(list, p.parseSelectorRule())
|
|
|
|
default:
|
|
list = append(list, p.parseDeclaration())
|
|
}
|
|
}
|
|
}
|
|
|
|
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 p.decoded() == "url" {
|
|
p.advance()
|
|
t = p.current()
|
|
text := p.decoded()
|
|
if p.expect(css_lexer.TString) && p.expect(css_lexer.TCloseParen) {
|
|
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
|
|
atRuleEmpty
|
|
)
|
|
|
|
var specialAtRules = map[string]atRuleKind{
|
|
"font-face": atRuleDeclarations,
|
|
"page": atRuleDeclarations,
|
|
|
|
"document": atRuleInheritContext,
|
|
"media": atRuleInheritContext,
|
|
"scope": atRuleInheritContext,
|
|
"supports": atRuleInheritContext,
|
|
}
|
|
|
|
type atRuleContext struct {
|
|
isDeclarationList bool
|
|
}
|
|
|
|
func (p *parser) parseAtRule(context atRuleContext) css_ast.R {
|
|
// Parse the name
|
|
atToken := p.decoded()
|
|
atRange := p.current().Range
|
|
kind := specialAtRules[atToken]
|
|
p.advance()
|
|
|
|
// Parse the prelude
|
|
preludeStart := p.index
|
|
switch atToken {
|
|
case "charset":
|
|
kind = atRuleEmpty
|
|
p.expect(css_lexer.TWhitespace)
|
|
if p.peek(css_lexer.TString) {
|
|
encoding := p.decoded()
|
|
if encoding != "UTF-8" {
|
|
p.log.AddRangeWarning(&p.source, 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.RAtCharset{Encoding: encoding}
|
|
}
|
|
p.expect(css_lexer.TString)
|
|
|
|
case "namespace":
|
|
kind = atRuleEmpty
|
|
p.eat(css_lexer.TWhitespace)
|
|
prefix := ""
|
|
if p.peek(css_lexer.TIdent) {
|
|
prefix = p.decoded()
|
|
p.advance()
|
|
p.eat(css_lexer.TWhitespace)
|
|
}
|
|
if path, _, ok := p.expectURLOrString(); ok {
|
|
p.eat(css_lexer.TWhitespace)
|
|
p.expect(css_lexer.TSemicolon)
|
|
return &css_ast.RAtNamespace{Prefix: prefix, Path: path}
|
|
}
|
|
|
|
case "import":
|
|
kind = atRuleEmpty
|
|
p.eat(css_lexer.TWhitespace)
|
|
if path, r, ok := p.expectURLOrString(); ok {
|
|
p.eat(css_lexer.TWhitespace)
|
|
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,
|
|
})
|
|
return &css_ast.RAtImport{ImportRecordIndex: importRecordIndex}
|
|
}
|
|
|
|
case "keyframes", "-webkit-keyframes", "-moz-keyframes", "-ms-keyframes", "-o-keyframes":
|
|
p.eat(css_lexer.TWhitespace)
|
|
var name string
|
|
|
|
if p.peek(css_lexer.TIdent) {
|
|
name = p.decoded()
|
|
p.advance()
|
|
} else if !p.expect(css_lexer.TIdent) && !p.eat(css_lexer.TString) && !p.peek(css_lexer.TOpenBrace) {
|
|
// Consider string names a syntax error even though they are allowed by
|
|
// the specification and they work in Firefox because they do not work in
|
|
// Chrome or Safari.
|
|
break
|
|
}
|
|
|
|
p.eat(css_lexer.TWhitespace)
|
|
if p.expect(css_lexer.TOpenBrace) {
|
|
var blocks []css_ast.KeyframeBlock
|
|
|
|
blocks:
|
|
for {
|
|
switch p.current().Kind {
|
|
case css_lexer.TWhitespace:
|
|
p.advance()
|
|
continue
|
|
|
|
case css_lexer.TCloseBrace, css_lexer.TEndOfFile:
|
|
break blocks
|
|
|
|
case css_lexer.TOpenBrace:
|
|
p.expect(css_lexer.TPercentage)
|
|
p.parseComponentValue()
|
|
|
|
default:
|
|
var selectors []string
|
|
|
|
selectors:
|
|
for {
|
|
t := p.current()
|
|
switch t.Kind {
|
|
case css_lexer.TWhitespace:
|
|
p.advance()
|
|
continue
|
|
|
|
case css_lexer.TOpenBrace, css_lexer.TEndOfFile:
|
|
break selectors
|
|
|
|
case css_lexer.TIdent, css_lexer.TPercentage:
|
|
text := p.decoded()
|
|
if t.Kind == css_lexer.TIdent {
|
|
if text == "from" {
|
|
if p.options.MangleSyntax {
|
|
text = "0%" // "0%" is equivalent to but shorter than "from"
|
|
}
|
|
} else if text != "to" {
|
|
p.expect(css_lexer.TPercentage)
|
|
}
|
|
} else if p.options.MangleSyntax && text == "100%" {
|
|
text = "to" // "to" is equivalent to but shorter than "100%"
|
|
}
|
|
selectors = append(selectors, text)
|
|
p.advance()
|
|
|
|
default:
|
|
p.expect(css_lexer.TPercentage)
|
|
p.parseComponentValue()
|
|
}
|
|
|
|
p.eat(css_lexer.TWhitespace)
|
|
if t.Kind != css_lexer.TComma && !p.peek(css_lexer.TOpenBrace) {
|
|
p.expect(css_lexer.TComma)
|
|
}
|
|
}
|
|
|
|
if p.expect(css_lexer.TOpenBrace) {
|
|
rules := p.parseListOfDeclarations()
|
|
p.expect(css_lexer.TCloseBrace)
|
|
blocks = append(blocks, css_ast.KeyframeBlock{
|
|
Selectors: selectors,
|
|
Rules: rules,
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
p.expect(css_lexer.TCloseBrace)
|
|
return &css_ast.RAtKeyframes{
|
|
AtToken: atToken,
|
|
Name: name,
|
|
Blocks: blocks,
|
|
}
|
|
}
|
|
|
|
default:
|
|
// Warn about unsupported at-rules since they will be passed through
|
|
// unmodified and may be part of a CSS preprocessor syntax that should
|
|
// have been compiled away but wasn't.
|
|
//
|
|
// The list of supported at-rules that esbuild draws from is here:
|
|
// https://developer.mozilla.org/en-US/docs/Web/CSS/At-rule. Deprecated
|
|
// and Firefox-only at-rules have been removed.
|
|
if kind == atRuleUnknown {
|
|
p.log.AddRangeWarning(&p.source, atRange, fmt.Sprintf("%q is not a known rule name", "@"+atToken))
|
|
}
|
|
}
|
|
|
|
// Parse an unknown prelude
|
|
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])
|
|
|
|
// Report an error for rules that should have blocks
|
|
if kind != atRuleEmpty && kind != atRuleUnknown {
|
|
p.expect(css_lexer.TOpenBrace)
|
|
p.eat(css_lexer.TSemicolon)
|
|
return &css_ast.RUnknownAt{AtToken: atToken, Prelude: prelude}
|
|
}
|
|
|
|
// Otherwise, parse an unknown at rule
|
|
p.expect(css_lexer.TSemicolon)
|
|
return &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.RUnknownAt{AtToken: atToken, Prelude: prelude, Block: block}
|
|
|
|
case atRuleDeclarations:
|
|
// Parse known rules whose blocks consist of whatever the current context is
|
|
p.advance()
|
|
rules := p.parseListOfDeclarations()
|
|
p.expect(css_lexer.TCloseBrace)
|
|
return &css_ast.RKnownAt{AtToken: atToken, Prelude: prelude, Rules: rules}
|
|
|
|
case atRuleInheritContext:
|
|
// Parse known rules whose blocks consist of whatever the current context is
|
|
p.advance()
|
|
var rules []css_ast.R
|
|
if context.isDeclarationList {
|
|
rules = p.parseListOfDeclarations()
|
|
} else {
|
|
rules = p.parseListOfRules(ruleContext{
|
|
parseSelectors: true,
|
|
})
|
|
}
|
|
p.expect(css_lexer.TCloseBrace)
|
|
return &css_ast.RKnownAt{AtToken: atToken, Prelude: prelude, Rules: rules}
|
|
|
|
default:
|
|
// Otherwise, parse an unknown rule
|
|
p.parseBlock(css_lexer.TOpenBrace, css_lexer.TCloseBrace)
|
|
block := p.convertTokensWithImports(p.tokens[blockStart:p.index])
|
|
return &css_ast.RUnknownAt{AtToken: atToken, Prelude: prelude, Block: block}
|
|
}
|
|
}
|
|
|
|
func (p *parser) convertTokens(tokens []css_lexer.Token) []css_ast.Token {
|
|
result, _ := p.convertTokensHelper(tokens, css_lexer.TEndOfFile, false)
|
|
return result
|
|
}
|
|
|
|
func (p *parser) convertTokensWithImports(tokens []css_lexer.Token) []css_ast.Token {
|
|
result, _ := p.convertTokensHelper(tokens, css_lexer.TEndOfFile, true)
|
|
return result
|
|
}
|
|
|
|
func (p *parser) convertTokensHelper(tokens []css_lexer.Token, close css_lexer.T, allowImports bool) ([]css_ast.Token, []css_lexer.Token) {
|
|
var result []css_ast.Token
|
|
loop:
|
|
for len(tokens) > 0 {
|
|
t := tokens[0]
|
|
tokens = tokens[1:]
|
|
token := css_ast.Token{
|
|
Kind: t.Kind,
|
|
Text: t.DecodedText(p.source.Contents),
|
|
}
|
|
|
|
switch t.Kind {
|
|
case css_lexer.TWhitespace:
|
|
if last := len(result) - 1; last >= 0 {
|
|
result[last].HasWhitespaceAfter = true
|
|
}
|
|
continue
|
|
|
|
case css_lexer.TComma:
|
|
// Assume that whitespace can always be removed before a comma
|
|
if last := len(result) - 1; last >= 0 {
|
|
result[last].HasWhitespaceAfter = false
|
|
}
|
|
|
|
// Assume whitespace can always be added after a comma (it will be
|
|
// automatically omitted by the printer if we're minifying)
|
|
token.HasWhitespaceAfter = true
|
|
|
|
case css_lexer.TNumber:
|
|
if p.options.MangleSyntax {
|
|
if text, ok := mangleNumber(token.Text); ok {
|
|
token.Text = text
|
|
}
|
|
}
|
|
|
|
case css_lexer.TPercentage:
|
|
if p.options.MangleSyntax {
|
|
if text, ok := mangleNumber(token.PercentValue()); ok {
|
|
token.Text = text + "%"
|
|
}
|
|
}
|
|
|
|
case css_lexer.TDimension:
|
|
token.UnitOffset = t.UnitOffset
|
|
|
|
if p.options.MangleSyntax {
|
|
if text, ok := mangleNumber(token.DimensionValue()); ok {
|
|
token.Text = text + token.DimensionUnit()
|
|
token.UnitOffset = uint16(len(text))
|
|
}
|
|
}
|
|
|
|
case css_lexer.TURL:
|
|
token.ImportRecordIndex = uint32(len(p.importRecords))
|
|
p.importRecords = append(p.importRecords, ast.ImportRecord{
|
|
Kind: ast.ImportURL,
|
|
Path: logger.Path{Text: token.Text},
|
|
Range: t.Range,
|
|
IsUnused: !allowImports,
|
|
})
|
|
token.Text = ""
|
|
|
|
case css_lexer.TFunction:
|
|
var nested []css_ast.Token
|
|
original := tokens
|
|
nested, tokens = p.convertTokensHelper(tokens, css_lexer.TCloseParen, allowImports)
|
|
token.Children = &nested
|
|
|
|
// Treat a URL function call with a string just like a URL token
|
|
if token.Text == "url" && len(nested) == 1 && nested[0].Kind == css_lexer.TString {
|
|
token.Kind = css_lexer.TURL
|
|
token.Text = ""
|
|
token.Children = nil
|
|
token.ImportRecordIndex = uint32(len(p.importRecords))
|
|
p.importRecords = append(p.importRecords, ast.ImportRecord{
|
|
Kind: ast.ImportURL,
|
|
Path: logger.Path{Text: nested[0].Text},
|
|
Range: original[0].Range,
|
|
IsUnused: !allowImports,
|
|
})
|
|
}
|
|
|
|
case css_lexer.TOpenParen:
|
|
var nested []css_ast.Token
|
|
nested, tokens = p.convertTokensHelper(tokens, css_lexer.TCloseParen, allowImports)
|
|
token.Children = &nested
|
|
|
|
case css_lexer.TOpenBrace:
|
|
var nested []css_ast.Token
|
|
nested, tokens = p.convertTokensHelper(tokens, css_lexer.TCloseBrace, allowImports)
|
|
token.Children = &nested
|
|
|
|
case css_lexer.TOpenBracket:
|
|
var nested []css_ast.Token
|
|
nested, tokens = p.convertTokensHelper(tokens, css_lexer.TCloseBracket, allowImports)
|
|
token.Children = &nested
|
|
|
|
default:
|
|
if t.Kind == close {
|
|
break loop
|
|
}
|
|
}
|
|
|
|
result = append(result, token)
|
|
}
|
|
return result, tokens
|
|
}
|
|
|
|
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]
|
|
} 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() css_ast.R {
|
|
preludeStart := p.index
|
|
|
|
// Try parsing the prelude as a selector list
|
|
if list, ok := p.parseSelectorList(); ok {
|
|
rule := css_ast.RSelector{Selectors: list}
|
|
if p.expect(css_lexer.TOpenBrace) {
|
|
rule.Rules = p.parseListOfDeclarations()
|
|
p.expect(css_lexer.TCloseBrace)
|
|
return &rule
|
|
}
|
|
}
|
|
|
|
// Otherwise, parse a generic qualified rule
|
|
return p.parseQualifiedRuleFrom(preludeStart, true /* isAlreadyInvalid */)
|
|
}
|
|
|
|
func (p *parser) parseQualifiedRuleFrom(preludeStart int, isAlreadyInvalid bool) *css_ast.RQualified {
|
|
loop:
|
|
for {
|
|
switch p.current().Kind {
|
|
case css_lexer.TOpenBrace, css_lexer.TEndOfFile:
|
|
break loop
|
|
|
|
case css_lexer.TSemicolon:
|
|
// Error recovery if the block is omitted (likely some CSS meta-syntax)
|
|
if !isAlreadyInvalid {
|
|
p.expect(css_lexer.TOpenBrace)
|
|
}
|
|
prelude := p.convertTokens(p.tokens[preludeStart:p.index])
|
|
p.advance()
|
|
return &css_ast.RQualified{Prelude: prelude}
|
|
|
|
default:
|
|
p.parseComponentValue()
|
|
}
|
|
}
|
|
|
|
rule := css_ast.RQualified{
|
|
Prelude: p.convertTokens(p.tokens[preludeStart:p.index]),
|
|
}
|
|
|
|
if p.eat(css_lexer.TOpenBrace) {
|
|
rule.Rules = p.parseListOfDeclarations()
|
|
p.expect(css_lexer.TCloseBrace)
|
|
} else if !isAlreadyInvalid {
|
|
p.expect(css_lexer.TOpenBrace)
|
|
}
|
|
|
|
return &rule
|
|
}
|
|
|
|
func (p *parser) parseDeclaration() css_ast.R {
|
|
// Parse the key
|
|
keyStart := p.index
|
|
ok := false
|
|
if p.expect(css_lexer.TIdent) {
|
|
p.eat(css_lexer.TWhitespace)
|
|
if p.expect(css_lexer.TColon) {
|
|
ok = true
|
|
}
|
|
}
|
|
|
|
// Parse the value
|
|
valueStart := p.index
|
|
stop:
|
|
for {
|
|
switch p.current().Kind {
|
|
case css_lexer.TEndOfFile, css_lexer.TSemicolon, css_lexer.TCloseBrace:
|
|
break stop
|
|
|
|
case css_lexer.TOpenBrace:
|
|
// Error recovery if there is an unexpected block (likely some CSS meta-syntax)
|
|
p.parseComponentValue()
|
|
p.eat(css_lexer.TWhitespace)
|
|
if ok && !p.peek(css_lexer.TSemicolon) {
|
|
p.expect(css_lexer.TSemicolon)
|
|
}
|
|
break stop
|
|
|
|
default:
|
|
p.parseComponentValue()
|
|
}
|
|
}
|
|
|
|
// Stop now if this is not a valid declaration
|
|
if !ok {
|
|
return &css_ast.RBadDeclaration{
|
|
Tokens: p.convertTokens(p.tokens[keyStart:p.index]),
|
|
}
|
|
}
|
|
|
|
// Remove leading and trailing whitespace from the value
|
|
value := trimWhitespace(p.tokens[valueStart:p.index])
|
|
|
|
// Remove trailing "!important"
|
|
important := false
|
|
if last := len(value) - 1; last >= 0 {
|
|
if t := value[last]; t.Kind == css_lexer.TIdent && strings.EqualFold(t.DecodedText(p.source.Contents), "important") {
|
|
i := len(value) - 2
|
|
if i >= 0 && value[i].Kind == css_lexer.TWhitespace {
|
|
i--
|
|
}
|
|
if i >= 0 && value[i].Kind == css_lexer.TDelimExclamation {
|
|
if i >= 1 && value[i-1].Kind == css_lexer.TWhitespace {
|
|
i--
|
|
}
|
|
value = value[:i]
|
|
important = true
|
|
}
|
|
}
|
|
}
|
|
|
|
keyToken := p.tokens[keyStart]
|
|
keyText := keyToken.DecodedText(p.source.Contents)
|
|
return &css_ast.RDeclaration{
|
|
Key: css_ast.KnownDeclarations[keyText],
|
|
KeyText: keyText,
|
|
KeyRange: keyToken.Range,
|
|
Value: p.convertTokensWithImports(value),
|
|
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) {
|
|
if p.expect(open) {
|
|
for !p.eat(close) {
|
|
if p.peek(css_lexer.TEndOfFile) {
|
|
p.expect(close)
|
|
return
|
|
}
|
|
|
|
p.parseComponentValue()
|
|
}
|
|
}
|
|
}
|
|
|
|
func trimWhitespace(tokens []css_lexer.Token) []css_lexer.Token {
|
|
if len(tokens) > 0 && tokens[0].Kind == css_lexer.TWhitespace {
|
|
tokens = tokens[1:]
|
|
}
|
|
if i := len(tokens) - 1; i >= 0 && tokens[i].Kind == css_lexer.TWhitespace {
|
|
tokens = tokens[:i]
|
|
}
|
|
return tokens
|
|
}
|