this repo has no description
1// heavily inspired by: https://github.com/kaleocheng/goldmark-extensions
2
3package extension
4
5import (
6 "regexp"
7
8 "github.com/yuin/goldmark"
9 "github.com/yuin/goldmark/ast"
10 "github.com/yuin/goldmark/parser"
11 "github.com/yuin/goldmark/renderer"
12 "github.com/yuin/goldmark/renderer/html"
13 "github.com/yuin/goldmark/text"
14 "github.com/yuin/goldmark/util"
15)
16
17// An AtNode struct represents an AtNode
18type AtNode struct {
19 Handle string
20 ast.BaseInline
21}
22
23var _ ast.Node = &AtNode{}
24
25// Dump implements Node.Dump.
26func (n *AtNode) Dump(source []byte, level int) {
27 ast.DumpHelper(n, source, level, nil, nil)
28}
29
30// KindAt is a NodeKind of the At node.
31var KindAt = ast.NewNodeKind("At")
32
33// Kind implements Node.Kind.
34func (n *AtNode) Kind() ast.NodeKind {
35 return KindAt
36}
37
38var atRegexp = regexp.MustCompile(`(^|\s|\()(@)([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\b)`)
39var markdownLinkRegexp = regexp.MustCompile(`(?ms)\[.*\]\(.*\)`)
40
41type atParser struct{}
42
43// NewAtParser return a new InlineParser that parses
44// at expressions.
45func NewAtParser() parser.InlineParser {
46 return &atParser{}
47}
48
49func (s *atParser) Trigger() []byte {
50 return []byte{'@'}
51}
52
53func (s *atParser) Parse(parent ast.Node, block text.Reader, pc parser.Context) ast.Node {
54 line, segment := block.PeekLine()
55 m := atRegexp.FindSubmatchIndex(line)
56 if m == nil {
57 return nil
58 }
59
60 // Check for all links in the markdown to see if the handle found is inside one
61 linksIndexes := markdownLinkRegexp.FindAllIndex(block.Source(), -1)
62 for _, linkMatch := range linksIndexes {
63 if linkMatch[0] < segment.Start && segment.Start < linkMatch[1] {
64 return nil
65 }
66 }
67
68 atSegment := text.NewSegment(segment.Start, segment.Start+m[1])
69 block.Advance(m[1])
70 node := &AtNode{}
71 node.AppendChild(node, ast.NewTextSegment(atSegment))
72 node.Handle = string(atSegment.Value(block.Source())[1:])
73 return node
74}
75
76// atHtmlRenderer is a renderer.NodeRenderer implementation that
77// renders At nodes.
78type atHtmlRenderer struct {
79 html.Config
80}
81
82// NewAtHTMLRenderer returns a new AtHTMLRenderer.
83func NewAtHTMLRenderer(opts ...html.Option) renderer.NodeRenderer {
84 r := &atHtmlRenderer{
85 Config: html.NewConfig(),
86 }
87 for _, opt := range opts {
88 opt.SetHTMLOption(&r.Config)
89 }
90 return r
91}
92
93// RegisterFuncs implements renderer.NodeRenderer.RegisterFuncs.
94func (r *atHtmlRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) {
95 reg.Register(KindAt, r.renderAt)
96}
97
98func (r *atHtmlRenderer) renderAt(w util.BufWriter, source []byte, n ast.Node, entering bool) (ast.WalkStatus, error) {
99 if entering {
100 w.WriteString(`<a href="/`)
101 w.WriteString(n.(*AtNode).Handle)
102 w.WriteString(`" class="mention">`)
103 } else {
104 w.WriteString("</a>")
105 }
106 return ast.WalkContinue, nil
107}
108
109type atExt struct{}
110
111// At is an extension that allow you to use at expression like '@user.bsky.social' .
112var AtExt = &atExt{}
113
114func (e *atExt) Extend(m goldmark.Markdown) {
115 m.Parser().AddOptions(parser.WithInlineParsers(
116 util.Prioritized(NewAtParser(), 500),
117 ))
118 m.Renderer().AddOptions(renderer.WithNodeRenderers(
119 util.Prioritized(NewAtHTMLRenderer(), 500),
120 ))
121}