mirror of
https://github.com/hanzoai/esbuild.git
synced 2026-09-24 16:58:50 +00:00
606 lines
18 KiB
Go
606 lines
18 KiB
Go
package css_parser
|
||
|
||
import (
|
||
"fmt"
|
||
"math"
|
||
"strconv"
|
||
"strings"
|
||
|
||
"github.com/evanw/esbuild/internal/css_ast"
|
||
"github.com/evanw/esbuild/internal/css_lexer"
|
||
"github.com/evanw/esbuild/internal/logger"
|
||
)
|
||
|
||
func (p *parser) tryToReduceCalcExpression(token css_ast.Token) css_ast.Token {
|
||
if term := tryToParseCalcTerm(*token.Children); term != nil {
|
||
whitespace := css_ast.WhitespaceBefore | css_ast.WhitespaceAfter
|
||
if p.options.minifyWhitespace {
|
||
whitespace = 0
|
||
}
|
||
term = term.partiallySimplify()
|
||
if result, ok := term.convertToToken(whitespace); ok {
|
||
if result.Kind == css_lexer.TOpenParen {
|
||
result.Kind = css_lexer.TFunction
|
||
result.Text = "calc"
|
||
}
|
||
result.Loc = token.Loc
|
||
result.Whitespace = css_ast.WhitespaceBefore | css_ast.WhitespaceAfter
|
||
return result
|
||
}
|
||
}
|
||
return token
|
||
}
|
||
|
||
type calcTermWithOp struct {
|
||
data calcTerm
|
||
opLoc logger.Loc
|
||
}
|
||
|
||
// See: https://www.w3.org/TR/css-values-4/#calc-internal
|
||
type calcTerm interface {
|
||
convertToToken(whitespace css_ast.WhitespaceFlags) (css_ast.Token, bool)
|
||
partiallySimplify() calcTerm
|
||
}
|
||
|
||
type calcSum struct {
|
||
terms []calcTermWithOp
|
||
}
|
||
|
||
type calcProduct struct {
|
||
terms []calcTermWithOp
|
||
}
|
||
|
||
type calcNegate struct {
|
||
term calcTermWithOp
|
||
}
|
||
|
||
type calcInvert struct {
|
||
term calcTermWithOp
|
||
}
|
||
|
||
type calcNumeric struct {
|
||
unit string
|
||
number float64
|
||
loc logger.Loc
|
||
}
|
||
|
||
type calcValue struct {
|
||
token css_ast.Token
|
||
isInvalidPlusOrMinus bool
|
||
}
|
||
|
||
func floatToStringForCalc(a float64) (string, bool) {
|
||
// Handle non-finite cases
|
||
if math.IsNaN(a) || math.IsInf(a, 0) {
|
||
return "", false
|
||
}
|
||
|
||
// Print the number as a string
|
||
text := fmt.Sprintf("%.05f", a)
|
||
for text[len(text)-1] == '0' {
|
||
text = text[:len(text)-1]
|
||
}
|
||
if text[len(text)-1] == '.' {
|
||
text = text[:len(text)-1]
|
||
}
|
||
if strings.HasPrefix(text, "0.") {
|
||
text = text[1:]
|
||
} else if strings.HasPrefix(text, "-0.") {
|
||
text = "-" + text[2:]
|
||
}
|
||
|
||
// Bail if the number is not exactly represented
|
||
if number, err := strconv.ParseFloat(text, 64); err != nil || number != a {
|
||
return "", false
|
||
}
|
||
|
||
return text, true
|
||
}
|
||
|
||
func (c *calcSum) convertToToken(whitespace css_ast.WhitespaceFlags) (css_ast.Token, bool) {
|
||
// Specification: https://www.w3.org/TR/css-values-4/#calc-serialize
|
||
tokens := make([]css_ast.Token, 0, len(c.terms)*2)
|
||
|
||
// ALGORITHM DEVIATION: Avoid parenthesizing product nodes inside sum nodes
|
||
if product, ok := c.terms[0].data.(*calcProduct); ok {
|
||
token, ok := product.convertToToken(whitespace)
|
||
if !ok {
|
||
return css_ast.Token{}, false
|
||
}
|
||
tokens = append(tokens, *token.Children...)
|
||
} else {
|
||
token, ok := c.terms[0].data.convertToToken(whitespace)
|
||
if !ok {
|
||
return css_ast.Token{}, false
|
||
}
|
||
tokens = append(tokens, token)
|
||
}
|
||
|
||
for _, term := range c.terms[1:] {
|
||
// If child is a Negate node, append " - " to s, then serialize the Negate’s child and append the result to s.
|
||
if negate, ok := term.data.(*calcNegate); ok {
|
||
token, ok := negate.term.data.convertToToken(whitespace)
|
||
if !ok {
|
||
return css_ast.Token{}, false
|
||
}
|
||
tokens = append(tokens, css_ast.Token{
|
||
Loc: term.opLoc,
|
||
Kind: css_lexer.TDelimMinus,
|
||
Text: "-",
|
||
Whitespace: css_ast.WhitespaceBefore | css_ast.WhitespaceAfter,
|
||
}, token)
|
||
continue
|
||
}
|
||
|
||
// If child is a negative numeric value, append " - " to s, then serialize the negation of child as normal and append the result to s.
|
||
if numeric, ok := term.data.(*calcNumeric); ok && numeric.number < 0 {
|
||
clone := *numeric
|
||
clone.number = -clone.number
|
||
token, ok := clone.convertToToken(whitespace)
|
||
if !ok {
|
||
return css_ast.Token{}, false
|
||
}
|
||
tokens = append(tokens, css_ast.Token{
|
||
Loc: term.opLoc,
|
||
Kind: css_lexer.TDelimMinus,
|
||
Text: "-",
|
||
Whitespace: css_ast.WhitespaceBefore | css_ast.WhitespaceAfter,
|
||
}, token)
|
||
continue
|
||
}
|
||
|
||
// Otherwise, append " + " to s, then serialize child and append the result to s.
|
||
tokens = append(tokens, css_ast.Token{
|
||
Loc: term.opLoc,
|
||
Kind: css_lexer.TDelimPlus,
|
||
Text: "+",
|
||
Whitespace: css_ast.WhitespaceBefore | css_ast.WhitespaceAfter,
|
||
})
|
||
|
||
// ALGORITHM DEVIATION: Avoid parenthesizing product nodes inside sum nodes
|
||
if product, ok := term.data.(*calcProduct); ok {
|
||
token, ok := product.convertToToken(whitespace)
|
||
if !ok {
|
||
return css_ast.Token{}, false
|
||
}
|
||
tokens = append(tokens, *token.Children...)
|
||
} else {
|
||
token, ok := term.data.convertToToken(whitespace)
|
||
if !ok {
|
||
return css_ast.Token{}, false
|
||
}
|
||
tokens = append(tokens, token)
|
||
}
|
||
}
|
||
|
||
return css_ast.Token{
|
||
Loc: tokens[0].Loc,
|
||
Kind: css_lexer.TOpenParen,
|
||
Text: "(",
|
||
Children: &tokens,
|
||
}, true
|
||
}
|
||
|
||
func (c *calcProduct) convertToToken(whitespace css_ast.WhitespaceFlags) (css_ast.Token, bool) {
|
||
// Specification: https://www.w3.org/TR/css-values-4/#calc-serialize
|
||
tokens := make([]css_ast.Token, 0, len(c.terms)*2)
|
||
token, ok := c.terms[0].data.convertToToken(whitespace)
|
||
if !ok {
|
||
return css_ast.Token{}, false
|
||
}
|
||
tokens = append(tokens, token)
|
||
|
||
for _, term := range c.terms[1:] {
|
||
// If child is an Invert node, append " / " to s, then serialize the Invert’s child and append the result to s.
|
||
if invert, ok := term.data.(*calcInvert); ok {
|
||
token, ok := invert.term.data.convertToToken(whitespace)
|
||
if !ok {
|
||
return css_ast.Token{}, false
|
||
}
|
||
tokens = append(tokens, css_ast.Token{
|
||
Loc: term.opLoc,
|
||
Kind: css_lexer.TDelimSlash,
|
||
Text: "/",
|
||
Whitespace: whitespace,
|
||
}, token)
|
||
continue
|
||
}
|
||
|
||
// Otherwise, append " * " to s, then serialize child and append the result to s.
|
||
token, ok := term.data.convertToToken(whitespace)
|
||
if !ok {
|
||
return css_ast.Token{}, false
|
||
}
|
||
tokens = append(tokens, css_ast.Token{
|
||
Loc: term.opLoc,
|
||
Kind: css_lexer.TDelimAsterisk,
|
||
Text: "*",
|
||
Whitespace: whitespace,
|
||
}, token)
|
||
}
|
||
|
||
return css_ast.Token{
|
||
Loc: tokens[0].Loc,
|
||
Kind: css_lexer.TOpenParen,
|
||
Text: "(",
|
||
Children: &tokens,
|
||
}, true
|
||
}
|
||
|
||
func (c *calcNegate) convertToToken(whitespace css_ast.WhitespaceFlags) (css_ast.Token, bool) {
|
||
// Specification: https://www.w3.org/TR/css-values-4/#calc-serialize
|
||
token, ok := c.term.data.convertToToken(whitespace)
|
||
if !ok {
|
||
return css_ast.Token{}, false
|
||
}
|
||
return css_ast.Token{
|
||
Kind: css_lexer.TOpenParen,
|
||
Text: "(",
|
||
Children: &[]css_ast.Token{
|
||
{Loc: c.term.opLoc, Kind: css_lexer.TNumber, Text: "-1"},
|
||
{Loc: c.term.opLoc, Kind: css_lexer.TDelimSlash, Text: "*", Whitespace: css_ast.WhitespaceBefore | css_ast.WhitespaceAfter},
|
||
token,
|
||
},
|
||
}, true
|
||
}
|
||
|
||
func (c *calcInvert) convertToToken(whitespace css_ast.WhitespaceFlags) (css_ast.Token, bool) {
|
||
// Specification: https://www.w3.org/TR/css-values-4/#calc-serialize
|
||
token, ok := c.term.data.convertToToken(whitespace)
|
||
if !ok {
|
||
return css_ast.Token{}, false
|
||
}
|
||
return css_ast.Token{
|
||
Kind: css_lexer.TOpenParen,
|
||
Text: "(",
|
||
Children: &[]css_ast.Token{
|
||
{Loc: c.term.opLoc, Kind: css_lexer.TNumber, Text: "1"},
|
||
{Loc: c.term.opLoc, Kind: css_lexer.TDelimSlash, Text: "/", Whitespace: css_ast.WhitespaceBefore | css_ast.WhitespaceAfter},
|
||
token,
|
||
},
|
||
}, true
|
||
}
|
||
|
||
func (c *calcNumeric) convertToToken(whitespace css_ast.WhitespaceFlags) (css_ast.Token, bool) {
|
||
text, ok := floatToStringForCalc(c.number)
|
||
if !ok {
|
||
return css_ast.Token{}, false
|
||
}
|
||
if c.unit == "" {
|
||
return css_ast.Token{
|
||
Loc: c.loc,
|
||
Kind: css_lexer.TNumber,
|
||
Text: text,
|
||
}, true
|
||
}
|
||
if c.unit == "%" {
|
||
return css_ast.Token{
|
||
Loc: c.loc,
|
||
Kind: css_lexer.TPercentage,
|
||
Text: text + "%",
|
||
}, true
|
||
}
|
||
return css_ast.Token{
|
||
Loc: c.loc,
|
||
Kind: css_lexer.TDimension,
|
||
Text: text + c.unit,
|
||
UnitOffset: uint16(len(text)),
|
||
}, true
|
||
}
|
||
|
||
func (c *calcValue) convertToToken(whitespace css_ast.WhitespaceFlags) (css_ast.Token, bool) {
|
||
t := c.token
|
||
t.Whitespace = 0
|
||
return t, true
|
||
}
|
||
|
||
func (c *calcSum) partiallySimplify() calcTerm {
|
||
// Specification: https://www.w3.org/TR/css-values-4/#calc-simplification
|
||
|
||
// For each of root’s children that are Sum nodes, replace them with their children.
|
||
terms := make([]calcTermWithOp, 0, len(c.terms))
|
||
for _, term := range c.terms {
|
||
term.data = term.data.partiallySimplify()
|
||
if sum, ok := term.data.(*calcSum); ok {
|
||
terms = append(terms, sum.terms...)
|
||
} else {
|
||
terms = append(terms, term)
|
||
}
|
||
}
|
||
|
||
// For each set of root’s children that are numeric values with identical units, remove
|
||
// those children and replace them with a single numeric value containing the sum of the
|
||
// removed nodes, and with the same unit. (E.g. combine numbers, combine percentages,
|
||
// combine px values, etc.)
|
||
for i := 0; i < len(terms); i++ {
|
||
term := terms[i]
|
||
if numeric, ok := term.data.(*calcNumeric); ok {
|
||
end := i + 1
|
||
for j := end; j < len(terms); j++ {
|
||
term2 := terms[j]
|
||
if numeric2, ok := term2.data.(*calcNumeric); ok && strings.EqualFold(numeric2.unit, numeric.unit) {
|
||
numeric.number += numeric2.number
|
||
} else {
|
||
terms[end] = term2
|
||
end++
|
||
}
|
||
}
|
||
terms = terms[:end]
|
||
}
|
||
}
|
||
|
||
// If root has only a single child at this point, return the child.
|
||
if len(terms) == 1 {
|
||
return terms[0].data
|
||
}
|
||
|
||
// Otherwise, return root.
|
||
c.terms = terms
|
||
return c
|
||
}
|
||
|
||
func (c *calcProduct) partiallySimplify() calcTerm {
|
||
// Specification: https://www.w3.org/TR/css-values-4/#calc-simplification
|
||
|
||
// For each of root’s children that are Product nodes, replace them with their children.
|
||
terms := make([]calcTermWithOp, 0, len(c.terms))
|
||
for _, term := range c.terms {
|
||
term.data = term.data.partiallySimplify()
|
||
if product, ok := term.data.(*calcProduct); ok {
|
||
terms = append(terms, product.terms...)
|
||
} else {
|
||
terms = append(terms, term)
|
||
}
|
||
}
|
||
|
||
// If root has multiple children that are numbers (not percentages or dimensions), remove
|
||
// them and replace them with a single number containing the product of the removed nodes.
|
||
for i, term := range terms {
|
||
if numeric, ok := term.data.(*calcNumeric); ok && numeric.unit == "" {
|
||
end := i + 1
|
||
for j := end; j < len(terms); j++ {
|
||
term2 := terms[j]
|
||
if numeric2, ok := term2.data.(*calcNumeric); ok && numeric2.unit == "" {
|
||
numeric.number *= numeric2.number
|
||
} else {
|
||
terms[end] = term2
|
||
end++
|
||
}
|
||
}
|
||
terms = terms[:end]
|
||
break
|
||
}
|
||
}
|
||
|
||
// If root contains only numeric values and/or Invert nodes containing numeric values,
|
||
// and multiplying the types of all the children (noting that the type of an Invert
|
||
// node is the inverse of its child’s type) results in a type that matches any of the
|
||
// types that a math function can resolve to, return the result of multiplying all the
|
||
// values of the children (noting that the value of an Invert node is the reciprocal
|
||
// of its child’s value), expressed in the result’s canonical unit.
|
||
if len(terms) == 2 {
|
||
// Right now, only handle the case of two numbers, one of which has no unit
|
||
if first, ok := terms[0].data.(*calcNumeric); ok {
|
||
if second, ok := terms[1].data.(*calcNumeric); ok {
|
||
if first.unit == "" {
|
||
second.number *= first.number
|
||
return second
|
||
}
|
||
if second.unit == "" {
|
||
first.number *= second.number
|
||
return first
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// ALGORITHM DEVIATION: Divide instead of multiply if the reciprocal is shorter
|
||
for i := 1; i < len(terms); i++ {
|
||
if numeric, ok := terms[i].data.(*calcNumeric); ok {
|
||
reciprocal := 1 / numeric.number
|
||
if multiply, ok := floatToStringForCalc(numeric.number); ok {
|
||
if divide, ok := floatToStringForCalc(reciprocal); ok && len(divide) < len(multiply) {
|
||
numeric.number = reciprocal
|
||
terms[i].data = &calcInvert{term: calcTermWithOp{
|
||
data: numeric,
|
||
opLoc: terms[i].opLoc,
|
||
}}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// If root has only a single child at this point, return the child.
|
||
if len(terms) == 1 {
|
||
return terms[0].data
|
||
}
|
||
|
||
// Otherwise, return root.
|
||
c.terms = terms
|
||
return c
|
||
}
|
||
|
||
func (c *calcNegate) partiallySimplify() calcTerm {
|
||
// Specification: https://www.w3.org/TR/css-values-4/#calc-simplification
|
||
|
||
c.term.data = c.term.data.partiallySimplify()
|
||
|
||
// If root’s child is a numeric value, return an equivalent numeric value, but with the value negated (0 - value).
|
||
if numeric, ok := c.term.data.(*calcNumeric); ok {
|
||
numeric.number = -numeric.number
|
||
return numeric
|
||
}
|
||
|
||
// If root’s child is a Negate node, return the child’s child.
|
||
if negate, ok := c.term.data.(*calcNegate); ok {
|
||
return negate.term.data
|
||
}
|
||
|
||
return c
|
||
}
|
||
|
||
func (c *calcInvert) partiallySimplify() calcTerm {
|
||
// Specification: https://www.w3.org/TR/css-values-4/#calc-simplification
|
||
|
||
c.term.data = c.term.data.partiallySimplify()
|
||
|
||
// If root’s child is a number (not a percentage or dimension) return the reciprocal of the child’s value.
|
||
if numeric, ok := c.term.data.(*calcNumeric); ok && numeric.unit == "" {
|
||
numeric.number = 1 / numeric.number
|
||
return numeric
|
||
}
|
||
|
||
// If root’s child is an Invert node, return the child’s child.
|
||
if invert, ok := c.term.data.(*calcInvert); ok {
|
||
return invert.term.data
|
||
}
|
||
|
||
return c
|
||
}
|
||
|
||
func (c *calcNumeric) partiallySimplify() calcTerm {
|
||
return c
|
||
}
|
||
|
||
func (c *calcValue) partiallySimplify() calcTerm {
|
||
return c
|
||
}
|
||
|
||
func tryToParseCalcTerm(tokens []css_ast.Token) calcTerm {
|
||
// Specification: https://www.w3.org/TR/css-values-4/#calc-internal
|
||
terms := make([]calcTermWithOp, len(tokens))
|
||
|
||
for i, token := range tokens {
|
||
var term calcTerm
|
||
if token.Kind == css_lexer.TFunction && strings.EqualFold(token.Text, "var") {
|
||
// Using "var()" should bail because it can expand to any number of tokens
|
||
return nil
|
||
} else if token.Kind == css_lexer.TOpenParen || (token.Kind == css_lexer.TFunction && strings.EqualFold(token.Text, "calc")) {
|
||
term = tryToParseCalcTerm(*token.Children)
|
||
if term == nil {
|
||
return nil
|
||
}
|
||
} else if token.Kind == css_lexer.TNumber {
|
||
if number, err := strconv.ParseFloat(token.Text, 64); err == nil {
|
||
term = &calcNumeric{loc: token.Loc, number: number}
|
||
} else {
|
||
term = &calcValue{token: token}
|
||
}
|
||
} else if token.Kind == css_lexer.TPercentage {
|
||
if number, err := strconv.ParseFloat(token.PercentageValue(), 64); err == nil {
|
||
term = &calcNumeric{loc: token.Loc, number: number, unit: "%"}
|
||
} else {
|
||
term = &calcValue{token: token}
|
||
}
|
||
} else if token.Kind == css_lexer.TDimension {
|
||
if number, err := strconv.ParseFloat(token.DimensionValue(), 64); err == nil {
|
||
term = &calcNumeric{loc: token.Loc, number: number, unit: token.DimensionUnit()}
|
||
} else {
|
||
term = &calcValue{token: token}
|
||
}
|
||
} else if token.Kind == css_lexer.TIdent && strings.EqualFold(token.Text, "Infinity") {
|
||
term = &calcNumeric{loc: token.Loc, number: math.Inf(1)}
|
||
} else if token.Kind == css_lexer.TIdent && strings.EqualFold(token.Text, "-Infinity") {
|
||
term = &calcNumeric{loc: token.Loc, number: math.Inf(-1)}
|
||
} else if token.Kind == css_lexer.TIdent && strings.EqualFold(token.Text, "NaN") {
|
||
term = &calcNumeric{loc: token.Loc, number: math.NaN()}
|
||
} else {
|
||
term = &calcValue{
|
||
token: token,
|
||
|
||
// From the specification: "In addition, whitespace is required on both sides of the
|
||
// + and - operators. (The * and / operators can be used without white space around them.)"
|
||
isInvalidPlusOrMinus: i > 0 && i+1 < len(tokens) &&
|
||
(token.Kind == css_lexer.TDelimPlus || token.Kind == css_lexer.TDelimMinus) &&
|
||
(((token.Whitespace&css_ast.WhitespaceBefore) == 0 && (tokens[i-1].Whitespace&css_ast.WhitespaceAfter) == 0) ||
|
||
(token.Whitespace&css_ast.WhitespaceAfter) == 0 && (tokens[i+1].Whitespace&css_ast.WhitespaceBefore) == 0),
|
||
}
|
||
}
|
||
terms[i].data = term
|
||
}
|
||
|
||
// Collect children into Product and Invert nodes
|
||
first := 1
|
||
for first+1 < len(terms) {
|
||
// If this is a "*" or "/" operator
|
||
if value, ok := terms[first].data.(*calcValue); ok && (value.token.Kind == css_lexer.TDelimAsterisk || value.token.Kind == css_lexer.TDelimSlash) {
|
||
// Scan over the run
|
||
last := first
|
||
for last+3 < len(terms) {
|
||
if value, ok := terms[last+2].data.(*calcValue); ok && (value.token.Kind == css_lexer.TDelimAsterisk || value.token.Kind == css_lexer.TDelimSlash) {
|
||
last += 2
|
||
} else {
|
||
break
|
||
}
|
||
}
|
||
|
||
// Generate a node for the run
|
||
product := calcProduct{terms: make([]calcTermWithOp, (last-first)/2+2)}
|
||
for i := range product.terms {
|
||
term := terms[first+i*2-1]
|
||
if i > 0 {
|
||
op := terms[first+i*2-2].data.(*calcValue).token
|
||
term.opLoc = op.Loc
|
||
if op.Kind == css_lexer.TDelimSlash {
|
||
term.data = &calcInvert{term: term}
|
||
}
|
||
}
|
||
product.terms[i] = term
|
||
}
|
||
|
||
// Replace the run with a single node
|
||
terms[first-1].data = &product
|
||
terms = append(terms[:first], terms[last+2:]...)
|
||
continue
|
||
}
|
||
|
||
first++
|
||
}
|
||
|
||
// Collect children into Sum and Negate nodes
|
||
first = 1
|
||
for first+1 < len(terms) {
|
||
// If this is a "+" or "-" operator
|
||
if value, ok := terms[first].data.(*calcValue); ok && !value.isInvalidPlusOrMinus &&
|
||
(value.token.Kind == css_lexer.TDelimPlus || value.token.Kind == css_lexer.TDelimMinus) {
|
||
// Scan over the run
|
||
last := first
|
||
for last+3 < len(terms) {
|
||
if value, ok := terms[last+2].data.(*calcValue); ok && !value.isInvalidPlusOrMinus &&
|
||
(value.token.Kind == css_lexer.TDelimPlus || value.token.Kind == css_lexer.TDelimMinus) {
|
||
last += 2
|
||
} else {
|
||
break
|
||
}
|
||
}
|
||
|
||
// Generate a node for the run
|
||
sum := calcSum{terms: make([]calcTermWithOp, (last-first)/2+2)}
|
||
for i := range sum.terms {
|
||
term := terms[first+i*2-1]
|
||
if i > 0 {
|
||
op := terms[first+i*2-2].data.(*calcValue).token
|
||
term.opLoc = op.Loc
|
||
if op.Kind == css_lexer.TDelimMinus {
|
||
term.data = &calcNegate{term: term}
|
||
}
|
||
}
|
||
sum.terms[i] = term
|
||
}
|
||
|
||
// Replace the run with a single node
|
||
terms[first-1].data = &sum
|
||
terms = append(terms[:first], terms[last+2:]...)
|
||
continue
|
||
}
|
||
|
||
first++
|
||
}
|
||
|
||
// This only succeeds if everything reduces to a single term
|
||
if len(terms) == 1 {
|
||
return terms[0].data
|
||
}
|
||
return nil
|
||
}
|