diff --git a/app/assets/javascripts/work_packages.js.erb b/app/assets/javascripts/work_packages.js.erb index 9ec1000463d4..2b237d00d178 100644 --- a/app/assets/javascripts/work_packages.js.erb +++ b/app/assets/javascripts/work_packages.js.erb @@ -73,7 +73,7 @@ var WorkPackage = WorkPackage || {}; var user = content.attr('data-user'); text = text.trim().replace(/
((.|\s)*?)<\/pre>/g, '[...]');
-                // remove blank lines generated by redmine textilizable
+                // remove blank lines generated by redmine format_text
                 text = text.replace(/^\s*$[\n\r]{1,}/gm, '');
 
                 var quotedText = "<%= ::I18n.t(:text_user_wrote, :value => "{{{user}}}", :locale => Setting.default_language.to_s.gsub(%r{(.+)\-(.+)$}) { "#{$1}-#{$2.upcase}" }) %>\n> ";
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index 9298148d4c89..6111c7147207 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -31,9 +31,9 @@
 require 'cgi'
 
 module ApplicationHelper
-  include Redmine::WikiFormatting::Macros::Definitions
+  include OpenProject::TextFormatting
+  include OpenProject::ObjectLinking
   include Redmine::I18n
-  include ERB::Util # for h()
 
   extend Forwardable
   def_delegators :wiki_helper, :wikitoolbar_for, :heads_for_wiki_formatter
@@ -74,28 +74,6 @@ def li_unless_nil(link)
     content_tag(:li, link) if link
   end
 
-  # Displays a link to user's account page if active or registered
-  def link_to_user(user, options={})
-    if user.is_a?(User)
-      name = user.name(options.delete(:format))
-      if user.active? || user.registered?
-        link_to(name, user, options)
-      else
-        name
-      end
-    else
-      h(user.to_s)
-    end
-  end
-
-  def link_to_work_package_preview(context = nil, options = {})
-    form_id = options[:form_id] || 'work_package-form-preview'
-    path = (context.is_a? WorkPackage) ? preview_work_package_path(context)
-                                       : preview_work_packages_path
-
-    preview_link path, form_id, { class: 'preview button' }
-  end
-
   # Show a sorted linkified (if active) comma-joined list of users
   def list_users(users, options={})
     users.sort.collect{|u| link_to_user(u, options)}.join(", ")
@@ -110,79 +88,6 @@ def user_status_i18n(user)
     l(('status_' + user.status_name).to_sym)
   end
 
-  # 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'
-    only_path = options.delete(:only_path) { true }
-
-    link_to h(text),
-            {:controller => '/attachments',
-             :action => action,
-             :id => attachment,
-             :filename => attachment.filename,
-             :host => Setting.host_name,
-             :protocol => Setting.protocol,
-             :only_path => only_path },
-            options
-  end
-
-  # Generates a link to a SCM revision
-  # Options:
-  # * :text - Link text (default to the formatted revision)
-  def link_to_revision(revision, project, options={})
-    text = options.delete(:text) || format_revision(revision)
-    rev = revision.respond_to?(:identifier) ? revision.identifier : revision
-
-    link_to(h(text), {:controller => '/repositories', :action => 'revision', :project_id => project, :rev => rev},
-            :title => l(:label_revision_id, format_revision(revision)))
-  end
-
-  # Generates a link to a message
-  def link_to_message(message, options={}, html_options = nil)
-    link_to(
-      h(truncate(message.subject, :length => 60)),
-      topic_path(message.root,
-                 { :r => (message.parent_id && message.id),
-                   :anchor => (message.parent_id ? "message-#{message.id}" : nil)
-                 }.merge(options)),
-      html_options
-    )
-  end
-
-  # Generates a link to a project if active
-  # Examples:
-  #
-  #   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, show_icon = false)
-    link = ''
-    project_link_name = project.name
-
-    if show_icon && User.current.member_of?(project)
-      project_link_name = icon_wrapper("icon-context icon-star1",I18n.t(:description_my_project).html_safe + " ".html_safe) + project_link_name
-    end
-
-    if project.active?
-      # backwards compatibility
-      if options.delete(:action) == 'settings'
-        link << link_to(project_link_name, settings_project_path(project, options), html_options)
-      else
-        link << link_to(project_link_name, project_path(project, options), html_options)
-      end
-    else
-      link << project_link_name
-    end
-
-    link.html_safe
-  end
-
   def toggle_link(name, id, options = {}, html_options = {})
     onclick = "jQuery('##{id}').toggle(); "
     onclick << (options[:focus] ? "jQuery('##{options[:focus]}').focus(); " : 'this.blur(); ')
@@ -396,21 +301,6 @@ def labeled_check_box_tags(name, collection, options = {})
     end.join.html_safe
   end
 
-  # Truncates and returns the string as a single line
-  def truncate_single_line(string, *args)
-    truncate(string.to_s, *args).gsub(%r{[\r\n]+}m, ' ')
-  end
-
-  # 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
-
   def html_hours(text)
     text.gsub(%r{(\d+)\.(\d+)}, '\1.\2').html_safe
   end
@@ -512,357 +402,6 @@ def accesskey(s)
     OpenProject::AccessKeys.key_for s
   end
 
