Files
esbuild/internal/css_parser/css_parser_selector.go
T

1031 lines
29 KiB
Go

package css_parser
import (
"fmt"
"strings"
"github.com/evanw/esbuild/internal/css_ast"
"github.com/evanw/esbuild/internal/css_lexer"
"github.com/evanw/esbuild/internal/logger"
)
type parseSelectorOpts struct {
composesContext *composesContext
pseudoClassKind css_ast.PseudoClassKind
isDeclarationContext bool
stopOnCloseParen bool
onlyOneComplexSelector bool
isForgivingSelectorList bool
noLeadingCombinator bool
}
func (p *parser) parseSelectorList(opts parseSelectorOpts) (list []css_ast.ComplexSelector, ok bool) {
// Potentially parse an empty list for ":is()" and ":where()"
if opts.isForgivingSelectorList && opts.stopOnCloseParen && p.peek(css_lexer.TCloseParen) {
ok = true
return
}
// Parse the first selector
sel, good := p.parseComplexSelector(parseComplexSelectorOpts{
parseSelectorOpts: opts,
isFirst: true,
})
if !good {
return
}
list = p.flattenLocalAndGlobalSelectors(list, sel)
// Parse the remaining selectors
if opts.onlyOneComplexSelector {
if t := p.current(); t.Kind == css_lexer.TComma {
p.prevError = t.Range.Loc
kind := fmt.Sprintf(":%s(...)", opts.pseudoClassKind.String())
p.log.AddIDWithNotes(logger.MsgID_CSS_CSSSyntaxError, logger.Warning, &p.tracker, t.Range,
fmt.Sprintf("Unexpected \",\" inside %q", kind),
[]logger.MsgData{{Text: fmt.Sprintf("Different CSS tools behave differently in this case, so esbuild doesn't allow it. "+
"Either remove this comma or split this selector up into multiple comma-separated %q selectors instead.", kind)}})
return
}
} else {
skip:
for {
p.eat(css_lexer.TWhitespace)
if !p.eat(css_lexer.TComma) {
break
}
p.eat(css_lexer.TWhitespace)
sel, good := p.parseComplexSelector(parseComplexSelectorOpts{
parseSelectorOpts: opts,
})
if !good {
return
}
// Omit duplicate selectors
if p.options.minifySyntax {
for _, existing := range list {
if sel.Equal(existing, nil) {
continue skip
}
}
}
list = p.flattenLocalAndGlobalSelectors(list, sel)
}
}
// Remove the leading ampersand when minifying and it can be implied:
//
// "a { & b {} }" => "a { b {} }"
//
// It can't be implied if it's not at the beginning, if there are multiple of
// them, or if the selector list is inside of a pseudo-class selector:
//
// "a { b & {} }"
// "a { & b & {} }"
// "a { :has(& b) {} }"
//
if p.options.minifySyntax && !opts.stopOnCloseParen {
for i := 1; i < len(list); i++ {
if analyzeLeadingAmpersand(list[i], opts.isDeclarationContext) != cannotRemoveLeadingAmpersand {
list[i].Selectors = list[i].Selectors[1:]
}
}
switch analyzeLeadingAmpersand(list[0], opts.isDeclarationContext) {
case canAlwaysRemoveLeadingAmpersand:
list[0].Selectors = list[0].Selectors[1:]
case canRemoveLeadingAmpersandIfNotFirst:
for i := 1; i < len(list); i++ {
if sel := list[i].Selectors[0]; len(sel.NestingSelectorLocs) == 0 && (sel.Combinator.Byte != 0 || sel.TypeSelector == nil) {
list[0].Selectors = list[0].Selectors[1:]
list[0], list[i] = list[i], list[0]
break
}
}
}
}
ok = true
return
}
func mergeCompoundSelectors(target *css_ast.CompoundSelector, source css_ast.CompoundSelector) {
// ".foo:local(&)" => "&.foo"
if len(source.NestingSelectorLocs) > 0 && len(target.NestingSelectorLocs) == 0 {
target.NestingSelectorLocs = source.NestingSelectorLocs
}
if source.TypeSelector != nil {
if target.TypeSelector == nil {
// ".foo:local(div)" => "div.foo"
target.TypeSelector = source.TypeSelector
} else {
// "div:local(span)" => "div:is(span)"
//
// Note: All other implementations of this (Lightning CSS, PostCSS, and
// Webpack) do something really weird here. They do this instead:
//
// "div:local(span)" => "divspan"
//
// But that just seems so obviously wrong that I'm not going to do that.
target.SubclassSelectors = append(target.SubclassSelectors, css_ast.SubclassSelector{
Range: source.TypeSelector.Range(),
Data: &css_ast.SSPseudoClassWithSelectorList{
Kind: css_ast.PseudoClassIs,
Selectors: []css_ast.ComplexSelector{{Selectors: []css_ast.CompoundSelector{{TypeSelector: source.TypeSelector}}}},
},
})
}
}
// ".foo:local(.bar)" => ".foo.bar"
target.SubclassSelectors = append(target.SubclassSelectors, source.SubclassSelectors...)
}
func containsLocalOrGlobalSelector(sel css_ast.ComplexSelector) bool {
for _, s := range sel.Selectors {
for _, ss := range s.SubclassSelectors {
switch pseudo := ss.Data.(type) {
case *css_ast.SSPseudoClass:
if pseudo.Name == "global" || pseudo.Name == "local" {
return true
}
case *css_ast.SSPseudoClassWithSelectorList:
if pseudo.Kind == css_ast.PseudoClassGlobal || pseudo.Kind == css_ast.PseudoClassLocal {
return true
}
}
}
}
return false
}
// This handles the ":local()" and ":global()" annotations from CSS modules
func (p *parser) flattenLocalAndGlobalSelectors(list []css_ast.ComplexSelector, sel css_ast.ComplexSelector) []css_ast.ComplexSelector {
// Only do the work to flatten the whole list if there's a ":local" or a ":global"
if p.options.symbolMode != symbolModeDisabled && containsLocalOrGlobalSelector(sel) {
var selectors []css_ast.CompoundSelector
for _, s := range sel.Selectors {
oldSubclassSelectors := s.SubclassSelectors
s.SubclassSelectors = make([]css_ast.SubclassSelector, 0, len(oldSubclassSelectors))
for _, ss := range oldSubclassSelectors {
switch pseudo := ss.Data.(type) {
case *css_ast.SSPseudoClass:
if pseudo.Name == "global" || pseudo.Name == "local" {
// Remove bare ":global" and ":local" pseudo-classes
continue
}
case *css_ast.SSPseudoClassWithSelectorList:
if pseudo.Kind == css_ast.PseudoClassGlobal || pseudo.Kind == css_ast.PseudoClassLocal {
inner := pseudo.Selectors[0].Selectors
// Replace this pseudo-class with all inner compound selectors.
// The first inner compound selector is merged with the compound
// selector before it and the last inner compound selector is
// merged with the compound selector after it:
//
// "div:local(.a .b):hover" => "div.a b:hover"
//
// This behavior is really strange since this is not how anything
// involving pseudo-classes in real CSS works at all. However, all
// other implementations (Lightning CSS, PostCSS, and Webpack) are
// consistent with this strange behavior, so we do it too.
if inner[0].Combinator.Byte == 0 {
mergeCompoundSelectors(&s, inner[0])
inner = inner[1:]
} else {
// "div:local(+ .foo):hover" => "div + .foo:hover"
}
if n := len(inner); n > 0 {
if !s.IsInvalidBecauseEmpty() {
// Don't add this selector if it consisted only of a bare ":global" or ":local"
selectors = append(selectors, s)
}
selectors = append(selectors, inner[:n-1]...)
s = inner[n-1]
}
continue
}
}
s.SubclassSelectors = append(s.SubclassSelectors, ss)
}
if !s.IsInvalidBecauseEmpty() {
// Don't add this selector if it consisted only of a bare ":global" or ":local"
selectors = append(selectors, s)
}
}
if len(selectors) == 0 {
// Treat a bare ":global" or ":local" as a bare "&" nesting selector
selectors = append(selectors, css_ast.CompoundSelector{
NestingSelectorLocs: []logger.Loc{sel.Selectors[0].Range().Loc},
WasEmptyFromLocalOrGlobal: true,
})
// Make sure we report that nesting is present so that it can be lowered
p.nestingIsPresent = true
}
sel.Selectors = selectors
}
return append(list, sel)
}
type leadingAmpersand uint8
const (
cannotRemoveLeadingAmpersand leadingAmpersand = iota
canAlwaysRemoveLeadingAmpersand
canRemoveLeadingAmpersandIfNotFirst
)
func analyzeLeadingAmpersand(sel css_ast.ComplexSelector, isDeclarationContext bool) leadingAmpersand {
if len(sel.Selectors) > 1 {
if first := sel.Selectors[0]; first.IsSingleAmpersand() {
second := sel.Selectors[1]
rest := css_ast.ComplexSelector{Selectors: sel.Selectors[1:]}
if second.Combinator.Byte == 0 && rest.ContainsNestingCombinator() {
// ".foo { & &.bar {} }" => ".foo { & &.bar {} }"
// ".foo { & .bar:not(& .baz) {} }" => ".foo { & .bar:not(& .baz) {} }"
// The specification says: "If a selector in the <relative-selector-list>
// does not start with a combinator but does contain the nesting selector,
// it is interpreted as a non-relative selector." So we don't want to
// remove the leading "&" here as that would change the meaning to a
// non-relative selector. See: https://www.w3.org/TR/css-nesting-1/
} else if second.Combinator.Byte != 0 || second.TypeSelector == nil || !isDeclarationContext {
// "& + div {}" => "+ div {}"
// "& div {}" => "div {}"
// ".foo { & + div {} }" => ".foo { + div {} }"
// ".foo { & + &.bar {} }" => ".foo { + &.bar {} }"
// ".foo { & :hover {} }" => ".foo { :hover {} }"
return canAlwaysRemoveLeadingAmpersand
} else {
// ".foo { & div {} }"
// ".foo { .bar, & div {} }" => ".foo { .bar, div {} }"
// The first iteration of CSS nesting didn't support an implicit "&" if
// the selector started with an identifier. This was done to avoid
// confusion with declarations in the parser, as the parser would
// otherwise need unbounded lookahead to distinguish between the two.
// Consider the following:
//
// span {
// color:blue;
// div:hover span {}
// }
//
// The "color:blue" is a declaration while "div:hover" is actually a
// relative selector in a nested style rule. The parser would need to
// scan to the ";" or "{" to distinguish between the two cases.
// Generally you want to avoid parsers with unbounded lookahead.
//
// However, this edge case is commonly encountered and it's confusing
// that it doesn't work. So the CSS working group later added unbounded
// lookahead to fix it: https://github.com/w3c/csswg-drafts/issues/7961.
//
// We deliberately don't take advantage of that newest syntax addition
// here so that our output still works in browsers before that newest
// syntax addition. We still keep a leading "&" before identifiers.
return canRemoveLeadingAmpersandIfNotFirst
}
}
} else {
// "& {}" => "& {}"
}
return cannotRemoveLeadingAmpersand
}
type parseComplexSelectorOpts struct {
parseSelectorOpts
isFirst bool
}
func (p *parser) parseComplexSelector(opts parseComplexSelectorOpts) (result css_ast.ComplexSelector, ok bool) {
// This is an extension: https://drafts.csswg.org/css-nesting-1/
var combinator css_ast.Combinator
if !opts.noLeadingCombinator {
combinator = p.parseCombinator()
if combinator.Byte != 0 {
p.nestingIsPresent = true
p.eat(css_lexer.TWhitespace)
}
}
// Parent
sel, good := p.parseCompoundSelector(parseComplexSelectorOpts{
parseSelectorOpts: opts.parseSelectorOpts,
isFirst: opts.isFirst,
})
if !good {
return
}
sel.Combinator = combinator
result.Selectors = append(result.Selectors, sel)
stop := css_lexer.TOpenBrace
if opts.stopOnCloseParen {
stop = css_lexer.TCloseParen
}
for {
p.eat(css_lexer.TWhitespace)
if p.peek(css_lexer.TEndOfFile) || p.peek(css_lexer.TComma) || p.peek(stop) {
break
}
// Optional combinator
combinator := p.parseCombinator()
if combinator.Byte != 0 {
p.eat(css_lexer.TWhitespace)
}
// Child
sel, good := p.parseCompoundSelector(parseComplexSelectorOpts{
parseSelectorOpts: opts.parseSelectorOpts,
})
if !good {
return
}
sel.Combinator = combinator
result.Selectors = append(result.Selectors, sel)
}
ok = true
return
}
func (p *parser) nameToken() css_ast.NameToken {
t := p.current()
return css_ast.NameToken{
Kind: t.Kind,
Range: t.Range,
Text: p.decoded(),
}
}
func (p *parser) parseCompoundSelector(opts parseComplexSelectorOpts) (sel css_ast.CompoundSelector, ok bool) {
startLoc := p.current().Range.Loc
// This is an extension: https://drafts.csswg.org/css-nesting-1/
hasLeadingNestingSelector := p.peek(css_lexer.TDelimAmpersand)
if hasLeadingNestingSelector {
p.nestingIsPresent = true
sel.NestingSelectorLocs = append(sel.NestingSelectorLocs, startLoc)
p.advance()
}
// Parse the type selector
typeSelectorLoc := p.current().Range.Loc
switch p.current().Kind {
case css_lexer.TDelimBar, css_lexer.TIdent, css_lexer.TDelimAsterisk:
nsName := css_ast.NamespacedName{}
if !p.peek(css_lexer.TDelimBar) {
nsName.Name = p.nameToken()
p.advance()
} else {
// Hack: Create an empty "identifier" to represent this
nsName.Name.Kind = css_lexer.TIdent
}
if p.eat(css_lexer.TDelimBar) {
if !p.peek(css_lexer.TIdent) && !p.peek(css_lexer.TDelimAsterisk) {
p.expect(css_lexer.TIdent)
return
}
prefix := nsName.Name
nsName.NamespacePrefix = &prefix
nsName.Name = p.nameToken()
p.advance()
}
sel.TypeSelector = &nsName
}
// Parse the subclass selectors
subclassSelectors:
for {
subclassToken := p.current()
switch subclassToken.Kind {
case css_lexer.THash:
if (subclassToken.Flags & css_lexer.IsID) == 0 {
break subclassSelectors
}
nameLoc := logger.Loc{Start: subclassToken.Range.Loc.Start + 1}
name := p.decoded()
sel.SubclassSelectors = append(sel.SubclassSelectors, css_ast.SubclassSelector{
Range: subclassToken.Range,
Data: &css_ast.SSHash{
Name: p.symbolForName(nameLoc, name),
},
})
p.advance()
case css_lexer.TDelimDot:
p.advance()
nameRange := p.current().Range
name := p.decoded()
sel.SubclassSelectors = append(sel.SubclassSelectors, css_ast.SubclassSelector{
Range: logger.Range{Loc: subclassToken.Range.Loc, Len: nameRange.End() - subclassToken.Range.Loc.Start},
Data: &css_ast.SSClass{
Name: p.symbolForName(nameRange.Loc, name),
},
})
if !p.expect(css_lexer.TIdent) {
return
}
case css_lexer.TOpenBracket:
attr, r := p.parseAttributeSelector()
if r.Len == 0 {
return
}
sel.SubclassSelectors = append(sel.SubclassSelectors, css_ast.SubclassSelector{
Range: r,
Data: &attr,
})
case css_lexer.TColon:
if p.next().Kind == css_lexer.TColon {
// Special-case the start of the pseudo-element selector section
for p.peek(css_lexer.TColon) {
firstColonLoc := p.current().Range.Loc
isElement := p.next().Kind == css_lexer.TColon
if isElement {
p.advance()
}
pseudo, r := p.parsePseudoClassSelector(firstColonLoc, isElement)
// https://www.w3.org/TR/selectors-4/#single-colon-pseudos
// The four Level 2 pseudo-elements (::before, ::after, ::first-line,
// and ::first-letter) may, for legacy reasons, be represented using
// the <pseudo-class-selector> grammar, with only a single ":"
// character at their start.
if p.options.minifySyntax && isElement {
if pseudo, ok := pseudo.(*css_ast.SSPseudoClass); ok && len(pseudo.Args) == 0 {
switch pseudo.Name {
case "before", "after", "first-line", "first-letter":
pseudo.IsElement = false
}
}
}
sel.SubclassSelectors = append(sel.SubclassSelectors, css_ast.SubclassSelector{
Range: r,
Data: pseudo,
})
}
break subclassSelectors
}
pseudo, r := p.parsePseudoClassSelector(subclassToken.Range.Loc, false)
sel.SubclassSelectors = append(sel.SubclassSelectors, css_ast.SubclassSelector{
Range: r,
Data: pseudo,
})
case css_lexer.TDelimAmpersand:
// This is an extension: https://drafts.csswg.org/css-nesting-1/
p.nestingIsPresent = true
sel.NestingSelectorLocs = append(sel.NestingSelectorLocs, subclassToken.Range.Loc)
p.advance()
default:
break subclassSelectors
}
}
// The compound selector must be non-empty
if sel.IsInvalidBecauseEmpty() {
p.unexpected()
return
}
// Note: "&div {}" was originally valid, but is now an invalid selector:
// https://github.com/w3c/csswg-drafts/issues/8662#issuecomment-1514977935.
// This is because SASS already uses that syntax to mean something very
// different, so that syntax has been removed to avoid mistakes.
if hasLeadingNestingSelector && sel.TypeSelector != nil {
r := logger.Range{Loc: typeSelectorLoc, Len: p.at(p.index-1).Range.End() - typeSelectorLoc.Start}
text := sel.TypeSelector.Name.Text
if sel.TypeSelector.NamespacePrefix != nil {
text = fmt.Sprintf("%s|%s", sel.TypeSelector.NamespacePrefix.Text, text)
}
var howToFix string
suggestion := p.source.TextForRange(r)
if opts.isFirst {
suggestion = fmt.Sprintf(":is(%s)", suggestion)
howToFix = "You can wrap this selector in \":is(...)\" as a workaround. "
} else {
r = logger.Range{Loc: startLoc, Len: r.End() - startLoc.Start}
suggestion += "&"
howToFix = "You can move the \"&\" to the end of this selector as a workaround. "
}
msg := logger.Msg{
Kind: logger.Warning,
Data: p.tracker.MsgData(r, fmt.Sprintf("Cannot use type selector %q directly after nesting selector \"&\"", text)),
Notes: []logger.MsgData{{Text: "CSS nesting syntax does not allow the \"&\" selector to come before a type selector. " +
howToFix +
"This restriction exists to avoid problems with SASS nesting, where the same syntax means something very different " +
"that has no equivalent in real CSS (appending a suffix to the parent selector)."}},
}
msg.Data.Location.Suggestion = suggestion
p.log.AddMsgID(logger.MsgID_CSS_CSSSyntaxError, msg)
return
}
// The type selector must always come first
switch p.current().Kind {
case css_lexer.TDelimBar, css_lexer.TIdent, css_lexer.TDelimAsterisk:
p.unexpected()
return
}
ok = true
return
}
func (p *parser) parseAttributeSelector() (attr css_ast.SSAttribute, r logger.Range) {
matchingLoc := p.current().Range.Loc
p.advance()
// Parse the namespaced name
switch p.current().Kind {
case css_lexer.TDelimBar, css_lexer.TDelimAsterisk:
// "[|x]"
// "[*|x]"
if p.peek(css_lexer.TDelimAsterisk) {
prefix := p.nameToken()
p.advance()
attr.NamespacedName.NamespacePrefix = &prefix
} else {
// "[|attr]" is equivalent to "[attr]". From the specification:
// "In keeping with the Namespaces in the XML recommendation, default
// namespaces do not apply to attributes, therefore attribute selectors
// without a namespace component apply only to attributes that have no
// namespace (equivalent to |attr)."
}
if !p.expect(css_lexer.TDelimBar) {
return
}
attr.NamespacedName.Name = p.nameToken()
if !p.expect(css_lexer.TIdent) {
return
}
default:
// "[x]"
// "[x|y]"
attr.NamespacedName.Name = p.nameToken()
if !p.expect(css_lexer.TIdent) {
return
}
if p.next().Kind != css_lexer.TDelimEquals && p.eat(css_lexer.TDelimBar) {
prefix := attr.NamespacedName.Name
attr.NamespacedName.NamespacePrefix = &prefix
attr.NamespacedName.Name = p.nameToken()
if !p.expect(css_lexer.TIdent) {
return
}
}
}
// Parse the optional matcher operator
p.eat(css_lexer.TWhitespace)
if p.eat(css_lexer.TDelimEquals) {
attr.MatcherOp = "="
} else {
switch p.current().Kind {
case css_lexer.TDelimTilde:
attr.MatcherOp = "~="
case css_lexer.TDelimBar:
attr.MatcherOp = "|="
case css_lexer.TDelimCaret:
attr.MatcherOp = "^="
case css_lexer.TDelimDollar:
attr.MatcherOp = "$="
case css_lexer.TDelimAsterisk:
attr.MatcherOp = "*="
}
if attr.MatcherOp != "" {
p.advance()
if !p.expect(css_lexer.TDelimEquals) {
return
}
}
}
// Parse the optional matcher value
if attr.MatcherOp != "" {
p.eat(css_lexer.TWhitespace)
if !p.peek(css_lexer.TString) && !p.peek(css_lexer.TIdent) {
p.unexpected()
}
attr.MatcherValue = p.decoded()
p.advance()
p.eat(css_lexer.TWhitespace)
if p.peek(css_lexer.TIdent) {
if modifier := p.decoded(); len(modifier) == 1 {
if c := modifier[0]; c == 'i' || c == 'I' || c == 's' || c == 'S' {
attr.MatcherModifier = c
p.advance()
}
}
}
}
closeRange := p.current().Range
if !p.expectWithMatchingLoc(css_lexer.TCloseBracket, matchingLoc) {
closeRange.Len = 0
}
r = logger.Range{Loc: matchingLoc, Len: closeRange.End() - matchingLoc.Start}
return
}
func (p *parser) parsePseudoClassSelector(loc logger.Loc, isElement bool) (css_ast.SS, logger.Range) {
p.advance()
if p.peek(css_lexer.TFunction) {
text := p.decoded()
matchingLoc := logger.Loc{Start: p.current().Range.End() - 1}
p.advance()
// Potentially parse a pseudo-class with a selector list
if !isElement {
var kind css_ast.PseudoClassKind
local := p.makeLocalSymbols
ok := true
switch text {
case "global":
kind = css_ast.PseudoClassGlobal
if p.options.symbolMode != symbolModeDisabled {
local = false
}
case "has":
kind = css_ast.PseudoClassHas
case "is":
kind = css_ast.PseudoClassIs
case "local":
kind = css_ast.PseudoClassLocal
if p.options.symbolMode != symbolModeDisabled {
local = true
}
case "not":
kind = css_ast.PseudoClassNot
case "nth-child":
kind = css_ast.PseudoClassNthChild
case "nth-last-child":
kind = css_ast.PseudoClassNthLastChild
case "nth-of-type":
kind = css_ast.PseudoClassNthOfType
case "nth-last-of-type":
kind = css_ast.PseudoClassNthLastOfType
case "where":
kind = css_ast.PseudoClassWhere
default:
ok = false
}
if ok {
old := p.index
if kind.HasNthIndex() {
p.eat(css_lexer.TWhitespace)
// Parse the "An+B" syntax
if index, ok := p.parseNthIndex(); ok {
var selectors []css_ast.ComplexSelector
// Parse the optional "of" clause
if (kind == css_ast.PseudoClassNthChild || kind == css_ast.PseudoClassNthLastChild) &&
p.peek(css_lexer.TIdent) && strings.EqualFold(p.decoded(), "of") {
p.advance()
p.eat(css_lexer.TWhitespace)
// Contain the effects of ":local" and ":global"
oldLocal := p.makeLocalSymbols
selectors, ok = p.parseSelectorList(parseSelectorOpts{
stopOnCloseParen: true,
noLeadingCombinator: true,
})
p.makeLocalSymbols = oldLocal
}
// "2n+0" => "2n"
if p.options.minifySyntax {
index.Minify()
}
// Match the closing ")"
if ok {
closeRange := p.current().Range
if !p.expectWithMatchingLoc(css_lexer.TCloseParen, matchingLoc) {
closeRange.Len = 0
}
return &css_ast.SSPseudoClassWithSelectorList{Kind: kind, Selectors: selectors, Index: index},
logger.Range{Loc: loc, Len: closeRange.End() - loc.Start}
}
}
} else {
p.eat(css_lexer.TWhitespace)
// ":local" forces local names and ":global" forces global names
oldLocal := p.makeLocalSymbols
p.makeLocalSymbols = local
selectors, ok := p.parseSelectorList(parseSelectorOpts{
pseudoClassKind: kind,
stopOnCloseParen: true,
onlyOneComplexSelector: kind == css_ast.PseudoClassGlobal || kind == css_ast.PseudoClassLocal,
isForgivingSelectorList: kind == css_ast.PseudoClassIs || kind == css_ast.PseudoClassWhere,
})
p.makeLocalSymbols = oldLocal
// Match the closing ")"
if ok {
closeRange := p.current().Range
if !p.expectWithMatchingLoc(css_lexer.TCloseParen, matchingLoc) {
closeRange.Len = 0
}
return &css_ast.SSPseudoClassWithSelectorList{Kind: kind, Selectors: selectors},
logger.Range{Loc: loc, Len: closeRange.End() - loc.Start}
}
}
p.index = old
}
}
args := p.convertTokens(p.parseAnyValue())
closeRange := p.current().Range
if !p.expectWithMatchingLoc(css_lexer.TCloseParen, matchingLoc) {
closeRange.Len = 0
}
return &css_ast.SSPseudoClass{IsElement: isElement, Name: text, Args: args},
logger.Range{Loc: loc, Len: closeRange.End() - loc.Start}
}
nameRange := p.current().Range
name := p.decoded()
sel := css_ast.SSPseudoClass{IsElement: isElement}
if p.expect(css_lexer.TIdent) {
sel.Name = name
// ":local .local_name :global .global_name {}"
// ":local { .local_name { :global { .global_name {} } }"
if p.options.symbolMode != symbolModeDisabled {
switch name {
case "local":
p.makeLocalSymbols = true
case "global":
p.makeLocalSymbols = false
}
}
} else {
nameRange.Len = 0
}
return &sel, logger.Range{Loc: loc, Len: nameRange.End() - loc.Start}
}
func (p *parser) parseAnyValue() []css_lexer.Token {
// Reference: https://drafts.csswg.org/css-syntax-3/#typedef-declaration-value
p.stack = p.stack[:0] // Reuse allocated memory
start := p.index
loop:
for {
switch p.current().Kind {
case css_lexer.TCloseParen, css_lexer.TCloseBracket, css_lexer.TCloseBrace:
last := len(p.stack) - 1
if last < 0 || !p.peek(p.stack[last]) {
break loop
}
p.stack = p.stack[:last]
case css_lexer.TSemicolon, css_lexer.TDelimExclamation:
if len(p.stack) == 0 {
break loop
}
case css_lexer.TOpenParen, css_lexer.TFunction:
p.stack = append(p.stack, css_lexer.TCloseParen)
case css_lexer.TOpenBracket:
p.stack = append(p.stack, css_lexer.TCloseBracket)
case css_lexer.TOpenBrace:
p.stack = append(p.stack, css_lexer.TCloseBrace)
case css_lexer.TEndOfFile:
break loop
}
p.advance()
}
tokens := p.tokens[start:p.index]
if len(tokens) == 0 {
p.unexpected()
}
return tokens
}
func (p *parser) parseCombinator() css_ast.Combinator {
t := p.current()
switch t.Kind {
case css_lexer.TDelimGreaterThan:
p.advance()
return css_ast.Combinator{Loc: t.Range.Loc, Byte: '>'}
case css_lexer.TDelimPlus:
p.advance()
return css_ast.Combinator{Loc: t.Range.Loc, Byte: '+'}
case css_lexer.TDelimTilde:
p.advance()
return css_ast.Combinator{Loc: t.Range.Loc, Byte: '~'}
default:
return css_ast.Combinator{}
}
}
func parseInteger(text string) (string, bool) {
n := len(text)
if n == 0 {
return "", false
}
// Trim leading zeros
start := 0
for start < n && text[start] == '0' {
start++
}
// Make sure remaining characters are digits
if start == n {
return "0", true
}
for i := start; i < n; i++ {
if c := text[i]; c < '0' || c > '9' {
return "", false
}
}
return text[start:], true
}
func (p *parser) parseNthIndex() (css_ast.NthIndex, bool) {
type sign uint8
const (
none sign = iota
negative
positive
)
// Reference: https://drafts.csswg.org/css-syntax-3/#anb-microsyntax
t0 := p.current()
text0 := p.decoded()
// Handle "even" and "odd"
if t0.Kind == css_lexer.TIdent && (text0 == "even" || text0 == "odd") {
p.advance()
p.eat(css_lexer.TWhitespace)
return css_ast.NthIndex{B: text0}, true
}
// Handle a single number
if t0.Kind == css_lexer.TNumber {
bNeg := false
if strings.HasPrefix(text0, "-") {
bNeg = true
text0 = text0[1:]
} else {
text0 = strings.TrimPrefix(text0, "+")
}
if b, ok := parseInteger(text0); ok {
if bNeg {
b = "-" + b
}
p.advance()
p.eat(css_lexer.TWhitespace)
return css_ast.NthIndex{B: b}, true
}
p.unexpected()
return css_ast.NthIndex{}, false
}
aSign := none
if p.eat(css_lexer.TDelimPlus) {
aSign = positive
t0 = p.current()
text0 = p.decoded()
}
// Everything from here must be able to contain an "n"
if t0.Kind != css_lexer.TIdent && t0.Kind != css_lexer.TDimension {
p.unexpected()
return css_ast.NthIndex{}, false
}
// Check for a leading sign
if aSign == none {
if strings.HasPrefix(text0, "-") {
aSign = negative
text0 = text0[1:]
} else {
text0 = strings.TrimPrefix(text0, "+")
}
}
// The string must contain an "n"
n := strings.IndexByte(text0, 'n')
if n < 0 {
p.unexpected()
return css_ast.NthIndex{}, false
}
// Parse the number before the "n"
var a string
if n == 0 {
if aSign == negative {
a = "-1"
} else {
a = "1"
}
} else if aInt, ok := parseInteger(text0[:n]); ok {
if aSign == negative {
aInt = "-" + aInt
}
a = aInt
} else {
p.unexpected()
return css_ast.NthIndex{}, false
}
text0 = text0[n+1:]
// Parse the stuff after the "n"
bSign := none
if strings.HasPrefix(text0, "-") {
text0 = text0[1:]
if b, ok := parseInteger(text0); ok {
p.advance()
p.eat(css_lexer.TWhitespace)
return css_ast.NthIndex{A: a, B: "-" + b}, true
}
bSign = negative
}
if text0 != "" {
p.unexpected()
return css_ast.NthIndex{}, false
}
p.advance()
p.eat(css_lexer.TWhitespace)
// Parse an optional sign delimiter
if bSign == none {
if p.eat(css_lexer.TDelimMinus) {
bSign = negative
p.eat(css_lexer.TWhitespace)
} else if p.eat(css_lexer.TDelimPlus) {
bSign = positive
p.eat(css_lexer.TWhitespace)
}
}
// Parse an optional trailing number
t1 := p.current()
text1 := p.decoded()
if t1.Kind == css_lexer.TNumber {
if bSign == none {
if strings.HasPrefix(text1, "-") {
bSign = negative
text1 = text1[1:]
} else if strings.HasPrefix(text1, "+") {
text1 = text1[1:]
}
}
if b, ok := parseInteger(text1); ok {
if bSign == negative {
b = "-" + b
}
p.advance()
p.eat(css_lexer.TWhitespace)
return css_ast.NthIndex{A: a, B: b}, true
}
}
// If there is a trailing sign, then there must also be a trailing number
if bSign != none {
p.expect(css_lexer.TNumber)
return css_ast.NthIndex{}, false
}
return css_ast.NthIndex{A: a}, true
}