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(host string, source string) ([]string, []models.ReferenceLink) {
22 var (
23 refLinkSet = make(map[models.ReferenceLink]struct{})
24 mentionsSet = make(map[string]struct{})
25 md = NewMarkdown(host)
26 sourceBytes = []byte(source)
27 root = md.Parser().Parse(text.NewReader(sourceBytes))
28 )
29
30 ast.Walk(root, func(n ast.Node, entering bool) (ast.WalkStatus, error) {
31 if !entering {
32 return ast.WalkContinue, nil
33 }
34 switch n.Kind() {
35 case textension.KindAt:
36 handle := n.(*textension.AtNode).Handle
37 mentionsSet[handle] = struct{}{}
38 return ast.WalkSkipChildren, nil
39 case ast.KindLink:
40 dest := string(n.(*ast.Link).Destination)
41 ref := parseTangledLink(host, dest)
42 if ref != nil {
43 refLinkSet[*ref] = struct{}{}
44 }
45 return ast.WalkSkipChildren, nil
46 case ast.KindAutoLink:
47 an := n.(*ast.AutoLink)
48 if an.AutoLinkType == ast.AutoLinkURL {
49 dest := string(an.URL(sourceBytes))
50 ref := parseTangledLink(host, dest)
51 if ref != nil {
52 refLinkSet[*ref] = struct{}{}
53 }
54 }
55 return ast.WalkSkipChildren, nil
56 }
57 return ast.WalkContinue, nil
58 })
59 mentions := slices.Collect(maps.Keys(mentionsSet))
60 references := slices.Collect(maps.Keys(refLinkSet))
61 return mentions, references
62}
63
64func parseTangledLink(baseHost string, urlStr string) *models.ReferenceLink {
65 u, err := url.Parse(urlStr)
66 if err != nil {
67 return nil
68 }
69
70 if u.Host != "" && !strings.EqualFold(u.Host, baseHost) {
71 return nil
72 }
73
74 p := path.Clean(u.Path)
75 parts := strings.FieldsFunc(p, func(r rune) bool { return r == '/' })
76 if len(parts) < 4 {
77 // need at least: handle / repo / kind / id
78 return nil
79 }
80
81 var (
82 handle = parts[0]
83 repo = parts[1]
84 kindSeg = parts[2]
85 subjectSeg = parts[3]
86 )
87
88 handle = strings.TrimPrefix(handle, "@")
89
90 var kind models.RefKind
91 switch kindSeg {
92 case "issues":
93 kind = models.RefKindIssue
94 case "pulls":
95 kind = models.RefKindPull
96 default:
97 return nil
98 }
99
100 subjectId, err := strconv.Atoi(subjectSeg)
101 if err != nil {
102 return nil
103 }
104 var commentId *int
105 if u.Fragment != "" {
106 if strings.HasPrefix(u.Fragment, "comment-") {
107 commentIdStr := u.Fragment[len("comment-"):]
108 if id, err := strconv.Atoi(commentIdStr); err == nil {
109 commentId = &id
110 }
111 }
112 }
113
114 return &models.ReferenceLink{
115 Handle: handle,
116 Repo: repo,
117 Kind: kind,
118 SubjectId: subjectId,
119 CommentId: commentId,
120 }
121}