144 lines
3.5 KiB
Go
144 lines
3.5 KiB
Go
//go:build linux || freebsd
|
|||
|
|
// +build linux freebsd
|
||
|
|
|
||
|
|
// Package linkrewrite rewrites HTML links for local offline viewing.
|
||
|
|
package linkrewrite
|
||
|
|
|
||
|
|
import (
|
||
|
|
"net/url"
|
||
|
|
"path/filepath"
|
||
|
|
"strings"
|
||
|
|
"sync"
|
||
|
|
|
||
|
|
"golang.org/x/net/html"
|
||
|
|
)
|
||
|
|
|
||
|
|
// Rewriter converts absolute URLs in HTML to local relative paths.
|
||
|
|
type Rewriter struct {
|
||
|
|
baseURL *url.URL
|
||
|
|
outputDir string
|
||
|
|
visited map[string]string
|
||
|
|
visitedMu sync.RWMutex
|
||
|
|
}
|
||
|
|
|
||
|
|
// New creates a link rewriter for a mirror or recursive download.
|
||
|
|
func New(baseURL *url.URL, outputDir string) *Rewriter {
|
||
|
|
return &Rewriter{
|
||
|
|
baseURL: baseURL,
|
||
|
|
outputDir: outputDir,
|
||
|
|
visited: make(map[string]string),
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Register records a downloaded URL and its local path.
|
||
|
|
func (lr *Rewriter) Register(u *url.URL, localPath string) {
|
||
|
|
lr.visitedMu.Lock()
|
||
|
|
defer lr.visitedMu.Unlock()
|
||
|
|
lr.visited[u.String()] = localPath
|
||
|
|
}
|
||
|
|
|
||
|
|
// RewriteLinks converts URLs in an HTML document to local paths.
|
||
|
|
func (lr *Rewriter) RewriteLinks(doc *html.Node, filePath string) int {
|
||
|
|
converted := 0
|
||
|
|
dir := filepath.Dir(filePath)
|
||
|
|
|
||
|
|
var traverse func(*html.Node)
|
||
|
|
traverse = func(n *html.Node) {
|
||
|
|
if n.Type == html.ElementNode {
|
||
|
|
switch n.Data {
|
||
|
|
case "a":
|
||
|
|
converted += lr.rewriteAttr(n, "href", dir)
|
||
|
|
case "img", "script", "iframe", "video", "audio", "source", "track", "embed", "object":
|
||
|
|
converted += lr.rewriteAttr(n, "src", dir)
|
||
|
|
case "link":
|
||
|
|
converted += lr.rewriteAttr(n, "href", dir)
|
||
|
|
case "form":
|
||
|
|
converted += lr.rewriteAttr(n, "action", dir)
|
||
|
|
case "area":
|
||
|
|
converted += lr.rewriteAttr(n, "href", dir)
|
||
|
|
case "meta":
|
||
|
|
for _, attr := range n.Attr {
|
||
|
|
if attr.Key == "content" && strings.Contains(attr.Val, "url=") {
|
||
|
|
converted += lr.rewriteMetaRefresh(n, dir)
|
||
|
|
}
|
||
|
|
if attr.Key == "property" && strings.Contains(attr.Val, "image") {
|
||
|
|
converted += lr.rewriteAttr(n, "content", dir)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
for c := n.FirstChild; c != nil; c = c.NextSibling {
|
||
|
|
traverse(c)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
traverse(doc)
|
||
|
|
return converted
|
||
|
|
}
|
||
|
|
|
||
|
|
func (lr *Rewriter) rewriteMetaRefresh(n *html.Node, baseDir string) int {
|
||
|
|
for i, attr := range n.Attr {
|
||
|
|
if attr.Key == "content" {
|
||
|
|
content := attr.Val
|
||
|
|
idx := strings.Index(strings.ToLower(content), "url=")
|
||
|
|
if idx == -1 {
|
||
|
|
return 0
|
||
|
|
}
|
||
|
|
urlStr := strings.TrimSpace(content[idx+4:])
|
||
|
|
parsed, err := url.Parse(urlStr)
|
||
|
|
if err != nil {
|
||
|
|
return 0
|
||
|
|
}
|
||
|
|
resolved := lr.baseURL.ResolveReference(parsed)
|
||
|
|
lr.visitedMu.RLock()
|
||
|
|
localPath, exists := lr.visited[resolved.String()]
|
||
|
|
lr.visitedMu.RUnlock()
|
||
|
|
if exists {
|
||
|
|
relPath, err := filepath.Rel(baseDir, localPath)
|
||
|
|
if err != nil {
|
||
|
|
relPath = localPath
|
||
|
|
}
|
||
|
|
n.Attr[i].Val = content[:idx+4] + filepath.ToSlash(relPath)
|
||
|
|
return 1
|
||
|
|
}
|
||
|
|
return 0
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return 0
|
||
|
|
}
|
||
|
|
|
||
|
|
func (lr *Rewriter) rewriteAttr(n *html.Node, attrName, baseDir string) int {
|
||
|
|
for i, attr := range n.Attr {
|
||
|
|
if attr.Key != attrName {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
value := attr.Val
|
||
|
|
if strings.HasPrefix(value, "#") ||
|
||
|
|
strings.HasPrefix(value, "javascript:") ||
|
||
|
|
strings.HasPrefix(value, "mailto:") ||
|
||
|
|
strings.HasPrefix(value, "tel:") ||
|
||
|
|
strings.HasPrefix(value, "data:") ||
|
||
|
|
strings.HasPrefix(value, "about:") {
|
||
|
|
return 0
|
||
|
|
}
|
||
|
|
parsed, err := url.Parse(value)
|
||
|
|
if err != nil {
|
||
|
|
return 0
|
||
|
|
}
|
||
|
|
resolved := lr.baseURL.ResolveReference(parsed)
|
||
|
|
lr.visitedMu.RLock()
|
||
|
|
localPath, exists := lr.visited[resolved.String()]
|
||
|
|
lr.visitedMu.RUnlock()
|
||
|
|
if !exists {
|
||
|
|
return 0
|
||
|
|
}
|
||
|
|
relPath, err := filepath.Rel(baseDir, localPath)
|
||
|
|
if err != nil {
|
||
|
|
relPath = localPath
|
||
|
|
}
|
||
|
|
n.Attr[i].Val = filepath.ToSlash(relPath)
|
||
|
|
return 1
|
||
|
|
}
|
||
|
|
return 0
|
||
|
|
}
|