actionpack 2.1.2 → 2.2.2

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of actionpack might be problematic. Click here for more details.

Files changed (200) hide show
  1. data/CHANGELOG +223 -7
  2. data/README +6 -12
  3. data/Rakefile +11 -11
  4. data/lib/action_controller.rb +9 -9
  5. data/lib/action_controller/assertions/response_assertions.rb +29 -78
  6. data/lib/action_controller/assertions/routing_assertions.rb +33 -33
  7. data/lib/action_controller/assertions/selector_assertions.rb +9 -5
  8. data/lib/action_controller/base.rb +227 -161
  9. data/lib/action_controller/benchmarking.rb +37 -24
  10. data/lib/action_controller/caching/actions.rb +53 -21
  11. data/lib/action_controller/caching/fragments.rb +10 -36
  12. data/lib/action_controller/caching/sweeping.rb +3 -3
  13. data/lib/action_controller/cgi_ext/session.rb +2 -22
  14. data/lib/action_controller/cgi_process.rb +8 -46
  15. data/lib/action_controller/components.rb +4 -1
  16. data/lib/action_controller/cookies.rb +10 -0
  17. data/lib/action_controller/dispatcher.rb +49 -15
  18. data/lib/action_controller/filters.rb +48 -10
  19. data/lib/action_controller/headers.rb +16 -14
  20. data/lib/action_controller/helpers.rb +2 -2
  21. data/lib/action_controller/http_authentication.rb +1 -1
  22. data/lib/action_controller/integration.rb +57 -60
  23. data/lib/action_controller/layout.rb +27 -53
  24. data/lib/action_controller/mime_responds.rb +5 -1
  25. data/lib/action_controller/mime_type.rb +64 -42
  26. data/lib/action_controller/mime_types.rb +2 -1
  27. data/lib/action_controller/performance_test.rb +16 -0
  28. data/lib/action_controller/polymorphic_routes.rb +16 -9
  29. data/lib/action_controller/rack_process.rb +303 -0
  30. data/lib/action_controller/request.rb +205 -97
  31. data/lib/action_controller/request_forgery_protection.rb +2 -2
  32. data/lib/action_controller/request_profiler.rb +0 -0
  33. data/lib/action_controller/rescue.rb +20 -115
  34. data/lib/action_controller/resources.rb +186 -83
  35. data/lib/action_controller/response.rb +140 -26
  36. data/lib/action_controller/routing.rb +28 -30
  37. data/lib/action_controller/routing/builder.rb +45 -54
  38. data/lib/action_controller/routing/optimisations.rb +31 -21
  39. data/lib/action_controller/routing/recognition_optimisation.rb +33 -27
  40. data/lib/action_controller/routing/route.rb +162 -147
  41. data/lib/action_controller/routing/route_set.rb +8 -7
  42. data/lib/action_controller/routing/routing_ext.rb +4 -1
  43. data/lib/action_controller/routing/segments.rb +50 -21
  44. data/lib/action_controller/session/cookie_store.rb +3 -2
  45. data/lib/action_controller/session/drb_server.rb +7 -7
  46. data/lib/action_controller/session_management.rb +6 -2
  47. data/lib/action_controller/streaming.rb +15 -8
  48. data/lib/action_controller/templates/rescues/diagnostics.erb +2 -2
  49. data/lib/action_controller/templates/rescues/template_error.erb +2 -2
  50. data/lib/action_controller/test_case.rb +66 -2
  51. data/lib/action_controller/test_process.rb +71 -66
  52. data/lib/action_controller/translation.rb +13 -0
  53. data/lib/action_controller/url_rewriter.rb +90 -13
  54. data/lib/action_controller/vendor/html-scanner/html/node.rb +9 -2
  55. data/lib/action_controller/vendor/html-scanner/html/sanitizer.rb +1 -1
  56. data/lib/action_controller/vendor/html-scanner/html/selector.rb +2 -2
  57. data/lib/action_controller/verification.rb +2 -2
  58. data/lib/action_pack/version.rb +1 -1
  59. data/lib/action_view.rb +19 -11
  60. data/lib/action_view/base.rb +184 -150
  61. data/lib/action_view/helpers.rb +38 -0
  62. data/lib/action_view/helpers/active_record_helper.rb +56 -27
  63. data/lib/action_view/helpers/asset_tag_helper.rb +356 -153
  64. data/lib/action_view/helpers/atom_feed_helper.rb +74 -19
  65. data/lib/action_view/helpers/benchmark_helper.rb +3 -3
  66. data/lib/action_view/helpers/cache_helper.rb +1 -2
  67. data/lib/action_view/helpers/capture_helper.rb +19 -44
  68. data/lib/action_view/helpers/date_helper.rb +486 -296
  69. data/lib/action_view/helpers/debug_helper.rb +20 -13
  70. data/lib/action_view/helpers/form_helper.rb +71 -30
  71. data/lib/action_view/helpers/form_options_helper.rb +15 -85
  72. data/lib/action_view/helpers/form_tag_helper.rb +61 -38
  73. data/lib/action_view/helpers/javascript_helper.rb +80 -89
  74. data/lib/action_view/helpers/number_helper.rb +179 -74
  75. data/lib/action_view/helpers/prototype_helper.rb +216 -201
  76. data/lib/action_view/helpers/record_tag_helper.rb +4 -5
  77. data/lib/action_view/helpers/sanitize_helper.rb +65 -33
  78. data/lib/action_view/helpers/scriptaculous_helper.rb +2 -2
  79. data/lib/action_view/helpers/tag_helper.rb +39 -22
  80. data/lib/action_view/helpers/text_helper.rb +212 -118
  81. data/lib/action_view/helpers/translation_helper.rb +21 -0
  82. data/lib/action_view/helpers/url_helper.rb +100 -58
  83. data/lib/action_view/inline_template.rb +13 -14
  84. data/lib/action_view/locale/en.yml +91 -0
  85. data/lib/action_view/partials.rb +100 -55
  86. data/lib/action_view/paths.rb +125 -0
  87. data/lib/action_view/renderable.rb +102 -0
  88. data/lib/action_view/renderable_partial.rb +48 -0
  89. data/lib/action_view/template.rb +90 -101
  90. data/lib/action_view/template_error.rb +11 -21
  91. data/lib/action_view/template_handler.rb +8 -28
  92. data/lib/action_view/template_handlers.rb +45 -0
  93. data/lib/action_view/template_handlers/builder.rb +5 -15
  94. data/lib/action_view/template_handlers/erb.rb +9 -6
  95. data/lib/action_view/template_handlers/rjs.rb +2 -17
  96. data/lib/action_view/test_case.rb +7 -4
  97. data/test/abstract_unit.rb +4 -1
  98. data/test/active_record_unit.rb +28 -30
  99. data/test/activerecord/render_partial_with_record_identification_test.rb +25 -12
  100. data/test/controller/action_pack_assertions_test.rb +8 -37
  101. data/test/controller/addresses_render_test.rb +0 -3
  102. data/test/controller/assert_select_test.rb +51 -24
  103. data/test/controller/base_test.rb +4 -4
  104. data/test/controller/caching_test.rb +136 -66
  105. data/test/controller/capture_test.rb +1 -21
  106. data/test/controller/cgi_test.rb +157 -10
  107. data/test/controller/components_test.rb +41 -25
  108. data/test/controller/content_type_test.rb +49 -17
  109. data/test/controller/cookie_test.rb +1 -1
  110. data/test/controller/deprecation/deprecated_base_methods_test.rb +0 -3
  111. data/test/controller/dispatcher_test.rb +9 -1
  112. data/test/controller/filter_params_test.rb +2 -2
  113. data/test/controller/filters_test.rb +13 -13
  114. data/test/controller/html-scanner/cdata_node_test.rb +15 -0
  115. data/test/controller/html-scanner/node_test.rb +21 -0
  116. data/test/controller/html-scanner/sanitizer_test.rb +14 -0
  117. data/test/controller/integration_test.rb +167 -6
  118. data/test/controller/layout_test.rb +11 -68
  119. data/test/controller/logging_test.rb +46 -0
  120. data/test/controller/mime_responds_test.rb +61 -59
  121. data/test/controller/mime_type_test.rb +6 -6
  122. data/test/controller/polymorphic_routes_test.rb +37 -2
  123. data/test/controller/rack_test.rb +323 -0
  124. data/test/controller/redirect_test.rb +72 -71
  125. data/test/controller/render_test.rb +1120 -108
  126. data/test/controller/request_forgery_protection_test.rb +66 -52
  127. data/test/controller/request_test.rb +103 -146
  128. data/test/controller/rescue_test.rb +20 -24
  129. data/test/controller/resources_test.rb +408 -25
  130. data/test/controller/routing_test.rb +1774 -1774
  131. data/test/controller/send_file_test.rb +0 -4
  132. data/test/controller/session/cookie_store_test.rb +53 -1
  133. data/test/controller/test_test.rb +15 -37
  134. data/test/controller/translation_test.rb +26 -0
  135. data/test/controller/url_rewriter_test.rb +27 -28
  136. data/test/controller/view_paths_test.rb +48 -47
  137. data/test/fixtures/_top_level_partial.html.erb +1 -0
  138. data/test/fixtures/_top_level_partial_only.erb +1 -0
  139. data/test/fixtures/developers/_developer.erb +1 -0
  140. data/test/fixtures/fun/games/_game.erb +1 -0
  141. data/test/fixtures/fun/serious/games/_game.erb +1 -0
  142. data/test/fixtures/functional_caching/formatted_fragment_cached.html.erb +3 -0
  143. data/test/fixtures/functional_caching/formatted_fragment_cached.js.rjs +6 -0
  144. data/test/fixtures/functional_caching/formatted_fragment_cached.xml.builder +5 -0
  145. data/test/fixtures/functional_caching/inline_fragment_cached.html.erb +2 -0
  146. data/test/fixtures/layouts/_column.html.erb +2 -0
  147. data/test/fixtures/projects/_project.erb +1 -0
  148. data/test/fixtures/public/javascripts/subdir/subdir.js +1 -0
  149. data/test/fixtures/public/stylesheets/subdir/subdir.css +1 -0
  150. data/test/fixtures/replies/_reply.erb +1 -0
  151. data/test/fixtures/test/_counter.html.erb +1 -0
  152. data/test/fixtures/test/_customer.erb +1 -1
  153. data/test/fixtures/test/_customer_with_var.erb +1 -0
  154. data/test/fixtures/test/_layout_for_block_with_args.html.erb +3 -0
  155. data/test/fixtures/test/_local_inspector.html.erb +1 -0
  156. data/test/fixtures/test/_partial_with_only_html_version.html.erb +1 -0
  157. data/test/fixtures/test/hello.builder +1 -1
  158. data/test/fixtures/test/hyphen-ated.erb +1 -0
  159. data/test/fixtures/test/implicit_content_type.atom.builder +2 -0
  160. data/test/fixtures/test/nested_layout.erb +3 -0
  161. data/test/fixtures/test/non_erb_block_content_for.builder +1 -1
  162. data/test/fixtures/test/sub_template_raise.html.erb +1 -0
  163. data/test/fixtures/test/template.erb +1 -0
  164. data/test/fixtures/test/using_layout_around_block_with_args.html.erb +1 -0
  165. data/test/template/active_record_helper_i18n_test.rb +46 -0
  166. data/test/template/active_record_helper_test.rb +24 -24
  167. data/test/template/asset_tag_helper_test.rb +161 -29
  168. data/test/template/atom_feed_helper_test.rb +114 -5
  169. data/test/template/compiled_templates_test.rb +59 -0
  170. data/test/template/date_helper_i18n_test.rb +113 -0
  171. data/test/template/date_helper_test.rb +403 -109
  172. data/test/template/form_helper_test.rb +213 -154
  173. data/test/template/form_options_helper_test.rb +249 -897
  174. data/test/template/form_tag_helper_test.rb +80 -32
  175. data/test/template/javascript_helper_test.rb +17 -18
  176. data/test/template/number_helper_i18n_test.rb +54 -0
  177. data/test/template/number_helper_test.rb +43 -13
  178. data/test/template/prototype_helper_test.rb +101 -84
  179. data/test/template/record_tag_helper_test.rb +24 -20
  180. data/test/template/render_test.rb +193 -0
  181. data/test/template/sanitize_helper_test.rb +3 -3
  182. data/test/template/tag_helper_test.rb +34 -14
  183. data/test/template/text_helper_test.rb +83 -9
  184. data/test/template/translation_helper_test.rb +28 -0
  185. data/test/template/url_helper_test.rb +55 -18
  186. metadata +57 -18
  187. data/lib/action_view/helpers/javascripts/controls.js +0 -963
  188. data/lib/action_view/helpers/javascripts/dragdrop.js +0 -972
  189. data/lib/action_view/helpers/javascripts/effects.js +0 -1120
  190. data/lib/action_view/helpers/javascripts/prototype.js +0 -4225
  191. data/lib/action_view/partial_template.rb +0 -70
  192. data/lib/action_view/template_finder.rb +0 -177
  193. data/lib/action_view/template_handlers/compilable.rb +0 -128
  194. data/test/controller/custom_handler_test.rb +0 -45
  195. data/test/controller/new_render_test.rb +0 -945
  196. data/test/fixtures/test/block_content_for.erb +0 -2
  197. data/test/fixtures/test/erb_content_for.erb +0 -2
  198. data/test/template/deprecated_erb_variable_test.rb +0 -9
  199. data/test/template/template_finder_test.rb +0 -73
  200. data/test/template/template_object_test.rb +0 -95
