lookup_context.rb 7.7 KB
Newer Older
1
require 'thread_safe'
2
require 'active_support/core_ext/module/remove_method'
3
require 'active_support/core_ext/module/attribute_accessors'
4
require 'action_view/template/resolver'
5

6
module ActionView
7 8
  # = Action View Lookup Context
  #
9 10 11 12 13
  # LookupContext is the object responsible to hold all information required to lookup
  # templates, i.e. view paths and details. The LookupContext is also responsible to
  # generate a key, given to view paths, used in the resolver cache lookup. Since
  # this key is generated just once during the request, it speeds up all cache accesses.
  class LookupContext #:nodoc:
14
    attr_accessor :prefixes, :rendered_format
G
Guillermo Iguaran 已提交
15

16
    mattr_accessor :fallbacks
17
    @@fallbacks = FallbackFileSystemResolver.instances
18

19
    mattr_accessor :registered_details
J
José Valim 已提交
20
    self.registered_details = []
21

22
    def self.register_detail(name, options = {}, &block)
J
José Valim 已提交
23
      self.registered_details << name
24
      initialize = registered_details.map { |n| "@details[:#{n}] = details[:#{n}] || default_#{n}" }
25

26
      Accessors.send :define_method, :"default_#{name}", &block
27 28
      Accessors.module_eval <<-METHOD, __FILE__, __LINE__ + 1
        def #{name}
