focalboard/server/services/notify/notifysubscriptions/diff2markdown.go
Doug Lauder 605c0079eb
Multi product architecture (#3309)
* skeleton lifecycle

* bare minimum to satisfy mm-server import

* added boards_imports.go

* move boards_imports.go to correct package

* bump mmserver version; remove replace in go.mod; use module workspaces; remove logger service

* rename product.go --> boards.go

* add FileInfoStore and Cloud services for product; create minimal pluginAPI interfaces for all packages

* rename Boards -> BoardsProduct

* compile success

* remove hooks service; guard for nil BoardsApp

* update to latest mmserver ver

* upgrade mmserver to master tip

* upgrade mmserver to master tip

* bump plugin-api to master tip

* fix users service

* fix OnActivate crash; normalize AppError returns

* fileBackend interface for server/app

* feature flag

* bump mmserver version

* fix linter errors

* make go.work when linting

* fix go.work creation for CI

* add execute flag for script

* fix more linter errors

* always create a go.work

* fix ci go.work

* OS agnostic go.work generator

* fix path

* fix path again

* partially disable cypress test

* fix case Id --> ID

* bump mmserver version

* include  in go.work for dev

* addressed review comments.

Co-authored-by: Mattermod <mattermod@users.noreply.github.com>
2022-07-15 07:51:50 +02:00

181 lines
4.1 KiB
Go

package notifysubscriptions
import (
"strings"
"github.com/sergi/go-diff/diffmatchpatch"
"github.com/mattermost/mattermost-server/v6/shared/mlog"
)
func generateMarkdownDiff(oldText string, newText string, logger mlog.LoggerIFace) string {
oldTxtNorm := normalizeText(oldText)
newTxtNorm := normalizeText(newText)
dmp := diffmatchpatch.New()
diffs := dmp.DiffMain(oldTxtNorm, newTxtNorm, false)
diffs = dmp.DiffCleanupSemantic(diffs)
diffs = dmp.DiffCleanupEfficiency(diffs)
// check there is at least one insert or delete
var editFound bool
for _, d := range diffs {
if (d.Type == diffmatchpatch.DiffInsert || d.Type == diffmatchpatch.DiffDelete) && strings.TrimSpace(d.Text) != "" {
editFound = true
break
}
}
if !editFound {
logger.Debug("skipping notification for superficial diff")
return ""
}
cfg := markDownCfg{
insertOpen: "`",
insertClose: "`",
deleteOpen: "~~`",
deleteClose: "`~~",
}
markdown := generateMarkdown(diffs, cfg)
markdown = strings.ReplaceAll(markdown, "¶", "\n")
return markdown
}
const (
truncLenEquals = 60
truncLenInserts = 120
truncLenDeletes = 80
)
type markDownCfg struct {
insertOpen string
insertClose string
deleteOpen string
deleteClose string
}
func generateMarkdown(diffs []diffmatchpatch.Diff, cfg markDownCfg) string {
sb := &strings.Builder{}
var first, last bool
for i, diff := range diffs {
first = i == 0
last = i == len(diffs)-1
switch diff.Type {
case diffmatchpatch.DiffInsert:
sb.WriteString(cfg.insertOpen)
sb.WriteString(truncate(diff.Text, truncLenInserts, first, last))
sb.WriteString(cfg.insertClose)
case diffmatchpatch.DiffDelete:
sb.WriteString(cfg.deleteOpen)
sb.WriteString(truncate(diff.Text, truncLenDeletes, first, last))
sb.WriteString(cfg.deleteClose)
case diffmatchpatch.DiffEqual:
sb.WriteString(truncate(diff.Text, truncLenEquals, first, last))
}
}
return sb.String()
}
func truncate(s string, maxLen int, first bool, last bool) string {
if len(s) < maxLen {
return s
}
var result string
switch {
case first:
// truncate left
result = " ... " + rightWords(s, maxLen)
case last:
// truncate right
result = leftWords(s, maxLen) + " ... "
default:
// truncate in the middle
half := len(s) / 2
left := leftWords(s[:half], maxLen/2)
right := rightWords(s[half:], maxLen/2)
result = left + " ... " + right
}
return strings.ReplaceAll(result, "¶", "↩")
}
func normalizeText(s string) string {
s = strings.ReplaceAll(s, "\t", " ")
s = strings.ReplaceAll(s, " ", " ")
s = strings.ReplaceAll(s, "\n\n", "\n")
s = strings.ReplaceAll(s, "\n", "¶")
return s
}
// leftWords returns approximately maxLen characters from the left part of the source string by truncating on the right,
// with best effort to include whole words.
func leftWords(s string, maxLen int) string {
if len(s) < maxLen {
return s
}
fields := strings.Fields(s)
fields = words(fields, maxLen)
return strings.Join(fields, " ")
}
// rightWords returns approximately maxLen from the right part of the source string by truncating from the left,
// with best effort to include whole words.
func rightWords(s string, maxLen int) string {
if len(s) < maxLen {
return s
}
fields := strings.Fields(s)
// reverse the fields so that the right-most words end up at the beginning.
reverse(fields)
fields = words(fields, maxLen)
// reverse the fields again so that the original order is restored.
reverse(fields)
return strings.Join(fields, " ")
}
func reverse(ss []string) {
ssLen := len(ss)
for i := 0; i < ssLen/2; i++ {
ss[i], ss[ssLen-i-1] = ss[ssLen-i-1], ss[i]
}
}
// words returns a subslice containing approximately maxChars of characters. The last item may be truncated.
func words(words []string, maxChars int) []string {
var count int
result := make([]string, 0, len(words))
for i, w := range words {
wordLen := len(w)
if wordLen+count > maxChars {
switch {
case i == 0:
result = append(result, w[:maxChars])
case wordLen < 8:
result = append(result, w)
}
return result
}
count += wordLen
result = append(result, w)
}
return result
}