summaryrefslogtreecommitdiff
path: root/lib/gitlab/markdown/abstract_reference_filter.rb
blob: 9488e980c086f22756e2f59eb6ec2fee3d9ff501 (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
require 'gitlab/markdown'

module Gitlab
  module Markdown
    # 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

      delegate :object_class, :object_sym, :references_in, to: :class

      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
        # `#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

        # `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

      # 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 = escape_once(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
            unless text
              text = object.reference_link_text(context[:project])

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

            %(<a href="#{url}" #{data}
                 title="#{title}"
                 class="#{klass}">#{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
    end
  end
end