lib/macros4cuke/templating/engine.rb in macros4cuke-0.3.15 vs lib/macros4cuke/templating/engine.rb in macros4cuke-0.3.16

- old
+ new

@@ -13,17 +13,19 @@ # 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 when rendering 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 from the template that must be rendered verbatim. + # @param aSourceText [String] A piece of text extracted + # from the template that must be rendered verbatim. def initialize(aSourceText) @source = aSourceText end public @@ -62,11 +64,12 @@ @name = aVarName end protected # This method has the same signature as the {Engine#render} method. - # @return [Object] The actual value from the locals or context that is assigned to the variable. + # @return [Object] The actual value from the locals or context + # that is assigned to the variable. def retrieve_value_from(aContextObject, theLocals) actual_value = theLocals[name] if actual_value.nil? && aContextObject.respond_to?(name.to_sym) actual_value = aContextObject.send(name.to_sym) end @@ -79,11 +82,12 @@ # Class used internally by the template engine. # Represents a named placeholder in a template, that is, # a name placed between <..> in the template. -# At rendition, a placeholder is replaced by the text value that is associated with it. +# At rendition, a placeholder is replaced by the text value +# that is associated with it. class Placeholder < UnaryElement public # Render the placeholder given the passed arguments. # This method has the same signature as the {Engine#render} method. @@ -153,26 +157,29 @@ # Render the placeholder given the passed arguments. # This method has the same signature as the {Engine#render} method. # @return [String] The text value assigned to the placeholder. # Returns an empty string when no value is assigned to the placeholder. def render(aContextObject, theLocals) - raise NotImplementedError, "Method Section.#{__method__} must be implemented in subclass." + msg = "Method Section.#{__method__} must be implemented in subclass." + raise NotImplementedError, msg end end # class # Represents a section in a template, that is, # a set of template elements for which its rendition depends # on the (in)existence of an actual value bound to the variable name. class ConditionalSection < Section - # A boolean that indicates whether the rendition condition is the existence of a value for the variable (true) + # A boolean that indicates whether the rendition condition is + # the existence of a value for the variable (true) # or its inexistence (false). attr_reader(:existence) # @param aVarName [String] The name of the placeholder from a template. - # @param renderWhenExisting [boolean] When true, render the children elements if a value exists for the variable. + # @param renderWhenExisting [boolean] When true, render the children elements + # if a value exists for the variable. def initialize(aVarName, renderWhenExisting = true) super(aVarName) @existence = renderWhenExisting end @@ -206,42 +213,54 @@ SectionEndMarker = Struct.new(:name) # 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. +# 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 <...>, +# - 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, any punctuation sign or delimiter that is forbidden between chevrons <...> template tags. + # The regular expression that matches a space, + # any punctuation sign or delimiter that is forbidden + # between chevrons <...> template tags. DisallowedSigns = begin - forbidden = ' !"#' + "$%&'()*+,-./:;<=>?[\\]^`{|}~" # Used concatenation (+) to work around Ruby bug! + # Use concatenation (+) to work around Ruby bug! + 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) - # 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 <...>. + # 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 <...>. def initialize(aSourceTemplate) @source = aSourceTemplate @representation = compile(aSourceTemplate) end public - # Render the template within the given scope object and with the locals specified. + # 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. + # @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. def render(aContextObject = Object.new, theLocals) return '' if @representation.empty? result = @representation.each_with_object('') do |element, subResult| subResult << element.render(aContextObject, theLocals) @@ -283,11 +302,13 @@ result = [] until scanner.eos? # Scan tag at current position... tag_literal = scanner.scan(/<(?:[^\\<>]|\\.)*>/) - result << [:dynamic, tag_literal.gsub(/^<|>$/, '')] unless tag_literal.nil? + unless tag_literal.nil? + result << [:dynamic, tag_literal.gsub(/^<|>$/, '')] + end # ... or scan plain text at current position text_literal = scanner.scan(/(?:[^\\<>]|\\.)+/) result << [:static, text_literal] unless text_literal.nil? identify_parse_error(aTextLine) if tag_literal.nil? && text_literal.nil? @@ -301,27 +322,29 @@ # Raises an exception with the syntax issue identified. # @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... - no_escaped = aTextLine.gsub(/\\[<>]/, "--") # First: replace escaped chevron(s) - unbalance = 0 # This variable equals: count of < - count of > (can only be 0 or -temporarily- 1) + # 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.scan(/(.)/) do |match| - case match[0] + no_escaped.each_char do |ch| + case ch when '<' unbalance += 1 when '>' unbalance -= 1 end raise StandardError, "Nested opening chevron '<'." if unbalance > 1 raise StandardError, "Missing opening chevron '<'." if unbalance < 0 end - raise StandardError, "Missing closing chevron '>'." if unbalance == 1 - raise StandardError, "Cannot parse:\n'#{aTextLine}'" + raise StandardError, "Missing closing chevron '>'." if unbalance == 1 end # Create the internal representation of the given template. def compile(aSourceTemplate) @@ -331,26 +354,29 @@ # 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 - raise EmptyArgumentError.new(line.strip) if (kind == :dynamic) && text.strip.empty? + if (kind == :dynamic) && text.strip.empty? + raise 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) into full-fledged template elements. + # 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 and a section element, - # then remove all the spaces from that line. + # 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+/ @@ -368,11 +394,12 @@ end if line_to_squeeze && ! section_item.nil? line_rep = [section_item] else # Apply another 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. + # then place eoline before that item. + # Otherwise, end the line with a eoline marker. if line_rep.last.is_a?(SectionEndMarker) section_end = line_rep.pop() line_rep << EOLine.new line_rep << section_end else @@ -393,12 +420,10 @@ when :static StaticText.new(text) when :dynamic parse_tag(text) - else - raise StandardError, "Internal error: Don't know template element of kind #{kind}" end return result end @@ -412,11 +437,10 @@ # Disallow punctuation and delimiter signs in tags. matching = DisallowedSigns.match(aText) end raise InvalidCharError.new(aText, matching[0]) if matching - SectionEndMarker result = case aText[0, 1] when '?' ConditionalSection.new(aText[1..-1], true) when '/' @@ -441,11 +465,12 @@ when SectionEndMarker if open_sections.empty? raise StandardError, "End of section</#{element.name}> found while no corresponding section is open." end if element.name != open_sections.last.name - raise StandardError, "End of section</#{element.name}> doesn't match current section '#{open_sections.last.name}'." + msg = "End of section</#{element.name}> doesn't match current section '#{open_sections.last.name}'." + raise StandardError, msg end subResult << open_sections.pop() else if open_sections.empty? @@ -455,10 +480,13 @@ end end end - raise StandardError, "Unterminated section #{open_sections.last}." unless open_sections.empty? + unless open_sections.empty? + error_message = "Unterminated section #{open_sections.last}." + raise StandardError, error_message + end return compiled end end # class \ No newline at end of file