@@ -182,6 +182,8 @@ var (
182182 removedCodePrefix = []byte (`<span class="removed-code">` )
183183 codeTagSuffix = []byte (`</span>` )
184184)
185+
186+ var unfinishedtagRegex = regexp .MustCompile (`<[^>]*$` )
185187var trailingSpanRegex = regexp .MustCompile (`<span\s*[[:alpha:]="]*?[>]?$` )
186188var entityRegex = regexp .MustCompile (`&[#]*?[0-9[:alpha:]]*$` )
187189
@@ -196,10 +198,218 @@ func shouldWriteInline(diff diffmatchpatch.Diff, lineType DiffLineType) bool {
196198 return false
197199}
198200
201+ func fixupBrokenSpans (diffs []diffmatchpatch.Diff ) []diffmatchpatch.Diff {
202+
203+ // Create a new array to store our fixed up blocks
204+ fixedup := make ([]diffmatchpatch.Diff , 0 , len (diffs ))
205+
206+ // semantically label some numbers
207+ const insert , delete , equal = 0 , 1 , 2
208+
209+ // record the positions of the last type of each block in the fixedup blocks
210+ last := []int {- 1 , - 1 , - 1 }
211+ operation := []diffmatchpatch.Operation {diffmatchpatch .DiffInsert , diffmatchpatch .DiffDelete , diffmatchpatch .DiffEqual }
212+
213+ // create a writer for insert and deletes
214+ toWrite := []strings.Builder {
215+ {},
216+ {},
217+ }
218+
219+ // make some flags for insert and delete
220+ unfinishedTag := []bool {false , false }
221+ unfinishedEnt := []bool {false , false }
222+
223+ // store stores the provided text in the writer for the typ
224+ store := func (text string , typ int ) {
225+ (& (toWrite [typ ])).WriteString (text )
226+ }
227+
228+ // hasStored returns true if there is stored content
229+ hasStored := func (typ int ) bool {
230+ return (& toWrite [typ ]).Len () > 0
231+ }
232+
233+ // stored will return that content
234+ stored := func (typ int ) string {
235+ return (& toWrite [typ ]).String ()
236+ }
237+
238+ // empty will empty the stored content
239+ empty := func (typ int ) {
240+ (& toWrite [typ ]).Reset ()
241+ }
242+
243+ // pop will remove the stored content appending to a diff block for that typ
244+ pop := func (typ int , fixedup []diffmatchpatch.Diff ) []diffmatchpatch.Diff {
245+ if hasStored (typ ) {
246+ if last [typ ] > last [equal ] {
247+ fixedup [last [typ ]].Text += stored (typ )
248+ } else {
249+ fixedup = append (fixedup , diffmatchpatch.Diff {
250+ Type : operation [typ ],
251+ Text : stored (typ ),
252+ })
253+ }
254+ empty (typ )
255+ }
256+ return fixedup
257+ }
258+
259+ // Now we walk the provided diffs and check the type of each block in turn
260+ for _ , diff := range diffs {
261+
262+ typ := delete // flag for handling insert or delete typs
263+ switch diff .Type {
264+ case diffmatchpatch .DiffEqual :
265+ // First check if there is anything stored
266+ if hasStored (insert ) || hasStored (delete ) {
267+ // There are two reasons for storing content:
268+ // 1. Unfinished Entity <- Could be more efficient here by not doing this if we're looking for a tag
269+ if unfinishedEnt [insert ] || unfinishedEnt [delete ] {
270+ // we look for a ';' to finish an entity
271+ idx := strings .IndexRune (diff .Text , ';' )
272+ if idx >= 0 {
273+ // if we find a ';' store the preceding content to both insert and delete
274+ store (diff .Text [:idx + 1 ], insert )
275+ store (diff .Text [:idx + 1 ], delete )
276+
277+ // and remove it from this block
278+ diff .Text = diff .Text [idx + 1 :]
279+
280+ // reset the ent flags
281+ unfinishedEnt [insert ] = false
282+ unfinishedEnt [delete ] = false
283+ } else {
284+ // otherwise store it all on insert and delete
285+ store (diff .Text , insert )
286+ store (diff .Text , delete )
287+ // and empty this block
288+ diff .Text = ""
289+ }
290+ }
291+ // 2. Unfinished Tag
292+ if unfinishedTag [insert ] || unfinishedTag [delete ] {
293+ // we look for a '>' to finish a tag
294+ idx := strings .IndexRune (diff .Text , '>' )
295+ if idx >= 0 {
296+ store (diff .Text [:idx + 1 ], insert )
297+ store (diff .Text [:idx + 1 ], delete )
298+ diff .Text = diff .Text [idx + 1 :]
299+ unfinishedTag [insert ] = false
300+ unfinishedTag [delete ] = false
301+ } else {
302+ store (diff .Text , insert )
303+ store (diff .Text , delete )
304+ diff .Text = ""
305+ }
306+ }
307+
308+ // If we've completed the required tag/entities
309+ if ! (unfinishedTag [insert ] || unfinishedTag [delete ] || unfinishedEnt [insert ] || unfinishedEnt [delete ]) {
310+ // pop off the stack
311+ fixedup = pop (insert , fixedup )
312+ fixedup = pop (delete , fixedup )
313+ }
314+
315+ // If that has left this diff block empty then shortcut
316+ if len (diff .Text ) == 0 {
317+ continue
318+ }
319+ }
320+
321+ // check if this block ends in an unfinished tag?
322+ idx := unfinishedtagRegex .FindStringIndex (diff .Text )
323+ if idx != nil {
324+ unfinishedTag [insert ] = true
325+ unfinishedTag [delete ] = true
326+ } else {
327+ // otherwise does it end in an unfinished entity?
328+ idx = entityRegex .FindStringIndex (diff .Text )
329+ if idx != nil {
330+ unfinishedEnt [insert ] = true
331+ unfinishedEnt [delete ] = true
332+ }
333+ }
334+
335+ // If there is an unfinished component
336+ if idx != nil {
337+ // Store the fragment
338+ store (diff .Text [idx [0 ]:], insert )
339+ store (diff .Text [idx [0 ]:], delete )
340+ // and remove it from this block
341+ diff .Text = diff .Text [:idx [0 ]]
342+ }
343+
344+ // If that hasn't left the block empty
345+ if len (diff .Text ) > 0 {
346+ // store the position of the last equal block and store it in our diffs
347+ last [equal ] = len (fixedup )
348+ fixedup = append (fixedup , diff )
349+ }
350+ continue
351+ case diffmatchpatch .DiffInsert :
352+ typ = insert
353+ fallthrough
354+ case diffmatchpatch .DiffDelete :
355+ // First check if there is anything stored for this type
356+ if hasStored (typ ) {
357+ // if there is prepend it to this block, empty the storage and reset our flags
358+ diff .Text = stored (typ ) + diff .Text
359+ empty (typ )
360+ unfinishedEnt [typ ] = false
361+ unfinishedTag [typ ] = false
362+ }
363+
364+ // check if this block ends in an unfinished tag
365+ idx := unfinishedtagRegex .FindStringIndex (diff .Text )
366+ if idx != nil {
367+ unfinishedTag [typ ] = true
368+ } else {
369+ // otherwise does it end in an unfinished entity
370+ idx = entityRegex .FindStringIndex (diff .Text )
371+ if idx != nil {
372+ unfinishedEnt [typ ] = true
373+ }
374+ }
375+
376+ // If there is an unfinished component
377+ if idx != nil {
378+ // Store the fragment
379+ store (diff .Text [idx [0 ]:], typ )
380+ // and remove it from this block
381+ diff .Text = diff .Text [:idx [0 ]]
382+ }
383+
384+ // If that hasn't left the block empty
385+ if len (diff .Text ) > 0 {
386+ // if the last block of this type was after the last equal block
387+ if last [typ ] > last [equal ] {
388+ // store this blocks content on that block
389+ fixedup [last [typ ]].Text += diff .Text
390+ } else {
391+ // otherwise store the position of the last block of this type and store the block
392+ last [typ ] = len (fixedup )
393+ fixedup = append (fixedup , diff )
394+ }
395+ }
396+ continue
397+ }
398+ }
399+
400+ // pop off any remaining stored content
401+ fixedup = pop (insert , fixedup )
402+ fixedup = pop (delete , fixedup )
403+
404+ return fixedup
405+ }
406+
199407func diffToHTML (fileName string , diffs []diffmatchpatch.Diff , lineType DiffLineType ) template.HTML {
200408 buf := bytes .NewBuffer (nil )
201409 match := ""
202410
411+ diffs = fixupBrokenSpans (diffs )
412+
203413 for _ , diff := range diffs {
204414 if shouldWriteInline (diff , lineType ) {
205415 if len (match ) > 0 {
0 commit comments