v0.5.1 657f658

value.go RawHistoryFolder

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}

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