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]

	for i, t := range p.tokens[p.index:] {
		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 {
				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()
		}
	}
}
