summaryrefslogtreecommitdiff
path: root/lib/gitlab/gfm/reference_unfolder.rb
blob: 0a68d6f977f5bf0d4c85fe39f25d42b98d6612f3 (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
module Gitlab
  module Gfm
    ##
    # Class that unfolds local references in text.
    #
    # The initializer takes text in Markdown and project this text is valid
    # in context of.
    #
    # `unfold` method tries to find all local references and unfold each of
    # those local references to cross reference format.
    #
    # Examples:
    #
    # 'Hello, this issue is related to #123 and
    #  other issues labeled with ~"label"', will be converted to:
    #
    # 'Hello, this issue is related to gitlab-org/gitlab-ce#123 and
    #  other issue labeled with gitlab-org/gitlab-ce~"label"'.
    #
    # It does respect markdown lexical rules, so text in code block will not be
    # replaced, see another example:
    #
    # 'Merge request for issue #1234, see also link:
    #  http://gitlab.com/some/link/#1234, and code `puts #1234`' =>
    #
    # 'Merge request for issue gitlab-org/gitlab-ce#1234, se also link:
    #  http://gitlab.com/some/link/#1234, and code `puts #1234`'
    #
    class ReferenceUnfolder
      def initialize(text, project)
        @text = text
        @project = project
        @original = markdown(text)
      end

      def unfold(from_project)
        return @text unless @text =~ references_pattern

        unfolded = @text.gsub(references_pattern) do |reference|
          unfold_reference(reference, Regexp.last_match, from_project)
        end

        unless substitution_valid?(unfolded)
          raise StandardError, 'Invalid references unfolding!'
        end

        unfolded
      end

      private

      def unfold_reference(reference, match, from_project)
        before = @text[0...match.begin(0)]
        after = @text[match.end(0)...@text.length]
        referable = find_referable(reference)

        return reference unless referable
        cross_reference = referable.to_reference(from_project)
        new_text = before + cross_reference + after

        substitution_valid?(new_text) ? cross_reference : reference
      end

      def references_pattern
        return @pattern if @pattern

        patterns = Gitlab::ReferenceExtractor::REFERABLES.map do |ref|
          ref.to_s.classify.constantize.try(:reference_pattern)
        end

        @pattern = Regexp.union(patterns.compact)
      end

      def referables
        return @referables if @referables

        extractor = Gitlab::ReferenceExtractor.new(@project)
        extractor.analyze(@text)
        @referables = extractor.all
      end

      def find_referable(reference)
        referables.find { |ref| ref.to_reference == reference }
      end

      def substitution_valid?(substituted)
        @original == markdown(substituted)
      end

      def markdown(text)
        Banzai.render(text, project: @project, no_original_data: true)
      end
    end
  end
end