all repos

mugit @ 6e228d9192b98e484881b8e702734ae920511538

🐮 git server that your cow will love
2 files changed, 108 insertions(+), 3 deletions(-)
ui: render images with relative links
Author: Oleksandr Smirnov olexsmir@gmail.com
Committed at: 2026-02-15 16:46:22 +0200
Change ID: xzzqlkskrvnsqxzwrzotyxsymzpspqvu
Parent: 850c26a
M internal/handlers/repo.go

@@ -19,6 +19,7 @@ "github.com/yuin/goldmark"

"github.com/yuin/goldmark/extension" "github.com/yuin/goldmark/renderer/html" "olexsmir.xyz/mugit/internal/git" + "olexsmir.xyz/mugit/internal/mdx" ) func (h *handlers) indexHandler(w http.ResponseWriter, r *http.Request) {

@@ -408,10 +409,12 @@ goldmark.WithRendererOptions(html.WithUnsafe()),

goldmark.WithExtensions( extension.GFM, extension.Linkify, + mdx.RelativeLink, )) func (h *handlers) renderReadme(r *git.Repo) (template.HTML, error) { - if v, found := h.readmeCache.Get(r.Name()); found { + name := r.Name() + if v, found := h.readmeCache.Get(name); found { return v, nil }

@@ -432,7 +435,8 @@ if len(content) > 0 {

switch ext { case ".md", ".markdown", ".mkd": var buf bytes.Buffer - if cerr := markdown.Convert([]byte(content), &buf); cerr != nil { + if cerr := markdown.Convert([]byte(content), &buf, + mdx.NewRelativeLinkCtx(name, readme)); cerr != nil { return "", cerr } readmeContents = template.HTML(buf.String())

@@ -443,6 +447,6 @@ break

} } - h.readmeCache.Set(r.Name(), readmeContents) + h.readmeCache.Set(name, readmeContents) return readmeContents, nil }
A internal/mdx/relative_link.go

@@ -0,0 +1,101 @@

+package mdx + +import ( + "fmt" + "net/url" + "path" + "path/filepath" + "strings" + + "github.com/yuin/goldmark" + "github.com/yuin/goldmark/ast" + "github.com/yuin/goldmark/parser" + "github.com/yuin/goldmark/text" + "github.com/yuin/goldmark/util" +) + +var ( + repoNameKey = parser.NewContextKey() + baseDirKey = parser.NewContextKey() +) + +func NewRelativeLinkCtx(repoName, readmePath string) parser.ParseOption { + ctx := parser.NewContext() + ctx.Set(repoNameKey, repoName) + ctx.Set(baseDirKey, filepath.Dir(readmePath)) + return parser.WithContext(ctx) +} + +var RelativeLink = &relativeLink{} + +type relativeLink struct{} + +func (e *relativeLink) Extend(m goldmark.Markdown) { + m.Parser().AddOptions( + parser.WithASTTransformers( + util.Prioritized(&relinkTransformer{}, 100), + ), + ) +} + +type relinkTransformer struct{} + +func (t *relinkTransformer) Transform(node *ast.Document, reader text.Reader, pc parser.Context) { + repoName, _ := pc.Get(repoNameKey).(string) + baseDir, _ := pc.Get(baseDirKey).(string) + + if repoName == "" { + return + } + + _ = ast.Walk(node, func(n ast.Node, entering bool) (ast.WalkStatus, error) { + if !entering { + return ast.WalkContinue, nil + } + + var dest *[]byte + switch v := n.(type) { + case *ast.Image: + dest = &v.Destination + case *ast.Link: + dest = &v.Destination + default: + return ast.WalkContinue, nil + } + + urlStr := string(*dest) + + // skip absolute URLs + if strings.HasPrefix(urlStr, "http://") || + strings.HasPrefix(urlStr, "https://") || + strings.HasPrefix(urlStr, "//") || + strings.HasPrefix(urlStr, "#") || + strings.HasPrefix(urlStr, "mailto:") || + strings.HasPrefix(urlStr, "data:") { + return ast.WalkContinue, nil + } + + urlStr = strings.TrimPrefix(urlStr, "./") + + var absPath string + if after, ok := strings.CutPrefix(urlStr, "/"); ok { + absPath = after // abs from repo root + } else { + // relative to repo location + if baseDir == "" || baseDir == "." { + absPath = urlStr + } else { + absPath = path.Join(baseDir, urlStr) + } + } + + absPath = path.Clean(absPath) + absPath = strings.TrimPrefix(absPath, "/") + + // FIXME:hardcoded link + *dest = fmt.Appendf(nil, "/%s/blob/HEAD/%s?raw=true", + url.PathEscape(repoName), absPath) + + return ast.WalkContinue, nil + }) +}