Class: Arrow::Template::Parser

Inherits:
Object
  • Object
show all
Includes:
HashUtilities, Patterns
Defined in:
lib/arrow/template/parser.rb

Overview

The Arrow::Template::Parser class, a derivative of Arrow::Object. This is the default parser class for the default Arrow templating system.

Authors

Please see the file LICENSE in the top-level directory for licensing details.

Defined Under Namespace

Modules: Patterns Classes: State

Constant Summary collapse

Defaults =

Default configuration hash

{
	:strict_end_tags			=> false,
	:ignore_unknown_PIs		=> true,
}

Constants included from Patterns

Patterns::ALTERNATION, Patterns::ARGDEFAULT, Patterns::ARGUMENT, Patterns::CAPTURE, Patterns::COMMA, Patterns::DBLQSTRING, Patterns::DOT, Patterns::EQUALS, Patterns::IDENTIFIER, Patterns::INFIX, Patterns::LBRACKET, Patterns::NUMBER, Patterns::PATHNAME, Patterns::QUOTEDSTRING, Patterns::RBRACKET, Patterns::REBINDOP, Patterns::REGEXP, Patterns::SLASHQSTRING, Patterns::SYMBOL, Patterns::TAGCLOSE, Patterns::TAGMIDDLE, Patterns::TAGOPEN, Patterns::TICKQSTRING, Patterns::VARIABLE, Patterns::WHITESPACE

Constants included from HashUtilities

HashUtilities::HashMergeFunction

Instance Attribute Summary collapse

Instance Method Summary collapse

Methods included from HashUtilities

stringify_keys, symbolify_keys

Methods inherited from Object

deprecate_class_method, deprecate_method, inherited

Constructor Details

#initialize(config = {}) ⇒ Parser

Create a new parser using the specified config. The config can contain one or more of the following keys:

:strict_end_tags

Raise an error if the optional name associated with an <?end?> tag doesn’t match the directive it closes. Defaults to false.

:ignore_unknown_PIs

When this is set to true, any processing instructions found in a template that don’t parse will be kept as-is in the output. If this is false, unrecognized PIs will raise an error at parse time. Defaults to true.



248
249
250
# File 'lib/arrow/template/parser.rb', line 248

def initialize( config={} )
	@config = Defaults.merge( config, &HashMergeFunction )
end

Instance Attribute Details

#configObject (readonly)

The configuration object which contains the parser’s config.



265
266
267
# File 'lib/arrow/template/parser.rb', line 265

def config
  @config
end

Instance Method Details

#initialize_copy(original) ⇒ Object

Initialize a duplicate of the original parser.



254
255
256
257
# File 'lib/arrow/template/parser.rb', line 254

def initialize_copy( original )
	super
	@config = original.config.dup
end

#parse(string, template, initialData = {}) ⇒ Object

Parse and return a template syntax tree from the given string.



269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
# File 'lib/arrow/template/parser.rb', line 269

def parse( string, template, initialData={} )

	# Create a new parse state and build the parse tree with it.
	begin
		state = State.new( string, template, initialData )
		syntax_tree = self.scan_for_nodes( state )
	
	rescue Arrow::TemplateError => err
		Kernel.raise( err ) unless defined? state
		state.scanner.unscan if state.scanner.matched? #<- segfaults
		
		# Get the linecount and chunk of erroring content
		errorContent = get_parse_context( state.scanner )
		
		msg = err.message.split( /:/ ).uniq.join( ':' ) +
			%{ at line %d of %s: %s...} %
			[ state.line, template._file, errorContent ]
		Kernel.raise( err.class, msg )
	end

	return syntax_tree
end

#scan_directive(state, context = nil) ⇒ Object

Given the specified parse state which is pointing past the opening of a directive tag, parse the directive.



340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
# File 'lib/arrow/template/parser.rb', line 340

