lib/macros4cuke/templating/engine.rb in macros4cuke-0.3.42 vs lib/macros4cuke/templating/engine.rb in macros4cuke-0.4.00

- old
+ new

@@ -37,10 +37,38 @@ 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 +# 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 + # from the template that must be rendered verbatim. + def initialize(aSourceText) + @source = aSourceText + end + + public + + # Render the comment. + # Comments are rendered as empty text. This is necessary because + # Cucumber::RbSupport::RbWorld#steps complains when it sees a comment. + # This method has the same signature as the {Engine#render} method. + # @return [String] Empty string ("as is") + def render(aContextObject, theLocals) + return '' + end +end # class + + +# 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. @@ -158,11 +186,11 @@ # 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) msg = "Method Section.#{__method__} must be implemented in subclass." - raise NotImplementedError, msg + fail(NotImplementedError, msg) end end # class @@ -237,10 +265,13 @@ 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. # It may contain zero or tags enclosed between chevrons <...>. def initialize(aSourceTemplate) @@ -258,15 +289,23 @@ # 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? - + + prev = nil result = @representation.each_with_object('') do |element, subResult| - subResult << element.render(aContextObject, theLocals) + # 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 # Retrieve all placeholder names that appear in the template. @@ -293,28 +332,32 @@ return @variables end # Class method. Parse the given line text into a raw representation. # @return [Array] Couples of the form: - # [:static, text] or [:dynamic, tag text] + # [:static, text], [:comment, text] or [:dynamic, tag text] def self.parse(aTextLine) scanner = StringScanner.new(aTextLine) result = [] - - until scanner.eos? - # Scan tag at current position... - tag_literal = scanner.scan(/<(?:[^\\<>]|\\.)*>/) - unless tag_literal.nil? - result << [:dynamic, tag_literal.gsub(/^<|>$/, '')] + + 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 - - # ... 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? end - + return result end private @@ -334,15 +377,15 @@ case ch when '<' then unbalance += 1 when '>' then unbalance -= 1 end - raise StandardError, "Nested opening chevron '<'." if unbalance > 1 - raise StandardError, "Missing opening chevron '<'." if unbalance < 0 + fail(StandardError, "Nested opening chevron '<'.") if unbalance > 1 + fail(StandardError, "Missing opening chevron '<'.") if unbalance < 0 end - raise 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) @@ -353,11 +396,11 @@ 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? - raise EmptyArgumentError.new(line.strip) + fail(EmptyArgumentError.new(line.strip)) end end line_items end @@ -388,20 +431,20 @@ end else false end end - if line_to_squeeze && ! section_item.nil? + 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. def line_rep_ending(theLineRep) if theLineRep.last.is_a?(SectionEndMarker) @@ -416,30 +459,32 @@ # @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 - + 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 - + + # Parse the contents of a tag entry. # @param aText [String] The text that is enclosed between chevrons. def parse_tag(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 + fail(InvalidCharError.new(aText, matching[0])) if matching result = case aText[0, 1] when '?' ConditionalSection.new(aText[1..-1], true) @@ -475,11 +520,11 @@ end end unless open_sections.empty? error_message = "Unterminated section #{open_sections.last}." - raise StandardError, error_message + fail(StandardError, error_message) end return compiled end @@ -488,14 +533,14 @@ 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 + fail(StandardError, msg_prefix + msg) end if marker.name != sections.last.name msg = "doesn't match current section '#{sections.last.name}'." - raise StandardError, msg_prefix + msg + fail(StandardError, msg_prefix + msg) end end end # class