package css_ast import ( "strconv" "strings" "github.com/evanw/esbuild/internal/ast" "github.com/evanw/esbuild/internal/css_lexer" "github.com/evanw/esbuild/internal/helpers" "github.com/evanw/esbuild/internal/logger" ) // CSS syntax comes in two layers: a minimal syntax that generally accepts // anything that looks vaguely like CSS, and a large set of built-in rules // (the things browsers actually interpret). That way CSS parsers can read // unknown rules and skip over them without having to stop due to errors. // // This AST format is mostly just the minimal syntax. It parses unknown rules // into a tree with enough information that it can write them back out again. // There are some additional layers of syntax including selectors and @-rules // which allow for better pretty-printing and minification. // // Most of the AST just references ranges of the original file by keeping the // original "Token" values around from the lexer. This is a memory-efficient // representation that helps provide good parsing and printing performance. type AST struct { Symbols []ast.Symbol CharFreq *ast.CharFreq ImportRecords []ast.ImportRecord Rules []Rule SourceMapComment logger.Span ApproximateLineCount int32 LocalSymbols []ast.LocRef LocalScope map[string]ast.LocRef GlobalScope map[string]ast.LocRef Composes map[ast.Ref]*Composes // These contain all layer names in the file. It can be used to replace the // layer-related side effects of importing this file. They are split into two // groups (those before and after "@import" rules) so that the linker can put // them in the right places. LayersPreImport [][]string LayersPostImport [][]string } type Composes struct { // Note that each of these can be either local or global. Local examples: // // .foo { composes: bar } // .bar { color: red } // // Global examples: // // .foo { composes: bar from global } // .foo :global { composes: bar } // .foo { :global { composes: bar } } // :global .bar { color: red } // Names []ast.LocRef // Each of these is local in another file. For example: // // .foo { composes: bar from "bar.css" } // .foo { composes: bar from url(bar.css) } // ImportedNames []ImportedComposesName // This tracks what CSS properties each class uses so that we can warn when // "composes" is used incorrectly to compose two classes from separate files // that declare the same CSS properties. Properties map[string]logger.Loc } type ImportedComposesName struct { Alias string AliasLoc logger.Loc ImportRecordIndex uint32 } // We create a lot of tokens, so make sure this layout is memory-efficient. // The layout here isn't optimal because it biases for convenience (e.g. // "string" could be shorter) but at least the ordering of fields was // deliberately chosen to minimize size. type Token struct { // Contains the child tokens for component values that are simple blocks. // These are either "(", "{", "[", or function tokens. The closing token is // implicit and is not stored. Children *[]Token // 8 bytes // This is the raw contents of the token most of the time. However, it // contains the decoded string contents for "TString" tokens. Text string // 16 bytes // The source location at the start of the token Loc logger.Loc // 4 bytes // URL tokens have an associated import record at the top-level of the AST. // This index points to that import record. // // Symbol tokens have an associated symbol. This index is the "InnerIndex" // of the "Ref" for this symbol. The "SourceIndex" for the "Ref" is just // the source index of the file for this AST. PayloadIndex uint32 // 4 bytes // The division between the number and the unit for "TDimension" tokens. UnitOffset uint16 // 2 bytes // This will never be "TWhitespace" because whitespace isn't stored as a // token directly. Instead it is stored in "HasWhitespaceAfter" on the // previous token. This is to make it easier to pattern-match against // tokens when handling CSS rules, since whitespace almost always doesn't // matter. That way you can pattern match against e.g. "rgb(r, g, b)" and // not have to handle all possible combinations of embedded whitespace // tokens. // // There is one exception to this: when in verbatim whitespace mode and // the token list is non-empty and is only whitespace tokens. In that case // a single whitespace token is emitted. This is because otherwise there // would be no tokens to attach the whitespace before/after flags to. Kind css_lexer.T // 1 byte // These flags indicate the presence of a "TWhitespace" token before or after // this token. There should be whitespace printed between two tokens if either // token indicates that there should be whitespace. Note that whitespace may // be altered by processing in certain situations (e.g. minification). Whitespace WhitespaceFlags // 1 byte } type WhitespaceFlags uint8 const ( WhitespaceBefore WhitespaceFlags = 1 << iota WhitespaceAfter ) // This is necessary when comparing tokens between two different files type CrossFileEqualityCheck struct { ImportRecordsA []ast.ImportRecord ImportRecordsB []ast.ImportRecord Symbols ast.SymbolMap SourceIndexA uint32 SourceIndexB uint32 } func (check *CrossFileEqualityCheck) RefsAreEquivalent(a ast.Ref, b ast.Ref) bool { if a == b { return true } if check == nil || check.Symbols.SymbolsForSource == nil { return false } a = ast.FollowSymbols(check.Symbols, a) b = ast.FollowSymbols(check.Symbols, b) if a == b { return true } symbolA := check.Symbols.Get(a) symbolB := check.Symbols.Get(b) return symbolA.Kind == ast.SymbolGlobalCSS && symbolB.Kind == ast.SymbolGlobalCSS && symbolA.OriginalName == symbolB.OriginalName } func (a Token) Equal(b Token, check *CrossFileEqualityCheck) bool { if a.Kind == b.Kind && a.Text == b.Text && a.Whitespace == b.Whitespace { // URLs should be compared based on the text of the associated import record // (which is what will actually be printed) instead of the original text if a.Kind == css_lexer.TURL { if check == nil { // If both tokens are in the same file, just compare the index if a.PayloadIndex != b.PayloadIndex { return false } } else { // If the tokens come from separate files, compare the import records // themselves instead of comparing the indices. This can happen when // the linker runs a "DuplicateRuleRemover" during bundling. This // doesn't compare the source indices because at this point during // linking, paths inside the bundle (e.g. due to the "copy" loader) // should have already been converted into text (e.g. the "unique key" // string). if check.ImportRecordsA[a.PayloadIndex].Path.Text != check.ImportRecordsB[b.PayloadIndex].Path.Text { return false } } } // Symbols should be compared based on the symbol reference instead of the // original text if a.Kind == css_lexer.TSymbol { if check == nil { // If both tokens are in the same file, just compare the index if a.PayloadIndex != b.PayloadIndex { return false } } else { // If the tokens come from separate files, compare the symbols themselves refA := ast.Ref{SourceIndex: check.SourceIndexA, InnerIndex: a.PayloadIndex} refB := ast.Ref{SourceIndex: check.SourceIndexB, InnerIndex: b.PayloadIndex} if !check.RefsAreEquivalent(refA, refB) { return false } } } if a.Children == nil && b.Children == nil { return true } if a.Children != nil && b.Children != nil && TokensEqual(*a.Children, *b.Children, check) { return true } } return false } func TokensEqual(a []Token, b []Token, check *CrossFileEqualityCheck) bool { if len(a) != len(b) { return false } for i, ai := range a { if !ai.Equal(b[i], check) { return false } } return true } func HashTokens(hash uint32, tokens []Token) uint32 { hash = helpers.HashCombine(hash, uint32(len(tokens))) for _, t := range tokens { hash = helpers.HashCombine(hash, uint32(t.Kind)) if t.Kind != css_lexer.TURL { hash = helpers.HashCombineString(hash, t.Text) } if t.Children != nil { hash = HashTokens(hash, *t.Children) } } return hash } func (a Token) EqualIgnoringWhitespace(b Token) bool { if a.Kind == b.Kind && a.Text == b.Text && a.PayloadIndex == b.PayloadIndex { if a.Children == nil && b.Children == nil { return true } if a.Children != nil && b.Children != nil && TokensEqualIgnoringWhitespace(*a.Children, *b.Children) { return true } } return false } func TokensEqualIgnoringWhitespace(a []Token, b []Token) bool { if len(a) != len(b) { return false } for i, c := range a { if !c.EqualIgnoringWhitespace(b[i]) { return false } } return true } func TokensAreCommaSeparated(tokens []Token) bool { if n := len(tokens); (n & 1) != 0 { for i := 1; i < n; i += 2 { if tokens[i].Kind != css_lexer.TComma { return false } } return true } return false } type PercentageFlags uint8 const ( AllowPercentageBelow0 PercentageFlags = 1 << iota AllowPercentageAbove100 AllowAnyPercentage = AllowPercentageBelow0 | AllowPercentageAbove100 ) func (t Token) NumberOrFractionForPercentage(percentReferenceRange float64, flags PercentageFlags) (float64, bool) { switch t.Kind { case css_lexer.TNumber: if f, err := strconv.ParseFloat(t.Text, 64); err == nil { return f, true } case css_lexer.TPercentage: if f, err := strconv.ParseFloat(t.PercentageValue(), 64); err == nil { if (flags&AllowPercentageBelow0) == 0 && f < 0 { return 0, true } if (flags&AllowPercentageAbove100) == 0 && f > 100 { return percentReferenceRange, true } return f / 100 * percentReferenceRange, true } } return 0, false } func (t Token) ClampedFractionForPercentage() (float64, bool) { if t.Kind == css_lexer.TPercentage { if f, err := strconv.ParseFloat(t.PercentageValue(), 64); err == nil { if f < 0 { return 0, true } if f > 100 { return 1, true } return f / 100, true } } return 0, false } // https://drafts.csswg.org/css-values-3/#lengths // For zero lengths the unit identifier is optional // (i.e. can be syntactically represented as the 0). func (t *Token) TurnLengthIntoNumberIfZero() bool { if t.Kind == css_lexer.TDimension && t.DimensionValue() == "0" { t.Kind = css_lexer.TNumber t.Text = "0" return true } return false } func (t *Token) TurnLengthOrPercentageIntoNumberIfZero() bool { if t.Kind == css_lexer.TPercentage && t.PercentageValue() == "0" { t.Kind = css_lexer.TNumber t.Text = "0" return true } return t.TurnLengthIntoNumberIfZero() } func (t Token) PercentageValue() string { return t.Text[:len(t.Text)-1] } func (t Token) DimensionValue() string { return t.Text[:t.UnitOffset] } func (t Token) DimensionUnit() string { return t.Text[t.UnitOffset:] } func (t Token) DimensionUnitIsSafeLength() bool { switch strings.ToLower(t.DimensionUnit()) { // These units can be reasonably expected to be supported everywhere. // Information used: https://developer.mozilla.org/en-US/docs/Web/CSS/length case "cm", "em", "in", "mm", "pc", "pt", "px": return true } return false } func (t Token) IsZero() bool { return t.Kind == css_lexer.TNumber && t.Text == "0" } func (t Token) IsOne() bool { return t.Kind == css_lexer.TNumber && t.Text == "1" } func (t Token) IsAngle() bool { if t.Kind == css_lexer.TDimension { unit := strings.ToLower(t.DimensionUnit()) return unit == "deg" || unit == "grad" || unit == "rad" || unit == "turn" } return false } func CloneTokensWithoutImportRecords(tokensIn []Token) (tokensOut []Token) { for _, t := range tokensIn { if t.Children != nil { children := CloneTokensWithoutImportRecords(*t.Children) t.Children = &children } tokensOut = append(tokensOut, t) } return } func CloneTokensWithImportRecords( tokensIn []Token, importRecordsIn []ast.ImportRecord, tokensOut []Token, importRecordsOut []ast.ImportRecord, ) ([]Token, []ast.ImportRecord) { // Preallocate the output array if we can if tokensOut == nil { tokensOut = make([]Token, 0, len(tokensIn)) } for _, t := range tokensIn { // Clear the source mapping if this token is being used in another file t.Loc.Start = 0 // If this is a URL token, also clone the import record if t.Kind == css_lexer.TURL { importRecordIndex := uint32(len(importRecordsOut)) importRecordsOut = append(importRecordsOut, importRecordsIn[t.PayloadIndex]) t.PayloadIndex = importRecordIndex } // Also search for URL tokens in this token's children if t.Children != nil { var children []Token children, importRecordsOut = CloneTokensWithImportRecords(*t.Children, importRecordsIn, children, importRecordsOut) t.Children = &children } tokensOut = append(tokensOut, t) } return tokensOut, importRecordsOut } func CloneMediaQueriesWithImportRecords( queriesIn []MediaQuery, importRecordsIn []ast.ImportRecord, queriesOut []MediaQuery, importRecordsOut []ast.ImportRecord, ) ([]MediaQuery, []ast.ImportRecord) { // Preallocate the output array if we can if queriesOut == nil { queriesOut = make([]MediaQuery, 0, len(queriesIn)) } // Recursively clone each query for _, query := range queriesIn { query.Data, importRecordsOut = query.Data.CloneWithImportRecords(importRecordsIn, importRecordsOut) queriesOut = append(queriesOut, query) } return queriesOut, importRecordsOut } type Rule struct { Data R Loc logger.Loc } type R interface { Equal(rule R, check *CrossFileEqualityCheck) bool Hash() (uint32, bool) } func RulesEqual(a []Rule, b []Rule, check *CrossFileEqualityCheck) bool { if len(a) != len(b) { return false } for i, ai := range a { if !ai.Data.Equal(b[i].Data, check) { return false } } return true } func HashRules(hash uint32, rules []Rule) uint32 { hash = helpers.HashCombine(hash, uint32(len(rules))) for _, child := range rules { if childHash, ok := child.Data.Hash(); ok { hash = helpers.HashCombine(hash, childHash) } else { hash = helpers.HashCombine(hash, 0) } } return hash } type RAtCharset struct { Encoding string } func (a *RAtCharset) Equal(rule R, check *CrossFileEqualityCheck) bool { b, ok := rule.(*RAtCharset) return ok && a.Encoding == b.Encoding } func (r *RAtCharset) Hash() (uint32, bool) { hash := uint32(1) hash = helpers.HashCombineString(hash, r.Encoding) return hash, true } type ImportConditions struct { // The syntax for "@import" has been extended with optional conditions that // behave as if the imported file was wrapped in a "@layer", "@supports", // and/or "@media" rule. The possible syntax combinations are as follows: // // @import url(...); // @import url(...) layer; // @import url(...) layer(layer-name); // @import url(...) layer(layer-name) supports(supports-condition); // @import url(...) layer(layer-name) supports(supports-condition) list-of-media-queries; // @import url(...) layer(layer-name) list-of-media-queries; // @import url(...) supports(supports-condition); // @import url(...) supports(supports-condition) list-of-media-queries; // @import url(...) list-of-media-queries; // // From: https://developer.mozilla.org/en-US/docs/Web/CSS/@import#syntax Queries []MediaQuery // These two fields will only ever have zero or one tokens. However, they are // implemented as arrays for convenience because most of esbuild's helper // functions that operate on tokens take arrays instead of individual tokens. Layers []Token Supports []Token } func (c *ImportConditions) CloneWithImportRecords(importRecordsIn []ast.ImportRecord, importRecordsOut []ast.ImportRecord) (ImportConditions, []ast.ImportRecord) { result := ImportConditions{} result.Layers, importRecordsOut = CloneTokensWithImportRecords(c.Layers, importRecordsIn, nil, importRecordsOut) result.Supports, importRecordsOut = CloneTokensWithImportRecords(c.Supports, importRecordsIn, nil, importRecordsOut) result.Queries, importRecordsOut = CloneMediaQueriesWithImportRecords(c.Queries, importRecordsIn, nil, importRecordsOut) return result, importRecordsOut } type RAtImport struct { ImportConditions *ImportConditions ImportRecordIndex uint32 } func (*RAtImport) Equal(rule R, check *CrossFileEqualityCheck) bool { return false } func (r *RAtImport) Hash() (uint32, bool) { return 0, false } type RAtKeyframes struct { AtToken string Name ast.LocRef Blocks []KeyframeBlock CloseBraceLoc logger.Loc } type KeyframeBlock struct { Selectors []string Rules []Rule Loc logger.Loc CloseBraceLoc logger.Loc } func (a *RAtKeyframes) Equal(rule R, check *CrossFileEqualityCheck) bool { if b, ok := rule.(*RAtKeyframes); ok && strings.EqualFold(a.AtToken, b.AtToken) && check.RefsAreEquivalent(a.Name.Ref, b.Name.Ref) && len(a.Blocks) == len(b.Blocks) { for i, ai := range a.Blocks { bi := b.Blocks[i] if len(ai.Selectors) != len(bi.Selectors) { return false } for j, aj := range ai.Selectors { if aj != bi.Selectors[j] { return false } } if !RulesEqual(ai.Rules, bi.Rules, check) { return false } } return true } return false } func (r *RAtKeyframes) Hash() (uint32, bool) { hash := uint32(2) hash = helpers.HashCombineString(hash, r.AtToken) hash = helpers.HashCombine(hash, uint32(len(r.Blocks))) for _, block := range r.Blocks { hash = helpers.HashCombine(hash, uint32(len(block.Selectors))) for _, sel := range block.Selectors { hash = helpers.HashCombineString(hash, sel) } hash = HashRules(hash, block.Rules) } return hash, true } type RKnownAt struct { AtToken string Prelude []Token Rules []Rule CloseBraceLoc logger.Loc } func (a *RKnownAt) Equal(rule R, check *CrossFileEqualityCheck) bool { b, ok := rule.(*RKnownAt) return ok && strings.EqualFold(a.AtToken, b.AtToken) && TokensEqual(a.Prelude, b.Prelude, check) && RulesEqual(a.Rules, b.Rules, check) } func (r *RKnownAt) Hash() (uint32, bool) { hash := uint32(3) hash = helpers.HashCombineString(hash, r.AtToken) hash = HashTokens(hash, r.Prelude) hash = HashRules(hash, r.Rules) return hash, true } type RUnknownAt struct { AtToken string Prelude []Token Block []Token } func (a *RUnknownAt) Equal(rule R, check *CrossFileEqualityCheck) bool { b, ok := rule.(*RUnknownAt) return ok && strings.EqualFold(a.AtToken, b.AtToken) && TokensEqual(a.Prelude, b.Prelude, check) && TokensEqual(a.Block, b.Block, check) } func (r *RUnknownAt) Hash() (uint32, bool) { hash := uint32(4) hash = helpers.HashCombineString(hash, r.AtToken) hash = HashTokens(hash, r.Prelude) hash = HashTokens(hash, r.Block) return hash, true } type RSelector struct { Selectors []ComplexSelector Rules []Rule CloseBraceLoc logger.Loc } func (a *RSelector) Equal(rule R, check *CrossFileEqualityCheck) bool { b, ok := rule.(*RSelector) return ok && ComplexSelectorsEqual(a.Selectors, b.Selectors, check) && RulesEqual(a.Rules, b.Rules, check) } func (r *RSelector) Hash() (uint32, bool) { hash := uint32(5) hash = helpers.HashCombine(hash, uint32(len(r.Selectors))) hash = HashComplexSelectors(hash, r.Selectors) hash = HashRules(hash, r.Rules) return hash, true } type RQualified struct { Prelude []Token Rules []Rule CloseBraceLoc logger.Loc } func (a *RQualified) Equal(rule R, check *CrossFileEqualityCheck) bool { b, ok := rule.(*RQualified) return ok && TokensEqual(a.Prelude, b.Prelude, check) && RulesEqual(a.Rules, b.Rules, check) } func (r *RQualified) Hash() (uint32, bool) { hash := uint32(6) hash = HashTokens(hash, r.Prelude) hash = HashRules(hash, r.Rules) return hash, true } type RDeclaration struct { KeyText string Value []Token KeyRange logger.Range Key D // Compare using this instead of "Key" for speed Important bool } func (a *RDeclaration) Equal(rule R, check *CrossFileEqualityCheck) bool { b, ok := rule.(*RDeclaration) return ok && a.KeyText == b.KeyText && TokensEqual(a.Value, b.Value, check) && a.Important == b.Important } func (r *RDeclaration) Hash() (uint32, bool) { var hash uint32 if r.Key == DUnknown { if r.Important { hash = uint32(7) } else { hash = uint32(8) } hash = helpers.HashCombineString(hash, r.KeyText) } else { if r.Important { hash = uint32(9) } else { hash = uint32(10) } hash = helpers.HashCombine(hash, uint32(r.Key)) } hash = HashTokens(hash, r.Value) return hash, true } type RBadDeclaration struct { Tokens []Token } func (a *RBadDeclaration) Equal(rule R, check *CrossFileEqualityCheck) bool { b, ok := rule.(*RBadDeclaration) return ok && TokensEqual(a.Tokens, b.Tokens, check) } func (r *RBadDeclaration) Hash() (uint32, bool) { hash := uint32(7) hash = HashTokens(hash, r.Tokens) return hash, true } type RComment struct { Text string } func (a *RComment) Equal(rule R, check *CrossFileEqualityCheck) bool { b, ok := rule.(*RComment) return ok && a.Text == b.Text } func (r *RComment) Hash() (uint32, bool) { hash := uint32(8) hash = helpers.HashCombineString(hash, r.Text) return hash, true } type RAtLayer struct { Names [][]string Rules []Rule CloseBraceLoc logger.Loc } func (a *RAtLayer) Equal(rule R, check *CrossFileEqualityCheck) bool { if b, ok := rule.(*RAtLayer); ok && len(a.Names) == len(b.Names) && len(a.Rules) == len(b.Rules) { for i, ai := range a.Names { bi := b.Names[i] if len(ai) != len(bi) { return false } for j, aj := range ai { if aj != bi[j] { return false } } } if !RulesEqual(a.Rules, b.Rules, check) { return false } } return false } func (r *RAtLayer) Hash() (uint32, bool) { hash := uint32(9) hash = helpers.HashCombine(hash, uint32(len(r.Names))) for _, parts := range r.Names { hash = helpers.HashCombine(hash, uint32(len(parts))) for _, part := range parts { hash = helpers.HashCombineString(hash, part) } } hash = HashRules(hash, r.Rules) return hash, true } type RAtMedia struct { Queries []MediaQuery Rules []Rule CloseBraceLoc logger.Loc } func (a *RAtMedia) Equal(rule R, check *CrossFileEqualityCheck) bool { b, ok := rule.(*RAtMedia) return ok && MediaQueriesEqual(a.Queries, b.Queries, check) && RulesEqual(a.Rules, b.Rules, check) } func (r *RAtMedia) Hash() (uint32, bool) { hash := uint32(10) hash = HashMediaQueries(hash, r.Queries) hash = HashRules(hash, r.Rules) return hash, true } type MediaQuery struct { Loc logger.Loc Data MQ } type MQ interface { Equal(query MQ, check *CrossFileEqualityCheck) bool EqualIgnoringWhitespace(query MQ) bool Hash() uint32 CloneWithImportRecords(importRecordsIn []ast.ImportRecord, importRecordsOut []ast.ImportRecord) (MQ, []ast.ImportRecord) } func MediaQueriesEqual(a []MediaQuery, b []MediaQuery, check *CrossFileEqualityCheck) bool { if len(a) != len(b) { return false } for i, ai := range a { if !ai.Data.Equal(b[i].Data, check) { return false } } return true } func MediaQueriesEqualIgnoringWhitespace(a []MediaQuery, b []MediaQuery) bool { if len(a) != len(b) { return false } for i, ai := range a { if !ai.Data.EqualIgnoringWhitespace(b[i].Data) { return false } } return true } func HashMediaQueries(hash uint32, queries []MediaQuery) uint32 { hash = helpers.HashCombine(hash, uint32(len(queries))) for _, q := range queries { hash = helpers.HashCombine(hash, q.Data.Hash()) } return hash } type MQTypeOp uint8 const ( MQTypeOpNone MQTypeOp = iota MQTypeOpNot MQTypeOpOnly ) type MQType struct { Op MQTypeOp Type string AndOrNull MediaQuery } func (q *MQType) Equal(query MQ, check *CrossFileEqualityCheck) bool { if p, ok := query.(*MQType); ok && q.Op == p.Op && q.Type == p.Type { return (q.AndOrNull.Data == nil && p.AndOrNull.Data == nil) || (q.AndOrNull.Data != nil && p.AndOrNull.Data != nil && q.AndOrNull.Data.Equal(p.AndOrNull.Data, check)) } return false } func (q *MQType) EqualIgnoringWhitespace(query MQ) bool { if p, ok := query.(*MQType); ok && q.Op == p.Op && q.Type == p.Type { return (q.AndOrNull.Data == nil && p.AndOrNull.Data == nil) || (q.AndOrNull.Data != nil && p.AndOrNull.Data != nil && q.AndOrNull.Data.EqualIgnoringWhitespace(p.AndOrNull.Data)) } return false } func (q *MQType) Hash() uint32 { hash := uint32(0) hash = helpers.HashCombine(hash, uint32(q.Op)) hash = helpers.HashCombineString(hash, q.Type) if q.AndOrNull.Data != nil { hash = helpers.HashCombine(hash, q.AndOrNull.Data.Hash()) } return hash } func (q *MQType) CloneWithImportRecords(importRecordsIn []ast.ImportRecord, importRecordsOut []ast.ImportRecord) (MQ, []ast.ImportRecord) { var andOrNull MQ if q.AndOrNull.Data != nil { andOrNull, importRecordsOut = q.AndOrNull.Data.CloneWithImportRecords(importRecordsIn, importRecordsOut) } return &MQType{Op: q.Op, Type: q.Type, AndOrNull: MediaQuery{Data: andOrNull}}, importRecordsOut } type MQNot struct { Inner MediaQuery } func (q *MQNot) Equal(query MQ, check *CrossFileEqualityCheck) bool { p, ok := query.(*MQNot) return ok && q.Inner.Data.Equal(p.Inner.Data, check) } func (q *MQNot) EqualIgnoringWhitespace(query MQ) bool { p, ok := query.(*MQNot) return ok && q.Inner.Data.EqualIgnoringWhitespace(p.Inner.Data) } func (q *MQNot) Hash() uint32 { hash := uint32(1) hash = helpers.HashCombine(hash, q.Inner.Data.Hash()) return hash } func (q *MQNot) CloneWithImportRecords(importRecordsIn []ast.ImportRecord, importRecordsOut []ast.ImportRecord) (MQ, []ast.ImportRecord) { inner, importRecordsOut := q.Inner.Data.CloneWithImportRecords(importRecordsIn, importRecordsOut) return &MQNot{Inner: MediaQuery{Data: inner}}, importRecordsOut } type MQBinaryOp uint8 const ( MQBinaryOpAnd MQBinaryOp = iota MQBinaryOpOr ) type MQBinary struct { Op MQBinaryOp Terms []MediaQuery } func (q *MQBinary) Equal(query MQ, check *CrossFileEqualityCheck) bool { p, ok := query.(*MQBinary) return ok && q.Op == p.Op && MediaQueriesEqual(q.Terms, p.Terms, check) } func (q *MQBinary) EqualIgnoringWhitespace(query MQ) bool { p, ok := query.(*MQBinary) return ok && q.Op == p.Op && MediaQueriesEqualIgnoringWhitespace(q.Terms, p.Terms) } func (q *MQBinary) Hash() uint32 { hash := uint32(2) hash = helpers.HashCombine(hash, uint32(q.Op)) hash = HashMediaQueries(hash, q.Terms) return hash } func (q *MQBinary) CloneWithImportRecords(importRecordsIn []ast.ImportRecord, importRecordsOut []ast.ImportRecord) (MQ, []ast.ImportRecord) { terms := make([]MediaQuery, 0, len(q.Terms)) for _, term := range q.Terms { var clone MQ clone, importRecordsOut = term.Data.CloneWithImportRecords(importRecordsIn, importRecordsOut) terms = append(terms, MediaQuery{Data: clone}) } return &MQBinary{Op: q.Op, Terms: terms}, importRecordsOut } type MQArbitraryTokens struct { Tokens []Token } func (q *MQArbitraryTokens) Equal(query MQ, check *CrossFileEqualityCheck) bool { p, ok := query.(*MQArbitraryTokens) return ok && TokensEqual(q.Tokens, p.Tokens, check) } func (q *MQArbitraryTokens) EqualIgnoringWhitespace(query MQ) bool { p, ok := query.(*MQArbitraryTokens) return ok && TokensEqualIgnoringWhitespace(q.Tokens, p.Tokens) } func (q *MQArbitraryTokens) Hash() uint32 { hash := uint32(3) hash = HashTokens(hash, q.Tokens) return hash } func (q *MQArbitraryTokens) CloneWithImportRecords(importRecordsIn []ast.ImportRecord, importRecordsOut []ast.ImportRecord) (MQ, []ast.ImportRecord) { tokens, importRecordsOut := CloneTokensWithImportRecords(q.Tokens, importRecordsIn, nil, importRecordsOut) return &MQArbitraryTokens{Tokens: tokens}, importRecordsOut } type MQPlainOrBoolean struct { Name string ValueOrNil []Token } func (q *MQPlainOrBoolean) Equal(query MQ, check *CrossFileEqualityCheck) bool { p, ok := query.(*MQPlainOrBoolean) return ok && q.Name == p.Name && TokensEqual(q.ValueOrNil, p.ValueOrNil, check) } func (q *MQPlainOrBoolean) EqualIgnoringWhitespace(query MQ) bool { p, ok := query.(*MQPlainOrBoolean) return ok && q.Name == p.Name && TokensEqualIgnoringWhitespace(q.ValueOrNil, p.ValueOrNil) } func (q *MQPlainOrBoolean) Hash() uint32 { hash := uint32(4) hash = helpers.HashCombineString(hash, q.Name) hash = HashTokens(hash, q.ValueOrNil) return hash } func (q *MQPlainOrBoolean) CloneWithImportRecords(importRecordsIn []ast.ImportRecord, importRecordsOut []ast.ImportRecord) (MQ, []ast.ImportRecord) { var valueOrNil []Token if q.ValueOrNil != nil { valueOrNil, importRecordsOut = CloneTokensWithImportRecords(q.ValueOrNil, importRecordsIn, nil, importRecordsOut) } return &MQPlainOrBoolean{Name: q.Name, ValueOrNil: valueOrNil}, importRecordsOut } type MQRange struct { Before []Token Name string After []Token NameLoc logger.Loc BeforeCmp MQCmp AfterCmp MQCmp } func (q *MQRange) Equal(query MQ, check *CrossFileEqualityCheck) bool { p, ok := query.(*MQRange) return ok && q.BeforeCmp == p.BeforeCmp && q.AfterCmp == p.AfterCmp && q.Name == p.Name && TokensEqual(q.Before, p.Before, check) && TokensEqual(q.After, p.After, check) } func (q *MQRange) EqualIgnoringWhitespace(query MQ) bool { p, ok := query.(*MQRange) return ok && q.BeforeCmp == p.BeforeCmp && q.AfterCmp == p.AfterCmp && q.Name == p.Name && TokensEqualIgnoringWhitespace(q.Before, p.Before) && TokensEqualIgnoringWhitespace(q.After, p.After) } func (q *MQRange) Hash() uint32 { hash := uint32(5) hash = HashTokens(hash, q.Before) hash = helpers.HashCombine(hash, uint32(q.BeforeCmp)) hash = helpers.HashCombineString(hash, q.Name) hash = helpers.HashCombine(hash, uint32(q.AfterCmp)) hash = HashTokens(hash, q.After) return hash } func (q *MQRange) CloneWithImportRecords(importRecordsIn []ast.ImportRecord, importRecordsOut []ast.ImportRecord) (MQ, []ast.ImportRecord) { before, importRecordsOut := CloneTokensWithImportRecords(q.Before, importRecordsIn, nil, importRecordsOut) after, importRecordsOut := CloneTokensWithImportRecords(q.After, importRecordsIn, nil, importRecordsOut) return &MQRange{ Before: before, BeforeCmp: q.BeforeCmp, Name: q.Name, AfterCmp: q.AfterCmp, After: after, }, importRecordsOut } type MQCmp uint8 const ( MQCmpNone MQCmp = iota MQCmpEq MQCmpLt MQCmpLe MQCmpGt MQCmpGe ) func (cmp MQCmp) String() string { switch cmp { case MQCmpLt: return "<" case MQCmpLe: return "<=" case MQCmpGt: return ">" case MQCmpGe: return ">=" } return "=" } func (cmp MQCmp) Dir() int { switch cmp { case MQCmpLt, MQCmpLe: return -1 case MQCmpGt, MQCmpGe: return 1 } return 0 } func (cmp MQCmp) Flip() MQCmp { switch cmp { case MQCmpLt: return MQCmpGe case MQCmpLe: return MQCmpGt case MQCmpGt: return MQCmpLe case MQCmpGe: return MQCmpLt } return cmp } func (cmp MQCmp) Reverse() MQCmp { switch cmp { case MQCmpLt: return MQCmpGt case MQCmpLe: return MQCmpGe case MQCmpGt: return MQCmpLt case MQCmpGe: return MQCmpLe } return cmp } type RAtScope struct { Start []ComplexSelector End []ComplexSelector Rules []Rule CloseBraceLoc logger.Loc } func (a *RAtScope) Equal(rule R, check *CrossFileEqualityCheck) bool { b, ok := rule.(*RAtScope) return ok && ComplexSelectorsEqual(a.Start, b.Start, check) && ComplexSelectorsEqual(a.End, b.End, check) && RulesEqual(a.Rules, b.Rules, check) } func (r *RAtScope) Hash() (uint32, bool) { hash := uint32(11) hash = HashComplexSelectors(hash, r.Start) hash = HashComplexSelectors(hash, r.End) hash = HashRules(hash, r.Rules) return hash, true } type ComplexSelector struct { Selectors []CompoundSelector } func ComplexSelectorsEqual(a []ComplexSelector, b []ComplexSelector, check *CrossFileEqualityCheck) bool { if len(a) != len(b) { return false } for i, ai := range a { if !ai.Equal(b[i], check) { return false } } return true } func HashComplexSelectors(hash uint32, selectors []ComplexSelector) uint32 { for _, complex := range selectors { hash = helpers.HashCombine(hash, uint32(len(complex.Selectors))) for _, sel := range complex.Selectors { if sel.TypeSelector != nil { hash = helpers.HashCombineString(hash, sel.TypeSelector.Name.Text) } else { hash = helpers.HashCombine(hash, 0) } hash = helpers.HashCombine(hash, uint32(len(sel.SubclassSelectors))) for _, ss := range sel.SubclassSelectors { hash = helpers.HashCombine(hash, ss.Data.Hash()) } hash = helpers.HashCombine(hash, uint32(sel.Combinator.Byte)) } } return hash } func (s ComplexSelector) Clone() ComplexSelector { clone := ComplexSelector{Selectors: make([]CompoundSelector, len(s.Selectors))} for i, sel := range s.Selectors { clone.Selectors[i] = sel.Clone() } return clone } func (sel ComplexSelector) ContainsNestingCombinator() bool { for _, inner := range sel.Selectors { if len(inner.NestingSelectorLocs) > 0 { return true } for _, ss := range inner.SubclassSelectors { if pseudo, ok := ss.Data.(*SSPseudoClassWithSelectorList); ok { for _, nested := range pseudo.Selectors { if nested.ContainsNestingCombinator() { return true } } } } } return false } func (sel ComplexSelector) IsRelative() bool { // https://www.w3.org/TR/css-nesting-1/#syntax // "If a selector in the does not start with a // combinator but does contain the nesting selector, it is interpreted // as a non-relative selector." if sel.Selectors[0].Combinator.Byte == 0 && sel.ContainsNestingCombinator() { return false } return true } func tokensContainAmpersandRecursive(tokens []Token) bool { for _, t := range tokens { if t.Kind == css_lexer.TDelimAmpersand { return true } if children := t.Children; children != nil && tokensContainAmpersandRecursive(*children) { return true } } return false } func (sel ComplexSelector) UsesPseudoElement() bool { for _, sel := range sel.Selectors { for _, ss := range sel.SubclassSelectors { if class, ok := ss.Data.(*SSPseudoClass); ok { if class.IsElement { return true } // https://www.w3.org/TR/selectors-4/#single-colon-pseudos // The four Level 2 pseudo-elements (::before, ::after, ::first-line, // and ::first-letter) may, for legacy reasons, be represented using // the grammar, with only a single ":" // character at their start. switch class.Name { case "before", "after", "first-line", "first-letter": return true } } } } return false } func (a ComplexSelector) Equal(b ComplexSelector, check *CrossFileEqualityCheck) bool { if len(a.Selectors) != len(b.Selectors) { return false } for i, ai := range a.Selectors { bi := b.Selectors[i] if len(ai.NestingSelectorLocs) != len(bi.NestingSelectorLocs) || ai.Combinator.Byte != bi.Combinator.Byte { return false } if ats, bts := ai.TypeSelector, bi.TypeSelector; (ats == nil) != (bts == nil) { return false } else if ats != nil && bts != nil && !ats.Equal(*bts) { return false } if len(ai.SubclassSelectors) != len(bi.SubclassSelectors) { return false } for j, aj := range ai.SubclassSelectors { if !aj.Data.Equal(bi.SubclassSelectors[j].Data, check) { return false } } } return true } type Combinator struct { Loc logger.Loc Byte uint8 // Optional, may be 0 for no combinator } type CompoundSelector struct { TypeSelector *NamespacedName SubclassSelectors []SubclassSelector NestingSelectorLocs []logger.Loc // "&" vs. "&&" is different specificity Combinator Combinator // Optional, may be 0 // If this is true, this is a "&" that was generated by a bare ":local" or ":global" WasEmptyFromLocalOrGlobal bool } func (sel CompoundSelector) IsSingleAmpersand() bool { return len(sel.NestingSelectorLocs) == 1 && sel.Combinator.Byte == 0 && sel.TypeSelector == nil && len(sel.SubclassSelectors) == 0 } func (sel CompoundSelector) IsInvalidBecauseEmpty() bool { return len(sel.NestingSelectorLocs) == 0 && sel.TypeSelector == nil && len(sel.SubclassSelectors) == 0 } func (sel CompoundSelector) Range() (r logger.Range) { if sel.Combinator.Byte != 0 { r = logger.Range{Loc: sel.Combinator.Loc, Len: 1} } if sel.TypeSelector != nil { r.ExpandBy(sel.TypeSelector.Range()) } for _, loc := range sel.NestingSelectorLocs { r.ExpandBy(logger.Range{Loc: loc, Len: 1}) } if len(sel.SubclassSelectors) > 0 { for _, ss := range sel.SubclassSelectors { r.ExpandBy(ss.Range) } } return } func (sel CompoundSelector) Clone() CompoundSelector { clone := sel if sel.TypeSelector != nil { t := sel.TypeSelector.Clone() clone.TypeSelector = &t } if sel.SubclassSelectors != nil { selectors := make([]SubclassSelector, len(sel.SubclassSelectors)) for i, ss := range sel.SubclassSelectors { ss.Data = ss.Data.Clone() selectors[i] = ss } clone.SubclassSelectors = selectors } return clone } type NameToken struct { Text string Range logger.Range Kind css_lexer.T } func (a NameToken) Equal(b NameToken) bool { return a.Text == b.Text && a.Kind == b.Kind } type NamespacedName struct { // If present, this is an identifier or "*" and is followed by a "|" character NamespacePrefix *NameToken // This is an identifier or "*" Name NameToken } func (n NamespacedName) Range() logger.Range { if n.NamespacePrefix != nil { loc := n.NamespacePrefix.Range.Loc return logger.Range{Loc: loc, Len: n.Name.Range.End() - loc.Start} } return n.Name.Range } func (n NamespacedName) Clone() NamespacedName { clone := n if n.NamespacePrefix != nil { prefix := *n.NamespacePrefix clone.NamespacePrefix = &prefix } return clone } func (a NamespacedName) Equal(b NamespacedName) bool { return a.Name.Equal(b.Name) && (a.NamespacePrefix == nil) == (b.NamespacePrefix == nil) && (a.NamespacePrefix == nil || b.NamespacePrefix == nil || a.NamespacePrefix.Equal(b.Name)) } type SubclassSelector struct { Data SS Range logger.Range } type SS interface { Equal(ss SS, check *CrossFileEqualityCheck) bool Hash() uint32 Clone() SS } type SSHash struct { Name ast.LocRef } func (a *SSHash) Equal(ss SS, check *CrossFileEqualityCheck) bool { b, ok := ss.(*SSHash) return ok && check.RefsAreEquivalent(a.Name.Ref, b.Name.Ref) } func (ss *SSHash) Hash() uint32 { hash := uint32(1) return hash } func (ss *SSHash) Clone() SS { clone := *ss return &clone } type SSClass struct { Name ast.LocRef } func (a *SSClass) Equal(ss SS, check *CrossFileEqualityCheck) bool { b, ok := ss.(*SSClass) return ok && check.RefsAreEquivalent(a.Name.Ref, b.Name.Ref) } func (ss *SSClass) Hash() uint32 { hash := uint32(2) return hash } func (ss *SSClass) Clone() SS { clone := *ss return &clone } type SSAttribute struct { MatcherOp string // Either "" or one of: "=" "~=" "|=" "^=" "$=" "*=" MatcherValue string NamespacedName NamespacedName MatcherModifier byte // Either 0 or one of: 'i' 'I' 's' 'S' } func (a *SSAttribute) Equal(ss SS, check *CrossFileEqualityCheck) bool { b, ok := ss.(*SSAttribute) return ok && a.NamespacedName.Equal(b.NamespacedName) && a.MatcherOp == b.MatcherOp && a.MatcherValue == b.MatcherValue && a.MatcherModifier == b.MatcherModifier } func (ss *SSAttribute) Hash() uint32 { hash := uint32(3) hash = helpers.HashCombineString(hash, ss.NamespacedName.Name.Text) hash = helpers.HashCombineString(hash, ss.MatcherOp) hash = helpers.HashCombineString(hash, ss.MatcherValue) return hash } func (ss *SSAttribute) Clone() SS { clone := *ss clone.NamespacedName = ss.NamespacedName.Clone() return &clone } type SSPseudoClass struct { Name string Args []Token IsElement bool // If true, this is prefixed by "::" instead of ":" } func (a *SSPseudoClass) Equal(ss SS, check *CrossFileEqualityCheck) bool { b, ok := ss.(*SSPseudoClass) return ok && a.Name == b.Name && TokensEqual(a.Args, b.Args, check) && a.IsElement == b.IsElement } func (ss *SSPseudoClass) Hash() uint32 { hash := uint32(4) hash = helpers.HashCombineString(hash, ss.Name) hash = HashTokens(hash, ss.Args) return hash } func (ss *SSPseudoClass) Clone() SS { clone := *ss if ss.Args != nil { ss.Args = CloneTokensWithoutImportRecords(ss.Args) } return &clone } type PseudoClassKind uint8 const ( PseudoClassGlobal PseudoClassKind = iota PseudoClassHas PseudoClassIs PseudoClassLocal PseudoClassNot PseudoClassNthChild PseudoClassNthLastChild PseudoClassNthLastOfType PseudoClassNthOfType PseudoClassWhere ) func (kind PseudoClassKind) HasNthIndex() bool { return kind >= PseudoClassNthChild && kind <= PseudoClassNthOfType } func (kind PseudoClassKind) String() string { switch kind { case PseudoClassGlobal: return "global" case PseudoClassHas: return "has" case PseudoClassIs: return "is" case PseudoClassLocal: return "local" case PseudoClassNot: return "not" case PseudoClassNthChild: return "nth-child" case PseudoClassNthLastChild: return "nth-last-child" case PseudoClassNthLastOfType: return "nth-last-of-type" case PseudoClassNthOfType: return "nth-of-type" case PseudoClassWhere: return "where" default: panic("Internal error") } } // This is the "An+B" syntax type NthIndex struct { A string B string // May be "even" or "odd" } func (index *NthIndex) Minify() { // "even" => "2n" if index.B == "even" { index.A = "2" index.B = "" return } // "2n+1" => "odd" if index.A == "2" && index.B == "1" { index.A = "" index.B = "odd" return } // "0n+1" => "1" if index.A == "0" { index.A = "" if index.B == "" { // "0n" => "0" index.B = "0" } return } // "1n+0" => "1n" if index.B == "0" && index.A != "" { index.B = "" } } // See https://drafts.csswg.org/selectors/#grouping type SSPseudoClassWithSelectorList struct { Selectors []ComplexSelector Index NthIndex Kind PseudoClassKind } func (a *SSPseudoClassWithSelectorList) Equal(ss SS, check *CrossFileEqualityCheck) bool { b, ok := ss.(*SSPseudoClassWithSelectorList) return ok && a.Kind == b.Kind && a.Index == b.Index && ComplexSelectorsEqual(a.Selectors, b.Selectors, check) } func (ss *SSPseudoClassWithSelectorList) Hash() uint32 { hash := uint32(5) hash = helpers.HashCombine(hash, uint32(ss.Kind)) hash = helpers.HashCombineString(hash, ss.Index.A) hash = helpers.HashCombineString(hash, ss.Index.B) hash = HashComplexSelectors(hash, ss.Selectors) return hash } func (ss *SSPseudoClassWithSelectorList) Clone() SS { clone := *ss clone.Selectors = make([]ComplexSelector, len(ss.Selectors)) for i, sel := range ss.Selectors { clone.Selectors[i] = sel.Clone() } return &clone }