application_helper.rb 45.7 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
  include Redmine::Pagination::Helper
28

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

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

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

47
  # Displays a link to user's account page if active
48
  def link_to_user(user, options={})
jplang's avatar
jplang committed
49
    if user.is_a?(User)
50
      name = h(user.name(options[:format]))
51
      if user.active? || (User.current.admin? && user.logged?)
jplang's avatar
jplang committed
52
        link_to name, user_path(user), :class => user.css_classes
53 54 55
      else
        name
      end
jplang's avatar
jplang committed
56
    else
57
      h(user.to_s)
jplang's avatar
jplang committed
58
    end
59
  end
60

61 62
  # Displays a link to +issue+ with its subject.
  # Examples:
63
  #
64 65 66
  #   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
67
  #   link_to_issue(issue, :project => true)      # => Foo - Defect #6
68
  #   link_to_issue(issue, :subject => false, :tracker => false)     # => #6
69
  #
70
  def link_to_issue(issue, options={})
71 72
    title = nil
    subject = nil
73
    text = options[:tracker] == false ? "##{issue.id}" : "#{issue.tracker} ##{issue.id}"
74 75 76 77 78 79 80 81
    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
jplang's avatar
jplang committed
82
    s = link_to text, issue_path(issue), :class => issue.css_classes, :title => title
jplang's avatar
jplang committed
83 84
    s << h(": #{subject}") if subject
    s = h("#{issue.project} - ") + s if options[:project]
85
    s
jplang's avatar
jplang committed
86
  end
87

88 89 90 91 92 93 94
  # 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'
95 96
    opt_only_path = {}
    opt_only_path[:only_path] = (options[:only_path] == false ? false : true)
97
    options.delete(:only_path)
98 99
    link_to(h(text),
           {:controller => 'attachments', :action => action,
100
            :id => attachment, :filename => attachment.filename}.merge(opt_only_path),
101
           options)
102
  end
103

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

120 121 122 123 124 125
  # 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,
126
        :id => (message.parent_id || message.id),
127 128 129 130 131 132
        :r => (message.parent_id && message.id),
        :anchor => (message.parent_id ? "message-#{message.id}" : nil)
      }.merge(options),
      html_options
    )
  end
133

134 135
  # Generates a link to a project if active
  # Examples:
136
  #
137 138 139 140 141 142
  #   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)
143 144 145
    if project.archived?
      h(project)
    else
jplang's avatar
jplang committed
146 147
      url = {:controller => 'projects', :action => 'show', :id => project}.merge(options)
      link_to(h(project), url, html_options)
148 149 150
    end
  end

151 152 153 154
  def wiki_page_path(page, options={})
    url_for({:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title}.merge(options))
  end

155 156 157 158 159 160
  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
161
  def toggle_link(name, id, options={})
162 163
    onclick = "$('##{id}').toggle(); "
    onclick << (options[:focus] ? "$('##{options[:focus]}').focus(); " : "this.blur(); ")
jplang's avatar
jplang committed
164 165 166
    onclick << "return false;"
    link_to(name, "#", :onclick => onclick)
  end
167

168 169
  def image_to_function(name, function, html_options = {})
    html_options.symbolize_keys!
170 171 172
    tag(:input, html_options.merge({
        :type => "image", :src => image_path(name),
        :onclick => (html_options[:onclick] ? "#{html_options[:onclick]}; " : "") + "#{function};"
173 174
        }))
  end
175

176
  def format_activity_title(text)
177
    h(truncate_single_line(text, :length => 100))
178
  end
179

180
  def format_activity_day(date)
181
    date == User.current.today ? l(:label_today).titleize : format_date(date)
182
  end
183

184
  def format_activity_description(text)
185 186
    h(truncate(text.to_s, :length => 120).gsub(%r{[\r\n]*<(pre|code)>.*$}m, '...')
       ).gsub(/[\r\n]+/, "<br />").html_safe
187
  end
188

189 190 191 192 193 194 195
  def format_version_name(version)
    if version.project == @project
    	h(version)
    else
      h("#{version.project} - #{version}")
    end
  end
196

197 198 199 200 201
  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
202

203 204 205 206 207 208 209 210
  # 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
211
      projects.sort_by(&:lft).each do |project|
212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235
        # 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

236
  def render_page_hierarchy(pages, node=nil, options={})
