123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532 |
- require 'rdoc'
- require 'find'
- require 'fileutils'
- require 'time'
- ##
- # This is the driver for generating RDoc output. It file parsing and
- # generation of output.
- #
- # To use this class to generate RDoc output via the API, the recommended way
- # is:
- #
- # rdoc = RDoc::RDoc.new
- # options = rdoc.load_options # returns an RDoc::Options instance
- # # set extra options
- # rdoc.document options
- #
- # You can also generate output like the +rdoc+ executable:
- #
- # rdoc = RDoc::RDoc.new
- # rdoc.document argv
- #
- # Where +argv+ is an array of strings, each corresponding to an argument you'd
- # give rdoc on the command line. See <tt>rdoc --help<tt> for details.
- class RDoc::RDoc
- @current = nil
- ##
- # This is the list of supported output generators
- GENERATORS = {}
- ##
- # File pattern to exclude
- attr_accessor :exclude
- ##
- # Generator instance used for creating output
- attr_accessor :generator
- ##
- # Hash of files and their last modified times.
- attr_reader :last_modified
- ##
- # RDoc options
- attr_accessor :options
- ##
- # Accessor for statistics. Available after each call to parse_files
- attr_reader :stats
- ##
- # Add +klass+ that can generate output after parsing
- def self.add_generator(klass)
- name = klass.name.sub(/^RDoc::Generator::/, '').downcase
- GENERATORS[name] = klass
- end
- ##
- # Active RDoc::RDoc instance
- def self.current
- @current
- end
- ##
- # Sets the active RDoc::RDoc instance
- def self.current= rdoc
- @current = rdoc
- end
- ##
- # Resets all internal state
- def self.reset
- RDoc::TopLevel.reset
- RDoc::Parser::C.reset
- RDoc::RDoc.current = nil
- end
- ##
- # Creates a new RDoc::RDoc instance. Call #document to parse files and
- # generate documentation.
- def initialize
- @current = nil
- @exclude = nil
- @generator = nil
- @last_modified = {}
- @old_siginfo = nil
- @options = nil
- @stats = nil
- end
- ##
- # Report an error message and exit
- def error(msg)
- raise RDoc::Error, msg
- end
- ##
- # Gathers a set of parseable files from the files and directories listed in
- # +files+.
- def gather_files files
- files = ["."] if files.empty?
- file_list = normalized_file_list files, true, @exclude
- file_list = file_list.uniq
- file_list = remove_unparseable file_list
- end
- ##
- # Turns RDoc from stdin into HTML
- def handle_pipe
- @html = RDoc::Markup::ToHtml.new
- parser = RDoc::Text::MARKUP_FORMAT[@options.markup]
- document = parser.parse $stdin.read
- out = @html.convert document
- $stdout.write out
- end
- ##
- # Installs a siginfo handler that prints the current filename.
- def install_siginfo_handler
- return unless Signal.list.include? 'INFO'
- @old_siginfo = trap 'INFO' do
- puts @current if @current
- end
- end
- ##
- # Loads options from .rdoc_options if the file exists, otherwise creates a
- # new RDoc::Options instance.
- def load_options
- options_file = File.expand_path '.rdoc_options'
- return RDoc::Options.new unless File.exist? options_file
- RDoc.load_yaml
- parse_error = if Object.const_defined? :Psych then
- Psych::SyntaxError
- else
- ArgumentError
- end
- begin
- options = YAML.load_file '.rdoc_options'
- rescue *parse_error
- end
- raise RDoc::Error, "#{options_file} is not a valid rdoc options file" unless
- RDoc::Options === options
- options
- end
- ##
- # Create an output dir if it doesn't exist. If it does exist, but doesn't
- # contain the flag file <tt>created.rid</tt> then we refuse to use it, as
- # we may clobber some manually generated documentation
- def setup_output_dir(dir, force)
- flag_file = output_flag_file dir
- last = {}
- if @options.dry_run then
- # do nothing
- elsif File.exist? dir then
- error "#{dir} exists and is not a directory" unless File.directory? dir
- begin
- open flag_file do |io|
- unless force then
- Time.parse io.gets
- io.each do |line|
- file, time = line.split "\t", 2
- time = Time.parse(time) rescue next
- last[file] = time
- end
- end
- end
- rescue SystemCallError, TypeError
- error <<-ERROR
- Directory #{dir} already exists, but it looks like it isn't an RDoc directory.
- Because RDoc doesn't want to risk destroying any of your existing files,
- you'll need to specify a different output directory name (using the --op <dir>
- option)
- ERROR
- end unless @options.force_output
- else
- FileUtils.mkdir_p dir
- FileUtils.touch output_flag_file dir
- end
- last
- end
- ##
- # Update the flag file in an output directory.
- def update_output_dir(op_dir, time, last = {})
- return if @options.dry_run or not @options.update_output_dir
- open output_flag_file(op_dir), "w" do |f|
- f.puts time.rfc2822
- last.each do |n, t|
- f.puts "#{n}\t#{t.rfc2822}"
- end
- end
- end
- ##
- # Return the path name of the flag file in an output directory.
- def output_flag_file(op_dir)
- File.join op_dir, "created.rid"
- end
- ##
- # The .document file contains a list of file and directory name patterns,
- # representing candidates for documentation. It may also contain comments
- # (starting with '#')
- def parse_dot_doc_file in_dir, filename
- # read and strip comments
- patterns = File.read(filename).gsub(/#.*/, '')
- result = []
- patterns.split.each do |patt|
- candidates = Dir.glob(File.join(in_dir, patt))
- result.concat normalized_file_list(candidates)
- end
- result
- end
- ##
- # Given a list of files and directories, create a list of all the Ruby
- # files they contain.
- #
- # If +force_doc+ is true we always add the given files, if false, only
- # add files that we guarantee we can parse. It is true when looking at
- # files given on the command line, false when recursing through
- # subdirectories.
- #
- # The effect of this is that if you want a file with a non-standard
- # extension parsed, you must name it explicitly.
- def normalized_file_list(relative_files, force_doc = false,
- exclude_pattern = nil)
- file_list = []
- relative_files.each do |rel_file_name|
- next if exclude_pattern && exclude_pattern =~ rel_file_name
- stat = File.stat rel_file_name rescue next
- case type = stat.ftype
- when "file" then
- next if last_modified = @last_modified[rel_file_name] and
- stat.mtime.to_i <= last_modified.to_i
- if force_doc or RDoc::Parser.can_parse(rel_file_name) then
- file_list << rel_file_name.sub(/^\.\//, '')
- @last_modified[rel_file_name] = stat.mtime
- end
- when "directory" then
- next if rel_file_name == "CVS" || rel_file_name == ".svn"
- dot_doc = File.join rel_file_name, RDoc::DOT_DOC_FILENAME
- if File.file? dot_doc then
- file_list << parse_dot_doc_file(rel_file_name, dot_doc)
- else
- file_list << list_files_in_directory(rel_file_name)
- end
- else
- raise RDoc::Error, "I can't deal with a #{type} #{rel_file_name}"
- end
- end
- file_list.flatten
- end
- ##
- # Return a list of the files to be processed in a directory. We know that
- # this directory doesn't have a .document file, so we're looking for real
- # files. However we may well contain subdirectories which must be tested
- # for .document files.
- def list_files_in_directory dir
- files = Dir.glob File.join(dir, "*")
- normalized_file_list files, false, @options.exclude
- end
- ##
- # Parses +filename+ and returns an RDoc::TopLevel
- def parse_file filename
- if defined?(Encoding) then
- encoding = @options.encoding
- filename = filename.encode encoding
- end
- @stats.add_file filename
- content = RDoc::Encoding.read_file filename, encoding
- return unless content
- top_level = RDoc::TopLevel.new filename
- parser = RDoc::Parser.for top_level, filename, content, @options, @stats
- return unless parser
- parser.scan
- # restart documentation for the classes & modules found
- top_level.classes_or_modules.each do |cm|
- cm.done_documenting = false
- end
- top_level
- rescue => e
- $stderr.puts <<-EOF
- Before reporting this, could you check that the file you're documenting
- has proper syntax:
- #{Gem.ruby} -c #{filename}
- RDoc is not a full Ruby parser and will fail when fed invalid ruby programs.
- The internal error was:
- \t(#{e.class}) #{e.message}
- EOF
- $stderr.puts e.backtrace.join("\n\t") if $DEBUG_RDOC
- raise e
- nil
- end
- ##
- # Parse each file on the command line, recursively entering directories.
- def parse_files files
- file_list = gather_files files
- @stats = RDoc::Stats.new file_list.size, @options.verbosity
- return [] if file_list.empty?
- file_info = []
- @stats.begin_adding
- file_info = file_list.map do |filename|
- @current = filename
- parse_file filename
- end.compact
- @stats.done_adding
- file_info
- end
- ##
- # Removes file extensions known to be unparseable from +files+ and TAGS
- # files for emacs and vim.
- def remove_unparseable files
- files.reject do |file|
- file =~ /\.(?:class|eps|erb|scpt\.txt|ttf|yml)$/i or
- (file =~ /tags$/i and
- open(file, 'rb') { |io|
- io.read(100) =~ /\A(\f\n[^,]+,\d+$|!_TAG_)/
- })
- end
- end
- ##
- # Generates documentation or a coverage report depending upon the settings
- # in +options+.
- #
- # +options+ can be either an RDoc::Options instance or an array of strings
- # equivalent to the strings that would be passed on the command line like
- # <tt>%w[-q -o doc -t My\ Doc\ Title]</tt>. #document will automatically
- # call RDoc::Options#finish if an options instance was given.
- #
- # For a list of options, see either RDoc::Options or <tt>rdoc --help</tt>.
- #
- # By default, output will be stored in a directory called "doc" below the
- # current directory, so make sure you're somewhere writable before invoking.
- def document options
- RDoc::RDoc.reset
- RDoc::RDoc.current = self
- if RDoc::Options === options then
- @options = options
- @options.finish
- else
- @options = load_options
- @options.parse options
- end
- if @options.pipe then
- handle_pipe
- exit
- end
- @exclude = @options.exclude
- unless @options.coverage_report then
- @last_modified = setup_output_dir @options.op_dir, @options.force_update
- end
- @start_time = Time.now
- file_info = parse_files @options.files
- @options.default_title = "RDoc Documentation"
- RDoc::TopLevel.complete @options.visibility
- @stats.coverage_level = @options.coverage_report
- if @options.coverage_report then
- puts
- puts @stats.report
- elsif file_info.empty? then
- $stderr.puts "\nNo newer files." unless @options.quiet
- else
- gen_klass = @options.generator
- @generator = gen_klass.new @options
- generate file_info
- end
- if @stats and (@options.coverage_report or not @options.quiet) then
- puts
- puts @stats.summary
- end
- exit @stats.fully_documented? if @options.coverage_report
- end
- ##
- # Generates documentation for +file_info+ (from #parse_files) into the
- # output dir using the generator selected
- # by the RDoc options
- def generate file_info
- Dir.chdir @options.op_dir do
- unless @options.quiet then
- $stderr.puts "\nGenerating #{@generator.class.name.sub(/^.*::/, '')} format into #{Dir.pwd}..."
- end
- @generator.generate file_info
- update_output_dir '.', @start_time, @last_modified
- end
- end
- ##
- # Removes a siginfo handler and replaces the previous
- def remove_siginfo_handler
- return unless Signal.list.key? 'INFO'
- handler = @old_siginfo || 'DEFAULT'
- trap 'INFO', handler
- end
- end
- begin
- require 'rubygems'
- if Gem.respond_to? :find_files then
- rdoc_extensions = Gem.find_files 'rdoc/discover'
- rdoc_extensions.each do |extension|
- begin
- load extension
- rescue => e
- warn "error loading #{extension.inspect}: #{e.message} (#{e.class})"
- warn "\t#{e.backtrace.join "\n\t"}" if $DEBUG
- end
- end
- end
- rescue LoadError
- end
- # require built-in generators after discovery in case they've been replaced
- require 'rdoc/generator/darkfish'
- require 'rdoc/generator/ri'
|