Class: LtdTemplate

Inherits:
Object
  • Object
show all
Includes:
Rubyverse
Defined in:
lib/ltdtemplate.rb

Defined Under Namespace

Modules: Consumer, Method_Handler, Univalue, Value Classes: Code, Proxy, ResourceLimitExceeded

Constant Summary collapse

VERSION =
'1.0.1'
TOKEN_MAP =
{
	?. => :dot,		# method separator
	'..' => :dotdot,	# begin named values
	?( => :lparen,		# begin call parameters
	?, => :comma,		# next call parameter
	?) => :rparen,		# end call parameters
	?[ => :lbrack,		# begin array subscripts
	?] => :rbrack,		# end array subscripts
	?{ => :lbrace,		# begin code block
	?} => :rbrace		# end code block
}
@@classes =

@@classes contains the default factory classes. These can be overridden globally using the #set_classes class method or per-template using the #set_classes instance method.

NOTE: Factory types are also used for resource tracking, and therefore must be unique among resource usage symbols.

{
	#
	# These represent storable values.
	#
	:array => 'Sarah',
	:array_splat => 'LtdTemplate::Value::Array_Splat',
	:code_block => 'LtdTemplate::Value::Code_Block',
	:namespace => 'LtdTemplate::Value::Namespace',

	#
	# These proxies provide template methods for native values.
	#
	:array_proxy => 'LtdTemplate::Proxy::Array',
	:boolean_proxy => 'LtdTemplate::Proxy::Boolean',
	:match_proxy => 'LtdTemplate::Proxy::Match',
	:nil_proxy => 'LtdTemplate::Proxy::Nil',
	:number_proxy => 'LtdTemplate::Proxy::Number',
	:regexp_proxy => 'LtdTemplate::Proxy::Regexp',
	:string_proxy => 'LtdTemplate::Proxy::String',

	#
	# These only occur as part of code blocks.
	#
	:call => 'LtdTemplate::Code::Call',
	:code_seq => 'LtdTemplate::Code::Sequence',
	:parameters => 'LtdTemplate::Code::Parameters',
	:subscript => 'LtdTemplate::Code::Subscript',
	:variable => 'LtdTemplate::Code::Variable',
}
@@proxies =

@@proxies contains the default mapping of native Ruby classes to factory symbols for the corresponding proxy classes. These can be overridden globally using the #set_proxies class method or per-template using the #set_proxies instance method. Unproxied native types will be invisible.

{
	'Array' => :array_proxy,
	'FalseClass' => :boolean_proxy,
	'Hash' => :array_proxy,
	'MatchData' => :match_proxy,
	'NilClass' => :nil_proxy,
	'Numeric' => :number_proxy,
	'Regexp' => :regexp_proxy,
	'Sarah' => :array_proxy,
	'String' => :string_proxy,
	'TrueClass' => :boolean_proxy,
}

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(options = {}) ⇒ LtdTemplate

Constructor

Parameters:

  • options (Hash) (defaults to: {})

    Options hash

Options Hash (options):

  • :loader (Proc)

    Callback for $.use method

  • :missing_method (Proc)

    Callback for missing methods

  • :regexp (Boolean)

    Set to true to enable regular expression processing



162
163
164
165
166
167
# File 'lib/ltdtemplate.rb', line 162

def initialize (options = {})
	@classes, @proxies = {}, {}
	@code, @namespace = nil, nil
	@limits, @usage, @used = {}, {}, {}
	@options = options
end

Instance Attribute Details

#exceededSymbol? (readonly)

The resource whose limit was being exceeded when an LtdTemplate::ResourceLimitExceeded exception was raised.

Returns:

  • (Symbol, nil)


118
119
120
# File 'lib/ltdtemplate.rb', line 118

def exceeded
  @exceeded
end

#limitsHash (readonly)

A hash of resource limits to enforce during parsing and rendering.

Returns:

  • (Hash)


106
107
108
# File 'lib/ltdtemplate.rb', line 106

def limits
  @limits
end

#namespaceLtdTemplate::Value::Namespace? (readonly)

The current namespace (at the bottom of the rendering namespace stack).



128
129
130
# File 'lib/ltdtemplate.rb', line 128

def namespace
  @namespace
