Skip to content

Commit

Permalink
css gradients: lower colors, fix double positions
Browse files Browse the repository at this point in the history
  • Loading branch information
evanw committed Dec 9, 2023
1 parent a389c52 commit e4c55af
Show file tree
Hide file tree
Showing 8 changed files with 338 additions and 11 deletions.
1 change: 1 addition & 0 deletions compat-table/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ export const jsFeatures = {
export type CSSFeature = keyof typeof cssFeatures
export const cssFeatures = {
ColorFunctions: true,
GradientDoublePosition: true,
HexRGBA: true,
HWB: true,
InlineStyle: true,
Expand Down
7 changes: 7 additions & 0 deletions compat-table/src/mdn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,13 @@ const cssFeatures: Partial<Record<CSSFeature, string | string[]>> = {
'css.types.color.oklab',
'css.types.color.oklch',
],
GradientDoublePosition: [
'css.types.image.gradient.linear-gradient.doubleposition',
'css.types.image.gradient.radial-gradient.doubleposition',
'css.types.image.gradient.conic-gradient.doubleposition',
'css.types.image.gradient.repeating-linear-gradient.doubleposition',
'css.types.image.gradient.repeating-radial-gradient.doubleposition',
],
HexRGBA: 'css.types.color.rgb_hexadecimal_notation.alpha_hexadecimal_notation',
HWB: 'css.types.color.hwb',
InsetProperty: 'css.properties.inset',
Expand Down
28 changes: 19 additions & 9 deletions internal/compat/css_table.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ type CSSFeature uint16

const (
ColorFunctions CSSFeature = 1 << iota
GradientDoublePosition
HWB
HexRGBA
InlineStyle
Expand All @@ -21,15 +22,16 @@ const (
)

var StringToCSSFeature = map[string]CSSFeature{
"color-functions": ColorFunctions,
"hwb": HWB,
"hex-rgba": HexRGBA,
"inline-style": InlineStyle,
"inset-property": InsetProperty,
"is-pseudo-class": IsPseudoClass,
"modern-rgb-hsl": Modern_RGB_HSL,
"nesting": Nesting,
"rebecca-purple": RebeccaPurple,
"color-functions": ColorFunctions,
"gradient-double-position": GradientDoublePosition,
"hwb": HWB,
"hex-rgba": HexRGBA,
"inline-style": InlineStyle,
"inset-property": InsetProperty,
"is-pseudo-class": IsPseudoClass,
"modern-rgb-hsl": Modern_RGB_HSL,
"nesting": Nesting,
"rebecca-purple": RebeccaPurple,
}

func (features CSSFeature) Has(feature CSSFeature) bool {
Expand All @@ -49,6 +51,14 @@ var cssTable = map[CSSFeature]map[Engine][]versionRange{
Opera: {{start: v{97, 0, 0}}},
Safari: {{start: v{15, 4, 0}}},
},
GradientDoublePosition: {
Chrome: {{start: v{72, 0, 0}}},
Edge: {{start: v{79, 0, 0}}},
Firefox: {{start: v{83, 0, 0}}},
IOS: {{start: v{12, 2, 0}}},
Opera: {{start: v{60, 0, 0}}},
Safari: {{start: v{12, 1, 0}}},
},
HWB: {
Chrome: {{start: v{101, 0, 0}}},
Edge: {{start: v{101, 0, 0}}},
Expand Down
13 changes: 12 additions & 1 deletion internal/css_parser/css_decls.go
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,18 @@ func (p *parser) processDeclarations(rules []css_ast.Rule, composesContext *comp

case css_ast.DBackground:
for i, t := range decl.Value {
decl.Value[i] = p.lowerAndMinifyColor(t, wouldClipColor)
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,
Expand Down
2 changes: 1 addition & 1 deletion internal/css_parser/css_decls_box_shadow.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ func (p *parser) lowerAndMangleBoxShadow(tokens []css_ast.Token, wouldClipColor
numbersDone = true
}

if _, ok := parseColor(t); ok {
if looksLikeColor(t) {
colorCount++
tokens[i] = p.lowerAndMinifyColor(t, wouldClipColor)
} else if t.Kind == css_lexer.TIdent && strings.EqualFold(t.Text, "inset") {
Expand Down
36 changes: 36 additions & 0 deletions internal/css_parser/css_decls_color.go
Original file line number Diff line number Diff line change
Expand Up @@ -426,6 +426,42 @@ type parsedColor struct {
sRGB bool
}

func looksLikeColor(token css_ast.Token) bool {
switch token.Kind {
case css_lexer.TIdent:
if _, ok := colorNameToHex[strings.ToLower(token.Text)]; ok {
return true
}

case css_lexer.THash:
switch len(token.Text) {
case 3, 4, 6, 8:
if _, ok := parseHex(token.Text); ok {
return true
}
}

case css_lexer.TFunction:
switch strings.ToLower(token.Text) {
case
"color-mix",
"color",
"hsl",
"hsla",
"hwb",
"lab",
"lch",
"oklab",
"oklch",
"rgb",
"rgba":
return true
}
}

return false
}

func parseColor(token css_ast.Token) (parsedColor, bool) {
text := token.Text

Expand Down
207 changes: 207 additions & 0 deletions internal/css_parser/css_decls_gradient.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
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"
)

type gradientKind uint8

const (
linearGradient gradientKind = iota
radialGradient
conicGradient
)

type parsedGradient struct {
initialTokens []css_ast.Token
colorStops []colorStop
kind gradientKind
repeating bool
}

type colorStop struct {
positions []css_ast.Token
color css_ast.Token
}

func parseGradient(token css_ast.Token) (gradient parsedGradient, success bool) {
if token.Kind != css_lexer.TFunction {
return
}

switch strings.ToLower(token.Text) {
case "linear-gradient":
gradient.kind = linearGradient

case "radial-gradient":
gradient.kind = radialGradient

case "conic-gradient":
gradient.kind = conicGradient

case "repeating-linear-gradient":
gradient.kind = linearGradient
gradient.repeating = true

case "repeating-radial-gradient":
gradient.kind = radialGradient
gradient.repeating = true

case "repeating-conic-gradient":
gradient.kind = conicGradient
gradient.repeating = true

default:
return
}

// Bail if any token is a "var()" since it may introduce commas
tokens := *token.Children
for _, t := range tokens {
if t.Kind == css_lexer.TFunction && strings.EqualFold(t.Text, "var") {
return
}
}

// Try to strip the initial tokens
if len(tokens) > 0 && !looksLikeColor(tokens[0]) {
i := 0
for i < len(tokens) && tokens[i].Kind != css_lexer.TComma {
i++
}
gradient.initialTokens = tokens[:i]
if i < len(tokens) {
tokens = tokens[i+1:]
} else {
tokens = nil
}
}

// Try to parse the color stops
for len(tokens) > 0 {
// Parse the color
color := tokens[0]
if !looksLikeColor(color) {
return
}
tokens = tokens[1:]

// Parse up to two positions
var positions []css_ast.Token
for len(positions) < 2 && len(tokens) > 0 {
position := tokens[0]
if position.Kind.IsNumeric() || (position.Kind == css_lexer.TFunction && strings.EqualFold(position.Text, "calc")) {
positions = append(positions, position)
} else {
break
}
tokens = tokens[1:]
}

// Add the color stop
gradient.colorStops = append(gradient.colorStops, colorStop{
color: color,
positions: positions,
})

// Parse the comma
if len(tokens) > 0 {
if tokens[0].Kind != css_lexer.TComma {
return
}
tokens = tokens[1:]
}
}

success = true
return
}

func (p *parser) generateGradient(token css_ast.Token, gradient parsedGradient) css_ast.Token {
var children []css_ast.Token
commaToken := p.commaToken(token.Loc)

children = append(children, gradient.initialTokens...)
for _, stop := range gradient.colorStops {
if len(children) > 0 {
children = append(children, commaToken)
}
children = append(children, stop.color)
children = append(children, stop.positions...)
}

token.Children = &children
return token
}

func (p *parser) lowerAndMinifyGradient(token css_ast.Token, wouldClipColor *bool) css_ast.Token {
gradient, ok := parseGradient(token)
if !ok {
return token
}

// Lower all colors in the gradient stop
for i, stop := range gradient.colorStops {
gradient.colorStops[i].color = p.lowerAndMinifyColor(stop.color, wouldClipColor)
}

if p.options.unsupportedCSSFeatures.Has(compat.GradientDoublePosition) {
// Replace double positions with duplicated single positions
for _, stop := range gradient.colorStops {
if len(stop.positions) > 1 {
gradient.colorStops = switchToSinglePositions(gradient.colorStops)
break
}
}
} else if p.options.minifySyntax {
// Replace duplicated single positions with double positions
for i, stop := range gradient.colorStops {
if i > 0 && len(stop.positions) == 1 {
if prev := gradient.colorStops[i-1]; len(prev.positions) == 1 &&
css_ast.TokensEqual([]css_ast.Token{prev.color}, []css_ast.Token{stop.color}, nil) {
gradient.colorStops = switchToDoublePositions(gradient.colorStops)
break
}
}
}
}

return p.generateGradient(token, gradient)
}

func switchToSinglePositions(double []colorStop) (single []colorStop) {
for _, stop := range double {
for _, position := range stop.positions {
position.Whitespace = css_ast.WhitespaceBefore
stop.positions = []css_ast.Token{position}
single = append(single, stop)
}
if len(stop.positions) == 0 {
single = append(single, stop)
}
}
return
}

func switchToDoublePositions(single []colorStop) (double []colorStop) {
for i := 0; i < len(single); i++ {
stop := single[i]
if i+1 < len(single) && len(stop.positions) == 1 {
if next := single[i+1]; len(next.positions) == 1 &&
css_ast.TokensEqual([]css_ast.Token{stop.color}, []css_ast.Token{next.color}, nil) {
double = append(double, colorStop{
color: stop.color,
positions: []css_ast.Token{stop.positions[0], next.positions[0]},
})
i++
continue
}
}
double = append(double, stop)
}
return
}
Loading

0 comments on commit e4c55af

Please sign in to comment.