1 | # See README.
|
---|
2 | #
|
---|
3 |
|
---|
4 |
|
---|
5 | VERSION_STRING = %{RDoc V1.0.1 - 20041108}
|
---|
6 |
|
---|
7 |
|
---|
8 | require 'rdoc/parsers/parse_rb.rb'
|
---|
9 | require 'rdoc/parsers/parse_c.rb'
|
---|
10 | require 'rdoc/parsers/parse_f95.rb'
|
---|
11 |
|
---|
12 | require 'rdoc/parsers/parse_simple.rb'
|
---|
13 | require 'rdoc/options'
|
---|
14 |
|
---|
15 | require 'rdoc/diagram'
|
---|
16 |
|
---|
17 | require 'find'
|
---|
18 | require 'ftools'
|
---|
19 | require 'time'
|
---|
20 |
|
---|
21 | # We put rdoc stuff in the RDoc module to avoid namespace
|
---|
22 | # clutter.
|
---|
23 | #
|
---|
24 | # ToDo: This isn't universally true.
|
---|
25 | #
|
---|
26 | # :include: README
|
---|
27 |
|
---|
28 | module RDoc
|
---|
29 |
|
---|
30 | # Name of the dotfile that contains the description of files to be
|
---|
31 | # processed in the current directory
|
---|
32 | DOT_DOC_FILENAME = ".document"
|
---|
33 |
|
---|
34 | # Simple stats collector
|
---|
35 | class Stats
|
---|
36 | attr_accessor :num_files, :num_classes, :num_modules, :num_methods
|
---|
37 | def initialize
|
---|
38 | @num_files = @num_classes = @num_modules = @num_methods = 0
|
---|
39 | @start = Time.now
|
---|
40 | end
|
---|
41 | def print
|
---|
42 | puts "Files: #@num_files"
|
---|
43 | puts "Classes: #@num_classes"
|
---|
44 | puts "Modules: #@num_modules"
|
---|
45 | puts "Methods: #@num_methods"
|
---|
46 | puts "Elapsed: " + sprintf("%0.3fs", Time.now - @start)
|
---|
47 | end
|
---|
48 | end
|
---|
49 |
|
---|
50 |
|
---|
51 | # Exception thrown by any rdoc error. Only the #message part is
|
---|
52 | # of use externally.
|
---|
53 |
|
---|
54 | class RDocError < Exception
|
---|
55 | end
|
---|
56 |
|
---|
57 | # Encapsulate the production of rdoc documentation. Basically
|
---|
58 | # you can use this as you would invoke rdoc from the command
|
---|
59 | # line:
|
---|
60 | #
|
---|
61 | # rdoc = RDoc::RDoc.new
|
---|
62 | # rdoc.document(args)
|
---|
63 | #
|
---|
64 | # where _args_ is an array of strings, each corresponding to
|
---|
65 | # an argument you'd give rdoc on the command line. See rdoc/rdoc.rb
|
---|
66 | # for details.
|
---|
67 |
|
---|
68 | class RDoc
|
---|
69 |
|
---|
70 | ##
|
---|
71 | # This is the list of output generators that we
|
---|
72 | # support
|
---|
73 |
|
---|
74 | Generator = Struct.new(:file_name, :class_name, :key)
|
---|
75 |
|
---|
76 | GENERATORS = {}
|
---|
77 | $:.collect {|d|
|
---|
78 | File::expand_path(d)
|
---|
79 | }.find_all {|d|
|
---|
80 | File::directory?("#{d}/rdoc/generators")
|
---|
81 | }.each {|dir|
|
---|
82 | Dir::entries("#{dir}/rdoc/generators").each {|gen|
|
---|
83 | next unless /(\w+)_generator.rb$/ =~ gen
|
---|
84 | type = $1
|
---|
85 | unless GENERATORS.has_key? type
|
---|
86 | GENERATORS[type] = Generator.new("rdoc/generators/#{gen}",
|
---|
87 | "#{type.upcase}Generator".intern,
|
---|
88 | type)
|
---|
89 | end
|
---|
90 | }
|
---|
91 | }
|
---|
92 |
|
---|
93 | #######
|
---|
94 | private
|
---|
95 | #######
|
---|
96 |
|
---|
97 | ##
|
---|
98 | # Report an error message and exit
|
---|
99 |
|
---|
100 | def error(msg)
|
---|
101 | raise RDocError.new(msg)
|
---|
102 | end
|
---|
103 |
|
---|
104 | ##
|
---|
105 | # Create an output dir if it doesn't exist. If it does
|
---|
106 | # exist, but doesn't contain the flag file <tt>created.rid</tt>
|
---|
107 | # then we refuse to use it, as we may clobber some
|
---|
108 | # manually generated documentation
|
---|
109 |
|
---|
110 | def setup_output_dir(op_dir, force)
|
---|
111 | flag_file = output_flag_file(op_dir)
|
---|
112 | if File.exist?(op_dir)
|
---|
113 | unless File.directory?(op_dir)
|
---|
114 | error "'#{op_dir}' exists, and is not a directory"
|
---|
115 | end
|
---|
116 | begin
|
---|
117 | created = File.read(flag_file)
|
---|
118 | rescue SystemCallError
|
---|
119 | error "\nDirectory #{op_dir} already exists, but it looks like it\n" +
|
---|
120 | "isn't an RDoc directory. Because RDoc doesn't want to risk\n" +
|
---|
121 | "destroying any of your existing files, you'll need to\n" +
|
---|
122 | "specify a different output directory name (using the\n" +
|
---|
123 | "--op <dir> option).\n\n"
|
---|
124 | else
|
---|
125 | last = (Time.parse(created) unless force rescue nil)
|
---|
126 | end
|
---|
127 | else
|
---|
128 | File.makedirs(op_dir)
|
---|
129 | end
|
---|
130 | last
|
---|
131 | end
|
---|
132 |
|
---|
133 | # Update the flag file in an output directory.
|
---|
134 | def update_output_dir(op_dir, time)
|
---|
135 | File.open(output_flag_file(op_dir), "w") {|f| f.puts time.rfc2822 }
|
---|
136 | end
|
---|
137 |
|
---|
138 | # Return the path name of the flag file in an output directory.
|
---|
139 | def output_flag_file(op_dir)
|
---|
140 | File.join(op_dir, "created.rid")
|
---|
141 | end
|
---|
142 |
|
---|
143 | # The .document file contains a list of file and directory name
|
---|
144 | # patterns, representing candidates for documentation. It may
|
---|
145 | # also contain comments (starting with '#')
|
---|
146 | def parse_dot_doc_file(in_dir, filename, options)
|
---|
147 | # read and strip comments
|
---|
148 | patterns = File.read(filename).gsub(/#.*/, '')
|
---|
149 |
|
---|
150 | result = []
|
---|
151 |
|
---|
152 | patterns.split.each do |patt|
|
---|
153 | candidates = Dir.glob(File.join(in_dir, patt))
|
---|
154 | result.concat(normalized_file_list(options, candidates))
|
---|
155 | end
|
---|
156 | result
|
---|
157 | end
|
---|
158 |
|
---|
159 |
|
---|
160 | # Given a list of files and directories, create a list
|
---|
161 | # of all the Ruby files they contain.
|
---|
162 | #
|
---|
163 | # If +force_doc+ is true, we always add the given files.
|
---|
164 | # If false, only add files that we guarantee we can parse
|
---|
165 | # It is true when looking at files given on the command line,
|
---|
166 | # false when recursing through subdirectories.
|
---|
167 | #
|
---|
168 | # The effect of this is that if you want a file with a non-
|
---|
169 | # standard extension parsed, you must name it explicity.
|
---|
170 | #
|
---|
171 |
|
---|
172 | def normalized_file_list(options, relative_files, force_doc = false, exclude_pattern=nil)
|
---|
173 | file_list = []
|
---|
174 |
|
---|
175 | relative_files.each do |rel_file_name|
|
---|
176 | next if exclude_pattern && exclude_pattern =~ rel_file_name
|
---|
177 | stat = File.stat(rel_file_name)
|
---|
178 | case type = stat.ftype
|
---|
179 | when "file"
|
---|
180 | next if @last_created and stat.mtime < @last_created
|
---|
181 | file_list << rel_file_name.sub(/^\.\//, '') if force_doc || ParserFactory.can_parse(rel_file_name)
|
---|
182 | when "directory"
|
---|
183 | next if rel_file_name == "CVS" || rel_file_name == ".svn"
|
---|
184 | dot_doc = File.join(rel_file_name, DOT_DOC_FILENAME)
|
---|
185 | if File.file?(dot_doc)
|
---|
186 | file_list.concat(parse_dot_doc_file(rel_file_name, dot_doc, options))
|
---|
187 | else
|
---|
188 | file_list.concat(list_files_in_directory(rel_file_name, options))
|
---|
189 | end
|
---|
190 | else
|
---|
191 | raise RDocError.new("I can't deal with a #{type} #{rel_file_name}")
|
---|
192 | end
|
---|
193 | end
|
---|
194 | file_list
|
---|
195 | end
|
---|
196 |
|
---|
197 | # Return a list of the files to be processed in
|
---|
198 | # a directory. We know that this directory doesn't have
|
---|
199 | # a .document file, so we're looking for real files. However
|
---|
200 | # we may well contain subdirectories which must
|
---|
201 | # be tested for .document files
|
---|
202 | def list_files_in_directory(dir, options)
|
---|
203 | normalized_file_list(options, Dir.glob(File.join(dir, "*")), false, options.exclude)
|
---|
204 | end
|
---|
205 |
|
---|
206 |
|
---|
207 | # Parse each file on the command line, recursively entering
|
---|
208 | # directories
|
---|
209 |
|
---|
210 | def parse_files(options)
|
---|
211 |
|
---|
212 | file_info = []
|
---|
213 |
|
---|
214 | files = options.files
|
---|
215 | files = ["."] if files.empty?
|
---|
216 |
|
---|
217 | file_list = normalized_file_list(options, files, true)
|
---|
218 |
|
---|
219 | file_list.each do |fn|
|
---|
220 | $stderr.printf("\n%35s: ", File.basename(fn)) unless options.quiet
|
---|
221 |
|
---|
222 | content = File.open(fn, "r") {|f| f.read}
|
---|
223 |
|
---|
224 | top_level = TopLevel.new(fn)
|
---|
225 | parser = ParserFactory.parser_for(top_level, fn, content, options, @stats)
|
---|
226 | file_info << parser.scan
|
---|
227 | @stats.num_files += 1
|
---|
228 | end
|
---|
229 |
|
---|
230 | file_info
|
---|
231 | end
|
---|
232 |
|
---|
233 |
|
---|
234 | public
|
---|
235 |
|
---|
236 | ###################################################################
|
---|
237 | #
|
---|
238 | # Format up one or more files according to the given arguments.
|
---|
239 | # For simplicity, _argv_ is an array of strings, equivalent to the
|
---|
240 | # strings that would be passed on the command line. (This isn't a
|
---|
241 | # coincidence, as we _do_ pass in ARGV when running
|
---|
242 | # interactively). For a list of options, see rdoc/rdoc.rb. By
|
---|
243 | # default, output will be stored in a directory called +doc+ below
|
---|
244 | # the current directory, so make sure you're somewhere writable
|
---|
245 | # before invoking.
|
---|
246 | #
|
---|
247 | # Throws: RDocError on error
|
---|
248 |
|
---|
249 | def document(argv)
|
---|
250 |
|
---|
251 | TopLevel::reset
|
---|
252 |
|
---|
253 | @stats = Stats.new
|
---|
254 |
|
---|
255 | options = Options.instance
|
---|
256 | options.parse(argv, GENERATORS)
|
---|
257 |
|
---|
258 | @last_created = nil
|
---|
259 | unless options.all_one_file
|
---|
260 | @last_created = setup_output_dir(options.op_dir, options.force_update)
|
---|
261 | end
|
---|
262 | start_time = Time.now
|
---|
263 |
|
---|
264 | file_info = parse_files(options)
|
---|
265 |
|
---|
266 | if file_info.empty?
|
---|
267 | $stderr.puts "\nNo newer files." unless options.quiet
|
---|
268 | else
|
---|
269 | gen = options.generator
|
---|
270 |
|
---|
271 | $stderr.puts "\nGenerating #{gen.key.upcase}..." unless options.quiet
|
---|
272 |
|
---|
273 | require gen.file_name
|
---|
274 |
|
---|
275 | gen_class = Generators.const_get(gen.class_name)
|
---|
276 | gen = gen_class.for(options)
|
---|
277 |
|
---|
278 | pwd = Dir.pwd
|
---|
279 |
|
---|
280 | Dir.chdir(options.op_dir) unless options.all_one_file
|
---|
281 |
|
---|
282 | begin
|
---|
283 | Diagram.new(file_info, options).draw if options.diagram
|
---|
284 | gen.generate(file_info)
|
---|
285 | update_output_dir(".", start_time)
|
---|
286 | ensure
|
---|
287 | Dir.chdir(pwd)
|
---|
288 | end
|
---|
289 | end
|
---|
290 |
|
---|
291 | unless options.quiet
|
---|
292 | puts
|
---|
293 | @stats.print
|
---|
294 | end
|
---|
295 | end
|
---|
296 | end
|
---|
297 | end
|
---|
298 |
|
---|