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

     1  package css_parser
     2  
     3  import (
     4  	"strings"
     5  
     6  	"github.com/evanw/esbuild/internal/compat"
     7  	"github.com/evanw/esbuild/internal/css_ast"
     8  	"github.com/evanw/esbuild/internal/css_lexer"
     9  	"github.com/evanw/esbuild/internal/logger"
    10  )
    11  
    12  func (p *parser) commaToken(loc logger.Loc) css_ast.Token {
    13  	t := css_ast.Token{
    14  		Loc:  loc,
    15  		Kind: css_lexer.TComma,
    16  		Text: ",",
    17  	}
    18  	if !p.options.minifyWhitespace {
    19  		t.Whitespace = css_ast.WhitespaceAfter
    20  	}
    21  	return t
    22  }
    23  
    24  func expandTokenQuad(tokens []css_ast.Token, allowedIdent string) (result [4]css_ast.Token, ok bool) {
    25  	n := len(tokens)
    26  	if n < 1 || n > 4 {
    27  		return
    28  	}
    29  
    30  	// Don't do this if we encounter any unexpected tokens such as "var()"
    31  	for i := 0; i < n; i++ {
    32  		if t := tokens[i]; !t.Kind.IsNumeric() && (t.Kind != css_lexer.TIdent || allowedIdent == "" || t.Text != allowedIdent) {
    33  			return
    34  		}
    35  	}
    36  
    37  	result[0] = tokens[0]
    38  	if n > 1 {
    39  		result[1] = tokens[1]
    40  	} else {
    41  		result[1] = result[0]
    42  	}
    43  	if n > 2 {
    44  		result[2] = tokens[2]
    45  	} else {
    46  		result[2] = result[0]
    47  	}
    48  	if n > 3 {
    49  		result[3] = tokens[3]
    50  	} else {
    51  		result[3] = result[1]
    52  	}
    53  
    54  	ok = true
    55  	return
    56  }
    57  
    58  func compactTokenQuad(a css_ast.Token, b css_ast.Token, c css_ast.Token, d css_ast.Token, minifyWhitespace bool) []css_ast.Token {
    59  	tokens := []css_ast.Token{a, b, c, d}
    60  	if tokens[3].EqualIgnoringWhitespace(tokens[1]) {
    61  		if tokens[2].EqualIgnoringWhitespace(tokens[0]) {
    62  			if tokens[1].EqualIgnoringWhitespace(tokens[0]) {
    63  				tokens = tokens[:1]
    64  			} else {
    65  				tokens = tokens[:2]
    66  			}
    67  		} else {
    68  			tokens = tokens[:3]
    69  		}
    70  	}
    71  	for i := range tokens {
    72  		var whitespace css_ast.WhitespaceFlags
    73  		if !minifyWhitespace || i > 0 {
    74  			whitespace |= css_ast.WhitespaceBefore
    75  		}
    76  		if i+1 < len(tokens) {
    77  			whitespace |= css_ast.WhitespaceAfter
    78  		}
    79  		tokens[i].Whitespace = whitespace
    80  	}
    81  	return tokens
    82  }
    83  
    84  func (p *parser) processDeclarations(rules []css_ast.Rule, composesContext *composesContext) (rewrittenRules []css_ast.Rule) {
    85  	margin := boxTracker{key: css_ast.DMargin, keyText: "margin", allowAuto: true}
    86  	padding := boxTracker{key: css_ast.DPadding, keyText: "padding", allowAuto: false}
    87  	inset := boxTracker{key: css_ast.DInset, keyText: "inset", allowAuto: true}
    88  	borderRadius := borderRadiusTracker{}
    89  	rewrittenRules = make([]css_ast.Rule, 0, len(rules))
    90  	didWarnAboutComposes := false
    91  	wouldClipColorFlag := false
    92  	var declarationKeys map[string]struct{}
    93  
    94  	// Don't automatically generate the "inset" property if it's not supported
    95  	if p.options.unsupportedCSSFeatures.Has(compat.InsetProperty) {
    96  		inset.key = css_ast.DUnknown
    97  		inset.keyText = ""
    98  	}
    99  
   100  	// If this is a local class selector, track which CSS properties it declares.
   101  	// This is used to warn when CSS "composes" is used incorrectly.
   102  	if composesContext != nil {
   103  		for _, ref := range composesContext.parentRefs {
   104  			composes, ok := p.composes[ref]
   105  			if !ok {
   106  				composes = &css_ast.Composes{}
   107  				p.composes[ref] = composes
   108  			}
   109  			properties := composes.Properties
   110  			if properties == nil {
   111  				properties = make(map[string]logger.Loc)
   112  				composes.Properties = properties
   113  			}
   114  			for _, rule := range rules {
   115  				if decl, ok := rule.Data.(*css_ast.RDeclaration); ok && decl.Key != css_ast.DComposes {
   116  					properties[decl.KeyText] = decl.KeyRange.Loc
   117  				}
   118  			}
   119  		}
   120  	}
   121  
   122  	for i := 0; i < len(rules); i++ {
   123  		rule := rules[i]
   124  		rewrittenRules = append(rewrittenRules, rule)
   125  		decl, ok := rule.Data.(*css_ast.RDeclaration)
   126  		if !ok {
   127  			continue
   128  		}
   129  
   130  		// If the previous loop iteration would have clipped a color, we will
   131  		// duplicate it and insert the clipped copy before the unclipped copy
   132  		var wouldClipColor *bool
   133  		if wouldClipColorFlag {
   134  			wouldClipColorFlag = false
   135  			clone := *decl
   136  			clone.Value = css_ast.CloneTokensWithoutImportRecords(clone.Value)
   137  			decl = &clone
   138  			rule.Data = decl
   139  			n := len(rewrittenRules) - 2
   140  			rewrittenRules = append(rewrittenRules[:n], rule, rewrittenRules[n])
   141  		} else {
   142  			wouldClipColor = &wouldClipColorFlag
   143  		}
   144  
   145  		switch decl.Key {
   146  		case css_ast.DComposes:
   147  			// Only process "composes" directives if we're in "local-css" or
   148  			// "global-css" mode. In these cases, "composes" directives will always
   149  			// be removed (because they are being processed) even if they contain
   150  			// errors. Otherwise we leave "composes" directives there untouched and
   151  			// don't check them for errors.
   152  			if p.options.symbolMode != symbolModeDisabled {
   153  				if composesContext == nil {
   154  					if !didWarnAboutComposes {
   155  						didWarnAboutComposes = true
   156  						p.log.AddID(logger.MsgID_CSS_CSSSyntaxError, logger.Warning, &p.tracker, decl.KeyRange, "\"composes\" is not valid here")
   157  					}
   158  				} else if composesContext.problemRange.Len > 0 {
   159  					if !didWarnAboutComposes {
   160  						didWarnAboutComposes = true
   161  						p.log.AddIDWithNotes(logger.MsgID_CSS_CSSSyntaxError, logger.Warning, &p.tracker, decl.KeyRange, "\"composes\" only works inside single class selectors",
   162  							[]logger.MsgData{p.tracker.MsgData(composesContext.problemRange, "The parent selector is not a single class selector because of the syntax here:")})
   163  					}
   164  				} else {
   165  					p.handleComposesPragma(*composesContext, decl.Value)
   166  				}
   167  				rewrittenRules = rewrittenRules[:len(rewrittenRules)-1]
   168  			}
   169  
   170  		case css_ast.DBackground:
   171  			for i, t := range decl.Value {
   172  				t = p.lowerAndMinifyColor(t, wouldClipColor)
   173  				t = p.lowerAndMinifyGradient(t, wouldClipColor)
   174  				decl.Value[i] = t
   175  			}
   176  
   177  		case css_ast.DBackgroundImage,
   178  			css_ast.DBorderImage,
   179  			css_ast.DMaskImage:
   180  
   181  			for i, t := range decl.Value {
   182  				t = p.lowerAndMinifyGradient(t, wouldClipColor)
   183  				decl.Value[i] = t
   184  			}
   185  
   186  		case css_ast.DBackgroundColor,
   187  			css_ast.DBorderBlockEndColor,
   188  			css_ast.DBorderBlockStartColor,
   189  			css_ast.DBorderBottomColor,
   190  			css_ast.DBorderColor,
   191  			css_ast.DBorderInlineEndColor,
   192  			css_ast.DBorderInlineStartColor,
   193  			css_ast.DBorderLeftColor,
   194  			css_ast.DBorderRightColor,
   195  			css_ast.DBorderTopColor,
   196  			css_ast.DCaretColor,
   197  			css_ast.DColor,
   198  			css_ast.DColumnRuleColor,
   199  			css_ast.DFill,
   200  			css_ast.DFloodColor,
   201  			css_ast.DLightingColor,
   202  			css_ast.DOutlineColor,
   203  			css_ast.DStopColor,
   204  			css_ast.DStroke,
   205  			css_ast.DTextDecorationColor,
   206  			css_ast.DTextEmphasisColor:
   207  
   208  			if len(decl.Value) == 1 {
   209  				decl.Value[0] = p.lowerAndMinifyColor(decl.Value[0], wouldClipColor)
   210  			}
   211  
   212  		case css_ast.DTransform:
   213  			if p.options.minifySyntax {
   214  				decl.Value = p.mangleTransforms(decl.Value)
   215  			}
   216  
   217  		case css_ast.DBoxShadow:
   218  			decl.Value = p.lowerAndMangleBoxShadows(decl.Value, wouldClipColor)
   219  
   220  		// Container name
   221  		case css_ast.DContainer:
   222  			p.processContainerShorthand(decl.Value)
   223  		case css_ast.DContainerName:
   224  			p.processContainerName(decl.Value)
   225  
   226  			// Animation name
   227  		case css_ast.DAnimation:
   228  			p.processAnimationShorthand(decl.Value)
   229  		case css_ast.DAnimationName:
   230  			p.processAnimationName(decl.Value)
   231  
   232  		// List style
   233  		case css_ast.DListStyle:
   234  			p.processListStyleShorthand(decl.Value)
   235  		case css_ast.DListStyleType:
   236  			if len(decl.Value) == 1 {
   237  				p.processListStyleType(&decl.Value[0])
   238  			}
   239  
   240  			// Font
   241  		case css_ast.DFont:
   242  			if p.options.minifySyntax {
   243  				decl.Value = p.mangleFont(decl.Value)
   244  			}
   245  		case css_ast.DFontFamily:
   246  			if p.options.minifySyntax {
   247  				if value, ok := p.mangleFontFamily(decl.Value); ok {
   248  					decl.Value = value
   249  				}
   250  			}
   251  		case css_ast.DFontWeight:
   252  			if len(decl.Value) == 1 && p.options.minifySyntax {
   253  				decl.Value[0] = p.mangleFontWeight(decl.Value[0])
   254  			}
   255  
   256  			// Margin
   257  		case css_ast.DMargin:
   258  			if p.options.minifySyntax {
   259  				margin.mangleSides(rewrittenRules, decl, p.options.minifyWhitespace)
   260  			}
   261  		case css_ast.DMarginTop:
   262  			if p.options.minifySyntax {
   263  				margin.mangleSide(rewrittenRules, decl, p.options.minifyWhitespace, boxTop)
   264  			}
   265  		case css_ast.DMarginRight:
   266  			if p.options.minifySyntax {
   267  				margin.mangleSide(rewrittenRules, decl, p.options.minifyWhitespace, boxRight)
   268  			}
   269  		case css_ast.DMarginBottom:
   270  			if p.options.minifySyntax {
   271  				margin.mangleSide(rewrittenRules, decl, p.options.minifyWhitespace, boxBottom)
   272  			}
   273  		case css_ast.DMarginLeft:
   274  			if p.options.minifySyntax {
   275  				margin.mangleSide(rewrittenRules, decl, p.options.minifyWhitespace, boxLeft)
   276  			}
   277  
   278  		// Padding
   279  		case css_ast.DPadding:
   280  			if p.options.minifySyntax {
   281  				padding.mangleSides(rewrittenRules, decl, p.options.minifyWhitespace)
   282  			}
   283  		case css_ast.DPaddingTop:
   284  			if p.options.minifySyntax {
   285  				padding.mangleSide(rewrittenRules, decl, p.options.minifyWhitespace, boxTop)
   286  			}
   287  		case css_ast.DPaddingRight:
   288  			if p.options.minifySyntax {
   289  				padding.mangleSide(rewrittenRules, decl, p.options.minifyWhitespace, boxRight)
   290  			}
   291  		case css_ast.DPaddingBottom:
   292  			if p.options.minifySyntax {
   293  				padding.mangleSide(rewrittenRules, decl, p.options.minifyWhitespace, boxBottom)
   294  			}
   295  		case css_ast.DPaddingLeft:
   296  			if p.options.minifySyntax {
   297  				padding.mangleSide(rewrittenRules, decl, p.options.minifyWhitespace, boxLeft)
   298  			}
   299  
   300  		// Inset
   301  		case css_ast.DInset:
   302  			if p.options.unsupportedCSSFeatures.Has(compat.InsetProperty) {
   303  				if decls, ok := p.lowerInset(rule.Loc, decl); ok {
   304  					rewrittenRules = rewrittenRules[:len(rewrittenRules)-1]
   305  					for i := range decls {
   306  						rewrittenRules = append(rewrittenRules, decls[i])
   307  						if p.options.minifySyntax {
   308  							inset.mangleSide(rewrittenRules, decls[i].Data.(*css_ast.RDeclaration), p.options.minifyWhitespace, i)
   309  						}
   310  					}
   311  					break
   312  				}
   313  			}
   314  			if p.options.minifySyntax {
   315  				inset.mangleSides(rewrittenRules, decl, p.options.minifyWhitespace)
   316  			}
   317  		case css_ast.DTop:
   318  			if p.options.minifySyntax {
   319  				inset.mangleSide(rewrittenRules, decl, p.options.minifyWhitespace, boxTop)
   320  			}
   321  		case css_ast.DRight:
   322  			if p.options.minifySyntax {
   323  				inset.mangleSide(rewrittenRules, decl, p.options.minifyWhitespace, boxRight)
   324  			}
   325  		case css_ast.DBottom:
   326  			if p.options.minifySyntax {
   327  				inset.mangleSide(rewrittenRules, decl, p.options.minifyWhitespace, boxBottom)
   328  			}
   329  		case css_ast.DLeft:
   330  			if p.options.minifySyntax {
   331  				inset.mangleSide(rewrittenRules, decl, p.options.minifyWhitespace, boxLeft)
   332  			}
   333  
   334  		// Border radius
   335  		case css_ast.DBorderRadius:
   336  			if p.options.minifySyntax {
   337  				borderRadius.mangleCorners(rewrittenRules, decl, p.options.minifyWhitespace)
   338  			}
   339  		case css_ast.DBorderTopLeftRadius:
   340  			if p.options.minifySyntax {
   341  				borderRadius.mangleCorner(rewrittenRules, decl, p.options.minifyWhitespace, borderRadiusTopLeft)
   342  			}
   343  		case css_ast.DBorderTopRightRadius:
   344  			if p.options.minifySyntax {
   345  				borderRadius.mangleCorner(rewrittenRules, decl, p.options.minifyWhitespace, borderRadiusTopRight)
   346  			}
   347  		case css_ast.DBorderBottomRightRadius:
   348  			if p.options.minifySyntax {
   349  				borderRadius.mangleCorner(rewrittenRules, decl, p.options.minifyWhitespace, borderRadiusBottomRight)
   350  			}
   351  		case css_ast.DBorderBottomLeftRadius:
   352  			if p.options.minifySyntax {
   353  				borderRadius.mangleCorner(rewrittenRules, decl, p.options.minifyWhitespace, borderRadiusBottomLeft)
   354  			}
   355  		}
   356  
   357  		if prefixes, ok := p.options.cssPrefixData[decl.Key]; ok {
   358  			if declarationKeys == nil {
   359  				// Only generate this map if it's needed
   360  				declarationKeys = make(map[string]struct{})
   361  				for _, rule := range rules {
   362  					if decl, ok := rule.Data.(*css_ast.RDeclaration); ok {
   363  						declarationKeys[decl.KeyText] = struct{}{}
   364  					}
   365  				}
   366  			}
   367  			if (prefixes & compat.WebkitPrefix) != 0 {
   368  				rewrittenRules = p.insertPrefixedDeclaration(rewrittenRules, "-webkit-", rule.Loc, decl, declarationKeys)
   369  			}
   370  			if (prefixes & compat.KhtmlPrefix) != 0 {
   371  				rewrittenRules = p.insertPrefixedDeclaration(rewrittenRules, "-khtml-", rule.Loc, decl, declarationKeys)
   372  			}
   373  			if (prefixes & compat.MozPrefix) != 0 {
   374  				rewrittenRules = p.insertPrefixedDeclaration(rewrittenRules, "-moz-", rule.Loc, decl, declarationKeys)
   375  			}
   376  			if (prefixes & compat.MsPrefix) != 0 {
   377  				rewrittenRules = p.insertPrefixedDeclaration(rewrittenRules, "-ms-", rule.Loc, decl, declarationKeys)
   378  			}
   379  			if (prefixes & compat.OPrefix) != 0 {
   380  				rewrittenRules = p.insertPrefixedDeclaration(rewrittenRules, "-o-", rule.Loc, decl, declarationKeys)
   381  			}
   382  		}
   383  
   384  		// If this loop iteration would have clipped a color, the out-of-gamut
   385  		// colors will not be clipped and this flag will be set. We then set up the
   386  		// next iteration of the loop to duplicate this rule and process it again
   387  		// with color clipping enabled.
   388  		if wouldClipColorFlag {
   389  			if p.options.unsupportedCSSFeatures.Has(compat.ColorFunctions) {
   390  				// Only do this if there was no previous instance of that property so
   391  				// we avoid overwriting any manually-specified fallback values
   392  				for j := len(rewrittenRules) - 2; j >= 0; j-- {
   393  					if prev, ok := rewrittenRules[j].Data.(*css_ast.RDeclaration); ok && prev.Key == decl.Key {
   394  						wouldClipColorFlag = false
   395  						break
   396  					}
   397  				}
   398  				if wouldClipColorFlag {
   399  					// If the code above would have clipped a color outside of the sRGB gamut,
   400  					// process this rule again so we can generate the clipped version next time
   401  					i -= 1
   402  					continue
   403  				}
   404  			}
   405  			wouldClipColorFlag = false
   406  		}
   407  	}
   408  
   409  	// Compact removed rules
   410  	if p.options.minifySyntax {
   411  		end := 0
   412  		for _, rule := range rewrittenRules {
   413  			if rule.Data != nil {
   414  				rewrittenRules[end] = rule
   415  				end++
   416  			}
   417  		}
   418  		rewrittenRules = rewrittenRules[:end]
   419  	}
   420  
   421  	return
   422  }
   423  
   424  func (p *parser) insertPrefixedDeclaration(rules []css_ast.Rule, prefix string, loc logger.Loc, decl *css_ast.RDeclaration, declarationKeys map[string]struct{}) []css_ast.Rule {
   425  	keyText := prefix + decl.KeyText
   426  
   427  	// Don't insert a prefixed declaration if there already is one
   428  	if _, ok := declarationKeys[keyText]; ok {
   429  		// We found a previous declaration with a matching prefixed property.
   430  		// The value is ignored, which matches the behavior of "autoprefixer".
   431  		return rules
   432  	}
   433  
   434  	// Additional special cases for when the prefix applies
   435  	switch decl.Key {
   436  	case css_ast.DBackgroundClip:
   437  		// The prefix is only needed for "background-clip: text"
   438  		if len(decl.Value) != 1 || decl.Value[0].Kind != css_lexer.TIdent || !strings.EqualFold(decl.Value[0].Text, "text") {
   439  			return rules
   440  		}
   441  
   442  	case css_ast.DPosition:
   443  		// The prefix is only needed for "position: sticky"
   444  		if len(decl.Value) != 1 || decl.Value[0].Kind != css_lexer.TIdent || !strings.EqualFold(decl.Value[0].Text, "sticky") {
   445  			return rules
   446  		}
   447  	}
   448  
   449  	value := css_ast.CloneTokensWithoutImportRecords(decl.Value)
   450  
   451  	// Additional special cases for how to transform the contents
   452  	switch decl.Key {
   453  	case css_ast.DPosition:
   454  		// The prefix applies to the value, not the property
   455  		keyText = decl.KeyText
   456  		value[0].Text = "-webkit-sticky"
   457  
   458  	case css_ast.DUserSelect:
   459  		// The prefix applies to the value as well as the property
   460  		if prefix == "-moz-" && len(value) == 1 && value[0].Kind == css_lexer.TIdent && strings.EqualFold(value[0].Text, "none") {
   461  			value[0].Text = "-moz-none"
   462  		}
   463  
   464  	case css_ast.DMaskComposite:
   465  		// WebKit uses different names for these values
   466  		if prefix == "-webkit-" {
   467  			for i, token := range value {
   468  				if token.Kind == css_lexer.TIdent {
   469  					switch token.Text {
   470  					case "add":
   471  						value[i].Text = "source-over"
   472  					case "subtract":
   473  						value[i].Text = "source-out"
   474  					case "intersect":
   475  						value[i].Text = "source-in"
   476  					case "exclude":
   477  						value[i].Text = "xor"
   478  					}
   479  				}
   480  			}
   481  		}
   482  	}
   483  
   484  	// Overwrite the latest declaration with the prefixed declaration
   485  	rules[len(rules)-1] = css_ast.Rule{Loc: loc, Data: &css_ast.RDeclaration{
   486  		KeyText:   keyText,
   487  		KeyRange:  decl.KeyRange,
   488  		Value:     value,
   489  		Important: decl.Important,
   490  	}}
   491  
   492  	// Re-add the latest declaration after the inserted declaration
   493  	rules = append(rules, css_ast.Rule{Loc: loc, Data: decl})
   494  	return rules
   495  }
   496  
   497  func (p *parser) lowerInset(loc logger.Loc, decl *css_ast.RDeclaration) ([]css_ast.Rule, bool) {
   498  	if tokens, ok := expandTokenQuad(decl.Value, ""); ok {
   499  		mask := ^css_ast.WhitespaceAfter
   500  		if p.options.minifyWhitespace {
   501  			mask = 0
   502  		}
   503  		for i := range tokens {
   504  			tokens[i].Whitespace &= mask
   505  		}
   506  		return []css_ast.Rule{
   507  			{Loc: loc, Data: &css_ast.RDeclaration{
   508  				KeyText:   "top",
   509  				KeyRange:  decl.KeyRange,
   510  				Key:       css_ast.DTop,
   511  				Value:     tokens[0:1],
   512  				Important: decl.Important,
   513  			}},
   514  			{Loc: loc, Data: &css_ast.RDeclaration{
   515  				KeyText:   "right",
   516  				KeyRange:  decl.KeyRange,
   517  				Key:       css_ast.DRight,
   518  				Value:     tokens[1:2],
   519  				Important: decl.Important,
   520  			}},
   521  			{Loc: loc, Data: &css_ast.RDeclaration{
   522  				KeyText:   "bottom",
   523  				KeyRange:  decl.KeyRange,
   524  				Key:       css_ast.DBottom,
   525  				Value:     tokens[2:3],
   526  				Important: decl.Important,
   527  			}},
   528  			{Loc: loc, Data: &css_ast.RDeclaration{
   529  				KeyText:   "left",
   530  				KeyRange:  decl.KeyRange,
   531  				Key:       css_ast.DLeft,
   532  				Value:     tokens[3:4],
   533  				Important: decl.Important,
   534  			}},
   535  		}, true
   536  	}
   537  	return nil, false
   538  }