##// END OF EJS Templates
Fixed that Redmine links should not be parsed inside links (#18301)....
Jean-Philippe Lang -
r13214:eac3b2b27def
parent child
Show More
@@ -1,1352 +1,1356
1 # encoding: utf-8
1 # encoding: utf-8
2 #
2 #
3 # Redmine - project management software
3 # Redmine - project management software
4 # Copyright (C) 2006-2014 Jean-Philippe Lang
4 # Copyright (C) 2006-2014 Jean-Philippe Lang
5 #
5 #
6 # This program is free software; you can redistribute it and/or
6 # This program is free software; you can redistribute it and/or
7 # modify it under the terms of the GNU General Public License
7 # modify it under the terms of the GNU General Public License
8 # as published by the Free Software Foundation; either version 2
8 # as published by the Free Software Foundation; either version 2
9 # of the License, or (at your option) any later version.
9 # of the License, or (at your option) any later version.
10 #
10 #
11 # This program is distributed in the hope that it will be useful,
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
14 # GNU General Public License for more details.
15 #
15 #
16 # You should have received a copy of the GNU General Public License
16 # You should have received a copy of the GNU General Public License
17 # along with this program; if not, write to the Free Software
17 # along with this program; if not, write to the Free Software
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19
19
20 require 'forwardable'
20 require 'forwardable'
21 require 'cgi'
21 require 'cgi'
22
22
23 module ApplicationHelper
23 module ApplicationHelper
24 include Redmine::WikiFormatting::Macros::Definitions
24 include Redmine::WikiFormatting::Macros::Definitions
25 include Redmine::I18n
25 include Redmine::I18n
26 include GravatarHelper::PublicMethods
26 include GravatarHelper::PublicMethods
27 include Redmine::Pagination::Helper
27 include Redmine::Pagination::Helper
28
28
29 extend Forwardable
29 extend Forwardable
30 def_delegators :wiki_helper, :wikitoolbar_for, :heads_for_wiki_formatter
30 def_delegators :wiki_helper, :wikitoolbar_for, :heads_for_wiki_formatter
31
31
32 # Return true if user is authorized for controller/action, otherwise false
32 # Return true if user is authorized for controller/action, otherwise false
33 def authorize_for(controller, action)
33 def authorize_for(controller, action)
34 User.current.allowed_to?({:controller => controller, :action => action}, @project)
34 User.current.allowed_to?({:controller => controller, :action => action}, @project)
35 end
35 end
36
36
37 # Display a link if user is authorized
37 # Display a link if user is authorized
38 #
38 #
39 # @param [String] name Anchor text (passed to link_to)
39 # @param [String] name Anchor text (passed to link_to)
40 # @param [Hash] options Hash params. This will checked by authorize_for to see if the user is authorized
40 # @param [Hash] options Hash params. This will checked by authorize_for to see if the user is authorized
41 # @param [optional, Hash] html_options Options passed to link_to
41 # @param [optional, Hash] html_options Options passed to link_to
42 # @param [optional, Hash] parameters_for_method_reference Extra parameters for link_to
42 # @param [optional, Hash] parameters_for_method_reference Extra parameters for link_to
43 def link_to_if_authorized(name, options = {}, html_options = nil, *parameters_for_method_reference)
43 def link_to_if_authorized(name, options = {}, html_options = nil, *parameters_for_method_reference)
44 link_to(name, options, html_options, *parameters_for_method_reference) if authorize_for(options[:controller] || params[:controller], options[:action])
44 link_to(name, options, html_options, *parameters_for_method_reference) if authorize_for(options[:controller] || params[:controller], options[:action])
45 end
45 end
46
46
47 # Displays a link to user's account page if active
47 # Displays a link to user's account page if active
48 def link_to_user(user, options={})
48 def link_to_user(user, options={})
49 if user.is_a?(User)
49 if user.is_a?(User)
50 name = h(user.name(options[:format]))
50 name = h(user.name(options[:format]))
51 if user.active? || (User.current.admin? && user.logged?)
51 if user.active? || (User.current.admin? && user.logged?)
52 link_to name, user_path(user), :class => user.css_classes
52 link_to name, user_path(user), :class => user.css_classes
53 else
53 else
54 name
54 name
55 end
55 end
56 else
56 else
57 h(user.to_s)
57 h(user.to_s)
58 end
58 end
59 end
59 end
60
60
61 # Displays a link to +issue+ with its subject.
61 # Displays a link to +issue+ with its subject.
62 # Examples:
62 # Examples:
63 #
63 #
64 # link_to_issue(issue) # => Defect #6: This is the subject
64 # link_to_issue(issue) # => Defect #6: This is the subject
65 # link_to_issue(issue, :truncate => 6) # => Defect #6: This i...
65 # link_to_issue(issue, :truncate => 6) # => Defect #6: This i...
66 # link_to_issue(issue, :subject => false) # => Defect #6
66 # link_to_issue(issue, :subject => false) # => Defect #6
67 # link_to_issue(issue, :project => true) # => Foo - Defect #6
67 # link_to_issue(issue, :project => true) # => Foo - Defect #6
68 # link_to_issue(issue, :subject => false, :tracker => false) # => #6
68 # link_to_issue(issue, :subject => false, :tracker => false) # => #6
69 #
69 #
70 def link_to_issue(issue, options={})
70 def link_to_issue(issue, options={})
71 title = nil
71 title = nil
72 subject = nil
72 subject = nil
73 text = options[:tracker] == false ? "##{issue.id}" : "#{issue.tracker} ##{issue.id}"
73 text = options[:tracker] == false ? "##{issue.id}" : "#{issue.tracker} ##{issue.id}"
74 if options[:subject] == false
74 if options[:subject] == false
75 title = issue.subject.truncate(60)
75 title = issue.subject.truncate(60)
76 else
76 else
77 subject = issue.subject
77 subject = issue.subject
78 if truncate_length = options[:truncate]
78 if truncate_length = options[:truncate]
79 subject = subject.truncate(truncate_length)
79 subject = subject.truncate(truncate_length)
80 end
80 end
81 end
81 end
82 only_path = options[:only_path].nil? ? true : options[:only_path]
82 only_path = options[:only_path].nil? ? true : options[:only_path]
83 s = link_to(text, issue_path(issue, :only_path => only_path),
83 s = link_to(text, issue_path(issue, :only_path => only_path),
84 :class => issue.css_classes, :title => title)
84 :class => issue.css_classes, :title => title)
85 s << h(": #{subject}") if subject
85 s << h(": #{subject}") if subject
86 s = h("#{issue.project} - ") + s if options[:project]
86 s = h("#{issue.project} - ") + s if options[:project]
87 s
87 s
88 end
88 end
89
89
90 # Generates a link to an attachment.
90 # Generates a link to an attachment.
91 # Options:
91 # Options:
92 # * :text - Link text (default to attachment filename)
92 # * :text - Link text (default to attachment filename)
93 # * :download - Force download (default: false)
93 # * :download - Force download (default: false)
94 def link_to_attachment(attachment, options={})
94 def link_to_attachment(attachment, options={})
95 text = options.delete(:text) || attachment.filename
95 text = options.delete(:text) || attachment.filename
96 route_method = options.delete(:download) ? :download_named_attachment_path : :named_attachment_path
96 route_method = options.delete(:download) ? :download_named_attachment_path : :named_attachment_path
97 html_options = options.slice!(:only_path)
97 html_options = options.slice!(:only_path)
98 url = send(route_method, attachment, attachment.filename, options)
98 url = send(route_method, attachment, attachment.filename, options)
99 link_to text, url, html_options
99 link_to text, url, html_options
100 end
100 end
101
101
102 # Generates a link to a SCM revision
102 # Generates a link to a SCM revision
103 # Options:
103 # Options:
104 # * :text - Link text (default to the formatted revision)
104 # * :text - Link text (default to the formatted revision)
105 def link_to_revision(revision, repository, options={})
105 def link_to_revision(revision, repository, options={})
106 if repository.is_a?(Project)
106 if repository.is_a?(Project)
107 repository = repository.repository
107 repository = repository.repository
108 end
108 end
109 text = options.delete(:text) || format_revision(revision)
109 text = options.delete(:text) || format_revision(revision)
110 rev = revision.respond_to?(:identifier) ? revision.identifier : revision
110 rev = revision.respond_to?(:identifier) ? revision.identifier : revision
111 link_to(
111 link_to(
112 h(text),
112 h(text),
113 {:controller => 'repositories', :action => 'revision', :id => repository.project, :repository_id => repository.identifier_param, :rev => rev},
113 {:controller => 'repositories', :action => 'revision', :id => repository.project, :repository_id => repository.identifier_param, :rev => rev},
114 :title => l(:label_revision_id, format_revision(revision))
114 :title => l(:label_revision_id, format_revision(revision))
115 )
115 )
116 end
116 end
117
117
118 # Generates a link to a message
118 # Generates a link to a message
119 def link_to_message(message, options={}, html_options = nil)
119 def link_to_message(message, options={}, html_options = nil)
120 link_to(
120 link_to(
121 message.subject.truncate(60),
121 message.subject.truncate(60),
122 board_message_path(message.board_id, message.parent_id || message.id, {
122 board_message_path(message.board_id, message.parent_id || message.id, {
123 :r => (message.parent_id && message.id),
123 :r => (message.parent_id && message.id),
124 :anchor => (message.parent_id ? "message-#{message.id}" : nil)
124 :anchor => (message.parent_id ? "message-#{message.id}" : nil)
125 }.merge(options)),
125 }.merge(options)),
126 html_options
126 html_options
127 )
127 )
128 end
128 end
129
129
130 # Generates a link to a project if active
130 # Generates a link to a project if active
131 # Examples:
131 # Examples:
132 #
132 #
133 # link_to_project(project) # => link to the specified project overview
133 # link_to_project(project) # => link to the specified project overview
134 # link_to_project(project, {:only_path => false}, :class => "project") # => 3rd arg adds html options
134 # link_to_project(project, {:only_path => false}, :class => "project") # => 3rd arg adds html options
135 # link_to_project(project, {}, :class => "project") # => html options with default url (project overview)
135 # link_to_project(project, {}, :class => "project") # => html options with default url (project overview)
136 #
136 #
137 def link_to_project(project, options={}, html_options = nil)
137 def link_to_project(project, options={}, html_options = nil)
138 if project.archived?
138 if project.archived?
139 h(project.name)
139 h(project.name)
140 else
140 else
141 link_to project.name, project_path(project, options), html_options
141 link_to project.name, project_path(project, options), html_options
142 end
142 end
143 end
143 end
144
144
145 # Generates a link to a project settings if active
145 # Generates a link to a project settings if active
146 def link_to_project_settings(project, options={}, html_options=nil)
146 def link_to_project_settings(project, options={}, html_options=nil)
147 if project.active?
147 if project.active?
148 link_to project.name, settings_project_path(project, options), html_options
148 link_to project.name, settings_project_path(project, options), html_options
149 elsif project.archived?
149 elsif project.archived?
150 h(project.name)
150 h(project.name)
151 else
151 else
152 link_to project.name, project_path(project, options), html_options
152 link_to project.name, project_path(project, options), html_options
153 end
153 end
154 end
154 end
155
155
156 # Generates a link to a version
156 # Generates a link to a version
157 def link_to_version(version, options = {})
157 def link_to_version(version, options = {})
158 return '' unless version && version.is_a?(Version)
158 return '' unless version && version.is_a?(Version)
159 options = {:title => format_date(version.effective_date)}.merge(options)
159 options = {:title => format_date(version.effective_date)}.merge(options)
160 link_to_if version.visible?, format_version_name(version), version_path(version), options
160 link_to_if version.visible?, format_version_name(version), version_path(version), options
161 end
161 end
162
162
163 # Helper that formats object for html or text rendering
163 # Helper that formats object for html or text rendering
164 def format_object(object, html=true, &block)
164 def format_object(object, html=true, &block)
165 if block_given?
165 if block_given?
166 object = yield object
166 object = yield object
167 end
167 end
168 case object.class.name
168 case object.class.name
169 when 'Array'
169 when 'Array'
170 object.map {|o| format_object(o, html)}.join(', ').html_safe
170 object.map {|o| format_object(o, html)}.join(', ').html_safe
171 when 'Time'
171 when 'Time'
172 format_time(object)
172 format_time(object)
173 when 'Date'
173 when 'Date'
174 format_date(object)
174 format_date(object)
175 when 'Fixnum'
175 when 'Fixnum'
176 object.to_s
176 object.to_s
177 when 'Float'
177 when 'Float'
178 sprintf "%.2f", object
178 sprintf "%.2f", object
179 when 'User'
179 when 'User'
180 html ? link_to_user(object) : object.to_s
180 html ? link_to_user(object) : object.to_s
181 when 'Project'
181 when 'Project'
182 html ? link_to_project(object) : object.to_s
182 html ? link_to_project(object) : object.to_s
183 when 'Version'
183 when 'Version'
184 html ? link_to_version(object) : object.to_s
184 html ? link_to_version(object) : object.to_s
185 when 'TrueClass'
185 when 'TrueClass'
186 l(:general_text_Yes)
186 l(:general_text_Yes)
187 when 'FalseClass'
187 when 'FalseClass'
188 l(:general_text_No)
188 l(:general_text_No)
189 when 'Issue'
189 when 'Issue'
190 object.visible? && html ? link_to_issue(object) : "##{object.id}"
190 object.visible? && html ? link_to_issue(object) : "##{object.id}"
191 when 'CustomValue', 'CustomFieldValue'
191 when 'CustomValue', 'CustomFieldValue'
192 if object.custom_field
192 if object.custom_field
193 f = object.custom_field.format.formatted_custom_value(self, object, html)
193 f = object.custom_field.format.formatted_custom_value(self, object, html)
194 if f.nil? || f.is_a?(String)
194 if f.nil? || f.is_a?(String)
195 f
195 f
196 else
196 else
197 format_object(f, html, &block)
197 format_object(f, html, &block)
198 end
198 end
199 else
199 else
200 object.value.to_s
200 object.value.to_s
201 end
201 end
202 else
202 else
203 html ? h(object) : object.to_s
203 html ? h(object) : object.to_s
204 end
204 end
205 end
205 end
206
206
207 def wiki_page_path(page, options={})
207 def wiki_page_path(page, options={})
208 url_for({:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title}.merge(options))
208 url_for({:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title}.merge(options))
209 end
209 end
210
210
211 def thumbnail_tag(attachment)
211 def thumbnail_tag(attachment)
212 link_to image_tag(thumbnail_path(attachment)),
212 link_to image_tag(thumbnail_path(attachment)),
213 named_attachment_path(attachment, attachment.filename),
213 named_attachment_path(attachment, attachment.filename),
214 :title => attachment.filename
214 :title => attachment.filename
215 end
215 end
216
216
217 def toggle_link(name, id, options={})
217 def toggle_link(name, id, options={})
218 onclick = "$('##{id}').toggle(); "
218 onclick = "$('##{id}').toggle(); "
219 onclick << (options[:focus] ? "$('##{options[:focus]}').focus(); " : "this.blur(); ")
219 onclick << (options[:focus] ? "$('##{options[:focus]}').focus(); " : "this.blur(); ")
220 onclick << "return false;"
220 onclick << "return false;"
221 link_to(name, "#", :onclick => onclick)
221 link_to(name, "#", :onclick => onclick)
222 end
222 end
223
223
224 def image_to_function(name, function, html_options = {})
224 def image_to_function(name, function, html_options = {})
225 html_options.symbolize_keys!
225 html_options.symbolize_keys!
226 tag(:input, html_options.merge({
226 tag(:input, html_options.merge({
227 :type => "image", :src => image_path(name),
227 :type => "image", :src => image_path(name),
228 :onclick => (html_options[:onclick] ? "#{html_options[:onclick]}; " : "") + "#{function};"
228 :onclick => (html_options[:onclick] ? "#{html_options[:onclick]}; " : "") + "#{function};"
229 }))
229 }))
230 end
230 end
231
231
232 def format_activity_title(text)
232 def format_activity_title(text)
233 h(truncate_single_line_raw(text, 100))
233 h(truncate_single_line_raw(text, 100))
234 end
234 end
235
235
236 def format_activity_day(date)
236 def format_activity_day(date)
237 date == User.current.today ? l(:label_today).titleize : format_date(date)
237 date == User.current.today ? l(:label_today).titleize : format_date(date)
238 end
238 end
239
239
240 def format_activity_description(text)
240 def format_activity_description(text)
241 h(text.to_s.truncate(120).gsub(%r{[\r\n]*<(pre|code)>.*$}m, '...')
241 h(text.to_s.truncate(120).gsub(%r{[\r\n]*<(pre|code)>.*$}m, '...')
242 ).gsub(/[\r\n]+/, "<br />").html_safe
242 ).gsub(/[\r\n]+/, "<br />").html_safe
243 end
243 end
244
244
245 def format_version_name(version)
245 def format_version_name(version)
246 if !version.shared? || version.project == @project
246 if !version.shared? || version.project == @project
247 h(version)
247 h(version)
248 else
248 else
249 h("#{version.project} - #{version}")
249 h("#{version.project} - #{version}")
250 end
250 end
251 end
251 end
252
252
253 def due_date_distance_in_words(date)
253 def due_date_distance_in_words(date)
254 if date
254 if date
255 l((date < Date.today ? :label_roadmap_overdue : :label_roadmap_due_in), distance_of_date_in_words(Date.today, date))
255 l((date < Date.today ? :label_roadmap_overdue : :label_roadmap_due_in), distance_of_date_in_words(Date.today, date))
256 end
256 end
257 end
257 end
258
258
259 # Renders a tree of projects as a nested set of unordered lists
259 # Renders a tree of projects as a nested set of unordered lists
260 # The given collection may be a subset of the whole project tree
260 # The given collection may be a subset of the whole project tree
261 # (eg. some intermediate nodes are private and can not be seen)
261 # (eg. some intermediate nodes are private and can not be seen)
262 def render_project_nested_lists(projects, &block)
262 def render_project_nested_lists(projects, &block)
263 s = ''
263 s = ''
264 if projects.any?
264 if projects.any?
265 ancestors = []
265 ancestors = []
266 original_project = @project
266 original_project = @project
267 projects.sort_by(&:lft).each do |project|
267 projects.sort_by(&:lft).each do |project|
268 # set the project environment to please macros.
268 # set the project environment to please macros.
269 @project = project
269 @project = project
270 if (ancestors.empty? || project.is_descendant_of?(ancestors.last))
270 if (ancestors.empty? || project.is_descendant_of?(ancestors.last))
271 s << "<ul class='projects #{ ancestors.empty? ? 'root' : nil}'>\n"
271 s << "<ul class='projects #{ ancestors.empty? ? 'root' : nil}'>\n"
272 else
272 else
273 ancestors.pop
273 ancestors.pop
274 s << "</li>"
274 s << "</li>"
275 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
275 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
276 ancestors.pop
276 ancestors.pop
277 s << "</ul></li>\n"
277 s << "</ul></li>\n"
278 end
278 end
279 end
279 end
280 classes = (ancestors.empty? ? 'root' : 'child')
280 classes = (ancestors.empty? ? 'root' : 'child')
281 s << "<li class='#{classes}'><div class='#{classes}'>"
281 s << "<li class='#{classes}'><div class='#{classes}'>"
282 s << h(block_given? ? capture(project, &block) : project.name)
282 s << h(block_given? ? capture(project, &block) : project.name)
283 s << "</div>\n"
283 s << "</div>\n"
284 ancestors << project
284 ancestors << project
285 end
285 end
286 s << ("</li></ul>\n" * ancestors.size)
286 s << ("</li></ul>\n" * ancestors.size)
287 @project = original_project
287 @project = original_project
288 end
288 end
289 s.html_safe
289 s.html_safe
290 end
290 end
291
291
292 def render_page_hierarchy(pages, node=nil, options={})
292 def render_page_hierarchy(pages, node=nil, options={})
293 content = ''
293 content = ''
294 if pages[node]
294 if pages[node]
295 content << "<ul class=\"pages-hierarchy\">\n"
295 content << "<ul class=\"pages-hierarchy\">\n"
296 pages[node].each do |page|
296 pages[node].each do |page|
297 content << "<li>"
297 content << "<li>"
298 content << link_to(h(page.pretty_title), {:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title, :version => nil},
298 content << link_to(h(page.pretty_title), {:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title, :version => nil},
299 :title => (options[:timestamp] && page.updated_on ? l(:label_updated_time, distance_of_time_in_words(Time.now, page.updated_on)) : nil))
299 :title => (options[:timestamp] && page.updated_on ? l(:label_updated_time, distance_of_time_in_words(Time.now, page.updated_on)) : nil))
300 content << "\n" + render_page_hierarchy(pages, page.id, options) if pages[page.id]
300 content << "\n" + render_page_hierarchy(pages, page.id, options) if pages[page.id]
301 content << "</li>\n"
301 content << "</li>\n"
302 end
302 end
303 content << "</ul>\n"
303 content << "</ul>\n"
304 end
304 end
305 content.html_safe
305 content.html_safe
306 end
306 end
307
307
308 # Renders flash messages
308 # Renders flash messages
309 def render_flash_messages
309 def render_flash_messages
310 s = ''
310 s = ''
311 flash.each do |k,v|
311 flash.each do |k,v|
312 s << content_tag('div', v.html_safe, :class => "flash #{k}", :id => "flash_#{k}")
312 s << content_tag('div', v.html_safe, :class => "flash #{k}", :id => "flash_#{k}")
313 end
313 end
314 s.html_safe
314 s.html_safe
315 end
315 end
316
316
317 # Renders tabs and their content
317 # Renders tabs and their content
318 def render_tabs(tabs, selected=params[:tab])
318 def render_tabs(tabs, selected=params[:tab])
319 if tabs.any?
319 if tabs.any?
320 unless tabs.detect {|tab| tab[:name] == selected}
320 unless tabs.detect {|tab| tab[:name] == selected}
321 selected = nil
321 selected = nil
322 end
322 end
323 selected ||= tabs.first[:name]
323 selected ||= tabs.first[:name]
324 render :partial => 'common/tabs', :locals => {:tabs => tabs, :selected_tab => selected}
324 render :partial => 'common/tabs', :locals => {:tabs => tabs, :selected_tab => selected}
325 else
325 else
326 content_tag 'p', l(:label_no_data), :class => "nodata"
326 content_tag 'p', l(:label_no_data), :class => "nodata"
327 end
327 end
328 end
328 end
329
329
330 # Renders the project quick-jump box
330 # Renders the project quick-jump box
331 def render_project_jump_box
331 def render_project_jump_box
332 return unless User.current.logged?
332 return unless User.current.logged?
333 projects = User.current.memberships.collect(&:project).compact.select(&:active?).uniq
333 projects = User.current.memberships.collect(&:project).compact.select(&:active?).uniq
334 if projects.any?
334 if projects.any?
335 options =
335 options =
336 ("<option value=''>#{ l(:label_jump_to_a_project) }</option>" +
336 ("<option value=''>#{ l(:label_jump_to_a_project) }</option>" +
337 '<option value="" disabled="disabled">---</option>').html_safe
337 '<option value="" disabled="disabled">---</option>').html_safe
338
338
339 options << project_tree_options_for_select(projects, :selected => @project) do |p|
339 options << project_tree_options_for_select(projects, :selected => @project) do |p|
340 { :value => project_path(:id => p, :jump => current_menu_item) }
340 { :value => project_path(:id => p, :jump => current_menu_item) }
341 end
341 end
342
342
343 select_tag('project_quick_jump_box', options, :onchange => 'if (this.value != \'\') { window.location = this.value; }')
343 select_tag('project_quick_jump_box', options, :onchange => 'if (this.value != \'\') { window.location = this.value; }')
344 end
344 end
345 end
345 end
346
346
347 def project_tree_options_for_select(projects, options = {})
347 def project_tree_options_for_select(projects, options = {})
348 s = ''.html_safe
348 s = ''.html_safe
349 if options[:include_blank]
349 if options[:include_blank]
350 s << content_tag('option', '&nbsp;'.html_safe, :value => '')
350 s << content_tag('option', '&nbsp;'.html_safe, :value => '')
351 end
351 end
352 project_tree(projects) do |project, level|
352 project_tree(projects) do |project, level|
353 name_prefix = (level > 0 ? '&nbsp;' * 2 * level + '&#187; ' : '').html_safe
353 name_prefix = (level > 0 ? '&nbsp;' * 2 * level + '&#187; ' : '').html_safe
354 tag_options = {:value => project.id}
354 tag_options = {:value => project.id}
355 if project == options[:selected] || (options[:selected].respond_to?(:include?) && options[:selected].include?(project))
355 if project == options[:selected] || (options[:selected].respond_to?(:include?) && options[:selected].include?(project))
356 tag_options[:selected] = 'selected'
356 tag_options[:selected] = 'selected'
357 else
357 else
358 tag_options[:selected] = nil
358 tag_options[:selected] = nil
359 end
359 end
360 tag_options.merge!(yield(project)) if block_given?
360 tag_options.merge!(yield(project)) if block_given?
361 s << content_tag('option', name_prefix + h(project), tag_options)
361 s << content_tag('option', name_prefix + h(project), tag_options)
362 end
362 end
363 s.html_safe
363 s.html_safe
364 end
364 end
365
365
366 # Yields the given block for each project with its level in the tree
366 # Yields the given block for each project with its level in the tree
367 #
367 #
368 # Wrapper for Project#project_tree
368 # Wrapper for Project#project_tree
369 def project_tree(projects, &block)
369 def project_tree(projects, &block)
370 Project.project_tree(projects, &block)
370 Project.project_tree(projects, &block)
371 end
371 end
372
372
373 def principals_check_box_tags(name, principals)
373 def principals_check_box_tags(name, principals)
374 s = ''
374 s = ''
375 principals.each do |principal|
375 principals.each do |principal|
376 s << "<label>#{ check_box_tag name, principal.id, false, :id => nil } #{h principal}</label>\n"
376 s << "<label>#{ check_box_tag name, principal.id, false, :id => nil } #{h principal}</label>\n"
377 end
377 end
378 s.html_safe
378 s.html_safe
379 end
379 end
380
380
381 # Returns a string for users/groups option tags
381 # Returns a string for users/groups option tags
382 def principals_options_for_select(collection, selected=nil)
382 def principals_options_for_select(collection, selected=nil)
383 s = ''
383 s = ''
384 if collection.include?(User.current)
384 if collection.include?(User.current)
385 s << content_tag('option', "<< #{l(:label_me)} >>", :value => User.current.id)
385 s << content_tag('option', "<< #{l(:label_me)} >>", :value => User.current.id)
386 end
386 end
387 groups = ''
387 groups = ''
388 collection.sort.each do |element|
388 collection.sort.each do |element|
389 selected_attribute = ' selected="selected"' if option_value_selected?(element, selected) || element.id.to_s == selected
389 selected_attribute = ' selected="selected"' if option_value_selected?(element, selected) || element.id.to_s == selected
390 (element.is_a?(Group) ? groups : s) << %(<option value="#{element.id}"#{selected_attribute}>#{h element.name}</option>)
390 (element.is_a?(Group) ? groups : s) << %(<option value="#{element.id}"#{selected_attribute}>#{h element.name}</option>)
391 end
391 end
392 unless groups.empty?
392 unless groups.empty?
393 s << %(<optgroup label="#{h(l(:label_group_plural))}">#{groups}</optgroup>)
393 s << %(<optgroup label="#{h(l(:label_group_plural))}">#{groups}</optgroup>)
394 end
394 end
395 s.html_safe
395 s.html_safe
396 end
396 end
397
397
398 # Options for the new membership projects combo-box
398 # Options for the new membership projects combo-box
399 def options_for_membership_project_select(principal, projects)
399 def options_for_membership_project_select(principal, projects)
400 options = content_tag('option', "--- #{l(:actionview_instancetag_blank_option)} ---")
400 options = content_tag('option', "--- #{l(:actionview_instancetag_blank_option)} ---")
401 options << project_tree_options_for_select(projects) do |p|
401 options << project_tree_options_for_select(projects) do |p|
402 {:disabled => principal.projects.to_a.include?(p)}
402 {:disabled => principal.projects.to_a.include?(p)}
403 end
403 end
404 options
404 options
405 end
405 end
406
406
407 def option_tag(name, text, value, selected=nil, options={})
407 def option_tag(name, text, value, selected=nil, options={})
408 content_tag 'option', value, options.merge(:value => value, :selected => (value == selected))
408 content_tag 'option', value, options.merge(:value => value, :selected => (value == selected))
409 end
409 end
410
410
411 # Truncates and returns the string as a single line
411 # Truncates and returns the string as a single line
412 def truncate_single_line(string, *args)
412 def truncate_single_line(string, *args)
413 ActiveSupport::Deprecation.warn(
413 ActiveSupport::Deprecation.warn(
414 "ApplicationHelper#truncate_single_line is deprecated and will be removed in Rails 4 poring")
414 "ApplicationHelper#truncate_single_line is deprecated and will be removed in Rails 4 poring")
415 # Rails 4 ActionView::Helpers::TextHelper#truncate escapes.
415 # Rails 4 ActionView::Helpers::TextHelper#truncate escapes.
416 # So, result is broken.
416 # So, result is broken.
417 truncate(string.to_s, *args).gsub(%r{[\r\n]+}m, ' ')
417 truncate(string.to_s, *args).gsub(%r{[\r\n]+}m, ' ')
418 end
418 end
419
419
420 def truncate_single_line_raw(string, length)
420 def truncate_single_line_raw(string, length)
421 string.truncate(length).gsub(%r{[\r\n]+}m, ' ')
421 string.truncate(length).gsub(%r{[\r\n]+}m, ' ')
422 end
422 end
423
423
424 # Truncates at line break after 250 characters or options[:length]
424 # Truncates at line break after 250 characters or options[:length]
425 def truncate_lines(string, options={})
425 def truncate_lines(string, options={})
426 length = options[:length] || 250
426 length = options[:length] || 250
427 if string.to_s =~ /\A(.{#{length}}.*?)$/m
427 if string.to_s =~ /\A(.{#{length}}.*?)$/m
428 "#{$1}..."
428 "#{$1}..."
429 else
429 else
430 string
430 string
431 end
431 end
432 end
432 end
433
433
434 def anchor(text)
434 def anchor(text)
435 text.to_s.gsub(' ', '_')
435 text.to_s.gsub(' ', '_')
436 end
436 end
437
437
438 def html_hours(text)
438 def html_hours(text)
439 text.gsub(%r{(\d+)\.(\d+)}, '<span class="hours hours-int">\1</span><span class="hours hours-dec">.\2</span>').html_safe
439 text.gsub(%r{(\d+)\.(\d+)}, '<span class="hours hours-int">\1</span><span class="hours hours-dec">.\2</span>').html_safe
440 end
440 end
441
441
442 def authoring(created, author, options={})
442 def authoring(created, author, options={})
443 l(options[:label] || :label_added_time_by, :author => link_to_user(author), :age => time_tag(created)).html_safe
443 l(options[:label] || :label_added_time_by, :author => link_to_user(author), :age => time_tag(created)).html_safe
444 end
444 end
445
445
446 def time_tag(time)
446 def time_tag(time)
447 text = distance_of_time_in_words(Time.now, time)
447 text = distance_of_time_in_words(Time.now, time)
448 if @project
448 if @project
449 link_to(text, {:controller => 'activities', :action => 'index', :id => @project, :from => User.current.time_to_date(time)}, :title => format_time(time))
449 link_to(text, {:controller => 'activities', :action => 'index', :id => @project, :from => User.current.time_to_date(time)}, :title => format_time(time))
450 else
450 else
451 content_tag('abbr', text, :title => format_time(time))
451 content_tag('abbr', text, :title => format_time(time))
452 end
452 end
453 end
453 end
454
454
455 def syntax_highlight_lines(name, content)
455 def syntax_highlight_lines(name, content)
456 lines = []
456 lines = []
457 syntax_highlight(name, content).each_line { |line| lines << line }
457 syntax_highlight(name, content).each_line { |line| lines << line }
458 lines
458 lines
459 end
459 end
460
460
461 def syntax_highlight(name, content)
461 def syntax_highlight(name, content)
462 Redmine::SyntaxHighlighting.highlight_by_filename(content, name)
462 Redmine::SyntaxHighlighting.highlight_by_filename(content, name)
463 end
463 end
464
464
465 def to_path_param(path)
465 def to_path_param(path)
466 str = path.to_s.split(%r{[/\\]}).select{|p| !p.blank?}.join("/")
466 str = path.to_s.split(%r{[/\\]}).select{|p| !p.blank?}.join("/")
467 str.blank? ? nil : str
467 str.blank? ? nil : str
468 end
468 end
469
469
470 def reorder_links(name, url, method = :post)
470 def reorder_links(name, url, method = :post)
471 link_to(image_tag('2uparrow.png', :alt => l(:label_sort_highest)),
471 link_to(image_tag('2uparrow.png', :alt => l(:label_sort_highest)),
472 url.merge({"#{name}[move_to]" => 'highest'}),
472 url.merge({"#{name}[move_to]" => 'highest'}),
473 :method => method, :title => l(:label_sort_highest)) +
473 :method => method, :title => l(:label_sort_highest)) +
474 link_to(image_tag('1uparrow.png', :alt => l(:label_sort_higher)),
474 link_to(image_tag('1uparrow.png', :alt => l(:label_sort_higher)),
475 url.merge({"#{name}[move_to]" => 'higher'}),
475 url.merge({"#{name}[move_to]" => 'higher'}),
476 :method => method, :title => l(:label_sort_higher)) +
476 :method => method, :title => l(:label_sort_higher)) +
477 link_to(image_tag('1downarrow.png', :alt => l(:label_sort_lower)),
477 link_to(image_tag('1downarrow.png', :alt => l(:label_sort_lower)),
478 url.merge({"#{name}[move_to]" => 'lower'}),
478 url.merge({"#{name}[move_to]" => 'lower'}),
479 :method => method, :title => l(:label_sort_lower)) +
479 :method => method, :title => l(:label_sort_lower)) +
480 link_to(image_tag('2downarrow.png', :alt => l(:label_sort_lowest)),
480 link_to(image_tag('2downarrow.png', :alt => l(:label_sort_lowest)),
481 url.merge({"#{name}[move_to]" => 'lowest'}),
481 url.merge({"#{name}[move_to]" => 'lowest'}),
482 :method => method, :title => l(:label_sort_lowest))
482 :method => method, :title => l(:label_sort_lowest))
483 end
483 end
484
484
485 def breadcrumb(*args)
485 def breadcrumb(*args)
486 elements = args.flatten
486 elements = args.flatten
487 elements.any? ? content_tag('p', (args.join(" \xc2\xbb ") + " \xc2\xbb ").html_safe, :class => 'breadcrumb') : nil
487 elements.any? ? content_tag('p', (args.join(" \xc2\xbb ") + " \xc2\xbb ").html_safe, :class => 'breadcrumb') : nil
488 end
488 end
489
489
490 def other_formats_links(&block)
490 def other_formats_links(&block)
491 concat('<p class="other-formats">'.html_safe + l(:label_export_to))
491 concat('<p class="other-formats">'.html_safe + l(:label_export_to))
492 yield Redmine::Views::OtherFormatsBuilder.new(self)
492 yield Redmine::Views::OtherFormatsBuilder.new(self)
493 concat('</p>'.html_safe)
493 concat('</p>'.html_safe)
494 end
494 end
495
495
496 def page_header_title
496 def page_header_title
497 if @project.nil? || @project.new_record?
497 if @project.nil? || @project.new_record?
498 h(Setting.app_title)
498 h(Setting.app_title)
499 else
499 else
500 b = []
500 b = []
501 ancestors = (@project.root? ? [] : @project.ancestors.visible.to_a)
501 ancestors = (@project.root? ? [] : @project.ancestors.visible.to_a)
502 if ancestors.any?
502 if ancestors.any?
503 root = ancestors.shift
503 root = ancestors.shift
504 b << link_to_project(root, {:jump => current_menu_item}, :class => 'root')
504 b << link_to_project(root, {:jump => current_menu_item}, :class => 'root')
505 if ancestors.size > 2
505 if ancestors.size > 2
506 b << "\xe2\x80\xa6"
506 b << "\xe2\x80\xa6"
507 ancestors = ancestors[-2, 2]
507 ancestors = ancestors[-2, 2]
508 end
508 end
509 b += ancestors.collect {|p| link_to_project(p, {:jump => current_menu_item}, :class => 'ancestor') }
509 b += ancestors.collect {|p| link_to_project(p, {:jump => current_menu_item}, :class => 'ancestor') }
510 end
510 end
511 b << h(@project)
511 b << h(@project)
512 b.join(" \xc2\xbb ").html_safe
512 b.join(" \xc2\xbb ").html_safe
513 end
513 end
514 end
514 end
515
515
516 # Returns a h2 tag and sets the html title with the given arguments
516 # Returns a h2 tag and sets the html title with the given arguments
517 def title(*args)
517 def title(*args)
518 strings = args.map do |arg|
518 strings = args.map do |arg|
519 if arg.is_a?(Array) && arg.size >= 2
519 if arg.is_a?(Array) && arg.size >= 2
520 link_to(*arg)
520 link_to(*arg)
521 else
521 else
522 h(arg.to_s)
522 h(arg.to_s)
523 end
523 end
524 end
524 end
525 html_title args.reverse.map {|s| (s.is_a?(Array) ? s.first : s).to_s}
525 html_title args.reverse.map {|s| (s.is_a?(Array) ? s.first : s).to_s}
526 content_tag('h2', strings.join(' &#187; ').html_safe)
526 content_tag('h2', strings.join(' &#187; ').html_safe)
527 end
527 end
528
528
529 # Sets the html title
529 # Sets the html title
530 # Returns the html title when called without arguments
530 # Returns the html title when called without arguments
531 # Current project name and app_title and automatically appended
531 # Current project name and app_title and automatically appended
532 # Exemples:
532 # Exemples:
533 # html_title 'Foo', 'Bar'
533 # html_title 'Foo', 'Bar'
534 # html_title # => 'Foo - Bar - My Project - Redmine'
534 # html_title # => 'Foo - Bar - My Project - Redmine'
535 def html_title(*args)
535 def html_title(*args)
536 if args.empty?
536 if args.empty?
537 title = @html_title || []
537 title = @html_title || []
538 title << @project.name if @project
538 title << @project.name if @project
539 title << Setting.app_title unless Setting.app_title == title.last
539 title << Setting.app_title unless Setting.app_title == title.last
540 title.reject(&:blank?).join(' - ')
540 title.reject(&:blank?).join(' - ')
541 else
541 else
542 @html_title ||= []
542 @html_title ||= []
543 @html_title += args
543 @html_title += args
544 end
544 end
545 end
545 end
546
546
547 # Returns the theme, controller name, and action as css classes for the
547 # Returns the theme, controller name, and action as css classes for the
548 # HTML body.
548 # HTML body.
549 def body_css_classes
549 def body_css_classes
550 css = []
550 css = []
551 if theme = Redmine::Themes.theme(Setting.ui_theme)
551 if theme = Redmine::Themes.theme(Setting.ui_theme)
552 css << 'theme-' + theme.name
552 css << 'theme-' + theme.name
553 end
553 end
554
554
555 css << 'project-' + @project.identifier if @project && @project.identifier.present?
555 css << 'project-' + @project.identifier if @project && @project.identifier.present?
556 css << 'controller-' + controller_name
556 css << 'controller-' + controller_name
557 css << 'action-' + action_name
557 css << 'action-' + action_name
558 css.join(' ')
558 css.join(' ')
559 end
559 end
560
560
561 def accesskey(s)
561 def accesskey(s)
562 @used_accesskeys ||= []
562 @used_accesskeys ||= []
563 key = Redmine::AccessKeys.key_for(s)
563 key = Redmine::AccessKeys.key_for(s)
564 return nil if @used_accesskeys.include?(key)
564 return nil if @used_accesskeys.include?(key)
565 @used_accesskeys << key
565 @used_accesskeys << key
566 key
566 key
567 end
567 end
568
568
569 # Formats text according to system settings.
569 # Formats text according to system settings.
570 # 2 ways to call this method:
570 # 2 ways to call this method:
571 # * with a String: textilizable(text, options)
571 # * with a String: textilizable(text, options)
572 # * with an object and one of its attribute: textilizable(issue, :description, options)
572 # * with an object and one of its attribute: textilizable(issue, :description, options)
573 def textilizable(*args)
573 def textilizable(*args)
574 options = args.last.is_a?(Hash) ? args.pop : {}
574 options = args.last.is_a?(Hash) ? args.pop : {}
575 case args.size
575 case args.size
576 when 1
576 when 1
577 obj = options[:object]
577 obj = options[:object]
578 text = args.shift
578 text = args.shift
579 when 2
579 when 2
580 obj = args.shift
580 obj = args.shift
581 attr = args.shift
581 attr = args.shift
582 text = obj.send(attr).to_s
582 text = obj.send(attr).to_s
583 else
583 else
584 raise ArgumentError, 'invalid arguments to textilizable'
584 raise ArgumentError, 'invalid arguments to textilizable'
585 end
585 end
586 return '' if text.blank?
586 return '' if text.blank?
587 project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
587 project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
588 @only_path = only_path = options.delete(:only_path) == false ? false : true
588 @only_path = only_path = options.delete(:only_path) == false ? false : true
589
589
590 text = text.dup
590 text = text.dup
591 macros = catch_macros(text)
591 macros = catch_macros(text)
592 text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text, :object => obj, :attribute => attr)
592 text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text, :object => obj, :attribute => attr)
593
593
594 @parsed_headings = []
594 @parsed_headings = []
595 @heading_anchors = {}
595 @heading_anchors = {}
596 @current_section = 0 if options[:edit_section_links]
596 @current_section = 0 if options[:edit_section_links]
597
597
598 parse_sections(text, project, obj, attr, only_path, options)
598 parse_sections(text, project, obj, attr, only_path, options)
599 text = parse_non_pre_blocks(text, obj, macros) do |text|
599 text = parse_non_pre_blocks(text, obj, macros) do |text|
600 [:parse_inline_attachments, :parse_wiki_links, :parse_redmine_links].each do |method_name|
600 [:parse_inline_attachments, :parse_wiki_links, :parse_redmine_links].each do |method_name|
601 send method_name, text, project, obj, attr, only_path, options
601 send method_name, text, project, obj, attr, only_path, options
602 end
602 end
603 end
603 end
604 parse_headings(text, project, obj, attr, only_path, options)
604 parse_headings(text, project, obj, attr, only_path, options)
605
605
606 if @parsed_headings.any?
606 if @parsed_headings.any?
607 replace_toc(text, @parsed_headings)
607 replace_toc(text, @parsed_headings)
608 end
608 end
609
609
610 text.html_safe
610 text.html_safe
611 end
611 end
612
612
613 def parse_non_pre_blocks(text, obj, macros)
613 def parse_non_pre_blocks(text, obj, macros)
614 s = StringScanner.new(text)
614 s = StringScanner.new(text)
615 tags = []
615 tags = []
616 parsed = ''
616 parsed = ''
617 while !s.eos?
617 while !s.eos?
618 s.scan(/(.*?)(<(\/)?(pre|code)(.*?)>|\z)/im)
618 s.scan(/(.*?)(<(\/)?(pre|code)(.*?)>|\z)/im)
619 text, full_tag, closing, tag = s[1], s[2], s[3], s[4]
619 text, full_tag, closing, tag = s[1], s[2], s[3], s[4]
620 if tags.empty?
620 if tags.empty?
621 yield text
621 yield text
622 inject_macros(text, obj, macros) if macros.any?
622 inject_macros(text, obj, macros) if macros.any?
623 else
623 else
624 inject_macros(text, obj, macros, false) if macros.any?
624 inject_macros(text, obj, macros, false) if macros.any?
625 end
625 end
626 parsed << text
626 parsed << text
627 if tag
627 if tag
628 if closing
628 if closing
629 if tags.last == tag.downcase
629 if tags.last == tag.downcase
630 tags.pop
630 tags.pop
631 end
631 end
632 else
632 else
633 tags << tag.downcase
633 tags << tag.downcase
634 end
634 end
635 parsed << full_tag
635 parsed << full_tag
636 end
636 end
637 end
637 end
638 # Close any non closing tags
638 # Close any non closing tags
639 while tag = tags.pop
639 while tag = tags.pop
640 parsed << "</#{tag}>"
640 parsed << "</#{tag}>"
641 end
641 end
642 parsed
642 parsed
643 end
643 end
644
644
645 def parse_inline_attachments(text, project, obj, attr, only_path, options)
645 def parse_inline_attachments(text, project, obj, attr, only_path, options)
646 # when using an image link, try to use an attachment, if possible
646 # when using an image link, try to use an attachment, if possible
647 attachments = options[:attachments] || []
647 attachments = options[:attachments] || []
648 attachments += obj.attachments if obj.respond_to?(:attachments)
648 attachments += obj.attachments if obj.respond_to?(:attachments)
649 if attachments.present?
649 if attachments.present?
650 text.gsub!(/src="([^\/"]+\.(bmp|gif|jpg|jpe|jpeg|png))"(\s+alt="([^"]*)")?/i) do |m|
650 text.gsub!(/src="([^\/"]+\.(bmp|gif|jpg|jpe|jpeg|png))"(\s+alt="([^"]*)")?/i) do |m|
651 filename, ext, alt, alttext = $1.downcase, $2, $3, $4
651 filename, ext, alt, alttext = $1.downcase, $2, $3, $4
652 # search for the picture in attachments
652 # search for the picture in attachments
653 if found = Attachment.latest_attach(attachments, filename)
653 if found = Attachment.latest_attach(attachments, filename)
654 image_url = download_named_attachment_path(found, found.filename, :only_path => only_path)
654 image_url = download_named_attachment_path(found, found.filename, :only_path => only_path)
655 desc = found.description.to_s.gsub('"', '')
655 desc = found.description.to_s.gsub('"', '')
656 if !desc.blank? && alttext.blank?
656 if !desc.blank? && alttext.blank?
657 alt = " title=\"#{desc}\" alt=\"#{desc}\""
657 alt = " title=\"#{desc}\" alt=\"#{desc}\""
658 end
658 end
659 "src=\"#{image_url}\"#{alt}"
659 "src=\"#{image_url}\"#{alt}"
660 else
660 else
661 m
661 m
662 end
662 end
663 end
663 end
664 end
664 end
665 end
665 end
666
666
667 # Wiki links
667 # Wiki links
668 #
668 #
669 # Examples:
669 # Examples:
670 # [[mypage]]
670 # [[mypage]]
671 # [[mypage|mytext]]
671 # [[mypage|mytext]]
672 # wiki links can refer other project wikis, using project name or identifier:
672 # wiki links can refer other project wikis, using project name or identifier:
673 # [[project:]] -> wiki starting page
673 # [[project:]] -> wiki starting page
674 # [[project:|mytext]]
674 # [[project:|mytext]]
675 # [[project:mypage]]
675 # [[project:mypage]]
676 # [[project:mypage|mytext]]
676 # [[project:mypage|mytext]]
677 def parse_wiki_links(text, project, obj, attr, only_path, options)
677 def parse_wiki_links(text, project, obj, attr, only_path, options)
678 text.gsub!(/(!)?(\[\[([^\]\n\|]+)(\|([^\]\n\|]+))?\]\])/) do |m|
678 text.gsub!(/(!)?(\[\[([^\]\n\|]+)(\|([^\]\n\|]+))?\]\])/) do |m|
679 link_project = project
679 link_project = project
680 esc, all, page, title = $1, $2, $3, $5
680 esc, all, page, title = $1, $2, $3, $5
681 if esc.nil?
681 if esc.nil?
682 if page =~ /^([^\:]+)\:(.*)$/
682 if page =~ /^([^\:]+)\:(.*)$/
683 identifier, page = $1, $2
683 identifier, page = $1, $2
684 link_project = Project.find_by_identifier(identifier) || Project.find_by_name(identifier)
684 link_project = Project.find_by_identifier(identifier) || Project.find_by_name(identifier)
685 title ||= identifier if page.blank?
685 title ||= identifier if page.blank?
686 end
686 end
687
687
688 if link_project && link_project.wiki
688 if link_project && link_project.wiki
689 # extract anchor
689 # extract anchor
690 anchor = nil
690 anchor = nil
691 if page =~ /^(.+?)\#(.+)$/
691 if page =~ /^(.+?)\#(.+)$/
692 page, anchor = $1, $2
692 page, anchor = $1, $2
693 end
693 end
694 anchor = sanitize_anchor_name(anchor) if anchor.present?
694 anchor = sanitize_anchor_name(anchor) if anchor.present?
695 # check if page exists
695 # check if page exists
696 wiki_page = link_project.wiki.find_page(page)
696 wiki_page = link_project.wiki.find_page(page)
697 url = if anchor.present? && wiki_page.present? && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version)) && obj.page == wiki_page
697 url = if anchor.present? && wiki_page.present? && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version)) && obj.page == wiki_page
698 "##{anchor}"
698 "##{anchor}"
699 else
699 else
700 case options[:wiki_links]
700 case options[:wiki_links]
701 when :local; "#{page.present? ? Wiki.titleize(page) : ''}.html" + (anchor.present? ? "##{anchor}" : '')
701 when :local; "#{page.present? ? Wiki.titleize(page) : ''}.html" + (anchor.present? ? "##{anchor}" : '')
702 when :anchor; "##{page.present? ? Wiki.titleize(page) : title}" + (anchor.present? ? "_#{anchor}" : '') # used for single-file wiki export
702 when :anchor; "##{page.present? ? Wiki.titleize(page) : title}" + (anchor.present? ? "_#{anchor}" : '') # used for single-file wiki export
703 else
703 else
704 wiki_page_id = page.present? ? Wiki.titleize(page) : nil
704 wiki_page_id = page.present? ? Wiki.titleize(page) : nil
705 parent = wiki_page.nil? && obj.is_a?(WikiContent) && obj.page && project == link_project ? obj.page.title : nil
705 parent = wiki_page.nil? && obj.is_a?(WikiContent) && obj.page && project == link_project ? obj.page.title : nil
706 url_for(:only_path => only_path, :controller => 'wiki', :action => 'show', :project_id => link_project,
706 url_for(:only_path => only_path, :controller => 'wiki', :action => 'show', :project_id => link_project,
707 :id => wiki_page_id, :version => nil, :anchor => anchor, :parent => parent)
707 :id => wiki_page_id, :version => nil, :anchor => anchor, :parent => parent)
708 end
708 end
709 end
709 end
710 link_to(title.present? ? title.html_safe : h(page), url, :class => ('wiki-page' + (wiki_page ? '' : ' new')))
710 link_to(title.present? ? title.html_safe : h(page), url, :class => ('wiki-page' + (wiki_page ? '' : ' new')))
711 else
711 else
712 # project or wiki doesn't exist
712 # project or wiki doesn't exist
713 all
713 all
714 end
714 end
715 else
715 else
716 all
716 all
717 end
717 end
718 end
718 end
719 end
719 end
720
720
721 # Redmine links
721 # Redmine links
722 #
722 #
723 # Examples:
723 # Examples:
724 # Issues:
724 # Issues:
725 # #52 -> Link to issue #52
725 # #52 -> Link to issue #52
726 # Changesets:
726 # Changesets:
727 # r52 -> Link to revision 52
727 # r52 -> Link to revision 52
728 # commit:a85130f -> Link to scmid starting with a85130f
728 # commit:a85130f -> Link to scmid starting with a85130f
729 # Documents:
729 # Documents:
730 # document#17 -> Link to document with id 17
730 # document#17 -> Link to document with id 17
731 # document:Greetings -> Link to the document with title "Greetings"
731 # document:Greetings -> Link to the document with title "Greetings"
732 # document:"Some document" -> Link to the document with title "Some document"
732 # document:"Some document" -> Link to the document with title "Some document"
733 # Versions:
733 # Versions:
734 # version#3 -> Link to version with id 3
734 # version#3 -> Link to version with id 3
735 # version:1.0.0 -> Link to version named "1.0.0"
735 # version:1.0.0 -> Link to version named "1.0.0"
736 # version:"1.0 beta 2" -> Link to version named "1.0 beta 2"
736 # version:"1.0 beta 2" -> Link to version named "1.0 beta 2"
737 # Attachments:
737 # Attachments:
738 # attachment:file.zip -> Link to the attachment of the current object named file.zip
738 # attachment:file.zip -> Link to the attachment of the current object named file.zip
739 # Source files:
739 # Source files:
740 # source:some/file -> Link to the file located at /some/file in the project's repository
740 # source:some/file -> Link to the file located at /some/file in the project's repository
741 # source:some/file@52 -> Link to the file's revision 52
741 # source:some/file@52 -> Link to the file's revision 52
742 # source:some/file#L120 -> Link to line 120 of the file
742 # source:some/file#L120 -> Link to line 120 of the file
743 # source:some/file@52#L120 -> Link to line 120 of the file's revision 52
743 # source:some/file@52#L120 -> Link to line 120 of the file's revision 52
744 # export:some/file -> Force the download of the file
744 # export:some/file -> Force the download of the file
745 # Forum messages:
745 # Forum messages:
746 # message#1218 -> Link to message with id 1218
746 # message#1218 -> Link to message with id 1218
747 # Projects:
747 # Projects:
748 # project:someproject -> Link to project named "someproject"
748 # project:someproject -> Link to project named "someproject"
749 # project#3 -> Link to project with id 3
749 # project#3 -> Link to project with id 3
750 #
750 #
751 # Links can refer other objects from other projects, using project identifier:
751 # Links can refer other objects from other projects, using project identifier:
752 # identifier:r52
752 # identifier:r52
753 # identifier:document:"Some document"
753 # identifier:document:"Some document"
754 # identifier:version:1.0.0
754 # identifier:version:1.0.0
755 # identifier:source:some/file
755 # identifier:source:some/file
756 def parse_redmine_links(text, default_project, obj, attr, only_path, options)
756 def parse_redmine_links(text, default_project, obj, attr, only_path, options)
757 text.gsub!(%r{([\s\(,\-\[\>]|^)(!)?(([a-z0-9\-_]+):)?(attachment|document|version|forum|news|message|project|commit|source|export)?(((#)|((([a-z0-9\-_]+)\|)?(r)))((\d+)((#note)?-(\d+))?)|(:)([^"\s<>][^\s<>]*?|"[^"]+?"))(?=(?=[[:punct:]][^A-Za-z0-9_/])|,|\s|\]|<|$)}) do |m|
757 text.gsub!(%r{<a( [^>]+?)?>(.*?)</a>|([\s\(,\-\[\>]|^)(!)?(([a-z0-9\-_]+):)?(attachment|document|version|forum|news|message|project|commit|source|export)?(((#)|((([a-z0-9\-_]+)\|)?(r)))((\d+)((#note)?-(\d+))?)|(:)([^"\s<>][^\s<>]*?|"[^"]+?"))(?=(?=[[:punct:]][^A-Za-z0-9_/])|,|\s|\]|<|$)}) do |m|
758 leading, esc, project_prefix, project_identifier, prefix, repo_prefix, repo_identifier, sep, identifier, comment_suffix, comment_id = $1, $2, $3, $4, $5, $10, $11, $8 || $12 || $18, $14 || $19, $15, $17
758 tag_content, leading, esc, project_prefix, project_identifier, prefix, repo_prefix, repo_identifier, sep, identifier, comment_suffix, comment_id = $1, $3, $4, $5, $6, $7, $12, $13, $10 || $14 || $20, $16 || $21, $17, $19
759 link = nil
759 if tag_content
760 project = default_project
760 $&
761 if project_identifier
761 else
762 project = Project.visible.find_by_identifier(project_identifier)
762 link = nil
763 end
763 project = default_project
764 if esc.nil?
764 if project_identifier
765 if prefix.nil? && sep == 'r'
765 project = Project.visible.find_by_identifier(project_identifier)
766 if project
766 end
767 repository = nil
767 if esc.nil?
768 if repo_identifier
768 if prefix.nil? && sep == 'r'
769 repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
770 else
771 repository = project.repository
772 end
773 # project.changesets.visible raises an SQL error because of a double join on repositories
774 if repository &&
775 (changeset = Changeset.visible.
776 find_by_repository_id_and_revision(repository.id, identifier))
777 link = link_to(h("#{project_prefix}#{repo_prefix}r#{identifier}"),
778 {:only_path => only_path, :controller => 'repositories',
779 :action => 'revision', :id => project,
780 :repository_id => repository.identifier_param,
781 :rev => changeset.revision},
782 :class => 'changeset',
783 :title => truncate_single_line_raw(changeset.comments, 100))
784 end
785 end
786 elsif sep == '#'
787 oid = identifier.to_i
788 case prefix
789 when nil
790 if oid.to_s == identifier &&
791 issue = Issue.visible.includes(:status).find_by_id(oid)
792 anchor = comment_id ? "note-#{comment_id}" : nil
793 link = link_to(h("##{oid}#{comment_suffix}"),
794 {:only_path => only_path, :controller => 'issues',
795 :action => 'show', :id => oid, :anchor => anchor},
796 :class => issue.css_classes,
797 :title => "#{issue.subject.truncate(100)} (#{issue.status.name})")
798 end
799 when 'document'
800 if document = Document.visible.find_by_id(oid)
801 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
802 :class => 'document'
803 end
804 when 'version'
805 if version = Version.visible.find_by_id(oid)
806 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
807 :class => 'version'
808 end
809 when 'message'
810 if message = Message.visible.includes(:parent).find_by_id(oid)
811 link = link_to_message(message, {:only_path => only_path}, :class => 'message')
812 end
813 when 'forum'
814 if board = Board.visible.find_by_id(oid)
815 link = link_to h(board.name), {:only_path => only_path, :controller => 'boards', :action => 'show', :id => board, :project_id => board.project},
816 :class => 'board'
817 end
818 when 'news'
819 if news = News.visible.find_by_id(oid)
820 link = link_to h(news.title), {:only_path => only_path, :controller => 'news', :action => 'show', :id => news},
821 :class => 'news'
822 end
823 when 'project'
824 if p = Project.visible.find_by_id(oid)
825 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
826 end
827 end
828 elsif sep == ':'
829 # removes the double quotes if any
830 name = identifier.gsub(%r{^"(.*)"$}, "\\1")
831 name = CGI.unescapeHTML(name)
832 case prefix
833 when 'document'
834 if project && document = project.documents.visible.find_by_title(name)
835 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
836 :class => 'document'
837 end
838 when 'version'
839 if project && version = project.versions.visible.find_by_name(name)
840 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
841 :class => 'version'
842 end
843 when 'forum'
844 if project && board = project.boards.visible.find_by_name(name)
845 link = link_to h(board.name), {:only_path => only_path, :controller => 'boards', :action => 'show', :id => board, :project_id => board.project},
846 :class => 'board'
847 end
848 when 'news'
849 if project && news = project.news.visible.find_by_title(name)
850 link = link_to h(news.title), {:only_path => only_path, :controller => 'news', :action => 'show', :id => news},
851 :class => 'news'
852 end
853 when 'commit', 'source', 'export'
854 if project
769 if project
855 repository = nil
770 repository = nil
856 if name =~ %r{^(([a-z0-9\-_]+)\|)(.+)$}
771 if repo_identifier
857 repo_prefix, repo_identifier, name = $1, $2, $3
858 repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
772 repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
859 else
773 else
860 repository = project.repository
774 repository = project.repository
861 end
775 end
862 if prefix == 'commit'
776 # project.changesets.visible raises an SQL error because of a double join on repositories
863 if repository && (changeset = Changeset.visible.where("repository_id = ? AND scmid LIKE ?", repository.id, "#{name}%").first)
777 if repository &&
864 link = link_to h("#{project_prefix}#{repo_prefix}#{name}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :repository_id => repository.identifier_param, :rev => changeset.identifier},
778 (changeset = Changeset.visible.
865 :class => 'changeset',
779 find_by_repository_id_and_revision(repository.id, identifier))
866 :title => truncate_single_line_raw(changeset.comments, 100)
780 link = link_to(h("#{project_prefix}#{repo_prefix}r#{identifier}"),
867 end
781 {:only_path => only_path, :controller => 'repositories',
868 else
782 :action => 'revision', :id => project,
869 if repository && User.current.allowed_to?(:browse_repository, project)
783 :repository_id => repository.identifier_param,
870 name =~ %r{^[/\\]*(.*?)(@([^/\\@]+?))?(#(L\d+))?$}
784 :rev => changeset.revision},
871 path, rev, anchor = $1, $3, $5
785 :class => 'changeset',
872 link = link_to h("#{project_prefix}#{prefix}:#{repo_prefix}#{name}"), {:only_path => only_path, :controller => 'repositories', :action => (prefix == 'export' ? 'raw' : 'entry'), :id => project, :repository_id => repository.identifier_param,
786 :title => truncate_single_line_raw(changeset.comments, 100))
873 :path => to_path_param(path),
874 :rev => rev,
875 :anchor => anchor},
876 :class => (prefix == 'export' ? 'source download' : 'source')
877 end
878 end
787 end
879 repo_prefix = nil
880 end
788 end
881 when 'attachment'
789 elsif sep == '#'
882 attachments = options[:attachments] || []
790 oid = identifier.to_i
883 attachments += obj.attachments if obj.respond_to?(:attachments)
791 case prefix
884 if attachments && attachment = Attachment.latest_attach(attachments, name)
792 when nil
885 link = link_to_attachment(attachment, :only_path => only_path, :download => true, :class => 'attachment')
793 if oid.to_s == identifier &&
794 issue = Issue.visible.includes(:status).find_by_id(oid)
795 anchor = comment_id ? "note-#{comment_id}" : nil
796 link = link_to(h("##{oid}#{comment_suffix}"),
797 {:only_path => only_path, :controller => 'issues',
798 :action => 'show', :id => oid, :anchor => anchor},
799 :class => issue.css_classes,
800 :title => "#{issue.subject.truncate(100)} (#{issue.status.name})")
801 end
802 when 'document'
803 if document = Document.visible.find_by_id(oid)
804 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
805 :class => 'document'
806 end
807 when 'version'
808 if version = Version.visible.find_by_id(oid)
809 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
810 :class => 'version'
811 end
812 when 'message'
813 if message = Message.visible.includes(:parent).find_by_id(oid)
814 link = link_to_message(message, {:only_path => only_path}, :class => 'message')
815 end
816 when 'forum'
817 if board = Board.visible.find_by_id(oid)
818 link = link_to h(board.name), {:only_path => only_path, :controller => 'boards', :action => 'show', :id => board, :project_id => board.project},
819 :class => 'board'
820 end
821 when 'news'
822 if news = News.visible.find_by_id(oid)
823 link = link_to h(news.title), {:only_path => only_path, :controller => 'news', :action => 'show', :id => news},
824 :class => 'news'
825 end
826 when 'project'
827 if p = Project.visible.find_by_id(oid)
828 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
829 end
886 end
830 end
887 when 'project'
831 elsif sep == ':'
888 if p = Project.visible.where("identifier = :s OR LOWER(name) = :s", :s => name.downcase).first
832 # removes the double quotes if any
889 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
833 name = identifier.gsub(%r{^"(.*)"$}, "\\1")
834 name = CGI.unescapeHTML(name)
835 case prefix
836 when 'document'
837 if project && document = project.documents.visible.find_by_title(name)
838 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
839 :class => 'document'
840 end
841 when 'version'
842 if project && version = project.versions.visible.find_by_name(name)
843 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
844 :class => 'version'
845 end
846 when 'forum'
847 if project && board = project.boards.visible.find_by_name(name)
848 link = link_to h(board.name), {:only_path => only_path, :controller => 'boards', :action => 'show', :id => board, :project_id => board.project},
849 :class => 'board'
850 end
851 when 'news'
852 if project && news = project.news.visible.find_by_title(name)
853 link = link_to h(news.title), {:only_path => only_path, :controller => 'news', :action => 'show', :id => news},
854 :class => 'news'
855 end
856 when 'commit', 'source', 'export'
857 if project
858 repository = nil
859 if name =~ %r{^(([a-z0-9\-_]+)\|)(.+)$}
860 repo_prefix, repo_identifier, name = $1, $2, $3
861 repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
862 else
863 repository = project.repository
864 end
865 if prefix == 'commit'
866 if repository && (changeset = Changeset.visible.where("repository_id = ? AND scmid LIKE ?", repository.id, "#{name}%").first)
867 link = link_to h("#{project_prefix}#{repo_prefix}#{name}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :repository_id => repository.identifier_param, :rev => changeset.identifier},
868 :class => 'changeset',
869 :title => truncate_single_line_raw(changeset.comments, 100)
870 end
871 else
872 if repository && User.current.allowed_to?(:browse_repository, project)
873 name =~ %r{^[/\\]*(.*?)(@([^/\\@]+?))?(#(L\d+))?$}
874 path, rev, anchor = $1, $3, $5
875 link = link_to h("#{project_prefix}#{prefix}:#{repo_prefix}#{name}"), {:only_path => only_path, :controller => 'repositories', :action => (prefix == 'export' ? 'raw' : 'entry'), :id => project, :repository_id => repository.identifier_param,
876 :path => to_path_param(path),
877 :rev => rev,
878 :anchor => anchor},
879 :class => (prefix == 'export' ? 'source download' : 'source')
880 end
881 end
882 repo_prefix = nil
883 end
884 when 'attachment'
885 attachments = options[:attachments] || []
886 attachments += obj.attachments if obj.respond_to?(:attachments)
887 if attachments && attachment = Attachment.latest_attach(attachments, name)
888 link = link_to_attachment(attachment, :only_path => only_path, :download => true, :class => 'attachment')
889 end
890 when 'project'
891 if p = Project.visible.where("identifier = :s OR LOWER(name) = :s", :s => name.downcase).first
892 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
893 end
890 end
894 end
891 end
895 end
892 end
896 end
897 (leading + (link || "#{project_prefix}#{prefix}#{repo_prefix}#{sep}#{identifier}#{comment_suffix}"))
893 end
898 end
894 (leading + (link || "#{project_prefix}#{prefix}#{repo_prefix}#{sep}#{identifier}#{comment_suffix}"))
895 end
899 end
896 end
900 end
897
901
898 HEADING_RE = /(<h(\d)( [^>]+)?>(.+?)<\/h(\d)>)/i unless const_defined?(:HEADING_RE)
902 HEADING_RE = /(<h(\d)( [^>]+)?>(.+?)<\/h(\d)>)/i unless const_defined?(:HEADING_RE)
899
903
900 def parse_sections(text, project, obj, attr, only_path, options)
904 def parse_sections(text, project, obj, attr, only_path, options)
901 return unless options[:edit_section_links]
905 return unless options[:edit_section_links]
902 text.gsub!(HEADING_RE) do
906 text.gsub!(HEADING_RE) do
903 heading = $1
907 heading = $1
904 @current_section += 1
908 @current_section += 1
905 if @current_section > 1
909 if @current_section > 1
906 content_tag('div',
910 content_tag('div',
907 link_to(image_tag('edit.png'), options[:edit_section_links].merge(:section => @current_section)),
911 link_to(image_tag('edit.png'), options[:edit_section_links].merge(:section => @current_section)),
908 :class => 'contextual',
912 :class => 'contextual',
909 :title => l(:button_edit_section),
913 :title => l(:button_edit_section),
910 :id => "section-#{@current_section}") + heading.html_safe
914 :id => "section-#{@current_section}") + heading.html_safe
911 else
915 else
912 heading
916 heading
913 end
917 end
914 end
918 end
915 end
919 end
916
920
917 # Headings and TOC
921 # Headings and TOC
918 # Adds ids and links to headings unless options[:headings] is set to false
922 # Adds ids and links to headings unless options[:headings] is set to false
919 def parse_headings(text, project, obj, attr, only_path, options)
923 def parse_headings(text, project, obj, attr, only_path, options)
920 return if options[:headings] == false
924 return if options[:headings] == false
921
925
922 text.gsub!(HEADING_RE) do
926 text.gsub!(HEADING_RE) do
923 level, attrs, content = $2.to_i, $3, $4
927 level, attrs, content = $2.to_i, $3, $4
924 item = strip_tags(content).strip
928 item = strip_tags(content).strip
925 anchor = sanitize_anchor_name(item)
929 anchor = sanitize_anchor_name(item)
926 # used for single-file wiki export
930 # used for single-file wiki export
927 anchor = "#{obj.page.title}_#{anchor}" if options[:wiki_links] == :anchor && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version))
931 anchor = "#{obj.page.title}_#{anchor}" if options[:wiki_links] == :anchor && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version))
928 @heading_anchors[anchor] ||= 0
932 @heading_anchors[anchor] ||= 0
929 idx = (@heading_anchors[anchor] += 1)
933 idx = (@heading_anchors[anchor] += 1)
930 if idx > 1
934 if idx > 1
931 anchor = "#{anchor}-#{idx}"
935 anchor = "#{anchor}-#{idx}"
932 end
936 end
933 @parsed_headings << [level, anchor, item]
937 @parsed_headings << [level, anchor, item]
934 "<a name=\"#{anchor}\"></a>\n<h#{level} #{attrs}>#{content}<a href=\"##{anchor}\" class=\"wiki-anchor\">&para;</a></h#{level}>"
938 "<a name=\"#{anchor}\"></a>\n<h#{level} #{attrs}>#{content}<a href=\"##{anchor}\" class=\"wiki-anchor\">&para;</a></h#{level}>"
935 end
939 end
936 end
940 end
937
941
938 MACROS_RE = /(
942 MACROS_RE = /(
939 (!)? # escaping
943 (!)? # escaping
940 (
944 (
941 \{\{ # opening tag
945 \{\{ # opening tag
942 ([\w]+) # macro name
946 ([\w]+) # macro name
943 (\(([^\n\r]*?)\))? # optional arguments
947 (\(([^\n\r]*?)\))? # optional arguments
944 ([\n\r].*?[\n\r])? # optional block of text
948 ([\n\r].*?[\n\r])? # optional block of text
945 \}\} # closing tag
949 \}\} # closing tag
946 )
950 )
947 )/mx unless const_defined?(:MACROS_RE)
951 )/mx unless const_defined?(:MACROS_RE)
948
952
949 MACRO_SUB_RE = /(
953 MACRO_SUB_RE = /(
950 \{\{
954 \{\{
951 macro\((\d+)\)
955 macro\((\d+)\)
952 \}\}
956 \}\}
953 )/x unless const_defined?(:MACRO_SUB_RE)
957 )/x unless const_defined?(:MACRO_SUB_RE)
954
958
955 # Extracts macros from text
959 # Extracts macros from text
956 def catch_macros(text)
960 def catch_macros(text)
957 macros = {}
961 macros = {}
958 text.gsub!(MACROS_RE) do
962 text.gsub!(MACROS_RE) do
959 all, macro = $1, $4.downcase
963 all, macro = $1, $4.downcase
960 if macro_exists?(macro) || all =~ MACRO_SUB_RE
964 if macro_exists?(macro) || all =~ MACRO_SUB_RE
961 index = macros.size
965 index = macros.size
962 macros[index] = all
966 macros[index] = all
963 "{{macro(#{index})}}"
967 "{{macro(#{index})}}"
964 else
968 else
965 all
969 all
966 end
970 end
967 end
971 end
968 macros
972 macros
969 end
973 end
970
974
971 # Executes and replaces macros in text
975 # Executes and replaces macros in text
972 def inject_macros(text, obj, macros, execute=true)
976 def inject_macros(text, obj, macros, execute=true)
973 text.gsub!(MACRO_SUB_RE) do
977 text.gsub!(MACRO_SUB_RE) do
974 all, index = $1, $2.to_i
978 all, index = $1, $2.to_i
975 orig = macros.delete(index)
979 orig = macros.delete(index)
976 if execute && orig && orig =~ MACROS_RE
980 if execute && orig && orig =~ MACROS_RE
977 esc, all, macro, args, block = $2, $3, $4.downcase, $6.to_s, $7.try(:strip)
981 esc, all, macro, args, block = $2, $3, $4.downcase, $6.to_s, $7.try(:strip)
978 if esc.nil?
982 if esc.nil?
979 h(exec_macro(macro, obj, args, block) || all)
983 h(exec_macro(macro, obj, args, block) || all)
980 else
984 else
981 h(all)
985 h(all)
982 end
986 end
983 elsif orig
987 elsif orig
984 h(orig)
988 h(orig)
985 else
989 else
986 h(all)
990 h(all)
987 end
991 end
988 end
992 end
989 end
993 end
990
994
991 TOC_RE = /<p>\{\{((<|&lt;)|(>|&gt;))?toc\}\}<\/p>/i unless const_defined?(:TOC_RE)
995 TOC_RE = /<p>\{\{((<|&lt;)|(>|&gt;))?toc\}\}<\/p>/i unless const_defined?(:TOC_RE)
992
996
993 # Renders the TOC with given headings
997 # Renders the TOC with given headings
994 def replace_toc(text, headings)
998 def replace_toc(text, headings)
995 text.gsub!(TOC_RE) do
999 text.gsub!(TOC_RE) do
996 left_align, right_align = $2, $3
1000 left_align, right_align = $2, $3
997 # Keep only the 4 first levels
1001 # Keep only the 4 first levels
998 headings = headings.select{|level, anchor, item| level <= 4}
1002 headings = headings.select{|level, anchor, item| level <= 4}
999 if headings.empty?
1003 if headings.empty?
1000 ''
1004 ''
1001 else
1005 else
1002 div_class = 'toc'
1006 div_class = 'toc'
1003 div_class << ' right' if right_align
1007 div_class << ' right' if right_align
1004 div_class << ' left' if left_align
1008 div_class << ' left' if left_align
1005 out = "<ul class=\"#{div_class}\"><li>"
1009 out = "<ul class=\"#{div_class}\"><li>"
1006 root = headings.map(&:first).min
1010 root = headings.map(&:first).min
1007 current = root
1011 current = root
1008 started = false
1012 started = false
1009 headings.each do |level, anchor, item|
1013 headings.each do |level, anchor, item|
1010 if level > current
1014 if level > current
1011 out << '<ul><li>' * (level - current)
1015 out << '<ul><li>' * (level - current)
1012 elsif level < current
1016 elsif level < current
1013 out << "</li></ul>\n" * (current - level) + "</li><li>"
1017 out << "</li></ul>\n" * (current - level) + "</li><li>"
1014 elsif started
1018 elsif started
1015 out << '</li><li>'
1019 out << '</li><li>'
1016 end
1020 end
1017 out << "<a href=\"##{anchor}\">#{item}</a>"
1021 out << "<a href=\"##{anchor}\">#{item}</a>"
1018 current = level
1022 current = level
1019 started = true
1023 started = true
1020 end
1024 end
1021 out << '</li></ul>' * (current - root)
1025 out << '</li></ul>' * (current - root)
1022 out << '</li></ul>'
1026 out << '</li></ul>'
1023 end
1027 end
1024 end
1028 end
1025 end
1029 end
1026
1030
1027 # Same as Rails' simple_format helper without using paragraphs
1031 # Same as Rails' simple_format helper without using paragraphs
1028 def simple_format_without_paragraph(text)
1032 def simple_format_without_paragraph(text)
1029 text.to_s.
1033 text.to_s.
1030 gsub(/\r\n?/, "\n"). # \r\n and \r -> \n
1034 gsub(/\r\n?/, "\n"). # \r\n and \r -> \n
1031 gsub(/\n\n+/, "<br /><br />"). # 2+ newline -> 2 br
1035 gsub(/\n\n+/, "<br /><br />"). # 2+ newline -> 2 br
1032 gsub(/([^\n]\n)(?=[^\n])/, '\1<br />'). # 1 newline -> br
1036 gsub(/([^\n]\n)(?=[^\n])/, '\1<br />'). # 1 newline -> br
1033 html_safe
1037 html_safe
1034 end
1038 end
1035
1039
1036 def lang_options_for_select(blank=true)
1040 def lang_options_for_select(blank=true)
1037 (blank ? [["(auto)", ""]] : []) + languages_options
1041 (blank ? [["(auto)", ""]] : []) + languages_options
1038 end
1042 end
1039
1043
1040 def label_tag_for(name, option_tags = nil, options = {})
1044 def label_tag_for(name, option_tags = nil, options = {})
1041 label_text = l(("field_"+field.to_s.gsub(/\_id$/, "")).to_sym) + (options.delete(:required) ? @template.content_tag("span", " *", :class => "required"): "")
1045 label_text = l(("field_"+field.to_s.gsub(/\_id$/, "")).to_sym) + (options.delete(:required) ? @template.content_tag("span", " *", :class => "required"): "")
1042 content_tag("label", label_text)
1046 content_tag("label", label_text)
1043 end
1047 end
1044
1048
1045 def labelled_form_for(*args, &proc)
1049 def labelled_form_for(*args, &proc)
1046 args << {} unless args.last.is_a?(Hash)
1050 args << {} unless args.last.is_a?(Hash)
1047 options = args.last
1051 options = args.last
1048 if args.first.is_a?(Symbol)
1052 if args.first.is_a?(Symbol)
1049 options.merge!(:as => args.shift)
1053 options.merge!(:as => args.shift)
1050 end
1054 end
1051 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
1055 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
1052 form_for(*args, &proc)
1056 form_for(*args, &proc)
1053 end
1057 end
1054
1058
1055 def labelled_fields_for(*args, &proc)
1059 def labelled_fields_for(*args, &proc)
1056 args << {} unless args.last.is_a?(Hash)
1060 args << {} unless args.last.is_a?(Hash)
1057 options = args.last
1061 options = args.last
1058 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
1062 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
1059 fields_for(*args, &proc)
1063 fields_for(*args, &proc)
1060 end
1064 end
1061
1065
1062 def error_messages_for(*objects)
1066 def error_messages_for(*objects)
1063 html = ""
1067 html = ""
1064 objects = objects.map {|o| o.is_a?(String) ? instance_variable_get("@#{o}") : o}.compact
1068 objects = objects.map {|o| o.is_a?(String) ? instance_variable_get("@#{o}") : o}.compact
1065 errors = objects.map {|o| o.errors.full_messages}.flatten
1069 errors = objects.map {|o| o.errors.full_messages}.flatten
1066 if errors.any?
1070 if errors.any?
1067 html << "<div id='errorExplanation'><ul>\n"
1071 html << "<div id='errorExplanation'><ul>\n"
1068 errors.each do |error|
1072 errors.each do |error|
1069 html << "<li>#{h error}</li>\n"
1073 html << "<li>#{h error}</li>\n"
1070 end
1074 end
1071 html << "</ul></div>\n"
1075 html << "</ul></div>\n"
1072 end
1076 end
1073 html.html_safe
1077 html.html_safe
1074 end
1078 end
1075
1079
1076 def delete_link(url, options={})
1080 def delete_link(url, options={})
1077 options = {
1081 options = {
1078 :method => :delete,
1082 :method => :delete,
1079 :data => {:confirm => l(:text_are_you_sure)},
1083 :data => {:confirm => l(:text_are_you_sure)},
1080 :class => 'icon icon-del'
1084 :class => 'icon icon-del'
1081 }.merge(options)
1085 }.merge(options)
1082
1086
1083 link_to l(:button_delete), url, options
1087 link_to l(:button_delete), url, options
1084 end
1088 end
1085
1089
1086 def preview_link(url, form, target='preview', options={})
1090 def preview_link(url, form, target='preview', options={})
1087 content_tag 'a', l(:label_preview), {
1091 content_tag 'a', l(:label_preview), {
1088 :href => "#",
1092 :href => "#",
1089 :onclick => %|submitPreview("#{escape_javascript url_for(url)}", "#{escape_javascript form}", "#{escape_javascript target}"); return false;|,
1093 :onclick => %|submitPreview("#{escape_javascript url_for(url)}", "#{escape_javascript form}", "#{escape_javascript target}"); return false;|,
1090 :accesskey => accesskey(:preview)
1094 :accesskey => accesskey(:preview)
1091 }.merge(options)
1095 }.merge(options)
1092 end
1096 end
1093
1097
1094 def link_to_function(name, function, html_options={})
1098 def link_to_function(name, function, html_options={})
1095 content_tag(:a, name, {:href => '#', :onclick => "#{function}; return false;"}.merge(html_options))
1099 content_tag(:a, name, {:href => '#', :onclick => "#{function}; return false;"}.merge(html_options))
1096 end
1100 end
1097
1101
1098 # Helper to render JSON in views
1102 # Helper to render JSON in views
1099 def raw_json(arg)
1103 def raw_json(arg)
1100 arg.to_json.to_s.gsub('/', '\/').html_safe
1104 arg.to_json.to_s.gsub('/', '\/').html_safe
1101 end
1105 end
1102
1106
1103 def back_url
1107 def back_url
1104 url = params[:back_url]
1108 url = params[:back_url]
1105 if url.nil? && referer = request.env['HTTP_REFERER']
1109 if url.nil? && referer = request.env['HTTP_REFERER']
1106 url = CGI.unescape(referer.to_s)
1110 url = CGI.unescape(referer.to_s)
1107 end
1111 end
1108 url
1112 url
1109 end
1113 end
1110
1114
1111 def back_url_hidden_field_tag
1115 def back_url_hidden_field_tag
1112 url = back_url
1116 url = back_url
1113 hidden_field_tag('back_url', url, :id => nil) unless url.blank?
1117 hidden_field_tag('back_url', url, :id => nil) unless url.blank?
1114 end
1118 end
1115
1119
1116 def check_all_links(form_name)
1120 def check_all_links(form_name)
1117 link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") +
1121 link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") +
1118 " | ".html_safe +
1122 " | ".html_safe +
1119 link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)")
1123 link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)")
1120 end
1124 end
1121
1125
1122 def toggle_checkboxes_link(selector)
1126 def toggle_checkboxes_link(selector)
1123 link_to_function image_tag('toggle_check.png'),
1127 link_to_function image_tag('toggle_check.png'),
1124 "toggleCheckboxesBySelector('#{selector}')",
1128 "toggleCheckboxesBySelector('#{selector}')",
1125 :title => "#{l(:button_check_all)} / #{l(:button_uncheck_all)}"
1129 :title => "#{l(:button_check_all)} / #{l(:button_uncheck_all)}"
1126 end
1130 end
1127
1131
1128 def progress_bar(pcts, options={})
1132 def progress_bar(pcts, options={})
1129 pcts = [pcts, pcts] unless pcts.is_a?(Array)
1133 pcts = [pcts, pcts] unless pcts.is_a?(Array)
1130 pcts = pcts.collect(&:round)
1134 pcts = pcts.collect(&:round)
1131 pcts[1] = pcts[1] - pcts[0]
1135 pcts[1] = pcts[1] - pcts[0]
1132 pcts << (100 - pcts[1] - pcts[0])
1136 pcts << (100 - pcts[1] - pcts[0])
1133 width = options[:width] || '100px;'
1137 width = options[:width] || '100px;'
1134 legend = options[:legend] || ''
1138 legend = options[:legend] || ''
1135 content_tag('table',
1139 content_tag('table',
1136 content_tag('tr',
1140 content_tag('tr',
1137 (pcts[0] > 0 ? content_tag('td', '', :style => "width: #{pcts[0]}%;", :class => 'closed') : ''.html_safe) +
1141 (pcts[0] > 0 ? content_tag('td', '', :style => "width: #{pcts[0]}%;", :class => 'closed') : ''.html_safe) +
1138 (pcts[1] > 0 ? content_tag('td', '', :style => "width: #{pcts[1]}%;", :class => 'done') : ''.html_safe) +
1142 (pcts[1] > 0 ? content_tag('td', '', :style => "width: #{pcts[1]}%;", :class => 'done') : ''.html_safe) +
1139 (pcts[2] > 0 ? content_tag('td', '', :style => "width: #{pcts[2]}%;", :class => 'todo') : ''.html_safe)
1143 (pcts[2] > 0 ? content_tag('td', '', :style => "width: #{pcts[2]}%;", :class => 'todo') : ''.html_safe)
1140 ), :class => "progress progress-#{pcts[0]}", :style => "width: #{width};").html_safe +
1144 ), :class => "progress progress-#{pcts[0]}", :style => "width: #{width};").html_safe +
1141 content_tag('p', legend, :class => 'percent').html_safe
1145 content_tag('p', legend, :class => 'percent').html_safe
1142 end
1146 end
1143
1147
1144 def checked_image(checked=true)
1148 def checked_image(checked=true)
1145 if checked
1149 if checked
1146 image_tag 'toggle_check.png'
1150 image_tag 'toggle_check.png'
1147 end
1151 end
1148 end
1152 end
1149
1153
1150 def context_menu(url)
1154 def context_menu(url)
1151 unless @context_menu_included
1155 unless @context_menu_included
1152 content_for :header_tags do
1156 content_for :header_tags do
1153 javascript_include_tag('context_menu') +
1157 javascript_include_tag('context_menu') +
1154 stylesheet_link_tag('context_menu')
1158 stylesheet_link_tag('context_menu')
1155 end
1159 end
1156 if l(:direction) == 'rtl'
1160 if l(:direction) == 'rtl'
1157 content_for :header_tags do
1161 content_for :header_tags do
1158 stylesheet_link_tag('context_menu_rtl')
1162 stylesheet_link_tag('context_menu_rtl')
1159 end
1163 end
1160 end
1164 end
1161 @context_menu_included = true
1165 @context_menu_included = true
1162 end
1166 end
1163 javascript_tag "contextMenuInit('#{ url_for(url) }')"
1167 javascript_tag "contextMenuInit('#{ url_for(url) }')"
1164 end
1168 end
1165
1169
1166 def calendar_for(field_id)
1170 def calendar_for(field_id)
1167 include_calendar_headers_tags
1171 include_calendar_headers_tags
1168 javascript_tag("$(function() { $('##{field_id}').datepicker(datepickerOptions); });")
1172 javascript_tag("$(function() { $('##{field_id}').datepicker(datepickerOptions); });")
1169 end
1173 end
1170
1174
1171 def include_calendar_headers_tags
1175 def include_calendar_headers_tags
1172 unless @calendar_headers_tags_included
1176 unless @calendar_headers_tags_included
1173 tags = javascript_include_tag("datepicker")
1177 tags = javascript_include_tag("datepicker")
1174 @calendar_headers_tags_included = true
1178 @calendar_headers_tags_included = true
1175 content_for :header_tags do
1179 content_for :header_tags do
1176 start_of_week = Setting.start_of_week
1180 start_of_week = Setting.start_of_week
1177 start_of_week = l(:general_first_day_of_week, :default => '1') if start_of_week.blank?
1181 start_of_week = l(:general_first_day_of_week, :default => '1') if start_of_week.blank?
1178 # Redmine uses 1..7 (monday..sunday) in settings and locales
1182 # Redmine uses 1..7 (monday..sunday) in settings and locales
1179 # JQuery uses 0..6 (sunday..saturday), 7 needs to be changed to 0
1183 # JQuery uses 0..6 (sunday..saturday), 7 needs to be changed to 0
1180 start_of_week = start_of_week.to_i % 7
1184 start_of_week = start_of_week.to_i % 7
1181 tags << javascript_tag(
1185 tags << javascript_tag(
1182 "var datepickerOptions={dateFormat: 'yy-mm-dd', firstDay: #{start_of_week}, " +
1186 "var datepickerOptions={dateFormat: 'yy-mm-dd', firstDay: #{start_of_week}, " +
1183 "showOn: 'button', buttonImageOnly: true, buttonImage: '" +
1187 "showOn: 'button', buttonImageOnly: true, buttonImage: '" +
1184 path_to_image('/images/calendar.png') +
1188 path_to_image('/images/calendar.png') +
1185 "', showButtonPanel: true, showWeek: true, showOtherMonths: true, " +
1189 "', showButtonPanel: true, showWeek: true, showOtherMonths: true, " +
1186 "selectOtherMonths: true, changeMonth: true, changeYear: true, " +
1190 "selectOtherMonths: true, changeMonth: true, changeYear: true, " +
1187 "beforeShow: beforeShowDatePicker};")
1191 "beforeShow: beforeShowDatePicker};")
1188 jquery_locale = l('jquery.locale', :default => current_language.to_s)
1192 jquery_locale = l('jquery.locale', :default => current_language.to_s)
1189 unless jquery_locale == 'en'
1193 unless jquery_locale == 'en'
1190 tags << javascript_include_tag("i18n/datepicker-#{jquery_locale}.js")
1194 tags << javascript_include_tag("i18n/datepicker-#{jquery_locale}.js")
1191 end
1195 end
1192 tags
1196 tags
1193 end
1197 end
1194 end
1198 end
1195 end
1199 end
1196
1200
1197 # Overrides Rails' stylesheet_link_tag with themes and plugins support.
1201 # Overrides Rails' stylesheet_link_tag with themes and plugins support.
1198 # Examples:
1202 # Examples:
1199 # stylesheet_link_tag('styles') # => picks styles.css from the current theme or defaults
1203 # stylesheet_link_tag('styles') # => picks styles.css from the current theme or defaults
1200 # stylesheet_link_tag('styles', :plugin => 'foo) # => picks styles.css from plugin's assets
1204 # stylesheet_link_tag('styles', :plugin => 'foo) # => picks styles.css from plugin's assets
1201 #
1205 #
1202 def stylesheet_link_tag(*sources)
1206 def stylesheet_link_tag(*sources)
1203 options = sources.last.is_a?(Hash) ? sources.pop : {}
1207 options = sources.last.is_a?(Hash) ? sources.pop : {}
1204 plugin = options.delete(:plugin)
1208 plugin = options.delete(:plugin)
1205 sources = sources.map do |source|
1209 sources = sources.map do |source|
1206 if plugin
1210 if plugin
1207 "/plugin_assets/#{plugin}/stylesheets/#{source}"
1211 "/plugin_assets/#{plugin}/stylesheets/#{source}"
1208 elsif current_theme && current_theme.stylesheets.include?(source)
1212 elsif current_theme && current_theme.stylesheets.include?(source)
1209 current_theme.stylesheet_path(source)
1213 current_theme.stylesheet_path(source)
1210 else
1214 else
1211 source
1215 source
1212 end
1216 end
1213 end
1217 end
1214 super *sources, options
1218 super *sources, options
1215 end
1219 end
1216
1220
1217 # Overrides Rails' image_tag with themes and plugins support.
1221 # Overrides Rails' image_tag with themes and plugins support.
1218 # Examples:
1222 # Examples:
1219 # image_tag('image.png') # => picks image.png from the current theme or defaults
1223 # image_tag('image.png') # => picks image.png from the current theme or defaults
1220 # image_tag('image.png', :plugin => 'foo) # => picks image.png from plugin's assets
1224 # image_tag('image.png', :plugin => 'foo) # => picks image.png from plugin's assets
1221 #
1225 #
1222 def image_tag(source, options={})
1226 def image_tag(source, options={})
1223 if plugin = options.delete(:plugin)
1227 if plugin = options.delete(:plugin)
1224 source = "/plugin_assets/#{plugin}/images/#{source}"
1228 source = "/plugin_assets/#{plugin}/images/#{source}"
1225 elsif current_theme && current_theme.images.include?(source)
1229 elsif current_theme && current_theme.images.include?(source)
1226 source = current_theme.image_path(source)
1230 source = current_theme.image_path(source)
1227 end
1231 end
1228 super source, options
1232 super source, options
1229 end
1233 end
1230
1234
1231 # Overrides Rails' javascript_include_tag with plugins support
1235 # Overrides Rails' javascript_include_tag with plugins support
1232 # Examples:
1236 # Examples:
1233 # javascript_include_tag('scripts') # => picks scripts.js from defaults
1237 # javascript_include_tag('scripts') # => picks scripts.js from defaults
1234 # javascript_include_tag('scripts', :plugin => 'foo) # => picks scripts.js from plugin's assets
1238 # javascript_include_tag('scripts', :plugin => 'foo) # => picks scripts.js from plugin's assets
1235 #
1239 #
1236 def javascript_include_tag(*sources)
1240 def javascript_include_tag(*sources)
1237 options = sources.last.is_a?(Hash) ? sources.pop : {}
1241 options = sources.last.is_a?(Hash) ? sources.pop : {}
1238 if plugin = options.delete(:plugin)
1242 if plugin = options.delete(:plugin)
1239 sources = sources.map do |source|
1243 sources = sources.map do |source|
1240 if plugin
1244 if plugin
1241 "/plugin_assets/#{plugin}/javascripts/#{source}"
1245 "/plugin_assets/#{plugin}/javascripts/#{source}"
1242 else
1246 else
1243 source
1247 source
1244 end
1248 end
1245 end
1249 end
1246 end
1250 end
1247 super *sources, options
1251 super *sources, options
1248 end
1252 end
1249
1253
1250 # TODO: remove this in 2.5.0
1254 # TODO: remove this in 2.5.0
1251 def has_content?(name)
1255 def has_content?(name)
1252 content_for?(name)
1256 content_for?(name)
1253 end
1257 end
1254
1258
1255 def sidebar_content?
1259 def sidebar_content?
1256 content_for?(:sidebar) || view_layouts_base_sidebar_hook_response.present?
1260 content_for?(:sidebar) || view_layouts_base_sidebar_hook_response.present?
1257 end
1261 end
1258
1262
1259 def view_layouts_base_sidebar_hook_response
1263 def view_layouts_base_sidebar_hook_response
1260 @view_layouts_base_sidebar_hook_response ||= call_hook(:view_layouts_base_sidebar)
1264 @view_layouts_base_sidebar_hook_response ||= call_hook(:view_layouts_base_sidebar)
1261 end
1265 end
1262
1266
1263 def email_delivery_enabled?
1267 def email_delivery_enabled?
1264 !!ActionMailer::Base.perform_deliveries
1268 !!ActionMailer::Base.perform_deliveries
1265 end
1269 end
1266
1270
1267 # Returns the avatar image tag for the given +user+ if avatars are enabled
1271 # Returns the avatar image tag for the given +user+ if avatars are enabled
1268 # +user+ can be a User or a string that will be scanned for an email address (eg. 'joe <joe@foo.bar>')
1272 # +user+ can be a User or a string that will be scanned for an email address (eg. 'joe <joe@foo.bar>')
1269 def avatar(user, options = { })
1273 def avatar(user, options = { })
1270 if Setting.gravatar_enabled?
1274 if Setting.gravatar_enabled?
1271 options.merge!({:ssl => (request && request.ssl?), :default => Setting.gravatar_default})
1275 options.merge!({:ssl => (request && request.ssl?), :default => Setting.gravatar_default})
1272 email = nil
1276 email = nil
1273 if user.respond_to?(:mail)
1277 if user.respond_to?(:mail)
1274 email = user.mail
1278 email = user.mail
1275 elsif user.to_s =~ %r{<(.+?)>}
1279 elsif user.to_s =~ %r{<(.+?)>}
1276 email = $1
1280 email = $1
1277 end
1281 end
1278 return gravatar(email.to_s.downcase, options) unless email.blank? rescue nil
1282 return gravatar(email.to_s.downcase, options) unless email.blank? rescue nil
1279 else
1283 else
1280 ''
1284 ''
1281 end
1285 end
1282 end
1286 end
1283
1287
1284 def sanitize_anchor_name(anchor)
1288 def sanitize_anchor_name(anchor)
1285 anchor.gsub(%r{[^\s\-\p{Word}]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
1289 anchor.gsub(%r{[^\s\-\p{Word}]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
1286 end
1290 end
1287
1291
1288 # Returns the javascript tags that are included in the html layout head
1292 # Returns the javascript tags that are included in the html layout head
1289 def javascript_heads
1293 def javascript_heads
1290 tags = javascript_include_tag('jquery-1.11.1-ui-1.11.0-ujs-3.1.1', 'application')
1294 tags = javascript_include_tag('jquery-1.11.1-ui-1.11.0-ujs-3.1.1', 'application')
1291 unless User.current.pref.warn_on_leaving_unsaved == '0'
1295 unless User.current.pref.warn_on_leaving_unsaved == '0'
1292 tags << "\n".html_safe + javascript_tag("$(window).load(function(){ warnLeavingUnsaved('#{escape_javascript l(:text_warn_on_leaving_unsaved)}'); });")
1296 tags << "\n".html_safe + javascript_tag("$(window).load(function(){ warnLeavingUnsaved('#{escape_javascript l(:text_warn_on_leaving_unsaved)}'); });")
1293 end
1297 end
1294 tags
1298 tags
1295 end
1299 end
1296
1300
1297 def favicon
1301 def favicon
1298 "<link rel='shortcut icon' href='#{favicon_path}' />".html_safe
1302 "<link rel='shortcut icon' href='#{favicon_path}' />".html_safe
1299 end
1303 end
1300
1304
1301 # Returns the path to the favicon
1305 # Returns the path to the favicon
1302 def favicon_path
1306 def favicon_path
1303 icon = (current_theme && current_theme.favicon?) ? current_theme.favicon_path : '/favicon.ico'
1307 icon = (current_theme && current_theme.favicon?) ? current_theme.favicon_path : '/favicon.ico'
1304 image_path(icon)
1308 image_path(icon)
1305 end
1309 end
1306
1310
1307 # Returns the full URL to the favicon
1311 # Returns the full URL to the favicon
1308 def favicon_url
1312 def favicon_url
1309 # TODO: use #image_url introduced in Rails4
1313 # TODO: use #image_url introduced in Rails4
1310 path = favicon_path
1314 path = favicon_path
1311 base = url_for(:controller => 'welcome', :action => 'index', :only_path => false)
1315 base = url_for(:controller => 'welcome', :action => 'index', :only_path => false)
1312 base.sub(%r{/+$},'') + '/' + path.sub(%r{^/+},'')
1316 base.sub(%r{/+$},'') + '/' + path.sub(%r{^/+},'')
1313 end
1317 end
1314
1318
1315 def robot_exclusion_tag
1319 def robot_exclusion_tag
1316 '<meta name="robots" content="noindex,follow,noarchive" />'.html_safe
1320 '<meta name="robots" content="noindex,follow,noarchive" />'.html_safe
1317 end
1321 end
1318
1322
1319 # Returns true if arg is expected in the API response
1323 # Returns true if arg is expected in the API response
1320 def include_in_api_response?(arg)
1324 def include_in_api_response?(arg)
1321 unless @included_in_api_response
1325 unless @included_in_api_response
1322 param = params[:include]
1326 param = params[:include]
1323 @included_in_api_response = param.is_a?(Array) ? param.collect(&:to_s) : param.to_s.split(',')
1327 @included_in_api_response = param.is_a?(Array) ? param.collect(&:to_s) : param.to_s.split(',')
1324 @included_in_api_response.collect!(&:strip)
1328 @included_in_api_response.collect!(&:strip)
1325 end
1329 end
1326 @included_in_api_response.include?(arg.to_s)
1330 @included_in_api_response.include?(arg.to_s)
1327 end
1331 end
1328
1332
1329 # Returns options or nil if nometa param or X-Redmine-Nometa header
1333 # Returns options or nil if nometa param or X-Redmine-Nometa header
1330 # was set in the request
1334 # was set in the request
1331 def api_meta(options)
1335 def api_meta(options)
1332 if params[:nometa].present? || request.headers['X-Redmine-Nometa']
1336 if params[:nometa].present? || request.headers['X-Redmine-Nometa']
1333 # compatibility mode for activeresource clients that raise
1337 # compatibility mode for activeresource clients that raise
1334 # an error when deserializing an array with attributes
1338 # an error when deserializing an array with attributes
1335 nil
1339 nil
1336 else
1340 else
1337 options
1341 options
1338 end
1342 end
1339 end
1343 end
1340
1344
1341 private
1345 private
1342
1346
1343 def wiki_helper
1347 def wiki_helper
1344 helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
1348 helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
1345 extend helper
1349 extend helper
1346 return self
1350 return self
1347 end
1351 end
1348
1352
1349 def link_to_content_update(text, url_params = {}, html_options = {})
1353 def link_to_content_update(text, url_params = {}, html_options = {})
1350 link_to(text, url_params, html_options)
1354 link_to(text, url_params, html_options)
1351 end
1355 end
1352 end
1356 end
@@ -1,1500 +1,1506
1 # encoding: utf-8
1 # encoding: utf-8
2 #
2 #
3 # Redmine - project management software
3 # Redmine - project management software
4 # Copyright (C) 2006-2014 Jean-Philippe Lang
4 # Copyright (C) 2006-2014 Jean-Philippe Lang
5 #
5 #
6 # This program is free software; you can redistribute it and/or
6 # This program is free software; you can redistribute it and/or
7 # modify it under the terms of the GNU General Public License
7 # modify it under the terms of the GNU General Public License
8 # as published by the Free Software Foundation; either version 2
8 # as published by the Free Software Foundation; either version 2
9 # of the License, or (at your option) any later version.
9 # of the License, or (at your option) any later version.
10 #
10 #
11 # This program is distributed in the hope that it will be useful,
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
14 # GNU General Public License for more details.
15 #
15 #
16 # You should have received a copy of the GNU General Public License
16 # You should have received a copy of the GNU General Public License
17 # along with this program; if not, write to the Free Software
17 # along with this program; if not, write to the Free Software
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19
19
20 require File.expand_path('../../../test_helper', __FILE__)
20 require File.expand_path('../../../test_helper', __FILE__)
21
21
22 class ApplicationHelperTest < ActionView::TestCase
22 class ApplicationHelperTest < ActionView::TestCase
23 include Redmine::I18n
23 include Redmine::I18n
24 include ERB::Util
24 include ERB::Util
25 include Rails.application.routes.url_helpers
25 include Rails.application.routes.url_helpers
26
26
27 fixtures :projects, :roles, :enabled_modules, :users,
27 fixtures :projects, :roles, :enabled_modules, :users,
28 :repositories, :changesets,
28 :repositories, :changesets,
29 :projects_trackers,
29 :projects_trackers,
30 :trackers, :issue_statuses, :issues, :versions, :documents,
30 :trackers, :issue_statuses, :issues, :versions, :documents,
31 :wikis, :wiki_pages, :wiki_contents,
31 :wikis, :wiki_pages, :wiki_contents,
32 :boards, :messages, :news,
32 :boards, :messages, :news,
33 :attachments, :enumerations
33 :attachments, :enumerations
34
34
35 def setup
35 def setup
36 super
36 super
37 set_tmp_attachments_directory
37 set_tmp_attachments_directory
38 @russian_test = "\xd1\x82\xd0\xb5\xd1\x81\xd1\x82".force_encoding('UTF-8')
38 @russian_test = "\xd1\x82\xd0\xb5\xd1\x81\xd1\x82".force_encoding('UTF-8')
39 end
39 end
40
40
41 test "#link_to_if_authorized for authorized user should allow using the :controller and :action for the target link" do
41 test "#link_to_if_authorized for authorized user should allow using the :controller and :action for the target link" do
42 User.current = User.find_by_login('admin')
42 User.current = User.find_by_login('admin')
43
43
44 @project = Issue.first.project # Used by helper
44 @project = Issue.first.project # Used by helper
45 response = link_to_if_authorized('By controller/actionr',
45 response = link_to_if_authorized('By controller/actionr',
46 {:controller => 'issues', :action => 'edit', :id => Issue.first.id})
46 {:controller => 'issues', :action => 'edit', :id => Issue.first.id})
47 assert_match /href/, response
47 assert_match /href/, response
48 end
48 end
49
49
50 test "#link_to_if_authorized for unauthorized user should display nothing if user isn't authorized" do
50 test "#link_to_if_authorized for unauthorized user should display nothing if user isn't authorized" do
51 User.current = User.find_by_login('dlopper')
51 User.current = User.find_by_login('dlopper')
52 @project = Project.find('private-child')
52 @project = Project.find('private-child')
53 issue = @project.issues.first
53 issue = @project.issues.first
54 assert !issue.visible?
54 assert !issue.visible?
55
55
56 response = link_to_if_authorized('Never displayed',
56 response = link_to_if_authorized('Never displayed',
57 {:controller => 'issues', :action => 'show', :id => issue})
57 {:controller => 'issues', :action => 'show', :id => issue})
58 assert_nil response
58 assert_nil response
59 end
59 end
60
60
61 def test_auto_links
61 def test_auto_links
62 to_test = {
62 to_test = {
63 'http://foo.bar' => '<a class="external" href="http://foo.bar">http://foo.bar</a>',
63 'http://foo.bar' => '<a class="external" href="http://foo.bar">http://foo.bar</a>',
64 'http://foo.bar/~user' => '<a class="external" href="http://foo.bar/~user">http://foo.bar/~user</a>',
64 'http://foo.bar/~user' => '<a class="external" href="http://foo.bar/~user">http://foo.bar/~user</a>',
65 'http://foo.bar.' => '<a class="external" href="http://foo.bar">http://foo.bar</a>.',
65 'http://foo.bar.' => '<a class="external" href="http://foo.bar">http://foo.bar</a>.',
66 'https://foo.bar.' => '<a class="external" href="https://foo.bar">https://foo.bar</a>.',
66 'https://foo.bar.' => '<a class="external" href="https://foo.bar">https://foo.bar</a>.',
67 'This is a link: http://foo.bar.' => 'This is a link: <a class="external" href="http://foo.bar">http://foo.bar</a>.',
67 'This is a link: http://foo.bar.' => 'This is a link: <a class="external" href="http://foo.bar">http://foo.bar</a>.',
68 'A link (eg. http://foo.bar).' => 'A link (eg. <a class="external" href="http://foo.bar">http://foo.bar</a>).',
68 'A link (eg. http://foo.bar).' => 'A link (eg. <a class="external" href="http://foo.bar">http://foo.bar</a>).',
69 'http://foo.bar/foo.bar#foo.bar.' => '<a class="external" href="http://foo.bar/foo.bar#foo.bar">http://foo.bar/foo.bar#foo.bar</a>.',
69 'http://foo.bar/foo.bar#foo.bar.' => '<a class="external" href="http://foo.bar/foo.bar#foo.bar">http://foo.bar/foo.bar#foo.bar</a>.',
70 'http://www.foo.bar/Test_(foobar)' => '<a class="external" href="http://www.foo.bar/Test_(foobar)">http://www.foo.bar/Test_(foobar)</a>',
70 'http://www.foo.bar/Test_(foobar)' => '<a class="external" href="http://www.foo.bar/Test_(foobar)">http://www.foo.bar/Test_(foobar)</a>',
71 '(see inline link : http://www.foo.bar/Test_(foobar))' => '(see inline link : <a class="external" href="http://www.foo.bar/Test_(foobar)">http://www.foo.bar/Test_(foobar)</a>)',
71 '(see inline link : http://www.foo.bar/Test_(foobar))' => '(see inline link : <a class="external" href="http://www.foo.bar/Test_(foobar)">http://www.foo.bar/Test_(foobar)</a>)',
72 '(see inline link : http://www.foo.bar/Test)' => '(see inline link : <a class="external" href="http://www.foo.bar/Test">http://www.foo.bar/Test</a>)',
72 '(see inline link : http://www.foo.bar/Test)' => '(see inline link : <a class="external" href="http://www.foo.bar/Test">http://www.foo.bar/Test</a>)',
73 '(see inline link : http://www.foo.bar/Test).' => '(see inline link : <a class="external" href="http://www.foo.bar/Test">http://www.foo.bar/Test</a>).',
73 '(see inline link : http://www.foo.bar/Test).' => '(see inline link : <a class="external" href="http://www.foo.bar/Test">http://www.foo.bar/Test</a>).',
74 '(see "inline link":http://www.foo.bar/Test_(foobar))' => '(see <a href="http://www.foo.bar/Test_(foobar)" class="external">inline link</a>)',
74 '(see "inline link":http://www.foo.bar/Test_(foobar))' => '(see <a href="http://www.foo.bar/Test_(foobar)" class="external">inline link</a>)',
75 '(see "inline link":http://www.foo.bar/Test)' => '(see <a href="http://www.foo.bar/Test" class="external">inline link</a>)',
75 '(see "inline link":http://www.foo.bar/Test)' => '(see <a href="http://www.foo.bar/Test" class="external">inline link</a>)',
76 '(see "inline link":http://www.foo.bar/Test).' => '(see <a href="http://www.foo.bar/Test" class="external">inline link</a>).',
76 '(see "inline link":http://www.foo.bar/Test).' => '(see <a href="http://www.foo.bar/Test" class="external">inline link</a>).',
77 'www.foo.bar' => '<a class="external" href="http://www.foo.bar">www.foo.bar</a>',
77 'www.foo.bar' => '<a class="external" href="http://www.foo.bar">www.foo.bar</a>',
78 'http://foo.bar/page?p=1&t=z&s=' => '<a class="external" href="http://foo.bar/page?p=1&#38;t=z&#38;s=">http://foo.bar/page?p=1&#38;t=z&#38;s=</a>',
78 'http://foo.bar/page?p=1&t=z&s=' => '<a class="external" href="http://foo.bar/page?p=1&#38;t=z&#38;s=">http://foo.bar/page?p=1&#38;t=z&#38;s=</a>',
79 'http://foo.bar/page#125' => '<a class="external" href="http://foo.bar/page#125">http://foo.bar/page#125</a>',
79 'http://foo.bar/page#125' => '<a class="external" href="http://foo.bar/page#125">http://foo.bar/page#125</a>',
80 'http://foo@www.bar.com' => '<a class="external" href="http://foo@www.bar.com">http://foo@www.bar.com</a>',
80 'http://foo@www.bar.com' => '<a class="external" href="http://foo@www.bar.com">http://foo@www.bar.com</a>',
81 'http://foo:bar@www.bar.com' => '<a class="external" href="http://foo:bar@www.bar.com">http://foo:bar@www.bar.com</a>',
81 'http://foo:bar@www.bar.com' => '<a class="external" href="http://foo:bar@www.bar.com">http://foo:bar@www.bar.com</a>',
82 'ftp://foo.bar' => '<a class="external" href="ftp://foo.bar">ftp://foo.bar</a>',
82 'ftp://foo.bar' => '<a class="external" href="ftp://foo.bar">ftp://foo.bar</a>',
83 'ftps://foo.bar' => '<a class="external" href="ftps://foo.bar">ftps://foo.bar</a>',
83 'ftps://foo.bar' => '<a class="external" href="ftps://foo.bar">ftps://foo.bar</a>',
84 'sftp://foo.bar' => '<a class="external" href="sftp://foo.bar">sftp://foo.bar</a>',
84 'sftp://foo.bar' => '<a class="external" href="sftp://foo.bar">sftp://foo.bar</a>',
85 # two exclamation marks
85 # two exclamation marks
86 'http://example.net/path!602815048C7B5C20!302.html' => '<a class="external" href="http://example.net/path!602815048C7B5C20!302.html">http://example.net/path!602815048C7B5C20!302.html</a>',
86 'http://example.net/path!602815048C7B5C20!302.html' => '<a class="external" href="http://example.net/path!602815048C7B5C20!302.html">http://example.net/path!602815048C7B5C20!302.html</a>',
87 # escaping
87 # escaping
88 'http://foo"bar' => '<a class="external" href="http://foo&quot;bar">http://foo&quot;bar</a>',
88 'http://foo"bar' => '<a class="external" href="http://foo&quot;bar">http://foo&quot;bar</a>',
89 # wrap in angle brackets
89 # wrap in angle brackets
90 '<http://foo.bar>' => '&lt;<a class="external" href="http://foo.bar">http://foo.bar</a>&gt;',
90 '<http://foo.bar>' => '&lt;<a class="external" href="http://foo.bar">http://foo.bar</a>&gt;',
91 # invalid urls
91 # invalid urls
92 'http://' => 'http://',
92 'http://' => 'http://',
93 'www.' => 'www.',
93 'www.' => 'www.',
94 'test-www.bar.com' => 'test-www.bar.com',
94 'test-www.bar.com' => 'test-www.bar.com',
95 }
95 }
96 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
96 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
97 end
97 end
98
98
99 def test_auto_links_with_non_ascii_characters
99 def test_auto_links_with_non_ascii_characters
100 to_test = {
100 to_test = {
101 "http://foo.bar/#{@russian_test}" =>
101 "http://foo.bar/#{@russian_test}" =>
102 %|<a class="external" href="http://foo.bar/#{@russian_test}">http://foo.bar/#{@russian_test}</a>|
102 %|<a class="external" href="http://foo.bar/#{@russian_test}">http://foo.bar/#{@russian_test}</a>|
103 }
103 }
104 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
104 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
105 end
105 end
106
106
107 def test_auto_mailto
107 def test_auto_mailto
108 to_test = {
108 to_test = {
109 'test@foo.bar' => '<a class="email" href="mailto:test@foo.bar">test@foo.bar</a>',
109 'test@foo.bar' => '<a class="email" href="mailto:test@foo.bar">test@foo.bar</a>',
110 'test@www.foo.bar' => '<a class="email" href="mailto:test@www.foo.bar">test@www.foo.bar</a>',
110 'test@www.foo.bar' => '<a class="email" href="mailto:test@www.foo.bar">test@www.foo.bar</a>',
111 }
111 }
112 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
112 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
113 end
113 end
114
114
115 def test_inline_images
115 def test_inline_images
116 to_test = {
116 to_test = {
117 '!http://foo.bar/image.jpg!' => '<img src="http://foo.bar/image.jpg" alt="" />',
117 '!http://foo.bar/image.jpg!' => '<img src="http://foo.bar/image.jpg" alt="" />',
118 'floating !>http://foo.bar/image.jpg!' => 'floating <div style="float:right"><img src="http://foo.bar/image.jpg" alt="" /></div>',
118 'floating !>http://foo.bar/image.jpg!' => 'floating <div style="float:right"><img src="http://foo.bar/image.jpg" alt="" /></div>',
119 'with class !(some-class)http://foo.bar/image.jpg!' => 'with class <img src="http://foo.bar/image.jpg" class="some-class" alt="" />',
119 'with class !(some-class)http://foo.bar/image.jpg!' => 'with class <img src="http://foo.bar/image.jpg" class="some-class" alt="" />',
120 'with style !{width:100px;height:100px}http://foo.bar/image.jpg!' => 'with style <img src="http://foo.bar/image.jpg" style="width:100px;height:100px;" alt="" />',
120 'with style !{width:100px;height:100px}http://foo.bar/image.jpg!' => 'with style <img src="http://foo.bar/image.jpg" style="width:100px;height:100px;" alt="" />',
121 'with title !http://foo.bar/image.jpg(This is a title)!' => 'with title <img src="http://foo.bar/image.jpg" title="This is a title" alt="This is a title" />',
121 'with title !http://foo.bar/image.jpg(This is a title)!' => 'with title <img src="http://foo.bar/image.jpg" title="This is a title" alt="This is a title" />',
122 'with title !http://foo.bar/image.jpg(This is a double-quoted "title")!' => 'with title <img src="http://foo.bar/image.jpg" title="This is a double-quoted &quot;title&quot;" alt="This is a double-quoted &quot;title&quot;" />',
122 'with title !http://foo.bar/image.jpg(This is a double-quoted "title")!' => 'with title <img src="http://foo.bar/image.jpg" title="This is a double-quoted &quot;title&quot;" alt="This is a double-quoted &quot;title&quot;" />',
123 }
123 }
124 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
124 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
125 end
125 end
126
126
127 def test_inline_images_inside_tags
127 def test_inline_images_inside_tags
128 raw = <<-RAW
128 raw = <<-RAW
129 h1. !foo.png! Heading
129 h1. !foo.png! Heading
130
130
131 Centered image:
131 Centered image:
132
132
133 p=. !bar.gif!
133 p=. !bar.gif!
134 RAW
134 RAW
135
135
136 assert textilizable(raw).include?('<img src="foo.png" alt="" />')
136 assert textilizable(raw).include?('<img src="foo.png" alt="" />')
137 assert textilizable(raw).include?('<img src="bar.gif" alt="" />')
137 assert textilizable(raw).include?('<img src="bar.gif" alt="" />')
138 end
138 end
139
139
140 def test_attached_images
140 def test_attached_images
141 to_test = {
141 to_test = {
142 'Inline image: !logo.gif!' => 'Inline image: <img src="/attachments/download/3/logo.gif" title="This is a logo" alt="This is a logo" />',
142 'Inline image: !logo.gif!' => 'Inline image: <img src="/attachments/download/3/logo.gif" title="This is a logo" alt="This is a logo" />',
143 'Inline image: !logo.GIF!' => 'Inline image: <img src="/attachments/download/3/logo.gif" title="This is a logo" alt="This is a logo" />',
143 'Inline image: !logo.GIF!' => 'Inline image: <img src="/attachments/download/3/logo.gif" title="This is a logo" alt="This is a logo" />',
144 'No match: !ogo.gif!' => 'No match: <img src="ogo.gif" alt="" />',
144 'No match: !ogo.gif!' => 'No match: <img src="ogo.gif" alt="" />',
145 'No match: !ogo.GIF!' => 'No match: <img src="ogo.GIF" alt="" />',
145 'No match: !ogo.GIF!' => 'No match: <img src="ogo.GIF" alt="" />',
146 # link image
146 # link image
147 '!logo.gif!:http://foo.bar/' => '<a href="http://foo.bar/"><img src="/attachments/download/3/logo.gif" title="This is a logo" alt="This is a logo" /></a>',
147 '!logo.gif!:http://foo.bar/' => '<a href="http://foo.bar/"><img src="/attachments/download/3/logo.gif" title="This is a logo" alt="This is a logo" /></a>',
148 }
148 }
149 attachments = Attachment.all
149 attachments = Attachment.all
150 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text, :attachments => attachments) }
150 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text, :attachments => attachments) }
151 end
151 end
152
152
153 def test_attached_images_filename_extension
153 def test_attached_images_filename_extension
154 set_tmp_attachments_directory
154 set_tmp_attachments_directory
155 a1 = Attachment.new(
155 a1 = Attachment.new(
156 :container => Issue.find(1),
156 :container => Issue.find(1),
157 :file => mock_file_with_options({:original_filename => "testtest.JPG"}),
157 :file => mock_file_with_options({:original_filename => "testtest.JPG"}),
158 :author => User.find(1))
158 :author => User.find(1))
159 assert a1.save
159 assert a1.save
160 assert_equal "testtest.JPG", a1.filename
160 assert_equal "testtest.JPG", a1.filename
161 assert_equal "image/jpeg", a1.content_type
161 assert_equal "image/jpeg", a1.content_type
162 assert a1.image?
162 assert a1.image?
163
163
164 a2 = Attachment.new(
164 a2 = Attachment.new(
165 :container => Issue.find(1),
165 :container => Issue.find(1),
166 :file => mock_file_with_options({:original_filename => "testtest.jpeg"}),
166 :file => mock_file_with_options({:original_filename => "testtest.jpeg"}),
167 :author => User.find(1))
167 :author => User.find(1))
168 assert a2.save
168 assert a2.save
169 assert_equal "testtest.jpeg", a2.filename
169 assert_equal "testtest.jpeg", a2.filename
170 assert_equal "image/jpeg", a2.content_type
170 assert_equal "image/jpeg", a2.content_type
171 assert a2.image?
171 assert a2.image?
172
172
173 a3 = Attachment.new(
173 a3 = Attachment.new(
174 :container => Issue.find(1),
174 :container => Issue.find(1),
175 :file => mock_file_with_options({:original_filename => "testtest.JPE"}),
175 :file => mock_file_with_options({:original_filename => "testtest.JPE"}),
176 :author => User.find(1))
176 :author => User.find(1))
177 assert a3.save
177 assert a3.save
178 assert_equal "testtest.JPE", a3.filename
178 assert_equal "testtest.JPE", a3.filename
179 assert_equal "image/jpeg", a3.content_type
179 assert_equal "image/jpeg", a3.content_type
180 assert a3.image?
180 assert a3.image?
181
181
182 a4 = Attachment.new(
182 a4 = Attachment.new(
183 :container => Issue.find(1),
183 :container => Issue.find(1),
184 :file => mock_file_with_options({:original_filename => "Testtest.BMP"}),
184 :file => mock_file_with_options({:original_filename => "Testtest.BMP"}),
185 :author => User.find(1))
185 :author => User.find(1))
186 assert a4.save
186 assert a4.save
187 assert_equal "Testtest.BMP", a4.filename
187 assert_equal "Testtest.BMP", a4.filename
188 assert_equal "image/x-ms-bmp", a4.content_type
188 assert_equal "image/x-ms-bmp", a4.content_type
189 assert a4.image?
189 assert a4.image?
190
190
191 to_test = {
191 to_test = {
192 'Inline image: !testtest.jpg!' =>
192 'Inline image: !testtest.jpg!' =>
193 'Inline image: <img src="/attachments/download/' + a1.id.to_s + '/testtest.JPG" alt="" />',
193 'Inline image: <img src="/attachments/download/' + a1.id.to_s + '/testtest.JPG" alt="" />',
194 'Inline image: !testtest.jpeg!' =>
194 'Inline image: !testtest.jpeg!' =>
195 'Inline image: <img src="/attachments/download/' + a2.id.to_s + '/testtest.jpeg" alt="" />',
195 'Inline image: <img src="/attachments/download/' + a2.id.to_s + '/testtest.jpeg" alt="" />',
196 'Inline image: !testtest.jpe!' =>
196 'Inline image: !testtest.jpe!' =>
197 'Inline image: <img src="/attachments/download/' + a3.id.to_s + '/testtest.JPE" alt="" />',
197 'Inline image: <img src="/attachments/download/' + a3.id.to_s + '/testtest.JPE" alt="" />',
198 'Inline image: !testtest.bmp!' =>
198 'Inline image: !testtest.bmp!' =>
199 'Inline image: <img src="/attachments/download/' + a4.id.to_s + '/Testtest.BMP" alt="" />',
199 'Inline image: <img src="/attachments/download/' + a4.id.to_s + '/Testtest.BMP" alt="" />',
200 }
200 }
201
201
202 attachments = [a1, a2, a3, a4]
202 attachments = [a1, a2, a3, a4]
203 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text, :attachments => attachments) }
203 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text, :attachments => attachments) }
204 end
204 end
205
205
206 def test_attached_images_should_read_later
206 def test_attached_images_should_read_later
207 set_fixtures_attachments_directory
207 set_fixtures_attachments_directory
208 a1 = Attachment.find(16)
208 a1 = Attachment.find(16)
209 assert_equal "testfile.png", a1.filename
209 assert_equal "testfile.png", a1.filename
210 assert a1.readable?
210 assert a1.readable?
211 assert (! a1.visible?(User.anonymous))
211 assert (! a1.visible?(User.anonymous))
212 assert a1.visible?(User.find(2))
212 assert a1.visible?(User.find(2))
213 a2 = Attachment.find(17)
213 a2 = Attachment.find(17)
214 assert_equal "testfile.PNG", a2.filename
214 assert_equal "testfile.PNG", a2.filename
215 assert a2.readable?
215 assert a2.readable?
216 assert (! a2.visible?(User.anonymous))
216 assert (! a2.visible?(User.anonymous))
217 assert a2.visible?(User.find(2))
217 assert a2.visible?(User.find(2))
218 assert a1.created_on < a2.created_on
218 assert a1.created_on < a2.created_on
219
219
220 to_test = {
220 to_test = {
221 'Inline image: !testfile.png!' =>
221 'Inline image: !testfile.png!' =>
222 'Inline image: <img src="/attachments/download/' + a2.id.to_s + '/testfile.PNG" alt="" />',
222 'Inline image: <img src="/attachments/download/' + a2.id.to_s + '/testfile.PNG" alt="" />',
223 'Inline image: !Testfile.PNG!' =>
223 'Inline image: !Testfile.PNG!' =>
224 'Inline image: <img src="/attachments/download/' + a2.id.to_s + '/testfile.PNG" alt="" />',
224 'Inline image: <img src="/attachments/download/' + a2.id.to_s + '/testfile.PNG" alt="" />',
225 }
225 }
226 attachments = [a1, a2]
226 attachments = [a1, a2]
227 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text, :attachments => attachments) }
227 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text, :attachments => attachments) }
228 set_tmp_attachments_directory
228 set_tmp_attachments_directory
229 end
229 end
230
230
231 def test_textile_external_links
231 def test_textile_external_links
232 to_test = {
232 to_test = {
233 'This is a "link":http://foo.bar' => 'This is a <a href="http://foo.bar" class="external">link</a>',
233 'This is a "link":http://foo.bar' => 'This is a <a href="http://foo.bar" class="external">link</a>',
234 'This is an intern "link":/foo/bar' => 'This is an intern <a href="/foo/bar">link</a>',
234 'This is an intern "link":/foo/bar' => 'This is an intern <a href="/foo/bar">link</a>',
235 '"link (Link title)":http://foo.bar' => '<a href="http://foo.bar" title="Link title" class="external">link</a>',
235 '"link (Link title)":http://foo.bar' => '<a href="http://foo.bar" title="Link title" class="external">link</a>',
236 '"link (Link title with "double-quotes")":http://foo.bar' => '<a href="http://foo.bar" title="Link title with &quot;double-quotes&quot;" class="external">link</a>',
236 '"link (Link title with "double-quotes")":http://foo.bar' => '<a href="http://foo.bar" title="Link title with &quot;double-quotes&quot;" class="external">link</a>',
237 "This is not a \"Link\":\n\nAnother paragraph" => "This is not a \"Link\":</p>\n\n\n\t<p>Another paragraph",
237 "This is not a \"Link\":\n\nAnother paragraph" => "This is not a \"Link\":</p>\n\n\n\t<p>Another paragraph",
238 # no multiline link text
238 # no multiline link text
239 "This is a double quote \"on the first line\nand another on a second line\":test" => "This is a double quote \"on the first line<br />and another on a second line\":test",
239 "This is a double quote \"on the first line\nand another on a second line\":test" => "This is a double quote \"on the first line<br />and another on a second line\":test",
240 # mailto link
240 # mailto link
241 "\"system administrator\":mailto:sysadmin@example.com?subject=redmine%20permissions" => "<a href=\"mailto:sysadmin@example.com?subject=redmine%20permissions\">system administrator</a>",
241 "\"system administrator\":mailto:sysadmin@example.com?subject=redmine%20permissions" => "<a href=\"mailto:sysadmin@example.com?subject=redmine%20permissions\">system administrator</a>",
242 # two exclamation marks
242 # two exclamation marks
243 '"a link":http://example.net/path!602815048C7B5C20!302.html' => '<a href="http://example.net/path!602815048C7B5C20!302.html" class="external">a link</a>',
243 '"a link":http://example.net/path!602815048C7B5C20!302.html' => '<a href="http://example.net/path!602815048C7B5C20!302.html" class="external">a link</a>',
244 # escaping
244 # escaping
245 '"test":http://foo"bar' => '<a href="http://foo&quot;bar" class="external">test</a>',
245 '"test":http://foo"bar' => '<a href="http://foo&quot;bar" class="external">test</a>',
246 }
246 }
247 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
247 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
248 end
248 end
249
249
250 def test_textile_external_links_with_non_ascii_characters
250 def test_textile_external_links_with_non_ascii_characters
251 to_test = {
251 to_test = {
252 %|This is a "link":http://foo.bar/#{@russian_test}| =>
252 %|This is a "link":http://foo.bar/#{@russian_test}| =>
253 %|This is a <a href="http://foo.bar/#{@russian_test}" class="external">link</a>|
253 %|This is a <a href="http://foo.bar/#{@russian_test}" class="external">link</a>|
254 }
254 }
255 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
255 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
256 end
256 end
257
257
258 def test_redmine_links
258 def test_redmine_links
259 issue_link = link_to('#3', {:controller => 'issues', :action => 'show', :id => 3},
259 issue_link = link_to('#3', {:controller => 'issues', :action => 'show', :id => 3},
260 :class => Issue.find(3).css_classes, :title => 'Error 281 when updating a recipe (New)')
260 :class => Issue.find(3).css_classes, :title => 'Error 281 when updating a recipe (New)')
261 note_link = link_to('#3-14', {:controller => 'issues', :action => 'show', :id => 3, :anchor => 'note-14'},
261 note_link = link_to('#3-14', {:controller => 'issues', :action => 'show', :id => 3, :anchor => 'note-14'},
262 :class => Issue.find(3).css_classes, :title => 'Error 281 when updating a recipe (New)')
262 :class => Issue.find(3).css_classes, :title => 'Error 281 when updating a recipe (New)')
263 note_link2 = link_to('#3#note-14', {:controller => 'issues', :action => 'show', :id => 3, :anchor => 'note-14'},
263 note_link2 = link_to('#3#note-14', {:controller => 'issues', :action => 'show', :id => 3, :anchor => 'note-14'},
264 :class => Issue.find(3).css_classes, :title => 'Error 281 when updating a recipe (New)')
264 :class => Issue.find(3).css_classes, :title => 'Error 281 when updating a recipe (New)')
265
265
266 revision_link = link_to('r1', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :rev => 1},
266 revision_link = link_to('r1', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :rev => 1},
267 :class => 'changeset', :title => 'My very first commit do not escaping #<>&')
267 :class => 'changeset', :title => 'My very first commit do not escaping #<>&')
268 revision_link2 = link_to('r2', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :rev => 2},
268 revision_link2 = link_to('r2', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :rev => 2},
269 :class => 'changeset', :title => 'This commit fixes #1, #2 and references #1 & #3')
269 :class => 'changeset', :title => 'This commit fixes #1, #2 and references #1 & #3')
270
270
271 changeset_link2 = link_to('691322a8eb01e11fd7',
271 changeset_link2 = link_to('691322a8eb01e11fd7',
272 {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :rev => 1},
272 {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :rev => 1},
273 :class => 'changeset', :title => 'My very first commit do not escaping #<>&')
273 :class => 'changeset', :title => 'My very first commit do not escaping #<>&')
274
274
275 document_link = link_to('Test document', {:controller => 'documents', :action => 'show', :id => 1},
275 document_link = link_to('Test document', {:controller => 'documents', :action => 'show', :id => 1},
276 :class => 'document')
276 :class => 'document')
277
277
278 version_link = link_to('1.0', {:controller => 'versions', :action => 'show', :id => 2},
278 version_link = link_to('1.0', {:controller => 'versions', :action => 'show', :id => 2},
279 :class => 'version')
279 :class => 'version')
280
280
281 board_url = {:controller => 'boards', :action => 'show', :id => 2, :project_id => 'ecookbook'}
281 board_url = {:controller => 'boards', :action => 'show', :id => 2, :project_id => 'ecookbook'}
282
282
283 message_url = {:controller => 'messages', :action => 'show', :board_id => 1, :id => 4}
283 message_url = {:controller => 'messages', :action => 'show', :board_id => 1, :id => 4}
284
284
285 news_url = {:controller => 'news', :action => 'show', :id => 1}
285 news_url = {:controller => 'news', :action => 'show', :id => 1}
286
286
287 project_url = {:controller => 'projects', :action => 'show', :id => 'subproject1'}
287 project_url = {:controller => 'projects', :action => 'show', :id => 'subproject1'}
288
288
289 source_url = '/projects/ecookbook/repository/entry/some/file'
289 source_url = '/projects/ecookbook/repository/entry/some/file'
290 source_url_with_rev = '/projects/ecookbook/repository/revisions/52/entry/some/file'
290 source_url_with_rev = '/projects/ecookbook/repository/revisions/52/entry/some/file'
291 source_url_with_ext = '/projects/ecookbook/repository/entry/some/file.ext'
291 source_url_with_ext = '/projects/ecookbook/repository/entry/some/file.ext'
292 source_url_with_rev_and_ext = '/projects/ecookbook/repository/revisions/52/entry/some/file.ext'
292 source_url_with_rev_and_ext = '/projects/ecookbook/repository/revisions/52/entry/some/file.ext'
293 source_url_with_branch = '/projects/ecookbook/repository/revisions/branch/entry/some/file'
293 source_url_with_branch = '/projects/ecookbook/repository/revisions/branch/entry/some/file'
294
294
295 export_url = '/projects/ecookbook/repository/raw/some/file'
295 export_url = '/projects/ecookbook/repository/raw/some/file'
296 export_url_with_rev = '/projects/ecookbook/repository/revisions/52/raw/some/file'
296 export_url_with_rev = '/projects/ecookbook/repository/revisions/52/raw/some/file'
297 export_url_with_ext = '/projects/ecookbook/repository/raw/some/file.ext'
297 export_url_with_ext = '/projects/ecookbook/repository/raw/some/file.ext'
298 export_url_with_rev_and_ext = '/projects/ecookbook/repository/revisions/52/raw/some/file.ext'
298 export_url_with_rev_and_ext = '/projects/ecookbook/repository/revisions/52/raw/some/file.ext'
299 export_url_with_branch = '/projects/ecookbook/repository/revisions/branch/raw/some/file'
299 export_url_with_branch = '/projects/ecookbook/repository/revisions/branch/raw/some/file'
300
300
301 to_test = {
301 to_test = {
302 # tickets
302 # tickets
303 '#3, [#3], (#3) and #3.' => "#{issue_link}, [#{issue_link}], (#{issue_link}) and #{issue_link}.",
303 '#3, [#3], (#3) and #3.' => "#{issue_link}, [#{issue_link}], (#{issue_link}) and #{issue_link}.",
304 # ticket notes
304 # ticket notes
305 '#3-14' => note_link,
305 '#3-14' => note_link,
306 '#3#note-14' => note_link2,
306 '#3#note-14' => note_link2,
307 # should not ignore leading zero
307 # should not ignore leading zero
308 '#03' => '#03',
308 '#03' => '#03',
309 # changesets
309 # changesets
310 'r1' => revision_link,
310 'r1' => revision_link,
311 'r1.' => "#{revision_link}.",
311 'r1.' => "#{revision_link}.",
312 'r1, r2' => "#{revision_link}, #{revision_link2}",
312 'r1, r2' => "#{revision_link}, #{revision_link2}",
313 'r1,r2' => "#{revision_link},#{revision_link2}",
313 'r1,r2' => "#{revision_link},#{revision_link2}",
314 'commit:691322a8eb01e11fd7' => changeset_link2,
314 'commit:691322a8eb01e11fd7' => changeset_link2,
315 # documents
315 # documents
316 'document#1' => document_link,
316 'document#1' => document_link,
317 'document:"Test document"' => document_link,
317 'document:"Test document"' => document_link,
318 # versions
318 # versions
319 'version#2' => version_link,
319 'version#2' => version_link,
320 'version:1.0' => version_link,
320 'version:1.0' => version_link,
321 'version:"1.0"' => version_link,
321 'version:"1.0"' => version_link,
322 # source
322 # source
323 'source:some/file' => link_to('source:some/file', source_url, :class => 'source'),
323 'source:some/file' => link_to('source:some/file', source_url, :class => 'source'),
324 'source:/some/file' => link_to('source:/some/file', source_url, :class => 'source'),
324 'source:/some/file' => link_to('source:/some/file', source_url, :class => 'source'),
325 'source:/some/file.' => link_to('source:/some/file', source_url, :class => 'source') + ".",
325 'source:/some/file.' => link_to('source:/some/file', source_url, :class => 'source') + ".",
326 'source:/some/file.ext.' => link_to('source:/some/file.ext', source_url_with_ext, :class => 'source') + ".",
326 'source:/some/file.ext.' => link_to('source:/some/file.ext', source_url_with_ext, :class => 'source') + ".",
327 'source:/some/file. ' => link_to('source:/some/file', source_url, :class => 'source') + ".",
327 'source:/some/file. ' => link_to('source:/some/file', source_url, :class => 'source') + ".",
328 'source:/some/file.ext. ' => link_to('source:/some/file.ext', source_url_with_ext, :class => 'source') + ".",
328 'source:/some/file.ext. ' => link_to('source:/some/file.ext', source_url_with_ext, :class => 'source') + ".",
329 'source:/some/file, ' => link_to('source:/some/file', source_url, :class => 'source') + ",",
329 'source:/some/file, ' => link_to('source:/some/file', source_url, :class => 'source') + ",",
330 'source:/some/file@52' => link_to('source:/some/file@52', source_url_with_rev, :class => 'source'),
330 'source:/some/file@52' => link_to('source:/some/file@52', source_url_with_rev, :class => 'source'),
331 'source:/some/file@branch' => link_to('source:/some/file@branch', source_url_with_branch, :class => 'source'),
331 'source:/some/file@branch' => link_to('source:/some/file@branch', source_url_with_branch, :class => 'source'),
332 'source:/some/file.ext@52' => link_to('source:/some/file.ext@52', source_url_with_rev_and_ext, :class => 'source'),
332 'source:/some/file.ext@52' => link_to('source:/some/file.ext@52', source_url_with_rev_and_ext, :class => 'source'),
333 'source:/some/file#L110' => link_to('source:/some/file#L110', source_url + "#L110", :class => 'source'),
333 'source:/some/file#L110' => link_to('source:/some/file#L110', source_url + "#L110", :class => 'source'),
334 'source:/some/file.ext#L110' => link_to('source:/some/file.ext#L110', source_url_with_ext + "#L110", :class => 'source'),
334 'source:/some/file.ext#L110' => link_to('source:/some/file.ext#L110', source_url_with_ext + "#L110", :class => 'source'),
335 'source:/some/file@52#L110' => link_to('source:/some/file@52#L110', source_url_with_rev + "#L110", :class => 'source'),
335 'source:/some/file@52#L110' => link_to('source:/some/file@52#L110', source_url_with_rev + "#L110", :class => 'source'),
336 # export
336 # export
337 'export:/some/file' => link_to('export:/some/file', export_url, :class => 'source download'),
337 'export:/some/file' => link_to('export:/some/file', export_url, :class => 'source download'),
338 'export:/some/file.ext' => link_to('export:/some/file.ext', export_url_with_ext, :class => 'source download'),
338 'export:/some/file.ext' => link_to('export:/some/file.ext', export_url_with_ext, :class => 'source download'),
339 'export:/some/file@52' => link_to('export:/some/file@52', export_url_with_rev, :class => 'source download'),
339 'export:/some/file@52' => link_to('export:/some/file@52', export_url_with_rev, :class => 'source download'),
340 'export:/some/file.ext@52' => link_to('export:/some/file.ext@52', export_url_with_rev_and_ext, :class => 'source download'),
340 'export:/some/file.ext@52' => link_to('export:/some/file.ext@52', export_url_with_rev_and_ext, :class => 'source download'),
341 'export:/some/file@branch' => link_to('export:/some/file@branch', export_url_with_branch, :class => 'source download'),
341 'export:/some/file@branch' => link_to('export:/some/file@branch', export_url_with_branch, :class => 'source download'),
342 # forum
342 # forum
343 'forum#2' => link_to('Discussion', board_url, :class => 'board'),
343 'forum#2' => link_to('Discussion', board_url, :class => 'board'),
344 'forum:Discussion' => link_to('Discussion', board_url, :class => 'board'),
344 'forum:Discussion' => link_to('Discussion', board_url, :class => 'board'),
345 # message
345 # message
346 'message#4' => link_to('Post 2', message_url, :class => 'message'),
346 'message#4' => link_to('Post 2', message_url, :class => 'message'),
347 'message#5' => link_to('RE: post 2', message_url.merge(:anchor => 'message-5', :r => 5), :class => 'message'),
347 'message#5' => link_to('RE: post 2', message_url.merge(:anchor => 'message-5', :r => 5), :class => 'message'),
348 # news
348 # news
349 'news#1' => link_to('eCookbook first release !', news_url, :class => 'news'),
349 'news#1' => link_to('eCookbook first release !', news_url, :class => 'news'),
350 'news:"eCookbook first release !"' => link_to('eCookbook first release !', news_url, :class => 'news'),
350 'news:"eCookbook first release !"' => link_to('eCookbook first release !', news_url, :class => 'news'),
351 # project
351 # project
352 'project#3' => link_to('eCookbook Subproject 1', project_url, :class => 'project'),
352 'project#3' => link_to('eCookbook Subproject 1', project_url, :class => 'project'),
353 'project:subproject1' => link_to('eCookbook Subproject 1', project_url, :class => 'project'),
353 'project:subproject1' => link_to('eCookbook Subproject 1', project_url, :class => 'project'),
354 'project:"eCookbook subProject 1"' => link_to('eCookbook Subproject 1', project_url, :class => 'project'),
354 'project:"eCookbook subProject 1"' => link_to('eCookbook Subproject 1', project_url, :class => 'project'),
355 # not found
355 # not found
356 '#0123456789' => '#0123456789',
356 '#0123456789' => '#0123456789',
357 # invalid expressions
357 # invalid expressions
358 'source:' => 'source:',
358 'source:' => 'source:',
359 # url hash
359 # url hash
360 "http://foo.bar/FAQ#3" => '<a class="external" href="http://foo.bar/FAQ#3">http://foo.bar/FAQ#3</a>',
360 "http://foo.bar/FAQ#3" => '<a class="external" href="http://foo.bar/FAQ#3">http://foo.bar/FAQ#3</a>',
361 }
361 }
362 @project = Project.find(1)
362 @project = Project.find(1)
363 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text), "#{text} failed" }
363 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text), "#{text} failed" }
364 end
364 end
365
365
366 def test_should_not_parse_redmine_links_inside_link
367 raw = "r1 should not be parsed in http://example.com/url-r1/"
368 assert_match %r{<p><a class="changeset".*>r1</a> should not be parsed in <a class="external" href="http://example.com/url-r1/">http://example.com/url-r1/</a></p>},
369 textilizable(raw, :project => Project.find(1))
370 end
371
366 def test_redmine_links_with_a_different_project_before_current_project
372 def test_redmine_links_with_a_different_project_before_current_project
367 vp1 = Version.generate!(:project_id => 1, :name => '1.4.4')
373 vp1 = Version.generate!(:project_id => 1, :name => '1.4.4')
368 vp3 = Version.generate!(:project_id => 3, :name => '1.4.4')
374 vp3 = Version.generate!(:project_id => 3, :name => '1.4.4')
369 @project = Project.find(3)
375 @project = Project.find(3)
370 result1 = link_to("1.4.4", "/versions/#{vp1.id}", :class => "version")
376 result1 = link_to("1.4.4", "/versions/#{vp1.id}", :class => "version")
371 result2 = link_to("1.4.4", "/versions/#{vp3.id}", :class => "version")
377 result2 = link_to("1.4.4", "/versions/#{vp3.id}", :class => "version")
372 assert_equal "<p>#{result1} #{result2}</p>",
378 assert_equal "<p>#{result1} #{result2}</p>",
373 textilizable("ecookbook:version:1.4.4 version:1.4.4")
379 textilizable("ecookbook:version:1.4.4 version:1.4.4")
374 end
380 end
375
381
376 def test_escaped_redmine_links_should_not_be_parsed
382 def test_escaped_redmine_links_should_not_be_parsed
377 to_test = [
383 to_test = [
378 '#3.',
384 '#3.',
379 '#3-14.',
385 '#3-14.',
380 '#3#-note14.',
386 '#3#-note14.',
381 'r1',
387 'r1',
382 'document#1',
388 'document#1',
383 'document:"Test document"',
389 'document:"Test document"',
384 'version#2',
390 'version#2',
385 'version:1.0',
391 'version:1.0',
386 'version:"1.0"',
392 'version:"1.0"',
387 'source:/some/file'
393 'source:/some/file'
388 ]
394 ]
389 @project = Project.find(1)
395 @project = Project.find(1)
390 to_test.each { |text| assert_equal "<p>#{text}</p>", textilizable("!" + text), "#{text} failed" }
396 to_test.each { |text| assert_equal "<p>#{text}</p>", textilizable("!" + text), "#{text} failed" }
391 end
397 end
392
398
393 def test_cross_project_redmine_links
399 def test_cross_project_redmine_links
394 source_link = link_to('ecookbook:source:/some/file',
400 source_link = link_to('ecookbook:source:/some/file',
395 {:controller => 'repositories', :action => 'entry',
401 {:controller => 'repositories', :action => 'entry',
396 :id => 'ecookbook', :path => ['some', 'file']},
402 :id => 'ecookbook', :path => ['some', 'file']},
397 :class => 'source')
403 :class => 'source')
398 changeset_link = link_to('ecookbook:r2',
404 changeset_link = link_to('ecookbook:r2',
399 {:controller => 'repositories', :action => 'revision',
405 {:controller => 'repositories', :action => 'revision',
400 :id => 'ecookbook', :rev => 2},
406 :id => 'ecookbook', :rev => 2},
401 :class => 'changeset',
407 :class => 'changeset',
402 :title => 'This commit fixes #1, #2 and references #1 & #3')
408 :title => 'This commit fixes #1, #2 and references #1 & #3')
403 to_test = {
409 to_test = {
404 # documents
410 # documents
405 'document:"Test document"' => 'document:"Test document"',
411 'document:"Test document"' => 'document:"Test document"',
406 'ecookbook:document:"Test document"' =>
412 'ecookbook:document:"Test document"' =>
407 link_to("Test document", "/documents/1", :class => "document"),
413 link_to("Test document", "/documents/1", :class => "document"),
408 'invalid:document:"Test document"' => 'invalid:document:"Test document"',
414 'invalid:document:"Test document"' => 'invalid:document:"Test document"',
409 # versions
415 # versions
410 'version:"1.0"' => 'version:"1.0"',
416 'version:"1.0"' => 'version:"1.0"',
411 'ecookbook:version:"1.0"' =>
417 'ecookbook:version:"1.0"' =>
412 link_to("1.0", "/versions/2", :class => "version"),
418 link_to("1.0", "/versions/2", :class => "version"),
413 'invalid:version:"1.0"' => 'invalid:version:"1.0"',
419 'invalid:version:"1.0"' => 'invalid:version:"1.0"',
414 # changeset
420 # changeset
415 'r2' => 'r2',
421 'r2' => 'r2',
416 'ecookbook:r2' => changeset_link,
422 'ecookbook:r2' => changeset_link,
417 'invalid:r2' => 'invalid:r2',
423 'invalid:r2' => 'invalid:r2',
418 # source
424 # source
419 'source:/some/file' => 'source:/some/file',
425 'source:/some/file' => 'source:/some/file',
420 'ecookbook:source:/some/file' => source_link,
426 'ecookbook:source:/some/file' => source_link,
421 'invalid:source:/some/file' => 'invalid:source:/some/file',
427 'invalid:source:/some/file' => 'invalid:source:/some/file',
422 }
428 }
423 @project = Project.find(3)
429 @project = Project.find(3)
424 to_test.each do |text, result|
430 to_test.each do |text, result|
425 assert_equal "<p>#{result}</p>", textilizable(text), "#{text} failed"
431 assert_equal "<p>#{result}</p>", textilizable(text), "#{text} failed"
426 end
432 end
427 end
433 end
428
434
429 def test_redmine_links_by_name_should_work_with_html_escaped_characters
435 def test_redmine_links_by_name_should_work_with_html_escaped_characters
430 v = Version.generate!(:name => "Test & Show.txt", :project_id => 1)
436 v = Version.generate!(:name => "Test & Show.txt", :project_id => 1)
431 link = link_to("Test & Show.txt", "/versions/#{v.id}", :class => "version")
437 link = link_to("Test & Show.txt", "/versions/#{v.id}", :class => "version")
432
438
433 @project = v.project
439 @project = v.project
434 assert_equal "<p>#{link}</p>", textilizable('version:"Test & Show.txt"')
440 assert_equal "<p>#{link}</p>", textilizable('version:"Test & Show.txt"')
435 end
441 end
436
442
437 def test_link_to_issue_subject
443 def test_link_to_issue_subject
438 issue = Issue.generate!(:subject => "01234567890123456789")
444 issue = Issue.generate!(:subject => "01234567890123456789")
439 str = link_to_issue(issue, :truncate => 10)
445 str = link_to_issue(issue, :truncate => 10)
440 result = link_to("Bug ##{issue.id}", "/issues/#{issue.id}", :class => issue.css_classes)
446 result = link_to("Bug ##{issue.id}", "/issues/#{issue.id}", :class => issue.css_classes)
441 assert_equal "#{result}: 0123456...", str
447 assert_equal "#{result}: 0123456...", str
442
448
443 issue = Issue.generate!(:subject => "<&>")
449 issue = Issue.generate!(:subject => "<&>")
444 str = link_to_issue(issue)
450 str = link_to_issue(issue)
445 result = link_to("Bug ##{issue.id}", "/issues/#{issue.id}", :class => issue.css_classes)
451 result = link_to("Bug ##{issue.id}", "/issues/#{issue.id}", :class => issue.css_classes)
446 assert_equal "#{result}: &lt;&amp;&gt;", str
452 assert_equal "#{result}: &lt;&amp;&gt;", str
447
453
448 issue = Issue.generate!(:subject => "<&>0123456789012345")
454 issue = Issue.generate!(:subject => "<&>0123456789012345")
449 str = link_to_issue(issue, :truncate => 10)
455 str = link_to_issue(issue, :truncate => 10)
450 result = link_to("Bug ##{issue.id}", "/issues/#{issue.id}", :class => issue.css_classes)
456 result = link_to("Bug ##{issue.id}", "/issues/#{issue.id}", :class => issue.css_classes)
451 assert_equal "#{result}: &lt;&amp;&gt;0123...", str
457 assert_equal "#{result}: &lt;&amp;&gt;0123...", str
452 end
458 end
453
459
454 def test_link_to_issue_title
460 def test_link_to_issue_title
455 long_str = "0123456789" * 5
461 long_str = "0123456789" * 5
456
462
457 issue = Issue.generate!(:subject => "#{long_str}01234567890123456789")
463 issue = Issue.generate!(:subject => "#{long_str}01234567890123456789")
458 str = link_to_issue(issue, :subject => false)
464 str = link_to_issue(issue, :subject => false)
459 result = link_to("Bug ##{issue.id}", "/issues/#{issue.id}",
465 result = link_to("Bug ##{issue.id}", "/issues/#{issue.id}",
460 :class => issue.css_classes,
466 :class => issue.css_classes,
461 :title => "#{long_str}0123456...")
467 :title => "#{long_str}0123456...")
462 assert_equal result, str
468 assert_equal result, str
463
469
464 issue = Issue.generate!(:subject => "<&>#{long_str}01234567890123456789")
470 issue = Issue.generate!(:subject => "<&>#{long_str}01234567890123456789")
465 str = link_to_issue(issue, :subject => false)
471 str = link_to_issue(issue, :subject => false)
466 result = link_to("Bug ##{issue.id}", "/issues/#{issue.id}",
472 result = link_to("Bug ##{issue.id}", "/issues/#{issue.id}",
467 :class => issue.css_classes,
473 :class => issue.css_classes,
468 :title => "<&>#{long_str}0123...")
474 :title => "<&>#{long_str}0123...")
469 assert_equal result, str
475 assert_equal result, str
470 end
476 end
471
477
472 def test_multiple_repositories_redmine_links
478 def test_multiple_repositories_redmine_links
473 svn = Repository::Subversion.create!(:project_id => 1, :identifier => 'svn_repo-1', :url => 'file:///foo/hg')
479 svn = Repository::Subversion.create!(:project_id => 1, :identifier => 'svn_repo-1', :url => 'file:///foo/hg')
474 Changeset.create!(:repository => svn, :committed_on => Time.now, :revision => '123')
480 Changeset.create!(:repository => svn, :committed_on => Time.now, :revision => '123')
475 hg = Repository::Mercurial.create!(:project_id => 1, :identifier => 'hg1', :url => '/foo/hg')
481 hg = Repository::Mercurial.create!(:project_id => 1, :identifier => 'hg1', :url => '/foo/hg')
476 Changeset.create!(:repository => hg, :committed_on => Time.now, :revision => '123', :scmid => 'abcd')
482 Changeset.create!(:repository => hg, :committed_on => Time.now, :revision => '123', :scmid => 'abcd')
477
483
478 changeset_link = link_to('r2', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :rev => 2},
484 changeset_link = link_to('r2', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :rev => 2},
479 :class => 'changeset', :title => 'This commit fixes #1, #2 and references #1 & #3')
485 :class => 'changeset', :title => 'This commit fixes #1, #2 and references #1 & #3')
480 svn_changeset_link = link_to('svn_repo-1|r123', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :repository_id => 'svn_repo-1', :rev => 123},
486 svn_changeset_link = link_to('svn_repo-1|r123', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :repository_id => 'svn_repo-1', :rev => 123},
481 :class => 'changeset', :title => '')
487 :class => 'changeset', :title => '')
482 hg_changeset_link = link_to('hg1|abcd', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :repository_id => 'hg1', :rev => 'abcd'},
488 hg_changeset_link = link_to('hg1|abcd', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :repository_id => 'hg1', :rev => 'abcd'},
483 :class => 'changeset', :title => '')
489 :class => 'changeset', :title => '')
484
490
485 source_link = link_to('source:some/file', {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :path => ['some', 'file']}, :class => 'source')
491 source_link = link_to('source:some/file', {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :path => ['some', 'file']}, :class => 'source')
486 hg_source_link = link_to('source:hg1|some/file', {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :repository_id => 'hg1', :path => ['some', 'file']}, :class => 'source')
492 hg_source_link = link_to('source:hg1|some/file', {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :repository_id => 'hg1', :path => ['some', 'file']}, :class => 'source')
487
493
488 to_test = {
494 to_test = {
489 'r2' => changeset_link,
495 'r2' => changeset_link,
490 'svn_repo-1|r123' => svn_changeset_link,
496 'svn_repo-1|r123' => svn_changeset_link,
491 'invalid|r123' => 'invalid|r123',
497 'invalid|r123' => 'invalid|r123',
492 'commit:hg1|abcd' => hg_changeset_link,
498 'commit:hg1|abcd' => hg_changeset_link,
493 'commit:invalid|abcd' => 'commit:invalid|abcd',
499 'commit:invalid|abcd' => 'commit:invalid|abcd',
494 # source
500 # source
495 'source:some/file' => source_link,
501 'source:some/file' => source_link,
496 'source:hg1|some/file' => hg_source_link,
502 'source:hg1|some/file' => hg_source_link,
497 'source:invalid|some/file' => 'source:invalid|some/file',
503 'source:invalid|some/file' => 'source:invalid|some/file',
498 }
504 }
499
505
500 @project = Project.find(1)
506 @project = Project.find(1)
501 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text), "#{text} failed" }
507 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text), "#{text} failed" }
502 end
508 end
503
509
504 def test_cross_project_multiple_repositories_redmine_links
510 def test_cross_project_multiple_repositories_redmine_links
505 svn = Repository::Subversion.create!(:project_id => 1, :identifier => 'svn1', :url => 'file:///foo/hg')
511 svn = Repository::Subversion.create!(:project_id => 1, :identifier => 'svn1', :url => 'file:///foo/hg')
506 Changeset.create!(:repository => svn, :committed_on => Time.now, :revision => '123')
512 Changeset.create!(:repository => svn, :committed_on => Time.now, :revision => '123')
507 hg = Repository::Mercurial.create!(:project_id => 1, :identifier => 'hg1', :url => '/foo/hg')
513 hg = Repository::Mercurial.create!(:project_id => 1, :identifier => 'hg1', :url => '/foo/hg')
508 Changeset.create!(:repository => hg, :committed_on => Time.now, :revision => '123', :scmid => 'abcd')
514 Changeset.create!(:repository => hg, :committed_on => Time.now, :revision => '123', :scmid => 'abcd')
509
515
510 changeset_link = link_to('ecookbook:r2', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :rev => 2},
516 changeset_link = link_to('ecookbook:r2', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :rev => 2},
511 :class => 'changeset', :title => 'This commit fixes #1, #2 and references #1 & #3')
517 :class => 'changeset', :title => 'This commit fixes #1, #2 and references #1 & #3')
512 svn_changeset_link = link_to('ecookbook:svn1|r123', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :repository_id => 'svn1', :rev => 123},
518 svn_changeset_link = link_to('ecookbook:svn1|r123', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :repository_id => 'svn1', :rev => 123},
513 :class => 'changeset', :title => '')
519 :class => 'changeset', :title => '')
514 hg_changeset_link = link_to('ecookbook:hg1|abcd', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :repository_id => 'hg1', :rev => 'abcd'},
520 hg_changeset_link = link_to('ecookbook:hg1|abcd', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :repository_id => 'hg1', :rev => 'abcd'},
515 :class => 'changeset', :title => '')
521 :class => 'changeset', :title => '')
516
522
517 source_link = link_to('ecookbook:source:some/file', {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :path => ['some', 'file']}, :class => 'source')
523 source_link = link_to('ecookbook:source:some/file', {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :path => ['some', 'file']}, :class => 'source')
518 hg_source_link = link_to('ecookbook:source:hg1|some/file', {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :repository_id => 'hg1', :path => ['some', 'file']}, :class => 'source')
524 hg_source_link = link_to('ecookbook:source:hg1|some/file', {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :repository_id => 'hg1', :path => ['some', 'file']}, :class => 'source')
519
525
520 to_test = {
526 to_test = {
521 'ecookbook:r2' => changeset_link,
527 'ecookbook:r2' => changeset_link,
522 'ecookbook:svn1|r123' => svn_changeset_link,
528 'ecookbook:svn1|r123' => svn_changeset_link,
523 'ecookbook:invalid|r123' => 'ecookbook:invalid|r123',
529 'ecookbook:invalid|r123' => 'ecookbook:invalid|r123',
524 'ecookbook:commit:hg1|abcd' => hg_changeset_link,
530 'ecookbook:commit:hg1|abcd' => hg_changeset_link,
525 'ecookbook:commit:invalid|abcd' => 'ecookbook:commit:invalid|abcd',
531 'ecookbook:commit:invalid|abcd' => 'ecookbook:commit:invalid|abcd',
526 'invalid:commit:invalid|abcd' => 'invalid:commit:invalid|abcd',
532 'invalid:commit:invalid|abcd' => 'invalid:commit:invalid|abcd',
527 # source
533 # source
528 'ecookbook:source:some/file' => source_link,
534 'ecookbook:source:some/file' => source_link,
529 'ecookbook:source:hg1|some/file' => hg_source_link,
535 'ecookbook:source:hg1|some/file' => hg_source_link,
530 'ecookbook:source:invalid|some/file' => 'ecookbook:source:invalid|some/file',
536 'ecookbook:source:invalid|some/file' => 'ecookbook:source:invalid|some/file',
531 'invalid:source:invalid|some/file' => 'invalid:source:invalid|some/file',
537 'invalid:source:invalid|some/file' => 'invalid:source:invalid|some/file',
532 }
538 }
533
539
534 @project = Project.find(3)
540 @project = Project.find(3)
535 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text), "#{text} failed" }
541 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text), "#{text} failed" }
536 end
542 end
537
543
538 def test_redmine_links_git_commit
544 def test_redmine_links_git_commit
539 changeset_link = link_to('abcd',
545 changeset_link = link_to('abcd',
540 {
546 {
541 :controller => 'repositories',
547 :controller => 'repositories',
542 :action => 'revision',
548 :action => 'revision',
543 :id => 'subproject1',
549 :id => 'subproject1',
544 :rev => 'abcd',
550 :rev => 'abcd',
545 },
551 },
546 :class => 'changeset', :title => 'test commit')
552 :class => 'changeset', :title => 'test commit')
547 to_test = {
553 to_test = {
548 'commit:abcd' => changeset_link,
554 'commit:abcd' => changeset_link,
549 }
555 }
550 @project = Project.find(3)
556 @project = Project.find(3)
551 r = Repository::Git.create!(:project => @project, :url => '/tmp/test/git')
557 r = Repository::Git.create!(:project => @project, :url => '/tmp/test/git')
552 assert r
558 assert r
553 c = Changeset.new(:repository => r,
559 c = Changeset.new(:repository => r,
554 :committed_on => Time.now,
560 :committed_on => Time.now,
555 :revision => 'abcd',
561 :revision => 'abcd',
556 :scmid => 'abcd',
562 :scmid => 'abcd',
557 :comments => 'test commit')
563 :comments => 'test commit')
558 assert( c.save )
564 assert( c.save )
559 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
565 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
560 end
566 end
561
567
562 # TODO: Bazaar commit id contains mail address, so it contains '@' and '_'.
568 # TODO: Bazaar commit id contains mail address, so it contains '@' and '_'.
563 def test_redmine_links_darcs_commit
569 def test_redmine_links_darcs_commit
564 changeset_link = link_to('20080308225258-98289-abcd456efg.gz',
570 changeset_link = link_to('20080308225258-98289-abcd456efg.gz',
565 {
571 {
566 :controller => 'repositories',
572 :controller => 'repositories',
567 :action => 'revision',
573 :action => 'revision',
568 :id => 'subproject1',
574 :id => 'subproject1',
569 :rev => '123',
575 :rev => '123',
570 },
576 },
571 :class => 'changeset', :title => 'test commit')
577 :class => 'changeset', :title => 'test commit')
572 to_test = {
578 to_test = {
573 'commit:20080308225258-98289-abcd456efg.gz' => changeset_link,
579 'commit:20080308225258-98289-abcd456efg.gz' => changeset_link,
574 }
580 }
575 @project = Project.find(3)
581 @project = Project.find(3)
576 r = Repository::Darcs.create!(
582 r = Repository::Darcs.create!(
577 :project => @project, :url => '/tmp/test/darcs',
583 :project => @project, :url => '/tmp/test/darcs',
578 :log_encoding => 'UTF-8')
584 :log_encoding => 'UTF-8')
579 assert r
585 assert r
580 c = Changeset.new(:repository => r,
586 c = Changeset.new(:repository => r,
581 :committed_on => Time.now,
587 :committed_on => Time.now,
582 :revision => '123',
588 :revision => '123',
583 :scmid => '20080308225258-98289-abcd456efg.gz',
589 :scmid => '20080308225258-98289-abcd456efg.gz',
584 :comments => 'test commit')
590 :comments => 'test commit')
585 assert( c.save )
591 assert( c.save )
586 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
592 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
587 end
593 end
588
594
589 def test_redmine_links_mercurial_commit
595 def test_redmine_links_mercurial_commit
590 changeset_link_rev = link_to('r123',
596 changeset_link_rev = link_to('r123',
591 {
597 {
592 :controller => 'repositories',
598 :controller => 'repositories',
593 :action => 'revision',
599 :action => 'revision',
594 :id => 'subproject1',
600 :id => 'subproject1',
595 :rev => '123' ,
601 :rev => '123' ,
596 },
602 },
597 :class => 'changeset', :title => 'test commit')
603 :class => 'changeset', :title => 'test commit')
598 changeset_link_commit = link_to('abcd',
604 changeset_link_commit = link_to('abcd',
599 {
605 {
600 :controller => 'repositories',
606 :controller => 'repositories',
601 :action => 'revision',
607 :action => 'revision',
602 :id => 'subproject1',
608 :id => 'subproject1',
603 :rev => 'abcd' ,
609 :rev => 'abcd' ,
604 },
610 },
605 :class => 'changeset', :title => 'test commit')
611 :class => 'changeset', :title => 'test commit')
606 to_test = {
612 to_test = {
607 'r123' => changeset_link_rev,
613 'r123' => changeset_link_rev,
608 'commit:abcd' => changeset_link_commit,
614 'commit:abcd' => changeset_link_commit,
609 }
615 }
610 @project = Project.find(3)
616 @project = Project.find(3)
611 r = Repository::Mercurial.create!(:project => @project, :url => '/tmp/test')
617 r = Repository::Mercurial.create!(:project => @project, :url => '/tmp/test')
612 assert r
618 assert r
613 c = Changeset.new(:repository => r,
619 c = Changeset.new(:repository => r,
614 :committed_on => Time.now,
620 :committed_on => Time.now,
615 :revision => '123',
621 :revision => '123',
616 :scmid => 'abcd',
622 :scmid => 'abcd',
617 :comments => 'test commit')
623 :comments => 'test commit')
618 assert( c.save )
624 assert( c.save )
619 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
625 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
620 end
626 end
621
627
622 def test_attachment_links
628 def test_attachment_links
623 text = 'attachment:error281.txt'
629 text = 'attachment:error281.txt'
624 result = link_to("error281.txt", "/attachments/download/1/error281.txt",
630 result = link_to("error281.txt", "/attachments/download/1/error281.txt",
625 :class => "attachment")
631 :class => "attachment")
626 assert_equal "<p>#{result}</p>",
632 assert_equal "<p>#{result}</p>",
627 textilizable(text,
633 textilizable(text,
628 :attachments => Issue.find(3).attachments),
634 :attachments => Issue.find(3).attachments),
629 "#{text} failed"
635 "#{text} failed"
630 end
636 end
631
637
632 def test_attachment_link_should_link_to_latest_attachment
638 def test_attachment_link_should_link_to_latest_attachment
633 set_tmp_attachments_directory
639 set_tmp_attachments_directory
634 a1 = Attachment.generate!(:filename => "test.txt", :created_on => 1.hour.ago)
640 a1 = Attachment.generate!(:filename => "test.txt", :created_on => 1.hour.ago)
635 a2 = Attachment.generate!(:filename => "test.txt")
641 a2 = Attachment.generate!(:filename => "test.txt")
636 result = link_to("test.txt", "/attachments/download/#{a2.id}/test.txt",
642 result = link_to("test.txt", "/attachments/download/#{a2.id}/test.txt",
637 :class => "attachment")
643 :class => "attachment")
638 assert_equal "<p>#{result}</p>",
644 assert_equal "<p>#{result}</p>",
639 textilizable('attachment:test.txt', :attachments => [a1, a2])
645 textilizable('attachment:test.txt', :attachments => [a1, a2])
640 end
646 end
641
647
642 def test_wiki_links
648 def test_wiki_links
643 russian_eacape = CGI.escape(@russian_test)
649 russian_eacape = CGI.escape(@russian_test)
644 to_test = {
650 to_test = {
645 '[[CookBook documentation]]' =>
651 '[[CookBook documentation]]' =>
646 link_to("CookBook documentation",
652 link_to("CookBook documentation",
647 "/projects/ecookbook/wiki/CookBook_documentation",
653 "/projects/ecookbook/wiki/CookBook_documentation",
648 :class => "wiki-page"),
654 :class => "wiki-page"),
649 '[[Another page|Page]]' =>
655 '[[Another page|Page]]' =>
650 link_to("Page",
656 link_to("Page",
651 "/projects/ecookbook/wiki/Another_page",
657 "/projects/ecookbook/wiki/Another_page",
652 :class => "wiki-page"),
658 :class => "wiki-page"),
653 # title content should be formatted
659 # title content should be formatted
654 '[[Another page|With _styled_ *title*]]' =>
660 '[[Another page|With _styled_ *title*]]' =>
655 link_to("With <em>styled</em> <strong>title</strong>".html_safe,
661 link_to("With <em>styled</em> <strong>title</strong>".html_safe,
656 "/projects/ecookbook/wiki/Another_page",
662 "/projects/ecookbook/wiki/Another_page",
657 :class => "wiki-page"),
663 :class => "wiki-page"),
658 '[[Another page|With title containing <strong>HTML entities &amp; markups</strong>]]' =>
664 '[[Another page|With title containing <strong>HTML entities &amp; markups</strong>]]' =>
659 link_to("With title containing &lt;strong&gt;HTML entities &amp; markups&lt;/strong&gt;".html_safe,
665 link_to("With title containing &lt;strong&gt;HTML entities &amp; markups&lt;/strong&gt;".html_safe,
660 "/projects/ecookbook/wiki/Another_page",
666 "/projects/ecookbook/wiki/Another_page",
661 :class => "wiki-page"),
667 :class => "wiki-page"),
662 # link with anchor
668 # link with anchor
663 '[[CookBook documentation#One-section]]' =>
669 '[[CookBook documentation#One-section]]' =>
664 link_to("CookBook documentation",
670 link_to("CookBook documentation",
665 "/projects/ecookbook/wiki/CookBook_documentation#One-section",
671 "/projects/ecookbook/wiki/CookBook_documentation#One-section",
666 :class => "wiki-page"),
672 :class => "wiki-page"),
667 '[[Another page#anchor|Page]]' =>
673 '[[Another page#anchor|Page]]' =>
668 link_to("Page",
674 link_to("Page",
669 "/projects/ecookbook/wiki/Another_page#anchor",
675 "/projects/ecookbook/wiki/Another_page#anchor",
670 :class => "wiki-page"),
676 :class => "wiki-page"),
671 # UTF8 anchor
677 # UTF8 anchor
672 "[[Another_page##{@russian_test}|#{@russian_test}]]" =>
678 "[[Another_page##{@russian_test}|#{@russian_test}]]" =>
673 link_to(@russian_test,
679 link_to(@russian_test,
674 "/projects/ecookbook/wiki/Another_page##{russian_eacape}",
680 "/projects/ecookbook/wiki/Another_page##{russian_eacape}",
675 :class => "wiki-page"),
681 :class => "wiki-page"),
676 # page that doesn't exist
682 # page that doesn't exist
677 '[[Unknown page]]' =>
683 '[[Unknown page]]' =>
678 link_to("Unknown page",
684 link_to("Unknown page",
679 "/projects/ecookbook/wiki/Unknown_page",
685 "/projects/ecookbook/wiki/Unknown_page",
680 :class => "wiki-page new"),
686 :class => "wiki-page new"),
681 '[[Unknown page|404]]' =>
687 '[[Unknown page|404]]' =>
682 link_to("404",
688 link_to("404",
683 "/projects/ecookbook/wiki/Unknown_page",
689 "/projects/ecookbook/wiki/Unknown_page",
684 :class => "wiki-page new"),
690 :class => "wiki-page new"),
685 # link to another project wiki
691 # link to another project wiki
686 '[[onlinestore:]]' =>
692 '[[onlinestore:]]' =>
687 link_to("onlinestore",
693 link_to("onlinestore",
688 "/projects/onlinestore/wiki",
694 "/projects/onlinestore/wiki",
689 :class => "wiki-page"),
695 :class => "wiki-page"),
690 '[[onlinestore:|Wiki]]' =>
696 '[[onlinestore:|Wiki]]' =>
691 link_to("Wiki",
697 link_to("Wiki",
692 "/projects/onlinestore/wiki",
698 "/projects/onlinestore/wiki",
693 :class => "wiki-page"),
699 :class => "wiki-page"),
694 '[[onlinestore:Start page]]' =>
700 '[[onlinestore:Start page]]' =>
695 link_to("Start page",
701 link_to("Start page",
696 "/projects/onlinestore/wiki/Start_page",
702 "/projects/onlinestore/wiki/Start_page",
697 :class => "wiki-page"),
703 :class => "wiki-page"),
698 '[[onlinestore:Start page|Text]]' =>
704 '[[onlinestore:Start page|Text]]' =>
699 link_to("Text",
705 link_to("Text",
700 "/projects/onlinestore/wiki/Start_page",
706 "/projects/onlinestore/wiki/Start_page",
701 :class => "wiki-page"),
707 :class => "wiki-page"),
702 '[[onlinestore:Unknown page]]' =>
708 '[[onlinestore:Unknown page]]' =>
703 link_to("Unknown page",
709 link_to("Unknown page",
704 "/projects/onlinestore/wiki/Unknown_page",
710 "/projects/onlinestore/wiki/Unknown_page",
705 :class => "wiki-page new"),
711 :class => "wiki-page new"),
706 # struck through link
712 # struck through link
707 '-[[Another page|Page]]-' =>
713 '-[[Another page|Page]]-' =>
708 "<del>".html_safe +
714 "<del>".html_safe +
709 link_to("Page",
715 link_to("Page",
710 "/projects/ecookbook/wiki/Another_page",
716 "/projects/ecookbook/wiki/Another_page",
711 :class => "wiki-page").html_safe +
717 :class => "wiki-page").html_safe +
712 "</del>".html_safe,
718 "</del>".html_safe,
713 '-[[Another page|Page]] link-' =>
719 '-[[Another page|Page]] link-' =>
714 "<del>".html_safe +
720 "<del>".html_safe +
715 link_to("Page",
721 link_to("Page",
716 "/projects/ecookbook/wiki/Another_page",
722 "/projects/ecookbook/wiki/Another_page",
717 :class => "wiki-page").html_safe +
723 :class => "wiki-page").html_safe +
718 " link</del>".html_safe,
724 " link</del>".html_safe,
719 # escaping
725 # escaping
720 '![[Another page|Page]]' => '[[Another page|Page]]',
726 '![[Another page|Page]]' => '[[Another page|Page]]',
721 # project does not exist
727 # project does not exist
722 '[[unknowproject:Start]]' => '[[unknowproject:Start]]',
728 '[[unknowproject:Start]]' => '[[unknowproject:Start]]',
723 '[[unknowproject:Start|Page title]]' => '[[unknowproject:Start|Page title]]',
729 '[[unknowproject:Start|Page title]]' => '[[unknowproject:Start|Page title]]',
724 }
730 }
725 @project = Project.find(1)
731 @project = Project.find(1)
726 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
732 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
727 end
733 end
728
734
729 def test_wiki_links_within_local_file_generation_context
735 def test_wiki_links_within_local_file_generation_context
730 to_test = {
736 to_test = {
731 # link to a page
737 # link to a page
732 '[[CookBook documentation]]' =>
738 '[[CookBook documentation]]' =>
733 link_to("CookBook documentation", "CookBook_documentation.html",
739 link_to("CookBook documentation", "CookBook_documentation.html",
734 :class => "wiki-page"),
740 :class => "wiki-page"),
735 '[[CookBook documentation|documentation]]' =>
741 '[[CookBook documentation|documentation]]' =>
736 link_to("documentation", "CookBook_documentation.html",
742 link_to("documentation", "CookBook_documentation.html",
737 :class => "wiki-page"),
743 :class => "wiki-page"),
738 '[[CookBook documentation#One-section]]' =>
744 '[[CookBook documentation#One-section]]' =>
739 link_to("CookBook documentation", "CookBook_documentation.html#One-section",
745 link_to("CookBook documentation", "CookBook_documentation.html#One-section",
740 :class => "wiki-page"),
746 :class => "wiki-page"),
741 '[[CookBook documentation#One-section|documentation]]' =>
747 '[[CookBook documentation#One-section|documentation]]' =>
742 link_to("documentation", "CookBook_documentation.html#One-section",
748 link_to("documentation", "CookBook_documentation.html#One-section",
743 :class => "wiki-page"),
749 :class => "wiki-page"),
744 # page that doesn't exist
750 # page that doesn't exist
745 '[[Unknown page]]' =>
751 '[[Unknown page]]' =>
746 link_to("Unknown page", "Unknown_page.html",
752 link_to("Unknown page", "Unknown_page.html",
747 :class => "wiki-page new"),
753 :class => "wiki-page new"),
748 '[[Unknown page|404]]' =>
754 '[[Unknown page|404]]' =>
749 link_to("404", "Unknown_page.html",
755 link_to("404", "Unknown_page.html",
750 :class => "wiki-page new"),
756 :class => "wiki-page new"),
751 '[[Unknown page#anchor]]' =>
757 '[[Unknown page#anchor]]' =>
752 link_to("Unknown page", "Unknown_page.html#anchor",
758 link_to("Unknown page", "Unknown_page.html#anchor",
753 :class => "wiki-page new"),
759 :class => "wiki-page new"),
754 '[[Unknown page#anchor|404]]' =>
760 '[[Unknown page#anchor|404]]' =>
755 link_to("404", "Unknown_page.html#anchor",
761 link_to("404", "Unknown_page.html#anchor",
756 :class => "wiki-page new"),
762 :class => "wiki-page new"),
757 }
763 }
758 @project = Project.find(1)
764 @project = Project.find(1)
759 to_test.each do |text, result|
765 to_test.each do |text, result|
760 assert_equal "<p>#{result}</p>", textilizable(text, :wiki_links => :local)
766 assert_equal "<p>#{result}</p>", textilizable(text, :wiki_links => :local)
761 end
767 end
762 end
768 end
763
769
764 def test_wiki_links_within_wiki_page_context
770 def test_wiki_links_within_wiki_page_context
765 page = WikiPage.find_by_title('Another_page' )
771 page = WikiPage.find_by_title('Another_page' )
766 to_test = {
772 to_test = {
767 '[[CookBook documentation]]' =>
773 '[[CookBook documentation]]' =>
768 link_to("CookBook documentation",
774 link_to("CookBook documentation",
769 "/projects/ecookbook/wiki/CookBook_documentation",
775 "/projects/ecookbook/wiki/CookBook_documentation",
770 :class => "wiki-page"),
776 :class => "wiki-page"),
771 '[[CookBook documentation|documentation]]' =>
777 '[[CookBook documentation|documentation]]' =>
772 link_to("documentation",
778 link_to("documentation",
773 "/projects/ecookbook/wiki/CookBook_documentation",
779 "/projects/ecookbook/wiki/CookBook_documentation",
774 :class => "wiki-page"),
780 :class => "wiki-page"),
775 '[[CookBook documentation#One-section]]' =>
781 '[[CookBook documentation#One-section]]' =>
776 link_to("CookBook documentation",
782 link_to("CookBook documentation",
777 "/projects/ecookbook/wiki/CookBook_documentation#One-section",
783 "/projects/ecookbook/wiki/CookBook_documentation#One-section",
778 :class => "wiki-page"),
784 :class => "wiki-page"),
779 '[[CookBook documentation#One-section|documentation]]' =>
785 '[[CookBook documentation#One-section|documentation]]' =>
780 link_to("documentation",
786 link_to("documentation",
781 "/projects/ecookbook/wiki/CookBook_documentation#One-section",
787 "/projects/ecookbook/wiki/CookBook_documentation#One-section",
782 :class => "wiki-page"),
788 :class => "wiki-page"),
783 # link to the current page
789 # link to the current page
784 '[[Another page]]' =>
790 '[[Another page]]' =>
785 link_to("Another page",
791 link_to("Another page",
786 "/projects/ecookbook/wiki/Another_page",
792 "/projects/ecookbook/wiki/Another_page",
787 :class => "wiki-page"),
793 :class => "wiki-page"),
788 '[[Another page|Page]]' =>
794 '[[Another page|Page]]' =>
789 link_to("Page",
795 link_to("Page",
790 "/projects/ecookbook/wiki/Another_page",
796 "/projects/ecookbook/wiki/Another_page",
791 :class => "wiki-page"),
797 :class => "wiki-page"),
792 '[[Another page#anchor]]' =>
798 '[[Another page#anchor]]' =>
793 link_to("Another page",
799 link_to("Another page",
794 "#anchor",
800 "#anchor",
795 :class => "wiki-page"),
801 :class => "wiki-page"),
796 '[[Another page#anchor|Page]]' =>
802 '[[Another page#anchor|Page]]' =>
797 link_to("Page",
803 link_to("Page",
798 "#anchor",
804 "#anchor",
799 :class => "wiki-page"),
805 :class => "wiki-page"),
800 # page that doesn't exist
806 # page that doesn't exist
801 '[[Unknown page]]' =>
807 '[[Unknown page]]' =>
802 link_to("Unknown page",
808 link_to("Unknown page",
803 "/projects/ecookbook/wiki/Unknown_page?parent=Another_page",
809 "/projects/ecookbook/wiki/Unknown_page?parent=Another_page",
804 :class => "wiki-page new"),
810 :class => "wiki-page new"),
805 '[[Unknown page|404]]' =>
811 '[[Unknown page|404]]' =>
806 link_to("404",
812 link_to("404",
807 "/projects/ecookbook/wiki/Unknown_page?parent=Another_page",
813 "/projects/ecookbook/wiki/Unknown_page?parent=Another_page",
808 :class => "wiki-page new"),
814 :class => "wiki-page new"),
809 '[[Unknown page#anchor]]' =>
815 '[[Unknown page#anchor]]' =>
810 link_to("Unknown page",
816 link_to("Unknown page",
811 "/projects/ecookbook/wiki/Unknown_page?parent=Another_page#anchor",
817 "/projects/ecookbook/wiki/Unknown_page?parent=Another_page#anchor",
812 :class => "wiki-page new"),
818 :class => "wiki-page new"),
813 '[[Unknown page#anchor|404]]' =>
819 '[[Unknown page#anchor|404]]' =>
814 link_to("404",
820 link_to("404",
815 "/projects/ecookbook/wiki/Unknown_page?parent=Another_page#anchor",
821 "/projects/ecookbook/wiki/Unknown_page?parent=Another_page#anchor",
816 :class => "wiki-page new"),
822 :class => "wiki-page new"),
817 }
823 }
818 @project = Project.find(1)
824 @project = Project.find(1)
819 to_test.each do |text, result|
825 to_test.each do |text, result|
820 assert_equal "<p>#{result}</p>",
826 assert_equal "<p>#{result}</p>",
821 textilizable(WikiContent.new( :text => text, :page => page ), :text)
827 textilizable(WikiContent.new( :text => text, :page => page ), :text)
822 end
828 end
823 end
829 end
824
830
825 def test_wiki_links_anchor_option_should_prepend_page_title_to_href
831 def test_wiki_links_anchor_option_should_prepend_page_title_to_href
826 to_test = {
832 to_test = {
827 # link to a page
833 # link to a page
828 '[[CookBook documentation]]' =>
834 '[[CookBook documentation]]' =>
829 link_to("CookBook documentation",
835 link_to("CookBook documentation",
830 "#CookBook_documentation",
836 "#CookBook_documentation",
831 :class => "wiki-page"),
837 :class => "wiki-page"),
832 '[[CookBook documentation|documentation]]' =>
838 '[[CookBook documentation|documentation]]' =>
833 link_to("documentation",
839 link_to("documentation",
834 "#CookBook_documentation",
840 "#CookBook_documentation",
835 :class => "wiki-page"),
841 :class => "wiki-page"),
836 '[[CookBook documentation#One-section]]' =>
842 '[[CookBook documentation#One-section]]' =>
837 link_to("CookBook documentation",
843 link_to("CookBook documentation",
838 "#CookBook_documentation_One-section",
844 "#CookBook_documentation_One-section",
839 :class => "wiki-page"),
845 :class => "wiki-page"),
840 '[[CookBook documentation#One-section|documentation]]' =>
846 '[[CookBook documentation#One-section|documentation]]' =>
841 link_to("documentation",
847 link_to("documentation",
842 "#CookBook_documentation_One-section",
848 "#CookBook_documentation_One-section",
843 :class => "wiki-page"),
849 :class => "wiki-page"),
844 # page that doesn't exist
850 # page that doesn't exist
845 '[[Unknown page]]' =>
851 '[[Unknown page]]' =>
846 link_to("Unknown page",
852 link_to("Unknown page",
847 "#Unknown_page",
853 "#Unknown_page",
848 :class => "wiki-page new"),
854 :class => "wiki-page new"),
849 '[[Unknown page|404]]' =>
855 '[[Unknown page|404]]' =>
850 link_to("404",
856 link_to("404",
851 "#Unknown_page",
857 "#Unknown_page",
852 :class => "wiki-page new"),
858 :class => "wiki-page new"),
853 '[[Unknown page#anchor]]' =>
859 '[[Unknown page#anchor]]' =>
854 link_to("Unknown page",
860 link_to("Unknown page",
855 "#Unknown_page_anchor",
861 "#Unknown_page_anchor",
856 :class => "wiki-page new"),
862 :class => "wiki-page new"),
857 '[[Unknown page#anchor|404]]' =>
863 '[[Unknown page#anchor|404]]' =>
858 link_to("404",
864 link_to("404",
859 "#Unknown_page_anchor",
865 "#Unknown_page_anchor",
860 :class => "wiki-page new"),
866 :class => "wiki-page new"),
861 }
867 }
862 @project = Project.find(1)
868 @project = Project.find(1)
863 to_test.each do |text, result|
869 to_test.each do |text, result|
864 assert_equal "<p>#{result}</p>", textilizable(text, :wiki_links => :anchor)
870 assert_equal "<p>#{result}</p>", textilizable(text, :wiki_links => :anchor)
865 end
871 end
866 end
872 end
867
873
868 def test_html_tags
874 def test_html_tags
869 to_test = {
875 to_test = {
870 "<div>content</div>" => "<p>&lt;div&gt;content&lt;/div&gt;</p>",
876 "<div>content</div>" => "<p>&lt;div&gt;content&lt;/div&gt;</p>",
871 "<div class=\"bold\">content</div>" => "<p>&lt;div class=\"bold\"&gt;content&lt;/div&gt;</p>",
877 "<div class=\"bold\">content</div>" => "<p>&lt;div class=\"bold\"&gt;content&lt;/div&gt;</p>",
872 "<script>some script;</script>" => "<p>&lt;script&gt;some script;&lt;/script&gt;</p>",
878 "<script>some script;</script>" => "<p>&lt;script&gt;some script;&lt;/script&gt;</p>",
873 # do not escape pre/code tags
879 # do not escape pre/code tags
874 "<pre>\nline 1\nline2</pre>" => "<pre>\nline 1\nline2</pre>",
880 "<pre>\nline 1\nline2</pre>" => "<pre>\nline 1\nline2</pre>",
875 "<pre><code>\nline 1\nline2</code></pre>" => "<pre><code>\nline 1\nline2</code></pre>",
881 "<pre><code>\nline 1\nline2</code></pre>" => "<pre><code>\nline 1\nline2</code></pre>",
876 "<pre><div>content</div></pre>" => "<pre>&lt;div&gt;content&lt;/div&gt;</pre>",
882 "<pre><div>content</div></pre>" => "<pre>&lt;div&gt;content&lt;/div&gt;</pre>",
877 "HTML comment: <!-- no comments -->" => "<p>HTML comment: &lt;!-- no comments --&gt;</p>",
883 "HTML comment: <!-- no comments -->" => "<p>HTML comment: &lt;!-- no comments --&gt;</p>",
878 "<!-- opening comment" => "<p>&lt;!-- opening comment</p>",
884 "<!-- opening comment" => "<p>&lt;!-- opening comment</p>",
879 # remove attributes except class
885 # remove attributes except class
880 "<pre class='foo'>some text</pre>" => "<pre class='foo'>some text</pre>",
886 "<pre class='foo'>some text</pre>" => "<pre class='foo'>some text</pre>",
881 '<pre class="foo">some text</pre>' => '<pre class="foo">some text</pre>',
887 '<pre class="foo">some text</pre>' => '<pre class="foo">some text</pre>',
882 "<pre class='foo bar'>some text</pre>" => "<pre class='foo bar'>some text</pre>",
888 "<pre class='foo bar'>some text</pre>" => "<pre class='foo bar'>some text</pre>",
883 '<pre class="foo bar">some text</pre>' => '<pre class="foo bar">some text</pre>',
889 '<pre class="foo bar">some text</pre>' => '<pre class="foo bar">some text</pre>',
884 "<pre onmouseover='alert(1)'>some text</pre>" => "<pre>some text</pre>",
890 "<pre onmouseover='alert(1)'>some text</pre>" => "<pre>some text</pre>",
885 # xss
891 # xss
886 '<pre><code class=""onmouseover="alert(1)">text</code></pre>' => '<pre><code>text</code></pre>',
892 '<pre><code class=""onmouseover="alert(1)">text</code></pre>' => '<pre><code>text</code></pre>',
887 '<pre class=""onmouseover="alert(1)">text</pre>' => '<pre>text</pre>',
893 '<pre class=""onmouseover="alert(1)">text</pre>' => '<pre>text</pre>',
888 }
894 }
889 to_test.each { |text, result| assert_equal result, textilizable(text) }
895 to_test.each { |text, result| assert_equal result, textilizable(text) }
890 end
896 end
891
897
892 def test_allowed_html_tags
898 def test_allowed_html_tags
893 to_test = {
899 to_test = {
894 "<pre>preformatted text</pre>" => "<pre>preformatted text</pre>",
900 "<pre>preformatted text</pre>" => "<pre>preformatted text</pre>",
895 "<notextile>no *textile* formatting</notextile>" => "no *textile* formatting",
901 "<notextile>no *textile* formatting</notextile>" => "no *textile* formatting",
896 "<notextile>this is <tag>a tag</tag></notextile>" => "this is &lt;tag&gt;a tag&lt;/tag&gt;"
902 "<notextile>this is <tag>a tag</tag></notextile>" => "this is &lt;tag&gt;a tag&lt;/tag&gt;"
897 }
903 }
898 to_test.each { |text, result| assert_equal result, textilizable(text) }
904 to_test.each { |text, result| assert_equal result, textilizable(text) }
899 end
905 end
900
906
901 def test_pre_tags
907 def test_pre_tags
902 raw = <<-RAW
908 raw = <<-RAW
903 Before
909 Before
904
910
905 <pre>
911 <pre>
906 <prepared-statement-cache-size>32</prepared-statement-cache-size>
912 <prepared-statement-cache-size>32</prepared-statement-cache-size>
907 </pre>
913 </pre>
908
914
909 After
915 After
910 RAW
916 RAW
911
917
912 expected = <<-EXPECTED
918 expected = <<-EXPECTED
913 <p>Before</p>
919 <p>Before</p>
914 <pre>
920 <pre>
915 &lt;prepared-statement-cache-size&gt;32&lt;/prepared-statement-cache-size&gt;
921 &lt;prepared-statement-cache-size&gt;32&lt;/prepared-statement-cache-size&gt;
916 </pre>
922 </pre>
917 <p>After</p>
923 <p>After</p>
918 EXPECTED
924 EXPECTED
919
925
920 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
926 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
921 end
927 end
922
928
923 def test_pre_content_should_not_parse_wiki_and_redmine_links
929 def test_pre_content_should_not_parse_wiki_and_redmine_links
924 raw = <<-RAW
930 raw = <<-RAW
925 [[CookBook documentation]]
931 [[CookBook documentation]]
926
932
927 #1
933 #1
928
934
929 <pre>
935 <pre>
930 [[CookBook documentation]]
936 [[CookBook documentation]]
931
937
932 #1
938 #1
933 </pre>
939 </pre>
934 RAW
940 RAW
935
941
936 result1 = link_to("CookBook documentation",
942 result1 = link_to("CookBook documentation",
937 "/projects/ecookbook/wiki/CookBook_documentation",
943 "/projects/ecookbook/wiki/CookBook_documentation",
938 :class => "wiki-page")
944 :class => "wiki-page")
939 result2 = link_to('#1',
945 result2 = link_to('#1',
940 "/issues/1",
946 "/issues/1",
941 :class => Issue.find(1).css_classes,
947 :class => Issue.find(1).css_classes,
942 :title => "Can't print recipes (New)")
948 :title => "Can't print recipes (New)")
943
949
944 expected = <<-EXPECTED
950 expected = <<-EXPECTED
945 <p>#{result1}</p>
951 <p>#{result1}</p>
946 <p>#{result2}</p>
952 <p>#{result2}</p>
947 <pre>
953 <pre>
948 [[CookBook documentation]]
954 [[CookBook documentation]]
949
955
950 #1
956 #1
951 </pre>
957 </pre>
952 EXPECTED
958 EXPECTED
953
959
954 @project = Project.find(1)
960 @project = Project.find(1)
955 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
961 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
956 end
962 end
957
963
958 def test_non_closing_pre_blocks_should_be_closed
964 def test_non_closing_pre_blocks_should_be_closed
959 raw = <<-RAW
965 raw = <<-RAW
960 <pre><code>
966 <pre><code>
961 RAW
967 RAW
962
968
963 expected = <<-EXPECTED
969 expected = <<-EXPECTED
964 <pre><code>
970 <pre><code>
965 </code></pre>
971 </code></pre>
966 EXPECTED
972 EXPECTED
967
973
968 @project = Project.find(1)
974 @project = Project.find(1)
969 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
975 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
970 end
976 end
971
977
972 def test_syntax_highlight
978 def test_syntax_highlight
973 raw = <<-RAW
979 raw = <<-RAW
974 <pre><code class="ruby">
980 <pre><code class="ruby">
975 # Some ruby code here
981 # Some ruby code here
976 </code></pre>
982 </code></pre>
977 RAW
983 RAW
978
984
979 expected = <<-EXPECTED
985 expected = <<-EXPECTED
980 <pre><code class="ruby syntaxhl"><span class=\"CodeRay\"><span class="comment"># Some ruby code here</span></span>
986 <pre><code class="ruby syntaxhl"><span class=\"CodeRay\"><span class="comment"># Some ruby code here</span></span>
981 </code></pre>
987 </code></pre>
982 EXPECTED
988 EXPECTED
983
989
984 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
990 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
985 end
991 end
986
992
987 def test_to_path_param
993 def test_to_path_param
988 assert_equal 'test1/test2', to_path_param('test1/test2')
994 assert_equal 'test1/test2', to_path_param('test1/test2')
989 assert_equal 'test1/test2', to_path_param('/test1/test2/')
995 assert_equal 'test1/test2', to_path_param('/test1/test2/')
990 assert_equal 'test1/test2', to_path_param('//test1/test2/')
996 assert_equal 'test1/test2', to_path_param('//test1/test2/')
991 assert_equal nil, to_path_param('/')
997 assert_equal nil, to_path_param('/')
992 end
998 end
993
999
994 def test_wiki_links_in_tables
1000 def test_wiki_links_in_tables
995 text = "|[[Page|Link title]]|[[Other Page|Other title]]|\n|Cell 21|[[Last page]]|"
1001 text = "|[[Page|Link title]]|[[Other Page|Other title]]|\n|Cell 21|[[Last page]]|"
996 link1 = link_to("Link title", "/projects/ecookbook/wiki/Page", :class => "wiki-page new")
1002 link1 = link_to("Link title", "/projects/ecookbook/wiki/Page", :class => "wiki-page new")
997 link2 = link_to("Other title", "/projects/ecookbook/wiki/Other_Page", :class => "wiki-page new")
1003 link2 = link_to("Other title", "/projects/ecookbook/wiki/Other_Page", :class => "wiki-page new")
998 link3 = link_to("Last page", "/projects/ecookbook/wiki/Last_page", :class => "wiki-page new")
1004 link3 = link_to("Last page", "/projects/ecookbook/wiki/Last_page", :class => "wiki-page new")
999 result = "<tr><td>#{link1}</td>" +
1005 result = "<tr><td>#{link1}</td>" +
1000 "<td>#{link2}</td>" +
1006 "<td>#{link2}</td>" +
1001 "</tr><tr><td>Cell 21</td><td>#{link3}</td></tr>"
1007 "</tr><tr><td>Cell 21</td><td>#{link3}</td></tr>"
1002 @project = Project.find(1)
1008 @project = Project.find(1)
1003 assert_equal "<table>#{result}</table>", textilizable(text).gsub(/[\t\n]/, '')
1009 assert_equal "<table>#{result}</table>", textilizable(text).gsub(/[\t\n]/, '')
1004 end
1010 end
1005
1011
1006 def test_text_formatting
1012 def test_text_formatting
1007 to_test = {'*_+bold, italic and underline+_*' => '<strong><em><ins>bold, italic and underline</ins></em></strong>',
1013 to_test = {'*_+bold, italic and underline+_*' => '<strong><em><ins>bold, italic and underline</ins></em></strong>',
1008 '(_text within parentheses_)' => '(<em>text within parentheses</em>)',
1014 '(_text within parentheses_)' => '(<em>text within parentheses</em>)',
1009 'a *Humane Web* Text Generator' => 'a <strong>Humane Web</strong> Text Generator',
1015 'a *Humane Web* Text Generator' => 'a <strong>Humane Web</strong> Text Generator',
1010 'a H *umane* W *eb* T *ext* G *enerator*' => 'a H <strong>umane</strong> W <strong>eb</strong> T <strong>ext</strong> G <strong>enerator</strong>',
1016 'a H *umane* W *eb* T *ext* G *enerator*' => 'a H <strong>umane</strong> W <strong>eb</strong> T <strong>ext</strong> G <strong>enerator</strong>',
1011 'a *H* umane *W* eb *T* ext *G* enerator' => 'a <strong>H</strong> umane <strong>W</strong> eb <strong>T</strong> ext <strong>G</strong> enerator',
1017 'a *H* umane *W* eb *T* ext *G* enerator' => 'a <strong>H</strong> umane <strong>W</strong> eb <strong>T</strong> ext <strong>G</strong> enerator',
1012 }
1018 }
1013 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
1019 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
1014 end
1020 end
1015
1021
1016 def test_wiki_horizontal_rule
1022 def test_wiki_horizontal_rule
1017 assert_equal '<hr />', textilizable('---')
1023 assert_equal '<hr />', textilizable('---')
1018 assert_equal '<p>Dashes: ---</p>', textilizable('Dashes: ---')
1024 assert_equal '<p>Dashes: ---</p>', textilizable('Dashes: ---')
1019 end
1025 end
1020
1026
1021 def test_footnotes
1027 def test_footnotes
1022 raw = <<-RAW
1028 raw = <<-RAW
1023 This is some text[1].
1029 This is some text[1].
1024
1030
1025 fn1. This is the foot note
1031 fn1. This is the foot note
1026 RAW
1032 RAW
1027
1033
1028 expected = <<-EXPECTED
1034 expected = <<-EXPECTED
1029 <p>This is some text<sup><a href=\"#fn1\">1</a></sup>.</p>
1035 <p>This is some text<sup><a href=\"#fn1\">1</a></sup>.</p>
1030 <p id="fn1" class="footnote"><sup>1</sup> This is the foot note</p>
1036 <p id="fn1" class="footnote"><sup>1</sup> This is the foot note</p>
1031 EXPECTED
1037 EXPECTED
1032
1038
1033 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
1039 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
1034 end
1040 end
1035
1041
1036 def test_headings
1042 def test_headings
1037 raw = 'h1. Some heading'
1043 raw = 'h1. Some heading'
1038 expected = %|<a name="Some-heading"></a>\n<h1 >Some heading<a href="#Some-heading" class="wiki-anchor">&para;</a></h1>|
1044 expected = %|<a name="Some-heading"></a>\n<h1 >Some heading<a href="#Some-heading" class="wiki-anchor">&para;</a></h1>|
1039
1045
1040 assert_equal expected, textilizable(raw)
1046 assert_equal expected, textilizable(raw)
1041 end
1047 end
1042
1048
1043 def test_headings_with_special_chars
1049 def test_headings_with_special_chars
1044 # This test makes sure that the generated anchor names match the expected
1050 # This test makes sure that the generated anchor names match the expected
1045 # ones even if the heading text contains unconventional characters
1051 # ones even if the heading text contains unconventional characters
1046 raw = 'h1. Some heading related to version 0.5'
1052 raw = 'h1. Some heading related to version 0.5'
1047 anchor = sanitize_anchor_name("Some-heading-related-to-version-0.5")
1053 anchor = sanitize_anchor_name("Some-heading-related-to-version-0.5")
1048 expected = %|<a name="#{anchor}"></a>\n<h1 >Some heading related to version 0.5<a href="##{anchor}" class="wiki-anchor">&para;</a></h1>|
1054 expected = %|<a name="#{anchor}"></a>\n<h1 >Some heading related to version 0.5<a href="##{anchor}" class="wiki-anchor">&para;</a></h1>|
1049
1055
1050 assert_equal expected, textilizable(raw)
1056 assert_equal expected, textilizable(raw)
1051 end
1057 end
1052
1058
1053 def test_headings_in_wiki_single_page_export_should_be_prepended_with_page_title
1059 def test_headings_in_wiki_single_page_export_should_be_prepended_with_page_title
1054 page = WikiPage.new( :title => 'Page Title', :wiki_id => 1 )
1060 page = WikiPage.new( :title => 'Page Title', :wiki_id => 1 )
1055 content = WikiContent.new( :text => 'h1. Some heading', :page => page )
1061 content = WikiContent.new( :text => 'h1. Some heading', :page => page )
1056
1062
1057 expected = %|<a name="Page_Title_Some-heading"></a>\n<h1 >Some heading<a href="#Page_Title_Some-heading" class="wiki-anchor">&para;</a></h1>|
1063 expected = %|<a name="Page_Title_Some-heading"></a>\n<h1 >Some heading<a href="#Page_Title_Some-heading" class="wiki-anchor">&para;</a></h1>|
1058
1064
1059 assert_equal expected, textilizable(content, :text, :wiki_links => :anchor )
1065 assert_equal expected, textilizable(content, :text, :wiki_links => :anchor )
1060 end
1066 end
1061
1067
1062 def test_table_of_content
1068 def test_table_of_content
1063 raw = <<-RAW
1069 raw = <<-RAW
1064 {{toc}}
1070 {{toc}}
1065
1071
1066 h1. Title
1072 h1. Title
1067
1073
1068 Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas sed libero.
1074 Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas sed libero.
1069
1075
1070 h2. Subtitle with a [[Wiki]] link
1076 h2. Subtitle with a [[Wiki]] link
1071
1077
1072 Nullam commodo metus accumsan nulla. Curabitur lobortis dui id dolor.
1078 Nullam commodo metus accumsan nulla. Curabitur lobortis dui id dolor.
1073
1079
1074 h2. Subtitle with [[Wiki|another Wiki]] link
1080 h2. Subtitle with [[Wiki|another Wiki]] link
1075
1081
1076 h2. Subtitle with %{color:red}red text%
1082 h2. Subtitle with %{color:red}red text%
1077
1083
1078 <pre>
1084 <pre>
1079 some code
1085 some code
1080 </pre>
1086 </pre>
1081
1087
1082 h3. Subtitle with *some* _modifiers_
1088 h3. Subtitle with *some* _modifiers_
1083
1089
1084 h3. Subtitle with @inline code@
1090 h3. Subtitle with @inline code@
1085
1091
1086 h1. Another title
1092 h1. Another title
1087
1093
1088 h3. An "Internet link":http://www.redmine.org/ inside subtitle
1094 h3. An "Internet link":http://www.redmine.org/ inside subtitle
1089
1095
1090 h2. "Project Name !/attachments/1234/logo_small.gif! !/attachments/5678/logo_2.png!":/projects/projectname/issues
1096 h2. "Project Name !/attachments/1234/logo_small.gif! !/attachments/5678/logo_2.png!":/projects/projectname/issues
1091
1097
1092 RAW
1098 RAW
1093
1099
1094 expected = '<ul class="toc">' +
1100 expected = '<ul class="toc">' +
1095 '<li><a href="#Title">Title</a>' +
1101 '<li><a href="#Title">Title</a>' +
1096 '<ul>' +
1102 '<ul>' +
1097 '<li><a href="#Subtitle-with-a-Wiki-link">Subtitle with a Wiki link</a></li>' +
1103 '<li><a href="#Subtitle-with-a-Wiki-link">Subtitle with a Wiki link</a></li>' +
1098 '<li><a href="#Subtitle-with-another-Wiki-link">Subtitle with another Wiki link</a></li>' +
1104 '<li><a href="#Subtitle-with-another-Wiki-link">Subtitle with another Wiki link</a></li>' +
1099 '<li><a href="#Subtitle-with-red-text">Subtitle with red text</a>' +
1105 '<li><a href="#Subtitle-with-red-text">Subtitle with red text</a>' +
1100 '<ul>' +
1106 '<ul>' +
1101 '<li><a href="#Subtitle-with-some-modifiers">Subtitle with some modifiers</a></li>' +
1107 '<li><a href="#Subtitle-with-some-modifiers">Subtitle with some modifiers</a></li>' +
1102 '<li><a href="#Subtitle-with-inline-code">Subtitle with inline code</a></li>' +
1108 '<li><a href="#Subtitle-with-inline-code">Subtitle with inline code</a></li>' +
1103 '</ul>' +
1109 '</ul>' +
1104 '</li>' +
1110 '</li>' +
1105 '</ul>' +
1111 '</ul>' +
1106 '</li>' +
1112 '</li>' +
1107 '<li><a href="#Another-title">Another title</a>' +
1113 '<li><a href="#Another-title">Another title</a>' +
1108 '<ul>' +
1114 '<ul>' +
1109 '<li>' +
1115 '<li>' +
1110 '<ul>' +
1116 '<ul>' +
1111 '<li><a href="#An-Internet-link-inside-subtitle">An Internet link inside subtitle</a></li>' +
1117 '<li><a href="#An-Internet-link-inside-subtitle">An Internet link inside subtitle</a></li>' +
1112 '</ul>' +
1118 '</ul>' +
1113 '</li>' +
1119 '</li>' +
1114 '<li><a href="#Project-Name">Project Name</a></li>' +
1120 '<li><a href="#Project-Name">Project Name</a></li>' +
1115 '</ul>' +
1121 '</ul>' +
1116 '</li>' +
1122 '</li>' +
1117 '</ul>'
1123 '</ul>'
1118
1124
1119 @project = Project.find(1)
1125 @project = Project.find(1)
1120 assert textilizable(raw).gsub("\n", "").include?(expected)
1126 assert textilizable(raw).gsub("\n", "").include?(expected)
1121 end
1127 end
1122
1128
1123 def test_table_of_content_should_generate_unique_anchors
1129 def test_table_of_content_should_generate_unique_anchors
1124 raw = <<-RAW
1130 raw = <<-RAW
1125 {{toc}}
1131 {{toc}}
1126
1132
1127 h1. Title
1133 h1. Title
1128
1134
1129 h2. Subtitle
1135 h2. Subtitle
1130
1136
1131 h2. Subtitle
1137 h2. Subtitle
1132 RAW
1138 RAW
1133
1139
1134 expected = '<ul class="toc">' +
1140 expected = '<ul class="toc">' +
1135 '<li><a href="#Title">Title</a>' +
1141 '<li><a href="#Title">Title</a>' +
1136 '<ul>' +
1142 '<ul>' +
1137 '<li><a href="#Subtitle">Subtitle</a></li>' +
1143 '<li><a href="#Subtitle">Subtitle</a></li>' +
1138 '<li><a href="#Subtitle-2">Subtitle</a></li>'
1144 '<li><a href="#Subtitle-2">Subtitle</a></li>'
1139 '</ul>'
1145 '</ul>'
1140 '</li>' +
1146 '</li>' +
1141 '</ul>'
1147 '</ul>'
1142
1148
1143 @project = Project.find(1)
1149 @project = Project.find(1)
1144 result = textilizable(raw).gsub("\n", "")
1150 result = textilizable(raw).gsub("\n", "")
1145 assert_include expected, result
1151 assert_include expected, result
1146 assert_include '<a name="Subtitle">', result
1152 assert_include '<a name="Subtitle">', result
1147 assert_include '<a name="Subtitle-2">', result
1153 assert_include '<a name="Subtitle-2">', result
1148 end
1154 end
1149
1155
1150 def test_table_of_content_should_contain_included_page_headings
1156 def test_table_of_content_should_contain_included_page_headings
1151 raw = <<-RAW
1157 raw = <<-RAW
1152 {{toc}}
1158 {{toc}}
1153
1159
1154 h1. Included
1160 h1. Included
1155
1161
1156 {{include(Child_1)}}
1162 {{include(Child_1)}}
1157 RAW
1163 RAW
1158
1164
1159 expected = '<ul class="toc">' +
1165 expected = '<ul class="toc">' +
1160 '<li><a href="#Included">Included</a></li>' +
1166 '<li><a href="#Included">Included</a></li>' +
1161 '<li><a href="#Child-page-1">Child page 1</a></li>' +
1167 '<li><a href="#Child-page-1">Child page 1</a></li>' +
1162 '</ul>'
1168 '</ul>'
1163
1169
1164 @project = Project.find(1)
1170 @project = Project.find(1)
1165 assert textilizable(raw).gsub("\n", "").include?(expected)
1171 assert textilizable(raw).gsub("\n", "").include?(expected)
1166 end
1172 end
1167
1173
1168 def test_toc_with_textile_formatting_should_be_parsed
1174 def test_toc_with_textile_formatting_should_be_parsed
1169 with_settings :text_formatting => 'textile' do
1175 with_settings :text_formatting => 'textile' do
1170 assert_select_in textilizable("{{toc}}\n\nh1. Heading"), 'ul.toc li', :text => 'Heading'
1176 assert_select_in textilizable("{{toc}}\n\nh1. Heading"), 'ul.toc li', :text => 'Heading'
1171 assert_select_in textilizable("{{<toc}}\n\nh1. Heading"), 'ul.toc.left li', :text => 'Heading'
1177 assert_select_in textilizable("{{<toc}}\n\nh1. Heading"), 'ul.toc.left li', :text => 'Heading'
1172 assert_select_in textilizable("{{>toc}}\n\nh1. Heading"), 'ul.toc.right li', :text => 'Heading'
1178 assert_select_in textilizable("{{>toc}}\n\nh1. Heading"), 'ul.toc.right li', :text => 'Heading'
1173 end
1179 end
1174 end
1180 end
1175
1181
1176 if Object.const_defined?(:Redcarpet)
1182 if Object.const_defined?(:Redcarpet)
1177 def test_toc_with_markdown_formatting_should_be_parsed
1183 def test_toc_with_markdown_formatting_should_be_parsed
1178 with_settings :text_formatting => 'markdown' do
1184 with_settings :text_formatting => 'markdown' do
1179 assert_select_in textilizable("{{toc}}\n\n# Heading"), 'ul.toc li', :text => 'Heading'
1185 assert_select_in textilizable("{{toc}}\n\n# Heading"), 'ul.toc li', :text => 'Heading'
1180 assert_select_in textilizable("{{<toc}}\n\n# Heading"), 'ul.toc.left li', :text => 'Heading'
1186 assert_select_in textilizable("{{<toc}}\n\n# Heading"), 'ul.toc.left li', :text => 'Heading'
1181 assert_select_in textilizable("{{>toc}}\n\n# Heading"), 'ul.toc.right li', :text => 'Heading'
1187 assert_select_in textilizable("{{>toc}}\n\n# Heading"), 'ul.toc.right li', :text => 'Heading'
1182 end
1188 end
1183 end
1189 end
1184 end
1190 end
1185
1191
1186 def test_section_edit_links
1192 def test_section_edit_links
1187 raw = <<-RAW
1193 raw = <<-RAW
1188 h1. Title
1194 h1. Title
1189
1195
1190 Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas sed libero.
1196 Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas sed libero.
1191
1197
1192 h2. Subtitle with a [[Wiki]] link
1198 h2. Subtitle with a [[Wiki]] link
1193
1199
1194 h2. Subtitle with *some* _modifiers_
1200 h2. Subtitle with *some* _modifiers_
1195
1201
1196 h2. Subtitle with @inline code@
1202 h2. Subtitle with @inline code@
1197
1203
1198 <pre>
1204 <pre>
1199 some code
1205 some code
1200
1206
1201 h2. heading inside pre
1207 h2. heading inside pre
1202
1208
1203 <h2>html heading inside pre</h2>
1209 <h2>html heading inside pre</h2>
1204 </pre>
1210 </pre>
1205
1211
1206 h2. Subtitle after pre tag
1212 h2. Subtitle after pre tag
1207 RAW
1213 RAW
1208
1214
1209 @project = Project.find(1)
1215 @project = Project.find(1)
1210 set_language_if_valid 'en'
1216 set_language_if_valid 'en'
1211 result = textilizable(raw, :edit_section_links => {:controller => 'wiki', :action => 'edit', :project_id => '1', :id => 'Test'}).gsub("\n", "")
1217 result = textilizable(raw, :edit_section_links => {:controller => 'wiki', :action => 'edit', :project_id => '1', :id => 'Test'}).gsub("\n", "")
1212
1218
1213 # heading that contains inline code
1219 # heading that contains inline code
1214 assert_match Regexp.new('<div class="contextual" id="section-4" title="Edit this section">' +
1220 assert_match Regexp.new('<div class="contextual" id="section-4" title="Edit this section">' +
1215 '<a href="/projects/1/wiki/Test/edit\?section=4"><img alt="Edit" src="/images/edit.png(\?\d+)?" /></a></div>' +
1221 '<a href="/projects/1/wiki/Test/edit\?section=4"><img alt="Edit" src="/images/edit.png(\?\d+)?" /></a></div>' +
1216 '<a name="Subtitle-with-inline-code"></a>' +
1222 '<a name="Subtitle-with-inline-code"></a>' +
1217 '<h2 >Subtitle with <code>inline code</code><a href="#Subtitle-with-inline-code" class="wiki-anchor">&para;</a></h2>'),
1223 '<h2 >Subtitle with <code>inline code</code><a href="#Subtitle-with-inline-code" class="wiki-anchor">&para;</a></h2>'),
1218 result
1224 result
1219
1225
1220 # last heading
1226 # last heading
1221 assert_match Regexp.new('<div class="contextual" id="section-5" title="Edit this section">' +
1227 assert_match Regexp.new('<div class="contextual" id="section-5" title="Edit this section">' +
1222 '<a href="/projects/1/wiki/Test/edit\?section=5"><img alt="Edit" src="/images/edit.png(\?\d+)?" /></a></div>' +
1228 '<a href="/projects/1/wiki/Test/edit\?section=5"><img alt="Edit" src="/images/edit.png(\?\d+)?" /></a></div>' +
1223 '<a name="Subtitle-after-pre-tag"></a>' +
1229 '<a name="Subtitle-after-pre-tag"></a>' +
1224 '<h2 >Subtitle after pre tag<a href="#Subtitle-after-pre-tag" class="wiki-anchor">&para;</a></h2>'),
1230 '<h2 >Subtitle after pre tag<a href="#Subtitle-after-pre-tag" class="wiki-anchor">&para;</a></h2>'),
1225 result
1231 result
1226 end
1232 end
1227
1233
1228 def test_default_formatter
1234 def test_default_formatter
1229 with_settings :text_formatting => 'unknown' do
1235 with_settings :text_formatting => 'unknown' do
1230 text = 'a *link*: http://www.example.net/'
1236 text = 'a *link*: http://www.example.net/'
1231 assert_equal '<p>a *link*: <a class="external" href="http://www.example.net/">http://www.example.net/</a></p>', textilizable(text)
1237 assert_equal '<p>a *link*: <a class="external" href="http://www.example.net/">http://www.example.net/</a></p>', textilizable(text)
1232 end
1238 end
1233 end
1239 end
1234
1240
1235 def test_due_date_distance_in_words
1241 def test_due_date_distance_in_words
1236 to_test = { Date.today => 'Due in 0 days',
1242 to_test = { Date.today => 'Due in 0 days',
1237 Date.today + 1 => 'Due in 1 day',
1243 Date.today + 1 => 'Due in 1 day',
1238 Date.today + 100 => 'Due in about 3 months',
1244 Date.today + 100 => 'Due in about 3 months',
1239 Date.today + 20000 => 'Due in over 54 years',
1245 Date.today + 20000 => 'Due in over 54 years',
1240 Date.today - 1 => '1 day late',
1246 Date.today - 1 => '1 day late',
1241 Date.today - 100 => 'about 3 months late',
1247 Date.today - 100 => 'about 3 months late',
1242 Date.today - 20000 => 'over 54 years late',
1248 Date.today - 20000 => 'over 54 years late',
1243 }
1249 }
1244 ::I18n.locale = :en
1250 ::I18n.locale = :en
1245 to_test.each do |date, expected|
1251 to_test.each do |date, expected|
1246 assert_equal expected, due_date_distance_in_words(date)
1252 assert_equal expected, due_date_distance_in_words(date)
1247 end
1253 end
1248 end
1254 end
1249
1255
1250 def test_avatar_enabled
1256 def test_avatar_enabled
1251 with_settings :gravatar_enabled => '1' do
1257 with_settings :gravatar_enabled => '1' do
1252 assert avatar(User.find_by_mail('jsmith@somenet.foo')).include?(Digest::MD5.hexdigest('jsmith@somenet.foo'))
1258 assert avatar(User.find_by_mail('jsmith@somenet.foo')).include?(Digest::MD5.hexdigest('jsmith@somenet.foo'))
1253 assert avatar('jsmith <jsmith@somenet.foo>').include?(Digest::MD5.hexdigest('jsmith@somenet.foo'))
1259 assert avatar('jsmith <jsmith@somenet.foo>').include?(Digest::MD5.hexdigest('jsmith@somenet.foo'))
1254 # Default size is 50
1260 # Default size is 50
1255 assert avatar('jsmith <jsmith@somenet.foo>').include?('size=50')
1261 assert avatar('jsmith <jsmith@somenet.foo>').include?('size=50')
1256 assert avatar('jsmith <jsmith@somenet.foo>', :size => 24).include?('size=24')
1262 assert avatar('jsmith <jsmith@somenet.foo>', :size => 24).include?('size=24')
1257 # Non-avatar options should be considered html options
1263 # Non-avatar options should be considered html options
1258 assert avatar('jsmith <jsmith@somenet.foo>', :title => 'John Smith').include?('title="John Smith"')
1264 assert avatar('jsmith <jsmith@somenet.foo>', :title => 'John Smith').include?('title="John Smith"')
1259 # The default class of the img tag should be gravatar
1265 # The default class of the img tag should be gravatar
1260 assert avatar('jsmith <jsmith@somenet.foo>').include?('class="gravatar"')
1266 assert avatar('jsmith <jsmith@somenet.foo>').include?('class="gravatar"')
1261 assert !avatar('jsmith <jsmith@somenet.foo>', :class => 'picture').include?('class="gravatar"')
1267 assert !avatar('jsmith <jsmith@somenet.foo>', :class => 'picture').include?('class="gravatar"')
1262 assert_nil avatar('jsmith')
1268 assert_nil avatar('jsmith')
1263 assert_nil avatar(nil)
1269 assert_nil avatar(nil)
1264 end
1270 end
1265 end
1271 end
1266
1272
1267 def test_avatar_disabled
1273 def test_avatar_disabled
1268 with_settings :gravatar_enabled => '0' do
1274 with_settings :gravatar_enabled => '0' do
1269 assert_equal '', avatar(User.find_by_mail('jsmith@somenet.foo'))
1275 assert_equal '', avatar(User.find_by_mail('jsmith@somenet.foo'))
1270 end
1276 end
1271 end
1277 end
1272
1278
1273 def test_link_to_user
1279 def test_link_to_user
1274 user = User.find(2)
1280 user = User.find(2)
1275 result = link_to("John Smith", "/users/2", :class => "user active")
1281 result = link_to("John Smith", "/users/2", :class => "user active")
1276 assert_equal result, link_to_user(user)
1282 assert_equal result, link_to_user(user)
1277 end
1283 end
1278
1284
1279 def test_link_to_user_should_not_link_to_locked_user
1285 def test_link_to_user_should_not_link_to_locked_user
1280 with_current_user nil do
1286 with_current_user nil do
1281 user = User.find(5)
1287 user = User.find(5)
1282 assert user.locked?
1288 assert user.locked?
1283 assert_equal 'Dave2 Lopper2', link_to_user(user)
1289 assert_equal 'Dave2 Lopper2', link_to_user(user)
1284 end
1290 end
1285 end
1291 end
1286
1292
1287 def test_link_to_user_should_link_to_locked_user_if_current_user_is_admin
1293 def test_link_to_user_should_link_to_locked_user_if_current_user_is_admin
1288 with_current_user User.find(1) do
1294 with_current_user User.find(1) do
1289 user = User.find(5)
1295 user = User.find(5)
1290 assert user.locked?
1296 assert user.locked?
1291 result = link_to("Dave2 Lopper2", "/users/5", :class => "user locked")
1297 result = link_to("Dave2 Lopper2", "/users/5", :class => "user locked")
1292 assert_equal result, link_to_user(user)
1298 assert_equal result, link_to_user(user)
1293 end
1299 end
1294 end
1300 end
1295
1301
1296 def test_link_to_user_should_not_link_to_anonymous
1302 def test_link_to_user_should_not_link_to_anonymous
1297 user = User.anonymous
1303 user = User.anonymous
1298 assert user.anonymous?
1304 assert user.anonymous?
1299 t = link_to_user(user)
1305 t = link_to_user(user)
1300 assert_equal ::I18n.t(:label_user_anonymous), t
1306 assert_equal ::I18n.t(:label_user_anonymous), t
1301 end
1307 end
1302
1308
1303 def test_link_to_attachment
1309 def test_link_to_attachment
1304 a = Attachment.find(3)
1310 a = Attachment.find(3)
1305 assert_equal '<a href="/attachments/3/logo.gif">logo.gif</a>',
1311 assert_equal '<a href="/attachments/3/logo.gif">logo.gif</a>',
1306 link_to_attachment(a)
1312 link_to_attachment(a)
1307 assert_equal '<a href="/attachments/3/logo.gif">Text</a>',
1313 assert_equal '<a href="/attachments/3/logo.gif">Text</a>',
1308 link_to_attachment(a, :text => 'Text')
1314 link_to_attachment(a, :text => 'Text')
1309 result = link_to("logo.gif", "/attachments/3/logo.gif", :class => "foo")
1315 result = link_to("logo.gif", "/attachments/3/logo.gif", :class => "foo")
1310 assert_equal result,
1316 assert_equal result,
1311 link_to_attachment(a, :class => 'foo')
1317 link_to_attachment(a, :class => 'foo')
1312 assert_equal '<a href="/attachments/download/3/logo.gif">logo.gif</a>',
1318 assert_equal '<a href="/attachments/download/3/logo.gif">logo.gif</a>',
1313 link_to_attachment(a, :download => true)
1319 link_to_attachment(a, :download => true)
1314 assert_equal '<a href="http://test.host/attachments/3/logo.gif">logo.gif</a>',
1320 assert_equal '<a href="http://test.host/attachments/3/logo.gif">logo.gif</a>',
1315 link_to_attachment(a, :only_path => false)
1321 link_to_attachment(a, :only_path => false)
1316 end
1322 end
1317
1323
1318 def test_thumbnail_tag
1324 def test_thumbnail_tag
1319 a = Attachment.find(3)
1325 a = Attachment.find(3)
1320 assert_equal '<a href="/attachments/3/logo.gif" title="logo.gif"><img alt="3" src="/attachments/thumbnail/3" /></a>',
1326 assert_equal '<a href="/attachments/3/logo.gif" title="logo.gif"><img alt="3" src="/attachments/thumbnail/3" /></a>',
1321 thumbnail_tag(a)
1327 thumbnail_tag(a)
1322 end
1328 end
1323
1329
1324 def test_link_to_project
1330 def test_link_to_project
1325 project = Project.find(1)
1331 project = Project.find(1)
1326 assert_equal %(<a href="/projects/ecookbook">eCookbook</a>),
1332 assert_equal %(<a href="/projects/ecookbook">eCookbook</a>),
1327 link_to_project(project)
1333 link_to_project(project)
1328 assert_equal %(<a href="http://test.host/projects/ecookbook?jump=blah">eCookbook</a>),
1334 assert_equal %(<a href="http://test.host/projects/ecookbook?jump=blah">eCookbook</a>),
1329 link_to_project(project, {:only_path => false, :jump => 'blah'})
1335 link_to_project(project, {:only_path => false, :jump => 'blah'})
1330 end
1336 end
1331
1337
1332 def test_link_to_project_settings
1338 def test_link_to_project_settings
1333 project = Project.find(1)
1339 project = Project.find(1)
1334 assert_equal '<a href="/projects/ecookbook/settings">eCookbook</a>', link_to_project_settings(project)
1340 assert_equal '<a href="/projects/ecookbook/settings">eCookbook</a>', link_to_project_settings(project)
1335
1341
1336 project.status = Project::STATUS_CLOSED
1342 project.status = Project::STATUS_CLOSED
1337 assert_equal '<a href="/projects/ecookbook">eCookbook</a>', link_to_project_settings(project)
1343 assert_equal '<a href="/projects/ecookbook">eCookbook</a>', link_to_project_settings(project)
1338
1344
1339 project.status = Project::STATUS_ARCHIVED
1345 project.status = Project::STATUS_ARCHIVED
1340 assert_equal 'eCookbook', link_to_project_settings(project)
1346 assert_equal 'eCookbook', link_to_project_settings(project)
1341 end
1347 end
1342
1348
1343 def test_link_to_legacy_project_with_numerical_identifier_should_use_id
1349 def test_link_to_legacy_project_with_numerical_identifier_should_use_id
1344 # numeric identifier are no longer allowed
1350 # numeric identifier are no longer allowed
1345 Project.where(:id => 1).update_all(:identifier => 25)
1351 Project.where(:id => 1).update_all(:identifier => 25)
1346 assert_equal '<a href="/projects/1">eCookbook</a>',
1352 assert_equal '<a href="/projects/1">eCookbook</a>',
1347 link_to_project(Project.find(1))
1353 link_to_project(Project.find(1))
1348 end
1354 end
1349
1355
1350 def test_principals_options_for_select_with_users
1356 def test_principals_options_for_select_with_users
1351 User.current = nil
1357 User.current = nil
1352 users = [User.find(2), User.find(4)]
1358 users = [User.find(2), User.find(4)]
1353 assert_equal %(<option value="2">John Smith</option><option value="4">Robert Hill</option>),
1359 assert_equal %(<option value="2">John Smith</option><option value="4">Robert Hill</option>),
1354 principals_options_for_select(users)
1360 principals_options_for_select(users)
1355 end
1361 end
1356
1362
1357 def test_principals_options_for_select_with_selected
1363 def test_principals_options_for_select_with_selected
1358 User.current = nil
1364 User.current = nil
1359 users = [User.find(2), User.find(4)]
1365 users = [User.find(2), User.find(4)]
1360 assert_equal %(<option value="2">John Smith</option><option value="4" selected="selected">Robert Hill</option>),
1366 assert_equal %(<option value="2">John Smith</option><option value="4" selected="selected">Robert Hill</option>),
1361 principals_options_for_select(users, User.find(4))
1367 principals_options_for_select(users, User.find(4))
1362 end
1368 end
1363
1369
1364 def test_principals_options_for_select_with_users_and_groups
1370 def test_principals_options_for_select_with_users_and_groups
1365 User.current = nil
1371 User.current = nil
1366 users = [User.find(2), Group.find(11), User.find(4), Group.find(10)]
1372 users = [User.find(2), Group.find(11), User.find(4), Group.find(10)]
1367 assert_equal %(<option value="2">John Smith</option><option value="4">Robert Hill</option>) +
1373 assert_equal %(<option value="2">John Smith</option><option value="4">Robert Hill</option>) +
1368 %(<optgroup label="Groups"><option value="10">A Team</option><option value="11">B Team</option></optgroup>),
1374 %(<optgroup label="Groups"><option value="10">A Team</option><option value="11">B Team</option></optgroup>),
1369 principals_options_for_select(users)
1375 principals_options_for_select(users)
1370 end
1376 end
1371
1377
1372 def test_principals_options_for_select_with_empty_collection
1378 def test_principals_options_for_select_with_empty_collection
1373 assert_equal '', principals_options_for_select([])
1379 assert_equal '', principals_options_for_select([])
1374 end
1380 end
1375
1381
1376 def test_principals_options_for_select_should_include_me_option_when_current_user_is_in_collection
1382 def test_principals_options_for_select_should_include_me_option_when_current_user_is_in_collection
1377 users = [User.find(2), User.find(4)]
1383 users = [User.find(2), User.find(4)]
1378 User.current = User.find(4)
1384 User.current = User.find(4)
1379 assert_include '<option value="4">&lt;&lt; me &gt;&gt;</option>', principals_options_for_select(users)
1385 assert_include '<option value="4">&lt;&lt; me &gt;&gt;</option>', principals_options_for_select(users)
1380 end
1386 end
1381
1387
1382 def test_stylesheet_link_tag_should_pick_the_default_stylesheet
1388 def test_stylesheet_link_tag_should_pick_the_default_stylesheet
1383 assert_match 'href="/stylesheets/styles.css"', stylesheet_link_tag("styles")
1389 assert_match 'href="/stylesheets/styles.css"', stylesheet_link_tag("styles")
1384 end
1390 end
1385
1391
1386 def test_stylesheet_link_tag_for_plugin_should_pick_the_plugin_stylesheet
1392 def test_stylesheet_link_tag_for_plugin_should_pick_the_plugin_stylesheet
1387 assert_match 'href="/plugin_assets/foo/stylesheets/styles.css"', stylesheet_link_tag("styles", :plugin => :foo)
1393 assert_match 'href="/plugin_assets/foo/stylesheets/styles.css"', stylesheet_link_tag("styles", :plugin => :foo)
1388 end
1394 end
1389
1395
1390 def test_image_tag_should_pick_the_default_image
1396 def test_image_tag_should_pick_the_default_image
1391 assert_match 'src="/images/image.png"', image_tag("image.png")
1397 assert_match 'src="/images/image.png"', image_tag("image.png")
1392 end
1398 end
1393
1399
1394 def test_image_tag_should_pick_the_theme_image_if_it_exists
1400 def test_image_tag_should_pick_the_theme_image_if_it_exists
1395 theme = Redmine::Themes.themes.last
1401 theme = Redmine::Themes.themes.last
1396 theme.images << 'image.png'
1402 theme.images << 'image.png'
1397
1403
1398 with_settings :ui_theme => theme.id do
1404 with_settings :ui_theme => theme.id do
1399 assert_match %|src="/themes/#{theme.dir}/images/image.png"|, image_tag("image.png")
1405 assert_match %|src="/themes/#{theme.dir}/images/image.png"|, image_tag("image.png")
1400 assert_match %|src="/images/other.png"|, image_tag("other.png")
1406 assert_match %|src="/images/other.png"|, image_tag("other.png")
1401 end
1407 end
1402 ensure
1408 ensure
1403 theme.images.delete 'image.png'
1409 theme.images.delete 'image.png'
1404 end
1410 end
1405
1411
1406 def test_image_tag_sfor_plugin_should_pick_the_plugin_image
1412 def test_image_tag_sfor_plugin_should_pick_the_plugin_image
1407 assert_match 'src="/plugin_assets/foo/images/image.png"', image_tag("image.png", :plugin => :foo)
1413 assert_match 'src="/plugin_assets/foo/images/image.png"', image_tag("image.png", :plugin => :foo)
1408 end
1414 end
1409
1415
1410 def test_javascript_include_tag_should_pick_the_default_javascript
1416 def test_javascript_include_tag_should_pick_the_default_javascript
1411 assert_match 'src="/javascripts/scripts.js"', javascript_include_tag("scripts")
1417 assert_match 'src="/javascripts/scripts.js"', javascript_include_tag("scripts")
1412 end
1418 end
1413
1419
1414 def test_javascript_include_tag_for_plugin_should_pick_the_plugin_javascript
1420 def test_javascript_include_tag_for_plugin_should_pick_the_plugin_javascript
1415 assert_match 'src="/plugin_assets/foo/javascripts/scripts.js"', javascript_include_tag("scripts", :plugin => :foo)
1421 assert_match 'src="/plugin_assets/foo/javascripts/scripts.js"', javascript_include_tag("scripts", :plugin => :foo)
1416 end
1422 end
1417
1423
1418 def test_raw_json_should_escape_closing_tags
1424 def test_raw_json_should_escape_closing_tags
1419 s = raw_json(["<foo>bar</foo>"])
1425 s = raw_json(["<foo>bar</foo>"])
1420 assert_include '\/foo', s
1426 assert_include '\/foo', s
1421 end
1427 end
1422
1428
1423 def test_raw_json_should_be_html_safe
1429 def test_raw_json_should_be_html_safe
1424 s = raw_json(["foo"])
1430 s = raw_json(["foo"])
1425 assert s.html_safe?
1431 assert s.html_safe?
1426 end
1432 end
1427
1433
1428 def test_html_title_should_app_title_if_not_set
1434 def test_html_title_should_app_title_if_not_set
1429 assert_equal 'Redmine', html_title
1435 assert_equal 'Redmine', html_title
1430 end
1436 end
1431
1437
1432 def test_html_title_should_join_items
1438 def test_html_title_should_join_items
1433 html_title 'Foo', 'Bar'
1439 html_title 'Foo', 'Bar'
1434 assert_equal 'Foo - Bar - Redmine', html_title
1440 assert_equal 'Foo - Bar - Redmine', html_title
1435 end
1441 end
1436
1442
1437 def test_html_title_should_append_current_project_name
1443 def test_html_title_should_append_current_project_name
1438 @project = Project.find(1)
1444 @project = Project.find(1)
1439 html_title 'Foo', 'Bar'
1445 html_title 'Foo', 'Bar'
1440 assert_equal 'Foo - Bar - eCookbook - Redmine', html_title
1446 assert_equal 'Foo - Bar - eCookbook - Redmine', html_title
1441 end
1447 end
1442
1448
1443 def test_title_should_return_a_h2_tag
1449 def test_title_should_return_a_h2_tag
1444 assert_equal '<h2>Foo</h2>', title('Foo')
1450 assert_equal '<h2>Foo</h2>', title('Foo')
1445 end
1451 end
1446
1452
1447 def test_title_should_set_html_title
1453 def test_title_should_set_html_title
1448 title('Foo')
1454 title('Foo')
1449 assert_equal 'Foo - Redmine', html_title
1455 assert_equal 'Foo - Redmine', html_title
1450 end
1456 end
1451
1457
1452 def test_title_should_turn_arrays_into_links
1458 def test_title_should_turn_arrays_into_links
1453 assert_equal '<h2><a href="/foo">Foo</a></h2>', title(['Foo', '/foo'])
1459 assert_equal '<h2><a href="/foo">Foo</a></h2>', title(['Foo', '/foo'])
1454 assert_equal 'Foo - Redmine', html_title
1460 assert_equal 'Foo - Redmine', html_title
1455 end
1461 end
1456
1462
1457 def test_title_should_join_items
1463 def test_title_should_join_items
1458 assert_equal '<h2>Foo &#187; Bar</h2>', title('Foo', 'Bar')
1464 assert_equal '<h2>Foo &#187; Bar</h2>', title('Foo', 'Bar')
1459 assert_equal 'Bar - Foo - Redmine', html_title
1465 assert_equal 'Bar - Foo - Redmine', html_title
1460 end
1466 end
1461
1467
1462 def test_favicon_path
1468 def test_favicon_path
1463 assert_match %r{^/favicon\.ico}, favicon_path
1469 assert_match %r{^/favicon\.ico}, favicon_path
1464 end
1470 end
1465
1471
1466 def test_favicon_path_with_suburi
1472 def test_favicon_path_with_suburi
1467 Redmine::Utils.relative_url_root = '/foo'
1473 Redmine::Utils.relative_url_root = '/foo'
1468 assert_match %r{^/foo/favicon\.ico}, favicon_path
1474 assert_match %r{^/foo/favicon\.ico}, favicon_path
1469 ensure
1475 ensure
1470 Redmine::Utils.relative_url_root = ''
1476 Redmine::Utils.relative_url_root = ''
1471 end
1477 end
1472
1478
1473 def test_favicon_url
1479 def test_favicon_url
1474 assert_match %r{^http://test\.host/favicon\.ico}, favicon_url
1480 assert_match %r{^http://test\.host/favicon\.ico}, favicon_url
1475 end
1481 end
1476
1482
1477 def test_favicon_url_with_suburi
1483 def test_favicon_url_with_suburi
1478 Redmine::Utils.relative_url_root = '/foo'
1484 Redmine::Utils.relative_url_root = '/foo'
1479 assert_match %r{^http://test\.host/foo/favicon\.ico}, favicon_url
1485 assert_match %r{^http://test\.host/foo/favicon\.ico}, favicon_url
1480 ensure
1486 ensure
1481 Redmine::Utils.relative_url_root = ''
1487 Redmine::Utils.relative_url_root = ''
1482 end
1488 end
1483
1489
1484 def test_truncate_single_line
1490 def test_truncate_single_line
1485 str = "01234"
1491 str = "01234"
1486 result = truncate_single_line_raw("#{str}\n#{str}", 10)
1492 result = truncate_single_line_raw("#{str}\n#{str}", 10)
1487 assert_equal "01234 0...", result
1493 assert_equal "01234 0...", result
1488 assert !result.html_safe?
1494 assert !result.html_safe?
1489 result = truncate_single_line_raw("#{str}<&#>\n#{str}#{str}", 16)
1495 result = truncate_single_line_raw("#{str}<&#>\n#{str}#{str}", 16)
1490 assert_equal "01234<&#> 012...", result
1496 assert_equal "01234<&#> 012...", result
1491 assert !result.html_safe?
1497 assert !result.html_safe?
1492 end
1498 end
1493
1499
1494 def test_truncate_single_line_non_ascii
1500 def test_truncate_single_line_non_ascii
1495 ja = "\xe6\x97\xa5\xe6\x9c\xac\xe8\xaa\x9e".force_encoding('UTF-8')
1501 ja = "\xe6\x97\xa5\xe6\x9c\xac\xe8\xaa\x9e".force_encoding('UTF-8')
1496 result = truncate_single_line_raw("#{ja}\n#{ja}\n#{ja}", 10)
1502 result = truncate_single_line_raw("#{ja}\n#{ja}\n#{ja}", 10)
1497 assert_equal "#{ja} #{ja}...", result
1503 assert_equal "#{ja} #{ja}...", result
1498 assert !result.html_safe?
1504 assert !result.html_safe?
1499 end
1505 end
1500 end
1506 end
General Comments 0
You need to be logged in to leave comments. Login now