Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'lib/banzai')
-rw-r--r--lib/banzai/filter/abstract_reference_filter.rb446
-rw-r--r--lib/banzai/filter/alert_reference_filter.rb29
-rw-r--r--lib/banzai/filter/autolink_filter.rb2
-rw-r--r--lib/banzai/filter/commit_range_reference_filter.rb46
-rw-r--r--lib/banzai/filter/commit_reference_filter.rb86
-rw-r--r--lib/banzai/filter/commit_trailers_filter.rb2
-rw-r--r--lib/banzai/filter/design_reference_filter.rb107
-rw-r--r--lib/banzai/filter/epic_reference_filter.rb22
-rw-r--r--lib/banzai/filter/external_issue_reference_filter.rb118
-rw-r--r--lib/banzai/filter/feature_flag_reference_filter.rb33
-rw-r--r--lib/banzai/filter/gollum_tags_filter.rb13
-rw-r--r--lib/banzai/filter/issuable_reference_filter.rb19
-rw-r--r--lib/banzai/filter/issue_reference_filter.rb57
-rw-r--r--lib/banzai/filter/iteration_reference_filter.rb16
-rw-r--r--lib/banzai/filter/label_reference_filter.rb129
-rw-r--r--lib/banzai/filter/math_filter.rb2
-rw-r--r--lib/banzai/filter/merge_request_reference_filter.rb97
-rw-r--r--lib/banzai/filter/milestone_reference_filter.rb138
-rw-r--r--lib/banzai/filter/project_reference_filter.rb117
-rw-r--r--lib/banzai/filter/reference_filter.rb215
-rw-r--r--lib/banzai/filter/references/abstract_reference_filter.rb448
-rw-r--r--lib/banzai/filter/references/alert_reference_filter.rb31
-rw-r--r--lib/banzai/filter/references/commit_range_reference_filter.rb48
-rw-r--r--lib/banzai/filter/references/commit_reference_filter.rb88
-rw-r--r--lib/banzai/filter/references/design_reference_filter.rb109
-rw-r--r--lib/banzai/filter/references/epic_reference_filter.rb24
-rw-r--r--lib/banzai/filter/references/external_issue_reference_filter.rb120
-rw-r--r--lib/banzai/filter/references/feature_flag_reference_filter.rb35
-rw-r--r--lib/banzai/filter/references/issuable_reference_filter.rb21
-rw-r--r--lib/banzai/filter/references/issue_reference_filter.rb59
-rw-r--r--lib/banzai/filter/references/iteration_reference_filter.rb18
-rw-r--r--lib/banzai/filter/references/label_reference_filter.rb132
-rw-r--r--lib/banzai/filter/references/merge_request_reference_filter.rb99
-rw-r--r--lib/banzai/filter/references/milestone_reference_filter.rb140
-rw-r--r--lib/banzai/filter/references/project_reference_filter.rb119
-rw-r--r--lib/banzai/filter/references/reference_filter.rb217
-rw-r--r--lib/banzai/filter/references/snippet_reference_filter.rb31
-rw-r--r--lib/banzai/filter/references/user_reference_filter.rb182
-rw-r--r--lib/banzai/filter/references/vulnerability_reference_filter.rb24
-rw-r--r--lib/banzai/filter/repository_link_filter.rb2
-rw-r--r--lib/banzai/filter/snippet_reference_filter.rb29
-rw-r--r--lib/banzai/filter/spaced_link_filter.rb2
-rw-r--r--lib/banzai/filter/suggestion_filter.rb2
-rw-r--r--lib/banzai/filter/syntax_highlight_filter.rb2
-rw-r--r--lib/banzai/filter/user_reference_filter.rb180
-rw-r--r--lib/banzai/filter/vulnerability_reference_filter.rb22
-rw-r--r--lib/banzai/filter/wiki_link_filter/rewriter.rb5
-rw-r--r--lib/banzai/pipeline/gfm_pipeline.rb26
-rw-r--r--lib/banzai/pipeline/label_pipeline.rb2
-rw-r--r--lib/banzai/pipeline/single_line_pipeline.rb18
-rw-r--r--lib/banzai/pipeline/wiki_pipeline.rb2
51 files changed, 1986 insertions, 1945 deletions
diff --git a/lib/banzai/filter/abstract_reference_filter.rb b/lib/banzai/filter/abstract_reference_filter.rb
deleted file mode 100644
index 2448c2c2bb2..00000000000
--- a/lib/banzai/filter/abstract_reference_filter.rb
+++ /dev/null
@@ -1,446 +0,0 @@
-# frozen_string_literal: true
-
-module Banzai
- module Filter
- # Issues, Merge Requests, Snippets, Commits and Commit Ranges share
- # similar functionality in reference filtering.
- class AbstractReferenceFilter < ReferenceFilter
- include CrossProjectReference
-
- # REFERENCE_PLACEHOLDER is used for re-escaping HTML text except found
- # reference (which we replace with placeholder during re-scaping). The
- # random number helps ensure it's pretty close to unique. Since it's a
- # transitory value (it never gets saved) we can initialize once, and it
- # doesn't matter if it changes on a restart.
- REFERENCE_PLACEHOLDER = "_reference_#{SecureRandom.hex(16)}_"
- REFERENCE_PLACEHOLDER_PATTERN = %r{#{REFERENCE_PLACEHOLDER}(\d+)}.freeze
-
- def self.object_class
- # Implement in child class
- # Example: MergeRequest
- end
-
- def self.object_name
- @object_name ||= object_class.name.underscore
- end
-
- def self.object_sym
- @object_sym ||= object_name.to_sym
- end
-
- # Public: Find references in text (like `!123` for merge requests)
- #
- # AnyReferenceFilter.references_in(text) do |match, id, project_ref, matches|
- # object = find_object(project_ref, id)
- # "<a href=...>#{object.to_reference}</a>"
- # end
- #
- # text - String text to search.
- #
- # Yields the String match, the Integer referenced object ID, an optional String
- # of the external project reference, and all of the matchdata.
- #
- # Returns a String replaced with the return of the block.
- def self.references_in(text, pattern = object_class.reference_pattern)
- text.gsub(pattern) do |match|
- if ident = identifier($~)
- yield match, ident, $~[:project], $~[:namespace], $~
- else
- match
- end
- end
- end
-
- def self.identifier(match_data)
- symbol = symbol_from_match(match_data)
-
- parse_symbol(symbol, match_data) if object_class.reference_valid?(symbol)
- end
-
- def identifier(match_data)
- self.class.identifier(match_data)
- end
-
- def self.symbol_from_match(match)
- key = object_sym
- match[key] if match.names.include?(key.to_s)
- end
-
- # Transform a symbol extracted from the text to a meaningful value
- # In most cases these will be integers, so we call #to_i by default
- #
- # This method has the contract that if a string `ref` refers to a
- # record `record`, then `parse_symbol(ref) == record_identifier(record)`.
- def self.parse_symbol(symbol, match_data)
- symbol.to_i
- end
-
- # We assume that most classes are identifying records by ID.
- #
- # This method has the contract that if a string `ref` refers to a
- # record `record`, then `class.parse_symbol(ref) == record_identifier(record)`.
- def record_identifier(record)
- record.id
- end
-
- def object_class
- self.class.object_class
- end
-
- def object_sym
- self.class.object_sym
- end
-
- def references_in(*args, &block)
- self.class.references_in(*args, &block)
- end
-
- # Implement in child class
- # Example: project.merge_requests.find
- def find_object(parent_object, id)
- end
-
- # Override if the link reference pattern produces a different ID (global
- # ID vs internal ID, for instance) to the regular reference pattern.
- def find_object_from_link(parent_object, id)
- find_object(parent_object, id)
- end
-
- # Implement in child class
- # Example: project_merge_request_url
- def url_for_object(object, parent_object)
- end
-
- def find_object_cached(parent_object, id)
- cached_call(:banzai_find_object, id, path: [object_class, parent_object.id]) do
- find_object(parent_object, id)
- end
- end
-
- def find_object_from_link_cached(parent_object, id)
- cached_call(:banzai_find_object_from_link, id, path: [object_class, parent_object.id]) do
- find_object_from_link(parent_object, id)
- end
- end
-
- def from_ref_cached(ref)
- cached_call("banzai_#{parent_type}_refs".to_sym, ref) do
- parent_from_ref(ref)
- end
- end
-
- def url_for_object_cached(object, parent_object)
- cached_call(:banzai_url_for_object, object, path: [object_class, parent_object.id]) do
- url_for_object(object, parent_object)
- end
- end
-
- def call
- return doc unless project || group || user
-
- ref_pattern = object_class.reference_pattern
- link_pattern = object_class.link_reference_pattern
-
- # Compile often used regexps only once outside of the loop
- ref_pattern_anchor = /\A#{ref_pattern}\z/
- link_pattern_start = /\A#{link_pattern}/
- link_pattern_anchor = /\A#{link_pattern}\z/
-
- nodes.each_with_index do |node, index|
- if text_node?(node) && ref_pattern
- replace_text_when_pattern_matches(node, index, ref_pattern) do |content|
- object_link_filter(content, ref_pattern)
- end
-
- elsif element_node?(node)
- yield_valid_link(node) do |link, inner_html|
- if ref_pattern && link =~ ref_pattern_anchor
- replace_link_node_with_href(node, index, link) do
- object_link_filter(link, ref_pattern, link_content: inner_html)
- end
-
- next
- end
-
- next unless link_pattern
-
- if link == inner_html && inner_html =~ link_pattern_start
- replace_link_node_with_text(node, index) do
- object_link_filter(inner_html, link_pattern, link_reference: true)
- end
-
- next
- end
-
- if link =~ link_pattern_anchor
- replace_link_node_with_href(node, index, link) do
- object_link_filter(link, link_pattern, link_content: inner_html, link_reference: true)
- end
-
- next
- end
- end
- end
- end
-
- doc
- end
-
- # Replace references (like `!123` for merge requests) in text with links
- # to the referenced object's details page.
- #
- # text - String text to replace references in.
- # pattern - Reference pattern to match against.
- # link_content - Original content of the link being replaced.
- # link_reference - True if this was using the link reference pattern,
- # false otherwise.
- #
- # Returns a String with references replaced with links. All links
- # have `gfm` and `gfm-OBJECT_NAME` class names attached for styling.
- def object_link_filter(text, pattern, link_content: nil, link_reference: false)
- references_in(text, pattern) do |match, id, project_ref, namespace_ref, matches|
- parent_path = if parent_type == :group
- full_group_path(namespace_ref)
- else
- full_project_path(namespace_ref, project_ref)
- end
-
- parent = from_ref_cached(parent_path)
-
- if parent
- object =
- if link_reference
- find_object_from_link_cached(parent, id)
- else
- find_object_cached(parent, id)
- end
- end
-
- if object
- title = object_link_title(object, matches)
- klass = reference_class(object_sym)
-
- data_attributes = data_attributes_for(link_content || match, parent, object,
- link_content: !!link_content,
- link_reference: link_reference)
- data = data_attribute(data_attributes)
-
- url =
- if matches.names.include?("url") && matches[:url]
- matches[:url]
- else
- url_for_object_cached(object, parent)
- end
-
- content = link_content || object_link_text(object, matches)
-
- link = %(<a href="#{url}" #{data}
- title="#{escape_once(title)}"
- class="#{klass}">#{content}</a>)
-
- wrap_link(link, object)
- else
- match
- end
- end
- end
-
- def wrap_link(link, object)
- link
- end
-
- def data_attributes_for(text, parent, object, link_content: false, link_reference: false)
- object_parent_type = parent.is_a?(Group) ? :group : :project
-
- {
- original: escape_html_entities(text),
- link: link_content,
- link_reference: link_reference,
- object_parent_type => parent.id,
- object_sym => object.id
- }
- end
-
- def object_link_text_extras(object, matches)
- extras = []
-
- if matches.names.include?("anchor") && matches[:anchor] && matches[:anchor] =~ /\A\#note_(\d+)\z/
- extras << "comment #{Regexp.last_match(1)}"
- end
-
- extension = matches[:extension] if matches.names.include?("extension")
-
- extras << extension if extension
-
- extras
- end
-
- def object_link_title(object, matches)
- object.title
- end
-
- def object_link_text(object, matches)
- parent = project || group || user
- text = object.reference_link_text(parent)
-
- extras = object_link_text_extras(object, matches)
- text += " (#{extras.join(", ")})" if extras.any?
-
- text
- end
-
- # Returns a Hash containing all object references (e.g. issue IDs) per the
- # project they belong to.
- def references_per_parent
- @references_per ||= {}
-
- @references_per[parent_type] ||= begin
- refs = Hash.new { |hash, key| hash[key] = Set.new }
- regex = [
- object_class.link_reference_pattern,
- object_class.reference_pattern
- ].compact.reduce { |a, b| Regexp.union(a, b) }
-
- nodes.each do |node|
- node.to_html.scan(regex) do
- path = if parent_type == :project
- full_project_path($~[:namespace], $~[:project])
- else
- full_group_path($~[:group])
- end
-
- if ident = identifier($~)
- refs[path] << ident
- end
- end
- end
-
- refs
- end
- end
-
- # Returns a Hash containing referenced projects grouped per their full
- # path.
- def parent_per_reference
- @per_reference ||= {}
-
- @per_reference[parent_type] ||= begin
- refs = Set.new
-
- references_per_parent.each do |ref, _|
- refs << ref
- end
-
- find_for_paths(refs.to_a).index_by(&:full_path)
- end
- end
-
- def relation_for_paths(paths)
- klass = parent_type.to_s.camelize.constantize
- result = klass.where_full_path_in(paths)
- return result if parent_type == :group
-
- result.includes(:namespace) if parent_type == :project
- end
-
- # Returns projects for the given paths.
- def find_for_paths(paths)
- if Gitlab::SafeRequestStore.active?
- cache = refs_cache
- to_query = paths - cache.keys
-
- unless to_query.empty?
- records = relation_for_paths(to_query)
-
- found = []
- records.each do |record|
- ref = record.full_path
- get_or_set_cache(cache, ref) { record }
- found << ref
- end
-
- not_found = to_query - found
- not_found.each do |ref|
- get_or_set_cache(cache, ref) { nil }
- end
- end
-
- cache.slice(*paths).values.compact
- else
- relation_for_paths(paths)
- end
- end
-
- def current_parent_path
- @current_parent_path ||= parent&.full_path
- end
-
- def current_project_namespace_path
- @current_project_namespace_path ||= project&.namespace&.full_path
- end
-
- def records_per_parent
- @_records_per_project ||= {}
-
- @_records_per_project[object_class.to_s.underscore] ||= begin
- hash = Hash.new { |h, k| h[k] = {} }
-
- parent_per_reference.each do |path, parent|
- record_ids = references_per_parent[path]
-
- parent_records(parent, record_ids).each do |record|
- hash[parent][record_identifier(record)] = record
- end
- end
-
- hash
- end
- end
-
- private
-
- def full_project_path(namespace, project_ref)
- return current_parent_path unless project_ref
-
- namespace_ref = namespace || current_project_namespace_path
- "#{namespace_ref}/#{project_ref}"
- end
-
- def refs_cache
- Gitlab::SafeRequestStore["banzai_#{parent_type}_refs".to_sym] ||= {}
- end
-
- def parent_type
- :project
- end
-
- def parent
- parent_type == :project ? project : group
- end
-
- def full_group_path(group_ref)
- return current_parent_path unless group_ref
-
- group_ref
- end
-
- def unescape_html_entities(text)
- CGI.unescapeHTML(text.to_s)
- end
-
- def escape_html_entities(text)
- CGI.escapeHTML(text.to_s)
- end
-
- def escape_with_placeholders(text, placeholder_data)
- escaped = escape_html_entities(text)
-
- escaped.gsub(REFERENCE_PLACEHOLDER_PATTERN) do |match|
- placeholder_data[Regexp.last_match(1).to_i]
- end
- end
- end
- end
-end
-
-Banzai::Filter::AbstractReferenceFilter.prepend_if_ee('EE::Banzai::Filter::AbstractReferenceFilter')
diff --git a/lib/banzai/filter/alert_reference_filter.rb b/lib/banzai/filter/alert_reference_filter.rb
deleted file mode 100644
index 228a4159c99..00000000000
--- a/lib/banzai/filter/alert_reference_filter.rb
+++ /dev/null
@@ -1,29 +0,0 @@
-# frozen_string_literal: true
-
-module Banzai
- module Filter
- class AlertReferenceFilter < IssuableReferenceFilter
- self.reference_type = :alert
-
- def self.object_class
- AlertManagement::Alert
- end
-
- def self.object_sym
- :alert
- end
-
- def parent_records(parent, ids)
- parent.alert_management_alerts.where(iid: ids.to_a)
- end
-
- def url_for_object(alert, project)
- ::Gitlab::Routing.url_helpers.details_project_alert_management_url(
- project,
- alert.iid,
- only_path: context[:only_path]
- )
- end
- end
- end
-end
diff --git a/lib/banzai/filter/autolink_filter.rb b/lib/banzai/filter/autolink_filter.rb
index d569711431c..a86c1bb2892 100644
--- a/lib/banzai/filter/autolink_filter.rb
+++ b/lib/banzai/filter/autolink_filter.rb
@@ -43,7 +43,7 @@ module Banzai
TEXT_QUERY = %Q(descendant-or-self::text()[
not(#{IGNORE_PARENTS.map { |p| "ancestor::#{p}" }.join(' or ')})
and contains(., '://')
- ]).freeze
+ ])
PUNCTUATION_PAIRS = {
"'" => "'",
diff --git a/lib/banzai/filter/commit_range_reference_filter.rb b/lib/banzai/filter/commit_range_reference_filter.rb
deleted file mode 100644
index d6b46236a49..00000000000
--- a/lib/banzai/filter/commit_range_reference_filter.rb
+++ /dev/null
@@ -1,46 +0,0 @@
-# frozen_string_literal: true
-
-module Banzai
- module Filter
- # HTML filter that replaces commit range references with links.
- #
- # This filter supports cross-project references.
- class CommitRangeReferenceFilter < AbstractReferenceFilter
- self.reference_type = :commit_range
-
- def self.object_class
- CommitRange
- end
-
- def self.references_in(text, pattern = CommitRange.reference_pattern)
- text.gsub(pattern) do |match|
- yield match, $~[:commit_range], $~[:project], $~[:namespace], $~
- end
- end
-
- def initialize(*args)
- super
-
- @commit_map = {}
- end
-
- def find_object(project, id)
- return unless project.is_a?(Project)
-
- range = CommitRange.new(id, project)
-
- range.valid_commits? ? range : nil
- end
-
- def url_for_object(range, project)
- h = Gitlab::Routing.url_helpers
- h.project_compare_url(project,
- range.to_param.merge(only_path: context[:only_path]))
- end
-
- def object_link_title(range, matches)
- nil
- end
- end
- end
-end
diff --git a/lib/banzai/filter/commit_reference_filter.rb b/lib/banzai/filter/commit_reference_filter.rb
deleted file mode 100644
index 3df003a88fa..00000000000
--- a/lib/banzai/filter/commit_reference_filter.rb
+++ /dev/null
@@ -1,86 +0,0 @@
-# frozen_string_literal: true
-
-module Banzai
- module Filter
- # HTML filter that replaces commit references with links.
- #
- # This filter supports cross-project references.
- class CommitReferenceFilter < AbstractReferenceFilter
- self.reference_type = :commit
-
- def self.object_class
- Commit
- end
-
- def self.references_in(text, pattern = Commit.reference_pattern)
- text.gsub(pattern) do |match|
- yield match, $~[:commit], $~[:project], $~[:namespace], $~
- end
- end
-
- def find_object(project, id)
- return unless project.is_a?(Project) && project.valid_repo?
-
- _, record = records_per_parent[project].detect { |k, _v| Gitlab::Git.shas_eql?(k, id) }
-
- record
- end
-
- def referenced_merge_request_commit_shas
- return [] unless noteable.is_a?(MergeRequest)
-
- @referenced_merge_request_commit_shas ||= begin
- referenced_shas = references_per_parent.values.reduce(:|).to_a
- noteable.all_commit_shas.select do |sha|
- referenced_shas.any? { |ref| Gitlab::Git.shas_eql?(sha, ref) }
- end
- end
- end
-
- # The default behaviour is `#to_i` - we just pass the hash through.
- def self.parse_symbol(sha_hash, _match)
- sha_hash
- end
-
- def url_for_object(commit, project)
- h = Gitlab::Routing.url_helpers
-
- if referenced_merge_request_commit_shas.include?(commit.id)
- h.diffs_project_merge_request_url(project,
- noteable,
- commit_id: commit.id,
- only_path: only_path?)
- else
- h.project_commit_url(project,
- commit,
- only_path: only_path?)
- end
- end
-
- def object_link_text_extras(object, matches)
- extras = super
-
- path = matches[:path] if matches.names.include?("path")
- if path == '/builds'
- extras.unshift "builds"
- end
-
- extras
- end
-
- private
-
- def parent_records(parent, ids)
- parent.commits_by(oids: ids.to_a)
- end
-
- def noteable
- context[:noteable]
- end
-
- def only_path?
- context[:only_path]
- end
- end
- end
-end
diff --git a/lib/banzai/filter/commit_trailers_filter.rb b/lib/banzai/filter/commit_trailers_filter.rb
index 5288db3b0cb..a615abc1989 100644
--- a/lib/banzai/filter/commit_trailers_filter.rb
+++ b/lib/banzai/filter/commit_trailers_filter.rb
@@ -36,7 +36,7 @@ module Banzai
next if html == content
- node.replace(html)
+ node.replace("\n\n#{html}")
end
doc
diff --git a/lib/banzai/filter/design_reference_filter.rb b/lib/banzai/filter/design_reference_filter.rb
deleted file mode 100644
index 1754fec93d4..00000000000
--- a/lib/banzai/filter/design_reference_filter.rb
+++ /dev/null
@@ -1,107 +0,0 @@
-# frozen_string_literal: true
-
-module Banzai
- module Filter
- class DesignReferenceFilter < AbstractReferenceFilter
- class Identifier
- include Comparable
- attr_reader :issue_iid, :filename
-
- def initialize(issue_iid:, filename:)
- @issue_iid = issue_iid
- @filename = filename
- end
-
- def as_composite_id(id_for_iid)
- id = id_for_iid[issue_iid]
- return unless id
-
- { issue_id: id, filename: filename }
- end
-
- def <=>(other)
- return unless other.is_a?(Identifier)
-
- [issue_iid, filename] <=> [other.issue_iid, other.filename]
- end
- alias_method :eql?, :==
-
- def hash
- [issue_iid, filename].hash
- end
- end
-
- self.reference_type = :design
-
- def find_object(project, identifier)
- records_per_parent[project][identifier]
- end
-
- def parent_records(project, identifiers)
- return [] unless project.design_management_enabled?
-
- iids = identifiers.map(&:issue_iid).to_set
- issues = project.issues.where(iid: iids)
- id_for_iid = issues.index_by(&:iid).transform_values(&:id)
- issue_by_id = issues.index_by(&:id)
-
- designs(identifiers, id_for_iid).each do |d|
- issue = issue_by_id[d.issue_id]
- # optimisation: assign values we have already fetched
- d.project = project
- d.issue = issue
- end
- end
-
- def relation_for_paths(paths)
- super.includes(:route, :namespace, :group)
- end
-
- def parent_type
- :project
- end
-
- # optimisation to reuse the parent_per_reference query information
- def parent_from_ref(ref)
- parent_per_reference[ref || current_parent_path]
- end
-
- def url_for_object(design, project)
- path_options = { vueroute: design.filename }
- Gitlab::Routing.url_helpers.designs_project_issue_path(project, design.issue, path_options)
- end
-
- def data_attributes_for(_text, _project, design, **_kwargs)
- super.merge(issue: design.issue_id)
- end
-
- def self.object_class
- ::DesignManagement::Design
- end
-
- def self.object_sym
- :design
- end
-
- def self.parse_symbol(raw, match_data)
- filename = match_data[:url_filename]
- iid = match_data[:issue].to_i
- Identifier.new(filename: CGI.unescape(filename), issue_iid: iid)
- end
-
- def record_identifier(design)
- Identifier.new(filename: design.filename, issue_iid: design.issue.iid)
- end
-
- private
-
- def designs(identifiers, id_for_iid)
- identifiers
- .map { |identifier| identifier.as_composite_id(id_for_iid) }
- .compact
- .in_groups_of(100, false) # limitation of by_issue_id_and_filename, so we batch
- .flat_map { |ids| DesignManagement::Design.by_issue_id_and_filename(ids) }
- end
- end
- end
-end
diff --git a/lib/banzai/filter/epic_reference_filter.rb b/lib/banzai/filter/epic_reference_filter.rb
deleted file mode 100644
index 70a6cb0a6dc..00000000000
--- a/lib/banzai/filter/epic_reference_filter.rb
+++ /dev/null
@@ -1,22 +0,0 @@
-# frozen_string_literal: true
-
-module Banzai
- module Filter
- # The actual filter is implemented in the EE mixin
- class EpicReferenceFilter < IssuableReferenceFilter
- self.reference_type = :epic
-
- def self.object_class
- Epic
- end
-
- private
-
- def group
- context[:group] || context[:project]&.group
- end
- end
- end
-end
-
-Banzai::Filter::EpicReferenceFilter.prepend_if_ee('EE::Banzai::Filter::EpicReferenceFilter')
diff --git a/lib/banzai/filter/external_issue_reference_filter.rb b/lib/banzai/filter/external_issue_reference_filter.rb
deleted file mode 100644
index fcf4863ab4f..00000000000
--- a/lib/banzai/filter/external_issue_reference_filter.rb
+++ /dev/null
@@ -1,118 +0,0 @@
-# frozen_string_literal: true
-
-module Banzai
- module Filter
- # HTML filter that replaces external issue tracker references with links.
- # References are ignored if the project doesn't use an external issue
- # tracker.
- #
- # This filter does not support cross-project references.
- class ExternalIssueReferenceFilter < ReferenceFilter
- self.reference_type = :external_issue
-
- # Public: Find `JIRA-123` issue references in text
- #
- # ExternalIssueReferenceFilter.references_in(text, pattern) do |match, issue|
- # "<a href=...>##{issue}</a>"
- # end
- #
- # text - String text to search.
- #
- # Yields the String match and the String issue reference.
- #
- # Returns a String replaced with the return of the block.
- def self.references_in(text, pattern)
- text.gsub(pattern) do |match|
- yield match, $~[:issue]
- end
- end
-
- def call
- # Early return if the project isn't using an external tracker
- return doc if project.nil? || default_issues_tracker?
-
- ref_pattern = issue_reference_pattern
- ref_start_pattern = /\A#{ref_pattern}\z/
-
- nodes.each_with_index do |node, index|
- if text_node?(node)
- replace_text_when_pattern_matches(node, index, ref_pattern) do |content|
- issue_link_filter(content)
- end
-
- elsif element_node?(node)
- yield_valid_link(node) do |link, inner_html|
- if link =~ ref_start_pattern
- replace_link_node_with_href(node, index, link) do
- issue_link_filter(link, link_content: inner_html)
- end
- end
- end
- end
- end
-
- doc
- end
-
- private
-
- # Replace `JIRA-123` issue references in text with links to the referenced
- # issue's details page.
- #
- # text - String text to replace references in.
- # link_content - Original content of the link being replaced.
- #
- # Returns a String with `JIRA-123` references replaced with links. All
- # links have `gfm` and `gfm-issue` class names attached for styling.
- def issue_link_filter(text, link_content: nil)
- self.class.references_in(text, issue_reference_pattern) do |match, id|
- url = url_for_issue(id)
- klass = reference_class(:issue)
- data = data_attribute(project: project.id, external_issue: id)
- content = link_content || match
-
- %(<a href="#{url}" #{data}
- title="#{escape_once(issue_title)}"
- class="#{klass}">#{content}</a>)
- end
- end
-
- def url_for_issue(issue_id)
- return '' if project.nil?
-
- url = if only_path?
- project.external_issue_tracker.issue_path(issue_id)
- else
- project.external_issue_tracker.issue_url(issue_id)
- end
-
- # Ensure we return a valid URL to prevent possible XSS.
- URI.parse(url).to_s
- rescue URI::InvalidURIError
- ''
- end
-
- def default_issues_tracker?
- external_issues_cached(:default_issues_tracker?)
- end
-
- def issue_reference_pattern
- external_issues_cached(:external_issue_reference_pattern)
- end
-
- def project
- context[:project]
- end
-
- def issue_title
- "Issue in #{project.external_issue_tracker.title}"
- end
-
- def external_issues_cached(attribute)
- cached_attributes = Gitlab::SafeRequestStore[:banzai_external_issues_tracker_attributes] ||= Hash.new { |h, k| h[k] = {} }
- cached_attributes[project.id][attribute] = project.public_send(attribute) if cached_attributes[project.id][attribute].nil? # rubocop:disable GitlabSecurity/PublicSend
- cached_attributes[project.id][attribute]
- end
- end
- end
-end
diff --git a/lib/banzai/filter/feature_flag_reference_filter.rb b/lib/banzai/filter/feature_flag_reference_filter.rb
deleted file mode 100644
index c11576901ce..00000000000
--- a/lib/banzai/filter/feature_flag_reference_filter.rb
+++ /dev/null
@@ -1,33 +0,0 @@
-# frozen_string_literal: true
-
-module Banzai
- module Filter
- class FeatureFlagReferenceFilter < IssuableReferenceFilter
- self.reference_type = :feature_flag
-
- def self.object_class
- Operations::FeatureFlag
- end
-
- def self.object_sym
- :feature_flag
- end
-
- def parent_records(parent, ids)
- parent.operations_feature_flags.where(iid: ids.to_a)
- end
-
- def url_for_object(feature_flag, project)
- ::Gitlab::Routing.url_helpers.edit_project_feature_flag_url(
- project,
- feature_flag.iid,
- only_path: context[:only_path]
- )
- end
-
- def object_link_title(object, matches)
- object.name
- end
- end
- end
-end
diff --git a/lib/banzai/filter/gollum_tags_filter.rb b/lib/banzai/filter/gollum_tags_filter.rb
index 8a7d3c49ffb..6de9f2b86f6 100644
--- a/lib/banzai/filter/gollum_tags_filter.rb
+++ b/lib/banzai/filter/gollum_tags_filter.rb
@@ -98,14 +98,15 @@ module Banzai
return unless image?(content)
- if url?(content)
- path = content
- elsif file = wiki.find_file(content, load_content: false)
- path = ::File.join(wiki_base_path, file.path)
- end
+ path =
+ if url?(content)
+ content
+ elsif file = wiki.find_file(content, load_content: false)
+ file.path
+ end
if path
- content_tag(:img, nil, data: { src: path }, class: 'gfm')
+ content_tag(:img, nil, src: path, class: 'gfm')
end
end
diff --git a/lib/banzai/filter/issuable_reference_filter.rb b/lib/banzai/filter/issuable_reference_filter.rb
deleted file mode 100644
index b91ba9f7256..00000000000
--- a/lib/banzai/filter/issuable_reference_filter.rb
+++ /dev/null
@@ -1,19 +0,0 @@
-# frozen_string_literal: true
-
-module Banzai
- module Filter
- class IssuableReferenceFilter < AbstractReferenceFilter
- def record_identifier(record)
- record.iid.to_i
- end
-
- def find_object(parent, iid)
- records_per_parent[parent][iid]
- end
-
- def parent_from_ref(ref)
- parent_per_reference[ref || current_parent_path]
- end
- end
- end
-end
diff --git a/lib/banzai/filter/issue_reference_filter.rb b/lib/banzai/filter/issue_reference_filter.rb
deleted file mode 100644
index 216418ee5fa..00000000000
--- a/lib/banzai/filter/issue_reference_filter.rb
+++ /dev/null
@@ -1,57 +0,0 @@
-# frozen_string_literal: true
-
-module Banzai
- module Filter
- # HTML filter that replaces issue references with links. References to
- # issues that do not exist are ignored.
- #
- # This filter supports cross-project references.
- #
- # When external issues tracker like Jira is activated we should not
- # use issue reference pattern, but we should still be able
- # to reference issues from other GitLab projects.
- class IssueReferenceFilter < IssuableReferenceFilter
- self.reference_type = :issue
-
- def self.object_class
- Issue
- end
-
- def url_for_object(issue, project)
- return issue_path(issue, project) if only_path?
-
- issue_url(issue, project)
- end
-
- def parent_records(parent, ids)
- parent.issues.where(iid: ids.to_a)
- end
-
- def object_link_text_extras(issue, matches)
- super + design_link_extras(issue, matches.named_captures['path'])
- end
-
- private
-
- def issue_path(issue, project)
- Gitlab::Routing.url_helpers.namespace_project_issue_path(namespace_id: project.namespace, project_id: project, id: issue.iid)
- end
-
- def issue_url(issue, project)
- Gitlab::Routing.url_helpers.namespace_project_issue_url(namespace_id: project.namespace, project_id: project, id: issue.iid)
- end
-
- def design_link_extras(issue, path)
- if path == '/designs' && read_designs?(issue)
- ['designs']
- else
- []
- end
- end
-
- def read_designs?(issue)
- issue.project.design_management_enabled?
- end
- end
- end
-end
diff --git a/lib/banzai/filter/iteration_reference_filter.rb b/lib/banzai/filter/iteration_reference_filter.rb
deleted file mode 100644
index 9d2b533e6da..00000000000
--- a/lib/banzai/filter/iteration_reference_filter.rb
+++ /dev/null
@@ -1,16 +0,0 @@
-# frozen_string_literal: true
-
-module Banzai
- module Filter
- # The actual filter is implemented in the EE mixin
- class IterationReferenceFilter < AbstractReferenceFilter
- self.reference_type = :iteration
-
- def self.object_class
- Iteration
- end
- end
- end
-end
-
-Banzai::Filter::IterationReferenceFilter.prepend_if_ee('EE::Banzai::Filter::IterationReferenceFilter')
diff --git a/lib/banzai/filter/label_reference_filter.rb b/lib/banzai/filter/label_reference_filter.rb
deleted file mode 100644
index a4d3e352051..00000000000
--- a/lib/banzai/filter/label_reference_filter.rb
+++ /dev/null
@@ -1,129 +0,0 @@
-# frozen_string_literal: true
-
-module Banzai
- module Filter
- # HTML filter that replaces label references with links.
- class LabelReferenceFilter < AbstractReferenceFilter
- self.reference_type = :label
-
- def self.object_class
- Label
- end
-
- def find_object(parent_object, id)
- find_labels(parent_object).find(id)
- end
-
- def references_in(text, pattern = Label.reference_pattern)
- labels = {}
- unescaped_html = unescape_html_entities(text).gsub(pattern) do |match|
- namespace, project = $~[:namespace], $~[:project]
- project_path = full_project_path(namespace, project)
- label = find_label_cached(project_path, $~[:label_id], $~[:label_name])
-
- if label
- labels[label.id] = yield match, label.id, project, namespace, $~
- "#{REFERENCE_PLACEHOLDER}#{label.id}"
- else
- match
- end
- end
-
- return text if labels.empty?
-
- escape_with_placeholders(unescaped_html, labels)
- end
-
- def find_label_cached(parent_ref, label_id, label_name)
- cached_call(:banzai_find_label_cached, label_name&.tr('"', '') || label_id, path: [object_class, parent_ref]) do
- find_label(parent_ref, label_id, label_name)
- end
- end
-
- def find_label(parent_ref, label_id, label_name)
- parent = parent_from_ref(parent_ref)
- return unless parent
-
- label_params = label_params(label_id, label_name)
- find_labels(parent).find_by(label_params)
- end
-
- def find_labels(parent)
- params = if parent.is_a?(Group)
- { group_id: parent.id,
- include_ancestor_groups: true,
- only_group_labels: true }
- else
- { project: parent,
- include_ancestor_groups: true }
- end
-
- LabelsFinder.new(nil, params).execute(skip_authorization: true)
- end
-
- # Parameters to pass to `Label.find_by` based on the given arguments
- #
- # id - Integer ID to pass. If present, returns {id: id}
- # name - String name to pass. If `id` is absent, finds by name without
- # surrounding quotes.
- #
- # Returns a Hash.
- def label_params(id, name)
- if name
- { name: name.tr('"', '') }
- else
- { id: id.to_i }
- end
- end
-
- def url_for_object(label, parent)
- label_url_method =
- if context[:label_url_method]
- context[:label_url_method]
- elsif parent.is_a?(Project)
- :project_issues_url
- end
-
- return unless label_url_method
-
- Gitlab::Routing.url_helpers.public_send(label_url_method, parent, label_name: label.name, only_path: context[:only_path]) # rubocop:disable GitlabSecurity/PublicSend
- end
-
- def object_link_text(object, matches)
- label_suffix = ''
- parent = project || group
-
- if project || full_path_ref?(matches)
- project_path = full_project_path(matches[:namespace], matches[:project])
- parent_from_ref = from_ref_cached(project_path)
- reference = parent_from_ref.to_human_reference(parent)
-
- label_suffix = " <i>in #{ERB::Util.html_escape(reference)}</i>" if reference.present?
- end
-
- presenter = object.present(issuable_subject: parent)
- LabelsHelper.render_colored_label(presenter, suffix: label_suffix)
- end
-
- def wrap_link(link, label)
- presenter = label.present(issuable_subject: project || group)
- LabelsHelper.wrap_label_html(link, small: true, label: presenter)
- end
-
- def full_path_ref?(matches)
- matches[:namespace] && matches[:project]
- end
-
- def reference_class(type, tooltip: true)
- super + ' gl-link gl-label-link'
- end
-
- def object_link_title(object, matches)
- presenter = object.present(issuable_subject: project || group)
- LabelsHelper.label_tooltip_title(presenter)
- end
- end
- end
-end
-
-Banzai::Filter::LabelReferenceFilter.prepend_if_ee('EE::Banzai::Filter::LabelReferenceFilter')
diff --git a/lib/banzai/filter/math_filter.rb b/lib/banzai/filter/math_filter.rb
index c915f0ee35b..2247984b86d 100644
--- a/lib/banzai/filter/math_filter.rb
+++ b/lib/banzai/filter/math_filter.rb
@@ -39,7 +39,7 @@ module Banzai
end
end
- doc.css('pre.code.math').each do |el|
+ doc.css('pre.code.language-math').each do |el|
el[STYLE_ATTRIBUTE] = 'display'
el[:class] += " #{TAG_CLASS}"
end
diff --git a/lib/banzai/filter/merge_request_reference_filter.rb b/lib/banzai/filter/merge_request_reference_filter.rb
deleted file mode 100644
index 0b8bd17a71b..00000000000
--- a/lib/banzai/filter/merge_request_reference_filter.rb
+++ /dev/null
@@ -1,97 +0,0 @@
-# frozen_string_literal: true
-
-module Banzai
- module Filter
- # HTML filter that replaces merge request references with links. References
- # to merge requests that do not exist are ignored.
- #
- # This filter supports cross-project references.
- class MergeRequestReferenceFilter < IssuableReferenceFilter
- self.reference_type = :merge_request
-
- def self.object_class
- MergeRequest
- end
-
- def url_for_object(mr, project)
- h = Gitlab::Routing.url_helpers
- h.project_merge_request_url(project, mr,
- only_path: context[:only_path])
- end
-
- def object_link_title(object, matches)
- # The method will return `nil` if object is not a commit
- # allowing for properly handling the extended MR Tooltip
- object_link_commit_title(object, matches)
- end
-
- def object_link_text_extras(object, matches)
- extras = super
-
- if commit_ref = object_link_commit_ref(object, matches)
- klass = reference_class(:commit, tooltip: false)
- commit_ref_tag = %(<span class="#{klass}">#{commit_ref}</span>)
-
- return extras.unshift(commit_ref_tag)
- end
-
- path = matches[:path] if matches.names.include?("path")
-
- case path
- when '/diffs'
- extras.unshift "diffs"
- when '/commits'
- extras.unshift "commits"
- when '/builds'
- extras.unshift "builds"
- end
-
- extras
- end
-
- def parent_records(parent, ids)
- parent.merge_requests
- .where(iid: ids.to_a)
- .includes(target_project: :namespace)
- end
-
- def reference_class(object_sym, options = {})
- super(object_sym, tooltip: false)
- end
-
- def data_attributes_for(text, parent, object, **data)
- super.merge(project_path: parent.full_path, iid: object.iid, mr_title: object.title)
- end
-
- private
-
- def object_link_commit_title(object, matches)
- object_link_commit(object, matches)&.title
- end
-
- def object_link_commit_ref(object, matches)
- object_link_commit(object, matches)&.short_id
- end
-
- def object_link_commit(object, matches)
- return unless matches.names.include?('query') && query = matches[:query]
-
- # Removes leading "?". CGI.parse expects "arg1&arg2&arg3"
- params = CGI.parse(query.sub(/^\?/, ''))
-
- return unless commit_sha = params['commit_id']&.first
-
- if commit = find_commit_by_sha(object, commit_sha)
- Commit.from_hash(commit.to_hash, object.project)
- end
- end
-
- def find_commit_by_sha(object, commit_sha)
- @all_commits ||= {}
- @all_commits[object.id] ||= object.all_commits
-
- @all_commits[object.id].find { |commit| commit.sha == commit_sha }
- end
- end
- end
-end
diff --git a/lib/banzai/filter/milestone_reference_filter.rb b/lib/banzai/filter/milestone_reference_filter.rb
deleted file mode 100644
index 126208db935..00000000000
--- a/lib/banzai/filter/milestone_reference_filter.rb
+++ /dev/null
@@ -1,138 +0,0 @@
-# frozen_string_literal: true
-
-module Banzai
- module Filter
- # HTML filter that replaces milestone references with links.
- class MilestoneReferenceFilter < AbstractReferenceFilter
- include Gitlab::Utils::StrongMemoize
-
- self.reference_type = :milestone
-
- def self.object_class
- Milestone
- end
-
- # Links to project milestones contain the IID, but when we're handling
- # 'regular' references, we need to use the global ID to disambiguate
- # between group and project milestones.
- def find_object(parent, id)
- return unless valid_context?(parent)
-
- find_milestone_with_finder(parent, id: id)
- end
-
- def find_object_from_link(parent, iid)
- return unless valid_context?(parent)
-
- find_milestone_with_finder(parent, iid: iid)
- end
-
- def valid_context?(parent)
- strong_memoize(:valid_context) do
- group_context?(parent) || project_context?(parent)
- end
- end
-
- def group_context?(parent)
- strong_memoize(:group_context) do
- parent.is_a?(Group)
- end
- end
-
- def project_context?(parent)
- strong_memoize(:project_context) do
- parent.is_a?(Project)
- end
- end
-
- def references_in(text, pattern = Milestone.reference_pattern)
- # We'll handle here the references that follow the `reference_pattern`.
- # Other patterns (for example, the link pattern) are handled by the
- # default implementation.
- return super(text, pattern) if pattern != Milestone.reference_pattern
-
- milestones = {}
- unescaped_html = unescape_html_entities(text).gsub(pattern) do |match|
- milestone = find_milestone($~[:project], $~[:namespace], $~[:milestone_iid], $~[:milestone_name])
-
- if milestone
- milestones[milestone.id] = yield match, milestone.id, $~[:project], $~[:namespace], $~
- "#{REFERENCE_PLACEHOLDER}#{milestone.id}"
- else
- match
- end
- end
-
- return text if milestones.empty?
-
- escape_with_placeholders(unescaped_html, milestones)
- end
-
- def find_milestone(project_ref, namespace_ref, milestone_id, milestone_name)
- project_path = full_project_path(namespace_ref, project_ref)
-
- # Returns group if project is not found by path
- parent = parent_from_ref(project_path)
-
- return unless parent
-
- milestone_params = milestone_params(milestone_id, milestone_name)
-
- find_milestone_with_finder(parent, milestone_params)
- end
-
- def milestone_params(iid, name)
- if name
- { name: name.tr('"', '') }
- else
- { iid: iid.to_i }
- end
- end
-
- def find_milestone_with_finder(parent, params)
- finder_params = milestone_finder_params(parent, params[:iid].present?)
-
- MilestonesFinder.new(finder_params).find_by(params)
- end
-
- def milestone_finder_params(parent, find_by_iid)
- { order: nil, state: 'all' }.tap do |params|
- params[:project_ids] = parent.id if project_context?(parent)
-
- # We don't support IID lookups because IIDs can clash between
- # group/project milestones and group/subgroup milestones.
- params[:group_ids] = self_and_ancestors_ids(parent) unless find_by_iid
- end
- end
-
- def self_and_ancestors_ids(parent)
- if group_context?(parent)
- parent.self_and_ancestors.select(:id)
- elsif project_context?(parent)
- parent.group&.self_and_ancestors&.select(:id)
- end
- end
-
- def url_for_object(milestone, project)
- Gitlab::Routing
- .url_helpers
- .milestone_url(milestone, only_path: context[:only_path])
- end
-
- def object_link_text(object, matches)
- milestone_link = escape_once(super)
- reference = object.project&.to_reference_base(project)
-
- if reference.present?
- "#{milestone_link} <i>in #{reference}</i>".html_safe
- else
- milestone_link
- end
- end
-
- def object_link_title(object, matches)
- nil
- end
- end
- end
-end
diff --git a/lib/banzai/filter/project_reference_filter.rb b/lib/banzai/filter/project_reference_filter.rb
deleted file mode 100644
index 50e23460cb8..00000000000
--- a/lib/banzai/filter/project_reference_filter.rb
+++ /dev/null
@@ -1,117 +0,0 @@
-# frozen_string_literal: true
-
-module Banzai
- module Filter
- # HTML filter that replaces project references with links.
- class ProjectReferenceFilter < ReferenceFilter
- self.reference_type = :project
-
- # Public: Find `namespace/project>` project references in text
- #
- # ProjectReferenceFilter.references_in(text) do |match, project|
- # "<a href=...>#{project}></a>"
- # end
- #
- # text - String text to search.
- #
- # Yields the String match, and the String project name.
- #
- # Returns a String replaced with the return of the block.
- def self.references_in(text)
- text.gsub(Project.markdown_reference_pattern) do |match|
- yield match, "#{$~[:namespace]}/#{$~[:project]}"
- end
- end
-
- def call
- ref_pattern = Project.markdown_reference_pattern
- ref_pattern_start = /\A#{ref_pattern}\z/
-
- nodes.each_with_index do |node, index|
- if text_node?(node)
- replace_text_when_pattern_matches(node, index, ref_pattern) do |content|
- project_link_filter(content)
- end
- elsif element_node?(node)
- yield_valid_link(node) do |link, inner_html|
- if link =~ ref_pattern_start
- replace_link_node_with_href(node, index, link) do
- project_link_filter(link, link_content: inner_html)
- end
- end
- end
- end
- end
-
- doc
- end
-
- # Replace `namespace/project>` project references in text with links to the referenced
- # project page.
- #
- # text - String text to replace references in.
- # link_content - Original content of the link being replaced.
- #
- # Returns a String with `namespace/project>` references replaced with links. All links
- # have `gfm` and `gfm-project` class names attached for styling.
- def project_link_filter(text, link_content: nil)
- self.class.references_in(text) do |match, project_path|
- cached_call(:banzai_url_for_object, match, path: [Project, project_path.downcase]) do
- if project = projects_hash[project_path.downcase]
- link_to_project(project, link_content: link_content) || match
- else
- match
- end
- end
- end
- end
-
- # Returns a Hash containing all Project objects for the project
- # references in the current document.
- #
- # The keys of this Hash are the project paths, the values the
- # corresponding Project objects.
- def projects_hash
- @projects ||= Project.eager_load(:route, namespace: [:route])
- .where_full_path_in(projects)
- .index_by(&:full_path)
- .transform_keys(&:downcase)
- end
-
- # Returns all projects referenced in the current document.
- def projects
- refs = Set.new
-
- nodes.each do |node|
- node.to_html.scan(Project.markdown_reference_pattern) do
- refs << "#{$~[:namespace]}/#{$~[:project]}"
- end
- end
-
- refs.to_a
- end
-
- private
-
- def urls
- Gitlab::Routing.url_helpers
- end
-
- def link_class
- reference_class(:project)
- end
-
- def link_to_project(project, link_content: nil)
- url = urls.project_url(project, only_path: context[:only_path])
- data = data_attribute(project: project.id)
- content = link_content || project.to_reference
-
- link_tag(url, data, content, project.name)
- end
-
- def link_tag(url, data, link_content, title)
- %(<a href="#{url}" #{data} class="#{link_class}" title="#{escape_once(title)}">#{link_content}</a>)
- end
- end
- end
-end
diff --git a/lib/banzai/filter/reference_filter.rb b/lib/banzai/filter/reference_filter.rb
deleted file mode 100644
index d22a0e0b504..00000000000
--- a/lib/banzai/filter/reference_filter.rb
+++ /dev/null
@@ -1,215 +0,0 @@
-# frozen_string_literal: true
-
-# Generated HTML is transformed back to GFM by app/assets/javascripts/behaviors/markdown/nodes/reference.js
-module Banzai
- module Filter
- # Base class for GitLab Flavored Markdown reference filters.
- #
- # References within <pre>, <code>, <a>, and <style> elements are ignored.
- #
- # Context options:
- # :project (required) - Current project, ignored if reference is cross-project.
- # :only_path - Generate path-only links.
- class ReferenceFilter < HTML::Pipeline::Filter
- include RequestStoreReferenceCache
- include OutputSafety
-
- class << self
- attr_accessor :reference_type
-
- def call(doc, context = nil, result = nil)
- new(doc, context, result).call_and_update_nodes
- end
- end
-
- def initialize(doc, context = nil, result = nil)
- super
-
- @new_nodes = {}
- @nodes = self.result[:reference_filter_nodes]
- end
-
- def call_and_update_nodes
- with_update_nodes { call }
- end
-
- # Returns a data attribute String to attach to a reference link
- #
- # attributes - Hash, where the key becomes the data attribute name and the
- # value is the data attribute value
- #
- # Examples:
- #
- # data_attribute(project: 1, issue: 2)
- # # => "data-reference-type=\"SomeReferenceFilter\" data-project=\"1\" data-issue=\"2\""
- #
- # data_attribute(project: 3, merge_request: 4)
- # # => "data-reference-type=\"SomeReferenceFilter\" data-project=\"3\" data-merge-request=\"4\""
- #
- # Returns a String
- def data_attribute(attributes = {})
- attributes = attributes.reject { |_, v| v.nil? }
-
- attributes[:reference_type] ||= self.class.reference_type
- attributes[:container] ||= 'body'
- attributes[:placement] ||= 'top'
- attributes.delete(:original) if context[:no_original_data]
- attributes.map do |key, value|
- %Q(data-#{key.to_s.dasherize}="#{escape_once(value)}")
- end.join(' ')
- end
-
- def ignore_ancestor_query
- @ignore_ancestor_query ||= begin
- parents = %w(pre code a style)
- parents << 'blockquote' if context[:ignore_blockquotes]
-
- parents.map { |n| "ancestor::#{n}" }.join(' or ')
- end
- end
-
- def project
- context[:project]
- end
-
- def group
- context[:group]
- end
-
- def user
- context[:user]
- end
-
- def skip_project_check?
- context[:skip_project_check]
- end
-
- def reference_class(type, tooltip: true)
- gfm_klass = "gfm gfm-#{type}"
-
- return gfm_klass unless tooltip
-
- "#{gfm_klass} has-tooltip"
- end
-
- # Ensure that a :project key exists in context
- #
- # Note that while the key might exist, its value could be nil!
- def validate
- needs :project unless skip_project_check?
- end
-
- # Iterates over all <a> and text() nodes in a document.
- #
- # Nodes are skipped whenever their ancestor is one of the nodes returned
- # by `ignore_ancestor_query`. Link tags are not processed if they have a
- # "gfm" class or the "href" attribute is empty.
- def each_node
- return to_enum(__method__) unless block_given?
-
- doc.xpath(query).each do |node|
- yield node
- end
- end
-
- # Returns an Array containing all HTML nodes.
- def nodes
- @nodes ||= each_node.to_a
- end
-
- # Yields the link's URL and inner HTML whenever the node is a valid <a> tag.
- def yield_valid_link(node)
- link = unescape_link(node.attr('href').to_s)
- inner_html = node.inner_html
-
- return unless link.force_encoding('UTF-8').valid_encoding?
-
- yield link, inner_html
- end
-
- def unescape_link(href)
- CGI.unescape(href)
- end
-
- def replace_text_when_pattern_matches(node, index, pattern)
- return unless node.text =~ pattern
-
- content = node.to_html
- html = yield content
-
- replace_text_with_html(node, index, html) unless html == content
- end
-
- def replace_link_node_with_text(node, index)
- html = yield
-
- replace_text_with_html(node, index, html) unless html == node.text
- end
-
- def replace_link_node_with_href(node, index, link)
- html = yield
-
- replace_text_with_html(node, index, html) unless html == link
- end
-
- def text_node?(node)
- node.is_a?(Nokogiri::XML::Text)
- end
-
- def element_node?(node)
- node.is_a?(Nokogiri::XML::Element)
- end
-
- private
-
- def query
- @query ||= %Q{descendant-or-self::text()[not(#{ignore_ancestor_query})]
- | descendant-or-self::a[
- not(contains(concat(" ", @class, " "), " gfm ")) and not(@href = "")
- ]}
- end
-
- def replace_text_with_html(node, index, html)
- replace_and_update_new_nodes(node, index, html)
- end
-
- def replace_and_update_new_nodes(node, index, html)
- previous_node = node.previous
- next_node = node.next
- parent_node = node.parent
- # Unfortunately node.replace(html) returns re-parented nodes, not the actual replaced nodes in the doc
- # We need to find the actual nodes in the doc that were replaced
- node.replace(html)
- @new_nodes[index] = []
-
- # We replaced node with new nodes, so we find first new node. If previous_node is nil, we take first parent child
- new_node = previous_node ? previous_node.next : parent_node&.children&.first
-
- # We iterate from first to last replaced node and store replaced nodes in @new_nodes
- while new_node && new_node != next_node
- @new_nodes[index] << new_node.xpath(query)
- new_node = new_node.next
- end
-
- @new_nodes[index].flatten!
- end
-
- def only_path?
- context[:only_path]
- end
-
- def with_update_nodes
- @new_nodes = {}
- yield.tap { update_nodes! }
- end
-
- # Once Filter completes replacing nodes, we update nodes with @new_nodes
- def update_nodes!
- @new_nodes.sort_by { |index, _new_nodes| -index }.each do |index, new_nodes|
- nodes[index, 1] = new_nodes
- end
- result[:reference_filter_nodes] = nodes
- end
- end
- end
-end
diff --git a/lib/banzai/filter/references/abstract_reference_filter.rb b/lib/banzai/filter/references/abstract_reference_filter.rb
new file mode 100644
index 00000000000..7109373dbce
--- /dev/null
+++ b/lib/banzai/filter/references/abstract_reference_filter.rb
@@ -0,0 +1,448 @@
+# frozen_string_literal: true
+
+module Banzai
+ module Filter
+ module References
+ # Issues, merge requests, Snippets, Commits and Commit Ranges share
+ # similar functionality in reference filtering.
+ class AbstractReferenceFilter < ReferenceFilter
+ include CrossProjectReference
+
+ # REFERENCE_PLACEHOLDER is used for re-escaping HTML text except found
+ # reference (which we replace with placeholder during re-scaping). The
+ # random number helps ensure it's pretty close to unique. Since it's a
+ # transitory value (it never gets saved) we can initialize once, and it
+ # doesn't matter if it changes on a restart.
+ REFERENCE_PLACEHOLDER = "_reference_#{SecureRandom.hex(16)}_"
+ REFERENCE_PLACEHOLDER_PATTERN = %r{#{REFERENCE_PLACEHOLDER}(\d+)}.freeze
+
+ def self.object_class
+ # Implement in child class
+ # Example: MergeRequest
+ end
+
+ def self.object_name
+ @object_name ||= object_class.name.underscore
+ end
+
+ def self.object_sym
+ @object_sym ||= object_name.to_sym
+ end
+
+ # Public: Find references in text (like `!123` for merge requests)
+ #
+ # AnyReferenceFilter.references_in(text) do |match, id, project_ref, matches|
+ # object = find_object(project_ref, id)
+ # "<a href=...>#{object.to_reference}</a>"
+ # end
+ #
+ # text - String text to search.
+ #
+ # Yields the String match, the Integer referenced object ID, an optional String
+ # of the external project reference, and all of the matchdata.
+ #
+ # Returns a String replaced with the return of the block.
+ def self.references_in(text, pattern = object_class.reference_pattern)
+ text.gsub(pattern) do |match|
+ if ident = identifier($~)
+ yield match, ident, $~[:project], $~[:namespace], $~
+ else
+ match
+ end
+ end
+ end
+
+ def self.identifier(match_data)
+ symbol = symbol_from_match(match_data)
+
+ parse_symbol(symbol, match_data) if object_class.reference_valid?(symbol)
+ end
+
+ def identifier(match_data)
+ self.class.identifier(match_data)
+ end
+
+ def self.symbol_from_match(match)
+ key = object_sym
+ match[key] if match.names.include?(key.to_s)
+ end
+
+ # Transform a symbol extracted from the text to a meaningful value
+ # In most cases these will be integers, so we call #to_i by default
+ #
+ # This method has the contract that if a string `ref` refers to a
+ # record `record`, then `parse_symbol(ref) == record_identifier(record)`.
+ def self.parse_symbol(symbol, match_data)
+ symbol.to_i
+ end
+
+ # We assume that most classes are identifying records by ID.
+ #
+ # This method has the contract that if a string `ref` refers to a
+ # record `record`, then `class.parse_symbol(ref) == record_identifier(record)`.
+ def record_identifier(record)
+ record.id
+ end
+
+ def object_class
+ self.class.object_class
+ end
+
+ def object_sym
+ self.class.object_sym
+ end
+
+ def references_in(*args, &block)
+ self.class.references_in(*args, &block)
+ end
+
+ # Implement in child class
+ # Example: project.merge_requests.find
+ def find_object(parent_object, id)
+ end
+
+ # Override if the link reference pattern produces a different ID (global
+ # ID vs internal ID, for instance) to the regular reference pattern.
+ def find_object_from_link(parent_object, id)
+ find_object(parent_object, id)
+ end
+
+ # Implement in child class
+ # Example: project_merge_request_url
+ def url_for_object(object, parent_object)
+ end
+
+ def find_object_cached(parent_object, id)
+ cached_call(:banzai_find_object, id, path: [object_class, parent_object.id]) do
+ find_object(parent_object, id)
+ end
+ end
+
+ def find_object_from_link_cached(parent_object, id)
+ cached_call(:banzai_find_object_from_link, id, path: [object_class, parent_object.id]) do
+ find_object_from_link(parent_object, id)
+ end
+ end
+
+ def from_ref_cached(ref)
+ cached_call("banzai_#{parent_type}_refs".to_sym, ref) do
+ parent_from_ref(ref)
+ end
+ end
+
+ def url_for_object_cached(object, parent_object)
+ cached_call(:banzai_url_for_object, object, path: [object_class, parent_object.id]) do
+ url_for_object(object, parent_object)
+ end
+ end
+
+ def call
+ return doc unless project || group || user
+
+ ref_pattern = object_class.reference_pattern
+ link_pattern = object_class.link_reference_pattern
+
+ # Compile often used regexps only once outside of the loop
+ ref_pattern_anchor = /\A#{ref_pattern}\z/
+ link_pattern_start = /\A#{link_pattern}/
+ link_pattern_anchor = /\A#{link_pattern}\z/
+
+ nodes.each_with_index do |node, index|
+ if text_node?(node) && ref_pattern
+ replace_text_when_pattern_matches(node, index, ref_pattern) do |content|
+ object_link_filter(content, ref_pattern)
+ end
+
+ elsif element_node?(node)
+ yield_valid_link(node) do |link, inner_html|
+ if ref_pattern && link =~ ref_pattern_anchor
+ replace_link_node_with_href(node, index, link) do
+ object_link_filter(link, ref_pattern, link_content: inner_html)
+ end
+
+ next
+ end
+
+ next unless link_pattern
+
+ if link == inner_html && inner_html =~ link_pattern_start
+ replace_link_node_with_text(node, index) do
+ object_link_filter(inner_html, link_pattern, link_reference: true)
+ end
+
+ next
+ end
+
+ if link =~ link_pattern_anchor
+ replace_link_node_with_href(node, index, link) do
+ object_link_filter(link, link_pattern, link_content: inner_html, link_reference: true)
+ end
+
+ next
+ end
+ end
+ end
+ end
+
+ doc
+ end
+
+ # Replace references (like `!123` for merge requests) in text with links
+ # to the referenced object's details page.
+ #
+ # text - String text to replace references in.
+ # pattern - Reference pattern to match against.
+ # link_content - Original content of the link being replaced.
+ # link_reference - True if this was using the link reference pattern,
+ # false otherwise.
+ #
+ # Returns a String with references replaced with links. All links
+ # have `gfm` and `gfm-OBJECT_NAME` class names attached for styling.
+ def object_link_filter(text, pattern, link_content: nil, link_reference: false)
+ references_in(text, pattern) do |match, id, project_ref, namespace_ref, matches|
+ parent_path = if parent_type == :group
+ full_group_path(namespace_ref)
+ else
+ full_project_path(namespace_ref, project_ref)
+ end
+
+ parent = from_ref_cached(parent_path)
+
+ if parent
+ object =
+ if link_reference
+ find_object_from_link_cached(parent, id)
+ else
+ find_object_cached(parent, id)
+ end
+ end
+
+ if object
+ title = object_link_title(object, matches)
+ klass = reference_class(object_sym)
+
+ data_attributes = data_attributes_for(link_content || match, parent, object,
+ link_content: !!link_content,
+ link_reference: link_reference)
+ data = data_attribute(data_attributes)
+
+ url =
+ if matches.names.include?("url") && matches[:url]
+ matches[:url]
+ else
+ url_for_object_cached(object, parent)
+ end
+
+ content = link_content || object_link_text(object, matches)
+
+ link = %(<a href="#{url}" #{data}
+ title="#{escape_once(title)}"
+ class="#{klass}">#{content}</a>)
+
+ wrap_link(link, object)
+ else
+ match
+ end
+ end
+ end
+
+ def wrap_link(link, object)
+ link
+ end
+
+ def data_attributes_for(text, parent, object, link_content: false, link_reference: false)
+ object_parent_type = parent.is_a?(Group) ? :group : :project
+
+ {
+ original: escape_html_entities(text),
+ link: link_content,
+ link_reference: link_reference,
+ object_parent_type => parent.id,
+ object_sym => object.id
+ }
+ end
+
+ def object_link_text_extras(object, matches)
+ extras = []
+
+ if matches.names.include?("anchor") && matches[:anchor] && matches[:anchor] =~ /\A\#note_(\d+)\z/
+ extras << "comment #{Regexp.last_match(1)}"
+ end
+
+ extension = matches[:extension] if matches.names.include?("extension")
+
+ extras << extension if extension
+
+ extras
+ end
+
+ def object_link_title(object, matches)
+ object.title
+ end
+
+ def object_link_text(object, matches)
+ parent = project || group || user
+ text = object.reference_link_text(parent)
+
+ extras = object_link_text_extras(object, matches)
+ text += " (#{extras.join(", ")})" if extras.any?
+
+ text
+ end
+
+ # Returns a Hash containing all object references (e.g. issue IDs) per the
+ # project they belong to.
+ def references_per_parent
+ @references_per ||= {}
+
+ @references_per[parent_type] ||= begin
+ refs = Hash.new { |hash, key| hash[key] = Set.new }
+ regex = [
+ object_class.link_reference_pattern,
+ object_class.reference_pattern
+ ].compact.reduce { |a, b| Regexp.union(a, b) }
+
+ nodes.each do |node|
+ node.to_html.scan(regex) do
+ path = if parent_type == :project
+ full_project_path($~[:namespace], $~[:project])
+ else
+ full_group_path($~[:group])
+ end
+
+ if ident = identifier($~)
+ refs[path] << ident
+ end
+ end
+ end
+
+ refs
+ end
+ end
+
+ # Returns a Hash containing referenced projects grouped per their full
+ # path.
+ def parent_per_reference
+ @per_reference ||= {}
+
+ @per_reference[parent_type] ||= begin
+ refs = Set.new
+
+ references_per_parent.each do |ref, _|
+ refs << ref
+ end
+
+ find_for_paths(refs.to_a).index_by(&:full_path)
+ end
+ end
+
+ def relation_for_paths(paths)
+ klass = parent_type.to_s.camelize.constantize
+ result = klass.where_full_path_in(paths)
+ return result if parent_type == :group
+
+ result.includes(:namespace) if parent_type == :project
+ end
+
+ # Returns projects for the given paths.
+ def find_for_paths(paths)
+ if Gitlab::SafeRequestStore.active?
+ cache = refs_cache
+ to_query = paths - cache.keys
+
+ unless to_query.empty?
+ records = relation_for_paths(to_query)
+
+ found = []
+ records.each do |record|
+ ref = record.full_path
+ get_or_set_cache(cache, ref) { record }
+ found << ref
+ end
+
+ not_found = to_query - found
+ not_found.each do |ref|
+ get_or_set_cache(cache, ref) { nil }
+ end
+ end
+
+ cache.slice(*paths).values.compact
+ else
+ relation_for_paths(paths)
+ end
+ end
+
+ def current_parent_path
+ @current_parent_path ||= parent&.full_path
+ end
+
+ def current_project_namespace_path
+ @current_project_namespace_path ||= project&.namespace&.full_path
+ end
+
+ def records_per_parent
+ @_records_per_project ||= {}
+
+ @_records_per_project[object_class.to_s.underscore] ||= begin
+ hash = Hash.new { |h, k| h[k] = {} }
+
+ parent_per_reference.each do |path, parent|
+ record_ids = references_per_parent[path]
+
+ parent_records(parent, record_ids).each do |record|
+ hash[parent][record_identifier(record)] = record
+ end
+ end
+
+ hash
+ end
+ end
+
+ private
+
+ def full_project_path(namespace, project_ref)
+ return current_parent_path unless project_ref
+
+ namespace_ref = namespace || current_project_namespace_path
+ "#{namespace_ref}/#{project_ref}"
+ end
+
+ def refs_cache
+ Gitlab::SafeRequestStore["banzai_#{parent_type}_refs".to_sym] ||= {}
+ end
+
+ def parent_type
+ :project
+ end
+
+ def parent
+ parent_type == :project ? project : group
+ end
+
+ def full_group_path(group_ref)
+ return current_parent_path unless group_ref
+
+ group_ref
+ end
+
+ def unescape_html_entities(text)
+ CGI.unescapeHTML(text.to_s)
+ end
+
+ def escape_html_entities(text)
+ CGI.escapeHTML(text.to_s)
+ end
+
+ def escape_with_placeholders(text, placeholder_data)
+ escaped = escape_html_entities(text)
+
+ escaped.gsub(REFERENCE_PLACEHOLDER_PATTERN) do |match|
+ placeholder_data[Regexp.last_match(1).to_i]
+ end
+ end
+ end
+ end
+ end
+end
+
+Banzai::Filter::References::AbstractReferenceFilter.prepend_if_ee('EE::Banzai::Filter::References::AbstractReferenceFilter')
diff --git a/lib/banzai/filter/references/alert_reference_filter.rb b/lib/banzai/filter/references/alert_reference_filter.rb
new file mode 100644
index 00000000000..90fef536605
--- /dev/null
+++ b/lib/banzai/filter/references/alert_reference_filter.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+module Banzai
+ module Filter
+ module References
+ class AlertReferenceFilter < IssuableReferenceFilter
+ self.reference_type = :alert
+
+ def self.object_class
+ AlertManagement::Alert
+ end
+
+ def self.object_sym
+ :alert
+ end
+
+ def parent_records(parent, ids)
+ parent.alert_management_alerts.where(iid: ids.to_a)
+ end
+
+ def url_for_object(alert, project)
+ ::Gitlab::Routing.url_helpers.details_project_alert_management_url(
+ project,
+ alert.iid,
+ only_path: context[:only_path]
+ )
+ end
+ end
+ end
+ end
+end
diff --git a/lib/banzai/filter/references/commit_range_reference_filter.rb b/lib/banzai/filter/references/commit_range_reference_filter.rb
new file mode 100644
index 00000000000..ad79f8a173c
--- /dev/null
+++ b/lib/banzai/filter/references/commit_range_reference_filter.rb
@@ -0,0 +1,48 @@
+# frozen_string_literal: true
+
+module Banzai
+ module Filter
+ module References
+ # HTML filter that replaces commit range references with links.
+ #
+ # This filter supports cross-project references.
+ class CommitRangeReferenceFilter < AbstractReferenceFilter
+ self.reference_type = :commit_range
+
+ def self.object_class
+ CommitRange
+ end
+
+ def self.references_in(text, pattern = CommitRange.reference_pattern)
+ text.gsub(pattern) do |match|
+ yield match, $~[:commit_range], $~[:project], $~[:namespace], $~
+ end
+ end
+
+ def initialize(*args)
+ super
+
+ @commit_map = {}
+ end
+
+ def find_object(project, id)
+ return unless project.is_a?(Project)
+
+ range = CommitRange.new(id, project)
+
+ range.valid_commits? ? range : nil
+ end
+
+ def url_for_object(range, project)
+ h = Gitlab::Routing.url_helpers
+ h.project_compare_url(project,
+ range.to_param.merge(only_path: context[:only_path]))
+ end
+
+ def object_link_title(range, matches)
+ nil
+ end
+ end
+ end
+ end
+end
diff --git a/lib/banzai/filter/references/commit_reference_filter.rb b/lib/banzai/filter/references/commit_reference_filter.rb
new file mode 100644
index 00000000000..457921bd07d
--- /dev/null
+++ b/lib/banzai/filter/references/commit_reference_filter.rb
@@ -0,0 +1,88 @@
+# frozen_string_literal: true
+
+module Banzai
+ module Filter
+ module References
+ # HTML filter that replaces commit references with links.
+ #
+ # This filter supports cross-project references.
+ class CommitReferenceFilter < AbstractReferenceFilter
+ self.reference_type = :commit
+
+ def self.object_class
+ Commit
+ end
+
+ def self.references_in(text, pattern = Commit.reference_pattern)
+ text.gsub(pattern) do |match|
+ yield match, $~[:commit], $~[:project], $~[:namespace], $~
+ end
+ end
+
+ def find_object(project, id)
+ return unless project.is_a?(Project) && project.valid_repo?
+
+ _, record = records_per_parent[project].detect { |k, _v| Gitlab::Git.shas_eql?(k, id) }
+
+ record
+ end
+
+ def referenced_merge_request_commit_shas
+ return [] unless noteable.is_a?(MergeRequest)
+
+ @referenced_merge_request_commit_shas ||= begin
+ referenced_shas = references_per_parent.values.reduce(:|).to_a
+ noteable.all_commit_shas.select do |sha|
+ referenced_shas.any? { |ref| Gitlab::Git.shas_eql?(sha, ref) }
+ end
+ end
+ end
+
+ # The default behaviour is `#to_i` - we just pass the hash through.
+ def self.parse_symbol(sha_hash, _match)
+ sha_hash
+ end
+
+ def url_for_object(commit, project)
+ h = Gitlab::Routing.url_helpers
+
+ if referenced_merge_request_commit_shas.include?(commit.id)
+ h.diffs_project_merge_request_url(project,
+ noteable,
+ commit_id: commit.id,
+ only_path: only_path?)
+ else
+ h.project_commit_url(project,
+ commit,
+ only_path: only_path?)
+ end
+ end
+
+ def object_link_text_extras(object, matches)
+ extras = super
+
+ path = matches[:path] if matches.names.include?("path")
+ if path == '/builds'
+ extras.unshift "builds"
+ end
+
+ extras
+ end
+
+ private
+
+ def parent_records(parent, ids)
+ parent.commits_by(oids: ids.to_a)
+ end
+
+ def noteable
+ context[:noteable]
+ end
+
+ def only_path?
+ context[:only_path]
+ end
+ end
+ end
+ end
+end
diff --git a/lib/banzai/filter/references/design_reference_filter.rb b/lib/banzai/filter/references/design_reference_filter.rb
new file mode 100644
index 00000000000..61234e61c15
--- /dev/null
+++ b/lib/banzai/filter/references/design_reference_filter.rb
@@ -0,0 +1,109 @@
+# frozen_string_literal: true
+
+module Banzai
+ module Filter
+ module References
+ class DesignReferenceFilter < AbstractReferenceFilter
+ class Identifier
+ include Comparable
+ attr_reader :issue_iid, :filename
+
+ def initialize(issue_iid:, filename:)
+ @issue_iid = issue_iid
+ @filename = filename
+ end
+
+ def as_composite_id(id_for_iid)
+ id = id_for_iid[issue_iid]
+ return unless id
+
+ { issue_id: id, filename: filename }
+ end
+
+ def <=>(other)
+ return unless other.is_a?(Identifier)
+
+ [issue_iid, filename] <=> [other.issue_iid, other.filename]
+ end
+ alias_method :eql?, :==
+
+ def hash
+ [issue_iid, filename].hash
+ end
+ end
+
+ self.reference_type = :design
+
+ def find_object(project, identifier)
+ records_per_parent[project][identifier]
+ end
+
+ def parent_records(project, identifiers)
+ return [] unless project.design_management_enabled?
+
+ iids = identifiers.map(&:issue_iid).to_set
+ issues = project.issues.where(iid: iids)
+ id_for_iid = issues.index_by(&:iid).transform_values(&:id)
+ issue_by_id = issues.index_by(&:id)
+
+ designs(identifiers, id_for_iid).each do |d|
+ issue = issue_by_id[d.issue_id]
+ # optimisation: assign values we have already fetched
+ d.project = project
+ d.issue = issue
+ end
+ end
+
+ def relation_for_paths(paths)
+ super.includes(:route, :namespace, :group)
+ end
+
+ def parent_type
+ :project
+ end
+
+ # optimisation to reuse the parent_per_reference query information
+ def parent_from_ref(ref)
+ parent_per_reference[ref || current_parent_path]
+ end
+
+ def url_for_object(design, project)
+ path_options = { vueroute: design.filename }
+ Gitlab::Routing.url_helpers.designs_project_issue_path(project, design.issue, path_options)
+ end
+
+ def data_attributes_for(_text, _project, design, **_kwargs)
+ super.merge(issue: design.issue_id)
+ end
+
+ def self.object_class
+ ::DesignManagement::Design
+ end
+
+ def self.object_sym
+ :design
+ end
+
+ def self.parse_symbol(raw, match_data)
+ filename = match_data[:url_filename]
+ iid = match_data[:issue].to_i
+ Identifier.new(filename: CGI.unescape(filename), issue_iid: iid)
+ end
+
+ def record_identifier(design)
+ Identifier.new(filename: design.filename, issue_iid: design.issue.iid)
+ end
+
+ private
+
+ def designs(identifiers, id_for_iid)
+ identifiers
+ .map { |identifier| identifier.as_composite_id(id_for_iid) }
+ .compact
+ .in_groups_of(100, false) # limitation of by_issue_id_and_filename, so we batch
+ .flat_map { |ids| DesignManagement::Design.by_issue_id_and_filename(ids) }
+ end
+ end
+ end
+ end
+end
diff --git a/lib/banzai/filter/references/epic_reference_filter.rb b/lib/banzai/filter/references/epic_reference_filter.rb
new file mode 100644
index 00000000000..4ee446e5317
--- /dev/null
+++ b/lib/banzai/filter/references/epic_reference_filter.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+module Banzai
+ module Filter
+ module References
+ # The actual filter is implemented in the EE mixin
+ class EpicReferenceFilter < IssuableReferenceFilter
+ self.reference_type = :epic
+
+ def self.object_class
+ Epic
+ end
+
+ private
+
+ def group
+ context[:group] || context[:project]&.group
+ end
+ end
+ end
+ end
+end
+
+Banzai::Filter::References::EpicReferenceFilter.prepend_if_ee('EE::Banzai::Filter::References::EpicReferenceFilter')
diff --git a/lib/banzai/filter/references/external_issue_reference_filter.rb b/lib/banzai/filter/references/external_issue_reference_filter.rb
new file mode 100644
index 00000000000..247e20967df
--- /dev/null
+++ b/lib/banzai/filter/references/external_issue_reference_filter.rb
@@ -0,0 +1,120 @@
+# frozen_string_literal: true
+
+module Banzai
+ module Filter
+ module References
+ # HTML filter that replaces external issue tracker references with links.
+ # References are ignored if the project doesn't use an external issue
+ # tracker.
+ #
+ # This filter does not support cross-project references.
+ class ExternalIssueReferenceFilter < ReferenceFilter
+ self.reference_type = :external_issue
+
+ # Public: Find `JIRA-123` issue references in text
+ #
+ # ExternalIssueReferenceFilter.references_in(text, pattern) do |match, issue|
+ # "<a href=...>##{issue}</a>"
+ # end
+ #
+ # text - String text to search.
+ #
+ # Yields the String match and the String issue reference.
+ #
+ # Returns a String replaced with the return of the block.
+ def self.references_in(text, pattern)
+ text.gsub(pattern) do |match|
+ yield match, $~[:issue]
+ end
+ end
+
+ def call
+ # Early return if the project isn't using an external tracker
+ return doc if project.nil? || default_issues_tracker?
+
+ ref_pattern = issue_reference_pattern
+ ref_start_pattern = /\A#{ref_pattern}\z/
+
+ nodes.each_with_index do |node, index|
+ if text_node?(node)
+ replace_text_when_pattern_matches(node, index, ref_pattern) do |content|
+ issue_link_filter(content)
+ end
+
+ elsif element_node?(node)
+ yield_valid_link(node) do |link, inner_html|
+ if link =~ ref_start_pattern
+ replace_link_node_with_href(node, index, link) do
+ issue_link_filter(link, link_content: inner_html)
+ end
+ end
+ end
+ end
+ end
+
+ doc
+ end
+
+ private
+
+ # Replace `JIRA-123` issue references in text with links to the referenced
+ # issue's details page.
+ #
+ # text - String text to replace references in.
+ # link_content - Original content of the link being replaced.
+ #
+ # Returns a String with `JIRA-123` references replaced with links. All
+ # links have `gfm` and `gfm-issue` class names attached for styling.
+ def issue_link_filter(text, link_content: nil)
+ self.class.references_in(text, issue_reference_pattern) do |match, id|
+ url = url_for_issue(id)
+ klass = reference_class(:issue)
+ data = data_attribute(project: project.id, external_issue: id)
+ content = link_content || match
+
+ %(<a href="#{url}" #{data}
+ title="#{escape_once(issue_title)}"
+ class="#{klass}">#{content}</a>)
+ end
+ end
+
+ def url_for_issue(issue_id)
+ return '' if project.nil?
+
+ url = if only_path?
+ project.external_issue_tracker.issue_path(issue_id)
+ else
+ project.external_issue_tracker.issue_url(issue_id)
+ end
+
+ # Ensure we return a valid URL to prevent possible XSS.
+ URI.parse(url).to_s
+ rescue URI::InvalidURIError
+ ''
+ end
+
+ def default_issues_tracker?
+ external_issues_cached(:default_issues_tracker?)
+ end
+
+ def issue_reference_pattern
+ external_issues_cached(:external_issue_reference_pattern)
+ end
+
+ def project
+ context[:project]
+ end
+
+ def issue_title
+ "Issue in #{project.external_issue_tracker.title}"
+ end
+
+ def external_issues_cached(attribute)
+ cached_attributes = Gitlab::SafeRequestStore[:banzai_external_issues_tracker_attributes] ||= Hash.new { |h, k| h[k] = {} }
+ cached_attributes[project.id][attribute] = project.public_send(attribute) if cached_attributes[project.id][attribute].nil? # rubocop:disable GitlabSecurity/PublicSend
+ cached_attributes[project.id][attribute]
+ end
+ end
+ end
+ end
+end
diff --git a/lib/banzai/filter/references/feature_flag_reference_filter.rb b/lib/banzai/filter/references/feature_flag_reference_filter.rb
new file mode 100644
index 00000000000..be9ded1ff43
--- /dev/null
+++ b/lib/banzai/filter/references/feature_flag_reference_filter.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+module Banzai
+ module Filter
+ module References
+ class FeatureFlagReferenceFilter < IssuableReferenceFilter
+ self.reference_type = :feature_flag
+
+ def self.object_class
+ Operations::FeatureFlag
+ end
+
+ def self.object_sym
+ :feature_flag
+ end
+
+ def parent_records(parent, ids)
+ parent.operations_feature_flags.where(iid: ids.to_a)
+ end
+
+ def url_for_object(feature_flag, project)
+ ::Gitlab::Routing.url_helpers.edit_project_feature_flag_url(
+ project,
+ feature_flag.iid,
+ only_path: context[:only_path]
+ )
+ end
+
+ def object_link_title(object, matches)
+ object.name
+ end
+ end
+ end
+ end
+end
diff --git a/lib/banzai/filter/references/issuable_reference_filter.rb b/lib/banzai/filter/references/issuable_reference_filter.rb
new file mode 100644
index 00000000000..b8ccb926ae9
--- /dev/null
+++ b/lib/banzai/filter/references/issuable_reference_filter.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+module Banzai
+ module Filter
+ module References
+ class IssuableReferenceFilter < AbstractReferenceFilter
+ def record_identifier(record)
+ record.iid.to_i
+ end
+
+ def find_object(parent, iid)
+ records_per_parent[parent][iid]
+ end
+
+ def parent_from_ref(ref)
+ parent_per_reference[ref || current_parent_path]
+ end
+ end
+ end
+ end
+end
diff --git a/lib/banzai/filter/references/issue_reference_filter.rb b/lib/banzai/filter/references/issue_reference_filter.rb
new file mode 100644
index 00000000000..eacf261b15f
--- /dev/null
+++ b/lib/banzai/filter/references/issue_reference_filter.rb
@@ -0,0 +1,59 @@
+# frozen_string_literal: true
+
+module Banzai
+ module Filter
+ module References
+ # HTML filter that replaces issue references with links. References to
+ # issues that do not exist are ignored.
+ #
+ # This filter supports cross-project references.
+ #
+ # When external issues tracker like Jira is activated we should not
+ # use issue reference pattern, but we should still be able
+ # to reference issues from other GitLab projects.
+ class IssueReferenceFilter < IssuableReferenceFilter
+ self.reference_type = :issue
+
+ def self.object_class
+ Issue
+ end
+
+ def url_for_object(issue, project)
+ return issue_path(issue, project) if only_path?
+
+ issue_url(issue, project)
+ end
+
+ def parent_records(parent, ids)
+ parent.issues.where(iid: ids.to_a)
+ end
+
+ def object_link_text_extras(issue, matches)
+ super + design_link_extras(issue, matches.named_captures['path'])
+ end
+
+ private
+
+ def issue_path(issue, project)
+ Gitlab::Routing.url_helpers.namespace_project_issue_path(namespace_id: project.namespace, project_id: project, id: issue.iid)
+ end
+
+ def issue_url(issue, project)
+ Gitlab::Routing.url_helpers.namespace_project_issue_url(namespace_id: project.namespace, project_id: project, id: issue.iid)
+ end
+
+ def design_link_extras(issue, path)
+ if path == '/designs' && read_designs?(issue)
+ ['designs']
+ else
+ []
+ end
+ end
+
+ def read_designs?(issue)
+ issue.project.design_management_enabled?
+ end
+ end
+ end
+ end
+end
diff --git a/lib/banzai/filter/references/iteration_reference_filter.rb b/lib/banzai/filter/references/iteration_reference_filter.rb
new file mode 100644
index 00000000000..cf3d446147f
--- /dev/null
+++ b/lib/banzai/filter/references/iteration_reference_filter.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+module Banzai
+ module Filter
+ module References
+ # The actual filter is implemented in the EE mixin
+ class IterationReferenceFilter < AbstractReferenceFilter
+ self.reference_type = :iteration
+
+ def self.object_class
+ Iteration
+ end
+ end
+ end
+ end
+end
+
+Banzai::Filter::References::IterationReferenceFilter.prepend_if_ee('EE::Banzai::Filter::References::IterationReferenceFilter')
diff --git a/lib/banzai/filter/references/label_reference_filter.rb b/lib/banzai/filter/references/label_reference_filter.rb
new file mode 100644
index 00000000000..a6a5eec5d9a
--- /dev/null
+++ b/lib/banzai/filter/references/label_reference_filter.rb
@@ -0,0 +1,132 @@
+# frozen_string_literal: true
+
+module Banzai
+ module Filter
+ module References
+ # HTML filter that replaces label references with links.
+ class LabelReferenceFilter < AbstractReferenceFilter
+ self.reference_type = :label
+
+ def self.object_class
+ Label
+ end
+
+ def find_object(parent_object, id)
+ find_labels(parent_object).find(id)
+ end
+
+ def references_in(text, pattern = Label.reference_pattern)
+ labels = {}
+ unescaped_html = unescape_html_entities(text).gsub(pattern) do |match|
+ namespace = $~[:namespace]
+ project = $~[:project]
+ project_path = full_project_path(namespace, project)
+ label = find_label_cached(project_path, $~[:label_id], $~[:label_name])
+
+ if label
+ labels[label.id] = yield match, label.id, project, namespace, $~
+ "#{REFERENCE_PLACEHOLDER}#{label.id}"
+ else
+ match
+ end
+ end
+
+ return text if labels.empty?
+
+ escape_with_placeholders(unescaped_html, labels)
+ end
+
+ def find_label_cached(parent_ref, label_id, label_name)
+ cached_call(:banzai_find_label_cached, label_name&.tr('"', '') || label_id, path: [object_class, parent_ref]) do
+ find_label(parent_ref, label_id, label_name)
+ end
+ end
+
+ def find_label(parent_ref, label_id, label_name)
+ parent = parent_from_ref(parent_ref)
+ return unless parent
+
+ label_params = label_params(label_id, label_name)
+ find_labels(parent).find_by(label_params)
+ end
+
+ def find_labels(parent)
+ params = if parent.is_a?(Group)
+ { group_id: parent.id,
+ include_ancestor_groups: true,
+ only_group_labels: true }
+ else
+ { project: parent,
+ include_ancestor_groups: true }
+ end
+
+ LabelsFinder.new(nil, params).execute(skip_authorization: true)
+ end
+
+ # Parameters to pass to `Label.find_by` based on the given arguments
+ #
+ # id - Integer ID to pass. If present, returns {id: id}
+ # name - String name to pass. If `id` is absent, finds by name without
+ # surrounding quotes.
+ #
+ # Returns a Hash.
+ def label_params(id, name)
+ if name
+ { name: name.tr('"', '') }
+ else
+ { id: id.to_i }
+ end
+ end
+
+ def url_for_object(label, parent)
+ label_url_method =
+ if context[:label_url_method]
+ context[:label_url_method]
+ elsif parent.is_a?(Project)
+ :project_issues_url
+ end
+
+ return unless label_url_method
+
+ Gitlab::Routing.url_helpers.public_send(label_url_method, parent, label_name: label.name, only_path: context[:only_path]) # rubocop:disable GitlabSecurity/PublicSend
+ end
+
+ def object_link_text(object, matches)
+ label_suffix = ''
+ parent = project || group
+
+ if project || full_path_ref?(matches)
+ project_path = full_project_path(matches[:namespace], matches[:project])
+ parent_from_ref = from_ref_cached(project_path)
+ reference = parent_from_ref.to_human_reference(parent)
+
+ label_suffix = " <i>in #{ERB::Util.html_escape(reference)}</i>" if reference.present?
+ end
+
+ presenter = object.present(issuable_subject: parent)
+ LabelsHelper.render_colored_label(presenter, suffix: label_suffix)
+ end
+
+ def wrap_link(link, label)
+ presenter = label.present(issuable_subject: project || group)
+ LabelsHelper.wrap_label_html(link, small: true, label: presenter)
+ end
+
+ def full_path_ref?(matches)
+ matches[:namespace] && matches[:project]
+ end
+
+ def reference_class(type, tooltip: true)
+ super + ' gl-link gl-label-link'
+ end
+
+ def object_link_title(object, matches)
+ presenter = object.present(issuable_subject: project || group)
+ LabelsHelper.label_tooltip_title(presenter)
+ end
+ end
+ end
+ end
+end
+
+Banzai::Filter::References::LabelReferenceFilter.prepend_if_ee('EE::Banzai::Filter::References::LabelReferenceFilter')
diff --git a/lib/banzai/filter/references/merge_request_reference_filter.rb b/lib/banzai/filter/references/merge_request_reference_filter.rb
new file mode 100644
index 00000000000..872c33f6873
--- /dev/null
+++ b/lib/banzai/filter/references/merge_request_reference_filter.rb
@@ -0,0 +1,99 @@
+# frozen_string_literal: true
+
+module Banzai
+ module Filter
+ module References
+ # HTML filter that replaces merge request references with links. References
+ # to merge requests that do not exist are ignored.
+ #
+ # This filter supports cross-project references.
+ class MergeRequestReferenceFilter < IssuableReferenceFilter
+ self.reference_type = :merge_request
+
+ def self.object_class
+ MergeRequest
+ end
+
+ def url_for_object(mr, project)
+ h = Gitlab::Routing.url_helpers
+ h.project_merge_request_url(project, mr,
+ only_path: context[:only_path])
+ end
+
+ def object_link_title(object, matches)
+ # The method will return `nil` if object is not a commit
+ # allowing for properly handling the extended MR Tooltip
+ object_link_commit_title(object, matches)
+ end
+
+ def object_link_text_extras(object, matches)
+ extras = super
+
+ if commit_ref = object_link_commit_ref(object, matches)
+ klass = reference_class(:commit, tooltip: false)
+ commit_ref_tag = %(<span class="#{klass}">#{commit_ref}</span>)
+
+ return extras.unshift(commit_ref_tag)
+ end
+
+ path = matches[:path] if matches.names.include?("path")
+
+ case path
+ when '/diffs'
+ extras.unshift "diffs"
+ when '/commits'
+ extras.unshift "commits"
+ when '/builds'
+ extras.unshift "builds"
+ end
+
+ extras
+ end
+
+ def parent_records(parent, ids)
+ parent.merge_requests
+ .where(iid: ids.to_a)
+ .includes(target_project: :namespace)
+ end
+
+ def reference_class(object_sym, options = {})
+ super(object_sym, tooltip: false)
+ end
+
+ def data_attributes_for(text, parent, object, **data)
+ super.merge(project_path: parent.full_path, iid: object.iid, mr_title: object.title)
+ end
+
+ private
+
+ def object_link_commit_title(object, matches)
+ object_link_commit(object, matches)&.title
+ end
+
+ def object_link_commit_ref(object, matches)
+ object_link_commit(object, matches)&.short_id
+ end
+
+ def object_link_commit(object, matches)
+ return unless matches.names.include?('query') && query = matches[:query]
+
+ # Removes leading "?". CGI.parse expects "arg1&arg2&arg3"
+ params = CGI.parse(query.sub(/^\?/, ''))
+
+ return unless commit_sha = params['commit_id']&.first
+
+ if commit = find_commit_by_sha(object, commit_sha)
+ Commit.from_hash(commit.to_hash, object.project)
+ end
+ end
+
+ def find_commit_by_sha(object, commit_sha)
+ @all_commits ||= {}
+ @all_commits[object.id] ||= object.all_commits
+
+ @all_commits[object.id].find { |commit| commit.sha == commit_sha }
+ end
+ end
+ end
+ end
+end
diff --git a/lib/banzai/filter/references/milestone_reference_filter.rb b/lib/banzai/filter/references/milestone_reference_filter.rb
new file mode 100644
index 00000000000..49110194ddc
--- /dev/null
+++ b/lib/banzai/filter/references/milestone_reference_filter.rb
@@ -0,0 +1,140 @@
+# frozen_string_literal: true
+
+module Banzai
+ module Filter
+ module References
+ # HTML filter that replaces milestone references with links.
+ class MilestoneReferenceFilter < AbstractReferenceFilter
+ include Gitlab::Utils::StrongMemoize
+
+ self.reference_type = :milestone
+
+ def self.object_class
+ Milestone
+ end
+
+ # Links to project milestones contain the IID, but when we're handling
+ # 'regular' references, we need to use the global ID to disambiguate
+ # between group and project milestones.
+ def find_object(parent, id)
+ return unless valid_context?(parent)
+
+ find_milestone_with_finder(parent, id: id)
+ end
+
+ def find_object_from_link(parent, iid)
+ return unless valid_context?(parent)
+
+ find_milestone_with_finder(parent, iid: iid)
+ end
+
+ def valid_context?(parent)
+ strong_memoize(:valid_context) do
+ group_context?(parent) || project_context?(parent)
+ end
+ end
+
+ def group_context?(parent)
+ strong_memoize(:group_context) do
+ parent.is_a?(Group)
+ end
+ end
+
+ def project_context?(parent)
+ strong_memoize(:project_context) do
+ parent.is_a?(Project)
+ end
+ end
+
+ def references_in(text, pattern = Milestone.reference_pattern)
+ # We'll handle here the references that follow the `reference_pattern`.
+ # Other patterns (for example, the link pattern) are handled by the
+ # default implementation.
+ return super(text, pattern) if pattern != Milestone.reference_pattern
+
+ milestones = {}
+ unescaped_html = unescape_html_entities(text).gsub(pattern) do |match|
+ milestone = find_milestone($~[:project], $~[:namespace], $~[:milestone_iid], $~[:milestone_name])
+
+ if milestone
+ milestones[milestone.id] = yield match, milestone.id, $~[:project], $~[:namespace], $~
+ "#{REFERENCE_PLACEHOLDER}#{milestone.id}"
+ else
+ match
+ end
+ end
+
+ return text if milestones.empty?
+
+ escape_with_placeholders(unescaped_html, milestones)
+ end
+
+ def find_milestone(project_ref, namespace_ref, milestone_id, milestone_name)
+ project_path = full_project_path(namespace_ref, project_ref)
+
+ # Returns group if project is not found by path
+ parent = parent_from_ref(project_path)
+
+ return unless parent
+
+ milestone_params = milestone_params(milestone_id, milestone_name)
+
+ find_milestone_with_finder(parent, milestone_params)
+ end
+
+ def milestone_params(iid, name)
+ if name
+ { name: name.tr('"', '') }
+ else
+ { iid: iid.to_i }
+ end
+ end
+
+ def find_milestone_with_finder(parent, params)
+ finder_params = milestone_finder_params(parent, params[:iid].present?)
+
+ MilestonesFinder.new(finder_params).find_by(params)
+ end
+
+ def milestone_finder_params(parent, find_by_iid)
+ { order: nil, state: 'all' }.tap do |params|
+ params[:project_ids] = parent.id if project_context?(parent)
+
+ # We don't support IID lookups because IIDs can clash between
+ # group/project milestones and group/subgroup milestones.
+ params[:group_ids] = self_and_ancestors_ids(parent) unless find_by_iid
+ end
+ end
+
+ def self_and_ancestors_ids(parent)
+ if group_context?(parent)
+ parent.self_and_ancestors.select(:id)
+ elsif project_context?(parent)
+ parent.group&.self_and_ancestors&.select(:id)
+ end
+ end
+
+ def url_for_object(milestone, project)
+ Gitlab::Routing
+ .url_helpers
+ .milestone_url(milestone, only_path: context[:only_path])
+ end
+
+ def object_link_text(object, matches)
+ milestone_link = escape_once(super)
+ reference = object.project&.to_reference_base(project)
+
+ if reference.present?
+ "#{milestone_link} <i>in #{reference}</i>".html_safe
+ else
+ milestone_link
+ end
+ end
+
+ def object_link_title(object, matches)
+ nil
+ end
+ end
+ end
+ end
+end
diff --git a/lib/banzai/filter/references/project_reference_filter.rb b/lib/banzai/filter/references/project_reference_filter.rb
new file mode 100644
index 00000000000..522c6e0f5f3
--- /dev/null
+++ b/lib/banzai/filter/references/project_reference_filter.rb
@@ -0,0 +1,119 @@
+# frozen_string_literal: true
+
+module Banzai
+ module Filter
+ module References
+ # HTML filter that replaces project references with links.
+ class ProjectReferenceFilter < ReferenceFilter
+ self.reference_type = :project
+
+ # Public: Find `namespace/project>` project references in text
+ #
+ # ProjectReferenceFilter.references_in(text) do |match, project|
+ # "<a href=...>#{project}></a>"
+ # end
+ #
+ # text - String text to search.
+ #
+ # Yields the String match, and the String project name.
+ #
+ # Returns a String replaced with the return of the block.
+ def self.references_in(text)
+ text.gsub(Project.markdown_reference_pattern) do |match|
+ yield match, "#{$~[:namespace]}/#{$~[:project]}"
+ end
+ end
+
+ def call
+ ref_pattern = Project.markdown_reference_pattern
+ ref_pattern_start = /\A#{ref_pattern}\z/
+
+ nodes.each_with_index do |node, index|
+ if text_node?(node)
+ replace_text_when_pattern_matches(node, index, ref_pattern) do |content|
+ project_link_filter(content)
+ end
+ elsif element_node?(node)
+ yield_valid_link(node) do |link, inner_html|
+ if link =~ ref_pattern_start
+ replace_link_node_with_href(node, index, link) do
+ project_link_filter(link, link_content: inner_html)
+ end
+ end
+ end
+ end
+ end
+
+ doc
+ end
+
+ # Replace `namespace/project>` project references in text with links to the referenced
+ # project page.
+ #
+ # text - String text to replace references in.
+ # link_content - Original content of the link being replaced.
+ #
+ # Returns a String with `namespace/project>` references replaced with links. All links
+ # have `gfm` and `gfm-project` class names attached for styling.
+ def project_link_filter(text, link_content: nil)
+ self.class.references_in(text) do |match, project_path|
+ cached_call(:banzai_url_for_object, match, path: [Project, project_path.downcase]) do
+ if project = projects_hash[project_path.downcase]
+ link_to_project(project, link_content: link_content) || match
+ else
+ match
+ end
+ end
+ end
+ end
+
+ # Returns a Hash containing all Project objects for the project
+ # references in the current document.
+ #
+ # The keys of this Hash are the project paths, the values the
+ # corresponding Project objects.
+ def projects_hash
+ @projects ||= Project.eager_load(:route, namespace: [:route])
+ .where_full_path_in(projects)
+ .index_by(&:full_path)
+ .transform_keys(&:downcase)
+ end
+
+ # Returns all projects referenced in the current document.
+ def projects
+ refs = Set.new
+
+ nodes.each do |node|
+ node.to_html.scan(Project.markdown_reference_pattern) do
+ refs << "#{$~[:namespace]}/#{$~[:project]}"
+ end
+ end
+
+ refs.to_a
+ end
+
+ private
+
+ def urls
+ Gitlab::Routing.url_helpers
+ end
+
+ def link_class
+ reference_class(:project)
+ end
+
+ def link_to_project(project, link_content: nil)
+ url = urls.project_url(project, only_path: context[:only_path])
+ data = data_attribute(project: project.id)
+ content = link_content || project.to_reference
+
+ link_tag(url, data, content, project.name)
+ end
+
+ def link_tag(url, data, link_content, title)
+ %(<a href="#{url}" #{data} class="#{link_class}" title="#{escape_once(title)}">#{link_content}</a>)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/banzai/filter/references/reference_filter.rb b/lib/banzai/filter/references/reference_filter.rb
new file mode 100644
index 00000000000..dd15c43f5d8
--- /dev/null
+++ b/lib/banzai/filter/references/reference_filter.rb
@@ -0,0 +1,217 @@
+# frozen_string_literal: true
+
+# Generated HTML is transformed back to GFM by app/assets/javascripts/behaviors/markdown/nodes/reference.js
+module Banzai
+ module Filter
+ module References
+ # Base class for GitLab Flavored Markdown reference filters.
+ #
+ # References within <pre>, <code>, <a>, and <style> elements are ignored.
+ #
+ # Context options:
+ # :project (required) - Current project, ignored if reference is cross-project.
+ # :only_path - Generate path-only links.
+ class ReferenceFilter < HTML::Pipeline::Filter
+ include RequestStoreReferenceCache
+ include OutputSafety
+
+ class << self
+ attr_accessor :reference_type
+
+ def call(doc, context = nil, result = nil)
+ new(doc, context, result).call_and_update_nodes
+ end
+ end
+
+ def initialize(doc, context = nil, result = nil)
+ super
+
+ @new_nodes = {}
+ @nodes = self.result[:reference_filter_nodes]
+ end
+
+ def call_and_update_nodes
+ with_update_nodes { call }
+ end
+
+ # Returns a data attribute String to attach to a reference link
+ #
+ # attributes - Hash, where the key becomes the data attribute name and the
+ # value is the data attribute value
+ #
+ # Examples:
+ #
+ # data_attribute(project: 1, issue: 2)
+ # # => "data-reference-type=\"SomeReferenceFilter\" data-project=\"1\" data-issue=\"2\""
+ #
+ # data_attribute(project: 3, merge_request: 4)
+ # # => "data-reference-type=\"SomeReferenceFilter\" data-project=\"3\" data-merge-request=\"4\""
+ #
+ # Returns a String
+ def data_attribute(attributes = {})
+ attributes = attributes.reject { |_, v| v.nil? }
+
+ attributes[:reference_type] ||= self.class.reference_type
+ attributes[:container] ||= 'body'
+ attributes[:placement] ||= 'top'
+ attributes.delete(:original) if context[:no_original_data]
+ attributes.map do |key, value|
+ %Q(data-#{key.to_s.dasherize}="#{escape_once(value)}")
+ end.join(' ')
+ end
+
+ def ignore_ancestor_query
+ @ignore_ancestor_query ||= begin
+ parents = %w(pre code a style)
+ parents << 'blockquote' if context[:ignore_blockquotes]
+
+ parents.map { |n| "ancestor::#{n}" }.join(' or ')
+ end
+ end
+
+ def project
+ context[:project]
+ end
+
+ def group
+ context[:group]
+ end
+
+ def user
+ context[:user]
+ end
+
+ def skip_project_check?
+ context[:skip_project_check]
+ end
+
+ def reference_class(type, tooltip: true)
+ gfm_klass = "gfm gfm-#{type}"
+
+ return gfm_klass unless tooltip
+
+ "#{gfm_klass} has-tooltip"
+ end
+
+ # Ensure that a :project key exists in context
+ #
+ # Note that while the key might exist, its value could be nil!
+ def validate
+ needs :project unless skip_project_check?
+ end
+
+ # Iterates over all <a> and text() nodes in a document.
+ #
+ # Nodes are skipped whenever their ancestor is one of the nodes returned
+ # by `ignore_ancestor_query`. Link tags are not processed if they have a
+ # "gfm" class or the "href" attribute is empty.
+ def each_node
+ return to_enum(__method__) unless block_given?
+
+ doc.xpath(query).each do |node|
+ yield node
+ end
+ end
+
+ # Returns an Array containing all HTML nodes.
+ def nodes
+ @nodes ||= each_node.to_a
+ end
+
+ # Yields the link's URL and inner HTML whenever the node is a valid <a> tag.
+ def yield_valid_link(node)
+ link = unescape_link(node.attr('href').to_s)
+ inner_html = node.inner_html
+
+ return unless link.force_encoding('UTF-8').valid_encoding?
+
+ yield link, inner_html
+ end
+
+ def unescape_link(href)
+ CGI.unescape(href)
+ end
+
+ def replace_text_when_pattern_matches(node, index, pattern)
+ return unless node.text =~ pattern
+
+ content = node.to_html
+ html = yield content
+
+ replace_text_with_html(node, index, html) unless html == content
+ end
+
+ def replace_link_node_with_text(node, index)
+ html = yield
+
+ replace_text_with_html(node, index, html) unless html == node.text
+ end
+
+ def replace_link_node_with_href(node, index, link)
+ html = yield
+
+ replace_text_with_html(node, index, html) unless html == link
+ end
+
+ def text_node?(node)
+ node.is_a?(Nokogiri::XML::Text)
+ end
+
+ def element_node?(node)
+ node.is_a?(Nokogiri::XML::Element)
+ end
+
+ private
+
+ def query
+ @query ||= %Q{descendant-or-self::text()[not(#{ignore_ancestor_query})]
+ | descendant-or-self::a[
+ not(contains(concat(" ", @class, " "), " gfm ")) and not(@href = "")
+ ]}
+ end
+
+ def replace_text_with_html(node, index, html)
+ replace_and_update_new_nodes(node, index, html)
+ end
+
+ def replace_and_update_new_nodes(node, index, html)
+ previous_node = node.previous
+ next_node = node.next
+ parent_node = node.parent
+ # Unfortunately node.replace(html) returns re-parented nodes, not the actual replaced nodes in the doc
+ # We need to find the actual nodes in the doc that were replaced
+ node.replace(html)
+ @new_nodes[index] = []
+
+ # We replaced node with new nodes, so we find first new node. If previous_node is nil, we take first parent child
+ new_node = previous_node ? previous_node.next : parent_node&.children&.first
+
+ # We iterate from first to last replaced node and store replaced nodes in @new_nodes
+ while new_node && new_node != next_node
+ @new_nodes[index] << new_node.xpath(query)
+ new_node = new_node.next
+ end
+
+ @new_nodes[index].flatten!
+ end
+
+ def only_path?
+ context[:only_path]
+ end
+
+ def with_update_nodes
+ @new_nodes = {}
+ yield.tap { update_nodes! }
+ end
+
+ # Once Filter completes replacing nodes, we update nodes with @new_nodes
+ def update_nodes!
+ @new_nodes.sort_by { |index, _new_nodes| -index }.each do |index, new_nodes|
+ nodes[index, 1] = new_nodes
+ end
+ result[:reference_filter_nodes] = nodes
+ end
+ end
+ end
+ end
+end
diff --git a/lib/banzai/filter/references/snippet_reference_filter.rb b/lib/banzai/filter/references/snippet_reference_filter.rb
new file mode 100644
index 00000000000..bf7e0f78609
--- /dev/null
+++ b/lib/banzai/filter/references/snippet_reference_filter.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+module Banzai
+ module Filter
+ module References
+ # HTML filter that replaces snippet references with links. References to
+ # snippets that do not exist are ignored.
+ #
+ # This filter supports cross-project references.
+ class SnippetReferenceFilter < AbstractReferenceFilter
+ self.reference_type = :snippet
+
+ def self.object_class
+ Snippet
+ end
+
+ def find_object(project, id)
+ return unless project.is_a?(Project)
+
+ project.snippets.find_by(id: id)
+ end
+
+ def url_for_object(snippet, project)
+ h = Gitlab::Routing.url_helpers
+ h.project_snippet_url(project, snippet,
+ only_path: context[:only_path])
+ end
+ end
+ end
+ end
+end
diff --git a/lib/banzai/filter/references/user_reference_filter.rb b/lib/banzai/filter/references/user_reference_filter.rb
new file mode 100644
index 00000000000..04665973f51
--- /dev/null
+++ b/lib/banzai/filter/references/user_reference_filter.rb
@@ -0,0 +1,182 @@
+# frozen_string_literal: true
+
+module Banzai
+ module Filter
+ module References
+ # HTML filter that replaces user or group references with links.
+ #
+ # A special `@all` reference is also supported.
+ class UserReferenceFilter < ReferenceFilter
+ self.reference_type = :user
+
+ # Public: Find `@user` user references in text
+ #
+ # UserReferenceFilter.references_in(text) do |match, username|
+ # "<a href=...>@#{user}</a>"
+ # end
+ #
+ # text - String text to search.
+ #
+ # Yields the String match, and the String user name.
+ #
+ # Returns a String replaced with the return of the block.
+ def self.references_in(text)
+ text.gsub(User.reference_pattern) do |match|
+ yield match, $~[:user]
+ end
+ end
+
+ def call
+ return doc if project.nil? && group.nil? && !skip_project_check?
+
+ ref_pattern = User.reference_pattern
+ ref_pattern_start = /\A#{ref_pattern}\z/
+
+ nodes.each_with_index do |node, index|
+ if text_node?(node)
+ replace_text_when_pattern_matches(node, index, ref_pattern) do |content|
+ user_link_filter(content)
+ end
+ elsif element_node?(node)
+ yield_valid_link(node) do |link, inner_html|
+ if link =~ ref_pattern_start
+ replace_link_node_with_href(node, index, link) do
+ user_link_filter(link, link_content: inner_html)
+ end
+ end
+ end
+ end
+ end
+
+ doc
+ end
+
+ # Replace `@user` user references in text with links to the referenced
+ # user's profile page.
+ #
+ # text - String text to replace references in.
+ # link_content - Original content of the link being replaced.
+ #
+ # Returns a String with `@user` references replaced with links. All links
+ # have `gfm` and `gfm-project_member` class names attached for styling.
+ def user_link_filter(text, link_content: nil)
+ self.class.references_in(text) do |match, username|
+ if username == 'all' && !skip_project_check?
+ link_to_all(link_content: link_content)
+ else
+ cached_call(:banzai_url_for_object, match, path: [User, username.downcase]) do
+ if namespace = namespaces[username.downcase]
+ link_to_namespace(namespace, link_content: link_content) || match
+ else
+ match
+ end
+ end
+ end
+ end
+ end
+
+ # Returns a Hash containing all Namespace objects for the username
+ # references in the current document.
+ #
+ # The keys of this Hash are the namespace paths, the values the
+ # corresponding Namespace objects.
+ def namespaces
+ @namespaces ||= Namespace.eager_load(:owner, :route)
+ .where_full_path_in(usernames)
+ .index_by(&:full_path)
+ .transform_keys(&:downcase)
+ end
+
+ # Returns all usernames referenced in the current document.
+ def usernames
+ refs = Set.new
+
+ nodes.each do |node|
+ node.to_html.scan(User.reference_pattern) do
+ refs << $~[:user]
+ end
+ end
+
+ refs.to_a
+ end
+
+ private
+
+ def urls
+ Gitlab::Routing.url_helpers
+ end
+
+ def link_class
+ [reference_class(:project_member, tooltip: false), "js-user-link"].join(" ")
+ end
+
+ def link_to_all(link_content: nil)
+ author = context[:author]
+
+ if author && !team_member?(author)
+ link_content
+ else
+ parent_url(link_content, author)
+ end
+ end
+
+ def link_to_namespace(namespace, link_content: nil)
+ if namespace.is_a?(Group)
+ link_to_group(namespace.full_path, namespace, link_content: link_content)
+ else
+ link_to_user(namespace.path, namespace, link_content: link_content)
+ end
+ end
+
+ def link_to_group(group, namespace, link_content: nil)
+ url = urls.group_url(group, only_path: context[:only_path])
+ data = data_attribute(group: namespace.id)
+ content = link_content || Group.reference_prefix + group
+
+ link_tag(url, data, content, namespace.full_name)
+ end
+
+ def link_to_user(user, namespace, link_content: nil)
+ url = urls.user_url(user, only_path: context[:only_path])
+ data = data_attribute(user: namespace.owner_id)
+ content = link_content || User.reference_prefix + user
+
+ link_tag(url, data, content, namespace.owner_name)
+ end
+
+ def link_tag(url, data, link_content, title)
+ %(<a href="#{url}" #{data} class="#{link_class}" title="#{escape_once(title)}">#{link_content}</a>)
+ end
+
+ def parent
+ context[:project] || context[:group]
+ end
+
+ def parent_group?
+ parent.is_a?(Group)
+ end
+
+ def team_member?(user)
+ if parent_group?
+ parent.member?(user)
+ else
+ parent.team.member?(user)
+ end
+ end
+
+ def parent_url(link_content, author)
+ if parent_group?
+ url = urls.group_url(parent, only_path: context[:only_path])
+ data = data_attribute(group: group.id, author: author.try(:id))
+ else
+ url = urls.project_url(parent, only_path: context[:only_path])
+ data = data_attribute(project: project.id, author: author.try(:id))
+ end
+
+ content = link_content || User.reference_prefix + 'all'
+ link_tag(url, data, content, 'All Project and Group Members')
+ end
+ end
+ end
+ end
+end
diff --git a/lib/banzai/filter/references/vulnerability_reference_filter.rb b/lib/banzai/filter/references/vulnerability_reference_filter.rb
new file mode 100644
index 00000000000..e5f2408eda4
--- /dev/null
+++ b/lib/banzai/filter/references/vulnerability_reference_filter.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+module Banzai
+ module Filter
+ module References
+ # The actual filter is implemented in the EE mixin
+ class VulnerabilityReferenceFilter < IssuableReferenceFilter
+ self.reference_type = :vulnerability
+
+ def self.object_class
+ Vulnerability
+ end
+
+ private
+
+ def project
+ context[:project]
+ end
+ end
+ end
+ end
+end
+
+Banzai::Filter::References::VulnerabilityReferenceFilter.prepend_if_ee('EE::Banzai::Filter::References::VulnerabilityReferenceFilter')
diff --git a/lib/banzai/filter/repository_link_filter.rb b/lib/banzai/filter/repository_link_filter.rb
index 66b9aac3e7e..04bbcabd93f 100644
--- a/lib/banzai/filter/repository_link_filter.rb
+++ b/lib/banzai/filter/repository_link_filter.rb
@@ -60,7 +60,7 @@ module Banzai
def get_uri_types(paths)
return {} if paths.empty?
- uri_types = Hash[paths.collect { |name| [name, nil] }]
+ uri_types = paths.to_h { |name| [name, nil] }
get_blob_types(paths).each do |name, type|
if type == :blob
diff --git a/lib/banzai/filter/snippet_reference_filter.rb b/lib/banzai/filter/snippet_reference_filter.rb
deleted file mode 100644
index f4b6edb6174..00000000000
--- a/lib/banzai/filter/snippet_reference_filter.rb
+++ /dev/null
@@ -1,29 +0,0 @@
-# frozen_string_literal: true
-
-module Banzai
- module Filter
- # HTML filter that replaces snippet references with links. References to
- # snippets that do not exist are ignored.
- #
- # This filter supports cross-project references.
- class SnippetReferenceFilter < AbstractReferenceFilter
- self.reference_type = :snippet
-
- def self.object_class
- Snippet
- end
-
- def find_object(project, id)
- return unless project.is_a?(Project)
-
- project.snippets.find_by(id: id)
- end
-
- def url_for_object(snippet, project)
- h = Gitlab::Routing.url_helpers
- h.project_snippet_url(project, snippet,
- only_path: context[:only_path])
- end
- end
- end
-end
diff --git a/lib/banzai/filter/spaced_link_filter.rb b/lib/banzai/filter/spaced_link_filter.rb
index f52ffe117d9..ca26e6d1581 100644
--- a/lib/banzai/filter/spaced_link_filter.rb
+++ b/lib/banzai/filter/spaced_link_filter.rb
@@ -42,7 +42,7 @@ module Banzai
TEXT_QUERY = %Q(descendant-or-self::text()[
not(#{IGNORE_PARENTS.map { |p| "ancestor::#{p}" }.join(' or ')})
and contains(., ']\(')
- ]).freeze
+ ])
def call
doc.xpath(TEXT_QUERY).each do |node|
diff --git a/lib/banzai/filter/suggestion_filter.rb b/lib/banzai/filter/suggestion_filter.rb
index ae093580001..56a14ec0737 100644
--- a/lib/banzai/filter/suggestion_filter.rb
+++ b/lib/banzai/filter/suggestion_filter.rb
@@ -10,7 +10,7 @@ module Banzai
def call
return doc unless suggestions_filter_enabled?
- doc.search('pre.suggestion > code').each do |node|
+ doc.search('pre.language-suggestion > code').each do |node|
node.add_class(TAG_CLASS)
end
diff --git a/lib/banzai/filter/syntax_highlight_filter.rb b/lib/banzai/filter/syntax_highlight_filter.rb
index 1d3bbe43344..731a2bb4c77 100644
--- a/lib/banzai/filter/syntax_highlight_filter.rb
+++ b/lib/banzai/filter/syntax_highlight_filter.rb
@@ -37,7 +37,7 @@ module Banzai
begin
code = Rouge::Formatters::HTMLGitlab.format(lex(lexer, node.text), tag: language)
- css_classes << " #{language}" if language
+ css_classes << " language-#{language}" if language
rescue
# Gracefully handle syntax highlighter bugs/errors to ensure users can
# still access an issue/comment/etc. First, retry with the plain text
diff --git a/lib/banzai/filter/user_reference_filter.rb b/lib/banzai/filter/user_reference_filter.rb
deleted file mode 100644
index 262385524f4..00000000000
--- a/lib/banzai/filter/user_reference_filter.rb
+++ /dev/null
@@ -1,180 +0,0 @@
-# frozen_string_literal: true
-
-module Banzai
- module Filter
- # HTML filter that replaces user or group references with links.
- #
- # A special `@all` reference is also supported.
- class UserReferenceFilter < ReferenceFilter
- self.reference_type = :user
-
- # Public: Find `@user` user references in text
- #
- # UserReferenceFilter.references_in(text) do |match, username|
- # "<a href=...>@#{user}</a>"
- # end
- #
- # text - String text to search.
- #
- # Yields the String match, and the String user name.
- #
- # Returns a String replaced with the return of the block.
- def self.references_in(text)
- text.gsub(User.reference_pattern) do |match|
- yield match, $~[:user]
- end
- end
-
- def call
- return doc if project.nil? && group.nil? && !skip_project_check?
-
- ref_pattern = User.reference_pattern
- ref_pattern_start = /\A#{ref_pattern}\z/
-
- nodes.each_with_index do |node, index|
- if text_node?(node)
- replace_text_when_pattern_matches(node, index, ref_pattern) do |content|
- user_link_filter(content)
- end
- elsif element_node?(node)
- yield_valid_link(node) do |link, inner_html|
- if link =~ ref_pattern_start
- replace_link_node_with_href(node, index, link) do
- user_link_filter(link, link_content: inner_html)
- end
- end
- end
- end
- end
-
- doc
- end
-
- # Replace `@user` user references in text with links to the referenced
- # user's profile page.
- #
- # text - String text to replace references in.
- # link_content - Original content of the link being replaced.
- #
- # Returns a String with `@user` references replaced with links. All links
- # have `gfm` and `gfm-project_member` class names attached for styling.
- def user_link_filter(text, link_content: nil)
- self.class.references_in(text) do |match, username|
- if username == 'all' && !skip_project_check?
- link_to_all(link_content: link_content)
- else
- cached_call(:banzai_url_for_object, match, path: [User, username.downcase]) do
- if namespace = namespaces[username.downcase]
- link_to_namespace(namespace, link_content: link_content) || match
- else
- match
- end
- end
- end
- end
- end
-
- # Returns a Hash containing all Namespace objects for the username
- # references in the current document.
- #
- # The keys of this Hash are the namespace paths, the values the
- # corresponding Namespace objects.
- def namespaces
- @namespaces ||= Namespace.eager_load(:owner, :route)
- .where_full_path_in(usernames)
- .index_by(&:full_path)
- .transform_keys(&:downcase)
- end
-
- # Returns all usernames referenced in the current document.
- def usernames
- refs = Set.new
-
- nodes.each do |node|
- node.to_html.scan(User.reference_pattern) do
- refs << $~[:user]
- end
- end
-
- refs.to_a
- end
-
- private
-
- def urls
- Gitlab::Routing.url_helpers
- end
-
- def link_class
- [reference_class(:project_member, tooltip: false), "js-user-link"].join(" ")
- end
-
- def link_to_all(link_content: nil)
- author = context[:author]
-
- if author && !team_member?(author)
- link_content
- else
- parent_url(link_content, author)
- end
- end
-
- def link_to_namespace(namespace, link_content: nil)
- if namespace.is_a?(Group)
- link_to_group(namespace.full_path, namespace, link_content: link_content)
- else
- link_to_user(namespace.path, namespace, link_content: link_content)
- end
- end
-
- def link_to_group(group, namespace, link_content: nil)
- url = urls.group_url(group, only_path: context[:only_path])
- data = data_attribute(group: namespace.id)
- content = link_content || Group.reference_prefix + group
-
- link_tag(url, data, content, namespace.full_name)
- end
-
- def link_to_user(user, namespace, link_content: nil)
- url = urls.user_url(user, only_path: context[:only_path])
- data = data_attribute(user: namespace.owner_id)
- content = link_content || User.reference_prefix + user
-
- link_tag(url, data, content, namespace.owner_name)
- end
-
- def link_tag(url, data, link_content, title)
- %(<a href="#{url}" #{data} class="#{link_class}" title="#{escape_once(title)}">#{link_content}</a>)
- end
-
- def parent
- context[:project] || context[:group]
- end
-
- def parent_group?
- parent.is_a?(Group)
- end
-
- def team_member?(user)
- if parent_group?
- parent.member?(user)
- else
- parent.team.member?(user)
- end
- end
-
- def parent_url(link_content, author)
- if parent_group?
- url = urls.group_url(parent, only_path: context[:only_path])
- data = data_attribute(group: group.id, author: author.try(:id))
- else
- url = urls.project_url(parent, only_path: context[:only_path])
- data = data_attribute(project: project.id, author: author.try(:id))
- end
-
- content = link_content || User.reference_prefix + 'all'
- link_tag(url, data, content, 'All Project and Group Members')
- end
- end
- end
-end
diff --git a/lib/banzai/filter/vulnerability_reference_filter.rb b/lib/banzai/filter/vulnerability_reference_filter.rb
deleted file mode 100644
index a59e9836d69..00000000000
--- a/lib/banzai/filter/vulnerability_reference_filter.rb
+++ /dev/null
@@ -1,22 +0,0 @@
-# frozen_string_literal: true
-
-module Banzai
- module Filter
- # The actual filter is implemented in the EE mixin
- class VulnerabilityReferenceFilter < IssuableReferenceFilter
- self.reference_type = :vulnerability
-
- def self.object_class
- Vulnerability
- end
-
- private
-
- def project
- context[:project]
- end
- end
- end
-end
-
-Banzai::Filter::VulnerabilityReferenceFilter.prepend_if_ee('EE::Banzai::Filter::VulnerabilityReferenceFilter')
diff --git a/lib/banzai/filter/wiki_link_filter/rewriter.rb b/lib/banzai/filter/wiki_link_filter/rewriter.rb
index f4cc8beeb52..b4c2e7efae3 100644
--- a/lib/banzai/filter/wiki_link_filter/rewriter.rb
+++ b/lib/banzai/filter/wiki_link_filter/rewriter.rb
@@ -6,7 +6,7 @@ module Banzai
class Rewriter
def initialize(link_string, wiki:, slug:)
@uri = Addressable::URI.parse(link_string)
- @wiki_base_path = wiki && wiki.wiki_base_path
+ @wiki_base_path = wiki&.wiki_base_path
@slug = slug
end
@@ -41,7 +41,8 @@ module Banzai
# Any link _not_ of the form `http://example.com/`
def apply_relative_link_rules!
if @uri.relative? && @uri.path.present?
- link = ::File.join(@wiki_base_path, @uri.path)
+ link = @uri.path
+ link = ::File.join(@wiki_base_path, link) unless link.starts_with?(@wiki_base_path)
link = "#{link}##{@uri.fragment}" if @uri.fragment
@uri = Addressable::URI.parse(link)
end
diff --git a/lib/banzai/pipeline/gfm_pipeline.rb b/lib/banzai/pipeline/gfm_pipeline.rb
index e5ec0a0a006..028e3c44dc3 100644
--- a/lib/banzai/pipeline/gfm_pipeline.rb
+++ b/lib/banzai/pipeline/gfm_pipeline.rb
@@ -51,19 +51,19 @@ module Banzai
def self.reference_filters
[
- Filter::UserReferenceFilter,
- Filter::ProjectReferenceFilter,
- Filter::DesignReferenceFilter,
- Filter::IssueReferenceFilter,
- Filter::ExternalIssueReferenceFilter,
- Filter::MergeRequestReferenceFilter,
- Filter::SnippetReferenceFilter,
- Filter::CommitRangeReferenceFilter,
- Filter::CommitReferenceFilter,
- Filter::LabelReferenceFilter,
- Filter::MilestoneReferenceFilter,
- Filter::AlertReferenceFilter,
- Filter::FeatureFlagReferenceFilter
+ Filter::References::UserReferenceFilter,
+ Filter::References::ProjectReferenceFilter,
+ Filter::References::DesignReferenceFilter,
+ Filter::References::IssueReferenceFilter,
+ Filter::References::ExternalIssueReferenceFilter,
+ Filter::References::MergeRequestReferenceFilter,
+ Filter::References::SnippetReferenceFilter,
+ Filter::References::CommitRangeReferenceFilter,
+ Filter::References::CommitReferenceFilter,
+ Filter::References::LabelReferenceFilter,
+ Filter::References::MilestoneReferenceFilter,
+ Filter::References::AlertReferenceFilter,
+ Filter::References::FeatureFlagReferenceFilter
]
end
diff --git a/lib/banzai/pipeline/label_pipeline.rb b/lib/banzai/pipeline/label_pipeline.rb
index 725cccc4b2b..ccfda2052e6 100644
--- a/lib/banzai/pipeline/label_pipeline.rb
+++ b/lib/banzai/pipeline/label_pipeline.rb
@@ -6,7 +6,7 @@ module Banzai
def self.filters
@filters ||= FilterArray[
Filter::SanitizationFilter,
- Filter::LabelReferenceFilter
+ Filter::References::LabelReferenceFilter
]
end
end
diff --git a/lib/banzai/pipeline/single_line_pipeline.rb b/lib/banzai/pipeline/single_line_pipeline.rb
index 4bf98099662..65a5e28b704 100644
--- a/lib/banzai/pipeline/single_line_pipeline.rb
+++ b/lib/banzai/pipeline/single_line_pipeline.rb
@@ -17,15 +17,15 @@ module Banzai
def self.reference_filters
[
- Filter::UserReferenceFilter,
- Filter::IssueReferenceFilter,
- Filter::ExternalIssueReferenceFilter,
- Filter::MergeRequestReferenceFilter,
- Filter::SnippetReferenceFilter,
- Filter::CommitRangeReferenceFilter,
- Filter::CommitReferenceFilter,
- Filter::AlertReferenceFilter,
- Filter::FeatureFlagReferenceFilter
+ Filter::References::UserReferenceFilter,
+ Filter::References::IssueReferenceFilter,
+ Filter::References::ExternalIssueReferenceFilter,
+ Filter::References::MergeRequestReferenceFilter,
+ Filter::References::SnippetReferenceFilter,
+ Filter::References::CommitRangeReferenceFilter,
+ Filter::References::CommitReferenceFilter,
+ Filter::References::AlertReferenceFilter,
+ Filter::References::FeatureFlagReferenceFilter
]
end
diff --git a/lib/banzai/pipeline/wiki_pipeline.rb b/lib/banzai/pipeline/wiki_pipeline.rb
index 97a03895ff3..caba9570ab9 100644
--- a/lib/banzai/pipeline/wiki_pipeline.rb
+++ b/lib/banzai/pipeline/wiki_pipeline.rb
@@ -5,7 +5,7 @@ module Banzai
class WikiPipeline < FullPipeline
def self.filters
@filters ||= begin
- super.insert_after(Filter::TableOfContentsFilter, Filter::GollumTagsFilter)
+ super.insert_before(Filter::ImageLazyLoadFilter, Filter::GollumTagsFilter)
.insert_before(Filter::TaskListFilter, Filter::WikiLinkFilter)
end
end