Here's a moderately complete solution:
package main
import (
"fmt"
"io"
"regexp"
"strings"
"github.com/gomarkdown/markdown"
"github.com/gomarkdown/markdown/ast"
"github.com/gomarkdown/markdown/html"
)
// levels tracks how deep we are in a heading "structure"
var levels []int
func hasLevels() bool {
return len(levels) > 0
}
func lastLevel() int {
if hasLevels() {
return levels[len(levels)-1]
}
return 0
}
func popLevel() int {
level := lastLevel()
levels = levels[:len(levels)-1]
return level
}
func pushLevel(x int) {
levels = append(levels, x)
}
var reID = regexp.MustCompile(`\s+`)
// renderSections catches an ast.Heading node, and wraps the node
// and its "children" nodes in <section>...</section> tags; there's no
// real hierarchy in Markdown, so we make one up by saying things like:
// - H2 is a child of H1, and so forth from 1 → 2 → 3 ... → N
// - an H1 is a sibling of another H1
func renderSections(w io.Writer, node ast.Node, entering bool) (ast.WalkStatus, bool) {
openSection := func(level int, id string) {
w.Write([]byte(fmt.Sprintf("<section id=\"%s\">\n", id)))
pushLevel(level)
}
closeSection := func() {
w.Write([]byte("</section>\n"))
popLevel()
}
if _, ok := node.(*ast.Heading); ok {
level := node.(*ast.Heading).Level
if entering {
// close heading-sections deeper than this level; we've "come up" some number of levels
for lastLevel() > level {
closeSection()
}
txtNode := node.GetChildren()[0]
if _, ok := txtNode.(*ast.Text); !ok {
panic(fmt.Errorf("expected txtNode to be *ast.Text; got %T", txtNode))
}
headTxt := string(txtNode.AsLeaf().Literal)
id := strings.ToLower(reID.ReplaceAllString(headTxt, "-"))
openSection(level, id)
}
}
// at end of document
if _, ok := node.(*ast.Document); ok {
if !entering {
for hasLevels() {
closeSection()
}
}
}
// continue as normal
return ast.GoToNext, false
}
func main() {
lines := []string{
"## Hello",
"### This is a test message",
"Ligisnfmkdfn",
}
md := strings.Join(lines, "\n")
opts := html.RendererOptions{
Flags: html.CommonFlags,
RenderNodeHook: renderSections,
}
renderer := html.NewRenderer(opts)
html := markdown.ToHTML([]byte(md), nil, renderer)
fmt.Println(string(html))
}
When I run that, I get:
<section id="hello">
<h2>Hello</h2>
<section id="this-is-a-test-message">
<h3>This is a test message</h3>
<p>Ligisnfmkdfn</p>
</section>
</section>
I say it's moderately complete because it's smart enough to deal with input like this:
lines := []string{
"# H1α",
"## H2A",
"## H2B",
"## H2C",
"### H31",
"#### H4I",
"## H2D",
"# H1β",
"## H2E",
}
and it produces:
<section id="h1α">
<h1>H1α</h1>
<section id="h2a">
<h2>H2A</h2>
</section>
<section id="h2b">
<h2>H2B</h2>
</section>
<section id="h2c">
<h2>H2C</h2>
<section id="h31">
<h3>H31</h3>
<section id="h4i">
<h4>H4I</h4>
</section>
</section>
</section>
<section id="h2d">
<h2>H2D</h2>
</section>
</section>
<section id="h1β">
<h1>H1β</h1>
<section id="h2e">
<h2>H2E</h2>
</section>
</section>
But I haven't rigorously tested this, so I'm not sure where it might not meet expectations.