forked from tangled.org/core
Monorepo for Tangled — https://tangled.org
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}