Last active
January 14, 2022 09:37
-
-
Save artyom/26c2674d459669a38eb8b84f95fa30fb to your computer and use it in GitHub Desktop.
Custom header IDs that work with headers which include formatting
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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