##// END OF EJS Templates
Extract headings and TOC parsing from the textile formatter....
Jean-Philippe Lang -
r4262:024ff96ee27a
parent child
Show More
@@ -1,841 +1,873
1 # redMine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2007 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
107
108 link_to(text, {:controller => 'repositories', :action => 'revision', :id => project, :rev => revision}, :title => l(:label_revision_id, revision))
108 link_to(text, {:controller => 'repositories', :action => 'revision', :id => project, :rev => revision}, :title => l(:label_revision_id, revision))
109 end
109 end
110
110
111 # Generates a link to a project if active
111 # Generates a link to a project if active
112 # Examples:
112 # Examples:
113 #
113 #
114 # link_to_project(project) # => link to the specified project overview
114 # link_to_project(project) # => link to the specified project overview
115 # link_to_project(project, :action=>'settings') # => link to project settings
115 # link_to_project(project, :action=>'settings') # => link to project settings
116 # link_to_project(project, {:only_path => false}, :class => "project") # => 3rd arg adds html options
116 # link_to_project(project, {:only_path => false}, :class => "project") # => 3rd arg adds html options
117 # link_to_project(project, {}, :class => "project") # => html options with default url (project overview)
117 # link_to_project(project, {}, :class => "project") # => html options with default url (project overview)
118 #
118 #
119 def link_to_project(project, options={}, html_options = nil)
119 def link_to_project(project, options={}, html_options = nil)
120 if project.active?
120 if project.active?
121 url = {:controller => 'projects', :action => 'show', :id => project}.merge(options)
121 url = {:controller => 'projects', :action => 'show', :id => project}.merge(options)
122 link_to(h(project), url, html_options)
122 link_to(h(project), url, html_options)
123 else
123 else
124 h(project)
124 h(project)
125 end
125 end
126 end
126 end
127
127
128 def toggle_link(name, id, options={})
128 def toggle_link(name, id, options={})
129 onclick = "Element.toggle('#{id}'); "
129 onclick = "Element.toggle('#{id}'); "
130 onclick << (options[:focus] ? "Form.Element.focus('#{options[:focus]}'); " : "this.blur(); ")
130 onclick << (options[:focus] ? "Form.Element.focus('#{options[:focus]}'); " : "this.blur(); ")
131 onclick << "return false;"
131 onclick << "return false;"
132 link_to(name, "#", :onclick => onclick)
132 link_to(name, "#", :onclick => onclick)
133 end
133 end
134
134
135 def image_to_function(name, function, html_options = {})
135 def image_to_function(name, function, html_options = {})
136 html_options.symbolize_keys!
136 html_options.symbolize_keys!
137 tag(:input, html_options.merge({
137 tag(:input, html_options.merge({
138 :type => "image", :src => image_path(name),
138 :type => "image", :src => image_path(name),
139 :onclick => (html_options[:onclick] ? "#{html_options[:onclick]}; " : "") + "#{function};"
139 :onclick => (html_options[:onclick] ? "#{html_options[:onclick]}; " : "") + "#{function};"
140 }))
140 }))
141 end
141 end
142
142
143 def prompt_to_remote(name, text, param, url, html_options = {})
143 def prompt_to_remote(name, text, param, url, html_options = {})
144 html_options[:onclick] = "promptToRemote('#{text}', '#{param}', '#{url_for(url)}'); return false;"
144 html_options[:onclick] = "promptToRemote('#{text}', '#{param}', '#{url_for(url)}'); return false;"
145 link_to name, {}, html_options
145 link_to name, {}, html_options
146 end
146 end
147
147
148 def format_activity_title(text)
148 def format_activity_title(text)
149 h(truncate_single_line(text, :length => 100))
149 h(truncate_single_line(text, :length => 100))
150 end
150 end
151
151
152 def format_activity_day(date)
152 def format_activity_day(date)
153 date == Date.today ? l(:label_today).titleize : format_date(date)
153 date == Date.today ? l(:label_today).titleize : format_date(date)
154 end
154 end
155
155
156 def format_activity_description(text)
156 def format_activity_description(text)
157 h(truncate(text.to_s, :length => 120).gsub(%r{[\r\n]*<(pre|code)>.*$}m, '...')).gsub(/[\r\n]+/, "<br />")
157 h(truncate(text.to_s, :length => 120).gsub(%r{[\r\n]*<(pre|code)>.*$}m, '...')).gsub(/[\r\n]+/, "<br />")
158 end
158 end
159
159
160 def format_version_name(version)
160 def format_version_name(version)
161 if version.project == @project
161 if version.project == @project
162 h(version)
162 h(version)
163 else
163 else
164 h("#{version.project} - #{version}")
164 h("#{version.project} - #{version}")
165 end
165 end
166 end
166 end
167
167
168 def due_date_distance_in_words(date)
168 def due_date_distance_in_words(date)
169 if date
169 if date
170 l((date < Date.today ? :label_roadmap_overdue : :label_roadmap_due_in), distance_of_date_in_words(Date.today, date))
170 l((date < Date.today ? :label_roadmap_overdue : :label_roadmap_due_in), distance_of_date_in_words(Date.today, date))
171 end
171 end
172 end
172 end
173
173
174 def render_page_hierarchy(pages, node=nil)
174 def render_page_hierarchy(pages, node=nil)
175 content = ''
175 content = ''
176 if pages[node]
176 if pages[node]
177 content << "<ul class=\"pages-hierarchy\">\n"
177 content << "<ul class=\"pages-hierarchy\">\n"
178 pages[node].each do |page|
178 pages[node].each do |page|
179 content << "<li>"
179 content << "<li>"
180 content << link_to(h(page.pretty_title), {:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title},
180 content << link_to(h(page.pretty_title), {:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title},
181 :title => (page.respond_to?(:updated_on) ? l(:label_updated_time, distance_of_time_in_words(Time.now, page.updated_on)) : nil))
181 :title => (page.respond_to?(:updated_on) ? l(:label_updated_time, distance_of_time_in_words(Time.now, page.updated_on)) : nil))
182 content << "\n" + render_page_hierarchy(pages, page.id) if pages[page.id]
182 content << "\n" + render_page_hierarchy(pages, page.id) if pages[page.id]
183 content << "</li>\n"
183 content << "</li>\n"
184 end
184 end
185 content << "</ul>\n"
185 content << "</ul>\n"
186 end
186 end
187 content
187 content
188 end
188 end
189
189
190 # Renders flash messages
190 # Renders flash messages
191 def render_flash_messages
191 def render_flash_messages
192 s = ''
192 s = ''
193 flash.each do |k,v|
193 flash.each do |k,v|
194 s << content_tag('div', v, :class => "flash #{k}")
194 s << content_tag('div', v, :class => "flash #{k}")
195 end
195 end
196 s
196 s
197 end
197 end
198
198
199 # Renders tabs and their content
199 # Renders tabs and their content
200 def render_tabs(tabs)
200 def render_tabs(tabs)
201 if tabs.any?
201 if tabs.any?
202 render :partial => 'common/tabs', :locals => {:tabs => tabs}
202 render :partial => 'common/tabs', :locals => {:tabs => tabs}
203 else
203 else
204 content_tag 'p', l(:label_no_data), :class => "nodata"
204 content_tag 'p', l(:label_no_data), :class => "nodata"
205 end
205 end
206 end
206 end
207
207
208 # Renders the project quick-jump box
208 # Renders the project quick-jump box
209 def render_project_jump_box
209 def render_project_jump_box
210 # Retrieve them now to avoid a COUNT query
210 # Retrieve them now to avoid a COUNT query
211 projects = User.current.projects.all
211 projects = User.current.projects.all
212 if projects.any?
212 if projects.any?
213 s = '<select onchange="if (this.value != \'\') { window.location = this.value; }">' +
213 s = '<select onchange="if (this.value != \'\') { window.location = this.value; }">' +
214 "<option value=''>#{ l(:label_jump_to_a_project) }</option>" +
214 "<option value=''>#{ l(:label_jump_to_a_project) }</option>" +
215 '<option value="" disabled="disabled">---</option>'
215 '<option value="" disabled="disabled">---</option>'
216 s << project_tree_options_for_select(projects, :selected => @project) do |p|
216 s << project_tree_options_for_select(projects, :selected => @project) do |p|
217 { :value => url_for(:controller => 'projects', :action => 'show', :id => p, :jump => current_menu_item) }
217 { :value => url_for(:controller => 'projects', :action => 'show', :id => p, :jump => current_menu_item) }
218 end
218 end
219 s << '</select>'
219 s << '</select>'
220 s
220 s
221 end
221 end
222 end
222 end
223
223
224 def project_tree_options_for_select(projects, options = {})
224 def project_tree_options_for_select(projects, options = {})
225 s = ''
225 s = ''
226 project_tree(projects) do |project, level|
226 project_tree(projects) do |project, level|
227 name_prefix = (level > 0 ? ('&nbsp;' * 2 * level + '&#187; ') : '')
227 name_prefix = (level > 0 ? ('&nbsp;' * 2 * level + '&#187; ') : '')
228 tag_options = {:value => project.id}
228 tag_options = {:value => project.id}
229 if project == options[:selected] || (options[:selected].respond_to?(:include?) && options[:selected].include?(project))
229 if project == options[:selected] || (options[:selected].respond_to?(:include?) && options[:selected].include?(project))
230 tag_options[:selected] = 'selected'
230 tag_options[:selected] = 'selected'
231 else
231 else
232 tag_options[:selected] = nil
232 tag_options[:selected] = nil
233 end
233 end
234 tag_options.merge!(yield(project)) if block_given?
234 tag_options.merge!(yield(project)) if block_given?
235 s << content_tag('option', name_prefix + h(project), tag_options)
235 s << content_tag('option', name_prefix + h(project), tag_options)
236 end
236 end
237 s
237 s
238 end
238 end
239
239
240 # Yields the given block for each project with its level in the tree
240 # Yields the given block for each project with its level in the tree
241 #
241 #
242 # Wrapper for Project#project_tree
242 # Wrapper for Project#project_tree
243 def project_tree(projects, &block)
243 def project_tree(projects, &block)
244 Project.project_tree(projects, &block)
244 Project.project_tree(projects, &block)
245 end
245 end
246
246
247 def project_nested_ul(projects, &block)
247 def project_nested_ul(projects, &block)
248 s = ''
248 s = ''
249 if projects.any?
249 if projects.any?
250 ancestors = []
250 ancestors = []
251 projects.sort_by(&:lft).each do |project|
251 projects.sort_by(&:lft).each do |project|
252 if (ancestors.empty? || project.is_descendant_of?(ancestors.last))
252 if (ancestors.empty? || project.is_descendant_of?(ancestors.last))
253 s << "<ul>\n"
253 s << "<ul>\n"
254 else
254 else
255 ancestors.pop
255 ancestors.pop
256 s << "</li>"
256 s << "</li>"
257 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
257 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
258 ancestors.pop
258 ancestors.pop
259 s << "</ul></li>\n"
259 s << "</ul></li>\n"
260 end
260 end
261 end
261 end
262 s << "<li>"
262 s << "<li>"
263 s << yield(project).to_s
263 s << yield(project).to_s
264 ancestors << project
264 ancestors << project
265 end
265 end
266 s << ("</li></ul>\n" * ancestors.size)
266 s << ("</li></ul>\n" * ancestors.size)
267 end
267 end
268 s
268 s
269 end
269 end
270
270
271 def principals_check_box_tags(name, principals)
271 def principals_check_box_tags(name, principals)
272 s = ''
272 s = ''
273 principals.sort.each do |principal|
273 principals.sort.each do |principal|
274 s << "<label>#{ check_box_tag name, principal.id, false } #{h principal}</label>\n"
274 s << "<label>#{ check_box_tag name, principal.id, false } #{h principal}</label>\n"
275 end
275 end
276 s
276 s
277 end
277 end
278
278
279 # Truncates and returns the string as a single line
279 # Truncates and returns the string as a single line
280 def truncate_single_line(string, *args)
280 def truncate_single_line(string, *args)
281 truncate(string.to_s, *args).gsub(%r{[\r\n]+}m, ' ')
281 truncate(string.to_s, *args).gsub(%r{[\r\n]+}m, ' ')
282 end
282 end
283
283
284 # Truncates at line break after 250 characters or options[:length]
284 # Truncates at line break after 250 characters or options[:length]
285 def truncate_lines(string, options={})
285 def truncate_lines(string, options={})
286 length = options[:length] || 250
286 length = options[:length] || 250
287 if string.to_s =~ /\A(.{#{length}}.*?)$/m
287 if string.to_s =~ /\A(.{#{length}}.*?)$/m
288 "#{$1}..."
288 "#{$1}..."
289 else
289 else
290 string
290 string
291 end
291 end
292 end
292 end
293
293
294 def html_hours(text)
294 def html_hours(text)
295 text.gsub(%r{(\d+)\.(\d+)}, '<span class="hours hours-int">\1</span><span class="hours hours-dec">.\2</span>')
295 text.gsub(%r{(\d+)\.(\d+)}, '<span class="hours hours-int">\1</span><span class="hours hours-dec">.\2</span>')
296 end
296 end
297
297
298 def authoring(created, author, options={})
298 def authoring(created, author, options={})
299 l(options[:label] || :label_added_time_by, :author => link_to_user(author), :age => time_tag(created))
299 l(options[:label] || :label_added_time_by, :author => link_to_user(author), :age => time_tag(created))
300 end
300 end
301
301
302 def time_tag(time)
302 def time_tag(time)
303 text = distance_of_time_in_words(Time.now, time)
303 text = distance_of_time_in_words(Time.now, time)
304 if @project
304 if @project
305 link_to(text, {:controller => 'activities', :action => 'index', :id => @project, :from => time.to_date}, :title => format_time(time))
305 link_to(text, {:controller => 'activities', :action => 'index', :id => @project, :from => time.to_date}, :title => format_time(time))
306 else
306 else
307 content_tag('acronym', text, :title => format_time(time))
307 content_tag('acronym', text, :title => format_time(time))
308 end
308 end
309 end
309 end
310
310
311 def syntax_highlight(name, content)
311 def syntax_highlight(name, content)
312 Redmine::SyntaxHighlighting.highlight_by_filename(content, name)
312 Redmine::SyntaxHighlighting.highlight_by_filename(content, name)
313 end
313 end
314
314
315 def to_path_param(path)
315 def to_path_param(path)
316 path.to_s.split(%r{[/\\]}).select {|p| !p.blank?}
316 path.to_s.split(%r{[/\\]}).select {|p| !p.blank?}
317 end
317 end
318
318
319 def pagination_links_full(paginator, count=nil, options={})
319 def pagination_links_full(paginator, count=nil, options={})
320 page_param = options.delete(:page_param) || :page
320 page_param = options.delete(:page_param) || :page
321 per_page_links = options.delete(:per_page_links)
321 per_page_links = options.delete(:per_page_links)
322 url_param = params.dup
322 url_param = params.dup
323 # don't reuse query params if filters are present
323 # don't reuse query params if filters are present
324 url_param.merge!(:fields => nil, :values => nil, :operators => nil) if url_param.delete(:set_filter)
324 url_param.merge!(:fields => nil, :values => nil, :operators => nil) if url_param.delete(:set_filter)
325
325
326 html = ''
326 html = ''
327 if paginator.current.previous
327 if paginator.current.previous
328 html << link_to_remote_content_update('&#171; ' + l(:label_previous), url_param.merge(page_param => paginator.current.previous)) + ' '
328 html << link_to_remote_content_update('&#171; ' + l(:label_previous), url_param.merge(page_param => paginator.current.previous)) + ' '
329 end
329 end
330
330
331 html << (pagination_links_each(paginator, options) do |n|
331 html << (pagination_links_each(paginator, options) do |n|
332 link_to_remote_content_update(n.to_s, url_param.merge(page_param => n))
332 link_to_remote_content_update(n.to_s, url_param.merge(page_param => n))
333 end || '')
333 end || '')
334
334
335 if paginator.current.next
335 if paginator.current.next
336 html << ' ' + link_to_remote_content_update((l(:label_next) + ' &#187;'), url_param.merge(page_param => paginator.current.next))
336 html << ' ' + link_to_remote_content_update((l(:label_next) + ' &#187;'), url_param.merge(page_param => paginator.current.next))
337 end
337 end
338
338
339 unless count.nil?
339 unless count.nil?
340 html << " (#{paginator.current.first_item}-#{paginator.current.last_item}/#{count})"
340 html << " (#{paginator.current.first_item}-#{paginator.current.last_item}/#{count})"
341 if per_page_links != false && links = per_page_links(paginator.items_per_page)
341 if per_page_links != false && links = per_page_links(paginator.items_per_page)
342 html << " | #{links}"
342 html << " | #{links}"
343 end
343 end
344 end
344 end
345
345
346 html
346 html
347 end
347 end
348
348
349 def per_page_links(selected=nil)
349 def per_page_links(selected=nil)
350 url_param = params.dup
350 url_param = params.dup
351 url_param.clear if url_param.has_key?(:set_filter)
351 url_param.clear if url_param.has_key?(:set_filter)
352
352
353 links = Setting.per_page_options_array.collect do |n|
353 links = Setting.per_page_options_array.collect do |n|
354 n == selected ? n : link_to_remote(n, {:update => "content",
354 n == selected ? n : link_to_remote(n, {:update => "content",
355 :url => params.dup.merge(:per_page => n),
355 :url => params.dup.merge(:per_page => n),
356 :method => :get},
356 :method => :get},
357 {:href => url_for(url_param.merge(:per_page => n))})
357 {:href => url_for(url_param.merge(:per_page => n))})
358 end
358 end
359 links.size > 1 ? l(:label_display_per_page, links.join(', ')) : nil
359 links.size > 1 ? l(:label_display_per_page, links.join(', ')) : nil
360 end
360 end
361
361
362 def reorder_links(name, url)
362 def reorder_links(name, url)
363 link_to(image_tag('2uparrow.png', :alt => l(:label_sort_highest)), url.merge({"#{name}[move_to]" => 'highest'}), :method => :post, :title => l(:label_sort_highest)) +
363 link_to(image_tag('2uparrow.png', :alt => l(:label_sort_highest)), url.merge({"#{name}[move_to]" => 'highest'}), :method => :post, :title => l(:label_sort_highest)) +
364 link_to(image_tag('1uparrow.png', :alt => l(:label_sort_higher)), url.merge({"#{name}[move_to]" => 'higher'}), :method => :post, :title => l(:label_sort_higher)) +
364 link_to(image_tag('1uparrow.png', :alt => l(:label_sort_higher)), url.merge({"#{name}[move_to]" => 'higher'}), :method => :post, :title => l(:label_sort_higher)) +
365 link_to(image_tag('1downarrow.png', :alt => l(:label_sort_lower)), url.merge({"#{name}[move_to]" => 'lower'}), :method => :post, :title => l(:label_sort_lower)) +
365 link_to(image_tag('1downarrow.png', :alt => l(:label_sort_lower)), url.merge({"#{name}[move_to]" => 'lower'}), :method => :post, :title => l(:label_sort_lower)) +
366 link_to(image_tag('2downarrow.png', :alt => l(:label_sort_lowest)), url.merge({"#{name}[move_to]" => 'lowest'}), :method => :post, :title => l(:label_sort_lowest))
366 link_to(image_tag('2downarrow.png', :alt => l(:label_sort_lowest)), url.merge({"#{name}[move_to]" => 'lowest'}), :method => :post, :title => l(:label_sort_lowest))
367 end
367 end
368
368
369 def breadcrumb(*args)
369 def breadcrumb(*args)
370 elements = args.flatten
370 elements = args.flatten
371 elements.any? ? content_tag('p', args.join(' &#187; ') + ' &#187; ', :class => 'breadcrumb') : nil
371 elements.any? ? content_tag('p', args.join(' &#187; ') + ' &#187; ', :class => 'breadcrumb') : nil
372 end
372 end
373
373
374 def other_formats_links(&block)
374 def other_formats_links(&block)
375 concat('<p class="other-formats">' + l(:label_export_to))
375 concat('<p class="other-formats">' + l(:label_export_to))
376 yield Redmine::Views::OtherFormatsBuilder.new(self)
376 yield Redmine::Views::OtherFormatsBuilder.new(self)
377 concat('</p>')
377 concat('</p>')
378 end
378 end
379
379
380 def page_header_title
380 def page_header_title
381 if @project.nil? || @project.new_record?
381 if @project.nil? || @project.new_record?
382 h(Setting.app_title)
382 h(Setting.app_title)
383 else
383 else
384 b = []
384 b = []
385 ancestors = (@project.root? ? [] : @project.ancestors.visible)
385 ancestors = (@project.root? ? [] : @project.ancestors.visible)
386 if ancestors.any?
386 if ancestors.any?
387 root = ancestors.shift
387 root = ancestors.shift
388 b << link_to_project(root, {:jump => current_menu_item}, :class => 'root')
388 b << link_to_project(root, {:jump => current_menu_item}, :class => 'root')
389 if ancestors.size > 2
389 if ancestors.size > 2
390 b << '&#8230;'
390 b << '&#8230;'
391 ancestors = ancestors[-2, 2]
391 ancestors = ancestors[-2, 2]
392 end
392 end
393 b += ancestors.collect {|p| link_to_project(p, {:jump => current_menu_item}, :class => 'ancestor') }
393 b += ancestors.collect {|p| link_to_project(p, {:jump => current_menu_item}, :class => 'ancestor') }
394 end
394 end
395 b << h(@project)
395 b << h(@project)
396 b.join(' &#187; ')
396 b.join(' &#187; ')
397 end
397 end
398 end
398 end
399
399
400 def html_title(*args)
400 def html_title(*args)
401 if args.empty?
401 if args.empty?
402 title = []
402 title = []
403 title << @project.name if @project
403 title << @project.name if @project
404 title += @html_title if @html_title
404 title += @html_title if @html_title
405 title << Setting.app_title
405 title << Setting.app_title
406 title.select {|t| !t.blank? }.join(' - ')
406 title.select {|t| !t.blank? }.join(' - ')
407 else
407 else
408 @html_title ||= []
408 @html_title ||= []
409 @html_title += args
409 @html_title += args
410 end
410 end
411 end
411 end
412
412
413 # Returns the theme, controller name, and action as css classes for the
413 # Returns the theme, controller name, and action as css classes for the
414 # HTML body.
414 # HTML body.
415 def body_css_classes
415 def body_css_classes
416 css = []
416 css = []
417 if theme = Redmine::Themes.theme(Setting.ui_theme)
417 if theme = Redmine::Themes.theme(Setting.ui_theme)
418 css << 'theme-' + theme.name
418 css << 'theme-' + theme.name
419 end
419 end
420
420
421 css << 'controller-' + params[:controller]
421 css << 'controller-' + params[:controller]
422 css << 'action-' + params[:action]
422 css << 'action-' + params[:action]
423 css.join(' ')
423 css.join(' ')
424 end
424 end
425
425
426 def accesskey(s)
426 def accesskey(s)
427 Redmine::AccessKeys.key_for s
427 Redmine::AccessKeys.key_for s
428 end
428 end
429
429
430 # Formats text according to system settings.
430 # Formats text according to system settings.
431 # 2 ways to call this method:
431 # 2 ways to call this method:
432 # * with a String: textilizable(text, options)
432 # * with a String: textilizable(text, options)
433 # * with an object and one of its attribute: textilizable(issue, :description, options)
433 # * with an object and one of its attribute: textilizable(issue, :description, options)
434 def textilizable(*args)
434 def textilizable(*args)
435 options = args.last.is_a?(Hash) ? args.pop : {}
435 options = args.last.is_a?(Hash) ? args.pop : {}
436 case args.size
436 case args.size
437 when 1
437 when 1
438 obj = options[:object]
438 obj = options[:object]
439 text = args.shift
439 text = args.shift
440 when 2
440 when 2
441 obj = args.shift
441 obj = args.shift
442 attr = args.shift
442 attr = args.shift
443 text = obj.send(attr).to_s
443 text = obj.send(attr).to_s
444 else
444 else
445 raise ArgumentError, 'invalid arguments to textilizable'
445 raise ArgumentError, 'invalid arguments to textilizable'
446 end
446 end
447 return '' if text.blank?
447 return '' if text.blank?
448 project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
448 project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
449 only_path = options.delete(:only_path) == false ? false : true
449 only_path = options.delete(:only_path) == false ? false : true
450
450
451 text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text, :object => obj, :attribute => attr) { |macro, args| exec_macro(macro, obj, args) }
451 text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text, :object => obj, :attribute => attr) { |macro, args| exec_macro(macro, obj, args) }
452
452
453 parse_non_pre_blocks(text) do |text|
453 parse_non_pre_blocks(text) do |text|
454 [:parse_inline_attachments, :parse_wiki_links, :parse_redmine_links].each do |method_name|
454 [:parse_inline_attachments, :parse_wiki_links, :parse_redmine_links, :parse_headings].each do |method_name|
455 send method_name, text, project, obj, attr, only_path, options
455 send method_name, text, project, obj, attr, only_path, options
456 end
456 end
457 end
457 end
458 end
458 end
459
459
460 def parse_non_pre_blocks(text)
460 def parse_non_pre_blocks(text)
461 s = StringScanner.new(text)
461 s = StringScanner.new(text)
462 tags = []
462 tags = []
463 parsed = ''
463 parsed = ''
464 while !s.eos?
464 while !s.eos?
465 s.scan(/(.*?)(<(\/)?(pre|code)(.*?)>|\z)/im)
465 s.scan(/(.*?)(<(\/)?(pre|code)(.*?)>|\z)/im)
466 text, full_tag, closing, tag = s[1], s[2], s[3], s[4]
466 text, full_tag, closing, tag = s[1], s[2], s[3], s[4]
467 if tags.empty?
467 if tags.empty?
468 yield text
468 yield text
469 end
469 end
470 parsed << text
470 parsed << text
471 if tag
471 if tag
472 if closing
472 if closing
473 if tags.last == tag.downcase
473 if tags.last == tag.downcase
474 tags.pop
474 tags.pop
475 end
475 end
476 else
476 else
477 tags << tag.downcase
477 tags << tag.downcase
478 end
478 end
479 parsed << full_tag
479 parsed << full_tag
480 end
480 end
481 end
481 end
482 # Close any non closing tags
482 # Close any non closing tags
483 while tag = tags.pop
483 while tag = tags.pop
484 parsed << "</#{tag}>"
484 parsed << "</#{tag}>"
485 end
485 end
486 parsed
486 parsed
487 end
487 end
488
488
489 def parse_inline_attachments(text, project, obj, attr, only_path, options)
489 def parse_inline_attachments(text, project, obj, attr, only_path, options)
490 # when using an image link, try to use an attachment, if possible
490 # when using an image link, try to use an attachment, if possible
491 if options[:attachments] || (obj && obj.respond_to?(:attachments))
491 if options[:attachments] || (obj && obj.respond_to?(:attachments))
492 attachments = nil
492 attachments = nil
493 text.gsub!(/src="([^\/"]+\.(bmp|gif|jpg|jpeg|png))"(\s+alt="([^"]*)")?/i) do |m|
493 text.gsub!(/src="([^\/"]+\.(bmp|gif|jpg|jpeg|png))"(\s+alt="([^"]*)")?/i) do |m|
494 filename, ext, alt, alttext = $1.downcase, $2, $3, $4
494 filename, ext, alt, alttext = $1.downcase, $2, $3, $4
495 attachments ||= (options[:attachments] || obj.attachments).sort_by(&:created_on).reverse
495 attachments ||= (options[:attachments] || obj.attachments).sort_by(&:created_on).reverse
496 # search for the picture in attachments
496 # search for the picture in attachments
497 if found = attachments.detect { |att| att.filename.downcase == filename }
497 if found = attachments.detect { |att| att.filename.downcase == filename }
498 image_url = url_for :only_path => only_path, :controller => 'attachments', :action => 'download', :id => found
498 image_url = url_for :only_path => only_path, :controller => 'attachments', :action => 'download', :id => found
499 desc = found.description.to_s.gsub('"', '')
499 desc = found.description.to_s.gsub('"', '')
500 if !desc.blank? && alttext.blank?
500 if !desc.blank? && alttext.blank?
501 alt = " title=\"#{desc}\" alt=\"#{desc}\""
501 alt = " title=\"#{desc}\" alt=\"#{desc}\""
502 end
502 end
503 "src=\"#{image_url}\"#{alt}"
503 "src=\"#{image_url}\"#{alt}"
504 else
504 else
505 m
505 m
506 end
506 end
507 end
507 end
508 end
508 end
509 end
509 end
510
510
511 # Wiki links
511 # Wiki links
512 #
512 #
513 # Examples:
513 # Examples:
514 # [[mypage]]
514 # [[mypage]]
515 # [[mypage|mytext]]
515 # [[mypage|mytext]]
516 # wiki links can refer other project wikis, using project name or identifier:
516 # wiki links can refer other project wikis, using project name or identifier:
517 # [[project:]] -> wiki starting page
517 # [[project:]] -> wiki starting page
518 # [[project:|mytext]]
518 # [[project:|mytext]]
519 # [[project:mypage]]
519 # [[project:mypage]]
520 # [[project:mypage|mytext]]
520 # [[project:mypage|mytext]]
521 def parse_wiki_links(text, project, obj, attr, only_path, options)
521 def parse_wiki_links(text, project, obj, attr, only_path, options)
522 text.gsub!(/(!)?(\[\[([^\]\n\|]+)(\|([^\]\n\|]+))?\]\])/) do |m|
522 text.gsub!(/(!)?(\[\[([^\]\n\|]+)(\|([^\]\n\|]+))?\]\])/) do |m|
523 link_project = project
523 link_project = project
524 esc, all, page, title = $1, $2, $3, $5
524 esc, all, page, title = $1, $2, $3, $5
525 if esc.nil?
525 if esc.nil?
526 if page =~ /^([^\:]+)\:(.*)$/
526 if page =~ /^([^\:]+)\:(.*)$/
527 link_project = Project.find_by_name($1) || Project.find_by_identifier($1)
527 link_project = Project.find_by_name($1) || Project.find_by_identifier($1)
528 page = $2
528 page = $2
529 title ||= $1 if page.blank?
529 title ||= $1 if page.blank?
530 end
530 end
531
531
532 if link_project && link_project.wiki
532 if link_project && link_project.wiki
533 # extract anchor
533 # extract anchor
534 anchor = nil
534 anchor = nil
535 if page =~ /^(.+?)\#(.+)$/
535 if page =~ /^(.+?)\#(.+)$/
536 page, anchor = $1, $2
536 page, anchor = $1, $2
537 end
537 end
538 # check if page exists
538 # check if page exists
539 wiki_page = link_project.wiki.find_page(page)
539 wiki_page = link_project.wiki.find_page(page)
540 url = case options[:wiki_links]
540 url = case options[:wiki_links]
541 when :local; "#{title}.html"
541 when :local; "#{title}.html"
542 when :anchor; "##{title}" # used for single-file wiki export
542 when :anchor; "##{title}" # used for single-file wiki export
543 else
543 else
544 wiki_page_id = page.present? ? Wiki.titleize(page) : nil
544 wiki_page_id = page.present? ? Wiki.titleize(page) : nil
545 url_for(:only_path => only_path, :controller => 'wiki', :action => 'show', :project_id => link_project, :id => wiki_page_id, :anchor => anchor)
545 url_for(:only_path => only_path, :controller => 'wiki', :action => 'show', :project_id => link_project, :id => wiki_page_id, :anchor => anchor)
546 end
546 end
547 link_to((title || page), url, :class => ('wiki-page' + (wiki_page ? '' : ' new')))
547 link_to((title || page), url, :class => ('wiki-page' + (wiki_page ? '' : ' new')))
548 else
548 else
549 # project or wiki doesn't exist
549 # project or wiki doesn't exist
550 all
550 all
551 end
551 end
552 else
552 else
553 all
553 all
554 end
554 end
555 end
555 end
556 end
556 end
557
557
558 # Redmine links
558 # Redmine links
559 #
559 #
560 # Examples:
560 # Examples:
561 # Issues:
561 # Issues:
562 # #52 -> Link to issue #52
562 # #52 -> Link to issue #52
563 # Changesets:
563 # Changesets:
564 # r52 -> Link to revision 52
564 # r52 -> Link to revision 52
565 # commit:a85130f -> Link to scmid starting with a85130f
565 # commit:a85130f -> Link to scmid starting with a85130f
566 # Documents:
566 # Documents:
567 # document#17 -> Link to document with id 17
567 # document#17 -> Link to document with id 17
568 # document:Greetings -> Link to the document with title "Greetings"
568 # document:Greetings -> Link to the document with title "Greetings"
569 # document:"Some document" -> Link to the document with title "Some document"
569 # document:"Some document" -> Link to the document with title "Some document"
570 # Versions:
570 # Versions:
571 # version#3 -> Link to version with id 3
571 # version#3 -> Link to version with id 3
572 # version:1.0.0 -> Link to version named "1.0.0"
572 # version:1.0.0 -> Link to version named "1.0.0"
573 # version:"1.0 beta 2" -> Link to version named "1.0 beta 2"
573 # version:"1.0 beta 2" -> Link to version named "1.0 beta 2"
574 # Attachments:
574 # Attachments:
575 # attachment:file.zip -> Link to the attachment of the current object named file.zip
575 # attachment:file.zip -> Link to the attachment of the current object named file.zip
576 # Source files:
576 # Source files:
577 # source:some/file -> Link to the file located at /some/file in the project's repository
577 # source:some/file -> Link to the file located at /some/file in the project's repository
578 # source:some/file@52 -> Link to the file's revision 52
578 # source:some/file@52 -> Link to the file's revision 52
579 # source:some/file#L120 -> Link to line 120 of the file
579 # source:some/file#L120 -> Link to line 120 of the file
580 # source:some/file@52#L120 -> Link to line 120 of the file's revision 52
580 # source:some/file@52#L120 -> Link to line 120 of the file's revision 52
581 # export:some/file -> Force the download of the file
581 # export:some/file -> Force the download of the file
582 # Forum messages:
582 # Forum messages:
583 # message#1218 -> Link to message with id 1218
583 # message#1218 -> Link to message with id 1218
584 def parse_redmine_links(text, project, obj, attr, only_path, options)
584 def parse_redmine_links(text, project, obj, attr, only_path, options)
585 text.gsub!(%r{([\s\(,\-\[\>]|^)(!)?(attachment|document|version|commit|source|export|message|project)?((#|r)(\d+)|(:)([^"\s<>][^\s<>]*?|"[^"]+?"))(?=(?=[[:punct:]]\W)|,|\s|\]|<|$)}) do |m|
585 text.gsub!(%r{([\s\(,\-\[\>]|^)(!)?(attachment|document|version|commit|source|export|message|project)?((#|r)(\d+)|(:)([^"\s<>][^\s<>]*?|"[^"]+?"))(?=(?=[[:punct:]]\W)|,|\s|\]|<|$)}) do |m|
586 leading, esc, prefix, sep, identifier = $1, $2, $3, $5 || $7, $6 || $8
586 leading, esc, prefix, sep, identifier = $1, $2, $3, $5 || $7, $6 || $8
587 link = nil
587 link = nil
588 if esc.nil?
588 if esc.nil?
589 if prefix.nil? && sep == 'r'
589 if prefix.nil? && sep == 'r'
590 if project && (changeset = project.changesets.find_by_revision(identifier))
590 if project && (changeset = project.changesets.find_by_revision(identifier))
591 link = link_to("r#{identifier}", {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :rev => changeset.revision},
591 link = link_to("r#{identifier}", {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :rev => changeset.revision},
592 :class => 'changeset',
592 :class => 'changeset',
593 :title => truncate_single_line(changeset.comments, :length => 100))
593 :title => truncate_single_line(changeset.comments, :length => 100))
594 end
594 end
595 elsif sep == '#'
595 elsif sep == '#'
596 oid = identifier.to_i
596 oid = identifier.to_i
597 case prefix
597 case prefix
598 when nil
598 when nil
599 if issue = Issue.visible.find_by_id(oid, :include => :status)
599 if issue = Issue.visible.find_by_id(oid, :include => :status)
600 link = link_to("##{oid}", {:only_path => only_path, :controller => 'issues', :action => 'show', :id => oid},
600 link = link_to("##{oid}", {:only_path => only_path, :controller => 'issues', :action => 'show', :id => oid},
601 :class => issue.css_classes,
601 :class => issue.css_classes,
602 :title => "#{truncate(issue.subject, :length => 100)} (#{issue.status.name})")
602 :title => "#{truncate(issue.subject, :length => 100)} (#{issue.status.name})")
603 end
603 end
604 when 'document'
604 when 'document'
605 if document = Document.find_by_id(oid, :include => [:project], :conditions => Project.visible_by(User.current))
605 if document = Document.find_by_id(oid, :include => [:project], :conditions => Project.visible_by(User.current))
606 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
606 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
607 :class => 'document'
607 :class => 'document'
608 end
608 end
609 when 'version'
609 when 'version'
610 if version = Version.find_by_id(oid, :include => [:project], :conditions => Project.visible_by(User.current))
610 if version = Version.find_by_id(oid, :include => [:project], :conditions => Project.visible_by(User.current))
611 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
611 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
612 :class => 'version'
612 :class => 'version'
613 end
613 end
614 when 'message'
614 when 'message'
615 if message = Message.find_by_id(oid, :include => [:parent, {:board => :project}], :conditions => Project.visible_by(User.current))
615 if message = Message.find_by_id(oid, :include => [:parent, {:board => :project}], :conditions => Project.visible_by(User.current))
616 link = link_to h(truncate(message.subject, :length => 60)), {:only_path => only_path,
616 link = link_to h(truncate(message.subject, :length => 60)), {:only_path => only_path,
617 :controller => 'messages',
617 :controller => 'messages',
618 :action => 'show',
618 :action => 'show',
619 :board_id => message.board,
619 :board_id => message.board,
620 :id => message.root,
620 :id => message.root,
621 :anchor => (message.parent ? "message-#{message.id}" : nil)},
621 :anchor => (message.parent ? "message-#{message.id}" : nil)},
622 :class => 'message'
622 :class => 'message'
623 end
623 end
624 when 'project'
624 when 'project'
625 if p = Project.visible.find_by_id(oid)
625 if p = Project.visible.find_by_id(oid)
626 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
626 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
627 end
627 end
628 end
628 end
629 elsif sep == ':'
629 elsif sep == ':'
630 # removes the double quotes if any
630 # removes the double quotes if any
631 name = identifier.gsub(%r{^"(.*)"$}, "\\1")
631 name = identifier.gsub(%r{^"(.*)"$}, "\\1")
632 case prefix
632 case prefix
633 when 'document'
633 when 'document'
634 if project && document = project.documents.find_by_title(name)
634 if project && document = project.documents.find_by_title(name)
635 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
635 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
636 :class => 'document'
636 :class => 'document'
637 end
637 end
638 when 'version'
638 when 'version'
639 if project && version = project.versions.find_by_name(name)
639 if project && version = project.versions.find_by_name(name)
640 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
640 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
641 :class => 'version'
641 :class => 'version'
642 end
642 end
643 when 'commit'
643 when 'commit'
644 if project && (changeset = project.changesets.find(:first, :conditions => ["scmid LIKE ?", "#{name}%"]))
644 if project && (changeset = project.changesets.find(:first, :conditions => ["scmid LIKE ?", "#{name}%"]))
645 link = link_to h("#{name}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :rev => changeset.revision},
645 link = link_to h("#{name}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :rev => changeset.revision},
646 :class => 'changeset',
646 :class => 'changeset',
647 :title => truncate_single_line(changeset.comments, :length => 100)
647 :title => truncate_single_line(changeset.comments, :length => 100)
648 end
648 end
649 when 'source', 'export'
649 when 'source', 'export'
650 if project && project.repository
650 if project && project.repository
651 name =~ %r{^[/\\]*(.*?)(@([0-9a-f]+))?(#(L\d+))?$}
651 name =~ %r{^[/\\]*(.*?)(@([0-9a-f]+))?(#(L\d+))?$}
652 path, rev, anchor = $1, $3, $5
652 path, rev, anchor = $1, $3, $5
653 link = link_to h("#{prefix}:#{name}"), {:controller => 'repositories', :action => 'entry', :id => project,
653 link = link_to h("#{prefix}:#{name}"), {:controller => 'repositories', :action => 'entry', :id => project,
654 :path => to_path_param(path),
654 :path => to_path_param(path),
655 :rev => rev,
655 :rev => rev,
656 :anchor => anchor,
656 :anchor => anchor,
657 :format => (prefix == 'export' ? 'raw' : nil)},
657 :format => (prefix == 'export' ? 'raw' : nil)},
658 :class => (prefix == 'export' ? 'source download' : 'source')
658 :class => (prefix == 'export' ? 'source download' : 'source')
659 end
659 end
660 when 'attachment'
660 when 'attachment'
661 attachments = options[:attachments] || (obj && obj.respond_to?(:attachments) ? obj.attachments : nil)
661 attachments = options[:attachments] || (obj && obj.respond_to?(:attachments) ? obj.attachments : nil)
662 if attachments && attachment = attachments.detect {|a| a.filename == name }
662 if attachments && attachment = attachments.detect {|a| a.filename == name }
663 link = link_to h(attachment.filename), {:only_path => only_path, :controller => 'attachments', :action => 'download', :id => attachment},
663 link = link_to h(attachment.filename), {:only_path => only_path, :controller => 'attachments', :action => 'download', :id => attachment},
664 :class => 'attachment'
664 :class => 'attachment'
665 end
665 end
666 when 'project'
666 when 'project'
667 if p = Project.visible.find(:first, :conditions => ["identifier = :s OR LOWER(name) = :s", {:s => name.downcase}])
667 if p = Project.visible.find(:first, :conditions => ["identifier = :s OR LOWER(name) = :s", {:s => name.downcase}])
668 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
668 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
669 end
669 end
670 end
670 end
671 end
671 end
672 end
672 end
673 leading + (link || "#{prefix}#{sep}#{identifier}")
673 leading + (link || "#{prefix}#{sep}#{identifier}")
674 end
674 end
675 end
675 end
676
676
677 TOC_RE = /<p>\{\{([<>]?)toc\}\}<\/p>/i unless const_defined?(:TOC_RE)
678 HEADING_RE = /<h(1|2|3)( [^>]+)?>(.+?)<\/h(1|2|3)>/i unless const_defined?(:HEADING_RE)
679
680 # Headings and TOC
681 # Adds ids and links to headings and renders the TOC if needed unless options[:headings] is set to false
682 def parse_headings(text, project, obj, attr, only_path, options)
683 headings = []
684 text.gsub!(HEADING_RE) do
685 level, attrs, content = $1, $2, $3
686 item = strip_tags(content).strip
687 anchor = item.gsub(%r{[^\w\s\-]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
688 headings << [level, anchor, item]
689 "<h#{level} #{attrs} id=\"#{anchor}\">#{content}<a href=\"##{anchor}\" class=\"wiki-anchor\">&para;</a></h#{level}>"
690 end unless options[:headings] == false
691
692 text.gsub!(TOC_RE) do
693 if headings.empty?
694 ''
695 else
696 div_class = 'toc'
697 div_class << ' right' if $1 == '>'
698 div_class << ' left' if $1 == '<'
699 out = "<ul class=\"#{div_class}\">"
700 headings.each do |level, anchor, item|
701 out << "<li class=\"heading#{level}\"><a href=\"##{anchor}\">#{item}</a></li>\n"
702 end
703 out << '</ul>'
704 out
705 end
706 end
707 end
708
677 # Same as Rails' simple_format helper without using paragraphs
709 # Same as Rails' simple_format helper without using paragraphs
678 def simple_format_without_paragraph(text)
710 def simple_format_without_paragraph(text)
679 text.to_s.
711 text.to_s.
680 gsub(/\r\n?/, "\n"). # \r\n and \r -> \n
712 gsub(/\r\n?/, "\n"). # \r\n and \r -> \n
681 gsub(/\n\n+/, "<br /><br />"). # 2+ newline -> 2 br
713 gsub(/\n\n+/, "<br /><br />"). # 2+ newline -> 2 br
682 gsub(/([^\n]\n)(?=[^\n])/, '\1<br />') # 1 newline -> br
714 gsub(/([^\n]\n)(?=[^\n])/, '\1<br />') # 1 newline -> br
683 end
715 end
684
716
685 def lang_options_for_select(blank=true)
717 def lang_options_for_select(blank=true)
686 (blank ? [["(auto)", ""]] : []) +
718 (blank ? [["(auto)", ""]] : []) +
687 valid_languages.collect{|lang| [ ll(lang.to_s, :general_lang_name), lang.to_s]}.sort{|x,y| x.last <=> y.last }
719 valid_languages.collect{|lang| [ ll(lang.to_s, :general_lang_name), lang.to_s]}.sort{|x,y| x.last <=> y.last }
688 end
720 end
689
721
690 def label_tag_for(name, option_tags = nil, options = {})
722 def label_tag_for(name, option_tags = nil, options = {})
691 label_text = l(("field_"+field.to_s.gsub(/\_id$/, "")).to_sym) + (options.delete(:required) ? @template.content_tag("span", " *", :class => "required"): "")
723 label_text = l(("field_"+field.to_s.gsub(/\_id$/, "")).to_sym) + (options.delete(:required) ? @template.content_tag("span", " *", :class => "required"): "")
692 content_tag("label", label_text)
724 content_tag("label", label_text)
693 end
725 end
694
726
695 def labelled_tabular_form_for(name, object, options, &proc)
727 def labelled_tabular_form_for(name, object, options, &proc)
696 options[:html] ||= {}
728 options[:html] ||= {}
697 options[:html][:class] = 'tabular' unless options[:html].has_key?(:class)
729 options[:html][:class] = 'tabular' unless options[:html].has_key?(:class)
698 form_for(name, object, options.merge({ :builder => TabularFormBuilder, :lang => current_language}), &proc)
730 form_for(name, object, options.merge({ :builder => TabularFormBuilder, :lang => current_language}), &proc)
699 end
731 end
700
732
701 def back_url_hidden_field_tag
733 def back_url_hidden_field_tag
702 back_url = params[:back_url] || request.env['HTTP_REFERER']
734 back_url = params[:back_url] || request.env['HTTP_REFERER']
703 back_url = CGI.unescape(back_url.to_s)
735 back_url = CGI.unescape(back_url.to_s)
704 hidden_field_tag('back_url', CGI.escape(back_url)) unless back_url.blank?
736 hidden_field_tag('back_url', CGI.escape(back_url)) unless back_url.blank?
705 end
737 end
706
738
707 def check_all_links(form_name)
739 def check_all_links(form_name)
708 link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") +
740 link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") +
709 " | " +
741 " | " +
710 link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)")
742 link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)")
711 end
743 end
712
744
713 def progress_bar(pcts, options={})
745 def progress_bar(pcts, options={})
714 pcts = [pcts, pcts] unless pcts.is_a?(Array)
746 pcts = [pcts, pcts] unless pcts.is_a?(Array)
715 pcts = pcts.collect(&:round)
747 pcts = pcts.collect(&:round)
716 pcts[1] = pcts[1] - pcts[0]
748 pcts[1] = pcts[1] - pcts[0]
717 pcts << (100 - pcts[1] - pcts[0])
749 pcts << (100 - pcts[1] - pcts[0])
718 width = options[:width] || '100px;'
750 width = options[:width] || '100px;'
719 legend = options[:legend] || ''
751 legend = options[:legend] || ''
720 content_tag('table',
752 content_tag('table',
721 content_tag('tr',
753 content_tag('tr',
722 (pcts[0] > 0 ? content_tag('td', '', :style => "width: #{pcts[0]}%;", :class => 'closed') : '') +
754 (pcts[0] > 0 ? content_tag('td', '', :style => "width: #{pcts[0]}%;", :class => 'closed') : '') +
723 (pcts[1] > 0 ? content_tag('td', '', :style => "width: #{pcts[1]}%;", :class => 'done') : '') +
755 (pcts[1] > 0 ? content_tag('td', '', :style => "width: #{pcts[1]}%;", :class => 'done') : '') +
724 (pcts[2] > 0 ? content_tag('td', '', :style => "width: #{pcts[2]}%;", :class => 'todo') : '')
756 (pcts[2] > 0 ? content_tag('td', '', :style => "width: #{pcts[2]}%;", :class => 'todo') : '')
725 ), :class => 'progress', :style => "width: #{width};") +
757 ), :class => 'progress', :style => "width: #{width};") +
726 content_tag('p', legend, :class => 'pourcent')
758 content_tag('p', legend, :class => 'pourcent')
727 end
759 end
728
760
729 def checked_image(checked=true)
761 def checked_image(checked=true)
730 if checked
762 if checked
731 image_tag 'toggle_check.png'
763 image_tag 'toggle_check.png'
732 end
764 end
733 end
765 end
734
766
735 def context_menu(url)
767 def context_menu(url)
736 unless @context_menu_included
768 unless @context_menu_included
737 content_for :header_tags do
769 content_for :header_tags do
738 javascript_include_tag('context_menu') +
770 javascript_include_tag('context_menu') +
739 stylesheet_link_tag('context_menu')
771 stylesheet_link_tag('context_menu')
740 end
772 end
741 if l(:direction) == 'rtl'
773 if l(:direction) == 'rtl'
742 content_for :header_tags do
774 content_for :header_tags do
743 stylesheet_link_tag('context_menu_rtl')
775 stylesheet_link_tag('context_menu_rtl')
744 end
776 end
745 end
777 end
746 @context_menu_included = true
778 @context_menu_included = true
747 end
779 end
748 javascript_tag "new ContextMenu('#{ url_for(url) }')"
780 javascript_tag "new ContextMenu('#{ url_for(url) }')"
749 end
781 end
750
782
751 def context_menu_link(name, url, options={})
783 def context_menu_link(name, url, options={})
752 options[:class] ||= ''
784 options[:class] ||= ''
753 if options.delete(:selected)
785 if options.delete(:selected)
754 options[:class] << ' icon-checked disabled'
786 options[:class] << ' icon-checked disabled'
755 options[:disabled] = true
787 options[:disabled] = true
756 end
788 end
757 if options.delete(:disabled)
789 if options.delete(:disabled)
758 options.delete(:method)
790 options.delete(:method)
759 options.delete(:confirm)
791 options.delete(:confirm)
760 options.delete(:onclick)
792 options.delete(:onclick)
761 options[:class] << ' disabled'
793 options[:class] << ' disabled'
762 url = '#'
794 url = '#'
763 end
795 end
764 link_to name, url, options
796 link_to name, url, options
765 end
797 end
766
798
767 def calendar_for(field_id)
799 def calendar_for(field_id)
768 include_calendar_headers_tags
800 include_calendar_headers_tags
769 image_tag("calendar.png", {:id => "#{field_id}_trigger",:class => "calendar-trigger"}) +
801 image_tag("calendar.png", {:id => "#{field_id}_trigger",:class => "calendar-trigger"}) +
770 javascript_tag("Calendar.setup({inputField : '#{field_id}', ifFormat : '%Y-%m-%d', button : '#{field_id}_trigger' });")
802 javascript_tag("Calendar.setup({inputField : '#{field_id}', ifFormat : '%Y-%m-%d', button : '#{field_id}_trigger' });")
771 end
803 end
772
804
773 def include_calendar_headers_tags
805 def include_calendar_headers_tags
774 unless @calendar_headers_tags_included
806 unless @calendar_headers_tags_included
775 @calendar_headers_tags_included = true
807 @calendar_headers_tags_included = true
776 content_for :header_tags do
808 content_for :header_tags do
777 start_of_week = case Setting.start_of_week.to_i
809 start_of_week = case Setting.start_of_week.to_i
778 when 1
810 when 1
779 'Calendar._FD = 1;' # Monday
811 'Calendar._FD = 1;' # Monday
780 when 7
812 when 7
781 'Calendar._FD = 0;' # Sunday
813 'Calendar._FD = 0;' # Sunday
782 else
814 else
783 '' # use language
815 '' # use language
784 end
816 end
785
817
786 javascript_include_tag('calendar/calendar') +
818 javascript_include_tag('calendar/calendar') +
787 javascript_include_tag("calendar/lang/calendar-#{current_language.to_s.downcase}.js") +
819 javascript_include_tag("calendar/lang/calendar-#{current_language.to_s.downcase}.js") +
788 javascript_tag(start_of_week) +
820 javascript_tag(start_of_week) +
789 javascript_include_tag('calendar/calendar-setup') +
821 javascript_include_tag('calendar/calendar-setup') +
790 stylesheet_link_tag('calendar')
822 stylesheet_link_tag('calendar')
791 end
823 end
792 end
824 end
793 end
825 end
794
826
795 def content_for(name, content = nil, &block)
827 def content_for(name, content = nil, &block)
796 @has_content ||= {}
828 @has_content ||= {}
797 @has_content[name] = true
829 @has_content[name] = true
798 super(name, content, &block)
830 super(name, content, &block)
799 end
831 end
800
832
801 def has_content?(name)
833 def has_content?(name)
802 (@has_content && @has_content[name]) || false
834 (@has_content && @has_content[name]) || false
803 end
835 end
804
836
805 # Returns the avatar image tag for the given +user+ if avatars are enabled
837 # Returns the avatar image tag for the given +user+ if avatars are enabled
806 # +user+ can be a User or a string that will be scanned for an email address (eg. 'joe <joe@foo.bar>')
838 # +user+ can be a User or a string that will be scanned for an email address (eg. 'joe <joe@foo.bar>')
807 def avatar(user, options = { })
839 def avatar(user, options = { })
808 if Setting.gravatar_enabled?
840 if Setting.gravatar_enabled?
809 options.merge!({:ssl => (defined?(request) && request.ssl?), :default => Setting.gravatar_default})
841 options.merge!({:ssl => (defined?(request) && request.ssl?), :default => Setting.gravatar_default})
810 email = nil
842 email = nil
811 if user.respond_to?(:mail)
843 if user.respond_to?(:mail)
812 email = user.mail
844 email = user.mail
813 elsif user.to_s =~ %r{<(.+?)>}
845 elsif user.to_s =~ %r{<(.+?)>}
814 email = $1
846 email = $1
815 end
847 end
816 return gravatar(email.to_s.downcase, options) unless email.blank? rescue nil
848 return gravatar(email.to_s.downcase, options) unless email.blank? rescue nil
817 else
849 else
818 ''
850 ''
819 end
851 end
820 end
852 end
821
853
822 def favicon
854 def favicon
823 "<link rel='shortcut icon' href='#{image_path('/favicon.ico')}' />"
855 "<link rel='shortcut icon' href='#{image_path('/favicon.ico')}' />"
824 end
856 end
825
857
826 private
858 private
827
859
828 def wiki_helper
860 def wiki_helper
829 helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
861 helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
830 extend helper
862 extend helper
831 return self
863 return self
832 end
864 end
833
865
834 def link_to_remote_content_update(text, url_params)
866 def link_to_remote_content_update(text, url_params)
835 link_to_remote(text,
867 link_to_remote(text,
836 {:url => url_params, :method => :get, :update => 'content', :complete => 'window.scrollTo(0,0)'},
868 {:url => url_params, :method => :get, :update => 'content', :complete => 'window.scrollTo(0,0)'},
837 {:href => url_for(:params => url_params)}
869 {:href => url_for(:params => url_params)}
838 )
870 )
839 end
871 end
840
872
841 end
873 end
@@ -1,121 +1,121
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
2 # Copyright (C) 2006-2007 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 module Redmine
18 module Redmine
19 module WikiFormatting
19 module WikiFormatting
20 module Macros
20 module Macros
21 module Definitions
21 module Definitions
22 def exec_macro(name, obj, args)
22 def exec_macro(name, obj, args)
23 method_name = "macro_#{name}"
23 method_name = "macro_#{name}"
24 send(method_name, obj, args) if respond_to?(method_name)
24 send(method_name, obj, args) if respond_to?(method_name)
25 end
25 end
26
26
27 def extract_macro_options(args, *keys)
27 def extract_macro_options(args, *keys)
28 options = {}
28 options = {}
29 while args.last.to_s.strip =~ %r{^(.+)\=(.+)$} && keys.include?($1.downcase.to_sym)
29 while args.last.to_s.strip =~ %r{^(.+)\=(.+)$} && keys.include?($1.downcase.to_sym)
30 options[$1.downcase.to_sym] = $2
30 options[$1.downcase.to_sym] = $2
31 args.pop
31 args.pop
32 end
32 end
33 return [args, options]
33 return [args, options]
34 end
34 end
35 end
35 end
36
36
37 @@available_macros = {}
37 @@available_macros = {}
38
38
39 class << self
39 class << self
40 # Called with a block to define additional macros.
40 # Called with a block to define additional macros.
41 # Macro blocks accept 2 arguments:
41 # Macro blocks accept 2 arguments:
42 # * obj: the object that is rendered
42 # * obj: the object that is rendered
43 # * args: macro arguments
43 # * args: macro arguments
44 #
44 #
45 # Plugins can use this method to define new macros:
45 # Plugins can use this method to define new macros:
46 #
46 #
47 # Redmine::WikiFormatting::Macros.register do
47 # Redmine::WikiFormatting::Macros.register do
48 # desc "This is my macro"
48 # desc "This is my macro"
49 # macro :my_macro do |obj, args|
49 # macro :my_macro do |obj, args|
50 # "My macro output"
50 # "My macro output"
51 # end
51 # end
52 # end
52 # end
53 def register(&block)
53 def register(&block)
54 class_eval(&block) if block_given?
54 class_eval(&block) if block_given?
55 end
55 end
56
56
57 private
57 private
58 # Defines a new macro with the given name and block.
58 # Defines a new macro with the given name and block.
59 def macro(name, &block)
59 def macro(name, &block)
60 name = name.to_sym if name.is_a?(String)
60 name = name.to_sym if name.is_a?(String)
61 @@available_macros[name] = @@desc || ''
61 @@available_macros[name] = @@desc || ''
62 @@desc = nil
62 @@desc = nil
63 raise "Can not create a macro without a block!" unless block_given?
63 raise "Can not create a macro without a block!" unless block_given?
64 Definitions.send :define_method, "macro_#{name}".downcase, &block
64 Definitions.send :define_method, "macro_#{name}".downcase, &block
65 end
65 end
66
66
67 # Sets description for the next macro to be defined
67 # Sets description for the next macro to be defined
68 def desc(txt)
68 def desc(txt)
69 @@desc = txt
69 @@desc = txt
70 end
70 end
71 end
71 end
72
72
73 # Builtin macros
73 # Builtin macros
74 desc "Sample macro."
74 desc "Sample macro."
75 macro :hello_world do |obj, args|
75 macro :hello_world do |obj, args|
76 "Hello world! Object: #{obj.class.name}, " + (args.empty? ? "Called with no argument." : "Arguments: #{args.join(', ')}")
76 "Hello world! Object: #{obj.class.name}, " + (args.empty? ? "Called with no argument." : "Arguments: #{args.join(', ')}")
77 end
77 end
78
78
79 desc "Displays a list of all available macros, including description if available."
79 desc "Displays a list of all available macros, including description if available."
80 macro :macro_list do
80 macro :macro_list do
81 out = ''
81 out = ''
82 @@available_macros.keys.collect(&:to_s).sort.each do |macro|
82 @@available_macros.keys.collect(&:to_s).sort.each do |macro|
83 out << content_tag('dt', content_tag('code', macro))
83 out << content_tag('dt', content_tag('code', macro))
84 out << content_tag('dd', textilizable(@@available_macros[macro.to_sym]))
84 out << content_tag('dd', textilizable(@@available_macros[macro.to_sym]))
85 end
85 end
86 content_tag('dl', out)
86 content_tag('dl', out)
87 end
87 end
88
88
89 desc "Displays a list of child pages. With no argument, it displays the child pages of the current wiki page. Examples:\n\n" +
89 desc "Displays a list of child pages. With no argument, it displays the child pages of the current wiki page. Examples:\n\n" +
90 " !{{child_pages}} -- can be used from a wiki page only\n" +
90 " !{{child_pages}} -- can be used from a wiki page only\n" +
91 " !{{child_pages(Foo)}} -- lists all children of page Foo\n" +
91 " !{{child_pages(Foo)}} -- lists all children of page Foo\n" +
92 " !{{child_pages(Foo, parent=1)}} -- same as above with a link to page Foo"
92 " !{{child_pages(Foo, parent=1)}} -- same as above with a link to page Foo"
93 macro :child_pages do |obj, args|
93 macro :child_pages do |obj, args|
94 args, options = extract_macro_options(args, :parent)
94 args, options = extract_macro_options(args, :parent)
95 page = nil
95 page = nil
96 if args.size > 0
96 if args.size > 0
97 page = Wiki.find_page(args.first.to_s, :project => @project)
97 page = Wiki.find_page(args.first.to_s, :project => @project)
98 elsif obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version)
98 elsif obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version)
99 page = obj.page
99 page = obj.page
100 else
100 else
101 raise 'With no argument, this macro can be called from wiki pages only.'
101 raise 'With no argument, this macro can be called from wiki pages only.'
102 end
102 end
103 raise 'Page not found' if page.nil? || !User.current.allowed_to?(:view_wiki_pages, page.wiki.project)
103 raise 'Page not found' if page.nil? || !User.current.allowed_to?(:view_wiki_pages, page.wiki.project)
104 pages = ([page] + page.descendants).group_by(&:parent_id)
104 pages = ([page] + page.descendants).group_by(&:parent_id)
105 render_page_hierarchy(pages, options[:parent] ? page.parent_id : page.id)
105 render_page_hierarchy(pages, options[:parent] ? page.parent_id : page.id)
106 end
106 end
107
107
108 desc "Include a wiki page. Example:\n\n !{{include(Foo)}}\n\nor to include a page of a specific project wiki:\n\n !{{include(projectname:Foo)}}"
108 desc "Include a wiki page. Example:\n\n !{{include(Foo)}}\n\nor to include a page of a specific project wiki:\n\n !{{include(projectname:Foo)}}"
109 macro :include do |obj, args|
109 macro :include do |obj, args|
110 page = Wiki.find_page(args.first.to_s, :project => @project)
110 page = Wiki.find_page(args.first.to_s, :project => @project)
111 raise 'Page not found' if page.nil? || !User.current.allowed_to?(:view_wiki_pages, page.wiki.project)
111 raise 'Page not found' if page.nil? || !User.current.allowed_to?(:view_wiki_pages, page.wiki.project)
112 @included_wiki_pages ||= []
112 @included_wiki_pages ||= []
113 raise 'Circular inclusion detected' if @included_wiki_pages.include?(page.title)
113 raise 'Circular inclusion detected' if @included_wiki_pages.include?(page.title)
114 @included_wiki_pages << page.title
114 @included_wiki_pages << page.title
115 out = textilizable(page.content, :text, :attachments => page.attachments)
115 out = textilizable(page.content, :text, :attachments => page.attachments, :headings => false)
116 @included_wiki_pages.pop
116 @included_wiki_pages.pop
117 out
117 out
118 end
118 end
119 end
119 end
120 end
120 end
121 end
121 end
@@ -1,163 +1,118
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2008 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 'redcloth3'
18 require 'redcloth3'
19
19
20 module Redmine
20 module Redmine
21 module WikiFormatting
21 module WikiFormatting
22 module Textile
22 module Textile
23 class Formatter < RedCloth3
23 class Formatter < RedCloth3
24 include ActionView::Helpers::TagHelper
24 include ActionView::Helpers::TagHelper
25
25
26 # auto_link rule after textile rules so that it doesn't break !image_url! tags
26 # auto_link rule after textile rules so that it doesn't break !image_url! tags
27 RULES = [:textile, :block_markdown_rule, :inline_auto_link, :inline_auto_mailto, :inline_toc]
27 RULES = [:textile, :block_markdown_rule, :inline_auto_link, :inline_auto_mailto]
28
28
29 def initialize(*args)
29 def initialize(*args)
30 super
30 super
31 self.hard_breaks=true
31 self.hard_breaks=true
32 self.no_span_caps=true
32 self.no_span_caps=true
33 self.filter_styles=true
33 self.filter_styles=true
34 end
34 end
35
35
36 def to_html(*rules)
36 def to_html(*rules)
37 @toc = []
37 @toc = []
38 super(*RULES).to_s
38 super(*RULES).to_s
39 end
39 end
40
40
41 private
41 private
42
42
43 # Patch for RedCloth. Fixed in RedCloth r128 but _why hasn't released it yet.
43 # Patch for RedCloth. Fixed in RedCloth r128 but _why hasn't released it yet.
44 # <a href="http://code.whytheluckystiff.net/redcloth/changeset/128">http://code.whytheluckystiff.net/redcloth/changeset/128</a>
44 # <a href="http://code.whytheluckystiff.net/redcloth/changeset/128">http://code.whytheluckystiff.net/redcloth/changeset/128</a>
45 def hard_break( text )
45 def hard_break( text )
46 text.gsub!( /(.)\n(?!\n|\Z|>| *([#*=]+(\s|$)|[{|]))/, "\\1<br />" ) if hard_breaks
46 text.gsub!( /(.)\n(?!\n|\Z|>| *([#*=]+(\s|$)|[{|]))/, "\\1<br />" ) if hard_breaks
47 end
47 end
48
48
49 # Patch to add code highlighting support to RedCloth
49 # Patch to add code highlighting support to RedCloth
50 def smooth_offtags( text )
50 def smooth_offtags( text )
51 unless @pre_list.empty?
51 unless @pre_list.empty?
52 ## replace <pre> content
52 ## replace <pre> content
53 text.gsub!(/<redpre#(\d+)>/) do
53 text.gsub!(/<redpre#(\d+)>/) do
54 content = @pre_list[$1.to_i]
54 content = @pre_list[$1.to_i]
55 if content.match(/<code\s+class="(\w+)">\s?(.+)/m)
55 if content.match(/<code\s+class="(\w+)">\s?(.+)/m)
56 content = "<code class=\"#{$1} syntaxhl\">" +
56 content = "<code class=\"#{$1} syntaxhl\">" +
57 Redmine::SyntaxHighlighting.highlight_by_language($2, $1)
57 Redmine::SyntaxHighlighting.highlight_by_language($2, $1)
58 end
58 end
59 content
59 content
60 end
60 end
61 end
61 end
62 end
62 end
63
63
64 # Patch to add 'table of content' support to RedCloth
65 def textile_p_withtoc(tag, atts, cite, content)
66 # removes wiki links from the item
67 toc_item = content.gsub(/(\[\[([^\]\|]*)(\|([^\]]*))?\]\])/) { $4 || $2 }
68 # sanitizes titles from links
69 # see redcloth3.rb, same as "#{pre}#{text}#{post}"
70 toc_item.gsub!(LINK_RE) { [$2, $4, $9].join }
71 # sanitizes image links from titles
72 toc_item.gsub!(IMAGE_RE) { [$5].join }
73 # removes styles
74 # eg. %{color:red}Triggers% => Triggers
75 toc_item.gsub! %r[%\{[^\}]*\}([^%]+)%], '\\1'
76
77 # replaces non word caracters by dashes
78 anchor = toc_item.gsub(%r{[^\w\s\-]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
79
80 unless anchor.blank?
81 if tag =~ /^h(\d)$/
82 @toc << [$1.to_i, anchor, toc_item]
83 end
84 atts << " id=\"#{anchor}\""
85 content = content + "<a href=\"##{anchor}\" class=\"wiki-anchor\">&para;</a>"
86 end
87 textile_p(tag, atts, cite, content)
88 end
89
90 alias :textile_h1 :textile_p_withtoc
91 alias :textile_h2 :textile_p_withtoc
92 alias :textile_h3 :textile_p_withtoc
93
94 def inline_toc(text)
95 text.gsub!(/<p>\{\{([<>]?)toc\}\}<\/p>/i) do
96 div_class = 'toc'
97 div_class << ' right' if $1 == '>'
98 div_class << ' left' if $1 == '<'
99 out = "<ul class=\"#{div_class}\">"
100 @toc.each do |heading|
101 level, anchor, toc_item = heading
102 out << "<li class=\"heading#{level}\"><a href=\"##{anchor}\">#{toc_item}</a></li>\n"
103 end
104 out << '</ul>'
105 out
106 end
107 end
108
109 AUTO_LINK_RE = %r{
64 AUTO_LINK_RE = %r{
110 ( # leading text
65 ( # leading text
111 <\w+.*?>| # leading HTML tag, or
66 <\w+.*?>| # leading HTML tag, or
112 [^=<>!:'"/]| # leading punctuation, or
67 [^=<>!:'"/]| # leading punctuation, or
113 ^ # beginning of line
68 ^ # beginning of line
114 )
69 )
115 (
70 (
116 (?:https?://)| # protocol spec, or
71 (?:https?://)| # protocol spec, or
117 (?:s?ftps?://)|
72 (?:s?ftps?://)|
118 (?:www\.) # www.*
73 (?:www\.) # www.*
119 )
74 )
120 (
75 (
121 (\S+?) # url
76 (\S+?) # url
122 (\/)? # slash
77 (\/)? # slash
123 )
78 )
124 ((?:&gt;)?|[^\w\=\/;\(\)]*?) # post
79 ((?:&gt;)?|[^\w\=\/;\(\)]*?) # post
125 (?=<|\s|$)
80 (?=<|\s|$)
126 }x unless const_defined?(:AUTO_LINK_RE)
81 }x unless const_defined?(:AUTO_LINK_RE)
127
82
128 # Turns all urls into clickable links (code from Rails).
83 # Turns all urls into clickable links (code from Rails).
129 def inline_auto_link(text)
84 def inline_auto_link(text)
130 text.gsub!(AUTO_LINK_RE) do
85 text.gsub!(AUTO_LINK_RE) do
131 all, leading, proto, url, post = $&, $1, $2, $3, $6
86 all, leading, proto, url, post = $&, $1, $2, $3, $6
132 if leading =~ /<a\s/i || leading =~ /![<>=]?/
87 if leading =~ /<a\s/i || leading =~ /![<>=]?/
133 # don't replace URL's that are already linked
88 # don't replace URL's that are already linked
134 # and URL's prefixed with ! !> !< != (textile images)
89 # and URL's prefixed with ! !> !< != (textile images)
135 all
90 all
136 else
91 else
137 # Idea below : an URL with unbalanced parethesis and
92 # Idea below : an URL with unbalanced parethesis and
138 # ending by ')' is put into external parenthesis
93 # ending by ')' is put into external parenthesis
139 if ( url[-1]==?) and ((url.count("(") - url.count(")")) < 0 ) )
94 if ( url[-1]==?) and ((url.count("(") - url.count(")")) < 0 ) )
140 url=url[0..-2] # discard closing parenth from url
95 url=url[0..-2] # discard closing parenth from url
141 post = ")"+post # add closing parenth to post
96 post = ")"+post # add closing parenth to post
142 end
97 end
143 tag = content_tag('a', proto + url, :href => "#{proto=="www."?"http://www.":proto}#{url}", :class => 'external')
98 tag = content_tag('a', proto + url, :href => "#{proto=="www."?"http://www.":proto}#{url}", :class => 'external')
144 %(#{leading}#{tag}#{post})
99 %(#{leading}#{tag}#{post})
145 end
100 end
146 end
101 end
147 end
102 end
148
103
149 # Turns all email addresses into clickable links (code from Rails).
104 # Turns all email addresses into clickable links (code from Rails).
150 def inline_auto_mailto(text)
105 def inline_auto_mailto(text)
151 text.gsub!(/([\w\.!#\$%\-+.]+@[A-Za-z0-9\-]+(\.[A-Za-z0-9\-]+)+)/) do
106 text.gsub!(/([\w\.!#\$%\-+.]+@[A-Za-z0-9\-]+(\.[A-Za-z0-9\-]+)+)/) do
152 mail = $1
107 mail = $1
153 if text.match(/<a\b[^>]*>(.*)(#{Regexp.escape(mail)})(.*)<\/a>/)
108 if text.match(/<a\b[^>]*>(.*)(#{Regexp.escape(mail)})(.*)<\/a>/)
154 mail
109 mail
155 else
110 else
156 content_tag('a', mail, :href => "mailto:#{mail}", :class => "email")
111 content_tag('a', mail, :href => "mailto:#{mail}", :class => "email")
157 end
112 end
158 end
113 end
159 end
114 end
160 end
115 end
161 end
116 end
162 end
117 end
163 end
118 end
@@ -1,617 +1,639
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2009 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.dirname(__FILE__) + '/../../test_helper'
18 require File.dirname(__FILE__) + '/../../test_helper'
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_attachment_links
228 def test_attachment_links
229 attachment_link = link_to('error281.txt', {:controller => 'attachments', :action => 'download', :id => '1'}, :class => 'attachment')
229 attachment_link = link_to('error281.txt', {:controller => 'attachments', :action => 'download', :id => '1'}, :class => 'attachment')
230 to_test = {
230 to_test = {
231 'attachment:error281.txt' => attachment_link
231 'attachment:error281.txt' => attachment_link
232 }
232 }
233 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text, :attachments => Issue.find(3).attachments), "#{text} failed" }
233 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text, :attachments => Issue.find(3).attachments), "#{text} failed" }
234 end
234 end
235
235
236 def test_wiki_links
236 def test_wiki_links
237 to_test = {
237 to_test = {
238 '[[CookBook documentation]]' => '<a href="/projects/ecookbook/wiki/CookBook_documentation" class="wiki-page">CookBook documentation</a>',
238 '[[CookBook documentation]]' => '<a href="/projects/ecookbook/wiki/CookBook_documentation" class="wiki-page">CookBook documentation</a>',
239 '[[Another page|Page]]' => '<a href="/projects/ecookbook/wiki/Another_page" class="wiki-page">Page</a>',
239 '[[Another page|Page]]' => '<a href="/projects/ecookbook/wiki/Another_page" class="wiki-page">Page</a>',
240 # link with anchor
240 # link with anchor
241 '[[CookBook documentation#One-section]]' => '<a href="/projects/ecookbook/wiki/CookBook_documentation#One-section" class="wiki-page">CookBook documentation</a>',
241 '[[CookBook documentation#One-section]]' => '<a href="/projects/ecookbook/wiki/CookBook_documentation#One-section" class="wiki-page">CookBook documentation</a>',
242 '[[Another page#anchor|Page]]' => '<a href="/projects/ecookbook/wiki/Another_page#anchor" class="wiki-page">Page</a>',
242 '[[Another page#anchor|Page]]' => '<a href="/projects/ecookbook/wiki/Another_page#anchor" class="wiki-page">Page</a>',
243 # page that doesn't exist
243 # page that doesn't exist
244 '[[Unknown page]]' => '<a href="/projects/ecookbook/wiki/Unknown_page" class="wiki-page new">Unknown page</a>',
244 '[[Unknown page]]' => '<a href="/projects/ecookbook/wiki/Unknown_page" class="wiki-page new">Unknown page</a>',
245 '[[Unknown page|404]]' => '<a href="/projects/ecookbook/wiki/Unknown_page" class="wiki-page new">404</a>',
245 '[[Unknown page|404]]' => '<a href="/projects/ecookbook/wiki/Unknown_page" class="wiki-page new">404</a>',
246 # link to another project wiki
246 # link to another project wiki
247 '[[onlinestore:]]' => '<a href="/projects/onlinestore/wiki" class="wiki-page">onlinestore</a>',
247 '[[onlinestore:]]' => '<a href="/projects/onlinestore/wiki" class="wiki-page">onlinestore</a>',
248 '[[onlinestore:|Wiki]]' => '<a href="/projects/onlinestore/wiki" class="wiki-page">Wiki</a>',
248 '[[onlinestore:|Wiki]]' => '<a href="/projects/onlinestore/wiki" class="wiki-page">Wiki</a>',
249 '[[onlinestore:Start page]]' => '<a href="/projects/onlinestore/wiki/Start_page" class="wiki-page">Start page</a>',
249 '[[onlinestore:Start page]]' => '<a href="/projects/onlinestore/wiki/Start_page" class="wiki-page">Start page</a>',
250 '[[onlinestore:Start page|Text]]' => '<a href="/projects/onlinestore/wiki/Start_page" class="wiki-page">Text</a>',
250 '[[onlinestore:Start page|Text]]' => '<a href="/projects/onlinestore/wiki/Start_page" class="wiki-page">Text</a>',
251 '[[onlinestore:Unknown page]]' => '<a href="/projects/onlinestore/wiki/Unknown_page" class="wiki-page new">Unknown page</a>',
251 '[[onlinestore:Unknown page]]' => '<a href="/projects/onlinestore/wiki/Unknown_page" class="wiki-page new">Unknown page</a>',
252 # striked through link
252 # striked through link
253 '-[[Another page|Page]]-' => '<del><a href="/projects/ecookbook/wiki/Another_page" class="wiki-page">Page</a></del>',
253 '-[[Another page|Page]]-' => '<del><a href="/projects/ecookbook/wiki/Another_page" class="wiki-page">Page</a></del>',
254 '-[[Another page|Page]] link-' => '<del><a href="/projects/ecookbook/wiki/Another_page" class="wiki-page">Page</a> link</del>',
254 '-[[Another page|Page]] link-' => '<del><a href="/projects/ecookbook/wiki/Another_page" class="wiki-page">Page</a> link</del>',
255 # escaping
255 # escaping
256 '![[Another page|Page]]' => '[[Another page|Page]]',
256 '![[Another page|Page]]' => '[[Another page|Page]]',
257 # project does not exist
257 # project does not exist
258 '[[unknowproject:Start]]' => '[[unknowproject:Start]]',
258 '[[unknowproject:Start]]' => '[[unknowproject:Start]]',
259 '[[unknowproject:Start|Page title]]' => '[[unknowproject:Start|Page title]]',
259 '[[unknowproject:Start|Page title]]' => '[[unknowproject:Start|Page title]]',
260 }
260 }
261 @project = Project.find(1)
261 @project = Project.find(1)
262 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
262 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
263 end
263 end
264
264
265 def test_html_tags
265 def test_html_tags
266 to_test = {
266 to_test = {
267 "<div>content</div>" => "<p>&lt;div&gt;content&lt;/div&gt;</p>",
267 "<div>content</div>" => "<p>&lt;div&gt;content&lt;/div&gt;</p>",
268 "<div class=\"bold\">content</div>" => "<p>&lt;div class=\"bold\"&gt;content&lt;/div&gt;</p>",
268 "<div class=\"bold\">content</div>" => "<p>&lt;div class=\"bold\"&gt;content&lt;/div&gt;</p>",
269 "<script>some script;</script>" => "<p>&lt;script&gt;some script;&lt;/script&gt;</p>",
269 "<script>some script;</script>" => "<p>&lt;script&gt;some script;&lt;/script&gt;</p>",
270 # do not escape pre/code tags
270 # do not escape pre/code tags
271 "<pre>\nline 1\nline2</pre>" => "<pre>\nline 1\nline2</pre>",
271 "<pre>\nline 1\nline2</pre>" => "<pre>\nline 1\nline2</pre>",
272 "<pre><code>\nline 1\nline2</code></pre>" => "<pre><code>\nline 1\nline2</code></pre>",
272 "<pre><code>\nline 1\nline2</code></pre>" => "<pre><code>\nline 1\nline2</code></pre>",
273 "<pre><div>content</div></pre>" => "<pre>&lt;div&gt;content&lt;/div&gt;</pre>",
273 "<pre><div>content</div></pre>" => "<pre>&lt;div&gt;content&lt;/div&gt;</pre>",
274 "HTML comment: <!-- no comments -->" => "<p>HTML comment: &lt;!-- no comments --&gt;</p>",
274 "HTML comment: <!-- no comments -->" => "<p>HTML comment: &lt;!-- no comments --&gt;</p>",
275 "<!-- opening comment" => "<p>&lt;!-- opening comment</p>",
275 "<!-- opening comment" => "<p>&lt;!-- opening comment</p>",
276 # remove attributes except class
276 # remove attributes except class
277 "<pre class='foo'>some text</pre>" => "<pre class='foo'>some text</pre>",
277 "<pre class='foo'>some text</pre>" => "<pre class='foo'>some text</pre>",
278 "<pre onmouseover='alert(1)'>some text</pre>" => "<pre>some text</pre>",
278 "<pre onmouseover='alert(1)'>some text</pre>" => "<pre>some text</pre>",
279 }
279 }
280 to_test.each { |text, result| assert_equal result, textilizable(text) }
280 to_test.each { |text, result| assert_equal result, textilizable(text) }
281 end
281 end
282
282
283 def test_allowed_html_tags
283 def test_allowed_html_tags
284 to_test = {
284 to_test = {
285 "<pre>preformatted text</pre>" => "<pre>preformatted text</pre>",
285 "<pre>preformatted text</pre>" => "<pre>preformatted text</pre>",
286 "<notextile>no *textile* formatting</notextile>" => "no *textile* formatting",
286 "<notextile>no *textile* formatting</notextile>" => "no *textile* formatting",
287 "<notextile>this is <tag>a tag</tag></notextile>" => "this is &lt;tag&gt;a tag&lt;/tag&gt;"
287 "<notextile>this is <tag>a tag</tag></notextile>" => "this is &lt;tag&gt;a tag&lt;/tag&gt;"
288 }
288 }
289 to_test.each { |text, result| assert_equal result, textilizable(text) }
289 to_test.each { |text, result| assert_equal result, textilizable(text) }
290 end
290 end
291
291
292 def test_pre_tags
292 def test_pre_tags
293 raw = <<-RAW
293 raw = <<-RAW
294 Before
294 Before
295
295
296 <pre>
296 <pre>
297 <prepared-statement-cache-size>32</prepared-statement-cache-size>
297 <prepared-statement-cache-size>32</prepared-statement-cache-size>
298 </pre>
298 </pre>
299
299
300 After
300 After
301 RAW
301 RAW
302
302
303 expected = <<-EXPECTED
303 expected = <<-EXPECTED
304 <p>Before</p>
304 <p>Before</p>
305 <pre>
305 <pre>
306 &lt;prepared-statement-cache-size&gt;32&lt;/prepared-statement-cache-size&gt;
306 &lt;prepared-statement-cache-size&gt;32&lt;/prepared-statement-cache-size&gt;
307 </pre>
307 </pre>
308 <p>After</p>
308 <p>After</p>
309 EXPECTED
309 EXPECTED
310
310
311 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
311 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
312 end
312 end
313
313
314 def test_pre_content_should_not_parse_wiki_and_redmine_links
314 def test_pre_content_should_not_parse_wiki_and_redmine_links
315 raw = <<-RAW
315 raw = <<-RAW
316 [[CookBook documentation]]
316 [[CookBook documentation]]
317
317
318 #1
318 #1
319
319
320 <pre>
320 <pre>
321 [[CookBook documentation]]
321 [[CookBook documentation]]
322
322
323 #1
323 #1
324 </pre>
324 </pre>
325 RAW
325 RAW
326
326
327 expected = <<-EXPECTED
327 expected = <<-EXPECTED
328 <p><a href="/projects/ecookbook/wiki/CookBook_documentation" class="wiki-page">CookBook documentation</a></p>
328 <p><a href="/projects/ecookbook/wiki/CookBook_documentation" class="wiki-page">CookBook documentation</a></p>
329 <p><a href="/issues/1" class="issue status-1 priority-1" title="Can't print recipes (New)">#1</a></p>
329 <p><a href="/issues/1" class="issue status-1 priority-1" title="Can't print recipes (New)">#1</a></p>
330 <pre>
330 <pre>
331 [[CookBook documentation]]
331 [[CookBook documentation]]
332
332
333 #1
333 #1
334 </pre>
334 </pre>
335 EXPECTED
335 EXPECTED
336
336
337 @project = Project.find(1)
337 @project = Project.find(1)
338 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
338 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
339 end
339 end
340
340
341 def test_non_closing_pre_blocks_should_be_closed
341 def test_non_closing_pre_blocks_should_be_closed
342 raw = <<-RAW
342 raw = <<-RAW
343 <pre><code>
343 <pre><code>
344 RAW
344 RAW
345
345
346 expected = <<-EXPECTED
346 expected = <<-EXPECTED
347 <pre><code>
347 <pre><code>
348 </code></pre>
348 </code></pre>
349 EXPECTED
349 EXPECTED
350
350
351 @project = Project.find(1)
351 @project = Project.find(1)
352 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
352 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
353 end
353 end
354
354
355 def test_syntax_highlight
355 def test_syntax_highlight
356 raw = <<-RAW
356 raw = <<-RAW
357 <pre><code class="ruby">
357 <pre><code class="ruby">
358 # Some ruby code here
358 # Some ruby code here
359 </code></pre>
359 </code></pre>
360 RAW
360 RAW
361
361
362 expected = <<-EXPECTED
362 expected = <<-EXPECTED
363 <pre><code class="ruby syntaxhl"><span class=\"CodeRay\"><span class="no">1</span> <span class="c"># Some ruby code here</span></span>
363 <pre><code class="ruby syntaxhl"><span class=\"CodeRay\"><span class="no">1</span> <span class="c"># Some ruby code here</span></span>
364 </code></pre>
364 </code></pre>
365 EXPECTED
365 EXPECTED
366
366
367 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
367 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
368 end
368 end
369
369
370 def test_wiki_links_in_tables
370 def test_wiki_links_in_tables
371 to_test = {"|[[Page|Link title]]|[[Other Page|Other title]]|\n|Cell 21|[[Last page]]|" =>
371 to_test = {"|[[Page|Link title]]|[[Other Page|Other title]]|\n|Cell 21|[[Last page]]|" =>
372 '<tr><td><a href="/projects/ecookbook/wiki/Page" class="wiki-page new">Link title</a></td>' +
372 '<tr><td><a href="/projects/ecookbook/wiki/Page" class="wiki-page new">Link title</a></td>' +
373 '<td><a href="/projects/ecookbook/wiki/Other_Page" class="wiki-page new">Other title</a></td>' +
373 '<td><a href="/projects/ecookbook/wiki/Other_Page" class="wiki-page new">Other title</a></td>' +
374 '</tr><tr><td>Cell 21</td><td><a href="/projects/ecookbook/wiki/Last_page" class="wiki-page new">Last page</a></td></tr>'
374 '</tr><tr><td>Cell 21</td><td><a href="/projects/ecookbook/wiki/Last_page" class="wiki-page new">Last page</a></td></tr>'
375 }
375 }
376 @project = Project.find(1)
376 @project = Project.find(1)
377 to_test.each { |text, result| assert_equal "<table>#{result}</table>", textilizable(text).gsub(/[\t\n]/, '') }
377 to_test.each { |text, result| assert_equal "<table>#{result}</table>", textilizable(text).gsub(/[\t\n]/, '') }
378 end
378 end
379
379
380 def test_text_formatting
380 def test_text_formatting
381 to_test = {'*_+bold, italic and underline+_*' => '<strong><em><ins>bold, italic and underline</ins></em></strong>',
381 to_test = {'*_+bold, italic and underline+_*' => '<strong><em><ins>bold, italic and underline</ins></em></strong>',
382 '(_text within parentheses_)' => '(<em>text within parentheses</em>)',
382 '(_text within parentheses_)' => '(<em>text within parentheses</em>)',
383 'a *Humane Web* Text Generator' => 'a <strong>Humane Web</strong> Text Generator',
383 'a *Humane Web* Text Generator' => 'a <strong>Humane Web</strong> Text Generator',
384 '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>',
384 '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>',
385 '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',
385 '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',
386 }
386 }
387 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
387 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
388 end
388 end
389
389
390 def test_wiki_horizontal_rule
390 def test_wiki_horizontal_rule
391 assert_equal '<hr />', textilizable('---')
391 assert_equal '<hr />', textilizable('---')
392 assert_equal '<p>Dashes: ---</p>', textilizable('Dashes: ---')
392 assert_equal '<p>Dashes: ---</p>', textilizable('Dashes: ---')
393 end
393 end
394
394
395 def test_footnotes
395 def test_footnotes
396 raw = <<-RAW
396 raw = <<-RAW
397 This is some text[1].
397 This is some text[1].
398
398
399 fn1. This is the foot note
399 fn1. This is the foot note
400 RAW
400 RAW
401
401
402 expected = <<-EXPECTED
402 expected = <<-EXPECTED
403 <p>This is some text<sup><a href=\"#fn1\">1</a></sup>.</p>
403 <p>This is some text<sup><a href=\"#fn1\">1</a></sup>.</p>
404 <p id="fn1" class="footnote"><sup>1</sup> This is the foot note</p>
404 <p id="fn1" class="footnote"><sup>1</sup> This is the foot note</p>
405 EXPECTED
405 EXPECTED
406
406
407 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
407 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
408 end
408 end
409
409
410 def test_table_of_content
410 def test_table_of_content
411 raw = <<-RAW
411 raw = <<-RAW
412 {{toc}}
412 {{toc}}
413
413
414 h1. Title
414 h1. Title
415
415
416 Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas sed libero.
416 Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas sed libero.
417
417
418 h2. Subtitle with a [[Wiki]] link
418 h2. Subtitle with a [[Wiki]] link
419
419
420 Nullam commodo metus accumsan nulla. Curabitur lobortis dui id dolor.
420 Nullam commodo metus accumsan nulla. Curabitur lobortis dui id dolor.
421
421
422 h2. Subtitle with [[Wiki|another Wiki]] link
422 h2. Subtitle with [[Wiki|another Wiki]] link
423
423
424 h2. Subtitle with %{color:red}red text%
424 h2. Subtitle with %{color:red}red text%
425
425
426 h2. Subtitle with *some* _modifiers_
427
426 h1. Another title
428 h1. Another title
427
429
428 h2. An "Internet link":http://www.redmine.org/ inside subtitle
430 h2. An "Internet link":http://www.redmine.org/ inside subtitle
429
431
430 h2. "Project Name !/attachments/1234/logo_small.gif! !/attachments/5678/logo_2.png!":/projects/projectname/issues
432 h2. "Project Name !/attachments/1234/logo_small.gif! !/attachments/5678/logo_2.png!":/projects/projectname/issues
431
433
432 RAW
434 RAW
433
435
434 expected = '<ul class="toc">' +
436 expected = '<ul class="toc">' +
435 '<li class="heading1"><a href="#Title">Title</a></li>' +
437 '<li class="heading1"><a href="#Title">Title</a></li>' +
436 '<li class="heading2"><a href="#Subtitle-with-a-Wiki-link">Subtitle with a Wiki link</a></li>' +
438 '<li class="heading2"><a href="#Subtitle-with-a-Wiki-link">Subtitle with a Wiki link</a></li>' +
437 '<li class="heading2"><a href="#Subtitle-with-another-Wiki-link">Subtitle with another Wiki link</a></li>' +
439 '<li class="heading2"><a href="#Subtitle-with-another-Wiki-link">Subtitle with another Wiki link</a></li>' +
438 '<li class="heading2"><a href="#Subtitle-with-red-text">Subtitle with red text</a></li>' +
440 '<li class="heading2"><a href="#Subtitle-with-red-text">Subtitle with red text</a></li>' +
441 '<li class="heading2"><a href="#Subtitle-with-some-modifiers">Subtitle with some modifiers</a></li>' +
439 '<li class="heading1"><a href="#Another-title">Another title</a></li>' +
442 '<li class="heading1"><a href="#Another-title">Another title</a></li>' +
440 '<li class="heading2"><a href="#An-Internet-link-inside-subtitle">An Internet link inside subtitle</a></li>' +
443 '<li class="heading2"><a href="#An-Internet-link-inside-subtitle">An Internet link inside subtitle</a></li>' +
441 '<li class="heading2"><a href="#Project-Name">Project Name</a></li>' +
444 '<li class="heading2"><a href="#Project-Name">Project Name</a></li>' +
442 '</ul>'
445 '</ul>'
443
446
447 @project = Project.find(1)
448 assert textilizable(raw).gsub("\n", "").include?(expected)
449 end
450
451 def test_table_of_content_should_contain_included_page_headings
452 raw = <<-RAW
453 {{toc}}
454
455 h1. Included
456
457 {{include(Child_1)}}
458 RAW
459
460 expected = '<ul class="toc">' +
461 '<li class="heading1"><a href="#Included">Included</a></li>' +
462 '<li class="heading1"><a href="#Child-page-1">Child page 1</a></li>' +
463 '</ul>'
464
465 @project = Project.find(1)
444 assert textilizable(raw).gsub("\n", "").include?(expected)
466 assert textilizable(raw).gsub("\n", "").include?(expected)
445 end
467 end
446
468
447 def test_blockquote
469 def test_blockquote
448 # orig raw text
470 # orig raw text
449 raw = <<-RAW
471 raw = <<-RAW
450 John said:
472 John said:
451 > Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas sed libero.
473 > Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas sed libero.
452 > Nullam commodo metus accumsan nulla. Curabitur lobortis dui id dolor.
474 > Nullam commodo metus accumsan nulla. Curabitur lobortis dui id dolor.
453 > * Donec odio lorem,
475 > * Donec odio lorem,
454 > * sagittis ac,
476 > * sagittis ac,
455 > * malesuada in,
477 > * malesuada in,
456 > * adipiscing eu, dolor.
478 > * adipiscing eu, dolor.
457 >
479 >
458 > >Nulla varius pulvinar diam. Proin id arcu id lorem scelerisque condimentum. Proin vehicula turpis vitae lacus.
480 > >Nulla varius pulvinar diam. Proin id arcu id lorem scelerisque condimentum. Proin vehicula turpis vitae lacus.
459 > Proin a tellus. Nam vel neque.
481 > Proin a tellus. Nam vel neque.
460
482
461 He's right.
483 He's right.
462 RAW
484 RAW
463
485
464 # expected html
486 # expected html
465 expected = <<-EXPECTED
487 expected = <<-EXPECTED
466 <p>John said:</p>
488 <p>John said:</p>
467 <blockquote>
489 <blockquote>
468 Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas sed libero.
490 Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas sed libero.
469 Nullam commodo metus accumsan nulla. Curabitur lobortis dui id dolor.
491 Nullam commodo metus accumsan nulla. Curabitur lobortis dui id dolor.
470 <ul>
492 <ul>
471 <li>Donec odio lorem,</li>
493 <li>Donec odio lorem,</li>
472 <li>sagittis ac,</li>
494 <li>sagittis ac,</li>
473 <li>malesuada in,</li>
495 <li>malesuada in,</li>
474 <li>adipiscing eu, dolor.</li>
496 <li>adipiscing eu, dolor.</li>
475 </ul>
497 </ul>
476 <blockquote>
498 <blockquote>
477 <p>Nulla varius pulvinar diam. Proin id arcu id lorem scelerisque condimentum. Proin vehicula turpis vitae lacus.</p>
499 <p>Nulla varius pulvinar diam. Proin id arcu id lorem scelerisque condimentum. Proin vehicula turpis vitae lacus.</p>
478 </blockquote>
500 </blockquote>
479 <p>Proin a tellus. Nam vel neque.</p>
501 <p>Proin a tellus. Nam vel neque.</p>
480 </blockquote>
502 </blockquote>
481 <p>He's right.</p>
503 <p>He's right.</p>
482 EXPECTED
504 EXPECTED
483
505
484 assert_equal expected.gsub(%r{\s+}, ''), textilizable(raw).gsub(%r{\s+}, '')
506 assert_equal expected.gsub(%r{\s+}, ''), textilizable(raw).gsub(%r{\s+}, '')
485 end
507 end
486
508
487 def test_table
509 def test_table
488 raw = <<-RAW
510 raw = <<-RAW
489 This is a table with empty cells:
511 This is a table with empty cells:
490
512
491 |cell11|cell12||
513 |cell11|cell12||
492 |cell21||cell23|
514 |cell21||cell23|
493 |cell31|cell32|cell33|
515 |cell31|cell32|cell33|
494 RAW
516 RAW
495
517
496 expected = <<-EXPECTED
518 expected = <<-EXPECTED
497 <p>This is a table with empty cells:</p>
519 <p>This is a table with empty cells:</p>
498
520
499 <table>
521 <table>
500 <tr><td>cell11</td><td>cell12</td><td></td></tr>
522 <tr><td>cell11</td><td>cell12</td><td></td></tr>
501 <tr><td>cell21</td><td></td><td>cell23</td></tr>
523 <tr><td>cell21</td><td></td><td>cell23</td></tr>
502 <tr><td>cell31</td><td>cell32</td><td>cell33</td></tr>
524 <tr><td>cell31</td><td>cell32</td><td>cell33</td></tr>
503 </table>
525 </table>
504 EXPECTED
526 EXPECTED
505
527
506 assert_equal expected.gsub(%r{\s+}, ''), textilizable(raw).gsub(%r{\s+}, '')
528 assert_equal expected.gsub(%r{\s+}, ''), textilizable(raw).gsub(%r{\s+}, '')
507 end
529 end
508
530
509 def test_table_with_line_breaks
531 def test_table_with_line_breaks
510 raw = <<-RAW
532 raw = <<-RAW
511 This is a table with line breaks:
533 This is a table with line breaks:
512
534
513 |cell11
535 |cell11
514 continued|cell12||
536 continued|cell12||
515 |-cell21-||cell23
537 |-cell21-||cell23
516 cell23 line2
538 cell23 line2
517 cell23 *line3*|
539 cell23 *line3*|
518 |cell31|cell32
540 |cell31|cell32
519 cell32 line2|cell33|
541 cell32 line2|cell33|
520
542
521 RAW
543 RAW
522
544
523 expected = <<-EXPECTED
545 expected = <<-EXPECTED
524 <p>This is a table with line breaks:</p>
546 <p>This is a table with line breaks:</p>
525
547
526 <table>
548 <table>
527 <tr>
549 <tr>
528 <td>cell11<br />continued</td>
550 <td>cell11<br />continued</td>
529 <td>cell12</td>
551 <td>cell12</td>
530 <td></td>
552 <td></td>
531 </tr>
553 </tr>
532 <tr>
554 <tr>
533 <td><del>cell21</del></td>
555 <td><del>cell21</del></td>
534 <td></td>
556 <td></td>
535 <td>cell23<br/>cell23 line2<br/>cell23 <strong>line3</strong></td>
557 <td>cell23<br/>cell23 line2<br/>cell23 <strong>line3</strong></td>
536 </tr>
558 </tr>
537 <tr>
559 <tr>
538 <td>cell31</td>
560 <td>cell31</td>
539 <td>cell32<br/>cell32 line2</td>
561 <td>cell32<br/>cell32 line2</td>
540 <td>cell33</td>
562 <td>cell33</td>
541 </tr>
563 </tr>
542 </table>
564 </table>
543 EXPECTED
565 EXPECTED
544
566
545 assert_equal expected.gsub(%r{\s+}, ''), textilizable(raw).gsub(%r{\s+}, '')
567 assert_equal expected.gsub(%r{\s+}, ''), textilizable(raw).gsub(%r{\s+}, '')
546 end
568 end
547
569
548 def test_textile_should_not_mangle_brackets
570 def test_textile_should_not_mangle_brackets
549 assert_equal '<p>[msg1][msg2]</p>', textilizable('[msg1][msg2]')
571 assert_equal '<p>[msg1][msg2]</p>', textilizable('[msg1][msg2]')
550 end
572 end
551
573
552 def test_default_formatter
574 def test_default_formatter
553 Setting.text_formatting = 'unknown'
575 Setting.text_formatting = 'unknown'
554 text = 'a *link*: http://www.example.net/'
576 text = 'a *link*: http://www.example.net/'
555 assert_equal '<p>a *link*: <a href="http://www.example.net/">http://www.example.net/</a></p>', textilizable(text)
577 assert_equal '<p>a *link*: <a href="http://www.example.net/">http://www.example.net/</a></p>', textilizable(text)
556 Setting.text_formatting = 'textile'
578 Setting.text_formatting = 'textile'
557 end
579 end
558
580
559 def test_due_date_distance_in_words
581 def test_due_date_distance_in_words
560 to_test = { Date.today => 'Due in 0 days',
582 to_test = { Date.today => 'Due in 0 days',
561 Date.today + 1 => 'Due in 1 day',
583 Date.today + 1 => 'Due in 1 day',
562 Date.today + 100 => 'Due in about 3 months',
584 Date.today + 100 => 'Due in about 3 months',
563 Date.today + 20000 => 'Due in over 54 years',
585 Date.today + 20000 => 'Due in over 54 years',
564 Date.today - 1 => '1 day late',
586 Date.today - 1 => '1 day late',
565 Date.today - 100 => 'about 3 months late',
587 Date.today - 100 => 'about 3 months late',
566 Date.today - 20000 => 'over 54 years late',
588 Date.today - 20000 => 'over 54 years late',
567 }
589 }
568 to_test.each do |date, expected|
590 to_test.each do |date, expected|
569 assert_equal expected, due_date_distance_in_words(date)
591 assert_equal expected, due_date_distance_in_words(date)
570 end
592 end
571 end
593 end
572
594
573 def test_avatar
595 def test_avatar
574 # turn on avatars
596 # turn on avatars
575 Setting.gravatar_enabled = '1'
597 Setting.gravatar_enabled = '1'
576 assert avatar(User.find_by_mail('jsmith@somenet.foo')).include?(Digest::MD5.hexdigest('jsmith@somenet.foo'))
598 assert avatar(User.find_by_mail('jsmith@somenet.foo')).include?(Digest::MD5.hexdigest('jsmith@somenet.foo'))
577 assert avatar('jsmith <jsmith@somenet.foo>').include?(Digest::MD5.hexdigest('jsmith@somenet.foo'))
599 assert avatar('jsmith <jsmith@somenet.foo>').include?(Digest::MD5.hexdigest('jsmith@somenet.foo'))
578 assert_nil avatar('jsmith')
600 assert_nil avatar('jsmith')
579 assert_nil avatar(nil)
601 assert_nil avatar(nil)
580
602
581 # turn off avatars
603 # turn off avatars
582 Setting.gravatar_enabled = '0'
604 Setting.gravatar_enabled = '0'
583 assert_equal '', avatar(User.find_by_mail('jsmith@somenet.foo'))
605 assert_equal '', avatar(User.find_by_mail('jsmith@somenet.foo'))
584 end
606 end
585
607
586 def test_link_to_user
608 def test_link_to_user
587 user = User.find(2)
609 user = User.find(2)
588 t = link_to_user(user)
610 t = link_to_user(user)
589 assert_equal "<a href=\"/users/2\">#{ user.name }</a>", t
611 assert_equal "<a href=\"/users/2\">#{ user.name }</a>", t
590 end
612 end
591
613
592 def test_link_to_user_should_not_link_to_locked_user
614 def test_link_to_user_should_not_link_to_locked_user
593 user = User.find(5)
615 user = User.find(5)
594 assert user.locked?
616 assert user.locked?
595 t = link_to_user(user)
617 t = link_to_user(user)
596 assert_equal user.name, t
618 assert_equal user.name, t
597 end
619 end
598
620
599 def test_link_to_user_should_not_link_to_anonymous
621 def test_link_to_user_should_not_link_to_anonymous
600 user = User.anonymous
622 user = User.anonymous
601 assert user.anonymous?
623 assert user.anonymous?
602 t = link_to_user(user)
624 t = link_to_user(user)
603 assert_equal ::I18n.t(:label_user_anonymous), t
625 assert_equal ::I18n.t(:label_user_anonymous), t
604 end
626 end
605
627
606 def test_link_to_project
628 def test_link_to_project
607 project = Project.find(1)
629 project = Project.find(1)
608 assert_equal %(<a href="/projects/ecookbook">eCookbook</a>),
630 assert_equal %(<a href="/projects/ecookbook">eCookbook</a>),
609 link_to_project(project)
631 link_to_project(project)
610 assert_equal %(<a href="/projects/ecookbook/settings">eCookbook</a>),
632 assert_equal %(<a href="/projects/ecookbook/settings">eCookbook</a>),
611 link_to_project(project, :action => 'settings')
633 link_to_project(project, :action => 'settings')
612 assert_equal %(<a href="http://test.host/projects/ecookbook?jump=blah">eCookbook</a>),
634 assert_equal %(<a href="http://test.host/projects/ecookbook?jump=blah">eCookbook</a>),
613 link_to_project(project, {:only_path => false, :jump => 'blah'})
635 link_to_project(project, {:only_path => false, :jump => 'blah'})
614 assert_equal %(<a href="/projects/ecookbook/settings" class="project">eCookbook</a>),
636 assert_equal %(<a href="/projects/ecookbook/settings" class="project">eCookbook</a>),
615 link_to_project(project, {:action => 'settings'}, :class => "project")
637 link_to_project(project, {:action => 'settings'}, :class => "project")
616 end
638 end
617 end
639 end
@@ -1,98 +1,101
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2008 Jean-Philippe Lang
2 # Copyright (C) 2006-2008 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.dirname(__FILE__) + '/../../../../test_helper'
18 require File.dirname(__FILE__) + '/../../../../test_helper'
19
19
20 class Redmine::WikiFormatting::MacrosTest < HelperTestCase
20 class Redmine::WikiFormatting::MacrosTest < HelperTestCase
21 include ApplicationHelper
21 include ApplicationHelper
22 include ActionView::Helpers::TextHelper
22 include ActionView::Helpers::TextHelper
23 include ActionView::Helpers::SanitizeHelper
24 extend ActionView::Helpers::SanitizeHelper::ClassMethods
25
23 fixtures :projects, :roles, :enabled_modules, :users,
26 fixtures :projects, :roles, :enabled_modules, :users,
24 :repositories, :changesets,
27 :repositories, :changesets,
25 :trackers, :issue_statuses, :issues,
28 :trackers, :issue_statuses, :issues,
26 :versions, :documents,
29 :versions, :documents,
27 :wikis, :wiki_pages, :wiki_contents,
30 :wikis, :wiki_pages, :wiki_contents,
28 :boards, :messages,
31 :boards, :messages,
29 :attachments
32 :attachments
30
33
31 def setup
34 def setup
32 super
35 super
33 @project = nil
36 @project = nil
34 end
37 end
35
38
36 def teardown
39 def teardown
37 end
40 end
38
41
39 def test_macro_hello_world
42 def test_macro_hello_world
40 text = "{{hello_world}}"
43 text = "{{hello_world}}"
41 assert textilizable(text).match(/Hello world!/)
44 assert textilizable(text).match(/Hello world!/)
42 # escaping
45 # escaping
43 text = "!{{hello_world}}"
46 text = "!{{hello_world}}"
44 assert_equal '<p>{{hello_world}}</p>', textilizable(text)
47 assert_equal '<p>{{hello_world}}</p>', textilizable(text)
45 end
48 end
46
49
47 def test_macro_include
50 def test_macro_include
48 @project = Project.find(1)
51 @project = Project.find(1)
49 # include a page of the current project wiki
52 # include a page of the current project wiki
50 text = "{{include(Another page)}}"
53 text = "{{include(Another page)}}"
51 assert textilizable(text).match(/This is a link to a ticket/)
54 assert textilizable(text).match(/This is a link to a ticket/)
52
55
53 @project = nil
56 @project = nil
54 # include a page of a specific project wiki
57 # include a page of a specific project wiki
55 text = "{{include(ecookbook:Another page)}}"
58 text = "{{include(ecookbook:Another page)}}"
56 assert textilizable(text).match(/This is a link to a ticket/)
59 assert textilizable(text).match(/This is a link to a ticket/)
57
60
58 text = "{{include(ecookbook:)}}"
61 text = "{{include(ecookbook:)}}"
59 assert textilizable(text).match(/CookBook documentation/)
62 assert textilizable(text).match(/CookBook documentation/)
60
63
61 text = "{{include(unknowidentifier:somepage)}}"
64 text = "{{include(unknowidentifier:somepage)}}"
62 assert textilizable(text).match(/Page not found/)
65 assert textilizable(text).match(/Page not found/)
63 end
66 end
64
67
65 def test_macro_child_pages
68 def test_macro_child_pages
66 expected = "<p><ul class=\"pages-hierarchy\">\n" +
69 expected = "<p><ul class=\"pages-hierarchy\">\n" +
67 "<li><a href=\"/projects/ecookbook/wiki/Child_1\">Child 1</a></li>\n" +
70 "<li><a href=\"/projects/ecookbook/wiki/Child_1\">Child 1</a></li>\n" +
68 "<li><a href=\"/projects/ecookbook/wiki/Child_2\">Child 2</a></li>\n" +
71 "<li><a href=\"/projects/ecookbook/wiki/Child_2\">Child 2</a></li>\n" +
69 "</ul>\n</p>"
72 "</ul>\n</p>"
70
73
71 @project = Project.find(1)
74 @project = Project.find(1)
72 # child pages of the current wiki page
75 # child pages of the current wiki page
73 assert_equal expected, textilizable("{{child_pages}}", :object => WikiPage.find(2).content)
76 assert_equal expected, textilizable("{{child_pages}}", :object => WikiPage.find(2).content)
74 # child pages of another page
77 # child pages of another page
75 assert_equal expected, textilizable("{{child_pages(Another_page)}}", :object => WikiPage.find(1).content)
78 assert_equal expected, textilizable("{{child_pages(Another_page)}}", :object => WikiPage.find(1).content)
76
79
77 @project = Project.find(2)
80 @project = Project.find(2)
78 assert_equal expected, textilizable("{{child_pages(ecookbook:Another_page)}}", :object => WikiPage.find(1).content)
81 assert_equal expected, textilizable("{{child_pages(ecookbook:Another_page)}}", :object => WikiPage.find(1).content)
79 end
82 end
80
83
81 def test_macro_child_pages_with_option
84 def test_macro_child_pages_with_option
82 expected = "<p><ul class=\"pages-hierarchy\">\n" +
85 expected = "<p><ul class=\"pages-hierarchy\">\n" +
83 "<li><a href=\"/projects/ecookbook/wiki/Another_page\">Another page</a>\n" +
86 "<li><a href=\"/projects/ecookbook/wiki/Another_page\">Another page</a>\n" +
84 "<ul class=\"pages-hierarchy\">\n" +
87 "<ul class=\"pages-hierarchy\">\n" +
85 "<li><a href=\"/projects/ecookbook/wiki/Child_1\">Child 1</a></li>\n" +
88 "<li><a href=\"/projects/ecookbook/wiki/Child_1\">Child 1</a></li>\n" +
86 "<li><a href=\"/projects/ecookbook/wiki/Child_2\">Child 2</a></li>\n" +
89 "<li><a href=\"/projects/ecookbook/wiki/Child_2\">Child 2</a></li>\n" +
87 "</ul>\n</li>\n</ul>\n</p>"
90 "</ul>\n</li>\n</ul>\n</p>"
88
91
89 @project = Project.find(1)
92 @project = Project.find(1)
90 # child pages of the current wiki page
93 # child pages of the current wiki page
91 assert_equal expected, textilizable("{{child_pages(parent=1)}}", :object => WikiPage.find(2).content)
94 assert_equal expected, textilizable("{{child_pages(parent=1)}}", :object => WikiPage.find(2).content)
92 # child pages of another page
95 # child pages of another page
93 assert_equal expected, textilizable("{{child_pages(Another_page, parent=1)}}", :object => WikiPage.find(1).content)
96 assert_equal expected, textilizable("{{child_pages(Another_page, parent=1)}}", :object => WikiPage.find(1).content)
94
97
95 @project = Project.find(2)
98 @project = Project.find(2)
96 assert_equal expected, textilizable("{{child_pages(ecookbook:Another_page, parent=1)}}", :object => WikiPage.find(1).content)
99 assert_equal expected, textilizable("{{child_pages(ecookbook:Another_page, parent=1)}}", :object => WikiPage.find(1).content)
97 end
100 end
98 end
101 end
General Comments 0
You need to be logged in to leave comments. Login now