237 238 239 240 241
    content = ''
    if pages[node]
      content << "<ul class=\"pages-hierarchy\">\n"
      pages[node].each do |page|
        content << "<li>"
jplang's avatar
jplang committed
242
        content << link_to(h(page.pretty_title), {:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title, :version => nil},
243 244
                           :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]
245 246 247 248
        content << "</li>\n"
      end
      content << "</ul>\n"
    end
249
    content.html_safe
250
  end
251

252 253 254 255
  # Renders flash messages
  def render_flash_messages
    s = ''
    flash.each do |k,v|
256
      s << content_tag('div', v.html_safe, :class => "flash #{k}", :id => "flash_#{k}")
257
    end
258
    s.html_safe
259
  end
260

jplang's avatar
jplang committed
261 262 263 264 265 266 267 268
  # 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
269

270 271
  # Renders the project quick-jump box
  def render_project_jump_box
272
    return unless User.current.logged?
273
    projects = User.current.memberships.collect(&:project).compact.select(&:active?).uniq
274
    if projects.any?
emassip's avatar
emassip committed
275 276 277 278 279 280
      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) }
281
      end
emassip's avatar
emassip committed
282 283

      select_tag('project_quick_jump_box', options, :onchange => 'if (this.value != \'\') { window.location = this.value; }')
284 285
    end
  end
286

287 288 289
  def project_tree_options_for_select(projects, options = {})
    s = ''
    project_tree(projects) do |project, level|
emassip's avatar
emassip committed
290
      name_prefix = (level > 0 ? '&nbsp;' * 2 * level + '&#187; ' : '').html_safe
291 292 293 294 295 296
      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
297 298 299
      tag_options.merge!(yield(project)) if block_given?
      s << content_tag('option', name_prefix + h(project), tag_options)
    end
300
    s.html_safe
301
  end
302

303
  # Yields the given block for each project with its level in the tree
304 305
  #
  # Wrapper for Project#project_tree
306
  def project_tree(projects, &block)
307
    Project.project_tree(projects, &block)
308
  end
309

jplang's avatar
jplang committed
310 311
  def principals_check_box_tags(name, principals)
    s = ''
312
    principals.sort.each do |principal|
jplang's avatar
jplang committed
313 314
      s << "<label>#{ check_box_tag name, principal.id, false } #{h principal}</label>\n"
    end
315
    s.html_safe
jplang's avatar
jplang committed
316
  end
317

318 319 320
  # Returns a string for users/groups option tags
  def principals_options_for_select(collection, selected=nil)
    s = ''
321
    if collection.include?(User.current)
jplang's avatar
jplang committed
322
      s << content_tag('option', "<< #{l(:label_me)} >>", :value => User.current.id)
323
    end
324 325 326 327 328 329 330 331
    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
332
    s.html_safe
333
  end
334

335 336 337 338 339 340 341 342 343
  # Options for the new membership projects combo-box
  def options_for_membership_project_select(principal, projects)
    options = content_tag('option', "--- #{l(:actionview_instancetag_blank_option)} ---")
    options << project_tree_options_for_select(projects) do |p|
      {:disabled => principal.projects.include?(p)}
    end
    options
  end

344 345
  # Truncates and returns the string as a single line
  def truncate_single_line(string, *args)
346
    truncate(string.to_s, *args).gsub(%r{[\r\n]+}m, ' ')
347
  end
348

349 350 351 352 353 354 355 356 357
  # 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
358

jplang's avatar
jplang committed
359 360 361 362
  def anchor(text)
    text.to_s.gsub(' ', '_')
  end

363
  def html_hours(text)
364
    text.gsub(%r{(\d+)\.(\d+)}, '<span class="hours hours-int">\1</span><span class="hours hours-dec">.\2</span>').html_safe
365
  end
366

jplang's avatar
jplang committed
367
  def authoring(created, author, options={})
368
    l(options[:label] || :label_added_time_by, :author => link_to_user(author), :age => time_tag(created)).html_safe
369
  end
370

371 372 373
  def time_tag(time)
    text = distance_of_time_in_words(Time.now, time)
    if @project
374
      link_to(text, {:controller => 'activities', :action => 'index', :id => @project, :from => User.current.time_to_date(time)}, :title => format_time(time))
375 376 377
    else
      content_tag('acronym', text, :title => format_time(time))
    end
