summaryrefslogtreecommitdiff
path: root/lib/gitlab/dependency_linker/base_linker.rb
blob: 40a4ad113724786dc2f338b8567c011ea764b10b (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
module Gitlab
  module DependencyLinker
    class BaseLinker
      def self.link(plain_text, highlighted_text)
        new(plain_text, highlighted_text).link
      end

      attr_accessor :plain_text, :highlighted_text

      def initialize(plain_text, highlighted_text)
        @plain_text = plain_text
        @highlighted_text = highlighted_text
      end

      def link
        link_dependencies

        highlighted_lines.join.html_safe
      end

      private

      def package_url(name)
        raise NotImplementedError
      end

      def link_dependencies
        raise NotImplementedError
      end

      def package_link(name, url = package_url(name))
        return name unless url

        %{<a href="#{ERB::Util.html_escape_once(url)}" rel="noopener noreferrer" target="_blank">#{ERB::Util.html_escape_once(name)}</a>}
      end

      # Links package names in a method call or assignment string argument.
      #
      # Example:
      #   link_method_call("gem")
      #   # Will link `package` in `gem "package"`, `gem("package")` and `gem = "package"`
      #
      #   link_method_call("gem", "specific_package")
      #   # Will link `specific_package` in `gem "specific_package"`
      #
      #   link_method_call("github", /[^\/]+\/[^\/]+/)
      #   # Will link `user/repo` in `github "user/repo"`, but not `github "package"`
      #
      #   link_method_call(%w[add_dependency add_development_dependency])
      #   # Will link `spec.add_dependency "package"` and `spec.add_development_dependency "package"`
      #
      #   link_method_call("name")
      #   # Will link `package` in `self.name = "package"`
      def link_method_call(method_names, value = nil, &url_proc)
        value =
          case value
          when String
            Regexp.escape(value)
          when nil
            /[^'"]+/
          else
            value
          end

        method_names = Array(method_names).map { |name| Regexp.escape(name) }

        regex = %r{
          #{Regexp.union(method_names)} # Method name
          \s*                           # Whitespace
          [(=]?                         # Opening brace or equals sign
          \s*                           # Whitespace
          ['"](?<name>#{value})['"]     # Package name in quotes
        }x

        link_regex(regex, &url_proc)
      end

      # Links package names based on regex.
      #
      # Example:
      #   link_regex(/(github:|:github =>)\s*['"](?<name>[^'"]+)['"]/)
      #   # Will link `user/repo` in `github: "user/repo"` or `:github => "user/repo"`
      def link_regex(regex)
        highlighted_lines.map!.with_index do |rich_line, i|
          marker = StringRegexMarker.new(plain_lines[i], rich_line.html_safe)

          marker.mark(regex, group: :name) do |text, left:, right:|
            url = block_given? ? yield(text) : package_url(text)
            package_link(text, url)
          end
        end
      end

      def plain_lines
        @plain_lines ||= plain_text.lines
      end

      def highlighted_lines
        @highlighted_lines ||= highlighted_text.lines
      end
    end
  end
end