application_helper.rb 47 KB
Newer Older
1 2
# encoding: utf-8
#
3
# Redmine - project management software
jplang's avatar
jplang committed
4
# Copyright (C) 2006-2012  Jean-Philippe Lang
5 6 7 8 9
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
10
#
11 12 13 14
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
15
#
16 17 18 19
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.

20
require 'forwardable'
21
require 'cgi'
22

23
module ApplicationHelper
24
  include Redmine::WikiFormatting::Macros::Definitions
25
  include Redmine::I18n
26
  include GravatarHelper::PublicMethods
27

28 29 30
  extend Forwardable
  def_delegators :wiki_helper, :wikitoolbar_for, :heads_for_wiki_formatter

31
  # Return true if user is authorized for controller/action, otherwise false
jplang's avatar
jplang committed
32 33
  def authorize_for(controller, action)
    User.current.allowed_to?({:controller => controller, :action => action}, @project)
34 35 36
  end

  # Display a link if user is authorized
37 38
  #
  # @param [String] name Anchor text (passed to link_to)
edavis10's avatar
edavis10 committed
39
  # @param [Hash] options Hash params. This will checked by authorize_for to see if the user is authorized
40 41
  # @param [optional, Hash] html_options Options passed to link_to
  # @param [optional, Hash] parameters_for_method_reference Extra parameters for link_to
42
  def link_to_if_authorized(name, options = {}, html_options = nil, *parameters_for_method_reference)
edavis10's avatar
edavis10 committed
43
    link_to(name, options, html_options, *parameters_for_method_reference) if authorize_for(options[:controller] || params[:controller], options[:action])
44
  end
45

46
  # Displays a link to user's account page if active
47
  def link_to_user(user, options={})
jplang's avatar
jplang committed
48
    if user.is_a?(User)
49 50 51 52 53 54
      name = h(user.name(options[:format]))
      if user.active?
        link_to name, :controller => 'users', :action => 'show', :id => user
      else
        name
      end
jplang's avatar
jplang committed
55
    else
56
      h(user.to_s)
jplang's avatar
jplang committed
57
    end
58
  end
59

60 61
  # Displays a link to +issue+ with its subject.
  # Examples:
62
  #
63 64 65
  #   link_to_issue(issue)                        # => Defect #6: This is the subject
  #   link_to_issue(issue, :truncate => 6)        # => Defect #6: This i...
  #   link_to_issue(issue, :subject => false)     # => Defect #6
66
  #   link_to_issue(issue, :project => true)      # => Foo - Defect #6
67
  #
68
  def link_to_issue(issue, options={})
69 70 71 72 73 74 75 76 77 78
    title = nil
    subject = nil
    if options[:subject] == false
      title = truncate(issue.subject, :length => 60)
    else
      subject = issue.subject
      if options[:truncate]
        subject = truncate(subject, :length => options[:truncate])
      end
    end
tmaruyama's avatar
tmaruyama committed
79
    s = link_to "#{h(issue.tracker)} ##{issue.id}", {:controller => "issues", :action => "show", :id => issue},
80 81
                                                 :class => issue.css_classes,
                                                 :title => title
jplang's avatar
jplang committed
82 83
    s << h(": #{subject}") if subject
    s = h("#{issue.project} - ") + s if options[:project]
84
    s
jplang's avatar
jplang committed
85
  end
86

87 88 89 90 91 92 93
  # Generates a link to an attachment.
  # Options:
  # * :text - Link text (default to attachment filename)
  # * :download - Force download (default: false)
  def link_to_attachment(attachment, options={})
    text = options.delete(:text) || attachment.filename
    action = options.delete(:download) ? 'download' : 'show'
94 95
    opt_only_path = {}
    opt_only_path[:only_path] = (options[:only_path] == false ? false : true)
96
    options.delete(:only_path)
97 98
    link_to(h(text),
           {:controller => 'attachments', :action => action,
99
            :id => attachment, :filename => attachment.filename}.merge(opt_only_path),
100
           options)
101
  end
102

103 104 105
  # Generates a link to a SCM revision
  # Options:
  # * :text - Link text (default to the formatted revision)
106 107 108 109
  def link_to_revision(revision, repository, options={})
    if repository.is_a?(Project)
      repository = repository.repository
    end
110
    text = options.delete(:text) || format_revision(revision)
111
    rev = revision.respond_to?(:identifier) ? revision.identifier : revision
112 113
    link_to(
        h(text),
114
        {:controller => 'repositories', :action => 'revision', :id => repository.project, :repository_id => repository.identifier_param, :rev => rev},
115 116
        :title => l(:label_revision_id, format_revision(revision))
      )
117
  end
118