@@ -0,0 +1,38 @@
1
+ Dir.entries(File.expand_path("#{File.dirname(__FILE__)}/helpers")).sort.each do |file|
2
+ next unless file =~ /^([a-z][a-z_]*_helper).rb$/
3
+ require "action_view/helpers/#{$1}"
4
+ end
5
+
6
+ module ActionView #:nodoc:
7
+ module Helpers #:nodoc:
8
+ def self.included(base)
9
+ base.extend(ClassMethods)
10
+ end
11
+
12
+ module ClassMethods
13
+ include SanitizeHelper::ClassMethods
14
+ end
15
+
16
+ include ActiveRecordHelper
17
+ include AssetTagHelper
18
+ include AtomFeedHelper
19
+ include BenchmarkHelper
20
+ include CacheHelper
21
+ include CaptureHelper
22
+ include DateHelper
23
+ include DebugHelper
24
+ include FormHelper
25
+ include FormOptionsHelper
26
+ include FormTagHelper
27
+ include NumberHelper
28
+ include PrototypeHelper
29
+ include RecordIdentificationHelper
30
+ include RecordTagHelper
31
+ include SanitizeHelper
32
+ include ScriptaculousHelper
33
+ include TagHelper
34
+ include TextHelper
35
+ include TranslationHelper
36
+ include UrlHelper
37
+ end
38
+ end
@@ -25,7 +25,7 @@ module ActionView
25
25
  # Returns an entire form with all needed input tags for a specified Active Record object. For example, if <tt>@post</tt>
