package core import ( "bytes" "encoding/json" "fmt" "html/template" "net/http" ) // TemplateSet holds one independently-parsed template tree per view. // Views are parsed separately because html/template has a single namespace // for defined blocks -- two views both defining "main" would collide. type TemplateSet map[string]*template.Template // Renderer turns a Ctx into bytes plus a content type. HTML pages and JSON // endpoints are the same shape of thing; this is where they unify. type Renderer interface { Render(c *Ctx, ts TemplateSet) ([]byte, error) ContentType() string } // View renders an HTML template. The template calls modules via `mod`. type View struct{ Template string } func (v View) ContentType() string { return "text/html; charset=utf-8" } func (v View) Render(c *Ctx, ts TemplateSet) ([]byte, error) { t, ok := ts[v.Template] if !ok { return nil, fmt.Errorf("no such view %q", v.Template) } // Clone per render so the funcmap can close over this Ctx. tt, err := t.Clone() if err != nil { return nil, err } tt = tt.Funcs(FuncMap(c)) var buf bytes.Buffer if err := tt.ExecuteTemplate(&buf, v.Template, c); err != nil { return nil, fmt.Errorf("view %s: %w", v.Template, err) } return buf.Bytes(), nil } // Endpoint builds the value that will be serialized. It must return a // purpose-built DTO, never a raw module value -- see internal/api. type Endpoint func(c *Ctx) (any, error) // JSON renders an API endpoint. type JSON struct{ Build Endpoint } func (j JSON) ContentType() string { return "application/json; charset=utf-8" } func (j JSON) Render(c *Ctx, _ TemplateSet) ([]byte, error) { v, err := j.Build(c) if err != nil { return nil, err } b, err := json.MarshalIndent(v, "", " ") if err != nil { return nil, err } return append(b, '\n'), nil } // ------------------------------------------------------------------ routes // Route binds a renderer to a net/http pattern (Go 1.22 wildcards). // // /{repo}/tree/{rest...} -> r.PathValue("repo"), r.PathValue("rest") // // Scope decides how the Ctx is built before Render runs. type Route struct { Name string Pattern string // e.g. "GET /{repo}/commit/{rev}" Render Renderer Scope Scope // Immutable marks responses whose content cannot change for a given // URL -- anything addressed by a commit SHA. Those get a year-long // Cache-Control; ref-addressed URLs get revalidation instead. Immutable bool // CacheableAnon opts a route into Server's in-memory response cache for // anonymous, shareless requests (see Server.cacheEligible) -- a full // rendered response is stored and replayed to the next matching // request within Ctx.cacheMaxAge, without touching git at all. Reserved // for the small-HTML page routes most likely to be a spike's hot page; // left off routes whose bodies can be large or unboundedly numerous // (blob, raw, compare -- see routes.go) and the JSON api-* endpoints, // to keep this pass's scope and memory footprint bounded: respCache is // an LRU bounded by entry count, not bytes. CacheableAnon bool // NavLabel, when set on a ScopeRepo route, adds a tab for it to the // repo-scoped navigation (see nav.go) -- the top tab bar and the // page-nav chips both derive from this instead of hardcoding links. NavLabel string } type Scope int const ( ScopeRepo Scope = iota // resolves {repo}, and {rest...}/{rev} if present ScopeSite // no repository in the URL ) // Page sizes for routes that paginate via ?ofs=. Kept in core (not // modules) so buildCtx can quantize an incoming ofs to a real page // boundary without an import cycle. Must match the literal args the // log.html/graph.html templates pass to `mod "log"`/`mod "graph"`. const ( LogPageSize = 20 GraphPageSize = 150 ) // httpError lets a module or endpoint choose the status code. type httpError struct { Code int Err error } func (e httpError) Error() string { return e.Err.Error() } func (e httpError) Unwrap() error { return e.Err } func Errorf(code int, format string, a ...any) error { return httpError{Code: code, Err: fmt.Errorf(format, a...)} } func statusOf(err error) int { var he httpError if ok := asHTTPError(err, &he); ok { return he.Code } return http.StatusInternalServerError } // HeaderSetter lets a renderer add response headers. Optional: the server // type-asserts for it after Render succeeds. type HeaderSetter interface { Headers(c *Ctx, h http.Header) } // Raw serves file bytes straight out of the object database. // // SECURITY: a repository can contain an .html file with a