lib/chunker.rb
branchruby-modules
changeset 9 bab54dae339a
parent 4 01a3332bfe0a
--- /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
+