A
Aaron Patterson 已提交
29
          @details.fetch(:#{name}, [])
30
        end
J
José Valim 已提交
31

32
        def #{name}=(value)
33
          value = value.present? ? Array(value) : default_#{name}
34
          _set_detail(:#{name}, value) if value != @details[:#{name}]
J
José Valim 已提交
35
        end
36 37 38 39 40

        remove_possible_method :initialize_details
        def initialize_details(details)
          #{initialize.join("\n")}
        end
41
      METHOD
42 43
    end

44 45
    # Holds accessors for the registered details.
    module Accessors #:nodoc:
J
José Valim 已提交
46 47
    end

48 49 50 51 52 53 54
    register_detail(:locale) do
      locales = [I18n.locale]
      locales.concat(I18n.fallbacks[I18n.locale]) if I18n.respond_to? :fallbacks
      locales << I18n.default_locale
      locales.uniq!
      locales
    end
55
    register_detail(:formats) { ActionView::Base.default_formats || [:html, :text, :js, :css,  :xml, :json] }
Ł
Łukasz Strzałkowski 已提交
56
    register_detail(:variants) { [] }
J
José Valim 已提交
57
    register_detail(:handlers){ Template::Handlers.extensions }
58

59 60
    class DetailsKey #:nodoc:
      alias :eql? :equal?
J
José Valim 已提交
61
      alias :object_hash :hash
62

J
José Valim 已提交
63
      attr_reader :hash
64
      @details_keys = ThreadSafe::Cache.new
65 66

      def self.get(details)
67 68
        if details[:formats]
          details = details.dup
69
          syms    = Mime::SET.symbols
70 71 72 73
          details[:formats] = details[:formats].select { |v|
            syms.include? v
          }
        end
A
Aaron Patterson 已提交
74
        @details_keys[details] ||= new
75 76
      end

77 78 79 80
      def self.clear
        @details_keys.clear
      end

J
José Valim 已提交
81 82
      def initialize
        @hash = object_hash
83 84 85
      end
    end

86 87 88
    # Add caching behavior on top of Details.
    module DetailsCache
      attr_accessor :cache
89

90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106
      # Calculate the details key. Remove the handlers from calculation to improve performance
      # since the user cannot modify it explicitly.
      def details_key #:nodoc:
        @details_key ||= DetailsKey.get(@details) if @cache
      end

      # Temporary skip passing the details_key forward.
      def disable_cache
        old_value, @cache = @cache, false
        yield
      ensure
        @cache = old_value
      end

    protected

      def _set_detail(key, value)
107
        @details = @details.dup if @details_key
108
        @details_key = nil
A
Aaron Patterson 已提交
109
        @details[key] = value
110
      end
111 112
    end

113
    # Helpers related to template lookup using the lookup context information.
114
    module ViewPaths
115
      attr_reader :view_paths, :html_fallback_for_js
116

117
      # Whenever setting view paths, makes a copy so that we can manipulate them in
118 119
      # instance objects as we wish.
      def view_paths=(paths)
120
        @view_paths = ActionView::PathSet.new(Array(paths))
121
      end
122

123 124
      def find(name, prefixes = [], partial = false, keys = [], options = {})
        @view_paths.find(*args_for_lookup(name, prefixes, partial, keys, options))
125
      end
126
      alias :find_template :find
127

128 129
      def find_all(name, prefixes = [], partial = false, keys = [], options = {})
        @view_paths.find_all(*args_for_lookup(name, prefixes, partial, keys, options))
130 131
      end

132 133
      def exists?(name, prefixes = [], partial = false, keys = [], options = {})
        @view_paths.exists?(*args_for_lookup(name, prefixes, partial, keys, options))
134
      end
135
      alias :template_exists? :exists?
136

137 138
      # Adds fallbacks to the view paths. Useful in cases when you are rendering
      # a :file.
139 140 141 142 143 144
      def with_fallbacks
        added_resolvers = 0
        self.class.fallbacks.each do |resolver|
          next if view_paths.include?(resolver)
          view_paths.push(resolver)
          added_resolvers += 1
145
        end
146 147 148
        yield
      ensure
        added_resolvers.times { view_paths.pop }
149
      end
150 151 152

    protected

153
      def args_for_lookup(name, prefixes, partial, keys, details_options) #:nodoc:
A
artemave 已提交
154
        name, prefixes = normalize_name(name, prefixes)
155 156 157 158 159 160 161 162
        details, details_key = detail_args_for(details_options)
        [name, prefixes, partial || false, details, details_key, keys]
      end

      # Compute details hash and key according to user options (e.g. passed from #render).
      def detail_args_for(options)
        return @details, details_key if options.empty? # most common path.
        user_details = @details.merge(options)
163 164 165 166 167 168 169 170

        if @cache
          details_key = DetailsKey.get(user_details)
        else
          details_key = nil
        end

        [user_details, details_key]
171 172 173 174 175
      end

      # Support legacy foo.erb names even though we now ignore .erb
      # as well as incorrectly putting part of the path in the template
      # name instead of the prefix.
A
artemave 已提交
176
      def normalize_name(name, prefixes) #:nodoc:
177
        prefixes = prefixes.presence
178
        parts    = name.to_s.split('/')
179
        parts.shift if parts.first.empty?
180
        name     = parts.pop
A
artemave 已提交
181

182 183 184 185
        return name, prefixes || [""] if parts.empty?

        parts    = parts.join('/')
        prefixes = prefixes ? prefixes.map { |p| "#{p}/#{parts}" } : [parts]
186 187

        return name, prefixes
188
      end
189 190
    end

191 192 193
    include Accessors
    include DetailsCache
    include ViewPaths
194

195
    def initialize(view_paths, details = {}, prefixes = [])
J
José Valim 已提交
196
      @details, @details_key = {}, nil
197
      @skip_default_locale = false
198 199
      @cache = true
      @prefixes = prefixes
200
      @rendered_format = nil
201

202 203 204
      self.view_paths = view_paths
      initialize_details(details)
    end
205

206 207 208 209
    # Override formats= to expand ["*/*"] values and automatically
    # add :html as fallback to :js.
    def formats=(values)
      if values
210
        values.concat(default_formats) if values.delete "*/*"
211 212 213 214
        if values == [:js]
          values << :html
          @html_fallback_for_js = true
        end
215
      end
216 217
      super(values)
    end
218

219 220 221 222 223
    # Do not use the default locale on template lookup.
    def skip_default_locale!
      @skip_default_locale = true
      self.locale = nil
    end
224

225 226 227 228
    # Override locale to return a symbol instead of array.
    def locale
      @details[:locale].first
    end
229

230
    # Overload locale= to also set the I18n.locale. If the current I18n.config object responds
231
    # to original_config, it means that it has a copy of the original I18n configuration and it's
232 233 234 235 236
    # acting as proxy, which we need to skip.
    def locale=(value)
      if value
        config = I18n.config.respond_to?(:original_config) ? I18n.config.original_config : I18n.config
        config.locale = value
W
wycats 已提交
237 238
      end

239
      super(@skip_default_locale ? I18n.locale : default_locale)
240
    end
241

242
    # Uses the first format in the formats array for layout lookup.
243 244 245 246 247 248
    def with_layout_format
      if formats.size == 1
        yield
      else
        old_formats = formats
        _set_detail(:formats, formats[0,1])
249

250 251 252
        begin
          yield
        ensure
253
          _set_detail(:formats, old_formats)
254 255
        end
      end
256 257
    end
  end
A
artemave 已提交
258
end