@ 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 treepage.go. All files

treepage.go

new file mode 100644@@ -0,0 +1,342 @@+package main++import (+	"bytes"+	"errors"+	"fmt"+	"html/template"+	"io"+	"io/fs"+	"net/http"+	"net/url"+	"path"+	"slices"+	"strings"+	"unicode/utf8"+)++// maxCode caps the size of a file shown as a code page; larger files go raw.+const maxCode = 1 << 20++// name is the file below the tree that urlPath names, or "." for its root.+func (t *tree) name(urlPath string) string {+	rest := strings.TrimPrefix(urlPath, t.prefix)+	name := strings.TrimPrefix(path.Clean("/"+rest), "/")+	if name == "" {+		return "."+	}+	return name+}++func (t *tree) serve(w http.ResponseWriter, r *http.Request) {+	if first, rest, ok := t.versionIn(r.URL.Path); ok {+		t.serveVersion(w, r, first, rest)+		return+	}+	name := t.name(r.URL.Path)+	// A first segment such as @5430ac3 named a version before versions+	// moved beside the tree's name, unless the tree holds an entry by that+	// name. It moves there for good, so old links still lead to it.+	if first, rest, _ := strings.Cut(name, "/"); t.hasVersions() && atSegment.MatchString(first) {+		if _, err := fs.Stat(t.fsys, first); err != nil {+			if t.prefix == "/" {+				t.serveVersion(w, r, first, rest)+				return+			}+			u := (&url.URL{Path: versionPrefix(t.prefix, first) + rest}).String()+			if strings.HasSuffix(r.URL.Path, "/") && rest != "" {+				u += "/"+			}+			if r.URL.RawQuery != "" {+				u += "?" + r.URL.RawQuery+			}+			http.Redirect(w, r, u, http.StatusMovedPermanently)+			return+		}+	}+	info, err := fs.Stat(t.fsys, name)+	if ref := (*refusal)(nil); errors.As(err, &ref) {+		t.fail(w, r, name, http.StatusForbidden, t.why(ref))+		return+	}+	if err != nil {+		t.notFound(w, r, name)+		return+	}+	if info.IsDir() && !strings.HasSuffix(r.URL.Path, "/") {+		// Without the slash, relative links in the listing resolve one level up.+		u := r.URL.EscapedPath() + "/"+		if r.URL.RawQuery != "" {+			u += "?" + r.URL.RawQuery+		}+		http.Redirect(w, r, u, http.StatusMovedPermanently)+		return+	}+	if q, ok := r.URL.Query()["search"]; ok && info.IsDir() {+		t.serveSearch(w, r, name, q[0])+		return+	}+	if _, ok := r.URL.Query()["backlinks"]; ok && !info.IsDir() {+		t.serveBacklinks(w, r, name)+		return+	}+	if _, log := r.URL.Query()["log"]; log {+		t.serveLog(w, r, name, info.IsDir())+		return+	}+	if _, ok := r.URL.Query()["timeline"]; ok {+		t.serveTimeline(w, r, name, info.IsDir())+		return+	}+	if hash, ok := r.URL.Query()["commit"]; ok {+		if q := r.URL.Query(); q.Has("decl") {+			t.serveDecl(w, r, name, info.IsDir(), hash[0], q.Get("file"), q.Get("decl"))+			return+		}+		t.serveCommit(w, r, name, info.IsDir(), hash[0])+		return+	}+	if info.IsDir() {+		t.serveDir(w, r, name)+		return+	}+	if _, raw := r.URL.Query()["raw"]; raw {+		t.serveRaw(w, r, name, info)+		return+	}+	switch {+	case isMarkdown(name):+		t.serveMarkdown(w, r, name)+	case info.Size() <= maxCode:+		t.serveCode(w, r, name, info)+	default:+		t.serveRaw(w, r, name, info)+	}+}++func isMarkdown(name string) bool {+	switch strings.ToLower(path.Ext(name)) {+	case ".md", ".markdown":+		return true+	}+	return false+}++func isText(b []byte) bool {+	return utf8.Valid(b) && bytes.IndexByte(b, 0) < 0+}++func (t *tree) serveMarkdown(w http.ResponseWriter, r *http.Request, name string) {+	b, err := fs.ReadFile(t.fsys, name)+	if err != nil {+		t.fail(w, r, name, http.StatusInternalServerError, err.Error())+		return+	}+	body, err := renderMarkdown(b, t.missing(name), t.webBase())+	if err != nil {+		t.fail(w, r, name, http.StatusInternalServerError, err.Error())+		return+	}+	body = template.HTML(t.logLine(r.Context(), name, b)) + body+	t.render(w, r, http.StatusOK, page{Title: name, Crumbs: t.crumbs(name), Kind: "doc", Body: body, Raw: true})+}++// webBase is where t's docs send a link to a forge: the web address, ""+// on the web address itself, or nil where no server here serves it, eg on+// a share.+func (t *tree) webBase() *string {+	if t.base != nil {+		return t.base.webBase()+	}+	switch s := t.srv; {+	case s == nil:+		return nil+	case s.web != nil:+		return new(string)+	case s.webURL != "":+		return &s.webURL+	}+	return nil+}++// missing reports, for the doc name, whether a relative link names+// nothing the reader could open. A link that climbs out of the tree may+// land in another tree, so it counts as found.+func (t *tree) missing(name string) func(string) bool {+	dir := path.Dir(name)+	return func(dest string) bool {+		target := path.Join(dir, dest)+		if target == ".." || strings.HasPrefix(target, "../") {+			return false+		}+		_, err := fs.Stat(t.fsys, target)+		return errors.Is(err, fs.ErrNotExist)+	}+}++// serveCode shows a text file as a highlighted code page with numbered,+// linkable lines; ?scopes draws each block of code as a box, ?tree+// shows the syntax tree, and ?gron shows a JSON file as one line per node.+// Binary files go raw.+func (t *tree) serveCode(w http.ResponseWriter, r *http.Request, name string, info fs.FileInfo) {+	b, err := fs.ReadFile(t.fsys, name)+	if err != nil {+		t.fail(w, r, name, http.StatusInternalServerError, err.Error())+		return+	}+	if !isText(b) {+		t.serveRaw(w, r, name, info)+		return+	}+	var body strings.Builder+	body.WriteString(t.logLine(r.Context(), name, b))+	// The links to the other views and to the raw file float over the+	// code's top right corner; div.code bounds how far they follow the+	// page down. A tree that cannot be had says why above the flat code.+	q := r.URL.Query()+	_, scoped := q["scopes"]+	if _, ok := q["gron"]; ok && isJSON(name) {+		out, err := gron(b)+		if err == nil {+			body.WriteString(`<div class="code">` + codeViews(name, "gron") + highlightPage(gronLexer, out, false) + `</div>`)+			t.render(w, r, http.StatusOK, page{Title: name, Crumbs: t.crumbs(name), Kind: "code", Body: template.HTML(body.String())})+			return+		}+		body.WriteString("<p class=\"own warn\">No gron view, since this is not valid JSON: " + template.HTMLEscapeString(err.Error()) + "</p>\n")+	}+	if _, ok := q["tree"]; ok {+		tree, err := syntaxPage(r.Context(), name, pageHref(r.URL.Path), b)+		if err == nil {+			// A large file's tree is megabytes, so it is written once,+			// into room made for it.+			body.Grow(len(tree) + 1024)+			body.WriteString(`<div class="code">` + codeViews(name, "tree"))+			body.WriteString(tree)+			body.WriteString(`</div>`)+			t.render(w, r, http.StatusOK, page{Title: name, Crumbs: t.crumbs(name), Kind: "tree", Body: template.HTML(body.String())})+			return+		}+		logErr("no syntax tree for %s: %v", name, err)+		body.WriteString(treeNote(name, err))+	}+	view := "flat"+	if scoped {+		view = "scopes"+	}+	body.WriteString(`<div class="code">` + codeViews(name, view))+	body.WriteString(highlightPage(codeLexer(name), b, scoped))+	body.WriteString(`</div>`)+	t.render(w, r, http.StatusOK, page{Title: name, Crumbs: t.crumbs(name), Kind: "code", Body: template.HTML(body.String())})+}++// serveRaw sends the file itself. Text goes as plain text, so a stray .html+// or .ts file shows as source instead of rendering or downloading.+func (t *tree) serveRaw(w http.ResponseWriter, r *http.Request, name string, info fs.FileInfo) {+	f, err := t.fsys.Open(name)+	if err != nil {+		t.notFound(w, r, name)+		return+	}+	defer f.Close()+	rs, ok := f.(io.ReadSeeker)+	if !ok {+		t.fail(w, r, name, http.StatusInternalServerError, "file is not seekable")+		return+	}+	if strings.ToLower(path.Ext(name)) != ".svg" {+		head := make([]byte, 512)+		n, _ := io.ReadFull(rs, head)+		if isText(head[:n]) {+			w.Header().Set("Content-Type", "text/plain; charset=utf-8")+		}+		rs.Seek(0, io.SeekStart)+	}+	http.ServeContent(w, r, name, info.ModTime(), rs)+}++func (t *tree) serveDir(w http.ResponseWriter, r *http.Request, name string) {+	entries, err := fs.ReadDir(t.fsys, name)+	if err != nil {+		t.fail(w, r, name, http.StatusInternalServerError, err.Error())+		return+	}+	type item struct {+		name string+		dir  bool+		ref  *refusal+	}+	// Entries reached through a link out of the tree are checked one by+	// one, since each may be refused. So is every entry of a shared tree.+	// A version holds only what git stored, so nothing leads out of it.+	outside := false+	if t.at == nil {+		_, err = t.root.Stat(name)+		outside = err != nil || t.shared+	}+	var items []item+	for _, e := range entries {+		n := e.Name()+		if t.unlisted(name, n) {+			continue+		}+		dir := e.IsDir()+		var ref *refusal+		if e.Type()&fs.ModeSymlink != 0 || outside {+			ref = t.refused(path.Join(name, n))+			if ref != nil {+				if ref.link == path.Join(name, n) {+					dir = ref.dir+				}+			} else if info, err := fs.Stat(t.fsys, path.Join(name, n)); err == nil {+				dir = info.IsDir()+			}+		}+		items = append(items, item{n, dir, ref})+	}+	slices.SortStableFunc(items, func(a, b item) int {+		if a.dir != b.dir {+			if a.dir {+				return -1+			}+			return 1+		}+		return strings.Compare(a.name, b.name)+	})++	var body strings.Builder+	if t.at != nil {+		body.WriteString(t.versionLine(r.Context(), name, true))+	}+	body.WriteString("<ul class=\"listing\">\n")+	if t.lastCommit(r.Context(), name) != nil {+		body.WriteString("<li class=\"log\"><a href=\"?log\">" + template.HTMLEscapeString(logLabel) + "</a></li>\n")+	}+	if name != "." {+		body.WriteString("<li><a href=\"../\">../</a></li>\n")+	}+	for _, it := range items {+		label := it.name+		if it.dir {+			label += "/"+		}+		href := (&url.URL{Path: label}).String()+		if it.ref != nil && (t.shared || t.public) {+			fmt.Fprintf(&body, "<li class=\"refused\">%s <span class=\"own\">refused</span></li>\n", template.HTMLEscapeString(label))+			continue+		}+		if it.ref != nil {+			fmt.Fprintf(&body, "<li class=\"refused\"><a href=\"%s\" title=\"%s\">%s</a> <span class=\"own\">refused</span></li>\n",+				template.HTMLEscapeString(href), template.HTMLEscapeString(it.ref.Error()), template.HTMLEscapeString(label))+			continue+		}+		fmt.Fprintf(&body, "<li><a href=\"%s\">%s</a></li>\n",+			template.HTMLEscapeString(href), template.HTMLEscapeString(label))+	}+	body.WriteString("</ul>\n")+	t.render(w, r, http.StatusOK, page{+		Title:  t.title(name),+		Crumbs: t.crumbs(name),+		Kind:   "dir",+		Body:   template.HTML(body.String()),+	})+}