From 621aa67e7d42a11305a437e189606400c58d4306 Mon Sep 17 00:00:00 2001 From: silverwind Date: Tue, 19 May 2026 15:09:56 +0200 Subject: [PATCH] fix(markup): wrap indented code blocks for the code-copy button (#37748) Indented (4-space) code blocks were emitted by goldmark's default renderer as plain `
` without the `code-block-container`
wrapper that the JS `initMarkupCodeCopy` keys on. As a result, only
fenced code blocks received the copy button. Register
`ast.KindCodeBlock` with a renderer that produces the same wrapper as
the highlighting renderer so both syntaxes get the button.

Extends `TestMarkdownFencedCodeBlock` to assert the wrapper is emitted
for indented blocks (and that HTML inside is escaped).

Co-authored-by: Claude (Opus 4.7) 
Co-authored-by: wxiaoguang 
---
 modules/markup/markdown/goldmark.go      | 21 +++++++++++++++++++++
 modules/markup/markdown/markdown_test.go |  4 +++-
 2 files changed, 24 insertions(+), 1 deletion(-)

diff --git a/modules/markup/markdown/goldmark.go b/modules/markup/markdown/goldmark.go
index 4a560517f22..b2b16f5d9ff 100644
--- a/modules/markup/markdown/goldmark.go
+++ b/modules/markup/markdown/goldmark.go
@@ -119,12 +119,33 @@ func (r *HTMLRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) {
 	reg.Register(KindDetails, r.renderDetails)
 	reg.Register(KindSummary, r.renderSummary)
 	reg.Register(ast.KindCodeSpan, r.renderCodeSpan)
+	reg.Register(ast.KindCodeBlock, r.renderCodeBlock)
 	reg.Register(KindAttention, r.renderAttention)
 	reg.Register(KindTaskCheckBoxListItem, r.renderTaskCheckBoxListItem)
 	reg.Register(east.KindTaskCheckBox, r.renderTaskCheckBox)
 	reg.Register(KindRawHTML, r.renderRawHTML)
 }
 
+// renderCodeBlock wraps indented code blocks like the fenced renderer
+func (r *HTMLRenderer) renderCodeBlock(w util.BufWriter, source []byte, n ast.Node, entering bool) (ast.WalkStatus, error) {
+	if entering {
+		opening := r.renderInternal.ProtectSafeAttrs(`
`)
+		if _, err := w.WriteString(string(opening)); err != nil {
+			return ast.WalkStop, err
+		}
+		lines := n.Lines()
+		for i := 0; i < lines.Len(); i++ {
+			line := lines.At(i)
+			r.Writer.RawWrite(w, line.Value(source))
+		}
+	} else {
+		if _, err := w.WriteString("
"); err != nil { + return ast.WalkStop, err + } + } + return ast.WalkContinue, nil +} + func (r *HTMLRenderer) renderDocument(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) { n := node.(*ast.Document) diff --git a/modules/markup/markdown/markdown_test.go b/modules/markup/markdown/markdown_test.go index 2f14a0fae98..c759dbf8b10 100644 --- a/modules/markup/markdown/markdown_test.go +++ b/modules/markup/markdown/markdown_test.go @@ -601,7 +601,7 @@ func TestMarkdownUlDir(t *testing.T) { `, string(result)) } -func TestMarkdownFencedCodeBlock(t *testing.T) { +func TestMarkdownCodeBlock(t *testing.T) { testRender := func(input, expected string) { buffer, err := markdown.RenderString(markup.NewTestRenderContext(), input) assert.NoError(t, err) @@ -618,4 +618,6 @@ func TestMarkdownFencedCodeBlock(t *testing.T) { testRender("```js:app.ts\ncode\n```", jsCommon) testRender("```js,ignore\ncode\n```", jsCommon) testRender("```js ignore\ncode\n```", jsCommon) + testRender(" code\n", prefix+`code`+nl+``+suffix) + testRender(" \n", prefix+`<script>alert(1)</script>`+nl+``+suffix) }