repos
/ orchard main

orchard

mirror

Every 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

3.0 KB · 147 lines · Go Raw History
  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}