summaryrefslogtreecommitdiff
path: root/lib/banzai/filter/abstract_reference_filter.rb
blob: cdbaecf8d9077f3bd7db00b616d2487c1cee73ab (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
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
module Banzai
  module Filter
    # Issues, Merge Requests, Snippets, Commits and Commit Ranges share
    # similar functionality in reference filtering.
    class AbstractReferenceFilter < ReferenceFilter
      include CrossProjectReference

      def self.object_class
        # Implement in child class
        # Example: MergeRequest
      end

      def self.object_name
        object_class.name.underscore
      end

      def self.object_sym
        object_name.to_sym
      end

      def self.data_reference
        "data-#{object_name.dasherize}"
      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|
          yield match, $~[object_sym].to_i, $~[:project], $~
        end
      end

      def self.referenced_by(node)
        { object_sym => LazyReference.new(object_class, node.attr(data_reference)) }
      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

      def find_object(project, id)
        # Implement in child class
        # Example: project.merge_requests.find
      end

      def url_for_object(object, project)
        # Implement in child class
        # Example: project_merge_request_url
      end

      def call
        if object_class.reference_pattern
          # `#123`
          replace_text_nodes_matching(object_class.reference_pattern) do |content|
            object_link_filter(content, object_class.reference_pattern)
          end

          # `[Issue](#123)`, which is turned into
          # `<a href="#123">Issue</a>`
          replace_link_nodes_with_href(object_class.reference_pattern) do |link, text|
            object_link_filter(link, object_class.reference_pattern, link_text: text)
          end
        end

        if object_class.link_reference_pattern
          # `http://gitlab.example.com/namespace/project/issues/123`, which is turned into
          # `<a href="http://gitlab.example.com/namespace/project/issues/123">http://gitlab.example.com/namespace/project/issues/123</a>`
          replace_link_nodes_with_text(object_class.link_reference_pattern) do |text|
            object_link_filter(text, object_class.link_reference_pattern)
          end

          # `[Issue](http://gitlab.example.com/namespace/project/issues/123)`, which is turned into
          # `<a href="http://gitlab.example.com/namespace/project/issues/123">Issue</a>`
          replace_link_nodes_with_href(object_class.link_reference_pattern) do |link, text|
            object_link_filter(link, object_class.link_reference_pattern, link_text: text)
          end
        end
      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_text - Original content of the link being replaced.
      #
      # 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_text: nil)
        references_in(text, pattern) do |match, id, project_ref, matches|
          project = project_from_ref(project_ref)

          if project && object = find_object(project, id)
            title = object_link_title(object)
            klass = reference_class(object_sym)

            data  = data_attribute(
              original:     link_text || match,
              project:      project.id,
              object_sym => object.id
            )

            url = matches[:url] if matches.names.include?("url")
            url ||= url_for_object(object, project)

            text = link_text || object_link_text(object, matches)

            %(<a href="#{url}" #{data}
                 title="#{escape_once(title)}"
                 class="#{klass}">#{escape_once(text)}</a>)
          else
            match
          end
        end
      end

      def object_link_text_extras(object, matches)
        extras = []

        if matches.names.include?("anchor") && matches[:anchor] && matches[:anchor] =~ /\A\#note_(\d+)\z/
          extras << "comment #{$1}"
        end

        extras
      end

      def object_link_title(object)
        "#{object_class.name.titleize}: #{object.title}"
      end

      def object_link_text(object, matches)
        text = object.reference_link_text(context[:project])

        extras = object_link_text_extras(object, matches)
        text += " (#{extras.join(", ")})" if extras.any?

        text
      end
    end
  end
end