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