userland: add archive tools and pinned GoAWK, esbuild, and QuickJS

This commit is contained in:
2026-11-07 10:56:29 +00:00
parent 7e7e49bfa2
commit d79b2bc2e1
1798 changed files with 562969 additions and 1 deletions
@@ -0,0 +1,620 @@
package css_parser
import (
"math"
"github.com/evanw/esbuild/internal/helpers"
)
// Wrap float64 math to avoid compiler optimizations that break determinism
type F64 = helpers.F64
// Reference: https://drafts.csswg.org/css-color/#color-conversion-code
type colorSpace uint8
const (
colorSpace_a98_rgb colorSpace = iota
colorSpace_display_p3
colorSpace_hsl
colorSpace_hwb
colorSpace_lab
colorSpace_lch
colorSpace_oklab
colorSpace_oklch
colorSpace_prophoto_rgb
colorSpace_rec2020
colorSpace_srgb
colorSpace_srgb_linear
colorSpace_xyz
colorSpace_xyz_d50
colorSpace_xyz_d65
)
func (colorSpace colorSpace) isPolar() bool {
switch colorSpace {
case colorSpace_hsl, colorSpace_hwb, colorSpace_lch, colorSpace_oklch:
return true
}
return false
}
type hueMethod uint8
const (
shorterHue hueMethod = iota
longerHue
increasingHue
decreasingHue
)
func lin_srgb(r F64, g F64, b F64) (F64, F64, F64) {
f := func(val F64) F64 {
if abs := val.Abs(); abs.Value() < 0.04045 {
return val.DivConst(12.92)
} else {
return abs.AddConst(0.055).DivConst(1.055).PowConst(2.4).WithSignFrom(val)
}
}
return f(r), f(g), f(b)
}
func gam_srgb(r F64, g F64, b F64) (F64, F64, F64) {
f := func(val F64) F64 {
if abs := val.Abs(); abs.Value() > 0.0031308 {
return abs.PowConst(1 / 2.4).MulConst(1.055).SubConst(0.055).WithSignFrom(val)
} else {
return val.MulConst(12.92)
}
}
return f(r), f(g), f(b)
}
func lin_srgb_to_xyz(r F64, g F64, b F64) (F64, F64, F64) {
M := [9]float64{
506752.0 / 1228815, 87881.0 / 245763, 12673.0 / 70218,
87098.0 / 409605, 175762.0 / 245763, 12673.0 / 175545,
7918.0 / 409605, 87881.0 / 737289, 1001167.0 / 1053270,
}
return multiplyMatrices(M, r, g, b)
}
func xyz_to_lin_srgb(x F64, y F64, z F64) (F64, F64, F64) {
M := [9]float64{
12831.0 / 3959, -329.0 / 214, -1974.0 / 3959,
-851781.0 / 878810, 1648619.0 / 878810, 36519.0 / 878810,
705.0 / 12673, -2585.0 / 12673, 705.0 / 667,
}
return multiplyMatrices(M, x, y, z)
}
func lin_p3(r F64, g F64, b F64) (F64, F64, F64) {
return lin_srgb(r, g, b)
}
func gam_p3(r F64, g F64, b F64) (F64, F64, F64) {
return gam_srgb(r, g, b)
}
func lin_p3_to_xyz(r F64, g F64, b F64) (F64, F64, F64) {
M := [9]float64{
608311.0 / 1250200, 189793.0 / 714400, 198249.0 / 1000160,
35783.0 / 156275, 247089.0 / 357200, 198249.0 / 2500400,
0.0 / 1, 32229.0 / 714400, 5220557.0 / 5000800,
}
return multiplyMatrices(M, r, g, b)
}
func xyz_to_lin_p3(x F64, y F64, z F64) (F64, F64, F64) {
M := [9]float64{
446124.0 / 178915, -333277.0 / 357830, -72051.0 / 178915,
-14852.0 / 17905, 63121.0 / 35810, 423.0 / 17905,
11844.0 / 330415, -50337.0 / 660830, 316169.0 / 330415,
}
return multiplyMatrices(M, x, y, z)
}
func lin_prophoto(r F64, g F64, b F64) (F64, F64, F64) {
f := func(val F64) F64 {
const Et2 = 16.0 / 512
if abs := val.Abs(); abs.Value() <= Et2 {
return val.DivConst(16)
} else {
return abs.PowConst(1.8).WithSignFrom(val)
}
}
return f(r), f(g), f(b)
}
func gam_prophoto(r F64, g F64, b F64) (F64, F64, F64) {
f := func(val F64) F64 {
const Et = 1.0 / 512
if abs := val.Abs(); abs.Value() >= Et {
return abs.PowConst(1 / 1.8).WithSignFrom(val)
} else {
return val.MulConst(16)
}
}
return f(r), f(g), f(b)
}
func lin_prophoto_to_xyz(r F64, g F64, b F64) (F64, F64, F64) {
M := [9]float64{
0.7977604896723027, 0.13518583717574031, 0.0313493495815248,
0.2880711282292934, 0.7118432178101014, 0.00008565396060525902,
0.0, 0.0, 0.8251046025104601,
}
return multiplyMatrices(M, r, g, b)
}
func xyz_to_lin_prophoto(x F64, y F64, z F64) (F64, F64, F64) {
M := [9]float64{
1.3457989731028281, -0.25558010007997534, -0.05110628506753401,
-0.5446224939028347, 1.5082327413132781, 0.02053603239147973,
0.0, 0.0, 1.2119675456389454,
}
return multiplyMatrices(M, x, y, z)
}
func lin_a98rgb(r F64, g F64, b F64) (F64, F64, F64) {
f := func(val F64) F64 {
return val.Abs().PowConst(563.0 / 256).WithSignFrom(val)
}
return f(r), f(g), f(b)
}
func gam_a98rgb(r F64, g F64, b F64) (F64, F64, F64) {
f := func(val F64) F64 {
return val.Abs().PowConst(256.0 / 563).WithSignFrom(val)
}
return f(r), f(g), f(b)
}
func lin_a98rgb_to_xyz(r F64, g F64, b F64) (F64, F64, F64) {
M := [9]float64{
573536.0 / 994567, 263643.0 / 1420810, 187206.0 / 994567,
591459.0 / 1989134, 6239551.0 / 9945670, 374412.0 / 4972835,
53769.0 / 1989134, 351524.0 / 4972835, 4929758.0 / 4972835,
}
return multiplyMatrices(M, r, g, b)
}
func xyz_to_lin_a98rgb(x F64, y F64, z F64) (F64, F64, F64) {
M := [9]float64{
1829569.0 / 896150, -506331.0 / 896150, -308931.0 / 896150,
-851781.0 / 878810, 1648619.0 / 878810, 36519.0 / 878810,
16779.0 / 1248040, -147721.0 / 1248040, 1266979.0 / 1248040,
}
return multiplyMatrices(M, x, y, z)
}
func lin_2020(r F64, g F64, b F64) (F64, F64, F64) {
f := func(val F64) F64 {
const α = 1.09929682680944
const β = 0.018053968510807
if abs := val.Abs(); abs.Value() < β*4.5 {
return val.DivConst(4.5)
} else {
return abs.AddConst(α - 1).DivConst(α).PowConst(1 / 0.45).WithSignFrom(val)
}
}
return f(r), f(g), f(b)
}
func gam_2020(r F64, g F64, b F64) (F64, F64, F64) {
f := func(val F64) F64 {
const α = 1.09929682680944
const β = 0.018053968510807
if abs := val.Abs(); abs.Value() > β {
return abs.PowConst(0.45).MulConst(α).SubConst(α - 1).WithSignFrom(val)
} else {
return val.MulConst(4.5)
}
}
return f(r), f(g), f(b)
}
func lin_2020_to_xyz(r F64, g F64, b F64) (F64, F64, F64) {
var M = [9]float64{
63426534.0 / 99577255, 20160776.0 / 139408157, 47086771.0 / 278816314,
26158966.0 / 99577255, 472592308.0 / 697040785, 8267143.0 / 139408157,
0.0 / 1, 19567812.0 / 697040785, 295819943.0 / 278816314,
}
return multiplyMatrices(M, r, g, b)
}
func xyz_to_lin_2020(x F64, y F64, z F64) (F64, F64, F64) {
M := [9]float64{
30757411.0 / 17917100, -6372589.0 / 17917100, -4539589.0 / 17917100,
-19765991.0 / 29648200, 47925759.0 / 29648200, 467509.0 / 29648200,
792561.0 / 44930125, -1921689.0 / 44930125, 42328811.0 / 44930125,
}
return multiplyMatrices(M, x, y, z)
}
func d65_to_d50(x F64, y F64, z F64) (F64, F64, F64) {
M := [9]float64{
1.0479297925449969, 0.022946870601609652, -0.05019226628920524,
0.02962780877005599, 0.9904344267538799, -0.017073799063418826,
-0.009243040646204504, 0.015055191490298152, 0.7518742814281371,
}
return multiplyMatrices(M, x, y, z)
}
func d50_to_d65(x F64, y F64, z F64) (F64, F64, F64) {
M := [9]float64{
0.955473421488075, -0.02309845494876471, 0.06325924320057072,
-0.0283697093338637, 1.0099953980813041, 0.021041441191917323,
0.012314014864481998, -0.020507649298898964, 1.330365926242124,
}
return multiplyMatrices(M, x, y, z)
}
const d50_x = 0.3457 / 0.3585
const d50_z = (1.0 - 0.3457 - 0.3585) / 0.3585
func xyz_to_lab(x F64, y F64, z F64) (F64, F64, F64) {
const ε = 216.0 / 24389
const κ = 24389.0 / 27
x = x.DivConst(d50_x)
z = z.DivConst(d50_z)
var f0, f1, f2 F64
if x.Value() > ε {
f0 = x.Cbrt()
} else {
f0 = x.MulConst(κ).AddConst(16).DivConst(116)
}
if y.Value() > ε {
f1 = y.Cbrt()
} else {
f1 = y.MulConst(κ).AddConst(16).DivConst(116)
}
if z.Value() > ε {
f2 = z.Cbrt()
} else {
f2 = z.MulConst(κ).AddConst(16).DivConst(116)
}
return f1.MulConst(116).SubConst(16),
f0.Sub(f1).MulConst(500),
f1.Sub(f2).MulConst(200)
}
func lab_to_xyz(l F64, a F64, b F64) (x F64, y F64, z F64) {
const κ = 24389.0 / 27
const ε = 216.0 / 24389
f1 := l.AddConst(16).DivConst(116)
f0 := a.DivConst(500).Add(f1)
f2 := f1.Sub(b.DivConst(200))
f0_3 := f0.Cubed()
f2_3 := f2.Cubed()
if f0_3.Value() > ε {
x = f0_3
} else {
x = f0.MulConst(116).SubConst(16).DivConst(κ)
}
if l.Value() > κ*ε {
y = l.AddConst(16).DivConst(116)
y = y.Cubed()
} else {
y = l.DivConst(κ)
}
if f2_3.Value() > ε {
z = f2_3
} else {
z = f2.MulConst(116).SubConst(16).DivConst(κ)
}
return x.MulConst(d50_x), y, z.MulConst(d50_z)
}
func lab_to_lch(l F64, a F64, b F64) (F64, F64, F64) {
hue := b.Atan2(a).MulConst(180 / math.Pi)
if hue.Value() < 0 {
hue = hue.AddConst(360)
}
return l,
a.Squared().Add(b.Squared()).Sqrt(),
hue
}
func lch_to_lab(l F64, c F64, h F64) (F64, F64, F64) {
return l,
h.MulConst(math.Pi / 180).Cos().Mul(c),
h.MulConst(math.Pi / 180).Sin().Mul(c)
}
func xyz_to_oklab(x F64, y F64, z F64) (F64, F64, F64) {
XYZtoLMS := [9]float64{
0.8190224432164319, 0.3619062562801221, -0.12887378261216414,
0.0329836671980271, 0.9292868468965546, 0.03614466816999844,
0.048177199566046255, 0.26423952494422764, 0.6335478258136937,
}
LMStoOKLab := [9]float64{
0.2104542553, 0.7936177850, -0.0040720468,
1.9779984951, -2.4285922050, 0.4505937099,
0.0259040371, 0.7827717662, -0.8086757660,
}
l, m, s := multiplyMatrices(XYZtoLMS, x, y, z)
return multiplyMatrices(LMStoOKLab, l.Cbrt(), m.Cbrt(), s.Cbrt())
}
func oklab_to_xyz(l F64, a F64, b F64) (F64, F64, F64) {
LMStoXYZ := [9]float64{
1.2268798733741557, -0.5578149965554813, 0.28139105017721583,
-0.04057576262431372, 1.1122868293970594, -0.07171106666151701,
-0.07637294974672142, -0.4214933239627914, 1.5869240244272418,
}
OKLabtoLMS := [9]float64{
0.99999999845051981432, 0.39633779217376785678, 0.21580375806075880339,
1.0000000088817607767, -0.1055613423236563494, -0.063854174771705903402,
1.0000000546724109177, -0.089484182094965759684, -1.2914855378640917399,
}
l, m, s := multiplyMatrices(OKLabtoLMS, l, a, b)
return multiplyMatrices(LMStoXYZ, l.Cubed(), m.Cubed(), s.Cubed())
}
func oklab_to_oklch(l F64, a F64, b F64) (F64, F64, F64) {
return lab_to_lch(l, a, b)
}
func oklch_to_oklab(l F64, c F64, h F64) (F64, F64, F64) {
return lch_to_lab(l, c, h)
}
func multiplyMatrices(A [9]float64, b0 F64, b1 F64, b2 F64) (F64, F64, F64) {
return b0.MulConst(A[0]).Add(b1.MulConst(A[1])).Add(b2.MulConst(A[2])),
b0.MulConst(A[3]).Add(b1.MulConst(A[4])).Add(b2.MulConst(A[5])),
b0.MulConst(A[6]).Add(b1.MulConst(A[7])).Add(b2.MulConst(A[8]))
}
func delta_eok(L1 F64, a1 F64, b1 F64, L2 F64, a2 F64, b2 F64) F64 {
ΔL_sq := L1.Sub(L2).Squared()
Δa_sq := a1.Sub(a2).Squared()
Δb_sq := b1.Sub(b2).Squared()
return ΔL_sq.Add(Δa_sq).Add(Δb_sq).Sqrt()
}
func gamut_mapping_xyz_to_srgb(x F64, y F64, z F64) (F64, F64, F64) {
origin_l, origin_c, origin_h := oklab_to_oklch(xyz_to_oklab(x, y, z))
if origin_l.Value() >= 1 || origin_l.Value() <= 0 {
return origin_l, origin_l, origin_l
}
oklch_to_srgb := func(l F64, c F64, h F64) (F64, F64, F64) {
l, a, b := oklch_to_oklab(l, c, h)
x, y, z := oklab_to_xyz(l, a, b)
r, g, b := xyz_to_lin_srgb(x, y, z)
return gam_srgb(r, g, b)
}
srgb_to_oklab := func(r F64, g F64, b F64) (F64, F64, F64) {
r, g, b = lin_srgb(r, g, b)
x, y, z := lin_srgb_to_xyz(r, g, b)
return xyz_to_oklab(x, y, z)
}
inGamut := func(r F64, g F64, b F64) bool {
return r.Value() >= 0 && r.Value() <= 1 &&
g.Value() >= 0 && g.Value() <= 1 &&
b.Value() >= 0 && b.Value() <= 1
}
r, g, b := oklch_to_srgb(origin_l, origin_c, origin_h)
if inGamut(r, g, b) {
return r, g, b
}
const JND = 0.02
const epsilon = 0.0001
min := helpers.NewF64(0.0)
max := origin_c
clip := func(x F64) F64 {
if x.Value() < 0 {
return helpers.NewF64(0)
}
if x.Value() > 1 {
return helpers.NewF64(1)
}
return x
}
for max.Sub(min).Value() > epsilon {
chroma := min.Add(max).DivConst(2)
origin_c = chroma
r, g, b = oklch_to_srgb(origin_l, origin_c, origin_h)
if inGamut(r, g, b) {
min = chroma
continue
}
clipped_r, clipped_g, clipped_b := clip(r), clip(g), clip(b)
L1, a1, b1 := srgb_to_oklab(clipped_r, clipped_b, clipped_g)
L2, a2, b2 := srgb_to_oklab(r, g, b)
E := delta_eok(L1, a1, b1, L2, a2, b2)
if E.Value() < JND {
return clipped_r, clipped_g, clipped_b
}
max = chroma
}
return r, g, b
}
func hsl_to_rgb(hue F64, sat F64, light F64) (F64, F64, F64) {
hue = hue.DivConst(360)
hue = hue.Sub(hue.Floor())
hue = hue.MulConst(360)
sat = sat.DivConst(100)
light = light.DivConst(100)
f := func(n float64) F64 {
k := hue.DivConst(30).AddConst(n)
k = k.DivConst(12)
k = k.Sub(k.Floor())
k = k.MulConst(12)
a := helpers.Min2(light, light.Neg().AddConst(1)).Mul(sat)
return light.Sub(helpers.Max2(helpers.NewF64(-1), helpers.Min3(k.SubConst(3), k.Neg().AddConst(9), helpers.NewF64(1))).Mul(a))
}
return f(0), f(8), f(4)
}
func rgb_to_hsl(red F64, green F64, blue F64) (F64, F64, F64) {
max := helpers.Max3(red, green, blue)
min := helpers.Min3(red, green, blue)
hue, sat, light := helpers.NewF64(math.NaN()), helpers.NewF64(0.0), min.Add(max).DivConst(2)
d := max.Sub(min)
if d.Value() != 0 {
if div := helpers.Min2(light, light.Neg().AddConst(1)); div.Value() != 0 {
sat = max.Sub(light).Div(div)
}
switch max {
case red:
hue = green.Sub(blue).Div(d)
if green.Value() < blue.Value() {
hue = hue.AddConst(6)
}
case green:
hue = blue.Sub(red).Div(d).AddConst(2)
case blue:
hue = red.Sub(green).Div(d).AddConst(4)
}
hue = hue.MulConst(60)
}
return hue, sat.MulConst(100), light.MulConst(100)
}
func hwb_to_rgb(hue F64, white F64, black F64) (F64, F64, F64) {
white = white.DivConst(100)
black = black.DivConst(100)
if white.Add(black).Value() >= 1 {
gray := white.Div(white.Add(black))
return gray, gray, gray
}
delta := white.Add(black).Neg().AddConst(1)
r, g, b := hsl_to_rgb(hue, helpers.NewF64(100), helpers.NewF64(50))
r = delta.Mul(r).Add(white)
g = delta.Mul(g).Add(white)
b = delta.Mul(b).Add(white)
return r, g, b
}
func rgb_to_hwb(red F64, green F64, blue F64) (F64, F64, F64) {
h, _, _ := rgb_to_hsl(red, green, blue)
white := helpers.Min3(red, green, blue)
black := helpers.Max3(red, green, blue).Neg().AddConst(1)
return h, white.MulConst(100), black.MulConst(100)
}
func xyz_to_colorSpace(x F64, y F64, z F64, colorSpace colorSpace) (F64, F64, F64) {
switch colorSpace {
case colorSpace_a98_rgb:
return gam_a98rgb(xyz_to_lin_a98rgb(x, y, z))
case colorSpace_display_p3:
return gam_p3(xyz_to_lin_p3(x, y, z))
case colorSpace_hsl:
return rgb_to_hsl(gam_srgb(xyz_to_lin_srgb(x, y, z)))
case colorSpace_hwb:
return rgb_to_hwb(gam_srgb(xyz_to_lin_srgb(x, y, z)))
case colorSpace_lab:
return xyz_to_lab(d65_to_d50(x, y, z))
case colorSpace_lch:
return lab_to_lch(xyz_to_lab(d65_to_d50(x, y, z)))
case colorSpace_oklab:
return xyz_to_oklab(x, y, z)
case colorSpace_oklch:
return oklab_to_oklch(xyz_to_oklab(x, y, z))
case colorSpace_prophoto_rgb:
return gam_prophoto(xyz_to_lin_prophoto(d65_to_d50(x, y, z)))
case colorSpace_rec2020:
return gam_2020(xyz_to_lin_2020(x, y, z))
case colorSpace_srgb:
return gam_srgb(xyz_to_lin_srgb(x, y, z))
case colorSpace_srgb_linear:
return xyz_to_lin_srgb(x, y, z)
case colorSpace_xyz, colorSpace_xyz_d65:
return x, y, z
case colorSpace_xyz_d50:
return d65_to_d50(x, y, z)
default:
panic("Internal error")
}
}
func colorSpace_to_xyz(v0 F64, v1 F64, v2 F64, colorSpace colorSpace) (F64, F64, F64) {
switch colorSpace {
case colorSpace_a98_rgb:
return lin_a98rgb_to_xyz(lin_a98rgb(v0, v1, v2))
case colorSpace_display_p3:
return lin_p3_to_xyz(lin_p3(v0, v1, v2))
case colorSpace_hsl:
return lin_srgb_to_xyz(lin_srgb(hsl_to_rgb(v0, v1, v2)))
case colorSpace_hwb:
return lin_srgb_to_xyz(lin_srgb(hwb_to_rgb(v0, v1, v2)))
case colorSpace_lab:
return d50_to_d65(lab_to_xyz(v0, v1, v2))
case colorSpace_lch:
return d50_to_d65(lab_to_xyz(lch_to_lab(v0, v1, v2)))
case colorSpace_oklab:
return oklab_to_xyz(v0, v1, v2)
case colorSpace_oklch:
return oklab_to_xyz(oklch_to_oklab(v0, v1, v2))
case colorSpace_prophoto_rgb:
return d50_to_d65(lin_prophoto_to_xyz(lin_prophoto(v0, v1, v2)))
case colorSpace_rec2020:
return lin_2020_to_xyz(lin_2020(v0, v1, v2))
case colorSpace_srgb:
return lin_srgb_to_xyz(lin_srgb(v0, v1, v2))
case colorSpace_srgb_linear:
return lin_srgb_to_xyz(v0, v1, v2)
case colorSpace_xyz, colorSpace_xyz_d65:
return v0, v1, v2
case colorSpace_xyz_d50:
return d50_to_d65(v0, v1, v2)
default:
panic("Internal error")
}
}
@@ -0,0 +1,567 @@
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
}
@@ -0,0 +1,119 @@
package css_parser
import (
"strings"
"github.com/evanw/esbuild/internal/css_ast"
"github.com/evanw/esbuild/internal/css_lexer"
)
// Scan for animation names in the "animation" shorthand property
func (p *parser) processAnimationShorthand(tokens []css_ast.Token) {
type foundFlags struct {
timingFunction bool
iterationCount bool
direction bool
fillMode bool
playState bool
name bool
}
found := foundFlags{}
for i, t := range tokens {
switch t.Kind {
case css_lexer.TComma:
// Reset the flags when we encounter a comma
found = foundFlags{}
case css_lexer.TNumber:
if !found.iterationCount {
found.iterationCount = true
continue
}
case css_lexer.TIdent:
if !found.timingFunction {
switch strings.ToLower(t.Text) {
case "linear", "ease", "ease-in", "ease-out", "ease-in-out", "step-start", "step-end":
found.timingFunction = true
continue
}
}
if !found.iterationCount && strings.ToLower(t.Text) == "infinite" {
found.iterationCount = true
continue
}
if !found.direction {
switch strings.ToLower(t.Text) {
case "normal", "reverse", "alternate", "alternate-reverse":
found.direction = true
continue
}
}
if !found.fillMode {
switch strings.ToLower(t.Text) {
case "none", "forwards", "backwards", "both":
found.fillMode = true
continue
}
}
if !found.playState {
switch strings.ToLower(t.Text) {
case "running", "paused":
found.playState = true
continue
}
}
if !found.name {
p.handleSingleAnimationName(&tokens[i])
found.name = true
continue
}
case css_lexer.TString:
if !found.name {
p.handleSingleAnimationName(&tokens[i])
found.name = true
continue
}
}
}
}
func (p *parser) processAnimationName(tokens []css_ast.Token) {
for i, t := range tokens {
if t.Kind == css_lexer.TIdent || t.Kind == css_lexer.TString {
p.handleSingleAnimationName(&tokens[i])
}
}
}
func (p *parser) handleSingleAnimationName(token *css_ast.Token) {
// Do not transform CSS keywords into symbols because they have special
// meaning in declarations. For example, "animation-name: none" clears
// the animation name. It does not set it to the animation named "none".
// You need to use "animation-name: 'none'" to do that.
//
// Also don't transform strings containing CSS keywords into global symbols
// because global symbols are passed through without being renamed, which
// will print them as keywords. However, we still want to unconditionally
// transform strings into local symbols because local symbols are always
// renamed, so they will never be printed as keywords.
if (token.Kind == css_lexer.TIdent || (token.Kind == css_lexer.TString && !p.makeLocalSymbols)) && isInvalidAnimationName(token.Text) {
return
}
token.Kind = css_lexer.TSymbol
token.PayloadIndex = p.symbolForName(token.Loc, token.Text).Ref.InnerIndex
}
func isInvalidAnimationName(text string) bool {
lower := strings.ToLower(text)
return lower == "none" || cssWideAndReservedKeywords[lower]
}
@@ -0,0 +1,217 @@
package css_parser
import (
"github.com/evanw/esbuild/internal/css_ast"
"github.com/evanw/esbuild/internal/css_lexer"
"github.com/evanw/esbuild/internal/logger"
)
const (
borderRadiusTopLeft = iota
borderRadiusTopRight
borderRadiusBottomRight
borderRadiusBottomLeft
)
type borderRadiusCorner struct {
firstToken css_ast.Token
secondToken css_ast.Token
unitSafety unitSafetyTracker
ruleIndex uint32 // The index of the originating rule in the rules array
wasSingleRule bool // True if the originating rule was just for this side
}
type borderRadiusTracker struct {
corners [4]borderRadiusCorner
important bool // True if all active rules were flagged as "!important"
}
func (borderRadius *borderRadiusTracker) updateCorner(rules []css_ast.Rule, corner int, new borderRadiusCorner) {
if old := borderRadius.corners[corner]; old.firstToken.Kind != css_lexer.TEndOfFile &&
(!new.wasSingleRule || old.wasSingleRule) &&
old.unitSafety.status == unitSafe && new.unitSafety.status == unitSafe {
rules[old.ruleIndex] = css_ast.Rule{}
}
borderRadius.corners[corner] = new
}
func (borderRadius *borderRadiusTracker) mangleCorners(rules []css_ast.Rule, decl *css_ast.RDeclaration, minifyWhitespace bool) {
// Reset if we see a change in the "!important" flag
if borderRadius.important != decl.Important {
borderRadius.corners = [4]borderRadiusCorner{}
borderRadius.important = decl.Important
}
tokens := decl.Value
beforeSplit := len(tokens)
afterSplit := len(tokens)
// Search for the single slash if present
for i, t := range tokens {
if t.Kind == css_lexer.TDelimSlash {
if beforeSplit == len(tokens) {
beforeSplit = i
afterSplit = i + 1
} else {
// Multiple slashes are an error
borderRadius.corners = [4]borderRadiusCorner{}
return
}
}
}
// Use a single tracker for the whole rule
unitSafety := unitSafetyTracker{}
for _, t := range tokens[:beforeSplit] {
unitSafety.includeUnitOf(t)
}
for _, t := range tokens[afterSplit:] {
unitSafety.includeUnitOf(t)
}
firstRadii, firstRadiiOk := expandTokenQuad(tokens[:beforeSplit], "")
lastRadii, lastRadiiOk := expandTokenQuad(tokens[afterSplit:], "")
// Stop now if the pattern wasn't matched
if !firstRadiiOk || (beforeSplit < afterSplit && !lastRadiiOk) {
borderRadius.corners = [4]borderRadiusCorner{}
return
}
// Handle the first radii
for corner, t := range firstRadii {
if unitSafety.status == unitSafe {
t.TurnLengthIntoNumberIfZero()
}
borderRadius.updateCorner(rules, corner, borderRadiusCorner{
firstToken: t,
secondToken: t,
unitSafety: unitSafety,
ruleIndex: uint32(len(rules) - 1),
})
}
// Handle the last radii
if lastRadiiOk {
for corner, t := range lastRadii {
if unitSafety.status == unitSafe {
t.TurnLengthIntoNumberIfZero()
}
borderRadius.corners[corner].secondToken = t
}
}
// Success
borderRadius.compactRules(rules, decl.KeyRange, minifyWhitespace)
}
func (borderRadius *borderRadiusTracker) mangleCorner(rules []css_ast.Rule, decl *css_ast.RDeclaration, minifyWhitespace bool, corner int) {
// Reset if we see a change in the "!important" flag
if borderRadius.important != decl.Important {
borderRadius.corners = [4]borderRadiusCorner{}
borderRadius.important = decl.Important
}
if tokens := decl.Value; (len(tokens) == 1 && tokens[0].Kind.IsNumeric()) ||
(len(tokens) == 2 && tokens[0].Kind.IsNumeric() && tokens[1].Kind.IsNumeric()) {
firstToken := tokens[0]
secondToken := firstToken
if len(tokens) == 2 {
secondToken = tokens[1]
}
// Check to see if these units are safe to use in every browser
unitSafety := unitSafetyTracker{}
unitSafety.includeUnitOf(firstToken)
unitSafety.includeUnitOf(secondToken)
// Only collapse "0unit" into "0" if the unit is safe
if unitSafety.status == unitSafe && firstToken.TurnLengthIntoNumberIfZero() {
tokens[0] = firstToken
}
if len(tokens) == 2 {
if unitSafety.status == unitSafe && secondToken.TurnLengthIntoNumberIfZero() {
tokens[1] = secondToken
}
// If both tokens are equal, merge them into one
if firstToken.EqualIgnoringWhitespace(secondToken) {
tokens[0].Whitespace &= ^css_ast.WhitespaceAfter
decl.Value = tokens[:1]
}
}
borderRadius.updateCorner(rules, corner, borderRadiusCorner{
firstToken: firstToken,
secondToken: secondToken,
unitSafety: unitSafety,
ruleIndex: uint32(len(rules) - 1),
wasSingleRule: true,
})
borderRadius.compactRules(rules, decl.KeyRange, minifyWhitespace)
} else {
borderRadius.corners = [4]borderRadiusCorner{}
}
}
func (borderRadius *borderRadiusTracker) compactRules(rules []css_ast.Rule, keyRange logger.Range, minifyWhitespace bool) {
// All tokens must be present
if eof := css_lexer.TEndOfFile; borderRadius.corners[0].firstToken.Kind == eof || borderRadius.corners[1].firstToken.Kind == eof ||
borderRadius.corners[2].firstToken.Kind == eof || borderRadius.corners[3].firstToken.Kind == eof {
return
}
// All tokens must have the same unit
for _, side := range borderRadius.corners[1:] {
if !side.unitSafety.isSafeWith(borderRadius.corners[0].unitSafety) {
return
}
}
// Generate the most minimal representation
tokens := compactTokenQuad(
borderRadius.corners[0].firstToken,
borderRadius.corners[1].firstToken,
borderRadius.corners[2].firstToken,
borderRadius.corners[3].firstToken,
minifyWhitespace,
)
secondTokens := compactTokenQuad(
borderRadius.corners[0].secondToken,
borderRadius.corners[1].secondToken,
borderRadius.corners[2].secondToken,
borderRadius.corners[3].secondToken,
minifyWhitespace,
)
if !css_ast.TokensEqualIgnoringWhitespace(tokens, secondTokens) {
var whitespace css_ast.WhitespaceFlags
if !minifyWhitespace {
whitespace = css_ast.WhitespaceBefore | css_ast.WhitespaceAfter
}
tokens = append(tokens, css_ast.Token{
Loc: tokens[len(tokens)-1].Loc,
Kind: css_lexer.TDelimSlash,
Text: "/",
Whitespace: whitespace,
})
tokens = append(tokens, secondTokens...)
}
// Remove all of the existing declarations
var minLoc logger.Loc
for i, corner := range borderRadius.corners {
if loc := rules[corner.ruleIndex].Loc; i == 0 || loc.Start < minLoc.Start {
minLoc = loc
}
rules[corner.ruleIndex] = css_ast.Rule{}
}
// Insert the combined declaration where the last rule was
rules[borderRadius.corners[3].ruleIndex] = css_ast.Rule{Loc: minLoc, Data: &css_ast.RDeclaration{
Key: css_ast.DBorderRadius,
KeyText: "border-radius",
Value: tokens,
KeyRange: keyRange,
Important: borderRadius.important,
}}
}
@@ -0,0 +1,206 @@
package css_parser
import (
"strings"
"github.com/evanw/esbuild/internal/css_ast"
"github.com/evanw/esbuild/internal/css_lexer"
"github.com/evanw/esbuild/internal/logger"
)
const (
boxTop = iota
boxRight
boxBottom
boxLeft
)
type boxSide struct {
token css_ast.Token
unitSafety unitSafetyTracker
ruleIndex uint32 // The index of the originating rule in the rules array
wasSingleRule bool // True if the originating rule was just for this side
}
type boxTracker struct {
keyText string
sides [4]boxSide
allowAuto bool // If true, allow the "auto" keyword
important bool // True if all active rules were flagged as "!important"
key css_ast.D
}
type unitSafetyStatus uint8
const (
unitSafe unitSafetyStatus = iota // "margin: 0 1px 2cm 3%;"
unitUnsafeSingle // "margin: 0 1vw 2vw 3vw;"
unitUnsafeMixed // "margin: 0 1vw 2vh 3ch;"
)
// We can only compact rules together if they have the same unit safety level.
// We want to avoid a situation where the browser treats some of the original
// rules as valid and others as invalid.
//
// Safe:
// top: 1px; left: 0; bottom: 1px; right: 0;
// top: 1Q; left: 2Q; bottom: 3Q; right: 4Q;
//
// Unsafe:
// top: 1vh; left: 2vw; bottom: 3vh; right: 4vw;
// top: 1Q; left: 2Q; bottom: 3Q; right: 0;
// inset: 1Q 0 0 0; top: 0;
type unitSafetyTracker struct {
unit string
status unitSafetyStatus
}
func (a unitSafetyTracker) isSafeWith(b unitSafetyTracker) bool {
return a.status == b.status && a.status != unitUnsafeMixed && (a.status != unitUnsafeSingle || a.unit == b.unit)
}
func (t *unitSafetyTracker) includeUnitOf(token css_ast.Token) {
switch token.Kind {
case css_lexer.TNumber:
if token.Text == "0" {
return
}
case css_lexer.TPercentage:
return
case css_lexer.TDimension:
if token.DimensionUnitIsSafeLength() {
return
} else if unit := token.DimensionUnit(); t.status == unitSafe {
t.status = unitUnsafeSingle
t.unit = unit
return
} else if t.status == unitUnsafeSingle && t.unit == unit {
return
}
}
t.status = unitUnsafeMixed
}
func (box *boxTracker) updateSide(rules []css_ast.Rule, side int, new boxSide) {
if old := box.sides[side]; old.token.Kind != css_lexer.TEndOfFile &&
(!new.wasSingleRule || old.wasSingleRule) &&
old.unitSafety.status == unitSafe && new.unitSafety.status == unitSafe {
rules[old.ruleIndex] = css_ast.Rule{}
}
box.sides[side] = new
}
func (box *boxTracker) mangleSides(rules []css_ast.Rule, decl *css_ast.RDeclaration, minifyWhitespace bool) {
// Reset if we see a change in the "!important" flag
if box.important != decl.Important {
box.sides = [4]boxSide{}
box.important = decl.Important
}
allowedIdent := ""
if box.allowAuto {
allowedIdent = "auto"
}
if quad, ok := expandTokenQuad(decl.Value, allowedIdent); ok {
// Use a single tracker for the whole rule
unitSafety := unitSafetyTracker{}
for _, t := range quad {
if !box.allowAuto || t.Kind.IsNumeric() {
unitSafety.includeUnitOf(t)
}
}
for side, t := range quad {
if unitSafety.status == unitSafe {
t.TurnLengthIntoNumberIfZero()
}
box.updateSide(rules, side, boxSide{
token: t,
ruleIndex: uint32(len(rules) - 1),
unitSafety: unitSafety,
})
}
box.compactRules(rules, decl.KeyRange, minifyWhitespace)
} else {
box.sides = [4]boxSide{}
}
}
func (box *boxTracker) mangleSide(rules []css_ast.Rule, decl *css_ast.RDeclaration, minifyWhitespace bool, side int) {
// Reset if we see a change in the "!important" flag
if box.important != decl.Important {
box.sides = [4]boxSide{}
box.important = decl.Important
}
if tokens := decl.Value; len(tokens) == 1 {
if t := tokens[0]; t.Kind.IsNumeric() || (t.Kind == css_lexer.TIdent && box.allowAuto && strings.EqualFold(t.Text, "auto")) {
unitSafety := unitSafetyTracker{}
if !box.allowAuto || t.Kind.IsNumeric() {
unitSafety.includeUnitOf(t)
}
if unitSafety.status == unitSafe && t.TurnLengthIntoNumberIfZero() {
tokens[0] = t
}
box.updateSide(rules, side, boxSide{
token: t,
ruleIndex: uint32(len(rules) - 1),
wasSingleRule: true,
unitSafety: unitSafety,
})
box.compactRules(rules, decl.KeyRange, minifyWhitespace)
return
}
}
box.sides = [4]boxSide{}
}
func (box *boxTracker) compactRules(rules []css_ast.Rule, keyRange logger.Range, minifyWhitespace bool) {
// Don't compact if the shorthand form is unsupported
if box.key == css_ast.DUnknown {
return
}
// All tokens must be present
if eof := css_lexer.TEndOfFile; box.sides[0].token.Kind == eof || box.sides[1].token.Kind == eof ||
box.sides[2].token.Kind == eof || box.sides[3].token.Kind == eof {
return
}
// All tokens must have the same unit
for _, side := range box.sides[1:] {
if !side.unitSafety.isSafeWith(box.sides[0].unitSafety) {
return
}
}
// Generate the most minimal representation
tokens := compactTokenQuad(
box.sides[0].token,
box.sides[1].token,
box.sides[2].token,
box.sides[3].token,
minifyWhitespace,
)
// Remove all of the existing declarations
var minLoc logger.Loc
for i, side := range box.sides {
if loc := rules[side.ruleIndex].Loc; i == 0 || loc.Start < minLoc.Start {
minLoc = loc
}
rules[side.ruleIndex] = css_ast.Rule{}
}
// Insert the combined declaration where the last rule was
rules[box.sides[3].ruleIndex] = css_ast.Rule{Loc: minLoc, Data: &css_ast.RDeclaration{
Key: box.key,
KeyText: box.keyText,
Value: tokens,
KeyRange: keyRange,
Important: box.important,
}}
}
@@ -0,0 +1,106 @@
package css_parser
import (
"strings"
"github.com/evanw/esbuild/internal/css_ast"
"github.com/evanw/esbuild/internal/css_lexer"
)
func (p *parser) lowerAndMangleBoxShadow(tokens []css_ast.Token, wouldClipColor *bool) []css_ast.Token {
insetCount := 0
colorCount := 0
numbersBegin := 0
numbersCount := 0
numbersDone := false
foundUnexpectedToken := false
for i, t := range tokens {
if t.Kind == css_lexer.TNumber || t.Kind == css_lexer.TDimension {
if numbersDone {
// Track if we found a non-number in between two numbers
foundUnexpectedToken = true
}
if p.options.minifySyntax && t.TurnLengthIntoNumberIfZero() {
// "0px" => "0"
tokens[i] = t
}
if numbersCount == 0 {
// Track the index of the first number
numbersBegin = i
}
numbersCount++
} else {
if numbersCount != 0 {
// Track when we find a non-number after a number
numbersDone = true
}
if looksLikeColor(t) {
colorCount++
tokens[i] = p.lowerAndMinifyColor(t, wouldClipColor)
} else if t.Kind == css_lexer.TIdent && strings.EqualFold(t.Text, "inset") {
insetCount++
} else {
// Track if we found a token other than a number, a color, or "inset"
foundUnexpectedToken = true
}
}
}
// If everything looks like a valid rule, trim trailing zeros off the numbers.
// There are three valid configurations of numbers:
//
// offset-x | offset-y
// offset-x | offset-y | blur-radius
// offset-x | offset-y | blur-radius | spread-radius
//
// If omitted, blur-radius and spread-radius are implied to be zero.
if p.options.minifySyntax && insetCount <= 1 && colorCount <= 1 && numbersCount > 2 && numbersCount <= 4 && !foundUnexpectedToken {
numbersEnd := numbersBegin + numbersCount
for numbersCount > 2 && tokens[numbersBegin+numbersCount-1].IsZero() {
numbersCount--
}
tokens = append(tokens[:numbersBegin+numbersCount], tokens[numbersEnd:]...)
}
// Set the whitespace flags
for i := range tokens {
var whitespace css_ast.WhitespaceFlags
if i > 0 || !p.options.minifyWhitespace {
whitespace |= css_ast.WhitespaceBefore
}
if i+1 < len(tokens) {
whitespace |= css_ast.WhitespaceAfter
}
tokens[i].Whitespace = whitespace
}
return tokens
}
func (p *parser) lowerAndMangleBoxShadows(tokens []css_ast.Token, wouldClipColor *bool) []css_ast.Token {
n := len(tokens)
end := 0
i := 0
for i < n {
// Find the comma or the end of the token list
comma := i
for comma < n && tokens[comma].Kind != css_lexer.TComma {
comma++
}
// Mangle this individual shadow
end += copy(tokens[end:], p.lowerAndMangleBoxShadow(tokens[i:comma], wouldClipColor))
// Skip over the comma
if comma < n {
tokens[end] = tokens[comma]
end++
comma++
}
i = comma
}
return tokens[:end]
}
@@ -0,0 +1,938 @@
package css_parser
import (
"fmt"
"math"
"strconv"
"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/helpers"
)
// These names are shorter than their hex codes
var shortColorName = map[uint32]string{
0x000080ff: "navy",
0x008000ff: "green",
0x008080ff: "teal",
0x4b0082ff: "indigo",
0x800000ff: "maroon",
0x800080ff: "purple",
0x808000ff: "olive",
0x808080ff: "gray",
0xa0522dff: "sienna",
0xa52a2aff: "brown",
0xc0c0c0ff: "silver",
0xcd853fff: "peru",
0xd2b48cff: "tan",
0xda70d6ff: "orchid",
0xdda0ddff: "plum",
0xee82eeff: "violet",
0xf0e68cff: "khaki",
0xf0ffffff: "azure",
0xf5deb3ff: "wheat",
0xf5f5dcff: "beige",
0xfa8072ff: "salmon",
0xfaf0e6ff: "linen",
0xff0000ff: "red",
0xff6347ff: "tomato",
0xff7f50ff: "coral",
0xffa500ff: "orange",
0xffc0cbff: "pink",
0xffd700ff: "gold",
0xffe4c4ff: "bisque",
0xfffafaff: "snow",
0xfffff0ff: "ivory",
}
var colorNameToHex = map[string]uint32{
"black": 0x000000ff,
"silver": 0xc0c0c0ff,
"gray": 0x808080ff,
"white": 0xffffffff,
"maroon": 0x800000ff,
"red": 0xff0000ff,
"purple": 0x800080ff,
"fuchsia": 0xff00ffff,
"green": 0x008000ff,
"lime": 0x00ff00ff,
"olive": 0x808000ff,
"yellow": 0xffff00ff,
"navy": 0x000080ff,
"blue": 0x0000ffff,
"teal": 0x008080ff,
"aqua": 0x00ffffff,
"orange": 0xffa500ff,
"aliceblue": 0xf0f8ffff,
"antiquewhite": 0xfaebd7ff,
"aquamarine": 0x7fffd4ff,
"azure": 0xf0ffffff,
"beige": 0xf5f5dcff,
"bisque": 0xffe4c4ff,
"blanchedalmond": 0xffebcdff,
"blueviolet": 0x8a2be2ff,
"brown": 0xa52a2aff,
"burlywood": 0xdeb887ff,
"cadetblue": 0x5f9ea0ff,
"chartreuse": 0x7fff00ff,
"chocolate": 0xd2691eff,
"coral": 0xff7f50ff,
"cornflowerblue": 0x6495edff,
"cornsilk": 0xfff8dcff,
"crimson": 0xdc143cff,
"cyan": 0x00ffffff,
"darkblue": 0x00008bff,
"darkcyan": 0x008b8bff,
"darkgoldenrod": 0xb8860bff,
"darkgray": 0xa9a9a9ff,
"darkgreen": 0x006400ff,
"darkgrey": 0xa9a9a9ff,
"darkkhaki": 0xbdb76bff,
"darkmagenta": 0x8b008bff,
"darkolivegreen": 0x556b2fff,
"darkorange": 0xff8c00ff,
"darkorchid": 0x9932ccff,
"darkred": 0x8b0000ff,
"darksalmon": 0xe9967aff,
"darkseagreen": 0x8fbc8fff,
"darkslateblue": 0x483d8bff,
"darkslategray": 0x2f4f4fff,
"darkslategrey": 0x2f4f4fff,
"darkturquoise": 0x00ced1ff,
"darkviolet": 0x9400d3ff,
"deeppink": 0xff1493ff,
"deepskyblue": 0x00bfffff,
"dimgray": 0x696969ff,
"dimgrey": 0x696969ff,
"dodgerblue": 0x1e90ffff,
"firebrick": 0xb22222ff,
"floralwhite": 0xfffaf0ff,
"forestgreen": 0x228b22ff,
"gainsboro": 0xdcdcdcff,
"ghostwhite": 0xf8f8ffff,
"gold": 0xffd700ff,
"goldenrod": 0xdaa520ff,
"greenyellow": 0xadff2fff,
"grey": 0x808080ff,
"honeydew": 0xf0fff0ff,
"hotpink": 0xff69b4ff,
"indianred": 0xcd5c5cff,
"indigo": 0x4b0082ff,
"ivory": 0xfffff0ff,
"khaki": 0xf0e68cff,
"lavender": 0xe6e6faff,
"lavenderblush": 0xfff0f5ff,
"lawngreen": 0x7cfc00ff,
"lemonchiffon": 0xfffacdff,
"lightblue": 0xadd8e6ff,
"lightcoral": 0xf08080ff,
"lightcyan": 0xe0ffffff,
"lightgoldenrodyellow": 0xfafad2ff,
"lightgray": 0xd3d3d3ff,
"lightgreen": 0x90ee90ff,
"lightgrey": 0xd3d3d3ff,
"lightpink": 0xffb6c1ff,
"lightsalmon": 0xffa07aff,
"lightseagreen": 0x20b2aaff,
"lightskyblue": 0x87cefaff,
"lightslategray": 0x778899ff,
"lightslategrey": 0x778899ff,
"lightsteelblue": 0xb0c4deff,
"lightyellow": 0xffffe0ff,
"limegreen": 0x32cd32ff,
"linen": 0xfaf0e6ff,
"magenta": 0xff00ffff,
"mediumaquamarine": 0x66cdaaff,
"mediumblue": 0x0000cdff,
"mediumorchid": 0xba55d3ff,
"mediumpurple": 0x9370dbff,
"mediumseagreen": 0x3cb371ff,
"mediumslateblue": 0x7b68eeff,
"mediumspringgreen": 0x00fa9aff,
"mediumturquoise": 0x48d1ccff,
"mediumvioletred": 0xc71585ff,
"midnightblue": 0x191970ff,
"mintcream": 0xf5fffaff,
"mistyrose": 0xffe4e1ff,
"moccasin": 0xffe4b5ff,
"navajowhite": 0xffdeadff,
"oldlace": 0xfdf5e6ff,
"olivedrab": 0x6b8e23ff,
"orangered": 0xff4500ff,
"orchid": 0xda70d6ff,
"palegoldenrod": 0xeee8aaff,
"palegreen": 0x98fb98ff,
"paleturquoise": 0xafeeeeff,
"palevioletred": 0xdb7093ff,
"papayawhip": 0xffefd5ff,
"peachpuff": 0xffdab9ff,
"peru": 0xcd853fff,
"pink": 0xffc0cbff,
"plum": 0xdda0ddff,
"powderblue": 0xb0e0e6ff,
"rosybrown": 0xbc8f8fff,
"royalblue": 0x4169e1ff,
"saddlebrown": 0x8b4513ff,
"salmon": 0xfa8072ff,
"sandybrown": 0xf4a460ff,
"seagreen": 0x2e8b57ff,
"seashell": 0xfff5eeff,
"sienna": 0xa0522dff,
"skyblue": 0x87ceebff,
"slateblue": 0x6a5acdff,
"slategray": 0x708090ff,
"slategrey": 0x708090ff,
"snow": 0xfffafaff,
"springgreen": 0x00ff7fff,
"steelblue": 0x4682b4ff,
"tan": 0xd2b48cff,
"thistle": 0xd8bfd8ff,
"tomato": 0xff6347ff,
"turquoise": 0x40e0d0ff,
"violet": 0xee82eeff,
"wheat": 0xf5deb3ff,
"whitesmoke": 0xf5f5f5ff,
"yellowgreen": 0x9acd32ff,
"rebeccapurple": 0x663399ff,
}
func parseHex(text string) (uint32, bool) {
hex := uint32(0)
for _, c := range text {
hex <<= 4
switch {
case c >= '0' && c <= '9':
hex |= uint32(c) - '0'
case c >= 'a' && c <= 'f':
hex |= uint32(c) - ('a' - 10)
case c >= 'A' && c <= 'F':
hex |= uint32(c) - ('A' - 10)
default:
return 0, false
}
}
return hex, true
}
// 0xAABBCCDD => 0xABCD
func compactHex(v uint32) uint32 {
return ((v & 0x0FF00000) >> 12) | ((v & 0x00000FF0) >> 4)
}
// 0xABCD => 0xAABBCCDD
func expandHex(v uint32) uint32 {
return ((v & 0xF000) << 16) | ((v & 0xFF00) << 12) | ((v & 0x0FF0) << 8) | ((v & 0x00FF) << 4) | (v & 0x000F)
}
func hexR(v uint32) int { return int(v >> 24) }
func hexG(v uint32) int { return int((v >> 16) & 255) }
func hexB(v uint32) int { return int((v >> 8) & 255) }
func hexA(v uint32) int { return int(v & 255) }
func floatToStringForColor(a float64) string {
text := fmt.Sprintf("%.03f", a)
for text[len(text)-1] == '0' {
text = text[:len(text)-1]
}
if text[len(text)-1] == '.' {
text = text[:len(text)-1]
}
return text
}
func degreesForAngle(token css_ast.Token) (float64, bool) {
switch token.Kind {
case css_lexer.TNumber:
if value, err := strconv.ParseFloat(token.Text, 64); err == nil {
return value, true
}
case css_lexer.TDimension:
if value, err := strconv.ParseFloat(token.DimensionValue(), 64); err == nil {
switch token.DimensionUnit() {
case "deg":
return value, true
case "grad":
return value * (360.0 / 400.0), true
case "rad":
return value * (180.0 / math.Pi), true
case "turn":
return value * 360.0, true
}
}
}
return 0, false
}
func lowerAlphaPercentageToNumber(token css_ast.Token) css_ast.Token {
if token.Kind == css_lexer.TPercentage {
if value, err := strconv.ParseFloat(token.Text[:len(token.Text)-1], 64); err == nil {
token.Kind = css_lexer.TNumber
token.Text = floatToStringForColor(value / 100.0)
}
}
return token
}
// Convert newer color syntax to older color syntax for older browsers
func (p *parser) lowerAndMinifyColor(token css_ast.Token, wouldClipColor *bool) css_ast.Token {
text := token.Text
switch token.Kind {
case css_lexer.THash:
if p.options.unsupportedCSSFeatures.Has(compat.HexRGBA) {
switch len(text) {
case 4:
// "#1234" => "rgba(1, 2, 3, 0.004)"
if hex, ok := parseHex(text); ok {
hex = expandHex(hex)
return p.tryToGenerateColor(token, parsedColor{hex: hex}, nil)
}
case 8:
// "#12345678" => "rgba(18, 52, 86, 0.47)"
if hex, ok := parseHex(text); ok {
return p.tryToGenerateColor(token, parsedColor{hex: hex}, nil)
}
}
}
case css_lexer.TIdent:
if p.options.unsupportedCSSFeatures.Has(compat.RebeccaPurple) && strings.EqualFold(text, "rebeccapurple") {
token.Kind = css_lexer.THash
token.Text = "663399"
}
case css_lexer.TFunction:
switch strings.ToLower(text) {
case "rgb", "rgba", "hsl", "hsla":
if p.options.unsupportedCSSFeatures.Has(compat.Modern_RGB_HSL) {
args := *token.Children
removeAlpha := false
addAlpha := false
// "hsl(1deg, 2%, 3%)" => "hsl(1, 2%, 3%)"
if (text == "hsl" || text == "hsla") && len(args) > 0 {
if degrees, ok := degreesForAngle(args[0]); ok {
args[0].Kind = css_lexer.TNumber
args[0].Text = floatToStringForColor(degrees)
}
}
// These check for "IsNumeric" to reject "var()" since a single "var()"
// can substitute for multiple tokens and that messes up pattern matching
switch len(args) {
case 3:
// "rgba(1 2 3)" => "rgb(1, 2, 3)"
// "hsla(1 2% 3%)" => "hsl(1, 2%, 3%)"
if args[0].Kind.IsNumeric() && args[1].Kind.IsNumeric() && args[2].Kind.IsNumeric() {
removeAlpha = true
args[0].Whitespace = 0
args[1].Whitespace = 0
commaToken := p.commaToken(token.Loc)
token.Children = &[]css_ast.Token{
args[0], commaToken,
args[1], commaToken,
args[2],
}
}
case 5:
// "rgba(1, 2, 3)" => "rgb(1, 2, 3)"
// "hsla(1, 2%, 3%)" => "hsl(1%, 2%, 3%)"
if args[0].Kind.IsNumeric() && args[1].Kind == css_lexer.TComma &&
args[2].Kind.IsNumeric() && args[3].Kind == css_lexer.TComma &&
args[4].Kind.IsNumeric() {
removeAlpha = true
break
}
// "rgb(1 2 3 / 4%)" => "rgba(1, 2, 3, 0.04)"
// "hsl(1 2% 3% / 4%)" => "hsla(1, 2%, 3%, 0.04)"
if args[0].Kind.IsNumeric() && args[1].Kind.IsNumeric() && args[2].Kind.IsNumeric() &&
args[3].Kind == css_lexer.TDelimSlash && args[4].Kind.IsNumeric() {
addAlpha = true
args[0].Whitespace = 0
args[1].Whitespace = 0
args[2].Whitespace = 0
commaToken := p.commaToken(token.Loc)
token.Children = &[]css_ast.Token{
args[0], commaToken,
args[1], commaToken,
args[2], commaToken,
lowerAlphaPercentageToNumber(args[4]),
}
}
case 7:
// "rgb(1%, 2%, 3%, 4%)" => "rgba(1%, 2%, 3%, 0.04)"
// "hsl(1, 2%, 3%, 4%)" => "hsla(1, 2%, 3%, 0.04)"
if args[0].Kind.IsNumeric() && args[1].Kind == css_lexer.TComma &&
args[2].Kind.IsNumeric() && args[3].Kind == css_lexer.TComma &&
args[4].Kind.IsNumeric() && args[5].Kind == css_lexer.TComma &&
args[6].Kind.IsNumeric() {
addAlpha = true
args[6] = lowerAlphaPercentageToNumber(args[6])
}
}
if removeAlpha {
if strings.EqualFold(text, "rgba") {
token.Text = "rgb"
} else if strings.EqualFold(text, "hsla") {
token.Text = "hsl"
}
} else if addAlpha {
if strings.EqualFold(text, "rgb") {
token.Text = "rgba"
} else if strings.EqualFold(text, "hsl") {
token.Text = "hsla"
}
}
}
case "hwb":
if p.options.unsupportedCSSFeatures.Has(compat.HWB) {
if color, ok := parseColor(token); ok {
return p.tryToGenerateColor(token, color, wouldClipColor)
}
}
case "color", "lab", "lch", "oklab", "oklch":
if p.options.unsupportedCSSFeatures.Has(compat.ColorFunctions) {
if color, ok := parseColor(token); ok {
return p.tryToGenerateColor(token, color, wouldClipColor)
}
}
}
}
// When minifying, try to parse the color and print it back out. This minifies
// the color because we always print it out using the shortest encoding.
if p.options.minifySyntax {
if hex, ok := parseColor(token); ok {
token = p.tryToGenerateColor(token, hex, wouldClipColor)
}
}
return token
}
type parsedColor struct {
x, y, z F64 // color if hasColorSpace == true
hex uint32 // color and alpha if hasColorSpace == false, alpha if hasColorSpace == true
hasColorSpace 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
switch token.Kind {
case css_lexer.TIdent:
if hex, ok := colorNameToHex[strings.ToLower(text)]; ok {
return parsedColor{hex: hex}, true
}
case css_lexer.THash:
switch len(text) {
case 3:
// "#123"
if hex, ok := parseHex(text); ok {
return parsedColor{hex: (expandHex(hex) << 8) | 0xFF}, true
}
case 4:
// "#1234"
if hex, ok := parseHex(text); ok {
return parsedColor{hex: expandHex(hex)}, true
}
case 6:
// "#112233"
if hex, ok := parseHex(text); ok {
return parsedColor{hex: (hex << 8) | 0xFF}, true
}
case 8:
// "#11223344"
if hex, ok := parseHex(text); ok {
return parsedColor{hex: hex}, true
}
}
case css_lexer.TFunction:
lowerText := strings.ToLower(text)
switch lowerText {
case "rgb", "rgba":
args := *token.Children
var r, g, b, a css_ast.Token
switch len(args) {
case 3:
// "rgb(1 2 3)"
r, g, b = args[0], args[1], args[2]
case 5:
// "rgba(1, 2, 3)"
if args[1].Kind == css_lexer.TComma && args[3].Kind == css_lexer.TComma {
r, g, b = args[0], args[2], args[4]
break
}
// "rgb(1 2 3 / 4%)"
if args[3].Kind == css_lexer.TDelimSlash {
r, g, b, a = args[0], args[1], args[2], args[4]
}
case 7:
// "rgb(1%, 2%, 3%, 4%)"
if args[1].Kind == css_lexer.TComma && args[3].Kind == css_lexer.TComma && args[5].Kind == css_lexer.TComma {
r, g, b, a = args[0], args[2], args[4], args[6]
}
}
if r, ok := parseColorByte(r, 1); ok {
if g, ok := parseColorByte(g, 1); ok {
if b, ok := parseColorByte(b, 1); ok {
if a, ok := parseAlphaByte(a); ok {
return parsedColor{hex: (r << 24) | (g << 16) | (b << 8) | a}, true
}
}
}
}
case "hsl", "hsla":
args := *token.Children
var h, s, l, a css_ast.Token
switch len(args) {
case 3:
// "hsl(1 2 3)"
h, s, l = args[0], args[1], args[2]
case 5:
// "hsla(1, 2, 3)"
if args[1].Kind == css_lexer.TComma && args[3].Kind == css_lexer.TComma {
h, s, l = args[0], args[2], args[4]
break
}
// "hsl(1 2 3 / 4%)"
if args[3].Kind == css_lexer.TDelimSlash {
h, s, l, a = args[0], args[1], args[2], args[4]
}
case 7:
// "hsl(1%, 2%, 3%, 4%)"
if args[1].Kind == css_lexer.TComma && args[3].Kind == css_lexer.TComma && args[5].Kind == css_lexer.TComma {
h, s, l, a = args[0], args[2], args[4], args[6]
}
}
// HSL => RGB
if h, ok := degreesForAngle(h); ok {
if s, ok := s.ClampedFractionForPercentage(); ok {
if l, ok := l.ClampedFractionForPercentage(); ok {
if a, ok := parseAlphaByte(a); ok {
r, g, b := hslToRgb(helpers.NewF64(h), helpers.NewF64(s), helpers.NewF64(l))
return parsedColor{hex: packRGBA(r, g, b, a)}, true
}
}
}
}
case "hwb":
args := *token.Children
var h, s, l, a css_ast.Token
switch len(args) {
case 3:
// "hwb(1 2 3)"
h, s, l = args[0], args[1], args[2]
case 5:
// "hwb(1 2 3 / 4%)"
if args[3].Kind == css_lexer.TDelimSlash {
h, s, l, a = args[0], args[1], args[2], args[4]
}
}
// HWB => RGB
if h, ok := degreesForAngle(h); ok {
if white, ok := s.ClampedFractionForPercentage(); ok {
if black, ok := l.ClampedFractionForPercentage(); ok {
if a, ok := parseAlphaByte(a); ok {
r, g, b := hwbToRgb(helpers.NewF64(h), helpers.NewF64(white), helpers.NewF64(black))
return parsedColor{hex: packRGBA(r, g, b, a)}, true
}
}
}
}
case "color":
args := *token.Children
var colorSpace, alpha css_ast.Token
switch len(args) {
case 4:
// "color(xyz 1 2 3)"
colorSpace = args[0]
case 6:
// "color(xyz 1 2 3 / 50%)"
if args[4].Kind == css_lexer.TDelimSlash {
colorSpace, alpha = args[0], args[5]
}
}
if colorSpace.Kind == css_lexer.TIdent {
if v0, ok := args[1].NumberOrFractionForPercentage(1, 0); ok {
if v1, ok := args[2].NumberOrFractionForPercentage(1, 0); ok {
if v2, ok := args[3].NumberOrFractionForPercentage(1, 0); ok {
if a, ok := parseAlphaByte(alpha); ok {
v0, v1, v2 := helpers.NewF64(v0), helpers.NewF64(v1), helpers.NewF64(v2)
switch strings.ToLower(colorSpace.Text) {
case "a98-rgb":
r, g, b := lin_a98rgb(v0, v1, v2)
x, y, z := lin_a98rgb_to_xyz(r, g, b)
return parsedColor{hasColorSpace: true, x: x, y: y, z: z, hex: a}, true
case "display-p3":
r, g, b := lin_p3(v0, v1, v2)
x, y, z := lin_p3_to_xyz(r, g, b)
return parsedColor{hasColorSpace: true, x: x, y: y, z: z, hex: a}, true
case "prophoto-rgb":
r, g, b := lin_prophoto(v0, v1, v2)
x, y, z := lin_prophoto_to_xyz(r, g, b)
x, y, z = d50_to_d65(x, y, z)
return parsedColor{hasColorSpace: true, x: x, y: y, z: z, hex: a}, true
case "rec2020":
r, g, b := lin_2020(v0, v1, v2)
x, y, z := lin_2020_to_xyz(r, g, b)
return parsedColor{hasColorSpace: true, x: x, y: y, z: z, hex: a}, true
case "srgb":
r, g, b := lin_srgb(v0, v1, v2)
x, y, z := lin_srgb_to_xyz(r, g, b)
return parsedColor{hasColorSpace: true, x: x, y: y, z: z, hex: a}, true
case "srgb-linear":
x, y, z := lin_srgb_to_xyz(v0, v1, v2)
return parsedColor{hasColorSpace: true, x: x, y: y, z: z, hex: a}, true
case "xyz", "xyz-d65":
return parsedColor{hasColorSpace: true, x: v0, y: v1, z: v2, hex: a}, true
case "xyz-d50":
x, y, z := d50_to_d65(v0, v1, v2)
return parsedColor{hasColorSpace: true, x: x, y: y, z: z, hex: a}, true
}
}
}
}
}
}
case "lab", "lch", "oklab", "oklch":
args := *token.Children
var v0, v1, v2, alpha css_ast.Token
switch len(args) {
case 3:
// "lab(1 2 3)"
v0, v1, v2 = args[0], args[1], args[2]
case 5:
// "lab(1 2 3 / 50%)"
if args[3].Kind == css_lexer.TDelimSlash {
v0, v1, v2, alpha = args[0], args[1], args[2], args[4]
}
}
if v0.Kind != css_lexer.T(0) {
if alpha, ok := parseAlphaByte(alpha); ok {
switch lowerText {
case "lab":
if v0, ok := v0.NumberOrFractionForPercentage(100, 0); ok {
if v1, ok := v1.NumberOrFractionForPercentage(125, css_ast.AllowAnyPercentage); ok {
if v2, ok := v2.NumberOrFractionForPercentage(125, css_ast.AllowAnyPercentage); ok {
v0, v1, v2 := helpers.NewF64(v0), helpers.NewF64(v1), helpers.NewF64(v2)
x, y, z := lab_to_xyz(v0, v1, v2)
x, y, z = d50_to_d65(x, y, z)
return parsedColor{hasColorSpace: true, x: x, y: y, z: z, hex: alpha}, true
}
}
}
case "lch":
if v0, ok := v0.NumberOrFractionForPercentage(100, 0); ok {
if v1, ok := v1.NumberOrFractionForPercentage(125, css_ast.AllowPercentageAbove100); ok {
if v2, ok := degreesForAngle(v2); ok {
v0, v1, v2 := helpers.NewF64(v0), helpers.NewF64(v1), helpers.NewF64(v2)
l, a, b := lch_to_lab(v0, v1, v2)
x, y, z := lab_to_xyz(l, a, b)
x, y, z = d50_to_d65(x, y, z)
return parsedColor{hasColorSpace: true, x: x, y: y, z: z, hex: alpha}, true
}
}
}
case "oklab":
if v0, ok := v0.NumberOrFractionForPercentage(1, 0); ok {
if v1, ok := v1.NumberOrFractionForPercentage(0.4, css_ast.AllowAnyPercentage); ok {
if v2, ok := v2.NumberOrFractionForPercentage(0.4, css_ast.AllowAnyPercentage); ok {
v0, v1, v2 := helpers.NewF64(v0), helpers.NewF64(v1), helpers.NewF64(v2)
x, y, z := oklab_to_xyz(v0, v1, v2)
return parsedColor{hasColorSpace: true, x: x, y: y, z: z, hex: alpha}, true
}
}
}
case "oklch":
if v0, ok := v0.NumberOrFractionForPercentage(1, 0); ok {
if v1, ok := v1.NumberOrFractionForPercentage(0.4, css_ast.AllowPercentageAbove100); ok {
if v2, ok := degreesForAngle(v2); ok {
v0, v1, v2 := helpers.NewF64(v0), helpers.NewF64(v1), helpers.NewF64(v2)
l, a, b := oklch_to_oklab(v0, v1, v2)
x, y, z := oklab_to_xyz(l, a, b)
return parsedColor{hasColorSpace: true, x: x, y: y, z: z, hex: alpha}, true
}
}
}
}
}
}
}
}
return parsedColor{}, false
}
// Reference: https://drafts.csswg.org/css-color/#hwb-to-rgb
func hwbToRgb(hue F64, white F64, black F64) (r F64, g F64, b F64) {
if white.Add(black).Value() >= 1 {
gray := white.Div(white.Add(black))
return gray, gray, gray
}
delta := white.Add(black).Neg().AddConst(1)
r, g, b = hslToRgb(hue, helpers.NewF64(1), helpers.NewF64(0.5))
r = delta.Mul(r).Add(white)
g = delta.Mul(g).Add(white)
b = delta.Mul(b).Add(white)
return
}
// Reference https://drafts.csswg.org/css-color/#hsl-to-rgb
func hslToRgb(hue F64, sat F64, light F64) (r F64, g F64, b F64) {
hue = hue.DivConst(360.0)
var t2 F64
if light.Value() <= 0.5 {
t2 = sat.AddConst(1).Mul(light)
} else {
t2 = light.Add(sat).Sub(light.Mul(sat))
}
t1 := light.MulConst(2).Sub(t2)
r = hueToRgb(t1, t2, hue.AddConst(1.0/3.0))
g = hueToRgb(t1, t2, hue)
b = hueToRgb(t1, t2, hue.SubConst(1.0/3.0))
return
}
func hueToRgb(t1 F64, t2 F64, hue F64) F64 {
hue = hue.Sub(hue.Floor())
hue = hue.MulConst(6)
var f F64
if hue.Value() < 1 {
f = helpers.Lerp(t1, t2, hue)
} else if hue.Value() < 3 {
f = t2
} else if hue.Value() < 4 {
f = helpers.Lerp(t1, t2, hue.Neg().AddConst(4))
} else {
f = t1
}
return f
}
func packRGBA(rf F64, gf F64, bf F64, a uint32) uint32 {
r := floatToByte(rf.Value())
g := floatToByte(gf.Value())
b := floatToByte(bf.Value())
return (r << 24) | (g << 16) | (b << 8) | a
}
func floatToByte(f float64) uint32 {
i := int(math.Round(f * 255))
if i < 0 {
i = 0
} else if i > 255 {
i = 255
}
return uint32(i)
}
func parseAlphaByte(token css_ast.Token) (uint32, bool) {
if token.Kind == css_lexer.T(0) {
return 255, true
}
return parseColorByte(token, 255)
}
func parseColorByte(token css_ast.Token, scale float64) (uint32, bool) {
var i int
var ok bool
switch token.Kind {
case css_lexer.TNumber:
if f, err := strconv.ParseFloat(token.Text, 64); err == nil {
i = int(math.Round(f * scale))
ok = true
}
case css_lexer.TPercentage:
if f, err := strconv.ParseFloat(token.PercentageValue(), 64); err == nil {
i = int(math.Round(f * (255.0 / 100.0)))
ok = true
}
}
if i < 0 {
i = 0
} else if i > 255 {
i = 255
}
return uint32(i), ok
}
func tryToConvertToHexWithoutClipping(x F64, y F64, z F64, a uint32) (uint32, bool) {
r, g, b := gam_srgb(xyz_to_lin_srgb(x, y, z))
if r.Value() < -0.5/255 || r.Value() > 255.5/255 ||
g.Value() < -0.5/255 || g.Value() > 255.5/255 ||
b.Value() < -0.5/255 || b.Value() > 255.5/255 {
return 0, false
}
return packRGBA(r, g, b, a), true
}
func (p *parser) tryToGenerateColor(token css_ast.Token, color parsedColor, wouldClipColor *bool) css_ast.Token {
// Note: Do NOT remove color information from fully transparent colors.
// Safari behaves differently than other browsers for color interpolation:
// https://css-tricks.com/thing-know-gradients-transparent-black/
// Attempt to convert other color spaces to sRGB, and only continue if the
// result (rounded to the nearest byte) will be in the 0-to-1 sRGB range
var hex uint32
if !color.hasColorSpace {
hex = color.hex
} else if result, ok := tryToConvertToHexWithoutClipping(color.x, color.y, color.z, color.hex); ok {
hex = result
} else if wouldClipColor != nil {
*wouldClipColor = true
return token
} else {
r, g, b := gamut_mapping_xyz_to_srgb(color.x, color.y, color.z)
hex = packRGBA(r, g, b, color.hex)
}
if hexA(hex) == 255 {
token.Children = nil
if name, ok := shortColorName[hex]; ok && p.options.minifySyntax {
token.Kind = css_lexer.TIdent
token.Text = name
} else {
token.Kind = css_lexer.THash
hex >>= 8
compact := compactHex(hex)
if p.options.minifySyntax && hex == expandHex(compact) {
token.Text = fmt.Sprintf("%03x", compact)
} else {
token.Text = fmt.Sprintf("%06x", hex)
}
}
} else if !p.options.unsupportedCSSFeatures.Has(compat.HexRGBA) {
token.Children = nil
token.Kind = css_lexer.THash
compact := compactHex(hex)
if p.options.minifySyntax && hex == expandHex(compact) {
token.Text = fmt.Sprintf("%04x", compact)
} else {
token.Text = fmt.Sprintf("%08x", hex)
}
} else {
token.Kind = css_lexer.TFunction
token.Text = "rgba"
commaToken := p.commaToken(token.Loc)
index := hexA(hex) * 4
alpha := alphaFractionTable[index : index+4]
if space := strings.IndexByte(alpha, ' '); space != -1 {
alpha = alpha[:space]
}
token.Children = &[]css_ast.Token{
{Loc: token.Loc, Kind: css_lexer.TNumber, Text: strconv.Itoa(hexR(hex))}, commaToken,
{Loc: token.Loc, Kind: css_lexer.TNumber, Text: strconv.Itoa(hexG(hex))}, commaToken,
{Loc: token.Loc, Kind: css_lexer.TNumber, Text: strconv.Itoa(hexB(hex))}, commaToken,
{Loc: token.Loc, Kind: css_lexer.TNumber, Text: alpha},
}
}
return token
}
// Every four characters in this table is the fraction for that index
const alphaFractionTable string = "" +
"0 .004.008.01 .016.02 .024.027.03 .035.04 .043.047.05 .055.06 " +
".063.067.07 .075.08 .082.086.09 .094.098.1 .106.11 .114.118.12 " +
".125.13 .133.137.14 .145.15 .153.157.16 .165.17 .173.176.18 .184" +
".19 .192.196.2 .204.208.21 .216.22 .224.227.23 .235.24 .243.247" +
".25 .255.26 .263.267.27 .275.28 .282.286.29 .294.298.3 .306.31 " +
".314.318.32 .325.33 .333.337.34 .345.35 .353.357.36 .365.37 .373" +
".376.38 .384.39 .392.396.4 .404.408.41 .416.42 .424.427.43 .435" +
".44 .443.447.45 .455.46 .463.467.47 .475.48 .482.486.49 .494.498" +
".5 .506.51 .514.518.52 .525.53 .533.537.54 .545.55 .553.557.56 " +
".565.57 .573.576.58 .584.59 .592.596.6 .604.608.61 .616.62 .624" +
".627.63 .635.64 .643.647.65 .655.66 .663.667.67 .675.68 .682.686" +
".69 .694.698.7 .706.71 .714.718.72 .725.73 .733.737.74 .745.75 " +
".753.757.76 .765.77 .773.776.78 .784.79 .792.796.8 .804.808.81 " +
".816.82 .824.827.83 .835.84 .843.847.85 .855.86 .863.867.87 .875" +
".88 .882.886.89 .894.898.9 .906.91 .914.918.92 .925.93 .933.937" +
".94 .945.95 .953.957.96 .965.97 .973.976.98 .984.99 .992.9961 "
@@ -0,0 +1,103 @@
package css_parser
import (
"fmt"
"strings"
"github.com/evanw/esbuild/internal/ast"
"github.com/evanw/esbuild/internal/css_ast"
"github.com/evanw/esbuild/internal/css_lexer"
"github.com/evanw/esbuild/internal/logger"
)
type composesContext struct {
parentRefs []ast.Ref
parentRange logger.Range
problemRange logger.Range
}
func (p *parser) handleComposesPragma(context composesContext, tokens []css_ast.Token) {
type nameWithLoc struct {
loc logger.Loc
text string
}
var names []nameWithLoc
fromGlobal := false
for i, t := range tokens {
if t.Kind == css_lexer.TIdent {
// Check for a "from" clause at the end
if strings.EqualFold(t.Text, "from") && i+2 == len(tokens) {
last := tokens[i+1]
// A string or a URL is an external file
if last.Kind == css_lexer.TString || last.Kind == css_lexer.TURL {
var importRecordIndex uint32
if last.Kind == css_lexer.TString {
importRecordIndex = uint32(len(p.importRecords))
p.importRecords = append(p.importRecords, ast.ImportRecord{
Kind: ast.ImportComposesFrom,
Path: logger.Path{Text: last.Text},
Range: p.source.RangeOfString(last.Loc),
})
} else {
importRecordIndex = last.PayloadIndex
p.importRecords[importRecordIndex].Kind = ast.ImportComposesFrom
}
for _, parentRef := range context.parentRefs {
composes := p.composes[parentRef]
for _, name := range names {
composes.ImportedNames = append(composes.ImportedNames, css_ast.ImportedComposesName{
ImportRecordIndex: importRecordIndex,
Alias: name.text,
AliasLoc: name.loc,
})
}
}
return
}
// An identifier must be "global"
if last.Kind == css_lexer.TIdent {
if strings.EqualFold(last.Text, "global") {
fromGlobal = true
break
}
p.log.AddID(logger.MsgID_CSS_CSSSyntaxError, logger.Warning, &p.tracker, css_lexer.RangeOfIdentifier(p.source, last.Loc),
fmt.Sprintf("\"composes\" declaration uses invalid location %q", last.Text))
p.prevError = t.Loc
return
}
}
names = append(names, nameWithLoc{t.Loc, t.Text})
continue
}
// Any unexpected tokens are a syntax error
var text string
switch t.Kind {
case css_lexer.TURL, css_lexer.TBadURL, css_lexer.TString, css_lexer.TUnterminatedString:
text = fmt.Sprintf("Unexpected %s", t.Kind.String())
default:
text = fmt.Sprintf("Unexpected %q", t.Text)
}
p.log.AddID(logger.MsgID_CSS_CSSSyntaxError, logger.Warning, &p.tracker, logger.Range{Loc: t.Loc}, text)
p.prevError = t.Loc
return
}
// If we get here, all of these names are not references to another file
old := p.makeLocalSymbols
if fromGlobal {
p.makeLocalSymbols = false
}
for _, parentRef := range context.parentRefs {
composes := p.composes[parentRef]
for _, name := range names {
composes.Names = append(composes.Names, p.symbolForName(name.loc, name.text))
}
}
p.makeLocalSymbols = old
}
@@ -0,0 +1,53 @@
package css_parser
import (
"strings"
"github.com/evanw/esbuild/internal/css_ast"
"github.com/evanw/esbuild/internal/css_lexer"
)
// Scan for container names in the "container" shorthand property
func (p *parser) processContainerShorthand(tokens []css_ast.Token) {
// Validate the syntax
for i, t := range tokens {
if t.Kind == css_lexer.TIdent {
continue
}
if t.Kind == css_lexer.TDelimSlash && i+2 == len(tokens) && tokens[i+1].Kind == css_lexer.TIdent {
break
}
return
}
// Convert any local names
for i, t := range tokens {
if t.Kind != css_lexer.TIdent {
break
}
p.handleSingleContainerName(&tokens[i])
}
}
func (p *parser) processContainerName(tokens []css_ast.Token) {
// Validate the syntax
for _, t := range tokens {
if t.Kind != css_lexer.TIdent {
return
}
}
// Convert any local names
for i := range tokens {
p.handleSingleContainerName(&tokens[i])
}
}
func (p *parser) handleSingleContainerName(token *css_ast.Token) {
if lower := strings.ToLower(token.Text); lower == "none" || cssWideAndReservedKeywords[lower] {
return
}
token.Kind = css_lexer.TSymbol
token.PayloadIndex = p.symbolForName(token.Loc, token.Text).Ref.InnerIndex
}
@@ -0,0 +1,138 @@
package css_parser
import (
"strconv"
"strings"
"github.com/evanw/esbuild/internal/css_ast"
"github.com/evanw/esbuild/internal/css_lexer"
)
// Specification: https://drafts.csswg.org/css-fonts/#font-prop
// [ <font-style> || <font-variant-css2> || <font-weight> || <font-stretch-css3> ]? <font-size> [ / <line-height> ]? <font-family>
func (p *parser) mangleFont(tokens []css_ast.Token) []css_ast.Token {
var result []css_ast.Token
// Scan up to the font size
pos := 0
for ; pos < len(tokens); pos++ {
token := tokens[pos]
if isFontSize(token) {
break
}
switch token.Kind {
case css_lexer.TIdent:
switch strings.ToLower(token.Text) {
case "normal":
// "All subproperties of the font property are first reset to their initial values"
// This implies that "normal" doesn't do anything. Also all of the optional values
// contain "normal" as an option and they are unordered so it's impossible to say
// what property "normal" corresponds to. Just drop these tokens to save space.
continue
// <font-style>
case "italic":
case "oblique":
if pos+1 < len(tokens) && tokens[pos+1].IsAngle() {
result = append(result, token, tokens[pos+1])
pos++
continue
}
// <font-variant-css2>
case "small-caps":
// <font-weight>
case "bold", "bolder", "lighter":
result = append(result, p.mangleFontWeight(token))
continue
// <font-stretch-css3>
case "ultra-condensed", "extra-condensed", "condensed", "semi-condensed",
"semi-expanded", "expanded", "extra-expanded", "ultra-expanded":
default:
// All other tokens are unrecognized, so we bail if we hit one
return tokens
}
result = append(result, token)
case css_lexer.TNumber:
// "Only values greater than or equal to 1, and less than or equal to
// 1000, are valid, and all other values are invalid."
if value, err := strconv.ParseFloat(token.Text, 64); err != nil || value < 1 || value > 1000 {
return tokens
}
result = append(result, token)
default:
// All other tokens are unrecognized, so we bail if we hit one
return tokens
}
}
// <font-size>
if pos == len(tokens) {
return tokens
}
result = append(result, tokens[pos])
pos++
// / <line-height>
if pos < len(tokens) && tokens[pos].Kind == css_lexer.TDelimSlash {
if pos+1 == len(tokens) {
return tokens
}
result = append(result, tokens[pos], tokens[pos+1])
pos += 2
// Remove the whitespace around the "/" character
if p.options.minifyWhitespace {
result[len(result)-3].Whitespace &= ^css_ast.WhitespaceAfter
result[len(result)-2].Whitespace = 0
result[len(result)-1].Whitespace &= ^css_ast.WhitespaceBefore
}
}
// <font-family>
if family, ok := p.mangleFontFamily(tokens[pos:]); ok {
if len(result) > 0 && len(family) > 0 && family[0].Kind != css_lexer.TString {
family[0].Whitespace |= css_ast.WhitespaceBefore
}
return append(result, family...)
}
return tokens
}
var fontSizeKeywords = map[string]bool{
// <absolute-size>: https://drafts.csswg.org/css-fonts/#valdef-font-size-absolute-size
"xx-small": true,
"x-small": true,
"small": true,
"medium": true,
"large": true,
"x-large": true,
"xx-large": true,
"xxx-large": true,
// <relative-size>: https://drafts.csswg.org/css-fonts/#valdef-font-size-relative-size
"larger": true,
"smaller": true,
}
// Specification: https://drafts.csswg.org/css-fonts/#font-size-prop
func isFontSize(token css_ast.Token) bool {
// <length-percentage>
if token.Kind == css_lexer.TDimension || token.Kind == css_lexer.TPercentage {
return true
}
// <absolute-size> or <relative-size>
if token.Kind == css_lexer.TIdent {
_, ok := fontSizeKeywords[strings.ToLower(token.Text)]
return ok
}
return false
}
@@ -0,0 +1,162 @@
package css_parser
import (
"strings"
"github.com/evanw/esbuild/internal/css_ast"
"github.com/evanw/esbuild/internal/css_lexer"
)
// These keywords usually require special handling when parsing.
// Declaring a property to have these values explicitly specifies a particular
// defaulting behavior instead of setting the property to that identifier value.
// As specified in CSS Values and Units Level 3, all CSS properties can accept
// these values.
//
// For example, "font-family: 'inherit'" sets the font family to the font named
// "inherit" while "font-family: inherit" sets the font family to the inherited
// value.
//
// Note that other CSS specifications can define additional CSS-wide keywords,
// which we should copy here whenever new ones are created so we can quote those
// identifiers to avoid collisions with any newly-created CSS-wide keywords.
var cssWideAndReservedKeywords = map[string]bool{
// CSS Values and Units Level 3: https://drafts.csswg.org/css-values-3/#common-keywords
"initial": true, // CSS-wide keyword
"inherit": true, // CSS-wide keyword
"unset": true, // CSS-wide keyword
"default": true, // CSS reserved keyword
// CSS Cascading and Inheritance Level 5: https://drafts.csswg.org/css-cascade-5/#defaulting-keywords
"revert": true, // Cascade-dependent keyword
"revert-layer": true, // Cascade-dependent keyword
}
// Font family names that happen to be the same as a keyword value must be
// quoted to prevent confusion with the keywords with the same names. UAs must
// not consider these keywords as matching the <family-name> type.
// Specification: https://drafts.csswg.org/css-fonts/#generic-font-families
var genericFamilyNames = map[string]bool{
"serif": true,
"sans-serif": true,
"cursive": true,
"fantasy": true,
"monospace": true,
"system-ui": true,
"emoji": true,
"math": true,
"fangsong": true,
"ui-serif": true,
"ui-sans-serif": true,
"ui-monospace": true,
"ui-rounded": true,
}
// Specification: https://drafts.csswg.org/css-fonts/#font-family-prop
func (p *parser) mangleFontFamily(tokens []css_ast.Token) ([]css_ast.Token, bool) {
result, rest, ok := p.mangleFamilyNameOrGenericName(nil, tokens)
if !ok {
return nil, false
}
for len(rest) > 0 && rest[0].Kind == css_lexer.TComma {
result, rest, ok = p.mangleFamilyNameOrGenericName(append(result, rest[0]), rest[1:])
if !ok {
return nil, false
}
}
if len(rest) > 0 {
return nil, false
}
return result, true
}
func (p *parser) mangleFamilyNameOrGenericName(result []css_ast.Token, tokens []css_ast.Token) ([]css_ast.Token, []css_ast.Token, bool) {
if len(tokens) > 0 {
t := tokens[0]
// Handle <generic-family>
if t.Kind == css_lexer.TIdent && genericFamilyNames[t.Text] {
return append(result, t), tokens[1:], true
}
// Handle <family-name>
if t.Kind == css_lexer.TString {
// "If a sequence of identifiers is given as a <family-name>, the computed
// value is the name converted to a string by joining all the identifiers
// in the sequence by single spaces."
//
// More information: https://mathiasbynens.be/notes/unquoted-font-family
names := strings.Split(t.Text, " ")
for _, name := range names {
if !isValidCustomIdent(name, genericFamilyNames) {
return append(result, t), tokens[1:], true
}
}
for i, name := range names {
var whitespace css_ast.WhitespaceFlags
if i != 0 || !p.options.minifyWhitespace {
whitespace = css_ast.WhitespaceBefore
}
result = append(result, css_ast.Token{
Loc: t.Loc,
Kind: css_lexer.TIdent,
Text: name,
Whitespace: whitespace,
})
}
return result, tokens[1:], true
}
// "Font family names other than generic families must either be given
// quoted as <string>s, or unquoted as a sequence of one or more
// <custom-ident>."
if t.Kind == css_lexer.TIdent {
for {
if !isValidCustomIdent(t.Text, genericFamilyNames) {
return nil, nil, false
}
result = append(result, t)
tokens = tokens[1:]
if len(tokens) == 0 || tokens[0].Kind != css_lexer.TIdent {
break
}
t = tokens[0]
}
return result, tokens, true
}
}
// Anything other than the cases listed above causes us to bail
return nil, nil, false
}
// Specification: https://drafts.csswg.org/css-values-4/#custom-idents
func isValidCustomIdent(text string, predefinedKeywords map[string]bool) bool {
loweredText := strings.ToLower(text)
if predefinedKeywords[loweredText] {
return false
}
if cssWideAndReservedKeywords[loweredText] {
return false
}
if loweredText == "" {
return false
}
// validate if it contains characters which needs to be escaped
if !css_lexer.WouldStartIdentifierWithoutEscapes(text) {
return false
}
for _, c := range text {
if !css_lexer.IsNameContinue(c) {
return false
}
}
return true
}
@@ -0,0 +1,25 @@
package css_parser
import (
"strings"
"github.com/evanw/esbuild/internal/css_ast"
"github.com/evanw/esbuild/internal/css_lexer"
)
func (p *parser) mangleFontWeight(token css_ast.Token) css_ast.Token {
if token.Kind != css_lexer.TIdent {
return token
}
switch strings.ToLower(token.Text) {
case "normal":
token.Text = "400"
token.Kind = css_lexer.TNumber
case "bold":
token.Text = "700"
token.Kind = css_lexer.TNumber
}
return token
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,179 @@
package css_parser
import (
"strings"
"github.com/evanw/esbuild/internal/css_ast"
"github.com/evanw/esbuild/internal/css_lexer"
)
// list-style-image: <image> | none
// <image>: <url> | <gradient>
// <url>: <url()> | <src()>
// <gradient>: <linear-gradient()> | <repeating-linear-gradient()> | <radial-gradient()> | <repeating-radial-gradient()>
//
// list-style-type: <counter-style> | <string> | none (where the string is a literal bullet marker)
// <counter-style>: <counter-style-name> | <symbols()>
// <counter-style-name>: not: decimal | disc | square | circle | disclosure-open | disclosure-closed | <wide keyword>
// when parsing a <custom-ident> with conflicts, only parse one if no other thing can claim it
func (p *parser) processListStyleShorthand(tokens []css_ast.Token) {
if len(tokens) < 1 || len(tokens) > 3 {
return
}
foundImage := false
foundPosition := false
typeIndex := -1
noneCount := 0
for i, t := range tokens {
switch t.Kind {
case css_lexer.TString:
// "list-style-type" is definitely not a <custom-ident>
return
case css_lexer.TURL:
if !foundImage {
foundImage = true
continue
}
case css_lexer.TFunction:
if !foundImage {
switch strings.ToLower(t.Text) {
case "src", "linear-gradient", "repeating-linear-gradient", "radial-gradient", "radial-linear-gradient":
foundImage = true
continue
}
}
case css_lexer.TIdent:
lower := strings.ToLower(t.Text)
// Note: If "none" is present, it's ambiguous whether it applies to
// "list-style-image" or "list-style-type". To resolve ambiguity it's
// applied at the end to whichever property isn't otherwise set.
if lower == "none" {
noneCount++
continue
}
if !foundPosition && (lower == "inside" || lower == "outside") {
foundPosition = true
continue
}
if typeIndex == -1 {
if cssWideAndReservedKeywords[lower] || predefinedCounterStyles[lower] {
// "list-style-type" is definitely not a <custom-ident>
return
}
typeIndex = i
continue
}
}
// Bail if we hit an unexpected token
return
}
if typeIndex != -1 {
// The first "none" applies to "list-style-image" if it's missing
if !foundImage && noneCount > 0 {
noneCount--
}
if noneCount > 0 {
// "list-style-type" is "none", not a <custom-ident>
return
}
if t := &tokens[typeIndex]; t.Kind == css_lexer.TIdent {
t.Kind = css_lexer.TSymbol
t.PayloadIndex = p.symbolForName(t.Loc, t.Text).Ref.InnerIndex
}
}
}
func (p *parser) processListStyleType(t *css_ast.Token) {
if t.Kind == css_lexer.TIdent {
if lower := strings.ToLower(t.Text); lower != "none" && !cssWideAndReservedKeywords[lower] && !predefinedCounterStyles[lower] {
t.Kind = css_lexer.TSymbol
t.PayloadIndex = p.symbolForName(t.Loc, t.Text).Ref.InnerIndex
}
}
}
// https://drafts.csswg.org/css-counter-styles-3/#predefined-counters
var predefinedCounterStyles = map[string]bool{
// 6.1. Numeric:
"arabic-indic": true,
"armenian": true,
"bengali": true,
"cambodian": true,
"cjk-decimal": true,
"decimal-leading-zero": true,
"decimal": true,
"devanagari": true,
"georgian": true,
"gujarati": true,
"gurmukhi": true,
"hebrew": true,
"kannada": true,
"khmer": true,
"lao": true,
"lower-armenian": true,
"lower-roman": true,
"malayalam": true,
"mongolian": true,
"myanmar": true,
"oriya": true,
"persian": true,
"tamil": true,
"telugu": true,
"thai": true,
"tibetan": true,
"upper-armenian": true,
"upper-roman": true,
// 6.2. Alphabetic:
"hiragana-iroha": true,
"hiragana": true,
"katakana-iroha": true,
"katakana": true,
"lower-alpha": true,
"lower-greek": true,
"lower-latin": true,
"upper-alpha": true,
"upper-latin": true,
// 6.3. Symbolic:
"circle": true,
"disc": true,
"disclosure-closed": true,
"disclosure-open": true,
"square": true,
// 6.4. Fixed:
"cjk-earthly-branch": true,
"cjk-heavenly-stem": true,
// 7.1.1. Japanese:
"japanese-formal": true,
"japanese-informal": true,
// 7.1.2. Korean:
"korean-hangul-formal": true,
"korean-hanja-formal": true,
"korean-hanja-informal": true,
// 7.1.3. Chinese:
"simp-chinese-formal": true,
"simp-chinese-informal": true,
"trad-chinese-formal": true,
"trad-chinese-informal": true,
// 7.2. Ethiopic Numeric Counter Style:
"ethiopic-numeric": true,
}
@@ -0,0 +1,347 @@
package css_parser
import (
"strings"
"github.com/evanw/esbuild/internal/css_ast"
"github.com/evanw/esbuild/internal/css_lexer"
)
func turnPercentIntoNumberIfShorter(t *css_ast.Token) {
if t.Kind == css_lexer.TPercentage {
if shifted, ok := shiftDot(t.PercentageValue(), -2); ok && len(shifted) < len(t.Text) {
t.Kind = css_lexer.TNumber
t.Text = shifted
}
}
}
// https://www.w3.org/TR/css-transforms-1/#two-d-transform-functions
// https://drafts.csswg.org/css-transforms-2/#transform-functions
func (p *parser) mangleTransforms(tokens []css_ast.Token) []css_ast.Token {
for i := range tokens {
if token := &tokens[i]; token.Kind == css_lexer.TFunction {
if args := *token.Children; css_ast.TokensAreCommaSeparated(args) {
n := len(args)
switch strings.ToLower(token.Text) {
////////////////////////////////////////////////////////////////////////////////
// 2D transforms
case "matrix":
// specifies a 2D transformation in the form of a transformation
// matrix of the six values a, b, c, d, e, f.
if n == 11 {
// | a c 0 e |
// | b d 0 f |
// | 0 0 1 0 |
// | 0 0 0 1 |
a, b, c, d, e, f := args[0], args[2], args[4], args[6], args[8], args[10]
if b.IsZero() && c.IsZero() && e.IsZero() && f.IsZero() {
// | a 0 0 0 |
// | 0 d 0 0 |
// | 0 0 1 0 |
// | 0 0 0 1 |
if a.EqualIgnoringWhitespace(d) {
// "matrix(a, 0, 0, a, 0, 0)" => "scale(a)"
token.Text = "scale"
*token.Children = args[:1]
} else if d.IsOne() {
// "matrix(a, 0, 0, 1, 0, 0)" => "scaleX(a)"
token.Text = "scaleX"
*token.Children = args[:1]
} else if a.IsOne() {
// "matrix(1, 0, 0, d, 0, 0)" => "scaleY(d)"
token.Text = "scaleY"
*token.Children = args[6:7]
} else {
// "matrix(a, 0, 0, d, 0, 0)" => "scale(a, d)"
token.Text = "scale"
*token.Children = append(args[:2], d)
}
// Note: A "matrix" cannot be directly converted into a "translate"
// because "translate" requires units while "matrix" requires no
// units. I'm not sure exactly what the semantics are so I'm not
// sure if you can just add "px" or not. Even if that did work,
// you still couldn't substitute values containing "var()" since
// units would still not be substituted in that case.
}
}
case "translate":
// specifies a 2D translation by the vector [tx, ty], where tx is the
// first translation-value parameter and ty is the optional second
// translation-value parameter. If <ty> is not provided, ty has zero
// as a value.
if n == 1 {
args[0].TurnLengthOrPercentageIntoNumberIfZero()
} else if n == 3 {
tx, ty := &args[0], &args[2]
tx.TurnLengthOrPercentageIntoNumberIfZero()
ty.TurnLengthOrPercentageIntoNumberIfZero()
if ty.IsZero() {
// "translate(tx, 0)" => "translate(tx)"
*token.Children = args[:1]
} else if tx.IsZero() {
// "translate(0, ty)" => "translateY(ty)"
token.Text = "translateY"
*token.Children = args[2:]
}
}
case "translatex":
// specifies a translation by the given amount in the X direction.
if n == 1 {
// "translateX(tx)" => "translate(tx)"
token.Text = "translate"
args[0].TurnLengthOrPercentageIntoNumberIfZero()
}
case "translatey":
// specifies a translation by the given amount in the Y direction.
if n == 1 {
args[0].TurnLengthOrPercentageIntoNumberIfZero()
}
case "scale":
// specifies a 2D scale operation by the [sx,sy] scaling vector
// described by the 2 parameters. If the second parameter is not
// provided, it takes a value equal to the first. For example,
// scale(1, 1) would leave an element unchanged, while scale(2, 2)
// would cause it to appear twice as long in both the X and Y axes,
// or four times its typical geometric size.
if n == 1 {
turnPercentIntoNumberIfShorter(&args[0])
} else if n == 3 {
sx, sy := &args[0], &args[2]
turnPercentIntoNumberIfShorter(sx)
turnPercentIntoNumberIfShorter(sy)
if sx.EqualIgnoringWhitespace(*sy) {
// "scale(s, s)" => "scale(s)"
*token.Children = args[:1]
} else if sy.IsOne() {
// "scale(s, 1)" => "scaleX(s)"
token.Text = "scaleX"
*token.Children = args[:1]
} else if sx.IsOne() {
// "scale(1, s)" => "scaleY(s)"
token.Text = "scaleY"
*token.Children = args[2:]
}
}
case "scalex":
// specifies a 2D scale operation using the [sx,1] scaling vector,
// where sx is given as the parameter.
if n == 1 {
turnPercentIntoNumberIfShorter(&args[0])
}
case "scaley":
// specifies a 2D scale operation using the [1,sy] scaling vector,
// where sy is given as the parameter.
if n == 1 {
turnPercentIntoNumberIfShorter(&args[0])
}
case "rotate":
// specifies a 2D rotation by the angle specified in the parameter
// about the origin of the element, as defined by the
// transform-origin property. For example, rotate(90deg) would
// cause elements to appear rotated one-quarter of a turn in the
// clockwise direction.
if n == 1 {
args[0].TurnLengthIntoNumberIfZero()
}
// Note: This is considered a 2D transform even though it's specified
// in terms of a 3D transform because it doesn't trigger Safari's 3D
// transform bugs.
case "rotatez":
// same as rotate3d(0, 0, 1, <angle>), which is a 3d transform
// equivalent to the 2d transform rotate(<angle>).
if n == 1 {
// "rotateZ(angle)" => "rotate(angle)"
token.Text = "rotate"
args[0].TurnLengthIntoNumberIfZero()
}
case "skew":
// specifies a 2D skew by [ax,ay] for X and Y. If the second
// parameter is not provided, it has a zero value.
if n == 1 {
args[0].TurnLengthIntoNumberIfZero()
} else if n == 3 {
ax, ay := &args[0], &args[2]
ax.TurnLengthIntoNumberIfZero()
ay.TurnLengthIntoNumberIfZero()
if ay.IsZero() {
// "skew(ax, 0)" => "skew(ax)"
*token.Children = args[:1]
}
}
case "skewx":
// specifies a 2D skew transformation along the X axis by the given
// angle.
if n == 1 {
// "skewX(ax)" => "skew(ax)"
token.Text = "skew"
args[0].TurnLengthIntoNumberIfZero()
}
case "skewy":
// specifies a 2D skew transformation along the Y axis by the given
// angle.
if n == 1 {
args[0].TurnLengthIntoNumberIfZero()
}
////////////////////////////////////////////////////////////////////////////////
// 3D transforms
// Note: Safari has a bug where 3D transforms render differently than
// other transforms. This means we should not minify a 3D transform
// into a 2D transform or it will cause a rendering difference in
// Safari.
case "matrix3d":
// specifies a 3D transformation as a 4x4 homogeneous matrix of 16
// values in column-major order.
if n == 31 {
// | m0 m4 m8 m12 |
// | m1 m5 m9 m13 |
// | m2 m6 m10 m14 |
// | m3 m7 m11 m15 |
mask := uint32(0)
for i := 0; i < 16; i++ {
if arg := args[i*2]; arg.IsZero() {
mask |= 1 << i
} else if arg.IsOne() {
mask |= (1 << 16) << i
}
}
const onlyScale = 0b1000_0000_0000_0000_0111_1011_1101_1110
if (mask & onlyScale) == onlyScale {
// | m0 0 0 0 |
// | 0 m5 0 0 |
// | 0 0 m10 0 |
// | 0 0 0 1 |
sx, sy := args[0], args[10]
if sx.IsOne() && sy.IsOne() {
token.Text = "scaleZ"
*token.Children = args[20:21]
} else {
token.Text = "scale3d"
*token.Children = append(append(args[0:2], args[10:12]...), args[20])
}
}
// Note: A "matrix3d" cannot be directly converted into a "translate3d"
// because "translate3d" requires units while "matrix3d" requires no
// units. I'm not sure exactly what the semantics are so I'm not
// sure if you can just add "px" or not. Even if that did work,
// you still couldn't substitute values containing "var()" since
// units would still not be substituted in that case.
}
case "translate3d":
// specifies a 3D translation by the vector [tx,ty,tz], with tx,
// ty and tz being the first, second and third translation-value
// parameters respectively.
if n == 5 {
tx, ty, tz := &args[0], &args[2], &args[4]
tx.TurnLengthOrPercentageIntoNumberIfZero()
ty.TurnLengthOrPercentageIntoNumberIfZero()
tz.TurnLengthIntoNumberIfZero()
if tx.IsZero() && ty.IsZero() {
// "translate3d(0, 0, tz)" => "translateZ(tz)"
token.Text = "translateZ"
*token.Children = args[4:]
}
}
case "translatez":
// specifies a 3D translation by the vector [0,0,tz] with the given
// amount in the Z direction.
if n == 1 {
args[0].TurnLengthIntoNumberIfZero()
}
case "scale3d":
// specifies a 3D scale operation by the [sx,sy,sz] scaling vector
// described by the 3 parameters.
if n == 5 {
sx, sy, sz := &args[0], &args[2], &args[4]
turnPercentIntoNumberIfShorter(sx)
turnPercentIntoNumberIfShorter(sy)
turnPercentIntoNumberIfShorter(sz)
if sx.IsOne() && sy.IsOne() {
// "scale3d(1, 1, sz)" => "scaleZ(sz)"
token.Text = "scaleZ"
*token.Children = args[4:]
}
}
case "scalez":
// specifies a 3D scale operation using the [1,1,sz] scaling vector,
// where sz is given as the parameter.
if n == 1 {
turnPercentIntoNumberIfShorter(&args[0])
}
case "rotate3d":
// specifies a 3D rotation by the angle specified in last parameter
// about the [x,y,z] direction vector described by the first three
// parameters. A direction vector that cannot be normalized, such as
// [0,0,0], will cause the rotation to not be applied.
if n == 7 {
x, y, z, angle := &args[0], &args[2], &args[4], &args[6]
angle.TurnLengthIntoNumberIfZero()
if x.IsOne() && y.IsZero() && z.IsZero() {
// "rotate3d(1, 0, 0, angle)" => "rotateX(angle)"
token.Text = "rotateX"
*token.Children = args[6:]
} else if x.IsZero() && y.IsOne() && z.IsZero() {
// "rotate3d(0, 1, 0, angle)" => "rotateY(angle)"
token.Text = "rotateY"
*token.Children = args[6:]
}
}
case "rotatex":
// same as rotate3d(1, 0, 0, <angle>).
if n == 1 {
args[0].TurnLengthIntoNumberIfZero()
}
case "rotatey":
// same as rotate3d(0, 1, 0, <angle>).
if n == 1 {
args[0].TurnLengthIntoNumberIfZero()
}
case "perspective":
// specifies a perspective projection matrix. This matrix scales
// points in X and Y based on their Z value, scaling points with
// positive Z values away from the origin, and those with negative Z
// values towards the origin. Points on the z=0 plane are unchanged.
// The parameter represents the distance of the z=0 plane from the
// viewer.
if n == 1 {
args[0].TurnLengthIntoNumberIfZero()
}
}
// Trim whitespace at the ends
if args := *token.Children; len(args) > 0 {
args[0].Whitespace &= ^css_ast.WhitespaceBefore
args[len(args)-1].Whitespace &= ^css_ast.WhitespaceAfter
}
}
}
}
return tokens
}
@@ -0,0 +1,511 @@
package css_parser
import (
"fmt"
"github.com/evanw/esbuild/internal/compat"
"github.com/evanw/esbuild/internal/css_ast"
"github.com/evanw/esbuild/internal/logger"
)
func (p *parser) lowerNestingInRule(rule css_ast.Rule, results []css_ast.Rule) []css_ast.Rule {
switch r := rule.Data.(type) {
case *css_ast.RSelector:
scope := func(loc logger.Loc) css_ast.ComplexSelector {
return css_ast.ComplexSelector{
Selectors: []css_ast.CompoundSelector{{
SubclassSelectors: []css_ast.SubclassSelector{{
Range: logger.Range{Loc: loc},
Data: &css_ast.SSPseudoClass{Name: "scope"},
}},
}},
}
}
parentSelectorsWithPseudo := make([]css_ast.ComplexSelector, 0, len(r.Selectors))
parentSelectorsNoPseudo := make([]css_ast.ComplexSelector, 0, len(r.Selectors))
for i, sel := range r.Selectors {
// Top-level "&" should be replaced with ":scope" to avoid recursion.
// From https://www.w3.org/TR/css-nesting-1/#nest-selector:
//
// "When used in the selector of a nested style rule, the nesting
// selector represents the elements matched by the parent rule. When
// used in any other context, it represents the same elements as
// :scope in that context (unless otherwise defined)."
//
substituted := make([]css_ast.CompoundSelector, 0, len(sel.Selectors))
for _, x := range sel.Selectors {
substituted = p.substituteAmpersandsInCompoundSelector(x, scope, substituted, keepLeadingCombinator)
}
r.Selectors[i] = css_ast.ComplexSelector{Selectors: substituted}
// Filter out pseudo elements because they are ignored by nested style
// rules. This is because pseudo-elements are not valid within :is():
// https://www.w3.org/TR/selectors-4/#matches-pseudo. This restriction
// may be relaxed in the future, but this restriction has shipped so
// we're stuck with it: https://github.com/w3c/csswg-drafts/issues/7433.
//
// Note: This is only for the parent selector list that is used to
// substitute "&" within child rules. Do not filter out the pseudo
// element from the top-level selector list.
if !sel.UsesPseudoElement() {
parentSelectorsNoPseudo = append(parentSelectorsNoPseudo, css_ast.ComplexSelector{Selectors: substituted})
}
// This filtering is only done conditionally because it seems to only
// apply sometimes. Specifically it doesn't seem to apply when the
// nested rule is an at-rule. So we use the unfiltered list in that
// case. See: https://github.com/evanw/esbuild/issues/4265
parentSelectorsWithPseudo = append(parentSelectorsWithPseudo, css_ast.ComplexSelector{Selectors: substituted})
}
// Emit this selector before its nested children
start := len(results)
results = append(results, rule)
// Lower all children and filter out ones that become empty
context := lowerNestingContext{
parentSelectorsWithPseudo: parentSelectorsWithPseudo,
parentSelectorsNoPseudo: parentSelectorsNoPseudo,
loweredRules: results,
}
r.Rules = p.lowerNestingInRulesAndReturnRemaining(r.Rules, &context)
// Omit this selector entirely if it's now empty
if len(r.Rules) == 0 {
copy(context.loweredRules[start:], context.loweredRules[start+1:])
context.loweredRules = context.loweredRules[:len(context.loweredRules)-1]
}
return context.loweredRules
case *css_ast.RKnownAt:
var rules []css_ast.Rule
for _, child := range r.Rules {
rules = p.lowerNestingInRule(child, rules)
}
r.Rules = rules
case *css_ast.RAtLayer:
var rules []css_ast.Rule
for _, child := range r.Rules {
rules = p.lowerNestingInRule(child, rules)
}
r.Rules = rules
case *css_ast.RAtMedia:
var rules []css_ast.Rule
for _, child := range r.Rules {
rules = p.lowerNestingInRule(child, rules)
}
r.Rules = rules
}
return append(results, rule)
}
// Lower all children and filter out ones that become empty
func (p *parser) lowerNestingInRulesAndReturnRemaining(rules []css_ast.Rule, context *lowerNestingContext) []css_ast.Rule {
n := 0
for _, child := range rules {
child = p.lowerNestingInRuleWithContext(child, context)
if child.Data != nil {
rules[n] = child
n++
}
}
return rules[:n]
}
func compoundSelectorTermCount(sel css_ast.CompoundSelector) int {
count := 0
for _, ss := range sel.SubclassSelectors {
count++
if list, ok := ss.Data.(*css_ast.SSPseudoClassWithSelectorList); ok {
count += complexSelectorTermCount(list.Selectors)
}
}
return count
}
func complexSelectorTermCount(selectors []css_ast.ComplexSelector) int {
count := 0
for _, sel := range selectors {
for _, inner := range sel.Selectors {
count += compoundSelectorTermCount(inner)
}
}
return count
}
func (p *parser) addExpansionError(loc logger.Loc, n int) {
p.log.AddErrorWithNotes(&p.tracker, logger.Range{Loc: loc}, "CSS nesting is causing too much expansion",
[]logger.MsgData{{Text: fmt.Sprintf("CSS nesting expansion was terminated because a rule was generated with %d selectors. "+
"This limit exists to prevent esbuild from using too much time and/or memory. "+
"Please change your CSS to use fewer levels of nesting.", n)}})
}
type lowerNestingContext struct {
parentSelectorsWithPseudo []css_ast.ComplexSelector
parentSelectorsNoPseudo []css_ast.ComplexSelector
loweredRules []css_ast.Rule
}
func (p *parser) lowerNestingInRuleWithContext(rule css_ast.Rule, context *lowerNestingContext) css_ast.Rule {
switch r := rule.Data.(type) {
case *css_ast.RSelector:
oldSelectorsLen := len(r.Selectors)
oldSelectorsComplexity := complexSelectorTermCount(r.Selectors)
// "a { & b {} }" => "a b {}"
// "a { &b {} }" => "a:is(b) {}"
// "a { &:hover {} }" => "a:hover {}"
// ".x { &b {} }" => "b.x {}"
// "a, b { .c, d {} }" => ":is(a, b) :is(.c, d) {}"
// "a, b { &.c, & d, e & {} }" => ":is(a, b).c, :is(a, b) d, e :is(a, b) {}"
// Pass 1: Canonicalize and analyze our selectors
for i := range r.Selectors {
sel := &r.Selectors[i]
// Inject the implicit "&" now for simplicity later on
if sel.IsRelative() {
sel.Selectors = append([]css_ast.CompoundSelector{{NestingSelectorLocs: []logger.Loc{rule.Loc}}}, sel.Selectors...)
}
}
// Pass 2: Substitute "&" for the parent selector
if !p.options.unsupportedCSSFeatures.Has(compat.IsPseudoClass) || len(context.parentSelectorsNoPseudo) <= 1 {
// If we can use ":is", or we don't have to because there's only one
// parent selector, or we are using ":is()" to match zero parent selectors
// (even if ":is" is unsupported), then substituting "&" for the parent
// selector is easy.
for i := range r.Selectors {
complex := &r.Selectors[i]
results := make([]css_ast.CompoundSelector, 0, len(complex.Selectors))
parent := p.multipleComplexSelectorsToSingleComplexSelector(context.parentSelectorsNoPseudo)
for _, compound := range complex.Selectors {
results = p.substituteAmpersandsInCompoundSelector(compound, parent, results, keepLeadingCombinator)
}
complex.Selectors = results
}
} else {
// Otherwise if we can't use ":is", the transform is more complicated.
// Avoiding ":is" can lead to a combinatorial explosion of cases so we
// want to avoid this if possible. For example:
//
// .first, .second, .third {
// & > & {
// color: red;
// }
// }
//
// If we can use ":is" (the easy case above) then we can do this:
//
// :is(.first, .second, .third) > :is(.first, .second, .third) {
// color: red;
// }
//
// But if we can't use ":is" then we have to do this instead:
//
// .first > .first,
// .first > .second,
// .first > .third,
// .second > .first,
// .second > .second,
// .second > .third,
// .third > .first,
// .third > .second,
// .third > .third {
// color: red;
// }
//
// That combinatorial explosion is what the loop below implements. Note
// that PostCSS's implementation of nesting gets this wrong. It generates
// this instead:
//
// .first > .first,
// .second > .second,
// .third > .third {
// color: red;
// }
//
// That's not equivalent, so that's an incorrect transformation.
var selectors []css_ast.ComplexSelector
var indices []int
for {
// Every time we encounter another "&", add another dimension
offset := 0
parent := func(loc logger.Loc) css_ast.ComplexSelector {
if offset == len(indices) {
indices = append(indices, 0)
}
index := indices[offset]
offset++
return context.parentSelectorsNoPseudo[index]
}
// Do the substitution for this particular combination
for i := range r.Selectors {
complex := r.Selectors[i]
results := make([]css_ast.CompoundSelector, 0, len(complex.Selectors))
for _, compound := range complex.Selectors {
results = p.substituteAmpersandsInCompoundSelector(compound, parent, results, keepLeadingCombinator)
}
complex.Selectors = results
selectors = append(selectors, complex)
offset = 0
}
// Do addition with carry on the indices across dimensions
carry := len(indices)
for carry > 0 {
index := &indices[carry-1]
if *index+1 < len(context.parentSelectorsNoPseudo) {
*index++
break
}
*index = 0
carry--
}
if carry == 0 {
break
}
}
r.Selectors = selectors
}
// Put limits on the combinatorial explosion to avoid using too much time and/or memory
if n := len(r.Selectors); n > oldSelectorsLen && n > 0xFF00 {
p.addExpansionError(rule.Loc, n)
return css_ast.Rule{}
}
if n := complexSelectorTermCount(r.Selectors); n > oldSelectorsComplexity && n > 0xFF00 {
p.addExpansionError(rule.Loc, n)
return css_ast.Rule{}
}
// Lower all child rules using our newly substituted selector
context.loweredRules = p.lowerNestingInRule(rule, context.loweredRules)
return css_ast.Rule{}
case *css_ast.RKnownAt:
childContext := lowerNestingContext{
parentSelectorsWithPseudo: context.parentSelectorsWithPseudo,
parentSelectorsNoPseudo: context.parentSelectorsNoPseudo,
}
r.Rules = p.lowerNestingInRulesAndReturnRemaining(r.Rules, &childContext)
// "div { @supports (color: red) { color: red } }" "@supports (color: red) { div { color: red } }"
if len(r.Rules) > 0 {
childContext.loweredRules = append([]css_ast.Rule{{Loc: rule.Loc, Data: &css_ast.RSelector{
Selectors: context.parentSelectorsWithPseudo,
Rules: r.Rules,
}}}, childContext.loweredRules...)
}
// "div { @supports (color: red) { &:hover { color: red } } }" "@supports (color: red) { div:hover { color: red } }"
if len(childContext.loweredRules) > 0 {
r.Rules = childContext.loweredRules
context.loweredRules = append(context.loweredRules, rule)
}
return css_ast.Rule{}
case *css_ast.RAtMedia:
childContext := lowerNestingContext{
parentSelectorsWithPseudo: context.parentSelectorsWithPseudo,
parentSelectorsNoPseudo: context.parentSelectorsNoPseudo,
}
r.Rules = p.lowerNestingInRulesAndReturnRemaining(r.Rules, &childContext)
// "div { @media screen { color: red } }" "@media screen { div { color: red } }"
if len(r.Rules) > 0 {
childContext.loweredRules = append([]css_ast.Rule{{Loc: rule.Loc, Data: &css_ast.RSelector{
Selectors: context.parentSelectorsWithPseudo,
Rules: r.Rules,
}}}, childContext.loweredRules...)
}
// "div { @media screen { &:hover { color: red } } }" "@media screen { div:hover { color: red } }"
if len(childContext.loweredRules) > 0 {
r.Rules = childContext.loweredRules
context.loweredRules = append(context.loweredRules, rule)
}
return css_ast.Rule{}
case *css_ast.RAtLayer:
// Lower all children and filter out ones that become empty
childContext := lowerNestingContext{
parentSelectorsWithPseudo: context.parentSelectorsWithPseudo,
parentSelectorsNoPseudo: context.parentSelectorsNoPseudo,
}
r.Rules = p.lowerNestingInRulesAndReturnRemaining(r.Rules, &childContext)
// "div { @layer foo { color: red } }" "@layer foo { div { color: red } }"
if len(r.Rules) > 0 {
childContext.loweredRules = append([]css_ast.Rule{{Loc: rule.Loc, Data: &css_ast.RSelector{
Selectors: context.parentSelectorsWithPseudo,
Rules: r.Rules,
}}}, childContext.loweredRules...)
}
// "div { @layer foo { &:hover { color: red } } }" "@layer foo { div:hover { color: red } }"
// "div { @layer foo {} }" => "@layer foo {}" (layers have side effects, so don't remove empty ones)
r.Rules = childContext.loweredRules
context.loweredRules = append(context.loweredRules, rule)
return css_ast.Rule{}
}
return rule
}
type leadingCombinatorStrip uint8
const (
keepLeadingCombinator leadingCombinatorStrip = iota
stripLeadingCombinator
)
func (p *parser) substituteAmpersandsInCompoundSelector(
sel css_ast.CompoundSelector,
replacementFn func(logger.Loc) css_ast.ComplexSelector,
results []css_ast.CompoundSelector,
strip leadingCombinatorStrip,
) []css_ast.CompoundSelector {
for _, nestingSelectorLoc := range sel.NestingSelectorLocs {
replacement := replacementFn(nestingSelectorLoc)
// Convert the replacement to a single compound selector
var single css_ast.CompoundSelector
if sel.Combinator.Byte == 0 && (len(replacement.Selectors) == 1 || len(results) == 0) {
// ".foo { :hover & {} }" => ":hover .foo {}"
// ".foo .bar { &:hover {} }" => ".foo .bar:hover {}"
last := len(replacement.Selectors) - 1
results = append(results, replacement.Selectors[:last]...)
single = replacement.Selectors[last]
if strip == stripLeadingCombinator {
single.Combinator = css_ast.Combinator{}
}
sel.Combinator = single.Combinator
} else if len(replacement.Selectors) == 1 {
// ".foo { > &:hover {} }" => ".foo > .foo:hover {}"
single = replacement.Selectors[0]
if strip == stripLeadingCombinator {
single.Combinator = css_ast.Combinator{}
}
} else {
// ".foo .bar { :hover & {} }" => ":hover :is(.foo .bar) {}"
// ".foo .bar { > &:hover {} }" => ".foo .bar > :is(.foo .bar):hover {}"
p.reportNestingWithGeneratedPseudoClassIs(nestingSelectorLoc)
single = css_ast.CompoundSelector{
SubclassSelectors: []css_ast.SubclassSelector{{
Range: logger.Range{Loc: nestingSelectorLoc},
Data: &css_ast.SSPseudoClassWithSelectorList{
Kind: css_ast.PseudoClassIs,
Selectors: []css_ast.ComplexSelector{replacement.Clone()},
},
}},
}
}
var subclassSelectorPrefix []css_ast.SubclassSelector
// Insert the type selector
if single.TypeSelector != nil {
if sel.TypeSelector != nil {
p.reportNestingWithGeneratedPseudoClassIs(nestingSelectorLoc)
subclassSelectorPrefix = append(subclassSelectorPrefix, css_ast.SubclassSelector{
Range: sel.TypeSelector.Range(),
Data: &css_ast.SSPseudoClassWithSelectorList{
Kind: css_ast.PseudoClassIs,
Selectors: []css_ast.ComplexSelector{{Selectors: []css_ast.CompoundSelector{{TypeSelector: sel.TypeSelector}}}},
},
})
}
sel.TypeSelector = single.TypeSelector
}
// Insert the subclass selectors
subclassSelectorPrefix = append(subclassSelectorPrefix, single.SubclassSelectors...)
// Write the changes back
if len(subclassSelectorPrefix) > 0 {
sel.SubclassSelectors = append(subclassSelectorPrefix, sel.SubclassSelectors...)
}
}
sel.NestingSelectorLocs = nil
// "div { :is(&.foo) {} }" => ":is(div.foo) {}"
for _, ss := range sel.SubclassSelectors {
if class, ok := ss.Data.(*css_ast.SSPseudoClassWithSelectorList); ok {
outer := make([]css_ast.ComplexSelector, 0, len(class.Selectors))
for _, complex := range class.Selectors {
inner := make([]css_ast.CompoundSelector, 0, len(complex.Selectors))
for _, sel := range complex.Selectors {
inner = p.substituteAmpersandsInCompoundSelector(sel, replacementFn, inner, stripLeadingCombinator)
}
outer = append(outer, css_ast.ComplexSelector{Selectors: inner})
}
class.Selectors = outer
}
}
return append(results, sel)
}
// Turn the list of selectors into a single selector by wrapping lists
// without a single element with ":is(...)". Note that this may result
// in an empty ":is()" selector (which matches nothing).
func (p *parser) multipleComplexSelectorsToSingleComplexSelector(selectors []css_ast.ComplexSelector) func(logger.Loc) css_ast.ComplexSelector {
if len(selectors) == 1 {
return func(logger.Loc) css_ast.ComplexSelector {
return selectors[0]
}
}
var leadingCombinator css_ast.Combinator
clones := make([]css_ast.ComplexSelector, len(selectors))
for i, sel := range selectors {
// "> a, > b" => "> :is(a, b)" (the caller should have already checked that all leading combinators are the same)
leadingCombinator = sel.Selectors[0].Combinator
clones[i] = sel.Clone()
}
return func(loc logger.Loc) css_ast.ComplexSelector {
return css_ast.ComplexSelector{
Selectors: []css_ast.CompoundSelector{{
Combinator: leadingCombinator,
SubclassSelectors: []css_ast.SubclassSelector{{
Range: logger.Range{Loc: loc},
Data: &css_ast.SSPseudoClassWithSelectorList{
Kind: css_ast.PseudoClassIs,
Selectors: clones,
},
}},
}},
}
}
}
func (p *parser) reportNestingWithGeneratedPseudoClassIs(nestingSelectorLoc logger.Loc) {
if p.options.unsupportedCSSFeatures.Has(compat.IsPseudoClass) {
_, didWarn := p.nestingWarnings[nestingSelectorLoc]
if didWarn {
// Only warn at each location once
return
}
if p.nestingWarnings == nil {
p.nestingWarnings = make(map[logger.Loc]struct{})
}
p.nestingWarnings[nestingSelectorLoc] = struct{}{}
text := "Transforming this CSS nesting syntax is not supported in the configured target environment"
if p.options.originalTargetEnv != "" {
text = fmt.Sprintf("%s (%s)", text, p.options.originalTargetEnv)
}
r := logger.Range{Loc: nestingSelectorLoc, Len: 1}
p.log.AddIDWithNotes(logger.MsgID_CSS_UnsupportedCSSNesting, logger.Warning, &p.tracker, r, text, []logger.MsgData{{
Text: "The nesting transform for this case must generate an \":is(...)\" but the configured target environment does not support the \":is\" pseudo-class."}})
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,412 @@
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"
)
// Reference: https://drafts.csswg.org/mediaqueries-4/
func (p *parser) parseMediaQueryListUntil(stop func(css_lexer.T) bool) []css_ast.MediaQuery {
var queries []css_ast.MediaQuery
p.eat(css_lexer.TWhitespace)
for !p.peek(css_lexer.TEndOfFile) && !stop(p.current().Kind) {
start := p.index
query, ok := p.parseMediaQuery()
if !ok {
// If parsing failed, parse an arbitrary sequence of tokens instead
p.index = start
loc := p.current().Range.Loc
for !p.peek(css_lexer.TEndOfFile) && !stop(p.current().Kind) && !p.peek(css_lexer.TComma) {
p.parseComponentValue()
}
tokens := p.convertTokens(p.tokens[start:p.index])
query = css_ast.MediaQuery{Loc: loc, Data: &css_ast.MQArbitraryTokens{Tokens: tokens}}
}
queries = append(queries, query)
p.eat(css_lexer.TWhitespace)
if !p.eat(css_lexer.TComma) {
break
}
p.eat(css_lexer.TWhitespace)
}
return queries
}
func (p *parser) parseMediaQuery() (css_ast.MediaQuery, bool) {
loc := p.current().Range.Loc
// Check for a media condition first
if p.looksLikeMediaCondition() {
return p.parseMediaCondition(mediaWithOr)
}
// Parse the media type and potentially the leading "not" or "only" keyword
mediaType := p.decoded()
if !p.peek(css_lexer.TIdent) {
p.expect(css_lexer.TIdent)
return css_ast.MediaQuery{}, false
}
op := css_ast.MQTypeOpNone
if strings.EqualFold(mediaType, "not") {
op = css_ast.MQTypeOpNot
} else if strings.EqualFold(mediaType, "only") {
op = css_ast.MQTypeOpOnly
}
if op != css_ast.MQTypeOpNone {
p.advance()
p.eat(css_lexer.TWhitespace)
mediaType = p.decoded()
if !p.peek(css_lexer.TIdent) {
p.expect(css_lexer.TIdent)
return css_ast.MediaQuery{}, false
}
}
// The <media-type> production does not include the keywords "only", "not", "and", "or", and "layer".
if strings.EqualFold(mediaType, "only") ||
strings.EqualFold(mediaType, "not") ||
strings.EqualFold(mediaType, "and") ||
strings.EqualFold(mediaType, "or") ||
strings.EqualFold(mediaType, "layer") {
p.unexpected()
return css_ast.MediaQuery{}, false
}
p.advance()
p.eat(css_lexer.TWhitespace)
// Potentially parse a chain of "and" operators
var andOrNull css_ast.MediaQuery
if p.peek(css_lexer.TIdent) && strings.EqualFold(p.decoded(), "and") {
p.advance()
p.eat(css_lexer.TWhitespace)
var ok bool
andOrNull, ok = p.parseMediaCondition(mediaWithoutOr)
if !ok {
return css_ast.MediaQuery{}, false
}
}
return css_ast.MediaQuery{Loc: loc, Data: &css_ast.MQType{Op: op, Type: mediaType, AndOrNull: andOrNull}}, true
}
func (p *parser) looksLikeMediaCondition() bool {
kind := p.current().Kind
return kind == css_lexer.TOpenParen || kind == css_lexer.TFunction ||
(kind == css_lexer.TIdent && strings.EqualFold(p.decoded(), "not") &&
p.next().Kind == css_lexer.TWhitespace &&
p.at(p.index+2).Kind == css_lexer.TOpenParen)
}
type mediaOr uint8
const (
mediaWithOr mediaOr = iota
mediaWithoutOr
)
func (p *parser) parseMediaCondition(or mediaOr) (css_ast.MediaQuery, bool) {
loc := p.current().Range.Loc
// Handle a leading "not"
if p.peek(css_lexer.TIdent) && strings.EqualFold(p.decoded(), "not") {
p.advance()
p.eat(css_lexer.TWhitespace)
if inner, ok := p.parseMediaInParens(); !ok {
return css_ast.MediaQuery{}, false
} else {
return p.maybeSimplifyMediaNot(loc, inner), true
}
}
// Parse the first term
first, ok := p.parseMediaInParens()
if !ok {
return css_ast.MediaQuery{}, false
}
p.eat(css_lexer.TWhitespace)
// Potentially parse a chain of "and" or "or" operators
if p.peek(css_lexer.TIdent) {
if keyword := p.decoded(); strings.EqualFold(keyword, "and") || (or == mediaWithOr && strings.EqualFold(keyword, "or")) {
op := css_ast.MQBinaryOpAnd
if len(keyword) == 2 {
op = css_ast.MQBinaryOpOr
}
inner := p.appendMediaTerm([]css_ast.MediaQuery{}, first, op)
for {
p.advance()
p.eat(css_lexer.TWhitespace)
next, ok := p.parseMediaInParens()
if !ok {
return css_ast.MediaQuery{}, false
}
inner = p.appendMediaTerm(inner, next, op)
p.eat(css_lexer.TWhitespace)
if !p.peek(css_lexer.TIdent) || !strings.EqualFold(p.decoded(), keyword) {
break
}
}
return css_ast.MediaQuery{Loc: loc, Data: &css_ast.MQBinary{Op: op, Terms: inner}}, true
}
}
return first, true
}
func (p *parser) appendMediaTerm(inner []css_ast.MediaQuery, term css_ast.MediaQuery, op css_ast.MQBinaryOp) []css_ast.MediaQuery {
// "(a and b) and c" => "a and b and c"
// "(a or b) or c" => "a or b or c"
if binary, ok := term.Data.(*css_ast.MQBinary); ok && binary.Op == op && p.options.minifySyntax {
return append(inner, binary.Terms...)
} else {
return append(inner, term)
}
}
func (p *parser) parseMediaInParens() (css_ast.MediaQuery, bool) {
p.eat(css_lexer.TWhitespace)
start := p.index
// Consume the opening token
isFunction := p.eat(css_lexer.TFunction)
if !isFunction && !p.expect(css_lexer.TOpenParen) {
return css_ast.MediaQuery{}, false
}
p.eat(css_lexer.TWhitespace)
// Handle a media condition
if !isFunction && p.looksLikeMediaCondition() {
if inner, ok := p.parseMediaCondition(mediaWithOr); !ok {
return css_ast.MediaQuery{}, false
} else {
p.eat(css_lexer.TWhitespace)
if !p.expect(css_lexer.TCloseParen) {
return css_ast.MediaQuery{}, false
}
return inner, ok
}
}
// Scan over the remaining tokens
for !p.peek(css_lexer.TCloseParen) && !p.peek(css_lexer.TEndOfFile) {
p.parseComponentValue()
}
end := p.index
if !p.expect(css_lexer.TCloseParen) {
return css_ast.MediaQuery{}, false
}
tokens := p.convertTokens(p.tokens[start:end])
loc := tokens[0].Loc
// Potentially pattern-match the tokens inside the parentheses
if !isFunction && len(tokens) == 1 {
if children := tokens[0].Children; children != nil {
if term, ok := parsePlainOrBooleanMediaFeature(*children); ok {
return css_ast.MediaQuery{Loc: loc, Data: term}, true
}
if term, ok := parseRangeMediaFeature(*children); ok {
if p.options.unsupportedCSSFeatures.Has(compat.MediaRange) {
var terms []css_ast.MediaQuery
if term.BeforeCmp != css_ast.MQCmpNone {
terms = append(terms, lowerMediaRange(term.NameLoc, term.Name, term.BeforeCmp.Reverse(), term.Before))
}
if term.AfterCmp != css_ast.MQCmpNone {
terms = append(terms, lowerMediaRange(term.NameLoc, term.Name, term.AfterCmp, term.After))
}
if len(terms) == 1 {
return terms[0], true
} else {
return css_ast.MediaQuery{Loc: loc, Data: &css_ast.MQBinary{Op: css_ast.MQBinaryOpAnd, Terms: terms}}, true
}
}
return css_ast.MediaQuery{Loc: loc, Data: term}, true
}
}
}
return css_ast.MediaQuery{Loc: loc, Data: &css_ast.MQArbitraryTokens{Tokens: tokens}}, true
}
func lowerMediaRange(loc logger.Loc, name string, cmp css_ast.MQCmp, value []css_ast.Token) css_ast.MediaQuery {
switch cmp {
case css_ast.MQCmpLe:
// "foo <= 123" => "max-foo: 123"
return css_ast.MediaQuery{Loc: loc, Data: &css_ast.MQPlainOrBoolean{Name: "max-" + name, ValueOrNil: value}}
case css_ast.MQCmpGe:
// "foo >= 123" => "min-foo: 123"
return css_ast.MediaQuery{Loc: loc, Data: &css_ast.MQPlainOrBoolean{Name: "min-" + name, ValueOrNil: value}}
case css_ast.MQCmpLt:
// "foo < 123" => "not (min-foo: 123)"
return css_ast.MediaQuery{Loc: loc, Data: &css_ast.MQNot{
Inner: css_ast.MediaQuery{Loc: loc, Data: &css_ast.MQPlainOrBoolean{Name: "min-" + name, ValueOrNil: value}},
}}
case css_ast.MQCmpGt:
// "foo > 123" => "not (max-foo: 123)"
return css_ast.MediaQuery{Loc: loc, Data: &css_ast.MQNot{
Inner: css_ast.MediaQuery{Loc: loc, Data: &css_ast.MQPlainOrBoolean{Name: "max-" + name, ValueOrNil: value}},
}}
default:
// "foo = 123" => "foo: 123"
return css_ast.MediaQuery{Loc: loc, Data: &css_ast.MQPlainOrBoolean{Name: name, ValueOrNil: value}}
}
}
func parsePlainOrBooleanMediaFeature(tokens []css_ast.Token) (*css_ast.MQPlainOrBoolean, bool) {
if len(tokens) == 1 && tokens[0].Kind == css_lexer.TIdent {
return &css_ast.MQPlainOrBoolean{Name: tokens[0].Text}, true
}
if len(tokens) >= 3 && tokens[0].Kind == css_lexer.TIdent && tokens[1].Kind == css_lexer.TColon {
if value, rest := scanMediaValue(tokens[2:]); len(rest) == 0 {
return &css_ast.MQPlainOrBoolean{Name: tokens[0].Text, ValueOrNil: value}, true
}
}
return nil, false
}
func parseRangeMediaFeature(tokens []css_ast.Token) (*css_ast.MQRange, bool) {
if first, tokens := scanMediaValue(tokens); len(first) > 0 {
if firstCmp, tokens := scanMediaComparison(tokens); firstCmp != css_ast.MQCmpNone {
if second, tokens := scanMediaValue(tokens); len(second) > 0 {
if len(tokens) == 0 {
if name, nameLoc, ok := isSingleIdent(first); ok {
return &css_ast.MQRange{
Name: name,
NameLoc: nameLoc,
AfterCmp: firstCmp,
After: second,
}, true
} else if name, nameLoc, ok := isSingleIdent(second); ok {
return &css_ast.MQRange{
Before: first,
BeforeCmp: firstCmp,
Name: name,
NameLoc: nameLoc,
}, true
}
} else if name, nameLoc, ok := isSingleIdent(second); ok {
if secondCmp, tokens := scanMediaComparison(tokens); secondCmp != css_ast.MQCmpNone {
if f, s := firstCmp.Dir(), secondCmp.Dir(); (f < 0 && s < 0) || (f > 0 && s > 0) {
if third, tokens := scanMediaValue(tokens); len(third) > 0 && len(tokens) == 0 {
return &css_ast.MQRange{
Before: first,
BeforeCmp: firstCmp,
Name: name,
NameLoc: nameLoc,
AfterCmp: secondCmp,
After: third,
}, true
}
}
}
}
}
}
}
return nil, false
}
func (p *parser) maybeSimplifyMediaNot(loc logger.Loc, inner css_ast.MediaQuery) css_ast.MediaQuery {
if p.options.minifySyntax {
switch data := inner.Data.(type) {
case *css_ast.MQNot:
// "not (not a)" => "a"
// "not (not (not a))" => "not a"
return data.Inner
case *css_ast.MQBinary:
// "not ((not a) and (not b))" => "a or b"
// "not ((not a) or (not b))" => "a and b"
terms := make([]css_ast.MediaQuery, 0, len(data.Terms))
for _, term := range data.Terms {
if not, ok := term.Data.(*css_ast.MQNot); ok {
terms = append(terms, not.Inner)
} else {
break
}
}
if len(terms) == len(data.Terms) {
data.Op ^= 1
data.Terms = terms
return inner
}
case *css_ast.MQRange:
if (data.BeforeCmp == css_ast.MQCmpNone && data.AfterCmp != css_ast.MQCmpEq) ||
(data.AfterCmp == css_ast.MQCmpNone && data.BeforeCmp != css_ast.MQCmpEq) {
data.BeforeCmp = data.BeforeCmp.Flip()
data.AfterCmp = data.AfterCmp.Flip()
return inner
}
}
}
return css_ast.MediaQuery{Loc: loc, Data: &css_ast.MQNot{Inner: inner}}
}
func isSingleIdent(tokens []css_ast.Token) (string, logger.Loc, bool) {
if len(tokens) == 1 && tokens[0].Kind == css_lexer.TIdent {
return tokens[0].Text, tokens[0].Loc, true
} else {
return "", logger.Loc{}, false
}
}
func scanMediaComparison(tokens []css_ast.Token) (css_ast.MQCmp, []css_ast.Token) {
if len(tokens) >= 1 {
switch tokens[0].Kind {
case css_lexer.TDelimEquals:
return css_ast.MQCmpEq, tokens[1:]
case css_lexer.TDelimLessThan:
// Handle "<=" or "<"
if len(tokens) >= 2 && tokens[1].Kind == css_lexer.TDelimEquals &&
((tokens[0].Whitespace&css_ast.WhitespaceAfter)|(tokens[1].Whitespace&css_ast.WhitespaceBefore)) == 0 {
return css_ast.MQCmpLe, tokens[2:]
}
return css_ast.MQCmpLt, tokens[1:]
case css_lexer.TDelimGreaterThan:
// Handle ">=" or ">"
if len(tokens) >= 2 && tokens[1].Kind == css_lexer.TDelimEquals &&
((tokens[0].Whitespace&css_ast.WhitespaceAfter)|(tokens[1].Whitespace&css_ast.WhitespaceBefore)) == 0 {
return css_ast.MQCmpGe, tokens[2:]
}
return css_ast.MQCmpGt, tokens[1:]
}
}
return css_ast.MQCmpNone, tokens
}
func scanMediaValue(tokens []css_ast.Token) ([]css_ast.Token, []css_ast.Token) {
n := 0
if len(tokens) >= 1 {
switch tokens[0].Kind {
case css_lexer.TDimension, css_lexer.TIdent:
n = 1
case css_lexer.TNumber:
// Potentially recognize a ratio which is "<number> / <number>"
if len(tokens) >= 3 && tokens[1].Kind == css_lexer.TDelimSlash && tokens[2].Kind == css_lexer.TNumber {
n = 3
} else {
n = 1
}
}
}
// Trim whitespace at the endpoints
if n > 0 {
tokens[0].Whitespace &= ^css_ast.WhitespaceBefore
tokens[n-1].Whitespace &= ^css_ast.WhitespaceAfter
}
return tokens[:n], tokens[n:]
}
@@ -0,0 +1,997 @@
package css_parser
import (
"fmt"
"strings"
"github.com/evanw/esbuild/internal/css_ast"
"github.com/evanw/esbuild/internal/css_lexer"
"github.com/evanw/esbuild/internal/logger"
)
type parseSelectorOpts struct {
composesContext *composesContext
pseudoClassKind css_ast.PseudoClassKind
isDeclarationContext bool
stopOnCloseParen bool
onlyOneComplexSelector bool
isForgivingSelectorList bool
noLeadingCombinator bool
}
func (p *parser) parseSelectorList(opts parseSelectorOpts) (list []css_ast.ComplexSelector, ok bool) {
// Potentially parse an empty list for ":is()" and ":where()"
if opts.isForgivingSelectorList && opts.stopOnCloseParen && p.peek(css_lexer.TCloseParen) {
ok = true
return
}
// Parse the first selector
sel, good := p.parseComplexSelector(parseComplexSelectorOpts{
parseSelectorOpts: opts,
isFirst: true,
})
if !good {
return
}
list = p.flattenLocalAndGlobalSelectors(list, sel)
// Parse the remaining selectors
if opts.onlyOneComplexSelector {
if t := p.current(); t.Kind == css_lexer.TComma {
p.prevError = t.Range.Loc
kind := fmt.Sprintf(":%s(...)", opts.pseudoClassKind.String())
p.log.AddIDWithNotes(logger.MsgID_CSS_CSSSyntaxError, logger.Warning, &p.tracker, t.Range,
fmt.Sprintf("Unexpected \",\" inside %q", kind),
[]logger.MsgData{{Text: fmt.Sprintf("Different CSS tools behave differently in this case, so esbuild doesn't allow it. "+
"Either remove this comma or split this selector up into multiple comma-separated %q selectors instead.", kind)}})
return
}
} else {
skip:
for {
p.eat(css_lexer.TWhitespace)
if !p.eat(css_lexer.TComma) {
break
}
p.eat(css_lexer.TWhitespace)
sel, good := p.parseComplexSelector(parseComplexSelectorOpts{
parseSelectorOpts: opts,
})
if !good {
return
}
// Omit duplicate selectors
if p.options.minifySyntax {
for _, existing := range list {
if sel.Equal(existing, nil) {
continue skip
}
}
}
list = p.flattenLocalAndGlobalSelectors(list, sel)
}
}
// Remove the leading ampersand when minifying and it can be implied:
//
// "a { & b {} }" => "a { b {} }"
//
// It can't be implied if it's not at the beginning, if there are multiple of
// them, or if the selector list is inside of a pseudo-class selector:
//
// "a { b & {} }"
// "a { & b & {} }"
// "a { :has(& b) {} }"
//
if p.options.minifySyntax && !opts.stopOnCloseParen {
for i := 1; i < len(list); i++ {
if analyzeLeadingAmpersand(list[i], opts.isDeclarationContext) != cannotRemoveLeadingAmpersand {
list[i].Selectors = list[i].Selectors[1:]
}
}
switch analyzeLeadingAmpersand(list[0], opts.isDeclarationContext) {
case canAlwaysRemoveLeadingAmpersand:
list[0].Selectors = list[0].Selectors[1:]
case canRemoveLeadingAmpersandIfNotFirst:
for i := 1; i < len(list); i++ {
if sel := list[i].Selectors[0]; len(sel.NestingSelectorLocs) == 0 && (sel.Combinator.Byte != 0 || sel.TypeSelector == nil) {
list[0].Selectors = list[0].Selectors[1:]
list[0], list[i] = list[i], list[0]
break
}
}
}
}
ok = true
return
}
func mergeCompoundSelectors(target *css_ast.CompoundSelector, source css_ast.CompoundSelector) {
// ".foo:local(&)" => "&.foo"
if len(source.NestingSelectorLocs) > 0 && len(target.NestingSelectorLocs) == 0 {
target.NestingSelectorLocs = source.NestingSelectorLocs
}
if source.TypeSelector != nil {
if target.TypeSelector == nil {
// ".foo:local(div)" => "div.foo"
target.TypeSelector = source.TypeSelector
} else {
// "div:local(span)" => "div:is(span)"
//
// Note: All other implementations of this (Lightning CSS, PostCSS, and
// Webpack) do something really weird here. They do this instead:
//
// "div:local(span)" => "divspan"
//
// But that just seems so obviously wrong that I'm not going to do that.
target.SubclassSelectors = append(target.SubclassSelectors, css_ast.SubclassSelector{
Range: source.TypeSelector.Range(),
Data: &css_ast.SSPseudoClassWithSelectorList{
Kind: css_ast.PseudoClassIs,
Selectors: []css_ast.ComplexSelector{{Selectors: []css_ast.CompoundSelector{{TypeSelector: source.TypeSelector}}}},
},
})
}
}
// ".foo:local(.bar)" => ".foo.bar"
target.SubclassSelectors = append(target.SubclassSelectors, source.SubclassSelectors...)
}
func containsLocalOrGlobalSelector(sel css_ast.ComplexSelector) bool {
for _, s := range sel.Selectors {
for _, ss := range s.SubclassSelectors {
switch pseudo := ss.Data.(type) {
case *css_ast.SSPseudoClass:
if pseudo.Name == "global" || pseudo.Name == "local" {
return true
}
case *css_ast.SSPseudoClassWithSelectorList:
if pseudo.Kind == css_ast.PseudoClassGlobal || pseudo.Kind == css_ast.PseudoClassLocal {
return true
}
}
}
}
return false
}
// This handles the ":local()" and ":global()" annotations from CSS modules
func (p *parser) flattenLocalAndGlobalSelectors(list []css_ast.ComplexSelector, sel css_ast.ComplexSelector) []css_ast.ComplexSelector {
// Only do the work to flatten the whole list if there's a ":local" or a ":global"
if p.options.symbolMode != symbolModeDisabled && containsLocalOrGlobalSelector(sel) {
var selectors []css_ast.CompoundSelector
for _, s := range sel.Selectors {
oldSubclassSelectors := s.SubclassSelectors
s.SubclassSelectors = make([]css_ast.SubclassSelector, 0, len(oldSubclassSelectors))
for _, ss := range oldSubclassSelectors {
switch pseudo := ss.Data.(type) {
case *css_ast.SSPseudoClass:
if pseudo.Name == "global" || pseudo.Name == "local" {
// Remove bare ":global" and ":local" pseudo-classes
continue
}
case *css_ast.SSPseudoClassWithSelectorList:
if pseudo.Kind == css_ast.PseudoClassGlobal || pseudo.Kind == css_ast.PseudoClassLocal {
inner := pseudo.Selectors[0].Selectors
// Replace this pseudo-class with all inner compound selectors.
// The first inner compound selector is merged with the compound
// selector before it and the last inner compound selector is
// merged with the compound selector after it:
//
// "div:local(.a .b):hover" => "div.a b:hover"
//
// This behavior is really strange since this is not how anything
// involving pseudo-classes in real CSS works at all. However, all
// other implementations (Lightning CSS, PostCSS, and Webpack) are
// consistent with this strange behavior, so we do it too.
if inner[0].Combinator.Byte == 0 {
mergeCompoundSelectors(&s, inner[0])
inner = inner[1:]
} else {
// "div:local(+ .foo):hover" => "div + .foo:hover"
}
if n := len(inner); n > 0 {
if !s.IsInvalidBecauseEmpty() {
// Don't add this selector if it consisted only of a bare ":global" or ":local"
selectors = append(selectors, s)
}
selectors = append(selectors, inner[:n-1]...)
s = inner[n-1]
}
continue
}
}
s.SubclassSelectors = append(s.SubclassSelectors, ss)
}
if !s.IsInvalidBecauseEmpty() {
// Don't add this selector if it consisted only of a bare ":global" or ":local"
selectors = append(selectors, s)
}
}
if len(selectors) == 0 {
// Treat a bare ":global" or ":local" as a bare "&" nesting selector
selectors = append(selectors, css_ast.CompoundSelector{
NestingSelectorLocs: []logger.Loc{sel.Selectors[0].Range().Loc},
WasEmptyFromLocalOrGlobal: true,
})
// Make sure we report that nesting is present so that it can be lowered
p.nestingIsPresent = true
}
sel.Selectors = selectors
}
return append(list, sel)
}
type leadingAmpersand uint8
const (
cannotRemoveLeadingAmpersand leadingAmpersand = iota
canAlwaysRemoveLeadingAmpersand
canRemoveLeadingAmpersandIfNotFirst
)
func analyzeLeadingAmpersand(sel css_ast.ComplexSelector, isDeclarationContext bool) leadingAmpersand {
if len(sel.Selectors) > 1 {
if first := sel.Selectors[0]; first.IsSingleAmpersand() {
if second := sel.Selectors[1]; second.Combinator.Byte == 0 && len(second.NestingSelectorLocs) > 0 {
// ".foo { & &.bar {} }" => ".foo { & &.bar {} }"
} else if second.Combinator.Byte != 0 || second.TypeSelector == nil || !isDeclarationContext {
// "& + div {}" => "+ div {}"
// "& div {}" => "div {}"
// ".foo { & + div {} }" => ".foo { + div {} }"
// ".foo { & + &.bar {} }" => ".foo { + &.bar {} }"
// ".foo { & :hover {} }" => ".foo { :hover {} }"
return canAlwaysRemoveLeadingAmpersand
} else {
// ".foo { & div {} }"
// ".foo { .bar, & div {} }" => ".foo { .bar, div {} }"
return canRemoveLeadingAmpersandIfNotFirst
}
}
} else {
// "& {}" => "& {}"
}
return cannotRemoveLeadingAmpersand
}
type parseComplexSelectorOpts struct {
parseSelectorOpts
isFirst bool
}
func (p *parser) parseComplexSelector(opts parseComplexSelectorOpts) (result css_ast.ComplexSelector, ok bool) {
// This is an extension: https://drafts.csswg.org/css-nesting-1/
var combinator css_ast.Combinator
if !opts.noLeadingCombinator {
combinator = p.parseCombinator()
if combinator.Byte != 0 {
p.nestingIsPresent = true
p.eat(css_lexer.TWhitespace)
}
}
// Parent
sel, good := p.parseCompoundSelector(parseComplexSelectorOpts{
parseSelectorOpts: opts.parseSelectorOpts,
isFirst: opts.isFirst,
})
if !good {
return
}
sel.Combinator = combinator
result.Selectors = append(result.Selectors, sel)
stop := css_lexer.TOpenBrace
if opts.stopOnCloseParen {
stop = css_lexer.TCloseParen
}
for {
p.eat(css_lexer.TWhitespace)
if p.peek(css_lexer.TEndOfFile) || p.peek(css_lexer.TComma) || p.peek(stop) {
break
}
// Optional combinator
combinator := p.parseCombinator()
if combinator.Byte != 0 {
p.eat(css_lexer.TWhitespace)
}
// Child
sel, good := p.parseCompoundSelector(parseComplexSelectorOpts{
parseSelectorOpts: opts.parseSelectorOpts,
})
if !good {
return
}
sel.Combinator = combinator
result.Selectors = append(result.Selectors, sel)
}
ok = true
return
}
func (p *parser) nameToken() css_ast.NameToken {
t := p.current()
return css_ast.NameToken{
Kind: t.Kind,
Range: t.Range,
Text: p.decoded(),
}
}
func (p *parser) parseCompoundSelector(opts parseComplexSelectorOpts) (sel css_ast.CompoundSelector, ok bool) {
startLoc := p.current().Range.Loc
// This is an extension: https://drafts.csswg.org/css-nesting-1/
hasLeadingNestingSelector := p.peek(css_lexer.TDelimAmpersand)
if hasLeadingNestingSelector {
p.nestingIsPresent = true
sel.NestingSelectorLocs = append(sel.NestingSelectorLocs, startLoc)
p.advance()
}
// Parse the type selector
typeSelectorLoc := p.current().Range.Loc
switch p.current().Kind {
case css_lexer.TDelimBar, css_lexer.TIdent, css_lexer.TDelimAsterisk:
nsName := css_ast.NamespacedName{}
if !p.peek(css_lexer.TDelimBar) {
nsName.Name = p.nameToken()
p.advance()
} else {
// Hack: Create an empty "identifier" to represent this
nsName.Name.Kind = css_lexer.TIdent
}
if p.eat(css_lexer.TDelimBar) {
if !p.peek(css_lexer.TIdent) && !p.peek(css_lexer.TDelimAsterisk) {
p.expect(css_lexer.TIdent)
return
}
prefix := nsName.Name
nsName.NamespacePrefix = &prefix
nsName.Name = p.nameToken()
p.advance()
}
sel.TypeSelector = &nsName
}
// Parse the subclass selectors
subclassSelectors:
for {
subclassToken := p.current()
switch subclassToken.Kind {
case css_lexer.THash:
if (subclassToken.Flags & css_lexer.IsID) == 0 {
break subclassSelectors
}
nameLoc := logger.Loc{Start: subclassToken.Range.Loc.Start + 1}
name := p.decoded()
sel.SubclassSelectors = append(sel.SubclassSelectors, css_ast.SubclassSelector{
Range: subclassToken.Range,
Data: &css_ast.SSHash{
Name: p.symbolForName(nameLoc, name),
},
})
p.advance()
case css_lexer.TDelimDot:
p.advance()
nameRange := p.current().Range
name := p.decoded()
sel.SubclassSelectors = append(sel.SubclassSelectors, css_ast.SubclassSelector{
Range: logger.Range{Loc: subclassToken.Range.Loc, Len: nameRange.End() - subclassToken.Range.Loc.Start},
Data: &css_ast.SSClass{
Name: p.symbolForName(nameRange.Loc, name),
},
})
if !p.expect(css_lexer.TIdent) {
return
}
case css_lexer.TOpenBracket:
attr, r := p.parseAttributeSelector()
if r.Len == 0 {
return
}
sel.SubclassSelectors = append(sel.SubclassSelectors, css_ast.SubclassSelector{
Range: r,
Data: &attr,
})
case css_lexer.TColon:
if p.next().Kind == css_lexer.TColon {
// Special-case the start of the pseudo-element selector section
for p.peek(css_lexer.TColon) {
firstColonLoc := p.current().Range.Loc
isElement := p.next().Kind == css_lexer.TColon
if isElement {
p.advance()
}
pseudo, r := p.parsePseudoClassSelector(firstColonLoc, isElement)
// https://www.w3.org/TR/selectors-4/#single-colon-pseudos
// The four Level 2 pseudo-elements (::before, ::after, ::first-line,
// and ::first-letter) may, for legacy reasons, be represented using
// the <pseudo-class-selector> grammar, with only a single ":"
// character at their start.
if p.options.minifySyntax && isElement {
if pseudo, ok := pseudo.(*css_ast.SSPseudoClass); ok && len(pseudo.Args) == 0 {
switch pseudo.Name {
case "before", "after", "first-line", "first-letter":
pseudo.IsElement = false
}
}
}
sel.SubclassSelectors = append(sel.SubclassSelectors, css_ast.SubclassSelector{
Range: r,
Data: pseudo,
})
}
break subclassSelectors
}
pseudo, r := p.parsePseudoClassSelector(subclassToken.Range.Loc, false)
sel.SubclassSelectors = append(sel.SubclassSelectors, css_ast.SubclassSelector{
Range: r,
Data: pseudo,
})
case css_lexer.TDelimAmpersand:
// This is an extension: https://drafts.csswg.org/css-nesting-1/
p.nestingIsPresent = true
sel.NestingSelectorLocs = append(sel.NestingSelectorLocs, subclassToken.Range.Loc)
p.advance()
default:
break subclassSelectors
}
}
// The compound selector must be non-empty
if sel.IsInvalidBecauseEmpty() {
p.unexpected()
return
}
// Note: "&div {}" was originally valid, but is now an invalid selector:
// https://github.com/w3c/csswg-drafts/issues/8662#issuecomment-1514977935.
// This is because SASS already uses that syntax to mean something very
// different, so that syntax has been removed to avoid mistakes.
if hasLeadingNestingSelector && sel.TypeSelector != nil {
r := logger.Range{Loc: typeSelectorLoc, Len: p.at(p.index-1).Range.End() - typeSelectorLoc.Start}
text := sel.TypeSelector.Name.Text
if sel.TypeSelector.NamespacePrefix != nil {
text = fmt.Sprintf("%s|%s", sel.TypeSelector.NamespacePrefix.Text, text)
}
var howToFix string
suggestion := p.source.TextForRange(r)
if opts.isFirst {
suggestion = fmt.Sprintf(":is(%s)", suggestion)
howToFix = "You can wrap this selector in \":is(...)\" as a workaround. "
} else {
r = logger.Range{Loc: startLoc, Len: r.End() - startLoc.Start}
suggestion += "&"
howToFix = "You can move the \"&\" to the end of this selector as a workaround. "
}
msg := logger.Msg{
Kind: logger.Warning,
Data: p.tracker.MsgData(r, fmt.Sprintf("Cannot use type selector %q directly after nesting selector \"&\"", text)),
Notes: []logger.MsgData{{Text: "CSS nesting syntax does not allow the \"&\" selector to come before a type selector. " +
howToFix +
"This restriction exists to avoid problems with SASS nesting, where the same syntax means something very different " +
"that has no equivalent in real CSS (appending a suffix to the parent selector)."}},
}
msg.Data.Location.Suggestion = suggestion
p.log.AddMsgID(logger.MsgID_CSS_CSSSyntaxError, msg)
return
}
// The type selector must always come first
switch p.current().Kind {
case css_lexer.TDelimBar, css_lexer.TIdent, css_lexer.TDelimAsterisk:
p.unexpected()
return
}
ok = true
return
}
func (p *parser) parseAttributeSelector() (attr css_ast.SSAttribute, r logger.Range) {
matchingLoc := p.current().Range.Loc
p.advance()
// Parse the namespaced name
switch p.current().Kind {
case css_lexer.TDelimBar, css_lexer.TDelimAsterisk:
// "[|x]"
// "[*|x]"
if p.peek(css_lexer.TDelimAsterisk) {
prefix := p.nameToken()
p.advance()
attr.NamespacedName.NamespacePrefix = &prefix
} else {
// "[|attr]" is equivalent to "[attr]". From the specification:
// "In keeping with the Namespaces in the XML recommendation, default
// namespaces do not apply to attributes, therefore attribute selectors
// without a namespace component apply only to attributes that have no
// namespace (equivalent to |attr)."
}
if !p.expect(css_lexer.TDelimBar) {
return
}
attr.NamespacedName.Name = p.nameToken()
if !p.expect(css_lexer.TIdent) {
return
}
default:
// "[x]"
// "[x|y]"
attr.NamespacedName.Name = p.nameToken()
if !p.expect(css_lexer.TIdent) {
return
}
if p.next().Kind != css_lexer.TDelimEquals && p.eat(css_lexer.TDelimBar) {
prefix := attr.NamespacedName.Name
attr.NamespacedName.NamespacePrefix = &prefix
attr.NamespacedName.Name = p.nameToken()
if !p.expect(css_lexer.TIdent) {
return
}
}
}
// Parse the optional matcher operator
p.eat(css_lexer.TWhitespace)
if p.eat(css_lexer.TDelimEquals) {
attr.MatcherOp = "="
} else {
switch p.current().Kind {
case css_lexer.TDelimTilde:
attr.MatcherOp = "~="
case css_lexer.TDelimBar:
attr.MatcherOp = "|="
case css_lexer.TDelimCaret:
attr.MatcherOp = "^="
case css_lexer.TDelimDollar:
attr.MatcherOp = "$="
case css_lexer.TDelimAsterisk:
attr.MatcherOp = "*="
}
if attr.MatcherOp != "" {
p.advance()
if !p.expect(css_lexer.TDelimEquals) {
return
}
}
}
// Parse the optional matcher value
if attr.MatcherOp != "" {
p.eat(css_lexer.TWhitespace)
if !p.peek(css_lexer.TString) && !p.peek(css_lexer.TIdent) {
p.unexpected()
}
attr.MatcherValue = p.decoded()
p.advance()
p.eat(css_lexer.TWhitespace)
if p.peek(css_lexer.TIdent) {
if modifier := p.decoded(); len(modifier) == 1 {
if c := modifier[0]; c == 'i' || c == 'I' || c == 's' || c == 'S' {
attr.MatcherModifier = c
p.advance()
}
}
}
}
closeRange := p.current().Range
if !p.expectWithMatchingLoc(css_lexer.TCloseBracket, matchingLoc) {
closeRange.Len = 0
}
r = logger.Range{Loc: matchingLoc, Len: closeRange.End() - matchingLoc.Start}
return
}
func (p *parser) parsePseudoClassSelector(loc logger.Loc, isElement bool) (css_ast.SS, logger.Range) {
p.advance()
if p.peek(css_lexer.TFunction) {
text := p.decoded()
matchingLoc := logger.Loc{Start: p.current().Range.End() - 1}
p.advance()
// Potentially parse a pseudo-class with a selector list
if !isElement {
var kind css_ast.PseudoClassKind
local := p.makeLocalSymbols
ok := true
switch text {
case "global":
kind = css_ast.PseudoClassGlobal
if p.options.symbolMode != symbolModeDisabled {
local = false
}
case "has":
kind = css_ast.PseudoClassHas
case "is":
kind = css_ast.PseudoClassIs
case "local":
kind = css_ast.PseudoClassLocal
if p.options.symbolMode != symbolModeDisabled {
local = true
}
case "not":
kind = css_ast.PseudoClassNot
case "nth-child":
kind = css_ast.PseudoClassNthChild
case "nth-last-child":
kind = css_ast.PseudoClassNthLastChild
case "nth-of-type":
kind = css_ast.PseudoClassNthOfType
case "nth-last-of-type":
kind = css_ast.PseudoClassNthLastOfType
case "where":
kind = css_ast.PseudoClassWhere
default:
ok = false
}
if ok {
old := p.index
if kind.HasNthIndex() {
p.eat(css_lexer.TWhitespace)
// Parse the "An+B" syntax
if index, ok := p.parseNthIndex(); ok {
var selectors []css_ast.ComplexSelector
// Parse the optional "of" clause
if (kind == css_ast.PseudoClassNthChild || kind == css_ast.PseudoClassNthLastChild) &&
p.peek(css_lexer.TIdent) && strings.EqualFold(p.decoded(), "of") {
p.advance()
p.eat(css_lexer.TWhitespace)
// Contain the effects of ":local" and ":global"
oldLocal := p.makeLocalSymbols
selectors, ok = p.parseSelectorList(parseSelectorOpts{
stopOnCloseParen: true,
noLeadingCombinator: true,
})
p.makeLocalSymbols = oldLocal
}
// "2n+0" => "2n"
if p.options.minifySyntax {
index.Minify()
}
// Match the closing ")"
if ok {
closeRange := p.current().Range
if !p.expectWithMatchingLoc(css_lexer.TCloseParen, matchingLoc) {
closeRange.Len = 0
}
return &css_ast.SSPseudoClassWithSelectorList{Kind: kind, Selectors: selectors, Index: index},
logger.Range{Loc: loc, Len: closeRange.End() - loc.Start}
}
}
} else {
p.eat(css_lexer.TWhitespace)
// ":local" forces local names and ":global" forces global names
oldLocal := p.makeLocalSymbols
p.makeLocalSymbols = local
selectors, ok := p.parseSelectorList(parseSelectorOpts{
pseudoClassKind: kind,
stopOnCloseParen: true,
onlyOneComplexSelector: kind == css_ast.PseudoClassGlobal || kind == css_ast.PseudoClassLocal,
isForgivingSelectorList: kind == css_ast.PseudoClassIs || kind == css_ast.PseudoClassWhere,
})
p.makeLocalSymbols = oldLocal
// Match the closing ")"
if ok {
closeRange := p.current().Range
if !p.expectWithMatchingLoc(css_lexer.TCloseParen, matchingLoc) {
closeRange.Len = 0
}
return &css_ast.SSPseudoClassWithSelectorList{Kind: kind, Selectors: selectors},
logger.Range{Loc: loc, Len: closeRange.End() - loc.Start}
}
}
p.index = old
}
}
args := p.convertTokens(p.parseAnyValue())
closeRange := p.current().Range
if !p.expectWithMatchingLoc(css_lexer.TCloseParen, matchingLoc) {
closeRange.Len = 0
}
return &css_ast.SSPseudoClass{IsElement: isElement, Name: text, Args: args},
logger.Range{Loc: loc, Len: closeRange.End() - loc.Start}
}
nameRange := p.current().Range
name := p.decoded()
sel := css_ast.SSPseudoClass{IsElement: isElement}
if p.expect(css_lexer.TIdent) {
sel.Name = name
// ":local .local_name :global .global_name {}"
// ":local { .local_name { :global { .global_name {} } }"
if p.options.symbolMode != symbolModeDisabled {
switch name {
case "local":
p.makeLocalSymbols = true
case "global":
p.makeLocalSymbols = false
}
}
} else {
nameRange.Len = 0
}
return &sel, logger.Range{Loc: loc, Len: nameRange.End() - loc.Start}
}
func (p *parser) parseAnyValue() []css_lexer.Token {
// Reference: https://drafts.csswg.org/css-syntax-3/#typedef-declaration-value
p.stack = p.stack[:0] // Reuse allocated memory
start := p.index
loop:
for {
switch p.current().Kind {
case css_lexer.TCloseParen, css_lexer.TCloseBracket, css_lexer.TCloseBrace:
last := len(p.stack) - 1
if last < 0 || !p.peek(p.stack[last]) {
break loop
}
p.stack = p.stack[:last]
case css_lexer.TSemicolon, css_lexer.TDelimExclamation:
if len(p.stack) == 0 {
break loop
}
case css_lexer.TOpenParen, css_lexer.TFunction:
p.stack = append(p.stack, css_lexer.TCloseParen)
case css_lexer.TOpenBracket:
p.stack = append(p.stack, css_lexer.TCloseBracket)
case css_lexer.TOpenBrace:
p.stack = append(p.stack, css_lexer.TCloseBrace)
case css_lexer.TEndOfFile:
break loop
}
p.advance()
}
tokens := p.tokens[start:p.index]
if len(tokens) == 0 {
p.unexpected()
}
return tokens
}
func (p *parser) parseCombinator() css_ast.Combinator {
t := p.current()
switch t.Kind {
case css_lexer.TDelimGreaterThan:
p.advance()
return css_ast.Combinator{Loc: t.Range.Loc, Byte: '>'}
case css_lexer.TDelimPlus:
p.advance()
return css_ast.Combinator{Loc: t.Range.Loc, Byte: '+'}
case css_lexer.TDelimTilde:
p.advance()
return css_ast.Combinator{Loc: t.Range.Loc, Byte: '~'}
default:
return css_ast.Combinator{}
}
}
func parseInteger(text string) (string, bool) {
n := len(text)
if n == 0 {
return "", false
}
// Trim leading zeros
start := 0
for start < n && text[start] == '0' {
start++
}
// Make sure remaining characters are digits
if start == n {
return "0", true
}
for i := start; i < n; i++ {
if c := text[i]; c < '0' || c > '9' {
return "", false
}
}
return text[start:], true
}
func (p *parser) parseNthIndex() (css_ast.NthIndex, bool) {
type sign uint8
const (
none sign = iota
negative
positive
)
// Reference: https://drafts.csswg.org/css-syntax-3/#anb-microsyntax
t0 := p.current()
text0 := p.decoded()
// Handle "even" and "odd"
if t0.Kind == css_lexer.TIdent && (text0 == "even" || text0 == "odd") {
p.advance()
p.eat(css_lexer.TWhitespace)
return css_ast.NthIndex{B: text0}, true
}
// Handle a single number
if t0.Kind == css_lexer.TNumber {
bNeg := false
if strings.HasPrefix(text0, "-") {
bNeg = true
text0 = text0[1:]
} else {
text0 = strings.TrimPrefix(text0, "+")
}
if b, ok := parseInteger(text0); ok {
if bNeg {
b = "-" + b
}
p.advance()
p.eat(css_lexer.TWhitespace)
return css_ast.NthIndex{B: b}, true
}
p.unexpected()
return css_ast.NthIndex{}, false
}
aSign := none
if p.eat(css_lexer.TDelimPlus) {
aSign = positive
t0 = p.current()
text0 = p.decoded()
}
// Everything from here must be able to contain an "n"
if t0.Kind != css_lexer.TIdent && t0.Kind != css_lexer.TDimension {
p.unexpected()
return css_ast.NthIndex{}, false
}
// Check for a leading sign
if aSign == none {
if strings.HasPrefix(text0, "-") {
aSign = negative
text0 = text0[1:]
} else {
text0 = strings.TrimPrefix(text0, "+")
}
}
// The string must contain an "n"
n := strings.IndexByte(text0, 'n')
if n < 0 {
p.unexpected()
return css_ast.NthIndex{}, false
}
// Parse the number before the "n"
var a string
if n == 0 {
if aSign == negative {
a = "-1"
} else {
a = "1"
}
} else if aInt, ok := parseInteger(text0[:n]); ok {
if aSign == negative {
aInt = "-" + aInt
}
a = aInt
} else {
p.unexpected()
return css_ast.NthIndex{}, false
}
text0 = text0[n+1:]
// Parse the stuff after the "n"
bSign := none
if strings.HasPrefix(text0, "-") {
text0 = text0[1:]
if b, ok := parseInteger(text0); ok {
p.advance()
p.eat(css_lexer.TWhitespace)
return css_ast.NthIndex{A: a, B: "-" + b}, true
}
bSign = negative
}
if text0 != "" {
p.unexpected()
return css_ast.NthIndex{}, false
}
p.advance()
p.eat(css_lexer.TWhitespace)
// Parse an optional sign delimiter
if bSign == none {
if p.eat(css_lexer.TDelimMinus) {
bSign = negative
p.eat(css_lexer.TWhitespace)
} else if p.eat(css_lexer.TDelimPlus) {
bSign = positive
p.eat(css_lexer.TWhitespace)
}
}
// Parse an optional trailing number
t1 := p.current()
text1 := p.decoded()
if t1.Kind == css_lexer.TNumber {
if bSign == none {
if strings.HasPrefix(text1, "-") {
bSign = negative
text1 = text1[1:]
} else if strings.HasPrefix(text1, "+") {
text1 = text1[1:]
}
}
if b, ok := parseInteger(text1); ok {
if bSign == negative {
b = "-" + b
}
p.advance()
p.eat(css_lexer.TWhitespace)
return css_ast.NthIndex{A: a, B: b}, true
}
}
// If there is a trailing sign, then there must also be a trailing number
if bSign != none {
p.expect(css_lexer.TNumber)
return css_ast.NthIndex{}, false
}
return css_ast.NthIndex{A: a}, true
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,605 @@
package css_parser
import (
"fmt"
"math"
"strconv"
"strings"
"github.com/evanw/esbuild/internal/css_ast"
"github.com/evanw/esbuild/internal/css_lexer"
"github.com/evanw/esbuild/internal/logger"
)
func (p *parser) tryToReduceCalcExpression(token css_ast.Token) css_ast.Token {
if term := tryToParseCalcTerm(*token.Children); term != nil {
whitespace := css_ast.WhitespaceBefore | css_ast.WhitespaceAfter
if p.options.minifyWhitespace {
whitespace = 0
}
term = term.partiallySimplify()
if result, ok := term.convertToToken(whitespace); ok {
if result.Kind == css_lexer.TOpenParen {
result.Kind = css_lexer.TFunction
result.Text = "calc"
}
result.Loc = token.Loc
result.Whitespace = css_ast.WhitespaceBefore | css_ast.WhitespaceAfter
return result
}
}
return token
}
type calcTermWithOp struct {
data calcTerm
opLoc logger.Loc
}
// See: https://www.w3.org/TR/css-values-4/#calc-internal
type calcTerm interface {
convertToToken(whitespace css_ast.WhitespaceFlags) (css_ast.Token, bool)
partiallySimplify() calcTerm
}
type calcSum struct {
terms []calcTermWithOp
}
type calcProduct struct {
terms []calcTermWithOp
}
type calcNegate struct {
term calcTermWithOp
}
type calcInvert struct {
term calcTermWithOp
}
type calcNumeric struct {
unit string
number float64
loc logger.Loc
}
type calcValue struct {
token css_ast.Token
isInvalidPlusOrMinus bool
}
func floatToStringForCalc(a float64) (string, bool) {
// Handle non-finite cases
if math.IsNaN(a) || math.IsInf(a, 0) {
return "", false
}
// Print the number as a string
text := fmt.Sprintf("%.05f", a)
for text[len(text)-1] == '0' {
text = text[:len(text)-1]
}
if text[len(text)-1] == '.' {
text = text[:len(text)-1]
}
if strings.HasPrefix(text, "0.") {
text = text[1:]
} else if strings.HasPrefix(text, "-0.") {
text = "-" + text[2:]
}
// Bail if the number is not exactly represented
if number, err := strconv.ParseFloat(text, 64); err != nil || number != a {
return "", false
}
return text, true
}
func (c *calcSum) convertToToken(whitespace css_ast.WhitespaceFlags) (css_ast.Token, bool) {
// Specification: https://www.w3.org/TR/css-values-4/#calc-serialize
tokens := make([]css_ast.Token, 0, len(c.terms)*2)
// ALGORITHM DEVIATION: Avoid parenthesizing product nodes inside sum nodes
if product, ok := c.terms[0].data.(*calcProduct); ok {
token, ok := product.convertToToken(whitespace)
if !ok {
return css_ast.Token{}, false
}
tokens = append(tokens, *token.Children...)
} else {
token, ok := c.terms[0].data.convertToToken(whitespace)
if !ok {
return css_ast.Token{}, false
}
tokens = append(tokens, token)
}
for _, term := range c.terms[1:] {
// If child is a Negate node, append " - " to s, then serialize the Negate’s child and append the result to s.
if negate, ok := term.data.(*calcNegate); ok {
token, ok := negate.term.data.convertToToken(whitespace)
if !ok {
return css_ast.Token{}, false
}
tokens = append(tokens, css_ast.Token{
Loc: term.opLoc,
Kind: css_lexer.TDelimMinus,
Text: "-",
Whitespace: css_ast.WhitespaceBefore | css_ast.WhitespaceAfter,
}, token)
continue
}
// If child is a negative numeric value, append " - " to s, then serialize the negation of child as normal and append the result to s.
if numeric, ok := term.data.(*calcNumeric); ok && numeric.number < 0 {
clone := *numeric
clone.number = -clone.number
token, ok := clone.convertToToken(whitespace)
if !ok {
return css_ast.Token{}, false
}
tokens = append(tokens, css_ast.Token{
Loc: term.opLoc,
Kind: css_lexer.TDelimMinus,
Text: "-",
Whitespace: css_ast.WhitespaceBefore | css_ast.WhitespaceAfter,
}, token)
continue
}
// Otherwise, append " + " to s, then serialize child and append the result to s.
tokens = append(tokens, css_ast.Token{
Loc: term.opLoc,
Kind: css_lexer.TDelimPlus,
Text: "+",
Whitespace: css_ast.WhitespaceBefore | css_ast.WhitespaceAfter,
})
// ALGORITHM DEVIATION: Avoid parenthesizing product nodes inside sum nodes
if product, ok := term.data.(*calcProduct); ok {
token, ok := product.convertToToken(whitespace)
if !ok {
return css_ast.Token{}, false
}
tokens = append(tokens, *token.Children...)
} else {
token, ok := term.data.convertToToken(whitespace)
if !ok {
return css_ast.Token{}, false
}
tokens = append(tokens, token)
}
}
return css_ast.Token{
Loc: tokens[0].Loc,
Kind: css_lexer.TOpenParen,
Text: "(",
Children: &tokens,
}, true
}
func (c *calcProduct) convertToToken(whitespace css_ast.WhitespaceFlags) (css_ast.Token, bool) {
// Specification: https://www.w3.org/TR/css-values-4/#calc-serialize
tokens := make([]css_ast.Token, 0, len(c.terms)*2)
token, ok := c.terms[0].data.convertToToken(whitespace)
if !ok {
return css_ast.Token{}, false
}
tokens = append(tokens, token)
for _, term := range c.terms[1:] {
// If child is an Invert node, append " / " to s, then serialize the Invert’s child and append the result to s.
if invert, ok := term.data.(*calcInvert); ok {
token, ok := invert.term.data.convertToToken(whitespace)
if !ok {
return css_ast.Token{}, false
}
tokens = append(tokens, css_ast.Token{
Loc: term.opLoc,
Kind: css_lexer.TDelimSlash,
Text: "/",
Whitespace: whitespace,
}, token)
continue
}
// Otherwise, append " * " to s, then serialize child and append the result to s.
token, ok := term.data.convertToToken(whitespace)
if !ok {
return css_ast.Token{}, false
}
tokens = append(tokens, css_ast.Token{
Loc: term.opLoc,
Kind: css_lexer.TDelimAsterisk,
Text: "*",
Whitespace: whitespace,
}, token)
}
return css_ast.Token{
Loc: tokens[0].Loc,
Kind: css_lexer.TOpenParen,
Text: "(",
Children: &tokens,
}, true
}
func (c *calcNegate) convertToToken(whitespace css_ast.WhitespaceFlags) (css_ast.Token, bool) {
// Specification: https://www.w3.org/TR/css-values-4/#calc-serialize
token, ok := c.term.data.convertToToken(whitespace)
if !ok {
return css_ast.Token{}, false
}
return css_ast.Token{
Kind: css_lexer.TOpenParen,
Text: "(",
Children: &[]css_ast.Token{
{Loc: c.term.opLoc, Kind: css_lexer.TNumber, Text: "-1"},
{Loc: c.term.opLoc, Kind: css_lexer.TDelimSlash, Text: "*", Whitespace: css_ast.WhitespaceBefore | css_ast.WhitespaceAfter},
token,
},
}, true
}
func (c *calcInvert) convertToToken(whitespace css_ast.WhitespaceFlags) (css_ast.Token, bool) {
// Specification: https://www.w3.org/TR/css-values-4/#calc-serialize
token, ok := c.term.data.convertToToken(whitespace)
if !ok {
return css_ast.Token{}, false
}
return css_ast.Token{
Kind: css_lexer.TOpenParen,
Text: "(",
Children: &[]css_ast.Token{
{Loc: c.term.opLoc, Kind: css_lexer.TNumber, Text: "1"},
{Loc: c.term.opLoc, Kind: css_lexer.TDelimSlash, Text: "/", Whitespace: css_ast.WhitespaceBefore | css_ast.WhitespaceAfter},
token,
},
}, true
}
func (c *calcNumeric) convertToToken(whitespace css_ast.WhitespaceFlags) (css_ast.Token, bool) {
text, ok := floatToStringForCalc(c.number)
if !ok {
return css_ast.Token{}, false
}
if c.unit == "" {
return css_ast.Token{
Loc: c.loc,
Kind: css_lexer.TNumber,
Text: text,
}, true
}
if c.unit == "%" {
return css_ast.Token{
Loc: c.loc,
Kind: css_lexer.TPercentage,
Text: text + "%",
}, true
}
return css_ast.Token{
Loc: c.loc,
Kind: css_lexer.TDimension,
Text: text + c.unit,
UnitOffset: uint16(len(text)),
}, true
}
func (c *calcValue) convertToToken(whitespace css_ast.WhitespaceFlags) (css_ast.Token, bool) {
t := c.token
t.Whitespace = 0
return t, true
}
func (c *calcSum) partiallySimplify() calcTerm {
// Specification: https://www.w3.org/TR/css-values-4/#calc-simplification
// For each of root’s children that are Sum nodes, replace them with their children.
terms := make([]calcTermWithOp, 0, len(c.terms))
for _, term := range c.terms {
term.data = term.data.partiallySimplify()
if sum, ok := term.data.(*calcSum); ok {
terms = append(terms, sum.terms...)
} else {
terms = append(terms, term)
}
}
// For each set of root’s children that are numeric values with identical units, remove
// those children and replace them with a single numeric value containing the sum of the
// removed nodes, and with the same unit. (E.g. combine numbers, combine percentages,
// combine px values, etc.)
for i := 0; i < len(terms); i++ {
term := terms[i]
if numeric, ok := term.data.(*calcNumeric); ok {
end := i + 1
for j := end; j < len(terms); j++ {
term2 := terms[j]
if numeric2, ok := term2.data.(*calcNumeric); ok && strings.EqualFold(numeric2.unit, numeric.unit) {
numeric.number += numeric2.number
} else {
terms[end] = term2
end++
}
}
terms = terms[:end]
}
}
// If root has only a single child at this point, return the child.
if len(terms) == 1 {
return terms[0].data
}
// Otherwise, return root.
c.terms = terms
return c
}
func (c *calcProduct) partiallySimplify() calcTerm {
// Specification: https://www.w3.org/TR/css-values-4/#calc-simplification
// For each of root’s children that are Product nodes, replace them with their children.
terms := make([]calcTermWithOp, 0, len(c.terms))
for _, term := range c.terms {
term.data = term.data.partiallySimplify()
if product, ok := term.data.(*calcProduct); ok {
terms = append(terms, product.terms...)
} else {
terms = append(terms, term)
}
}
// If root has multiple children that are numbers (not percentages or dimensions), remove
// them and replace them with a single number containing the product of the removed nodes.
for i, term := range terms {
if numeric, ok := term.data.(*calcNumeric); ok && numeric.unit == "" {
end := i + 1
for j := end; j < len(terms); j++ {
term2 := terms[j]
if numeric2, ok := term2.data.(*calcNumeric); ok && numeric2.unit == "" {
numeric.number *= numeric2.number
} else {
terms[end] = term2
end++
}
}
terms = terms[:end]
break
}
}
// If root contains only numeric values and/or Invert nodes containing numeric values,
// and multiplying the types of all the children (noting that the type of an Invert
// node is the inverse of its child’s type) results in a type that matches any of the
// types that a math function can resolve to, return the result of multiplying all the
// values of the children (noting that the value of an Invert node is the reciprocal
// of its child’s value), expressed in the result’s canonical unit.
if len(terms) == 2 {
// Right now, only handle the case of two numbers, one of which has no unit
if first, ok := terms[0].data.(*calcNumeric); ok {
if second, ok := terms[1].data.(*calcNumeric); ok {
if first.unit == "" {
second.number *= first.number
return second
}
if second.unit == "" {
first.number *= second.number
return first
}
}
}
}
// ALGORITHM DEVIATION: Divide instead of multiply if the reciprocal is shorter
for i := 1; i < len(terms); i++ {
if numeric, ok := terms[i].data.(*calcNumeric); ok {
reciprocal := 1 / numeric.number
if multiply, ok := floatToStringForCalc(numeric.number); ok {
if divide, ok := floatToStringForCalc(reciprocal); ok && len(divide) < len(multiply) {
numeric.number = reciprocal
terms[i].data = &calcInvert{term: calcTermWithOp{
data: numeric,
opLoc: terms[i].opLoc,
}}
}
}
}
}
// If root has only a single child at this point, return the child.
if len(terms) == 1 {
return terms[0].data
}
// Otherwise, return root.
c.terms = terms
return c
}
func (c *calcNegate) partiallySimplify() calcTerm {
// Specification: https://www.w3.org/TR/css-values-4/#calc-simplification
c.term.data = c.term.data.partiallySimplify()
// If root’s child is a numeric value, return an equivalent numeric value, but with the value negated (0 - value).
if numeric, ok := c.term.data.(*calcNumeric); ok {
numeric.number = -numeric.number
return numeric
}
// If root’s child is a Negate node, return the child’s child.
if negate, ok := c.term.data.(*calcNegate); ok {
return negate.term.data
}
return c
}
func (c *calcInvert) partiallySimplify() calcTerm {
// Specification: https://www.w3.org/TR/css-values-4/#calc-simplification
c.term.data = c.term.data.partiallySimplify()
// If root’s child is a number (not a percentage or dimension) return the reciprocal of the child’s value.
if numeric, ok := c.term.data.(*calcNumeric); ok && numeric.unit == "" {
numeric.number = 1 / numeric.number
return numeric
}
// If root’s child is an Invert node, return the child’s child.
if invert, ok := c.term.data.(*calcInvert); ok {
return invert.term.data
}
return c
}
func (c *calcNumeric) partiallySimplify() calcTerm {
return c
}
func (c *calcValue) partiallySimplify() calcTerm {
return c
}
func tryToParseCalcTerm(tokens []css_ast.Token) calcTerm {
// Specification: https://www.w3.org/TR/css-values-4/#calc-internal
terms := make([]calcTermWithOp, len(tokens))
for i, token := range tokens {
var term calcTerm
if token.Kind == css_lexer.TFunction && strings.EqualFold(token.Text, "var") {
// Using "var()" should bail because it can expand to any number of tokens
return nil
} else if token.Kind == css_lexer.TOpenParen || (token.Kind == css_lexer.TFunction && strings.EqualFold(token.Text, "calc")) {
term = tryToParseCalcTerm(*token.Children)
if term == nil {
return nil
}
} else if token.Kind == css_lexer.TNumber {
if number, err := strconv.ParseFloat(token.Text, 64); err == nil {
term = &calcNumeric{loc: token.Loc, number: number}
} else {
term = &calcValue{token: token}
}
} else if token.Kind == css_lexer.TPercentage {
if number, err := strconv.ParseFloat(token.PercentageValue(), 64); err == nil {
term = &calcNumeric{loc: token.Loc, number: number, unit: "%"}
} else {
term = &calcValue{token: token}
}
} else if token.Kind == css_lexer.TDimension {
if number, err := strconv.ParseFloat(token.DimensionValue(), 64); err == nil {
term = &calcNumeric{loc: token.Loc, number: number, unit: token.DimensionUnit()}
} else {
term = &calcValue{token: token}
}
} else if token.Kind == css_lexer.TIdent && strings.EqualFold(token.Text, "Infinity") {
term = &calcNumeric{loc: token.Loc, number: math.Inf(1)}
} else if token.Kind == css_lexer.TIdent && strings.EqualFold(token.Text, "-Infinity") {
term = &calcNumeric{loc: token.Loc, number: math.Inf(-1)}
} else if token.Kind == css_lexer.TIdent && strings.EqualFold(token.Text, "NaN") {
term = &calcNumeric{loc: token.Loc, number: math.NaN()}
} else {
term = &calcValue{
token: token,
// From the specification: "In addition, whitespace is required on both sides of the
// + and - operators. (The * and / operators can be used without white space around them.)"
isInvalidPlusOrMinus: i > 0 && i+1 < len(tokens) &&
(token.Kind == css_lexer.TDelimPlus || token.Kind == css_lexer.TDelimMinus) &&
(((token.Whitespace&css_ast.WhitespaceBefore) == 0 && (tokens[i-1].Whitespace&css_ast.WhitespaceAfter) == 0) ||
(token.Whitespace&css_ast.WhitespaceAfter) == 0 && (tokens[i+1].Whitespace&css_ast.WhitespaceBefore) == 0),
}
}
terms[i].data = term
}
// Collect children into Product and Invert nodes
first := 1
for first+1 < len(terms) {
// If this is a "*" or "/" operator
if value, ok := terms[first].data.(*calcValue); ok && (value.token.Kind == css_lexer.TDelimAsterisk || value.token.Kind == css_lexer.TDelimSlash) {
// Scan over the run
last := first
for last+3 < len(terms) {
if value, ok := terms[last+2].data.(*calcValue); ok && (value.token.Kind == css_lexer.TDelimAsterisk || value.token.Kind == css_lexer.TDelimSlash) {
last += 2
} else {
break
}
}
// Generate a node for the run
product := calcProduct{terms: make([]calcTermWithOp, (last-first)/2+2)}
for i := range product.terms {
term := terms[first+i*2-1]
if i > 0 {
op := terms[first+i*2-2].data.(*calcValue).token
term.opLoc = op.Loc
if op.Kind == css_lexer.TDelimSlash {
term.data = &calcInvert{term: term}
}
}
product.terms[i] = term
}
// Replace the run with a single node
terms[first-1].data = &product
terms = append(terms[:first], terms[last+2:]...)
continue
}
first++
}
// Collect children into Sum and Negate nodes
first = 1
for first+1 < len(terms) {
// If this is a "+" or "-" operator
if value, ok := terms[first].data.(*calcValue); ok && !value.isInvalidPlusOrMinus &&
(value.token.Kind == css_lexer.TDelimPlus || value.token.Kind == css_lexer.TDelimMinus) {
// Scan over the run
last := first
for last+3 < len(terms) {
if value, ok := terms[last+2].data.(*calcValue); ok && !value.isInvalidPlusOrMinus &&
(value.token.Kind == css_lexer.TDelimPlus || value.token.Kind == css_lexer.TDelimMinus) {
last += 2
} else {
break
}
}
// Generate a node for the run
sum := calcSum{terms: make([]calcTermWithOp, (last-first)/2+2)}
for i := range sum.terms {
term := terms[first+i*2-1]
if i > 0 {
op := terms[first+i*2-2].data.(*calcValue).token
term.opLoc = op.Loc
if op.Kind == css_lexer.TDelimMinus {
term.data = &calcNegate{term: term}
}
}
sum.terms[i] = term
}
// Replace the run with a single node
terms[first-1].data = &sum
terms = append(terms[:first], terms[last+2:]...)
continue
}
first++
}
// This only succeeds if everything reduces to a single term
if len(terms) == 1 {
return terms[0].data
}
return nil
}