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