Class: LtdTemplate

Inherits:
Object
  • Object
show all
Defined in:
lib/ltdtemplate.rb,
lib/ltdtemplate/code.rb

Overview

LtdTemplate - Ltd (limited) Template

A template system with limitable resource usage.

Defined Under Namespace

Classes: Code, ResourceLimitExceeded, Value

Constant Summary collapse

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.

{
  #
  # These represent storable values. Some may also occur as
  # literals in code blocks.
  #
  :array => 'LtdTemplate::Value::Array',
  :boolean => 'LtdTemplate::Value::Boolean',
  :explicit_block => 'LtdTemplate::Value::Code_Block',
  :namespace => 'LtdTemplate::Value::Namespace',
  :nil => 'LtdTemplate::Value::Nil',
  :number => 'LtdTemplate::Value::Number',
  :string => 'LtdTemplate::Value::String',

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

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(options = {}) ⇒ LtdTemplate

Returns a new instance of LtdTemplate.



96
97
98
99
100
101
102
103
104
105
# File 'lib/ltdtemplate.rb', line 96

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

Instance Attribute Details

#exceededSymbol? (readonly)

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

Returns:

  • (Symbol, nil)


27
28
29
# File 'lib/ltdtemplate.rb', line 27

def exceeded
  @exceeded
end

#factory_singletonsHash (readonly)

A hash of factory singletons (e.g. nil, true, and false values) for this template.

Returns:

  • (Hash)


33
34
35
# File 'lib/ltdtemplate.rb', line 33

def factory_singletons
  @factory_singletons
end

#limitsHash (readonly)

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

Returns:

  • (Hash)


38
39
40
# File 'lib/ltdtemplate.rb', line 38

def limits
  @limits
end

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

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



43
44
45
# File 'lib/ltdtemplate.rb', line 43

def namespace
  @namespace
end

#optionsHash (readonly)

Instance initialization options

Returns:

  • (Hash)


48
49
50
# File 'lib/ltdtemplate.rb', line 48

def options
  @options
end

#usageHash (readonly)

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

Returns:

  • (Hash)


54
55
56
# File 'lib/ltdtemplate.rb', line 54

def usage
  @usage
end

#usedHash (readonly)

A hash of used resources for this template

Returns:

  • (Hash)


59
60
61
# File 'lib/ltdtemplate.rb', line 59

def used
  @used
end

Class Method Details

.set_classes(classes) ⇒ Hash

Change default factory classes globally

Parameters:

  • classes (Hash)

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

Returns:

  • (Hash)

    Returns the current class mapping.



92
93
94
# File 'lib/ltdtemplate.rb', line 92

def self.set_classes (classes)
  @@classes.merge! classes
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.



248
249
250
251
252
253
254
# File 'lib/ltdtemplate.rb', line 248

def check_limit (resource)
  if @limits[resource] and @usage[resource] and
    @usage[resource] > @limits[resource]
 @exceeded = resource
 raise LtdTemplate::ResourceLimitExceeded
  end
end

#factory(type, *args) ⇒ Object

Generate new code/value objects.

Parameters:

  • type (Symbol)

    The symbol for the type of object to generate, e.g. :number, :string, :implicit_block, etc.

  • args (Array)

    Type-specific initialization parameters.

Returns:

  • Returns the new code/value object.



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

def factory (type, *args)
  use :factory
  type = @classes[type]
  file = type.downcase.gsub '::', '/'
  require file
  eval(type).instance(self, *args)
end

#get_tokens(str) ⇒ Array<Array>

Convert a string into an array of parsing tokens.

Parameters:

  • str (String)

    The string to split into parsing tokens.

Returns:

  • (Array<Array>)


260
261
262
263
264
265
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
297
298
299
300
301
302
303
# File 'lib/ltdtemplate.rb', line 260

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 ]
 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

#parse(template) ⇒ LtdTemplate

Parse a template from a string.

Parameters:

  • template (String)

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

Returns:



112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
# File 'lib/ltdtemplate.rb', line 112

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:



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
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
# File 'lib/ltdtemplate.rb', line 317

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  # string literal
    code.push factory(:string, token[1])
 when :number # numeric literal
    code.push factory(:number, 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(:explicit_block,
parse_block(tokens, [ :rbrace ]))
    tokens.shift if tokens[0] # Consume }
 end
  end

  (code.size == 1) ? code[0] : factory(:implied_block, 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:



399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
# File 'lib/ltdtemplate.rb', line 399

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:



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

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.



430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
# File 'lib/ltdtemplate.rb', line 430

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:



366
367
368
369
370
# File 'lib/ltdtemplate.rb', line 366

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

#parse_template(tokens) ⇒ LtdTemplate::Code

This is the top-level token parser.

Parameters:

  • tokens (Array<Array>)

    The tokens to be parsed.

Returns:



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

def parse_template (tokens); parse_block tokens; end

#pop_namespaceObject

Pop the current namespace from the stack.



216
217
218
219
220
221
# File 'lib/ltdtemplate.rb', line 216

def pop_namespace
  if @namespace.parent
 @namespace = @namespace.parent
 use :namespace_depth, -1
  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 (Array<LtdTemplate::Code>) (defaults to: nil)

    These are code blocks to set the namespace parameters (available as the “_” array in the template).

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

    Options hash.

Options Hash (opts):

  • :target (LtdTemplate::Value)

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



208
209
210
211
212
213
# File 'lib/ltdtemplate.rb', line 208

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.

Returns:

  • (String)

    The result of rendering the template.



176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
# File 'lib/ltdtemplate.rb', line 176

def render (options = {})
  @exceeded = nil    # No limit exceeded yet
  @namespace = nil # Reset the namespace stack between runs
  @usage = {}    # Reset resource usage
  @used = {}   # No libraries used yet

  #
  # Accept template parameters from an array or hash.
  #
  parameters = factory :array
  parameters.set_from_array options[:parameters] if
    options[:parameters].is_a? Array
  parameters.set_from_hash options[:parameters] if
    options[:parameters].is_a? Hash

  #
  # Create the root namespace and evaluate the template code.
  #
  push_namespace 'render', parameters
  @code ? @code.get_value.to_text : ''
end

#set_classes(classes) ⇒ LtdTemplate

Change default factory classes for this template.

Parameters:

  • classes (Hash)

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

Returns:



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

def set_classes (classes)
  @classes.merge! classes
  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).



228
229
230
231
232
# File 'lib/ltdtemplate.rb', line 228

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.



239
240
241
242
243
# File 'lib/ltdtemplate.rb', line 239

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