mirror of
https://github.com/go-gitea/gitea.git
synced 2026-05-20 05:04:50 +02:00
Fix Mermaid code blocks broken on dashboard feed (#36582)
Fix two bugs causing broken rendering of fenced code blocks (e.g. Mermaid diagrams) in dashboard feed comments: 1. Truncation at 200 chars could cut mid-code-block, leaving an unclosed fence that produces a parse error. Add trimUnclosedCodeBlock() to strip the partial block after truncation. 2. The action content format "index|body" was parsed with SplitN(..., 3), splitting the comment body at its first "|" character. Mermaid syntax commonly uses "|" (e.g. "A -->|text| B"). Add GetIssueContentBody() which splits only on the first "|" and use it in the feed template. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
514f322dcf
commit
a56cc4da86
@ -372,6 +372,18 @@ func (a *Action) IsIssueEvent() bool {
|
|||||||
return a.OpType.InActions("comment_issue", "approve_pull_request", "reject_pull_request", "comment_pull", "merge_pull_request")
|
return a.OpType.InActions("comment_issue", "approve_pull_request", "reject_pull_request", "comment_pull", "merge_pull_request")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetIssueContentBody returns the comment body from the action content.
|
||||||
|
// Unlike GetIssueInfos which splits on "|" into 3 parts, this only splits on
|
||||||
|
// the first "|" to preserve any "|" characters within the comment body
|
||||||
|
// (e.g. Mermaid diagram syntax like "A -->|text| B").
|
||||||
|
func (a *Action) GetIssueContentBody() string {
|
||||||
|
parts := strings.SplitN(a.Content, "|", 2)
|
||||||
|
if len(parts) < 2 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return parts[1]
|
||||||
|
}
|
||||||
|
|
||||||
// GetIssueInfos returns a list of associated information with the action.
|
// GetIssueInfos returns a list of associated information with the action.
|
||||||
func (a *Action) GetIssueInfos() []string {
|
func (a *Action) GetIssueInfos() []string {
|
||||||
// make sure it always returns 3 elements, because there are some access to the a[1] and a[2] without checking the length
|
// make sure it always returns 3 elements, because there are some access to the a[1] and a[2] without checking the length
|
||||||
|
|||||||
@ -25,6 +25,41 @@ type actionNotifier struct {
|
|||||||
notify_service.NullNotifier
|
notify_service.NullNotifier
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// trimUnclosedCodeBlock removes a trailing unclosed fenced code block from a
|
||||||
|
// truncated string. When content is cut at an arbitrary character limit, a
|
||||||
|
// fenced code block (``` ...) may be left open, producing invalid Markdown that
|
||||||
|
// the renderer tries (and fails) to process. This function detects the
|
||||||
|
// situation and strips the partial block.
|
||||||
|
func trimUnclosedCodeBlock(s string) string {
|
||||||
|
inBlock := false
|
||||||
|
lastOpenIdx := -1
|
||||||
|
i := 0
|
||||||
|
for i < len(s) {
|
||||||
|
lineStart := i
|
||||||
|
lineEnd := strings.Index(s[i:], "\n")
|
||||||
|
var line string
|
||||||
|
if lineEnd == -1 {
|
||||||
|
line = s[i:]
|
||||||
|
i = len(s)
|
||||||
|
} else {
|
||||||
|
line = s[i : i+lineEnd]
|
||||||
|
i = i + lineEnd + 1
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(strings.TrimLeft(line, " "), "```") {
|
||||||
|
if !inBlock {
|
||||||
|
lastOpenIdx = lineStart
|
||||||
|
inBlock = true
|
||||||
|
} else {
|
||||||
|
inBlock = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if inBlock && lastOpenIdx >= 0 {
|
||||||
|
return strings.TrimRight(s[:lastOpenIdx], " \t\n")
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
var _ notify_service.Notifier = &actionNotifier{}
|
var _ notify_service.Notifier = &actionNotifier{}
|
||||||
|
|
||||||
func Init() error {
|
func Init() error {
|
||||||
@ -117,6 +152,7 @@ func (a *actionNotifier) CreateIssueComment(ctx context.Context, doer *user_mode
|
|||||||
truncatedContent = truncatedContent[:lastSpaceIdx] + "…"
|
truncatedContent = truncatedContent[:lastSpaceIdx] + "…"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
truncatedContent = trimUnclosedCodeBlock(truncatedContent)
|
||||||
act.Content = fmt.Sprintf("%d|%s", issue.Index, truncatedContent)
|
act.Content = fmt.Sprintf("%d|%s", issue.Index, truncatedContent)
|
||||||
|
|
||||||
if issue.IsPull {
|
if issue.IsPull {
|
||||||
|
|||||||
@ -22,6 +22,55 @@ func TestMain(m *testing.M) {
|
|||||||
unittest.MainTest(m)
|
unittest.MainTest(m)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestTrimUnclosedCodeBlock(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
input string
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "no code block",
|
||||||
|
input: "hello world",
|
||||||
|
expected: "hello world",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "closed code block",
|
||||||
|
input: "before\n```go\nfmt.Println()\n```\nafter",
|
||||||
|
expected: "before\n```go\nfmt.Println()\n```\nafter",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "unclosed code block",
|
||||||
|
input: "before\n```mermaid\ngraph LR\nA --> B",
|
||||||
|
expected: "before",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "unclosed code block with leading text",
|
||||||
|
input: "some text here\n```\ncode line 1\ncode line 2",
|
||||||
|
expected: "some text here",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "closed then unclosed",
|
||||||
|
input: "```\nblock1\n```\ntext\n```\nunclosed",
|
||||||
|
expected: "```\nblock1\n```\ntext",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "only unclosed fence",
|
||||||
|
input: "```mermaid\ngraph LR",
|
||||||
|
expected: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty string",
|
||||||
|
input: "",
|
||||||
|
expected: "",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
assert.Equal(t, tt.expected, trimUnclosedCodeBlock(tt.input))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestRenameRepoAction(t *testing.T) {
|
func TestRenameRepoAction(t *testing.T) {
|
||||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||||
|
|
||||||
|
|||||||
@ -108,7 +108,7 @@
|
|||||||
<span class="text truncate issue title">{{index .GetIssueInfos 1 | ctx.RenderUtils.RenderIssueSimpleTitle}}</span>
|
<span class="text truncate issue title">{{index .GetIssueInfos 1 | ctx.RenderUtils.RenderIssueSimpleTitle}}</span>
|
||||||
{{else if .GetOpType.InActions "comment_issue" "approve_pull_request" "reject_pull_request" "comment_pull"}}
|
{{else if .GetOpType.InActions "comment_issue" "approve_pull_request" "reject_pull_request" "comment_pull"}}
|
||||||
<a href="{{.GetCommentLink ctx}}" class="text truncate issue title">{{(.GetIssueTitle ctx) | ctx.RenderUtils.RenderIssueSimpleTitle}}</a>
|
<a href="{{.GetCommentLink ctx}}" class="text truncate issue title">{{(.GetIssueTitle ctx) | ctx.RenderUtils.RenderIssueSimpleTitle}}</a>
|
||||||
{{$comment := index .GetIssueInfos 1}}
|
{{$comment := .GetIssueContentBody}}
|
||||||
{{if $comment}}
|
{{if $comment}}
|
||||||
<div class="render-content markup tw-text-14">{{ctx.RenderUtils.MarkdownToHtml $comment}}</div>
|
<div class="render-content markup tw-text-14">{{ctx.RenderUtils.MarkdownToHtml $comment}}</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user