New Upstream Snapshot - ruby-erubi

Ready changes

Summary

Merged new upstream version: 1.12.0+git20221226.1.7fedb8c (was: 1.9.0).

Resulting package

Built on 2023-01-19T20:58 (took 5m4s)

The resulting binary packages can be installed (if you have the apt repository enabled) by running one of:

apt install -t fresh-snapshots ruby-erubi

Lintian Result

Diff

diff --git a/.ci.gemfile b/.ci.gemfile
new file mode 100644
index 0000000..33c634d
--- /dev/null
+++ b/.ci.gemfile
@@ -0,0 +1,11 @@
+source 'https://rubygems.org'
+
+gem 'minitest-global_expectations'
+
+if RUBY_VERSION < '2.4.0'
+  # Until mintest 5.12.0 is fixed
+  gem 'minitest', '5.11.3'
+  gem 'rake', '<10.0.0'
+else
+  gem 'rake'
+end
diff --git a/CHANGELOG b/CHANGELOG
index f9b13bf..41c8815 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,3 +1,25 @@
+=== 1.12.0 (2022-12-22)
+
+* Use erb/escape for faster html escaping if available (jeremyevans)
+
+* Default :freeze_template_literals option to false if running with --enable-frozen-string-literal (casperisfine) (#35)
+
+=== 1.11.0 (2022-08-02)
+
+* Support :freeze_template_literals option for configuring whether to add .freeze to template literal strings (casperisfine) (#33)
+
+* Support :chain_appends option for chaining appends to the buffer variable (casperisfine, jeremyevans) (#32)
+
+* Avoid unnecessary defined? usage on Ruby 3+ when using the :ensure option (jeremyevans)
+
+=== 1.10.0 (2020-11-13)
+
+* Improve template parsing, mostly by reducing allocations (jeremyevans)
+
+* Do not ship tests in the gem, reducing gem size about 20% (jeremyevans)
+
+* Support :literal_prefix and :literal_postfix options for how to output literal tags (e.g. <%% code %>) (jaredcwhite) (#26, #27)
+
 === 1.9.0 (2019-09-25)
 
 * Change default :bufvar from 'String.new' to '::String.new' to work with BasicObject (jeremyevans)
diff --git a/MIT-LICENSE b/MIT-LICENSE
index 036808e..a8950e2 100644
--- a/MIT-LICENSE
+++ b/MIT-LICENSE
@@ -1,5 +1,5 @@
 copyright(c) 2006-2011 kuwata-lab.com all rights reserved.
-copyright(c) 2016-2018 Jeremy Evans
+copyright(c) 2016-2021 Jeremy Evans
 
 Permission is hereby granted, free of charge, to any person obtaining
 a copy of this software and associated documentation files (the
diff --git a/README.rdoc b/README.rdoc
index b87a685..1502aad 100644
--- a/README.rdoc
+++ b/README.rdoc
@@ -5,11 +5,11 @@ the same basic algorithm, with the following differences:
 
 * Handles postfix conditionals when using escaping (e.g. <tt><%= foo if bar %></tt>)
 * Supports frozen_string_literal: true in templates via :freeze option
-* Works with ruby's --enable-frozen-string-literal option
+* Works with ruby's <tt>--enable-frozen-string-literal</tt> option
 * Automatically freezes strings for template text when ruby optimizes it (on ruby 2.1+)
-* Escapes ' (apostrophe) when escaping for better XSS protection 
+* Escapes <tt>'</tt> (apostrophe) when escaping for better XSS protection 
 * Has 6x faster escaping on ruby 2.3+ by using cgi/escape
-* Has 86% smaller memory footprint
+* Has 81% smaller memory footprint (calculated using +ObjectSpace.memsize_of_all+)
 * Does no monkey patching (Erubis adds a method to Kernel)
 * Uses an immutable design (all options passed to the constructor, which returns a frozen object)
 * Has simpler internals (1 file, <150 lines of code)
@@ -92,7 +92,7 @@ instance variable.  Example:
   # </form>
   # after
 
-Alternatively, passing the option +:yield_returns_buffer => true+ will return the
+Alternatively, passing the option <tt>:yield_returns_buffer => true</tt> will return the
 buffer captured by the block instead of the last expression in the block.
 
 = Reporting Bugs
diff --git a/Rakefile b/Rakefile
index 753ff1b..4497485 100644
--- a/Rakefile
+++ b/Rakefile
@@ -42,7 +42,7 @@ end
 
 spec = proc do |env|
   env.each{|k,v| ENV[k] = v}
-  sh "#{FileUtils::RUBY} test/test.rb"
+  sh "#{FileUtils::RUBY} #{'-w' if RUBY_VERSION >= '3'} test/test.rb"
   env.each{|k,v| ENV.delete(k)}
 end
 
@@ -57,13 +57,6 @@ desc "Run specs with coverage"
 task "spec_cov" do
   spec.call('COVERAGE'=>'1')
 end
-  
-desc "Run specs with -w, some warnings filtered"
-task "spec_w" do
-  ENV['RUBYOPT'] ? (ENV['RUBYOPT'] += " -w") : (ENV['RUBYOPT'] = '-w')
-  rake = ENV['RAKE'] || "#{FileUtils::RUBY} -S rake"
-  sh %{#{rake} 2>&1 | egrep -v \": warning: instance variable @.* not initialized|: warning: method redefined; discarding old|: warning: previous definition of|: warning: statement not reached"}
-end
 
 ### Other
 
diff --git a/debian/changelog b/debian/changelog
index c68893e..c6507de 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,9 @@
+ruby-erubi (1.12.0+git20221226.1.7fedb8c-1) UNRELEASED; urgency=low
+
+  * New upstream snapshot.
+
+ -- Debian Janitor <janitor@jelmer.uk>  Thu, 19 Jan 2023 20:55:33 -0000
+
 ruby-erubi (1.9.0-2) unstable; urgency=medium
 
   * Team Upload
diff --git a/erubi.gemspec b/erubi.gemspec
index def0102..ea548a2 100644
--- a/erubi.gemspec
+++ b/erubi.gemspec
@@ -1,39 +1,25 @@
-#########################################################
-# This file has been automatically generated by gem2tgz #
-#########################################################
-# -*- encoding: utf-8 -*-
-# stub: erubi 1.9.0 ruby lib
+# frozen_string_literal: true
+require File.expand_path("../lib/erubi", __FILE__)
 
 Gem::Specification.new do |s|
-  s.name = "erubi".freeze
-  s.version = "1.9.0"
-
-  s.required_rubygems_version = Gem::Requirement.new(">= 0".freeze) if s.respond_to? :required_rubygems_version=
-  s.require_paths = ["lib".freeze]
-  s.authors = ["Jeremy Evans".freeze, "kuwata-lab.com".freeze]
-  s.date = "2019-09-25"
-  s.description = "Erubi is a ERB template engine for ruby. It is a simplified fork of Erubis".freeze
-  s.email = "code@jeremyevans.net".freeze
-  s.extra_rdoc_files = ["CHANGELOG".freeze, "MIT-LICENSE".freeze, "README.rdoc".freeze]
-  s.files = ["CHANGELOG".freeze, "MIT-LICENSE".freeze, "README.rdoc".freeze, "Rakefile".freeze, "lib/erubi.rb".freeze, "lib/erubi/capture_end.rb".freeze, "test/test.rb".freeze]
-  s.homepage = "https://github.com/jeremyevans/erubi".freeze
-  s.licenses = ["MIT".freeze]
-  s.rdoc_options = ["--quiet".freeze, "--line-numbers".freeze, "--inline-source".freeze, "--title".freeze, "Erubi: Small ERB Implementation".freeze, "--main".freeze, "README.rdoc".freeze]
-  s.rubygems_version = "2.5.2.1".freeze
-  s.summary = "Small ERB Implementation".freeze
-
-  if s.respond_to? :specification_version then
-    s.specification_version = 4
-
-    if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
-      s.add_development_dependency(%q<minitest>.freeze, [">= 0"])
-      s.add_development_dependency(%q<minitest-global_expectations>.freeze, [">= 0"])
-    else
-      s.add_dependency(%q<minitest>.freeze, [">= 0"])
-      s.add_dependency(%q<minitest-global_expectations>.freeze, [">= 0"])
-    end
-  else
-    s.add_dependency(%q<minitest>.freeze, [">= 0"])
-    s.add_dependency(%q<minitest-global_expectations>.freeze, [">= 0"])
-  end
+  s.name = 'erubi'
+  s.version = Erubi::VERSION
+  s.platform = Gem::Platform::RUBY
+  s.extra_rdoc_files = ["README.rdoc", "CHANGELOG", "MIT-LICENSE"]
+  s.rdoc_options += ["--quiet", "--line-numbers", "--inline-source", '--title', 'Erubi: Small ERB Implementation', '--main', 'README.rdoc']
+  s.license = "MIT"
+  s.summary = "Small ERB Implementation"
+  s.author = ["Jeremy Evans", 'kuwata-lab.com']
+  s.email = "code@jeremyevans.net"
+  s.homepage = "https://github.com/jeremyevans/erubi"
+  s.files = %w(MIT-LICENSE CHANGELOG README.rdoc Rakefile lib/erubi.rb lib/erubi/capture_end.rb)
+  s.description = "Erubi is a ERB template engine for ruby. It is a simplified fork of Erubis"
+  s.add_development_dependency "minitest"
+  s.add_development_dependency "minitest-global_expectations"
+  s.metadata = {
+    'bug_tracker_uri'   => 'https://github.com/jeremyevans/erubi/issues',
+    'mailing_list_uri'  => 'https://github.com/jeremyevans/erubi/discussions',
+    'changelog_uri'     => 'https://github.com/jeremyevans/erubi/blob/master/CHANGELOG',
+    'source_code_uri'   => 'https://github.com/jeremyevans/erubi',
+  }
 end
diff --git a/lib/erubi.rb b/lib/erubi.rb
index 0d5e738..e419d36 100644
--- a/lib/erubi.rb
+++ b/lib/erubi.rb
@@ -1,45 +1,60 @@
 # frozen_string_literal: true
 
 module Erubi
-  VERSION = '1.9.0'
-  RANGE_ALL = 0..-1
+  VERSION = '1.12.0'
 
+  # :nocov:
   if RUBY_VERSION >= '1.9'
     RANGE_FIRST = 0
     RANGE_LAST = -1
-    TEXT_END = RUBY_VERSION >= '2.1' ? "'.freeze;" : "';"
   else
-    # :nocov:
     RANGE_FIRST = 0..0
     RANGE_LAST = -1..-1
-    TEXT_END = "';"
   end
 
+  MATCH_METHOD = RUBY_VERSION >= '2.4' ? :match? : :match
+  SKIP_DEFINED_FOR_INSTANCE_VARIABLE = RUBY_VERSION > '3'
+  FREEZE_TEMPLATE_LITERALS = !eval("''").frozen? && RUBY_VERSION >= '2.1'
+  # :nocov:
+
   begin
-    require 'cgi/escape'
-    unless CGI.respond_to?(:escapeHTML) # work around for JRuby 9.1
-      CGI = Object.new
-      CGI.extend(defined?(::CGI::Escape) ? ::CGI::Escape : ::CGI::Util)
-    end
-    def self.h(value)
-      CGI.escapeHTML(value.to_s)
-    end
+    require 'erb/escape'
+    # :nocov:
+    define_singleton_method(:h, ERB::Escape.instance_method(:html_escape))
+    # :nocov:
   rescue LoadError
-    ESCAPE_TABLE = {'&' => '&amp;'.freeze, '<' => '&lt;'.freeze, '>' => '&gt;'.freeze, '"' => '&quot;'.freeze, "'" => '&#39;'.freeze}.freeze
-    if RUBY_VERSION >= '1.9'
-      # Escape the following characters with their HTML/XML
-      # equivalents.
-      def self.h(value)
-        value.to_s.gsub(/[&<>"']/, ESCAPE_TABLE)
+    begin
+      require 'cgi/escape'
+      # :nocov:
+      unless CGI.respond_to?(:escapeHTML) # work around for JRuby 9.1
+        CGI = Object.new
+        CGI.extend(defined?(::CGI::Escape) ? ::CGI::Escape : ::CGI::Util)
       end
-    else
+      # :nocov:
+      # Escape characters with their HTML/XML equivalents.
       def self.h(value)
-        value.to_s.gsub(/[&<>"']/){|s| ESCAPE_TABLE[s]}
+        CGI.escapeHTML(value.to_s)
+      end
+    rescue LoadError
+      # :nocov:
+      ESCAPE_TABLE = {'&' => '&amp;'.freeze, '<' => '&lt;'.freeze, '>' => '&gt;'.freeze, '"' => '&quot;'.freeze, "'" => '&#39;'.freeze}.freeze
+      if RUBY_VERSION >= '1.9'
+        def self.h(value)
+          value.to_s.gsub(/[&<>"']/, ESCAPE_TABLE)
+        end
+      else
+        def self.h(value)
+          value.to_s.gsub(/[&<>"']/){|s| ESCAPE_TABLE[s]}
+        end
       end
+      # :nocov:
     end
   end
 
   class Engine
+    # The default regular expression used for scanning.
+    DEFAULT_REGEXP = /<%(={1,2}|-|\#|%)?(.*?)([-=])?%>([ \t]*\r?\n)?/m
+    
     # The frozen ruby source code generated from the template, which can be evaled.
     attr_reader :src
 
@@ -50,38 +65,66 @@ module Erubi
     attr_reader :bufvar
 
     # Initialize a new Erubi::Engine.  Options:
-    # :bufval :: The value to use for the buffer variable, as a string.
-    # :bufvar :: The variable name to use for the buffer variable, as a string (default '::String.new')
-    # :ensure :: Wrap the template in a begin/ensure block restoring the previous value of bufvar.
-    # :escapefunc :: The function to use for escaping, as a string (default: '::Erubi.h').
-    # :escape :: Whether to make <%= escape by default, and <%== not escape by default.
-    # :escape_html :: Same as :escape, with lower priority.
-    # :filename :: The filename for the template.
-    # :freeze :: Whether to enable frozen string literals in the resulting source code.
-    # :outvar :: Same as bufvar, with lower priority.
-    # :postamble :: The postamble for the template, by default returns the resulting source code.
-    # :preamble :: The preamble for the template, by default initializes up the buffer variable.
-    # :regexp :: The regexp to use for scanning.
-    # :src :: The initial value to use for the source code
-    # :trim :: Whether to trim leading and trailing whitespace, true by default.
+    # +:bufval+ :: The value to use for the buffer variable, as a string (default <tt>'::String.new'</tt>).
+    # +:bufvar+ :: The variable name to use for the buffer variable, as a string.
+    # +:chain_appends+ :: Whether to chain <tt><<</t> calls to the buffer variable. Offers better
+    #                     performance, but can cause issues when the buffer variable is reassigned during
+    #                     template rendering (default +false+).
+    # +:ensure+ :: Wrap the template in a begin/ensure block restoring the previous value of bufvar.
+    # +:escapefunc+ :: The function to use for escaping, as a string (default: <tt>'::Erubi.h'</tt>).
+    # +:escape+ :: Whether to make <tt><%=</tt> escape by default, and <tt><%==</tt> not escape by default.
+    # +:escape_html+ :: Same as +:escape+, with lower priority.
+    # +:filename+ :: The filename for the template.
+    # +:freeze+ :: Whether to enable add a <tt>frozen_string_literal: true</tt> magic comment at the top of
+    #              the resulting source code.  Note this may cause problems if you are wrapping the resulting
+    #              source code in other code, because the magic comment only has an effect at the beginning of
+    #              the file, and having the magic comment later in the file can trigger warnings.
+    # +:freeze_template_literals+ :: Whether to suffix all literal strings for template code with <tt>.freeze</tt>
+    #                                (default: +true+ on Ruby 2.1+, +false+ on Ruby 2.0 and older).
+    #                                Can be set to +false+ on Ruby 2.3+ when frozen string literals are enabled
+    #                                in order to improve performance.
+    # +:literal_prefix+ :: The prefix to output when using escaped tag delimiters (default <tt>'<%'</tt>).
+    # +:literal_postfix+ :: The postfix to output when using escaped tag delimiters (default <tt>'%>'</tt>).
+    # +:outvar+ :: Same as +:bufvar+, with lower priority.
+    # +:postamble+ :: The postamble for the template, by default returns the resulting source code.
+    # +:preamble+ :: The preamble for the template, by default initializes the buffer variable.
+    # +:regexp+ :: The regexp to use for scanning.
+    # +:src+ :: The initial value to use for the source code, an empty string by default.
+    # +:trim+ :: Whether to trim leading and trailing whitespace, true by default.
     def initialize(input, properties={})
       @escape = escape = properties.fetch(:escape){properties.fetch(:escape_html, false)}
       trim       = properties[:trim] != false
       @filename  = properties[:filename]
       @bufvar = bufvar = properties[:bufvar] || properties[:outvar] || "_buf"
       bufval = properties[:bufval] || '::String.new'
-      regexp = properties[:regexp] || /<%(={1,2}|-|\#|%)?(.*?)([-=])?%>([ \t]*\r?\n)?/m
+      regexp = properties[:regexp] || DEFAULT_REGEXP
+      literal_prefix = properties[:literal_prefix] || '<%'
+      literal_postfix = properties[:literal_postfix] || '%>'
       preamble   = properties[:preamble] || "#{bufvar} = #{bufval};"
       postamble  = properties[:postamble] || "#{bufvar}.to_s\n"
+      @chain_appends = properties[:chain_appends]
+      @text_end = if properties.fetch(:freeze_template_literals, FREEZE_TEMPLATE_LITERALS)
+        "'.freeze"
+      else
+        "'"
+      end
 
+      @buffer_on_stack = false
       @src = src = properties[:src] || String.new
       src << "# frozen_string_literal: true\n" if properties[:freeze]
-      src << "begin; __original_outvar = #{bufvar} if defined?(#{bufvar}); " if properties[:ensure]
+      if properties[:ensure]
+        src << "begin; __original_outvar = #{bufvar}"
+        if SKIP_DEFINED_FOR_INSTANCE_VARIABLE && /\A@[^@]/ =~ bufvar
+          src << "; "
+        else
+          src << " if defined?(#{bufvar}); "
+        end
+      end
 
       unless @escapefunc = properties[:escapefunc]
         if escape
           @escapefunc = '__erubi.h'
-          src << "__erubi = ::Erubi;"
+          src << "__erubi = ::Erubi; "
         else
           @escapefunc = '::Erubi.h'
         end
@@ -110,46 +153,45 @@ module Erubi
             if rindex
               range = rindex+1..-1
               s = text[range]
-              if s =~ /\A[ \t]*\z/
+              if /\A[ \t]*\z/.send(MATCH_METHOD, s)
                 lspace = s
                 text[range] = ''
               end
             else
-              if is_bol && text =~ /\A[ \t]*\z/
-                lspace = text.dup
-                text[RANGE_ALL] = ''
+              if is_bol && /\A[ \t]*\z/.send(MATCH_METHOD, text)
+                lspace = text
+                text = ''
               end
             end
           end
         end
 
         is_bol = rspace
-        add_text(text) if text && !text.empty?
+        add_text(text)
         case ch
         when '='
           rspace = nil if tailch && !tailch.empty?
-          add_text(lspace) if lspace
           add_expression(indicator, code)
           add_text(rspace) if rspace
-        when '#'
-          n = code.count("\n") + (rspace ? 1 : 0)
+        when nil, '-'
           if trim && lspace && rspace
-            add_code("\n" * n)
+            add_code("#{lspace}#{code}#{rspace}")
           else
             add_text(lspace) if lspace
-            add_code("\n" * n)
+            add_code(code)
             add_text(rspace) if rspace
           end
-        when '%'
-          add_text("#{lspace}#{prefix||='<%'}#{code}#{tailch}#{postfix||='%>'}#{rspace}")
-        when nil, '-'
+        when '#'
+          n = code.count("\n") + (rspace ? 1 : 0)
           if trim && lspace && rspace
-            add_code("#{lspace}#{code}#{rspace}")
+            add_code("\n" * n)
           else
             add_text(lspace) if lspace
-            add_code(code)
+            add_code("\n" * n)
             add_text(rspace) if rspace
           end
+        when '%'
+          add_text("#{lspace}#{literal_prefix}#{code}#{tailch}#{literal_postfix}#{rspace}")
         else
           handle(indicator, code, tailch, rspace, lspace)
         end
@@ -159,22 +201,33 @@ module Erubi
 
       src << "\n" unless src[RANGE_LAST] == "\n"
       add_postamble(postamble)
-      src << "; ensure\n  #{bufvar} = __original_outvar\nend\n" if properties[:ensure]
+      src << "; ensure\n  " << bufvar << " = __original_outvar\nend\n" if properties[:ensure]
       src.freeze
       freeze
     end
 
     private
 
-    # Add raw text to the template
+    # Add raw text to the template.  Modifies argument if argument is mutable as a memory optimization.
+    # Must be called with a string, cannot be called with nil (Rails's subclass depends on it).
     def add_text(text)
-      @src << " #{@bufvar} << '" << text.gsub(/['\\]/, '\\\\\&') << TEXT_END unless text.empty?
+      return if text.empty?
+
+      if text.frozen?
+        text = text.gsub(/['\\]/, '\\\\\&')
+      else
+        text.gsub!(/['\\]/, '\\\\\&')
+      end
+
+      with_buffer{@src << " << '" << text << @text_end}
     end
 
     # Add ruby code to the template
     def add_code(code)
+      terminate_expression
       @src << code
       @src << ';' unless code[RANGE_LAST] == "\n"
+      @buffer_on_stack = false
     end
 
     # Add the given ruby expression result to the template,
@@ -189,23 +242,52 @@ module Erubi
 
     # Add the result of Ruby expression to the template
     def add_expression_result(code)
-      @src << " #{@bufvar} << (" << code << ').to_s;'
+      with_buffer{@src << ' << (' << code << ').to_s'}
     end
 
     # Add the escaped result of Ruby expression to the template
     def add_expression_result_escaped(code)
-      @src << " #{@bufvar} << #{@escapefunc}((" << code << '));'
+      with_buffer{@src << ' << ' << @escapefunc << '((' << code << '))'}
     end
 
     # Add the given postamble to the src.  Can be overridden in subclasses
     # to make additional changes to src that depend on the current state.
     def add_postamble(postamble)
-      src << postamble
+      terminate_expression
+      @src << postamble
     end
 
     # Raise an exception, as the base engine class does not support handling other indicators.
     def handle(indicator, code, tailch, rspace, lspace)
       raise ArgumentError, "Invalid indicator: #{indicator}"
     end
+
+    # Make sure the buffer variable is the target of the next append
+    # before yielding to the block. Mark that the buffer is the target
+    # of the next append after the block executes.
+    #
+    # This method should only be called if the block will result in
+    # code where << will append to the bufvar.
+    def with_buffer
+      if @chain_appends
+        unless @buffer_on_stack
+          @src << '; ' << @bufvar
+        end
+        yield
+        @buffer_on_stack = true
+      else
+        @src << ' ' << @bufvar
+        yield
+        @src << ';'
+      end
+    end
+
+    # Make sure that any current expression has been terminated.
+    # The default is to terminate all expressions, but when
+    # the chain_appends option is used, expressions may not be
+    # terminated.
+    def terminate_expression
+      @src << '; ' if @chain_appends
+    end
   end
 end
diff --git a/lib/erubi/capture_end.rb b/lib/erubi/capture_end.rb
index 5bc04f7..e381dfb 100644
--- a/lib/erubi/capture_end.rb
+++ b/lib/erubi/capture_end.rb
@@ -3,15 +3,15 @@
 require 'erubi'
 
 module Erubi
-  # An engine class that supports capturing blocks via the <%|= and <%|== tags,
-  # explicitly ending the captures using <%| end %> blocks.
+  # An engine class that supports capturing blocks via the <tt><%|=</tt> and <tt><%|==</tt> tags,
+  # explicitly ending the captures using <tt><%|</tt> end <tt>%></tt> blocks.
   class CaptureEndEngine < Engine
     # Initializes the engine.  Accepts the same arguments as ::Erubi::Engine, and these
     # additional options:
-    # :escape_capture :: Whether to make <%|= escape by default, and <%|== not escape by default,
+    # :escape_capture :: Whether to make <tt><%|=</tt> escape by default, and <tt><%|==</tt> not escape by default,
     #                    defaults to the same value as :escape.
-    # :yield_returns_buffer :: Whether to have <%| tags insert the buffer as an expression, so that
-    #                          <%| end %> tags will have the buffer be the last expression inside
+    # :yield_returns_buffer :: Whether to have <tt><%|</tt> tags insert the buffer as an expression, so that
+    #                          <tt><%| end %></tt> tags will have the buffer be the last expression inside
     #                          the block, and therefore have the buffer be returned by the yield
     #                          expression.  Normally the buffer will be returned anyway, but there
     #                          are cases where the last expression will not be the buffer,
@@ -36,13 +36,19 @@ module Erubi
         rspace = nil if tailch && !tailch.empty?
         add_text(lspace) if lspace
         escape_capture = !((indicator == '|=') ^ @escape_capture)
-        src << "begin; (#{@bufstack} ||= []) << #{@bufvar}; #{@bufvar} = #{@bufval}; #{@bufstack}.last << #{@escapefunc if escape_capture}((" << code
+        terminate_expression
+        @src << "begin; (#{@bufstack} ||= []) << #{@bufvar}; #{@bufvar} = #{@bufval}; #{@bufstack}.last << #{@escapefunc if escape_capture}((" << code
+        @buffer_on_stack = false
         add_text(rspace) if rspace
       when '|'
         rspace = nil if tailch && !tailch.empty?
         add_text(lspace) if lspace
-        result = @yield_returns_buffer ? " #{@bufvar}; " : ""
-        src << result << code << ")).to_s; ensure; #{@bufvar} = #{@bufstack}.pop; end;"
+        if @yield_returns_buffer
+          terminate_expression
+          @src << " #{@bufvar}; "
+        end
+        @src << code << ")).to_s; ensure; #{@bufvar} = #{@bufstack}.pop; end;"
+        @buffer_on_stack = false
         add_text(rspace) if rspace
       else
         super
diff --git a/test/test.rb b/test/test.rb
index 940e5d7..7ec6ce8 100644
--- a/test/test.rb
+++ b/test/test.rb
@@ -13,6 +13,8 @@ if ENV['COVERAGE']
 
   ENV.delete('COVERAGE')
   SimpleCov.instance_eval do
+    enable_coverage :branch
+
     start do
       add_filter "/test/"
       add_group('Missing'){|src| src.covered_percent < 100}
@@ -37,7 +39,8 @@ describe Erubi::Engine do
     t = (@options[:engine] || Erubi::Engine).new(input, @options)
     tsrc = t.src
     eval(tsrc, block.binding).must_equal result
-    tsrc = tsrc.gsub("'.freeze;", "';") if RUBY_VERSION >= '2.1'
+    strip_freeze = defined?(@strip_freeze) ? @strip_freeze : RUBY_VERSION >= '2.1'
+    tsrc = tsrc.gsub(/\.freeze/, '') if strip_freeze
     tsrc.must_equal src
   end
 
@@ -78,8 +81,20 @@ describe Erubi::Engine do
     end
   end
 
+  it "should handle no tags with frozen source" do
+    check_output(<<END1.freeze, <<END2, <<END3){}
+a
+END1
+_buf = ::String.new; _buf << 'a
+';
+_buf.to_s
+END2
+a
+END3
+  end
+
   it "should handle no options" do
-    list = ['&\'<>"2']
+    list = list = ['&\'<>"2']
     check_output(<<END1, <<END2, <<END3){}
 <table>
  <tbody>
@@ -121,43 +136,89 @@ END2
 END3
   end
 
+  it "should escape all backslashes and apostrophes in text" do
+    list = list = ['&\'<>"2']
+    check_output(<<END1.chomp, <<END2, <<END3){}
+<table>
+ <tbody>' ' \\ \\
+  <% i = 0
+     list.each_with_index do |item, i| %>
+  <tr>
+   <td><%= i+1 -%>
+</td>
+   <td><%== item %></td>
+  </tr>
+ <% end %>
+ </tbody>
+</table>
+<%== i+1 %>
+<%
+%>
+END1
+_buf = ::String.new; _buf << '<table>
+ <tbody>\\' \\' \\\\ \\\\
+';   i = 0
+     list.each_with_index do |item, i| 
+ _buf << '  <tr>
+   <td>'; _buf << ( i+1 ).to_s; _buf << '</td>
+   <td>'; _buf << ::Erubi.h(( item )); _buf << '</td>
+  </tr>
+';  end 
+ _buf << ' </tbody>
+</table>
+'; _buf << ::Erubi.h(( i+1 )); _buf << '
+';
+_buf.to_s
+END2
+<table>
+ <tbody>' ' \\ \\
+  <tr>
+   <td>1</td>
+   <td>&amp;&#39;&lt;&gt;&quot;2</td>
+  </tr>
+ </tbody>
+</table>
+1
+END3
+  end
+
   it "should strip only whitespace for <%, <%- and <%# tags" do
     check_output(<<END1, <<END2, <<END3){}
-  <% 1 %>  
+  <% a = 1 %>  
 a
-  <%- 2 %>  
+  <%- a = 2 %>  
 b
-  <%# 3 %>  
+  <%# a = 3 %>  
 c
- /<% 1 %>  
+ /<% a = 1 %>  
 a
-/ <%- 2 %>  
+/ <%- a = 2 %>  
 b
-//<%# 3 %>  
+//<%# a = 3 %>  
 c
-  <% 1 %> /
+  <% a = 1 %> /
 a
-  <%- 2 %>/ 
+  <%- a = 2 %>/ 
 b
-  <%# 3 %>//
+  <%# a = 3 %>//
 c
 END1
-_buf = ::String.new;   1   
+_buf = ::String.new;   a = 1   
  _buf << 'a
-';   2   
+';   a = 2   
  _buf << 'b
 ';
  _buf << 'c
- /'; 1 ; _buf << '  
+ /'; a = 1 ; _buf << '  
 '; _buf << 'a
-/ '; 2 ; _buf << '  
+/ '; a = 2 ; _buf << '  
 '; _buf << 'b
 //';
  _buf << '  
 '; _buf << 'c
-'; _buf << '  '; 1 ; _buf << ' /
+'; _buf << '  '; a = 1 ; _buf << ' /
 a
-'; _buf << '  '; 2 ; _buf << '/ 
+'; _buf << '  '; a = 2 ; _buf << '/ 
 b
 '; _buf << '  ';; _buf << '//
 c
@@ -183,7 +244,7 @@ END3
   end
 
   it "should handle ensure option" do
-    list = ['&\'<>"2']
+    list = list = ['&\'<>"2']
     @options[:ensure] = true
     @options[:bufvar] = '@a'
     @a = 'bar'
@@ -201,7 +262,7 @@ END3
 </table>
 <%== i+1 %>
 END1
-begin; __original_outvar = @a if defined?(@a); @a = ::String.new; @a << '<table>
+begin; __original_outvar = @a#{' if defined?(@a)' if RUBY_VERSION < '3'}; @a = ::String.new; @a << '<table>
  <tbody>
 ';   i = 0
      list.each_with_index do |item, i| 
@@ -232,6 +293,195 @@ END3
     @a.must_equal 'bar'
   end
 
+  it "should handle chain_appends option" do
+    @options[:chain_appends] = true
+    list = list = ['&\'<>"2']
+    check_output(<<END1, <<END2, <<END3){}
+<table>
+ <tbody>
+  <% i = 0
+     list.each_with_index do |item, i| %>
+  <tr>
+   <td><%= i+1 %></td>
+   <td><%== item %></td>
+  </tr>
+ <% end %>
+ </tbody>
+</table>
+<%== i+1 %>
+END1
+_buf = ::String.new;; _buf << '<table>
+ <tbody>
+';    i = 0
+     list.each_with_index do |item, i| 
+; _buf << '  <tr>
+   <td>' << ( i+1 ).to_s << '</td>
+   <td>' << ::Erubi.h(( item )) << '</td>
+  </tr>
+';   end 
+; _buf << ' </tbody>
+</table>
+' << ::Erubi.h(( i+1 )) << '
+'
+; _buf.to_s
+END2
+<table>
+ <tbody>
+  <tr>
+   <td>1</td>
+   <td>&amp;&#39;&lt;&gt;&quot;2</td>
+  </tr>
+ </tbody>
+</table>
+1
+END3
+  end
+
+  it "should handle :freeze_template_literals => true option" do
+    @options[:freeze_template_literals] = true
+    list = list = ['&\'<>"2']
+    @strip_freeze = false
+    check_output(<<END1, <<END2, <<END3){}
+<table>
+ <tbody>
+  <% i = 0
+     list.each_with_index do |item, i| %>
+  <tr>
+   <td><%= i+1 %></td>
+   <td><%== item %></td>
+  </tr>
+ <% end %>
+ </tbody>
+</table>
+<%== i+1 %>
+END1
+_buf = ::String.new; _buf << '<table>
+ <tbody>
+'.freeze;   i = 0
+     list.each_with_index do |item, i| 
+ _buf << '  <tr>
+   <td>'.freeze; _buf << ( i+1 ).to_s; _buf << '</td>
+   <td>'.freeze; _buf << ::Erubi.h(( item )); _buf << '</td>
+  </tr>
+'.freeze;  end 
+ _buf << ' </tbody>
+</table>
+'.freeze; _buf << ::Erubi.h(( i+1 )); _buf << '
+'.freeze;
+_buf.to_s
+END2
+<table>
+ <tbody>
+  <tr>
+   <td>1</td>
+   <td>&amp;&#39;&lt;&gt;&quot;2</td>
+  </tr>
+ </tbody>
+</table>
+1
+END3
+  end
+
+  it "should handle :freeze_template_literals => false option" do
+    @options[:freeze_template_literals] = false
+    list = list = ['&\'<>"2']
+    @strip_freeze = false
+    check_output(<<END1, <<END2, <<END3){}
+<table>
+ <tbody>
+  <% i = 0
+     list.each_with_index do |item, i| %>
+  <tr>
+   <td><%= i+1 %></td>
+   <td><%== item %></td>
+  </tr>
+ <% end %>
+ </tbody>
+</table>
+<%== i+1 %>
+END1
+_buf = ::String.new; _buf << '<table>
+ <tbody>
+';   i = 0
+     list.each_with_index do |item, i| 
+ _buf << '  <tr>
+   <td>'; _buf << ( i+1 ).to_s; _buf << '</td>
+   <td>'; _buf << ::Erubi.h(( item )); _buf << '</td>
+  </tr>
+';  end 
+ _buf << ' </tbody>
+</table>
+'; _buf << ::Erubi.h(( i+1 )); _buf << '
+';
+_buf.to_s
+END2
+<table>
+ <tbody>
+  <tr>
+   <td>1</td>
+   <td>&amp;&#39;&lt;&gt;&quot;2</td>
+  </tr>
+ </tbody>
+</table>
+1
+END3
+  end
+
+  it "should handle ensure option with no bufvar" do
+    list = list = ['&\'<>"2']
+    @options[:ensure] = true
+    check_output(<<END1, <<END2, <<END3){}
+<table>
+ <tbody>
+  <% i = 0
+     list.each_with_index do |item, i| %>
+  <tr>
+   <td><%= i+1 %></td>
+   <td><%== item %></td>
+  </tr>
+ <% end %>
+ </tbody>
+</table>
+<%== i+1 %>
+END1
+begin; __original_outvar = _buf if defined?(_buf); _buf = ::String.new; _buf << '<table>
+ <tbody>
+';   i = 0
+     list.each_with_index do |item, i| 
+ _buf << '  <tr>
+   <td>'; _buf << ( i+1 ).to_s; _buf << '</td>
+   <td>'; _buf << ::Erubi.h(( item )); _buf << '</td>
+  </tr>
+';  end 
+ _buf << ' </tbody>
+</table>
+'; _buf << ::Erubi.h(( i+1 )); _buf << '
+';
+_buf.to_s
+; ensure
+  _buf = __original_outvar
+end
+END2
+<table>
+ <tbody>
+  <tr>
+   <td>1</td>
+   <td>&amp;&#39;&lt;&gt;&quot;2</td>
+  </tr>
+ </tbody>
+</table>
+1
+END3
+  end
+
+  it "should handle trailing rspace with - modifier in <%|= and <%|" do
+    eval(::Erubi::CaptureEndEngine.new("<%|= '&' -%>\n<%| -%>\n").src).must_equal '&'
+  end
+
+  it "should handle lspace in <%|=" do
+    eval(::Erubi::CaptureEndEngine.new("<%|= %><%| %><%|= %><%| %>").src).must_equal ''
+  end
+
   it "should have <%|= with CaptureEndEngine not escape by default" do
     eval(::Erubi::CaptureEndEngine.new('<%|= "&" %><%| %>').src).must_equal '&'
     eval(::Erubi::CaptureEndEngine.new('<%|= "&" %><%| %>', :escape=>false).src).must_equal '&'
@@ -265,7 +515,7 @@ END3
  </tbody>
 </table>
 END1
-#{'__erubi = ::Erubi;' unless escape}@a = ::String.new; @a << '<table>
+#{'__erubi = ::Erubi; ' unless escape}@a = ::String.new; @a << '<table>
  <tbody>
 '; @a << '  ';begin; (__erubi_stack ||= []) << @a; @a = ::String.new; __erubi_stack.last << (( bar do  @a << '
 '; @a << '   <b>'; @a << #{!escape ? '__erubi' : '::Erubi'}.h(( '&' )); @a << '</b>
@@ -301,7 +551,7 @@ END3
  </tbody>
 </table>
 END1
-#{'__erubi = ::Erubi;' if escape}@a = ::String.new; @a << '<table>
+#{'__erubi = ::Erubi; ' if escape}@a = ::String.new; @a << '<table>
  <tbody>
 '; @a << '  ';begin; (__erubi_stack ||= []) << @a; @a = ::String.new; __erubi_stack.last << #{escape ? '__erubi' : '::Erubi'}.h(( bar do  @a << '
 '; @a << '   <b>'; @a << #{escape ? '__erubi' : '::Erubi'}.h(( '&' )); @a << '</b>
@@ -335,7 +585,7 @@ END3
  </tbody>
 </table>
 END1
-#{'__erubi = ::Erubi;' if escape}@a = ::String.new; @a << '<table>
+#{'__erubi = ::Erubi; ' if escape}@a = ::String.new; @a << '<table>
  <tbody>
 '; @a << '  ';begin; (__erubi_stack ||= []) << @a; @a = ::String.new; __erubi_stack.last << #{escape ? '__erubi' : '::Erubi'}.h(( quux do |i|  @a << '
 '; @a << '   <b>'; @a << #{escape ? '__erubi' : '::Erubi'}.h(( "\#{i}&" )); @a << '</b>
@@ -374,7 +624,7 @@ END3
  </tbody>
 </table>
 END1
-#{'__erubi = ::Erubi;' if escape}@a = ::String.new; @a << '<table>
+#{'__erubi = ::Erubi; ' if escape}@a = ::String.new; @a << '<table>
  <tbody>
 '; @a << '  ';begin; (__erubi_stack ||= []) << @a; @a = ::String.new; __erubi_stack.last << #{escape ? '__erubi' : '::Erubi'}.h(( bar do  @a << '
 '; @a << '   <b>'; @a << #{escape ? '__erubi' : '::Erubi'}.h(( '&' )); @a << '</b>
@@ -402,7 +652,7 @@ END3
       @options[var] = "@_out_buf"
       @options[:freeze] = true
       @items = [2]
-      i = 0
+      i = i = 0
       check_output(<<END1, <<END2, <<END3){}
 <table>
   <% for item in @items %>
@@ -437,7 +687,7 @@ END3
 
   it "should handle <%% and <%# syntax" do
     @items = [2]
-    i = 0
+    i = i = 0
     check_output(<<END1, <<END2, <<END3){}
 <table>
 <%% for item in @items %>
@@ -470,10 +720,44 @@ END2
 END3
   end
 
+  it "should handle <%% with a different literal prefix/postfix" do
+    @options[:literal_prefix] = "{%"
+    @options[:literal_postfix] = "%}"
+    @items = [2]
+    i = i = 0
+    check_output(<<END1, <<END2, <<END3){}
+<table>
+  <%% for item in @items %>
+  <tr>
+  </tr>
+  <%% end %>
+  <%%= "literal" %>
+</table>
+END1
+_buf = ::String.new; _buf << '<table>
+'; _buf << '  {% for item in @items %}
+'; _buf << '  <tr>
+  </tr>
+'; _buf << '  {% end %}
+'; _buf << '  {%= "literal" %}
+'; _buf << '</table>
+';
+_buf.to_s
+END2
+<table>
+  {% for item in @items %}
+  <tr>
+  </tr>
+  {% end %}
+  {%= "literal" %}
+</table>
+END3
+  end
+
   it "should handle :trim => false option" do
     @options[:trim] = false
     @items = [2]
-    i = 0
+    i = i = 0
     check_output(<<END1, <<END2, <<END3){}
 <table>
   <% for item in @items %>
@@ -484,8 +768,8 @@ END3
     <td><%== item %></td>
   </tr>
   <% end %><%#%>
-  <% i %>a
-  <% i %>
+  <% i = 1 %>a
+  <% i = 1 %>
 </table>
 END1
 _buf = ::String.new; _buf << '<table>
@@ -498,8 +782,8 @@ _buf = ::String.new; _buf << '<table>
   </tr>
 '; _buf << '  '; end ;
  _buf << '
-'; _buf << '  '; i ; _buf << 'a
-'; _buf << '  '; i ; _buf << '
+'; _buf << '  '; i = 1 ; _buf << 'a
+'; _buf << '  '; i = 1 ; _buf << '
 '; _buf << '</table>
 ';
 _buf.to_s
@@ -521,8 +805,8 @@ END3
     it "should handle :#{opt} and :escapefunc options" do
       @options[opt] = true
       @options[:escapefunc] = 'h.call'
-      h = proc{|s| s.to_s*2}
-      list = ['2']
+      h = h = proc{|s| s.to_s*2}
+      list = list = ['2']
       check_output(<<END1, <<END2, <<END3){}
 <table>
  <tbody>
@@ -567,7 +851,7 @@ END3
 
   it "should handle :escape option without :escapefunc option" do
     @options[:escape] = true
-    list = ['&\'<>"2']
+    list = list = ['&\'<>"2']
     check_output(<<END1, <<END2, <<END3){}
 <table>
  <tbody>
@@ -581,7 +865,7 @@ END3
  </tbody>
 </table>
 END1
-__erubi = ::Erubi;_buf = ::String.new; _buf << '<table>
+__erubi = ::Erubi; _buf = ::String.new; _buf << '<table>
  <tbody>
 ';   i = 0
      list.each_with_index do |item, i| 
@@ -609,7 +893,7 @@ END3
   it "should handle :preamble and :postamble options" do
     @options[:preamble] = '_buf = String.new("1");'
     @options[:postamble] = "_buf[0...18]\n"
-    list = ['2']
+    list = list = ['2']
     check_output(<<END1, <<END2, <<END3){}
 <table>
  <tbody>
@@ -707,11 +991,11 @@ END3
     check_output(<<END1, <<END2, <<END3) {}
 <%|= bar do |item| %>
 Let's eat <%= item %>!
-<% nil %><%| end %>
+<% i = i = nil %><%| end %>
 END1
 @a = ::String.new;begin; (__erubi_stack ||= []) << @a; @a = ::String.new; __erubi_stack.last << (( bar do |item|  @a << '
 '; @a << 'Let\\'s eat '; @a << ( item ).to_s; @a << '!
-'; nil ; @a;  end )).to_s; ensure; @a = __erubi_stack.pop; end; @a << '
+'; i = i = nil ; @a;  end )).to_s; ensure; @a = __erubi_stack.pop; end; @a << '
 ';
 @a.to_s
 END2

Debdiff

[The following lists of changes regard files as different if they have different names, permissions or owners.]

Files in second set of .debs but not in first

-rw-r--r--  root/root   /usr/share/rubygems-integration/all/gems/erubi-1.12.0/lib/erubi.rb
-rw-r--r--  root/root   /usr/share/rubygems-integration/all/gems/erubi-1.12.0/lib/erubi/capture_end.rb
-rw-r--r--  root/root   /usr/share/rubygems-integration/all/specifications/erubi-1.12.0.gemspec

Files in first set of .debs but not in second

-rw-r--r--  root/root   /usr/share/rubygems-integration/all/gems/erubi-1.9.0/lib/erubi.rb
-rw-r--r--  root/root   /usr/share/rubygems-integration/all/gems/erubi-1.9.0/lib/erubi/capture_end.rb
-rw-r--r--  root/root   /usr/share/rubygems-integration/all/specifications/erubi-1.9.0.gemspec

No differences were encountered in the control files

More details

Full run details