Monorepo for Tangled
tangled.org
1package markup
2
3import (
4 "maps"
5 "net/url"
6 "path"
7 "slices"
8 "strconv"
9 "strings"
10
11 "github.com/bluesky-social/indigo/atproto/syntax"
12 "github.com/yuin/goldmark/ast"
13 "github.com/yuin/goldmark/text"
14 "tangled.org/core/appview/models"
15 textension "tangled.org/core/appview/pages/markup/extension"
16)
17
18// FindReferences collects all links referencing tangled-related objects
19// like issues, PRs, comments or even @-mentions
20// This function doesn't actually check for the existence of records in the DB
21// or the PDS; it merely returns a list of what are presumed to be references.
22func FindReferences(host string, source string) ([]string, []models.ReferenceLink) {
23 var (
24 refLinkSet = make(map[models.ReferenceLink]struct{})
25 mentionsSet = make(map[string]struct{})
26 md = NewMarkdown(host)
27 sourceBytes = []byte(source)
28 root = md.Parser().Parse(text.NewReader(sourceBytes))
29 )
30
31 ast.Walk(root, func(n ast.Node, entering bool) (ast.WalkStatus, error) {
32 if !entering {
33 return ast.WalkContinue, nil
34 }
35 switch n.Kind() {
36 case textension.KindAt:
37 handle := n.(*textension.AtNode).Handle
38 mentionsSet[handle] = struct{}{}
39 return ast.WalkSkipChildren, nil
40 case ast.KindLink:
41 dest := string(n.(*ast.Link).Destination)
42 ref := parseTangledLink(host, dest)
43 if ref != nil {
44 refLinkSet[*ref] = struct{}{}
45 }
46 return ast.WalkSkipChildren, nil
47 case ast.KindAutoLink:
48 an := n.(*ast.AutoLink)
49 if an.AutoLinkType == ast.AutoLinkURL {
50 dest := string(an.URL(sourceBytes))
51 ref := parseTangledLink(host, dest)
52 if ref != nil {
53 refLinkSet[*ref] = struct{}{}
54 }
55 }
56 return ast.WalkSkipChildren, nil
57 }
58 return ast.WalkContinue, nil
59 })
60 mentions := slices.Collect(maps.Keys(mentionsSet))
61 references := slices.Collect(maps.Keys(refLinkSet))
62 return mentions, references
63}
64
65func parseTangledLink(baseHost string, urlStr string) *models.ReferenceLink {
66 u, err := url.Parse(urlStr)
67 if err != nil {
68 return nil
69 }
70
71 if u.Host != "" && !strings.EqualFold(u.Host, baseHost) {
72 return nil
73 }
74
75 p := path.Clean(u.Path)
76 parts := strings.FieldsFunc(p, func(r rune) bool { return r == '/' })
77 if len(parts) < 4 {
78 // need at least: handle / repo / kind / id
79 return nil
80 }
81
82 var (
83 handle = parts[0]
84 repo = parts[1]
85 kindSeg = parts[2]
86 subjectSeg = parts[3]
87 )
88
89 handle = strings.TrimPrefix(handle, "@")
90
91 var kind models.RefKind
92 switch kindSeg {
93 case "issues":
94 kind = models.RefKindIssue
95 case "pulls":
96 kind = models.RefKindPull
97 default:
98 return nil
99 }
100
101 subjectId, err := strconv.Atoi(subjectSeg)
102 if err != nil {
103 return nil
104 }
105 var commentRkey *syntax.RecordKey
106 if u.Fragment != "" {
107 if strings.HasPrefix(u.Fragment, "comment-") {
108 if rkey, err := syntax.ParseRecordKey(u.Fragment[len("comment-"):]); err != nil {
109 commentRkey = &rkey
110 }
111 }
112 }
113
114 return &models.ReferenceLink{
115 Handle: handle,
116 Repo: repo,
117 Kind: kind,
118 SubjectId: subjectId,
119 CommentRkey: commentRkey,
120 }
121}