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