From 82cf75b68a381d724346d7355383b611cf18b66a Mon Sep 17 00:00:00 2001 From: silverwind Date: Sat, 30 May 2026 18:55:26 +0200 Subject: [PATCH] enhance(markup): improve issue title rendering (#37908) --- modules/markup/html_emoji.go | 5 +++++ modules/markup/html_test.go | 3 +++ modules/templates/util_render.go | 17 +++++++++----- modules/templates/util_render_test.go | 32 +++++++++++++++++++++++++++ web_src/css/repo/issue-list.css | 1 + 5 files changed, 53 insertions(+), 5 deletions(-) diff --git a/modules/markup/html_emoji.go b/modules/markup/html_emoji.go index 99e28371503..e7c2d1ba6c4 100644 --- a/modules/markup/html_emoji.go +++ b/modules/markup/html_emoji.go @@ -69,6 +69,11 @@ func emojiShortCodeProcessor(ctx *RenderContext, node *html.Node) { m[1] += start start = m[1] + // don't render a shortcode whose ":" directly follows a backtick (an unclosed code span) + if m[0] > 0 && node.Data[m[0]-1] == '`' { + continue + } + alias := node.Data[m[0]:m[1]] var nextChar byte diff --git a/modules/markup/html_test.go b/modules/markup/html_test.go index 518fe7cf240..f3aec68b6ad 100644 --- a/modules/markup/html_test.go +++ b/modules/markup/html_test.go @@ -377,6 +377,9 @@ func TestRender_emoji(t *testing.T) { test(":100:200", `

:100:200

`) test("std::thread::something", `

std::thread::something

`) test(":not exist:", `

:not exist:

`) + test("foo `:smile:", "

foo `:smile:

") + test("foo `:smile:`", `

foo :smile:

`) + test("foo ` :smile:", "

foo ` 😄

") } func TestRender_ShortLinks(t *testing.T) { diff --git a/modules/templates/util_render.go b/modules/templates/util_render.go index 2ff2f758f50..3c7f9b0a7a1 100644 --- a/modules/templates/util_render.go +++ b/modules/templates/util_render.go @@ -107,19 +107,26 @@ func renderCodeBlock(htmlEscapedTextToRender template.HTML) template.HTML { // RenderIssueTitle renders issue/pull title with defined post processors func (ut *RenderUtils) RenderIssueTitle(text string, repo *repo.Repository) template.HTML { - renderedText, err := markup.PostProcessIssueTitle(renderhelper.NewRenderContextRepoComment(ut.ctx, repo), template.HTMLEscapeString(text)) + // wrap "`…`" in before post-processing so code-span content stays literal, like comment bodies + htmlWithCode := renderCodeBlock(template.HTML(template.HTMLEscapeString(text))) + renderedText, err := markup.PostProcessIssueTitle(renderhelper.NewRenderContextRepoComment(ut.ctx, repo), string(htmlWithCode)) if err != nil { log.Error("PostProcessIssueTitle: %v", err) return "" } - return renderCodeBlock(template.HTML(renderedText)) + return template.HTML(renderedText) } // RenderIssueSimpleTitle only renders with emoji and inline code block func (ut *RenderUtils) RenderIssueSimpleTitle(text string) template.HTML { - ret := ut.RenderEmoji(text) - ret = renderCodeBlock(ret) - return ret + // see RenderIssueTitle: wrap code spans before processing emoji + htmlWithCode := renderCodeBlock(template.HTML(template.HTMLEscapeString(text))) + renderedText, err := markup.PostProcessEmoji(markup.NewRenderContext(ut.ctx), string(htmlWithCode)) + if err != nil { + log.Error("RenderIssueSimpleTitle: %v", err) + return "" + } + return template.HTML(renderedText) } func (ut *RenderUtils) RenderLabel(label *issues_model.Label) template.HTML { diff --git a/modules/templates/util_render_test.go b/modules/templates/util_render_test.go index be1190cc496..50a443c7468 100644 --- a/modules/templates/util_render_test.go +++ b/modules/templates/util_render_test.go @@ -180,6 +180,38 @@ mail@domain.com }) } +func TestRenderIssueTitleCodeSpan(t *testing.T) { + defer test.MockVariableValue(&markup.RenderBehaviorForTesting.DisableAdditionalAttributes, true)() + mockRepo := &repo.Repository{ + ID: 1, OwnerName: "user13", Name: "repo11", + Owner: &user_model.User{ID: 13, Name: "user13"}, + Units: []*repo.RepoUnit{}, + } + ut := newTestRenderUtils(t) + + cases := []struct { + input string + expected string + emojiSafe bool + }{ + {"foo `:100:`", `foo :100:`, true}, + {"`#123`", `#123`, false}, + {"`88fc37a3c0a4dda553bdcfc80c178a58247f42fb`", `88fc37a3c0a4dda553bdcfc80c178a58247f42fb`, false}, + {"foo `:100:", "foo `:100:", true}, + {"foo ` :100:", `foo ` + "`" + ` 💯`, true}, + {":100:", `💯`, true}, + {"#123", `#123`, false}, + {"`x`:100:", `x💯`, true}, + {"a `:100:` b `:+1:` c", `a :100: b :+1: c`, true}, + } + for _, c := range cases { + assert.Equal(t, c.expected, string(ut.RenderIssueTitle(c.input, mockRepo)), "input=%q", c.input) + if c.emojiSafe { + assert.Equal(t, c.expected, string(ut.RenderIssueSimpleTitle(c.input)), "simple input=%q", c.input) + } + } +} + func TestRenderMarkdownToHtml(t *testing.T) { defer test.MockVariableValue(&markup.RenderBehaviorForTesting.DisableAdditionalAttributes, true)() expected := `

space @mention-user
diff --git a/web_src/css/repo/issue-list.css b/web_src/css/repo/issue-list.css index 2d55ef83dce..1a635f20199 100644 --- a/web_src/css/repo/issue-list.css +++ b/web_src/css/repo/issue-list.css @@ -39,6 +39,7 @@ font-weight: var(--font-weight-semibold); color: var(--color-text); text-decoration: none; + overflow-wrap: anywhere; } #issue-list .branches {