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