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