Migration This is my new public git source viewer. Source coming soon. Please contact with concerns/bugs. Contact me

internal/core/funcs.go

3a50036d96eb14462e96598fd169d1c79264ac4b · 3.4 KB · 129 lines raw

1 package core
2
3 import (
4 "fmt"
5 "html/template"
6 "strings"
7 "time"
8 )
9
10 // FuncMap is bound to a single Ctx at render time. `mod` is the only
11 // entry point a template needs; everything else is sugar.
12 func FuncMap(c *Ctx) template.FuncMap {
13 return template.FuncMap{
14 // mod runs a module. A template that calls it is what *causes*
15 // the work -- views that don't ask, don't pay.
16 "mod": func(name string, args ...string) (any, error) {
17 return c.Mod(name, args...)
18 },
19 // modOr swallows the error, for optional modules (no README, etc).
20 "modOr": func(name string, args ...string) any {
21 v, err := c.Mod(name, args...)
22 if err != nil {
23 return nil
24 }
25 return v
26 },
27 "has": func(name string) bool {
28 v, err := c.Mod(name)
29 return err == nil && v != nil
30 },
31 "url": func(parts ...string) string {
32 return "/" + strings.Trim(strings.Join(parts, "/"), "/")
33 },
34 "dict": dict,
35 "date": func(t time.Time) string {
36 if t.IsZero() {
37 return ""
38 }
39 return t.Format("2006-01-02 15:04")
40 },
41 "rfc3339": func(t time.Time) string { return t.Format(time.RFC3339) },
42 "trunc": func(n int, s string) string {
43 if len(s) <= n {
44 return s
45 }
46 return s[:n] + "\u2026"
47 },
48 "bytes": humanBytes,
49 "patch": renderPatch,
50 "slug": slugify,
51 "bytes64": func(n int) string { return humanBytes(int64(n)) },
52 }
53 }
54
55 // slugify makes a section name usable as a fragment id.
56 func slugify(s string) string {
57 var b strings.Builder
58 prevDash := false
59 for _, r := range strings.ToLower(s) {
60 switch {
61 case (r >= 'a' && r <= 'z') || (r >= '0' && r <= '9'):
62 b.WriteRune(r)
63 prevDash = false
64 case !prevDash && b.Len() > 0:
65 b.WriteByte('-')
66 prevDash = true
67 }
68 }
69 return strings.TrimRight(b.String(), "-")
70 }
71
72 // renderPatch tags each diff line so the stylesheet can colour it. Doing
73 // this here rather than in CSS lets the gutter carry the signal instead of
74 // flooding whole rows with green and red.
75 func renderPatch(p string) template.HTML {
76 var b strings.Builder
77 b.WriteString(`<div class="patch">`)
78 for _, ln := range strings.Split(strings.TrimRight(p, "\n"), "\n") {
79 cls := "ctx"
80 switch {
81 case strings.HasPrefix(ln, "+++"), strings.HasPrefix(ln, "---"),
82 strings.HasPrefix(ln, "diff "), strings.HasPrefix(ln, "index "),
83 strings.HasPrefix(ln, "new file"), strings.HasPrefix(ln, "deleted file"),
84 strings.HasPrefix(ln, "similarity"), strings.HasPrefix(ln, "rename "):
85 cls = "meta"
86 case strings.HasPrefix(ln, "@@"):
87 cls = "hunk"
88 case strings.HasPrefix(ln, "+"):
89 cls = "add"
90 case strings.HasPrefix(ln, "-"):
91 cls = "del"
92 }
93 b.WriteString(`<div class="pl ` + cls + `">`)
94 b.WriteString(template.HTMLEscapeString(ln))
95 b.WriteString("</div>")
96 }
97 b.WriteString(`</div>`)
98 return template.HTML(b.String())
99 }
100
101 // dict builds a map for passing named args to partials, mirroring Hugo's
102 // dict so shared partials stay portable later.
103 func dict(kv ...any) (map[string]any, error) {
104 if len(kv)%2 != 0 {
105 return nil, fmt.Errorf("dict: odd number of arguments")
106 }
107 m := make(map[string]any, len(kv)/2)
108 for i := 0; i < len(kv); i += 2 {
109 k, ok := kv[i].(string)
110 if !ok {
111 return nil, fmt.Errorf("dict: key %d is not a string", i)
112 }
113 m[k] = kv[i+1]
114 }
115 return m, nil
116 }
117
118 func humanBytes(n int64) string {
119 const unit = 1024
120 if n < unit {
121 return fmt.Sprintf("%d B", n)
122 }
123 div, exp := int64(unit), 0
124 for x := n / unit; x >= unit; x /= unit {
125 div *= unit
126 exp++
127 }
128 return fmt.Sprintf("%.1f %cB", float64(n)/float64(div), "KMGT"[exp])
129 }