Seed from go-template-repo, renamed to simplexcalc

The template's files at a77fd30, without its history or LICENSE, after
script/rename simplexcalc.

Model: opus-5-5
This commit is contained in:
clawbot
2026-09-26 21:38:57 +00:00
parent e336f46e34
commit f8ce8cef83
79 changed files with 7131 additions and 0 deletions
+13
View File
@@ -0,0 +1,13 @@
package render
import "errors"
var (
// errNoTemplates means the embed matched nothing — a build that
// produced a binary with no pages in it.
errNoTemplates = errors.New("no page templates were embedded")
// errUnknownTemplate means a handler asked for a page that is not
// in the embedded set: a typo, caught by that handler's test.
errUnknownTemplate = errors.New("unknown template")
)
+240
View File
@@ -0,0 +1,240 @@
// Package render parses the embedded templates once, at startup, and
// executes them into a buffer before writing anything to the client.
//
// Two decisions worth keeping when this is seeded into a real project:
//
// - Every template is parsed in New. A template that does not compile
// is a process that does not start, rather than a 500 the first time
// someone visits the page it broke.
// - Execution goes to a buffer first. A template that fails halfway
// through would otherwise have already written a 200 and half a
// page, and the error could no longer be reported as one.
package render
import (
"bytes"
"fmt"
"html/template"
"io"
"io/fs"
"net/http"
"path"
"sort"
"strings"
"time"
"github.com/dustin/go-humanize"
"go.uber.org/fx"
"sneak.berlin/go/simplexcalc/internal/database"
"sneak.berlin/go/simplexcalc/internal/globals"
"sneak.berlin/go/simplexcalc/templates"
)
// baseTemplate is the outer document every page is rendered through.
const baseTemplate = "base"
// Params defines dependencies for Renderer.
type Params struct {
fx.In
Globals *globals.Globals
}
// Renderer holds one compiled template set per page.
type Renderer struct {
pages map[string]*template.Template
globals *globals.Globals
started time.Time
}
// Page is the data every template can rely on, embedded by the
// page-specific types below so that promoted fields keep the templates
// free of a data-envelope prefix.
type Page struct {
AppName string
Version string
Buildarch string
Uptime string
// CSRFField is the hidden input gorilla/csrf validates. It is
// template.HTML because it is markup this process generated, not
// input; nothing user-supplied is ever assigned to it.
CSRFField template.HTML
}
// IndexPage is the data for index.html.
type IndexPage struct {
Page
WidgetCount int
Widgets []database.Widget
}
// ErrorPage is the data for error.html.
type ErrorPage struct {
Page
Status int
Message string
}
// funcs are the template helpers. Deliberately few: logic belongs in
// the handler, where it can be tested without parsing HTML.
func funcs() template.FuncMap {
return template.FuncMap{
// bytes renders a byte count the way an operator reads one.
"bytes": func(n int64) string {
if n < 0 {
return "-"
}
return humanize.IBytes(uint64(n))
},
// since renders a timestamp as "3 minutes ago".
"since": humanize.Time,
}
}
// New compiles every page template against the base document and the
// partials.
//
//nolint:revive // lc parameter is required by fx even if unused.
func New(lc fx.Lifecycle, params Params) (*Renderer, error) {
r := &Renderer{
pages: map[string]*template.Template{},
globals: params.Globals,
started: time.Now(),
}
shared, pages, err := split(templates.FS)
if err != nil {
return nil, err
}
for _, page := range pages {
// Each page gets its own set: pages define blocks of the same
// names ("title", "content"), so parsing them all into one
// template would leave whichever was parsed last defining both
// for everybody.
set := template.New(baseTemplate).Funcs(funcs())
set, err = set.ParseFS(templates.FS, append(append([]string{}, shared...), page)...)
if err != nil {
return nil, fmt.Errorf("parsing template %s: %w", page, err)
}
r.pages[path.Base(page)] = set
}
if len(r.pages) == 0 {
return nil, errNoTemplates
}
return r, nil
}
// split separates the embedded set into the files every page needs
// (the base document and the partials) and the page templates
// themselves.
func split(fsys fs.FS) ([]string, []string, error) {
partials, err := fs.Glob(fsys, "partials/*.html")
if err != nil {
return nil, nil, fmt.Errorf("globbing partials: %w", err)
}
top, err := fs.Glob(fsys, "*.html")
if err != nil {
return nil, nil, fmt.Errorf("globbing templates: %w", err)
}
var pages []string
shared := append([]string{}, partials...)
for _, f := range top {
if strings.TrimSuffix(path.Base(f), ".html") == baseTemplate {
shared = append(shared, f)
continue
}
pages = append(pages, f)
}
sort.Strings(shared)
sort.Strings(pages)
return shared, pages, nil
}
// NewPage returns the common data, filled in from build-time globals
// and the request's CSRF field.
func (r *Renderer) NewPage(csrfField template.HTML) Page {
return Page{
AppName: r.globals.Appname,
Version: r.globals.Version,
Buildarch: r.globals.Buildarch,
Uptime: time.Since(r.started).Round(time.Second).String(),
CSRFField: csrfField,
}
}
// Execute renders a page into w. It buffers first: see the package
// comment.
func (r *Renderer) Execute(w io.Writer, name string, data any) error {
set, ok := r.pages[name]
if !ok {
return fmt.Errorf("%w: %s", errUnknownTemplate, name)
}
var buf bytes.Buffer
err := set.ExecuteTemplate(&buf, baseTemplate, data)
if err != nil {
return fmt.Errorf("executing template %s: %w", name, err)
}
_, err = buf.WriteTo(w)
if err != nil {
return fmt.Errorf("writing rendered template %s: %w", name, err)
}
return nil
}
// HTML renders a page to an http.ResponseWriter with the given status.
// A render failure after the buffer succeeded cannot happen, so the
// status written here is always the status the client sees.
func (r *Renderer) HTML(
w http.ResponseWriter, status int, name string, data any,
) error {
var buf bytes.Buffer
err := r.Execute(&buf, name, data)
if err != nil {
return err
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.WriteHeader(status)
_, err = buf.WriteTo(w)
if err != nil {
return fmt.Errorf("writing response: %w", err)
}
return nil
}
// Names returns the compiled page names, sorted. Tests use it to assert
// that every embedded page really compiled.
func (r *Renderer) Names() []string {
names := make([]string, 0, len(r.pages))
for name := range r.pages {
names = append(names, name)
}
sort.Strings(names)
return names
}
+197
View File
@@ -0,0 +1,197 @@
package render_test
import (
"bytes"
"html/template"
"net/http/httptest"
"strings"
"testing"
"time"
"sneak.berlin/go/simplexcalc/internal/database"
"sneak.berlin/go/simplexcalc/internal/globals"
"sneak.berlin/go/simplexcalc/internal/render"
"sneak.berlin/go/simplexcalc/templates"
)
func newRenderer(t *testing.T) *render.Renderer {
t.Helper()
r, err := render.New(nil, render.Params{
Globals: &globals.Globals{
Appname: "simplexcalc", Version: "test", Buildarch: "amd64",
},
})
if err != nil {
t.Fatalf("compiling templates: %v", err)
}
return r
}
// TestEveryEmbeddedPageCompiles is the reason New parses everything at
// startup: a template that does not compile must be a process that does
// not start, and this is what proves the set is complete rather than
// just non-empty.
func TestEveryEmbeddedPageCompiles(t *testing.T) {
t.Parallel()
r := newRenderer(t)
embedded, err := templates.FS.ReadDir(".")
if err != nil {
t.Fatalf("reading embedded templates: %v", err)
}
want := 0
for _, e := range embedded {
if !e.IsDir() && strings.HasSuffix(e.Name(), ".html") && e.Name() != "base.html" {
want++
}
}
if want == 0 {
t.Fatal("no page templates were embedded, so this test proves nothing")
}
if got := len(r.Names()); got != want {
t.Errorf("compiled %d pages (%v), embedded %d", got, r.Names(), want)
}
}
// TestIndexRendersEmbeddedContent renders the page the service serves
// at /, with real data, and checks that the base document, both
// partials and the page body all made it into one response.
func TestIndexRendersEmbeddedContent(t *testing.T) {
t.Parallel()
r := newRenderer(t)
data := render.IndexPage{
Page: r.NewPage(template.HTML(`<input type="hidden" name="csrf" />`)),
WidgetCount: 1,
Widgets: []database.Widget{
{
ID: "abc", Name: "a widget", SizeBytes: 4096,
CreatedAt: time.Now().Add(-time.Hour),
},
},
}
var buf bytes.Buffer
err := r.Execute(&buf, "index.html", data)
if err != nil {
t.Fatalf("rendering index: %v", err)
}
out := buf.String()
for _, want := range []string{
"<!doctype html>", // base document
`href="/static/css/style.css"`, // base document links the embedded asset
"<nav>", // navbar partial
"<footer>", // footer partial
"a widget", // page data
"4.0 KiB", // the bytes template func
"ago", // the since template func
`name="csrf"`, // the CSRF field was placed
} {
if !strings.Contains(out, want) {
t.Errorf("rendered page is missing %q\n---\n%s", want, out)
}
}
}
// TestHTMLEscapesUserData: the name comes from a form, and html/template
// is only a defence if the data actually goes through it.
func TestHTMLEscapesUserData(t *testing.T) {
t.Parallel()
r := newRenderer(t)
data := render.IndexPage{
Page: r.NewPage(""),
WidgetCount: 1,
Widgets: []database.Widget{
{ID: "x", Name: `<script>alert(1)</script>`, CreatedAt: time.Now()},
},
}
var buf bytes.Buffer
err := r.Execute(&buf, "index.html", data)
if err != nil {
t.Fatalf("rendering: %v", err)
}
if strings.Contains(buf.String(), "<script>alert(1)</script>") {
t.Error("a widget name was rendered as live markup")
}
}
// TestPagesDoNotShareBlocks: index.html and error.html both define
// "title" and "content". Parsed into one set, the last one parsed would
// define both for everybody, and every page would render as whichever
// that was.
func TestPagesDoNotShareBlocks(t *testing.T) {
t.Parallel()
r := newRenderer(t)
var buf bytes.Buffer
err := r.Execute(&buf, "error.html", render.ErrorPage{
Page: r.NewPage(""), Status: 404, Message: "gone fishing",
})
if err != nil {
t.Fatalf("rendering error page: %v", err)
}
out := buf.String()
if !strings.Contains(out, "gone fishing") {
t.Errorf("error page did not render its own content:\n%s", out)
}
if strings.Contains(out, "widgets (") {
t.Error("the error page rendered the index's content block")
}
}
// TestUnknownTemplateIsAnError, rather than an empty 200.
func TestUnknownTemplateIsAnError(t *testing.T) {
t.Parallel()
r := newRenderer(t)
err := r.Execute(&bytes.Buffer{}, "nope.html", nil)
if err == nil {
t.Fatal("rendering a template that does not exist must fail")
}
}
// TestHTMLWritesStatusAndType checks the response contract, since the
// handlers rely on it for every page they serve.
func TestHTMLWritesStatusAndType(t *testing.T) {
t.Parallel()
r := newRenderer(t)
w := httptest.NewRecorder()
err := r.HTML(w, 404, "error.html", render.ErrorPage{
Page: r.NewPage(""), Status: 404, Message: "no",
})
if err != nil {
t.Fatalf("rendering: %v", err)
}
if w.Code != 404 {
t.Errorf("status = %d, want 404", w.Code)
}
if ct := w.Header().Get("Content-Type"); !strings.HasPrefix(ct, "text/html") {
t.Errorf("Content-Type = %q", ct)
}
}