orchard
mirrorEvery site I host, in one repo, along with the Cloudflare Tunnel and Caddy that front them. It's all Go, Vite, and SQLite, and it runs on a desktop at home with nothing listening on an inbound port.
blogbuncaddycloudflare-tunneldockergogolanghomelabhtml-templatemonorepoself-hostedseosqlitestatic-sitetypstuptime-monitoringviteweb-analytics
1package main
2
3import (
4 "fmt"
5 "html/template"
6 "net/url"
7 "strings"
8 "time"
9)
10
11var templateFuncs = template.FuncMap{
12 "dict": dict,
13 "humanBytes": humanBytes,
14 "humanTime": humanTime,
15 "shortSHA": shortSHA,
16 "pathSegs": pathSegs,
17 "urlPath": urlPath,
18 "firstLine": firstLine,
19 "pluralize": pluralize,
20 "percentOf": percentOf,
21 "add": func(a, b int) int { return a + b },
22 "sub": func(a, b int) int { return a - b },
23}
24
25// dict lets a partial take more than one value, since a template has one dot.
26func dict(pairs ...any) (map[string]any, error) {
27 if len(pairs)%2 != 0 {
28 return nil, fmt.Errorf("dict: odd number of arguments (%d)", len(pairs))
29 }
30 out := make(map[string]any, len(pairs)/2)
31 for i := 0; i < len(pairs); i += 2 {
32 key, ok := pairs[i].(string)
33 if !ok {
34 return nil, fmt.Errorf("dict: key %d is %T, want string", i, pairs[i])
35 }
36 out[key] = pairs[i+1]
37 }
38 return out, nil
39}
40
41func humanBytes(n int64) string {
42 const unit = 1024
43 if n < unit {
44 return fmt.Sprintf("%d B", n)
45 }
46 div, exp := int64(unit), 0
47 for v := n / unit; v >= unit; v /= unit {
48 div *= unit
49 exp++
50 }
51 return fmt.Sprintf("%.1f %cB", float64(n)/float64(div), "KMGTPE"[exp])
52}
53
54// humanTime is the relative form; templates keep the absolute date in a title attribute.
55func humanTime(t time.Time) string {
56 if t.IsZero() {
57 return "never"
58 }
59 d := time.Since(t)
60 switch {
61 case d < time.Minute:
62 return "just now"
63 case d < time.Hour:
64 return ago(int(d.Minutes()), "minute")
65 case d < 24*time.Hour:
66 return ago(int(d.Hours()), "hour")
67 case d < 30*24*time.Hour:
68 return ago(int(d.Hours()/24), "day")
69 case d < 365*24*time.Hour:
70 return ago(int(d.Hours()/24/30), "month")
71 default:
72 return ago(int(d.Hours()/24/365), "year")
73 }
74}
75
76func ago(n int, unit string) string {
77 if n == 1 {
78 return "1 " + unit + " ago"
79 }
80 return fmt.Sprintf("%d %ss ago", n, unit)
81}
82
83func shortSHA(sha string) string {
84 if len(sha) > 8 {
85 return sha[:8]
86 }
87 return sha
88}
89
90// Crumb is one element of the breadcrumb above a tree or blob view.
91type Crumb struct {
92 Name string
93 Path string
94}
95
96func pathSegs(p string) []Crumb {
97 if p == "" {
98 return nil
99 }
100 var out []Crumb
101 var acc []string
102 for _, seg := range strings.Split(p, "/") {
103 if seg == "" {
104 continue
105 }
106 acc = append(acc, seg)
107 out = append(out, Crumb{Name: seg, Path: strings.Join(acc, "/")})
108 }
109 return out
110}
111
112// urlPath percent-encodes a path for an href segment by segment, so separators
113// survive and a "?", "#" or space does not break the link.
114func urlPath(p string) string {
115 segs := strings.Split(p, "/")
116 for i, s := range segs {
117 segs[i] = url.PathEscape(s)
118 }
119 return strings.Join(segs, "/")
120}
121
122func firstLine(s string) string {
123 if i := strings.IndexByte(s, '\n'); i >= 0 {
124 return s[:i]
125 }
126 return s
127}
128
129func pluralize(n int, one, many string) string {
130 if n == 1 {
131 return one
132 }
133 return many
134}
135
136// percentOf drives the push size meter, clamped to 100.
137func percentOf(n, total int64) int {
138 if total <= 0 {
139 return 0
140 }
141 p := int(n * 100 / total)
142 if p > 100 {
143 return 100
144 }
145 return p
146}