github.com/evanw/esbuild@v0.21.4/internal/css_printer/css_printer.go (about) 1 package css_printer 2 3 import ( 4 "fmt" 5 "strings" 6 "unicode/utf8" 7 8 "github.com/evanw/esbuild/internal/ast" 9 "github.com/evanw/esbuild/internal/compat" 10 "github.com/evanw/esbuild/internal/config" 11 "github.com/evanw/esbuild/internal/css_ast" 12 "github.com/evanw/esbuild/internal/css_lexer" 13 "github.com/evanw/esbuild/internal/helpers" 14 "github.com/evanw/esbuild/internal/logger" 15 "github.com/evanw/esbuild/internal/sourcemap" 16 ) 17 18 const quoteForURL byte = 0 19 20 type printer struct { 21 options Options 22 symbols ast.SymbolMap 23 importRecords []ast.ImportRecord 24 css []byte 25 hasLegalComment map[string]struct{} 26 extractedLegalComments []string 27 jsonMetadataImports []string 28 builder sourcemap.ChunkBuilder 29 oldLineStart int 30 oldLineEnd int 31 } 32 33 type Options struct { 34 // This will be present if the input file had a source map. In that case we 35 // want to map all the way back to the original input file(s). 36 InputSourceMap *sourcemap.SourceMap 37 38 // If we're writing out a source map, this table of line start indices lets 39 // us do binary search on to figure out what line a given AST node came from 40 LineOffsetTables []sourcemap.LineOffsetTable 41 42 // Local symbol renaming results go here 43 LocalNames map[ast.Ref]string 44 45 LineLimit int 46 InputSourceIndex uint32 47 UnsupportedFeatures compat.CSSFeature 48 MinifyWhitespace bool 49 ASCIIOnly bool 50 SourceMap config.SourceMap 51 AddSourceMappings bool 52 LegalComments config.LegalComments 53 NeedsMetafile bool 54 } 55 56 type PrintResult struct { 57 CSS []byte 58 ExtractedLegalComments []string 59 JSONMetadataImports []string 60 61 // This source map chunk just contains the VLQ-encoded offsets for the "CSS" 62 // field above. It's not a full source map. The bundler will be joining many 63 // source map chunks together to form the final source map. 64 SourceMapChunk sourcemap.Chunk 65 } 66 67 func Print(tree css_ast.AST, symbols ast.SymbolMap, options Options) PrintResult { 68 p := printer{ 69 options: options, 70 symbols: symbols, 71 importRecords: tree.ImportRecords, 72 builder: sourcemap.MakeChunkBuilder(options.InputSourceMap, options.LineOffsetTables, options.ASCIIOnly), 73 } 74 for _, rule := range tree.Rules { 75 p.printRule(rule, 0, false) 76 } 77 result := PrintResult{ 78 CSS: p.css, 79 ExtractedLegalComments: p.extractedLegalComments, 80 JSONMetadataImports: p.jsonMetadataImports, 81 } 82 if options.SourceMap != config.SourceMapNone { 83 // This is expensive. Only do this if it's necessary. For example, skipping 84 // this if it's not needed sped up end-to-end parsing and printing of a 85 // large CSS file from 66ms to 52ms (around 25% faster). 86 result.SourceMapChunk = p.builder.GenerateChunk(p.css) 87 } 88 return result 89 } 90 91 func (p *printer) recordImportPathForMetafile(importRecordIndex uint32) { 92 if p.options.NeedsMetafile { 93 record := p.importRecords[importRecordIndex] 94 external := "" 95 if (record.Flags & ast.ShouldNotBeExternalInMetafile) == 0 { 96 external = ",\n \"external\": true" 97 } 98 p.jsonMetadataImports = append(p.jsonMetadataImports, fmt.Sprintf("\n {\n \"path\": %s,\n \"kind\": %s%s\n }", 99 helpers.QuoteForJSON(record.Path.Text, p.options.ASCIIOnly), 100 helpers.QuoteForJSON(record.Kind.StringForMetafile(), p.options.ASCIIOnly), 101 external)) 102 } 103 } 104 105 func (p *printer) printRule(rule css_ast.Rule, indent int32, omitTrailingSemicolon bool) { 106 if r, ok := rule.Data.(*css_ast.RComment); ok { 107 switch p.options.LegalComments { 108 case config.LegalCommentsNone: 109 return 110 111 case config.LegalCommentsEndOfFile, 112 config.LegalCommentsLinkedWithComment, 113 config.LegalCommentsExternalWithoutComment: 114 115 // Don't record the same legal comment more than once per file 116 if p.hasLegalComment == nil { 117 p.hasLegalComment = make(map[string]struct{}) 118 } else if _, ok := p.hasLegalComment[r.Text]; ok { 119 return 120 } 121 p.hasLegalComment[r.Text] = struct{}{} 122 p.extractedLegalComments = append(p.extractedLegalComments, r.Text) 123 return 124 } 125 } 126 127 if p.options.LineLimit > 0 { 128 p.printNewlinePastLineLimit(indent) 129 } 130 131 if p.options.AddSourceMappings { 132 shouldPrintMapping := true 133 if indent == 0 || p.options.MinifyWhitespace { 134 switch rule.Data.(type) { 135 case *css_ast.RSelector, *css_ast.RQualified, *css_ast.RBadDeclaration: 136 // These rules will begin with a potentially more accurate mapping. We 137 // shouldn't print a mapping here if there's no indent in between this 138 // mapping and the rule. 139 shouldPrintMapping = false 140 } 141 } 142 if shouldPrintMapping { 143 p.builder.AddSourceMapping(rule.Loc, "", p.css) 144 } 145 } 146 147 if !p.options.MinifyWhitespace { 148 p.printIndent(indent) 149 } 150 151 switch r := rule.Data.(type) { 152 case *css_ast.RAtCharset: 153 // It's not valid to remove the space in between these two tokens 154 p.print("@charset ") 155 156 // It's not valid to print the string with single quotes 157 p.printQuotedWithQuote(r.Encoding, '"', 0) 158 p.print(";") 159 160 case *css_ast.RAtImport: 161 if p.options.MinifyWhitespace { 162 p.print("@import") 163 } else { 164 p.print("@import ") 165 } 166 record := p.importRecords[r.ImportRecordIndex] 167 var flags printQuotedFlags 168 if record.Flags.Has(ast.ContainsUniqueKey) { 169 flags |= printQuotedNoWrap 170 } 171 p.printQuoted(record.Path.Text, flags) 172 p.recordImportPathForMetafile(r.ImportRecordIndex) 173 if conditions := r.ImportConditions; conditions != nil { 174 space := !p.options.MinifyWhitespace 175 if len(conditions.Layers) > 0 { 176 if space { 177 p.print(" ") 178 } 179 p.printTokens(conditions.Layers, printTokensOpts{}) 180 space = true 181 } 182 if len(conditions.Supports) > 0 { 183 if space { 184 p.print(" ") 185 } 186 p.printTokens(conditions.Supports, printTokensOpts{}) 187 space = true 188 } 189 if len(conditions.Media) > 0 { 190 if space { 191 p.print(" ") 192 } 193 p.printTokens(conditions.Media, printTokensOpts{}) 194 } 195 } 196 p.print(";") 197 198 case *css_ast.RAtKeyframes: 199 p.print("@") 200 p.printIdent(r.AtToken, identNormal, mayNeedWhitespaceAfter) 201 p.print(" ") 202 p.printSymbol(r.Name.Loc, r.Name.Ref, identNormal, canDiscardWhitespaceAfter) 203 if !p.options.MinifyWhitespace { 204 p.print(" ") 205 } 206 if p.options.MinifyWhitespace { 207 p.print("{") 208 } else { 209 p.print("{\n") 210 } 211 indent++ 212 for _, block := range r.Blocks { 213 if p.options.AddSourceMappings { 214 p.builder.AddSourceMapping(block.Loc, "", p.css) 215 } 216 if !p.options.MinifyWhitespace { 217 p.printIndent(indent) 218 } 219 for i, sel := range block.Selectors { 220 if i > 0 { 221 if p.options.MinifyWhitespace { 222 p.print(",") 223 } else { 224 p.print(", ") 225 } 226 } 227 p.print(sel) 228 } 229 if !p.options.MinifyWhitespace { 230 p.print(" ") 231 } 232 p.printRuleBlock(block.Rules, indent, block.CloseBraceLoc) 233 if !p.options.MinifyWhitespace { 234 p.print("\n") 235 } 236 } 237 indent-- 238 if p.options.AddSourceMappings && r.CloseBraceLoc.Start != 0 { 239 p.builder.AddSourceMapping(r.CloseBraceLoc, "", p.css) 240 } 241 if !p.options.MinifyWhitespace { 242 p.printIndent(indent) 243 } 244 p.print("}") 245 246 case *css_ast.RKnownAt: 247 p.print("@") 248 whitespace := mayNeedWhitespaceAfter 249 if len(r.Prelude) == 0 { 250 whitespace = canDiscardWhitespaceAfter 251 } 252 p.printIdent(r.AtToken, identNormal, whitespace) 253 if (!p.options.MinifyWhitespace && r.Rules != nil) || len(r.Prelude) > 0 { 254 p.print(" ") 255 } 256 p.printTokens(r.Prelude, printTokensOpts{}) 257 if r.Rules == nil { 258 p.print(";") 259 } else { 260 if !p.options.MinifyWhitespace && len(r.Prelude) > 0 { 261 p.print(" ") 262 } 263 p.printRuleBlock(r.Rules, indent, r.CloseBraceLoc) 264 } 265 266 case *css_ast.RUnknownAt: 267 p.print("@") 268 whitespace := mayNeedWhitespaceAfter 269 if len(r.Prelude) == 0 { 270 whitespace = canDiscardWhitespaceAfter 271 } 272 p.printIdent(r.AtToken, identNormal, whitespace) 273 if (!p.options.MinifyWhitespace && len(r.Block) != 0) || len(r.Prelude) > 0 { 274 p.print(" ") 275 } 276 p.printTokens(r.Prelude, printTokensOpts{}) 277 if !p.options.MinifyWhitespace && len(r.Block) != 0 && len(r.Prelude) > 0 { 278 p.print(" ") 279 } 280 if len(r.Block) == 0 { 281 p.print(";") 282 } else { 283 p.printTokens(r.Block, printTokensOpts{}) 284 } 285 286 case *css_ast.RSelector: 287 p.printComplexSelectors(r.Selectors, indent, layoutMultiLine) 288 if !p.options.MinifyWhitespace { 289 p.print(" ") 290 } 291 p.printRuleBlock(r.Rules, indent, r.CloseBraceLoc) 292 293 case *css_ast.RQualified: 294 hasWhitespaceAfter := p.printTokens(r.Prelude, printTokensOpts{}) 295 if !hasWhitespaceAfter && !p.options.MinifyWhitespace { 296 p.print(" ") 297 } 298 p.printRuleBlock(r.Rules, indent, r.CloseBraceLoc) 299 300 case *css_ast.RDeclaration: 301 p.printIdent(r.KeyText, identNormal, canDiscardWhitespaceAfter) 302 p.print(":") 303 hasWhitespaceAfter := p.printTokens(r.Value, printTokensOpts{ 304 indent: indent, 305 isDeclaration: true, 306 }) 307 if r.Important { 308 if !hasWhitespaceAfter && !p.options.MinifyWhitespace && len(r.Value) > 0 { 309 p.print(" ") 310 } 311 p.print("!important") 312 } 313 if !omitTrailingSemicolon { 314 p.print(";") 315 } 316 317 case *css_ast.RBadDeclaration: 318 p.printTokens(r.Tokens, printTokensOpts{}) 319 if !omitTrailingSemicolon { 320 p.print(";") 321 } 322 323 case *css_ast.RComment: 324 p.printIndentedComment(indent, r.Text) 325 326 case *css_ast.RAtLayer: 327 p.print("@layer") 328 for i, parts := range r.Names { 329 if i == 0 { 330 p.print(" ") 331 } else if !p.options.MinifyWhitespace { 332 p.print(", ") 333 } else { 334 p.print(",") 335 } 336 p.print(strings.Join(parts, ".")) 337 } 338 if r.Rules == nil { 339 p.print(";") 340 } else { 341 if !p.options.MinifyWhitespace { 342 p.print(" ") 343 } 344 p.printRuleBlock(r.Rules, indent, r.CloseBraceLoc) 345 } 346 347 default: 348 panic("Internal error") 349 } 350 351 if !p.options.MinifyWhitespace { 352 p.print("\n") 353 } 354 } 355 356 func (p *printer) printIndentedComment(indent int32, text string) { 357 // Avoid generating a comment containing the character sequence "</style" 358 if !p.options.UnsupportedFeatures.Has(compat.InlineStyle) { 359 text = helpers.EscapeClosingTag(text, "/style") 360 } 361 362 // Re-indent multi-line comments 363 for { 364 newline := strings.IndexByte(text, '\n') 365 if newline == -1 { 366 break 367 } 368 p.print(text[:newline+1]) 369 if !p.options.MinifyWhitespace { 370 p.printIndent(indent) 371 } 372 text = text[newline+1:] 373 } 374 p.print(text) 375 } 376 377 func (p *printer) printRuleBlock(rules []css_ast.Rule, indent int32, closeBraceLoc logger.Loc) { 378 if p.options.MinifyWhitespace { 379 p.print("{") 380 } else { 381 p.print("{\n") 382 } 383 384 for i, decl := range rules { 385 omitTrailingSemicolon := p.options.MinifyWhitespace && i+1 == len(rules) 386 p.printRule(decl, indent+1, omitTrailingSemicolon) 387 } 388 389 if p.options.AddSourceMappings && closeBraceLoc.Start != 0 { 390 p.builder.AddSourceMapping(closeBraceLoc, "", p.css) 391 } 392 if !p.options.MinifyWhitespace { 393 p.printIndent(indent) 394 } 395 p.print("}") 396 } 397 398 type selectorLayout uint8 399 400 const ( 401 layoutMultiLine selectorLayout = iota 402 layoutSingleLine 403 ) 404 405 func (p *printer) printComplexSelectors(selectors []css_ast.ComplexSelector, indent int32, layout selectorLayout) { 406 for i, complex := range selectors { 407 if i > 0 { 408 if p.options.MinifyWhitespace { 409 p.print(",") 410 if p.options.LineLimit > 0 { 411 p.printNewlinePastLineLimit(indent) 412 } 413 } else if layout == layoutMultiLine { 414 p.print(",\n") 415 p.printIndent(indent) 416 } else { 417 p.print(", ") 418 } 419 } 420 421 for j, compound := range complex.Selectors { 422 p.printCompoundSelector(compound, j == 0, j+1 == len(complex.Selectors), indent) 423 } 424 } 425 } 426 427 func (p *printer) printCompoundSelector(sel css_ast.CompoundSelector, isFirst bool, isLast bool, indent int32) { 428 if !isFirst && sel.Combinator.Byte == 0 { 429 // A space is required in between compound selectors if there is no 430 // combinator in the middle. It's fine to convert "a + b" into "a+b" 431 // but not to convert "a b" into "ab". 432 if p.options.LineLimit <= 0 || !p.printNewlinePastLineLimit(indent) { 433 p.print(" ") 434 } 435 } 436 437 if sel.Combinator.Byte != 0 { 438 if !isFirst && !p.options.MinifyWhitespace { 439 p.print(" ") 440 } 441 442 if p.options.AddSourceMappings { 443 p.builder.AddSourceMapping(sel.Combinator.Loc, "", p.css) 444 } 445 p.css = append(p.css, sel.Combinator.Byte) 446 447 if (p.options.LineLimit <= 0 || !p.printNewlinePastLineLimit(indent)) && !p.options.MinifyWhitespace { 448 p.print(" ") 449 } 450 } 451 452 if sel.TypeSelector != nil { 453 whitespace := mayNeedWhitespaceAfter 454 if len(sel.SubclassSelectors) > 0 { 455 // There is no chance of whitespace before a subclass selector or pseudo 456 // class selector 457 whitespace = canDiscardWhitespaceAfter 458 } 459 p.printNamespacedName(*sel.TypeSelector, whitespace) 460 } 461 462 if sel.HasNestingSelector() { 463 if p.options.AddSourceMappings { 464 p.builder.AddSourceMapping(logger.Loc{Start: int32(sel.NestingSelectorLoc.GetIndex())}, "", p.css) 465 } 466 467 p.print("&") 468 } 469 470 for i, ss := range sel.SubclassSelectors { 471 whitespace := mayNeedWhitespaceAfter 472 473 // There is no chance of whitespace between subclass selectors 474 if i+1 < len(sel.SubclassSelectors) { 475 whitespace = canDiscardWhitespaceAfter 476 } 477 478 if p.options.AddSourceMappings { 479 p.builder.AddSourceMapping(ss.Range.Loc, "", p.css) 480 } 481 482 switch s := ss.Data.(type) { 483 case *css_ast.SSHash: 484 p.print("#") 485 486 // This deliberately does not use identHash. From the specification: 487 // "In <id-selector>, the <hash-token>'s value must be an identifier." 488 p.printSymbol(s.Name.Loc, s.Name.Ref, identNormal, whitespace) 489 490 case *css_ast.SSClass: 491 p.print(".") 492 p.printSymbol(s.Name.Loc, s.Name.Ref, identNormal, whitespace) 493 494 case *css_ast.SSAttribute: 495 p.print("[") 496 p.printNamespacedName(s.NamespacedName, canDiscardWhitespaceAfter) 497 if s.MatcherOp != "" { 498 p.print(s.MatcherOp) 499 printAsIdent := false 500 501 // Print the value as an identifier if it's possible 502 if css_lexer.WouldStartIdentifierWithoutEscapes(s.MatcherValue) { 503 printAsIdent = true 504 for _, c := range s.MatcherValue { 505 if !css_lexer.IsNameContinue(c) { 506 printAsIdent = false 507 break 508 } 509 } 510 } 511 512 if printAsIdent { 513 p.printIdent(s.MatcherValue, identNormal, canDiscardWhitespaceAfter) 514 } else { 515 p.printQuoted(s.MatcherValue, 0) 516 } 517 } 518 if s.MatcherModifier != 0 { 519 p.print(" ") 520 p.print(string(rune(s.MatcherModifier))) 521 } 522 p.print("]") 523 524 case *css_ast.SSPseudoClass: 525 p.printPseudoClassSelector(*s, whitespace) 526 527 case *css_ast.SSPseudoClassWithSelectorList: 528 p.print(":") 529 p.print(s.Kind.String()) 530 p.print("(") 531 if s.Index.A != "" || s.Index.B != "" { 532 p.printNthIndex(s.Index) 533 if len(s.Selectors) > 0 { 534 if p.options.MinifyWhitespace && s.Selectors[0].Selectors[0].TypeSelector == nil { 535 p.print(" of") 536 } else { 537 p.print(" of ") 538 } 539 } 540 } 541 p.printComplexSelectors(s.Selectors, indent, layoutSingleLine) 542 p.print(")") 543 544 default: 545 panic("Internal error") 546 } 547 } 548 } 549 550 func (p *printer) printNthIndex(index css_ast.NthIndex) { 551 if index.A != "" { 552 if index.A == "-1" { 553 p.print("-") 554 } else if index.A != "1" { 555 p.print(index.A) 556 } 557 p.print("n") 558 if index.B != "" { 559 if !strings.HasPrefix(index.B, "-") { 560 p.print("+") 561 } 562 p.print(index.B) 563 } 564 } else if index.B != "" { 565 p.print(index.B) 566 } 567 } 568 569 func (p *printer) printNamespacedName(nsName css_ast.NamespacedName, whitespace trailingWhitespace) { 570 if prefix := nsName.NamespacePrefix; prefix != nil { 571 if p.options.AddSourceMappings { 572 p.builder.AddSourceMapping(prefix.Range.Loc, "", p.css) 573 } 574 575 switch prefix.Kind { 576 case css_lexer.TIdent: 577 p.printIdent(prefix.Text, identNormal, canDiscardWhitespaceAfter) 578 case css_lexer.TDelimAsterisk: 579 p.print("*") 580 default: 581 panic("Internal error") 582 } 583 584 p.print("|") 585 } 586 587 if p.options.AddSourceMappings { 588 p.builder.AddSourceMapping(nsName.Name.Range.Loc, "", p.css) 589 } 590 591 switch nsName.Name.Kind { 592 case css_lexer.TIdent: 593 p.printIdent(nsName.Name.Text, identNormal, whitespace) 594 case css_lexer.TDelimAsterisk: 595 p.print("*") 596 case css_lexer.TDelimAmpersand: 597 p.print("&") 598 default: 599 panic("Internal error") 600 } 601 } 602 603 func (p *printer) printPseudoClassSelector(pseudo css_ast.SSPseudoClass, whitespace trailingWhitespace) { 604 if pseudo.IsElement { 605 p.print("::") 606 } else { 607 p.print(":") 608 } 609 610 // This checks for "nil" so we can distinguish ":is()" from ":is" 611 if pseudo.Args != nil { 612 p.printIdent(pseudo.Name, identNormal, canDiscardWhitespaceAfter) 613 p.print("(") 614 p.printTokens(pseudo.Args, printTokensOpts{}) 615 p.print(")") 616 } else { 617 p.printIdent(pseudo.Name, identNormal, whitespace) 618 } 619 } 620 621 func (p *printer) print(text string) { 622 p.css = append(p.css, text...) 623 } 624 625 func bestQuoteCharForString(text string, forURL bool) byte { 626 forURLCost := 0 627 singleCost := 2 628 doubleCost := 2 629 630 for _, c := range text { 631 switch c { 632 case '\'': 633 forURLCost++ 634 singleCost++ 635 636 case '"': 637 forURLCost++ 638 doubleCost++ 639 640 case '(', ')', ' ', '\t': 641 forURLCost++ 642 643 case '\\', '\n', '\r', '\f': 644 forURLCost++ 645 singleCost++ 646 doubleCost++ 647 } 648 } 649 650 // Quotes can sometimes be omitted for URL tokens 651 if forURL && forURLCost < singleCost && forURLCost < doubleCost { 652 return quoteForURL 653 } 654 655 // Prefer double quotes to single quotes if there is no cost difference 656 if singleCost < doubleCost { 657 return '\'' 658 } 659 660 return '"' 661 } 662 663 type printQuotedFlags uint8 664 665 const ( 666 printQuotedNoWrap printQuotedFlags = 1 << iota 667 ) 668 669 func (p *printer) printQuoted(text string, flags printQuotedFlags) { 670 p.printQuotedWithQuote(text, bestQuoteCharForString(text, false), flags) 671 } 672 673 type escapeKind uint8 674 675 const ( 676 escapeNone escapeKind = iota 677 escapeBackslash 678 escapeHex 679 ) 680 681 func (p *printer) printWithEscape(c rune, escape escapeKind, remainingText string, mayNeedWhitespaceAfter bool) { 682 var temp [utf8.UTFMax]byte 683 684 if escape == escapeBackslash && ((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F')) { 685 // Hexadecimal characters cannot use a plain backslash escape 686 escape = escapeHex 687 } 688 689 switch escape { 690 case escapeNone: 691 width := utf8.EncodeRune(temp[:], c) 692 p.css = append(p.css, temp[:width]...) 693 694 case escapeBackslash: 695 p.css = append(p.css, '\\') 696 width := utf8.EncodeRune(temp[:], c) 697 p.css = append(p.css, temp[:width]...) 698 699 case escapeHex: 700 text := fmt.Sprintf("\\%x", c) 701 p.css = append(p.css, text...) 702 703 // Make sure the next character is not interpreted as part of the escape sequence 704 if len(text) < 1+6 { 705 if next := utf8.RuneLen(c); next < len(remainingText) { 706 c = rune(remainingText[next]) 707 if c == ' ' || c == '\t' || (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F') { 708 p.css = append(p.css, ' ') 709 } 710 } else if mayNeedWhitespaceAfter { 711 // If the last character is a hexadecimal escape, print a space afterwards 712 // for the escape sequence to consume. That way we're sure it won't 713 // accidentally consume a semantically significant space afterward. 714 p.css = append(p.css, ' ') 715 } 716 } 717 } 718 } 719 720 // Note: This function is hot in profiles 721 func (p *printer) printQuotedWithQuote(text string, quote byte, flags printQuotedFlags) { 722 if quote != quoteForURL { 723 p.css = append(p.css, quote) 724 } 725 726 n := len(text) 727 i := 0 728 runStart := 0 729 730 // Only compute the line length if necessary 731 var startLineLength int 732 wrapLongLines := false 733 if p.options.LineLimit > 0 && quote != quoteForURL && (flags&printQuotedNoWrap) == 0 { 734 startLineLength = p.currentLineLength() 735 if startLineLength > p.options.LineLimit { 736 startLineLength = p.options.LineLimit 737 } 738 wrapLongLines = true 739 } 740 741 for i < n { 742 // Wrap long lines that are over the limit using escaped newlines 743 if wrapLongLines && startLineLength+i >= p.options.LineLimit { 744 if runStart < i { 745 p.css = append(p.css, text[runStart:i]...) 746 runStart = i 747 } 748 p.css = append(p.css, "\\\n"...) 749 startLineLength -= p.options.LineLimit 750 } 751 752 c, width := utf8.DecodeRuneInString(text[i:]) 753 escape := escapeNone 754 755 switch c { 756 case '\x00', '\r', '\n', '\f': 757 // Use a hexadecimal escape for characters that would be invalid escapes 758 escape = escapeHex 759 760 case '\\', rune(quote): 761 escape = escapeBackslash 762 763 case '(', ')', ' ', '\t', '"', '\'': 764 // These characters must be escaped in URL tokens 765 if quote == quoteForURL { 766 escape = escapeBackslash 767 } 768 769 case '/': 770 // Avoid generating the sequence "</style" in CSS code 771 if !p.options.UnsupportedFeatures.Has(compat.InlineStyle) && i >= 1 && text[i-1] == '<' && i+6 <= len(text) && strings.EqualFold(text[i+1:i+6], "style") { 772 escape = escapeBackslash 773 } 774 775 default: 776 if (p.options.ASCIIOnly && c >= 0x80) || c == '\uFEFF' { 777 escape = escapeHex 778 } 779 } 780 781 if escape != escapeNone { 782 if runStart < i { 783 p.css = append(p.css, text[runStart:i]...) 784 } 785 p.printWithEscape(c, escape, text[i:], false) 786 runStart = i + width 787 } 788 i += width 789 } 790 791 if runStart < n { 792 p.css = append(p.css, text[runStart:]...) 793 } 794 795 if quote != quoteForURL { 796 p.css = append(p.css, quote) 797 } 798 } 799 800 func (p *printer) currentLineLength() int { 801 css := p.css 802 n := len(css) 803 stop := p.oldLineEnd 804 805 // Update "oldLineStart" to the start of the current line 806 for i := n; i > stop; i-- { 807 if c := css[i-1]; c == '\r' || c == '\n' { 808 p.oldLineStart = i 809 break 810 } 811 } 812 813 p.oldLineEnd = n 814 return n - p.oldLineStart 815 } 816 817 func (p *printer) printNewlinePastLineLimit(indent int32) bool { 818 if p.currentLineLength() < p.options.LineLimit { 819 return false 820 } 821 p.print("\n") 822 if !p.options.MinifyWhitespace { 823 p.printIndent(indent) 824 } 825 return true 826 } 827 828 type identMode uint8 829 830 const ( 831 identNormal identMode = iota 832 identHash 833 identDimensionUnit 834 identDimensionUnitAfterExponent 835 ) 836 837 type trailingWhitespace uint8 838 839 const ( 840 mayNeedWhitespaceAfter trailingWhitespace = iota 841 canDiscardWhitespaceAfter 842 ) 843 844 // Note: This function is hot in profiles 845 func (p *printer) printIdent(text string, mode identMode, whitespace trailingWhitespace) { 846 n := len(text) 847 848 // Special escape behavior for the first character 849 initialEscape := escapeNone 850 switch mode { 851 case identNormal: 852 if !css_lexer.WouldStartIdentifierWithoutEscapes(text) { 853 initialEscape = escapeBackslash 854 } 855 case identDimensionUnit, identDimensionUnitAfterExponent: 856 if !css_lexer.WouldStartIdentifierWithoutEscapes(text) { 857 initialEscape = escapeBackslash 858 } else if n > 0 { 859 if c := text[0]; c >= '0' && c <= '9' { 860 // Unit: "2x" 861 initialEscape = escapeHex 862 } else if (c == 'e' || c == 'E') && mode != identDimensionUnitAfterExponent { 863 if n >= 2 && text[1] >= '0' && text[1] <= '9' { 864 // Unit: "e2x" 865 initialEscape = escapeHex 866 } else if n >= 3 && text[1] == '-' && text[2] >= '0' && text[2] <= '9' { 867 // Unit: "e-2x" 868 initialEscape = escapeHex 869 } 870 } 871 } 872 } 873 874 // Fast path: the identifier does not need to be escaped. This fast path is 875 // important for performance. For example, doing this sped up end-to-end 876 // parsing and printing of a large CSS file from 84ms to 66ms (around 25% 877 // faster). 878 if initialEscape == escapeNone { 879 for i := 0; i < n; i++ { 880 if c := text[i]; c >= 0x80 || !css_lexer.IsNameContinue(rune(c)) { 881 goto slowPath 882 } 883 } 884 p.css = append(p.css, text...) 885 return 886 slowPath: 887 } 888 889 // Slow path: the identifier needs to be escaped 890 for i, c := range text { 891 escape := escapeNone 892 893 if p.options.ASCIIOnly && c >= 0x80 { 894 escape = escapeHex 895 } else if c == '\r' || c == '\n' || c == '\f' || c == '\uFEFF' { 896 // Use a hexadecimal escape for characters that would be invalid escapes 897 escape = escapeHex 898 } else { 899 // Escape non-identifier characters 900 if !css_lexer.IsNameContinue(c) { 901 escape = escapeBackslash 902 } 903 904 // Special escape behavior for the first character 905 if i == 0 && initialEscape != escapeNone { 906 escape = initialEscape 907 } 908 } 909 910 // If the last character is a hexadecimal escape, print a space afterwards 911 // for the escape sequence to consume. That way we're sure it won't 912 // accidentally consume a semantically significant space afterward. 913 mayNeedWhitespaceAfter := whitespace == mayNeedWhitespaceAfter && escape != escapeNone && i+utf8.RuneLen(c) == n 914 p.printWithEscape(c, escape, text[i:], mayNeedWhitespaceAfter) 915 } 916 } 917 918 func (p *printer) printSymbol(loc logger.Loc, ref ast.Ref, mode identMode, whitespace trailingWhitespace) { 919 ref = ast.FollowSymbols(p.symbols, ref) 920 originalName := p.symbols.Get(ref).OriginalName 921 name, ok := p.options.LocalNames[ref] 922 if !ok { 923 name = originalName 924 } 925 if p.options.AddSourceMappings { 926 if originalName == name { 927 originalName = "" 928 } 929 p.builder.AddSourceMapping(loc, originalName, p.css) 930 } 931 p.printIdent(name, mode, whitespace) 932 } 933 934 func (p *printer) printIndent(indent int32) { 935 n := int(indent) 936 if p.options.LineLimit > 0 && n*2 >= p.options.LineLimit { 937 n = p.options.LineLimit / 2 938 } 939 for i := 0; i < n; i++ { 940 p.css = append(p.css, " "...) 941 } 942 } 943 944 type printTokensOpts struct { 945 indent int32 946 multiLineCommaPeriod uint8 947 isDeclaration bool 948 } 949 950 func functionMultiLineCommaPeriod(token css_ast.Token) uint8 { 951 if token.Kind == css_lexer.TFunction { 952 commaCount := 0 953 for _, t := range *token.Children { 954 if t.Kind == css_lexer.TComma { 955 commaCount++ 956 } 957 } 958 959 switch strings.ToLower(token.Text) { 960 case "linear-gradient", "radial-gradient", "conic-gradient", 961 "repeating-linear-gradient", "repeating-radial-gradient", "repeating-conic-gradient": 962 if commaCount >= 2 { 963 return 1 964 } 965 966 case "matrix": 967 if commaCount == 5 { 968 return 2 969 } 970 971 case "matrix3d": 972 if commaCount == 15 { 973 return 4 974 } 975 } 976 } 977 return 0 978 } 979 980 func (p *printer) printTokens(tokens []css_ast.Token, opts printTokensOpts) bool { 981 hasWhitespaceAfter := len(tokens) > 0 && (tokens[0].Whitespace&css_ast.WhitespaceBefore) != 0 982 983 // Pretty-print long comma-separated declarations of 3 or more items 984 commaPeriod := int(opts.multiLineCommaPeriod) 985 if !p.options.MinifyWhitespace && opts.isDeclaration { 986 commaCount := 0 987 for _, t := range tokens { 988 if t.Kind == css_lexer.TComma { 989 commaCount++ 990 if commaCount >= 2 { 991 commaPeriod = 1 992 break 993 } 994 } 995 if t.Kind == css_lexer.TFunction && functionMultiLineCommaPeriod(t) > 0 { 996 commaPeriod = 1 997 break 998 } 999 } 1000 } 1001 1002 commaCount := 0 1003 for i, t := range tokens { 1004 if t.Kind == css_lexer.TComma { 1005 commaCount++ 1006 } 1007 if t.Kind == css_lexer.TWhitespace { 1008 hasWhitespaceAfter = true 1009 continue 1010 } 1011 if hasWhitespaceAfter { 1012 if commaPeriod > 0 && (i == 0 || (tokens[i-1].Kind == css_lexer.TComma && commaCount%commaPeriod == 0)) { 1013 p.print("\n") 1014 p.printIndent(opts.indent + 1) 1015 } else if p.options.LineLimit <= 0 || !p.printNewlinePastLineLimit(opts.indent+1) { 1016 p.print(" ") 1017 } 1018 } 1019 hasWhitespaceAfter = (t.Whitespace&css_ast.WhitespaceAfter) != 0 || 1020 (i+1 < len(tokens) && (tokens[i+1].Whitespace&css_ast.WhitespaceBefore) != 0) 1021 1022 whitespace := mayNeedWhitespaceAfter 1023 if !hasWhitespaceAfter { 1024 whitespace = canDiscardWhitespaceAfter 1025 } 1026 1027 if p.options.AddSourceMappings { 1028 p.builder.AddSourceMapping(t.Loc, "", p.css) 1029 } 1030 1031 switch t.Kind { 1032 case css_lexer.TIdent: 1033 p.printIdent(t.Text, identNormal, whitespace) 1034 1035 case css_lexer.TSymbol: 1036 ref := ast.Ref{SourceIndex: p.options.InputSourceIndex, InnerIndex: t.PayloadIndex} 1037 p.printSymbol(t.Loc, ref, identNormal, whitespace) 1038 1039 case css_lexer.TFunction: 1040 p.printIdent(t.Text, identNormal, whitespace) 1041 p.print("(") 1042 1043 case css_lexer.TDimension: 1044 value := t.DimensionValue() 1045 p.print(value) 1046 mode := identDimensionUnit 1047 if strings.ContainsAny(value, "eE") { 1048 mode = identDimensionUnitAfterExponent 1049 } 1050 p.printIdent(t.DimensionUnit(), mode, whitespace) 1051 1052 case css_lexer.TAtKeyword: 1053 p.print("@") 1054 p.printIdent(t.Text, identNormal, whitespace) 1055 1056 case css_lexer.THash: 1057 p.print("#") 1058 p.printIdent(t.Text, identHash, whitespace) 1059 1060 case css_lexer.TString: 1061 p.printQuoted(t.Text, 0) 1062 1063 case css_lexer.TURL: 1064 record := p.importRecords[t.PayloadIndex] 1065 text := record.Path.Text 1066 tryToAvoidQuote := true 1067 var flags printQuotedFlags 1068 if record.Flags.Has(ast.ContainsUniqueKey) { 1069 flags |= printQuotedNoWrap 1070 1071 // If the caller will be substituting a path here later using string 1072 // substitution, then we can't be sure that it will form a valid URL 1073 // token when unquoted (e.g. it may contain spaces). So we need to 1074 // quote the unique key here just in case. For more info see this 1075 // issue: https://github.com/evanw/esbuild/issues/3410 1076 tryToAvoidQuote = false 1077 } else if p.options.LineLimit > 0 && p.currentLineLength()+len(text) >= p.options.LineLimit { 1078 tryToAvoidQuote = false 1079 } 1080 p.print("url(") 1081 p.printQuotedWithQuote(text, bestQuoteCharForString(text, tryToAvoidQuote), flags) 1082 p.print(")") 1083 p.recordImportPathForMetafile(t.PayloadIndex) 1084 1085 case css_lexer.TUnterminatedString: 1086 // We must end this with a newline so that this string stays unterminated 1087 p.print(t.Text) 1088 p.print("\n") 1089 if !p.options.MinifyWhitespace { 1090 p.printIndent(opts.indent) 1091 } 1092 hasWhitespaceAfter = false 1093 1094 default: 1095 p.print(t.Text) 1096 } 1097 1098 if t.Children != nil { 1099 childCommaPeriod := uint8(0) 1100 1101 if commaPeriod > 0 && opts.isDeclaration { 1102 childCommaPeriod = functionMultiLineCommaPeriod(t) 1103 } 1104 1105 if childCommaPeriod > 0 { 1106 opts.indent++ 1107 if !p.options.MinifyWhitespace { 1108 p.print("\n") 1109 p.printIndent(opts.indent + 1) 1110 } 1111 } 1112 1113 p.printTokens(*t.Children, printTokensOpts{ 1114 indent: opts.indent, 1115 multiLineCommaPeriod: childCommaPeriod, 1116 }) 1117 1118 if childCommaPeriod > 0 { 1119 opts.indent-- 1120 } 1121 1122 switch t.Kind { 1123 case css_lexer.TFunction: 1124 p.print(")") 1125 1126 case css_lexer.TOpenParen: 1127 p.print(")") 1128 1129 case css_lexer.TOpenBrace: 1130 p.print("}") 1131 1132 case css_lexer.TOpenBracket: 1133 p.print("]") 1134 } 1135 } 1136 } 1137 if hasWhitespaceAfter { 1138 p.print(" ") 1139 } 1140 return hasWhitespaceAfter 1141 }