26
26
  # has attributes named +title+ of type +VARCHAR+ and +body+ of type +TEXT+ then
27
27
  #
28
- # form("post")
28
+ # form("post")
29
29
  #
30
30
  # would yield a form like the following (modulus formatting):
31
31
  #
@@ -90,23 +90,41 @@ module ActionView
90
90
  end
91
91
 
92
92
  # Returns a string containing the error message attached to the +method+ on the +object+ if one exists.
93
- # This error message is wrapped in a <tt>DIV</tt> tag, which can be extended to include a +prepend_text+ and/or +append_text+
94
- # (to properly explain the error), and a +css_class+ to style it accordingly. +object+ should either be the name of an instance variable or
95
- # the actual object. As an example, let's say you have a model <tt>@post</tt> that has an error message on the +title+ attribute:
93
+ # This error message is wrapped in a <tt>DIV</tt> tag, which can be extended to include a <tt>:prepend_text</tt>
94
+ # and/or <tt>:append_text</tt> (to properly explain the error), and a <tt>:css_class</tt> to style it
95
+ # accordingly. +object+ should either be the name of an instance variable or the actual object. The method can be
96
+ # passed in either as a string or a symbol.
97
+ # As an example, let's say you have a model <tt>@post</tt> that has an error message on the +title+ attribute:
96
98
  #
97
99
  # <%= error_message_on "post", "title" %>
98
100
  # # => <div class="formError">can't be empty</div>
99
101
  #
100
- # <%= error_message_on @post, "title" %>
102
+ # <%= error_message_on @post, :title %>
101
103
  # # => <div class="formError">can't be empty</div>
102
104
  #
103
- # <%= error_message_on "post", "title", "Title simply ", " (or it won't work).", "inputError" %>
104
- # # => <div class="inputError">Title simply can't be empty (or it won't work).</div>
105
- def error_message_on(object, method, prepend_text = "", append_text = "", css_class = "formError")
105
+ # <%= error_message_on "post", "title",
106
+ # :prepend_text => "Title simply ",
107
+ # :append_text => " (or it won't work).",
108
+ # :css_class => "inputError" %>
109
+ def error_message_on(object, method, *args)
110
+ options = args.extract_options!
111
+ unless args.empty?
112
+ ActiveSupport::Deprecation.warn('error_message_on takes an option hash instead of separate ' +
113
+ 'prepend_text, append_text, and css_class arguments', caller)
114
+
115
+ options[:prepend_text] = args[0] || ''
116
+ options[:append_text] = args[1] || ''
117
+ options[:css_class] = args[2] || 'formError'
118
+ end
119
+ options.reverse_merge!(:prepend_text => '', :append_text => '', :css_class => 'formError')
120
+
106
121
  if (obj = (object.respond_to?(:errors) ? object : instance_variable_get("@#{object}"))) &&
107
122
  (errors = obj.errors.on(method))
108
- content_tag("div", "#{prepend_text}#{errors.is_a?(Array) ? errors.first : errors}#{append_text}", :class => css_class)
109
- else
123
+ content_tag("div",
124
+ "#{options[:prepend_text]}#{errors.is_a?(Array) ? errors.first : errors}#{options[:append_text]}",
125
+ :class => options[:css_class]
126
+ )
127
+ else
110
128
  ''
111
129
  end
112
130
  end
@@ -133,7 +151,7 @@ module ActionView
133
151
  #
134
152
  # To specify the display for one object, you simply provide its name as a parameter.
135
153
  # For example, for the <tt>@user</tt> model:
136
- #
154
+ #
137
155
  # error_messages_for 'user'
138
156
  #
139
157
  # To specify more than one object, you simply list them; optionally, you can add an extra <tt>:object_name</tt> parameter, which
@@ -141,7 +159,7 @@ module ActionView
141
159
  #
142
160
  # error_messages_for 'user_common', 'user', :object_name => 'user'
143
161
  #
144
- # If the objects cannot be located as instance variables, you can add an extra <tt>:object</tt> paremeter which gives the actual
162
+ # If the objects cannot be located as instance variables, you can add an extra <tt>:object</tt> parameter which gives the actual
145
163
  # object (or array of objects to use):
146
164
  #
147
165
  # error_messages_for 'user', :object => @question.user
@@ -151,12 +169,14 @@ module ActionView
151
169
  # instance yourself and set it up. View the source of this method to see how easy it is.
152
170
  def error_messages_for(*params)
153
171
  options = params.extract_options!.symbolize_keys
172
+
154
173
  if object = options.delete(:object)
155
174
  objects = [object].flatten
156
175
  else
157
176
  objects = params.collect {|object_name| instance_variable_get("@#{object_name}") }.compact
158
177
  end
159
- count = objects.inject(0) {|sum, object| sum + object.errors.count }
178
+
179
+ count = objects.inject(0) {|sum, object| sum + object.errors.count }
160
180
  unless count.zero?
161
181
  html = {}
162
182
  [:id, :class].each do |key|
@@ -168,16 +188,25 @@ module ActionView
168
188
  end
169
189
  end
170
190
  options[:object_name] ||= params.first
171
- options[:header_message] = "#{pluralize(count, 'error')} prohibited this #{options[:object_name].to_s.gsub('_', ' ')} from being saved" unless options.include?(:header_message)
172
- options[:message] ||= 'There were problems with the following fields:' unless options.include?(:message)
173
- error_messages = objects.sum {|object| object.errors.full_messages.map {|msg| content_tag(:li, msg) } }.join
174
191
 
175
- contents = ''
176
- contents << content_tag(options[:header_tag] || :h2, options[:header_message]) unless options[:header_message].blank?
177
- contents << content_tag(:p, options[:message]) unless options[:message].blank?
178
- contents << content_tag(:ul, error_messages)
192
+ I18n.with_options :locale => options[:locale], :scope => [:activerecord, :errors, :template] do |locale|
193
+ header_message = if options.include?(:header_message)
194
+ options[:header_message]
195
+ else
196
+ object_name = options[:object_name].to_s.gsub('_', ' ')
197
+ object_name = I18n.t(object_name, :default => object_name, :scope => [:activerecord, :models], :count => 1)
198
+ locale.t :header, :count => count, :model => object_name
199
+ end
200
+ message = options.include?(:message) ? options[:message] : locale.t(:body)
201
+ error_messages = objects.sum {|object| object.errors.full_messages.map {|msg| content_tag(:li, msg) } }.join
179
202
 
180
- content_tag(:div, contents, html)
203
+ contents = ''
204
+ contents << content_tag(options[:header_tag] || :h2, header_message) unless header_message.blank?
205
+ contents << content_tag(:p, message) unless message.blank?
206
+ contents << content_tag(:ul, error_messages)
207
+
208
+ content_tag(:div, contents, html)
209
+ end
181
210
  else
182
211
  ''
183
212
  end
@@ -217,7 +246,7 @@ module ActionView
217
246
 
218
247
  alias_method :tag_without_error_wrapping, :tag
219
248
  def tag(name, options)