378 379
  end

380 381 382 383 384 385
  def syntax_highlight_lines(name, content)
    lines = []
    syntax_highlight(name, content).each_line { |line| lines << line }
    lines
  end

386
  def syntax_highlight(name, content)
387
    Redmine::SyntaxHighlighting.highlight_by_filename(content, name)
388
  end
389

jplang's avatar
jplang committed
390
  def to_path_param(path)
391 392
    str = path.to_s.split(%r{[/\\]}).select{|p| !p.blank?}.join("/")
    str.blank? ? nil : str
jplang's avatar
jplang committed
393 394
  end

jplang's avatar
jplang committed
395
  def reorder_links(name, url, method = :post)
396 397
    link_to(image_tag('2uparrow.png', :alt => l(:label_sort_highest)),
            url.merge({"#{name}[move_to]" => 'highest'}),
jplang's avatar
jplang committed
398
            :method => method, :title => l(:label_sort_highest)) +
399 400
    link_to(image_tag('1uparrow.png',   :alt => l(:label_sort_higher)),
            url.merge({"#{name}[move_to]" => 'higher'}),
jplang's avatar
jplang committed
401
           :method => method, :title => l(:label_sort_higher)) +
402 403
    link_to(image_tag('1downarrow.png', :alt => l(:label_sort_lower)),
            url.merge({"#{name}[move_to]" => 'lower'}),
jplang's avatar
jplang committed
404
            :method => method, :title => l(:label_sort_lower)) +
405 406
    link_to(image_tag('2downarrow.png', :alt => l(:label_sort_lowest)),
            url.merge({"#{name}[move_to]" => 'lowest'}),
jplang's avatar
jplang committed
407
           :method => method, :title => l(:label_sort_lowest))
jplang's avatar
jplang committed
408
  end
409

410
  def breadcrumb(*args)
411
    elements = args.flatten
412
    elements.any? ? content_tag('p', (args.join(" \xc2\xbb ") + " \xc2\xbb ").html_safe, :class => 'breadcrumb') : nil
413
  end
414

415
  def other_formats_links(&block)
jplang's avatar
jplang committed
416
    concat('<p class="other-formats">'.html_safe + l(:label_export_to))
417
    yield Redmine::Views::OtherFormatsBuilder.new(self)
jplang's avatar
jplang committed
418
    concat('</p>'.html_safe)
419
  end
420

421 422 423 424 425
  def page_header_title
    if @project.nil? || @project.new_record?
      h(Setting.app_title)
    else
      b = []
jplang's avatar
jplang committed
426
      ancestors = (@project.root? ? [] : @project.ancestors.visible.all)
427 428
      if ancestors.any?
        root = ancestors.shift
429
        b << link_to_project(root, {:jump => current_menu_item}, :class => 'root')
430
        if ancestors.size > 2
431
          b << "\xe2\x80\xa6"
432 433
          ancestors = ancestors[-2, 2]
        end
434
        b += ancestors.collect {|p| link_to_project(p, {:jump => current_menu_item}, :class => 'ancestor') }
435 436
      end
      b << h(@project)
437
      b.join(" \xc2\xbb ").html_safe
438 439
    end
  end
440

441 442
  def html_title(*args)
    if args.empty?
443
      title = @html_title || []
tmaruyama's avatar
tmaruyama committed
444
      title << @project.name if @project
445
      title << Setting.app_title unless Setting.app_title == title.last
jplang's avatar
jplang committed
446
      title.select {|t| !t.blank? }.join(' - ')
447 448 449 450
    else
      @html_title ||= []
      @html_title += args
    end
451
  end
jplang's avatar
jplang committed
452

453 454 455 456 457 458 459 460
  # 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

461 462
    css << 'controller-' + controller_name
    css << 'action-' + action_name
463 464 465
    css.join(' ')
  end

jplang's avatar
jplang committed
466
  def accesskey(s)
467
    Redmine::AccessKeys.key_for s
jplang's avatar
jplang committed
468 469
  end

470 471 472 473 474 475 476 477
  # 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
478
      obj = options[:object]
jplang's avatar
jplang committed
479
      text = args.shift
480 481
    when 2
      obj = args.shift
482 483
      attr = args.shift
      text = obj.send(attr).to_s
484 485 486
    else
      raise ArgumentError, 'invalid arguments to textilizable'
    end
