##// END OF EJS Templates
Extracts a diff helper from the WikiDiff class....
Jean-Philippe Lang -
r4832:d06f4d013d32
parent child
Show More
@@ -0,0 +1,72
1 # Redmine - project management software
2 # Copyright (C) 2006-2011 Jean-Philippe Lang
3 #
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
18 module Redmine
19 module Helpers
20 class Diff
21 include ERB::Util
22 include ActionView::Helpers::TagHelper
23 include ActionView::Helpers::TextHelper
24 attr_reader :diff, :words
25
26 def initialize(content_to, content_from)
27 @words = content_to.to_s.split(/(\s+)/)
28 @words = @words.select {|word| word != ' '}
29 words_from = content_from.to_s.split(/(\s+)/)
30 words_from = words_from.select {|word| word != ' '}
31 @diff = words_from.diff @words
32 end
33
34 def to_html
35 words = self.words.collect{|word| h(word)}
36 words_add = 0
37 words_del = 0
38 dels = 0
39 del_off = 0
40 diff.diffs.each do |diff|
41 add_at = nil
42 add_to = nil
43 del_at = nil
44 deleted = ""
45 diff.each do |change|
46 pos = change[1]
47 if change[0] == "+"
48 add_at = pos + dels unless add_at
49 add_to = pos + dels
50 words_add += 1
51 else
52 del_at = pos unless del_at
53 deleted << ' ' + h(change[2])
54 words_del += 1
55 end
56 end
57 if add_at
58 words[add_at] = '<span class="diff_in">' + words[add_at]
59 words[add_to] = words[add_to] + '</span>'
60 end
61 if del_at
62 words.insert del_at - del_off + dels + words_add, '<span class="diff_out">' + deleted + '</span>'
63 dels += 1
64 del_off += words_del
65 words_del = 0
66 end
67 end
68 simple_format(words.join(' '))
69 end
70 end
71 end
72 end
@@ -1,69 +1,32
1 # redMine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
2 # Copyright (C) 2006-2011 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 module WikiHelper
18 module WikiHelper
19
19
20 def wiki_page_options_for_select(pages, selected = nil, parent = nil, level = 0)
20 def wiki_page_options_for_select(pages, selected = nil, parent = nil, level = 0)
21 s = ''
21 s = ''
22 pages.select {|p| p.parent == parent}.each do |page|
22 pages.select {|p| p.parent == parent}.each do |page|
23 attrs = "value='#{page.id}'"
23 attrs = "value='#{page.id}'"
24 attrs << " selected='selected'" if selected == page
24 attrs << " selected='selected'" if selected == page
25 indent = (level > 0) ? ('&nbsp;' * level * 2 + '&#187; ') : nil
25 indent = (level > 0) ? ('&nbsp;' * level * 2 + '&#187; ') : nil
26
26
27 s << "<option #{attrs}>#{indent}#{h page.pretty_title}</option>\n" +
27 s << "<option #{attrs}>#{indent}#{h page.pretty_title}</option>\n" +
28 wiki_page_options_for_select(pages, selected, page, level + 1)
28 wiki_page_options_for_select(pages, selected, page, level + 1)
29 end
29 end
30 s
30 s
31 end
31 end
32
33 def html_diff(wdiff)
34 words = wdiff.words.collect{|word| h(word)}
35 words_add = 0
36 words_del = 0
37 dels = 0
38 del_off = 0
39 wdiff.diff.diffs.each do |diff|
40 add_at = nil
41 add_to = nil
42 del_at = nil
43 deleted = ""
44 diff.each do |change|
45 pos = change[1]
46 if change[0] == "+"
47 add_at = pos + dels unless add_at
48 add_to = pos + dels
49 words_add += 1
50 else
51 del_at = pos unless del_at
52 deleted << ' ' + h(change[2])
53 words_del += 1
54 end
55 end
56 if add_at
57 words[add_at] = '<span class="diff_in">' + words[add_at]
58 words[add_to] = words[add_to] + '</span>'
59 end
60 if del_at
61 words.insert del_at - del_off + dels + words_add, '<span class="diff_out">' + deleted + '</span>'
62 dels += 1
63 del_off += words_del
64 words_del = 0
65 end
66 end
67 simple_format_without_paragraph(words.join(' '))
68 end
69 end
32 end
@@ -1,202 +1,198
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2009 Jean-Philippe Lang
2 # Copyright (C) 2006-2011 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require 'diff'
18 require 'diff'
19 require 'enumerator'
19 require 'enumerator'
20
20
21 class WikiPage < ActiveRecord::Base
21 class WikiPage < ActiveRecord::Base
22 belongs_to :wiki
22 belongs_to :wiki
23 has_one :content, :class_name => 'WikiContent', :foreign_key => 'page_id', :dependent => :destroy
23 has_one :content, :class_name => 'WikiContent', :foreign_key => 'page_id', :dependent => :destroy
24 acts_as_attachable :delete_permission => :delete_wiki_pages_attachments
24 acts_as_attachable :delete_permission => :delete_wiki_pages_attachments
25 acts_as_tree :dependent => :nullify, :order => 'title'
25 acts_as_tree :dependent => :nullify, :order => 'title'
26
26
27 acts_as_watchable
27 acts_as_watchable
28 acts_as_event :title => Proc.new {|o| "#{l(:label_wiki)}: #{o.title}"},
28 acts_as_event :title => Proc.new {|o| "#{l(:label_wiki)}: #{o.title}"},
29 :description => :text,
29 :description => :text,
30 :datetime => :created_on,
30 :datetime => :created_on,
31 :url => Proc.new {|o| {:controller => 'wiki', :action => 'show', :project_id => o.wiki.project, :id => o.title}}
31 :url => Proc.new {|o| {:controller => 'wiki', :action => 'show', :project_id => o.wiki.project, :id => o.title}}
32
32
33 acts_as_searchable :columns => ['title', 'text'],
33 acts_as_searchable :columns => ['title', 'text'],
34 :include => [{:wiki => :project}, :content],
34 :include => [{:wiki => :project}, :content],
35 :project_key => "#{Wiki.table_name}.project_id"
35 :project_key => "#{Wiki.table_name}.project_id"
36
36
37 attr_accessor :redirect_existing_links
37 attr_accessor :redirect_existing_links
38
38
39 validates_presence_of :title
39 validates_presence_of :title
40 validates_format_of :title, :with => /^[^,\.\/\?\;\|\s]*$/
40 validates_format_of :title, :with => /^[^,\.\/\?\;\|\s]*$/
41 validates_uniqueness_of :title, :scope => :wiki_id, :case_sensitive => false
41 validates_uniqueness_of :title, :scope => :wiki_id, :case_sensitive => false
42 validates_associated :content
42 validates_associated :content
43
43
44 # Wiki pages that are protected by default
44 # Wiki pages that are protected by default
45 DEFAULT_PROTECTED_PAGES = %w(sidebar)
45 DEFAULT_PROTECTED_PAGES = %w(sidebar)
46
46
47 def after_initialize
47 def after_initialize
48 if new_record? && DEFAULT_PROTECTED_PAGES.include?(title.to_s.downcase)
48 if new_record? && DEFAULT_PROTECTED_PAGES.include?(title.to_s.downcase)
49 self.protected = true
49 self.protected = true
50 end
50 end
51 end
51 end
52
52
53 def visible?(user=User.current)
53 def visible?(user=User.current)
54 !user.nil? && user.allowed_to?(:view_wiki_pages, project)
54 !user.nil? && user.allowed_to?(:view_wiki_pages, project)
55 end
55 end
56
56
57 def title=(value)
57 def title=(value)
58 value = Wiki.titleize(value)
58 value = Wiki.titleize(value)
59 @previous_title = read_attribute(:title) if @previous_title.blank?
59 @previous_title = read_attribute(:title) if @previous_title.blank?
60 write_attribute(:title, value)
60 write_attribute(:title, value)
61 end
61 end
62
62
63 def before_save
63 def before_save
64 self.title = Wiki.titleize(title)
64 self.title = Wiki.titleize(title)
65 # Manage redirects if the title has changed
65 # Manage redirects if the title has changed
66 if !@previous_title.blank? && (@previous_title != title) && !new_record?
66 if !@previous_title.blank? && (@previous_title != title) && !new_record?
67 # Update redirects that point to the old title
67 # Update redirects that point to the old title
68 wiki.redirects.find_all_by_redirects_to(@previous_title).each do |r|
68 wiki.redirects.find_all_by_redirects_to(@previous_title).each do |r|
69 r.redirects_to = title
69 r.redirects_to = title
70 r.title == r.redirects_to ? r.destroy : r.save
70 r.title == r.redirects_to ? r.destroy : r.save
71 end
71 end
72 # Remove redirects for the new title
72 # Remove redirects for the new title
73 wiki.redirects.find_all_by_title(title).each(&:destroy)
73 wiki.redirects.find_all_by_title(title).each(&:destroy)
74 # Create a redirect to the new title
74 # Create a redirect to the new title
75 wiki.redirects << WikiRedirect.new(:title => @previous_title, :redirects_to => title) unless redirect_existing_links == "0"
75 wiki.redirects << WikiRedirect.new(:title => @previous_title, :redirects_to => title) unless redirect_existing_links == "0"
76 @previous_title = nil
76 @previous_title = nil
77 end
77 end
78 end
78 end
79
79
80 def before_destroy
80 def before_destroy
81 # Remove redirects to this page
81 # Remove redirects to this page
82 wiki.redirects.find_all_by_redirects_to(title).each(&:destroy)
82 wiki.redirects.find_all_by_redirects_to(title).each(&:destroy)
83 end
83 end
84
84
85 def pretty_title
85 def pretty_title
86 WikiPage.pretty_title(title)
86 WikiPage.pretty_title(title)
87 end
87 end
88
88
89 def content_for_version(version=nil)
89 def content_for_version(version=nil)
90 result = content.versions.find_by_version(version.to_i) if version
90 result = content.versions.find_by_version(version.to_i) if version
91 result ||= content
91 result ||= content
92 result
92 result
93 end
93 end
94
94
95 def diff(version_to=nil, version_from=nil)
95 def diff(version_to=nil, version_from=nil)
96 version_to = version_to ? version_to.to_i : self.content.version
96 version_to = version_to ? version_to.to_i : self.content.version
97 version_from = version_from ? version_from.to_i : version_to - 1
97 version_from = version_from ? version_from.to_i : version_to - 1
98 version_to, version_from = version_from, version_to unless version_from < version_to
98 version_to, version_from = version_from, version_to unless version_from < version_to
99
99
100 content_to = content.versions.find_by_version(version_to)
100 content_to = content.versions.find_by_version(version_to)
101 content_from = content.versions.find_by_version(version_from)
101 content_from = content.versions.find_by_version(version_from)
102
102
103 (content_to && content_from) ? WikiDiff.new(content_to, content_from) : nil
103 (content_to && content_from) ? WikiDiff.new(content_to, content_from) : nil
104 end
104 end
105
105
106 def annotate(version=nil)
106 def annotate(version=nil)
107 version = version ? version.to_i : self.content.version
107 version = version ? version.to_i : self.content.version
108 c = content.versions.find_by_version(version)
108 c = content.versions.find_by_version(version)
109 c ? WikiAnnotate.new(c) : nil
109 c ? WikiAnnotate.new(c) : nil
110 end
110 end
111
111
112 def self.pretty_title(str)
112 def self.pretty_title(str)
113 (str && str.is_a?(String)) ? str.tr('_', ' ') : str
113 (str && str.is_a?(String)) ? str.tr('_', ' ') : str
114 end
114 end
115
115
116 def project
116 def project
117 wiki.project
117 wiki.project
118 end
118 end
119
119
120 def text
120 def text
121 content.text if content
121 content.text if content
122 end
122 end
123
123
124 # Returns true if usr is allowed to edit the page, otherwise false
124 # Returns true if usr is allowed to edit the page, otherwise false
125 def editable_by?(usr)
125 def editable_by?(usr)
126 !protected? || usr.allowed_to?(:protect_wiki_pages, wiki.project)
126 !protected? || usr.allowed_to?(:protect_wiki_pages, wiki.project)
127 end
127 end
128
128
129 def attachments_deletable?(usr=User.current)
129 def attachments_deletable?(usr=User.current)
130 editable_by?(usr) && super(usr)
130 editable_by?(usr) && super(usr)
131 end
131 end
132
132
133 def parent_title
133 def parent_title
134 @parent_title || (self.parent && self.parent.pretty_title)
134 @parent_title || (self.parent && self.parent.pretty_title)
135 end
135 end
136
136
137 def parent_title=(t)
137 def parent_title=(t)
138 @parent_title = t
138 @parent_title = t
139 parent_page = t.blank? ? nil : self.wiki.find_page(t)
139 parent_page = t.blank? ? nil : self.wiki.find_page(t)
140 self.parent = parent_page
140 self.parent = parent_page
141 end
141 end
142
142
143 protected
143 protected
144
144
145 def validate
145 def validate
146 errors.add(:parent_title, :invalid) if !@parent_title.blank? && parent.nil?
146 errors.add(:parent_title, :invalid) if !@parent_title.blank? && parent.nil?
147 errors.add(:parent_title, :circular_dependency) if parent && (parent == self || parent.ancestors.include?(self))
147 errors.add(:parent_title, :circular_dependency) if parent && (parent == self || parent.ancestors.include?(self))
148 errors.add(:parent_title, :not_same_project) if parent && (parent.wiki_id != wiki_id)
148 errors.add(:parent_title, :not_same_project) if parent && (parent.wiki_id != wiki_id)
149 end
149 end
150 end
150 end
151
151
152 class WikiDiff
152 class WikiDiff < Redmine::Helpers::Diff
153 attr_reader :diff, :words, :content_to, :content_from
153 attr_reader :content_to, :content_from
154
154
155 def initialize(content_to, content_from)
155 def initialize(content_to, content_from)
156 @content_to = content_to
156 @content_to = content_to
157 @content_from = content_from
157 @content_from = content_from
158 @words = content_to.text.split(/(\s+)/)
158 super(content_to.text, content_from.text)
159 @words = @words.select {|word| word != ' '}
160 words_from = content_from.text.split(/(\s+)/)
161 words_from = words_from.select {|word| word != ' '}
162 @diff = words_from.diff @words
163 end
159 end
164 end
160 end
165
161
166 class WikiAnnotate
162 class WikiAnnotate
167 attr_reader :lines, :content
163 attr_reader :lines, :content
168
164
169 def initialize(content)
165 def initialize(content)
170 @content = content
166 @content = content
171 current = content
167 current = content
172 current_lines = current.text.split(/\r?\n/)
168 current_lines = current.text.split(/\r?\n/)
173 @lines = current_lines.collect {|t| [nil, nil, t]}
169 @lines = current_lines.collect {|t| [nil, nil, t]}
174 positions = []
170 positions = []
175 current_lines.size.times {|i| positions << i}
171 current_lines.size.times {|i| positions << i}
176 while (current.previous)
172 while (current.previous)
177 d = current.previous.text.split(/\r?\n/).diff(current.text.split(/\r?\n/)).diffs.flatten
173 d = current.previous.text.split(/\r?\n/).diff(current.text.split(/\r?\n/)).diffs.flatten
178 d.each_slice(3) do |s|
174 d.each_slice(3) do |s|
179 sign, line = s[0], s[1]
175 sign, line = s[0], s[1]
180 if sign == '+' && positions[line] && positions[line] != -1
176 if sign == '+' && positions[line] && positions[line] != -1
181 if @lines[positions[line]][0].nil?
177 if @lines[positions[line]][0].nil?
182 @lines[positions[line]][0] = current.version
178 @lines[positions[line]][0] = current.version
183 @lines[positions[line]][1] = current.author
179 @lines[positions[line]][1] = current.author
184 end
180 end
185 end
181 end
186 end
182 end
187 d.each_slice(3) do |s|
183 d.each_slice(3) do |s|
188 sign, line = s[0], s[1]
184 sign, line = s[0], s[1]
189 if sign == '-'
185 if sign == '-'
190 positions.insert(line, -1)
186 positions.insert(line, -1)
191 else
187 else
192 positions[line] = nil
188 positions[line] = nil
193 end
189 end
194 end
190 end
195 positions.compact!
191 positions.compact!
196 # Stop if every line is annotated
192 # Stop if every line is annotated
197 break unless @lines.detect { |line| line[0].nil? }
193 break unless @lines.detect { |line| line[0].nil? }
198 current = current.previous
194 current = current.previous
199 end
195 end
200 @lines.each { |line| line[0] ||= current.version }
196 @lines.each { |line| line[0] ||= current.version }
201 end
197 end
202 end
198 end
@@ -1,17 +1,17
1 <div class="contextual">
1 <div class="contextual">
2 <%= link_to(l(:label_history), {:action => 'history', :id => @page.title}, :class => 'icon icon-history') %>
2 <%= link_to(l(:label_history), {:action => 'history', :id => @page.title}, :class => 'icon icon-history') %>
3 </div>
3 </div>
4
4
5 <h2><%= @page.pretty_title %></h2>
5 <h2><%= @page.pretty_title %></h2>
6
6
7 <p>
7 <p>
8 <%= l(:label_version) %> <%= link_to @diff.content_from.version, :action => 'show', :id => @page.title, :project_id => @page.project, :version => @diff.content_from.version %>
8 <%= l(:label_version) %> <%= link_to @diff.content_from.version, :action => 'show', :id => @page.title, :project_id => @page.project, :version => @diff.content_from.version %>
9 <em>(<%= @diff.content_from.author ? @diff.content_from.author.name : "anonyme" %>, <%= format_time(@diff.content_from.updated_on) %>)</em>
9 <em>(<%= @diff.content_from.author ? @diff.content_from.author.name : "anonyme" %>, <%= format_time(@diff.content_from.updated_on) %>)</em>
10 &#8594;
10 &#8594;
11 <%= l(:label_version) %> <%= link_to @diff.content_to.version, :action => 'show', :id => @page.title, :project_id => @page.project, :version => @diff.content_to.version %>/<%= @page.content.version %>
11 <%= l(:label_version) %> <%= link_to @diff.content_to.version, :action => 'show', :id => @page.title, :project_id => @page.project, :version => @diff.content_to.version %>/<%= @page.content.version %>
12 <em>(<%= @diff.content_to.author ? @diff.content_to.author.name : "anonyme" %>, <%= format_time(@diff.content_to.updated_on) %>)</em>
12 <em>(<%= @diff.content_to.author ? @diff.content_to.author.name : "anonyme" %>, <%= format_time(@diff.content_to.updated_on) %>)</em>
13 </p>
13 </p>
14
14
15 <hr />
15 <div class="text-diff">
16
16 <%= @diff.to_html %>
17 <%= html_diff(@diff) %>
17 </div>
General Comments 0
You need to be logged in to leave comments. Login now