end

#optionsHash (readonly)

Instance initialization options.

Returns:

  • (Hash)


123
124
125
# File 'lib/ltdtemplate.rb', line 123

def options
  @options
end

#usageHash (readonly)

A hash of resource usage. It is updated after calls to #parse and #render.

Returns:

  • (Hash)


112
113
114
# File 'lib/ltdtemplate.rb', line 112

def usage
  @usage
end

#usedHash (readonly)

The resources already $.use’d for this template

Returns:

  • (Hash)


133
134
135
# File 'lib/ltdtemplate.rb', line 133

def used
  @used
end

Class Method Details

.set_classes(mapping) ⇒ Hash

Change default factory classes globally

Parameters:

  • mapping (Hash)

    A hash of factory symbols and corresponding classes to be instantiated.

Returns:

  • (Hash)

    Returns the current class mapping.



140
141
142
# File 'lib/ltdtemplate.rb', line 140

def self.set_classes (mapping)
	@@classes.merge! mapping
end

.set_proxies(mapping) ⇒ Hash

Change default proxy types globally

Parameters:

  • mapping (Hash)

    A hash of native object classes and corresponding factory types for proxy objects to be instantiated.

Returns:

  • (Hash)

    Returns the current proxy mapping.



149
150
151
# File 'lib/ltdtemplate.rb', line 149

def self.set_proxies (mapping)
	@@proxies.merge! mapping
end

Instance Method Details

#check_limit(resource) ⇒ Object

Throw an exception if a resource limit has been exceeded.

Parameters:

  • resource (Symbol)

    The resource limit to be checked.



174
175
176
177
178
179
180
181
# File 'lib/ltdtemplate.rb', line 174

def check_limit (resource)
	if @limits[resource] && @usage[resource] &&
	  @usage[resource] > @limits[resource]
 @exceeded = resource
 raise LtdTemplate::ResourceLimitExceeded.new(
   "Exceeded #{resource} #{@limits[resource]}")
	end
end

#factory(type, *args) ⇒ Object

Generate new code, value, or proxy objects.

Parameters:

  • type (Symbol)

    The symbol for the type of object to generate, e.g. :number_proxy, :string_proxy, :code_block, etc.

  • args (Array)

    Type-specific initialization parameters.

Returns:

  • Returns the new code/value object.



189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
# File 'lib/ltdtemplate.rb', line 189

def factory (type, *args)
	use :factory
	spec = @classes[type] || @@classes[type]
	case spec
	when nil
 raise ArgumentError, "Unknown template factory type #{type}"
	when String
 # Convert the class name to a class on first use and cache it
 require spec.downcase.gsub('::', File::SEPARATOR)
 @classes[type] = klass = eval(spec)
	else klass = spec
	end
	use type
	args.unshift self if klass.is_a? LtdTemplate::Consumer
	if klass.respond_to? :new then klass.new *args
	else klass
	end
end

#get_tokens(str) ⇒ Array<Array>

Convert a string into an array of parser tokens.

Parameters:

  • str (String)

    The string to split into parser tokens.

Returns:

  • (Array<Array>)


212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
# File 'lib/ltdtemplate.rb', line 212

