#!/usr/local/bin/ruby
# :include: ../README
#
# ----
#
# = Usage
#
# RDoc is invoked from the command line using:
#
#   % rdoc <options> [name...]
#
# Files are parsed, and the information they contain collected, before
# any output is produced. This allows cross references between all files
# to be resolved. If a name is a directory, it is traversed. If no
# names are specified, all Ruby files in the current directory (and
# subdirectories) are processed.
#
# Options are:
#
# <tt>--all</tt>::         include protected and private methods in the output
#                          (by default only public methods are included)
#
# <tt>--main</tt> _name_:: set the class, module, or file to appear
#                          on the index page
#
# <tt>--quiet</tt>::       do not display progress messages
#
# <tt>--op</tt> _dir_::    set the output directory to _dir_ (the default is
#                          the directory "doc")                      
#
# <tt>--fmt</tt> _fmt_::   generate output in a particular format. Valid
#                          formats are: HTML
#
# <tt>--include</tt> <i>dir,...</i>::   
#                          specify one or more directories to be
#                          searched when satifying :include:
#                          directives. Multiple <tt>--include</tt>
#                          options may be given. The directory
#                          containing the file currently being
#                          processed is always searched.
#
# <tt>--show_hash</tt>::   A name of the form #name in a comment is 
#                          a possible hyperlink to an instance method
#                          name. When displayed, the '#' is removed
#                          unless this option is specified
#
# <tt>--template</tt> <i>name</i>::
#                          specify an alternate template to use when
#                          generating output (the default is
#                          'standard'). This template should be in a
#                          directory accessible via $: as
#                          rdoc/generators/xxxx_template, where 'xxxx'
#                          depends on the output formatter.
#
# <tt>--diagram</tt>::     include diagrams showing modules and classes.
#                          This is currently an experimental feature,
#                          and may not be supported by all output templates.
#
# = Example
#
# A typical small Ruby program commented using RDoc might be as follows. You
# can see the formatted result in Anagram.
#
#       :include:../EXAMPLE.rb
#
# = Markup
#
# Comment blocks can be written fairly naturally. 
#
# Paragraphs are lines that share the left 
# margin. Text indented past this margin are
# formatted verbatim.
#
# 1. Lists are typed as indented paragraphs with:
#    * a '*' or '-' (for bullet lists)
#    * a digit followed by a period for 
#      numbered lists
#
#    For example, the input that produced the above paragraph looked like
#        1. Lists are typed as indented 
#           paragraphs with:
#           * a '*' or '-' (for bullet lists)
#           * a digit followed by a period for 
#             numbered lists
#
#
# 2. Labeled lists (sometimes called description
#    lists are typed using square brackets for the label.
#       [cat]   small domestic animal
#       [+cat+] command to copy standard input
#
# 2. Labeled lists may also be produced by putting a double colon
#    after the label. This sets the result in tabular form, so the
#    descriptions all line up. This was used to create the 'author'
#    block at the bottom of this description.
#       cat::   small domestic animal
#       +cat+:: command to copy standard input
#
#    For both kinds of labeled lists, if the body text starts on the same
#    line as the label, then the start of that text determines the block
#    indent for the rest of the body. The text may also start on the line
#    following the label, indented from the start of the label. This is
#    often preferable if the label is long. Both the following are
#    valid labeled list entries:
#
#       <tt>--output</tt> <i>name [, name]</i>::
#           specify the name of one or more output files. If multiple
#           files are present, the first is used as the index.
#
#       <tt>--quiet:</tt>:: do not output the names, sizes, byte counts,
#                           index areas, or bit ratios of units as
#                           they are processed.
#
# 3. Headings are entered using equals signs
#
#      = Level One Heading
#      == Level Two Heading
#      and so on
#
# 4. Rules (horizontal lines) are entered using three or
#    more hyphens.
#
# 5. Non-verbatim text can be marked up:
#
#    _italic_::     \_word_ or \<em>text</em>
#    *bold*::       \*word* or \<b>text</b>
#    +typewriter+:: \+word+ or \<tt>text</tt>
#
#    Putting a backslash before inline markup stops it
#    being interpreted, which is how I created the table above:
#
#      _italic_::     \_word_ or \<em>text</em>
#      *bold*::       \*word* or \<b>text</b>
#      +typewriter+:: \+word+ or \<tt>text</tt>
#
# 6. Names of classes, source files, and any method names
#    containing an underscore or preceded by a hash
#    character are automatically hyperlinked from
#    comment text to their description. 
#
# 7. Hyperlinks to the web starting http:, mailto:, ftp:, or
#    www. are recognized. An HTTP url that references an
#    external image file is converted into an inline <IMG..>.
#       
# 8. Method parameter lists are extracted and displayed with
#    the method description. If a method calls +yield+, then
#    the parameters passed to yield will also be displayed:
#
#       def fred
#         ...
#         yield line, address
#
#    Will get documented as
#
#       fred() { |line, address| ... }
#
#    You can override this using a comment containing 
#    ':yields: ...' immediately after the method definition
#
#       def fred      # :yields: index, position
#         ...
#         yield line, address
#
#    Will get documented as
#
#        fred() { |index, position| ... }
#
#
# 9. ':yields:' is an example of a documentation modifier. These appear
#    immediately after the start of the document element they are modifying.
#    Other modifiers include
#
#    [<tt>:nodoc:</tt><i>[all]</i>]
#            don't include this element in the documentation.  For
#            classes and modules, methods, aliases, and attributes
#            directly within the affect class will also be omitted.
#            By default, though, modules and classes within that class
#            of module _will_ be documented. This is turned off by
#            adding the +all+ modifier.
#
#               module SM  #:nodoc:
#                 class Input
#                 end
#               end
#               module Markup #:nodoc: all
#                 class Output
#                 end
#               end
#
#            In the above code, only class <tt>SM::Input</tt> will be
#            documented.
#
# 9. RDoc stops processing comments if it finds a comment
#    line containing '<tt>#--</tt>'. This can be used to 
#    separate external from internal comments, or 
#    to stop a comment being associated with a method, 
#    class, or module. Commenting can be turned back on with
#    a line that starts '<tt>#++</tt>'.
#
#        # Extract the age and calculate the
#        # date-of-birth.
#        #--
#        # FIXME: fails if the birthday falls on
#        # February 29th
#        #++
#        # The DOB is returned as a Time object.
#
#        def get_dob(person)
#           ...
#
# 10. Comment blocks can contain other directives:
#     [<tt>:include:</tt><i>filename</i>] 
#            include the contents of the named file at this point. The
#            file will be searched for in the directories listed by
#            the <tt>--include</tt> option, or in the current
#            directory by default.  The contents of the file will be
#            shifted to have the same indentation as the ':' at the
#            start of the :include: directive.
#
# ---
# See also markup/simple_markup.rb.
#
# = Other stuff
#
# Author::   Dave Thomas <dave@pragmaticprogrammer.com>
# Requires:: Ruby 1.6.5 or later
# License::  Copyright (c) 2001 Dave Thomas.
#            Released under the same license as Ruby.
 
