1 # frozen_string_literal: true
2 require_relative '../rdoc'
10 # This is the driver for generating RDoc output. It handles file parsing and
11 # generation of output.
13 # To use this class to generate RDoc output via the API, the recommended way
16 # rdoc = RDoc::RDoc.new
17 # options = RDoc::Options.load_options # returns an RDoc::Options instance
19 # rdoc.document options
21 # You can also generate output like the +rdoc+ executable:
23 # rdoc = RDoc::RDoc.new
26 # Where +argv+ is an array of strings, each corresponding to an argument you'd
27 # give rdoc on the command line. See <tt>rdoc --help</tt> for details.
34 # This is the list of supported output generators
39 # List of directory names always skipped
41 UNCONDITIONALLY_SKIPPED_DIRECTORIES = %w[CVS .svn .git].freeze
44 # List of directory names skipped if test suites should be skipped
46 TEST_SUITE_DIRECTORY_NAMES = %w[spec test].freeze
50 # Generator instance used for creating output
52 attr_accessor :generator
55 # Hash of files and their last modified times.
57 attr_reader :last_modified
62 attr_accessor :options
65 # Accessor for statistics. Available after each call to parse_files
70 # The current documentation store
75 # Add +klass+ that can generate output after parsing
77 def self.add_generator(klass)
78 name = klass.name.sub(/^RDoc::Generator::/, '').downcase
79 GENERATORS[name] = klass
83 # Active RDoc::RDoc instance
90 # Sets the active RDoc::RDoc instance
92 def self.current= rdoc
97 # Creates a new RDoc::RDoc instance. Call #document to parse files and
98 # generate documentation.
111 # Report an error message and exit
114 raise RDoc::Error, msg
118 # Gathers a set of parseable files from the files and directories listed in
121 def gather_files files
122 files = [@options.root.to_s] if files.empty?
124 file_list = normalized_file_list files, true, @options.exclude
126 file_list = remove_unparseable(file_list)
128 if file_list.count {|name, mtime|
129 file_list[name] = @last_modified[name] unless mtime
132 @last_modified.replace file_list
140 # Turns RDoc from stdin into HTML
143 @html = RDoc::Markup::ToHtml.new @options
145 parser = RDoc::Text::MARKUP_FORMAT[@options.markup]
147 document = parser.parse $stdin.read
149 out = @html.convert document
155 # Installs a siginfo handler that prints the current filename.
157 def install_siginfo_handler
158 return unless Signal.list.include? 'INFO'
160 @old_siginfo = trap 'INFO' do
161 puts @current if @current
166 # Create an output dir if it doesn't exist. If it does exist, but doesn't
167 # contain the flag file <tt>created.rid</tt> then we refuse to use it, as
168 # we may clobber some manually generated documentation
170 def setup_output_dir(dir, force)
171 flag_file = output_flag_file dir
175 if @options.dry_run then
177 elsif File.exist? dir then
178 error "#{dir} exists and is not a directory" unless File.directory? dir
181 File.open flag_file do |io|
186 file, time = line.split "\t", 2
187 time = Time.parse(time) rescue next
192 rescue SystemCallError, TypeError
195 Directory #{dir} already exists, but it looks like it isn't an RDoc directory.
197 Because RDoc doesn't want to risk destroying any of your existing files,
198 you'll need to specify a different output directory name (using the --op <dir>
202 end unless @options.force_output
204 FileUtils.mkdir_p dir
205 FileUtils.touch flag_file
212 # Sets the current documentation tree to +store+ and sets the store's rdoc
213 # driver to this instance.
221 # Update the flag file in an output directory.
223 def update_output_dir(op_dir, time, last = {})
224 return if @options.dry_run or not @options.update_output_dir
225 unless ENV['SOURCE_DATE_EPOCH'].nil?
226 time = Time.at(ENV['SOURCE_DATE_EPOCH'].to_i).gmtime
229 File.open output_flag_file(op_dir), "w" do |f|
232 f.puts "#{n}\t#{t.rfc2822}"
238 # Return the path name of the flag file in an output directory.
240 def output_flag_file(op_dir)
241 File.join op_dir, "created.rid"
245 # The .document file contains a list of file and directory name patterns,
246 # representing candidates for documentation. It may also contain comments
247 # (starting with '#')
249 def parse_dot_doc_file in_dir, filename
250 # read and strip comments
251 patterns = File.read(filename).gsub(/#.*/, '')
255 patterns.split(' ').each do |patt|
256 candidates = Dir.glob(File.join(in_dir, patt))
257 result.update normalized_file_list(candidates, false, @options.exclude)
264 # Given a list of files and directories, create a list of all the Ruby
265 # files they contain.
267 # If +force_doc+ is true we always add the given files, if false, only
268 # add files that we guarantee we can parse. It is true when looking at
269 # files given on the command line, false when recursing through
272 # The effect of this is that if you want a file with a non-standard
273 # extension parsed, you must name it explicitly.
275 def normalized_file_list(relative_files, force_doc = false,
276 exclude_pattern = nil)
279 relative_files.each do |rel_file_name|
280 rel_file_name = rel_file_name.sub(/^\.\//, '')
281 next if rel_file_name.end_with? 'created.rid'
282 next if exclude_pattern && exclude_pattern =~ rel_file_name
283 stat = File.stat rel_file_name rescue next
285 case type = stat.ftype
287 mtime = (stat.mtime unless (last_modified = @last_modified[rel_file_name] and
288 stat.mtime.to_i <= last_modified.to_i))
290 if force_doc or RDoc::Parser.can_parse(rel_file_name) then
291 file_list[rel_file_name] = mtime
293 when "directory" then
294 next if UNCONDITIONALLY_SKIPPED_DIRECTORIES.include?(rel_file_name)
296 basename = File.basename(rel_file_name)
297 next if options.skip_tests && TEST_SUITE_DIRECTORY_NAMES.include?(basename)
299 created_rid = File.join rel_file_name, "created.rid"
300 next if File.file? created_rid
302 dot_doc = File.join rel_file_name, RDoc::DOT_DOC_FILENAME
304 if File.file? dot_doc then
305 file_list.update(parse_dot_doc_file(rel_file_name, dot_doc))
307 file_list.update(list_files_in_directory(rel_file_name))
310 warn "rdoc can't parse the #{type} #{rel_file_name}"
318 # Return a list of the files to be processed in a directory. We know that
319 # this directory doesn't have a .document file, so we're looking for real
320 # files. However we may well contain subdirectories which must be tested
321 # for .document files.
323 def list_files_in_directory dir
324 files = Dir.glob File.join(dir, "*")
326 normalized_file_list files, false, @options.exclude
330 # Parses +filename+ and returns an RDoc::TopLevel
332 def parse_file filename
333 encoding = @options.encoding
334 filename = filename.encode encoding
336 @stats.add_file filename
338 return if RDoc::Parser.binary? filename
340 content = RDoc::Encoding.read_file filename, encoding
342 return unless content
344 filename_path = Pathname(filename).expand_path
346 relative_path = filename_path.relative_path_from @options.root
348 relative_path = filename_path
351 if @options.page_dir and
352 relative_path.to_s.start_with? @options.page_dir.to_s then
354 relative_path.relative_path_from @options.page_dir
357 top_level = @store.add_file filename, relative_name: relative_path.to_s
359 parser = RDoc::Parser.for top_level, content, @options, @stats
365 # restart documentation for the classes & modules found
366 top_level.classes_or_modules.each do |cm|
367 cm.done_documenting = false
372 rescue Errno::EACCES => e
374 Unable to read #{filename}, #{e.message}
376 Please check the permissions for this file. Perhaps you do not have access to
377 it or perhaps the original author's permissions are to restrictive. If the
378 this is not your library please report a bug to the author.
382 Before reporting this, could you check that the file you're documenting
385 #{Gem.ruby} -c #{filename}
387 RDoc is not a full Ruby parser and will fail when fed invalid ruby programs.
389 The internal error was:
391 \t(#{e.class}) #{e.message}
395 $stderr.puts e.backtrace.join("\n\t") if $DEBUG_RDOC
402 # Parse each file on the command line, recursively entering directories.
404 def parse_files files
405 file_list = gather_files files
406 @stats = RDoc::Stats.new @store, file_list.length, @options.verbosity
408 return [] if file_list.empty?
410 original_options = @options.dup
413 file_info = file_list.map do |filename|
419 @options = original_options
425 # Removes file extensions known to be unparseable from +files+ and TAGS
426 # files for emacs and vim.
428 def remove_unparseable files
429 files.reject do |file, *|
430 file =~ /\.(?:class|eps|erb|scpt\.txt|svg|ttf|yml)$/i or
431 (file =~ /tags$/i and
432 /\A(\f\n[^,]+,\d+$|!_TAG_)/.match?(File.binread(file, 100)))
437 # Generates documentation or a coverage report depending upon the settings
440 # +options+ can be either an RDoc::Options instance or an array of strings
441 # equivalent to the strings that would be passed on the command line like
442 # <tt>%w[-q -o doc -t My\ Doc\ Title]</tt>. #document will automatically
443 # call RDoc::Options#finish if an options instance was given.
445 # For a list of options, see either RDoc::Options or <tt>rdoc --help</tt>.
447 # By default, output will be stored in a directory called "doc" below the
448 # current directory, so make sure you're somewhere writable before invoking.
451 self.store = RDoc::Store.new
453 if RDoc::Options === options then
456 @options = RDoc::Options.load_options
457 @options.parse options
461 if @options.pipe then
466 unless @options.coverage_report then
467 @last_modified = setup_output_dir @options.op_dir, @options.force_update
470 @store.encoding = @options.encoding
471 @store.dry_run = @options.dry_run
472 @store.main = @options.main_page
473 @store.title = @options.title
474 @store.path = @options.op_dir
476 @start_time = Time.now
480 file_info = parse_files @options.files
482 @options.default_title = "RDoc Documentation"
484 @store.complete @options.visibility
486 @stats.coverage_level = @options.coverage_report
488 if @options.coverage_report then
491 puts @stats.report.accept RDoc::Markup::ToRdoc.new
492 elsif file_info.empty? then
493 $stderr.puts "\nNo newer files." unless @options.quiet
495 gen_klass = @options.generator
497 @generator = gen_klass.new @store, @options
502 if @stats and (@options.coverage_report or not @options.quiet) then
504 puts @stats.summary.accept RDoc::Markup::ToRdoc.new
507 exit @stats.fully_documented? if @options.coverage_report
511 # Generates documentation for +file_info+ (from #parse_files) into the
512 # output dir using the generator selected
513 # by the RDoc options
516 if @options.dry_run then
520 Dir.chdir @options.op_dir do
521 unless @options.quiet then
522 $stderr.puts "\nGenerating #{@generator.class.name.sub(/^.*::/, '')} format into #{Dir.pwd}..."
523 $stderr.puts "\nYou can visit the home page at: \e]8;;file://#{Dir.pwd}/index.html\e\\file://#{Dir.pwd}/index.html\e]8;;\e\\"
527 update_output_dir '.', @start_time, @last_modified
533 # Removes a siginfo handler and replaces the previous
535 def remove_siginfo_handler
536 return unless Signal.list.key? 'INFO'
538 handler = @old_siginfo || 'DEFAULT'
548 rdoc_extensions = Gem.find_latest_files 'rdoc/discover'
550 rdoc_extensions.each do |extension|
554 warn "error loading #{extension.inspect}: #{e.message} (#{e.class})"
555 warn "\t#{e.backtrace.join "\n\t"}" if $DEBUG
561 # require built-in generators after discovery in case they've been replaced
562 require_relative 'generator/darkfish'
563 require_relative 'generator/ri'
564 require_relative 'generator/pot'