all repos

mugit @ cc59ce4becd876f686c626f299c8969541c1d90f

🐮 git server that your cow will love

mugit/internal/mdx/relative_link.go (view raw)

Oleksandr Smirnov Oleksandr Smirnov
olexsmir@gmail.com
mdx: fix raw <img> sources, 3 months ago
1
package mdx
2
3
import (
4
	"bytes"
5
	"fmt"
6
	"net/url"
7
	"path"
8
	"path/filepath"
9
	"regexp"
10
	"strings"
11
12
	"github.com/yuin/goldmark"
13
	"github.com/yuin/goldmark/ast"
14
	"github.com/yuin/goldmark/parser"
15
	"github.com/yuin/goldmark/renderer"
16
	"github.com/yuin/goldmark/text"
17
	"github.com/yuin/goldmark/util"
18
)
19
20
var (
21
	repoNameKey = parser.NewContextKey()
22
	baseDirKey  = parser.NewContextKey()
23
)
24
25
func NewRelativeLinkCtx(repoName, readmePath string) parser.ParseOption {
26
	ctx := parser.NewContext()
27
	ctx.Set(repoNameKey, repoName)
28
	ctx.Set(baseDirKey, filepath.Dir(readmePath))
29
	return parser.WithContext(ctx)
30
}
31
32
var RelativeLink = &relativeLink{}
33
34
type relativeLink struct{}
35
36
func (e *relativeLink) Extend(m goldmark.Markdown) {
37
	m.Parser().AddOptions(
38
		parser.WithASTTransformers(util.Prioritized(&relinkTransformer{}, 100)),
39
	)
40
	m.Renderer().AddOptions(
41
		renderer.WithNodeRenderers(util.Prioritized(&rawBlockRenderer{}, 100)),
42
	)
43
}
44
45
type rawBlock struct {
46
	ast.BaseBlock
47
	data []byte
48
}
49
50
var rawBlockKind = ast.NewNodeKind("RawBlock")
51
52
func (r *rawBlock) Kind() ast.NodeKind   { return rawBlockKind }
53
func (r *rawBlock) Dump(_ []byte, _ int) {}
54
55
type rawBlockRenderer struct{}
56
57
func (r *rawBlockRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) {
58
	reg.Register(rawBlockKind, func(w util.BufWriter, _ []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
59
		if entering {
60
			w.Write(node.(*rawBlock).data)
61
		}
62
		return ast.WalkContinue, nil
63
	})
64
}
65
66
type relinkTransformer struct{}
67
68
func (t *relinkTransformer) Transform(node *ast.Document, reader text.Reader, pc parser.Context) {
69
	repoName, _ := pc.Get(repoNameKey).(string)
70
	baseDir, _ := pc.Get(baseDirKey).(string)
71
72
	_ = ast.Walk(node, func(n ast.Node, entering bool) (ast.WalkStatus, error) {
73
		if !entering {
74
			return ast.WalkContinue, nil
75
		}
76
77
		switch v := n.(type) {
78
		case *ast.Image:
79
			if rewritten := t.rewriteDest(v.Destination, repoName, baseDir); rewritten != nil {
80
				v.Destination = rewritten
81
			}
82
		case *ast.Link:
83
			if rewritten := t.rewriteDest(v.Destination, repoName, baseDir); rewritten != nil {
84
				v.Destination = rewritten
85
			}
86
		case *ast.RawHTML:
87
			var buf []byte
88
			for i := 0; i < v.Segments.Len(); i++ {
89
				buf = append(buf, reader.Value(v.Segments.At(i))...)
90
			}
91
			updated := t.rewriteSrc(buf, repoName, baseDir)
92
			if !bytes.Equal(updated, buf) {
93
				str := ast.NewString(updated)
94
				str.SetRaw(true)
95
				n.Parent().ReplaceChild(n.Parent(), n, str)
96
			}
97
		case *ast.HTMLBlock:
98
			var buf []byte
99
			for i := 0; i < v.Lines().Len(); i++ {
100
				buf = append(buf, reader.Value(v.Lines().At(i))...)
101
			}
102
			updated := t.rewriteSrc(buf, repoName, baseDir)
103
			if !bytes.Equal(updated, buf) {
104
				n.Parent().ReplaceChild(n.Parent(), n, &rawBlock{data: updated})
105
			}
106
		}
107
108
		return ast.WalkContinue, nil
109
	})
110
}
111
112
func (t *relinkTransformer) rewriteDest(dest []byte, repoName, baseDir string) []byte {
113
	urlStr := string(dest)
114
	if strings.HasPrefix(urlStr, "http://") ||
115
		strings.HasPrefix(urlStr, "https://") ||
116
		strings.HasPrefix(urlStr, "//") ||
117
		strings.HasPrefix(urlStr, "#") ||
118
		strings.HasPrefix(urlStr, "mailto:") ||
119
		strings.HasPrefix(urlStr, "data:") {
120
		return nil
121
	}
122
	urlStr = strings.TrimPrefix(urlStr, "./")
123
124
	var absPath string
125
	if after, ok := strings.CutPrefix(urlStr, "/"); ok {
126
		absPath = after
127
	} else {
128
		if baseDir == "" || baseDir == "." {
129
			absPath = urlStr
130
		} else {
131
			absPath = path.Join(baseDir, urlStr)
132
		}
133
	}
134
135
	absPath = strings.TrimPrefix(path.Clean(absPath), "/")
136
137
	// FIXME: hardcoded ref and link
138
	return fmt.Appendf(nil, "/%s/blob/HEAD/%s?raw=true", url.PathEscape(repoName), absPath)
139
}
140
141
var imgSrcRe = regexp.MustCompile(`src="([^"]*)"`)
142
143
func (t *relinkTransformer) rewriteSrc(buf []byte, repoName, baseDir string) []byte {
144
	return imgSrcRe.ReplaceAllFunc(buf, func(match []byte) []byte {
145
		src := imgSrcRe.FindSubmatch(match)[1]
146
		rewritten := t.rewriteDest(src, repoName, baseDir)
147
		if rewritten == nil {
148
			return match
149
		}
150
		return append([]byte(`src="`), append(rewritten, '"')...)
151
	})
152
}