all repos

mugit @ f16095b7455c197b40bcac1367367ed9545807e4

🐮 git server that your cow will love

mugit/internal/markdown/relink.go (view raw)

Oleksandr Smirnov Oleksandr Smirnov
olexsmir@gmail.com
serve raw files on separate route, 2 months ago
1
package markdown
2
3
import (
4
	"bytes"
5
	"net/url"
6
	"path"
7
	"regexp"
8
	"strings"
9
10
	"github.com/yuin/goldmark"
11
	"github.com/yuin/goldmark/ast"
12
	"github.com/yuin/goldmark/parser"
13
	"github.com/yuin/goldmark/renderer"
14
	"github.com/yuin/goldmark/text"
15
	"github.com/yuin/goldmark/util"
16
)
17
18
type relativeLink struct{}
19
20
func (e *relativeLink) Extend(m goldmark.Markdown) {
21
	m.Parser().AddOptions(parser.WithASTTransformers(util.Prioritized(&relLinkTransformer{}, 99)))
22
	m.Renderer().AddOptions(renderer.WithNodeRenderers(util.Prioritized(&rawBlockRenderer{}, 100)))
23
}
24
25
type relLinkTransformer struct {
26
	repoName string
27
	repoRef  string
28
	baseDir  string
29
}
30
31
func (m *relLinkTransformer) Transform(node *ast.Document, reader text.Reader, pc parser.Context) {
32
	m.repoName, _ = pc.Get(repoNameKey).(string)
33
	m.repoRef, _ = pc.Get(repoRefKey).(string)
34
	m.baseDir, _ = pc.Get(baseDirKey).(string)
35
36
	_ = ast.Walk(node, func(n ast.Node, entering bool) (ast.WalkStatus, error) {
37
		if !entering {
38
			return ast.WalkContinue, nil
39
		}
40
41
		switch n := n.(type) {
42
		case *ast.Link:
43
			m.relativeLinkTransformer(n)
44
45
		case *ast.Image:
46
			m.imageFromRepoTransformer(n)
47
48
		case *ast.RawHTML:
49
			buf := m.segmentsToBytes(n.Segments, reader)
50
			updated := m.rewriteSrc(buf)
51
			if !bytes.Equal(updated, buf) {
52
				s := ast.NewString(updated)
53
				s.SetRaw(true)
54
				n.Parent().ReplaceChild(n.Parent(), n, s)
55
			}
56
57
		case *ast.HTMLBlock:
58
			buf := m.segmentsToBytes(n.Lines(), reader)
59
			updated := m.rewriteSrc(buf)
60
			if !bytes.Equal(updated, buf) {
61
				n.Parent().ReplaceChild(n.Parent(), n, &rawBlock{data: updated})
62
			}
63
		}
64
65
		return ast.WalkContinue, nil
66
	})
67
}
68
69
func (m *relLinkTransformer) relativeLinkTransformer(link *ast.Link) {
70
	dst := string(link.Destination)
71
	if isAbsoluteURL(dst) {
72
		return
73
	}
74
75
	act := m.path(dst)
76
	link.Destination = []byte(path.Join("/", m.repoName, "tree", m.repoRef, act))
77
}
78
79
func (m *relLinkTransformer) imageFromRepoTransformer(img *ast.Image) {
80
	img.Destination = []byte(m.imageFromRepo(
81
		string(img.Destination)))
82
}
83
84
func (m *relLinkTransformer) imageFromRepo(dst string) string {
85
	if isAbsoluteURL(dst) {
86
		return dst
87
	}
88
89
	absPath := m.path(dst)
90
	return path.Join("/", url.PathEscape(m.repoName), "raw", m.repoRef, absPath)
91
}
92
93
func (m *relLinkTransformer) path(dst string) string {
94
	if path.IsAbs(dst) {
95
		return dst
96
	}
97
	return path.Join(m.baseDir, dst)
98
}
99
100
var imgSrcRe = regexp.MustCompile(`src="([^"]*)"`)
101
102
func (m *relLinkTransformer) rewriteSrc(buf []byte) []byte {
103
	return imgSrcRe.ReplaceAllFunc(buf, func(match []byte) []byte {
104
		start := bytes.IndexByte(match, '"') + 1
105
		end := bytes.LastIndexByte(match, '"')
106
		src := string(match[start:end])
107
		return []byte(`src="` + m.imageFromRepo(src) + `"`)
108
	})
109
}
110
111
func (m *relLinkTransformer) segmentsToBytes(segs *text.Segments, reader text.Reader) []byte {
112
	var buf []byte
113
	for i := 0; i < segs.Len(); i++ {
114
		buf = append(buf, reader.Value(segs.At(i))...)
115
	}
116
	return buf
117
}
118
119
func isAbsoluteURL(link string) bool {
120
	if strings.HasPrefix(link, "#") {
121
		return true
122
	}
123
	u, err := url.Parse(link)
124
	return err == nil && (u.Scheme != "" || strings.HasPrefix(link, "//"))
125
}
126
127
// row block
128
129
type rawBlock struct {
130
	ast.BaseBlock
131
	data []byte
132
}
133
134
var rawBlockKind = ast.NewNodeKind("RawBlock")
135
136
func (r *rawBlock) Kind() ast.NodeKind   { return rawBlockKind }
137
func (r *rawBlock) Dump(_ []byte, _ int) {}
138
139
type rawBlockRenderer struct{}
140
141
func (r *rawBlockRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) {
142
	reg.Register(rawBlockKind, func(w util.BufWriter, _ []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
143
		if entering {
144
			w.Write(node.(*rawBlock).data)
145
		}
146
		return ast.WalkContinue, nil
147
	})
148
}