diff options
| -rw-r--r-- | app/controllers/revisions_controller.rb | 34 | ||||
| -rw-r--r-- | app/helpers/revisions_helper.rb | 2 | ||||
| -rw-r--r-- | app/views/admin/index.html.erb | 2 | ||||
| -rw-r--r-- | app/views/nodes/edit.html.erb | 2 | ||||
| -rw-r--r-- | app/views/nodes/index.html.erb | 7 | ||||
| -rw-r--r-- | app/views/nodes/show.html.erb | 8 | ||||
| -rw-r--r-- | app/views/revisions/diff.html.erb | 59 | ||||
| -rw-r--r-- | app/views/revisions/index.html.erb | 1 | ||||
| -rw-r--r-- | app/views/revisions/show.html.erb | 18 | ||||
| -rw-r--r-- | public/javascripts/cacycle_diff.js | 1112 | ||||
| -rw-r--r-- | public/stylesheets/admin.css | 4 | ||||
| -rw-r--r-- | test/functional/revisions_controller_test.rb | 8 | ||||
| -rw-r--r-- | test/unit/helpers/revisions_helper_test.rb | 4 |
13 files changed, 1256 insertions, 5 deletions
diff --git a/app/controllers/revisions_controller.rb b/app/controllers/revisions_controller.rb new file mode 100644 index 0000000..565a25f --- /dev/null +++ b/app/controllers/revisions_controller.rb | |||
| @@ -0,0 +1,34 @@ | |||
| 1 | class RevisionsController < ApplicationController | ||
| 2 | |||
| 3 | layout 'admin' | ||
| 4 | |||
| 5 | def index | ||
| 6 | end | ||
| 7 | |||
| 8 | def diff | ||
| 9 | @node = Node.find(params[:id]) | ||
| 10 | |||
| 11 | if @node.pages.length > 1 | ||
| 12 | params[:start] ||= @node.pages.all[-1].revision | ||
| 13 | params[:end] ||= @node.pages.all[-2].revision | ||
| 14 | else | ||
| 15 | params[:start], params[:end] = 1, 1 | ||
| 16 | end | ||
| 17 | |||
| 18 | @start = Page.find( :first, :conditions => { | ||
| 19 | :node_id => params[:id], | ||
| 20 | :revision => params[:start] | ||
| 21 | }) | ||
| 22 | |||
| 23 | @end = Page.find( :first, :conditions => { | ||
| 24 | :node_id => params[:id], | ||
| 25 | :revision => params[:end] | ||
| 26 | }) | ||
| 27 | |||
| 28 | end | ||
| 29 | |||
| 30 | def show | ||
| 31 | @node = Node.find(params[:id]) | ||
| 32 | end | ||
| 33 | |||
| 34 | end | ||
diff --git a/app/helpers/revisions_helper.rb b/app/helpers/revisions_helper.rb new file mode 100644 index 0000000..fdb51f8 --- /dev/null +++ b/app/helpers/revisions_helper.rb | |||
| @@ -0,0 +1,2 @@ | |||
| 1 | module RevisionsHelper | ||
| 2 | end | ||
diff --git a/app/views/admin/index.html.erb b/app/views/admin/index.html.erb index 00a5c09..c5bb5de 100644 --- a/app/views/admin/index.html.erb +++ b/app/views/admin/index.html.erb | |||
| @@ -14,6 +14,8 @@ | |||
| 14 | <td><%= draft.node.unique_name %></td> | 14 | <td><%= draft.node.unique_name %></td> |
| 15 | <td><%= draft.user.login rescue "" %></td> | 15 | <td><%= draft.user.login rescue "" %></td> |
| 16 | <td><%= link_to 'Show', node_path(draft.node) %></td> | 16 | <td><%= link_to 'Show', node_path(draft.node) %></td> |
| 17 | <td><%= link_to "Diff revisions", :controller => :revisions, :action => :diff, :id => draft.node.id %></td> | ||
| 18 | <td><%= link_to "Publish", publish_node_path(draft.node), :method => :put, :confirm => "Do you really want to publish?" %></td> | ||
| 17 | </tr> | 19 | </tr> |
| 18 | <% end %> | 20 | <% end %> |
| 19 | </table> | 21 | </table> |
diff --git a/app/views/nodes/edit.html.erb b/app/views/nodes/edit.html.erb index 2dade0f..577a075 100644 --- a/app/views/nodes/edit.html.erb +++ b/app/views/nodes/edit.html.erb | |||
| @@ -2,6 +2,8 @@ | |||
| 2 | <%= link_to 'Show', @node %> | 2 | <%= link_to 'Show', @node %> |
| 3 | <%= link_to 'Back', nodes_path %> | 3 | <%= link_to 'Back', nodes_path %> |
| 4 | <%= link_to 'Publish', publish_node_path, :method => :put %> | 4 | <%= link_to 'Publish', publish_node_path, :method => :put %> |
| 5 | <%= link_to 'Diff revisions', :controller => :revisions, :action => :diff, :id => params[:id] %> | ||
| 6 | |||
| 5 | </div> | 7 | </div> |
| 6 | 8 | ||
| 7 | <h1>Editing page</h1> | 9 | <h1>Editing page</h1> |
diff --git a/app/views/nodes/index.html.erb b/app/views/nodes/index.html.erb index a2a42b1..ad60498 100644 --- a/app/views/nodes/index.html.erb +++ b/app/views/nodes/index.html.erb | |||
| @@ -23,15 +23,16 @@ | |||
| 23 | <td><%= node.unique_name %></td> | 23 | <td><%= node.unique_name %></td> |
| 24 | <td> | 24 | <td> |
| 25 | <%= link_to 'Show', node_path(node) %> | 25 | <%= link_to 'Show', node_path(node) %> |
| 26 | <%= link_to 'Edit', edit_node_path(node) %> | 26 | <%= link_to 'Edit', edit_node_path(node) %> |
| 27 | <%= link_to 'Destroy', node, :method => :delete, :confirm => "Are you sure you want to delete this node?" %> | 27 | <%= link_to 'Revision', :controller => :revisions, :action => :show, :id => node.id %> |
| 28 | <%# link_to 'Destroy', node, :method => :delete, :confirm => "Are you sure you want to delete this node?" %> | ||
| 28 | <%= link_to 'Unlock', unlock_node_path(node), :method => :put, :confirm => "Are you sure you want to unlock?" %> | 29 | <%= link_to 'Unlock', unlock_node_path(node), :method => :put, :confirm => "Are you sure you want to unlock?" %> |
| 29 | </td> | 30 | </td> |
| 30 | <td> | 31 | <td> |
| 31 | <%= "#{node.draft.user.login}" if node.draft && node.draft.user %> | 32 | <%= "#{node.draft.user.login}" if node.draft && node.draft.user %> |
| 32 | </td> | 33 | </td> |
| 33 | <td> | 34 | <td> |
| 34 | <%= node.head.revision if node.head %> | 35 | <%= node.pages.length %> |
| 35 | </td> | 36 | </td> |
| 36 | </tr> | 37 | </tr> |
| 37 | <% end %> | 38 | <% end %> |
diff --git a/app/views/nodes/show.html.erb b/app/views/nodes/show.html.erb index 67dd7fc..bbbefe9 100644 --- a/app/views/nodes/show.html.erb +++ b/app/views/nodes/show.html.erb | |||
| @@ -1,5 +1,9 @@ | |||
| 1 | <h1>Node</h1> | 1 | <h1>Node</h1> |
| 2 | 2 | <p> | |
| 3 | There is no draft to preview. Click <%= link_to 'edit', edit_node_path %> to | 3 | There is no draft to preview. Click <%= link_to 'edit', edit_node_path %> to |
| 4 | create one or view the currently | 4 | create one or view the currently |
| 5 | <%= link_to_path 'published version', @node.unique_path %>. \ No newline at end of file | 5 | <%= link_to_path 'published version', @node.unique_path %>. |
| 6 | </p> | ||
| 7 | <p> | ||
| 8 | View the revisions of this node | ||
| 9 | </p> \ No newline at end of file | ||
diff --git a/app/views/revisions/diff.html.erb b/app/views/revisions/diff.html.erb new file mode 100644 index 0000000..a8a0276 --- /dev/null +++ b/app/views/revisions/diff.html.erb | |||
| @@ -0,0 +1,59 @@ | |||
| 1 | <h1>Revisions#diff</h1> | ||
| 2 | |||
| 3 | <% form_tag url_for(:action => :diff), :method => :get do %> | ||
| 4 | <%= select_tag :start, options_for_select(@node.pages.map{|x| x.revision}, params[:start].to_i) %> | ||
| 5 | <%= select_tag :end, options_for_select(@node.pages.map{|x| x.revision}, params[:end].to_i) %> | ||
| 6 | <%= submit_tag 'Diff' %> | ||
| 7 | <% end %> | ||
| 8 | |||
| 9 | |||
| 10 | <div id="start_title" style="display: none;"> | ||
| 11 | <%= (@start.title) %> | ||
| 12 | </div> | ||
| 13 | |||
| 14 | <div id="end_title" style="display: none;"> | ||
| 15 | <%= (@end.title) %> | ||
| 16 | </div> | ||
| 17 | |||
| 18 | <div id="start_abstract" style="display: none;"> | ||
| 19 | <%= (@start.abstract) %> | ||
| 20 | </div> | ||
| 21 | |||
| 22 | <div id="end_abstract" style="display: none;"> | ||
| 23 | <%= (@end.abstract) %> | ||
| 24 | </div> | ||
| 25 | |||
| 26 | <div id="start_body" style="display: none;"> | ||
| 27 | <%= (@start.body) %> | ||
| 28 | </div> | ||
| 29 | |||
| 30 | <div id="end_body" style="display: none;"> | ||
| 31 | <%= (@end.body) %> | ||
| 32 | </div> | ||
| 33 | |||
| 34 | |||
| 35 | <%= javascript_include_tag 'cacycle_diff' %> | ||
| 36 | <script type="text/javascript" charset="utf-8"> | ||
| 37 | window.onload = function() { | ||
| 38 | title1 = document.getElementById('start_title').innerHTML; | ||
| 39 | title2 = document.getElementById('end_title').innerHTML; | ||
| 40 | abstract1 = document.getElementById('start_abstract').innerHTML; | ||
| 41 | abstract2 = document.getElementById('end_abstract').innerHTML; | ||
| 42 | body1 = document.getElementById('start_body').innerHTML; | ||
| 43 | body2 = document.getElementById('end_body').innerHTML; | ||
| 44 | document.getElementById('diffview_title').innerHTML = WDiffString(title1, title2); | ||
| 45 | document.getElementById('diffview_abstract').innerHTML = WDiffString(abstract1, abstract2); | ||
| 46 | document.getElementById('diffview_body').innerHTML = WDiffString(body1, body2); | ||
| 47 | } | ||
| 48 | </script> | ||
| 49 | |||
| 50 | <div id="diffview"> | ||
| 51 | <h3>Title</h3> | ||
| 52 | <p id="diffview_title"></p> | ||
| 53 | |||
| 54 | <h3>Abstract</h3> | ||
| 55 | <p id="diffview_abstract"></p> | ||
| 56 | |||
| 57 | <h3>Body</h3> | ||
| 58 | <p id="diffview_body"></p> | ||
| 59 | </div> \ No newline at end of file | ||
diff --git a/app/views/revisions/index.html.erb b/app/views/revisions/index.html.erb new file mode 100644 index 0000000..e3134d7 --- /dev/null +++ b/app/views/revisions/index.html.erb | |||
| @@ -0,0 +1 @@ | |||
| <h1>Revisions#index</h1> | |||
diff --git a/app/views/revisions/show.html.erb b/app/views/revisions/show.html.erb new file mode 100644 index 0000000..64979fc --- /dev/null +++ b/app/views/revisions/show.html.erb | |||
| @@ -0,0 +1,18 @@ | |||
| 1 | <div id="subnavigation"> | ||
| 2 | <%= link_to 'Diff revisions', :action => :diff, :id => params[:id] %> | ||
| 3 | <%= link_to 'Edit', edit_node_path(@node) %> | ||
| 4 | </div> | ||
| 5 | |||
| 6 | <h2>Revisions for Node: <%= @node.unique_name %></h2> | ||
| 7 | |||
| 8 | <h3>Current title: <%= @node.head.title %></h3> | ||
| 9 | <table> | ||
| 10 | <% @node.pages.reverse.each do |page| %> | ||
| 11 | <tr> | ||
| 12 | <td><%= page.revision %></td> | ||
| 13 | <td><%= page.title %></td> | ||
| 14 | <td><%= page.user.try(:login) %></td> | ||
| 15 | <td><%= page.updated_at %></td> | ||
| 16 | </tr> | ||
| 17 | <% end %> | ||
| 18 | </table> \ No newline at end of file | ||
diff --git a/public/javascripts/cacycle_diff.js b/public/javascripts/cacycle_diff.js new file mode 100644 index 0000000..24f9d0b --- /dev/null +++ b/public/javascripts/cacycle_diff.js | |||
| @@ -0,0 +1,1112 @@ | |||
| 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 3e67231..49578f0 100644 --- a/public/stylesheets/admin.css +++ b/public/stylesheets/admin.css | |||
| @@ -71,4 +71,8 @@ input[type=text] { | |||
| 71 | 71 | ||
| 72 | #navigation a { | 72 | #navigation a { |
| 73 | text-decoration: none; | 73 | text-decoration: none; |
| 74 | } | ||
| 75 | |||
| 76 | #diffview { | ||
| 77 | width: 1024px; | ||
| 74 | } \ No newline at end of file | 78 | } \ No newline at end of file |
diff --git a/test/functional/revisions_controller_test.rb b/test/functional/revisions_controller_test.rb new file mode 100644 index 0000000..d752b0e --- /dev/null +++ b/test/functional/revisions_controller_test.rb | |||
| @@ -0,0 +1,8 @@ | |||
| 1 | require 'test_helper' | ||
| 2 | |||
| 3 | class RevisionsControllerTest < ActionController::TestCase | ||
| 4 | # Replace this with your real tests. | ||
| 5 | test "the truth" do | ||
| 6 | assert true | ||
| 7 | end | ||
| 8 | end | ||
diff --git a/test/unit/helpers/revisions_helper_test.rb b/test/unit/helpers/revisions_helper_test.rb new file mode 100644 index 0000000..6bb5f07 --- /dev/null +++ b/test/unit/helpers/revisions_helper_test.rb | |||
| @@ -0,0 +1,4 @@ | |||
| 1 | require 'test_helper' | ||
| 2 | |||
| 3 | class RevisionsHelperTest < ActionView::TestCase | ||
| 4 | end | ||
