go-help/templates.go
Snider 62c5d65374
All checks were successful
Security Scan / security (push) Successful in 14s
Test / test (push) Successful in 1m44s
feat: modernise to Go 1.26 iterators and stdlib helpers
Add Catalog.All(), Catalog.SearchResults(), AllSections(), Tokens()
iterators. Use slices.SortFunc, slices.Clone, maps.Values, built-in
min replacing min3, strings.SplitSeq for streaming, range-over-int
in levenshtein and benchmarks.

Co-Authored-By: Gemini <noreply@google.com>
Co-Authored-By: Virgil <virgil@lethean.io>
2026-02-23 05:32:21 +00:00

129 lines
3 KiB
Go

// SPDX-Licence-Identifier: EUPL-1.2
package help
import (
"cmp"
"embed"
"html/template"
"io"
"slices"
"strings"
)
//go:embed templates/*.html
var templateFS embed.FS
// templateFuncs returns the function map for help templates.
func templateFuncs() template.FuncMap {
return template.FuncMap{
"renderMarkdown": func(content string) template.HTML {
html, err := RenderMarkdown(content)
if err != nil {
return template.HTML("<p>Error rendering content.</p>")
}
return template.HTML(html) //nolint:gosec // trusted content from catalog
},
"truncate": func(s string, n int) string {
// Strip markdown headings for truncation preview
lines := strings.SplitSeq(s, "\n")
var clean []string
for line := range lines {
trimmed := strings.TrimSpace(line)
if trimmed == "" || strings.HasPrefix(trimmed, "#") {
continue
}
clean = append(clean, trimmed)
}
text := strings.Join(clean, " ")
runes := []rune(text)
if len(runes) <= n {
return text
}
return string(runes[:n]) + "..."
},
"pluralise": func(count int, singular, plural string) string {
if count == 1 {
return singular
}
return plural
},
"multiply": func(a, b int) int {
return a * b
},
"sub": func(a, b int) int {
return a - b
},
}
}
// parseTemplates parses the base layout together with a page template.
func parseTemplates(page string) (*template.Template, error) {
return template.New("base.html").Funcs(templateFuncs()).ParseFS(
templateFS, "templates/base.html", "templates/"+page,
)
}
// topicGroup groups topics under a tag for the index page.
type topicGroup struct {
Tag string
Topics []*Topic
}
// indexData holds template data for the index page.
type indexData struct {
Topics []*Topic
Groups []topicGroup
}
// topicData holds template data for a single topic page.
type topicData struct {
Topic *Topic
}
// searchData holds template data for the search results page.
type searchData struct {
Query string
Results []*SearchResult
}
// groupTopicsByTag groups topics by their first tag.
// Topics without tags are grouped under "other".
// Groups are sorted alphabetically by tag name.
func groupTopicsByTag(topics []*Topic) []topicGroup {
// Sort topics first by Order then Title
sorted := slices.Clone(topics)
slices.SortFunc(sorted, func(a, b *Topic) int {
if a.Order != b.Order {
return cmp.Compare(a.Order, b.Order)
}
return cmp.Compare(a.Title, b.Title)
})
groups := make(map[string][]*Topic)
for _, t := range sorted {
tag := "other"
if len(t.Tags) > 0 {
tag = t.Tags[0]
}
groups[tag] = append(groups[tag], t)
}
var result []topicGroup
for tag, topics := range groups {
result = append(result, topicGroup{Tag: tag, Topics: topics})
}
slices.SortFunc(result, func(a, b topicGroup) int {
return cmp.Compare(a.Tag, b.Tag)
})
return result
}
// renderPage renders a named page template into the writer.
func renderPage(w io.Writer, page string, data any) error {
tmpl, err := parseTemplates(page)
if err != nil {
return err
}
return tmpl.Execute(w, data)
}