lib/macros4cuke/templating/engine.rb in macros4cuke-0.4.08 vs lib/macros4cuke/templating/engine.rb in macros4cuke-0.4.09

- old
+ new

@@ -11,23 +11,23 @@ module Macros4Cuke # Module used as a namespace # Module containing all classes implementing the simple template engine -# used internally in Macros4Cuke. +# used internally in Macros4Cuke. module Templating -# Class used internally by the template engine. -# Represents a static piece of text from a template. -# A static text is a text that is reproduced verbatim +# Class used internally by the template engine. +# Represents a static piece of text from a template. +# A static text is a text that is reproduced verbatim # when rendering a template. class StaticText # The static text extracted from the original template. attr_reader(:source) - - # @param aSourceText [String] A piece of text extracted + + # @param aSourceText [String] A piece of text extracted # from the template that must be rendered verbatim. def initialize(aSourceText) @source = aSourceText end @@ -40,20 +40,20 @@ return source end end # class -# Class used internally by the template engine. -# Represents a comment from a template. -# A static text is a text that is reproduced verbatim +# Class used internally by the template engine. +# Represents a comment from a template. +# A static text is a text that is reproduced verbatim # when rendering a template. class Comment # The comment as extracted from the original template. attr_reader(:source) - - # @param aSourceText [String] A piece of text extracted + + # @param aSourceText [String] A piece of text extracted # from the template that must be rendered verbatim. def initialize(aSourceText) @source = aSourceText end @@ -68,115 +68,115 @@ return '' end end # class -# Class used internally by the template engine. +# Class used internally by the template engine. # Represents an end of line that must be rendered as such. class EOLine public - + # Render an end of line. # This method has the same signature as the {Engine#render} method. # @return [String] An end of line marker. Its exact value is OS-dependent. def render(aContextObject, theLocals) return "\n" end end # class -# 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: -# - Be closer to the usual Gherkin syntax (parameters of scenario outlines -# use chevrons <...>, -# while Mustache use !{{...}} delimiters), -# - Feature files are meant to be simple, so should the template engine be. +# 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: +# - Be closer to the usual Gherkin syntax (parameters of scenario outlines +# use chevrons <...>, +# while Mustache use !{{...}} delimiters), +# - Feature files are meant to be simple, so should the template engine be. class Engine - # The regular expression that matches a space, + # The regular expression that matches a space, # any punctuation sign or delimiter that is forbidden # between chevrons <...> template tags. - DisallowedSigns = begin + DisallowedSigns = begin # Use concatenation (+) to work around Ruby bug! - forbidden = ' !"#' + "$%&'()*+,-./:;<=>?[\\]^`{|}~" - all_escaped = [] + forbidden = ' !"#' + "$%&'()*+,-./:;<=>?[\\]^`{|}~" + all_escaped = [] forbidden.each_char { |ch| all_escaped << Regexp.escape(ch) } pattern = all_escaped.join('|') Regexp.new(pattern) end # The original text of the template is kept here. attr_reader(:source) - + # The internal representation of the template text attr_reader(:representation) - + # Builds an Engine and compiles the given template text into # an internal representation. - # @param aSourceTemplate [String] The template source text. + # @param aSourceTemplate [String] The template source text. # It may contain zero or tags enclosed between chevrons <...>. def initialize(aSourceTemplate) @source = aSourceTemplate @representation = compile(aSourceTemplate) end public - # Render the template within the given scope object and + # 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 + # @return [String] The rendition of the template given # the passed argument values. 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) && + 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 - + # Retrieve all placeholder names that appear in the template. # @return [Array] The list of placeholder names. def variables() # The result will be cached/memoized... @variables ||= begin vars = @representation.each_with_object([]) do |element, subResult| case element - when Placeholder + when Placeholder subResult << element.name - + when Section subResult.concat(element.variables) - + else # Do nothing end end - + vars.flatten.uniq end - + return @variables end # Class method. Parse the given line text into a raw representation. # @return [Array] Couples of the form: @@ -190,16 +190,16 @@ else until scanner.eos? # Scan tag at current position... tag_literal = scanner.scan(/<(?:[^\\<>]|\\.)*>/) unless tag_literal.nil? - result << [:dynamic, tag_literal.gsub(/^<|>$/, '')] + result << [:dynamic, tag_literal.gsub(/^<|>$/, '')] end - + # ... or scan plain text at current position literal = scanner.scan(/(?:[^\\<>]|\\.)+/) - result << [:static, literal] unless literal.nil? + result << [:static, literal] unless literal.nil? identify_parse_error(aTextLine) if tag_literal.nil? && literal.nil? end end return result @@ -207,11 +207,11 @@ private # 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. + # @param aTextLine [String] A text line from the template. 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(/\\[<>]/, '--') @@ -219,57 +219,57 @@ # 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 + when '<' then unbalance += 1 + when '>' then unbalance -= 1 end - + fail(StandardError, "Nested opening chevron '<'.") if unbalance > 1 fail(StandardError, "Missing opening chevron '<'.") if unbalance < 0 end - - fail(StandardError, "Missing closing chevron '>'.") if unbalance == 1 + + fail(StandardError, "Missing closing chevron '>'.") if unbalance == 1 end - - + + # Create the internal representation of the given template. 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| + 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 if (kind == :dynamic) && text.strip.empty? fail(EmptyArgumentError.new(line.strip)) end end - + line_items end - + compiled_lines = raw_lines.map { |line| compile_line(line) } return compile_sections(compiled_lines.flatten) end - - # Convert the array of raw entries (per line) + + # Convert the array of raw entries (per line) # into full-fledged template elements. def compile_line(aRawLine) line_rep = aRawLine.map { |couple| compile_couple(couple) } - - # Apply the rule: when a line just consist of spaces + + # 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 @@ -282,29 +282,29 @@ if line_to_squeeze && !section_item.nil? line_rep = [section_item] else line_rep_ending(line_rep) end - + return line_rep end - # 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. + # 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. 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 end - + # @param aCouple [Array] a two-element array of the form: [kind, text] # Where kind must be one of :static, :dynamic def compile_couple(aCouple) (kind, text) = aCouple @@ -327,69 +327,69 @@ else # Disallow punctuation and delimiter signs in tags. matching = DisallowedSigns.match(aText) end fail(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 - + # Transform a flat sequence of elements into a hierarchy of sections. # @param flat_sequence [Array] a linear list of elements (including sections) 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 + + 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 end - + unless open_sections.empty? error_message = "Unterminated section #{open_sections.last}." fail(StandardError, error_message) end - + return compiled end - - # Validate the given end of section marker taking into account + + # Validate the given end of section marker taking into account # the open sections. def validate_section_end(marker, sections) msg_prefix = "End of section</#{marker.name}> " - + if sections.empty? msg = 'found while no corresponding section is open.' fail(StandardError, msg_prefix + msg) end if marker.name != sections.last.name msg = "doesn't match current section '#{sections.last.name}'." fail(StandardError, msg_prefix + msg) end end - + end # class end # module end # module