package main
import (
"html"
"regexp"
"strconv"
"strings"
)
var (
headingRe = regexp.MustCompile(`^(#{1,6})\s+(.*?)\s*#*\s*$`)
listRe = regexp.MustCompile(`^(\s*)([-*+]|\d+[.)])\s+(.*)$`)
sepRe = regexp.MustCompile(`^\s*\|?\s*:?-{2,}:?\s*(\|\s*:?-{2,}:?\s*)*\|?\s*$`)
linkRe = regexp.MustCompile(`\[([^\]]+)\]\(([^)\s]+)\)`)
boldRe = regexp.MustCompile(`\*\*([^*]+)\*\*`)
slugRe = regexp.MustCompile(`[^\p{L}\p{N}]+`)
)
// markdown converts the subset of Markdown used by the docs to HTML; link rewrites relative hrefs.
type markdown struct {
link func(href string) string
}
func (m markdown) render(src string) string {
lines := strings.Split(strings.ReplaceAll(src, "\r\n", "\n"), "\n")
var b strings.Builder
m.blocks(&b, lines)
return b.String()
}
func (m markdown) blocks(b *strings.Builder, lines []string) {
for i := 0; i < len(lines); {
line := lines[i]
trim := strings.TrimSpace(line)
switch {
case trim == "":
i++
case strings.HasPrefix(trim, "```"):
i = m.fence(b, lines, i)
case headingRe.MatchString(trim):
g := headingRe.FindStringSubmatch(trim)
n := len(g[1])
text := m.inline(g[2])
if n == 1 {
b.WriteString("
" + text + "
\n")
} else {
id := slug(g[2])
b.WriteString("` + text + "\n")
}
i++
case trim == "---" || trim == "***":
b.WriteString("\n")
i++
case strings.HasPrefix(trim, "|") && i+1 < len(lines) && sepRe.MatchString(lines[i+1]):
i = m.table(b, lines, i)
case listRe.MatchString(line):
i = m.list(b, lines, i)
case strings.HasPrefix(trim, ">"):
var inner []string
for i < len(lines) && strings.HasPrefix(strings.TrimSpace(lines[i]), ">") {
inner = append(inner, strings.TrimPrefix(strings.TrimPrefix(strings.TrimSpace(lines[i]), ">"), " "))
i++
}
b.WriteString("
\n")
m.blocks(b, inner)
b.WriteString("
\n")
default:
var para []string
for i < len(lines) && strings.TrimSpace(lines[i]) != "" && !startsBlock(lines, i) {
para = append(para, strings.TrimSpace(lines[i]))
i++
}
if len(para) == 0 {
para = append(para, trim)
i++
}
b.WriteString("
" + m.inline(strings.Join(para, "\n")) + "
\n")
}
}
}
func startsBlock(lines []string, i int) bool {
t := strings.TrimSpace(lines[i])
return strings.HasPrefix(t, "```") || headingRe.MatchString(t) || listRe.MatchString(lines[i]) ||
strings.HasPrefix(t, ">") || (strings.HasPrefix(t, "|") && i+1 < len(lines) && sepRe.MatchString(lines[i+1]))
}
func (m markdown) fence(b *strings.Builder, lines []string, i int) int {
open := strings.TrimSpace(lines[i])
lang := strings.TrimSpace(strings.TrimLeft(open, "`"))
var code []string
i++
for i < len(lines) && !strings.HasPrefix(strings.TrimSpace(lines[i]), "```") {
code = append(code, lines[i])
i++
}
class := ""
if lang != "" {
class = ` class="lang-` + html.EscapeString(lang) + `"`
}
b.WriteString(`