49
46
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
50
47
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
51
48
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
53
50
class RDoc::Generator::Darkfish
55
RDoc::RDoc.add_generator( self )
63
SVNId = %$Id: darkfish.rb 52 2009-01-07 02:08:11Z deveiant $
65
# Path to this file's parent directory. Used to find templates and other
67
GENERATOR_DIR = File.join 'rdoc', 'generator'
72
# Directory where generated classes live relative to the root
75
# Directory where generated files live relative to the root
79
#################################################################
80
### C L A S S M E T H O D S
81
#################################################################
83
### Standard generator factory method
84
def self::for( options )
89
#################################################################
90
### I N S T A N C E M E T H O D S
91
#################################################################
93
### Initialize a few instance variables before we start
94
def initialize( options )
97
template = @options.template || 'darkfish'
99
template_dir = $LOAD_PATH.map do |path|
100
File.join File.expand_path(path), GENERATOR_DIR, 'template', template
105
raise RDoc::Error, "could not find template #{template.inspect}" unless
108
@template_dir = Pathname.new File.expand_path(template_dir)
113
@basedir = Pathname.pwd.expand_path
120
# The output directory
121
attr_reader :outputdir
124
### Output progress information if debugging is enabled
125
def debug_msg( *msg )
126
return unless $DEBUG_RDOC
138
### Create the directories the generated docs will live in if
139
### they don't already exist.
140
def gen_sub_directories
144
### Copy over the stylesheet into the appropriate place in the output
146
def write_style_sheet
147
debug_msg "Copying static files"
148
options = { :verbose => $DEBUG_RDOC, :noop => $DARKFISH_DRYRUN }
150
FileUtils.cp @template_dir + 'rdoc.css', '.', options
152
Dir[(@template_dir + "{js,images}/**/*").to_s].each do |path|
153
next if File.directory? path
154
next if path =~ /#{File::SEPARATOR}\./
156
dst = Pathname.new(path).relative_path_from @template_dir
159
dst_dir = dst.dirname
160
FileUtils.mkdir_p dst_dir, options unless File.exist? dst_dir
162
FileUtils.cp @template_dir + path, dst, options
166
### Build the initial indices and output objects
167
### based on an array of TopLevel objects containing
168
### the extracted information.
169
def generate( top_levels )
170
@outputdir = Pathname.new( @options.op_dir ).expand_path( @basedir )
172
@files = top_levels.sort
173
@classes = RDoc::TopLevel.all_classes_and_modules.sort
174
@methods = @classes.map { |m| m.method_list }.flatten.sort
175
@modsort = get_sorted_module_list( @classes )
177
# Now actually write the output
183
rescue StandardError => err
184
debug_msg "%s: %s\n %s" % [ err.class.name, err.message, err.backtrace.join("\n ") ]
192
### Return a list of the documented modules sorted by salience first, then
194
def get_sorted_module_list( classes )
195
nscounts = classes.inject({}) do |counthash, klass|
196
top_level = klass.full_name.gsub( /::.*/, '' )
197
counthash[top_level] ||= 0
198
counthash[top_level] += 1
203
# Sort based on how often the top level namespace occurs, and then on the
204
# name of the module -- this works for projects that put their stuff into
205
# a namespace, of course, but doesn't hurt if they don't.
206
classes.sort_by do |klass|
207
top_level = klass.full_name.gsub( /::.*/, '' )
209
nscounts[ top_level ] * -1,
212
end.select do |klass|
217
### Generate an index page which lists all the classes which
220
template_file = @template_dir + 'index.rhtml'
221
return unless template_file.exist?
223
debug_msg "Rendering the index page..."
225
template_src = template_file.read
226
template = ERB.new( template_src, nil, '<>' )
227
template.filename = template_file.to_s
233
output = template.result( context )
234
rescue NoMethodError => err
235
raise RDoc::Error, "Error while evaluating %s: %s (at %p)" % [
238
eval( "_erbout[-50,50]", context )
242
outfile = @basedir + @options.op_dir + 'index.html'
243
unless $DARKFISH_DRYRUN
244
debug_msg "Outputting to %s" % [outfile.expand_path]
245
outfile.open( 'w', 0644 ) do |fh|
249
debug_msg "Would have output to %s" % [outfile.expand_path]
253
### Generate a documentation file for each class
254
def generate_class_files
255
template_file = @template_dir + 'classpage.rhtml'
256
return unless template_file.exist?
257
debug_msg "Generating class documentation in #@outputdir"
259
@classes.each do |klass|
260
debug_msg " working on %s (%s)" % [ klass.full_name, klass.path ]
261
outfile = @outputdir + klass.path
262
rel_prefix = @outputdir.relative_path_from( outfile.dirname )
263
svninfo = self.get_svninfo( klass )
265
debug_msg " rendering #{outfile}"
266
self.render_template( template_file, binding(), outfile )
270
### Generate a documentation file for each file
271
def generate_file_files
272
template_file = @template_dir + 'filepage.rhtml'
273
return unless template_file.exist?
274
debug_msg "Generating file documentation in #@outputdir"
276
@files.each do |file|
277
outfile = @outputdir + file.path
278
debug_msg " working on %s (%s)" % [ file.full_name, outfile ]
279
rel_prefix = @outputdir.relative_path_from( outfile.dirname )
282
debug_msg " rendering #{outfile}"
283
self.render_template( template_file, binding(), outfile )
288
### Return a string describing the amount of time in the given number of
289
### seconds in terms a human can understand easily.
290
def time_delta_string( seconds )
291
return 'less than a minute' if seconds < 1.minute
292
return (seconds / 1.minute).to_s + ' minute' + (seconds/60 == 1 ? '' : 's') if seconds < 50.minutes
293
return 'about one hour' if seconds < 90.minutes
294
return (seconds / 1.hour).to_s + ' hours' if seconds < 18.hours
295
return 'one day' if seconds < 1.day
296
return 'about one day' if seconds < 2.days
297
return (seconds / 1.day).to_s + ' days' if seconds < 1.week
298
return 'about one week' if seconds < 2.week
299
return (seconds / 1.week).to_s + ' weeks' if seconds < 3.months
300
return (seconds / 1.month).to_s + ' months' if seconds < 1.year
301
return (seconds / 1.year).to_s + ' years'
305
# %q$Id: darkfish.rb 52 2009-01-07 02:08:11Z deveiant $"
310
(\d{4}-\d{2}-\d{2})\s # Date (YYYY-MM-DD)
311
(\d{2}:\d{2}:\d{2}Z)\s # Time (HH:MM:SSZ)
316
### Try to extract Subversion information out of the first constant whose value looks like
317
### a subversion Id tag. If no matching constant is found, and empty hash is returned.
318
def get_svninfo( klass )
319
constants = klass.constants or return {}
321
constants.find {|c| c.value =~ SVNID_PATTERN } or return {}
323
filename, rev, date, time, committer = $~.captures
324
commitdate = Time.parse( date + ' ' + time )
327
:filename => filename,
328
:rev => Integer( rev ),
329
:commitdate => commitdate,
330
:commitdelta => time_delta_string( Time.now.to_i - commitdate.to_i ),
331
:committer => committer,
336
### Load and render the erb template in the given +template_file+ within the
337
### specified +context+ (a Binding object) and write it out to +outfile+.
338
### Both +template_file+ and +outfile+ should be Pathname-like objects.
340
def render_template( template_file, context, outfile )
341
template_src = template_file.read
342
template = ERB.new( template_src, nil, '<>' )
343
template.filename = template_file.to_s
346
template.result( context )
347
rescue NoMethodError => err
348
raise RDoc::Error, "Error while evaluating %s: %s (at %p)" % [
351
eval( "_erbout[-50,50]", context )
355
unless $DARKFISH_DRYRUN
356
outfile.dirname.mkpath
357
outfile.open( 'w', 0644 ) do |ofh|
361
debug_msg " would have written %d bytes to %s" %
362
[ output.length, outfile ]
366
end # Roc::Generator::Darkfish
371
module TimeConstantMethods # :nodoc:
373
### Number of seconds (returns receiver unmodified)
377
alias_method :second, :seconds
379
### Returns number of seconds in <receiver> minutes
383
alias_method :minute, :minutes
385
### Returns the number of seconds in <receiver> hours
387
return self * 60.minutes
389
alias_method :hour, :hours
391
### Returns the number of seconds in <receiver> days
393
return self * 24.hours
395
alias_method :day, :days
397
### Return the number of seconds in <receiver> weeks
401
alias_method :week, :weeks
403
### Returns the number of seconds in <receiver> fortnights
405
return self * 2.weeks
407
alias_method :fortnight, :fortnights
409
### Returns the number of seconds in <receiver> months (approximate)
411
return self * 30.days
413
alias_method :month, :months
415
### Returns the number of seconds in <receiver> years (approximate)
417
return (self * 365.25.days).to_i
419
alias_method :year, :years
422
### Returns the Time <receiver> number of seconds before the
423
### specified +time+. E.g., 2.hours.before( header.expiration )
429
### Returns the Time <receiver> number of seconds ago. (e.g.,
430
### expiration > 2.hours.ago )
432
return self.before( ::Time.now )
436
### Returns the Time <receiver> number of seconds after the given +time+.
437
### E.g., 10.minutes.after( header.expiration )
442
# Reads best without arguments: 10.minutes.from_now
444
return self.after( ::Time.now )
446
end # module TimeConstantMethods
449
# Extend Numeric with time constants
450
class Numeric # :nodoc:
451
include TimeConstantMethods
52
RDoc::RDoc.add_generator self
56
# Path to this file's parent directory. Used to find templates and other
59
GENERATOR_DIR = File.join 'rdoc', 'generator'
67
# Description of this generator
69
DESCRIPTION = 'HTML generator, written by Michael Granger'
72
# Initialize a few instance variables before we start
74
def initialize options
77
@template_dir = Pathname.new options.template_dir
83
@basedir = Pathname.pwd.expand_path
87
# The output directory
89
attr_reader :outputdir
92
# Output progress information if debugging is enabled
95
return unless $DEBUG_RDOC
100
# Directory where generated class HTML files live relative to the output
108
# Directory where generated class HTML files live relative to the output
116
# Create the directories the generated docs will live in if they don't
119
def gen_sub_directories
124
# Copy over the stylesheet into the appropriate place in the output
127
def write_style_sheet
128
debug_msg "Copying static files"
129
options = { :verbose => $DEBUG_RDOC, :noop => @options.dry_run }
131
FileUtils.cp @template_dir + 'rdoc.css', '.', options
133
Dir[(@template_dir + "{js,images}/**/*").to_s].each do |path|
134
next if File.directory? path
135
next if File.basename(path) =~ /^\./
137
dst = Pathname.new(path).relative_path_from @template_dir
140
dst_dir = dst.dirname
141
FileUtils.mkdir_p dst_dir, options unless File.exist? dst_dir
143
FileUtils.cp @template_dir + path, dst, options
148
# Build the initial indices and output objects based on an array of TopLevel
149
# objects containing the extracted information.
151
def generate top_levels
152
@outputdir = Pathname.new(@options.op_dir).expand_path(@basedir)
154
@files = top_levels.sort
155
@classes = RDoc::TopLevel.all_classes_and_modules.sort
156
@methods = @classes.map { |m| m.method_list }.flatten.sort
157
@modsort = get_sorted_module_list(@classes)
159
# Now actually write the output
166
debug_msg "%s: %s\n %s" % [
167
e.class.name, e.message, e.backtrace.join("\n ")
176
# Return a list of the documented modules sorted by salience first, then
179
def get_sorted_module_list(classes)
180
nscounts = classes.inject({}) do |counthash, klass|
181
top_level = klass.full_name.gsub(/::.*/, '')
182
counthash[top_level] ||= 0
183
counthash[top_level] += 1
188
# Sort based on how often the top level namespace occurs, and then on the
189
# name of the module -- this works for projects that put their stuff into
190
# a namespace, of course, but doesn't hurt if they don't.
191
classes.sort_by do |klass|
192
top_level = klass.full_name.gsub( /::.*/, '' )
193
[nscounts[top_level] * -1, klass.full_name]
194
end.select do |klass|
200
# Generate an index page which lists all the classes which are documented.
203
template_file = @template_dir + 'index.rhtml'
204
return unless template_file.exist?
206
debug_msg "Rendering the index page..."
208
out_file = @basedir + @options.op_dir + 'index.html'
210
render_template template_file, out_file do |io| binding end
212
error = RDoc::Error.new \
213
"error generating index.html: #{e.message} (#{e.class})"
214
error.set_backtrace e.backtrace
220
# Generate a documentation file for each class
222
def generate_class_files
223
template_file = @template_dir + 'classpage.rhtml'
224
return unless template_file.exist?
225
debug_msg "Generating class documentation in #{@outputdir}"
229
@classes.each do |klass|
231
debug_msg " working on %s (%s)" % [klass.full_name, klass.path]
232
out_file = @outputdir + klass.path
233
# suppress 1.9.3 warning
234
rel_prefix = rel_prefix = @outputdir.relative_path_from(out_file.dirname)
235
svninfo = svninfo = self.get_svninfo(klass)
237
debug_msg " rendering #{out_file}"
238
render_template template_file, out_file do |io| binding end
241
error = RDoc::Error.new \
242
"error generating #{current.path}: #{e.message} (#{e.class})"
243
error.set_backtrace e.backtrace
249
# Generate a documentation file for each file
251
def generate_file_files
252
template_file = @template_dir + 'filepage.rhtml'
253
return unless template_file.exist?
254
debug_msg "Generating file documentation in #{@outputdir}"
258
@files.each do |file|
259
out_file = @outputdir + file.path
260
debug_msg " working on %s (%s)" % [file.full_name, out_file]
261
# suppress 1.9.3 warning
262
rel_prefix = rel_prefix = @outputdir.relative_path_from(out_file.dirname)
264
debug_msg " rendering #{out_file}"
265
render_template template_file, out_file do |io| binding end
269
RDoc::Error.new "error generating #{out_file}: #{e.message} (#{e.class})"
270
error.set_backtrace e.backtrace
276
# Return a string describing the amount of time in the given number of
277
# seconds in terms a human can understand easily.
279
def time_delta_string seconds
280
return 'less than a minute' if seconds < 60
281
return "#{seconds / 60} minute#{seconds / 60 == 1 ? '' : 's'}" if
282
seconds < 3000 # 50 minutes
283
return 'about one hour' if seconds < 5400 # 90 minutes
284
return "#{seconds / 3600} hours" if seconds < 64800 # 18 hours
285
return 'one day' if seconds < 86400 # 1 day
286
return 'about one day' if seconds < 172800 # 2 days
287
return "#{seconds / 86400} days" if seconds < 604800 # 1 week
288
return 'about one week' if seconds < 1209600 # 2 week
289
return "#{seconds / 604800} weeks" if seconds < 7257600 # 3 months
290
return "#{seconds / 2419200} months" if seconds < 31536000 # 1 year
291
return "#{seconds / 31536000} years"
294
# %q$Id: darkfish.rb 52 2009-01-07 02:08:11Z deveiant $"
299
(\d{4}-\d{2}-\d{2})\s # Date (YYYY-MM-DD)
300
(\d{2}:\d{2}:\d{2}Z)\s # Time (HH:MM:SSZ)
306
# Try to extract Subversion information out of the first constant whose
307
# value looks like a subversion Id tag. If no matching constant is found,
308
# and empty hash is returned.
310
def get_svninfo klass
311
constants = klass.constants or return {}
313
constants.find { |c| c.value =~ SVNID_PATTERN } or return {}
315
filename, rev, date, time, committer = $~.captures
316
commitdate = Time.parse "#{date} #{time}"
319
:filename => filename,
320
:rev => Integer(rev),
321
:commitdate => commitdate,
322
:commitdelta => time_delta_string(Time.now - commitdate),
323
:committer => committer,
328
# Load and render the erb template in the given +template_file+ and write
329
# it out to +out_file+.
331
# Both +template_file+ and +out_file+ should be Pathname-like objects.
333
# An io will be yielded which must be captured by binding in the caller.
335
def render_template template_file, out_file # :yield: io
336
template = template_for template_file
338
unless @options.dry_run then
339
debug_msg "Outputting to %s" % [out_file.expand_path]
341
out_file.dirname.mkpath
342
out_file.open 'w', 0644 do |io|
343
io.set_encoding @options.encoding if Object.const_defined? :Encoding
347
template_result template, context, template_file
352
output = template_result template, context, template_file
354
debug_msg " would have written %d characters to %s" % [
355
output.length, out_file.expand_path
361
# Creates the result for +template+ with +context+. If an error is raised a
362
# Pathname +template_file+ will indicate the file where the error occurred.
364
def template_result template, context, template_file
365
template.filename = template_file.to_s
366
template.result context
367
rescue NoMethodError => e
368
raise RDoc::Error, "Error while evaluating %s: %s" % [
369
template_file.expand_path,
375
# Retrieves a cache template for +file+, if present, or fills the cache.
377
def template_for file
378
template = @template_cache[file]
380
return template if template
382
klass = @options.dry_run ? ERB : RDoc::ERBIO
384
template = klass.new file.read, nil, '<>'
385
@template_cache[file] = template