Monorepo for Tangled tangled.org
6

Configure Feed

Select the types of activity you want to include in your feed.

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}