summaryrefslogtreecommitdiff
path: root/lib/rdoc/rdoc.rb
blob: ca9c5da2bff43a771cdfcea00c1569f97e2060c0 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
# See README.
#
 

RDOC_VERSION = "1.0pr1"

rcs = '$Date$ $Revision$'.
  gsub(/\$/, '').
  sub(/Date: /, ': ').
  sub(/ Revision: (\S+)/) { "(#$1)" }

VERSION_STRING = %{RDoc V} + RDOC_VERSION + rcs


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'

# 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, :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 <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 "\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 <dir> option).\n\n"
        end
      else
        File.makedirs(op_dir)
      end
      File.open(flag_file, "w") {|f| f.puts Time.now }
    end
    

    # Given a list of files and directories, create a list
    # of all the Ruby files they contain. 
    
    def normalized_file_list(options, *relative_files)
      file_list = []

      relative_files.each do |rel_file_name|

        case type = File.stat(rel_file_name).ftype
        when "file"
          file_list << rel_file_name
        when "directory"
          next if options.exclude && options.exclude =~ rel_file_name
          Find.find(rel_file_name) do |fn|
            next if options.exclude && options.exclude =~ fn
            next unless ParserFactory.can_parse(fn)
            next unless File.file?(fn)
            
            file_list << fn.sub(%r{\./}, '')
          end
        else
          raise RDocError.new("I can't deal with a #{type} #{rel_file_name}")
        end
      end
      file_list
    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)

      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)
        file_info << parser.scan
      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

      options = Options.instance
      options.parse(argv, GENERATORS)
    
      file_info = parse_files(options)

      gen = options.generator
      
      $stderr.puts "\nGenerating #{gen.key.upcase}..." unless options.quiet
      
      require gen.file_name
      
      gen_class = Generators.const_get(gen.class_name)
      
      unless file_info.empty?
        gen = gen_class.for(options)

        pwd = Dir.pwd

        unless options.all_one_file
          setup_output_dir(options.op_dir)
          Dir.chdir(options.op_dir)
        end

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

      end
    end
  end
end