##// END OF EJS Templates
Extract headings and TOC parsing from the textile formatter....
Jean-Philippe Lang -
r4262:024ff96ee27a
parent child
Show More
@@ -1,841 +1,873
1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
1 # Redmine - project management software
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 '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
108 108 link_to(text, {:controller => 'repositories', :action => 'revision', :id => project, :rev => revision}, :title => l(:label_revision_id, revision))
109 109 end
110 110
111 111 # Generates a link to a project if active
112 112 # Examples:
113 113 #
114 114 # link_to_project(project) # => link to the specified project overview
115 115 # link_to_project(project, :action=>'settings') # => link to project settings
116 116 # link_to_project(project, {:only_path => false}, :class => "project") # => 3rd arg adds html options
117 117 # link_to_project(project, {}, :class => "project") # => html options with default url (project overview)
118 118 #
119 119 def link_to_project(project, options={}, html_options = nil)
120 120 if project.active?
121 121 url = {:controller => 'projects', :action => 'show', :id => project}.merge(options)
122 122 link_to(h(project), url, html_options)
123 123 else
124 124 h(project)
125 125 end
126 126 end
127 127
128 128 def toggle_link(name, id, options={})
129 129 onclick = "Element.toggle('#{id}'); "
130 130 onclick << (options[:focus] ? "Form.Element.focus('#{options[:focus]}'); " : "this.blur(); ")
131 131 onclick << "return false;"
132 132 link_to(name, "#", :onclick => onclick)
133 133 end
134 134
135 135 def image_to_function(name, function, html_options = {})
136 136 html_options.symbolize_keys!
137 137 tag(:input, html_options.merge({
138 138 :type => "image", :src => image_path(name),
139 139 :onclick => (html_options[:onclick] ? "#{html_options[:onclick]}; " : "") + "#{function};"
140 140 }))
141 141 end
142 142
143 143 def prompt_to_remote(name, text, param, url, html_options = {})
144 144 html_options[:onclick] = "promptToRemote('#{text}', '#{param}', '#{url_for(url)}'); return false;"
145 145 link_to name, {}, html_options
146 146 end
147 147
148 148 def format_activity_title(text)
149 149 h(truncate_single_line(text, :length => 100))
150 150 end
151 151
152 152 def format_activity_day(date)
153 153 date == Date.today ? l(:label_today).titleize : format_date(date)
154 154 end
155 155
156 156 def format_activity_description(text)
157 157 h(truncate(text.to_s, :length => 120).gsub(%r{[\r\n]*<(pre|code)>.*$}m, '...')).gsub(/[\r\n]+/, "<br />")
158 158 end
159 159
160 160 def format_version_name(version)
161 161 if version.project == @project
162 162 h(version)
163 163 else
164 164 h("#{version.project} - #{version}")
165 165 end
166 166 end
167 167
168 168 def due_date_distance_in_words(date)
169 169 if date
170 170 l((date < Date.today ? :label_roadmap_overdue : :label_roadmap_due_in), distance_of_date_in_words(Date.today, date))
171 171 end
172 172 end
173 173
174 174 def render_page_hierarchy(pages, node=nil)
175 175 content = ''
176 176 if pages[node]
177 177 content << "<ul class=\"pages-hierarchy\">\n"
178 178 pages[node].each do |page|
179 179 content << "<li>"
180 180 content << link_to(h(page.pretty_title), {:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title},
181 181 :title => (page.respond_to?(:updated_on) ? l(:label_updated_time, distance_of_time_in_words(Time.now, page.updated_on)) : nil))
182 182 content << "\n" + render_page_hierarchy(pages, page.id) if pages[page.id]
183 183 content << "</li>\n"
184 184 end
185 185 content << "</ul>\n"
186 186 end
187 187 content
188 188 end
189 189
190 190 # Renders flash messages
191 191 def render_flash_messages
192 192 s = ''
193 193 flash.each do |k,v|
194 194 s << content_tag('div', v, :class => "flash #{k}")
195 195 end
196 196 s
197 197 end
198 198
199 199 # Renders tabs and their content
200 200 def render_tabs(tabs)
201 201 if tabs.any?
202 202 render :partial => 'common/tabs', :locals => {:tabs => tabs}
203 203 else
204 204 content_tag 'p', l(:label_no_data), :class => "nodata"
205 205 end
206 206 end
207 207
208 208 # Renders the project quick-jump box
209 209 def render_project_jump_box
210 210 # Retrieve them now to avoid a COUNT query
211 211 projects = User.current.projects.all
212 212 if projects.any?
213 213 s = '<select onchange="if (this.value != \'\') { window.location = this.value; }">' +
214 214 "<option value=''>#{ l(:label_jump_to_a_project) }</option>" +
215 215 '<option value="" disabled="disabled">---</option>'
216 216 s << project_tree_options_for_select(projects, :selected => @project) do |p|
217 217 { :value => url_for(:controller => 'projects', :action => 'show', :id => p, :jump => current_menu_item) }
218 218 end
219 219 s << '</select>'
220 220 s
221 221 end
222 222 end
223 223
224 224 def project_tree_options_for_select(projects, options = {})
225 225 s = ''
226 226 project_tree(projects) do |project, level|
227 227 name_prefix = (level > 0 ? ('&nbsp;' * 2 * level + '&#187; ') : '')
228 228 tag_options = {:value => project.id}
229 229 if project == options[:selected] || (options[:selected].respond_to?(:include?) && options[:selected].include?(project))
230 230 tag_options[:selected] = 'selected'
231 231 else
232 232 tag_options[:selected] = nil
233 233 end
234 234 tag_options.merge!(yield(project)) if block_given?
235 235 s << content_tag('option', name_prefix + h(project), tag_options)
236 236 end
237 237 s
238 238 end
239 239
240 240 # Yields the given block for each project with its level in the tree
241 241 #
242 242 # Wrapper for Project#project_tree
243 243 def project_tree(projects, &block)
244 244 Project.project_tree(projects, &block)
245 245 end
246 246
247 247 def project_nested_ul(projects, &block)
248 248 s = ''
249 249 if projects.any?
250 250 ancestors = []
251 251 projects.sort_by(&:lft).each do |project|
252 252 if (ancestors.empty? || project.is_descendant_of?(ancestors.last))
253 253 s << "<ul>\n"
254 254 else
255 255 ancestors.pop
256 256 s << "</li>"
257 257 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
258 258 ancestors.pop
259 259 s << "</ul></li>\n"
260 260 end
261 261 end
262 262 s << "<li>"
263 263 s << yield(project).to_s
264 264 ancestors << project
265 265 end
266 266 s << ("</li></ul>\n" * ancestors.size)
267 267 end
268 268 s
269 269 end
270 270
271 271 def principals_check_box_tags(name, principals)
272 272 s = ''
273 273 principals.sort.each do |principal|
274 274 s << "<label>#{ check_box_tag name, principal.id, false } #{h principal}</label>\n"
275 275 end
276 276 s
277 277 end
278 278
279 279 # Truncates and returns the string as a single line
280 280 def truncate_single_line(string, *args)
281 281 truncate(string.to_s, *args).gsub(%r{[\r\n]+}m, ' ')
282 282 end
283 283
284 284 # Truncates at line break after 250 characters or options[:length]
285 285 def truncate_lines(string, options={})
286 286 length = options[:length] || 250
287 287 if string.to_s =~ /\A(.{#{length}}.*?)$/m
288 288 "#{$1}..."
289 289 else
290 290 string
291 291 end
292 292 end
293 293
294 294 def html_hours(text)
295 295 text.gsub(%r{(\d+)\.(\d+)}, '<span class="hours hours-int">\1</span><span class="hours hours-dec">.\2</span>')
296 296 end
297 297
298 298 def authoring(created, author, options={})
299 299 l(options[:label] || :label_added_time_by, :author => link_to_user(author), :age => time_tag(created))
300 300 end
301 301
302 302 def time_tag(time)
303 303 text = distance_of_time_in_words(Time.now, time)
304 304 if @project
305 305 link_to(text, {:controller => 'activities', :action => 'index', :id => @project, :from => time.to_date}, :title => format_time(time))
306 306 else
307 307 content_tag('acronym', text, :title => format_time(time))
308 308 end
309 309 end
310 310
311 311 def syntax_highlight(name, content)
312 312 Redmine::SyntaxHighlighting.highlight_by_filename(content, name)
313 313 end
314 314
315 315 def to_path_param(path)
316 316 path.to_s.split(%r{[/\\]}).select {|p| !p.blank?}
317 317 end
318 318
319 319 def pagination_links_full(paginator, count=nil, options={})
320 320 page_param = options.delete(:page_param) || :page
321 321 per_page_links = options.delete(:per_page_links)
322 322 url_param = params.dup
323 323 # don't reuse query params if filters are present
324 324 url_param.merge!(:fields => nil, :values => nil, :operators => nil) if url_param.delete(:set_filter)
325 325
326 326 html = ''
327 327 if paginator.current.previous
328 328 html << link_to_remote_content_update('&#171; ' + l(:label_previous), url_param.merge(page_param => paginator.current.previous)) + ' '
329 329 end
330 330
331 331 html << (pagination_links_each(paginator, options) do |n|
332 332 link_to_remote_content_update(n.to_s, url_param.merge(page_param => n))
333 333 end || '')
334 334
335 335 if paginator.current.next
336 336 html << ' ' + link_to_remote_content_update((l(:label_next) + ' &#187;'), url_param.merge(page_param => paginator.current.next))
337 337 end
338 338
339 339 unless count.nil?
340 340 html << " (#{paginator.current.first_item}-#{paginator.current.last_item}/#{count})"
341 341 if per_page_links != false && links = per_page_links(paginator.items_per_page)
342 342 html << " | #{links}"
343 343 end
344 344 end
345 345
346 346 html
347 347 end
348 348
349 349 def per_page_links(selected=nil)
350 350 url_param = params.dup
351 351 url_param.clear if url_param.has_key?(:set_filter)
352 352
353 353 links = Setting.per_page_options_array.collect do |n|
354 354 n == selected ? n : link_to_remote(n, {:update => "content",
355 355 :url => params.dup.merge(:per_page => n),
356 356 :method => :get},
357 357 {:href => url_for(url_param.merge(:per_page => n))})
358 358 end
359 359 links.size > 1 ? l(:label_display_per_page, links.join(', ')) : nil
360 360 end
361 361
362 362 def reorder_links(name, url)
363 363 link_to(image_tag('2uparrow.png', :alt => l(:label_sort_highest)), url.merge({"#{name}[move_to]" => 'highest'}), :method => :post, :title => l(:label_sort_highest)) +
364 364 link_to(image_tag('1uparrow.png', :alt => l(:label_sort_higher)), url.merge({"#{name}[move_to]" => 'higher'}), :method => :post, :title => l(:label_sort_higher)) +
365 365 link_to(image_tag('1downarrow.png', :alt => l(:label_sort_lower)), url.merge({"#{name}[move_to]" => 'lower'}), :method => :post, :title => l(:label_sort_lower)) +
366 366 link_to(image_tag('2downarrow.png', :alt => l(:label_sort_lowest)), url.merge({"#{name}[move_to]" => 'lowest'}), :method => :post, :title => l(:label_sort_lowest))
367 367 end
368 368
369 369 def breadcrumb(*args)
370 370 elements = args.flatten
371 371 elements.any? ? content_tag('p', args.join(' &#187; ') + ' &#187; ', :class => 'breadcrumb') : nil
372 372 end
373 373
374 374 def other_formats_links(&block)
375 375 concat('<p class="other-formats">' + l(:label_export_to))
376 376 yield Redmine::Views::OtherFormatsBuilder.new(self)
377 377 concat('</p>')
378 378 end
379 379
380 380 def page_header_title
381 381 if @project.nil? || @project.new_record?
382 382 h(Setting.app_title)
383 383 else
384 384 b = []
385 385 ancestors = (@project.root? ? [] : @project.ancestors.visible)
386 386 if ancestors.any?
387 387 root = ancestors.shift
388 388 b << link_to_project(root, {:jump => current_menu_item}, :class => 'root')
389 389 if ancestors.size > 2
390 390 b << '&#8230;'
391 391 ancestors = ancestors[-2, 2]
392 392 end
393 393 b += ancestors.collect {|p| link_to_project(p, {:jump => current_menu_item}, :class => 'ancestor') }
394 394 end
395 395 b << h(@project)
396 396 b.join(' &#187; ')
397 397 end
398 398 end
399 399
400 400 def html_title(*args)
401 401 if args.empty?
402 402 title = []
403 403 title << @project.name if @project
404 404 title += @html_title if @html_title
405 405 title << Setting.app_title
406 406 title.select {|t| !t.blank? }.join(' - ')
407 407 else
408 408 @html_title ||= []
409 409 @html_title += args
410 410 end
411 411 end
412 412
413 413 # Returns the theme, controller name, and action as css classes for the
414 414 # HTML body.
415 415 def body_css_classes
416 416 css = []
417 417 if theme = Redmine::Themes.theme(Setting.ui_theme)
418 418 css << 'theme-' + theme.name
419 419 end
420 420
421 421 css << 'controller-' + params[:controller]
422 422 css << 'action-' + params[:action]
423 423 css.join(' ')
424 424 end
425 425
426 426 def accesskey(s)
427 427 Redmine::AccessKeys.key_for s
428 428 end
429 429
430 430 # Formats text according to system settings.
431 431 # 2 ways to call this method:
432 432 # * with a String: textilizable(text, options)
433 433 # * with an object and one of its attribute: textilizable(issue, :description, options)
434 434 def textilizable(*args)
435 435 options = args.last.is_a?(Hash) ? args.pop : {}
436 436 case args.size
437 437 when 1
438 438 obj = options[:object]
439 439 text = args.shift
440 440 when 2
441 441 obj = args.shift
442 442 attr = args.shift
443 443 text = obj.send(attr).to_s
444 444 else
445 445 raise ArgumentError, 'invalid arguments to textilizable'
446 446 end
447 447 return '' if text.blank?
448 448 project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
449 449 only_path = options.delete(:only_path) == false ? false : true
450 450
451 451 text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text, :object => obj, :attribute => attr) { |macro, args| exec_macro(macro, obj, args) }
452 452
453 453 parse_non_pre_blocks(text) do |text|
454 [:parse_inline_attachments, :parse_wiki_links, :parse_redmine_links].each do |method_name|
454 [:parse_inline_attachments, :parse_wiki_links, :parse_redmine_links, :parse_headings].each do |method_name|
455 455 send method_name, text, project, obj, attr, only_path, options
456 456 end
457 457 end
458 458 end
459 459
460 460 def parse_non_pre_blocks(text)
461 461 s = StringScanner.new(text)
462 462 tags = []
463 463 parsed = ''
464 464 while !s.eos?
465 465 s.scan(/(.*?)(<(\/)?(pre|code)(.*?)>|\z)/im)
466 466 text, full_tag, closing, tag = s[1], s[2], s[3], s[4]
467 467 if tags.empty?
468 468 yield text
469 469 end
470 470 parsed << text
471 471 if tag
472 472 if closing
473 473 if tags.last == tag.downcase
474 474 tags.pop
475 475 end
476 476 else
477 477 tags << tag.downcase
478 478 end
479 479 parsed << full_tag
480 480 end
481 481 end
482 482 # Close any non closing tags
483 483 while tag = tags.pop
484 484 parsed << "</#{tag}>"
485 485 end
486 486 parsed
487 487 end
488 488
489 489 def parse_inline_attachments(text, project, obj, attr, only_path, options)
490 490 # when using an image link, try to use an attachment, if possible
491 491 if options[:attachments] || (obj && obj.respond_to?(:attachments))
492 492 attachments = nil
493 493 text.gsub!(/src="([^\/"]+\.(bmp|gif|jpg|jpeg|png))"(\s+alt="([^"]*)")?/i) do |m|
494 494 filename, ext, alt, alttext = $1.downcase, $2, $3, $4
495 495 attachments ||= (options[:attachments] || obj.attachments).sort_by(&:created_on).reverse
496 496 # search for the picture in attachments
497 497 if found = attachments.detect { |att| att.filename.downcase == filename }
498 498 image_url = url_for :only_path => only_path, :controller => 'attachments', :action => 'download', :id => found
499 499 desc = found.description.to_s.gsub('"', '')
500 500 if !desc.blank? && alttext.blank?
501 501 alt = " title=\"#{desc}\" alt=\"#{desc}\""
502 502 end
503 503 "src=\"#{image_url}\"#{alt}"
504 504 else
505 505 m
506 506 end
507 507 end
508 508 end
509 509 end
510 510
511 511 # Wiki links
512 512 #
513 513 # Examples:
514 514 # [[mypage]]
515 515 # [[mypage|mytext]]
516 516 # wiki links can refer other project wikis, using project name or identifier:
517 517 # [[project:]] -> wiki starting page
518 518 # [[project:|mytext]]
519 519 # [[project:mypage]]
520 520 # [[project:mypage|mytext]]
521 521 def parse_wiki_links(text, project, obj, attr, only_path, options)
522 522 text.gsub!(/(!)?(\[\[([^\]\n\|]+)(\|([^\]\n\|]+))?\]\])/) do |m|
523 523 link_project = project
524 524 esc, all, page, title = $1, $2, $3, $5
525 525 if esc.nil?
526 526 if page =~ /^([^\:]+)\:(.*)$/
527 527 link_project = Project.find_by_name($1) || Project.find_by_identifier($1)
528 528 page = $2
529 529 title ||= $1 if page.blank?
530 530 end
531 531
532 532 if link_project && link_project.wiki
533 533 # extract anchor
534 534 anchor = nil
535 535 if page =~ /^(.+?)\#(.+)$/
536 536 page, anchor = $1, $2
537 537 end
538 538 # check if page exists
539 539 wiki_page = link_project.wiki.find_page(page)
540 540 url = case options[:wiki_links]
541 541 when :local; "#{title}.html"
542 542 when :anchor; "##{title}" # used for single-file wiki export
543 543 else
544 544 wiki_page_id = page.present? ? Wiki.titleize(page) : nil
545 545 url_for(:only_path => only_path, :controller => 'wiki', :action => 'show', :project_id => link_project, :id => wiki_page_id, :anchor => anchor)
546 546 end
547 547 link_to((title || page), url, :class => ('wiki-page' + (wiki_page ? '' : ' new')))
548 548 else
549 549 # project or wiki doesn't exist
550 550 all
551 551 end
552 552 else
553 553 all
554 554 end
555 555 end
556 556 end
557 557
558 558 # Redmine links
559 559 #
560 560 # Examples:
561 561 # Issues:
562 562 # #52 -> Link to issue #52
563 563 # Changesets:
564 564 # r52 -> Link to revision 52
565 565 # commit:a85130f -> Link to scmid starting with a85130f
566 566 # Documents:
567 567 # document#17 -> Link to document with id 17
568 568 # document:Greetings -> Link to the document with title "Greetings"
569 569 # document:"Some document" -> Link to the document with title "Some document"
570 570 # Versions:
571 571 # version#3 -> Link to version with id 3
572 572 # version:1.0.0 -> Link to version named "1.0.0"
573 573 # version:"1.0 beta 2" -> Link to version named "1.0 beta 2"
574 574 # Attachments:
575 575 # attachment:file.zip -> Link to the attachment of the current object named file.zip
576 576 # Source files:
577 577 # source:some/file -> Link to the file located at /some/file in the project's repository
578 578 # source:some/file@52 -> Link to the file's revision 52
579 579 # source:some/file#L120 -> Link to line 120 of the file
580 580 # source:some/file@52#L120 -> Link to line 120 of the file's revision 52
581 581 # export:some/file -> Force the download of the file
582 582 # Forum messages:
583 583 # message#1218 -> Link to message with id 1218
584 584 def parse_redmine_links(text, project, obj, attr, only_path, options)
585 585 text.gsub!(%r{([\s\(,\-\[\>]|^)(!)?(attachment|document|version|commit|source|export|message|project)?((#|r)(\d+)|(:)([^"\s<>][^\s<>]*?|"[^"]+?"))(?=(?=[[:punct:]]\W)|,|\s|\]|<|$)}) do |m|
586 586 leading, esc, prefix, sep, identifier = $1, $2, $3, $5 || $7, $6 || $8
587 587 link = nil
588 588 if esc.nil?
589 589 if prefix.nil? && sep == 'r'
590 590 if project && (changeset = project.changesets.find_by_revision(identifier))
591 591 link = link_to("r#{identifier}", {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :rev => changeset.revision},
592 592 :class => 'changeset',
593 593 :title => truncate_single_line(changeset.comments, :length => 100))
594 594 end
595 595 elsif sep == '#'
596 596 oid = identifier.to_i
597 597 case prefix
598 598 when nil
599 599 if issue = Issue.visible.find_by_id(oid, :include => :status)
600 600 link = link_to("##{oid}", {:only_path => only_path, :controller => 'issues', :action => 'show', :id => oid},
601 601 :class => issue.css_classes,
602 602 :title => "#{truncate(issue.subject, :length => 100)} (#{issue.status.name})")
603 603 end
604 604 when 'document'
605 605 if document = Document.find_by_id(oid, :include => [:project], :conditions => Project.visible_by(User.current))
606 606 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
607 607 :class => 'document'
608 608 end
609 609 when 'version'
610 610 if version = Version.find_by_id(oid, :include => [:project], :conditions => Project.visible_by(User.current))
611 611 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
612 612 :class => 'version'
613 613 end
614 614 when 'message'
615 615 if message = Message.find_by_id(oid, :include => [:parent, {:board => :project}], :conditions => Project.visible_by(User.current))
616 616 link = link_to h(truncate(message.subject, :length => 60)), {:only_path => only_path,
617 617 :controller => 'messages',
618 618 :action => 'show',
619 619 :board_id => message.board,
620 620 :id => message.root,
621 621 :anchor => (message.parent ? "message-#{message.id}" : nil)},
622 622 :class => 'message'
623 623 end
624 624 when 'project'
625 625 if p = Project.visible.find_by_id(oid)
626 626 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
627 627 end
628 628 end
629 629 elsif sep == ':'
630 630 # removes the double quotes if any
631 631 name = identifier.gsub(%r{^"(.*)"$}, "\\1")
632 632 case prefix
633 633 when 'document'
634 634 if project && document = project.documents.find_by_title(name)
635 635 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
636 636 :class => 'document'
637 637 end
638 638 when 'version'
639 639 if project && version = project.versions.find_by_name(name)
640 640 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
641 641 :class => 'version'
642 642 end
643 643 when 'commit'
644 644 if project && (changeset = project.changesets.find(:first, :conditions => ["scmid LIKE ?", "#{name}%"]))
645 645 link = link_to h("#{name}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :rev => changeset.revision},
646 646 :class => 'changeset',
647 647 :title => truncate_single_line(changeset.comments, :length => 100)
648 648 end
649 649 when 'source', 'export'
650 650 if project && project.repository
651 651 name =~ %r{^[/\\]*(.*?)(@([0-9a-f]+))?(#(L\d+))?$}
652 652 path, rev, anchor = $1, $3, $5
653 653 link = link_to h("#{prefix}:#{name}"), {:controller => 'repositories', :action => 'entry', :id => project,
654 654 :path => to_path_param(path),
655 655 :rev => rev,
656 656 :anchor => anchor,
657 657 :format => (prefix == 'export' ? 'raw' : nil)},
658 658 :class => (prefix == 'export' ? 'source download' : 'source')
659 659 end
660 660 when 'attachment'
661 661 attachments = options[:attachments] || (obj && obj.respond_to?(:attachments) ? obj.attachments : nil)
662 662 if attachments && attachment = attachments.detect {|a| a.filename == name }
663 663 link = link_to h(attachment.filename), {:only_path => only_path, :controller => 'attachments', :action => 'download', :id => attachment},
664 664 :class => 'attachment'
665 665 end
666 666 when 'project'
667 667 if p = Project.visible.find(:first, :conditions => ["identifier = :s OR LOWER(name) = :s", {:s => name.downcase}])
668 668 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
669 669 end
670 670 end
671 671 end
672 672 end
673 673 leading + (link || "#{prefix}#{sep}#{identifier}")
674 674 end
675 675 end
676
677 TOC_RE = /<p>\{\{([<>]?)toc\}\}<\/p>/i unless const_defined?(:TOC_RE)
678 HEADING_RE = /<h(1|2|3)( [^>]+)?>(.+?)<\/h(1|2|3)>/i unless const_defined?(:HEADING_RE)
679
680 # Headings and TOC
681 # Adds ids and links to headings and renders the TOC if needed unless options[:headings] is set to false
682 def parse_headings(text, project, obj, attr, only_path, options)
683 headings = []
684 text.gsub!(HEADING_RE) do
685 level, attrs, content = $1, $2, $3
686 item = strip_tags(content).strip
687 anchor = item.gsub(%r{[^\w\s\-]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
688 headings << [level, anchor, item]
689 "<h#{level} #{attrs} id=\"#{anchor}\">#{content}<a href=\"##{anchor}\" class=\"wiki-anchor\">&para;</a></h#{level}>"
690 end unless options[:headings] == false
691
692 text.gsub!(TOC_RE) do
693 if headings.empty?
694 ''
695 else
696 div_class = 'toc'
697 div_class << ' right' if $1 == '>'
698 div_class << ' left' if $1 == '<'
699 out = "<ul class=\"#{div_class}\">"
700 headings.each do |level, anchor, item|
701 out << "<li class=\"heading#{level}\"><a href=\"##{anchor}\">#{item}</a></li>\n"
702 end
703 out << '</ul>'
704 out
705 end
706 end
707 end
676 708
677 709 # Same as Rails' simple_format helper without using paragraphs
678 710 def simple_format_without_paragraph(text)
679 711 text.to_s.
680 712 gsub(/\r\n?/, "\n"). # \r\n and \r -> \n
681 713 gsub(/\n\n+/, "<br /><br />"). # 2+ newline -> 2 br
682 714 gsub(/([^\n]\n)(?=[^\n])/, '\1<br />') # 1 newline -> br
683 715 end
684 716
685 717 def lang_options_for_select(blank=true)
686 718 (blank ? [["(auto)", ""]] : []) +
687 719 valid_languages.collect{|lang| [ ll(lang.to_s, :general_lang_name), lang.to_s]}.sort{|x,y| x.last <=> y.last }
688 720 end
689 721
690 722 def label_tag_for(name, option_tags = nil, options = {})
691 723 label_text = l(("field_"+field.to_s.gsub(/\_id$/, "")).to_sym) + (options.delete(:required) ? @template.content_tag("span", " *", :class => "required"): "")
692 724 content_tag("label", label_text)
693 725 end
694 726
695 727 def labelled_tabular_form_for(name, object, options, &proc)
696 728 options[:html] ||= {}
697 729 options[:html][:class] = 'tabular' unless options[:html].has_key?(:class)
698 730 form_for(name, object, options.merge({ :builder => TabularFormBuilder, :lang => current_language}), &proc)
699 731 end
700 732
701 733 def back_url_hidden_field_tag
702 734 back_url = params[:back_url] || request.env['HTTP_REFERER']
703 735 back_url = CGI.unescape(back_url.to_s)
704 736 hidden_field_tag('back_url', CGI.escape(back_url)) unless back_url.blank?
705 737 end
706 738
707 739 def check_all_links(form_name)
708 740 link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") +
709 741 " | " +
710 742 link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)")
711 743 end
712 744
713 745 def progress_bar(pcts, options={})
714 746 pcts = [pcts, pcts] unless pcts.is_a?(Array)
715 747 pcts = pcts.collect(&:round)
716 748 pcts[1] = pcts[1] - pcts[0]
717 749 pcts << (100 - pcts[1] - pcts[0])
718 750 width = options[:width] || '100px;'
719 751 legend = options[:legend] || ''
720 752 content_tag('table',
721 753 content_tag('tr',
722 754 (pcts[0] > 0 ? content_tag('td', '', :style => "width: #{pcts[0]}%;", :class => 'closed') : '') +
723 755 (pcts[1] > 0 ? content_tag('td', '', :style => "width: #{pcts[1]}%;", :class => 'done') : '') +
724 756 (pcts[2] > 0 ? content_tag('td', '', :style => "width: #{pcts[2]}%;", :class => 'todo') : '')
725 757 ), :class => 'progress', :style => "width: #{width};") +
726 758 content_tag('p', legend, :class => 'pourcent')
727 759 end
728 760
729 761 def checked_image(checked=true)
730 762 if checked
731 763 image_tag 'toggle_check.png'
732 764 end
733 765 end
734 766
735 767 def context_menu(url)
736 768 unless @context_menu_included
737 769 content_for :header_tags do
738 770 javascript_include_tag('context_menu') +
739 771 stylesheet_link_tag('context_menu')
740 772 end
741 773 if l(:direction) == 'rtl'
742 774 content_for :header_tags do
743 775 stylesheet_link_tag('context_menu_rtl')
744 776 end
745 777 end
746 778 @context_menu_included = true
747 779 end
748 780 javascript_tag "new ContextMenu('#{ url_for(url) }')"
749 781 end
750 782
751 783 def context_menu_link(name, url, options={})
752 784 options[:class] ||= ''
753 785 if options.delete(:selected)
754 786 options[:class] << ' icon-checked disabled'
755 787 options[:disabled] = true
756 788 end
757 789 if options.delete(:disabled)
758 790 options.delete(:method)
759 791 options.delete(:confirm)
760 792 options.delete(:onclick)
761 793 options[:class] << ' disabled'
762 794 url = '#'
763 795 end
764 796 link_to name, url, options
765 797 end
766 798
767 799 def calendar_for(field_id)
768 800 include_calendar_headers_tags
769 801 image_tag("calendar.png", {:id => "#{field_id}_trigger",:class => "calendar-trigger"}) +
770 802 javascript_tag("Calendar.setup({inputField : '#{field_id}', ifFormat : '%Y-%m-%d', button : '#{field_id}_trigger' });")
771 803 end
772 804
773 805 def include_calendar_headers_tags
774 806 unless @calendar_headers_tags_included
775 807 @calendar_headers_tags_included = true
776 808 content_for :header_tags do
777 809 start_of_week = case Setting.start_of_week.to_i
778 810 when 1
779 811 'Calendar._FD = 1;' # Monday
780 812 when 7
781 813 'Calendar._FD = 0;' # Sunday
782 814 else
783 815 '' # use language
784 816 end
785 817
786 818 javascript_include_tag('calendar/calendar') +
787 819 javascript_include_tag("calendar/lang/calendar-#{current_language.to_s.downcase}.js") +
788 820 javascript_tag(start_of_week) +
789 821 javascript_include_tag('calendar/calendar-setup') +
790 822 stylesheet_link_tag('calendar')
791 823 end
792 824 end
793 825 end
794 826
795 827 def content_for(name, content = nil, &block)
796 828 @has_content ||= {}
797 829 @has_content[name] = true
798 830 super(name, content, &block)
799 831 end
800 832
801 833 def has_content?(name)
802 834 (@has_content && @has_content[name]) || false
803 835 end
804 836
805 837 # Returns the avatar image tag for the given +user+ if avatars are enabled
806 838 # +user+ can be a User or a string that will be scanned for an email address (eg. 'joe <joe@foo.bar>')
807 839 def avatar(user, options = { })
808 840 if Setting.gravatar_enabled?
809 841 options.merge!({:ssl => (defined?(request) && request.ssl?), :default => Setting.gravatar_default})
810 842 email = nil
811 843 if user.respond_to?(:mail)
812 844 email = user.mail
813 845 elsif user.to_s =~ %r{<(.+?)>}
814 846 email = $1
815 847 end
816 848 return gravatar(email.to_s.downcase, options) unless email.blank? rescue nil
817 849 else
818 850 ''
819 851 end
820 852 end
821 853
822 854 def favicon
823 855 "<link rel='shortcut icon' href='#{image_path('/favicon.ico')}' />"
824 856 end
825 857
826 858 private
827 859
828 860 def wiki_helper
829 861 helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
830 862 extend helper
831 863 return self
832 864 end
833 865
834 866 def link_to_remote_content_update(text, url_params)
835 867 link_to_remote(text,
836 868 {:url => url_params, :method => :get, :update => 'content', :complete => 'window.scrollTo(0,0)'},
837 869 {:href => url_for(:params => url_params)}
838 870 )
839 871 end
840 872
841 873 end
@@ -1,121 +1,121
1 1 # redMine - project management software
2 2 # Copyright (C) 2006-2007 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 module Redmine
19 19 module WikiFormatting
20 20 module Macros
21 21 module Definitions
22 22 def exec_macro(name, obj, args)
23 23 method_name = "macro_#{name}"
24 24 send(method_name, obj, args) if respond_to?(method_name)
25 25 end
26 26
27 27 def extract_macro_options(args, *keys)
28 28 options = {}
29 29 while args.last.to_s.strip =~ %r{^(.+)\=(.+)$} && keys.include?($1.downcase.to_sym)
30 30 options[$1.downcase.to_sym] = $2
31 31 args.pop
32 32 end
33 33 return [args, options]
34 34 end
35 35 end
36 36
37 37 @@available_macros = {}
38 38
39 39 class << self
40 40 # Called with a block to define additional macros.
41 41 # Macro blocks accept 2 arguments:
42 42 # * obj: the object that is rendered
43 43 # * args: macro arguments
44 44 #
45 45 # Plugins can use this method to define new macros:
46 46 #
47 47 # Redmine::WikiFormatting::Macros.register do
48 48 # desc "This is my macro"
49 49 # macro :my_macro do |obj, args|
50 50 # "My macro output"
51 51 # end
52 52 # end
53 53 def register(&block)
54 54 class_eval(&block) if block_given?
55 55 end
56 56
57 57 private
58 58 # Defines a new macro with the given name and block.
59 59 def macro(name, &block)
60 60 name = name.to_sym if name.is_a?(String)
61 61 @@available_macros[name] = @@desc || ''
62 62 @@desc = nil
63 63 raise "Can not create a macro without a block!" unless block_given?
64 64 Definitions.send :define_method, "macro_#{name}".downcase, &block
65 65 end
66 66
67 67 # Sets description for the next macro to be defined
68 68 def desc(txt)
69 69 @@desc = txt
70 70 end
71 71 end
72 72
73 73 # Builtin macros
74 74 desc "Sample macro."
75 75 macro :hello_world do |obj, args|
76 76 "Hello world! Object: #{obj.class.name}, " + (args.empty? ? "Called with no argument." : "Arguments: #{args.join(', ')}")
77 77 end
78 78
79 79 desc "Displays a list of all available macros, including description if available."
80 80 macro :macro_list do
81 81 out = ''
82 82 @@available_macros.keys.collect(&:to_s).sort.each do |macro|
83 83 out << content_tag('dt', content_tag('code', macro))
84 84 out << content_tag('dd', textilizable(@@available_macros[macro.to_sym]))
85 85 end
86 86 content_tag('dl', out)
87 87 end
88 88
89 89 desc "Displays a list of child pages. With no argument, it displays the child pages of the current wiki page. Examples:\n\n" +
90 90 " !{{child_pages}} -- can be used from a wiki page only\n" +
91 91 " !{{child_pages(Foo)}} -- lists all children of page Foo\n" +
92 92 " !{{child_pages(Foo, parent=1)}} -- same as above with a link to page Foo"
93 93 macro :child_pages do |obj, args|
94 94 args, options = extract_macro_options(args, :parent)
95 95 page = nil
96 96 if args.size > 0
97 97 page = Wiki.find_page(args.first.to_s, :project => @project)
98 98 elsif obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version)
99 99 page = obj.page
100 100 else
101 101 raise 'With no argument, this macro can be called from wiki pages only.'
102 102 end
103 103 raise 'Page not found' if page.nil? || !User.current.allowed_to?(:view_wiki_pages, page.wiki.project)
104 104 pages = ([page] + page.descendants).group_by(&:parent_id)
105 105 render_page_hierarchy(pages, options[:parent] ? page.parent_id : page.id)
106 106 end
107 107
108 108 desc "Include a wiki page. Example:\n\n !{{include(Foo)}}\n\nor to include a page of a specific project wiki:\n\n !{{include(projectname:Foo)}}"
109 109 macro :include do |obj, args|
110 110 page = Wiki.find_page(args.first.to_s, :project => @project)
111 111 raise 'Page not found' if page.nil? || !User.current.allowed_to?(:view_wiki_pages, page.wiki.project)
112 112 @included_wiki_pages ||= []
113 113 raise 'Circular inclusion detected' if @included_wiki_pages.include?(page.title)
114 114 @included_wiki_pages << page.title
115 out = textilizable(page.content, :text, :attachments => page.attachments)
115 out = textilizable(page.content, :text, :attachments => page.attachments, :headings => false)
116 116 @included_wiki_pages.pop
117 117 out
118 118 end
119 119 end
120 120 end
121 121 end
@@ -1,163 +1,118
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2008 Jean-Philippe Lang
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 'redcloth3'
19 19
20 20 module Redmine
21 21 module WikiFormatting
22 22 module Textile
23 23 class Formatter < RedCloth3
24 24 include ActionView::Helpers::TagHelper
25 25
26 26 # auto_link rule after textile rules so that it doesn't break !image_url! tags
27 RULES = [:textile, :block_markdown_rule, :inline_auto_link, :inline_auto_mailto, :inline_toc]
27 RULES = [:textile, :block_markdown_rule, :inline_auto_link, :inline_auto_mailto]
28 28
29 29 def initialize(*args)
30 30 super
31 31 self.hard_breaks=true
32 32 self.no_span_caps=true
33 33 self.filter_styles=true
34 34 end
35 35
36 36 def to_html(*rules)
37 37 @toc = []
38 38 super(*RULES).to_s
39 39 end
40 40
41 41 private
42 42
43 43 # Patch for RedCloth. Fixed in RedCloth r128 but _why hasn't released it yet.
44 44 # <a href="http://code.whytheluckystiff.net/redcloth/changeset/128">http://code.whytheluckystiff.net/redcloth/changeset/128</a>
45 45 def hard_break( text )
46 46 text.gsub!( /(.)\n(?!\n|\Z|>| *([#*=]+(\s|$)|[{|]))/, "\\1<br />" ) if hard_breaks
47 47 end
48 48
49 49 # Patch to add code highlighting support to RedCloth
50 50 def smooth_offtags( text )
51 51 unless @pre_list.empty?
52 52 ## replace <pre> content
53 53 text.gsub!(/<redpre#(\d+)>/) do
54 54 content = @pre_list[$1.to_i]
55 55 if content.match(/<code\s+class="(\w+)">\s?(.+)/m)
56 56 content = "<code class=\"#{$1} syntaxhl\">" +
57 57 Redmine::SyntaxHighlighting.highlight_by_language($2, $1)
58 58 end
59 59 content
60 60 end
61 61 end
62 62 end
63 63
64 # Patch to add 'table of content' support to RedCloth
65 def textile_p_withtoc(tag, atts, cite, content)
66 # removes wiki links from the item
67 toc_item = content.gsub(/(\[\[([^\]\|]*)(\|([^\]]*))?\]\])/) { $4 || $2 }
68 # sanitizes titles from links
69 # see redcloth3.rb, same as "#{pre}#{text}#{post}"
70 toc_item.gsub!(LINK_RE) { [$2, $4, $9].join }
71 # sanitizes image links from titles
72 toc_item.gsub!(IMAGE_RE) { [$5].join }
73 # removes styles
74 # eg. %{color:red}Triggers% => Triggers
75 toc_item.gsub! %r[%\{[^\}]*\}([^%]+)%], '\\1'
76
77 # replaces non word caracters by dashes
78 anchor = toc_item.gsub(%r{[^\w\s\-]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
79
80 unless anchor.blank?
81 if tag =~ /^h(\d)$/
82 @toc << [$1.to_i, anchor, toc_item]
83 end
84 atts << " id=\"#{anchor}\""
85 content = content + "<a href=\"##{anchor}\" class=\"wiki-anchor\">&para;</a>"
86 end
87 textile_p(tag, atts, cite, content)
88 end
89
90 alias :textile_h1 :textile_p_withtoc
91 alias :textile_h2 :textile_p_withtoc
92 alias :textile_h3 :textile_p_withtoc
93
94 def inline_toc(text)
95 text.gsub!(/<p>\{\{([<>]?)toc\}\}<\/p>/i) do
96 div_class = 'toc'
97 div_class << ' right' if $1 == '>'
98 div_class << ' left' if $1 == '<'
99 out = "<ul class=\"#{div_class}\">"
100 @toc.each do |heading|
101 level, anchor, toc_item = heading
102 out << "<li class=\"heading#{level}\"><a href=\"##{anchor}\">#{toc_item}</a></li>\n"
103 end
104 out << '</ul>'
105 out
106 end
107 end
108
109 64 AUTO_LINK_RE = %r{
110 65 ( # leading text
111 66 <\w+.*?>| # leading HTML tag, or
112 67 [^=<>!:'"/]| # leading punctuation, or
113 68 ^ # beginning of line
114 69 )
115 70 (
116 71 (?:https?://)| # protocol spec, or
117 72 (?:s?ftps?://)|
118 73 (?:www\.) # www.*
119 74 )
120 75 (
121 76 (\S+?) # url
122 77 (\/)? # slash
123 78 )
124 79 ((?:&gt;)?|[^\w\=\/;\(\)]*?) # post
125 80 (?=<|\s|$)
126 81 }x unless const_defined?(:AUTO_LINK_RE)
127 82
128 83 # Turns all urls into clickable links (code from Rails).
129 84 def inline_auto_link(text)
130 85 text.gsub!(AUTO_LINK_RE) do
131 86 all, leading, proto, url, post = $&, $1, $2, $3, $6
132 87 if leading =~ /<a\s/i || leading =~ /![<>=]?/
133 88 # don't replace URL's that are already linked
134 89 # and URL's prefixed with ! !> !< != (textile images)
135 90 all
136 91 else
137 92 # Idea below : an URL with unbalanced parethesis and
138 93 # ending by ')' is put into external parenthesis
139 94 if ( url[-1]==?) and ((url.count("(") - url.count(")")) < 0 ) )
140 95 url=url[0..-2] # discard closing parenth from url
141 96 post = ")"+post # add closing parenth to post
142 97 end
143 98 tag = content_tag('a', proto + url, :href => "#{proto=="www."?"http://www.":proto}#{url}", :class => 'external')
144 99 %(#{leading}#{tag}#{post})
145 100 end
146 101 end
147 102 end
148 103
149 104 # Turns all email addresses into clickable links (code from Rails).
150 105 def inline_auto_mailto(text)
151 106 text.gsub!(/([\w\.!#\$%\-+.]+@[A-Za-z0-9\-]+(\.[A-Za-z0-9\-]+)+)/) do
152 107 mail = $1
153 108 if text.match(/<a\b[^>]*>(.*)(#{Regexp.escape(mail)})(.*)<\/a>/)
154 109 mail
155 110 else
156 111 content_tag('a', mail, :href => "mailto:#{mail}", :class => "email")
157 112 end
158 113 end
159 114 end
160 115 end
161 116 end
162 117 end
163 118 end
@@ -1,617 +1,639
1 1 # Redmine - project management software
2 # Copyright (C) 2006-2009 Jean-Philippe Lang
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.dirname(__FILE__) + '/../../test_helper'
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'), :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_attachment_links
229 229 attachment_link = link_to('error281.txt', {:controller => 'attachments', :action => 'download', :id => '1'}, :class => 'attachment')
230 230 to_test = {
231 231 'attachment:error281.txt' => attachment_link
232 232 }
233 233 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text, :attachments => Issue.find(3).attachments), "#{text} failed" }
234 234 end
235 235
236 236 def test_wiki_links
237 237 to_test = {
238 238 '[[CookBook documentation]]' => '<a href="/projects/ecookbook/wiki/CookBook_documentation" class="wiki-page">CookBook documentation</a>',
239 239 '[[Another page|Page]]' => '<a href="/projects/ecookbook/wiki/Another_page" class="wiki-page">Page</a>',
240 240 # link with anchor
241 241 '[[CookBook documentation#One-section]]' => '<a href="/projects/ecookbook/wiki/CookBook_documentation#One-section" class="wiki-page">CookBook documentation</a>',
242 242 '[[Another page#anchor|Page]]' => '<a href="/projects/ecookbook/wiki/Another_page#anchor" class="wiki-page">Page</a>',
243 243 # page that doesn't exist
244 244 '[[Unknown page]]' => '<a href="/projects/ecookbook/wiki/Unknown_page" class="wiki-page new">Unknown page</a>',
245 245 '[[Unknown page|404]]' => '<a href="/projects/ecookbook/wiki/Unknown_page" class="wiki-page new">404</a>',
246 246 # link to another project wiki
247 247 '[[onlinestore:]]' => '<a href="/projects/onlinestore/wiki" class="wiki-page">onlinestore</a>',
248 248 '[[onlinestore:|Wiki]]' => '<a href="/projects/onlinestore/wiki" class="wiki-page">Wiki</a>',
249 249 '[[onlinestore:Start page]]' => '<a href="/projects/onlinestore/wiki/Start_page" class="wiki-page">Start page</a>',
250 250 '[[onlinestore:Start page|Text]]' => '<a href="/projects/onlinestore/wiki/Start_page" class="wiki-page">Text</a>',
251 251 '[[onlinestore:Unknown page]]' => '<a href="/projects/onlinestore/wiki/Unknown_page" class="wiki-page new">Unknown page</a>',
252 252 # striked through link
253 253 '-[[Another page|Page]]-' => '<del><a href="/projects/ecookbook/wiki/Another_page" class="wiki-page">Page</a></del>',
254 254 '-[[Another page|Page]] link-' => '<del><a href="/projects/ecookbook/wiki/Another_page" class="wiki-page">Page</a> link</del>',
255 255 # escaping
256 256 '![[Another page|Page]]' => '[[Another page|Page]]',
257 257 # project does not exist
258 258 '[[unknowproject:Start]]' => '[[unknowproject:Start]]',
259 259 '[[unknowproject:Start|Page title]]' => '[[unknowproject:Start|Page title]]',
260 260 }
261 261 @project = Project.find(1)
262 262 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
263 263 end
264 264
265 265 def test_html_tags
266 266 to_test = {
267 267 "<div>content</div>" => "<p>&lt;div&gt;content&lt;/div&gt;</p>",
268 268 "<div class=\"bold\">content</div>" => "<p>&lt;div class=\"bold\"&gt;content&lt;/div&gt;</p>",
269 269 "<script>some script;</script>" => "<p>&lt;script&gt;some script;&lt;/script&gt;</p>",
270 270 # do not escape pre/code tags
271 271 "<pre>\nline 1\nline2</pre>" => "<pre>\nline 1\nline2</pre>",
272 272 "<pre><code>\nline 1\nline2</code></pre>" => "<pre><code>\nline 1\nline2</code></pre>",
273 273 "<pre><div>content</div></pre>" => "<pre>&lt;div&gt;content&lt;/div&gt;</pre>",
274 274 "HTML comment: <!-- no comments -->" => "<p>HTML comment: &lt;!-- no comments --&gt;</p>",
275 275 "<!-- opening comment" => "<p>&lt;!-- opening comment</p>",
276 276 # remove attributes except class
277 277 "<pre class='foo'>some text</pre>" => "<pre class='foo'>some text</pre>",
278 278 "<pre onmouseover='alert(1)'>some text</pre>" => "<pre>some text</pre>",
279 279 }
280 280 to_test.each { |text, result| assert_equal result, textilizable(text) }
281 281 end
282 282
283 283 def test_allowed_html_tags
284 284 to_test = {
285 285 "<pre>preformatted text</pre>" => "<pre>preformatted text</pre>",
286 286 "<notextile>no *textile* formatting</notextile>" => "no *textile* formatting",
287 287 "<notextile>this is <tag>a tag</tag></notextile>" => "this is &lt;tag&gt;a tag&lt;/tag&gt;"
288 288 }
289 289 to_test.each { |text, result| assert_equal result, textilizable(text) }
290 290 end
291 291
292 292 def test_pre_tags
293 293 raw = <<-RAW
294 294 Before
295 295
296 296 <pre>
297 297 <prepared-statement-cache-size>32</prepared-statement-cache-size>
298 298 </pre>
299 299
300 300 After
301 301 RAW
302 302
303 303 expected = <<-EXPECTED
304 304 <p>Before</p>
305 305 <pre>
306 306 &lt;prepared-statement-cache-size&gt;32&lt;/prepared-statement-cache-size&gt;
307 307 </pre>
308 308 <p>After</p>
309 309 EXPECTED
310 310
311 311 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
312 312 end
313 313
314 314 def test_pre_content_should_not_parse_wiki_and_redmine_links
315 315 raw = <<-RAW
316 316 [[CookBook documentation]]
317 317
318 318 #1
319 319
320 320 <pre>
321 321 [[CookBook documentation]]
322 322
323 323 #1
324 324 </pre>
325 325 RAW
326 326
327 327 expected = <<-EXPECTED
328 328 <p><a href="/projects/ecookbook/wiki/CookBook_documentation" class="wiki-page">CookBook documentation</a></p>
329 329 <p><a href="/issues/1" class="issue status-1 priority-1" title="Can't print recipes (New)">#1</a></p>
330 330 <pre>
331 331 [[CookBook documentation]]
332 332
333 333 #1
334 334 </pre>
335 335 EXPECTED
336 336
337 337 @project = Project.find(1)
338 338 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
339 339 end
340 340
341 341 def test_non_closing_pre_blocks_should_be_closed
342 342 raw = <<-RAW
343 343 <pre><code>
344 344 RAW
345 345
346 346 expected = <<-EXPECTED
347 347 <pre><code>
348 348 </code></pre>
349 349 EXPECTED
350 350
351 351 @project = Project.find(1)
352 352 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
353 353 end
354 354
355 355 def test_syntax_highlight
356 356 raw = <<-RAW
357 357 <pre><code class="ruby">
358 358 # Some ruby code here
359 359 </code></pre>
360 360 RAW
361 361
362 362 expected = <<-EXPECTED
363 363 <pre><code class="ruby syntaxhl"><span class=\"CodeRay\"><span class="no">1</span> <span class="c"># Some ruby code here</span></span>
364 364 </code></pre>
365 365 EXPECTED
366 366
367 367 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
368 368 end
369 369
370 370 def test_wiki_links_in_tables
371 371 to_test = {"|[[Page|Link title]]|[[Other Page|Other title]]|\n|Cell 21|[[Last page]]|" =>
372 372 '<tr><td><a href="/projects/ecookbook/wiki/Page" class="wiki-page new">Link title</a></td>' +
373 373 '<td><a href="/projects/ecookbook/wiki/Other_Page" class="wiki-page new">Other title</a></td>' +
374 374 '</tr><tr><td>Cell 21</td><td><a href="/projects/ecookbook/wiki/Last_page" class="wiki-page new">Last page</a></td></tr>'
375 375 }
376 376 @project = Project.find(1)
377 377 to_test.each { |text, result| assert_equal "<table>#{result}</table>", textilizable(text).gsub(/[\t\n]/, '') }
378 378 end
379 379
380 380 def test_text_formatting
381 381 to_test = {'*_+bold, italic and underline+_*' => '<strong><em><ins>bold, italic and underline</ins></em></strong>',
382 382 '(_text within parentheses_)' => '(<em>text within parentheses</em>)',
383 383 'a *Humane Web* Text Generator' => 'a <strong>Humane Web</strong> Text Generator',
384 384 '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>',
385 385 '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',
386 386 }
387 387 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
388 388 end
389 389
390 390 def test_wiki_horizontal_rule
391 391 assert_equal '<hr />', textilizable('---')
392 392 assert_equal '<p>Dashes: ---</p>', textilizable('Dashes: ---')
393 393 end
394 394
395 395 def test_footnotes
396 396 raw = <<-RAW
397 397 This is some text[1].
398 398
399 399 fn1. This is the foot note
400 400 RAW
401 401
402 402 expected = <<-EXPECTED
403 403 <p>This is some text<sup><a href=\"#fn1\">1</a></sup>.</p>
404 404 <p id="fn1" class="footnote"><sup>1</sup> This is the foot note</p>
405 405 EXPECTED
406 406
407 407 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
408 408 end
409 409
410 410 def test_table_of_content
411 411 raw = <<-RAW
412 412 {{toc}}
413 413
414 414 h1. Title
415 415
416 416 Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas sed libero.
417 417
418 418 h2. Subtitle with a [[Wiki]] link
419 419
420 420 Nullam commodo metus accumsan nulla. Curabitur lobortis dui id dolor.
421 421
422 422 h2. Subtitle with [[Wiki|another Wiki]] link
423 423
424 424 h2. Subtitle with %{color:red}red text%
425
426 h2. Subtitle with *some* _modifiers_
425 427
426 428 h1. Another title
427 429
428 430 h2. An "Internet link":http://www.redmine.org/ inside subtitle
429 431
430 432 h2. "Project Name !/attachments/1234/logo_small.gif! !/attachments/5678/logo_2.png!":/projects/projectname/issues
431 433
432 434 RAW
433 435
434 436 expected = '<ul class="toc">' +
435 437 '<li class="heading1"><a href="#Title">Title</a></li>' +
436 438 '<li class="heading2"><a href="#Subtitle-with-a-Wiki-link">Subtitle with a Wiki link</a></li>' +
437 439 '<li class="heading2"><a href="#Subtitle-with-another-Wiki-link">Subtitle with another Wiki link</a></li>' +
438 440 '<li class="heading2"><a href="#Subtitle-with-red-text">Subtitle with red text</a></li>' +
441 '<li class="heading2"><a href="#Subtitle-with-some-modifiers">Subtitle with some modifiers</a></li>' +
439 442 '<li class="heading1"><a href="#Another-title">Another title</a></li>' +
440 443 '<li class="heading2"><a href="#An-Internet-link-inside-subtitle">An Internet link inside subtitle</a></li>' +
441 444 '<li class="heading2"><a href="#Project-Name">Project Name</a></li>' +
442 445 '</ul>'
443 446
447 @project = Project.find(1)
448 assert textilizable(raw).gsub("\n", "").include?(expected)
449 end
450
451 def test_table_of_content_should_contain_included_page_headings
452 raw = <<-RAW
453 {{toc}}
454
455 h1. Included
456
457 {{include(Child_1)}}
458 RAW
459
460 expected = '<ul class="toc">' +
461 '<li class="heading1"><a href="#Included">Included</a></li>' +
462 '<li class="heading1"><a href="#Child-page-1">Child page 1</a></li>' +
463 '</ul>'
464
465 @project = Project.find(1)
444 466 assert textilizable(raw).gsub("\n", "").include?(expected)
445 467 end
446 468
447 469 def test_blockquote
448 470 # orig raw text
449 471 raw = <<-RAW
450 472 John said:
451 473 > Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas sed libero.
452 474 > Nullam commodo metus accumsan nulla. Curabitur lobortis dui id dolor.
453 475 > * Donec odio lorem,
454 476 > * sagittis ac,
455 477 > * malesuada in,
456 478 > * adipiscing eu, dolor.
457 479 >
458 480 > >Nulla varius pulvinar diam. Proin id arcu id lorem scelerisque condimentum. Proin vehicula turpis vitae lacus.
459 481 > Proin a tellus. Nam vel neque.
460 482
461 483 He's right.
462 484 RAW
463 485
464 486 # expected html
465 487 expected = <<-EXPECTED
466 488 <p>John said:</p>
467 489 <blockquote>
468 490 Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas sed libero.
469 491 Nullam commodo metus accumsan nulla. Curabitur lobortis dui id dolor.
470 492 <ul>
471 493 <li>Donec odio lorem,</li>
472 494 <li>sagittis ac,</li>
473 495 <li>malesuada in,</li>
474 496 <li>adipiscing eu, dolor.</li>
475 497 </ul>
476 498 <blockquote>
477 499 <p>Nulla varius pulvinar diam. Proin id arcu id lorem scelerisque condimentum. Proin vehicula turpis vitae lacus.</p>
478 500 </blockquote>
479 501 <p>Proin a tellus. Nam vel neque.</p>
480 502 </blockquote>
481 503 <p>He's right.</p>
482 504 EXPECTED
483 505
484 506 assert_equal expected.gsub(%r{\s+}, ''), textilizable(raw).gsub(%r{\s+}, '')
485 507 end
486 508
487 509 def test_table
488 510 raw = <<-RAW
489 511 This is a table with empty cells:
490 512
491 513 |cell11|cell12||
492 514 |cell21||cell23|
493 515 |cell31|cell32|cell33|
494 516 RAW
495 517
496 518 expected = <<-EXPECTED
497 519 <p>This is a table with empty cells:</p>
498 520
499 521 <table>
500 522 <tr><td>cell11</td><td>cell12</td><td></td></tr>
501 523 <tr><td>cell21</td><td></td><td>cell23</td></tr>
502 524 <tr><td>cell31</td><td>cell32</td><td>cell33</td></tr>
503 525 </table>
504 526 EXPECTED
505 527
506 528 assert_equal expected.gsub(%r{\s+}, ''), textilizable(raw).gsub(%r{\s+}, '')
507 529 end
508 530
509 531 def test_table_with_line_breaks
510 532 raw = <<-RAW
511 533 This is a table with line breaks:
512 534
513 535 |cell11
514 536 continued|cell12||
515 537 |-cell21-||cell23
516 538 cell23 line2
517 539 cell23 *line3*|
518 540 |cell31|cell32
519 541 cell32 line2|cell33|
520 542
521 543 RAW
522 544
523 545 expected = <<-EXPECTED
524 546 <p>This is a table with line breaks:</p>
525 547
526 548 <table>
527 549 <tr>
528 550 <td>cell11<br />continued</td>
529 551 <td>cell12</td>
530 552 <td></td>
531 553 </tr>
532 554 <tr>
533 555 <td><del>cell21</del></td>
534 556 <td></td>
535 557 <td>cell23<br/>cell23 line2<br/>cell23 <strong>line3</strong></td>
536 558 </tr>
537 559 <tr>
538 560 <td>cell31</td>
539 561 <td>cell32<br/>cell32 line2</td>
540 562 <td>cell33</td>
541 563 </tr>
542 564 </table>
543 565 EXPECTED
544 566
545 567 assert_equal expected.gsub(%r{\s+}, ''), textilizable(raw).gsub(%r{\s+}, '')
546 568 end
547 569
548 570 def test_textile_should_not_mangle_brackets
549 571 assert_equal '<p>[msg1][msg2]</p>', textilizable('[msg1][msg2]')
550 572 end
551 573
552 574 def test_default_formatter
553 575 Setting.text_formatting = 'unknown'
554 576 text = 'a *link*: http://www.example.net/'
555 577 assert_equal '<p>a *link*: <a href="http://www.example.net/">http://www.example.net/</a></p>', textilizable(text)
556 578 Setting.text_formatting = 'textile'
557 579 end
558 580
559 581 def test_due_date_distance_in_words
560 582 to_test = { Date.today => 'Due in 0 days',
561 583 Date.today + 1 => 'Due in 1 day',
562 584 Date.today + 100 => 'Due in about 3 months',
563 585 Date.today + 20000 => 'Due in over 54 years',
564 586 Date.today - 1 => '1 day late',
565 587 Date.today - 100 => 'about 3 months late',
566 588 Date.today - 20000 => 'over 54 years late',
567 589 }
568 590 to_test.each do |date, expected|
569 591 assert_equal expected, due_date_distance_in_words(date)
570 592 end
571 593 end
572 594
573 595 def test_avatar
574 596 # turn on avatars
575 597 Setting.gravatar_enabled = '1'
576 598 assert avatar(User.find_by_mail('jsmith@somenet.foo')).include?(Digest::MD5.hexdigest('jsmith@somenet.foo'))
577 599 assert avatar('jsmith <jsmith@somenet.foo>').include?(Digest::MD5.hexdigest('jsmith@somenet.foo'))
578 600 assert_nil avatar('jsmith')
579 601 assert_nil avatar(nil)
580 602
581 603 # turn off avatars
582 604 Setting.gravatar_enabled = '0'
583 605 assert_equal '', avatar(User.find_by_mail('jsmith@somenet.foo'))
584 606 end
585 607
586 608 def test_link_to_user
587 609 user = User.find(2)
588 610 t = link_to_user(user)
589 611 assert_equal "<a href=\"/users/2\">#{ user.name }</a>", t
590 612 end
591 613
592 614 def test_link_to_user_should_not_link_to_locked_user
593 615 user = User.find(5)
594 616 assert user.locked?
595 617 t = link_to_user(user)
596 618 assert_equal user.name, t
597 619 end
598 620
599 621 def test_link_to_user_should_not_link_to_anonymous
600 622 user = User.anonymous
601 623 assert user.anonymous?
602 624 t = link_to_user(user)
603 625 assert_equal ::I18n.t(:label_user_anonymous), t
604 626 end
605 627
606 628 def test_link_to_project
607 629 project = Project.find(1)
608 630 assert_equal %(<a href="/projects/ecookbook">eCookbook</a>),
609 631 link_to_project(project)
610 632 assert_equal %(<a href="/projects/ecookbook/settings">eCookbook</a>),
611 633 link_to_project(project, :action => 'settings')
612 634 assert_equal %(<a href="http://test.host/projects/ecookbook?jump=blah">eCookbook</a>),
613 635 link_to_project(project, {:only_path => false, :jump => 'blah'})
614 636 assert_equal %(<a href="/projects/ecookbook/settings" class="project">eCookbook</a>),
615 637 link_to_project(project, {:action => 'settings'}, :class => "project")
616 638 end
617 639 end
@@ -1,98 +1,101
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2008 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.dirname(__FILE__) + '/../../../../test_helper'
19 19
20 20 class Redmine::WikiFormatting::MacrosTest < HelperTestCase
21 21 include ApplicationHelper
22 22 include ActionView::Helpers::TextHelper
23 include ActionView::Helpers::SanitizeHelper
24 extend ActionView::Helpers::SanitizeHelper::ClassMethods
25
23 26 fixtures :projects, :roles, :enabled_modules, :users,
24 27 :repositories, :changesets,
25 28 :trackers, :issue_statuses, :issues,
26 29 :versions, :documents,
27 30 :wikis, :wiki_pages, :wiki_contents,
28 31 :boards, :messages,
29 32 :attachments
30 33
31 34 def setup
32 35 super
33 36 @project = nil
34 37 end
35 38
36 39 def teardown
37 40 end
38 41
39 42 def test_macro_hello_world
40 43 text = "{{hello_world}}"
41 44 assert textilizable(text).match(/Hello world!/)
42 45 # escaping
43 46 text = "!{{hello_world}}"
44 47 assert_equal '<p>{{hello_world}}</p>', textilizable(text)
45 48 end
46 49
47 50 def test_macro_include
48 51 @project = Project.find(1)
49 52 # include a page of the current project wiki
50 53 text = "{{include(Another page)}}"
51 54 assert textilizable(text).match(/This is a link to a ticket/)
52 55
53 56 @project = nil
54 57 # include a page of a specific project wiki
55 58 text = "{{include(ecookbook:Another page)}}"
56 59 assert textilizable(text).match(/This is a link to a ticket/)
57 60
58 61 text = "{{include(ecookbook:)}}"
59 62 assert textilizable(text).match(/CookBook documentation/)
60 63
61 64 text = "{{include(unknowidentifier:somepage)}}"
62 65 assert textilizable(text).match(/Page not found/)
63 66 end
64 67
65 68 def test_macro_child_pages
66 69 expected = "<p><ul class=\"pages-hierarchy\">\n" +
67 70 "<li><a href=\"/projects/ecookbook/wiki/Child_1\">Child 1</a></li>\n" +
68 71 "<li><a href=\"/projects/ecookbook/wiki/Child_2\">Child 2</a></li>\n" +
69 72 "</ul>\n</p>"
70 73
71 74 @project = Project.find(1)
72 75 # child pages of the current wiki page
73 76 assert_equal expected, textilizable("{{child_pages}}", :object => WikiPage.find(2).content)
74 77 # child pages of another page
75 78 assert_equal expected, textilizable("{{child_pages(Another_page)}}", :object => WikiPage.find(1).content)
76 79
77 80 @project = Project.find(2)
78 81 assert_equal expected, textilizable("{{child_pages(ecookbook:Another_page)}}", :object => WikiPage.find(1).content)
79 82 end
80 83
81 84 def test_macro_child_pages_with_option
82 85 expected = "<p><ul class=\"pages-hierarchy\">\n" +
83 86 "<li><a href=\"/projects/ecookbook/wiki/Another_page\">Another page</a>\n" +
84 87 "<ul class=\"pages-hierarchy\">\n" +
85 88 "<li><a href=\"/projects/ecookbook/wiki/Child_1\">Child 1</a></li>\n" +
86 89 "<li><a href=\"/projects/ecookbook/wiki/Child_2\">Child 2</a></li>\n" +
87 90 "</ul>\n</li>\n</ul>\n</p>"
88 91
89 92 @project = Project.find(1)
90 93 # child pages of the current wiki page
91 94 assert_equal expected, textilizable("{{child_pages(parent=1)}}", :object => WikiPage.find(2).content)
92 95 # child pages of another page
93 96 assert_equal expected, textilizable("{{child_pages(Another_page, parent=1)}}", :object => WikiPage.find(1).content)
94 97
95 98 @project = Project.find(2)
96 99 assert_equal expected, textilizable("{{child_pages(ecookbook:Another_page, parent=1)}}", :object => WikiPage.find(1).content)
97 100 end
98 101 end
General Comments 0
You need to be logged in to leave comments. Login now