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