linkparser 1.0.3

Sign up to get free protection for your applications and to get access to all the features.
data/rake/helpers.rb ADDED
@@ -0,0 +1,395 @@
1
+ #####################################################################
2
+ ### G L O B A L H E L P E R F U N C T I O N S
3
+ #####################################################################
4
+
5
+ require 'pathname'
6
+ require 'readline'
7
+
8
+ # Set some ANSI escape code constants (Shamelessly stolen from Perl's
9
+ # Term::ANSIColor by Russ Allbery <rra@stanford.edu> and Zenin <zenin@best.com>
10
+ ANSI_ATTRIBUTES = {
11
+ 'clear' => 0,
12
+ 'reset' => 0,
13
+ 'bold' => 1,
14
+ 'dark' => 2,
15
+ 'underline' => 4,
16
+ 'underscore' => 4,
17
+ 'blink' => 5,
18
+ 'reverse' => 7,
19
+ 'concealed' => 8,
20
+
21
+ 'black' => 30, 'on_black' => 40,
22
+ 'red' => 31, 'on_red' => 41,
23
+ 'green' => 32, 'on_green' => 42,
24
+ 'yellow' => 33, 'on_yellow' => 43,
25
+ 'blue' => 34, 'on_blue' => 44,
26
+ 'magenta' => 35, 'on_magenta' => 45,
27
+ 'cyan' => 36, 'on_cyan' => 46,
28
+ 'white' => 37, 'on_white' => 47
29
+ }
30
+
31
+
32
+ MULTILINE_PROMPT = <<-'EOF'
33
+ Enter one or more values for '%s'.
34
+ A blank line finishes input.
35
+ EOF
36
+
37
+
38
+ CLEAR_TO_EOL = "\e[K"
39
+ CLEAR_CURRENT_LINE = "\e[2K"
40
+
41
+
42
+ ### Output a logging message
43
+ def log( *msg )
44
+ output = colorize( msg.flatten.join(' '), 'cyan' )
45
+ $deferr.puts( output )
46
+ end
47
+
48
+
49
+ ### Output a logging message if tracing is on
50
+ def trace( *msg )
51
+ return unless $trace
52
+ output = colorize( msg.flatten.join(' '), 'yellow' )
53
+ $deferr.puts( output )
54
+ end
55
+
56
+
57
+ ### Run the specified command +cmd+ with system(), failing if the execution
58
+ ### fails.
59
+ def run( *cmd )
60
+ cmd.flatten!
61
+
62
+ if cmd.length > 1
63
+ trace( cmd.collect {|part| part =~ /\s/ ? part.inspect : part} )
64
+ else
65
+ trace( cmd )
66
+ end
67
+
68
+ if $dryrun
69
+ $deferr.puts "(dry run mode)"
70
+ else
71
+ system( *cmd )
72
+ unless $?.success?
73
+ fail "Command failed: [%s]" % [cmd.join(' ')]
74
+ end
75
+ end
76
+ end
77
+
78
+
79
+ ### Run a subordinate Rake process with the same options and the specified +targets+.
80
+ def rake( *targets )
81
+ opts = ARGV.select {|arg| arg[0,1] == '-' }
82
+ args = opts + targets.map {|t| t.to_s }
83
+ run 'rake', '-N', *args
84
+ end
85
+
86
+
87
+ ### Open a pipe to a process running the given +cmd+ and call the given block with it.
88
+ def pipeto( *cmd )
89
+ $DEBUG = true
90
+
91
+ cmd.flatten!
92
+ log( "Opening a pipe to: ", cmd.collect {|part| part =~ /\s/ ? part.inspect : part} )
93
+ if $dryrun
94
+ $deferr.puts "(dry run mode)"
95
+ else
96
+ open( '|-', 'w+' ) do |io|
97
+
98
+ # Parent
99
+ if io
100
+ yield( io )
101
+
102
+ # Child
103
+ else
104
+ exec( *cmd )
105
+ fail "Command failed: [%s]" % [cmd.join(' ')]
106
+ end
107
+ end
108
+ end
109
+ end
110
+
111
+
112
+ ### Download the file at +sourceuri+ via HTTP and write it to +targetfile+.
113
+ def download( sourceuri, targetfile=nil )
114
+ oldsync = $defout.sync
115
+ $defout.sync = true
116
+ require 'net/http'
117
+ require 'uri'
118
+
119
+ targetpath = Pathname.new( targetfile )
120
+
121
+ log "Downloading %s to %s" % [sourceuri, targetfile]
122
+ targetpath.open( File::WRONLY|File::TRUNC|File::CREAT, 0644 ) do |ofh|
123
+
124
+ url = sourceuri.is_a?( URI ) ? sourceuri : URI.parse( sourceuri )
125
+ downloaded = false
126
+ limit = 5
127
+
128
+ until downloaded or limit.zero?
129
+ Net::HTTP.start( url.host, url.port ) do |http|
130
+ req = Net::HTTP::Get.new( url.path )
131
+
132
+ http.request( req ) do |res|
133
+ if res.is_a?( Net::HTTPSuccess )
134
+ log "Downloading..."
135
+ res.read_body do |buf|
136
+ ofh.print( buf )
137
+ end
138
+ downloaded = true
139
+ puts "done."
140
+
141
+ elsif res.is_a?( Net::HTTPRedirection )
142
+ url = URI.parse( res['location'] )
143
+ log "...following redirection to: %s" % [ url ]
144
+ limit -= 1
145
+ sleep 0.2
146
+ next
147
+
148
+ else
149
+ res.error!
150
+ end
151
+ end
152
+ end
153
+ end
154
+
155
+ end
156
+
157
+ return targetpath
158
+ ensure
159
+ $defout.sync = oldsync
160
+ end
161
+
162
+
163
+ ### Return the fully-qualified path to the specified +program+ in the PATH.
164
+ def which( program )
165
+ ENV['PATH'].split(/:/).
166
+ collect {|dir| Pathname.new(dir) + program }.
167
+ find {|path| path.exist? && path.executable? }
168
+ end
169
+
170
+
171
+ ### Create a string that contains the ANSI codes specified and return it
172
+ def ansi_code( *attributes )
173
+ attributes.flatten!
174
+ attributes.collect! {|at| at.to_s }
175
+ # $deferr.puts "Returning ansicode for TERM = %p: %p" %
176
+ # [ ENV['TERM'], attributes ]
177
+ return '' unless /(?:vt10[03]|xterm(?:-color)?|linux|screen)/i =~ ENV['TERM']
178
+ attributes = ANSI_ATTRIBUTES.values_at( *attributes ).compact.join(';')
179
+
180
+ # $deferr.puts " attr is: %p" % [attributes]
181
+ if attributes.empty?
182
+ return ''
183
+ else
184
+ return "\e[%sm" % attributes
185
+ end
186
+ end
187
+
188
+
189
+ ### Colorize the given +string+ with the specified +attributes+ and return it, handling
190
+ ### line-endings, color reset, etc.
191
+ def colorize( *args )
192
+ string = ''
193
+
194
+ if block_given?
195
+ string = yield
196
+ else
197
+ string = args.shift
198
+ end
199
+
200
+ ending = string[/(\s)$/] || ''
201
+ string = string.rstrip
202
+
203
+ return ansi_code( args.flatten ) + string + ansi_code( 'reset' ) + ending
204
+ end
205
+
206
+
207
+ ### Output the specified <tt>msg</tt> as an ANSI-colored error message
208
+ ### (white on red).
209
+ def error_message( msg, details='' )
210
+ $deferr.puts colorize( 'bold', 'white', 'on_red' ) { msg } + details
211
+ end
212
+ alias :error :error_message
213
+
214
+
215
+ ### Highlight and embed a prompt control character in the given +string+ and return it.
216
+ def make_prompt_string( string )
217
+ return CLEAR_CURRENT_LINE + colorize( 'bold', 'green' ) { string + ' ' }
218
+ end
219
+
220
+
221
+ ### Output the specified <tt>prompt_string</tt> as a prompt (in green) and
222
+ ### return the user's input with leading and trailing spaces removed. If a
223
+ ### test is provided, the prompt will repeat until the test returns true.
224
+ ### An optional failure message can also be passed in.
225
+ def prompt( prompt_string, failure_msg="Try again." ) # :yields: response
226
+ prompt_string.chomp!
227
+ prompt_string << ":" unless /\W$/.match( prompt_string )
228
+ response = nil
229
+
230
+ begin
231
+ prompt = make_prompt_string( prompt_string )
232
+ response = Readline.readline( prompt ) || ''
233
+ response.strip!
234
+ if block_given? && ! yield( response )
235
+ error_message( failure_msg + "\n\n" )
236
+ response = nil
237
+ end
238
+ end while response.nil?
239
+
240
+ return response
241
+ end
242
+
243
+
244
+ ### Prompt the user with the given <tt>prompt_string</tt> via #prompt,
245
+ ### substituting the given <tt>default</tt> if the user doesn't input
246
+ ### anything. If a test is provided, the prompt will repeat until the test
247
+ ### returns true. An optional failure message can also be passed in.
248
+ def prompt_with_default( prompt_string, default, failure_msg="Try again." )
249
+ response = nil
250
+
251
+ begin
252
+ default ||= '~'
253
+ response = prompt( "%s [%s]" % [ prompt_string, default ] )
254
+ response = default.to_s if !response.nil? && response.empty?
255
+
256
+ trace "Validating response %p" % [ response ]
257
+
258
+ # the block is a validator. We need to make sure that the user didn't
259
+ # enter '~', because if they did, it's nil and we should move on. If
260
+ # they didn't, then call the block.
261
+ if block_given? && response != '~' && ! yield( response )
262
+ error_message( failure_msg + "\n\n" )
263
+ response = nil
264
+ end
265
+ end while response.nil?
266
+
267
+ return nil if response == '~'
268
+ return response
269
+ end
270
+
271
+
272
+ ### Prompt for an array of values
273
+ def prompt_for_multiple_values( label, default=nil )
274
+ $stderr.puts( MULTILINE_PROMPT % [label] )
275
+ if default
276
+ $stderr.puts "Enter a single blank line to keep the default:\n %p" % [ default ]
277
+ end
278
+
279
+ results = []
280
+ result = nil
281
+
282
+ begin
283
+ result = Readline.readline( make_prompt_string("> ") )
284
+ if result.nil? || result.empty?
285
+ results << default if default && results.empty?
286
+ else
287
+ results << result
288
+ end
289
+ end until result.nil? || result.empty?
290
+
291
+ return results.flatten
292
+ end
293
+
294
+
295
+ ### Turn echo and masking of input on/off.
296
+ def noecho( masked=false )
297
+ require 'termios'
298
+
299
+ rval = nil
300
+ term = Termios.getattr( $stdin )
301
+
302
+ begin
303
+ newt = term.dup
304
+ newt.c_lflag &= ~Termios::ECHO
305
+ newt.c_lflag &= ~Termios::ICANON if masked
306
+
307
+ Termios.tcsetattr( $stdin, Termios::TCSANOW, newt )
308
+
309
+ rval = yield
310
+ ensure
311
+ Termios.tcsetattr( $stdin, Termios::TCSANOW, term )
312
+ end
313
+
314
+ return rval
315
+ end
316
+
317
+
318
+ ### Prompt the user for her password, turning off echo if the 'termios' module is
319
+ ### available.
320
+ def prompt_for_password( prompt="Password: " )
321
+ return noecho( true ) do
322
+ $stderr.print( prompt )
323
+ ($stdin.gets || '').chomp
324
+ end
325
+ end
326
+
327
+
328
+ ### Display a description of a potentially-dangerous task, and prompt
329
+ ### for confirmation. If the user answers with anything that begins
330
+ ### with 'y', yield to the block. If +abort_on_decline+ is +true+,
331
+ ### any non-'y' answer will fail with an error message.
332
+ def ask_for_confirmation( description, abort_on_decline=true )
333
+ puts description
334
+
335
+ answer = prompt_with_default( "Continue?", 'n' ) do |input|
336
+ input =~ /^[yn]/i
337
+ end
338
+
339
+ if answer =~ /^y/i
340
+ return yield
341
+ elsif abort_on_decline
342
+ error "Aborted."
343
+ fail
344
+ end
345
+
346
+ return false
347
+ end
348
+ alias :prompt_for_confirmation :ask_for_confirmation
349
+
350
+
351
+ ### Search line-by-line in the specified +file+ for the given +regexp+, returning the
352
+ ### first match, or nil if no match was found. If the +regexp+ has any capture groups,
353
+ ### those will be returned in an Array, else the whole matching line is returned.
354
+ def find_pattern_in_file( regexp, file )
355
+ rval = nil
356
+
357
+ File.open( file, 'r' ).each do |line|
358
+ if (( match = regexp.match(line) ))
359
+ rval = match.captures.empty? ? match[0] : match.captures
360
+ break
361
+ end
362
+ end
363
+
364
+ return rval
365
+ end
366
+
367
+
368
+ ### Invoke the user's editor on the given +filename+ and return the exit code
369
+ ### from doing so.
370
+ def edit( filename )
371
+ editor = ENV['EDITOR'] || ENV['VISUAL'] || DEFAULT_EDITOR
372
+ system editor, filename
373
+ unless $?.success?
374
+ fail "Editor exited uncleanly."
375
+ end
376
+ end
377
+
378
+
379
+ ### Extract all the non Rake-target arguments from ARGV and return them.
380
+ def get_target_args
381
+ args = ARGV.reject {|arg| arg =~ /^-/ || Rake::Task.task_defined?(arg) }
382
+ return args
383
+ end
384
+
385
+
386
+ ### Log a subdirectory change, execute a block, and exit the subdirectory
387
+ def in_subdirectory( subdir )
388
+ block = Proc.new
389
+
390
+ log "Entering #{subdir}"
391
+ Dir.chdir( subdir, &block )
392
+ log "Leaving #{subdir}"
393
+ end
394
+
395
+
data/rake/manual.rb ADDED
@@ -0,0 +1,755 @@
1
+ #
2
+ # Manual-generation Rake tasks and classes
3
+ # $Id: manual.rb 41 2008-08-28 16:46:09Z deveiant $
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' => true,
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
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 || subpath
360
+ else
361
+ trace "Using the path for the sort of directory %p" % [ subpath ]
362
+ subpath
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 || subpath
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
+
388
+ # Call the callback with :section -- determine the section title from
389
+ # the 'index.page' file underneath it, or the directory name if no
390
+ # index.page exists.
391
+ if section.key?( INDEX_PATH )
392
+ if section[INDEX_PATH] == from
393
+ from_current = true
394
+ builder.call( :current_section, section[INDEX_PATH].title, path )
395
+ else
396
+ builder.call( :section, section[INDEX_PATH].title, path )
397
+ end
398
+ else
399
+ title = File.dirname( path ).gsub( /_/, ' ' )
400
+ builder.call( :section, title, path )
401
+ end
402
+
403
+ # Recurse
404
+ self.traverse_hierarchy( path, section, from, &builder )
405
+
406
+ # Call the callback with :section_end
407
+ if from_current
408
+ builder.call( :current_section_end, '', path )
409
+ else
410
+ builder.call( :section_end, '', path )
411
+ end
412
+ end
413
+
414
+
415
+ ### Yield the specified +page+ to the builder
416
+ def handle_page_callback( path, page, from=nil )
417
+ if from == page
418
+ yield( :current_entry, page.title, path )
419
+ else
420
+ yield( :entry, page.title, path )
421
+ end
422
+ end
423
+
424
+
425
+ ### Find and store
426
+
427
+ ### Find all .page files under the configured +sourcedir+ and create a new
428
+ ### Manual::Page object for each one.
429
+ def find_and_load_pages
430
+ Pathname.glob( @sourcedir + '**/*.page' ).each do |pagefile|
431
+ path_to_base = @sourcedir.relative_path_from( pagefile.dirname )
432
+
433
+ page = Manual::Page.new( self, pagefile, @layoutsdir, path_to_base )
434
+ hierpath = pagefile.relative_path_from( @sourcedir )
435
+
436
+ @pages << page
437
+ @path_index[ pagefile ] = page
438
+ @title_index[ page.title ] = page
439
+ @uri_index[ hierpath.to_s ] = page
440
+
441
+ # Place the page in the page hierarchy by using inject to find and/or create the
442
+ # necessary subhashes. The last run of inject will return the leaf hash in which
443
+ # the page will live
444
+ section = hierpath.dirname.split[1..-1].inject( @hierarchy ) do |hier, component|
445
+ hier[ component ] ||= {}
446
+ hier[ component ]
447
+ end
448
+
449
+ section[ pagefile.basename('.page') ] = page
450
+ end
451
+ end
452
+
453
+ end
454
+
455
+
456
+ ### A Textile filter for the manual generation tasklib, implemented using RedCloth.
457
+ class TextileFilter < Manual::Page::Filter
458
+
459
+ ### Load RedCloth when the filter is first created
460
+ def initialize( *args )
461
+ require 'redcloth'
462
+ super
463
+ end
464
+
465
+
466
+ ### Process the given +source+ as Textile and return the resulting HTML
467
+ ### fragment.
468
+ def process( source, *ignored )
469
+ formatter = RedCloth::TextileDoc.new( source )
470
+ formatter.hard_breaks = false
471
+ formatter.no_span_caps = true
472
+ return formatter.to_html
473
+ end
474
+
475
+ end
476
+
477
+
478
+ ### An ERB filter for the manual generation tasklib, implemented using Erubis.
479
+ class ErbFilter < Manual::Page::Filter
480
+
481
+ ### Process the given +source+ as ERB and return the resulting HTML
482
+ ### fragment.
483
+ def process( source, page, metadata )
484
+ template_name = page.sourcefile.basename
485
+ template = ERB.new( source )
486
+ return template.result( binding() )
487
+ end
488
+
489
+ end
490
+
491
+
492
+ ### Manual generation task library
493
+ class GenTask < Rake::TaskLib
494
+
495
+ # Default values for task config variables
496
+ DEFAULT_NAME = :manual
497
+ DEFAULT_BASE_DIR = Pathname.new( 'docs/manual' )
498
+ DEFAULT_SOURCE_DIR = 'source'
499
+ DEFAULT_LAYOUTS_DIR = 'layouts'
500
+ DEFAULT_OUTPUT_DIR = 'output'
501
+ DEFAULT_RESOURCE_DIR = 'resources'
502
+ DEFAULT_LIB_DIR = 'lib'
503
+ DEFAULT_METADATA = OpenStruct.new
504
+
505
+
506
+ ### Define a new manual-generation task with the given +name+.
507
+ def initialize( name=:manual )
508
+ @name = name
509
+
510
+ @source_dir = DEFAULT_SOURCE_DIR
511
+ @layouts_dir = DEFAULT_LAYOUTS_DIR
512
+ @output_dir = DEFAULT_OUTPUT_DIR
513
+ @resource_dir = DEFAULT_RESOURCE_DIR
514
+ @lib_dir = DEFAULT_LIB_DIR
515
+ @metadata = DEFAULT_METADATA
516
+
517
+ yield( self ) if block_given?
518
+
519
+ self.define
520
+ end
521
+
522
+
523
+ ######
524
+ public
525
+ ######
526
+
527
+ attr_accessor :base_dir,
528
+ :source_dir,
529
+ :layouts_dir,
530
+ :output_dir,
531
+ :resource_dir,
532
+ :lib_dir,
533
+ :metadata
534
+
535
+ attr_reader :name
536
+
537
+
538
+ ### Set up the tasks for building the manual
539
+ def define
540
+
541
+ # Set up a description if the caller hasn't already defined one
542
+ unless Rake.application.last_comment
543
+ desc "Generate the manual"
544
+ end
545
+
546
+ # Make Pathnames of the directories relative to the base_dir
547
+ basedir = Pathname.new( @base_dir )
548
+ sourcedir = basedir + @source_dir
549
+ layoutsdir = basedir + @layouts_dir
550
+ outputdir = @output_dir
551
+ resourcedir = basedir + @resource_dir
552
+ libdir = basedir + @lib_dir
553
+
554
+ load_filter_libraries( libdir )
555
+ catalog = Manual::PageCatalog.new( sourcedir, layoutsdir )
556
+
557
+ # Declare the tasks outside the namespace that point in
558
+ task @name => "#@name:build"
559
+ task "clobber_#@name" => "#@name:clobber"
560
+
561
+ namespace( self.name ) do
562
+ setup_resource_copy_tasks( resourcedir, outputdir )
563
+ manual_pages = setup_page_conversion_tasks( sourcedir, outputdir, catalog )
564
+
565
+ desc "Build the manual"
566
+ task :build => [ :rdoc, :copy_resources, :generate_pages ]
567
+
568
+ task :clobber do
569
+ RakeFileUtils.verbose( $verbose ) do
570
+ rm_f manual_pages.to_a
571
+ end
572
+ remove_dir( outputdir ) if ( outputdir + '.buildtime' ).exist?
573
+ end
574
+
575
+ desc "Remove any previously-generated parts of the manual and rebuild it"
576
+ task :rebuild => [ :clobber, self.name ]
577
+ end
578
+
579
+ end # def define
580
+
581
+
582
+ ### Load the filter libraries provided in the given +libdir+
583
+ def load_filter_libraries( libdir )
584
+ Pathname.glob( libdir + '*.rb' ) do |filterlib|
585
+ trace " loading filter library #{filterlib}"
586
+ require( filterlib )
587
+ end
588
+ end
589
+
590
+
591
+ ### Set up the main HTML-generation task that will convert files in the given +sourcedir+ to
592
+ ### HTML in the +outputdir+
593
+ def setup_page_conversion_tasks( sourcedir, outputdir, catalog )
594
+
595
+ # we need to figure out what HTML pages need to be generated so we can set up the
596
+ # dependency that causes the rule to be fired for each one when the task is invoked.
597
+ manual_sources = FileList[ catalog.path_index.keys.map {|pn| pn.to_s} ]
598
+ trace " found %d source files" % [ manual_sources.length ]
599
+
600
+ # Map .page files to their equivalent .html output
601
+ html_pathmap = "%%{%s,%s}X.html" % [ sourcedir, outputdir ]
602
+ manual_pages = manual_sources.pathmap( html_pathmap )
603
+ trace "Mapping sources like so: \n %p -> %p" %
604
+ [ manual_sources.first, manual_pages.first ]
605
+
606
+ # Output directory task
607
+ directory( outputdir.to_s )
608
+ file outputdir.to_s do
609
+ touch outputdir + '.buildtime'
610
+ end
611
+
612
+ # Rule to generate .html files from .page files
613
+ rule(
614
+ %r{#{outputdir}/.*\.html$} => [
615
+ proc {|name| name.sub(/\.[^.]+$/, '.page').sub( outputdir, sourcedir) },
616
+ outputdir.to_s
617
+ ]) do |task|
618
+
619
+ source = Pathname.new( task.source )
620
+ target = Pathname.new( task.name )
621
+ log " #{ source } -> #{ target }"
622
+
623
+ page = catalog.path_index[ source ]
624
+ #trace " page object is: %p" % [ page ]
625
+
626
+ target.dirname.mkpath
627
+ target.open( File::WRONLY|File::CREAT|File::TRUNC ) do |io|
628
+ io.write( page.generate(metadata) )
629
+ end
630
+ end
631
+
632
+ # Group all the manual page output files targets into a containing task
633
+ desc "Generate any pages of the manual that have changed"
634
+ task :generate_pages => manual_pages
635
+ return manual_pages
636
+ end
637
+
638
+
639
+ ### Copy method for resources -- passed as a block to the various file tasks that copy
640
+ ### resources to the output directory.
641
+ def copy_resource( task )
642
+ source = task.prerequisites[ 1 ]
643
+ target = task.name
644
+
645
+ when_writing do
646
+ log " #{source} -> #{target}"
647
+ mkpath File.dirname( target )
648
+ cp source, target, :verbose => $trace
649
+ end
650
+ end
651
+
652
+
653
+ ### Set up a rule for copying files from the resources directory to the output dir.
654
+ def setup_resource_copy_tasks( resourcedir, outputdir )
655
+ resources = FileList[ resourcedir + '**/*.{js,css,png,gif,jpg,html}' ]
656
+ resources.exclude( /\.svn/ )
657
+ target_pathmap = "%%{%s,%s}p" % [ resourcedir, outputdir ]
658
+ targets = resources.pathmap( target_pathmap )
659
+ copier = self.method( :copy_resource ).to_proc
660
+
661
+ # Create a file task to copy each file to the output directory
662
+ resources.each_with_index do |resource, i|
663
+ file( targets[i] => [ outputdir.to_s, resource ], &copier )
664
+ end
665
+
666
+ # Now group all the resource file tasks into a containing task
667
+ desc "Copy manual resources to the output directory"
668
+ task :copy_resources => targets
669
+ end
670
+
671
+ end # class Manual::GenTask
672
+
673
+ end
674
+
675
+
676
+
677
+ ### Task: manual generation
678
+ if MANUALDIR.exist?
679
+ MANUALOUTPUTDIR = MANUALDIR + 'output'
680
+ trace "Manual will be generated in: #{MANUALOUTPUTDIR}"
681
+
682
+ begin
683
+ directory MANUALOUTPUTDIR.to_s
684
+
685
+ Manual::GenTask.new do |manual|
686
+ manual.metadata.version = PKG_VERSION
687
+ manual.metadata.api_dir = RDOCDIR
688
+ manual.output_dir = MANUALOUTPUTDIR
689
+ manual.base_dir = MANUALDIR
690
+ manual.source_dir = 'src'
691
+ end
692
+
693
+ task :clobber_manual do
694
+ rmtree( MANUALOUTPUTDIR, :verbose => true )
695
+ end
696
+
697
+ rescue LoadError => err
698
+ task :no_manual do
699
+ $stderr.puts "Manual-generation tasks not defined: %s" % [ err.message ]
700
+ end
701
+
702
+ task :manual => :no_manual
703
+ task :clobber_manual => :no_manual
704
+ end
705
+
706
+ else
707
+ TEMPLATEDIR = RAKE_TASKDIR + 'manualdir'
708
+
709
+ if TEMPLATEDIR.exist?
710
+
711
+ desc "Create a manual for this project from a template"
712
+ task :manual do
713
+ log "No manual directory (#{MANUALDIR}) currently exists."
714
+ ask_for_confirmation( "Create a new manual directory tree from a template?" ) do
715
+ MANUALDIR.mkpath
716
+
717
+ %w[layouts lib output resources src].each do |dir|
718
+ FileUtils.mkpath( MANUALDIR + dir, :mode => 0755, :verbose => true, :noop => $dryrun )
719
+ end
720
+
721
+ Pathname.glob( TEMPLATEDIR + '**/*.{rb,css,png,js,erb,page}' ).each do |tmplfile|
722
+ trace "extname is: #{tmplfile.extname}"
723
+
724
+ # Render ERB files
725
+ if tmplfile.extname == '.erb'
726
+ rname = tmplfile.basename( '.erb' )
727
+ target = MANUALDIR + tmplfile.dirname.relative_path_from( TEMPLATEDIR ) + rname
728
+ template = ERB.new( tmplfile.read, nil, '<>' )
729
+
730
+ target.dirname.mkpath( :mode => 0755, :verbose => true, :noop => $dryrun ) unless
731
+ target.dirname.directory?
732
+ html = template.result( binding() )
733
+ log "generating #{target}: html => #{html[0,20]}"
734
+
735
+ target.open( File::WRONLY|File::CREAT|File::EXCL, 0644 ) do |fh|
736
+ fh.print( html )
737
+ end
738
+
739
+ # Just copy anything else
740
+ else
741
+ target = MANUALDIR + tmplfile.relative_path_from( TEMPLATEDIR )
742
+ FileUtils.mkpath target.dirname,
743
+ :mode => 0755, :verbose => true, :noop => $dryrun unless target.dirname.directory?
744
+ FileUtils.install tmplfile, target,
745
+ :mode => 0644, :verbose => true, :noop => $dryrun
746
+ end
747
+ end
748
+ end
749
+
750
+ end # task :manual
751
+
752
+ end
753
+ end
754
+
755
+