all repos

mugit @ e2cbacb4e9ab1dbae47ef104e6e11344e62e88a0

🐮 git server that your cow will love

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

Oleksandr Smirnov Oleksandr Smirnov
olexsmir@gmail.com
refactor: markdown renderer , 3 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), "blob", m.repoRef, absPath) +
91
		"?raw=true"
92
}
93
94
func (m *relLinkTransformer) path(dst string) string {
95
	if path.IsAbs(dst) {
96
		return dst
97
	}
98
	return path.Join(m.baseDir, dst)
99
}
100
101
var imgSrcRe = regexp.MustCompile(`src="([^"]*)"`)
102
103
func (m *relLinkTransformer) rewriteSrc(buf []byte) []byte {
104
	return imgSrcRe.ReplaceAllFunc(buf, func(match []byte) []byte {
105
		start := bytes.IndexByte(match, '"') + 1
106
		end := bytes.LastIndexByte(match, '"')
107
		src := string(match[start:end])
108
		return []byte(`src="` + m.imageFromRepo(src) + `"`)
109
	})
110
}
111
112
func (m *relLinkTransformer) segmentsToBytes(segs *text.Segments, reader text.Reader) []byte {
113
	var buf []byte
114
	for i := 0; i < segs.Len(); i++ {
115
		buf = append(buf, reader.Value(segs.At(i))...)
116
	}
117
	return buf
118
}
119
120
func isAbsoluteURL(link string) bool {
121
	if strings.HasPrefix(link, "#") {
122
		return true
123
	}
124
	u, err := url.Parse(link)
125
	return err == nil && (u.Scheme != "" || strings.HasPrefix(link, "//"))
126
}
127
128
// row block
129
130
type rawBlock struct {
131
	ast.BaseBlock
132
	data []byte
133
}
134
135
var rawBlockKind = ast.NewNodeKind("RawBlock")
136
137
func (r *rawBlock) Kind() ast.NodeKind   { return rawBlockKind }
138
func (r *rawBlock) Dump(_ []byte, _ int) {}
139
140
type rawBlockRenderer struct{}
141
142
func (r *rawBlockRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) {
143
	reg.Register(rawBlockKind, func(w util.BufWriter, _ []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
144
		if entering {
145
			w.Write(node.(*rawBlock).data)
146
		}
147
		return ast.WalkContinue, nil
148
	})
149
}