~bigbes/sr-ht-dolt

ref: 7d799ed0ff8809cdad362a092d4437bd72d58a82 sr-ht-dolt/web/templates.go -rw-r--r-- 5.2 KiB
7d799ed0 — Eugene Blikh beads: drop the Bead/Beads prefix from the moved types 5 days ago
                                                                                
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
package web

import (
	"embed"
	"fmt"
	"html/template"
	"io/fs"
	"log/slog"
	"net/http"
	"net/url"
	"strings"

	"go.bigb.es/auxilia/scribe"

	"sourcecraft.dev/bigbes/sr-ht-ecore/pages"
)

//go:embed templates/*.html templates/icons/*.svg
var templateFS embed.FS

// loadPages parses one template set per page in templates/, through
// sr-ht-ecore's pages: the layout, the shared chrome partials, our own
// _partials.html and that page's content.
//
// There is no list of pages here any more. Pages are discovered from the
// directory, so a new page — or a new View's beads.html — is registered by
// existing as a file, and the loader a registration used to have to remember is
// gone with it. A page that defines no "content" block, and a missing layout,
// are startup errors: the first would otherwise serve the chrome around a hole
// under a 200.
//
// The error page is not ours: pages ships one (registered as pages.ErrorPage)
// and parses its "srht-error" body into every set, so the 404 and 403 templates
// this service used to carry are gone rather than reworded.
func loadPages() (pages.Set, error) {
	icons, err := loadIcons()
	if err != nil {
		return nil, err
	}
	return pages.Load(templateFS, pages.Options{Funcs: templateFuncs(icons)})
}

// pageName maps a template file name ("beads.html", what a View declares) to
// the name pages registers it under ("beads", what Render takes).
func pageName(file string) string {
	return strings.TrimSuffix(file, ".html")
}

// loadIcons reads every embedded icon SVG into a name→markup map for the icon
// template func.
func loadIcons() (map[string]template.HTML, error) {
	entries, err := fs.ReadDir(templateFS, "templates/icons")
	if err != nil {
		return nil, fmt.Errorf("web: read icons dir: %w", err)
	}
	icons := make(map[string]template.HTML, len(entries))
	for _, e := range entries {
		if e.IsDir() || !strings.HasSuffix(e.Name(), ".svg") {
			continue
		}
		data, err := templateFS.ReadFile("templates/icons/" + e.Name())
		if err != nil {
			return nil, fmt.Errorf("web: read icon %s: %w", e.Name(), err)
		}
		name := strings.TrimSuffix(e.Name(), ".svg")
		icons[name] = template.HTML(fmt.Sprintf(
			`<span class="icon icon-%s" aria-hidden="true">%s</span>`, name, data))
	}
	return icons, nil
}

// templateFuncs is this service's own funcmap. pages merges it over
// chrome.Funcs — "dict", "shortsha", "reltime" and "abstime", which the shared
// partials and half this family's pages were written against — so only the
// helpers nobody else has are listed here. The local copies of the relative and
// absolute time formatters are gone with the rest; chrome's reltime also faces
// forward ("in 3 weeks"), where ours called every future instant "just now".
func templateFuncs(icons map[string]template.HTML) template.FuncMap {
	m := template.FuncMap{}

	// icon renders a named inline SVG (from templates/icons). An unknown name
	// yields empty output rather than a hard error, so a missing icon never
	// crashes a page.
	m["icon"] = func(name string) template.HTML { return icons[name] }
	// humansize renders a byte count as a human-readable size.
	m["humansize"] = humanizeSize
	// "upper" and "lower" used to be here for the environment banner and the
	// database listing's visibility label. Both are the shared chrome's markup
	// now, and it does its own casing, so nothing in this service's templates
	// calls them any more.
	//
	// inc/dec support 1-based page arithmetic in pagination links.
	m["inc"] = func(n int) int { return n + 1 }
	m["dec"] = func(n int) int { return n - 1 }
	// doltHost derives the host:port a `dolt login --auth-endpoint` expects
	// from our origin URL (defaulting to :443 for https).
	m["doltHost"] = doltHost

	return m
}

// doltHost renders the host:port for `dolt login --auth-endpoint` from an origin
// URL. It appends the default TLS/plain port when the origin omits one.
func doltHost(origin string) string {
	u, err := url.Parse(origin)
	if err != nil || u.Host == "" {
		return origin
	}
	if u.Port() != "" {
		return u.Host
	}
	if u.Scheme == "http" {
		return u.Host + ":80"
	}
	return u.Host + ":443"
}

// humanizeSize renders a byte count with binary (1024) units.
func humanizeSize(n uint64) string {
	const unit = 1024
	if n < unit {
		return fmt.Sprintf("%d B", n)
	}
	div, exp := uint64(unit), 0
	for m := n / unit; m >= unit; m /= unit {
		div *= unit
		exp++
	}
	return fmt.Sprintf("%.1f %ciB", float64(n)/float64(div), "KMGTPE"[exp])
}

// render is pages.Set.Render with this service's log line on the end.
//
// It is the whole of what is left of the old renderer, and the deleted half is
// the point: the previous one wrote "template render error: "+err.Error() into
// the response body, publishing template names, field paths and whatever the
// payload's String method produced to whoever asked for the page. pages answers
// a fixed sentence and hands the error back for the log, which is where a
// broken template belongs.
//
// A returned error means the response is already answered; there is nothing to
// do with it here but say so.
func (a *app) render(w http.ResponseWriter, status int, page string, data any) {
	if err := a.pages.Render(w, status, page, data); err != nil {
		slog.Error("rendering a page failed",
			"component", "web", "page", page, "status", status, scribe.Err(err))
	}
}