##// END OF EJS Templates
Use local links in wiki pages when possible (#3276)....
Etienne Massip -
r7438:1ee65e046913
parent child
Show More
@@ -1,966 +1,970
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 573 # check if page exists
574 574 wiki_page = link_project.wiki.find_page(page)
575 url = case options[:wiki_links]
575 url = if anchor.present? && wiki_page.present? && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version)) && obj.page == wiki_page
576 "##{anchor}"
577 else
578 case options[:wiki_links]
576 579 when :local; "#{title}.html"
577 580 when :anchor; "##{title}" # used for single-file wiki export
578 581 else
579 582 wiki_page_id = page.present? ? Wiki.titleize(page) : nil
580 583 url_for(:only_path => only_path, :controller => 'wiki', :action => 'show', :project_id => link_project, :id => wiki_page_id, :anchor => anchor)
581 584 end
585 end
582 586 link_to(h(title || page), url, :class => ('wiki-page' + (wiki_page ? '' : ' new')))
583 587 else
584 588 # project or wiki doesn't exist
585 589 all.html_safe
586 590 end
587 591 else
588 592 all.html_safe
589 593 end
590 594 end
591 595 end
592 596
593 597 # Redmine links
594 598 #
595 599 # Examples:
596 600 # Issues:
597 601 # #52 -> Link to issue #52
598 602 # Changesets:
599 603 # r52 -> Link to revision 52
600 604 # commit:a85130f -> Link to scmid starting with a85130f
601 605 # Documents:
602 606 # document#17 -> Link to document with id 17
603 607 # document:Greetings -> Link to the document with title "Greetings"
604 608 # document:"Some document" -> Link to the document with title "Some document"
605 609 # Versions:
606 610 # version#3 -> Link to version with id 3
607 611 # version:1.0.0 -> Link to version named "1.0.0"
608 612 # version:"1.0 beta 2" -> Link to version named "1.0 beta 2"
609 613 # Attachments:
610 614 # attachment:file.zip -> Link to the attachment of the current object named file.zip
611 615 # Source files:
612 616 # source:some/file -> Link to the file located at /some/file in the project's repository
613 617 # source:some/file@52 -> Link to the file's revision 52
614 618 # source:some/file#L120 -> Link to line 120 of the file
615 619 # source:some/file@52#L120 -> Link to line 120 of the file's revision 52
616 620 # export:some/file -> Force the download of the file
617 621 # Forum messages:
618 622 # message#1218 -> Link to message with id 1218
619 623 #
620 624 # Links can refer other objects from other projects, using project identifier:
621 625 # identifier:r52
622 626 # identifier:document:"Some document"
623 627 # identifier:version:1.0.0
624 628 # identifier:source:some/file
625 629 def parse_redmine_links(text, project, obj, attr, only_path, options)
626 630 text.gsub!(%r{([\s\(,\-\[\>]|^)(!)?(([a-z0-9\-]+):)?(attachment|document|version|commit|source|export|message|project)?((#|r)(\d+)|(:)([^"\s<>][^\s<>]*?|"[^"]+?"))(?=(?=[[:punct:]]\W)|,|\s|\]|<|$)}) do |m|
627 631 leading, esc, project_prefix, project_identifier, prefix, sep, identifier = $1, $2, $3, $4, $5, $7 || $9, $8 || $10
628 632 link = nil
629 633 if project_identifier
630 634 project = Project.visible.find_by_identifier(project_identifier)
631 635 end
632 636 if esc.nil?
633 637 if prefix.nil? && sep == 'r'
634 638 # project.changesets.visible raises an SQL error because of a double join on repositories
635 639 if project && project.repository && (changeset = Changeset.visible.find_by_repository_id_and_revision(project.repository.id, identifier))
636 640 link = link_to(h("#{project_prefix}r#{identifier}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :rev => changeset.revision},
637 641 :class => 'changeset',
638 642 :title => truncate_single_line(changeset.comments, :length => 100))
639 643 end
640 644 elsif sep == '#'
641 645 oid = identifier.to_i
642 646 case prefix
643 647 when nil
644 648 if issue = Issue.visible.find_by_id(oid, :include => :status)
645 649 link = link_to("##{oid}", {:only_path => only_path, :controller => 'issues', :action => 'show', :id => oid},
646 650 :class => issue.css_classes,
647 651 :title => "#{truncate(issue.subject, :length => 100)} (#{issue.status.name})")
648 652 end
649 653 when 'document'
650 654 if document = Document.visible.find_by_id(oid)
651 655 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
652 656 :class => 'document'
653 657 end
654 658 when 'version'
655 659 if version = Version.visible.find_by_id(oid)
656 660 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
657 661 :class => 'version'
658 662 end
659 663 when 'message'
660 664 if message = Message.visible.find_by_id(oid, :include => :parent)
661 665 link = link_to_message(message, {:only_path => only_path}, :class => 'message')
662 666 end
663 667 when 'project'
664 668 if p = Project.visible.find_by_id(oid)
665 669 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
666 670 end
667 671 end
668 672 elsif sep == ':'
669 673 # removes the double quotes if any
670 674 name = identifier.gsub(%r{^"(.*)"$}, "\\1")
671 675 case prefix
672 676 when 'document'
673 677 if project && document = project.documents.visible.find_by_title(name)
674 678 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
675 679 :class => 'document'
676 680 end
677 681 when 'version'
678 682 if project && version = project.versions.visible.find_by_name(name)
679 683 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
680 684 :class => 'version'
681 685 end
682 686 when 'commit'
683 687 if project && project.repository && (changeset = Changeset.visible.find(:first, :conditions => ["repository_id = ? AND scmid LIKE ?", project.repository.id, "#{name}%"]))
684 688 link = link_to h("#{project_prefix}#{name}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :rev => changeset.identifier},
685 689 :class => 'changeset',
686 690 :title => truncate_single_line(h(changeset.comments), :length => 100)
687 691 end
688 692 when 'source', 'export'
689 693 if project && project.repository && User.current.allowed_to?(:browse_repository, project)
690 694 name =~ %r{^[/\\]*(.*?)(@([0-9a-f]+))?(#(L\d+))?$}
691 695 path, rev, anchor = $1, $3, $5
692 696 link = link_to h("#{project_prefix}#{prefix}:#{name}"), {:controller => 'repositories', :action => 'entry', :id => project,
693 697 :path => to_path_param(path),
694 698 :rev => rev,
695 699 :anchor => anchor,
696 700 :format => (prefix == 'export' ? 'raw' : nil)},
697 701 :class => (prefix == 'export' ? 'source download' : 'source')
698 702 end
699 703 when 'attachment'
700 704 attachments = options[:attachments] || (obj && obj.respond_to?(:attachments) ? obj.attachments : nil)
701 705 if attachments && attachment = attachments.detect {|a| a.filename == name }
702 706 link = link_to h(attachment.filename), {:only_path => only_path, :controller => 'attachments', :action => 'download', :id => attachment},
703 707 :class => 'attachment'
704 708 end
705 709 when 'project'
706 710 if p = Project.visible.find(:first, :conditions => ["identifier = :s OR LOWER(name) = :s", {:s => name.downcase}])
707 711 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
708 712 end
709 713 end
710 714 end
711 715 end
712 716 (leading + (link || "#{project_prefix}#{prefix}#{sep}#{identifier}")).html_safe
713 717 end
714 718 end
715 719
716 720 HEADING_RE = /<h(1|2|3|4)( [^>]+)?>(.+?)<\/h(1|2|3|4)>/i unless const_defined?(:HEADING_RE)
717 721
718 722 # Headings and TOC
719 723 # Adds ids and links to headings unless options[:headings] is set to false
720 724 def parse_headings(text, project, obj, attr, only_path, options)
721 725 return if options[:headings] == false
722 726
723 727 text.gsub!(HEADING_RE) do
724 728 level, attrs, content = $1.to_i, $2, $3
725 729 item = strip_tags(content).strip
726 730 anchor = item.gsub(%r{[^\w\s\-]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
727 731 @parsed_headings << [level, anchor, item]
728 732 "<a name=\"#{anchor}\"></a>\n<h#{level} #{attrs}>#{content}<a href=\"##{anchor}\" class=\"wiki-anchor\">&para;</a></h#{level}>"
729 733 end
730 734 end
731 735
732 736 TOC_RE = /<p>\{\{([<>]?)toc\}\}<\/p>/i unless const_defined?(:TOC_RE)
733 737
734 738 # Renders the TOC with given headings
735 739 def replace_toc(text, headings)
736 740 text.gsub!(TOC_RE) do
737 741 if headings.empty?
738 742 ''
739 743 else
740 744 div_class = 'toc'
741 745 div_class << ' right' if $1 == '>'
742 746 div_class << ' left' if $1 == '<'
743 747 out = "<ul class=\"#{div_class}\"><li>"
744 748 root = headings.map(&:first).min
745 749 current = root
746 750 started = false
747 751 headings.each do |level, anchor, item|
748 752 if level > current
749 753 out << '<ul><li>' * (level - current)
750 754 elsif level < current
751 755 out << "</li></ul>\n" * (current - level) + "</li><li>"
752 756 elsif started
753 757 out << '</li><li>'
754 758 end
755 759 out << "<a href=\"##{anchor}\">#{item}</a>"
756 760 current = level
757 761 started = true
758 762 end
759 763 out << '</li></ul>' * (current - root)
760 764 out << '</li></ul>'
761 765 end
762 766 end
763 767 end
764 768
765 769 # Same as Rails' simple_format helper without using paragraphs
766 770 def simple_format_without_paragraph(text)
767 771 text.to_s.
768 772 gsub(/\r\n?/, "\n"). # \r\n and \r -> \n
769 773 gsub(/\n\n+/, "<br /><br />"). # 2+ newline -> 2 br
770 774 gsub(/([^\n]\n)(?=[^\n])/, '\1<br />') # 1 newline -> br
771 775 end
772 776
773 777 def lang_options_for_select(blank=true)
774 778 (blank ? [["(auto)", ""]] : []) +
775 779 valid_languages.collect{|lang| [ ll(lang.to_s, :general_lang_name), lang.to_s]}.sort{|x,y| x.last <=> y.last }
776 780 end
777 781
778 782 def label_tag_for(name, option_tags = nil, options = {})
779 783 label_text = l(("field_"+field.to_s.gsub(/\_id$/, "")).to_sym) + (options.delete(:required) ? @template.content_tag("span", " *", :class => "required"): "")
780 784 content_tag("label", label_text)
781 785 end
782 786
783 787 def labelled_tabular_form_for(name, object, options, &proc)
784 788 options[:html] ||= {}
785 789 options[:html][:class] = 'tabular' unless options[:html].has_key?(:class)
786 790 form_for(name, object, options.merge({ :builder => TabularFormBuilder, :lang => current_language}), &proc)
787 791 end
788 792
789 793 def back_url_hidden_field_tag
790 794 back_url = params[:back_url] || request.env['HTTP_REFERER']
791 795 back_url = CGI.unescape(back_url.to_s)
792 796 hidden_field_tag('back_url', CGI.escape(back_url)) unless back_url.blank?
793 797 end
794 798
795 799 def check_all_links(form_name)
796 800 link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") +
797 801 " | ".html_safe +
798 802 link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)")
799 803 end
800 804
801 805 def progress_bar(pcts, options={})
802 806 pcts = [pcts, pcts] unless pcts.is_a?(Array)
803 807 pcts = pcts.collect(&:round)
804 808 pcts[1] = pcts[1] - pcts[0]
805 809 pcts << (100 - pcts[1] - pcts[0])
806 810 width = options[:width] || '100px;'
807 811 legend = options[:legend] || ''
808 812 content_tag('table',
809 813 content_tag('tr',
810 814 (pcts[0] > 0 ? content_tag('td', '', :style => "width: #{pcts[0]}%;", :class => 'closed') : ''.html_safe) +
811 815 (pcts[1] > 0 ? content_tag('td', '', :style => "width: #{pcts[1]}%;", :class => 'done') : ''.html_safe) +
812 816 (pcts[2] > 0 ? content_tag('td', '', :style => "width: #{pcts[2]}%;", :class => 'todo') : ''.html_safe)
813 817 ), :class => 'progress', :style => "width: #{width};").html_safe +
814 818 content_tag('p', legend, :class => 'pourcent').html_safe
815 819 end
816 820
817 821 def checked_image(checked=true)
818 822 if checked
819 823 image_tag 'toggle_check.png'
820 824 end
821 825 end
822 826
823 827 def context_menu(url)
824 828 unless @context_menu_included
825 829 content_for :header_tags do
826 830 javascript_include_tag('context_menu') +
827 831 stylesheet_link_tag('context_menu')
828 832 end
829 833 if l(:direction) == 'rtl'
830 834 content_for :header_tags do
831 835 stylesheet_link_tag('context_menu_rtl')
832 836 end
833 837 end
834 838 @context_menu_included = true
835 839 end
836 840 javascript_tag "new ContextMenu('#{ url_for(url) }')"
837 841 end
838 842
839 843 def context_menu_link(name, url, options={})
840 844 options[:class] ||= ''
841 845 if options.delete(:selected)
842 846 options[:class] << ' icon-checked disabled'
843 847 options[:disabled] = true
844 848 end
845 849 if options.delete(:disabled)
846 850 options.delete(:method)
847 851 options.delete(:confirm)
848 852 options.delete(:onclick)
849 853 options[:class] << ' disabled'
850 854 url = '#'
851 855 end
852 856 link_to h(name), url, options
853 857 end
854 858
855 859 def calendar_for(field_id)
856 860 include_calendar_headers_tags
857 861 image_tag("calendar.png", {:id => "#{field_id}_trigger",:class => "calendar-trigger"}) +
858 862 javascript_tag("Calendar.setup({inputField : '#{field_id}', ifFormat : '%Y-%m-%d', button : '#{field_id}_trigger' });")
859 863 end
860 864
861 865 def include_calendar_headers_tags
862 866 unless @calendar_headers_tags_included
863 867 @calendar_headers_tags_included = true
864 868 content_for :header_tags do
865 869 start_of_week = case Setting.start_of_week.to_i
866 870 when 1
867 871 'Calendar._FD = 1;' # Monday
868 872 when 7
869 873 'Calendar._FD = 0;' # Sunday
870 874 when 6
871 875 'Calendar._FD = 6;' # Saturday
872 876 else
873 877 '' # use language
874 878 end
875 879
876 880 javascript_include_tag('calendar/calendar') +
877 881 javascript_include_tag("calendar/lang/calendar-#{current_language.to_s.downcase}.js") +
878 882 javascript_tag(start_of_week) +
879 883 javascript_include_tag('calendar/calendar-setup') +
880 884 stylesheet_link_tag('calendar')
881 885 end
882 886 end
883 887 end
884 888
885 889 def content_for(name, content = nil, &block)
886 890 @has_content ||= {}
887 891 @has_content[name] = true
888 892 super(name, content, &block)
889 893 end
890 894
891 895 def has_content?(name)
892 896 (@has_content && @has_content[name]) || false
893 897 end
894 898
895 899 def email_delivery_enabled?
896 900 !!ActionMailer::Base.perform_deliveries
897 901 end
898 902
899 903 # Returns the avatar image tag for the given +user+ if avatars are enabled
900 904 # +user+ can be a User or a string that will be scanned for an email address (eg. 'joe <joe@foo.bar>')
901 905 def avatar(user, options = { })
902 906 if Setting.gravatar_enabled?
903 907 options.merge!({:ssl => (defined?(request) && request.ssl?), :default => Setting.gravatar_default})
904 908 email = nil
905 909 if user.respond_to?(:mail)
906 910 email = user.mail
907 911 elsif user.to_s =~ %r{<(.+?)>}
908 912 email = $1
909 913 end
910 914 return gravatar(email.to_s.downcase, options) unless email.blank? rescue nil
911 915 else
912 916 ''
913 917 end
914 918 end
915 919
916 920 # Returns the javascript tags that are included in the html layout head
917 921 def javascript_heads
918 922 tags = javascript_include_tag(:defaults)
919 923 unless User.current.pref.warn_on_leaving_unsaved == '0'
920 924 tags << "\n".html_safe + javascript_tag("Event.observe(window, 'load', function(){ new WarnLeavingUnsaved('#{escape_javascript( l(:text_warn_on_leaving_unsaved) )}'); });")
921 925 end
922 926 tags
923 927 end
924 928
925 929 def favicon
926 930 "<link rel='shortcut icon' href='#{image_path('/favicon.ico')}' />".html_safe
927 931 end
928 932
929 933 def robot_exclusion_tag
930 934 '<meta name="robots" content="noindex,follow,noarchive" />'
931 935 end
932 936
933 937 # Returns true if arg is expected in the API response
934 938 def include_in_api_response?(arg)
935 939 unless @included_in_api_response
936 940 param = params[:include]
937 941 @included_in_api_response = param.is_a?(Array) ? param.collect(&:to_s) : param.to_s.split(',')
938 942 @included_in_api_response.collect!(&:strip)
939 943 end
940 944 @included_in_api_response.include?(arg.to_s)
941 945 end
942 946
943 947 # Returns options or nil if nometa param or X-Redmine-Nometa header
944 948 # was set in the request
945 949 def api_meta(options)
946 950 if params[:nometa].present? || request.headers['X-Redmine-Nometa']
947 951 # compatibility mode for activeresource clients that raise
948 952 # an error when unserializing an array with attributes
949 953 nil
950 954 else
951 955 options
952 956 end
953 957 end
954 958
955 959 private
956 960
957 961 def wiki_helper
958 962 helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
959 963 extend helper
960 964 return self
961 965 end
962 966
963 967 def link_to_content_update(text, url_params = {}, html_options = {})
964 968 link_to(text, url_params, html_options)
965 969 end
966 970 end
@@ -1,701 +1,728
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_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 529 def test_headings
530 530 raw = 'h1. Some heading'
531 531 expected = %|<a name="Some-heading"></a>\n<h1 >Some heading<a href="#Some-heading" class="wiki-anchor">&para;</a></h1>|
532 532
533 533 assert_equal expected, textilizable(raw)
534 534 end
535 535
536 def test_wiki_links_within_wiki_page_context
537
538 page = WikiPage.find_by_title('Another_page' )
539
540 to_test = {
541 # link to another page
542 '[[CookBook documentation]]' => '<a href="/projects/ecookbook/wiki/CookBook_documentation" class="wiki-page">CookBook documentation</a>',
543 '[[CookBook documentation|documentation]]' => '<a href="/projects/ecookbook/wiki/CookBook_documentation" class="wiki-page">documentation</a>',
544 '[[CookBook documentation#One-section]]' => '<a href="/projects/ecookbook/wiki/CookBook_documentation#One-section" class="wiki-page">CookBook documentation</a>',
545 '[[CookBook documentation#One-section|documentation]]' => '<a href="/projects/ecookbook/wiki/CookBook_documentation#One-section" class="wiki-page">documentation</a>',
546 # link to the current page
547 '[[Another page]]' => '<a href="/projects/ecookbook/wiki/Another_page" class="wiki-page">Another page</a>',
548 '[[Another page|Page]]' => '<a href="/projects/ecookbook/wiki/Another_page" class="wiki-page">Page</a>',
549 '[[Another page#anchor]]' => '<a href="#anchor" class="wiki-page">Another page</a>',
550 '[[Another page#anchor|Page]]' => '<a href="#anchor" class="wiki-page">Page</a>',
551 # page that doesn't exist
552 '[[Unknown page]]' => '<a href="/projects/ecookbook/wiki/Unknown_page" class="wiki-page new">Unknown page</a>',
553 '[[Unknown page|404]]' => '<a href="/projects/ecookbook/wiki/Unknown_page" class="wiki-page new">404</a>',
554 '[[Unknown page#anchor]]' => '<a href="/projects/ecookbook/wiki/Unknown_page#anchor" class="wiki-page new">Unknown page</a>',
555 '[[Unknown page#anchor|404]]' => '<a href="/projects/ecookbook/wiki/Unknown_page#anchor" class="wiki-page new">404</a>',
556 }
557
558 @project = Project.find(1)
559
560 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(WikiContent.generate!( :text => text, :page => page ), :text) }
561 end
562
536 563 def test_table_of_content
537 564 raw = <<-RAW
538 565 {{toc}}
539 566
540 567 h1. Title
541 568
542 569 Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas sed libero.
543 570
544 571 h2. Subtitle with a [[Wiki]] link
545 572
546 573 Nullam commodo metus accumsan nulla. Curabitur lobortis dui id dolor.
547 574
548 575 h2. Subtitle with [[Wiki|another Wiki]] link
549 576
550 577 h2. Subtitle with %{color:red}red text%
551 578
552 579 <pre>
553 580 some code
554 581 </pre>
555 582
556 583 h3. Subtitle with *some* _modifiers_
557 584
558 585 h1. Another title
559 586
560 587 h3. An "Internet link":http://www.redmine.org/ inside subtitle
561 588
562 589 h2. "Project Name !/attachments/1234/logo_small.gif! !/attachments/5678/logo_2.png!":/projects/projectname/issues
563 590
564 591 RAW
565 592
566 593 expected = '<ul class="toc">' +
567 594 '<li><a href="#Title">Title</a>' +
568 595 '<ul>' +
569 596 '<li><a href="#Subtitle-with-a-Wiki-link">Subtitle with a Wiki link</a></li>' +
570 597 '<li><a href="#Subtitle-with-another-Wiki-link">Subtitle with another Wiki link</a></li>' +
571 598 '<li><a href="#Subtitle-with-red-text">Subtitle with red text</a>' +
572 599 '<ul>' +
573 600 '<li><a href="#Subtitle-with-some-modifiers">Subtitle with some modifiers</a></li>' +
574 601 '</ul>' +
575 602 '</li>' +
576 603 '</ul>' +
577 604 '</li>' +
578 605 '<li><a href="#Another-title">Another title</a>' +
579 606 '<ul>' +
580 607 '<li>' +
581 608 '<ul>' +
582 609 '<li><a href="#An-Internet-link-inside-subtitle">An Internet link inside subtitle</a></li>' +
583 610 '</ul>' +
584 611 '</li>' +
585 612 '<li><a href="#Project-Name">Project Name</a></li>' +
586 613 '</ul>' +
587 614 '</li>' +
588 615 '</ul>'
589 616
590 617 @project = Project.find(1)
591 618 assert textilizable(raw).gsub("\n", "").include?(expected), textilizable(raw)
592 619 end
593 620
594 621 def test_table_of_content_should_contain_included_page_headings
595 622 raw = <<-RAW
596 623 {{toc}}
597 624
598 625 h1. Included
599 626
600 627 {{include(Child_1)}}
601 628 RAW
602 629
603 630 expected = '<ul class="toc">' +
604 631 '<li><a href="#Included">Included</a></li>' +
605 632 '<li><a href="#Child-page-1">Child page 1</a></li>' +
606 633 '</ul>'
607 634
608 635 @project = Project.find(1)
609 636 assert textilizable(raw).gsub("\n", "").include?(expected)
610 637 end
611 638
612 639 def test_default_formatter
613 640 Setting.text_formatting = 'unknown'
614 641 text = 'a *link*: http://www.example.net/'
615 642 assert_equal '<p>a *link*: <a href="http://www.example.net/">http://www.example.net/</a></p>', textilizable(text)
616 643 Setting.text_formatting = 'textile'
617 644 end
618 645
619 646 def test_due_date_distance_in_words
620 647 to_test = { Date.today => 'Due in 0 days',
621 648 Date.today + 1 => 'Due in 1 day',
622 649 Date.today + 100 => 'Due in about 3 months',
623 650 Date.today + 20000 => 'Due in over 54 years',
624 651 Date.today - 1 => '1 day late',
625 652 Date.today - 100 => 'about 3 months late',
626 653 Date.today - 20000 => 'over 54 years late',
627 654 }
628 655 ::I18n.locale = :en
629 656 to_test.each do |date, expected|
630 657 assert_equal expected, due_date_distance_in_words(date)
631 658 end
632 659 end
633 660
634 661 def test_avatar
635 662 # turn on avatars
636 663 Setting.gravatar_enabled = '1'
637 664 assert avatar(User.find_by_mail('jsmith@somenet.foo')).include?(Digest::MD5.hexdigest('jsmith@somenet.foo'))
638 665 assert avatar('jsmith <jsmith@somenet.foo>').include?(Digest::MD5.hexdigest('jsmith@somenet.foo'))
639 666 assert_nil avatar('jsmith')
640 667 assert_nil avatar(nil)
641 668
642 669 # turn off avatars
643 670 Setting.gravatar_enabled = '0'
644 671 assert_equal '', avatar(User.find_by_mail('jsmith@somenet.foo'))
645 672 end
646 673
647 674 def test_link_to_user
648 675 user = User.find(2)
649 676 t = link_to_user(user)
650 677 assert_equal "<a href=\"/users/2\">#{ user.name }</a>", t
651 678 end
652 679
653 680 def test_link_to_user_should_not_link_to_locked_user
654 681 user = User.find(5)
655 682 assert user.locked?
656 683 t = link_to_user(user)
657 684 assert_equal user.name, t
658 685 end
659 686
660 687 def test_link_to_user_should_not_link_to_anonymous
661 688 user = User.anonymous
662 689 assert user.anonymous?
663 690 t = link_to_user(user)
664 691 assert_equal ::I18n.t(:label_user_anonymous), t
665 692 end
666 693
667 694 def test_link_to_project
668 695 project = Project.find(1)
669 696 assert_equal %(<a href="/projects/ecookbook">eCookbook</a>),
670 697 link_to_project(project)
671 698 assert_equal %(<a href="/projects/ecookbook/settings">eCookbook</a>),
672 699 link_to_project(project, :action => 'settings')
673 700 assert_equal %(<a href="http://test.host/projects/ecookbook?jump=blah">eCookbook</a>),
674 701 link_to_project(project, {:only_path => false, :jump => 'blah'})
675 702 assert_equal %(<a href="/projects/ecookbook/settings" class="project">eCookbook</a>),
676 703 link_to_project(project, {:action => 'settings'}, :class => "project")
677 704 end
678 705
679 706 def test_principals_options_for_select_with_users
680 707 users = [User.find(2), User.find(4)]
681 708 assert_equal %(<option value="2">John Smith</option><option value="4">Robert Hill</option>),
682 709 principals_options_for_select(users)
683 710 end
684 711
685 712 def test_principals_options_for_select_with_selected
686 713 users = [User.find(2), User.find(4)]
687 714 assert_equal %(<option value="2">John Smith</option><option value="4" selected="selected">Robert Hill</option>),
688 715 principals_options_for_select(users, User.find(4))
689 716 end
690 717
691 718 def test_principals_options_for_select_with_users_and_groups
692 719 users = [User.find(2), Group.find(11), User.find(4), Group.find(10)]
693 720 assert_equal %(<option value="2">John Smith</option><option value="4">Robert Hill</option>) +
694 721 %(<optgroup label="Groups"><option value="10">A Team</option><option value="11">B Team</option></optgroup>),
695 722 principals_options_for_select(users)
696 723 end
697 724
698 725 def test_principals_options_for_select_with_empty_collection
699 726 assert_equal '', principals_options_for_select([])
700 727 end
701 728 end
General Comments 0
You need to be logged in to leave comments. Login now