def scan_directive( state, context=nil )
	scanner = state.scanner

	# Set the patterns in the parse state to compliment the
	# opening tag.
	state.set_tag_patterns( scanner.matched )
	tag_begin = state.line

	# Scan for the directive name; if no valid name can be
	# found, handle an unknown PI/directive.
	scanner.skip( WHITESPACE )
	unless (( tag = scanner.scan( IDENTIFIER ) ))
		#self.log.debug "No identifier at '%s...'" % scanner.rest[0,20]

		# If the tag_open is <?, then this is a PI that we don't
		# grok. The reaction to this is configurable, so decide what to
		# do.
		if state.tag_open == '<?'
			return handle_unknown_pi( state )

		# ...otherwise, it's just a malformed non-PI tag, which
		# is always an error.
		else
			raise Arrow::ParseError, "malformed directive name"
		end
	end

	# If it's anything but an 'end' tag, create a directive object.
	unless tag == 'end'
		begin
			node = Arrow::Template::Directive.create( tag, self, state )
		rescue ::FactoryError => err
			return self.handle_unknown_pi( state, tag )
		end

	# If it's an 'end', 
	else
		#self.log.debug "Found end tag."

		# If this scan is occuring in a recursive parse, make sure the
		# 'end' is closing the correct thing and break out of the node
		# search. Note that the trailing '?>' is left in the scanner to
		# match at the end of the loop that opened this recursion.
		if context
			scanner.skip( WHITESPACE )
			closed_tag = scanner.scan( IDENTIFIER )
			#self.log.debug "End found for #{closed_tag}"

			# If strict end tags is turned on, check to be sure we
			# got the correct 'end'.
			if @config[:strict_end_tags]
				raise Arrow::ParseError,
					"missing or malformed closing tag name" if
					closed_tag.nil?
				raise Arrow::ParseError,
					"mismatched closing tag name '#{closed_tag}'" unless
					closed_tag.downcase == context.downcase
			end

			# Jump out of the loop in #scan_for_nodes...
			throw :endscan 
		else
			raise Arrow::ParseError, "dangling end"
		end
	end

	# Skip to the end of the tag
	self.scan_for_tag_ending( state ) or
		raise Arrow::ParseError,
			"malformed tag starting at line %d: no closing tag "\
			"delimiters %p found" % [ tag_begin, state.tag_close ]

	return node
end

#scan_for_arglist(state, skip_whitespace = true) ⇒ Object

Given the specified state (an Arrow::Template::Parser::State object), scan for and return two Arrays of identifiers. The first is the list of parsed arguments as they appeared in the source, and the second is the same list with all non-word characters removed. Given an arglist like:

foo, bar=baz, *bim, &boozle

the returned arrays will contain:

["foo", "bar=baz", "*bim", "&boozle"]

and

["foo", "bar", "bim", "boozle"]

respectively.



495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
# File 'lib/arrow/template/parser.rb', line 495

def scan_for_arglist( state, skip_whitespace=true )
	scanner = state.scanner
	#self.log.debug "Scanning for arglist at %p" %
	#	scanner.rest[0,20]

	args = []
	pureargs = []
	scanner.skip( WHITESPACE ) if skip_whitespace
	while (( rval = scanner.scan(ARGUMENT) ))
		args << rval
		pureargs << rval.gsub( /\W+/, '' )
		scanner.skip( WHITESPACE )
		if (( rval = scanner.scan(ARGDEFAULT) ))
			args.last << rval
		end
		break unless scanner.skip( WHITESPACE + COMMA + WHITESPACE )
	end

	return nil if args.empty?

	#self.log.debug "Found args: %p, pureargs: %p" %
	#	[ args, pureargs ]
	return args, pureargs
end

#scan_for_identifier(state, skip_whitespace = true) ⇒ Object

Given the specified state (an Arrow::Template::Parser::State object), scan for and return an indentifier. If skip_whitespace is true, any leading whitespace characters will be skipped. Returns nil if no identifier is found.



420
421
422
423
424
425
426
427
428
429
# File 'lib/arrow/template/parser.rb', line 420

def scan_for_identifier( state, skip_whitespace=true )
	#self.log.debug "Scanning for identifier at %p" %
	#	state.scanner.rest[0,20]

	state.scanner.skip( WHITESPACE ) if skip_whitespace
	rval = state.scanner.scan( IDENTIFIER ) or return nil

	#self.log.debug "Found identifier %p" % rval
	return rval
end

#scan_for_methodchain(state) ⇒ Object

Given the specified state (an Arrow::Template::Parser::State object), scan for and return a methodchain. Returns nil if no methodchain is found.



453
454
455
456
457
458
459
460
461
462
463
464
# File 'lib/arrow/template/parser.rb', line 453

