roda 1.0.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|