Files

606 lines
18 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
}