119 120 121 122 123 124
  # Generates a link to a message
  def link_to_message(message, options={}, html_options = nil)
    link_to(
      h(truncate(message.subject, :length => 60)),
      { :controller => 'messages', :action => 'show',
        :board_id => message.board_id,
125
        :id => (message.parent_id || message.id),
126 127 128 129 130 131
        :r => (message.parent_id && message.id),
        :anchor => (message.parent_id ? "message-#{message.id}" : nil)
      }.merge(options),
      html_options
    )
  end
132

133 134
  # Generates a link to a project if active
  # Examples:
135
  #
136 137 138 139 140 141
  #   link_to_project(project)                          # => link to the specified project overview
  #   link_to_project(project, :action=>'settings')     # => link to project settings
  #   link_to_project(project, {:only_path => false}, :class => "project") # => 3rd arg adds html options
  #   link_to_project(project, {}, :class => "project") # => html options with default url (project overview)
  #
  def link_to_project(project, options={}, html_options = nil)
142 143 144
    if project.archived?
      h(project)
    else
145 146 147 148 149
      url = {:controller => 'projects', :action => 'show', :id => project}.merge(options)
      link_to(h(project), url, html_options)
    end
  end

150 151 152 153 154 155
  def thumbnail_tag(attachment)
    link_to image_tag(url_for(:controller => 'attachments', :action => 'thumbnail', :id => attachment)),
      {:controller => 'attachments', :action => 'show', :id => attachment, :filename => attachment.filename},
      :title => attachment.filename
  end

jplang's avatar
jplang committed
156
  def toggle_link(name, id, options={})
157 158
    onclick = "$('##{id}').toggle(); "
    onclick << (options[:focus] ? "$('##{options[:focus]}').focus(); " : "this.blur(); ")
jplang's avatar
jplang committed
159 160 161
    onclick << "return false;"
    link_to(name, "#", :onclick => onclick)
  end
162

163 164
  def image_to_function(name, function, html_options = {})
    html_options.symbolize_keys!
165 166 167
    tag(:input, html_options.merge({
        :type => "image", :src => image_path(name),
        :onclick => (html_options[:onclick] ? "#{html_options[:onclick]}; " : "") + "#{function};"
168 169
        }))
  end
170

171
  def format_activity_title(text)
172
    h(truncate_single_line(text, :length => 100))
173
  end
174

175
  def format_activity_day(date)
176
    date == User.current.today ? l(:label_today).titleize : format_date(date)
177
  end
178

179
  def format_activity_description(text)
180 181
    h(truncate(text.to_s, :length => 120).gsub(%r{[\r\n]*<(pre|code)>.*$}m, '...')
       ).gsub(/[\r\n]+/, "<br />").html_safe
182
  end
183

184 185 186 187 188 189 190
  def format_version_name(version)
    if version.project == @project
    	h(version)
    else
      h("#{version.project} - #{version}")
    end
  end
191

192 193 194 195 196
  def due_date_distance_in_words(date)
    if date
      l((date < Date.today ? :label_roadmap_overdue : :label_roadmap_due_in), distance_of_date_in_words(Date.today, date))
    end
  end
197

198 199 200 201 202 203 204 205
  # Renders a tree of projects as a nested set of unordered lists
  # The given collection may be a subset of the whole project tree
  # (eg. some intermediate nodes are private and can not be seen)
  def render_project_nested_lists(projects)
    s = ''
    if projects.any?
      ancestors = []
      original_project = @project
jplang's avatar
jplang committed
206
      projects.sort_by(&:lft).each do |project|
207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230
        # set the project environment to please macros.
        @project = project
        if (ancestors.empty? || project.is_descendant_of?(ancestors.last))
          s << "<ul class='projects #{ ancestors.empty? ? 'root' : nil}'>\n"
        else
          ancestors.pop
          s << "</li>"
          while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
            ancestors.pop
            s << "</ul></li>\n"
          end
        end
        classes = (ancestors.empty? ? 'root' : 'child')
        s << "<li class='#{classes}'><div class='#{classes}'>"
        s << h(block_given? ? yield(project) : project.name)
        s << "</div>\n"
        ancestors << project
      end
      s << ("</li></ul>\n" * ancestors.size)
      @project = original_project
    end
    s.html_safe
  end

231
  def render_page_hierarchy(pages, node=nil, options={})
232 233 234 235 236
    content = ''
    if pages[node]
      content << "<ul class=\"pages-hierarchy\">\n"
      pages[node].each do |page|
        content << "<li>"
237
        content << link_to(h(page.pretty_title), {:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title},
238 239
                           :title => (options[:timestamp] && page.updated_on ? l(:label_updated_time, distance_of_time_in_words(Time.now, page.updated_on)) : nil))
        content << "\n" + render_page_hierarchy(pages, page.id, options) if pages[page.id]
240 241 242 243
        content << "</li>\n"
      end
      content << "</ul>\n"
    end
244
    content.html_safe
245
  end
246

247 248 249 250
  # Renders flash messages
  def render_flash_messages
    s = ''
    flash.each do |k,v|
