diff options
| author | erdgeist <erdgeist@erdgeist.org> | 2026-07-10 00:35:24 +0200 |
|---|---|---|
| committer | erdgeist <erdgeist@erdgeist.org> | 2026-07-10 00:35:24 +0200 |
| commit | a25d90335ad3738b6831288190132c2f7498465c (patch) | |
| tree | 5aa7083d9f8c8701644fe60bc7d28542775694b8 | |
| parent | da0b79a7c1f3116e2b450ecbe50efdf31cf23414 (diff) | |
Retire vendored cacycle_diff.js for a diff-lcs-based diff view
Revisions#diff now computes an inline or side-by-side word diff
server-side (Page#diff_against, lib/html_word_diff.rb) instead of
shipping raw/escaped content to the browser for a 2008-era vendored
JS differ to mangle. View mode is picked via a `view` param, settable
either on the diff page itself or directly from the revisions#index
sticky bar next to "Diff revisions".
Also fixes a pre-existing bug in RevisionsController#diff's single-
revision branch, which set params[:start]/params[:end] instead of
params[:start_revision]/params[:end_revision].
| -rw-r--r-- | Gemfile | 3 | ||||
| -rw-r--r-- | Gemfile.lock | 3 | ||||
| -rw-r--r-- | app/controllers/revisions_controller.rb | 12 | ||||
| -rw-r--r-- | app/models/page.rb | 16 | ||||
| -rw-r--r-- | app/views/revisions/diff.html.erb | 78 | ||||
| -rw-r--r-- | app/views/revisions/index.html.erb | 8 | ||||
| -rw-r--r-- | lib/html_word_diff.rb | 52 | ||||
| -rw-r--r-- | public/javascripts/cacycle_diff.js | 1112 | ||||
| -rw-r--r-- | public/stylesheets/admin.css | 25 | ||||
| -rw-r--r-- | test/controllers/revisions_controller_test.rb | 29 | ||||
| -rw-r--r-- | test/models/page_test.rb | 34 |
11 files changed, 204 insertions, 1168 deletions
| @@ -45,10 +45,11 @@ gem 'acts-as-taggable-on', | |||
| 45 | git: 'https://github.com/mbleigh/acts-as-taggable-on.git', | 45 | git: 'https://github.com/mbleigh/acts-as-taggable-on.git', |
| 46 | branch: 'master' | 46 | branch: 'master' |
| 47 | 47 | ||
| 48 | # ── XML / parsing ───────────────────────────────────────────────────────────── | 48 | # ── XML / parsing / diffing ─────────────────────────────────────────────────── |
| 49 | 49 | ||
| 50 | gem 'libxml-ruby', '~> 5.0', require: 'xml' # body link rewriting in Page model | 50 | gem 'libxml-ruby', '~> 5.0', require: 'xml' # body link rewriting in Page model |
| 51 | gem 'nokogiri', '~> 1.18' | 51 | gem 'nokogiri', '~> 1.18' |
| 52 | gem 'diff-lcs', require: 'diff/lcs' | ||
| 52 | 53 | ||
| 53 | # ── Operational ─────────────────────────────────────────────────────────────── | 54 | # ── Operational ─────────────────────────────────────────────────────────────── |
| 54 | 55 | ||
diff --git a/Gemfile.lock b/Gemfile.lock index 02c71ff..e6f5fa0 100644 --- a/Gemfile.lock +++ b/Gemfile.lock | |||
| @@ -108,6 +108,7 @@ GEM | |||
| 108 | connection_pool (3.0.2) | 108 | connection_pool (3.0.2) |
| 109 | crass (1.0.7) | 109 | crass (1.0.7) |
| 110 | date (3.5.1) | 110 | date (3.5.1) |
| 111 | diff-lcs (2.0.0) | ||
| 111 | drb (2.2.3) | 112 | drb (2.2.3) |
| 112 | erb (6.0.4) | 113 | erb (6.0.4) |
| 113 | erubi (1.13.1) | 114 | erubi (1.13.1) |
| @@ -330,6 +331,7 @@ DEPENDENCIES | |||
| 330 | chaos_calendar! | 331 | chaos_calendar! |
| 331 | coffee-rails (~> 4.0) | 332 | coffee-rails (~> 4.0) |
| 332 | concurrent-ruby (~> 1.3) | 333 | concurrent-ruby (~> 1.3) |
| 334 | diff-lcs | ||
| 333 | exception_notification (~> 4.5) | 335 | exception_notification (~> 4.5) |
| 334 | globalize (~> 7.0) | 336 | globalize (~> 7.0) |
| 335 | jquery-rails | 337 | jquery-rails |
| @@ -377,6 +379,7 @@ CHECKSUMS | |||
| 377 | connection_pool (3.0.2) sha256=33fff5ba71a12d2aa26cb72b1db8bba2a1a01823559fb01d29eb74c286e62e0a | 379 | connection_pool (3.0.2) sha256=33fff5ba71a12d2aa26cb72b1db8bba2a1a01823559fb01d29eb74c286e62e0a |
| 378 | crass (1.0.7) sha256=94868719948664c89ddcaf0a37c65048413dfcb1c869470a5f7a7ceb5390b295 | 380 | crass (1.0.7) sha256=94868719948664c89ddcaf0a37c65048413dfcb1c869470a5f7a7ceb5390b295 |
| 379 | date (3.5.1) sha256=750d06384d7b9c15d562c76291407d89e368dda4d4fff957eb94962d325a0dc0 | 381 | date (3.5.1) sha256=750d06384d7b9c15d562c76291407d89e368dda4d4fff957eb94962d325a0dc0 |
| 382 | diff-lcs (2.0.0) sha256=708a5d52ec2945b50f8f53a181174aa1ef2c496edf81c05957fe956dabb363d5 | ||
| 380 | drb (2.2.3) sha256=0b00d6fdb50995fe4a45dea13663493c841112e4068656854646f418fda13373 | 383 | drb (2.2.3) sha256=0b00d6fdb50995fe4a45dea13663493c841112e4068656854646f418fda13373 |
| 381 | erb (6.0.4) sha256=38e3803694be357fe2bfe312487c74beaf9fb4e5beb3e22498952fe1645b95d9 | 384 | erb (6.0.4) sha256=38e3803694be357fe2bfe312487c74beaf9fb4e5beb3e22498952fe1645b95d9 |
| 382 | erubi (1.13.1) sha256=a082103b0885dbc5ecf1172fede897f9ebdb745a4b97a5e8dc63953db1ee4ad9 | 385 | erubi (1.13.1) sha256=a082103b0885dbc5ecf1172fede897f9ebdb745a4b97a5e8dc63953db1ee4ad9 |
diff --git a/app/controllers/revisions_controller.rb b/app/controllers/revisions_controller.rb index 42d667e..9acb26f 100644 --- a/app/controllers/revisions_controller.rb +++ b/app/controllers/revisions_controller.rb | |||
| @@ -13,16 +13,18 @@ class RevisionsController < ApplicationController | |||
| 13 | 13 | ||
| 14 | def diff | 14 | def diff |
| 15 | @node = Node.find(params[:node_id]) | 15 | @node = Node.find(params[:node_id]) |
| 16 | 16 | ||
| 17 | if @node.pages.length > 1 | 17 | if @node.pages.length > 1 |
| 18 | params[:start_revision] ||= @node.pages.all[-2].revision | 18 | params[:start_revision] ||= @node.pages.all[-2].revision |
| 19 | params[:end_revision] ||= @node.pages.all[-1].revision | 19 | params[:end_revision] ||= @node.pages.all[-1].revision |
| 20 | else | 20 | else |
| 21 | params[:start], params[:end] = 1, 1 | 21 | params[:start_revision], params[:end_revision] = 1, 1 |
| 22 | end | 22 | end |
| 23 | 23 | ||
| 24 | @start = @node.pages.find_by_revision( params[:start_revision] ) | 24 | @start = @node.pages.find_by_revision( params[:start_revision] ) |
| 25 | @end = @node.pages.find_by_revision( params[:end_revision] ) | 25 | @end = @node.pages.find_by_revision( params[:end_revision] ) |
| 26 | @diff_view = params[:view] == "side_by_side" ? :side_by_side : :inline | ||
| 27 | @diff = @end.diff_against( @start, view: @diff_view ) | ||
| 26 | end | 28 | end |
| 27 | 29 | ||
| 28 | def show | 30 | def show |
diff --git a/app/models/page.rb b/app/models/page.rb index 1a98e08..ea04cd6 100644 --- a/app/models/page.rb +++ b/app/models/page.rb | |||
| @@ -175,6 +175,22 @@ class Page < ApplicationRecord | |||
| 175 | self.save | 175 | self.save |
| 176 | end | 176 | end |
| 177 | 177 | ||
| 178 | def diff_against other, view: :inline | ||
| 179 | if view == :side_by_side | ||
| 180 | { | ||
| 181 | title: HtmlWordDiff.side_by_side(other.title.to_s, title.to_s), | ||
| 182 | abstract: HtmlWordDiff.side_by_side(other.abstract.to_s, abstract.to_s), | ||
| 183 | body: HtmlWordDiff.side_by_side(other.body.to_s, body.to_s) | ||
| 184 | } | ||
| 185 | else | ||
| 186 | { | ||
| 187 | title: HtmlWordDiff.inline(other.title.to_s, title.to_s), | ||
| 188 | abstract: HtmlWordDiff.inline(other.abstract.to_s, abstract.to_s), | ||
| 189 | body: HtmlWordDiff.inline(other.body.to_s, body.to_s) | ||
| 190 | } | ||
| 191 | end | ||
| 192 | end | ||
| 193 | |||
| 178 | def public? | 194 | def public? |
| 179 | published_at.nil? ? true : published_at < Time.now | 195 | published_at.nil? ? true : published_at < Time.now |
| 180 | end | 196 | end |
diff --git a/app/views/revisions/diff.html.erb b/app/views/revisions/diff.html.erb index d8c6a47..d7bb528 100644 --- a/app/views/revisions/diff.html.erb +++ b/app/views/revisions/diff.html.erb | |||
| @@ -7,58 +7,36 @@ | |||
| 7 | <%= form_tag diff_node_revisions_path do %> | 7 | <%= form_tag diff_node_revisions_path do %> |
| 8 | <%= select_tag :start_revision, options_for_select(@node.pages.map{|x| x.revision}, params[:start_revision].to_i) %> | 8 | <%= select_tag :start_revision, options_for_select(@node.pages.map{|x| x.revision}, params[:start_revision].to_i) %> |
| 9 | <%= select_tag :end_revision, options_for_select(@node.pages.map{|x| x.revision}, params[:end_revision].to_i) %> | 9 | <%= select_tag :end_revision, options_for_select(@node.pages.map{|x| x.revision}, params[:end_revision].to_i) %> |
| 10 | <%= select_tag :view, options_for_select([['Inline', 'inline'], ['Side by side', 'side_by_side']], @diff_view) %> | ||
| 10 | <%= submit_tag 'Diff' %> | 11 | <%= submit_tag 'Diff' %> |
| 11 | <% end %> | 12 | <% end %> |
| 12 | 13 | ||
| 13 | <div id="start_title" style="display: none;"> | ||
| 14 | <%= h(@start.title) %> | ||
| 15 | </div> | ||
| 16 | |||
| 17 | <div id="end_title" style="display: none;"> | ||
| 18 | <%= h(@end.title) %> | ||
| 19 | </div> | ||
| 20 | |||
| 21 | <div id="start_abstract" style="display: none;"> | ||
| 22 | <%= h(@start.abstract) %> | ||
| 23 | </div> | ||
| 24 | |||
| 25 | <div id="end_abstract" style="display: none;"> | ||
| 26 | <%= h(@end.abstract) %> | ||
| 27 | </div> | ||
| 28 | |||
| 29 | <pre> | ||
| 30 | <div id="start_body" style="display: none;"> | ||
| 31 | <%= (@start.body) %> | ||
| 32 | </div> | ||
| 33 | </pre> | ||
| 34 | <pre> | ||
| 35 | <div id="end_body" style="display: none;"> | ||
| 36 | <%= (@end.body) %> | ||
| 37 | </div> | ||
| 38 | </pre> | ||
| 39 | |||
| 40 | <script src="/javascripts/cacycle_diff.js"></script> | ||
| 41 | <script type="text/javascript" charset="utf-8"> | ||
| 42 | window.onload = function() { | ||
| 43 | title1 = document.getElementById('start_title').innerHTML; | ||
| 44 | title2 = document.getElementById('end_title').innerHTML; | ||
| 45 | abstract1 = document.getElementById('start_abstract').innerHTML; | ||
| 46 | abstract2 = document.getElementById('end_abstract').innerHTML; | ||
| 47 | body1 = document.getElementById('start_body').innerHTML; | ||
| 48 | body2 = document.getElementById('end_body').innerHTML; | ||
| 49 | document.getElementById('diffview_title').innerHTML = WDiffString(title1, title2); | ||
| 50 | document.getElementById('diffview_abstract').innerHTML = WDiffString(abstract1, abstract2); | ||
| 51 | document.getElementById('diffview_body').innerHTML = WDiffString(body1, body2); | ||
| 52 | } | ||
| 53 | </script> | ||
| 54 | |||
| 55 | <div id="diffview"> | 14 | <div id="diffview"> |
| 56 | <h3>Title</h3> | 15 | <% if @diff_view == :side_by_side %> |
| 57 | <p id="diffview_title"></p> | 16 | <div class="diff_side_by_side"> |
| 58 | 17 | <div class="diff_column"> | |
| 59 | <h3>Abstract</h3> | 18 | <h3>Title</h3> |
| 60 | <p id="diffview_abstract"></p> | 19 | <p><%= raw @diff[:title][0] %></p> |
| 61 | 20 | <h3>Abstract</h3> | |
| 62 | <h3>Body</h3> | 21 | <p><%= raw @diff[:abstract][0] %></p> |
| 63 | <p id="diffview_body"></p> | 22 | <h3>Body</h3> |
| 23 | <%= raw @diff[:body][0] %> | ||
| 24 | </div> | ||
| 25 | <div class="diff_column"> | ||
| 26 | <h3>Title</h3> | ||
| 27 | <p><%= raw @diff[:title][1] %></p> | ||
| 28 | <h3>Abstract</h3> | ||
| 29 | <p><%= raw @diff[:abstract][1] %></p> | ||
| 30 | <h3>Body</h3> | ||
| 31 | <%= raw @diff[:body][1] %> | ||
| 32 | </div> | ||
| 33 | </div> | ||
| 34 | <% else %> | ||
| 35 | <h3>Title</h3> | ||
| 36 | <p><%= raw @diff[:title] %></p> | ||
| 37 | <h3>Abstract</h3> | ||
| 38 | <p><%= raw @diff[:abstract] %></p> | ||
| 39 | <h3>Body</h3> | ||
| 40 | <%= raw @diff[:body] %> | ||
| 41 | <% end %> | ||
| 64 | </div> | 42 | </div> |
diff --git a/app/views/revisions/index.html.erb b/app/views/revisions/index.html.erb index a6a981a..58c08b7 100644 --- a/app/views/revisions/index.html.erb +++ b/app/views/revisions/index.html.erb | |||
| @@ -19,6 +19,8 @@ | |||
| 19 | form: { id: 'diff_form', class: 'button_to computation' }, | 19 | form: { id: 'diff_form', class: 'button_to computation' }, |
| 20 | disabled: true %> | 20 | disabled: true %> |
| 21 | <span id="diff_selection_label"></span> | 21 | <span id="diff_selection_label"></span> |
| 22 | <label><%= radio_button_tag :view, 'inline', true %> Inline</label> | ||
| 23 | <label><%= radio_button_tag :view, 'side_by_side', false %> Side by side</label> | ||
| 22 | </td> | 24 | </td> |
| 23 | </tr> | 25 | </tr> |
| 24 | </thead> | 26 | </thead> |
| @@ -68,6 +70,7 @@ | |||
| 68 | document.getElementById('diff_form').addEventListener('submit', function(e) { | 70 | document.getElementById('diff_form').addEventListener('submit', function(e) { |
| 69 | var start = document.querySelector('input[name="start_revision"]:checked'); | 71 | var start = document.querySelector('input[name="start_revision"]:checked'); |
| 70 | var end = document.querySelector('input[name="end_revision"]:checked'); | 72 | var end = document.querySelector('input[name="end_revision"]:checked'); |
| 73 | var view = document.querySelector('input[name="view"]:checked'); | ||
| 71 | if (start) { | 74 | if (start) { |
| 72 | var s = document.createElement('input'); | 75 | var s = document.createElement('input'); |
| 73 | s.type = 'hidden'; s.name = 'start_revision'; s.value = start.value; | 76 | s.type = 'hidden'; s.name = 'start_revision'; s.value = start.value; |
| @@ -78,5 +81,10 @@ | |||
| 78 | en.type = 'hidden'; en.name = 'end_revision'; en.value = end.value; | 81 | en.type = 'hidden'; en.name = 'end_revision'; en.value = end.value; |
| 79 | this.appendChild(en); | 82 | this.appendChild(en); |
| 80 | } | 83 | } |
| 84 | if (view) { | ||
| 85 | var v = document.createElement('input'); | ||
| 86 | v.type = 'hidden'; v.name = 'view'; v.value = view.value; | ||
| 87 | this.appendChild(v); | ||
| 88 | } | ||
| 81 | }); | 89 | }); |
| 82 | </script> | 90 | </script> |
diff --git a/lib/html_word_diff.rb b/lib/html_word_diff.rb new file mode 100644 index 0000000..1f28cf5 --- /dev/null +++ b/lib/html_word_diff.rb | |||
| @@ -0,0 +1,52 @@ | |||
| 1 | require 'diff/lcs' | ||
| 2 | |||
| 3 | module HtmlWordDiff | ||
| 4 | TOKEN_REGEXP = /<[^>]*>|[[:space:]]+|[[:alnum:]]+|[^[:space:][:alnum:]<>]/ | ||
| 5 | |||
| 6 | def self.inline(old_html, new_html) | ||
| 7 | html = +'' | ||
| 8 | each_change(old_html, new_html) do |action, old_token, new_token| | ||
| 9 | case action | ||
| 10 | when '=' | ||
| 11 | html << new_token | ||
| 12 | when '-' | ||
| 13 | html << "<del>#{old_token}</del>" | ||
| 14 | when '+' | ||
| 15 | html << "<ins>#{new_token}</ins>" | ||
| 16 | when '!' | ||
| 17 | html << "<del>#{old_token}</del><ins>#{new_token}</ins>" | ||
| 18 | end | ||
| 19 | end | ||
| 20 | html | ||
| 21 | end | ||
| 22 | |||
| 23 | def self.side_by_side(old_html, new_html) | ||
| 24 | old_out = +'' | ||
| 25 | new_out = +'' | ||
| 26 | each_change(old_html, new_html) do |action, old_token, new_token| | ||
| 27 | case action | ||
| 28 | when '=' | ||
| 29 | old_out << old_token | ||
| 30 | new_out << new_token | ||
| 31 | when '-' | ||
| 32 | old_out << "<del>#{old_token}</del>" | ||
| 33 | when '+' | ||
| 34 | new_out << "<ins>#{new_token}</ins>" | ||
| 35 | when '!' | ||
| 36 | old_out << "<del>#{old_token}</del>" | ||
| 37 | new_out << "<ins>#{new_token}</ins>" | ||
| 38 | end | ||
| 39 | end | ||
| 40 | [old_out, new_out] | ||
| 41 | end | ||
| 42 | |||
| 43 | def self.each_change(old_html, new_html) | ||
| 44 | Diff::LCS.sdiff(tokenize(old_html), tokenize(new_html)).each do |change| | ||
| 45 | yield change.action, change.old_element, change.new_element | ||
| 46 | end | ||
| 47 | end | ||
| 48 | |||
| 49 | def self.tokenize(html) | ||
| 50 | html.to_s.scan(TOKEN_REGEXP) | ||
| 51 | end | ||
| 52 | end | ||
diff --git a/public/javascripts/cacycle_diff.js b/public/javascripts/cacycle_diff.js deleted file mode 100644 index 24f9d0b..0000000 --- a/public/javascripts/cacycle_diff.js +++ /dev/null | |||
| @@ -1,1112 +0,0 @@ | |||
| 1 | // <pre><nowiki> | ||
| 2 | |||
| 3 | /* | ||
| 4 | |||
| 5 | Name: diff.js | ||
| 6 | Version: 0.9.5a (April 6, 2008) | ||
| 7 | Info: http://en.wikipedia.org/wiki/User:Cacycle/diff | ||
| 8 | Code: http://en.wikipedia.org/wiki/User:Cacycle/diff.js | ||
| 9 | |||
| 10 | JavaScript diff algorithm by [[en:User:Cacycle]] (http://en.wikipedia.org/wiki/User_talk:Cacycle). | ||
| 11 | Outputs html/css-formatted new text with highlighted deletions, inserts, and block moves. | ||
| 12 | |||
| 13 | The program uses cross-browser code and should work with all modern browsers. It has been tested with: | ||
| 14 | * Mozilla Firefox 1.5.0.1 | ||
| 15 | * Mozilla SeaMonkey 1.0 | ||
| 16 | * Opera 8.53 | ||
| 17 | * Internet Explorer 6.0.2900.2180 | ||
| 18 | * Internet Explorer 7.0.5730.11 | ||
| 19 | This program is also compatibel with Greasemonkey | ||
| 20 | |||
| 21 | An implementation of the word-based algorithm from: | ||
| 22 | |||
| 23 | Communications of the ACM 21(4):264 (1978) | ||
| 24 | http://doi.acm.org/10.1145/359460.359467 | ||
| 25 | |||
| 26 | With the following additional feature: | ||
| 27 | |||
| 28 | * Word types have been optimized for MediaWiki source texts | ||
| 29 | * Additional post-pass 5 code for resolving islands caused by adding | ||
| 30 | two common words at the end of sequences of common words | ||
| 31 | * Additional detection of block borders and color coding of moved blocks and their original position | ||
| 32 | * Optional "intelligent" omission of unchanged parts from the output | ||
| 33 | |||
| 34 | This code is used by the MediaWiki in-browser text editors [[en:User:Cacycle/editor]] and [[en:User:Cacycle/wikEd]] | ||
| 35 | and the enhanced diff view tool wikEdDiff [[en:User:Cacycle/wikEd]]. | ||
| 36 | |||
| 37 | Usage: var htmlText = WDiffString(oldText, newText); | ||
| 38 | |||
| 39 | This code has been released into the public domain. | ||
| 40 | |||
| 41 | Datastructures: | ||
| 42 | |||
| 43 | text: an object that holds all text related datastructures | ||
| 44 | .newWords: consecutive words of the new text (N) | ||
| 45 | .oldWords: consecutive words of the old text (O) | ||
| 46 | .newToOld: array of corresponding word number in old text (NA) | ||
| 47 | .oldToNew: array of corresponding word number in new text (OA) | ||
| 48 | .message: output message for testing purposes | ||
| 49 | |||
| 50 | symbol['word']: symbol table for passes 1 - 3, holds words as a hash | ||
| 51 | .newCtr: new word occurences counter (NC) | ||
| 52 | .oldCtr: old word occurences counter (OC) | ||
| 53 | .toNew: table last old word number | ||
| 54 | .toOld: last new word number (OLNA) | ||
| 55 | |||
| 56 | block: an object that holds block move information | ||
| 57 | blocks indexed after new text: | ||
| 58 | .newStart: new text word number of start of this block | ||
| 59 | .newLength: element number of this block including non-words | ||
| 60 | .newWords: true word number of this block | ||
| 61 | .newNumber: corresponding block index in old text | ||
| 62 | .newBlock: moved-block-number of a block that has been moved here | ||
| 63 | .newLeft: moved-block-number of a block that has been moved from this border leftwards | ||
| 64 | .newRight: moved-block-number of a block that has been moved from this border rightwards | ||
| 65 | .newLeftIndex: index number of a block that has been moved from this border leftwards | ||
| 66 | .newRightIndex: index number of a block that has been moved from this border rightwards | ||
| 67 | blocks indexed after old text: | ||
| 68 | .oldStart: word number of start of this block | ||
| 69 | .oldToNew: corresponding new text word number of start | ||
| 70 | .oldLength: element number of this block including non-words | ||
| 71 | .oldWords: true word number of this block | ||
| 72 | |||
| 73 | */ | ||
| 74 | |||
| 75 | |||
| 76 | // css for change indicators | ||
| 77 | if (typeof(wDiffStyleDelete) == 'undefined') { window.wDiffStyleDelete = 'font-weight: normal; text-decoration: none; color: #fff; background-color: #990033;'; } | ||
| 78 | if (typeof(wDiffStyleInsert) == 'undefined') { window.wDiffStyleInsert = 'font-weight: normal; text-decoration: none; color: #fff; background-color: #009933;'; } | ||
| 79 | if (typeof(wDiffStyleMoved) == 'undefined') { window.wDiffStyleMoved = 'font-weight: bold; color: #000; vertical-align: text-bottom; font-size: xx-small; padding: 0; border: solid 1px;'; } | ||
| 80 | if (typeof(wDiffStyleBlock) == 'undefined') { window.wDiffStyleBlock = [ | ||
| 81 | 'color: #000; background-color: #ffff80;', | ||
| 82 | 'color: #000; background-color: #c0ffff;', | ||
| 83 | 'color: #000; background-color: #ffd0f0;', | ||
| 84 | 'color: #000; background-color: #ffe080;', | ||
| 85 | 'color: #000; background-color: #aaddff;', | ||
| 86 | 'color: #000; background-color: #ddaaff;', | ||
| 87 | 'color: #000; background-color: #ffbbbb;', | ||
| 88 | 'color: #000; background-color: #d8ffa0;', | ||
| 89 | 'color: #000; background-color: #d0d0d0;' | ||
| 90 | ]; } | ||
| 91 | |||
| 92 | // html for change indicators, {number} is replaced by the block number | ||
| 93 | // {block} is replaced by the block style, class and html comments are important for shortening the output | ||
| 94 | if (typeof(wDiffHtmlMovedRight) == 'undefined') { window.wDiffHtmlMovedRight = '<input class="wDiffHtmlMovedRight" type="button" value=">" style="' + wDiffStyleMoved + ' {block}"><!--wDiffHtmlMovedRight-->'; } | ||
| 95 | if (typeof(wDiffHtmlMovedLeft) == 'undefined') { window.wDiffHtmlMovedLeft = '<input class="wDiffHtmlMovedLeft" type="button" value="<" style="' + wDiffStyleMoved + ' {block}"><!--wDiffHtmlMovedLeft-->'; } | ||
| 96 | |||
| 97 | if (typeof(wDiffHtmlBlockStart) == 'undefined') { window.wDiffHtmlBlockStart = '<span class="wDiffHtmlBlock" style="{block}">'; } | ||
| 98 | if (typeof(wDiffHtmlBlockEnd) == 'undefined') { window.wDiffHtmlBlockEnd = '</span><!--wDiffHtmlBlock-->'; } | ||
| 99 | |||
| 100 | if (typeof(wDiffHtmlDeleteStart) == 'undefined') { window.wDiffHtmlDeleteStart = '<span class="wDiffHtmlDelete" style="' + wDiffStyleDelete + '">'; } | ||
| 101 | if (typeof(wDiffHtmlDeleteEnd) == 'undefined') { window.wDiffHtmlDeleteEnd = '</span><!--wDiffHtmlDelete-->'; } | ||
| 102 | |||
| 103 | if (typeof(wDiffHtmlInsertStart) == 'undefined') { window.wDiffHtmlInsertStart = '<span class="wDiffHtmlInsert" style="' + wDiffStyleInsert + '">'; } | ||
| 104 | if (typeof(wDiffHtmlInsertEnd) == 'undefined') { window.wDiffHtmlInsertEnd = '</span><!--wDiffHtmlInsert-->'; } | ||
| 105 | |||
| 106 | // minimal number of real words for a moved block (0 for always displaying block move indicators) | ||
| 107 | if (typeof(wDiffBlockMinLength) == 'undefined') { window.wDiffBlockMinLength = 3; } | ||
| 108 | |||
| 109 | // exclude identical sequence starts and endings from change marking | ||
| 110 | if (typeof(wDiffWordDiff) == 'undefined') { window.wDiffWordDiff = true; } | ||
| 111 | |||
| 112 | // enable recursive diff to resolve problematic sequences | ||
| 113 | if (typeof(wDiffRecursiveDiff) == 'undefined') { window.wDiffRecursiveDiff = true; } | ||
| 114 | |||
| 115 | // enable block move display | ||
| 116 | if (typeof(wDiffShowBlockMoves) == 'undefined') { window.wDiffShowBlockMoves = true; } | ||
| 117 | |||
| 118 | // remove unchanged parts from final output | ||
| 119 | |||
| 120 | // characters before diff tag to search for previous heading, paragraph, line break, cut characters | ||
| 121 | if (typeof(wDiffHeadingBefore) == 'undefined') { window.wDiffHeadingBefore = 1500; } | ||
| 122 | if (typeof(wDiffParagraphBefore) == 'undefined') { window.wDiffParagraphBefore = 1500; } | ||
| 123 | if (typeof(wDiffLineBeforeMax) == 'undefined') { window.wDiffLineBeforeMax = 1000; } | ||
| 124 | if (typeof(wDiffLineBeforeMin) == 'undefined') { window.wDiffLineBeforeMin = 500; } | ||
| 125 | if (typeof(wDiffBlankBeforeMax) == 'undefined') { window.wDiffBlankBeforeMax = 1000; } | ||
| 126 | if (typeof(wDiffBlankBeforeMin) == 'undefined') { window.wDiffBlankBeforeMin = 500; } | ||
| 127 | if (typeof(wDiffCharsBefore) == 'undefined') { window.wDiffCharsBefore = 500; } | ||
| 128 | |||
| 129 | // characters after diff tag to search for next heading, paragraph, line break, or characters | ||
| 130 | if (typeof(wDiffHeadingAfter) == 'undefined') { window.wDiffHeadingAfter = 1500; } | ||
| 131 | if (typeof(wDiffParagraphAfter) == 'undefined') { window.wDiffParagraphAfter = 1500; } | ||
| 132 | if (typeof(wDiffLineAfterMax) == 'undefined') { window.wDiffLineAfterMax = 1000; } | ||
| 133 | if (typeof(wDiffLineAfterMin) == 'undefined') { window.wDiffLineAfterMin = 500; } | ||
| 134 | if (typeof(wDiffBlankAfterMax) == 'undefined') { window.wDiffBlankAfterMax = 1000; } | ||
| 135 | if (typeof(wDiffBlankAfterMin) == 'undefined') { window.wDiffBlankAfterMin = 500; } | ||
| 136 | if (typeof(wDiffCharsAfter) == 'undefined') { window.wDiffCharsAfter = 500; } | ||
| 137 | |||
| 138 | // maximal fragment distance to join close fragments | ||
| 139 | if (typeof(wDiffFragmentJoin) == 'undefined') { window.wDiffFragmentJoin = 1000; } | ||
| 140 | if (typeof(wDiffOmittedChars) == 'undefined') { window.wDiffOmittedChars = '…'; } | ||
| 141 | if (typeof(wDiffOmittedLines) == 'undefined') { window.wDiffOmittedLines = '<hr style="height: 2px; margin: 1em 10%;">'; } | ||
| 142 | if (typeof(wDiffNoChange) == 'undefined') { window.wDiffNoChange = '<hr style="height: 2px; margin: 1em 20%;">'; } | ||
| 143 | |||
| 144 | // compatibility fix for old name of main function | ||
| 145 | window.StringDiff = window.WDiffString; | ||
| 146 | |||
| 147 | |||
| 148 | // WDiffString: main program | ||
| 149 | // input: oldText, newText, strings containing the texts | ||
| 150 | // returns: html diff | ||
| 151 | |||
| 152 | window.WDiffString = function(oldText, newText) { | ||
| 153 | |||
| 154 | // IE / Mac fix | ||
| 155 | oldText = oldText.replace(/(\r\n)/g, '\n'); | ||
| 156 | newText = newText.replace(/(\r\n)/g, '\n'); | ||
| 157 | |||
| 158 | var text = {}; | ||
| 159 | text.newWords = []; | ||
| 160 | text.oldWords = []; | ||
| 161 | text.newToOld = []; | ||
| 162 | text.oldToNew = []; | ||
| 163 | text.message = ''; | ||
| 164 | var block = {}; | ||
| 165 | var outText = ''; | ||
| 166 | |||
| 167 | // trap trivial changes: no change | ||
| 168 | if (oldText == newText) { | ||
| 169 | outText = newText; | ||
| 170 | outText = WDiffEscape(outText); | ||
| 171 | outText = WDiffHtmlFormat(outText); | ||
| 172 | return(outText); | ||
| 173 | } | ||
| 174 | |||
| 175 | // trap trivial changes: old text deleted | ||
| 176 | if ( (oldText == null) || (oldText.length == 0) ) { | ||
| 177 | outText = newText; | ||
| 178 | outText = WDiffEscape(outText); | ||
| 179 | outText = WDiffHtmlFormat(outText); | ||
| 180 | outText = wDiffHtmlInsertStart + outText + wDiffHtmlInsertEnd; | ||
| 181 | return(outText); | ||
| 182 | } | ||
| 183 | |||
| 184 | // trap trivial changes: new text deleted | ||
| 185 | if ( (newText == null) || (newText.length == 0) ) { | ||
| 186 | outText = oldText; | ||
| 187 | outText = WDiffEscape(outText); | ||
| 188 | outText = WDiffHtmlFormat(outText); | ||
| 189 | outText = wDiffHtmlDeleteStart + outText + wDiffHtmlDeleteEnd; | ||
| 190 | return(outText); | ||
| 191 | } | ||
| 192 | |||
| 193 | // split new and old text into words | ||
| 194 | WDiffSplitText(oldText, newText, text); | ||
| 195 | |||
| 196 | // calculate diff information | ||
| 197 | WDiffText(text); | ||
| 198 | |||
| 199 | //detect block borders and moved blocks | ||
| 200 | WDiffDetectBlocks(text, block); | ||
| 201 | |||
| 202 | // process diff data into formatted html text | ||
| 203 | outText = WDiffToHtml(text, block); | ||
| 204 | |||
| 205 | // IE fix | ||
| 206 | outText = outText.replace(/> ( *)</g, '> $1<'); | ||
| 207 | |||
| 208 | return(outText); | ||
| 209 | } | ||
| 210 | |||
| 211 | |||
| 212 | // WDiffSplitText: split new and old text into words | ||
| 213 | // input: oldText, newText, strings containing the texts | ||
| 214 | // changes: text.newWords and text.oldWords, arrays containing the texts in arrays of words | ||
| 215 | |||
| 216 | window.WDiffSplitText = function(oldText, newText, text) { | ||
| 217 | |||
| 218 | // convert strange spaces | ||
| 219 | oldText = oldText.replace(/[\t\u000b\u00a0\u2028\u2029]+/g, ' '); | ||
| 220 | newText = newText.replace(/[\t\u000b\u00a0\u2028\u2029]+/g, ' '); | ||
| 221 | |||
| 222 | // split old text into words | ||
| 223 | |||
| 224 | // / | | | | | | | | | | | | | | / | ||
| 225 | var pattern = /[\w]+|\[\[|\]\]|\{\{|\}\}|\n+| +|&\w+;|'''|''|=+|\{\||\|\}|\|\-|./g; | ||
| 226 | var result; | ||
| 227 | do { | ||
| 228 | result = pattern.exec(oldText); | ||
| 229 | if (result != null) { | ||
| 230 | text.oldWords.push(result[0]); | ||
| 231 | } | ||
| 232 | } while (result != null); | ||
| 233 | |||
| 234 | // split new text into words | ||
| 235 | do { | ||
| 236 | result = pattern.exec(newText); | ||
| 237 | if (result != null) { | ||
| 238 | text.newWords.push(result[0]); | ||
| 239 | } | ||
| 240 | } while (result != null); | ||
| 241 | |||
| 242 | return; | ||
| 243 | } | ||
| 244 | |||
| 245 | |||
| 246 | // WDiffText: calculate diff information | ||
| 247 | // input: text.newWords and text.oldWords, arrays containing the texts in arrays of words | ||
| 248 | // optionally for recursive calls: newStart, newEnd, oldStart, oldEnd, recursionLevel | ||
| 249 | // changes: text.newToOld and text.oldToNew, containing the line numbers in the other version | ||
| 250 | |||
| 251 | window.WDiffText = function(text, newStart, newEnd, oldStart, oldEnd, recursionLevel) { | ||
| 252 | |||
| 253 | symbol = new Object(); | ||
| 254 | symbol.newCtr = []; | ||
| 255 | symbol.oldCtr = []; | ||
| 256 | symbol.toNew = []; | ||
| 257 | symbol.toOld = []; | ||
| 258 | |||
| 259 | // set defaults | ||
| 260 | newStart = newStart || 0; | ||
| 261 | newEnd = newEnd || text.newWords.length; | ||
| 262 | oldStart = oldStart || 0; | ||
| 263 | oldEnd = oldEnd || text.oldWords.length; | ||
| 264 | recursionLevel = recursionLevel || 0; | ||
| 265 | |||
| 266 | // limit recursion depth | ||
| 267 | if (recursionLevel > 10) { | ||
| 268 | return; | ||
| 269 | } | ||
| 270 | |||
| 271 | // pass 1: parse new text into symbol table s | ||
| 272 | |||
| 273 | var word; | ||
| 274 | for (var i = newStart; i < newEnd; i ++) { | ||
| 275 | word = text.newWords[i]; | ||
| 276 | |||
| 277 | // add new entry to symbol table | ||
| 278 | if ( symbol[word] == null) { | ||
| 279 | symbol[word] = { newCtr: 0, oldCtr: 0, toNew: null, toOld: null }; | ||
| 280 | } | ||
| 281 | |||
| 282 | // increment symbol table word counter for new text | ||
| 283 | symbol[word].newCtr ++; | ||
| 284 | |||
| 285 | // add last word number in new text | ||
| 286 | symbol[word].toNew = i; | ||
| 287 | } | ||
| 288 | |||
| 289 | // pass 2: parse old text into symbol table | ||
| 290 | |||
| 291 | for (var j = oldStart; j < oldEnd; j ++) { | ||
| 292 | word = text.oldWords[j]; | ||
| 293 | |||
| 294 | // add new entry to symbol table | ||
| 295 | if ( symbol[word] == null) { | ||
| 296 | symbol[word] = { newCtr: 0, oldCtr: 0, toNew: null, toOld: null }; | ||
| 297 | } | ||
| 298 | |||
| 299 | // increment symbol table word counter for old text | ||
| 300 | symbol[word].oldCtr ++; | ||
| 301 | |||
| 302 | // add last word number in old text | ||
| 303 | symbol[word].toOld = j; | ||
| 304 | } | ||
| 305 | |||
| 306 | // pass 3: connect unique words | ||
| 307 | |||
| 308 | for (var i in symbol) { | ||
| 309 | |||
| 310 | // find words in the symbol table that occur only once in both versions | ||
| 311 | if ( (symbol[i].newCtr == 1) && (symbol[i].oldCtr == 1) ) { | ||
| 312 | var toNew = symbol[i].toNew; | ||
| 313 | var toOld = symbol[i].toOld; | ||
| 314 | |||
| 315 | // do not use spaces as unique markers | ||
| 316 | if ( ! /\s/.test( text.newWords[toNew] ) ) { | ||
| 317 | |||
| 318 | // connect from new to old and from old to new | ||
| 319 | text.newToOld[toNew] = toOld; | ||
| 320 | text.oldToNew[toOld] = toNew; | ||
| 321 | } | ||
| 322 | } | ||
| 323 | } | ||
| 324 | |||
| 325 | // pass 4: connect adjacent identical words downwards | ||
| 326 | |||
| 327 | for (var i = newStart; i < newEnd - 1; i ++) { | ||
| 328 | |||
| 329 | // find already connected pairs | ||
| 330 | if (text.newToOld[i] != null) { | ||
| 331 | j = text.newToOld[i]; | ||
| 332 | |||
| 333 | // check if the following words are not yet connected | ||
| 334 | if ( (text.newToOld[i + 1] == null) && (text.oldToNew[j + 1] == null) ) { | ||
| 335 | |||
| 336 | // if the following words are the same connect them | ||
| 337 | if ( text.newWords[i + 1] == text.oldWords[j + 1] ) { | ||
| 338 | text.newToOld[i + 1] = j + 1; | ||
| 339 | text.oldToNew[j + 1] = i + 1; | ||
| 340 | } | ||
| 341 | } | ||
| 342 | } | ||
| 343 | } | ||
| 344 | |||
| 345 | // pass 5: connect adjacent identical words upwards | ||
| 346 | |||
| 347 | for (var i = newEnd - 1; i > newStart; i --) { | ||
| 348 | |||
| 349 | // find already connected pairs | ||
| 350 | if (text.newToOld[i] != null) { | ||
| 351 | j = text.newToOld[i]; | ||
| 352 | |||
| 353 | // check if the preceeding words are not yet connected | ||
| 354 | if ( (text.newToOld[i - 1] == null) && (text.oldToNew[j - 1] == null) ) { | ||
| 355 | |||
| 356 | // if the preceeding words are the same connect them | ||
| 357 | if ( text.newWords[i - 1] == text.oldWords[j - 1] ) { | ||
| 358 | text.newToOld[i - 1] = j - 1; | ||
| 359 | text.oldToNew[j - 1] = i - 1; | ||
| 360 | } | ||
| 361 | } | ||
| 362 | } | ||
| 363 | } | ||
| 364 | |||
| 365 | // recursively diff still unresolved regions downwards | ||
| 366 | |||
| 367 | if (wDiffRecursiveDiff) { | ||
| 368 | i = newStart; | ||
| 369 | j = oldStart; | ||
| 370 | while (i < newEnd) { | ||
| 371 | if (text.newToOld[i - 1] != null) { | ||
| 372 | j = text.newToOld[i - 1] + 1; | ||
| 373 | } | ||
| 374 | |||
| 375 | // check for the start of an unresolved sequence | ||
| 376 | if ( (text.newToOld[i] == null) && (text.oldToNew[j] == null) ) { | ||
| 377 | |||
| 378 | // determine the ends of the sequences | ||
| 379 | var iStart = i; | ||
| 380 | var iEnd = i; | ||
| 381 | while ( (text.newToOld[iEnd] == null) && (iEnd < newEnd) ) { | ||
| 382 | iEnd ++; | ||
| 383 | } | ||
| 384 | var iLength = iEnd - iStart; | ||
| 385 | |||
| 386 | var jStart = j; | ||
| 387 | var jEnd = j; | ||
| 388 | while ( (text.oldToNew[jEnd] == null) && (jEnd < oldEnd) ) { | ||
| 389 | jEnd ++; | ||
| 390 | } | ||
| 391 | var jLength = jEnd - jStart; | ||
| 392 | |||
| 393 | // recursively diff the unresolved sequence | ||
| 394 | if ( (iLength > 0) && (jLength > 0) ) { | ||
| 395 | if ( (iLength > 1) || (jLength > 1) ) { | ||
| 396 | if ( (iStart != newStart) || (iEnd != newEnd) || (jStart != oldStart) || (jEnd != oldEnd) ) { | ||
| 397 | WDiffText(text, iStart, iEnd, jStart, jEnd, recursionLevel + 1); | ||
| 398 | } | ||
| 399 | } | ||
| 400 | } | ||
| 401 | i = iEnd; | ||
| 402 | } | ||
| 403 | else { | ||
| 404 | i ++; | ||
| 405 | } | ||
| 406 | } | ||
| 407 | } | ||
| 408 | |||
| 409 | // recursively diff still unresolved regions upwards | ||
| 410 | |||
| 411 | if (wDiffRecursiveDiff) { | ||
| 412 | i = newEnd - 1; | ||
| 413 | j = oldEnd - 1; | ||
| 414 | while (i >= newStart) { | ||
| 415 | if (text.newToOld[i + 1] != null) { | ||
| 416 | j = text.newToOld[i + 1] - 1; | ||
| 417 | } | ||
| 418 | |||
| 419 | // check for the start of an unresolved sequence | ||
| 420 | if ( (text.newToOld[i] == null) && (text.oldToNew[j] == null) ) { | ||
| 421 | |||
| 422 | // determine the ends of the sequences | ||
| 423 | var iStart = i; | ||
| 424 | var iEnd = i + 1; | ||
| 425 | while ( (text.newToOld[iStart - 1] == null) && (iStart >= newStart) ) { | ||
| 426 | iStart --; | ||
| 427 | } | ||
| 428 | var iLength = iEnd - iStart; | ||
| 429 | |||
| 430 | var jStart = j; | ||
| 431 | var jEnd = j + 1; | ||
| 432 | while ( (text.oldToNew[jStart - 1] == null) && (jStart >= oldStart) ) { | ||
| 433 | jStart --; | ||
| 434 | } | ||
| 435 | var jLength = jEnd - jStart; | ||
| 436 | |||
| 437 | // recursively diff the unresolved sequence | ||
| 438 | if ( (iLength > 0) && (jLength > 0) ) { | ||
| 439 | if ( (iLength > 1) || (jLength > 1) ) { | ||
| 440 | if ( (iStart != newStart) || (iEnd != newEnd) || (jStart != oldStart) || (jEnd != oldEnd) ) { | ||
| 441 | WDiffText(text, iStart, iEnd, jStart, jEnd, recursionLevel + 1); | ||
| 442 | } | ||
| 443 | } | ||
| 444 | } | ||
| 445 | i = iStart - 1; | ||
| 446 | } | ||
| 447 | else { | ||
| 448 | i --; | ||
| 449 | } | ||
| 450 | } | ||
| 451 | } | ||
| 452 | return; | ||
| 453 | } | ||
| 454 | |||
| 455 | |||
| 456 | // WDiffToHtml: process diff data into formatted html text | ||
| 457 | // input: text.newWords and text.oldWords, arrays containing the texts in arrays of words | ||
| 458 | // text.newToOld and text.oldToNew, containing the line numbers in the other version | ||
| 459 | // block data structure | ||
| 460 | // returns: outText, a html string | ||
| 461 | |||
| 462 | window.WDiffToHtml = function(text, block) { | ||
| 463 | |||
| 464 | var outText = text.message; | ||
| 465 | |||
| 466 | var blockNumber = 0; | ||
| 467 | var i = 0; | ||
| 468 | var j = 0; | ||
| 469 | var movedAsInsertion; | ||
| 470 | |||
| 471 | // cycle through the new text | ||
| 472 | do { | ||
| 473 | var movedIndex = []; | ||
| 474 | var movedBlock = []; | ||
| 475 | var movedLeft = []; | ||
| 476 | var blockText = ''; | ||
| 477 | var identText = ''; | ||
| 478 | var delText = ''; | ||
| 479 | var insText = ''; | ||
| 480 | var identStart = ''; | ||
| 481 | |||
| 482 | // check if a block ends here and finish previous block | ||
| 483 | if (movedAsInsertion != null) { | ||
| 484 | if (movedAsInsertion == false) { | ||
| 485 | identStart += wDiffHtmlBlockEnd; | ||
| 486 | } | ||
| 487 | else { | ||
| 488 | identStart += wDiffHtmlInsertEnd; | ||
| 489 | } | ||
| 490 | movedAsInsertion = null; | ||
| 491 | } | ||
| 492 | |||
| 493 | // detect block boundary | ||
| 494 | if ( (text.newToOld[i] != j) || (blockNumber == 0 ) ) { | ||
| 495 | if ( ( (text.newToOld[i] != null) || (i >= text.newWords.length) ) && ( (text.oldToNew[j] != null) || (j >= text.oldWords.length) ) ) { | ||
| 496 | |||
| 497 | // block moved right | ||
| 498 | var moved = block.newRight[blockNumber]; | ||
| 499 | if (moved > 0) { | ||
| 500 | var index = block.newRightIndex[blockNumber]; | ||
| 501 | movedIndex.push(index); | ||
| 502 | movedBlock.push(moved); | ||
| 503 | movedLeft.push(false); | ||
| 504 | } | ||
| 505 | |||
| 506 | // block moved left | ||
| 507 | moved = block.newLeft[blockNumber]; | ||
| 508 | if (moved > 0) { | ||
| 509 | var index = block.newLeftIndex[blockNumber]; | ||
| 510 | movedIndex.push(index); | ||
| 511 | movedBlock.push(moved); | ||
| 512 | movedLeft.push(true); | ||
| 513 | } | ||
| 514 | |||
| 515 | // check if a block starts here | ||
| 516 | moved = block.newBlock[blockNumber]; | ||
| 517 | if (moved > 0) { | ||
| 518 | |||
| 519 | // mark block as inserted text | ||
| 520 | if (block.newWords[blockNumber] < wDiffBlockMinLength) { | ||
| 521 | identStart += wDiffHtmlInsertStart; | ||
| 522 | movedAsInsertion = true; | ||
| 523 | } | ||
| 524 | |||
| 525 | // mark block by color | ||
| 526 | else { | ||
| 527 | if (moved > wDiffStyleBlock.length) { | ||
| 528 | moved = wDiffStyleBlock.length; | ||
| 529 | } | ||
| 530 | identStart += WDiffHtmlCustomize(wDiffHtmlBlockStart, moved - 1); | ||
| 531 | movedAsInsertion = false; | ||
| 532 | } | ||
| 533 | } | ||
| 534 | |||
| 535 | if (i >= text.newWords.length) { | ||
| 536 | i ++; | ||
| 537 | } | ||
| 538 | else { | ||
| 539 | j = text.newToOld[i]; | ||
| 540 | blockNumber ++; | ||
| 541 | } | ||
| 542 | } | ||
| 543 | } | ||
| 544 | |||
| 545 | // get the correct order if moved to the left as well as to the right from here | ||
| 546 | if (movedIndex.length == 2) { | ||
| 547 | if (movedIndex[0] > movedIndex[1]) { | ||
| 548 | movedIndex.reverse(); | ||
| 549 | movedBlock.reverse(); | ||
| 550 | movedLeft.reverse(); | ||
| 551 | } | ||
| 552 | } | ||
| 553 | |||
| 554 | // handle left and right block moves from this position | ||
| 555 | for (var m = 0; m < movedIndex.length; m ++) { | ||
| 556 | |||
| 557 | // insert the block as deleted text | ||
| 558 | if (block.newWords[ movedIndex[m] ] < wDiffBlockMinLength) { | ||
| 559 | var movedStart = block.newStart[ movedIndex[m] ]; | ||
| 560 | var movedLength = block.newLength[ movedIndex[m] ]; | ||
| 561 | var str = ''; | ||
| 562 | for (var n = movedStart; n < movedStart + movedLength; n ++) { | ||
| 563 | str += text.newWords[n]; | ||
| 564 | } | ||
| 565 | str = WDiffEscape(str); | ||
| 566 | str = str.replace(/\n/g, '¶<br>'); | ||
| 567 | blockText += wDiffHtmlDeleteStart + str + wDiffHtmlDeleteEnd; | ||
| 568 | } | ||
| 569 | |||
| 570 | // add a placeholder / move direction indicator | ||
| 571 | else { | ||
| 572 | if (movedBlock[m] > wDiffStyleBlock.length) { | ||
| 573 | movedBlock[m] = wDiffStyleBlock.length; | ||
| 574 | } | ||
| 575 | if (movedLeft[m]) { | ||
| 576 | blockText += WDiffHtmlCustomize(wDiffHtmlMovedLeft, movedBlock[m] - 1); | ||
| 577 | } | ||
| 578 | else { | ||
| 579 | blockText += WDiffHtmlCustomize(wDiffHtmlMovedRight, movedBlock[m] - 1); | ||
| 580 | } | ||
| 581 | } | ||
| 582 | } | ||
| 583 | |||
| 584 | // collect consecutive identical text | ||
| 585 | while ( (i < text.newWords.length) && (j < text.oldWords.length) ) { | ||
| 586 | if ( (text.newToOld[i] == null) || (text.oldToNew[j] == null) ) { | ||
| 587 | break; | ||
| 588 | } | ||
| 589 | if (text.newToOld[i] != j) { | ||
| 590 | break; | ||
| 591 | } | ||
| 592 | identText += text.newWords[i]; | ||
| 593 | i ++; | ||
| 594 | j ++; | ||
| 595 | } | ||
| 596 | |||
| 597 | // collect consecutive deletions | ||
| 598 | while ( (text.oldToNew[j] == null) && (j < text.oldWords.length) ) { | ||
| 599 | delText += text.oldWords[j]; | ||
| 600 | j ++; | ||
| 601 | } | ||
| 602 | |||
| 603 | // collect consecutive inserts | ||
| 604 | while ( (text.newToOld[i] == null) && (i < text.newWords.length) ) { | ||
| 605 | insText += text.newWords[i]; | ||
| 606 | i ++; | ||
| 607 | } | ||
| 608 | |||
| 609 | // remove leading and trailing similarities betweein delText and ins from highlighting | ||
| 610 | var preText = ''; | ||
| 611 | var postText = ''; | ||
| 612 | if (wDiffWordDiff) { | ||
| 613 | if ( (delText != '') && (insText != '') ) { | ||
| 614 | |||
| 615 | // remove leading similarities | ||
| 616 | while ( delText.charAt(0) == insText.charAt(0) && (delText != '') && (insText != '') ) { | ||
| 617 | preText = preText + delText.charAt(0); | ||
| 618 | delText = delText.substr(1); | ||
| 619 | insText = insText.substr(1); | ||
| 620 | } | ||
| 621 | |||
| 622 | // remove trailing similarities | ||
| 623 | while ( delText.charAt(delText.length - 1) == insText.charAt(insText.length - 1) && (delText != '') && (insText != '') ) { | ||
| 624 | postText = delText.charAt(delText.length - 1) + postText; | ||
| 625 | delText = delText.substr(0, delText.length - 1); | ||
| 626 | insText = insText.substr(0, insText.length - 1); | ||
| 627 | } | ||
| 628 | } | ||
| 629 | } | ||
| 630 | |||
| 631 | // output the identical text, deletions and inserts | ||
| 632 | |||
| 633 | // moved from here indicator | ||
| 634 | if (blockText != '') { | ||
| 635 | outText += blockText; | ||
| 636 | } | ||
| 637 | |||
| 638 | // identical text | ||
| 639 | if (identText != '') { | ||
| 640 | outText += identStart + WDiffEscape(identText); | ||
| 641 | } | ||
| 642 | outText += preText; | ||
| 643 | |||
| 644 | // deleted text | ||
| 645 | if (delText != '') { | ||
| 646 | delText = wDiffHtmlDeleteStart + WDiffEscape(delText) + wDiffHtmlDeleteEnd; | ||
| 647 | delText = delText.replace(/\n/g, '¶<br>'); | ||
| 648 | outText += delText; | ||
| 649 | } | ||
| 650 | |||
| 651 | // inserted text | ||
| 652 | if (insText != '') { | ||
| 653 | insText = wDiffHtmlInsertStart + WDiffEscape(insText) + wDiffHtmlInsertEnd; | ||
| 654 | insText = insText.replace(/\n/g, '¶<br>'); | ||
| 655 | outText += insText; | ||
| 656 | } | ||
| 657 | outText += postText; | ||
| 658 | } while (i <= text.newWords.length); | ||
| 659 | |||
| 660 | outText += '\n'; | ||
| 661 | outText = WDiffHtmlFormat(outText); | ||
| 662 | |||
| 663 | return(outText); | ||
| 664 | } | ||
| 665 | |||
| 666 | |||
| 667 | // WDiffEscape: replaces html-sensitive characters in output text with character entities | ||
| 668 | |||
| 669 | window.WDiffEscape = function(text) { | ||
| 670 | |||
| 671 | text = text.replace(/&/g, '&'); | ||
| 672 | text = text.replace(/</g, '<'); | ||
| 673 | text = text.replace(/>/g, '>'); | ||
| 674 | text = text.replace(/\"/g, '"'); | ||
| 675 | |||
| 676 | return(text); | ||
| 677 | } | ||
| 678 | |||
| 679 | |||
| 680 | // HtmlCustomize: customize indicator html: replace {number} with the block number, {block} with the block style | ||
| 681 | |||
| 682 | window.WDiffHtmlCustomize = function(text, block) { | ||
| 683 | |||
| 684 | text = text.replace(/\{number\}/, block); | ||
| 685 | text = text.replace(/\{block\}/, wDiffStyleBlock[block]); | ||
| 686 | |||
| 687 | return(text); | ||
| 688 | } | ||
| 689 | |||
| 690 | |||
| 691 | // HtmlFormat: replaces newlines and multiple spaces in text with html code | ||
| 692 | |||
| 693 | window.WDiffHtmlFormat = function(text) { | ||
| 694 | |||
| 695 | text = text.replace(/ /g, ' '); | ||
| 696 | text = text.replace(/\n/g, '<br>'); | ||
| 697 | |||
| 698 | return(text); | ||
| 699 | } | ||
| 700 | |||
| 701 | |||
| 702 | // WDiffDetectBlocks: detect block borders and moved blocks | ||
| 703 | // input: text object, block object | ||
| 704 | |||
| 705 | window.WDiffDetectBlocks = function(text, block) { | ||
| 706 | |||
| 707 | block.oldStart = []; | ||
| 708 | block.oldToNew = []; | ||
| 709 | block.oldLength = []; | ||
| 710 | block.oldWords = []; | ||
| 711 | block.newStart = []; | ||
| 712 | block.newLength = []; | ||
| 713 | block.newWords = []; | ||
| 714 | block.newNumber = []; | ||
| 715 | block.newBlock = []; | ||
| 716 | block.newLeft = []; | ||
| 717 | block.newRight = []; | ||
| 718 | block.newLeftIndex = []; | ||
| 719 | block.newRightIndex = []; | ||
| 720 | |||
| 721 | var blockNumber = 0; | ||
| 722 | var wordCounter = 0; | ||
| 723 | var realWordCounter = 0; | ||
| 724 | |||
| 725 | // get old text block order | ||
| 726 | if (wDiffShowBlockMoves) { | ||
| 727 | var j = 0; | ||
| 728 | var i = 0; | ||
| 729 | do { | ||
| 730 | |||
| 731 | // detect block boundaries on old text | ||
| 732 | if ( (text.oldToNew[j] != i) || (blockNumber == 0 ) ) { | ||
| 733 | if ( ( (text.oldToNew[j] != null) || (j >= text.oldWords.length) ) && ( (text.newToOld[i] != null) || (i >= text.newWords.length) ) ) { | ||
| 734 | if (blockNumber > 0) { | ||
| 735 | block.oldLength[blockNumber - 1] = wordCounter; | ||
| 736 | block.oldWords[blockNumber - 1] = realWordCounter; | ||
| 737 | wordCounter = 0; | ||
| 738 | realWordCounter = 0; | ||
| 739 | } | ||
| 740 | |||
| 741 | if (j >= text.oldWords.length) { | ||
| 742 | j ++; | ||
| 743 | } | ||
| 744 | else { | ||
| 745 | i = text.oldToNew[j]; | ||
| 746 | block.oldStart[blockNumber] = j; | ||
| 747 | block.oldToNew[blockNumber] = text.oldToNew[j]; | ||
| 748 | blockNumber ++; | ||
| 749 | } | ||
| 750 | } | ||
| 751 | } | ||
| 752 | |||
| 753 | // jump over identical pairs | ||
| 754 | while ( (i < text.newWords.length) && (j < text.oldWords.length) ) { | ||
| 755 | if ( (text.newToOld[i] == null) || (text.oldToNew[j] == null) ) { | ||
| 756 | break; | ||
| 757 | } | ||
| 758 | if (text.oldToNew[j] != i) { | ||
| 759 | break; | ||
| 760 | } | ||
| 761 | i ++; | ||
| 762 | j ++; | ||
| 763 | wordCounter ++; | ||
| 764 | if ( /\w/.test( text.newWords[i] ) ) { | ||
| 765 | realWordCounter ++; | ||
| 766 | } | ||
| 767 | } | ||
| 768 | |||
| 769 | // jump over consecutive deletions | ||
| 770 | while ( (text.oldToNew[j] == null) && (j < text.oldWords.length) ) { | ||
| 771 | j ++; | ||
| 772 | } | ||
| 773 | |||
| 774 | // jump over consecutive inserts | ||
| 775 | while ( (text.newToOld[i] == null) && (i < text.newWords.length) ) { | ||
| 776 | i ++; | ||
| 777 | } | ||
| 778 | } while (j <= text.oldWords.length); | ||
| 779 | |||
| 780 | // get the block order in the new text | ||
| 781 | var lastMin; | ||
| 782 | var currMinIndex; | ||
| 783 | lastMin = null; | ||
| 784 | |||
| 785 | // sort the data by increasing start numbers into new text block info | ||
| 786 | for (var i = 0; i < blockNumber; i ++) { | ||
| 787 | currMin = null; | ||
| 788 | for (var j = 0; j < blockNumber; j ++) { | ||
| 789 | curr = block.oldToNew[j]; | ||
| 790 | if ( (curr > lastMin) || (lastMin == null) ) { | ||
| 791 | if ( (curr < currMin) || (currMin == null) ) { | ||
| 792 | currMin = curr; | ||
| 793 | currMinIndex = j; | ||
| 794 | } | ||
| 795 | } | ||
| 796 | } | ||
| 797 | block.newStart[i] = block.oldToNew[currMinIndex]; | ||
| 798 | block.newLength[i] = block.oldLength[currMinIndex]; | ||
| 799 | block.newWords[i] = block.oldWords[currMinIndex]; | ||
| 800 | block.newNumber[i] = currMinIndex; | ||
| 801 | lastMin = currMin; | ||
| 802 | } | ||
| 803 | |||
| 804 | // detect not moved blocks | ||
| 805 | for (var i = 0; i < blockNumber; i ++) { | ||
| 806 | if (block.newBlock[i] == null) { | ||
| 807 | if (block.newNumber[i] == i) { | ||
| 808 | block.newBlock[i] = 0; | ||
| 809 | } | ||
| 810 | } | ||
| 811 | } | ||
| 812 | |||
| 813 | // detect switches of neighbouring blocks | ||
| 814 | for (var i = 0; i < blockNumber - 1; i ++) { | ||
| 815 | if ( (block.newBlock[i] == null) && (block.newBlock[i + 1] == null) ) { | ||
| 816 | if (block.newNumber[i] - block.newNumber[i + 1] == 1) { | ||
| 817 | if ( (block.newNumber[i + 1] - block.newNumber[i + 2] != 1) || (i + 2 >= blockNumber) ) { | ||
| 818 | |||
| 819 | // the shorter one is declared the moved one | ||
| 820 | if (block.newLength[i] < block.newLength[i + 1]) { | ||
| 821 | block.newBlock[i] = 1; | ||
| 822 | block.newBlock[i + 1] = 0; | ||
| 823 | } | ||
| 824 | else { | ||
| 825 | block.newBlock[i] = 0; | ||
| 826 | block.newBlock[i + 1] = 1; | ||
| 827 | } | ||
| 828 | } | ||
| 829 | } | ||
| 830 | } | ||
| 831 | } | ||
| 832 | |||
| 833 | // mark all others as moved and number the moved blocks | ||
| 834 | j = 1; | ||
| 835 | for (var i = 0; i < blockNumber; i ++) { | ||
| 836 | if ( (block.newBlock[i] == null) || (block.newBlock[i] == 1) ) { | ||
| 837 | block.newBlock[i] = j++; | ||
| 838 | } | ||
| 839 | } | ||
| 840 | |||
| 841 | // check if a block has been moved from this block border | ||
| 842 | for (var i = 0; i < blockNumber; i ++) { | ||
| 843 | for (var j = 0; j < blockNumber; j ++) { | ||
| 844 | |||
| 845 | if (block.newNumber[j] == i) { | ||
| 846 | if (block.newBlock[j] > 0) { | ||
| 847 | |||
| 848 | // block moved right | ||
| 849 | if (block.newNumber[j] < j) { | ||
| 850 | block.newRight[i] = block.newBlock[j]; | ||
| 851 | block.newRightIndex[i] = j; | ||
| 852 | } | ||
| 853 | |||
| 854 | // block moved left | ||
| 855 | else { | ||
| 856 | block.newLeft[i + 1] = block.newBlock[j]; | ||
| 857 | block.newLeftIndex[i + 1] = j; | ||
| 858 | } | ||
| 859 | } | ||
| 860 | } | ||
| 861 | } | ||
| 862 | } | ||
| 863 | } | ||
| 864 | return; | ||
| 865 | } | ||
| 866 | |||
| 867 | |||
| 868 | // WDiffShortenOutput: remove unchanged parts from final output | ||
| 869 | // input: the output of WDiffString | ||
| 870 | // returns: the text with removed unchanged passages indicated by (...) | ||
| 871 | |||
| 872 | window.WDiffShortenOutput = function(diffText) { | ||
| 873 | |||
| 874 | // html <br/> to newlines | ||
| 875 | diffText = diffText.replace(/<br[^>]*>/g, '\n'); | ||
| 876 | |||
| 877 | // scan for diff html tags | ||
| 878 | var regExpDiff = new RegExp('<\\w+ class=\\"(\\w+)\\"[^>]*>(.|\\n)*?<!--\\1-->', 'g'); | ||
| 879 | var tagStart = []; | ||
| 880 | var tagEnd = []; | ||
| 881 | var i = 0; | ||
| 882 | var found; | ||
| 883 | while ( (found = regExpDiff.exec(diffText)) != null ) { | ||
| 884 | |||
| 885 | // combine consecutive diff tags | ||
| 886 | if ( (i > 0) && (tagEnd[i - 1] == found.index) ) { | ||
| 887 | tagEnd[i - 1] = found.index + found[0].length; | ||
| 888 | } | ||
| 889 | else { | ||
| 890 | tagStart[i] = found.index; | ||
| 891 | tagEnd[i] = found.index + found[0].length; | ||
| 892 | i ++; | ||
| 893 | } | ||
| 894 | } | ||
| 895 | |||
| 896 | // no diff tags detected | ||
| 897 | if (tagStart.length == 0) { | ||
| 898 | return(wDiffNoChange); | ||
| 899 | } | ||
| 900 | |||
| 901 | // define regexps | ||
| 902 | var regExpHeading = new RegExp('\\n=+.+?=+ *\\n|\\n\\{\\||\\n\\|\\}', 'g'); | ||
| 903 | var regExpParagraph = new RegExp('\\n\\n+', 'g'); | ||
| 904 | var regExpLine = new RegExp('\\n+', 'g'); | ||
| 905 | var regExpBlank = new RegExp('(<[^>]+>)*\\s+', 'g'); | ||
| 906 | |||
| 907 | // determine fragment border positions around diff tags | ||
| 908 | var rangeStart = []; | ||
| 909 | var rangeEnd = []; | ||
| 910 | var rangeStartType = []; | ||
| 911 | var rangeEndType = []; | ||
| 912 | for (var i = 0; i < tagStart.length; i ++) { | ||
| 913 | var found; | ||
| 914 | |||
| 915 | // find last heading before diff tag | ||
| 916 | var lastPos = tagStart[i] - wDiffHeadingBefore; | ||
| 917 | if (lastPos < 0) { | ||
| 918 | lastPos = 0; | ||
| 919 | } | ||
| 920 | regExpHeading.lastIndex = lastPos; | ||
| 921 | while ( (found = regExpHeading.exec(diffText)) != null ) { | ||
| 922 | if (found.index > tagStart[i]) { | ||
| 923 | break; | ||
| 924 | } | ||
| 925 | rangeStart[i] = found.index; | ||
| 926 | rangeStartType[i] = 'heading'; | ||
| 927 | } | ||
| 928 | |||
| 929 | // find last paragraph before diff tag | ||
| 930 | if (rangeStart[i] == null) { | ||
| 931 | lastPos = tagStart[i] - wDiffParagraphBefore; | ||
| 932 | if (lastPos < 0) { | ||
| 933 | lastPos = 0; | ||
| 934 | } | ||
| 935 | regExpParagraph.lastIndex = lastPos; | ||
| 936 | while ( (found = regExpParagraph.exec(diffText)) != null ) { | ||
| 937 | if (found.index > tagStart[i]) { | ||
| 938 | break; | ||
| 939 | } | ||
| 940 | rangeStart[i] = found.index; | ||
| 941 | rangeStartType[i] = 'paragraph'; | ||
| 942 | } | ||
| 943 | } | ||
| 944 | |||
| 945 | // find line break before diff tag | ||
| 946 | if (rangeStart[i] == null) { | ||
| 947 | lastPos = tagStart[i] - wDiffLineBeforeMax; | ||
| 948 | if (lastPos < 0) { | ||
| 949 | lastPos = 0; | ||
| 950 | } | ||
| 951 | regExpLine.lastIndex = lastPos; | ||
| 952 | while ( (found = regExpLine.exec(diffText)) != null ) { | ||
| 953 | if (found.index > tagStart[i] - wDiffLineBeforeMin) { | ||
| 954 | break; | ||
| 955 | } | ||
| 956 | rangeStart[i] = found.index; | ||
| 957 | rangeStartType[i] = 'line'; | ||
| 958 | } | ||
| 959 | } | ||
| 960 | |||
| 961 | // find blank before diff tag | ||
| 962 | if (rangeStart[i] == null) { | ||
| 963 | lastPos = tagStart[i] - wDiffBlankBeforeMax; | ||
| 964 | if (lastPos < 0) { | ||
| 965 | lastPos = 0; | ||
| 966 | } | ||
| 967 | regExpBlank.lastIndex = lastPos; | ||
| 968 | while ( (found = regExpBlank.exec(diffText)) != null ) { | ||
| 969 | if (found.index > tagStart[i] - wDiffBlankBeforeMin) { | ||
| 970 | break; | ||
| 971 | } | ||
| 972 | rangeStart[i] = found.index; | ||
| 973 | rangeStartType[i] = 'blank'; | ||
| 974 | } | ||
| 975 | } | ||
| 976 | |||
| 977 | // fixed number of chars before diff tag | ||
| 978 | if (rangeStart[i] == null) { | ||
| 979 | rangeStart[i] = tagStart[i] - wDiffCharsBefore; | ||
| 980 | rangeStartType[i] = 'chars'; | ||
| 981 | if (rangeStart[i] < 0) { | ||
| 982 | rangeStart[i] = 0; | ||
| 983 | } | ||
| 984 | } | ||
| 985 | |||
| 986 | // find first heading after diff tag | ||
| 987 | regExpHeading.lastIndex = tagEnd[i]; | ||
| 988 | if ( (found = regExpHeading.exec(diffText)) != null ) { | ||
| 989 | if (found.index < tagEnd[i] + wDiffHeadingAfter) { | ||
| 990 | rangeEnd[i] = found.index + found[0].length; | ||
| 991 | rangeEndType[i] = 'heading'; | ||
| 992 | } | ||
| 993 | } | ||
| 994 | |||
| 995 | // find first paragraph after diff tag | ||
| 996 | if (rangeEnd[i] == null) { | ||
| 997 | regExpParagraph.lastIndex = tagEnd[i]; | ||
| 998 | if ( (found = regExpParagraph.exec(diffText)) != null ) { | ||
| 999 | if (found.index < tagEnd[i] + wDiffParagraphAfter) { | ||
| 1000 | rangeEnd[i] = found.index; | ||
| 1001 | rangeEndType[i] = 'paragraph'; | ||
| 1002 | } | ||
| 1003 | } | ||
| 1004 | } | ||
| 1005 | |||
| 1006 | // find first line break after diff tag | ||
| 1007 | if (rangeEnd[i] == null) { | ||
| 1008 | regExpLine.lastIndex = tagEnd[i] + wDiffLineAfterMin; | ||
| 1009 | if ( (found = regExpLine.exec(diffText)) != null ) { | ||
| 1010 | if (found.index < tagEnd[i] + wDiffLineAfterMax) { | ||
| 1011 | rangeEnd[i] = found.index; | ||
| 1012 | rangeEndType[i] = 'break'; | ||
| 1013 | } | ||
| 1014 | } | ||
| 1015 | } | ||
| 1016 | |||
| 1017 | // find blank after diff tag | ||
| 1018 | if (rangeEnd[i] == null) { | ||
| 1019 | regExpBlank.lastIndex = tagEnd[i] + wDiffBlankAfterMin; | ||
| 1020 | if ( (found = regExpBlank.exec(diffText)) != null ) { | ||
| 1021 | if (found.index < tagEnd[i] + wDiffBlankAfterMax) { | ||
| 1022 | rangeEnd[i] = found.index; | ||
| 1023 | rangeEndType[i] = 'blank'; | ||
| 1024 | } | ||
| 1025 | } | ||
| 1026 | } | ||
| 1027 | |||
| 1028 | // fixed number of chars after diff tag | ||
| 1029 | if (rangeEnd[i] == null) { | ||
| 1030 | rangeEnd[i] = tagEnd[i] + wDiffCharsAfter; | ||
| 1031 | if (rangeEnd[i] > diffText.length) { | ||
| 1032 | rangeEnd[i] = diffText.length; | ||
| 1033 | rangeEndType[i] = 'chars'; | ||
| 1034 | } | ||
| 1035 | } | ||
| 1036 | } | ||
| 1037 | |||
| 1038 | // remove overlaps, join close fragments | ||
| 1039 | var fragmentStart = []; | ||
| 1040 | var fragmentEnd = []; | ||
| 1041 | var fragmentStartType = []; | ||
| 1042 | var fragmentEndType = []; | ||
| 1043 | fragmentStart[0] = rangeStart[0]; | ||
| 1044 | fragmentEnd[0] = rangeEnd[0]; | ||
| 1045 | fragmentStartType[0] = rangeStartType[0]; | ||
| 1046 | fragmentEndType[0] = rangeEndType[0]; | ||
| 1047 | var j = 1; | ||
| 1048 | for (var i = 1; i < rangeStart.length; i ++) { | ||
| 1049 | if (rangeStart[i] > fragmentEnd[j - 1] + wDiffFragmentJoin) { | ||
| 1050 | fragmentStart[j] = rangeStart[i]; | ||
| 1051 | fragmentEnd[j] = rangeEnd[i]; | ||
| 1052 | fragmentStartType[j] = rangeStartType[i]; | ||
| 1053 | fragmentEndType[j] = rangeEndType[i]; | ||
| 1054 | j ++; | ||
| 1055 | } | ||
| 1056 | else { | ||
| 1057 | fragmentEnd[j - 1] = rangeEnd[i]; | ||
| 1058 | fragmentEndType[j - 1] = rangeEndType[i]; | ||
| 1059 | } | ||
| 1060 | } | ||
| 1061 | |||
| 1062 | // assemble the fragments | ||
| 1063 | var outText = ''; | ||
| 1064 | for (var i = 0; i < fragmentStart.length; i ++) { | ||
| 1065 | |||
| 1066 | // get text fragment | ||
| 1067 | var fragment = diffText.substring(fragmentStart[i], fragmentEnd[i]); | ||
| 1068 | var fragment = fragment.replace(/^\n+|\n+$/g, ''); | ||
| 1069 | |||
| 1070 | // add inline marks for omitted chars and words | ||
| 1071 | if (fragmentStart[i] > 0) { | ||
| 1072 | if (fragmentStartType[i] == 'chars') { | ||
| 1073 | fragment = wDiffOmittedChars + fragment; | ||
| 1074 | } | ||
| 1075 | else if (fragmentStartType[i] == 'blank') { | ||
| 1076 | fragment = wDiffOmittedChars + ' ' + fragment; | ||
| 1077 | } | ||
| 1078 | } | ||
| 1079 | if (fragmentEnd[i] < diffText.length) { | ||
| 1080 | if (fragmentStartType[i] == 'chars') { | ||
| 1081 | fragment = fragment + wDiffOmittedChars; | ||
| 1082 | } | ||
| 1083 | else if (fragmentStartType[i] == 'blank') { | ||
| 1084 | fragment = fragment + ' ' + wDiffOmittedChars; | ||
| 1085 | } | ||
| 1086 | } | ||
| 1087 | |||
| 1088 | // add omitted line separator | ||
| 1089 | if (fragmentStart[i] > 0) { | ||
| 1090 | outText += wDiffOmittedLines; | ||
| 1091 | } | ||
| 1092 | |||
| 1093 | // encapsulate span errors | ||
| 1094 | outText += '<div>' + fragment + '</div>'; | ||
| 1095 | } | ||
| 1096 | |||
| 1097 | // add trailing omitted line separator | ||
| 1098 | if (fragmentEnd[i - 1] < diffText.length) { | ||
| 1099 | outText = outText + wDiffOmittedLines; | ||
| 1100 | } | ||
| 1101 | |||
| 1102 | // remove leading and trailing empty lines | ||
| 1103 | outText = outText.replace(/^(<div>)\n+|\n+(<\/div>)$/g, '$1$2'); | ||
| 1104 | |||
| 1105 | // convert to html linebreaks | ||
| 1106 | outText = outText.replace(/\n/g, '<br />'); | ||
| 1107 | |||
| 1108 | return(outText); | ||
| 1109 | } | ||
| 1110 | |||
| 1111 | |||
| 1112 | // <pre><nowiki> \ No newline at end of file | ||
diff --git a/public/stylesheets/admin.css b/public/stylesheets/admin.css index 38c9e5a..1bb6cf4 100644 --- a/public/stylesheets/admin.css +++ b/public/stylesheets/admin.css | |||
| @@ -511,6 +511,31 @@ table.revisions_table tr:hover { | |||
| 511 | background-color: #f1f1f1; | 511 | background-color: #f1f1f1; |
| 512 | } | 512 | } |
| 513 | 513 | ||
| 514 | #diffview del { | ||
| 515 | background: #ffd7d5; | ||
| 516 | color: #82071e; | ||
| 517 | text-decoration: line-through; | ||
| 518 | padding: 0 2px; | ||
| 519 | border-radius: 2px; | ||
| 520 | } | ||
| 521 | |||
| 522 | #diffview ins { | ||
| 523 | background: #ccffd8; | ||
| 524 | color: #055d20; | ||
| 525 | text-decoration: none; | ||
| 526 | padding: 0 2px; | ||
| 527 | border-radius: 2px; | ||
| 528 | } | ||
| 529 | |||
| 530 | #diffview .diff_side_by_side { | ||
| 531 | display: flex; | ||
| 532 | gap: 1rem; | ||
| 533 | } | ||
| 534 | |||
| 535 | #diffview .diff_column { | ||
| 536 | flex: 1; | ||
| 537 | } | ||
| 538 | |||
| 514 | table.user_table td.user_login { | 539 | table.user_table td.user_login { |
| 515 | padding-right: 30px; | 540 | padding-right: 30px; |
| 516 | } | 541 | } |
diff --git a/test/controllers/revisions_controller_test.rb b/test/controllers/revisions_controller_test.rb index b4dcd8f..e2fc976 100644 --- a/test/controllers/revisions_controller_test.rb +++ b/test/controllers/revisions_controller_test.rb | |||
| @@ -59,4 +59,33 @@ class RevisionsControllerTest < ActionController::TestCase | |||
| 59 | assert_equal @node.head, @node.pages.first | 59 | assert_equal @node.head, @node.pages.first |
| 60 | assert_equal "first", @node.head.reload.body | 60 | assert_equal "first", @node.head.reload.body |
| 61 | end | 61 | end |
| 62 | |||
| 63 | test "diffing two revisions renders real markup with only the changed words marked" do | ||
| 64 | login_as :quentin | ||
| 65 | post( | ||
| 66 | :diff, params: { | ||
| 67 | :node_id => @node.id, | ||
| 68 | :start_revision => @node.pages.first.revision, | ||
| 69 | :end_revision => @node.pages.last.revision | ||
| 70 | } | ||
| 71 | ) | ||
| 72 | assert_response :success | ||
| 73 | assert_select "del", "first" | ||
| 74 | assert_select "ins", "second" | ||
| 75 | assert_no_match /</, response.body | ||
| 76 | end | ||
| 77 | |||
| 78 | test "diffing two revisions in side by side view renders two columns" do | ||
| 79 | login_as :quentin | ||
| 80 | post( | ||
| 81 | :diff, params: { | ||
| 82 | :node_id => @node.id, | ||
| 83 | :start_revision => @node.pages.first.revision, | ||
| 84 | :end_revision => @node.pages.last.revision, | ||
| 85 | :view => "side_by_side" | ||
| 86 | } | ||
| 87 | ) | ||
| 88 | assert_response :success | ||
| 89 | assert_select ".diff_column", 2 | ||
| 90 | end | ||
| 62 | end | 91 | end |
diff --git a/test/models/page_test.rb b/test/models/page_test.rb index ad2742f..ac5691a 100644 --- a/test/models/page_test.rb +++ b/test/models/page_test.rb | |||
| @@ -176,4 +176,38 @@ class PageTest < ActiveSupport::TestCase | |||
| 176 | assert_not_equal first_page.id, first_page.node.head_id | 176 | assert_not_equal first_page.id, first_page.node.head_id |
| 177 | assert first_page.published_at.present? | 177 | assert first_page.published_at.present? |
| 178 | end | 178 | end |
| 179 | |||
| 180 | def test_diff_against_inline_keeps_tags_and_marks_only_the_changed_word | ||
| 181 | n = Node.root.children.create! :slug => "diff_against_test" | ||
| 182 | d = n.find_or_create_draft @user1 | ||
| 183 | d.title = "Old heading" | ||
| 184 | d.save! | ||
| 185 | n.publish_draft! | ||
| 186 | |||
| 187 | d2 = n.find_or_create_draft @user1 | ||
| 188 | d2.title = "New heading" | ||
| 189 | d2.save! | ||
| 190 | |||
| 191 | diff = d2.diff_against(n.head) | ||
| 192 | |||
| 193 | assert_match "<del>Old</del>", diff[:title] | ||
| 194 | assert_match "<ins>New</ins>", diff[:title] | ||
| 195 | end | ||
| 196 | |||
| 197 | def test_diff_against_side_by_side_returns_two_annotated_strings | ||
| 198 | n = Node.root.children.create! :slug => "diff_against_sbs_test" | ||
| 199 | d = n.find_or_create_draft @user1 | ||
| 200 | d.title = "Old heading" | ||
| 201 | d.save! | ||
| 202 | n.publish_draft! | ||
| 203 | |||
| 204 | d2 = n.find_or_create_draft @user1 | ||
| 205 | d2.title = "New heading" | ||
| 206 | d2.save! | ||
| 207 | |||
| 208 | old_html, new_html = d2.diff_against(n.head, view: :side_by_side)[:title] | ||
| 209 | |||
| 210 | assert_match "<del>Old</del>", old_html | ||
| 211 | assert_match "<ins>New</ins>", new_html | ||
| 212 | end | ||
| 179 | end | 213 | end |
