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  }