251
      s << content_tag('div', v.html_safe, :class => "flash #{k}", :id => "flash_#{k}")
252
    end
253
    s.html_safe
254
  end
255

jplang's avatar
jplang committed
256 257 258 259 260 261 262 263
  # Renders tabs and their content
  def render_tabs(tabs)
    if tabs.any?
      render :partial => 'common/tabs', :locals => {:tabs => tabs}
    else
      content_tag 'p', l(:label_no_data), :class => "nodata"
    end
  end
264

265 266
  # Renders the project quick-jump box
  def render_project_jump_box
267
    return unless User.current.logged?
268
    projects = User.current.memberships.collect(&:project).compact.select(&:active?).uniq
269
    if projects.any?
emassip's avatar
emassip committed
270 271 272 273 274 275
      options =
        ("<option value=''>#{ l(:label_jump_to_a_project) }</option>" +
         '<option value="" disabled="disabled">---</option>').html_safe

      options << project_tree_options_for_select(projects, :selected => @project) do |p|
        { :value => project_path(:id => p, :jump => current_menu_item) }
276
      end
emassip's avatar
emassip committed
277 278

      select_tag('project_quick_jump_box', options, :onchange => 'if (this.value != \'\') { window.location = this.value; }')
279 280
    end
  end
281

282 283 284
  def project_tree_options_for_select(projects, options = {})
    s = ''
    project_tree(projects) do |project, level|
emassip's avatar
emassip committed
285
      name_prefix = (level > 0 ? '&nbsp;' * 2 * level + '&#187; ' : '').html_safe
286 287 288 289 290 291
      tag_options = {:value => project.id}
      if project == options[:selected] || (options[:selected].respond_to?(:include?) && options[:selected].include?(project))
        tag_options[:selected] = 'selected'
      else
        tag_options[:selected] = nil
      end
292 293 294
      tag_options.merge!(yield(project)) if block_given?
      s << content_tag('option', name_prefix + h(project), tag_options)
    end
295
    s.html_safe
296
  end
297

298
  # Yields the given block for each project with its level in the tree
299 300
  #
  # Wrapper for Project#project_tree
301
  def project_tree(projects, &block)
302
    Project.project_tree(projects, &block)
303
  end
304

jplang's avatar
jplang committed
305 306
  def principals_check_box_tags(name, principals)
    s = ''
307
    principals.sort.each do |principal|
jplang's avatar
jplang committed
308 309
      s << "<label>#{ check_box_tag name, principal.id, false } #{h principal}</label>\n"
    end
310
    s.html_safe
jplang's avatar
jplang committed
311
  end
312

313 314 315
  # Returns a string for users/groups option tags
  def principals_options_for_select(collection, selected=nil)
    s = ''
316
    if collection.include?(User.current)
jplang's avatar
jplang committed
317
      s << content_tag('option', "<< #{l(:label_me)} >>", :value => User.current.id)
318
    end