def get_tokens (str)
	tokens = []
	str.split(%r{(
	  /\*.*?\*/		# /* comment */
	  |
	  '(?:\\.|[^\.,\[\](){}\s])+# 'string
	  |
	  "(?:\\"|[^"])*"	# "string"
	  |
	  [@^][a-zA-Z0-9_]+	# root or parent identifier
	  |
	  [a-zA-Z_][a-zA-Z0-9_]*# alphanumeric identifier
	  |
	  -?\d+(?:\.\d+)?	# integer or real numeric literal
	  |
	  \.\.			# begin keyed values
	  |
	  [\.(,)\[\]{}]		# methods, calls, elements, blocks
	  |
	  \s+
	  )}mx).grep(/\S/).each do |token|
 if token =~ %r{^/\*.*\*/$}s
		# Ignore comment
 elsif token =~ /^'(.*)/s or token =~ /^"(.*)"$/s
		# String literal
		#tokens.push [ :string, $1.gsub(/\\(.)/, '\1') ]
		tokens.push [ :string, parse_strlit($1) ]
 elsif token =~ /^-?\d+(?:\.\d+)?/
		# Numeric literal
		tokens.push [ :number, token =~ /\./ ?
token.to_f : token.to_i ]
 elsif TOKEN_MAP[token]
		# Delimiter
		tokens.push [ TOKEN_MAP[token] ]
 elsif token =~ /^[@^][a-z0-9_]|^[a-z_]|^[@^\$]$/i
		# Variable or alphanumeric method name
		tokens.push [ :name, token ]
 else
		# Punctuated method name
		tokens.push [ :method, token ]
 end
	end

	tokens
end

#inspectObject

Don’t “spill our guts” upon inspection.



259
# File 'lib/ltdtemplate.rb', line 259

def inspect; "#<#{self.class.name}##{self.object_id}>"; end

#parse(template) ⇒ LtdTemplate

Parse a template from a string.

Parameters:

  • template (String)

    A template string. Templates look like 'literal<<template code>>literal...'.

Returns:



266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
# File 'lib/ltdtemplate.rb', line 266

def parse (template)
	@usage = {}
	tokens = []
	literal = true
	trim_next = false

	# Template "text<<code>>text<<code>>..." =>
	# (text, code, text, code, ...)
	template.split(/<<(?!<)((?:[^<>]|<[^<]|>[^>])*)>>/s).each do |part|
 if part.length > 0
		if literal
  part.sub!(/^\s+/s, '') if trim_next
  tokens.push [ :ext_string, part ]
		else
  if part[0] == '.'
			tokens[-1][1].sub!(/\s+$/s, '') if
 tokens[0] and tokens[-1][0] == :ext_string
			part = part[1..-1] if part.length > 1
  end
  part = part[0..-2] if trim_next = (part[-1] == '.')
  tokens += get_tokens part
		end
 else
		trim_next = false
 end
 literal = !literal
	end

	@code = parse_template tokens
	self
end

#parse_block(tokens, stops = []) ⇒ LtdTemplate::Code

Parse a code block, stopping at any stop token.

Parameters:

  • tokens (Array<Array>)

    The raw token stream.

  • stops (Array<Symbol>) (defaults to: [])

    An optional list of stop tokens, such as :comma (comma) or :rparen (right parenthesis).

Returns:



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
336
337
338
339
340
341
342
343
344
345
# File 'lib/ltdtemplate.rb', line 304

def parse_block (tokens, stops = [])
	code = []
	while tokens[0]
 break if stops.include? tokens[0][0]

 token = tokens.shift# Consume the current token
 case token[0]
 when :string, :ext_string, :number	# string and numeric literals
		code.push token[1]
 when :name		# variable
		code.push factory(:variable, token[1])
 when :lbrack	# variable element subscripts
		subs = parse_subscripts tokens
		code.push factory(:subscript, code.pop, subs) if code[0]
 when :dot		# method call w/ or w/out parameters
		case tokens[0][0]
		when :name, :method, :string
  method = tokens.shift
  if tokens[0] and tokens[0][0] == :lparen
			tokens.shift	# Consume (
			params = parse_parameters tokens
  else
			params = factory :parameters
  end
  code.push factory(:call, code.pop, method[1], params) if
    code[0]
		end if tokens[0]
 when :method	# punctuated method call
		# Insert the implied dot and re-parse
		tokens.unshift [ :dot ], token
 when :lparen	# call
		params = parse_parameters tokens
		code.push factory(:call, code.pop, 'call', params) if code[0]
 when :lbrace	# explicit code block
		code.push factory(:code_block,
parse_block(tokens, [ :rbrace ]))
		tokens.shift if tokens[0]	# Consume }
 end
	end

	(code.size == 1) ? code[0] : factory(:code_seq, code)
end

#parse_list(tokens, stops, resume = []) ⇒ Array<LtdTemplate::Code>

Common code for parsing various lists.

Parameters:

  • tokens (Array<Array>)

    The remaining unparsed tokens.

  • stops (Array<Symbol>)

    The list of tokens that stop parsing of the list.

  • resume (Array<Symbol>) (defaults to: [])

    The list of tokens that will resume parsing if they occur after a stop token (e.g. subscript parsing stops at ‘]’ but resumes if followed by ‘[’).

Returns:



356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
# File 'lib/ltdtemplate.rb', line 356

def parse_list (tokens, stops, resume = [])
	list = []
	block_stops = stops + [ :comma ]
	while tokens[0]
 if !stops.include? tokens[0][0]
		block = parse_block tokens, block_stops
		list.push block if block
		tokens.shift if tokens[0] and tokens[0][0] == :comma
 elsif tokens[1] and resume.include? tokens[1][0]
		tokens.shift 2	# Consume stop and resume tokens
 else
		break
 end
	end

	list
end

#parse_parameters(tokens) ⇒ LtdTemplate::Code::Parameters

Parse a positional and/or named parameter list

Parameters:

  • tokens (Array<Array>)

    The token stream.

Returns:



378
379
380
381
382
383
384
385
386
387
388
389
390
# File 'lib/ltdtemplate.rb', line 378

def parse_parameters (tokens)
	positional = parse_list tokens, [ :dotdot, :rparen ]

	if tokens[0] and tokens[0][0] == :dotdot
 tokens.shift	# Consume ..
 named = parse_list tokens, [ :rparen ]
	else
 named = nil
	end

	tokens.shift		# Consume )
	factory :parameters, positional, named
end

#parse_strlit(raw) ⇒ String

Parse escape sequences in string literals.

These are the same as in Ruby double-quoted strings:
\M-\C-x    meta-control-x
\M-x       meta-x
\C-x, \cx  control-x
\udddd     Unicode four-digit, 16-bit hexadecimal code point dddd
\xdd       two-digit, 8-bit hexadecimal dd
\ddd       one-, two-, or three-digit, 8-bit octal ddd
\c         any other character c is just itself

Parameters:

  • raw (String)

    The original (raw) string.

Returns:

  • (String)

    The string after escape processing.



405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
# File 'lib/ltdtemplate.rb', line 405

def parse_strlit (raw)
	raw.split(%r{(\\M-\\C-.|\\M-.|\\C-.|\\c.|
	  \\u[0-9a-fA-F]{4}|\\x[0-9a-fA-f]{2}|\\[0-7]{1,3}|\\.)}x).
	  map do |part|
 case part
 when /\\M-\\C-(.)/ then ($1.ord & 31 | 128).chr
 when /\\M-(.)/ then ($1.ord | 128).chr
 when /\\C-(.)/, /\\c(.)/ then ($1.ord & 31).chr
 when /\\u(.*)/ then $1.to_i(16).chr(Encoding::UTF_16BE)
 when /\\x(..)/ then $1.to_i(16).chr
 when /\\([0-7]+)/ then $1.to_i(8).chr
 when /\\(.)/ then "\a\b\e\f\n\r\s\t\v"["abefnrstv".
   index($1) || 9] || $1
 else part
 end
	  end.join ''
end

#parse_subscripts(tokens) ⇒ Array<LtdTemplate::Code>

Parse subscripts after the opening left bracket

Parameters:

  • tokens (Array<Array>)

    ] The token stream.

