package css_parser

import (
	
	

	
	
	
	
	
)
This is mostly a normal CSS parser with one exception: the addition of support for parsing https://drafts.csswg.org/css-nesting-1/.

type parser struct {
	log           logger.Log
	source        logger.Source
	options       Options
	tokens        []css_lexer.Token
	stack         []css_lexer.T
	index         int
	end           int
	prevError     logger.Loc
	importRecords []ast.ImportRecord
}

type Options struct {
	UnsupportedCSSFeatures compat.CSSFeature
	MangleSyntax           bool
	RemoveWhitespace       bool
}

func ( logger.Log,  logger.Source,  Options) css_ast.AST {
	 := parser{
		log:       ,
		source:    ,
		options:   ,
		tokens:    css_lexer.Tokenize(, ),
		prevError: logger.Loc{Start: -1},
	}
	.end = len(.tokens)
	 := css_ast.AST{}
	.Rules = .parseListOfRules(ruleContext{
		isTopLevel:     true,
		parseSelectors: true,
	})
	.ImportRecords = .importRecords
	.expect(css_lexer.TEndOfFile)
	return 
}

func ( *parser) () {
	if .index < .end {
		.index++
	}
}

func ( *parser) ( int) css_lexer.Token {
	if  < .end {
		return .tokens[]
	}
	if .end < len(.tokens) {
		return css_lexer.Token{
			Kind:  css_lexer.TEndOfFile,
			Range: logger.Range{Loc: .tokens[.end].Range.Loc},
		}
	}
	return css_lexer.Token{
		Kind:  css_lexer.TEndOfFile,
		Range: logger.Range{Loc: logger.Loc{Start: int32(len(.source.Contents))}},
	}
}

func ( *parser) () css_lexer.Token {
	return .at(.index)
}

func ( *parser) () css_lexer.Token {
	return .at(.index + 1)
}

func ( *parser) () string {
	 := .current()
	return .source.Contents[.Range.Loc.Start:.Range.End()]
}

func ( *parser) () string {
	return .current().DecodedText(.source.Contents)
}

func ( *parser) ( css_lexer.T) bool {
	return  == .current().Kind
}

func ( *parser) ( css_lexer.T) bool {
	if .peek() {
		.advance()
		return true
	}
	return false
}

func ( *parser) ( css_lexer.T) bool {
	if .eat() {
		return true
	}
	 := .current()
	var  string
Have a nice error message for forgetting a trailing semicolon
		 = "Expected \";\""
		 = .at(.index - 1)
	} else {
		switch .Kind {
		case css_lexer.TEndOfFile, css_lexer.TWhitespace:
			 = fmt.Sprintf("Expected %s but found %s", .String(), .Kind.String())
			.Range.Len = 0
		case css_lexer.TBadURL, css_lexer.TBadString:
			 = fmt.Sprintf("Expected %s but found %s", .String(), .Kind.String())
		default:
			 = fmt.Sprintf("Expected %s but found %q", .String(), .raw())
		}
	}
	if .Range.Loc.Start > .prevError.Start {
		.log.AddRangeWarning(&.source, .Range, )
		.prevError = .Range.Loc
	}
	return false
}

func ( *parser) () {
	if  := .current(); .Range.Loc.Start > .prevError.Start {
		var  string
		switch .Kind {
		case css_lexer.TEndOfFile, css_lexer.TWhitespace:
			 = fmt.Sprintf("Unexpected %s", .Kind.String())
			.Range.Len = 0
		case css_lexer.TBadURL, css_lexer.TBadString:
			 = fmt.Sprintf("Unexpected %s", .Kind.String())
		default:
			 = fmt.Sprintf("Unexpected %q", .raw())
		}
		.log.AddRangeWarning(&.source, .Range, )
		.prevError = .Range.Loc
	}
}

type ruleContext struct {
	isTopLevel     bool
	parseSelectors bool
}

