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 }