diff options
| author | erdgeist <erdgeist@erdgeist.org> | 2026-07-10 03:59:34 +0200 |
|---|---|---|
| committer | erdgeist <erdgeist@erdgeist.org> | 2026-07-10 03:59:34 +0200 |
| commit | fd98a2d3053279bd4a848faa52a3ac676db10cbb (patch) | |
| tree | 8a2aa2a16380d779fe0f8a51dcab917edff636b8 /lib | |
| parent | d18e0fa70de9b5029ebed93a1760aea46e8abf19 (diff) | |
Stop diff output from corrupting the document on structural tag changes
sdiff aligned an inserted paragraph break correctly, but the renderer
wrapped the raw </p>/<p> tokens in <ins> regardless -- invalid nesting
that browsers silently repair by dropping the unmatched closing tag,
leaving everything downstream rendered inside an <ins> with nothing
left to close it. Same failure mode as the retired cacycle_diff.js,
different cause.
Tag tokens now render as a plain-text pilcrow with a tooltip naming
the actual tag, never as literal markup inside <ins>/<del>. Applies to
both inline and side-by-side. Side-by-side's new panel flattening a
real paragraph break into a marker, rather than showing its own true
structure, is a known, accepted trade-off for now -- not the same
problem, and not fixed here.
Diffstat (limited to 'lib')
| -rw-r--r-- | lib/html_word_diff.rb | 36 |
1 files changed, 29 insertions, 7 deletions
diff --git a/lib/html_word_diff.rb b/lib/html_word_diff.rb index 1f28cf5..aa0cb86 100644 --- a/lib/html_word_diff.rb +++ b/lib/html_word_diff.rb | |||
| @@ -1,4 +1,5 @@ | |||
| 1 | require 'diff/lcs' | 1 | require 'diff/lcs' |
| 2 | require 'cgi' | ||
| 2 | 3 | ||
| 3 | module HtmlWordDiff | 4 | module HtmlWordDiff |
| 4 | TOKEN_REGEXP = /<[^>]*>|[[:space:]]+|[[:alnum:]]+|[^[:space:][:alnum:]<>]/ | 5 | TOKEN_REGEXP = /<[^>]*>|[[:space:]]+|[[:alnum:]]+|[^[:space:][:alnum:]<>]/ |
| @@ -10,11 +11,12 @@ module HtmlWordDiff | |||
| 10 | when '=' | 11 | when '=' |
| 11 | html << new_token | 12 | html << new_token |
| 12 | when '-' | 13 | when '-' |
| 13 | html << "<del>#{old_token}</del>" | 14 | html << wrap_removed(old_token) |
| 14 | when '+' | 15 | when '+' |
| 15 | html << "<ins>#{new_token}</ins>" | 16 | html << wrap_inserted(new_token) |
| 16 | when '!' | 17 | when '!' |
| 17 | html << "<del>#{old_token}</del><ins>#{new_token}</ins>" | 18 | html << wrap_removed(old_token) |
| 19 | html << wrap_inserted(new_token) | ||
| 18 | end | 20 | end |
| 19 | end | 21 | end |
| 20 | html | 22 | html |
| @@ -29,12 +31,12 @@ module HtmlWordDiff | |||
| 29 | old_out << old_token | 31 | old_out << old_token |
| 30 | new_out << new_token | 32 | new_out << new_token |
| 31 | when '-' | 33 | when '-' |
| 32 | old_out << "<del>#{old_token}</del>" | 34 | old_out << wrap_removed(old_token) |
| 33 | when '+' | 35 | when '+' |
| 34 | new_out << "<ins>#{new_token}</ins>" | 36 | new_out << wrap_inserted(new_token) |
| 35 | when '!' | 37 | when '!' |
| 36 | old_out << "<del>#{old_token}</del>" | 38 | old_out << wrap_removed(old_token) |
| 37 | new_out << "<ins>#{new_token}</ins>" | 39 | new_out << wrap_inserted(new_token) |
| 38 | end | 40 | end |
| 39 | end | 41 | end |
| 40 | [old_out, new_out] | 42 | [old_out, new_out] |
| @@ -49,4 +51,24 @@ module HtmlWordDiff | |||
| 49 | def self.tokenize(html) | 51 | def self.tokenize(html) |
| 50 | html.to_s.scan(TOKEN_REGEXP) | 52 | html.to_s.scan(TOKEN_REGEXP) |
| 51 | end | 53 | end |
| 54 | |||
| 55 | def self.tag_token?(token) | ||
| 56 | token.to_s.match?(/\A<[^>]*>\z/) | ||
| 57 | end | ||
| 58 | |||
| 59 | def self.wrap_removed(token) | ||
| 60 | if tag_token?(token) | ||
| 61 | %(<del class="diff_structural" title="removed: #{CGI.escapeHTML(token)}">\u00b6</del>) | ||
| 62 | else | ||
| 63 | "<del>#{token}</del>" | ||
| 64 | end | ||
| 65 | end | ||
| 66 | |||
| 67 | def self.wrap_inserted(token) | ||
| 68 | if tag_token?(token) | ||
| 69 | %(<ins class="diff_structural" title="added: #{CGI.escapeHTML(token)}">\u00b6</ins>) | ||
| 70 | else | ||
| 71 | "<ins>#{token}</ins>" | ||
| 72 | end | ||
| 73 | end | ||
| 52 | end | 74 | end |
