lib/macros4cuke/templating/engine.rb in macros4cuke-0.2.20 vs lib/macros4cuke/templating/engine.rb in macros4cuke-0.2.21

- old
+ new

@@ -35,32 +35,63 @@ end end # class # 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. -class Placeholder +# 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 + + +# Base class used internally by the template engine. +# The generalization of any element from a template that has one variable +# whose actual value influences the rendition. +class UnaryElement # The name of the placeholder/variable. attr_reader(:name) # @param aVarName [String] The name of the placeholder from a template. def initialize(aVarName) @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. + 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 + + return actual_value + end + +end # class + + + +# 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. +class Placeholder < UnaryElement + public # 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) - actual_value = theLocals[name] - if actual_value.nil? && aContextObject.respond_to?(name.to_sym) - actual_value = aContextObject.send(name.to_sym) - end + actual_value = retrieve_value_from(aContextObject, theLocals) result = case actual_value when NilClass '' @@ -78,34 +109,93 @@ end end # class -# Class used internally by the template engine. -# Represents an end of line that must be rendered as such. -class EOLine +# Base class used internally by the template engine. +# Represents a section in a template, that is, +# a set of template elements for which its rendition depends +# on the value of a variable. +class Section < UnaryElement + # The child elements of the section + attr_reader(:children) + + # @param aVarName [String] The name of the placeholder from a template. + def initialize(aVarName) + super(aVarName) + @children = [] + end + public - # Render an end of line. + # Add a child element as member of the section + def add_child(aChild) + children << aChild + end + +protected + # Render the placeholder given the passed arguments. # This method has the same signature as the {Engine#render} method. - # @return [String] An end of line marker. Its exact value is OS-dependent. + # @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) - return "\n" + raise NotImplementedError, "Method Section::#{_method_} must be implemented in subclass(es)." + 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) + # 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. + def initialize(aVarName, renderWhenExisting = true) + super(aVarName) + @existence = renderWhenExisting end + +public + # 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) + actual_value = retrieve_value_from(aContextObject, theLocals) + if (!actual_value.nil? && existence) || (actual_value.nil? && !existence) + # Let render the children + result = children.each_with_object('') do |a_child, sub_result| + sub_result << a_child.render(aContextObject, theLocals) + end + else + result = '' + end + + return result + end + end # class +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. # 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 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! + forbidden = ' !"#' + "$%&'()*+,-./:;<=>?[\\]^`{|}~" # Used concatenation (+) to work around Ruby bug! all_escaped = [] forbidden.each_char() { |ch| all_escaped << Regexp.escape(ch) } pattern = all_escaped.join("|") Regexp.new(pattern) end @@ -238,14 +328,30 @@ end # Parse the contents of a tag entry. # @param aText [String] The text that is enclosed between chevrons. def parse_tag(aText) - # Disallow punctuation and delimiter signs in tags. - matching = DisallowedSigns.match(aText) + # Recognize the first character + if aText =~ /^[\?\/]/ + 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 + + SectionEndMarker + result = case aText[0, 1] + when '?' + ConditionalSection.new(aText[1..-1], true) + + when '/' + SectionEndMarker.new(aText[1..-1]) + else + Placeholder.new(aText) + end - return Placeholder.new(aText) + return result end end # class end # module \ No newline at end of file