Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions models/activities/action.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}

// 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.
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
Expand Down
18 changes: 18 additions & 0 deletions models/activities/action_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,24 @@ func TestConsistencyUpdateAction(t *testing.T) {
unittest.CheckConsistencyFor(t, &activities_model.Action{})
}

func TestGetIssueContentBody(t *testing.T) {
tests := []struct {
content string
expected string
}{
{content: "1|simple body", expected: "simple body"},
{content: "1|A -->|text| B", expected: "A -->|text| B"},
{content: "1|first|second|third", expected: "first|second|third"},
{content: "1|", expected: ""},
{content: "no-delimiter", expected: ""},
{content: "", expected: ""},
}
for _, test := range tests {
action := &activities_model.Action{Content: test.content}
assert.Equal(t, test.expected, action.GetIssueContentBody(), "content: %q", test.content)
}
}

func TestDeleteIssueActions(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())

Expand Down
38 changes: 38 additions & 0 deletions services/feed/notifier.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,41 @@ type actionNotifier struct {
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{}

func Init() error {
Expand Down Expand Up @@ -117,6 +152,9 @@ func (a *actionNotifier) CreateIssueComment(ctx context.Context, doer *user_mode
truncatedContent = truncatedContent[:lastSpaceIdx] + "…"
}
}
if truncatedRight != "" {
truncatedContent = trimUnclosedCodeBlock(truncatedContent)
}
act.Content = fmt.Sprintf("%d|%s", issue.Index, truncatedContent)

if issue.IsPull {
Expand Down
49 changes: 49 additions & 0 deletions services/feed/notifier_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,55 @@ func TestMain(m *testing.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) {
assert.NoError(t, unittest.PrepareTestDatabase())

Expand Down
2 changes: 1 addition & 1 deletion templates/user/dashboard/feeds.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@
<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"}}
<a href="{{.GetCommentLink ctx}}" class="text truncate issue title">{{(.GetIssueTitle ctx) | ctx.RenderUtils.RenderIssueSimpleTitle}}</a>
{{$comment := index .GetIssueInfos 1}}
{{$comment := .GetIssueContentBody}}
{{if $comment}}
<div class="render-content markup tw-text-14">{{ctx.RenderUtils.MarkdownToHtml $comment}}</div>
{{end}}
Expand Down