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

     1  package css_parser
     2  
     3  import (
     4  	"strings"
     5  
     6  	"github.com/evanw/esbuild/internal/css_ast"
     7  	"github.com/evanw/esbuild/internal/css_lexer"
     8  	"github.com/evanw/esbuild/internal/logger"
     9  )
    10  
    11  const (
    12  	boxTop = iota
    13  	boxRight
    14  	boxBottom
    15  	boxLeft
    16  )
    17  
    18  type boxSide struct {
    19  	token         css_ast.Token
    20  	unitSafety    unitSafetyTracker
    21  	ruleIndex     uint32 // The index of the originating rule in the rules array
    22  	wasSingleRule bool   // True if the originating rule was just for this side
    23  }
    24  
    25  type boxTracker struct {
    26  	keyText   string
    27  	sides     [4]boxSide
    28  	allowAuto bool // If true, allow the "auto" keyword
    29  	important bool // True if all active rules were flagged as "!important"
    30  	key       css_ast.D
    31  }
    32  
    33  type unitSafetyStatus uint8
    34  
    35  const (
    36  	unitSafe         unitSafetyStatus = iota // "margin: 0 1px 2cm 3%;"
    37  	unitUnsafeSingle                         // "margin: 0 1vw 2vw 3vw;"
    38  	unitUnsafeMixed                          // "margin: 0 1vw 2vh 3ch;"
    39  )
    40  
    41  // We can only compact rules together if they have the same unit safety level.
    42  // We want to avoid a situation where the browser treats some of the original
    43  // rules as valid and others as invalid.
    44  //
    45  //	Safe:
    46  //	  top: 1px; left: 0; bottom: 1px; right: 0;
    47  //	  top: 1Q; left: 2Q; bottom: 3Q; right: 4Q;
    48  //
    49  //	Unsafe:
    50  //	  top: 1vh; left: 2vw; bottom: 3vh; right: 4vw;
    51  //	  top: 1Q; left: 2Q; bottom: 3Q; right: 0;
    52  //	  inset: 1Q 0 0 0; top: 0;
    53  type unitSafetyTracker struct {
    54  	unit   string
    55  	status unitSafetyStatus
    56  }
    57  
    58  func (a unitSafetyTracker) isSafeWith(b unitSafetyTracker) bool {
    59  	return a.status == b.status && a.status != unitUnsafeMixed && (a.status != unitUnsafeSingle || a.unit == b.unit)
    60  }
    61  
    62  func (t *unitSafetyTracker) includeUnitOf(token css_ast.Token) {
    63  	switch token.Kind {
    64  	case css_lexer.TNumber:
    65  		if token.Text == "0" {
    66  			return
    67  		}
    68  
    69  	case css_lexer.TPercentage:
    70  		return
    71  
    72  	case css_lexer.TDimension:
    73  		if token.DimensionUnitIsSafeLength() {
    74  			return
    75  		} else if unit := token.DimensionUnit(); t.status == unitSafe {
    76  			t.status = unitUnsafeSingle
    77  			t.unit = unit
    78  			return
    79  		} else if t.status == unitUnsafeSingle && t.unit == unit {
    80  			return
    81  		}
    82  	}
    83  
    84  	t.status = unitUnsafeMixed
    85  }
    86  
    87  func (box *boxTracker) updateSide(rules []css_ast.Rule, side int, new boxSide) {
    88  	if old := box.sides[side]; old.token.Kind != css_lexer.TEndOfFile &&
    89  		(!new.wasSingleRule || old.wasSingleRule) &&
    90  		old.unitSafety.status == unitSafe && new.unitSafety.status == unitSafe {
    91  		rules[old.ruleIndex] = css_ast.Rule{}
    92  	}
    93  	box.sides[side] = new
    94  }
    95  
    96  func (box *boxTracker) mangleSides(rules []css_ast.Rule, decl *css_ast.RDeclaration, minifyWhitespace bool) {
    97  	// Reset if we see a change in the "!important" flag
    98  	if box.important != decl.Important {
    99  		box.sides = [4]boxSide{}
   100  		box.important = decl.Important
   101  	}
   102  
   103  	allowedIdent := ""
   104  	if box.allowAuto {
   105  		allowedIdent = "auto"
   106  	}
   107  	if quad, ok := expandTokenQuad(decl.Value, allowedIdent); ok {
   108  		// Use a single tracker for the whole rule
   109  		unitSafety := unitSafetyTracker{}
   110  		for _, t := range quad {
   111  			if !box.allowAuto || t.Kind.IsNumeric() {
   112  				unitSafety.includeUnitOf(t)
   113  			}
   114  		}
   115  		for side, t := range quad {
   116  			if unitSafety.status == unitSafe {
   117  				t.TurnLengthIntoNumberIfZero()
   118  			}
   119  			box.updateSide(rules, side, boxSide{
   120  				token:      t,
   121  				ruleIndex:  uint32(len(rules) - 1),
   122  				unitSafety: unitSafety,
   123  			})
   124  		}
   125  		box.compactRules(rules, decl.KeyRange, minifyWhitespace)
   126  	} else {
   127  		box.sides = [4]boxSide{}
   128  	}
   129  }
   130  
   131  func (box *boxTracker) mangleSide(rules []css_ast.Rule, decl *css_ast.RDeclaration, minifyWhitespace bool, side int) {
   132  	// Reset if we see a change in the "!important" flag
   133  	if box.important != decl.Important {
   134  		box.sides = [4]boxSide{}
   135  		box.important = decl.Important
   136  	}
   137  
   138  	if tokens := decl.Value; len(tokens) == 1 {
   139  		if t := tokens[0]; t.Kind.IsNumeric() || (t.Kind == css_lexer.TIdent && box.allowAuto && strings.EqualFold(t.Text, "auto")) {
   140  			unitSafety := unitSafetyTracker{}
   141  			if !box.allowAuto || t.Kind.IsNumeric() {
   142  				unitSafety.includeUnitOf(t)
   143  			}
   144  			if unitSafety.status == unitSafe && t.TurnLengthIntoNumberIfZero() {
   145  				tokens[0] = t
   146  			}
   147  			box.updateSide(rules, side, boxSide{
   148  				token:         t,
   149  				ruleIndex:     uint32(len(rules) - 1),
   150  				wasSingleRule: true,
   151  				unitSafety:    unitSafety,
   152  			})
   153  			box.compactRules(rules, decl.KeyRange, minifyWhitespace)
   154  			return
   155  		}
   156  	}
   157  
   158  	box.sides = [4]boxSide{}
   159  }
   160  
   161  func (box *boxTracker) compactRules(rules []css_ast.Rule, keyRange logger.Range, minifyWhitespace bool) {
   162  	// Don't compact if the shorthand form is unsupported
   163  	if box.key == css_ast.DUnknown {
   164  		return
   165  	}
   166  
   167  	// All tokens must be present
   168  	if eof := css_lexer.TEndOfFile; box.sides[0].token.Kind == eof || box.sides[1].token.Kind == eof ||
   169  		box.sides[2].token.Kind == eof || box.sides[3].token.Kind == eof {
   170  		return
   171  	}
   172  
   173  	// All tokens must have the same unit
   174  	for _, side := range box.sides[1:] {
   175  		if !side.unitSafety.isSafeWith(box.sides[0].unitSafety) {
   176  			return
   177  		}
   178  	}
   179  
   180  	// Generate the most minimal representation
   181  	tokens := compactTokenQuad(
   182  		box.sides[0].token,
   183  		box.sides[1].token,
   184  		box.sides[2].token,
   185  		box.sides[3].token,
   186  		minifyWhitespace,
   187  	)
   188  
   189  	// Remove all of the existing declarations
   190  	var minLoc logger.Loc
   191  	for i, side := range box.sides {
   192  		if loc := rules[side.ruleIndex].Loc; i == 0 || loc.Start < minLoc.Start {
   193  			minLoc = loc
   194  		}
   195  		rules[side.ruleIndex] = css_ast.Rule{}
   196  	}
   197  
   198  	// Insert the combined declaration where the last rule was
   199  	rules[box.sides[3].ruleIndex] = css_ast.Rule{Loc: minLoc, Data: &css_ast.RDeclaration{
   200  		Key:       box.key,
   201  		KeyText:   box.keyText,
   202  		Value:     tokens,
   203  		KeyRange:  keyRange,
   204  		Important: box.important,
   205  	}}
   206  }