Returns:



427
428
429
430
431
# File 'lib/ltdtemplate.rb', line 427

def parse_subscripts (tokens)
	subs = parse_list tokens, [ :rbrack ], [ :lbrack ]
	tokens.shift		# Consume ]
	subs
end

#parse_template(tokens) ⇒ Object

This is the top-level token parser.

Parameters:

  • tokens (Array<Array>)

    The tokens to be parsed.



436
# File 'lib/ltdtemplate.rb', line 436

def parse_template (tokens); parse_block tokens; end

#pop_namespaceObject

Pop the current namespace from the stack.



439
440
441
442
443
444
# File 'lib/ltdtemplate.rb', line 439

def pop_namespace
	if @namespace.parent
 @namespace = @namespace.parent
 use :namespace_depth, -1
	end
end

#proxy_type(type) ⇒ Object

Return the factory type symbol for a value’s method handler class.

Parameters:

  • type (Class)

    The value’s class.

Returns:

  • The factory type symbol for the value’s method-handler class.



450
451
452
453
454
455
456
457
458
459
460
461
462
463
# File 'lib/ltdtemplate.rb', line 450

def proxy_type (type)
	if type
 type_name = type.name
 if !@proxies.has_key? type_name
		# No local proxy class for this type; check the global
		# settings and the proxy type for the superclass.
		@proxies[type_name] = @@proxies[type_name] ||
