summaryrefslogtreecommitdiffstats
path: root/lib/redmine
diff options
context:
space:
mode:
Diffstat (limited to 'lib/redmine')
-rw-r--r--lib/redmine/activity.rb15
-rw-r--r--lib/redmine/core_ext/string/conversions.rb2
-rw-r--r--lib/redmine/database.rb2
-rw-r--r--lib/redmine/diff.rb8
-rw-r--r--lib/redmine/field_format.rb29
-rw-r--r--lib/redmine/helpers/gantt.rb42
-rw-r--r--lib/redmine/i18n.rb25
-rw-r--r--lib/redmine/plugin.rb10
-rw-r--r--lib/redmine/preparation.rb9
-rw-r--r--lib/redmine/quote_reply.rb25
-rw-r--r--lib/redmine/reaction.rb70
-rw-r--r--lib/redmine/scm/adapters/mercurial_adapter.rb22
-rw-r--r--lib/redmine/sort_criteria.rb4
-rw-r--r--lib/redmine/subclass_factory.rb4
-rw-r--r--lib/redmine/syntax_highlighting.rb1
-rw-r--r--lib/redmine/version.rb2
-rw-r--r--lib/redmine/wiki_formatting/common_mark/alerts_icons_filter.rb63
-rw-r--r--lib/redmine/wiki_formatting/common_mark/formatter.rb9
-rw-r--r--lib/redmine/wiki_formatting/common_mark/sanitization_filter.rb70
-rw-r--r--lib/redmine/wiki_formatting/macros.rb2
20 files changed, 322 insertions, 92 deletions
diff --git a/lib/redmine/activity.rb b/lib/redmine/activity.rb
index 826b81c9e..c972eeee2 100644
--- a/lib/redmine/activity.rb
+++ b/lib/redmine/activity.rb
@@ -19,11 +19,11 @@
module Redmine
module Activity
- mattr_accessor :available_event_types, :default_event_types, :plugins_event_types, :providers
+ mattr_accessor :available_event_types, :default_event_types, :plugins_event_classes, :providers
@@available_event_types = []
@@default_event_types = []
- @@plugins_event_types = {}
+ @@plugins_event_classes = {}
@@providers = Hash.new {|h, k| h[k]=[]}
class << self
@@ -41,19 +41,22 @@ module Redmine
@@available_event_types << event_type unless @@available_event_types.include?(event_type)
@@default_event_types << event_type unless options[:default] == false
- @@plugins_event_types = { event_type => options[:plugin].to_s } unless options[:plugin].nil?
+ if options[:plugin]
+ providers.each do |provider|
+ @@plugins_event_classes[provider] = options[:plugin].to_s
+ end
+ end
@@providers[event_type] += providers
end
def delete(event_type)
@@available_event_types.delete event_type
@@default_event_types.delete event_type
- @@plugins_event_types.delete(event_type)
@@providers.delete(event_type)
end
- def plugin_name(event_type)
- @@plugins_event_types[event_type]
+ def plugin_name(class_name)
+ @@plugins_event_classes[class_name.to_s]
end
end
end
diff --git a/lib/redmine/core_ext/string/conversions.rb b/lib/redmine/core_ext/string/conversions.rb
index e98e400e4..bbf57e363 100644
--- a/lib/redmine/core_ext/string/conversions.rb
+++ b/lib/redmine/core_ext/string/conversions.rb
@@ -39,7 +39,7 @@ module Redmine
end
# 2,5 => 2.5
s.tr!(',', '.')
- begin; Kernel.Float(s); rescue; nil; end
+ Kernel.Float(s, exception: false)
end
end
end
diff --git a/lib/redmine/database.rb b/lib/redmine/database.rb
index b3cbdc661..13c92b8a4 100644
--- a/lib/redmine/database.rb
+++ b/lib/redmine/database.rb
@@ -58,7 +58,7 @@ module Redmine
# Returns true if the database is MySQL
def mysql?
- /mysql/i.match?(ActiveRecord::Base.connection.adapter_name)
+ /mysql|trilogy/i.match?(ActiveRecord::Base.connection.adapter_name)
end
def mysql_version
diff --git a/lib/redmine/diff.rb b/lib/redmine/diff.rb
index 40c444a42..c925d463a 100644
--- a/lib/redmine/diff.rb
+++ b/lib/redmine/diff.rb
@@ -76,13 +76,9 @@ module Redmine
def line_to_html_raw(line, offsets)
if offsets
s = +''
- unless offsets.first == 0
- s << CGI.escapeHTML(line[0..offsets.first-1])
- end
+ s << CGI.escapeHTML(line[0..(offsets.first - 1)]) unless offsets.first == 0
s << '<span>' + CGI.escapeHTML(line[offsets.first..offsets.last]) + '</span>'
- unless offsets.last == -1
- s << CGI.escapeHTML(line[offsets.last+1..-1])
- end
+ s << CGI.escapeHTML(line[(offsets.last + 1)..-1]) unless offsets.last == -1
s
else
CGI.escapeHTML(line)
diff --git a/lib/redmine/field_format.rb b/lib/redmine/field_format.rb
index c4fd1b592..39b21c874 100644
--- a/lib/redmine/field_format.rb
+++ b/lib/redmine/field_format.rb
@@ -110,8 +110,8 @@ module Redmine
end
private_class_method :add
- def self.field_attributes(*args)
- CustomField.store_accessor :format_store, *args
+ def self.field_attributes(*)
+ CustomField.store_accessor(:format_store, *)
end
field_attributes :url_pattern, :full_width_layout
@@ -1086,8 +1086,15 @@ module Redmine
class ProgressbarFormat < Numeric
add 'progressbar'
- self.form_partial = nil
+ self.form_partial = 'custom_fields/formats/progressbar'
self.totalable_supported = false
+ field_attributes :ratio_interval
+
+ # Take the default value from Setting.issue_done_ratio_interval.to_i
+ # in order to have a consistent behaviour for default ratio interval.
+ def self.default_ratio_interval
+ Setting.issue_done_ratio_interval.to_i
+ end
def label
"label_progressbar"
@@ -1112,11 +1119,19 @@ module Redmine
order_statement(custom_field)
end
+ def before_custom_field_save(custom_field)
+ super
+
+ if custom_field.ratio_interval.blank?
+ custom_field.ratio_interval = self.class.default_ratio_interval
+ end
+ end
+
def edit_tag(view, tag_id, tag_name, custom_value, options={})
view.select_tag(
tag_name,
view.options_for_select(
- (0..100).step(Setting.issue_done_ratio_interval.to_i).to_a.collect {|r| ["#{r} %", r]},
+ (0..100).step(custom_value.custom_field.ratio_interval.to_i).to_a.collect {|r| ["#{r} %", r]},
custom_value.value
),
options.merge(id: tag_id, style: "width: 75px;")
@@ -1124,17 +1139,17 @@ module Redmine
end
def bulk_edit_tag(view, tag_id, tag_name, custom_field, objects, value, options={})
- opts = view.options_for_select([[l(:label_no_change_option), '']] + (0..100).step(Setting.issue_done_ratio_interval.to_i).to_a.collect {|r| ["#{r} %", r]})
+ opts = view.options_for_select([[l(:label_no_change_option), '']] + (0..100).step(custom_field.ratio_interval.to_i).to_a.collect {|r| ["#{r} %", r]})
view.select_tag(tag_name, opts, options.merge(id: tag_id, style: "width: 75px;")) +
bulk_clear_tag(view, tag_id, tag_name, custom_field, value)
end
def formatted_value(view, custom_field, value, customized=nil, html=false)
- text = "#{value}%"
if html
+ text = "#{value}%"
view.progress_bar(value.to_i, legend: (text if view.action_name == 'show'))
else
- text
+ value.to_s
end
end
end
diff --git a/lib/redmine/helpers/gantt.rb b/lib/redmine/helpers/gantt.rb
index 43e0986c6..d23c40b38 100644
--- a/lib/redmine/helpers/gantt.rb
+++ b/lib/redmine/helpers/gantt.rb
@@ -198,12 +198,18 @@ module Redmine
# Returns the distinct versions of the issues that belong to +project+
def project_versions(project)
- project_issues(project).filter_map(&:fixed_version).uniq
+ @project_versions ||= {}
+ @project_versions[project&.id] ||= begin
+ ids = project_issues(project).filter_map(&:fixed_version_id).uniq
+ Version.where(id: ids).to_a
+ end
end
# Returns the issues that belong to +project+ and are assigned to +version+
def version_issues(project, version)
- project_issues(project).select {|issue| issue.fixed_version == version}
+ @version_issues ||= {}
+ @version_issues[[project&.id, version&.id]] ||=
+ project_issues(project).select {|issue| issue.fixed_version_id == version&.id}
end
def render(options={})
@@ -232,7 +238,7 @@ module Redmine
render_object_row(project, options)
increment_indent(options) do
# render issue that are not assigned to a version
- issues = project_issues(project).select {|i| i.fixed_version.nil?}
+ issues = project_issues(project).select {|i| i.fixed_version_id.nil?}
render_issues(issues, options)
# then render project versions and their issues
versions = project_versions(project)
@@ -396,7 +402,15 @@ module Redmine
Redmine::Configuration['rmagick_font_path'].presence
img = MiniMagick::Image.create(".#{format}")
if Redmine::Configuration['imagemagick_convert_command'].present?
- MiniMagick.cli_path = File.dirname(Redmine::Configuration['imagemagick_convert_command'])
+ if MiniMagick.respond_to?(:cli_path)
+ MiniMagick.cli_path = File.dirname(Redmine::Configuration['imagemagick_convert_command'])
+ else
+ Rails.logger.warn(
+ 'imagemagick_convert_command option is ignored ' \
+ 'because MiniMagick has removed the option to define a custom path for the binary. ' \
+ 'Please ensure the convert binary is available in your PATH.'
+ )
+ end
end
MiniMagick.convert do |gc|
gc.size('%dx%d' % [subject_width + g_width + 1, height])
@@ -494,7 +508,7 @@ module Redmine
lines(:image => gc, :top => top, :zoom => zoom,
:subject_width => subject_width, :format => :image)
# today red line
- if User.current.today >= @date_from and User.current.today <= date_to
+ if User.current.today.between?(@date_from, date_to)
gc.stroke('red')
x = (User.current.today - @date_from + 1) * zoom + subject_width
gc.draw('line %g,%g %g,%g' % [
@@ -717,7 +731,7 @@ module Redmine
css_classes = +''
css_classes << ' issue-overdue' if issue.overdue?
css_classes << ' issue-behind-schedule' if issue.behind_schedule?
- css_classes << ' icon icon-issue' unless Setting.gravatar_enabled? && issue.assigned_to
+ css_classes << ' icon icon-issue' unless issue.assigned_to
css_classes << ' issue-closed' if issue.closed?
if issue.start_date && issue.due_before && issue.done_ratio
progress_date = calc_progress_date(issue.start_date,
@@ -726,8 +740,8 @@ module Redmine
css_classes << ' over-end-date' if progress_date > self.date_to && issue.done_ratio > 0
end
s = (+"").html_safe
- s << view.sprite_icon('issue').html_safe unless Setting.gravatar_enabled? && issue.assigned_to
- s << view.assignee_avatar(issue.assigned_to, :size => 13, :class => 'icon-gravatar')
+ s << view.sprite_icon('issue').html_safe unless issue.assigned_to
+ s << view.assignee_avatar(issue.assigned_to, :size => 13, :class => 'icon-avatar')
s << view.link_to_issue(issue).html_safe
s << view.content_tag(:input, nil, :type => 'checkbox', :name => 'ids[]',
:value => issue.id, :style => 'display:none;',
@@ -740,7 +754,7 @@ module Redmine
html_class << (version.behind_schedule? ? 'version-behind-schedule' : '') << " "
html_class << (version.overdue? ? 'version-overdue' : '')
html_class << ' version-closed' unless version.open?
- if version.start_date && version.due_date && version.visible_fixed_issues.completed_percent
+ if version.due_date && version.start_date && version.visible_fixed_issues.completed_percent
progress_date = calc_progress_date(version.start_date,
version.due_date, version.visible_fixed_issues.completed_percent)
html_class << ' behind-start-date' if progress_date < self.date_from
@@ -770,10 +784,14 @@ module Redmine
tag_options[:id] = "issue-#{object.id}"
tag_options[:class] = "issue-subject hascontextmenu"
tag_options[:title] = object.subject
- children = object.leaf? ? [] : object.children & project_issues(object.project)
has_children =
- children.present? &&
- children.collect(&:fixed_version).uniq.intersect?([object.fixed_version])
+ if object.leaf?
+ false
+ else
+ children = object.children & project_issues(object.project)
+ fixed_version_id = object.fixed_version_id
+ children.any? {|child| child.fixed_version_id == fixed_version_id}
+ end
when Version
tag_options[:id] = "version-#{object.id}"
tag_options[:class] = "version-name"
diff --git a/lib/redmine/i18n.rb b/lib/redmine/i18n.rb
index dc59819a2..0b31cb235 100644
--- a/lib/redmine/i18n.rb
+++ b/lib/redmine/i18n.rb
@@ -65,9 +65,9 @@ module Redmine
end
# Localizes the given args with user's language
- def lu(user, *args)
+ def lu(user, *)
lang = user.try(:language).presence || Setting.default_language
- ll(lang, *args)
+ ll(lang, *)
end
def format_date(date)
@@ -152,7 +152,7 @@ module Redmine
languages_options :cache => false
end
end
- options.map {|name, lang| [name.force_encoding("UTF-8"), lang.force_encoding("UTF-8")]}
+ options.map {|name, lang| [(+name).force_encoding("UTF-8"), (+lang).force_encoding("UTF-8")]}
end
def find_language(lang)
@@ -173,24 +173,5 @@ module Redmine
def current_language
::I18n.locale
end
-
- # Custom backend based on I18n::Backend::Simple with the following changes:
- # * available_locales are determined by looking at translation file names
- class Backend < ::I18n::Backend::Simple
- module Implementation
- # Get available locales from the translations filenames
- def available_locales
- @available_locales ||= begin
- redmine_locales = Dir[Rails.root / 'config' / 'locales' / '*.yml'].map { |f| File.basename(f, '.yml').to_sym }
- super & redmine_locales
- end
- end
- end
-
- # Adds custom pluralization rules
- include ::I18n::Backend::Pluralization
- # Adds fallback to default locale for untranslated strings
- include ::I18n::Backend::Fallbacks
- end
end
end
diff --git a/lib/redmine/plugin.rb b/lib/redmine/plugin.rb
index 223b3927a..74ae4efaf 100644
--- a/lib/redmine/plugin.rb
+++ b/lib/redmine/plugin.rb
@@ -250,7 +250,7 @@ module Redmine
)
end
elsif req.is_a?(Range)
- unless compare_versions(req.first, current) <= 0 && compare_versions(req.last, current) >= 0
+ unless compare_versions(req.first, current) <= 0 && compare_versions(req.last, current) >= 0 # rubocop:disable Style/ComparableBetween
raise PluginRequirementError.new(
"#{id} plugin requires a Redmine version between #{req.first} " \
"and #{req.last} but current is #{current.join('.')}"
@@ -406,8 +406,8 @@ module Redmine
# Meeting.find_events('scrums', User.current, 5.days.ago, Date.today, :project => foo) # events for project foo only
#
# Note that :view_scrums permission is required to view these events in the activity view.
- def activity_provider(*args)
- Redmine::Activity.register(*args)
+ def activity_provider(*)
+ Redmine::Activity.register(*)
end
# Registers a wiki formatter.
@@ -423,8 +423,8 @@ module Redmine
# Examples:
# wiki_format_provider(:custom_formatter, CustomFormatter, :label => "My custom formatter")
#
- def wiki_format_provider(name, *args)
- Redmine::WikiFormatting.register(name, *args)
+ def wiki_format_provider(name, *)
+ Redmine::WikiFormatting.register(name, *)
end
# Register plugin models that use acts_as_attachable.
diff --git a/lib/redmine/preparation.rb b/lib/redmine/preparation.rb
index 822662e11..a7387f5dc 100644
--- a/lib/redmine/preparation.rb
+++ b/lib/redmine/preparation.rb
@@ -280,6 +280,11 @@ module Redmine
{:controller => 'auth_sources', :action => 'index'},
:icon => 'server-authentication',
:html => {:class => 'icon icon-server-authentication'}
+ menu.push :applications, {:controller => 'oauth2_applications', :action => 'index'},
+ :if => Proc.new { Setting.rest_api_enabled? },
+ :caption => :'doorkeeper.layouts.admin.nav.applications',
+ :icon => 'apps',
+ :html => {:class => 'icon icon-applications'}
menu.push :plugins, {:controller => 'admin', :action => 'plugins'},
:last => true,
:icon => 'plugins',
@@ -408,9 +413,7 @@ module Redmine
WikiFormatting.map do |format|
format.register :textile
- if Object.const_defined?(:Commonmarker)
- format.register :common_mark, label: 'CommonMark Markdown (GitHub Flavored)'
- end
+ format.register :common_mark, label: 'CommonMark Markdown (GitHub Flavored)'
end
ActionView::Template.register_template_handler :rsb, Views::ApiTemplateHandler
diff --git a/lib/redmine/quote_reply.rb b/lib/redmine/quote_reply.rb
index 05737c079..f6d7821cd 100644
--- a/lib/redmine/quote_reply.rb
+++ b/lib/redmine/quote_reply.rb
@@ -20,21 +20,18 @@
module Redmine
module QuoteReply
module Helper
- def javascripts_for_quote_reply_include_tag
- javascript_include_tag 'turndown-7.2.0.min', 'quote_reply'
- end
-
- def quote_reply(url, selector_for_content, icon_only: false)
- quote_reply_function = "quoteReply('#{j url}', '#{j selector_for_content}', '#{j Setting.text_formatting}')"
+ def quote_reply_button(url:, icon_only: false)
+ button_params = {
+ data: {
+ action: 'quote-reply#quote',
+ quote_reply_url_param: url,
+ quote_reply_text_formatting_param: Setting.text_formatting
+ },
+ class: "#{icon_only ? "icon-only" : "icon"} icon-quote"
+ }
+ button_params[:title] = l(:button_quote) if icon_only
- html_options = { class: 'icon icon-comment' }
- html_options[:title] = l(:button_quote) if icon_only
-
- link_to_function(
- sprite_icon('comment', l(:button_quote), icon_only: icon_only),
- quote_reply_function,
- html_options
- )
+ link_to sprite_icon('quote-filled', l(:button_quote), icon_only: icon_only, style: :filled), '#', button_params
end
end
diff --git a/lib/redmine/reaction.rb b/lib/redmine/reaction.rb
new file mode 100644
index 000000000..09fb78ef8
--- /dev/null
+++ b/lib/redmine/reaction.rb
@@ -0,0 +1,70 @@
+# frozen_string_literal: true
+
+# Redmine - project management software
+# Copyright (C) 2006- Jean-Philippe Lang
+#
+# 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.
+
+module Redmine
+ module Reaction
+ # Types of objects that can have reactions
+ REACTABLE_TYPES = %w(Journal Issue Message News Comment)
+
+ # Returns true if the user can view the reaction of the object
+ def self.visible?(object, user = User.current)
+ Setting.reactions_enabled? && object.visible?(user)
+ end
+
+ # Returns true if the user can add/remove a reaction to/from the object
+ def self.editable?(object, user = User.current)
+ user.logged? && visible?(object, user) && object&.project&.active?
+ end
+
+ module Reactable
+ extend ActiveSupport::Concern
+
+ included do
+ has_many :reactions, as: :reactable, dependent: :delete_all
+
+ attr_writer :reaction_detail
+ end
+
+ class_methods do
+ # Preloads reaction details for a collection of objects
+ def preload_reaction_details(objects)
+ return unless Setting.reactions_enabled?
+
+ details = ::Reaction.build_detail_map_for(objects, User.current)
+
+ objects.each do |object|
+ object.reaction_detail = details.fetch(object.id) { ::Reaction::Detail.new }
+ end
+ end
+ end
+
+ def reaction_detail
+ # Loads and returns reaction details if they are not already loaded.
+ # This is intended for cases where explicit preloading is unnecessary,
+ # such as retrieving reactions for a single issue on its detail page.
+ load_reaction_detail unless defined?(@reaction_detail)
+ @reaction_detail
+ end
+
+ def load_reaction_detail
+ self.class.preload_reaction_details([self])
+ end
+ end
+ end
+end
diff --git a/lib/redmine/scm/adapters/mercurial_adapter.rb b/lib/redmine/scm/adapters/mercurial_adapter.rb
index 28a1922ca..562dd59d5 100644
--- a/lib/redmine/scm/adapters/mercurial_adapter.rb
+++ b/lib/redmine/scm/adapters/mercurial_adapter.rb
@@ -50,7 +50,10 @@ module Redmine
end
def client_available
- client_version_above?([5, 1])
+ client_version_above?([5, 1]) &&
+ # Redmine >= 6.1 has dropped support for Python 2.7, and
+ # Mercurial has never supported Python 3.0 to 3.4
+ (python_version <=> [3, 5]) >= 0
end
def hgversion
@@ -67,6 +70,23 @@ module Redmine
shellout("#{sq_bin} --version") {|io| io.read}.to_s
end
+ def python_version
+ @@python_version ||= begin
+ debuginstall = hgdebuginstall_from_command_line
+ if (m = debuginstall.match(/checking Python version \(([\d.]+)\)/))
+ m[1].scan(%r{\d+})
+ .collect(&:to_i)
+ .presence
+ else
+ nil
+ end
+ end
+ end
+
+ def hgdebuginstall_from_command_line
+ shellout("#{sq_bin} debuginstall") {|io| io.read}.to_s
+ end
+
def template_path
@@template_path ||= template_path_for(client_version)
end
diff --git a/lib/redmine/sort_criteria.rb b/lib/redmine/sort_criteria.rb
index 461cd3ac1..01cb95871 100644
--- a/lib/redmine/sort_criteria.rb
+++ b/lib/redmine/sort_criteria.rb
@@ -48,8 +48,8 @@ module Redmine
normalize!
end
- def add(*args)
- self.class.new(self).add!(*args)
+ def add(*)
+ self.class.new(self).add!(*)
end
def first_key
diff --git a/lib/redmine/subclass_factory.rb b/lib/redmine/subclass_factory.rb
index 0905f907a..53db6696b 100644
--- a/lib/redmine/subclass_factory.rb
+++ b/lib/redmine/subclass_factory.rb
@@ -38,10 +38,10 @@ module Redmine
end
# Returns an instance of the given subclass name
- def new_subclass_instance(class_name, *args)
+ def new_subclass_instance(class_name, *)
klass = get_subclass(class_name)
if klass
- klass.new(*args)
+ klass.new(*)
end
end
end
diff --git a/lib/redmine/syntax_highlighting.rb b/lib/redmine/syntax_highlighting.rb
index b5785f837..9ae5fed44 100644
--- a/lib/redmine/syntax_highlighting.rb
+++ b/lib/redmine/syntax_highlighting.rb
@@ -125,6 +125,7 @@ module Redmine
'java_script' => 'javascript',
'xhtml' => 'html'
}
+ private_constant :LANG_ALIASES
def find_lexer(language)
::Rouge::Lexer.find(language) ||
diff --git a/lib/redmine/version.rb b/lib/redmine/version.rb
index 31d80381d..79e1b5984 100644
--- a/lib/redmine/version.rb
+++ b/lib/redmine/version.rb
@@ -24,7 +24,7 @@ module Redmine
module VERSION
MAJOR = 6
MINOR = 0
- TINY = 4
+ TINY = 6
# Branch values:
# * official release: nil
diff --git a/lib/redmine/wiki_formatting/common_mark/alerts_icons_filter.rb b/lib/redmine/wiki_formatting/common_mark/alerts_icons_filter.rb
new file mode 100644
index 000000000..27429d778
--- /dev/null
+++ b/lib/redmine/wiki_formatting/common_mark/alerts_icons_filter.rb
@@ -0,0 +1,63 @@
+# frozen_string_literal: true
+
+# Redmine - project management software
+# Copyright (C) 2006- Jean-Philippe Lang
+#
+# 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.
+
+module Redmine
+ module WikiFormatting
+ module CommonMark
+ # Defines the mapping from alert type (from CSS class) to SVG icon name.
+ # These icon names must correspond to IDs in your SVG sprite sheet (e.g., icons.svg).
+ ALERT_TYPE_TO_ICON_NAME = {
+ 'note' => 'help',
+ 'tip' => 'bulb',
+ 'warning' => 'warning',
+ 'caution' => 'alert-circle',
+ 'important' => 'message-report',
+ }.freeze
+
+ class AlertsIconsFilter < HTML::Pipeline::Filter
+ def call
+ doc.search("p.markdown-alert-title").each do |node|
+ parent_node = node.parent
+ parent_class_attr = parent_node['class'] # e.g., "markdown-alert markdown-alert-note"
+ next unless parent_class_attr
+
+ # Extract the specific alert type (e.g., "note", "tip", "warning")
+ # from the parent div's classes.
+ match_data = parent_class_attr.match(/markdown-alert-(\w+)/)
+ next unless match_data && match_data[1] # Ensure a type is found
+
+ alert_type = match_data[1]
+
+ # Get the corresponding icon name from our map.
+ icon_name = ALERT_TYPE_TO_ICON_NAME[alert_type]
+ next unless icon_name # Skip if no specific icon is defined for this alert type
+
+ icon_html = ApplicationController.helpers.sprite_icon(icon_name, node.text)
+
+ if icon_html
+ # Replace the existing text node with the icon HTML and label (text).
+ node.children.first.replace(icon_html)
+ end
+ end
+ doc
+ end
+ end
+ end
+ end
+end
diff --git a/lib/redmine/wiki_formatting/common_mark/formatter.rb b/lib/redmine/wiki_formatting/common_mark/formatter.rb
index aab8eed8b..8b7a18394 100644
--- a/lib/redmine/wiki_formatting/common_mark/formatter.rb
+++ b/lib/redmine/wiki_formatting/common_mark/formatter.rb
@@ -18,7 +18,6 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
require 'html/pipeline'
-require 'task_list/filter'
module Redmine
module WikiFormatting
@@ -32,6 +31,10 @@ module Redmine
tagfilter: true,
autolink: true,
footnotes: true,
+ header_ids: nil,
+ tasklist: true,
+ shortcodes: false,
+ alerts: true,
}.freeze,
# https://github.com/gjtorikian/commonmarker#parse-options
@@ -41,7 +44,9 @@ module Redmine
# https://github.com/gjtorikian/commonmarker#render-options
commonmarker_render_options: {
unsafe: true,
+ github_pre_lang: false,
hardbreaks: Redmine::Configuration['common_mark_enable_hardbreaks'] == true,
+ tasklist_classes: true,
}.freeze,
commonmarker_plugins: {
syntax_highlighter: nil
@@ -54,7 +59,7 @@ module Redmine
SyntaxHighlightFilter,
FixupAutoLinksFilter,
ExternalLinksFilter,
- TaskList::Filter
+ AlertsIconsFilter
], PIPELINE_CONFIG
class Formatter
diff --git a/lib/redmine/wiki_formatting/common_mark/sanitization_filter.rb b/lib/redmine/wiki_formatting/common_mark/sanitization_filter.rb
index cdefc372b..af72adc32 100644
--- a/lib/redmine/wiki_formatting/common_mark/sanitization_filter.rb
+++ b/lib/redmine/wiki_formatting/common_mark/sanitization_filter.rb
@@ -68,6 +68,26 @@ module Redmine
end
}
+ # Allow class on div and p tags only for alert blocks
+ # (must be exactly: "markdown-alert markdown-alert-*" for div, and "markdown-alert-title" for p)
+ (allowlist[:attributes]["div"] ||= []) << "class"
+ (allowlist[:attributes]["p"] ||= []) << "class"
+ allowlist[:transformers].push lambda{|env|
+ node = env[:node]
+ return unless node.element?
+
+ case node.name
+ when 'div'
+ unless /\Amarkdown-alert markdown-alert-[a-z]+\z/.match?(node['class'])
+ node.remove_attribute('class')
+ end
+ when 'p'
+ unless node['class'] == 'markdown-alert-title'
+ node.remove_attribute('class')
+ end
+ end
+ }
+
# Allow table cell alignment by style attribute
#
# Only necessary if we used the TABLE_PREFER_STYLE_ATTRIBUTES
@@ -78,20 +98,58 @@ module Redmine
# allowlist[:attributes]["td"] = %w(style)
# allowlist[:css] = { properties: ["text-align"] }
- # Allow `id` in a and li elements for footnotes
- # and remove any `id` properties not matching for footnotes
+ # Allow `id` in a elements for footnotes
allowlist[:attributes]["a"].push "id"
- allowlist[:attributes]["li"] = %w(id)
+ # Remove any `id` property not matching for footnotes
allowlist[:transformers].push lambda{|env|
node = env[:node]
- return unless node.name == "a" || node.name == "li"
+ return unless node.name == "a"
return unless node.has_attribute?("id")
- return if node.name == "a" && node["id"] =~ /\Afnref-\d+\z/
- return if node.name == "li" && node["id"] =~ /\Afn-\d+\z/
+ return if node.name == "a" && node["id"] =~ /\Afnref(-\d+){1,2}\z/
node.remove_attribute("id")
}
+ # allow `id` in li element for footnotes
+ # allow `class` in li element for task list items
+ allowlist[:attributes]["li"] = %w(id class)
+ allowlist[:transformers].push lambda{|env|
+ node = env[:node]
+ return unless node.name == "li"
+
+ if node.has_attribute?("id") && !(node["id"] =~ /\Afn-\d+\z/)
+ node.remove_attribute("id")
+ end
+
+ if node.has_attribute?("class") && node["class"] != "task-list-item"
+ node.remove_attribute("class")
+ end
+ }
+
+ # allow input type = "checkbox" with class "task-list-item-checkbox"
+ # for task list items
+ allowlist[:elements].push('input')
+ allowlist[:attributes]["input"] = %w(class type)
+ allowlist[:transformers].push lambda{|env|
+ node = env[:node]
+
+ return unless node.name == "input"
+ return if node['type'] == "checkbox" && node['class'] == "task-list-item-checkbox"
+
+ node.replace(node.children)
+ }
+
+ # allow class "contains-task-list" on ul for task list items
+ allowlist[:attributes]["ul"] = %w(class)
+ allowlist[:transformers].push lambda{|env|
+ node = env[:node]
+
+ return unless node.name == "ul"
+ return if node["class"] == "contains-task-list"
+
+ node.remove_attribute("class")
+ }
+
# https://github.com/rgrove/sanitize/issues/209
allowlist[:protocols].delete("a")
allowlist[:transformers].push lambda{|env|
diff --git a/lib/redmine/wiki_formatting/macros.rb b/lib/redmine/wiki_formatting/macros.rb
index ef1135cb6..16abcb429 100644
--- a/lib/redmine/wiki_formatting/macros.rb
+++ b/lib/redmine/wiki_formatting/macros.rb
@@ -248,7 +248,7 @@ module Redmine
hide_label = args[1] || args[0] || l(:button_hide)
js = "$('##{html_id}-show, ##{html_id}-hide').toggle(); $('##{html_id}').fadeToggle(150);"
out = ''.html_safe
- out << link_to_function(sprite_icon('angle-right', show_label), js, :id => "#{html_id}-show", :class => 'icon icon-collapsed collapsible')
+ out << link_to_function(sprite_icon('angle-right', show_label, rtl: true), js, :id => "#{html_id}-show", :class => 'icon icon-collapsed collapsible')
out <<
link_to_function(
sprite_icon('angle-down', hide_label), js,