this repo has no description
at master 3.2 kB view raw
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}