From daaf6e1bc2ac05fe736ba248aea83e0558748d3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20G=C3=BCnther?= Date: Thu, 9 Nov 2017 14:18:43 +0100 Subject: [PATCH] Introduce commonmark and html-pipeline --- Gemfile | 8 + Gemfile.lock | 16 + app/helpers/application_helper.rb | 2 +- lib/open_project/text_formatting.rb | 486 +--------------- .../filters/markdown_filter.rb | 48 ++ .../text_formatting/filters/plain_filter.rb | 44 ++ .../filters/sanitization_filter.rb | 40 ++ .../text_formatting/formatters.rb} | 35 +- .../formatters/markdown/formatter.rb | 143 +++++ .../formatters/markdown/helper.rb | 48 ++ .../formatters}/null_formatter/formatter.rb | 26 +- .../formatters}/null_formatter/helper.rb | 20 +- .../formatters/textile/formatter.rb | 143 +++++ .../formatters/textile/helper.rb | 66 +++ .../textile/legacy_text_formatting.rb | 519 ++++++++++++++++++ lib/open_project/text_formatting/pipeline.rb | 72 +++ .../wiki_formatting/textile/formatter.rb | 145 ----- lib/redmine/wiki_formatting/textile/helper.rb | 68 --- 18 files changed, 1194 insertions(+), 735 deletions(-) create mode 100644 lib/open_project/text_formatting/filters/markdown_filter.rb create mode 100644 lib/open_project/text_formatting/filters/plain_filter.rb create mode 100644 lib/open_project/text_formatting/filters/sanitization_filter.rb rename lib/{redmine/wiki_formatting.rb => open_project/text_formatting/formatters.rb} (62%) create mode 100644 lib/open_project/text_formatting/formatters/markdown/formatter.rb create mode 100644 lib/open_project/text_formatting/formatters/markdown/helper.rb rename lib/{redmine/wiki_formatting => open_project/text_formatting/formatters}/null_formatter/formatter.rb (75%) rename lib/{redmine/wiki_formatting => open_project/text_formatting/formatters}/null_formatter/helper.rb (82%) create mode 100644 lib/open_project/text_formatting/formatters/textile/formatter.rb create mode 100644 lib/open_project/text_formatting/formatters/textile/helper.rb create mode 100644 lib/open_project/text_formatting/formatters/textile/legacy_text_formatting.rb create mode 100644 lib/open_project/text_formatting/pipeline.rb delete mode 100644 lib/redmine/wiki_formatting/textile/formatter.rb delete mode 100644 lib/redmine/wiki_formatting/textile/helper.rb diff --git a/Gemfile b/Gemfile index 292fb0f8027..9a8d4827a39 100644 --- a/Gemfile +++ b/Gemfile @@ -71,6 +71,14 @@ gem 'htmldiff' # Generate url slugs with #to_url and other string niceties gem 'stringex', '~> 2.7.1' +# CommonMark markdown parser with GFM extension +gem 'commonmarker', '~> 0.17.5' +# HTML pipeline for transformations on text formatter output +# such as sanitization or additional features +gem 'html-pipeline', '~> 2.7.1' +# HTML sanitization used for html-pipeline +gem 'sanitize', '~> 4.5.0' + # generates SVG Graphs # used for statistics on svn repositories gem 'svg-graph', '~> 2.1.0' diff --git a/Gemfile.lock b/Gemfile.lock index 37698725ba8..191ab59b6c8 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -203,6 +203,8 @@ GEM coercible (1.0.0) descendants_tracker (~> 0.0.1) color-tools (1.3.0) + commonmarker (0.17.5) + ruby-enum (~> 0.5) concurrent-ruby (1.0.5) crack (0.4.3) safe_yaml (~> 1.0.0) @@ -315,6 +317,9 @@ GEM hashie (3.5.6) health_check (2.6.0) rails (>= 4.0) + html-pipeline (2.7.1) + activesupport (>= 2) + nokogiri (>= 1.4) htmldiff (0.0.1) http-cookie (1.0.3) domain_name (~> 0.5) @@ -369,6 +374,8 @@ GEM nio4r (2.1.0) nokogiri (1.8.1) mini_portile2 (~> 2.3.0) + nokogumbo (1.4.13) + nokogiri oj (3.3.8) okcomputer (1.16.0) openproject-token (1.0.1) @@ -519,9 +526,15 @@ GEM activesupport (>= 3.0.0) i18n iso8601 + ruby-enum (0.7.1) + i18n ruby-progressbar (1.9.0) rubyzip (1.2.1) safe_yaml (1.0.4) + sanitize (4.5.0) + crass (~> 1.0.2) + nokogiri (>= 1.4.4) + nokogumbo (~> 1.4.1) sass (3.5.1) sass-listen (~> 4.0.0) sass-listen (4.0.0) @@ -632,6 +645,7 @@ DEPENDENCIES cocaine (~> 0.5.8) coderay (~> 1.1.2) color-tools (~> 1.3.0) + commonmarker (~> 0.17.5) cucumber (~> 3.0.0) cucumber-rails (~> 1.5.0) daemons @@ -650,6 +664,7 @@ DEPENDENCIES gon (~> 6.2.0) grape (~> 1.0) health_check + html-pipeline (~> 2.7.1) htmldiff i18n-js (~> 3.0.0) json_spec (~> 1.1.4) @@ -703,6 +718,7 @@ DEPENDENCIES ruby-duration (~> 3.2.0) ruby-progressbar rubytree! + sanitize (~> 4.5.0) sass (= 3.5.1) sass-rails (~> 5.0.6) selenium-webdriver (~> 3.6) diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 4eb36f4440a..f0de3d248c5 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -631,7 +631,7 @@ module ApplicationHelper private def wiki_helper - helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting) + helper = OpenProject::TextFormatting::Formatters.helper_for(Setting.text_formatting) extend helper self end diff --git a/lib/open_project/text_formatting.rb b/lib/open_project/text_formatting.rb index e4c5e050bc0..5634c97f7b0 100644 --- a/lib/open_project/text_formatting.rb +++ b/lib/open_project/text_formatting.rb @@ -29,19 +29,8 @@ 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 + # Used for truncation include ActionView::Helpers::TextHelper - include OpenProject::ObjectLinking - include OpenProject::SafeParams - # The WorkPackagesHelper is required to get access to the methods - # 'work_package_css_classes' and 'work_package_quick_info'. - include WorkPackagesHelper # Truncates and returns the string as a single line def truncate_single_line(string, *args) @@ -63,476 +52,41 @@ module OpenProject # * with a String: format_text(text, options) # * with an object and one of its attribute: format_text(issue, :description, options) def format_text(*args) + + # Forward to the legacy text formatting for textile syntax + if Setting.text_formatting == 'textile' + return Formatters::Textile::LegacyTextFormatting.format_text(*args) + end + options = args.last.is_a?(Hash) ? args.pop : {} case args.size when 1 - obj = options[:object] + object = options[:object] text = args.shift when 2 - obj = args.shift + object = args.shift attr = args.shift - text = obj.send(attr).to_s + text = object.send(attr).to_s else raise ArgumentError, 'invalid arguments to format_text' end return '' if text.blank? - edit = !!options[: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) + project = options.fetch(:project) { @project || obj.try(:project) } only_path = options.delete(:only_path) != false # offer 'plain' as readable version for 'no formatting' to callers - options_format = options[:format] == 'plain' ? '' : options[:format] - format = options_format || Setting.text_formatting - text = Redmine::WikiFormatting.to_html(format, text, - object: obj, - attribute: attr, - 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) { |text| - [:execute_macros, :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 - } + format = options.fetch(:format, Setting.text_formatting) - if @parsed_headings.any? - replace_toc(text, @parsed_headings) - end + # Get the associated formatter + pipeline = Pipeline.new format, + context: { + object: object, + project: project, + only_path: only_path + } - escape_non_macros(text) - text.html_safe - end - deprecated_alias :textilizable, :format_text - deprecated_alias :textilize, :format_text - - ## - # Escape double curly braces after macro expansion. - # This will avoid arbitrary angular expressions to be evaluated in - # formatted text marked html_safe. - def escape_non_macros(text) - text.gsub!(/\{\{(?! \$root\.DOUBLE_LEFT_CURLY_BRACE)/, '{{ $root.DOUBLE_LEFT_CURLY_BRACE }}') - end - - def parse_non_pre_blocks(text) - s = StringScanner.new(text) - tags = [] - parsed = '' - while !s.eos? - s.scan(/(.*?)(<(\/)?(pre|code)(.*?)>|\z)/im) - text = s[1] - full_tag = s[2] - closing = s[3] - tag = 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 - - - MACROS_RE = / - (!)? # escaping - ( - \{\{ # opening tag - ([\w]+) # macro name - (\(([^\}]*)\))? # optional arguments - \}\} # closing tag - ) - /x unless const_defined?(:MACROS_RE) - - # Macros substitution - def execute_macros(text, project, obj, _attr, _only_path, options) - return if !!options[:edit] - text.gsub!(MACROS_RE) do - esc = $1 - all = $2 - macro = $3 - args = ($5 || '').split(',').each(&:strip!) - if esc.nil? - begin - exec_macro(macro, obj, args, view: self, project: project) - rescue => e - "\ - #{::I18n.t(:macro_execution_error, macro_name: macro)} (#{e})\ - ".squish - rescue NotImplementedError - "\ - #{::I18n.t(:macro_unavailable, macro_name: macro)}\ - ".squish - end || all - else - all - end - end - 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 = $1 - relative_url = $2 || $3 - next m unless href.present? - if defined?(request) && request.present? - # we have a request! - protocol = request.protocol - host_with_port = 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 = "#{url_opts[:protocol]}://" - host_with_port = 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|svg))"(\s+alt="([^"]*)")?/i) do |m| - filename = $1.downcase - ext = $2 - alt = $3 - alttext = $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 = $1 - all = $2 - page = $3 - title = $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 = $1 - anchor = $2 - end - # Unescape the escaped entities from textile - page = CGI.unescapeHTML(page) - # check if page exists - wiki_page = link_project.wiki.find_page(page) - wiki_title = wiki_page.nil? ? page : wiki_page.title - url = case options[:wiki_links] - when :local; "#{title}.html" - when :anchor; "##{title}" # used for single-file wiki export - else - wiki_page_id = wiki_page.nil? ? page.to_url : wiki_page.slug - 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 || wiki_title), 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|user)?((#+|r)(\d+)|(:)([^"\s<>][^\s<>]*?|"[^"]+?"))(?=(?=[[:punct:]]\W)|,|\s|\]|<|$)}) do |_m| - leading = $1 - esc = $2 - project_prefix = $3 - project_identifier = $4 - prefix = $5 - sep = $7 || $9 - identifier = $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: project.repository.id, revision: 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 - .includes(:status) - .references(:statuses) - .find_by(id: oid) - link = link_to("##{oid}", - work_package_path_or_url(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.includes(:parent).find_by(id: oid) - 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 - when 'user' - if user = User.in_visible_project.find_by(id: oid) - link = link_to_user(user, class: 'user-mention') - end - end - elsif sep == '##' - oid = identifier.to_i - if work_package = WorkPackage.visible - .includes(:status) - .references(:statuses) - .find_by(id: oid) - link = work_package_quick_info(work_package, only_path: only_path) - end - elsif sep == '###' - oid = identifier.to_i - work_package = WorkPackage.visible - .includes(:status) - .references(:statuses) - .find_by(id: oid) - if work_package && obj && !(attr == :description && obj.id == work_package.id) - link = work_package_quick_info_with_description(work_package, only_path: only_path) - 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.where(['repository_id = ? AND scmid LIKE ?', project.repository.id, "#{name}%"]).first) - 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(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 = $1 - rev = $3 - anchor = $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' - p = Project - .visible - .where(['projects.identifier = :s OR LOWER(projects.name) = :s', - { s: name.downcase }]) - .first - if p - link = link_to_project(p, { only_path: only_path }, class: 'project') - end - when 'user' - if user = User.in_visible_project.find_by(login: name) - link = link_to_user(user, class: 'user-mention') - 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 = $1.to_i - attrs = $2 - content = $3 - item = strip_tags(content).strip - tocitem = strip_tags(content.gsub(/
/, ' ')) - anchor = item.gsub(%r{[^\w\s\-]}, '').gsub(%r{\s+(\-+\s*)?}, '-') - @parsed_headings << [level, anchor, tocitem] - url = full_url(anchor) - "
\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 = "