RDOC_VERSION = "alpha-a"
VERSION_STRING = %{RDoc V} + RDOC_VERSION


require 'rdoc/parse.rb'
require 'rdoc/options'

require 'rdoc/diagram'

require 'find'
require 'ftools'

# We put rdoc stuff in the RDoc module to avoid namespace
# clutter.
#
# ToDo: This isn't universally true.

module RDoc

  # 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, :desc)
    
    GENERATORS = {}
    $:.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.upcase)
        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 <tt>created.rid</tt>
    # then we refuse to use it, as we may clobber some
    # manually generated documentation
    
    def setup_output_dir(op_dir)
      flag_file = File.join(op_dir, "created.rid")
      if File.exist?(op_dir)
        unless File.directory?(op_dir)
          error "'#{op_dir}' exists, and is not a directory" 
        end
        unless File.file?(flag_file)
          error "Directory #{op_dir} is not a 'rid' directory"
        end
      else
        File.makedirs(op_dir)
      end
      File.open(flag_file, "w") {|f| f.puts Time.now }
    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)
      
      options = Options.instance
      options.parse(argv, GENERATORS)
      
      # Parse each file on the command line, recursively entering
      # directories
      
      file_info = []
      
      files = options.files
      files = ["."] if files.empty?
      
      Find.find(*files) do |fn|
        next unless /rb$/ =~ fn
        next unless File.file?(fn)
        
        fn.sub!( %r{^\./}, '')
        
        if !options.quiet
          puts
          printf "%35s: ", fn
        end

        begin
          content = File.open(fn, "r") {|f| f.read}
          parser = Parser.new(fn, content, options)
          file_info << parser.scan
        rescue
          $stderr.puts "Error opening file #{fn}. Skipping..."
        end
      end
      
      gen = options.generator
      
      if !options.quiet
        puts
        puts "Generating #{gen.desc}..."
      end
      
      load gen.file_name
      
      gen_class = Generators.const_get(gen.class_name)
      
      unless file_info.empty?
        gen = gen_class.new(options)

        setup_output_dir(options.op_dir)

        pwd = Dir.pwd
        Dir.chdir(options.op_dir)

        begin
          Diagram.new(file_info, options).draw if options.diagram
          
          gen.generate(file_info)
        ensure
          Dir.chdir(pwd)
        end

      end
    end
  end

end

#
# When run as a main program
#

if __FILE__ == $0

  begin
    r = RDoc::RDoc.new
    r.document(ARGV)
  rescue RDoc::RDocError => e
    $stderr.puts e.message
    exit(1)
  end

end
