2015-09-11 14:41:01 +05:30
|
|
|
require 'active_support/core_ext/string/output_safety'
|
2015-12-23 02:04:40 +05:30
|
|
|
require 'banzai'
|
2015-09-11 14:41:01 +05:30
|
|
|
require 'html/pipeline/filter'
|
|
|
|
|
2015-12-23 02:04:40 +05:30
|
|
|
module Banzai
|
|
|
|
module Filter
|
2015-09-11 14:41:01 +05:30
|
|
|
# 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
|
2015-10-24 18:46:33 +05:30
|
|
|
def self.user_can_reference?(user, node, context)
|
|
|
|
if node.has_attribute?('data-project')
|
|
|
|
project_id = node.attr('data-project').to_i
|
|
|
|
return true if project_id == context[:project].try(:id)
|
|
|
|
|
|
|
|
project = Project.find(project_id) rescue nil
|
|
|
|
Ability.abilities.allowed?(user, :read_project, project)
|
|
|
|
else
|
|
|
|
true
|
|
|
|
end
|
|
|
|
end
|
2015-09-11 14:41:01 +05:30
|
|
|
|
2015-10-24 18:46:33 +05:30
|
|
|
def self.referenced_by(node)
|
|
|
|
raise NotImplementedError, "#{self} does not implement #{__method__}"
|
2015-09-11 14:41:01 +05:30
|
|
|
end
|
|
|
|
|
|
|
|
# Returns a data attribute String to attach to a reference link
|
|
|
|
#
|
2015-10-24 18:46:33 +05:30
|
|
|
# attributes - Hash, where the key becomes the data attribute name and the
|
|
|
|
# value is the data attribute value
|
2015-09-11 14:41:01 +05:30
|
|
|
#
|
|
|
|
# Examples:
|
|
|
|
#
|
2015-10-24 18:46:33 +05:30
|
|
|
# data_attribute(project: 1, issue: 2)
|
2015-12-23 02:04:40 +05:30
|
|
|
# # => "data-reference-filter=\"SomeReferenceFilter\" data-project=\"1\" data-issue=\"2\""
|
2015-10-24 18:46:33 +05:30
|
|
|
#
|
|
|
|
# data_attribute(project: 3, merge_request: 4)
|
2015-12-23 02:04:40 +05:30
|
|
|
# # => "data-reference-filter=\"SomeReferenceFilter\" data-project=\"3\" data-merge-request=\"4\""
|
2015-09-11 14:41:01 +05:30
|
|
|
#
|
|
|
|
# Returns a String
|
2015-10-24 18:46:33 +05:30
|
|
|
def data_attribute(attributes = {})
|
2015-12-23 02:04:40 +05:30
|
|
|
attributes[:reference_filter] = self.class.name.demodulize
|
2015-10-24 18:46:33 +05:30
|
|
|
attributes.map { |key, value| %Q(data-#{key.to_s.dasherize}="#{value}") }.join(" ")
|
2015-09-11 14:41:01 +05:30
|
|
|
end
|
|
|
|
|
|
|
|
def escape_once(html)
|
|
|
|
ERB::Util.html_escape_once(html)
|
|
|
|
end
|
|
|
|
|
|
|
|
def ignore_parents
|
|
|
|
@ignore_parents ||= begin
|
|
|
|
# Don't look for references in text nodes that are children of these
|
|
|
|
# elements.
|
|
|
|
parents = %w(pre code a style)
|
|
|
|
parents << 'blockquote' if context[:ignore_blockquotes]
|
|
|
|
parents.to_set
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def ignored_ancestry?(node)
|
|
|
|
has_ancestor?(node, ignore_parents)
|
|
|
|
end
|
|
|
|
|
|
|
|
def project
|
|
|
|
context[:project]
|
|
|
|
end
|
|
|
|
|
|
|
|
def reference_class(type)
|
2015-09-25 12:07:36 +05:30
|
|
|
"gfm gfm-#{type}"
|
2015-09-11 14:41:01 +05:30
|
|
|
end
|
|
|
|
|
|
|
|
# Iterate through the document's text nodes, yielding the current node's
|
|
|
|
# content if:
|
|
|
|
#
|
|
|
|
# * The `project` context value is present AND
|
|
|
|
# * The node's content matches `pattern` AND
|
|
|
|
# * The node is not an ancestor of an ignored node type
|
|
|
|
#
|
|
|
|
# pattern - Regex pattern against which to match the node's content
|
|
|
|
#
|
|
|
|
# Yields the current node's String contents. The result of the block will
|
|
|
|
# replace the node's existing content and update the current document.
|
|
|
|
#
|
2015-10-24 18:46:33 +05:30
|
|
|
# Returns the updated Nokogiri::HTML::DocumentFragment object.
|
2015-09-11 14:41:01 +05:30
|
|
|
def replace_text_nodes_matching(pattern)
|
|
|
|
return doc if project.nil?
|
|
|
|
|
|
|
|
search_text_nodes(doc).each do |node|
|
|
|
|
next if ignored_ancestry?(node)
|
2015-11-26 14:37:03 +05:30
|
|
|
next unless node.text =~ pattern
|
|
|
|
|
|
|
|
content = node.to_html
|
2015-09-11 14:41:01 +05:30
|
|
|
|
|
|
|
html = yield content
|
|
|
|
|
|
|
|
next if html == content
|
|
|
|
|
|
|
|
node.replace(html)
|
|
|
|
end
|
|
|
|
|
|
|
|
doc
|
|
|
|
end
|
|
|
|
|
2015-12-23 02:04:40 +05:30
|
|
|
# Iterate through the document's link nodes, yielding the current node's
|
|
|
|
# content if:
|
|
|
|
#
|
|
|
|
# * The `project` context value is present AND
|
|
|
|
# * The node's content matches `pattern`
|
|
|
|
#
|
|
|
|
# pattern - Regex pattern against which to match the node's content
|
|
|
|
#
|
|
|
|
# Yields the current node's String contents. The result of the block will
|
|
|
|
# replace the node and update the current document.
|
|
|
|
#
|
|
|
|
# Returns the updated Nokogiri::HTML::DocumentFragment object.
|
|
|
|
def replace_link_nodes_with_text(pattern)
|
|
|
|
return doc if project.nil?
|
|
|
|
|
|
|
|
doc.search('a').each do |node|
|
|
|
|
klass = node.attr('class')
|
|
|
|
next if klass && klass.include?('gfm')
|
|
|
|
|
|
|
|
link = node.attr('href')
|
|
|
|
text = node.text
|
|
|
|
|
|
|
|
next unless link && text
|
|
|
|
|
|
|
|
link = URI.decode(link)
|
|
|
|
# Ignore ending punctionation like periods or commas
|
|
|
|
next unless link == text && text =~ /\A#{pattern}/
|
|
|
|
|
|
|
|
html = yield text
|
|
|
|
|
|
|
|
next if html == text
|
|
|
|
|
|
|
|
node.replace(html)
|
|
|
|
end
|
|
|
|
|
|
|
|
doc
|
|
|
|
end
|
|
|
|
|
|
|
|
# Iterate through the document's link nodes, yielding the current node's
|
|
|
|
# content if:
|
|
|
|
#
|
|
|
|
# * The `project` context value is present AND
|
|
|
|
# * The node's HREF matches `pattern`
|
|
|
|
#
|
|
|
|
# pattern - Regex pattern against which to match the node's HREF
|
|
|
|
#
|
|
|
|
# Yields the current node's String HREF and String content.
|
|
|
|
# The result of the block will replace the node and update the current document.
|
|
|
|
#
|
|
|
|
# Returns the updated Nokogiri::HTML::DocumentFragment object.
|
|
|
|
def replace_link_nodes_with_href(pattern)
|
|
|
|
return doc if project.nil?
|
|
|
|
|
|
|
|
doc.search('a').each do |node|
|
|
|
|
klass = node.attr('class')
|
|
|
|
next if klass && klass.include?('gfm')
|
|
|
|
|
|
|
|
link = node.attr('href')
|
|
|
|
text = node.text
|
|
|
|
|
|
|
|
next unless link && text
|
|
|
|
link = URI.decode(link)
|
|
|
|
next unless link && link =~ /\A#{pattern}\z/
|
|
|
|
|
|
|
|
html = yield link, text
|
|
|
|
|
|
|
|
next if html == link
|
|
|
|
|
|
|
|
node.replace(html)
|
|
|
|
end
|
|
|
|
|
|
|
|
doc
|
|
|
|
end
|
|
|
|
|
2015-09-11 14:41:01 +05:30
|
|
|
# Ensure that a :project key exists in context
|
|
|
|
#
|
|
|
|
# Note that while the key might exist, its value could be nil!
|
|
|
|
def validate
|
|
|
|
needs :project
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|