phase2: markdown converter with goldmark, chroma, and ASCII-translit anchors
This commit is contained in:
@@ -0,0 +1,170 @@
|
||||
package converter
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"io/fs"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
chromahtml "github.com/alecthomas/chroma/v2/formatters/html"
|
||||
"github.com/yuin/goldmark"
|
||||
emoji "github.com/yuin/goldmark-emoji"
|
||||
highlighting "github.com/yuin/goldmark-highlighting/v2"
|
||||
"github.com/yuin/goldmark/ast"
|
||||
"github.com/yuin/goldmark/extension"
|
||||
"github.com/yuin/goldmark/renderer"
|
||||
"github.com/yuin/goldmark/text"
|
||||
"github.com/yuin/goldmark/util"
|
||||
)
|
||||
|
||||
const documentLang = "ru"
|
||||
|
||||
type Result struct {
|
||||
HTML []byte
|
||||
Title string
|
||||
}
|
||||
|
||||
type Converter struct {
|
||||
md goldmark.Markdown
|
||||
tmpl *template.Template
|
||||
bufferPool sync.Pool
|
||||
}
|
||||
|
||||
type templateData struct {
|
||||
Lang string
|
||||
Title string
|
||||
Body template.HTML
|
||||
ShowTitle bool
|
||||
}
|
||||
|
||||
func New(templateFS fs.FS) (*Converter, error) {
|
||||
tmpl, err := template.ParseFS(templateFS, "document.html")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Converter{
|
||||
md: goldmark.New(
|
||||
goldmark.WithExtensions(
|
||||
extension.GFM,
|
||||
extension.Footnote,
|
||||
emoji.Emoji,
|
||||
highlighting.NewHighlighting(
|
||||
highlighting.WithStyle("github"),
|
||||
highlighting.WithFormatOptions(chromahtml.WithClasses(false)),
|
||||
),
|
||||
&anchorExtension{},
|
||||
),
|
||||
goldmark.WithRendererOptions(
|
||||
renderer.WithNodeRenderers(
|
||||
util.Prioritized(&escapedRawHTMLRenderer{}, 999),
|
||||
),
|
||||
),
|
||||
),
|
||||
tmpl: tmpl,
|
||||
bufferPool: sync.Pool{
|
||||
New: func() any {
|
||||
return new(bytes.Buffer)
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *Converter) Convert(md []byte, fallbackTitle string) (Result, error) {
|
||||
body, title, hasH1, err := c.render(md)
|
||||
if err != nil {
|
||||
return Result{}, err
|
||||
}
|
||||
|
||||
if title == "" {
|
||||
title = fallbackTitle
|
||||
}
|
||||
|
||||
buf := c.getBuffer()
|
||||
defer c.putBuffer(buf)
|
||||
|
||||
data := templateData{
|
||||
Lang: documentLang,
|
||||
Title: title,
|
||||
Body: template.HTML(body),
|
||||
ShowTitle: !hasH1 && title != "",
|
||||
}
|
||||
|
||||
if err := c.tmpl.Execute(buf, data); err != nil {
|
||||
return Result{}, err
|
||||
}
|
||||
|
||||
return Result{
|
||||
HTML: append([]byte(nil), buf.Bytes()...),
|
||||
Title: title,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *Converter) RenderBody(md []byte) ([]byte, string, error) {
|
||||
body, title, _, err := c.render(md)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
return body, title, nil
|
||||
}
|
||||
|
||||
func (c *Converter) render(md []byte) ([]byte, string, bool, error) {
|
||||
root := c.md.Parser().Parse(text.NewReader(md))
|
||||
doc, ok := root.(*ast.Document)
|
||||
if !ok {
|
||||
return nil, "", false, fmt.Errorf("expected *ast.Document, got %T", root)
|
||||
}
|
||||
title, hasH1 := extractDocumentTitle(doc, md)
|
||||
|
||||
buf := c.getBuffer()
|
||||
defer c.putBuffer(buf)
|
||||
|
||||
if err := c.md.Renderer().Render(buf, md, doc); err != nil {
|
||||
return nil, "", false, err
|
||||
}
|
||||
|
||||
return append([]byte(nil), buf.Bytes()...), title, hasH1, nil
|
||||
}
|
||||
|
||||
func extractDocumentTitle(doc *ast.Document, src []byte) (string, bool) {
|
||||
var (
|
||||
title string
|
||||
hasH1 bool
|
||||
)
|
||||
|
||||
_ = 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
|
||||
}
|
||||
|
||||
if h.Level == 1 {
|
||||
hasH1 = true
|
||||
}
|
||||
|
||||
if title == "" {
|
||||
title = strings.TrimSpace(extractHeadingText(h, src))
|
||||
}
|
||||
|
||||
return ast.WalkContinue, nil
|
||||
})
|
||||
|
||||
return title, hasH1
|
||||
}
|
||||
|
||||
func (c *Converter) getBuffer() *bytes.Buffer {
|
||||
buf := c.bufferPool.Get().(*bytes.Buffer)
|
||||
buf.Reset()
|
||||
return buf
|
||||
}
|
||||
|
||||
func (c *Converter) putBuffer(buf *bytes.Buffer) {
|
||||
buf.Reset()
|
||||
c.bufferPool.Put(buf)
|
||||
}
|
||||
Reference in New Issue
Block a user