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