319 320 321 322 323 324 325 326
    groups = ''
    collection.sort.each do |element|
      selected_attribute = ' selected="selected"' if option_value_selected?(element, selected)
      (element.is_a?(Group) ? groups : s) << %(<option value="#{element.id}"#{selected_attribute}>#{h element.name}</option>)
    end
    unless groups.empty?
      s << %(<optgroup label="#{h(l(:label_group_plural))}">#{groups}</optgroup>)
    end
327
    s.html_safe
328
  end
329

330 331
  # Truncates and returns the string as a single line
  def truncate_single_line(string, *args)
332
    truncate(string.to_s, *args).gsub(%r{[\r\n]+}m, ' ')
333
  end
334

335 336 337 338 339 340 341 342 343
  # Truncates at line break after 250 characters or options[:length]
  def truncate_lines(string, options={})
    length = options[:length] || 250
    if string.to_s =~ /\A(.{#{length}}.*?)$/m
      "#{$1}..."
    else
      string
    end
  end
344

jplang's avatar
jplang committed
345 346 347 348
  def anchor(text)
    text.to_s.gsub(' ', '_')
  end

349
  def html_hours(text)
350
    text.gsub(%r{(\d+)\.(\d+)}, '<span class="hours hours-int">\1</span><span class="hours hours-dec">.\2</span>').html_safe
351
  end
352

jplang's avatar
jplang committed
353
  def authoring(created, author, options={})
354
    l(options[:label] || :label_added_time_by, :author => link_to_user(author), :age => time_tag(created)).html_safe
355
  end
356

357 358 359
  def time_tag(time)
    text = distance_of_time_in_words(Time.now, time)
    if @project
360
      link_to(text, {:controller => 'activities', :action => 'index', :id => @project, :from => User.current.time_to_date(time)}, :title => format_time(time))
361 362 363
    else
      content_tag('acronym', text, :title => format_time(time))
    end
364 365
  end

366 367 368 369 370 371
  def syntax_highlight_lines(name, content)
    lines = []
    syntax_highlight(name, content).each_line { |line| lines << line }
    lines
  end

372
  def syntax_highlight(name, content)
373
    Redmine::SyntaxHighlighting.highlight_by_filename(content, name)
374
  end
375

jplang's avatar
jplang committed
376
  def to_path_param(path)
377 378
    str = path.to_s.split(%r{[/\\]}).select{|p| !p.blank?}.join("/")
    str.blank? ? nil : str
jplang's avatar
jplang committed
379 380
  end

381
  def pagination_links_full(paginator, count=nil, options={})
382
    page_param = options.delete(:page_param) || :page
383
    per_page_links = options.delete(:per_page_links)
384
    url_param = params.dup
385 386

    html = ''
387
    if paginator.current.previous
388 389 390 391
      # \xc2\xab(utf-8) = &#171;
      html << link_to_content_update(
                   "\xc2\xab " + l(:label_previous),
                   url_param.merge(page_param => paginator.current.previous)) + ' '
392
    end
393

394
    html << (pagination_links_each(paginator, options) do |n|
395
      link_to_content_update(n.to_s, url_param.merge(page_param => n))
396
    end || '')
397

398
    if paginator.current.next
399 400 401 402
      # \xc2\xbb(utf-8) = &#187;
      html << ' ' + link_to_content_update(
                      (l(:label_next) + " \xc2\xbb"),
                      url_param.merge(page_param => paginator.current.next))
403
    end
404

405
    unless count.nil?
406
      html << " (#{paginator.current.first_item}-#{paginator.current.last_item}/#{count})"
407
      if per_page_links != false && links = per_page_links(paginator.items_per_page, count)
408 409
	      html << " | #{links}"
      end
410
    end
411

412
    html.html_safe
413
  end
414

415 416 417 418 419 420 421 422 423 424 425 426 427 428
  def per_page_links(selected=nil, item_count=nil)
    values = Setting.per_page_options_array
    if item_count && values.any?
      if item_count > values.first
        max = values.detect {|value| value >= item_count} || item_count
      else
        max = item_count
      end
      values = values.select {|value| value <= max || value == selected}
    end
    if values.empty? || (values.size == 1 && values.first == selected)
      return nil
    end
    links = values.collect do |n|
429
      n == selected ? n : link_to_content_update(n, params.merge(:per_page => n))
430
    end
431
    l(:label_display_per_page, links.join(', '))
432
  end
433

jplang's avatar
jplang committed
434
  def reorder_links(name, url, method = :post)
435 436
    link_to(image_tag('2uparrow.png', :alt => l(:label_sort_highest)),
            url.merge({"#{name}[move_to]" => 'highest'}),
jplang's avatar
jplang committed
437
            :method => method, :title => l(:label_sort_highest)) +
438 439
    link_to(image_tag('1uparrow.png',   :alt => l(:label_sort_higher)),
            url.merge({"#{name}[move_to]" => 'higher'}),
jplang's avatar
jplang committed
440
           :method => method, :title => l(:label_sort_higher)) +
441 442
    link_to(image_tag('1downarrow.png', :alt => l(:label_sort_lower)),
            url.merge({"#{name}[move_to]" => 'lower'}),
jplang's avatar
jplang committed
443
            :method => method, :title => l(:label_sort_lower)) +
444 445
    link_to(image_tag('2downarrow.png', :alt => l(:label_sort_lowest)),
            url.merge({"#{name}[move_to]" => 'lowest'}),
jplang's avatar
jplang committed
446
           :method => method, :title => l(:label_sort_lowest))
jplang's avatar
jplang committed
447
  end
448

449
  def breadcrumb(*args)
450
    elements = args.flatten
451
    elements.any? ? content_tag('p', (args.join(" \xc2\xbb ") + " \xc2\xbb ").html_safe, :class => 'breadcrumb') : nil
452
  end
453

454
  def other_formats_links(&block)
jplang's avatar
jplang committed
455
    concat('<p class="other-formats">'.html_safe + l(:label_export_to))
456
    yield Redmine::Views::OtherFormatsBuilder.new(self)
jplang's avatar
jplang committed
457
    concat('</p>'.html_safe)
458
  end
459

460 461 462 463 464
  def page_header_title
    if @project.nil? || @project.new_record?
      h(Setting.app_title)
    else
      b = []
jplang's avatar
jplang committed
465
      ancestors = (@project.root? ? [] : @project.ancestors.visible.all)
466 467
      if ancestors.any?
        root = ancestors.shift
468
        b << link_to_project(root, {:jump => current_menu_item}, :class => 'root')
469
        if ancestors.size > 2
470
          b << "\xe2\x80\xa6"
471 472
          ancestors = ancestors[-2, 2]
        end
473
        b += ancestors.collect {|p| link_to_project(p, {:jump => current_menu_item}, :class => 'ancestor') }
474 475
      end
      b << h(@project)
476
      b.join(" \xc2\xbb ").html_safe
477 478
    end
  end
479

480 481
  def html_title(*args)
    if args.empty?
482
      title = @html_title || []
tmaruyama's avatar
tmaruyama committed
483
      title << @project.name if @project
484
      title << Setting.app_title unless Setting.app_title == title.last
jplang's avatar
jplang committed
485
      title.select {|t| !t.blank? }.join(' - ')
486 487 488 489
    else
      @html_title ||= []
      @html_title += args
    end
490
  end
jplang's avatar
jplang committed
491

492 493 494 495 496 497 498 499
  # Returns the theme, controller name, and action as css classes for the
  # HTML body.
  def body_css_classes
    css = []
    if theme = Redmine::Themes.theme(Setting.ui_theme)
      css << 'theme-' + theme.name
    end

500 501
    css << 'controller-' + controller_name
    css << 'action-' + action_name
502 503 504
    css.join(' ')
  end

jplang's avatar
jplang committed
505
  def accesskey(s)
506
    Redmine::AccessKeys.key_for s
jplang's avatar
jplang committed
507 508
  end

509 510 511 512 513 514 515 516
  # Formats text according to system settings.
  # 2 ways to call this method:
  # * with a String: textilizable(text, options)
  # * with an object and one of its attribute: textilizable(issue, :description, options)
  def textilizable(*args)
    options = args.last.is_a?(Hash) ? args.pop : {}
    case args.size
    when 1
517
      obj = options[:object]
jplang's avatar
jplang committed
518
      text = args.shift
519 520
    when 2
      obj = args.shift
521 522
      attr = args.shift
      text = obj.send(attr).to_s
523 524 525
    else
      raise ArgumentError, 'invalid arguments to textilizable'
    end
jplang's avatar
jplang committed
526
    return '' if text.blank?
527 528
    project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
    only_path = options.delete(:only_path) == false ? false : true
529

530 531
    text = text.dup
    macros = catch_macros(text)
532
    text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text, :object => obj, :attribute => attr)
533

534
    @parsed_headings = []
535
    @heading_anchors = {}
536
    @current_section = 0 if options[:edit_section_links]
537 538

    parse_sections(text, project, obj, attr, only_path, options)
539 540
    text = parse_non_pre_blocks(text, obj, macros) do |text|
      [:parse_inline_attachments, :parse_wiki_links, :parse_redmine_links].each do |method_name|
541 542
        send method_name, text, project, obj, attr, only_path, options
      end
543
    end
544
    parse_headings(text, project, obj, attr, only_path, options)
545

546 547 548
    if @parsed_headings.any?
      replace_toc(text, @parsed_headings)
    end
549

jplang's avatar
jplang committed
550
    text.html_safe
551
  end
552

553
  def parse_non_pre_blocks(text, obj, macros)
554 555 556 557 558 559 560 561
    s = StringScanner.new(text)
    tags = []
    parsed = ''
    while !s.eos?
      s.scan(/(.*?)(<(\/)?(pre|code)(.*?)>|\z)/im)
      text, full_tag, closing, tag = s[1], s[2], s[3], s[4]
      if tags.empty?
        yield text
562 563 564
        inject_macros(text, obj, macros) if macros.any?
      else
        inject_macros(text, obj, macros, false) if macros.any?
565 566 567 568 569 570 571 572 573 574 575 576 577
      end
      parsed << text
      if tag
        if closing
          if tags.last == tag.downcase
            tags.pop
          end
        else
          tags << tag.downcase
        end
        parsed << full_tag
      end
    end
578 579 580 581
    # Close any non closing tags
    while tag = tags.pop
      parsed << "</#{tag}>"
    end
582
    parsed
583
  end
584

585
  def parse_inline_attachments(text, project, obj, attr, only_path, options)
586
    # when using an image link, try to use an attachment, if possible
587
    if options[:attachments] || (obj && obj.respond_to?(:attachments))
588
      attachments = options[:attachments] || obj.attachments
589
      text.gsub!(/src="([^\/"]+\.(bmp|gif|jpg|jpe|jpeg|png))"(\s+alt="([^"]*)")?/i) do |m|
590
        filename, ext, alt, alttext = $1.downcase, $2, $3, $4
591
        # search for the picture in attachments
592 593 594
        if found = Attachment.latest_attach(attachments, filename)
          image_url = url_for :only_path => only_path, :controller => 'attachments',
                              :action => 'download', :id => found
595 596 597 598
          desc = found.description.to_s.gsub('"', '')
          if !desc.blank? && alttext.blank?
            alt = " title=\"#{desc}\" alt=\"#{desc}\""
          end
599
          "src=\"#{image_url}\"#{alt}"
600
        else
601
          m
602 603 604
        end
      end
    end
605
  end
606

607 608 609 610 611 612 613 614 615 616 617 618
  # Wiki links
  #
  # Examples:
  #   [[mypage]]
  #   [[mypage|mytext]]
  # wiki links can refer other project wikis, using project name or identifier:
  #   [[project:]] -> wiki starting page
  #   [[project:|mytext]]
  #   [[project:mypage]]
  #   [[project:mypage|mytext]]
  def parse_wiki_links(text, project, obj, attr, only_path, options)
    text.gsub!(/(!)?(\[\[([^\]\n\|]+)(\|([^\]\n\|]+))?\]\])/) do |m|