func ( *parser) ( ruleContext) []css_ast.R {
	 := false
	 := false
	 := []css_ast.R{}
	 := []logger.Loc{}

	for {
		switch .current().Kind {
		case css_lexer.TEndOfFile, css_lexer.TCloseBrace:
			return 

		case css_lexer.TWhitespace:
			.advance()
			continue

		case css_lexer.TAtKeyword:
			 := .current().Range
			 := .parseAtRule(atRuleContext{})
Validate structure
			if .isTopLevel {
				switch .(type) {
				case *css_ast.RAtCharset:
					if ! && len() > 0 {
						.log.AddRangeWarningWithNotes(&.source, , "\"@charset\" must be the first rule in the file",
							[]logger.MsgData{logger.RangeData(&.source, logger.Range{Loc: [len()-1]},
								"This rule cannot come before a \"@charset\" rule")})
						 = true
					}

				case *css_ast.RAtImport:
					if ! {
					:
						for ,  := range  {
							switch .(type) {
							case *css_ast.RAtCharset, *css_ast.RAtImport:
							default:
								.log.AddRangeWarningWithNotes(&.source, , "All \"@import\" rules must come first",
									[]logger.MsgData{logger.RangeData(&.source, logger.Range{Loc: []},
										"This rule cannot come before an \"@import\" rule")})
								 = true
								break 
							}
						}
					}
				}
			}

			 = append(, )
			if .isTopLevel {
				 = append(, .Loc)
			}
			continue

		case css_lexer.TCDO, css_lexer.TCDC:
			if .isTopLevel {
				.advance()
				continue
			}
		}

		if .isTopLevel {
			 = append(, .current().Range.Loc)
		}
		if .parseSelectors {
			 = append(, .parseSelectorRule())
		} else {
			 = append(, .parseQualifiedRuleFrom(.index, false /* isAlreadyInvalid */))
		}
	}
}

func ( *parser) () ( []css_ast.R) {
	for {
		switch .current().Kind {
		case css_lexer.TWhitespace, css_lexer.TSemicolon:
			.advance()

		case css_lexer.TEndOfFile, css_lexer.TCloseBrace:
			.processDeclarations()
			return

		case css_lexer.TAtKeyword:
			 = append(, .parseAtRule(atRuleContext{
				isDeclarationList: true,
			}))

Reference: https://drafts.csswg.org/css-nesting-1/
			 = append(, .parseSelectorRule())

		default:
			 = append(, .parseDeclaration())
		}
	}
}

func ( *parser) () (string, logger.Range, bool) {
	 := .current()
	switch .Kind {
	case css_lexer.TString:
		 := .decoded()
		.advance()
		return , .Range, true

	case css_lexer.TURL:
		 := .decoded()
		.advance()
		return , .Range, true

	case css_lexer.TFunction:
		if .decoded() == "url" {
			.advance()
			 = .current()
			 := .decoded()
			if .expect(css_lexer.TString) && .expect(css_lexer.TCloseParen) {
				return , .Range, true
			}
		}
	}

	return "", logger.Range{}, false
}

func ( *parser) () ( string,  logger.Range,  bool) {
	, ,  = .parseURLOrString()
	if ! {
		.expect(css_lexer.TURL)
	}
	return
}

type atRuleKind uint8

const (
	atRuleUnknown atRuleKind = iota
	atRuleDeclarations
	atRuleInheritContext
	atRuleEmpty
)

var specialAtRules = map[string]atRuleKind{
	"font-face": atRuleDeclarations,
	"page":      atRuleDeclarations,

	"document": atRuleInheritContext,
	"media":    atRuleInheritContext,
	"scope":    atRuleInheritContext,
	"supports": atRuleInheritContext,
}

type atRuleContext struct {
	isDeclarationList bool
}

Parse the name
	 := .decoded()
	 := .current().Range
	 := specialAtRules[]
	.advance()
