include_processor.rb 3.78 KB
Newer Older
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# frozen_string_literal: true

require 'asciidoctor/include_ext/include_processor'

module Gitlab
  module Asciidoc
    # Asciidoctor extension for processing includes (macro include::[]) within
    # documents inside the same repository.
    class IncludeProcessor < Asciidoctor::IncludeExt::IncludeProcessor
      extend ::Gitlab::Utils::Override

      def initialize(context)
        super(logger: Gitlab::AppLogger)

        @context = context
16
17
18
        @repository = context[:repository] || context[:project].try(:repository)
        @max_includes = context[:max_includes].to_i
        @included = []
19
20
21
22
23
24
25
26
27
28
29
30
31
32

        # Note: Asciidoctor calls #freeze on extensions, so we can't set new
        # instance variables after initialization.
        @cache = {
            uri_types: {}
        }
      end

      protected

      override :include_allowed?
      def include_allowed?(target, reader)
        doc = reader.document

33
34
35
        max_include_depth = doc.attributes.fetch('max-include-depth').to_i

        return false if max_include_depth < 1
36
        return false if target_uri?(target)
37
        return false if included.size >= max_includes
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

        true
      end

      override :resolve_target_path
      def resolve_target_path(target, reader)
        return unless repository.try(:exists?)

        base_path = reader.include_stack.empty? ? requested_path : reader.file
        path = resolve_relative_path(target, base_path)

        path if Gitlab::Git::Blob.find(repository, ref, path)
      end

      override :read_lines
      def read_lines(filename, selector)
        blob = read_blob(ref, filename)

        if selector
          blob.data.each_line.select.with_index(1, &selector)
        else
          blob.data
        end
      end

      override :unresolved_include!
      def unresolved_include!(target, reader)
        reader.unshift_line("*[ERROR: include::#{target}[] - unresolved directive]*")
      end

      private

70
      attr_reader :context, :repository, :cache, :max_includes, :included
71
72
73
74
75
76
77
78
79
80
81
82
83
84

      # Gets a Blob at a path for a specific revision.
      # This method will check that the Blob exists and contains readable text.
      #
      # revision - The String SHA1.
      # path     - The String file path.
      #
      # Returns a Blob
      def read_blob(ref, filename)
        blob = repository&.blob_at(ref, filename)

        raise 'Blob not found' unless blob
        raise 'File is not readable' unless blob.readable_text?

85
86
        included << filename

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
        blob
      end

      # Resolves the given relative path of file in repository into canonical
      # path based on the specified base_path.
      #
      # Examples:
      #
      #   # File in the same directory as the current path
      #   resolve_relative_path("users.adoc", "doc/api/README.adoc")
      #   # => "doc/api/users.adoc"
      #
      #   # File in the same directory, which is also the current path
      #   resolve_relative_path("users.adoc", "doc/api")
      #   # => "doc/api/users.adoc"
      #
      #   # Going up one level to a different directory
      #   resolve_relative_path("../update/7.14-to-8.0.adoc", "doc/api/README.adoc")
      #   # => "doc/update/7.14-to-8.0.adoc"
      #
      # Returns a String
      def resolve_relative_path(path, base_path)
        p = Pathname(base_path)
        p = p.dirname unless p.extname.empty?
        p += path

        p.cleanpath.to_s
      end

      def current_commit
        cache[:current_commit] ||= context[:commit] || repository&.commit(ref)
      end

      def ref
        context[:ref] || context[:project].default_branch
      end

      def requested_path
        cache[:requested_path] ||= Addressable::URI.unescape(context[:requested_path])
      end

      def uri_type(path)
        cache[:uri_types][path] ||= current_commit&.uri_type(path)
      end
    end
  end
end