1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299
|
# frozen_string_literal: true
module Erubi
VERSION = '1.13.1'
# :nocov:
if RUBY_VERSION >= '1.9'
RANGE_FIRST = 0
RANGE_LAST = -1
else
RANGE_FIRST = 0..0
RANGE_LAST = -1..-1
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 'erb/escape'
define_method(:h, ERB::Escape.instance_method(:html_escape))
# :nocov:
rescue LoadError
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
# Escape characters with their HTML/XML equivalents.
def h(value)
CGI.escapeHTML(value.to_s)
end
rescue LoadError
ESCAPE_TABLE = {'&' => '&'.freeze, '<' => '<'.freeze, '>' => '>'.freeze, '"' => '"'.freeze, "'" => '''.freeze}.freeze
if RUBY_VERSION >= '1.9'
def h(value)
value.to_s.gsub(/[&<>"']/, ESCAPE_TABLE)
end
else
def h(value)
value.to_s.gsub(/[&<>"']/){|s| ESCAPE_TABLE[s]}
end
end
end
end
# :nocov:
module_function :h
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
# The variable name used for the buffer variable.
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 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] || 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]
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; "
else
@escapefunc = '::Erubi.h'
end
end
src << preamble
pos = 0
is_bol = true
input.scan(regexp) do |indicator, code, tailch, rspace|
match = Regexp.last_match
len = match.begin(0) - pos
text = input[pos, len]
pos = match.end(0)
ch = indicator ? indicator[RANGE_FIRST] : nil
lspace = nil
unless ch == '='
if text.empty?
lspace = "" if is_bol
elsif text[RANGE_LAST] == "\n"
lspace = ""
else
rindex = text.rindex("\n")
if rindex
range = rindex+1..-1
s = text[range]
if /\A[ \t]*\z/.send(MATCH_METHOD, s)
lspace = s
text[range] = ''
end
else
if is_bol && /\A[ \t]*\z/.send(MATCH_METHOD, text)
lspace = text
text = ''
end
end
end
end
is_bol = rspace
add_text(text)
case ch
when '='
rspace = nil if tailch && !tailch.empty?
add_expression(indicator, code)
add_text(rspace) if rspace
when nil, '-'
if trim && lspace && rspace
add_code("#{lspace}#{code}#{rspace}")
else
add_text(lspace) if lspace
add_code(code)
add_text(rspace) if rspace
end
when '#'
n = code.count("\n") + (rspace ? 1 : 0)
if trim && lspace && rspace
add_code("\n" * n)
else
add_text(lspace) if lspace
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
end
rest = pos == 0 ? input : input[pos..-1]
add_text(rest)
src << "\n" unless src[RANGE_LAST] == "\n"
add_postamble(postamble)
src << "; ensure\n " << bufvar << " = __original_outvar\nend\n" if properties[:ensure]
src.freeze
freeze
end
private
if RUBY_VERSION >= '2.3'
def _dup_string_if_frozen(string)
+string
end
# :nocov:
else
def _dup_string_if_frozen(string)
string.frozen? ? string.dup : string
end
end
# :nocov:
# 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)
return if text.empty?
text = _dup_string_if_frozen(text)
text.gsub!(/['\\]/, '\\\\\&')
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)
if ((indicator == '=') ^ @escape)
add_expression_result(code)
else
add_expression_result_escaped(code)
end
end
# Add the result of Ruby expression to the template
def add_expression_result(code)
with_buffer{@src << ' << (' << code << ').to_s'}
end
# Add the escaped result of Ruby expression to the template
def add_expression_result_escaped(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)
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
|