pg 0.8.0 → 0.9.0.pre156

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