jplang's avatar
jplang committed
487
    return '' if text.blank?
488 489
    project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
    only_path = options.delete(:only_path) == false ? false : true
490

491 492
    text = text.dup
    macros = catch_macros(text)
493
    text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text, :object => obj, :attribute => attr)
494

495
    @parsed_headings = []
496
    @heading_anchors = {}
497
    @current_section = 0 if options[:edit_section_links]
498 499

    parse_sections(text, project, obj, attr, only_path, options)
500 501
    text = parse_non_pre_blocks(text, obj, macros) do |text|
      [:parse_inline_attachments, :parse_wiki_links, :parse_redmine_links].each do |method_name|
502 503
        send method_name, text, project, obj, attr, only_path, options
      end
504
    end
505
    parse_headings(text, project, obj, attr, only_path, options)
506

507 508 509
    if @parsed_headings.any?
      replace_toc(text, @parsed_headings)
    end
510

jplang's avatar
jplang committed
511
    text.html_safe
512
  end
513

514
  def parse_non_pre_blocks(text, obj, macros)
515 516 517 518 519 520 521 522
    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
523 524 525
        inject_macros(text, obj, macros) if macros.any?
      else
        inject_macros(text, obj, macros, false) if macros.any?
526 527 528 529 530 531 532 533 534 535 536 537 538
      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
539 540 541 542
    # Close any non closing tags
    while tag = tags.pop
      parsed << "</#{tag}>"
    end
543
    parsed
544
  end
545

546
  def parse_inline_attachments(text, project, obj, attr, only_path, options)
547
    # when using an image link, try to use an attachment, if possible
548
    if options[:attachments].present? || obj.respond_to?(:attachments)
jplang's avatar
jplang committed
549
      attachments = options[:attachments] || []
550
      attachments += obj.attachments if obj.respond_to?(:attachments)
551
      text.gsub!(/src="([^\/"]+\.(bmp|gif|jpg|jpe|jpeg|png))"(\s+alt="([^"]*)")?/i) do |m|
552
        filename, ext, alt, alttext = $1.downcase, $2, $3, $4
553
        # search for the picture in attachments
554 555 556
        if found = Attachment.latest_attach(attachments, filename)
          image_url = url_for :only_path => only_path, :controller => 'attachments',
                              :action => 'download', :id => found
557 558 559 560
          desc = found.description.to_s.gsub('"', '')
          if !desc.blank? && alttext.blank?
            alt = " title=\"#{desc}\" alt=\"#{desc}\""
          end
561
          "src=\"#{image_url}\"#{alt}"
562
        else
563
          m
564 565 566
        end
      end
    end
567
  end
568

569 570 571 572 573 574 575 576 577 578 579 580
  # 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|
581
      link_project = project
582 583 584
      esc, all, page, title = $1, $2, $3, $5
      if esc.nil?
        if page =~ /^([^\:]+)\:(.*)$/
585
          link_project = Project.find_by_identifier($1) || Project.find_by_name($1)
586 587 588
          page = $2
          title ||= $1 if page.blank?
        end
589

590
        if link_project && link_project.wiki
591 592 593 594 595
          # extract anchor
          anchor = nil
          if page =~ /^(.+?)\#(.+)$/
            page, anchor = $1, $2
          end
596
          anchor = sanitize_anchor_name(anchor) if anchor.present?
597 598
          # check if page exists
          wiki_page = link_project.wiki.find_page(page)
599 600 601 602
          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]
603
            when :local; "#{page.present? ? Wiki.titleize(page) : ''}.html" + (anchor.present? ? "##{anchor}" : '')
604
            when :anchor; "##{page.present? ? Wiki.titleize(page) : title}" + (anchor.present? ? "_#{anchor}" : '') # used for single-file wiki export
605
            else
606
              wiki_page_id = page.present? ? Wiki.titleize(page) : nil
607 608
              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, 
jplang's avatar
jplang committed
609
               :id => wiki_page_id, :version => nil, :anchor => anchor, :parent => parent)
610
            end
611
          end
emassip's avatar
emassip committed
612
          link_to(title.present? ? title.html_safe : h(page), url, :class => ('wiki-page' + (wiki_page ? '' : ' new')))
613 614
        else
          # project or wiki doesn't exist
615
          all
616
        end
jplang's avatar
jplang committed
617
      else
618
        all
