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

     1  package css_parser
     2  
     3  import (
     4  	"fmt"
     5  	"math"
     6  	"strconv"
     7  	"strings"
     8  
     9  	"github.com/evanw/esbuild/internal/compat"
    10  	"github.com/evanw/esbuild/internal/css_ast"
    11  	"github.com/evanw/esbuild/internal/css_lexer"
    12  	"github.com/evanw/esbuild/internal/helpers"
    13  	"github.com/evanw/esbuild/internal/logger"
    14  )
    15  
    16  type gradientKind uint8
    17  
    18  const (
    19  	linearGradient gradientKind = iota
    20  	radialGradient
    21  	conicGradient
    22  )
    23  
    24  type parsedGradient struct {
    25  	leadingTokens []css_ast.Token
    26  	colorStops    []colorStop
    27  	kind          gradientKind
    28  	repeating     bool
    29  }
    30  
    31  type colorStop struct {
    32  	positions []css_ast.Token
    33  	color     css_ast.Token
    34  	midpoint  css_ast.Token // Absent if "midpoint.Kind == css_lexer.T(0)"
    35  }
    36  
    37  func parseGradient(token css_ast.Token) (gradient parsedGradient, success bool) {
    38  	if token.Kind != css_lexer.TFunction {
    39  		return
    40  	}
    41  
    42  	switch strings.ToLower(token.Text) {
    43  	case "linear-gradient":
    44  		gradient.kind = linearGradient
    45  
    46  	case "radial-gradient":
    47  		gradient.kind = radialGradient
    48  
    49  	case "conic-gradient":
    50  		gradient.kind = conicGradient
    51  
    52  	case "repeating-linear-gradient":
    53  		gradient.kind = linearGradient
    54  		gradient.repeating = true
    55  
    56  	case "repeating-radial-gradient":
    57  		gradient.kind = radialGradient
    58  		gradient.repeating = true
    59  
    60  	case "repeating-conic-gradient":
    61  		gradient.kind = conicGradient
    62  		gradient.repeating = true
    63  
    64  	default:
    65  		return
    66  	}
    67  
    68  	// Bail if any token is a "var()" since it may introduce commas
    69  	tokens := *token.Children
    70  	for _, t := range tokens {
    71  		if t.Kind == css_lexer.TFunction && strings.EqualFold(t.Text, "var") {
    72  			return
    73  		}
    74  	}
    75  
    76  	// Try to strip the initial tokens
    77  	if len(tokens) > 0 && !looksLikeColor(tokens[0]) {
    78  		i := 0
    79  		for i < len(tokens) && tokens[i].Kind != css_lexer.TComma {
    80  			i++
    81  		}
    82  		gradient.leadingTokens = tokens[:i]
    83  		if i < len(tokens) {
    84  			tokens = tokens[i+1:]
    85  		} else {
    86  			tokens = nil
    87  		}
    88  	}
    89  
    90  	// Try to parse the color stops
    91  	for len(tokens) > 0 {
    92  		// Parse the color
    93  		color := tokens[0]
    94  		if !looksLikeColor(color) {
    95  			return
    96  		}
    97  		tokens = tokens[1:]
    98  
    99  		// Parse up to two positions
   100  		var positions []css_ast.Token
   101  		for len(positions) < 2 && len(tokens) > 0 {
   102  			position := tokens[0]
   103  			if position.Kind.IsNumeric() || (position.Kind == css_lexer.TFunction && strings.EqualFold(position.Text, "calc")) {
   104  				positions = append(positions, position)
   105  			} else {
   106  				break
   107  			}
   108  			tokens = tokens[1:]
   109  		}
   110  
   111  		// Parse the comma
   112  		var midpoint css_ast.Token
   113  		if len(tokens) > 0 {
   114  			if tokens[0].Kind != css_lexer.TComma {
   115  				return
   116  			}
   117  			tokens = tokens[1:]
   118  			if len(tokens) == 0 {
   119  				return
   120  			}
   121  
   122  			// Parse the midpoint, if any
   123  			if len(tokens) > 0 && tokens[0].Kind.IsNumeric() {
   124  				midpoint = tokens[0]
   125  				tokens = tokens[1:]
   126  
   127  				// Followed by a mandatory comma
   128  				if len(tokens) == 0 || tokens[0].Kind != css_lexer.TComma {
   129  					return
   130  				}
   131  				tokens = tokens[1:]
   132  			}
   133  		}
   134  
   135  		// Add the color stop
   136  		gradient.colorStops = append(gradient.colorStops, colorStop{
   137  			color:     color,
   138  			positions: positions,
   139  			midpoint:  midpoint,
   140  		})
   141  	}
   142  
   143  	success = true
   144  	return
   145  }
   146  
   147  func (p *parser) generateGradient(token css_ast.Token, gradient parsedGradient) css_ast.Token {
   148  	var children []css_ast.Token
   149  	commaToken := p.commaToken(token.Loc)
   150  
   151  	children = append(children, gradient.leadingTokens...)
   152  	for _, stop := range gradient.colorStops {
   153  		if len(children) > 0 {
   154  			children = append(children, commaToken)
   155  		}
   156  		if len(stop.positions) == 0 && stop.midpoint.Kind == css_lexer.T(0) {
   157  			stop.color.Whitespace &= ^css_ast.WhitespaceAfter
   158  		}
   159  		children = append(children, stop.color)
   160  		children = append(children, stop.positions...)
   161  		if stop.midpoint.Kind != css_lexer.T(0) {
   162  			children = append(children, commaToken, stop.midpoint)
   163  		}
   164  	}
   165  
   166  	token.Children = &children
   167  	return token
   168  }
   169  
   170  func (p *parser) lowerAndMinifyGradient(token css_ast.Token, wouldClipColor *bool) css_ast.Token {
   171  	gradient, ok := parseGradient(token)
   172  	if !ok {
   173  		return token
   174  	}
   175  
   176  	lowerMidpoints := p.options.unsupportedCSSFeatures.Has(compat.GradientMidpoints)
   177  	lowerColorSpaces := p.options.unsupportedCSSFeatures.Has(compat.ColorFunctions)
   178  	lowerInterpolation := p.options.unsupportedCSSFeatures.Has(compat.GradientInterpolation)
   179  
   180  	// Assume that if the browser doesn't support color spaces in gradients, then
   181  	// it doesn't correctly interpolate non-sRGB colors even when a color space
   182  	// is not specified. This is the case for Firefox 120, for example, which has
   183  	// support for the "color()" syntax but not for color spaces in gradients.
   184  	// There is no entry in our feature support matrix for this edge case so we
   185  	// make this assumption instead.
   186  	//
   187  	// Note that this edge case means we have to _replace_ the original gradient
   188  	// with the expanded one instead of inserting a fallback before it. Otherwise
   189  	// Firefox 120 would use the original gradient instead of the fallback because
   190  	// it supports the syntax, but just renders it incorrectly.
   191  	if lowerInterpolation {
   192  		lowerColorSpaces = true
   193  	}
   194  
   195  	// Potentially expand the gradient to handle unsupported features
   196  	didExpand := false
   197  	if lowerMidpoints || lowerColorSpaces || lowerInterpolation {
   198  		if colorStops, ok := tryToParseColorStops(gradient); ok {
   199  			hasColorSpace := false
   200  			hasMidpoint := false
   201  			for _, stop := range colorStops {
   202  				if stop.hasColorSpace {
   203  					hasColorSpace = true
   204  				}
   205  				if stop.midpoint != nil {
   206  					hasMidpoint = true
   207  				}
   208  			}
   209  			remaining, colorSpace, hueMethod, hasInterpolation := removeColorInterpolation(gradient.leadingTokens)
   210  			if (hasInterpolation && lowerInterpolation) || (hasColorSpace && lowerColorSpaces) || (hasMidpoint && lowerMidpoints) {
   211  				if hasInterpolation {
   212  					tryToExpandGradient(token.Loc, &gradient, colorStops, remaining, colorSpace, hueMethod)
   213  				} else {
   214  					if hasColorSpace {
   215  						colorSpace = colorSpace_oklab
   216  					} else {
   217  						colorSpace = colorSpace_srgb
   218  					}
   219  					tryToExpandGradient(token.Loc, &gradient, colorStops, gradient.leadingTokens, colorSpace, shorterHue)
   220  				}
   221  				didExpand = true
   222  			}
   223  		}
   224  	}
   225  
   226  	// Lower all colors in the gradient stop
   227  	for i, stop := range gradient.colorStops {
   228  		gradient.colorStops[i].color = p.lowerAndMinifyColor(stop.color, wouldClipColor)
   229  	}
   230  
   231  	if p.options.unsupportedCSSFeatures.Has(compat.GradientDoublePosition) {
   232  		// Replace double positions with duplicated single positions
   233  		for _, stop := range gradient.colorStops {
   234  			if len(stop.positions) > 1 {
   235  				gradient.colorStops = switchToSinglePositions(gradient.colorStops)
   236  				break
   237  			}
   238  		}
   239  	} else if p.options.minifySyntax {
   240  		// Replace duplicated single positions with double positions
   241  		for i, stop := range gradient.colorStops {
   242  			if i > 0 && len(stop.positions) == 1 {
   243  				if prev := gradient.colorStops[i-1]; len(prev.positions) == 1 && prev.midpoint.Kind == css_lexer.T(0) &&
   244  					css_ast.TokensEqual([]css_ast.Token{prev.color}, []css_ast.Token{stop.color}, nil) {
   245  					gradient.colorStops = switchToDoublePositions(gradient.colorStops)
   246  					break
   247  				}
   248  			}
   249  		}
   250  	}
   251  
   252  	if p.options.minifySyntax || didExpand {
   253  		gradient.colorStops = removeImpliedPositions(gradient.kind, gradient.colorStops)
   254  	}
   255  
   256  	return p.generateGradient(token, gradient)
   257  }
   258  
   259  func removeImpliedPositions(kind gradientKind, colorStops []colorStop) []colorStop {
   260  	if len(colorStops) == 0 {
   261  		return colorStops
   262  	}
   263  
   264  	positions := make([]valueWithUnit, len(colorStops))
   265  	for i, stop := range colorStops {
   266  		if len(stop.positions) == 1 {
   267  			if pos, ok := tryToParseValue(stop.positions[0], kind); ok {
   268  				positions[i] = pos
   269  				continue
   270  			}
   271  		}
   272  		positions[i].value = helpers.NewF64(math.NaN())
   273  	}
   274  
   275  	start := 0
   276  	for start < len(colorStops) {
   277  		if startPos := positions[start]; !startPos.value.IsNaN() {
   278  			end := start + 1
   279  		run:
   280  			for colorStops[end-1].midpoint.Kind == css_lexer.T(0) && end < len(colorStops) {
   281  				endPos := positions[end]
   282  				if endPos.value.IsNaN() || endPos.unit != startPos.unit {
   283  					break
   284  				}
   285  
   286  				// Check that all values in this run are implied. Interpolation is done
   287  				// using the start and end positions instead of the first and second
   288  				// positions because it's more accurate.
   289  				for i := start + 1; i < end; i++ {
   290  					t := helpers.NewF64(float64(i - start)).DivConst(float64(end - start))
   291  					impliedValue := helpers.Lerp(startPos.value, endPos.value, t)
   292  					if positions[i].value.Sub(impliedValue).Abs().Value() > 0.01 {
   293  						break run
   294  					}
   295  				}
   296  				end++
   297  			}
   298  
   299  			// Clear out all implied values
   300  			if end-start > 1 {
   301  				for i := start + 1; i+1 < end; i++ {
   302  					colorStops[i].positions = nil
   303  				}
   304  				start = end - 1
   305  				continue
   306  			}
   307  		}
   308  		start++
   309  	}
   310  
   311  	if first := colorStops[0].positions; len(first) == 1 &&
   312  		((first[0].Kind == css_lexer.TPercentage && first[0].PercentageValue() == "0") ||
   313  			(first[0].Kind == css_lexer.TDimension && first[0].DimensionValue() == "0")) {
   314  		colorStops[0].positions = nil
   315  	}
   316  
   317  	if last := colorStops[len(colorStops)-1].positions; len(last) == 1 &&
   318  		last[0].Kind == css_lexer.TPercentage && last[0].PercentageValue() == "100" {
   319  		colorStops[len(colorStops)-1].positions = nil
   320  	}
   321  
   322  	return colorStops
   323  }
   324  
   325  func switchToSinglePositions(double []colorStop) (single []colorStop) {
   326  	for _, stop := range double {
   327  		for i := range stop.positions {
   328  			stop.positions[i].Whitespace = css_ast.WhitespaceBefore
   329  		}
   330  		for len(stop.positions) > 1 {
   331  			clone := stop
   332  			clone.positions = stop.positions[:1]
   333  			clone.midpoint = css_ast.Token{}
   334  			single = append(single, clone)
   335  			stop.positions = stop.positions[1:]
   336  		}
   337  		single = append(single, stop)
   338  	}
   339  	return
   340  }
   341  
   342  func switchToDoublePositions(single []colorStop) (double []colorStop) {
   343  	for i := 0; i < len(single); i++ {
   344  		stop := single[i]
   345  		if i+1 < len(single) && len(stop.positions) == 1 && stop.midpoint.Kind == css_lexer.T(0) {
   346  			if next := single[i+1]; len(next.positions) == 1 &&
   347  				css_ast.TokensEqual([]css_ast.Token{stop.color}, []css_ast.Token{next.color}, nil) {
   348  				double = append(double, colorStop{
   349  					color:     stop.color,
   350  					positions: []css_ast.Token{stop.positions[0], next.positions[0]},
   351  					midpoint:  next.midpoint,
   352  				})
   353  				i++
   354  				continue
   355  			}
   356  		}
   357  		double = append(double, stop)
   358  	}
   359  	return
   360  }
   361  
   362  func removeColorInterpolation(tokens []css_ast.Token) ([]css_ast.Token, colorSpace, hueMethod, bool) {
   363  	for i := 0; i+1 < len(tokens); i++ {
   364  		if in := tokens[i]; in.Kind == css_lexer.TIdent && strings.EqualFold(in.Text, "in") {
   365  			if space := tokens[i+1]; space.Kind == css_lexer.TIdent {
   366  				var colorSpace colorSpace
   367  				hueMethod := shorterHue
   368  				start := i
   369  				end := i + 2
   370  
   371  				// Parse the color space
   372  				switch strings.ToLower(space.Text) {
   373  				case "a98-rgb":
   374  					colorSpace = colorSpace_a98_rgb
   375  				case "display-p3":
   376  					colorSpace = colorSpace_display_p3
   377  				case "hsl":
   378  					colorSpace = colorSpace_hsl
   379  				case "hwb":
   380  					colorSpace = colorSpace_hwb
   381  				case "lab":
   382  					colorSpace = colorSpace_lab
   383  				case "lch":
   384  					colorSpace = colorSpace_lch
   385  				case "oklab":
   386  					colorSpace = colorSpace_oklab
   387  				case "oklch":
   388  					colorSpace = colorSpace_oklch
   389  				case "prophoto-rgb":
   390  					colorSpace = colorSpace_prophoto_rgb
   391  				case "rec2020":
   392  					colorSpace = colorSpace_rec2020
   393  				case "srgb":
   394  					colorSpace = colorSpace_srgb
   395  				case "srgb-linear":
   396  					colorSpace = colorSpace_srgb_linear
   397  				case "xyz":
   398  					colorSpace = colorSpace_xyz
   399  				case "xyz-d50":
   400  					colorSpace = colorSpace_xyz_d50
   401  				case "xyz-d65":
   402  					colorSpace = colorSpace_xyz_d65
   403  				default:
   404  					return nil, 0, 0, false
   405  				}
   406  
   407  				// Parse the optional hue mode for polar color spaces
   408  				if colorSpace.isPolar() && i+3 < len(tokens) {
   409  					if hue := tokens[i+3]; hue.Kind == css_lexer.TIdent && strings.EqualFold(hue.Text, "hue") {
   410  						if method := tokens[i+2]; method.Kind == css_lexer.TIdent {
   411  							switch strings.ToLower(method.Text) {
   412  							case "shorter":
   413  								hueMethod = shorterHue
   414  							case "longer":
   415  								hueMethod = longerHue
   416  							case "increasing":
   417  								hueMethod = increasingHue
   418  							case "decreasing":
   419  								hueMethod = decreasingHue
   420  							default:
   421  								return nil, 0, 0, false
   422  							}
   423  							end = i + 4
   424  						}
   425  					}
   426  				}
   427  
   428  				// Remove all parsed tokens
   429  				remaining := append(append([]css_ast.Token{}, tokens[:start]...), tokens[end:]...)
   430  				if n := len(remaining); n > 0 {
   431  					remaining[0].Whitespace &= ^css_ast.WhitespaceBefore
   432  					remaining[n-1].Whitespace &= ^css_ast.WhitespaceAfter
   433  				}
   434  				return remaining, colorSpace, hueMethod, true
   435  			}
   436  		}
   437  	}
   438  
   439  	return nil, 0, 0, false
   440  }
   441  
   442  type valueWithUnit struct {
   443  	unit  string
   444  	value F64
   445  }
   446  
   447  type parsedColorStop struct {
   448  	// Position information (may be a sum of two different units)
   449  	positionTerms []valueWithUnit
   450  
   451  	// Color midpoint (a.k.a. transition hint) information
   452  	midpoint *valueWithUnit
   453  
   454  	// Non-premultiplied color information in XYZ space
   455  	x, y, z, alpha F64
   456  
   457  	// Non-premultiplied color information in sRGB space
   458  	r, g, b F64
   459  
   460  	// Premultiplied color information in the interpolation color space
   461  	v0, v1, v2 F64
   462  
   463  	// True if the original color has a color space
   464  	hasColorSpace bool
   465  }
   466  
   467  func tryToParseColorStops(gradient parsedGradient) ([]parsedColorStop, bool) {
   468  	var colorStops []parsedColorStop
   469  
   470  	for _, stop := range gradient.colorStops {
   471  		color, ok := parseColor(stop.color)
   472  		if !ok {
   473  			return nil, false
   474  		}
   475  		var r, g, b F64
   476  		if !color.hasColorSpace {
   477  			r = helpers.NewF64(float64(hexR(color.hex))).DivConst(255)
   478  			g = helpers.NewF64(float64(hexG(color.hex))).DivConst(255)
   479  			b = helpers.NewF64(float64(hexB(color.hex))).DivConst(255)
   480  			color.x, color.y, color.z = lin_srgb_to_xyz(lin_srgb(r, g, b))
   481  		} else {
   482  			r, g, b = gam_srgb(xyz_to_lin_srgb(color.x, color.y, color.z))
   483  		}
   484  		parsedStop := parsedColorStop{
   485  			x:             color.x,
   486  			y:             color.y,
   487  			z:             color.z,
   488  			r:             r,
   489  			g:             g,
   490  			b:             b,
   491  			alpha:         helpers.NewF64(float64(hexA(color.hex))).DivConst(255),
   492  			hasColorSpace: color.hasColorSpace,
   493  		}
   494  
   495  		for i, position := range stop.positions {
   496  			if position, ok := tryToParseValue(position, gradient.kind); ok {
   497  				parsedStop.positionTerms = []valueWithUnit{position}
   498  			} else {
   499  				return nil, false
   500  			}
   501  
   502  			// Expand double positions
   503  			if i+1 < len(stop.positions) {
   504  				colorStops = append(colorStops, parsedStop)
   505  			}
   506  		}
   507  
   508  		if stop.midpoint.Kind != css_lexer.T(0) {
   509  			if midpoint, ok := tryToParseValue(stop.midpoint, gradient.kind); ok {
   510  				parsedStop.midpoint = &midpoint
   511  			} else {
   512  				return nil, false
   513  			}
   514  		}
   515  
   516  		colorStops = append(colorStops, parsedStop)
   517  	}
   518  
   519  	// Automatically fill in missing positions
   520  	if len(colorStops) > 0 {
   521  		type stopInfo struct {
   522  			fromPos   valueWithUnit
   523  			toPos     valueWithUnit
   524  			fromCount int32
   525  			toCount   int32
   526  		}
   527  
   528  		// Fill in missing positions for the endpoints first
   529  		if first := &colorStops[0]; len(first.positionTerms) == 0 {
   530  			first.positionTerms = []valueWithUnit{{value: helpers.NewF64(0), unit: "%"}}
   531  		}
   532  		if last := &colorStops[len(colorStops)-1]; len(last.positionTerms) == 0 {
   533  			last.positionTerms = []valueWithUnit{{value: helpers.NewF64(100), unit: "%"}}
   534  		}
   535  
   536  		// Set all positions to be greater than the position before them
   537  		for i, stop := range colorStops {
   538  			var prevPos valueWithUnit
   539  			for j := i - 1; j >= 0; j-- {
   540  				prev := colorStops[j]
   541  				if prev.midpoint != nil {
   542  					prevPos = *prev.midpoint
   543  					break
   544  				}
   545  				if len(prev.positionTerms) == 1 {
   546  					prevPos = prev.positionTerms[0]
   547  					break
   548  				}
   549  			}
   550  			if len(stop.positionTerms) == 1 {
   551  				if prevPos.unit == stop.positionTerms[0].unit {
   552  					stop.positionTerms[0].value = helpers.Max2(prevPos.value, stop.positionTerms[0].value)
   553  				}
   554  				prevPos = stop.positionTerms[0]
   555  			}
   556  			if stop.midpoint != nil && prevPos.unit == stop.midpoint.unit {
   557  				stop.midpoint.value = helpers.Max2(prevPos.value, stop.midpoint.value)
   558  			}
   559  		}
   560  
   561  		// Scan over all other stops with missing positions
   562  		infos := make([]stopInfo, len(colorStops))
   563  		for i, stop := range colorStops {
   564  			if len(stop.positionTerms) == 1 {
   565  				continue
   566  			}
   567  			info := &infos[i]
   568  
   569  			// Scan backward
   570  			for from := i - 1; from >= 0; from-- {
   571  				fromStop := colorStops[from]
   572  				info.fromCount++
   573  				if fromStop.midpoint != nil {
   574  					info.fromPos = *fromStop.midpoint
   575  					break
   576  				}
   577  				if len(fromStop.positionTerms) == 1 {
   578  					info.fromPos = fromStop.positionTerms[0]
   579  					break
   580  				}
   581  			}
   582  
   583  			// Scan forward
   584  			for to := i; to < len(colorStops); to++ {
   585  				info.toCount++
   586  				if toStop := colorStops[to]; toStop.midpoint != nil {
   587  					info.toPos = *toStop.midpoint
   588  					break
   589  				}
   590  				if to+1 < len(colorStops) {
   591  					if toStop := colorStops[to+1]; len(toStop.positionTerms) == 1 {
   592  						info.toPos = toStop.positionTerms[0]
   593  						break
   594  					}
   595  				}
   596  			}
   597  		}
   598  
   599  		// Then fill in all other missing positions
   600  		for i, stop := range colorStops {
   601  			if len(stop.positionTerms) != 1 {
   602  				info := infos[i]
   603  				t := helpers.NewF64(float64(info.fromCount)).DivConst(float64(info.fromCount + info.toCount))
   604  				if info.fromPos.unit == info.toPos.unit {
   605  					colorStops[i].positionTerms = []valueWithUnit{{
   606  						value: helpers.Lerp(info.fromPos.value, info.toPos.value, t),
   607  						unit:  info.fromPos.unit,
   608  					}}
   609  				} else {
   610  					colorStops[i].positionTerms = []valueWithUnit{{
   611  						value: t.Neg().AddConst(1).Mul(info.fromPos.value),
   612  						unit:  info.fromPos.unit,
   613  					}, {
   614  						value: t.Mul(info.toPos.value),
   615  						unit:  info.toPos.unit,
   616  					}}
   617  				}
   618  			}
   619  		}
   620  
   621  		// Midpoints are only supported if they use the same units as their neighbors
   622  		for i, stop := range colorStops {
   623  			if stop.midpoint != nil {
   624  				next := colorStops[i+1]
   625  				if len(stop.positionTerms) != 1 || stop.midpoint.unit != stop.positionTerms[0].unit ||
   626  					len(next.positionTerms) != 1 || stop.midpoint.unit != next.positionTerms[0].unit {
   627  					return nil, false
   628  				}
   629  			}
   630  		}
   631  	}
   632  
   633  	return colorStops, true
   634  }
   635  
   636  func tryToParseValue(token css_ast.Token, kind gradientKind) (result valueWithUnit, success bool) {
   637  	if kind == conicGradient {
   638  		// <angle-percentage>
   639  		switch token.Kind {
   640  		case css_lexer.TDimension:
   641  			degrees, ok := degreesForAngle(token)
   642  			if !ok {
   643  				return
   644  			}
   645  			result.value = helpers.NewF64(degrees).MulConst(100.0 / 360)
   646  			result.unit = "%"
   647  
   648  		case css_lexer.TPercentage:
   649  			percent, err := strconv.ParseFloat(token.PercentageValue(), 64)
   650  			if err != nil {
   651  				return
   652  			}
   653  			result.value = helpers.NewF64(percent)
   654  			result.unit = "%"
   655  
   656  		default:
   657  			return
   658  		}
   659  	} else {
   660  		// <length-percentage>
   661  		switch token.Kind {
   662  		case css_lexer.TNumber:
   663  			zero, err := strconv.ParseFloat(token.Text, 64)
   664  			if err != nil || zero != 0 {
   665  				return
   666  			}
   667  			result.value = helpers.NewF64(0)
   668  			result.unit = "%"
   669  
   670  		case css_lexer.TDimension:
   671  			dimensionValue, err := strconv.ParseFloat(token.DimensionValue(), 64)
   672  			if err != nil {
   673  				return
   674  			}
   675  			result.value = helpers.NewF64(dimensionValue)
   676  			result.unit = token.DimensionUnit()
   677  
   678  		case css_lexer.TPercentage:
   679  			percentageValue, err := strconv.ParseFloat(token.PercentageValue(), 64)
   680  			if err != nil {
   681  				return
   682  			}
   683  			result.value = helpers.NewF64(percentageValue)
   684  			result.unit = "%"
   685  
   686  		default:
   687  			return
   688  		}
   689  	}
   690  
   691  	success = true
   692  	return
   693  }
   694  
   695  func tryToExpandGradient(
   696  	loc logger.Loc,
   697  	gradient *parsedGradient,
   698  	colorStops []parsedColorStop,
   699  	remaining []css_ast.Token,
   700  	colorSpace colorSpace,
   701  	hueMethod hueMethod,
   702  ) bool {
   703  	// Convert color stops into the interpolation color space
   704  	for i := range colorStops {
   705  		stop := &colorStops[i]
   706  		v0, v1, v2 := xyz_to_colorSpace(stop.x, stop.y, stop.z, colorSpace)
   707  		stop.v0, stop.v1, stop.v2 = premultiply(v0, v1, v2, stop.alpha, colorSpace)
   708  	}
   709  
   710  	// Duplicate the endpoints if they should wrap around to themselves
   711  	if hueMethod == longerHue && colorSpace.isPolar() && len(colorStops) > 0 {
   712  		if first := colorStops[0]; len(first.positionTerms) == 1 {
   713  			if first.positionTerms[0].value.Value() < 0 {
   714  				colorStops[0].positionTerms[0].value = helpers.NewF64(0)
   715  			} else if first.positionTerms[0].value.Value() > 0 {
   716  				first.midpoint = nil
   717  				first.positionTerms = []valueWithUnit{{value: helpers.NewF64(0), unit: first.positionTerms[0].unit}}
   718  				colorStops = append([]parsedColorStop{first}, colorStops...)
   719  			}
   720  		}
   721  		if last := colorStops[len(colorStops)-1]; len(last.positionTerms) == 1 {
   722  			if last.positionTerms[0].unit != "%" || last.positionTerms[0].value.Value() < 100 {
   723  				last.positionTerms = []valueWithUnit{{value: helpers.NewF64(100), unit: "%"}}
   724  				colorStops = append(colorStops, last)
   725  			}
   726  		}
   727  	}
   728  
   729  	var newColorStops []colorStop
   730  	var generateColorStops func(
   731  		int, parsedColorStop, parsedColorStop,
   732  		F64, F64, F64, F64, F64, F64, F64, F64,
   733  		F64, F64, F64, F64, F64, F64, F64, F64,
   734  	)
   735  
   736  	generateColorStops = func(
   737  		depth int,
   738  		from parsedColorStop, to parsedColorStop,
   739  		prevX, prevY, prevZ, prevR, prevG, prevB, prevA, prevT F64,
   740  		nextX, nextY, nextZ, nextR, nextG, nextB, nextA, nextT F64,
   741  	) {
   742  		if depth > 4 {
   743  			return
   744  		}
   745  
   746  		t := prevT.Add(nextT).DivConst(2)
   747  		positionT := t
   748  
   749  		// Handle midpoints (which we have already checked uses the same units)
   750  		if from.midpoint != nil {
   751  			fromPos := from.positionTerms[0].value
   752  			toPos := to.positionTerms[0].value
   753  			stopPos := helpers.Lerp(fromPos, toPos, t)
   754  			H := from.midpoint.value.Sub(fromPos).Div(toPos.Sub(fromPos))
   755  			P := stopPos.Sub(fromPos).Div(toPos.Sub(fromPos))
   756  			if H.Value() <= 0 {
   757  				positionT = helpers.NewF64(1)
   758  			} else if H.Value() >= 1 {
   759  				positionT = helpers.NewF64(0)
   760  			} else {
   761  				positionT = P.Pow(helpers.NewF64(-1).Div(H.Log2()))
   762  			}
   763  		}
   764  
   765  		v0, v1, v2 := interpolateColors(from.v0, from.v1, from.v2, to.v0, to.v1, to.v2, colorSpace, hueMethod, positionT)
   766  		a := helpers.Lerp(from.alpha, to.alpha, positionT)
   767  		v0, v1, v2 = unpremultiply(v0, v1, v2, a, colorSpace)
   768  		x, y, z := colorSpace_to_xyz(v0, v1, v2, colorSpace)
   769  
   770  		// Stop when the color is similar enough to the sRGB midpoint
   771  		const epsilon = 4.0 / 255
   772  		r, g, b := gam_srgb(xyz_to_lin_srgb(x, y, z))
   773  		dr := r.Mul(a).Sub(prevR.Mul(prevA).Add(nextR.Mul(nextA)).DivConst(2))
   774  		dg := g.Mul(a).Sub(prevG.Mul(prevA).Add(nextG.Mul(nextA)).DivConst(2))
   775  		db := b.Mul(a).Sub(prevB.Mul(prevA).Add(nextB.Mul(nextA)).DivConst(2))
   776  		if d := dr.Squared().Add(dg.Squared()).Add(db.Squared()); d.Value() < epsilon*epsilon {
   777  			return
   778  		}
   779  
   780  		// Recursive split before this stop
   781  		generateColorStops(depth+1, from, to,
   782  			prevX, prevY, prevZ, prevR, prevG, prevB, prevA, prevT,
   783  			x, y, z, r, g, b, a, t)
   784  
   785  		// Generate this stop
   786  		color := makeColorToken(loc, x, y, z, a)
   787  		positionTerms := interpolatePositions(from.positionTerms, to.positionTerms, t)
   788  		position := makePositionToken(loc, positionTerms)
   789  		position.Whitespace = css_ast.WhitespaceBefore
   790  		newColorStops = append(newColorStops, colorStop{
   791  			color:     color,
   792  			positions: []css_ast.Token{position},
   793  		})
   794  
   795  		// Recursive split after this stop
   796  		generateColorStops(depth+1, from, to,
   797  			x, y, z, r, g, b, a, t,
   798  			nextX, nextY, nextZ, nextR, nextG, nextB, nextA, nextT)
   799  	}
   800  
   801  	for i, stop := range colorStops {
   802  		color := makeColorToken(loc, stop.x, stop.y, stop.z, stop.alpha)
   803  		position := makePositionToken(loc, stop.positionTerms)
   804  		position.Whitespace = css_ast.WhitespaceBefore
   805  		newColorStops = append(newColorStops, colorStop{
   806  			color:     color,
   807  			positions: []css_ast.Token{position},
   808  		})
   809  
   810  		// Generate new color stops in between as needed
   811  		if i+1 < len(colorStops) {
   812  			next := colorStops[i+1]
   813  			generateColorStops(0, stop, next,
   814  				stop.x, stop.y, stop.z, stop.r, stop.g, stop.b, stop.alpha, helpers.NewF64(0),
   815  				next.x, next.y, next.z, next.r, next.g, next.b, next.alpha, helpers.NewF64(1))
   816  		}
   817  	}
   818  
   819  	gradient.leadingTokens = remaining
   820  	gradient.colorStops = newColorStops
   821  	return true
   822  }
   823  
   824  func formatFloat(value F64, decimals int) string {
   825  	return strings.TrimSuffix(strings.TrimRight(strconv.FormatFloat(value.Value(), 'f', decimals, 64), "0"), ".")
   826  }
   827  
   828  func makeDimensionOrPercentToken(loc logger.Loc, value F64, unit string) (token css_ast.Token) {
   829  	token.Loc = loc
   830  	token.Text = formatFloat(value, 2)
   831  	if unit == "%" {
   832  		token.Kind = css_lexer.TPercentage
   833  	} else {
   834  		token.Kind = css_lexer.TDimension
   835  		token.UnitOffset = uint16(len(token.Text))
   836  	}
   837  	token.Text += unit
   838  	return
   839  }
   840  
   841  func makePositionToken(loc logger.Loc, positionTerms []valueWithUnit) css_ast.Token {
   842  	if len(positionTerms) == 1 {
   843  		return makeDimensionOrPercentToken(loc, positionTerms[0].value, positionTerms[0].unit)
   844  	}
   845  
   846  	children := make([]css_ast.Token, 0, 1+2*len(positionTerms))
   847  	for i, term := range positionTerms {
   848  		if i > 0 {
   849  			children = append(children, css_ast.Token{
   850  				Loc:        loc,
   851  				Kind:       css_lexer.TDelimPlus,
   852  				Text:       "+",
   853  				Whitespace: css_ast.WhitespaceBefore | css_ast.WhitespaceAfter,
   854  			})
   855  		}
   856  		children = append(children, makeDimensionOrPercentToken(loc, term.value, term.unit))
   857  	}
   858  
   859  	return css_ast.Token{
   860  		Loc:      loc,
   861  		Kind:     css_lexer.TFunction,
   862  		Text:     "calc",
   863  		Children: &children,
   864  	}
   865  }
   866  
   867  func makeColorToken(loc logger.Loc, x F64, y F64, z F64, a F64) (color css_ast.Token) {
   868  	color.Loc = loc
   869  	alpha := uint32(a.MulConst(255).Round().Value())
   870  	if hex, ok := tryToConvertToHexWithoutClipping(x, y, z, alpha); ok {
   871  		color.Kind = css_lexer.THash
   872  		if alpha == 255 {
   873  			color.Text = fmt.Sprintf("%06x", hex>>8)
   874  		} else {
   875  			color.Text = fmt.Sprintf("%08x", hex)
   876  		}
   877  	} else {
   878  		children := []css_ast.Token{
   879  			{
   880  				Loc:        loc,
   881  				Kind:       css_lexer.TIdent,
   882  				Text:       "xyz",
   883  				Whitespace: css_ast.WhitespaceAfter,
   884  			},
   885  			{
   886  				Loc:        loc,
   887  				Kind:       css_lexer.TNumber,
   888  				Text:       formatFloat(x, 3),
   889  				Whitespace: css_ast.WhitespaceBefore | css_ast.WhitespaceAfter,
   890  			},
   891  			{
   892  				Loc:        loc,
   893  				Kind:       css_lexer.TNumber,
   894  				Text:       formatFloat(y, 3),
   895  				Whitespace: css_ast.WhitespaceBefore | css_ast.WhitespaceAfter,
   896  			},
   897  			{
   898  				Loc:        loc,
   899  				Kind:       css_lexer.TNumber,
   900  				Text:       formatFloat(z, 3),
   901  				Whitespace: css_ast.WhitespaceBefore,
   902  			},
   903  		}
   904  		if a.Value() < 1 {
   905  			children = append(children,
   906  				css_ast.Token{
   907  					Loc:        loc,
   908  					Kind:       css_lexer.TDelimSlash,
   909  					Text:       "/",
   910  					Whitespace: css_ast.WhitespaceBefore | css_ast.WhitespaceAfter,
   911  				},
   912  				css_ast.Token{
   913  					Loc:        loc,
   914  					Kind:       css_lexer.TNumber,
   915  					Text:       formatFloat(a, 3),
   916  					Whitespace: css_ast.WhitespaceBefore,
   917  				},
   918  			)
   919  		}
   920  		color.Kind = css_lexer.TFunction
   921  		color.Text = "color"
   922  		color.Children = &children
   923  	}
   924  	return
   925  }
   926  
   927  func interpolateHues(a, b, t F64, hueMethod hueMethod) F64 {
   928  	a = a.DivConst(360)
   929  	b = b.DivConst(360)
   930  	a = a.Sub(a.Floor())
   931  	b = b.Sub(b.Floor())
   932  
   933  	switch hueMethod {
   934  	case shorterHue:
   935  		delta := b.Sub(a)
   936  		if delta.Value() > 0.5 {
   937  			a = a.AddConst(1)
   938  		}
   939  		if delta.Value() < -0.5 {
   940  			b = b.AddConst(1)
   941  		}
   942  
   943  	case longerHue:
   944  		delta := b.Sub(a)
   945  		if delta.Value() > 0 && delta.Value() < 0.5 {
   946  			a = a.AddConst(1)
   947  		}
   948  		if delta.Value() > -0.5 && delta.Value() <= 0 {
   949  			b = b.AddConst(1)
   950  		}
   951  
   952  	case increasingHue:
   953  		if b.Value() < a.Value() {
   954  			b = b.AddConst(1)
   955  		}
   956  
   957  	case decreasingHue:
   958  		if a.Value() < b.Value() {
   959  			a = a.AddConst(1)
   960  		}
   961  	}
   962  
   963  	return helpers.Lerp(a, b, t).MulConst(360)
   964  }
   965  
   966  func interpolateColors(
   967  	a0, a1, a2 F64, b0, b1, b2 F64,
   968  	colorSpace colorSpace, hueMethod hueMethod, t F64,
   969  ) (v0 F64, v1 F64, v2 F64) {
   970  	v1 = helpers.Lerp(a1, b1, t)
   971  
   972  	switch colorSpace {
   973  	case colorSpace_hsl, colorSpace_hwb:
   974  		v2 = helpers.Lerp(a2, b2, t)
   975  		v0 = interpolateHues(a0, b0, t, hueMethod)
   976  
   977  	case colorSpace_lch, colorSpace_oklch:
   978  		v0 = helpers.Lerp(a0, b0, t)
   979  		v2 = interpolateHues(a2, b2, t, hueMethod)
   980  
   981  	default:
   982  		v0 = helpers.Lerp(a0, b0, t)
   983  		v2 = helpers.Lerp(a2, b2, t)
   984  	}
   985  
   986  	return v0, v1, v2
   987  }
   988  
   989  func interpolatePositions(a []valueWithUnit, b []valueWithUnit, t F64) (result []valueWithUnit) {
   990  	findUnit := func(unit string) int {
   991  		for i, x := range result {
   992  			if x.unit == unit {
   993  				return i
   994  			}
   995  		}
   996  		result = append(result, valueWithUnit{unit: unit})
   997  		return len(result) - 1
   998  	}
   999  
  1000  	// "result += a * (1 - t)"
  1001  	for _, term := range a {
  1002  		ptr := &result[findUnit(term.unit)]
  1003  		ptr.value = t.Neg().AddConst(1).Mul(term.value).Add(ptr.value)
  1004  	}
  1005  
  1006  	// "result += b * t"
  1007  	for _, term := range b {
  1008  		ptr := &result[findUnit(term.unit)]
  1009  		ptr.value = t.Mul(term.value).Add(ptr.value)
  1010  	}
  1011  
  1012  	// Remove an extra zero value for neatness. We don't remove all
  1013  	// of them because it may be important to retain a single zero.
  1014  	if len(result) > 1 {
  1015  		for i, term := range result {
  1016  			if term.value.Value() == 0 {
  1017  				copy(result[i:], result[i+1:])
  1018  				result = result[:len(result)-1]
  1019  				break
  1020  			}
  1021  		}
  1022  	}
  1023  
  1024  	return
  1025  }
  1026  
  1027  func premultiply(v0, v1, v2, alpha F64, colorSpace colorSpace) (F64, F64, F64) {
  1028  	if alpha.Value() < 1 {
  1029  		switch colorSpace {
  1030  		case colorSpace_hsl, colorSpace_hwb:
  1031  			v2 = v2.Mul(alpha)
  1032  		case colorSpace_lch, colorSpace_oklch:
  1033  			v0 = v0.Mul(alpha)
  1034  		default:
  1035  			v0 = v0.Mul(alpha)
  1036  			v2 = v2.Mul(alpha)
  1037  		}
  1038  		v1 = v1.Mul(alpha)
  1039  	}
  1040  	return v0, v1, v2
  1041  }
  1042  
  1043  func unpremultiply(v0, v1, v2, alpha F64, colorSpace colorSpace) (F64, F64, F64) {
  1044  	if alpha.Value() > 0 && alpha.Value() < 1 {
  1045  		switch colorSpace {
  1046  		case colorSpace_hsl, colorSpace_hwb:
  1047  			v2 = v2.Div(alpha)
  1048  		case colorSpace_lch, colorSpace_oklch:
  1049  			v0 = v0.Div(alpha)
  1050  		default:
  1051  			v0 = v0.Div(alpha)
  1052  			v2 = v2.Div(alpha)
  1053  		}
  1054  		v1 = v1.Div(alpha)
  1055  	}
  1056  	return v0, v1, v2
  1057  }