##// END OF EJS Templates
Make sure that anchor names generated for headings fully match wiki links (#7215)....
Etienne Massip -
r7443:65b533a8ec1f
parent child
Show More
@@ -1,972 +1,977
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
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)), url.merge({"#{name}[move_to]" => 'highest'}), :method => :post, :title => l(:label_sort_highest)) +
392 392 link_to(image_tag('1uparrow.png', :alt => l(:label_sort_higher)), url.merge({"#{name}[move_to]" => 'higher'}), :method => :post, :title => l(:label_sort_higher)) +
393 393 link_to(image_tag('1downarrow.png', :alt => l(:label_sort_lower)), url.merge({"#{name}[move_to]" => 'lower'}), :method => :post, :title => l(:label_sort_lower)) +
394 394 link_to(image_tag('2downarrow.png', :alt => l(:label_sort_lowest)), url.merge({"#{name}[move_to]" => 'lowest'}), :method => :post, :title => l(:label_sort_lowest))
395 395 end
396 396
397 397 def breadcrumb(*args)
398 398 elements = args.flatten
399 399 elements.any? ? content_tag('p', (args.join(" \xc2\xbb ") + " \xc2\xbb ").html_safe, :class => 'breadcrumb') : nil
400 400 end
401 401
402 402 def other_formats_links(&block)
403 403 concat('<p class="other-formats">' + l(:label_export_to))
404 404 yield Redmine::Views::OtherFormatsBuilder.new(self)
405 405 concat('</p>')
406 406 end
407 407
408 408 def page_header_title
409 409 if @project.nil? || @project.new_record?
410 410 h(Setting.app_title)
411 411 else
412 412 b = []
413 413 ancestors = (@project.root? ? [] : @project.ancestors.visible.all)
414 414 if ancestors.any?
415 415 root = ancestors.shift
416 416 b << link_to_project(root, {:jump => current_menu_item}, :class => 'root')
417 417 if ancestors.size > 2
418 418 b << '&#8230;'
419 419 ancestors = ancestors[-2, 2]
420 420 end
421 421 b += ancestors.collect {|p| link_to_project(p, {:jump => current_menu_item}, :class => 'ancestor') }
422 422 end
423 423 b << h(@project)
424 424 b.join(' &#187; ')
425 425 end
426 426 end
427 427
428 428 def html_title(*args)
429 429 if args.empty?
430 430 title = []
431 431 title << @project.name if @project
432 432 title += @html_title if @html_title
433 433 title << Setting.app_title
434 434 title.select {|t| !t.blank? }.join(' - ')
435 435 else
436 436 @html_title ||= []
437 437 @html_title += args
438 438 end
439 439 end
440 440
441 441 # Returns the theme, controller name, and action as css classes for the
442 442 # HTML body.
443 443 def body_css_classes
444 444 css = []
445 445 if theme = Redmine::Themes.theme(Setting.ui_theme)
446 446 css << 'theme-' + theme.name
447 447 end
448 448
449 449 css << 'controller-' + params[:controller]
450 450 css << 'action-' + params[:action]
451 451 css.join(' ')
452 452 end
453 453
454 454 def accesskey(s)
455 455 Redmine::AccessKeys.key_for s
456 456 end
457 457
458 458 # Formats text according to system settings.
459 459 # 2 ways to call this method:
460 460 # * with a String: textilizable(text, options)
461 461 # * with an object and one of its attribute: textilizable(issue, :description, options)
462 462 def textilizable(*args)
463 463 options = args.last.is_a?(Hash) ? args.pop : {}
464 464 case args.size
465 465 when 1
466 466 obj = options[:object]
467 467 text = args.shift
468 468 when 2
469 469 obj = args.shift
470 470 attr = args.shift
471 471 text = obj.send(attr).to_s
472 472 else
473 473 raise ArgumentError, 'invalid arguments to textilizable'
474 474 end
475 475 return '' if text.blank?
476 476 project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
477 477 only_path = options.delete(:only_path) == false ? false : true
478 478
479 479 text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text, :object => obj, :attribute => attr) { |macro, args| exec_macro(macro, obj, args) }
480 480
481 481 @parsed_headings = []
482 482 text = parse_non_pre_blocks(text) do |text|
483 483 [:parse_inline_attachments, :parse_wiki_links, :parse_redmine_links, :parse_headings].each do |method_name|
484 484 send method_name, text, project, obj, attr, only_path, options
485 485 end
486 486 end
487 487
488 488 if @parsed_headings.any?
489 489 replace_toc(text, @parsed_headings)
490 490 end
491 491
492 492 text
493 493 end
494 494
495 495 def parse_non_pre_blocks(text)
496 496 s = StringScanner.new(text)
497 497 tags = []
498 498 parsed = ''
499 499 while !s.eos?
500 500 s.scan(/(.*?)(<(\/)?(pre|code)(.*?)>|\z)/im)
501 501 text, full_tag, closing, tag = s[1], s[2], s[3], s[4]
502 502 if tags.empty?
503 503 yield text
504 504 end
505 505 parsed << text
506 506 if tag
507 507 if closing
508 508 if tags.last == tag.downcase
509 509 tags.pop
510 510 end
511 511 else
512 512 tags << tag.downcase
513 513 end
514 514 parsed << full_tag
515 515 end
516 516 end
517 517 # Close any non closing tags
518 518 while tag = tags.pop
519 519 parsed << "</#{tag}>"
520 520 end
521 521 parsed.html_safe
522 522 end
523 523
524 524 def parse_inline_attachments(text, project, obj, attr, only_path, options)
525 525 # when using an image link, try to use an attachment, if possible
526 526 if options[:attachments] || (obj && obj.respond_to?(:attachments))
527 527 attachments = nil
528 528 text.gsub!(/src="([^\/"]+\.(bmp|gif|jpg|jpeg|png))"(\s+alt="([^"]*)")?/i) do |m|
529 529 filename, ext, alt, alttext = $1.downcase, $2, $3, $4
530 530 attachments ||= (options[:attachments] || obj.attachments).sort_by(&:created_on).reverse
531 531 # search for the picture in attachments
532 532 if found = attachments.detect { |att| att.filename.downcase == filename }
533 533 image_url = url_for :only_path => only_path, :controller => 'attachments', :action => 'download', :id => found
534 534 desc = found.description.to_s.gsub('"', '')
535 535 if !desc.blank? && alttext.blank?
536 536 alt = " title=\"#{desc}\" alt=\"#{desc}\""
537 537 end
538 538 "src=\"#{image_url}\"#{alt}".html_safe
539 539 else
540 540 m.html_safe
541 541 end
542 542 end
543 543 end
544 544 end
545 545
546 546 # Wiki links
547 547 #
548 548 # Examples:
549 549 # [[mypage]]
550 550 # [[mypage|mytext]]
551 551 # wiki links can refer other project wikis, using project name or identifier:
552 552 # [[project:]] -> wiki starting page
553 553 # [[project:|mytext]]
554 554 # [[project:mypage]]
555 555 # [[project:mypage|mytext]]
556 556 def parse_wiki_links(text, project, obj, attr, only_path, options)
557 557 text.gsub!(/(!)?(\[\[([^\]\n\|]+)(\|([^\]\n\|]+))?\]\])/) do |m|
558 558 link_project = project
559 559 esc, all, page, title = $1, $2, $3, $5
560 560 if esc.nil?
561 561 if page =~ /^([^\:]+)\:(.*)$/
562 562 link_project = Project.find_by_identifier($1) || Project.find_by_name($1)
563 563 page = $2
564 564 title ||= $1 if page.blank?
565 565 end
566 566
567 567 if link_project && link_project.wiki
568 568 # extract anchor
569 569 anchor = nil
570 570 if page =~ /^(.+?)\#(.+)$/
571 571 page, anchor = $1, $2
572 572 end
573 anchor = sanitize_anchor_name(anchor) if anchor.present?
573 574 # check if page exists
574 575 wiki_page = link_project.wiki.find_page(page)
575 576 url = if anchor.present? && wiki_page.present? && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version)) && obj.page == wiki_page
576 577 "##{anchor}"
577 578 else
578 579 case options[:wiki_links]
579 580 when :local; "#{page.present? ? Wiki.titleize(page) : ''}.html" + (anchor.present? ? "##{anchor}" : '')
580 581 when :anchor; "##{page.present? ? Wiki.titleize(page) : title}" + (anchor.present? ? "_#{anchor}" : '') # used for single-file wiki export
581 582 else
582 583 wiki_page_id = page.present? ? Wiki.titleize(page) : nil
583 584 url_for(:only_path => only_path, :controller => 'wiki', :action => 'show', :project_id => link_project, :id => wiki_page_id, :anchor => anchor)
584 585 end
585 586 end
586 587 link_to(h(title || page), url, :class => ('wiki-page' + (wiki_page ? '' : ' new')))
587 588 else
588 589 # project or wiki doesn't exist
589 590 all.html_safe
590 591 end
591 592 else
592 593 all.html_safe
593 594 end
594 595 end
595 596 end
596 597
597 598 # Redmine links
598 599 #
599 600 # Examples:
600 601 # Issues:
601 602 # #52 -> Link to issue #52
602 603 # Changesets:
603 604 # r52 -> Link to revision 52
604 605 # commit:a85130f -> Link to scmid starting with a85130f
605 606 # Documents:
606 607 # document#17 -> Link to document with id 17
607 608 # document:Greetings -> Link to the document with title "Greetings"
608 609 # document:"Some document" -> Link to the document with title "Some document"
609 610 # Versions:
610 611 # version#3 -> Link to version with id 3
611 612 # version:1.0.0 -> Link to version named "1.0.0"
612 613 # version:"1.0 beta 2" -> Link to version named "1.0 beta 2"
613 614 # Attachments:
614 615 # attachment:file.zip -> Link to the attachment of the current object named file.zip
615 616 # Source files:
616 617 # source:some/file -> Link to the file located at /some/file in the project's repository
617 618 # source:some/file@52 -> Link to the file's revision 52
618 619 # source:some/file#L120 -> Link to line 120 of the file
619 620 # source:some/file@52#L120 -> Link to line 120 of the file's revision 52
620 621 # export:some/file -> Force the download of the file
621 622 # Forum messages:
622 623 # message#1218 -> Link to message with id 1218
623 624 #
624 625 # Links can refer other objects from other projects, using project identifier:
625 626 # identifier:r52
626 627 # identifier:document:"Some document"
627 628 # identifier:version:1.0.0
628 629 # identifier:source:some/file
629 630 def parse_redmine_links(text, project, obj, attr, only_path, options)
630 631 text.gsub!(%r{([\s\(,\-\[\>]|^)(!)?(([a-z0-9\-]+):)?(attachment|document|version|commit|source|export|message|project)?((#|r)(\d+)|(:)([^"\s<>][^\s<>]*?|"[^"]+?"))(?=(?=[[:punct:]]\W)|,|\s|\]|<|$)}) do |m|
631 632 leading, esc, project_prefix, project_identifier, prefix, sep, identifier = $1, $2, $3, $4, $5, $7 || $9, $8 || $10
632 633 link = nil
633 634 if project_identifier
634 635 project = Project.visible.find_by_identifier(project_identifier)
635 636 end
636 637 if esc.nil?
637 638 if prefix.nil? && sep == 'r'
638 639 # project.changesets.visible raises an SQL error because of a double join on repositories
639 640 if project && project.repository && (changeset = Changeset.visible.find_by_repository_id_and_revision(project.repository.id, identifier))
640 641 link = link_to(h("#{project_prefix}r#{identifier}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :rev => changeset.revision},
641 642 :class => 'changeset',
642 643 :title => truncate_single_line(changeset.comments, :length => 100))
643 644 end
644 645 elsif sep == '#'
645 646 oid = identifier.to_i
646 647 case prefix
647 648 when nil
648 649 if issue = Issue.visible.find_by_id(oid, :include => :status)
649 650 link = link_to("##{oid}", {:only_path => only_path, :controller => 'issues', :action => 'show', :id => oid},
650 651 :class => issue.css_classes,
651 652 :title => "#{truncate(issue.subject, :length => 100)} (#{issue.status.name})")
652 653 end
653 654 when 'document'
654 655 if document = Document.visible.find_by_id(oid)
655 656 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
656 657 :class => 'document'
657 658 end
658 659 when 'version'
659 660 if version = Version.visible.find_by_id(oid)
660 661 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
661 662 :class => 'version'
662 663 end
663 664 when 'message'
664 665 if message = Message.visible.find_by_id(oid, :include => :parent)
665 666 link = link_to_message(message, {:only_path => only_path}, :class => 'message')
666 667 end
667 668 when 'project'
668 669 if p = Project.visible.find_by_id(oid)
669 670 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
670 671 end
671 672 end
672 673 elsif sep == ':'
673 674 # removes the double quotes if any
674 675 name = identifier.gsub(%r{^"(.*)"$}, "\\1")
675 676 case prefix
676 677 when 'document'
677 678 if project && document = project.documents.visible.find_by_title(name)
678 679 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
679 680 :class => 'document'
680 681 end
681 682 when 'version'
682 683 if project && version = project.versions.visible.find_by_name(name)
683 684 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
684 685 :class => 'version'
685 686 end
686 687 when 'commit'
687 688 if project && project.repository && (changeset = Changeset.visible.find(:first, :conditions => ["repository_id = ? AND scmid LIKE ?", project.repository.id, "#{name}%"]))
688 689 link = link_to h("#{project_prefix}#{name}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :rev => changeset.identifier},
689 690 :class => 'changeset',
690 691 :title => truncate_single_line(h(changeset.comments), :length => 100)
691 692 end
692 693 when 'source', 'export'
693 694 if project && project.repository && User.current.allowed_to?(:browse_repository, project)
694 695 name =~ %r{^[/\\]*(.*?)(@([0-9a-f]+))?(#(L\d+))?$}
695 696 path, rev, anchor = $1, $3, $5
696 697 link = link_to h("#{project_prefix}#{prefix}:#{name}"), {:controller => 'repositories', :action => 'entry', :id => project,
697 698 :path => to_path_param(path),
698 699 :rev => rev,
699 700 :anchor => anchor,
700 701 :format => (prefix == 'export' ? 'raw' : nil)},
701 702 :class => (prefix == 'export' ? 'source download' : 'source')
702 703 end
703 704 when 'attachment'
704 705 attachments = options[:attachments] || (obj && obj.respond_to?(:attachments) ? obj.attachments : nil)
705 706 if attachments && attachment = attachments.detect {|a| a.filename == name }
706 707 link = link_to h(attachment.filename), {:only_path => only_path, :controller => 'attachments', :action => 'download', :id => attachment},
707 708 :class => 'attachment'
708 709 end
709 710 when 'project'
710 711 if p = Project.visible.find(:first, :conditions => ["identifier = :s OR LOWER(name) = :s", {:s => name.downcase}])
711 712 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
712 713 end
713 714 end
714 715 end
715 716 end
716 717 (leading + (link || "#{project_prefix}#{prefix}#{sep}#{identifier}")).html_safe
717 718 end
718 719 end
719 720
720 721 HEADING_RE = /<h(1|2|3|4)( [^>]+)?>(.+?)<\/h(1|2|3|4)>/i unless const_defined?(:HEADING_RE)
721 722
722 723 # Headings and TOC
723 724 # Adds ids and links to headings unless options[:headings] is set to false
724 725 def parse_headings(text, project, obj, attr, only_path, options)
725 726 return if options[:headings] == false
726 727
727 728 text.gsub!(HEADING_RE) do
728 729 level, attrs, content = $1.to_i, $2, $3
729 730 item = strip_tags(content).strip
730 anchor = item.gsub(%r{[^\w\s\-]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
731 anchor = sanitize_anchor_name(item)
731 732 # used for single-file wiki export
732 733 anchor = "#{obj.page.title}_#{anchor}" if options[:wiki_links] == :anchor && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version))
733 734 @parsed_headings << [level, anchor, item]
734 735 "<a name=\"#{anchor}\"></a>\n<h#{level} #{attrs}>#{content}<a href=\"##{anchor}\" class=\"wiki-anchor\">&para;</a></h#{level}>"
735 736 end
736 737 end
737 738
738 739 TOC_RE = /<p>\{\{([<>]?)toc\}\}<\/p>/i unless const_defined?(:TOC_RE)
739 740
740 741 # Renders the TOC with given headings
741 742 def replace_toc(text, headings)
742 743 text.gsub!(TOC_RE) do
743 744 if headings.empty?
744 745 ''
745 746 else
746 747 div_class = 'toc'
747 748 div_class << ' right' if $1 == '>'
748 749 div_class << ' left' if $1 == '<'
749 750 out = "<ul class=\"#{div_class}\"><li>"
750 751 root = headings.map(&:first).min
751 752 current = root
752 753 started = false
753 754 headings.each do |level, anchor, item|
754 755 if level > current
755 756 out << '<ul><li>' * (level - current)
756 757 elsif level < current
757 758 out << "</li></ul>\n" * (current - level) + "</li><li>"
758 759 elsif started
759 760 out << '</li><li>'
760 761 end
761 762 out << "<a href=\"##{anchor}\">#{item}</a>"
762 763 current = level
763 764 started = true
764 765 end
765 766 out << '</li></ul>' * (current - root)
766 767 out << '</li></ul>'
767 768 end
768 769 end
769 770 end
770 771
771 772 # Same as Rails' simple_format helper without using paragraphs
772 773 def simple_format_without_paragraph(text)
773 774 text.to_s.
774 775 gsub(/\r\n?/, "\n"). # \r\n and \r -> \n
775 776 gsub(/\n\n+/, "<br /><br />"). # 2+ newline -> 2 br
776 777 gsub(/([^\n]\n)(?=[^\n])/, '\1<br />') # 1 newline -> br
777 778 end
778 779
779 780 def lang_options_for_select(blank=true)
780 781 (blank ? [["(auto)", ""]] : []) +
781 782 valid_languages.collect{|lang| [ ll(lang.to_s, :general_lang_name), lang.to_s]}.sort{|x,y| x.last <=> y.last }
782 783 end
783 784
784 785 def label_tag_for(name, option_tags = nil, options = {})
785 786 label_text = l(("field_"+field.to_s.gsub(/\_id$/, "")).to_sym) + (options.delete(:required) ? @template.content_tag("span", " *", :class => "required"): "")
786 787 content_tag("label", label_text)
787 788 end
788 789
789 790 def labelled_tabular_form_for(name, object, options, &proc)
790 791 options[:html] ||= {}
791 792 options[:html][:class] = 'tabular' unless options[:html].has_key?(:class)
792 793 form_for(name, object, options.merge({ :builder => TabularFormBuilder, :lang => current_language}), &proc)
793 794 end
794 795
795 796 def back_url_hidden_field_tag
796 797 back_url = params[:back_url] || request.env['HTTP_REFERER']
797 798 back_url = CGI.unescape(back_url.to_s)
798 799 hidden_field_tag('back_url', CGI.escape(back_url)) unless back_url.blank?
799 800 end
800 801
801 802 def check_all_links(form_name)
802 803 link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") +
803 804 " | ".html_safe +
804 805 link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)")
805 806 end
806 807
807 808 def progress_bar(pcts, options={})
808 809 pcts = [pcts, pcts] unless pcts.is_a?(Array)
809 810 pcts = pcts.collect(&:round)
810 811 pcts[1] = pcts[1] - pcts[0]
811 812 pcts << (100 - pcts[1] - pcts[0])
812 813 width = options[:width] || '100px;'
813 814 legend = options[:legend] || ''
814 815 content_tag('table',
815 816 content_tag('tr',
816 817 (pcts[0] > 0 ? content_tag('td', '', :style => "width: #{pcts[0]}%;", :class => 'closed') : ''.html_safe) +
817 818 (pcts[1] > 0 ? content_tag('td', '', :style => "width: #{pcts[1]}%;", :class => 'done') : ''.html_safe) +
818 819 (pcts[2] > 0 ? content_tag('td', '', :style => "width: #{pcts[2]}%;", :class => 'todo') : ''.html_safe)
819 820 ), :class => 'progress', :style => "width: #{width};").html_safe +
820 821 content_tag('p', legend, :class => 'pourcent').html_safe
821 822 end
822 823
823 824 def checked_image(checked=true)
824 825 if checked
825 826 image_tag 'toggle_check.png'
826 827 end
827 828 end
828 829
829 830 def context_menu(url)
830 831 unless @context_menu_included
831 832 content_for :header_tags do
832 833 javascript_include_tag('context_menu') +
833 834 stylesheet_link_tag('context_menu')
834 835 end
835 836 if l(:direction) == 'rtl'
836 837 content_for :header_tags do
837 838 stylesheet_link_tag('context_menu_rtl')
838 839 end
839 840 end
840 841 @context_menu_included = true
841 842 end
842 843 javascript_tag "new ContextMenu('#{ url_for(url) }')"
843 844 end
844 845
845 846 def context_menu_link(name, url, options={})
846 847 options[:class] ||= ''
847 848 if options.delete(:selected)
848 849 options[:class] << ' icon-checked disabled'
849 850 options[:disabled] = true
850 851 end
851 852 if options.delete(:disabled)
852 853 options.delete(:method)
853 854 options.delete(:confirm)
854 855 options.delete(:onclick)
855 856 options[:class] << ' disabled'
856 857 url = '#'
857 858 end
858 859 link_to h(name), url, options
859 860 end
860 861
861 862 def calendar_for(field_id)
862 863 include_calendar_headers_tags
863 864 image_tag("calendar.png", {:id => "#{field_id}_trigger",:class => "calendar-trigger"}) +
864 865 javascript_tag("Calendar.setup({inputField : '#{field_id}', ifFormat : '%Y-%m-%d', button : '#{field_id}_trigger' });")
865 866 end
866 867
867 868 def include_calendar_headers_tags
868 869 unless @calendar_headers_tags_included
869 870 @calendar_headers_tags_included = true
870 871 content_for :header_tags do
871 872 start_of_week = case Setting.start_of_week.to_i
872 873 when 1
873 874 'Calendar._FD = 1;' # Monday
874 875 when 7
875 876 'Calendar._FD = 0;' # Sunday
876 877 when 6
877 878 'Calendar._FD = 6;' # Saturday
878 879 else
879 880 '' # use language
880 881 end
881 882
882 883 javascript_include_tag('calendar/calendar') +
883 884 javascript_include_tag("calendar/lang/calendar-#{current_language.to_s.downcase}.js") +
884 885 javascript_tag(start_of_week) +
885 886 javascript_include_tag('calendar/calendar-setup') +
886 887 stylesheet_link_tag('calendar')
887 888 end
888 889 end
889 890 end
890 891
891 892 def content_for(name, content = nil, &block)
892 893 @has_content ||= {}
893 894 @has_content[name] = true
894 895 super(name, content, &block)
895 896 end
896 897
897 898 def has_content?(name)
898 899 (@has_content && @has_content[name]) || false
899 900 end
900 901
901 902 def email_delivery_enabled?
902 903 !!ActionMailer::Base.perform_deliveries
903 904 end
904 905
905 906 # Returns the avatar image tag for the given +user+ if avatars are enabled
906 907 # +user+ can be a User or a string that will be scanned for an email address (eg. 'joe <joe@foo.bar>')
907 908 def avatar(user, options = { })
908 909 if Setting.gravatar_enabled?
909 910 options.merge!({:ssl => (defined?(request) && request.ssl?), :default => Setting.gravatar_default})
910 911 email = nil
911 912 if user.respond_to?(:mail)
912 913 email = user.mail
913 914 elsif user.to_s =~ %r{<(.+?)>}
914 915 email = $1
915 916 end
916 917 return gravatar(email.to_s.downcase, options) unless email.blank? rescue nil
917 918 else
918 919 ''
919 920 end
920 921 end
921 922
923 def sanitize_anchor_name(anchor)
924 anchor.gsub(%r{[^\w\s\-]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
925 end
926
922 927 # Returns the javascript tags that are included in the html layout head
923 928 def javascript_heads
924 929 tags = javascript_include_tag(:defaults)
925 930 unless User.current.pref.warn_on_leaving_unsaved == '0'
926 931 tags << "\n".html_safe + javascript_tag("Event.observe(window, 'load', function(){ new WarnLeavingUnsaved('#{escape_javascript( l(:text_warn_on_leaving_unsaved) )}'); });")
927 932 end
928 933 tags
929 934 end
930 935
931 936 def favicon
932 937 "<link rel='shortcut icon' href='#{image_path('/favicon.ico')}' />".html_safe
933 938 end
934 939
935 940 def robot_exclusion_tag
936 941 '<meta name="robots" content="noindex,follow,noarchive" />'
937 942 end
938 943
939 944 # Returns true if arg is expected in the API response
940 945 def include_in_api_response?(arg)
941 946 unless @included_in_api_response
942 947 param = params[:include]
943 948 @included_in_api_response = param.is_a?(Array) ? param.collect(&:to_s) : param.to_s.split(',')
944 949 @included_in_api_response.collect!(&:strip)
945 950 end
946 951 @included_in_api_response.include?(arg.to_s)
947 952 end
948 953
949 954 # Returns options or nil if nometa param or X-Redmine-Nometa header
950 955 # was set in the request
951 956 def api_meta(options)
952 957 if params[:nometa].present? || request.headers['X-Redmine-Nometa']
953 958 # compatibility mode for activeresource clients that raise
954 959 # an error when unserializing an array with attributes
955 960 nil
956 961 else
957 962 options
958 963 end
959 964 end
960 965
961 966 private
962 967
963 968 def wiki_helper
964 969 helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
965 970 extend helper
966 971 return self
967 972 end
968 973
969 974 def link_to_content_update(text, url_params = {}, html_options = {})
970 975 link_to(text, url_params, html_options)
971 976 end
972 977 end
@@ -1,777 +1,787
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 353 # link with anchor
354 354 '[[CookBook documentation#One-section]]' => '<a href="/projects/ecookbook/wiki/CookBook_documentation#One-section" class="wiki-page">CookBook documentation</a>',
355 355 '[[Another page#anchor|Page]]' => '<a href="/projects/ecookbook/wiki/Another_page#anchor" class="wiki-page">Page</a>',
356 356 # page that doesn't exist
357 357 '[[Unknown page]]' => '<a href="/projects/ecookbook/wiki/Unknown_page" class="wiki-page new">Unknown page</a>',
358 358 '[[Unknown page|404]]' => '<a href="/projects/ecookbook/wiki/Unknown_page" class="wiki-page new">404</a>',
359 359 # link to another project wiki
360 360 '[[onlinestore:]]' => '<a href="/projects/onlinestore/wiki" class="wiki-page">onlinestore</a>',
361 361 '[[onlinestore:|Wiki]]' => '<a href="/projects/onlinestore/wiki" class="wiki-page">Wiki</a>',
362 362 '[[onlinestore:Start page]]' => '<a href="/projects/onlinestore/wiki/Start_page" class="wiki-page">Start page</a>',
363 363 '[[onlinestore:Start page|Text]]' => '<a href="/projects/onlinestore/wiki/Start_page" class="wiki-page">Text</a>',
364 364 '[[onlinestore:Unknown page]]' => '<a href="/projects/onlinestore/wiki/Unknown_page" class="wiki-page new">Unknown page</a>',
365 365 # striked through link
366 366 '-[[Another page|Page]]-' => '<del><a href="/projects/ecookbook/wiki/Another_page" class="wiki-page">Page</a></del>',
367 367 '-[[Another page|Page]] link-' => '<del><a href="/projects/ecookbook/wiki/Another_page" class="wiki-page">Page</a> link</del>',
368 368 # escaping
369 369 '![[Another page|Page]]' => '[[Another page|Page]]',
370 370 # project does not exist
371 371 '[[unknowproject:Start]]' => '[[unknowproject:Start]]',
372 372 '[[unknowproject:Start|Page title]]' => '[[unknowproject:Start|Page title]]',
373 373 }
374 374 @project = Project.find(1)
375 375 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
376 376 end
377 377
378 378 def test_wiki_links_within_local_file_generation_context
379 379
380 380 to_test = {
381 381 # link to a page
382 382 '[[CookBook documentation]]' => '<a href="CookBook_documentation.html" class="wiki-page">CookBook documentation</a>',
383 383 '[[CookBook documentation|documentation]]' => '<a href="CookBook_documentation.html" class="wiki-page">documentation</a>',
384 384 '[[CookBook documentation#One-section]]' => '<a href="CookBook_documentation.html#One-section" class="wiki-page">CookBook documentation</a>',
385 385 '[[CookBook documentation#One-section|documentation]]' => '<a href="CookBook_documentation.html#One-section" class="wiki-page">documentation</a>',
386 386 # page that doesn't exist
387 387 '[[Unknown page]]' => '<a href="Unknown_page.html" class="wiki-page new">Unknown page</a>',
388 388 '[[Unknown page|404]]' => '<a href="Unknown_page.html" class="wiki-page new">404</a>',
389 389 '[[Unknown page#anchor]]' => '<a href="Unknown_page.html#anchor" class="wiki-page new">Unknown page</a>',
390 390 '[[Unknown page#anchor|404]]' => '<a href="Unknown_page.html#anchor" class="wiki-page new">404</a>',
391 391 }
392 392
393 393 @project = Project.find(1)
394 394
395 395 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text, :wiki_links => :local) }
396 396 end
397 397
398 398 def test_html_tags
399 399 to_test = {
400 400 "<div>content</div>" => "<p>&lt;div&gt;content&lt;/div&gt;</p>",
401 401 "<div class=\"bold\">content</div>" => "<p>&lt;div class=\"bold\"&gt;content&lt;/div&gt;</p>",
402 402 "<script>some script;</script>" => "<p>&lt;script&gt;some script;&lt;/script&gt;</p>",
403 403 # do not escape pre/code tags
404 404 "<pre>\nline 1\nline2</pre>" => "<pre>\nline 1\nline2</pre>",
405 405 "<pre><code>\nline 1\nline2</code></pre>" => "<pre><code>\nline 1\nline2</code></pre>",
406 406 "<pre><div>content</div></pre>" => "<pre>&lt;div&gt;content&lt;/div&gt;</pre>",
407 407 "HTML comment: <!-- no comments -->" => "<p>HTML comment: &lt;!-- no comments --&gt;</p>",
408 408 "<!-- opening comment" => "<p>&lt;!-- opening comment</p>",
409 409 # remove attributes except class
410 410 "<pre class='foo'>some text</pre>" => "<pre class='foo'>some text</pre>",
411 411 '<pre class="foo">some text</pre>' => '<pre class="foo">some text</pre>',
412 412 "<pre class='foo bar'>some text</pre>" => "<pre class='foo bar'>some text</pre>",
413 413 '<pre class="foo bar">some text</pre>' => '<pre class="foo bar">some text</pre>',
414 414 "<pre onmouseover='alert(1)'>some text</pre>" => "<pre>some text</pre>",
415 415 # xss
416 416 '<pre><code class=""onmouseover="alert(1)">text</code></pre>' => '<pre><code>text</code></pre>',
417 417 '<pre class=""onmouseover="alert(1)">text</pre>' => '<pre>text</pre>',
418 418 }
419 419 to_test.each { |text, result| assert_equal result, textilizable(text) }
420 420 end
421 421
422 422 def test_allowed_html_tags
423 423 to_test = {
424 424 "<pre>preformatted text</pre>" => "<pre>preformatted text</pre>",
425 425 "<notextile>no *textile* formatting</notextile>" => "no *textile* formatting",
426 426 "<notextile>this is <tag>a tag</tag></notextile>" => "this is &lt;tag&gt;a tag&lt;/tag&gt;"
427 427 }
428 428 to_test.each { |text, result| assert_equal result, textilizable(text) }
429 429 end
430 430
431 431 def test_pre_tags
432 432 raw = <<-RAW
433 433 Before
434 434
435 435 <pre>
436 436 <prepared-statement-cache-size>32</prepared-statement-cache-size>
437 437 </pre>
438 438
439 439 After
440 440 RAW
441 441
442 442 expected = <<-EXPECTED
443 443 <p>Before</p>
444 444 <pre>
445 445 &lt;prepared-statement-cache-size&gt;32&lt;/prepared-statement-cache-size&gt;
446 446 </pre>
447 447 <p>After</p>
448 448 EXPECTED
449 449
450 450 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
451 451 end
452 452
453 453 def test_pre_content_should_not_parse_wiki_and_redmine_links
454 454 raw = <<-RAW
455 455 [[CookBook documentation]]
456 456
457 457 #1
458 458
459 459 <pre>
460 460 [[CookBook documentation]]
461 461
462 462 #1
463 463 </pre>
464 464 RAW
465 465
466 466 expected = <<-EXPECTED
467 467 <p><a href="/projects/ecookbook/wiki/CookBook_documentation" class="wiki-page">CookBook documentation</a></p>
468 468 <p><a href="/issues/1" class="issue status-1 priority-1" title="Can't print recipes (New)">#1</a></p>
469 469 <pre>
470 470 [[CookBook documentation]]
471 471
472 472 #1
473 473 </pre>
474 474 EXPECTED
475 475
476 476 @project = Project.find(1)
477 477 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
478 478 end
479 479
480 480 def test_non_closing_pre_blocks_should_be_closed
481 481 raw = <<-RAW
482 482 <pre><code>
483 483 RAW
484 484
485 485 expected = <<-EXPECTED
486 486 <pre><code>
487 487 </code></pre>
488 488 EXPECTED
489 489
490 490 @project = Project.find(1)
491 491 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
492 492 end
493 493
494 494 def test_syntax_highlight
495 495 raw = <<-RAW
496 496 <pre><code class="ruby">
497 497 # Some ruby code here
498 498 </code></pre>
499 499 RAW
500 500
501 501 expected = <<-EXPECTED
502 502 <pre><code class="ruby syntaxhl"><span class=\"CodeRay\"><span class="no">1</span> <span class="c"># Some ruby code here</span></span>
503 503 </code></pre>
504 504 EXPECTED
505 505
506 506 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
507 507 end
508 508
509 509 def test_wiki_links_in_tables
510 510 to_test = {"|[[Page|Link title]]|[[Other Page|Other title]]|\n|Cell 21|[[Last page]]|" =>
511 511 '<tr><td><a href="/projects/ecookbook/wiki/Page" class="wiki-page new">Link title</a></td>' +
512 512 '<td><a href="/projects/ecookbook/wiki/Other_Page" class="wiki-page new">Other title</a></td>' +
513 513 '</tr><tr><td>Cell 21</td><td><a href="/projects/ecookbook/wiki/Last_page" class="wiki-page new">Last page</a></td></tr>'
514 514 }
515 515 @project = Project.find(1)
516 516 to_test.each { |text, result| assert_equal "<table>#{result}</table>", textilizable(text).gsub(/[\t\n]/, '') }
517 517 end
518 518
519 519 def test_text_formatting
520 520 to_test = {'*_+bold, italic and underline+_*' => '<strong><em><ins>bold, italic and underline</ins></em></strong>',
521 521 '(_text within parentheses_)' => '(<em>text within parentheses</em>)',
522 522 'a *Humane Web* Text Generator' => 'a <strong>Humane Web</strong> Text Generator',
523 523 '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 524 '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 525 }
526 526 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
527 527 end
528 528
529 529 def test_wiki_horizontal_rule
530 530 assert_equal '<hr />', textilizable('---')
531 531 assert_equal '<p>Dashes: ---</p>', textilizable('Dashes: ---')
532 532 end
533 533
534 534 def test_footnotes
535 535 raw = <<-RAW
536 536 This is some text[1].
537 537
538 538 fn1. This is the foot note
539 539 RAW
540 540
541 541 expected = <<-EXPECTED
542 542 <p>This is some text<sup><a href=\"#fn1\">1</a></sup>.</p>
543 543 <p id="fn1" class="footnote"><sup>1</sup> This is the foot note</p>
544 544 EXPECTED
545 545
546 546 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
547 547 end
548 548
549 549 def test_headings
550 550 raw = 'h1. Some heading'
551 551 expected = %|<a name="Some-heading"></a>\n<h1 >Some heading<a href="#Some-heading" class="wiki-anchor">&para;</a></h1>|
552 552
553 553 assert_equal expected, textilizable(raw)
554 554 end
555 555
556 def test_headings_with_special_chars
557 # This test makes sure that the generated anchor names match the expected
558 # ones even if the heading text contains unconventional characters
559 raw = 'h1. Some heading related to version 0.5'
560 anchor = sanitize_anchor_name("Some-heading-related-to-version-0.5")
561 expected = %|<a name="#{anchor}"></a>\n<h1 >Some heading related to version 0.5<a href="##{anchor}" class="wiki-anchor">&para;</a></h1>|
562
563 assert_equal expected, textilizable(raw)
564 end
565
556 566 def test_wiki_links_within_wiki_page_context
557 567
558 568 page = WikiPage.find_by_title('Another_page' )
559 569
560 570 to_test = {
561 571 # link to another page
562 572 '[[CookBook documentation]]' => '<a href="/projects/ecookbook/wiki/CookBook_documentation" class="wiki-page">CookBook documentation</a>',
563 573 '[[CookBook documentation|documentation]]' => '<a href="/projects/ecookbook/wiki/CookBook_documentation" class="wiki-page">documentation</a>',
564 574 '[[CookBook documentation#One-section]]' => '<a href="/projects/ecookbook/wiki/CookBook_documentation#One-section" class="wiki-page">CookBook documentation</a>',
565 575 '[[CookBook documentation#One-section|documentation]]' => '<a href="/projects/ecookbook/wiki/CookBook_documentation#One-section" class="wiki-page">documentation</a>',
566 576 # link to the current page
567 577 '[[Another page]]' => '<a href="/projects/ecookbook/wiki/Another_page" class="wiki-page">Another page</a>',
568 578 '[[Another page|Page]]' => '<a href="/projects/ecookbook/wiki/Another_page" class="wiki-page">Page</a>',
569 579 '[[Another page#anchor]]' => '<a href="#anchor" class="wiki-page">Another page</a>',
570 580 '[[Another page#anchor|Page]]' => '<a href="#anchor" class="wiki-page">Page</a>',
571 581 # page that doesn't exist
572 582 '[[Unknown page]]' => '<a href="/projects/ecookbook/wiki/Unknown_page" class="wiki-page new">Unknown page</a>',
573 583 '[[Unknown page|404]]' => '<a href="/projects/ecookbook/wiki/Unknown_page" class="wiki-page new">404</a>',
574 584 '[[Unknown page#anchor]]' => '<a href="/projects/ecookbook/wiki/Unknown_page#anchor" class="wiki-page new">Unknown page</a>',
575 585 '[[Unknown page#anchor|404]]' => '<a href="/projects/ecookbook/wiki/Unknown_page#anchor" class="wiki-page new">404</a>',
576 586 }
577 587
578 588 @project = Project.find(1)
579 589
580 590 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(WikiContent.generate!( :text => text, :page => page ), :text) }
581 591 end
582 592
583 593 def test_wiki_links_anchor_option_should_prepend_page_title_to_href
584 594
585 595 to_test = {
586 596 # link to a page
587 597 '[[CookBook documentation]]' => '<a href="#CookBook_documentation" class="wiki-page">CookBook documentation</a>',
588 598 '[[CookBook documentation|documentation]]' => '<a href="#CookBook_documentation" class="wiki-page">documentation</a>',
589 599 '[[CookBook documentation#One-section]]' => '<a href="#CookBook_documentation_One-section" class="wiki-page">CookBook documentation</a>',
590 600 '[[CookBook documentation#One-section|documentation]]' => '<a href="#CookBook_documentation_One-section" class="wiki-page">documentation</a>',
591 601 # page that doesn't exist
592 602 '[[Unknown page]]' => '<a href="#Unknown_page" class="wiki-page new">Unknown page</a>',
593 603 '[[Unknown page|404]]' => '<a href="#Unknown_page" class="wiki-page new">404</a>',
594 604 '[[Unknown page#anchor]]' => '<a href="#Unknown_page_anchor" class="wiki-page new">Unknown page</a>',
595 605 '[[Unknown page#anchor|404]]' => '<a href="#Unknown_page_anchor" class="wiki-page new">404</a>',
596 606 }
597 607
598 608 @project = Project.find(1)
599 609
600 610 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text, :wiki_links => :anchor) }
601 611 end
602 612
603 613 def test_headings_in_wiki_single_page_export_should_be_prepended_with_page_title
604 614 page = WikiPage.generate!( :title => 'Page Title' )
605 615 content = WikiContent.generate!( :text => 'h1. Some heading', :page => page )
606 616
607 617 expected = %|<a name="Page_Title_Some-heading"></a>\n<h1 >Some heading<a href="#Page_Title_Some-heading" class="wiki-anchor">&para;</a></h1>|
608 618
609 619 assert_equal expected, textilizable(content, :text, :wiki_links => :anchor )
610 620 end
611 621
612 622 def test_table_of_content
613 623 raw = <<-RAW
614 624 {{toc}}
615 625
616 626 h1. Title
617 627
618 628 Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas sed libero.
619 629
620 630 h2. Subtitle with a [[Wiki]] link
621 631
622 632 Nullam commodo metus accumsan nulla. Curabitur lobortis dui id dolor.
623 633
624 634 h2. Subtitle with [[Wiki|another Wiki]] link
625 635
626 636 h2. Subtitle with %{color:red}red text%
627 637
628 638 <pre>
629 639 some code
630 640 </pre>
631 641
632 642 h3. Subtitle with *some* _modifiers_
633 643
634 644 h1. Another title
635 645
636 646 h3. An "Internet link":http://www.redmine.org/ inside subtitle
637 647
638 648 h2. "Project Name !/attachments/1234/logo_small.gif! !/attachments/5678/logo_2.png!":/projects/projectname/issues
639 649
640 650 RAW
641 651
642 652 expected = '<ul class="toc">' +
643 653 '<li><a href="#Title">Title</a>' +
644 654 '<ul>' +
645 655 '<li><a href="#Subtitle-with-a-Wiki-link">Subtitle with a Wiki link</a></li>' +
646 656 '<li><a href="#Subtitle-with-another-Wiki-link">Subtitle with another Wiki link</a></li>' +
647 657 '<li><a href="#Subtitle-with-red-text">Subtitle with red text</a>' +
648 658 '<ul>' +
649 659 '<li><a href="#Subtitle-with-some-modifiers">Subtitle with some modifiers</a></li>' +
650 660 '</ul>' +
651 661 '</li>' +
652 662 '</ul>' +
653 663 '</li>' +
654 664 '<li><a href="#Another-title">Another title</a>' +
655 665 '<ul>' +
656 666 '<li>' +
657 667 '<ul>' +
658 668 '<li><a href="#An-Internet-link-inside-subtitle">An Internet link inside subtitle</a></li>' +
659 669 '</ul>' +
660 670 '</li>' +
661 671 '<li><a href="#Project-Name">Project Name</a></li>' +
662 672 '</ul>' +
663 673 '</li>' +
664 674 '</ul>'
665 675
666 676 @project = Project.find(1)
667 677 assert textilizable(raw).gsub("\n", "").include?(expected), textilizable(raw)
668 678 end
669 679
670 680 def test_table_of_content_should_contain_included_page_headings
671 681 raw = <<-RAW
672 682 {{toc}}
673 683
674 684 h1. Included
675 685
676 686 {{include(Child_1)}}
677 687 RAW
678 688
679 689 expected = '<ul class="toc">' +
680 690 '<li><a href="#Included">Included</a></li>' +
681 691 '<li><a href="#Child-page-1">Child page 1</a></li>' +
682 692 '</ul>'
683 693
684 694 @project = Project.find(1)
685 695 assert textilizable(raw).gsub("\n", "").include?(expected)
686 696 end
687 697
688 698 def test_default_formatter
689 699 Setting.text_formatting = 'unknown'
690 700 text = 'a *link*: http://www.example.net/'
691 701 assert_equal '<p>a *link*: <a href="http://www.example.net/">http://www.example.net/</a></p>', textilizable(text)
692 702 Setting.text_formatting = 'textile'
693 703 end
694 704
695 705 def test_due_date_distance_in_words
696 706 to_test = { Date.today => 'Due in 0 days',
697 707 Date.today + 1 => 'Due in 1 day',
698 708 Date.today + 100 => 'Due in about 3 months',
699 709 Date.today + 20000 => 'Due in over 54 years',
700 710 Date.today - 1 => '1 day late',
701 711 Date.today - 100 => 'about 3 months late',
702 712 Date.today - 20000 => 'over 54 years late',
703 713 }
704 714 ::I18n.locale = :en
705 715 to_test.each do |date, expected|
706 716 assert_equal expected, due_date_distance_in_words(date)
707 717 end
708 718 end
709 719
710 720 def test_avatar
711 721 # turn on avatars
712 722 Setting.gravatar_enabled = '1'
713 723 assert avatar(User.find_by_mail('jsmith@somenet.foo')).include?(Digest::MD5.hexdigest('jsmith@somenet.foo'))
714 724 assert avatar('jsmith <jsmith@somenet.foo>').include?(Digest::MD5.hexdigest('jsmith@somenet.foo'))
715 725 assert_nil avatar('jsmith')
716 726 assert_nil avatar(nil)
717 727
718 728 # turn off avatars
719 729 Setting.gravatar_enabled = '0'
720 730 assert_equal '', avatar(User.find_by_mail('jsmith@somenet.foo'))
721 731 end
722 732
723 733 def test_link_to_user
724 734 user = User.find(2)
725 735 t = link_to_user(user)
726 736 assert_equal "<a href=\"/users/2\">#{ user.name }</a>", t
727 737 end
728 738
729 739 def test_link_to_user_should_not_link_to_locked_user
730 740 user = User.find(5)
731 741 assert user.locked?
732 742 t = link_to_user(user)
733 743 assert_equal user.name, t
734 744 end
735 745
736 746 def test_link_to_user_should_not_link_to_anonymous
737 747 user = User.anonymous
738 748 assert user.anonymous?
739 749 t = link_to_user(user)
740 750 assert_equal ::I18n.t(:label_user_anonymous), t
741 751 end
742 752
743 753 def test_link_to_project
744 754 project = Project.find(1)
745 755 assert_equal %(<a href="/projects/ecookbook">eCookbook</a>),
746 756 link_to_project(project)
747 757 assert_equal %(<a href="/projects/ecookbook/settings">eCookbook</a>),
748 758 link_to_project(project, :action => 'settings')
749 759 assert_equal %(<a href="http://test.host/projects/ecookbook?jump=blah">eCookbook</a>),
750 760 link_to_project(project, {:only_path => false, :jump => 'blah'})
751 761 assert_equal %(<a href="/projects/ecookbook/settings" class="project">eCookbook</a>),
752 762 link_to_project(project, {:action => 'settings'}, :class => "project")
753 763 end
754 764
755 765 def test_principals_options_for_select_with_users
756 766 users = [User.find(2), User.find(4)]
757 767 assert_equal %(<option value="2">John Smith</option><option value="4">Robert Hill</option>),
758 768 principals_options_for_select(users)
759 769 end
760 770
761 771 def test_principals_options_for_select_with_selected
762 772 users = [User.find(2), User.find(4)]
763 773 assert_equal %(<option value="2">John Smith</option><option value="4" selected="selected">Robert Hill</option>),
764 774 principals_options_for_select(users, User.find(4))
765 775 end
766 776
767 777 def test_principals_options_for_select_with_users_and_groups
768 778 users = [User.find(2), Group.find(11), User.find(4), Group.find(10)]
769 779 assert_equal %(<option value="2">John Smith</option><option value="4">Robert Hill</option>) +
770 780 %(<optgroup label="Groups"><option value="10">A Team</option><option value="11">B Team</option></optgroup>),
771 781 principals_options_for_select(users)
772 782 end
773 783
774 784 def test_principals_options_for_select_with_empty_collection
775 785 assert_equal '', principals_options_for_select([])
776 786 end
777 787 end
General Comments 0
You need to be logged in to leave comments. Login now