jplang's avatar
jplang committed
619
      end
620
    end
621
  end
622

623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646
  # 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
647
  #   Forum messages:
648
  #     message#1218 -> Link to message with id 1218
649 650 651 652 653 654
  #
  #   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
655
  def parse_redmine_links(text, project, obj, attr, only_path, options)
656 657
    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
658
      link = nil
659 660 661
      if project_identifier
        project = Project.visible.find_by_identifier(project_identifier)
      end
662 663
      if esc.nil?
        if prefix.nil? && sep == 'r'
664 665 666 667 668 669 670 671 672 673 674 675 676
          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
677 678
          end
        elsif sep == '#'
679
          oid = identifier.to_i
680 681
          case prefix
          when nil
682
            if oid.to_s == identifier && issue = Issue.visible.find_by_id(oid, :include => :status)
683 684
              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
685
                                        :class => issue.css_classes,
686
                                        :title => "#{truncate(issue.subject, :length => 100)} (#{issue.status.name})")
687 688
            end
          when 'document'
689
            if document = Document.visible.find_by_id(oid)
jplang's avatar
jplang committed
690 691
              link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
                                                :class => 'document'
692 693
            end
          when 'version'
694
            if version = Version.visible.find_by_id(oid)
jplang's avatar
jplang committed
695 696
              link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
                                              :class => 'version'
697
            end
698
          when 'message'
699
            if message = Message.visible.find_by_id(oid, :include => :parent)
700
              link = link_to_message(message, {:only_path => only_path}, :class => 'message')
701
            end
702 703 704 705 706 707 708 709 710 711
          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
712 713
          when 'project'
            if p = Project.visible.find_by_id(oid)
714
              link = link_to_project(p, {:only_path => only_path}, :class => 'project')
jplang's avatar
jplang committed
715
            end
716 717 718
          end
        elsif sep == ':'
          # removes the double quotes if any
719
          name = identifier.gsub(%r{^"(.*)"$}, "\\1")
720 721
          case prefix
          when 'document'
722
            if project && document = project.documents.visible.find_by_title(name)
jplang's avatar
jplang committed
723 724
              link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
                                                :class => 'document'
725 726
            end
          when 'version'
727
            if project && version = project.versions.visible.find_by_name(name)
jplang's avatar
jplang committed
728 729
              link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
                                              :class => 'version'
730
            end
731 732 733 734 735 736 737 738 739 740
          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
741 742 743 744 745 746 747 748 749 750
          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'
jplang's avatar
jplang committed
751
                if repository && (changeset = Changeset.visible.where("repository_id = ? AND scmid LIKE ?", repository.id, "#{name}%").first)
752 753 754 755 756 757
                  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)
