v0.5.1 657f658

cascade.go RawHistoryFolder

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}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 12

↑↓ moveenter openesc close