Files

568 lines
17 KiB
Go

package css_parser
import (
"strings"
"github.com/evanw/esbuild/internal/compat"
"github.com/evanw/esbuild/internal/css_ast"
"github.com/evanw/esbuild/internal/css_lexer"
"github.com/evanw/esbuild/internal/logger"
)
func (p *parser) commaToken(loc logger.Loc) css_ast.Token {
t := css_ast.Token{
Loc: loc,
Kind: css_lexer.TComma,
Text: ",",
}
if !p.options.minifyWhitespace {
t.Whitespace = css_ast.WhitespaceAfter
}
return t
}
func expandTokenQuad(tokens []css_ast.Token, allowedIdent string) (result [4]css_ast.Token, ok bool) {
n := len(tokens)
if n < 1 || n > 4 {
return
}
// Don't do this if we encounter any unexpected tokens such as "var()"
for i := 0; i < n; i++ {
if t := tokens[i]; !t.Kind.IsNumeric() && (t.Kind != css_lexer.TIdent || allowedIdent == "" || t.Text != allowedIdent) {
return
}
}
result[0] = tokens[0]
if n > 1 {
result[1] = tokens[1]
} else {
result[1] = result[0]
}
if n > 2 {
result[2] = tokens[2]
} else {
result[2] = result[0]
}
if n > 3 {
result[3] = tokens[3]
} else {
result[3] = result[1]
}
ok = true
return
}
func compactTokenQuad(a css_ast.Token, b css_ast.Token, c css_ast.Token, d css_ast.Token, minifyWhitespace bool) []css_ast.Token {
tokens := []css_ast.Token{a, b, c, d}
if tokens[3].EqualIgnoringWhitespace(tokens[1]) {
if tokens[2].EqualIgnoringWhitespace(tokens[0]) {
if tokens[1].EqualIgnoringWhitespace(tokens[0]) {
tokens = tokens[:1]
} else {
tokens = tokens[:2]
}
} else {
tokens = tokens[:3]
}
}
for i := range tokens {
var whitespace css_ast.WhitespaceFlags
if !minifyWhitespace || i > 0 {
whitespace |= css_ast.WhitespaceBefore
}
if i+1 < len(tokens) {
whitespace |= css_ast.WhitespaceAfter
}
tokens[i].Whitespace = whitespace
}
return tokens
}
func (p *parser) processDeclarations(rules []css_ast.Rule, composesContext *composesContext) (rewrittenRules []css_ast.Rule) {
margin := boxTracker{key: css_ast.DMargin, keyText: "margin", allowAuto: true}
padding := boxTracker{key: css_ast.DPadding, keyText: "padding", allowAuto: false}
inset := boxTracker{key: css_ast.DInset, keyText: "inset", allowAuto: true}
borderRadius := borderRadiusTracker{}
rewrittenRules = make([]css_ast.Rule, 0, len(rules))
didWarnAboutComposes := false
wouldClipColorFlag := false
var declarationKeys map[string]struct{}
// Don't automatically generate the "inset" property if it's not supported
if p.options.unsupportedCSSFeatures.Has(compat.InsetProperty) {
inset.key = css_ast.DUnknown
inset.keyText = ""
}
// If this is a local class selector, track which CSS properties it declares.
// This is used to warn when CSS "composes" is used incorrectly.
if composesContext != nil {
for _, ref := range composesContext.parentRefs {
composes, ok := p.composes[ref]
if !ok {
composes = &css_ast.Composes{}
p.composes[ref] = composes
}
properties := composes.Properties
if properties == nil {
properties = make(map[string]logger.Loc)
composes.Properties = properties
}
for _, rule := range rules {
if decl, ok := rule.Data.(*css_ast.RDeclaration); ok && decl.Key != css_ast.DComposes {
properties[decl.KeyText] = decl.KeyRange.Loc
}
}
}
}
for i := 0; i < len(rules); i++ {
rule := rules[i]
rewrittenRules = append(rewrittenRules, rule)
decl, ok := rule.Data.(*css_ast.RDeclaration)
if !ok {
continue
}
// If the previous loop iteration would have clipped a color, we will
// duplicate it and insert the clipped copy before the unclipped copy
var wouldClipColor *bool
if wouldClipColorFlag {
wouldClipColorFlag = false
clone := *decl
clone.Value = css_ast.CloneTokensWithoutImportRecords(clone.Value)
decl = &clone
rule.Data = decl
n := len(rewrittenRules) - 2
rewrittenRules = append(rewrittenRules[:n], rule, rewrittenRules[n])
} else {
wouldClipColor = &wouldClipColorFlag
}
switch decl.Key {
case css_ast.DComposes:
// Only process "composes" directives if we're in "local-css" or
// "global-css" mode. In these cases, "composes" directives will always
// be removed (because they are being processed) even if they contain
// errors. Otherwise we leave "composes" directives there untouched and
// don't check them for errors.
if p.options.symbolMode != symbolModeDisabled {
if composesContext == nil {
if !didWarnAboutComposes {
didWarnAboutComposes = true
p.log.AddID(logger.MsgID_CSS_CSSSyntaxError, logger.Warning, &p.tracker, decl.KeyRange, "\"composes\" is not valid here")
}
} else if composesContext.problemRange.Len > 0 {
if !didWarnAboutComposes {
didWarnAboutComposes = true
p.log.AddIDWithNotes(logger.MsgID_CSS_CSSSyntaxError, logger.Warning, &p.tracker, decl.KeyRange, "\"composes\" only works inside single class selectors",
[]logger.MsgData{p.tracker.MsgData(composesContext.problemRange, "The parent selector is not a single class selector because of the syntax here:")})
}
} else {
p.handleComposesPragma(*composesContext, decl.Value)
}
rewrittenRules = rewrittenRules[:len(rewrittenRules)-1]
}
case css_ast.DBackground:
for i, t := range decl.Value {
t = p.lowerAndMinifyColor(t, wouldClipColor)
t = p.lowerAndMinifyGradient(t, wouldClipColor)
decl.Value[i] = t
}
case css_ast.DBackgroundImage,
css_ast.DBorderImage,
css_ast.DMaskImage:
for i, t := range decl.Value {
t = p.lowerAndMinifyGradient(t, wouldClipColor)
decl.Value[i] = t
}
case css_ast.DBackgroundColor,
css_ast.DBorderBlockEndColor,
css_ast.DBorderBlockStartColor,
css_ast.DBorderBottomColor,
css_ast.DBorderColor,
css_ast.DBorderInlineEndColor,
css_ast.DBorderInlineStartColor,
css_ast.DBorderLeftColor,
css_ast.DBorderRightColor,
css_ast.DBorderTopColor,
css_ast.DCaretColor,
css_ast.DColor,
css_ast.DColumnRuleColor,
css_ast.DFill,
css_ast.DFloodColor,
css_ast.DLightingColor,
css_ast.DOutlineColor,
css_ast.DStopColor,
css_ast.DStroke,
css_ast.DTextDecorationColor,
css_ast.DTextEmphasisColor:
if len(decl.Value) == 1 {
decl.Value[0] = p.lowerAndMinifyColor(decl.Value[0], wouldClipColor)
}
case css_ast.DTransform:
if p.options.minifySyntax {
decl.Value = p.mangleTransforms(decl.Value)
}
case css_ast.DBoxShadow:
decl.Value = p.lowerAndMangleBoxShadows(decl.Value, wouldClipColor)
// Container name
case css_ast.DContainer:
p.processContainerShorthand(decl.Value)
case css_ast.DContainerName:
p.processContainerName(decl.Value)
// Animation name
case css_ast.DAnimation:
p.processAnimationShorthand(decl.Value)
case css_ast.DAnimationName:
p.processAnimationName(decl.Value)
// List style
case css_ast.DListStyle:
p.processListStyleShorthand(decl.Value)
case css_ast.DListStyleType:
if len(decl.Value) == 1 {
p.processListStyleType(&decl.Value[0])
}
// Font
case css_ast.DFont:
if p.options.minifySyntax {
decl.Value = p.mangleFont(decl.Value)
}
case css_ast.DFontFamily:
if p.options.minifySyntax {
if value, ok := p.mangleFontFamily(decl.Value); ok {
decl.Value = value
}
}
case css_ast.DFontWeight:
if len(decl.Value) == 1 && p.options.minifySyntax {
decl.Value[0] = p.mangleFontWeight(decl.Value[0])
}
// Margin
case css_ast.DMargin:
if p.options.minifySyntax {
margin.mangleSides(rewrittenRules, decl, p.options.minifyWhitespace)
}
case css_ast.DMarginTop:
if p.options.minifySyntax {
margin.mangleSide(rewrittenRules, decl, p.options.minifyWhitespace, boxTop)
}
case css_ast.DMarginRight:
if p.options.minifySyntax {
margin.mangleSide(rewrittenRules, decl, p.options.minifyWhitespace, boxRight)
}
case css_ast.DMarginBottom:
if p.options.minifySyntax {
margin.mangleSide(rewrittenRules, decl, p.options.minifyWhitespace, boxBottom)
}
case css_ast.DMarginLeft:
if p.options.minifySyntax {
margin.mangleSide(rewrittenRules, decl, p.options.minifyWhitespace, boxLeft)
}
// Padding
case css_ast.DPadding:
if p.options.minifySyntax {
padding.mangleSides(rewrittenRules, decl, p.options.minifyWhitespace)
}
case css_ast.DPaddingTop:
if p.options.minifySyntax {
padding.mangleSide(rewrittenRules, decl, p.options.minifyWhitespace, boxTop)
}
case css_ast.DPaddingRight:
if p.options.minifySyntax {
padding.mangleSide(rewrittenRules, decl, p.options.minifyWhitespace, boxRight)
}
case css_ast.DPaddingBottom:
if p.options.minifySyntax {
padding.mangleSide(rewrittenRules, decl, p.options.minifyWhitespace, boxBottom)
}
case css_ast.DPaddingLeft:
if p.options.minifySyntax {
padding.mangleSide(rewrittenRules, decl, p.options.minifyWhitespace, boxLeft)
}
// Inset
case css_ast.DInset:
if p.options.unsupportedCSSFeatures.Has(compat.InsetProperty) {
if decls, ok := p.lowerInset(rule.Loc, decl); ok {
rewrittenRules = rewrittenRules[:len(rewrittenRules)-1]
for i := range decls {
rewrittenRules = append(rewrittenRules, decls[i])
if p.options.minifySyntax {
inset.mangleSide(rewrittenRules, decls[i].Data.(*css_ast.RDeclaration), p.options.minifyWhitespace, i)
}
}
break
}
}
if p.options.minifySyntax {
inset.mangleSides(rewrittenRules, decl, p.options.minifyWhitespace)
}
case css_ast.DTop:
if p.options.minifySyntax {
inset.mangleSide(rewrittenRules, decl, p.options.minifyWhitespace, boxTop)
}
case css_ast.DRight:
if p.options.minifySyntax {
inset.mangleSide(rewrittenRules, decl, p.options.minifyWhitespace, boxRight)
}
case css_ast.DBottom:
if p.options.minifySyntax {
inset.mangleSide(rewrittenRules, decl, p.options.minifyWhitespace, boxBottom)
}
case css_ast.DLeft:
if p.options.minifySyntax {
inset.mangleSide(rewrittenRules, decl, p.options.minifyWhitespace, boxLeft)
}
// Border radius
case css_ast.DBorderRadius:
if p.options.minifySyntax {
borderRadius.mangleCorners(rewrittenRules, decl, p.options.minifyWhitespace)
}
case css_ast.DBorderTopLeftRadius:
if p.options.minifySyntax {
borderRadius.mangleCorner(rewrittenRules, decl, p.options.minifyWhitespace, borderRadiusTopLeft)
}
case css_ast.DBorderTopRightRadius:
if p.options.minifySyntax {
borderRadius.mangleCorner(rewrittenRules, decl, p.options.minifyWhitespace, borderRadiusTopRight)
}
case css_ast.DBorderBottomRightRadius:
if p.options.minifySyntax {
borderRadius.mangleCorner(rewrittenRules, decl, p.options.minifyWhitespace, borderRadiusBottomRight)
}
case css_ast.DBorderBottomLeftRadius:
if p.options.minifySyntax {
borderRadius.mangleCorner(rewrittenRules, decl, p.options.minifyWhitespace, borderRadiusBottomLeft)
}
}
if prefixes, ok := p.options.cssPrefixData[decl.Key]; ok {
if declarationKeys == nil {
// Only generate this map if it's needed
declarationKeys = make(map[string]struct{})
for _, rule := range rules {
if decl, ok := rule.Data.(*css_ast.RDeclaration); ok {
declarationKeys[decl.KeyText] = struct{}{}
}
}
}
if (prefixes & compat.WebkitPrefix) != 0 {
rewrittenRules = p.insertPrefixedDeclaration(rewrittenRules, "-webkit-", rule.Loc, decl, declarationKeys)
}
if (prefixes & compat.KhtmlPrefix) != 0 {
rewrittenRules = p.insertPrefixedDeclaration(rewrittenRules, "-khtml-", rule.Loc, decl, declarationKeys)
}
if (prefixes & compat.MozPrefix) != 0 {
rewrittenRules = p.insertPrefixedDeclaration(rewrittenRules, "-moz-", rule.Loc, decl, declarationKeys)
}
if (prefixes & compat.MsPrefix) != 0 {
rewrittenRules = p.insertPrefixedDeclaration(rewrittenRules, "-ms-", rule.Loc, decl, declarationKeys)
}
if (prefixes & compat.OPrefix) != 0 {
rewrittenRules = p.insertPrefixedDeclaration(rewrittenRules, "-o-", rule.Loc, decl, declarationKeys)
}
}
// If this loop iteration would have clipped a color, the out-of-gamut
// colors will not be clipped and this flag will be set. We then set up the
// next iteration of the loop to duplicate this rule and process it again
// with color clipping enabled.
if wouldClipColorFlag {
if p.options.unsupportedCSSFeatures.Has(compat.ColorFunctions) {
// Only do this if there was no previous instance of that property so
// we avoid overwriting any manually-specified fallback values
for j := len(rewrittenRules) - 2; j >= 0; j-- {
if prev, ok := rewrittenRules[j].Data.(*css_ast.RDeclaration); ok && prev.Key == decl.Key {
wouldClipColorFlag = false
break
}
}
if wouldClipColorFlag {
// If the code above would have clipped a color outside of the sRGB gamut,
// process this rule again so we can generate the clipped version next time
i -= 1
continue
}
}
wouldClipColorFlag = false
}
}
// Compact removed rules
if p.options.minifySyntax {
end := 0
for _, rule := range rewrittenRules {
if rule.Data != nil {
rewrittenRules[end] = rule
end++
}
}
rewrittenRules = rewrittenRules[:end]
}
return
}
func (p *parser) insertPrefixedDeclaration(rules []css_ast.Rule, prefix string, loc logger.Loc, decl *css_ast.RDeclaration, declarationKeys map[string]struct{}) []css_ast.Rule {
keyText := prefix + decl.KeyText
// Don't insert a prefixed declaration if there already is one
if _, ok := declarationKeys[keyText]; ok {
// We found a previous declaration with a matching prefixed property.
// The value is ignored, which matches the behavior of "autoprefixer".
return rules
}
// Additional special cases for when the prefix applies
switch decl.Key {
case css_ast.DBackgroundClip:
// The prefix is only needed for "background-clip: text"
if len(decl.Value) != 1 || decl.Value[0].Kind != css_lexer.TIdent || !strings.EqualFold(decl.Value[0].Text, "text") {
return rules
}
case css_ast.DPosition:
// The prefix is only needed for "position: sticky"
if len(decl.Value) != 1 || decl.Value[0].Kind != css_lexer.TIdent || !strings.EqualFold(decl.Value[0].Text, "sticky") {
return rules
}
case css_ast.DWidth, css_ast.DMinWidth, css_ast.DMaxWidth,
css_ast.DHeight, css_ast.DMinHeight, css_ast.DMaxHeight:
// The prefix is only needed for "width: stretch"
if len(decl.Value) != 1 || decl.Value[0].Kind != css_lexer.TIdent || !strings.EqualFold(decl.Value[0].Text, "stretch") {
return rules
}
}
value := css_ast.CloneTokensWithoutImportRecords(decl.Value)
// Additional special cases for how to transform the contents
switch decl.Key {
case css_ast.DPosition:
// The prefix applies to the value, not the property
keyText = decl.KeyText
value[0].Text = "-webkit-sticky"
case css_ast.DWidth, css_ast.DMinWidth, css_ast.DMaxWidth,
css_ast.DHeight, css_ast.DMinHeight, css_ast.DMaxHeight:
// The prefix applies to the value, not the property
keyText = decl.KeyText
// This currently only applies to "stretch" (already checked above)
switch prefix {
case "-webkit-":
value[0].Text = "-webkit-fill-available"
case "-moz-":
value[0].Text = "-moz-available"
}
case css_ast.DUserSelect:
// The prefix applies to the value as well as the property
if prefix == "-moz-" && len(value) == 1 && value[0].Kind == css_lexer.TIdent && strings.EqualFold(value[0].Text, "none") {
value[0].Text = "-moz-none"
}
case css_ast.DMaskComposite:
// WebKit uses different names for these values
if prefix == "-webkit-" {
for i, token := range value {
if token.Kind == css_lexer.TIdent {
switch token.Text {
case "add":
value[i].Text = "source-over"
case "subtract":
value[i].Text = "source-out"
case "intersect":
value[i].Text = "source-in"
case "exclude":
value[i].Text = "xor"
}
}
}
}
}
// If we didn't change the key, manually search for a previous duplicate rule
if keyText == decl.KeyText {
for _, rule := range rules {
if prevDecl, ok := rule.Data.(*css_ast.RDeclaration); ok && prevDecl.KeyText == keyText && css_ast.TokensEqual(prevDecl.Value, value, nil) {
return rules
}
}
}
// Overwrite the latest declaration with the prefixed declaration
rules[len(rules)-1] = css_ast.Rule{Loc: loc, Data: &css_ast.RDeclaration{
KeyText: keyText,
KeyRange: decl.KeyRange,
Value: value,
Important: decl.Important,
}}
// Re-add the latest declaration after the inserted declaration
rules = append(rules, css_ast.Rule{Loc: loc, Data: decl})
return rules
}
func (p *parser) lowerInset(loc logger.Loc, decl *css_ast.RDeclaration) ([]css_ast.Rule, bool) {
if tokens, ok := expandTokenQuad(decl.Value, ""); ok {
mask := ^css_ast.WhitespaceAfter
if p.options.minifyWhitespace {
mask = 0
}
for i := range tokens {
tokens[i].Whitespace &= mask
}
return []css_ast.Rule{
{Loc: loc, Data: &css_ast.RDeclaration{
KeyText: "top",
KeyRange: decl.KeyRange,
Key: css_ast.DTop,
Value: tokens[0:1],
Important: decl.Important,
}},
{Loc: loc, Data: &css_ast.RDeclaration{
KeyText: "right",
KeyRange: decl.KeyRange,
Key: css_ast.DRight,
Value: tokens[1:2],
Important: decl.Important,
}},
{Loc: loc, Data: &css_ast.RDeclaration{
KeyText: "bottom",
KeyRange: decl.KeyRange,
Key: css_ast.DBottom,
Value: tokens[2:3],
Important: decl.Important,
}},
{Loc: loc, Data: &css_ast.RDeclaration{
KeyText: "left",
KeyRange: decl.KeyRange,
Key: css_ast.DLeft,
Value: tokens[3:4],
Important: decl.Important,
}},
}, true
}
return nil, false
}