758
                  name =~ %r{^[/\\]*(.*?)(@([^/\\@]+?))?(#(L\d+))?$}
759
                  path, rev, anchor = $1, $3, $5
760
                  link = link_to h("#{project_prefix}#{prefix}:#{repo_prefix}#{name}"), {:controller => 'repositories', :action => (prefix == 'export' ? 'raw' : 'entry'), :id => project, :repository_id => repository.identifier_param,
761 762
                                                          :path => to_path_param(path),
                                                          :rev => rev,
763
                                                          :anchor => anchor},
764 765 766 767
                                                         :class => (prefix == 'export' ? 'source download' : 'source')
                end
              end
              repo_prefix = nil
768
            end
769
          when 'attachment'
770
            attachments = options[:attachments] || (obj && obj.respond_to?(:attachments) ? obj.attachments : nil)
771
            if attachments && attachment = attachments.detect {|a| a.filename == name }
jplang's avatar
jplang committed
772 773
              link = link_to h(attachment.filename), {:only_path => only_path, :controller => 'attachments', :action => 'download', :id => attachment},
                                                     :class => 'attachment'
774
            end
jplang's avatar
jplang committed
775
          when 'project'
jplang's avatar
jplang committed
776
            if p = Project.visible.where("identifier = :s OR LOWER(name) = :s", :s => name.downcase).first
777
              link = link_to_project(p, {:only_path => only_path}, :class => 'project')
jplang's avatar
jplang committed
778
            end
779
          end
jplang's avatar
jplang committed
780
        end
781
      end
782
      (leading + (link || "#{project_prefix}#{prefix}#{repo_prefix}#{sep}#{identifier}#{comment_suffix}"))
783
    end
784
  end
785

jplang's avatar
jplang committed
786
  HEADING_RE = /(<h(\d)( [^>]+)?>(.+?)<\/h(\d)>)/i unless const_defined?(:HEADING_RE)
787 788 789 790

  def parse_sections(text, project, obj, attr, only_path, options)
    return unless options[:edit_section_links]
    text.gsub!(HEADING_RE) do
791
      heading = $1
792 793
      @current_section += 1
      if @current_section > 1
794
        content_tag('div',
795
          link_to(image_tag('edit.png'), options[:edit_section_links].merge(:section => @current_section)),
796
          :class => 'contextual',
797
          :title => l(:button_edit_section)) + heading.html_safe
798
      else
799
        heading
800 801 802
      end
    end
  end
803

804
  # Headings and TOC
805
  # Adds ids and links to headings unless options[:headings] is set to false
806
  def parse_headings(text, project, obj, attr, only_path, options)
807
    return if options[:headings] == false
808

809
    text.gsub!(HEADING_RE) do
810
      level, attrs, content = $2.to_i, $3, $4
811
      item = strip_tags(content).strip
812
      anchor = sanitize_anchor_name(item)
813 814
      # 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))
815 816 817 818 819
      @heading_anchors[anchor] ||= 0
      idx = (@heading_anchors[anchor] += 1)
      if idx > 1
        anchor = "#{anchor}-#{idx}"
      end
820
      @parsed_headings << [level, anchor, item]
821
      "<a name=\"#{anchor}\"></a>\n<h#{level} #{attrs}>#{content}<a href=\"##{anchor}\" class=\"wiki-anchor\">&para;</a></h#{level}>"
822 823
    end
  end
824

825
  MACROS_RE = /(
826 827 828 829
                (!)?                        # escaping
                (
                \{\{                        # opening tag
                ([\w]+)                     # macro name
830
                (\(([^\n\r]*?)\))?          # optional arguments
831
                ([\n\r].*?[\n\r])?          # optional block of text
832 833
                \}\}                        # closing tag
                )
834
               )/mx unless const_defined?(:MACROS_RE)
835 836 837 838 839

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

842 843 844
  # Extracts macros from text
  def catch_macros(text)
    macros = {}
845
    text.gsub!(MACROS_RE) do
846 847 848 849 850
      all, macro = $1, $4.downcase
      if macro_exists?(macro) || all =~ MACRO_SUB_RE
        index = macros.size
        macros[index] = all
        "{{macro(#{index})}}"
851 852 853 854
      else
        all
      end
    end
855 856 857 858 859 860 861 862 863
    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
864
        esc, all, macro, args, block = $2, $3, $4.downcase, $6.to_s, $7.try(:strip)
865
        if esc.nil?
866
          h(exec_macro(macro, obj, args, block) || all)
867 868 869 870 871 872 873 874 875
        else
          h(all)
        end
      elsif orig
        h(orig)
      else
        h(all)
      end
    end
876 877
  end

878
  TOC_RE = /<p>\{\{([<>]?)toc\}\}<\/p>/i unless const_defined?(:TOC_RE)
879

880 881
  # Renders the TOC with given headings
  def replace_toc(text, headings)
882
    text.gsub!(TOC_RE) do
jplang's avatar
jplang committed
883 884
      # Keep only the 4 first levels
      headings = headings.select{|level, anchor, item| level <= 4}
885 886 887 888 889 890
      if headings.empty?
        ''
      else
        div_class = 'toc'
        div_class << ' right' if $1 == '>'
        div_class << ' left' if $1 == '<'
jplang's avatar
jplang committed
891 892 893 894
        out = "<ul class=\"#{div_class}\"><li>"
        root = headings.map(&:first).min
        current = root
        started = false
895
        headings.each do |level, anchor, item|
jplang's avatar
jplang committed
896 897 898 899 900 901 902 903 904 905
          if level > current
            out << '<ul><li>' * (level - current)
          elsif level < current
            out << "</li></ul>\n" * (current - level) + "</li><li>"
          elsif started
            out << '</li><li>'
          end
          out << "<a href=\"##{anchor}\">#{item}</a>"
          current = level
          started = true
906
        end
jplang's avatar
jplang committed
907 908
        out << '</li></ul>' * (current - root)
        out << '</li></ul>'