package core import ( "fmt" "html/template" "strings" "time" ) // FuncMap is bound to a single Ctx at render time. `mod` is the only // entry point a template needs; everything else is sugar. func FuncMap(c *Ctx) template.FuncMap { return template.FuncMap{ // mod runs a module. A template that calls it is what *causes* // the work -- views that don't ask, don't pay. "mod": func(name string, args ...string) (any, error) { return c.Mod(name, args...) }, // modOr swallows the error, for optional modules (no README, etc). "modOr": func(name string, args ...string) any { v, err := c.Mod(name, args...) if err != nil { return nil } return v }, "has": func(name string) bool { v, err := c.Mod(name) return err == nil && v != nil }, "url": func(parts ...string) string { return "/" + strings.Trim(strings.Join(parts, "/"), "/") }, // static builds a cache-busting asset URL, e.g. "css/base.css" -> // "/static/css/base.css?v=". The `v` param is what lets // main.go's static handler hand out a year-long immutable // Cache-Control instead of a short one -- a URL without it is // still served, just not frozen for a year. "static": func(p string) string { return "/static/" + p + "?v=" + c.StaticTag }, "dict": dict, "date": func(t time.Time) string { if t.IsZero() { return "" } return t.Format("2006-01-02 15:04") }, "rfc3339": func(t time.Time) string { return t.Format(time.RFC3339) }, "trunc": func(n int, s string) string { if len(s) <= n { return s } return s[:n] + "…" }, "bytes": humanBytes, "patch": renderPatch, "slug": slugify, "bytes64": func(n int) string { return humanBytes(int64(n)) }, } } // slugify makes a section name usable as a fragment id. func slugify(s string) string { var b strings.Builder prevDash := false for _, r := range strings.ToLower(s) { switch { case (r >= 'a' && r <= 'z') || (r >= '0' && r <= '9'): b.WriteRune(r) prevDash = false case !prevDash && b.Len() > 0: b.WriteByte('-') prevDash = true } } return strings.TrimRight(b.String(), "-") } // renderPatch tags each diff line so the stylesheet can colour it. Doing // this here rather than in CSS lets the gutter carry the signal instead of // flooding whole rows with green and red. func renderPatch(p string) template.HTML { var b strings.Builder b.WriteString(`
`) for _, ln := range strings.Split(strings.TrimRight(p, "\n"), "\n") { cls := "ctx" switch { case strings.HasPrefix(ln, "+++"), strings.HasPrefix(ln, "---"), strings.HasPrefix(ln, "diff "), strings.HasPrefix(ln, "index "), strings.HasPrefix(ln, "new file"), strings.HasPrefix(ln, "deleted file"), strings.HasPrefix(ln, "similarity"), strings.HasPrefix(ln, "rename "): cls = "meta" case strings.HasPrefix(ln, "@@"): cls = "hunk" case strings.HasPrefix(ln, "+"): cls = "add" case strings.HasPrefix(ln, "-"): cls = "del" } b.WriteString(`
`) b.WriteString(template.HTMLEscapeString(ln)) b.WriteString("
") } b.WriteString(`
`) return template.HTML(b.String()) } // dict builds a map for passing named args to partials, mirroring Hugo's // dict so shared partials stay portable later. func dict(kv ...any) (map[string]any, error) { if len(kv)%2 != 0 { return nil, fmt.Errorf("dict: odd number of arguments") } m := make(map[string]any, len(kv)/2) for i := 0; i < len(kv); i += 2 { k, ok := kv[i].(string) if !ok { return nil, fmt.Errorf("dict: key %d is not a string", i) } m[k] = kv[i+1] } return m, nil } func humanBytes(n int64) string { const unit = 1024 if n < unit { return fmt.Sprintf("%d B", n) } div, exp := int64(unit), 0 for x := n / unit; x >= unit; x /= unit { div *= unit exp++ } return fmt.Sprintf("%.1f %cB", float64(n)/float64(div), "KMGT"[exp]) }