220
- if object.respond_to?("errors") && object.errors.respond_to?("on")
249
+ if object.respond_to?(:errors) && object.errors.respond_to?(:on)
221
250
  error_wrapping(tag_without_error_wrapping(name, options), object.errors.on(@method_name))
222
251
  else
223
252
  tag_without_error_wrapping(name, options)
@@ -226,7 +255,7 @@ module ActionView
226
255
 
227
256
  alias_method :content_tag_without_error_wrapping, :content_tag
228
257
  def content_tag(name, value, options)
229
- if object.respond_to?("errors") && object.errors.respond_to?("on")
258
+ if object.respond_to?(:errors) && object.errors.respond_to?(:on)
230
259
  error_wrapping(content_tag_without_error_wrapping(name, value, options), object.errors.on(@method_name))
231
260
  else
232
261
  content_tag_without_error_wrapping(name, value, options)
@@ -235,7 +264,7 @@ module ActionView
235
264
 
236
265
  alias_method :to_date_select_tag_without_error_wrapping, :to_date_select_tag
237
266
  def to_date_select_tag(options = {}, html_options = {})
238
- if object.respond_to?("errors") && object.errors.respond_to?("on")
267
+ if object.respond_to?(:errors) && object.errors.respond_to?(:on)
239
268
  error_wrapping(to_date_select_tag_without_error_wrapping(options, html_options), object.errors.on(@method_name))
240
269
  else
241
270
  to_date_select_tag_without_error_wrapping(options, html_options)
@@ -244,7 +273,7 @@ module ActionView
244
273
 
245
274
  alias_method :to_datetime_select_tag_without_error_wrapping, :to_datetime_select_tag
246
275
  def to_datetime_select_tag(options = {}, html_options = {})
247
- if object.respond_to?("errors") && object.errors.respond_to?("on")
276
+ if object.respond_to?(:errors) && object.errors.respond_to?(:on)
248
277
  error_wrapping(to_datetime_select_tag_without_error_wrapping(options, html_options), object.errors.on(@method_name))
249
278
  else
250
279
  to_datetime_select_tag_without_error_wrapping(options, html_options)
@@ -253,7 +282,7 @@ module ActionView
253
282
 
254
283
  alias_method :to_time_select_tag_without_error_wrapping, :to_time_select_tag
255
284
  def to_time_select_tag(options = {}, html_options = {})
256
- if object.respond_to?("errors") && object.errors.respond_to?("on")
285
+ if object.respond_to?(:errors) && object.errors.respond_to?(:on)
257
286
  error_wrapping(to_time_select_tag_without_error_wrapping(options, html_options), object.errors.on(@method_name))
258
287
  else
259
288
  to_time_select_tag_without_error_wrapping(options, html_options)
@@ -269,7 +298,7 @@ module ActionView
269
298
  end
270
299
 
271
300
  def column_type
272
- object.send("column_for_attribute", @method_name).type
301
+ object.send(:column_for_attribute, @method_name).type
273
302
  end
274
303
  end
275
304
  end
@@ -5,12 +5,12 @@ require 'action_view/helpers/tag_helper'
5
5
  module ActionView
6
6
  module Helpers #:nodoc:
7
7
  # This module provides methods for generating HTML that links views to assets such
8
- # as images, javascripts, stylesheets, and feeds. These methods do not verify
9
- # the assets exist before linking to them.
8
+ # as images, javascripts, stylesheets, and feeds. These methods do not verify
9
+ # the assets exist before linking to them.
10
10
  #
11
11
  # === Using asset hosts
12
12
  # By default, Rails links to these assets on the current host in the public
13
- # folder, but you can direct Rails to link to assets from a dedicated assets server by
13
+ # folder, but you can direct Rails to link to assets from a dedicated assets server by
14
14
  # setting ActionController::Base.asset_host in your <tt>config/environment.rb</tt>. For example,
15
15
  # let's say your asset host is <tt>assets.example.com</tt>.
16
16
  #
@@ -22,16 +22,16 @@ module ActionView
22
22
  #
23
23
  # This is useful since browsers typically open at most two connections to a single host,
24
24
  # which means your assets often wait in single file for their turn to load. You can
25
- # alleviate this by using a <tt>%d</tt> wildcard in <tt>asset_host</tt> (for example, "assets%d.example.com")
25
+ # alleviate this by using a <tt>%d</tt> wildcard in <tt>asset_host</tt> (for example, "assets%d.example.com")
26
26
  # to automatically distribute asset requests among four hosts (e.g., "assets0.example.com" through "assets3.example.com")
27
- # so browsers will open eight connections rather than two.
27
+ # so browsers will open eight connections rather than two.
28
28
  #
29
29
  # image_tag("rails.png")
30
30
  # => <img src="http://assets0.example.com/images/rails.png" alt="Rails" />
31
31
  # stylesheet_link_tag("application")
32
32
  # => <link href="http://assets3.example.com/stylesheets/application.css" media="screen" rel="stylesheet" type="text/css" />
33
33
  #
34
- # To do this, you can either setup 4 actual hosts, or you can use wildcard DNS to CNAME
34
+ # To do this, you can either setup 4 actual hosts, or you can use wildcard DNS to CNAME
35
35
  # the wildcard to a single asset host. You can read more about setting up your DNS CNAME records from
36
36
  # your ISP.
37
37
  #
@@ -86,7 +86,7 @@ module ActionView
86
86
  # asset far into the future, but still be able to instantly invalidate it by simply updating the file (and hence updating the timestamp,
87
87
  # which then updates the URL as the timestamp is part of that, which in turn busts the cache).
88
88
  #
89
- # It's the responsibility of the web server you use to set the far-future expiration date on cache assets that you need to take
89
+ # It's the responsibility of the web server you use to set the far-future expiration date on cache assets that you need to take
90
90
  # advantage of this feature. Here's an example for Apache:
91
91
  #
92
92
  # # Asset Expiration
@@ -95,16 +95,17 @@ module ActionView
95
95
  # ExpiresDefault "access plus 1 year"
96
96
  # </FilesMatch>
97
97
  #
98
- # Also note that in order for this to work, all your application servers must return the same timestamps. This means that they must
98
+ # Also note that in order for this to work, all your application servers must return the same timestamps. This means that they must
99
99
  # have their clocks synchronized. If one of them drift out of sync, you'll see different timestamps at random and the cache won't
100
100
  # work. Which means that the browser will request the same assets over and over again even thought they didn't change. You can use
101
- # something like Live HTTP Headers for Firefox to verify that the cache is indeed working (and that the assets are not being
101
+ # something like Live HTTP Headers for Firefox to verify that the cache is indeed working (and that the assets are not being
102
102
  # requested over and over).
103
103
  module AssetTagHelper
104
104
  ASSETS_DIR = defined?(Rails.public_path) ? Rails.public_path : "public"
105
105
  JAVASCRIPTS_DIR = "#{ASSETS_DIR}/javascripts"
106
106
  STYLESHEETS_DIR = "#{ASSETS_DIR}/stylesheets"
107
-
107
+ JAVASCRIPT_DEFAULT_SOURCES = ['prototype', 'effects', 'dragdrop', 'controls'].freeze unless const_defined?(:JAVASCRIPT_DEFAULT_SOURCES)
108
+
108
109
  # Returns a link tag that browsers and news readers can use to auto-detect
109
110
  # an RSS or ATOM feed. The +type+ can either be <tt>:rss</tt> (default) or
