lib/chunker.rb
author Mahlon E. Smith <mahlon@martini.nu>
Wed, 06 Jan 2016 14:36:04 -0800
branchruby-modules
changeset 9 bab54dae339a
parent 4 chunker/lib/chunker.rb@01a3332bfe0a
permissions -rw-r--r--
Directory structure reorganization.

# 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