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

internal/core/funcs.go

8d1bb5bfdd2cba82488775b46308efed740132a8 · 3.8 KB · 137 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 // static builds a cache-busting asset URL, e.g. "css/base.css" ->
35 // "/static/css/base.css?v=<tag>". The `v` param is what lets
36 // main.go's static handler hand out a year-long immutable
37 // Cache-Control instead of a short one -- a URL without it is
38 // still served, just not frozen for a year.
39 "static": func(p string) string {
40 return "/static/" + p + "?v=" + c.StaticTag
41 },
42 "dict": dict,
43 "date": func(t time.Time) string {
44 if t.IsZero() {
45 return ""
46 }
47 return t.Format("2006-01-02 15:04")
48 },
49 "rfc3339": func(t time.Time) string { return t.Format(time.RFC3339) },
50 "trunc": func(n int, s string) string {
51 if len(s) <= n {
52 return s
53 }
54 return s[:n] + "…"
55 },
56 "bytes": humanBytes,
57 "patch": renderPatch,
58 "slug": slugify,
59 "bytes64": func(n int) string { return humanBytes(int64(n)) },
60 }
61 }
62
63 // slugify makes a section name usable as a fragment id.
64 func slugify(s string) string {
65 var b strings.Builder
66 prevDash := false
67 for _, r := range strings.ToLower(s) {
68 switch {
69 case (r >= 'a' && r <= 'z') || (r >= '0' && r <= '9'):
70 b.WriteRune(r)
71 prevDash = false
72 case !prevDash && b.Len() > 0:
73 b.WriteByte('-')
74 prevDash = true
75 }
76 }
77 return strings.TrimRight(b.String(), "-")
78 }
79
80 // renderPatch tags each diff line so the stylesheet can colour it. Doing
81 // this here rather than in CSS lets the gutter carry the signal instead of
82 // flooding whole rows with green and red.
83 func renderPatch(p string) template.HTML {
84 var b strings.Builder
85 b.WriteString(`<div class="patch">`)
86 for _, ln := range strings.Split(strings.TrimRight(p, "\n"), "\n") {
87 cls := "ctx"
88 switch {
89 case strings.HasPrefix(ln, "+++"), strings.HasPrefix(ln, "---"),
90 strings.HasPrefix(ln, "diff "), strings.HasPrefix(ln, "index "),
91 strings.HasPrefix(ln, "new file"), strings.HasPrefix(ln, "deleted file"),
92 strings.HasPrefix(ln, "similarity"), strings.HasPrefix(ln, "rename "):
93 cls = "meta"
94 case strings.HasPrefix(ln, "@@"):
95 cls = "hunk"
96 case strings.HasPrefix(ln, "+"):
97 cls = "add"
98 case strings.HasPrefix(ln, "-"):
99 cls = "del"
100 }
101 b.WriteString(`<div class="pl ` + cls + `">`)
102 b.WriteString(template.HTMLEscapeString(ln))
103 b.WriteString("</div>")
104 }
105 b.WriteString(`</div>`)
106 return template.HTML(b.String())
107 }
108
109 // dict builds a map for passing named args to partials, mirroring Hugo's
110 // dict so shared partials stay portable later.
111 func dict(kv ...any) (map[string]any, error) {
112 if len(kv)%2 != 0 {
113 return nil, fmt.Errorf("dict: odd number of arguments")
114 }
115 m := make(map[string]any, len(kv)/2)
116 for i := 0; i < len(kv); i += 2 {
117 k, ok := kv[i].(string)
118 if !ok {
119 return nil, fmt.Errorf("dict: key %d is not a string", i)
120 }
121 m[k] = kv[i+1]
122 }
123 return m, nil
124 }
125
126 func humanBytes(n int64) string {
127 const unit = 1024
128 if n < unit {
129 return fmt.Sprintf("%d B", n)
130 }
131 div, exp := int64(unit), 0
132 for x := n / unit; x >= unit; x /= unit {
133 div *= unit
134 exp++
135 }
136 return fmt.Sprintf("%.1f %cB", float64(n)/float64(div), "KMGT"[exp])
137 }