@ 261101.7

261101.1 · 0ad750f7e905ca3494043be38b127f94a06eb874

server.go splits by purpose

server.go held the server's identity, request guarding, the index, tree pages and page rendering. Each now has a file whose name says what is in it: about.go, guard.go, index.go, treepage.go and page.go. bugOff joins bug.go. server.go keeps the server and tree types, mounting, the embedded assets, the handler and the stop button.

Declarations moved unchanged, as a sorted comparison of every line outside the import blocks confirms. Follow them with git log --follow or blame -C.

reading declarations

Changes to page.go. All files

page.go

new file mode 100644@@ -0,0 +1,245 @@+package main++import (+	"bytes"+	"fmt"+	"html/template"+	"net/http"+	"net/url"+	"path"+	"strings"+	"time"+	"unicode/utf8"+)++// leadStyle sets a list's --lead to the width of its longest label, so+// the labels that lead its rows line up: in monospace a width in ch fits+// every label exactly.+func leadStyle(labels []string) string {+	n := 0+	for _, l := range labels {+		n = max(n, utf8.RuneCountInString(l))+	}+	return fmt.Sprintf(" style=\"--lead:%dch\"", n)+}++// errorLine is a failure's message as a page shows it.+func errorLine(msg string) string {+	return "<pre class=\"error\">" + template.HTMLEscapeString(msg) + "</pre>\n"+}++// segments writes a path or URL as one box per name, each ending in its+// slash, so a line breaks between names and not inside one. The // of a+// URL stays with its scheme.+func segments(p string) string {+	var parts []string+	for _, s := range strings.SplitAfter(p, "/") {+		switch {+		case s == "":+		case s == "/" && len(parts) > 0:+			parts[len(parts)-1] += s+		default:+			parts = append(parts, s)+		}+	}+	for i, s := range parts {+		parts[i] = `<span class="seg">` + template.HTMLEscapeString(s) + `</span>`+	}+	return strings.Join(parts, "<wbr>")+}++type crumb struct {+	Label string+	Href  string+	Title string // shown on hover, eg the tree's full path+}++// indexCrumb leads every tree page's breadcrumbs to the index, the parent+// of all trees. Its label is not www, which a tree may be called.+var indexCrumb = crumb{Label: "index", Href: "/"}++type page struct {+	Title   string+	Crumbs  []crumb+	Kind    string+	Body    template.HTML+	Raw     bool+	Tree    string        // prefix the stop button withdraws, or "" for every tree+	Search  string        // the tree's URL, which answers ?search=, or "" off a tree+	Recents string        // the tree whose pages a search lists as recent; a version's is its tree's+	Status  template.HTML // the server's age, on the index, or a repo's commit on the web address+	Style   template.CSS  // the stylesheet inline, for a page the server may not outlive+	Stopped bool+	Bug     bool   // the bug button at the page's foot+	Shared  bool   // a shared server's page, which has no button+	NoStop  bool   // a page on the web address, which has no button+	Live    string // the URL of the page's live reload stream, or ""+	Fixed   bool   // a version, which never changes, or a page that must not reload: the script runs but holds no stream+	// Forge is, for a page of a repo on the web address, the forge's own+	// permanent URL for it, which the share button copies; ForgeLines is+	// how that forge marks lines.+	Forge, ForgeLines string+	Built             *stamp // when the binary was built, above the header, with --debug; none on a share+}++func (t *tree) title(name string) string {+	at := ""+	if t.at != nil {+		at = " @ " + t.atLabel+	}+	if name == "." {+		return path.Base(t.label) + at+	}+	if at != "" {+		return name + at+	}+	return name+}++// rootCrumb is the tree's first breadcrumb: its base name, with the full+// path on hover. The address bar already shows the path.+func (t *tree) rootCrumb() crumb {+	if t.at != nil {+		return crumb{Label: path.Base(t.label) + " @ " + t.atLabel, Title: t.label + " as of " + t.at.hash}+	}+	return crumb{Label: path.Base(t.label), Title: t.label}+}++// crumbs links the index, the root and every parent directory; the last+// part is plain.+func (t *tree) crumbs(name string) []crumb {+	root := t.rootCrumb()+	out := []crumb{indexCrumb}+	// A repo on the web address links its host and account, which list+	// their repos.+	if t.web != nil {+		segs := strings.Split(t.web.key, "/")+		for i := range segs[:len(segs)-1] {+			out = append(out, crumb{Label: segs[i], Href: (&url.URL{Path: "/" + strings.Join(segs[:i+1], "/") + "/"}).String()})+		}+	}+	if name == "." {+		return append(out, root)+	}+	root.Href = t.prefixURL()+	out = append(out, root)+	parts := strings.Split(name, "/")+	for i, p := range parts {+		c := crumb{Label: p}+		if i < len(parts)-1 {+			c.Href = (&url.URL{Path: t.prefix + strings.Join(parts[:i+1], "/") + "/"}).String()+		}+		out = append(out, c)+	}+	return out+}++func render(w http.ResponseWriter, r *http.Request, status int, p page) {+	if p.Live == "" {+		p.Live = liveURL(r)+	}+	p.Bug = !bugOff && !p.Shared && !p.Stopped+	if debugOn && !p.Shared && !built.IsZero() {+		p.Built = &stamp{Clock: clock(built), Millis: built.UnixMilli(), Ago: int(time.Since(built).Seconds()),+			Commit: buildCommit, Rendered: rendered(r)}+	}+	var b bytes.Buffer+	// The body is most of the page, and can be megabytes.+	b.Grow(len(p.Body) + 16<<10)+	if err := layout.ExecuteTemplate(&b, "page", p); err != nil {+		http.Error(w, err.Error(), http.StatusInternalServerError)+		return+	}+	w.Header().Set("Content-Type", "text/html; charset=utf-8")+	// no-store keeps rendered pages out of the back/forward cache, so Back+	// after Stop shows the server is gone instead of a dead snapshot.+	w.Header().Set("Cache-Control", "no-store")+	w.WriteHeader(status)+	w.Write(b.Bytes())+}++// render gives the page this tree's Delete lock button, unless the tree is+// shared, and a search box for the directory the page is in, with a button+// that copies the page's path on disk. A failed page searches the whole+// tree, since its directory may not exist, and has no path to copy.+// Recent pages stay within the tree, so one repo's reading never shows in+// another's search.+func (t *tree) render(w http.ResponseWriter, r *http.Request, status int, p page) {+	p.Tree, p.Shared, p.Search = t.prefix, t.shared, t.prefixURL()+	p.Recents = t.prefixURL()+	if t.base != nil {+		p.Recents = t.base.prefixURL()+	}+	if t.web != nil {+		p.NoStop, p.Status = true, t.web.status(r.Context(), t.prefix, r.URL.Path)+	}+	// A version never changes and has no lock: no live reload and no stop+	// button. Its page still runs the script, for the keys, the search box,+	// line marks and the copy button.+	if t.at != nil {+		p.NoStop, p.Fixed = true, true+	}+	if status == http.StatusOK {+		p.Search = searchURL(r.URL.Path)+		p.Forge, p.ForgeLines = t.forgeLink(r, p.Kind)+	}+	render(w, r, status, p)+}++// searchURL is the directory a page at urlPath is in: a listing itself,+// or a file's directory.+func searchURL(urlPath string) string {+	return (&url.URL{Path: urlPath[:strings.LastIndex(urlPath, "/")+1]}).String()+}++// why is what a refusal page says: the reason in full, or on a shared or+// public tree only that the name is refused, since the reason names paths+// outside it.+func (t *tree) why(ref *refusal) string {+	if t.shared || t.public {+		return "Refused: " + ref.link+	}+	return ref.Error()+}++// coin is a status code that turns like a coin above a 404's table, or a+// stop's 410.+func coin(code string) string {+	return `<p class="coin" aria-hidden="true"><span><span>` + code + `</span><span>` + code + `</span></span></p>`+}++// fullURL is the URL r asked for, without its query, so "not found"+// names it whole when copied away from the address bar.+func fullURL(r *http.Request) string {+	return (&url.URL{Scheme: "http", Host: r.Host, Path: r.URL.Path}).String()+}++// notFound is the page for a path the tree lacks: 404 turns like a coin+// above a table of labelled rows, the URL first, then where git says the+// path went and the names close to it. Other failures say why in the+// plain line alone.+func (t *tree) notFound(w http.ResponseWriter, r *http.Request, name string) {+	t.render(w, r, http.StatusNotFound, page{+		Title:  t.title(name),+		Crumbs: t.crumbs(name),+		Kind:   "error",+		Body:   template.HTML(coin("404") + `<table class="gone">` + "\n" + notFoundRow(r) + t.goneRows(r.Context(), name) + "</table>\n"),+	})+}++func (t *tree) fail(w http.ResponseWriter, r *http.Request, name string, status int, msg string) {+	// The request's own entry says not found; a refusal or fault says why+	// only here.+	switch {+	case status >= 500:+		logErr("%s: %s", t.dir, msg)+	case status != http.StatusNotFound:+		logInfo("%s: %s", t.dir, msg)+	}+	t.render(w, r, status, page{+		Title:  t.title(name),+		Crumbs: t.crumbs(name),+		Kind:   "error",+		Body:   template.HTML("<pre class=\"error\">" + template.HTMLEscapeString(msg) + "</pre>"),+	})+}