1# frozen_string_literal: true 2 3require 'asciidoctor/include_ext/include_processor' 4 5module Gitlab 6 module Asciidoc 7 # Asciidoctor extension for processing includes (macro include::[]) within 8 # documents inside the same repository. 9 class IncludeProcessor < Asciidoctor::IncludeExt::IncludeProcessor 10 extend ::Gitlab::Utils::Override 11 12 def initialize(context) 13 super(logger: Gitlab::AppLogger) 14 15 @context = context 16 @repository = context[:repository] || context[:project].try(:repository) 17 @max_includes = context[:max_includes].to_i 18 @included = [] 19 20 # Note: Asciidoctor calls #freeze on extensions, so we can't set new 21 # instance variables after initialization. 22 @cache = { 23 uri_types: {} 24 } 25 end 26 27 protected 28 29 override :include_allowed? 30 def include_allowed?(target, reader) 31 doc = reader.document 32 33 max_include_depth = doc.attributes.fetch('max-include-depth').to_i 34 35 return false if max_include_depth < 1 36 return false if target_uri?(target) 37 return false if included.size >= max_includes 38 39 true 40 end 41 42 override :resolve_target_path 43 def resolve_target_path(target, reader) 44 return unless repository.try(:exists?) 45 46 base_path = reader.include_stack.empty? ? requested_path : reader.file 47 path = resolve_relative_path(target, base_path) 48 49 path if Gitlab::Git::Blob.find(repository, ref, path) 50 end 51 52 override :read_lines 53 def read_lines(filename, selector) 54 blob = read_blob(ref, filename) 55 56 if selector 57 blob.data.each_line.select.with_index(1, &selector) 58 else 59 blob.data 60 end 61 end 62 63 override :unresolved_include! 64 def unresolved_include!(target, reader) 65 reader.unshift_line("*[ERROR: include::#{target}[] - unresolved directive]*") 66 end 67 68 private 69 70 attr_reader :context, :repository, :cache, :max_includes, :included 71 72 # Gets a Blob at a path for a specific revision. 73 # This method will check that the Blob exists and contains readable text. 74 # 75 # revision - The String SHA1. 76 # path - The String file path. 77 # 78 # Returns a Blob 79 def read_blob(ref, filename) 80 blob = repository&.blob_at(ref, filename) 81 82 raise 'Blob not found' unless blob 83 raise 'File is not readable' unless blob.readable_text? 84 85 included << filename 86 87 blob 88 end 89 90 # Resolves the given relative path of file in repository into canonical 91 # path based on the specified base_path. 92 # 93 # Examples: 94 # 95 # # File in the same directory as the current path 96 # resolve_relative_path("users.adoc", "doc/api/README.adoc") 97 # # => "doc/api/users.adoc" 98 # 99 # # File in the same directory, which is also the current path 100 # resolve_relative_path("users.adoc", "doc/api") 101 # # => "doc/api/users.adoc" 102 # 103 # # Going up one level to a different directory 104 # resolve_relative_path("../update/7.14-to-8.0.adoc", "doc/api/README.adoc") 105 # # => "doc/update/7.14-to-8.0.adoc" 106 # 107 # Returns a String 108 def resolve_relative_path(path, base_path) 109 p = Pathname(base_path) 110 p = p.dirname unless p.extname.empty? 111 p += path 112 113 p.cleanpath.to_s 114 end 115 116 def current_commit 117 cache[:current_commit] ||= context[:commit] || repository&.commit(ref) 118 end 119 120 def ref 121 context[:ref] || repository&.root_ref 122 end 123 124 def requested_path 125 cache[:requested_path] ||= Addressable::URI.unescape(context[:requested_path]) 126 end 127 128 def uri_type(path) 129 cache[:uri_types][path] ||= current_commit&.uri_type(path) 130 end 131 end 132 end 133end 134