commit 74068cf0454b23a66fe24ee8ca961304abf6d0a9
| author | 斟酌 鵬兄 <tgckpg@gmail.com> |
| date | 2026-10-13T13:39:17Z |
| subject | markdown to show image correctly |
commit 74068cf0454b23a66fe24ee8ca961304abf6d0a9
Author: 斟酌 鵬兄 <tgckpg@gmail.com>
Date: 2026-10-13T13:39:17Z
markdown to show image correctly
---
internal/buildinfo/buildinfo_gen.go | 4 +--
internal/render/markdown.go | 37 +++++++++++++++++++++++--
internal/render/markdown_test.go | 55 +++++++++++++++++++++++++++++++++++++
internal/render/render.go | 2 +-
internal/render/rtree.go | 2 +-
5 files changed, 94 insertions(+), 6 deletions(-)
diff --git a/internal/buildinfo/buildinfo_gen.go b/internal/buildinfo/buildinfo_gen.go
index ab795c1..da365ca 100644
--- a/internal/buildinfo/buildinfo_gen.go
+++ b/internal/buildinfo/buildinfo_gen.go
@@ -2,6 +2,6 @@ package buildinfo
const (
Version = "dev"
- GitRevision = "249de61fd9ac8f7c418b6a45f54f28ddfb220173"
- Timestamp = "20261005.051202"
+ GitRevision = "0bbb52a7252c199f9cebc6fb42d219a14c073a3b"
+ Timestamp = "20261013.132630"
)
diff --git a/internal/render/markdown.go b/internal/render/markdown.go
index 343eb29..0836d35 100644
--- a/internal/render/markdown.go
+++ b/internal/render/markdown.go
@@ -2,23 +2,38 @@ package render
import (
"bytes"
+ "net/url"
+ "path"
"path/filepath"
"strings"
"github.com/microcosm-cc/bluemonday"
"github.com/yuin/goldmark"
+ "github.com/yuin/goldmark/ast"
"github.com/yuin/goldmark/extension"
"github.com/yuin/goldmark/parser"
+ "github.com/yuin/goldmark/text"
)
-func renderMarkdownSafe(src []byte) ([]byte, error) {
+// rawBase is the path from the rendered page to raw/<revision>.
+func renderMarkdownSafe(src []byte, markdownPath, rawBase string) ([]byte, error) {
md := goldmark.New(
goldmark.WithExtensions(extension.GFM),
goldmark.WithParserOptions(parser.WithAutoHeadingID()),
)
+ doc := md.Parser().Parse(text.NewReader(src))
+ if err := ast.Walk(doc, func(node ast.Node, entering bool) (ast.WalkStatus, error) {
+ if img, ok := node.(*ast.Image); ok && entering {
+ img.Destination = []byte(markdownImageURL(string(img.Destination), markdownPath, rawBase))
+ }
+ return ast.WalkContinue, nil
+ }); err != nil {
+ return nil, err
+ }
+
var buf bytes.Buffer
- if err := md.Convert(src, &buf); err != nil {
+ if err := md.Renderer().Render(&buf, src, doc); err != nil {
return nil, err
}
@@ -26,6 +41,24 @@ func renderMarkdownSafe(src []byte) ([]byte, error) {
return policy.SanitizeBytes(buf.Bytes()), nil
}
+func markdownImageURL(destination, markdownPath, rawBase string) string {
+ u, err := url.Parse(destination)
+ if err != nil || u.IsAbs() || u.Host != "" || strings.HasPrefix(destination, "//") || u.Path == "" {
+ return destination
+ }
+
+ // Resolve within the repository, including repository-root paths. Clamp
+ // parent traversal at its root so images cannot escape the raw revision.
+ imagePath := u.Path
+ if !strings.HasPrefix(imagePath, "/") {
+ imagePath = path.Dir(markdownPath) + "/" + imagePath
+ }
+ imagePath = strings.TrimPrefix(path.Clean("/"+imagePath), "/")
+ u.Path = path.Join(rawBase, imagePath)
+ u.RawPath = ""
+ return u.String()
+}
+
func isMarkdownPath(path string) bool {
name := strings.ToLower(filepath.Base(path))
ext := strings.ToLower(filepath.Ext(path))
diff --git a/internal/render/markdown_test.go b/internal/render/markdown_test.go
new file mode 100644
index 0000000..1cf6973
--- /dev/null
+++ b/internal/render/markdown_test.go
@@ -0,0 +1,55 @@
+package render
+
+import (
+ "strings"
+ "testing"
+)
+
+func TestMarkdownImageURL(t *testing.T) {
+ tests := []struct {
+ name, destination, markdownPath, rawBase, want string
+ }{
+ {"summary", "images/logo.png", "README.md", "raw/master", "raw/master/images/logo.png"},
+ {"blob", "images/logo.png", "README.md", "../../raw/master", "../../raw/master/images/logo.png"},
+ {"nested", "./images/logo.png", "docs/guide.md", "../../../raw/master", "../../../raw/master/docs/images/logo.png"},
+ {"parent", "../images/logo.png", "docs/guide.md", "../../../raw/master", "../../../raw/master/images/logo.png"},
+ {"root", "/images/logo.png", "docs/guide.md", "../../../raw/master", "../../../raw/master/images/logo.png"},
+ {"commit", "logo.png", "docs/guide.md", "../../../raw/abc123", "../../../raw/abc123/docs/logo.png"},
+ {"escaping", "images/my%20logo%23%3F.png?v=1#preview", "README.md", "raw/master", "raw/master/images/my%20logo%23%3F.png?v=1#preview"},
+ {"traversal", "../../../logo.png", "docs/guide.md", "../../../raw/master", "../../../raw/master/logo.png"},
+ {"https", "https://example.com/logo.png", "README.md", "raw/master", "https://example.com/logo.png"},
+ {"protocol relative", "//example.com/logo.png", "README.md", "raw/master", "//example.com/logo.png"},
+ {"data", "data:image/png;base64,AAAA", "README.md", "raw/master", "data:image/png;base64,AAAA"},
+ {"fragment", "#logo", "README.md", "raw/master", "#logo"},
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ if got := markdownImageURL(tt.destination, tt.markdownPath, tt.rawBase); got != tt.want {
+ t.Fatalf("got %q, want %q", got, tt.want)
+ }
+ })
+ }
+}
+
+func TestRenderMarkdownImages(t *testing.T) {
+ src := []byte("\n\n![reference][logo]\n\n[logo]: ./local.png \"Title\"\n\n[link](../images/logo.png)\n\n\n\n)\n\n``\n")
+ html, err := renderMarkdownSafe(src, "docs/guide.md", "../../../raw/master")
+ if err != nil {
+ t.Fatal(err)
+ }
+ for _, want := range []string{
+ `src="../../../raw/master/images/logo.png"`,
+ `src="../../../raw/master/docs/local.png"`,
+ `title="Title"`,
+ `href="../images/logo.png"`,
+ `src="https://example.com/logo.png"`,
+ `<code></code>`,
+ } {
+ if !strings.Contains(string(html), want) {
+ t.Errorf("missing %q in %s", want, html)
+ }
+ }
+ if strings.Contains(string(html), "javascript:") {
+ t.Errorf("unsafe image URL survived sanitization: %s", html)
+ }
+}
diff --git a/internal/render/render.go b/internal/render/render.go
index dfc95ad..990b957 100644
--- a/internal/render/render.go
+++ b/internal/render/render.go
@@ -228,7 +228,7 @@ func (r *Renderer) renderReadmeHTML(
return "", err
}
- htmlBytes, err := renderMarkdownSafe(content)
+ htmlBytes, err := renderMarkdownSafe(content, readmePath, "raw/"+refSlug(repo.DefaultBranch))
if err != nil {
return "", err
}
diff --git a/internal/render/rtree.go b/internal/render/rtree.go
index f65074b..24c9647 100644
--- a/internal/render/rtree.go
+++ b/internal/render/rtree.go
@@ -170,7 +170,7 @@ func (r *Renderer) renderBlobEntry(
}
if !blob.Binary && isMarkdownPath(blob.Path) {
- htmlBytes, err := renderMarkdownSafe(content)
+ htmlBytes, err := renderMarkdownSafe(content, e.Path, relPath(filepath.Dir(blobPath), filepath.Join(next, "raw", revSlug)))
if err != nil {
return err
}