110
111
  # <tt>:atom</tt>. Control the link options in url_for format using the
@@ -150,14 +151,10 @@ module ActionView
150
151
  # javascript_path "http://www.railsapplication.com/js/xmlhr" # => http://www.railsapplication.com/js/xmlhr.js
151
152
  # javascript_path "http://www.railsapplication.com/js/xmlhr.js" # => http://www.railsapplication.com/js/xmlhr.js
152
153
  def javascript_path(source)
153
- compute_public_path(source, 'javascripts', 'js')
154
+ JavaScriptTag.new(self, @controller, source).public_path
154
155
  end
155
156
  alias_method :path_to_javascript, :javascript_path # aliased to avoid conflicts with a javascript_path named route
156
157
 
157
- JAVASCRIPT_DEFAULT_SOURCES = ['prototype', 'effects', 'dragdrop', 'controls'] unless const_defined?(:JAVASCRIPT_DEFAULT_SOURCES)
158
- @@javascript_expansions = { :defaults => JAVASCRIPT_DEFAULT_SOURCES.dup }
159
- @@stylesheet_expansions = {}
160
-
161
158
  # Returns an html script tag for each of the +sources+ provided. You
162
159
  # can pass in the filename (.js extension is optional) of javascript files
163
160
  # that exist in your public/javascripts directory for inclusion into the
@@ -193,7 +190,7 @@ module ActionView
193
190
  #
194
191
  # * = The application.js file is only referenced if it exists
195
192
  #
196
- # Though it's not really recommended practice, if you need to extend the default JavaScript set for any reason
193
+ # Though it's not really recommended practice, if you need to extend the default JavaScript set for any reason
197
194
  # (e.g., you're going to be using a certain .js file in every action), then take a look at the register_javascript_include_default method.
198
195
  #
199
196
  # You can also include all javascripts in the javascripts directory using <tt>:all</tt> as the source:
@@ -209,12 +206,16 @@ module ActionView
209
206
  # Note that the default javascript files will be included first. So Prototype and Scriptaculous are available to
210
207
  # all subsequently included files.
211
208
  #
209
+ # If you want Rails to search in all the subdirectories under javascripts, you should explicitly set <tt>:recursive</tt>:
210
+ #
211
+ # javascript_include_tag :all, :recursive => true
212
+ #
212
213
  # == Caching multiple javascripts into one
213
214
  #
214
215
  # You can also cache multiple javascripts into one file, which requires less HTTP connections to download and can better be
215
216
  # compressed by gzip (leading to faster transfers). Caching will only happen if ActionController::Base.perform_caching
216
217
  # is set to <tt>true</tt> (which is the case by default for the Rails production environment, but not for the development
217
- # environment).
218
+ # environment).
218
219
  #
219
220
  # ==== Examples
220
221
  # javascript_include_tag :all, :cache => true # when ActionController::Base.perform_caching is false =>
@@ -235,18 +236,27 @@ module ActionView
235
236
  #
236
237
  # javascript_include_tag "prototype", "cart", "checkout", :cache => "shop" # when ActionController::Base.perform_caching is true =>
237
238
  # <script type="text/javascript" src="/javascripts/shop.js"></script>
239
+ #
240
+ # The <tt>:recursive</tt> option is also available for caching:
241
+ #
242
+ # javascript_include_tag :all, :cache => true, :recursive => true
238
243
  def javascript_include_tag(*sources)
239
244
  options = sources.extract_options!.stringify_keys
240
245
  cache = options.delete("cache")
246
+ recursive = options.delete("recursive")
241
247
 
242
248
  if ActionController::Base.perform_caching && cache
243
249
  joined_javascript_name = (cache == true ? "all" : cache) + ".js"
244
250
  joined_javascript_path = File.join(JAVASCRIPTS_DIR, joined_javascript_name)
245
251
 
246
- write_asset_file_contents(joined_javascript_path, compute_javascript_paths(sources))
252
+ unless File.exists?(joined_javascript_path)
253
+ JavaScriptSources.create(self, @controller, sources, recursive).write_asset_file_contents(joined_javascript_path)
254
+ end
247
255
  javascript_src_tag(joined_javascript_name, options)
248
256
  else
249
- expand_javascript_sources(sources).collect { |source| javascript_src_tag(source, options) }.join("\n")
257
+ JavaScriptSources.create(self, @controller, sources, recursive).expand_sources.collect { |source|
258
+ javascript_src_tag(source, options)
259
+ }.join("\n")
250
260
  end
251
261
  end
252
262
 
@@ -262,7 +272,7 @@ module ActionView
262
272
  # <script type="text/javascript" src="/javascripts/body.js"></script>
263
273
  # <script type="text/javascript" src="/javascripts/tail.js"></script>
264
274
  def self.register_javascript_expansion(expansions)
265
- @@javascript_expansions.merge!(expansions)
275
+ JavaScriptSources.expansions.merge!(expansions)
266
276
  end
267
277
 
268
278
  # Register one or more stylesheet files to be included when <tt>symbol</tt>
@@ -277,7 +287,7 @@ module ActionView
277
287
  # <link href="/stylesheets/body.css" media="screen" rel="stylesheet" type="text/css" />
278
288
  # <link href="/stylesheets/tail.css" media="screen" rel="stylesheet" type="text/css" />
279
289
  def self.register_stylesheet_expansion(expansions)
280
- @@stylesheet_expansions.merge!(expansions)
290
+ StylesheetSources.expansions.merge!(expansions)
281
291
  end
282
292
 
283
293
  # Register one or more additional JavaScript files to be included when
@@ -285,11 +295,11 @@ module ActionView
285
295
  # typically intended to be called from plugin initialization to register additional
286
296
  # .js files that the plugin installed in <tt>public/javascripts</tt>.
287
297
  def self.register_javascript_include_default(*sources)
288
- @@javascript_expansions[:defaults].concat(sources)
298
+ JavaScriptSources.expansions[:defaults].concat(sources)
289
299
  end
290
300
 
291
301
  def self.reset_javascript_include_default #:nodoc:
292
- @@javascript_expansions[:defaults] = JAVASCRIPT_DEFAULT_SOURCES.dup
302
+ JavaScriptSources.expansions[:defaults] = JAVASCRIPT_DEFAULT_SOURCES.dup
293
303
  end
294
304
 
295
305
  # Computes the path to a stylesheet asset in the public stylesheets directory.
@@ -304,7 +314,7 @@ module ActionView
304
314
  # stylesheet_path "http://www.railsapplication.com/css/style" # => http://www.railsapplication.com/css/style.css
305
315
  # stylesheet_path "http://www.railsapplication.com/css/style.js" # => http://www.railsapplication.com/css/style.css
306
316
  def stylesheet_path(source)
307
- compute_public_path(source, 'stylesheets', 'css')
317
+ StylesheetTag.new(self, @controller, source).public_path
308
318
  end
309
319
  alias_method :path_to_stylesheet, :stylesheet_path # aliased to avoid conflicts with a stylesheet_path named route
310
320
 
@@ -332,13 +342,17 @@ module ActionView
332
342
  # <link href="/stylesheets/random.styles" media="screen" rel="stylesheet" type="text/css" />
333
343
  # <link href="/css/stylish.css" media="screen" rel="stylesheet" type="text/css" />
334
344
  #
335
- # You can also include all styles in the stylesheet directory using <tt>:all</tt> as the source:
345
+ # You can also include all styles in the stylesheets directory using <tt>:all</tt> as the source:
336
346
  #
