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}