" - 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 - url = full_url anchor - out << "#{item}" - current = level - started = true - end - out << '
' * (current - root) - out << '' - out << '
' - end - end - end - - # - # displays the current url plus an optional anchor - # - def full_url(anchor_name = '') - return "##{anchor_name}" if current_request.nil? - url_for pagination_params_whitelist.merge(anchor: anchor_name, only_path: true) - rescue ActionController::UrlGenerationError - # In a context outside params, we don't know what the relative anchor url is - "##{anchor_name}" - end - - def current_request - request rescue nil + pipeline.to_html(text) end end end diff --git a/lib/open_project/text_formatting/filters/markdown_filter.rb b/lib/open_project/text_formatting/filters/markdown_filter.rb new file mode 100644 index 00000000000..e90082deedd --- /dev/null +++ b/lib/open_project/text_formatting/filters/markdown_filter.rb @@ -0,0 +1,48 @@ +#-- encoding: UTF-8 + +#-- copyright +# OpenProject is a project management system. +# Copyright (C) 2012-2017 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-2017 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::TextFormatting + module Filters + class MarkdownFilter < HTML::Pipeline::MarkdownFilter + + # Convert Markdown to HTML using CommonMarker + def call + options = [:GITHUB_PRE_LANG] + options << :HARDBREAKS if context[:gfm] != false + extensions = context.fetch :commonmarker_extensions, + %i[table strikethrough tagfilter autolink] + + html = CommonMarker.render_html(text, options, extensions) + html.rstrip! + html + end + end + end +end diff --git a/lib/open_project/text_formatting/filters/plain_filter.rb b/lib/open_project/text_formatting/filters/plain_filter.rb new file mode 100644 index 00000000000..ce074f0191d --- /dev/null +++ b/lib/open_project/text_formatting/filters/plain_filter.rb @@ -0,0 +1,44 @@ +#-- encoding: UTF-8 + +#-- copyright +# OpenProject is a project management system. +# Copyright (C) 2012-2017 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-2017 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::TextFormatting + module Filters + class PlainFilter < HTML::Pipeline::TextFilter + include ERB::Util + include ActionView::Helpers::TagHelper + include ActionView::Helpers::TextHelper + include ActionView::Helpers::UrlHelper + + def call + simple_format(auto_link(CGI::escapeHTML(text))) + end + end + end +end diff --git a/lib/open_project/text_formatting/filters/sanitization_filter.rb b/lib/open_project/text_formatting/filters/sanitization_filter.rb new file mode 100644 index 00000000000..6d5a511bb6e --- /dev/null +++ b/lib/open_project/text_formatting/filters/sanitization_filter.rb @@ -0,0 +1,40 @@ +#-- encoding: UTF-8 + +#-- copyright +# OpenProject is a project management system. +# Copyright (C) 2012-2017 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-2017 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::TextFormatting + module Filters + class SanitizationFilter < HTML::Pipeline::SanitizationFilter + def call + binding.pry + super + end + end + end +end diff --git a/lib/redmine/wiki_formatting.rb b/lib/open_project/text_formatting/formatters.rb similarity index 62% rename from lib/redmine/wiki_formatting.rb rename to lib/open_project/text_formatting/formatters.rb index 2454c6a572a..84113d87e88 100644 --- a/lib/redmine/wiki_formatting.rb +++ b/lib/open_project/text_formatting/formatters.rb @@ -1,4 +1,5 @@ #-- encoding: UTF-8 + #-- copyright # OpenProject is a project management system. # Copyright (C) 2012-2018 the OpenProject Foundation (OPF) @@ -27,8 +28,8 @@ # See docs/COPYRIGHT.rdoc for more details. #++ -module Redmine - module WikiFormatting +module OpenProject::TextFormatting + module Formatters class << self def registered @@ -51,8 +52,8 @@ module Redmine raise ArgumentError, "format name '#{name}' is already taken" if registered?(key) begin - formatter = "Redmine::WikiFormatting::#{modulename}::Formatter".constantize - helper = "Redmine::WikiFormatting::#{modulename}::Helper".constantize + formatter = "OpenProject::TextFormatting::Formatters::#{modulename}::Formatter".constantize + helper = "OpenProject::TextFormatting::Formatters::#{modulename}::Helper".constantize registered[key] = { formatter: formatter, helper: helper } rescue NameError => e Rails.logger.error "Failed to register wiki formatting #{namespace}: #{e}" @@ -74,32 +75,6 @@ module Redmine registered.keys.map end - def to_html(format, text, options = {}, &block) - edit = !!options[:edit] - text = if Setting.cache_formatted_text? && text.size > 2.kilobyte && cache_store && cache_key = cache_key_for(format, options[:object], options[:attribute], edit) - # Text retrieved from the cache store may be frozen - # We need to dup it so we can do in-place substitutions with gsub! - cache_store.fetch cache_key do - formatter_for(format).new(text).to_html edit ? :edit : nil - end.dup - else - formatter_for(format).new(text).to_html edit ? :edit : nil - end - text - end - - # Returns a cache key for the given text +format+, +object+ and +attribute+ or nil if no caching should be done - def cache_key_for(format, object, attribute, edit) - if object && attribute && edit && !object.new_record? && object.respond_to?(:updated_on) && !format.blank? - "formatted_text/#{format}/#{object.class.model_name.cache_key}/#{object.id}-#{attribute}-#{edit}-#{object.updated_on.to_s(:number)}" - end - end - - # Returns the cache store used to cache HTML output - def cache_store - ActionController::Base.cache_store - end - private def register_default! diff --git a/lib/open_project/text_formatting/formatters/markdown/formatter.rb b/lib/open_project/text_formatting/formatters/markdown/formatter.rb new file mode 100644 index 00000000000..f276abb35b6 --- /dev/null +++ b/lib/open_project/text_formatting/formatters/markdown/formatter.rb @@ -0,0 +1,143 @@ +#-- encoding: UTF-8 +#-- copyright +# OpenProject is a project management system. +# Copyright (C) 2012-2017 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-2017 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 'redcloth3' + +module OpenProject::TextFormatting::Formatters + module Markdown + class Formatter < RedCloth3 + include ERB::Util + include ActionView::Helpers::TagHelper + + # auto_link rule after textile rules so that it doesn't break !image_url! tags + RULES = [:textile, :block_markdown_rule, :inline_auto_link, :inline_auto_mailto] + + def initialize(*args) + super + + self.hard_breaks = true + self.no_span_caps = true + self.filter_styles = true + end + + def to_html(*_rules) + @toc = [] + super(*RULES).to_s + end + + private + + # Patch for RedCloth. Fixed in RedCloth r128 but _why hasn't released it yet. + # http://code.whytheluckystiff.net/redcloth/changeset/128 + def hard_break(text) + text.gsub!(/(.)\n(?!\n|\Z| *([#*=]+(\s|$)|[{|]))/, '\\1
') if hard_breaks + end + + # Patch to add code highlighting support to RedCloth + def smooth_offtags(text) + unless @pre_list.empty? + ## replace
 content