619
      link_project = project
620 621 622
      esc, all, page, title = $1, $2, $3, $5
      if esc.nil?
        if page =~ /^([^\:]+)\:(.*)$/
623
          link_project = Project.find_by_identifier($1) || Project.find_by_name($1)
624 625 626
          page = $2
          title ||= $1 if page.blank?
        end
627

628
        if link_project && link_project.wiki
629 630 631 632 633
          # extract anchor
          anchor = nil
          if page =~ /^(.+?)\#(.+)$/
            page, anchor = $1, $2
          end
634
          anchor = sanitize_anchor_name(anchor) if anchor.present?
635 636
          # check if page exists
          wiki_page = link_project.wiki.find_page(page)
637 638 639 640
          url = if anchor.present? && wiki_page.present? && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version)) && obj.page == wiki_page
            "##{anchor}"
          else
            case options[:wiki_links]
641
            when :local; "#{page.present? ? Wiki.titleize(page) : ''}.html" + (anchor.present? ? "##{anchor}" : '')
642
            when :anchor; "##{page.present? ? Wiki.titleize(page) : title}" + (anchor.present? ? "_#{anchor}" : '') # used for single-file wiki export
643
            else
644
              wiki_page_id = page.present? ? Wiki.titleize(page) : nil
645 646 647
              parent = wiki_page.nil? && obj.is_a?(WikiContent) && obj.page && project == link_project ? obj.page.title : nil
              url_for(:only_path => only_path, :controller => 'wiki', :action => 'show', :project_id => link_project, 
               :id => wiki_page_id, :anchor => anchor, :parent => parent)
