aboutsummaryrefslogtreecommitdiffstats
path: root/actionview/lib/action_view/template/handlers/erb.rb
blob: 88a857070674880d48dd8a33b134deb779f61d62 (plain) (blame)
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
require 'erubis'

module ActionView
  class Template
    module Handlers
      class Erubis < ::Erubis::Eruby
        def add_preamble(src)
          @newline_pending = 0
          src << "@output_buffer = output_buffer || ActionView::OutputBuffer.new;"
        end

        def add_text(src, text)
          return if text.empty?

          if text == "\n"
            @newline_pending += 1
          else
            src << "@output_buffer.safe_append='"
            src << "\n" * @newline_pending if @newline_pending > 0
            src << escape_text(text)
            src << "'.freeze;"

            @newline_pending = 0
          end
        end

        # Erubis toggles <%= and <%== behavior when escaping is enabled.
        # We override to always treat <%== as escaped.
        def add_expr(src, code, indicator)
          case indicator
          when '=='
            add_expr_escaped(src, code)
          else
            super
          end
        end

        BLOCK_EXPR = /\s*((\s+|\))do|\{)(\s*\|[^|]*\|)?\s*\Z/

        def add_expr_literal(src, code)
          flush_newline_if_pending(src)
          if code =~ BLOCK_EXPR
            src << '@output_buffer.append= ' << code
          else
            src << '@output_buffer.append=(' << code << ');'
          end
        end

        def add_expr_escaped(src, code)
          flush_newline_if_pending(src)
          if code =~ BLOCK_EXPR
            src << "@output_buffer.safe_expr_append= " << code
          else
            src << "@output_buffer.safe_expr_append=(" << code << ");"
          end
        end

        def add_stmt(src, code)
          flush_newline_if_pending(src)
          super
        end

        def add_postamble(src)
          flush_newline_if_pending(src)
          src << '@output_buffer.to_s'
        end

        def flush_newline_if_pending(src)
          if @newline_pending > 0
            src << "@output_buffer.safe_append='#{"\n" * @newline_pending}'.freeze;"
            @newline_pending = 0
          end
        end
      end

      class ERB
        # Specify trim mode for the ERB compiler. Defaults to '-'.
        # See ERB documentation for suitable values.
        class_attribute :erb_trim_mode
        self.erb_trim_mode = '-'

        # Default implementation used.
        class_attribute :erb_implementation
        self.erb_implementation = Erubis

        # Do not escape templates of these mime types.
        class_attribute :escape_whitelist
        self.escape_whitelist = ["text/plain"]

        ENCODING_TAG = Regexp.new("\\A(<%#{ENCODING_FLAG}-?%>)[ \\t]*")

        def self.call(template)
          new.call(template)
        end

        def supports_streaming?
          true
        end

        def handles_encoding?
          true
        end

        def call(template)
          # First, convert to BINARY, so in case the encoding is
          # wrong, we can still find an encoding tag
          # (<%# encoding %>) inside the String using a regular
          # expression
          template_source = template.source.dup.force_encoding(Encoding::ASCII_8BIT)

          erb = template_source.gsub(ENCODING_TAG, '')
          encoding = $2

          erb.force_encoding valid_encoding(template.source.dup, encoding)

          # Always make sure we return a String in the default_internal
          erb.encode!

          self.class.erb_implementation.new(
            erb,
            :escape => (self.class.escape_whitelist.include? template.type),
            :trim => (self.class.erb_trim_mode == "-")
          ).src
        end

        # Returns Regexp to extract a cached resource's name from a cache call at the
        # first line of a template.
        # The extracted cache name is expected in $1.
        #
        #   <% cache notification do %> # => notification
        #
        # The pattern should support templates with a beginning comment:
        #
        #   <%# Still extractable even though there's a comment %>
        #   <% cache notification do %> # => notification
        #
        # But fail to extract a name if a resource association is cached.
        #
        #   <% cache notification.event do %> # => nil
        def resource_cache_call_pattern
          /\A(?:<%#.*%>\n?)?<% cache\(?\s*(\w+\.?)/
        end

      private

        def valid_encoding(string, encoding)
          # If a magic encoding comment was found, tag the
          # String with this encoding. This is for a case
          # where the original String was assumed to be,
          # for instance, UTF-8, but a magic comment
          # proved otherwise
          string.force_encoding(encoding) if encoding

          # If the String is valid, return the encoding we found
          return string.encoding if string.valid_encoding?

          # Otherwise, raise an exception
          raise WrongEncodingError.new(string, string.encoding)
        end
      end
    end
  end
end