##// END OF EJS Templates
Don't escape already parsed wiki link title (#9471)....
Etienne Massip -
r7578:1032210edd00
parent child
Show More
@@ -1,986 +1,986
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2011 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require 'forwardable'
19 19 require 'cgi'
20 20
21 21 module ApplicationHelper
22 22 include Redmine::WikiFormatting::Macros::Definitions
23 23 include Redmine::I18n
24 24 include GravatarHelper::PublicMethods
25 25
26 26 extend Forwardable
27 27 def_delegators :wiki_helper, :wikitoolbar_for, :heads_for_wiki_formatter
28 28
29 29 # Return true if user is authorized for controller/action, otherwise false
30 30 def authorize_for(controller, action)
31 31 User.current.allowed_to?({:controller => controller, :action => action}, @project)
32 32 end
33 33
34 34 # Display a link if user is authorized
35 35 #
36 36 # @param [String] name Anchor text (passed to link_to)
37 37 # @param [Hash] options Hash params. This will checked by authorize_for to see if the user is authorized
38 38 # @param [optional, Hash] html_options Options passed to link_to
39 39 # @param [optional, Hash] parameters_for_method_reference Extra parameters for link_to
40 40 def link_to_if_authorized(name, options = {}, html_options = nil, *parameters_for_method_reference)
41 41 link_to(name, options, html_options, *parameters_for_method_reference) if authorize_for(options[:controller] || params[:controller], options[:action])
42 42 end
43 43
44 44 # Display a link to remote if user is authorized
45 45 def link_to_remote_if_authorized(name, options = {}, html_options = nil)
46 46 url = options[:url] || {}
47 47 link_to_remote(name, options, html_options) if authorize_for(url[:controller] || params[:controller], url[:action])
48 48 end
49 49
50 50 # Displays a link to user's account page if active
51 51 def link_to_user(user, options={})
52 52 if user.is_a?(User)
53 53 name = h(user.name(options[:format]))
54 54 if user.active?
55 55 link_to name, :controller => 'users', :action => 'show', :id => user
56 56 else
57 57 name
58 58 end
59 59 else
60 60 h(user.to_s)
61 61 end
62 62 end
63 63
64 64 # Displays a link to +issue+ with its subject.
65 65 # Examples:
66 66 #
67 67 # link_to_issue(issue) # => Defect #6: This is the subject
68 68 # link_to_issue(issue, :truncate => 6) # => Defect #6: This i...
69 69 # link_to_issue(issue, :subject => false) # => Defect #6
70 70 # link_to_issue(issue, :project => true) # => Foo - Defect #6
71 71 #
72 72 def link_to_issue(issue, options={})
73 73 title = nil
74 74 subject = nil
75 75 if options[:subject] == false
76 76 title = truncate(issue.subject, :length => 60)
77 77 else
78 78 subject = issue.subject
79 79 if options[:truncate]
80 80 subject = truncate(subject, :length => options[:truncate])
81 81 end
82 82 end
83 83 s = link_to "#{h(issue.tracker)} ##{issue.id}", {:controller => "issues", :action => "show", :id => issue},
84 84 :class => issue.css_classes,
85 85 :title => title
86 86 s << ": #{h subject}" if subject
87 87 s = "#{h issue.project} - " + s if options[:project]
88 88 s
89 89 end
90 90
91 91 # Generates a link to an attachment.
92 92 # Options:
93 93 # * :text - Link text (default to attachment filename)
94 94 # * :download - Force download (default: false)
95 95 def link_to_attachment(attachment, options={})
96 96 text = options.delete(:text) || attachment.filename
97 97 action = options.delete(:download) ? 'download' : 'show'
98 98
99 99 link_to(h(text), {:controller => 'attachments', :action => action, :id => attachment, :filename => attachment.filename }, 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, project, options={})
106 106 text = options.delete(:text) || format_revision(revision)
107 107 rev = revision.respond_to?(:identifier) ? revision.identifier : revision
108 108
109 109 link_to(h(text), {:controller => 'repositories', :action => 'revision', :id => project, :rev => rev},
110 110 :title => l(:label_revision_id, format_revision(revision)))
111 111 end
112 112
113 113 # Generates a link to a message
114 114 def link_to_message(message, options={}, html_options = nil)
115 115 link_to(
116 116 h(truncate(message.subject, :length => 60)),
117 117 { :controller => 'messages', :action => 'show',
118 118 :board_id => message.board_id,
119 119 :id => message.root,
120 120 :r => (message.parent_id && message.id),
121 121 :anchor => (message.parent_id ? "message-#{message.id}" : nil)
122 122 }.merge(options),
123 123 html_options
124 124 )
125 125 end
126 126
127 127 # Generates a link to a project if active
128 128 # Examples:
129 129 #
130 130 # link_to_project(project) # => link to the specified project overview
131 131 # link_to_project(project, :action=>'settings') # => link to project settings
132 132 # link_to_project(project, {:only_path => false}, :class => "project") # => 3rd arg adds html options
133 133 # link_to_project(project, {}, :class => "project") # => html options with default url (project overview)
134 134 #
135 135 def link_to_project(project, options={}, html_options = nil)
136 136 if project.active?
137 137 url = {:controller => 'projects', :action => 'show', :id => project}.merge(options)
138 138 link_to(h(project), url, html_options)
139 139 else
140 140 h(project)
141 141 end
142 142 end
143 143
144 144 def toggle_link(name, id, options={})
145 145 onclick = "Element.toggle('#{id}'); "
146 146 onclick << (options[:focus] ? "Form.Element.focus('#{options[:focus]}'); " : "this.blur(); ")
147 147 onclick << "return false;"
148 148 link_to(name, "#", :onclick => onclick)
149 149 end
150 150
151 151 def image_to_function(name, function, html_options = {})
152 152 html_options.symbolize_keys!
153 153 tag(:input, html_options.merge({
154 154 :type => "image", :src => image_path(name),
155 155 :onclick => (html_options[:onclick] ? "#{html_options[:onclick]}; " : "") + "#{function};"
156 156 }))
157 157 end
158 158
159 159 def prompt_to_remote(name, text, param, url, html_options = {})
160 160 html_options[:onclick] = "promptToRemote('#{text}', '#{param}', '#{url_for(url)}'); return false;"
161 161 link_to name, {}, html_options
162 162 end
163 163
164 164 def format_activity_title(text)
165 165 h(truncate_single_line(text, :length => 100))
166 166 end
167 167
168 168 def format_activity_day(date)
169 169 date == Date.today ? l(:label_today).titleize : format_date(date)
170 170 end
171 171
172 172 def format_activity_description(text)
173 173 h(truncate(text.to_s, :length => 120).gsub(%r{[\r\n]*<(pre|code)>.*$}m, '...')).gsub(/[\r\n]+/, "<br />")
174 174 end
175 175
176 176 def format_version_name(version)
177 177 if version.project == @project
178 178 h(version)
179 179 else
180 180 h("#{version.project} - #{version}")
181 181 end
182 182 end
183 183
184 184 def due_date_distance_in_words(date)
185 185 if date
186 186 l((date < Date.today ? :label_roadmap_overdue : :label_roadmap_due_in), distance_of_date_in_words(Date.today, date))
187 187 end
188 188 end
189 189
190 190 def render_page_hierarchy(pages, node=nil, options={})
191 191 content = ''
192 192 if pages[node]
193 193 content << "<ul class=\"pages-hierarchy\">\n"
194 194 pages[node].each do |page|
195 195 content << "<li>"
196 196 content << link_to(h(page.pretty_title), {:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title},
197 197 :title => (options[:timestamp] && page.updated_on ? l(:label_updated_time, distance_of_time_in_words(Time.now, page.updated_on)) : nil))
198 198 content << "\n" + render_page_hierarchy(pages, page.id, options) if pages[page.id]
199 199 content << "</li>\n"
200 200 end
201 201 content << "</ul>\n"
202 202 end
203 203 content.html_safe
204 204 end
205 205
206 206 # Renders flash messages
207 207 def render_flash_messages
208 208 s = ''
209 209 flash.each do |k,v|
210 210 s << content_tag('div', v, :class => "flash #{k}")
211 211 end
212 212 s.html_safe
213 213 end
214 214
215 215 # Renders tabs and their content
216 216 def render_tabs(tabs)
217 217 if tabs.any?
218 218 render :partial => 'common/tabs', :locals => {:tabs => tabs}
219 219 else
220 220 content_tag 'p', l(:label_no_data), :class => "nodata"
221 221 end
222 222 end
223 223
224 224 # Renders the project quick-jump box
225 225 def render_project_jump_box
226 226 return unless User.current.logged?
227 227 projects = User.current.memberships.collect(&:project).compact.uniq
228 228 if projects.any?
229 229 s = '<select onchange="if (this.value != \'\') { window.location = this.value; }">' +
230 230 "<option value=''>#{ l(:label_jump_to_a_project) }</option>" +
231 231 '<option value="" disabled="disabled">---</option>'
232 232 s << project_tree_options_for_select(projects, :selected => @project) do |p|
233 233 { :value => url_for(:controller => 'projects', :action => 'show', :id => p, :jump => current_menu_item) }
234 234 end
235 235 s << '</select>'
236 236 s.html_safe
237 237 end
238 238 end
239 239
240 240 def project_tree_options_for_select(projects, options = {})
241 241 s = ''
242 242 project_tree(projects) do |project, level|
243 243 name_prefix = (level > 0 ? ('&nbsp;' * 2 * level + '&#187; ') : '')
244 244 tag_options = {:value => project.id}
245 245 if project == options[:selected] || (options[:selected].respond_to?(:include?) && options[:selected].include?(project))
246 246 tag_options[:selected] = 'selected'
247 247 else
248 248 tag_options[:selected] = nil
249 249 end
250 250 tag_options.merge!(yield(project)) if block_given?
251 251 s << content_tag('option', name_prefix + h(project), tag_options)
252 252 end
253 253 s.html_safe
254 254 end
255 255
256 256 # Yields the given block for each project with its level in the tree
257 257 #
258 258 # Wrapper for Project#project_tree
259 259 def project_tree(projects, &block)
260 260 Project.project_tree(projects, &block)
261 261 end
262 262
263 263 def project_nested_ul(projects, &block)
264 264 s = ''
265 265 if projects.any?
266 266 ancestors = []
267 267 projects.sort_by(&:lft).each do |project|
268 268 if (ancestors.empty? || project.is_descendant_of?(ancestors.last))
269 269 s << "<ul>\n"
270 270 else
271 271 ancestors.pop
272 272 s << "</li>"
273 273 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
274 274 ancestors.pop
275 275 s << "</ul></li>\n"
276 276 end
277 277 end
278 278 s << "<li>"
279 279 s << yield(project).to_s
280 280 ancestors << project
281 281 end
282 282 s << ("</li></ul>\n" * ancestors.size)
283 283 end
284 284 s.html_safe
285 285 end
286 286
287 287 def principals_check_box_tags(name, principals)
288 288 s = ''
289 289 principals.sort.each do |principal|
290 290 s << "<label>#{ check_box_tag name, principal.id, false } #{h principal}</label>\n"
291 291 end
292 292 s.html_safe
293 293 end
294 294
295 295 # Returns a string for users/groups option tags
296 296 def principals_options_for_select(collection, selected=nil)
297 297 s = ''
298 298 groups = ''
299 299 collection.sort.each do |element|
300 300 selected_attribute = ' selected="selected"' if option_value_selected?(element, selected)
301 301 (element.is_a?(Group) ? groups : s) << %(<option value="#{element.id}"#{selected_attribute}>#{h element.name}</option>)
302 302 end
303 303 unless groups.empty?
304 304 s << %(<optgroup label="#{h(l(:label_group_plural))}">#{groups}</optgroup>)
305 305 end
306 306 s
307 307 end
308 308
309 309 # Truncates and returns the string as a single line
310 310 def truncate_single_line(string, *args)
311 311 truncate(string.to_s, *args).gsub(%r{[\r\n]+}m, ' ')
312 312 end
313 313
314 314 # Truncates at line break after 250 characters or options[:length]
315 315 def truncate_lines(string, options={})
316 316 length = options[:length] || 250
317 317 if string.to_s =~ /\A(.{#{length}}.*?)$/m
318 318 "#{$1}..."
319 319 else
320 320 string
321 321 end
322 322 end
323 323
324 324 def html_hours(text)
325 325 text.gsub(%r{(\d+)\.(\d+)}, '<span class="hours hours-int">\1</span><span class="hours hours-dec">.\2</span>').html_safe
326 326 end
327 327
328 328 def authoring(created, author, options={})
329 329 l(options[:label] || :label_added_time_by, :author => link_to_user(author), :age => time_tag(created)).html_safe
330 330 end
331 331
332 332 def time_tag(time)
333 333 text = distance_of_time_in_words(Time.now, time)
334 334 if @project
335 335 link_to(text, {:controller => 'activities', :action => 'index', :id => @project, :from => time.to_date}, :title => format_time(time))
336 336 else
337 337 content_tag('acronym', text, :title => format_time(time))
338 338 end
339 339 end
340 340
341 341 def syntax_highlight(name, content)
342 342 Redmine::SyntaxHighlighting.highlight_by_filename(content, name)
343 343 end
344 344
345 345 def to_path_param(path)
346 346 path.to_s.split(%r{[/\\]}).select {|p| !p.blank?}
347 347 end
348 348
349 349 def pagination_links_full(paginator, count=nil, options={})
350 350 page_param = options.delete(:page_param) || :page
351 351 per_page_links = options.delete(:per_page_links)
352 352 url_param = params.dup
353 353
354 354 html = ''
355 355 if paginator.current.previous
356 356 # \xc2\xab(utf-8) = &#171;
357 357 html << link_to_content_update(
358 358 "\xc2\xab " + l(:label_previous),
359 359 url_param.merge(page_param => paginator.current.previous)) + ' '
360 360 end
361 361
362 362 html << (pagination_links_each(paginator, options) do |n|
363 363 link_to_content_update(n.to_s, url_param.merge(page_param => n))
364 364 end || '')
365 365
366 366 if paginator.current.next
367 367 # \xc2\xbb(utf-8) = &#187;
368 368 html << ' ' + link_to_content_update(
369 369 (l(:label_next) + " \xc2\xbb"),
370 370 url_param.merge(page_param => paginator.current.next))
371 371 end
372 372
373 373 unless count.nil?
374 374 html << " (#{paginator.current.first_item}-#{paginator.current.last_item}/#{count})"
375 375 if per_page_links != false && links = per_page_links(paginator.items_per_page)
376 376 html << " | #{links}"
377 377 end
378 378 end
379 379
380 380 html.html_safe
381 381 end
382 382
383 383 def per_page_links(selected=nil)
384 384 links = Setting.per_page_options_array.collect do |n|
385 385 n == selected ? n : link_to_content_update(n, params.merge(:per_page => n))
386 386 end
387 387 links.size > 1 ? l(:label_display_per_page, links.join(', ')) : nil
388 388 end
389 389
390 390 def reorder_links(name, url)
391 391 link_to(image_tag('2uparrow.png', :alt => l(:label_sort_highest)),
392 392 url.merge({"#{name}[move_to]" => 'highest'}),
393 393 :method => :post, :title => l(:label_sort_highest)) +
394 394 link_to(image_tag('1uparrow.png', :alt => l(:label_sort_higher)),
395 395 url.merge({"#{name}[move_to]" => 'higher'}),
396 396 :method => :post, :title => l(:label_sort_higher)) +
397 397 link_to(image_tag('1downarrow.png', :alt => l(:label_sort_lower)),
398 398 url.merge({"#{name}[move_to]" => 'lower'}),
399 399 :method => :post, :title => l(:label_sort_lower)) +
400 400 link_to(image_tag('2downarrow.png', :alt => l(:label_sort_lowest)),
401 401 url.merge({"#{name}[move_to]" => 'lowest'}),
402 402 :method => :post, :title => l(:label_sort_lowest))
403 403 end
404 404
405 405 def breadcrumb(*args)
406 406 elements = args.flatten
407 407 elements.any? ? content_tag('p', (args.join(" \xc2\xbb ") + " \xc2\xbb ").html_safe, :class => 'breadcrumb') : nil
408 408 end
409 409
410 410 def other_formats_links(&block)
411 411 concat('<p class="other-formats">' + l(:label_export_to))
412 412 yield Redmine::Views::OtherFormatsBuilder.new(self)
413 413 concat('</p>')
414 414 end
415 415
416 416 def page_header_title
417 417 if @project.nil? || @project.new_record?
418 418 h(Setting.app_title)
419 419 else
420 420 b = []
421 421 ancestors = (@project.root? ? [] : @project.ancestors.visible.all)
422 422 if ancestors.any?
423 423 root = ancestors.shift
424 424 b << link_to_project(root, {:jump => current_menu_item}, :class => 'root')
425 425 if ancestors.size > 2
426 426 b << "\xe2\x80\xa6"
427 427 ancestors = ancestors[-2, 2]
428 428 end
429 429 b += ancestors.collect {|p| link_to_project(p, {:jump => current_menu_item}, :class => 'ancestor') }
430 430 end
431 431 b << h(@project)
432 432 b.join(" \xc2\xbb ").html_safe
433 433 end
434 434 end
435 435
436 436 def html_title(*args)
437 437 if args.empty?
438 438 title = []
439 439 title << @project.name if @project
440 440 title += @html_title if @html_title
441 441 title << Setting.app_title
442 442 title.select {|t| !t.blank? }.join(' - ')
443 443 else
444 444 @html_title ||= []
445 445 @html_title += args
446 446 end
447 447 end
448 448
449 449 # Returns the theme, controller name, and action as css classes for the
450 450 # HTML body.
451 451 def body_css_classes
452 452 css = []
453 453 if theme = Redmine::Themes.theme(Setting.ui_theme)
454 454 css << 'theme-' + theme.name
455 455 end
456 456
457 457 css << 'controller-' + params[:controller]
458 458 css << 'action-' + params[:action]
459 459 css.join(' ')
460 460 end
461 461
462 462 def accesskey(s)
463 463 Redmine::AccessKeys.key_for s
464 464 end
465 465
466 466 # Formats text according to system settings.
467 467 # 2 ways to call this method:
468 468 # * with a String: textilizable(text, options)
469 469 # * with an object and one of its attribute: textilizable(issue, :description, options)
470 470 def textilizable(*args)
471 471 options = args.last.is_a?(Hash) ? args.pop : {}
472 472 case args.size
473 473 when 1
474 474 obj = options[:object]
475 475 text = args.shift
476 476 when 2
477 477 obj = args.shift
478 478 attr = args.shift
479 479 text = obj.send(attr).to_s
480 480 else
481 481 raise ArgumentError, 'invalid arguments to textilizable'
482 482 end
483 483 return '' if text.blank?
484 484 project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
485 485 only_path = options.delete(:only_path) == false ? false : true
486 486
487 487 text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text, :object => obj, :attribute => attr) { |macro, args| exec_macro(macro, obj, args) }
488 488
489 489 @parsed_headings = []
490 490 text = parse_non_pre_blocks(text) do |text|
491 491 [:parse_inline_attachments, :parse_wiki_links, :parse_redmine_links, :parse_headings].each do |method_name|
492 492 send method_name, text, project, obj, attr, only_path, options
493 493 end
494 494 end
495 495
496 496 if @parsed_headings.any?
497 497 replace_toc(text, @parsed_headings)
498 498 end
499 499
500 500 text
501 501 end
502 502
503 503 def parse_non_pre_blocks(text)
504 504 s = StringScanner.new(text)
505 505 tags = []
506 506 parsed = ''
507 507 while !s.eos?
508 508 s.scan(/(.*?)(<(\/)?(pre|code)(.*?)>|\z)/im)
509 509 text, full_tag, closing, tag = s[1], s[2], s[3], s[4]
510 510 if tags.empty?
511 511 yield text
512 512 end
513 513 parsed << text
514 514 if tag
515 515 if closing
516 516 if tags.last == tag.downcase
517 517 tags.pop
518 518 end
519 519 else
520 520 tags << tag.downcase
521 521 end
522 522 parsed << full_tag
523 523 end
524 524 end
525 525 # Close any non closing tags
526 526 while tag = tags.pop
527 527 parsed << "</#{tag}>"
528 528 end
529 529 parsed.html_safe
530 530 end
531 531
532 532 def parse_inline_attachments(text, project, obj, attr, only_path, options)
533 533 # when using an image link, try to use an attachment, if possible
534 534 if options[:attachments] || (obj && obj.respond_to?(:attachments))
535 535 attachments = nil
536 536 text.gsub!(/src="([^\/"]+\.(bmp|gif|jpg|jpeg|png))"(\s+alt="([^"]*)")?/i) do |m|
537 537 filename, ext, alt, alttext = $1.downcase, $2, $3, $4
538 538 attachments ||= (options[:attachments] || obj.attachments).sort_by(&:created_on).reverse
539 539 # search for the picture in attachments
540 540 if found = attachments.detect { |att| att.filename.downcase == filename }
541 541 image_url = url_for :only_path => only_path, :controller => 'attachments', :action => 'download', :id => found
542 542 desc = found.description.to_s.gsub('"', '')
543 543 if !desc.blank? && alttext.blank?
544 544 alt = " title=\"#{desc}\" alt=\"#{desc}\""
545 545 end
546 546 "src=\"#{image_url}\"#{alt}".html_safe
547 547 else
548 548 m.html_safe
549 549 end
550 550 end
551 551 end
552 552 end
553 553
554 554 # Wiki links
555 555 #
556 556 # Examples:
557 557 # [[mypage]]
558 558 # [[mypage|mytext]]
559 559 # wiki links can refer other project wikis, using project name or identifier:
560 560 # [[project:]] -> wiki starting page
561 561 # [[project:|mytext]]
562 562 # [[project:mypage]]
563 563 # [[project:mypage|mytext]]
564 564 def parse_wiki_links(text, project, obj, attr, only_path, options)
565 565 text.gsub!(/(!)?(\[\[([^\]\n\|]+)(\|([^\]\n\|]+))?\]\])/) do |m|
566 566 link_project = project
567 567 esc, all, page, title = $1, $2, $3, $5
568 568 if esc.nil?
569 569 if page =~ /^([^\:]+)\:(.*)$/
570 570 link_project = Project.find_by_identifier($1) || Project.find_by_name($1)
571 571 page = $2
572 572 title ||= $1 if page.blank?
573 573 end
574 574
575 575 if link_project && link_project.wiki
576 576 # extract anchor
577 577 anchor = nil
578 578 if page =~ /^(.+?)\#(.+)$/
579 579 page, anchor = $1, $2
580 580 end
581 581 anchor = sanitize_anchor_name(anchor) if anchor.present?
582 582 # check if page exists
583 583 wiki_page = link_project.wiki.find_page(page)
584 584 url = if anchor.present? && wiki_page.present? && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version)) && obj.page == wiki_page
585 585 "##{anchor}"
586 586 else
587 587 case options[:wiki_links]
588 588 when :local; "#{page.present? ? Wiki.titleize(page) : ''}.html" + (anchor.present? ? "##{anchor}" : '')
589 589 when :anchor; "##{page.present? ? Wiki.titleize(page) : title}" + (anchor.present? ? "_#{anchor}" : '') # used for single-file wiki export
590 590 else
591 591 wiki_page_id = page.present? ? Wiki.titleize(page) : nil
592 592 url_for(:only_path => only_path, :controller => 'wiki', :action => 'show', :project_id => link_project, :id => wiki_page_id, :anchor => anchor)
593 593 end
594 594 end
595 link_to(h(title || page), url, :class => ('wiki-page' + (wiki_page ? '' : ' new')))
595 link_to(title || h(page), url, :class => ('wiki-page' + (wiki_page ? '' : ' new')))
596 596 else
597 597 # project or wiki doesn't exist
598 598 all.html_safe
599 599 end
600 600 else
601 601 all.html_safe
602 602 end
603 603 end
604 604 end
605 605
606 606 # Redmine links
607 607 #
608 608 # Examples:
609 609 # Issues:
610 610 # #52 -> Link to issue #52
611 611 # Changesets:
612 612 # r52 -> Link to revision 52
613 613 # commit:a85130f -> Link to scmid starting with a85130f
614 614 # Documents:
615 615 # document#17 -> Link to document with id 17
616 616 # document:Greetings -> Link to the document with title "Greetings"
617 617 # document:"Some document" -> Link to the document with title "Some document"
618 618 # Versions:
619 619 # version#3 -> Link to version with id 3
620 620 # version:1.0.0 -> Link to version named "1.0.0"
621 621 # version:"1.0 beta 2" -> Link to version named "1.0 beta 2"
622 622 # Attachments:
623 623 # attachment:file.zip -> Link to the attachment of the current object named file.zip
624 624 # Source files:
625 625 # source:some/file -> Link to the file located at /some/file in the project's repository
626 626 # source:some/file@52 -> Link to the file's revision 52
627 627 # source:some/file#L120 -> Link to line 120 of the file
628 628 # source:some/file@52#L120 -> Link to line 120 of the file's revision 52
629 629 # export:some/file -> Force the download of the file
630 630 # Forum messages:
631 631 # message#1218 -> Link to message with id 1218
632 632 #
633 633 # Links can refer other objects from other projects, using project identifier:
634 634 # identifier:r52
635 635 # identifier:document:"Some document"
636 636 # identifier:version:1.0.0
637 637 # identifier:source:some/file
638 638 def parse_redmine_links(text, project, obj, attr, only_path, options)
639 639 text.gsub!(%r{([\s\(,\-\[\>]|^)(!)?(([a-z0-9\-]+):)?(attachment|document|version|commit|source|export|message|project)?((#|r)(\d+)|(:)([^"\s<>][^\s<>]*?|"[^"]+?"))(?=(?=[[:punct:]]\W)|,|\s|\]|<|$)}) do |m|
640 640 leading, esc, project_prefix, project_identifier, prefix, sep, identifier = $1, $2, $3, $4, $5, $7 || $9, $8 || $10
641 641 link = nil
642 642 if project_identifier
643 643 project = Project.visible.find_by_identifier(project_identifier)
644 644 end
645 645 if esc.nil?
646 646 if prefix.nil? && sep == 'r'
647 647 # project.changesets.visible raises an SQL error because of a double join on repositories
648 648 if project && project.repository && (changeset = Changeset.visible.find_by_repository_id_and_revision(project.repository.id, identifier))
649 649 link = link_to(h("#{project_prefix}r#{identifier}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :rev => changeset.revision},
650 650 :class => 'changeset',
651 651 :title => truncate_single_line(changeset.comments, :length => 100))
652 652 end
653 653 elsif sep == '#'
654 654 oid = identifier.to_i
655 655 case prefix
656 656 when nil
657 657 if issue = Issue.visible.find_by_id(oid, :include => :status)
658 658 link = link_to("##{oid}", {:only_path => only_path, :controller => 'issues', :action => 'show', :id => oid},
659 659 :class => issue.css_classes,
660 660 :title => "#{truncate(issue.subject, :length => 100)} (#{issue.status.name})")
661 661 end
662 662 when 'document'
663 663 if document = Document.visible.find_by_id(oid)
664 664 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
665 665 :class => 'document'
666 666 end
667 667 when 'version'
668 668 if version = Version.visible.find_by_id(oid)
669 669 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
670 670 :class => 'version'
671 671 end
672 672 when 'message'
673 673 if message = Message.visible.find_by_id(oid, :include => :parent)
674 674 link = link_to_message(message, {:only_path => only_path}, :class => 'message')
675 675 end
676 676 when 'project'
677 677 if p = Project.visible.find_by_id(oid)
678 678 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
679 679 end
680 680 end
681 681 elsif sep == ':'
682 682 # removes the double quotes if any
683 683 name = identifier.gsub(%r{^"(.*)"$}, "\\1")
684 684 case prefix
685 685 when 'document'
686 686 if project && document = project.documents.visible.find_by_title(name)
687 687 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
688 688 :class => 'document'
689 689 end
690 690 when 'version'
691 691 if project && version = project.versions.visible.find_by_name(name)
692 692 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
693 693 :class => 'version'
694 694 end
695 695 when 'commit'
696 696 if project && project.repository && (changeset = Changeset.visible.find(:first, :conditions => ["repository_id = ? AND scmid LIKE ?", project.repository.id, "#{name}%"]))
697 697 link = link_to h("#{project_prefix}#{name}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :rev => changeset.identifier},
698 698 :class => 'changeset',
699 699 :title => truncate_single_line(h(changeset.comments), :length => 100)
700 700 end
701 701 when 'source', 'export'
702 702 if project && project.repository && User.current.allowed_to?(:browse_repository, project)
703 703 name =~ %r{^[/\\]*(.*?)(@([0-9a-f]+))?(#(L\d+))?$}
704 704 path, rev, anchor = $1, $3, $5
705 705 link = link_to h("#{project_prefix}#{prefix}:#{name}"), {:controller => 'repositories', :action => 'entry', :id => project,
706 706 :path => to_path_param(path),
707 707 :rev => rev,
708 708 :anchor => anchor,
709 709 :format => (prefix == 'export' ? 'raw' : nil)},
710 710 :class => (prefix == 'export' ? 'source download' : 'source')
711 711 end
712 712 when 'attachment'
713 713 attachments = options[:attachments] || (obj && obj.respond_to?(:attachments) ? obj.attachments : nil)
714 714 if attachments && attachment = attachments.detect {|a| a.filename == name }
715 715 link = link_to h(attachment.filename), {:only_path => only_path, :controller => 'attachments', :action => 'download', :id => attachment},
716 716 :class => 'attachment'
717 717 end
718 718 when 'project'
719 719 if p = Project.visible.find(:first, :conditions => ["identifier = :s OR LOWER(name) = :s", {:s => name.downcase}])
720 720 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
721 721 end
722 722 end
723 723 end
724 724 end
725 725 (leading + (link || "#{project_prefix}#{prefix}#{sep}#{identifier}")).html_safe
726 726 end
727 727 end
728 728
729 729 HEADING_RE = /<h(1|2|3|4)( [^>]+)?>(.+?)<\/h(1|2|3|4)>/i unless const_defined?(:HEADING_RE)
730 730
731 731 # Headings and TOC
732 732 # Adds ids and links to headings unless options[:headings] is set to false
733 733 def parse_headings(text, project, obj, attr, only_path, options)
734 734 return if options[:headings] == false
735 735
736 736 text.gsub!(HEADING_RE) do
737 737 level, attrs, content = $1.to_i, $2, $3
738 738 item = strip_tags(content).strip
739 739 anchor = sanitize_anchor_name(item)
740 740 # used for single-file wiki export
741 741 anchor = "#{obj.page.title}_#{anchor}" if options[:wiki_links] == :anchor && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version))
742 742 @parsed_headings << [level, anchor, item]
743 743 "<a name=\"#{anchor}\"></a>\n<h#{level} #{attrs}>#{content}<a href=\"##{anchor}\" class=\"wiki-anchor\">&para;</a></h#{level}>"
744 744 end
745 745 end
746 746
747 747 TOC_RE = /<p>\{\{([<>]?)toc\}\}<\/p>/i unless const_defined?(:TOC_RE)
748 748
749 749 # Renders the TOC with given headings
750 750 def replace_toc(text, headings)
751 751 text.gsub!(TOC_RE) do
752 752 if headings.empty?
753 753 ''
754 754 else
755 755 div_class = 'toc'
756 756 div_class << ' right' if $1 == '>'
757 757 div_class << ' left' if $1 == '<'
758 758 out = "<ul class=\"#{div_class}\"><li>"
759 759 root = headings.map(&:first).min
760 760 current = root
761 761 started = false
762 762 headings.each do |level, anchor, item|
763 763 if level > current
764 764 out << '<ul><li>' * (level - current)
765 765 elsif level < current
766 766 out << "</li></ul>\n" * (current - level) + "</li><li>"
767 767 elsif started
768 768 out << '</li><li>'
769 769 end
770 770 out << "<a href=\"##{anchor}\">#{item}</a>"
771 771 current = level
772 772 started = true
773 773 end
774 774 out << '</li></ul>' * (current - root)
775 775 out << '</li></ul>'
776 776 end
777 777 end
778 778 end
779 779
780 780 # Same as Rails' simple_format helper without using paragraphs
781 781 def simple_format_without_paragraph(text)
782 782 text.to_s.
783 783 gsub(/\r\n?/, "\n"). # \r\n and \r -> \n
784 784 gsub(/\n\n+/, "<br /><br />"). # 2+ newline -> 2 br
785 785 gsub(/([^\n]\n)(?=[^\n])/, '\1<br />'). # 1 newline -> br
786 786 html_safe
787 787 end
788 788
789 789 def lang_options_for_select(blank=true)
790 790 (blank ? [["(auto)", ""]] : []) +
791 791 valid_languages.collect{|lang| [ ll(lang.to_s, :general_lang_name), lang.to_s]}.sort{|x,y| x.last <=> y.last }
792 792 end
793 793
794 794 def label_tag_for(name, option_tags = nil, options = {})
795 795 label_text = l(("field_"+field.to_s.gsub(/\_id$/, "")).to_sym) + (options.delete(:required) ? @template.content_tag("span", " *", :class => "required"): "")
796 796 content_tag("label", label_text)
797 797 end
798 798
799 799 def labelled_tabular_form_for(name, object, options, &proc)
800 800 options[:html] ||= {}
801 801 options[:html][:class] = 'tabular' unless options[:html].has_key?(:class)
802 802 form_for(name, object, options.merge({ :builder => TabularFormBuilder, :lang => current_language}), &proc)
803 803 end
804 804
805 805 def back_url_hidden_field_tag
806 806 back_url = params[:back_url] || request.env['HTTP_REFERER']
807 807 back_url = CGI.unescape(back_url.to_s)
808 808 hidden_field_tag('back_url', CGI.escape(back_url)) unless back_url.blank?
809 809 end
810 810
811 811 def check_all_links(form_name)
812 812 link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") +
813 813 " | ".html_safe +
814 814 link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)")
815 815 end
816 816
817 817 def progress_bar(pcts, options={})
818 818 pcts = [pcts, pcts] unless pcts.is_a?(Array)
819 819 pcts = pcts.collect(&:round)
820 820 pcts[1] = pcts[1] - pcts[0]
821 821 pcts << (100 - pcts[1] - pcts[0])
822 822 width = options[:width] || '100px;'
823 823 legend = options[:legend] || ''
824 824 content_tag('table',
825 825 content_tag('tr',
826 826 (pcts[0] > 0 ? content_tag('td', '', :style => "width: #{pcts[0]}%;", :class => 'closed') : ''.html_safe) +
827 827 (pcts[1] > 0 ? content_tag('td', '', :style => "width: #{pcts[1]}%;", :class => 'done') : ''.html_safe) +
828 828 (pcts[2] > 0 ? content_tag('td', '', :style => "width: #{pcts[2]}%;", :class => 'todo') : ''.html_safe)
829 829 ), :class => 'progress', :style => "width: #{width};").html_safe +
830 830 content_tag('p', legend, :class => 'pourcent').html_safe
831 831 end
832 832
833 833 def checked_image(checked=true)
834 834 if checked
835 835 image_tag 'toggle_check.png'
836 836 end
837 837 end
838 838
839 839 def context_menu(url)
840 840 unless @context_menu_included
841 841 content_for :header_tags do
842 842 javascript_include_tag('context_menu') +
843 843 stylesheet_link_tag('context_menu')
844 844 end
845 845 if l(:direction) == 'rtl'
846 846 content_for :header_tags do
847 847 stylesheet_link_tag('context_menu_rtl')
848 848 end
849 849 end
850 850 @context_menu_included = true
851 851 end
852 852 javascript_tag "new ContextMenu('#{ url_for(url) }')"
853 853 end
854 854
855 855 def context_menu_link(name, url, options={})
856 856 options[:class] ||= ''
857 857 if options.delete(:selected)
858 858 options[:class] << ' icon-checked disabled'
859 859 options[:disabled] = true
860 860 end
861 861 if options.delete(:disabled)
862 862 options.delete(:method)
863 863 options.delete(:confirm)
864 864 options.delete(:onclick)
865 865 options[:class] << ' disabled'
866 866 url = '#'
867 867 end
868 868 link_to h(name), url, options
869 869 end
870 870
871 871 def calendar_for(field_id)
872 872 include_calendar_headers_tags
873 873 image_tag("calendar.png", {:id => "#{field_id}_trigger",:class => "calendar-trigger"}) +
874 874 javascript_tag("Calendar.setup({inputField : '#{field_id}', ifFormat : '%Y-%m-%d', button : '#{field_id}_trigger' });")
875 875 end
876 876
877 877 def include_calendar_headers_tags
878 878 unless @calendar_headers_tags_included
879 879 @calendar_headers_tags_included = true
880 880 content_for :header_tags do
881 881 start_of_week = case Setting.start_of_week.to_i
882 882 when 1
883 883 'Calendar._FD = 1;' # Monday
884 884 when 7
885 885 'Calendar._FD = 0;' # Sunday
886 886 when 6
887 887 'Calendar._FD = 6;' # Saturday
888 888 else
889 889 '' # use language
890 890 end
891 891
892 892 javascript_include_tag('calendar/calendar') +
893 893 javascript_include_tag("calendar/lang/calendar-#{current_language.to_s.downcase}.js") +
894 894 javascript_tag(start_of_week) +
895 895 javascript_include_tag('calendar/calendar-setup') +
896 896 stylesheet_link_tag('calendar')
897 897 end
898 898 end
899 899 end
900 900
901 901 def content_for(name, content = nil, &block)
902 902 @has_content ||= {}
903 903 @has_content[name] = true
904 904 super(name, content, &block)
905 905 end
906 906
907 907 def has_content?(name)
908 908 (@has_content && @has_content[name]) || false
909 909 end
910 910
911 911 def email_delivery_enabled?
912 912 !!ActionMailer::Base.perform_deliveries
913 913 end
914 914
915 915 # Returns the avatar image tag for the given +user+ if avatars are enabled
916 916 # +user+ can be a User or a string that will be scanned for an email address (eg. 'joe <joe@foo.bar>')
917 917 def avatar(user, options = { })
918 918 if Setting.gravatar_enabled?
919 919 options.merge!({:ssl => (defined?(request) && request.ssl?), :default => Setting.gravatar_default})
920 920 email = nil
921 921 if user.respond_to?(:mail)
922 922 email = user.mail
923 923 elsif user.to_s =~ %r{<(.+?)>}
924 924 email = $1
925 925 end
926 926 return gravatar(email.to_s.downcase, options) unless email.blank? rescue nil
927 927 else
928 928 ''
929 929 end
930 930 end
931 931
932 932 def sanitize_anchor_name(anchor)
933 933 anchor.gsub(%r{[^\w\s\-]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
934 934 end
935 935
936 936 # Returns the javascript tags that are included in the html layout head
937 937 def javascript_heads
938 938 tags = javascript_include_tag(:defaults)
939 939 unless User.current.pref.warn_on_leaving_unsaved == '0'
940 940 tags << "\n".html_safe + javascript_tag("Event.observe(window, 'load', function(){ new WarnLeavingUnsaved('#{escape_javascript( l(:text_warn_on_leaving_unsaved) )}'); });")
941 941 end
942 942 tags
943 943 end
944 944
945 945 def favicon
946 946 "<link rel='shortcut icon' href='#{image_path('/favicon.ico')}' />".html_safe
947 947 end
948 948
949 949 def robot_exclusion_tag
950 950 '<meta name="robots" content="noindex,follow,noarchive" />'
951 951 end
952 952
953 953 # Returns true if arg is expected in the API response
954 954 def include_in_api_response?(arg)
955 955 unless @included_in_api_response
956 956 param = params[:include]
957 957 @included_in_api_response = param.is_a?(Array) ? param.collect(&:to_s) : param.to_s.split(',')
958 958 @included_in_api_response.collect!(&:strip)
959 959 end
960 960 @included_in_api_response.include?(arg.to_s)
961 961 end
962 962
963 963 # Returns options or nil if nometa param or X-Redmine-Nometa header
964 964 # was set in the request
965 965 def api_meta(options)
966 966 if params[:nometa].present? || request.headers['X-Redmine-Nometa']
967 967 # compatibility mode for activeresource clients that raise
968 968 # an error when unserializing an array with attributes
969 969 nil
970 970 else
971 971 options
972 972 end
973 973 end
974 974
975 975 private
976 976
977 977 def wiki_helper
978 978 helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
979 979 extend helper
980 980 return self
981 981 end
982 982
983 983 def link_to_content_update(text, url_params = {}, html_options = {})
984 984 link_to(text, url_params, html_options)
985 985 end
986 986 end
@@ -1,787 +1,791
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2011 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require File.expand_path('../../../test_helper', __FILE__)
19 19
20 20 class ApplicationHelperTest < ActionView::TestCase
21 21
22 22 fixtures :projects, :roles, :enabled_modules, :users,
23 23 :repositories, :changesets,
24 24 :trackers, :issue_statuses, :issues, :versions, :documents,
25 25 :wikis, :wiki_pages, :wiki_contents,
26 26 :boards, :messages,
27 27 :attachments,
28 28 :enumerations
29 29
30 30 def setup
31 31 super
32 32 end
33 33
34 34 context "#link_to_if_authorized" do
35 35 context "authorized user" do
36 36 should "be tested"
37 37 end
38 38
39 39 context "unauthorized user" do
40 40 should "be tested"
41 41 end
42 42
43 43 should "allow using the :controller and :action for the target link" do
44 44 User.current = User.find_by_login('admin')
45 45
46 46 @project = Issue.first.project # Used by helper
47 47 response = link_to_if_authorized("By controller/action",
48 48 {:controller => 'issues', :action => 'edit', :id => Issue.first.id})
49 49 assert_match /href/, response
50 50 end
51 51
52 52 end
53 53
54 54 def test_auto_links
55 55 to_test = {
56 56 'http://foo.bar' => '<a class="external" href="http://foo.bar">http://foo.bar</a>',
57 57 'http://foo.bar/~user' => '<a class="external" href="http://foo.bar/~user">http://foo.bar/~user</a>',
58 58 'http://foo.bar.' => '<a class="external" href="http://foo.bar">http://foo.bar</a>.',
59 59 'https://foo.bar.' => '<a class="external" href="https://foo.bar">https://foo.bar</a>.',
60 60 'This is a link: http://foo.bar.' => 'This is a link: <a class="external" href="http://foo.bar">http://foo.bar</a>.',
61 61 'A link (eg. http://foo.bar).' => 'A link (eg. <a class="external" href="http://foo.bar">http://foo.bar</a>).',
62 62 '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>.',
63 63 'http://www.foo.bar/Test_(foobar)' => '<a class="external" href="http://www.foo.bar/Test_(foobar)">http://www.foo.bar/Test_(foobar)</a>',
64 64 '(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>)',
65 65 '(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>)',
66 66 '(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>).',
67 67 '(see "inline link":http://www.foo.bar/Test_(foobar))' => '(see <a href="http://www.foo.bar/Test_(foobar)" class="external">inline link</a>)',
68 68 '(see "inline link":http://www.foo.bar/Test)' => '(see <a href="http://www.foo.bar/Test" class="external">inline link</a>)',
69 69 '(see "inline link":http://www.foo.bar/Test).' => '(see <a href="http://www.foo.bar/Test" class="external">inline link</a>).',
70 70 'www.foo.bar' => '<a class="external" href="http://www.foo.bar">www.foo.bar</a>',
71 71 '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>',
72 72 'http://foo.bar/page#125' => '<a class="external" href="http://foo.bar/page#125">http://foo.bar/page#125</a>',
73 73 'http://foo@www.bar.com' => '<a class="external" href="http://foo@www.bar.com">http://foo@www.bar.com</a>',
74 74 'http://foo:bar@www.bar.com' => '<a class="external" href="http://foo:bar@www.bar.com">http://foo:bar@www.bar.com</a>',
75 75 'ftp://foo.bar' => '<a class="external" href="ftp://foo.bar">ftp://foo.bar</a>',
76 76 'ftps://foo.bar' => '<a class="external" href="ftps://foo.bar">ftps://foo.bar</a>',
77 77 'sftp://foo.bar' => '<a class="external" href="sftp://foo.bar">sftp://foo.bar</a>',
78 78 # two exclamation marks
79 79 '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>',
80 80 # escaping
81 81 'http://foo"bar' => '<a class="external" href="http://foo&quot;bar">http://foo"bar</a>',
82 82 # wrap in angle brackets
83 83 '<http://foo.bar>' => '&lt;<a class="external" href="http://foo.bar">http://foo.bar</a>&gt;'
84 84 }
85 85 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
86 86 end
87 87
88 88 def test_auto_mailto
89 89 assert_equal '<p><a class="email" href="mailto:test@foo.bar">test@foo.bar</a></p>',
90 90 textilizable('test@foo.bar')
91 91 end
92 92
93 93 def test_inline_images
94 94 to_test = {
95 95 '!http://foo.bar/image.jpg!' => '<img src="http://foo.bar/image.jpg" alt="" />',
96 96 'floating !>http://foo.bar/image.jpg!' => 'floating <div style="float:right"><img src="http://foo.bar/image.jpg" alt="" /></div>',
97 97 'with class !(some-class)http://foo.bar/image.jpg!' => 'with class <img src="http://foo.bar/image.jpg" class="some-class" alt="" />',
98 98 # inline styles should be stripped
99 99 'with style !{width:100px;height100px}http://foo.bar/image.jpg!' => 'with style <img src="http://foo.bar/image.jpg" alt="" />',
100 100 '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" />',
101 101 '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;" />',
102 102 }
103 103 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
104 104 end
105 105
106 106 def test_inline_images_inside_tags
107 107 raw = <<-RAW
108 108 h1. !foo.png! Heading
109 109
110 110 Centered image:
111 111
112 112 p=. !bar.gif!
113 113 RAW
114 114
115 115 assert textilizable(raw).include?('<img src="foo.png" alt="" />')
116 116 assert textilizable(raw).include?('<img src="bar.gif" alt="" />')
117 117 end
118 118
119 119 def test_attached_images
120 120 to_test = {
121 121 'Inline image: !logo.gif!' => 'Inline image: <img src="/attachments/download/3" title="This is a logo" alt="This is a logo" />',
122 122 'Inline image: !logo.GIF!' => 'Inline image: <img src="/attachments/download/3" title="This is a logo" alt="This is a logo" />',
123 123 'No match: !ogo.gif!' => 'No match: <img src="ogo.gif" alt="" />',
124 124 'No match: !ogo.GIF!' => 'No match: <img src="ogo.GIF" alt="" />',
125 125 # link image
126 126 '!logo.gif!:http://foo.bar/' => '<a href="http://foo.bar/"><img src="/attachments/download/3" title="This is a logo" alt="This is a logo" /></a>',
127 127 }
128 128 attachments = Attachment.find(:all)
129 129 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text, :attachments => attachments) }
130 130 end
131 131
132 132 def test_textile_external_links
133 133 to_test = {
134 134 'This is a "link":http://foo.bar' => 'This is a <a href="http://foo.bar" class="external">link</a>',
135 135 'This is an intern "link":/foo/bar' => 'This is an intern <a href="/foo/bar">link</a>',
136 136 '"link (Link title)":http://foo.bar' => '<a href="http://foo.bar" title="Link title" class="external">link</a>',
137 137 '"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>',
138 138 "This is not a \"Link\":\n\nAnother paragraph" => "This is not a \"Link\":</p>\n\n\n\t<p>Another paragraph",
139 139 # no multiline link text
140 140 "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",
141 141 # mailto link
142 142 "\"system administrator\":mailto:sysadmin@example.com?subject=redmine%20permissions" => "<a href=\"mailto:sysadmin@example.com?subject=redmine%20permissions\">system administrator</a>",
143 143 # two exclamation marks
144 144 '"a link":http://example.net/path!602815048C7B5C20!302.html' => '<a href="http://example.net/path!602815048C7B5C20!302.html" class="external">a link</a>',
145 145 # escaping
146 146 '"test":http://foo"bar' => '<a href="http://foo&quot;bar" class="external">test</a>',
147 147 }
148 148 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
149 149 end
150 150
151 151 def test_redmine_links
152 152 issue_link = link_to('#3', {:controller => 'issues', :action => 'show', :id => 3},
153 153 :class => 'issue status-1 priority-1 overdue', :title => 'Error 281 when updating a recipe (New)')
154 154
155 155 changeset_link = link_to('r1', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :rev => 1},
156 156 :class => 'changeset', :title => 'My very first commit')
157 157 changeset_link2 = link_to('r2', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :rev => 2},
158 158 :class => 'changeset', :title => 'This commit fixes #1, #2 and references #1 & #3')
159 159
160 160 document_link = link_to('Test document', {:controller => 'documents', :action => 'show', :id => 1},
161 161 :class => 'document')
162 162
163 163 version_link = link_to('1.0', {:controller => 'versions', :action => 'show', :id => 2},
164 164 :class => 'version')
165 165
166 166 message_url = {:controller => 'messages', :action => 'show', :board_id => 1, :id => 4}
167 167
168 168 project_url = {:controller => 'projects', :action => 'show', :id => 'subproject1'}
169 169
170 170 source_url = {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :path => ['some', 'file']}
171 171 source_url_with_ext = {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :path => ['some', 'file.ext']}
172 172
173 173 to_test = {
174 174 # tickets
175 175 '#3, [#3], (#3) and #3.' => "#{issue_link}, [#{issue_link}], (#{issue_link}) and #{issue_link}.",
176 176 # changesets
177 177 'r1' => changeset_link,
178 178 'r1.' => "#{changeset_link}.",
179 179 'r1, r2' => "#{changeset_link}, #{changeset_link2}",
180 180 'r1,r2' => "#{changeset_link},#{changeset_link2}",
181 181 # documents
182 182 'document#1' => document_link,
183 183 'document:"Test document"' => document_link,
184 184 # versions
185 185 'version#2' => version_link,
186 186 'version:1.0' => version_link,
187 187 'version:"1.0"' => version_link,
188 188 # source
189 189 'source:/some/file' => link_to('source:/some/file', source_url, :class => 'source'),
190 190 'source:/some/file.' => link_to('source:/some/file', source_url, :class => 'source') + ".",
191 191 'source:/some/file.ext.' => link_to('source:/some/file.ext', source_url_with_ext, :class => 'source') + ".",
192 192 'source:/some/file. ' => link_to('source:/some/file', source_url, :class => 'source') + ".",
193 193 'source:/some/file.ext. ' => link_to('source:/some/file.ext', source_url_with_ext, :class => 'source') + ".",
194 194 'source:/some/file, ' => link_to('source:/some/file', source_url, :class => 'source') + ",",
195 195 'source:/some/file@52' => link_to('source:/some/file@52', source_url.merge(:rev => 52), :class => 'source'),
196 196 'source:/some/file.ext@52' => link_to('source:/some/file.ext@52', source_url_with_ext.merge(:rev => 52), :class => 'source'),
197 197 'source:/some/file#L110' => link_to('source:/some/file#L110', source_url.merge(:anchor => 'L110'), :class => 'source'),
198 198 'source:/some/file.ext#L110' => link_to('source:/some/file.ext#L110', source_url_with_ext.merge(:anchor => 'L110'), :class => 'source'),
199 199 'source:/some/file@52#L110' => link_to('source:/some/file@52#L110', source_url.merge(:rev => 52, :anchor => 'L110'), :class => 'source'),
200 200 'export:/some/file' => link_to('export:/some/file', source_url.merge(:format => 'raw'), :class => 'source download'),
201 201 # message
202 202 'message#4' => link_to('Post 2', message_url, :class => 'message'),
203 203 'message#5' => link_to('RE: post 2', message_url.merge(:anchor => 'message-5', :r => 5), :class => 'message'),
204 204 # project
205 205 'project#3' => link_to('eCookbook Subproject 1', project_url, :class => 'project'),
206 206 'project:subproject1' => link_to('eCookbook Subproject 1', project_url, :class => 'project'),
207 207 'project:"eCookbook subProject 1"' => link_to('eCookbook Subproject 1', project_url, :class => 'project'),
208 208 # escaping
209 209 '!#3.' => '#3.',
210 210 '!r1' => 'r1',
211 211 '!document#1' => 'document#1',
212 212 '!document:"Test document"' => 'document:"Test document"',
213 213 '!version#2' => 'version#2',
214 214 '!version:1.0' => 'version:1.0',
215 215 '!version:"1.0"' => 'version:"1.0"',
216 216 '!source:/some/file' => 'source:/some/file',
217 217 # not found
218 218 '#0123456789' => '#0123456789',
219 219 # invalid expressions
220 220 'source:' => 'source:',
221 221 # url hash
222 222 "http://foo.bar/FAQ#3" => '<a class="external" href="http://foo.bar/FAQ#3">http://foo.bar/FAQ#3</a>',
223 223 }
224 224 @project = Project.find(1)
225 225 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text), "#{text} failed" }
226 226 end
227 227
228 228 def test_cross_project_redmine_links
229 229 source_link = link_to('ecookbook:source:/some/file', {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :path => ['some', 'file']},
230 230 :class => 'source')
231 231
232 232 changeset_link = link_to('ecookbook:r2', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :rev => 2},
233 233 :class => 'changeset', :title => 'This commit fixes #1, #2 and references #1 & #3')
234 234
235 235 to_test = {
236 236 # documents
237 237 'document:"Test document"' => 'document:"Test document"',
238 238 'ecookbook:document:"Test document"' => '<a href="/documents/1" class="document">Test document</a>',
239 239 'invalid:document:"Test document"' => 'invalid:document:"Test document"',
240 240 # versions
241 241 'version:"1.0"' => 'version:"1.0"',
242 242 'ecookbook:version:"1.0"' => '<a href="/versions/2" class="version">1.0</a>',
243 243 'invalid:version:"1.0"' => 'invalid:version:"1.0"',
244 244 # changeset
245 245 'r2' => 'r2',
246 246 'ecookbook:r2' => changeset_link,
247 247 'invalid:r2' => 'invalid:r2',
248 248 # source
249 249 'source:/some/file' => 'source:/some/file',
250 250 'ecookbook:source:/some/file' => source_link,
251 251 'invalid:source:/some/file' => 'invalid:source:/some/file',
252 252 }
253 253 @project = Project.find(3)
254 254 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text), "#{text} failed" }
255 255 end
256 256
257 257 def test_redmine_links_git_commit
258 258 changeset_link = link_to('abcd',
259 259 {
260 260 :controller => 'repositories',
261 261 :action => 'revision',
262 262 :id => 'subproject1',
263 263 :rev => 'abcd',
264 264 },
265 265 :class => 'changeset', :title => 'test commit')
266 266 to_test = {
267 267 'commit:abcd' => changeset_link,
268 268 }
269 269 @project = Project.find(3)
270 270 r = Repository::Git.create!(:project => @project, :url => '/tmp/test/git')
271 271 assert r
272 272 c = Changeset.new(:repository => r,
273 273 :committed_on => Time.now,
274 274 :revision => 'abcd',
275 275 :scmid => 'abcd',
276 276 :comments => 'test commit')
277 277 assert( c.save )
278 278 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
279 279 end
280 280
281 281 # TODO: Bazaar commit id contains mail address, so it contains '@' and '_'.
282 282 def test_redmine_links_darcs_commit
283 283 changeset_link = link_to('20080308225258-98289-abcd456efg.gz',
284 284 {
285 285 :controller => 'repositories',
286 286 :action => 'revision',
287 287 :id => 'subproject1',
288 288 :rev => '123',
289 289 },
290 290 :class => 'changeset', :title => 'test commit')
291 291 to_test = {
292 292 'commit:20080308225258-98289-abcd456efg.gz' => changeset_link,
293 293 }
294 294 @project = Project.find(3)
295 295 r = Repository::Darcs.create!(
296 296 :project => @project, :url => '/tmp/test/darcs',
297 297 :log_encoding => 'UTF-8')
298 298 assert r
299 299 c = Changeset.new(:repository => r,
300 300 :committed_on => Time.now,
301 301 :revision => '123',
302 302 :scmid => '20080308225258-98289-abcd456efg.gz',
303 303 :comments => 'test commit')
304 304 assert( c.save )
305 305 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
306 306 end
307 307
308 308 def test_redmine_links_mercurial_commit
309 309 changeset_link_rev = link_to('r123',
310 310 {
311 311 :controller => 'repositories',
312 312 :action => 'revision',
313 313 :id => 'subproject1',
314 314 :rev => '123' ,
315 315 },
316 316 :class => 'changeset', :title => 'test commit')
317 317 changeset_link_commit = link_to('abcd',
318 318 {
319 319 :controller => 'repositories',
320 320 :action => 'revision',
321 321 :id => 'subproject1',
322 322 :rev => 'abcd' ,
323 323 },
324 324 :class => 'changeset', :title => 'test commit')
325 325 to_test = {
326 326 'r123' => changeset_link_rev,
327 327 'commit:abcd' => changeset_link_commit,
328 328 }
329 329 @project = Project.find(3)
330 330 r = Repository::Mercurial.create!(:project => @project, :url => '/tmp/test')
331 331 assert r
332 332 c = Changeset.new(:repository => r,
333 333 :committed_on => Time.now,
334 334 :revision => '123',
335 335 :scmid => 'abcd',
336 336 :comments => 'test commit')
337 337 assert( c.save )
338 338 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
339 339 end
340 340
341 341 def test_attachment_links
342 342 attachment_link = link_to('error281.txt', {:controller => 'attachments', :action => 'download', :id => '1'}, :class => 'attachment')
343 343 to_test = {
344 344 'attachment:error281.txt' => attachment_link
345 345 }
346 346 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text, :attachments => Issue.find(3).attachments), "#{text} failed" }
347 347 end
348 348
349 349 def test_wiki_links
350 350 to_test = {
351 351 '[[CookBook documentation]]' => '<a href="/projects/ecookbook/wiki/CookBook_documentation" class="wiki-page">CookBook documentation</a>',
352 352 '[[Another page|Page]]' => '<a href="/projects/ecookbook/wiki/Another_page" class="wiki-page">Page</a>',
353 # title content should be formatted
354 '[[Another page|With _styled_ *title*]]' => '<a href="/projects/ecookbook/wiki/Another_page" class="wiki-page">With <em>styled</em> <strong>title</strong></a>',
355 '[[Another page|With title containing <strong>HTML entities &amp; markups</strong>]]' => '<a href="/projects/ecookbook/wiki/Another_page" class="wiki-page">With title containing &lt;strong&gt;HTML entities &amp; markups&lt;/strong&gt;</a>',
353 356 # link with anchor
354 357 '[[CookBook documentation#One-section]]' => '<a href="/projects/ecookbook/wiki/CookBook_documentation#One-section" class="wiki-page">CookBook documentation</a>',
355 358 '[[Another page#anchor|Page]]' => '<a href="/projects/ecookbook/wiki/Another_page#anchor" class="wiki-page">Page</a>',
356 359 # page that doesn't exist
357 360 '[[Unknown page]]' => '<a href="/projects/ecookbook/wiki/Unknown_page" class="wiki-page new">Unknown page</a>',
358 361 '[[Unknown page|404]]' => '<a href="/projects/ecookbook/wiki/Unknown_page" class="wiki-page new">404</a>',
359 362 # link to another project wiki
360 363 '[[onlinestore:]]' => '<a href="/projects/onlinestore/wiki" class="wiki-page">onlinestore</a>',
361 364 '[[onlinestore:|Wiki]]' => '<a href="/projects/onlinestore/wiki" class="wiki-page">Wiki</a>',
362 365 '[[onlinestore:Start page]]' => '<a href="/projects/onlinestore/wiki/Start_page" class="wiki-page">Start page</a>',
363 366 '[[onlinestore:Start page|Text]]' => '<a href="/projects/onlinestore/wiki/Start_page" class="wiki-page">Text</a>',
364 367 '[[onlinestore:Unknown page]]' => '<a href="/projects/onlinestore/wiki/Unknown_page" class="wiki-page new">Unknown page</a>',
365 368 # striked through link
366 369 '-[[Another page|Page]]-' => '<del><a href="/projects/ecookbook/wiki/Another_page" class="wiki-page">Page</a></del>',
367 370 '-[[Another page|Page]] link-' => '<del><a href="/projects/ecookbook/wiki/Another_page" class="wiki-page">Page</a> link</del>',
368 371 # escaping
369 372 '![[Another page|Page]]' => '[[Another page|Page]]',
370 373 # project does not exist
371 374 '[[unknowproject:Start]]' => '[[unknowproject:Start]]',
372 375 '[[unknowproject:Start|Page title]]' => '[[unknowproject:Start|Page title]]',
373 376 }
377
374 378 @project = Project.find(1)
375 379 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
376 380 end
377 381
378 382 def test_wiki_links_within_local_file_generation_context
379 383
380 384 to_test = {
381 385 # link to a page
382 386 '[[CookBook documentation]]' => '<a href="CookBook_documentation.html" class="wiki-page">CookBook documentation</a>',
383 387 '[[CookBook documentation|documentation]]' => '<a href="CookBook_documentation.html" class="wiki-page">documentation</a>',
384 388 '[[CookBook documentation#One-section]]' => '<a href="CookBook_documentation.html#One-section" class="wiki-page">CookBook documentation</a>',
385 389 '[[CookBook documentation#One-section|documentation]]' => '<a href="CookBook_documentation.html#One-section" class="wiki-page">documentation</a>',
386 390 # page that doesn't exist
387 391 '[[Unknown page]]' => '<a href="Unknown_page.html" class="wiki-page new">Unknown page</a>',
388 392 '[[Unknown page|404]]' => '<a href="Unknown_page.html" class="wiki-page new">404</a>',
389 393 '[[Unknown page#anchor]]' => '<a href="Unknown_page.html#anchor" class="wiki-page new">Unknown page</a>',
390 394 '[[Unknown page#anchor|404]]' => '<a href="Unknown_page.html#anchor" class="wiki-page new">404</a>',
391 395 }
392 396
393 397 @project = Project.find(1)
394 398
395 399 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text, :wiki_links => :local) }
396 400 end
397 401
398 402 def test_html_tags
399 403 to_test = {
400 404 "<div>content</div>" => "<p>&lt;div&gt;content&lt;/div&gt;</p>",
401 405 "<div class=\"bold\">content</div>" => "<p>&lt;div class=\"bold\"&gt;content&lt;/div&gt;</p>",
402 406 "<script>some script;</script>" => "<p>&lt;script&gt;some script;&lt;/script&gt;</p>",
403 407 # do not escape pre/code tags
404 408 "<pre>\nline 1\nline2</pre>" => "<pre>\nline 1\nline2</pre>",
405 409 "<pre><code>\nline 1\nline2</code></pre>" => "<pre><code>\nline 1\nline2</code></pre>",
406 410 "<pre><div>content</div></pre>" => "<pre>&lt;div&gt;content&lt;/div&gt;</pre>",
407 411 "HTML comment: <!-- no comments -->" => "<p>HTML comment: &lt;!-- no comments --&gt;</p>",
408 412 "<!-- opening comment" => "<p>&lt;!-- opening comment</p>",
409 413 # remove attributes except class
410 414 "<pre class='foo'>some text</pre>" => "<pre class='foo'>some text</pre>",
411 415 '<pre class="foo">some text</pre>' => '<pre class="foo">some text</pre>',
412 416 "<pre class='foo bar'>some text</pre>" => "<pre class='foo bar'>some text</pre>",
413 417 '<pre class="foo bar">some text</pre>' => '<pre class="foo bar">some text</pre>',
414 418 "<pre onmouseover='alert(1)'>some text</pre>" => "<pre>some text</pre>",
415 419 # xss
416 420 '<pre><code class=""onmouseover="alert(1)">text</code></pre>' => '<pre><code>text</code></pre>',
417 421 '<pre class=""onmouseover="alert(1)">text</pre>' => '<pre>text</pre>',
418 422 }
419 423 to_test.each { |text, result| assert_equal result, textilizable(text) }
420 424 end
421 425
422 426 def test_allowed_html_tags
423 427 to_test = {
424 428 "<pre>preformatted text</pre>" => "<pre>preformatted text</pre>",
425 429 "<notextile>no *textile* formatting</notextile>" => "no *textile* formatting",
426 430 "<notextile>this is <tag>a tag</tag></notextile>" => "this is &lt;tag&gt;a tag&lt;/tag&gt;"
427 431 }
428 432 to_test.each { |text, result| assert_equal result, textilizable(text) }
429 433 end
430 434
431 435 def test_pre_tags
432 436 raw = <<-RAW
433 437 Before
434 438
435 439 <pre>
436 440 <prepared-statement-cache-size>32</prepared-statement-cache-size>
437 441 </pre>
438 442
439 443 After
440 444 RAW
441 445
442 446 expected = <<-EXPECTED
443 447 <p>Before</p>
444 448 <pre>
445 449 &lt;prepared-statement-cache-size&gt;32&lt;/prepared-statement-cache-size&gt;
446 450 </pre>
447 451 <p>After</p>
448 452 EXPECTED
449 453
450 454 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
451 455 end
452 456
453 457 def test_pre_content_should_not_parse_wiki_and_redmine_links
454 458 raw = <<-RAW
455 459 [[CookBook documentation]]
456 460
457 461 #1
458 462
459 463 <pre>
460 464 [[CookBook documentation]]
461 465
462 466 #1
463 467 </pre>
464 468 RAW
465 469
466 470 expected = <<-EXPECTED
467 471 <p><a href="/projects/ecookbook/wiki/CookBook_documentation" class="wiki-page">CookBook documentation</a></p>
468 472 <p><a href="/issues/1" class="issue status-1 priority-1" title="Can't print recipes (New)">#1</a></p>
469 473 <pre>
470 474 [[CookBook documentation]]
471 475
472 476 #1
473 477 </pre>
474 478 EXPECTED
475 479
476 480 @project = Project.find(1)
477 481 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
478 482 end
479 483
480 484 def test_non_closing_pre_blocks_should_be_closed
481 485 raw = <<-RAW
482 486 <pre><code>
483 487 RAW
484 488
485 489 expected = <<-EXPECTED
486 490 <pre><code>
487 491 </code></pre>
488 492 EXPECTED
489 493
490 494 @project = Project.find(1)
491 495 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
492 496 end
493 497
494 498 def test_syntax_highlight
495 499 raw = <<-RAW
496 500 <pre><code class="ruby">
497 501 # Some ruby code here
498 502 </code></pre>
499 503 RAW
500 504
501 505 expected = <<-EXPECTED
502 506 <pre><code class="ruby syntaxhl"><span class=\"CodeRay\"><span class="line-numbers">1</span><span class="comment"># Some ruby code here</span></span>
503 507 </code></pre>
504 508 EXPECTED
505 509
506 510 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
507 511 end
508 512
509 513 def test_wiki_links_in_tables
510 514 to_test = {"|[[Page|Link title]]|[[Other Page|Other title]]|\n|Cell 21|[[Last page]]|" =>
511 515 '<tr><td><a href="/projects/ecookbook/wiki/Page" class="wiki-page new">Link title</a></td>' +
512 516 '<td><a href="/projects/ecookbook/wiki/Other_Page" class="wiki-page new">Other title</a></td>' +
513 517 '</tr><tr><td>Cell 21</td><td><a href="/projects/ecookbook/wiki/Last_page" class="wiki-page new">Last page</a></td></tr>'
514 518 }
515 519 @project = Project.find(1)
516 520 to_test.each { |text, result| assert_equal "<table>#{result}</table>", textilizable(text).gsub(/[\t\n]/, '') }
517 521 end
518 522
519 523 def test_text_formatting
520 524 to_test = {'*_+bold, italic and underline+_*' => '<strong><em><ins>bold, italic and underline</ins></em></strong>',
521 525 '(_text within parentheses_)' => '(<em>text within parentheses</em>)',
522 526 'a *Humane Web* Text Generator' => 'a <strong>Humane Web</strong> Text Generator',
523 527 '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>',
524 528 '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',
525 529 }
526 530 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
527 531 end
528 532
529 533 def test_wiki_horizontal_rule
530 534 assert_equal '<hr />', textilizable('---')
531 535 assert_equal '<p>Dashes: ---</p>', textilizable('Dashes: ---')
532 536 end
533 537
534 538 def test_footnotes
535 539 raw = <<-RAW
536 540 This is some text[1].
537 541
538 542 fn1. This is the foot note
539 543 RAW
540 544
541 545 expected = <<-EXPECTED
542 546 <p>This is some text<sup><a href=\"#fn1\">1</a></sup>.</p>
543 547 <p id="fn1" class="footnote"><sup>1</sup> This is the foot note</p>
544 548 EXPECTED
545 549
546 550 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
547 551 end
548 552
549 553 def test_headings
550 554 raw = 'h1. Some heading'
551 555 expected = %|<a name="Some-heading"></a>\n<h1 >Some heading<a href="#Some-heading" class="wiki-anchor">&para;</a></h1>|
552 556
553 557 assert_equal expected, textilizable(raw)
554 558 end
555 559
556 560 def test_headings_with_special_chars
557 561 # This test makes sure that the generated anchor names match the expected
558 562 # ones even if the heading text contains unconventional characters
559 563 raw = 'h1. Some heading related to version 0.5'
560 564 anchor = sanitize_anchor_name("Some-heading-related-to-version-0.5")
561 565 expected = %|<a name="#{anchor}"></a>\n<h1 >Some heading related to version 0.5<a href="##{anchor}" class="wiki-anchor">&para;</a></h1>|
562 566
563 567 assert_equal expected, textilizable(raw)
564 568 end
565 569
566 570 def test_wiki_links_within_wiki_page_context
567 571
568 572 page = WikiPage.find_by_title('Another_page' )
569 573
570 574 to_test = {
571 575 # link to another page
572 576 '[[CookBook documentation]]' => '<a href="/projects/ecookbook/wiki/CookBook_documentation" class="wiki-page">CookBook documentation</a>',
573 577 '[[CookBook documentation|documentation]]' => '<a href="/projects/ecookbook/wiki/CookBook_documentation" class="wiki-page">documentation</a>',
574 578 '[[CookBook documentation#One-section]]' => '<a href="/projects/ecookbook/wiki/CookBook_documentation#One-section" class="wiki-page">CookBook documentation</a>',
575 579 '[[CookBook documentation#One-section|documentation]]' => '<a href="/projects/ecookbook/wiki/CookBook_documentation#One-section" class="wiki-page">documentation</a>',
576 580 # link to the current page
577 581 '[[Another page]]' => '<a href="/projects/ecookbook/wiki/Another_page" class="wiki-page">Another page</a>',
578 582 '[[Another page|Page]]' => '<a href="/projects/ecookbook/wiki/Another_page" class="wiki-page">Page</a>',
579 583 '[[Another page#anchor]]' => '<a href="#anchor" class="wiki-page">Another page</a>',
580 584 '[[Another page#anchor|Page]]' => '<a href="#anchor" class="wiki-page">Page</a>',
581 585 # page that doesn't exist
582 586 '[[Unknown page]]' => '<a href="/projects/ecookbook/wiki/Unknown_page" class="wiki-page new">Unknown page</a>',
583 587 '[[Unknown page|404]]' => '<a href="/projects/ecookbook/wiki/Unknown_page" class="wiki-page new">404</a>',
584 588 '[[Unknown page#anchor]]' => '<a href="/projects/ecookbook/wiki/Unknown_page#anchor" class="wiki-page new">Unknown page</a>',
585 589 '[[Unknown page#anchor|404]]' => '<a href="/projects/ecookbook/wiki/Unknown_page#anchor" class="wiki-page new">404</a>',
586 590 }
587 591
588 592 @project = Project.find(1)
589 593
590 594 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(WikiContent.generate!( :text => text, :page => page ), :text) }
591 595 end
592 596
593 597 def test_wiki_links_anchor_option_should_prepend_page_title_to_href
594 598
595 599 to_test = {
596 600 # link to a page
597 601 '[[CookBook documentation]]' => '<a href="#CookBook_documentation" class="wiki-page">CookBook documentation</a>',
598 602 '[[CookBook documentation|documentation]]' => '<a href="#CookBook_documentation" class="wiki-page">documentation</a>',
599 603 '[[CookBook documentation#One-section]]' => '<a href="#CookBook_documentation_One-section" class="wiki-page">CookBook documentation</a>',
600 604 '[[CookBook documentation#One-section|documentation]]' => '<a href="#CookBook_documentation_One-section" class="wiki-page">documentation</a>',
601 605 # page that doesn't exist
602 606 '[[Unknown page]]' => '<a href="#Unknown_page" class="wiki-page new">Unknown page</a>',
603 607 '[[Unknown page|404]]' => '<a href="#Unknown_page" class="wiki-page new">404</a>',
604 608 '[[Unknown page#anchor]]' => '<a href="#Unknown_page_anchor" class="wiki-page new">Unknown page</a>',
605 609 '[[Unknown page#anchor|404]]' => '<a href="#Unknown_page_anchor" class="wiki-page new">404</a>',
606 610 }
607 611
608 612 @project = Project.find(1)
609 613
610 614 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text, :wiki_links => :anchor) }
611 615 end
612 616
613 617 def test_headings_in_wiki_single_page_export_should_be_prepended_with_page_title
614 618 page = WikiPage.generate!( :title => 'Page Title' )
615 619 content = WikiContent.generate!( :text => 'h1. Some heading', :page => page )
616 620
617 621 expected = %|<a name="Page_Title_Some-heading"></a>\n<h1 >Some heading<a href="#Page_Title_Some-heading" class="wiki-anchor">&para;</a></h1>|
618 622
619 623 assert_equal expected, textilizable(content, :text, :wiki_links => :anchor )
620 624 end
621 625
622 626 def test_table_of_content
623 627 raw = <<-RAW
624 628 {{toc}}
625 629
626 630 h1. Title
627 631
628 632 Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas sed libero.
629 633
630 634 h2. Subtitle with a [[Wiki]] link
631 635
632 636 Nullam commodo metus accumsan nulla. Curabitur lobortis dui id dolor.
633 637
634 638 h2. Subtitle with [[Wiki|another Wiki]] link
635 639
636 640 h2. Subtitle with %{color:red}red text%
637 641
638 642 <pre>
639 643 some code
640 644 </pre>
641 645
642 646 h3. Subtitle with *some* _modifiers_
643 647
644 648 h1. Another title
645 649
646 650 h3. An "Internet link":http://www.redmine.org/ inside subtitle
647 651
648 652 h2. "Project Name !/attachments/1234/logo_small.gif! !/attachments/5678/logo_2.png!":/projects/projectname/issues
649 653
650 654 RAW
651 655
652 656 expected = '<ul class="toc">' +
653 657 '<li><a href="#Title">Title</a>' +
654 658 '<ul>' +
655 659 '<li><a href="#Subtitle-with-a-Wiki-link">Subtitle with a Wiki link</a></li>' +
656 660 '<li><a href="#Subtitle-with-another-Wiki-link">Subtitle with another Wiki link</a></li>' +
657 661 '<li><a href="#Subtitle-with-red-text">Subtitle with red text</a>' +
658 662 '<ul>' +
659 663 '<li><a href="#Subtitle-with-some-modifiers">Subtitle with some modifiers</a></li>' +
660 664 '</ul>' +
661 665 '</li>' +
662 666 '</ul>' +
663 667 '</li>' +
664 668 '<li><a href="#Another-title">Another title</a>' +
665 669 '<ul>' +
666 670 '<li>' +
667 671 '<ul>' +
668 672 '<li><a href="#An-Internet-link-inside-subtitle">An Internet link inside subtitle</a></li>' +
669 673 '</ul>' +
670 674 '</li>' +
671 675 '<li><a href="#Project-Name">Project Name</a></li>' +
672 676 '</ul>' +
673 677 '</li>' +
674 678 '</ul>'
675 679
676 680 @project = Project.find(1)
677 681 assert textilizable(raw).gsub("\n", "").include?(expected), textilizable(raw)
678 682 end
679 683
680 684 def test_table_of_content_should_contain_included_page_headings
681 685 raw = <<-RAW
682 686 {{toc}}
683 687
684 688 h1. Included
685 689
686 690 {{include(Child_1)}}
687 691 RAW
688 692
689 693 expected = '<ul class="toc">' +
690 694 '<li><a href="#Included">Included</a></li>' +
691 695 '<li><a href="#Child-page-1">Child page 1</a></li>' +
692 696 '</ul>'
693 697
694 698 @project = Project.find(1)
695 699 assert textilizable(raw).gsub("\n", "").include?(expected)
696 700 end
697 701
698 702 def test_default_formatter
699 703 Setting.text_formatting = 'unknown'
700 704 text = 'a *link*: http://www.example.net/'
701 705 assert_equal '<p>a *link*: <a href="http://www.example.net/">http://www.example.net/</a></p>', textilizable(text)
702 706 Setting.text_formatting = 'textile'
703 707 end
704 708
705 709 def test_due_date_distance_in_words
706 710 to_test = { Date.today => 'Due in 0 days',
707 711 Date.today + 1 => 'Due in 1 day',
708 712 Date.today + 100 => 'Due in about 3 months',
709 713 Date.today + 20000 => 'Due in over 54 years',
710 714 Date.today - 1 => '1 day late',
711 715 Date.today - 100 => 'about 3 months late',
712 716 Date.today - 20000 => 'over 54 years late',
713 717 }
714 718 ::I18n.locale = :en
715 719 to_test.each do |date, expected|
716 720 assert_equal expected, due_date_distance_in_words(date)
717 721 end
718 722 end
719 723
720 724 def test_avatar
721 725 # turn on avatars
722 726 Setting.gravatar_enabled = '1'
723 727 assert avatar(User.find_by_mail('jsmith@somenet.foo')).include?(Digest::MD5.hexdigest('jsmith@somenet.foo'))
724 728 assert avatar('jsmith <jsmith@somenet.foo>').include?(Digest::MD5.hexdigest('jsmith@somenet.foo'))
725 729 assert_nil avatar('jsmith')
726 730 assert_nil avatar(nil)
727 731
728 732 # turn off avatars
729 733 Setting.gravatar_enabled = '0'
730 734 assert_equal '', avatar(User.find_by_mail('jsmith@somenet.foo'))
731 735 end
732 736
733 737 def test_link_to_user
734 738 user = User.find(2)
735 739 t = link_to_user(user)
736 740 assert_equal "<a href=\"/users/2\">#{ user.name }</a>", t
737 741 end
738 742
739 743 def test_link_to_user_should_not_link_to_locked_user
740 744 user = User.find(5)
741 745 assert user.locked?
742 746 t = link_to_user(user)
743 747 assert_equal user.name, t
744 748 end
745 749
746 750 def test_link_to_user_should_not_link_to_anonymous
747 751 user = User.anonymous
748 752 assert user.anonymous?
749 753 t = link_to_user(user)
750 754 assert_equal ::I18n.t(:label_user_anonymous), t
751 755 end
752 756
753 757 def test_link_to_project
754 758 project = Project.find(1)
755 759 assert_equal %(<a href="/projects/ecookbook">eCookbook</a>),
756 760 link_to_project(project)
757 761 assert_equal %(<a href="/projects/ecookbook/settings">eCookbook</a>),
758 762 link_to_project(project, :action => 'settings')
759 763 assert_equal %(<a href="http://test.host/projects/ecookbook?jump=blah">eCookbook</a>),
760 764 link_to_project(project, {:only_path => false, :jump => 'blah'})
761 765 assert_equal %(<a href="/projects/ecookbook/settings" class="project">eCookbook</a>),
762 766 link_to_project(project, {:action => 'settings'}, :class => "project")
763 767 end
764 768
765 769 def test_principals_options_for_select_with_users
766 770 users = [User.find(2), User.find(4)]
767 771 assert_equal %(<option value="2">John Smith</option><option value="4">Robert Hill</option>),
768 772 principals_options_for_select(users)
769 773 end
770 774
771 775 def test_principals_options_for_select_with_selected
772 776 users = [User.find(2), User.find(4)]
773 777 assert_equal %(<option value="2">John Smith</option><option value="4" selected="selected">Robert Hill</option>),
774 778 principals_options_for_select(users, User.find(4))
775 779 end
776 780
777 781 def test_principals_options_for_select_with_users_and_groups
778 782 users = [User.find(2), Group.find(11), User.find(4), Group.find(10)]
779 783 assert_equal %(<option value="2">John Smith</option><option value="4">Robert Hill</option>) +
780 784 %(<optgroup label="Groups"><option value="10">A Team</option><option value="11">B Team</option></optgroup>),
781 785 principals_options_for_select(users)
782 786 end
783 787
784 788 def test_principals_options_for_select_with_empty_collection
785 789 assert_equal '', principals_options_for_select([])
786 790 end
787 791 end
General Comments 0
You need to be logged in to leave comments. Login now