feat(11.1-01): verify src= code blocks and add summer docs:sync

- src= fences name a file, a Go declaration or Example body, or a docs:start region
- confinement: relative clean paths inside the root, no dotfiles or .env,
  no nested go.mod modules, Examples need // Output:, test regions must run
- a drifted or missing snippet is a problem, so docs:build writes nothing
- docs:sync rewrites drifted fence bodies in place
- fences render in figure.code with a source caption; .md fences keep only the language
- bonfire ExampleCall is the first verified example, shown in setup/installation
This commit is contained in:
Jakub Zych
2026-09-30 21:26:52 +02:00
parent e433dcf0c9
commit dc6a03c714
10 changed files with 987 additions and 2 deletions

View File

@@ -4,6 +4,7 @@ import (
"bytes"
"cmp"
"fmt"
"html"
"path"
"slices"
"strings"
@@ -14,6 +15,7 @@ import (
"github.com/yuin/goldmark/ast"
"github.com/yuin/goldmark/extension"
"github.com/yuin/goldmark/parser"
"github.com/yuin/goldmark/renderer"
"github.com/yuin/goldmark/text"
"github.com/yuin/goldmark/util"
)
@@ -30,11 +32,84 @@ func newMarkdown() goldmark.Markdown {
parser.WithASTTransformers(
util.Prioritized(h1Stripper{}, 100),
util.Prioritized(linkRewriter{}, 200),
util.Prioritized(fenceAnnotator{}, 300),
),
),
// 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))),
)
}
// fenceAnnotator marks fences that carry src= with the reference and its
// source_url link, for codeRenderer's caption.
type fenceAnnotator struct{}
func (fenceAnnotator) Transform(doc *ast.Document, reader text.Reader, pc parser.Context) {
pctx, _ := pc.Get(pageKey).(*pageContext)
src := reader.Source()
_ = ast.Walk(doc, func(n ast.Node, entering bool) (ast.WalkStatus, error) {
fc, ok := n.(*ast.FencedCodeBlock)
if !entering || !ok || fc.Info == nil {
return ast.WalkContinue, nil
}
ref, ok := ParseSrc(string(fc.Info.Segment.Value(src)))
if !ok {
return ast.WalkContinue, nil
}
fc.SetAttributeString("data-src", []byte(ref.String()))
if pctx != nil && pctx.site.cfg.SourceURL != "" {
fc.SetAttributeString("data-href", []byte(strings.ReplaceAll(pctx.site.cfg.SourceURL, "{path}", ref.Path)))
}
return ast.WalkSkipChildren, nil
})
}
// 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{}
func (codeRenderer) RegisterFuncs(r renderer.NodeRendererFuncRegisterer) {
r.Register(ast.KindFencedCodeBlock, renderFence)
}
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 {
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))
}
_, _ = w.WriteString("</figcaption>")
}
_, _ = w.WriteString("<pre><code")
if lang := n.Language(src); lang != nil {
_, _ = w.WriteString(` class="language-` + html.EscapeString(string(lang)) + `"`)
}
_ = 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
}
// h1Stripper removes the page's leading "# Title" heading; the template
// renders the title once.
type h1Stripper struct{}
@@ -327,7 +402,8 @@ func closesFence(line string, f fence) bool {
}
// rewriteMarkdown applies the raw-output transforms to a Markdown body:
// link destinations outside code fences are replaced through links.
// fence info strings are reduced to their language word (dropping src=),
// and link destinations outside code fences are replaced through links.
func rewriteMarkdown(body string, links map[string]string) string {
lines := strings.Split(body, "\n")
inFence := make([]bool, len(lines))
@@ -345,6 +421,11 @@ func rewriteMarkdown(body string, links map[string]string) string {
origs = append(origs, o)
}
slices.SortFunc(origs, func(a, b string) int { return cmp.Or(cmp.Compare(len(b), len(a)), strings.Compare(a, b)) })
for _, f := range scanFences(lines) {
if lang, _, _ := strings.Cut(f.info, " "); lang != f.info {
lines[f.open] = strings.Repeat(" ", f.indent) + strings.Repeat(string(f.char), f.count) + lang
}
}
for i, line := range lines {
if inFence[i] || !strings.Contains(line, "](") {
continue