slug.go
package main
import (
"strconv"
"strings"
"github.com/yuin/goldmark/ast"
"github.com/yuin/goldmark/parser"
"github.com/yuin/goldmark/text"
)
// slugify turns heading text into a URL fragment:
// lowercase, alphanumerics kept, every other run of characters becomes one dash.
func slugify(s string) string {
var b strings.Builder
dash := false
for _, r := range strings.ToLower(s) {
switch {
case r >= 'a' && r <= 'z' || r >= '0' && r <= '9':
if dash && b.Len() > 0 {
b.WriteByte('-')
}
dash = false
b.WriteRune(r)
default:
dash = true
}
}
return b.String()
}
// slugIDs implements parser.IDs so goldmark assigns our slugs as heading ids.
// Duplicate headings get "-2", "-3", ... suffixes.
type slugIDs struct {
used map[string]bool
}
func newSlugIDs() *slugIDs {
return &slugIDs{used: make(map[string]bool)}
}
func (s *slugIDs) Generate(value []byte, kind ast.NodeKind) []byte {
slug := slugify(string(value))
if slug == "" {
slug = "section"
}
candidate := slug
for n := 2; s.used[candidate]; n++ {
candidate = slug + "-" + strconv.Itoa(n)
}
s.used[candidate] = true
return []byte(candidate)
}
func (s *slugIDs) Put(value []byte) {
s.used[string(value)] = true
}
// headingAnchors is a goldmark AST transformer that appends a chainlink
// anchor to every heading that received an id, linking to its own fragment.
// Pure HTML output: works without javascript.
type headingAnchors struct{}
func (headingAnchors) Transform(doc *ast.Document, reader text.Reader, pc parser.Context) {
_ = ast.Walk(doc, func(n ast.Node, entering bool) (ast.WalkStatus, error) {
if !entering {
return ast.WalkContinue, nil
}
h, ok := n.(*ast.Heading)
if !ok {
return ast.WalkContinue, nil
}
id, ok := h.AttributeString("id")
if !ok {
return ast.WalkContinue, nil
}
link := ast.NewLink()
link.Destination = append([]byte("#"), id.([]byte)...)
link.SetAttributeString("class", []byte("anchor"))
link.AppendChild(link, ast.NewString([]byte("\U0001F517"))) // chainlink emoji
h.AppendChild(h, link)
return ast.WalkContinue, nil
})
}
slug.go · 2.1 KiB · 6c942a8c45