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