171 lines
3.3 KiB
Go
171 lines
3.3 KiB
Go
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)
|
|
}
|