userland: add archive tools and pinned GoAWK, esbuild, and QuickJS
This commit is contained in:
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user