configurability 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/rake/hg.rb ADDED
@@ -0,0 +1,275 @@
1
+ #
2
+ # Mercurial Rake Tasks
3
+
4
+ require 'enumerator'
5
+
6
+ #
7
+ # Authors:
8
+ # * Michael Granger <ged@FaerieMUD.org>
9
+ #
10
+
11
+ unless defined?( HG_DOTDIR )
12
+
13
+ # Mercurial constants
14
+ HG_DOTDIR = BASEDIR + '.hg'
15
+ HG_STORE = HG_DOTDIR + 'store'
16
+
17
+ IGNORE_FILE = BASEDIR + '.hgignore'
18
+
19
+
20
+ ###
21
+ ### Helpers
22
+ ###
23
+
24
+ module MercurialHelpers
25
+ require './helpers.rb' unless defined?( RakefileHelpers )
26
+ include RakefileHelpers
27
+
28
+ ###############
29
+ module_function
30
+ ###############
31
+
32
+ ### Generate a commit log from a diff and return it as a String.
33
+ def make_commit_log
34
+ diff = read_command_output( 'hg', 'diff' )
35
+ fail "No differences." if diff.empty?
36
+
37
+ return diff
38
+ end
39
+
40
+ ### Generate a commit log and invoke the user's editor on it.
41
+ def edit_commit_log
42
+ diff = make_commit_log()
43
+
44
+ File.open( COMMIT_MSG_FILE, File::WRONLY|File::TRUNC|File::CREAT ) do |fh|
45
+ fh.print( diff )
46
+ end
47
+
48
+ edit( COMMIT_MSG_FILE )
49
+ end
50
+
51
+ ### Generate a changelog.
52
+ def make_changelog
53
+ log = read_command_output( 'hg', 'log', '--style', 'compact' )
54
+ return log
55
+ end
56
+
57
+ ### Get the 'tip' info and return it as a Hash
58
+ def get_tip_info
59
+ data = read_command_output( 'hg', 'tip' )
60
+ return YAML.load( data )
61
+ end
62
+
63
+ ### Return the ID for the current rev
64
+ def get_current_rev
65
+ id = read_command_output( 'hg', '-q', 'identify' )
66
+ return id.chomp
67
+ end
68
+
69
+ ### Read the list of existing tags and return them as an Array
70
+ def get_tags
71
+ taglist = read_command_output( 'hg', 'tags' )
72
+ return taglist.split( /\n/ )
73
+ end
74
+
75
+
76
+ ### Read any remote repo paths known by the current repo and return them as a hash.
77
+ def get_repo_paths
78
+ paths = {}
79
+ pathspec = read_command_output( 'hg', 'paths' )
80
+ pathspec.split.each_slice( 3 ) do |name, _, url|
81
+ paths[ name ] = url
82
+ end
83
+ return paths
84
+ end
85
+
86
+ ### Return the list of files which are of status 'unknown'
87
+ def get_unknown_files
88
+ list = read_command_output( 'hg', 'status', '-un', '--color', 'never' )
89
+ list = list.split( /\n/ )
90
+
91
+ trace "New files: %p" % [ list ]
92
+ return list
93
+ end
94
+
95
+ ### Returns a human-scannable file list by joining and truncating the list if it's too long.
96
+ def humanize_file_list( list, indent=FILE_INDENT )
97
+ listtext = list[0..5].join( "\n#{indent}" )
98
+ if list.length > 5
99
+ listtext << " (and %d other/s)" % [ list.length - 5 ]
100
+ end
101
+
102
+ return listtext
103
+ end
104
+
105
+
106
+ ### Add the list of +pathnames+ to the .hgignore list.
107
+ def hg_ignore_files( *pathnames )
108
+ patterns = pathnames.flatten.collect do |path|
109
+ '^' + Regexp.escape(path) + '$'
110
+ end
111
+ trace "Ignoring %d files." % [ pathnames.length ]
112
+
113
+ IGNORE_FILE.open( File::CREAT|File::WRONLY|File::APPEND, 0644 ) do |fh|
114
+ fh.puts( patterns )
115
+ end
116
+ end
117
+
118
+
119
+ ### Delete the files in the given +filelist+ after confirming with the user.
120
+ def delete_extra_files( filelist )
121
+ description = humanize_file_list( filelist, ' ' )
122
+ log "Files to delete:\n ", description
123
+ ask_for_confirmation( "Really delete them?", false ) do
124
+ filelist.each do |f|
125
+ rm_rf( f, :verbose => true )
126
+ end
127
+ end
128
+ end
129
+
130
+ end # module MercurialHelpers
131
+
132
+
133
+ ### Rakefile support
134
+ def get_vcs_rev( dir='.' )
135
+ return MercurialHelpers.get_current_rev
136
+ end
137
+ def make_changelog
138
+ return MercurialHelpers.make_changelog
139
+ end
140
+
141
+
142
+ ###
143
+ ### Tasks
144
+ ###
145
+
146
+ desc "Mercurial tasks"
147
+ namespace :hg do
148
+ include MercurialHelpers
149
+
150
+ desc "Prepare for a new release"
151
+ task :prep_release do
152
+ tags = get_tags()
153
+ rev = get_current_rev()
154
+
155
+ # Look for a tag for the current release version, and if it exists abort
156
+ if tags.include?( PKG_VERSION )
157
+ error "Version #{PKG_VERSION} already has a tag. Did you mean " +
158
+ "to increment the version in #{VERSION_FILE}?"
159
+ fail
160
+ end
161
+
162
+ # Sign the current rev
163
+ log "Signing rev #{rev}"
164
+ run 'hg', 'sign'
165
+
166
+ # Tag the current rev
167
+ log "Tagging rev #{rev} as #{PKG_VERSION}"
168
+ run 'hg', 'tag', PKG_VERSION
169
+
170
+ # Offer to push
171
+ Rake::Task['hg:push'].invoke
172
+ end
173
+
174
+ desc "Check for new files and offer to add/ignore/delete them."
175
+ task :newfiles do
176
+ log "Checking for new files..."
177
+
178
+ entries = get_unknown_files()
179
+
180
+ unless entries.empty?
181
+ files_to_add = []
182
+ files_to_ignore = []
183
+ files_to_delete = []
184
+
185
+ entries.each do |entry|
186
+ action = prompt_with_default( " #{entry}: (a)dd, (i)gnore, (s)kip (d)elete", 's' )
187
+ case action
188
+ when 'a'
189
+ files_to_add << entry
190
+ when 'i'
191
+ files_to_ignore << entry
192
+ when 'd'
193
+ files_to_delete << entry
194
+ end
195
+ end
196
+
197
+ unless files_to_add.empty?
198
+ run 'hg', 'add', *files_to_add
199
+ end
200
+
201
+ unless files_to_ignore.empty?
202
+ hg_ignore_files( *files_to_ignore )
203
+ end
204
+
205
+ unless files_to_delete.empty?
206
+ delete_extra_files( files_to_delete )
207
+ end
208
+ end
209
+ end
210
+ task :add => :newfiles
211
+
212
+
213
+ desc "Pull and update from the default repo"
214
+ task :pull do
215
+ paths = get_repo_paths()
216
+ if origin_url = paths['default']
217
+ ask_for_confirmation( "Pull and update from '#{origin_url}'?", false ) do
218
+ run 'hg', 'pull', '-u'
219
+ end
220
+ else
221
+ trace "Skipping pull: No 'default' path."
222
+ end
223
+ end
224
+
225
+ desc "Check the current code in if tests pass"
226
+ task :checkin => ['hg:pull', 'hg:newfiles', 'test', COMMIT_MSG_FILE] do
227
+ targets = get_target_args()
228
+ $stderr.puts '---', File.read( COMMIT_MSG_FILE ), '---'
229
+ ask_for_confirmation( "Continue with checkin?" ) do
230
+ run 'hg', 'ci', '-l', COMMIT_MSG_FILE, targets
231
+ rm_f COMMIT_MSG_FILE
232
+ end
233
+ Rake::Task['hg:push'].invoke
234
+ end
235
+ task :commit => :checkin
236
+ task :ci => :checkin
237
+
238
+ CLEAN.include( COMMIT_MSG_FILE )
239
+
240
+ desc "Push to the default origin repo (if there is one)"
241
+ task :push do
242
+ paths = get_repo_paths()
243
+ if origin_url = paths['default']
244
+ ask_for_confirmation( "Push to '#{origin_url}'?", false ) do
245
+ run 'hg', 'push'
246
+ end
247
+ else
248
+ trace "Skipping push: No 'default' path."
249
+ end
250
+ end
251
+
252
+ end
253
+
254
+ if HG_DOTDIR.exist?
255
+ trace "Defining mercurial VCS tasks"
256
+
257
+ desc "Check in all the changes in your current working copy"
258
+ task :ci => 'hg:ci'
259
+ desc "Check in all the changes in your current working copy"
260
+ task :checkin => 'hg:ci'
261
+
262
+ desc "Tag and sign revision before a release"
263
+ task :prep_release => 'hg:prep_release'
264
+
265
+ file COMMIT_MSG_FILE do
266
+ edit_commit_log()
267
+ end
268
+
269
+ else
270
+ trace "Not defining mercurial tasks: no #{HG_DOTDIR}"
271
+ end
272
+
273
+ end
274
+
275
+
data/rake/manual.rb ADDED
@@ -0,0 +1,787 @@
1
+ #
2
+ # Manual-generation Rake tasks and classes
3
+
4
+ #
5
+ # Authors:
6
+ # * Michael Granger <ged@FaerieMUD.org>
7
+ # * Mahlon E. Smith <mahlon@martini.nu>
8
+ #
9
+ # This was born out of a frustration with other static HTML generation modules
10
+ # and systems. I've tried webby, webgen, rote, staticweb, staticmatic, and
11
+ # nanoc, but I didn't find any of them really suitable (except rote, which was
12
+ # excellent but apparently isn't maintained and has a fundamental
13
+ # incompatibilty with Rake because of some questionable monkeypatching.)
14
+ #
15
+ # So, since nothing seemed to scratch my itch, I'm going to scratch it myself.
16
+ #
17
+
18
+ require 'pathname'
19
+ require 'singleton'
20
+ require 'rake/tasklib'
21
+ require 'erb'
22
+
23
+
24
+ ### Namespace for Manual-generation classes
25
+ module Manual
26
+
27
+ ### Manual page-generation class
28
+ class Page
29
+
30
+ ### An abstract filter class for manual content transformation.
31
+ class Filter
32
+ include Singleton
33
+
34
+ # A list of inheriting classes, keyed by normalized name
35
+ @derivatives = {}
36
+ class << self; attr_reader :derivatives; end
37
+
38
+ ### Inheritance callback -- keep track of all inheriting classes for
39
+ ### later.
40
+ def self::inherited( subclass )
41
+ key = subclass.name.
42
+ sub( /^.*::/, '' ).
43
+ gsub( /[^[:alpha:]]+/, '_' ).
44
+ downcase.
45
+ sub( /filter$/, '' )
46
+
47
+ self.derivatives[ key ] = subclass
48
+ self.derivatives[ key.to_sym ] = subclass
49
+
50
+ super
51
+ end
52
+
53
+
54
+ ### Export any static resources required by this filter to the given +output_dir+.
55
+ def export_resources( output_dir )
56
+ # No-op by default
57
+ end
58
+
59
+
60
+ ### Process the +page+'s source with the filter and return the altered content.
61
+ def process( source, page, metadata )
62
+ raise NotImplementedError,
63
+ "%s does not implement the #process method" % [ self.class.name ]
64
+ end
65
+ end # class Filter
66
+
67
+
68
+ ### The default page configuration if none is specified.
69
+ DEFAULT_CONFIG = {
70
+ 'filters' => [ 'erb', 'links', 'textile' ],
71
+ 'layout' => 'default.page',
72
+ 'cleanup' => false,
73
+ }.freeze
74
+
75
+ # Pattern to match a source page with a YAML header
76
+ PAGE_WITH_YAML_HEADER = /
77
+ \A---\s*$ # It should should start with three hyphens
78
+ (.*?) # ...have some YAML stuff
79
+ ^---\s*$ # then have another three-hyphen line,
80
+ (.*)\Z # then the rest of the document
81
+ /xm
82
+
83
+ # Options to pass to libtidy
84
+ TIDY_OPTIONS = {
85
+ :show_warnings => true,
86
+ :indent => true,
87
+ :indent_attributes => false,
88
+ :indent_spaces => 4,
89
+ :vertical_space => true,
90
+ :tab_size => 4,
91
+ :wrap_attributes => true,
92
+ :wrap => 100,
93
+ :char_encoding => 'utf8'
94
+ }
95
+
96
+
97
+ ### Create a new page-generator for the given +sourcefile+, which will use
98
+ ### ones of the templates in +layouts_dir+ as a wrapper. The +basepath+
99
+ ### is the path to the base output directory, and the +catalog+ is the
100
+ ### Manual::PageCatalog to which the page belongs.
101
+ def initialize( catalog, sourcefile, layouts_dir, basepath='.' )
102
+ @catalog = catalog
103
+ @sourcefile = Pathname.new( sourcefile )
104
+ @layouts_dir = Pathname.new( layouts_dir )
105
+ @basepath = basepath
106
+
107
+ rawsource = @sourcefile.read
108
+ @config, @source = self.read_page_config( rawsource )
109
+
110
+ # $stderr.puts "Config is: %p" % [@config],
111
+ # "Source is: %p" % [ @source[0,100] ]
112
+ @filters = self.load_filters( @config['filters'] )
113
+
114
+ super()
115
+ end
116
+
117
+
118
+ ######
119
+ public
120
+ ######
121
+
122
+ # The Manual::PageCatalog to which the page belongs
123
+ attr_reader :catalog
124
+
125
+ # The relative path to the base directory, for prepending to page paths
126
+ attr_reader :basepath
127
+
128
+ # The Pathname object that specifys the page source file
129
+ attr_reader :sourcefile
130
+
131
+ # The configured layouts directory as a Pathname object.
132
+ attr_reader :layouts_dir
133
+
134
+ # The page configuration, as read from its YAML header
135
+ attr_reader :config
136
+
137
+ # The raw source of the page
138
+ attr_reader :source
139
+
140
+ # The filters the page will use to render itself
141
+ attr_reader :filters
142
+
143
+
144
+ ### Generate HTML output from the page and return it.
145
+ def generate( metadata )
146
+ content = self.generate_content( @source, metadata )
147
+
148
+ layout = self.config['layout'].sub( /\.page$/, '' )
149
+ templatepath = @layouts_dir + "#{layout}.page"
150
+ template = nil
151
+ if Object.const_defined?( :Encoding )
152
+ template = ERB.new( templatepath.read(:encoding => 'UTF-8') )
153
+ else
154
+ template = ERB.new( templatepath.read )
155
+ end
156
+
157
+ page = self
158
+ html = template.result( binding() )
159
+
160
+ # Use Tidy to clean up the html if 'cleanup' is turned on, but remove the Tidy
161
+ # meta-generator propaganda/advertising.
162
+ html = self.cleanup( html ).sub( %r:<meta name="generator"[^>]*tidy[^>]*/>:im, '' ) if
163
+ self.config['cleanup']
164
+
165
+ return html
166
+ end
167
+
168
+
169
+ ### Return the page title as specified in the YAML options
170
+ def title
171
+ return self.config['title'] || self.sourcefile.basename
172
+ end
173
+
174
+
175
+ ### Run the various filters on the given input and return the transformed
176
+ ### content.
177
+ def generate_content( input, metadata )
178
+ return @filters.inject( input ) do |source, filter|
179
+ filter.process( source, self, metadata )
180
+ end
181
+ end
182
+
183
+
184
+ ### Trim the YAML header from the provided page +source+, convert it to
185
+ ### a Ruby object, and return it.
186
+ def read_page_config( source )
187
+ unless source =~ PAGE_WITH_YAML_HEADER
188
+ return DEFAULT_CONFIG.dup, source
189
+ end
190
+
191
+ pageconfig = YAML.load( $1 )
192
+ source = $2
193
+
194
+ return DEFAULT_CONFIG.merge( pageconfig ), source
195
+ end
196
+
197
+
198
+ ### Clean up and return the given HTML +source+.
199
+ def cleanup( source )
200
+ require 'tidy'
201
+
202
+ Tidy.path = '/usr/lib/libtidy.dylib'
203
+ Tidy.open( TIDY_OPTIONS ) do |tidy|
204
+ tidy.options.output_xhtml = true
205
+
206
+ xml = tidy.clean( source )
207
+ errors = tidy.errors
208
+ error_message( errors.join ) unless errors.empty?
209
+ trace tidy.diagnostics
210
+ return xml
211
+ end
212
+ rescue LoadError => err
213
+ trace "No cleanup: " + err.message
214
+ return source
215
+ end
216
+
217
+
218
+ ### Get (singleton) instances of the filters named in +filterlist+ and return them.
219
+ def load_filters( filterlist )
220
+ filterlist.flatten.collect do |key|
221
+ raise ArgumentError, "filter '#{key}' is not loaded" unless
222
+ Manual::Page::Filter.derivatives.key?( key )
223
+ Manual::Page::Filter.derivatives[ key ].instance
224
+ end
225
+ end
226
+
227
+
228
+ ### Build the index relative to the receiving page and return it as a String
229
+ def make_index_html
230
+ items = [ '<div class="index">' ]
231
+
232
+ @catalog.traverse_page_hierarchy( self ) do |type, title, path|
233
+ case type
234
+ when :section
235
+ items << %Q{<div class="section">}
236
+ items << %Q{<h2><a href="#{self.basepath + path}/">#{title}</a></h2>}
237
+ items << '<ul class="index-section">'
238
+
239
+ when :current_section
240
+ items << %Q{<div class="section current-section">}
241
+ items << %Q{<h2><a href="#{self.basepath + path}/">#{title}</a></h2>}
242
+ items << '<ul class="index-section current-index-section">'
243
+
244
+ when :section_end, :current_section_end
245
+ items << '</ul></div>'
246
+
247
+ when :entry
248
+ items << %Q{<li><a href="#{self.basepath + path}.html">#{title}</a></li>}
249
+
250
+ when :current_entry
251
+ items << %Q{<li class="current-entry">#{title}</li>}
252
+
253
+ else
254
+ raise "Unknown index entry type %p" % [ type ]
255
+ end
256
+
257
+ end
258
+
259
+ items << '</div>'
260
+
261
+ return items.join("\n")
262
+ end
263
+
264
+ end
265
+
266
+
267
+ ### A catalog of Manual::Page objects that can be referenced by various criteria.
268
+ class PageCatalog
269
+
270
+ ### Create a new PageCatalog that will load Manual::Page objects for .page files
271
+ ### in the specified +sourcedir+.
272
+ def initialize( sourcedir, layoutsdir )
273
+ @sourcedir = sourcedir
274
+ @layoutsdir = layoutsdir
275
+
276
+ @pages = []
277
+ @path_index = {}
278
+ @uri_index = {}
279
+ @title_index = {}
280
+ @hierarchy = {}
281
+
282
+ self.find_and_load_pages
283
+ end
284
+
285
+
286
+ ######
287
+ public
288
+ ######
289
+
290
+ # An index of the pages in the catalog by Pathname
291
+ attr_reader :path_index
292
+
293
+ # An index of the pages in the catalog by title
294
+ attr_reader :title_index
295
+
296
+ # An index of the pages in the catalog by the URI of their source relative to the source
297
+ # directory
298
+ attr_reader :uri_index
299
+
300
+ # The hierarchy of pages in the catalog, suitable for generating an on-page index
301
+ attr_reader :hierarchy
302
+
303
+ # An Array of all Manual::Page objects found
304
+ attr_reader :pages
305
+
306
+ # The Pathname location of the .page files.
307
+ attr_reader :sourcedir
308
+
309
+ # The Pathname location of look and feel templates.
310
+ attr_reader :layoutsdir
311
+
312
+
313
+ ### Traverse the catalog's #hierarchy, yielding to the given +builder+
314
+ ### block for each entry, as well as each time a sub-hash is entered or
315
+ ### exited, setting the +type+ appropriately. Valid values for +type+ are:
316
+ ###
317
+ ### :entry, :section, :section_end
318
+ ###
319
+ ### If the optional +from+ value is given, it should be the Manual::Page object
320
+ ### which is considered "current"; if the +from+ object is the same as the
321
+ ### hierarchy entry being yielded, it will be yielded with the +type+ set to
322
+ ### one of:
323
+ ###
324
+ ### :current_entry, :current_section, :current_section_end
325
+ ###
326
+ ### each of which correspond to the like-named type from above.
327
+ def traverse_page_hierarchy( from=nil, &builder ) # :yields: type, title, path
328
+ raise LocalJumpError, "no block given" unless builder
329
+ self.traverse_hierarchy( Pathname.new(''), self.hierarchy, from, &builder )
330
+ end
331
+
332
+
333
+ #########
334
+ protected
335
+ #########
336
+
337
+ ### Sort and traverse the specified +hash+ recursively, yielding for each entry.
338
+ def traverse_hierarchy( path, hash, from=nil, &builder )
339
+ # Now generate the index in the sorted order
340
+ sort_hierarchy( hash ).each do |subpath, page_or_section|
341
+ if page_or_section.is_a?( Hash )
342
+ self.handle_section_callback( path + subpath, page_or_section, from, &builder )
343
+ else
344
+ next if subpath == INDEX_PATH
345
+ self.handle_page_callback( path + subpath, page_or_section, from, &builder )
346
+ end
347
+ end
348
+ end
349
+
350
+
351
+ ### Return the specified hierarchy of pages as a sorted Array of tuples.
352
+ ### Sort the hierarchy using the 'index' config value of either the
353
+ ### page, or the directory's index page if it's a directory.
354
+ def sort_hierarchy( hierarchy )
355
+ hierarchy.sort_by do |subpath, page_or_section|
356
+
357
+ # Directory
358
+ if page_or_section.is_a?( Hash )
359
+
360
+ # Use the index of the index page if it exists
361
+ if page_or_section[INDEX_PATH]
362
+ idx = page_or_section[INDEX_PATH].config['index']
363
+ trace "Index page's index for directory '%s' is: %p" % [ subpath, idx ]
364
+ idx.to_s || subpath.to_s
365
+ else
366
+ trace "Using the path for the sort of directory %p" % [ subpath ]
367
+ subpath.to_s
368
+ end
369
+
370
+ # Page
371
+ else
372
+ if subpath == INDEX_PATH
373
+ trace "Sort index for index page %p is 0" % [ subpath ]
374
+ '0'
375
+ else
376
+ idx = page_or_section.config['index']
377
+ trace "Sort index for page %p is: %p" % [ subpath, idx ]
378
+ idx.to_s || subpath.to_s
379
+ end
380
+ end
381
+
382
+ end # sort_by
383
+ end
384
+
385
+
386
+ INDEX_PATH = Pathname.new('index')
387
+
388
+ ### Build up the data structures necessary for calling the +builder+ callback
389
+ ### for an index section and call it, then recurse into the section contents.
390
+ def handle_section_callback( path, section, from=nil, &builder )
391
+ from_current = false
392
+ trace "Section handler: path=%p, section keys=%p, from=%s" %
393
+ [ path, section.keys, from.sourcefile ]
394
+
395
+ # Call the callback with :section -- determine the section title from
396
+ # the 'index.page' file underneath it, or the directory name if no
397
+ # index.page exists.
398
+ if section.key?( INDEX_PATH )
399
+ if section[INDEX_PATH].sourcefile.dirname == from.sourcefile.dirname
400
+ from_current = true
401
+ builder.call( :current_section, section[INDEX_PATH].title, path )
402
+ else
403
+ builder.call( :section, section[INDEX_PATH].title, path )
404
+ end
405
+ else
406
+ title = File.dirname( path ).gsub( /_/, ' ' )
407
+ builder.call( :section, title, path )
408
+ end
409
+
410
+ # Recurse
411
+ self.traverse_hierarchy( path, section, from, &builder )
412
+
413
+ # Call the callback with :section_end
414
+ if from_current
415
+ builder.call( :current_section_end, '', path )
416
+ else
417
+ builder.call( :section_end, '', path )
418
+ end
419
+ end
420
+
421
+
422
+ ### Yield the specified +page+ to the builder
423
+ def handle_page_callback( path, page, from=nil )
424
+ if from == page
425
+ yield( :current_entry, page.title, path )
426
+ else
427
+ yield( :entry, page.title, path )
428
+ end
429
+ end
430
+
431
+
432
+ ### Find and store
433
+
434
+ ### Find all .page files under the configured +sourcedir+ and create a new
435
+ ### Manual::Page object for each one.
436
+ def find_and_load_pages
437
+ Pathname.glob( @sourcedir + '**/*.page' ).each do |pagefile|
438
+ path_to_base = @sourcedir.relative_path_from( pagefile.dirname )
439
+
440
+ page = Manual::Page.new( self, pagefile, @layoutsdir, path_to_base )
441
+ hierpath = pagefile.relative_path_from( @sourcedir )
442
+
443
+ @pages << page
444
+ @path_index[ pagefile ] = page
445
+ @title_index[ page.title ] = page
446
+ @uri_index[ hierpath.to_s ] = page
447
+
448
+ # Place the page in the page hierarchy by using inject to find and/or create the
449
+ # necessary subhashes. The last run of inject will return the leaf hash in which
450
+ # the page will live
451
+ section = hierpath.dirname.split[1..-1].inject( @hierarchy ) do |hier, component|
452
+ hier[ component ] ||= {}
453
+ hier[ component ]
454
+ end
455
+
456
+ section[ pagefile.basename('.page') ] = page
457
+ end
458
+ end
459
+
460
+ end
461
+
462
+
463
+ ### A Textile filter for the manual generation tasklib, implemented using RedCloth.
464
+ class TextileFilter < Manual::Page::Filter
465
+
466
+ ### Load RedCloth when the filter is first created
467
+ def initialize( *args )
468
+ require 'redcloth'
469
+ super
470
+ end
471
+
472
+
473
+ ### Process the given +source+ as Textile and return the resulting HTML
474
+ ### fragment.
475
+ def process( source, *ignored )
476
+ formatter = RedCloth::TextileDoc.new( source )
477
+ formatter.hard_breaks = false
478
+ formatter.no_span_caps = true
479
+ return formatter.to_html
480
+ end
481
+
482
+ end
483
+
484
+
485
+ ### An ERB filter for the manual generation tasklib, implemented using Erubis.
486
+ class ErbFilter < Manual::Page::Filter
487
+
488
+ ### Process the given +source+ as ERB and return the resulting HTML
489
+ ### fragment.
490
+ def process( source, page, metadata )
491
+ template_name = page.sourcefile.basename
492
+ template = ERB.new( source )
493
+ return template.result( binding() )
494
+ end
495
+
496
+ end
497
+
498
+
499
+ ### Manual generation task library
500
+ class GenTask < Rake::TaskLib
501
+
502
+ # Default values for task config variables
503
+ DEFAULT_NAME = :manual
504
+ DEFAULT_BASE_DIR = Pathname.new( 'docs/manual' )
505
+ DEFAULT_SOURCE_DIR = 'source'
506
+ DEFAULT_LAYOUTS_DIR = 'layouts'
507
+ DEFAULT_OUTPUT_DIR = 'output'
508
+ DEFAULT_RESOURCE_DIR = 'resources'
509
+ DEFAULT_LIB_DIR = 'lib'
510
+ DEFAULT_METADATA = OpenStruct.new
511
+
512
+
513
+ ### Define a new manual-generation task with the given +name+.
514
+ def initialize( name=:manual )
515
+ @name = name
516
+
517
+ @source_dir = DEFAULT_SOURCE_DIR
518
+ @layouts_dir = DEFAULT_LAYOUTS_DIR
519
+ @output_dir = DEFAULT_OUTPUT_DIR
520
+ @resource_dir = DEFAULT_RESOURCE_DIR
521
+ @lib_dir = DEFAULT_LIB_DIR
522
+ @metadata = DEFAULT_METADATA
523
+
524
+ yield( self ) if block_given?
525
+
526
+ self.define
527
+ end
528
+
529
+
530
+ ######
531
+ public
532
+ ######
533
+
534
+ attr_accessor :base_dir,
535
+ :source_dir,
536
+ :layouts_dir,
537
+ :output_dir,
538
+ :resource_dir,
539
+ :lib_dir,
540
+ :metadata
541
+
542
+ attr_reader :name
543
+
544
+
545
+ ### Set up the tasks for building the manual
546
+ def define
547
+
548
+ # Set up a description if the caller hasn't already defined one
549
+ unless Rake.application.last_comment
550
+ desc "Generate the manual"
551
+ end
552
+
553
+ # Make Pathnames of the directories relative to the base_dir
554
+ basedir = Pathname.new( @base_dir )
555
+ sourcedir = basedir + @source_dir
556
+ layoutsdir = basedir + @layouts_dir
557
+ outputdir = @output_dir
558
+ resourcedir = basedir + @resource_dir
559
+ libdir = basedir + @lib_dir
560
+
561
+ load_filter_libraries( libdir )
562
+ catalog = Manual::PageCatalog.new( sourcedir, layoutsdir )
563
+
564
+ # Declare the tasks outside the namespace that point in
565
+ task @name => "#@name:build"
566
+ task "clobber_#@name" => "#@name:clobber"
567
+
568
+ namespace( self.name ) do
569
+ setup_resource_copy_tasks( resourcedir, outputdir )
570
+ manual_pages = setup_page_conversion_tasks( sourcedir, outputdir, catalog )
571
+
572
+ desc "Build the manual"
573
+ task :build => [ :apidocs, :copy_resources, :copy_apidocs, :generate_pages ]
574
+
575
+ task :clobber do
576
+ RakeFileUtils.verbose( $verbose ) do
577
+ rm_f manual_pages.to_a
578
+ end
579
+ remove_dir( outputdir ) if ( outputdir + '.buildtime' ).exist?
580
+ end
581
+
582
+ desc "Remove any previously-generated parts of the manual and rebuild it"
583
+ task :rebuild => [ :clobber, self.name ]
584
+
585
+ desc "Watch for changes to the source files and rebuild when they change"
586
+ task :autobuild do
587
+ scope = [ self.name ]
588
+ loop do
589
+ t = Rake.application.lookup( :build, scope )
590
+ t.reenable
591
+ t.prerequisites.each do |pt|
592
+ if task = Rake.application.lookup( pt, scope )
593
+ task.reenable
594
+ else
595
+ trace "Hmmm... no %p task in scope %p?" % [ pt, scope ]
596
+ end
597
+ end
598
+ t.invoke
599
+ sleep 2
600
+ trace " waking up..."
601
+ end
602
+ end
603
+ end
604
+
605
+ end # def define
606
+
607
+
608
+ ### Load the filter libraries provided in the given +libdir+
609
+ def load_filter_libraries( libdir )
610
+ Pathname.glob( libdir + '*.rb' ) do |filterlib|
611
+ trace " loading filter library #{filterlib}"
612
+ require( filterlib )
613
+ end
614
+ end
615
+
616
+
617
+ ### Set up the main HTML-generation task that will convert files in the given +sourcedir+ to
618
+ ### HTML in the +outputdir+
619
+ def setup_page_conversion_tasks( sourcedir, outputdir, catalog )
620
+
621
+ # we need to figure out what HTML pages need to be generated so we can set up the
622
+ # dependency that causes the rule to be fired for each one when the task is invoked.
623
+ manual_sources = FileList[ catalog.path_index.keys.map {|pn| pn.to_s} ]
624
+ trace " found %d source files" % [ manual_sources.length ]
625
+
626
+ # Map .page files to their equivalent .html output
627
+ html_pathmap = "%%{%s,%s}X.html" % [ sourcedir, outputdir ]
628
+ manual_pages = manual_sources.pathmap( html_pathmap )
629
+ trace "Mapping sources like so: \n %p -> %p" %
630
+ [ manual_sources.first, manual_pages.first ]
631
+
632
+ # Output directory task
633
+ directory( outputdir.to_s )
634
+ file outputdir.to_s do
635
+ touch outputdir + '.buildtime'
636
+ end
637
+
638
+ # Rule to generate .html files from .page files
639
+ rule(
640
+ %r{#{outputdir}/.*\.html$} => [
641
+ proc {|name| name.sub(/\.[^.]+$/, '.page').sub( outputdir, sourcedir) },
642
+ outputdir.to_s
643
+ ]) do |task|
644
+
645
+ source = Pathname.new( task.source )
646
+ target = Pathname.new( task.name )
647
+ log " #{ source } -> #{ target }"
648
+
649
+ page = catalog.path_index[ source ]
650
+ #trace " page object is: %p" % [ page ]
651
+
652
+ target.dirname.mkpath
653
+ target.open( File::WRONLY|File::CREAT|File::TRUNC ) do |io|
654
+ io.write( page.generate(metadata) )
655
+ end
656
+ end
657
+
658
+ # Group all the manual page output files targets into a containing task
659
+ desc "Generate any pages of the manual that have changed"
660
+ task :generate_pages => manual_pages
661
+ return manual_pages
662
+ end
663
+
664
+
665
+ ### Copy method for resources -- passed as a block to the various file tasks that copy
666
+ ### resources to the output directory.
667
+ def copy_resource( task )
668
+ source = task.prerequisites[ 1 ]
669
+ target = task.name
670
+
671
+ when_writing do
672
+ trace " #{source} -> #{target}"
673
+ mkpath File.dirname( target ), :verbose => $trace unless
674
+ File.directory?( File.dirname(target) )
675
+ install source, target, :mode => 0644, :verbose => $trace
676
+ end
677
+ end
678
+
679
+
680
+ ### Set up a rule for copying files from the resources directory to the output dir.
681
+ def setup_resource_copy_tasks( resourcedir, outputdir )
682
+ resources = FileList[ resourcedir + '**/*.{js,css,png,gif,jpg,html,svg,svgz,swf}' ]
683
+ resources.exclude( /\.svn/ )
684
+ target_pathmap = "%%{%s,%s}p" % [ resourcedir, outputdir ]
685
+ targets = resources.pathmap( target_pathmap )
686
+ copier = self.method( :copy_resource ).to_proc
687
+
688
+ # Create a file task to copy each file to the output directory
689
+ resources.each_with_index do |resource, i|
690
+ file( targets[i] => [ outputdir.to_s, resource ], &copier )
691
+ end
692
+
693
+ desc "Copy API documentation to the manual output directory"
694
+ task :copy_apidocs => :apidocs do
695
+ cp_r( API_DOCSDIR, outputdir )
696
+ end
697
+
698
+ # Now group all the resource file tasks into a containing task
699
+ desc "Copy manual resources to the output directory"
700
+ task :copy_resources => targets do
701
+ log "Copying manual resources"
702
+ end
703
+ end
704
+
705
+ end # class Manual::GenTask
706
+
707
+ end
708
+
709
+
710
+
711
+ ### Task: manual generation
712
+ if MANUALDIR.exist?
713
+ MANUALOUTPUTDIR = MANUALDIR + 'output'
714
+ trace "Manual will be generated in: #{MANUALOUTPUTDIR}"
715
+
716
+ begin
717
+ directory MANUALOUTPUTDIR.to_s
718
+
719
+ Manual::GenTask.new do |manual|
720
+ manual.metadata.version = PKG_VERSION
721
+ manual.metadata.api_dir = API_DOCSDIR
722
+ manual.output_dir = MANUALOUTPUTDIR
723
+ manual.base_dir = MANUALDIR
724
+ manual.source_dir = 'src'
725
+ end
726
+
727
+ CLOBBER.include( MANUALOUTPUTDIR.to_s )
728
+
729
+ rescue LoadError => err
730
+ task :no_manual do
731
+ $stderr.puts "Manual-generation tasks not defined: %s" % [ err.message ]
732
+ end
733
+
734
+ task :manual => :no_manual
735
+ task :clobber_manual => :no_manual
736
+ end
737
+
738
+ else
739
+ TEMPLATEDIR = RAKE_TASKDIR + 'manualdir'
740
+
741
+ if TEMPLATEDIR.exist?
742
+
743
+ desc "Create a manual for this project from a template"
744
+ task :manual do
745
+ log "No manual directory (#{MANUALDIR}) currently exists."
746
+ ask_for_confirmation( "Create a new manual directory tree from a template?" ) do
747
+ MANUALDIR.mkpath
748
+
749
+ %w[layouts lib output resources src].each do |dir|
750
+ FileUtils.mkpath( MANUALDIR + dir, :mode => 0755, :verbose => true, :noop => $dryrun )
751
+ end
752
+
753
+ Pathname.glob( TEMPLATEDIR + '**/*.{rb,css,png,js,erb,page}' ).each do |tmplfile|
754
+ trace "extname is: #{tmplfile.extname}"
755
+
756
+ # Render ERB files
757
+ if tmplfile.extname == '.erb'
758
+ rname = tmplfile.basename( '.erb' )
759
+ target = MANUALDIR + tmplfile.dirname.relative_path_from( TEMPLATEDIR ) + rname
760
+ template = ERB.new( tmplfile.read, nil, '<>' )
761
+
762
+ target.dirname.mkpath( :mode => 0755, :verbose => true, :noop => $dryrun ) unless
763
+ target.dirname.directory?
764
+ html = template.result( binding() )
765
+ log "generating #{target}: html => #{html[0,20]}"
766
+
767
+ target.open( File::WRONLY|File::CREAT|File::EXCL, 0644 ) do |fh|
768
+ fh.print( html )
769
+ end
770
+
771
+ # Just copy anything else
772
+ else
773
+ target = MANUALDIR + tmplfile.relative_path_from( TEMPLATEDIR )
774
+ FileUtils.mkpath target.dirname,
775
+ :mode => 0755, :verbose => true, :noop => $dryrun unless target.dirname.directory?
776
+ FileUtils.install tmplfile, target,
777
+ :mode => 0644, :verbose => true, :noop => $dryrun
778
+ end
779
+ end
780
+ end
781
+
782
+ end # task :manual
783
+
784
+ end
785
+ end
786
+
787
+