rdoc-generator-sixfish 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,413 @@
1
+ # -*- mode: ruby; ruby-indent-level: 4; tab-width: 4 -*-
2
+
3
+ gem 'rdoc'
4
+
5
+ require 'uri'
6
+ require 'yajl'
7
+ require 'inversion'
8
+ require 'loggability'
9
+ require 'fileutils'
10
+ require 'pathname'
11
+ require 'rdoc/rdoc'
12
+ require 'rdoc/generator/json_index'
13
+
14
+ require 'inversion/template/striptag'
15
+
16
+ require 'sixfish'
17
+
18
+ # The Sixfish generator class.
19
+ class RDoc::Generator::Sixfish
20
+ extend Loggability
21
+ include FileUtils
22
+
23
+
24
+ # Loggability API -- set up a Logger for Sixfish
25
+ log_as :sixfish
26
+
27
+
28
+ # The data directory in the project if that exists, otherwise the gem datadir
29
+ DATADIR = if ENV['SIXFISH_DATADIR']
30
+ Pathname( ENV['SIXFISH_DATADIR'] )
31
+ elsif Gem.loaded_specs[ 'rdoc-generator-sixfish' ] &&
32
+ File.exist?( Gem.loaded_specs['rdoc-generator-sixfish'].datadir )
33
+ Pathname( Gem.loaded_specs['rdoc-generator-sixfish'].datadir )
34
+ else
35
+ Pathname( __FILE__ ).dirname.parent + 'data/rdoc-generator-sixfish'
36
+ end
37
+
38
+ # Register with RDoc as an alternative generator
39
+ RDoc::RDoc.add_generator( self )
40
+
41
+
42
+ ### Add generator-specific options to the option-parser
43
+ def self::setup_options( rdoc_options )
44
+ op = rdoc_options.option_parser
45
+
46
+ op.accept( URI ) do |string|
47
+ uri = URI.parse( string ) rescue nil
48
+ raise OptionParser::InvalidArgument unless uri
49
+ uri
50
+ end
51
+ op.on( '--additional-stylesheet=URL', URI,
52
+ "Add an additional (preferred) stylesheet",
53
+ "link to each generated page. This allows",
54
+ "the output style to be overridden." ) do |url|
55
+ rdoc_options.additional_stylesheet = url
56
+ end
57
+ end
58
+
59
+
60
+ ### Set up some instance variables
61
+ def initialize( store, options )
62
+ @store = store
63
+ @options = options
64
+ $DEBUG_RDOC = $VERBOSE || $DEBUG
65
+
66
+ self.log.debug "Setting up generator for %p with options: %p" % [ @store, @options ]
67
+
68
+ extend( FileUtils::Verbose ) if $DEBUG_RDOC
69
+ extend( FileUtils::DryRun ) if options.dry_run
70
+
71
+ @base_dir = Pathname.pwd.expand_path
72
+ @template_dir = DATADIR
73
+ @output_dir = Pathname( @options.op_dir ).expand_path( @base_dir )
74
+
75
+ @template_cache = {}
76
+ @files = nil
77
+ @classes = nil
78
+ @search_index = {}
79
+
80
+ Inversion::Template.configure( :template_paths => [self.template_dir + 'templates'] )
81
+ end
82
+
83
+
84
+ ######
85
+ public
86
+ ######
87
+
88
+ # The base directory (current working directory) as a Pathname
89
+ attr_reader :base_dir
90
+
91
+ # The directory containing templates as a Pathname
92
+ attr_reader :template_dir
93
+
94
+ # The output directory as a Pathname
95
+ attr_reader :output_dir
96
+
97
+ # The command-line options given to the rdoc command
98
+ attr_reader :options
99
+
100
+ # The RDoc::Store that contains the parsed CodeObjects
101
+ attr_reader :store
102
+
103
+
104
+ ### Output progress information if debugging is enabled
105
+ def debug_msg( *msg )
106
+ return unless $DEBUG_RDOC
107
+ $stderr.puts( *msg )
108
+ end
109
+
110
+
111
+ ### Backward-compatible (no-op) method.
112
+ def class_dir # :nodoc:
113
+ nil
114
+ end
115
+ alias_method :file_dir, :class_dir
116
+
117
+
118
+ ### Create the directories the generated docs will live in if they don't
119
+ ### already exist.
120
+ def gen_sub_directories
121
+ self.output_dir.mkpath
122
+ end
123
+
124
+
125
+ ### Build the initial indices and output objects based on the files in the generator's store.
126
+ def generate
127
+ self.populate_data_objects
128
+
129
+ self.generate_index_page
130
+ self.generate_class_files
131
+ self.generate_file_files
132
+
133
+ self.generate_search_index
134
+
135
+ self.copy_static_assets
136
+ end
137
+
138
+
139
+ ### Populate the data objects necessary to generate documentation from the generator's
140
+ ### store.
141
+ def populate_data_objects
142
+ @files = self.store.all_files.sort
143
+ @classes = self.store.all_classes_and_modules.sort
144
+ @methods = @classes.map {|m| m.method_list }.flatten.sort
145
+ @modsort = self.get_sorted_module_list( @classes )
146
+ end
147
+
148
+
149
+ ### Generate an index page which lists all the classes which are documented.
150
+ def generate_index_page
151
+ self.log.debug "Generating index page"
152
+ template = self.load_template( 'index.tmpl' )
153
+ self.set_toplevel_variables( template )
154
+
155
+ out_file = self.output_dir + 'index.html'
156
+ out_file.dirname.mkpath
157
+
158
+ template.rel_prefix = self.output_dir.relative_path_from( out_file.dirname )
159
+ template.pageclass = 'index-page'
160
+
161
+ out_file.open( 'w', 0644 ) {|io| io.print(template.render) }
162
+ end
163
+
164
+
165
+ ### Generate a documentation file for each class and module
166
+ def generate_class_files
167
+ layout = self.load_layout_template
168
+ template = self.load_template( 'class.tmpl' )
169
+
170
+ self.log.debug "Generating class documentation in #{self.output_dir}"
171
+
172
+ @classes.each do |klass|
173
+ self.log.debug " working on %s (%s)" % [klass.full_name, klass.path]
174
+
175
+ out_file = self.output_dir + klass.path
176
+ out_file.dirname.mkpath
177
+
178
+ template.klass = klass
179
+
180
+ layout.contents = template
181
+ layout.rel_prefix = self.output_dir.relative_path_from( out_file.dirname )
182
+ layout.pageclass = 'class-page'
183
+
184
+ out_file.open( 'w', 0644 ) {|io| io.print(layout.render) }
185
+ end
186
+ end
187
+
188
+
189
+ ### Generate a documentation file for each file
190
+ def generate_file_files
191
+ layout = self.load_layout_template
192
+ template = self.load_template( 'file.tmpl' )
193
+
194
+ self.log.debug "Generating file documentation in #{self.output_dir}"
195
+
196
+ @files.select {|f| f.text? }.each do |file|
197
+ out_file = self.output_dir + file.path
198
+ out_file.dirname.mkpath
199
+
200
+ self.log.debug " working on %s (%s)" % [file.full_name, out_file]
201
+
202
+ template.file = file
203
+
204
+ # If the page itself has an H1, use it for the header, otherwise make one
205
+ # out of the name of the file
206
+ if md = file.description.match( %r{<h1.*?>.*?</h1>}i )
207
+ template.header = md[ 0 ]
208
+ template.description = file.description[ md.offset(0)[1] + 1 .. -1 ]
209
+ else
210
+ template.header = File.basename( file.full_name, File.extname(file.full_name) )
211
+ template.description = file.description
212
+ end
213
+
214
+ layout.contents = template
215
+ layout.rel_prefix = self.output_dir.relative_path_from(out_file.dirname)
216
+ layout.pageclass = 'file-page'
217
+
218
+ out_file.open( 'w', 0644 ) {|io| io.print(layout.render) }
219
+ end
220
+ end
221
+
222
+
223
+ ### Generate a JSON search index for the quicksearch blank.
224
+ def generate_search_index
225
+ out_file = self.output_dir + 'js/searchindex.json'
226
+
227
+ self.log.debug "Generating search index (%s)." % [ out_file ]
228
+ index = []
229
+
230
+ objs = self.get_indexable_objects
231
+ objs.each do |codeobj|
232
+ self.log.debug " #{codeobj.name}..."
233
+ record = codeobj.search_record
234
+ index << {
235
+ name: record[2],
236
+ link: record[4],
237
+ snippet: record[6],
238
+ type: codeobj.class.name.downcase.sub( /.*::/, '' )
239
+ }
240
+ end
241
+
242
+ self.log.debug " dumping JSON..."
243
+ out_file.dirname.mkpath
244
+ ofh = out_file.open( 'w:utf-8', 0644 )
245
+
246
+ json = Yajl.dump( index, pretty: true, indent: "\t" )
247
+
248
+ ofh.puts( 'var SearchIndex = ', json, ';' )
249
+ end
250
+
251
+
252
+ ### Copies static files from the static_path into the output directory
253
+ def copy_static_assets
254
+ asset_paths = self.find_static_assets
255
+
256
+ self.log.debug "Copying assets from paths: %s" % [ asset_paths.join(', ') ]
257
+
258
+ asset_paths.each do |path|
259
+
260
+ # For plain files, just install them
261
+ if path.file?
262
+ self.log.debug " plain file; installing as-is"
263
+ install( path, self.output_dir, :mode => 0644 )
264
+
265
+ # Glob all the files out of subdirectories and install them
266
+ elsif path.directory?
267
+ self.log.debug " directory %p; copying contents" % [ path ]
268
+
269
+ Pathname.glob( path + '{css,fa,fonts,img,js}/**/*'.to_s ).each do |asset|
270
+ next if asset.directory? || asset.basename.to_s.start_with?( '.' )
271
+
272
+ dst = asset.relative_path_from( path )
273
+ dst.dirname.mkpath
274
+
275
+ self.log.debug " %p -> %p" % [ asset, dst ]
276
+ install asset, dst, :mode => 0644
277
+ end
278
+ end
279
+ end
280
+ end
281
+
282
+
283
+ #########
284
+ protected
285
+ #########
286
+
287
+ ### Return an Array of Pathname objects for each file/directory in the
288
+ ### list of static assets that should be copied into the output directory.
289
+ def find_static_assets
290
+ paths = self.options.static_path || []
291
+ self.log.debug "Finding asset paths. Static paths: %p" % [ paths ]
292
+
293
+ # Add each subdirectory of the template dir
294
+ self.log.debug " adding directories under %s" % [ self.template_dir ]
295
+ paths << self.template_dir
296
+ self.log.debug " paths are now: %p" % [ paths ]
297
+
298
+ return paths.flatten.compact.uniq
299
+ end
300
+
301
+
302
+ ### Return a list of the documented modules sorted by salience first, then
303
+ ### by name.
304
+ def get_sorted_module_list(classes)
305
+ nscounts = classes.inject({}) do |counthash, klass|
306
+ top_level = klass.full_name.gsub( /::.*/, '' )
307
+ counthash[top_level] ||= 0
308
+ counthash[top_level] += 1
309
+
310
+ counthash
311
+ end
312
+
313
+ # Sort based on how often the top level namespace occurs, and then on the
314
+ # name of the module -- this works for projects that put their stuff into
315
+ # a namespace, of course, but doesn't hurt if they don't.
316
+ classes.sort_by do |klass|
317
+ top_level = klass.full_name.gsub( /::.*/, '' )
318
+ [nscounts[top_level] * -1, klass.full_name]
319
+ end.select do |klass|
320
+ klass.display?
321
+ end
322
+ end
323
+
324
+
325
+ ### Fetch the template with the specified +name+ from the cache or load it
326
+ ### and cache it.
327
+ def load_template( name )
328
+ unless @template_cache.key?( name )
329
+ @template_cache[ name ] = Inversion::Template.load( name, encoding:'utf-8' )
330
+ end
331
+
332
+ return @template_cache[ name ].dup
333
+ end
334
+
335
+
336
+ ### Load the layout template and return it after setting any values it needs.
337
+ def load_layout_template
338
+ template = self.load_template( 'layout.tmpl' )
339
+
340
+ self.set_toplevel_variables( template )
341
+
342
+ return template
343
+ end
344
+
345
+
346
+ ### Return a list of CodeObjects that belong in the index.
347
+ def get_indexable_objects
348
+ objs = []
349
+
350
+ objs += @classes.select( &:document_self_or_methods ).uniq( &:path )
351
+ objs += @classes.map( &:method_list ).flatten.uniq( &:path )
352
+ objs += @files.select( &:text? )
353
+
354
+ return objs
355
+ end
356
+
357
+
358
+ ### If the "main" page was set in the options, set it in the template, otherwise set the
359
+ ### main page to be the README if one exists.
360
+ def set_toplevel_variables( template )
361
+ mpname = self.options.main_page
362
+ mainpage, files = @files.partition do |f|
363
+ if mpname
364
+ f.full_name == mpname
365
+ else
366
+ f.full_name =~ /\breadme\b/i
367
+ end
368
+ end
369
+
370
+ template.files = files
371
+ if mainpage.first
372
+ template.mainpage = mainpage.first
373
+ template.synopsis = self.extract_synopsis( mainpage.first )
374
+ end
375
+
376
+ template.classes = @classes
377
+ template.methods = @methods
378
+ template.modsort = @modsort
379
+ template.rdoc_options = @options
380
+
381
+ template.rdoc_version = RDoc::VERSION
382
+ template.sixfish_version = Sixfish.version_string
383
+
384
+ end
385
+
386
+
387
+ ### Extract a synopsis for the project from the specified +mainpage+ and
388
+ ### return it as a String.
389
+ def extract_synopsis( mainpage )
390
+ desc = mainpage.description
391
+ heading = desc[ %r{(<h1.*?/h1>)}im ] || ''
392
+ paras = desc.scan( %r{<p\b.*?/p>}im )
393
+
394
+ first_para = paras.map( &:strip ).find do |para|
395
+ # Discard paragraphs consisting only of a link
396
+ !( para.start_with?('<p><a') && para.end_with?('/a></p>') )
397
+ end
398
+
399
+ return first_para
400
+ end
401
+
402
+ end # class RDoc::Generator::Sixfish
403
+
404
+
405
+ # Reopen to add custom option attrs.
406
+ class RDoc::Options
407
+
408
+ ##
409
+ # Allow setting a custom stylesheet
410
+ attr_accessor :additional_stylesheet
411
+
412
+ end
413
+
@@ -0,0 +1,73 @@
1
+ # -*- ruby -*-
2
+ # frozen_string_literal: true
3
+
4
+ require 'rdoc/markup/to_html'
5
+
6
+ require 'sixfish' unless defined?( Sixfish )
7
+
8
+
9
+ module Sixfish::Patches
10
+
11
+ LIST_TYPE_TO_HTML = {
12
+ :BULLET => ['<ul>', '</ul>'],
13
+ :LABEL => ['<dl class="rdoc-list label-list">', '</dl>'],
14
+ :LALPHA => ['<ol style="list-style-type: lower-alpha">', '</ol>'],
15
+ :NOTE => [
16
+ '<table class="rdoc-list note-list table box"><tbody>',
17
+ '</tbody></table>'
18
+ ],
19
+ :NUMBER => ['<ol>', '</ol>'],
20
+ :UALPHA => ['<ol style="list-style-type: upper-alpha">', '</ol>'],
21
+ }
22
+
23
+
24
+ def html_list_name(list_type, open_tag)
25
+ tags = Sixfish::Patches::LIST_TYPE_TO_HTML[list_type]
26
+ raise RDoc::Error, "Invalid list type: #{list_type.inspect}" unless tags
27
+ tags[open_tag ? 0 : 1]
28
+ end
29
+
30
+
31
+ def list_item_start(list_item, list_type)
32
+ case list_type
33
+ when :BULLET, :LALPHA, :NUMBER, :UALPHA then
34
+ "<li>"
35
+ when :LABEL, :NOTE then
36
+ Array(list_item.label).map do |label|
37
+ "<tr><td>#{to_html label}\n"
38
+ end.join << "</td><td>"
39
+ else
40
+ raise RDoc::Error, "Invalid list type: #{list_type.inspect}"
41
+ end
42
+ end
43
+
44
+
45
+ def list_end_for(list_type)
46
+ case list_type
47
+ when :BULLET, :LALPHA, :NUMBER, :UALPHA then
48
+ "</li>"
49
+ when :LABEL, :NOTE then
50
+ "</td></tr>"
51
+ else
52
+ raise RDoc::Error, "Invalid list type: #{list_type.inspect}"
53
+ end
54
+ end
55
+
56
+
57
+ def accept_heading( heading )
58
+ level = [6, heading.level].min
59
+ label = heading.label @code_object
60
+
61
+ @res << if @options.output_decoration
62
+ "\n<h#{level} id=\"#{label}\">"
63
+ else
64
+ "\n<h#{level}>"
65
+ end
66
+
67
+ @res << to_html(heading.text)
68
+ @res << "</h#{level}>\n"
69
+ end
70
+
71
+ end # module Sixfish::Patches
72
+
73
+
data/lib/sixfish.rb ADDED
@@ -0,0 +1,31 @@
1
+ # -*- mode: ruby; ruby-indent-level: 4; tab-width: 4 -*-
2
+
3
+ # :title: Sixfish RDoc
4
+ #
5
+ # Toplevel namespace for Sixfish. The main goods are in RDoc::Generator::Sixfish.
6
+ module Sixfish
7
+
8
+ # Library version constant
9
+ VERSION = '0.1.0'
10
+
11
+ # Fivefish project URL
12
+ PROJECT_URL = 'https://hg.sr.ht/~ged/Sixfish'
13
+
14
+
15
+ ### Get the library version. If +include_buildnum+ is true, the version string will
16
+ ### include the VCS rev ID.
17
+ def self::version_string( include_buildnum=false )
18
+ vstring = "Sixfish RDoc %s" % [ VERSION ]
19
+ return vstring
20
+ end
21
+
22
+
23
+ autoload :Patches, 'sixfish/patches'
24
+
25
+ end # module Sixfish
26
+
27
+ require 'rdoc/rdoc'
28
+ require 'rdoc/generator/sixfish'
29
+
30
+ RDoc::Markup::ToHtml.prepend( Sixfish::Patches )
31
+
data/spec/helpers.rb ADDED
@@ -0,0 +1,45 @@
1
+ # -*- ruby -*-
2
+ #encoding: utf-8
3
+
4
+ # SimpleCov test coverage reporting; enable this using the :coverage rake task
5
+ if ENV['COVERAGE']
6
+ $stderr.puts "\n\n>>> Enabling coverage report.\n\n"
7
+ require 'simplecov'
8
+ SimpleCov.start do
9
+ add_filter 'spec'
10
+ add_group "Needing tests" do |file|
11
+ file.covered_percent < 90
12
+ end
13
+ end
14
+ end
15
+
16
+
17
+ require 'loggability'
18
+ require 'loggability/spechelpers'
19
+
20
+ require 'rspec'
21
+ require 'sixfish'
22
+
23
+ Loggability.format_with( :color ) if $stdout.tty?
24
+
25
+
26
+ ### RSpec helper functions.
27
+ module Sixfish::SpecHelpers
28
+ end
29
+
30
+
31
+ ### Mock with RSpec
32
+ RSpec.configure do |config|
33
+ config.run_all_when_everything_filtered = true
34
+ config.filter_run :focus
35
+ config.order = 'random'
36
+ config.mock_with( :rspec ) do |mock|
37
+ mock.syntax = :expect
38
+ end
39
+
40
+ config.include( Loggability::SpecHelpers )
41
+ config.include( Sixfish::SpecHelpers )
42
+ end
43
+
44
+ # vim: set nosta noet ts=4 sw=4:
45
+