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