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