summaryrefslogtreecommitdiff
path: root/lib/rdoc/rdoc.rb
blob: 493dada4f01582f8fd5cbd30ffcce33b2b31a2ea (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
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
require 'rdoc'

require 'rdoc/parser'

# Simple must come first
require 'rdoc/parser/simple'
require 'rdoc/parser/ruby'
require 'rdoc/parser/c'
require 'rdoc/parser/perl'

require 'rdoc/stats'
require 'rdoc/options'

require 'find'
require 'fileutils'
require 'time'

##
# 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::RDoc

  ##
  # File pattern to exclude

  attr_accessor :exclude

  ##
  # Generator instance used for creating output

  attr_accessor :generator

  ##
  # RDoc options

  attr_accessor :options

  ##
  # Accessor for statistics.  Available after each call to parse_files

  attr_reader :stats

  ##
  # This is the list of supported output generators

  GENERATORS = {}

  ##
  # Add +klass+ that can generate output after parsing

  def self.add_generator(klass)
    name = klass.name.sub(/^RDoc::Generator::/, '').downcase
    GENERATORS[name] = klass
  end

  ##
  # Active RDoc::RDoc instance

  def self.current
    @current
  end

  ##
  # Sets the active RDoc::RDoc instance

  def self.current=(rdoc)
    @current = rdoc
  end

  def initialize
    @current      = nil
    @exclude      = nil
    @generator    = nil
    @last_created = {}
    @old_siginfo  = nil
    @options      = nil
    @stats        = nil
  end

  ##
  # Report an error message and exit

  def error(msg)
    raise RDoc::Error, msg
  end

  ##
  # Gathers a set of parseable files from the files and directories listed in
  # +files+.

  def gather_files files
    files = ["."] if files.empty?

    file_list = normalized_file_list files, true, @exclude

    file_list = file_list.uniq

    file_list = remove_unparseable file_list
  end

  ##
  # Turns RDoc from stdin into HTML

  def handle_pipe
    @html = RDoc::Markup::ToHtml.new

    out = @html.convert $stdin.read

    $stdout.write out
  end

  ##
  # Installs a siginfo handler that prints the current filename.

  def install_siginfo_handler
    return unless Signal.list.include? 'INFO'

    @old_siginfo = trap 'INFO' do
      puts @current if @current
    end
  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, force)
    flag_file = output_flag_file op_dir

    last = @last_created

    if File.exist? op_dir then
      unless File.directory? op_dir then
        error "'#{op_dir}' exists, and is not a directory"
      end
      begin
        open(flag_file) do |f|
          unless force
            Time.parse(f.gets)
            f.each do |line|
              file, time = line.split(/\t/, 2)
              time = Time.parse(time) rescue next
              last[file] = time
            end
          end
        end
      rescue
        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
      FileUtils.mkdir_p(op_dir)
    end

    last
  end

  ##
  # Update the flag file in an output directory.

  def update_output_dir(op_dir, time, last = {})
    File.open(output_flag_file(op_dir), "w") do |f|
      f.puts time.rfc2822
      last.each do |n, t|
        f.puts "#{n}\t#{t.rfc2822}"
      end
    end
  end

  ##
  # Return the path name of the flag file in an output directory.

  def output_flag_file(op_dir)
    File.join op_dir, "created.rid"
  end

  ##
  # The .document file contains a list of file and directory name patterns,
  # representing candidates for documentation. It may also contain comments
  # (starting with '#')

  def parse_dot_doc_file in_dir, filename
    # read and strip comments
    patterns = File.read(filename).gsub(/#.*/, '')

    result = []

    patterns.split.each do |patt|
      candidates = Dir.glob(File.join(in_dir, patt))
      result.concat normalized_file_list(candidates)
    end

    result
  end

  ##
  # Given a list of files and directories, create a list of all the Ruby
  # files they contain.
  #
  # If +force_doc+ is true we always add the given files, if false, only
  # add files that we guarantee we can parse.  It is true when looking at
  # files given on the command line, false when recursing through
  # subdirectories.
  #
  # The effect of this is that if you want a file with a non-standard
  # extension parsed, you must name it explicitly.

  def normalized_file_list(relative_files, force_doc = false,
                           exclude_pattern = nil)
    file_list = []

    relative_files.each do |rel_file_name|
      next if exclude_pattern && exclude_pattern =~ rel_file_name
      stat = File.stat rel_file_name rescue next

      case type = stat.ftype
      when "file"
        next if last_created = @last_created[rel_file_name] and stat.mtime <= last_created

        if force_doc or RDoc::Parser.can_parse(rel_file_name) then
          file_list << rel_file_name.sub(/^\.\//, '')
          @last_created[rel_file_name] = stat.mtime
        end
      when "directory"
        next if rel_file_name == "CVS" || rel_file_name == ".svn"

        dot_doc = File.join rel_file_name, RDoc::DOT_DOC_FILENAME

        if File.file?(dot_doc) then
          file_list << parse_dot_doc_file(rel_file_name, dot_doc)
        else
          file_list << list_files_in_directory(rel_file_name)
        end
      else
        raise RDoc::Error, "I can't deal with a #{type} #{rel_file_name}"
      end
    end

    file_list.flatten
  end

  ##
  # Return a list of the files to be processed in a directory. We know that
  # this directory doesn't have a .document file, so we're looking for real
  # files. However we may well contain subdirectories which must be tested
  # for .document files.

  def list_files_in_directory dir
    files = Dir.glob File.join(dir, "*")

    normalized_file_list files, false, @options.exclude
  end

  ##
  # Parses +filename+ and returns an RDoc::TopLevel

  def parse_file filename
    @stats.add_file filename
    content = read_file_contents filename

    return unless content

    top_level = RDoc::TopLevel.new filename

    parser = RDoc::Parser.for top_level, filename, content, @options, @stats

    return unless parser

    parser.scan
  rescue => e
    $stderr.puts <<-EOF
Before reporting this, could you check that the file you're documenting
compiles cleanly--RDoc is not a full Ruby parser, and gets confused easily if
fed invalid programs.

The internal error was:

\t(#{e.class}) #{e.message}

    EOF

    $stderr.puts e.backtrace.join("\n\t") if $RDOC_DEBUG

    raise e
    nil
  end

  ##
  # Parse each file on the command line, recursively entering directories.

  def parse_files files
    file_list = gather_files files

    return [] if file_list.empty?

    file_info = []

    @stats = RDoc::Stats.new file_list.size, @options.verbosity
    @stats.begin_adding

    file_info = file_list.map do |filename|
      @current = filename
      parse_file filename
    end.compact

    @stats.done_adding

    file_info
  end

  ##
  # Removes file extensions known to be unparseable from +files+

  def remove_unparseable files
    files.reject do |file|
      file =~ /\.(?:class|eps|erb|scpt\.txt|ttf|yml)$/i
    end
  end

  ##
  # 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: RDoc::Error on error

  def document(argv)
    RDoc::TopLevel.reset
    RDoc::Parser::C.reset
    RDoc::AnyMethod.reset

    @options = RDoc::Options.new
    @options.parse argv

    if @options.pipe then
      handle_pipe
      exit
    end

    @exclude = @options.exclude

    setup_output_dir @options.op_dir, @options.force_update

    start_time = Time.now

    file_info = parse_files @options.files

    @options.title = "RDoc Documentation"

    if file_info.empty?
      $stderr.puts "\nNo newer files." unless @options.quiet
    else
      gen_klass = @options.generator

      unless @options.quiet then
        $stderr.puts "\nGenerating #{gen_klass.name.sub(/^.*::/, '')}..."
      end

      @generator = gen_klass.for @options

      pwd = Dir.pwd

      Dir.chdir @options.op_dir do
        begin
          self.class.current = self

          @generator.generate file_info
          update_output_dir ".", start_time, @last_created
        ensure
          self.class.current = nil
        end
      end
    end

    unless @options.quiet or not @stats then
      puts
      @stats.print
    end
  end

  def read_file_contents(filename)
    content = File.open(filename, "rb") { |f| f.read }

    if defined? Encoding then
      if /coding[=:]\s*([^\s;]+)/i =~ content[%r"\A(?:#!.*\n)?.*\n"]
        if enc = ::Encoding.find($1)
          content.force_encoding(enc)
        end
      end
    end

    content
  rescue Errno::EISDIR, Errno::ENOENT
    nil
  end

  ##
  # Removes a siginfo handler and replaces the previous

  def remove_siginfo_handler
    return unless Signal.list.key? 'INFO'

    handler = @old_siginfo || 'DEFAULT'

    trap 'INFO', handler
  end

end

begin
  require 'rubygems'

  if Gem.respond_to? :find_files then
    rdoc_extensions = Gem.find_files 'rdoc/discover'

    rdoc_extensions.each do |extension|
      begin
        load extension
      rescue => e
        warn "error loading #{extension.inspect}: #{e.message} (#{e.class})"
      end
    end
  end
rescue LoadError
end

# require built-in generators after discovery in case they've been replaced
require 'rdoc/generator/darkfish'
require 'rdoc/generator/ri'