def scan_for_methodchain( state )
	scanner = state.scanner
	#self.log.debug "Scanning for methodchain at %p" %
	#	scanner.rest[0,20]

	rval = scanner.scan( INFIX ) || ''
	rval << (scanner.scan( WHITESPACE ) || '')
	rval << (scanner.scan( state.tag_middle ) || '')

	#self.log.debug "Found methodchain %p" % rval
	return rval
end

#scan_for_nodes(state, context = nil, node = nil) ⇒ Object

Use the specified state (a StringScanner object) to scan for directive and plain-text nodes. The context argument, if set, indicates a recursive call for the directive named. The node will be used as the branch node in the parse state.



297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
# File 'lib/arrow/template/parser.rb', line 297

def scan_for_nodes( state, context=nil, node=nil )
	return state.branch( node ) do
		scanner = state.scanner

		# Scan until the scanner reaches the end of its string. Early exits
		# 'break' of this loop.
		catch( :endscan ) {
			until scanner.eos?
				startpos = scanner.pos
				#self.log.debug %{Scanning from %d:%p} %
				#	[ scanner.pos, scanner.rest[0,20] + '..' ]
			
				# Scan for the next directive. When the scanner reaches
				# the end of the parsed string, just append any plain
				# text that's left and stop scanning.
				if scanner.skip_until( TAGOPEN )

					# Add the literal String node leading up to the tag
					# as a text node :FIXME: Have to do it this way
					# because StringScanner#pre_match does weird crap if
					# skip_until skips only one or no character/s.
					if ( scanner.pos - startpos > scanner.matched.length )
						offset = scanner.pos - scanner.matched.length - 1
						state << Arrow::Template::TextNode.
							new( scanner.string[startpos..offset] )
						#self.log.debug "Added text node %p" %
						#	scanner.string[startpos..offset]
					end

					# Now scan the directive that was found
					state << self.scan_directive( state, context )
				else
					state << Arrow::Template::TextNode.new( scanner.rest )
					scanner.terminate
				end
			end
		}
	end
end

#scan_for_pathname(state, skip_whitespace = true) ⇒ Object

Given the specified state (an Arrow::Template::Parser::State object), scan for and return a valid-looking file pathname.



523
524
525
526
527
528
529
530
531
532
533
534
# File 'lib/arrow/template/parser.rb', line 523

def scan_for_pathname( state, skip_whitespace=true )
	scanner = state.scanner
	#self.log.debug "Scanning for file path at %p" %
	#	scanner.rest[0,20]

	scanner.skip( WHITESPACE ) if skip_whitespace
	rval = scanner.scan( PATHNAME ) or
		return nil

	#self.log.debug "Found path: %p" % rval
	return rval
end

#scan_for_quoted_string(state, skip_whitespace = true) ⇒ Object

Given the specified state (an Arrow::Template::Parser::State object), scan for and return a quoted string, including the quotes. If skip_whitespace is true, any leading whitespace characters will be skipped. Returns nil if no quoted string is found.



437
438
439
440
441
442
443
444
445
446
447
# File 'lib/arrow/template/parser.rb', line 437

def scan_for_quoted_string( state, skip_whitespace=true )
	#self.log.debug "Scanning for quoted string at %p" %
	#	state.scanner.rest[0,20]

	state.scanner.skip( WHITESPACE ) if skip_whitespace

	rval = state.scanner.scan( QUOTEDSTRING ) or return nil

	#self.log.debug "Found quoted string %p" % rval
	return rval
end

#scan_for_tag_ending(state, skip_whitespace = true) ⇒ Object

Given the specified state (an Arrow::Template::Parser::State object), scan for and return the current tag ending. Returns nil if no tag ending is found.



470
471
472
473
474
475
476
477
478
479
480
481
# File 'lib/arrow/template/parser.rb', line 470

def scan_for_tag_ending( state, skip_whitespace=true )
	scanner = state.scanner
	#self.log.debug "Scanning for tag ending at %p" %
	#	scanner.rest[0,20]

	scanner.skip( WHITESPACE ) if skip_whitespace
	rval = scanner.scan( state.tag_close ) or
		return nil

	#self.log.debug "Found tag ending %p" % rval
	return rval
end