+7
-1
appview/pages/markup/markdown.go
+7
-1
appview/pages/markup/markdown.go
···
45
45
Sanitizer Sanitizer
46
46
}
47
47
48
-
func (rctx *RenderContext) RenderMarkdown(source string) string {
48
+
func NewMarkdown() goldmark.Markdown {
49
49
md := goldmark.New(
50
50
goldmark.WithExtensions(
51
51
extension.GFM,
···
59
59
extension.NewFootnote(
60
60
extension.WithFootnoteIDPrefix([]byte("footnote")),
61
61
),
62
+
AtExt,
62
63
),
63
64
goldmark.WithParserOptions(
64
65
parser.WithAutoHeadingID(),
65
66
),
66
67
goldmark.WithRendererOptions(html.WithUnsafe()),
67
68
)
69
+
return md
70
+
}
71
+
72
+
func (rctx *RenderContext) RenderMarkdown(source string) string {
73
+
md := NewMarkdown()
68
74
69
75
if rctx != nil {
70
76
var transformers []util.PrioritizedValue
+134
appview/pages/markup/markdown_at_extension.go
+134
appview/pages/markup/markdown_at_extension.go
···
1
+
// heavily inspired by: https://github.com/kaleocheng/goldmark-extensions
2
+
3
+
package markup
4
+
5
+
import (
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
18
+
type AtNode struct {
19
+
handle string
20
+
ast.BaseInline
21
+
}
22
+
23
+
var _ ast.Node = &AtNode{}
24
+
25
+
// Dump implements Node.Dump.
26
+
func (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.
31
+
var KindAt = ast.NewNodeKind("At")
32
+
33
+
// Kind implements Node.Kind.
34
+
func (n *AtNode) Kind() ast.NodeKind {
35
+
return KindAt
36
+
}
37
+
38
+
var atRegexp = regexp.MustCompile(`(^|\s|\()(@)([a-zA-Z0-9.-]+)(\b)`)
39
+
40
+
type atParser struct{}
41
+
42
+
// NewAtParser return a new InlineParser that parses
43
+
// at expressions.
44
+
func NewAtParser() parser.InlineParser {
45
+
return &atParser{}
46
+
}
47
+
48
+
func (s *atParser) Trigger() []byte {
49
+
return []byte{'@'}
50
+
}
51
+
52
+
func (s *atParser) Parse(parent ast.Node, block text.Reader, pc parser.Context) ast.Node {
53
+
line, segment := block.PeekLine()
54
+
m := atRegexp.FindSubmatchIndex(line)
55
+
if m == nil {
56
+
return nil
57
+
}
58
+
block.Advance(m[1])
59
+
node := &AtNode{}
60
+
node.AppendChild(node, ast.NewTextSegment(text.NewSegment(segment.Start, segment.Start+m[1])))
61
+
node.handle = string(node.Text(block.Source())[1:])
62
+
return node
63
+
}
64
+
65
+
// atHtmlRenderer is a renderer.NodeRenderer implementation that
66
+
// renders At nodes.
67
+
type atHtmlRenderer struct {
68
+
html.Config
69
+
}
70
+
71
+
// NewAtHTMLRenderer returns a new AtHTMLRenderer.
72
+
func NewAtHTMLRenderer(opts ...html.Option) renderer.NodeRenderer {
73
+
r := &atHtmlRenderer{
74
+
Config: html.NewConfig(),
75
+
}
76
+
for _, opt := range opts {
77
+
opt.SetHTMLOption(&r.Config)
78
+
}
79
+
return r
80
+
}
81
+
82
+
// RegisterFuncs implements renderer.NodeRenderer.RegisterFuncs.
83
+
func (r *atHtmlRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) {
84
+
reg.Register(KindAt, r.renderAt)
85
+
}
86
+
87
+
func (r *atHtmlRenderer) renderAt(w util.BufWriter, source []byte, n ast.Node, entering bool) (ast.WalkStatus, error) {
88
+
if entering {
89
+
w.WriteString(`<a href="/@`)
90
+
w.WriteString(n.(*AtNode).handle)
91
+
w.WriteString(`" class="text-red-500">`)
92
+
} else {
93
+
w.WriteString("</a>")
94
+
}
95
+
return ast.WalkContinue, nil
96
+
}
97
+
98
+
type atExt struct{}
99
+
100
+
// At is an extension that allow you to use at expression like '@user.bsky.social' .
101
+
var AtExt = &atExt{}
102
+
103
+
func (e *atExt) Extend(m goldmark.Markdown) {
104
+
m.Parser().AddOptions(parser.WithInlineParsers(
105
+
util.Prioritized(NewAtParser(), 500),
106
+
))
107
+
m.Renderer().AddOptions(renderer.WithNodeRenderers(
108
+
util.Prioritized(NewAtHTMLRenderer(), 500),
109
+
))
110
+
}
111
+
112
+
// FindUserMentions returns Set of user handles from given markup soruce.
113
+
// It doesn't guarntee unique DIDs
114
+
func FindUserMentions(source string) []string {
115
+
var (
116
+
mentions []string
117
+
mentionsSet = make(map[string]struct{})
118
+
md = NewMarkdown()
119
+
sourceBytes = []byte(source)
120
+
root = md.Parser().Parse(text.NewReader(sourceBytes))
121
+
)
122
+
ast.Walk(root, func(n ast.Node, entering bool) (ast.WalkStatus, error) {
123
+
if entering && n.Kind() == KindAt {
124
+
handle := n.(*AtNode).handle
125
+
mentionsSet[handle] = struct{}{}
126
+
return ast.WalkSkipChildren, nil
127
+
}
128
+
return ast.WalkContinue, nil
129
+
})
130
+
for handle := range mentionsSet {
131
+
mentions = append(mentions, handle)
132
+
}
133
+
return mentions
134
+
}