337
347
  # stylesheet_link_tag :all # =>
338
348
  # <link href="/stylesheets/style1.css" media="screen" rel="stylesheet" type="text/css" />
339
349
  # <link href="/stylesheets/styleB.css" media="screen" rel="stylesheet" type="text/css" />
340
350
  # <link href="/stylesheets/styleX2.css" media="screen" rel="stylesheet" type="text/css" />
341
351
  #
352
+ # If you want Rails to search in all the subdirectories under stylesheets, you should explicitly set <tt>:recursive</tt>:
353
+ #
354
+ # stylesheet_link_tag :all, :recursive => true
355
+ #
342
356
  # == Caching multiple stylesheets into one
343
357
  #
344
358
  # You can also cache multiple stylesheets into one file, which requires less HTTP connections and can better be
@@ -362,18 +376,27 @@ module ActionView
362
376
  #
363
377
  # stylesheet_link_tag "shop", "cart", "checkout", :cache => "payment" # when ActionController::Base.perform_caching is true =>
364
378
  # <link href="/stylesheets/payment.css" media="screen" rel="stylesheet" type="text/css" />
379
+ #
380
+ # The <tt>:recursive</tt> option is also available for caching:
381
+ #
382
+ # stylesheet_link_tag :all, :cache => true, :recursive => true
365
383
  def stylesheet_link_tag(*sources)
366
384
  options = sources.extract_options!.stringify_keys
367
385
  cache = options.delete("cache")
386
+ recursive = options.delete("recursive")
368
387
 
369
388
  if ActionController::Base.perform_caching && cache
370
389
  joined_stylesheet_name = (cache == true ? "all" : cache) + ".css"
371
390
  joined_stylesheet_path = File.join(STYLESHEETS_DIR, joined_stylesheet_name)
372
391
 
373
- write_asset_file_contents(joined_stylesheet_path, compute_stylesheet_paths(sources))
392
+ unless File.exists?(joined_stylesheet_path)
393
+ StylesheetSources.create(self, @controller, sources, recursive).write_asset_file_contents(joined_stylesheet_path)
394
+ end
374
395
  stylesheet_tag(joined_stylesheet_name, options)
375
396
  else
376
- expand_stylesheet_sources(sources).collect { |source| stylesheet_tag(source, options) }.join("\n")
397
+ StylesheetSources.create(self, @controller, sources, recursive).expand_sources.collect { |source|
398
+ stylesheet_tag(source, options)
399
+ }.join("\n")
377
400
  end
378
401
  end
379
402
 
@@ -388,7 +411,7 @@ module ActionView
388
411
  # image_path("/icons/edit.png") # => /icons/edit.png
389
412
  # image_path("http://www.railsapplication.com/img/edit.png") # => http://www.railsapplication.com/img/edit.png
390
413
  def image_path(source)
391
- compute_public_path(source, 'images')
414
+ ImageTag.new(self, @controller, source).public_path
392
415
  end
393
416
  alias_method :path_to_image, :image_path # aliased to avoid conflicts with an image_path named route
394
417
 
@@ -421,9 +444,9 @@ module ActionView
421
444
  # <img alt="Icon" height="32" src="/icons/icon.gif" width="32" />
422
445
  # image_tag("/icons/icon.gif", :class => "menu_icon") # =>
423
446
  # <img alt="Icon" class="menu_icon" src="/icons/icon.gif" />
424
- # image_tag("mouse.png", :mouseover => "/images/mouse_over.png") # =>
447
+ # image_tag("mouse.png", :mouseover => "/images/mouse_over.png") # =>
425
448
  # <img src="/images/mouse.png" onmouseover="this.src='/images/mouse_over.png'" onmouseout="this.src='/images/mouse.png'" alt="Mouse" />
426
- # image_tag("mouse.png", :mouseover => image_path("mouse_over.png")) # =>
449
+ # image_tag("mouse.png", :mouseover => image_path("mouse_over.png")) # =>
427
450
  # <img src="/images/mouse.png" onmouseover="this.src='/images/mouse_over.png'" onmouseout="this.src='/images/mouse.png'" alt="Mouse" />
428
451
  def image_tag(source, options = {})
429
452
  options.symbolize_keys!
@@ -436,178 +459,358 @@ module ActionView
436
459
  end
437
460
 
438
461
  if mouseover = options.delete(:mouseover)
439
- options[:onmouseover] = "this.src='#{image_path(mouseover)}'"
440
- options[:onmouseout] = "this.src='#{image_path(options[:src])}'"
462
+ options[:onmouseover] = "this.src='#{image_path(mouseover)}'"
463
+ options[:onmouseout] = "this.src='#{image_path(options[:src])}'"
441
464
  end
442
465
 
443
466
  tag("img", options)
444
467
  end
445
468
 
446
469
  private
447
- def file_exist?(path)
448
- @@file_exist_cache ||= {}
449
- if !(@@file_exist_cache[path] ||= File.exist?(path))
450
- @@file_exist_cache[path] = true
451
- false
452
- else
453
- true
470
+ def javascript_src_tag(source, options)
471
+ content_tag("script", "", { "type" => Mime::JS, "src" => path_to_javascript(source) }.merge(options))
472
+ end
473
+
474
+ def stylesheet_tag(source, options)
475
+ tag("link", { "rel" => "stylesheet", "type" => Mime::CSS, "media" => "screen", "href" => html_escape(path_to_stylesheet(source)) }.merge(options), false, false)
476
+ end
477
+
478
+ module ImageAsset
479
+ DIRECTORY = 'images'.freeze
480
+
481
+ def directory
482
+ DIRECTORY
483
+ end
484
+
485
+ def extension
486
+ nil
487
+ end
488
+ end
489
+
490
+ module JavaScriptAsset
491
+ DIRECTORY = 'javascripts'.freeze
492
+ EXTENSION = 'js'.freeze
493
+
494
+ def public_directory
495
+ JAVASCRIPTS_DIR
496
+ end
497
+
498
+ def directory
499
+ DIRECTORY
500
+ end
501
+
502
+ def extension
503
+ EXTENSION
454
504
  end
455
505
  end
456
506
 
