--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/lib/chunker.rb Wed Jan 06 14:36:04 2016 -0800
@@ -0,0 +1,133 @@
+# vim: set nosta noet ts=4 sw=4:
+
+require 'strscan'
+require 'stringio'
+
+#
+# Chunker: A convenience library for parsing __END__ tokens consistently.
+#
+# == Version
+#
+# $Id$
+#
+# == Author
+#
+# * Mahlon E. Smith <mahlon@martini.nu>
+#
+# :include: LICENSE
+#
+
+### Namespace for the datablock parser.
+###
+module Chunker
+
+ # VCS Revision
+ VCSRev = %q$Rev$
+
+ # VCS Id
+ VCSId = %q$Id$
+
+ # Package version
+ VERSION = '1.0.0'
+
+
+ ### Parser class for __END__ data blocks.
+ ### Find each __TOKEN__ within the __END__, and put each into a
+ ### DATA_TOKEN constant within the namespace that included us.
+ ###
+ class DataParser
+
+ # The mark for a DATA block.
+ END_TOKEN = /^__END__\r?\n/
+
+ # The mark for a 'sub' block.
+ CHUNK_TOKEN = /^__([A-Z\_0-9]+)__\r?\n/
+
+
+ ### Constructor: Given a +klass+ and an +io+ to the class file,
+ ### extract the data blocks and install constants.
+ ###
+ def initialize( klass, io )
+ io.open if io.closed?
+ end_string = io.read.split( END_TOKEN, 2 ).last
+
+ @klass = klass
+ @scanner = StringScanner.new( end_string )
+ io.close
+
+ # put each chunk into its own constant
+ #
+ if @scanner.check_until( CHUNK_TOKEN )
+ self.extract_blocks
+
+ # no sub blocks, put the whole mess into DATA_END
+ #
+ else
+ @klass.const_set( :DATA_END, StringIO.new( end_string ) )
+ end
+ end
+
+
+ #########
+ protected
+ #########
+
+ ### Parse the current +io+ for data blocks, set contents to
+ ### IO constants in the including class.
+ ###
+ def extract_blocks
+ label = nil
+
+ while @scanner.scan_until( CHUNK_TOKEN ) and ! @scanner.eos?
+ data = ''
+
+ # First pass, __END__ contents (until next token, instead
+ # of entire data block.)
+ #
+ if label.nil?
+ label = 'END'
+ data = @scanner.pre_match
+
+ @scanner.pos = self.next_position
+ else
+ label = @scanner[1]
+
+ # Pull the next token text out of the data, set up the next pass
+ #
+ if data = @scanner.scan_until( CHUNK_TOKEN )
+ data = data[ 0, data.length - @scanner[0].length ]
+ @scanner.pos = self.next_position
+
+ # No additional blocks
+ #
+ else
+ data = @scanner.rest
+ end
+ end
+
+ # Add the IO constant to the class that included me.
+ @klass.const_set( "DATA_#{label}".to_sym, StringIO.new( data ) )
+ end
+ end
+
+
+ ### Return the next scanner position for searching.
+ ###
+ def next_position
+ return @scanner.pos - @scanner[0].length
+ end
+ end
+
+
+ ### Hook included: Find the file path for how we arrived here, and open
+ ### it as an IO object. Parse the IO for data block tokens.
+ ###
+ def self.included( klass )
+ # klass.instance_eval{ __FILE__ } awww, nope.
+ # __FILE__ won't work here, so we find the filename via caller().
+ io = File.open( caller(1).last.sub(/:.*?$/, ''), 'r' )
+
+ DataParser.new( klass, io )
+ end
+end
+