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 { if embeddedReadme != "" { return embeddedReadme } 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 inCodeBlock := false codeLines := []string{} closeList := func() { if inList { out.WriteString("") inList = false } } for _, rawLine := range lines { if strings.TrimSpace(rawLine) == "```" { closeList() if inCodeBlock { out.WriteString(renderChangelogBlock(codeLines)) codeLines = []string{} inCodeBlock = false } else { inCodeBlock = true } continue } if inCodeBlock { codeLines = append(codeLines, rawLine) continue } line := strings.TrimSpace(rawLine) if line == "" { closeList() continue } if strings.HasPrefix(line, "### ") { closeList() out.WriteString("

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

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

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

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

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

") continue } if strings.HasPrefix(line, "**") && strings.HasSuffix(line, "**") && len(line) > 4 { closeList() out.WriteString("

" + html.EscapeString(strings.TrimSuffix(strings.TrimPrefix(line, "**"), "**")) + "

") continue } if strings.HasPrefix(line, "- ") { if !inList { out.WriteString("") inList = false } } out.WriteString(`
`) for _, rawLine := range lines { line := strings.TrimSpace(rawLine) if line == "" { closeList() continue } if strings.HasPrefix(line, "- ") { if !inList { out.WriteString("
") return out.String() } func renderInlineMarkdown(value string) string { escaped := html.EscapeString(value) escaped = replaceDelimited(escaped, "**", "strong") escaped = replaceDelimited(escaped, "`", "code") return escaped } func replaceDelimited(value, marker, tag string) string { var out strings.Builder for { start := strings.Index(value, marker) if start < 0 { out.WriteString(value) break } end := strings.Index(value[start+len(marker):], marker) if end < 0 { out.WriteString(value) break } end += start + len(marker) out.WriteString(value[:start]) out.WriteString("<" + tag + ">") out.WriteString(value[start+len(marker) : end]) out.WriteString("") value = value[end+len(marker):] } return out.String() } func leadingSpaceCount(value string) int { count := 0 for _, ch := range value { if ch != ' ' { break } count++ } return count }