package css_printer import ( "fmt" "strings" "unicode/utf8" "github.com/evanw/esbuild/internal/ast" "github.com/evanw/esbuild/internal/compat" "github.com/evanw/esbuild/internal/config" "github.com/evanw/esbuild/internal/css_ast" "github.com/evanw/esbuild/internal/css_lexer" "github.com/evanw/esbuild/internal/helpers" "github.com/evanw/esbuild/internal/logger" "github.com/evanw/esbuild/internal/sourcemap" ) const quoteForURL byte = 0 type printer struct { options Options symbols ast.SymbolMap importRecords []ast.ImportRecord css []byte hasLegalComment map[string]struct{} extractedLegalComments []string jsonMetadataImports []string builder sourcemap.ChunkBuilder oldLineStart int oldLineEnd int } type Options struct { // This will be present if the input file had a source map. In that case we // want to map all the way back to the original input file(s). InputSourceMap *sourcemap.SourceMap // If we're writing out a source map, this table of line start indices lets // us do binary search on to figure out what line a given AST node came from LineOffsetTables []sourcemap.LineOffsetTable // Local symbol renaming results go here LocalNames map[ast.Ref]string LineLimit int InputSourceIndex uint32 UnsupportedFeatures compat.CSSFeature MinifyWhitespace bool ASCIIOnly bool SourceMap config.SourceMap AddSourceMappings bool LegalComments config.LegalComments NeedsMetafile bool MetafileFormat config.MetafileFormat } type PrintResult struct { CSS []byte ExtractedLegalComments []string JSONMetadataImports []string // This source map chunk just contains the VLQ-encoded offsets for the "CSS" // field above. It's not a full source map. The bundler will be joining many // source map chunks together to form the final source map. SourceMapChunk sourcemap.Chunk } func Print(tree css_ast.AST, symbols ast.SymbolMap, options Options) PrintResult { p := printer{ options: options, symbols: symbols, importRecords: tree.ImportRecords, builder: sourcemap.MakeChunkBuilder(options.InputSourceMap, options.LineOffsetTables, options.ASCIIOnly), } for _, rule := range tree.Rules { p.printRule(rule, 0, false) } result := PrintResult{ CSS: p.css, ExtractedLegalComments: p.extractedLegalComments, JSONMetadataImports: p.jsonMetadataImports, } if options.SourceMap != config.SourceMapNone { // This is expensive. Only do this if it's necessary. For example, skipping // this if it's not needed sped up end-to-end parsing and printing of a // large CSS file from 66ms to 52ms (around 25% faster). result.SourceMapChunk = p.builder.GenerateChunk(p.css) } return result } func (p *printer) recordImportPathForMetafile(importRecordIndex uint32) { if p.options.NeedsMetafile { record := p.importRecords[importRecordIndex] external := "" if (record.Flags & ast.ShouldNotBeExternalInMetafile) == 0 { external = p.options.MetafileFormat.MaybeRemoveWhitespace(",\n \"external\": true") } p.jsonMetadataImports = append(p.jsonMetadataImports, fmt.Sprintf( p.options.MetafileFormat.MaybeRemoveWhitespace("\n {\n \"path\": %s,\n \"kind\": %s%s\n }"), helpers.QuoteForJSON(record.Path.Text, p.options.ASCIIOnly), helpers.QuoteForJSON(record.Kind.StringForMetafile(), p.options.ASCIIOnly), external)) } } func (p *printer) printRule(rule css_ast.Rule, indent int32, omitTrailingSemicolon bool) { if r, ok := rule.Data.(*css_ast.RComment); ok { switch p.options.LegalComments { case config.LegalCommentsNone: return case config.LegalCommentsEndOfFile, config.LegalCommentsLinkedWithComment, config.LegalCommentsExternalWithoutComment: // Don't record the same legal comment more than once per file if p.hasLegalComment == nil { p.hasLegalComment = make(map[string]struct{}) } else if _, ok := p.hasLegalComment[r.Text]; ok { return } p.hasLegalComment[r.Text] = struct{}{} p.extractedLegalComments = append(p.extractedLegalComments, r.Text) return } } if p.options.LineLimit > 0 { p.printNewlinePastLineLimit(indent) } if p.options.AddSourceMappings { shouldPrintMapping := true if indent == 0 || p.options.MinifyWhitespace { switch rule.Data.(type) { case *css_ast.RSelector, *css_ast.RQualified, *css_ast.RBadDeclaration: // These rules will begin with a potentially more accurate mapping. We // shouldn't print a mapping here if there's no indent in between this // mapping and the rule. shouldPrintMapping = false } } if shouldPrintMapping { p.builder.AddSourceMapping(rule.Loc, "", p.css) } } if !p.options.MinifyWhitespace { p.printIndent(indent) } switch r := rule.Data.(type) { case *css_ast.RAtCharset: // It's not valid to remove the space in between these two tokens p.print("@charset ") // It's not valid to print the string with single quotes p.printQuotedWithQuote(r.Encoding, '"', 0) p.print(";") case *css_ast.RAtImport: if p.options.MinifyWhitespace { p.print("@import") } else { p.print("@import ") } record := p.importRecords[r.ImportRecordIndex] var flags printQuotedFlags if record.Flags.Has(ast.ContainsUniqueKey) { flags |= printQuotedNoWrap } p.printQuoted(record.Path.Text, flags) p.recordImportPathForMetafile(r.ImportRecordIndex) if conditions := r.ImportConditions; conditions != nil { space := !p.options.MinifyWhitespace if len(conditions.Layers) > 0 { if space { p.print(" ") } p.printTokens(conditions.Layers, printTokensOpts{}) space = true } if len(conditions.Supports) > 0 { if space { p.print(" ") } p.printTokens(conditions.Supports, printTokensOpts{}) space = true } if len(conditions.Queries) > 0 { if space { p.print(" ") } for i, query := range conditions.Queries { if i > 0 { if p.options.MinifyWhitespace { p.print(",") } else { p.print(", ") } } p.printMediaQuery(query, 0) } } } p.print(";") case *css_ast.RAtKeyframes: p.print("@") p.printIdent(r.AtToken, identNormal, mayNeedWhitespaceAfter) p.print(" ") p.printSymbol(r.Name.Loc, r.Name.Ref, identNormal, canDiscardWhitespaceAfter) if !p.options.MinifyWhitespace { p.print(" ") } if p.options.MinifyWhitespace { p.print("{") } else { p.print("{\n") } indent++ for _, block := range r.Blocks { if p.options.AddSourceMappings { p.builder.AddSourceMapping(block.Loc, "", p.css) } if !p.options.MinifyWhitespace { p.printIndent(indent) } for i, sel := range block.Selectors { if i > 0 { if p.options.MinifyWhitespace { p.print(",") } else { p.print(", ") } } p.print(sel) } if !p.options.MinifyWhitespace { p.print(" ") } p.printRuleBlock(block.Rules, indent, block.CloseBraceLoc) if !p.options.MinifyWhitespace { p.print("\n") } } indent-- if p.options.AddSourceMappings && r.CloseBraceLoc.Start != 0 { p.builder.AddSourceMapping(r.CloseBraceLoc, "", p.css) } if !p.options.MinifyWhitespace { p.printIndent(indent) } p.print("}") case *css_ast.RKnownAt: p.print("@") whitespace := mayNeedWhitespaceAfter if len(r.Prelude) == 0 { whitespace = canDiscardWhitespaceAfter } p.printIdent(r.AtToken, identNormal, whitespace) if (!p.options.MinifyWhitespace && r.Rules != nil) || len(r.Prelude) > 0 { p.print(" ") } p.printTokens(r.Prelude, printTokensOpts{}) if r.Rules == nil { p.print(";") } else { if !p.options.MinifyWhitespace && len(r.Prelude) > 0 { p.print(" ") } p.printRuleBlock(r.Rules, indent, r.CloseBraceLoc) } case *css_ast.RUnknownAt: p.print("@") whitespace := mayNeedWhitespaceAfter if len(r.Prelude) == 0 { whitespace = canDiscardWhitespaceAfter } p.printIdent(r.AtToken, identNormal, whitespace) if (!p.options.MinifyWhitespace && len(r.Block) != 0) || len(r.Prelude) > 0 { p.print(" ") } p.printTokens(r.Prelude, printTokensOpts{}) if !p.options.MinifyWhitespace && len(r.Block) != 0 && len(r.Prelude) > 0 { p.print(" ") } if len(r.Block) == 0 { p.print(";") } else { p.printTokens(r.Block, printTokensOpts{}) } case *css_ast.RSelector: p.printComplexSelectors(r.Selectors, indent, layoutMultiLine) if !p.options.MinifyWhitespace { p.print(" ") } p.printRuleBlock(r.Rules, indent, r.CloseBraceLoc) case *css_ast.RQualified: hasWhitespaceAfter := p.printTokens(r.Prelude, printTokensOpts{}) if !hasWhitespaceAfter && !p.options.MinifyWhitespace { p.print(" ") } p.printRuleBlock(r.Rules, indent, r.CloseBraceLoc) case *css_ast.RDeclaration: p.printIdent(r.KeyText, identNormal, canDiscardWhitespaceAfter) p.print(":") hasWhitespaceAfter := p.printTokens(r.Value, printTokensOpts{ indent: indent, isDeclaration: true, }) if r.Important { if !hasWhitespaceAfter && !p.options.MinifyWhitespace && len(r.Value) > 0 { p.print(" ") } p.print("!important") } if !omitTrailingSemicolon { p.print(";") } case *css_ast.RBadDeclaration: p.printTokens(r.Tokens, printTokensOpts{}) if !omitTrailingSemicolon { p.print(";") } case *css_ast.RComment: p.printIndentedComment(indent, r.Text) case *css_ast.RAtLayer: p.print("@layer") for i, parts := range r.Names { if i == 0 { p.print(" ") } else if !p.options.MinifyWhitespace { p.print(", ") } else { p.print(",") } p.print(strings.Join(parts, ".")) } if r.Rules == nil { p.print(";") } else { if !p.options.MinifyWhitespace { p.print(" ") } p.printRuleBlock(r.Rules, indent, r.CloseBraceLoc) } case *css_ast.RAtMedia: p.print("@media") var flags mqFlags if p.options.MinifyWhitespace { flags = mqAfterIdentifier } else { p.print(" ") } for i, query := range r.Queries { if i > 0 { if p.options.MinifyWhitespace { p.print(",") } else { p.print(", ") } } p.printMediaQuery(query, flags) flags = 0 } if !p.options.MinifyWhitespace && len(r.Queries) > 0 { p.print(" ") } p.printRuleBlock(r.Rules, indent, r.CloseBraceLoc) case *css_ast.RAtScope: p.print("@scope") if len(r.Start) > 0 { if p.options.MinifyWhitespace { p.print("(") } else { p.print(" (") } p.printComplexSelectors(r.Start, indent, layoutSingleLine) p.print(")") } if len(r.End) > 0 { if p.options.MinifyWhitespace { p.print("to (") } else { p.print(" to (") } p.printComplexSelectors(r.End, indent, layoutSingleLine) p.print(")") } if !p.options.MinifyWhitespace { p.print(" ") } p.printRuleBlock(r.Rules, indent, r.CloseBraceLoc) default: panic("Internal error") } if !p.options.MinifyWhitespace { p.print("\n") } } type mqFlags uint8 const ( mqNeedsParens mqFlags = 1 << iota mqAfterIdentifier ) func (p *printer) printMediaQuery(query css_ast.MediaQuery, flags mqFlags) { if q, ok := query.Data.(*css_ast.MQArbitraryTokens); ok { if (flags & mqAfterIdentifier) != 0 { p.print(" ") } p.printTokens(q.Tokens, printTokensOpts{}) return } switch q := query.Data.(type) { case *css_ast.MQType: if (flags & mqAfterIdentifier) != 0 { p.print(" ") } if p.options.AddSourceMappings { p.builder.AddSourceMapping(query.Loc, "", p.css) } switch q.Op { case css_ast.MQTypeOpNot: p.print("not ") case css_ast.MQTypeOpOnly: p.print("only ") } p.printIdent(q.Type, identNormal, 0) if q.AndOrNull.Data != nil { p.print(" and ") var flags mqFlags if binary, ok := q.AndOrNull.Data.(*css_ast.MQBinary); ok && binary.Op == css_ast.MQBinaryOpOr { flags = mqNeedsParens } p.printMediaQuery(q.AndOrNull, flags) } case *css_ast.MQNot: if (flags & mqNeedsParens) != 0 { p.print("(") } else if (flags & mqAfterIdentifier) != 0 { p.print(" ") } if p.options.AddSourceMappings { p.builder.AddSourceMapping(query.Loc, "", p.css) } p.print("not ") p.printMediaQuery(q.Inner, mqNeedsParens) if (flags & mqNeedsParens) != 0 { p.print(")") } case *css_ast.MQBinary: if (flags & mqNeedsParens) != 0 { p.print("(") } for i, inner := range q.Terms { if i > 0 { if !p.options.MinifyWhitespace { p.print(" ") } switch q.Op { case css_ast.MQBinaryOpAnd: p.print("and ") case css_ast.MQBinaryOpOr: p.print("or ") } } p.printMediaQuery(inner, mqNeedsParens) } if (flags & mqNeedsParens) != 0 { p.print(")") } case *css_ast.MQPlainOrBoolean: p.print("(") if p.options.AddSourceMappings { p.builder.AddSourceMapping(query.Loc, "", p.css) } p.printIdent(q.Name, identNormal, 0) if q.ValueOrNil != nil { if p.options.MinifyWhitespace { p.print(":") } else { p.print(": ") } p.printTokens(q.ValueOrNil, printTokensOpts{}) } p.print(")") case *css_ast.MQRange: space := " " if p.options.MinifyWhitespace { space = "" } p.print("(") if q.BeforeCmp != css_ast.MQCmpNone { p.printTokens(q.Before, printTokensOpts{}) p.print(space) p.print(q.BeforeCmp.String()) p.print(space) } if p.options.AddSourceMappings { p.builder.AddSourceMapping(q.NameLoc, "", p.css) } p.printIdent(q.Name, identNormal, 0) if q.AfterCmp != css_ast.MQCmpNone { p.print(space) p.print(q.AfterCmp.String()) p.print(space) p.printTokens(q.After, printTokensOpts{}) } p.print(")") default: panic("Internal error") } } func (p *printer) printIndentedComment(indent int32, text string) { // Avoid generating a comment containing the character sequence " 0 { if p.options.MinifyWhitespace { p.print(",") if p.options.LineLimit > 0 { p.printNewlinePastLineLimit(indent) } } else if layout == layoutMultiLine { p.print(",\n") p.printIndent(indent) } else { p.print(", ") } } for j, compound := range complex.Selectors { p.printCompoundSelector(compound, j == 0, indent) } } } func (p *printer) printCompoundSelector(sel css_ast.CompoundSelector, isFirst bool, indent int32) { if !isFirst && sel.Combinator.Byte == 0 { // A space is required in between compound selectors if there is no // combinator in the middle. It's fine to convert "a + b" into "a+b" // but not to convert "a b" into "ab". if p.options.LineLimit <= 0 || !p.printNewlinePastLineLimit(indent) { p.print(" ") } } if sel.Combinator.Byte != 0 { if !isFirst && !p.options.MinifyWhitespace { p.print(" ") } if p.options.AddSourceMappings { p.builder.AddSourceMapping(sel.Combinator.Loc, "", p.css) } p.css = append(p.css, sel.Combinator.Byte) if (p.options.LineLimit <= 0 || !p.printNewlinePastLineLimit(indent)) && !p.options.MinifyWhitespace { p.print(" ") } } if sel.TypeSelector != nil { whitespace := mayNeedWhitespaceAfter if len(sel.SubclassSelectors) > 0 { // There is no chance of whitespace before a subclass selector or pseudo // class selector whitespace = canDiscardWhitespaceAfter } p.printNamespacedName(*sel.TypeSelector, whitespace) } for _, loc := range sel.NestingSelectorLocs { if p.options.AddSourceMappings { p.builder.AddSourceMapping(loc, "", p.css) } p.print("&") } for i, ss := range sel.SubclassSelectors { whitespace := mayNeedWhitespaceAfter // There is no chance of whitespace between subclass selectors if i+1 < len(sel.SubclassSelectors) { whitespace = canDiscardWhitespaceAfter } if p.options.AddSourceMappings { p.builder.AddSourceMapping(ss.Range.Loc, "", p.css) } switch s := ss.Data.(type) { case *css_ast.SSHash: p.print("#") // This deliberately does not use identHash. From the specification: // "In , the 's value must be an identifier." p.printSymbol(s.Name.Loc, s.Name.Ref, identNormal, whitespace) case *css_ast.SSClass: p.print(".") p.printSymbol(s.Name.Loc, s.Name.Ref, identNormal, whitespace) case *css_ast.SSAttribute: p.print("[") p.printNamespacedName(s.NamespacedName, canDiscardWhitespaceAfter) if s.MatcherOp != "" { p.print(s.MatcherOp) printAsIdent := false // Print the value as an identifier if it's possible if css_lexer.WouldStartIdentifierWithoutEscapes(s.MatcherValue) { printAsIdent = true for _, c := range s.MatcherValue { if !css_lexer.IsNameContinue(c) { printAsIdent = false break } } } if printAsIdent { p.printIdent(s.MatcherValue, identNormal, canDiscardWhitespaceAfter) } else { p.printQuoted(s.MatcherValue, 0) } } if s.MatcherModifier != 0 { p.print(" ") p.print(string(rune(s.MatcherModifier))) } p.print("]") case *css_ast.SSPseudoClass: p.printPseudoClassSelector(*s, whitespace) case *css_ast.SSPseudoClassWithSelectorList: p.print(":") p.print(s.Kind.String()) p.print("(") if s.Index.A != "" || s.Index.B != "" { p.printNthIndex(s.Index) if len(s.Selectors) > 0 { if p.options.MinifyWhitespace && s.Selectors[0].Selectors[0].TypeSelector == nil { p.print(" of") } else { p.print(" of ") } } } p.printComplexSelectors(s.Selectors, indent, layoutSingleLine) p.print(")") default: panic("Internal error") } } } func (p *printer) printNthIndex(index css_ast.NthIndex) { if index.A != "" { if index.A == "-1" { p.print("-") } else if index.A != "1" { p.print(index.A) } p.print("n") if index.B != "" { if !strings.HasPrefix(index.B, "-") { p.print("+") } p.print(index.B) } } else if index.B != "" { p.print(index.B) } } func (p *printer) printNamespacedName(nsName css_ast.NamespacedName, whitespace trailingWhitespace) { if prefix := nsName.NamespacePrefix; prefix != nil { if p.options.AddSourceMappings { p.builder.AddSourceMapping(prefix.Range.Loc, "", p.css) } switch prefix.Kind { case css_lexer.TIdent: p.printIdent(prefix.Text, identNormal, canDiscardWhitespaceAfter) case css_lexer.TDelimAsterisk: p.print("*") default: panic("Internal error") } p.print("|") } if p.options.AddSourceMappings { p.builder.AddSourceMapping(nsName.Name.Range.Loc, "", p.css) } switch nsName.Name.Kind { case css_lexer.TIdent: p.printIdent(nsName.Name.Text, identNormal, whitespace) case css_lexer.TDelimAsterisk: p.print("*") case css_lexer.TDelimAmpersand: p.print("&") default: panic("Internal error") } } func (p *printer) printPseudoClassSelector(pseudo css_ast.SSPseudoClass, whitespace trailingWhitespace) { if pseudo.IsElement { p.print("::") } else { p.print(":") } // This checks for "nil" so we can distinguish ":is()" from ":is" if pseudo.Args != nil { p.printIdent(pseudo.Name, identNormal, canDiscardWhitespaceAfter) p.print("(") p.printTokens(pseudo.Args, printTokensOpts{}) p.print(")") } else { p.printIdent(pseudo.Name, identNormal, whitespace) } } func (p *printer) print(text string) { p.css = append(p.css, text...) } func bestQuoteCharForString(text string, forURL bool) byte { forURLCost := 0 singleCost := 2 doubleCost := 2 for _, c := range text { switch c { case '\'': forURLCost++ singleCost++ case '"': forURLCost++ doubleCost++ case '(', ')', ' ', '\t': forURLCost++ case '\\', '\n', '\r', '\f': forURLCost++ singleCost++ doubleCost++ } } // Quotes can sometimes be omitted for URL tokens if forURL && forURLCost < singleCost && forURLCost < doubleCost { return quoteForURL } // Prefer double quotes to single quotes if there is no cost difference if singleCost < doubleCost { return '\'' } return '"' } type printQuotedFlags uint8 const ( printQuotedNoWrap printQuotedFlags = 1 << iota ) func (p *printer) printQuoted(text string, flags printQuotedFlags) { p.printQuotedWithQuote(text, bestQuoteCharForString(text, false), flags) } type escapeKind uint8 const ( escapeNone escapeKind = iota escapeBackslash escapeHex ) func (p *printer) printWithEscape(c rune, escape escapeKind, remainingText string, mayNeedWhitespaceAfter bool) { var temp [utf8.UTFMax]byte if escape == escapeBackslash && ((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F')) { // Hexadecimal characters cannot use a plain backslash escape escape = escapeHex } switch escape { case escapeNone: width := utf8.EncodeRune(temp[:], c) p.css = append(p.css, temp[:width]...) case escapeBackslash: p.css = append(p.css, '\\') width := utf8.EncodeRune(temp[:], c) p.css = append(p.css, temp[:width]...) case escapeHex: text := fmt.Sprintf("\\%x", c) p.css = append(p.css, text...) // Make sure the next character is not interpreted as part of the escape sequence if len(text) < 1+6 { if next := utf8.RuneLen(c); next < len(remainingText) { c = rune(remainingText[next]) if c == ' ' || c == '\t' || (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F') { p.css = append(p.css, ' ') } } else if mayNeedWhitespaceAfter { // If the last character is a hexadecimal escape, print a space afterwards // for the escape sequence to consume. That way we're sure it won't // accidentally consume a semantically significant space afterward. p.css = append(p.css, ' ') } } } } // Note: This function is hot in profiles func (p *printer) printQuotedWithQuote(text string, quote byte, flags printQuotedFlags) { if quote != quoteForURL { p.css = append(p.css, quote) } n := len(text) i := 0 runStart := 0 // Only compute the line length if necessary var startLineLength int wrapLongLines := false if p.options.LineLimit > 0 && quote != quoteForURL && (flags&printQuotedNoWrap) == 0 { startLineLength = p.currentLineLength() if startLineLength > p.options.LineLimit { startLineLength = p.options.LineLimit } wrapLongLines = true } for i < n { // Wrap long lines that are over the limit using escaped newlines if wrapLongLines && startLineLength+i >= p.options.LineLimit { if runStart < i { p.css = append(p.css, text[runStart:i]...) runStart = i } p.css = append(p.css, "\\\n"...) startLineLength -= p.options.LineLimit } c, width := utf8.DecodeRuneInString(text[i:]) escape := escapeNone switch c { case '\x00', '\r', '\n', '\f': // Use a hexadecimal escape for characters that would be invalid escapes escape = escapeHex case '\\', rune(quote): escape = escapeBackslash case '(', ')', ' ', '\t', '"', '\'': // These characters must be escaped in URL tokens if quote == quoteForURL { escape = escapeBackslash } case '/': // Avoid generating the sequence "= 1 && text[i-1] == '<' && i+6 <= len(text) && strings.EqualFold(text[i+1:i+6], "style") { escape = escapeBackslash } default: if (p.options.ASCIIOnly && c >= 0x80) || c == '\uFEFF' { escape = escapeHex } } if escape != escapeNone { if runStart < i { p.css = append(p.css, text[runStart:i]...) } p.printWithEscape(c, escape, text[i:], false) runStart = i + width } i += width } if runStart < n { p.css = append(p.css, text[runStart:]...) } if quote != quoteForURL { p.css = append(p.css, quote) } } func (p *printer) currentLineLength() int { css := p.css n := len(css) stop := p.oldLineEnd // Update "oldLineStart" to the start of the current line for i := n; i > stop; i-- { if c := css[i-1]; c == '\r' || c == '\n' { p.oldLineStart = i break } } p.oldLineEnd = n return n - p.oldLineStart } func (p *printer) printNewlinePastLineLimit(indent int32) bool { if p.currentLineLength() < p.options.LineLimit { return false } p.print("\n") if !p.options.MinifyWhitespace { p.printIndent(indent) } return true } type identMode uint8 const ( identNormal identMode = iota identHash identDimensionUnit identDimensionUnitAfterExponent ) type trailingWhitespace uint8 const ( mayNeedWhitespaceAfter trailingWhitespace = iota canDiscardWhitespaceAfter ) // Note: This function is hot in profiles func (p *printer) printIdent(text string, mode identMode, whitespace trailingWhitespace) { n := len(text) // Special escape behavior for the first character initialEscape := escapeNone switch mode { case identNormal: if !css_lexer.WouldStartIdentifierWithoutEscapes(text) { initialEscape = escapeBackslash } case identDimensionUnit, identDimensionUnitAfterExponent: if !css_lexer.WouldStartIdentifierWithoutEscapes(text) { initialEscape = escapeBackslash } else if n > 0 { if c := text[0]; c >= '0' && c <= '9' { // Unit: "2x" initialEscape = escapeHex } else if (c == 'e' || c == 'E') && mode != identDimensionUnitAfterExponent { if n >= 2 && text[1] >= '0' && text[1] <= '9' { // Unit: "e2x" initialEscape = escapeHex } else if n >= 3 && text[1] == '-' && text[2] >= '0' && text[2] <= '9' { // Unit: "e-2x" initialEscape = escapeHex } } } } // Fast path: the identifier does not need to be escaped. This fast path is // important for performance. For example, doing this sped up end-to-end // parsing and printing of a large CSS file from 84ms to 66ms (around 25% // faster). if initialEscape == escapeNone { for i := 0; i < n; i++ { if c := text[i]; c >= 0x80 || !css_lexer.IsNameContinue(rune(c)) { goto slowPath } } p.css = append(p.css, text...) return slowPath: } // Slow path: the identifier needs to be escaped for i, c := range text { escape := escapeNone if p.options.ASCIIOnly && c >= 0x80 { escape = escapeHex } else if c == '\r' || c == '\n' || c == '\f' || c == '\uFEFF' { // Use a hexadecimal escape for characters that would be invalid escapes escape = escapeHex } else { // Escape non-identifier characters if !css_lexer.IsNameContinue(c) { escape = escapeBackslash } // Special escape behavior for the first character if i == 0 && initialEscape != escapeNone { escape = initialEscape } } // If the last character is a hexadecimal escape, print a space afterwards // for the escape sequence to consume. That way we're sure it won't // accidentally consume a semantically significant space afterward. mayNeedWhitespaceAfter := whitespace == mayNeedWhitespaceAfter && escape != escapeNone && i+utf8.RuneLen(c) == n p.printWithEscape(c, escape, text[i:], mayNeedWhitespaceAfter) } } func (p *printer) printSymbol(loc logger.Loc, ref ast.Ref, mode identMode, whitespace trailingWhitespace) { ref = ast.FollowSymbols(p.symbols, ref) originalName := p.symbols.Get(ref).OriginalName name, ok := p.options.LocalNames[ref] if !ok { name = originalName } if p.options.AddSourceMappings { if originalName == name { originalName = "" } p.builder.AddSourceMapping(loc, originalName, p.css) } p.printIdent(name, mode, whitespace) } func (p *printer) printIndent(indent int32) { n := int(indent) if p.options.LineLimit > 0 && n*2 >= p.options.LineLimit { n = p.options.LineLimit / 2 } for i := 0; i < n; i++ { p.css = append(p.css, " "...) } } type printTokensOpts struct { indent int32 multiLineCommaPeriod uint8 isDeclaration bool } func functionMultiLineCommaPeriod(token css_ast.Token) uint8 { if token.Kind == css_lexer.TFunction { commaCount := 0 for _, t := range *token.Children { if t.Kind == css_lexer.TComma { commaCount++ } } switch strings.ToLower(token.Text) { case "linear-gradient", "radial-gradient", "conic-gradient", "repeating-linear-gradient", "repeating-radial-gradient", "repeating-conic-gradient": if commaCount >= 2 { return 1 } case "matrix": if commaCount == 5 { return 2 } case "matrix3d": if commaCount == 15 { return 4 } } } return 0 } func (p *printer) printTokens(tokens []css_ast.Token, opts printTokensOpts) bool { hasWhitespaceAfter := len(tokens) > 0 && (tokens[0].Whitespace&css_ast.WhitespaceBefore) != 0 // Pretty-print long comma-separated declarations of 3 or more items commaPeriod := int(opts.multiLineCommaPeriod) if !p.options.MinifyWhitespace && opts.isDeclaration { commaCount := 0 for _, t := range tokens { if t.Kind == css_lexer.TComma { commaCount++ if commaCount >= 2 { commaPeriod = 1 break } } if t.Kind == css_lexer.TFunction && functionMultiLineCommaPeriod(t) > 0 { commaPeriod = 1 break } } } commaCount := 0 for i, t := range tokens { if t.Kind == css_lexer.TComma { commaCount++ } if t.Kind == css_lexer.TWhitespace { hasWhitespaceAfter = true continue } if hasWhitespaceAfter { if commaPeriod > 0 && (i == 0 || (tokens[i-1].Kind == css_lexer.TComma && commaCount%commaPeriod == 0)) { p.print("\n") p.printIndent(opts.indent + 1) } else if p.options.LineLimit <= 0 || !p.printNewlinePastLineLimit(opts.indent+1) { p.print(" ") } } hasWhitespaceAfter = (t.Whitespace&css_ast.WhitespaceAfter) != 0 || (i+1 < len(tokens) && (tokens[i+1].Whitespace&css_ast.WhitespaceBefore) != 0) whitespace := mayNeedWhitespaceAfter if !hasWhitespaceAfter { whitespace = canDiscardWhitespaceAfter } if p.options.AddSourceMappings { p.builder.AddSourceMapping(t.Loc, "", p.css) } switch t.Kind { case css_lexer.TIdent: p.printIdent(t.Text, identNormal, whitespace) case css_lexer.TSymbol: ref := ast.Ref{SourceIndex: p.options.InputSourceIndex, InnerIndex: t.PayloadIndex} p.printSymbol(t.Loc, ref, identNormal, whitespace) case css_lexer.TFunction: p.printIdent(t.Text, identNormal, whitespace) p.print("(") case css_lexer.TDimension: value := t.DimensionValue() p.print(value) mode := identDimensionUnit if strings.ContainsAny(value, "eE") { mode = identDimensionUnitAfterExponent } p.printIdent(t.DimensionUnit(), mode, whitespace) case css_lexer.TAtKeyword: p.print("@") p.printIdent(t.Text, identNormal, whitespace) case css_lexer.THash: p.print("#") p.printIdent(t.Text, identHash, whitespace) case css_lexer.TString: p.printQuoted(t.Text, 0) case css_lexer.TURL: record := p.importRecords[t.PayloadIndex] text := record.Path.Text tryToAvoidQuote := true var flags printQuotedFlags if record.Flags.Has(ast.ContainsUniqueKey) { flags |= printQuotedNoWrap // If the caller will be substituting a path here later using string // substitution, then we can't be sure that it will form a valid URL // token when unquoted (e.g. it may contain spaces). So we need to // quote the unique key here just in case. For more info see this // issue: https://github.com/evanw/esbuild/issues/3410 tryToAvoidQuote = false } else if p.options.LineLimit > 0 && p.currentLineLength()+len(text) >= p.options.LineLimit { tryToAvoidQuote = false } p.print("url(") p.printQuotedWithQuote(text, bestQuoteCharForString(text, tryToAvoidQuote), flags) p.print(")") p.recordImportPathForMetafile(t.PayloadIndex) case css_lexer.TUnterminatedString: // We must end this with a newline so that this string stays unterminated p.print(t.Text) p.print("\n") if !p.options.MinifyWhitespace { p.printIndent(opts.indent) } hasWhitespaceAfter = false default: p.print(t.Text) } if t.Children != nil { childCommaPeriod := uint8(0) if commaPeriod > 0 && opts.isDeclaration { childCommaPeriod = functionMultiLineCommaPeriod(t) } if childCommaPeriod > 0 { opts.indent++ if !p.options.MinifyWhitespace { p.print("\n") p.printIndent(opts.indent + 1) } } p.printTokens(*t.Children, printTokensOpts{ indent: opts.indent, multiLineCommaPeriod: childCommaPeriod, }) if childCommaPeriod > 0 { opts.indent-- } switch t.Kind { case css_lexer.TFunction: p.print(")") case css_lexer.TOpenParen: p.print(")") case css_lexer.TOpenBrace: p.print("}") case css_lexer.TOpenBracket: p.print("]") } } } if hasWhitespaceAfter { p.print(" ") } return hasWhitespaceAfter }