1##
2# Handle common directives that can occur in a block of text:
3#
4#   \:include: filename
5#
6# Directives can be escaped by preceding them with a backslash.
7#
8# RDoc plugin authors can register additional directives to be handled by
9# using RDoc::Markup::PreProcess::register.
10#
11# Any directive that is not built-in to RDoc (including those registered via
12# plugins) will be stored in the metadata hash on the CodeObject the comment
13# is attached to.  See RDoc::Markup@Directives for the list of built-in
14# directives.
15
16class RDoc::Markup::PreProcess
17
18  ##
19  # An RDoc::Options instance that will be filled in with overrides from
20  # directives
21
22  attr_accessor :options
23
24  ##
25  # Adds a post-process handler for directives.  The handler will be called
26  # with the result RDoc::Comment (or text String) and the code object for the
27  # comment (if any).
28
29  def self.post_process &block
30    @post_processors << block
31  end
32
33  ##
34  # Registered post-processors
35
36  def self.post_processors
37    @post_processors
38  end
39
40  ##
41  # Registers +directive+ as one handled by RDoc.  If a block is given the
42  # directive will be replaced by the result of the block, otherwise the
43  # directive will be removed from the processed text.
44  #
45  # The block will be called with the directive name and the directive
46  # parameter:
47  #
48  #   RDoc::Markup::PreProcess.register 'my-directive' do |directive, param|
49  #     # replace text, etc.
50  #   end
51
52  def self.register directive, &block
53    @registered[directive] = block
54  end
55
56  ##
57  # Registered directives
58
59  def self.registered
60    @registered
61  end
62
63  ##
64  # Clears all registered directives and post-processors
65
66  def self.reset
67    @post_processors = []
68    @registered = {}
69  end
70
71  reset
72
73  ##
74  # Creates a new pre-processor for +input_file_name+ that will look for
75  # included files in +include_path+
76
77  def initialize(input_file_name, include_path)
78    @input_file_name = input_file_name
79    @include_path = include_path
80    @options = nil
81  end
82
83  ##
84  # Look for directives in the given +text+.
85  #
86  # Options that we don't handle are yielded.  If the block returns false the
87  # directive is restored to the text.  If the block returns nil or no block
88  # was given the directive is handled according to the registered directives.
89  # If a String was returned the directive is replaced with the string.
90  #
91  # If no matching directive was registered the directive is restored to the
92  # text.
93  #
94  # If +code_object+ is given and the directive is unknown then the
95  # directive's parameter is set as metadata on the +code_object+.  See
96  # RDoc::CodeObject#metadata for details.
97
98  def handle text, code_object = nil, &block
99    if RDoc::Comment === text then
100      comment = text
101      text = text.text
102    end
103
104    encoding = text.encoding if defined?(Encoding)
105
106    # regexp helper (square brackets for optional)
107    # $1      $2  $3        $4      $5
108    # [prefix][\]:directive:[spaces][param]newline
109    text.gsub!(/^([ \t]*(?:#|\/?\*)?[ \t]*)(\\?):(\w+):([ \t]*)(.+)?(\r?\n|$)/) do
110      # skip something like ':toto::'
111      next $& if $4.empty? and $5 and $5[0, 1] == ':'
112
113      # skip if escaped
114      next "#$1:#$3:#$4#$5\n" unless $2.empty?
115
116      # This is not in handle_directive because I didn't want to pass another
117      # argument into it
118      if comment and $3 == 'markup' then
119        next "#{$1.strip}\n" unless $5
120        comment.format = $5.downcase
121        next "#{$1.strip}\n"
122      end
123
124      handle_directive $1, $3, $5, code_object, encoding, &block
125    end
126
127    comment = text unless comment
128
129    self.class.post_processors.each do |handler|
130      handler.call comment, code_object
131    end
132
133    text
134  end
135
136  ##
137  # Performs the actions described by +directive+ and its parameter +param+.
138  #
139  # +code_object+ is used for directives that operate on a class or module.
140  # +prefix+ is used to ensure the replacement for handled directives is
141  # correct.  +encoding+ is used for the <tt>include</tt> directive.
142  #
143  # For a list of directives in RDoc see RDoc::Markup.
144  #--
145  # When 1.8.7 support is ditched prefix can be defaulted to ''
146
147  def handle_directive prefix, directive, param, code_object = nil,
148                       encoding = nil
149    blankline = "#{prefix.strip}\n"
150    directive = directive.downcase
151
152    case directive
153    when 'arg', 'args' then
154      return blankline unless code_object
155
156      code_object.params = param
157
158      blankline
159    when 'category' then
160      if RDoc::Context === code_object then
161        section = code_object.add_section param
162        code_object.temporary_section = section
163      end
164
165      blankline # ignore category if we're not on an RDoc::Context
166    when 'doc' then
167      return blankline unless code_object
168      code_object.document_self = true
169      code_object.force_documentation = true
170
171      blankline
172    when 'enddoc' then
173      return blankline unless code_object
174      code_object.done_documenting = true
175
176      blankline
177    when 'include' then
178      filename = param.split.first
179      include_file filename, prefix, encoding
180    when 'main' then
181      @options.main_page = param if @options.respond_to? :main_page
182
183      blankline
184    when 'nodoc' then
185      return blankline unless code_object
186      code_object.document_self = nil # notify nodoc
187      code_object.document_children = param !~ /all/i
188
189      blankline
190    when 'notnew', 'not_new', 'not-new' then
191      return blankline unless RDoc::AnyMethod === code_object
192
193      code_object.dont_rename_initialize = true
194
195      blankline
196    when 'startdoc' then
197      return blankline unless code_object
198
199      code_object.start_doc
200      code_object.force_documentation = true
201
202      blankline
203    when 'stopdoc' then
204      return blankline unless code_object
205
206      code_object.stop_doc
207
208      blankline
209    when 'title' then
210      @options.default_title = param if @options.respond_to? :default_title=
211
212      blankline
213    when 'yield', 'yields' then
214      return blankline unless code_object
215      # remove parameter &block
216      code_object.params.sub!(/,?\s*&\w+/, '') if code_object.params
217
218      code_object.block_params = param
219
220      blankline
221    else
222      result = yield directive, param if block_given?
223
224      case result
225      when nil then
226        code_object.metadata[directive] = param if code_object
227
228        if RDoc::Markup::PreProcess.registered.include? directive then
229          handler = RDoc::Markup::PreProcess.registered[directive]
230          result = handler.call directive, param if handler
231        else
232          result = "#{prefix}:#{directive}: #{param}\n"
233        end
234      when false then
235        result = "#{prefix}:#{directive}: #{param}\n"
236      end
237
238      result
239    end
240  end
241
242  ##
243  # Handles the <tt>:include: _filename_</tt> directive.
244  #
245  # If the first line of the included file starts with '#', and contains
246  # an encoding information in the form 'coding:' or 'coding=', it is
247  # removed.
248  #
249  # If all lines in the included file start with a '#', this leading '#'
250  # is removed before inclusion. The included content is indented like
251  # the <tt>:include:</tt> directive.
252  #--
253  # so all content will be verbatim because of the likely space after '#'?
254  # TODO shift left the whole file content in that case
255  # TODO comment stop/start #-- and #++ in included file must be processed here
256
257  def include_file name, indent, encoding
258    full_name = find_include_file name
259
260    unless full_name then
261      warn "Couldn't find file to include '#{name}' from #{@input_file_name}"
262      return ''
263    end
264
265    content = RDoc::Encoding.read_file full_name, encoding, true
266
267    # strip magic comment
268    content = content.sub(/\A# .*coding[=:].*$/, '').lstrip
269
270    # strip leading '#'s, but only if all lines start with them
271    if content =~ /^[^#]/ then
272      content.gsub(/^/, indent)
273    else
274      content.gsub(/^#?/, indent)
275    end
276  end
277
278  ##
279  # Look for the given file in the directory containing the current file,
280  # and then in each of the directories specified in the RDOC_INCLUDE path
281
282  def find_include_file(name)
283    to_search = [File.dirname(@input_file_name)].concat @include_path
284    to_search.each do |dir|
285      full_name = File.join(dir, name)
286      stat = File.stat(full_name) rescue next
287      return full_name if stat.readable?
288    end
289    nil
290  end
291
292end
293
294