Parse the prelude
	 := .index
	switch  {
	case "charset":
		 = atRuleEmpty
		.expect(css_lexer.TWhitespace)
		if .peek(css_lexer.TString) {
			 := .decoded()
			if  != "UTF-8" {
				.log.AddRangeWarning(&.source, .current().Range,
					fmt.Sprintf("\"UTF-8\" will be used instead of unsupported charset %q", ))
			}
			.advance()
			.expect(css_lexer.TSemicolon)
			return &css_ast.RAtCharset{Encoding: }
		}
		.expect(css_lexer.TString)

	case "import":
		 = atRuleEmpty
		.eat(css_lexer.TWhitespace)
		if , ,  := .expectURLOrString();  {
			.eat(css_lexer.TWhitespace)
			.expect(css_lexer.TSemicolon)
			 := uint32(len(.importRecords))
			.importRecords = append(.importRecords, ast.ImportRecord{
				Kind:  ast.ImportAt,
				Path:  logger.Path{Text: },
				Range: ,
			})
			return &css_ast.RAtImport{ImportRecordIndex: }
		}

	case "keyframes", "-webkit-keyframes", "-moz-keyframes", "-ms-keyframes", "-o-keyframes":
		.eat(css_lexer.TWhitespace)
		var  string

		if .peek(css_lexer.TIdent) {
			 = .decoded()
			.advance()
Consider string names a syntax error even though they are allowed by the specification and they work in Firefox because they do not work in Chrome or Safari.
			break
		}

		.eat(css_lexer.TWhitespace)
		if .expect(css_lexer.TOpenBrace) {
			var  []css_ast.KeyframeBlock

		:
			for {
				switch .current().Kind {
				case css_lexer.TWhitespace:
					.advance()
					continue

				case css_lexer.TCloseBrace, css_lexer.TEndOfFile:
					break 

				case css_lexer.TOpenBrace:
					.expect(css_lexer.TPercentage)
					.parseComponentValue()

				default:
					var  []string

				:
					for {
						 := .current()
						switch .Kind {
						case css_lexer.TWhitespace:
							.advance()
							continue

						case css_lexer.TOpenBrace, css_lexer.TEndOfFile:
							break 

						case css_lexer.TIdent, css_lexer.TPercentage:
							 := .decoded()
							if .Kind == css_lexer.TIdent {
								if  == "from" {
									if .options.MangleSyntax {
										 = "0%" // "0%" is equivalent to but shorter than "from"
									}
								} else if  != "to" {
									.expect(css_lexer.TPercentage)
								}
							} else if .options.MangleSyntax &&  == "100%" {
								 = "to" // "to" is equivalent to but shorter than "100%"
							}
							 = append(, )
							.advance()

						default:
							.expect(css_lexer.TPercentage)
							.parseComponentValue()
						}

						.eat(css_lexer.TWhitespace)
						if .Kind != css_lexer.TComma && !.peek(css_lexer.TOpenBrace) {
							.expect(css_lexer.TComma)
						}
					}

					if .expect(css_lexer.TOpenBrace) {
						 := .parseListOfDeclarations()
						.expect(css_lexer.TCloseBrace)
						 = append(, css_ast.KeyframeBlock{
							Selectors: ,
							Rules:     ,
						})
					}
				}
			}

			.expect(css_lexer.TCloseBrace)
			return &css_ast.RAtKeyframes{
				AtToken: ,
				Name:    ,
				Blocks:  ,
			}
		}

Warn about unsupported at-rules since they will be passed through unmodified and may be part of a CSS preprocessor syntax that should have been compiled away but wasn't. The list of supported at-rules that esbuild draws from is here: https://developer.mozilla.org/en-US/docs/Web/CSS/At-rule. Deprecated and Firefox-only at-rules have been removed.
		if  == atRuleUnknown {
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.
				.log.AddRangeWarning(&.source, , "\"@namespace\" rules are not supported")
			} else {
				.log.AddRangeWarning(&.source, , fmt.Sprintf("%q is not a known rule name", "@"+))
			}
		}
	}
Parse an unknown prelude
:
	for {
		switch .current().Kind {
		case css_lexer.TOpenBrace, css_lexer.TEndOfFile:
			break 

		case css_lexer.TSemicolon, css_lexer.TCloseBrace:
			 := .convertTokens(.tokens[:.index])
Report an error for rules that should have blocks
			if  != atRuleEmpty &&  != atRuleUnknown {
				.expect(css_lexer.TOpenBrace)
				.eat(css_lexer.TSemicolon)
				return &css_ast.RUnknownAt{AtToken: , Prelude: }
			}
Otherwise, parse an unknown at rule
			.expect(css_lexer.TSemicolon)
			return &css_ast.RUnknownAt{AtToken: , Prelude: }

		default:
			.parseComponentValue()
		}
	}
	 := .convertTokens(.tokens[:.index])
	 := .index

	switch  {
Report an error for rules that shouldn't have blocks
Parse known rules whose blocks consist of whatever the current context is
		.advance()
		 := .parseListOfDeclarations()
		.expect(css_lexer.TCloseBrace)
		return &css_ast.RKnownAt{AtToken: , Prelude: , Rules: }

Parse known rules whose blocks consist of whatever the current context is
		.advance()
		var  []css_ast.R
		if .isDeclarationList {
			 = .parseListOfDeclarations()
		} else {
			 = .parseListOfRules(ruleContext{
				parseSelectors: true,
			})
		}
		.expect(css_lexer.TCloseBrace)
		return &css_ast.RKnownAt{AtToken: , Prelude: , Rules: }

Otherwise, parse an unknown rule
		.parseBlock(css_lexer.TOpenBrace, css_lexer.TCloseBrace)
		,  := .convertTokensHelper(.tokens[:.index], css_lexer.TEndOfFile, convertTokensOpts{allowImports: true})
		return &css_ast.RUnknownAt{AtToken: , Prelude: , Block: }
	}
}

