1package css
2
3import (
4 _ "embed"
5 "strings"
6)
7
8//go:embed ua.css
9var uaSource string
10var uaSheet = Parse(uaSource)
11
12// UA returns the embedded user-agent sheet. Treat the returned sheet as immutable.
13func UA() Sheet { return uaSheet }
14
15// Declarations are parsed inline CSS. Do not modify them after passing them to Compute.
16type Declarations struct {
17 compiled []compiledDecl
18}
19
20// ParseInline parses a declaration list using the stylesheet declaration grammar.
21func ParseInline(src string) (*Declarations, []Diagnostic) {
22 s := Parse("app{" + src + "}")
23 d := &Declarations{}
24 if len(s.Rules) > 0 {
25 d.compiled = compileDecls(s.Rules[0].Declarations)
26 }
27 for i := range s.Diagnostics {
28 if s.Diagnostics[i].Line == 1 {
29 s.Diagnostics[i].Col -= len("app{")
30 }
31 }
32 return d, s.Diagnostics
33}
34
35type compiledDecl struct {
36 property propertyID
37 custom string
38 text string
39 value Value
40 parsed bool
41 raw []Token
42 group *pendingShorthand
43 important bool
44 order int
45}
46
47// One pending substitution is shared by all of a shorthand's longhand winners.
48// Expanding and validating together prevents partial computed values.
49type pendingShorthand struct {
50 property string
51 text string
52 targets []propertyID
53}
54
55type compiledRule struct {
56 selector Selector
57 decls []compiledDecl
58 origin int
59 right compound
60}
61type candidate struct {
62 decl *compiledDecl
63 spec Specificity
64 origin, order int
65}
66
67func better(a, b candidate) bool {
68 if b.decl == nil {
69 return true
70 }
71 if a.decl.important != b.decl.important {
72 return a.decl.important
73 }
74 if a.origin != b.origin {
75 return a.origin > b.origin
76 }
77 if a.spec != b.spec {
78 return a.spec.greater(b.spec)
79 }
80 return a.order >= b.order
81}
82
83type memoKey struct {
84 parent *memoNode
85 typ, id, class uint32
86 state StateFlags
87 inline *Declarations
88}
89type memoNode struct {
90 key memoKey
91 style *Style
92 custom map[string]string
93 root float64
94 used uint64
95}
96
97// Computed holds both the typed style and the identity needed for descendant matching.
98type Computed struct {
99 Style *Style
100 node *memoNode
101}
102
103// Engine is single-goroutine only; neither Compute nor EndFrame is synchronized.
104// Compiled rules never change. Compile a new Engine when CSS changes.
105type Engine struct {
106 ids map[string]uint32
107 sets map[uint64][]classSet
108 classMembers map[uint32][]uint32
109 nextID, nextSet uint32
110 usedIDs, usedSets map[uint32]bool
111 rules []compiledRule
112 byID, byClass, byType map[string][]int
113 universal []int
114 memo map[memoKey]*Computed
115 scratch []*memoNode
116 generation uint64
117}
118type classSet struct {
119 id uint32
120 members []uint32
121}
122
123// Compile indexes the rightmost compound of every selector and freezes the sheets.
124func Compile(ua, author Sheet) *Engine {
125 en := &Engine{ids: make(map[string]uint32), sets: make(map[uint64][]classSet), classMembers: make(map[uint32][]uint32), usedIDs: make(map[uint32]bool), usedSets: make(map[uint32]bool), byID: make(map[string][]int), byClass: make(map[string][]int), byType: make(map[string][]int), memo: make(map[memoKey]*Computed)}
126 order := 0
127 for origin, sheet := range []Sheet{ua, author} {
128 for _, r := range sheet.Rules {
129 ds := compileDecls(r.Declarations)
130 for i := range ds {
131 ds[i].order += order
132 }
133 order += len(r.Declarations)
134 for _, sel := range r.Selectors {
135 sel.parts = append([]compound(nil), sel.parts...)
136 for pi := range sel.parts {
137 part := &sel.parts[pi]
138 part.typID = en.intern(part.typ)
139 part.idID = en.intern(part.id)
140 for _, cl := range part.classes {
141 part.classIDs = append(part.classIDs, en.intern(cl))
142 }
143 for _, pseudo := range part.pseudos {
144 part.state |= pseudoFlag(pseudo)
145 }
146 }
147 i := len(en.rules)
148 en.rules = append(en.rules, compiledRule{selector: sel, decls: ds, origin: origin, right: sel.parts[len(sel.parts)-1]})
149 right := sel.parts[len(sel.parts)-1]
150 switch {
151 case right.id != "":
152 en.byID[right.id] = append(en.byID[right.id], i)
153 case len(right.classes) > 0:
154 en.byClass[right.classes[0]] = append(en.byClass[right.classes[0]], i)
155 case right.typ != "":
156 en.byType[right.typ] = append(en.byType[right.typ], i)
157 default:
158 en.universal = append(en.universal, i)
159 }
160 }
161 }
162 }
163 return en
164}
165func (en *Engine) intern(s string) uint32 {
166 if s == "" {
167 return 0
168 }
169 if id := en.ids[s]; id != 0 {
170 return id
171 }
172 en.nextID++
173 id := en.nextID
174 en.ids[s] = id
175 return id
176}
177func hashClasses(xs []string) uint64 {
178 var sum uint64
179 for _, x := range xs {
180 h := uint64(14695981039346656037)
181 for i := 0; i < len(x); i++ {
182 h ^= uint64(x[i])
183 h *= 1099511628211
184 }
185 sum += h * 0x9e3779b97f4a7c15
186 }
187 return sum
188}
189func (en *Engine) classes(xs []string) uint32 {
190 if len(xs) == 0 {
191 return 0
192 }
193 h := hashClasses(xs)
194 for _, set := range en.sets[h] {
195 if len(set.members) != len(xs) {
196 continue
197 }
198 ok := true
199 for _, x := range xs {
200 id := en.intern(x)
201 found := false
202 for _, v := range set.members {
203 if v == id {
204 found = true
205 break
206 }
207 }
208 if !found {
209 ok = false
210 break
211 }
212 }
213 if ok {
214 return set.id
215 }
216 }
217 en.nextSet++
218 id := en.nextSet
219 s := classSet{id: id, members: make([]uint32, 0, len(xs))}
220 for _, x := range xs {
221 s.members = append(s.members, en.intern(x))
222 }
223 en.sets[h] = append(en.sets[h], s)
224 en.classMembers[id] = s.members
225 return id
226}
227func pseudoFlag(name string) StateFlags {
228 switch name {
229 case "hover":
230 return Hover
231 case "active":
232 return Active
233 case "focus":
234 return Focus
235 case "focus-visible":
236 return FocusVisible
237 case "disabled":
238 return Disabled
239 case "checked":
240 return Checked
241 }
242 return 0
243}
244func (en *Engine) matchPart(p compound, k memoKey) bool {
245 if p.typID != 0 && p.typID != k.typ || p.idID != 0 && p.idID != k.id || p.state&k.state != p.state {
246 return false
247 }
248 for _, id := range p.classIDs {
249 found := false
250 for _, member := range en.classMembers[k.class] {
251 if member == id {
252 found = true
253 break
254 }
255 }
256 if !found {
257 return false
258 }
259 }
260 return true
261}
262func (en *Engine) matchesAncestors(s Selector, ancestor *memoNode) bool {
263 for i := len(s.parts) - 1; i > 0; {
264 comb := s.parts[i].combinator
265 i--
266 if comb == ">" {
267 if ancestor == nil || !en.matchPart(s.parts[i], ancestor.key) {
268 return false
269 }
270 ancestor = ancestor.key.parent
271 } else {
272 for ancestor != nil && !en.matchPart(s.parts[i], ancestor.key) {
273 ancestor = ancestor.key.parent
274 }
275 if ancestor == nil {
276 return false
277 }
278 ancestor = ancestor.key.parent
279 }
280 }
281 return true
282}
283
284// Compute returns the same *Computed on memo hits. parent must come from this Engine.
285func (en *Engine) Compute(e *Element, parent *Computed) *Computed {
286 var p *memoNode
287 if parent != nil {
288 p = parent.node
289 }
290 typ := strings.ToLower(e.Type)
291 k := memoKey{parent: p, typ: en.intern(typ), id: en.intern(e.ID), class: en.classes(e.Classes), state: e.State, inline: e.Inline}
292 if c := en.memo[k]; c != nil {
293 c.node.used = en.generation
294 return c
295 }
296 n := &memoNode{key: k, used: en.generation}
297 var winners [propertyCount]candidate
298 customWinners := map[string]candidate{}
299 visit := func(indices []int) {
300 for _, idx := range indices {
301 r := &en.rules[idx]
302 if !en.matchPart(r.right, k) || len(r.selector.parts) > 1 && !en.matchesAncestors(r.selector, k.parent) {
303 continue
304 }
305 for di := range r.decls {
306 d := &r.decls[di]
307 c := candidate{decl: d, spec: r.selector.Specificity, origin: r.origin, order: d.order}
308 if d.custom != "" {
309 if better(c, customWinners[d.custom]) {
310 customWinners[d.custom] = c
311 }
312 } else if better(c, winners[d.property]) {
313 winners[d.property] = c
314 }
315 }
316 }
317 }
318 visit(en.universal)
319 visit(en.byType[typ])
320 visit(en.byID[e.ID])
321 for _, cl := range e.Classes {
322 visit(en.byClass[cl])
323 }
324 if e.Inline != nil {
325 for i := range e.Inline.compiled {
326 d := &e.Inline.compiled[i]
327 c := candidate{decl: d, origin: 2, order: d.order}
328 if d.custom != "" {
329 if better(c, customWinners[d.custom]) {
330 customWinners[d.custom] = c
331 }
332 } else if better(c, winners[d.property]) {
333 winners[d.property] = c
334 }
335 }
336 }
337 if p != nil {
338 n.custom = p.custom
339 n.root = p.root
340 }
341 if len(customWinners) > 0 {
342 n.custom = make(map[string]string, len(customWinners)+len(n.custom))
343 if p != nil {
344 for key, value := range p.custom {
345 n.custom[key] = value
346 }
347 }
348 for key, c := range customWinners {
349 n.custom[key] = c.decl.text
350 }
351 }
352 style := new(Style)
353 *style = initialStyle
354 if p != nil {
355 style.Color = p.style.Color
356 style.FontFamily = p.style.FontFamily
357 style.FontSize = p.style.FontSize
358 style.FontWeight = p.style.FontWeight
359 style.FontStyle = p.style.FontStyle
360 style.LineHeight = p.style.LineHeight
361 style.LineHeightNumber = p.style.LineHeightNumber
362 style.LineHeightKind = p.style.LineHeightKind
363 style.TextAlign = p.style.TextAlign
364 style.LetterSpacing = p.style.LetterSpacing
365 style.LetterSpacingNormal = p.style.LetterSpacingNormal
366 style.Cursor = p.style.Cursor
367 style.AccentColor = p.style.AccentColor
368 }
369 pending := map[*pendingShorthand]shorthandResult{}
370 compute := func(id propertyID, font, root float64, current Color) Value {
371 name := propertyNames[id]
372 c := winners[id]
373 if c.decl == nil {
374 if isInheritedID(id) && p != nil {
375 return p.style.value(id)
376 }
377 if id == p_color {
378 return Value{Color: Color{A: 1}}
379 }
380 if isCurrentColorDefault(id) {
381 return Value{Color: current}
382 }
383 return defaultValues[id]
384 }
385 text := c.decl.text
386 if text == "inherit" || text == "unset" && isInheritedID(id) {
387 if p != nil {
388 return p.style.value(id)
389 }
390 text = initial(name)
391 } else if text == "initial" || text == "unset" {
392 if id != p_color && id != p_font_size {
393 if isCurrentColorDefault(id) {
394 return Value{Color: current}
395 }
396 return defaultValues[id]
397 }
398 text = initial(name)
399 }
400 if len(c.decl.raw) > 0 {
401 var ok bool
402 if c.decl.group != nil {
403 g := c.decl.group
404 if result, seen := pending[g]; seen {
405 ok = result.ok
406 if ok {
407 text = result.texts[id]
408 }
409 } else {
410 result := resolveShorthand(g, n.custom, current, font, root)
411 pending[g] = result
412 ok = result.ok
413 if ok {
414 text = result.texts[id]
415 }
416 }
417 } else {
418 text, ok = resolveVars(text, n.custom, map[string]bool{}, 0)
419 }
420
421 if !ok {
422 if inherited(name) && p != nil {
423 return p.style.value(id)
424 }
425 text = initial(name)
426 }
427 }
428 if text != c.decl.text {
429 if text == "inherit" || text == "unset" && isInheritedID(id) {
430 if p != nil {
431 return p.style.value(id)
432 }
433 text = initial(name)
434 }
435 if text == "initial" || text == "unset" {
436 text = initial(name)
437 }
438 }
439 if text != c.decl.text && text == initial(name) && id != p_font_size && id != p_color {
440 if isCurrentColorDefault(id) {
441 return Value{Color: current}
442 }
443 return defaultValues[id]
444 }
445 if c.decl.parsed && text == c.decl.text {
446 if staticIndependent(name, text) {
447 return c.decl.value
448 }
449 if strings.EqualFold(text, "currentcolor") && (id == p_color || isCurrentColorDefault(id)) {
450 return Value{Color: current}
451 }
452 }
453 v, ok := parseValue(name, text, current, font, root)
454 if !ok {
455 if inherited(name) && p != nil {
456 return p.style.value(id)
457 }
458 if isCurrentColorDefault(id) {
459 return Value{Color: current}
460 }
461 return defaultValues[id]
462 }
463 return v
464 }
465 font, root := 16.0, 16.0
466 current := Color{A: 1}
467 if p != nil {
468 font = p.style.FontSize.Value
469 root = p.root
470 current = p.style.Color
471 }
472 if winners[p_font_size].decl != nil {
473 style.set(p_font_size, compute(p_font_size, font, root, current))
474 }
475 font = style.FontSize.Value
476 if p == nil {
477 root = font
478 }
479 n.root = root
480 if winners[p_color].decl != nil {
481 style.set(p_color, compute(p_color, font, root, current))
482 }
483 current = style.Color
484 style.BorderColor = ColorSides{current, current, current, current}
485 style.OutlineColor = current
486 for id := propertyID(0); id < propertyCount; id++ {
487 if id != p_color && id != p_font_size && winners[id].decl != nil {
488 style.set(id, compute(id, font, root, current))
489 }
490 }
491 n.style = style
492
493 c := &Computed{Style: style, node: n}
494 en.memo[k] = c
495 return c
496}
497
498// EndFrame advances the generation and drops entries not used in the last two frames.
499func (en *Engine) EndFrame() {
500 en.generation++
501 if en.generation < 2 {
502 return
503 }
504 // Each recently used descendant pins its ancestor chain before sweeping.
505 live := en.scratch[:0]
506 for _, c := range en.memo {
507 if c.node.used == en.generation-1 {
508 live = append(live, c.node)
509 }
510 }
511 for _, n := range live {
512 for p := n.key.parent; p != nil; p = p.key.parent {
513 p.used = en.generation - 1
514 }
515 }
516 for k, c := range en.memo {
517 if c.node.used+2 <= en.generation {
518 delete(en.memo, k)
519 }
520 }
521 // Keep selector IDs and every identity referenced by a surviving memo key.
522 usedIDs, usedSets := en.usedIDs, en.usedSets
523 clear(usedIDs)
524 clear(usedSets)
525 for _, r := range en.rules {
526 for _, part := range r.selector.parts {
527 usedIDs[part.typID], usedIDs[part.idID] = true, true
528 for _, id := range part.classIDs {
529 usedIDs[id] = true
530 }
531 }
532 }
533 for k := range en.memo {
534 usedIDs[k.typ], usedIDs[k.id] = true, true
535 usedSets[k.class] = true
536 }
537 for hash, bucket := range en.sets {
538 kept := bucket[:0]
539 for _, set := range bucket {
540 if usedSets[set.id] {
541 kept = append(kept, set)
542 for _, id := range set.members {
543 usedIDs[id] = true
544 }
545 } else {
546 delete(en.classMembers, set.id)
547 }
548 }
549 if len(kept) == 0 {
550 delete(en.sets, hash)
551 } else {
552 en.sets[hash] = kept
553 }
554 }
555 for name, id := range en.ids {
556 if !usedIDs[id] {
557 delete(en.ids, name)
558 }
559 }
560 clear(live)
561 en.scratch = live[:0]
562}
563
564// Shorthand targets are the longhands affected even before var() is substituted.
565func shorthandTargets(name string) []string {
566 switch name {
567 case "margin":
568 return []string{"margin-top", "margin-right", "margin-bottom", "margin-left"}
569 case "padding":
570 return []string{"padding-top", "padding-right", "padding-bottom", "padding-left"}
571 case "border-width":
572 return []string{"border-top-width", "border-right-width", "border-bottom-width", "border-left-width"}
573 case "border-style":
574 return []string{"border-top-style", "border-right-style", "border-bottom-style", "border-left-style"}
575 case "border-color":
576 return []string{"border-top-color", "border-right-color", "border-bottom-color", "border-left-color"}
577 case "border-radius":
578 return []string{"border-top-left-radius", "border-top-right-radius", "border-bottom-right-radius", "border-bottom-left-radius"}
579 case "background":
580 return []string{"background-color"}
581 case "border":
582 return []string{"border-top-width", "border-top-style", "border-top-color", "border-right-width", "border-right-style", "border-right-color", "border-bottom-width", "border-bottom-style", "border-bottom-color", "border-left-width", "border-left-style", "border-left-color"}
583 case "border-top":
584 return []string{"border-top-width", "border-top-style", "border-top-color"}
585 case "border-right":
586 return []string{"border-right-width", "border-right-style", "border-right-color"}
587 case "border-bottom":
588 return []string{"border-bottom-width", "border-bottom-style", "border-bottom-color"}
589 case "border-left":
590 return []string{"border-left-width", "border-left-style", "border-left-color"}
591 case "outline":
592 return []string{"outline-width", "outline-style", "outline-color"}
593 case "flex":
594 return []string{"flex-grow", "flex-shrink", "flex-basis"}
595 case "overflow":
596 return []string{"overflow-x", "overflow-y"}
597 case "gap":
598 return []string{"row-gap", "column-gap"}
599 }
600 return nil
601}
602func staticIndependent(name, text string) bool {
603 lower := strings.ToLower(text)
604 // em/rem are resolved using the current element/root font. "em" in a font
605 // family name does not affect its value but keeping that case dynamic is safe.
606 return !strings.Contains(lower, "currentcolor") && !strings.Contains(lower, "em") && name != "font-size" && name != "box-shadow" && name != "line-height"
607}
608
609// Initial values independent of color and font metrics, prepared once rather than parsed per node.
610var defaultValues = func() [propertyCount]Value {
611 var values [propertyCount]Value
612 for i := propertyID(0); i < propertyCount; i++ {
613 values[i], _ = parseValue(propertyNames[i], initial(propertyNames[i]), Color{A: 1}, 16, 16)
614 }
615 return values
616}()
617
618func isCurrentColorDefault(id propertyID) bool {
619 return id == p_outline_color || id == p_border_top_color || id == p_border_right_color || id == p_border_bottom_color || id == p_border_left_color
620}
621
622func isInheritedID(id propertyID) bool {
623 switch id {
624 case p_color, p_font_family, p_font_size, p_font_weight, p_font_style, p_line_height, p_text_align, p_letter_spacing, p_cursor, p_accent_color:
625 return true
626 }
627 return false
628}
629
630func compileDecls(declarations []Declaration) []compiledDecl {
631 var result []compiledDecl
632 for i, d := range declarations {
633 if strings.HasPrefix(d.Property, "--") {
634 result = append(result, compiledDecl{custom: d.Property, text: d.Value, important: d.Important, order: i})
635 continue
636 }
637 expanded, ok := expand(d)
638 if d.Value == "inherit" || d.Value == "initial" || d.Value == "unset" {
639 if targets := shorthandTargets(d.Property); len(targets) > 0 {
640 expanded = nil
641 for _, name := range targets {
642 expanded = append(expanded, Declaration{Property: name, Value: d.Value, Important: d.Important})
643 }
644 ok = true
645 }
646 }
647 hasVar := containsVar(d.Value)
648 if targets := shorthandTargets(d.Property); !hasVar && len(targets) > 0 && ok {
649 for _, one := range expanded {
650 if one.Value != "inherit" && one.Value != "initial" && one.Value != "unset" {
651 if _, valid := parseValue(one.Property, one.Value, Color{A: 1}, 16, 16); !valid {
652 ok = false
653 break
654 }
655 }
656 }
657 }
658 if targets := shorthandTargets(d.Property); hasVar && len(targets) > 0 {
659 expanded = nil
660 for _, name := range targets {
661 expanded = append(expanded, Declaration{Property: name, Value: d.Value, Important: d.Important})
662 }
663 ok = true
664 }
665 if !ok {
666 continue
667 }
668 var group *pendingShorthand
669 var groupRaw []Token
670 if hasVar && len(shorthandTargets(d.Property)) > 0 {
671 groupRaw = Tokenize(d.Value)
672 group = &pendingShorthand{property: d.Property, text: d.Value}
673 for _, one := range expanded {
674 if id, exists := propertyIndex(one.Property); exists {
675 group.targets = append(group.targets, id)
676 }
677 }
678 }
679 for _, one := range expanded {
680 p, exists := propertyIndex(one.Property)
681 if !exists {
682 continue
683 }
684 cd := compiledDecl{property: p, text: one.Value, important: one.Important, order: i}
685 if hasVar {
686 cd.group = group
687 if group == nil {
688 cd.raw = Tokenize(one.Value)
689 } else {
690 cd.raw = groupRaw
691 }
692 }
693 if !hasVar && one.Value != "inherit" && one.Value != "initial" && one.Value != "unset" {
694 var valid bool
695 cd.value, valid = parseValue(one.Property, one.Value, Color{A: 1}, 16, 16)
696 if !valid {
697 continue
698 }
699 cd.parsed = true
700 }
701 result = append(result, cd)
702 }
703 }
704 return result
705}
706
707var initialStyle = func() Style {
708 var s Style
709 for i := propertyID(0); i < propertyCount; i++ {
710 s.set(i, defaultValues[i])
711 }
712 return s
713}()
714
715type shorthandResult struct {
716 texts map[propertyID]string
717 ok bool
718}
719
720func resolveShorthand(g *pendingShorthand, custom map[string]string, current Color, font, root float64) shorthandResult {
721 text, ok := resolveVars(g.text, custom, map[string]bool{}, 0)
722 if !ok {
723 return shorthandResult{}
724 }
725 result := shorthandResult{texts: make(map[propertyID]string, len(g.targets)), ok: true}
726 if text == "inherit" || text == "initial" || text == "unset" {
727 for _, id := range g.targets {
728 result.texts[id] = text
729 }
730 return result
731 }
732 ds, ok := expand(Declaration{Property: g.property, Value: text})
733 if !ok {
734 return shorthandResult{}
735 }
736 for _, d := range ds {
737 id, exists := propertyIndex(d.Property)
738 if !exists {
739 return shorthandResult{}
740 }
741 if _, valid := parseValue(d.Property, d.Value, current, font, root); !valid {
742 return shorthandResult{}
743 }
744 result.texts[id] = d.Value
745 }
746 for _, id := range g.targets {
747 if _, exists := result.texts[id]; !exists {
748 return shorthandResult{}
749 }
750 }
751 return result
752}