##// END OF EJS Templates
Fixed: {{toc}} uses identical anchors for subsections with the same name (#8194)....
Jean-Philippe Lang -
r8751:4ccb8983606f
parent child
Show More
@@ -1,1099 +1,1105
1 # encoding: utf-8
1 # encoding: utf-8
2 #
2 #
3 # Redmine - project management software
3 # Redmine - project management software
4 # Copyright (C) 2006-2011 Jean-Philippe Lang
4 # Copyright (C) 2006-2011 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
27
28 extend Forwardable
28 extend Forwardable
29 def_delegators :wiki_helper, :wikitoolbar_for, :heads_for_wiki_formatter
29 def_delegators :wiki_helper, :wikitoolbar_for, :heads_for_wiki_formatter
30
30
31 # Return true if user is authorized for controller/action, otherwise false
31 # Return true if user is authorized for controller/action, otherwise false
32 def authorize_for(controller, action)
32 def authorize_for(controller, action)
33 User.current.allowed_to?({:controller => controller, :action => action}, @project)
33 User.current.allowed_to?({:controller => controller, :action => action}, @project)
34 end
34 end
35
35
36 # Display a link if user is authorized
36 # Display a link if user is authorized
37 #
37 #
38 # @param [String] name Anchor text (passed to link_to)
38 # @param [String] name Anchor text (passed to link_to)
39 # @param [Hash] options Hash params. This will checked by authorize_for to see if the user is authorized
39 # @param [Hash] options Hash params. This will checked by authorize_for to see if the user is authorized
40 # @param [optional, Hash] html_options Options passed to link_to
40 # @param [optional, Hash] html_options Options passed to link_to
41 # @param [optional, Hash] parameters_for_method_reference Extra parameters for link_to
41 # @param [optional, Hash] parameters_for_method_reference Extra parameters for link_to
42 def link_to_if_authorized(name, options = {}, html_options = nil, *parameters_for_method_reference)
42 def link_to_if_authorized(name, options = {}, html_options = nil, *parameters_for_method_reference)
43 link_to(name, options, html_options, *parameters_for_method_reference) if authorize_for(options[:controller] || params[:controller], options[:action])
43 link_to(name, options, html_options, *parameters_for_method_reference) if authorize_for(options[:controller] || params[:controller], options[:action])
44 end
44 end
45
45
46 # Display a link to remote if user is authorized
46 # Display a link to remote if user is authorized
47 def link_to_remote_if_authorized(name, options = {}, html_options = nil)
47 def link_to_remote_if_authorized(name, options = {}, html_options = nil)
48 url = options[:url] || {}
48 url = options[:url] || {}
49 link_to_remote(name, options, html_options) if authorize_for(url[:controller] || params[:controller], url[:action])
49 link_to_remote(name, options, html_options) if authorize_for(url[:controller] || params[:controller], url[:action])
50 end
50 end
51
51
52 # Displays a link to user's account page if active
52 # Displays a link to user's account page if active
53 def link_to_user(user, options={})
53 def link_to_user(user, options={})
54 if user.is_a?(User)
54 if user.is_a?(User)
55 name = h(user.name(options[:format]))
55 name = h(user.name(options[:format]))
56 if user.active?
56 if user.active?
57 link_to name, :controller => 'users', :action => 'show', :id => user
57 link_to name, :controller => 'users', :action => 'show', :id => user
58 else
58 else
59 name
59 name
60 end
60 end
61 else
61 else
62 h(user.to_s)
62 h(user.to_s)
63 end
63 end
64 end
64 end
65
65
66 # Displays a link to +issue+ with its subject.
66 # Displays a link to +issue+ with its subject.
67 # Examples:
67 # Examples:
68 #
68 #
69 # link_to_issue(issue) # => Defect #6: This is the subject
69 # link_to_issue(issue) # => Defect #6: This is the subject
70 # link_to_issue(issue, :truncate => 6) # => Defect #6: This i...
70 # link_to_issue(issue, :truncate => 6) # => Defect #6: This i...
71 # link_to_issue(issue, :subject => false) # => Defect #6
71 # link_to_issue(issue, :subject => false) # => Defect #6
72 # link_to_issue(issue, :project => true) # => Foo - Defect #6
72 # link_to_issue(issue, :project => true) # => Foo - Defect #6
73 #
73 #
74 def link_to_issue(issue, options={})
74 def link_to_issue(issue, options={})
75 title = nil
75 title = nil
76 subject = nil
76 subject = nil
77 if options[:subject] == false
77 if options[:subject] == false
78 title = truncate(issue.subject, :length => 60)
78 title = truncate(issue.subject, :length => 60)
79 else
79 else
80 subject = issue.subject
80 subject = issue.subject
81 if options[:truncate]
81 if options[:truncate]
82 subject = truncate(subject, :length => options[:truncate])
82 subject = truncate(subject, :length => options[:truncate])
83 end
83 end
84 end
84 end
85 s = link_to "#{h(issue.tracker)} ##{issue.id}", {:controller => "issues", :action => "show", :id => issue},
85 s = link_to "#{h(issue.tracker)} ##{issue.id}", {:controller => "issues", :action => "show", :id => issue},
86 :class => issue.css_classes,
86 :class => issue.css_classes,
87 :title => title
87 :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 action = options.delete(:download) ? 'download' : 'show'
99 action = options.delete(:download) ? 'download' : 'show'
100 link_to(h(text),
100 link_to(h(text),
101 {:controller => 'attachments', :action => action,
101 {:controller => 'attachments', :action => action,
102 :id => attachment, :filename => attachment.filename },
102 :id => attachment, :filename => attachment.filename },
103 options)
103 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 )
119 )
120 end
120 end
121
121
122 # Generates a link to a message
122 # Generates a link to a message
123 def link_to_message(message, options={}, html_options = nil)
123 def link_to_message(message, options={}, html_options = nil)
124 link_to(
124 link_to(
125 h(truncate(message.subject, :length => 60)),
125 h(truncate(message.subject, :length => 60)),
126 { :controller => 'messages', :action => 'show',
126 { :controller => 'messages', :action => 'show',
127 :board_id => message.board_id,
127 :board_id => message.board_id,
128 :id => message.root,
128 :id => message.root,
129 :r => (message.parent_id && message.id),
129 :r => (message.parent_id && message.id),
130 :anchor => (message.parent_id ? "message-#{message.id}" : nil)
130 :anchor => (message.parent_id ? "message-#{message.id}" : nil)
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, :action=>'settings') # => link to project settings
140 # link_to_project(project, :action=>'settings') # => link to project settings
141 # link_to_project(project, {:only_path => false}, :class => "project") # => 3rd arg adds html options
141 # link_to_project(project, {:only_path => false}, :class => "project") # => 3rd arg adds html options
142 # link_to_project(project, {}, :class => "project") # => html options with default url (project overview)
142 # link_to_project(project, {}, :class => "project") # => html options with default url (project overview)
143 #
143 #
144 def link_to_project(project, options={}, html_options = nil)
144 def link_to_project(project, options={}, html_options = nil)
145 if project.active?
145 if project.active?
146 url = {:controller => 'projects', :action => 'show', :id => project}.merge(options)
146 url = {:controller => 'projects', :action => 'show', :id => project}.merge(options)
147 link_to(h(project), url, html_options)
147 link_to(h(project), url, html_options)
148 else
148 else
149 h(project)
149 h(project)
150 end
150 end
151 end
151 end
152
152
153 def toggle_link(name, id, options={})
153 def toggle_link(name, id, options={})
154 onclick = "Element.toggle('#{id}'); "
154 onclick = "Element.toggle('#{id}'); "
155 onclick << (options[:focus] ? "Form.Element.focus('#{options[:focus]}'); " : "this.blur(); ")
155 onclick << (options[:focus] ? "Form.Element.focus('#{options[:focus]}'); " : "this.blur(); ")
156 onclick << "return false;"
156 onclick << "return false;"
157 link_to(name, "#", :onclick => onclick)
157 link_to(name, "#", :onclick => onclick)
158 end
158 end
159
159
160 def image_to_function(name, function, html_options = {})
160 def image_to_function(name, function, html_options = {})
161 html_options.symbolize_keys!
161 html_options.symbolize_keys!
162 tag(:input, html_options.merge({
162 tag(:input, html_options.merge({
163 :type => "image", :src => image_path(name),
163 :type => "image", :src => image_path(name),
164 :onclick => (html_options[:onclick] ? "#{html_options[:onclick]}; " : "") + "#{function};"
164 :onclick => (html_options[:onclick] ? "#{html_options[:onclick]}; " : "") + "#{function};"
165 }))
165 }))
166 end
166 end
167
167
168 def prompt_to_remote(name, text, param, url, html_options = {})
168 def prompt_to_remote(name, text, param, url, html_options = {})
169 html_options[:onclick] = "promptToRemote('#{text}', '#{param}', '#{url_for(url)}'); return false;"
169 html_options[:onclick] = "promptToRemote('#{text}', '#{param}', '#{url_for(url)}'); return false;"
170 link_to name, {}, html_options
170 link_to name, {}, html_options
171 end
171 end
172
172
173 def format_activity_title(text)
173 def format_activity_title(text)
174 h(truncate_single_line(text, :length => 100))
174 h(truncate_single_line(text, :length => 100))
175 end
175 end
176
176
177 def format_activity_day(date)
177 def format_activity_day(date)
178 date == Date.today ? l(:label_today).titleize : format_date(date)
178 date == Date.today ? l(:label_today).titleize : format_date(date)
179 end
179 end
180
180
181 def format_activity_description(text)
181 def format_activity_description(text)
182 h(truncate(text.to_s, :length => 120).gsub(%r{[\r\n]*<(pre|code)>.*$}m, '...')
182 h(truncate(text.to_s, :length => 120).gsub(%r{[\r\n]*<(pre|code)>.*$}m, '...')
183 ).gsub(/[\r\n]+/, "<br />").html_safe
183 ).gsub(/[\r\n]+/, "<br />").html_safe
184 end
184 end
185
185
186 def format_version_name(version)
186 def format_version_name(version)
187 if version.project == @project
187 if version.project == @project
188 h(version)
188 h(version)
189 else
189 else
190 h("#{version.project} - #{version}")
190 h("#{version.project} - #{version}")
191 end
191 end
192 end
192 end
193
193
194 def due_date_distance_in_words(date)
194 def due_date_distance_in_words(date)
195 if date
195 if date
196 l((date < Date.today ? :label_roadmap_overdue : :label_roadmap_due_in), distance_of_date_in_words(Date.today, date))
196 l((date < Date.today ? :label_roadmap_overdue : :label_roadmap_due_in), distance_of_date_in_words(Date.today, date))
197 end
197 end
198 end
198 end
199
199
200 def render_page_hierarchy(pages, node=nil, options={})
200 def render_page_hierarchy(pages, node=nil, options={})
201 content = ''
201 content = ''
202 if pages[node]
202 if pages[node]
203 content << "<ul class=\"pages-hierarchy\">\n"
203 content << "<ul class=\"pages-hierarchy\">\n"
204 pages[node].each do |page|
204 pages[node].each do |page|
205 content << "<li>"
205 content << "<li>"
206 content << link_to(h(page.pretty_title), {:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title},
206 content << link_to(h(page.pretty_title), {:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title},
207 :title => (options[:timestamp] && page.updated_on ? l(:label_updated_time, distance_of_time_in_words(Time.now, page.updated_on)) : nil))
207 :title => (options[:timestamp] && page.updated_on ? l(:label_updated_time, distance_of_time_in_words(Time.now, page.updated_on)) : nil))
208 content << "\n" + render_page_hierarchy(pages, page.id, options) if pages[page.id]
208 content << "\n" + render_page_hierarchy(pages, page.id, options) if pages[page.id]
209 content << "</li>\n"
209 content << "</li>\n"
210 end
210 end
211 content << "</ul>\n"
211 content << "</ul>\n"
212 end
212 end
213 content.html_safe
213 content.html_safe
214 end
214 end
215
215
216 # Renders flash messages
216 # Renders flash messages
217 def render_flash_messages
217 def render_flash_messages
218 s = ''
218 s = ''
219 flash.each do |k,v|
219 flash.each do |k,v|
220 s << (content_tag('div', v.html_safe, :class => "flash #{k}"))
220 s << (content_tag('div', v.html_safe, :class => "flash #{k}"))
221 end
221 end
222 s.html_safe
222 s.html_safe
223 end
223 end
224
224
225 # Renders tabs and their content
225 # Renders tabs and their content
226 def render_tabs(tabs)
226 def render_tabs(tabs)
227 if tabs.any?
227 if tabs.any?
228 render :partial => 'common/tabs', :locals => {:tabs => tabs}
228 render :partial => 'common/tabs', :locals => {:tabs => tabs}
229 else
229 else
230 content_tag 'p', l(:label_no_data), :class => "nodata"
230 content_tag 'p', l(:label_no_data), :class => "nodata"
231 end
231 end
232 end
232 end
233
233
234 # Renders the project quick-jump box
234 # Renders the project quick-jump box
235 def render_project_jump_box
235 def render_project_jump_box
236 return unless User.current.logged?
236 return unless User.current.logged?
237 projects = User.current.memberships.collect(&:project).compact.uniq
237 projects = User.current.memberships.collect(&:project).compact.uniq
238 if projects.any?
238 if projects.any?
239 s = '<select onchange="if (this.value != \'\') { window.location = this.value; }">' +
239 s = '<select onchange="if (this.value != \'\') { window.location = this.value; }">' +
240 "<option value=''>#{ l(:label_jump_to_a_project) }</option>" +
240 "<option value=''>#{ l(:label_jump_to_a_project) }</option>" +
241 '<option value="" disabled="disabled">---</option>'
241 '<option value="" disabled="disabled">---</option>'
242 s << project_tree_options_for_select(projects, :selected => @project) do |p|
242 s << project_tree_options_for_select(projects, :selected => @project) do |p|
243 { :value => url_for(:controller => 'projects', :action => 'show', :id => p, :jump => current_menu_item) }
243 { :value => url_for(:controller => 'projects', :action => 'show', :id => p, :jump => current_menu_item) }
244 end
244 end
245 s << '</select>'
245 s << '</select>'
246 s.html_safe
246 s.html_safe
247 end
247 end
248 end
248 end
249
249
250 def project_tree_options_for_select(projects, options = {})
250 def project_tree_options_for_select(projects, options = {})
251 s = ''
251 s = ''
252 project_tree(projects) do |project, level|
252 project_tree(projects) do |project, level|
253 name_prefix = (level > 0 ? ('&nbsp;' * 2 * level + '&#187; ') : '')
253 name_prefix = (level > 0 ? ('&nbsp;' * 2 * level + '&#187; ') : '')
254 tag_options = {:value => project.id}
254 tag_options = {:value => project.id}
255 if project == options[:selected] || (options[:selected].respond_to?(:include?) && options[:selected].include?(project))
255 if project == options[:selected] || (options[:selected].respond_to?(:include?) && options[:selected].include?(project))
256 tag_options[:selected] = 'selected'
256 tag_options[:selected] = 'selected'
257 else
257 else
258 tag_options[:selected] = nil
258 tag_options[:selected] = nil
259 end
259 end
260 tag_options.merge!(yield(project)) if block_given?
260 tag_options.merge!(yield(project)) if block_given?
261 s << content_tag('option', name_prefix + h(project), tag_options)
261 s << content_tag('option', name_prefix + h(project), tag_options)
262 end
262 end
263 s.html_safe
263 s.html_safe
264 end
264 end
265
265
266 # Yields the given block for each project with its level in the tree
266 # Yields the given block for each project with its level in the tree
267 #
267 #
268 # Wrapper for Project#project_tree
268 # Wrapper for Project#project_tree
269 def project_tree(projects, &block)
269 def project_tree(projects, &block)
270 Project.project_tree(projects, &block)
270 Project.project_tree(projects, &block)
271 end
271 end
272
272
273 def project_nested_ul(projects, &block)
273 def project_nested_ul(projects, &block)
274 s = ''
274 s = ''
275 if projects.any?
275 if projects.any?
276 ancestors = []
276 ancestors = []
277 projects.sort_by(&:lft).each do |project|
277 projects.sort_by(&:lft).each do |project|
278 if (ancestors.empty? || project.is_descendant_of?(ancestors.last))
278 if (ancestors.empty? || project.is_descendant_of?(ancestors.last))
279 s << "<ul>\n"
279 s << "<ul>\n"
280 else
280 else
281 ancestors.pop
281 ancestors.pop
282 s << "</li>"
282 s << "</li>"
283 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
283 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
284 ancestors.pop
284 ancestors.pop
285 s << "</ul></li>\n"
285 s << "</ul></li>\n"
286 end
286 end
287 end
287 end
288 s << "<li>"
288 s << "<li>"
289 s << yield(project).to_s
289 s << yield(project).to_s
290 ancestors << project
290 ancestors << project
291 end
291 end
292 s << ("</li></ul>\n" * ancestors.size)
292 s << ("</li></ul>\n" * ancestors.size)
293 end
293 end
294 s.html_safe
294 s.html_safe
295 end
295 end
296
296
297 def principals_check_box_tags(name, principals)
297 def principals_check_box_tags(name, principals)
298 s = ''
298 s = ''
299 principals.sort.each do |principal|
299 principals.sort.each do |principal|
300 s << "<label>#{ check_box_tag name, principal.id, false } #{h principal}</label>\n"
300 s << "<label>#{ check_box_tag name, principal.id, false } #{h principal}</label>\n"
301 end
301 end
302 s.html_safe
302 s.html_safe
303 end
303 end
304
304
305 # Returns a string for users/groups option tags
305 # Returns a string for users/groups option tags
306 def principals_options_for_select(collection, selected=nil)
306 def principals_options_for_select(collection, selected=nil)
307 s = ''
307 s = ''
308 if collection.include?(User.current)
308 if collection.include?(User.current)
309 s << content_tag('option', "<< #{l(:label_me)} >>", :value => User.current.id)
309 s << content_tag('option', "<< #{l(:label_me)} >>", :value => User.current.id)
310 end
310 end
311 groups = ''
311 groups = ''
312 collection.sort.each do |element|
312 collection.sort.each do |element|
313 selected_attribute = ' selected="selected"' if option_value_selected?(element, selected)
313 selected_attribute = ' selected="selected"' if option_value_selected?(element, selected)
314 (element.is_a?(Group) ? groups : s) << %(<option value="#{element.id}"#{selected_attribute}>#{h element.name}</option>)
314 (element.is_a?(Group) ? groups : s) << %(<option value="#{element.id}"#{selected_attribute}>#{h element.name}</option>)
315 end
315 end
316 unless groups.empty?
316 unless groups.empty?
317 s << %(<optgroup label="#{h(l(:label_group_plural))}">#{groups}</optgroup>)
317 s << %(<optgroup label="#{h(l(:label_group_plural))}">#{groups}</optgroup>)
318 end
318 end
319 s
319 s
320 end
320 end
321
321
322 # Truncates and returns the string as a single line
322 # Truncates and returns the string as a single line
323 def truncate_single_line(string, *args)
323 def truncate_single_line(string, *args)
324 truncate(string.to_s, *args).gsub(%r{[\r\n]+}m, ' ')
324 truncate(string.to_s, *args).gsub(%r{[\r\n]+}m, ' ')
325 end
325 end
326
326
327 # Truncates at line break after 250 characters or options[:length]
327 # Truncates at line break after 250 characters or options[:length]
328 def truncate_lines(string, options={})
328 def truncate_lines(string, options={})
329 length = options[:length] || 250
329 length = options[:length] || 250
330 if string.to_s =~ /\A(.{#{length}}.*?)$/m
330 if string.to_s =~ /\A(.{#{length}}.*?)$/m
331 "#{$1}..."
331 "#{$1}..."
332 else
332 else
333 string
333 string
334 end
334 end
335 end
335 end
336
336
337 def anchor(text)
337 def anchor(text)
338 text.to_s.gsub(' ', '_')
338 text.to_s.gsub(' ', '_')
339 end
339 end
340
340
341 def html_hours(text)
341 def html_hours(text)
342 text.gsub(%r{(\d+)\.(\d+)}, '<span class="hours hours-int">\1</span><span class="hours hours-dec">.\2</span>').html_safe
342 text.gsub(%r{(\d+)\.(\d+)}, '<span class="hours hours-int">\1</span><span class="hours hours-dec">.\2</span>').html_safe
343 end
343 end
344
344
345 def authoring(created, author, options={})
345 def authoring(created, author, options={})
346 l(options[:label] || :label_added_time_by, :author => link_to_user(author), :age => time_tag(created)).html_safe
346 l(options[:label] || :label_added_time_by, :author => link_to_user(author), :age => time_tag(created)).html_safe
347 end
347 end
348
348
349 def time_tag(time)
349 def time_tag(time)
350 text = distance_of_time_in_words(Time.now, time)
350 text = distance_of_time_in_words(Time.now, time)
351 if @project
351 if @project
352 link_to(text, {:controller => 'activities', :action => 'index', :id => @project, :from => time.to_date}, :title => format_time(time))
352 link_to(text, {:controller => 'activities', :action => 'index', :id => @project, :from => time.to_date}, :title => format_time(time))
353 else
353 else
354 content_tag('acronym', text, :title => format_time(time))
354 content_tag('acronym', text, :title => format_time(time))
355 end
355 end
356 end
356 end
357
357
358 def syntax_highlight(name, content)
358 def syntax_highlight(name, content)
359 Redmine::SyntaxHighlighting.highlight_by_filename(content, name)
359 Redmine::SyntaxHighlighting.highlight_by_filename(content, name)
360 end
360 end
361
361
362 def to_path_param(path)
362 def to_path_param(path)
363 path.to_s.split(%r{[/\\]}).select {|p| !p.blank?}
363 path.to_s.split(%r{[/\\]}).select {|p| !p.blank?}
364 end
364 end
365
365
366 def pagination_links_full(paginator, count=nil, options={})
366 def pagination_links_full(paginator, count=nil, options={})
367 page_param = options.delete(:page_param) || :page
367 page_param = options.delete(:page_param) || :page
368 per_page_links = options.delete(:per_page_links)
368 per_page_links = options.delete(:per_page_links)
369 url_param = params.dup
369 url_param = params.dup
370
370
371 html = ''
371 html = ''
372 if paginator.current.previous
372 if paginator.current.previous
373 # \xc2\xab(utf-8) = &#171;
373 # \xc2\xab(utf-8) = &#171;
374 html << link_to_content_update(
374 html << link_to_content_update(
375 "\xc2\xab " + l(:label_previous),
375 "\xc2\xab " + l(:label_previous),
376 url_param.merge(page_param => paginator.current.previous)) + ' '
376 url_param.merge(page_param => paginator.current.previous)) + ' '
377 end
377 end
378
378
379 html << (pagination_links_each(paginator, options) do |n|
379 html << (pagination_links_each(paginator, options) do |n|
380 link_to_content_update(n.to_s, url_param.merge(page_param => n))
380 link_to_content_update(n.to_s, url_param.merge(page_param => n))
381 end || '')
381 end || '')
382
382
383 if paginator.current.next
383 if paginator.current.next
384 # \xc2\xbb(utf-8) = &#187;
384 # \xc2\xbb(utf-8) = &#187;
385 html << ' ' + link_to_content_update(
385 html << ' ' + link_to_content_update(
386 (l(:label_next) + " \xc2\xbb"),
386 (l(:label_next) + " \xc2\xbb"),
387 url_param.merge(page_param => paginator.current.next))
387 url_param.merge(page_param => paginator.current.next))
388 end
388 end
389
389
390 unless count.nil?
390 unless count.nil?
391 html << " (#{paginator.current.first_item}-#{paginator.current.last_item}/#{count})"
391 html << " (#{paginator.current.first_item}-#{paginator.current.last_item}/#{count})"
392 if per_page_links != false && links = per_page_links(paginator.items_per_page)
392 if per_page_links != false && links = per_page_links(paginator.items_per_page)
393 html << " | #{links}"
393 html << " | #{links}"
394 end
394 end
395 end
395 end
396
396
397 html.html_safe
397 html.html_safe
398 end
398 end
399
399
400 def per_page_links(selected=nil)
400 def per_page_links(selected=nil)
401 links = Setting.per_page_options_array.collect do |n|
401 links = Setting.per_page_options_array.collect do |n|
402 n == selected ? n : link_to_content_update(n, params.merge(:per_page => n))
402 n == selected ? n : link_to_content_update(n, params.merge(:per_page => n))
403 end
403 end
404 links.size > 1 ? l(:label_display_per_page, links.join(', ')) : nil
404 links.size > 1 ? l(:label_display_per_page, links.join(', ')) : nil
405 end
405 end
406
406
407 def reorder_links(name, url, method = :post)
407 def reorder_links(name, url, method = :post)
408 link_to(image_tag('2uparrow.png', :alt => l(:label_sort_highest)),
408 link_to(image_tag('2uparrow.png', :alt => l(:label_sort_highest)),
409 url.merge({"#{name}[move_to]" => 'highest'}),
409 url.merge({"#{name}[move_to]" => 'highest'}),
410 :method => method, :title => l(:label_sort_highest)) +
410 :method => method, :title => l(:label_sort_highest)) +
411 link_to(image_tag('1uparrow.png', :alt => l(:label_sort_higher)),
411 link_to(image_tag('1uparrow.png', :alt => l(:label_sort_higher)),
412 url.merge({"#{name}[move_to]" => 'higher'}),
412 url.merge({"#{name}[move_to]" => 'higher'}),
413 :method => method, :title => l(:label_sort_higher)) +
413 :method => method, :title => l(:label_sort_higher)) +
414 link_to(image_tag('1downarrow.png', :alt => l(:label_sort_lower)),
414 link_to(image_tag('1downarrow.png', :alt => l(:label_sort_lower)),
415 url.merge({"#{name}[move_to]" => 'lower'}),
415 url.merge({"#{name}[move_to]" => 'lower'}),
416 :method => method, :title => l(:label_sort_lower)) +
416 :method => method, :title => l(:label_sort_lower)) +
417 link_to(image_tag('2downarrow.png', :alt => l(:label_sort_lowest)),
417 link_to(image_tag('2downarrow.png', :alt => l(:label_sort_lowest)),
418 url.merge({"#{name}[move_to]" => 'lowest'}),
418 url.merge({"#{name}[move_to]" => 'lowest'}),
419 :method => method, :title => l(:label_sort_lowest))
419 :method => method, :title => l(:label_sort_lowest))
420 end
420 end
421
421
422 def breadcrumb(*args)
422 def breadcrumb(*args)
423 elements = args.flatten
423 elements = args.flatten
424 elements.any? ? content_tag('p', (args.join(" \xc2\xbb ") + " \xc2\xbb ").html_safe, :class => 'breadcrumb') : nil
424 elements.any? ? content_tag('p', (args.join(" \xc2\xbb ") + " \xc2\xbb ").html_safe, :class => 'breadcrumb') : nil
425 end
425 end
426
426
427 def other_formats_links(&block)
427 def other_formats_links(&block)
428 concat('<p class="other-formats">'.html_safe + l(:label_export_to))
428 concat('<p class="other-formats">'.html_safe + l(:label_export_to))
429 yield Redmine::Views::OtherFormatsBuilder.new(self)
429 yield Redmine::Views::OtherFormatsBuilder.new(self)
430 concat('</p>'.html_safe)
430 concat('</p>'.html_safe)
431 end
431 end
432
432
433 def page_header_title
433 def page_header_title
434 if @project.nil? || @project.new_record?
434 if @project.nil? || @project.new_record?
435 h(Setting.app_title)
435 h(Setting.app_title)
436 else
436 else
437 b = []
437 b = []
438 ancestors = (@project.root? ? [] : @project.ancestors.visible.all)
438 ancestors = (@project.root? ? [] : @project.ancestors.visible.all)
439 if ancestors.any?
439 if ancestors.any?
440 root = ancestors.shift
440 root = ancestors.shift
441 b << link_to_project(root, {:jump => current_menu_item}, :class => 'root')
441 b << link_to_project(root, {:jump => current_menu_item}, :class => 'root')
442 if ancestors.size > 2
442 if ancestors.size > 2
443 b << "\xe2\x80\xa6"
443 b << "\xe2\x80\xa6"
444 ancestors = ancestors[-2, 2]
444 ancestors = ancestors[-2, 2]
445 end
445 end
446 b += ancestors.collect {|p| link_to_project(p, {:jump => current_menu_item}, :class => 'ancestor') }
446 b += ancestors.collect {|p| link_to_project(p, {:jump => current_menu_item}, :class => 'ancestor') }
447 end
447 end
448 b << h(@project)
448 b << h(@project)
449 b.join(" \xc2\xbb ").html_safe
449 b.join(" \xc2\xbb ").html_safe
450 end
450 end
451 end
451 end
452
452
453 def html_title(*args)
453 def html_title(*args)
454 if args.empty?
454 if args.empty?
455 title = @html_title || []
455 title = @html_title || []
456 title << @project.name if @project
456 title << @project.name if @project
457 title << Setting.app_title unless Setting.app_title == title.last
457 title << Setting.app_title unless Setting.app_title == title.last
458 title.select {|t| !t.blank? }.join(' - ')
458 title.select {|t| !t.blank? }.join(' - ')
459 else
459 else
460 @html_title ||= []
460 @html_title ||= []
461 @html_title += args
461 @html_title += args
462 end
462 end
463 end
463 end
464
464
465 # Returns the theme, controller name, and action as css classes for the
465 # Returns the theme, controller name, and action as css classes for the
466 # HTML body.
466 # HTML body.
467 def body_css_classes
467 def body_css_classes
468 css = []
468 css = []
469 if theme = Redmine::Themes.theme(Setting.ui_theme)
469 if theme = Redmine::Themes.theme(Setting.ui_theme)
470 css << 'theme-' + theme.name
470 css << 'theme-' + theme.name
471 end
471 end
472
472
473 css << 'controller-' + params[:controller]
473 css << 'controller-' + params[:controller]
474 css << 'action-' + params[:action]
474 css << 'action-' + params[:action]
475 css.join(' ')
475 css.join(' ')
476 end
476 end
477
477
478 def accesskey(s)
478 def accesskey(s)
479 Redmine::AccessKeys.key_for s
479 Redmine::AccessKeys.key_for s
480 end
480 end
481
481
482 # Formats text according to system settings.
482 # Formats text according to system settings.
483 # 2 ways to call this method:
483 # 2 ways to call this method:
484 # * with a String: textilizable(text, options)
484 # * with a String: textilizable(text, options)
485 # * with an object and one of its attribute: textilizable(issue, :description, options)
485 # * with an object and one of its attribute: textilizable(issue, :description, options)
486 def textilizable(*args)
486 def textilizable(*args)
487 options = args.last.is_a?(Hash) ? args.pop : {}
487 options = args.last.is_a?(Hash) ? args.pop : {}
488 case args.size
488 case args.size
489 when 1
489 when 1
490 obj = options[:object]
490 obj = options[:object]
491 text = args.shift
491 text = args.shift
492 when 2
492 when 2
493 obj = args.shift
493 obj = args.shift
494 attr = args.shift
494 attr = args.shift
495 text = obj.send(attr).to_s
495 text = obj.send(attr).to_s
496 else
496 else
497 raise ArgumentError, 'invalid arguments to textilizable'
497 raise ArgumentError, 'invalid arguments to textilizable'
498 end
498 end
499 return '' if text.blank?
499 return '' if text.blank?
500 project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
500 project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
501 only_path = options.delete(:only_path) == false ? false : true
501 only_path = options.delete(:only_path) == false ? false : true
502
502
503 text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text, :object => obj, :attribute => attr)
503 text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text, :object => obj, :attribute => attr)
504
504
505 @parsed_headings = []
505 @parsed_headings = []
506 @heading_anchors = {}
506 @current_section = 0 if options[:edit_section_links]
507 @current_section = 0 if options[:edit_section_links]
507
508
508 parse_sections(text, project, obj, attr, only_path, options)
509 parse_sections(text, project, obj, attr, only_path, options)
509 text = parse_non_pre_blocks(text) do |text|
510 text = parse_non_pre_blocks(text) do |text|
510 [:parse_inline_attachments, :parse_wiki_links, :parse_redmine_links, :parse_macros].each do |method_name|
511 [:parse_inline_attachments, :parse_wiki_links, :parse_redmine_links, :parse_macros].each do |method_name|
511 send method_name, text, project, obj, attr, only_path, options
512 send method_name, text, project, obj, attr, only_path, options
512 end
513 end
513 end
514 end
514 parse_headings(text, project, obj, attr, only_path, options)
515 parse_headings(text, project, obj, attr, only_path, options)
515
516
516 if @parsed_headings.any?
517 if @parsed_headings.any?
517 replace_toc(text, @parsed_headings)
518 replace_toc(text, @parsed_headings)
518 end
519 end
519
520
520 text.html_safe
521 text.html_safe
521 end
522 end
522
523
523 def parse_non_pre_blocks(text)
524 def parse_non_pre_blocks(text)
524 s = StringScanner.new(text)
525 s = StringScanner.new(text)
525 tags = []
526 tags = []
526 parsed = ''
527 parsed = ''
527 while !s.eos?
528 while !s.eos?
528 s.scan(/(.*?)(<(\/)?(pre|code)(.*?)>|\z)/im)
529 s.scan(/(.*?)(<(\/)?(pre|code)(.*?)>|\z)/im)
529 text, full_tag, closing, tag = s[1], s[2], s[3], s[4]
530 text, full_tag, closing, tag = s[1], s[2], s[3], s[4]
530 if tags.empty?
531 if tags.empty?
531 yield text
532 yield text
532 end
533 end
533 parsed << text
534 parsed << text
534 if tag
535 if tag
535 if closing
536 if closing
536 if tags.last == tag.downcase
537 if tags.last == tag.downcase
537 tags.pop
538 tags.pop
538 end
539 end
539 else
540 else
540 tags << tag.downcase
541 tags << tag.downcase
541 end
542 end
542 parsed << full_tag
543 parsed << full_tag
543 end
544 end
544 end
545 end
545 # Close any non closing tags
546 # Close any non closing tags
546 while tag = tags.pop
547 while tag = tags.pop
547 parsed << "</#{tag}>"
548 parsed << "</#{tag}>"
548 end
549 end
549 parsed.html_safe
550 parsed.html_safe
550 end
551 end
551
552
552 def parse_inline_attachments(text, project, obj, attr, only_path, options)
553 def parse_inline_attachments(text, project, obj, attr, only_path, options)
553 # when using an image link, try to use an attachment, if possible
554 # when using an image link, try to use an attachment, if possible
554 if options[:attachments] || (obj && obj.respond_to?(:attachments))
555 if options[:attachments] || (obj && obj.respond_to?(:attachments))
555 attachments = options[:attachments] || obj.attachments
556 attachments = options[:attachments] || obj.attachments
556 text.gsub!(/src="([^\/"]+\.(bmp|gif|jpg|jpe|jpeg|png))"(\s+alt="([^"]*)")?/i) do |m|
557 text.gsub!(/src="([^\/"]+\.(bmp|gif|jpg|jpe|jpeg|png))"(\s+alt="([^"]*)")?/i) do |m|
557 filename, ext, alt, alttext = $1.downcase, $2, $3, $4
558 filename, ext, alt, alttext = $1.downcase, $2, $3, $4
558 # search for the picture in attachments
559 # search for the picture in attachments
559 if found = Attachment.latest_attach(attachments, filename)
560 if found = Attachment.latest_attach(attachments, filename)
560 image_url = url_for :only_path => only_path, :controller => 'attachments',
561 image_url = url_for :only_path => only_path, :controller => 'attachments',
561 :action => 'download', :id => found
562 :action => 'download', :id => found
562 desc = found.description.to_s.gsub('"', '')
563 desc = found.description.to_s.gsub('"', '')
563 if !desc.blank? && alttext.blank?
564 if !desc.blank? && alttext.blank?
564 alt = " title=\"#{desc}\" alt=\"#{desc}\""
565 alt = " title=\"#{desc}\" alt=\"#{desc}\""
565 end
566 end
566 "src=\"#{image_url}\"#{alt}".html_safe
567 "src=\"#{image_url}\"#{alt}".html_safe
567 else
568 else
568 m.html_safe
569 m.html_safe
569 end
570 end
570 end
571 end
571 end
572 end
572 end
573 end
573
574
574 # Wiki links
575 # Wiki links
575 #
576 #
576 # Examples:
577 # Examples:
577 # [[mypage]]
578 # [[mypage]]
578 # [[mypage|mytext]]
579 # [[mypage|mytext]]
579 # wiki links can refer other project wikis, using project name or identifier:
580 # wiki links can refer other project wikis, using project name or identifier:
580 # [[project:]] -> wiki starting page
581 # [[project:]] -> wiki starting page
581 # [[project:|mytext]]
582 # [[project:|mytext]]
582 # [[project:mypage]]
583 # [[project:mypage]]
583 # [[project:mypage|mytext]]
584 # [[project:mypage|mytext]]
584 def parse_wiki_links(text, project, obj, attr, only_path, options)
585 def parse_wiki_links(text, project, obj, attr, only_path, options)
585 text.gsub!(/(!)?(\[\[([^\]\n\|]+)(\|([^\]\n\|]+))?\]\])/) do |m|
586 text.gsub!(/(!)?(\[\[([^\]\n\|]+)(\|([^\]\n\|]+))?\]\])/) do |m|
586 link_project = project
587 link_project = project
587 esc, all, page, title = $1, $2, $3, $5
588 esc, all, page, title = $1, $2, $3, $5
588 if esc.nil?
589 if esc.nil?
589 if page =~ /^([^\:]+)\:(.*)$/
590 if page =~ /^([^\:]+)\:(.*)$/
590 link_project = Project.find_by_identifier($1) || Project.find_by_name($1)
591 link_project = Project.find_by_identifier($1) || Project.find_by_name($1)
591 page = $2
592 page = $2
592 title ||= $1 if page.blank?
593 title ||= $1 if page.blank?
593 end
594 end
594
595
595 if link_project && link_project.wiki
596 if link_project && link_project.wiki
596 # extract anchor
597 # extract anchor
597 anchor = nil
598 anchor = nil
598 if page =~ /^(.+?)\#(.+)$/
599 if page =~ /^(.+?)\#(.+)$/
599 page, anchor = $1, $2
600 page, anchor = $1, $2
600 end
601 end
601 anchor = sanitize_anchor_name(anchor) if anchor.present?
602 anchor = sanitize_anchor_name(anchor) if anchor.present?
602 # check if page exists
603 # check if page exists
603 wiki_page = link_project.wiki.find_page(page)
604 wiki_page = link_project.wiki.find_page(page)
604 url = if anchor.present? && wiki_page.present? && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version)) && obj.page == wiki_page
605 url = if anchor.present? && wiki_page.present? && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version)) && obj.page == wiki_page
605 "##{anchor}"
606 "##{anchor}"
606 else
607 else
607 case options[:wiki_links]
608 case options[:wiki_links]
608 when :local; "#{page.present? ? Wiki.titleize(page) : ''}.html" + (anchor.present? ? "##{anchor}" : '')
609 when :local; "#{page.present? ? Wiki.titleize(page) : ''}.html" + (anchor.present? ? "##{anchor}" : '')
609 when :anchor; "##{page.present? ? Wiki.titleize(page) : title}" + (anchor.present? ? "_#{anchor}" : '') # used for single-file wiki export
610 when :anchor; "##{page.present? ? Wiki.titleize(page) : title}" + (anchor.present? ? "_#{anchor}" : '') # used for single-file wiki export
610 else
611 else
611 wiki_page_id = page.present? ? Wiki.titleize(page) : nil
612 wiki_page_id = page.present? ? Wiki.titleize(page) : nil
612 parent = wiki_page.nil? && obj.is_a?(WikiContent) && obj.page && project == link_project ? obj.page.title : nil
613 parent = wiki_page.nil? && obj.is_a?(WikiContent) && obj.page && project == link_project ? obj.page.title : nil
613 url_for(:only_path => only_path, :controller => 'wiki', :action => 'show', :project_id => link_project,
614 url_for(:only_path => only_path, :controller => 'wiki', :action => 'show', :project_id => link_project,
614 :id => wiki_page_id, :anchor => anchor, :parent => parent)
615 :id => wiki_page_id, :anchor => anchor, :parent => parent)
615 end
616 end
616 end
617 end
617 link_to(title.present? ? title.html_safe : h(page), url, :class => ('wiki-page' + (wiki_page ? '' : ' new')))
618 link_to(title.present? ? title.html_safe : h(page), url, :class => ('wiki-page' + (wiki_page ? '' : ' new')))
618 else
619 else
619 # project or wiki doesn't exist
620 # project or wiki doesn't exist
620 all.html_safe
621 all.html_safe
621 end
622 end
622 else
623 else
623 all.html_safe
624 all.html_safe
624 end
625 end
625 end
626 end
626 end
627 end
627
628
628 # Redmine links
629 # Redmine links
629 #
630 #
630 # Examples:
631 # Examples:
631 # Issues:
632 # Issues:
632 # #52 -> Link to issue #52
633 # #52 -> Link to issue #52
633 # Changesets:
634 # Changesets:
634 # r52 -> Link to revision 52
635 # r52 -> Link to revision 52
635 # commit:a85130f -> Link to scmid starting with a85130f
636 # commit:a85130f -> Link to scmid starting with a85130f
636 # Documents:
637 # Documents:
637 # document#17 -> Link to document with id 17
638 # document#17 -> Link to document with id 17
638 # document:Greetings -> Link to the document with title "Greetings"
639 # document:Greetings -> Link to the document with title "Greetings"
639 # document:"Some document" -> Link to the document with title "Some document"
640 # document:"Some document" -> Link to the document with title "Some document"
640 # Versions:
641 # Versions:
641 # version#3 -> Link to version with id 3
642 # version#3 -> Link to version with id 3
642 # version:1.0.0 -> Link to version named "1.0.0"
643 # version:1.0.0 -> Link to version named "1.0.0"
643 # version:"1.0 beta 2" -> Link to version named "1.0 beta 2"
644 # version:"1.0 beta 2" -> Link to version named "1.0 beta 2"
644 # Attachments:
645 # Attachments:
645 # attachment:file.zip -> Link to the attachment of the current object named file.zip
646 # attachment:file.zip -> Link to the attachment of the current object named file.zip
646 # Source files:
647 # Source files:
647 # source:some/file -> Link to the file located at /some/file in the project's repository
648 # source:some/file -> Link to the file located at /some/file in the project's repository
648 # source:some/file@52 -> Link to the file's revision 52
649 # source:some/file@52 -> Link to the file's revision 52
649 # source:some/file#L120 -> Link to line 120 of the file
650 # source:some/file#L120 -> Link to line 120 of the file
650 # source:some/file@52#L120 -> Link to line 120 of the file's revision 52
651 # source:some/file@52#L120 -> Link to line 120 of the file's revision 52
651 # export:some/file -> Force the download of the file
652 # export:some/file -> Force the download of the file
652 # Forum messages:
653 # Forum messages:
653 # message#1218 -> Link to message with id 1218
654 # message#1218 -> Link to message with id 1218
654 #
655 #
655 # Links can refer other objects from other projects, using project identifier:
656 # Links can refer other objects from other projects, using project identifier:
656 # identifier:r52
657 # identifier:r52
657 # identifier:document:"Some document"
658 # identifier:document:"Some document"
658 # identifier:version:1.0.0
659 # identifier:version:1.0.0
659 # identifier:source:some/file
660 # identifier:source:some/file
660 def parse_redmine_links(text, project, obj, attr, only_path, options)
661 def parse_redmine_links(text, project, obj, attr, only_path, options)
661 text.gsub!(%r{([\s\(,\-\[\>]|^)(!)?(([a-z0-9\-_]+):)?(attachment|document|version|forum|news|message|project|commit|source|export)?(((#)|((([a-z0-9\-]+)\|)?(r)))(\d+)|(:)([^"\s<>][^\s<>]*?|"[^"]+?"))(?=(?=[[:punct:]][^A-Za-z0-9_/])|,|\s|\]|<|$)}) do |m|
662 text.gsub!(%r{([\s\(,\-\[\>]|^)(!)?(([a-z0-9\-_]+):)?(attachment|document|version|forum|news|message|project|commit|source|export)?(((#)|((([a-z0-9\-]+)\|)?(r)))(\d+)|(:)([^"\s<>][^\s<>]*?|"[^"]+?"))(?=(?=[[:punct:]][^A-Za-z0-9_/])|,|\s|\]|<|$)}) do |m|
662 leading, esc, project_prefix, project_identifier, prefix, repo_prefix, repo_identifier, sep, identifier = $1, $2, $3, $4, $5, $10, $11, $8 || $12 || $14, $13 || $15
663 leading, esc, project_prefix, project_identifier, prefix, repo_prefix, repo_identifier, sep, identifier = $1, $2, $3, $4, $5, $10, $11, $8 || $12 || $14, $13 || $15
663 link = nil
664 link = nil
664 if project_identifier
665 if project_identifier
665 project = Project.visible.find_by_identifier(project_identifier)
666 project = Project.visible.find_by_identifier(project_identifier)
666 end
667 end
667 if esc.nil?
668 if esc.nil?
668 if prefix.nil? && sep == 'r'
669 if prefix.nil? && sep == 'r'
669 if project
670 if project
670 repository = nil
671 repository = nil
671 if repo_identifier
672 if repo_identifier
672 repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
673 repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
673 else
674 else
674 repository = project.repository
675 repository = project.repository
675 end
676 end
676 # project.changesets.visible raises an SQL error because of a double join on repositories
677 # project.changesets.visible raises an SQL error because of a double join on repositories
677 if repository && (changeset = Changeset.visible.find_by_repository_id_and_revision(repository.id, identifier))
678 if repository && (changeset = Changeset.visible.find_by_repository_id_and_revision(repository.id, identifier))
678 link = link_to(h("#{project_prefix}#{repo_prefix}r#{identifier}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :repository_id => repository.identifier_param, :rev => changeset.revision},
679 link = link_to(h("#{project_prefix}#{repo_prefix}r#{identifier}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :repository_id => repository.identifier_param, :rev => changeset.revision},
679 :class => 'changeset',
680 :class => 'changeset',
680 :title => truncate_single_line(changeset.comments, :length => 100))
681 :title => truncate_single_line(changeset.comments, :length => 100))
681 end
682 end
682 end
683 end
683 elsif sep == '#'
684 elsif sep == '#'
684 oid = identifier.to_i
685 oid = identifier.to_i
685 case prefix
686 case prefix
686 when nil
687 when nil
687 if issue = Issue.visible.find_by_id(oid, :include => :status)
688 if issue = Issue.visible.find_by_id(oid, :include => :status)
688 link = link_to("##{oid}", {:only_path => only_path, :controller => 'issues', :action => 'show', :id => oid},
689 link = link_to("##{oid}", {:only_path => only_path, :controller => 'issues', :action => 'show', :id => oid},
689 :class => issue.css_classes,
690 :class => issue.css_classes,
690 :title => "#{truncate(issue.subject, :length => 100)} (#{issue.status.name})")
691 :title => "#{truncate(issue.subject, :length => 100)} (#{issue.status.name})")
691 end
692 end
692 when 'document'
693 when 'document'
693 if document = Document.visible.find_by_id(oid)
694 if document = Document.visible.find_by_id(oid)
694 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
695 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
695 :class => 'document'
696 :class => 'document'
696 end
697 end
697 when 'version'
698 when 'version'
698 if version = Version.visible.find_by_id(oid)
699 if version = Version.visible.find_by_id(oid)
699 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
700 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
700 :class => 'version'
701 :class => 'version'
701 end
702 end
702 when 'message'
703 when 'message'
703 if message = Message.visible.find_by_id(oid, :include => :parent)
704 if message = Message.visible.find_by_id(oid, :include => :parent)
704 link = link_to_message(message, {:only_path => only_path}, :class => 'message')
705 link = link_to_message(message, {:only_path => only_path}, :class => 'message')
705 end
706 end
706 when 'forum'
707 when 'forum'
707 if board = Board.visible.find_by_id(oid)
708 if board = Board.visible.find_by_id(oid)
708 link = link_to h(board.name), {:only_path => only_path, :controller => 'boards', :action => 'show', :id => board, :project_id => board.project},
709 link = link_to h(board.name), {:only_path => only_path, :controller => 'boards', :action => 'show', :id => board, :project_id => board.project},
709 :class => 'board'
710 :class => 'board'
710 end
711 end
711 when 'news'
712 when 'news'
712 if news = News.visible.find_by_id(oid)
713 if news = News.visible.find_by_id(oid)
713 link = link_to h(news.title), {:only_path => only_path, :controller => 'news', :action => 'show', :id => news},
714 link = link_to h(news.title), {:only_path => only_path, :controller => 'news', :action => 'show', :id => news},
714 :class => 'news'
715 :class => 'news'
715 end
716 end
716 when 'project'
717 when 'project'
717 if p = Project.visible.find_by_id(oid)
718 if p = Project.visible.find_by_id(oid)
718 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
719 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
719 end
720 end
720 end
721 end
721 elsif sep == ':'
722 elsif sep == ':'
722 # removes the double quotes if any
723 # removes the double quotes if any
723 name = identifier.gsub(%r{^"(.*)"$}, "\\1")
724 name = identifier.gsub(%r{^"(.*)"$}, "\\1")
724 case prefix
725 case prefix
725 when 'document'
726 when 'document'
726 if project && document = project.documents.visible.find_by_title(name)
727 if project && document = project.documents.visible.find_by_title(name)
727 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
728 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
728 :class => 'document'
729 :class => 'document'
729 end
730 end
730 when 'version'
731 when 'version'
731 if project && version = project.versions.visible.find_by_name(name)
732 if project && version = project.versions.visible.find_by_name(name)
732 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
733 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
733 :class => 'version'
734 :class => 'version'
734 end
735 end
735 when 'forum'
736 when 'forum'
736 if project && board = project.boards.visible.find_by_name(name)
737 if project && board = project.boards.visible.find_by_name(name)
737 link = link_to h(board.name), {:only_path => only_path, :controller => 'boards', :action => 'show', :id => board, :project_id => board.project},
738 link = link_to h(board.name), {:only_path => only_path, :controller => 'boards', :action => 'show', :id => board, :project_id => board.project},
738 :class => 'board'
739 :class => 'board'
739 end
740 end
740 when 'news'
741 when 'news'
741 if project && news = project.news.visible.find_by_title(name)
742 if project && news = project.news.visible.find_by_title(name)
742 link = link_to h(news.title), {:only_path => only_path, :controller => 'news', :action => 'show', :id => news},
743 link = link_to h(news.title), {:only_path => only_path, :controller => 'news', :action => 'show', :id => news},
743 :class => 'news'
744 :class => 'news'
744 end
745 end
745 when 'commit', 'source', 'export'
746 when 'commit', 'source', 'export'
746 if project
747 if project
747 repository = nil
748 repository = nil
748 if name =~ %r{^(([a-z0-9\-]+)\|)(.+)$}
749 if name =~ %r{^(([a-z0-9\-]+)\|)(.+)$}
749 repo_prefix, repo_identifier, name = $1, $2, $3
750 repo_prefix, repo_identifier, name = $1, $2, $3
750 repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
751 repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
751 else
752 else
752 repository = project.repository
753 repository = project.repository
753 end
754 end
754 if prefix == 'commit'
755 if prefix == 'commit'
755 if repository && (changeset = Changeset.visible.find(:first, :conditions => ["repository_id = ? AND scmid LIKE ?", repository.id, "#{name}%"]))
756 if repository && (changeset = Changeset.visible.find(:first, :conditions => ["repository_id = ? AND scmid LIKE ?", repository.id, "#{name}%"]))
756 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},
757 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},
757 :class => 'changeset',
758 :class => 'changeset',
758 :title => truncate_single_line(h(changeset.comments), :length => 100)
759 :title => truncate_single_line(h(changeset.comments), :length => 100)
759 end
760 end
760 else
761 else
761 if repository && User.current.allowed_to?(:browse_repository, project)
762 if repository && User.current.allowed_to?(:browse_repository, project)
762 name =~ %r{^[/\\]*(.*?)(@([0-9a-f]+))?(#(L\d+))?$}
763 name =~ %r{^[/\\]*(.*?)(@([0-9a-f]+))?(#(L\d+))?$}
763 path, rev, anchor = $1, $3, $5
764 path, rev, anchor = $1, $3, $5
764 link = link_to h("#{project_prefix}#{prefix}:#{repo_prefix}#{name}"), {:controller => 'repositories', :action => 'entry', :id => project, :repository_id => repository.identifier_param,
765 link = link_to h("#{project_prefix}#{prefix}:#{repo_prefix}#{name}"), {:controller => 'repositories', :action => 'entry', :id => project, :repository_id => repository.identifier_param,
765 :path => to_path_param(path),
766 :path => to_path_param(path),
766 :rev => rev,
767 :rev => rev,
767 :anchor => anchor,
768 :anchor => anchor,
768 :format => (prefix == 'export' ? 'raw' : nil)},
769 :format => (prefix == 'export' ? 'raw' : nil)},
769 :class => (prefix == 'export' ? 'source download' : 'source')
770 :class => (prefix == 'export' ? 'source download' : 'source')
770 end
771 end
771 end
772 end
772 repo_prefix = nil
773 repo_prefix = nil
773 end
774 end
774 when 'attachment'
775 when 'attachment'
775 attachments = options[:attachments] || (obj && obj.respond_to?(:attachments) ? obj.attachments : nil)
776 attachments = options[:attachments] || (obj && obj.respond_to?(:attachments) ? obj.attachments : nil)
776 if attachments && attachment = attachments.detect {|a| a.filename == name }
777 if attachments && attachment = attachments.detect {|a| a.filename == name }
777 link = link_to h(attachment.filename), {:only_path => only_path, :controller => 'attachments', :action => 'download', :id => attachment},
778 link = link_to h(attachment.filename), {:only_path => only_path, :controller => 'attachments', :action => 'download', :id => attachment},
778 :class => 'attachment'
779 :class => 'attachment'
779 end
780 end
780 when 'project'
781 when 'project'
781 if p = Project.visible.find(:first, :conditions => ["identifier = :s OR LOWER(name) = :s", {:s => name.downcase}])
782 if p = Project.visible.find(:first, :conditions => ["identifier = :s OR LOWER(name) = :s", {:s => name.downcase}])
782 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
783 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
783 end
784 end
784 end
785 end
785 end
786 end
786 end
787 end
787 (leading + (link || "#{project_prefix}#{prefix}#{repo_prefix}#{sep}#{identifier}")).html_safe
788 (leading + (link || "#{project_prefix}#{prefix}#{repo_prefix}#{sep}#{identifier}")).html_safe
788 end
789 end
789 end
790 end
790
791
791 HEADING_RE = /(<h(1|2|3|4)( [^>]+)?>(.+?)<\/h(1|2|3|4)>)/i unless const_defined?(:HEADING_RE)
792 HEADING_RE = /(<h(1|2|3|4)( [^>]+)?>(.+?)<\/h(1|2|3|4)>)/i unless const_defined?(:HEADING_RE)
792
793
793 def parse_sections(text, project, obj, attr, only_path, options)
794 def parse_sections(text, project, obj, attr, only_path, options)
794 return unless options[:edit_section_links]
795 return unless options[:edit_section_links]
795 text.gsub!(HEADING_RE) do
796 text.gsub!(HEADING_RE) do
796 @current_section += 1
797 @current_section += 1
797 if @current_section > 1
798 if @current_section > 1
798 content_tag('div',
799 content_tag('div',
799 link_to(image_tag('edit.png'), options[:edit_section_links].merge(:section => @current_section)),
800 link_to(image_tag('edit.png'), options[:edit_section_links].merge(:section => @current_section)),
800 :class => 'contextual',
801 :class => 'contextual',
801 :title => l(:button_edit_section)) + $1
802 :title => l(:button_edit_section)) + $1
802 else
803 else
803 $1
804 $1
804 end
805 end
805 end
806 end
806 end
807 end
807
808
808 # Headings and TOC
809 # Headings and TOC
809 # Adds ids and links to headings unless options[:headings] is set to false
810 # Adds ids and links to headings unless options[:headings] is set to false
810 def parse_headings(text, project, obj, attr, only_path, options)
811 def parse_headings(text, project, obj, attr, only_path, options)
811 return if options[:headings] == false
812 return if options[:headings] == false
812
813
813 text.gsub!(HEADING_RE) do
814 text.gsub!(HEADING_RE) do
814 level, attrs, content = $2.to_i, $3, $4
815 level, attrs, content = $2.to_i, $3, $4
815 item = strip_tags(content).strip
816 item = strip_tags(content).strip
816 anchor = sanitize_anchor_name(item)
817 anchor = sanitize_anchor_name(item)
817 # used for single-file wiki export
818 # used for single-file wiki export
818 anchor = "#{obj.page.title}_#{anchor}" if options[:wiki_links] == :anchor && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version))
819 anchor = "#{obj.page.title}_#{anchor}" if options[:wiki_links] == :anchor && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version))
820 @heading_anchors[anchor] ||= 0
821 idx = (@heading_anchors[anchor] += 1)
822 if idx > 1
823 anchor = "#{anchor}-#{idx}"
824 end
819 @parsed_headings << [level, anchor, item]
825 @parsed_headings << [level, anchor, item]
820 "<a name=\"#{anchor}\"></a>\n<h#{level} #{attrs}>#{content}<a href=\"##{anchor}\" class=\"wiki-anchor\">&para;</a></h#{level}>"
826 "<a name=\"#{anchor}\"></a>\n<h#{level} #{attrs}>#{content}<a href=\"##{anchor}\" class=\"wiki-anchor\">&para;</a></h#{level}>"
821 end
827 end
822 end
828 end
823
829
824 MACROS_RE = /
830 MACROS_RE = /
825 (!)? # escaping
831 (!)? # escaping
826 (
832 (
827 \{\{ # opening tag
833 \{\{ # opening tag
828 ([\w]+) # macro name
834 ([\w]+) # macro name
829 (\(([^\}]*)\))? # optional arguments
835 (\(([^\}]*)\))? # optional arguments
830 \}\} # closing tag
836 \}\} # closing tag
831 )
837 )
832 /x unless const_defined?(:MACROS_RE)
838 /x unless const_defined?(:MACROS_RE)
833
839
834 # Macros substitution
840 # Macros substitution
835 def parse_macros(text, project, obj, attr, only_path, options)
841 def parse_macros(text, project, obj, attr, only_path, options)
836 text.gsub!(MACROS_RE) do
842 text.gsub!(MACROS_RE) do
837 esc, all, macro = $1, $2, $3.downcase
843 esc, all, macro = $1, $2, $3.downcase
838 args = ($5 || '').split(',').each(&:strip)
844 args = ($5 || '').split(',').each(&:strip)
839 if esc.nil?
845 if esc.nil?
840 begin
846 begin
841 exec_macro(macro, obj, args)
847 exec_macro(macro, obj, args)
842 rescue => e
848 rescue => e
843 "<div class=\"flash error\">Error executing the <strong>#{macro}</strong> macro (#{e})</div>"
849 "<div class=\"flash error\">Error executing the <strong>#{macro}</strong> macro (#{e})</div>"
844 end || all
850 end || all
845 else
851 else
846 all
852 all
847 end
853 end
848 end
854 end
849 end
855 end
850
856
851 TOC_RE = /<p>\{\{([<>]?)toc\}\}<\/p>/i unless const_defined?(:TOC_RE)
857 TOC_RE = /<p>\{\{([<>]?)toc\}\}<\/p>/i unless const_defined?(:TOC_RE)
852
858
853 # Renders the TOC with given headings
859 # Renders the TOC with given headings
854 def replace_toc(text, headings)
860 def replace_toc(text, headings)
855 text.gsub!(TOC_RE) do
861 text.gsub!(TOC_RE) do
856 if headings.empty?
862 if headings.empty?
857 ''
863 ''
858 else
864 else
859 div_class = 'toc'
865 div_class = 'toc'
860 div_class << ' right' if $1 == '>'
866 div_class << ' right' if $1 == '>'
861 div_class << ' left' if $1 == '<'
867 div_class << ' left' if $1 == '<'
862 out = "<ul class=\"#{div_class}\"><li>"
868 out = "<ul class=\"#{div_class}\"><li>"
863 root = headings.map(&:first).min
869 root = headings.map(&:first).min
864 current = root
870 current = root
865 started = false
871 started = false
866 headings.each do |level, anchor, item|
872 headings.each do |level, anchor, item|
867 if level > current
873 if level > current
868 out << '<ul><li>' * (level - current)
874 out << '<ul><li>' * (level - current)
869 elsif level < current
875 elsif level < current
870 out << "</li></ul>\n" * (current - level) + "</li><li>"
876 out << "</li></ul>\n" * (current - level) + "</li><li>"
871 elsif started
877 elsif started
872 out << '</li><li>'
878 out << '</li><li>'
873 end
879 end
874 out << "<a href=\"##{anchor}\">#{item}</a>"
880 out << "<a href=\"##{anchor}\">#{item}</a>"
875 current = level
881 current = level
876 started = true
882 started = true
877 end
883 end
878 out << '</li></ul>' * (current - root)
884 out << '</li></ul>' * (current - root)
879 out << '</li></ul>'
885 out << '</li></ul>'
880 end
886 end
881 end
887 end
882 end
888 end
883
889
884 # Same as Rails' simple_format helper without using paragraphs
890 # Same as Rails' simple_format helper without using paragraphs
885 def simple_format_without_paragraph(text)
891 def simple_format_without_paragraph(text)
886 text.to_s.
892 text.to_s.
887 gsub(/\r\n?/, "\n"). # \r\n and \r -> \n
893 gsub(/\r\n?/, "\n"). # \r\n and \r -> \n
888 gsub(/\n\n+/, "<br /><br />"). # 2+ newline -> 2 br
894 gsub(/\n\n+/, "<br /><br />"). # 2+ newline -> 2 br
889 gsub(/([^\n]\n)(?=[^\n])/, '\1<br />'). # 1 newline -> br
895 gsub(/([^\n]\n)(?=[^\n])/, '\1<br />'). # 1 newline -> br
890 html_safe
896 html_safe
891 end
897 end
892
898
893 def lang_options_for_select(blank=true)
899 def lang_options_for_select(blank=true)
894 (blank ? [["(auto)", ""]] : []) +
900 (blank ? [["(auto)", ""]] : []) +
895 valid_languages.collect{|lang| [ ll(lang.to_s, :general_lang_name), lang.to_s]}.sort{|x,y| x.last <=> y.last }
901 valid_languages.collect{|lang| [ ll(lang.to_s, :general_lang_name), lang.to_s]}.sort{|x,y| x.last <=> y.last }
896 end
902 end
897
903
898 def label_tag_for(name, option_tags = nil, options = {})
904 def label_tag_for(name, option_tags = nil, options = {})
899 label_text = l(("field_"+field.to_s.gsub(/\_id$/, "")).to_sym) + (options.delete(:required) ? @template.content_tag("span", " *", :class => "required"): "")
905 label_text = l(("field_"+field.to_s.gsub(/\_id$/, "")).to_sym) + (options.delete(:required) ? @template.content_tag("span", " *", :class => "required"): "")
900 content_tag("label", label_text)
906 content_tag("label", label_text)
901 end
907 end
902
908
903 def labelled_tabular_form_for(*args, &proc)
909 def labelled_tabular_form_for(*args, &proc)
904 ActiveSupport::Deprecation.warn "ApplicationHelper#labelled_tabular_form_for is deprecated and will be removed in Redmine 1.5. Use #labelled_form_for instead."
910 ActiveSupport::Deprecation.warn "ApplicationHelper#labelled_tabular_form_for is deprecated and will be removed in Redmine 1.5. Use #labelled_form_for instead."
905 args << {} unless args.last.is_a?(Hash)
911 args << {} unless args.last.is_a?(Hash)
906 options = args.last
912 options = args.last
907 options[:html] ||= {}
913 options[:html] ||= {}
908 options[:html][:class] = 'tabular' unless options[:html].has_key?(:class)
914 options[:html][:class] = 'tabular' unless options[:html].has_key?(:class)
909 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
915 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
910 form_for(*args, &proc)
916 form_for(*args, &proc)
911 end
917 end
912
918
913 def labelled_form_for(*args, &proc)
919 def labelled_form_for(*args, &proc)
914 args << {} unless args.last.is_a?(Hash)
920 args << {} unless args.last.is_a?(Hash)
915 options = args.last
921 options = args.last
916 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
922 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
917 form_for(*args, &proc)
923 form_for(*args, &proc)
918 end
924 end
919
925
920 def labelled_fields_for(*args, &proc)
926 def labelled_fields_for(*args, &proc)
921 args << {} unless args.last.is_a?(Hash)
927 args << {} unless args.last.is_a?(Hash)
922 options = args.last
928 options = args.last
923 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
929 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
924 fields_for(*args, &proc)
930 fields_for(*args, &proc)
925 end
931 end
926
932
927 def labelled_remote_form_for(*args, &proc)
933 def labelled_remote_form_for(*args, &proc)
928 args << {} unless args.last.is_a?(Hash)
934 args << {} unless args.last.is_a?(Hash)
929 options = args.last
935 options = args.last
930 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
936 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
931 remote_form_for(*args, &proc)
937 remote_form_for(*args, &proc)
932 end
938 end
933
939
934 def back_url_hidden_field_tag
940 def back_url_hidden_field_tag
935 back_url = params[:back_url] || request.env['HTTP_REFERER']
941 back_url = params[:back_url] || request.env['HTTP_REFERER']
936 back_url = CGI.unescape(back_url.to_s)
942 back_url = CGI.unescape(back_url.to_s)
937 hidden_field_tag('back_url', CGI.escape(back_url)) unless back_url.blank?
943 hidden_field_tag('back_url', CGI.escape(back_url)) unless back_url.blank?
938 end
944 end
939
945
940 def check_all_links(form_name)
946 def check_all_links(form_name)
941 link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") +
947 link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") +
942 " | ".html_safe +
948 " | ".html_safe +
943 link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)")
949 link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)")
944 end
950 end
945
951
946 def progress_bar(pcts, options={})
952 def progress_bar(pcts, options={})
947 pcts = [pcts, pcts] unless pcts.is_a?(Array)
953 pcts = [pcts, pcts] unless pcts.is_a?(Array)
948 pcts = pcts.collect(&:round)
954 pcts = pcts.collect(&:round)
949 pcts[1] = pcts[1] - pcts[0]
955 pcts[1] = pcts[1] - pcts[0]
950 pcts << (100 - pcts[1] - pcts[0])
956 pcts << (100 - pcts[1] - pcts[0])
951 width = options[:width] || '100px;'
957 width = options[:width] || '100px;'
952 legend = options[:legend] || ''
958 legend = options[:legend] || ''
953 content_tag('table',
959 content_tag('table',
954 content_tag('tr',
960 content_tag('tr',
955 (pcts[0] > 0 ? content_tag('td', '', :style => "width: #{pcts[0]}%;", :class => 'closed') : ''.html_safe) +
961 (pcts[0] > 0 ? content_tag('td', '', :style => "width: #{pcts[0]}%;", :class => 'closed') : ''.html_safe) +
956 (pcts[1] > 0 ? content_tag('td', '', :style => "width: #{pcts[1]}%;", :class => 'done') : ''.html_safe) +
962 (pcts[1] > 0 ? content_tag('td', '', :style => "width: #{pcts[1]}%;", :class => 'done') : ''.html_safe) +
957 (pcts[2] > 0 ? content_tag('td', '', :style => "width: #{pcts[2]}%;", :class => 'todo') : ''.html_safe)
963 (pcts[2] > 0 ? content_tag('td', '', :style => "width: #{pcts[2]}%;", :class => 'todo') : ''.html_safe)
958 ), :class => 'progress', :style => "width: #{width};").html_safe +
964 ), :class => 'progress', :style => "width: #{width};").html_safe +
959 content_tag('p', legend, :class => 'pourcent').html_safe
965 content_tag('p', legend, :class => 'pourcent').html_safe
960 end
966 end
961
967
962 def checked_image(checked=true)
968 def checked_image(checked=true)
963 if checked
969 if checked
964 image_tag 'toggle_check.png'
970 image_tag 'toggle_check.png'
965 end
971 end
966 end
972 end
967
973
968 def context_menu(url)
974 def context_menu(url)
969 unless @context_menu_included
975 unless @context_menu_included
970 content_for :header_tags do
976 content_for :header_tags do
971 javascript_include_tag('context_menu') +
977 javascript_include_tag('context_menu') +
972 stylesheet_link_tag('context_menu')
978 stylesheet_link_tag('context_menu')
973 end
979 end
974 if l(:direction) == 'rtl'
980 if l(:direction) == 'rtl'
975 content_for :header_tags do
981 content_for :header_tags do
976 stylesheet_link_tag('context_menu_rtl')
982 stylesheet_link_tag('context_menu_rtl')
977 end
983 end
978 end
984 end
979 @context_menu_included = true
985 @context_menu_included = true
980 end
986 end
981 javascript_tag "new ContextMenu('#{ url_for(url) }')"
987 javascript_tag "new ContextMenu('#{ url_for(url) }')"
982 end
988 end
983
989
984 def calendar_for(field_id)
990 def calendar_for(field_id)
985 include_calendar_headers_tags
991 include_calendar_headers_tags
986 image_tag("calendar.png", {:id => "#{field_id}_trigger",:class => "calendar-trigger"}) +
992 image_tag("calendar.png", {:id => "#{field_id}_trigger",:class => "calendar-trigger"}) +
987 javascript_tag("Calendar.setup({inputField : '#{field_id}', ifFormat : '%Y-%m-%d', button : '#{field_id}_trigger' });")
993 javascript_tag("Calendar.setup({inputField : '#{field_id}', ifFormat : '%Y-%m-%d', button : '#{field_id}_trigger' });")
988 end
994 end
989
995
990 def include_calendar_headers_tags
996 def include_calendar_headers_tags
991 unless @calendar_headers_tags_included
997 unless @calendar_headers_tags_included
992 @calendar_headers_tags_included = true
998 @calendar_headers_tags_included = true
993 content_for :header_tags do
999 content_for :header_tags do
994 start_of_week = case Setting.start_of_week.to_i
1000 start_of_week = case Setting.start_of_week.to_i
995 when 1
1001 when 1
996 'Calendar._FD = 1;' # Monday
1002 'Calendar._FD = 1;' # Monday
997 when 7
1003 when 7
998 'Calendar._FD = 0;' # Sunday
1004 'Calendar._FD = 0;' # Sunday
999 when 6
1005 when 6
1000 'Calendar._FD = 6;' # Saturday
1006 'Calendar._FD = 6;' # Saturday
1001 else
1007 else
1002 '' # use language
1008 '' # use language
1003 end
1009 end
1004
1010
1005 javascript_include_tag('calendar/calendar') +
1011 javascript_include_tag('calendar/calendar') +
1006 javascript_include_tag("calendar/lang/calendar-#{current_language.to_s.downcase}.js") +
1012 javascript_include_tag("calendar/lang/calendar-#{current_language.to_s.downcase}.js") +
1007 javascript_tag(start_of_week) +
1013 javascript_tag(start_of_week) +
1008 javascript_include_tag('calendar/calendar-setup') +
1014 javascript_include_tag('calendar/calendar-setup') +
1009 stylesheet_link_tag('calendar')
1015 stylesheet_link_tag('calendar')
1010 end
1016 end
1011 end
1017 end
1012 end
1018 end
1013
1019
1014 def content_for(name, content = nil, &block)
1020 def content_for(name, content = nil, &block)
1015 @has_content ||= {}
1021 @has_content ||= {}
1016 @has_content[name] = true
1022 @has_content[name] = true
1017 super(name, content, &block)
1023 super(name, content, &block)
1018 end
1024 end
1019
1025
1020 def has_content?(name)
1026 def has_content?(name)
1021 (@has_content && @has_content[name]) || false
1027 (@has_content && @has_content[name]) || false
1022 end
1028 end
1023
1029
1024 def email_delivery_enabled?
1030 def email_delivery_enabled?
1025 !!ActionMailer::Base.perform_deliveries
1031 !!ActionMailer::Base.perform_deliveries
1026 end
1032 end
1027
1033
1028 # Returns the avatar image tag for the given +user+ if avatars are enabled
1034 # Returns the avatar image tag for the given +user+ if avatars are enabled
1029 # +user+ can be a User or a string that will be scanned for an email address (eg. 'joe <joe@foo.bar>')
1035 # +user+ can be a User or a string that will be scanned for an email address (eg. 'joe <joe@foo.bar>')
1030 def avatar(user, options = { })
1036 def avatar(user, options = { })
1031 if Setting.gravatar_enabled?
1037 if Setting.gravatar_enabled?
1032 options.merge!({:ssl => (defined?(request) && request.ssl?), :default => Setting.gravatar_default})
1038 options.merge!({:ssl => (defined?(request) && request.ssl?), :default => Setting.gravatar_default})
1033 email = nil
1039 email = nil
1034 if user.respond_to?(:mail)
1040 if user.respond_to?(:mail)
1035 email = user.mail
1041 email = user.mail
1036 elsif user.to_s =~ %r{<(.+?)>}
1042 elsif user.to_s =~ %r{<(.+?)>}
1037 email = $1
1043 email = $1
1038 end
1044 end
1039 return gravatar(email.to_s.downcase, options) unless email.blank? rescue nil
1045 return gravatar(email.to_s.downcase, options) unless email.blank? rescue nil
1040 else
1046 else
1041 ''
1047 ''
1042 end
1048 end
1043 end
1049 end
1044
1050
1045 def sanitize_anchor_name(anchor)
1051 def sanitize_anchor_name(anchor)
1046 anchor.gsub(%r{[^\w\s\-]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
1052 anchor.gsub(%r{[^\w\s\-]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
1047 end
1053 end
1048
1054
1049 # Returns the javascript tags that are included in the html layout head
1055 # Returns the javascript tags that are included in the html layout head
1050 def javascript_heads
1056 def javascript_heads
1051 tags = javascript_include_tag(:defaults)
1057 tags = javascript_include_tag(:defaults)
1052 unless User.current.pref.warn_on_leaving_unsaved == '0'
1058 unless User.current.pref.warn_on_leaving_unsaved == '0'
1053 tags << "\n".html_safe + javascript_tag("Event.observe(window, 'load', function(){ new WarnLeavingUnsaved('#{escape_javascript( l(:text_warn_on_leaving_unsaved) )}'); });")
1059 tags << "\n".html_safe + javascript_tag("Event.observe(window, 'load', function(){ new WarnLeavingUnsaved('#{escape_javascript( l(:text_warn_on_leaving_unsaved) )}'); });")
1054 end
1060 end
1055 tags
1061 tags
1056 end
1062 end
1057
1063
1058 def favicon
1064 def favicon
1059 "<link rel='shortcut icon' href='#{image_path('/favicon.ico')}' />".html_safe
1065 "<link rel='shortcut icon' href='#{image_path('/favicon.ico')}' />".html_safe
1060 end
1066 end
1061
1067
1062 def robot_exclusion_tag
1068 def robot_exclusion_tag
1063 '<meta name="robots" content="noindex,follow,noarchive" />'.html_safe
1069 '<meta name="robots" content="noindex,follow,noarchive" />'.html_safe
1064 end
1070 end
1065
1071
1066 # Returns true if arg is expected in the API response
1072 # Returns true if arg is expected in the API response
1067 def include_in_api_response?(arg)
1073 def include_in_api_response?(arg)
1068 unless @included_in_api_response
1074 unless @included_in_api_response
1069 param = params[:include]
1075 param = params[:include]
1070 @included_in_api_response = param.is_a?(Array) ? param.collect(&:to_s) : param.to_s.split(',')
1076 @included_in_api_response = param.is_a?(Array) ? param.collect(&:to_s) : param.to_s.split(',')
1071 @included_in_api_response.collect!(&:strip)
1077 @included_in_api_response.collect!(&:strip)
1072 end
1078 end
1073 @included_in_api_response.include?(arg.to_s)
1079 @included_in_api_response.include?(arg.to_s)
1074 end
1080 end
1075
1081
1076 # Returns options or nil if nometa param or X-Redmine-Nometa header
1082 # Returns options or nil if nometa param or X-Redmine-Nometa header
1077 # was set in the request
1083 # was set in the request
1078 def api_meta(options)
1084 def api_meta(options)
1079 if params[:nometa].present? || request.headers['X-Redmine-Nometa']
1085 if params[:nometa].present? || request.headers['X-Redmine-Nometa']
1080 # compatibility mode for activeresource clients that raise
1086 # compatibility mode for activeresource clients that raise
1081 # an error when unserializing an array with attributes
1087 # an error when unserializing an array with attributes
1082 nil
1088 nil
1083 else
1089 else
1084 options
1090 options
1085 end
1091 end
1086 end
1092 end
1087
1093
1088 private
1094 private
1089
1095
1090 def wiki_helper
1096 def wiki_helper
1091 helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
1097 helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
1092 extend helper
1098 extend helper
1093 return self
1099 return self
1094 end
1100 end
1095
1101
1096 def link_to_content_update(text, url_params = {}, html_options = {})
1102 def link_to_content_update(text, url_params = {}, html_options = {})
1097 link_to(text, url_params, html_options)
1103 link_to(text, url_params, html_options)
1098 end
1104 end
1099 end
1105 end
@@ -1,1006 +1,1033
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2011 Jean-Philippe Lang
2 # Copyright (C) 2006-2011 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require File.expand_path('../../../test_helper', __FILE__)
18 require File.expand_path('../../../test_helper', __FILE__)
19
19
20 class ApplicationHelperTest < ActionView::TestCase
20 class ApplicationHelperTest < ActionView::TestCase
21 fixtures :projects, :roles, :enabled_modules, :users,
21 fixtures :projects, :roles, :enabled_modules, :users,
22 :repositories, :changesets,
22 :repositories, :changesets,
23 :trackers, :issue_statuses, :issues, :versions, :documents,
23 :trackers, :issue_statuses, :issues, :versions, :documents,
24 :wikis, :wiki_pages, :wiki_contents,
24 :wikis, :wiki_pages, :wiki_contents,
25 :boards, :messages, :news,
25 :boards, :messages, :news,
26 :attachments, :enumerations
26 :attachments, :enumerations
27
27
28 def setup
28 def setup
29 super
29 super
30 set_tmp_attachments_directory
30 set_tmp_attachments_directory
31 end
31 end
32
32
33 context "#link_to_if_authorized" do
33 context "#link_to_if_authorized" do
34 context "authorized user" do
34 context "authorized user" do
35 should "be tested"
35 should "be tested"
36 end
36 end
37
37
38 context "unauthorized user" do
38 context "unauthorized user" do
39 should "be tested"
39 should "be tested"
40 end
40 end
41
41
42 should "allow using the :controller and :action for the target link" do
42 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/action",
46 response = link_to_if_authorized("By controller/action",
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 end
51 end
52
52
53 def test_auto_links
53 def test_auto_links
54 to_test = {
54 to_test = {
55 'http://foo.bar' => '<a class="external" href="http://foo.bar">http://foo.bar</a>',
55 'http://foo.bar' => '<a class="external" href="http://foo.bar">http://foo.bar</a>',
56 'http://foo.bar/~user' => '<a class="external" href="http://foo.bar/~user">http://foo.bar/~user</a>',
56 'http://foo.bar/~user' => '<a class="external" href="http://foo.bar/~user">http://foo.bar/~user</a>',
57 'http://foo.bar.' => '<a class="external" href="http://foo.bar">http://foo.bar</a>.',
57 'http://foo.bar.' => '<a class="external" href="http://foo.bar">http://foo.bar</a>.',
58 'https://foo.bar.' => '<a class="external" href="https://foo.bar">https://foo.bar</a>.',
58 'https://foo.bar.' => '<a class="external" href="https://foo.bar">https://foo.bar</a>.',
59 'This is a link: http://foo.bar.' => 'This is a link: <a class="external" href="http://foo.bar">http://foo.bar</a>.',
59 'This is a link: http://foo.bar.' => 'This is a link: <a class="external" href="http://foo.bar">http://foo.bar</a>.',
60 'A link (eg. http://foo.bar).' => 'A link (eg. <a class="external" href="http://foo.bar">http://foo.bar</a>).',
60 'A link (eg. http://foo.bar).' => 'A link (eg. <a class="external" href="http://foo.bar">http://foo.bar</a>).',
61 '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>.',
61 '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>.',
62 'http://www.foo.bar/Test_(foobar)' => '<a class="external" href="http://www.foo.bar/Test_(foobar)">http://www.foo.bar/Test_(foobar)</a>',
62 'http://www.foo.bar/Test_(foobar)' => '<a class="external" href="http://www.foo.bar/Test_(foobar)">http://www.foo.bar/Test_(foobar)</a>',
63 '(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>)',
63 '(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>)',
64 '(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>)',
64 '(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>)',
65 '(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>).',
65 '(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>).',
66 '(see "inline link":http://www.foo.bar/Test_(foobar))' => '(see <a href="http://www.foo.bar/Test_(foobar)" class="external">inline link</a>)',
66 '(see "inline link":http://www.foo.bar/Test_(foobar))' => '(see <a href="http://www.foo.bar/Test_(foobar)" class="external">inline link</a>)',
67 '(see "inline link":http://www.foo.bar/Test)' => '(see <a href="http://www.foo.bar/Test" class="external">inline link</a>)',
67 '(see "inline link":http://www.foo.bar/Test)' => '(see <a href="http://www.foo.bar/Test" class="external">inline link</a>)',
68 '(see "inline link":http://www.foo.bar/Test).' => '(see <a href="http://www.foo.bar/Test" class="external">inline link</a>).',
68 '(see "inline link":http://www.foo.bar/Test).' => '(see <a href="http://www.foo.bar/Test" class="external">inline link</a>).',
69 'www.foo.bar' => '<a class="external" href="http://www.foo.bar">www.foo.bar</a>',
69 'www.foo.bar' => '<a class="external" href="http://www.foo.bar">www.foo.bar</a>',
70 '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>',
70 '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>',
71 'http://foo.bar/page#125' => '<a class="external" href="http://foo.bar/page#125">http://foo.bar/page#125</a>',
71 'http://foo.bar/page#125' => '<a class="external" href="http://foo.bar/page#125">http://foo.bar/page#125</a>',
72 'http://foo@www.bar.com' => '<a class="external" href="http://foo@www.bar.com">http://foo@www.bar.com</a>',
72 'http://foo@www.bar.com' => '<a class="external" href="http://foo@www.bar.com">http://foo@www.bar.com</a>',
73 'http://foo:bar@www.bar.com' => '<a class="external" href="http://foo:bar@www.bar.com">http://foo:bar@www.bar.com</a>',
73 'http://foo:bar@www.bar.com' => '<a class="external" href="http://foo:bar@www.bar.com">http://foo:bar@www.bar.com</a>',
74 'ftp://foo.bar' => '<a class="external" href="ftp://foo.bar">ftp://foo.bar</a>',
74 'ftp://foo.bar' => '<a class="external" href="ftp://foo.bar">ftp://foo.bar</a>',
75 'ftps://foo.bar' => '<a class="external" href="ftps://foo.bar">ftps://foo.bar</a>',
75 'ftps://foo.bar' => '<a class="external" href="ftps://foo.bar">ftps://foo.bar</a>',
76 'sftp://foo.bar' => '<a class="external" href="sftp://foo.bar">sftp://foo.bar</a>',
76 'sftp://foo.bar' => '<a class="external" href="sftp://foo.bar">sftp://foo.bar</a>',
77 # two exclamation marks
77 # two exclamation marks
78 '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>',
78 '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>',
79 # escaping
79 # escaping
80 'http://foo"bar' => '<a class="external" href="http://foo&quot;bar">http://foo"bar</a>',
80 'http://foo"bar' => '<a class="external" href="http://foo&quot;bar">http://foo"bar</a>',
81 # wrap in angle brackets
81 # wrap in angle brackets
82 '<http://foo.bar>' => '&lt;<a class="external" href="http://foo.bar">http://foo.bar</a>&gt;'
82 '<http://foo.bar>' => '&lt;<a class="external" href="http://foo.bar">http://foo.bar</a>&gt;'
83 }
83 }
84 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
84 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
85 end
85 end
86
86
87 def test_auto_mailto
87 def test_auto_mailto
88 assert_equal '<p><a class="email" href="mailto:test@foo.bar">test@foo.bar</a></p>',
88 assert_equal '<p><a class="email" href="mailto:test@foo.bar">test@foo.bar</a></p>',
89 textilizable('test@foo.bar')
89 textilizable('test@foo.bar')
90 end
90 end
91
91
92 def test_inline_images
92 def test_inline_images
93 to_test = {
93 to_test = {
94 '!http://foo.bar/image.jpg!' => '<img src="http://foo.bar/image.jpg" alt="" />',
94 '!http://foo.bar/image.jpg!' => '<img src="http://foo.bar/image.jpg" alt="" />',
95 'floating !>http://foo.bar/image.jpg!' => 'floating <div style="float:right"><img src="http://foo.bar/image.jpg" alt="" /></div>',
95 'floating !>http://foo.bar/image.jpg!' => 'floating <div style="float:right"><img src="http://foo.bar/image.jpg" alt="" /></div>',
96 'with class !(some-class)http://foo.bar/image.jpg!' => 'with class <img src="http://foo.bar/image.jpg" class="some-class" alt="" />',
96 'with class !(some-class)http://foo.bar/image.jpg!' => 'with class <img src="http://foo.bar/image.jpg" class="some-class" alt="" />',
97 '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="" />',
97 '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="" />',
98 '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" />',
98 '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" />',
99 '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;" />',
99 '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;" />',
100 }
100 }
101 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
101 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
102 end
102 end
103
103
104 def test_inline_images_inside_tags
104 def test_inline_images_inside_tags
105 raw = <<-RAW
105 raw = <<-RAW
106 h1. !foo.png! Heading
106 h1. !foo.png! Heading
107
107
108 Centered image:
108 Centered image:
109
109
110 p=. !bar.gif!
110 p=. !bar.gif!
111 RAW
111 RAW
112
112
113 assert textilizable(raw).include?('<img src="foo.png" alt="" />')
113 assert textilizable(raw).include?('<img src="foo.png" alt="" />')
114 assert textilizable(raw).include?('<img src="bar.gif" alt="" />')
114 assert textilizable(raw).include?('<img src="bar.gif" alt="" />')
115 end
115 end
116
116
117 def test_attached_images
117 def test_attached_images
118 to_test = {
118 to_test = {
119 'Inline image: !logo.gif!' => 'Inline image: <img src="/attachments/download/3" title="This is a logo" alt="This is a logo" />',
119 'Inline image: !logo.gif!' => 'Inline image: <img src="/attachments/download/3" title="This is a logo" alt="This is a logo" />',
120 'Inline image: !logo.GIF!' => 'Inline image: <img src="/attachments/download/3" title="This is a logo" alt="This is a logo" />',
120 'Inline image: !logo.GIF!' => 'Inline image: <img src="/attachments/download/3" title="This is a logo" alt="This is a logo" />',
121 'No match: !ogo.gif!' => 'No match: <img src="ogo.gif" alt="" />',
121 'No match: !ogo.gif!' => 'No match: <img src="ogo.gif" alt="" />',
122 'No match: !ogo.GIF!' => 'No match: <img src="ogo.GIF" alt="" />',
122 'No match: !ogo.GIF!' => 'No match: <img src="ogo.GIF" alt="" />',
123 # link image
123 # link image
124 '!logo.gif!:http://foo.bar/' => '<a href="http://foo.bar/"><img src="/attachments/download/3" title="This is a logo" alt="This is a logo" /></a>',
124 '!logo.gif!:http://foo.bar/' => '<a href="http://foo.bar/"><img src="/attachments/download/3" title="This is a logo" alt="This is a logo" /></a>',
125 }
125 }
126 attachments = Attachment.find(:all)
126 attachments = Attachment.find(:all)
127 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text, :attachments => attachments) }
127 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text, :attachments => attachments) }
128 end
128 end
129
129
130 def test_attached_images_filename_extension
130 def test_attached_images_filename_extension
131 set_tmp_attachments_directory
131 set_tmp_attachments_directory
132 a1 = Attachment.new(
132 a1 = Attachment.new(
133 :container => Issue.find(1),
133 :container => Issue.find(1),
134 :file => mock_file_with_options({:original_filename => "testtest.JPG"}),
134 :file => mock_file_with_options({:original_filename => "testtest.JPG"}),
135 :author => User.find(1))
135 :author => User.find(1))
136 assert a1.save
136 assert a1.save
137 assert_equal "testtest.JPG", a1.filename
137 assert_equal "testtest.JPG", a1.filename
138 assert_equal "image/jpeg", a1.content_type
138 assert_equal "image/jpeg", a1.content_type
139 assert a1.image?
139 assert a1.image?
140
140
141 a2 = Attachment.new(
141 a2 = Attachment.new(
142 :container => Issue.find(1),
142 :container => Issue.find(1),
143 :file => mock_file_with_options({:original_filename => "testtest.jpeg"}),
143 :file => mock_file_with_options({:original_filename => "testtest.jpeg"}),
144 :author => User.find(1))
144 :author => User.find(1))
145 assert a2.save
145 assert a2.save
146 assert_equal "testtest.jpeg", a2.filename
146 assert_equal "testtest.jpeg", a2.filename
147 assert_equal "image/jpeg", a2.content_type
147 assert_equal "image/jpeg", a2.content_type
148 assert a2.image?
148 assert a2.image?
149
149
150 a3 = Attachment.new(
150 a3 = Attachment.new(
151 :container => Issue.find(1),
151 :container => Issue.find(1),
152 :file => mock_file_with_options({:original_filename => "testtest.JPE"}),
152 :file => mock_file_with_options({:original_filename => "testtest.JPE"}),
153 :author => User.find(1))
153 :author => User.find(1))
154 assert a3.save
154 assert a3.save
155 assert_equal "testtest.JPE", a3.filename
155 assert_equal "testtest.JPE", a3.filename
156 assert_equal "image/jpeg", a3.content_type
156 assert_equal "image/jpeg", a3.content_type
157 assert a3.image?
157 assert a3.image?
158
158
159 a4 = Attachment.new(
159 a4 = Attachment.new(
160 :container => Issue.find(1),
160 :container => Issue.find(1),
161 :file => mock_file_with_options({:original_filename => "Testtest.BMP"}),
161 :file => mock_file_with_options({:original_filename => "Testtest.BMP"}),
162 :author => User.find(1))
162 :author => User.find(1))
163 assert a4.save
163 assert a4.save
164 assert_equal "Testtest.BMP", a4.filename
164 assert_equal "Testtest.BMP", a4.filename
165 assert_equal "image/x-ms-bmp", a4.content_type
165 assert_equal "image/x-ms-bmp", a4.content_type
166 assert a4.image?
166 assert a4.image?
167
167
168 to_test = {
168 to_test = {
169 'Inline image: !testtest.jpg!' =>
169 'Inline image: !testtest.jpg!' =>
170 'Inline image: <img src="/attachments/download/' + a1.id.to_s + '" alt="" />',
170 'Inline image: <img src="/attachments/download/' + a1.id.to_s + '" alt="" />',
171 'Inline image: !testtest.jpeg!' =>
171 'Inline image: !testtest.jpeg!' =>
172 'Inline image: <img src="/attachments/download/' + a2.id.to_s + '" alt="" />',
172 'Inline image: <img src="/attachments/download/' + a2.id.to_s + '" alt="" />',
173 'Inline image: !testtest.jpe!' =>
173 'Inline image: !testtest.jpe!' =>
174 'Inline image: <img src="/attachments/download/' + a3.id.to_s + '" alt="" />',
174 'Inline image: <img src="/attachments/download/' + a3.id.to_s + '" alt="" />',
175 'Inline image: !testtest.bmp!' =>
175 'Inline image: !testtest.bmp!' =>
176 'Inline image: <img src="/attachments/download/' + a4.id.to_s + '" alt="" />',
176 'Inline image: <img src="/attachments/download/' + a4.id.to_s + '" alt="" />',
177 }
177 }
178
178
179 attachments = [a1, a2, a3, a4]
179 attachments = [a1, a2, a3, a4]
180 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text, :attachments => attachments) }
180 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text, :attachments => attachments) }
181 end
181 end
182
182
183 def test_attached_images_should_read_later
183 def test_attached_images_should_read_later
184 set_fixtures_attachments_directory
184 set_fixtures_attachments_directory
185 a1 = Attachment.find(16)
185 a1 = Attachment.find(16)
186 assert_equal "testfile.png", a1.filename
186 assert_equal "testfile.png", a1.filename
187 assert a1.readable?
187 assert a1.readable?
188 assert (! a1.visible?(User.anonymous))
188 assert (! a1.visible?(User.anonymous))
189 assert a1.visible?(User.find(2))
189 assert a1.visible?(User.find(2))
190 a2 = Attachment.find(17)
190 a2 = Attachment.find(17)
191 assert_equal "testfile.PNG", a2.filename
191 assert_equal "testfile.PNG", a2.filename
192 assert a2.readable?
192 assert a2.readable?
193 assert (! a2.visible?(User.anonymous))
193 assert (! a2.visible?(User.anonymous))
194 assert a2.visible?(User.find(2))
194 assert a2.visible?(User.find(2))
195 assert a1.created_on < a2.created_on
195 assert a1.created_on < a2.created_on
196
196
197 to_test = {
197 to_test = {
198 'Inline image: !testfile.png!' =>
198 'Inline image: !testfile.png!' =>
199 'Inline image: <img src="/attachments/download/' + a2.id.to_s + '" alt="" />',
199 'Inline image: <img src="/attachments/download/' + a2.id.to_s + '" alt="" />',
200 'Inline image: !Testfile.PNG!' =>
200 'Inline image: !Testfile.PNG!' =>
201 'Inline image: <img src="/attachments/download/' + a2.id.to_s + '" alt="" />',
201 'Inline image: <img src="/attachments/download/' + a2.id.to_s + '" alt="" />',
202 }
202 }
203 attachments = [a1, a2]
203 attachments = [a1, a2]
204 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text, :attachments => attachments) }
204 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text, :attachments => attachments) }
205 set_tmp_attachments_directory
205 set_tmp_attachments_directory
206 end
206 end
207
207
208 def test_textile_external_links
208 def test_textile_external_links
209 to_test = {
209 to_test = {
210 'This is a "link":http://foo.bar' => 'This is a <a href="http://foo.bar" class="external">link</a>',
210 'This is a "link":http://foo.bar' => 'This is a <a href="http://foo.bar" class="external">link</a>',
211 'This is an intern "link":/foo/bar' => 'This is an intern <a href="/foo/bar">link</a>',
211 'This is an intern "link":/foo/bar' => 'This is an intern <a href="/foo/bar">link</a>',
212 '"link (Link title)":http://foo.bar' => '<a href="http://foo.bar" title="Link title" class="external">link</a>',
212 '"link (Link title)":http://foo.bar' => '<a href="http://foo.bar" title="Link title" class="external">link</a>',
213 '"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>',
213 '"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>',
214 "This is not a \"Link\":\n\nAnother paragraph" => "This is not a \"Link\":</p>\n\n\n\t<p>Another paragraph",
214 "This is not a \"Link\":\n\nAnother paragraph" => "This is not a \"Link\":</p>\n\n\n\t<p>Another paragraph",
215 # no multiline link text
215 # no multiline link text
216 "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",
216 "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",
217 # mailto link
217 # mailto link
218 "\"system administrator\":mailto:sysadmin@example.com?subject=redmine%20permissions" => "<a href=\"mailto:sysadmin@example.com?subject=redmine%20permissions\">system administrator</a>",
218 "\"system administrator\":mailto:sysadmin@example.com?subject=redmine%20permissions" => "<a href=\"mailto:sysadmin@example.com?subject=redmine%20permissions\">system administrator</a>",
219 # two exclamation marks
219 # two exclamation marks
220 '"a link":http://example.net/path!602815048C7B5C20!302.html' => '<a href="http://example.net/path!602815048C7B5C20!302.html" class="external">a link</a>',
220 '"a link":http://example.net/path!602815048C7B5C20!302.html' => '<a href="http://example.net/path!602815048C7B5C20!302.html" class="external">a link</a>',
221 # escaping
221 # escaping
222 '"test":http://foo"bar' => '<a href="http://foo&quot;bar" class="external">test</a>',
222 '"test":http://foo"bar' => '<a href="http://foo&quot;bar" class="external">test</a>',
223 }
223 }
224 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
224 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
225 end
225 end
226
226
227 def test_redmine_links
227 def test_redmine_links
228 issue_link = link_to('#3', {:controller => 'issues', :action => 'show', :id => 3},
228 issue_link = link_to('#3', {:controller => 'issues', :action => 'show', :id => 3},
229 :class => 'issue status-1 priority-1 overdue', :title => 'Error 281 when updating a recipe (New)')
229 :class => 'issue status-1 priority-1 overdue', :title => 'Error 281 when updating a recipe (New)')
230
230
231 changeset_link = link_to('r1', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :rev => 1},
231 changeset_link = link_to('r1', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :rev => 1},
232 :class => 'changeset', :title => 'My very first commit')
232 :class => 'changeset', :title => 'My very first commit')
233 changeset_link2 = link_to('r2', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :rev => 2},
233 changeset_link2 = link_to('r2', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :rev => 2},
234 :class => 'changeset', :title => 'This commit fixes #1, #2 and references #1 & #3')
234 :class => 'changeset', :title => 'This commit fixes #1, #2 and references #1 & #3')
235
235
236 document_link = link_to('Test document', {:controller => 'documents', :action => 'show', :id => 1},
236 document_link = link_to('Test document', {:controller => 'documents', :action => 'show', :id => 1},
237 :class => 'document')
237 :class => 'document')
238
238
239 version_link = link_to('1.0', {:controller => 'versions', :action => 'show', :id => 2},
239 version_link = link_to('1.0', {:controller => 'versions', :action => 'show', :id => 2},
240 :class => 'version')
240 :class => 'version')
241
241
242 board_url = {:controller => 'boards', :action => 'show', :id => 2, :project_id => 'ecookbook'}
242 board_url = {:controller => 'boards', :action => 'show', :id => 2, :project_id => 'ecookbook'}
243
243
244 message_url = {:controller => 'messages', :action => 'show', :board_id => 1, :id => 4}
244 message_url = {:controller => 'messages', :action => 'show', :board_id => 1, :id => 4}
245
245
246 news_url = {:controller => 'news', :action => 'show', :id => 1}
246 news_url = {:controller => 'news', :action => 'show', :id => 1}
247
247
248 project_url = {:controller => 'projects', :action => 'show', :id => 'subproject1'}
248 project_url = {:controller => 'projects', :action => 'show', :id => 'subproject1'}
249
249
250 source_url = {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :path => ['some', 'file']}
250 source_url = {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :path => ['some', 'file']}
251 source_url_with_ext = {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :path => ['some', 'file.ext']}
251 source_url_with_ext = {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :path => ['some', 'file.ext']}
252
252
253 to_test = {
253 to_test = {
254 # tickets
254 # tickets
255 '#3, [#3], (#3) and #3.' => "#{issue_link}, [#{issue_link}], (#{issue_link}) and #{issue_link}.",
255 '#3, [#3], (#3) and #3.' => "#{issue_link}, [#{issue_link}], (#{issue_link}) and #{issue_link}.",
256 # changesets
256 # changesets
257 'r1' => changeset_link,
257 'r1' => changeset_link,
258 'r1.' => "#{changeset_link}.",
258 'r1.' => "#{changeset_link}.",
259 'r1, r2' => "#{changeset_link}, #{changeset_link2}",
259 'r1, r2' => "#{changeset_link}, #{changeset_link2}",
260 'r1,r2' => "#{changeset_link},#{changeset_link2}",
260 'r1,r2' => "#{changeset_link},#{changeset_link2}",
261 # documents
261 # documents
262 'document#1' => document_link,
262 'document#1' => document_link,
263 'document:"Test document"' => document_link,
263 'document:"Test document"' => document_link,
264 # versions
264 # versions
265 'version#2' => version_link,
265 'version#2' => version_link,
266 'version:1.0' => version_link,
266 'version:1.0' => version_link,
267 'version:"1.0"' => version_link,
267 'version:"1.0"' => version_link,
268 # source
268 # source
269 'source:some/file' => link_to('source:some/file', source_url, :class => 'source'),
269 'source:some/file' => link_to('source:some/file', source_url, :class => 'source'),
270 'source:/some/file' => link_to('source:/some/file', source_url, :class => 'source'),
270 'source:/some/file' => link_to('source:/some/file', source_url, :class => 'source'),
271 'source:/some/file.' => link_to('source:/some/file', source_url, :class => 'source') + ".",
271 'source:/some/file.' => link_to('source:/some/file', source_url, :class => 'source') + ".",
272 'source:/some/file.ext.' => link_to('source:/some/file.ext', source_url_with_ext, :class => 'source') + ".",
272 'source:/some/file.ext.' => link_to('source:/some/file.ext', source_url_with_ext, :class => 'source') + ".",
273 'source:/some/file. ' => link_to('source:/some/file', source_url, :class => 'source') + ".",
273 'source:/some/file. ' => link_to('source:/some/file', source_url, :class => 'source') + ".",
274 'source:/some/file.ext. ' => link_to('source:/some/file.ext', source_url_with_ext, :class => 'source') + ".",
274 'source:/some/file.ext. ' => link_to('source:/some/file.ext', source_url_with_ext, :class => 'source') + ".",
275 'source:/some/file, ' => link_to('source:/some/file', source_url, :class => 'source') + ",",
275 'source:/some/file, ' => link_to('source:/some/file', source_url, :class => 'source') + ",",
276 'source:/some/file@52' => link_to('source:/some/file@52', source_url.merge(:rev => 52), :class => 'source'),
276 'source:/some/file@52' => link_to('source:/some/file@52', source_url.merge(:rev => 52), :class => 'source'),
277 'source:/some/file.ext@52' => link_to('source:/some/file.ext@52', source_url_with_ext.merge(:rev => 52), :class => 'source'),
277 'source:/some/file.ext@52' => link_to('source:/some/file.ext@52', source_url_with_ext.merge(:rev => 52), :class => 'source'),
278 'source:/some/file#L110' => link_to('source:/some/file#L110', source_url.merge(:anchor => 'L110'), :class => 'source'),
278 'source:/some/file#L110' => link_to('source:/some/file#L110', source_url.merge(:anchor => 'L110'), :class => 'source'),
279 'source:/some/file.ext#L110' => link_to('source:/some/file.ext#L110', source_url_with_ext.merge(:anchor => 'L110'), :class => 'source'),
279 'source:/some/file.ext#L110' => link_to('source:/some/file.ext#L110', source_url_with_ext.merge(:anchor => 'L110'), :class => 'source'),
280 'source:/some/file@52#L110' => link_to('source:/some/file@52#L110', source_url.merge(:rev => 52, :anchor => 'L110'), :class => 'source'),
280 'source:/some/file@52#L110' => link_to('source:/some/file@52#L110', source_url.merge(:rev => 52, :anchor => 'L110'), :class => 'source'),
281 'export:/some/file' => link_to('export:/some/file', source_url.merge(:format => 'raw'), :class => 'source download'),
281 'export:/some/file' => link_to('export:/some/file', source_url.merge(:format => 'raw'), :class => 'source download'),
282 # forum
282 # forum
283 'forum#2' => link_to('Discussion', board_url, :class => 'board'),
283 'forum#2' => link_to('Discussion', board_url, :class => 'board'),
284 'forum:Discussion' => link_to('Discussion', board_url, :class => 'board'),
284 'forum:Discussion' => link_to('Discussion', board_url, :class => 'board'),
285 # message
285 # message
286 'message#4' => link_to('Post 2', message_url, :class => 'message'),
286 'message#4' => link_to('Post 2', message_url, :class => 'message'),
287 'message#5' => link_to('RE: post 2', message_url.merge(:anchor => 'message-5', :r => 5), :class => 'message'),
287 'message#5' => link_to('RE: post 2', message_url.merge(:anchor => 'message-5', :r => 5), :class => 'message'),
288 # news
288 # news
289 'news#1' => link_to('eCookbook first release !', news_url, :class => 'news'),
289 'news#1' => link_to('eCookbook first release !', news_url, :class => 'news'),
290 'news:"eCookbook first release !"' => link_to('eCookbook first release !', news_url, :class => 'news'),
290 'news:"eCookbook first release !"' => link_to('eCookbook first release !', news_url, :class => 'news'),
291 # project
291 # project
292 'project#3' => link_to('eCookbook Subproject 1', project_url, :class => 'project'),
292 'project#3' => link_to('eCookbook Subproject 1', project_url, :class => 'project'),
293 'project:subproject1' => link_to('eCookbook Subproject 1', project_url, :class => 'project'),
293 'project:subproject1' => link_to('eCookbook Subproject 1', project_url, :class => 'project'),
294 'project:"eCookbook subProject 1"' => link_to('eCookbook Subproject 1', project_url, :class => 'project'),
294 'project:"eCookbook subProject 1"' => link_to('eCookbook Subproject 1', project_url, :class => 'project'),
295 # escaping
295 # escaping
296 '!#3.' => '#3.',
296 '!#3.' => '#3.',
297 '!r1' => 'r1',
297 '!r1' => 'r1',
298 '!document#1' => 'document#1',
298 '!document#1' => 'document#1',
299 '!document:"Test document"' => 'document:"Test document"',
299 '!document:"Test document"' => 'document:"Test document"',
300 '!version#2' => 'version#2',
300 '!version#2' => 'version#2',
301 '!version:1.0' => 'version:1.0',
301 '!version:1.0' => 'version:1.0',
302 '!version:"1.0"' => 'version:"1.0"',
302 '!version:"1.0"' => 'version:"1.0"',
303 '!source:/some/file' => 'source:/some/file',
303 '!source:/some/file' => 'source:/some/file',
304 # not found
304 # not found
305 '#0123456789' => '#0123456789',
305 '#0123456789' => '#0123456789',
306 # invalid expressions
306 # invalid expressions
307 'source:' => 'source:',
307 'source:' => 'source:',
308 # url hash
308 # url hash
309 "http://foo.bar/FAQ#3" => '<a class="external" href="http://foo.bar/FAQ#3">http://foo.bar/FAQ#3</a>',
309 "http://foo.bar/FAQ#3" => '<a class="external" href="http://foo.bar/FAQ#3">http://foo.bar/FAQ#3</a>',
310 }
310 }
311 @project = Project.find(1)
311 @project = Project.find(1)
312 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text), "#{text} failed" }
312 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text), "#{text} failed" }
313 end
313 end
314
314
315 def test_cross_project_redmine_links
315 def test_cross_project_redmine_links
316 source_link = link_to('ecookbook:source:/some/file', {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :path => ['some', 'file']},
316 source_link = link_to('ecookbook:source:/some/file', {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :path => ['some', 'file']},
317 :class => 'source')
317 :class => 'source')
318
318
319 changeset_link = link_to('ecookbook:r2', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :rev => 2},
319 changeset_link = link_to('ecookbook:r2', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :rev => 2},
320 :class => 'changeset', :title => 'This commit fixes #1, #2 and references #1 & #3')
320 :class => 'changeset', :title => 'This commit fixes #1, #2 and references #1 & #3')
321
321
322 to_test = {
322 to_test = {
323 # documents
323 # documents
324 'document:"Test document"' => 'document:"Test document"',
324 'document:"Test document"' => 'document:"Test document"',
325 'ecookbook:document:"Test document"' => '<a href="/documents/1" class="document">Test document</a>',
325 'ecookbook:document:"Test document"' => '<a href="/documents/1" class="document">Test document</a>',
326 'invalid:document:"Test document"' => 'invalid:document:"Test document"',
326 'invalid:document:"Test document"' => 'invalid:document:"Test document"',
327 # versions
327 # versions
328 'version:"1.0"' => 'version:"1.0"',
328 'version:"1.0"' => 'version:"1.0"',
329 'ecookbook:version:"1.0"' => '<a href="/versions/2" class="version">1.0</a>',
329 'ecookbook:version:"1.0"' => '<a href="/versions/2" class="version">1.0</a>',
330 'invalid:version:"1.0"' => 'invalid:version:"1.0"',
330 'invalid:version:"1.0"' => 'invalid:version:"1.0"',
331 # changeset
331 # changeset
332 'r2' => 'r2',
332 'r2' => 'r2',
333 'ecookbook:r2' => changeset_link,
333 'ecookbook:r2' => changeset_link,
334 'invalid:r2' => 'invalid:r2',
334 'invalid:r2' => 'invalid:r2',
335 # source
335 # source
336 'source:/some/file' => 'source:/some/file',
336 'source:/some/file' => 'source:/some/file',
337 'ecookbook:source:/some/file' => source_link,
337 'ecookbook:source:/some/file' => source_link,
338 'invalid:source:/some/file' => 'invalid:source:/some/file',
338 'invalid:source:/some/file' => 'invalid:source:/some/file',
339 }
339 }
340 @project = Project.find(3)
340 @project = Project.find(3)
341 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text), "#{text} failed" }
341 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text), "#{text} failed" }
342 end
342 end
343
343
344 def test_multiple_repositories_redmine_links
344 def test_multiple_repositories_redmine_links
345 svn = Repository::Subversion.create!(:project_id => 1, :identifier => 'svn1', :url => 'file:///foo/hg')
345 svn = Repository::Subversion.create!(:project_id => 1, :identifier => 'svn1', :url => 'file:///foo/hg')
346 Changeset.create!(:repository => svn, :committed_on => Time.now, :revision => '123')
346 Changeset.create!(:repository => svn, :committed_on => Time.now, :revision => '123')
347 hg = Repository::Mercurial.create!(:project_id => 1, :identifier => 'hg1', :url => '/foo/hg')
347 hg = Repository::Mercurial.create!(:project_id => 1, :identifier => 'hg1', :url => '/foo/hg')
348 Changeset.create!(:repository => hg, :committed_on => Time.now, :revision => '123', :scmid => 'abcd')
348 Changeset.create!(:repository => hg, :committed_on => Time.now, :revision => '123', :scmid => 'abcd')
349
349
350 changeset_link = link_to('r2', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :rev => 2},
350 changeset_link = link_to('r2', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :rev => 2},
351 :class => 'changeset', :title => 'This commit fixes #1, #2 and references #1 & #3')
351 :class => 'changeset', :title => 'This commit fixes #1, #2 and references #1 & #3')
352 svn_changeset_link = link_to('svn1|r123', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :repository_id => 'svn1', :rev => 123},
352 svn_changeset_link = link_to('svn1|r123', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :repository_id => 'svn1', :rev => 123},
353 :class => 'changeset', :title => '')
353 :class => 'changeset', :title => '')
354 hg_changeset_link = link_to('hg1|abcd', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :repository_id => 'hg1', :rev => 'abcd'},
354 hg_changeset_link = link_to('hg1|abcd', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :repository_id => 'hg1', :rev => 'abcd'},
355 :class => 'changeset', :title => '')
355 :class => 'changeset', :title => '')
356
356
357 source_link = link_to('source:some/file', {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :path => ['some', 'file']}, :class => 'source')
357 source_link = link_to('source:some/file', {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :path => ['some', 'file']}, :class => 'source')
358 hg_source_link = link_to('source:hg1|some/file', {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :repository_id => 'hg1', :path => ['some', 'file']}, :class => 'source')
358 hg_source_link = link_to('source:hg1|some/file', {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :repository_id => 'hg1', :path => ['some', 'file']}, :class => 'source')
359
359
360 to_test = {
360 to_test = {
361 'r2' => changeset_link,
361 'r2' => changeset_link,
362 'svn1|r123' => svn_changeset_link,
362 'svn1|r123' => svn_changeset_link,
363 'invalid|r123' => 'invalid|r123',
363 'invalid|r123' => 'invalid|r123',
364 'commit:hg1|abcd' => hg_changeset_link,
364 'commit:hg1|abcd' => hg_changeset_link,
365 'commit:invalid|abcd' => 'commit:invalid|abcd',
365 'commit:invalid|abcd' => 'commit:invalid|abcd',
366 # source
366 # source
367 'source:some/file' => source_link,
367 'source:some/file' => source_link,
368 'source:hg1|some/file' => hg_source_link,
368 'source:hg1|some/file' => hg_source_link,
369 'source:invalid|some/file' => 'source:invalid|some/file',
369 'source:invalid|some/file' => 'source:invalid|some/file',
370 }
370 }
371
371
372 @project = Project.find(1)
372 @project = Project.find(1)
373 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text), "#{text} failed" }
373 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text), "#{text} failed" }
374 end
374 end
375
375
376 def test_cross_project_multiple_repositories_redmine_links
376 def test_cross_project_multiple_repositories_redmine_links
377 svn = Repository::Subversion.create!(:project_id => 1, :identifier => 'svn1', :url => 'file:///foo/hg')
377 svn = Repository::Subversion.create!(:project_id => 1, :identifier => 'svn1', :url => 'file:///foo/hg')
378 Changeset.create!(:repository => svn, :committed_on => Time.now, :revision => '123')
378 Changeset.create!(:repository => svn, :committed_on => Time.now, :revision => '123')
379 hg = Repository::Mercurial.create!(:project_id => 1, :identifier => 'hg1', :url => '/foo/hg')
379 hg = Repository::Mercurial.create!(:project_id => 1, :identifier => 'hg1', :url => '/foo/hg')
380 Changeset.create!(:repository => hg, :committed_on => Time.now, :revision => '123', :scmid => 'abcd')
380 Changeset.create!(:repository => hg, :committed_on => Time.now, :revision => '123', :scmid => 'abcd')
381
381
382 changeset_link = link_to('ecookbook:r2', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :rev => 2},
382 changeset_link = link_to('ecookbook:r2', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :rev => 2},
383 :class => 'changeset', :title => 'This commit fixes #1, #2 and references #1 & #3')
383 :class => 'changeset', :title => 'This commit fixes #1, #2 and references #1 & #3')
384 svn_changeset_link = link_to('ecookbook:svn1|r123', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :repository_id => 'svn1', :rev => 123},
384 svn_changeset_link = link_to('ecookbook:svn1|r123', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :repository_id => 'svn1', :rev => 123},
385 :class => 'changeset', :title => '')
385 :class => 'changeset', :title => '')
386 hg_changeset_link = link_to('ecookbook:hg1|abcd', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :repository_id => 'hg1', :rev => 'abcd'},
386 hg_changeset_link = link_to('ecookbook:hg1|abcd', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :repository_id => 'hg1', :rev => 'abcd'},
387 :class => 'changeset', :title => '')
387 :class => 'changeset', :title => '')
388
388
389 source_link = link_to('ecookbook:source:some/file', {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :path => ['some', 'file']}, :class => 'source')
389 source_link = link_to('ecookbook:source:some/file', {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :path => ['some', 'file']}, :class => 'source')
390 hg_source_link = link_to('ecookbook:source:hg1|some/file', {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :repository_id => 'hg1', :path => ['some', 'file']}, :class => 'source')
390 hg_source_link = link_to('ecookbook:source:hg1|some/file', {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :repository_id => 'hg1', :path => ['some', 'file']}, :class => 'source')
391
391
392 to_test = {
392 to_test = {
393 'ecookbook:r2' => changeset_link,
393 'ecookbook:r2' => changeset_link,
394 'ecookbook:svn1|r123' => svn_changeset_link,
394 'ecookbook:svn1|r123' => svn_changeset_link,
395 'ecookbook:invalid|r123' => 'ecookbook:invalid|r123',
395 'ecookbook:invalid|r123' => 'ecookbook:invalid|r123',
396 'ecookbook:commit:hg1|abcd' => hg_changeset_link,
396 'ecookbook:commit:hg1|abcd' => hg_changeset_link,
397 'ecookbook:commit:invalid|abcd' => 'ecookbook:commit:invalid|abcd',
397 'ecookbook:commit:invalid|abcd' => 'ecookbook:commit:invalid|abcd',
398 'invalid:commit:invalid|abcd' => 'invalid:commit:invalid|abcd',
398 'invalid:commit:invalid|abcd' => 'invalid:commit:invalid|abcd',
399 # source
399 # source
400 'ecookbook:source:some/file' => source_link,
400 'ecookbook:source:some/file' => source_link,
401 'ecookbook:source:hg1|some/file' => hg_source_link,
401 'ecookbook:source:hg1|some/file' => hg_source_link,
402 'ecookbook:source:invalid|some/file' => 'ecookbook:source:invalid|some/file',
402 'ecookbook:source:invalid|some/file' => 'ecookbook:source:invalid|some/file',
403 'invalid:source:invalid|some/file' => 'invalid:source:invalid|some/file',
403 'invalid:source:invalid|some/file' => 'invalid:source:invalid|some/file',
404 }
404 }
405
405
406 @project = Project.find(3)
406 @project = Project.find(3)
407 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text), "#{text} failed" }
407 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text), "#{text} failed" }
408 end
408 end
409
409
410 def test_redmine_links_git_commit
410 def test_redmine_links_git_commit
411 changeset_link = link_to('abcd',
411 changeset_link = link_to('abcd',
412 {
412 {
413 :controller => 'repositories',
413 :controller => 'repositories',
414 :action => 'revision',
414 :action => 'revision',
415 :id => 'subproject1',
415 :id => 'subproject1',
416 :rev => 'abcd',
416 :rev => 'abcd',
417 },
417 },
418 :class => 'changeset', :title => 'test commit')
418 :class => 'changeset', :title => 'test commit')
419 to_test = {
419 to_test = {
420 'commit:abcd' => changeset_link,
420 'commit:abcd' => changeset_link,
421 }
421 }
422 @project = Project.find(3)
422 @project = Project.find(3)
423 r = Repository::Git.create!(:project => @project, :url => '/tmp/test/git')
423 r = Repository::Git.create!(:project => @project, :url => '/tmp/test/git')
424 assert r
424 assert r
425 c = Changeset.new(:repository => r,
425 c = Changeset.new(:repository => r,
426 :committed_on => Time.now,
426 :committed_on => Time.now,
427 :revision => 'abcd',
427 :revision => 'abcd',
428 :scmid => 'abcd',
428 :scmid => 'abcd',
429 :comments => 'test commit')
429 :comments => 'test commit')
430 assert( c.save )
430 assert( c.save )
431 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
431 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
432 end
432 end
433
433
434 # TODO: Bazaar commit id contains mail address, so it contains '@' and '_'.
434 # TODO: Bazaar commit id contains mail address, so it contains '@' and '_'.
435 def test_redmine_links_darcs_commit
435 def test_redmine_links_darcs_commit
436 changeset_link = link_to('20080308225258-98289-abcd456efg.gz',
436 changeset_link = link_to('20080308225258-98289-abcd456efg.gz',
437 {
437 {
438 :controller => 'repositories',
438 :controller => 'repositories',
439 :action => 'revision',
439 :action => 'revision',
440 :id => 'subproject1',
440 :id => 'subproject1',
441 :rev => '123',
441 :rev => '123',
442 },
442 },
443 :class => 'changeset', :title => 'test commit')
443 :class => 'changeset', :title => 'test commit')
444 to_test = {
444 to_test = {
445 'commit:20080308225258-98289-abcd456efg.gz' => changeset_link,
445 'commit:20080308225258-98289-abcd456efg.gz' => changeset_link,
446 }
446 }
447 @project = Project.find(3)
447 @project = Project.find(3)
448 r = Repository::Darcs.create!(
448 r = Repository::Darcs.create!(
449 :project => @project, :url => '/tmp/test/darcs',
449 :project => @project, :url => '/tmp/test/darcs',
450 :log_encoding => 'UTF-8')
450 :log_encoding => 'UTF-8')
451 assert r
451 assert r
452 c = Changeset.new(:repository => r,
452 c = Changeset.new(:repository => r,
453 :committed_on => Time.now,
453 :committed_on => Time.now,
454 :revision => '123',
454 :revision => '123',
455 :scmid => '20080308225258-98289-abcd456efg.gz',
455 :scmid => '20080308225258-98289-abcd456efg.gz',
456 :comments => 'test commit')
456 :comments => 'test commit')
457 assert( c.save )
457 assert( c.save )
458 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
458 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
459 end
459 end
460
460
461 def test_redmine_links_mercurial_commit
461 def test_redmine_links_mercurial_commit
462 changeset_link_rev = link_to('r123',
462 changeset_link_rev = link_to('r123',
463 {
463 {
464 :controller => 'repositories',
464 :controller => 'repositories',
465 :action => 'revision',
465 :action => 'revision',
466 :id => 'subproject1',
466 :id => 'subproject1',
467 :rev => '123' ,
467 :rev => '123' ,
468 },
468 },
469 :class => 'changeset', :title => 'test commit')
469 :class => 'changeset', :title => 'test commit')
470 changeset_link_commit = link_to('abcd',
470 changeset_link_commit = link_to('abcd',
471 {
471 {
472 :controller => 'repositories',
472 :controller => 'repositories',
473 :action => 'revision',
473 :action => 'revision',
474 :id => 'subproject1',
474 :id => 'subproject1',
475 :rev => 'abcd' ,
475 :rev => 'abcd' ,
476 },
476 },
477 :class => 'changeset', :title => 'test commit')
477 :class => 'changeset', :title => 'test commit')
478 to_test = {
478 to_test = {
479 'r123' => changeset_link_rev,
479 'r123' => changeset_link_rev,
480 'commit:abcd' => changeset_link_commit,
480 'commit:abcd' => changeset_link_commit,
481 }
481 }
482 @project = Project.find(3)
482 @project = Project.find(3)
483 r = Repository::Mercurial.create!(:project => @project, :url => '/tmp/test')
483 r = Repository::Mercurial.create!(:project => @project, :url => '/tmp/test')
484 assert r
484 assert r
485 c = Changeset.new(:repository => r,
485 c = Changeset.new(:repository => r,
486 :committed_on => Time.now,
486 :committed_on => Time.now,
487 :revision => '123',
487 :revision => '123',
488 :scmid => 'abcd',
488 :scmid => 'abcd',
489 :comments => 'test commit')
489 :comments => 'test commit')
490 assert( c.save )
490 assert( c.save )
491 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
491 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
492 end
492 end
493
493
494 def test_attachment_links
494 def test_attachment_links
495 attachment_link = link_to('error281.txt', {:controller => 'attachments', :action => 'download', :id => '1'}, :class => 'attachment')
495 attachment_link = link_to('error281.txt', {:controller => 'attachments', :action => 'download', :id => '1'}, :class => 'attachment')
496 to_test = {
496 to_test = {
497 'attachment:error281.txt' => attachment_link
497 'attachment:error281.txt' => attachment_link
498 }
498 }
499 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text, :attachments => Issue.find(3).attachments), "#{text} failed" }
499 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text, :attachments => Issue.find(3).attachments), "#{text} failed" }
500 end
500 end
501
501
502 def test_wiki_links
502 def test_wiki_links
503 to_test = {
503 to_test = {
504 '[[CookBook documentation]]' => '<a href="/projects/ecookbook/wiki/CookBook_documentation" class="wiki-page">CookBook documentation</a>',
504 '[[CookBook documentation]]' => '<a href="/projects/ecookbook/wiki/CookBook_documentation" class="wiki-page">CookBook documentation</a>',
505 '[[Another page|Page]]' => '<a href="/projects/ecookbook/wiki/Another_page" class="wiki-page">Page</a>',
505 '[[Another page|Page]]' => '<a href="/projects/ecookbook/wiki/Another_page" class="wiki-page">Page</a>',
506 # title content should be formatted
506 # title content should be formatted
507 '[[Another page|With _styled_ *title*]]' => '<a href="/projects/ecookbook/wiki/Another_page" class="wiki-page">With <em>styled</em> <strong>title</strong></a>',
507 '[[Another page|With _styled_ *title*]]' => '<a href="/projects/ecookbook/wiki/Another_page" class="wiki-page">With <em>styled</em> <strong>title</strong></a>',
508 '[[Another page|With title containing <strong>HTML entities &amp; markups</strong>]]' => '<a href="/projects/ecookbook/wiki/Another_page" class="wiki-page">With title containing &lt;strong&gt;HTML entities &amp; markups&lt;/strong&gt;</a>',
508 '[[Another page|With title containing <strong>HTML entities &amp; markups</strong>]]' => '<a href="/projects/ecookbook/wiki/Another_page" class="wiki-page">With title containing &lt;strong&gt;HTML entities &amp; markups&lt;/strong&gt;</a>',
509 # link with anchor
509 # link with anchor
510 '[[CookBook documentation#One-section]]' => '<a href="/projects/ecookbook/wiki/CookBook_documentation#One-section" class="wiki-page">CookBook documentation</a>',
510 '[[CookBook documentation#One-section]]' => '<a href="/projects/ecookbook/wiki/CookBook_documentation#One-section" class="wiki-page">CookBook documentation</a>',
511 '[[Another page#anchor|Page]]' => '<a href="/projects/ecookbook/wiki/Another_page#anchor" class="wiki-page">Page</a>',
511 '[[Another page#anchor|Page]]' => '<a href="/projects/ecookbook/wiki/Another_page#anchor" class="wiki-page">Page</a>',
512 # page that doesn't exist
512 # page that doesn't exist
513 '[[Unknown page]]' => '<a href="/projects/ecookbook/wiki/Unknown_page" class="wiki-page new">Unknown page</a>',
513 '[[Unknown page]]' => '<a href="/projects/ecookbook/wiki/Unknown_page" class="wiki-page new">Unknown page</a>',
514 '[[Unknown page|404]]' => '<a href="/projects/ecookbook/wiki/Unknown_page" class="wiki-page new">404</a>',
514 '[[Unknown page|404]]' => '<a href="/projects/ecookbook/wiki/Unknown_page" class="wiki-page new">404</a>',
515 # link to another project wiki
515 # link to another project wiki
516 '[[onlinestore:]]' => '<a href="/projects/onlinestore/wiki" class="wiki-page">onlinestore</a>',
516 '[[onlinestore:]]' => '<a href="/projects/onlinestore/wiki" class="wiki-page">onlinestore</a>',
517 '[[onlinestore:|Wiki]]' => '<a href="/projects/onlinestore/wiki" class="wiki-page">Wiki</a>',
517 '[[onlinestore:|Wiki]]' => '<a href="/projects/onlinestore/wiki" class="wiki-page">Wiki</a>',
518 '[[onlinestore:Start page]]' => '<a href="/projects/onlinestore/wiki/Start_page" class="wiki-page">Start page</a>',
518 '[[onlinestore:Start page]]' => '<a href="/projects/onlinestore/wiki/Start_page" class="wiki-page">Start page</a>',
519 '[[onlinestore:Start page|Text]]' => '<a href="/projects/onlinestore/wiki/Start_page" class="wiki-page">Text</a>',
519 '[[onlinestore:Start page|Text]]' => '<a href="/projects/onlinestore/wiki/Start_page" class="wiki-page">Text</a>',
520 '[[onlinestore:Unknown page]]' => '<a href="/projects/onlinestore/wiki/Unknown_page" class="wiki-page new">Unknown page</a>',
520 '[[onlinestore:Unknown page]]' => '<a href="/projects/onlinestore/wiki/Unknown_page" class="wiki-page new">Unknown page</a>',
521 # striked through link
521 # striked through link
522 '-[[Another page|Page]]-' => '<del><a href="/projects/ecookbook/wiki/Another_page" class="wiki-page">Page</a></del>',
522 '-[[Another page|Page]]-' => '<del><a href="/projects/ecookbook/wiki/Another_page" class="wiki-page">Page</a></del>',
523 '-[[Another page|Page]] link-' => '<del><a href="/projects/ecookbook/wiki/Another_page" class="wiki-page">Page</a> link</del>',
523 '-[[Another page|Page]] link-' => '<del><a href="/projects/ecookbook/wiki/Another_page" class="wiki-page">Page</a> link</del>',
524 # escaping
524 # escaping
525 '![[Another page|Page]]' => '[[Another page|Page]]',
525 '![[Another page|Page]]' => '[[Another page|Page]]',
526 # project does not exist
526 # project does not exist
527 '[[unknowproject:Start]]' => '[[unknowproject:Start]]',
527 '[[unknowproject:Start]]' => '[[unknowproject:Start]]',
528 '[[unknowproject:Start|Page title]]' => '[[unknowproject:Start|Page title]]',
528 '[[unknowproject:Start|Page title]]' => '[[unknowproject:Start|Page title]]',
529 }
529 }
530
530
531 @project = Project.find(1)
531 @project = Project.find(1)
532 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
532 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
533 end
533 end
534
534
535 def test_wiki_links_within_local_file_generation_context
535 def test_wiki_links_within_local_file_generation_context
536
536
537 to_test = {
537 to_test = {
538 # link to a page
538 # link to a page
539 '[[CookBook documentation]]' => '<a href="CookBook_documentation.html" class="wiki-page">CookBook documentation</a>',
539 '[[CookBook documentation]]' => '<a href="CookBook_documentation.html" class="wiki-page">CookBook documentation</a>',
540 '[[CookBook documentation|documentation]]' => '<a href="CookBook_documentation.html" class="wiki-page">documentation</a>',
540 '[[CookBook documentation|documentation]]' => '<a href="CookBook_documentation.html" class="wiki-page">documentation</a>',
541 '[[CookBook documentation#One-section]]' => '<a href="CookBook_documentation.html#One-section" class="wiki-page">CookBook documentation</a>',
541 '[[CookBook documentation#One-section]]' => '<a href="CookBook_documentation.html#One-section" class="wiki-page">CookBook documentation</a>',
542 '[[CookBook documentation#One-section|documentation]]' => '<a href="CookBook_documentation.html#One-section" class="wiki-page">documentation</a>',
542 '[[CookBook documentation#One-section|documentation]]' => '<a href="CookBook_documentation.html#One-section" class="wiki-page">documentation</a>',
543 # page that doesn't exist
543 # page that doesn't exist
544 '[[Unknown page]]' => '<a href="Unknown_page.html" class="wiki-page new">Unknown page</a>',
544 '[[Unknown page]]' => '<a href="Unknown_page.html" class="wiki-page new">Unknown page</a>',
545 '[[Unknown page|404]]' => '<a href="Unknown_page.html" class="wiki-page new">404</a>',
545 '[[Unknown page|404]]' => '<a href="Unknown_page.html" class="wiki-page new">404</a>',
546 '[[Unknown page#anchor]]' => '<a href="Unknown_page.html#anchor" class="wiki-page new">Unknown page</a>',
546 '[[Unknown page#anchor]]' => '<a href="Unknown_page.html#anchor" class="wiki-page new">Unknown page</a>',
547 '[[Unknown page#anchor|404]]' => '<a href="Unknown_page.html#anchor" class="wiki-page new">404</a>',
547 '[[Unknown page#anchor|404]]' => '<a href="Unknown_page.html#anchor" class="wiki-page new">404</a>',
548 }
548 }
549
549
550 @project = Project.find(1)
550 @project = Project.find(1)
551
551
552 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text, :wiki_links => :local) }
552 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text, :wiki_links => :local) }
553 end
553 end
554
554
555 def test_wiki_links_within_wiki_page_context
555 def test_wiki_links_within_wiki_page_context
556
556
557 page = WikiPage.find_by_title('Another_page' )
557 page = WikiPage.find_by_title('Another_page' )
558
558
559 to_test = {
559 to_test = {
560 # link to another page
560 # link to another page
561 '[[CookBook documentation]]' => '<a href="/projects/ecookbook/wiki/CookBook_documentation" class="wiki-page">CookBook documentation</a>',
561 '[[CookBook documentation]]' => '<a href="/projects/ecookbook/wiki/CookBook_documentation" class="wiki-page">CookBook documentation</a>',
562 '[[CookBook documentation|documentation]]' => '<a href="/projects/ecookbook/wiki/CookBook_documentation" class="wiki-page">documentation</a>',
562 '[[CookBook documentation|documentation]]' => '<a href="/projects/ecookbook/wiki/CookBook_documentation" class="wiki-page">documentation</a>',
563 '[[CookBook documentation#One-section]]' => '<a href="/projects/ecookbook/wiki/CookBook_documentation#One-section" class="wiki-page">CookBook documentation</a>',
563 '[[CookBook documentation#One-section]]' => '<a href="/projects/ecookbook/wiki/CookBook_documentation#One-section" class="wiki-page">CookBook documentation</a>',
564 '[[CookBook documentation#One-section|documentation]]' => '<a href="/projects/ecookbook/wiki/CookBook_documentation#One-section" class="wiki-page">documentation</a>',
564 '[[CookBook documentation#One-section|documentation]]' => '<a href="/projects/ecookbook/wiki/CookBook_documentation#One-section" class="wiki-page">documentation</a>',
565 # link to the current page
565 # link to the current page
566 '[[Another page]]' => '<a href="/projects/ecookbook/wiki/Another_page" class="wiki-page">Another page</a>',
566 '[[Another page]]' => '<a href="/projects/ecookbook/wiki/Another_page" class="wiki-page">Another page</a>',
567 '[[Another page|Page]]' => '<a href="/projects/ecookbook/wiki/Another_page" class="wiki-page">Page</a>',
567 '[[Another page|Page]]' => '<a href="/projects/ecookbook/wiki/Another_page" class="wiki-page">Page</a>',
568 '[[Another page#anchor]]' => '<a href="#anchor" class="wiki-page">Another page</a>',
568 '[[Another page#anchor]]' => '<a href="#anchor" class="wiki-page">Another page</a>',
569 '[[Another page#anchor|Page]]' => '<a href="#anchor" class="wiki-page">Page</a>',
569 '[[Another page#anchor|Page]]' => '<a href="#anchor" class="wiki-page">Page</a>',
570 # page that doesn't exist
570 # page that doesn't exist
571 '[[Unknown page]]' => '<a href="/projects/ecookbook/wiki/Unknown_page?parent=Another_page" class="wiki-page new">Unknown page</a>',
571 '[[Unknown page]]' => '<a href="/projects/ecookbook/wiki/Unknown_page?parent=Another_page" class="wiki-page new">Unknown page</a>',
572 '[[Unknown page|404]]' => '<a href="/projects/ecookbook/wiki/Unknown_page?parent=Another_page" class="wiki-page new">404</a>',
572 '[[Unknown page|404]]' => '<a href="/projects/ecookbook/wiki/Unknown_page?parent=Another_page" class="wiki-page new">404</a>',
573 '[[Unknown page#anchor]]' => '<a href="/projects/ecookbook/wiki/Unknown_page?parent=Another_page#anchor" class="wiki-page new">Unknown page</a>',
573 '[[Unknown page#anchor]]' => '<a href="/projects/ecookbook/wiki/Unknown_page?parent=Another_page#anchor" class="wiki-page new">Unknown page</a>',
574 '[[Unknown page#anchor|404]]' => '<a href="/projects/ecookbook/wiki/Unknown_page?parent=Another_page#anchor" class="wiki-page new">404</a>',
574 '[[Unknown page#anchor|404]]' => '<a href="/projects/ecookbook/wiki/Unknown_page?parent=Another_page#anchor" class="wiki-page new">404</a>',
575 }
575 }
576
576
577 @project = Project.find(1)
577 @project = Project.find(1)
578
578
579 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(WikiContent.new( :text => text, :page => page ), :text) }
579 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(WikiContent.new( :text => text, :page => page ), :text) }
580 end
580 end
581
581
582 def test_wiki_links_anchor_option_should_prepend_page_title_to_href
582 def test_wiki_links_anchor_option_should_prepend_page_title_to_href
583
583
584 to_test = {
584 to_test = {
585 # link to a page
585 # link to a page
586 '[[CookBook documentation]]' => '<a href="#CookBook_documentation" class="wiki-page">CookBook documentation</a>',
586 '[[CookBook documentation]]' => '<a href="#CookBook_documentation" class="wiki-page">CookBook documentation</a>',
587 '[[CookBook documentation|documentation]]' => '<a href="#CookBook_documentation" class="wiki-page">documentation</a>',
587 '[[CookBook documentation|documentation]]' => '<a href="#CookBook_documentation" class="wiki-page">documentation</a>',
588 '[[CookBook documentation#One-section]]' => '<a href="#CookBook_documentation_One-section" class="wiki-page">CookBook documentation</a>',
588 '[[CookBook documentation#One-section]]' => '<a href="#CookBook_documentation_One-section" class="wiki-page">CookBook documentation</a>',
589 '[[CookBook documentation#One-section|documentation]]' => '<a href="#CookBook_documentation_One-section" class="wiki-page">documentation</a>',
589 '[[CookBook documentation#One-section|documentation]]' => '<a href="#CookBook_documentation_One-section" class="wiki-page">documentation</a>',
590 # page that doesn't exist
590 # page that doesn't exist
591 '[[Unknown page]]' => '<a href="#Unknown_page" class="wiki-page new">Unknown page</a>',
591 '[[Unknown page]]' => '<a href="#Unknown_page" class="wiki-page new">Unknown page</a>',
592 '[[Unknown page|404]]' => '<a href="#Unknown_page" class="wiki-page new">404</a>',
592 '[[Unknown page|404]]' => '<a href="#Unknown_page" class="wiki-page new">404</a>',
593 '[[Unknown page#anchor]]' => '<a href="#Unknown_page_anchor" class="wiki-page new">Unknown page</a>',
593 '[[Unknown page#anchor]]' => '<a href="#Unknown_page_anchor" class="wiki-page new">Unknown page</a>',
594 '[[Unknown page#anchor|404]]' => '<a href="#Unknown_page_anchor" class="wiki-page new">404</a>',
594 '[[Unknown page#anchor|404]]' => '<a href="#Unknown_page_anchor" class="wiki-page new">404</a>',
595 }
595 }
596
596
597 @project = Project.find(1)
597 @project = Project.find(1)
598
598
599 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text, :wiki_links => :anchor) }
599 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text, :wiki_links => :anchor) }
600 end
600 end
601
601
602 def test_html_tags
602 def test_html_tags
603 to_test = {
603 to_test = {
604 "<div>content</div>" => "<p>&lt;div&gt;content&lt;/div&gt;</p>",
604 "<div>content</div>" => "<p>&lt;div&gt;content&lt;/div&gt;</p>",
605 "<div class=\"bold\">content</div>" => "<p>&lt;div class=\"bold\"&gt;content&lt;/div&gt;</p>",
605 "<div class=\"bold\">content</div>" => "<p>&lt;div class=\"bold\"&gt;content&lt;/div&gt;</p>",
606 "<script>some script;</script>" => "<p>&lt;script&gt;some script;&lt;/script&gt;</p>",
606 "<script>some script;</script>" => "<p>&lt;script&gt;some script;&lt;/script&gt;</p>",
607 # do not escape pre/code tags
607 # do not escape pre/code tags
608 "<pre>\nline 1\nline2</pre>" => "<pre>\nline 1\nline2</pre>",
608 "<pre>\nline 1\nline2</pre>" => "<pre>\nline 1\nline2</pre>",
609 "<pre><code>\nline 1\nline2</code></pre>" => "<pre><code>\nline 1\nline2</code></pre>",
609 "<pre><code>\nline 1\nline2</code></pre>" => "<pre><code>\nline 1\nline2</code></pre>",
610 "<pre><div>content</div></pre>" => "<pre>&lt;div&gt;content&lt;/div&gt;</pre>",
610 "<pre><div>content</div></pre>" => "<pre>&lt;div&gt;content&lt;/div&gt;</pre>",
611 "HTML comment: <!-- no comments -->" => "<p>HTML comment: &lt;!-- no comments --&gt;</p>",
611 "HTML comment: <!-- no comments -->" => "<p>HTML comment: &lt;!-- no comments --&gt;</p>",
612 "<!-- opening comment" => "<p>&lt;!-- opening comment</p>",
612 "<!-- opening comment" => "<p>&lt;!-- opening comment</p>",
613 # remove attributes except class
613 # remove attributes except class
614 "<pre class='foo'>some text</pre>" => "<pre class='foo'>some text</pre>",
614 "<pre class='foo'>some text</pre>" => "<pre class='foo'>some text</pre>",
615 '<pre class="foo">some text</pre>' => '<pre class="foo">some text</pre>',
615 '<pre class="foo">some text</pre>' => '<pre class="foo">some text</pre>',
616 "<pre class='foo bar'>some text</pre>" => "<pre class='foo bar'>some text</pre>",
616 "<pre class='foo bar'>some text</pre>" => "<pre class='foo bar'>some text</pre>",
617 '<pre class="foo bar">some text</pre>' => '<pre class="foo bar">some text</pre>',
617 '<pre class="foo bar">some text</pre>' => '<pre class="foo bar">some text</pre>',
618 "<pre onmouseover='alert(1)'>some text</pre>" => "<pre>some text</pre>",
618 "<pre onmouseover='alert(1)'>some text</pre>" => "<pre>some text</pre>",
619 # xss
619 # xss
620 '<pre><code class=""onmouseover="alert(1)">text</code></pre>' => '<pre><code>text</code></pre>',
620 '<pre><code class=""onmouseover="alert(1)">text</code></pre>' => '<pre><code>text</code></pre>',
621 '<pre class=""onmouseover="alert(1)">text</pre>' => '<pre>text</pre>',
621 '<pre class=""onmouseover="alert(1)">text</pre>' => '<pre>text</pre>',
622 }
622 }
623 to_test.each { |text, result| assert_equal result, textilizable(text) }
623 to_test.each { |text, result| assert_equal result, textilizable(text) }
624 end
624 end
625
625
626 def test_allowed_html_tags
626 def test_allowed_html_tags
627 to_test = {
627 to_test = {
628 "<pre>preformatted text</pre>" => "<pre>preformatted text</pre>",
628 "<pre>preformatted text</pre>" => "<pre>preformatted text</pre>",
629 "<notextile>no *textile* formatting</notextile>" => "no *textile* formatting",
629 "<notextile>no *textile* formatting</notextile>" => "no *textile* formatting",
630 "<notextile>this is <tag>a tag</tag></notextile>" => "this is &lt;tag&gt;a tag&lt;/tag&gt;"
630 "<notextile>this is <tag>a tag</tag></notextile>" => "this is &lt;tag&gt;a tag&lt;/tag&gt;"
631 }
631 }
632 to_test.each { |text, result| assert_equal result, textilizable(text) }
632 to_test.each { |text, result| assert_equal result, textilizable(text) }
633 end
633 end
634
634
635 def test_pre_tags
635 def test_pre_tags
636 raw = <<-RAW
636 raw = <<-RAW
637 Before
637 Before
638
638
639 <pre>
639 <pre>
640 <prepared-statement-cache-size>32</prepared-statement-cache-size>
640 <prepared-statement-cache-size>32</prepared-statement-cache-size>
641 </pre>
641 </pre>
642
642
643 After
643 After
644 RAW
644 RAW
645
645
646 expected = <<-EXPECTED
646 expected = <<-EXPECTED
647 <p>Before</p>
647 <p>Before</p>
648 <pre>
648 <pre>
649 &lt;prepared-statement-cache-size&gt;32&lt;/prepared-statement-cache-size&gt;
649 &lt;prepared-statement-cache-size&gt;32&lt;/prepared-statement-cache-size&gt;
650 </pre>
650 </pre>
651 <p>After</p>
651 <p>After</p>
652 EXPECTED
652 EXPECTED
653
653
654 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
654 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
655 end
655 end
656
656
657 def test_pre_content_should_not_parse_wiki_and_redmine_links
657 def test_pre_content_should_not_parse_wiki_and_redmine_links
658 raw = <<-RAW
658 raw = <<-RAW
659 [[CookBook documentation]]
659 [[CookBook documentation]]
660
660
661 #1
661 #1
662
662
663 <pre>
663 <pre>
664 [[CookBook documentation]]
664 [[CookBook documentation]]
665
665
666 #1
666 #1
667 </pre>
667 </pre>
668 RAW
668 RAW
669
669
670 expected = <<-EXPECTED
670 expected = <<-EXPECTED
671 <p><a href="/projects/ecookbook/wiki/CookBook_documentation" class="wiki-page">CookBook documentation</a></p>
671 <p><a href="/projects/ecookbook/wiki/CookBook_documentation" class="wiki-page">CookBook documentation</a></p>
672 <p><a href="/issues/1" class="issue status-1 priority-1" title="Can't print recipes (New)">#1</a></p>
672 <p><a href="/issues/1" class="issue status-1 priority-1" title="Can't print recipes (New)">#1</a></p>
673 <pre>
673 <pre>
674 [[CookBook documentation]]
674 [[CookBook documentation]]
675
675
676 #1
676 #1
677 </pre>
677 </pre>
678 EXPECTED
678 EXPECTED
679
679
680 @project = Project.find(1)
680 @project = Project.find(1)
681 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
681 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
682 end
682 end
683
683
684 def test_non_closing_pre_blocks_should_be_closed
684 def test_non_closing_pre_blocks_should_be_closed
685 raw = <<-RAW
685 raw = <<-RAW
686 <pre><code>
686 <pre><code>
687 RAW
687 RAW
688
688
689 expected = <<-EXPECTED
689 expected = <<-EXPECTED
690 <pre><code>
690 <pre><code>
691 </code></pre>
691 </code></pre>
692 EXPECTED
692 EXPECTED
693
693
694 @project = Project.find(1)
694 @project = Project.find(1)
695 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
695 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
696 end
696 end
697
697
698 def test_syntax_highlight
698 def test_syntax_highlight
699 raw = <<-RAW
699 raw = <<-RAW
700 <pre><code class="ruby">
700 <pre><code class="ruby">
701 # Some ruby code here
701 # Some ruby code here
702 </code></pre>
702 </code></pre>
703 RAW
703 RAW
704
704
705 expected = <<-EXPECTED
705 expected = <<-EXPECTED
706 <pre><code class="ruby syntaxhl"><span class=\"CodeRay\"><span class="line-numbers">1</span><span class="comment"># Some ruby code here</span></span>
706 <pre><code class="ruby syntaxhl"><span class=\"CodeRay\"><span class="line-numbers">1</span><span class="comment"># Some ruby code here</span></span>
707 </code></pre>
707 </code></pre>
708 EXPECTED
708 EXPECTED
709
709
710 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
710 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
711 end
711 end
712
712
713 def test_wiki_links_in_tables
713 def test_wiki_links_in_tables
714 to_test = {"|[[Page|Link title]]|[[Other Page|Other title]]|\n|Cell 21|[[Last page]]|" =>
714 to_test = {"|[[Page|Link title]]|[[Other Page|Other title]]|\n|Cell 21|[[Last page]]|" =>
715 '<tr><td><a href="/projects/ecookbook/wiki/Page" class="wiki-page new">Link title</a></td>' +
715 '<tr><td><a href="/projects/ecookbook/wiki/Page" class="wiki-page new">Link title</a></td>' +
716 '<td><a href="/projects/ecookbook/wiki/Other_Page" class="wiki-page new">Other title</a></td>' +
716 '<td><a href="/projects/ecookbook/wiki/Other_Page" class="wiki-page new">Other title</a></td>' +
717 '</tr><tr><td>Cell 21</td><td><a href="/projects/ecookbook/wiki/Last_page" class="wiki-page new">Last page</a></td></tr>'
717 '</tr><tr><td>Cell 21</td><td><a href="/projects/ecookbook/wiki/Last_page" class="wiki-page new">Last page</a></td></tr>'
718 }
718 }
719 @project = Project.find(1)
719 @project = Project.find(1)
720 to_test.each { |text, result| assert_equal "<table>#{result}</table>", textilizable(text).gsub(/[\t\n]/, '') }
720 to_test.each { |text, result| assert_equal "<table>#{result}</table>", textilizable(text).gsub(/[\t\n]/, '') }
721 end
721 end
722
722
723 def test_text_formatting
723 def test_text_formatting
724 to_test = {'*_+bold, italic and underline+_*' => '<strong><em><ins>bold, italic and underline</ins></em></strong>',
724 to_test = {'*_+bold, italic and underline+_*' => '<strong><em><ins>bold, italic and underline</ins></em></strong>',
725 '(_text within parentheses_)' => '(<em>text within parentheses</em>)',
725 '(_text within parentheses_)' => '(<em>text within parentheses</em>)',
726 'a *Humane Web* Text Generator' => 'a <strong>Humane Web</strong> Text Generator',
726 'a *Humane Web* Text Generator' => 'a <strong>Humane Web</strong> Text Generator',
727 '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>',
727 '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>',
728 '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',
728 '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',
729 }
729 }
730 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
730 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
731 end
731 end
732
732
733 def test_wiki_horizontal_rule
733 def test_wiki_horizontal_rule
734 assert_equal '<hr />', textilizable('---')
734 assert_equal '<hr />', textilizable('---')
735 assert_equal '<p>Dashes: ---</p>', textilizable('Dashes: ---')
735 assert_equal '<p>Dashes: ---</p>', textilizable('Dashes: ---')
736 end
736 end
737
737
738 def test_footnotes
738 def test_footnotes
739 raw = <<-RAW
739 raw = <<-RAW
740 This is some text[1].
740 This is some text[1].
741
741
742 fn1. This is the foot note
742 fn1. This is the foot note
743 RAW
743 RAW
744
744
745 expected = <<-EXPECTED
745 expected = <<-EXPECTED
746 <p>This is some text<sup><a href=\"#fn1\">1</a></sup>.</p>
746 <p>This is some text<sup><a href=\"#fn1\">1</a></sup>.</p>
747 <p id="fn1" class="footnote"><sup>1</sup> This is the foot note</p>
747 <p id="fn1" class="footnote"><sup>1</sup> This is the foot note</p>
748 EXPECTED
748 EXPECTED
749
749
750 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
750 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
751 end
751 end
752
752
753 def test_headings
753 def test_headings
754 raw = 'h1. Some heading'
754 raw = 'h1. Some heading'
755 expected = %|<a name="Some-heading"></a>\n<h1 >Some heading<a href="#Some-heading" class="wiki-anchor">&para;</a></h1>|
755 expected = %|<a name="Some-heading"></a>\n<h1 >Some heading<a href="#Some-heading" class="wiki-anchor">&para;</a></h1>|
756
756
757 assert_equal expected, textilizable(raw)
757 assert_equal expected, textilizable(raw)
758 end
758 end
759
759
760 def test_headings_with_special_chars
760 def test_headings_with_special_chars
761 # This test makes sure that the generated anchor names match the expected
761 # This test makes sure that the generated anchor names match the expected
762 # ones even if the heading text contains unconventional characters
762 # ones even if the heading text contains unconventional characters
763 raw = 'h1. Some heading related to version 0.5'
763 raw = 'h1. Some heading related to version 0.5'
764 anchor = sanitize_anchor_name("Some-heading-related-to-version-0.5")
764 anchor = sanitize_anchor_name("Some-heading-related-to-version-0.5")
765 expected = %|<a name="#{anchor}"></a>\n<h1 >Some heading related to version 0.5<a href="##{anchor}" class="wiki-anchor">&para;</a></h1>|
765 expected = %|<a name="#{anchor}"></a>\n<h1 >Some heading related to version 0.5<a href="##{anchor}" class="wiki-anchor">&para;</a></h1>|
766
766
767 assert_equal expected, textilizable(raw)
767 assert_equal expected, textilizable(raw)
768 end
768 end
769
769
770 def test_headings_in_wiki_single_page_export_should_be_prepended_with_page_title
770 def test_headings_in_wiki_single_page_export_should_be_prepended_with_page_title
771 page = WikiPage.new( :title => 'Page Title', :wiki_id => 1 )
771 page = WikiPage.new( :title => 'Page Title', :wiki_id => 1 )
772 content = WikiContent.new( :text => 'h1. Some heading', :page => page )
772 content = WikiContent.new( :text => 'h1. Some heading', :page => page )
773
773
774 expected = %|<a name="Page_Title_Some-heading"></a>\n<h1 >Some heading<a href="#Page_Title_Some-heading" class="wiki-anchor">&para;</a></h1>|
774 expected = %|<a name="Page_Title_Some-heading"></a>\n<h1 >Some heading<a href="#Page_Title_Some-heading" class="wiki-anchor">&para;</a></h1>|
775
775
776 assert_equal expected, textilizable(content, :text, :wiki_links => :anchor )
776 assert_equal expected, textilizable(content, :text, :wiki_links => :anchor )
777 end
777 end
778
778
779 def test_table_of_content
779 def test_table_of_content
780 raw = <<-RAW
780 raw = <<-RAW
781 {{toc}}
781 {{toc}}
782
782
783 h1. Title
783 h1. Title
784
784
785 Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas sed libero.
785 Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas sed libero.
786
786
787 h2. Subtitle with a [[Wiki]] link
787 h2. Subtitle with a [[Wiki]] link
788
788
789 Nullam commodo metus accumsan nulla. Curabitur lobortis dui id dolor.
789 Nullam commodo metus accumsan nulla. Curabitur lobortis dui id dolor.
790
790
791 h2. Subtitle with [[Wiki|another Wiki]] link
791 h2. Subtitle with [[Wiki|another Wiki]] link
792
792
793 h2. Subtitle with %{color:red}red text%
793 h2. Subtitle with %{color:red}red text%
794
794
795 <pre>
795 <pre>
796 some code
796 some code
797 </pre>
797 </pre>
798
798
799 h3. Subtitle with *some* _modifiers_
799 h3. Subtitle with *some* _modifiers_
800
800
801 h3. Subtitle with @inline code@
801 h3. Subtitle with @inline code@
802
802
803 h1. Another title
803 h1. Another title
804
804
805 h3. An "Internet link":http://www.redmine.org/ inside subtitle
805 h3. An "Internet link":http://www.redmine.org/ inside subtitle
806
806
807 h2. "Project Name !/attachments/1234/logo_small.gif! !/attachments/5678/logo_2.png!":/projects/projectname/issues
807 h2. "Project Name !/attachments/1234/logo_small.gif! !/attachments/5678/logo_2.png!":/projects/projectname/issues
808
808
809 RAW
809 RAW
810
810
811 expected = '<ul class="toc">' +
811 expected = '<ul class="toc">' +
812 '<li><a href="#Title">Title</a>' +
812 '<li><a href="#Title">Title</a>' +
813 '<ul>' +
813 '<ul>' +
814 '<li><a href="#Subtitle-with-a-Wiki-link">Subtitle with a Wiki link</a></li>' +
814 '<li><a href="#Subtitle-with-a-Wiki-link">Subtitle with a Wiki link</a></li>' +
815 '<li><a href="#Subtitle-with-another-Wiki-link">Subtitle with another Wiki link</a></li>' +
815 '<li><a href="#Subtitle-with-another-Wiki-link">Subtitle with another Wiki link</a></li>' +
816 '<li><a href="#Subtitle-with-red-text">Subtitle with red text</a>' +
816 '<li><a href="#Subtitle-with-red-text">Subtitle with red text</a>' +
817 '<ul>' +
817 '<ul>' +
818 '<li><a href="#Subtitle-with-some-modifiers">Subtitle with some modifiers</a></li>' +
818 '<li><a href="#Subtitle-with-some-modifiers">Subtitle with some modifiers</a></li>' +
819 '<li><a href="#Subtitle-with-inline-code">Subtitle with inline code</a></li>' +
819 '<li><a href="#Subtitle-with-inline-code">Subtitle with inline code</a></li>' +
820 '</ul>' +
820 '</ul>' +
821 '</li>' +
821 '</li>' +
822 '</ul>' +
822 '</ul>' +
823 '</li>' +
823 '</li>' +
824 '<li><a href="#Another-title">Another title</a>' +
824 '<li><a href="#Another-title">Another title</a>' +
825 '<ul>' +
825 '<ul>' +
826 '<li>' +
826 '<li>' +
827 '<ul>' +
827 '<ul>' +
828 '<li><a href="#An-Internet-link-inside-subtitle">An Internet link inside subtitle</a></li>' +
828 '<li><a href="#An-Internet-link-inside-subtitle">An Internet link inside subtitle</a></li>' +
829 '</ul>' +
829 '</ul>' +
830 '</li>' +
830 '</li>' +
831 '<li><a href="#Project-Name">Project Name</a></li>' +
831 '<li><a href="#Project-Name">Project Name</a></li>' +
832 '</ul>' +
832 '</ul>' +
833 '</li>' +
833 '</li>' +
834 '</ul>'
834 '</ul>'
835
835
836 @project = Project.find(1)
836 @project = Project.find(1)
837 assert textilizable(raw).gsub("\n", "").include?(expected)
837 assert textilizable(raw).gsub("\n", "").include?(expected)
838 end
838 end
839
839
840 def test_table_of_content_should_generate_unique_anchors
841 raw = <<-RAW
842 {{toc}}
843
844 h1. Title
845
846 h2. Subtitle
847
848 h2. Subtitle
849 RAW
850
851 expected = '<ul class="toc">' +
852 '<li><a href="#Title">Title</a>' +
853 '<ul>' +
854 '<li><a href="#Subtitle">Subtitle</a></li>' +
855 '<li><a href="#Subtitle-2">Subtitle</a></li>'
856 '</ul>'
857 '</li>' +
858 '</ul>'
859
860 @project = Project.find(1)
861 result = textilizable(raw).gsub("\n", "")
862 assert_include expected, result
863 assert_include '<a name="Subtitle">', result
864 assert_include '<a name="Subtitle-2">', result
865 end
866
840 def test_table_of_content_should_contain_included_page_headings
867 def test_table_of_content_should_contain_included_page_headings
841 raw = <<-RAW
868 raw = <<-RAW
842 {{toc}}
869 {{toc}}
843
870
844 h1. Included
871 h1. Included
845
872
846 {{include(Child_1)}}
873 {{include(Child_1)}}
847 RAW
874 RAW
848
875
849 expected = '<ul class="toc">' +
876 expected = '<ul class="toc">' +
850 '<li><a href="#Included">Included</a></li>' +
877 '<li><a href="#Included">Included</a></li>' +
851 '<li><a href="#Child-page-1">Child page 1</a></li>' +
878 '<li><a href="#Child-page-1">Child page 1</a></li>' +
852 '</ul>'
879 '</ul>'
853
880
854 @project = Project.find(1)
881 @project = Project.find(1)
855 assert textilizable(raw).gsub("\n", "").include?(expected)
882 assert textilizable(raw).gsub("\n", "").include?(expected)
856 end
883 end
857
884
858 def test_section_edit_links
885 def test_section_edit_links
859 raw = <<-RAW
886 raw = <<-RAW
860 h1. Title
887 h1. Title
861
888
862 Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas sed libero.
889 Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas sed libero.
863
890
864 h2. Subtitle with a [[Wiki]] link
891 h2. Subtitle with a [[Wiki]] link
865
892
866 h2. Subtitle with *some* _modifiers_
893 h2. Subtitle with *some* _modifiers_
867
894
868 h2. Subtitle with @inline code@
895 h2. Subtitle with @inline code@
869
896
870 <pre>
897 <pre>
871 some code
898 some code
872
899
873 h2. heading inside pre
900 h2. heading inside pre
874
901
875 <h2>html heading inside pre</h2>
902 <h2>html heading inside pre</h2>
876 </pre>
903 </pre>
877
904
878 h2. Subtitle after pre tag
905 h2. Subtitle after pre tag
879 RAW
906 RAW
880
907
881 @project = Project.find(1)
908 @project = Project.find(1)
882 set_language_if_valid 'en'
909 set_language_if_valid 'en'
883 result = textilizable(raw, :edit_section_links => {:controller => 'wiki', :action => 'edit', :project_id => '1', :id => 'Test'}).gsub("\n", "")
910 result = textilizable(raw, :edit_section_links => {:controller => 'wiki', :action => 'edit', :project_id => '1', :id => 'Test'}).gsub("\n", "")
884
911
885 # heading that contains inline code
912 # heading that contains inline code
886 assert_match Regexp.new('<div class="contextual" title="Edit this section">' +
913 assert_match Regexp.new('<div class="contextual" title="Edit this section">' +
887 '<a href="/projects/1/wiki/Test/edit\?section=4"><img alt="Edit" src="/images/edit.png(\?\d+)?" /></a></div>' +
914 '<a href="/projects/1/wiki/Test/edit\?section=4"><img alt="Edit" src="/images/edit.png(\?\d+)?" /></a></div>' +
888 '<a name="Subtitle-with-inline-code"></a>' +
915 '<a name="Subtitle-with-inline-code"></a>' +
889 '<h2 >Subtitle with <code>inline code</code><a href="#Subtitle-with-inline-code" class="wiki-anchor">&para;</a></h2>'),
916 '<h2 >Subtitle with <code>inline code</code><a href="#Subtitle-with-inline-code" class="wiki-anchor">&para;</a></h2>'),
890 result
917 result
891
918
892 # last heading
919 # last heading
893 assert_match Regexp.new('<div class="contextual" title="Edit this section">' +
920 assert_match Regexp.new('<div class="contextual" title="Edit this section">' +
894 '<a href="/projects/1/wiki/Test/edit\?section=5"><img alt="Edit" src="/images/edit.png(\?\d+)?" /></a></div>' +
921 '<a href="/projects/1/wiki/Test/edit\?section=5"><img alt="Edit" src="/images/edit.png(\?\d+)?" /></a></div>' +
895 '<a name="Subtitle-after-pre-tag"></a>' +
922 '<a name="Subtitle-after-pre-tag"></a>' +
896 '<h2 >Subtitle after pre tag<a href="#Subtitle-after-pre-tag" class="wiki-anchor">&para;</a></h2>'),
923 '<h2 >Subtitle after pre tag<a href="#Subtitle-after-pre-tag" class="wiki-anchor">&para;</a></h2>'),
897 result
924 result
898 end
925 end
899
926
900 def test_default_formatter
927 def test_default_formatter
901 with_settings :text_formatting => 'unknown' do
928 with_settings :text_formatting => 'unknown' do
902 text = 'a *link*: http://www.example.net/'
929 text = 'a *link*: http://www.example.net/'
903 assert_equal '<p>a *link*: <a href="http://www.example.net/">http://www.example.net/</a></p>', textilizable(text)
930 assert_equal '<p>a *link*: <a href="http://www.example.net/">http://www.example.net/</a></p>', textilizable(text)
904 end
931 end
905 end
932 end
906
933
907 def test_due_date_distance_in_words
934 def test_due_date_distance_in_words
908 to_test = { Date.today => 'Due in 0 days',
935 to_test = { Date.today => 'Due in 0 days',
909 Date.today + 1 => 'Due in 1 day',
936 Date.today + 1 => 'Due in 1 day',
910 Date.today + 100 => 'Due in about 3 months',
937 Date.today + 100 => 'Due in about 3 months',
911 Date.today + 20000 => 'Due in over 54 years',
938 Date.today + 20000 => 'Due in over 54 years',
912 Date.today - 1 => '1 day late',
939 Date.today - 1 => '1 day late',
913 Date.today - 100 => 'about 3 months late',
940 Date.today - 100 => 'about 3 months late',
914 Date.today - 20000 => 'over 54 years late',
941 Date.today - 20000 => 'over 54 years late',
915 }
942 }
916 ::I18n.locale = :en
943 ::I18n.locale = :en
917 to_test.each do |date, expected|
944 to_test.each do |date, expected|
918 assert_equal expected, due_date_distance_in_words(date)
945 assert_equal expected, due_date_distance_in_words(date)
919 end
946 end
920 end
947 end
921
948
922 def test_avatar
949 def test_avatar
923 # turn on avatars
950 # turn on avatars
924 Setting.gravatar_enabled = '1'
951 Setting.gravatar_enabled = '1'
925 assert avatar(User.find_by_mail('jsmith@somenet.foo')).include?(Digest::MD5.hexdigest('jsmith@somenet.foo'))
952 assert avatar(User.find_by_mail('jsmith@somenet.foo')).include?(Digest::MD5.hexdigest('jsmith@somenet.foo'))
926 assert avatar('jsmith <jsmith@somenet.foo>').include?(Digest::MD5.hexdigest('jsmith@somenet.foo'))
953 assert avatar('jsmith <jsmith@somenet.foo>').include?(Digest::MD5.hexdigest('jsmith@somenet.foo'))
927 assert_nil avatar('jsmith')
954 assert_nil avatar('jsmith')
928 assert_nil avatar(nil)
955 assert_nil avatar(nil)
929
956
930 # turn off avatars
957 # turn off avatars
931 Setting.gravatar_enabled = '0'
958 Setting.gravatar_enabled = '0'
932 assert_equal '', avatar(User.find_by_mail('jsmith@somenet.foo'))
959 assert_equal '', avatar(User.find_by_mail('jsmith@somenet.foo'))
933 end
960 end
934
961
935 def test_link_to_user
962 def test_link_to_user
936 user = User.find(2)
963 user = User.find(2)
937 t = link_to_user(user)
964 t = link_to_user(user)
938 assert_equal "<a href=\"/users/2\">#{ user.name }</a>", t
965 assert_equal "<a href=\"/users/2\">#{ user.name }</a>", t
939 end
966 end
940
967
941 def test_link_to_user_should_not_link_to_locked_user
968 def test_link_to_user_should_not_link_to_locked_user
942 user = User.find(5)
969 user = User.find(5)
943 assert user.locked?
970 assert user.locked?
944 t = link_to_user(user)
971 t = link_to_user(user)
945 assert_equal user.name, t
972 assert_equal user.name, t
946 end
973 end
947
974
948 def test_link_to_user_should_not_link_to_anonymous
975 def test_link_to_user_should_not_link_to_anonymous
949 user = User.anonymous
976 user = User.anonymous
950 assert user.anonymous?
977 assert user.anonymous?
951 t = link_to_user(user)
978 t = link_to_user(user)
952 assert_equal ::I18n.t(:label_user_anonymous), t
979 assert_equal ::I18n.t(:label_user_anonymous), t
953 end
980 end
954
981
955 def test_link_to_project
982 def test_link_to_project
956 project = Project.find(1)
983 project = Project.find(1)
957 assert_equal %(<a href="/projects/ecookbook">eCookbook</a>),
984 assert_equal %(<a href="/projects/ecookbook">eCookbook</a>),
958 link_to_project(project)
985 link_to_project(project)
959 assert_equal %(<a href="/projects/ecookbook/settings">eCookbook</a>),
986 assert_equal %(<a href="/projects/ecookbook/settings">eCookbook</a>),
960 link_to_project(project, :action => 'settings')
987 link_to_project(project, :action => 'settings')
961 assert_equal %(<a href="http://test.host/projects/ecookbook?jump=blah">eCookbook</a>),
988 assert_equal %(<a href="http://test.host/projects/ecookbook?jump=blah">eCookbook</a>),
962 link_to_project(project, {:only_path => false, :jump => 'blah'})
989 link_to_project(project, {:only_path => false, :jump => 'blah'})
963 assert_equal %(<a href="/projects/ecookbook/settings" class="project">eCookbook</a>),
990 assert_equal %(<a href="/projects/ecookbook/settings" class="project">eCookbook</a>),
964 link_to_project(project, {:action => 'settings'}, :class => "project")
991 link_to_project(project, {:action => 'settings'}, :class => "project")
965 end
992 end
966
993
967 def test_link_to_legacy_project_with_numerical_identifier_should_use_id
994 def test_link_to_legacy_project_with_numerical_identifier_should_use_id
968 # numeric identifier are no longer allowed
995 # numeric identifier are no longer allowed
969 Project.update_all "identifier=25", "id=1"
996 Project.update_all "identifier=25", "id=1"
970
997
971 assert_equal '<a href="/projects/1">eCookbook</a>',
998 assert_equal '<a href="/projects/1">eCookbook</a>',
972 link_to_project(Project.find(1))
999 link_to_project(Project.find(1))
973 end
1000 end
974
1001
975 def test_principals_options_for_select_with_users
1002 def test_principals_options_for_select_with_users
976 User.current = nil
1003 User.current = nil
977 users = [User.find(2), User.find(4)]
1004 users = [User.find(2), User.find(4)]
978 assert_equal %(<option value="2">John Smith</option><option value="4">Robert Hill</option>),
1005 assert_equal %(<option value="2">John Smith</option><option value="4">Robert Hill</option>),
979 principals_options_for_select(users)
1006 principals_options_for_select(users)
980 end
1007 end
981
1008
982 def test_principals_options_for_select_with_selected
1009 def test_principals_options_for_select_with_selected
983 User.current = nil
1010 User.current = nil
984 users = [User.find(2), User.find(4)]
1011 users = [User.find(2), User.find(4)]
985 assert_equal %(<option value="2">John Smith</option><option value="4" selected="selected">Robert Hill</option>),
1012 assert_equal %(<option value="2">John Smith</option><option value="4" selected="selected">Robert Hill</option>),
986 principals_options_for_select(users, User.find(4))
1013 principals_options_for_select(users, User.find(4))
987 end
1014 end
988
1015
989 def test_principals_options_for_select_with_users_and_groups
1016 def test_principals_options_for_select_with_users_and_groups
990 User.current = nil
1017 User.current = nil
991 users = [User.find(2), Group.find(11), User.find(4), Group.find(10)]
1018 users = [User.find(2), Group.find(11), User.find(4), Group.find(10)]
992 assert_equal %(<option value="2">John Smith</option><option value="4">Robert Hill</option>) +
1019 assert_equal %(<option value="2">John Smith</option><option value="4">Robert Hill</option>) +
993 %(<optgroup label="Groups"><option value="10">A Team</option><option value="11">B Team</option></optgroup>),
1020 %(<optgroup label="Groups"><option value="10">A Team</option><option value="11">B Team</option></optgroup>),
994 principals_options_for_select(users)
1021 principals_options_for_select(users)
995 end
1022 end
996
1023
997 def test_principals_options_for_select_with_empty_collection
1024 def test_principals_options_for_select_with_empty_collection
998 assert_equal '', principals_options_for_select([])
1025 assert_equal '', principals_options_for_select([])
999 end
1026 end
1000
1027
1001 def test_principals_options_for_select_should_include_me_option_when_current_user_is_in_collection
1028 def test_principals_options_for_select_should_include_me_option_when_current_user_is_in_collection
1002 users = [User.find(2), User.find(4)]
1029 users = [User.find(2), User.find(4)]
1003 User.current = User.find(4)
1030 User.current = User.find(4)
1004 assert_include '<option value="4"><< me >></option>', principals_options_for_select(users)
1031 assert_include '<option value="4"><< me >></option>', principals_options_for_select(users)
1005 end
1032 end
1006 end
1033 end
General Comments 0
You need to be logged in to leave comments. Login now