phase2: markdown converter with goldmark, chroma, and ASCII-translit anchors
This commit is contained in:
@@ -0,0 +1,123 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user