Files

207 lines
5.5 KiB
Go

package css_parser
import (
"strings"
"github.com/evanw/esbuild/internal/css_ast"
"github.com/evanw/esbuild/internal/css_lexer"
"github.com/evanw/esbuild/internal/logger"
)
const (
boxTop = iota
boxRight
boxBottom
boxLeft
)
type boxSide struct {
token css_ast.Token
unitSafety unitSafetyTracker
ruleIndex uint32 // The index of the originating rule in the rules array
wasSingleRule bool // True if the originating rule was just for this side
}
type boxTracker struct {
keyText string
sides [4]boxSide
allowAuto bool // If true, allow the "auto" keyword
important bool // True if all active rules were flagged as "!important"
key css_ast.D
}
type unitSafetyStatus uint8
const (
unitSafe unitSafetyStatus = iota // "margin: 0 1px 2cm 3%;"
unitUnsafeSingle // "margin: 0 1vw 2vw 3vw;"
unitUnsafeMixed // "margin: 0 1vw 2vh 3ch;"
)
// We can only compact rules together if they have the same unit safety level.
// We want to avoid a situation where the browser treats some of the original
// rules as valid and others as invalid.
//
// Safe:
// top: 1px; left: 0; bottom: 1px; right: 0;
// top: 1Q; left: 2Q; bottom: 3Q; right: 4Q;
//
// Unsafe:
// top: 1vh; left: 2vw; bottom: 3vh; right: 4vw;
// top: 1Q; left: 2Q; bottom: 3Q; right: 0;
// inset: 1Q 0 0 0; top: 0;
type unitSafetyTracker struct {
unit string
status unitSafetyStatus
}
func (a unitSafetyTracker) isSafeWith(b unitSafetyTracker) bool {
return a.status == b.status && a.status != unitUnsafeMixed && (a.status != unitUnsafeSingle || a.unit == b.unit)
}
func (t *unitSafetyTracker) includeUnitOf(token css_ast.Token) {
switch token.Kind {
case css_lexer.TNumber:
if token.Text == "0" {
return
}
case css_lexer.TPercentage:
return
case css_lexer.TDimension:
if token.DimensionUnitIsSafeLength() {
return
} else if unit := token.DimensionUnit(); t.status == unitSafe {
t.status = unitUnsafeSingle
t.unit = unit
return
} else if t.status == unitUnsafeSingle && t.unit == unit {
return
}
}
t.status = unitUnsafeMixed
}
func (box *boxTracker) updateSide(rules []css_ast.Rule, side int, new boxSide) {
if old := box.sides[side]; old.token.Kind != css_lexer.TEndOfFile &&
(!new.wasSingleRule || old.wasSingleRule) &&
old.unitSafety.status == unitSafe && new.unitSafety.status == unitSafe {
rules[old.ruleIndex] = css_ast.Rule{}
}
box.sides[side] = new
}
func (box *boxTracker) mangleSides(rules []css_ast.Rule, decl *css_ast.RDeclaration, minifyWhitespace bool) {
// Reset if we see a change in the "!important" flag
if box.important != decl.Important {
box.sides = [4]boxSide{}
box.important = decl.Important
}
allowedIdent := ""
if box.allowAuto {
allowedIdent = "auto"
}
if quad, ok := expandTokenQuad(decl.Value, allowedIdent); ok {
// Use a single tracker for the whole rule
unitSafety := unitSafetyTracker{}
for _, t := range quad {
if !box.allowAuto || t.Kind.IsNumeric() {
unitSafety.includeUnitOf(t)
}
}
for side, t := range quad {
if unitSafety.status == unitSafe {
t.TurnLengthIntoNumberIfZero()
}
box.updateSide(rules, side, boxSide{
token: t,
ruleIndex: uint32(len(rules) - 1),
unitSafety: unitSafety,
})
}
box.compactRules(rules, decl.KeyRange, minifyWhitespace)
} else {
box.sides = [4]boxSide{}
}
}
func (box *boxTracker) mangleSide(rules []css_ast.Rule, decl *css_ast.RDeclaration, minifyWhitespace bool, side int) {
// Reset if we see a change in the "!important" flag
if box.important != decl.Important {
box.sides = [4]boxSide{}
box.important = decl.Important
}
if tokens := decl.Value; len(tokens) == 1 {
if t := tokens[0]; t.Kind.IsNumeric() || (t.Kind == css_lexer.TIdent && box.allowAuto && strings.EqualFold(t.Text, "auto")) {
unitSafety := unitSafetyTracker{}
if !box.allowAuto || t.Kind.IsNumeric() {
unitSafety.includeUnitOf(t)
}
if unitSafety.status == unitSafe && t.TurnLengthIntoNumberIfZero() {
tokens[0] = t
}
box.updateSide(rules, side, boxSide{
token: t,
ruleIndex: uint32(len(rules) - 1),
wasSingleRule: true,
unitSafety: unitSafety,
})
box.compactRules(rules, decl.KeyRange, minifyWhitespace)
return
}
}
box.sides = [4]boxSide{}
}
func (box *boxTracker) compactRules(rules []css_ast.Rule, keyRange logger.Range, minifyWhitespace bool) {
// Don't compact if the shorthand form is unsupported
if box.key == css_ast.DUnknown {
return
}
// All tokens must be present
if eof := css_lexer.TEndOfFile; box.sides[0].token.Kind == eof || box.sides[1].token.Kind == eof ||
box.sides[2].token.Kind == eof || box.sides[3].token.Kind == eof {
return
}
// All tokens must have the same unit
for _, side := range box.sides[1:] {
if !side.unitSafety.isSafeWith(box.sides[0].unitSafety) {
return
}
}
// Generate the most minimal representation
tokens := compactTokenQuad(
box.sides[0].token,
box.sides[1].token,
box.sides[2].token,
box.sides[3].token,
minifyWhitespace,
)
// Remove all of the existing declarations
var minLoc logger.Loc
for i, side := range box.sides {
if loc := rules[side.ruleIndex].Loc; i == 0 || loc.Start < minLoc.Start {
minLoc = loc
}
rules[side.ruleIndex] = css_ast.Rule{}
}
// Insert the combined declaration where the last rule was
rules[box.sides[3].ruleIndex] = css_ast.Rule{Loc: minLoc, Data: &css_ast.RDeclaration{
Key: box.key,
KeyText: box.keyText,
Value: tokens,
KeyRange: keyRange,
Important: box.important,
}}
}