Files

1581 lines
42 KiB
Go

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 <number> 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 <relative-selector-list> 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 <pseudo-class-selector> 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
}