proxy_type(type.superclass)
 end
 @proxies[type_name]
	else
 nil
	end
end

#push_namespace(method, parameters = nil, opts = {}) ⇒ Object

Push a new namespace onto the namespace stack.

Parameters:

  • method (String)

    The (native) method string. This will be available as $.method within the template.

  • parameters (Sarah) (defaults to: nil)

    The code block parameters (available as the “_” array in the template).

  • opts (Hash) (defaults to: {})

    Options hash.

Options Hash (opts):

  • :target (Object)

    The target of the method call. This will be available as $.target within the template.



474
475
476
477
478
479
# File 'lib/ltdtemplate.rb', line 474

def push_namespace (method, parameters = nil, opts = {})
	use :namespaces
	use :namespace_depth
	@namespace = factory :namespace, method, parameters, @namespace
	@namespace.target = opts[:target] if opts[:target]
end

#render(options = {}) ⇒ String

Render the template.

The options hash may include :parameters, which may be an array or hash. These values will form the parameter array “_” in the root namespace.

Parameters:

  • options (Hash) (defaults to: {})

    Rendering options.

Options Hash (options):

  • :parameters (Array|Sarah)

    Parameters (copied to array “_” in the template)

  • :cleanup (Boolean)

    Set to false to disable post-rendering cleanup.

Returns:

  • (String)

    The result of rendering the template.



493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
# File 'lib/ltdtemplate.rb', line 493

def render (options = {})
	#
	# Reset for each rendering
	#
	@exceeded, @usage, @used = nil, {}, {}
	@namespace = nil

	#
	# Create the root namespace and evaluate the template code.
	#
	if @code
 params = Sarah.new
 params.set *options[:parameters] if options[:parameters]
 push_namespace 'render', params
 rubyversed(@code).evaluate.in_rubyverse(self).tpl_text
	else ''
	end
ensure
	self.rubyverse_map.clear unless options[:cleanup] == false
end

#rubyverse_new(object) ⇒ Object

Return an object to handle template methods for the supplied object.

Unrecognized object types will be treated as nil.



517
518
519
520
521
522
523
524
# File 'lib/ltdtemplate.rb', line 517

def rubyverse_new (object)
	if object.is_a? LtdTemplate::Method_Handler
 object	# The object handles its own template methods.
	elsif type = proxy_type(object.class)
 factory type, object
	else rubyversed nil
	end
end

#set_classes(mapping) ⇒ LtdTemplate

Override the default factory class mapping for this template instance.

Parameters:

  • mapping (Hash)

    A hash of factory symbols and corresponding classes to be instantiated.

Returns:



531
532
533
534
# File 'lib/ltdtemplate.rb', line 531

def set_classes (mapping)
	@classes.merge! mapping
	self
end

#set_proxies(mapping) ⇒ LtdTemplate

Override the default proxy types for this template instance.

Parameters:

  • mapping (Hash)

    A hash of class names and corresponding factory types for proxy objects to be instantiated.

Returns:



541
542
543
544
# File 'lib/ltdtemplate.rb', line 541

def set_proxies (mapping)
	@proxies.merge! mapping
	self
end

#use(resource, amount = 1) ⇒ Object

Track incremental usage of a resource.

Parameters:

  • resource (Symbol)

    The resource being used.

  • amount (Integer) (defaults to: 1)

    The additional amount of the resource being used (or released, if negative).



551
552
553
554
555
# File 'lib/ltdtemplate.rb', line 551

def use (resource, amount = 1)
	@usage[resource] ||= 0
	@usage[resource] += amount
	check_limit resource
end

#using(resource, amount) ⇒ Object

Track peak usage of a resource.

Parameters:

  • resource (Symbol)

    The resource being used.

  • amount (Integer)

    The total amount of the resource currently being used.



562
563
564
565
566
567
568
# File 'lib/ltdtemplate.rb', line 562

def using (resource, amount)
	@usage[resource] ||= 0
	if amount > @usage[resource]
 @usage[resource] = amount
 check_limit resource
	end
end