648
            end
649
          end
emassip's avatar
emassip committed
650
          link_to(title.present? ? title.html_safe : h(page), url, :class => ('wiki-page' + (wiki_page ? '' : ' new')))
651 652
        else
          # project or wiki doesn't exist
653
          all
654
        end
jplang's avatar
jplang committed
655
      else
656
        all
jplang's avatar
jplang committed
657
      end
658
    end
659
  end
660

661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684
  # Redmine links
  #
  # Examples:
  #   Issues:
  #     #52 -> Link to issue #52
  #   Changesets:
  #     r52 -> Link to revision 52
  #     commit:a85130f -> Link to scmid starting with a85130f
  #   Documents:
  #     document#17 -> Link to document with id 17
  #     document:Greetings -> Link to the document with title "Greetings"
  #     document:"Some document" -> Link to the document with title "Some document"
  #   Versions:
  #     version#3 -> Link to version with id 3
  #     version:1.0.0 -> Link to version named "1.0.0"
  #     version:"1.0 beta 2" -> Link to version named "1.0 beta 2"
  #   Attachments:
  #     attachment:file.zip -> Link to the attachment of the current object named file.zip
  #   Source files:
  #     source:some/file -> Link to the file located at /some/file in the project's repository
  #     source:some/file@52 -> Link to the file's revision 52
  #     source:some/file#L120 -> Link to line 120 of the file
  #     source:some/file@52#L120 -> Link to line 120 of the file's revision 52
  #     export:some/file -> Force the download of the file
685
  #   Forum messages:
686
  #     message#1218 -> Link to message with id 1218
687 688 689 690 691 692
  #
  #   Links can refer other objects from other projects, using project identifier:
  #     identifier:r52
  #     identifier:document:"Some document"
  #     identifier:version:1.0.0
  #     identifier:source:some/file
693
  def parse_redmine_links(text, project, obj, attr, only_path, options)
