1require 'rdoc'
2
3require 'find'
4require 'fileutils'
5require 'pathname'
6require 'time'
7
8##
9# This is the driver for generating RDoc output.  It handles file parsing and
10# generation of output.
11#
12# To use this class to generate RDoc output via the API, the recommended way
13# is:
14#
15#   rdoc = RDoc::RDoc.new
16#   options = rdoc.load_options # returns an RDoc::Options instance
17#   # set extra options
18#   rdoc.document options
19#
20# You can also generate output like the +rdoc+ executable:
21#
22#   rdoc = RDoc::RDoc.new
23#   rdoc.document argv
24#
25# Where +argv+ is an array of strings, each corresponding to an argument you'd
26# give rdoc on the command line.  See <tt>rdoc --help<tt> for details.
27
28class RDoc::RDoc
29
30  @current = nil
31
32  ##
33  # This is the list of supported output generators
34
35  GENERATORS = {}
36
37  ##
38  # File pattern to exclude
39
40  attr_accessor :exclude
41
42  ##
43  # Generator instance used for creating output
44
45  attr_accessor :generator
46
47  ##
48  # Hash of files and their last modified times.
49
50  attr_reader :last_modified
51
52  ##
53  # RDoc options
54
55  attr_accessor :options
56
57  ##
58  # Accessor for statistics.  Available after each call to parse_files
59
60  attr_reader :stats
61
62  ##
63  # The current documentation store
64
65  attr_reader :store
66
67  ##
68  # Add +klass+ that can generate output after parsing
69
70  def self.add_generator(klass)
71    name = klass.name.sub(/^RDoc::Generator::/, '').downcase
72    GENERATORS[name] = klass
73  end
74
75  ##
76  # Active RDoc::RDoc instance
77
78  def self.current
79    @current
80  end
81
82  ##
83  # Sets the active RDoc::RDoc instance
84
85  def self.current= rdoc
86    @current = rdoc
87  end
88
89  ##
90  # Creates a new RDoc::RDoc instance.  Call #document to parse files and
91  # generate documentation.
92
93  def initialize
94    @current       = nil
95    @exclude       = nil
96    @generator     = nil
97    @last_modified = {}
98    @old_siginfo   = nil
99    @options       = nil
100    @stats         = nil
101    @store         = nil
102  end
103
104  ##
105  # Report an error message and exit
106
107  def error(msg)
108    raise RDoc::Error, msg
109  end
110
111  ##
112  # Gathers a set of parseable files from the files and directories listed in
113  # +files+.
114
115  def gather_files files
116    files = ["."] if files.empty?
117
118    file_list = normalized_file_list files, true, @exclude
119
120    file_list = file_list.uniq
121
122    file_list = remove_unparseable file_list
123
124    file_list.sort
125  end
126
127  ##
128  # Turns RDoc from stdin into HTML
129
130  def handle_pipe
131    @html = RDoc::Markup::ToHtml.new @options
132
133    parser = RDoc::Text::MARKUP_FORMAT[@options.markup]
134
135    document = parser.parse $stdin.read
136
137    out = @html.convert document
138
139    $stdout.write out
140  end
141
142  ##
143  # Installs a siginfo handler that prints the current filename.
144
145  def install_siginfo_handler
146    return unless Signal.list.include? 'INFO'
147
148    @old_siginfo = trap 'INFO' do
149      puts @current if @current
150    end
151  end
152
153  ##
154  # Loads options from .rdoc_options if the file exists, otherwise creates a
155  # new RDoc::Options instance.
156
157  def load_options
158    options_file = File.expand_path '.rdoc_options'
159    return RDoc::Options.new unless File.exist? options_file
160
161    RDoc.load_yaml
162
163    parse_error = if Object.const_defined? :Psych then
164                    Psych::SyntaxError
165                  else
166                    ArgumentError
167                  end
168
169    begin
170      options = YAML.load_file '.rdoc_options'
171    rescue *parse_error
172    end
173
174    raise RDoc::Error, "#{options_file} is not a valid rdoc options file" unless
175      RDoc::Options === options
176
177    options
178  end
179
180  ##
181  # Create an output dir if it doesn't exist. If it does exist, but doesn't
182  # contain the flag file <tt>created.rid</tt> then we refuse to use it, as
183  # we may clobber some manually generated documentation
184
185  def setup_output_dir(dir, force)
186    flag_file = output_flag_file dir
187
188    last = {}
189
190    if @options.dry_run then
191      # do nothing
192    elsif File.exist? dir then
193      error "#{dir} exists and is not a directory" unless File.directory? dir
194
195      begin
196        open flag_file do |io|
197          unless force then
198            Time.parse io.gets
199
200            io.each do |line|
201              file, time = line.split "\t", 2
202              time = Time.parse(time) rescue next
203              last[file] = time
204            end
205          end
206        end
207      rescue SystemCallError, TypeError
208        error <<-ERROR
209
210Directory #{dir} already exists, but it looks like it isn't an RDoc directory.
211
212Because RDoc doesn't want to risk destroying any of your existing files,
213you'll need to specify a different output directory name (using the --op <dir>
214option)
215
216        ERROR
217      end unless @options.force_output
218    else
219      FileUtils.mkdir_p dir
220      FileUtils.touch output_flag_file dir
221    end
222
223    last
224  end
225
226  ##
227  # Sets the current documentation tree to +store+ and sets the store's rdoc
228  # driver to this instance.
229
230  def store= store
231    @store = store
232    @store.rdoc = self
233  end
234
235  ##
236  # Update the flag file in an output directory.
237
238  def update_output_dir(op_dir, time, last = {})
239    return if @options.dry_run or not @options.update_output_dir
240
241    open output_flag_file(op_dir), "w" do |f|
242      f.puts time.rfc2822
243      last.each do |n, t|
244        f.puts "#{n}\t#{t.rfc2822}"
245      end
246    end
247  end
248
249  ##
250  # Return the path name of the flag file in an output directory.
251
252  def output_flag_file(op_dir)
253    File.join op_dir, "created.rid"
254  end
255
256  ##
257  # The .document file contains a list of file and directory name patterns,
258  # representing candidates for documentation. It may also contain comments
259  # (starting with '#')
260
261  def parse_dot_doc_file in_dir, filename
262    # read and strip comments
263    patterns = File.read(filename).gsub(/#.*/, '')
264
265    result = []
266
267    patterns.split.each do |patt|
268      candidates = Dir.glob(File.join(in_dir, patt))
269      result.concat normalized_file_list(candidates)
270    end
271
272    result
273  end
274
275  ##
276  # Given a list of files and directories, create a list of all the Ruby
277  # files they contain.
278  #
279  # If +force_doc+ is true we always add the given files, if false, only
280  # add files that we guarantee we can parse.  It is true when looking at
281  # files given on the command line, false when recursing through
282  # subdirectories.
283  #
284  # The effect of this is that if you want a file with a non-standard
285  # extension parsed, you must name it explicitly.
286
287  def normalized_file_list(relative_files, force_doc = false,
288                           exclude_pattern = nil)
289    file_list = []
290
291    relative_files.each do |rel_file_name|
292      next if exclude_pattern && exclude_pattern =~ rel_file_name
293      stat = File.stat rel_file_name rescue next
294
295      case type = stat.ftype
296      when "file" then
297        next if last_modified = @last_modified[rel_file_name] and
298                stat.mtime.to_i <= last_modified.to_i
299
300        if force_doc or RDoc::Parser.can_parse(rel_file_name) then
301          file_list << rel_file_name.sub(/^\.\//, '')
302          @last_modified[rel_file_name] = stat.mtime
303        end
304      when "directory" then
305        next if rel_file_name == "CVS" || rel_file_name == ".svn"
306
307        dot_doc = File.join rel_file_name, RDoc::DOT_DOC_FILENAME
308
309        if File.file? dot_doc then
310          file_list << parse_dot_doc_file(rel_file_name, dot_doc)
311        else
312          file_list << list_files_in_directory(rel_file_name)
313        end
314      else
315        warn "rdoc can't parse the #{type} #{rel_file_name}"
316      end
317    end
318
319    file_list.flatten
320  end
321
322  ##
323  # Return a list of the files to be processed in a directory. We know that
324  # this directory doesn't have a .document file, so we're looking for real
325  # files. However we may well contain subdirectories which must be tested
326  # for .document files.
327
328  def list_files_in_directory dir
329    files = Dir.glob File.join(dir, "*")
330
331    normalized_file_list files, false, @options.exclude
332  end
333
334  ##
335  # Parses +filename+ and returns an RDoc::TopLevel
336
337  def parse_file filename
338    if defined?(Encoding) then
339      encoding = @options.encoding
340      filename = filename.encode encoding
341    end
342
343    @stats.add_file filename
344
345    content = RDoc::Encoding.read_file filename, encoding
346
347    return unless content
348
349    filename_path = Pathname(filename).expand_path
350    relative_path = filename_path.relative_path_from @options.root
351
352    if @options.page_dir and
353       relative_path.to_s.start_with? @options.page_dir.to_s then
354      relative_path =
355        relative_path.relative_path_from @options.page_dir
356    end
357
358    top_level = @store.add_file filename, relative_path.to_s
359
360    parser = RDoc::Parser.for top_level, filename, content, @options, @stats
361
362    return unless parser
363
364    parser.scan
365
366    # restart documentation for the classes & modules found
367    top_level.classes_or_modules.each do |cm|
368      cm.done_documenting = false
369    end
370
371    top_level
372
373  rescue Errno::EACCES => e
374    $stderr.puts <<-EOF
375Unable to read #{filename}, #{e.message}
376
377Please check the permissions for this file.  Perhaps you do not have access to
378it or perhaps the original author's permissions are to restrictive.  If the
379this is not your library please report a bug to the author.
380    EOF
381  rescue => e
382    $stderr.puts <<-EOF
383Before reporting this, could you check that the file you're documenting
384has proper syntax:
385
386  #{Gem.ruby} -c #{filename}
387
388RDoc is not a full Ruby parser and will fail when fed invalid ruby programs.
389
390The internal error was:
391
392\t(#{e.class}) #{e.message}
393
394    EOF
395
396    $stderr.puts e.backtrace.join("\n\t") if $DEBUG_RDOC
397
398    raise e
399    nil
400  end
401
402  ##
403  # Parse each file on the command line, recursively entering directories.
404
405  def parse_files files
406    file_list = gather_files files
407    @stats = RDoc::Stats.new @store, file_list.length, @options.verbosity
408
409    return [] if file_list.empty?
410
411    file_info = []
412
413    @stats.begin_adding
414
415    file_info = file_list.map do |filename|
416      @current = filename
417      parse_file filename
418    end.compact
419
420    @stats.done_adding
421
422    file_info
423  end
424
425  ##
426  # Removes file extensions known to be unparseable from +files+ and TAGS
427  # files for emacs and vim.
428
429  def remove_unparseable files
430    files.reject do |file|
431      file =~ /\.(?:class|eps|erb|scpt\.txt|ttf|yml)$/i or
432        (file =~ /tags$/i and
433         open(file, 'rb') { |io|
434           io.read(100) =~ /\A(\f\n[^,]+,\d+$|!_TAG_)/
435         })
436    end
437  end
438
439  ##
440  # Generates documentation or a coverage report depending upon the settings
441  # in +options+.
442  #
443  # +options+ can be either an RDoc::Options instance or an array of strings
444  # equivalent to the strings that would be passed on the command line like
445  # <tt>%w[-q -o doc -t My\ Doc\ Title]</tt>.  #document will automatically
446  # call RDoc::Options#finish if an options instance was given.
447  #
448  # For a list of options, see either RDoc::Options or <tt>rdoc --help</tt>.
449  #
450  # By default, output will be stored in a directory called "doc" below the
451  # current directory, so make sure you're somewhere writable before invoking.
452
453  def document options
454    self.store = RDoc::Store.new
455
456    if RDoc::Options === options then
457      @options = options
458      @options.finish
459    else
460      @options = load_options
461      @options.parse options
462    end
463
464    if @options.pipe then
465      handle_pipe
466      exit
467    end
468
469    @exclude = @options.exclude
470
471    unless @options.coverage_report then
472      @last_modified = setup_output_dir @options.op_dir, @options.force_update
473    end
474
475    @store.encoding = @options.encoding if @options.respond_to? :encoding
476    @store.dry_run  = @options.dry_run
477    @store.main     = @options.main_page
478    @store.title    = @options.title
479    @store.path     = @options.op_dir
480
481    @start_time = Time.now
482
483    @store.load_cache
484
485    file_info = parse_files @options.files
486
487    @options.default_title = "RDoc Documentation"
488
489    @store.complete @options.visibility
490
491    @stats.coverage_level = @options.coverage_report
492
493    if @options.coverage_report then
494      puts
495
496      puts @stats.report
497    elsif file_info.empty? then
498      $stderr.puts "\nNo newer files." unless @options.quiet
499    else
500      gen_klass = @options.generator
501
502      @generator = gen_klass.new @store, @options
503
504      generate
505    end
506
507    if @stats and (@options.coverage_report or not @options.quiet) then
508      puts
509      puts @stats.summary
510    end
511
512    exit @stats.fully_documented? if @options.coverage_report
513  end
514
515  ##
516  # Generates documentation for +file_info+ (from #parse_files) into the
517  # output dir using the generator selected
518  # by the RDoc options
519
520  def generate
521    Dir.chdir @options.op_dir do
522      unless @options.quiet then
523        $stderr.puts "\nGenerating #{@generator.class.name.sub(/^.*::/, '')} format into #{Dir.pwd}..."
524      end
525
526      @generator.generate
527      update_output_dir '.', @start_time, @last_modified
528    end
529  end
530
531  ##
532  # Removes a siginfo handler and replaces the previous
533
534  def remove_siginfo_handler
535    return unless Signal.list.key? 'INFO'
536
537    handler = @old_siginfo || 'DEFAULT'
538
539    trap 'INFO', handler
540  end
541
542end
543
544begin
545  require 'rubygems'
546
547  if Gem.respond_to? :find_files then
548    rdoc_extensions = Gem.find_files 'rdoc/discover'
549
550    rdoc_extensions.each do |extension|
551      begin
552        load extension
553      rescue => e
554        warn "error loading #{extension.inspect}: #{e.message} (#{e.class})"
555        warn "\t#{e.backtrace.join "\n\t"}" if $DEBUG
556      end
557    end
558  end
559rescue LoadError
560end
561
562# require built-in generators after discovery in case they've been replaced
563require 'rdoc/generator/darkfish'
564require 'rdoc/generator/ri'
565
566