github.com/evanw/esbuild@v0.21.4/internal/css_parser/css_parser_selector.go (about)

     1  package css_parser
     2  
     3  import (
     4  	"fmt"
     5  	"strings"
     6  
     7  	"github.com/evanw/esbuild/internal/ast"
     8  	"github.com/evanw/esbuild/internal/css_ast"
     9  	"github.com/evanw/esbuild/internal/css_lexer"
    10  	"github.com/evanw/esbuild/internal/logger"
    11  )
    12  
    13  type parseSelectorOpts struct {
    14  	composesContext        *composesContext
    15  	pseudoClassKind        css_ast.PseudoClassKind
    16  	isDeclarationContext   bool
    17  	stopOnCloseParen       bool
    18  	onlyOneComplexSelector bool
    19  	noLeadingCombinator    bool
    20  }
    21  
    22  func (p *parser) parseSelectorList(opts parseSelectorOpts) (list []css_ast.ComplexSelector, ok bool) {
    23  	// Parse the first selector
    24  	sel, good := p.parseComplexSelector(parseComplexSelectorOpts{
    25  		parseSelectorOpts: opts,
    26  		isFirst:           true,
    27  	})
    28  	if !good {
    29  		return
    30  	}
    31  	list = p.flattenLocalAndGlobalSelectors(list, sel)
    32  
    33  	// Parse the remaining selectors
    34  	if opts.onlyOneComplexSelector {
    35  		if t := p.current(); t.Kind == css_lexer.TComma {
    36  			p.prevError = t.Range.Loc
    37  			kind := fmt.Sprintf(":%s(...)", opts.pseudoClassKind.String())
    38  			p.log.AddIDWithNotes(logger.MsgID_CSS_CSSSyntaxError, logger.Warning, &p.tracker, t.Range,
    39  				fmt.Sprintf("Unexpected \",\" inside %q", kind),
    40  				[]logger.MsgData{{Text: fmt.Sprintf("Different CSS tools behave differently in this case, so esbuild doesn't allow it. "+
    41  					"Either remove this comma or split this selector up into multiple comma-separated %q selectors instead.", kind)}})
    42  			return
    43  		}
    44  	} else {
    45  	skip:
    46  		for {
    47  			p.eat(css_lexer.TWhitespace)
    48  			if !p.eat(css_lexer.TComma) {
    49  				break
    50  			}
    51  			p.eat(css_lexer.TWhitespace)
    52  			sel, good := p.parseComplexSelector(parseComplexSelectorOpts{
    53  				parseSelectorOpts: opts,
    54  			})
    55  			if !good {
    56  				return
    57  			}
    58  
    59  			// Omit duplicate selectors
    60  			if p.options.minifySyntax {
    61  				for _, existing := range list {
    62  					if sel.Equal(existing, nil) {
    63  						continue skip
    64  					}
    65  				}
    66  			}
    67  
    68  			list = p.flattenLocalAndGlobalSelectors(list, sel)
    69  		}
    70  	}
    71  
    72  	if p.options.minifySyntax {
    73  		for i := 1; i < len(list); i++ {
    74  			if analyzeLeadingAmpersand(list[i], opts.isDeclarationContext) != cannotRemoveLeadingAmpersand {
    75  				list[i].Selectors = list[i].Selectors[1:]
    76  			}
    77  		}
    78  
    79  		switch analyzeLeadingAmpersand(list[0], opts.isDeclarationContext) {
    80  		case canAlwaysRemoveLeadingAmpersand:
    81  			list[0].Selectors = list[0].Selectors[1:]
    82  
    83  		case canRemoveLeadingAmpersandIfNotFirst:
    84  			for i := 1; i < len(list); i++ {
    85  				if sel := list[i].Selectors[0]; !sel.HasNestingSelector() && (sel.Combinator.Byte != 0 || sel.TypeSelector == nil) {
    86  					list[0].Selectors = list[0].Selectors[1:]
    87  					list[0], list[i] = list[i], list[0]
    88  					break
    89  				}
    90  			}
    91  		}
    92  	}
    93  
    94  	ok = true
    95  	return
    96  }
    97  
    98  func mergeCompoundSelectors(target *css_ast.CompoundSelector, source css_ast.CompoundSelector) {
    99  	// ".foo:local(&)" => "&.foo"
   100  	if source.HasNestingSelector() && !target.HasNestingSelector() {
   101  		target.NestingSelectorLoc = source.NestingSelectorLoc
   102  	}
   103  
   104  	if source.TypeSelector != nil {
   105  		if target.TypeSelector == nil {
   106  			// ".foo:local(div)" => "div.foo"
   107  			target.TypeSelector = source.TypeSelector
   108  		} else {
   109  			// "div:local(span)" => "div:is(span)"
   110  			//
   111  			// Note: All other implementations of this (Lightning CSS, PostCSS, and
   112  			// Webpack) do something really weird here. They do this instead:
   113  			//
   114  			// "div:local(span)" => "divspan"
   115  			//
   116  			// But that just seems so obviously wrong that I'm not going to do that.
   117  			target.SubclassSelectors = append(target.SubclassSelectors, css_ast.SubclassSelector{
   118  				Range: source.TypeSelector.Range(),
   119  				Data: &css_ast.SSPseudoClassWithSelectorList{
   120  					Kind:      css_ast.PseudoClassIs,
   121  					Selectors: []css_ast.ComplexSelector{{Selectors: []css_ast.CompoundSelector{{TypeSelector: source.TypeSelector}}}},
   122  				},
   123  			})
   124  		}
   125  	}
   126  
   127  	// ".foo:local(.bar)" => ".foo.bar"
   128  	target.SubclassSelectors = append(target.SubclassSelectors, source.SubclassSelectors...)
   129  }
   130  
   131  func containsLocalOrGlobalSelector(sel css_ast.ComplexSelector) bool {
   132  	for _, s := range sel.Selectors {
   133  		for _, ss := range s.SubclassSelectors {
   134  			switch pseudo := ss.Data.(type) {
   135  			case *css_ast.SSPseudoClass:
   136  				if pseudo.Name == "global" || pseudo.Name == "local" {
   137  					return true
   138  				}
   139  
   140  			case *css_ast.SSPseudoClassWithSelectorList:
   141  				if pseudo.Kind == css_ast.PseudoClassGlobal || pseudo.Kind == css_ast.PseudoClassLocal {
   142  					return true
   143  				}
   144  			}
   145  		}
   146  	}
   147  	return false
   148  }
   149  
   150  // This handles the ":local()" and ":global()" annotations from CSS modules
   151  func (p *parser) flattenLocalAndGlobalSelectors(list []css_ast.ComplexSelector, sel css_ast.ComplexSelector) []css_ast.ComplexSelector {
   152  	// Only do the work to flatten the whole list if there's a ":local" or a ":global"
   153  	if p.options.symbolMode != symbolModeDisabled && containsLocalOrGlobalSelector(sel) {
   154  		var selectors []css_ast.CompoundSelector
   155  
   156  		for _, s := range sel.Selectors {
   157  			oldSubclassSelectors := s.SubclassSelectors
   158  			s.SubclassSelectors = make([]css_ast.SubclassSelector, 0, len(oldSubclassSelectors))
   159  
   160  			for _, ss := range oldSubclassSelectors {
   161  				switch pseudo := ss.Data.(type) {
   162  				case *css_ast.SSPseudoClass:
   163  					if pseudo.Name == "global" || pseudo.Name == "local" {
   164  						// Remove bare ":global" and ":local" pseudo-classes
   165  						continue
   166  					}
   167  
   168  				case *css_ast.SSPseudoClassWithSelectorList:
   169  					if pseudo.Kind == css_ast.PseudoClassGlobal || pseudo.Kind == css_ast.PseudoClassLocal {
   170  						inner := pseudo.Selectors[0].Selectors
   171  
   172  						// Replace this pseudo-class with all inner compound selectors.
   173  						// The first inner compound selector is merged with the compound
   174  						// selector before it and the last inner compound selector is
   175  						// merged with the compound selector after it:
   176  						//
   177  						// "div:local(.a .b):hover" => "div.a b:hover"
   178  						//
   179  						// This behavior is really strange since this is not how anything
   180  						// involving pseudo-classes in real CSS works at all. However, all
   181  						// other implementations (Lightning CSS, PostCSS, and Webpack) are
   182  						// consistent with this strange behavior, so we do it too.
   183  						if inner[0].Combinator.Byte == 0 {
   184  							mergeCompoundSelectors(&s, inner[0])
   185  							inner = inner[1:]
   186  						} else {
   187  							// "div:local(+ .foo):hover" => "div + .foo:hover"
   188  						}
   189  						if n := len(inner); n > 0 {
   190  							if !s.IsInvalidBecauseEmpty() {
   191  								// Don't add this selector if it consisted only of a bare ":global" or ":local"
   192  								selectors = append(selectors, s)
   193  							}
   194  							selectors = append(selectors, inner[:n-1]...)
   195  							s = inner[n-1]
   196  						}
   197  						continue
   198  					}
   199  				}
   200  
   201  				s.SubclassSelectors = append(s.SubclassSelectors, ss)
   202  			}
   203  
   204  			if !s.IsInvalidBecauseEmpty() {
   205  				// Don't add this selector if it consisted only of a bare ":global" or ":local"
   206  				selectors = append(selectors, s)
   207  			}
   208  		}
   209  
   210  		if len(selectors) == 0 {
   211  			// Treat a bare ":global" or ":local" as a bare "&" nesting selector
   212  			selectors = append(selectors, css_ast.CompoundSelector{
   213  				NestingSelectorLoc:        ast.MakeIndex32(uint32(sel.Selectors[0].Range().Loc.Start)),
   214  				WasEmptyFromLocalOrGlobal: true,
   215  			})
   216  
   217  			// Make sure we report that nesting is present so that it can be lowered
   218  			p.nestingIsPresent = true
   219  		}
   220  
   221  		sel.Selectors = selectors
   222  	}
   223  
   224  	return append(list, sel)
   225  }
   226  
   227  type leadingAmpersand uint8
   228  
   229  const (
   230  	cannotRemoveLeadingAmpersand leadingAmpersand = iota
   231  	canAlwaysRemoveLeadingAmpersand
   232  	canRemoveLeadingAmpersandIfNotFirst
   233  )
   234  
   235  func analyzeLeadingAmpersand(sel css_ast.ComplexSelector, isDeclarationContext bool) leadingAmpersand {
   236  	if len(sel.Selectors) > 1 {
   237  		if first := sel.Selectors[0]; first.IsSingleAmpersand() {
   238  			if second := sel.Selectors[1]; second.Combinator.Byte == 0 && second.HasNestingSelector() {
   239  				// ".foo { & &.bar {} }" => ".foo { & &.bar {} }"
   240  			} else if second.Combinator.Byte != 0 || second.TypeSelector == nil || !isDeclarationContext {
   241  				// "& + div {}" => "+ div {}"
   242  				// "& div {}" => "div {}"
   243  				// ".foo { & + div {} }" => ".foo { + div {} }"
   244  				// ".foo { & + &.bar {} }" => ".foo { + &.bar {} }"
   245  				// ".foo { & :hover {} }" => ".foo { :hover {} }"
   246  				return canAlwaysRemoveLeadingAmpersand
   247  			} else {
   248  				// ".foo { & div {} }"
   249  				// ".foo { .bar, & div {} }" => ".foo { .bar, div {} }"
   250  				return canRemoveLeadingAmpersandIfNotFirst
   251  			}
   252  		}
   253  	} else {
   254  		// "& {}" => "& {}"
   255  	}
   256  	return cannotRemoveLeadingAmpersand
   257  }
   258  
   259  type parseComplexSelectorOpts struct {
   260  	parseSelectorOpts
   261  	isFirst bool
   262  }
   263  
   264  func (p *parser) parseComplexSelector(opts parseComplexSelectorOpts) (result css_ast.ComplexSelector, ok bool) {
   265  	// This is an extension: https://drafts.csswg.org/css-nesting-1/
   266  	var combinator css_ast.Combinator
   267  	if !opts.noLeadingCombinator {
   268  		combinator = p.parseCombinator()
   269  		if combinator.Byte != 0 {
   270  			p.nestingIsPresent = true
   271  			p.eat(css_lexer.TWhitespace)
   272  		}
   273  	}
   274  
   275  	// Parent
   276  	sel, good := p.parseCompoundSelector(parseComplexSelectorOpts{
   277  		parseSelectorOpts: opts.parseSelectorOpts,
   278  		isFirst:           opts.isFirst,
   279  	})
   280  	if !good {
   281  		return
   282  	}
   283  	sel.Combinator = combinator
   284  	result.Selectors = append(result.Selectors, sel)
   285  
   286  	stop := css_lexer.TOpenBrace
   287  	if opts.stopOnCloseParen {
   288  		stop = css_lexer.TCloseParen
   289  	}
   290  	for {
   291  		p.eat(css_lexer.TWhitespace)
   292  		if p.peek(css_lexer.TEndOfFile) || p.peek(css_lexer.TComma) || p.peek(stop) {
   293  			break
   294  		}
   295  
   296  		// Optional combinator
   297  		combinator := p.parseCombinator()
   298  		if combinator.Byte != 0 {
   299  			p.eat(css_lexer.TWhitespace)
   300  		}
   301  
   302  		// Child
   303  		sel, good := p.parseCompoundSelector(parseComplexSelectorOpts{
   304  			parseSelectorOpts: opts.parseSelectorOpts,
   305  		})
   306  		if !good {
   307  			return
   308  		}
   309  		sel.Combinator = combinator
   310  		result.Selectors = append(result.Selectors, sel)
   311  	}
   312  
   313  	ok = true
   314  	return
   315  }
   316  
   317  func (p *parser) nameToken() css_ast.NameToken {
   318  	t := p.current()
   319  	return css_ast.NameToken{
   320  		Kind:  t.Kind,
   321  		Range: t.Range,
   322  		Text:  p.decoded(),
   323  	}
   324  }
   325  
   326  func (p *parser) parseCompoundSelector(opts parseComplexSelectorOpts) (sel css_ast.CompoundSelector, ok bool) {
   327  	startLoc := p.current().Range.Loc
   328  
   329  	// This is an extension: https://drafts.csswg.org/css-nesting-1/
   330  	hasLeadingNestingSelector := p.peek(css_lexer.TDelimAmpersand)
   331  	if hasLeadingNestingSelector {
   332  		p.nestingIsPresent = true
   333  		sel.NestingSelectorLoc = ast.MakeIndex32(uint32(startLoc.Start))
   334  		p.advance()
   335  	}
   336  
   337  	// Parse the type selector
   338  	typeSelectorLoc := p.current().Range.Loc
   339  	switch p.current().Kind {
   340  	case css_lexer.TDelimBar, css_lexer.TIdent, css_lexer.TDelimAsterisk:
   341  		nsName := css_ast.NamespacedName{}
   342  		if !p.peek(css_lexer.TDelimBar) {
   343  			nsName.Name = p.nameToken()
   344  			p.advance()
   345  		} else {
   346  			// Hack: Create an empty "identifier" to represent this
   347  			nsName.Name.Kind = css_lexer.TIdent
   348  		}
   349  		if p.eat(css_lexer.TDelimBar) {
   350  			if !p.peek(css_lexer.TIdent) && !p.peek(css_lexer.TDelimAsterisk) {
   351  				p.expect(css_lexer.TIdent)
   352  				return
   353  			}
   354  			prefix := nsName.Name
   355  			nsName.NamespacePrefix = &prefix
   356  			nsName.Name = p.nameToken()
   357  			p.advance()
   358  		}
   359  		sel.TypeSelector = &nsName
   360  	}
   361  
   362  	// Parse the subclass selectors
   363  subclassSelectors:
   364  	for {
   365  		subclassToken := p.current()
   366  
   367  		switch subclassToken.Kind {
   368  		case css_lexer.THash:
   369  			if (subclassToken.Flags & css_lexer.IsID) == 0 {
   370  				break subclassSelectors
   371  			}
   372  			nameLoc := logger.Loc{Start: subclassToken.Range.Loc.Start + 1}
   373  			name := p.decoded()
   374  			sel.SubclassSelectors = append(sel.SubclassSelectors, css_ast.SubclassSelector{
   375  				Range: subclassToken.Range,
   376  				Data: &css_ast.SSHash{
   377  					Name: p.symbolForName(nameLoc, name),
   378  				},
   379  			})
   380  			p.advance()
   381  
   382  		case css_lexer.TDelimDot:
   383  			p.advance()
   384  			nameRange := p.current().Range
   385  			name := p.decoded()
   386  			sel.SubclassSelectors = append(sel.SubclassSelectors, css_ast.SubclassSelector{
   387  				Range: logger.Range{Loc: subclassToken.Range.Loc, Len: nameRange.End() - subclassToken.Range.Loc.Start},
   388  				Data: &css_ast.SSClass{
   389  					Name: p.symbolForName(nameRange.Loc, name),
   390  				},
   391  			})
   392  			if !p.expect(css_lexer.TIdent) {
   393  				return
   394  			}
   395  
   396  		case css_lexer.TOpenBracket:
   397  			attr, r := p.parseAttributeSelector()
   398  			if r.Len == 0 {
   399  				return
   400  			}
   401  			sel.SubclassSelectors = append(sel.SubclassSelectors, css_ast.SubclassSelector{
   402  				Range: r,
   403  				Data:  &attr,
   404  			})
   405  
   406  		case css_lexer.TColon:
   407  			if p.next().Kind == css_lexer.TColon {
   408  				// Special-case the start of the pseudo-element selector section
   409  				for p.current().Kind == css_lexer.TColon {
   410  					firstColonLoc := p.current().Range.Loc
   411  					isElement := p.next().Kind == css_lexer.TColon
   412  					if isElement {
   413  						p.advance()
   414  					}
   415  					pseudo, r := p.parsePseudoClassSelector(firstColonLoc, isElement)
   416  
   417  					// https://www.w3.org/TR/selectors-4/#single-colon-pseudos
   418  					// The four Level 2 pseudo-elements (::before, ::after, ::first-line,
   419  					// and ::first-letter) may, for legacy reasons, be represented using
   420  					// the <pseudo-class-selector> grammar, with only a single ":"
   421  					// character at their start.
   422  					if p.options.minifySyntax && isElement {
   423  						if pseudo, ok := pseudo.(*css_ast.SSPseudoClass); ok && len(pseudo.Args) == 0 {
   424  							switch pseudo.Name {
   425  							case "before", "after", "first-line", "first-letter":
   426  								pseudo.IsElement = false
   427  							}
   428  						}
   429  					}
   430  
   431  					sel.SubclassSelectors = append(sel.SubclassSelectors, css_ast.SubclassSelector{
   432  						Range: r,
   433  						Data:  pseudo,
   434  					})
   435  				}
   436  				break subclassSelectors
   437  			}
   438  
   439  			pseudo, r := p.parsePseudoClassSelector(subclassToken.Range.Loc, false)
   440  			sel.SubclassSelectors = append(sel.SubclassSelectors, css_ast.SubclassSelector{
   441  				Range: r,
   442  				Data:  pseudo,
   443  			})
   444  
   445  		case css_lexer.TDelimAmpersand:
   446  			// This is an extension: https://drafts.csswg.org/css-nesting-1/
   447  			p.nestingIsPresent = true
   448  			sel.NestingSelectorLoc = ast.MakeIndex32(uint32(subclassToken.Range.Loc.Start))
   449  			p.advance()
   450  
   451  		default:
   452  			break subclassSelectors
   453  		}
   454  	}
   455  
   456  	// The compound selector must be non-empty
   457  	if sel.IsInvalidBecauseEmpty() {
   458  		p.unexpected()
   459  		return
   460  	}
   461  
   462  	// Note: "&div {}" was originally valid, but is now an invalid selector:
   463  	// https://github.com/w3c/csswg-drafts/issues/8662#issuecomment-1514977935.
   464  	// This is because SASS already uses that syntax to mean something very
   465  	// different, so that syntax has been removed to avoid mistakes.
   466  	if hasLeadingNestingSelector && sel.TypeSelector != nil {
   467  		r := logger.Range{Loc: typeSelectorLoc, Len: p.at(p.index-1).Range.End() - typeSelectorLoc.Start}
   468  		text := sel.TypeSelector.Name.Text
   469  		if sel.TypeSelector.NamespacePrefix != nil {
   470  			text = fmt.Sprintf("%s|%s", sel.TypeSelector.NamespacePrefix.Text, text)
   471  		}
   472  		var howToFix string
   473  		suggestion := p.source.TextForRange(r)
   474  		if opts.isFirst {
   475  			suggestion = fmt.Sprintf(":is(%s)", suggestion)
   476  			howToFix = "You can wrap this selector in \":is(...)\" as a workaround. "
   477  		} else {
   478  			r = logger.Range{Loc: startLoc, Len: r.End() - startLoc.Start}
   479  			suggestion += "&"
   480  			howToFix = "You can move the \"&\" to the end of this selector as a workaround. "
   481  		}
   482  		msg := logger.Msg{
   483  			Kind: logger.Warning,
   484  			Data: p.tracker.MsgData(r, fmt.Sprintf("Cannot use type selector %q directly after nesting selector \"&\"", text)),
   485  			Notes: []logger.MsgData{{Text: "CSS nesting syntax does not allow the \"&\" selector to come before a type selector. " +
   486  				howToFix +
   487  				"This restriction exists to avoid problems with SASS nesting, where the same syntax means something very different " +
   488  				"that has no equivalent in real CSS (appending a suffix to the parent selector)."}},
   489  		}
   490  		msg.Data.Location.Suggestion = suggestion
   491  		p.log.AddMsgID(logger.MsgID_CSS_CSSSyntaxError, msg)
   492  		return
   493  	}
   494  
   495  	// The type selector must always come first
   496  	switch p.current().Kind {
   497  	case css_lexer.TDelimBar, css_lexer.TIdent, css_lexer.TDelimAsterisk:
   498  		p.unexpected()
   499  		return
   500  	}
   501  
   502  	ok = true
   503  	return
   504  }
   505  
   506  func (p *parser) parseAttributeSelector() (attr css_ast.SSAttribute, r logger.Range) {
   507  	matchingLoc := p.current().Range.Loc
   508  	p.advance()
   509  
   510  	// Parse the namespaced name
   511  	switch p.current().Kind {
   512  	case css_lexer.TDelimBar, css_lexer.TDelimAsterisk:
   513  		// "[|x]"
   514  		// "[*|x]"
   515  		if p.peek(css_lexer.TDelimAsterisk) {
   516  			prefix := p.nameToken()
   517  			p.advance()
   518  			attr.NamespacedName.NamespacePrefix = &prefix
   519  		} else {
   520  			// "[|attr]" is equivalent to "[attr]". From the specification:
   521  			// "In keeping with the Namespaces in the XML recommendation, default
   522  			// namespaces do not apply to attributes, therefore attribute selectors
   523  			// without a namespace component apply only to attributes that have no
   524  			// namespace (equivalent to |attr)."
   525  		}
   526  		if !p.expect(css_lexer.TDelimBar) {
   527  			return
   528  		}
   529  		attr.NamespacedName.Name = p.nameToken()
   530  		if !p.expect(css_lexer.TIdent) {
   531  			return
   532  		}
   533  
   534  	default:
   535  		// "[x]"
   536  		// "[x|y]"
   537  		attr.NamespacedName.Name = p.nameToken()
   538  		if !p.expect(css_lexer.TIdent) {
   539  			return
   540  		}
   541  		if p.next().Kind != css_lexer.TDelimEquals && p.eat(css_lexer.TDelimBar) {
   542  			prefix := attr.NamespacedName.Name
   543  			attr.NamespacedName.NamespacePrefix = &prefix
   544  			attr.NamespacedName.Name = p.nameToken()
   545  			if !p.expect(css_lexer.TIdent) {
   546  				return
   547  			}
   548  		}
   549  	}
   550  
   551  	// Parse the optional matcher operator
   552  	p.eat(css_lexer.TWhitespace)
   553  	if p.eat(css_lexer.TDelimEquals) {
   554  		attr.MatcherOp = "="
   555  	} else {
   556  		switch p.current().Kind {
   557  		case css_lexer.TDelimTilde:
   558  			attr.MatcherOp = "~="
   559  		case css_lexer.TDelimBar:
   560  			attr.MatcherOp = "|="
   561  		case css_lexer.TDelimCaret:
   562  			attr.MatcherOp = "^="
   563  		case css_lexer.TDelimDollar:
   564  			attr.MatcherOp = "$="
   565  		case css_lexer.TDelimAsterisk:
   566  			attr.MatcherOp = "*="
   567  		}
   568  		if attr.MatcherOp != "" {
   569  			p.advance()
   570  			if !p.expect(css_lexer.TDelimEquals) {
   571  				return
   572  			}
   573  		}
   574  	}
   575  
   576  	// Parse the optional matcher value
   577  	if attr.MatcherOp != "" {
   578  		p.eat(css_lexer.TWhitespace)
   579  		if !p.peek(css_lexer.TString) && !p.peek(css_lexer.TIdent) {
   580  			p.unexpected()
   581  		}
   582  		attr.MatcherValue = p.decoded()
   583  		p.advance()
   584  		p.eat(css_lexer.TWhitespace)
   585  		if p.peek(css_lexer.TIdent) {
   586  			if modifier := p.decoded(); len(modifier) == 1 {
   587  				if c := modifier[0]; c == 'i' || c == 'I' || c == 's' || c == 'S' {
   588  					attr.MatcherModifier = c
   589  					p.advance()
   590  				}
   591  			}
   592  		}
   593  	}
   594  
   595  	closeRange := p.current().Range
   596  	if !p.expectWithMatchingLoc(css_lexer.TCloseBracket, matchingLoc) {
   597  		closeRange.Len = 0
   598  	}
   599  	r = logger.Range{Loc: matchingLoc, Len: closeRange.End() - matchingLoc.Start}
   600  	return
   601  }
   602  
   603  func (p *parser) parsePseudoClassSelector(loc logger.Loc, isElement bool) (css_ast.SS, logger.Range) {
   604  	p.advance()
   605  
   606  	if p.peek(css_lexer.TFunction) {
   607  		text := p.decoded()
   608  		matchingLoc := logger.Loc{Start: p.current().Range.End() - 1}
   609  		p.advance()
   610  
   611  		// Potentially parse a pseudo-class with a selector list
   612  		if !isElement {
   613  			var kind css_ast.PseudoClassKind
   614  			local := p.makeLocalSymbols
   615  			ok := true
   616  			switch text {
   617  			case "global":
   618  				kind = css_ast.PseudoClassGlobal
   619  				if p.options.symbolMode != symbolModeDisabled {
   620  					local = false
   621  				}
   622  			case "has":
   623  				kind = css_ast.PseudoClassHas
   624  			case "is":
   625  				kind = css_ast.PseudoClassIs
   626  			case "local":
   627  				kind = css_ast.PseudoClassLocal
   628  				if p.options.symbolMode != symbolModeDisabled {
   629  					local = true
   630  				}
   631  			case "not":
   632  				kind = css_ast.PseudoClassNot
   633  			case "nth-child":
   634  				kind = css_ast.PseudoClassNthChild
   635  			case "nth-last-child":
   636  				kind = css_ast.PseudoClassNthLastChild
   637  			case "nth-of-type":
   638  				kind = css_ast.PseudoClassNthOfType
   639  			case "nth-last-of-type":
   640  				kind = css_ast.PseudoClassNthLastOfType
   641  			case "where":
   642  				kind = css_ast.PseudoClassWhere
   643  			default:
   644  				ok = false
   645  			}
   646  			if ok {
   647  				old := p.index
   648  				if kind.HasNthIndex() {
   649  					p.eat(css_lexer.TWhitespace)
   650  
   651  					// Parse the "An+B" syntax
   652  					if index, ok := p.parseNthIndex(); ok {
   653  						var selectors []css_ast.ComplexSelector
   654  
   655  						// Parse the optional "of" clause
   656  						if (kind == css_ast.PseudoClassNthChild || kind == css_ast.PseudoClassNthLastChild) &&
   657  							p.peek(css_lexer.TIdent) && strings.EqualFold(p.decoded(), "of") {
   658  							p.advance()
   659  							p.eat(css_lexer.TWhitespace)
   660  
   661  							// Contain the effects of ":local" and ":global"
   662  							oldLocal := p.makeLocalSymbols
   663  							selectors, ok = p.parseSelectorList(parseSelectorOpts{
   664  								stopOnCloseParen:    true,
   665  								noLeadingCombinator: true,
   666  							})
   667  							p.makeLocalSymbols = oldLocal
   668  						}
   669  
   670  						// "2n+0" => "2n"
   671  						if p.options.minifySyntax {
   672  							index.Minify()
   673  						}
   674  
   675  						// Match the closing ")"
   676  						if ok {
   677  							closeRange := p.current().Range
   678  							if !p.expectWithMatchingLoc(css_lexer.TCloseParen, matchingLoc) {
   679  								closeRange.Len = 0
   680  							}
   681  							return &css_ast.SSPseudoClassWithSelectorList{Kind: kind, Selectors: selectors, Index: index},
   682  								logger.Range{Loc: loc, Len: closeRange.End() - loc.Start}
   683  						}
   684  					}
   685  				} else {
   686  					p.eat(css_lexer.TWhitespace)
   687  
   688  					// ":local" forces local names and ":global" forces global names
   689  					oldLocal := p.makeLocalSymbols
   690  					p.makeLocalSymbols = local
   691  					selectors, ok := p.parseSelectorList(parseSelectorOpts{
   692  						pseudoClassKind:        kind,
   693  						stopOnCloseParen:       true,
   694  						onlyOneComplexSelector: kind == css_ast.PseudoClassGlobal || kind == css_ast.PseudoClassLocal,
   695  					})
   696  					p.makeLocalSymbols = oldLocal
   697  
   698  					// Match the closing ")"
   699  					if ok {
   700  						closeRange := p.current().Range
   701  						if !p.expectWithMatchingLoc(css_lexer.TCloseParen, matchingLoc) {
   702  							closeRange.Len = 0
   703  						}
   704  						return &css_ast.SSPseudoClassWithSelectorList{Kind: kind, Selectors: selectors},
   705  							logger.Range{Loc: loc, Len: closeRange.End() - loc.Start}
   706  					}
   707  				}
   708  				p.index = old
   709  			}
   710  		}
   711  
   712  		args := p.convertTokens(p.parseAnyValue())
   713  		closeRange := p.current().Range
   714  		if !p.expectWithMatchingLoc(css_lexer.TCloseParen, matchingLoc) {
   715  			closeRange.Len = 0
   716  		}
   717  		return &css_ast.SSPseudoClass{IsElement: isElement, Name: text, Args: args},
   718  			logger.Range{Loc: loc, Len: closeRange.End() - loc.Start}
   719  	}
   720  
   721  	nameRange := p.current().Range
   722  	name := p.decoded()
   723  	sel := css_ast.SSPseudoClass{IsElement: isElement}
   724  	if p.expect(css_lexer.TIdent) {
   725  		sel.Name = name
   726  
   727  		// ":local .local_name :global .global_name {}"
   728  		// ":local { .local_name { :global { .global_name {} } }"
   729  		if p.options.symbolMode != symbolModeDisabled {
   730  			switch name {
   731  			case "local":
   732  				p.makeLocalSymbols = true
   733  			case "global":
   734  				p.makeLocalSymbols = false
   735  			}
   736  		}
   737  	} else {
   738  		nameRange.Len = 0
   739  	}
   740  	return &sel, logger.Range{Loc: loc, Len: nameRange.End() - loc.Start}
   741  }
   742  
   743  func (p *parser) parseAnyValue() []css_lexer.Token {
   744  	// Reference: https://drafts.csswg.org/css-syntax-3/#typedef-declaration-value
   745  
   746  	p.stack = p.stack[:0] // Reuse allocated memory
   747  	start := p.index
   748  
   749  loop:
   750  	for {
   751  		switch p.current().Kind {
   752  		case css_lexer.TCloseParen, css_lexer.TCloseBracket, css_lexer.TCloseBrace:
   753  			last := len(p.stack) - 1
   754  			if last < 0 || !p.peek(p.stack[last]) {
   755  				break loop
   756  			}
   757  			p.stack = p.stack[:last]
   758  
   759  		case css_lexer.TSemicolon, css_lexer.TDelimExclamation:
   760  			if len(p.stack) == 0 {
   761  				break loop
   762  			}
   763  
   764  		case css_lexer.TOpenParen, css_lexer.TFunction:
   765  			p.stack = append(p.stack, css_lexer.TCloseParen)
   766  
   767  		case css_lexer.TOpenBracket:
   768  			p.stack = append(p.stack, css_lexer.TCloseBracket)
   769  
   770  		case css_lexer.TOpenBrace:
   771  			p.stack = append(p.stack, css_lexer.TCloseBrace)
   772  
   773  		case css_lexer.TEndOfFile:
   774  			break loop
   775  		}
   776  
   777  		p.advance()
   778  	}
   779  
   780  	tokens := p.tokens[start:p.index]
   781  	if len(tokens) == 0 {
   782  		p.unexpected()
   783  	}
   784  	return tokens
   785  }
   786  
   787  func (p *parser) parseCombinator() css_ast.Combinator {
   788  	t := p.current()
   789  
   790  	switch t.Kind {
   791  	case css_lexer.TDelimGreaterThan:
   792  		p.advance()
   793  		return css_ast.Combinator{Loc: t.Range.Loc, Byte: '>'}
   794  
   795  	case css_lexer.TDelimPlus:
   796  		p.advance()
   797  		return css_ast.Combinator{Loc: t.Range.Loc, Byte: '+'}
   798  
   799  	case css_lexer.TDelimTilde:
   800  		p.advance()
   801  		return css_ast.Combinator{Loc: t.Range.Loc, Byte: '~'}
   802  
   803  	default:
   804  		return css_ast.Combinator{}
   805  	}
   806  }
   807  
   808  func parseInteger(text string) (string, bool) {
   809  	n := len(text)
   810  	if n == 0 {
   811  		return "", false
   812  	}
   813  
   814  	// Trim leading zeros
   815  	start := 0
   816  	for start < n && text[start] == '0' {
   817  		start++
   818  	}
   819  
   820  	// Make sure remaining characters are digits
   821  	if start == n {
   822  		return "0", true
   823  	}
   824  	for i := start; i < n; i++ {
   825  		if c := text[i]; c < '0' || c > '9' {
   826  			return "", false
   827  		}
   828  	}
   829  	return text[start:], true
   830  }
   831  
   832  func (p *parser) parseNthIndex() (css_ast.NthIndex, bool) {
   833  	type sign uint8
   834  	const (
   835  		none sign = iota
   836  		negative
   837  		positive
   838  	)
   839  
   840  	// Reference: https://drafts.csswg.org/css-syntax-3/#anb-microsyntax
   841  	t0 := p.current()
   842  	text0 := p.decoded()
   843  
   844  	// Handle "even" and "odd"
   845  	if t0.Kind == css_lexer.TIdent && (text0 == "even" || text0 == "odd") {
   846  		p.advance()
   847  		p.eat(css_lexer.TWhitespace)
   848  		return css_ast.NthIndex{B: text0}, true
   849  	}
   850  
   851  	// Handle a single number
   852  	if t0.Kind == css_lexer.TNumber {
   853  		bNeg := false
   854  		if strings.HasPrefix(text0, "-") {
   855  			bNeg = true
   856  			text0 = text0[1:]
   857  		} else {
   858  			text0 = strings.TrimPrefix(text0, "+")
   859  		}
   860  		if b, ok := parseInteger(text0); ok {
   861  			if bNeg {
   862  				b = "-" + b
   863  			}
   864  			p.advance()
   865  			p.eat(css_lexer.TWhitespace)
   866  			return css_ast.NthIndex{B: b}, true
   867  		}
   868  		p.unexpected()
   869  		return css_ast.NthIndex{}, false
   870  	}
   871  
   872  	aSign := none
   873  	if p.eat(css_lexer.TDelimPlus) {
   874  		aSign = positive
   875  		t0 = p.current()
   876  		text0 = p.decoded()
   877  	}
   878  
   879  	// Everything from here must be able to contain an "n"
   880  	if t0.Kind != css_lexer.TIdent && t0.Kind != css_lexer.TDimension {
   881  		p.unexpected()
   882  		return css_ast.NthIndex{}, false
   883  	}
   884  
   885  	// Check for a leading sign
   886  	if aSign == none {
   887  		if strings.HasPrefix(text0, "-") {
   888  			aSign = negative
   889  			text0 = text0[1:]
   890  		} else {
   891  			text0 = strings.TrimPrefix(text0, "+")
   892  		}
   893  	}
   894  
   895  	// The string must contain an "n"
   896  	n := strings.IndexByte(text0, 'n')
   897  	if n < 0 {
   898  		p.unexpected()
   899  		return css_ast.NthIndex{}, false
   900  	}
   901  
   902  	// Parse the number before the "n"
   903  	var a string
   904  	if n == 0 {
   905  		if aSign == negative {
   906  			a = "-1"
   907  		} else {
   908  			a = "1"
   909  		}
   910  	} else if aInt, ok := parseInteger(text0[:n]); ok {
   911  		if aSign == negative {
   912  			aInt = "-" + aInt
   913  		}
   914  		a = aInt
   915  	} else {
   916  		p.unexpected()
   917  		return css_ast.NthIndex{}, false
   918  	}
   919  	text0 = text0[n+1:]
   920  
   921  	// Parse the stuff after the "n"
   922  	bSign := none
   923  	if strings.HasPrefix(text0, "-") {
   924  		text0 = text0[1:]
   925  		if b, ok := parseInteger(text0); ok {
   926  			p.advance()
   927  			p.eat(css_lexer.TWhitespace)
   928  			return css_ast.NthIndex{A: a, B: "-" + b}, true
   929  		}
   930  		bSign = negative
   931  	}
   932  	if text0 != "" {
   933  		p.unexpected()
   934  		return css_ast.NthIndex{}, false
   935  	}
   936  	p.advance()
   937  	p.eat(css_lexer.TWhitespace)
   938  
   939  	// Parse an optional sign delimiter
   940  	if bSign == none {
   941  		if p.eat(css_lexer.TDelimMinus) {
   942  			bSign = negative
   943  			p.eat(css_lexer.TWhitespace)
   944  		} else if p.eat(css_lexer.TDelimPlus) {
   945  			bSign = positive
   946  			p.eat(css_lexer.TWhitespace)
   947  		}
   948  	}
   949  
   950  	// Parse an optional trailing number
   951  	t1 := p.current()
   952  	text1 := p.decoded()
   953  	if t1.Kind == css_lexer.TNumber {
   954  		if bSign == none {
   955  			if strings.HasPrefix(text1, "-") {
   956  				bSign = negative
   957  				text1 = text1[1:]
   958  			} else if strings.HasPrefix(text1, "+") {
   959  				text1 = text1[1:]
   960  			}
   961  		}
   962  		if b, ok := parseInteger(text1); ok {
   963  			if bSign == negative {
   964  				b = "-" + b
   965  			}
   966  			p.advance()
   967  			p.eat(css_lexer.TWhitespace)
   968  			return css_ast.NthIndex{A: a, B: b}, true
   969  		}
   970  	}
   971  
   972  	// If there is a trailing sign, then there must also be a trailing number
   973  	if bSign != none {
   974  		p.expect(css_lexer.TNumber)
   975  		return css_ast.NthIndex{}, false
   976  	}
   977  
   978  	return css_ast.NthIndex{A: a}, true
   979  }