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 }