Commit 03f9cc03 authored by Nick Thomas's avatar Nick Thomas

Merge branch 'bw-refactor-reference-parser' into 'master'

Refactor banzai reference filter

See merge request gitlab-org/gitlab!59681
parents 27786443 5c31f4f4
...@@ -11,15 +11,13 @@ module EE ...@@ -11,15 +11,13 @@ module EE
module EpicReferenceFilter module EpicReferenceFilter
extend ActiveSupport::Concern extend ActiveSupport::Concern
class_methods do def references_in(text, pattern = object_class.reference_pattern)
def references_in(text, pattern = object_class.reference_pattern) text.gsub(pattern) do |match|
text.gsub(pattern) do |match| symbol = $~[object_sym]
symbol = $~[object_sym] if object_class.reference_valid?(symbol)
if object_class.reference_valid?(symbol) yield match, symbol.to_i, nil, $~[:group], $~
yield match, symbol.to_i, nil, $~[:group], $~ else
else match
match
end
end end
end end
end end
......
...@@ -11,15 +11,13 @@ module EE ...@@ -11,15 +11,13 @@ module EE
module VulnerabilityReferenceFilter module VulnerabilityReferenceFilter
extend ActiveSupport::Concern extend ActiveSupport::Concern
class_methods do def references_in(text, pattern = object_class.reference_pattern)
def references_in(text, pattern = object_class.reference_pattern) text.gsub(pattern) do |match|
text.gsub(pattern) do |match| symbol = $~[object_sym]
symbol = $~[object_sym] if object_class.reference_valid?(symbol)
if object_class.reference_valid?(symbol) yield match, symbol.to_i, $~[:project], $~[:namespace], $~
yield match, symbol.to_i, $~[:project], $~[:namespace], $~ else
else match
match
end
end end
end end
end end
......
...@@ -16,22 +16,9 @@ module Banzai ...@@ -16,22 +16,9 @@ module Banzai
REFERENCE_PLACEHOLDER = "_reference_#{SecureRandom.hex(16)}_" REFERENCE_PLACEHOLDER = "_reference_#{SecureRandom.hex(16)}_"
REFERENCE_PLACEHOLDER_PATTERN = %r{#{REFERENCE_PLACEHOLDER}(\d+)}.freeze 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) # Public: Find references in text (like `!123` for merge requests)
# #
# AnyReferenceFilter.references_in(text) do |match, id, project_ref, matches| # references_in(text) do |match, id, project_ref, matches|
# object = find_object(project_ref, id) # object = find_object(project_ref, id)
# "<a href=...>#{object.to_reference}</a>" # "<a href=...>#{object.to_reference}</a>"
# end # end
...@@ -42,7 +29,7 @@ module Banzai ...@@ -42,7 +29,7 @@ module Banzai
# of the external project reference, and all of the matchdata. # of the external project reference, and all of the matchdata.
# #
# Returns a String replaced with the return of the block. # Returns a String replaced with the return of the block.
def self.references_in(text, pattern = object_class.reference_pattern) def references_in(text, pattern = object_class.reference_pattern)
text.gsub(pattern) do |match| text.gsub(pattern) do |match|
if ident = identifier($~) if ident = identifier($~)
yield match, ident, $~[:project], $~[:namespace], $~ yield match, ident, $~[:project], $~[:namespace], $~
...@@ -52,17 +39,13 @@ module Banzai ...@@ -52,17 +39,13 @@ module Banzai
end end
end end
def self.identifier(match_data) def identifier(match_data)
symbol = symbol_from_match(match_data) symbol = symbol_from_match(match_data)
parse_symbol(symbol, match_data) if object_class.reference_valid?(symbol) parse_symbol(symbol, match_data) if object_class.reference_valid?(symbol)
end end
def identifier(match_data) def symbol_from_match(match)
self.class.identifier(match_data)
end
def self.symbol_from_match(match)
key = object_sym key = object_sym
match[key] if match.names.include?(key.to_s) match[key] if match.names.include?(key.to_s)
end end
...@@ -72,7 +55,7 @@ module Banzai ...@@ -72,7 +55,7 @@ module Banzai
# #
# This method has the contract that if a string `ref` refers to a # This method has the contract that if a string `ref` refers to a
# record `record`, then `parse_symbol(ref) == record_identifier(record)`. # record `record`, then `parse_symbol(ref) == record_identifier(record)`.
def self.parse_symbol(symbol, match_data) def parse_symbol(symbol, match_data)
symbol.to_i symbol.to_i
end end
...@@ -84,21 +67,10 @@ module Banzai ...@@ -84,21 +67,10 @@ module Banzai
record.id record.id
end 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 # Implement in child class
# Example: project.merge_requests.find # Example: project.merge_requests.find
def find_object(parent_object, id) def find_object(parent_object, id)
raise NotImplementedError, "#{self.class} must implement method: #{__callee__}"
end end
# Override if the link reference pattern produces a different ID (global # Override if the link reference pattern produces a different ID (global
...@@ -110,6 +82,7 @@ module Banzai ...@@ -110,6 +82,7 @@ module Banzai
# Implement in child class # Implement in child class
# Example: project_merge_request_url # Example: project_merge_request_url
def url_for_object(object, parent_object) def url_for_object(object, parent_object)
raise NotImplementedError, "#{self.class} must implement method: #{__callee__}"
end end
def find_object_cached(parent_object, id) def find_object_cached(parent_object, id)
...@@ -139,7 +112,7 @@ module Banzai ...@@ -139,7 +112,7 @@ module Banzai
def call def call
return doc unless project || group || user return doc unless project || group || user
ref_pattern = object_class.reference_pattern ref_pattern = object_reference_pattern
link_pattern = object_class.link_reference_pattern link_pattern = object_class.link_reference_pattern
# Compile often used regexps only once outside of the loop # Compile often used regexps only once outside of the loop
...@@ -425,14 +398,6 @@ module Banzai ...@@ -425,14 +398,6 @@ module Banzai
group_ref group_ref
end 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) def escape_with_placeholders(text, placeholder_data)
escaped = escape_html_entities(text) escaped = escape_html_entities(text)
......
...@@ -5,12 +5,9 @@ module Banzai ...@@ -5,12 +5,9 @@ module Banzai
module References module References
class AlertReferenceFilter < IssuableReferenceFilter class AlertReferenceFilter < IssuableReferenceFilter
self.reference_type = :alert self.reference_type = :alert
self.object_class = AlertManagement::Alert
def self.object_class def object_sym
AlertManagement::Alert
end
def self.object_sym
:alert :alert
end end
......
...@@ -8,12 +8,9 @@ module Banzai ...@@ -8,12 +8,9 @@ module Banzai
# This filter supports cross-project references. # This filter supports cross-project references.
class CommitRangeReferenceFilter < AbstractReferenceFilter class CommitRangeReferenceFilter < AbstractReferenceFilter
self.reference_type = :commit_range self.reference_type = :commit_range
self.object_class = CommitRange
def self.object_class def references_in(text, pattern = object_reference_pattern)
CommitRange
end
def self.references_in(text, pattern = CommitRange.reference_pattern)
text.gsub(pattern) do |match| text.gsub(pattern) do |match|
yield match, $~[:commit_range], $~[:project], $~[:namespace], $~ yield match, $~[:commit_range], $~[:project], $~[:namespace], $~
end end
......
...@@ -8,12 +8,9 @@ module Banzai ...@@ -8,12 +8,9 @@ module Banzai
# This filter supports cross-project references. # This filter supports cross-project references.
class CommitReferenceFilter < AbstractReferenceFilter class CommitReferenceFilter < AbstractReferenceFilter
self.reference_type = :commit self.reference_type = :commit
self.object_class = Commit
def self.object_class def references_in(text, pattern = object_reference_pattern)
Commit
end
def self.references_in(text, pattern = Commit.reference_pattern)
text.gsub(pattern) do |match| text.gsub(pattern) do |match|
yield match, $~[:commit], $~[:project], $~[:namespace], $~ yield match, $~[:commit], $~[:project], $~[:namespace], $~
end end
...@@ -39,7 +36,7 @@ module Banzai ...@@ -39,7 +36,7 @@ module Banzai
end end
# The default behaviour is `#to_i` - we just pass the hash through. # The default behaviour is `#to_i` - we just pass the hash through.
def self.parse_symbol(sha_hash, _match) def parse_symbol(sha_hash, _match)
sha_hash sha_hash
end end
......
...@@ -33,6 +33,7 @@ module Banzai ...@@ -33,6 +33,7 @@ module Banzai
end end
self.reference_type = :design self.reference_type = :design
self.object_class = ::DesignManagement::Design
def find_object(project, identifier) def find_object(project, identifier)
records_per_parent[project][identifier] records_per_parent[project][identifier]
...@@ -76,15 +77,11 @@ module Banzai ...@@ -76,15 +77,11 @@ module Banzai
super.merge(issue: design.issue_id) super.merge(issue: design.issue_id)
end end
def self.object_class def object_sym
::DesignManagement::Design
end
def self.object_sym
:design :design
end end
def self.parse_symbol(raw, match_data) def parse_symbol(raw, match_data)
filename = match_data[:url_filename] filename = match_data[:url_filename]
iid = match_data[:issue].to_i iid = match_data[:issue].to_i
Identifier.new(filename: CGI.unescape(filename), issue_iid: iid) Identifier.new(filename: CGI.unescape(filename), issue_iid: iid)
......
...@@ -10,10 +10,11 @@ module Banzai ...@@ -10,10 +10,11 @@ module Banzai
# This filter does not support cross-project references. # This filter does not support cross-project references.
class ExternalIssueReferenceFilter < ReferenceFilter class ExternalIssueReferenceFilter < ReferenceFilter
self.reference_type = :external_issue self.reference_type = :external_issue
self.object_class = ExternalIssue
# Public: Find `JIRA-123` issue references in text # Public: Find `JIRA-123` issue references in text
# #
# ExternalIssueReferenceFilter.references_in(text, pattern) do |match, issue| # references_in(text, pattern) do |match, issue|
# "<a href=...>##{issue}</a>" # "<a href=...>##{issue}</a>"
# end # end
# #
...@@ -22,7 +23,7 @@ module Banzai ...@@ -22,7 +23,7 @@ module Banzai
# Yields the String match and the String issue reference. # Yields the String match and the String issue reference.
# #
# Returns a String replaced with the return of the block. # Returns a String replaced with the return of the block.
def self.references_in(text, pattern) def references_in(text, pattern = object_reference_pattern)
text.gsub(pattern) do |match| text.gsub(pattern) do |match|
yield match, $~[:issue] yield match, $~[:issue]
end end
...@@ -32,27 +33,7 @@ module Banzai ...@@ -32,27 +33,7 @@ module Banzai
# Early return if the project isn't using an external tracker # Early return if the project isn't using an external tracker
return doc if project.nil? || default_issues_tracker? return doc if project.nil? || default_issues_tracker?
ref_pattern = issue_reference_pattern super
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 end
private private
...@@ -65,8 +46,8 @@ module Banzai ...@@ -65,8 +46,8 @@ module Banzai
# #
# Returns a String with `JIRA-123` references replaced with links. All # Returns a String with `JIRA-123` references replaced with links. All
# links have `gfm` and `gfm-issue` class names attached for styling. # links have `gfm` and `gfm-issue` class names attached for styling.
def issue_link_filter(text, link_content: nil) def object_link_filter(text, pattern, link_content: nil, link_reference: false)
self.class.references_in(text, issue_reference_pattern) do |match, id| references_in(text) do |match, id|
url = url_for_issue(id) url = url_for_issue(id)
klass = reference_class(:issue) klass = reference_class(:issue)
data = data_attribute(project: project.id, external_issue: id) data = data_attribute(project: project.id, external_issue: id)
...@@ -97,14 +78,10 @@ module Banzai ...@@ -97,14 +78,10 @@ module Banzai
external_issues_cached(:default_issues_tracker?) external_issues_cached(:default_issues_tracker?)
end end
def issue_reference_pattern def object_reference_pattern
external_issues_cached(:external_issue_reference_pattern) external_issues_cached(:external_issue_reference_pattern)
end end
def project
context[:project]
end
def issue_title def issue_title
"Issue in #{project.external_issue_tracker.title}" "Issue in #{project.external_issue_tracker.title}"
end end
......
...@@ -5,12 +5,9 @@ module Banzai ...@@ -5,12 +5,9 @@ module Banzai
module References module References
class FeatureFlagReferenceFilter < IssuableReferenceFilter class FeatureFlagReferenceFilter < IssuableReferenceFilter
self.reference_type = :feature_flag self.reference_type = :feature_flag
self.object_class = Operations::FeatureFlag
def self.object_class def object_sym
Operations::FeatureFlag
end
def self.object_sym
:feature_flag :feature_flag
end end
......
...@@ -13,10 +13,7 @@ module Banzai ...@@ -13,10 +13,7 @@ module Banzai
# to reference issues from other GitLab projects. # to reference issues from other GitLab projects.
class IssueReferenceFilter < IssuableReferenceFilter class IssueReferenceFilter < IssuableReferenceFilter
self.reference_type = :issue self.reference_type = :issue
self.object_class = Issue
def self.object_class
Issue
end
def url_for_object(issue, project) def url_for_object(issue, project)
return issue_path(issue, project) if only_path? return issue_path(issue, project) if only_path?
......
...@@ -6,10 +6,7 @@ module Banzai ...@@ -6,10 +6,7 @@ module Banzai
# The actual filter is implemented in the EE mixin # The actual filter is implemented in the EE mixin
class IterationReferenceFilter < AbstractReferenceFilter class IterationReferenceFilter < AbstractReferenceFilter
self.reference_type = :iteration self.reference_type = :iteration
self.object_class = Iteration
def self.object_class
Iteration
end
end end
end end
end end
......
...@@ -6,10 +6,7 @@ module Banzai ...@@ -6,10 +6,7 @@ module Banzai
# HTML filter that replaces label references with links. # HTML filter that replaces label references with links.
class LabelReferenceFilter < AbstractReferenceFilter class LabelReferenceFilter < AbstractReferenceFilter
self.reference_type = :label self.reference_type = :label
self.object_class = Label
def self.object_class
Label
end
def find_object(parent_object, id) def find_object(parent_object, id)
find_labels(parent_object).find(id) find_labels(parent_object).find(id)
......
...@@ -9,10 +9,7 @@ module Banzai ...@@ -9,10 +9,7 @@ module Banzai
# This filter supports cross-project references. # This filter supports cross-project references.
class MergeRequestReferenceFilter < IssuableReferenceFilter class MergeRequestReferenceFilter < IssuableReferenceFilter
self.reference_type = :merge_request self.reference_type = :merge_request
self.object_class = MergeRequest
def self.object_class
MergeRequest
end
def url_for_object(mr, project) def url_for_object(mr, project)
h = Gitlab::Routing.url_helpers h = Gitlab::Routing.url_helpers
......
...@@ -8,10 +8,7 @@ module Banzai ...@@ -8,10 +8,7 @@ module Banzai
include Gitlab::Utils::StrongMemoize include Gitlab::Utils::StrongMemoize
self.reference_type = :milestone self.reference_type = :milestone
self.object_class = Milestone
def self.object_class
Milestone
end
# Links to project milestones contain the IID, but when we're handling # Links to project milestones contain the IID, but when we're handling
# 'regular' references, we need to use the global ID to disambiguate # 'regular' references, we need to use the global ID to disambiguate
......
...@@ -6,10 +6,11 @@ module Banzai ...@@ -6,10 +6,11 @@ module Banzai
# HTML filter that replaces project references with links. # HTML filter that replaces project references with links.
class ProjectReferenceFilter < ReferenceFilter class ProjectReferenceFilter < ReferenceFilter
self.reference_type = :project self.reference_type = :project
self.object_class = Project
# Public: Find `namespace/project>` project references in text # Public: Find `namespace/project>` project references in text
# #
# ProjectReferenceFilter.references_in(text) do |match, project| # references_in(text) do |match, project|
# "<a href=...>#{project}></a>" # "<a href=...>#{project}></a>"
# end # end
# #
...@@ -18,33 +19,16 @@ module Banzai ...@@ -18,33 +19,16 @@ module Banzai
# Yields the String match, and the String project name. # Yields the String match, and the String project name.
# #
# Returns a String replaced with the return of the block. # Returns a String replaced with the return of the block.
def self.references_in(text) def references_in(text, pattern = object_reference_pattern)
text.gsub(Project.markdown_reference_pattern) do |match| text.gsub(pattern) do |match|
yield match, "#{$~[:namespace]}/#{$~[:project]}" yield match, "#{$~[:namespace]}/#{$~[:project]}"
end end
end end
def call private
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 def object_reference_pattern
@object_reference_pattern ||= Project.markdown_reference_pattern
end end
# Replace `namespace/project>` project references in text with links to the referenced # Replace `namespace/project>` project references in text with links to the referenced
...@@ -55,8 +39,8 @@ module Banzai ...@@ -55,8 +39,8 @@ module Banzai
# #
# Returns a String with `namespace/project>` references replaced with links. All links # Returns a String with `namespace/project>` references replaced with links. All links
# have `gfm` and `gfm-project` class names attached for styling. # have `gfm` and `gfm-project` class names attached for styling.
def project_link_filter(text, link_content: nil) def object_link_filter(text, pattern, link_content: nil, link_reference: false)
self.class.references_in(text) do |match, project_path| references_in(text) do |match, project_path|
cached_call(:banzai_url_for_object, match, path: [Project, project_path.downcase]) do cached_call(:banzai_url_for_object, match, path: [Project, project_path.downcase]) do
if project = projects_hash[project_path.downcase] if project = projects_hash[project_path.downcase]
link_to_project(project, link_content: link_content) || match link_to_project(project, link_content: link_content) || match
...@@ -92,8 +76,6 @@ module Banzai ...@@ -92,8 +76,6 @@ module Banzai
refs.to_a refs.to_a
end end
private
def urls def urls
Gitlab::Routing.url_helpers Gitlab::Routing.url_helpers
end end
......
...@@ -16,8 +16,14 @@ module Banzai ...@@ -16,8 +16,14 @@ module Banzai
include OutputSafety include OutputSafety
class << self class << self
# Implement in child class
# Example: self.reference_type = :merge_request
attr_accessor :reference_type attr_accessor :reference_type
# Implement in child class
# Example: self.object_class = MergeRequest
attr_accessor :object_class
def call(doc, context = nil, result = nil) def call(doc, context = nil, result = nil)
new(doc, context, result).call_and_update_nodes new(doc, context, result).call_and_update_nodes
end end
...@@ -34,6 +40,65 @@ module Banzai ...@@ -34,6 +40,65 @@ module Banzai
with_update_nodes { call } with_update_nodes { call }
end end
def call
ref_pattern_start = /\A#{object_reference_pattern}\z/
nodes.each_with_index do |node, index|
if text_node?(node)
replace_text_when_pattern_matches(node, index, object_reference_pattern) do |content|
object_link_filter(content, object_reference_pattern)
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
object_link_filter(link, object_reference_pattern, link_content: inner_html)
end
end
end
end
end
doc
end
# Public: Find references in text (like `!123` for merge requests)
#
# 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 references_in(text, pattern = object_reference_pattern)
raise NotImplementedError, "#{self.class} must implement method: #{__callee__}"
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
private
# Returns a data attribute String to attach to a reference link # Returns a data attribute String to attach to a reference link
# #
# attributes - Hash, where the key becomes the data attribute name and the # attributes - Hash, where the key becomes the data attribute name and the
...@@ -69,6 +134,13 @@ module Banzai ...@@ -69,6 +134,13 @@ module Banzai
end end
end 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
def project def project
context[:project] context[:project]
end end
...@@ -93,31 +165,6 @@ module Banzai ...@@ -93,31 +165,6 @@ module Banzai
"#{gfm_klass} has-tooltip" "#{gfm_klass} has-tooltip"
end 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. # Yields the link's URL and inner HTML whenever the node is a valid <a> tag.
def yield_valid_link(node) def yield_valid_link(node)
link = unescape_link(node.attr('href').to_s) link = unescape_link(node.attr('href').to_s)
...@@ -132,6 +179,14 @@ module Banzai ...@@ -132,6 +179,14 @@ module Banzai
CGI.unescape(href) CGI.unescape(href)
end end
def unescape_html_entities(text)
CGI.unescapeHTML(text.to_s)
end
def escape_html_entities(text)
CGI.escapeHTML(text.to_s)
end
def replace_text_when_pattern_matches(node, index, pattern) def replace_text_when_pattern_matches(node, index, pattern)
return unless node.text =~ pattern return unless node.text =~ pattern
...@@ -161,7 +216,25 @@ module Banzai ...@@ -161,7 +216,25 @@ module Banzai
node.is_a?(Nokogiri::XML::Element) node.is_a?(Nokogiri::XML::Element)
end end
private def object_class
self.class.object_class
end
def object_reference_pattern
@object_reference_pattern ||= object_class.reference_pattern
end
def object_name
@object_name ||= object_class.name.underscore
end
def object_sym
@object_sym ||= object_name.to_sym
end
def object_link_filter(text, pattern, link_content: nil, link_reference: false)
raise NotImplementedError, "#{self.class} must implement method: #{__callee__}"
end
def query def query
@query ||= %Q{descendant-or-self::text()[not(#{ignore_ancestor_query})] @query ||= %Q{descendant-or-self::text()[not(#{ignore_ancestor_query})]
......
...@@ -9,10 +9,7 @@ module Banzai ...@@ -9,10 +9,7 @@ module Banzai
# This filter supports cross-project references. # This filter supports cross-project references.
class SnippetReferenceFilter < AbstractReferenceFilter class SnippetReferenceFilter < AbstractReferenceFilter
self.reference_type = :snippet self.reference_type = :snippet
self.object_class = Snippet
def self.object_class
Snippet
end
def find_object(project, id) def find_object(project, id)
return unless project.is_a?(Project) return unless project.is_a?(Project)
......
...@@ -8,10 +8,11 @@ module Banzai ...@@ -8,10 +8,11 @@ module Banzai
# A special `@all` reference is also supported. # A special `@all` reference is also supported.
class UserReferenceFilter < ReferenceFilter class UserReferenceFilter < ReferenceFilter
self.reference_type = :user self.reference_type = :user
self.object_class = User
# Public: Find `@user` user references in text # Public: Find `@user` user references in text
# #
# UserReferenceFilter.references_in(text) do |match, username| # references_in(text) do |match, username|
# "<a href=...>@#{user}</a>" # "<a href=...>@#{user}</a>"
# end # end
# #
...@@ -20,8 +21,8 @@ module Banzai ...@@ -20,8 +21,8 @@ module Banzai
# Yields the String match, and the String user name. # Yields the String match, and the String user name.
# #
# Returns a String replaced with the return of the block. # Returns a String replaced with the return of the block.
def self.references_in(text) def references_in(text, pattern = object_reference_pattern)
text.gsub(User.reference_pattern) do |match| text.gsub(pattern) do |match|
yield match, $~[:user] yield match, $~[:user]
end end
end end
...@@ -29,28 +30,11 @@ module Banzai ...@@ -29,28 +30,11 @@ module Banzai
def call def call
return doc if project.nil? && group.nil? && !skip_project_check? return doc if project.nil? && group.nil? && !skip_project_check?
ref_pattern = User.reference_pattern super
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 end
private
# Replace `@user` user references in text with links to the referenced # Replace `@user` user references in text with links to the referenced
# user's profile page. # user's profile page.
# #
...@@ -59,8 +43,8 @@ module Banzai ...@@ -59,8 +43,8 @@ module Banzai
# #
# Returns a String with `@user` references replaced with links. All links # Returns a String with `@user` references replaced with links. All links
# have `gfm` and `gfm-project_member` class names attached for styling. # have `gfm` and `gfm-project_member` class names attached for styling.
def user_link_filter(text, link_content: nil) def object_link_filter(text, pattern, link_content: nil, link_reference: false)
self.class.references_in(text) do |match, username| references_in(text, pattern) do |match, username|
if username == 'all' && !skip_project_check? if username == 'all' && !skip_project_check?
link_to_all(link_content: link_content) link_to_all(link_content: link_content)
else else
...@@ -100,8 +84,6 @@ module Banzai ...@@ -100,8 +84,6 @@ module Banzai
refs.to_a refs.to_a
end end
private
def urls def urls
Gitlab::Routing.url_helpers Gitlab::Routing.url_helpers
end end
......
...@@ -6,16 +6,7 @@ module Banzai ...@@ -6,16 +6,7 @@ module Banzai
# The actual filter is implemented in the EE mixin # The actual filter is implemented in the EE mixin
class VulnerabilityReferenceFilter < IssuableReferenceFilter class VulnerabilityReferenceFilter < IssuableReferenceFilter
self.reference_type = :vulnerability self.reference_type = :vulnerability
self.object_class = Vulnerability
def self.object_class
Vulnerability
end
private
def project
context[:project]
end
end end
end end
end end
......
...@@ -61,13 +61,13 @@ RSpec.describe Banzai::Filter::References::AbstractReferenceFilter do ...@@ -61,13 +61,13 @@ RSpec.describe Banzai::Filter::References::AbstractReferenceFilter do
.to eq([project]) .to eq([project])
end end
context "when no project with that path exists" do context 'when no project with that path exists' do
it "returns no value" do it 'returns no value' do
expect(filter.find_for_paths(['nonexistent/project'])) expect(filter.find_for_paths(['nonexistent/project']))
.to eq([]) .to eq([])
end end
it "adds the ref to the project refs cache" do it 'adds the ref to the project refs cache' do
project_refs_cache = {} project_refs_cache = {}
allow(filter).to receive(:refs_cache).and_return(project_refs_cache) allow(filter).to receive(:refs_cache).and_return(project_refs_cache)
...@@ -99,4 +99,18 @@ RSpec.describe Banzai::Filter::References::AbstractReferenceFilter do ...@@ -99,4 +99,18 @@ RSpec.describe Banzai::Filter::References::AbstractReferenceFilter do
expect(filter.current_parent_path).to eq(project.full_path) expect(filter.current_parent_path).to eq(project.full_path)
end end
end end
context 'abstract methods' do
describe '#find_object' do
it 'raises NotImplementedError' do
expect { filter.find_object(nil, nil) }.to raise_error(NotImplementedError)
end
end
describe '#url_for_object' do
it 'raises NotImplementedError' do
expect { filter.url_for_object(nil, nil) }.to raise_error(NotImplementedError)
end
end
end
end end
...@@ -104,7 +104,7 @@ RSpec.describe Banzai::Filter::References::DesignReferenceFilter do ...@@ -104,7 +104,7 @@ RSpec.describe Banzai::Filter::References::DesignReferenceFilter do
let(:pattern) { described_class.object_class.link_reference_pattern } let(:pattern) { described_class.object_class.link_reference_pattern }
let(:parsed) do let(:parsed) do
m = pattern.match(url) m = pattern.match(url)
described_class.identifier(m) if m described_class.new('', project: nil).identifier(m) if m
end end
it 'can parse the reference' do it 'can parse the reference' do
...@@ -119,9 +119,11 @@ RSpec.describe Banzai::Filter::References::DesignReferenceFilter do ...@@ -119,9 +119,11 @@ RSpec.describe Banzai::Filter::References::DesignReferenceFilter do
describe 'static properties' do describe 'static properties' do
specify do specify do
expect(described_class).to have_attributes( expect(described_class).to have_attributes(
object_sym: :design, reference_type: :design,
object_class: ::DesignManagement::Design object_class: ::DesignManagement::Design
) )
expect(described_class.new('', project: nil).object_sym).to eq :design
end end
end end
......
...@@ -493,19 +493,19 @@ RSpec.describe Banzai::Filter::References::IssueReferenceFilter do ...@@ -493,19 +493,19 @@ RSpec.describe Banzai::Filter::References::IssueReferenceFilter do
it 'yields valid references' do it 'yields valid references' do
expect do |b| expect do |b|
described_class.references_in(issue.to_reference, &b) described_class.new('', project: nil).references_in(issue.to_reference, &b)
end.to yield_with_args(issue.to_reference, issue.iid, nil, nil, MatchData) end.to yield_with_args(issue.to_reference, issue.iid, nil, nil, MatchData)
end end
it "doesn't yield invalid references" do it "doesn't yield invalid references" do
expect do |b| expect do |b|
described_class.references_in('#0', &b) described_class.new('', project: nil).references_in('#0', &b)
end.not_to yield_control end.not_to yield_control
end end
it "doesn't yield unsupported references" do it "doesn't yield unsupported references" do
expect do |b| expect do |b|
described_class.references_in(merge_request.to_reference, &b) described_class.new('', project: nil).references_in(merge_request.to_reference, &b)
end.not_to yield_control end.not_to yield_control
end end
end end
......
...@@ -85,7 +85,7 @@ RSpec.describe Banzai::Filter::References::ProjectReferenceFilter do ...@@ -85,7 +85,7 @@ RSpec.describe Banzai::Filter::References::ProjectReferenceFilter do
document = Nokogiri::HTML.fragment("<p>#{get_reference(project)}</p>") document = Nokogiri::HTML.fragment("<p>#{get_reference(project)}</p>")
filter = described_class.new(document, project: project) filter = described_class.new(document, project: project)
expect(filter.projects_hash).to eq({ project.full_path => project }) expect(filter.send(:projects_hash)).to eq({ project.full_path => project })
end end
end end
...@@ -94,7 +94,7 @@ RSpec.describe Banzai::Filter::References::ProjectReferenceFilter do ...@@ -94,7 +94,7 @@ RSpec.describe Banzai::Filter::References::ProjectReferenceFilter do
document = Nokogiri::HTML.fragment("<p>#{get_reference(project)}</p>") document = Nokogiri::HTML.fragment("<p>#{get_reference(project)}</p>")
filter = described_class.new(document, project: project) filter = described_class.new(document, project: project)
expect(filter.projects).to eq([project.full_path]) expect(filter.send(:projects)).to eq([project.full_path])
end end
end end
end end
...@@ -155,7 +155,7 @@ RSpec.describe Banzai::Filter::References::ReferenceFilter do ...@@ -155,7 +155,7 @@ RSpec.describe Banzai::Filter::References::ReferenceFilter do
let(:nodes) { [node] } let(:nodes) { [node] }
it 'skips node' do it 'skips node' do
expect { |b| filter.replace_text_when_pattern_matches(filter.nodes[0], 0, ref_pattern, &b) }.not_to yield_control expect { |b| filter.send(:replace_text_when_pattern_matches, filter.nodes[0], 0, ref_pattern, &b) }.not_to yield_control
end end
end end
...@@ -183,12 +183,12 @@ RSpec.describe Banzai::Filter::References::ReferenceFilter do ...@@ -183,12 +183,12 @@ RSpec.describe Banzai::Filter::References::ReferenceFilter do
end end
end end
describe "#call_and_update_nodes" do describe '#call_and_update_nodes' do
include_context 'new nodes' include_context 'new nodes'
let(:document) { Nokogiri::HTML.fragment('<a href="foo">foo</a>') } let(:document) { Nokogiri::HTML.fragment('<a href="foo">foo</a>') }
let(:filter) { described_class.new(document, project: project) } let(:filter) { described_class.new(document, project: project) }
it "updates all new nodes", :aggregate_failures do it 'updates all new nodes', :aggregate_failures do
filter.instance_variable_set('@nodes', nodes) filter.instance_variable_set('@nodes', nodes)
expect(filter).to receive(:call) { filter.instance_variable_set('@new_nodes', new_nodes) } expect(filter).to receive(:call) { filter.instance_variable_set('@new_nodes', new_nodes) }
...@@ -201,14 +201,14 @@ RSpec.describe Banzai::Filter::References::ReferenceFilter do ...@@ -201,14 +201,14 @@ RSpec.describe Banzai::Filter::References::ReferenceFilter do
end end
end end
describe ".call" do describe '.call' do
include_context 'new nodes' include_context 'new nodes'
let(:document) { Nokogiri::HTML.fragment('<a href="foo">foo</a>') } let(:document) { Nokogiri::HTML.fragment('<a href="foo">foo</a>') }
let(:result) { { reference_filter_nodes: nodes } } let(:result) { { reference_filter_nodes: nodes } }
it "updates all nodes", :aggregate_failures do it 'updates all nodes', :aggregate_failures do
expect_next_instance_of(described_class) do |filter| expect_next_instance_of(described_class) do |filter|
expect(filter).to receive(:call_and_update_nodes).and_call_original expect(filter).to receive(:call_and_update_nodes).and_call_original
expect(filter).to receive(:with_update_nodes).and_call_original expect(filter).to receive(:with_update_nodes).and_call_original
...@@ -221,4 +221,21 @@ RSpec.describe Banzai::Filter::References::ReferenceFilter do ...@@ -221,4 +221,21 @@ RSpec.describe Banzai::Filter::References::ReferenceFilter do
expect(result[:reference_filter_nodes]).to eq(expected_nodes) expect(result[:reference_filter_nodes]).to eq(expected_nodes)
end end
end end
context 'abstract methods' do
let(:document) { Nokogiri::HTML.fragment('<a href="foo">foo</a>') }
let(:filter) { described_class.new(document, project: project) }
describe '#references_in' do
it 'raises NotImplementedError' do
expect { filter.references_in('foo', %r{(?<!\w)}) }.to raise_error(NotImplementedError)
end
end
describe '#object_link_filter' do
it 'raises NotImplementedError' do
expect { filter.send(:object_link_filter, 'foo', %r{(?<!\w)}) }.to raise_error(NotImplementedError)
end
end
end
end end
...@@ -189,7 +189,7 @@ RSpec.describe Banzai::Filter::References::UserReferenceFilter do ...@@ -189,7 +189,7 @@ RSpec.describe Banzai::Filter::References::UserReferenceFilter do
filter = described_class.new(document, project: project) filter = described_class.new(document, project: project)
ns = user.namespace ns = user.namespace
expect(filter.namespaces).to eq({ ns.path => ns }) expect(filter.send(:namespaces)).to eq({ ns.path => ns })
end end
end end
...@@ -198,7 +198,7 @@ RSpec.describe Banzai::Filter::References::UserReferenceFilter do ...@@ -198,7 +198,7 @@ RSpec.describe Banzai::Filter::References::UserReferenceFilter do
document = Nokogiri::HTML.fragment("<p>#{get_reference(user)}</p>") document = Nokogiri::HTML.fragment("<p>#{get_reference(user)}</p>")
filter = described_class.new(document, project: project) filter = described_class.new(document, project: project)
expect(filter.usernames).to eq([user.username]) expect(filter.send(:usernames)).to eq([user.username])
end end
end end
end end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment