package main import ( "embed" "fmt" "html" "html/template" "io" "path" "regexp" "strings" "time" ) //go:embed templates/*.html var templateFS embed.FS //go:embed static/style.css var cssContent []byte //go:embed static/radiant.svg var logoContent []byte //go:embed static/fonts/* var fontsFS embed.FS //go:embed static/avatars/* var avatarsFS embed.FS //go:embed static/js/hirad.js var hiradJS []byte //go:embed static/js/hiril.js var hirilJS []byte type templateSet struct { templates map[string]*template.Template } var funcMap = template.FuncMap{ "shortHash": func(h string) string { if len(h) > 8 { return h[:8] } return h }, "timeAgo": func(t time.Time) template.HTML { d := time.Since(t) var ago string switch { case d < time.Minute: ago = "just now" case d < time.Hour: m := int(d.Minutes()) if m == 1 { ago = "1 minute" } else { ago = fmt.Sprintf("%d minutes", m) } case d < 24*time.Hour: h := int(d.Hours()) if h == 1 { ago = "1 hour" } else { ago = fmt.Sprintf("%d hours", h) } case d < 30*24*time.Hour: days := int(d.Hours() / 24) if days == 1 { ago = "1 day" } else { ago = fmt.Sprintf("%d days", days) } case d < 365*24*time.Hour: months := int(d.Hours() / 24 / 30) if months == 1 { ago = "1 month" } else { ago = fmt.Sprintf("%d months", months) } default: years := int(d.Hours() / 24 / 365) if years == 1 { ago = "1 year" } else { ago = fmt.Sprintf("%d years", years) } } full := t.Format("2006-01-02 15:04:05 -0700") return template.HTML(fmt.Sprintf(``, full, ago)) }, "formatDate": func(t time.Time) string { return t.Format("2006-01-02 15:04:05 -0700") }, "add": func(a, b int) int { return a + b }, "diffFileName": func(f DiffFile) string { if f.NewName != "" { return f.NewName } return f.OldName }, "statusLabel": func(s string) string { switch s { case "A": return "added" case "D": return "deleted" case "M": return "modified" case "R": return "renamed" } return s }, "formatSize": func(size int64) string { switch { case size < 1024: return fmt.Sprintf("%d B", size) case size < 1024*1024: return fmt.Sprintf("%.1f KiB", float64(size)/1024) default: return fmt.Sprintf("%.1f MiB", float64(size)/(1024*1024)) } }, "diffBar": func(added, deleted int) template.HTML { total := added + deleted if total == 0 { return "" } const maxBlocks = 5 blocks := maxBlocks if total < blocks { blocks = total } addBlocks := 0 if total > 0 { addBlocks = (added*blocks + total - 1) / total } if addBlocks > blocks { addBlocks = blocks } delBlocks := blocks - addBlocks var b strings.Builder b.WriteString(``) return template.HTML(b.String()) }, "langClass": func(name string) string { ext := path.Ext(name) switch ext { case ".rad": return "language-radiance" case ".ril": return "language-ril" } return "" }, "parentPath": func(p string) string { dir := path.Dir(p) if dir == "." { return "" } return dir }, "indent": func(depth int) string { if depth == 0 { return "" } return fmt.Sprintf("padding-left: %grem", float64(depth)*1.2) }, "formatBody": func(s string) template.HTML { s = strings.TrimSpace(s) if s == "" { return "" } var b strings.Builder // Split into alternating prose / fenced-code-block segments. for s != "" { // Find the next opening fence. openIdx := strings.Index(s, "```") if openIdx < 0 { formatParagraphs(&b, s) break } // Prose before the fence. if openIdx > 0 { formatParagraphs(&b, s[:openIdx]) } // Skip the opening ``` and any language tag on the same line. rest := s[openIdx+3:] if nl := strings.Index(rest, "\n"); nl >= 0 { rest = rest[nl+1:] } else { // ``` at end of input with nothing after — treat as prose. formatParagraphs(&b, s[openIdx:]) break } // Find the closing fence. closeIdx := strings.Index(rest, "```") var code string if closeIdx < 0 { code = rest s = "" } else { code = rest[:closeIdx] s = rest[closeIdx+3:] } code = strings.TrimRight(code, "\n") b.WriteString("
")
b.WriteString(html.EscapeString(code))
b.WriteString("")
}
return template.HTML(b.String())
},
"discussionPath": func(baseURL, repo string) string {
if repo == "" {
return baseURL + "/discussions"
}
return baseURL + "/" + repo + "/discussions"
},
"autolink": func(s string) template.HTML {
var b strings.Builder
last := 0
for _, loc := range urlRe.FindAllStringIndex(s, -1) {
// Strip trailing punctuation.
end := loc[1]
for end > loc[0] && strings.ContainsRune(".,;:!?)]", rune(s[end-1])) {
end--
}
b.WriteString(html.EscapeString(s[last:loc[0]]))
u := s[loc[0]:end]
b.WriteString(``)
b.WriteString(html.EscapeString(u))
b.WriteString(``)
last = end
}
b.WriteString(html.EscapeString(s[last:]))
return template.HTML(b.String())
},
}
var urlRe = regexp.MustCompile(`https?://[^\s<>"'` + "`" + `\x00-\x1f]+`)
// inlineRe matches (in order): backtick code, angle-bracket links, bare URLs, or *italic*.
var inlineRe = regexp.MustCompile("`" + `([^` + "`" + `\n]+)` + "`" +
`|<(https?://[^\s<>]+)>` +
`|(https?://[^\s<>"'` + "`" + `\x00-\x1f]+)` +
`|\*([^\s*][^*]*[^\s*])\*|\*([^\s*])\*`)
// formatParagraphs splits prose text on blank lines and writes elements // with inline formatting into b. func formatParagraphs(b *strings.Builder, s string) { for _, p := range strings.Split(s, "\n\n") { p = strings.TrimSpace(p) if p == "" { continue } b.WriteString("
") b.WriteString(formatInline(p)) b.WriteString("
") } } // formatInline applies inline formatting to a paragraph of raw text. // It handles backtick `code`, *italic*,")
b.WriteString(html.EscapeString(code))
b.WriteString("")
case m[4] >= 0: // angle-bracket link: group 2
u := p[m[4]:m[5]]
b.WriteString(``)
b.WriteString(html.EscapeString(u))
b.WriteString("")
case m[6] >= 0: // bare URL: group 3
// Strip trailing punctuation.
urlEnd := m[7]
for urlEnd > m[6] && strings.ContainsRune(".,;:!?)]", rune(p[urlEnd-1])) {
urlEnd--
}
u := p[m[6]:urlEnd]
b.WriteString(``)
b.WriteString(html.EscapeString(u))
b.WriteString("")
// Anything between stripped punctuation and regex end is plain text.
b.WriteString(html.EscapeString(p[urlEnd:end]))
case m[8] >= 0: // italic (multi-char): group 4
inner := p[m[8]:m[9]]
b.WriteString("")
b.WriteString(html.EscapeString(inner))
b.WriteString("")
case m[10] >= 0: // italic (single-char): group 5
inner := p[m[10]:m[11]]
b.WriteString("")
b.WriteString(html.EscapeString(inner))
b.WriteString("")
}
last = end
}
b.WriteString(html.EscapeString(p[last:]))
return b.String()
}
func loadTemplates() (*templateSet, error) {
layoutContent, err := templateFS.ReadFile("templates/layout.html")
if err != nil {
return nil, fmt.Errorf("read layout: %w", err)
}
pages := []string{"index", "home", "log", "commit", "refs", "error", "discussions", "discussion", "discussion_new", "login"}
ts := &templateSet{
templates: make(map[string]*template.Template, len(pages)),
}
for _, page := range pages {
t := template.New("layout").Funcs(funcMap)
t, err = t.Parse(string(layoutContent))
if err != nil {
return nil, fmt.Errorf("parse layout: %w", err)
}
pageContent, err := templateFS.ReadFile(fmt.Sprintf("templates/%s.html", page))
if err != nil {
return nil, fmt.Errorf("read %s: %w", page, err)
}
t, err = t.Parse(string(pageContent))
if err != nil {
return nil, fmt.Errorf("parse %s: %w", page, err)
}
ts.templates[page] = t
}
return ts, nil
}
func (ts *templateSet) render(w io.Writer, name string, data any) {
t, ok := ts.templates[name]
if !ok {
fmt.Fprintf(w, "template %q not found", name)
return
}
if err := t.Execute(w, data); err != nil {
fmt.Fprintf(w, "template error: %v", err)
}
}