class Macros4Cuke::Templating::Engine

A very simple implementation of a templating engine. Earlier versions of Macros4Cuke relied on the logic-less Mustache template engine. But it was decided afterwards to replace it by a very simple template engine. The reasons were the following:

Constants

DisallowedSigns

The regular expression that matches a space, any punctuation sign or delimiter that is forbidden between chevrons <…> template tags.

Attributes

representation[R]

The internal representation of the template text

source[R]

The original text of the template is kept here.

Public Class Methods

identify_parse_error(aTextLine) click to toggle source

Called when the given text line could not be parsed. Raises an exception with the syntax issue identified. @param aTextLine [String] A text line from the template.

# File lib/macros4cuke/templating/engine.rb, line 137
def self.identify_parse_error(aTextLine)
  # Unsuccessful scanning: we typically have improperly balanced chevrons.
  # We will analyze the opening and closing chevrons...
  # First: replace escaped chevron(s)
  no_escaped = aTextLine.gsub(/\\[<>]/, '--')

  # var. equals count_of(<) -  count_of(>): can only be 0 or temporarily 1
  unbalance = 0

  no_escaped.each_char do |ch|
    case ch
      when '<' then unbalance += 1
      when '>' then unbalance -= 1
    end
    suffix = "opening chevron '<'."
    raise(StandardError, 'Nested ' + suffix) if unbalance > 1
    raise(StandardError, 'Missing ' + suffix) if unbalance.negative?
  end

  raise(StandardError, "Missing closing chevron '>'.") if unbalance == 1
end
new(aSourceTemplate) click to toggle source

Builds an Engine and compiles the given template text into

an internal representation.

@param aSourceTemplate [String] The template source text.

It may contain zero or tags enclosed between chevrons <...>.
# File lib/macros4cuke/templating/engine.rb, line 54
def initialize(aSourceTemplate)
  @source = aSourceTemplate
  @representation = compile(aSourceTemplate)
end
parse(aTextLine) click to toggle source

Class method. Parse the given line text into a raw representation. @return [Array] Couples of the form:

:static, text], [:comment, text

or [:dynamic, tag text]

# File lib/macros4cuke/templating/engine.rb, line 110
def self.parse(aTextLine)
  scanner = StringScanner.new(aTextLine)
  result = []

  if scanner.check(/\s*#/) # Detect comment line
    result << [:comment, aTextLine]
  else
    until scanner.eos?
      # Scan tag at current position...
      tag_literal = scanner.scan(/<(?:[^\\<>]|\\.)*>/)
      unless tag_literal.nil?
        result << [:dynamic, tag_literal.gsub(/^<|>$/, '')]
      end

      # ... or scan plain text at current position
      literal = scanner.scan(/(?:[^\\<>]|\\.)+/)
      result << [:static, literal] unless literal.nil?
      identify_parse_error(aTextLine) if tag_literal.nil? && literal.nil?
    end
  end

  return result
end

Public Instance Methods

render(aContextObject = Object.new, theLocals = {}) click to toggle source

Render the template within the given scope object and with the locals specified. The method mimicks the signature of the Tilt::Template#render method. @param aContextObject [anything] context object to get actual values

(when not present in the locals Hash).

@param theLocals [Hash] Contains one or more pairs of the form:

tag/placeholder name => actual value.

@return [String] The rendition of the template given

the passed argument values.
# File lib/macros4cuke/templating/engine.rb, line 68
def render(aContextObject = Object.new, theLocals = {})
  return '' if @representation.empty?

  prev = nil
  result = @representation.each_with_object(+'') do |element, subResult|
    # Output compaction rules:
    # -In case of consecutive eol's only one is rendered.
    # -In case of comment followed by one eol, both aren't rendered
    unless element.is_a?(EOLine) &&
           (prev.is_a?(EOLine) || prev.is_a?(Comment))
      subResult << element.render(aContextObject, theLocals)
    end
    prev = element
  end

  return result
end
variables() click to toggle source

Retrieve all placeholder names that appear in the template. @return [Array] The list of placeholder names.

# File lib/macros4cuke/templating/engine.rb, line 88
def variables()
  # The result will be cached/memoized...
  @variables ||= begin
    vars = @representation.each_with_object([]) do |element, subResult|
      case element
        when Placeholder
          subResult << element.name

        when Section
          subResult.concat(element.variables)
      end
    end

    vars.flatten.uniq
  end

  return @variables
end

Private Instance Methods

compile(aSourceTemplate) click to toggle source

Create the internal representation of the given template.

# File lib/macros4cuke/templating/engine.rb, line 162
def compile(aSourceTemplate)
  # Split the input text into lines.
  input_lines = aSourceTemplate.split(/\r\n?|\n/)

  # Parse the input text into raw data.
  raw_lines = input_lines.map do |line|
    line_items = self.class.parse(line)
    line_items.each do |(kind, text)|
      # A tag text cannot be empty nor blank
      next if (kind != :dynamic) || !text.strip.empty?

      raise(EmptyArgumentError.new(line.strip))
    end

    line_items
  end

  compiled_lines = raw_lines.map { |line| compile_line(line) }
  return compile_sections(compiled_lines.flatten)
end
compile_couple(aCouple) click to toggle source

@param aCouple [Array] a two-element array of the form: [kind, text] Where kind must be one of :static, :dynamic

# File lib/macros4cuke/templating/engine.rb, line 231
def compile_couple(aCouple)
  (kind, text) = aCouple

  result = case kind
             when :static then StaticText.new(text)
             when :comment then Comment.new(text)
             when :dynamic then parse_tag(text)
           end

  return result
end
compile_line(aRawLine) click to toggle source

Convert the array of raw entries (per line) into full-fledged template elements.

# File lib/macros4cuke/templating/engine.rb, line 185
def compile_line(aRawLine)
  line_rep = aRawLine.map { |couple| compile_couple(couple) }

  # Apply the rule: when a line just consist of spaces
  # and a section element, then remove all the spaces from that line.
  section_item = nil
  line_to_squeeze = line_rep.all? do |item|
    case item
      when StaticText
        item.source =~ /\s+/

      when Section, SectionEndMarker
        if section_item.nil?
          section_item = item
          true
        else
          false
        end
      else
        false
    end
  end
  if line_to_squeeze && !section_item.nil?
    line_rep = [section_item]
  else
    line_rep_ending(line_rep)
  end

  return line_rep
end
compile_sections(flat_sequence) click to toggle source

Transform a flat sequence of elements into a hierarchy of sections. @param flat_sequence [Array] a linear list of elements (including sections)

# File lib/macros4cuke/templating/engine.rb, line 270
def compile_sections(flat_sequence)
  open_sections = [] # The list of nested open sections

  compiled = flat_sequence.each_with_object([]) do |element, subResult|
    case element
      when Section
        open_sections << element

      when SectionEndMarker
        validate_section_end(element, open_sections)
        subResult << open_sections.pop

      else
        if open_sections.empty?
          subResult << element
        else
          open_sections.last.add_child(element)
        end
    end
  end

  unless open_sections.empty?
    error_message = "Unterminated section #{open_sections.last}."
    raise(StandardError, error_message)
  end

  return compiled
end
line_rep_ending(theLineRep) click to toggle source

Apply rule: if last item in line is an end of section marker, then place eoline before that item. Otherwise, end the line with a eoline marker.

# File lib/macros4cuke/templating/engine.rb, line 219
def line_rep_ending(theLineRep)
  if theLineRep.last.is_a?(SectionEndMarker)
    section_end = theLineRep.pop
    theLineRep << EOLine.new
    theLineRep << section_end
  else
    theLineRep << EOLine.new
  end
end
parse_tag(aText) click to toggle source

Parse the contents of a tag entry. @param aText [String] The text that is enclosed between chevrons.

# File lib/macros4cuke/templating/engine.rb, line 245
def parse_tag(aText)
  # Recognize the first character
  if aText =~ %r{^[\?/]}
    matching = DisallowedSigns.match(aText[1..-1])
  else
    # Disallow punctuation and delimiter signs in tags.
    matching = DisallowedSigns.match(aText)
  end
  raise(InvalidCharError.new(aText, matching[0])) if matching

  result = case aText[0, 1]
             when '?'
               ConditionalSection.new(aText[1..-1], true)

             when '/'
               SectionEndMarker.new(aText[1..-1])
             else
               Placeholder.new(aText)
           end

  return result
end
validate_section_end(marker, sections) click to toggle source

Validate the given end of section marker taking into account the open sections.

# File lib/macros4cuke/templating/engine.rb, line 301
def validate_section_end(marker, sections)
  msg_prefix = "End of section</#{marker.name}> "

  if sections.empty?
    msg = 'found while no corresponding section is open.'
    raise(StandardError, msg_prefix + msg)
  end
  return if marker.name == sections.last.name

  msg = "doesn't match current section '#{sections.last.name}'."
  raise(StandardError, msg_prefix + msg)
end