lib/erubi.rb in erubi-1.10.0 vs lib/erubi.rb in erubi-1.11.0
- old
+ new
@@ -1,22 +1,21 @@
# frozen_string_literal: true
module Erubi
- VERSION = '1.10.0'
- RANGE_ALL = 0..-1
+ VERSION = '1.11.0'
# :nocov:
if RUBY_VERSION >= '1.9'
RANGE_FIRST = 0
RANGE_LAST = -1
else
RANGE_FIRST = 0..0
RANGE_LAST = -1..-1
end
- TEXT_END = RUBY_VERSION >= '2.1' ? "'.freeze;" : "';"
MATCH_METHOD = RUBY_VERSION >= '2.4' ? :match? : :match
+ SKIP_DEFINED_FOR_INSTANCE_VARIABLE = RUBY_VERSION > '3'
# :nocov:
begin
require 'cgi/escape'
# :nocov:
@@ -43,10 +42,13 @@
end
# :nocov:
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
# The filename of the template, if one was given.
attr_reader :filename
@@ -55,16 +57,26 @@
attr_reader :bufvar
# Initialize a new Erubi::Engine. Options:
# +: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 frozen string literals in the resulting source code.
+ # +: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.
@@ -75,24 +87,38 @@
@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, RUBY_VERSION >= '2.1')
+ "'.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
end
@@ -182,17 +208,20 @@
if text.frozen?
text = text.gsub(/['\\]/, '\\\\\&')
else
text.gsub!(/['\\]/, '\\\\\&')
end
- @src << " " << @bufvar << " << '" << text << TEXT_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,
# escaping it based on the indicator given and escape flag.
def add_expression(indicator, code)
@@ -203,25 +232,54 @@
end
end
# 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