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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user