##// END OF EJS Templates
Slight change to the macro regexp....
Jean-Philippe Lang -
r9995:b90739878805
parent child
Show More
@@ -1,1204 +1,1204
1 # encoding: utf-8
1 # encoding: utf-8
2 #
2 #
3 # Redmine - project management software
3 # Redmine - project management software
4 # Copyright (C) 2006-2012 Jean-Philippe Lang
4 # Copyright (C) 2006-2012 Jean-Philippe Lang
5 #
5 #
6 # This program is free software; you can redistribute it and/or
6 # This program is free software; you can redistribute it and/or
7 # modify it under the terms of the GNU General Public License
7 # modify it under the terms of the GNU General Public License
8 # as published by the Free Software Foundation; either version 2
8 # as published by the Free Software Foundation; either version 2
9 # of the License, or (at your option) any later version.
9 # of the License, or (at your option) any later version.
10 #
10 #
11 # This program is distributed in the hope that it will be useful,
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
14 # GNU General Public License for more details.
15 #
15 #
16 # You should have received a copy of the GNU General Public License
16 # You should have received a copy of the GNU General Public License
17 # along with this program; if not, write to the Free Software
17 # along with this program; if not, write to the Free Software
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19
19
20 require 'forwardable'
20 require 'forwardable'
21 require 'cgi'
21 require 'cgi'
22
22
23 module ApplicationHelper
23 module ApplicationHelper
24 include Redmine::WikiFormatting::Macros::Definitions
24 include Redmine::WikiFormatting::Macros::Definitions
25 include Redmine::I18n
25 include Redmine::I18n
26 include GravatarHelper::PublicMethods
26 include GravatarHelper::PublicMethods
27
27
28 extend Forwardable
28 extend Forwardable
29 def_delegators :wiki_helper, :wikitoolbar_for, :heads_for_wiki_formatter
29 def_delegators :wiki_helper, :wikitoolbar_for, :heads_for_wiki_formatter
30
30
31 # Return true if user is authorized for controller/action, otherwise false
31 # Return true if user is authorized for controller/action, otherwise false
32 def authorize_for(controller, action)
32 def authorize_for(controller, action)
33 User.current.allowed_to?({:controller => controller, :action => action}, @project)
33 User.current.allowed_to?({:controller => controller, :action => action}, @project)
34 end
34 end
35
35
36 # Display a link if user is authorized
36 # Display a link if user is authorized
37 #
37 #
38 # @param [String] name Anchor text (passed to link_to)
38 # @param [String] name Anchor text (passed to link_to)
39 # @param [Hash] options Hash params. This will checked by authorize_for to see if the user is authorized
39 # @param [Hash] options Hash params. This will checked by authorize_for to see if the user is authorized
40 # @param [optional, Hash] html_options Options passed to link_to
40 # @param [optional, Hash] html_options Options passed to link_to
41 # @param [optional, Hash] parameters_for_method_reference Extra parameters for link_to
41 # @param [optional, Hash] parameters_for_method_reference Extra parameters for link_to
42 def link_to_if_authorized(name, options = {}, html_options = nil, *parameters_for_method_reference)
42 def link_to_if_authorized(name, options = {}, html_options = nil, *parameters_for_method_reference)
43 link_to(name, options, html_options, *parameters_for_method_reference) if authorize_for(options[:controller] || params[:controller], options[:action])
43 link_to(name, options, html_options, *parameters_for_method_reference) if authorize_for(options[:controller] || params[:controller], options[:action])
44 end
44 end
45
45
46 # Displays a link to user's account page if active
46 # Displays a link to user's account page if active
47 def link_to_user(user, options={})
47 def link_to_user(user, options={})
48 if user.is_a?(User)
48 if user.is_a?(User)
49 name = h(user.name(options[:format]))
49 name = h(user.name(options[:format]))
50 if user.active?
50 if user.active?
51 link_to name, :controller => 'users', :action => 'show', :id => user
51 link_to name, :controller => 'users', :action => 'show', :id => user
52 else
52 else
53 name
53 name
54 end
54 end
55 else
55 else
56 h(user.to_s)
56 h(user.to_s)
57 end
57 end
58 end
58 end
59
59
60 # Displays a link to +issue+ with its subject.
60 # Displays a link to +issue+ with its subject.
61 # Examples:
61 # Examples:
62 #
62 #
63 # link_to_issue(issue) # => Defect #6: This is the subject
63 # link_to_issue(issue) # => Defect #6: This is the subject
64 # link_to_issue(issue, :truncate => 6) # => Defect #6: This i...
64 # link_to_issue(issue, :truncate => 6) # => Defect #6: This i...
65 # link_to_issue(issue, :subject => false) # => Defect #6
65 # link_to_issue(issue, :subject => false) # => Defect #6
66 # link_to_issue(issue, :project => true) # => Foo - Defect #6
66 # link_to_issue(issue, :project => true) # => Foo - Defect #6
67 #
67 #
68 def link_to_issue(issue, options={})
68 def link_to_issue(issue, options={})
69 title = nil
69 title = nil
70 subject = nil
70 subject = nil
71 if options[:subject] == false
71 if options[:subject] == false
72 title = truncate(issue.subject, :length => 60)
72 title = truncate(issue.subject, :length => 60)
73 else
73 else
74 subject = issue.subject
74 subject = issue.subject
75 if options[:truncate]
75 if options[:truncate]
76 subject = truncate(subject, :length => options[:truncate])
76 subject = truncate(subject, :length => options[:truncate])
77 end
77 end
78 end
78 end
79 s = link_to "#{h(issue.tracker)} ##{issue.id}", {:controller => "issues", :action => "show", :id => issue},
79 s = link_to "#{h(issue.tracker)} ##{issue.id}", {:controller => "issues", :action => "show", :id => issue},
80 :class => issue.css_classes,
80 :class => issue.css_classes,
81 :title => title
81 :title => title
82 s << h(": #{subject}") if subject
82 s << h(": #{subject}") if subject
83 s = h("#{issue.project} - ") + s if options[:project]
83 s = h("#{issue.project} - ") + s if options[:project]
84 s
84 s
85 end
85 end
86
86
87 # Generates a link to an attachment.
87 # Generates a link to an attachment.
88 # Options:
88 # Options:
89 # * :text - Link text (default to attachment filename)
89 # * :text - Link text (default to attachment filename)
90 # * :download - Force download (default: false)
90 # * :download - Force download (default: false)
91 def link_to_attachment(attachment, options={})
91 def link_to_attachment(attachment, options={})
92 text = options.delete(:text) || attachment.filename
92 text = options.delete(:text) || attachment.filename
93 action = options.delete(:download) ? 'download' : 'show'
93 action = options.delete(:download) ? 'download' : 'show'
94 opt_only_path = {}
94 opt_only_path = {}
95 opt_only_path[:only_path] = (options[:only_path] == false ? false : true)
95 opt_only_path[:only_path] = (options[:only_path] == false ? false : true)
96 options.delete(:only_path)
96 options.delete(:only_path)
97 link_to(h(text),
97 link_to(h(text),
98 {:controller => 'attachments', :action => action,
98 {:controller => 'attachments', :action => action,
99 :id => attachment, :filename => attachment.filename}.merge(opt_only_path),
99 :id => attachment, :filename => attachment.filename}.merge(opt_only_path),
100 options)
100 options)
101 end
101 end
102
102
103 # Generates a link to a SCM revision
103 # Generates a link to a SCM revision
104 # Options:
104 # Options:
105 # * :text - Link text (default to the formatted revision)
105 # * :text - Link text (default to the formatted revision)
106 def link_to_revision(revision, repository, options={})
106 def link_to_revision(revision, repository, options={})
107 if repository.is_a?(Project)
107 if repository.is_a?(Project)
108 repository = repository.repository
108 repository = repository.repository
109 end
109 end
110 text = options.delete(:text) || format_revision(revision)
110 text = options.delete(:text) || format_revision(revision)
111 rev = revision.respond_to?(:identifier) ? revision.identifier : revision
111 rev = revision.respond_to?(:identifier) ? revision.identifier : revision
112 link_to(
112 link_to(
113 h(text),
113 h(text),
114 {:controller => 'repositories', :action => 'revision', :id => repository.project, :repository_id => repository.identifier_param, :rev => rev},
114 {:controller => 'repositories', :action => 'revision', :id => repository.project, :repository_id => repository.identifier_param, :rev => rev},
115 :title => l(:label_revision_id, format_revision(revision))
115 :title => l(:label_revision_id, format_revision(revision))
116 )
116 )
117 end
117 end
118
118
119 # Generates a link to a message
119 # Generates a link to a message
120 def link_to_message(message, options={}, html_options = nil)
120 def link_to_message(message, options={}, html_options = nil)
121 link_to(
121 link_to(
122 h(truncate(message.subject, :length => 60)),
122 h(truncate(message.subject, :length => 60)),
123 { :controller => 'messages', :action => 'show',
123 { :controller => 'messages', :action => 'show',
124 :board_id => message.board_id,
124 :board_id => message.board_id,
125 :id => (message.parent_id || message.id),
125 :id => (message.parent_id || message.id),
126 :r => (message.parent_id && message.id),
126 :r => (message.parent_id && message.id),
127 :anchor => (message.parent_id ? "message-#{message.id}" : nil)
127 :anchor => (message.parent_id ? "message-#{message.id}" : nil)
128 }.merge(options),
128 }.merge(options),
129 html_options
129 html_options
130 )
130 )
131 end
131 end
132
132
133 # Generates a link to a project if active
133 # Generates a link to a project if active
134 # Examples:
134 # Examples:
135 #
135 #
136 # link_to_project(project) # => link to the specified project overview
136 # link_to_project(project) # => link to the specified project overview
137 # link_to_project(project, :action=>'settings') # => link to project settings
137 # link_to_project(project, :action=>'settings') # => link to project settings
138 # link_to_project(project, {:only_path => false}, :class => "project") # => 3rd arg adds html options
138 # link_to_project(project, {:only_path => false}, :class => "project") # => 3rd arg adds html options
139 # link_to_project(project, {}, :class => "project") # => html options with default url (project overview)
139 # link_to_project(project, {}, :class => "project") # => html options with default url (project overview)
140 #
140 #
141 def link_to_project(project, options={}, html_options = nil)
141 def link_to_project(project, options={}, html_options = nil)
142 if project.archived?
142 if project.archived?
143 h(project)
143 h(project)
144 else
144 else
145 url = {:controller => 'projects', :action => 'show', :id => project}.merge(options)
145 url = {:controller => 'projects', :action => 'show', :id => project}.merge(options)
146 link_to(h(project), url, html_options)
146 link_to(h(project), url, html_options)
147 end
147 end
148 end
148 end
149
149
150 def thumbnail_tag(attachment)
150 def thumbnail_tag(attachment)
151 link_to image_tag(url_for(:controller => 'attachments', :action => 'thumbnail', :id => attachment)),
151 link_to image_tag(url_for(:controller => 'attachments', :action => 'thumbnail', :id => attachment)),
152 {:controller => 'attachments', :action => 'show', :id => attachment, :filename => attachment.filename},
152 {:controller => 'attachments', :action => 'show', :id => attachment, :filename => attachment.filename},
153 :title => attachment.filename
153 :title => attachment.filename
154 end
154 end
155
155
156 def toggle_link(name, id, options={})
156 def toggle_link(name, id, options={})
157 onclick = "$('##{id}').toggle(); "
157 onclick = "$('##{id}').toggle(); "
158 onclick << (options[:focus] ? "$('##{options[:focus]}').focus(); " : "this.blur(); ")
158 onclick << (options[:focus] ? "$('##{options[:focus]}').focus(); " : "this.blur(); ")
159 onclick << "return false;"
159 onclick << "return false;"
160 link_to(name, "#", :onclick => onclick)
160 link_to(name, "#", :onclick => onclick)
161 end
161 end
162
162
163 def image_to_function(name, function, html_options = {})
163 def image_to_function(name, function, html_options = {})
164 html_options.symbolize_keys!
164 html_options.symbolize_keys!
165 tag(:input, html_options.merge({
165 tag(:input, html_options.merge({
166 :type => "image", :src => image_path(name),
166 :type => "image", :src => image_path(name),
167 :onclick => (html_options[:onclick] ? "#{html_options[:onclick]}; " : "") + "#{function};"
167 :onclick => (html_options[:onclick] ? "#{html_options[:onclick]}; " : "") + "#{function};"
168 }))
168 }))
169 end
169 end
170
170
171 def format_activity_title(text)
171 def format_activity_title(text)
172 h(truncate_single_line(text, :length => 100))
172 h(truncate_single_line(text, :length => 100))
173 end
173 end
174
174
175 def format_activity_day(date)
175 def format_activity_day(date)
176 date == User.current.today ? l(:label_today).titleize : format_date(date)
176 date == User.current.today ? l(:label_today).titleize : format_date(date)
177 end
177 end
178
178
179 def format_activity_description(text)
179 def format_activity_description(text)
180 h(truncate(text.to_s, :length => 120).gsub(%r{[\r\n]*<(pre|code)>.*$}m, '...')
180 h(truncate(text.to_s, :length => 120).gsub(%r{[\r\n]*<(pre|code)>.*$}m, '...')
181 ).gsub(/[\r\n]+/, "<br />").html_safe
181 ).gsub(/[\r\n]+/, "<br />").html_safe
182 end
182 end
183
183
184 def format_version_name(version)
184 def format_version_name(version)
185 if version.project == @project
185 if version.project == @project
186 h(version)
186 h(version)
187 else
187 else
188 h("#{version.project} - #{version}")
188 h("#{version.project} - #{version}")
189 end
189 end
190 end
190 end
191
191
192 def due_date_distance_in_words(date)
192 def due_date_distance_in_words(date)
193 if date
193 if date
194 l((date < Date.today ? :label_roadmap_overdue : :label_roadmap_due_in), distance_of_date_in_words(Date.today, date))
194 l((date < Date.today ? :label_roadmap_overdue : :label_roadmap_due_in), distance_of_date_in_words(Date.today, date))
195 end
195 end
196 end
196 end
197
197
198 def render_page_hierarchy(pages, node=nil, options={})
198 def render_page_hierarchy(pages, node=nil, options={})
199 content = ''
199 content = ''
200 if pages[node]
200 if pages[node]
201 content << "<ul class=\"pages-hierarchy\">\n"
201 content << "<ul class=\"pages-hierarchy\">\n"
202 pages[node].each do |page|
202 pages[node].each do |page|
203 content << "<li>"
203 content << "<li>"
204 content << link_to(h(page.pretty_title), {:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title},
204 content << link_to(h(page.pretty_title), {:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title},
205 :title => (options[:timestamp] && page.updated_on ? l(:label_updated_time, distance_of_time_in_words(Time.now, page.updated_on)) : nil))
205 :title => (options[:timestamp] && page.updated_on ? l(:label_updated_time, distance_of_time_in_words(Time.now, page.updated_on)) : nil))
206 content << "\n" + render_page_hierarchy(pages, page.id, options) if pages[page.id]
206 content << "\n" + render_page_hierarchy(pages, page.id, options) if pages[page.id]
207 content << "</li>\n"
207 content << "</li>\n"
208 end
208 end
209 content << "</ul>\n"
209 content << "</ul>\n"
210 end
210 end
211 content.html_safe
211 content.html_safe
212 end
212 end
213
213
214 # Renders flash messages
214 # Renders flash messages
215 def render_flash_messages
215 def render_flash_messages
216 s = ''
216 s = ''
217 flash.each do |k,v|
217 flash.each do |k,v|
218 s << content_tag('div', v.html_safe, :class => "flash #{k}", :id => "flash_#{k}")
218 s << content_tag('div', v.html_safe, :class => "flash #{k}", :id => "flash_#{k}")
219 end
219 end
220 s.html_safe
220 s.html_safe
221 end
221 end
222
222
223 # Renders tabs and their content
223 # Renders tabs and their content
224 def render_tabs(tabs)
224 def render_tabs(tabs)
225 if tabs.any?
225 if tabs.any?
226 render :partial => 'common/tabs', :locals => {:tabs => tabs}
226 render :partial => 'common/tabs', :locals => {:tabs => tabs}
227 else
227 else
228 content_tag 'p', l(:label_no_data), :class => "nodata"
228 content_tag 'p', l(:label_no_data), :class => "nodata"
229 end
229 end
230 end
230 end
231
231
232 # Renders the project quick-jump box
232 # Renders the project quick-jump box
233 def render_project_jump_box
233 def render_project_jump_box
234 return unless User.current.logged?
234 return unless User.current.logged?
235 projects = User.current.memberships.collect(&:project).compact.select(&:active?).uniq
235 projects = User.current.memberships.collect(&:project).compact.select(&:active?).uniq
236 if projects.any?
236 if projects.any?
237 options =
237 options =
238 ("<option value=''>#{ l(:label_jump_to_a_project) }</option>" +
238 ("<option value=''>#{ l(:label_jump_to_a_project) }</option>" +
239 '<option value="" disabled="disabled">---</option>').html_safe
239 '<option value="" disabled="disabled">---</option>').html_safe
240
240
241 options << project_tree_options_for_select(projects, :selected => @project) do |p|
241 options << project_tree_options_for_select(projects, :selected => @project) do |p|
242 { :value => project_path(:id => p, :jump => current_menu_item) }
242 { :value => project_path(:id => p, :jump => current_menu_item) }
243 end
243 end
244
244
245 select_tag('project_quick_jump_box', options, :onchange => 'if (this.value != \'\') { window.location = this.value; }')
245 select_tag('project_quick_jump_box', options, :onchange => 'if (this.value != \'\') { window.location = this.value; }')
246 end
246 end
247 end
247 end
248
248
249 def project_tree_options_for_select(projects, options = {})
249 def project_tree_options_for_select(projects, options = {})
250 s = ''
250 s = ''
251 project_tree(projects) do |project, level|
251 project_tree(projects) do |project, level|
252 name_prefix = (level > 0 ? '&nbsp;' * 2 * level + '&#187; ' : '').html_safe
252 name_prefix = (level > 0 ? '&nbsp;' * 2 * level + '&#187; ' : '').html_safe
253 tag_options = {:value => project.id}
253 tag_options = {:value => project.id}
254 if project == options[:selected] || (options[:selected].respond_to?(:include?) && options[:selected].include?(project))
254 if project == options[:selected] || (options[:selected].respond_to?(:include?) && options[:selected].include?(project))
255 tag_options[:selected] = 'selected'
255 tag_options[:selected] = 'selected'
256 else
256 else
257 tag_options[:selected] = nil
257 tag_options[:selected] = nil
258 end
258 end
259 tag_options.merge!(yield(project)) if block_given?
259 tag_options.merge!(yield(project)) if block_given?
260 s << content_tag('option', name_prefix + h(project), tag_options)
260 s << content_tag('option', name_prefix + h(project), tag_options)
261 end
261 end
262 s.html_safe
262 s.html_safe
263 end
263 end
264
264
265 # Yields the given block for each project with its level in the tree
265 # Yields the given block for each project with its level in the tree
266 #
266 #
267 # Wrapper for Project#project_tree
267 # Wrapper for Project#project_tree
268 def project_tree(projects, &block)
268 def project_tree(projects, &block)
269 Project.project_tree(projects, &block)
269 Project.project_tree(projects, &block)
270 end
270 end
271
271
272 def project_nested_ul(projects, &block)
272 def project_nested_ul(projects, &block)
273 s = ''
273 s = ''
274 if projects.any?
274 if projects.any?
275 ancestors = []
275 ancestors = []
276 projects.sort_by(&:lft).each do |project|
276 projects.sort_by(&:lft).each do |project|
277 if (ancestors.empty? || project.is_descendant_of?(ancestors.last))
277 if (ancestors.empty? || project.is_descendant_of?(ancestors.last))
278 s << "<ul>\n"
278 s << "<ul>\n"
279 else
279 else
280 ancestors.pop
280 ancestors.pop
281 s << "</li>"
281 s << "</li>"
282 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
282 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
283 ancestors.pop
283 ancestors.pop
284 s << "</ul></li>\n"
284 s << "</ul></li>\n"
285 end
285 end
286 end
286 end
287 s << "<li>"
287 s << "<li>"
288 s << yield(project).to_s
288 s << yield(project).to_s
289 ancestors << project
289 ancestors << project
290 end
290 end
291 s << ("</li></ul>\n" * ancestors.size)
291 s << ("</li></ul>\n" * ancestors.size)
292 end
292 end
293 s.html_safe
293 s.html_safe
294 end
294 end
295
295
296 def principals_check_box_tags(name, principals)
296 def principals_check_box_tags(name, principals)
297 s = ''
297 s = ''
298 principals.sort.each do |principal|
298 principals.sort.each do |principal|
299 s << "<label>#{ check_box_tag name, principal.id, false } #{h principal}</label>\n"
299 s << "<label>#{ check_box_tag name, principal.id, false } #{h principal}</label>\n"
300 end
300 end
301 s.html_safe
301 s.html_safe
302 end
302 end
303
303
304 # Returns a string for users/groups option tags
304 # Returns a string for users/groups option tags
305 def principals_options_for_select(collection, selected=nil)
305 def principals_options_for_select(collection, selected=nil)
306 s = ''
306 s = ''
307 if collection.include?(User.current)
307 if collection.include?(User.current)
308 s << content_tag('option', "<< #{l(:label_me)} >>", :value => User.current.id)
308 s << content_tag('option', "<< #{l(:label_me)} >>", :value => User.current.id)
309 end
309 end
310 groups = ''
310 groups = ''
311 collection.sort.each do |element|
311 collection.sort.each do |element|
312 selected_attribute = ' selected="selected"' if option_value_selected?(element, selected)
312 selected_attribute = ' selected="selected"' if option_value_selected?(element, selected)
313 (element.is_a?(Group) ? groups : s) << %(<option value="#{element.id}"#{selected_attribute}>#{h element.name}</option>)
313 (element.is_a?(Group) ? groups : s) << %(<option value="#{element.id}"#{selected_attribute}>#{h element.name}</option>)
314 end
314 end
315 unless groups.empty?
315 unless groups.empty?
316 s << %(<optgroup label="#{h(l(:label_group_plural))}">#{groups}</optgroup>)
316 s << %(<optgroup label="#{h(l(:label_group_plural))}">#{groups}</optgroup>)
317 end
317 end
318 s.html_safe
318 s.html_safe
319 end
319 end
320
320
321 # Truncates and returns the string as a single line
321 # Truncates and returns the string as a single line
322 def truncate_single_line(string, *args)
322 def truncate_single_line(string, *args)
323 truncate(string.to_s, *args).gsub(%r{[\r\n]+}m, ' ')
323 truncate(string.to_s, *args).gsub(%r{[\r\n]+}m, ' ')
324 end
324 end
325
325
326 # Truncates at line break after 250 characters or options[:length]
326 # Truncates at line break after 250 characters or options[:length]
327 def truncate_lines(string, options={})
327 def truncate_lines(string, options={})
328 length = options[:length] || 250
328 length = options[:length] || 250
329 if string.to_s =~ /\A(.{#{length}}.*?)$/m
329 if string.to_s =~ /\A(.{#{length}}.*?)$/m
330 "#{$1}..."
330 "#{$1}..."
331 else
331 else
332 string
332 string
333 end
333 end
334 end
334 end
335
335
336 def anchor(text)
336 def anchor(text)
337 text.to_s.gsub(' ', '_')
337 text.to_s.gsub(' ', '_')
338 end
338 end
339
339
340 def html_hours(text)
340 def html_hours(text)
341 text.gsub(%r{(\d+)\.(\d+)}, '<span class="hours hours-int">\1</span><span class="hours hours-dec">.\2</span>').html_safe
341 text.gsub(%r{(\d+)\.(\d+)}, '<span class="hours hours-int">\1</span><span class="hours hours-dec">.\2</span>').html_safe
342 end
342 end
343
343
344 def authoring(created, author, options={})
344 def authoring(created, author, options={})
345 l(options[:label] || :label_added_time_by, :author => link_to_user(author), :age => time_tag(created)).html_safe
345 l(options[:label] || :label_added_time_by, :author => link_to_user(author), :age => time_tag(created)).html_safe
346 end
346 end
347
347
348 def time_tag(time)
348 def time_tag(time)
349 text = distance_of_time_in_words(Time.now, time)
349 text = distance_of_time_in_words(Time.now, time)
350 if @project
350 if @project
351 link_to(text, {:controller => 'activities', :action => 'index', :id => @project, :from => User.current.time_to_date(time)}, :title => format_time(time))
351 link_to(text, {:controller => 'activities', :action => 'index', :id => @project, :from => User.current.time_to_date(time)}, :title => format_time(time))
352 else
352 else
353 content_tag('acronym', text, :title => format_time(time))
353 content_tag('acronym', text, :title => format_time(time))
354 end
354 end
355 end
355 end
356
356
357 def syntax_highlight_lines(name, content)
357 def syntax_highlight_lines(name, content)
358 lines = []
358 lines = []
359 syntax_highlight(name, content).each_line { |line| lines << line }
359 syntax_highlight(name, content).each_line { |line| lines << line }
360 lines
360 lines
361 end
361 end
362
362
363 def syntax_highlight(name, content)
363 def syntax_highlight(name, content)
364 Redmine::SyntaxHighlighting.highlight_by_filename(content, name)
364 Redmine::SyntaxHighlighting.highlight_by_filename(content, name)
365 end
365 end
366
366
367 def to_path_param(path)
367 def to_path_param(path)
368 str = path.to_s.split(%r{[/\\]}).select{|p| !p.blank?}.join("/")
368 str = path.to_s.split(%r{[/\\]}).select{|p| !p.blank?}.join("/")
369 str.blank? ? nil : str
369 str.blank? ? nil : str
370 end
370 end
371
371
372 def pagination_links_full(paginator, count=nil, options={})
372 def pagination_links_full(paginator, count=nil, options={})
373 page_param = options.delete(:page_param) || :page
373 page_param = options.delete(:page_param) || :page
374 per_page_links = options.delete(:per_page_links)
374 per_page_links = options.delete(:per_page_links)
375 url_param = params.dup
375 url_param = params.dup
376
376
377 html = ''
377 html = ''
378 if paginator.current.previous
378 if paginator.current.previous
379 # \xc2\xab(utf-8) = &#171;
379 # \xc2\xab(utf-8) = &#171;
380 html << link_to_content_update(
380 html << link_to_content_update(
381 "\xc2\xab " + l(:label_previous),
381 "\xc2\xab " + l(:label_previous),
382 url_param.merge(page_param => paginator.current.previous)) + ' '
382 url_param.merge(page_param => paginator.current.previous)) + ' '
383 end
383 end
384
384
385 html << (pagination_links_each(paginator, options) do |n|
385 html << (pagination_links_each(paginator, options) do |n|
386 link_to_content_update(n.to_s, url_param.merge(page_param => n))
386 link_to_content_update(n.to_s, url_param.merge(page_param => n))
387 end || '')
387 end || '')
388
388
389 if paginator.current.next
389 if paginator.current.next
390 # \xc2\xbb(utf-8) = &#187;
390 # \xc2\xbb(utf-8) = &#187;
391 html << ' ' + link_to_content_update(
391 html << ' ' + link_to_content_update(
392 (l(:label_next) + " \xc2\xbb"),
392 (l(:label_next) + " \xc2\xbb"),
393 url_param.merge(page_param => paginator.current.next))
393 url_param.merge(page_param => paginator.current.next))
394 end
394 end
395
395
396 unless count.nil?
396 unless count.nil?
397 html << " (#{paginator.current.first_item}-#{paginator.current.last_item}/#{count})"
397 html << " (#{paginator.current.first_item}-#{paginator.current.last_item}/#{count})"
398 if per_page_links != false && links = per_page_links(paginator.items_per_page, count)
398 if per_page_links != false && links = per_page_links(paginator.items_per_page, count)
399 html << " | #{links}"
399 html << " | #{links}"
400 end
400 end
401 end
401 end
402
402
403 html.html_safe
403 html.html_safe
404 end
404 end
405
405
406 def per_page_links(selected=nil, item_count=nil)
406 def per_page_links(selected=nil, item_count=nil)
407 values = Setting.per_page_options_array
407 values = Setting.per_page_options_array
408 if item_count && values.any?
408 if item_count && values.any?
409 if item_count > values.first
409 if item_count > values.first
410 max = values.detect {|value| value >= item_count} || item_count
410 max = values.detect {|value| value >= item_count} || item_count
411 else
411 else
412 max = item_count
412 max = item_count
413 end
413 end
414 values = values.select {|value| value <= max || value == selected}
414 values = values.select {|value| value <= max || value == selected}
415 end
415 end
416 if values.empty? || (values.size == 1 && values.first == selected)
416 if values.empty? || (values.size == 1 && values.first == selected)
417 return nil
417 return nil
418 end
418 end
419 links = values.collect do |n|
419 links = values.collect do |n|
420 n == selected ? n : link_to_content_update(n, params.merge(:per_page => n))
420 n == selected ? n : link_to_content_update(n, params.merge(:per_page => n))
421 end
421 end
422 l(:label_display_per_page, links.join(', '))
422 l(:label_display_per_page, links.join(', '))
423 end
423 end
424
424
425 def reorder_links(name, url, method = :post)
425 def reorder_links(name, url, method = :post)
426 link_to(image_tag('2uparrow.png', :alt => l(:label_sort_highest)),
426 link_to(image_tag('2uparrow.png', :alt => l(:label_sort_highest)),
427 url.merge({"#{name}[move_to]" => 'highest'}),
427 url.merge({"#{name}[move_to]" => 'highest'}),
428 :method => method, :title => l(:label_sort_highest)) +
428 :method => method, :title => l(:label_sort_highest)) +
429 link_to(image_tag('1uparrow.png', :alt => l(:label_sort_higher)),
429 link_to(image_tag('1uparrow.png', :alt => l(:label_sort_higher)),
430 url.merge({"#{name}[move_to]" => 'higher'}),
430 url.merge({"#{name}[move_to]" => 'higher'}),
431 :method => method, :title => l(:label_sort_higher)) +
431 :method => method, :title => l(:label_sort_higher)) +
432 link_to(image_tag('1downarrow.png', :alt => l(:label_sort_lower)),
432 link_to(image_tag('1downarrow.png', :alt => l(:label_sort_lower)),
433 url.merge({"#{name}[move_to]" => 'lower'}),
433 url.merge({"#{name}[move_to]" => 'lower'}),
434 :method => method, :title => l(:label_sort_lower)) +
434 :method => method, :title => l(:label_sort_lower)) +
435 link_to(image_tag('2downarrow.png', :alt => l(:label_sort_lowest)),
435 link_to(image_tag('2downarrow.png', :alt => l(:label_sort_lowest)),
436 url.merge({"#{name}[move_to]" => 'lowest'}),
436 url.merge({"#{name}[move_to]" => 'lowest'}),
437 :method => method, :title => l(:label_sort_lowest))
437 :method => method, :title => l(:label_sort_lowest))
438 end
438 end
439
439
440 def breadcrumb(*args)
440 def breadcrumb(*args)
441 elements = args.flatten
441 elements = args.flatten
442 elements.any? ? content_tag('p', (args.join(" \xc2\xbb ") + " \xc2\xbb ").html_safe, :class => 'breadcrumb') : nil
442 elements.any? ? content_tag('p', (args.join(" \xc2\xbb ") + " \xc2\xbb ").html_safe, :class => 'breadcrumb') : nil
443 end
443 end
444
444
445 def other_formats_links(&block)
445 def other_formats_links(&block)
446 concat('<p class="other-formats">'.html_safe + l(:label_export_to))
446 concat('<p class="other-formats">'.html_safe + l(:label_export_to))
447 yield Redmine::Views::OtherFormatsBuilder.new(self)
447 yield Redmine::Views::OtherFormatsBuilder.new(self)
448 concat('</p>'.html_safe)
448 concat('</p>'.html_safe)
449 end
449 end
450
450
451 def page_header_title
451 def page_header_title
452 if @project.nil? || @project.new_record?
452 if @project.nil? || @project.new_record?
453 h(Setting.app_title)
453 h(Setting.app_title)
454 else
454 else
455 b = []
455 b = []
456 ancestors = (@project.root? ? [] : @project.ancestors.visible.all)
456 ancestors = (@project.root? ? [] : @project.ancestors.visible.all)
457 if ancestors.any?
457 if ancestors.any?
458 root = ancestors.shift
458 root = ancestors.shift
459 b << link_to_project(root, {:jump => current_menu_item}, :class => 'root')
459 b << link_to_project(root, {:jump => current_menu_item}, :class => 'root')
460 if ancestors.size > 2
460 if ancestors.size > 2
461 b << "\xe2\x80\xa6"
461 b << "\xe2\x80\xa6"
462 ancestors = ancestors[-2, 2]
462 ancestors = ancestors[-2, 2]
463 end
463 end
464 b += ancestors.collect {|p| link_to_project(p, {:jump => current_menu_item}, :class => 'ancestor') }
464 b += ancestors.collect {|p| link_to_project(p, {:jump => current_menu_item}, :class => 'ancestor') }
465 end
465 end
466 b << h(@project)
466 b << h(@project)
467 b.join(" \xc2\xbb ").html_safe
467 b.join(" \xc2\xbb ").html_safe
468 end
468 end
469 end
469 end
470
470
471 def html_title(*args)
471 def html_title(*args)
472 if args.empty?
472 if args.empty?
473 title = @html_title || []
473 title = @html_title || []
474 title << @project.name if @project
474 title << @project.name if @project
475 title << Setting.app_title unless Setting.app_title == title.last
475 title << Setting.app_title unless Setting.app_title == title.last
476 title.select {|t| !t.blank? }.join(' - ')
476 title.select {|t| !t.blank? }.join(' - ')
477 else
477 else
478 @html_title ||= []
478 @html_title ||= []
479 @html_title += args
479 @html_title += args
480 end
480 end
481 end
481 end
482
482
483 # Returns the theme, controller name, and action as css classes for the
483 # Returns the theme, controller name, and action as css classes for the
484 # HTML body.
484 # HTML body.
485 def body_css_classes
485 def body_css_classes
486 css = []
486 css = []
487 if theme = Redmine::Themes.theme(Setting.ui_theme)
487 if theme = Redmine::Themes.theme(Setting.ui_theme)
488 css << 'theme-' + theme.name
488 css << 'theme-' + theme.name
489 end
489 end
490
490
491 css << 'controller-' + controller_name
491 css << 'controller-' + controller_name
492 css << 'action-' + action_name
492 css << 'action-' + action_name
493 css.join(' ')
493 css.join(' ')
494 end
494 end
495
495
496 def accesskey(s)
496 def accesskey(s)
497 Redmine::AccessKeys.key_for s
497 Redmine::AccessKeys.key_for s
498 end
498 end
499
499
500 # Formats text according to system settings.
500 # Formats text according to system settings.
501 # 2 ways to call this method:
501 # 2 ways to call this method:
502 # * with a String: textilizable(text, options)
502 # * with a String: textilizable(text, options)
503 # * with an object and one of its attribute: textilizable(issue, :description, options)
503 # * with an object and one of its attribute: textilizable(issue, :description, options)
504 def textilizable(*args)
504 def textilizable(*args)
505 options = args.last.is_a?(Hash) ? args.pop : {}
505 options = args.last.is_a?(Hash) ? args.pop : {}
506 case args.size
506 case args.size
507 when 1
507 when 1
508 obj = options[:object]
508 obj = options[:object]
509 text = args.shift
509 text = args.shift
510 when 2
510 when 2
511 obj = args.shift
511 obj = args.shift
512 attr = args.shift
512 attr = args.shift
513 text = obj.send(attr).to_s
513 text = obj.send(attr).to_s
514 else
514 else
515 raise ArgumentError, 'invalid arguments to textilizable'
515 raise ArgumentError, 'invalid arguments to textilizable'
516 end
516 end
517 return '' if text.blank?
517 return '' if text.blank?
518 project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
518 project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
519 only_path = options.delete(:only_path) == false ? false : true
519 only_path = options.delete(:only_path) == false ? false : true
520
520
521 text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text, :object => obj, :attribute => attr)
521 text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text, :object => obj, :attribute => attr)
522
522
523 @parsed_headings = []
523 @parsed_headings = []
524 @heading_anchors = {}
524 @heading_anchors = {}
525 @current_section = 0 if options[:edit_section_links]
525 @current_section = 0 if options[:edit_section_links]
526
526
527 parse_sections(text, project, obj, attr, only_path, options)
527 parse_sections(text, project, obj, attr, only_path, options)
528 text = parse_non_pre_blocks(text) do |text|
528 text = parse_non_pre_blocks(text) do |text|
529 [:parse_inline_attachments, :parse_wiki_links, :parse_redmine_links, :parse_macros].each do |method_name|
529 [:parse_inline_attachments, :parse_wiki_links, :parse_redmine_links, :parse_macros].each do |method_name|
530 send method_name, text, project, obj, attr, only_path, options
530 send method_name, text, project, obj, attr, only_path, options
531 end
531 end
532 end
532 end
533 parse_headings(text, project, obj, attr, only_path, options)
533 parse_headings(text, project, obj, attr, only_path, options)
534
534
535 if @parsed_headings.any?
535 if @parsed_headings.any?
536 replace_toc(text, @parsed_headings)
536 replace_toc(text, @parsed_headings)
537 end
537 end
538
538
539 text.html_safe
539 text.html_safe
540 end
540 end
541
541
542 def parse_non_pre_blocks(text)
542 def parse_non_pre_blocks(text)
543 s = StringScanner.new(text)
543 s = StringScanner.new(text)
544 tags = []
544 tags = []
545 parsed = ''
545 parsed = ''
546 while !s.eos?
546 while !s.eos?
547 s.scan(/(.*?)(<(\/)?(pre|code)(.*?)>|\z)/im)
547 s.scan(/(.*?)(<(\/)?(pre|code)(.*?)>|\z)/im)
548 text, full_tag, closing, tag = s[1], s[2], s[3], s[4]
548 text, full_tag, closing, tag = s[1], s[2], s[3], s[4]
549 if tags.empty?
549 if tags.empty?
550 yield text
550 yield text
551 end
551 end
552 parsed << text
552 parsed << text
553 if tag
553 if tag
554 if closing
554 if closing
555 if tags.last == tag.downcase
555 if tags.last == tag.downcase
556 tags.pop
556 tags.pop
557 end
557 end
558 else
558 else
559 tags << tag.downcase
559 tags << tag.downcase
560 end
560 end
561 parsed << full_tag
561 parsed << full_tag
562 end
562 end
563 end
563 end
564 # Close any non closing tags
564 # Close any non closing tags
565 while tag = tags.pop
565 while tag = tags.pop
566 parsed << "</#{tag}>"
566 parsed << "</#{tag}>"
567 end
567 end
568 parsed
568 parsed
569 end
569 end
570
570
571 def parse_inline_attachments(text, project, obj, attr, only_path, options)
571 def parse_inline_attachments(text, project, obj, attr, only_path, options)
572 # when using an image link, try to use an attachment, if possible
572 # when using an image link, try to use an attachment, if possible
573 if options[:attachments] || (obj && obj.respond_to?(:attachments))
573 if options[:attachments] || (obj && obj.respond_to?(:attachments))
574 attachments = options[:attachments] || obj.attachments
574 attachments = options[:attachments] || obj.attachments
575 text.gsub!(/src="([^\/"]+\.(bmp|gif|jpg|jpe|jpeg|png))"(\s+alt="([^"]*)")?/i) do |m|
575 text.gsub!(/src="([^\/"]+\.(bmp|gif|jpg|jpe|jpeg|png))"(\s+alt="([^"]*)")?/i) do |m|
576 filename, ext, alt, alttext = $1.downcase, $2, $3, $4
576 filename, ext, alt, alttext = $1.downcase, $2, $3, $4
577 # search for the picture in attachments
577 # search for the picture in attachments
578 if found = Attachment.latest_attach(attachments, filename)
578 if found = Attachment.latest_attach(attachments, filename)
579 image_url = url_for :only_path => only_path, :controller => 'attachments',
579 image_url = url_for :only_path => only_path, :controller => 'attachments',
580 :action => 'download', :id => found
580 :action => 'download', :id => found
581 desc = found.description.to_s.gsub('"', '')
581 desc = found.description.to_s.gsub('"', '')
582 if !desc.blank? && alttext.blank?
582 if !desc.blank? && alttext.blank?
583 alt = " title=\"#{desc}\" alt=\"#{desc}\""
583 alt = " title=\"#{desc}\" alt=\"#{desc}\""
584 end
584 end
585 "src=\"#{image_url}\"#{alt}"
585 "src=\"#{image_url}\"#{alt}"
586 else
586 else
587 m
587 m
588 end
588 end
589 end
589 end
590 end
590 end
591 end
591 end
592
592
593 # Wiki links
593 # Wiki links
594 #
594 #
595 # Examples:
595 # Examples:
596 # [[mypage]]
596 # [[mypage]]
597 # [[mypage|mytext]]
597 # [[mypage|mytext]]
598 # wiki links can refer other project wikis, using project name or identifier:
598 # wiki links can refer other project wikis, using project name or identifier:
599 # [[project:]] -> wiki starting page
599 # [[project:]] -> wiki starting page
600 # [[project:|mytext]]
600 # [[project:|mytext]]
601 # [[project:mypage]]
601 # [[project:mypage]]
602 # [[project:mypage|mytext]]
602 # [[project:mypage|mytext]]
603 def parse_wiki_links(text, project, obj, attr, only_path, options)
603 def parse_wiki_links(text, project, obj, attr, only_path, options)
604 text.gsub!(/(!)?(\[\[([^\]\n\|]+)(\|([^\]\n\|]+))?\]\])/) do |m|
604 text.gsub!(/(!)?(\[\[([^\]\n\|]+)(\|([^\]\n\|]+))?\]\])/) do |m|
605 link_project = project
605 link_project = project
606 esc, all, page, title = $1, $2, $3, $5
606 esc, all, page, title = $1, $2, $3, $5
607 if esc.nil?
607 if esc.nil?
608 if page =~ /^([^\:]+)\:(.*)$/
608 if page =~ /^([^\:]+)\:(.*)$/
609 link_project = Project.find_by_identifier($1) || Project.find_by_name($1)
609 link_project = Project.find_by_identifier($1) || Project.find_by_name($1)
610 page = $2
610 page = $2
611 title ||= $1 if page.blank?
611 title ||= $1 if page.blank?
612 end
612 end
613
613
614 if link_project && link_project.wiki
614 if link_project && link_project.wiki
615 # extract anchor
615 # extract anchor
616 anchor = nil
616 anchor = nil
617 if page =~ /^(.+?)\#(.+)$/
617 if page =~ /^(.+?)\#(.+)$/
618 page, anchor = $1, $2
618 page, anchor = $1, $2
619 end
619 end
620 anchor = sanitize_anchor_name(anchor) if anchor.present?
620 anchor = sanitize_anchor_name(anchor) if anchor.present?
621 # check if page exists
621 # check if page exists
622 wiki_page = link_project.wiki.find_page(page)
622 wiki_page = link_project.wiki.find_page(page)
623 url = if anchor.present? && wiki_page.present? && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version)) && obj.page == wiki_page
623 url = if anchor.present? && wiki_page.present? && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version)) && obj.page == wiki_page
624 "##{anchor}"
624 "##{anchor}"
625 else
625 else
626 case options[:wiki_links]
626 case options[:wiki_links]
627 when :local; "#{page.present? ? Wiki.titleize(page) : ''}.html" + (anchor.present? ? "##{anchor}" : '')
627 when :local; "#{page.present? ? Wiki.titleize(page) : ''}.html" + (anchor.present? ? "##{anchor}" : '')
628 when :anchor; "##{page.present? ? Wiki.titleize(page) : title}" + (anchor.present? ? "_#{anchor}" : '') # used for single-file wiki export
628 when :anchor; "##{page.present? ? Wiki.titleize(page) : title}" + (anchor.present? ? "_#{anchor}" : '') # used for single-file wiki export
629 else
629 else
630 wiki_page_id = page.present? ? Wiki.titleize(page) : nil
630 wiki_page_id = page.present? ? Wiki.titleize(page) : nil
631 parent = wiki_page.nil? && obj.is_a?(WikiContent) && obj.page && project == link_project ? obj.page.title : nil
631 parent = wiki_page.nil? && obj.is_a?(WikiContent) && obj.page && project == link_project ? obj.page.title : nil
632 url_for(:only_path => only_path, :controller => 'wiki', :action => 'show', :project_id => link_project,
632 url_for(:only_path => only_path, :controller => 'wiki', :action => 'show', :project_id => link_project,
633 :id => wiki_page_id, :anchor => anchor, :parent => parent)
633 :id => wiki_page_id, :anchor => anchor, :parent => parent)
634 end
634 end
635 end
635 end
636 link_to(title.present? ? title.html_safe : h(page), url, :class => ('wiki-page' + (wiki_page ? '' : ' new')))
636 link_to(title.present? ? title.html_safe : h(page), url, :class => ('wiki-page' + (wiki_page ? '' : ' new')))
637 else
637 else
638 # project or wiki doesn't exist
638 # project or wiki doesn't exist
639 all
639 all
640 end
640 end
641 else
641 else
642 all
642 all
643 end
643 end
644 end
644 end
645 end
645 end
646
646
647 # Redmine links
647 # Redmine links
648 #
648 #
649 # Examples:
649 # Examples:
650 # Issues:
650 # Issues:
651 # #52 -> Link to issue #52
651 # #52 -> Link to issue #52
652 # Changesets:
652 # Changesets:
653 # r52 -> Link to revision 52
653 # r52 -> Link to revision 52
654 # commit:a85130f -> Link to scmid starting with a85130f
654 # commit:a85130f -> Link to scmid starting with a85130f
655 # Documents:
655 # Documents:
656 # document#17 -> Link to document with id 17
656 # document#17 -> Link to document with id 17
657 # document:Greetings -> Link to the document with title "Greetings"
657 # document:Greetings -> Link to the document with title "Greetings"
658 # document:"Some document" -> Link to the document with title "Some document"
658 # document:"Some document" -> Link to the document with title "Some document"
659 # Versions:
659 # Versions:
660 # version#3 -> Link to version with id 3
660 # version#3 -> Link to version with id 3
661 # version:1.0.0 -> Link to version named "1.0.0"
661 # version:1.0.0 -> Link to version named "1.0.0"
662 # version:"1.0 beta 2" -> Link to version named "1.0 beta 2"
662 # version:"1.0 beta 2" -> Link to version named "1.0 beta 2"
663 # Attachments:
663 # Attachments:
664 # attachment:file.zip -> Link to the attachment of the current object named file.zip
664 # attachment:file.zip -> Link to the attachment of the current object named file.zip
665 # Source files:
665 # Source files:
666 # source:some/file -> Link to the file located at /some/file in the project's repository
666 # source:some/file -> Link to the file located at /some/file in the project's repository
667 # source:some/file@52 -> Link to the file's revision 52
667 # source:some/file@52 -> Link to the file's revision 52
668 # source:some/file#L120 -> Link to line 120 of the file
668 # source:some/file#L120 -> Link to line 120 of the file
669 # source:some/file@52#L120 -> Link to line 120 of the file's revision 52
669 # source:some/file@52#L120 -> Link to line 120 of the file's revision 52
670 # export:some/file -> Force the download of the file
670 # export:some/file -> Force the download of the file
671 # Forum messages:
671 # Forum messages:
672 # message#1218 -> Link to message with id 1218
672 # message#1218 -> Link to message with id 1218
673 #
673 #
674 # Links can refer other objects from other projects, using project identifier:
674 # Links can refer other objects from other projects, using project identifier:
675 # identifier:r52
675 # identifier:r52
676 # identifier:document:"Some document"
676 # identifier:document:"Some document"
677 # identifier:version:1.0.0
677 # identifier:version:1.0.0
678 # identifier:source:some/file
678 # identifier:source:some/file
679 def parse_redmine_links(text, project, obj, attr, only_path, options)
679 def parse_redmine_links(text, project, obj, attr, only_path, options)
680 text.gsub!(%r{([\s\(,\-\[\>]|^)(!)?(([a-z0-9\-_]+):)?(attachment|document|version|forum|news|message|project|commit|source|export)?(((#)|((([a-z0-9\-]+)\|)?(r)))((\d+)((#note)?-(\d+))?)|(:)([^"\s<>][^\s<>]*?|"[^"]+?"))(?=(?=[[:punct:]][^A-Za-z0-9_/])|,|\s|\]|<|$)}) do |m|
680 text.gsub!(%r{([\s\(,\-\[\>]|^)(!)?(([a-z0-9\-_]+):)?(attachment|document|version|forum|news|message|project|commit|source|export)?(((#)|((([a-z0-9\-]+)\|)?(r)))((\d+)((#note)?-(\d+))?)|(:)([^"\s<>][^\s<>]*?|"[^"]+?"))(?=(?=[[:punct:]][^A-Za-z0-9_/])|,|\s|\]|<|$)}) do |m|
681 leading, esc, project_prefix, project_identifier, prefix, repo_prefix, repo_identifier, sep, identifier, comment_suffix, comment_id = $1, $2, $3, $4, $5, $10, $11, $8 || $12 || $18, $14 || $19, $15, $17
681 leading, esc, project_prefix, project_identifier, prefix, repo_prefix, repo_identifier, sep, identifier, comment_suffix, comment_id = $1, $2, $3, $4, $5, $10, $11, $8 || $12 || $18, $14 || $19, $15, $17
682 link = nil
682 link = nil
683 if project_identifier
683 if project_identifier
684 project = Project.visible.find_by_identifier(project_identifier)
684 project = Project.visible.find_by_identifier(project_identifier)
685 end
685 end
686 if esc.nil?
686 if esc.nil?
687 if prefix.nil? && sep == 'r'
687 if prefix.nil? && sep == 'r'
688 if project
688 if project
689 repository = nil
689 repository = nil
690 if repo_identifier
690 if repo_identifier
691 repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
691 repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
692 else
692 else
693 repository = project.repository
693 repository = project.repository
694 end
694 end
695 # project.changesets.visible raises an SQL error because of a double join on repositories
695 # project.changesets.visible raises an SQL error because of a double join on repositories
696 if repository && (changeset = Changeset.visible.find_by_repository_id_and_revision(repository.id, identifier))
696 if repository && (changeset = Changeset.visible.find_by_repository_id_and_revision(repository.id, identifier))
697 link = link_to(h("#{project_prefix}#{repo_prefix}r#{identifier}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :repository_id => repository.identifier_param, :rev => changeset.revision},
697 link = link_to(h("#{project_prefix}#{repo_prefix}r#{identifier}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :repository_id => repository.identifier_param, :rev => changeset.revision},
698 :class => 'changeset',
698 :class => 'changeset',
699 :title => truncate_single_line(changeset.comments, :length => 100))
699 :title => truncate_single_line(changeset.comments, :length => 100))
700 end
700 end
701 end
701 end
702 elsif sep == '#'
702 elsif sep == '#'
703 oid = identifier.to_i
703 oid = identifier.to_i
704 case prefix
704 case prefix
705 when nil
705 when nil
706 if oid.to_s == identifier && issue = Issue.visible.find_by_id(oid, :include => :status)
706 if oid.to_s == identifier && issue = Issue.visible.find_by_id(oid, :include => :status)
707 anchor = comment_id ? "note-#{comment_id}" : nil
707 anchor = comment_id ? "note-#{comment_id}" : nil
708 link = link_to("##{oid}", {:only_path => only_path, :controller => 'issues', :action => 'show', :id => oid, :anchor => anchor},
708 link = link_to("##{oid}", {:only_path => only_path, :controller => 'issues', :action => 'show', :id => oid, :anchor => anchor},
709 :class => issue.css_classes,
709 :class => issue.css_classes,
710 :title => "#{truncate(issue.subject, :length => 100)} (#{issue.status.name})")
710 :title => "#{truncate(issue.subject, :length => 100)} (#{issue.status.name})")
711 end
711 end
712 when 'document'
712 when 'document'
713 if document = Document.visible.find_by_id(oid)
713 if document = Document.visible.find_by_id(oid)
714 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
714 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
715 :class => 'document'
715 :class => 'document'
716 end
716 end
717 when 'version'
717 when 'version'
718 if version = Version.visible.find_by_id(oid)
718 if version = Version.visible.find_by_id(oid)
719 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
719 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
720 :class => 'version'
720 :class => 'version'
721 end
721 end
722 when 'message'
722 when 'message'
723 if message = Message.visible.find_by_id(oid, :include => :parent)
723 if message = Message.visible.find_by_id(oid, :include => :parent)
724 link = link_to_message(message, {:only_path => only_path}, :class => 'message')
724 link = link_to_message(message, {:only_path => only_path}, :class => 'message')
725 end
725 end
726 when 'forum'
726 when 'forum'
727 if board = Board.visible.find_by_id(oid)
727 if board = Board.visible.find_by_id(oid)
728 link = link_to h(board.name), {:only_path => only_path, :controller => 'boards', :action => 'show', :id => board, :project_id => board.project},
728 link = link_to h(board.name), {:only_path => only_path, :controller => 'boards', :action => 'show', :id => board, :project_id => board.project},
729 :class => 'board'
729 :class => 'board'
730 end
730 end
731 when 'news'
731 when 'news'
732 if news = News.visible.find_by_id(oid)
732 if news = News.visible.find_by_id(oid)
733 link = link_to h(news.title), {:only_path => only_path, :controller => 'news', :action => 'show', :id => news},
733 link = link_to h(news.title), {:only_path => only_path, :controller => 'news', :action => 'show', :id => news},
734 :class => 'news'
734 :class => 'news'
735 end
735 end
736 when 'project'
736 when 'project'
737 if p = Project.visible.find_by_id(oid)
737 if p = Project.visible.find_by_id(oid)
738 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
738 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
739 end
739 end
740 end
740 end
741 elsif sep == ':'
741 elsif sep == ':'
742 # removes the double quotes if any
742 # removes the double quotes if any
743 name = identifier.gsub(%r{^"(.*)"$}, "\\1")
743 name = identifier.gsub(%r{^"(.*)"$}, "\\1")
744 case prefix
744 case prefix
745 when 'document'
745 when 'document'
746 if project && document = project.documents.visible.find_by_title(name)
746 if project && document = project.documents.visible.find_by_title(name)
747 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
747 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
748 :class => 'document'
748 :class => 'document'
749 end
749 end
750 when 'version'
750 when 'version'
751 if project && version = project.versions.visible.find_by_name(name)
751 if project && version = project.versions.visible.find_by_name(name)
752 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
752 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
753 :class => 'version'
753 :class => 'version'
754 end
754 end
755 when 'forum'
755 when 'forum'
756 if project && board = project.boards.visible.find_by_name(name)
756 if project && board = project.boards.visible.find_by_name(name)
757 link = link_to h(board.name), {:only_path => only_path, :controller => 'boards', :action => 'show', :id => board, :project_id => board.project},
757 link = link_to h(board.name), {:only_path => only_path, :controller => 'boards', :action => 'show', :id => board, :project_id => board.project},
758 :class => 'board'
758 :class => 'board'
759 end
759 end
760 when 'news'
760 when 'news'
761 if project && news = project.news.visible.find_by_title(name)
761 if project && news = project.news.visible.find_by_title(name)
762 link = link_to h(news.title), {:only_path => only_path, :controller => 'news', :action => 'show', :id => news},
762 link = link_to h(news.title), {:only_path => only_path, :controller => 'news', :action => 'show', :id => news},
763 :class => 'news'
763 :class => 'news'
764 end
764 end
765 when 'commit', 'source', 'export'
765 when 'commit', 'source', 'export'
766 if project
766 if project
767 repository = nil
767 repository = nil
768 if name =~ %r{^(([a-z0-9\-]+)\|)(.+)$}
768 if name =~ %r{^(([a-z0-9\-]+)\|)(.+)$}
769 repo_prefix, repo_identifier, name = $1, $2, $3
769 repo_prefix, repo_identifier, name = $1, $2, $3
770 repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
770 repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
771 else
771 else
772 repository = project.repository
772 repository = project.repository
773 end
773 end
774 if prefix == 'commit'
774 if prefix == 'commit'
775 if repository && (changeset = Changeset.visible.find(:first, :conditions => ["repository_id = ? AND scmid LIKE ?", repository.id, "#{name}%"]))
775 if repository && (changeset = Changeset.visible.find(:first, :conditions => ["repository_id = ? AND scmid LIKE ?", repository.id, "#{name}%"]))
776 link = link_to h("#{project_prefix}#{repo_prefix}#{name}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :repository_id => repository.identifier_param, :rev => changeset.identifier},
776 link = link_to h("#{project_prefix}#{repo_prefix}#{name}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :repository_id => repository.identifier_param, :rev => changeset.identifier},
777 :class => 'changeset',
777 :class => 'changeset',
778 :title => truncate_single_line(h(changeset.comments), :length => 100)
778 :title => truncate_single_line(h(changeset.comments), :length => 100)
779 end
779 end
780 else
780 else
781 if repository && User.current.allowed_to?(:browse_repository, project)
781 if repository && User.current.allowed_to?(:browse_repository, project)
782 name =~ %r{^[/\\]*(.*?)(@([0-9a-f]+))?(#(L\d+))?$}
782 name =~ %r{^[/\\]*(.*?)(@([0-9a-f]+))?(#(L\d+))?$}
783 path, rev, anchor = $1, $3, $5
783 path, rev, anchor = $1, $3, $5
784 link = link_to h("#{project_prefix}#{prefix}:#{repo_prefix}#{name}"), {:controller => 'repositories', :action => 'entry', :id => project, :repository_id => repository.identifier_param,
784 link = link_to h("#{project_prefix}#{prefix}:#{repo_prefix}#{name}"), {:controller => 'repositories', :action => 'entry', :id => project, :repository_id => repository.identifier_param,
785 :path => to_path_param(path),
785 :path => to_path_param(path),
786 :rev => rev,
786 :rev => rev,
787 :anchor => anchor,
787 :anchor => anchor,
788 :format => (prefix == 'export' ? 'raw' : nil)},
788 :format => (prefix == 'export' ? 'raw' : nil)},
789 :class => (prefix == 'export' ? 'source download' : 'source')
789 :class => (prefix == 'export' ? 'source download' : 'source')
790 end
790 end
791 end
791 end
792 repo_prefix = nil
792 repo_prefix = nil
793 end
793 end
794 when 'attachment'
794 when 'attachment'
795 attachments = options[:attachments] || (obj && obj.respond_to?(:attachments) ? obj.attachments : nil)
795 attachments = options[:attachments] || (obj && obj.respond_to?(:attachments) ? obj.attachments : nil)
796 if attachments && attachment = attachments.detect {|a| a.filename == name }
796 if attachments && attachment = attachments.detect {|a| a.filename == name }
797 link = link_to h(attachment.filename), {:only_path => only_path, :controller => 'attachments', :action => 'download', :id => attachment},
797 link = link_to h(attachment.filename), {:only_path => only_path, :controller => 'attachments', :action => 'download', :id => attachment},
798 :class => 'attachment'
798 :class => 'attachment'
799 end
799 end
800 when 'project'
800 when 'project'
801 if p = Project.visible.find(:first, :conditions => ["identifier = :s OR LOWER(name) = :s", {:s => name.downcase}])
801 if p = Project.visible.find(:first, :conditions => ["identifier = :s OR LOWER(name) = :s", {:s => name.downcase}])
802 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
802 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
803 end
803 end
804 end
804 end
805 end
805 end
806 end
806 end
807 (leading + (link || "#{project_prefix}#{prefix}#{repo_prefix}#{sep}#{identifier}#{comment_suffix}"))
807 (leading + (link || "#{project_prefix}#{prefix}#{repo_prefix}#{sep}#{identifier}#{comment_suffix}"))
808 end
808 end
809 end
809 end
810
810
811 HEADING_RE = /(<h(1|2|3|4)( [^>]+)?>(.+?)<\/h(1|2|3|4)>)/i unless const_defined?(:HEADING_RE)
811 HEADING_RE = /(<h(1|2|3|4)( [^>]+)?>(.+?)<\/h(1|2|3|4)>)/i unless const_defined?(:HEADING_RE)
812
812
813 def parse_sections(text, project, obj, attr, only_path, options)
813 def parse_sections(text, project, obj, attr, only_path, options)
814 return unless options[:edit_section_links]
814 return unless options[:edit_section_links]
815 text.gsub!(HEADING_RE) do
815 text.gsub!(HEADING_RE) do
816 heading = $1
816 heading = $1
817 @current_section += 1
817 @current_section += 1
818 if @current_section > 1
818 if @current_section > 1
819 content_tag('div',
819 content_tag('div',
820 link_to(image_tag('edit.png'), options[:edit_section_links].merge(:section => @current_section)),
820 link_to(image_tag('edit.png'), options[:edit_section_links].merge(:section => @current_section)),
821 :class => 'contextual',
821 :class => 'contextual',
822 :title => l(:button_edit_section)) + heading.html_safe
822 :title => l(:button_edit_section)) + heading.html_safe
823 else
823 else
824 heading
824 heading
825 end
825 end
826 end
826 end
827 end
827 end
828
828
829 # Headings and TOC
829 # Headings and TOC
830 # Adds ids and links to headings unless options[:headings] is set to false
830 # Adds ids and links to headings unless options[:headings] is set to false
831 def parse_headings(text, project, obj, attr, only_path, options)
831 def parse_headings(text, project, obj, attr, only_path, options)
832 return if options[:headings] == false
832 return if options[:headings] == false
833
833
834 text.gsub!(HEADING_RE) do
834 text.gsub!(HEADING_RE) do
835 level, attrs, content = $2.to_i, $3, $4
835 level, attrs, content = $2.to_i, $3, $4
836 item = strip_tags(content).strip
836 item = strip_tags(content).strip
837 anchor = sanitize_anchor_name(item)
837 anchor = sanitize_anchor_name(item)
838 # used for single-file wiki export
838 # used for single-file wiki export
839 anchor = "#{obj.page.title}_#{anchor}" if options[:wiki_links] == :anchor && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version))
839 anchor = "#{obj.page.title}_#{anchor}" if options[:wiki_links] == :anchor && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version))
840 @heading_anchors[anchor] ||= 0
840 @heading_anchors[anchor] ||= 0
841 idx = (@heading_anchors[anchor] += 1)
841 idx = (@heading_anchors[anchor] += 1)
842 if idx > 1
842 if idx > 1
843 anchor = "#{anchor}-#{idx}"
843 anchor = "#{anchor}-#{idx}"
844 end
844 end
845 @parsed_headings << [level, anchor, item]
845 @parsed_headings << [level, anchor, item]
846 "<a name=\"#{anchor}\"></a>\n<h#{level} #{attrs}>#{content}<a href=\"##{anchor}\" class=\"wiki-anchor\">&para;</a></h#{level}>"
846 "<a name=\"#{anchor}\"></a>\n<h#{level} #{attrs}>#{content}<a href=\"##{anchor}\" class=\"wiki-anchor\">&para;</a></h#{level}>"
847 end
847 end
848 end
848 end
849
849
850 MACROS_RE = /
850 MACROS_RE = /
851 (!)? # escaping
851 (!)? # escaping
852 (
852 (
853 \{\{ # opening tag
853 \{\{ # opening tag
854 ([\w]+) # macro name
854 ([\w]+) # macro name
855 (\(([^\}]*)\))? # optional arguments
855 (\((.*?)\))? # optional arguments
856 \}\} # closing tag
856 \}\} # closing tag
857 )
857 )
858 /x unless const_defined?(:MACROS_RE)
858 /x unless const_defined?(:MACROS_RE)
859
859
860 # Macros substitution
860 # Macros substitution
861 def parse_macros(text, project, obj, attr, only_path, options)
861 def parse_macros(text, project, obj, attr, only_path, options)
862 text.gsub!(MACROS_RE) do
862 text.gsub!(MACROS_RE) do
863 esc, all, macro, args = $1, $2, $3.downcase, $5.to_s
863 esc, all, macro, args = $1, $2, $3.downcase, $5.to_s
864 if esc.nil?
864 if esc.nil?
865 begin
865 begin
866 exec_macro(macro, obj, args)
866 exec_macro(macro, obj, args)
867 rescue => e
867 rescue => e
868 "<div class=\"flash error\">Error executing the <strong>#{macro}</strong> macro (#{e})</div>"
868 "<div class=\"flash error\">Error executing the <strong>#{macro}</strong> macro (#{e})</div>"
869 end || all
869 end || all
870 else
870 else
871 all
871 all
872 end
872 end
873 end
873 end
874 end
874 end
875
875
876 TOC_RE = /<p>\{\{([<>]?)toc\}\}<\/p>/i unless const_defined?(:TOC_RE)
876 TOC_RE = /<p>\{\{([<>]?)toc\}\}<\/p>/i unless const_defined?(:TOC_RE)
877
877
878 # Renders the TOC with given headings
878 # Renders the TOC with given headings
879 def replace_toc(text, headings)
879 def replace_toc(text, headings)
880 text.gsub!(TOC_RE) do
880 text.gsub!(TOC_RE) do
881 if headings.empty?
881 if headings.empty?
882 ''
882 ''
883 else
883 else
884 div_class = 'toc'
884 div_class = 'toc'
885 div_class << ' right' if $1 == '>'
885 div_class << ' right' if $1 == '>'
886 div_class << ' left' if $1 == '<'
886 div_class << ' left' if $1 == '<'
887 out = "<ul class=\"#{div_class}\"><li>"
887 out = "<ul class=\"#{div_class}\"><li>"
888 root = headings.map(&:first).min
888 root = headings.map(&:first).min
889 current = root
889 current = root
890 started = false
890 started = false
891 headings.each do |level, anchor, item|
891 headings.each do |level, anchor, item|
892 if level > current
892 if level > current
893 out << '<ul><li>' * (level - current)
893 out << '<ul><li>' * (level - current)
894 elsif level < current
894 elsif level < current
895 out << "</li></ul>\n" * (current - level) + "</li><li>"
895 out << "</li></ul>\n" * (current - level) + "</li><li>"
896 elsif started
896 elsif started
897 out << '</li><li>'
897 out << '</li><li>'
898 end
898 end
899 out << "<a href=\"##{anchor}\">#{item}</a>"
899 out << "<a href=\"##{anchor}\">#{item}</a>"
900 current = level
900 current = level
901 started = true
901 started = true
902 end
902 end
903 out << '</li></ul>' * (current - root)
903 out << '</li></ul>' * (current - root)
904 out << '</li></ul>'
904 out << '</li></ul>'
905 end
905 end
906 end
906 end
907 end
907 end
908
908
909 # Same as Rails' simple_format helper without using paragraphs
909 # Same as Rails' simple_format helper without using paragraphs
910 def simple_format_without_paragraph(text)
910 def simple_format_without_paragraph(text)
911 text.to_s.
911 text.to_s.
912 gsub(/\r\n?/, "\n"). # \r\n and \r -> \n
912 gsub(/\r\n?/, "\n"). # \r\n and \r -> \n
913 gsub(/\n\n+/, "<br /><br />"). # 2+ newline -> 2 br
913 gsub(/\n\n+/, "<br /><br />"). # 2+ newline -> 2 br
914 gsub(/([^\n]\n)(?=[^\n])/, '\1<br />'). # 1 newline -> br
914 gsub(/([^\n]\n)(?=[^\n])/, '\1<br />'). # 1 newline -> br
915 html_safe
915 html_safe
916 end
916 end
917
917
918 def lang_options_for_select(blank=true)
918 def lang_options_for_select(blank=true)
919 (blank ? [["(auto)", ""]] : []) +
919 (blank ? [["(auto)", ""]] : []) +
920 valid_languages.collect{|lang| [ ll(lang.to_s, :general_lang_name), lang.to_s]}.sort{|x,y| x.last <=> y.last }
920 valid_languages.collect{|lang| [ ll(lang.to_s, :general_lang_name), lang.to_s]}.sort{|x,y| x.last <=> y.last }
921 end
921 end
922
922
923 def label_tag_for(name, option_tags = nil, options = {})
923 def label_tag_for(name, option_tags = nil, options = {})
924 label_text = l(("field_"+field.to_s.gsub(/\_id$/, "")).to_sym) + (options.delete(:required) ? @template.content_tag("span", " *", :class => "required"): "")
924 label_text = l(("field_"+field.to_s.gsub(/\_id$/, "")).to_sym) + (options.delete(:required) ? @template.content_tag("span", " *", :class => "required"): "")
925 content_tag("label", label_text)
925 content_tag("label", label_text)
926 end
926 end
927
927
928 def labelled_form_for(*args, &proc)
928 def labelled_form_for(*args, &proc)
929 args << {} unless args.last.is_a?(Hash)
929 args << {} unless args.last.is_a?(Hash)
930 options = args.last
930 options = args.last
931 if args.first.is_a?(Symbol)
931 if args.first.is_a?(Symbol)
932 options.merge!(:as => args.shift)
932 options.merge!(:as => args.shift)
933 end
933 end
934 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
934 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
935 form_for(*args, &proc)
935 form_for(*args, &proc)
936 end
936 end
937
937
938 def labelled_fields_for(*args, &proc)
938 def labelled_fields_for(*args, &proc)
939 args << {} unless args.last.is_a?(Hash)
939 args << {} unless args.last.is_a?(Hash)
940 options = args.last
940 options = args.last
941 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
941 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
942 fields_for(*args, &proc)
942 fields_for(*args, &proc)
943 end
943 end
944
944
945 def labelled_remote_form_for(*args, &proc)
945 def labelled_remote_form_for(*args, &proc)
946 ActiveSupport::Deprecation.warn "ApplicationHelper#labelled_remote_form_for is deprecated and will be removed in Redmine 2.2."
946 ActiveSupport::Deprecation.warn "ApplicationHelper#labelled_remote_form_for is deprecated and will be removed in Redmine 2.2."
947 args << {} unless args.last.is_a?(Hash)
947 args << {} unless args.last.is_a?(Hash)
948 options = args.last
948 options = args.last
949 options.merge!({:builder => Redmine::Views::LabelledFormBuilder, :remote => true})
949 options.merge!({:builder => Redmine::Views::LabelledFormBuilder, :remote => true})
950 form_for(*args, &proc)
950 form_for(*args, &proc)
951 end
951 end
952
952
953 def error_messages_for(*objects)
953 def error_messages_for(*objects)
954 html = ""
954 html = ""
955 objects = objects.map {|o| o.is_a?(String) ? instance_variable_get("@#{o}") : o}.compact
955 objects = objects.map {|o| o.is_a?(String) ? instance_variable_get("@#{o}") : o}.compact
956 errors = objects.map {|o| o.errors.full_messages}.flatten
956 errors = objects.map {|o| o.errors.full_messages}.flatten
957 if errors.any?
957 if errors.any?
958 html << "<div id='errorExplanation'><ul>\n"
958 html << "<div id='errorExplanation'><ul>\n"
959 errors.each do |error|
959 errors.each do |error|
960 html << "<li>#{h error}</li>\n"
960 html << "<li>#{h error}</li>\n"
961 end
961 end
962 html << "</ul></div>\n"
962 html << "</ul></div>\n"
963 end
963 end
964 html.html_safe
964 html.html_safe
965 end
965 end
966
966
967 def delete_link(url, options={})
967 def delete_link(url, options={})
968 options = {
968 options = {
969 :method => :delete,
969 :method => :delete,
970 :data => {:confirm => l(:text_are_you_sure)},
970 :data => {:confirm => l(:text_are_you_sure)},
971 :class => 'icon icon-del'
971 :class => 'icon icon-del'
972 }.merge(options)
972 }.merge(options)
973
973
974 link_to l(:button_delete), url, options
974 link_to l(:button_delete), url, options
975 end
975 end
976
976
977 def preview_link(url, form, target='preview', options={})
977 def preview_link(url, form, target='preview', options={})
978 content_tag 'a', l(:label_preview), {
978 content_tag 'a', l(:label_preview), {
979 :href => "#",
979 :href => "#",
980 :onclick => %|submitPreview("#{escape_javascript url_for(url)}", "#{escape_javascript form}", "#{escape_javascript target}"); return false;|,
980 :onclick => %|submitPreview("#{escape_javascript url_for(url)}", "#{escape_javascript form}", "#{escape_javascript target}"); return false;|,
981 :accesskey => accesskey(:preview)
981 :accesskey => accesskey(:preview)
982 }.merge(options)
982 }.merge(options)
983 end
983 end
984
984
985 def link_to_function(name, function, html_options={})
985 def link_to_function(name, function, html_options={})
986 content_tag(:a, name, {:href => '#', :onclick => "#{function}; return false;"}.merge(html_options))
986 content_tag(:a, name, {:href => '#', :onclick => "#{function}; return false;"}.merge(html_options))
987 end
987 end
988
988
989 def back_url_hidden_field_tag
989 def back_url_hidden_field_tag
990 back_url = params[:back_url] || request.env['HTTP_REFERER']
990 back_url = params[:back_url] || request.env['HTTP_REFERER']
991 back_url = CGI.unescape(back_url.to_s)
991 back_url = CGI.unescape(back_url.to_s)
992 hidden_field_tag('back_url', CGI.escape(back_url), :id => nil) unless back_url.blank?
992 hidden_field_tag('back_url', CGI.escape(back_url), :id => nil) unless back_url.blank?
993 end
993 end
994
994
995 def check_all_links(form_name)
995 def check_all_links(form_name)
996 link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") +
996 link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") +
997 " | ".html_safe +
997 " | ".html_safe +
998 link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)")
998 link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)")
999 end
999 end
1000
1000
1001 def progress_bar(pcts, options={})
1001 def progress_bar(pcts, options={})
1002 pcts = [pcts, pcts] unless pcts.is_a?(Array)
1002 pcts = [pcts, pcts] unless pcts.is_a?(Array)
1003 pcts = pcts.collect(&:round)
1003 pcts = pcts.collect(&:round)
1004 pcts[1] = pcts[1] - pcts[0]
1004 pcts[1] = pcts[1] - pcts[0]
1005 pcts << (100 - pcts[1] - pcts[0])
1005 pcts << (100 - pcts[1] - pcts[0])
1006 width = options[:width] || '100px;'
1006 width = options[:width] || '100px;'
1007 legend = options[:legend] || ''
1007 legend = options[:legend] || ''
1008 content_tag('table',
1008 content_tag('table',
1009 content_tag('tr',
1009 content_tag('tr',
1010 (pcts[0] > 0 ? content_tag('td', '', :style => "width: #{pcts[0]}%;", :class => 'closed') : ''.html_safe) +
1010 (pcts[0] > 0 ? content_tag('td', '', :style => "width: #{pcts[0]}%;", :class => 'closed') : ''.html_safe) +
1011 (pcts[1] > 0 ? content_tag('td', '', :style => "width: #{pcts[1]}%;", :class => 'done') : ''.html_safe) +
1011 (pcts[1] > 0 ? content_tag('td', '', :style => "width: #{pcts[1]}%;", :class => 'done') : ''.html_safe) +
1012 (pcts[2] > 0 ? content_tag('td', '', :style => "width: #{pcts[2]}%;", :class => 'todo') : ''.html_safe)
1012 (pcts[2] > 0 ? content_tag('td', '', :style => "width: #{pcts[2]}%;", :class => 'todo') : ''.html_safe)
1013 ), :class => 'progress', :style => "width: #{width};").html_safe +
1013 ), :class => 'progress', :style => "width: #{width};").html_safe +
1014 content_tag('p', legend, :class => 'pourcent').html_safe
1014 content_tag('p', legend, :class => 'pourcent').html_safe
1015 end
1015 end
1016
1016
1017 def checked_image(checked=true)
1017 def checked_image(checked=true)
1018 if checked
1018 if checked
1019 image_tag 'toggle_check.png'
1019 image_tag 'toggle_check.png'
1020 end
1020 end
1021 end
1021 end
1022
1022
1023 def context_menu(url)
1023 def context_menu(url)
1024 unless @context_menu_included
1024 unless @context_menu_included
1025 content_for :header_tags do
1025 content_for :header_tags do
1026 javascript_include_tag('context_menu') +
1026 javascript_include_tag('context_menu') +
1027 stylesheet_link_tag('context_menu')
1027 stylesheet_link_tag('context_menu')
1028 end
1028 end
1029 if l(:direction) == 'rtl'
1029 if l(:direction) == 'rtl'
1030 content_for :header_tags do
1030 content_for :header_tags do
1031 stylesheet_link_tag('context_menu_rtl')
1031 stylesheet_link_tag('context_menu_rtl')
1032 end
1032 end
1033 end
1033 end
1034 @context_menu_included = true
1034 @context_menu_included = true
1035 end
1035 end
1036 javascript_tag "contextMenuInit('#{ url_for(url) }')"
1036 javascript_tag "contextMenuInit('#{ url_for(url) }')"
1037 end
1037 end
1038
1038
1039 def calendar_for(field_id)
1039 def calendar_for(field_id)
1040 include_calendar_headers_tags
1040 include_calendar_headers_tags
1041 javascript_tag("$(function() { $('##{field_id}').datepicker(datepickerOptions); });")
1041 javascript_tag("$(function() { $('##{field_id}').datepicker(datepickerOptions); });")
1042 end
1042 end
1043
1043
1044 def include_calendar_headers_tags
1044 def include_calendar_headers_tags
1045 unless @calendar_headers_tags_included
1045 unless @calendar_headers_tags_included
1046 @calendar_headers_tags_included = true
1046 @calendar_headers_tags_included = true
1047 content_for :header_tags do
1047 content_for :header_tags do
1048 tags = javascript_tag("var datepickerOptions={dateFormat: 'yy-mm-dd', showOn: 'button', buttonImageOnly: true, buttonImage: '" + path_to_image('/images/calendar.png') + "'};")
1048 tags = javascript_tag("var datepickerOptions={dateFormat: 'yy-mm-dd', showOn: 'button', buttonImageOnly: true, buttonImage: '" + path_to_image('/images/calendar.png') + "'};")
1049 jquery_locale = l('jquery.locale', :default => current_language.to_s)
1049 jquery_locale = l('jquery.locale', :default => current_language.to_s)
1050 unless jquery_locale == 'en'
1050 unless jquery_locale == 'en'
1051 tags << javascript_include_tag("i18n/jquery.ui.datepicker-#{jquery_locale}.js")
1051 tags << javascript_include_tag("i18n/jquery.ui.datepicker-#{jquery_locale}.js")
1052 end
1052 end
1053 tags
1053 tags
1054 end
1054 end
1055 end
1055 end
1056 end
1056 end
1057
1057
1058 # Overrides Rails' stylesheet_link_tag with themes and plugins support.
1058 # Overrides Rails' stylesheet_link_tag with themes and plugins support.
1059 # Examples:
1059 # Examples:
1060 # stylesheet_link_tag('styles') # => picks styles.css from the current theme or defaults
1060 # stylesheet_link_tag('styles') # => picks styles.css from the current theme or defaults
1061 # stylesheet_link_tag('styles', :plugin => 'foo) # => picks styles.css from plugin's assets
1061 # stylesheet_link_tag('styles', :plugin => 'foo) # => picks styles.css from plugin's assets
1062 #
1062 #
1063 def stylesheet_link_tag(*sources)
1063 def stylesheet_link_tag(*sources)
1064 options = sources.last.is_a?(Hash) ? sources.pop : {}
1064 options = sources.last.is_a?(Hash) ? sources.pop : {}
1065 plugin = options.delete(:plugin)
1065 plugin = options.delete(:plugin)
1066 sources = sources.map do |source|
1066 sources = sources.map do |source|
1067 if plugin
1067 if plugin
1068 "/plugin_assets/#{plugin}/stylesheets/#{source}"
1068 "/plugin_assets/#{plugin}/stylesheets/#{source}"
1069 elsif current_theme && current_theme.stylesheets.include?(source)
1069 elsif current_theme && current_theme.stylesheets.include?(source)
1070 current_theme.stylesheet_path(source)
1070 current_theme.stylesheet_path(source)
1071 else
1071 else
1072 source
1072 source
1073 end
1073 end
1074 end
1074 end
1075 super sources, options
1075 super sources, options
1076 end
1076 end
1077
1077
1078 # Overrides Rails' image_tag with themes and plugins support.
1078 # Overrides Rails' image_tag with themes and plugins support.
1079 # Examples:
1079 # Examples:
1080 # image_tag('image.png') # => picks image.png from the current theme or defaults
1080 # image_tag('image.png') # => picks image.png from the current theme or defaults
1081 # image_tag('image.png', :plugin => 'foo) # => picks image.png from plugin's assets
1081 # image_tag('image.png', :plugin => 'foo) # => picks image.png from plugin's assets
1082 #
1082 #
1083 def image_tag(source, options={})
1083 def image_tag(source, options={})
1084 if plugin = options.delete(:plugin)
1084 if plugin = options.delete(:plugin)
1085 source = "/plugin_assets/#{plugin}/images/#{source}"
1085 source = "/plugin_assets/#{plugin}/images/#{source}"
1086 elsif current_theme && current_theme.images.include?(source)
1086 elsif current_theme && current_theme.images.include?(source)
1087 source = current_theme.image_path(source)
1087 source = current_theme.image_path(source)
1088 end
1088 end
1089 super source, options
1089 super source, options
1090 end
1090 end
1091
1091
1092 # Overrides Rails' javascript_include_tag with plugins support
1092 # Overrides Rails' javascript_include_tag with plugins support
1093 # Examples:
1093 # Examples:
1094 # javascript_include_tag('scripts') # => picks scripts.js from defaults
1094 # javascript_include_tag('scripts') # => picks scripts.js from defaults
1095 # javascript_include_tag('scripts', :plugin => 'foo) # => picks scripts.js from plugin's assets
1095 # javascript_include_tag('scripts', :plugin => 'foo) # => picks scripts.js from plugin's assets
1096 #
1096 #
1097 def javascript_include_tag(*sources)
1097 def javascript_include_tag(*sources)
1098 options = sources.last.is_a?(Hash) ? sources.pop : {}
1098 options = sources.last.is_a?(Hash) ? sources.pop : {}
1099 if plugin = options.delete(:plugin)
1099 if plugin = options.delete(:plugin)
1100 sources = sources.map do |source|
1100 sources = sources.map do |source|
1101 if plugin
1101 if plugin
1102 "/plugin_assets/#{plugin}/javascripts/#{source}"
1102 "/plugin_assets/#{plugin}/javascripts/#{source}"
1103 else
1103 else
1104 source
1104 source
1105 end
1105 end
1106 end
1106 end
1107 end
1107 end
1108 super sources, options
1108 super sources, options
1109 end
1109 end
1110
1110
1111 def content_for(name, content = nil, &block)
1111 def content_for(name, content = nil, &block)
1112 @has_content ||= {}
1112 @has_content ||= {}
1113 @has_content[name] = true
1113 @has_content[name] = true
1114 super(name, content, &block)
1114 super(name, content, &block)
1115 end
1115 end
1116
1116
1117 def has_content?(name)
1117 def has_content?(name)
1118 (@has_content && @has_content[name]) || false
1118 (@has_content && @has_content[name]) || false
1119 end
1119 end
1120
1120
1121 def sidebar_content?
1121 def sidebar_content?
1122 has_content?(:sidebar) || view_layouts_base_sidebar_hook_response.present?
1122 has_content?(:sidebar) || view_layouts_base_sidebar_hook_response.present?
1123 end
1123 end
1124
1124
1125 def view_layouts_base_sidebar_hook_response
1125 def view_layouts_base_sidebar_hook_response
1126 @view_layouts_base_sidebar_hook_response ||= call_hook(:view_layouts_base_sidebar)
1126 @view_layouts_base_sidebar_hook_response ||= call_hook(:view_layouts_base_sidebar)
1127 end
1127 end
1128
1128
1129 def email_delivery_enabled?
1129 def email_delivery_enabled?
1130 !!ActionMailer::Base.perform_deliveries
1130 !!ActionMailer::Base.perform_deliveries
1131 end
1131 end
1132
1132
1133 # Returns the avatar image tag for the given +user+ if avatars are enabled
1133 # Returns the avatar image tag for the given +user+ if avatars are enabled
1134 # +user+ can be a User or a string that will be scanned for an email address (eg. 'joe <joe@foo.bar>')
1134 # +user+ can be a User or a string that will be scanned for an email address (eg. 'joe <joe@foo.bar>')
1135 def avatar(user, options = { })
1135 def avatar(user, options = { })
1136 if Setting.gravatar_enabled?
1136 if Setting.gravatar_enabled?
1137 options.merge!({:ssl => (request && request.ssl?), :default => Setting.gravatar_default})
1137 options.merge!({:ssl => (request && request.ssl?), :default => Setting.gravatar_default})
1138 email = nil
1138 email = nil
1139 if user.respond_to?(:mail)
1139 if user.respond_to?(:mail)
1140 email = user.mail
1140 email = user.mail
1141 elsif user.to_s =~ %r{<(.+?)>}
1141 elsif user.to_s =~ %r{<(.+?)>}
1142 email = $1
1142 email = $1
1143 end
1143 end
1144 return gravatar(email.to_s.downcase, options) unless email.blank? rescue nil
1144 return gravatar(email.to_s.downcase, options) unless email.blank? rescue nil
1145 else
1145 else
1146 ''
1146 ''
1147 end
1147 end
1148 end
1148 end
1149
1149
1150 def sanitize_anchor_name(anchor)
1150 def sanitize_anchor_name(anchor)
1151 anchor.gsub(%r{[^\w\s\-]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
1151 anchor.gsub(%r{[^\w\s\-]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
1152 end
1152 end
1153
1153
1154 # Returns the javascript tags that are included in the html layout head
1154 # Returns the javascript tags that are included in the html layout head
1155 def javascript_heads
1155 def javascript_heads
1156 tags = javascript_include_tag('jquery-1.7.2-ui-1.8.21-ujs-2.0.2', 'application')
1156 tags = javascript_include_tag('jquery-1.7.2-ui-1.8.21-ujs-2.0.2', 'application')
1157 unless User.current.pref.warn_on_leaving_unsaved == '0'
1157 unless User.current.pref.warn_on_leaving_unsaved == '0'
1158 tags << "\n".html_safe + javascript_tag("$(window).load(function(){ warnLeavingUnsaved('#{escape_javascript l(:text_warn_on_leaving_unsaved)}'); });")
1158 tags << "\n".html_safe + javascript_tag("$(window).load(function(){ warnLeavingUnsaved('#{escape_javascript l(:text_warn_on_leaving_unsaved)}'); });")
1159 end
1159 end
1160 tags
1160 tags
1161 end
1161 end
1162
1162
1163 def favicon
1163 def favicon
1164 "<link rel='shortcut icon' href='#{image_path('/favicon.ico')}' />".html_safe
1164 "<link rel='shortcut icon' href='#{image_path('/favicon.ico')}' />".html_safe
1165 end
1165 end
1166
1166
1167 def robot_exclusion_tag
1167 def robot_exclusion_tag
1168 '<meta name="robots" content="noindex,follow,noarchive" />'.html_safe
1168 '<meta name="robots" content="noindex,follow,noarchive" />'.html_safe
1169 end
1169 end
1170
1170
1171 # Returns true if arg is expected in the API response
1171 # Returns true if arg is expected in the API response
1172 def include_in_api_response?(arg)
1172 def include_in_api_response?(arg)
1173 unless @included_in_api_response
1173 unless @included_in_api_response
1174 param = params[:include]
1174 param = params[:include]
1175 @included_in_api_response = param.is_a?(Array) ? param.collect(&:to_s) : param.to_s.split(',')
1175 @included_in_api_response = param.is_a?(Array) ? param.collect(&:to_s) : param.to_s.split(',')
1176 @included_in_api_response.collect!(&:strip)
1176 @included_in_api_response.collect!(&:strip)
1177 end
1177 end
1178 @included_in_api_response.include?(arg.to_s)
1178 @included_in_api_response.include?(arg.to_s)
1179 end
1179 end
1180
1180
1181 # Returns options or nil if nometa param or X-Redmine-Nometa header
1181 # Returns options or nil if nometa param or X-Redmine-Nometa header
1182 # was set in the request
1182 # was set in the request
1183 def api_meta(options)
1183 def api_meta(options)
1184 if params[:nometa].present? || request.headers['X-Redmine-Nometa']
1184 if params[:nometa].present? || request.headers['X-Redmine-Nometa']
1185 # compatibility mode for activeresource clients that raise
1185 # compatibility mode for activeresource clients that raise
1186 # an error when unserializing an array with attributes
1186 # an error when unserializing an array with attributes
1187 nil
1187 nil
1188 else
1188 else
1189 options
1189 options
1190 end
1190 end
1191 end
1191 end
1192
1192
1193 private
1193 private
1194
1194
1195 def wiki_helper
1195 def wiki_helper
1196 helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
1196 helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
1197 extend helper
1197 extend helper
1198 return self
1198 return self
1199 end
1199 end
1200
1200
1201 def link_to_content_update(text, url_params = {}, html_options = {})
1201 def link_to_content_update(text, url_params = {}, html_options = {})
1202 link_to(text, url_params, html_options)
1202 link_to(text, url_params, html_options)
1203 end
1203 end
1204 end
1204 end
@@ -1,156 +1,167
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require File.expand_path('../../../../../test_helper', __FILE__)
18 require File.expand_path('../../../../../test_helper', __FILE__)
19
19
20 class Redmine::WikiFormatting::MacrosTest < ActionView::TestCase
20 class Redmine::WikiFormatting::MacrosTest < ActionView::TestCase
21 include ApplicationHelper
21 include ApplicationHelper
22 include ActionView::Helpers::TextHelper
22 include ActionView::Helpers::TextHelper
23 include ActionView::Helpers::SanitizeHelper
23 include ActionView::Helpers::SanitizeHelper
24 include ERB::Util
24 include ERB::Util
25 extend ActionView::Helpers::SanitizeHelper::ClassMethods
25 extend ActionView::Helpers::SanitizeHelper::ClassMethods
26
26
27 fixtures :projects, :roles, :enabled_modules, :users,
27 fixtures :projects, :roles, :enabled_modules, :users,
28 :repositories, :changesets,
28 :repositories, :changesets,
29 :trackers, :issue_statuses, :issues,
29 :trackers, :issue_statuses, :issues,
30 :versions, :documents,
30 :versions, :documents,
31 :wikis, :wiki_pages, :wiki_contents,
31 :wikis, :wiki_pages, :wiki_contents,
32 :boards, :messages,
32 :boards, :messages,
33 :attachments
33 :attachments
34
34
35 def setup
35 def setup
36 super
36 super
37 @project = nil
37 @project = nil
38 end
38 end
39
39
40 def teardown
40 def teardown
41 end
41 end
42
42
43 def test_macro_registration
43 def test_macro_registration
44 Redmine::WikiFormatting::Macros.register do
44 Redmine::WikiFormatting::Macros.register do
45 macro :foo do |obj, args|
45 macro :foo do |obj, args|
46 "Foo: #{args.size} (#{args.join(',')}) (#{args.class.name})"
46 "Foo: #{args.size} (#{args.join(',')}) (#{args.class.name})"
47 end
47 end
48 end
48 end
49
49
50 assert_equal '<p>Foo: 0 () (Array)</p>', textilizable("{{foo}}")
50 assert_equal '<p>Foo: 0 () (Array)</p>', textilizable("{{foo}}")
51 assert_equal '<p>Foo: 0 () (Array)</p>', textilizable("{{foo()}}")
51 assert_equal '<p>Foo: 0 () (Array)</p>', textilizable("{{foo()}}")
52 assert_equal '<p>Foo: 1 (arg1) (Array)</p>', textilizable("{{foo(arg1)}}")
52 assert_equal '<p>Foo: 1 (arg1) (Array)</p>', textilizable("{{foo(arg1)}}")
53 assert_equal '<p>Foo: 2 (arg1,arg2) (Array)</p>', textilizable("{{foo(arg1, arg2)}}")
53 assert_equal '<p>Foo: 2 (arg1,arg2) (Array)</p>', textilizable("{{foo(arg1, arg2)}}")
54 end
54 end
55
55
56 def test_macro_registration_parse_args_set_to_false_should_disable_arguments_parsing
56 def test_macro_registration_parse_args_set_to_false_should_disable_arguments_parsing
57 Redmine::WikiFormatting::Macros.register do
57 Redmine::WikiFormatting::Macros.register do
58 macro :bar, :parse_args => false do |obj, args|
58 macro :bar, :parse_args => false do |obj, args|
59 "Bar: (#{args}) (#{args.class.name})"
59 "Bar: (#{args}) (#{args.class.name})"
60 end
60 end
61 end
61 end
62
62
63 assert_equal '<p>Bar: (args, more args) (String)</p>', textilizable("{{bar(args, more args)}}")
63 assert_equal '<p>Bar: (args, more args) (String)</p>', textilizable("{{bar(args, more args)}}")
64 assert_equal '<p>Bar: () (String)</p>', textilizable("{{bar}}")
64 assert_equal '<p>Bar: () (String)</p>', textilizable("{{bar}}")
65 assert_equal '<p>Bar: () (String)</p>', textilizable("{{bar()}}")
65 assert_equal '<p>Bar: () (String)</p>', textilizable("{{bar()}}")
66 end
66 end
67
67
68 def test_multiple_macros_on_the_same_line
69 Redmine::WikiFormatting::Macros.macro :foo do |obj, args|
70 args.any? ? "args: #{args.join(',')}" : "no args"
71 end
72
73 assert_equal '<p>no args no args</p>', textilizable("{{foo}} {{foo}}")
74 assert_equal '<p>args: a,b no args</p>', textilizable("{{foo(a,b)}} {{foo}}")
75 assert_equal '<p>args: a,b args: c,d</p>', textilizable("{{foo(a,b)}} {{foo(c,d)}}")
76 assert_equal '<p>no args args: c,d</p>', textilizable("{{foo}} {{foo(c,d)}}")
77 end
78
68 def test_macro_hello_world
79 def test_macro_hello_world
69 text = "{{hello_world}}"
80 text = "{{hello_world}}"
70 assert textilizable(text).match(/Hello world!/)
81 assert textilizable(text).match(/Hello world!/)
71 # escaping
82 # escaping
72 text = "!{{hello_world}}"
83 text = "!{{hello_world}}"
73 assert_equal '<p>{{hello_world}}</p>', textilizable(text)
84 assert_equal '<p>{{hello_world}}</p>', textilizable(text)
74 end
85 end
75
86
76 def test_macro_macro_list
87 def test_macro_macro_list
77 text = "{{macro_list}}"
88 text = "{{macro_list}}"
78 assert_match %r{<code>hello_world</code>}, textilizable(text)
89 assert_match %r{<code>hello_world</code>}, textilizable(text)
79 end
90 end
80
91
81 def test_macro_include
92 def test_macro_include
82 @project = Project.find(1)
93 @project = Project.find(1)
83 # include a page of the current project wiki
94 # include a page of the current project wiki
84 text = "{{include(Another page)}}"
95 text = "{{include(Another page)}}"
85 assert textilizable(text).match(/This is a link to a ticket/)
96 assert textilizable(text).match(/This is a link to a ticket/)
86
97
87 @project = nil
98 @project = nil
88 # include a page of a specific project wiki
99 # include a page of a specific project wiki
89 text = "{{include(ecookbook:Another page)}}"
100 text = "{{include(ecookbook:Another page)}}"
90 assert textilizable(text).match(/This is a link to a ticket/)
101 assert textilizable(text).match(/This is a link to a ticket/)
91
102
92 text = "{{include(ecookbook:)}}"
103 text = "{{include(ecookbook:)}}"
93 assert textilizable(text).match(/CookBook documentation/)
104 assert textilizable(text).match(/CookBook documentation/)
94
105
95 text = "{{include(unknowidentifier:somepage)}}"
106 text = "{{include(unknowidentifier:somepage)}}"
96 assert textilizable(text).match(/Page not found/)
107 assert textilizable(text).match(/Page not found/)
97 end
108 end
98
109
99 def test_macro_child_pages
110 def test_macro_child_pages
100 expected = "<p><ul class=\"pages-hierarchy\">\n" +
111 expected = "<p><ul class=\"pages-hierarchy\">\n" +
101 "<li><a href=\"/projects/ecookbook/wiki/Child_1\">Child 1</a></li>\n" +
112 "<li><a href=\"/projects/ecookbook/wiki/Child_1\">Child 1</a></li>\n" +
102 "<li><a href=\"/projects/ecookbook/wiki/Child_2\">Child 2</a></li>\n" +
113 "<li><a href=\"/projects/ecookbook/wiki/Child_2\">Child 2</a></li>\n" +
103 "</ul>\n</p>"
114 "</ul>\n</p>"
104
115
105 @project = Project.find(1)
116 @project = Project.find(1)
106 # child pages of the current wiki page
117 # child pages of the current wiki page
107 assert_equal expected, textilizable("{{child_pages}}", :object => WikiPage.find(2).content)
118 assert_equal expected, textilizable("{{child_pages}}", :object => WikiPage.find(2).content)
108 # child pages of another page
119 # child pages of another page
109 assert_equal expected, textilizable("{{child_pages(Another_page)}}", :object => WikiPage.find(1).content)
120 assert_equal expected, textilizable("{{child_pages(Another_page)}}", :object => WikiPage.find(1).content)
110
121
111 @project = Project.find(2)
122 @project = Project.find(2)
112 assert_equal expected, textilizable("{{child_pages(ecookbook:Another_page)}}", :object => WikiPage.find(1).content)
123 assert_equal expected, textilizable("{{child_pages(ecookbook:Another_page)}}", :object => WikiPage.find(1).content)
113 end
124 end
114
125
115 def test_macro_child_pages_with_option
126 def test_macro_child_pages_with_option
116 expected = "<p><ul class=\"pages-hierarchy\">\n" +
127 expected = "<p><ul class=\"pages-hierarchy\">\n" +
117 "<li><a href=\"/projects/ecookbook/wiki/Another_page\">Another page</a>\n" +
128 "<li><a href=\"/projects/ecookbook/wiki/Another_page\">Another page</a>\n" +
118 "<ul class=\"pages-hierarchy\">\n" +
129 "<ul class=\"pages-hierarchy\">\n" +
119 "<li><a href=\"/projects/ecookbook/wiki/Child_1\">Child 1</a></li>\n" +
130 "<li><a href=\"/projects/ecookbook/wiki/Child_1\">Child 1</a></li>\n" +
120 "<li><a href=\"/projects/ecookbook/wiki/Child_2\">Child 2</a></li>\n" +
131 "<li><a href=\"/projects/ecookbook/wiki/Child_2\">Child 2</a></li>\n" +
121 "</ul>\n</li>\n</ul>\n</p>"
132 "</ul>\n</li>\n</ul>\n</p>"
122
133
123 @project = Project.find(1)
134 @project = Project.find(1)
124 # child pages of the current wiki page
135 # child pages of the current wiki page
125 assert_equal expected, textilizable("{{child_pages(parent=1)}}", :object => WikiPage.find(2).content)
136 assert_equal expected, textilizable("{{child_pages(parent=1)}}", :object => WikiPage.find(2).content)
126 # child pages of another page
137 # child pages of another page
127 assert_equal expected, textilizable("{{child_pages(Another_page, parent=1)}}", :object => WikiPage.find(1).content)
138 assert_equal expected, textilizable("{{child_pages(Another_page, parent=1)}}", :object => WikiPage.find(1).content)
128
139
129 @project = Project.find(2)
140 @project = Project.find(2)
130 assert_equal expected, textilizable("{{child_pages(ecookbook:Another_page, parent=1)}}", :object => WikiPage.find(1).content)
141 assert_equal expected, textilizable("{{child_pages(ecookbook:Another_page, parent=1)}}", :object => WikiPage.find(1).content)
131 end
142 end
132
143
133 def test_macro_child_pages_without_wiki_page_should_fail
144 def test_macro_child_pages_without_wiki_page_should_fail
134 assert_match /can be called from wiki pages only/, textilizable("{{child_pages}}")
145 assert_match /can be called from wiki pages only/, textilizable("{{child_pages}}")
135 end
146 end
136
147
137 def test_macro_thumbnail
148 def test_macro_thumbnail
138 assert_equal '<p><a href="/attachments/17" class="thumbnail" title="testfile.PNG"><img alt="testfile.PNG" src="/attachments/thumbnail/17" /></a></p>',
149 assert_equal '<p><a href="/attachments/17" class="thumbnail" title="testfile.PNG"><img alt="testfile.PNG" src="/attachments/thumbnail/17" /></a></p>',
139 textilizable("{{thumbnail(testfile.png)}}", :object => Issue.find(14))
150 textilizable("{{thumbnail(testfile.png)}}", :object => Issue.find(14))
140 end
151 end
141
152
142 def test_macro_thumbnail_with_size
153 def test_macro_thumbnail_with_size
143 assert_equal '<p><a href="/attachments/17" class="thumbnail" title="testfile.PNG"><img alt="testfile.PNG" src="/attachments/thumbnail/17/200" /></a></p>',
154 assert_equal '<p><a href="/attachments/17" class="thumbnail" title="testfile.PNG"><img alt="testfile.PNG" src="/attachments/thumbnail/17/200" /></a></p>',
144 textilizable("{{thumbnail(testfile.png, size=200)}}", :object => Issue.find(14))
155 textilizable("{{thumbnail(testfile.png, size=200)}}", :object => Issue.find(14))
145 end
156 end
146
157
147 def test_macro_thumbnail_with_title
158 def test_macro_thumbnail_with_title
148 assert_equal '<p><a href="/attachments/17" class="thumbnail" title="Cool image"><img alt="testfile.PNG" src="/attachments/thumbnail/17" /></a></p>',
159 assert_equal '<p><a href="/attachments/17" class="thumbnail" title="Cool image"><img alt="testfile.PNG" src="/attachments/thumbnail/17" /></a></p>',
149 textilizable("{{thumbnail(testfile.png, title=Cool image)}}", :object => Issue.find(14))
160 textilizable("{{thumbnail(testfile.png, title=Cool image)}}", :object => Issue.find(14))
150 end
161 end
151
162
152 def test_macro_thumbnail_with_invalid_filename_should_fail
163 def test_macro_thumbnail_with_invalid_filename_should_fail
153 assert_include 'test.png not found',
164 assert_include 'test.png not found',
154 textilizable("{{thumbnail(test.png)}}", :object => Issue.find(14))
165 textilizable("{{thumbnail(test.png)}}", :object => Issue.find(14))
155 end
166 end
156 end
167 end
General Comments 0
You need to be logged in to leave comments. Login now