# See README. # VERSION_STRING = %{RDoc V1.0.1 - 20041108} require 'rdoc/parsers/parse_rb.rb' require 'rdoc/parsers/parse_c.rb' require 'rdoc/parsers/parse_f95.rb' require 'rdoc/parsers/parse_simple.rb' require 'rdoc/options' require 'rdoc/diagram' require 'find' require 'ftools' require 'time' # We put rdoc stuff in the RDoc module to avoid namespace # clutter. # # ToDo: This isn't universally true. # # :include: README module RDoc # Name of the dotfile that contains the description of files to be # processed in the current directory DOT_DOC_FILENAME = ".document" # Simple stats collector class Stats attr_accessor :num_files, :num_classes, :num_modules, :num_methods def initialize @num_files = @num_classes = @num_modules = @num_methods = 0 @start = Time.now end def print puts "Files: #@num_files" puts "Classes: #@num_classes" puts "Modules: #@num_modules" puts "Methods: #@num_methods" puts "Elapsed: " + sprintf("%0.3fs", Time.now - @start) end end # Exception thrown by any rdoc error. Only the #message part is # of use externally. class RDocError < Exception end # Encapsulate the production of rdoc documentation. Basically # you can use this as you would invoke rdoc from the command # line: # # rdoc = RDoc::RDoc.new # rdoc.document(args) # # where _args_ is an array of strings, each corresponding to # an argument you'd give rdoc on the command line. See rdoc/rdoc.rb # for details. class RDoc ## # This is the list of output generators that we # support Generator = Struct.new(:file_name, :class_name, :key) GENERATORS = {} $:.collect {|d| File::expand_path(d) }.find_all {|d| File::directory?("#{d}/rdoc/generators") }.each {|dir| Dir::entries("#{dir}/rdoc/generators").each {|gen| next unless /(\w+)_generator.rb$/ =~ gen type = $1 unless GENERATORS.has_key? type GENERATORS[type] = Generator.new("rdoc/generators/#{gen}", "#{type.upcase}Generator".intern, type) end } } ####### private ####### ## # Report an error message and exit def error(msg) raise RDocError.new(msg) end ## # Create an output dir if it doesn't exist. If it does # exist, but doesn't contain the flag file created.rid # then we refuse to use it, as we may clobber some # manually generated documentation def setup_output_dir(op_dir, force) flag_file = output_flag_file(op_dir) if File.exist?(op_dir) unless File.directory?(op_dir) error "'#{op_dir}' exists, and is not a directory" end begin created = File.read(flag_file) rescue SystemCallError error "\nDirectory #{op_dir} already exists, but it looks like it\n" + "isn't an RDoc directory. Because RDoc doesn't want to risk\n" + "destroying any of your existing files, you'll need to\n" + "specify a different output directory name (using the\n" + "--op option).\n\n" else last = (Time.parse(created) unless force rescue nil) end else File.makedirs(op_dir) end last end # Update the flag file in an output directory. def update_output_dir(op_dir, time) File.open(output_flag_file(op_dir), "w") {|f| f.puts time.rfc2822 } 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, options) # 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(options, 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 explicity. # def normalized_file_list(options, 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) case type = stat.ftype when "file" next if @last_created and stat.mtime < @last_created file_list << rel_file_name.sub(/^\.\//, '') if force_doc || ParserFactory.can_parse(rel_file_name) when "directory" next if rel_file_name == "CVS" || rel_file_name == ".svn" dot_doc = File.join(rel_file_name, DOT_DOC_FILENAME) if File.file?(dot_doc) file_list.concat(parse_dot_doc_file(rel_file_name, dot_doc, options)) else file_list.concat(list_files_in_directory(rel_file_name, options)) end else raise RDocError.new("I can't deal with a #{type} #{rel_file_name}") end end file_list 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, options) normalized_file_list(options, Dir.glob(File.join(dir, "*")), false, options.exclude) end # Parse each file on the command line, recursively entering # directories def parse_files(options) file_info = [] files = options.files files = ["."] if files.empty? file_list = normalized_file_list(options, files, true) file_list.each do |fn| $stderr.printf("\n%35s: ", File.basename(fn)) unless options.quiet content = File.open(fn, "r") {|f| f.read} top_level = TopLevel.new(fn) parser = ParserFactory.parser_for(top_level, fn, content, options, @stats) file_info << parser.scan @stats.num_files += 1 end file_info end public ################################################################### # # Format up one or more files according to the given arguments. # For simplicity, _argv_ is an array of strings, equivalent to the # strings that would be passed on the command line. (This isn't a # coincidence, as we _do_ pass in ARGV when running # interactively). For a list of options, see rdoc/rdoc.rb. By # default, output will be stored in a directory called +doc+ below # the current directory, so make sure you're somewhere writable # before invoking. # # Throws: RDocError on error def document(argv) TopLevel::reset @stats = Stats.new options = Options.instance options.parse(argv, GENERATORS) @last_created = nil unless options.all_one_file @last_created = setup_output_dir(options.op_dir, options.force_update) end start_time = Time.now file_info = parse_files(options) if file_info.empty? $stderr.puts "\nNo newer files." unless options.quiet else gen = options.generator $stderr.puts "\nGenerating #{gen.key.upcase}..." unless options.quiet require gen.file_name gen_class = Generators.const_get(gen.class_name) gen = gen_class.for(options) pwd = Dir.pwd Dir.chdir(options.op_dir) unless options.all_one_file begin Diagram.new(file_info, options).draw if options.diagram gen.generate(file_info) update_output_dir(".", start_time) ensure Dir.chdir(pwd) end end unless options.quiet puts @stats.print end end end end