summaryrefslogtreecommitdiff
path: root/lib/banzai/filter/reference_filter.rb
blob: 2d6f34c9cd8633fec321e97241b5e556e9981765 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
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
      class << self
        attr_accessor :reference_type
      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-filter=\"SomeReferenceFilter\" data-project=\"1\" data-issue=\"2\""
      #
      #   data_attribute(project: 3, merge_request: 4)
      #   # => "data-reference-filter=\"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.delete(:original) if context[:no_original_data]
        attributes.map { |key, value| %Q(data-#{key.to_s.dasherize}="#{escape_once(value)}") }.join(" ")
      end

      def escape_once(html)
        html.html_safe? ? html : ERB::Util.html_escape_once(html)
      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 reference_class(type)
        "gfm gfm-#{type}"
      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
      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?

        query = %Q{descendant-or-self::text()[not(#{ignore_ancestor_query})]
        | descendant-or-self::a[
          not(contains(concat(" ", @class, " "), " gfm ")) and not(@href = "")
        ]}

        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 text whenever the node is a valid <a> tag.
      def yield_valid_link(node)
        link = CGI.unescape(node.attr('href').to_s)
        text = node.text

        return unless link.force_encoding('UTF-8').valid_encoding?

        yield link, text
      end

      def replace_text_when_pattern_matches(node, pattern)
        return unless node.text =~ pattern

        content = node.to_html
        html = yield content

        node.replace(html) unless content == html
      end

      def replace_link_node_with_text(node, link)
        html = yield

        node.replace(html) unless html == node.text
      end

      def replace_link_node_with_href(node, link)
        html = yield

        node.replace(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
    end
  end
end