##// END OF EJS Templates
Adds support for cross project Redmine links (#7409)....
Jean-Philippe Lang -
r4638:777ccf1328c9
parent child
Show More
@@ -1,921 +1,930
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 '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 project if active
113 # Generates a link to a project if active
114 # Examples:
114 # Examples:
115 #
115 #
116 # link_to_project(project) # => link to the specified project overview
116 # link_to_project(project) # => link to the specified project overview
117 # link_to_project(project, :action=>'settings') # => link to project settings
117 # link_to_project(project, :action=>'settings') # => link to project settings
118 # link_to_project(project, {:only_path => false}, :class => "project") # => 3rd arg adds html options
118 # link_to_project(project, {:only_path => false}, :class => "project") # => 3rd arg adds html options
119 # link_to_project(project, {}, :class => "project") # => html options with default url (project overview)
119 # link_to_project(project, {}, :class => "project") # => html options with default url (project overview)
120 #
120 #
121 def link_to_project(project, options={}, html_options = nil)
121 def link_to_project(project, options={}, html_options = nil)
122 if project.active?
122 if project.active?
123 url = {:controller => 'projects', :action => 'show', :id => project}.merge(options)
123 url = {:controller => 'projects', :action => 'show', :id => project}.merge(options)
124 link_to(h(project), url, html_options)
124 link_to(h(project), url, html_options)
125 else
125 else
126 h(project)
126 h(project)
127 end
127 end
128 end
128 end
129
129
130 def toggle_link(name, id, options={})
130 def toggle_link(name, id, options={})
131 onclick = "Element.toggle('#{id}'); "
131 onclick = "Element.toggle('#{id}'); "
132 onclick << (options[:focus] ? "Form.Element.focus('#{options[:focus]}'); " : "this.blur(); ")
132 onclick << (options[:focus] ? "Form.Element.focus('#{options[:focus]}'); " : "this.blur(); ")
133 onclick << "return false;"
133 onclick << "return false;"
134 link_to(name, "#", :onclick => onclick)
134 link_to(name, "#", :onclick => onclick)
135 end
135 end
136
136
137 def image_to_function(name, function, html_options = {})
137 def image_to_function(name, function, html_options = {})
138 html_options.symbolize_keys!
138 html_options.symbolize_keys!
139 tag(:input, html_options.merge({
139 tag(:input, html_options.merge({
140 :type => "image", :src => image_path(name),
140 :type => "image", :src => image_path(name),
141 :onclick => (html_options[:onclick] ? "#{html_options[:onclick]}; " : "") + "#{function};"
141 :onclick => (html_options[:onclick] ? "#{html_options[:onclick]}; " : "") + "#{function};"
142 }))
142 }))
143 end
143 end
144
144
145 def prompt_to_remote(name, text, param, url, html_options = {})
145 def prompt_to_remote(name, text, param, url, html_options = {})
146 html_options[:onclick] = "promptToRemote('#{text}', '#{param}', '#{url_for(url)}'); return false;"
146 html_options[:onclick] = "promptToRemote('#{text}', '#{param}', '#{url_for(url)}'); return false;"
147 link_to name, {}, html_options
147 link_to name, {}, html_options
148 end
148 end
149
149
150 def format_activity_title(text)
150 def format_activity_title(text)
151 h(truncate_single_line(text, :length => 100))
151 h(truncate_single_line(text, :length => 100))
152 end
152 end
153
153
154 def format_activity_day(date)
154 def format_activity_day(date)
155 date == Date.today ? l(:label_today).titleize : format_date(date)
155 date == Date.today ? l(:label_today).titleize : format_date(date)
156 end
156 end
157
157
158 def format_activity_description(text)
158 def format_activity_description(text)
159 h(truncate(text.to_s, :length => 120).gsub(%r{[\r\n]*<(pre|code)>.*$}m, '...')).gsub(/[\r\n]+/, "<br />")
159 h(truncate(text.to_s, :length => 120).gsub(%r{[\r\n]*<(pre|code)>.*$}m, '...')).gsub(/[\r\n]+/, "<br />")
160 end
160 end
161
161
162 def format_version_name(version)
162 def format_version_name(version)
163 if version.project == @project
163 if version.project == @project
164 h(version)
164 h(version)
165 else
165 else
166 h("#{version.project} - #{version}")
166 h("#{version.project} - #{version}")
167 end
167 end
168 end
168 end
169
169
170 def due_date_distance_in_words(date)
170 def due_date_distance_in_words(date)
171 if date
171 if date
172 l((date < Date.today ? :label_roadmap_overdue : :label_roadmap_due_in), distance_of_date_in_words(Date.today, date))
172 l((date < Date.today ? :label_roadmap_overdue : :label_roadmap_due_in), distance_of_date_in_words(Date.today, date))
173 end
173 end
174 end
174 end
175
175
176 def render_page_hierarchy(pages, node=nil)
176 def render_page_hierarchy(pages, node=nil)
177 content = ''
177 content = ''
178 if pages[node]
178 if pages[node]
179 content << "<ul class=\"pages-hierarchy\">\n"
179 content << "<ul class=\"pages-hierarchy\">\n"
180 pages[node].each do |page|
180 pages[node].each do |page|
181 content << "<li>"
181 content << "<li>"
182 content << link_to(h(page.pretty_title), {:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title},
182 content << link_to(h(page.pretty_title), {:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title},
183 :title => (page.respond_to?(:updated_on) ? l(:label_updated_time, distance_of_time_in_words(Time.now, page.updated_on)) : nil))
183 :title => (page.respond_to?(:updated_on) ? l(:label_updated_time, distance_of_time_in_words(Time.now, page.updated_on)) : nil))
184 content << "\n" + render_page_hierarchy(pages, page.id) if pages[page.id]
184 content << "\n" + render_page_hierarchy(pages, page.id) if pages[page.id]
185 content << "</li>\n"
185 content << "</li>\n"
186 end
186 end
187 content << "</ul>\n"
187 content << "</ul>\n"
188 end
188 end
189 content
189 content
190 end
190 end
191
191
192 # Renders flash messages
192 # Renders flash messages
193 def render_flash_messages
193 def render_flash_messages
194 s = ''
194 s = ''
195 flash.each do |k,v|
195 flash.each do |k,v|
196 s << content_tag('div', v, :class => "flash #{k}")
196 s << content_tag('div', v, :class => "flash #{k}")
197 end
197 end
198 s
198 s
199 end
199 end
200
200
201 # Renders tabs and their content
201 # Renders tabs and their content
202 def render_tabs(tabs)
202 def render_tabs(tabs)
203 if tabs.any?
203 if tabs.any?
204 render :partial => 'common/tabs', :locals => {:tabs => tabs}
204 render :partial => 'common/tabs', :locals => {:tabs => tabs}
205 else
205 else
206 content_tag 'p', l(:label_no_data), :class => "nodata"
206 content_tag 'p', l(:label_no_data), :class => "nodata"
207 end
207 end
208 end
208 end
209
209
210 # Renders the project quick-jump box
210 # Renders the project quick-jump box
211 def render_project_jump_box
211 def render_project_jump_box
212 # Retrieve them now to avoid a COUNT query
212 # Retrieve them now to avoid a COUNT query
213 projects = User.current.projects.all
213 projects = User.current.projects.all
214 if projects.any?
214 if projects.any?
215 s = '<select onchange="if (this.value != \'\') { window.location = this.value; }">' +
215 s = '<select onchange="if (this.value != \'\') { window.location = this.value; }">' +
216 "<option value=''>#{ l(:label_jump_to_a_project) }</option>" +
216 "<option value=''>#{ l(:label_jump_to_a_project) }</option>" +
217 '<option value="" disabled="disabled">---</option>'
217 '<option value="" disabled="disabled">---</option>'
218 s << project_tree_options_for_select(projects, :selected => @project) do |p|
218 s << project_tree_options_for_select(projects, :selected => @project) do |p|
219 { :value => url_for(:controller => 'projects', :action => 'show', :id => p, :jump => current_menu_item) }
219 { :value => url_for(:controller => 'projects', :action => 'show', :id => p, :jump => current_menu_item) }
220 end
220 end
221 s << '</select>'
221 s << '</select>'
222 s
222 s
223 end
223 end
224 end
224 end
225
225
226 def project_tree_options_for_select(projects, options = {})
226 def project_tree_options_for_select(projects, options = {})
227 s = ''
227 s = ''
228 project_tree(projects) do |project, level|
228 project_tree(projects) do |project, level|
229 name_prefix = (level > 0 ? ('&nbsp;' * 2 * level + '&#187; ') : '')
229 name_prefix = (level > 0 ? ('&nbsp;' * 2 * level + '&#187; ') : '')
230 tag_options = {:value => project.id}
230 tag_options = {:value => project.id}
231 if project == options[:selected] || (options[:selected].respond_to?(:include?) && options[:selected].include?(project))
231 if project == options[:selected] || (options[:selected].respond_to?(:include?) && options[:selected].include?(project))
232 tag_options[:selected] = 'selected'
232 tag_options[:selected] = 'selected'
233 else
233 else
234 tag_options[:selected] = nil
234 tag_options[:selected] = nil
235 end
235 end
236 tag_options.merge!(yield(project)) if block_given?
236 tag_options.merge!(yield(project)) if block_given?
237 s << content_tag('option', name_prefix + h(project), tag_options)
237 s << content_tag('option', name_prefix + h(project), tag_options)
238 end
238 end
239 s
239 s
240 end
240 end
241
241
242 # Yields the given block for each project with its level in the tree
242 # Yields the given block for each project with its level in the tree
243 #
243 #
244 # Wrapper for Project#project_tree
244 # Wrapper for Project#project_tree
245 def project_tree(projects, &block)
245 def project_tree(projects, &block)
246 Project.project_tree(projects, &block)
246 Project.project_tree(projects, &block)
247 end
247 end
248
248
249 def project_nested_ul(projects, &block)
249 def project_nested_ul(projects, &block)
250 s = ''
250 s = ''
251 if projects.any?
251 if projects.any?
252 ancestors = []
252 ancestors = []
253 projects.sort_by(&:lft).each do |project|
253 projects.sort_by(&:lft).each do |project|
254 if (ancestors.empty? || project.is_descendant_of?(ancestors.last))
254 if (ancestors.empty? || project.is_descendant_of?(ancestors.last))
255 s << "<ul>\n"
255 s << "<ul>\n"
256 else
256 else
257 ancestors.pop
257 ancestors.pop
258 s << "</li>"
258 s << "</li>"
259 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
259 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
260 ancestors.pop
260 ancestors.pop
261 s << "</ul></li>\n"
261 s << "</ul></li>\n"
262 end
262 end
263 end
263 end
264 s << "<li>"
264 s << "<li>"
265 s << yield(project).to_s
265 s << yield(project).to_s
266 ancestors << project
266 ancestors << project
267 end
267 end
268 s << ("</li></ul>\n" * ancestors.size)
268 s << ("</li></ul>\n" * ancestors.size)
269 end
269 end
270 s
270 s
271 end
271 end
272
272
273 def principals_check_box_tags(name, principals)
273 def principals_check_box_tags(name, principals)
274 s = ''
274 s = ''
275 principals.sort.each do |principal|
275 principals.sort.each do |principal|
276 s << "<label>#{ check_box_tag name, principal.id, false } #{h principal}</label>\n"
276 s << "<label>#{ check_box_tag name, principal.id, false } #{h principal}</label>\n"
277 end
277 end
278 s
278 s
279 end
279 end
280
280
281 # Truncates and returns the string as a single line
281 # Truncates and returns the string as a single line
282 def truncate_single_line(string, *args)
282 def truncate_single_line(string, *args)
283 truncate(string.to_s, *args).gsub(%r{[\r\n]+}m, ' ')
283 truncate(string.to_s, *args).gsub(%r{[\r\n]+}m, ' ')
284 end
284 end
285
285
286 # Truncates at line break after 250 characters or options[:length]
286 # Truncates at line break after 250 characters or options[:length]
287 def truncate_lines(string, options={})
287 def truncate_lines(string, options={})
288 length = options[:length] || 250
288 length = options[:length] || 250
289 if string.to_s =~ /\A(.{#{length}}.*?)$/m
289 if string.to_s =~ /\A(.{#{length}}.*?)$/m
290 "#{$1}..."
290 "#{$1}..."
291 else
291 else
292 string
292 string
293 end
293 end
294 end
294 end
295
295
296 def html_hours(text)
296 def html_hours(text)
297 text.gsub(%r{(\d+)\.(\d+)}, '<span class="hours hours-int">\1</span><span class="hours hours-dec">.\2</span>')
297 text.gsub(%r{(\d+)\.(\d+)}, '<span class="hours hours-int">\1</span><span class="hours hours-dec">.\2</span>')
298 end
298 end
299
299
300 def authoring(created, author, options={})
300 def authoring(created, author, options={})
301 l(options[:label] || :label_added_time_by, :author => link_to_user(author), :age => time_tag(created))
301 l(options[:label] || :label_added_time_by, :author => link_to_user(author), :age => time_tag(created))
302 end
302 end
303
303
304 def time_tag(time)
304 def time_tag(time)
305 text = distance_of_time_in_words(Time.now, time)
305 text = distance_of_time_in_words(Time.now, time)
306 if @project
306 if @project
307 link_to(text, {:controller => 'activities', :action => 'index', :id => @project, :from => time.to_date}, :title => format_time(time))
307 link_to(text, {:controller => 'activities', :action => 'index', :id => @project, :from => time.to_date}, :title => format_time(time))
308 else
308 else
309 content_tag('acronym', text, :title => format_time(time))
309 content_tag('acronym', text, :title => format_time(time))
310 end
310 end
311 end
311 end
312
312
313 def syntax_highlight(name, content)
313 def syntax_highlight(name, content)
314 Redmine::SyntaxHighlighting.highlight_by_filename(content, name)
314 Redmine::SyntaxHighlighting.highlight_by_filename(content, name)
315 end
315 end
316
316
317 def to_path_param(path)
317 def to_path_param(path)
318 path.to_s.split(%r{[/\\]}).select {|p| !p.blank?}
318 path.to_s.split(%r{[/\\]}).select {|p| !p.blank?}
319 end
319 end
320
320
321 def pagination_links_full(paginator, count=nil, options={})
321 def pagination_links_full(paginator, count=nil, options={})
322 page_param = options.delete(:page_param) || :page
322 page_param = options.delete(:page_param) || :page
323 per_page_links = options.delete(:per_page_links)
323 per_page_links = options.delete(:per_page_links)
324 url_param = params.dup
324 url_param = params.dup
325 # don't reuse query params if filters are present
325 # don't reuse query params if filters are present
326 url_param.merge!(:fields => nil, :values => nil, :operators => nil) if url_param.delete(:set_filter)
326 url_param.merge!(:fields => nil, :values => nil, :operators => nil) if url_param.delete(:set_filter)
327
327
328 html = ''
328 html = ''
329 if paginator.current.previous
329 if paginator.current.previous
330 html << link_to_remote_content_update('&#171; ' + l(:label_previous), url_param.merge(page_param => paginator.current.previous)) + ' '
330 html << link_to_remote_content_update('&#171; ' + l(:label_previous), url_param.merge(page_param => paginator.current.previous)) + ' '
331 end
331 end
332
332
333 html << (pagination_links_each(paginator, options) do |n|
333 html << (pagination_links_each(paginator, options) do |n|
334 link_to_remote_content_update(n.to_s, url_param.merge(page_param => n))
334 link_to_remote_content_update(n.to_s, url_param.merge(page_param => n))
335 end || '')
335 end || '')
336
336
337 if paginator.current.next
337 if paginator.current.next
338 html << ' ' + link_to_remote_content_update((l(:label_next) + ' &#187;'), url_param.merge(page_param => paginator.current.next))
338 html << ' ' + link_to_remote_content_update((l(:label_next) + ' &#187;'), url_param.merge(page_param => paginator.current.next))
339 end
339 end
340
340
341 unless count.nil?
341 unless count.nil?
342 html << " (#{paginator.current.first_item}-#{paginator.current.last_item}/#{count})"
342 html << " (#{paginator.current.first_item}-#{paginator.current.last_item}/#{count})"
343 if per_page_links != false && links = per_page_links(paginator.items_per_page)
343 if per_page_links != false && links = per_page_links(paginator.items_per_page)
344 html << " | #{links}"
344 html << " | #{links}"
345 end
345 end
346 end
346 end
347
347
348 html
348 html
349 end
349 end
350
350
351 def per_page_links(selected=nil)
351 def per_page_links(selected=nil)
352 url_param = params.dup
352 url_param = params.dup
353 url_param.clear if url_param.has_key?(:set_filter)
353 url_param.clear if url_param.has_key?(:set_filter)
354
354
355 links = Setting.per_page_options_array.collect do |n|
355 links = Setting.per_page_options_array.collect do |n|
356 n == selected ? n : link_to_remote(n, {:update => "content",
356 n == selected ? n : link_to_remote(n, {:update => "content",
357 :url => params.dup.merge(:per_page => n),
357 :url => params.dup.merge(:per_page => n),
358 :method => :get},
358 :method => :get},
359 {:href => url_for(url_param.merge(:per_page => n))})
359 {:href => url_for(url_param.merge(:per_page => n))})
360 end
360 end
361 links.size > 1 ? l(:label_display_per_page, links.join(', ')) : nil
361 links.size > 1 ? l(:label_display_per_page, links.join(', ')) : nil
362 end
362 end
363
363
364 def reorder_links(name, url)
364 def reorder_links(name, url)
365 link_to(image_tag('2uparrow.png', :alt => l(:label_sort_highest)), url.merge({"#{name}[move_to]" => 'highest'}), :method => :post, :title => l(:label_sort_highest)) +
365 link_to(image_tag('2uparrow.png', :alt => l(:label_sort_highest)), url.merge({"#{name}[move_to]" => 'highest'}), :method => :post, :title => l(:label_sort_highest)) +
366 link_to(image_tag('1uparrow.png', :alt => l(:label_sort_higher)), url.merge({"#{name}[move_to]" => 'higher'}), :method => :post, :title => l(:label_sort_higher)) +
366 link_to(image_tag('1uparrow.png', :alt => l(:label_sort_higher)), url.merge({"#{name}[move_to]" => 'higher'}), :method => :post, :title => l(:label_sort_higher)) +
367 link_to(image_tag('1downarrow.png', :alt => l(:label_sort_lower)), url.merge({"#{name}[move_to]" => 'lower'}), :method => :post, :title => l(:label_sort_lower)) +
367 link_to(image_tag('1downarrow.png', :alt => l(:label_sort_lower)), url.merge({"#{name}[move_to]" => 'lower'}), :method => :post, :title => l(:label_sort_lower)) +
368 link_to(image_tag('2downarrow.png', :alt => l(:label_sort_lowest)), url.merge({"#{name}[move_to]" => 'lowest'}), :method => :post, :title => l(:label_sort_lowest))
368 link_to(image_tag('2downarrow.png', :alt => l(:label_sort_lowest)), url.merge({"#{name}[move_to]" => 'lowest'}), :method => :post, :title => l(:label_sort_lowest))
369 end
369 end
370
370
371 def breadcrumb(*args)
371 def breadcrumb(*args)
372 elements = args.flatten
372 elements = args.flatten
373 elements.any? ? content_tag('p', args.join(' &#187; ') + ' &#187; ', :class => 'breadcrumb') : nil
373 elements.any? ? content_tag('p', args.join(' &#187; ') + ' &#187; ', :class => 'breadcrumb') : nil
374 end
374 end
375
375
376 def other_formats_links(&block)
376 def other_formats_links(&block)
377 concat('<p class="other-formats">' + l(:label_export_to))
377 concat('<p class="other-formats">' + l(:label_export_to))
378 yield Redmine::Views::OtherFormatsBuilder.new(self)
378 yield Redmine::Views::OtherFormatsBuilder.new(self)
379 concat('</p>')
379 concat('</p>')
380 end
380 end
381
381
382 def page_header_title
382 def page_header_title
383 if @project.nil? || @project.new_record?
383 if @project.nil? || @project.new_record?
384 h(Setting.app_title)
384 h(Setting.app_title)
385 else
385 else
386 b = []
386 b = []
387 ancestors = (@project.root? ? [] : @project.ancestors.visible)
387 ancestors = (@project.root? ? [] : @project.ancestors.visible)
388 if ancestors.any?
388 if ancestors.any?
389 root = ancestors.shift
389 root = ancestors.shift
390 b << link_to_project(root, {:jump => current_menu_item}, :class => 'root')
390 b << link_to_project(root, {:jump => current_menu_item}, :class => 'root')
391 if ancestors.size > 2
391 if ancestors.size > 2
392 b << '&#8230;'
392 b << '&#8230;'
393 ancestors = ancestors[-2, 2]
393 ancestors = ancestors[-2, 2]
394 end
394 end
395 b += ancestors.collect {|p| link_to_project(p, {:jump => current_menu_item}, :class => 'ancestor') }
395 b += ancestors.collect {|p| link_to_project(p, {:jump => current_menu_item}, :class => 'ancestor') }
396 end
396 end
397 b << h(@project)
397 b << h(@project)
398 b.join(' &#187; ')
398 b.join(' &#187; ')
399 end
399 end
400 end
400 end
401
401
402 def html_title(*args)
402 def html_title(*args)
403 if args.empty?
403 if args.empty?
404 title = []
404 title = []
405 title << @project.name if @project
405 title << @project.name if @project
406 title += @html_title if @html_title
406 title += @html_title if @html_title
407 title << Setting.app_title
407 title << Setting.app_title
408 title.select {|t| !t.blank? }.join(' - ')
408 title.select {|t| !t.blank? }.join(' - ')
409 else
409 else
410 @html_title ||= []
410 @html_title ||= []
411 @html_title += args
411 @html_title += args
412 end
412 end
413 end
413 end
414
414
415 # Returns the theme, controller name, and action as css classes for the
415 # Returns the theme, controller name, and action as css classes for the
416 # HTML body.
416 # HTML body.
417 def body_css_classes
417 def body_css_classes
418 css = []
418 css = []
419 if theme = Redmine::Themes.theme(Setting.ui_theme)
419 if theme = Redmine::Themes.theme(Setting.ui_theme)
420 css << 'theme-' + theme.name
420 css << 'theme-' + theme.name
421 end
421 end
422
422
423 css << 'controller-' + params[:controller]
423 css << 'controller-' + params[:controller]
424 css << 'action-' + params[:action]
424 css << 'action-' + params[:action]
425 css.join(' ')
425 css.join(' ')
426 end
426 end
427
427
428 def accesskey(s)
428 def accesskey(s)
429 Redmine::AccessKeys.key_for s
429 Redmine::AccessKeys.key_for s
430 end
430 end
431
431
432 # Formats text according to system settings.
432 # Formats text according to system settings.
433 # 2 ways to call this method:
433 # 2 ways to call this method:
434 # * with a String: textilizable(text, options)
434 # * with a String: textilizable(text, options)
435 # * with an object and one of its attribute: textilizable(issue, :description, options)
435 # * with an object and one of its attribute: textilizable(issue, :description, options)
436 def textilizable(*args)
436 def textilizable(*args)
437 options = args.last.is_a?(Hash) ? args.pop : {}
437 options = args.last.is_a?(Hash) ? args.pop : {}
438 case args.size
438 case args.size
439 when 1
439 when 1
440 obj = options[:object]
440 obj = options[:object]
441 text = args.shift
441 text = args.shift
442 when 2
442 when 2
443 obj = args.shift
443 obj = args.shift
444 attr = args.shift
444 attr = args.shift
445 text = obj.send(attr).to_s
445 text = obj.send(attr).to_s
446 else
446 else
447 raise ArgumentError, 'invalid arguments to textilizable'
447 raise ArgumentError, 'invalid arguments to textilizable'
448 end
448 end
449 return '' if text.blank?
449 return '' if text.blank?
450 project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
450 project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
451 only_path = options.delete(:only_path) == false ? false : true
451 only_path = options.delete(:only_path) == false ? false : true
452
452
453 text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text, :object => obj, :attribute => attr) { |macro, args| exec_macro(macro, obj, args) }
453 text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text, :object => obj, :attribute => attr) { |macro, args| exec_macro(macro, obj, args) }
454
454
455 @parsed_headings = []
455 @parsed_headings = []
456 text = parse_non_pre_blocks(text) do |text|
456 text = parse_non_pre_blocks(text) do |text|
457 [:parse_inline_attachments, :parse_wiki_links, :parse_redmine_links, :parse_headings].each do |method_name|
457 [:parse_inline_attachments, :parse_wiki_links, :parse_redmine_links, :parse_headings].each do |method_name|
458 send method_name, text, project, obj, attr, only_path, options
458 send method_name, text, project, obj, attr, only_path, options
459 end
459 end
460 end
460 end
461
461
462 if @parsed_headings.any?
462 if @parsed_headings.any?
463 replace_toc(text, @parsed_headings)
463 replace_toc(text, @parsed_headings)
464 end
464 end
465
465
466 text
466 text
467 end
467 end
468
468
469 def parse_non_pre_blocks(text)
469 def parse_non_pre_blocks(text)
470 s = StringScanner.new(text)
470 s = StringScanner.new(text)
471 tags = []
471 tags = []
472 parsed = ''
472 parsed = ''
473 while !s.eos?
473 while !s.eos?
474 s.scan(/(.*?)(<(\/)?(pre|code)(.*?)>|\z)/im)
474 s.scan(/(.*?)(<(\/)?(pre|code)(.*?)>|\z)/im)
475 text, full_tag, closing, tag = s[1], s[2], s[3], s[4]
475 text, full_tag, closing, tag = s[1], s[2], s[3], s[4]
476 if tags.empty?
476 if tags.empty?
477 yield text
477 yield text
478 end
478 end
479 parsed << text
479 parsed << text
480 if tag
480 if tag
481 if closing
481 if closing
482 if tags.last == tag.downcase
482 if tags.last == tag.downcase
483 tags.pop
483 tags.pop
484 end
484 end
485 else
485 else
486 tags << tag.downcase
486 tags << tag.downcase
487 end
487 end
488 parsed << full_tag
488 parsed << full_tag
489 end
489 end
490 end
490 end
491 # Close any non closing tags
491 # Close any non closing tags
492 while tag = tags.pop
492 while tag = tags.pop
493 parsed << "</#{tag}>"
493 parsed << "</#{tag}>"
494 end
494 end
495 parsed
495 parsed
496 end
496 end
497
497
498 def parse_inline_attachments(text, project, obj, attr, only_path, options)
498 def parse_inline_attachments(text, project, obj, attr, only_path, options)
499 # when using an image link, try to use an attachment, if possible
499 # when using an image link, try to use an attachment, if possible
500 if options[:attachments] || (obj && obj.respond_to?(:attachments))
500 if options[:attachments] || (obj && obj.respond_to?(:attachments))
501 attachments = nil
501 attachments = nil
502 text.gsub!(/src="([^\/"]+\.(bmp|gif|jpg|jpeg|png))"(\s+alt="([^"]*)")?/i) do |m|
502 text.gsub!(/src="([^\/"]+\.(bmp|gif|jpg|jpeg|png))"(\s+alt="([^"]*)")?/i) do |m|
503 filename, ext, alt, alttext = $1.downcase, $2, $3, $4
503 filename, ext, alt, alttext = $1.downcase, $2, $3, $4
504 attachments ||= (options[:attachments] || obj.attachments).sort_by(&:created_on).reverse
504 attachments ||= (options[:attachments] || obj.attachments).sort_by(&:created_on).reverse
505 # search for the picture in attachments
505 # search for the picture in attachments
506 if found = attachments.detect { |att| att.filename.downcase == filename }
506 if found = attachments.detect { |att| att.filename.downcase == filename }
507 image_url = url_for :only_path => only_path, :controller => 'attachments', :action => 'download', :id => found
507 image_url = url_for :only_path => only_path, :controller => 'attachments', :action => 'download', :id => found
508 desc = found.description.to_s.gsub('"', '')
508 desc = found.description.to_s.gsub('"', '')
509 if !desc.blank? && alttext.blank?
509 if !desc.blank? && alttext.blank?
510 alt = " title=\"#{desc}\" alt=\"#{desc}\""
510 alt = " title=\"#{desc}\" alt=\"#{desc}\""
511 end
511 end
512 "src=\"#{image_url}\"#{alt}"
512 "src=\"#{image_url}\"#{alt}"
513 else
513 else
514 m
514 m
515 end
515 end
516 end
516 end
517 end
517 end
518 end
518 end
519
519
520 # Wiki links
520 # Wiki links
521 #
521 #
522 # Examples:
522 # Examples:
523 # [[mypage]]
523 # [[mypage]]
524 # [[mypage|mytext]]
524 # [[mypage|mytext]]
525 # wiki links can refer other project wikis, using project name or identifier:
525 # wiki links can refer other project wikis, using project name or identifier:
526 # [[project:]] -> wiki starting page
526 # [[project:]] -> wiki starting page
527 # [[project:|mytext]]
527 # [[project:|mytext]]
528 # [[project:mypage]]
528 # [[project:mypage]]
529 # [[project:mypage|mytext]]
529 # [[project:mypage|mytext]]
530 def parse_wiki_links(text, project, obj, attr, only_path, options)
530 def parse_wiki_links(text, project, obj, attr, only_path, options)
531 text.gsub!(/(!)?(\[\[([^\]\n\|]+)(\|([^\]\n\|]+))?\]\])/) do |m|
531 text.gsub!(/(!)?(\[\[([^\]\n\|]+)(\|([^\]\n\|]+))?\]\])/) do |m|
532 link_project = project
532 link_project = project
533 esc, all, page, title = $1, $2, $3, $5
533 esc, all, page, title = $1, $2, $3, $5
534 if esc.nil?
534 if esc.nil?
535 if page =~ /^([^\:]+)\:(.*)$/
535 if page =~ /^([^\:]+)\:(.*)$/
536 link_project = Project.find_by_identifier($1) || Project.find_by_name($1)
536 link_project = Project.find_by_identifier($1) || Project.find_by_name($1)
537 page = $2
537 page = $2
538 title ||= $1 if page.blank?
538 title ||= $1 if page.blank?
539 end
539 end
540
540
541 if link_project && link_project.wiki
541 if link_project && link_project.wiki
542 # extract anchor
542 # extract anchor
543 anchor = nil
543 anchor = nil
544 if page =~ /^(.+?)\#(.+)$/
544 if page =~ /^(.+?)\#(.+)$/
545 page, anchor = $1, $2
545 page, anchor = $1, $2
546 end
546 end
547 # check if page exists
547 # check if page exists
548 wiki_page = link_project.wiki.find_page(page)
548 wiki_page = link_project.wiki.find_page(page)
549 url = case options[:wiki_links]
549 url = case options[:wiki_links]
550 when :local; "#{title}.html"
550 when :local; "#{title}.html"
551 when :anchor; "##{title}" # used for single-file wiki export
551 when :anchor; "##{title}" # used for single-file wiki export
552 else
552 else
553 wiki_page_id = page.present? ? Wiki.titleize(page) : nil
553 wiki_page_id = page.present? ? Wiki.titleize(page) : nil
554 url_for(:only_path => only_path, :controller => 'wiki', :action => 'show', :project_id => link_project, :id => wiki_page_id, :anchor => anchor)
554 url_for(:only_path => only_path, :controller => 'wiki', :action => 'show', :project_id => link_project, :id => wiki_page_id, :anchor => anchor)
555 end
555 end
556 link_to((title || page), url, :class => ('wiki-page' + (wiki_page ? '' : ' new')))
556 link_to((title || page), url, :class => ('wiki-page' + (wiki_page ? '' : ' new')))
557 else
557 else
558 # project or wiki doesn't exist
558 # project or wiki doesn't exist
559 all
559 all
560 end
560 end
561 else
561 else
562 all
562 all
563 end
563 end
564 end
564 end
565 end
565 end
566
566
567 # Redmine links
567 # Redmine links
568 #
568 #
569 # Examples:
569 # Examples:
570 # Issues:
570 # Issues:
571 # #52 -> Link to issue #52
571 # #52 -> Link to issue #52
572 # Changesets:
572 # Changesets:
573 # r52 -> Link to revision 52
573 # r52 -> Link to revision 52
574 # commit:a85130f -> Link to scmid starting with a85130f
574 # commit:a85130f -> Link to scmid starting with a85130f
575 # Documents:
575 # Documents:
576 # document#17 -> Link to document with id 17
576 # document#17 -> Link to document with id 17
577 # document:Greetings -> Link to the document with title "Greetings"
577 # document:Greetings -> Link to the document with title "Greetings"
578 # document:"Some document" -> Link to the document with title "Some document"
578 # document:"Some document" -> Link to the document with title "Some document"
579 # Versions:
579 # Versions:
580 # version#3 -> Link to version with id 3
580 # version#3 -> Link to version with id 3
581 # version:1.0.0 -> Link to version named "1.0.0"
581 # version:1.0.0 -> Link to version named "1.0.0"
582 # version:"1.0 beta 2" -> Link to version named "1.0 beta 2"
582 # version:"1.0 beta 2" -> Link to version named "1.0 beta 2"
583 # Attachments:
583 # Attachments:
584 # attachment:file.zip -> Link to the attachment of the current object named file.zip
584 # attachment:file.zip -> Link to the attachment of the current object named file.zip
585 # Source files:
585 # Source files:
586 # source:some/file -> Link to the file located at /some/file in the project's repository
586 # source:some/file -> Link to the file located at /some/file in the project's repository
587 # source:some/file@52 -> Link to the file's revision 52
587 # source:some/file@52 -> Link to the file's revision 52
588 # source:some/file#L120 -> Link to line 120 of the file
588 # source:some/file#L120 -> Link to line 120 of the file
589 # source:some/file@52#L120 -> Link to line 120 of the file's revision 52
589 # source:some/file@52#L120 -> Link to line 120 of the file's revision 52
590 # export:some/file -> Force the download of the file
590 # export:some/file -> Force the download of the file
591 # Forum messages:
591 # Forum messages:
592 # message#1218 -> Link to message with id 1218
592 # message#1218 -> Link to message with id 1218
593 #
594 # Links can refer other objects from other projects, using project identifier:
595 # identifier:r52
596 # identifier:document:"Some document"
597 # identifier:version:1.0.0
598 # identifier:source:some/file
593 def parse_redmine_links(text, project, obj, attr, only_path, options)
599 def parse_redmine_links(text, project, obj, attr, only_path, options)
594 text.gsub!(%r{([\s\(,\-\[\>]|^)(!)?(attachment|document|version|commit|source|export|message|project)?((#|r)(\d+)|(:)([^"\s<>][^\s<>]*?|"[^"]+?"))(?=(?=[[:punct:]]\W)|,|\s|\]|<|$)}) do |m|
600 text.gsub!(%r{([\s\(,\-\[\>]|^)(!)?(([a-z0-9\-]+):)?(attachment|document|version|commit|source|export|message|project)?((#|r)(\d+)|(:)([^"\s<>][^\s<>]*?|"[^"]+?"))(?=(?=[[:punct:]]\W)|,|\s|\]|<|$)}) do |m|
595 leading, esc, prefix, sep, identifier = $1, $2, $3, $5 || $7, $6 || $8
601 leading, esc, project_prefix, project_identifier, prefix, sep, identifier = $1, $2, $3, $4, $5, $7 || $9, $8 || $10
596 link = nil
602 link = nil
603 if project_identifier
604 project = Project.visible.find_by_identifier(project_identifier)
605 end
597 if esc.nil?
606 if esc.nil?
598 if prefix.nil? && sep == 'r'
607 if prefix.nil? && sep == 'r'
599 if project && (changeset = project.changesets.find_by_revision(identifier))
608 if project && (changeset = project.changesets.find_by_revision(identifier))
600 link = link_to("r#{identifier}", {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :rev => changeset.revision},
609 link = link_to("#{project_prefix}r#{identifier}", {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :rev => changeset.revision},
601 :class => 'changeset',
610 :class => 'changeset',
602 :title => truncate_single_line(changeset.comments, :length => 100))
611 :title => truncate_single_line(changeset.comments, :length => 100))
603 end
612 end
604 elsif sep == '#'
613 elsif sep == '#'
605 oid = identifier.to_i
614 oid = identifier.to_i
606 case prefix
615 case prefix
607 when nil
616 when nil
608 if issue = Issue.visible.find_by_id(oid, :include => :status)
617 if issue = Issue.visible.find_by_id(oid, :include => :status)
609 link = link_to("##{oid}", {:only_path => only_path, :controller => 'issues', :action => 'show', :id => oid},
618 link = link_to("##{oid}", {:only_path => only_path, :controller => 'issues', :action => 'show', :id => oid},
610 :class => issue.css_classes,
619 :class => issue.css_classes,
611 :title => "#{truncate(issue.subject, :length => 100)} (#{issue.status.name})")
620 :title => "#{truncate(issue.subject, :length => 100)} (#{issue.status.name})")
612 end
621 end
613 when 'document'
622 when 'document'
614 if document = Document.find_by_id(oid, :include => [:project], :conditions => Project.visible_by(User.current))
623 if document = Document.find_by_id(oid, :include => [:project], :conditions => Project.visible_by(User.current))
615 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
624 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
616 :class => 'document'
625 :class => 'document'
617 end
626 end
618 when 'version'
627 when 'version'
619 if version = Version.find_by_id(oid, :include => [:project], :conditions => Project.visible_by(User.current))
628 if version = Version.find_by_id(oid, :include => [:project], :conditions => Project.visible_by(User.current))
620 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
629 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
621 :class => 'version'
630 :class => 'version'
622 end
631 end
623 when 'message'
632 when 'message'
624 if message = Message.find_by_id(oid, :include => [:parent, {:board => :project}], :conditions => Project.visible_by(User.current))
633 if message = Message.find_by_id(oid, :include => [:parent, {:board => :project}], :conditions => Project.visible_by(User.current))
625 link = link_to h(truncate(message.subject, :length => 60)), {:only_path => only_path,
634 link = link_to h(truncate(message.subject, :length => 60)), {:only_path => only_path,
626 :controller => 'messages',
635 :controller => 'messages',
627 :action => 'show',
636 :action => 'show',
628 :board_id => message.board,
637 :board_id => message.board,
629 :id => message.root,
638 :id => message.root,
630 :anchor => (message.parent ? "message-#{message.id}" : nil)},
639 :anchor => (message.parent ? "message-#{message.id}" : nil)},
631 :class => 'message'
640 :class => 'message'
632 end
641 end
633 when 'project'
642 when 'project'
634 if p = Project.visible.find_by_id(oid)
643 if p = Project.visible.find_by_id(oid)
635 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
644 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
636 end
645 end
637 end
646 end
638 elsif sep == ':'
647 elsif sep == ':'
639 # removes the double quotes if any
648 # removes the double quotes if any
640 name = identifier.gsub(%r{^"(.*)"$}, "\\1")
649 name = identifier.gsub(%r{^"(.*)"$}, "\\1")
641 case prefix
650 case prefix
642 when 'document'
651 when 'document'
643 if project && document = project.documents.find_by_title(name)
652 if project && document = project.documents.find_by_title(name)
644 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
653 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
645 :class => 'document'
654 :class => 'document'
646 end
655 end
647 when 'version'
656 when 'version'
648 if project && version = project.versions.find_by_name(name)
657 if project && version = project.versions.find_by_name(name)
649 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
658 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
650 :class => 'version'
659 :class => 'version'
651 end
660 end
652 when 'commit'
661 when 'commit'
653 if project && (changeset = project.changesets.find(:first, :conditions => ["scmid LIKE ?", "#{name}%"]))
662 if project && (changeset = project.changesets.find(:first, :conditions => ["scmid LIKE ?", "#{name}%"]))
654 link = link_to h("#{name}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :rev => changeset.identifier},
663 link = link_to h("#{project_prefix}#{name}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :rev => changeset.identifier},
655 :class => 'changeset',
664 :class => 'changeset',
656 :title => truncate_single_line(changeset.comments, :length => 100)
665 :title => truncate_single_line(changeset.comments, :length => 100)
657 end
666 end
658 when 'source', 'export'
667 when 'source', 'export'
659 if project && project.repository
668 if project && project.repository
660 name =~ %r{^[/\\]*(.*?)(@([0-9a-f]+))?(#(L\d+))?$}
669 name =~ %r{^[/\\]*(.*?)(@([0-9a-f]+))?(#(L\d+))?$}
661 path, rev, anchor = $1, $3, $5
670 path, rev, anchor = $1, $3, $5
662 link = link_to h("#{prefix}:#{name}"), {:controller => 'repositories', :action => 'entry', :id => project,
671 link = link_to h("#{project_prefix}#{prefix}:#{name}"), {:controller => 'repositories', :action => 'entry', :id => project,
663 :path => to_path_param(path),
672 :path => to_path_param(path),
664 :rev => rev,
673 :rev => rev,
665 :anchor => anchor,
674 :anchor => anchor,
666 :format => (prefix == 'export' ? 'raw' : nil)},
675 :format => (prefix == 'export' ? 'raw' : nil)},
667 :class => (prefix == 'export' ? 'source download' : 'source')
676 :class => (prefix == 'export' ? 'source download' : 'source')
668 end
677 end
669 when 'attachment'
678 when 'attachment'
670 attachments = options[:attachments] || (obj && obj.respond_to?(:attachments) ? obj.attachments : nil)
679 attachments = options[:attachments] || (obj && obj.respond_to?(:attachments) ? obj.attachments : nil)
671 if attachments && attachment = attachments.detect {|a| a.filename == name }
680 if attachments && attachment = attachments.detect {|a| a.filename == name }
672 link = link_to h(attachment.filename), {:only_path => only_path, :controller => 'attachments', :action => 'download', :id => attachment},
681 link = link_to h(attachment.filename), {:only_path => only_path, :controller => 'attachments', :action => 'download', :id => attachment},
673 :class => 'attachment'
682 :class => 'attachment'
674 end
683 end
675 when 'project'
684 when 'project'
676 if p = Project.visible.find(:first, :conditions => ["identifier = :s OR LOWER(name) = :s", {:s => name.downcase}])
685 if p = Project.visible.find(:first, :conditions => ["identifier = :s OR LOWER(name) = :s", {:s => name.downcase}])
677 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
686 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
678 end
687 end
679 end
688 end
680 end
689 end
681 end
690 end
682 leading + (link || "#{prefix}#{sep}#{identifier}")
691 leading + (link || "#{project_prefix}#{prefix}#{sep}#{identifier}")
683 end
692 end
684 end
693 end
685
694
686 HEADING_RE = /<h(1|2|3|4)( [^>]+)?>(.+?)<\/h(1|2|3|4)>/i unless const_defined?(:HEADING_RE)
695 HEADING_RE = /<h(1|2|3|4)( [^>]+)?>(.+?)<\/h(1|2|3|4)>/i unless const_defined?(:HEADING_RE)
687
696
688 # Headings and TOC
697 # Headings and TOC
689 # Adds ids and links to headings unless options[:headings] is set to false
698 # Adds ids and links to headings unless options[:headings] is set to false
690 def parse_headings(text, project, obj, attr, only_path, options)
699 def parse_headings(text, project, obj, attr, only_path, options)
691 return if options[:headings] == false
700 return if options[:headings] == false
692
701
693 text.gsub!(HEADING_RE) do
702 text.gsub!(HEADING_RE) do
694 level, attrs, content = $1.to_i, $2, $3
703 level, attrs, content = $1.to_i, $2, $3
695 item = strip_tags(content).strip
704 item = strip_tags(content).strip
696 anchor = item.gsub(%r{[^\w\s\-]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
705 anchor = item.gsub(%r{[^\w\s\-]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
697 @parsed_headings << [level, anchor, item]
706 @parsed_headings << [level, anchor, item]
698 "<h#{level} #{attrs} id=\"#{anchor}\">#{content}<a href=\"##{anchor}\" class=\"wiki-anchor\">&para;</a></h#{level}>"
707 "<h#{level} #{attrs} id=\"#{anchor}\">#{content}<a href=\"##{anchor}\" class=\"wiki-anchor\">&para;</a></h#{level}>"
699 end
708 end
700 end
709 end
701
710
702 TOC_RE = /<p>\{\{([<>]?)toc\}\}<\/p>/i unless const_defined?(:TOC_RE)
711 TOC_RE = /<p>\{\{([<>]?)toc\}\}<\/p>/i unless const_defined?(:TOC_RE)
703
712
704 # Renders the TOC with given headings
713 # Renders the TOC with given headings
705 def replace_toc(text, headings)
714 def replace_toc(text, headings)
706 text.gsub!(TOC_RE) do
715 text.gsub!(TOC_RE) do
707 if headings.empty?
716 if headings.empty?
708 ''
717 ''
709 else
718 else
710 div_class = 'toc'
719 div_class = 'toc'
711 div_class << ' right' if $1 == '>'
720 div_class << ' right' if $1 == '>'
712 div_class << ' left' if $1 == '<'
721 div_class << ' left' if $1 == '<'
713 out = "<ul class=\"#{div_class}\"><li>"
722 out = "<ul class=\"#{div_class}\"><li>"
714 root = headings.map(&:first).min
723 root = headings.map(&:first).min
715 current = root
724 current = root
716 started = false
725 started = false
717 headings.each do |level, anchor, item|
726 headings.each do |level, anchor, item|
718 if level > current
727 if level > current
719 out << '<ul><li>' * (level - current)
728 out << '<ul><li>' * (level - current)
720 elsif level < current
729 elsif level < current
721 out << "</li></ul>\n" * (current - level) + "</li><li>"
730 out << "</li></ul>\n" * (current - level) + "</li><li>"
722 elsif started
731 elsif started
723 out << '</li><li>'
732 out << '</li><li>'
724 end
733 end
725 out << "<a href=\"##{anchor}\">#{item}</a>"
734 out << "<a href=\"##{anchor}\">#{item}</a>"
726 current = level
735 current = level
727 started = true
736 started = true
728 end
737 end
729 out << '</li></ul>' * (current - root)
738 out << '</li></ul>' * (current - root)
730 out << '</li></ul>'
739 out << '</li></ul>'
731 end
740 end
732 end
741 end
733 end
742 end
734
743
735 # Same as Rails' simple_format helper without using paragraphs
744 # Same as Rails' simple_format helper without using paragraphs
736 def simple_format_without_paragraph(text)
745 def simple_format_without_paragraph(text)
737 text.to_s.
746 text.to_s.
738 gsub(/\r\n?/, "\n"). # \r\n and \r -> \n
747 gsub(/\r\n?/, "\n"). # \r\n and \r -> \n
739 gsub(/\n\n+/, "<br /><br />"). # 2+ newline -> 2 br
748 gsub(/\n\n+/, "<br /><br />"). # 2+ newline -> 2 br
740 gsub(/([^\n]\n)(?=[^\n])/, '\1<br />') # 1 newline -> br
749 gsub(/([^\n]\n)(?=[^\n])/, '\1<br />') # 1 newline -> br
741 end
750 end
742
751
743 def lang_options_for_select(blank=true)
752 def lang_options_for_select(blank=true)
744 (blank ? [["(auto)", ""]] : []) +
753 (blank ? [["(auto)", ""]] : []) +
745 valid_languages.collect{|lang| [ ll(lang.to_s, :general_lang_name), lang.to_s]}.sort{|x,y| x.last <=> y.last }
754 valid_languages.collect{|lang| [ ll(lang.to_s, :general_lang_name), lang.to_s]}.sort{|x,y| x.last <=> y.last }
746 end
755 end
747
756
748 def label_tag_for(name, option_tags = nil, options = {})
757 def label_tag_for(name, option_tags = nil, options = {})
749 label_text = l(("field_"+field.to_s.gsub(/\_id$/, "")).to_sym) + (options.delete(:required) ? @template.content_tag("span", " *", :class => "required"): "")
758 label_text = l(("field_"+field.to_s.gsub(/\_id$/, "")).to_sym) + (options.delete(:required) ? @template.content_tag("span", " *", :class => "required"): "")
750 content_tag("label", label_text)
759 content_tag("label", label_text)
751 end
760 end
752
761
753 def labelled_tabular_form_for(name, object, options, &proc)
762 def labelled_tabular_form_for(name, object, options, &proc)
754 options[:html] ||= {}
763 options[:html] ||= {}
755 options[:html][:class] = 'tabular' unless options[:html].has_key?(:class)
764 options[:html][:class] = 'tabular' unless options[:html].has_key?(:class)
756 form_for(name, object, options.merge({ :builder => TabularFormBuilder, :lang => current_language}), &proc)
765 form_for(name, object, options.merge({ :builder => TabularFormBuilder, :lang => current_language}), &proc)
757 end
766 end
758
767
759 def back_url_hidden_field_tag
768 def back_url_hidden_field_tag
760 back_url = params[:back_url] || request.env['HTTP_REFERER']
769 back_url = params[:back_url] || request.env['HTTP_REFERER']
761 back_url = CGI.unescape(back_url.to_s)
770 back_url = CGI.unescape(back_url.to_s)
762 hidden_field_tag('back_url', CGI.escape(back_url)) unless back_url.blank?
771 hidden_field_tag('back_url', CGI.escape(back_url)) unless back_url.blank?
763 end
772 end
764
773
765 def check_all_links(form_name)
774 def check_all_links(form_name)
766 link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") +
775 link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") +
767 " | " +
776 " | " +
768 link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)")
777 link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)")
769 end
778 end
770
779
771 def progress_bar(pcts, options={})
780 def progress_bar(pcts, options={})
772 pcts = [pcts, pcts] unless pcts.is_a?(Array)
781 pcts = [pcts, pcts] unless pcts.is_a?(Array)
773 pcts = pcts.collect(&:round)
782 pcts = pcts.collect(&:round)
774 pcts[1] = pcts[1] - pcts[0]
783 pcts[1] = pcts[1] - pcts[0]
775 pcts << (100 - pcts[1] - pcts[0])
784 pcts << (100 - pcts[1] - pcts[0])
776 width = options[:width] || '100px;'
785 width = options[:width] || '100px;'
777 legend = options[:legend] || ''
786 legend = options[:legend] || ''
778 content_tag('table',
787 content_tag('table',
779 content_tag('tr',
788 content_tag('tr',
780 (pcts[0] > 0 ? content_tag('td', '', :style => "width: #{pcts[0]}%;", :class => 'closed') : '') +
789 (pcts[0] > 0 ? content_tag('td', '', :style => "width: #{pcts[0]}%;", :class => 'closed') : '') +
781 (pcts[1] > 0 ? content_tag('td', '', :style => "width: #{pcts[1]}%;", :class => 'done') : '') +
790 (pcts[1] > 0 ? content_tag('td', '', :style => "width: #{pcts[1]}%;", :class => 'done') : '') +
782 (pcts[2] > 0 ? content_tag('td', '', :style => "width: #{pcts[2]}%;", :class => 'todo') : '')
791 (pcts[2] > 0 ? content_tag('td', '', :style => "width: #{pcts[2]}%;", :class => 'todo') : '')
783 ), :class => 'progress', :style => "width: #{width};") +
792 ), :class => 'progress', :style => "width: #{width};") +
784 content_tag('p', legend, :class => 'pourcent')
793 content_tag('p', legend, :class => 'pourcent')
785 end
794 end
786
795
787 def checked_image(checked=true)
796 def checked_image(checked=true)
788 if checked
797 if checked
789 image_tag 'toggle_check.png'
798 image_tag 'toggle_check.png'
790 end
799 end
791 end
800 end
792
801
793 def context_menu(url)
802 def context_menu(url)
794 unless @context_menu_included
803 unless @context_menu_included
795 content_for :header_tags do
804 content_for :header_tags do
796 javascript_include_tag('context_menu') +
805 javascript_include_tag('context_menu') +
797 stylesheet_link_tag('context_menu')
806 stylesheet_link_tag('context_menu')
798 end
807 end
799 if l(:direction) == 'rtl'
808 if l(:direction) == 'rtl'
800 content_for :header_tags do
809 content_for :header_tags do
801 stylesheet_link_tag('context_menu_rtl')
810 stylesheet_link_tag('context_menu_rtl')
802 end
811 end
803 end
812 end
804 @context_menu_included = true
813 @context_menu_included = true
805 end
814 end
806 javascript_tag "new ContextMenu('#{ url_for(url) }')"
815 javascript_tag "new ContextMenu('#{ url_for(url) }')"
807 end
816 end
808
817
809 def context_menu_link(name, url, options={})
818 def context_menu_link(name, url, options={})
810 options[:class] ||= ''
819 options[:class] ||= ''
811 if options.delete(:selected)
820 if options.delete(:selected)
812 options[:class] << ' icon-checked disabled'
821 options[:class] << ' icon-checked disabled'
813 options[:disabled] = true
822 options[:disabled] = true
814 end
823 end
815 if options.delete(:disabled)
824 if options.delete(:disabled)
816 options.delete(:method)
825 options.delete(:method)
817 options.delete(:confirm)
826 options.delete(:confirm)
818 options.delete(:onclick)
827 options.delete(:onclick)
819 options[:class] << ' disabled'
828 options[:class] << ' disabled'
820 url = '#'
829 url = '#'
821 end
830 end
822 link_to name, url, options
831 link_to name, url, options
823 end
832 end
824
833
825 def calendar_for(field_id)
834 def calendar_for(field_id)
826 include_calendar_headers_tags
835 include_calendar_headers_tags
827 image_tag("calendar.png", {:id => "#{field_id}_trigger",:class => "calendar-trigger"}) +
836 image_tag("calendar.png", {:id => "#{field_id}_trigger",:class => "calendar-trigger"}) +
828 javascript_tag("Calendar.setup({inputField : '#{field_id}', ifFormat : '%Y-%m-%d', button : '#{field_id}_trigger' });")
837 javascript_tag("Calendar.setup({inputField : '#{field_id}', ifFormat : '%Y-%m-%d', button : '#{field_id}_trigger' });")
829 end
838 end
830
839
831 def include_calendar_headers_tags
840 def include_calendar_headers_tags
832 unless @calendar_headers_tags_included
841 unless @calendar_headers_tags_included
833 @calendar_headers_tags_included = true
842 @calendar_headers_tags_included = true
834 content_for :header_tags do
843 content_for :header_tags do
835 start_of_week = case Setting.start_of_week.to_i
844 start_of_week = case Setting.start_of_week.to_i
836 when 1
845 when 1
837 'Calendar._FD = 1;' # Monday
846 'Calendar._FD = 1;' # Monday
838 when 7
847 when 7
839 'Calendar._FD = 0;' # Sunday
848 'Calendar._FD = 0;' # Sunday
840 else
849 else
841 '' # use language
850 '' # use language
842 end
851 end
843
852
844 javascript_include_tag('calendar/calendar') +
853 javascript_include_tag('calendar/calendar') +
845 javascript_include_tag("calendar/lang/calendar-#{current_language.to_s.downcase}.js") +
854 javascript_include_tag("calendar/lang/calendar-#{current_language.to_s.downcase}.js") +
846 javascript_tag(start_of_week) +
855 javascript_tag(start_of_week) +
847 javascript_include_tag('calendar/calendar-setup') +
856 javascript_include_tag('calendar/calendar-setup') +
848 stylesheet_link_tag('calendar')
857 stylesheet_link_tag('calendar')
849 end
858 end
850 end
859 end
851 end
860 end
852
861
853 def content_for(name, content = nil, &block)
862 def content_for(name, content = nil, &block)
854 @has_content ||= {}
863 @has_content ||= {}
855 @has_content[name] = true
864 @has_content[name] = true
856 super(name, content, &block)
865 super(name, content, &block)
857 end
866 end
858
867
859 def has_content?(name)
868 def has_content?(name)
860 (@has_content && @has_content[name]) || false
869 (@has_content && @has_content[name]) || false
861 end
870 end
862
871
863 # Returns the avatar image tag for the given +user+ if avatars are enabled
872 # Returns the avatar image tag for the given +user+ if avatars are enabled
864 # +user+ can be a User or a string that will be scanned for an email address (eg. 'joe <joe@foo.bar>')
873 # +user+ can be a User or a string that will be scanned for an email address (eg. 'joe <joe@foo.bar>')
865 def avatar(user, options = { })
874 def avatar(user, options = { })
866 if Setting.gravatar_enabled?
875 if Setting.gravatar_enabled?
867 options.merge!({:ssl => (defined?(request) && request.ssl?), :default => Setting.gravatar_default})
876 options.merge!({:ssl => (defined?(request) && request.ssl?), :default => Setting.gravatar_default})
868 email = nil
877 email = nil
869 if user.respond_to?(:mail)
878 if user.respond_to?(:mail)
870 email = user.mail
879 email = user.mail
871 elsif user.to_s =~ %r{<(.+?)>}
880 elsif user.to_s =~ %r{<(.+?)>}
872 email = $1
881 email = $1
873 end
882 end
874 return gravatar(email.to_s.downcase, options) unless email.blank? rescue nil
883 return gravatar(email.to_s.downcase, options) unless email.blank? rescue nil
875 else
884 else
876 ''
885 ''
877 end
886 end
878 end
887 end
879
888
880 def favicon
889 def favicon
881 "<link rel='shortcut icon' href='#{image_path('/favicon.ico')}' />"
890 "<link rel='shortcut icon' href='#{image_path('/favicon.ico')}' />"
882 end
891 end
883
892
884 # Returns true if arg is expected in the API response
893 # Returns true if arg is expected in the API response
885 def include_in_api_response?(arg)
894 def include_in_api_response?(arg)
886 unless @included_in_api_response
895 unless @included_in_api_response
887 param = params[:include]
896 param = params[:include]
888 @included_in_api_response = param.is_a?(Array) ? param.collect(&:to_s) : param.to_s.split(',')
897 @included_in_api_response = param.is_a?(Array) ? param.collect(&:to_s) : param.to_s.split(',')
889 @included_in_api_response.collect!(&:strip)
898 @included_in_api_response.collect!(&:strip)
890 end
899 end
891 @included_in_api_response.include?(arg.to_s)
900 @included_in_api_response.include?(arg.to_s)
892 end
901 end
893
902
894 # Returns options or nil if nometa param or X-Redmine-Nometa header
903 # Returns options or nil if nometa param or X-Redmine-Nometa header
895 # was set in the request
904 # was set in the request
896 def api_meta(options)
905 def api_meta(options)
897 if params[:nometa].present? || request.headers['X-Redmine-Nometa']
906 if params[:nometa].present? || request.headers['X-Redmine-Nometa']
898 # compatibility mode for activeresource clients that raise
907 # compatibility mode for activeresource clients that raise
899 # an error when unserializing an array with attributes
908 # an error when unserializing an array with attributes
900 nil
909 nil
901 else
910 else
902 options
911 options
903 end
912 end
904 end
913 end
905
914
906 private
915 private
907
916
908 def wiki_helper
917 def wiki_helper
909 helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
918 helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
910 extend helper
919 extend helper
911 return self
920 return self
912 end
921 end
913
922
914 def link_to_remote_content_update(text, url_params)
923 def link_to_remote_content_update(text, url_params)
915 link_to_remote(text,
924 link_to_remote(text,
916 {:url => url_params, :method => :get, :update => 'content', :complete => 'window.scrollTo(0,0)'},
925 {:url => url_params, :method => :get, :update => 'content', :complete => 'window.scrollTo(0,0)'},
917 {:href => url_for(:params => url_params)}
926 {:href => url_for(:params => url_params)}
918 )
927 )
919 end
928 end
920
929
921 end
930 end
@@ -1,265 +1,270
1 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
1 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
2 <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
2 <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
3 <head>
3 <head>
4 <title>RedmineWikiFormatting</title>
4 <title>RedmineWikiFormatting</title>
5 <meta http-equiv="content-type" content="text/html; charset=utf-8" />
5 <meta http-equiv="content-type" content="text/html; charset=utf-8" />
6 <style type="text/css">
6 <style type="text/css">
7 body { font:80% Verdana,Tahoma,Arial,sans-serif; }
7 body { font:80% Verdana,Tahoma,Arial,sans-serif; }
8 h1, h2, h3, h4 { font-family: Trebuchet MS,Georgia,"Times New Roman",serif; }
8 h1, h2, h3, h4 { font-family: Trebuchet MS,Georgia,"Times New Roman",serif; }
9 pre, code { font-size:120%; }
9 pre, code { font-size:120%; }
10 pre code { font-size:100%; }
10 pre code { font-size:100%; }
11 pre {
11 pre {
12 margin: 1em 1em 1em 1.6em;
12 margin: 1em 1em 1em 1.6em;
13 padding: 2px;
13 padding: 2px;
14 background-color: #fafafa;
14 background-color: #fafafa;
15 border: 1px solid #dadada;
15 border: 1px solid #dadada;
16 width:95%;
16 width:95%;
17 overflow-x: auto;
17 overflow-x: auto;
18 }
18 }
19 a.new { color: #b73535; }
19 a.new { color: #b73535; }
20
20
21 .CodeRay .c { color:#666; }
21 .CodeRay .c { color:#666; }
22
22
23 .CodeRay .cl { color:#B06; font-weight:bold }
23 .CodeRay .cl { color:#B06; font-weight:bold }
24 .CodeRay .dl { color:black }
24 .CodeRay .dl { color:black }
25 .CodeRay .fu { color:#06B; font-weight:bold }
25 .CodeRay .fu { color:#06B; font-weight:bold }
26
26
27 .CodeRay .il { background: #eee }
27 .CodeRay .il { background: #eee }
28 .CodeRay .il .idl { font-weight: bold; color: #888 }
28 .CodeRay .il .idl { font-weight: bold; color: #888 }
29
29
30 .CodeRay .iv { color:#33B }
30 .CodeRay .iv { color:#33B }
31 .CodeRay .r { color:#080; font-weight:bold }
31 .CodeRay .r { color:#080; font-weight:bold }
32
32
33 .CodeRay .s { background-color:#fff0f0 }
33 .CodeRay .s { background-color:#fff0f0 }
34 .CodeRay .s .dl { color:#710 }
34 .CodeRay .s .dl { color:#710 }
35 </style>
35 </style>
36 </head>
36 </head>
37
37
38 <body>
38 <body>
39 <h1><a name="1" class="wiki-page"></a>Wiki formatting</h1>
39 <h1><a name="1" class="wiki-page"></a>Wiki formatting</h1>
40
40
41 <h2><a name="2" class="wiki-page"></a>Links</h2>
41 <h2><a name="2" class="wiki-page"></a>Links</h2>
42
42
43 <h3><a name="3" class="wiki-page"></a>Redmine links</h3>
43 <h3><a name="3" class="wiki-page"></a>Redmine links</h3>
44
44
45 <p>Redmine allows hyperlinking between issues, changesets and wiki pages from anywhere wiki formatting is used.</p>
45 <p>Redmine allows hyperlinking between issues, changesets and wiki pages from anywhere wiki formatting is used.</p>
46 <ul>
46 <ul>
47 <li>Link to an issue: <strong>#124</strong> (displays <del><a href="#" class="issue" title="bulk edit doesn't change the category or fixed version properties (Closed)">#124</a></del>, link is striked-through if the issue is closed)</li>
47 <li>Link to an issue: <strong>#124</strong> (displays <del><a href="#" class="issue" title="bulk edit doesn't change the category or fixed version properties (Closed)">#124</a></del>, link is striked-through if the issue is closed)</li>
48 <li>Link to a changeset: <strong>r758</strong> (displays <a href="#" class="changeset" title="Search engine now only searches objects the user is allowed to view.">r758</a>)</li>
48 <li>Link to a changeset: <strong>r758</strong> (displays <a href="#" class="changeset" title="Search engine now only searches objects the user is allowed to view.">r758</a>)</li>
49 <li>Link to a changeset with a non-numeric hash: <strong>commit:c6f4d0fd</strong> (displays c6f4d0fd). Added in <a href="#" class="changeset" title="Merged Git support branch (r1200 to r1226).">r1236</a>.</li>
49 <li>Link to a changeset with a non-numeric hash: <strong>commit:c6f4d0fd</strong> (displays <a href="#" class="changeset">c6f4d0fd</a>).</li>
50 <li>Link to a changeset of another project: <strong>sandbox:r758</strong> (displays <a href="#" class="changeset" title="Search engine now only searches objects the user is allowed to view.">sanbox:r758</a>)</li>
51 <li>Link to a changeset with a non-numeric hash: <strong>sandbox:c6f4d0fd</strong> (displays <a href="#" class="changeset">sandbox:c6f4d0fd</a>).</li>
50 </ul>
52 </ul>
51
53
52 <p>Wiki links:</p>
54 <p>Wiki links:</p>
53
55
54 <ul>
56 <ul>
55 <li><strong>[[Guide]]</strong> displays a link to the page named 'Guide': <a href="#" class="wiki-page">Guide</a></li>
57 <li><strong>[[Guide]]</strong> displays a link to the page named 'Guide': <a href="#" class="wiki-page">Guide</a></li>
56 <li><strong>[[Guide#further-reading]]</strong> takes you to the anchor "further-reading". Headings get automatically assigned anchors so that you can refer to them: <a href="#" class="wiki-page">Guide</a></li>
58 <li><strong>[[Guide#further-reading]]</strong> takes you to the anchor "further-reading". Headings get automatically assigned anchors so that you can refer to them: <a href="#" class="wiki-page">Guide</a></li>
57 <li><strong>[[Guide|User manual]]</strong> displays a link to the same page but with a different text: <a href="#" class="wiki-page">User manual</a></li>
59 <li><strong>[[Guide|User manual]]</strong> displays a link to the same page but with a different text: <a href="#" class="wiki-page">User manual</a></li>
58 </ul>
60 </ul>
59
61
60 <p>You can also link to pages of an other project wiki:</p>
62 <p>You can also link to pages of an other project wiki:</p>
61
63
62 <ul>
64 <ul>
63 <li><strong>[[sandbox:some page]]</strong> displays a link to the page named 'Some page' of the Sandbox wiki</li>
65 <li><strong>[[sandbox:some page]]</strong> displays a link to the page named 'Some page' of the Sandbox wiki</li>
64 <li><strong>[[sandbox:]]</strong> displays a link to the Sandbox wiki main page</li>
66 <li><strong>[[sandbox:]]</strong> displays a link to the Sandbox wiki main page</li>
65 </ul>
67 </ul>
66
68
67 <p>Wiki links are displayed in red if the page doesn't exist yet, eg: <a href="#" class="wiki-page new">Nonexistent page</a>.</p>
69 <p>Wiki links are displayed in red if the page doesn't exist yet, eg: <a href="#" class="wiki-page new">Nonexistent page</a>.</p>
68
70
69 <p>Links to other resources:</p>
71 <p>Links to other resources:</p>
70
72
71 <ul>
73 <ul>
72 <li>Documents:
74 <li>Documents:
73 <ul>
75 <ul>
74 <li><strong>document#17</strong> (link to document with id 17)</li>
76 <li><strong>document#17</strong> (link to document with id 17)</li>
75 <li><strong>document:Greetings</strong> (link to the document with title "Greetings")</li>
77 <li><strong>document:Greetings</strong> (link to the document with title "Greetings")</li>
76 <li><strong>document:"Some document"</strong> (double quotes can be used when document title contains spaces)</li>
78 <li><strong>document:"Some document"</strong> (double quotes can be used when document title contains spaces)</li>
77 <li><strong>document:some_project:"Some document"</strong> (link to a document with title "Some document" in other project "some_project")
79 <li><strong>sandbox:document:"Some document"</strong> (link to a document with title "Some document" in other project "sandbox")</li>
78 </ul></li>
80 </ul></li>
79 </ul>
81 </ul>
80
82
81 <ul>
83 <ul>
82 <li>Versions:
84 <li>Versions:
83 <ul>
85 <ul>
84 <li><strong>version#3</strong> (link to version with id 3)</li>
86 <li><strong>version#3</strong> (link to version with id 3)</li>
85 <li><strong>version:1.0.0</strong> (link to version named "1.0.0")</li>
87 <li><strong>version:1.0.0</strong> (link to version named "1.0.0")</li>
86 <li><strong>version:"1.0 beta 2"</strong></li>
88 <li><strong>version:"1.0 beta 2"</strong></li>
89 <li><strong>sandbox:version:1.0.0</strong> (link to version "1.0.0" in the project "sandbox")</li>
87 </ul></li>
90 </ul></li>
88 </ul>
91 </ul>
89
92
90 <ul>
93 <ul>
91 <li>Attachments:
94 <li>Attachments:
92 <ul>
95 <ul>
93 <li><strong>attachment:file.zip</strong> (link to the attachment of the current object named file.zip)</li>
96 <li><strong>attachment:file.zip</strong> (link to the attachment of the current object named file.zip)</li>
94 <li>For now, attachments of the current object can be referenced only (if you're on an issue, it's possible to reference attachments of this issue only)</li>
97 <li>For now, attachments of the current object can be referenced only (if you're on an issue, it's possible to reference attachments of this issue only)</li>
95 </ul></li>
98 </ul></li>
96 </ul>
99 </ul>
97
100
98 <ul>
101 <ul>
99 <li>Repository files:
102 <li>Repository files:
100 <ul>
103 <ul>
101 <li><strong>source:some/file</strong> (link to the file located at /some/file in the project's repository)</li>
104 <li><strong>source:some/file</strong> (link to the file located at /some/file in the project's repository)</li>
102 <li><strong>source:some/file@52</strong> (link to the file's revision 52)</li>
105 <li><strong>source:some/file@52</strong> (link to the file's revision 52)</li>
103 <li><strong>source:some/file#L120</strong> (link to line 120 of the file)</li>
106 <li><strong>source:some/file#L120</strong> (link to line 120 of the file)</li>
104 <li><strong>source:some/file@52#L120</strong> (link to line 120 of the file's revision 52)</li>
107 <li><strong>source:some/file@52#L120</strong> (link to line 120 of the file's revision 52)</li>
105 <li><strong>source:"some file@52#L120"</strong> (use double quotes when the URL contains spaces</li>
108 <li><strong>source:"some file@52#L120"</strong> (use double quotes when the URL contains spaces</li>
106 <li><strong>export:some/file</strong> (force the download of the file)</li>
109 <li><strong>export:some/file</strong> (force the download of the file)</li>
110 <li><strong>sandbox:source:some/file</strong> (link to the file located at /some/file in the repository of the project "sandbox")</li>
111 <li><strong>sandbox:export:some/file</strong> (force the download of the file)</li>
107 </ul></li>
112 </ul></li>
108 </ul>
113 </ul>
109
114
110 <ul>
115 <ul>
111 <li>Forum messages:
116 <li>Forum messages:
112 <ul>
117 <ul>
113 <li><strong>message#1218</strong> (link to message with id 1218)</li>
118 <li><strong>message#1218</strong> (link to message with id 1218)</li>
114 </ul></li>
119 </ul></li>
115 </ul>
120 </ul>
116
121
117 <ul>
122 <ul>
118 <li>Projects:
123 <li>Projects:
119 <ul>
124 <ul>
120 <li><strong>project#3</strong> (link to project with id 3)</li>
125 <li><strong>project#3</strong> (link to project with id 3)</li>
121 <li><strong>project:someproject</strong> (link to project named "someproject")</li>
126 <li><strong>project:someproject</strong> (link to project named "someproject")</li>
122 </ul></li>
127 </ul></li>
123 </ul>
128 </ul>
124
129
125
130
126 <p>Escaping:</p>
131 <p>Escaping:</p>
127
132
128 <ul>
133 <ul>
129 <li>You can prevent Redmine links from being parsed by preceding them with an exclamation mark: !</li>
134 <li>You can prevent Redmine links from being parsed by preceding them with an exclamation mark: !</li>
130 </ul>
135 </ul>
131
136
132
137
133 <h3><a name="4" class="wiki-page"></a>External links</h3>
138 <h3><a name="4" class="wiki-page"></a>External links</h3>
134
139
135 <p>HTTP URLs and email addresses are automatically turned into clickable links:</p>
140 <p>HTTP URLs and email addresses are automatically turned into clickable links:</p>
136
141
137 <pre>
142 <pre>
138 http://www.redmine.org, someone@foo.bar
143 http://www.redmine.org, someone@foo.bar
139 </pre>
144 </pre>
140
145
141 <p>displays: <a class="external" href="http://www.redmine.org">http://www.redmine.org</a>, <a href="mailto:someone@foo.bar" class="email">someone@foo.bar</a></p>
146 <p>displays: <a class="external" href="http://www.redmine.org">http://www.redmine.org</a>, <a href="mailto:someone@foo.bar" class="email">someone@foo.bar</a></p>
142
147
143 <p>If you want to display a specific text instead of the URL, you can use the standard textile syntax:</p>
148 <p>If you want to display a specific text instead of the URL, you can use the standard textile syntax:</p>
144
149
145 <pre>
150 <pre>
146 "Redmine web site":http://www.redmine.org
151 "Redmine web site":http://www.redmine.org
147 </pre>
152 </pre>
148
153
149 <p>displays: <a href="http://www.redmine.org" class="external">Redmine web site</a></p>
154 <p>displays: <a href="http://www.redmine.org" class="external">Redmine web site</a></p>
150
155
151
156
152 <h2><a name="5" class="wiki-page"></a>Text formatting</h2>
157 <h2><a name="5" class="wiki-page"></a>Text formatting</h2>
153
158
154
159
155 <p>For things such as headlines, bold, tables, lists, Redmine supports Textile syntax. See <a class="external" href="http://www.textism.com/tools/textile/">http://www.textism.com/tools/textile/</a> for information on using any of these features. A few samples are included below, but the engine is capable of much more of that.</p>
160 <p>For things such as headlines, bold, tables, lists, Redmine supports Textile syntax. See <a class="external" href="http://www.textism.com/tools/textile/">http://www.textism.com/tools/textile/</a> for information on using any of these features. A few samples are included below, but the engine is capable of much more of that.</p>
156
161
157 <h3><a name="6" class="wiki-page"></a>Font style</h3>
162 <h3><a name="6" class="wiki-page"></a>Font style</h3>
158
163
159 <pre>
164 <pre>
160 * *bold*
165 * *bold*
161 * _italic_
166 * _italic_
162 * _*bold italic*_
167 * _*bold italic*_
163 * +underline+
168 * +underline+
164 * -strike-through-
169 * -strike-through-
165 </pre>
170 </pre>
166
171
167 <p>Display:</p>
172 <p>Display:</p>
168
173
169 <ul>
174 <ul>
170 <li><strong>bold</strong></li>
175 <li><strong>bold</strong></li>
171 <li><em>italic</em></li>
176 <li><em>italic</em></li>
172 <li><em>*bold italic*</em></li>
177 <li><em>*bold italic*</em></li>
173 <li><ins>underline</ins></li>
178 <li><ins>underline</ins></li>
174 <li><del>strike-through</del></li>
179 <li><del>strike-through</del></li>
175 </ul>
180 </ul>
176
181
177 <h3><a name="7" class="wiki-page"></a>Inline images</h3>
182 <h3><a name="7" class="wiki-page"></a>Inline images</h3>
178
183
179 <ul>
184 <ul>
180 <li><strong>!image_url!</strong> displays an image located at image_url (textile syntax)</li>
185 <li><strong>!image_url!</strong> displays an image located at image_url (textile syntax)</li>
181 <li><strong>!>image_url!</strong> right floating image</li>
186 <li><strong>!>image_url!</strong> right floating image</li>
182 <li>If you have an image attached to your wiki page, it can be displayed inline using its filename: <strong>!attached_image.png!</strong></li>
187 <li>If you have an image attached to your wiki page, it can be displayed inline using its filename: <strong>!attached_image.png!</strong></li>
183 </ul>
188 </ul>
184
189
185 <h3><a name="8" class="wiki-page"></a>Headings</h3>
190 <h3><a name="8" class="wiki-page"></a>Headings</h3>
186
191
187 <pre>
192 <pre>
188 h1. Heading
193 h1. Heading
189 h2. Subheading
194 h2. Subheading
190 h3. Subsubheading
195 h3. Subsubheading
191 </pre>
196 </pre>
192
197
193 <p>Redmine assigns an anchor to each of those headings thus you can link to them with "#Heading", "#Subheading" and so forth.</p>
198 <p>Redmine assigns an anchor to each of those headings thus you can link to them with "#Heading", "#Subheading" and so forth.</p>
194
199
195
200
196 <h3><a name="9" class="wiki-page"></a>Paragraphs</h3>
201 <h3><a name="9" class="wiki-page"></a>Paragraphs</h3>
197
202
198 <pre>
203 <pre>
199 p>. right aligned
204 p>. right aligned
200 p=. centered
205 p=. centered
201 </pre>
206 </pre>
202
207
203 <p style="text-align:center;">This is centered paragraph.</p>
208 <p style="text-align:center;">This is centered paragraph.</p>
204
209
205
210
206 <h3><a name="10" class="wiki-page"></a>Blockquotes</h3>
211 <h3><a name="10" class="wiki-page"></a>Blockquotes</h3>
207
212
208 <p>Start the paragraph with <strong>bq.</strong></p>
213 <p>Start the paragraph with <strong>bq.</strong></p>
209
214
210 <pre>
215 <pre>
211 bq. Rails is a full-stack framework for developing database-backed web applications according to the Model-View-Control pattern.
216 bq. Rails is a full-stack framework for developing database-backed web applications according to the Model-View-Control pattern.
212 To go live, all you need to add is a database and a web server.
217 To go live, all you need to add is a database and a web server.
213 </pre>
218 </pre>
214
219
215 <p>Display:</p>
220 <p>Display:</p>
216
221
217 <blockquote>
222 <blockquote>
218 <p>Rails is a full-stack framework for developing database-backed web applications according to the Model-View-Control pattern.<br />To go live, all you need to add is a database and a web server.</p>
223 <p>Rails is a full-stack framework for developing database-backed web applications according to the Model-View-Control pattern.<br />To go live, all you need to add is a database and a web server.</p>
219 </blockquote>
224 </blockquote>
220
225
221
226
222 <h3><a name="11" class="wiki-page"></a>Table of content</h3>
227 <h3><a name="11" class="wiki-page"></a>Table of content</h3>
223
228
224 <pre>
229 <pre>
225 {{toc}} => left aligned toc
230 {{toc}} => left aligned toc
226 {{>toc}} => right aligned toc
231 {{>toc}} => right aligned toc
227 </pre>
232 </pre>
228
233
229 <h2><a name="12" class="wiki-page"></a>Macros</h2>
234 <h2><a name="12" class="wiki-page"></a>Macros</h2>
230
235
231 <p>Redmine has the following builtin macros:</p>
236 <p>Redmine has the following builtin macros:</p>
232
237
233 <p><dl><dt><code>hello_world</code></dt><dd><p>Sample macro.</p></dd><dt><code>include</code></dt><dd><p>Include a wiki page. Example:</p>
238 <p><dl><dt><code>hello_world</code></dt><dd><p>Sample macro.</p></dd><dt><code>include</code></dt><dd><p>Include a wiki page. Example:</p>
234
239
235 <pre><code>{{include(Foo)}}</code></pre></dd><dt><code>macro_list</code></dt><dd><p>Displays a list of all available macros, including description if available.</p></dd></dl></p>
240 <pre><code>{{include(Foo)}}</code></pre></dd><dt><code>macro_list</code></dt><dd><p>Displays a list of all available macros, including description if available.</p></dd></dl></p>
236
241
237
242
238 <h2><a name="13" class="wiki-page"></a>Code highlighting</h2>
243 <h2><a name="13" class="wiki-page"></a>Code highlighting</h2>
239
244
240 <p>Code highlightment relies on <a href="http://coderay.rubychan.de/" class="external">CodeRay</a>, a fast syntax highlighting library written completely in Ruby. It currently supports c, cpp, css, delphi, groovy, html, java, javascript, json, php, python, rhtml, ruby, scheme, sql, xml and yaml languages.</p>
245 <p>Code highlightment relies on <a href="http://coderay.rubychan.de/" class="external">CodeRay</a>, a fast syntax highlighting library written completely in Ruby. It currently supports c, cpp, css, delphi, groovy, html, java, javascript, json, php, python, rhtml, ruby, scheme, sql, xml and yaml languages.</p>
241
246
242 <p>You can highlight code in your wiki page using this syntax:</p>
247 <p>You can highlight code in your wiki page using this syntax:</p>
243
248
244 <pre>
249 <pre>
245 &lt;pre&gt;&lt;code class="ruby"&gt;
250 &lt;pre&gt;&lt;code class="ruby"&gt;
246 Place you code here.
251 Place you code here.
247 &lt;/code&gt;&lt;/pre&gt;
252 &lt;/code&gt;&lt;/pre&gt;
248 </pre>
253 </pre>
249
254
250 <p>Example:</p>
255 <p>Example:</p>
251
256
252 <pre><code class="ruby CodeRay"><span class="no"> 1</span> <span class="c"># The Greeter class</span>
257 <pre><code class="ruby CodeRay"><span class="no"> 1</span> <span class="c"># The Greeter class</span>
253 <span class="no"> 2</span> <span class="r">class</span> <span class="cl">Greeter</span>
258 <span class="no"> 2</span> <span class="r">class</span> <span class="cl">Greeter</span>
254 <span class="no"> 3</span> <span class="r">def</span> <span class="fu">initialize</span>(name)
259 <span class="no"> 3</span> <span class="r">def</span> <span class="fu">initialize</span>(name)
255 <span class="no"> 4</span> <span class="iv">@name</span> = name.capitalize
260 <span class="no"> 4</span> <span class="iv">@name</span> = name.capitalize
256 <span class="no"> 5</span> <span class="r">end</span>
261 <span class="no"> 5</span> <span class="r">end</span>
257 <span class="no"> 6</span>
262 <span class="no"> 6</span>
258 <span class="no"> 7</span> <span class="r">def</span> <span class="fu">salute</span>
263 <span class="no"> 7</span> <span class="r">def</span> <span class="fu">salute</span>
259 <span class="no"> 8</span> puts <span class="s"><span class="dl">"</span><span class="k">Hello </span><span class="il"><span class="idl">#{</span><span class="iv">@name</span><span class="idl">}</span></span><span class="k">!</span><span class="dl">"</span></span>
264 <span class="no"> 8</span> puts <span class="s"><span class="dl">"</span><span class="k">Hello </span><span class="il"><span class="idl">#{</span><span class="iv">@name</span><span class="idl">}</span></span><span class="k">!</span><span class="dl">"</span></span>
260 <span class="no"> 9</span> <span class="r">end</span>
265 <span class="no"> 9</span> <span class="r">end</span>
261 <span class="no"><strong>10</strong></span> <span class="r">end</span>
266 <span class="no"><strong>10</strong></span> <span class="r">end</span>
262 </code>
267 </code>
263 </pre>
268 </pre>
264 </body>
269 </body>
265 </html>
270 </html>
@@ -1,745 +1,774
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'), :class => 'message'),
203 'message#5' => link_to('RE: post 2', message_url.merge(:anchor => 'message-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
229 source_link = link_to('ecookbook:source:/some/file', {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :path => ['some', 'file']},
230 :class => 'source')
231
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')
234
235 to_test = {
236 # documents
237 'document:"Test document"' => 'document:"Test document"',
238 'ecookbook:document:"Test document"' => '<a href="/documents/1" class="document">Test document</a>',
239 'invalid:document:"Test document"' => 'invalid:document:"Test document"',
240 # versions
241 'version:"1.0"' => 'version:"1.0"',
242 'ecookbook:version:"1.0"' => '<a href="/versions/show/2" class="version">1.0</a>',
243 'invalid:version:"1.0"' => 'invalid:version:"1.0"',
244 # changeset
245 'r2' => 'r2',
246 'ecookbook:r2' => changeset_link,
247 'invalid:r2' => 'invalid:r2',
248 # source
249 'source:/some/file' => 'source:/some/file',
250 'ecookbook:source:/some/file' => source_link,
251 'invalid:source:/some/file' => 'invalid:source:/some/file',
252 }
253 @project = Project.find(3)
254 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text), "#{text} failed" }
255 end
256
228 def test_redmine_links_git_commit
257 def test_redmine_links_git_commit
229 changeset_link = link_to('abcd',
258 changeset_link = link_to('abcd',
230 {
259 {
231 :controller => 'repositories',
260 :controller => 'repositories',
232 :action => 'revision',
261 :action => 'revision',
233 :id => 'subproject1',
262 :id => 'subproject1',
234 :rev => 'abcd',
263 :rev => 'abcd',
235 },
264 },
236 :class => 'changeset', :title => 'test commit')
265 :class => 'changeset', :title => 'test commit')
237 to_test = {
266 to_test = {
238 'commit:abcd' => changeset_link,
267 'commit:abcd' => changeset_link,
239 }
268 }
240 @project = Project.find(3)
269 @project = Project.find(3)
241 r = Repository::Git.create!(:project => @project, :url => '/tmp/test/git')
270 r = Repository::Git.create!(:project => @project, :url => '/tmp/test/git')
242 assert r
271 assert r
243 c = Changeset.new(:repository => r,
272 c = Changeset.new(:repository => r,
244 :committed_on => Time.now,
273 :committed_on => Time.now,
245 :revision => 'abcd',
274 :revision => 'abcd',
246 :scmid => 'abcd',
275 :scmid => 'abcd',
247 :comments => 'test commit')
276 :comments => 'test commit')
248 assert( c.save )
277 assert( c.save )
249 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) }
250 end
279 end
251
280
252 # TODO: Bazaar commit id contains mail address, so it contains '@' and '_'.
281 # TODO: Bazaar commit id contains mail address, so it contains '@' and '_'.
253 def test_redmine_links_darcs_commit
282 def test_redmine_links_darcs_commit
254 changeset_link = link_to('20080308225258-98289-abcd456efg.gz',
283 changeset_link = link_to('20080308225258-98289-abcd456efg.gz',
255 {
284 {
256 :controller => 'repositories',
285 :controller => 'repositories',
257 :action => 'revision',
286 :action => 'revision',
258 :id => 'subproject1',
287 :id => 'subproject1',
259 :rev => '123',
288 :rev => '123',
260 },
289 },
261 :class => 'changeset', :title => 'test commit')
290 :class => 'changeset', :title => 'test commit')
262 to_test = {
291 to_test = {
263 'commit:20080308225258-98289-abcd456efg.gz' => changeset_link,
292 'commit:20080308225258-98289-abcd456efg.gz' => changeset_link,
264 }
293 }
265 @project = Project.find(3)
294 @project = Project.find(3)
266 r = Repository::Darcs.create!(:project => @project, :url => '/tmp/test/darcs')
295 r = Repository::Darcs.create!(:project => @project, :url => '/tmp/test/darcs')
267 assert r
296 assert r
268 c = Changeset.new(:repository => r,
297 c = Changeset.new(:repository => r,
269 :committed_on => Time.now,
298 :committed_on => Time.now,
270 :revision => '123',
299 :revision => '123',
271 :scmid => '20080308225258-98289-abcd456efg.gz',
300 :scmid => '20080308225258-98289-abcd456efg.gz',
272 :comments => 'test commit')
301 :comments => 'test commit')
273 assert( c.save )
302 assert( c.save )
274 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
303 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
275 end
304 end
276
305
277 def test_redmine_links_mercurial_commit
306 def test_redmine_links_mercurial_commit
278 changeset_link_rev = link_to('r123',
307 changeset_link_rev = link_to('r123',
279 {
308 {
280 :controller => 'repositories',
309 :controller => 'repositories',
281 :action => 'revision',
310 :action => 'revision',
282 :id => 'subproject1',
311 :id => 'subproject1',
283 :rev => '123' ,
312 :rev => '123' ,
284 },
313 },
285 :class => 'changeset', :title => 'test commit')
314 :class => 'changeset', :title => 'test commit')
286 changeset_link_commit = link_to('abcd',
315 changeset_link_commit = link_to('abcd',
287 {
316 {
288 :controller => 'repositories',
317 :controller => 'repositories',
289 :action => 'revision',
318 :action => 'revision',
290 :id => 'subproject1',
319 :id => 'subproject1',
291 :rev => 'abcd' ,
320 :rev => 'abcd' ,
292 },
321 },
293 :class => 'changeset', :title => 'test commit')
322 :class => 'changeset', :title => 'test commit')
294 to_test = {
323 to_test = {
295 'r123' => changeset_link_rev,
324 'r123' => changeset_link_rev,
296 'commit:abcd' => changeset_link_commit,
325 'commit:abcd' => changeset_link_commit,
297 }
326 }
298 @project = Project.find(3)
327 @project = Project.find(3)
299 r = Repository::Mercurial.create!(:project => @project, :url => '/tmp/test')
328 r = Repository::Mercurial.create!(:project => @project, :url => '/tmp/test')
300 assert r
329 assert r
301 c = Changeset.new(:repository => r,
330 c = Changeset.new(:repository => r,
302 :committed_on => Time.now,
331 :committed_on => Time.now,
303 :revision => '123',
332 :revision => '123',
304 :scmid => 'abcd',
333 :scmid => 'abcd',
305 :comments => 'test commit')
334 :comments => 'test commit')
306 assert( c.save )
335 assert( c.save )
307 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
336 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
308 end
337 end
309
338
310 def test_attachment_links
339 def test_attachment_links
311 attachment_link = link_to('error281.txt', {:controller => 'attachments', :action => 'download', :id => '1'}, :class => 'attachment')
340 attachment_link = link_to('error281.txt', {:controller => 'attachments', :action => 'download', :id => '1'}, :class => 'attachment')
312 to_test = {
341 to_test = {
313 'attachment:error281.txt' => attachment_link
342 'attachment:error281.txt' => attachment_link
314 }
343 }
315 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text, :attachments => Issue.find(3).attachments), "#{text} failed" }
344 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text, :attachments => Issue.find(3).attachments), "#{text} failed" }
316 end
345 end
317
346
318 def test_wiki_links
347 def test_wiki_links
319 to_test = {
348 to_test = {
320 '[[CookBook documentation]]' => '<a href="/projects/ecookbook/wiki/CookBook_documentation" class="wiki-page">CookBook documentation</a>',
349 '[[CookBook documentation]]' => '<a href="/projects/ecookbook/wiki/CookBook_documentation" class="wiki-page">CookBook documentation</a>',
321 '[[Another page|Page]]' => '<a href="/projects/ecookbook/wiki/Another_page" class="wiki-page">Page</a>',
350 '[[Another page|Page]]' => '<a href="/projects/ecookbook/wiki/Another_page" class="wiki-page">Page</a>',
322 # link with anchor
351 # link with anchor
323 '[[CookBook documentation#One-section]]' => '<a href="/projects/ecookbook/wiki/CookBook_documentation#One-section" class="wiki-page">CookBook documentation</a>',
352 '[[CookBook documentation#One-section]]' => '<a href="/projects/ecookbook/wiki/CookBook_documentation#One-section" class="wiki-page">CookBook documentation</a>',
324 '[[Another page#anchor|Page]]' => '<a href="/projects/ecookbook/wiki/Another_page#anchor" class="wiki-page">Page</a>',
353 '[[Another page#anchor|Page]]' => '<a href="/projects/ecookbook/wiki/Another_page#anchor" class="wiki-page">Page</a>',
325 # page that doesn't exist
354 # page that doesn't exist
326 '[[Unknown page]]' => '<a href="/projects/ecookbook/wiki/Unknown_page" class="wiki-page new">Unknown page</a>',
355 '[[Unknown page]]' => '<a href="/projects/ecookbook/wiki/Unknown_page" class="wiki-page new">Unknown page</a>',
327 '[[Unknown page|404]]' => '<a href="/projects/ecookbook/wiki/Unknown_page" class="wiki-page new">404</a>',
356 '[[Unknown page|404]]' => '<a href="/projects/ecookbook/wiki/Unknown_page" class="wiki-page new">404</a>',
328 # link to another project wiki
357 # link to another project wiki
329 '[[onlinestore:]]' => '<a href="/projects/onlinestore/wiki" class="wiki-page">onlinestore</a>',
358 '[[onlinestore:]]' => '<a href="/projects/onlinestore/wiki" class="wiki-page">onlinestore</a>',
330 '[[onlinestore:|Wiki]]' => '<a href="/projects/onlinestore/wiki" class="wiki-page">Wiki</a>',
359 '[[onlinestore:|Wiki]]' => '<a href="/projects/onlinestore/wiki" class="wiki-page">Wiki</a>',
331 '[[onlinestore:Start page]]' => '<a href="/projects/onlinestore/wiki/Start_page" class="wiki-page">Start page</a>',
360 '[[onlinestore:Start page]]' => '<a href="/projects/onlinestore/wiki/Start_page" class="wiki-page">Start page</a>',
332 '[[onlinestore:Start page|Text]]' => '<a href="/projects/onlinestore/wiki/Start_page" class="wiki-page">Text</a>',
361 '[[onlinestore:Start page|Text]]' => '<a href="/projects/onlinestore/wiki/Start_page" class="wiki-page">Text</a>',
333 '[[onlinestore:Unknown page]]' => '<a href="/projects/onlinestore/wiki/Unknown_page" class="wiki-page new">Unknown page</a>',
362 '[[onlinestore:Unknown page]]' => '<a href="/projects/onlinestore/wiki/Unknown_page" class="wiki-page new">Unknown page</a>',
334 # striked through link
363 # striked through link
335 '-[[Another page|Page]]-' => '<del><a href="/projects/ecookbook/wiki/Another_page" class="wiki-page">Page</a></del>',
364 '-[[Another page|Page]]-' => '<del><a href="/projects/ecookbook/wiki/Another_page" class="wiki-page">Page</a></del>',
336 '-[[Another page|Page]] link-' => '<del><a href="/projects/ecookbook/wiki/Another_page" class="wiki-page">Page</a> link</del>',
365 '-[[Another page|Page]] link-' => '<del><a href="/projects/ecookbook/wiki/Another_page" class="wiki-page">Page</a> link</del>',
337 # escaping
366 # escaping
338 '![[Another page|Page]]' => '[[Another page|Page]]',
367 '![[Another page|Page]]' => '[[Another page|Page]]',
339 # project does not exist
368 # project does not exist
340 '[[unknowproject:Start]]' => '[[unknowproject:Start]]',
369 '[[unknowproject:Start]]' => '[[unknowproject:Start]]',
341 '[[unknowproject:Start|Page title]]' => '[[unknowproject:Start|Page title]]',
370 '[[unknowproject:Start|Page title]]' => '[[unknowproject:Start|Page title]]',
342 }
371 }
343 @project = Project.find(1)
372 @project = Project.find(1)
344 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
373 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
345 end
374 end
346
375
347 def test_html_tags
376 def test_html_tags
348 to_test = {
377 to_test = {
349 "<div>content</div>" => "<p>&lt;div&gt;content&lt;/div&gt;</p>",
378 "<div>content</div>" => "<p>&lt;div&gt;content&lt;/div&gt;</p>",
350 "<div class=\"bold\">content</div>" => "<p>&lt;div class=\"bold\"&gt;content&lt;/div&gt;</p>",
379 "<div class=\"bold\">content</div>" => "<p>&lt;div class=\"bold\"&gt;content&lt;/div&gt;</p>",
351 "<script>some script;</script>" => "<p>&lt;script&gt;some script;&lt;/script&gt;</p>",
380 "<script>some script;</script>" => "<p>&lt;script&gt;some script;&lt;/script&gt;</p>",
352 # do not escape pre/code tags
381 # do not escape pre/code tags
353 "<pre>\nline 1\nline2</pre>" => "<pre>\nline 1\nline2</pre>",
382 "<pre>\nline 1\nline2</pre>" => "<pre>\nline 1\nline2</pre>",
354 "<pre><code>\nline 1\nline2</code></pre>" => "<pre><code>\nline 1\nline2</code></pre>",
383 "<pre><code>\nline 1\nline2</code></pre>" => "<pre><code>\nline 1\nline2</code></pre>",
355 "<pre><div>content</div></pre>" => "<pre>&lt;div&gt;content&lt;/div&gt;</pre>",
384 "<pre><div>content</div></pre>" => "<pre>&lt;div&gt;content&lt;/div&gt;</pre>",
356 "HTML comment: <!-- no comments -->" => "<p>HTML comment: &lt;!-- no comments --&gt;</p>",
385 "HTML comment: <!-- no comments -->" => "<p>HTML comment: &lt;!-- no comments --&gt;</p>",
357 "<!-- opening comment" => "<p>&lt;!-- opening comment</p>",
386 "<!-- opening comment" => "<p>&lt;!-- opening comment</p>",
358 # remove attributes except class
387 # remove attributes except class
359 "<pre class='foo'>some text</pre>" => "<pre class='foo'>some text</pre>",
388 "<pre class='foo'>some text</pre>" => "<pre class='foo'>some text</pre>",
360 '<pre class="foo">some text</pre>' => '<pre class="foo">some text</pre>',
389 '<pre class="foo">some text</pre>' => '<pre class="foo">some text</pre>',
361 "<pre class='foo bar'>some text</pre>" => "<pre class='foo bar'>some text</pre>",
390 "<pre class='foo bar'>some text</pre>" => "<pre class='foo bar'>some text</pre>",
362 '<pre class="foo bar">some text</pre>' => '<pre class="foo bar">some text</pre>',
391 '<pre class="foo bar">some text</pre>' => '<pre class="foo bar">some text</pre>',
363 "<pre onmouseover='alert(1)'>some text</pre>" => "<pre>some text</pre>",
392 "<pre onmouseover='alert(1)'>some text</pre>" => "<pre>some text</pre>",
364 # xss
393 # xss
365 '<pre><code class=""onmouseover="alert(1)">text</code></pre>' => '<pre><code>text</code></pre>',
394 '<pre><code class=""onmouseover="alert(1)">text</code></pre>' => '<pre><code>text</code></pre>',
366 '<pre class=""onmouseover="alert(1)">text</pre>' => '<pre>text</pre>',
395 '<pre class=""onmouseover="alert(1)">text</pre>' => '<pre>text</pre>',
367 }
396 }
368 to_test.each { |text, result| assert_equal result, textilizable(text) }
397 to_test.each { |text, result| assert_equal result, textilizable(text) }
369 end
398 end
370
399
371 def test_allowed_html_tags
400 def test_allowed_html_tags
372 to_test = {
401 to_test = {
373 "<pre>preformatted text</pre>" => "<pre>preformatted text</pre>",
402 "<pre>preformatted text</pre>" => "<pre>preformatted text</pre>",
374 "<notextile>no *textile* formatting</notextile>" => "no *textile* formatting",
403 "<notextile>no *textile* formatting</notextile>" => "no *textile* formatting",
375 "<notextile>this is <tag>a tag</tag></notextile>" => "this is &lt;tag&gt;a tag&lt;/tag&gt;"
404 "<notextile>this is <tag>a tag</tag></notextile>" => "this is &lt;tag&gt;a tag&lt;/tag&gt;"
376 }
405 }
377 to_test.each { |text, result| assert_equal result, textilizable(text) }
406 to_test.each { |text, result| assert_equal result, textilizable(text) }
378 end
407 end
379
408
380 def test_pre_tags
409 def test_pre_tags
381 raw = <<-RAW
410 raw = <<-RAW
382 Before
411 Before
383
412
384 <pre>
413 <pre>
385 <prepared-statement-cache-size>32</prepared-statement-cache-size>
414 <prepared-statement-cache-size>32</prepared-statement-cache-size>
386 </pre>
415 </pre>
387
416
388 After
417 After
389 RAW
418 RAW
390
419
391 expected = <<-EXPECTED
420 expected = <<-EXPECTED
392 <p>Before</p>
421 <p>Before</p>
393 <pre>
422 <pre>
394 &lt;prepared-statement-cache-size&gt;32&lt;/prepared-statement-cache-size&gt;
423 &lt;prepared-statement-cache-size&gt;32&lt;/prepared-statement-cache-size&gt;
395 </pre>
424 </pre>
396 <p>After</p>
425 <p>After</p>
397 EXPECTED
426 EXPECTED
398
427
399 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
428 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
400 end
429 end
401
430
402 def test_pre_content_should_not_parse_wiki_and_redmine_links
431 def test_pre_content_should_not_parse_wiki_and_redmine_links
403 raw = <<-RAW
432 raw = <<-RAW
404 [[CookBook documentation]]
433 [[CookBook documentation]]
405
434
406 #1
435 #1
407
436
408 <pre>
437 <pre>
409 [[CookBook documentation]]
438 [[CookBook documentation]]
410
439
411 #1
440 #1
412 </pre>
441 </pre>
413 RAW
442 RAW
414
443
415 expected = <<-EXPECTED
444 expected = <<-EXPECTED
416 <p><a href="/projects/ecookbook/wiki/CookBook_documentation" class="wiki-page">CookBook documentation</a></p>
445 <p><a href="/projects/ecookbook/wiki/CookBook_documentation" class="wiki-page">CookBook documentation</a></p>
417 <p><a href="/issues/1" class="issue status-1 priority-1" title="Can't print recipes (New)">#1</a></p>
446 <p><a href="/issues/1" class="issue status-1 priority-1" title="Can't print recipes (New)">#1</a></p>
418 <pre>
447 <pre>
419 [[CookBook documentation]]
448 [[CookBook documentation]]
420
449
421 #1
450 #1
422 </pre>
451 </pre>
423 EXPECTED
452 EXPECTED
424
453
425 @project = Project.find(1)
454 @project = Project.find(1)
426 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
455 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
427 end
456 end
428
457
429 def test_non_closing_pre_blocks_should_be_closed
458 def test_non_closing_pre_blocks_should_be_closed
430 raw = <<-RAW
459 raw = <<-RAW
431 <pre><code>
460 <pre><code>
432 RAW
461 RAW
433
462
434 expected = <<-EXPECTED
463 expected = <<-EXPECTED
435 <pre><code>
464 <pre><code>
436 </code></pre>
465 </code></pre>
437 EXPECTED
466 EXPECTED
438
467
439 @project = Project.find(1)
468 @project = Project.find(1)
440 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
469 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
441 end
470 end
442
471
443 def test_syntax_highlight
472 def test_syntax_highlight
444 raw = <<-RAW
473 raw = <<-RAW
445 <pre><code class="ruby">
474 <pre><code class="ruby">
446 # Some ruby code here
475 # Some ruby code here
447 </code></pre>
476 </code></pre>
448 RAW
477 RAW
449
478
450 expected = <<-EXPECTED
479 expected = <<-EXPECTED
451 <pre><code class="ruby syntaxhl"><span class=\"CodeRay\"><span class="no">1</span> <span class="c"># Some ruby code here</span></span>
480 <pre><code class="ruby syntaxhl"><span class=\"CodeRay\"><span class="no">1</span> <span class="c"># Some ruby code here</span></span>
452 </code></pre>
481 </code></pre>
453 EXPECTED
482 EXPECTED
454
483
455 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
484 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
456 end
485 end
457
486
458 def test_wiki_links_in_tables
487 def test_wiki_links_in_tables
459 to_test = {"|[[Page|Link title]]|[[Other Page|Other title]]|\n|Cell 21|[[Last page]]|" =>
488 to_test = {"|[[Page|Link title]]|[[Other Page|Other title]]|\n|Cell 21|[[Last page]]|" =>
460 '<tr><td><a href="/projects/ecookbook/wiki/Page" class="wiki-page new">Link title</a></td>' +
489 '<tr><td><a href="/projects/ecookbook/wiki/Page" class="wiki-page new">Link title</a></td>' +
461 '<td><a href="/projects/ecookbook/wiki/Other_Page" class="wiki-page new">Other title</a></td>' +
490 '<td><a href="/projects/ecookbook/wiki/Other_Page" class="wiki-page new">Other title</a></td>' +
462 '</tr><tr><td>Cell 21</td><td><a href="/projects/ecookbook/wiki/Last_page" class="wiki-page new">Last page</a></td></tr>'
491 '</tr><tr><td>Cell 21</td><td><a href="/projects/ecookbook/wiki/Last_page" class="wiki-page new">Last page</a></td></tr>'
463 }
492 }
464 @project = Project.find(1)
493 @project = Project.find(1)
465 to_test.each { |text, result| assert_equal "<table>#{result}</table>", textilizable(text).gsub(/[\t\n]/, '') }
494 to_test.each { |text, result| assert_equal "<table>#{result}</table>", textilizable(text).gsub(/[\t\n]/, '') }
466 end
495 end
467
496
468 def test_text_formatting
497 def test_text_formatting
469 to_test = {'*_+bold, italic and underline+_*' => '<strong><em><ins>bold, italic and underline</ins></em></strong>',
498 to_test = {'*_+bold, italic and underline+_*' => '<strong><em><ins>bold, italic and underline</ins></em></strong>',
470 '(_text within parentheses_)' => '(<em>text within parentheses</em>)',
499 '(_text within parentheses_)' => '(<em>text within parentheses</em>)',
471 'a *Humane Web* Text Generator' => 'a <strong>Humane Web</strong> Text Generator',
500 'a *Humane Web* Text Generator' => 'a <strong>Humane Web</strong> Text Generator',
472 '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>',
501 '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>',
473 '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',
502 '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',
474 }
503 }
475 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
504 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
476 end
505 end
477
506
478 def test_wiki_horizontal_rule
507 def test_wiki_horizontal_rule
479 assert_equal '<hr />', textilizable('---')
508 assert_equal '<hr />', textilizable('---')
480 assert_equal '<p>Dashes: ---</p>', textilizable('Dashes: ---')
509 assert_equal '<p>Dashes: ---</p>', textilizable('Dashes: ---')
481 end
510 end
482
511
483 def test_footnotes
512 def test_footnotes
484 raw = <<-RAW
513 raw = <<-RAW
485 This is some text[1].
514 This is some text[1].
486
515
487 fn1. This is the foot note
516 fn1. This is the foot note
488 RAW
517 RAW
489
518
490 expected = <<-EXPECTED
519 expected = <<-EXPECTED
491 <p>This is some text<sup><a href=\"#fn1\">1</a></sup>.</p>
520 <p>This is some text<sup><a href=\"#fn1\">1</a></sup>.</p>
492 <p id="fn1" class="footnote"><sup>1</sup> This is the foot note</p>
521 <p id="fn1" class="footnote"><sup>1</sup> This is the foot note</p>
493 EXPECTED
522 EXPECTED
494
523
495 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
524 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
496 end
525 end
497
526
498 def test_table_of_content
527 def test_table_of_content
499 raw = <<-RAW
528 raw = <<-RAW
500 {{toc}}
529 {{toc}}
501
530
502 h1. Title
531 h1. Title
503
532
504 Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas sed libero.
533 Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas sed libero.
505
534
506 h2. Subtitle with a [[Wiki]] link
535 h2. Subtitle with a [[Wiki]] link
507
536
508 Nullam commodo metus accumsan nulla. Curabitur lobortis dui id dolor.
537 Nullam commodo metus accumsan nulla. Curabitur lobortis dui id dolor.
509
538
510 h2. Subtitle with [[Wiki|another Wiki]] link
539 h2. Subtitle with [[Wiki|another Wiki]] link
511
540
512 h2. Subtitle with %{color:red}red text%
541 h2. Subtitle with %{color:red}red text%
513
542
514 <pre>
543 <pre>
515 some code
544 some code
516 </pre>
545 </pre>
517
546
518 h3. Subtitle with *some* _modifiers_
547 h3. Subtitle with *some* _modifiers_
519
548
520 h1. Another title
549 h1. Another title
521
550
522 h3. An "Internet link":http://www.redmine.org/ inside subtitle
551 h3. An "Internet link":http://www.redmine.org/ inside subtitle
523
552
524 h2. "Project Name !/attachments/1234/logo_small.gif! !/attachments/5678/logo_2.png!":/projects/projectname/issues
553 h2. "Project Name !/attachments/1234/logo_small.gif! !/attachments/5678/logo_2.png!":/projects/projectname/issues
525
554
526 RAW
555 RAW
527
556
528 expected = '<ul class="toc">' +
557 expected = '<ul class="toc">' +
529 '<li><a href="#Title">Title</a>' +
558 '<li><a href="#Title">Title</a>' +
530 '<ul>' +
559 '<ul>' +
531 '<li><a href="#Subtitle-with-a-Wiki-link">Subtitle with a Wiki link</a></li>' +
560 '<li><a href="#Subtitle-with-a-Wiki-link">Subtitle with a Wiki link</a></li>' +
532 '<li><a href="#Subtitle-with-another-Wiki-link">Subtitle with another Wiki link</a></li>' +
561 '<li><a href="#Subtitle-with-another-Wiki-link">Subtitle with another Wiki link</a></li>' +
533 '<li><a href="#Subtitle-with-red-text">Subtitle with red text</a>' +
562 '<li><a href="#Subtitle-with-red-text">Subtitle with red text</a>' +
534 '<ul>' +
563 '<ul>' +
535 '<li><a href="#Subtitle-with-some-modifiers">Subtitle with some modifiers</a></li>' +
564 '<li><a href="#Subtitle-with-some-modifiers">Subtitle with some modifiers</a></li>' +
536 '</ul>' +
565 '</ul>' +
537 '</li>' +
566 '</li>' +
538 '</ul>' +
567 '</ul>' +
539 '</li>' +
568 '</li>' +
540 '<li><a href="#Another-title">Another title</a>' +
569 '<li><a href="#Another-title">Another title</a>' +
541 '<ul>' +
570 '<ul>' +
542 '<li>' +
571 '<li>' +
543 '<ul>' +
572 '<ul>' +
544 '<li><a href="#An-Internet-link-inside-subtitle">An Internet link inside subtitle</a></li>' +
573 '<li><a href="#An-Internet-link-inside-subtitle">An Internet link inside subtitle</a></li>' +
545 '</ul>' +
574 '</ul>' +
546 '</li>' +
575 '</li>' +
547 '<li><a href="#Project-Name">Project Name</a></li>' +
576 '<li><a href="#Project-Name">Project Name</a></li>' +
548 '</ul>' +
577 '</ul>' +
549 '</li>' +
578 '</li>' +
550 '</ul>'
579 '</ul>'
551
580
552 @project = Project.find(1)
581 @project = Project.find(1)
553 assert textilizable(raw).gsub("\n", "").include?(expected), textilizable(raw)
582 assert textilizable(raw).gsub("\n", "").include?(expected), textilizable(raw)
554 end
583 end
555
584
556 def test_table_of_content_should_contain_included_page_headings
585 def test_table_of_content_should_contain_included_page_headings
557 raw = <<-RAW
586 raw = <<-RAW
558 {{toc}}
587 {{toc}}
559
588
560 h1. Included
589 h1. Included
561
590
562 {{include(Child_1)}}
591 {{include(Child_1)}}
563 RAW
592 RAW
564
593
565 expected = '<ul class="toc">' +
594 expected = '<ul class="toc">' +
566 '<li><a href="#Included">Included</a></li>' +
595 '<li><a href="#Included">Included</a></li>' +
567 '<li><a href="#Child-page-1">Child page 1</a></li>' +
596 '<li><a href="#Child-page-1">Child page 1</a></li>' +
568 '</ul>'
597 '</ul>'
569
598
570 @project = Project.find(1)
599 @project = Project.find(1)
571 assert textilizable(raw).gsub("\n", "").include?(expected)
600 assert textilizable(raw).gsub("\n", "").include?(expected)
572 end
601 end
573
602
574 def test_blockquote
603 def test_blockquote
575 # orig raw text
604 # orig raw text
576 raw = <<-RAW
605 raw = <<-RAW
577 John said:
606 John said:
578 > Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas sed libero.
607 > Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas sed libero.
579 > Nullam commodo metus accumsan nulla. Curabitur lobortis dui id dolor.
608 > Nullam commodo metus accumsan nulla. Curabitur lobortis dui id dolor.
580 > * Donec odio lorem,
609 > * Donec odio lorem,
581 > * sagittis ac,
610 > * sagittis ac,
582 > * malesuada in,
611 > * malesuada in,
583 > * adipiscing eu, dolor.
612 > * adipiscing eu, dolor.
584 >
613 >
585 > >Nulla varius pulvinar diam. Proin id arcu id lorem scelerisque condimentum. Proin vehicula turpis vitae lacus.
614 > >Nulla varius pulvinar diam. Proin id arcu id lorem scelerisque condimentum. Proin vehicula turpis vitae lacus.
586 > Proin a tellus. Nam vel neque.
615 > Proin a tellus. Nam vel neque.
587
616
588 He's right.
617 He's right.
589 RAW
618 RAW
590
619
591 # expected html
620 # expected html
592 expected = <<-EXPECTED
621 expected = <<-EXPECTED
593 <p>John said:</p>
622 <p>John said:</p>
594 <blockquote>
623 <blockquote>
595 Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas sed libero.
624 Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas sed libero.
596 Nullam commodo metus accumsan nulla. Curabitur lobortis dui id dolor.
625 Nullam commodo metus accumsan nulla. Curabitur lobortis dui id dolor.
597 <ul>
626 <ul>
598 <li>Donec odio lorem,</li>
627 <li>Donec odio lorem,</li>
599 <li>sagittis ac,</li>
628 <li>sagittis ac,</li>
600 <li>malesuada in,</li>
629 <li>malesuada in,</li>
601 <li>adipiscing eu, dolor.</li>
630 <li>adipiscing eu, dolor.</li>
602 </ul>
631 </ul>
603 <blockquote>
632 <blockquote>
604 <p>Nulla varius pulvinar diam. Proin id arcu id lorem scelerisque condimentum. Proin vehicula turpis vitae lacus.</p>
633 <p>Nulla varius pulvinar diam. Proin id arcu id lorem scelerisque condimentum. Proin vehicula turpis vitae lacus.</p>
605 </blockquote>
634 </blockquote>
606 <p>Proin a tellus. Nam vel neque.</p>
635 <p>Proin a tellus. Nam vel neque.</p>
607 </blockquote>
636 </blockquote>
608 <p>He's right.</p>
637 <p>He's right.</p>
609 EXPECTED
638 EXPECTED
610
639
611 assert_equal expected.gsub(%r{\s+}, ''), textilizable(raw).gsub(%r{\s+}, '')
640 assert_equal expected.gsub(%r{\s+}, ''), textilizable(raw).gsub(%r{\s+}, '')
612 end
641 end
613
642
614 def test_table
643 def test_table
615 raw = <<-RAW
644 raw = <<-RAW
616 This is a table with empty cells:
645 This is a table with empty cells:
617
646
618 |cell11|cell12||
647 |cell11|cell12||
619 |cell21||cell23|
648 |cell21||cell23|
620 |cell31|cell32|cell33|
649 |cell31|cell32|cell33|
621 RAW
650 RAW
622
651
623 expected = <<-EXPECTED
652 expected = <<-EXPECTED
624 <p>This is a table with empty cells:</p>
653 <p>This is a table with empty cells:</p>
625
654
626 <table>
655 <table>
627 <tr><td>cell11</td><td>cell12</td><td></td></tr>
656 <tr><td>cell11</td><td>cell12</td><td></td></tr>
628 <tr><td>cell21</td><td></td><td>cell23</td></tr>
657 <tr><td>cell21</td><td></td><td>cell23</td></tr>
629 <tr><td>cell31</td><td>cell32</td><td>cell33</td></tr>
658 <tr><td>cell31</td><td>cell32</td><td>cell33</td></tr>
630 </table>
659 </table>
631 EXPECTED
660 EXPECTED
632
661
633 assert_equal expected.gsub(%r{\s+}, ''), textilizable(raw).gsub(%r{\s+}, '')
662 assert_equal expected.gsub(%r{\s+}, ''), textilizable(raw).gsub(%r{\s+}, '')
634 end
663 end
635
664
636 def test_table_with_line_breaks
665 def test_table_with_line_breaks
637 raw = <<-RAW
666 raw = <<-RAW
638 This is a table with line breaks:
667 This is a table with line breaks:
639
668
640 |cell11
669 |cell11
641 continued|cell12||
670 continued|cell12||
642 |-cell21-||cell23
671 |-cell21-||cell23
643 cell23 line2
672 cell23 line2
644 cell23 *line3*|
673 cell23 *line3*|
645 |cell31|cell32
674 |cell31|cell32
646 cell32 line2|cell33|
675 cell32 line2|cell33|
647
676
648 RAW
677 RAW
649
678
650 expected = <<-EXPECTED
679 expected = <<-EXPECTED
651 <p>This is a table with line breaks:</p>
680 <p>This is a table with line breaks:</p>
652
681
653 <table>
682 <table>
654 <tr>
683 <tr>
655 <td>cell11<br />continued</td>
684 <td>cell11<br />continued</td>
656 <td>cell12</td>
685 <td>cell12</td>
657 <td></td>
686 <td></td>
658 </tr>
687 </tr>
659 <tr>
688 <tr>
660 <td><del>cell21</del></td>
689 <td><del>cell21</del></td>
661 <td></td>
690 <td></td>
662 <td>cell23<br/>cell23 line2<br/>cell23 <strong>line3</strong></td>
691 <td>cell23<br/>cell23 line2<br/>cell23 <strong>line3</strong></td>
663 </tr>
692 </tr>
664 <tr>
693 <tr>
665 <td>cell31</td>
694 <td>cell31</td>
666 <td>cell32<br/>cell32 line2</td>
695 <td>cell32<br/>cell32 line2</td>
667 <td>cell33</td>
696 <td>cell33</td>
668 </tr>
697 </tr>
669 </table>
698 </table>
670 EXPECTED
699 EXPECTED
671
700
672 assert_equal expected.gsub(%r{\s+}, ''), textilizable(raw).gsub(%r{\s+}, '')
701 assert_equal expected.gsub(%r{\s+}, ''), textilizable(raw).gsub(%r{\s+}, '')
673 end
702 end
674
703
675 def test_textile_should_not_mangle_brackets
704 def test_textile_should_not_mangle_brackets
676 assert_equal '<p>[msg1][msg2]</p>', textilizable('[msg1][msg2]')
705 assert_equal '<p>[msg1][msg2]</p>', textilizable('[msg1][msg2]')
677 end
706 end
678
707
679 def test_default_formatter
708 def test_default_formatter
680 Setting.text_formatting = 'unknown'
709 Setting.text_formatting = 'unknown'
681 text = 'a *link*: http://www.example.net/'
710 text = 'a *link*: http://www.example.net/'
682 assert_equal '<p>a *link*: <a href="http://www.example.net/">http://www.example.net/</a></p>', textilizable(text)
711 assert_equal '<p>a *link*: <a href="http://www.example.net/">http://www.example.net/</a></p>', textilizable(text)
683 Setting.text_formatting = 'textile'
712 Setting.text_formatting = 'textile'
684 end
713 end
685
714
686 def test_due_date_distance_in_words
715 def test_due_date_distance_in_words
687 to_test = { Date.today => 'Due in 0 days',
716 to_test = { Date.today => 'Due in 0 days',
688 Date.today + 1 => 'Due in 1 day',
717 Date.today + 1 => 'Due in 1 day',
689 Date.today + 100 => 'Due in about 3 months',
718 Date.today + 100 => 'Due in about 3 months',
690 Date.today + 20000 => 'Due in over 54 years',
719 Date.today + 20000 => 'Due in over 54 years',
691 Date.today - 1 => '1 day late',
720 Date.today - 1 => '1 day late',
692 Date.today - 100 => 'about 3 months late',
721 Date.today - 100 => 'about 3 months late',
693 Date.today - 20000 => 'over 54 years late',
722 Date.today - 20000 => 'over 54 years late',
694 }
723 }
695 ::I18n.locale = :en
724 ::I18n.locale = :en
696 to_test.each do |date, expected|
725 to_test.each do |date, expected|
697 assert_equal expected, due_date_distance_in_words(date)
726 assert_equal expected, due_date_distance_in_words(date)
698 end
727 end
699 end
728 end
700
729
701 def test_avatar
730 def test_avatar
702 # turn on avatars
731 # turn on avatars
703 Setting.gravatar_enabled = '1'
732 Setting.gravatar_enabled = '1'
704 assert avatar(User.find_by_mail('jsmith@somenet.foo')).include?(Digest::MD5.hexdigest('jsmith@somenet.foo'))
733 assert avatar(User.find_by_mail('jsmith@somenet.foo')).include?(Digest::MD5.hexdigest('jsmith@somenet.foo'))
705 assert avatar('jsmith <jsmith@somenet.foo>').include?(Digest::MD5.hexdigest('jsmith@somenet.foo'))
734 assert avatar('jsmith <jsmith@somenet.foo>').include?(Digest::MD5.hexdigest('jsmith@somenet.foo'))
706 assert_nil avatar('jsmith')
735 assert_nil avatar('jsmith')
707 assert_nil avatar(nil)
736 assert_nil avatar(nil)
708
737
709 # turn off avatars
738 # turn off avatars
710 Setting.gravatar_enabled = '0'
739 Setting.gravatar_enabled = '0'
711 assert_equal '', avatar(User.find_by_mail('jsmith@somenet.foo'))
740 assert_equal '', avatar(User.find_by_mail('jsmith@somenet.foo'))
712 end
741 end
713
742
714 def test_link_to_user
743 def test_link_to_user
715 user = User.find(2)
744 user = User.find(2)
716 t = link_to_user(user)
745 t = link_to_user(user)
717 assert_equal "<a href=\"/users/2\">#{ user.name }</a>", t
746 assert_equal "<a href=\"/users/2\">#{ user.name }</a>", t
718 end
747 end
719
748
720 def test_link_to_user_should_not_link_to_locked_user
749 def test_link_to_user_should_not_link_to_locked_user
721 user = User.find(5)
750 user = User.find(5)
722 assert user.locked?
751 assert user.locked?
723 t = link_to_user(user)
752 t = link_to_user(user)
724 assert_equal user.name, t
753 assert_equal user.name, t
725 end
754 end
726
755
727 def test_link_to_user_should_not_link_to_anonymous
756 def test_link_to_user_should_not_link_to_anonymous
728 user = User.anonymous
757 user = User.anonymous
729 assert user.anonymous?
758 assert user.anonymous?
730 t = link_to_user(user)
759 t = link_to_user(user)
731 assert_equal ::I18n.t(:label_user_anonymous), t
760 assert_equal ::I18n.t(:label_user_anonymous), t
732 end
761 end
733
762
734 def test_link_to_project
763 def test_link_to_project
735 project = Project.find(1)
764 project = Project.find(1)
736 assert_equal %(<a href="/projects/ecookbook">eCookbook</a>),
765 assert_equal %(<a href="/projects/ecookbook">eCookbook</a>),
737 link_to_project(project)
766 link_to_project(project)
738 assert_equal %(<a href="/projects/ecookbook/settings">eCookbook</a>),
767 assert_equal %(<a href="/projects/ecookbook/settings">eCookbook</a>),
739 link_to_project(project, :action => 'settings')
768 link_to_project(project, :action => 'settings')
740 assert_equal %(<a href="http://test.host/projects/ecookbook?jump=blah">eCookbook</a>),
769 assert_equal %(<a href="http://test.host/projects/ecookbook?jump=blah">eCookbook</a>),
741 link_to_project(project, {:only_path => false, :jump => 'blah'})
770 link_to_project(project, {:only_path => false, :jump => 'blah'})
742 assert_equal %(<a href="/projects/ecookbook/settings" class="project">eCookbook</a>),
771 assert_equal %(<a href="/projects/ecookbook/settings" class="project">eCookbook</a>),
743 link_to_project(project, {:action => 'settings'}, :class => "project")
772 link_to_project(project, {:action => 'settings'}, :class => "project")
744 end
773 end
745 end
774 end
General Comments 0
You need to be logged in to leave comments. Login now