Skip to content

Commit 90c6440

Browse files
committed
placeholder for html entity
1 parent 3c2aa66 commit 90c6440

File tree

4 files changed

+313
-246
lines changed

4 files changed

+313
-246
lines changed

services/gitdiff/gitdiff.go

Lines changed: 0 additions & 181 deletions
Original file line numberDiff line numberDiff line change
@@ -283,187 +283,6 @@ func DiffInlineWithHighlightCode(fileName, language, code string) DiffInline {
283283
return DiffInline{EscapeStatus: status, Content: template.HTML(content)}
284284
}
285285

286-
// highlightCodeDiff is used to do diff with highlighted HTML code.
287-
// It totally depends on Chroma's valid HTML output and its structure, do not use these functions for other purposes.
288-
// The HTML tags will be replaced by Unicode placeholders: "<span>{TEXT}</span>" => "\uE000{TEXT}\uE001"
289-
// These Unicode placeholders are friendly to the diff.
290-
// Then after diff, the placeholders in diff result will be recovered to the HTML tags.
291-
// It's guaranteed that the tags in final diff result are paired correctly.
292-
type highlightCodeDiff struct {
293-
placeholderBegin rune
294-
placeholderMaxCount int
295-
placeholderIndex int
296-
placeholderTagMap map[rune]string
297-
tagPlaceholderMap map[string]rune
298-
299-
placeholderOverflowCount int
300-
301-
lineWrapperTags []string
302-
}
303-
304-
func newHighlightCodeDiff() *highlightCodeDiff {
305-
return &highlightCodeDiff{
306-
placeholderBegin: rune(0xE000), // Private Use Unicode: U+E000..U+F8FF, BMP(0), 6400
307-
placeholderMaxCount: 6400,
308-
placeholderTagMap: map[rune]string{},
309-
tagPlaceholderMap: map[string]rune{},
310-
}
311-
}
312-
313-
// nextPlaceholder returns 0 if no more placeholder can be used
314-
// the diff is done line by line, usually there are only a few (no more than 10) placeholders in one line
315-
// so the placeholderMaxCount is impossible to be exhausted in real cases.
316-
func (hcd *highlightCodeDiff) nextPlaceholder() rune {
317-
for hcd.placeholderIndex < hcd.placeholderMaxCount {
318-
r := hcd.placeholderBegin + rune(hcd.placeholderIndex)
319-
hcd.placeholderIndex++
320-
// only use non-existing (not used by code) rune as placeholders
321-
if _, ok := hcd.placeholderTagMap[r]; !ok {
322-
return r
323-
}
324-
}
325-
return 0 // no more available placeholder
326-
}
327-
328-
func (hcd *highlightCodeDiff) isInPlaceholderRange(r rune) bool {
329-
return hcd.placeholderBegin <= r && r < hcd.placeholderBegin+rune(hcd.placeholderMaxCount)
330-
}
331-
332-
func (hcd *highlightCodeDiff) collectUsedRunes(code string) {
333-
for _, r := range code {
334-
if hcd.isInPlaceholderRange(r) {
335-
// put the existing rune (used by code) in map, then this rune won't be used a placeholder anymore.
336-
hcd.placeholderTagMap[r] = ""
337-
}
338-
}
339-
}
340-
341-
func (hcd *highlightCodeDiff) diffWithHighlight(filename, language, codeA, codeB string) []diffmatchpatch.Diff {
342-
hcd.collectUsedRunes(codeA)
343-
hcd.collectUsedRunes(codeB)
344-
345-
highlightCodeA := highlight.Code(filename, language, codeA)
346-
highlightCodeB := highlight.Code(filename, language, codeB)
347-
348-
highlightCodeA = hcd.convertToPlaceholders(highlightCodeA)
349-
highlightCodeB = hcd.convertToPlaceholders(highlightCodeB)
350-
351-
diffs := diffMatchPatch.DiffMain(highlightCodeA, highlightCodeB, true)
352-
diffs = diffMatchPatch.DiffCleanupEfficiency(diffs)
353-
354-
for i := range diffs {
355-
hcd.recoverOneDiff(&diffs[i])
356-
}
357-
return diffs
358-
}
359-
360-
func (hcd *highlightCodeDiff) convertToPlaceholders(htmlCode string) string {
361-
var tagStack []string
362-
res := strings.Builder{}
363-
364-
firstRunForLineTags := hcd.lineWrapperTags == nil
365-
366-
// the standard chroma highlight HTML is "<span class="line [hl]"><span class="cl"> ... </span></span>"
367-
for {
368-
// find the next HTML tag
369-
pos1 := strings.IndexByte(htmlCode, '<')
370-
pos2 := strings.IndexByte(htmlCode, '>')
371-
if pos1 == -1 || pos2 == -1 || pos2 < pos1 {
372-
break
373-
}
374-
tag := htmlCode[pos1 : pos2+1]
375-
376-
// write the content before the tag into result string, and consume the tag in the string
377-
res.WriteString(htmlCode[:pos1])
378-
htmlCode = htmlCode[pos2+1:]
379-
380-
// the line wrapper tags should be removed before diff
381-
if strings.HasPrefix(tag, `<span class="line`) || strings.HasPrefix(tag, `<span class="cl"`) {
382-
if firstRunForLineTags {
383-
// if this is the first run for converting, save the line wrapper tags for later use, they should be added back
384-
hcd.lineWrapperTags = append(hcd.lineWrapperTags, tag)
385-
}
386-
htmlCode = strings.TrimSuffix(htmlCode, "</span>")
387-
continue
388-
}
389-
390-
var tagInMap string
391-
if tag[1] == '/' { // for closed tag
392-
if len(tagStack) == 0 {
393-
break // invalid diff result, no open tag but see close tag
394-
}
395-
// make sure the closed tag in map is related to the open tag, to make the diff algorithm can match the open/closed tags
396-
// the closed tag will be recorded in the map by key "</span><!-- <span the-open> -->" for "<span the-open>"
397-
tagInMap = tag + "<!-- " + tagStack[len(tagStack)-1] + "-->"
398-
tagStack = tagStack[:len(tagStack)-1]
399-
} else { // for open tag
400-
tagInMap = tag
401-
tagStack = append(tagStack, tag)
402-
}
403-
404-
// remember the placeholder and tag in the map
405-
placeholder, ok := hcd.tagPlaceholderMap[tagInMap]
406-
if !ok {
407-
placeholder = hcd.nextPlaceholder()
408-
if placeholder != 0 {
409-
hcd.tagPlaceholderMap[tagInMap] = placeholder
410-
hcd.placeholderTagMap[placeholder] = tagInMap
411-
}
412-
}
413-
414-
if placeholder != 0 {
415-
res.WriteRune(placeholder) // use the placeholder to replace the tag
416-
} else {
417-
// unfortunately, all private use runes has been exhausted, no more placeholder could be used, no more converting
418-
// usually, the exhausting won't occur in real cases, the magnitude of used placeholders is not larger than that of the CSS classes outputted by chroma.
419-
hcd.placeholderOverflowCount++
420-
}
421-
}
422-
// write the remaining string
423-
res.WriteString(htmlCode)
424-
return res.String()
425-
}
426-
427-
func (hcd *highlightCodeDiff) recoverOneDiff(diff *diffmatchpatch.Diff) {
428-
sb := strings.Builder{}
429-
var tagStack []string
430-
431-
for _, r := range diff.Text {
432-
tag, ok := hcd.placeholderTagMap[r]
433-
if !ok || tag == "" {
434-
sb.WriteRune(r) // if the rune is not a placeholder, write it as it is
435-
continue
436-
}
437-
var tagToRecover string
438-
if tag[1] == '/' { // Closing tag
439-
// only get the tag itself, ignore the trailing comment (for how the comment is generated, see the code in `convert` function)
440-
tagToRecover = tag[:strings.IndexByte(tag, '>')+1]
441-
if len(tagStack) == 0 {
442-
continue // if no open tag in stack yet, skip the closed tag
443-
}
444-
tagStack = tagStack[:len(tagStack)-1]
445-
} else {
446-
tagToRecover = tag
447-
tagStack = append(tagStack, tag)
448-
}
449-
sb.WriteString(tagToRecover)
450-
}
451-
452-
if len(tagStack) > 0 {
453-
// close all open tags
454-
for i := len(tagStack) - 1; i >= 0; i-- {
455-
tagToClose := tagStack[i]
456-
// get the closed tag "</span>" from "<span class=...>" or "<span>"
457-
pos := strings.IndexAny(tagToClose, " >")
458-
if pos != -1 {
459-
sb.WriteString("</" + tagToClose[1:pos] + ">")
460-
} // else: impossible. every tag was pushed into the stack by the code above and is valid HTML open tag
461-
}
462-
}
463-
464-
diff.Text = sb.String()
465-
}
466-
467286
// GetComputedInlineDiffFor computes inline diff for the given line.
468287
func (diffSection *DiffSection) GetComputedInlineDiffFor(diffLine *DiffLine) DiffInline {
469288
if setting.Git.DisableDiffHighlight {

services/gitdiff/gitdiff_test.go

Lines changed: 0 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -644,71 +644,6 @@ func TestGetDiffRangeWithWhitespaceBehavior(t *testing.T) {
644644
}
645645
}
646646

647-
func TestDiffWithHighlight(t *testing.T) {
648-
hcd := newHighlightCodeDiff()
649-
diffs := hcd.diffWithHighlight(
650-
"main.v", "",
651-
" run('<>')\n",
652-
" run(db)\n",
653-
)
654-
655-
expected := ` <span class="n">run</span><span class="o">(</span><span class="removed-code"><span class="k">&#39;</span><span class="o">&lt;</span><span class="o">&gt;</span><span class="k">&#39;</span></span><span class="o">)</span>` + "\n"
656-
output := diffToHTML(nil, diffs, DiffLineDel)
657-
assert.Equal(t, expected, output)
658-
659-
expected = ` <span class="n">run</span><span class="o">(</span><span class="added-code"><span class="n">db</span></span><span class="o">)</span>` + "\n"
660-
output = diffToHTML(nil, diffs, DiffLineAdd)
661-
assert.Equal(t, expected, output)
662-
}
663-
664-
func TestDiffWithHighlightPlaceholder(t *testing.T) {
665-
hcd := newHighlightCodeDiff()
666-
diffs := hcd.diffWithHighlight(
667-
"main.js", "",
668-
"a='\uE000'",
669-
"a='\uF8FF'",
670-
)
671-
assert.Equal(t, "", hcd.placeholderTagMap[0xE000])
672-
assert.Equal(t, "", hcd.placeholderTagMap[0xF8FF])
673-
674-
expected := fmt.Sprintf(`<span class="line"><span class="cl"><span class="nx">a</span><span class="o">=</span><span class="s1">&#39;</span><span class="removed-code">%s</span>&#39;</span></span>`, "\uE000")
675-
output := diffToHTML(hcd.lineWrapperTags, diffs, DiffLineDel)
676-
assert.Equal(t, expected, output)
677-
678-
hcd = newHighlightCodeDiff()
679-
diffs = hcd.diffWithHighlight(
680-
"main.js", "",
681-
"a='\uE000'",
682-
"a='\uF8FF'",
683-
)
684-
expected = fmt.Sprintf(`<span class="nx">a</span><span class="o">=</span><span class="s1">&#39;</span><span class="added-code">%s</span>&#39;`, "\uF8FF")
685-
output = diffToHTML(nil, diffs, DiffLineAdd)
686-
assert.Equal(t, expected, output)
687-
688-
totalOverflow := 0
689-
for i := 0; i < 100; i++ {
690-
hcd = newHighlightCodeDiff()
691-
hcd.placeholderMaxCount = i
692-
diffs = hcd.diffWithHighlight(
693-
"main.js", "",
694-
"a='1'",
695-
"b='2'",
696-
)
697-
totalOverflow += hcd.placeholderOverflowCount
698-
699-
output = diffToHTML(nil, diffs, DiffLineDel)
700-
c1 := strings.Count(output, "<span")
701-
c2 := strings.Count(output, "</span")
702-
assert.Equal(t, c1, c2)
703-
704-
output = diffToHTML(nil, diffs, DiffLineAdd)
705-
c1 = strings.Count(output, "<span")
706-
c2 = strings.Count(output, "</span")
707-
assert.Equal(t, c1, c2)
708-
}
709-
assert.NotZero(t, totalOverflow)
710-
}
711-
712647
func TestNoCrashes(t *testing.T) {
713648
type testcase struct {
714649
gitdiff string

0 commit comments

Comments
 (0)