package main
import (
"html"
"net/http"
"os"
"path/filepath"
"strings"
)
func ChangelogPage(w http.ResponseWriter, r *http.Request) {
markdown := readChangelog()
body := `
`
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("" + tag + ">")
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
}