230 lines
5.9 KiB
Go
230 lines
5.9 KiB
Go
package main
|
|
|
|
import (
|
|
"html"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
)
|
|
|
|
func ChangelogPage(w http.ResponseWriter, r *http.Request) {
|
|
markdown := readChangelog()
|
|
body := `
|
|
<header class="tool-header">
|
|
<h2 class="tool-title">Changelog</h2>
|
|
<p class="tool-description">Loaded from README.md.</p>
|
|
</header>
|
|
<section class="tool-panel changelog-panel">
|
|
<div class="panel-body changelog-content">
|
|
` + renderChangelogMarkdown(markdown) + `
|
|
</div>
|
|
</section>
|
|
<style>
|
|
.changelog-panel { max-width: 840px; }
|
|
.changelog-content h1 { margin: 0 0 14px; font-size: 20px; line-height: 1.25; }
|
|
.changelog-content h2 { margin: 18px 0 8px; padding-top: 12px; border-top: 1px solid #333; font-size: 15px; line-height: 1.3; }
|
|
.changelog-content h3 { margin: 14px 0 6px; font-size: 13px; line-height: 1.3; }
|
|
.changelog-content ul { margin: 6px 0 12px; padding-left: 20px; }
|
|
.changelog-content li { margin: 4px 0; line-height: 1.45; }
|
|
.changelog-content li.nested { margin-left: 18px; }
|
|
.changelog-content p { margin: 8px 0; line-height: 1.5; }
|
|
.changelog-content code { font-family: Consolas, monospace; font-size: 0.95em; background: rgba(128,128,128,0.12); padding: 1px 4px; border-radius: 3px; }
|
|
.changelog-block { margin: 8px 0 18px; padding: 10px 12px; border: 1px solid #333; border-radius: 4px; background: rgba(128,128,128,0.08); }
|
|
.change-section { margin: 8px 0 4px; font-weight: 600; }
|
|
.changelog-block ul { margin: 4px 0 10px; }
|
|
</style>`
|
|
|
|
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("</ul>")
|
|
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("<h2>" + renderInlineMarkdown(strings.TrimSpace(line[4:])) + "</h2>")
|
|
continue
|
|
}
|
|
if strings.HasPrefix(line, "## ") {
|
|
closeList()
|
|
out.WriteString("<h2>" + renderInlineMarkdown(strings.TrimSpace(line[3:])) + "</h2>")
|
|
continue
|
|
}
|
|
if strings.HasPrefix(line, "# ") {
|
|
closeList()
|
|
out.WriteString("<h1>" + renderInlineMarkdown(strings.TrimSpace(line[2:])) + "</h1>")
|
|
continue
|
|
}
|
|
if strings.HasPrefix(line, "**") && strings.HasSuffix(line, "**") && len(line) > 4 {
|
|
closeList()
|
|
out.WriteString("<h2>" + html.EscapeString(strings.TrimSuffix(strings.TrimPrefix(line, "**"), "**")) + "</h2>")
|
|
continue
|
|
}
|
|
if strings.HasPrefix(line, "- ") {
|
|
if !inList {
|
|
out.WriteString("<ul>")
|
|
inList = true
|
|
}
|
|
out.WriteString("<li>" + renderInlineMarkdown(strings.TrimSpace(line[2:])) + "</li>")
|
|
continue
|
|
}
|
|
closeList()
|
|
out.WriteString("<p>" + renderInlineMarkdown(line) + "</p>")
|
|
}
|
|
if inCodeBlock {
|
|
out.WriteString(renderChangelogBlock(codeLines))
|
|
}
|
|
closeList()
|
|
return out.String()
|
|
}
|
|
|
|
func renderChangelogBlock(lines []string) string {
|
|
var out strings.Builder
|
|
inList := false
|
|
closeList := func() {
|
|
if inList {
|
|
out.WriteString("</ul>")
|
|
inList = false
|
|
}
|
|
}
|
|
|
|
out.WriteString(`<div class="changelog-block">`)
|
|
for _, rawLine := range lines {
|
|
line := strings.TrimSpace(rawLine)
|
|
if line == "" {
|
|
closeList()
|
|
continue
|
|
}
|
|
if strings.HasPrefix(line, "- ") {
|
|
if !inList {
|
|
out.WriteString("<ul>")
|
|
inList = true
|
|
}
|
|
className := ""
|
|
if leadingSpaceCount(rawLine) >= 8 {
|
|
className = ` class="nested"`
|
|
}
|
|
out.WriteString("<li" + className + ">" + renderInlineMarkdown(strings.TrimSpace(line[2:])) + "</li>")
|
|
continue
|
|
}
|
|
closeList()
|
|
out.WriteString(`<div class="change-section">` + renderInlineMarkdown(line) + `</div>`)
|
|
}
|
|
closeList()
|
|
out.WriteString("</div>")
|
|
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
|
|
}
|