+          text.gsub!(//) do
+            content = @pre_list[$1.to_i]
+            if content.match(/\s?(.+)/m)
+              content = "" +
+                        Redmine::SyntaxHighlighting.highlight_by_language($2, $1)
+            end
+            content
+          end
+        end
+      end
+
+
+      def auto_link_regexp
+        @auto_link_regexp ||= begin
+          %r{
+            (                          # leading text
+              <\w+.*?>|                # leading HTML tag, or
+              [^=<>!:'"/]|             # leading punctuation, or
+              \{\{\w+\(|               # inside a macro?
+              ^                        # beginning of line
+            )
+            (
+              (?:https?://)|           # protocol spec, or
+              (?:s?ftps?://)|
+              (?:www\.)                # www.*
+            )
+            (
+              (\S+?)                   # url
+              (\/)?                    # slash
+            )
+            ((?:>)?|[^\w\=\/;\(\)]*?)               # post
+            (?=<|\s|$)
+           }x
+        end
+      end
+
+      # Turns all urls into clickable links (code from Rails).
+      def inline_auto_link(text)
+        text.gsub!(auto_link_regexp) do
+          all = $&
+          leading = $1
+          proto = $2
+          url = $3
+          post = $6
+          if url.nil? || leading =~ /=]?/ || leading =~ /\{\{\w+\(/
+            # don't replace URLs that are already linked
+            # and URLs prefixed with ! !> !< != (textile images)
+            all
+          else
+            # Idea below : an URL with unbalanced parethesis and
+            # ending by ')' is put into external parenthesis
+            if url[-1] == ?) and ((url.count('(') - url.count(')')) < 0)
+              url = url[0..-2] # discard closing parenth from url
+              post = ')' + post # add closing parenth to post
+            end
+            tag = content_tag('a',
+                              proto + url,
+                              href: "#{proto == 'www.' ? 'http://www.' : proto}#{url}",
+                              class: 'external icon-context icon-copy')
+            %(#{leading}#{tag}#{post})
+          end
+        end
+      end
+
+      # Turns all email addresses into clickable links (code from Rails).
+      def inline_auto_mailto(text)
+        text.gsub!(/((?]*>(.*)(#{Regexp.escape(mail)})(.*)<\/a>/)
+            mail
+          else
+            content_tag('a', mail, href: "mailto:#{mail}", class: 'email')
+          end
+        end
+      end
+    end
+  end
+end
diff --git a/lib/open_project/text_formatting/formatters/markdown/helper.rb b/lib/open_project/text_formatting/formatters/markdown/helper.rb
new file mode 100644
index 00000000000..dd27267fe04
--- /dev/null
+++ b/lib/open_project/text_formatting/formatters/markdown/helper.rb
@@ -0,0 +1,48 @@
+#-- encoding: UTF-8
+#-- copyright
+# OpenProject is a project management system.
+# Copyright (C) 2012-2017 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-2017 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::TextFormatting::Formatters
+  module Markdown
+    module Helper
+      def wikitoolbar_for(field_id)
+
+        javascript_tag(<<-EOF)
+            // Toolbar for markdown. Here be dragons
+        EOF
+      end
+
+      def initial_page_content(_page)
+        "h1. #{@page.title}"
+      end
+
+      def heads_for_wiki_formatter
+      end
+    end
+  end
+end
diff --git a/lib/redmine/wiki_formatting/null_formatter/formatter.rb b/lib/open_project/text_formatting/formatters/null_formatter/formatter.rb
similarity index 75%
rename from lib/redmine/wiki_formatting/null_formatter/formatter.rb
rename to lib/open_project/text_formatting/formatters/null_formatter/formatter.rb
index de348e92d7d..2600e5e87b0 100644
--- a/lib/redmine/wiki_formatting/null_formatter/formatter.rb
+++ b/lib/open_project/text_formatting/formatters/null_formatter/formatter.rb
@@ -27,22 +27,20 @@
 # See docs/COPYRIGHT.rdoc for more details.
 #++
 
-module Redmine
-  module WikiFormatting
-    module NullFormatter
-      class Formatter
-        include ERB::Util
-        include ActionView::Helpers::TagHelper
-        include ActionView::Helpers::TextHelper
-        include ActionView::Helpers::UrlHelper
+module OpenProject::TextFormatting::Formatters
+  module NullFormatter
+    class Formatter
+      include ERB::Util
+      include ActionView::Helpers::TagHelper
+      include ActionView::Helpers::TextHelper
+      include ActionView::Helpers::UrlHelper
 
-        def initialize(text)
-          @text = text
-        end
+      def initialize(text)
+        @text = text
+      end
 
-        def to_html(*_args)
-          simple_format(auto_link(CGI::escapeHTML(@text)))
-        end
+      def to_html(*_args)
+        simple_format(auto_link(CGI::escapeHTML(@text)))
       end
     end
   end
diff --git a/lib/redmine/wiki_formatting/null_formatter/helper.rb b/lib/open_project/text_formatting/formatters/null_formatter/helper.rb
similarity index 82%
rename from lib/redmine/wiki_formatting/null_formatter/helper.rb
rename to lib/open_project/text_formatting/formatters/null_formatter/helper.rb
index 253bcbd7e0b..9c7f16adc07 100644
--- a/lib/redmine/wiki_formatting/null_formatter/helper.rb
+++ b/lib/open_project/text_formatting/formatters/null_formatter/helper.rb
@@ -27,19 +27,17 @@
 # See docs/COPYRIGHT.rdoc for more details.
 #++
 
-module Redmine
-  module WikiFormatting
-    module NullFormatter
-      module Helper
-        def wikitoolbar_for(_field_id)
-        end
+module OpenProject::TextFormatting::Formatters
+  module NullFormatter
+    module Helper
+      def wikitoolbar_for(_field_id)
+      end
 
-        def heads_for_wiki_formatter
-        end
+      def heads_for_wiki_formatter
+      end
 
-        def initial_page_content(page)
-          page.title.to_s
-        end
+      def initial_page_content(page)
+        page.title.to_s
       end
     end
   end
diff --git a/lib/open_project/text_formatting/formatters/textile/formatter.rb b/lib/open_project/text_formatting/formatters/textile/formatter.rb
new file mode 100644
index 00000000000..e90e6ed3e74
--- /dev/null
+++ b/lib/open_project/text_formatting/formatters/textile/formatter.rb
@@ -0,0 +1,143 @@
+#-- encoding: UTF-8
+#-- copyright
+# OpenProject is a project management system.
+# Copyright (C) 2012-2017 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-2017 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 'redcloth3'
+
+module OpenProject::TextFormatting::Formatters
+  module Textile
+    class Formatter < RedCloth3
+      include ERB::Util
+      include ActionView::Helpers::TagHelper
+
+      # auto_link rule after textile rules so that it doesn't break !image_url! tags
+      RULES = [:textile, :block_markdown_rule, :inline_auto_link, :inline_auto_mailto]
+
+      def initialize(*args)
+        super
+
+        self.hard_breaks = true
+        self.no_span_caps = true
+        self.filter_styles = true
+      end
+
+      def to_html(*_rules)
+        @toc = []
+        super(*RULES).to_s
+      end
+
+      private
+
+      # Patch for RedCloth.  Fixed in RedCloth r128 but _why hasn't released it yet.
+      # http://code.whytheluckystiff.net/redcloth/changeset/128
+      def hard_break(text)
+        text.gsub!(/(.)\n(?!\n|\Z| *([#*=]+(\s|$)|[{|]))/, '\\1
') if hard_breaks + end + + # Patch to add code highlighting support to RedCloth + def smooth_offtags(text) + unless @pre_list.empty? + ## replace
 content
+          text.gsub!(//) do
+            content = @pre_list[$1.to_i]
+            if content.match(/\s?(.+)/m)
+              content = "" +
+                Redmine::SyntaxHighlighting.highlight_by_language($2, $1)
+            end
+            content
+          end
+        end
+      end
+
+
+      def auto_link_regexp
+        @auto_link_regexp ||= begin
+          %r{
+              (                          # leading text
+                <\w+.*?>|                # leading HTML tag, or
+                [^=<>!:'"/]|             # leading punctuation, or
+                \{\{\w+\(|               # inside a macro?
+                ^                        # beginning of line
+              )
+              (
+                (?:https?://)|           # protocol spec, or
+                (?:s?ftps?://)|
+                (?:www\.)                # www.*
+              )
+              (
+                (\S+?)                   # url
+                (\/)?                    # slash
+              )
+              ((?:>)?|[^\w\=\/;\(\)]*?)               # post
+              (?=<|\s|$)
+             }x
+        end
+      end
+
+      # Turns all urls into clickable links (code from Rails).
+      def inline_auto_link(text)
+        text.gsub!(auto_link_regexp) do
+          all = $&
+          leading = $1
+          proto = $2
+          url = $3
+          post = $6
+          if url.nil? || leading =~ /=]?/ || leading =~ /\{\{\w+\(/
+            # don't replace URLs that are already linked
+            # and URLs prefixed with ! !> !< != (textile images)
+            all
+          else
+            # Idea below : an URL with unbalanced parethesis and
+            # ending by ')' is put into external parenthesis
+            if url[-1] == ?) and ((url.count('(') - url.count(')')) < 0)
+              url = url[0..-2] # discard closing parenth from url
+              post = ')' + post # add closing parenth to post
+            end
+            tag = content_tag('a',
+                              proto + url,
+                              href: "#{proto == 'www.' ? 'http://www.' : proto}#{url}",
+                              class: 'external icon-context icon-copy')
+            %(#{leading}#{tag}#{post})
+          end
+        end
+      end
+
+      # Turns all email addresses into clickable links (code from Rails).
+      def inline_auto_mailto(text)
+        text.gsub!(/((?]*>(.*)(#{Regexp.escape(mail)})(.*)<\/a>/)
+            mail
+          else
+            content_tag('a', mail, href: "mailto:#{mail}", class: 'email')
+          end
+        end
+      end
+    end
+  end
+end
diff --git a/lib/open_project/text_formatting/formatters/textile/helper.rb b/lib/open_project/text_formatting/formatters/textile/helper.rb
new file mode 100644
index 00000000000..5148e565e78
--- /dev/null
+++ b/lib/open_project/text_formatting/formatters/textile/helper.rb
@@ -0,0 +1,66 @@
+#-- encoding: UTF-8
+#-- copyright
+# OpenProject is a project management system.
+# Copyright (C) 2012-2017 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-2017 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::TextFormatting::Formatters
+  module Textile
+    module Helper
+      def wikitoolbar_for(field_id)
+        heads_for_wiki_formatter
+        url = url_for(controller: '/help', action: 'wiki_syntax')
+        open_help = "window.open(\"#{ url }\", \"\", \"resizable=yes, location=no, width=600, " +
+          "height=640, menubar=no, status=no, scrollbars=yes\"); return false;"
+        help_button = content_tag :button,
+                                  '',
+                                  type: 'button',
+                                  class: 'jstb_help',
+                                  onclick: open_help,
+                                  :'aria-label' => ::I18n.t('js.inplace.link_formatting_help'),
+                                  title: ::I18n.t('js.inplace.link_formatting_help')
+
+        javascript_tag(<<-EOF)
+          // initialSetup the toolbar later, so that i18n-js has a chance to set the translations
+          // for the wiki-buttons first.
+          jQuery(document).ready(function(){
+            var wikiToolbar = new jsToolBar(document.getElementById('#{field_id}'));
+
+            wikiToolbar.setHelpLink(jQuery('#{escape_javascript help_button}')[0]);
+            wikiToolbar.draw();
+          });
+        EOF
+      end
+
+      def initial_page_content(_page)
+        "h1. #{@page.title}"
+      end
+
+      def heads_for_wiki_formatter
+      end
+    end
+  end
+end
diff --git a/lib/open_project/text_formatting/formatters/textile/legacy_text_formatting.rb b/lib/open_project/text_formatting/formatters/textile/legacy_text_formatting.rb
new file mode 100644
index 00000000000..ceb9441b01b
--- /dev/null
+++ b/lib/open_project/text_formatting/formatters/textile/legacy_text_formatting.rb
@@ -0,0 +1,519 @@
+#-- encoding: UTF-8
+#-- copyright
+# OpenProject is a project management system.
+# Copyright (C) 2012-2017 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-2017 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::TextFormatting::Formatters
+  module Textile
+    module LegacyTextFormatting
+      include Redmine::WikiFormatting::Macros::Definitions
+      include ActionView::Helpers::SanitizeHelper
+      include ERB::Util # for h()
+      include Redmine::I18n
+      include ActionView::Helpers::TextHelper
+      include OpenProject::ObjectLinking
+      # The WorkPackagesHelper is required to get access to the methods
+      # 'work_package_css_classes' and 'work_package_quick_info'.
+      include WorkPackagesHelper
+
+      class << self
+        # 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[: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
+
+          # offer 'plain' as readable version for 'no formatting' to callers
+          format = options[:format] == 'plain' ? '' : options[:format]
+          text = OpenProject::TextFormatting::Formatters.formatter_for(format).new(text).to_html
+
+          # 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) { |text|
+            [:execute_macros, :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
+          }
+
+          if @parsed_headings.any?
+            replace_toc(text, @parsed_headings)
+          end
+
+          escape_non_macros(text)
+          text.html_safe
+        end
+
+        ##
+        # Escape double curly braces after macro expansion.
+        # This will avoid arbitrary angular expressions to be evaluated in
+        # formatted text marked html_safe.
+        def escape_non_macros(text)
+          text.gsub!(/\{\{(?! \$root\.DOUBLE_LEFT_CURLY_BRACE)/, '{{ $root.DOUBLE_LEFT_CURLY_BRACE }}')
+        end
+
+        def parse_non_pre_blocks(text)
+          s = StringScanner.new(text)
+          tags = []
+          parsed = ''
+          while !s.eos?
+            s.scan(/(.*?)(<(\/)?(pre|code)(.*?)>|\z)/im)
+            text = s[1]
+            full_tag = s[2]
+            closing = s[3]
+            tag = 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
+
+
+        MACROS_RE = /
+                      (!)?                        # escaping
+                      (
+                      \{\{                        # opening tag
+                      ([\w]+)                     # macro name
+                      (\(([^\}]*)\))?             # optional arguments
+                      \}\}                        # closing tag
+                      )
+                    /x unless const_defined?(:MACROS_RE)
+
+        # Macros substitution
+        def execute_macros(text, project, obj, _attr, _only_path, options)
+          return if !!options[:edit]
+          text.gsub!(MACROS_RE) do
+            esc = $1
+            all = $2
+            macro = $3
+            args = ($5 || '').split(',').each(&:strip!)
+            if esc.nil?
+              begin
+                exec_macro(macro, obj, args, view: self, project: project)
+              rescue => e
+                "\
+                #{::I18n.t(:macro_execution_error, macro_name: macro)} (#{e})\
+                ".squish
+              rescue NotImplementedError
+                "\
+                #{::I18n.t(:macro_unavailable, macro_name: macro)}\
+                ".squish
+              end || all
+            else
+              all
+            end
+          end
+        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 = $1
+            relative_url = $2 || $3
+            next m unless href.present?
+            if defined?(request) && request.present?
+              # we have a request!
+              protocol = request.protocol
+              host_with_port = 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 = "#{url_opts[:protocol]}://"
+              host_with_port = 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 = $1.downcase
+              ext = $2
+              alt = $3
+              alttext = $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 = $1
+            all = $2
+            page = $3
+            title = $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 = $1
+                  anchor = $2
+                end
+                # Unescape the escaped entities from textile
+                page = CGI.unescapeHTML(page)
+                # check if page exists
+                wiki_page = link_project.wiki.find_page(page)
+                wiki_title = wiki_page.nil? ? page : wiki_page.title
+                url = case options[:wiki_links]
+                when :local;
+                  "#{title}.html"
+                when :anchor;
+                  "##{title}" # used for single-file wiki export
+                else
+                  wiki_page_id = wiki_page.nil? ? page.to_url : wiki_page.slug
+                  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 || wiki_title), 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|user)?((#+|r)(\d+)|(:)([^"\s<>][^\s<>]*?|"[^"]+?"))(?=(?=[[:punct:]]\W)|,|\s|\]|<|$)}) do |_m|
+            leading = $1
+            esc = $2
+            project_prefix = $3
+            project_identifier = $4
+            prefix = $5
+            sep = $7 || $9
+            identifier = $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: project.repository.id, revision: 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
+                    .includes(:status)
+                    .references(:statuses)
+                    .find_by(id: oid)
+                    link = link_to("##{oid}",
+                                   work_package_path_or_url(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.includes(:parent).find_by(id: oid)
+                    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
+                when 'user'
+                  if user = User.in_visible_project.find_by(id: oid)
+                    link = link_to_user(user, class: 'user-mention')
+                  end
+                end
+              elsif sep == '##'
+                oid = identifier.to_i
+                if work_package = WorkPackage.visible
+                  .includes(:status)
+                  .references(:statuses)
+                  .find_by(id: oid)
+                  link = work_package_quick_info(work_package, only_path: only_path)
+                end
+              elsif sep == '###'
+                oid = identifier.to_i
+                work_package = WorkPackage.visible
+                  .includes(:status)
+                  .references(:statuses)
+                  .find_by(id: oid)
+                if work_package && obj && !(attr == :description && obj.id == work_package.id)
+                  link = work_package_quick_info_with_description(work_package, only_path: only_path)
+                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.where(['repository_id = ? AND scmid LIKE ?', project.repository.id, "#{name}%"]).first)
+                    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(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 = $1
+                    rev = $3
+                    anchor = $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'
+                  p = Project
+                    .visible
+                    .where(['projects.identifier = :s OR LOWER(projects.name) = :s',
+                            { s: name.downcase }])
+                    .first
+                  if p
+                    link = link_to_project(p, { only_path: only_path }, class: 'project')
+                  end
+                when 'user'
+                  if user = User.in_visible_project.find_by(login: name)
+                    link = link_to_user(user, class: 'user-mention')
+                  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 = $1.to_i
+            attrs = $2
+            content = $3
+            item = strip_tags(content).strip
+            tocitem = strip_tags(content.gsub(/
/, ' ')) + anchor = item.gsub(%r{[^\w\s\-]}, '').gsub(%r{\s+(\-+\s*)?}, '-') + @parsed_headings << [level, anchor, tocitem] + url = full_url(anchor) + "
\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 = "

" + 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 + url = full_url anchor + out << "#{item}" + current = level + started = true + end + out << '
' * (current - root) + out << '' + out << '
' + end + end + end + + # + # displays the current url plus an optional anchor + # + def full_url(anchor_name = '') + return "##{anchor_name}" if current_request.nil? + current = request.original_fullpath + return current if anchor_name.blank? + "#{current}##{anchor_name}" + end + + def current_request + request rescue nil + end + end + end + end +end diff --git a/lib/open_project/text_formatting/pipeline.rb b/lib/open_project/text_formatting/pipeline.rb new file mode 100644 index 00000000000..4e4695fff4d --- /dev/null +++ b/lib/open_project/text_formatting/pipeline.rb @@ -0,0 +1,72 @@ +#-- encoding: UTF-8 + +#-- copyright +# OpenProject is a project management system. +# Copyright (C) 2012-2017 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-2017 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::TextFormatting + class Pipeline + attr_reader :formatter, + :context, + :pipeline + + def initialize(formatter, context:) + @formatter = formatter + @context = context + + @pipeline = HTML::Pipeline.new(located_filters, context) + end + + def to_html(text, call_context = {}) + pipeline.to_html text, call_context + end + + def to_document(text, call_context = {}) + pipeline.to_document text, call_context + end + + def filters + [ + formatter, + :sanitization + ] + end + + protected + + def located_filters + filters.map do |f| + if [Symbol, String].include? f.class + OpenProject::TextFormatting::Filters.const_get("#{f}_filter".classify) + else + f + end + end + end + end +end + diff --git a/lib/redmine/wiki_formatting/textile/formatter.rb b/lib/redmine/wiki_formatting/textile/formatter.rb deleted file mode 100644 index 54cb0ea0b67..00000000000 --- a/lib/redmine/wiki_formatting/textile/formatter.rb +++ /dev/null @@ -1,145 +0,0 @@ -#-- encoding: UTF-8 -#-- copyright -# OpenProject is a project management system. -# Copyright (C) 2012-2018 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-2017 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 docs/COPYRIGHT.rdoc for more details. -#++ - -require 'redcloth3' - -module Redmine - module WikiFormatting - module Textile - class Formatter < RedCloth3 - include ERB::Util - include ActionView::Helpers::TagHelper - - # auto_link rule after textile rules so that it doesn't break !image_url! tags - RULES = [:textile, :block_markdown_rule, :inline_auto_link, :inline_auto_mailto] - - def initialize(*args) - super - - self.hard_breaks = true - self.no_span_caps = true - self.filter_styles = true - end - - def to_html(*_rules) - @toc = [] - super(*RULES).to_s - end - - private - - # Patch for RedCloth. Fixed in RedCloth r128 but _why hasn't released it yet. - # http://code.whytheluckystiff.net/redcloth/changeset/128 - def hard_break(text) - text.gsub!(/(.)\n(?!\n|\Z| *([#*=]+(\s|$)|[{|]))/, '\\1
') if hard_breaks - end - - # Patch to add code highlighting support to RedCloth - def smooth_offtags(text) - unless @pre_list.empty? - ## replace
 content
-            text.gsub!(//) do
-              content = @pre_list[$1.to_i]
-              if content.match(/\s?(.+)/m)
-                content = "" +
-                          Redmine::SyntaxHighlighting.highlight_by_language($2, $1)
-              end
-              content
-            end
-          end
-        end
-
-
-        def auto_link_regexp
-          @auto_link_regexp ||= begin
-            %r{
-              (                          # leading text
-                <\w+.*?>|                # leading HTML tag, or
-                [^=<>!:'"/]|             # leading punctuation, or
-                \{\{\w+\(|               # inside a macro?
-                ^                        # beginning of line
-              )
-              (
-                (?:https?://)|           # protocol spec, or
-                (?:s?ftps?://)|
-                (?:www\.)                # www.*
-              )
-              (
-                (\S+?)                   # url
-                (\/)?                    # slash
-              )
-              ((?:>)?|[^\w\=\/;\(\)]*?)               # post
-              (?=<|\s|$)
-             }x
-          end
-        end
-
-        # Turns all urls into clickable links (code from Rails).
-        def inline_auto_link(text)
-          text.gsub!(auto_link_regexp) do
-            all = $&
-            leading = $1
-            proto = $2
-            url = $3
-            post = $6
-            if url.nil? || leading =~ /=]?/ || leading =~ /\{\{\w+\(/
-              # don't replace URLs that are already linked
-              # and URLs prefixed with ! !> !< != (textile images)
-              all
-            else
-              # Idea below : an URL with unbalanced parethesis and
-              # ending by ')' is put into external parenthesis
-              if url[-1] == ?) and ((url.count('(') - url.count(')')) < 0)
-                url = url[0..-2] # discard closing parenth from url
-                post = ')' + post # add closing parenth to post
-              end
-              tag = content_tag('a',
-                                proto + url,
-                                href: "#{proto == 'www.' ? 'http://www.' : proto}#{url}",
-                                class: 'external icon-context icon-copy')
-              %(#{leading}#{tag}#{post})
-            end
-          end
-        end
-
-        # Turns all email addresses into clickable links (code from Rails).
-        def inline_auto_mailto(text)
-          text.gsub!(/((?]*>(.*)(#{Regexp.escape(mail)})(.*)<\/a>/)
-              mail
-            else
-              content_tag('a', mail, href: "mailto:#{mail}", class: 'email')
-            end
-          end
-        end
-      end
-    end
-  end
-end
diff --git a/lib/redmine/wiki_formatting/textile/helper.rb b/lib/redmine/wiki_formatting/textile/helper.rb
deleted file mode 100644
index 00c0f1cdb91..00000000000
--- a/lib/redmine/wiki_formatting/textile/helper.rb
+++ /dev/null
@@ -1,68 +0,0 @@
-#-- encoding: UTF-8
-#-- copyright
-# OpenProject is a project management system.
-# Copyright (C) 2012-2018 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-2017 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 docs/COPYRIGHT.rdoc for more details.
-#++
-
-module Redmine
-  module WikiFormatting
-    module Textile
-      module Helper
-        def wikitoolbar_for(field_id)
-          heads_for_wiki_formatter
-          url = url_for(controller: '/help', action: 'wiki_syntax')
-          open_help = "window.open(\"#{ url }\", \"\", \"resizable=yes, location=no, width=600, " +
-                      "height=640, menubar=no, status=no, scrollbars=yes\"); return false;"
-          help_button = content_tag :button,
-                                    '',
-                                    type: 'button',
-                                    class: 'jstb_help',
-                                    onclick: open_help,
-                                    :'aria-label' => ::I18n.t('js.inplace.link_formatting_help'),
-                                    title: ::I18n.t('js.inplace.link_formatting_help')
-
-          javascript_tag(<<-EOF)
-            // initialSetup the toolbar later, so that i18n-js has a chance to set the translations
-            // for the wiki-buttons first.
-            jQuery(document).ready(function(){
-              var wikiToolbar = new jsToolBar(document.getElementById('#{field_id}'));
-
-              wikiToolbar.setHelpLink(jQuery('#{escape_javascript help_button}')[0]);
-              wikiToolbar.draw();
-            });
-          EOF
-        end
-
-        def initial_page_content(_page)
-          "h1. #{@page.title}"
-        end
-
-        def heads_for_wiki_formatter
-        end
-      end
-    end
-  end
-end