func ( *parser) ( []css_lexer.Token) []css_ast.Token {
	,  := .convertTokensHelper(, css_lexer.TEndOfFile, convertTokensOpts{})
	return 
}

type convertTokensOpts struct {
	allowImports       bool
	verbatimWhitespace bool
}

func ( *parser) ( []css_lexer.Token,  css_lexer.T,  convertTokensOpts) ([]css_ast.Token, []css_lexer.Token) {
	var  []css_ast.Token
	var  css_ast.WhitespaceFlags

:
	for len() > 0 {
		 := [0]
		 = [1:]
		if .Kind ==  {
			break 
		}
		 := css_ast.Token{
			Kind:       .Kind,
			Text:       .DecodedText(.source.Contents),
			Whitespace: ,
		}
		 = 0

		switch .Kind {
		case css_lexer.TWhitespace:
			if  := len() - 1;  >= 0 {
				[].Whitespace |= css_ast.WhitespaceAfter
			}
			 = css_ast.WhitespaceBefore
			continue

		case css_lexer.TNumber:
			if .options.MangleSyntax {
				if ,  := mangleNumber(.Text);  {
					.Text = 
				}
			}

		case css_lexer.TPercentage:
			if .options.MangleSyntax {
				if ,  := mangleNumber(.PercentValue());  {
					.Text =  + "%"
				}
			}

		case css_lexer.TDimension:
			.UnitOffset = .UnitOffset

			if .options.MangleSyntax {
				if ,  := mangleNumber(.DimensionValue());  {
					.Text =  + .DimensionUnit()
					.UnitOffset = uint16(len())
				}
			}

		case css_lexer.TURL:
			.ImportRecordIndex = uint32(len(.importRecords))
			.importRecords = append(.importRecords, ast.ImportRecord{
				Kind:     ast.ImportURL,
				Path:     logger.Path{Text: .Text},
				Range:    .Range,
				IsUnused: !.allowImports,
			})
			.Text = ""

		case css_lexer.TFunction:
			var  []css_ast.Token
			 := 
			 := 
CSS variables require verbatim whitespace for correctness
				.verbatimWhitespace = true
			}
			,  = .(, css_lexer.TCloseParen, )
			.Children = &
Treat a URL function call with a string just like a URL token
			if .Text == "url" && len() == 1 && [0].Kind == css_lexer.TString {
				.Kind = css_lexer.TURL
				.Text = ""
				.Children = nil
				.ImportRecordIndex = uint32(len(.importRecords))
				.importRecords = append(.importRecords, ast.ImportRecord{
					Kind:     ast.ImportURL,
					Path:     logger.Path{Text: [0].Text},
					Range:    [0].Range,
					IsUnused: !.allowImports,
				})
			}

		case css_lexer.TOpenParen:
			var  []css_ast.Token
			,  = .(, css_lexer.TCloseParen, )
			.Children = &

		case css_lexer.TOpenBrace:
			var  []css_ast.Token
			,  = .(, css_lexer.TCloseBrace, )
Pretty-printing: insert leading and trailing whitespace when not minifying
			if !.verbatimWhitespace && !.options.RemoveWhitespace && len() > 0 {
				[0].Whitespace |= css_ast.WhitespaceBefore
				[len()-1].Whitespace |= css_ast.WhitespaceAfter
			}

			.Children = &

		case css_lexer.TOpenBracket:
			var  []css_ast.Token
			,  = .(, css_lexer.TCloseBracket, )
			.Children = &
		}

		 = append(, )
	}

	if !.verbatimWhitespace {
		for  := range  {
			 := &[]
Always remove leading and trailing whitespace
			if  == 0 {
				.Whitespace &= ^css_ast.WhitespaceBefore
			}
			if +1 == len() {
				.Whitespace &= ^css_ast.WhitespaceAfter
			}

			switch .Kind {
Assume that whitespace can always be removed before a comma
				.Whitespace &= ^css_ast.WhitespaceBefore
				if  > 0 {
					[-1].Whitespace &= ^css_ast.WhitespaceAfter
				}
Assume whitespace can always be added after a comma
				if .options.RemoveWhitespace {
					.Whitespace &= ^css_ast.WhitespaceAfter
					if +1 < len() {
						[+1].Whitespace &= ^css_ast.WhitespaceBefore
					}
				} else {
					.Whitespace |= css_ast.WhitespaceAfter
					if +1 < len() {
						[+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 .verbatimWhitespace && len() == 0 &&  == css_ast.WhitespaceBefore {
		 = append(, css_ast.Token{
			Kind: css_lexer.TWhitespace,
		})
	}

	return , 
}

func ( string) (string, bool) {
	 := 

Remove trailing zeros
		for len() > 0 && [len()-1] == '0' {
			 = [:len()-1]
		}
Remove the decimal point if it's unnecessary
		if +1 == len() {
			 = [:]
			if  == "" ||  == "+" ||  == "-" {
				 += "0"
			}
Remove a leading zero
			if len() >= 3 && [0] == '0' && [1] == '.' && [2] >= '0' && [2] <= '9' {
				 = [1:]
			} else if len() >= 4 && ([0] == '+' || [0] == '-') && [1] == '0' && [2] == '.' && [3] >= '0' && [3] <= '9' {
				 = [0:1] + [2:]
			}
		}
	}

	return ,  != 
}

func ( *parser) () css_ast.R {
	 := .index
Try parsing the prelude as a selector list
	if ,  := .parseSelectorList();  {
		 := css_ast.RSelector{Selectors: }
		if .expect(css_lexer.TOpenBrace) {
			.Rules = .parseListOfDeclarations()
			.expect(css_lexer.TCloseBrace)
			return &
		}
	}
Otherwise, parse a generic qualified rule
	return .parseQualifiedRuleFrom(, true /* isAlreadyInvalid */)
}

func ( *parser) ( int,  bool) *css_ast.RQualified {
:
	for {
		switch .current().Kind {
		case css_lexer.TOpenBrace, css_lexer.TEndOfFile:
			break 

Error recovery if the block is omitted (likely some CSS meta-syntax)
			if ! {
				.expect(css_lexer.TOpenBrace)
			}
			 := .convertTokens(.tokens[:.index])
			.advance()
			return &css_ast.RQualified{Prelude: }

		default:
			.parseComponentValue()
		}
	}

	 := css_ast.RQualified{
		Prelude: .convertTokens(.tokens[:.index]),
	}

	if .eat(css_lexer.TOpenBrace) {
		.Rules = .parseListOfDeclarations()
		.expect(css_lexer.TCloseBrace)
	} else if ! {
		.expect(css_lexer.TOpenBrace)
	}

	return &
}

Parse the key
	 := .index
	 := false
	if .expect(css_lexer.TIdent) {
		.eat(css_lexer.TWhitespace)
		if .expect(css_lexer.TColon) {
			 = true
		}
	}
Parse the value
	 := .index
:
	for {
		switch .current().Kind {
		case css_lexer.TEndOfFile, css_lexer.TSemicolon, css_lexer.TCloseBrace:
			break 

Error recovery if there is an unexpected block (likely some CSS meta-syntax)
Stop now if this is not a valid declaration
	if ! {
		return &css_ast.RBadDeclaration{
			Tokens: .convertTokens(.tokens[:.index]),
		}
	}

	 := .tokens[]
	 := .DecodedText(.source.Contents)
	 := .tokens[:.index]
	 := strings.HasPrefix(, "--")
Remove trailing "!important"
	 := false
	 := len() - 1
	if  >= 0 && [].Kind == css_lexer.TWhitespace {
		--
	}
	if  >= 0 && [].Kind == css_lexer.TIdent && strings.EqualFold([].DecodedText(.source.Contents), "important") {
		--
		if  >= 0 && [].Kind == css_lexer.TWhitespace {
			--
		}
		if  >= 0 && [].Kind == css_lexer.TDelimExclamation {
			 = [:]
			 = true
		}
	}

	,  := .convertTokensHelper(, css_lexer.TEndOfFile, convertTokensOpts{
		allowImports: true,
CSS variables require verbatim whitespace for correctness
		verbatimWhitespace: ,
	})
Insert or remove whitespace before the first token