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

     1  package css_parser
     2  
     3  import (
     4  	"fmt"
     5  	"math"
     6  	"strconv"
     7  	"strings"
     8  
     9  	"github.com/evanw/esbuild/internal/css_ast"
    10  	"github.com/evanw/esbuild/internal/css_lexer"
    11  	"github.com/evanw/esbuild/internal/logger"
    12  )
    13  
    14  func (p *parser) tryToReduceCalcExpression(token css_ast.Token) css_ast.Token {
    15  	if term := tryToParseCalcTerm(*token.Children); term != nil {
    16  		whitespace := css_ast.WhitespaceBefore | css_ast.WhitespaceAfter
    17  		if p.options.minifyWhitespace {
    18  			whitespace = 0
    19  		}
    20  		term = term.partiallySimplify()
    21  		if result, ok := term.convertToToken(whitespace); ok {
    22  			if result.Kind == css_lexer.TOpenParen {
    23  				result.Kind = css_lexer.TFunction
    24  				result.Text = "calc"
    25  			}
    26  			result.Loc = token.Loc
    27  			result.Whitespace = css_ast.WhitespaceBefore | css_ast.WhitespaceAfter
    28  			return result
    29  		}
    30  	}
    31  	return token
    32  }
    33  
    34  type calcTermWithOp struct {
    35  	data  calcTerm
    36  	opLoc logger.Loc
    37  }
    38  
    39  // See: https://www.w3.org/TR/css-values-4/#calc-internal
    40  type calcTerm interface {
    41  	convertToToken(whitespace css_ast.WhitespaceFlags) (css_ast.Token, bool)
    42  	partiallySimplify() calcTerm
    43  }
    44  
    45  type calcSum struct {
    46  	terms []calcTermWithOp
    47  }
    48  
    49  type calcProduct struct {
    50  	terms []calcTermWithOp
    51  }
    52  
    53  type calcNegate struct {
    54  	term calcTermWithOp
    55  }
    56  
    57  type calcInvert struct {
    58  	term calcTermWithOp
    59  }
    60  
    61  type calcNumeric struct {
    62  	unit   string
    63  	number float64
    64  	loc    logger.Loc
    65  }
    66  
    67  type calcValue struct {
    68  	token                css_ast.Token
    69  	isInvalidPlusOrMinus bool
    70  }
    71  
    72  func floatToStringForCalc(a float64) (string, bool) {
    73  	// Handle non-finite cases
    74  	if math.IsNaN(a) || math.IsInf(a, 0) {
    75  		return "", false
    76  	}
    77  
    78  	// Print the number as a string
    79  	text := fmt.Sprintf("%.05f", a)
    80  	for text[len(text)-1] == '0' {
    81  		text = text[:len(text)-1]
    82  	}
    83  	if text[len(text)-1] == '.' {
    84  		text = text[:len(text)-1]
    85  	}
    86  	if strings.HasPrefix(text, "0.") {
    87  		text = text[1:]
    88  	} else if strings.HasPrefix(text, "-0.") {
    89  		text = "-" + text[2:]
    90  	}
    91  
    92  	// Bail if the number is not exactly represented
    93  	if number, err := strconv.ParseFloat(text, 64); err != nil || number != a {
    94  		return "", false
    95  	}
    96  
    97  	return text, true
    98  }
    99  
   100  func (c *calcSum) convertToToken(whitespace css_ast.WhitespaceFlags) (css_ast.Token, bool) {
   101  	// Specification: https://www.w3.org/TR/css-values-4/#calc-serialize
   102  	tokens := make([]css_ast.Token, 0, len(c.terms)*2)
   103  
   104  	// ALGORITHM DEVIATION: Avoid parenthesizing product nodes inside sum nodes
   105  	if product, ok := c.terms[0].data.(*calcProduct); ok {
   106  		token, ok := product.convertToToken(whitespace)
   107  		if !ok {
   108  			return css_ast.Token{}, false
   109  		}
   110  		tokens = append(tokens, *token.Children...)
   111  	} else {
   112  		token, ok := c.terms[0].data.convertToToken(whitespace)
   113  		if !ok {
   114  			return css_ast.Token{}, false
   115  		}
   116  		tokens = append(tokens, token)
   117  	}
   118  
   119  	for _, term := range c.terms[1:] {
   120  		// If child is a Negate node, append " - " to s, then serialize the Negate’s child and append the result to s.
   121  		if negate, ok := term.data.(*calcNegate); ok {
   122  			token, ok := negate.term.data.convertToToken(whitespace)
   123  			if !ok {
   124  				return css_ast.Token{}, false
   125  			}
   126  			tokens = append(tokens, css_ast.Token{
   127  				Loc:        term.opLoc,
   128  				Kind:       css_lexer.TDelimMinus,
   129  				Text:       "-",
   130  				Whitespace: css_ast.WhitespaceBefore | css_ast.WhitespaceAfter,
   131  			}, token)
   132  			continue
   133  		}
   134  
   135  		// If child is a negative numeric value, append " - " to s, then serialize the negation of child as normal and append the result to s.
   136  		if numeric, ok := term.data.(*calcNumeric); ok && numeric.number < 0 {
   137  			clone := *numeric
   138  			clone.number = -clone.number
   139  			token, ok := clone.convertToToken(whitespace)
   140  			if !ok {
   141  				return css_ast.Token{}, false
   142  			}
   143  			tokens = append(tokens, css_ast.Token{
   144  				Loc:        term.opLoc,
   145  				Kind:       css_lexer.TDelimMinus,
   146  				Text:       "-",
   147  				Whitespace: css_ast.WhitespaceBefore | css_ast.WhitespaceAfter,
   148  			}, token)
   149  			continue
   150  		}
   151  
   152  		// Otherwise, append " + " to s, then serialize child and append the result to s.
   153  		tokens = append(tokens, css_ast.Token{
   154  			Loc:        term.opLoc,
   155  			Kind:       css_lexer.TDelimPlus,
   156  			Text:       "+",
   157  			Whitespace: css_ast.WhitespaceBefore | css_ast.WhitespaceAfter,
   158  		})
   159  
   160  		// ALGORITHM DEVIATION: Avoid parenthesizing product nodes inside sum nodes
   161  		if product, ok := term.data.(*calcProduct); ok {
   162  			token, ok := product.convertToToken(whitespace)
   163  			if !ok {
   164  				return css_ast.Token{}, false
   165  			}
   166  			tokens = append(tokens, *token.Children...)
   167  		} else {
   168  			token, ok := term.data.convertToToken(whitespace)
   169  			if !ok {
   170  				return css_ast.Token{}, false
   171  			}
   172  			tokens = append(tokens, token)
   173  		}
   174  	}
   175  
   176  	return css_ast.Token{
   177  		Loc:      tokens[0].Loc,
   178  		Kind:     css_lexer.TOpenParen,
   179  		Text:     "(",
   180  		Children: &tokens,
   181  	}, true
   182  }
   183  
   184  func (c *calcProduct) convertToToken(whitespace css_ast.WhitespaceFlags) (css_ast.Token, bool) {
   185  	// Specification: https://www.w3.org/TR/css-values-4/#calc-serialize
   186  	tokens := make([]css_ast.Token, 0, len(c.terms)*2)
   187  	token, ok := c.terms[0].data.convertToToken(whitespace)
   188  	if !ok {
   189  		return css_ast.Token{}, false
   190  	}
   191  	tokens = append(tokens, token)
   192  
   193  	for _, term := range c.terms[1:] {
   194  		// If child is an Invert node, append " / " to s, then serialize the Invert’s child and append the result to s.
   195  		if invert, ok := term.data.(*calcInvert); ok {
   196  			token, ok := invert.term.data.convertToToken(whitespace)
   197  			if !ok {
   198  				return css_ast.Token{}, false
   199  			}
   200  			tokens = append(tokens, css_ast.Token{
   201  				Loc:        term.opLoc,
   202  				Kind:       css_lexer.TDelimSlash,
   203  				Text:       "/",
   204  				Whitespace: whitespace,
   205  			}, token)
   206  			continue
   207  		}
   208  
   209  		// Otherwise, append " * " to s, then serialize child and append the result to s.
   210  		token, ok := term.data.convertToToken(whitespace)
   211  		if !ok {
   212  			return css_ast.Token{}, false
   213  		}
   214  		tokens = append(tokens, css_ast.Token{
   215  			Loc:        term.opLoc,
   216  			Kind:       css_lexer.TDelimAsterisk,
   217  			Text:       "*",
   218  			Whitespace: whitespace,
   219  		}, token)
   220  	}
   221  
   222  	return css_ast.Token{
   223  		Loc:      tokens[0].Loc,
   224  		Kind:     css_lexer.TOpenParen,
   225  		Text:     "(",
   226  		Children: &tokens,
   227  	}, true
   228  }
   229  
   230  func (c *calcNegate) convertToToken(whitespace css_ast.WhitespaceFlags) (css_ast.Token, bool) {
   231  	// Specification: https://www.w3.org/TR/css-values-4/#calc-serialize
   232  	token, ok := c.term.data.convertToToken(whitespace)
   233  	if !ok {
   234  		return css_ast.Token{}, false
   235  	}
   236  	return css_ast.Token{
   237  		Kind: css_lexer.TOpenParen,
   238  		Text: "(",
   239  		Children: &[]css_ast.Token{
   240  			{Loc: c.term.opLoc, Kind: css_lexer.TNumber, Text: "-1"},
   241  			{Loc: c.term.opLoc, Kind: css_lexer.TDelimSlash, Text: "*", Whitespace: css_ast.WhitespaceBefore | css_ast.WhitespaceAfter},
   242  			token,
   243  		},
   244  	}, true
   245  }
   246  
   247  func (c *calcInvert) convertToToken(whitespace css_ast.WhitespaceFlags) (css_ast.Token, bool) {
   248  	// Specification: https://www.w3.org/TR/css-values-4/#calc-serialize
   249  	token, ok := c.term.data.convertToToken(whitespace)
   250  	if !ok {
   251  		return css_ast.Token{}, false
   252  	}
   253  	return css_ast.Token{
   254  		Kind: css_lexer.TOpenParen,
   255  		Text: "(",
   256  		Children: &[]css_ast.Token{
   257  			{Loc: c.term.opLoc, Kind: css_lexer.TNumber, Text: "1"},
   258  			{Loc: c.term.opLoc, Kind: css_lexer.TDelimSlash, Text: "/", Whitespace: css_ast.WhitespaceBefore | css_ast.WhitespaceAfter},
   259  			token,
   260  		},
   261  	}, true
   262  }
   263  
   264  func (c *calcNumeric) convertToToken(whitespace css_ast.WhitespaceFlags) (css_ast.Token, bool) {
   265  	text, ok := floatToStringForCalc(c.number)
   266  	if !ok {
   267  		return css_ast.Token{}, false
   268  	}
   269  	if c.unit == "" {
   270  		return css_ast.Token{
   271  			Loc:  c.loc,
   272  			Kind: css_lexer.TNumber,
   273  			Text: text,
   274  		}, true
   275  	}
   276  	if c.unit == "%" {
   277  		return css_ast.Token{
   278  			Loc:  c.loc,
   279  			Kind: css_lexer.TPercentage,
   280  			Text: text + "%",
   281  		}, true
   282  	}
   283  	return css_ast.Token{
   284  		Loc:        c.loc,
   285  		Kind:       css_lexer.TDimension,
   286  		Text:       text + c.unit,
   287  		UnitOffset: uint16(len(text)),
   288  	}, true
   289  }
   290  
   291  func (c *calcValue) convertToToken(whitespace css_ast.WhitespaceFlags) (css_ast.Token, bool) {
   292  	t := c.token
   293  	t.Whitespace = 0
   294  	return t, true
   295  }
   296  
   297  func (c *calcSum) partiallySimplify() calcTerm {
   298  	// Specification: https://www.w3.org/TR/css-values-4/#calc-simplification
   299  
   300  	// For each of root’s children that are Sum nodes, replace them with their children.
   301  	terms := make([]calcTermWithOp, 0, len(c.terms))
   302  	for _, term := range c.terms {
   303  		term.data = term.data.partiallySimplify()
   304  		if sum, ok := term.data.(*calcSum); ok {
   305  			terms = append(terms, sum.terms...)
   306  		} else {
   307  			terms = append(terms, term)
   308  		}
   309  	}
   310  
   311  	// For each set of root’s children that are numeric values with identical units, remove
   312  	// those children and replace them with a single numeric value containing the sum of the
   313  	// removed nodes, and with the same unit. (E.g. combine numbers, combine percentages,
   314  	// combine px values, etc.)
   315  	for i := 0; i < len(terms); i++ {
   316  		term := terms[i]
   317  		if numeric, ok := term.data.(*calcNumeric); ok {
   318  			end := i + 1
   319  			for j := end; j < len(terms); j++ {
   320  				term2 := terms[j]
   321  				if numeric2, ok := term2.data.(*calcNumeric); ok && strings.EqualFold(numeric2.unit, numeric.unit) {
   322  					numeric.number += numeric2.number
   323  				} else {
   324  					terms[end] = term2
   325  					end++
   326  				}
   327  			}
   328  			terms = terms[:end]
   329  		}
   330  	}
   331  
   332  	// If root has only a single child at this point, return the child.
   333  	if len(terms) == 1 {
   334  		return terms[0].data
   335  	}
   336  
   337  	// Otherwise, return root.
   338  	c.terms = terms
   339  	return c
   340  }
   341  
   342  func (c *calcProduct) partiallySimplify() calcTerm {
   343  	// Specification: https://www.w3.org/TR/css-values-4/#calc-simplification
   344  
   345  	// For each of root’s children that are Product nodes, replace them with their children.
   346  	terms := make([]calcTermWithOp, 0, len(c.terms))
   347  	for _, term := range c.terms {
   348  		term.data = term.data.partiallySimplify()
   349  		if product, ok := term.data.(*calcProduct); ok {
   350  			terms = append(terms, product.terms...)
   351  		} else {
   352  			terms = append(terms, term)
   353  		}
   354  	}
   355  
   356  	// If root has multiple children that are numbers (not percentages or dimensions), remove
   357  	// them and replace them with a single number containing the product of the removed nodes.
   358  	for i, term := range terms {
   359  		if numeric, ok := term.data.(*calcNumeric); ok && numeric.unit == "" {
   360  			end := i + 1
   361  			for j := end; j < len(terms); j++ {
   362  				term2 := terms[j]
   363  				if numeric2, ok := term2.data.(*calcNumeric); ok && numeric2.unit == "" {
   364  					numeric.number *= numeric2.number
   365  				} else {
   366  					terms[end] = term2
   367  					end++
   368  				}
   369  			}
   370  			terms = terms[:end]
   371  			break
   372  		}
   373  	}
   374  
   375  	// If root contains only numeric values and/or Invert nodes containing numeric values,
   376  	// and multiplying the types of all the children (noting that the type of an Invert
   377  	// node is the inverse of its child’s type) results in a type that matches any of the
   378  	// types that a math function can resolve to, return the result of multiplying all the
   379  	// values of the children (noting that the value of an Invert node is the reciprocal
   380  	// of its child’s value), expressed in the result’s canonical unit.
   381  	if len(terms) == 2 {
   382  		// Right now, only handle the case of two numbers, one of which has no unit
   383  		if first, ok := terms[0].data.(*calcNumeric); ok {
   384  			if second, ok := terms[1].data.(*calcNumeric); ok {
   385  				if first.unit == "" {
   386  					second.number *= first.number
   387  					return second
   388  				}
   389  				if second.unit == "" {
   390  					first.number *= second.number
   391  					return first
   392  				}
   393  			}
   394  		}
   395  	}
   396  
   397  	// ALGORITHM DEVIATION: Divide instead of multiply if the reciprocal is shorter
   398  	for i := 1; i < len(terms); i++ {
   399  		if numeric, ok := terms[i].data.(*calcNumeric); ok {
   400  			reciprocal := 1 / numeric.number
   401  			if multiply, ok := floatToStringForCalc(numeric.number); ok {
   402  				if divide, ok := floatToStringForCalc(reciprocal); ok && len(divide) < len(multiply) {
   403  					numeric.number = reciprocal
   404  					terms[i].data = &calcInvert{term: calcTermWithOp{
   405  						data:  numeric,
   406  						opLoc: terms[i].opLoc,
   407  					}}
   408  				}
   409  			}
   410  		}
   411  	}
   412  
   413  	// If root has only a single child at this point, return the child.
   414  	if len(terms) == 1 {
   415  		return terms[0].data
   416  	}
   417  
   418  	// Otherwise, return root.
   419  	c.terms = terms
   420  	return c
   421  }
   422  
   423  func (c *calcNegate) partiallySimplify() calcTerm {
   424  	// Specification: https://www.w3.org/TR/css-values-4/#calc-simplification
   425  
   426  	c.term.data = c.term.data.partiallySimplify()
   427  
   428  	// If root’s child is a numeric value, return an equivalent numeric value, but with the value negated (0 - value).
   429  	if numeric, ok := c.term.data.(*calcNumeric); ok {
   430  		numeric.number = -numeric.number
   431  		return numeric
   432  	}
   433  
   434  	// If root’s child is a Negate node, return the child’s child.
   435  	if negate, ok := c.term.data.(*calcNegate); ok {
   436  		return negate.term.data
   437  	}
   438  
   439  	return c
   440  }
   441  
   442  func (c *calcInvert) partiallySimplify() calcTerm {
   443  	// Specification: https://www.w3.org/TR/css-values-4/#calc-simplification
   444  
   445  	c.term.data = c.term.data.partiallySimplify()
   446  
   447  	// If root’s child is a number (not a percentage or dimension) return the reciprocal of the child’s value.
   448  	if numeric, ok := c.term.data.(*calcNumeric); ok && numeric.unit == "" {
   449  		numeric.number = 1 / numeric.number
   450  		return numeric
   451  	}
   452  
   453  	// If root’s child is an Invert node, return the child’s child.
   454  	if invert, ok := c.term.data.(*calcInvert); ok {
   455  		return invert.term.data
   456  	}
   457  
   458  	return c
   459  }
   460  
   461  func (c *calcNumeric) partiallySimplify() calcTerm {
   462  	return c
   463  }
   464  
   465  func (c *calcValue) partiallySimplify() calcTerm {
   466  	return c
   467  }
   468  
   469  func tryToParseCalcTerm(tokens []css_ast.Token) calcTerm {
   470  	// Specification: https://www.w3.org/TR/css-values-4/#calc-internal
   471  	terms := make([]calcTermWithOp, len(tokens))
   472  
   473  	for i, token := range tokens {
   474  		var term calcTerm
   475  		if token.Kind == css_lexer.TFunction && strings.EqualFold(token.Text, "var") {
   476  			// Using "var()" should bail because it can expand to any number of tokens
   477  			return nil
   478  		} else if token.Kind == css_lexer.TOpenParen || (token.Kind == css_lexer.TFunction && strings.EqualFold(token.Text, "calc")) {
   479  			term = tryToParseCalcTerm(*token.Children)
   480  			if term == nil {
   481  				return nil
   482  			}
   483  		} else if token.Kind == css_lexer.TNumber {
   484  			if number, err := strconv.ParseFloat(token.Text, 64); err == nil {
   485  				term = &calcNumeric{loc: token.Loc, number: number}
   486  			} else {
   487  				term = &calcValue{token: token}
   488  			}
   489  		} else if token.Kind == css_lexer.TPercentage {
   490  			if number, err := strconv.ParseFloat(token.PercentageValue(), 64); err == nil {
   491  				term = &calcNumeric{loc: token.Loc, number: number, unit: "%"}
   492  			} else {
   493  				term = &calcValue{token: token}
   494  			}
   495  		} else if token.Kind == css_lexer.TDimension {
   496  			if number, err := strconv.ParseFloat(token.DimensionValue(), 64); err == nil {
   497  				term = &calcNumeric{loc: token.Loc, number: number, unit: token.DimensionUnit()}
   498  			} else {
   499  				term = &calcValue{token: token}
   500  			}
   501  		} else if token.Kind == css_lexer.TIdent && strings.EqualFold(token.Text, "Infinity") {
   502  			term = &calcNumeric{loc: token.Loc, number: math.Inf(1)}
   503  		} else if token.Kind == css_lexer.TIdent && strings.EqualFold(token.Text, "-Infinity") {
   504  			term = &calcNumeric{loc: token.Loc, number: math.Inf(-1)}
   505  		} else if token.Kind == css_lexer.TIdent && strings.EqualFold(token.Text, "NaN") {
   506  			term = &calcNumeric{loc: token.Loc, number: math.NaN()}
   507  		} else {
   508  			term = &calcValue{
   509  				token: token,
   510  
   511  				// From the specification: "In addition, whitespace is required on both sides of the
   512  				// + and - operators. (The * and / operators can be used without white space around them.)"
   513  				isInvalidPlusOrMinus: i > 0 && i+1 < len(tokens) &&
   514  					(token.Kind == css_lexer.TDelimPlus || token.Kind == css_lexer.TDelimMinus) &&
   515  					(((token.Whitespace&css_ast.WhitespaceBefore) == 0 && (tokens[i-1].Whitespace&css_ast.WhitespaceAfter) == 0) ||
   516  						(token.Whitespace&css_ast.WhitespaceAfter) == 0 && (tokens[i+1].Whitespace&css_ast.WhitespaceBefore) == 0),
   517  			}
   518  		}
   519  		terms[i].data = term
   520  	}
   521  
   522  	// Collect children into Product and Invert nodes
   523  	first := 1
   524  	for first+1 < len(terms) {
   525  		// If this is a "*" or "/" operator
   526  		if value, ok := terms[first].data.(*calcValue); ok && (value.token.Kind == css_lexer.TDelimAsterisk || value.token.Kind == css_lexer.TDelimSlash) {
   527  			// Scan over the run
   528  			last := first
   529  			for last+3 < len(terms) {
   530  				if value, ok := terms[last+2].data.(*calcValue); ok && (value.token.Kind == css_lexer.TDelimAsterisk || value.token.Kind == css_lexer.TDelimSlash) {
   531  					last += 2
   532  				} else {
   533  					break
   534  				}
   535  			}
   536  
   537  			// Generate a node for the run
   538  			product := calcProduct{terms: make([]calcTermWithOp, (last-first)/2+2)}
   539  			for i := range product.terms {
   540  				term := terms[first+i*2-1]
   541  				if i > 0 {
   542  					op := terms[first+i*2-2].data.(*calcValue).token
   543  					term.opLoc = op.Loc
   544  					if op.Kind == css_lexer.TDelimSlash {
   545  						term.data = &calcInvert{term: term}
   546  					}
   547  				}
   548  				product.terms[i] = term
   549  			}
   550  
   551  			// Replace the run with a single node
   552  			terms[first-1].data = &product
   553  			terms = append(terms[:first], terms[last+2:]...)
   554  			continue
   555  		}
   556  
   557  		first++
   558  	}
   559  
   560  	// Collect children into Sum and Negate nodes
   561  	first = 1
   562  	for first+1 < len(terms) {
   563  		// If this is a "+" or "-" operator
   564  		if value, ok := terms[first].data.(*calcValue); ok && !value.isInvalidPlusOrMinus &&
   565  			(value.token.Kind == css_lexer.TDelimPlus || value.token.Kind == css_lexer.TDelimMinus) {
   566  			// Scan over the run
   567  			last := first
   568  			for last+3 < len(terms) {
   569  				if value, ok := terms[last+2].data.(*calcValue); ok && !value.isInvalidPlusOrMinus &&
   570  					(value.token.Kind == css_lexer.TDelimPlus || value.token.Kind == css_lexer.TDelimMinus) {
   571  					last += 2
   572  				} else {
   573  					break
   574  				}
   575  			}
   576  
   577  			// Generate a node for the run
   578  			sum := calcSum{terms: make([]calcTermWithOp, (last-first)/2+2)}
   579  			for i := range sum.terms {
   580  				term := terms[first+i*2-1]
   581  				if i > 0 {
   582  					op := terms[first+i*2-2].data.(*calcValue).token
   583  					term.opLoc = op.Loc
   584  					if op.Kind == css_lexer.TDelimMinus {
   585  						term.data = &calcNegate{term: term}
   586  					}
   587  				}
   588  				sum.terms[i] = term
   589  			}
   590  
   591  			// Replace the run with a single node
   592  			terms[first-1].data = &sum
   593  			terms = append(terms[:first], terms[last+2:]...)
   594  			continue
   595  		}
   596  
   597  		first++
   598  	}
   599  
   600  	// This only succeeds if everything reduces to a single term
   601  	if len(terms) == 1 {
   602  		return terms[0].data
   603  	}
   604  	return nil
   605  }