| 1 | // SPDX-License-Identifier: AGPL-3.0-or-later |
| 2 | |
| 3 | package repo |
| 4 | |
| 5 | import ( |
| 6 | "bytes" |
| 7 | "io" |
| 8 | "net/url" |
| 9 | "path" |
| 10 | "strings" |
| 11 | |
| 12 | "golang.org/x/net/html" |
| 13 | ) |
| 14 | |
| 15 | func codeRouteBase(owner, repoName, route, ref, dir string) string { |
| 16 | base := "/" + url.PathEscape(owner) + "/" + url.PathEscape(repoName) + "/" + route + "/" + escapePathSegments(ref) |
| 17 | if dir != "" { |
| 18 | base += "/" + escapePathSegments(dir) |
| 19 | } |
| 20 | return base |
| 21 | } |
| 22 | |
| 23 | func rewriteMarkdownRelativeURLs(fragment, linkBase, linkRoot, imageBase, imageRoot string) string { |
| 24 | if fragment == "" { |
| 25 | return "" |
| 26 | } |
| 27 | z := html.NewTokenizer(strings.NewReader(fragment)) |
| 28 | var out bytes.Buffer |
| 29 | for { |
| 30 | tt := z.Next() |
| 31 | if tt == html.ErrorToken { |
| 32 | if z.Err() == io.EOF { |
| 33 | return out.String() |
| 34 | } |
| 35 | return fragment |
| 36 | } |
| 37 | tok := z.Token() |
| 38 | switch tt { |
| 39 | case html.StartTagToken, html.SelfClosingTagToken: |
| 40 | rewriteMarkdownTokenURLs(&tok, linkBase, linkRoot, imageBase, imageRoot) |
| 41 | } |
| 42 | out.WriteString(tok.String()) |
| 43 | } |
| 44 | } |
| 45 | |
| 46 | func rewriteMarkdownTokenURLs(tok *html.Token, linkBase, linkRoot, imageBase, imageRoot string) { |
| 47 | switch tok.Data { |
| 48 | case "a": |
| 49 | rewriteAttr(tok, "href", linkBase, linkRoot) |
| 50 | case "img": |
| 51 | rewriteAttr(tok, "src", imageBase, imageRoot) |
| 52 | } |
| 53 | } |
| 54 | |
| 55 | func rewriteAttr(tok *html.Token, key, base, root string) { |
| 56 | for i := range tok.Attr { |
| 57 | if tok.Attr[i].Key == key { |
| 58 | tok.Attr[i].Val = rewriteRelativeMarkdownURL(tok.Attr[i].Val, base, root) |
| 59 | } |
| 60 | } |
| 61 | } |
| 62 | |
| 63 | func rewriteRelativeMarkdownURL(raw, base, root string) string { |
| 64 | if raw == "" || base == "" || root == "" || strings.TrimSpace(raw) != raw { |
| 65 | return raw |
| 66 | } |
| 67 | if strings.HasPrefix(raw, "#") || strings.HasPrefix(raw, "//") { |
| 68 | return raw |
| 69 | } |
| 70 | u, err := url.Parse(raw) |
| 71 | if err != nil || u.IsAbs() || u.Host != "" || strings.HasPrefix(u.Path, "/") || u.Path == "" { |
| 72 | return raw |
| 73 | } |
| 74 | next := path.Clean(path.Clean(base) + "/" + u.Path) |
| 75 | if next != root && !strings.HasPrefix(next, root+"/") { |
| 76 | return raw |
| 77 | } |
| 78 | u.Path = next |
| 79 | u.RawPath = "" |
| 80 | return u.String() |
| 81 | } |
| 82 |