rdoc-generator-sixfish 0.5.0.pre20180710102525

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