Skip to content

Commit

Permalink
[MM-1096]: Fixed improper markdown for Jira comments received as a su…
Browse files Browse the repository at this point in the history
…bscription in mattermost (#1115)

* [MM-1096]: added additional handling for comment markdown from jira

* [MM-1096]: review fixes

* [MM-1096]: added preProcessing for issueDescription notification

* [MM-1096]: review fixes

* Update server/webhook_parser.go

Co-authored-by: Raghav Aggarwal <raghav.aggarwal@brightscout.com>

* [MM-1096]: review fixes

* [MM-1096]: Added testcase for preProcessText function in webhook_parser.go file

* [MM-1096]: Fixed the markdown for quotes

* [MM-1096]: added testcase for quoted text

* [MM-1096]: Added handling non-language specific code blocks

* [MM-1096]: Fixed lint

---------

Co-authored-by: Raghav Aggarwal <raghav.aggarwal@brightscout.com>
  • Loading branch information
Kshitij-Katiyar and raghavaggarwal2308 authored Sep 10, 2024
1 parent 3f7e52b commit 9ad45c0
Show file tree
Hide file tree
Showing 2 changed files with 170 additions and 3 deletions.
96 changes: 93 additions & 3 deletions server/webhook_parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import (
"encoding/json"
"fmt"
"os"
"regexp"
"strconv"
"strings"
"time"

Expand Down Expand Up @@ -245,7 +247,7 @@ func parseWebhookCommentCreated(jwh *JiraWebhook) (Webhook, error) {
JiraWebhook: jwh,
eventTypes: NewStringSet(eventCreatedComment),
headline: fmt.Sprintf("%s **commented** on %s", commentAuthor, jwh.mdKeySummaryLink()),
text: truncate(quoteIssueComment(jwh.Comment.Body), 3000),
text: truncate(quoteIssueComment(preProcessText(jwh.Comment.Body)), 3000),
}

appendCommentNotifications(wh, "**mentioned** you in a new comment on")
Expand Down Expand Up @@ -316,6 +318,94 @@ func quoteIssueComment(comment string) string {
return "> " + strings.ReplaceAll(comment, "\n", "\n> ")
}

// preProcessText processes the given string to apply various formatting transformations.
// The purpose of the function is to convert the formatting provided by JIRA into the corresponding formatting supported by Mattermost.
// This includes converting asterisks to bold, hyphens to strikethrough, JIRA-style headings to Markdown headings,
// JIRA code blocks to inline code, numbered lists to Markdown lists, colored text to plain text, and JIRA links to Markdown links.
// For more reference, please visit https://github.com/mattermost/mattermost-plugin-jira/issues/1096
func preProcessText(jiraMarkdownString string) string {
asteriskRegex := regexp.MustCompile(`\*(\w+)\*`)
hyphenRegex := regexp.MustCompile(`-(\w+)-`)
headingRegex := regexp.MustCompile(`(?m)^(h[1-6]\.)\s+`)
langSpecificCodeBlockRegex := regexp.MustCompile(`\{code:[^}]+\}(.+?)\{code\}`)
numberedListRegex := regexp.MustCompile(`^#\s+`)
colouredTextRegex := regexp.MustCompile(`\{color:[^}]+\}(.*?)\{color\}`)
linkRegex := regexp.MustCompile(`\[(.*?)\|([^|\]]+)(?:\|([^|\]]+))?\]`)
quoteRegex := regexp.MustCompile(`\{quote\}(.*?)\{quote\}`)
codeBlockRegex := regexp.MustCompile(`\{\{(.+?)\}\}`)

// the below code converts lines starting with "#" into a numbered list. It increments the counter if consecutive lines are numbered,
// otherwise resets it to 1. The "#" is replaced with the corresponding number and period. Non-numbered lines are added unchanged.
var counter int
var lastLineWasNumberedList bool
var result []string
lines := strings.Split(jiraMarkdownString, "\n")
for _, line := range lines {
if numberedListRegex.MatchString(line) {
if !lastLineWasNumberedList {
counter = 1
} else {
counter++
}
line = strconv.Itoa(counter) + ". " + strings.TrimPrefix(line, "# ")
lastLineWasNumberedList = true
} else {
lastLineWasNumberedList = false
}
result = append(result, line)
}
processedString := strings.Join(result, "\n")

// the below code converts links in the format "[text|url]" or "[text|url|optional]" to Markdown links. If the text is empty,
// the URL is used for both the text and link. If the optional part is present, it's ignored. Unrecognized patterns remain unchanged.
processedString = linkRegex.ReplaceAllStringFunc(processedString, func(link string) string {
parts := linkRegex.FindStringSubmatch(link)
if len(parts) == 4 {
if parts[1] == "" {
return "[" + parts[2] + "](" + parts[2] + ")"
}
if parts[3] != "" {
return "[" + parts[1] + "](" + parts[2] + ")"
}
return "[" + parts[1] + "](" + parts[2] + ")"
}
return link
})

processedString = asteriskRegex.ReplaceAllStringFunc(processedString, func(word string) string {
return "**" + strings.Trim(word, "*") + "**"
})

processedString = hyphenRegex.ReplaceAllStringFunc(processedString, func(word string) string {
return "~~" + strings.Trim(word, "-") + "~~"
})

processedString = headingRegex.ReplaceAllStringFunc(processedString, func(heading string) string {
level := heading[1]
hashes := strings.Repeat("#", int(level-'0'))
return hashes + " "
})

processedString = langSpecificCodeBlockRegex.ReplaceAllStringFunc(processedString, func(codeBlock string) string {
codeContent := codeBlock[strings.Index(codeBlock, "}")+1 : strings.LastIndex(codeBlock, "{code}")]
return "`" + codeContent + "`"
})

processedString = codeBlockRegex.ReplaceAllStringFunc(processedString, func(match string) string {
curlyContent := codeBlockRegex.FindStringSubmatch(match)[1]
return "`" + curlyContent + "`"
})

processedString = colouredTextRegex.ReplaceAllString(processedString, "$1")

processedString = quoteRegex.ReplaceAllStringFunc(processedString, func(quote string) string {
quotedText := quote[strings.Index(quote, "}")+1 : strings.LastIndex(quote, "{quote}")]
return "> " + quotedText
})

return processedString
}

func parseWebhookCommentDeleted(jwh *JiraWebhook) (Webhook, error) {
if jwh.Issue.ID == "" {
return nil, ErrWebhookIgnored
Expand Down Expand Up @@ -348,7 +438,7 @@ func parseWebhookCommentUpdated(jwh *JiraWebhook) (Webhook, error) {
JiraWebhook: jwh,
eventTypes: NewStringSet(eventUpdatedComment),
headline: fmt.Sprintf("%s **edited comment** in %s", mdUser(&jwh.Comment.UpdateAuthor), jwh.mdKeySummaryLink()),
text: truncate(quoteIssueComment(jwh.Comment.Body), 3000),
text: truncate(quoteIssueComment(preProcessText(jwh.Comment.Body)), 3000),
}

return wh, nil
Expand Down Expand Up @@ -414,7 +504,7 @@ func parseWebhookUpdatedDescription(jwh *JiraWebhook, from, to string) *webhook
fromFmttd := "\n**From:** " + truncate(from, 500)
toFmttd := "\n**To:** " + truncate(to, 500)
wh.fieldInfo = webhookField{descriptionField, descriptionField, fromFmttd, toFmttd}
wh.text = jwh.mdIssueDescription()
wh.text = preProcessText(jwh.mdIssueDescription())
return wh
}

Expand Down
77 changes: 77 additions & 0 deletions server/webhook_parser_misc_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -136,3 +136,80 @@ func TestWebhookQuotedComment(t *testing.T) {
assert.True(t, strings.HasPrefix(w.text, ">"))
}
}

func TestPreProcessText(t *testing.T) {
tests := map[string]struct {
input string
expectedOutput string
}{
"BOLD formatting": {
input: "*BOLD*",
expectedOutput: "**BOLD**",
},
"STRIKETHROUGH formatting": {
input: "-STRIKETHROUGH-",
expectedOutput: "~~STRIKETHROUGH~~",
},
"Colored text formatting": {
input: "{color:#ff5630}RED{color} {color:#4c9aff}BLUE{color} {color:#36b37e}GREEN{color}",
expectedOutput: "RED BLUE GREEN",
},
"Numbered list with mixed content formatting": {
input: `# NUMBERED LIST ROW 1
# NUMBERED LIST ROW 2
non-numbered list text
# NUMBERED LIST ROW 1`,
expectedOutput: `1. NUMBERED LIST ROW 1
2. NUMBERED LIST ROW 2
non-numbered list text
1. NUMBERED LIST ROW 1`,
},
"Code block formatting": {
input: "{code:go}fruit := \"APPLE\"{code}",
expectedOutput: "`fruit := \"APPLE\"`",
},
"Bullet list formatting": {
input: `* BULLET LIST ROW 1
* BULLET LIST ROW 2`,
expectedOutput: `* BULLET LIST ROW 1
* BULLET LIST ROW 2`,
},
"Heading formatting": {
input: `h1. HEADING 1
h2. HEADING 2
h3. HEADING 3
h4. HEADING 4
h5. HEADING 5
h6. HEADING 6`,
expectedOutput: `# HEADING 1
## HEADING 2
### HEADING 3
#### HEADING 4
##### HEADING 5
###### HEADING 6`,
},
"Link formatting with text": {
input: "[www.googlesd.com|http://www.googlesd.com]",
expectedOutput: "[www.googlesd.com](http://www.googlesd.com)",
},
"Link formatting with smart-link": {
input: "[http://www.google.com|http://www.google.com|smart-link]",
expectedOutput: "[http://www.google.com](http://www.google.com)",
},
"Link formatting with title": {
input: "[google|http://www.google.com]",
expectedOutput: "[google](http://www.google.com)",
},
"Quote formatting": {
input: "{quote}This is a quote{quote}",
expectedOutput: "> This is a quote",
},
}

for name, tc := range tests {
t.Run(name, func(t *testing.T) {
actualOutput := preProcessText(tc.input)
assert.Equal(t, tc.expectedOutput, actualOutput)
})
}
}

0 comments on commit 9ad45c0

Please sign in to comment.