File opensuse-buildresults.patch of Package gitea

commit 9ae48b4769c076ef9aeb65b0f8bec066ce871c53
Author: Adam Majer <amajer@suse.com>
Date:   Sun Aug 24 23:55:12 2025 +0200

    object links to br.opensuse.org

diff --git a/modules/markup/html_test.go b/modules/markup/html_test.go
index 5fdbf43f7c..1a9a2d8b01 100644
--- a/modules/markup/html_test.go
+++ b/modules/markup/html_test.go
@@ -181,6 +181,9 @@ func TestRender_links(t *testing.T) {
 	test(
 		`[link](javascript:xss)`,
 		`<p>link</p>`)
+	test(
+		`![status in TW](https://br.opensuse.org/status/openSUSE:Tumbleweed/nodejs22/standard)`,
+		`<p><object data="https://br.opensuse.org/status/openSUSE:Tumbleweed/nodejs22/standard" type="image/svg+xml" aria-label="status in TW"></object></p>`)
 
 	// Test that should *not* be turned into URL
 	test(
diff --git a/modules/markup/markdown/buildresult_object_renderer.go b/modules/markup/markdown/buildresult_object_renderer.go
new file mode 100644
index 0000000000..e51ae273ab
--- /dev/null
+++ b/modules/markup/markdown/buildresult_object_renderer.go
@@ -0,0 +1,65 @@
+package markdown
+
+import (
+	"bytes"
+	"net/url"
+
+	"github.com/yuin/goldmark/ast"
+	"github.com/yuin/goldmark/renderer"
+	"github.com/yuin/goldmark/renderer/html"
+	"github.com/yuin/goldmark/util"
+)
+
+// buildresultsObjectRenderer renders images from br.opensuse.org as <object>.
+type buildresultsObjectRenderer struct {
+	defaultFuncs map[ast.NodeKind]renderer.NodeRendererFunc
+}
+
+// adapter so we can capture funcs into a map
+type funcRegisterer struct {
+	funcs map[ast.NodeKind]renderer.NodeRendererFunc
+}
+
+func (r *funcRegisterer) Register(kind ast.NodeKind, v renderer.NodeRendererFunc) {
+	r.funcs[kind] = v
+}
+
+func newObjectImageRenderer() *buildresultsObjectRenderer {
+	tmp := html.NewRenderer()
+	reg := &funcRegisterer{funcs: make(map[ast.NodeKind]renderer.NodeRendererFunc)}
+	tmp.RegisterFuncs(reg)
+	return &buildresultsObjectRenderer{defaultFuncs: reg.funcs}
+}
+
+func (r *buildresultsObjectRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) {
+	reg.Register(ast.KindImage, r.renderImage)
+}
+
+func (r *buildresultsObjectRenderer) renderImage(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
+	if !entering {
+		return ast.WalkContinue, nil
+	}
+	img := node.(*ast.Image)
+	if u, err := url.Parse(string(img.Destination)); err == nil && (u.Host == "br.opensuse.org" || u.Host == "buildresults.opensuse.org") {
+		// collect alt text
+		var alt bytes.Buffer
+		for c := img.FirstChild(); c != nil; c = c.NextSibling() {
+			if t, ok := c.(*ast.Text); ok {
+				alt.Write(t.Segment.Value(source))
+			}
+		}
+		// <object data="..."> (let sanitizer control which attrs are kept)
+		w.WriteString(`<object data="`)
+		w.Write(util.EscapeHTML([]byte(u.String())))
+		w.WriteString(`" type="image/svg+xml" aria-label="`)
+		w.Write(util.EscapeHTML(alt.Bytes()))
+		w.WriteString(`"></object>`)
+		return ast.WalkSkipChildren, nil
+	}
+
+	// Delegate to stock html image renderer for all other cases
+	if f := r.defaultFuncs[ast.KindImage]; f != nil {
+		return f(w, source, node, entering)
+	}
+	return ast.WalkContinue, nil
+}
diff --git a/modules/markup/markdown/markdown.go b/modules/markup/markdown/markdown.go
index 79df547c2c..e1463193d5 100644
--- a/modules/markup/markdown/markdown.go
+++ b/modules/markup/markdown/markdown.go
@@ -134,7 +134,10 @@ func SpecializedMarkdown(ctx *markup.RenderContext) *GlodmarkRender {
 			parser.WithAutoHeadingID(),
 			parser.WithASTTransformers(util.Prioritized(NewASTTransformer(&ctx.RenderInternal), 10000)),
 		),
-		goldmark.WithRendererOptions(html.WithUnsafe()),
+		goldmark.WithRendererOptions(
+			html.WithUnsafe(),
+			renderer.WithNodeRenderers(util.Prioritized(newObjectImageRenderer(), 500)),
+		),
 	)
 
 	// Override the original Tasklist renderer!
diff --git a/modules/markup/sanitizer.go b/modules/markup/sanitizer.go
index 391ddad46c..90b833ee1b 100644
--- a/modules/markup/sanitizer.go
+++ b/modules/markup/sanitizer.go
@@ -42,6 +42,14 @@ func GetDefaultSanitizer() *Sanitizer {
 		defaultSanitizer.defaultPolicy = defaultSanitizer.createDefaultPolicy()
 		defaultSanitizer.descriptionPolicy = defaultSanitizer.createRepoDescriptionPolicy()
 	})
+
+	// Allow <object> for br.opensuse.org image replacements.
+	p := defaultSanitizer.defaultPolicy
+	p.AllowElements("object")
+	p.AllowAttrs("data").Matching(regexp.MustCompile(`^https://(br|buildresults)\.opensuse\.org/.*$`)).OnElements("object")
+	p.AllowAttrs("type").Matching(regexp.MustCompile(`^image/svg\+xml$`)).OnElements("object")
+	p.AllowAttrs("aria-label").OnElements("object")
+
 	return defaultSanitizer
 }
 
openSUSE Build Service is sponsored by