mugit/internal/markdown/relink.go (view raw)
Oleksandr Smirnov
Oleksandr Smirnov
olexsmir@gmail.com serve raw files on separate route, 2 months ago
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 | } |