, but the embed renders a
// is auto-closed by the browser —
// which strands empty elements (they still carry .content paragraph margins)
// and breaks the float layout once several embeds share a page. Dissolving the
// wrapping paragraph makes each embed render as a clean block sibling with no
// stray .
type embedBlockTransformer struct{}
func (t embedBlockTransformer) Transform(doc *ast.Document, reader text.Reader, pc parser.Context) {
source := reader.Source()
var paras []*ast.Paragraph
ast.Walk(doc, func(n ast.Node, entering bool) (ast.WalkStatus, error) {
if entering {
if p, ok := n.(*ast.Paragraph); ok && t.paragraphFiguresOnly(p, source) {
paras = append(paras, p)
}
}
return ast.WalkContinue, nil
})
for _, p := range paras {
parent := p.Parent()
if parent == nil {
continue
}
// InsertBefore isolates the embed from p first, so hoist each embed to a
// block sibling ahead of the (soon removed) paragraph, then drop p.
for c := p.FirstChild(); c != nil; {
next := c.NextSibling()
if _, ok := c.(*ast.Image); ok {
parent.InsertBefore(parent, p, c)
}
c = next
}
parent.RemoveChild(parent, p)
}
}
// paragraphFiguresOnly reports whether p holds at least one figure-rendering
// image and nothing else visible — only such images and whitespace/line-break
// text. Only these are safe to dissolve: a paragraph carrying prose, or an
// image that degrades to an inline link (non-image destination), stays wrapped
// so the fallback anchor keeps its paragraph.
func (t embedBlockTransformer) paragraphFiguresOnly(p *ast.Paragraph, source []byte) bool {
hasFigure := false
for c := p.FirstChild(); c != nil; c = c.NextSibling() {
switch n := c.(type) {
case *ast.Image:
if !embedIsImage(n.Destination) {
return false
}
hasFigure = true
case *ast.Text:
if len(bytes.TrimSpace(n.Segment.Value(source))) != 0 {
return false
}
default:
return false
}
}
return hasFigure
}
type embedExt struct{}
// newEmbedExt returns a goldmark extension that renders Markdown images as
// wiki embeds. It adds no parser of its own: goldmark parses the images, and
// the renderer registered at 500 wins over the default HTML renderer
// (registered at 1000 — lowest priority number is applied last and wins).
func newEmbedExt() goldmark.Extender { return &embedExt{} }
func (e *embedExt) Extend(m goldmark.Markdown) {
m.Parser().AddOptions(parser.WithASTTransformers(
util.Prioritized(embedBlockTransformer{}, 100),
))
m.Renderer().AddOptions(renderer.WithNodeRenderers(
util.Prioritized(&embedRenderer{}, 500),
))
}