main
1package links
2
3import (
4 "bytes"
5 "net/url"
6 "path"
7 "strings"
8
9 "golang.org/x/net/html"
10
11 "mokhan.ca/xlgmokha/gitmal/internal/git"
12)
13
14type Set map[string]struct{}
15
16func BuildDirSet(files []git.Blob) Set {
17 dirs := make(Set)
18 for _, f := range files {
19 dir := path.Dir(f.Path)
20 for dir != "." && dir != "/" {
21 if _, ok := dirs[dir]; ok {
22 break
23 }
24 dirs[dir] = struct{}{}
25 if i := strings.LastIndex(dir, "/"); i != -1 {
26 dir = dir[:i]
27 } else {
28 break
29 }
30 }
31 }
32 return dirs
33}
34
35func BuildFileSet(files []git.Blob) Set {
36 filesSet := make(Set)
37 for _, f := range files {
38 filesSet[f.Path] = struct{}{}
39 }
40 return filesSet
41}
42
43func Resolve(content, currentPath, rootHref, ref string, dirs, files Set) string {
44 doc, err := html.Parse(strings.NewReader(content))
45 if err != nil {
46 return content
47 }
48
49 baseDir := path.Dir(currentPath)
50
51 var walk func(*html.Node)
52 walk = func(n *html.Node) {
53 if n.Type == html.ElementNode {
54 switch n.Data {
55 case "a":
56 for i, attr := range n.Attr {
57 if attr.Key == "href" {
58 newHref := transformHref(attr.Val, baseDir, rootHref, ref, files, dirs)
59 n.Attr[i].Val = newHref
60 break
61 }
62 }
63 case "img":
64 for i, attr := range n.Attr {
65 if attr.Key == "src" {
66 newSrc := transformImgSrc(attr.Val, baseDir, rootHref, ref)
67 n.Attr[i].Val = newSrc
68 break
69 }
70 }
71 }
72 }
73
74 for c := n.FirstChild; c != nil; c = c.NextSibling {
75 walk(c)
76 }
77 }
78 walk(doc)
79
80 var buf bytes.Buffer
81 if err := html.Render(&buf, doc); err != nil {
82 return content
83 }
84
85 return buf.String()
86}
87
88func transformHref(href, baseDir, rootHref, ref string, files, dirs Set) string {
89 if href == "" {
90 return href
91 }
92 if strings.HasPrefix(href, "#") {
93 return href
94 }
95
96 u, err := url.Parse(href)
97 if err != nil {
98 return href
99 }
100
101 // Absolute URLs are left untouched
102 if u.IsAbs() {
103 return href
104 }
105
106 // Skip mailto:, javascript:, data: etc. (url.Parse sets Scheme)
107 if u.Scheme != "" {
108 return href
109 }
110
111 // Resolve against the directory of the current file
112 relPath := u.Path
113 if relPath == "" {
114 return href
115 }
116
117 var repoPath string
118
119 if strings.HasPrefix(relPath, "/") {
120 // Root-relative repo path
121 relPath = strings.TrimPrefix(relPath, "/")
122 repoPath = path.Clean(relPath)
123 } else {
124 // Relative to current file directory
125 repoPath = path.Clean(path.Join(baseDir, relPath))
126 }
127
128 // Decide if this is a file or a directory in the repo
129 var newPath string
130
131 // 1) Exact file match
132 if _, ok := files[repoPath]; ok {
133 newPath = repoPath + ".html"
134 } else if _, ok := files[repoPath+".md"]; ok {
135 // 2) Maybe the link omitted ".md" but the repo has it
136 newPath = repoPath + ".md.html"
137 } else if _, ok := dirs[repoPath]; ok {
138 // 3) Directory: add /index.html
139 newPath = path.Join(repoPath, "index.html")
140 } else {
141 // Unknown target, leave as-is
142 return href
143 }
144
145 // Link from the root href
146 newPath = path.Join(rootHref, "blob", ref, newPath)
147
148 // Preserve any query/fragment if they existed
149 u.Path = newPath
150 return u.String()
151}
152
153func transformImgSrc(src, baseDir, rootHref, ref string) string {
154 u, err := url.Parse(src)
155 if err != nil {
156 return src
157 }
158
159 if u.IsAbs() {
160 return src
161 }
162
163 relPath := u.Path
164
165 var repoPath string
166 if strings.HasPrefix(relPath, "/") {
167 // Root-relative: drop leading slash
168 repoPath = strings.TrimPrefix(relPath, "/")
169 } else {
170 // Resolve against current file directory
171 repoPath = path.Clean(path.Join(baseDir, relPath))
172 }
173
174 final := path.Join(rootHref, "raw", ref, repoPath)
175
176 // Preserve any query/fragment if they existed
177 u.Path = final
178 return u.String()
179}