aboutsummaryrefslogtreecommitdiffstats
path: root/vendor/gems/rdoc-2.4.3/lib/rdoc/rdoc.rb
blob: b854988ac1f68e7c2b0a1c76c88440afd40857a3 (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
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 'rdoc/diagram'

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

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

  ##
  # Generator instance used for creating output

  attr_accessor :generator

  ##
  # RDoc options

  attr_reader :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
    @generator = nil
    @options = nil
    @stats = nil
  end

  ##
  # Report an error message and exit

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

  ##
  # Turns RDoc from stdin into HTML

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

    out = @html.convert $stdin.read

    $stdout.write out
  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

    if File.exist? op_dir then
      unless File.directory? op_dir then
        error "'#{op_dir}' exists, and is not a directory"
      end
      begin
        created = File.read(flag_file)
      rescue SystemCallError
        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"
      else
        last = (Time.parse(created) unless force rescue nil)
      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)
    File.open(output_flag_file(op_dir), "w") { |f| f.puts time.rfc2822 }
  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, options)
    # 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(options,  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(options, 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 and stat.mtime < @last_created

        if force_doc or RDoc::Parser.can_parse(rel_file_name) then
          file_list << rel_file_name.sub(/^\.\//, '')
        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)
          file_list.concat(parse_dot_doc_file(rel_file_name, dot_doc, options))
        else
          file_list.concat(list_files_in_directory(rel_file_name, options))
        end
      else
        raise RDoc::Error, "I can't deal with a #{type} #{rel_file_name}"
      end
    end

    file_list
  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, options)
    files = Dir.glob File.join(dir, "*")

    normalized_file_list options, files, false, options.exclude
  end

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

  def parse_files(options)
    files = options.files
    files = ["."] if files.empty?

    file_list = normalized_file_list(options, files, true, options.exclude)

    return [] if file_list.empty?

    jobs = SizedQueue.new(@options.threads * 3)
    workers = []
    file_info = []
    file_info_lock = Mutex.new

    Thread.abort_on_exception = true
    @stats = RDoc::Stats.new(file_list.size, options.verbosity)
    @stats.begin_adding @options.threads

    # Create worker threads.
    @options.threads.times do
      thread = Thread.new do
        while (filename = jobs.pop)
          @stats.add_file(filename)
          content = read_file_contents(filename)
          top_level = RDoc::TopLevel.new filename

          parser = RDoc::Parser.for(top_level, filename, content, options,
                                    @stats)
          result = parser.scan

          file_info_lock.synchronize do
            file_info << result
          end
        end
      end
      workers << thread
    end

    # Feed filenames to the parser worker threads...
    file_list.each do |filename|
      jobs << filename
    end
    workers.size.times do
      jobs << nil
    end

    # ...and wait until they're done.
    workers.each do |thread|
      thread.join
    end

    @stats.done_adding

    file_info
  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

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

    if @options.pipe then
      handle_pipe
      exit
    end

    @last_created = setup_output_dir @options.op_dir, @options.force_update

    start_time = Time.now

    file_info = parse_files @options

    @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

      begin
        self.class.current = self

        RDoc::Diagram.new(file_info, @options).draw if @options.diagram
        @generator.generate file_info
        update_output_dir ".", start_time
      ensure
        self.class.current = nil
        Dir.chdir pwd
      end
    end

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

  private

  def read_file_contents(filename)
    content = if RUBY_VERSION >= '1.9' then
                File.open(filename, "r:ascii-8bit") { |f| f.read }
              else
                File.read filename
              end

    if defined? Encoding then
      if /coding:\s*(\S+)/ =~ content[/\A(?:.*\n){0,2}/]
        if enc = ::Encoding.find($1)
          content.force_encoding(enc)
        end
      end
    end

    content
  end

end

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

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