1 files changed,
90 insertions(+),
39 deletions(-)
Author:
Oleksandr Smirnov
olexsmir@gmail.com
Committed at:
2026-02-17 21:37:40 +0200
Authored at:
2026-02-17 21:35:30 +0200
Change ID:
nyvxwyxtmkqkoorkkxlwkpmoluusvlly
Parent:
03f9034
M
internal/mdx/relative_link.go
路路路 1 1 package mdx 2 2 3 3 import ( 4 + "bytes" 4 5 "fmt" 5 6 "net/url" 6 7 "path" 7 8 "path/filepath" 9 + "regexp" 8 10 "strings" 9 11 10 12 "github.com/yuin/goldmark" 11 13 "github.com/yuin/goldmark/ast" 12 14 "github.com/yuin/goldmark/parser" 15 + "github.com/yuin/goldmark/renderer" 13 16 "github.com/yuin/goldmark/text" 14 17 "github.com/yuin/goldmark/util" 15 18 ) 路路路 32 35 33 36 func (e *relativeLink) Extend(m goldmark.Markdown) { 34 37 m.Parser().AddOptions( 35 - parser.WithASTTransformers( 36 - util.Prioritized(&relinkTransformer{}, 100), 37 - ), 38 + parser.WithASTTransformers(util.Prioritized(&relinkTransformer{}, 100)), 39 + ) 40 + m.Renderer().AddOptions( 41 + renderer.WithNodeRenderers(util.Prioritized(&rawBlockRenderer{}, 100)), 38 42 ) 39 43 } 40 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 + 41 66 type relinkTransformer struct{} 42 67 43 68 func (t *relinkTransformer) Transform(node *ast.Document, reader text.Reader, pc parser.Context) { 44 69 repoName, _ := pc.Get(repoNameKey).(string) 45 70 baseDir, _ := pc.Get(baseDirKey).(string) 46 71 47 - if repoName == "" { 48 - return 49 - } 50 - 51 72 _ = ast.Walk(node, func(n ast.Node, entering bool) (ast.WalkStatus, error) { 52 73 if !entering { 53 74 return ast.WalkContinue, nil 54 75 } 55 76 56 - var dest *[]byte 57 77 switch v := n.(type) { 58 78 case *ast.Image: 59 - dest = &v.Destination 79 + if rewritten := t.rewriteDest(v.Destination, repoName, baseDir); rewritten != nil { 80 + v.Destination = rewritten 81 + } 60 82 case *ast.Link: 61 - dest = &v.Destination 62 - default: 63 - return ast.WalkContinue, nil 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 + } 64 106 } 65 107 66 - urlStr := string(*dest) 67 - 68 - // skip absolute URLs 69 - if strings.HasPrefix(urlStr, "http://") || 70 - strings.HasPrefix(urlStr, "https://") || 71 - strings.HasPrefix(urlStr, "//") || 72 - strings.HasPrefix(urlStr, "#") || 73 - strings.HasPrefix(urlStr, "mailto:") || 74 - strings.HasPrefix(urlStr, "data:") { 75 - return ast.WalkContinue, nil 76 - } 108 + return ast.WalkContinue, nil 109 + }) 110 +} 77 111 78 - urlStr = strings.TrimPrefix(urlStr, "./") 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, "./") 79 123 80 - var absPath string 81 - if after, ok := strings.CutPrefix(urlStr, "/"); ok { 82 - absPath = after // abs from repo root 124 + var absPath string 125 + if after, ok := strings.CutPrefix(urlStr, "/"); ok { 126 + absPath = after 127 + } else { 128 + if baseDir == "" || baseDir == "." { 129 + absPath = urlStr 83 130 } else { 84 - // relative to repo location 85 - if baseDir == "" || baseDir == "." { 86 - absPath = urlStr 87 - } else { 88 - absPath = path.Join(baseDir, urlStr) 89 - } 131 + absPath = path.Join(baseDir, urlStr) 90 132 } 133 + } 91 134 92 - absPath = path.Clean(absPath) 93 - absPath = strings.TrimPrefix(absPath, "/") 135 + absPath = strings.TrimPrefix(path.Clean(absPath), "/") 94 136 95 - // FIXME:hardcoded link 96 - *dest = fmt.Appendf(nil, "/%s/blob/HEAD/%s?raw=true", 97 - url.PathEscape(repoName), absPath) 137 + // FIXME: hardcoded ref and link 138 + return fmt.Appendf(nil, "/%s/blob/HEAD/%s?raw=true", url.PathEscape(repoName), absPath) 139 +} 98 140 99 - return ast.WalkContinue, nil 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, '"')...) 100 151 }) 101 152 }