694 695
    text.gsub!(%r{([\s\(,\-\[\>]|^)(!)?(([a-z0-9\-_]+):)?(attachment|document|version|forum|news|message|project|commit|source|export)?(((#)|((([a-z0-9\-]+)\|)?(r)))((\d+)((#note)?-(\d+))?)|(:)([^"\s<>][^\s<>]*?|"[^"]+?"))(?=(?=[[:punct:]][^A-Za-z0-9_/])|,|\s|\]|<|$)}) do |m|
      leading, esc, project_prefix, project_identifier, prefix, repo_prefix, repo_identifier, sep, identifier, comment_suffix, comment_id = $1, $2, $3, $4, $5, $10, $11, $8 || $12 || $18, $14 || $19, $15, $17
696
      link = nil
697 698 699
      if project_identifier
        project = Project.visible.find_by_identifier(project_identifier)
      end
700 701
      if esc.nil?
        if prefix.nil? && sep == 'r'
702 703 704 705 706 707 708 709 710 711 712 713 714
          if project
            repository = nil
            if repo_identifier
              repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
            else
              repository = project.repository
            end
            # project.changesets.visible raises an SQL error because of a double join on repositories
            if repository && (changeset = Changeset.visible.find_by_repository_id_and_revision(repository.id, identifier))
              link = link_to(h("#{project_prefix}#{repo_prefix}r#{identifier}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :repository_id => repository.identifier_param, :rev => changeset.revision},
                                        :class => 'changeset',
                                        :title => truncate_single_line(changeset.comments, :length => 100))
            end
715 716
          end
        elsif sep == '#'
717
          oid = identifier.to_i
718 719
          case prefix
          when nil
720
            if oid.to_s == identifier && issue = Issue.visible.find_by_id(oid, :include => :status)
721 722
              anchor = comment_id ? "note-#{comment_id}" : nil
              link = link_to("##{oid}", {:only_path => only_path, :controller => 'issues', :action => 'show', :id => oid, :anchor => anchor},
jplang's avatar
jplang committed
723
                                        :class => issue.css_classes,
724
                                        :title => "#{truncate(issue.subject, :length => 100)} (#{issue.status.name})")
725 726
            end
          when 'document'
727
            if document = Document.visible.find_by_id(oid)
jplang's avatar
jplang committed
728 729
              link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
                                                :class => 'document'
730 731
            end
          when 'version'
732
            if version = Version.visible.find_by_id(oid)
jplang's avatar
jplang committed
733 734
              link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
                                              :class => 'version'
735
            end
736
          when 'message'
737
            if message = Message.visible.find_by_id(oid, :include => :parent)
738
              link = link_to_message(message, {:only_path => only_path}, :class => 'message')
739
            end
740 741 742 743 744 745 746 747 748 749
          when 'forum'
            if board = Board.visible.find_by_id(oid)
              link = link_to h(board.name), {:only_path => only_path, :controller => 'boards', :action => 'show', :id => board, :project_id => board.project},
                                             :class => 'board'
            end
          when 'news'
            if news = News.visible.find_by_id(oid)
              link = link_to h(news.title), {:only_path => only_path, :controller => 'news', :action => 'show', :id => news},
                                            :class => 'news'
            end
jplang's avatar
jplang committed
750 751
          when 'project'
            if p = Project.visible.find_by_id(oid)
752
              link = link_to_project(p, {:only_path => only_path}, :class => 'project')
jplang's avatar
jplang committed
753
            end
754 755 756
          end
        elsif sep == ':'
          # removes the double quotes if any
757
          name = identifier.gsub(%r{^"(.*)"$}, "\\1")
758 759
          case prefix
          when 'document'
760
            if project && document = project.documents.visible.find_by_title(name)
jplang's avatar
jplang committed
761 762
              link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
                                                :class => 'document'
763 764
            end
          when 'version'
765
            if project && version = project.versions.visible.find_by_name(name)
jplang's avatar
jplang committed
766 767
              link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
                                              :class => 'version'
768
            end
769 770 771 772 773 774 775 776 777 778
          when 'forum'
            if project && board = project.boards.visible.find_by_name(name)
              link = link_to h(board.name), {:only_path => only_path, :controller => 'boards', :action => 'show', :id => board, :project_id => board.project},
                                             :class => 'board'
            end
          when 'news'
            if project && news = project.news.visible.find_by_title(name)
              link = link_to h(news.title), {:only_path => only_path, :controller => 'news', :action => 'show', :id => news},
                                            :class => 'news'
            end
779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806
          when 'commit', 'source', 'export'
            if project
              repository = nil
              if name =~ %r{^(([a-z0-9\-]+)\|)(.+)$}
                repo_prefix, repo_identifier, name = $1, $2, $3
                repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
              else
                repository = project.repository
              end
              if prefix == 'commit'
                if repository && (changeset = Changeset.visible.find(:first, :conditions => ["repository_id = ? AND scmid LIKE ?", repository.id, "#{name}%"]))
                  link = link_to h("#{project_prefix}#{repo_prefix}#{name}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :repository_id => repository.identifier_param, :rev => changeset.identifier},
                                               :class => 'changeset',
                                               :title => truncate_single_line(h(changeset.comments), :length => 100)
                end
              else
                if repository && User.current.allowed_to?(:browse_repository, project)
                  name =~ %r{^[/\\]*(.*?)(@([0-9a-f]+))?(#(L\d+))?$}
                  path, rev, anchor = $1, $3, $5
                  link = link_to h("#{project_prefix}#{prefix}:#{repo_prefix}#{name}"), {:controller => 'repositories', :action => 'entry', :id => project, :repository_id => repository.identifier_param,
                                                          :path => to_path_param(path),
                                                          :rev => rev,
                                                          :anchor => anchor,
                                                          :format => (prefix == 'export' ? 'raw' : nil)},
                                                         :class => (prefix == 'export' ? 'source download' : 'source')
                end
              end
              repo_prefix = nil
807
            end
808
          when 'attachment'
809
            attachments = options[:attachments] || (obj && obj.respond_to?(:attachments) ? obj.attachments : nil)
810
            if attachments && attachment = attachments.detect {|a| a.filename == name }
jplang's avatar
jplang committed
811 812
              link = link_to h(attachment.filename), {:only_path => only_path, :controller => 'attachments', :action => 'download', :id => attachment},
                                                     :class => 'attachment'
813
            end
jplang's avatar
jplang committed
814 815
          when 'project'
            if p = Project.visible.find(:first, :conditions => ["identifier = :s OR LOWER(name) = :s", {:s => name.downcase}])
816
              link = link_to_project(p, {:only_path => only_path}, :class => 'project')
jplang's avatar
jplang committed
817
            end
818
          end
jplang's avatar
jplang committed
819
        end
820
      end
821
      (leading + (link || "#{project_prefix}#{prefix}#{repo_prefix}#{sep}#{identifier}#{comment_suffix}"))
822
    end
823
  end
824

jplang's avatar
jplang committed
825
  HEADING_RE = /(<h(\d)( [^>]+)?>(.+?)<\/h(\d)>)/i unless const_defined?(:HEADING_RE)
826 827 828 829

  def parse_sections(text, project, obj, attr, only_path, options)
    return unless options[:edit_section_links]
    text.gsub!(HEADING_RE) do
830
      heading = $1
831 832
      @current_section += 1
      if @current_section > 1
833
        content_tag('div',
834
          link_to(image_tag('edit.png'), options[:edit_section_links].merge(:section => @current_section)),
835
          :class => 'contextual',
836
          :title => l(:button_edit_section)) + heading.html_safe
837
      else
838
        heading
839 840 841
      end
    end
  end
842

843
  # Headings and TOC
844
  # Adds ids and links to headings unless options[:headings] is set to false
845
  def parse_headings(text, project, obj, attr, only_path, options)
846
    return if options[:headings] == false
847

848
    text.gsub!(HEADING_RE) do
849
      level, attrs, content = $2.to_i, $3, $4
850
      item = strip_tags(content).strip
851
      anchor = sanitize_anchor_name(item)
852 853
      # used for single-file wiki export
      anchor = "#{obj.page.title}_#{anchor}" if options[:wiki_links] == :anchor && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version))
854 855 856 857 858
      @heading_anchors[anchor] ||= 0
      idx = (@heading_anchors[anchor] += 1)
      if idx > 1
        anchor = "#{anchor}-#{idx}"
      end
859
      @parsed_headings << [level, anchor, item]
860
      "<a name=\"#{anchor}\"></a>\n<h#{level} #{attrs}>#{content}<a href=\"##{anchor}\" class=\"wiki-anchor\">&para;</a></h#{level}>"
861 862
    end
  end
863

864
  MACROS_RE = /(
865 866 867 868
                (!)?                        # escaping
                (
                \{\{                        # opening tag
                ([\w]+)                     # macro name
869
                (\(([^\n\r]*?)\))?          # optional arguments
870
                ([\n\r].*?[\n\r])?          # optional block of text
871 872
                \}\}                        # closing tag
                )
873
               )/mx unless const_defined?(:MACROS_RE)
874 875 876 877 878

  MACRO_SUB_RE = /(
                  \{\{
                  macro\((\d+)\)
                  \}\}
879
                  )/x unless const_defined?(:MACRO_SUB_RE)
880

881 882 883
  # Extracts macros from text
  def catch_macros(text)
    macros = {}
884
    text.gsub!(MACROS_RE) do
885 886 887 888 889
      all, macro = $1, $4.downcase
      if macro_exists?(macro) || all =~ MACRO_SUB_RE
        index = macros.size
        macros[index] = all
        "{{macro(#{index})}}"
890 891 892 893
      else
        all
      end
    end
894 895 896 897 898 899 900 901 902
    macros
  end

  # Executes and replaces macros in text
  def inject_macros(text, obj, macros, execute=true)
    text.gsub!(MACRO_SUB_RE) do
      all, index = $1, $2.to_i
      orig = macros.delete(index)
      if execute && orig && orig =~ MACROS_RE
903
        esc, all, macro, args, block = $2, $3, $4.downcase, $6.to_s, $7.try(:strip)
904
        if esc.nil?
905
          h(exec_macro(macro, obj, args, block) || all)
906 907