package main import ( "html" "net/http" "os" "path/filepath" "strings" ) func ChangelogPage(w http.ResponseWriter, r *http.Request) { markdown := readChangelog() body := `

Changelog

Loaded from README.md.

` + renderChangelogMarkdown(markdown) + `
` w.Header().Set("Content-Type", "text/html; charset=utf-8") _, _ = w.Write([]byte(Page("/changelog", "Changelog", body))) } func readChangelog() string { for _, start := range changelogSearchRoots() { for _, candidate := range changelogCandidates(start) { data, err := os.ReadFile(candidate) if err == nil { return string(data) } } } return "# Changelog\n\nREADME.md was not found." } func changelogSearchRoots() []string { roots := []string{} if wd, err := os.Getwd(); err == nil { roots = append(roots, wd) } roots = append(roots, appDir()) return roots } func changelogCandidates(start string) []string { candidates := []string{} dir := filepath.Clean(start) for i := 0; i < 5; i++ { candidates = append(candidates, filepath.Join(dir, "README.md")) parent := filepath.Dir(dir) if parent == dir { break } dir = parent } return candidates } func renderChangelogMarkdown(markdown string) string { lines := strings.Split(strings.ReplaceAll(markdown, "\r\n", "\n"), "\n") var out strings.Builder inList := false closeList := func() { if inList { out.WriteString("") inList = false } } for _, rawLine := range lines { line := strings.TrimSpace(rawLine) if line == "" { closeList() continue } if strings.HasPrefix(line, "### ") { closeList() out.WriteString("

" + html.EscapeString(strings.TrimSpace(line[4:])) + "

") continue } if strings.HasPrefix(line, "## ") { closeList() out.WriteString("

" + html.EscapeString(strings.TrimSpace(line[3:])) + "

") continue } if strings.HasPrefix(line, "# ") { closeList() out.WriteString("

" + html.EscapeString(strings.TrimSpace(line[2:])) + "

") continue } if strings.HasPrefix(line, "- ") { if !inList { out.WriteString("