GoBlog/markdown.go

231 lines
6.1 KiB
Go
Raw Normal View History

2020-07-28 19:38:12 +00:00
package main
import (
"bytes"
2020-10-18 10:10:00 +00:00
"strings"
"github.com/PuerkitoBio/goquery"
2020-12-31 16:15:05 +00:00
kemoji "github.com/kyokomi/emoji/v2"
2020-07-28 19:38:12 +00:00
"github.com/yuin/goldmark"
2020-10-18 10:10:00 +00:00
emoji "github.com/yuin/goldmark-emoji"
"github.com/yuin/goldmark-emoji/definition"
2020-09-21 14:53:20 +00:00
"github.com/yuin/goldmark/ast"
2020-07-28 19:38:12 +00:00
"github.com/yuin/goldmark/extension"
"github.com/yuin/goldmark/parser"
2020-09-21 14:53:20 +00:00
"github.com/yuin/goldmark/renderer"
2020-07-28 19:38:12 +00:00
"github.com/yuin/goldmark/renderer/html"
2021-05-20 18:18:13 +00:00
"github.com/yuin/goldmark/text"
2020-09-21 14:53:20 +00:00
"github.com/yuin/goldmark/util"
2020-07-28 19:38:12 +00:00
)
2020-09-18 11:11:25 +00:00
var emojilib definition.Emojis
var defaultMarkdown, absoluteMarkdown goldmark.Markdown
2020-07-28 19:38:12 +00:00
2020-08-24 19:09:30 +00:00
func initMarkdown() {
defaultGoldmarkOptions := []goldmark.Option{
2020-07-28 19:38:12 +00:00
goldmark.WithRendererOptions(
html.WithUnsafe(),
),
goldmark.WithParserOptions(
parser.WithAutoHeadingID(),
),
goldmark.WithExtensions(
2020-10-18 10:10:00 +00:00
extension.Table,
extension.Strikethrough,
2020-07-28 19:38:12 +00:00
extension.Footnote,
extension.Typographer,
2020-11-28 08:02:12 +00:00
extension.Linkify,
// Emojis
emoji.New(
2020-09-21 14:53:20 +00:00
emoji.WithEmojis(emojiGoLib()),
),
2020-07-28 19:38:12 +00:00
),
}
defaultMarkdown = goldmark.New(append(defaultGoldmarkOptions, goldmark.WithExtensions(&customExtension{absoluteLinks: false}))...)
absoluteMarkdown = goldmark.New(append(defaultGoldmarkOptions, goldmark.WithExtensions(&customExtension{absoluteLinks: true}))...)
2020-07-28 19:38:12 +00:00
}
func renderMarkdown(source string, absoluteLinks bool) (rendered []byte, err error) {
2020-07-28 19:38:12 +00:00
var buffer bytes.Buffer
if absoluteLinks {
err = absoluteMarkdown.Convert([]byte(source), &buffer)
} else {
err = defaultMarkdown.Convert([]byte(source), &buffer)
}
2020-12-14 21:05:54 +00:00
return buffer.Bytes(), err
2020-07-28 19:38:12 +00:00
}
2020-09-18 11:11:25 +00:00
func renderText(s string) string {
h, err := renderMarkdown(s, false)
if err != nil {
return ""
}
d, err := goquery.NewDocumentFromReader(bytes.NewReader(h))
if err != nil {
return ""
}
return d.Text()
}
2020-09-21 14:53:20 +00:00
// Extensions etc...
// All emojis from emoji lib
func emojiGoLib() definition.Emojis {
2020-12-14 21:05:54 +00:00
if emojilib == nil {
2020-09-18 11:11:25 +00:00
var emojis []definition.Emoji
for shotcode, e := range kemoji.CodeMap() {
emojis = append(emojis, definition.NewEmoji(e, []rune(e), strings.ReplaceAll(shotcode, ":", "")))
}
emojilib = definition.NewEmojis(emojis...)
2020-12-14 21:05:54 +00:00
}
2020-09-18 11:11:25 +00:00
return emojilib
}
2020-09-21 14:53:20 +00:00
// Links
type customExtension struct {
absoluteLinks bool
}
2020-09-21 14:53:20 +00:00
2020-09-21 15:05:50 +00:00
func (l *customExtension) Extend(m goldmark.Markdown) {
2021-05-20 18:18:13 +00:00
m.Parser().AddOptions(parser.WithInlineParsers(
util.Prioritized(&markdownMarkParser{}, 500),
))
2020-09-21 14:53:20 +00:00
m.Renderer().AddOptions(renderer.WithNodeRenderers(
util.Prioritized(&customRenderer{
absoluteLinks: l.absoluteLinks,
}, 500),
2020-09-21 14:53:20 +00:00
))
}
type customRenderer struct {
absoluteLinks bool
}
2020-09-21 14:53:20 +00:00
2020-09-21 15:05:50 +00:00
func (c *customRenderer) RegisterFuncs(r renderer.NodeRendererFuncRegisterer) {
r.Register(ast.KindLink, c.renderLink)
r.Register(ast.KindImage, c.renderImage)
2021-05-20 18:18:13 +00:00
r.Register(kindMarkdownMark, c.renderMarkTag)
2020-09-21 14:53:20 +00:00
}
2020-09-21 15:05:50 +00:00
func (c *customRenderer) renderLink(w util.BufWriter, _ []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
2020-09-21 14:53:20 +00:00
if entering {
2020-12-14 21:05:54 +00:00
n := node.(*ast.Link)
_, _ = w.WriteString("<a href=\"")
2020-09-21 14:53:20 +00:00
// Make URL absolute if it's relative
2020-12-14 21:05:54 +00:00
newDestination := util.URLEscape(n.Destination, true)
if c.absoluteLinks && bytes.HasPrefix(newDestination, []byte("/")) {
2020-12-14 21:05:54 +00:00
_, _ = w.Write(util.EscapeHTML([]byte(appConfig.Server.PublicAddress)))
2020-09-21 14:53:20 +00:00
}
2020-12-14 21:05:54 +00:00
_, _ = w.Write(util.EscapeHTML(newDestination))
_, _ = w.WriteRune('"')
2020-09-21 14:53:20 +00:00
// Open external links (links that start with "http") in new tab
2020-12-14 21:05:54 +00:00
if bytes.HasPrefix(n.Destination, []byte("http")) {
2020-09-21 14:53:20 +00:00
_, _ = w.WriteString(` target="_blank" rel="noopener"`)
}
// Title
if n.Title != nil {
2020-12-14 21:05:54 +00:00
_, _ = w.WriteString(" title=\"")
2020-09-21 14:53:20 +00:00
_, _ = w.Write(n.Title)
2020-12-14 21:05:54 +00:00
_, _ = w.WriteRune('"')
2020-09-21 14:53:20 +00:00
}
2020-12-14 21:05:54 +00:00
_, _ = w.WriteRune('>')
2020-09-21 14:53:20 +00:00
} else {
_, _ = w.WriteString("</a>")
}
return ast.WalkContinue, nil
}
2020-09-21 15:05:50 +00:00
func (c *customRenderer) renderImage(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
if !entering {
return ast.WalkContinue, nil
}
n := node.(*ast.Image)
// Make URL absolute if it's relative
2020-12-14 21:05:54 +00:00
destination := util.URLEscape(n.Destination, true)
if bytes.HasPrefix(destination, []byte("/")) {
destination = util.EscapeHTML(append([]byte(appConfig.Server.PublicAddress), destination...))
} else {
destination = util.EscapeHTML(destination)
2020-09-21 15:05:50 +00:00
}
_, _ = w.WriteString("<a href=\"")
2020-12-14 21:05:54 +00:00
_, _ = w.Write(destination)
2020-09-21 15:05:50 +00:00
_, _ = w.WriteString("\">")
_, _ = w.WriteString("<img src=\"")
2020-12-14 21:05:54 +00:00
_, _ = w.Write(destination)
_, _ = w.WriteString("\" alt=\"")
2020-09-21 15:05:50 +00:00
_, _ = w.Write(util.EscapeHTML(n.Text(source)))
_ = w.WriteByte('"')
_, _ = w.WriteString(" loading=\"lazy\"")
if n.Title != nil {
2020-12-14 21:05:54 +00:00
_, _ = w.WriteString(" title=\"")
2020-09-21 15:05:50 +00:00
_, _ = w.Write(n.Title)
_ = w.WriteByte('"')
}
_, _ = w.WriteString("></a>")
return ast.WalkSkipChildren, nil
}
2021-05-20 18:18:13 +00:00
type markdownMark struct {
ast.BaseInline
}
func (n *markdownMark) Kind() ast.NodeKind {
return kindMarkdownMark
}
func (n *markdownMark) Dump(source []byte, level int) {
ast.DumpHelper(n, source, level, nil, nil)
}
type markDelimiterProcessor struct {
}
func (p *markDelimiterProcessor) IsDelimiter(b byte) bool {
return b == '='
}
func (p *markDelimiterProcessor) CanOpenCloser(opener, closer *parser.Delimiter) bool {
return opener.Char == closer.Char
}
func (p *markDelimiterProcessor) OnMatch(consumes int) ast.Node {
return &markdownMark{}
}
var defaultMarkDelimiterProcessor = &markDelimiterProcessor{}
type markdownMarkParser struct {
}
func (s *markdownMarkParser) Trigger() []byte {
return []byte{'='}
}
func (s *markdownMarkParser) Parse(parent ast.Node, block text.Reader, pc parser.Context) ast.Node {
before := block.PrecendingCharacter()
line, segment := block.PeekLine()
node := parser.ScanDelimiter(line, before, 2, defaultMarkDelimiterProcessor)
if node == nil {
return nil
}
node.Segment = segment.WithStop(segment.Start + node.OriginalLength)
block.Advance(node.OriginalLength)
pc.PushDelimiter(node)
return node
}
func (s *markdownMarkParser) CloseBlock(parent ast.Node, pc parser.Context) {
}
var kindMarkdownMark = ast.NewNodeKind("Mark")
func (c *customRenderer) renderMarkTag(w util.BufWriter, source []byte, n ast.Node, entering bool) (ast.WalkStatus, error) {
if entering {
_, _ = w.WriteString("<mark>")
} else {
_, _ = w.WriteString("</mark>")
}
return ast.WalkContinue, nil
}