aboutsummaryrefslogtreecommitdiffstats
path: root/actionpack/lib/action_view/template_handlers/compilable.rb
blob: 1aef81ba1a4103f52c87ab28de6dbeb158c9d22a (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
module ActionView
  module TemplateHandlers
    module Compilable

      def self.included(base)
        base.extend ClassMethod

        # Map method names to their compile time
        base.cattr_accessor :compile_time
        base.compile_time = {}

        # Map method names to the names passed in local assigns so far
        base.cattr_accessor :template_args
        base.template_args = {}

        # Count the number of inline templates
        base.cattr_accessor :inline_template_count
        base.inline_template_count = 0
      end

      module ClassMethod
        # If a handler is mixin this module, set compilable to true
        def compilable?
          true
        end
      end
      
      def render(template)
        @view.send :execute, template
      end

      # Compile and evaluate the template's code
      def compile_template(template)
        return unless compile_template?(template)

        render_symbol = assign_method_name(template)
        render_source = create_template_source(template, render_symbol)
        line_offset   = self.template_args[render_symbol].size + self.line_offset

        begin
          file_name = template.filename || 'compiled-template'
          ActionView::Base::CompiledTemplates.module_eval(render_source, file_name, -line_offset)
        rescue Exception => e  # errors from template code
          if @view.logger
            @view.logger.debug "ERROR: compiling #{render_symbol} RAISED #{e}"
            @view.logger.debug "Function body: #{render_source}"
            @view.logger.debug "Backtrace: #{e.backtrace.join("\n")}"
          end

          raise ActionView::TemplateError.new(template, @view.assigns, e)
        end

        self.compile_time[render_symbol] = Time.now
        # logger.debug "Compiled template #{file_name || template}\n  ==> #{render_symbol}" if logger
      end

      private

      # Method to check whether template compilation is necessary.
      # The template will be compiled if the inline template or file has not been compiled yet,
      # if local_assigns has a new key, which isn't supported by the compiled code yet,
      # or if the file has changed on disk and checking file mods hasn't been disabled.
      def compile_template?(template)
        method_key    = template.method_key
        render_symbol = @view.method_names[method_key]

        compile_time = self.compile_time[render_symbol]
        if compile_time && supports_local_assigns?(render_symbol, template.locals)
          if template.filename && !@view.cache_template_loading
            template_changed_since?(template.filename, compile_time)
          end
        else
          true
        end
      end

      def assign_method_name(template)
        @view.method_names[template.method_key] ||= compiled_method_name(template)
      end

      def compiled_method_name(template)
        ['_run', self.class.to_s.demodulize.underscore, compiled_method_name_file_path_segment(template.filename)].compact.join('_').to_sym
      end

      def compiled_method_name_file_path_segment(file_name)
        if file_name
          s = File.expand_path(file_name)
          s.sub!(/^#{Regexp.escape(File.expand_path(RAILS_ROOT))}/, '') if defined?(RAILS_ROOT)
          s.gsub!(/([^a-zA-Z0-9_])/) { $1.ord }
          s
        else
          (self.inline_template_count += 1).to_s
        end
      end

      # Method to create the source code for a given template.
      def create_template_source(template, render_symbol)
        body = compile(template)

        self.template_args[render_symbol] ||= {}
        locals_keys = self.template_args[render_symbol].keys | template.locals.keys
        self.template_args[render_symbol] = locals_keys.inject({}) { |h, k| h[k] = true; h }

        locals_code = ""
        locals_keys.each do |key|
          locals_code << "#{key} = local_assigns[:#{key}]\n"
        end

        "def #{render_symbol}(local_assigns)\nold_output_buffer = output_buffer;#{locals_code}#{body}\nensure\nself.output_buffer = old_output_buffer\nend"
      end

      # Return true if the given template was compiled for a superset of the keys in local_assigns
      def supports_local_assigns?(render_symbol, local_assigns)
        local_assigns.empty? ||
          ((args = self.template_args[render_symbol]) && local_assigns.all? { |k,_| args.has_key?(k) })
      end

      # Method to handle checking a whether a template has changed since last compile; isolated so that templates
      # not stored on the file system can hook and extend appropriately.
      def template_changed_since?(file_name, compile_time)
        lstat = File.lstat(file_name)
        compile_time < lstat.mtime ||
          (lstat.symlink? && compile_time < File.stat(file_name).mtime)
      end

    end
  end
end