From 0171dda7288b73995f929c7627f7292ae81090f2 Mon Sep 17 00:00:00 2001
From: mrsdizzie <info@mrsdizzie.com>
Date: Sat, 8 Aug 2020 06:17:02 -0400
Subject: [PATCH] Improved diff syntax highlighting fix (#12455)

Make previous fix from #12238 more robust since I saw a case where a diff changes only a single character in a chroma class instead of the entire thing. Add another more complicated test to match.

Co-authored-by: Lauris BH <lauris@nix.lv>
---
 services/gitdiff/gitdiff.go      | 47 ++++++++++++++++++--------------
 services/gitdiff/gitdiff_test.go | 10 +++++++
 2 files changed, 37 insertions(+), 20 deletions(-)

diff --git a/services/gitdiff/gitdiff.go b/services/gitdiff/gitdiff.go
index 85354784d4..fca210bb10 100644
--- a/services/gitdiff/gitdiff.go
+++ b/services/gitdiff/gitdiff.go
@@ -16,6 +16,7 @@ import (
 	"net/url"
 	"os"
 	"os/exec"
+	"regexp"
 	"sort"
 	"strconv"
 	"strings"
@@ -180,55 +181,61 @@ var (
 	removedCodePrefix = []byte(`<span class="removed-code">`)
 	codeTagSuffix     = []byte(`</span>`)
 )
+var addSpanRegex = regexp.MustCompile(`<span class="[a-z]*$`)
 
 func diffToHTML(fileName string, diffs []diffmatchpatch.Diff, lineType DiffLineType) template.HTML {
 	buf := bytes.NewBuffer(nil)
-	var addSpan bool
+	var addSpan string
 	for i := range diffs {
 		switch {
 		case diffs[i].Type == diffmatchpatch.DiffEqual:
 			// Looking for the case where our 3rd party diff library previously detected a string difference
 			// in the middle of a span class because we highlight them first. This happens when added/deleted code
-			// also changes the chroma class name. If found, just move the openining span code forward into the next section
-			if addSpan {
-				diffs[i].Text = "<span class=\"" + diffs[i].Text
+			// also changes the chroma class name, either partially or fully. If found, just move the openining span code forward into the next section
+			// see TestDiffToHTML for examples
+			if len(addSpan) > 0 {
+				diffs[i].Text = addSpan + diffs[i].Text
+				addSpan = ""
 			}
-			if strings.HasSuffix(diffs[i].Text, "<span class=\"") {
-				addSpan = true
-				buf.WriteString(strings.TrimSuffix(diffs[i].Text, "<span class=\""))
+			m := addSpanRegex.FindStringSubmatchIndex(diffs[i].Text)
+			if m != nil {
+				addSpan = diffs[i].Text[m[0]:m[1]]
+				buf.WriteString(strings.TrimSuffix(diffs[i].Text, addSpan))
 			} else {
-				addSpan = false
+				addSpan = ""
 				buf.WriteString(getLineContent(diffs[i].Text))
 			}
 		case diffs[i].Type == diffmatchpatch.DiffInsert && lineType == DiffLineAdd:
-			if addSpan {
-				addSpan = false
-				diffs[i].Text = "<span class=\"" + diffs[i].Text
+			if len(addSpan) > 0 {
+				diffs[i].Text = addSpan + diffs[i].Text
+				addSpan = ""
 			}
 			// Print existing closing span first before opening added-code span so it doesn't unintentionally close it
 			if strings.HasPrefix(diffs[i].Text, "</span>") {
 				buf.WriteString("</span>")
 				diffs[i].Text = strings.TrimPrefix(diffs[i].Text, "</span>")
 			}
-			if strings.HasSuffix(diffs[i].Text, "<span class=\"") {
-				addSpan = true
-				diffs[i].Text = strings.TrimSuffix(diffs[i].Text, "<span class=\"")
+			m := addSpanRegex.FindStringSubmatchIndex(diffs[i].Text)
+			if m != nil {
+				addSpan = diffs[i].Text[m[0]:m[1]]
+				diffs[i].Text = strings.TrimSuffix(diffs[i].Text, addSpan)
 			}
 			buf.Write(addedCodePrefix)
 			buf.WriteString(getLineContent(diffs[i].Text))
 			buf.Write(codeTagSuffix)
 		case diffs[i].Type == diffmatchpatch.DiffDelete && lineType == DiffLineDel:
-			if addSpan {
-				addSpan = false
-				diffs[i].Text = "<span class=\"" + diffs[i].Text
+			if len(addSpan) > 0 {
+				diffs[i].Text = addSpan + diffs[i].Text
+				addSpan = ""
 			}
 			if strings.HasPrefix(diffs[i].Text, "</span>") {
 				buf.WriteString("</span>")
 				diffs[i].Text = strings.TrimPrefix(diffs[i].Text, "</span>")
 			}
-			if strings.HasSuffix(diffs[i].Text, "<span class=\"") {
-				addSpan = true
-				diffs[i].Text = strings.TrimSuffix(diffs[i].Text, "<span class=\"")
+			m := addSpanRegex.FindStringSubmatchIndex(diffs[i].Text)
+			if m != nil {
+				addSpan = diffs[i].Text[m[0]:m[1]]
+				diffs[i].Text = strings.TrimSuffix(diffs[i].Text, addSpan)
 			}
 			buf.Write(removedCodePrefix)
 			buf.WriteString(getLineContent(diffs[i].Text))
diff --git a/services/gitdiff/gitdiff_test.go b/services/gitdiff/gitdiff_test.go
index dabb49654e..b927abf07b 100644
--- a/services/gitdiff/gitdiff_test.go
+++ b/services/gitdiff/gitdiff_test.go
@@ -50,6 +50,16 @@ func TestDiffToHTML(t *testing.T) {
 		{Type: dmp.DiffInsert, Text: "</span> <span class=\"o\">||</span> <span class=\"nx\">r</span><span class=\"p\">.</span><span class=\"nx\">GuessLanguage</span><span class=\"p\">)"},
 		{Type: dmp.DiffEqual, Text: "</span> <span class=\"p\">{</span>"},
 	}, DiffLineAdd))
+
+	assertEqual(t, "<span class=\"nx\">tagURL</span> <span class=\"o\">:=</span> <span class=\"removed-code\"><span class=\"nx\">fmt</span><span class=\"p\">.</span><span class=\"nf\">Sprintf</span><span class=\"p\">(</span><span class=\"s\">&#34;## [%s](%s/%s/%s/%s?q=&amp;type=all&amp;state=closed&amp;milestone=%d) - %s&#34;</span><span class=\"p\">,</span> <span class=\"nx\">ge</span><span class=\"p\">.</span><span class=\"nx\">Milestone\"</span></span><span class=\"p\">,</span> <span class=\"nx\">ge</span><span class=\"p\">.</span><span class=\"nx\">BaseURL</span><span class=\"p\">,</span> <span class=\"nx\">ge</span><span class=\"p\">.</span><span class=\"nx\">Owner</span><span class=\"p\">,</span> <span class=\"nx\">ge</span><span class=\"p\">.</span><span class=\"nx\">Repo</span><span class=\"p\">,</span> <span class=\"nx\"><span class=\"removed-code\">from</span><span class=\"p\">,</span> <span class=\"nx\">milestoneID</span><span class=\"p\">,</span> <span class=\"nx\">time</span><span class=\"p\">.</span><span class=\"nf\">Now</span><span class=\"p\">(</span><span class=\"p\">)</span><span class=\"p\">.</span><span class=\"nf\">Format</span><span class=\"p\">(</span><span class=\"s\">&#34;2006-01-02&#34;</span><span class=\"p\">)</span></span><span class=\"p\">)</span>", diffToHTML("", []dmp.Diff{
+		{Type: dmp.DiffEqual, Text: "<span class=\"nx\">tagURL</span> <span class=\"o\">:=</span> <span class=\"n"},
+		{Type: dmp.DiffDelete, Text: "x\">fmt</span><span class=\"p\">.</span><span class=\"nf\">Sprintf</span><span class=\"p\">(</span><span class=\"s\">&#34;## [%s](%s/%s/%s/%s?q=&amp;type=all&amp;state=closed&amp;milestone=%d) - %s&#34;</span><span class=\"p\">,</span> <span class=\"nx\">ge</span><span class=\"p\">.</span><span class=\"nx\">Milestone\""},
+		{Type: dmp.DiffInsert, Text: "f\">getGiteaTagURL</span><span class=\"p\">(</span><span class=\"nx\">client"},
+		{Type: dmp.DiffEqual, Text: "</span><span class=\"p\">,</span> <span class=\"nx\">ge</span><span class=\"p\">.</span><span class=\"nx\">BaseURL</span><span class=\"p\">,</span> <span class=\"nx\">ge</span><span class=\"p\">.</span><span class=\"nx\">Owner</span><span class=\"p\">,</span> <span class=\"nx\">ge</span><span class=\"p\">.</span><span class=\"nx\">Repo</span><span class=\"p\">,</span> <span class=\"nx\">"},
+		{Type: dmp.DiffDelete, Text: "from</span><span class=\"p\">,</span> <span class=\"nx\">milestoneID</span><span class=\"p\">,</span> <span class=\"nx\">time</span><span class=\"p\">.</span><span class=\"nf\">Now</span><span class=\"p\">(</span><span class=\"p\">)</span><span class=\"p\">.</span><span class=\"nf\">Format</span><span class=\"p\">(</span><span class=\"s\">&#34;2006-01-02&#34;</span><span class=\"p\">)"},
+		{Type: dmp.DiffInsert, Text: "ge</span><span class=\"p\">.</span><span class=\"nx\">Milestone</span><span class=\"p\">,</span> <span class=\"nx\">from</span><span class=\"p\">,</span> <span class=\"nx\">milestoneID"},
+		{Type: dmp.DiffEqual, Text: "</span><span class=\"p\">)</span>"},
+	}, DiffLineDel))
 }
 
 func TestParsePatch(t *testing.T) {