+7
-1
appview/pages/markup/markdown.go
+7
-1
appview/pages/markup/markdown.go
···
50
50
Files fs.FS
51
51
}
52
52
53
-
func (rctx *RenderContext) RenderMarkdown(source string) string {
53
+
func NewMarkdown() goldmark.Markdown {
54
54
md := goldmark.New(
55
55
goldmark.WithExtensions(
56
56
extension.GFM,
···
66
66
),
67
67
treeblood.MathML(),
68
68
callout.CalloutExtention,
69
+
AtExt,
69
70
),
70
71
goldmark.WithParserOptions(
71
72
parser.WithAutoHeadingID(),
72
73
),
73
74
goldmark.WithRendererOptions(html.WithUnsafe()),
74
75
)
76
+
return md
77
+
}
78
+
79
+
func (rctx *RenderContext) RenderMarkdown(source string) string {
80
+
md := NewMarkdown()
75
81
76
82
if rctx != nil {
77
83
var transformers []util.PrioritizedValue
+135
appview/pages/markup/markdown_at_extension.go
+135
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
+
atSegment := text.NewSegment(segment.Start, segment.Start+m[1])
59
+
block.Advance(m[1])
60
+
node := &AtNode{}
61
+
node.AppendChild(node, ast.NewTextSegment(atSegment))
62
+
node.handle = string(atSegment.Value(block.Source())[1:])
63
+
return node
64
+
}
65
+
66
+
// atHtmlRenderer is a renderer.NodeRenderer implementation that
67
+
// renders At nodes.
68
+
type atHtmlRenderer struct {
69
+
html.Config
70
+
}
71
+
72
+
// NewAtHTMLRenderer returns a new AtHTMLRenderer.
73
+
func NewAtHTMLRenderer(opts ...html.Option) renderer.NodeRenderer {
74
+
r := &atHtmlRenderer{
75
+
Config: html.NewConfig(),
76
+
}
77
+
for _, opt := range opts {
78
+
opt.SetHTMLOption(&r.Config)
79
+
}
80
+
return r
81
+
}
82
+
83
+
// RegisterFuncs implements renderer.NodeRenderer.RegisterFuncs.
84
+
func (r *atHtmlRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) {
85
+
reg.Register(KindAt, r.renderAt)
86
+
}
87
+
88
+
func (r *atHtmlRenderer) renderAt(w util.BufWriter, source []byte, n ast.Node, entering bool) (ast.WalkStatus, error) {
89
+
if entering {
90
+
w.WriteString(`<a href="/@`)
91
+
w.WriteString(n.(*AtNode).handle)
92
+
w.WriteString(`" class="text-red-500">`)
93
+
} else {
94
+
w.WriteString("</a>")
95
+
}
96
+
return ast.WalkContinue, nil
97
+
}
98
+
99
+
type atExt struct{}
100
+
101
+
// At is an extension that allow you to use at expression like '@user.bsky.social' .
102
+
var AtExt = &atExt{}
103
+
104
+
func (e *atExt) Extend(m goldmark.Markdown) {
105
+
m.Parser().AddOptions(parser.WithInlineParsers(
106
+
util.Prioritized(NewAtParser(), 500),
107
+
))
108
+
m.Renderer().AddOptions(renderer.WithNodeRenderers(
109
+
util.Prioritized(NewAtHTMLRenderer(), 500),
110
+
))
111
+
}
112
+
113
+
// FindUserMentions returns Set of user handles from given markup soruce.
114
+
// It doesn't guarntee unique DIDs
115
+
func FindUserMentions(source string) []string {
116
+
var (
117
+
mentions []string
118
+
mentionsSet = make(map[string]struct{})
119
+
md = NewMarkdown()
120
+
sourceBytes = []byte(source)
121
+
root = md.Parser().Parse(text.NewReader(sourceBytes))
122
+
)
123
+
ast.Walk(root, func(n ast.Node, entering bool) (ast.WalkStatus, error) {
124
+
if entering && n.Kind() == KindAt {
125
+
handle := n.(*AtNode).handle
126
+
mentionsSet[handle] = struct{}{}
127
+
return ast.WalkSkipChildren, nil
128
+
}
129
+
return ast.WalkContinue, nil
130
+
})
131
+
for handle := range mentionsSet {
132
+
mentions = append(mentions, handle)
133
+
}
134
+
return mentions
135
+
}