roda 1.0.0 → 1.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG +34 -0
- data/README.rdoc +18 -13
- data/Rakefile +8 -0
- data/doc/conventions.rdoc +163 -0
- data/doc/release_notes/1.1.0.txt +226 -0
- data/lib/roda.rb +51 -22
- data/lib/roda/plugins/assets.rb +613 -0
- data/lib/roda/plugins/caching.rb +215 -0
- data/lib/roda/plugins/chunked.rb +278 -0
- data/lib/roda/plugins/error_email.rb +112 -0
- data/lib/roda/plugins/flash.rb +3 -3
- data/lib/roda/plugins/hooks.rb +1 -1
- data/lib/roda/plugins/indifferent_params.rb +3 -3
- data/lib/roda/plugins/middleware.rb +3 -8
- data/lib/roda/plugins/multi_route.rb +110 -18
- data/lib/roda/plugins/not_allowed.rb +3 -3
- data/lib/roda/plugins/path.rb +38 -0
- data/lib/roda/plugins/render.rb +18 -16
- data/lib/roda/plugins/render_each.rb +0 -2
- data/lib/roda/plugins/streaming.rb +1 -2
- data/lib/roda/plugins/view_subdirs.rb +7 -1
- data/lib/roda/version.rb +1 -1
- data/spec/assets/css/app.scss +1 -0
- data/spec/assets/css/no_access.css +1 -0
- data/spec/assets/css/raw.css +1 -0
- data/spec/assets/js/head/app.js +1 -0
- data/spec/integration_spec.rb +95 -3
- data/spec/matchers_spec.rb +2 -2
- data/spec/plugin/assets_spec.rb +413 -0
- data/spec/plugin/caching_spec.rb +335 -0
- data/spec/plugin/chunked_spec.rb +182 -0
- data/spec/plugin/default_headers_spec.rb +6 -5
- data/spec/plugin/error_email_spec.rb +76 -0
- data/spec/plugin/multi_route_spec.rb +120 -0
- data/spec/plugin/not_allowed_spec.rb +14 -3
- data/spec/plugin/path_spec.rb +29 -0
- data/spec/plugin/render_each_spec.rb +6 -1
- data/spec/plugin/symbol_matchers_spec.rb +7 -2
- data/spec/request_spec.rb +10 -0
- data/spec/response_spec.rb +47 -0
- data/spec/views/about.erb +1 -0
- data/spec/views/about.str +1 -0
- data/spec/views/content-yield.erb +1 -0
- data/spec/views/home.erb +2 -0
- data/spec/views/home.str +2 -0
- data/spec/views/layout-alternative.erb +2 -0
- data/spec/views/layout-yield.erb +3 -0
- data/spec/views/layout.erb +2 -0
- data/spec/views/layout.str +2 -0
- metadata +57 -2
data/lib/roda.rb
CHANGED
@@ -10,6 +10,7 @@ class Roda
|
|
10
10
|
class RodaError < StandardError; end
|
11
11
|
|
12
12
|
if defined?(RUBY_ENGINE) && RUBY_ENGINE != 'ruby'
|
13
|
+
# :nocov:
|
13
14
|
# A thread safe cache class, offering only #[] and #[]= methods,
|
14
15
|
# each protected by a mutex. Used on non-MRI where Hash is not
|
15
16
|
# thread safe.
|
@@ -30,6 +31,7 @@ class Roda
|
|
30
31
|
@mutex.synchronize{@hash[key] = value}
|
31
32
|
end
|
32
33
|
end
|
34
|
+
# :nocov:
|
33
35
|
else
|
34
36
|
# Hashes are already thread-safe in MRI, due to the GVL, so they
|
35
37
|
# can safely be used as a cache.
|
@@ -51,9 +53,10 @@ class Roda
|
|
51
53
|
@roda_class = ::Roda
|
52
54
|
end
|
53
55
|
|
54
|
-
@
|
56
|
+
@app = nil
|
55
57
|
@middleware = []
|
56
58
|
@opts = {}
|
59
|
+
@route_block = nil
|
57
60
|
|
58
61
|
# Module in which all Roda plugins should be stored. Also contains logic for
|
59
62
|
# registering and loading plugins.
|
@@ -86,6 +89,8 @@ class Roda
|
|
86
89
|
# Methods are put into a plugin so future plugins can easily override
|
87
90
|
# them and call super to get the default behavior.
|
88
91
|
module Base
|
92
|
+
SESSION_KEY = 'rack.session'.freeze
|
93
|
+
|
89
94
|
# Class methods for the Roda class.
|
90
95
|
module ClassMethods
|
91
96
|
# The rack application that this class uses.
|
@@ -94,6 +99,9 @@ class Roda
|
|
94
99
|
# The settings/options hash for the current class.
|
95
100
|
attr_reader :opts
|
96
101
|
|
102
|
+
# The route block that this class uses.
|
103
|
+
attr_reader :route_block
|
104
|
+
|
97
105
|
# Call the internal rack application with the given environment.
|
98
106
|
# This allows the class itself to be used as a rack application.
|
99
107
|
# However, for performance, it's better to use #app to get direct
|
@@ -122,17 +130,14 @@ class Roda
|
|
122
130
|
request_module{define_method(:"match_#{key}", &block)}
|
123
131
|
end
|
124
132
|
|
125
|
-
# When inheriting Roda,
|
126
|
-
#
|
127
|
-
# request and response classes in the subclasses to be subclasses
|
128
|
-
# of the request and responses classes in the parent class. This
|
129
|
-
# makes it so child classes inherit plugins from their parent,
|
130
|
-
# but using plugins in child classes does not affect the parent.
|
133
|
+
# When inheriting Roda, copy the shared data into the subclass,
|
134
|
+
# and setup the request and response subclasses.
|
131
135
|
def inherited(subclass)
|
132
136
|
super
|
133
|
-
subclass.instance_variable_set(:@builder, ::Rack::Builder.new)
|
134
137
|
subclass.instance_variable_set(:@middleware, @middleware.dup)
|
135
138
|
subclass.instance_variable_set(:@opts, opts.dup)
|
139
|
+
subclass.instance_variable_set(:@route_block, @route_block)
|
140
|
+
subclass.send(:build_rack_app)
|
136
141
|
|
137
142
|
request_class = Class.new(self::RodaRequest)
|
138
143
|
request_class.roda_class = subclass
|
@@ -235,9 +240,8 @@ class Roda
|
|
235
240
|
# This should only be called once per class, and if called multiple
|
236
241
|
# times will overwrite the previous routing.
|
237
242
|
def route(&block)
|
238
|
-
@
|
239
|
-
|
240
|
-
@app = @builder.to_app
|
243
|
+
@route_block = block
|
244
|
+
build_rack_app
|
241
245
|
end
|
242
246
|
|
243
247
|
# A new thread safe cache instance. This is a method so it can be
|
@@ -252,10 +256,21 @@ class Roda
|
|
252
256
|
# Roda.use Rack::Session::Cookie, :secret=>ENV['secret']
|
253
257
|
def use(*args, &block)
|
254
258
|
@middleware << [args, block]
|
259
|
+
build_rack_app
|
255
260
|
end
|
256
261
|
|
257
262
|
private
|
258
263
|
|
264
|
+
# Build the rack app to use
|
265
|
+
def build_rack_app
|
266
|
+
if block = @route_block
|
267
|
+
builder = Rack::Builder.new
|
268
|
+
@middleware.each{|a, b| builder.use(*a, &b)}
|
269
|
+
builder.run lambda{|env| allocate.call(env, &block)}
|
270
|
+
@app = builder.to_app
|
271
|
+
end
|
272
|
+
end
|
273
|
+
|
259
274
|
# Backbone of the request_module and response_module support.
|
260
275
|
def module_include(type, mod)
|
261
276
|
if type == :response
|
@@ -270,7 +285,9 @@ class Roda
|
|
270
285
|
raise RodaError, "can't provide both argument and block to response_module" if block_given?
|
271
286
|
klass.send(:include, mod)
|
272
287
|
else
|
273
|
-
|
288
|
+
if instance_variable_defined?(iv)
|
289
|
+
mod = instance_variable_get(iv)
|
290
|
+
else
|
274
291
|
mod = instance_variable_set(iv, Module.new)
|
275
292
|
klass.send(:include, mod)
|
276
293
|
end
|
@@ -284,8 +301,6 @@ class Roda
|
|
284
301
|
|
285
302
|
# Instance methods for the Roda class.
|
286
303
|
module InstanceMethods
|
287
|
-
SESSION_KEY = 'rack.session'.freeze
|
288
|
-
|
289
304
|
# Create a request and response of the appopriate
|
290
305
|
# class, the instance_exec the route block with
|
291
306
|
# the request, handling any halts. This is not usually
|
@@ -930,13 +945,31 @@ class Roda
|
|
930
945
|
end
|
931
946
|
|
932
947
|
# Return the rack response array of status, headers, and body
|
933
|
-
# for the current response.
|
948
|
+
# for the current response. If the status has not been set,
|
949
|
+
# uses a 200 status if the body has been written to, otherwise
|
950
|
+
# uses a 404 status. Adds the Content-Length header to the
|
951
|
+
# size of the response body.
|
934
952
|
#
|
935
|
-
#
|
953
|
+
# Example:
|
954
|
+
#
|
955
|
+
# response.finish
|
956
|
+
# # => [200,
|
957
|
+
# # {'Content-Type'=>'text/html', 'Content-Length'=>'0'},
|
958
|
+
# # []]
|
936
959
|
def finish
|
937
960
|
b = @body
|
938
961
|
s = (@status ||= b.empty? ? 404 : 200)
|
939
|
-
|
962
|
+
h = @headers
|
963
|
+
h[CONTENT_LENGTH] = @length.to_s
|
964
|
+
[s, h, b]
|
965
|
+
end
|
966
|
+
|
967
|
+
# Return the rack response array using a given body. Assumes a
|
968
|
+
# 200 response status unless status has been explicitly set,
|
969
|
+
# and doesn't add the Content-Length header or use the existing
|
970
|
+
# body.
|
971
|
+
def finish_with_body(body)
|
972
|
+
[@status || 200, @headers, body]
|
940
973
|
end
|
941
974
|
|
942
975
|
# Set the Location header to the given path, and the status
|
@@ -957,16 +990,12 @@ class Roda
|
|
957
990
|
::Rack::Utils.set_cookie_header!(@headers, key, value)
|
958
991
|
end
|
959
992
|
|
960
|
-
# Write to the response body.
|
961
|
-
# with the size of the string written. Returns nil. Example:
|
993
|
+
# Write to the response body. Returns nil.
|
962
994
|
#
|
963
995
|
# response.write('foo')
|
964
|
-
# response['Content-Length'] # =>'3'
|
965
996
|
def write(str)
|
966
997
|
s = str.to_s
|
967
|
-
|
968
998
|
@length += s.bytesize
|
969
|
-
@headers[CONTENT_LENGTH] = @length.to_s
|
970
999
|
@body << s
|
971
1000
|
nil
|
972
1001
|
end
|
@@ -0,0 +1,613 @@
|
|
1
|
+
class Roda
|
2
|
+
module RodaPlugins
|
3
|
+
# The assets plugin adds support for rendering your CSS and javascript
|
4
|
+
# asset files on the fly in development, and compiling them
|
5
|
+
# to a single, compressed file in production.
|
6
|
+
#
|
7
|
+
# This uses the render plugin for rendering the assets, and the render
|
8
|
+
# plugin uses tilt internally, so you can use any template engine
|
9
|
+
# supported by tilt for you assets. Tilt ships with support for
|
10
|
+
# the following asset template engines, assuming the necessary libaries
|
11
|
+
# are installed:
|
12
|
+
#
|
13
|
+
# css :: Less, Sass, Scss
|
14
|
+
# js :: CoffeeScript
|
15
|
+
#
|
16
|
+
# == Usage
|
17
|
+
#
|
18
|
+
# When loading the plugin, use the :css and :js options
|
19
|
+
# to set the source file(s) to use for CSS and javascript assets:
|
20
|
+
#
|
21
|
+
# plugin :assets, :css => 'some_file.scss', :js => 'some_file.coffee'
|
22
|
+
#
|
23
|
+
# This will look for the following files:
|
24
|
+
#
|
25
|
+
# assets/css/some_file.scss
|
26
|
+
# assets/js/some_file.coffee
|
27
|
+
#
|
28
|
+
# If you want to change the paths where asset files are stored, see the
|
29
|
+
# Options section below.
|
30
|
+
#
|
31
|
+
# === Serving
|
32
|
+
#
|
33
|
+
# In your routes, call the r.assets method to add a route to your assets,
|
34
|
+
# which will make your app serve the rendered assets:
|
35
|
+
#
|
36
|
+
# route do |r|
|
37
|
+
# r.assets
|
38
|
+
# end
|
39
|
+
#
|
40
|
+
# You should generally call +r.assets+ inside the route block itself, and not
|
41
|
+
# under any branches of the routing tree.
|
42
|
+
#
|
43
|
+
# === Views
|
44
|
+
#
|
45
|
+
# In your layout view, use the assets method to add links to your CSS and
|
46
|
+
# javascript assets:
|
47
|
+
#
|
48
|
+
# <%= assets(:css) %>
|
49
|
+
# <%= assets(:js) %>
|
50
|
+
#
|
51
|
+
# You can add attributes to the tags by using an options hash:
|
52
|
+
#
|
53
|
+
# <%= assets(:css, :media => 'print') %>
|
54
|
+
#
|
55
|
+
# == Asset Groups
|
56
|
+
#
|
57
|
+
# The asset plugin supports groups for the cases where you have different
|
58
|
+
# css/js files for your front end and back end. To use asset groups, you
|
59
|
+
# pass a hash for the :css and/or :js options:
|
60
|
+
#
|
61
|
+
# plugin :assets, :css => {:frontend => 'some_frontend_file.scss',
|
62
|
+
# :backend => 'some_backend_file.scss'}
|
63
|
+
#
|
64
|
+
# This expects the following directory structure for your assets:
|
65
|
+
#
|
66
|
+
# assets/css/frontend/some_frontend_file.scss
|
67
|
+
# assets/css/backend/some_backend_file.scss
|
68
|
+
#
|
69
|
+
# If you want do not want to force that directory structure when using
|
70
|
+
# asset groups, you can use the <tt>:group_subdirs => false</tt> option.
|
71
|
+
#
|
72
|
+
# In your view code use an array argument in your call to assets:
|
73
|
+
#
|
74
|
+
# <%= assets([:css, :frontend]) %>
|
75
|
+
#
|
76
|
+
# === Nesting
|
77
|
+
#
|
78
|
+
# Asset groups also supporting nesting, though that should only be needed
|
79
|
+
# in fairly large applications. You can use a nested hash when loading
|
80
|
+
# the plugin:
|
81
|
+
#
|
82
|
+
# plugin :assets,
|
83
|
+
# :css => {:frontend => {:dashboard => 'some_frontend_file.scss'}}
|
84
|
+
#
|
85
|
+
# and an extra entry per nesting level when creating the tags.
|
86
|
+
#
|
87
|
+
# <%= assets([:css, :frontend, :dashboard]) %>
|
88
|
+
#
|
89
|
+
# == Caching
|
90
|
+
#
|
91
|
+
# The assets plugin uses the caching plugin internally, and will set the
|
92
|
+
# Last-Modified header to the modified timestamp of the asset source file
|
93
|
+
# when rendering the asset.
|
94
|
+
#
|
95
|
+
# If you have assets that include other asset files, such as using @import
|
96
|
+
# in a sass file, you need to specify the dependencies for your assets so
|
97
|
+
# that the assets plugin will correctly pick up changes. You can do this
|
98
|
+
# using the :dependencies option to the plugin, which takes a hash where
|
99
|
+
# the keys are paths to asset files, and values are arrays of paths to
|
100
|
+
# dependencies of those asset files:
|
101
|
+
#
|
102
|
+
# app.plugin :assets,
|
103
|
+
# :dependencies=>{'assets/css/bootstrap.scss'=>Dir['assets/css/bootstrap/' '**/*.scss']}
|
104
|
+
#
|
105
|
+
# == Asset Compilation
|
106
|
+
#
|
107
|
+
# In production, you are generally going to want to compile your assets
|
108
|
+
# into a single file, with you can do by calling compile_assets after
|
109
|
+
# loading the plugin:
|
110
|
+
#
|
111
|
+
# plugin :assets, :css => 'some_file.scss', :js => 'some_file.coffee'
|
112
|
+
# compile_assets
|
113
|
+
#
|
114
|
+
# After calling compile_assets, calls to assets in your views will default
|
115
|
+
# to a using a single link each to your CSS and javascript compiled asset
|
116
|
+
# files. By default the compiled files are written to the public directory,
|
117
|
+
# so that they can be served by the webserver.
|
118
|
+
#
|
119
|
+
# === Asset Compression
|
120
|
+
#
|
121
|
+
# If you have the yuicompressor gem installed and working, it will be used
|
122
|
+
# automatically to compress your javascript and css assets. Otherwise,
|
123
|
+
# the assets will just be concatenated together and not compressed during
|
124
|
+
# compilation.
|
125
|
+
#
|
126
|
+
# === With Asset Groups
|
127
|
+
#
|
128
|
+
# When using asset groups, a separate compiled file will be produced per
|
129
|
+
# asset group.
|
130
|
+
#
|
131
|
+
# === Unique Asset Names
|
132
|
+
#
|
133
|
+
# When compiling assets, a unique name is given to each asset file, using the
|
134
|
+
# a SHA1 hash of the content of the file. This is done so that clients do
|
135
|
+
# not attempt to use cached versions of the assets if the asset has changed.
|
136
|
+
#
|
137
|
+
# === Serving
|
138
|
+
#
|
139
|
+
# If you call +r.assets+ when compiling assets, will serve the compiled asset
|
140
|
+
# files. However, it is recommended to have the main webserver (e.g. nginx)
|
141
|
+
# serve the compiled files, instead of relying on the application.
|
142
|
+
#
|
143
|
+
# Assuming you are using compiled assets in production mode that are served
|
144
|
+
# by the webserver, you can remove the serving of them by the application:
|
145
|
+
#
|
146
|
+
# route do |r|
|
147
|
+
# r.assets unless ENV['RACK_ENV'] == 'production'
|
148
|
+
# end
|
149
|
+
#
|
150
|
+
# If you do have the application serve the compiled assets, it will use the
|
151
|
+
# Last-Modified header to make sure that clients do not redownload compiled
|
152
|
+
# assets that haven't changed.
|
153
|
+
#
|
154
|
+
# === Asset Precompilation
|
155
|
+
#
|
156
|
+
# If you want to precompile your assets, so they do not need to be compiled
|
157
|
+
# every time you boot the application, you can provide a :precompiled option
|
158
|
+
# when loading the plugin. The value of this option should be the filename
|
159
|
+
# where the compiled asset metadata is stored.
|
160
|
+
#
|
161
|
+
# If the compiled assset metadata file does not exist when the assets plugin
|
162
|
+
# is loaded, the plugin will run in non-compiled mode. However, when you call
|
163
|
+
# compile_assets, it will write the compiled asset metadata file after
|
164
|
+
# compiling the assets.
|
165
|
+
#
|
166
|
+
# If the compiled asset metadata file already exists when the assets plugin
|
167
|
+
# is loaded, the plugin will read the file to get the compiled asset metadata,
|
168
|
+
# and it will run in compiled mode, assuming that the compiled asset files
|
169
|
+
# already exist.
|
170
|
+
#
|
171
|
+
# ==== On Heroku
|
172
|
+
#
|
173
|
+
# Heroku supports precompiling the assets when using Roda. You just need to
|
174
|
+
# add an assets:precompile task, similar to this:
|
175
|
+
#
|
176
|
+
# namespace :assets do
|
177
|
+
# desc "Precompile the assets"
|
178
|
+
# task :precompile do
|
179
|
+
# require './app'
|
180
|
+
# App.compile_assets
|
181
|
+
# end
|
182
|
+
# end
|
183
|
+
#
|
184
|
+
# == Plugin Options
|
185
|
+
#
|
186
|
+
# :add_suffix :: Whether to append a .css or .js extension to asset routes in non-compiled mode
|
187
|
+
# (default: false)
|
188
|
+
# :compiled_css_dir :: Directory name in which to store the compiled css file,
|
189
|
+
# inside :compiled_path (default: nil)
|
190
|
+
# :compiled_css_route :: Route under :prefix for compiled css assets (default: :compiled_css_dir)
|
191
|
+
# :compiled_js_dir :: Directory name in which to store the compiled javascript file,
|
192
|
+
# inside :compiled_path (default: nil)
|
193
|
+
# :compiled_js_route :: Route under :prefix for compiled javscript assets (default: :compiled_js_dir)
|
194
|
+
# :compiled_name :: Compiled file name prefix (default: 'app')
|
195
|
+
# :compiled_path:: Path inside public folder in which compiled files are stored (default: :prefix)
|
196
|
+
# :concat_only :: Whether to just concatenate instead of concatentating
|
197
|
+
# and compressing files (default: false)
|
198
|
+
# :css_dir :: Directory name containing your css source, inside :path (default: 'css')
|
199
|
+
# :css_headers :: A hash of additional headers for your rendered css files
|
200
|
+
# :css_opts :: Options to pass to the render plugin when rendering css assets
|
201
|
+
# :css_route :: Route under :prefix for css assets (default: :css_dir)
|
202
|
+
# :dependencies :: A hash of dependencies for your asset files. Keys should be paths to asset files,
|
203
|
+
# values should be arrays of paths your asset files depends on. This is used to
|
204
|
+
# detect changes in your asset files.
|
205
|
+
# :group_subdirs :: Whether a hash used in :css and :js options requires the assets for the
|
206
|
+
# related group are contained in a subdirectory with the same name (default: true)
|
207
|
+
# :headers :: A hash of additional headers for both js and css rendered files
|
208
|
+
# :js_dir :: Directory name containing your javascript source, inside :path (default: 'js')
|
209
|
+
# :js_headers :: A hash of additional headers for your rendered javascript files
|
210
|
+
# :js_opts :: Options to pass to the render plugin when rendering javascript assets
|
211
|
+
# :js_route :: Route under :prefix for javascript assets (default: :js_dir)
|
212
|
+
# :path :: Path to your asset source directory (default: 'assets')
|
213
|
+
# :prefix :: Prefix for assets path in your URL/routes (default: 'assets')
|
214
|
+
# :precompiled :: Path to the compiled asset metadata file. If the file exists, will use compiled
|
215
|
+
# mode using the metadata in the file. If the file does not exist, will use
|
216
|
+
# non-compiled mode, but will write the metadata to the file if compile_assets is called.
|
217
|
+
# :public :: Path to your public folder, in which compiled files are placed (default: 'public')
|
218
|
+
module Assets
|
219
|
+
DEFAULTS = {
|
220
|
+
:compiled_name => 'app'.freeze,
|
221
|
+
:js_dir => 'js'.freeze,
|
222
|
+
:css_dir => 'css'.freeze,
|
223
|
+
:path => 'assets'.freeze,
|
224
|
+
:prefix => 'assets'.freeze,
|
225
|
+
:public => 'public'.freeze,
|
226
|
+
:concat_only => false,
|
227
|
+
:compiled => false,
|
228
|
+
:add_suffix => false,
|
229
|
+
:group_subdirs => true,
|
230
|
+
:compiled_css_dir => nil,
|
231
|
+
:compiled_js_dir => nil,
|
232
|
+
}.freeze
|
233
|
+
JS_END = "\"></script>".freeze
|
234
|
+
CSS_END = "\" />".freeze
|
235
|
+
SPACE = ' '.freeze
|
236
|
+
DOT = '.'.freeze
|
237
|
+
SLASH = '/'.freeze
|
238
|
+
NEWLINE = "\n".freeze
|
239
|
+
EMPTY_STRING = ''.freeze
|
240
|
+
JS_SUFFIX = '.js'.freeze
|
241
|
+
CSS_SUFFIX = '.css'.freeze
|
242
|
+
|
243
|
+
# Load the render and caching plugins plugins, since the assets plugin
|
244
|
+
# depends on them.
|
245
|
+
def self.load_dependencies(app, _opts = {})
|
246
|
+
app.plugin :render
|
247
|
+
app.plugin :caching
|
248
|
+
end
|
249
|
+
|
250
|
+
# Setup the options for the plugin. See the Assets module RDoc
|
251
|
+
# for a description of the supported options.
|
252
|
+
def self.configure(app, opts = {})
|
253
|
+
if app.assets_opts
|
254
|
+
prev_opts = app.assets_opts[:orig_opts]
|
255
|
+
orig_opts = app.assets_opts[:orig_opts].merge(opts)
|
256
|
+
[:headers, :css_headers, :js_headers, :css_opts, :js_opts, :dependencies].each do |s|
|
257
|
+
if prev_opts[s]
|
258
|
+
if opts[s]
|
259
|
+
orig_opts[s] = prev_opts[s].merge(opts[s])
|
260
|
+
else
|
261
|
+
orig_opts[s] = prev_opts[s].dup
|
262
|
+
end
|
263
|
+
end
|
264
|
+
end
|
265
|
+
app.opts[:assets] = orig_opts.dup
|
266
|
+
app.opts[:assets][:orig_opts] = orig_opts
|
267
|
+
else
|
268
|
+
app.opts[:assets] = opts.dup
|
269
|
+
app.opts[:assets][:orig_opts] = opts
|
270
|
+
end
|
271
|
+
opts = app.opts[:assets]
|
272
|
+
|
273
|
+
# Combine multiple values into a path, ignoring trailing slashes
|
274
|
+
j = lambda do |*v|
|
275
|
+
opts.values_at(*v).
|
276
|
+
reject{|s| s.to_s.empty?}.
|
277
|
+
map{|s| s.chomp('/')}.
|
278
|
+
join('/').freeze
|
279
|
+
end
|
280
|
+
|
281
|
+
# Same as j, but add a trailing slash if not empty
|
282
|
+
sj = lambda do |*v|
|
283
|
+
s = j.call(*v)
|
284
|
+
s.empty? ? s : (s + '/').freeze
|
285
|
+
end
|
286
|
+
|
287
|
+
if opts[:precompiled] && !opts[:compiled] && ::File.exist?(opts[:precompiled])
|
288
|
+
require 'json'
|
289
|
+
opts[:compiled] = ::JSON.parse(::File.read(opts[:precompiled]))
|
290
|
+
end
|
291
|
+
|
292
|
+
DEFAULTS.each do |k, v|
|
293
|
+
opts[k] = v unless opts.has_key?(k)
|
294
|
+
end
|
295
|
+
|
296
|
+
[
|
297
|
+
[:compiled_path, :prefix],
|
298
|
+
[:js_route, :js_dir],
|
299
|
+
[:css_route, :css_dir],
|
300
|
+
[:compiled_js_route, :compiled_js_dir],
|
301
|
+
[:compiled_css_route, :compiled_css_dir]
|
302
|
+
].each do |k, v|
|
303
|
+
opts[k] = opts[v] unless opts.has_key?(k)
|
304
|
+
end
|
305
|
+
|
306
|
+
[:css_headers, :js_headers, :css_opts, :js_opts, :dependencies].each do |s|
|
307
|
+
opts[s] ||= {}
|
308
|
+
end
|
309
|
+
|
310
|
+
if headers = opts[:headers]
|
311
|
+
opts[:css_headers] = headers.merge(opts[:css_headers])
|
312
|
+
opts[:js_headers] = headers.merge(opts[:js_headers])
|
313
|
+
end
|
314
|
+
opts[:css_headers]['Content-Type'] ||= "text/css; charset=UTF-8".freeze
|
315
|
+
opts[:js_headers]['Content-Type'] ||= "application/javascript; charset=UTF-8".freeze
|
316
|
+
|
317
|
+
[:css_headers, :js_headers, :css_opts, :js_opts, :dependencies].each do |s|
|
318
|
+
opts[s].freeze
|
319
|
+
end
|
320
|
+
[:headers, :css, :js].each do |s|
|
321
|
+
opts[s].freeze if opts[s]
|
322
|
+
end
|
323
|
+
|
324
|
+
# Used for reading/writing files
|
325
|
+
opts[:js_path] = sj.call(:path, :js_dir)
|
326
|
+
opts[:css_path] = sj.call(:path, :css_dir)
|
327
|
+
opts[:compiled_js_path] = j.call(:public, :compiled_path, :compiled_js_dir, :compiled_name)
|
328
|
+
opts[:compiled_css_path] = j.call(:public, :compiled_path, :compiled_css_dir, :compiled_name)
|
329
|
+
|
330
|
+
# Used for URLs/routes
|
331
|
+
opts[:js_prefix] = sj.call(:prefix, :js_route)
|
332
|
+
opts[:css_prefix] = sj.call(:prefix, :css_route)
|
333
|
+
opts[:compiled_js_prefix] = j.call(:prefix, :compiled_js_route, :compiled_name)
|
334
|
+
opts[:compiled_css_prefix] = j.call(:prefix, :compiled_css_route, :compiled_name)
|
335
|
+
opts[:js_suffix] = opts[:add_suffix] ? JS_SUFFIX : EMPTY_STRING
|
336
|
+
opts[:css_suffix] = opts[:add_suffix] ? CSS_SUFFIX : EMPTY_STRING
|
337
|
+
|
338
|
+
opts.freeze
|
339
|
+
end
|
340
|
+
|
341
|
+
module ClassMethods
|
342
|
+
# Return the assets options for this class.
|
343
|
+
def assets_opts
|
344
|
+
opts[:assets]
|
345
|
+
end
|
346
|
+
|
347
|
+
# Compile options for the given asset type. If no asset_type
|
348
|
+
# is given, compile both the :css and :js asset types. You
|
349
|
+
# can specify an array of types (e.g. [:css, :frontend]) to
|
350
|
+
# compile assets for the given asset group.
|
351
|
+
def compile_assets(type=nil)
|
352
|
+
require 'fileutils'
|
353
|
+
|
354
|
+
unless assets_opts[:compiled]
|
355
|
+
opts[:assets] = assets_opts.merge(:compiled => {})
|
356
|
+
end
|
357
|
+
|
358
|
+
if type == nil
|
359
|
+
_compile_assets(:css)
|
360
|
+
_compile_assets(:js)
|
361
|
+
else
|
362
|
+
_compile_assets(type)
|
363
|
+
end
|
364
|
+
|
365
|
+
if assets_opts[:precompiled]
|
366
|
+
require 'json'
|
367
|
+
::FileUtils.mkdir_p(File.dirname(assets_opts[:precompiled]))
|
368
|
+
::File.open(assets_opts[:precompiled], 'wb'){|f| f.write(assets_opts[:compiled].to_json)}
|
369
|
+
end
|
370
|
+
|
371
|
+
assets_opts[:compiled]
|
372
|
+
end
|
373
|
+
|
374
|
+
private
|
375
|
+
|
376
|
+
# Internals of compile_assets, handling recursive calls for loading
|
377
|
+
# all asset groups under the given type.
|
378
|
+
def _compile_assets(type)
|
379
|
+
type, *dirs = type if type.is_a?(Array)
|
380
|
+
dirs ||= []
|
381
|
+
files = assets_opts[type]
|
382
|
+
dirs.each{|d| files = files[d]}
|
383
|
+
|
384
|
+
case files
|
385
|
+
when Hash
|
386
|
+
files.each_key{|dir| _compile_assets([type] + dirs + [dir])}
|
387
|
+
else
|
388
|
+
files = Array(files)
|
389
|
+
compile_assets_files(files, type, dirs) unless files.empty?
|
390
|
+
end
|
391
|
+
end
|
392
|
+
|
393
|
+
# Compile each array of files for the given type into a single
|
394
|
+
# file. Dirs should be an array of asset group names, if these
|
395
|
+
# are files in an asset group.
|
396
|
+
def compile_assets_files(files, type, dirs)
|
397
|
+
dirs = nil if dirs && dirs.empty?
|
398
|
+
o = assets_opts
|
399
|
+
app = new
|
400
|
+
|
401
|
+
content = files.map do |file|
|
402
|
+
file = "#{dirs.join('/')}/#{file}" if dirs && o[:group_subdirs]
|
403
|
+
file = "#{o[:"#{type}_path"]}#{file}"
|
404
|
+
app.read_asset_file(file, type)
|
405
|
+
end.join
|
406
|
+
|
407
|
+
unless o[:concat_only]
|
408
|
+
content = compress_asset(content, type)
|
409
|
+
end
|
410
|
+
|
411
|
+
suffix = ".#{dirs.join('.')}" if dirs
|
412
|
+
key = "#{type}#{suffix}"
|
413
|
+
unique_id = o[:compiled][key] = asset_digest(content)
|
414
|
+
path = "#{o[:"compiled_#{type}_path"]}#{suffix}.#{unique_id}.#{type}"
|
415
|
+
::FileUtils.mkdir_p(File.dirname(path))
|
416
|
+
::File.open(path, 'wb'){|f| f.write(content)}
|
417
|
+
nil
|
418
|
+
end
|
419
|
+
|
420
|
+
# Compress the given content for the given type using yuicompressor,
|
421
|
+
# but handle cases where yuicompressor isn't installed or can't find
|
422
|
+
# a java runtime. This method can be overridden by the application
|
423
|
+
# to use a different compressor.
|
424
|
+
def compress_asset(content, type)
|
425
|
+
require 'yuicompressor'
|
426
|
+
# :nocov:
|
427
|
+
content = YUICompressor.send("compress_#{type}", content, :munge => true)
|
428
|
+
# :nocov:
|
429
|
+
rescue LoadError, Errno::ENOENT
|
430
|
+
# yuicompressor or java not available, just use concatenated, uncompressed output
|
431
|
+
content
|
432
|
+
end
|
433
|
+
|
434
|
+
# Return a unique id for the given content. By default, uses the
|
435
|
+
# SHA1 hash of the content. This method can be overridden to use
|
436
|
+
# a different digest type or to return a static string if you don't
|
437
|
+
# want to use a unique value.
|
438
|
+
def asset_digest(content)
|
439
|
+
require 'digest/sha1'
|
440
|
+
Digest::SHA1.hexdigest(content)
|
441
|
+
end
|
442
|
+
end
|
443
|
+
|
444
|
+
module InstanceMethods
|
445
|
+
# Return a string containing html tags for the given asset type.
|
446
|
+
# This will use a script tag for the :js type and a link tag for
|
447
|
+
# the :css type.
|
448
|
+
#
|
449
|
+
# To return the tags for a specific asset group, use an array for
|
450
|
+
# the type, such as [:css, :frontend].
|
451
|
+
#
|
452
|
+
# When the assets are not compiled, this will result in a separate
|
453
|
+
# tag for each asset file. When the assets are compiled, this will
|
454
|
+
# result in a single tag to the compiled asset file.
|
455
|
+
def assets(type, attrs = nil)
|
456
|
+
o = self.class.assets_opts
|
457
|
+
type, *dirs = type if type.is_a?(Array)
|
458
|
+
stype = type.to_s
|
459
|
+
|
460
|
+
attrs = if attrs
|
461
|
+
ru = Rack::Utils
|
462
|
+
attrs.map{|k,v| "#{k}=\"#{ru.escape_html(v.to_s)}\""}.join(SPACE)
|
463
|
+
else
|
464
|
+
EMPTY_STRING
|
465
|
+
end
|
466
|
+
|
467
|
+
if type == :js
|
468
|
+
tag_start = "<script type=\"text/javascript\" #{attrs} src=\"/"
|
469
|
+
tag_end = JS_END
|
470
|
+
else
|
471
|
+
tag_start = "<link rel=\"stylesheet\" #{attrs} href=\"/"
|
472
|
+
tag_end = CSS_END
|
473
|
+
end
|
474
|
+
|
475
|
+
# Create a tag for each individual file
|
476
|
+
if compiled = o[:compiled]
|
477
|
+
if dirs && !dirs.empty?
|
478
|
+
key = dirs.join(DOT)
|
479
|
+
ckey = "#{stype}.#{key}"
|
480
|
+
if ukey = compiled[ckey]
|
481
|
+
"#{tag_start}#{o[:"compiled_#{stype}_prefix"]}.#{key}.#{ukey}.#{stype}#{tag_end}"
|
482
|
+
end
|
483
|
+
elsif ukey = compiled[stype]
|
484
|
+
"#{tag_start}#{o[:"compiled_#{stype}_prefix"]}.#{ukey}.#{stype}#{tag_end}"
|
485
|
+
end
|
486
|
+
else
|
487
|
+
asset_dir = o[type]
|
488
|
+
if dirs && !dirs.empty?
|
489
|
+
dirs.each{|f| asset_dir = asset_dir[f]}
|
490
|
+
prefix = "#{dirs.join(SLASH)}/" if o[:group_subdirs]
|
491
|
+
end
|
492
|
+
Array(asset_dir).map{|f| "#{tag_start}#{o[:"#{stype}_prefix"]}#{prefix}#{f}#{o[:"#{stype}_suffix"]}#{tag_end}"}.join(NEWLINE)
|
493
|
+
end
|
494
|
+
end
|
495
|
+
|
496
|
+
# Render the asset with the given filename. When assets are compiled,
|
497
|
+
# or when the file is already of the given type (no rendering necessary),
|
498
|
+
# this returns the contents of the compiled file.
|
499
|
+
# When assets are not compiled and the file is not already of the correct,
|
500
|
+
# this will render the asset using the render plugin.
|
501
|
+
# In both cases, if the file has not been modified since the last request,
|
502
|
+
# this will return a 304 response.
|
503
|
+
def render_asset(file, type)
|
504
|
+
o = self.class.assets_opts
|
505
|
+
if o[:compiled]
|
506
|
+
file = "#{o[:"compiled_#{type}_path"]}#{file}"
|
507
|
+
check_asset_request(file, type, ::File.stat(file).mtime)
|
508
|
+
::File.read(file)
|
509
|
+
else
|
510
|
+
file = "#{o[:"#{type}_path"]}#{file}"
|
511
|
+
check_asset_request(file, type, asset_last_modified(file))
|
512
|
+
read_asset_file(file, type)
|
513
|
+
end
|
514
|
+
end
|
515
|
+
|
516
|
+
# Return the content of the file if it is already of the correct type.
|
517
|
+
# Otherwise, render the file using the render plugin. +file+ should be
|
518
|
+
# the relative path to the file from the current directory.
|
519
|
+
def read_asset_file(file, type)
|
520
|
+
if file.end_with?(".#{type}")
|
521
|
+
::File.read(file)
|
522
|
+
else
|
523
|
+
render_asset_file(file, self.class.assets_opts[:"#{type}_opts"])
|
524
|
+
end
|
525
|
+
end
|
526
|
+
|
527
|
+
private
|
528
|
+
|
529
|
+
# Return when the file was last modified. If the file depends on any
|
530
|
+
# other files, check the modification times of all dependencies and
|
531
|
+
# return the maximum.
|
532
|
+
def asset_last_modified(file)
|
533
|
+
if deps = self.class.assets_opts[:dependencies][file]
|
534
|
+
([file] + Array(deps)).map{|f| ::File.stat(f).mtime}.max
|
535
|
+
else
|
536
|
+
::File.stat(file).mtime
|
537
|
+
end
|
538
|
+
end
|
539
|
+
|
540
|
+
# If the asset hasn't been modified since the last request, return
|
541
|
+
# a 304 response immediately. Otherwise, add the appropriate
|
542
|
+
# type-specific headers.
|
543
|
+
def check_asset_request(file, type, mtime)
|
544
|
+
request.last_modified(mtime)
|
545
|
+
response.headers.merge!(self.class.assets_opts[:"#{type}_headers"])
|
546
|
+
end
|
547
|
+
|
548
|
+
# Render the given asset file using the render plugin, with the given options.
|
549
|
+
# +file+ should be the relative path to the file from the current directory.
|
550
|
+
def render_asset_file(file, options)
|
551
|
+
render({:path => file}, options)
|
552
|
+
end
|
553
|
+
end
|
554
|
+
|
555
|
+
module RequestClassMethods
|
556
|
+
# An array of asset type strings and regexps for that type, for all asset types
|
557
|
+
# handled.
|
558
|
+
def assets_matchers
|
559
|
+
@assets_matchers ||= [:css, :js].map do |t|
|
560
|
+
[t.to_s.freeze, assets_regexp(t)].freeze if roda_class.assets_opts[t]
|
561
|
+
end.compact.freeze
|
562
|
+
end
|
563
|
+
|
564
|
+
private
|
565
|
+
|
566
|
+
# The regexp matcher to use for the given type. This handles any asset groups
|
567
|
+
# for the asset types.
|
568
|
+
def assets_regexp(type)
|
569
|
+
o = roda_class.assets_opts
|
570
|
+
if compiled = o[:compiled]
|
571
|
+
assets = compiled.select{|k,_| k =~ /\A#{type}/}.map do |k, md|
|
572
|
+
"#{k.sub(/\A#{type}/, '')}.#{md}.#{type}"
|
573
|
+
end
|
574
|
+
/#{o[:"compiled_#{type}_prefix"]}(#{Regexp.union(assets)})/
|
575
|
+
else
|
576
|
+
assets = unnest_assets_hash(o[type])
|
577
|
+
/#{o[:"#{type}_prefix"]}(#{Regexp.union(assets.uniq)})#{o[:"#{type}_suffix"]}/
|
578
|
+
end
|
579
|
+
end
|
580
|
+
|
581
|
+
# Recursively unnested the given assets hash, returning a single array of asset
|
582
|
+
# files for the given.
|
583
|
+
def unnest_assets_hash(h)
|
584
|
+
case h
|
585
|
+
when Hash
|
586
|
+
h.map do |k,v|
|
587
|
+
assets = unnest_assets_hash(v)
|
588
|
+
assets = assets.map{|x| "#{k}/#{x}"} if roda_class.assets_opts[:group_subdirs]
|
589
|
+
assets
|
590
|
+
end.flatten(1)
|
591
|
+
else
|
592
|
+
Array(h)
|
593
|
+
end
|
594
|
+
end
|
595
|
+
end
|
596
|
+
|
597
|
+
module RequestMethods
|
598
|
+
# Render the matching asset if this is a GET request for a supported asset.
|
599
|
+
def assets
|
600
|
+
if is_get?
|
601
|
+
self.class.assets_matchers.each do |type, matcher|
|
602
|
+
is matcher do |file|
|
603
|
+
scope.render_asset(file, type)
|
604
|
+
end
|
605
|
+
end
|
606
|
+
end
|
607
|
+
end
|
608
|
+
end
|
609
|
+
end
|
610
|
+
|
611
|
+
register_plugin(:assets, Assets)
|
612
|
+
end
|
613
|
+
end
|