all repos

mugit @ 9718c0f3da9512866c9e7e4e63b66a5dc7db8019

🐮 git server that your cow will love
8 files changed, 206 insertions(+), 173 deletions(-)
refactor: markdown renderer 
Author: Oleksandr Smirnov olexsmir@gmail.com
Committed at: 2026-03-01 23:47:04 +0200
Change ID: xnmzwkwturptswnkzyztruzsmutoxyxr
Parent: 365b06c
M flake.nix

@@ -15,7 +15,7 @@ mugit = pkgs.buildGoModule {

pname = "mugit"; version = version; src = ./.; - vendorHash = "sha256-xF8IRS0Ne1zp4u6uolKFpKEZObSM6VhV95JUj2krXPY="; + vendorHash = "sha256-TmCwI6axUTGZEVTXyBxQbO4mbK8Dn9vaTU+/Y+K1oxM="; ldflags = [ "-s" "-w" "-X main.version=${version}" ]; meta = with pkgs.lib; { homepage = "https://git.olexsmir.xyz/mugit";
M go.mod

@@ -11,6 +11,7 @@ github.com/gorilla/feeds v1.2.0

github.com/urfave/cli/v3 v3.6.2 github.com/yuin/goldmark v1.7.16 github.com/yuin/goldmark-emoji v1.0.6 + gitlab.com/staticnoise/goldmark-callout v0.0.0-20240609120641-6366b799e4ab golang.org/x/crypto v0.47.0 golang.org/x/sync v0.19.0 gopkg.in/yaml.v2 v2.4.0
M go.sum

@@ -77,6 +77,8 @@ github.com/yuin/goldmark v1.7.16 h1:n+CJdUxaFMiDUNnWC3dMWCIQJSkxH4uz3ZwQBkAlVNE=

github.com/yuin/goldmark v1.7.16/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg= github.com/yuin/goldmark-emoji v1.0.6 h1:QWfF2FYaXwL74tfGOW5izeiZepUDroDJfWubQI9HTHs= github.com/yuin/goldmark-emoji v1.0.6/go.mod h1:ukxJDKFpdFb5x0a5HqbdlcKtebh086iJpI31LTKmWuA= +gitlab.com/staticnoise/goldmark-callout v0.0.0-20240609120641-6366b799e4ab h1:gK9tS6QJw5F0SIhYJnGG2P83kuabOdmWBbSmZhJkz2A= +gitlab.com/staticnoise/goldmark-callout v0.0.0-20240609120641-6366b799e4ab/go.mod h1:SPu13/NPe1kMrbGoJldQwqtpNhXsmIuHCfm/aaGjU0c= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8= golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A=
M internal/handlers/repo.go

@@ -15,12 +15,8 @@ "strconv"

"strings" "time" - "github.com/yuin/goldmark" - emoji "github.com/yuin/goldmark-emoji" - "github.com/yuin/goldmark/extension" - "github.com/yuin/goldmark/renderer/html" "olexsmir.xyz/mugit/internal/git" - "olexsmir.xyz/mugit/internal/mdx" + "olexsmir.xyz/mugit/internal/markdown" ) type Meta struct {

@@ -459,15 +455,6 @@ h.diffCache.Set(cacheKey, diff)

return diff, nil } -var markdown = goldmark.New( - goldmark.WithRendererOptions(html.WithUnsafe()), - goldmark.WithExtensions( - extension.GFM, - extension.Linkify, - emoji.Emoji, - mdx.RelativeLink, - )) - func (h *handlers) renderReadme(r *git.Repo, ref, treePath string) (template.HTML, error) { name := r.Name() cacheKey := fmt.Sprintf("%s:%s:%s", name, ref, treePath)

@@ -492,12 +479,12 @@ content := fc.String()

if len(content) > 0 { switch ext { case ".md", ".markdown", ".mkd": - var buf bytes.Buffer - if cerr := markdown.Convert([]byte(content), &buf, - mdx.NewRelativeLinkCtx(name, fullPath)); cerr != nil { - return "", cerr + readme, err := markdown.Render(name, ref, fullPath, content) + if err != nil { + return "", err } - readmeContents = template.HTML(buf.String()) + return template.HTML(readme), nil + default: readmeContents = template.HTML(fmt.Sprintf(`<pre>%s</pre>`, content)) }
A internal/markdown/markdown.go

@@ -0,0 +1,43 @@

+package markdown + +import ( + "bytes" + "path/filepath" + + "github.com/yuin/goldmark" + emoji "github.com/yuin/goldmark-emoji" + "github.com/yuin/goldmark/extension" + "github.com/yuin/goldmark/parser" + "github.com/yuin/goldmark/renderer/html" + callout "gitlab.com/staticnoise/goldmark-callout" +) + +var ( + repoNameKey = parser.NewContextKey() + repoRefKey = parser.NewContextKey() + baseDirKey = parser.NewContextKey() + + markdown = goldmark.New( + goldmark.WithRendererOptions(html.WithUnsafe()), + goldmark.WithParserOptions(parser.WithAutoHeadingID()), + goldmark.WithExtensions( + extension.GFM, + emoji.Emoji, + callout.CalloutExtention, + &relativeLink{}, + )) +) + +func Render(repoName, repoRef, readmePath, readmeSource string) (string, error) { + ctx := parser.NewContext() + ctx.Set(repoNameKey, repoName) + ctx.Set(repoRefKey, repoRef) + ctx.Set(baseDirKey, filepath.Dir(readmePath)) + parserOpts := parser.WithContext(ctx) + + var buf bytes.Buffer + if err := markdown.Convert([]byte(readmeSource), &buf, parserOpts); err != nil { + return "", err + } + return buf.String(), nil +}
A internal/markdown/relink.go

@@ -0,0 +1,149 @@

+package markdown + +import ( + "bytes" + "net/url" + "path" + "regexp" + "strings" + + "github.com/yuin/goldmark" + "github.com/yuin/goldmark/ast" + "github.com/yuin/goldmark/parser" + "github.com/yuin/goldmark/renderer" + "github.com/yuin/goldmark/text" + "github.com/yuin/goldmark/util" +) + +type relativeLink struct{} + +func (e *relativeLink) Extend(m goldmark.Markdown) { + m.Parser().AddOptions(parser.WithASTTransformers(util.Prioritized(&relLinkTransformer{}, 99))) + m.Renderer().AddOptions(renderer.WithNodeRenderers(util.Prioritized(&rawBlockRenderer{}, 100))) +} + +type relLinkTransformer struct { + repoName string + repoRef string + baseDir string +} + +func (m *relLinkTransformer) Transform(node *ast.Document, reader text.Reader, pc parser.Context) { + m.repoName, _ = pc.Get(repoNameKey).(string) + m.repoRef, _ = pc.Get(repoRefKey).(string) + m.baseDir, _ = pc.Get(baseDirKey).(string) + + _ = ast.Walk(node, func(n ast.Node, entering bool) (ast.WalkStatus, error) { + if !entering { + return ast.WalkContinue, nil + } + + switch n := n.(type) { + case *ast.Link: + m.relativeLinkTransformer(n) + + case *ast.Image: + m.imageFromRepoTransformer(n) + + case *ast.RawHTML: + buf := m.segmentsToBytes(n.Segments, reader) + updated := m.rewriteSrc(buf) + if !bytes.Equal(updated, buf) { + s := ast.NewString(updated) + s.SetRaw(true) + n.Parent().ReplaceChild(n.Parent(), n, s) + } + + case *ast.HTMLBlock: + buf := m.segmentsToBytes(n.Lines(), reader) + updated := m.rewriteSrc(buf) + if !bytes.Equal(updated, buf) { + n.Parent().ReplaceChild(n.Parent(), n, &rawBlock{data: updated}) + } + } + + return ast.WalkContinue, nil + }) +} + +func (m *relLinkTransformer) relativeLinkTransformer(link *ast.Link) { + dst := string(link.Destination) + if isAbsoluteURL(dst) { + return + } + + act := m.path(dst) + link.Destination = []byte(path.Join("/", m.repoName, "tree", m.repoRef, act)) +} + +func (m *relLinkTransformer) imageFromRepoTransformer(img *ast.Image) { + img.Destination = []byte(m.imageFromRepo( + string(img.Destination))) +} + +func (m *relLinkTransformer) imageFromRepo(dst string) string { + if isAbsoluteURL(dst) { + return dst + } + + absPath := m.path(dst) + return path.Join("/", url.PathEscape(m.repoName), "blob", m.repoRef, absPath) + + "?raw=true" +} + +func (m *relLinkTransformer) path(dst string) string { + if path.IsAbs(dst) { + return dst + } + return path.Join(m.baseDir, dst) +} + +var imgSrcRe = regexp.MustCompile(`src="([^"]*)"`) + +func (m *relLinkTransformer) rewriteSrc(buf []byte) []byte { + return imgSrcRe.ReplaceAllFunc(buf, func(match []byte) []byte { + start := bytes.IndexByte(match, '"') + 1 + end := bytes.LastIndexByte(match, '"') + src := string(match[start:end]) + return []byte(`src="` + m.imageFromRepo(src) + `"`) + }) +} + +func (m *relLinkTransformer) segmentsToBytes(segs *text.Segments, reader text.Reader) []byte { + var buf []byte + for i := 0; i < segs.Len(); i++ { + buf = append(buf, reader.Value(segs.At(i))...) + } + return buf +} + +func isAbsoluteURL(link string) bool { + if strings.HasPrefix(link, "#") { + return true + } + u, err := url.Parse(link) + return err == nil && (u.Scheme != "" || strings.HasPrefix(link, "//")) +} + +// row block + +type rawBlock struct { + ast.BaseBlock + data []byte +} + +var rawBlockKind = ast.NewNodeKind("RawBlock") + +func (r *rawBlock) Kind() ast.NodeKind { return rawBlockKind } +func (r *rawBlock) Dump(_ []byte, _ int) {} + +type rawBlockRenderer struct{} + +func (r *rawBlockRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) { + reg.Register(rawBlockKind, func(w util.BufWriter, _ []byte, node ast.Node, entering bool) (ast.WalkStatus, error) { + if entering { + w.Write(node.(*rawBlock).data) + } + return ast.WalkContinue, nil + }) +}
D

@@ -1,152 +0,0 @@

-package mdx - -import ( - "bytes" - "fmt" - "net/url" - "path" - "path/filepath" - "regexp" - "strings" - - "github.com/yuin/goldmark" - "github.com/yuin/goldmark/ast" - "github.com/yuin/goldmark/parser" - "github.com/yuin/goldmark/renderer" - "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)), - ) - m.Renderer().AddOptions( - renderer.WithNodeRenderers(util.Prioritized(&rawBlockRenderer{}, 100)), - ) -} - -type rawBlock struct { - ast.BaseBlock - data []byte -} - -var rawBlockKind = ast.NewNodeKind("RawBlock") - -func (r *rawBlock) Kind() ast.NodeKind { return rawBlockKind } -func (r *rawBlock) Dump(_ []byte, _ int) {} - -type rawBlockRenderer struct{} - -func (r *rawBlockRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) { - reg.Register(rawBlockKind, func(w util.BufWriter, _ []byte, node ast.Node, entering bool) (ast.WalkStatus, error) { - if entering { - w.Write(node.(*rawBlock).data) - } - return ast.WalkContinue, nil - }) -} - -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) - - _ = ast.Walk(node, func(n ast.Node, entering bool) (ast.WalkStatus, error) { - if !entering { - return ast.WalkContinue, nil - } - - switch v := n.(type) { - case *ast.Image: - if rewritten := t.rewriteDest(v.Destination, repoName, baseDir); rewritten != nil { - v.Destination = rewritten - } - case *ast.Link: - if rewritten := t.rewriteDest(v.Destination, repoName, baseDir); rewritten != nil { - v.Destination = rewritten - } - case *ast.RawHTML: - var buf []byte - for i := 0; i < v.Segments.Len(); i++ { - buf = append(buf, reader.Value(v.Segments.At(i))...) - } - updated := t.rewriteSrc(buf, repoName, baseDir) - if !bytes.Equal(updated, buf) { - str := ast.NewString(updated) - str.SetRaw(true) - n.Parent().ReplaceChild(n.Parent(), n, str) - } - case *ast.HTMLBlock: - var buf []byte - for i := 0; i < v.Lines().Len(); i++ { - buf = append(buf, reader.Value(v.Lines().At(i))...) - } - updated := t.rewriteSrc(buf, repoName, baseDir) - if !bytes.Equal(updated, buf) { - n.Parent().ReplaceChild(n.Parent(), n, &rawBlock{data: updated}) - } - } - - return ast.WalkContinue, nil - }) -} - -func (t *relinkTransformer) rewriteDest(dest []byte, repoName, baseDir string) []byte { - urlStr := string(dest) - if strings.HasPrefix(urlStr, "http://") || - strings.HasPrefix(urlStr, "https://") || - strings.HasPrefix(urlStr, "//") || - strings.HasPrefix(urlStr, "#") || - strings.HasPrefix(urlStr, "mailto:") || - strings.HasPrefix(urlStr, "data:") { - return nil - } - urlStr = strings.TrimPrefix(urlStr, "./") - - var absPath string - if after, ok := strings.CutPrefix(urlStr, "/"); ok { - absPath = after - } else { - if baseDir == "" || baseDir == "." { - absPath = urlStr - } else { - absPath = path.Join(baseDir, urlStr) - } - } - - absPath = strings.TrimPrefix(path.Clean(absPath), "/") - - // FIXME: hardcoded ref and link - return fmt.Appendf(nil, "/%s/blob/HEAD/%s?raw=true", url.PathEscape(repoName), absPath) -} - -var imgSrcRe = regexp.MustCompile(`src="([^"]*)"`) - -func (t *relinkTransformer) rewriteSrc(buf []byte, repoName, baseDir string) []byte { - return imgSrcRe.ReplaceAllFunc(buf, func(match []byte) []byte { - src := imgSrcRe.FindSubmatch(match)[1] - rewritten := t.rewriteDest(src, repoName, baseDir) - if rewritten == nil { - return match - } - return append([]byte(`src="`), append(rewritten, '"')...) - }) -}
M web/static/style.css

@@ -370,9 +370,12 @@ }

/* readme */ .readme { padding: 0.6rem 0; } -.readme pre { white-space: pre-wrap; } .readme ul { padding: revert; } .readme img { max-width: 100%; } +.readme pre { + background: var(--light-gray); + padding: 0.6rem; +} @media (max-width: 600px) { .repo-index {