1require 'rdoc' 2 3require 'find' 4require 'fileutils' 5require 'pathname' 6require 'time' 7 8## 9# This is the driver for generating RDoc output. It handles file parsing and 10# generation of output. 11# 12# To use this class to generate RDoc output via the API, the recommended way 13# is: 14# 15# rdoc = RDoc::RDoc.new 16# options = rdoc.load_options # returns an RDoc::Options instance 17# # set extra options 18# rdoc.document options 19# 20# You can also generate output like the +rdoc+ executable: 21# 22# rdoc = RDoc::RDoc.new 23# rdoc.document argv 24# 25# Where +argv+ is an array of strings, each corresponding to an argument you'd 26# give rdoc on the command line. See <tt>rdoc --help<tt> for details. 27 28class RDoc::RDoc 29 30 @current = nil 31 32 ## 33 # This is the list of supported output generators 34 35 GENERATORS = {} 36 37 ## 38 # File pattern to exclude 39 40 attr_accessor :exclude 41 42 ## 43 # Generator instance used for creating output 44 45 attr_accessor :generator 46 47 ## 48 # Hash of files and their last modified times. 49 50 attr_reader :last_modified 51 52 ## 53 # RDoc options 54 55 attr_accessor :options 56 57 ## 58 # Accessor for statistics. Available after each call to parse_files 59 60 attr_reader :stats 61 62 ## 63 # The current documentation store 64 65 attr_reader :store 66 67 ## 68 # Add +klass+ that can generate output after parsing 69 70 def self.add_generator(klass) 71 name = klass.name.sub(/^RDoc::Generator::/, '').downcase 72 GENERATORS[name] = klass 73 end 74 75 ## 76 # Active RDoc::RDoc instance 77 78 def self.current 79 @current 80 end 81 82 ## 83 # Sets the active RDoc::RDoc instance 84 85 def self.current= rdoc 86 @current = rdoc 87 end 88 89 ## 90 # Creates a new RDoc::RDoc instance. Call #document to parse files and 91 # generate documentation. 92 93 def initialize 94 @current = nil 95 @exclude = nil 96 @generator = nil 97 @last_modified = {} 98 @old_siginfo = nil 99 @options = nil 100 @stats = nil 101 @store = nil 102 end 103 104 ## 105 # Report an error message and exit 106 107 def error(msg) 108 raise RDoc::Error, msg 109 end 110 111 ## 112 # Gathers a set of parseable files from the files and directories listed in 113 # +files+. 114 115 def gather_files files 116 files = ["."] if files.empty? 117 118 file_list = normalized_file_list files, true, @exclude 119 120 file_list = file_list.uniq 121 122 file_list = remove_unparseable file_list 123 124 file_list.sort 125 end 126 127 ## 128 # Turns RDoc from stdin into HTML 129 130 def handle_pipe 131 @html = RDoc::Markup::ToHtml.new @options 132 133 parser = RDoc::Text::MARKUP_FORMAT[@options.markup] 134 135 document = parser.parse $stdin.read 136 137 out = @html.convert document 138 139 $stdout.write out 140 end 141 142 ## 143 # Installs a siginfo handler that prints the current filename. 144 145 def install_siginfo_handler 146 return unless Signal.list.include? 'INFO' 147 148 @old_siginfo = trap 'INFO' do 149 puts @current if @current 150 end 151 end 152 153 ## 154 # Loads options from .rdoc_options if the file exists, otherwise creates a 155 # new RDoc::Options instance. 156 157 def load_options 158 options_file = File.expand_path '.rdoc_options' 159 return RDoc::Options.new unless File.exist? options_file 160 161 RDoc.load_yaml 162 163 parse_error = if Object.const_defined? :Psych then 164 Psych::SyntaxError 165 else 166 ArgumentError 167 end 168 169 begin 170 options = YAML.load_file '.rdoc_options' 171 rescue *parse_error 172 end 173 174 raise RDoc::Error, "#{options_file} is not a valid rdoc options file" unless 175 RDoc::Options === options 176 177 options 178 end 179 180 ## 181 # Create an output dir if it doesn't exist. If it does exist, but doesn't 182 # contain the flag file <tt>created.rid</tt> then we refuse to use it, as 183 # we may clobber some manually generated documentation 184 185 def setup_output_dir(dir, force) 186 flag_file = output_flag_file dir 187 188 last = {} 189 190 if @options.dry_run then 191 # do nothing 192 elsif File.exist? dir then 193 error "#{dir} exists and is not a directory" unless File.directory? dir 194 195 begin 196 open flag_file do |io| 197 unless force then 198 Time.parse io.gets 199 200 io.each do |line| 201 file, time = line.split "\t", 2 202 time = Time.parse(time) rescue next 203 last[file] = time 204 end 205 end 206 end 207 rescue SystemCallError, TypeError 208 error <<-ERROR 209 210Directory #{dir} already exists, but it looks like it isn't an RDoc directory. 211 212Because RDoc doesn't want to risk destroying any of your existing files, 213you'll need to specify a different output directory name (using the --op <dir> 214option) 215 216 ERROR 217 end unless @options.force_output 218 else 219 FileUtils.mkdir_p dir 220 FileUtils.touch output_flag_file dir 221 end 222 223 last 224 end 225 226 ## 227 # Sets the current documentation tree to +store+ and sets the store's rdoc 228 # driver to this instance. 229 230 def store= store 231 @store = store 232 @store.rdoc = self 233 end 234 235 ## 236 # Update the flag file in an output directory. 237 238 def update_output_dir(op_dir, time, last = {}) 239 return if @options.dry_run or not @options.update_output_dir 240 241 open output_flag_file(op_dir), "w" do |f| 242 f.puts time.rfc2822 243 last.each do |n, t| 244 f.puts "#{n}\t#{t.rfc2822}" 245 end 246 end 247 end 248 249 ## 250 # Return the path name of the flag file in an output directory. 251 252 def output_flag_file(op_dir) 253 File.join op_dir, "created.rid" 254 end 255 256 ## 257 # The .document file contains a list of file and directory name patterns, 258 # representing candidates for documentation. It may also contain comments 259 # (starting with '#') 260 261 def parse_dot_doc_file in_dir, filename 262 # read and strip comments 263 patterns = File.read(filename).gsub(/#.*/, '') 264 265 result = [] 266 267 patterns.split.each do |patt| 268 candidates = Dir.glob(File.join(in_dir, patt)) 269 result.concat normalized_file_list(candidates) 270 end 271 272 result 273 end 274 275 ## 276 # Given a list of files and directories, create a list of all the Ruby 277 # files they contain. 278 # 279 # If +force_doc+ is true we always add the given files, if false, only 280 # add files that we guarantee we can parse. It is true when looking at 281 # files given on the command line, false when recursing through 282 # subdirectories. 283 # 284 # The effect of this is that if you want a file with a non-standard 285 # extension parsed, you must name it explicitly. 286 287 def normalized_file_list(relative_files, force_doc = false, 288 exclude_pattern = nil) 289 file_list = [] 290 291 relative_files.each do |rel_file_name| 292 next if exclude_pattern && exclude_pattern =~ rel_file_name 293 stat = File.stat rel_file_name rescue next 294 295 case type = stat.ftype 296 when "file" then 297 next if last_modified = @last_modified[rel_file_name] and 298 stat.mtime.to_i <= last_modified.to_i 299 300 if force_doc or RDoc::Parser.can_parse(rel_file_name) then 301 file_list << rel_file_name.sub(/^\.\//, '') 302 @last_modified[rel_file_name] = stat.mtime 303 end 304 when "directory" then 305 next if rel_file_name == "CVS" || rel_file_name == ".svn" 306 307 dot_doc = File.join rel_file_name, RDoc::DOT_DOC_FILENAME 308 309 if File.file? dot_doc then 310 file_list << parse_dot_doc_file(rel_file_name, dot_doc) 311 else 312 file_list << list_files_in_directory(rel_file_name) 313 end 314 else 315 warn "rdoc can't parse the #{type} #{rel_file_name}" 316 end 317 end 318 319 file_list.flatten 320 end 321 322 ## 323 # Return a list of the files to be processed in a directory. We know that 324 # this directory doesn't have a .document file, so we're looking for real 325 # files. However we may well contain subdirectories which must be tested 326 # for .document files. 327 328 def list_files_in_directory dir 329 files = Dir.glob File.join(dir, "*") 330 331 normalized_file_list files, false, @options.exclude 332 end 333 334 ## 335 # Parses +filename+ and returns an RDoc::TopLevel 336 337 def parse_file filename 338 if defined?(Encoding) then 339 encoding = @options.encoding 340 filename = filename.encode encoding 341 end 342 343 @stats.add_file filename 344 345 content = RDoc::Encoding.read_file filename, encoding 346 347 return unless content 348 349 filename_path = Pathname(filename).expand_path 350 relative_path = filename_path.relative_path_from @options.root 351 352 if @options.page_dir and 353 relative_path.to_s.start_with? @options.page_dir.to_s then 354 relative_path = 355 relative_path.relative_path_from @options.page_dir 356 end 357 358 top_level = @store.add_file filename, relative_path.to_s 359 360 parser = RDoc::Parser.for top_level, filename, content, @options, @stats 361 362 return unless parser 363 364 parser.scan 365 366 # restart documentation for the classes & modules found 367 top_level.classes_or_modules.each do |cm| 368 cm.done_documenting = false 369 end 370 371 top_level 372 373 rescue Errno::EACCES => e 374 $stderr.puts <<-EOF 375Unable to read #{filename}, #{e.message} 376 377Please check the permissions for this file. Perhaps you do not have access to 378it or perhaps the original author's permissions are to restrictive. If the 379this is not your library please report a bug to the author. 380 EOF 381 rescue => e 382 $stderr.puts <<-EOF 383Before reporting this, could you check that the file you're documenting 384has proper syntax: 385 386 #{Gem.ruby} -c #{filename} 387 388RDoc is not a full Ruby parser and will fail when fed invalid ruby programs. 389 390The internal error was: 391 392\t(#{e.class}) #{e.message} 393 394 EOF 395 396 $stderr.puts e.backtrace.join("\n\t") if $DEBUG_RDOC 397 398 raise e 399 nil 400 end 401 402 ## 403 # Parse each file on the command line, recursively entering directories. 404 405 def parse_files files 406 file_list = gather_files files 407 @stats = RDoc::Stats.new @store, file_list.length, @options.verbosity 408 409 return [] if file_list.empty? 410 411 file_info = [] 412 413 @stats.begin_adding 414 415 file_info = file_list.map do |filename| 416 @current = filename 417 parse_file filename 418 end.compact 419 420 @stats.done_adding 421 422 file_info 423 end 424 425 ## 426 # Removes file extensions known to be unparseable from +files+ and TAGS 427 # files for emacs and vim. 428 429 def remove_unparseable files 430 files.reject do |file| 431 file =~ /\.(?:class|eps|erb|scpt\.txt|ttf|yml)$/i or 432 (file =~ /tags$/i and 433 open(file, 'rb') { |io| 434 io.read(100) =~ /\A(\f\n[^,]+,\d+$|!_TAG_)/ 435 }) 436 end 437 end 438 439 ## 440 # Generates documentation or a coverage report depending upon the settings 441 # in +options+. 442 # 443 # +options+ can be either an RDoc::Options instance or an array of strings 444 # equivalent to the strings that would be passed on the command line like 445 # <tt>%w[-q -o doc -t My\ Doc\ Title]</tt>. #document will automatically 446 # call RDoc::Options#finish if an options instance was given. 447 # 448 # For a list of options, see either RDoc::Options or <tt>rdoc --help</tt>. 449 # 450 # By default, output will be stored in a directory called "doc" below the 451 # current directory, so make sure you're somewhere writable before invoking. 452 453 def document options 454 self.store = RDoc::Store.new 455 456 if RDoc::Options === options then 457 @options = options 458 @options.finish 459 else 460 @options = load_options 461 @options.parse options 462 end 463 464 if @options.pipe then 465 handle_pipe 466 exit 467 end 468 469 @exclude = @options.exclude 470 471 unless @options.coverage_report then 472 @last_modified = setup_output_dir @options.op_dir, @options.force_update 473 end 474 475 @store.encoding = @options.encoding if @options.respond_to? :encoding 476 @store.dry_run = @options.dry_run 477 @store.main = @options.main_page 478 @store.title = @options.title 479 @store.path = @options.op_dir 480 481 @start_time = Time.now 482 483 @store.load_cache 484 485 file_info = parse_files @options.files 486 487 @options.default_title = "RDoc Documentation" 488 489 @store.complete @options.visibility 490 491 @stats.coverage_level = @options.coverage_report 492 493 if @options.coverage_report then 494 puts 495 496 puts @stats.report 497 elsif file_info.empty? then 498 $stderr.puts "\nNo newer files." unless @options.quiet 499 else 500 gen_klass = @options.generator 501 502 @generator = gen_klass.new @store, @options 503 504 generate 505 end 506 507 if @stats and (@options.coverage_report or not @options.quiet) then 508 puts 509 puts @stats.summary 510 end 511 512 exit @stats.fully_documented? if @options.coverage_report 513 end 514 515 ## 516 # Generates documentation for +file_info+ (from #parse_files) into the 517 # output dir using the generator selected 518 # by the RDoc options 519 520 def generate 521 Dir.chdir @options.op_dir do 522 unless @options.quiet then 523 $stderr.puts "\nGenerating #{@generator.class.name.sub(/^.*::/, '')} format into #{Dir.pwd}..." 524 end 525 526 @generator.generate 527 update_output_dir '.', @start_time, @last_modified 528 end 529 end 530 531 ## 532 # Removes a siginfo handler and replaces the previous 533 534 def remove_siginfo_handler 535 return unless Signal.list.key? 'INFO' 536 537 handler = @old_siginfo || 'DEFAULT' 538 539 trap 'INFO', handler 540 end 541 542end 543 544begin 545 require 'rubygems' 546 547 if Gem.respond_to? :find_files then 548 rdoc_extensions = Gem.find_files 'rdoc/discover' 549 550 rdoc_extensions.each do |extension| 551 begin 552 load extension 553 rescue => e 554 warn "error loading #{extension.inspect}: #{e.message} (#{e.class})" 555 warn "\t#{e.backtrace.join "\n\t"}" if $DEBUG 556 end 557 end 558 end 559rescue LoadError 560end 561 562# require built-in generators after discovery in case they've been replaced 563require 'rdoc/generator/darkfish' 564require 'rdoc/generator/ri' 565 566