package render
import ( "bytes" "html/template" "strings"
"golang.org/x/net/html"
"golang.org/x/net/html/atom"
)
// Rebase rewrites relative href/src URLs in a rendered HTML fragment so they // resolve against the repository rather than the current page. This is needed // when repo markdown (e.g. a README shown on the summary page) links to sibling // files with relative paths like "DEPLOY.md": on the summary URL the browser // would resolve that to a non-existent path, so we point links at linkBase and // embedded resources at rawBase. Both bases must end with "/". // // Absolute URLs (scheme://…), protocol-relative ("//host"), root-relative // ("/path"), and in-page anchors ("#x") are left unchanged. func Rebase(frag template.HTML, linkBase, rawBase string) template.HTML { body := &html.Node{Type: html.ElementNode, Data: "body", DataAtom: atom.Body} nodes, err := html.ParseFragment(strings.NewReader(string(frag)), body) if err != nil { return frag } for _, n := range nodes { rebaseNode(n, linkBase, rawBase) } var buf bytes.Buffer for _, n := range nodes { if err := html.Render(&buf, n); err != nil { return frag } } return template.HTML(buf.String()) }
func rebaseNode(n *html.Node, linkBase, rawBase string) { if n.Type == html.ElementNode { switch n.Data { case "a": rebaseAttr(n, "href", linkBase) case "img": rebaseAttr(n, "src", rawBase) } } for c := n.FirstChild; c != nil; c = c.NextSibling { rebaseNode(c, linkBase, rawBase) } }
func rebaseAttr(n *html.Node, key, base string) { for i, a := range n.Attr { if a.Key == key && isRelativeURL(a.Val) { n.Attr[i].Val = base + a.Val return } } }
// isRelativeURL reports whether u is a relative reference that should be rebased // (e.g. "DEPLOY.md", "docs/x.md", "../y.md") rather than an absolute URL, // protocol-relative URL, root-relative path, or fragment. func isRelativeURL(u string) bool { if u == "" || strings.HasPrefix(u, "#") || strings.HasPrefix(u, "/") { return false } return !hasScheme(u) }
// hasScheme reports whether u begins with a URL scheme ("http:", "mailto:", // "data:", …) — i.e. a ':' appears before any '/', '?', or '#', preceded only by // valid scheme characters. func hasScheme(u string) bool { for i := 0; i < len(u); i++ { c := u[i] switch { case c == ':': return i > 0 case c == '/' || c == '?' || c == '#': return false case c >= 'a' && c <= 'z', c >= 'A' && c <= 'Z', c >= '0' && c <= '9', c == '+', c == '-', c == '.': // valid scheme character; keep scanning default: return false } } return false }