-  # 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
-      obj = options[:object]
-      text = args.shift
-    when 2
-      obj = args.shift
-      attr = args.shift
-      text = obj.send(attr).to_s
-    else
-      raise ArgumentError, 'invalid arguments to textilizable'
-    end
-    return '' if text.blank?
-
-    edit = !!options.delete(:edit)
-    # don't return html in edit mode when textile or text formatting is enabled
-    return text if edit
-    project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
-    only_path = options.delete(:only_path) == false ? false : true
-
-    text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text, :object => obj, :attribute => attr, :edit => edit) { |macro, args| exec_macro(macro, obj, args, :view => self, :edit => edit) }
-
-    #TODO: transform modifications into WikiFormatting Helper, or at least ask the helper if he wants his stuff to be modified
-    @parsed_headings = []
-    text = parse_non_pre_blocks(text) do |text|
-      [:parse_inline_attachments, :parse_wiki_links, :parse_redmine_links, :parse_headings, :parse_relative_urls].each do |method_name|
-        send method_name, text, project, obj, attr, only_path, options
-      end
-    end
-
-    if @parsed_headings.any?
-      replace_toc(text, @parsed_headings)
-    end
-
-    text.html_safe
-  end
-  alias_method :textilize, :textilizable
-
-  def parse_non_pre_blocks(text)
-    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
-      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
-    # Close any non closing tags
-    while tag = tags.pop
-      parsed << ""
-    end
-    parsed
-  end
-
-  RELATIVE_LINK_RE = %r{
-    ]+?)')|
-          (?:"(\/[^>]+?)")
-        )
-      )|
-      [^>]
-    )*
-    >
-    [^<]*?<\/a>                     # content and closing link tag.
-  }x unless const_defined?(:RELATIVE_LINK_RE)
-
-  def parse_relative_urls(text, project, obj, attr, only_path, options)
-    return if only_path
-    text.gsub!(RELATIVE_LINK_RE) do |m|
-      href, relative_url = $1, $2 || $3
-      next m unless href.present?
-      if defined?(request) && request.present?
-        # we have a request!
-        protocol, host_with_port = request.protocol, request.host_with_port
-      elsif @controller
-        # use the same methods as url_for in the Mailer
-        url_opts = @controller.class.default_url_options
-        next m unless url_opts && url_opts[:protocol] && url_opts[:host]
-        protocol, host_with_port = "#{url_opts[:protocol]}://", url_opts[:host]
-      else
-        next m
-      end
-      m.sub href, " href=\"#{protocol}#{host_with_port}#{relative_url}\""
-    end
-  end
-
-  def parse_inline_attachments(text, project, obj, attr, only_path, options)
-    # when using an image link, try to use an attachment, if possible
-    if options[:attachments] || (obj && obj.respond_to?(:attachments))
-      attachments = nil
-      text.gsub!(/src="([^\/"]+\.(bmp|gif|jpg|jpeg|png))"(\s+alt="([^"]*)")?/i) do |m|
-        filename, ext, alt, alttext = $1.downcase, $2, $3, $4
-        attachments ||= (options[:attachments] || obj.attachments).sort_by(&:created_on).reverse
-        # search for the picture in attachments
-        if found = attachments.detect { |att| att.filename.downcase == filename }
-          image_url = url_for :only_path => only_path, :controller => '/attachments', :action => 'download', :id => found
-          desc = found.description.to_s.gsub('"', '')
-          if !desc.blank? && alttext.blank?
-            alt = " title=\"#{desc}\" alt=\"#{desc}\""
-          end
-          "src=\"#{image_url}\"#{alt}"
-        else
-          m
-        end
-      end
-    end
-  end
-
-  # 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|
-      link_project = project
-      esc, all, page, title = $1, $2, $3, $5
-      if esc.nil?
-        if page =~ /\A([^\:]+)\:(.*)\z/
-          link_project = Project.find_by_identifier($1) || Project.find_by_name($1)
-          page = $2
-          title ||= $1 if page.blank?
-        end
-
-        if link_project && link_project.wiki
-          # extract anchor
-          anchor = nil
-          if page =~ /\A(.+?)\#(.+)\z/
-            page, anchor = $1, $2
-          end
-          # check if page exists
-          wiki_page = link_project.wiki.find_page(page)
-          url = case options[:wiki_links]
-            when :local; "#{title}.html"
-            when :anchor; "##{title}"   # used for single-file wiki export
-            else
-              wiki_page_id = page.present? ? Wiki.titleize(page) : nil
-              url_for(:only_path => only_path, :controller => '/wiki', :action => 'show', :project_id => link_project, :id => wiki_page_id, :anchor => anchor)
-            end
-          link_to(h(title || page), url, :class => ('wiki-page' + (wiki_page ? '' : ' new')))
-        else
-          # project or wiki doesn't exist
-          all
-        end
-      else
-        all
-      end
-    end
-  end
-
-  # 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
-  #   Forum messages:
-  #     message#1218 -> Link to message with id 1218
-  #
-  #   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
-  def parse_redmine_links(text, project, obj, attr, only_path, options)
-    text.gsub!(%r{([\s\(,\-\[\>]|^)(!)?(([a-z0-9\-_]+):)?(attachment|version|commit|source|export|message|project)?((#+|r)(\d+)|(:)([^"\s<>][^\s<>]*?|"[^"]+?"))(?=(?=[[:punct:]]\W)|,|\s|\]|<|$)}) do |m|
-      leading, esc, project_prefix, project_identifier, prefix, sep, identifier = $1, $2, $3, $4, $5, $7 || $9, $8 || $10
-      link = nil
-      if project_identifier
-        project = Project.visible.find_by_identifier(project_identifier)
-      end
-      if esc.nil?
-        if prefix.nil? && sep == 'r'
-          # project.changesets.visible raises an SQL error because of a double join on repositories
-          if project && project.repository && (changeset = Changeset.visible.find_by_repository_id_and_revision(project.repository.id, identifier))
-            link = link_to(h("#{project_prefix}r#{identifier}"), {:only_path => only_path, :controller => '/repositories', :action => 'revision', :project_id => project, :rev => changeset.revision},
-                                      :class => 'changeset',
-                                      :title => truncate_single_line(changeset.comments, :length => 100))
-          end
-        elsif sep == '#'
-          oid = identifier.to_i
-          case prefix
-          when nil
-            if work_package = WorkPackage.visible.find_by_id(oid, :include => :status)
-              link = link_to("##{oid}", work_package_path(:id => oid, :only_path => only_path),
-                                        :class => work_package_css_classes(work_package),
-                                        :title => "#{truncate(work_package.subject, :length => 100)} (#{work_package.status.try(:name)})")
-            end
-          when 'version'
-            if version = Version.visible.find_by_id(oid)
-              link = link_to h(version.name), {:only_path => only_path, :controller => '/versions', :action => 'show', :id => version},
-                                              :class => 'version'
-            end
-          when 'message'
-            if message = Message.visible.find_by_id(oid, :include => :parent)
-              link = link_to_message(message, {:only_path => only_path}, :class => 'message')
-            end
-          when 'project'
-            if p = Project.visible.find_by_id(oid)
-              link = link_to_project(p, {:only_path => only_path}, :class => 'project')
-            end
-          end
-        elsif sep == '##'
-          oid = identifier.to_i
-          if work_package = WorkPackage.visible.find_by_id(oid, :include => :status)
-            link = work_package_quick_info(work_package)
-          end
-        elsif sep == '###'
-          oid = identifier.to_i
-          work_package = WorkPackage.visible.find_by_id(oid, :include => :status)
-          if work_package && obj && !(attr == :description && obj.id == work_package.id)
-            link = work_package_quick_info_with_description(work_package)
-          end
-        elsif sep == ':'
-          # removes the double quotes if any
-          name = identifier.gsub(%r{\A"(.*)"\z}, "\\1")
-          case prefix
-          when 'version'
-            if project && version = project.versions.visible.find_by_name(name)
-              link = link_to h(version.name), {:only_path => only_path, :controller => '/versions', :action => 'show', :id => version},
-                                              :class => 'version'
-            end
-          when 'commit'
-            if project && project.repository && (changeset = Changeset.visible.find(:first, :conditions => ["repository_id = ? AND scmid LIKE ?", project.repository.id, "#{name}%"]))
-              link = link_to h("#{project_prefix}#{name}"), {:only_path => only_path, :controller => '/repositories', :action => 'revision', :project_id => project, :rev => changeset.identifier},
-                                           :class => 'changeset',
-                                           :title => truncate_single_line(h(changeset.comments), :length => 100)
-            end
-          when 'source', 'export'
-            if project && project.repository && User.current.allowed_to?(:browse_repository, project)
-              name =~ %r{\A[/\\]*(.*?)(@([0-9a-f]+))?(#(L\d+))?\z}
-              path, rev, anchor = $1, $3, $5
-              link = link_to h("#{project_prefix}#{prefix}:#{name}"), {:controller => '/repositories', :action => 'entry', :project_id => project,
-                                                      :path => to_path_param(path),
-                                                      :rev => rev,
-                                                      :anchor => anchor,
-                                                      :format => (prefix == 'export' ? 'raw' : nil)},
-                                                     :class => (prefix == 'export' ? 'source download' : 'source')
-            end
-          when 'attachment'
-            attachments = options[:attachments] || (obj && obj.respond_to?(:attachments) ? obj.attachments : nil)
-            if attachments && attachment = attachments.detect {|a| a.filename == name }
-              link = link_to h(attachment.filename), {:only_path => only_path, :controller => '/attachments', :action => 'download', :id => attachment},
-                                                     :class => 'attachment'
-            end
-          when 'project'
-            if p = Project.visible.find(:first, :conditions => ["identifier = :s OR LOWER(name) = :s", {:s => name.downcase}])
-              link = link_to_project(p, {:only_path => only_path}, :class => 'project')
-            end
-          end
-        end
-      end
-      leading + (link || "#{project_prefix}#{prefix}#{sep}#{identifier}")
-    end
-  end
-
-  HEADING_RE = /]+)?>(.+?)<\/h(1|2|3|4)>/i unless const_defined?(:HEADING_RE)
-
-  # Headings and TOC
-  # Adds ids and links to headings unless options[:headings] is set to false
-  def parse_headings(text, project, obj, attr, only_path, options)
-    return if options[:headings] == false
-
-    text.gsub!(HEADING_RE) do
-      level, attrs, content = $1.to_i, $2, $3
-      item = strip_tags(content).strip
-      anchor = item.gsub(%r{[^\w\s\-]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
-      @parsed_headings << [level, anchor, item]
-      "\n#{content}"
-    end
-  end
-
-  TOC_RE = /

\{\{([<>]?)toc\}\}<\/p>/i unless const_defined?(:TOC_RE) - - # Renders the TOC with given headings - def replace_toc(text, headings) - text.gsub!(TOC_RE) do - if headings.empty? - '' - else - div_class = 'toc' - div_class << ' right' if $1 == '>' - div_class << ' left' if $1 == '<' - out = "

#{l(:label_table_of_contents)}
" - out << "
  • " - root = headings.map(&:first).min - current = root - started = false - headings.each do |level, anchor, item| - if level > current - out << '
    • ' * (level - current) - elsif level < current - out << "
    \n" * (current - level) + "
  • " - elsif started - out << '
  • ' - end - out << "#{item}" - current = level - started = true - end - out << '
' * (current - root) - out << '' - out << '
' - end - end - end - # Same as Rails' simple_format helper without using paragraphs def simple_format_without_paragraph(text) text.to_s. diff --git a/app/helpers/journals_helper.rb b/app/helpers/journals_helper.rb index 54aec3253ddc..d5f962f5298e 100644 --- a/app/helpers/journals_helper.rb +++ b/app/helpers/journals_helper.rb @@ -110,7 +110,7 @@ def render_notes(model, journal, options={}) content << content_tag('div', links.join(' '),{ :class => 'contextual' }, false) unless links.empty? attachments = model.try(:attachments) || [] content << content_tag('div', - textilizable(journal, :notes, :attachments => attachments), + format_text(journal, :notes, :attachments => attachments), :class => 'wikicontent', "data-user" => journal.journable.author) diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index 9222200407eb..476ebe6c0f82 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -85,7 +85,7 @@ def render_project_hierarchy(projects) classes = (ancestors.empty? ? 'root' : 'child') s << "
  • " + link_to_project(project, {}, {:class => "project"}, true) - s << "
    #{textilizable(project.short_description, :project => project)}
    " unless project.description.blank? + s << "
    #{format_text(project.short_description, :project => project)}
    " unless project.description.blank? s << "
    \n" ancestors << project end diff --git a/app/helpers/work_packages_helper.rb b/app/helpers/work_packages_helper.rb index 5646ade31fee..4f0e70a66c91 100644 --- a/app/helpers/work_packages_helper.rb +++ b/app/helpers/work_packages_helper.rb @@ -209,7 +209,7 @@ def work_package_quick_info_with_description(work_package, lines = 3) description = if work_package.description.blank? empty_element_tag else - textilizable(description_lines.join("")) + format_text(description_lines.join("")) end link = work_package_quick_info(work_package) diff --git a/app/mailers/user_mailer.rb b/app/mailers/user_mailer.rb index bf593f89cadb..8791b7dfee9c 100644 --- a/app/mailers/user_mailer.rb +++ b/app/mailers/user_mailer.rb @@ -28,7 +28,7 @@ #++ class UserMailer < ActionMailer::Base - helper :application, # for textilizable + helper :application, # for format_text :work_packages, # for css classes :custom_fields # for show_value diff --git a/app/views/common/feed.atom.builder b/app/views/common/feed.atom.builder index 49920ccb7b0e..b3464b831ba1 100644 --- a/app/views/common/feed.atom.builder +++ b/app/views/common/feed.atom.builder @@ -62,7 +62,7 @@ xml.feed "xmlns" => "http://www.w3.org/2005/Atom" do xml.email(author.mail) if author.is_a?(User) && !author.mail.blank? && !author.pref.hide_mail end if author xml.content "type" => "html" do - xml.text! textilizable(item_event, :event_description, :only_path => false) + xml.text! format_text(item_event, :event_description, :only_path => false) end end end diff --git a/app/views/common/preview.html.erb b/app/views/common/preview.html.erb index edb3df7a66da..d483276e48c7 100644 --- a/app/views/common/preview.html.erb +++ b/app/views/common/preview.html.erb @@ -32,7 +32,7 @@ See doc/COPYRIGHT.rdoc for more details. <% if text.blank? %> <%= l(:nothing_to_preview) %> <% else %> - <%= textilizable text, attachments: attachments, object: previewed %> + <%= format_text text, attachments: attachments, object: previewed %> <% end %> <% end %> diff --git a/app/views/journals/index.atom.builder b/app/views/journals/index.atom.builder index 13f7343cc600..9a435e3f3cca 100644 --- a/app/views/journals/index.atom.builder +++ b/app/views/journals/index.atom.builder @@ -52,7 +52,7 @@ xml.feed "xmlns" => "http://www.w3.org/2005/Atom" do xml.text!(content_tag(:li, change_content)) if change_content.present? end xml.text! '' - xml.text! textilizable(change, :notes, :only_path => false) unless change.notes.blank? + xml.text! format_text(change, :notes, :only_path => false) unless change.notes.blank? end end end diff --git a/app/views/journals/preview.html.erb b/app/views/journals/preview.html.erb index 136a7f8eb30a..28fd3499cfe6 100644 --- a/app/views/journals/preview.html.erb +++ b/app/views/journals/preview.html.erb @@ -32,5 +32,5 @@ See doc/COPYRIGHT.rdoc for more details. <%= Journal.human_attribute_name(:notes) %> - <%= textilizable journal.notes, object: journal %> + <%= format_text journal.notes, object: journal %> diff --git a/app/views/messages/show.html.erb b/app/views/messages/show.html.erb index b604cfc3cd06..d8cba9e04a73 100644 --- a/app/views/messages/show.html.erb +++ b/app/views/messages/show.html.erb @@ -57,7 +57,7 @@ See doc/COPYRIGHT.rdoc for more details.

    <%= authoring @topic.created_on, @topic.author %>

    -<%= textilizable(@topic.content, :object => @topic, :attachments => @topic.attachments) %> +<%= format_text(@topic.content, :object => @topic, :attachments => @topic.attachments) %>
    <%= link_to_attachments @topic, :author => false %>
    @@ -97,7 +97,7 @@ See doc/COPYRIGHT.rdoc for more details. :alt => l(:button_delete)) if message.destroyable_by?(User.current) %>
    - <%= textilizable message, :content, :attachments => message.attachments %> + <%= format_text message, :content, :attachments => message.attachments %>
    <%= link_to_attachments message, :author => false %> diff --git a/app/views/news/index.html.erb b/app/views/news/index.html.erb index 131b5e43827d..228802724930 100644 --- a/app/views/news/index.html.erb +++ b/app/views/news/index.html.erb @@ -38,7 +38,7 @@ See doc/COPYRIGHT.rdoc for more details. <%= "(#{l(:label_x_comments, :count => news.comments_count)})" if news.comments_count > 0 %>

    <%= authoring news.created_on, news.author %>

    - <%= textilizable(news.summary.present? ? news.summary : truncate(news.description), :object => news) %> + <%= format_text(news.summary.present? ? news.summary : truncate(news.description), :object => news) %>
    <% end %> <% end %> diff --git a/app/views/news/show.html.erb b/app/views/news/show.html.erb index 6b5d0027cb65..d941ea2d81ab 100644 --- a/app/views/news/show.html.erb +++ b/app/views/news/show.html.erb @@ -60,7 +60,7 @@ See doc/COPYRIGHT.rdoc for more details.

    <% unless @news.summary.blank? %><%=h @news.summary %>
    <% end %> <%= authoring @news.created_on, @news.author %>

    -<%= textilizable(@news.description, :object => @news) %> +<%= format_text(@news.description, :object => @news) %>

    @@ -78,7 +78,7 @@ See doc/COPYRIGHT.rdoc for more details. :alt => l(:button_delete) %>

    <%= avatar(comment.author, :size => "24") %><%= authoring comment.created_on, comment.author %>

    - <%= textilizable(comment.comments, :object => comment) %> + <%= format_text(comment.comments, :object => comment) %> <% end %> diff --git a/app/views/project_associations/confirm_destroy.html.erb b/app/views/project_associations/confirm_destroy.html.erb index 98a766505718..1eb751f9ed65 100644 --- a/app/views/project_associations/confirm_destroy.html.erb +++ b/app/views/project_associations/confirm_destroy.html.erb @@ -38,7 +38,7 @@ See doc/COPYRIGHT.rdoc for more details. :project_a => @project, :project_b => @project_association.project(@project)) %> - <%= textilizable @project_association, :description %> + <%= format_text @project_association, :description %> <%= submit_tag l(:button_delete) %> <%= link_to l(:button_cancel), project_project_associations_path(@project) %> diff --git a/app/views/project_associations/index.html.erb b/app/views/project_associations/index.html.erb index e141efdefcd0..3adb14eb0864 100644 --- a/app/views/project_associations/index.html.erb +++ b/app/views/project_associations/index.html.erb @@ -96,7 +96,7 @@ See doc/COPYRIGHT.rdoc for more details. <% end %> - <%= textilizable association, :description %> + <%= format_text association, :description %> <%= link_to_if_authorized({:controller => '/project_associations', diff --git a/app/views/projects/index.html.erb b/app/views/projects/index.html.erb index ca07002735aa..637779a69b66 100644 --- a/app/views/projects/index.html.erb +++ b/app/views/projects/index.html.erb @@ -46,7 +46,7 @@ See doc/COPYRIGHT.rdoc for more details. <%= render :partial => 'layouts/action_menu_specific' %>
    - <%= textilizable Setting.welcome_text %> + <%= format_text Setting.welcome_text %>
    <% if User.current.logged? %> diff --git a/app/views/projects/show.html.erb b/app/views/projects/show.html.erb index 9f9b09ea1081..f0f417e2dad2 100644 --- a/app/views/projects/show.html.erb +++ b/app/views/projects/show.html.erb @@ -39,7 +39,7 @@ See doc/COPYRIGHT.rdoc for more details.
    - <%= textilizable @project.description %> + <%= format_text @project.description %>
      <% if @project.homepage.present? %> diff --git a/app/views/reportings/_show.html.erb b/app/views/reportings/_show.html.erb index 42ee7d7edb34..b04d2f3446f3 100644 --- a/app/views/reportings/_show.html.erb +++ b/app/views/reportings/_show.html.erb @@ -42,6 +42,6 @@ See doc/COPYRIGHT.rdoc for more details.
      - <%= textilizable(@reporting, :reported_project_status_comment) %> + <%= format_text(@reporting, :reported_project_status_comment) %>

      diff --git a/app/views/reportings/index.html.erb b/app/views/reportings/index.html.erb index b14e509d617f..ab2b38768040 100644 --- a/app/views/reportings/index.html.erb +++ b/app/views/reportings/index.html.erb @@ -66,7 +66,7 @@ See doc/COPYRIGHT.rdoc for more details. <%=h reporting.reported_project_status.try(:name) || "-" %> - <%= textilizable reporting, :reported_project_status_comment %> + <%= format_text reporting, :reported_project_status_comment %> <%= format_date reporting.updated_at %> diff --git a/app/views/repositories/_revisions.html.erb b/app/views/repositories/_revisions.html.erb index f6ebbecce4d5..19daf8e183c8 100644 --- a/app/views/repositories/_revisions.html.erb +++ b/app/views/repositories/_revisions.html.erb @@ -60,7 +60,7 @@ See doc/COPYRIGHT.rdoc for more details. <%=h changeset.author %> - <%= textilizable(truncate_at_line_break(Changeset.to_utf8(changeset.comments, changeset.repository.repo_log_encoding))) %> + <%= format_text(truncate_at_line_break(Changeset.to_utf8(changeset.comments, changeset.repository.repo_log_encoding))) %> <% line_num += 1 %> diff --git a/app/views/repositories/revision.html.erb b/app/views/repositories/revision.html.erb index 16a461d91ae8..acbe96a0fc9e 100644 --- a/app/views/repositories/revision.html.erb +++ b/app/views/repositories/revision.html.erb @@ -55,7 +55,7 @@ See doc/COPYRIGHT.rdoc for more details.

      <% if @changeset.scmid %>ID: <%= h(@changeset.scmid) %>
      <% end %> <%= authoring(@changeset.committed_on, @changeset.author) %>

      -<%= textilizable @changeset.comments %> +<%= format_text @changeset.comments %> <% if @changeset.work_packages.visible.any? %>

      <%= l(:label_related_work_packages) %>

      diff --git a/app/views/user_mailer/_issue_details.html.erb b/app/views/user_mailer/_issue_details.html.erb index af15bfe4c107..3a716357c60a 100644 --- a/app/views/user_mailer/_issue_details.html.erb +++ b/app/views/user_mailer/_issue_details.html.erb @@ -43,4 +43,4 @@ See doc/COPYRIGHT.rdoc for more details. <% end %>
    -<%= textilizable(issue.description, :only_path => false, :object => issue, :project => issue.project) %> +<%= format_text(issue.description, :only_path => false, :object => issue, :project => issue.project) %> diff --git a/app/views/user_mailer/message_posted.html.erb b/app/views/user_mailer/message_posted.html.erb index 4ed68156f25c..22930c461715 100644 --- a/app/views/user_mailer/message_posted.html.erb +++ b/app/views/user_mailer/message_posted.html.erb @@ -32,4 +32,4 @@ See doc/COPYRIGHT.rdoc for more details. <%= @message.author %> -<%= textilizable @message.content %> +<%= format_text @message.content %> diff --git a/app/views/user_mailer/news_added.html.erb b/app/views/user_mailer/news_added.html.erb index 1b0250ca040c..5cf1bdd7fd09 100644 --- a/app/views/user_mailer/news_added.html.erb +++ b/app/views/user_mailer/news_added.html.erb @@ -30,4 +30,4 @@ See doc/COPYRIGHT.rdoc for more details.

    <%= link_to @news.title, news_url(@news) %>

    <%= @news.author.name if @news.author %> -<%= textilizable @news.description %> +<%= format_text @news.description %> diff --git a/app/views/user_mailer/news_comment_added.html.erb b/app/views/user_mailer/news_comment_added.html.erb index f71207040c43..820c8b0327d0 100644 --- a/app/views/user_mailer/news_comment_added.html.erb +++ b/app/views/user_mailer/news_comment_added.html.erb @@ -31,4 +31,4 @@ See doc/COPYRIGHT.rdoc for more details.

    <%= t(:text_user_wrote, :value => @comment.author) %>

    -<%= textilizable @comment.text %> +<%= format_text @comment.text %> diff --git a/app/views/user_mailer/work_package_updated.html.erb b/app/views/user_mailer/work_package_updated.html.erb index 651d6f1964fc..8857d32d19f5 100644 --- a/app/views/user_mailer/work_package_updated.html.erb +++ b/app/views/user_mailer/work_package_updated.html.erb @@ -35,6 +35,6 @@ See doc/COPYRIGHT.rdoc for more details. <% end %> -<%= textilizable(@journal.notes, :only_path => false) %> +<%= format_text(@journal.notes, :only_path => false) %>
    <%= render :partial => 'issue_details', :locals => { :issue => @issue } %> diff --git a/app/views/welcome/index.html.erb b/app/views/welcome/index.html.erb index f91780e0298d..f5396dfb3438 100644 --- a/app/views/welcome/index.html.erb +++ b/app/views/welcome/index.html.erb @@ -29,7 +29,7 @@ See doc/COPYRIGHT.rdoc for more details. <% breadcrumb_paths(nil) %>
    -
    <%= textilizable Setting.welcome_text %>
    +
    <%= format_text Setting.welcome_text %>
    <% if @news.any? %>

    <%=l(:label_news_latest)%>

    @@ -51,7 +51,7 @@ See doc/COPYRIGHT.rdoc for more details. <% @project = project %>
  • <%= link_to_project project %> (<%= format_time(project.created_on) %>) - <%= textilizable project.short_description, :project => project %> + <%= format_text project.short_description, :project => project %>
  • <% end %> <% @project = nil %> diff --git a/app/views/wiki/_content.html.erb b/app/views/wiki/_content.html.erb index 92422cd52f74..d192b3258216 100644 --- a/app/views/wiki/_content.html.erb +++ b/app/views/wiki/_content.html.erb @@ -28,5 +28,5 @@ See doc/COPYRIGHT.rdoc for more details. ++#%>
    - <%= textilizable content, :text, :attachments => content.page.attachments %> + <%= format_text content, :text, :attachments => content.page.attachments %>
    diff --git a/app/views/wiki/_sidebar.html.erb b/app/views/wiki/_sidebar.html.erb index bc77611c54c1..3a429ccacaf8 100644 --- a/app/views/wiki/_sidebar.html.erb +++ b/app/views/wiki/_sidebar.html.erb @@ -28,5 +28,5 @@ See doc/COPYRIGHT.rdoc for more details. ++#%> <% if @wiki && @wiki.sidebar -%> - <%= textilizable @wiki.sidebar.content, :text %> + <%= format_text @wiki.sidebar.content, :text %> <% end -%> diff --git a/app/views/wiki/edit.html.erb b/app/views/wiki/edit.html.erb index acc92da6ccfe..151cd4066820 100644 --- a/app/views/wiki/edit.html.erb +++ b/app/views/wiki/edit.html.erb @@ -34,7 +34,7 @@ See doc/COPYRIGHT.rdoc for more details. <%= error_messages_for 'content' %>

    <%= f.text_area :text, :cols => 100, :rows => 25, :class => 'wiki-edit', :accesskey => accesskey(:edit), - :value => textilizable(@content, :text, :attachments => @content.page.attachments, :edit => true), + :value => format_text(@content, :text, :attachments => @content.page.attachments, :edit => true), :'data-wp_autocomplete_url' => work_packages_auto_complete_path(:project_id => @project, :format => :json) %>


    <%= f.text_field :comments, :size => 120 %>


    <%= render :partial => 'attachments/form' %>

    diff --git a/app/views/wiki/export.html.erb b/app/views/wiki/export.html.erb index 9217d6a52798..02fd107d2f75 100644 --- a/app/views/wiki/export.html.erb +++ b/app/views/wiki/export.html.erb @@ -46,6 +46,6 @@ h1:hover a.wiki-anchor, h2:hover a.wiki-anchor, h3:hover a.wiki-anchor { display -<%= textilizable @content, :text, :wiki_links => :local, :only_path => false %> +<%= format_text @content, :text, :wiki_links => :local, :only_path => false %> diff --git a/app/views/wiki/export_multiple.html.erb b/app/views/wiki/export_multiple.html.erb index 5a59c60c7bb6..487d921026ed 100644 --- a/app/views/wiki/export_multiple.html.erb +++ b/app/views/wiki/export_multiple.html.erb @@ -56,7 +56,7 @@ h1:hover a.wiki-anchor, h2:hover a.wiki-anchor, h3:hover a.wiki-anchor { display <% @pages.each do |page| %>
    -<%= textilizable page.content ,:text, :wiki_links => :anchor %> +<%= format_text page.content ,:text, :wiki_links => :anchor %> <% end %> diff --git a/app/views/work_packages/_changesets.html.erb b/app/views/work_packages/_changesets.html.erb index 2749aaf763a2..94d0e5b2ce21 100644 --- a/app/views/work_packages/_changesets.html.erb +++ b/app/views/work_packages/_changesets.html.erb @@ -33,7 +33,7 @@ See doc/COPYRIGHT.rdoc for more details. :text => "#{l(:label_revision)} #{changeset.format_identifier}") %>
    <%= authoring(changeset.committed_on, changeset.author) %>

    - <%= textilizable(changeset, :comments) %> + <%= format_text(changeset, :comments) %>
    <% end %> diff --git a/app/views/work_packages/show.html.erb b/app/views/work_packages/show.html.erb index a6d382cbd4d4..199a1e1fb22e 100644 --- a/app/views/work_packages/show.html.erb +++ b/app/views/work_packages/show.html.erb @@ -59,7 +59,7 @@ See doc/COPYRIGHT.rdoc for more details.

    <%= WorkPackage.human_attribute_name(:description)%>

    - <%= textilizable work_package, :description, :attachments => work_package.attachments %> + <%= format_text work_package, :description, :attachments => work_package.attachments %>
    <% end %> diff --git a/config/locales/de.yml b/config/locales/de.yml index a555b959077c..ccc3c7e681c1 100644 --- a/config/locales/de.yml +++ b/config/locales/de.yml @@ -1025,6 +1025,9 @@ de: label_keyboard_shortcut_focus_previous_item: "Fokussiere vorheriges Listenelement (nur bei einigen Listen)" label_keyboard_shortcut_focus_next_item: "Fokussiere nächstes Listenelement (nur bei einigen Listen)" + macro_execution_error: "Das Makro %{macro_name} konnte nicht ausgeführt werden" + macro_unavailable: "Makro %{macro_name} kann nicht angezeigt werden" + mail_body_account_activation_request: "Ein neuer Benutzer (%{value}) hat sich registriert. Sein Konto wartet auf Ihre Genehmigung:" mail_body_account_information: "Ihre Konto-Informationen" mail_body_account_information_external: "Sie können sich mit Ihrem Konto %{value} anmelden." diff --git a/config/locales/en.yml b/config/locales/en.yml index ac74be76a028..ce5f23600d75 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -1022,6 +1022,9 @@ en: label_keyboard_shortcut_focus_previous_item: "Focus previous list element (on some lists only)" label_keyboard_shortcut_focus_next_item: "Focus next list element (on some lists only)" + macro_execution_error: "Error executing the macro %{macro_name}" + macro_unavailable: "Macro %{macro_name} cannot be displayed." + mail_body_account_activation_request: "A new user (%{value}) has registered. The account is pending your approval:" mail_body_account_information: "Your account information" mail_body_account_information_external: "You can use your %{value} account to log in." diff --git a/lib/api/v3/activities/activity_model.rb b/lib/api/v3/activities/activity_model.rb index 64ace6288e1a..a39772a7954a 100644 --- a/lib/api/v3/activities/activity_model.rb +++ b/lib/api/v3/activities/activity_model.rb @@ -35,10 +35,28 @@ module V3 module Activities class ActivityModel < Reform::Form include Coercion + include ActionView::Helpers::UrlHelper + include OpenProject::TextFormatting + include OpenProject::StaticRouting::UrlHelpers + include WorkPackagesHelper include GravatarImageTag + # N.B. required by ActionView::Helpers::UrlHelper + def controller; nil; end + property :user_id, type: Integer - property :notes, type: String + + def notes + format_text(raw_notes) + end + + def raw_notes + model.notes + end + + def raw_notes=(value) + model.notes = value + end end end end diff --git a/lib/api/v3/activities/activity_representer.rb b/lib/api/v3/activities/activity_representer.rb index 9d46274ddd1a..f529a2da25db 100644 --- a/lib/api/v3/activities/activity_representer.rb +++ b/lib/api/v3/activities/activity_representer.rb @@ -56,6 +56,7 @@ class ActivityRepresenter < Roar::Decorator property :id, getter: -> (*) { model.id }, render_nil: true property :notes, as: :comment, render_nil: true + property :raw_notes, as: :rawComment, render_nil: true property :details, exec_context: :decorator, render_nil: true property :html_details, exec_context: :decorator, render_nil: true property :version, getter: -> (*) { model.version }, render_nil: true diff --git a/lib/api/v3/work_packages/work_package_model.rb b/lib/api/v3/work_packages/work_package_model.rb index 61487950e38e..14e242777833 100644 --- a/lib/api/v3/work_packages/work_package_model.rb +++ b/lib/api/v3/work_packages/work_package_model.rb @@ -36,12 +36,18 @@ module WorkPackages class WorkPackageModel < Reform::Form include Composition include Coercion + include ActionView::Helpers::UrlHelper + include OpenProject::TextFormatting + include OpenProject::StaticRouting::UrlHelpers + include WorkPackagesHelper include GravatarImageTag + # N.B. required by ActionView::Helpers::UrlHelper + def controller; nil; end + model :work_package property :subject, on: :work_package, type: String - property :description, on: :work_package, type: String property :start_date, on: :work_package, type: Date property :due_date, on: :work_package, type: Date property :created_at, on: :work_package, type: DateTime @@ -56,6 +62,18 @@ def work_package model[:work_package] end + def description + format_text(work_package.description) + end + + def raw_description + work_package.description + end + + def raw_description=(value) + work_package.description = value + end + def type work_package.type.try(:name) end diff --git a/lib/api/v3/work_packages/work_package_representer.rb b/lib/api/v3/work_packages/work_package_representer.rb index d8b6e1764fe2..1d855526a42c 100644 --- a/lib/api/v3/work_packages/work_package_representer.rb +++ b/lib/api/v3/work_packages/work_package_representer.rb @@ -76,6 +76,7 @@ def initialize(options = {}, *expand) property :subject, render_nil: true property :type, render_nil: true property :description, render_nil: true + property :raw_description, render_nil: true property :status, render_nil: true property :priority, render_nil: true property :start_date, getter: -> (*) { work_package.start_date }, render_nil: true diff --git a/lib/open_project/object_linking.rb b/lib/open_project/object_linking.rb new file mode 100644 index 000000000000..5453317f9610 --- /dev/null +++ b/lib/open_project/object_linking.rb @@ -0,0 +1,128 @@ +#-- encoding: UTF-8 +#-- copyright +# OpenProject is a project management system. +# Copyright (C) 2012-2014 the OpenProject Foundation (OPF) +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# 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. +# +# 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. +# +# 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. +# +# See doc/COPYRIGHT.rdoc for more details. +#++ + +module OpenProject + module ObjectLinking + + # Displays a link to user's account page if active or registered + def link_to_user(user, options={}) + if user.is_a?(User) + name = user.name(options.delete(:format)) + if user.active? || user.registered? + link_to(name, user, options) + else + name + end + else + h(user.to_s) + end + end + + def link_to_work_package_preview(context = nil, options = {}) + form_id = options[:form_id] || 'work_package-form-preview' + path = (context.is_a? WorkPackage) ? preview_work_package_path(context) + : preview_work_packages_path + + preview_link path, form_id, { class: 'preview button' } + end + + # 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' + only_path = options.delete(:only_path) { true } + + link_to h(text), + {:controller => '/attachments', + :action => action, + :id => attachment, + :filename => attachment.filename, + :host => Setting.host_name, + :protocol => Setting.protocol, + :only_path => only_path }, + options + end + + # Generates a link to a SCM revision + # Options: + # * :text - Link text (default to the formatted revision) + def link_to_revision(revision, project, options={}) + text = options.delete(:text) || format_revision(revision) + rev = revision.respond_to?(:identifier) ? revision.identifier : revision + + link_to(h(text), {:controller => '/repositories', :action => 'revision', :project_id => project, :rev => rev}, + :title => l(:label_revision_id, format_revision(revision))) + end + + # Generates a link to a message + def link_to_message(message, options={}, html_options = nil) + link_to( + h(truncate(message.subject, :length => 60)), + topic_path(message.root, + { :r => (message.parent_id && message.id), + :anchor => (message.parent_id ? "message-#{message.id}" : nil) + }.merge(options)), + html_options + ) + end + + # Generates a link to a project if active + # Examples: + # + # 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, show_icon = false) + link = '' + project_link_name = project.name + + if show_icon && User.current.member_of?(project) + project_link_name = icon_wrapper("icon-context icon-star1",I18n.t(:description_my_project).html_safe + " ".html_safe) + project_link_name + end + + if project.active? + # backwards compatibility + if options.delete(:action) == 'settings' + link << link_to(project_link_name, settings_project_path(project, options), html_options) + else + link << link_to(project_link_name, project_path(project, options), html_options) + end + else + link << project_link_name + end + + link.html_safe + end + end +end diff --git a/lib/open_project/text_formatting.rb b/lib/open_project/text_formatting.rb new file mode 100644 index 000000000000..774cc924600c --- /dev/null +++ b/lib/open_project/text_formatting.rb @@ -0,0 +1,411 @@ +#-- encoding: UTF-8 +#-- copyright +# OpenProject is a project management system. +# Copyright (C) 2012-2014 the OpenProject Foundation (OPF) +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# 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. +# +# 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. +# +# 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. +# +# See doc/COPYRIGHT.rdoc for more details. +#++ + +module OpenProject + module TextFormatting + extend ActiveSupport::Concern + extend DeprecatedAlias + + include Redmine::WikiFormatting::Macros::Definitions + include ActionView::Helpers::SanitizeHelper + include ERB::Util # for h() + include Redmine::I18n + include ActionView::Helpers::TextHelper + include OpenProject::ObjectLinking + + # Truncates and returns the string as a single line + def truncate_single_line(string, *args) + truncate(string.to_s, *args).gsub(%r{[\r\n]+}m, ' ') + end + + # 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 + + # Formats text according to system settings. + # 2 ways to call this method: + # * with a String: format_text(text, options) + # * with an object and one of its attribute: format_text(issue, :description, options) + def format_text(*args) + options = args.last.is_a?(Hash) ? args.pop : {} + case args.size + when 1 + obj = options[:object] + text = args.shift + when 2 + obj = args.shift + attr = args.shift + text = obj.send(attr).to_s + else + raise ArgumentError, 'invalid arguments to format_text' + end + return '' if text.blank? + + edit = !!options.delete(:edit) + # don't return html in edit mode when textile or text formatting is enabled + return text if edit + project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil) + only_path = options.delete(:only_path) == false ? false : true + + text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text, :object => obj, :attribute => attr, :edit => edit) { |macro, args| exec_macro(macro, obj, args, :view => self, :edit => edit) } + + #TODO: transform modifications into WikiFormatting Helper, or at least ask the helper if he wants his stuff to be modified + @parsed_headings = [] + text = parse_non_pre_blocks(text) do |text| + [:parse_inline_attachments, :parse_wiki_links, :parse_redmine_links, :parse_headings, :parse_relative_urls].each do |method_name| + send method_name, text, project, obj, attr, only_path, options + end + end + + if @parsed_headings.any? + replace_toc(text, @parsed_headings) + end + + text.html_safe + end + deprecated_alias :textilizable, :format_text + deprecated_alias :textilize, :format_text + + + def parse_non_pre_blocks(text) + 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 + 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 + # Close any non closing tags + while tag = tags.pop + parsed << "" + end + parsed + end + + RELATIVE_LINK_RE = %r{ +
    ]+?)')| + (?:"(\/[^>]+?)") + ) + )| + [^>] + )* + > + [^<]*?<\/a> # content and closing link tag. + }x unless const_defined?(:RELATIVE_LINK_RE) + + def parse_relative_urls(text, project, obj, attr, only_path, options) + return if only_path + text.gsub!(RELATIVE_LINK_RE) do |m| + href, relative_url = $1, $2 || $3 + next m unless href.present? + if defined?(request) && request.present? + # we have a request! + protocol, host_with_port = request.protocol, request.host_with_port + elsif @controller + # use the same methods as url_for in the Mailer + url_opts = @controller.class.default_url_options + next m unless url_opts && url_opts[:protocol] && url_opts[:host] + protocol, host_with_port = "#{url_opts[:protocol]}://", url_opts[:host] + else + next m + end + m.sub href, " href=\"#{protocol}#{host_with_port}#{relative_url}\"" + end + end + + def parse_inline_attachments(text, project, obj, attr, only_path, options) + # when using an image link, try to use an attachment, if possible + if options[:attachments] || (obj && obj.respond_to?(:attachments)) + attachments = nil + text.gsub!(/src="([^\/"]+\.(bmp|gif|jpg|jpeg|png))"(\s+alt="([^"]*)")?/i) do |m| + filename, ext, alt, alttext = $1.downcase, $2, $3, $4 + attachments ||= (options[:attachments] || obj.attachments).sort_by(&:created_on).reverse + # search for the picture in attachments + if found = attachments.detect { |att| att.filename.downcase == filename } + image_url = url_for :only_path => only_path, :controller => '/attachments', :action => 'download', :id => found + desc = found.description.to_s.gsub('"', '') + if !desc.blank? && alttext.blank? + alt = " title=\"#{desc}\" alt=\"#{desc}\"" + end + "src=\"#{image_url}\"#{alt}" + else + m + end + end + end + end + + # 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| + link_project = project + esc, all, page, title = $1, $2, $3, $5 + if esc.nil? + if page =~ /\A([^\:]+)\:(.*)\z/ + link_project = Project.find_by_identifier($1) || Project.find_by_name($1) + page = $2 + title ||= $1 if page.blank? + end + + if link_project && link_project.wiki + # extract anchor + anchor = nil + if page =~ /\A(.+?)\#(.+)\z/ + page, anchor = $1, $2 + end + # check if page exists + wiki_page = link_project.wiki.find_page(page) + url = case options[:wiki_links] + when :local; "#{title}.html" + when :anchor; "##{title}" # used for single-file wiki export + else + wiki_page_id = page.present? ? Wiki.titleize(page) : nil + url_for(:only_path => only_path, :controller => '/wiki', :action => 'show', :project_id => link_project, :id => wiki_page_id, :anchor => anchor) + end + link_to(h(title || page), url, :class => ('wiki-page' + (wiki_page ? '' : ' new'))) + else + # project or wiki doesn't exist + all + end + else + all + end + end + end + + # 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 + # Forum messages: + # message#1218 -> Link to message with id 1218 + # + # 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 + def parse_redmine_links(text, project, obj, attr, only_path, options) + text.gsub!(%r{([\s\(,\-\[\>]|^)(!)?(([a-z0-9\-_]+):)?(attachment|version|commit|source|export|message|project)?((#+|r)(\d+)|(:)([^"\s<>][^\s<>]*?|"[^"]+?"))(?=(?=[[:punct:]]\W)|,|\s|\]|<|$)}) do |m| + leading, esc, project_prefix, project_identifier, prefix, sep, identifier = $1, $2, $3, $4, $5, $7 || $9, $8 || $10 + link = nil + if project_identifier + project = Project.visible.find_by_identifier(project_identifier) + end + if esc.nil? + if prefix.nil? && sep == 'r' + # project.changesets.visible raises an SQL error because of a double join on repositories + if project && project.repository && (changeset = Changeset.visible.find_by_repository_id_and_revision(project.repository.id, identifier)) + link = link_to(h("#{project_prefix}r#{identifier}"), {:only_path => only_path, :controller => '/repositories', :action => 'revision', :project_id => project, :rev => changeset.revision}, + :class => 'changeset', + :title => truncate_single_line(changeset.comments, :length => 100)) + end + elsif sep == '#' + oid = identifier.to_i + case prefix + when nil + if work_package = WorkPackage.visible.find_by_id(oid, :include => :status) + link = link_to("##{oid}", work_package_path(:id => oid, :only_path => only_path), + :class => work_package_css_classes(work_package), + :title => "#{truncate(work_package.subject, :length => 100)} (#{work_package.status.try(:name)})") + end + when 'version' + if version = Version.visible.find_by_id(oid) + link = link_to h(version.name), {:only_path => only_path, :controller => '/versions', :action => 'show', :id => version}, + :class => 'version' + end + when 'message' + if message = Message.visible.find_by_id(oid, :include => :parent) + link = link_to_message(message, {:only_path => only_path}, :class => 'message') + end + when 'project' + if p = Project.visible.find_by_id(oid) + link = link_to_project(p, {:only_path => only_path}, :class => 'project') + end + end + elsif sep == '##' + oid = identifier.to_i + if work_package = WorkPackage.visible.find_by_id(oid, :include => :status) + link = work_package_quick_info(work_package) + end + elsif sep == '###' + oid = identifier.to_i + work_package = WorkPackage.visible.find_by_id(oid, :include => :status) + if work_package && obj && !(attr == :description && obj.id == work_package.id) + link = work_package_quick_info_with_description(work_package) + end + elsif sep == ':' + # removes the double quotes if any + name = identifier.gsub(%r{\A"(.*)"\z}, "\\1") + case prefix + when 'version' + if project && version = project.versions.visible.find_by_name(name) + link = link_to h(version.name), {:only_path => only_path, :controller => '/versions', :action => 'show', :id => version}, + :class => 'version' + end + when 'commit' + if project && project.repository && (changeset = Changeset.visible.find(:first, :conditions => ["repository_id = ? AND scmid LIKE ?", project.repository.id, "#{name}%"])) + link = link_to h("#{project_prefix}#{name}"), {:only_path => only_path, :controller => '/repositories', :action => 'revision', :project_id => project, :rev => changeset.identifier}, + :class => 'changeset', + :title => truncate_single_line(h(changeset.comments), :length => 100) + end + when 'source', 'export' + if project && project.repository && User.current.allowed_to?(:browse_repository, project) + name =~ %r{\A[/\\]*(.*?)(@([0-9a-f]+))?(#(L\d+))?\z} + path, rev, anchor = $1, $3, $5 + link = link_to h("#{project_prefix}#{prefix}:#{name}"), {:controller => '/repositories', :action => 'entry', :project_id => project, + :path => path.to_s, + :rev => rev, + :anchor => anchor, + :format => (prefix == 'export' ? 'raw' : nil)}, + :class => (prefix == 'export' ? 'source download' : 'source') + end + when 'attachment' + attachments = options[:attachments] || (obj && obj.respond_to?(:attachments) ? obj.attachments : nil) + if attachments && attachment = attachments.detect {|a| a.filename == name } + link = link_to h(attachment.filename), {:only_path => only_path, :controller => '/attachments', :action => 'download', :id => attachment}, + :class => 'attachment' + end + when 'project' + if p = Project.visible.find(:first, :conditions => ["identifier = :s OR LOWER(name) = :s", {:s => name.downcase}]) + link = link_to_project(p, {:only_path => only_path}, :class => 'project') + end + end + end + end + leading + (link || "#{project_prefix}#{prefix}#{sep}#{identifier}") + end + end + + HEADING_RE = /]+)?>(.+?)<\/h(1|2|3|4)>/i unless const_defined?(:HEADING_RE) + + # Headings and TOC + # Adds ids and links to headings unless options[:headings] is set to false + def parse_headings(text, project, obj, attr, only_path, options) + return if options[:headings] == false + + text.gsub!(HEADING_RE) do + level, attrs, content = $1.to_i, $2, $3 + item = strip_tags(content).strip + anchor = item.gsub(%r{[^\w\s\-]}, '').gsub(%r{\s+(\-+\s*)?}, '-') + @parsed_headings << [level, anchor, item] + "\n#{content}" + end + end + + TOC_RE = /

    \{\{([<>]?)toc\}\}<\/p>/i unless const_defined?(:TOC_RE) + + # Renders the TOC with given headings + def replace_toc(text, headings) + text.gsub!(TOC_RE) do + if headings.empty? + '' + else + div_class = 'toc' + div_class << ' right' if $1 == '>' + div_class << ' left' if $1 == '<' + out = "

    #{l(:label_table_of_contents)}
    " + out << "
    • " + root = headings.map(&:first).min + current = root + started = false + headings.each do |level, anchor, item| + if level > current + out << '
      • ' * (level - current) + elsif level < current + out << "
      \n" * (current - level) + "
    • " + elsif started + out << '
    • ' + end + out << "#{item}" + current = level + started = true + end + out << '
    ' * (current - root) + out << '' + out << '
    ' + end + end + end + + end +end diff --git a/lib/open_project/wiki_formatting/macros/default.rb b/lib/open_project/wiki_formatting/macros/default.rb index 43aeccf2f2c6..fa7004a747d7 100644 --- a/lib/open_project/wiki_formatting/macros/default.rb +++ b/lib/open_project/wiki_formatting/macros/default.rb @@ -48,7 +48,7 @@ module Default available_macros.keys.collect(&:to_s).sort.each do |macro| out << content_tag('dt', content_tag('code', macro)) - out << content_tag('dd', textilizable(available_macros[macro.to_sym])) + out << content_tag('dd', format_text(available_macros[macro.to_sym])) end content_tag('dl', out.html_safe) end @@ -85,7 +85,7 @@ module Default @included_wiki_pages ||= [] raise 'Circular inclusion detected' if @included_wiki_pages.include?(page.title) @included_wiki_pages << page.title - out = textilizable(page.content, :text, :attachments => page.attachments, :headings => false) + out = format_text(page.content, :text, :attachments => page.attachments, :headings => false) @included_wiki_pages.pop out end diff --git a/lib/open_project/wiki_formatting/macros/timelines_wiki_macro.rb b/lib/open_project/wiki_formatting/macros/timelines_wiki_macro.rb index 04113b3d9bea..86dbd848a529 100644 --- a/lib/open_project/wiki_formatting/macros/timelines_wiki_macro.rb +++ b/lib/open_project/wiki_formatting/macros/timelines_wiki_macro.rb @@ -40,8 +40,12 @@ def apply(content, args, options={}) view = options[:view] - view.render :partial => '/timelines/timeline', - :locals => {:timeline => timeline} + if view.respond_to?(:render) + view.render :partial => '/timelines/timeline', + :locals => {:timeline => timeline} + else + raise NotImplementedError, 'Timeline rendering is not supported' + end end end end diff --git a/lib/redmine/wiki_formatting.rb b/lib/redmine/wiki_formatting.rb index df81567b3cc6..57877ba3987f 100644 --- a/lib/redmine/wiki_formatting.rb +++ b/lib/redmine/wiki_formatting.rb @@ -103,7 +103,9 @@ def execute_macros(text, macros_runner) begin macros_runner.call(macro, args) rescue => e - "
    Error executing the #{macro} macro (#{e})
    " + "
    #{::I18n.t(:macro_execution_error, macro_name: macro)} (#{e})
    " + rescue NotImplementedError + "
    #{::I18n.t(:macro_unavailable, macro_name: macro)}
    " end || all else all diff --git a/public/templates/work_packages/tabs/_user_activity.html b/public/templates/work_packages/tabs/_user_activity.html index d8aa2f47e28c..898c9c2edc7a 100644 --- a/public/templates/work_packages/tabs/_user_activity.html +++ b/public/templates/work_packages/tabs/_user_activity.html @@ -5,7 +5,9 @@ commented on - {{ activity.props.comment }} +
    • diff --git a/public/templates/work_packages/tabs/overview.html b/public/templates/work_packages/tabs/overview.html index 7bd1b944dbc7..8845299c4082 100644 --- a/public/templates/work_packages/tabs/overview.html +++ b/public/templates/work_packages/tabs/overview.html @@ -1,19 +1,10 @@

      Description

      +
      -

      - - -

      +
      - -
      diff --git a/spec/api/representers/activity_representer_spec.rb b/spec/api/representers/activity_representer_spec.rb index 50a93f95a5e6..c7ed187d6080 100644 --- a/spec/api/representers/activity_representer_spec.rb +++ b/spec/api/representers/activity_representer_spec.rb @@ -48,6 +48,7 @@ it { should have_json_path('id') } it { should have_json_path('version') } it { should have_json_path('comment') } + it { should have_json_path('rawComment') } it { should have_json_path('details') } it { should have_json_path('htmlDetails') } it { should have_json_path('createdAt') } diff --git a/spec/api/work_package_resource_spec.rb b/spec/api/work_package_resource_spec.rb index 5189eaad048b..ebc66fdcb76b 100644 --- a/spec/api/work_package_resource_spec.rb +++ b/spec/api/work_package_resource_spec.rb @@ -3,11 +3,38 @@ describe 'API v3 Work package resource' do include Rack::Test::Methods + include Capybara::RSpecMatchers + + let!(:timeline) { FactoryGirl.create(:timeline, project_id: project.id) } + let!(:other_wp) { FactoryGirl.create(:work_package, project_id: project.id) } + let(:work_package) { FactoryGirl.create(:work_package, project_id: project.id, + description: description + )} + let(:description) {%{ +{{>toc}} + +h1. OpenProject Masterplan for 2015 + +h2. three point plan + +# One ###{other_wp.id} +# Two +# Three + +h3. random thoughts + +h4. things we like + +* Pointed +* Relaxed +* Debonaire + +{{timeline(#{timeline.id})}} + }} - let(:work_package) { FactoryGirl.create(:work_package, :project_id => project.id) } let(:project) { FactoryGirl.create(:project, :identifier => 'test_project', :is_public => false) } let(:current_user) { FactoryGirl.create(:user) } - let(:role) { FactoryGirl.create(:role, permissions: [:view_work_packages]) } + let(:role) { FactoryGirl.create(:role, permissions: [:view_work_packages, :view_timelines]) } let(:unauthorize_user) { FactoryGirl.create(:user) } let(:type) { FactoryGirl.create(:type) } @@ -66,9 +93,27 @@ last_response.status.should eq(200) end - it 'should respond with work package in HAL+JSON format' do - parsed_response = JSON.parse(last_response.body) - parsed_response['id'].should eq(work_package.id) + describe 'response body' do + subject(:parsed_response) { JSON.parse(last_response.body) } + + it 'should respond with work package in HAL+JSON format' do + expect(parsed_response['id']).to eq(work_package.id) + end + + its(['description']) { should have_selector('h1') } + its(['description']) { should have_selector('h2') } + + it 'should resolve links' do + expect(parsed_response['description']).to have_selector("a[href='/work_packages/#{other_wp.id}']") + end + + it 'should resolve simple macros' do + expect(parsed_response['description']).to have_text('Table of Contents') + end + + it 'should not resolve/show complex macros' do + expect(parsed_response['description']).to have_text('Macro timeline cannot be displayed.') + end end context 'requesting nonexistent work package' do diff --git a/spec/helpers/application_helper_spec.rb b/spec/helpers/application_helper_spec.rb index d30a21557920..6b89e831e609 100644 --- a/spec/helpers/application_helper_spec.rb +++ b/spec/helpers/application_helper_spec.rb @@ -149,437 +149,6 @@ end end - describe ".textilizable" do - let(:project) { FactoryGirl.create :valid_project } - let(:identifier) { project.identifier } - let(:project_member) { FactoryGirl.create :user, - :member_in_project => project, - :member_through_role => FactoryGirl.create(:role, - :permissions => [:view_work_packages, :edit_work_packages, - :browse_repository, :view_changesets, :view_wiki_pages]) } - let(:issue) { FactoryGirl.create :work_package, - :project => project, - :author => project_member, - :type => project.types.first } - - before do - @project = project - - allow(User).to receive(:current).and_return(project_member) - - Setting.enabled_scm = Setting.enabled_scm << "Filesystem" unless Setting.enabled_scm.include? "Filesystem" - end - - after do - User.unstub(:current) - - Setting.enabled_scm.delete "Filesystem" - end - - context "Changeset links" do - let(:repository) { FactoryGirl.create :repository, :project => project } - let(:changeset1) { FactoryGirl.create :changeset, - :repository => repository, - :comments => 'My very first commit' } - let(:changeset2) { FactoryGirl.create :changeset, - :repository => repository, - :comments => 'This commit fixes #1, #2 and references #1 & #3' } - let(:changeset_link) { link_to("r#{changeset1.revision}", - {:controller => 'repositories', :action => 'revision', :project_id => identifier, :rev => changeset1.revision}, - :class => 'changeset', :title => 'My very first commit') } - let(:changeset_link2) { link_to("r#{changeset2.revision}", - {:controller => 'repositories', :action => 'revision', :project_id => identifier, :rev => changeset2.revision}, - :class => 'changeset', :title => 'This commit fixes #1, #2 and references #1 & #3') } - - before do - project.repository = repository - end - - context "Single link" do - subject { textilizable("r#{changeset1.revision}") } - - it { should eq("

      #{changeset_link}

      ") } - end - - context "Single link with dot" do - subject { textilizable("r#{changeset1.revision}.") } - - it { should eq("

      #{changeset_link}.

      ") } - end - - context "Two links comma separated" do - subject { textilizable("r#{changeset1.revision}, r#{changeset2.revision}") } - - it { should eq("

      #{changeset_link}, #{changeset_link2}

      ") } - end - - context "Single link comma separated without a space" do - subject { textilizable("r#{changeset1.revision},r#{changeset2.revision}") } - - it { should eq("

      #{changeset_link},#{changeset_link2}

      ") } - end - - context "Escaping" do - subject { textilizable("!r#{changeset1.id}") } - - it { should eq("

      r#{changeset1.id}

      ") } - end - end - - context "Version link" do - let(:version) { FactoryGirl.create :version, - :name => '1.0', - :project => project } - let(:version_link) { link_to('1.0', - {:controller => 'versions', :action => 'show', :id => version.id}, - :class => 'version') } - - context "Link with version id" do - subject { textilizable("version##{version.id}") } - - it { should eq("

      #{version_link}

      ") } - end - - context "Link with version" do - subject { textilizable("version:1.0") } - it { should eq("

      #{version_link}

      ") } - end - - context "Link with quoted version" do - subject { textilizable('version:"1.0"') } - - it { should eq("

      #{version_link}

      ") } - end - - context "Escaping link with version id" do - subject { textilizable("!version##{version.id}") } - - it { should eq("

      version##{version.id}

      ") } - end - - context "Escaping link with version" do - subject { textilizable("!version:1.0") } - - it { should eq("

      version:1.0

      ") } - end - - context "Escaping link with quoted version" do - subject { textilizable('!version:"1.0"') } - - it { should eq('

      version:"1.0"

      ') } - end - end - - context "Message links" do - let(:board) { FactoryGirl.create :board, :project => project } - let(:message1) { FactoryGirl.create :message, :board => board } - let(:message2) { FactoryGirl.create :message, - :board => board, - :parent => message1 } - - before do - message1.reload - end - - context "Plain message" do - subject { textilizable("message##{message1.id}") } - - it { should eq("

      #{link_to(message1.subject, topic_path(message1), :class => 'message')}

      ") } - end - - context "Message with parent" do - subject { textilizable("message##{message2.id}") } - - it { should eq("

      #{link_to(message2.subject, topic_path(message1, :anchor => "message-#{message2.id}", :r => message2.id), :class => 'message')}

      ") } - end - end - - context "Issue links" do - let(:issue_link) { link_to("##{issue.id}", - work_package_path(issue), - :class => 'issue work_package status-3 priority-1 created-by-me', :title => "#{issue.subject} (#{issue.status})") } - - context "Plain issue link" do - subject { textilizable("##{issue.id}, [##{issue.id}], (##{issue.id}) and ##{issue.id}.") } - - it { should eq("

      #{issue_link}, [#{issue_link}], (#{issue_link}) and #{issue_link}.

      ") } - end - - context "Plain issue link to non-existing element" do - subject { textilizable('#0123456789') } - - it { should eq('

      #0123456789

      ') } - end - - context "Escaping issue link" do - subject { textilizable("!##{issue.id}.") } - - it { should eq("

      ##{issue.id}.

      ") } - end - - context "Cyclic Description Links" do - let(:issue2) { FactoryGirl.create :work_package, - :project => project, - :author => project_member, - :type => project.types.first } - - before do - issue2.description = "####{issue.id}" - issue2.save! - issue.description = "####{issue2.id}" - issue.save! - end - - subject { textilizable issue, :description } - - it "doesn't replace description links with a cycle" do - expect(subject).to match("###{issue.id}") - end - end - - context "Description links" do - subject { textilizable issue, :description } - - it "replaces the macro with the issue description" do - expect(subject).to eq("

      #{issue.description}

      ") - end - end - end - - context "Project links" do - let(:subproject) { FactoryGirl.create :valid_project, :parent => project, :is_public => true } - let(:project_url) { {:controller => 'projects', :action => 'show', :id => subproject.identifier} } - - context "Plain project link" do - subject { textilizable("project##{subproject.id}") } - - it { should eq("

      #{link_to(subproject.name, project_url, :class => 'project')}

      ") } - end - - context "Plain project link via identifier" do - subject { textilizable("project:#{subproject.identifier}") } - - it { should eq("

      #{link_to(subproject.name, project_url, :class => 'project')}

      ") } - end - - context "Plain project link via name" do - subject { textilizable("project:\"#{subproject.name}\"") } - - it { should eq("

      #{link_to(subproject.name, project_url, :class => 'project')}

      ") } - end - end - - context "Url links" do - subject { textilizable("http://foo.bar/FAQ#3") } - - it { should eq('

      http://foo.bar/FAQ#3

      ') } - end - - context "Wiki links" do - let(:project_2) { FactoryGirl.create :valid_project, - :identifier => 'onlinestore' } - let(:wiki_1) { FactoryGirl.create :wiki, - :start_page => "CookBook documentation", - :project => project } - let(:wiki_page_1_1) { FactoryGirl.create :wiki_page_with_content, - :wiki => wiki_1, - :title => "CookBook_documentation" } - let(:wiki_page_1_2) { FactoryGirl.create :wiki_page_with_content, - :wiki => wiki_1, - :title => "Another page" } - - before do - project_2.reload - - wiki_page_2_1 = FactoryGirl.create :wiki_page_with_content, - :wiki => project_2.wiki, - :title => "Start_page" - - project_2.wiki.pages << wiki_page_2_1 - project_2.wiki.start_page = "Start Page" - project_2.wiki.save! - - project.wiki = wiki_1 - - wiki_1.pages << wiki_page_1_1 - wiki_1.pages << wiki_page_1_2 - end - - context "Plain wiki link" do - subject { textilizable('[[CookBook documentation]]') } - - it { should eq("

      CookBook documentation

      ") } - end - - context "Plain wiki page link" do - subject { textilizable('[[Another page|Page]]') } - - it { should eq("

      Page

      ") } - end - - context "Wiki link with anchor" do - subject { textilizable('[[CookBook documentation#One-section]]') } - - it { should eq("

      CookBook documentation

      ") } - end - - context "Wiki page link with anchor" do - subject { textilizable('[[Another page#anchor|Page]]') } - - it { should eq("

      Page

      ") } - end - - context "Wiki link to an unknown page" do - subject { textilizable('[[Unknown page]]') } - - it { should eq("

      Unknown page

      ") } - end - - context "Wiki page link to an unknown page" do - subject { textilizable('[[Unknown page|404]]') } - - it { should eq("

      404

      ") } - end - - context "Link to another project's wiki" do - subject { textilizable('[[onlinestore:]]') } - - it { should eq("

      onlinestore

      ") } - end - - context "Link to another project's wiki with label" do - subject { textilizable('[[onlinestore:|Wiki]]') } - - it { should eq("

      Wiki

      ") } - end - - context "Link to another project's wiki page" do - subject { textilizable('[[onlinestore:Start page]]') } - - it { should eq("

      Start page

      ") } - end - - context "Link to another project's wiki page with label" do - subject { textilizable('[[onlinestore:Start page|Text]]') } - - it { should eq("

      Text

      ") } - end - - context "Link to an unknown wiki page in another project" do - subject { textilizable('[[onlinestore:Unknown page]]') } - - it { should eq("

      Unknown page

      ") } - end - - context "Struck through link to wiki page" do - subject { textilizable('-[[Another page|Page]]-') } - - it { should eql("

      Page

      ") } - end - - context "Named struck through link to wiki page" do - subject { textilizable('-[[Another page|Page]] link-') } - - it { should eql("

      Page link

      ") } - end - - context "Escaped link to wiki page" do - subject { textilizable('![[Another page|Page]]') } - - it { should eql('

      [[Another page|Page]]

      ') } - end - - context "Link to wiki of non-existing project" do - subject { textilizable('[[unknowproject:Start]]') } - - it { should eql('

      [[unknowproject:Start]]

      ') } - end - - context "Link to wiki page of non-existing project" do - subject { textilizable('[[unknowproject:Start|Page title]]') } - - it { should eql('

      [[unknowproject:Start|Page title]]

      ') } - end - end - - context "Redmine links" do - let(:repository) { FactoryGirl.create :repository, :project => project } - let(:source_url) { {:controller => 'repositories', :action => 'entry', :project_id => identifier, :path => 'some/file'} } - let(:source_url_with_ext) { {:controller => 'repositories', :action => 'entry', :project_id => identifier, :path => 'some/file.ext'} } - - before do - project.repository = repository - - @to_test = { - # source - 'source:/some/file' => link_to('source:/some/file', source_url, :class => 'source'), - 'source:/some/file.' => link_to('source:/some/file', source_url, :class => 'source') + ".", - 'source:/some/file.ext.' => link_to('source:/some/file.ext', source_url_with_ext, :class => 'source') + ".", - 'source:/some/file. ' => link_to('source:/some/file', source_url, :class => 'source') + ".", - 'source:/some/file.ext. ' => link_to('source:/some/file.ext', source_url_with_ext, :class => 'source') + ".", - 'source:/some/file, ' => link_to('source:/some/file', source_url, :class => 'source') + ",", - 'source:/some/file@52' => link_to('source:/some/file@52', source_url.merge(:rev => 52), :class => 'source'), - 'source:/some/file.ext@52' => link_to('source:/some/file.ext@52', source_url_with_ext.merge(:rev => 52), :class => 'source'), - 'source:/some/file#L110' => link_to('source:/some/file#L110', source_url.merge(:anchor => 'L110'), :class => 'source'), - 'source:/some/file.ext#L110' => link_to('source:/some/file.ext#L110', source_url_with_ext.merge(:anchor => 'L110'), :class => 'source'), - 'source:/some/file@52#L110' => link_to('source:/some/file@52#L110', source_url.merge(:rev => 52, :anchor => 'L110'), :class => 'source'), - 'export:/some/file' => link_to('export:/some/file', source_url.merge(:format => 'raw'), :class => 'source download'), - # escaping - '!source:/some/file' => 'source:/some/file', - # invalid expressions - 'source:' => 'source:' - } - end - - it "" do - @to_test.each do |text, result| - expect(textilizable(text)).to eql("

      #{result}

      ") - end - end - end - - context "Pre content should not parse wiki and redmine links" do - let(:wiki) { FactoryGirl.create :wiki, - :start_page => "CookBook documentation", - :project => project } - let(:wiki_page) { FactoryGirl.create :wiki_page_with_content, - :wiki => wiki, - :title => "CookBook_documentation" } - let(:raw) { <<-RAW -[[CookBook documentation]] - -##{issue.id} - -
      -[[CookBook documentation]]
      -
      -##{issue.id}
      -
      -RAW - } - - let(:expected) { <<-EXPECTED -

      CookBook documentation

      -

      ##{issue.id}

      -
      -[[CookBook documentation]]
      -
      -##{issue.id}
      -
      -EXPECTED - } - - before do - project.wiki = wiki - wiki.pages << wiki_page - end - - subject { textilizable(raw).gsub(%r{[\r\n\t]}, '')} - - it { should eql(expected.gsub(%r{[\r\n\t]}, ''))} - end - end - describe "other_formats_links" do context "link given" do before do diff --git a/spec/lib/api/v3/activities/activity_model_spec.rb b/spec/lib/api/v3/activities/activity_model_spec.rb new file mode 100644 index 000000000000..051e1f2b4018 --- /dev/null +++ b/spec/lib/api/v3/activities/activity_model_spec.rb @@ -0,0 +1,58 @@ +#-- copyright +# OpenProject is a project management system. +# Copyright (C) 2012-2014 the OpenProject Foundation (OPF) +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# 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. +# +# 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. +# +# 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. +# +# See doc/COPYRIGHT.rdoc for more details. +#++ + +require 'spec_helper' + +describe ::API::V3::Activities::ActivityModel do + include Capybara::RSpecMatchers + + subject(:model) { ::API::V3::Activities::ActivityModel.new(journal) } + let(:journal) { FactoryGirl.build(:work_package_journal, attributes) } + + context 'with a formatted description' do + let(:attributes) { + { + notes: <<-DESC +h3. Plan update + +# More done +# More quickly + DESC + } + } + + its(:notes) { should have_selector 'h3' } + its(:notes) { should have_selector 'ol > li' } + its(:raw_notes) { should eq attributes[:notes] } + + it 'should allow raw_notes to be set' do + model.raw_notes = 'h4. Plan revision' + expect(model.notes).to have_selector 'h4' + end + end +end diff --git a/spec/lib/api/v3/work_packages/work_package_model_spec.rb b/spec/lib/api/v3/work_packages/work_package_model_spec.rb new file mode 100644 index 000000000000..bb8eb6a60b3d --- /dev/null +++ b/spec/lib/api/v3/work_packages/work_package_model_spec.rb @@ -0,0 +1,61 @@ +#-- copyright +# OpenProject is a project management system. +# Copyright (C) 2012-2014 the OpenProject Foundation (OPF) +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# 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. +# +# 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. +# +# 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. +# +# See doc/COPYRIGHT.rdoc for more details. +#++ + +require 'spec_helper' + +describe ::API::V3::WorkPackages::WorkPackageModel do + include Capybara::RSpecMatchers + + subject(:model) { ::API::V3::WorkPackages::WorkPackageModel.new( + work_package: work_package + ) + } + let(:work_package) { FactoryGirl.build(:work_package, attributes) } + + context 'with a formatted description' do + let(:attributes) { + { + description: <<-DESC +h2. Plan for this month + +# Important bug fixes +# Aesthetic improvements + DESC + } + } + + its(:description) { should have_selector 'h2' } + its(:description) { should have_selector 'ol > li' } + its(:raw_description) { should eq attributes[:description] } + + it 'should allow a raw_description to be set' do + model.raw_description = 'h4. More details' + expect(model.description).to have_selector 'h4' + end + end +end diff --git a/spec/lib/open_project/object_linking_spec.rb b/spec/lib/open_project/object_linking_spec.rb new file mode 100644 index 000000000000..52bd49b1e2c8 --- /dev/null +++ b/spec/lib/open_project/object_linking_spec.rb @@ -0,0 +1,34 @@ +#-- encoding: UTF-8 +#-- copyright +# OpenProject is a project management system. +# Copyright (C) 2012-2014 the OpenProject Foundation (OPF) +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# 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. +# +# 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. +# +# 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. +# +# See doc/COPYRIGHT.rdoc for more details. +#++ + +require 'spec_helper' + +describe OpenProject::ObjectLinking do + +end diff --git a/spec/lib/open_project/text_formatting_spec.rb b/spec/lib/open_project/text_formatting_spec.rb new file mode 100644 index 000000000000..3798c6bb9b17 --- /dev/null +++ b/spec/lib/open_project/text_formatting_spec.rb @@ -0,0 +1,479 @@ +#-- copyright +# OpenProject is a project management system. +# Copyright (C) 2012-2014 the OpenProject Foundation (OPF) +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# 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. +# +# 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. +# +# 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. +# +# See doc/COPYRIGHT.rdoc for more details. +#++ + +require 'spec_helper' + +describe OpenProject::TextFormatting do + include OpenProject::TextFormatting + include WorkPackagesHelper # soft-dependency + include ActionView::Helpers::UrlHelper # soft-dependency + include ActionView::Context + include OpenProject::StaticRouting::UrlHelpers + + def controller + # no-op + end + + describe ".format_text" do + let(:project) { FactoryGirl.create :valid_project } + let(:identifier) { project.identifier } + let(:project_member) { FactoryGirl.create :user, + :member_in_project => project, + :member_through_role => FactoryGirl.create(:role, + :permissions => [:view_work_packages, :edit_work_packages, + :browse_repository, :view_changesets, :view_wiki_pages]) } + let(:issue) { FactoryGirl.create :work_package, + :project => project, + :author => project_member, + :type => project.types.first } + + before do + @project = project + + allow(User).to receive(:current).and_return(project_member) + + Setting.enabled_scm = Setting.enabled_scm << "Filesystem" unless Setting.enabled_scm.include? "Filesystem" + end + + after do + User.unstub(:current) + + Setting.enabled_scm.delete "Filesystem" + end + + context "Changeset links" do + let(:repository) { FactoryGirl.create :repository, :project => project } + let(:changeset1) { FactoryGirl.create :changeset, + :repository => repository, + :comments => 'My very first commit' } + let(:changeset2) { FactoryGirl.create :changeset, + :repository => repository, + :comments => 'This commit fixes #1, #2 and references #1 & #3' } + let(:changeset_link) { link_to("r#{changeset1.revision}", + {:controller => 'repositories', :action => 'revision', :project_id => identifier, :rev => changeset1.revision}, + :class => 'changeset', :title => 'My very first commit') } + let(:changeset_link2) { link_to("r#{changeset2.revision}", + {:controller => 'repositories', :action => 'revision', :project_id => identifier, :rev => changeset2.revision}, + :class => 'changeset', :title => 'This commit fixes #1, #2 and references #1 & #3') } + + before do + project.repository = repository + end + + context "Single link" do + subject { format_text("r#{changeset1.revision}") } + + it { should eq("

      #{changeset_link}

      ") } + end + + context "Single link with dot" do + subject { format_text("r#{changeset1.revision}.") } + + it { should eq("

      #{changeset_link}.

      ") } + end + + context "Two links comma separated" do + subject { format_text("r#{changeset1.revision}, r#{changeset2.revision}") } + + it { should eq("

      #{changeset_link}, #{changeset_link2}

      ") } + end + + context "Single link comma separated without a space" do + subject { format_text("r#{changeset1.revision},r#{changeset2.revision}") } + + it { should eq("

      #{changeset_link},#{changeset_link2}

      ") } + end + + context "Escaping" do + subject { format_text("!r#{changeset1.id}") } + + it { should eq("

      r#{changeset1.id}

      ") } + end + end + + context "Version link" do + let(:version) { FactoryGirl.create :version, + :name => '1.0', + :project => project } + let(:version_link) { link_to('1.0', + {:controller => 'versions', :action => 'show', :id => version.id}, + :class => 'version') } + + context "Link with version id" do + subject { format_text("version##{version.id}") } + + it { should eq("

      #{version_link}

      ") } + end + + context "Link with version" do + subject { format_text("version:1.0") } + it { should eq("

      #{version_link}

      ") } + end + + context "Link with quoted version" do + subject { format_text('version:"1.0"') } + + it { should eq("

      #{version_link}

      ") } + end + + context "Escaping link with version id" do + subject { format_text("!version##{version.id}") } + + it { should eq("

      version##{version.id}

      ") } + end + + context "Escaping link with version" do + subject { format_text("!version:1.0") } + + it { should eq("

      version:1.0

      ") } + end + + context "Escaping link with quoted version" do + subject { format_text('!version:"1.0"') } + + it { should eq('

      version:"1.0"

      ') } + end + end + + context "Message links" do + let(:board) { FactoryGirl.create :board, :project => project } + let(:message1) { FactoryGirl.create :message, :board => board } + let(:message2) { FactoryGirl.create :message, + :board => board, + :parent => message1 } + + before do + message1.reload + end + + context "Plain message" do + subject { format_text("message##{message1.id}") } + + it { should eq("

      #{link_to(message1.subject, topic_path(message1), :class => 'message')}

      ") } + end + + context "Message with parent" do + subject { format_text("message##{message2.id}") } + + it { should eq("

      #{link_to(message2.subject, topic_path(message1, :anchor => "message-#{message2.id}", :r => message2.id), :class => 'message')}

      ") } + end + end + + context "Issue links" do + let(:issue_link) { link_to("##{issue.id}", + work_package_path(issue), + :class => 'issue work_package status-3 priority-1 created-by-me', :title => "#{issue.subject} (#{issue.status})") } + + context "Plain issue link" do + subject { format_text("##{issue.id}, [##{issue.id}], (##{issue.id}) and ##{issue.id}.") } + + it { should eq("

      #{issue_link}, [#{issue_link}], (#{issue_link}) and #{issue_link}.

      ") } + end + + context "Plain issue link to non-existing element" do + subject { format_text('#0123456789') } + + it { should eq('

      #0123456789

      ') } + end + + context "Escaping issue link" do + subject { format_text("!##{issue.id}.") } + + it { should eq("

      ##{issue.id}.

      ") } + end + + context "Cyclic Description Links" do + let(:issue2) { FactoryGirl.create :work_package, + :project => project, + :author => project_member, + :type => project.types.first } + + before do + issue2.description = "####{issue.id}" + issue2.save! + issue.description = "####{issue2.id}" + issue.save! + end + + subject { format_text issue, :description } + + it "doesn't replace description links with a cycle" do + expect(subject).to match("###{issue.id}") + end + end + + context "Description links" do + subject { format_text issue, :description } + + it "replaces the macro with the issue description" do + expect(subject).to eq("

      #{issue.description}

      ") + end + end + end + + context "Project links" do + let(:subproject) { FactoryGirl.create :valid_project, :parent => project, :is_public => true } + let(:project_url) { {:controller => 'projects', :action => 'show', :id => subproject.identifier} } + + context "Plain project link" do + subject { format_text("project##{subproject.id}") } + + it { should eq("

      #{link_to(subproject.name, project_url, :class => 'project')}

      ") } + end + + context "Plain project link via identifier" do + subject { format_text("project:#{subproject.identifier}") } + + it { should eq("

      #{link_to(subproject.name, project_url, :class => 'project')}

      ") } + end + + context "Plain project link via name" do + subject { format_text("project:\"#{subproject.name}\"") } + + it { should eq("

      #{link_to(subproject.name, project_url, :class => 'project')}

      ") } + end + end + + context "Url links" do + subject { format_text("http://foo.bar/FAQ#3") } + + it { should eq('

      http://foo.bar/FAQ#3

      ') } + end + + context "Wiki links" do + let(:project_2) { FactoryGirl.create :valid_project, + :identifier => 'onlinestore' } + let(:wiki_1) { FactoryGirl.create :wiki, + :start_page => "CookBook documentation", + :project => project } + let(:wiki_page_1_1) { FactoryGirl.create :wiki_page_with_content, + :wiki => wiki_1, + :title => "CookBook_documentation" } + let(:wiki_page_1_2) { FactoryGirl.create :wiki_page_with_content, + :wiki => wiki_1, + :title => "Another page" } + + before do + project_2.reload + + wiki_page_2_1 = FactoryGirl.create :wiki_page_with_content, + :wiki => project_2.wiki, + :title => "Start_page" + + project_2.wiki.pages << wiki_page_2_1 + project_2.wiki.start_page = "Start Page" + project_2.wiki.save! + + project.wiki = wiki_1 + + wiki_1.pages << wiki_page_1_1 + wiki_1.pages << wiki_page_1_2 + end + + context "Plain wiki link" do + subject { format_text('[[CookBook documentation]]') } + + it { should eq("

      CookBook documentation

      ") } + end + + context "Plain wiki page link" do + subject { format_text('[[Another page|Page]]') } + + it { should eq("

      Page

      ") } + end + + context "Wiki link with anchor" do + subject { format_text('[[CookBook documentation#One-section]]') } + + it { should eq("

      CookBook documentation

      ") } + end + + context "Wiki page link with anchor" do + subject { format_text('[[Another page#anchor|Page]]') } + + it { should eq("

      Page

      ") } + end + + context "Wiki link to an unknown page" do + subject { format_text('[[Unknown page]]') } + + it { should eq("

      Unknown page

      ") } + end + + context "Wiki page link to an unknown page" do + subject { format_text('[[Unknown page|404]]') } + + it { should eq("

      404

      ") } + end + + context "Link to another project's wiki" do + subject { format_text('[[onlinestore:]]') } + + it { should eq("

      onlinestore

      ") } + end + + context "Link to another project's wiki with label" do + subject { format_text('[[onlinestore:|Wiki]]') } + + it { should eq("

      Wiki

      ") } + end + + context "Link to another project's wiki page" do + subject { format_text('[[onlinestore:Start page]]') } + + it { should eq("

      Start page

      ") } + end + + context "Link to another project's wiki page with label" do + subject { format_text('[[onlinestore:Start page|Text]]') } + + it { should eq("

      Text

      ") } + end + + context "Link to an unknown wiki page in another project" do + subject { format_text('[[onlinestore:Unknown page]]') } + + it { should eq("

      Unknown page

      ") } + end + + context "Struck through link to wiki page" do + subject { format_text('-[[Another page|Page]]-') } + + it { should eql("

      Page

      ") } + end + + context "Named struck through link to wiki page" do + subject { format_text('-[[Another page|Page]] link-') } + + it { should eql("

      Page link

      ") } + end + + context "Escaped link to wiki page" do + subject { format_text('![[Another page|Page]]') } + + it { should eql('

      [[Another page|Page]]

      ') } + end + + context "Link to wiki of non-existing project" do + subject { format_text('[[unknowproject:Start]]') } + + it { should eql('

      [[unknowproject:Start]]

      ') } + end + + context "Link to wiki page of non-existing project" do + subject { format_text('[[unknowproject:Start|Page title]]') } + + it { should eql('

      [[unknowproject:Start|Page title]]

      ') } + end + end + + context "Redmine links" do + let(:repository) { FactoryGirl.create :repository, :project => project } + let(:source_url) { {:controller => 'repositories', :action => 'entry', :project_id => identifier, :path => 'some/file'} } + let(:source_url_with_ext) { {:controller => 'repositories', :action => 'entry', :project_id => identifier, :path => 'some/file.ext'} } + + before do + project.repository = repository + + @to_test = { + # source + 'source:/some/file' => link_to('source:/some/file', source_url, :class => 'source'), + 'source:/some/file.' => link_to('source:/some/file', source_url, :class => 'source') + ".", + 'source:/some/file.ext.' => link_to('source:/some/file.ext', source_url_with_ext, :class => 'source') + ".", + 'source:/some/file. ' => link_to('source:/some/file', source_url, :class => 'source') + ".", + 'source:/some/file.ext. ' => link_to('source:/some/file.ext', source_url_with_ext, :class => 'source') + ".", + 'source:/some/file, ' => link_to('source:/some/file', source_url, :class => 'source') + ",", + 'source:/some/file@52' => link_to('source:/some/file@52', source_url.merge(:rev => 52), :class => 'source'), + 'source:/some/file.ext@52' => link_to('source:/some/file.ext@52', source_url_with_ext.merge(:rev => 52), :class => 'source'), + 'source:/some/file#L110' => link_to('source:/some/file#L110', source_url.merge(:anchor => 'L110'), :class => 'source'), + 'source:/some/file.ext#L110' => link_to('source:/some/file.ext#L110', source_url_with_ext.merge(:anchor => 'L110'), :class => 'source'), + 'source:/some/file@52#L110' => link_to('source:/some/file@52#L110', source_url.merge(:rev => 52, :anchor => 'L110'), :class => 'source'), + 'export:/some/file' => link_to('export:/some/file', source_url.merge(:format => 'raw'), :class => 'source download'), + # escaping + '!source:/some/file' => 'source:/some/file', + # invalid expressions + 'source:' => 'source:' + } + end + + it "" do + @to_test.each do |text, result| + expect(format_text(text)).to eql("

      #{result}

      ") + end + end + end + + context "Pre content should not parse wiki and redmine links" do + let(:wiki) { FactoryGirl.create :wiki, + :start_page => "CookBook documentation", + :project => project } + let(:wiki_page) { FactoryGirl.create :wiki_page_with_content, + :wiki => wiki, + :title => "CookBook_documentation" } + let(:raw) { <<-RAW +[[CookBook documentation]] + +##{issue.id} + +
      +[[CookBook documentation]]
      +
      +##{issue.id}
      +
      +RAW + } + + let(:expected) { <<-EXPECTED +

      CookBook documentation

      +

      ##{issue.id}

      +
      +[[CookBook documentation]]
      +
      +##{issue.id}
      +
      +EXPECTED + } + + before do + project.wiki = wiki + wiki.pages << wiki_page + end + + subject { format_text(raw).gsub(%r{[\r\n\t]}, '')} + + it { should eql(expected.gsub(%r{[\r\n\t]}, ''))} + end + end + + context 'deprecated methods' do + subject { self } + + it { should respond_to :textilizable } + it { should respond_to :textilize } + end +end diff --git a/spec/representers/work_package_representer_spec.rb b/spec/representers/work_package_representer_spec.rb index e7c21f6cb80d..0e06d2890bbb 100644 --- a/spec/representers/work_package_representer_spec.rb +++ b/spec/representers/work_package_representer_spec.rb @@ -49,6 +49,8 @@ it { should have_json_path('id') } it { should have_json_path('description') } + it { should have_json_path('rawDescription') } + it { should have_json_path('dueDate') } it { should have_json_path('percentageDone') } diff --git a/test/unit/helpers/application_helper_test.rb b/test/unit/helpers/application_helper_test.rb index 676fad9f2fcb..daa569501eeb 100644 --- a/test/unit/helpers/application_helper_test.rb +++ b/test/unit/helpers/application_helper_test.rb @@ -95,12 +95,12 @@ def test_auto_links # wrap in angle brackets '' => '<http://foo.bar>' } - to_test.each { |text, result| assert_equal "

      #{result}

      ", textilizable(text) } + to_test.each { |text, result| assert_equal "

      #{result}

      ", format_text(text) } end def test_auto_mailto assert_equal '

      ', - textilizable('test@foo.bar') + format_text('test@foo.bar') end def test_inline_images @@ -113,7 +113,7 @@ def test_inline_images 'with title !http://foo.bar/image.jpg(This is a title)!' => 'with title This is a title', 'with title !http://foo.bar/image.jpg(This is a double-quoted "title")!' => 'with title This is a double-quoted "title"', } - to_test.each { |text, result| assert_equal "

      #{result}

      ", textilizable(text) } + to_test.each { |text, result| assert_equal "

      #{result}

      ", format_text(text) } end def test_inline_images_inside_tags @@ -125,8 +125,8 @@ def test_inline_images_inside_tags p=. !bar.gif! RAW - assert textilizable(raw).include?('') - assert textilizable(raw).include?('') + assert format_text(raw).include?('') + assert format_text(raw).include?('') end def test_attached_images @@ -138,7 +138,7 @@ def test_attached_images # link image '!logo.gif!:http://foo.bar/' => "\"This", } - to_test.each { |text, result| assert_equal "

      #{result}

      ", textilizable(text, :attachments => [@attachment]) } + to_test.each { |text, result| assert_equal "

      #{result}

      ", format_text(text, :attachments => [@attachment]) } end def test_textile_external_links @@ -157,7 +157,7 @@ def test_textile_external_links # escaping '"test":http://foo"bar' => 'test', } - to_test.each { |text, result| assert_equal "

      #{result}

      ", textilizable(text) } + to_test.each { |text, result| assert_equal "

      #{result}

      ", format_text(text) } end def test_textile_relative_to_full_links_in_a_controller @@ -167,7 +167,7 @@ def test_textile_relative_to_full_links_in_a_controller 'This is a "link":http://foo.bar' => 'This is a link', 'This is an intern "link":/foo/bar' => 'This is an intern link', 'This is an intern "link":/foo/bar and an extern "link":http://foo.bar' => 'This is an intern link and an extern link', - }.each { |text, result| assert_equal "

      #{result}

      ", textilizable(text, :only_path => false) } + }.each { |text, result| assert_equal "

      #{result}

      ", format_text(text, :only_path => false) } end def test_textile_relative_to_full_links_in_the_mailer @@ -185,7 +185,7 @@ def self.default_url_options 'This is a "link":http://foo.bar' => 'This is a link', 'This is an intern "link":/foo/bar' => 'This is an intern link', 'This is an intern "link":/foo/bar and an extern "link":http://foo.bar' => 'This is an intern link and an extern link', - }.each { |text, result| assert_equal "

      #{result}

      ", textilizable(text, :only_path => false) } + }.each { |text, result| assert_equal "

      #{result}

      ", format_text(text, :only_path => false) } end def test_cross_project_redmine_links @@ -210,7 +210,7 @@ def test_cross_project_redmine_links :class => 'changeset', :title => 'This commit fixes #1, #2 and references #1 & #3') - # textilizable "sees" the text is parses from the_other_project (and not @project) + # format_text "sees" the text is parses from the_other_project (and not @project) the_other_project = FactoryGirl.create :valid_project to_test = { @@ -227,7 +227,7 @@ def test_cross_project_redmine_links "#{identifier}:source:/some/file" => source_link, 'invalid:source:/some/file' => 'invalid:source:/some/file', } - to_test.each { |text, result| assert_equal "

      #{result}

      ", textilizable(text, :project => the_other_project), "#{text} failed" } + to_test.each { |text, result| assert_equal "

      #{result}

      ", format_text(text, :project => the_other_project), "#{text} failed" } end def test_redmine_links_git_commit @@ -252,7 +252,7 @@ def test_redmine_links_git_commit :comments => 'test commit') assert( c.save ) @project.reload - to_test.each { |text, result| assert_equal "

      #{result}

      ", textilizable(text) } + to_test.each { |text, result| assert_equal "

      #{result}

      ", format_text(text) } end def test_attachment_links @@ -260,7 +260,7 @@ def test_attachment_links to_test = { 'attachment:logo.gif' => attachment_link } - to_test.each { |text, result| assert_equal "

      #{result}

      ", textilizable(text, :attachments => [@attachment]), "#{text} failed" } + to_test.each { |text, result| assert_equal "

      #{result}

      ", format_text(text, :attachments => [@attachment]), "#{text} failed" } end def test_html_tags @@ -284,7 +284,7 @@ def test_html_tags '
      text
      ' => '
      text
      ', '
      text
      ' => '
      text
      ', } - to_test.each { |text, result| assert_equal result, textilizable(text) } + to_test.each { |text, result| assert_equal result, format_text(text) } end def test_allowed_html_tags @@ -293,7 +293,7 @@ def test_allowed_html_tags "no *textile* formatting" => "no *textile* formatting", "this is a tag" => "this is <tag>a tag</tag>" } - to_test.each { |text, result| assert_equal result, textilizable(text) } + to_test.each { |text, result| assert_equal result, format_text(text) } end def test_pre_tags @@ -315,7 +315,7 @@ def test_pre_tags

      After

      EXPECTED - assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '') + assert_equal expected.gsub(%r{[\r\n\t]}, ''), format_text(raw).gsub(%r{[\r\n\t]}, '') end def test_syntax_highlight @@ -330,7 +330,7 @@ def test_syntax_highlight
    EXPECTED - assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '') + assert_equal expected.gsub(%r{[\r\n\t]}, ''), format_text(raw).gsub(%r{[\r\n\t]}, '') end def test_wiki_links_in_tables @@ -345,7 +345,7 @@ def test_wiki_links_in_tables "Cell 21Last page" } - to_test.each { |text, result| assert_equal "#{result}
    ", textilizable(text).gsub(/[\t\n]/, '') } + to_test.each { |text, result| assert_equal "#{result}
    ", format_text(text).gsub(/[\t\n]/, '') } end def test_text_formatting @@ -355,12 +355,12 @@ def test_text_formatting 'a H *umane* W *eb* T *ext* G *enerator*' => 'a H umane W eb T ext G enerator', 'a *H* umane *W* eb *T* ext *G* enerator' => 'a H umane W eb T ext G enerator', } - to_test.each { |text, result| assert_equal "

    #{result}

    ", textilizable(text) } + to_test.each { |text, result| assert_equal "

    #{result}

    ", format_text(text) } end def test_wiki_horizontal_rule - assert_equal '
    ', textilizable('---') - assert_equal '

    Dashes: ---

    ', textilizable('Dashes: ---') + assert_equal '
    ', format_text('---') + assert_equal '

    Dashes: ---

    ', format_text('Dashes: ---') end def test_footnotes @@ -375,14 +375,14 @@ def test_footnotes

    1 This is the foot note

    EXPECTED - assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '') + assert_equal expected.gsub(%r{[\r\n\t]}, ''), format_text(raw).gsub(%r{[\r\n\t]}, '') end def test_headings raw = 'h1. Some heading' expected = %|\n

    Some heading

    | - assert_equal expected, textilizable(raw) + assert_equal expected, format_text(raw) end def test_table_of_content @@ -444,7 +444,7 @@ def test_table_of_content '' + '' - assert textilizable(raw).gsub("\n", "").include?(expected), textilizable(raw) + assert format_text(raw).gsub("\n", "").include?(expected), format_text(raw) end def test_table_of_content_should_contain_included_page_headings @@ -468,13 +468,13 @@ def test_table_of_content_should_contain_included_page_headings '
  • Child page 1
  • ' + '' - assert textilizable(raw).gsub("\n", "").include?(expected), textilizable(raw) + assert format_text(raw).gsub("\n", "").include?(expected), format_text(raw) end def test_default_formatter Setting.text_formatting = 'unknown' text = 'a *link*: http://www.example.net/' - assert_equal '

    a *link*: http://www.example.net/

    ', textilizable(text) + assert_equal '

    a *link*: http://www.example.net/

    ', format_text(text) Setting.text_formatting = 'textile' end diff --git a/test/unit/lib/redmine/wiki_formatting/macros_test.rb b/test/unit/lib/redmine/wiki_formatting/macros_test.rb index b614a8ef0b1d..89f57f731563 100644 --- a/test/unit/lib/redmine/wiki_formatting/macros_test.rb +++ b/test/unit/lib/redmine/wiki_formatting/macros_test.rb @@ -45,28 +45,28 @@ def setup def test_macro_hello_world text = "{{hello_world}}" - assert textilizable(text).match(/Hello world!/) + assert format_text(text).match(/Hello world!/) # escaping text = "!{{hello_world}}" - assert_equal '

    {{hello_world}}

    ', textilizable(text) + assert_equal '

    {{hello_world}}

    ', format_text(text) end def test_macro_include @project = Project.find(1) # include a page of the current project wiki text = "{{include(Another page)}}" - assert textilizable(text).match(/This is a link to a ticket/) + assert format_text(text).match(/This is a link to a ticket/) @project = nil # include a page of a specific project wiki text = "{{include(ecookbook:Another page)}}" - assert textilizable(text).match(/This is a link to a ticket/) + assert format_text(text).match(/This is a link to a ticket/) text = "{{include(ecookbook:)}}" - assert textilizable(text).match(/CookBook documentation/) + assert format_text(text).match(/CookBook documentation/) text = "{{include(unknowidentifier:somepage)}}" - assert textilizable(text).match(/Page not found/) + assert format_text(text).match(/Page not found/) end def test_macro_child_pages @@ -77,12 +77,12 @@ def test_macro_child_pages @project = Project.find(1) # child pages of the current wiki page - assert_equal expected, textilizable("{{child_pages}}", :object => WikiPage.find(2).content) + assert_equal expected, format_text("{{child_pages}}", :object => WikiPage.find(2).content) # child pages of another page - assert_equal expected, textilizable("{{child_pages(Another_page)}}", :object => WikiPage.find(1).content) + assert_equal expected, format_text("{{child_pages(Another_page)}}", :object => WikiPage.find(1).content) @project = Project.find(2) - assert_equal expected, textilizable("{{child_pages(ecookbook:Another_page)}}", :object => WikiPage.find(1).content) + assert_equal expected, format_text("{{child_pages(ecookbook:Another_page)}}", :object => WikiPage.find(1).content) end def test_macro_child_pages_with_option @@ -95,11 +95,11 @@ def test_macro_child_pages_with_option @project = Project.find(1) # child pages of the current wiki page - assert_equal expected, textilizable("{{child_pages(parent=1)}}", :object => WikiPage.find(2).content) + assert_equal expected, format_text("{{child_pages(parent=1)}}", :object => WikiPage.find(2).content) # child pages of another page - assert_equal expected, textilizable("{{child_pages(Another_page, parent=1)}}", :object => WikiPage.find(1).content) + assert_equal expected, format_text("{{child_pages(Another_page, parent=1)}}", :object => WikiPage.find(1).content) @project = Project.find(2) - assert_equal expected, textilizable("{{child_pages(ecookbook:Another_page, parent=1)}}", :object => WikiPage.find(1).content) + assert_equal expected, format_text("{{child_pages(ecookbook:Another_page, parent=1)}}", :object => WikiPage.find(1).content) end end