457
- # Add the the extension +ext+ if not present. Return full URLs otherwise untouched.
458
- # Prefix with <tt>/dir/</tt> if lacking a leading +/+. Account for relative URL
459
- # roots. Rewrite the asset path for cache-busting asset ids. Include
460
- # asset host, if configured, with the correct request protocol.
461
- def compute_public_path(source, dir, ext = nil, include_host = true)
462
- has_request = @controller.respond_to?(:request)
463
-
464
- cache_key =
465
- if has_request
466
- [ @controller.request.protocol,
467
- ActionController::Base.asset_host.to_s,
468
- @controller.request.relative_url_root,
469
- dir, source, ext, include_host ].join
507
+ module StylesheetAsset
508
+ DIRECTORY = 'stylesheets'.freeze
509
+ EXTENSION = 'css'.freeze
510
+
511
+ def public_directory
512
+ STYLESHEETS_DIR
513
+ end
514
+
515
+ def directory
516
+ DIRECTORY
517
+ end
518
+
519
+ def extension
520
+ EXTENSION
521
+ end
522
+ end
523
+
524
+ class AssetTag
525
+ extend ActiveSupport::Memoizable
526
+
527
+ Cache = {}
528
+ CacheGuard = Mutex.new
529
+
530
+ ProtocolRegexp = %r{^[-a-z]+://}.freeze
531
+
532
+ def initialize(template, controller, source, include_host = true)
533
+ # NOTE: The template arg is temporarily needed for a legacy plugin
534
+ # hook that is expected to call rewrite_asset_path on the
535
+ # template. This should eventually be removed.
536
+ @template = template
537
+ @controller = controller
538
+ @source = source
539
+ @include_host = include_host
540
+ @cache_key = if controller.respond_to?(:request)
541
+ [self.class.name,controller.request.protocol,
542
+ ActionController::Base.asset_host,
543
+ ActionController::Base.relative_url_root,
544
+ source, include_host]
470
545
  else
471
- [ ActionController::Base.asset_host.to_s,
472
- dir, source, ext, include_host ].join
546
+ [self.class.name,ActionController::Base.asset_host, source, include_host]
473
547
  end
548
+ end
549
+
550
+ def public_path
551
+ compute_public_path(@source)
552
+ end
553
+ memoize :public_path
554
+
555
+ def asset_file_path
556
+ File.join(ASSETS_DIR, public_path.split('?').first)
557
+ end
558
+ memoize :asset_file_path
559
+
560
+ def contents
561
+ File.read(asset_file_path)
562
+ end
563
+
564
+ def mtime
565
+ File.mtime(asset_file_path)
566
+ end
474
567
 
475
- ActionView::Base.computed_public_paths[cache_key] ||=
476
- begin
477
- source += ".#{ext}" if ext && File.extname(source).blank? || File.exist?(File.join(ASSETS_DIR, dir, "#{source}.#{ext}"))
568
+ private
569
+ def request
570
+ @controller.request
571
+ end
478
572
 
479
- if source =~ %r{^[-a-z]+://}
573
+ def request?
574
+ @controller.respond_to?(:request)
575
+ end
576
+
577
+ # Add the the extension +ext+ if not present. Return full URLs otherwise untouched.
578
+ # Prefix with <tt>/dir/</tt> if lacking a leading +/+. Account for relative URL
579
+ # roots. Rewrite the asset path for cache-busting asset ids. Include
580
+ # asset host, if configured, with the correct request protocol.
581
+ def compute_public_path(source)
582
+ if source =~ ProtocolRegexp
583
+ source += ".#{extension}" if missing_extension?(source)
584
+ source = prepend_asset_host(source)
480
585
  source
481
586
  else
482
- source = "/#{dir}/#{source}" unless source[0] == ?/
483
- if has_request
484
- unless source =~ %r{^#{@controller.request.relative_url_root}/}
485
- source = "#{@controller.request.relative_url_root}#{source}"
587
+ CacheGuard.synchronize do
588
+ Cache[@cache_key] ||= begin
589
+ source += ".#{extension}" if missing_extension?(source) || file_exists_with_extension?(source)
590
+ source = "/#{directory}/#{source}" unless source[0] == ?/
591
+ source = rewrite_asset_path(source)
592
+ source = prepend_relative_url_root(source)
593
+ source = prepend_asset_host(source)
594
+ source
486
595
  end
487
596
  end
597
+ end
598
+ end
599
+
600
+ def missing_extension?(source)
601
+ extension && File.extname(source).blank?
602
+ end
603
+
604
+ def file_exists_with_extension?(source)
605
+ extension && File.exist?(File.join(ASSETS_DIR, directory, "#{source}.#{extension}"))
606
+ end
607
+
608
+ def prepend_relative_url_root(source)
609
+ relative_url_root = ActionController::Base.relative_url_root
610
+ if request? && @include_host && source !~ %r{^#{relative_url_root}/}
611
+ "#{relative_url_root}#{source}"
612
+ else
613
+ source
614
+ end
615
+ end
488
616
 
489
- rewrite_asset_path(source)
617
+ def prepend_asset_host(source)
618
+ if @include_host && source !~ ProtocolRegexp
619
+ host = compute_asset_host(source)
620
+ if request? && !host.blank? && host !~ ProtocolRegexp
621
+ host = "#{request.protocol}#{host}"
622
+ end
623
+ "#{host}#{source}"
624
+ else
625
+ source
490
626
  end
491
627
  end
492
628
 
493
- source = ActionView::Base.computed_public_paths[cache_key]
629
+ # Pick an asset host for this source. Returns +nil+ if no host is set,
630
+ # the host if no wildcard is set, the host interpolated with the
631
+ # numbers 0-3 if it contains <tt>%d</tt> (the number is the source hash mod 4),
632
+ # or the value returned from invoking the proc if it's a proc.
633
+ def compute_asset_host(source)
634
+ if host = ActionController::Base.asset_host
635
+ if host.is_a?(Proc)
636
+ case host.arity
637
+ when 2
638
+ host.call(source, request)
639
+ else
640
+ host.call(source)
641
+ end
642
+ else
643
+ (host =~ /%d/) ? host % (source.hash % 4) : host
644
+ end
645
+ end
646
+ end
494
647
 
495
- if include_host && source !~ %r{^[-a-z]+://}
496
- host = compute_asset_host(source)
648
+ # Use the RAILS_ASSET_ID environment variable or the source's
649
+ # modification time as its cache-busting asset id.
650
+ def rails_asset_id(source)
651
+ if asset_id = ENV["RAILS_ASSET_ID"]
652
+ asset_id
653
+ else
654
+ path = File.join(ASSETS_DIR, source)
497
655
 
498
- if has_request && !host.blank? && host !~ %r{^[-a-z]+://}
499
- host = "#{@controller.request.protocol}#{host}"
656
+ if File.exist?(path)
657
+ File.mtime(path).to_i.to_s
658
+ else
659
+ ''
660
+ end
661
+ end
500
662
  end
501
663
 
502
- "#{host}#{source}"
503
- else
504
- source
505
- end
506
- end
507
-
508
- # Pick an asset host for this source. Returns +nil+ if no host is set,
509
- # the host if no wildcard is set, the host interpolated with the
510
- # numbers 0-3 if it contains <tt>%d</tt> (the number is the source hash mod 4),
511
- # or the value returned from invoking the proc if it's a proc.
512
- def compute_asset_host(source)
513
- if host = ActionController::Base.asset_host
514
- if host.is_a?(Proc)
515
- case host.arity
516
- when 2
517
- host.call(source, @controller.request)
664
+ # Break out the asset path rewrite in case plugins wish to put the asset id
665
+ # someplace other than the query string.
666
+ def rewrite_asset_path(source)
667
+ if @template.respond_to?(:rewrite_asset_path)
668
+ # DEPRECATE: This way to override rewrite_asset_path
669
+ @template.send(:rewrite_asset_path, source)
518
670
  else
519
- host.call(source)
671
+ asset_id = rails_asset_id(source)
672
+ if asset_id.blank?
673
+ source
674
+ else
675
+ "#{source}?#{asset_id}"
676
+ end
520
677
  end
521
- else
522
- (host =~ /%d/) ? host % (source.hash % 4) : host
523
678
  end
524
- end
525
679
  end
526
680
 
527
- # Use the RAILS_ASSET_ID environment variable or the source's
528
- # modification time as its cache-busting asset id.
529
- def rails_asset_id(source)
530
- if asset_id = ENV["RAILS_ASSET_ID"]
531
- asset_id
532
- else
533
- path = File.join(ASSETS_DIR, source)
534
-
535
- if File.exist?(path)
536
- File.mtime(path).to_i.to_s
537
- else
538
- ''
539
- end
540
- end
681
+ class ImageTag < AssetTag
682
+ include ImageAsset
541
683
  end
542
684
 
543
- # Break out the asset path rewrite in case plugins wish to put the asset id
544
- # someplace other than the query string.
545
- def rewrite_asset_path(source)
546
- asset_id = rails_asset_id(source)
547
- if asset_id.blank?
548
- source
549
- else
550
- source + "?#{asset_id}"
551
- end
685
+ class JavaScriptTag < AssetTag
686
+ include JavaScriptAsset
552
687
  end
553
688
 
554
- def javascript_src_tag(source, options)
555
- content_tag("script", "", { "type" => Mime::JS, "src" => path_to_javascript(source) }.merge(options))
689
+ class StylesheetTag < AssetTag
690
+ include StylesheetAsset
556
691
  end
557
692
 
558
- def stylesheet_tag(source, options)
559
- tag("link", { "rel" => "stylesheet", "type" => Mime::CSS, "media" => "screen", "href" => html_escape(path_to_stylesheet(source)) }.merge(options), false, false)
560
- end
693
+ class AssetCollection
694
+ extend ActiveSupport::Memoizable
561
695
 
562
- def compute_javascript_paths(sources)
563
- expand_javascript_sources(sources).collect { |source| compute_public_path(source, 'javascripts', 'js', false) }
564
- end
696
+ Cache = {}
697
+ CacheGuard = Mutex.new
565
698
 
566
- def compute_stylesheet_paths(sources)
567
- expand_stylesheet_sources(sources).collect { |source| compute_public_path(source, 'stylesheets', 'css', false) }
568
- end
699
+ def self.create(template, controller, sources, recursive)
700
+ CacheGuard.synchronize do
701
+ key = [self, sources, recursive]
702
+ Cache[key] ||= new(template, controller, sources, recursive).freeze
703
+ end
704
+ end
569
705
 
570
- def expand_javascript_sources(sources)
571
- if sources.include?(:all)
572
- all_javascript_files = Dir[File.join(JAVASCRIPTS_DIR, '*.js')].collect { |file| File.basename(file).gsub(/\.\w+$/, '') }.sort
573
- @@all_javascript_sources ||= ((determine_source(:defaults, @@javascript_expansions).dup & all_javascript_files) + all_javascript_files).uniq
574
- else
575
- expanded_sources = sources.collect do |source|
576
- determine_source(source, @@javascript_expansions)
577
- end.flatten
578
- expanded_sources << "application" if sources.include?(:defaults) && file_exist?(File.join(JAVASCRIPTS_DIR, "application.js"))
579
- expanded_sources
706
+ def initialize(template, controller, sources, recursive)
707
+ # NOTE: The template arg is temporarily needed for a legacy plugin
708
+ # hook. See NOTE under AssetTag#initialize for more details
709
+ @template = template
710
+ @controller = controller
711
+ @sources = sources
712
+ @recursive = recursive
580
713
  end
581
- end
582
714
 
583
- def expand_stylesheet_sources(sources)
584
- if sources.first == :all
585
- @@all_stylesheet_sources ||= Dir[File.join(STYLESHEETS_DIR, '*.css')].collect { |file| File.basename(file).gsub(/\.\w+$/, '') }.sort
586
- else
587
- sources.collect do |source|
588
- determine_source(source, @@stylesheet_expansions)
589
- end.flatten
715
+ def write_asset_file_contents(joined_asset_path)
716
+ FileUtils.mkdir_p(File.dirname(joined_asset_path))
717
+ File.open(joined_asset_path, "w+") { |cache| cache.write(joined_contents) }
718
+ mt = latest_mtime
719
+ File.utime(mt, mt, joined_asset_path)
590
720
  end
721
+
722
+ private
723
+ def determine_source(source, collection)
724
+ case source
725
+ when Symbol
726
+ collection[source] || raise(ArgumentError, "No expansion found for #{source.inspect}")
727
+ else
728
+ source
729
+ end
730
+ end
731
+
732
+ def validate_sources!
733
+ @sources.collect { |source| determine_source(source, self.class.expansions) }.flatten
734
+ end
735
+
736
+ def all_asset_files
737
+ path = [public_directory, ('**' if @recursive), "*.#{extension}"].compact
738
+ Dir[File.join(*path)].collect { |file|
739
+ file[-(file.size - public_directory.size - 1)..-1].sub(/\.\w+$/, '')
740
+ }.sort
741
+ end
742
+
743
+ def tag_sources
744
+ expand_sources.collect { |source| tag_class.new(@template, @controller, source, false) }
745
+ end
746
+
747
+ def joined_contents
748
+ tag_sources.collect { |source| source.contents }.join("\n\n")
749
+ end
750
+
751
+ # Set mtime to the latest of the combined files to allow for
752
+ # consistent ETag without a shared filesystem.
753
+ def latest_mtime
754
+ tag_sources.map { |source| source.mtime }.max
755
+ end
591
756
  end
592
757
 
593
- def determine_source(source, collection)
594
- case source
595
- when Symbol
596
- collection[source] || raise(ArgumentError, "No expansion found for #{source.inspect}")
597
- else
598
- source
758
+ class JavaScriptSources < AssetCollection
759
+ include JavaScriptAsset
760
+
761
+ EXPANSIONS = { :defaults => JAVASCRIPT_DEFAULT_SOURCES.dup }
762
+
763
+ def self.expansions
764
+ EXPANSIONS
599
765
  end
600
- end
601
766
 
602
- def join_asset_file_contents(paths)
603
- paths.collect { |path| File.read(File.join(ASSETS_DIR, path.split("?").first)) }.join("\n\n")
767
+ APPLICATION_JS = "application".freeze
768
+ APPLICATION_FILE = "application.js".freeze
769
+
770
+ def expand_sources
771
+ if @sources.include?(:all)
772
+ assets = all_asset_files
773
+ ((defaults.dup & assets) + assets).uniq!
774
+ else
775
+ expanded_sources = validate_sources!
776
+ expanded_sources << APPLICATION_JS if include_application?
777
+ expanded_sources
778
+ end
779
+ end
780
+ memoize :expand_sources
781
+
782
+ private
783
+ def tag_class
784
+ JavaScriptTag
785
+ end
786
+
787
+ def defaults
788
+ determine_source(:defaults, self.class.expansions)
789
+ end
790
+
791
+ def include_application?
792
+ @sources.include?(:defaults) && File.exist?(File.join(JAVASCRIPTS_DIR, APPLICATION_FILE))
793
+ end
604
794
  end
605
795
 
606
- def write_asset_file_contents(joined_asset_path, asset_paths)
607
- unless file_exist?(joined_asset_path)
608
- FileUtils.mkdir_p(File.dirname(joined_asset_path))
609
- File.open(joined_asset_path, "w+") { |cache| cache.write(join_asset_file_contents(asset_paths)) }
796
+ class StylesheetSources < AssetCollection
797
+ include StylesheetAsset
798
+
799
+ EXPANSIONS = {}
800
+
801
+ def self.expansions
802
+ EXPANSIONS
610
803
  end
804
+
805
+ def expand_sources
806
+ @sources.first == :all ? all_asset_files : validate_sources!
807
+ end
808
+ memoize :expand_sources
809
+
810
+ private
811
+ def tag_class
812
+ StylesheetTag
813
+ end
611
814
  end
612
815
  end
613
816
  end