124 lines
2.8 KiB
Go
124 lines
2.8 KiB
Go
package converter
|
|
|
|
import (
|
|
"strings"
|
|
|
|
"github.com/yuin/goldmark"
|
|
emojiast "github.com/yuin/goldmark-emoji/ast"
|
|
"github.com/yuin/goldmark/ast"
|
|
"github.com/yuin/goldmark/parser"
|
|
"github.com/yuin/goldmark/text"
|
|
"github.com/yuin/goldmark/util"
|
|
)
|
|
|
|
type anchorExtension struct{}
|
|
|
|
func (e *anchorExtension) Extend(m goldmark.Markdown) {
|
|
m.Parser().AddOptions(parser.WithASTTransformers(
|
|
util.Prioritized(&anchorTransformer{}, 900),
|
|
))
|
|
}
|
|
|
|
type anchorTransformer struct{}
|
|
|
|
func (t *anchorTransformer) Transform(doc *ast.Document, reader text.Reader, pc parser.Context) {
|
|
src := reader.Source()
|
|
used := map[string]int{}
|
|
|
|
_ = pc
|
|
|
|
_ = 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
|
|
}
|
|
|
|
slug := translitSlug(extractHeadingText(h, src), used)
|
|
h.SetAttributeString("id", []byte(slug))
|
|
|
|
link := ast.NewLink()
|
|
link.Destination = []byte("#" + slug)
|
|
link.SetAttributeString("class", []byte("heading-anchor"))
|
|
link.SetAttributeString("aria-hidden", []byte("true"))
|
|
link.AppendChild(link, ast.NewString([]byte("#")))
|
|
|
|
if first := h.FirstChild(); first != nil {
|
|
h.InsertBefore(h, first, link)
|
|
} else {
|
|
h.AppendChild(h, link)
|
|
}
|
|
|
|
return ast.WalkSkipChildren, nil
|
|
})
|
|
}
|
|
|
|
func extractHeadingText(h *ast.Heading, src []byte) string {
|
|
var b strings.Builder
|
|
|
|
_ = ast.Walk(h, func(n ast.Node, entering bool) (ast.WalkStatus, error) {
|
|
if !entering {
|
|
return ast.WalkContinue, nil
|
|
}
|
|
|
|
switch v := n.(type) {
|
|
case *ast.Link:
|
|
if isHeadingAnchor(v) {
|
|
return ast.WalkSkipChildren, nil
|
|
}
|
|
case *ast.Text:
|
|
b.Write(v.Segment.Value(src))
|
|
if v.HardLineBreak() || v.SoftLineBreak() {
|
|
b.WriteByte(' ')
|
|
}
|
|
case *ast.String:
|
|
b.Write(v.Value)
|
|
case *ast.CodeSpan:
|
|
for child := v.FirstChild(); child != nil; child = child.NextSibling() {
|
|
switch c := child.(type) {
|
|
case *ast.Text:
|
|
b.Write(c.Segment.Value(src))
|
|
case *ast.String:
|
|
b.Write(c.Value)
|
|
}
|
|
}
|
|
return ast.WalkSkipChildren, nil
|
|
case *ast.AutoLink:
|
|
b.Write(v.Label(src))
|
|
return ast.WalkSkipChildren, nil
|
|
case *emojiast.Emoji:
|
|
if v.Value != nil && len(v.Value.Unicode) > 0 {
|
|
b.WriteString(string(v.Value.Unicode))
|
|
} else if len(v.ShortName) > 0 {
|
|
b.WriteByte(':')
|
|
b.Write(v.ShortName)
|
|
b.WriteByte(':')
|
|
}
|
|
return ast.WalkSkipChildren, nil
|
|
}
|
|
|
|
return ast.WalkContinue, nil
|
|
})
|
|
|
|
return strings.TrimSpace(b.String())
|
|
}
|
|
|
|
func isHeadingAnchor(link *ast.Link) bool {
|
|
attr, ok := link.AttributeString("class")
|
|
if !ok {
|
|
return false
|
|
}
|
|
|
|
switch value := attr.(type) {
|
|
case []byte:
|
|
return string(value) == "heading-anchor"
|
|
case string:
|
|
return value == "heading-anchor"
|
|
default:
|
|
return false
|
|
}
|
|
}
|