rdoc-generator-sixfish 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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
+