feat(11.1-02): add the documentation theme, chroma highlighting and docs:serve

- WinterCMS-style shell: header with search and theme toggle, grouped
  sidebar, on-page TOC, pager, page actions, callouts, heading
  permalinks, footer and a 404 page
- fenced code highlighted at build time by chroma/v2 into tok-* classes,
  with a copy button; no inline script, style or handler
- vendored DM Sans/DM Mono fonts and Lucide icons with their licences
- client-side search over search-index.json built with textContent only
- summer docs:serve builds into a temp dir, serves on loopback by default,
  returns 404.html with status 404 and rebuilds on change
This commit is contained in:
Jakub Zych
2026-09-30 21:57:55 +02:00
parent d89e18bc8e
commit dd11bdb0c7
39 changed files with 3114 additions and 145 deletions

View File

@@ -6,6 +6,7 @@ import (
"fmt"
"html"
"path"
"regexp"
"slices"
"strings"
"unicode"
@@ -33,11 +34,16 @@ func newMarkdown() goldmark.Markdown {
util.Prioritized(h1Stripper{}, 100),
util.Prioritized(linkRewriter{}, 200),
util.Prioritized(fenceAnnotator{}, 300),
util.Prioritized(calloutTransformer{}, 400),
),
),
// goldmark registers lower priority values last, so 100 overrides the
// default html renderer (1000) for fenced code blocks.
goldmark.WithRendererOptions(renderer.WithNodeRenderers(util.Prioritized(codeRenderer{}, 100))),
// default html renderer (1000) for fenced code blocks and headings.
goldmark.WithRendererOptions(renderer.WithNodeRenderers(
util.Prioritized(codeRenderer{}, 100),
util.Prioritized(headingRenderer{}, 100),
util.Prioritized(calloutRenderer{}, 100),
)),
)
}
@@ -65,49 +71,123 @@ func (fenceAnnotator) Transform(doc *ast.Document, reader text.Reader, pc parser
})
}
// codeRenderer renders every fenced code block inside <figure class="code">;
// a src= fence gets a <figcaption> naming its source, linked to source_url.
type codeRenderer struct{}
// headingRenderer renders headings with their slug ID and, on H2 and H3,
// a permalink after the text.
type headingRenderer struct{}
func (codeRenderer) RegisterFuncs(r renderer.NodeRendererFuncRegisterer) {
r.Register(ast.KindFencedCodeBlock, renderFence)
func (headingRenderer) RegisterFuncs(r renderer.NodeRendererFuncRegisterer) {
r.Register(ast.KindHeading, renderHeading)
}
func attrString(n ast.Node, name string) string {
v, ok := n.AttributeString(name)
if !ok {
return ""
}
b, _ := v.([]byte)
return string(b)
}
func renderFence(w util.BufWriter, src []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
if !entering {
func renderHeading(w util.BufWriter, src []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
n := node.(*ast.Heading)
tag := fmt.Sprintf("h%d", n.Level)
id := attrString(n, "id")
if entering {
_, _ = w.WriteString("<" + tag)
if id != "" {
_, _ = w.WriteString(` id="` + html.EscapeString(id) + `"`)
}
_ = w.WriteByte('>')
return ast.WalkContinue, nil
}
n := node.(*ast.FencedCodeBlock)
_, _ = w.WriteString(`<figure class="code">`)
if ref := attrString(n, "data-src"); ref != "" {
_, _ = w.WriteString("<figcaption>")
if href := attrString(n, "data-href"); href != "" {
_, _ = w.WriteString(`<a href="` + html.EscapeString(href) + `">` + html.EscapeString(ref) + "</a>")
} else {
_, _ = w.WriteString(html.EscapeString(ref))
if id != "" && (n.Level == 2 || n.Level == 3) {
_, _ = w.WriteString(`<a class="heading-anchor" href="#` + html.EscapeString(id) +
`" aria-label="Link to section: ` + html.EscapeString(plainText(n, src)) + `">#</a>`)
}
_, _ = w.WriteString("</" + tag + ">\n")
return ast.WalkContinue, nil
}
// kindCallout is the AST node kind of a > [!NOTE], [!TIP] or [!WARNING]
// blockquote.
var kindCallout = ast.NewNodeKind("Callout")
// calloutNode holds the blocks of a callout; CalloutType is NOTE, TIP or WARNING.
type calloutNode struct {
ast.BaseBlock
CalloutType string
}
func (n *calloutNode) Kind() ast.NodeKind { return kindCallout }
func (n *calloutNode) Dump(src []byte, level int) {
ast.DumpHelper(n, src, level, map[string]string{"CalloutType": n.CalloutType}, nil)
}
// calloutTransformer turns a blockquote whose first line is exactly
// [!NOTE], [!TIP] or [!WARNING] into a callout without that line. Other
// types stay blockquotes; checkPolicy reports them.
type calloutTransformer struct{}
func (calloutTransformer) Transform(doc *ast.Document, reader text.Reader, _ parser.Context) {
src := reader.Source()
var quotes []*ast.Blockquote
_ = ast.Walk(doc, func(n ast.Node, entering bool) (ast.WalkStatus, error) {
if bq, ok := n.(*ast.Blockquote); ok && entering {
quotes = append(quotes, bq)
}
_, _ = w.WriteString("</figcaption>")
return ast.WalkContinue, nil
})
for _, bq := range quotes {
para, ok := bq.FirstChild().(*ast.Paragraph)
if !ok || para.Lines().Len() == 0 {
continue
}
first := para.Lines().At(0)
m := calloutLine.FindStringSubmatch(strings.TrimSpace(string(first.Value(src))))
if m == nil || !slices.Contains(calloutTypes, m[1]) {
continue
}
for c := para.FirstChild(); c != nil; {
next := c.NextSibling()
t, ok := c.(*ast.Text)
if !ok || t.Segment.Start >= first.Stop {
break
}
para.RemoveChild(para, c)
c = next
}
if para.ChildCount() == 0 {
bq.RemoveChild(bq, para)
}
callout := &calloutNode{CalloutType: m[1]}
for c := bq.FirstChild(); c != nil; {
next := c.NextSibling()
callout.AppendChild(callout, c)
c = next
}
bq.Parent().ReplaceChild(bq.Parent(), bq, callout)
}
_, _ = w.WriteString("<pre><code")
if lang := n.Language(src); lang != nil {
_, _ = w.WriteString(` class="language-` + html.EscapeString(string(lang)) + `"`)
}
// calloutLine matches the marker line of a callout.
var calloutLine = regexp.MustCompile(`^\[!([A-Za-z]+)\]$`)
// calloutRenderer renders a callout as <aside class="callout callout-…">
// with its icon and title.
type calloutRenderer struct{}
func (calloutRenderer) RegisterFuncs(r renderer.NodeRendererFuncRegisterer) {
r.Register(kindCallout, renderCallout)
}
var calloutMeta = map[string]struct{ class, icon, title string }{
"NOTE": {"callout-note", "info", "Note"},
"TIP": {"callout-tip", "lightbulb", "Tip"},
"WARNING": {"callout-warning", "triangle-alert", "Warning"},
}
func renderCallout(w util.BufWriter, _ []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
meta := calloutMeta[node.(*calloutNode).CalloutType]
if !entering {
_, _ = w.WriteString("</div></aside>\n")
return ast.WalkContinue, nil
}
_ = w.WriteByte('>')
for i := 0; i < n.Lines().Len(); i++ {
line := n.Lines().At(i)
_, _ = w.WriteString(html.EscapeString(string(line.Value(src))))
}
_, _ = w.WriteString("</code></pre></figure>\n")
return ast.WalkSkipChildren, nil
_, _ = w.WriteString(`<aside class="callout ` + meta.class + `" role="note"><p class="callout-title">`)
writeIcon(w, meta.icon)
_, _ = w.WriteString("<span>" + meta.title + `</span></p><div class="callout-body">`)
return ast.WalkContinue, nil
}
// h1Stripper removes the page's leading "# Title" heading; the template