linkparser 1.0.3

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