1package css
2
3import (
4 "math"
5 "strconv"
6 "strings"
7)
8
9type Length struct {
10 Value float64
11 Unit string
12}
13type Color struct{ R, G, B, A float64 }
14type Shadow struct {
15 Inset bool
16 X, Y, Blur, Spread Length
17 Color Color
18}
19type Value struct {
20 Length Length
21 Color Color
22 Number float64
23 Keyword string
24 Families []string
25 Shadows []Shadow
26}
27
28func finite(s string) (float64, bool) {
29 v, e := strconv.ParseFloat(s, 64)
30 return v, e == nil && !math.IsNaN(v) && !math.IsInf(v, 0)
31}
32func length(s string, negative, auto bool) (Length, bool) {
33 s = strings.TrimSpace(strings.ToLower(s))
34 if auto && s == "auto" {
35 return Length{Unit: "auto"}, true
36 }
37 for _, unit := range []string{"rem", "em", "px", "%"} {
38 if strings.HasSuffix(s, unit) {
39 v, ok := finite(strings.TrimSuffix(s, unit))
40 return Length{v, unit}, ok && (negative || v >= 0)
41 }
42 }
43 v, ok := finite(s)
44 return Length{v, "px"}, ok && v == 0
45}
46func resolveLen(l Length, font, root float64) Length {
47 switch l.Unit {
48 case "em":
49 l.Value *= font
50 l.Unit = "px"
51 case "rem":
52 l.Value *= root
53 l.Unit = "px"
54 }
55 return l
56}
57func validLength(l Length) bool { return !math.IsNaN(l.Value) && !math.IsInf(l.Value, 0) }
58func rgb(r, g, b, a float64) Color { return Color{clamp(r), clamp(g), clamp(b), clamp(a)} }
59func clamp(x float64) float64 { return math.Max(0, math.Min(1, x)) }
60func parseColor(s string, current Color) (Color, bool) {
61 s = strings.ToLower(strings.TrimSpace(s))
62 if s == "currentcolor" {
63 return current, true
64 }
65 if s == "transparent" {
66 return Color{}, true
67 }
68 if strings.HasPrefix(s, "#") {
69 h := s[1:]
70 if len(h) != 3 && len(h) != 4 && len(h) != 6 && len(h) != 8 {
71 return Color{}, false
72 }
73 var b [4]uint64
74 b[3] = 255
75 short := len(h) <= 4
76 n := 3
77 if len(h) == 4 || len(h) == 8 {
78 n = 4
79 }
80 for i := 0; i < n; i++ {
81 part := ""
82 if short {
83 part = strings.Repeat(string(h[i]), 2)
84 } else {
85 part = h[i*2 : i*2+2]
86 }
87 v, e := strconv.ParseUint(part, 16, 8)
88 if e != nil {
89 return Color{}, false
90 }
91 b[i] = v
92 }
93 return rgb(float64(b[0])/255, float64(b[1])/255, float64(b[2])/255, float64(b[3])/255), true
94 }
95 open := strings.IndexByte(s, '(')
96 if open < 0 || !strings.HasSuffix(s, ")") {
97 if v, ok := namedColors[s]; ok {
98 return v, true
99 }
100 return Color{}, false
101 }
102 fn := s[:open]
103 if fn != "rgb" && fn != "rgba" && fn != "hsl" && fn != "hsla" {
104 return Color{}, false
105 }
106 inner := strings.TrimSpace(s[open+1 : len(s)-1])
107 comma := strings.Contains(inner, ",")
108 var args []string
109 if comma {
110 args = strings.Split(inner, ",")
111 } else {
112 inner = strings.ReplaceAll(inner, "/", " / ")
113 args = strings.Fields(inner)
114 }
115 for i := range args {
116 args[i] = strings.TrimSpace(args[i])
117 }
118 if !comma && len(args) == 5 && args[3] == "/" {
119 args = append(args[:3], args[4])
120 }
121 if len(args) < 3 || len(args) > 4 || comma && strings.Contains(inner, "/") {
122 return Color{}, false
123 }
124 if (fn == "rgba" || fn == "hsla") && len(args) != 4 {
125 return Color{}, false
126 }
127 a := 1.0
128 if len(args) == 4 {
129 v, ok := channel(args[3], 1)
130 if !ok {
131 return Color{}, false
132 }
133 a = v
134 }
135 if fn == "rgb" || fn == "rgba" {
136 var ch [3]float64
137 for i := 0; i < 3; i++ {
138 v, ok := channel(args[i], 255)
139 if !ok {
140 return Color{}, false
141 }
142 ch[i] = v
143 }
144 return rgb(ch[0], ch[1], ch[2], a), true
145 }
146 hue := strings.TrimSuffix(args[0], "deg")
147 h, ok := finite(hue)
148 if !ok {
149 return Color{}, false
150 }
151 sat, ok := percent(args[1])
152 if !ok {
153 return Color{}, false
154 }
155 light, ok := percent(args[2])
156 if !ok {
157 return Color{}, false
158 }
159 h = math.Mod(h, 360)
160 if h < 0 {
161 h += 360
162 }
163 sat = clamp(sat)
164 light = clamp(light)
165 c := (1 - math.Abs(2*light-1)) * sat
166 x := c * (1 - math.Abs(math.Mod(h/60, 2)-1))
167 m := light - c/2
168 var r, g, b float64
169 switch {
170 case h < 60:
171 r, g = c, x
172 case h < 120:
173 r, g = x, c
174 case h < 180:
175 g, b = c, x
176 case h < 240:
177 g, b = x, c
178 case h < 300:
179 r, b = x, c
180 default:
181 r, b = c, x
182 }
183 return rgb(r+m, g+m, b+m, a), true
184}
185func percent(s string) (float64, bool) {
186 if !strings.HasSuffix(s, "%") {
187 return 0, false
188 }
189 v, ok := finite(strings.TrimSuffix(s, "%"))
190 return v / 100, ok
191}
192func channel(s string, scale float64) (float64, bool) {
193 if strings.HasSuffix(s, "%") {
194 return percent(s)
195 }
196 v, ok := finite(s)
197 return v / scale, ok
198}
199
200// splitTopLevel separates commas only outside functions and quoted strings.
201func splitTopLevel(s string) []string {
202 var parts []string
203 start, depth := 0, 0
204 pos := 0
205 for _, t := range Tokenize(s) {
206 if t.Kind == "," && depth == 0 {
207 parts = append(parts, s[start:pos])
208 start = pos + len(t.Raw)
209 }
210 if t.Kind == "function" || t.Kind == "(" {
211 depth++
212 }
213 if t.Kind == ")" {
214 depth--
215 }
216 pos += len(t.Raw)
217 }
218 parts = append(parts, s[start:])
219 return parts
220}
221
222func fields(s string) []string {
223 ts := Tokenize(s)
224 var parts []string
225 var buf strings.Builder
226 depth := 0
227 for _, t := range ts {
228 if t.Kind == "function" {
229 depth++
230 }
231 if t.Kind == ")" {
232 depth--
233 }
234 if t.Kind == "space" && depth == 0 {
235 if buf.Len() > 0 {
236 parts = append(parts, buf.String())
237 buf.Reset()
238 }
239 continue
240 }
241 buf.WriteString(t.Raw)
242 }
243 if buf.Len() > 0 {
244 parts = append(parts, buf.String())
245 }
246 return parts
247}
248func sides(xs []string) ([]string, bool) {
249 if len(xs) < 1 || len(xs) > 4 {
250 return nil, false
251 }
252 switch len(xs) {
253 case 1:
254 return []string{xs[0], xs[0], xs[0], xs[0]}, true
255 case 2:
256 return []string{xs[0], xs[1], xs[0], xs[1]}, true
257 case 3:
258 return []string{xs[0], xs[1], xs[2], xs[1]}, true
259 default:
260 return xs, true
261 }
262}
263
264var sideNames = []string{"top", "right", "bottom", "left"}
265var cornerNames = []string{"top-left", "top-right", "bottom-right", "bottom-left"}
266
267func expand(d Declaration) ([]Declaration, bool) {
268 n := d.Property
269 makeD := func(name, value string) Declaration { return Declaration{name, value, d.Important, d.Line, d.Col} }
270 v := d.Value
271 if n == "background" {
272 return []Declaration{makeD("background-color", v)}, true
273 }
274 for _, prefix := range []string{"margin", "padding", "border-width", "border-style", "border-color", "border-radius"} {
275 if n == prefix {
276 xs, ok := sides(fields(v))
277 if !ok {
278 return nil, false
279 }
280 var ds []Declaration
281 for i, x := range xs {
282 name := prefix + "-" + sideNames[i]
283 if prefix == "border-width" || prefix == "border-style" || prefix == "border-color" {
284 name = "border-" + sideNames[i] + "-" + strings.TrimPrefix(prefix, "border-")
285 }
286 if prefix == "border-radius" {
287 name = "border-" + cornerNames[i] + "-radius"
288 }
289 ds = append(ds, makeD(name, x))
290 }
291 return ds, true
292 }
293 }
294 if n == "gap" {
295 xs := fields(v)
296 if len(xs) < 1 || len(xs) > 2 {
297 return nil, false
298 }
299 if len(xs) == 1 {
300 xs = append(xs, xs[0])
301 }
302 return []Declaration{makeD("row-gap", xs[0]), makeD("column-gap", xs[1])}, true
303 }
304 if n == "overflow" {
305 xs := fields(v)
306 if len(xs) < 1 || len(xs) > 2 {
307 return nil, false
308 }
309 if len(xs) == 1 {
310 xs = append(xs, xs[0])
311 }
312 return []Declaration{makeD("overflow-x", xs[0]), makeD("overflow-y", xs[1])}, true
313 }
314 if n == "flex" {
315 xs := fields(v)
316 if v == "none" {
317 xs = []string{"0", "0", "auto"}
318 } else if v == "auto" {
319 xs = []string{"1", "1", "auto"}
320 } else {
321 if len(xs) < 1 || len(xs) > 3 {
322 return nil, false
323 }
324 if _, ok := finite(xs[0]); !ok {
325 return nil, false
326 }
327 if len(xs) == 1 {
328 xs = append(xs, "1", "0%")
329 } else if len(xs) == 2 {
330 if _, ok := finite(xs[1]); ok {
331 xs = append(xs, "0%")
332 } else {
333 xs = []string{xs[0], "1", xs[1]}
334 }
335 }
336 }
337 return []Declaration{makeD("flex-grow", xs[0]), makeD("flex-shrink", xs[1]), makeD("flex-basis", xs[2])}, true
338 }
339 if n == "border" || n == "outline" || strings.HasPrefix(n, "border-") && (n == "border-top" || n == "border-right" || n == "border-bottom" || n == "border-left") {
340 xs := fields(v)
341 width, style, color := "0", "none", "currentColor"
342 used := [3]bool{}
343 for _, x := range xs {
344 if _, ok := length(x, false, false); ok && !used[0] {
345 width = x
346 used[0] = true
347 continue
348 }
349 if (x == "solid" || x == "none") && !used[1] {
350 style = x
351 used[1] = true
352 continue
353 }
354 if _, ok := parseColor(x, Color{}); ok && !used[2] {
355 color = x
356 used[2] = true
357 continue
358 }
359 return nil, false
360 }
361 if len(xs) == 0 {
362 return nil, false
363 }
364 targets := []string{n}
365 if n == "border" {
366 targets = []string{"border-top", "border-right", "border-bottom", "border-left"}
367 }
368 var ds []Declaration
369 for _, t := range targets {
370 ds = append(ds, makeD(t+"-width", width), makeD(t+"-style", style), makeD(t+"-color", color))
371 }
372 return ds, true
373 }
374 return []Declaration{d}, true
375}
376func inherited(name string) bool {
377 switch name {
378 case "color", "font-family", "font-size", "font-weight", "font-style", "line-height", "text-align", "letter-spacing", "cursor", "accent-color":
379 return true
380 }
381 return false
382}
383func initial(name string) string {
384 switch name {
385 case "color":
386 return "black"
387 case "accent-color":
388 return "#3584e4"
389 case "opacity", "flex-shrink":
390 return "1"
391 case "background-color":
392 return "transparent"
393 case "box-shadow":
394 return "none"
395 case "width", "height", "min-width", "min-height", "max-width", "max-height", "flex-basis", "align-self", "cursor":
396 return "auto"
397 case "box-sizing":
398 return "content-box"
399 case "display":
400 return "block"
401 case "flex-direction":
402 return "row"
403 case "flex-wrap":
404 return "nowrap"
405 case "justify-content":
406 return "flex-start"
407 case "align-items":
408 return "stretch"
409 case "overflow-x", "overflow-y":
410 return "visible"
411 case "font-family":
412 return "sans-serif"
413 case "font-size":
414 return "16px"
415 case "font-weight":
416 return "400"
417 case "font-style", "letter-spacing", "line-height":
418 return "normal"
419 case "text-align":
420 return "start"
421 }
422 if strings.HasSuffix(name, "-color") {
423 return "currentColor"
424 }
425 if strings.HasSuffix(name, "-style") {
426 return "none"
427 }
428 return "0"
429}
430func parseValue(name, s string, current Color, font, root float64) (Value, bool) {
431 var out Value
432 s = strings.TrimSpace(s)
433 lower := strings.ToLower(s)
434 if name == "color" || name == "background-color" || strings.HasSuffix(name, "-color") {
435 var ok bool
436 out.Color, ok = parseColor(s, current)
437 return out, ok
438 }
439 switch name {
440 case "opacity", "flex-grow", "flex-shrink":
441 v, ok := finite(s)
442 if !ok || v < 0 || name == "opacity" && v > 1 {
443 return out, false
444 }
445 out.Number = v
446 return out, true
447 case "font-weight":
448 if lower == "normal" {
449 out.Number = 400
450 return out, true
451 }
452 if lower == "bold" {
453 out.Number = 700
454 return out, true
455 }
456 v, ok := finite(s)
457 if ok && v >= 100 && v <= 900 && math.Mod(v, 100) == 0 {
458 out.Number = v
459 return out, true
460 }
461 return out, false
462 case "font-family":
463 for _, p := range strings.Split(s, ",") {
464 p = strings.TrimSpace(p)
465 if p == "" {
466 return out, false
467 }
468 if strings.HasPrefix(p, "\"") || strings.HasPrefix(p, "'") {
469 ts := Tokenize(p)
470 if len(ts) != 1 || ts[0].Kind != "string" {
471 return out, false
472 }
473 p = ts[0].Value
474 }
475 out.Families = append(out.Families, p)
476 }
477 return out, len(out.Families) > 0
478 case "box-shadow":
479 if lower == "none" {
480 return out, true
481 }
482 for _, part := range splitTopLevel(s) {
483 xs := fields(part)
484 sh := Shadow{Color: current}
485 var ls []Length
486 for _, x := range xs {
487 if x == "inset" && !sh.Inset {
488 sh.Inset = true
489 continue
490 }
491 if l, ok := length(x, true, false); ok {
492 ls = append(ls, resolveLen(l, font, root))
493 continue
494 }
495 if c, ok := parseColor(x, current); ok {
496 sh.Color = c
497 continue
498 }
499 return out, false
500 }
501 if len(ls) < 2 || len(ls) > 4 {
502 return out, false
503 }
504 for _, l := range ls {
505 if !validLength(l) {
506 return out, false
507 }
508 }
509 sh.X, sh.Y = ls[0], ls[1]
510 if len(ls) > 2 {
511 sh.Blur = ls[2]
512 if sh.Blur.Value < 0 {
513 return out, false
514 }
515 }
516 if len(ls) > 3 {
517 sh.Spread = ls[3]
518 }
519 out.Shadows = append(out.Shadows, sh)
520 }
521 return out, true
522 case "font-size":
523 l, ok := length(s, false, false)
524 if !ok {
525 return out, false
526 }
527 switch l.Unit {
528 case "%":
529 l.Value = font * l.Value / 100
530 l.Unit = "px"
531 case "em":
532 l.Value *= font
533 l.Unit = "px"
534 case "rem":
535 l.Value *= root
536 l.Unit = "px"
537 }
538 out.Length = l
539 return out, validLength(l)
540 case "line-height":
541 if lower == "normal" {
542 out.Keyword = lower
543 return out, true
544 }
545 if v, ok := finite(s); ok && v >= 0 {
546 out.Number = v
547 out.Keyword = "number"
548 return out, true
549 }
550 }
551 lengthName := false
552 negative := false
553 auto := false
554 switch {
555 case name == "width" || name == "height" || name == "min-width" || name == "min-height" || name == "max-width" || name == "max-height" || name == "flex-basis":
556 lengthName = true
557 auto = true
558 case name == "outline-offset" || name == "letter-spacing" || strings.HasPrefix(name, "margin-"):
559 lengthName = true
560 negative = true
561 auto = strings.HasPrefix(name, "margin-")
562 case name == "font-size" || name == "line-height" || name == "row-gap" || name == "column-gap" || strings.HasPrefix(name, "padding-") || strings.HasSuffix(name, "-width") || strings.HasSuffix(name, "-radius"):
563 lengthName = true
564 }
565 if name == "letter-spacing" && lower == "normal" {
566 out.Keyword = lower
567 return out, true
568 }
569 if lengthName {
570 l, ok := length(s, negative, auto)
571 if name == "outline-offset" || name == "letter-spacing" {
572 ok = ok && l.Unit != "%"
573 }
574 out.Length = resolveLen(l, font, root)
575 return out, ok && validLength(out.Length)
576 }
577 valid := map[string]string{"box-sizing": "content-box border-box", "display": "flex block none", "flex-direction": "row column", "flex-wrap": "nowrap wrap", "justify-content": "flex-start flex-end center space-between space-around space-evenly", "align-items": "stretch flex-start flex-end center", "align-self": "auto stretch flex-start flex-end center", "overflow-x": "visible hidden scroll auto", "overflow-y": "visible hidden scroll auto", "font-style": "normal italic oblique", "text-align": "start end left right center", "cursor": "auto default pointer text not-allowed"}
578 if name == "outline-style" || strings.HasPrefix(name, "border-") && strings.HasSuffix(name, "-style") {
579 valid[name] = "none solid"
580 }
581 for _, v := range strings.Fields(valid[name]) {
582 if lower == v {
583 out.Keyword = v
584 return out, true
585 }
586 }
587 return out, false
588}
589func containsVar(s string) bool {
590 for _, t := range Tokenize(s) {
591 if t.Kind == "function" && strings.EqualFold(t.Value, "var") {
592 return true
593 }
594 }
595 return false
596}
597
598func resolveVars(s string, custom map[string]string, active map[string]bool, depth int) (string, bool) {
599 if strings.Contains(s, "/*") {
600 s = raw(Tokenize(s))
601 }
602 if depth > 64 {
603 return "", false
604 }
605 var out strings.Builder
606 for {
607 // Function tokens, unlike byte substrings, cannot occur inside strings or identifiers.
608 ts := Tokenize(s)
609 start, end, pos, depth, functionLen := -1, -1, 0, 0, 0
610 for _, t := range ts {
611 if start < 0 {
612 if t.Kind == "function" && strings.EqualFold(t.Value, "var") {
613 start = pos + len(t.Raw)
614 functionLen = len(t.Raw)
615 }
616 } else {
617 if t.Kind == "function" || t.Kind == "(" {
618 depth++
619 }
620 if t.Kind == ")" {
621 if depth == 0 {
622 end = pos
623 break
624 }
625 depth--
626 }
627 }
628 pos += len(t.Raw)
629 }
630 if start < 0 {
631 out.WriteString(s)
632 return out.String(), true
633 }
634 if end < 0 {
635 return "", false
636 }
637 out.WriteString(s[:start-functionLen])
638 inside := s[start:end]
639 s = s[end+1:]
640 parts := splitTopLevel(inside)
641 if len(parts) > 2 {
642 parts[1] = strings.Join(parts[1:], ",")
643 }
644 name := strings.TrimSpace(parts[0])
645 if !strings.HasPrefix(name, "--") {
646 return "", false
647 }
648 v, ok := custom[name]
649 if ok && !active[name] {
650 active[name] = true
651 v, ok = resolveVars(v, custom, active, depth+1)
652 delete(active, name)
653 } else {
654 ok = false
655 }
656 if !ok && len(parts) > 1 {
657 v, ok = resolveVars(strings.TrimSpace(parts[1]), custom, active, depth+1)
658 }
659 if !ok {
660 return "", false
661 }
662 out.WriteString(v)
663 }
664}