##// END OF EJS Templates
Fixed that #parse_redmine_links errors when given a link tag without attributes (#19304)....
Jean-Philippe Lang -
r14392:58d3dc354d68
parent child
Show More
@@ -1,1337 +1,1337
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 4 # Copyright (C) 2006-2015 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 include Redmine::SudoMode::Helper
29 29 include Redmine::Themes::Helper
30 30 include Redmine::Hook::Helper
31 31
32 32 extend Forwardable
33 33 def_delegators :wiki_helper, :wikitoolbar_for, :heads_for_wiki_formatter
34 34
35 35 # Return true if user is authorized for controller/action, otherwise false
36 36 def authorize_for(controller, action)
37 37 User.current.allowed_to?({:controller => controller, :action => action}, @project)
38 38 end
39 39
40 40 # Display a link if user is authorized
41 41 #
42 42 # @param [String] name Anchor text (passed to link_to)
43 43 # @param [Hash] options Hash params. This will checked by authorize_for to see if the user is authorized
44 44 # @param [optional, Hash] html_options Options passed to link_to
45 45 # @param [optional, Hash] parameters_for_method_reference Extra parameters for link_to
46 46 def link_to_if_authorized(name, options = {}, html_options = nil, *parameters_for_method_reference)
47 47 link_to(name, options, html_options, *parameters_for_method_reference) if authorize_for(options[:controller] || params[:controller], options[:action])
48 48 end
49 49
50 50 # Displays a link to user's account page if active
51 51 def link_to_user(user, options={})
52 52 if user.is_a?(User)
53 53 name = h(user.name(options[:format]))
54 54 if user.active? || (User.current.admin? && user.logged?)
55 55 link_to name, user_path(user), :class => user.css_classes
56 56 else
57 57 name
58 58 end
59 59 else
60 60 h(user.to_s)
61 61 end
62 62 end
63 63
64 64 # Displays a link to +issue+ with its subject.
65 65 # Examples:
66 66 #
67 67 # link_to_issue(issue) # => Defect #6: This is the subject
68 68 # link_to_issue(issue, :truncate => 6) # => Defect #6: This i...
69 69 # link_to_issue(issue, :subject => false) # => Defect #6
70 70 # link_to_issue(issue, :project => true) # => Foo - Defect #6
71 71 # link_to_issue(issue, :subject => false, :tracker => false) # => #6
72 72 #
73 73 def link_to_issue(issue, options={})
74 74 title = nil
75 75 subject = nil
76 76 text = options[:tracker] == false ? "##{issue.id}" : "#{issue.tracker} ##{issue.id}"
77 77 if options[:subject] == false
78 78 title = issue.subject.truncate(60)
79 79 else
80 80 subject = issue.subject
81 81 if truncate_length = options[:truncate]
82 82 subject = subject.truncate(truncate_length)
83 83 end
84 84 end
85 85 only_path = options[:only_path].nil? ? true : options[:only_path]
86 86 s = link_to(text, issue_url(issue, :only_path => only_path),
87 87 :class => issue.css_classes, :title => title)
88 88 s << h(": #{subject}") if subject
89 89 s = h("#{issue.project} - ") + s if options[:project]
90 90 s
91 91 end
92 92
93 93 # Generates a link to an attachment.
94 94 # Options:
95 95 # * :text - Link text (default to attachment filename)
96 96 # * :download - Force download (default: false)
97 97 def link_to_attachment(attachment, options={})
98 98 text = options.delete(:text) || attachment.filename
99 99 route_method = options.delete(:download) ? :download_named_attachment_url : :named_attachment_url
100 100 html_options = options.slice!(:only_path)
101 101 options[:only_path] = true unless options.key?(:only_path)
102 102 url = send(route_method, attachment, attachment.filename, options)
103 103 link_to text, url, html_options
104 104 end
105 105
106 106 # Generates a link to a SCM revision
107 107 # Options:
108 108 # * :text - Link text (default to the formatted revision)
109 109 def link_to_revision(revision, repository, options={})
110 110 if repository.is_a?(Project)
111 111 repository = repository.repository
112 112 end
113 113 text = options.delete(:text) || format_revision(revision)
114 114 rev = revision.respond_to?(:identifier) ? revision.identifier : revision
115 115 link_to(
116 116 h(text),
117 117 {:controller => 'repositories', :action => 'revision', :id => repository.project, :repository_id => repository.identifier_param, :rev => rev},
118 118 :title => l(:label_revision_id, format_revision(revision)),
119 119 :accesskey => options[:accesskey]
120 120 )
121 121 end
122 122
123 123 # Generates a link to a message
124 124 def link_to_message(message, options={}, html_options = nil)
125 125 link_to(
126 126 message.subject.truncate(60),
127 127 board_message_url(message.board_id, message.parent_id || message.id, {
128 128 :r => (message.parent_id && message.id),
129 129 :anchor => (message.parent_id ? "message-#{message.id}" : nil),
130 130 :only_path => true
131 131 }.merge(options)),
132 132 html_options
133 133 )
134 134 end
135 135
136 136 # Generates a link to a project if active
137 137 # Examples:
138 138 #
139 139 # link_to_project(project) # => link to the specified project overview
140 140 # link_to_project(project, {:only_path => false}, :class => "project") # => 3rd arg adds html options
141 141 # link_to_project(project, {}, :class => "project") # => html options with default url (project overview)
142 142 #
143 143 def link_to_project(project, options={}, html_options = nil)
144 144 if project.archived?
145 145 h(project.name)
146 146 else
147 147 link_to project.name,
148 148 project_url(project, {:only_path => true}.merge(options)),
149 149 html_options
150 150 end
151 151 end
152 152
153 153 # Generates a link to a project settings if active
154 154 def link_to_project_settings(project, options={}, html_options=nil)
155 155 if project.active?
156 156 link_to project.name, settings_project_path(project, options), html_options
157 157 elsif project.archived?
158 158 h(project.name)
159 159 else
160 160 link_to project.name, project_path(project, options), html_options
161 161 end
162 162 end
163 163
164 164 # Generates a link to a version
165 165 def link_to_version(version, options = {})
166 166 return '' unless version && version.is_a?(Version)
167 167 options = {:title => format_date(version.effective_date)}.merge(options)
168 168 link_to_if version.visible?, format_version_name(version), version_path(version), options
169 169 end
170 170
171 171 # Helper that formats object for html or text rendering
172 172 def format_object(object, html=true, &block)
173 173 if block_given?
174 174 object = yield object
175 175 end
176 176 case object.class.name
177 177 when 'Array'
178 178 object.map {|o| format_object(o, html)}.join(', ').html_safe
179 179 when 'Time'
180 180 format_time(object)
181 181 when 'Date'
182 182 format_date(object)
183 183 when 'Fixnum'
184 184 object.to_s
185 185 when 'Float'
186 186 sprintf "%.2f", object
187 187 when 'User'
188 188 html ? link_to_user(object) : object.to_s
189 189 when 'Project'
190 190 html ? link_to_project(object) : object.to_s
191 191 when 'Version'
192 192 html ? link_to_version(object) : object.to_s
193 193 when 'TrueClass'
194 194 l(:general_text_Yes)
195 195 when 'FalseClass'
196 196 l(:general_text_No)
197 197 when 'Issue'
198 198 object.visible? && html ? link_to_issue(object) : "##{object.id}"
199 199 when 'CustomValue', 'CustomFieldValue'
200 200 if object.custom_field
201 201 f = object.custom_field.format.formatted_custom_value(self, object, html)
202 202 if f.nil? || f.is_a?(String)
203 203 f
204 204 else
205 205 format_object(f, html, &block)
206 206 end
207 207 else
208 208 object.value.to_s
209 209 end
210 210 else
211 211 html ? h(object) : object.to_s
212 212 end
213 213 end
214 214
215 215 def wiki_page_path(page, options={})
216 216 url_for({:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title}.merge(options))
217 217 end
218 218
219 219 def thumbnail_tag(attachment)
220 220 link_to image_tag(thumbnail_path(attachment)),
221 221 named_attachment_path(attachment, attachment.filename),
222 222 :title => attachment.filename
223 223 end
224 224
225 225 def toggle_link(name, id, options={})
226 226 onclick = "$('##{id}').toggle(); "
227 227 onclick << (options[:focus] ? "$('##{options[:focus]}').focus(); " : "this.blur(); ")
228 228 onclick << "return false;"
229 229 link_to(name, "#", :onclick => onclick)
230 230 end
231 231
232 232 def format_activity_title(text)
233 233 h(truncate_single_line_raw(text, 100))
234 234 end
235 235
236 236 def format_activity_day(date)
237 237 date == User.current.today ? l(:label_today).titleize : format_date(date)
238 238 end
239 239
240 240 def format_activity_description(text)
241 241 h(text.to_s.truncate(120).gsub(%r{[\r\n]*<(pre|code)>.*$}m, '...')
242 242 ).gsub(/[\r\n]+/, "<br />").html_safe
243 243 end
244 244
245 245 def format_version_name(version)
246 246 if version.project == @project
247 247 h(version)
248 248 else
249 249 h("#{version.project} - #{version}")
250 250 end
251 251 end
252 252
253 253 def due_date_distance_in_words(date)
254 254 if date
255 255 l((date < Date.today ? :label_roadmap_overdue : :label_roadmap_due_in), distance_of_date_in_words(Date.today, date))
256 256 end
257 257 end
258 258
259 259 # Renders a tree of projects as a nested set of unordered lists
260 260 # The given collection may be a subset of the whole project tree
261 261 # (eg. some intermediate nodes are private and can not be seen)
262 262 def render_project_nested_lists(projects, &block)
263 263 s = ''
264 264 if projects.any?
265 265 ancestors = []
266 266 original_project = @project
267 267 projects.sort_by(&:lft).each do |project|
268 268 # set the project environment to please macros.
269 269 @project = project
270 270 if (ancestors.empty? || project.is_descendant_of?(ancestors.last))
271 271 s << "<ul class='projects #{ ancestors.empty? ? 'root' : nil}'>\n"
272 272 else
273 273 ancestors.pop
274 274 s << "</li>"
275 275 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
276 276 ancestors.pop
277 277 s << "</ul></li>\n"
278 278 end
279 279 end
280 280 classes = (ancestors.empty? ? 'root' : 'child')
281 281 s << "<li class='#{classes}'><div class='#{classes}'>"
282 282 s << h(block_given? ? capture(project, &block) : project.name)
283 283 s << "</div>\n"
284 284 ancestors << project
285 285 end
286 286 s << ("</li></ul>\n" * ancestors.size)
287 287 @project = original_project
288 288 end
289 289 s.html_safe
290 290 end
291 291
292 292 def render_page_hierarchy(pages, node=nil, options={})
293 293 content = ''
294 294 if pages[node]
295 295 content << "<ul class=\"pages-hierarchy\">\n"
296 296 pages[node].each do |page|
297 297 content << "<li>"
298 298 content << link_to(h(page.pretty_title), {:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title, :version => nil},
299 299 :title => (options[:timestamp] && page.updated_on ? l(:label_updated_time, distance_of_time_in_words(Time.now, page.updated_on)) : nil))
300 300 content << "\n" + render_page_hierarchy(pages, page.id, options) if pages[page.id]
301 301 content << "</li>\n"
302 302 end
303 303 content << "</ul>\n"
304 304 end
305 305 content.html_safe
306 306 end
307 307
308 308 # Renders flash messages
309 309 def render_flash_messages
310 310 s = ''
311 311 flash.each do |k,v|
312 312 s << content_tag('div', v.html_safe, :class => "flash #{k}", :id => "flash_#{k}")
313 313 end
314 314 s.html_safe
315 315 end
316 316
317 317 # Renders tabs and their content
318 318 def render_tabs(tabs, selected=params[:tab])
319 319 if tabs.any?
320 320 unless tabs.detect {|tab| tab[:name] == selected}
321 321 selected = nil
322 322 end
323 323 selected ||= tabs.first[:name]
324 324 render :partial => 'common/tabs', :locals => {:tabs => tabs, :selected_tab => selected}
325 325 else
326 326 content_tag 'p', l(:label_no_data), :class => "nodata"
327 327 end
328 328 end
329 329
330 330 # Renders the project quick-jump box
331 331 def render_project_jump_box
332 332 return unless User.current.logged?
333 333 projects = User.current.projects.active.select(:id, :name, :identifier, :lft, :rgt).to_a
334 334 if projects.any?
335 335 options =
336 336 ("<option value=''>#{ l(:label_jump_to_a_project) }</option>" +
337 337 '<option value="" disabled="disabled">---</option>').html_safe
338 338
339 339 options << project_tree_options_for_select(projects, :selected => @project) do |p|
340 340 { :value => project_path(:id => p, :jump => current_menu_item) }
341 341 end
342 342
343 343 select_tag('project_quick_jump_box', options, :onchange => 'if (this.value != \'\') { window.location = this.value; }')
344 344 end
345 345 end
346 346
347 347 def project_tree_options_for_select(projects, options = {})
348 348 s = ''.html_safe
349 349 if blank_text = options[:include_blank]
350 350 if blank_text == true
351 351 blank_text = '&nbsp;'.html_safe
352 352 end
353 353 s << content_tag('option', blank_text, :value => '')
354 354 end
355 355 project_tree(projects) do |project, level|
356 356 name_prefix = (level > 0 ? '&nbsp;' * 2 * level + '&#187; ' : '').html_safe
357 357 tag_options = {:value => project.id}
358 358 if project == options[:selected] || (options[:selected].respond_to?(:include?) && options[:selected].include?(project))
359 359 tag_options[:selected] = 'selected'
360 360 else
361 361 tag_options[:selected] = nil
362 362 end
363 363 tag_options.merge!(yield(project)) if block_given?
364 364 s << content_tag('option', name_prefix + h(project), tag_options)
365 365 end
366 366 s.html_safe
367 367 end
368 368
369 369 # Yields the given block for each project with its level in the tree
370 370 #
371 371 # Wrapper for Project#project_tree
372 372 def project_tree(projects, &block)
373 373 Project.project_tree(projects, &block)
374 374 end
375 375
376 376 def principals_check_box_tags(name, principals)
377 377 s = ''
378 378 principals.each do |principal|
379 379 s << "<label>#{ check_box_tag name, principal.id, false, :id => nil } #{h principal}</label>\n"
380 380 end
381 381 s.html_safe
382 382 end
383 383
384 384 # Returns a string for users/groups option tags
385 385 def principals_options_for_select(collection, selected=nil)
386 386 s = ''
387 387 if collection.include?(User.current)
388 388 s << content_tag('option', "<< #{l(:label_me)} >>", :value => User.current.id)
389 389 end
390 390 groups = ''
391 391 collection.sort.each do |element|
392 392 selected_attribute = ' selected="selected"' if option_value_selected?(element, selected) || element.id.to_s == selected
393 393 (element.is_a?(Group) ? groups : s) << %(<option value="#{element.id}"#{selected_attribute}>#{h element.name}</option>)
394 394 end
395 395 unless groups.empty?
396 396 s << %(<optgroup label="#{h(l(:label_group_plural))}">#{groups}</optgroup>)
397 397 end
398 398 s.html_safe
399 399 end
400 400
401 401 def option_tag(name, text, value, selected=nil, options={})
402 402 content_tag 'option', value, options.merge(:value => value, :selected => (value == selected))
403 403 end
404 404
405 405 def truncate_single_line_raw(string, length)
406 406 string.to_s.truncate(length).gsub(%r{[\r\n]+}m, ' ')
407 407 end
408 408
409 409 # Truncates at line break after 250 characters or options[:length]
410 410 def truncate_lines(string, options={})
411 411 length = options[:length] || 250
412 412 if string.to_s =~ /\A(.{#{length}}.*?)$/m
413 413 "#{$1}..."
414 414 else
415 415 string
416 416 end
417 417 end
418 418
419 419 def anchor(text)
420 420 text.to_s.gsub(' ', '_')
421 421 end
422 422
423 423 def html_hours(text)
424 424 text.gsub(%r{(\d+)\.(\d+)}, '<span class="hours hours-int">\1</span><span class="hours hours-dec">.\2</span>').html_safe
425 425 end
426 426
427 427 def authoring(created, author, options={})
428 428 l(options[:label] || :label_added_time_by, :author => link_to_user(author), :age => time_tag(created)).html_safe
429 429 end
430 430
431 431 def time_tag(time)
432 432 text = distance_of_time_in_words(Time.now, time)
433 433 if @project
434 434 link_to(text, project_activity_path(@project, :from => User.current.time_to_date(time)), :title => format_time(time))
435 435 else
436 436 content_tag('abbr', text, :title => format_time(time))
437 437 end
438 438 end
439 439
440 440 def syntax_highlight_lines(name, content)
441 441 lines = []
442 442 syntax_highlight(name, content).each_line { |line| lines << line }
443 443 lines
444 444 end
445 445
446 446 def syntax_highlight(name, content)
447 447 Redmine::SyntaxHighlighting.highlight_by_filename(content, name)
448 448 end
449 449
450 450 def to_path_param(path)
451 451 str = path.to_s.split(%r{[/\\]}).select{|p| !p.blank?}.join("/")
452 452 str.blank? ? nil : str
453 453 end
454 454
455 455 def reorder_links(name, url, method = :post)
456 456 link_to(image_tag('2uparrow.png', :alt => l(:label_sort_highest)),
457 457 url.merge({"#{name}[move_to]" => 'highest'}),
458 458 :method => method, :title => l(:label_sort_highest)) +
459 459 link_to(image_tag('1uparrow.png', :alt => l(:label_sort_higher)),
460 460 url.merge({"#{name}[move_to]" => 'higher'}),
461 461 :method => method, :title => l(:label_sort_higher)) +
462 462 link_to(image_tag('1downarrow.png', :alt => l(:label_sort_lower)),
463 463 url.merge({"#{name}[move_to]" => 'lower'}),
464 464 :method => method, :title => l(:label_sort_lower)) +
465 465 link_to(image_tag('2downarrow.png', :alt => l(:label_sort_lowest)),
466 466 url.merge({"#{name}[move_to]" => 'lowest'}),
467 467 :method => method, :title => l(:label_sort_lowest))
468 468 end
469 469
470 470 def breadcrumb(*args)
471 471 elements = args.flatten
472 472 elements.any? ? content_tag('p', (args.join(" \xc2\xbb ") + " \xc2\xbb ").html_safe, :class => 'breadcrumb') : nil
473 473 end
474 474
475 475 def other_formats_links(&block)
476 476 concat('<p class="other-formats">'.html_safe + l(:label_export_to))
477 477 yield Redmine::Views::OtherFormatsBuilder.new(self)
478 478 concat('</p>'.html_safe)
479 479 end
480 480
481 481 def page_header_title
482 482 if @project.nil? || @project.new_record?
483 483 h(Setting.app_title)
484 484 else
485 485 b = []
486 486 ancestors = (@project.root? ? [] : @project.ancestors.visible.to_a)
487 487 if ancestors.any?
488 488 root = ancestors.shift
489 489 b << link_to_project(root, {:jump => current_menu_item}, :class => 'root')
490 490 if ancestors.size > 2
491 491 b << "\xe2\x80\xa6"
492 492 ancestors = ancestors[-2, 2]
493 493 end
494 494 b += ancestors.collect {|p| link_to_project(p, {:jump => current_menu_item}, :class => 'ancestor') }
495 495 end
496 496 b << h(@project)
497 497 b.join(" \xc2\xbb ").html_safe
498 498 end
499 499 end
500 500
501 501 # Returns a h2 tag and sets the html title with the given arguments
502 502 def title(*args)
503 503 strings = args.map do |arg|
504 504 if arg.is_a?(Array) && arg.size >= 2
505 505 link_to(*arg)
506 506 else
507 507 h(arg.to_s)
508 508 end
509 509 end
510 510 html_title args.reverse.map {|s| (s.is_a?(Array) ? s.first : s).to_s}
511 511 content_tag('h2', strings.join(' &#187; ').html_safe)
512 512 end
513 513
514 514 # Sets the html title
515 515 # Returns the html title when called without arguments
516 516 # Current project name and app_title and automatically appended
517 517 # Exemples:
518 518 # html_title 'Foo', 'Bar'
519 519 # html_title # => 'Foo - Bar - My Project - Redmine'
520 520 def html_title(*args)
521 521 if args.empty?
522 522 title = @html_title || []
523 523 title << @project.name if @project
524 524 title << Setting.app_title unless Setting.app_title == title.last
525 525 title.reject(&:blank?).join(' - ')
526 526 else
527 527 @html_title ||= []
528 528 @html_title += args
529 529 end
530 530 end
531 531
532 532 # Returns the theme, controller name, and action as css classes for the
533 533 # HTML body.
534 534 def body_css_classes
535 535 css = []
536 536 if theme = Redmine::Themes.theme(Setting.ui_theme)
537 537 css << 'theme-' + theme.name
538 538 end
539 539
540 540 css << 'project-' + @project.identifier if @project && @project.identifier.present?
541 541 css << 'controller-' + controller_name
542 542 css << 'action-' + action_name
543 543 css.join(' ')
544 544 end
545 545
546 546 def accesskey(s)
547 547 @used_accesskeys ||= []
548 548 key = Redmine::AccessKeys.key_for(s)
549 549 return nil if @used_accesskeys.include?(key)
550 550 @used_accesskeys << key
551 551 key
552 552 end
553 553
554 554 # Formats text according to system settings.
555 555 # 2 ways to call this method:
556 556 # * with a String: textilizable(text, options)
557 557 # * with an object and one of its attribute: textilizable(issue, :description, options)
558 558 def textilizable(*args)
559 559 options = args.last.is_a?(Hash) ? args.pop : {}
560 560 case args.size
561 561 when 1
562 562 obj = options[:object]
563 563 text = args.shift
564 564 when 2
565 565 obj = args.shift
566 566 attr = args.shift
567 567 text = obj.send(attr).to_s
568 568 else
569 569 raise ArgumentError, 'invalid arguments to textilizable'
570 570 end
571 571 return '' if text.blank?
572 572 project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
573 573 @only_path = only_path = options.delete(:only_path) == false ? false : true
574 574
575 575 text = text.dup
576 576 macros = catch_macros(text)
577 577 text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text, :object => obj, :attribute => attr)
578 578
579 579 @parsed_headings = []
580 580 @heading_anchors = {}
581 581 @current_section = 0 if options[:edit_section_links]
582 582
583 583 parse_sections(text, project, obj, attr, only_path, options)
584 584 text = parse_non_pre_blocks(text, obj, macros) do |text|
585 585 [:parse_inline_attachments, :parse_wiki_links, :parse_redmine_links].each do |method_name|
586 586 send method_name, text, project, obj, attr, only_path, options
587 587 end
588 588 end
589 589 parse_headings(text, project, obj, attr, only_path, options)
590 590
591 591 if @parsed_headings.any?
592 592 replace_toc(text, @parsed_headings)
593 593 end
594 594
595 595 text.html_safe
596 596 end
597 597
598 598 def parse_non_pre_blocks(text, obj, macros)
599 599 s = StringScanner.new(text)
600 600 tags = []
601 601 parsed = ''
602 602 while !s.eos?
603 603 s.scan(/(.*?)(<(\/)?(pre|code)(.*?)>|\z)/im)
604 604 text, full_tag, closing, tag = s[1], s[2], s[3], s[4]
605 605 if tags.empty?
606 606 yield text
607 607 inject_macros(text, obj, macros) if macros.any?
608 608 else
609 609 inject_macros(text, obj, macros, false) if macros.any?
610 610 end
611 611 parsed << text
612 612 if tag
613 613 if closing
614 614 if tags.last && tags.last.casecmp(tag) == 0
615 615 tags.pop
616 616 end
617 617 else
618 618 tags << tag.downcase
619 619 end
620 620 parsed << full_tag
621 621 end
622 622 end
623 623 # Close any non closing tags
624 624 while tag = tags.pop
625 625 parsed << "</#{tag}>"
626 626 end
627 627 parsed
628 628 end
629 629
630 630 def parse_inline_attachments(text, project, obj, attr, only_path, options)
631 631 return if options[:inline_attachments] == false
632 632
633 633 # when using an image link, try to use an attachment, if possible
634 634 attachments = options[:attachments] || []
635 635 attachments += obj.attachments if obj.respond_to?(:attachments)
636 636 if attachments.present?
637 637 text.gsub!(/src="([^\/"]+\.(bmp|gif|jpg|jpe|jpeg|png))"(\s+alt="([^"]*)")?/i) do |m|
638 638 filename, ext, alt, alttext = $1.downcase, $2, $3, $4
639 639 # search for the picture in attachments
640 640 if found = Attachment.latest_attach(attachments, CGI.unescape(filename))
641 641 image_url = download_named_attachment_url(found, found.filename, :only_path => only_path)
642 642 desc = found.description.to_s.gsub('"', '')
643 643 if !desc.blank? && alttext.blank?
644 644 alt = " title=\"#{desc}\" alt=\"#{desc}\""
645 645 end
646 646 "src=\"#{image_url}\"#{alt}"
647 647 else
648 648 m
649 649 end
650 650 end
651 651 end
652 652 end
653 653
654 654 # Wiki links
655 655 #
656 656 # Examples:
657 657 # [[mypage]]
658 658 # [[mypage|mytext]]
659 659 # wiki links can refer other project wikis, using project name or identifier:
660 660 # [[project:]] -> wiki starting page
661 661 # [[project:|mytext]]
662 662 # [[project:mypage]]
663 663 # [[project:mypage|mytext]]
664 664 def parse_wiki_links(text, project, obj, attr, only_path, options)
665 665 text.gsub!(/(!)?(\[\[([^\]\n\|]+)(\|([^\]\n\|]+))?\]\])/) do |m|
666 666 link_project = project
667 667 esc, all, page, title = $1, $2, $3, $5
668 668 if esc.nil?
669 669 if page =~ /^([^\:]+)\:(.*)$/
670 670 identifier, page = $1, $2
671 671 link_project = Project.find_by_identifier(identifier) || Project.find_by_name(identifier)
672 672 title ||= identifier if page.blank?
673 673 end
674 674
675 675 if link_project && link_project.wiki
676 676 # extract anchor
677 677 anchor = nil
678 678 if page =~ /^(.+?)\#(.+)$/
679 679 page, anchor = $1, $2
680 680 end
681 681 anchor = sanitize_anchor_name(anchor) if anchor.present?
682 682 # check if page exists
683 683 wiki_page = link_project.wiki.find_page(page)
684 684 url = if anchor.present? && wiki_page.present? && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version)) && obj.page == wiki_page
685 685 "##{anchor}"
686 686 else
687 687 case options[:wiki_links]
688 688 when :local; "#{page.present? ? Wiki.titleize(page) : ''}.html" + (anchor.present? ? "##{anchor}" : '')
689 689 when :anchor; "##{page.present? ? Wiki.titleize(page) : title}" + (anchor.present? ? "_#{anchor}" : '') # used for single-file wiki export
690 690 else
691 691 wiki_page_id = page.present? ? Wiki.titleize(page) : nil
692 692 parent = wiki_page.nil? && obj.is_a?(WikiContent) && obj.page && project == link_project ? obj.page.title : nil
693 693 url_for(:only_path => only_path, :controller => 'wiki', :action => 'show', :project_id => link_project,
694 694 :id => wiki_page_id, :version => nil, :anchor => anchor, :parent => parent)
695 695 end
696 696 end
697 697 link_to(title.present? ? title.html_safe : h(page), url, :class => ('wiki-page' + (wiki_page ? '' : ' new')))
698 698 else
699 699 # project or wiki doesn't exist
700 700 all
701 701 end
702 702 else
703 703 all
704 704 end
705 705 end
706 706 end
707 707
708 708 # Redmine links
709 709 #
710 710 # Examples:
711 711 # Issues:
712 712 # #52 -> Link to issue #52
713 713 # Changesets:
714 714 # r52 -> Link to revision 52
715 715 # commit:a85130f -> Link to scmid starting with a85130f
716 716 # Documents:
717 717 # document#17 -> Link to document with id 17
718 718 # document:Greetings -> Link to the document with title "Greetings"
719 719 # document:"Some document" -> Link to the document with title "Some document"
720 720 # Versions:
721 721 # version#3 -> Link to version with id 3
722 722 # version:1.0.0 -> Link to version named "1.0.0"
723 723 # version:"1.0 beta 2" -> Link to version named "1.0 beta 2"
724 724 # Attachments:
725 725 # attachment:file.zip -> Link to the attachment of the current object named file.zip
726 726 # Source files:
727 727 # source:some/file -> Link to the file located at /some/file in the project's repository
728 728 # source:some/file@52 -> Link to the file's revision 52
729 729 # source:some/file#L120 -> Link to line 120 of the file
730 730 # source:some/file@52#L120 -> Link to line 120 of the file's revision 52
731 731 # export:some/file -> Force the download of the file
732 732 # Forum messages:
733 733 # message#1218 -> Link to message with id 1218
734 734 # Projects:
735 735 # project:someproject -> Link to project named "someproject"
736 736 # project#3 -> Link to project with id 3
737 737 #
738 738 # Links can refer other objects from other projects, using project identifier:
739 739 # identifier:r52
740 740 # identifier:document:"Some document"
741 741 # identifier:version:1.0.0
742 742 # identifier:source:some/file
743 743 def parse_redmine_links(text, default_project, obj, attr, only_path, options)
744 744 text.gsub!(%r{<a( [^>]+?)?>(.*?)</a>|([\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|
745 tag_content, leading, esc, project_prefix, project_identifier, prefix, repo_prefix, repo_identifier, sep, identifier, comment_suffix, comment_id = $1, $3, $4, $5, $6, $7, $12, $13, $10 || $14 || $20, $16 || $21, $17, $19
745 tag_content, leading, esc, project_prefix, project_identifier, prefix, repo_prefix, repo_identifier, sep, identifier, comment_suffix, comment_id = $2, $3, $4, $5, $6, $7, $12, $13, $10 || $14 || $20, $16 || $21, $17, $19
746 746 if tag_content
747 747 $&
748 748 else
749 749 link = nil
750 750 project = default_project
751 751 if project_identifier
752 752 project = Project.visible.find_by_identifier(project_identifier)
753 753 end
754 754 if esc.nil?
755 755 if prefix.nil? && sep == 'r'
756 756 if project
757 757 repository = nil
758 758 if repo_identifier
759 759 repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
760 760 else
761 761 repository = project.repository
762 762 end
763 763 # project.changesets.visible raises an SQL error because of a double join on repositories
764 764 if repository &&
765 765 (changeset = Changeset.visible.
766 766 find_by_repository_id_and_revision(repository.id, identifier))
767 767 link = link_to(h("#{project_prefix}#{repo_prefix}r#{identifier}"),
768 768 {:only_path => only_path, :controller => 'repositories',
769 769 :action => 'revision', :id => project,
770 770 :repository_id => repository.identifier_param,
771 771 :rev => changeset.revision},
772 772 :class => 'changeset',
773 773 :title => truncate_single_line_raw(changeset.comments, 100))
774 774 end
775 775 end
776 776 elsif sep == '#'
777 777 oid = identifier.to_i
778 778 case prefix
779 779 when nil
780 780 if oid.to_s == identifier &&
781 781 issue = Issue.visible.find_by_id(oid)
782 782 anchor = comment_id ? "note-#{comment_id}" : nil
783 783 link = link_to("##{oid}#{comment_suffix}",
784 784 issue_url(issue, :only_path => only_path, :anchor => anchor),
785 785 :class => issue.css_classes,
786 786 :title => "#{issue.tracker.name}: #{issue.subject.truncate(100)} (#{issue.status.name})")
787 787 end
788 788 when 'document'
789 789 if document = Document.visible.find_by_id(oid)
790 790 link = link_to(document.title, document_url(document, :only_path => only_path), :class => 'document')
791 791 end
792 792 when 'version'
793 793 if version = Version.visible.find_by_id(oid)
794 794 link = link_to(version.name, version_url(version, :only_path => only_path), :class => 'version')
795 795 end
796 796 when 'message'
797 797 if message = Message.visible.find_by_id(oid)
798 798 link = link_to_message(message, {:only_path => only_path}, :class => 'message')
799 799 end
800 800 when 'forum'
801 801 if board = Board.visible.find_by_id(oid)
802 802 link = link_to(board.name, project_board_url(board.project, board, :only_path => only_path), :class => 'board')
803 803 end
804 804 when 'news'
805 805 if news = News.visible.find_by_id(oid)
806 806 link = link_to(news.title, news_url(news, :only_path => only_path), :class => 'news')
807 807 end
808 808 when 'project'
809 809 if p = Project.visible.find_by_id(oid)
810 810 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
811 811 end
812 812 end
813 813 elsif sep == ':'
814 814 # removes the double quotes if any
815 815 name = identifier.gsub(%r{^"(.*)"$}, "\\1")
816 816 name = CGI.unescapeHTML(name)
817 817 case prefix
818 818 when 'document'
819 819 if project && document = project.documents.visible.find_by_title(name)
820 820 link = link_to(document.title, document_url(document, :only_path => only_path), :class => 'document')
821 821 end
822 822 when 'version'
823 823 if project && version = project.versions.visible.find_by_name(name)
824 824 link = link_to(version.name, version_url(version, :only_path => only_path), :class => 'version')
825 825 end
826 826 when 'forum'
827 827 if project && board = project.boards.visible.find_by_name(name)
828 828 link = link_to(board.name, project_board_url(board.project, board, :only_path => only_path), :class => 'board')
829 829 end
830 830 when 'news'
831 831 if project && news = project.news.visible.find_by_title(name)
832 832 link = link_to(news.title, news_url(news, :only_path => only_path), :class => 'news')
833 833 end
834 834 when 'commit', 'source', 'export'
835 835 if project
836 836 repository = nil
837 837 if name =~ %r{^(([a-z0-9\-_]+)\|)(.+)$}
838 838 repo_prefix, repo_identifier, name = $1, $2, $3
839 839 repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
840 840 else
841 841 repository = project.repository
842 842 end
843 843 if prefix == 'commit'
844 844 if repository && (changeset = Changeset.visible.where("repository_id = ? AND scmid LIKE ?", repository.id, "#{name}%").first)
845 845 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},
846 846 :class => 'changeset',
847 847 :title => truncate_single_line_raw(changeset.comments, 100)
848 848 end
849 849 else
850 850 if repository && User.current.allowed_to?(:browse_repository, project)
851 851 name =~ %r{^[/\\]*(.*?)(@([^/\\@]+?))?(#(L\d+))?$}
852 852 path, rev, anchor = $1, $3, $5
853 853 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,
854 854 :path => to_path_param(path),
855 855 :rev => rev,
856 856 :anchor => anchor},
857 857 :class => (prefix == 'export' ? 'source download' : 'source')
858 858 end
859 859 end
860 860 repo_prefix = nil
861 861 end
862 862 when 'attachment'
863 863 attachments = options[:attachments] || []
864 864 attachments += obj.attachments if obj.respond_to?(:attachments)
865 865 if attachments && attachment = Attachment.latest_attach(attachments, name)
866 866 link = link_to_attachment(attachment, :only_path => only_path, :download => true, :class => 'attachment')
867 867 end
868 868 when 'project'
869 869 if p = Project.visible.where("identifier = :s OR LOWER(name) = :s", :s => name.downcase).first
870 870 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
871 871 end
872 872 end
873 873 end
874 874 end
875 875 (leading + (link || "#{project_prefix}#{prefix}#{repo_prefix}#{sep}#{identifier}#{comment_suffix}"))
876 876 end
877 877 end
878 878 end
879 879
880 880 HEADING_RE = /(<h(\d)( [^>]+)?>(.+?)<\/h(\d)>)/i unless const_defined?(:HEADING_RE)
881 881
882 882 def parse_sections(text, project, obj, attr, only_path, options)
883 883 return unless options[:edit_section_links]
884 884 text.gsub!(HEADING_RE) do
885 885 heading = $1
886 886 @current_section += 1
887 887 if @current_section > 1
888 888 content_tag('div',
889 889 link_to(image_tag('edit.png'), options[:edit_section_links].merge(:section => @current_section)),
890 890 :class => 'contextual',
891 891 :title => l(:button_edit_section),
892 892 :id => "section-#{@current_section}") + heading.html_safe
893 893 else
894 894 heading
895 895 end
896 896 end
897 897 end
898 898
899 899 # Headings and TOC
900 900 # Adds ids and links to headings unless options[:headings] is set to false
901 901 def parse_headings(text, project, obj, attr, only_path, options)
902 902 return if options[:headings] == false
903 903
904 904 text.gsub!(HEADING_RE) do
905 905 level, attrs, content = $2.to_i, $3, $4
906 906 item = strip_tags(content).strip
907 907 anchor = sanitize_anchor_name(item)
908 908 # used for single-file wiki export
909 909 anchor = "#{obj.page.title}_#{anchor}" if options[:wiki_links] == :anchor && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version))
910 910 @heading_anchors[anchor] ||= 0
911 911 idx = (@heading_anchors[anchor] += 1)
912 912 if idx > 1
913 913 anchor = "#{anchor}-#{idx}"
914 914 end
915 915 @parsed_headings << [level, anchor, item]
916 916 "<a name=\"#{anchor}\"></a>\n<h#{level} #{attrs}>#{content}<a href=\"##{anchor}\" class=\"wiki-anchor\">&para;</a></h#{level}>"
917 917 end
918 918 end
919 919
920 920 MACROS_RE = /(
921 921 (!)? # escaping
922 922 (
923 923 \{\{ # opening tag
924 924 ([\w]+) # macro name
925 925 (\(([^\n\r]*?)\))? # optional arguments
926 926 ([\n\r].*?[\n\r])? # optional block of text
927 927 \}\} # closing tag
928 928 )
929 929 )/mx unless const_defined?(:MACROS_RE)
930 930
931 931 MACRO_SUB_RE = /(
932 932 \{\{
933 933 macro\((\d+)\)
934 934 \}\}
935 935 )/x unless const_defined?(:MACRO_SUB_RE)
936 936
937 937 # Extracts macros from text
938 938 def catch_macros(text)
939 939 macros = {}
940 940 text.gsub!(MACROS_RE) do
941 941 all, macro = $1, $4.downcase
942 942 if macro_exists?(macro) || all =~ MACRO_SUB_RE
943 943 index = macros.size
944 944 macros[index] = all
945 945 "{{macro(#{index})}}"
946 946 else
947 947 all
948 948 end
949 949 end
950 950 macros
951 951 end
952 952
953 953 # Executes and replaces macros in text
954 954 def inject_macros(text, obj, macros, execute=true)
955 955 text.gsub!(MACRO_SUB_RE) do
956 956 all, index = $1, $2.to_i
957 957 orig = macros.delete(index)
958 958 if execute && orig && orig =~ MACROS_RE
959 959 esc, all, macro, args, block = $2, $3, $4.downcase, $6.to_s, $7.try(:strip)
960 960 if esc.nil?
961 961 h(exec_macro(macro, obj, args, block) || all)
962 962 else
963 963 h(all)
964 964 end
965 965 elsif orig
966 966 h(orig)
967 967 else
968 968 h(all)
969 969 end
970 970 end
971 971 end
972 972
973 973 TOC_RE = /<p>\{\{((<|&lt;)|(>|&gt;))?toc\}\}<\/p>/i unless const_defined?(:TOC_RE)
974 974
975 975 # Renders the TOC with given headings
976 976 def replace_toc(text, headings)
977 977 text.gsub!(TOC_RE) do
978 978 left_align, right_align = $2, $3
979 979 # Keep only the 4 first levels
980 980 headings = headings.select{|level, anchor, item| level <= 4}
981 981 if headings.empty?
982 982 ''
983 983 else
984 984 div_class = 'toc'
985 985 div_class << ' right' if right_align
986 986 div_class << ' left' if left_align
987 987 out = "<ul class=\"#{div_class}\"><li>"
988 988 root = headings.map(&:first).min
989 989 current = root
990 990 started = false
991 991 headings.each do |level, anchor, item|
992 992 if level > current
993 993 out << '<ul><li>' * (level - current)
994 994 elsif level < current
995 995 out << "</li></ul>\n" * (current - level) + "</li><li>"
996 996 elsif started
997 997 out << '</li><li>'
998 998 end
999 999 out << "<a href=\"##{anchor}\">#{item}</a>"
1000 1000 current = level
1001 1001 started = true
1002 1002 end
1003 1003 out << '</li></ul>' * (current - root)
1004 1004 out << '</li></ul>'
1005 1005 end
1006 1006 end
1007 1007 end
1008 1008
1009 1009 # Same as Rails' simple_format helper without using paragraphs
1010 1010 def simple_format_without_paragraph(text)
1011 1011 text.to_s.
1012 1012 gsub(/\r\n?/, "\n"). # \r\n and \r -> \n
1013 1013 gsub(/\n\n+/, "<br /><br />"). # 2+ newline -> 2 br
1014 1014 gsub(/([^\n]\n)(?=[^\n])/, '\1<br />'). # 1 newline -> br
1015 1015 html_safe
1016 1016 end
1017 1017
1018 1018 def lang_options_for_select(blank=true)
1019 1019 (blank ? [["(auto)", ""]] : []) + languages_options
1020 1020 end
1021 1021
1022 1022 def labelled_form_for(*args, &proc)
1023 1023 args << {} unless args.last.is_a?(Hash)
1024 1024 options = args.last
1025 1025 if args.first.is_a?(Symbol)
1026 1026 options.merge!(:as => args.shift)
1027 1027 end
1028 1028 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
1029 1029 form_for(*args, &proc)
1030 1030 end
1031 1031
1032 1032 def labelled_fields_for(*args, &proc)
1033 1033 args << {} unless args.last.is_a?(Hash)
1034 1034 options = args.last
1035 1035 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
1036 1036 fields_for(*args, &proc)
1037 1037 end
1038 1038
1039 1039 def error_messages_for(*objects)
1040 1040 html = ""
1041 1041 objects = objects.map {|o| o.is_a?(String) ? instance_variable_get("@#{o}") : o}.compact
1042 1042 errors = objects.map {|o| o.errors.full_messages}.flatten
1043 1043 if errors.any?
1044 1044 html << "<div id='errorExplanation'><ul>\n"
1045 1045 errors.each do |error|
1046 1046 html << "<li>#{h error}</li>\n"
1047 1047 end
1048 1048 html << "</ul></div>\n"
1049 1049 end
1050 1050 html.html_safe
1051 1051 end
1052 1052
1053 1053 def delete_link(url, options={})
1054 1054 options = {
1055 1055 :method => :delete,
1056 1056 :data => {:confirm => l(:text_are_you_sure)},
1057 1057 :class => 'icon icon-del'
1058 1058 }.merge(options)
1059 1059
1060 1060 link_to l(:button_delete), url, options
1061 1061 end
1062 1062
1063 1063 def preview_link(url, form, target='preview', options={})
1064 1064 content_tag 'a', l(:label_preview), {
1065 1065 :href => "#",
1066 1066 :onclick => %|submitPreview("#{escape_javascript url_for(url)}", "#{escape_javascript form}", "#{escape_javascript target}"); return false;|,
1067 1067 :accesskey => accesskey(:preview)
1068 1068 }.merge(options)
1069 1069 end
1070 1070
1071 1071 def link_to_function(name, function, html_options={})
1072 1072 content_tag(:a, name, {:href => '#', :onclick => "#{function}; return false;"}.merge(html_options))
1073 1073 end
1074 1074
1075 1075 # Helper to render JSON in views
1076 1076 def raw_json(arg)
1077 1077 arg.to_json.to_s.gsub('/', '\/').html_safe
1078 1078 end
1079 1079
1080 1080 def back_url
1081 1081 url = params[:back_url]
1082 1082 if url.nil? && referer = request.env['HTTP_REFERER']
1083 1083 url = CGI.unescape(referer.to_s)
1084 1084 end
1085 1085 url
1086 1086 end
1087 1087
1088 1088 def back_url_hidden_field_tag
1089 1089 url = back_url
1090 1090 hidden_field_tag('back_url', url, :id => nil) unless url.blank?
1091 1091 end
1092 1092
1093 1093 def check_all_links(form_name)
1094 1094 link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") +
1095 1095 " | ".html_safe +
1096 1096 link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)")
1097 1097 end
1098 1098
1099 1099 def toggle_checkboxes_link(selector)
1100 1100 link_to_function image_tag('toggle_check.png'),
1101 1101 "toggleCheckboxesBySelector('#{selector}')",
1102 1102 :title => "#{l(:button_check_all)} / #{l(:button_uncheck_all)}"
1103 1103 end
1104 1104
1105 1105 def progress_bar(pcts, options={})
1106 1106 pcts = [pcts, pcts] unless pcts.is_a?(Array)
1107 1107 pcts = pcts.collect(&:round)
1108 1108 pcts[1] = pcts[1] - pcts[0]
1109 1109 pcts << (100 - pcts[1] - pcts[0])
1110 1110 width = options[:width] || '100px;'
1111 1111 legend = options[:legend] || ''
1112 1112 content_tag('table',
1113 1113 content_tag('tr',
1114 1114 (pcts[0] > 0 ? content_tag('td', '', :style => "width: #{pcts[0]}%;", :class => 'closed') : ''.html_safe) +
1115 1115 (pcts[1] > 0 ? content_tag('td', '', :style => "width: #{pcts[1]}%;", :class => 'done') : ''.html_safe) +
1116 1116 (pcts[2] > 0 ? content_tag('td', '', :style => "width: #{pcts[2]}%;", :class => 'todo') : ''.html_safe)
1117 1117 ), :class => "progress progress-#{pcts[0]}", :style => "width: #{width};").html_safe +
1118 1118 content_tag('p', legend, :class => 'percent').html_safe
1119 1119 end
1120 1120
1121 1121 def checked_image(checked=true)
1122 1122 if checked
1123 1123 @checked_image_tag ||= image_tag('toggle_check.png')
1124 1124 end
1125 1125 end
1126 1126
1127 1127 def context_menu(url)
1128 1128 unless @context_menu_included
1129 1129 content_for :header_tags do
1130 1130 javascript_include_tag('context_menu') +
1131 1131 stylesheet_link_tag('context_menu')
1132 1132 end
1133 1133 if l(:direction) == 'rtl'
1134 1134 content_for :header_tags do
1135 1135 stylesheet_link_tag('context_menu_rtl')
1136 1136 end
1137 1137 end
1138 1138 @context_menu_included = true
1139 1139 end
1140 1140 javascript_tag "contextMenuInit('#{ url_for(url) }')"
1141 1141 end
1142 1142
1143 1143 def calendar_for(field_id)
1144 1144 include_calendar_headers_tags
1145 1145 javascript_tag("$(function() { $('##{field_id}').datepicker(datepickerOptions); });")
1146 1146 end
1147 1147
1148 1148 def include_calendar_headers_tags
1149 1149 unless @calendar_headers_tags_included
1150 1150 tags = ''.html_safe
1151 1151 @calendar_headers_tags_included = true
1152 1152 content_for :header_tags do
1153 1153 start_of_week = Setting.start_of_week
1154 1154 start_of_week = l(:general_first_day_of_week, :default => '1') if start_of_week.blank?
1155 1155 # Redmine uses 1..7 (monday..sunday) in settings and locales
1156 1156 # JQuery uses 0..6 (sunday..saturday), 7 needs to be changed to 0
1157 1157 start_of_week = start_of_week.to_i % 7
1158 1158 tags << javascript_tag(
1159 1159 "var datepickerOptions={dateFormat: 'yy-mm-dd', firstDay: #{start_of_week}, " +
1160 1160 "showOn: 'button', buttonImageOnly: true, buttonImage: '" +
1161 1161 path_to_image('/images/calendar.png') +
1162 1162 "', showButtonPanel: true, showWeek: true, showOtherMonths: true, " +
1163 1163 "selectOtherMonths: true, changeMonth: true, changeYear: true, " +
1164 1164 "beforeShow: beforeShowDatePicker};")
1165 1165 jquery_locale = l('jquery.locale', :default => current_language.to_s)
1166 1166 unless jquery_locale == 'en'
1167 1167 tags << javascript_include_tag("i18n/datepicker-#{jquery_locale}.js")
1168 1168 end
1169 1169 tags
1170 1170 end
1171 1171 end
1172 1172 end
1173 1173
1174 1174 # Overrides Rails' stylesheet_link_tag with themes and plugins support.
1175 1175 # Examples:
1176 1176 # stylesheet_link_tag('styles') # => picks styles.css from the current theme or defaults
1177 1177 # stylesheet_link_tag('styles', :plugin => 'foo) # => picks styles.css from plugin's assets
1178 1178 #
1179 1179 def stylesheet_link_tag(*sources)
1180 1180 options = sources.last.is_a?(Hash) ? sources.pop : {}
1181 1181 plugin = options.delete(:plugin)
1182 1182 sources = sources.map do |source|
1183 1183 if plugin
1184 1184 "/plugin_assets/#{plugin}/stylesheets/#{source}"
1185 1185 elsif current_theme && current_theme.stylesheets.include?(source)
1186 1186 current_theme.stylesheet_path(source)
1187 1187 else
1188 1188 source
1189 1189 end
1190 1190 end
1191 1191 super *sources, options
1192 1192 end
1193 1193
1194 1194 # Overrides Rails' image_tag with themes and plugins support.
1195 1195 # Examples:
1196 1196 # image_tag('image.png') # => picks image.png from the current theme or defaults
1197 1197 # image_tag('image.png', :plugin => 'foo) # => picks image.png from plugin's assets
1198 1198 #
1199 1199 def image_tag(source, options={})
1200 1200 if plugin = options.delete(:plugin)
1201 1201 source = "/plugin_assets/#{plugin}/images/#{source}"
1202 1202 elsif current_theme && current_theme.images.include?(source)
1203 1203 source = current_theme.image_path(source)
1204 1204 end
1205 1205 super source, options
1206 1206 end
1207 1207
1208 1208 # Overrides Rails' javascript_include_tag with plugins support
1209 1209 # Examples:
1210 1210 # javascript_include_tag('scripts') # => picks scripts.js from defaults
1211 1211 # javascript_include_tag('scripts', :plugin => 'foo) # => picks scripts.js from plugin's assets
1212 1212 #
1213 1213 def javascript_include_tag(*sources)
1214 1214 options = sources.last.is_a?(Hash) ? sources.pop : {}
1215 1215 if plugin = options.delete(:plugin)
1216 1216 sources = sources.map do |source|
1217 1217 if plugin
1218 1218 "/plugin_assets/#{plugin}/javascripts/#{source}"
1219 1219 else
1220 1220 source
1221 1221 end
1222 1222 end
1223 1223 end
1224 1224 super *sources, options
1225 1225 end
1226 1226
1227 1227 def sidebar_content?
1228 1228 content_for?(:sidebar) || view_layouts_base_sidebar_hook_response.present?
1229 1229 end
1230 1230
1231 1231 def view_layouts_base_sidebar_hook_response
1232 1232 @view_layouts_base_sidebar_hook_response ||= call_hook(:view_layouts_base_sidebar)
1233 1233 end
1234 1234
1235 1235 def email_delivery_enabled?
1236 1236 !!ActionMailer::Base.perform_deliveries
1237 1237 end
1238 1238
1239 1239 # Returns the avatar image tag for the given +user+ if avatars are enabled
1240 1240 # +user+ can be a User or a string that will be scanned for an email address (eg. 'joe <joe@foo.bar>')
1241 1241 def avatar(user, options = { })
1242 1242 if Setting.gravatar_enabled?
1243 1243 options.merge!({:ssl => (request && request.ssl?), :default => Setting.gravatar_default})
1244 1244 email = nil
1245 1245 if user.respond_to?(:mail)
1246 1246 email = user.mail
1247 1247 elsif user.to_s =~ %r{<(.+?)>}
1248 1248 email = $1
1249 1249 end
1250 1250 return gravatar(email.to_s.downcase, options) unless email.blank? rescue nil
1251 1251 else
1252 1252 ''
1253 1253 end
1254 1254 end
1255 1255
1256 1256 # Returns a link to edit user's avatar if avatars are enabled
1257 1257 def avatar_edit_link(user, options={})
1258 1258 if Setting.gravatar_enabled?
1259 1259 url = "https://gravatar.com"
1260 1260 link_to avatar(user, {:title => l(:button_edit)}.merge(options)), url, :target => '_blank'
1261 1261 end
1262 1262 end
1263 1263
1264 1264 def sanitize_anchor_name(anchor)
1265 1265 anchor.gsub(%r{[^\s\-\p{Word}]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
1266 1266 end
1267 1267
1268 1268 # Returns the javascript tags that are included in the html layout head
1269 1269 def javascript_heads
1270 1270 tags = javascript_include_tag('jquery-1.11.1-ui-1.11.0-ujs-3.1.4', 'application')
1271 1271 unless User.current.pref.warn_on_leaving_unsaved == '0'
1272 1272 tags << "\n".html_safe + javascript_tag("$(window).load(function(){ warnLeavingUnsaved('#{escape_javascript l(:text_warn_on_leaving_unsaved)}'); });")
1273 1273 end
1274 1274 tags
1275 1275 end
1276 1276
1277 1277 def favicon
1278 1278 "<link rel='shortcut icon' href='#{favicon_path}' />".html_safe
1279 1279 end
1280 1280
1281 1281 # Returns the path to the favicon
1282 1282 def favicon_path
1283 1283 icon = (current_theme && current_theme.favicon?) ? current_theme.favicon_path : '/favicon.ico'
1284 1284 image_path(icon)
1285 1285 end
1286 1286
1287 1287 # Returns the full URL to the favicon
1288 1288 def favicon_url
1289 1289 # TODO: use #image_url introduced in Rails4
1290 1290 path = favicon_path
1291 1291 base = url_for(:controller => 'welcome', :action => 'index', :only_path => false)
1292 1292 base.sub(%r{/+$},'') + '/' + path.sub(%r{^/+},'')
1293 1293 end
1294 1294
1295 1295 def robot_exclusion_tag
1296 1296 '<meta name="robots" content="noindex,follow,noarchive" />'.html_safe
1297 1297 end
1298 1298
1299 1299 # Returns true if arg is expected in the API response
1300 1300 def include_in_api_response?(arg)
1301 1301 unless @included_in_api_response
1302 1302 param = params[:include]
1303 1303 @included_in_api_response = param.is_a?(Array) ? param.collect(&:to_s) : param.to_s.split(',')
1304 1304 @included_in_api_response.collect!(&:strip)
1305 1305 end
1306 1306 @included_in_api_response.include?(arg.to_s)
1307 1307 end
1308 1308
1309 1309 # Returns options or nil if nometa param or X-Redmine-Nometa header
1310 1310 # was set in the request
1311 1311 def api_meta(options)
1312 1312 if params[:nometa].present? || request.headers['X-Redmine-Nometa']
1313 1313 # compatibility mode for activeresource clients that raise
1314 1314 # an error when deserializing an array with attributes
1315 1315 nil
1316 1316 else
1317 1317 options
1318 1318 end
1319 1319 end
1320 1320
1321 1321 def generate_csv(&block)
1322 1322 decimal_separator = l(:general_csv_decimal_separator)
1323 1323 encoding = l(:general_csv_encoding)
1324 1324 end
1325 1325
1326 1326 private
1327 1327
1328 1328 def wiki_helper
1329 1329 helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
1330 1330 extend helper
1331 1331 return self
1332 1332 end
1333 1333
1334 1334 def link_to_content_update(text, url_params = {}, html_options = {})
1335 1335 link_to(text, url_params, html_options)
1336 1336 end
1337 1337 end
@@ -1,1534 +1,1541
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 4 # Copyright (C) 2006-2015 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 File.expand_path('../../../test_helper', __FILE__)
21 21
22 22 class ApplicationHelperTest < ActionView::TestCase
23 23 include Redmine::I18n
24 24 include ERB::Util
25 25 include Rails.application.routes.url_helpers
26 26
27 27 fixtures :projects, :roles, :enabled_modules, :users,
28 28 :email_addresses,
29 29 :repositories, :changesets,
30 30 :projects_trackers,
31 31 :trackers, :issue_statuses, :issues, :versions, :documents,
32 32 :wikis, :wiki_pages, :wiki_contents,
33 33 :boards, :messages, :news,
34 34 :attachments, :enumerations
35 35
36 36 def setup
37 37 super
38 38 set_tmp_attachments_directory
39 39 @russian_test = "\xd1\x82\xd0\xb5\xd1\x81\xd1\x82".force_encoding('UTF-8')
40 40 end
41 41
42 42 test "#link_to_if_authorized for authorized user should allow using the :controller and :action for the target link" do
43 43 User.current = User.find_by_login('admin')
44 44
45 45 @project = Issue.first.project # Used by helper
46 46 response = link_to_if_authorized('By controller/actionr',
47 47 {:controller => 'issues', :action => 'edit', :id => Issue.first.id})
48 48 assert_match /href/, response
49 49 end
50 50
51 51 test "#link_to_if_authorized for unauthorized user should display nothing if user isn't authorized" do
52 52 User.current = User.find_by_login('dlopper')
53 53 @project = Project.find('private-child')
54 54 issue = @project.issues.first
55 55 assert !issue.visible?
56 56
57 57 response = link_to_if_authorized('Never displayed',
58 58 {:controller => 'issues', :action => 'show', :id => issue})
59 59 assert_nil response
60 60 end
61 61
62 62 def test_auto_links
63 63 to_test = {
64 64 'http://foo.bar' => '<a class="external" href="http://foo.bar">http://foo.bar</a>',
65 65 'http://foo.bar/~user' => '<a class="external" href="http://foo.bar/~user">http://foo.bar/~user</a>',
66 66 'http://foo.bar.' => '<a class="external" href="http://foo.bar">http://foo.bar</a>.',
67 67 'https://foo.bar.' => '<a class="external" href="https://foo.bar">https://foo.bar</a>.',
68 68 'This is a link: http://foo.bar.' => 'This is a link: <a class="external" href="http://foo.bar">http://foo.bar</a>.',
69 69 'A link (eg. http://foo.bar).' => 'A link (eg. <a class="external" href="http://foo.bar">http://foo.bar</a>).',
70 70 'http://foo.bar/foo.bar#foo.bar.' => '<a class="external" href="http://foo.bar/foo.bar#foo.bar">http://foo.bar/foo.bar#foo.bar</a>.',
71 71 'http://www.foo.bar/Test_(foobar)' => '<a class="external" href="http://www.foo.bar/Test_(foobar)">http://www.foo.bar/Test_(foobar)</a>',
72 72 '(see inline link : http://www.foo.bar/Test_(foobar))' => '(see inline link : <a class="external" href="http://www.foo.bar/Test_(foobar)">http://www.foo.bar/Test_(foobar)</a>)',
73 73 '(see inline link : http://www.foo.bar/Test)' => '(see inline link : <a class="external" href="http://www.foo.bar/Test">http://www.foo.bar/Test</a>)',
74 74 '(see inline link : http://www.foo.bar/Test).' => '(see inline link : <a class="external" href="http://www.foo.bar/Test">http://www.foo.bar/Test</a>).',
75 75 '(see "inline link":http://www.foo.bar/Test_(foobar))' => '(see <a href="http://www.foo.bar/Test_(foobar)" class="external">inline link</a>)',
76 76 '(see "inline link":http://www.foo.bar/Test)' => '(see <a href="http://www.foo.bar/Test" class="external">inline link</a>)',
77 77 '(see "inline link":http://www.foo.bar/Test).' => '(see <a href="http://www.foo.bar/Test" class="external">inline link</a>).',
78 78 'www.foo.bar' => '<a class="external" href="http://www.foo.bar">www.foo.bar</a>',
79 79 'http://foo.bar/page?p=1&t=z&s=' => '<a class="external" href="http://foo.bar/page?p=1&#38;t=z&#38;s=">http://foo.bar/page?p=1&#38;t=z&#38;s=</a>',
80 80 'http://foo.bar/page#125' => '<a class="external" href="http://foo.bar/page#125">http://foo.bar/page#125</a>',
81 81 'http://foo@www.bar.com' => '<a class="external" href="http://foo@www.bar.com">http://foo@www.bar.com</a>',
82 82 'http://foo:bar@www.bar.com' => '<a class="external" href="http://foo:bar@www.bar.com">http://foo:bar@www.bar.com</a>',
83 83 'ftp://foo.bar' => '<a class="external" href="ftp://foo.bar">ftp://foo.bar</a>',
84 84 'ftps://foo.bar' => '<a class="external" href="ftps://foo.bar">ftps://foo.bar</a>',
85 85 'sftp://foo.bar' => '<a class="external" href="sftp://foo.bar">sftp://foo.bar</a>',
86 86 # two exclamation marks
87 87 'http://example.net/path!602815048C7B5C20!302.html' => '<a class="external" href="http://example.net/path!602815048C7B5C20!302.html">http://example.net/path!602815048C7B5C20!302.html</a>',
88 88 # escaping
89 89 'http://foo"bar' => '<a class="external" href="http://foo&quot;bar">http://foo&quot;bar</a>',
90 90 # wrap in angle brackets
91 91 '<http://foo.bar>' => '&lt;<a class="external" href="http://foo.bar">http://foo.bar</a>&gt;',
92 92 # invalid urls
93 93 'http://' => 'http://',
94 94 'www.' => 'www.',
95 95 'test-www.bar.com' => 'test-www.bar.com',
96 96 }
97 97 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
98 98 end
99 99
100 100 def test_auto_links_with_non_ascii_characters
101 101 to_test = {
102 102 "http://foo.bar/#{@russian_test}" =>
103 103 %|<a class="external" href="http://foo.bar/#{@russian_test}">http://foo.bar/#{@russian_test}</a>|
104 104 }
105 105 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
106 106 end
107 107
108 108 def test_auto_mailto
109 109 to_test = {
110 110 'test@foo.bar' => '<a class="email" href="mailto:test@foo.bar">test@foo.bar</a>',
111 111 'test@www.foo.bar' => '<a class="email" href="mailto:test@www.foo.bar">test@www.foo.bar</a>',
112 112 }
113 113 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
114 114 end
115 115
116 116 def test_inline_images
117 117 to_test = {
118 118 '!http://foo.bar/image.jpg!' => '<img src="http://foo.bar/image.jpg" alt="" />',
119 119 'floating !>http://foo.bar/image.jpg!' => 'floating <div style="float:right"><img src="http://foo.bar/image.jpg" alt="" /></div>',
120 120 'with class !(some-class)http://foo.bar/image.jpg!' => 'with class <img src="http://foo.bar/image.jpg" class="some-class" alt="" />',
121 121 'with style !{width:100px;height:100px}http://foo.bar/image.jpg!' => 'with style <img src="http://foo.bar/image.jpg" style="width:100px;height:100px;" alt="" />',
122 122 'with title !http://foo.bar/image.jpg(This is a title)!' => 'with title <img src="http://foo.bar/image.jpg" title="This is a title" alt="This is a title" />',
123 123 'with title !http://foo.bar/image.jpg(This is a double-quoted "title")!' => 'with title <img src="http://foo.bar/image.jpg" title="This is a double-quoted &quot;title&quot;" alt="This is a double-quoted &quot;title&quot;" />',
124 124 }
125 125 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
126 126 end
127 127
128 128 def test_inline_images_inside_tags
129 129 raw = <<-RAW
130 130 h1. !foo.png! Heading
131 131
132 132 Centered image:
133 133
134 134 p=. !bar.gif!
135 135 RAW
136 136
137 137 assert textilizable(raw).include?('<img src="foo.png" alt="" />')
138 138 assert textilizable(raw).include?('<img src="bar.gif" alt="" />')
139 139 end
140 140
141 141 def test_attached_images
142 142 to_test = {
143 143 'Inline image: !logo.gif!' => 'Inline image: <img src="/attachments/download/3/logo.gif" title="This is a logo" alt="This is a logo" />',
144 144 'Inline image: !logo.GIF!' => 'Inline image: <img src="/attachments/download/3/logo.gif" title="This is a logo" alt="This is a logo" />',
145 145 'No match: !ogo.gif!' => 'No match: <img src="ogo.gif" alt="" />',
146 146 'No match: !ogo.GIF!' => 'No match: <img src="ogo.GIF" alt="" />',
147 147 # link image
148 148 '!logo.gif!:http://foo.bar/' => '<a href="http://foo.bar/"><img src="/attachments/download/3/logo.gif" title="This is a logo" alt="This is a logo" /></a>',
149 149 }
150 150 attachments = Attachment.all
151 151 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text, :attachments => attachments) }
152 152 end
153 153
154 154 def test_attached_images_with_textile_and_non_ascii_filename
155 155 attachment = Attachment.generate!(:filename => 'cafΓ©.jpg')
156 156 with_settings :text_formatting => 'textile' do
157 157 assert_include %(<img src="/attachments/download/#{attachment.id}/caf%C3%A9.jpg" alt="" />),
158 158 textilizable("!cafΓ©.jpg!)", :attachments => [attachment])
159 159 end
160 160 end
161 161
162 162 def test_attached_images_with_markdown_and_non_ascii_filename
163 163 skip unless Object.const_defined?(:Redcarpet)
164 164
165 165 attachment = Attachment.generate!(:filename => 'cafΓ©.jpg')
166 166 with_settings :text_formatting => 'markdown' do
167 167 assert_include %(<img src="/attachments/download/#{attachment.id}/caf%C3%A9.jpg" alt="">),
168 168 textilizable("![](cafΓ©.jpg)", :attachments => [attachment])
169 169 end
170 170 end
171 171
172 172 def test_attached_images_filename_extension
173 173 set_tmp_attachments_directory
174 174 a1 = Attachment.new(
175 175 :container => Issue.find(1),
176 176 :file => mock_file_with_options({:original_filename => "testtest.JPG"}),
177 177 :author => User.find(1))
178 178 assert a1.save
179 179 assert_equal "testtest.JPG", a1.filename
180 180 assert_equal "image/jpeg", a1.content_type
181 181 assert a1.image?
182 182
183 183 a2 = Attachment.new(
184 184 :container => Issue.find(1),
185 185 :file => mock_file_with_options({:original_filename => "testtest.jpeg"}),
186 186 :author => User.find(1))
187 187 assert a2.save
188 188 assert_equal "testtest.jpeg", a2.filename
189 189 assert_equal "image/jpeg", a2.content_type
190 190 assert a2.image?
191 191
192 192 a3 = Attachment.new(
193 193 :container => Issue.find(1),
194 194 :file => mock_file_with_options({:original_filename => "testtest.JPE"}),
195 195 :author => User.find(1))
196 196 assert a3.save
197 197 assert_equal "testtest.JPE", a3.filename
198 198 assert_equal "image/jpeg", a3.content_type
199 199 assert a3.image?
200 200
201 201 a4 = Attachment.new(
202 202 :container => Issue.find(1),
203 203 :file => mock_file_with_options({:original_filename => "Testtest.BMP"}),
204 204 :author => User.find(1))
205 205 assert a4.save
206 206 assert_equal "Testtest.BMP", a4.filename
207 207 assert_equal "image/x-ms-bmp", a4.content_type
208 208 assert a4.image?
209 209
210 210 to_test = {
211 211 'Inline image: !testtest.jpg!' =>
212 212 'Inline image: <img src="/attachments/download/' + a1.id.to_s + '/testtest.JPG" alt="" />',
213 213 'Inline image: !testtest.jpeg!' =>
214 214 'Inline image: <img src="/attachments/download/' + a2.id.to_s + '/testtest.jpeg" alt="" />',
215 215 'Inline image: !testtest.jpe!' =>
216 216 'Inline image: <img src="/attachments/download/' + a3.id.to_s + '/testtest.JPE" alt="" />',
217 217 'Inline image: !testtest.bmp!' =>
218 218 'Inline image: <img src="/attachments/download/' + a4.id.to_s + '/Testtest.BMP" alt="" />',
219 219 }
220 220
221 221 attachments = [a1, a2, a3, a4]
222 222 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text, :attachments => attachments) }
223 223 end
224 224
225 225 def test_attached_images_should_read_later
226 226 set_fixtures_attachments_directory
227 227 a1 = Attachment.find(16)
228 228 assert_equal "testfile.png", a1.filename
229 229 assert a1.readable?
230 230 assert (! a1.visible?(User.anonymous))
231 231 assert a1.visible?(User.find(2))
232 232 a2 = Attachment.find(17)
233 233 assert_equal "testfile.PNG", a2.filename
234 234 assert a2.readable?
235 235 assert (! a2.visible?(User.anonymous))
236 236 assert a2.visible?(User.find(2))
237 237 assert a1.created_on < a2.created_on
238 238
239 239 to_test = {
240 240 'Inline image: !testfile.png!' =>
241 241 'Inline image: <img src="/attachments/download/' + a2.id.to_s + '/testfile.PNG" alt="" />',
242 242 'Inline image: !Testfile.PNG!' =>
243 243 'Inline image: <img src="/attachments/download/' + a2.id.to_s + '/testfile.PNG" alt="" />',
244 244 }
245 245 attachments = [a1, a2]
246 246 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text, :attachments => attachments) }
247 247 set_tmp_attachments_directory
248 248 end
249 249
250 250 def test_textile_external_links
251 251 to_test = {
252 252 'This is a "link":http://foo.bar' => 'This is a <a href="http://foo.bar" class="external">link</a>',
253 253 'This is an intern "link":/foo/bar' => 'This is an intern <a href="/foo/bar">link</a>',
254 254 '"link (Link title)":http://foo.bar' => '<a href="http://foo.bar" title="Link title" class="external">link</a>',
255 255 '"link (Link title with "double-quotes")":http://foo.bar' => '<a href="http://foo.bar" title="Link title with &quot;double-quotes&quot;" class="external">link</a>',
256 256 "This is not a \"Link\":\n\nAnother paragraph" => "This is not a \"Link\":</p>\n\n\n\t<p>Another paragraph",
257 257 # no multiline link text
258 258 "This is a double quote \"on the first line\nand another on a second line\":test" => "This is a double quote \"on the first line<br />and another on a second line\":test",
259 259 # mailto link
260 260 "\"system administrator\":mailto:sysadmin@example.com?subject=redmine%20permissions" => "<a href=\"mailto:sysadmin@example.com?subject=redmine%20permissions\">system administrator</a>",
261 261 # two exclamation marks
262 262 '"a link":http://example.net/path!602815048C7B5C20!302.html' => '<a href="http://example.net/path!602815048C7B5C20!302.html" class="external">a link</a>',
263 263 # escaping
264 264 '"test":http://foo"bar' => '<a href="http://foo&quot;bar" class="external">test</a>',
265 265 }
266 266 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
267 267 end
268 268
269 269 def test_textile_external_links_with_non_ascii_characters
270 270 to_test = {
271 271 %|This is a "link":http://foo.bar/#{@russian_test}| =>
272 272 %|This is a <a href="http://foo.bar/#{@russian_test}" class="external">link</a>|
273 273 }
274 274 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
275 275 end
276 276
277 277 def test_redmine_links
278 278 issue_link = link_to('#3', {:controller => 'issues', :action => 'show', :id => 3},
279 279 :class => Issue.find(3).css_classes, :title => 'Bug: Error 281 when updating a recipe (New)')
280 280 note_link = link_to('#3-14', {:controller => 'issues', :action => 'show', :id => 3, :anchor => 'note-14'},
281 281 :class => Issue.find(3).css_classes, :title => 'Bug: Error 281 when updating a recipe (New)')
282 282 note_link2 = link_to('#3#note-14', {:controller => 'issues', :action => 'show', :id => 3, :anchor => 'note-14'},
283 283 :class => Issue.find(3).css_classes, :title => 'Bug: Error 281 when updating a recipe (New)')
284 284
285 285 revision_link = link_to('r1', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :rev => 1},
286 286 :class => 'changeset', :title => 'My very first commit do not escaping #<>&')
287 287 revision_link2 = link_to('r2', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :rev => 2},
288 288 :class => 'changeset', :title => 'This commit fixes #1, #2 and references #1 & #3')
289 289
290 290 changeset_link2 = link_to('691322a8eb01e11fd7',
291 291 {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :rev => 1},
292 292 :class => 'changeset', :title => 'My very first commit do not escaping #<>&')
293 293
294 294 document_link = link_to('Test document', {:controller => 'documents', :action => 'show', :id => 1},
295 295 :class => 'document')
296 296
297 297 version_link = link_to('1.0', {:controller => 'versions', :action => 'show', :id => 2},
298 298 :class => 'version')
299 299
300 300 board_url = {:controller => 'boards', :action => 'show', :id => 2, :project_id => 'ecookbook'}
301 301
302 302 message_url = {:controller => 'messages', :action => 'show', :board_id => 1, :id => 4}
303 303
304 304 news_url = {:controller => 'news', :action => 'show', :id => 1}
305 305
306 306 project_url = {:controller => 'projects', :action => 'show', :id => 'subproject1'}
307 307
308 308 source_url = '/projects/ecookbook/repository/entry/some/file'
309 309 source_url_with_rev = '/projects/ecookbook/repository/revisions/52/entry/some/file'
310 310 source_url_with_ext = '/projects/ecookbook/repository/entry/some/file.ext'
311 311 source_url_with_rev_and_ext = '/projects/ecookbook/repository/revisions/52/entry/some/file.ext'
312 312 source_url_with_branch = '/projects/ecookbook/repository/revisions/branch/entry/some/file'
313 313
314 314 export_url = '/projects/ecookbook/repository/raw/some/file'
315 315 export_url_with_rev = '/projects/ecookbook/repository/revisions/52/raw/some/file'
316 316 export_url_with_ext = '/projects/ecookbook/repository/raw/some/file.ext'
317 317 export_url_with_rev_and_ext = '/projects/ecookbook/repository/revisions/52/raw/some/file.ext'
318 318 export_url_with_branch = '/projects/ecookbook/repository/revisions/branch/raw/some/file'
319 319
320 320 to_test = {
321 321 # tickets
322 322 '#3, [#3], (#3) and #3.' => "#{issue_link}, [#{issue_link}], (#{issue_link}) and #{issue_link}.",
323 323 # ticket notes
324 324 '#3-14' => note_link,
325 325 '#3#note-14' => note_link2,
326 326 # should not ignore leading zero
327 327 '#03' => '#03',
328 328 # changesets
329 329 'r1' => revision_link,
330 330 'r1.' => "#{revision_link}.",
331 331 'r1, r2' => "#{revision_link}, #{revision_link2}",
332 332 'r1,r2' => "#{revision_link},#{revision_link2}",
333 333 'commit:691322a8eb01e11fd7' => changeset_link2,
334 334 # documents
335 335 'document#1' => document_link,
336 336 'document:"Test document"' => document_link,
337 337 # versions
338 338 'version#2' => version_link,
339 339 'version:1.0' => version_link,
340 340 'version:"1.0"' => version_link,
341 341 # source
342 342 'source:some/file' => link_to('source:some/file', source_url, :class => 'source'),
343 343 'source:/some/file' => link_to('source:/some/file', source_url, :class => 'source'),
344 344 'source:/some/file.' => link_to('source:/some/file', source_url, :class => 'source') + ".",
345 345 'source:/some/file.ext.' => link_to('source:/some/file.ext', source_url_with_ext, :class => 'source') + ".",
346 346 'source:/some/file. ' => link_to('source:/some/file', source_url, :class => 'source') + ".",
347 347 'source:/some/file.ext. ' => link_to('source:/some/file.ext', source_url_with_ext, :class => 'source') + ".",
348 348 'source:/some/file, ' => link_to('source:/some/file', source_url, :class => 'source') + ",",
349 349 'source:/some/file@52' => link_to('source:/some/file@52', source_url_with_rev, :class => 'source'),
350 350 'source:/some/file@branch' => link_to('source:/some/file@branch', source_url_with_branch, :class => 'source'),
351 351 'source:/some/file.ext@52' => link_to('source:/some/file.ext@52', source_url_with_rev_and_ext, :class => 'source'),
352 352 'source:/some/file#L110' => link_to('source:/some/file#L110', source_url + "#L110", :class => 'source'),
353 353 'source:/some/file.ext#L110' => link_to('source:/some/file.ext#L110', source_url_with_ext + "#L110", :class => 'source'),
354 354 'source:/some/file@52#L110' => link_to('source:/some/file@52#L110', source_url_with_rev + "#L110", :class => 'source'),
355 355 # export
356 356 'export:/some/file' => link_to('export:/some/file', export_url, :class => 'source download'),
357 357 'export:/some/file.ext' => link_to('export:/some/file.ext', export_url_with_ext, :class => 'source download'),
358 358 'export:/some/file@52' => link_to('export:/some/file@52', export_url_with_rev, :class => 'source download'),
359 359 'export:/some/file.ext@52' => link_to('export:/some/file.ext@52', export_url_with_rev_and_ext, :class => 'source download'),
360 360 'export:/some/file@branch' => link_to('export:/some/file@branch', export_url_with_branch, :class => 'source download'),
361 361 # forum
362 362 'forum#2' => link_to('Discussion', board_url, :class => 'board'),
363 363 'forum:Discussion' => link_to('Discussion', board_url, :class => 'board'),
364 364 # message
365 365 'message#4' => link_to('Post 2', message_url, :class => 'message'),
366 366 'message#5' => link_to('RE: post 2', message_url.merge(:anchor => 'message-5', :r => 5), :class => 'message'),
367 367 # news
368 368 'news#1' => link_to('eCookbook first release !', news_url, :class => 'news'),
369 369 'news:"eCookbook first release !"' => link_to('eCookbook first release !', news_url, :class => 'news'),
370 370 # project
371 371 'project#3' => link_to('eCookbook Subproject 1', project_url, :class => 'project'),
372 372 'project:subproject1' => link_to('eCookbook Subproject 1', project_url, :class => 'project'),
373 373 'project:"eCookbook subProject 1"' => link_to('eCookbook Subproject 1', project_url, :class => 'project'),
374 374 # not found
375 375 '#0123456789' => '#0123456789',
376 376 # invalid expressions
377 377 'source:' => 'source:',
378 378 # url hash
379 379 "http://foo.bar/FAQ#3" => '<a class="external" href="http://foo.bar/FAQ#3">http://foo.bar/FAQ#3</a>',
380 380 }
381 381 @project = Project.find(1)
382 382 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text), "#{text} failed" }
383 383 end
384 384
385 385 def test_should_not_parse_redmine_links_inside_link
386 386 raw = "r1 should not be parsed in http://example.com/url-r1/"
387 387 assert_match %r{<p><a class="changeset".*>r1</a> should not be parsed in <a class="external" href="http://example.com/url-r1/">http://example.com/url-r1/</a></p>},
388 388 textilizable(raw, :project => Project.find(1))
389 389 end
390 390
391 391 def test_redmine_links_with_a_different_project_before_current_project
392 392 vp1 = Version.generate!(:project_id => 1, :name => '1.4.4')
393 393 vp3 = Version.generate!(:project_id => 3, :name => '1.4.4')
394 394 @project = Project.find(3)
395 395 result1 = link_to("1.4.4", "/versions/#{vp1.id}", :class => "version")
396 396 result2 = link_to("1.4.4", "/versions/#{vp3.id}", :class => "version")
397 397 assert_equal "<p>#{result1} #{result2}</p>",
398 398 textilizable("ecookbook:version:1.4.4 version:1.4.4")
399 399 end
400 400
401 401 def test_escaped_redmine_links_should_not_be_parsed
402 402 to_test = [
403 403 '#3.',
404 404 '#3-14.',
405 405 '#3#-note14.',
406 406 'r1',
407 407 'document#1',
408 408 'document:"Test document"',
409 409 'version#2',
410 410 'version:1.0',
411 411 'version:"1.0"',
412 412 'source:/some/file'
413 413 ]
414 414 @project = Project.find(1)
415 415 to_test.each { |text| assert_equal "<p>#{text}</p>", textilizable("!" + text), "#{text} failed" }
416 416 end
417 417
418 418 def test_cross_project_redmine_links
419 419 source_link = link_to('ecookbook:source:/some/file',
420 420 {:controller => 'repositories', :action => 'entry',
421 421 :id => 'ecookbook', :path => ['some', 'file']},
422 422 :class => 'source')
423 423 changeset_link = link_to('ecookbook:r2',
424 424 {:controller => 'repositories', :action => 'revision',
425 425 :id => 'ecookbook', :rev => 2},
426 426 :class => 'changeset',
427 427 :title => 'This commit fixes #1, #2 and references #1 & #3')
428 428 to_test = {
429 429 # documents
430 430 'document:"Test document"' => 'document:"Test document"',
431 431 'ecookbook:document:"Test document"' =>
432 432 link_to("Test document", "/documents/1", :class => "document"),
433 433 'invalid:document:"Test document"' => 'invalid:document:"Test document"',
434 434 # versions
435 435 'version:"1.0"' => 'version:"1.0"',
436 436 'ecookbook:version:"1.0"' =>
437 437 link_to("1.0", "/versions/2", :class => "version"),
438 438 'invalid:version:"1.0"' => 'invalid:version:"1.0"',
439 439 # changeset
440 440 'r2' => 'r2',
441 441 'ecookbook:r2' => changeset_link,
442 442 'invalid:r2' => 'invalid:r2',
443 443 # source
444 444 'source:/some/file' => 'source:/some/file',
445 445 'ecookbook:source:/some/file' => source_link,
446 446 'invalid:source:/some/file' => 'invalid:source:/some/file',
447 447 }
448 448 @project = Project.find(3)
449 449 to_test.each do |text, result|
450 450 assert_equal "<p>#{result}</p>", textilizable(text), "#{text} failed"
451 451 end
452 452 end
453 453
454 454 def test_redmine_links_by_name_should_work_with_html_escaped_characters
455 455 v = Version.generate!(:name => "Test & Show.txt", :project_id => 1)
456 456 link = link_to("Test & Show.txt", "/versions/#{v.id}", :class => "version")
457 457
458 458 @project = v.project
459 459 assert_equal "<p>#{link}</p>", textilizable('version:"Test & Show.txt"')
460 460 end
461 461
462 462 def test_link_to_issue_subject
463 463 issue = Issue.generate!(:subject => "01234567890123456789")
464 464 str = link_to_issue(issue, :truncate => 10)
465 465 result = link_to("Bug ##{issue.id}", "/issues/#{issue.id}", :class => issue.css_classes)
466 466 assert_equal "#{result}: 0123456...", str
467 467
468 468 issue = Issue.generate!(:subject => "<&>")
469 469 str = link_to_issue(issue)
470 470 result = link_to("Bug ##{issue.id}", "/issues/#{issue.id}", :class => issue.css_classes)
471 471 assert_equal "#{result}: &lt;&amp;&gt;", str
472 472
473 473 issue = Issue.generate!(:subject => "<&>0123456789012345")
474 474 str = link_to_issue(issue, :truncate => 10)
475 475 result = link_to("Bug ##{issue.id}", "/issues/#{issue.id}", :class => issue.css_classes)
476 476 assert_equal "#{result}: &lt;&amp;&gt;0123...", str
477 477 end
478 478
479 479 def test_link_to_issue_title
480 480 long_str = "0123456789" * 5
481 481
482 482 issue = Issue.generate!(:subject => "#{long_str}01234567890123456789")
483 483 str = link_to_issue(issue, :subject => false)
484 484 result = link_to("Bug ##{issue.id}", "/issues/#{issue.id}",
485 485 :class => issue.css_classes,
486 486 :title => "#{long_str}0123456...")
487 487 assert_equal result, str
488 488
489 489 issue = Issue.generate!(:subject => "<&>#{long_str}01234567890123456789")
490 490 str = link_to_issue(issue, :subject => false)
491 491 result = link_to("Bug ##{issue.id}", "/issues/#{issue.id}",
492 492 :class => issue.css_classes,
493 493 :title => "<&>#{long_str}0123...")
494 494 assert_equal result, str
495 495 end
496 496
497 497 def test_multiple_repositories_redmine_links
498 498 svn = Repository::Subversion.create!(:project_id => 1, :identifier => 'svn_repo-1', :url => 'file:///foo/hg')
499 499 Changeset.create!(:repository => svn, :committed_on => Time.now, :revision => '123')
500 500 hg = Repository::Mercurial.create!(:project_id => 1, :identifier => 'hg1', :url => '/foo/hg')
501 501 Changeset.create!(:repository => hg, :committed_on => Time.now, :revision => '123', :scmid => 'abcd')
502 502
503 503 changeset_link = link_to('r2', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :rev => 2},
504 504 :class => 'changeset', :title => 'This commit fixes #1, #2 and references #1 & #3')
505 505 svn_changeset_link = link_to('svn_repo-1|r123', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :repository_id => 'svn_repo-1', :rev => 123},
506 506 :class => 'changeset', :title => '')
507 507 hg_changeset_link = link_to('hg1|abcd', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :repository_id => 'hg1', :rev => 'abcd'},
508 508 :class => 'changeset', :title => '')
509 509
510 510 source_link = link_to('source:some/file', {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :path => ['some', 'file']}, :class => 'source')
511 511 hg_source_link = link_to('source:hg1|some/file', {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :repository_id => 'hg1', :path => ['some', 'file']}, :class => 'source')
512 512
513 513 to_test = {
514 514 'r2' => changeset_link,
515 515 'svn_repo-1|r123' => svn_changeset_link,
516 516 'invalid|r123' => 'invalid|r123',
517 517 'commit:hg1|abcd' => hg_changeset_link,
518 518 'commit:invalid|abcd' => 'commit:invalid|abcd',
519 519 # source
520 520 'source:some/file' => source_link,
521 521 'source:hg1|some/file' => hg_source_link,
522 522 'source:invalid|some/file' => 'source:invalid|some/file',
523 523 }
524 524
525 525 @project = Project.find(1)
526 526 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text), "#{text} failed" }
527 527 end
528 528
529 529 def test_cross_project_multiple_repositories_redmine_links
530 530 svn = Repository::Subversion.create!(:project_id => 1, :identifier => 'svn1', :url => 'file:///foo/hg')
531 531 Changeset.create!(:repository => svn, :committed_on => Time.now, :revision => '123')
532 532 hg = Repository::Mercurial.create!(:project_id => 1, :identifier => 'hg1', :url => '/foo/hg')
533 533 Changeset.create!(:repository => hg, :committed_on => Time.now, :revision => '123', :scmid => 'abcd')
534 534
535 535 changeset_link = link_to('ecookbook:r2', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :rev => 2},
536 536 :class => 'changeset', :title => 'This commit fixes #1, #2 and references #1 & #3')
537 537 svn_changeset_link = link_to('ecookbook:svn1|r123', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :repository_id => 'svn1', :rev => 123},
538 538 :class => 'changeset', :title => '')
539 539 hg_changeset_link = link_to('ecookbook:hg1|abcd', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :repository_id => 'hg1', :rev => 'abcd'},
540 540 :class => 'changeset', :title => '')
541 541
542 542 source_link = link_to('ecookbook:source:some/file', {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :path => ['some', 'file']}, :class => 'source')
543 543 hg_source_link = link_to('ecookbook:source:hg1|some/file', {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :repository_id => 'hg1', :path => ['some', 'file']}, :class => 'source')
544 544
545 545 to_test = {
546 546 'ecookbook:r2' => changeset_link,
547 547 'ecookbook:svn1|r123' => svn_changeset_link,
548 548 'ecookbook:invalid|r123' => 'ecookbook:invalid|r123',
549 549 'ecookbook:commit:hg1|abcd' => hg_changeset_link,
550 550 'ecookbook:commit:invalid|abcd' => 'ecookbook:commit:invalid|abcd',
551 551 'invalid:commit:invalid|abcd' => 'invalid:commit:invalid|abcd',
552 552 # source
553 553 'ecookbook:source:some/file' => source_link,
554 554 'ecookbook:source:hg1|some/file' => hg_source_link,
555 555 'ecookbook:source:invalid|some/file' => 'ecookbook:source:invalid|some/file',
556 556 'invalid:source:invalid|some/file' => 'invalid:source:invalid|some/file',
557 557 }
558 558
559 559 @project = Project.find(3)
560 560 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text), "#{text} failed" }
561 561 end
562 562
563 563 def test_redmine_links_git_commit
564 564 changeset_link = link_to('abcd',
565 565 {
566 566 :controller => 'repositories',
567 567 :action => 'revision',
568 568 :id => 'subproject1',
569 569 :rev => 'abcd',
570 570 },
571 571 :class => 'changeset', :title => 'test commit')
572 572 to_test = {
573 573 'commit:abcd' => changeset_link,
574 574 }
575 575 @project = Project.find(3)
576 576 r = Repository::Git.create!(:project => @project, :url => '/tmp/test/git')
577 577 assert r
578 578 c = Changeset.new(:repository => r,
579 579 :committed_on => Time.now,
580 580 :revision => 'abcd',
581 581 :scmid => 'abcd',
582 582 :comments => 'test commit')
583 583 assert( c.save )
584 584 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
585 585 end
586 586
587 587 # TODO: Bazaar commit id contains mail address, so it contains '@' and '_'.
588 588 def test_redmine_links_darcs_commit
589 589 changeset_link = link_to('20080308225258-98289-abcd456efg.gz',
590 590 {
591 591 :controller => 'repositories',
592 592 :action => 'revision',
593 593 :id => 'subproject1',
594 594 :rev => '123',
595 595 },
596 596 :class => 'changeset', :title => 'test commit')
597 597 to_test = {
598 598 'commit:20080308225258-98289-abcd456efg.gz' => changeset_link,
599 599 }
600 600 @project = Project.find(3)
601 601 r = Repository::Darcs.create!(
602 602 :project => @project, :url => '/tmp/test/darcs',
603 603 :log_encoding => 'UTF-8')
604 604 assert r
605 605 c = Changeset.new(:repository => r,
606 606 :committed_on => Time.now,
607 607 :revision => '123',
608 608 :scmid => '20080308225258-98289-abcd456efg.gz',
609 609 :comments => 'test commit')
610 610 assert( c.save )
611 611 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
612 612 end
613 613
614 614 def test_redmine_links_mercurial_commit
615 615 changeset_link_rev = link_to('r123',
616 616 {
617 617 :controller => 'repositories',
618 618 :action => 'revision',
619 619 :id => 'subproject1',
620 620 :rev => '123' ,
621 621 },
622 622 :class => 'changeset', :title => 'test commit')
623 623 changeset_link_commit = link_to('abcd',
624 624 {
625 625 :controller => 'repositories',
626 626 :action => 'revision',
627 627 :id => 'subproject1',
628 628 :rev => 'abcd' ,
629 629 },
630 630 :class => 'changeset', :title => 'test commit')
631 631 to_test = {
632 632 'r123' => changeset_link_rev,
633 633 'commit:abcd' => changeset_link_commit,
634 634 }
635 635 @project = Project.find(3)
636 636 r = Repository::Mercurial.create!(:project => @project, :url => '/tmp/test')
637 637 assert r
638 638 c = Changeset.new(:repository => r,
639 639 :committed_on => Time.now,
640 640 :revision => '123',
641 641 :scmid => 'abcd',
642 642 :comments => 'test commit')
643 643 assert( c.save )
644 644 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
645 645 end
646 646
647 647 def test_attachment_links
648 648 text = 'attachment:error281.txt'
649 649 result = link_to("error281.txt", "/attachments/download/1/error281.txt",
650 650 :class => "attachment")
651 651 assert_equal "<p>#{result}</p>",
652 652 textilizable(text,
653 653 :attachments => Issue.find(3).attachments),
654 654 "#{text} failed"
655 655 end
656 656
657 657 def test_attachment_link_should_link_to_latest_attachment
658 658 set_tmp_attachments_directory
659 659 a1 = Attachment.generate!(:filename => "test.txt", :created_on => 1.hour.ago)
660 660 a2 = Attachment.generate!(:filename => "test.txt")
661 661 result = link_to("test.txt", "/attachments/download/#{a2.id}/test.txt",
662 662 :class => "attachment")
663 663 assert_equal "<p>#{result}</p>",
664 664 textilizable('attachment:test.txt', :attachments => [a1, a2])
665 665 end
666 666
667 667 def test_wiki_links
668 668 russian_eacape = CGI.escape(@russian_test)
669 669 to_test = {
670 670 '[[CookBook documentation]]' =>
671 671 link_to("CookBook documentation",
672 672 "/projects/ecookbook/wiki/CookBook_documentation",
673 673 :class => "wiki-page"),
674 674 '[[Another page|Page]]' =>
675 675 link_to("Page",
676 676 "/projects/ecookbook/wiki/Another_page",
677 677 :class => "wiki-page"),
678 678 # title content should be formatted
679 679 '[[Another page|With _styled_ *title*]]' =>
680 680 link_to("With <em>styled</em> <strong>title</strong>".html_safe,
681 681 "/projects/ecookbook/wiki/Another_page",
682 682 :class => "wiki-page"),
683 683 '[[Another page|With title containing <strong>HTML entities &amp; markups</strong>]]' =>
684 684 link_to("With title containing &lt;strong&gt;HTML entities &amp; markups&lt;/strong&gt;".html_safe,
685 685 "/projects/ecookbook/wiki/Another_page",
686 686 :class => "wiki-page"),
687 687 # link with anchor
688 688 '[[CookBook documentation#One-section]]' =>
689 689 link_to("CookBook documentation",
690 690 "/projects/ecookbook/wiki/CookBook_documentation#One-section",
691 691 :class => "wiki-page"),
692 692 '[[Another page#anchor|Page]]' =>
693 693 link_to("Page",
694 694 "/projects/ecookbook/wiki/Another_page#anchor",
695 695 :class => "wiki-page"),
696 696 # UTF8 anchor
697 697 "[[Another_page##{@russian_test}|#{@russian_test}]]" =>
698 698 link_to(@russian_test,
699 699 "/projects/ecookbook/wiki/Another_page##{russian_eacape}",
700 700 :class => "wiki-page"),
701 701 # page that doesn't exist
702 702 '[[Unknown page]]' =>
703 703 link_to("Unknown page",
704 704 "/projects/ecookbook/wiki/Unknown_page",
705 705 :class => "wiki-page new"),
706 706 '[[Unknown page|404]]' =>
707 707 link_to("404",
708 708 "/projects/ecookbook/wiki/Unknown_page",
709 709 :class => "wiki-page new"),
710 710 # link to another project wiki
711 711 '[[onlinestore:]]' =>
712 712 link_to("onlinestore",
713 713 "/projects/onlinestore/wiki",
714 714 :class => "wiki-page"),
715 715 '[[onlinestore:|Wiki]]' =>
716 716 link_to("Wiki",
717 717 "/projects/onlinestore/wiki",
718 718 :class => "wiki-page"),
719 719 '[[onlinestore:Start page]]' =>
720 720 link_to("Start page",
721 721 "/projects/onlinestore/wiki/Start_page",
722 722 :class => "wiki-page"),
723 723 '[[onlinestore:Start page|Text]]' =>
724 724 link_to("Text",
725 725 "/projects/onlinestore/wiki/Start_page",
726 726 :class => "wiki-page"),
727 727 '[[onlinestore:Unknown page]]' =>
728 728 link_to("Unknown page",
729 729 "/projects/onlinestore/wiki/Unknown_page",
730 730 :class => "wiki-page new"),
731 731 # struck through link
732 732 '-[[Another page|Page]]-' =>
733 733 "<del>".html_safe +
734 734 link_to("Page",
735 735 "/projects/ecookbook/wiki/Another_page",
736 736 :class => "wiki-page").html_safe +
737 737 "</del>".html_safe,
738 738 '-[[Another page|Page]] link-' =>
739 739 "<del>".html_safe +
740 740 link_to("Page",
741 741 "/projects/ecookbook/wiki/Another_page",
742 742 :class => "wiki-page").html_safe +
743 743 " link</del>".html_safe,
744 744 # escaping
745 745 '![[Another page|Page]]' => '[[Another page|Page]]',
746 746 # project does not exist
747 747 '[[unknowproject:Start]]' => '[[unknowproject:Start]]',
748 748 '[[unknowproject:Start|Page title]]' => '[[unknowproject:Start|Page title]]',
749 749 }
750 750 @project = Project.find(1)
751 751 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
752 752 end
753 753
754 754 def test_wiki_links_within_local_file_generation_context
755 755 to_test = {
756 756 # link to a page
757 757 '[[CookBook documentation]]' =>
758 758 link_to("CookBook documentation", "CookBook_documentation.html",
759 759 :class => "wiki-page"),
760 760 '[[CookBook documentation|documentation]]' =>
761 761 link_to("documentation", "CookBook_documentation.html",
762 762 :class => "wiki-page"),
763 763 '[[CookBook documentation#One-section]]' =>
764 764 link_to("CookBook documentation", "CookBook_documentation.html#One-section",
765 765 :class => "wiki-page"),
766 766 '[[CookBook documentation#One-section|documentation]]' =>
767 767 link_to("documentation", "CookBook_documentation.html#One-section",
768 768 :class => "wiki-page"),
769 769 # page that doesn't exist
770 770 '[[Unknown page]]' =>
771 771 link_to("Unknown page", "Unknown_page.html",
772 772 :class => "wiki-page new"),
773 773 '[[Unknown page|404]]' =>
774 774 link_to("404", "Unknown_page.html",
775 775 :class => "wiki-page new"),
776 776 '[[Unknown page#anchor]]' =>
777 777 link_to("Unknown page", "Unknown_page.html#anchor",
778 778 :class => "wiki-page new"),
779 779 '[[Unknown page#anchor|404]]' =>
780 780 link_to("404", "Unknown_page.html#anchor",
781 781 :class => "wiki-page new"),
782 782 }
783 783 @project = Project.find(1)
784 784 to_test.each do |text, result|
785 785 assert_equal "<p>#{result}</p>", textilizable(text, :wiki_links => :local)
786 786 end
787 787 end
788 788
789 789 def test_wiki_links_within_wiki_page_context
790 790 page = WikiPage.find_by_title('Another_page' )
791 791 to_test = {
792 792 '[[CookBook documentation]]' =>
793 793 link_to("CookBook documentation",
794 794 "/projects/ecookbook/wiki/CookBook_documentation",
795 795 :class => "wiki-page"),
796 796 '[[CookBook documentation|documentation]]' =>
797 797 link_to("documentation",
798 798 "/projects/ecookbook/wiki/CookBook_documentation",
799 799 :class => "wiki-page"),
800 800 '[[CookBook documentation#One-section]]' =>
801 801 link_to("CookBook documentation",
802 802 "/projects/ecookbook/wiki/CookBook_documentation#One-section",
803 803 :class => "wiki-page"),
804 804 '[[CookBook documentation#One-section|documentation]]' =>
805 805 link_to("documentation",
806 806 "/projects/ecookbook/wiki/CookBook_documentation#One-section",
807 807 :class => "wiki-page"),
808 808 # link to the current page
809 809 '[[Another page]]' =>
810 810 link_to("Another page",
811 811 "/projects/ecookbook/wiki/Another_page",
812 812 :class => "wiki-page"),
813 813 '[[Another page|Page]]' =>
814 814 link_to("Page",
815 815 "/projects/ecookbook/wiki/Another_page",
816 816 :class => "wiki-page"),
817 817 '[[Another page#anchor]]' =>
818 818 link_to("Another page",
819 819 "#anchor",
820 820 :class => "wiki-page"),
821 821 '[[Another page#anchor|Page]]' =>
822 822 link_to("Page",
823 823 "#anchor",
824 824 :class => "wiki-page"),
825 825 # page that doesn't exist
826 826 '[[Unknown page]]' =>
827 827 link_to("Unknown page",
828 828 "/projects/ecookbook/wiki/Unknown_page?parent=Another_page",
829 829 :class => "wiki-page new"),
830 830 '[[Unknown page|404]]' =>
831 831 link_to("404",
832 832 "/projects/ecookbook/wiki/Unknown_page?parent=Another_page",
833 833 :class => "wiki-page new"),
834 834 '[[Unknown page#anchor]]' =>
835 835 link_to("Unknown page",
836 836 "/projects/ecookbook/wiki/Unknown_page?parent=Another_page#anchor",
837 837 :class => "wiki-page new"),
838 838 '[[Unknown page#anchor|404]]' =>
839 839 link_to("404",
840 840 "/projects/ecookbook/wiki/Unknown_page?parent=Another_page#anchor",
841 841 :class => "wiki-page new"),
842 842 }
843 843 @project = Project.find(1)
844 844 to_test.each do |text, result|
845 845 assert_equal "<p>#{result}</p>",
846 846 textilizable(WikiContent.new( :text => text, :page => page ), :text)
847 847 end
848 848 end
849 849
850 850 def test_wiki_links_anchor_option_should_prepend_page_title_to_href
851 851 to_test = {
852 852 # link to a page
853 853 '[[CookBook documentation]]' =>
854 854 link_to("CookBook documentation",
855 855 "#CookBook_documentation",
856 856 :class => "wiki-page"),
857 857 '[[CookBook documentation|documentation]]' =>
858 858 link_to("documentation",
859 859 "#CookBook_documentation",
860 860 :class => "wiki-page"),
861 861 '[[CookBook documentation#One-section]]' =>
862 862 link_to("CookBook documentation",
863 863 "#CookBook_documentation_One-section",
864 864 :class => "wiki-page"),
865 865 '[[CookBook documentation#One-section|documentation]]' =>
866 866 link_to("documentation",
867 867 "#CookBook_documentation_One-section",
868 868 :class => "wiki-page"),
869 869 # page that doesn't exist
870 870 '[[Unknown page]]' =>
871 871 link_to("Unknown page",
872 872 "#Unknown_page",
873 873 :class => "wiki-page new"),
874 874 '[[Unknown page|404]]' =>
875 875 link_to("404",
876 876 "#Unknown_page",
877 877 :class => "wiki-page new"),
878 878 '[[Unknown page#anchor]]' =>
879 879 link_to("Unknown page",
880 880 "#Unknown_page_anchor",
881 881 :class => "wiki-page new"),
882 882 '[[Unknown page#anchor|404]]' =>
883 883 link_to("404",
884 884 "#Unknown_page_anchor",
885 885 :class => "wiki-page new"),
886 886 }
887 887 @project = Project.find(1)
888 888 to_test.each do |text, result|
889 889 assert_equal "<p>#{result}</p>", textilizable(text, :wiki_links => :anchor)
890 890 end
891 891 end
892 892
893 893 def test_html_tags
894 894 to_test = {
895 895 "<div>content</div>" => "<p>&lt;div&gt;content&lt;/div&gt;</p>",
896 896 "<div class=\"bold\">content</div>" => "<p>&lt;div class=\"bold\"&gt;content&lt;/div&gt;</p>",
897 897 "<script>some script;</script>" => "<p>&lt;script&gt;some script;&lt;/script&gt;</p>",
898 898 # do not escape pre/code tags
899 899 "<pre>\nline 1\nline2</pre>" => "<pre>\nline 1\nline2</pre>",
900 900 "<pre><code>\nline 1\nline2</code></pre>" => "<pre><code>\nline 1\nline2</code></pre>",
901 901 "<pre><div>content</div></pre>" => "<pre>&lt;div&gt;content&lt;/div&gt;</pre>",
902 902 "HTML comment: <!-- no comments -->" => "<p>HTML comment: &lt;!-- no comments --&gt;</p>",
903 903 "<!-- opening comment" => "<p>&lt;!-- opening comment</p>",
904 904 # remove attributes except class
905 905 "<pre class='foo'>some text</pre>" => "<pre class='foo'>some text</pre>",
906 906 '<pre class="foo">some text</pre>' => '<pre class="foo">some text</pre>',
907 907 "<pre class='foo bar'>some text</pre>" => "<pre class='foo bar'>some text</pre>",
908 908 '<pre class="foo bar">some text</pre>' => '<pre class="foo bar">some text</pre>',
909 909 "<pre onmouseover='alert(1)'>some text</pre>" => "<pre>some text</pre>",
910 910 # xss
911 911 '<pre><code class=""onmouseover="alert(1)">text</code></pre>' => '<pre><code>text</code></pre>',
912 912 '<pre class=""onmouseover="alert(1)">text</pre>' => '<pre>text</pre>',
913 913 }
914 914 to_test.each { |text, result| assert_equal result, textilizable(text) }
915 915 end
916 916
917 917 def test_allowed_html_tags
918 918 to_test = {
919 919 "<pre>preformatted text</pre>" => "<pre>preformatted text</pre>",
920 920 "<notextile>no *textile* formatting</notextile>" => "no *textile* formatting",
921 921 "<notextile>this is <tag>a tag</tag></notextile>" => "this is &lt;tag&gt;a tag&lt;/tag&gt;"
922 922 }
923 923 to_test.each { |text, result| assert_equal result, textilizable(text) }
924 924 end
925 925
926 926 def test_pre_tags
927 927 raw = <<-RAW
928 928 Before
929 929
930 930 <pre>
931 931 <prepared-statement-cache-size>32</prepared-statement-cache-size>
932 932 </pre>
933 933
934 934 After
935 935 RAW
936 936
937 937 expected = <<-EXPECTED
938 938 <p>Before</p>
939 939 <pre>
940 940 &lt;prepared-statement-cache-size&gt;32&lt;/prepared-statement-cache-size&gt;
941 941 </pre>
942 942 <p>After</p>
943 943 EXPECTED
944 944
945 945 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
946 946 end
947 947
948 948 def test_pre_content_should_not_parse_wiki_and_redmine_links
949 949 raw = <<-RAW
950 950 [[CookBook documentation]]
951 951
952 952 #1
953 953
954 954 <pre>
955 955 [[CookBook documentation]]
956 956
957 957 #1
958 958 </pre>
959 959 RAW
960 960
961 961 result1 = link_to("CookBook documentation",
962 962 "/projects/ecookbook/wiki/CookBook_documentation",
963 963 :class => "wiki-page")
964 964 result2 = link_to('#1',
965 965 "/issues/1",
966 966 :class => Issue.find(1).css_classes,
967 967 :title => "Bug: Cannot print recipes (New)")
968 968
969 969 expected = <<-EXPECTED
970 970 <p>#{result1}</p>
971 971 <p>#{result2}</p>
972 972 <pre>
973 973 [[CookBook documentation]]
974 974
975 975 #1
976 976 </pre>
977 977 EXPECTED
978 978
979 979 @project = Project.find(1)
980 980 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
981 981 end
982 982
983 983 def test_non_closing_pre_blocks_should_be_closed
984 984 raw = <<-RAW
985 985 <pre><code>
986 986 RAW
987 987
988 988 expected = <<-EXPECTED
989 989 <pre><code>
990 990 </code></pre>
991 991 EXPECTED
992 992
993 993 @project = Project.find(1)
994 994 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
995 995 end
996 996
997 997 def test_unbalanced_closing_pre_tag_should_not_error
998 998 assert_nothing_raised do
999 999 textilizable("unbalanced</pre>")
1000 1000 end
1001 1001 end
1002 1002
1003 1003 def test_syntax_highlight
1004 1004 raw = <<-RAW
1005 1005 <pre><code class="ruby">
1006 1006 # Some ruby code here
1007 1007 </code></pre>
1008 1008 RAW
1009 1009
1010 1010 expected = <<-EXPECTED
1011 1011 <pre><code class="ruby syntaxhl"><span class=\"CodeRay\"><span class="comment"># Some ruby code here</span></span>
1012 1012 </code></pre>
1013 1013 EXPECTED
1014 1014
1015 1015 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
1016 1016 end
1017 1017
1018 1018 def test_to_path_param
1019 1019 assert_equal 'test1/test2', to_path_param('test1/test2')
1020 1020 assert_equal 'test1/test2', to_path_param('/test1/test2/')
1021 1021 assert_equal 'test1/test2', to_path_param('//test1/test2/')
1022 1022 assert_equal nil, to_path_param('/')
1023 1023 end
1024 1024
1025 1025 def test_wiki_links_in_tables
1026 1026 text = "|[[Page|Link title]]|[[Other Page|Other title]]|\n|Cell 21|[[Last page]]|"
1027 1027 link1 = link_to("Link title", "/projects/ecookbook/wiki/Page", :class => "wiki-page new")
1028 1028 link2 = link_to("Other title", "/projects/ecookbook/wiki/Other_Page", :class => "wiki-page new")
1029 1029 link3 = link_to("Last page", "/projects/ecookbook/wiki/Last_page", :class => "wiki-page new")
1030 1030 result = "<tr><td>#{link1}</td>" +
1031 1031 "<td>#{link2}</td>" +
1032 1032 "</tr><tr><td>Cell 21</td><td>#{link3}</td></tr>"
1033 1033 @project = Project.find(1)
1034 1034 assert_equal "<table>#{result}</table>", textilizable(text).gsub(/[\t\n]/, '')
1035 1035 end
1036 1036
1037 1037 def test_text_formatting
1038 1038 to_test = {'*_+bold, italic and underline+_*' => '<strong><em><ins>bold, italic and underline</ins></em></strong>',
1039 1039 '(_text within parentheses_)' => '(<em>text within parentheses</em>)',
1040 1040 'a *Humane Web* Text Generator' => 'a <strong>Humane Web</strong> Text Generator',
1041 1041 'a H *umane* W *eb* T *ext* G *enerator*' => 'a H <strong>umane</strong> W <strong>eb</strong> T <strong>ext</strong> G <strong>enerator</strong>',
1042 1042 'a *H* umane *W* eb *T* ext *G* enerator' => 'a <strong>H</strong> umane <strong>W</strong> eb <strong>T</strong> ext <strong>G</strong> enerator',
1043 1043 }
1044 1044 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
1045 1045 end
1046 1046
1047 1047 def test_wiki_horizontal_rule
1048 1048 assert_equal '<hr />', textilizable('---')
1049 1049 assert_equal '<p>Dashes: ---</p>', textilizable('Dashes: ---')
1050 1050 end
1051 1051
1052 1052 def test_footnotes
1053 1053 raw = <<-RAW
1054 1054 This is some text[1].
1055 1055
1056 1056 fn1. This is the foot note
1057 1057 RAW
1058 1058
1059 1059 expected = <<-EXPECTED
1060 1060 <p>This is some text<sup><a href=\"#fn1\">1</a></sup>.</p>
1061 1061 <p id="fn1" class="footnote"><sup>1</sup> This is the foot note</p>
1062 1062 EXPECTED
1063 1063
1064 1064 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
1065 1065 end
1066 1066
1067 1067 def test_headings
1068 1068 raw = 'h1. Some heading'
1069 1069 expected = %|<a name="Some-heading"></a>\n<h1 >Some heading<a href="#Some-heading" class="wiki-anchor">&para;</a></h1>|
1070 1070
1071 1071 assert_equal expected, textilizable(raw)
1072 1072 end
1073 1073
1074 1074 def test_headings_with_special_chars
1075 1075 # This test makes sure that the generated anchor names match the expected
1076 1076 # ones even if the heading text contains unconventional characters
1077 1077 raw = 'h1. Some heading related to version 0.5'
1078 1078 anchor = sanitize_anchor_name("Some-heading-related-to-version-0.5")
1079 1079 expected = %|<a name="#{anchor}"></a>\n<h1 >Some heading related to version 0.5<a href="##{anchor}" class="wiki-anchor">&para;</a></h1>|
1080 1080
1081 1081 assert_equal expected, textilizable(raw)
1082 1082 end
1083 1083
1084 1084 def test_headings_in_wiki_single_page_export_should_be_prepended_with_page_title
1085 1085 page = WikiPage.new( :title => 'Page Title', :wiki_id => 1 )
1086 1086 content = WikiContent.new( :text => 'h1. Some heading', :page => page )
1087 1087
1088 1088 expected = %|<a name="Page_Title_Some-heading"></a>\n<h1 >Some heading<a href="#Page_Title_Some-heading" class="wiki-anchor">&para;</a></h1>|
1089 1089
1090 1090 assert_equal expected, textilizable(content, :text, :wiki_links => :anchor )
1091 1091 end
1092 1092
1093 1093 def test_table_of_content
1094 1094 raw = <<-RAW
1095 1095 {{toc}}
1096 1096
1097 1097 h1. Title
1098 1098
1099 1099 Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas sed libero.
1100 1100
1101 1101 h2. Subtitle with a [[Wiki]] link
1102 1102
1103 1103 Nullam commodo metus accumsan nulla. Curabitur lobortis dui id dolor.
1104 1104
1105 1105 h2. Subtitle with [[Wiki|another Wiki]] link
1106 1106
1107 1107 h2. Subtitle with %{color:red}red text%
1108 1108
1109 1109 <pre>
1110 1110 some code
1111 1111 </pre>
1112 1112
1113 1113 h3. Subtitle with *some* _modifiers_
1114 1114
1115 1115 h3. Subtitle with @inline code@
1116 1116
1117 1117 h1. Another title
1118 1118
1119 1119 h3. An "Internet link":http://www.redmine.org/ inside subtitle
1120 1120
1121 1121 h2. "Project Name !/attachments/1234/logo_small.gif! !/attachments/5678/logo_2.png!":/projects/projectname/issues
1122 1122
1123 1123 RAW
1124 1124
1125 1125 expected = '<ul class="toc">' +
1126 1126 '<li><a href="#Title">Title</a>' +
1127 1127 '<ul>' +
1128 1128 '<li><a href="#Subtitle-with-a-Wiki-link">Subtitle with a Wiki link</a></li>' +
1129 1129 '<li><a href="#Subtitle-with-another-Wiki-link">Subtitle with another Wiki link</a></li>' +
1130 1130 '<li><a href="#Subtitle-with-red-text">Subtitle with red text</a>' +
1131 1131 '<ul>' +
1132 1132 '<li><a href="#Subtitle-with-some-modifiers">Subtitle with some modifiers</a></li>' +
1133 1133 '<li><a href="#Subtitle-with-inline-code">Subtitle with inline code</a></li>' +
1134 1134 '</ul>' +
1135 1135 '</li>' +
1136 1136 '</ul>' +
1137 1137 '</li>' +
1138 1138 '<li><a href="#Another-title">Another title</a>' +
1139 1139 '<ul>' +
1140 1140 '<li>' +
1141 1141 '<ul>' +
1142 1142 '<li><a href="#An-Internet-link-inside-subtitle">An Internet link inside subtitle</a></li>' +
1143 1143 '</ul>' +
1144 1144 '</li>' +
1145 1145 '<li><a href="#Project-Name">Project Name</a></li>' +
1146 1146 '</ul>' +
1147 1147 '</li>' +
1148 1148 '</ul>'
1149 1149
1150 1150 @project = Project.find(1)
1151 1151 assert textilizable(raw).gsub("\n", "").include?(expected)
1152 1152 end
1153 1153
1154 1154 def test_table_of_content_should_generate_unique_anchors
1155 1155 raw = <<-RAW
1156 1156 {{toc}}
1157 1157
1158 1158 h1. Title
1159 1159
1160 1160 h2. Subtitle
1161 1161
1162 1162 h2. Subtitle
1163 1163 RAW
1164 1164
1165 1165 expected = '<ul class="toc">' +
1166 1166 '<li><a href="#Title">Title</a>' +
1167 1167 '<ul>' +
1168 1168 '<li><a href="#Subtitle">Subtitle</a></li>' +
1169 1169 '<li><a href="#Subtitle-2">Subtitle</a></li>'
1170 1170 '</ul>'
1171 1171 '</li>' +
1172 1172 '</ul>'
1173 1173
1174 1174 @project = Project.find(1)
1175 1175 result = textilizable(raw).gsub("\n", "")
1176 1176 assert_include expected, result
1177 1177 assert_include '<a name="Subtitle">', result
1178 1178 assert_include '<a name="Subtitle-2">', result
1179 1179 end
1180 1180
1181 1181 def test_table_of_content_should_contain_included_page_headings
1182 1182 raw = <<-RAW
1183 1183 {{toc}}
1184 1184
1185 1185 h1. Included
1186 1186
1187 1187 {{include(Child_1)}}
1188 1188 RAW
1189 1189
1190 1190 expected = '<ul class="toc">' +
1191 1191 '<li><a href="#Included">Included</a></li>' +
1192 1192 '<li><a href="#Child-page-1">Child page 1</a></li>' +
1193 1193 '</ul>'
1194 1194
1195 1195 @project = Project.find(1)
1196 1196 assert textilizable(raw).gsub("\n", "").include?(expected)
1197 1197 end
1198 1198
1199 1199 def test_toc_with_textile_formatting_should_be_parsed
1200 1200 with_settings :text_formatting => 'textile' do
1201 1201 assert_select_in textilizable("{{toc}}\n\nh1. Heading"), 'ul.toc li', :text => 'Heading'
1202 1202 assert_select_in textilizable("{{<toc}}\n\nh1. Heading"), 'ul.toc.left li', :text => 'Heading'
1203 1203 assert_select_in textilizable("{{>toc}}\n\nh1. Heading"), 'ul.toc.right li', :text => 'Heading'
1204 1204 end
1205 1205 end
1206 1206
1207 1207 if Object.const_defined?(:Redcarpet)
1208 1208 def test_toc_with_markdown_formatting_should_be_parsed
1209 1209 with_settings :text_formatting => 'markdown' do
1210 1210 assert_select_in textilizable("{{toc}}\n\n# Heading"), 'ul.toc li', :text => 'Heading'
1211 1211 assert_select_in textilizable("{{<toc}}\n\n# Heading"), 'ul.toc.left li', :text => 'Heading'
1212 1212 assert_select_in textilizable("{{>toc}}\n\n# Heading"), 'ul.toc.right li', :text => 'Heading'
1213 1213 end
1214 1214 end
1215 1215 end
1216 1216
1217 1217 def test_section_edit_links
1218 1218 raw = <<-RAW
1219 1219 h1. Title
1220 1220
1221 1221 Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas sed libero.
1222 1222
1223 1223 h2. Subtitle with a [[Wiki]] link
1224 1224
1225 1225 h2. Subtitle with *some* _modifiers_
1226 1226
1227 1227 h2. Subtitle with @inline code@
1228 1228
1229 1229 <pre>
1230 1230 some code
1231 1231
1232 1232 h2. heading inside pre
1233 1233
1234 1234 <h2>html heading inside pre</h2>
1235 1235 </pre>
1236 1236
1237 1237 h2. Subtitle after pre tag
1238 1238 RAW
1239 1239
1240 1240 @project = Project.find(1)
1241 1241 set_language_if_valid 'en'
1242 1242 result = textilizable(raw, :edit_section_links => {:controller => 'wiki', :action => 'edit', :project_id => '1', :id => 'Test'}).gsub("\n", "")
1243 1243
1244 1244 # heading that contains inline code
1245 1245 assert_match Regexp.new('<div class="contextual" title="Edit this section" id="section-4">' +
1246 1246 '<a href="/projects/1/wiki/Test/edit\?section=4"><img src="/images/edit.png(\?\d+)?" alt="Edit" /></a></div>' +
1247 1247 '<a name="Subtitle-with-inline-code"></a>' +
1248 1248 '<h2 >Subtitle with <code>inline code</code><a href="#Subtitle-with-inline-code" class="wiki-anchor">&para;</a></h2>'),
1249 1249 result
1250 1250
1251 1251 # last heading
1252 1252 assert_match Regexp.new('<div class="contextual" title="Edit this section" id="section-5">' +
1253 1253 '<a href="/projects/1/wiki/Test/edit\?section=5"><img src="/images/edit.png(\?\d+)?" alt="Edit" /></a></div>' +
1254 1254 '<a name="Subtitle-after-pre-tag"></a>' +
1255 1255 '<h2 >Subtitle after pre tag<a href="#Subtitle-after-pre-tag" class="wiki-anchor">&para;</a></h2>'),
1256 1256 result
1257 1257 end
1258 1258
1259 1259 def test_default_formatter
1260 1260 with_settings :text_formatting => 'unknown' do
1261 1261 text = 'a *link*: http://www.example.net/'
1262 1262 assert_equal '<p>a *link*: <a class="external" href="http://www.example.net/">http://www.example.net/</a></p>', textilizable(text)
1263 1263 end
1264 1264 end
1265 1265
1266 def test_parse_redmine_links_should_handle_a_tag_without_attributes
1267 text = '<a>http://example.com</a>'
1268 expected = text.dup
1269 parse_redmine_links(text, nil, nil, nil, true, {})
1270 assert_equal expected, text
1271 end
1272
1266 1273 def test_due_date_distance_in_words
1267 1274 to_test = { Date.today => 'Due in 0 days',
1268 1275 Date.today + 1 => 'Due in 1 day',
1269 1276 Date.today + 100 => 'Due in about 3 months',
1270 1277 Date.today + 20000 => 'Due in over 54 years',
1271 1278 Date.today - 1 => '1 day late',
1272 1279 Date.today - 100 => 'about 3 months late',
1273 1280 Date.today - 20000 => 'over 54 years late',
1274 1281 }
1275 1282 ::I18n.locale = :en
1276 1283 to_test.each do |date, expected|
1277 1284 assert_equal expected, due_date_distance_in_words(date)
1278 1285 end
1279 1286 end
1280 1287
1281 1288 def test_avatar_enabled
1282 1289 with_settings :gravatar_enabled => '1' do
1283 1290 assert avatar(User.find_by_mail('jsmith@somenet.foo')).include?(Digest::MD5.hexdigest('jsmith@somenet.foo'))
1284 1291 assert avatar('jsmith <jsmith@somenet.foo>').include?(Digest::MD5.hexdigest('jsmith@somenet.foo'))
1285 1292 # Default size is 50
1286 1293 assert avatar('jsmith <jsmith@somenet.foo>').include?('size=50')
1287 1294 assert avatar('jsmith <jsmith@somenet.foo>', :size => 24).include?('size=24')
1288 1295 # Non-avatar options should be considered html options
1289 1296 assert avatar('jsmith <jsmith@somenet.foo>', :title => 'John Smith').include?('title="John Smith"')
1290 1297 # The default class of the img tag should be gravatar
1291 1298 assert avatar('jsmith <jsmith@somenet.foo>').include?('class="gravatar"')
1292 1299 assert !avatar('jsmith <jsmith@somenet.foo>', :class => 'picture').include?('class="gravatar"')
1293 1300 assert_nil avatar('jsmith')
1294 1301 assert_nil avatar(nil)
1295 1302 end
1296 1303 end
1297 1304
1298 1305 def test_avatar_disabled
1299 1306 with_settings :gravatar_enabled => '0' do
1300 1307 assert_equal '', avatar(User.find_by_mail('jsmith@somenet.foo'))
1301 1308 end
1302 1309 end
1303 1310
1304 1311 def test_link_to_user
1305 1312 user = User.find(2)
1306 1313 result = link_to("John Smith", "/users/2", :class => "user active")
1307 1314 assert_equal result, link_to_user(user)
1308 1315 end
1309 1316
1310 1317 def test_link_to_user_should_not_link_to_locked_user
1311 1318 with_current_user nil do
1312 1319 user = User.find(5)
1313 1320 assert user.locked?
1314 1321 assert_equal 'Dave2 Lopper2', link_to_user(user)
1315 1322 end
1316 1323 end
1317 1324
1318 1325 def test_link_to_user_should_link_to_locked_user_if_current_user_is_admin
1319 1326 with_current_user User.find(1) do
1320 1327 user = User.find(5)
1321 1328 assert user.locked?
1322 1329 result = link_to("Dave2 Lopper2", "/users/5", :class => "user locked")
1323 1330 assert_equal result, link_to_user(user)
1324 1331 end
1325 1332 end
1326 1333
1327 1334 def test_link_to_user_should_not_link_to_anonymous
1328 1335 user = User.anonymous
1329 1336 assert user.anonymous?
1330 1337 t = link_to_user(user)
1331 1338 assert_equal ::I18n.t(:label_user_anonymous), t
1332 1339 end
1333 1340
1334 1341 def test_link_to_attachment
1335 1342 a = Attachment.find(3)
1336 1343 assert_equal '<a href="/attachments/3/logo.gif">logo.gif</a>',
1337 1344 link_to_attachment(a)
1338 1345 assert_equal '<a href="/attachments/3/logo.gif">Text</a>',
1339 1346 link_to_attachment(a, :text => 'Text')
1340 1347 result = link_to("logo.gif", "/attachments/3/logo.gif", :class => "foo")
1341 1348 assert_equal result,
1342 1349 link_to_attachment(a, :class => 'foo')
1343 1350 assert_equal '<a href="/attachments/download/3/logo.gif">logo.gif</a>',
1344 1351 link_to_attachment(a, :download => true)
1345 1352 assert_equal '<a href="http://test.host/attachments/3/logo.gif">logo.gif</a>',
1346 1353 link_to_attachment(a, :only_path => false)
1347 1354 end
1348 1355
1349 1356 def test_thumbnail_tag
1350 1357 a = Attachment.find(3)
1351 1358 assert_select_in thumbnail_tag(a),
1352 1359 'a[href=?][title=?] img[alt="3"][src=?]',
1353 1360 "/attachments/3/logo.gif", "logo.gif", "/attachments/thumbnail/3"
1354 1361 end
1355 1362
1356 1363 def test_link_to_project
1357 1364 project = Project.find(1)
1358 1365 assert_equal %(<a href="/projects/ecookbook">eCookbook</a>),
1359 1366 link_to_project(project)
1360 1367 assert_equal %(<a href="http://test.host/projects/ecookbook?jump=blah">eCookbook</a>),
1361 1368 link_to_project(project, {:only_path => false, :jump => 'blah'})
1362 1369 end
1363 1370
1364 1371 def test_link_to_project_settings
1365 1372 project = Project.find(1)
1366 1373 assert_equal '<a href="/projects/ecookbook/settings">eCookbook</a>', link_to_project_settings(project)
1367 1374
1368 1375 project.status = Project::STATUS_CLOSED
1369 1376 assert_equal '<a href="/projects/ecookbook">eCookbook</a>', link_to_project_settings(project)
1370 1377
1371 1378 project.status = Project::STATUS_ARCHIVED
1372 1379 assert_equal 'eCookbook', link_to_project_settings(project)
1373 1380 end
1374 1381
1375 1382 def test_link_to_legacy_project_with_numerical_identifier_should_use_id
1376 1383 # numeric identifier are no longer allowed
1377 1384 Project.where(:id => 1).update_all(:identifier => 25)
1378 1385 assert_equal '<a href="/projects/1">eCookbook</a>',
1379 1386 link_to_project(Project.find(1))
1380 1387 end
1381 1388
1382 1389 def test_principals_options_for_select_with_users
1383 1390 User.current = nil
1384 1391 users = [User.find(2), User.find(4)]
1385 1392 assert_equal %(<option value="2">John Smith</option><option value="4">Robert Hill</option>),
1386 1393 principals_options_for_select(users)
1387 1394 end
1388 1395
1389 1396 def test_principals_options_for_select_with_selected
1390 1397 User.current = nil
1391 1398 users = [User.find(2), User.find(4)]
1392 1399 assert_equal %(<option value="2">John Smith</option><option value="4" selected="selected">Robert Hill</option>),
1393 1400 principals_options_for_select(users, User.find(4))
1394 1401 end
1395 1402
1396 1403 def test_principals_options_for_select_with_users_and_groups
1397 1404 User.current = nil
1398 1405 set_language_if_valid 'en'
1399 1406 users = [User.find(2), Group.find(11), User.find(4), Group.find(10)]
1400 1407 assert_equal %(<option value="2">John Smith</option><option value="4">Robert Hill</option>) +
1401 1408 %(<optgroup label="Groups"><option value="10">A Team</option><option value="11">B Team</option></optgroup>),
1402 1409 principals_options_for_select(users)
1403 1410 end
1404 1411
1405 1412 def test_principals_options_for_select_with_empty_collection
1406 1413 assert_equal '', principals_options_for_select([])
1407 1414 end
1408 1415
1409 1416 def test_principals_options_for_select_should_include_me_option_when_current_user_is_in_collection
1410 1417 set_language_if_valid 'en'
1411 1418 users = [User.find(2), User.find(4)]
1412 1419 User.current = User.find(4)
1413 1420 assert_include '<option value="4">&lt;&lt; me &gt;&gt;</option>', principals_options_for_select(users)
1414 1421 end
1415 1422
1416 1423 def test_stylesheet_link_tag_should_pick_the_default_stylesheet
1417 1424 assert_match 'href="/stylesheets/styles.css"', stylesheet_link_tag("styles")
1418 1425 end
1419 1426
1420 1427 def test_stylesheet_link_tag_for_plugin_should_pick_the_plugin_stylesheet
1421 1428 assert_match 'href="/plugin_assets/foo/stylesheets/styles.css"', stylesheet_link_tag("styles", :plugin => :foo)
1422 1429 end
1423 1430
1424 1431 def test_image_tag_should_pick_the_default_image
1425 1432 assert_match 'src="/images/image.png"', image_tag("image.png")
1426 1433 end
1427 1434
1428 1435 def test_image_tag_should_pick_the_theme_image_if_it_exists
1429 1436 theme = Redmine::Themes.themes.last
1430 1437 theme.images << 'image.png'
1431 1438
1432 1439 with_settings :ui_theme => theme.id do
1433 1440 assert_match %|src="/themes/#{theme.dir}/images/image.png"|, image_tag("image.png")
1434 1441 assert_match %|src="/images/other.png"|, image_tag("other.png")
1435 1442 end
1436 1443 ensure
1437 1444 theme.images.delete 'image.png'
1438 1445 end
1439 1446
1440 1447 def test_image_tag_sfor_plugin_should_pick_the_plugin_image
1441 1448 assert_match 'src="/plugin_assets/foo/images/image.png"', image_tag("image.png", :plugin => :foo)
1442 1449 end
1443 1450
1444 1451 def test_javascript_include_tag_should_pick_the_default_javascript
1445 1452 assert_match 'src="/javascripts/scripts.js"', javascript_include_tag("scripts")
1446 1453 end
1447 1454
1448 1455 def test_javascript_include_tag_for_plugin_should_pick_the_plugin_javascript
1449 1456 assert_match 'src="/plugin_assets/foo/javascripts/scripts.js"', javascript_include_tag("scripts", :plugin => :foo)
1450 1457 end
1451 1458
1452 1459 def test_raw_json_should_escape_closing_tags
1453 1460 s = raw_json(["<foo>bar</foo>"])
1454 1461 assert_include '\/foo', s
1455 1462 end
1456 1463
1457 1464 def test_raw_json_should_be_html_safe
1458 1465 s = raw_json(["foo"])
1459 1466 assert s.html_safe?
1460 1467 end
1461 1468
1462 1469 def test_html_title_should_app_title_if_not_set
1463 1470 assert_equal 'Redmine', html_title
1464 1471 end
1465 1472
1466 1473 def test_html_title_should_join_items
1467 1474 html_title 'Foo', 'Bar'
1468 1475 assert_equal 'Foo - Bar - Redmine', html_title
1469 1476 end
1470 1477
1471 1478 def test_html_title_should_append_current_project_name
1472 1479 @project = Project.find(1)
1473 1480 html_title 'Foo', 'Bar'
1474 1481 assert_equal 'Foo - Bar - eCookbook - Redmine', html_title
1475 1482 end
1476 1483
1477 1484 def test_title_should_return_a_h2_tag
1478 1485 assert_equal '<h2>Foo</h2>', title('Foo')
1479 1486 end
1480 1487
1481 1488 def test_title_should_set_html_title
1482 1489 title('Foo')
1483 1490 assert_equal 'Foo - Redmine', html_title
1484 1491 end
1485 1492
1486 1493 def test_title_should_turn_arrays_into_links
1487 1494 assert_equal '<h2><a href="/foo">Foo</a></h2>', title(['Foo', '/foo'])
1488 1495 assert_equal 'Foo - Redmine', html_title
1489 1496 end
1490 1497
1491 1498 def test_title_should_join_items
1492 1499 assert_equal '<h2>Foo &#187; Bar</h2>', title('Foo', 'Bar')
1493 1500 assert_equal 'Bar - Foo - Redmine', html_title
1494 1501 end
1495 1502
1496 1503 def test_favicon_path
1497 1504 assert_match %r{^/favicon\.ico}, favicon_path
1498 1505 end
1499 1506
1500 1507 def test_favicon_path_with_suburi
1501 1508 Redmine::Utils.relative_url_root = '/foo'
1502 1509 assert_match %r{^/foo/favicon\.ico}, favicon_path
1503 1510 ensure
1504 1511 Redmine::Utils.relative_url_root = ''
1505 1512 end
1506 1513
1507 1514 def test_favicon_url
1508 1515 assert_match %r{^http://test\.host/favicon\.ico}, favicon_url
1509 1516 end
1510 1517
1511 1518 def test_favicon_url_with_suburi
1512 1519 Redmine::Utils.relative_url_root = '/foo'
1513 1520 assert_match %r{^http://test\.host/foo/favicon\.ico}, favicon_url
1514 1521 ensure
1515 1522 Redmine::Utils.relative_url_root = ''
1516 1523 end
1517 1524
1518 1525 def test_truncate_single_line
1519 1526 str = "01234"
1520 1527 result = truncate_single_line_raw("#{str}\n#{str}", 10)
1521 1528 assert_equal "01234 0...", result
1522 1529 assert !result.html_safe?
1523 1530 result = truncate_single_line_raw("#{str}<&#>\n#{str}#{str}", 16)
1524 1531 assert_equal "01234<&#> 012...", result
1525 1532 assert !result.html_safe?
1526 1533 end
1527 1534
1528 1535 def test_truncate_single_line_non_ascii
1529 1536 ja = "\xe6\x97\xa5\xe6\x9c\xac\xe8\xaa\x9e".force_encoding('UTF-8')
1530 1537 result = truncate_single_line_raw("#{ja}\n#{ja}\n#{ja}", 10)
1531 1538 assert_equal "#{ja} #{ja}...", result
1532 1539 assert !result.html_safe?
1533 1540 end
1534 1541 end
General Comments 0
You need to be logged in to leave comments. Login now