File: erubi.rb

package info (click to toggle)
ruby-erubi 1.13.1-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 148 kB
  • sloc: ruby: 1,431; makefile: 4
file content (299 lines) | stat: -rw-r--r-- 10,637 bytes parent folder | download
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 = {'&' => '&amp;'.freeze, '<' => '&lt;'.freeze, '>' => '&gt;'.freeze, '"' => '&quot;'.freeze, "'" => '&#39;'.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