at master 121 lines 2.9 kB view raw
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}