Skip to content

Instantly share code, notes, and snippets.

@artyom
Last active January 14, 2022 09:37
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save artyom/26c2674d459669a38eb8b84f95fa30fb to your computer and use it in GitHub Desktop.
Save artyom/26c2674d459669a38eb8b84f95fa30fb to your computer and use it in GitHub Desktop.
Custom header IDs that work with headers which include formatting
package main
import (
"fmt"
"log"
"os"
"strings"
"unicode"
"github.com/yuin/goldmark"
"github.com/yuin/goldmark/ast"
"github.com/yuin/goldmark/text"
)
func main() {
if err := run(); err != nil {
log.Fatal(err)
}
}
const src = `# Hello, [world][1]!
Some text.
## <b>Hello</b> world
Another text under seemingly duplicate header.
[1]: https://example.com/
`
func run() error {
body := []byte(src)
var seen map[string]struct{} // keeps track of seen slugs to avoid duplicate ids
fn := func(n ast.Node, entering bool) (ast.WalkStatus, error) {
if !entering || n.Kind() != ast.KindHeading {
return ast.WalkContinue, nil
}
if name := slugify(nodeText(n, body)); name != "" {
if seen == nil {
seen = make(map[string]struct{})
}
for i := 0; i < 100; i++ {
var cand string
if i == 0 {
cand = name
} else {
cand = fmt.Sprintf("%s-%d", name, i)
}
if _, ok := seen[cand]; !ok {
seen[cand] = struct{}{}
n.SetAttributeString("id", []byte(cand))
break
}
}
}
return ast.WalkContinue, nil
}
doc := goldmark.DefaultParser().Parse(text.NewReader(body))
if err := ast.Walk(doc, fn); err != nil {
return err
}
return goldmark.DefaultRenderer().Render(os.Stdout, body, doc)
}
// nodeText walks node and extracts plain text from it and its descendants,
// effectively removing all markdown syntax
func nodeText(node ast.Node, src []byte) string {
var b strings.Builder
fn := func(n ast.Node, entering bool) (ast.WalkStatus, error) {
if !entering {
return ast.WalkContinue, nil
}
switch n.Kind() {
case ast.KindText:
if t, ok := n.(*ast.Text); ok {
b.Write(t.Text(src))
}
}
return ast.WalkContinue, nil
}
if err := ast.Walk(node, fn); err != nil {
return ""
}
return b.String()
}
func slugify(text string) string {
f := func(r rune) rune {
switch {
case r == '-' || r == '_':
return r
case unicode.IsSpace(r):
return '-'
case unicode.IsLetter(r) || unicode.IsNumber(r):
return unicode.ToLower(r)
}
return -1
}
return strings.Map(f, text)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment