@@ -192,8 +192,9 @@ var (
192
192
193
193
func diffToHTML (lineWrapperTags []string , diffs []diffmatchpatch.Diff , lineType DiffLineType ) string {
194
194
buf := bytes .NewBuffer (nil )
195
+ // restore the line wrapper tags <span class="line"> and <span class="cl">, if necessary
195
196
for _ , tag := range lineWrapperTags {
196
- buf .WriteString (tag ) // restore the line wrapper tags <span class="line"> and <span class="cl">
197
+ buf .WriteString (tag )
197
198
}
198
199
for _ , diff := range diffs {
199
200
switch {
@@ -282,6 +283,11 @@ func DiffInlineWithHighlightCode(fileName, language, code string) DiffInline {
282
283
return DiffInline {EscapeStatus : status , Content : template .HTML (content )}
283
284
}
284
285
286
+ // HighlightCodeDiff is used to do diff with highlighted HTML code.
287
+ // The HTML tags will be replaced by Unicode placeholders: "<span>{TEXT}</span>" => "\uE000{TEXT}\uE001"
288
+ // These Unicode placeholders are friendly to the diff.
289
+ // Then after diff, the placeholders in diff result will be recovered to the HTML tags.
290
+ // It's guaranteed that the tags in final diff result are paired correctly.
285
291
type HighlightCodeDiff struct {
286
292
placeholderBegin rune
287
293
placeholderMaxCount int
@@ -346,34 +352,33 @@ func (hcd *HighlightCodeDiff) diffWithHighlight(filename, language, codeA, codeB
346
352
return diffs
347
353
}
348
354
349
- func (hcd * HighlightCodeDiff ) convertToPlaceholders (highlightCode string ) string {
355
+ func (hcd * HighlightCodeDiff ) convertToPlaceholders (htmlCode string ) string {
350
356
var tagStack []string
351
357
res := strings.Builder {}
352
- s := highlightCode
353
358
354
359
firstRunForLineTags := hcd .lineWrapperTags == nil
355
360
356
361
// the standard chroma highlight HTML is "<span class="line [hl]"><span class="cl"> ... </span></span>"
357
362
for {
358
363
// find the next HTML tag
359
- pos1 := strings .IndexByte (s , '<' )
360
- pos2 := strings .IndexByte (s , '>' )
364
+ pos1 := strings .IndexByte (htmlCode , '<' )
365
+ pos2 := strings .IndexByte (htmlCode , '>' )
361
366
if pos1 == - 1 || pos2 == - 1 || pos2 < pos1 {
362
367
break
363
368
}
364
- tag := s [pos1 : pos2 + 1 ]
369
+ tag := htmlCode [pos1 : pos2 + 1 ]
365
370
366
371
// write the content before the tag into result string, and consume the tag in the string
367
- res .WriteString (s [:pos1 ])
368
- s = s [pos2 + 1 :]
372
+ res .WriteString (htmlCode [:pos1 ])
373
+ htmlCode = htmlCode [pos2 + 1 :]
369
374
370
375
// the line wrapper tags should be removed before diff
371
376
if strings .HasPrefix (tag , `<span class="line` ) || strings .HasPrefix (tag , `<span class="cl"` ) {
372
377
if firstRunForLineTags {
373
378
// if this is the first run for converting, save the line wrapper tags for later use, they should be added back
374
379
hcd .lineWrapperTags = append (hcd .lineWrapperTags , tag )
375
380
}
376
- s = strings .TrimSuffix (s , "</span>" )
381
+ htmlCode = strings .TrimSuffix (htmlCode , "</span>" )
377
382
continue
378
383
}
379
384
@@ -404,11 +409,11 @@ func (hcd *HighlightCodeDiff) convertToPlaceholders(highlightCode string) string
404
409
if placeholder != 0 {
405
410
res .WriteRune (placeholder ) // use the placeholder to replace the tag
406
411
} else {
407
- res .WriteString (tag ) // unfortunately, all private use runes has been exhausted, no more placeholder could be used, so do not covert the tag
412
+ res .WriteString (tag ) // unfortunately, all private use runes has been exhausted, no more placeholder could be used, so do not convert the tag
408
413
}
409
414
}
410
-
411
- res .WriteString (s )
415
+ // write the remaining string
416
+ res .WriteString (htmlCode )
412
417
return res .String ()
413
418
}
414
419
@@ -419,14 +424,15 @@ func (hcd *HighlightCodeDiff) recoverOneDiff(diff *diffmatchpatch.Diff) {
419
424
for _ , r := range diff .Text {
420
425
tag , ok := hcd .placeholderTagMap [r ]
421
426
if ! ok || tag == "" {
422
- sb .WriteRune (r )
427
+ sb .WriteRune (r ) // if the run is not a placeholder, write it as it is
423
428
continue
424
429
}
425
430
var tagToRecover string
426
431
if tag [1 ] == '/' {
432
+ // only get the tag itself, ignore the trailing comment (for how the comment is generated, see the code in `convert` function)
427
433
tagToRecover = tag [:strings .IndexByte (tag , '>' )+ 1 ]
428
434
if len (tagStack ) == 0 {
429
- continue // if no open tag yet, skip the closed tag
435
+ continue // if no open tag in stack yet, skip the closed tag
430
436
}
431
437
tagStack = tagStack [:len (tagStack )- 1 ]
432
438
} else {
@@ -440,13 +446,11 @@ func (hcd *HighlightCodeDiff) recoverOneDiff(diff *diffmatchpatch.Diff) {
440
446
// close all open tags
441
447
for i := len (tagStack ) - 1 ; i >= 0 ; i -- {
442
448
tagToClose := tagStack [i ]
443
- pos := strings .IndexByte (tagToClose , ' ' )
444
- if pos == - 1 {
445
- pos = strings .IndexByte (tagToClose , '>' )
446
- }
449
+ // get the closed tag "</span>" from "<span class=...>" or "<span>"
450
+ pos := strings .IndexAny (tagToClose , " >" )
447
451
if pos != - 1 {
448
452
sb .WriteString ("</" + tagToClose [1 :pos ] + ">" )
449
- }
453
+ } // else: impossible. every tag was pushed into the stack by the code above and is valid HTML open tag
450
454
}
451
455
}
452
456
0 commit comments