2 files changed,
108 insertions(+),
3 deletions(-)
Author:
Oleksandr Smirnov
olexsmir@gmail.com
Committed at:
2026-02-15 16:46:22 +0200
Authored at:
2026-02-15 16:44:09 +0200
Change ID:
xzzqlkskrvnsqxzwrzotyxsymzpspqvu
Parent:
850c26a
M
internal/handlers/repo.go
路路路 19 19 "github.com/yuin/goldmark/extension" 20 20 "github.com/yuin/goldmark/renderer/html" 21 21 "olexsmir.xyz/mugit/internal/git" 22 + "olexsmir.xyz/mugit/internal/mdx" 22 23 ) 23 24 24 25 func (h *handlers) indexHandler(w http.ResponseWriter, r *http.Request) { 路路路 408 409 goldmark.WithExtensions( 409 410 extension.GFM, 410 411 extension.Linkify, 412 + mdx.RelativeLink, 411 413 )) 412 414 413 415 func (h *handlers) renderReadme(r *git.Repo) (template.HTML, error) { 414 - if v, found := h.readmeCache.Get(r.Name()); found { 416 + name := r.Name() 417 + if v, found := h.readmeCache.Get(name); found { 415 418 return v, nil 416 419 } 417 420 路路路 432 435 switch ext { 433 436 case ".md", ".markdown", ".mkd": 434 437 var buf bytes.Buffer 435 - if cerr := markdown.Convert([]byte(content), &buf); cerr != nil { 438 + if cerr := markdown.Convert([]byte(content), &buf, 439 + mdx.NewRelativeLinkCtx(name, readme)); cerr != nil { 436 440 return "", cerr 437 441 } 438 442 readmeContents = template.HTML(buf.String()) 路路路 443 447 } 444 448 } 445 449 446 - h.readmeCache.Set(r.Name(), readmeContents) 450 + h.readmeCache.Set(name, readmeContents) 447 451 return readmeContents, nil 448 452 }
A
internal/mdx/relative_link.go
路路路 1 +package mdx 2 + 3 +import ( 4 + "fmt" 5 + "net/url" 6 + "path" 7 + "path/filepath" 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/text" 14 + "github.com/yuin/goldmark/util" 15 +) 16 + 17 +var ( 18 + repoNameKey = parser.NewContextKey() 19 + baseDirKey = parser.NewContextKey() 20 +) 21 + 22 +func NewRelativeLinkCtx(repoName, readmePath string) parser.ParseOption { 23 + ctx := parser.NewContext() 24 + ctx.Set(repoNameKey, repoName) 25 + ctx.Set(baseDirKey, filepath.Dir(readmePath)) 26 + return parser.WithContext(ctx) 27 +} 28 + 29 +var RelativeLink = &relativeLink{} 30 + 31 +type relativeLink struct{} 32 + 33 +func (e *relativeLink) Extend(m goldmark.Markdown) { 34 + m.Parser().AddOptions( 35 + parser.WithASTTransformers( 36 + util.Prioritized(&relinkTransformer{}, 100), 37 + ), 38 + ) 39 +} 40 + 41 +type relinkTransformer struct{} 42 + 43 +func (t *relinkTransformer) Transform(node *ast.Document, reader text.Reader, pc parser.Context) { 44 + repoName, _ := pc.Get(repoNameKey).(string) 45 + baseDir, _ := pc.Get(baseDirKey).(string) 46 + 47 + if repoName == "" { 48 + return 49 + } 50 + 51 + _ = ast.Walk(node, func(n ast.Node, entering bool) (ast.WalkStatus, error) { 52 + if !entering { 53 + return ast.WalkContinue, nil 54 + } 55 + 56 + var dest *[]byte 57 + switch v := n.(type) { 58 + case *ast.Image: 59 + dest = &v.Destination 60 + case *ast.Link: 61 + dest = &v.Destination 62 + default: 63 + return ast.WalkContinue, nil 64 + } 65 + 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 + } 77 + 78 + urlStr = strings.TrimPrefix(urlStr, "./") 79 + 80 + var absPath string 81 + if after, ok := strings.CutPrefix(urlStr, "/"); ok { 82 + absPath = after // abs from repo root 83 + } else { 84 + // relative to repo location 85 + if baseDir == "" || baseDir == "." { 86 + absPath = urlStr 87 + } else { 88 + absPath = path.Join(baseDir, urlStr) 89 + } 90 + } 91 + 92 + absPath = path.Clean(absPath) 93 + absPath = strings.TrimPrefix(absPath, "/") 94 + 95 + // FIXME:hardcoded link 96 + *dest = fmt.Appendf(nil, "/%s/blob/HEAD/%s?raw=true", 97 + url.PathEscape(repoName), absPath) 98 + 99 + return ast.WalkContinue, nil 100 + }) 101 +}