roda 3.28.0 → 3.33.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (208) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG +40 -0
  3. data/README.rdoc +15 -1
  4. data/doc/conventions.rdoc +17 -8
  5. data/doc/release_notes/3.29.0.txt +15 -0
  6. data/doc/release_notes/3.30.0.txt +14 -0
  7. data/doc/release_notes/3.31.0.txt +11 -0
  8. data/doc/release_notes/3.32.0.txt +42 -0
  9. data/doc/release_notes/3.33.0.txt +8 -0
  10. data/lib/roda.rb +3 -0
  11. data/lib/roda/plugins/_symbol_regexp_matchers.rb +2 -0
  12. data/lib/roda/plugins/assets.rb +26 -5
  13. data/lib/roda/plugins/caching.rb +2 -0
  14. data/lib/roda/plugins/common_logger.rb +1 -1
  15. data/lib/roda/plugins/content_security_policy.rb +2 -0
  16. data/lib/roda/plugins/default_headers.rb +2 -0
  17. data/lib/roda/plugins/exception_page.rb +9 -5
  18. data/lib/roda/plugins/hash_matcher.rb +1 -1
  19. data/lib/roda/plugins/header_matchers.rb +5 -1
  20. data/lib/roda/plugins/indifferent_params.rb +4 -0
  21. data/lib/roda/plugins/partials.rb +33 -6
  22. data/lib/roda/plugins/path.rb +42 -15
  23. data/lib/roda/plugins/placeholder_string_matchers.rb +2 -0
  24. data/lib/roda/plugins/public.rb +25 -17
  25. data/lib/roda/plugins/relative_path.rb +73 -0
  26. data/lib/roda/plugins/render.rb +17 -5
  27. data/lib/roda/plugins/render_each.rb +11 -3
  28. data/lib/roda/plugins/run_append_slash.rb +1 -1
  29. data/lib/roda/plugins/symbol_matchers.rb +2 -2
  30. data/lib/roda/version.rb +1 -1
  31. metadata +13 -214
  32. data/Rakefile +0 -108
  33. data/doc/release_notes/1.0.0.txt +0 -329
  34. data/doc/release_notes/1.1.0.txt +0 -226
  35. data/doc/release_notes/1.2.0.txt +0 -406
  36. data/doc/release_notes/1.3.0.txt +0 -109
  37. data/doc/release_notes/2.0.0.txt +0 -75
  38. data/doc/release_notes/2.1.0.txt +0 -124
  39. data/doc/release_notes/2.10.0.txt +0 -27
  40. data/doc/release_notes/2.11.0.txt +0 -70
  41. data/doc/release_notes/2.12.0.txt +0 -40
  42. data/doc/release_notes/2.13.0.txt +0 -10
  43. data/doc/release_notes/2.14.0.txt +0 -44
  44. data/doc/release_notes/2.15.0.txt +0 -53
  45. data/doc/release_notes/2.16.0.txt +0 -48
  46. data/doc/release_notes/2.17.0.txt +0 -62
  47. data/doc/release_notes/2.18.0.txt +0 -69
  48. data/doc/release_notes/2.19.0.txt +0 -30
  49. data/doc/release_notes/2.2.0.txt +0 -97
  50. data/doc/release_notes/2.20.0.txt +0 -5
  51. data/doc/release_notes/2.21.0.txt +0 -17
  52. data/doc/release_notes/2.22.0.txt +0 -41
  53. data/doc/release_notes/2.23.0.txt +0 -29
  54. data/doc/release_notes/2.24.0.txt +0 -65
  55. data/doc/release_notes/2.25.0.txt +0 -14
  56. data/doc/release_notes/2.26.0.txt +0 -13
  57. data/doc/release_notes/2.27.0.txt +0 -56
  58. data/doc/release_notes/2.28.0.txt +0 -17
  59. data/doc/release_notes/2.29.0.txt +0 -156
  60. data/doc/release_notes/2.3.0.txt +0 -109
  61. data/doc/release_notes/2.4.0.txt +0 -55
  62. data/doc/release_notes/2.5.0.txt +0 -23
  63. data/doc/release_notes/2.5.1.txt +0 -4
  64. data/doc/release_notes/2.6.0.txt +0 -21
  65. data/doc/release_notes/2.7.0.txt +0 -75
  66. data/doc/release_notes/2.8.0.txt +0 -44
  67. data/doc/release_notes/2.9.0.txt +0 -6
  68. data/spec/all.rb +0 -1
  69. data/spec/assets/css/app.scss +0 -1
  70. data/spec/assets/css/no_access.css +0 -1
  71. data/spec/assets/css/raw.css +0 -1
  72. data/spec/assets/js/head/app.js +0 -1
  73. data/spec/composition_spec.rb +0 -31
  74. data/spec/define_roda_method_spec.rb +0 -274
  75. data/spec/env_spec.rb +0 -11
  76. data/spec/freeze_spec.rb +0 -37
  77. data/spec/integration_spec.rb +0 -209
  78. data/spec/matchers_spec.rb +0 -832
  79. data/spec/opts_spec.rb +0 -42
  80. data/spec/plugin/_after_hook_spec.rb +0 -19
  81. data/spec/plugin/all_verbs_spec.rb +0 -29
  82. data/spec/plugin/assets_preloading_spec.rb +0 -98
  83. data/spec/plugin/assets_spec.rb +0 -745
  84. data/spec/plugin/backtracking_array_spec.rb +0 -42
  85. data/spec/plugin/branch_locals_spec.rb +0 -106
  86. data/spec/plugin/caching_spec.rb +0 -337
  87. data/spec/plugin/chunked_spec.rb +0 -201
  88. data/spec/plugin/class_level_routing_spec.rb +0 -164
  89. data/spec/plugin/class_matchers_spec.rb +0 -40
  90. data/spec/plugin/common_logger_spec.rb +0 -85
  91. data/spec/plugin/content_for_spec.rb +0 -162
  92. data/spec/plugin/content_security_policy_spec.rb +0 -175
  93. data/spec/plugin/cookies_spec.rb +0 -51
  94. data/spec/plugin/csrf_spec.rb +0 -111
  95. data/spec/plugin/default_headers_spec.rb +0 -82
  96. data/spec/plugin/default_status_spec.rb +0 -95
  97. data/spec/plugin/delay_build_spec.rb +0 -23
  98. data/spec/plugin/delegate_spec.rb +0 -23
  99. data/spec/plugin/delete_empty_headers_spec.rb +0 -27
  100. data/spec/plugin/direct_call_spec.rb +0 -28
  101. data/spec/plugin/disallow_file_uploads_spec.rb +0 -25
  102. data/spec/plugin/drop_body_spec.rb +0 -24
  103. data/spec/plugin/early_hints_spec.rb +0 -19
  104. data/spec/plugin/empty_root_spec.rb +0 -14
  105. data/spec/plugin/environments_spec.rb +0 -42
  106. data/spec/plugin/error_email_spec.rb +0 -97
  107. data/spec/plugin/error_handler_spec.rb +0 -216
  108. data/spec/plugin/error_mail_spec.rb +0 -93
  109. data/spec/plugin/exception_page_spec.rb +0 -168
  110. data/spec/plugin/flash_spec.rb +0 -121
  111. data/spec/plugin/h_spec.rb +0 -11
  112. data/spec/plugin/halt_spec.rb +0 -119
  113. data/spec/plugin/hash_matcher_spec.rb +0 -27
  114. data/spec/plugin/hash_routes_spec.rb +0 -535
  115. data/spec/plugin/head_spec.rb +0 -52
  116. data/spec/plugin/header_matchers_spec.rb +0 -98
  117. data/spec/plugin/heartbeat_spec.rb +0 -74
  118. data/spec/plugin/hooks_spec.rb +0 -152
  119. data/spec/plugin/indifferent_params_spec.rb +0 -14
  120. data/spec/plugin/json_parser_spec.rb +0 -141
  121. data/spec/plugin/json_spec.rb +0 -83
  122. data/spec/plugin/mail_processor_spec.rb +0 -451
  123. data/spec/plugin/mailer_spec.rb +0 -282
  124. data/spec/plugin/match_affix_spec.rb +0 -43
  125. data/spec/plugin/match_hook_spec.rb +0 -79
  126. data/spec/plugin/middleware_spec.rb +0 -237
  127. data/spec/plugin/middleware_stack_spec.rb +0 -81
  128. data/spec/plugin/module_include_spec.rb +0 -48
  129. data/spec/plugin/multi_route_spec.rb +0 -268
  130. data/spec/plugin/multi_run_spec.rb +0 -87
  131. data/spec/plugin/multi_view_spec.rb +0 -50
  132. data/spec/plugin/multibyte_string_matcher_spec.rb +0 -44
  133. data/spec/plugin/named_templates_spec.rb +0 -96
  134. data/spec/plugin/not_allowed_spec.rb +0 -69
  135. data/spec/plugin/not_found_spec.rb +0 -128
  136. data/spec/plugin/optimized_string_matchers_spec.rb +0 -43
  137. data/spec/plugin/padrino_render_spec.rb +0 -34
  138. data/spec/plugin/param_matchers_spec.rb +0 -69
  139. data/spec/plugin/params_capturing_spec.rb +0 -33
  140. data/spec/plugin/partials_spec.rb +0 -43
  141. data/spec/plugin/pass_spec.rb +0 -29
  142. data/spec/plugin/path_matchers_spec.rb +0 -42
  143. data/spec/plugin/path_rewriter_spec.rb +0 -45
  144. data/spec/plugin/path_spec.rb +0 -222
  145. data/spec/plugin/placeholder_string_matchers_spec.rb +0 -126
  146. data/spec/plugin/precompile_templates_spec.rb +0 -61
  147. data/spec/plugin/public_spec.rb +0 -85
  148. data/spec/plugin/render_each_spec.rb +0 -82
  149. data/spec/plugin/render_locals_spec.rb +0 -114
  150. data/spec/plugin/render_spec.rb +0 -912
  151. data/spec/plugin/request_aref_spec.rb +0 -51
  152. data/spec/plugin/request_headers_spec.rb +0 -39
  153. data/spec/plugin/response_request_spec.rb +0 -43
  154. data/spec/plugin/route_block_args_spec.rb +0 -86
  155. data/spec/plugin/route_csrf_spec.rb +0 -305
  156. data/spec/plugin/run_append_slash_spec.rb +0 -77
  157. data/spec/plugin/run_handler_spec.rb +0 -53
  158. data/spec/plugin/sessions_spec.rb +0 -452
  159. data/spec/plugin/shared_vars_spec.rb +0 -45
  160. data/spec/plugin/sinatra_helpers_spec.rb +0 -537
  161. data/spec/plugin/slash_path_empty_spec.rb +0 -22
  162. data/spec/plugin/static_routing_spec.rb +0 -192
  163. data/spec/plugin/static_spec.rb +0 -30
  164. data/spec/plugin/status_303_spec.rb +0 -28
  165. data/spec/plugin/status_handler_spec.rb +0 -158
  166. data/spec/plugin/streaming_spec.rb +0 -246
  167. data/spec/plugin/strip_path_prefix_spec.rb +0 -24
  168. data/spec/plugin/symbol_matchers_spec.rb +0 -51
  169. data/spec/plugin/symbol_status_spec.rb +0 -25
  170. data/spec/plugin/symbol_views_spec.rb +0 -32
  171. data/spec/plugin/timestamp_public_spec.rb +0 -85
  172. data/spec/plugin/type_routing_spec.rb +0 -348
  173. data/spec/plugin/typecast_params_spec.rb +0 -1370
  174. data/spec/plugin/unescape_path_spec.rb +0 -22
  175. data/spec/plugin/view_options_spec.rb +0 -170
  176. data/spec/plugin_spec.rb +0 -71
  177. data/spec/redirect_spec.rb +0 -41
  178. data/spec/request_spec.rb +0 -97
  179. data/spec/response_spec.rb +0 -199
  180. data/spec/route_spec.rb +0 -39
  181. data/spec/session_middleware_spec.rb +0 -129
  182. data/spec/session_spec.rb +0 -37
  183. data/spec/spec_helper.rb +0 -137
  184. data/spec/version_spec.rb +0 -14
  185. data/spec/views/_test.erb +0 -1
  186. data/spec/views/a.erb +0 -1
  187. data/spec/views/a.rdoc +0 -2
  188. data/spec/views/about.erb +0 -1
  189. data/spec/views/about.str +0 -1
  190. data/spec/views/about/_test.css.gz +0 -0
  191. data/spec/views/about/_test.erb +0 -1
  192. data/spec/views/about/_test.erb.gz +0 -0
  193. data/spec/views/about/comp_test.erb +0 -1
  194. data/spec/views/b.erb +0 -1
  195. data/spec/views/c.erb +0 -1
  196. data/spec/views/comp_layout.erb +0 -1
  197. data/spec/views/comp_test.erb +0 -1
  198. data/spec/views/content-yield.erb +0 -1
  199. data/spec/views/each.str +0 -1
  200. data/spec/views/home.erb +0 -2
  201. data/spec/views/home.str +0 -2
  202. data/spec/views/iv.erb +0 -1
  203. data/spec/views/layout-alternative.erb +0 -2
  204. data/spec/views/layout-yield.erb +0 -3
  205. data/spec/views/layout.erb +0 -2
  206. data/spec/views/layout.str +0 -2
  207. data/spec/views/multiple-layout.erb +0 -1
  208. data/spec/views/multiple.erb +0 -1
@@ -8,7 +8,7 @@ class Roda
8
8
  #
9
9
  # class App < Roda
10
10
  # hash_matcher(:foo) do |v|
11
- # self['foo'] == v
11
+ # params['foo'] == v
12
12
  # end
13
13
  #
14
14
  # route do
@@ -51,7 +51,11 @@ class Roda
51
51
 
52
52
  # Match if the given uppercase key is present inside the environment.
53
53
  def match_header(key)
54
- if v = @env["HTTP_#{key.upcase.tr("-","_")}"]
54
+ key = key.upcase.tr("-","_")
55
+ unless key == "CONTENT_TYPE" || key == "CONTENT_LENGTH"
56
+ key = "HTTP_#{key}"
57
+ end
58
+ if v = @env[key]
55
59
  @captures << v
56
60
  end
57
61
  end
@@ -42,6 +42,8 @@ class Roda
42
42
  INDIFFERENT_PROC = lambda{|h,k| h[k.to_s] if k.is_a?(Symbol)}
43
43
 
44
44
  if Rack.release > '2'
45
+ require 'rack/query_parser'
46
+
45
47
  class QueryParser < Rack::QueryParser
46
48
  # Work around for invalid optimization in rack
47
49
  def parse_nested_query(qs, d=nil)
@@ -62,6 +64,8 @@ class Roda
62
64
  module RequestMethods
63
65
  QUERY_PARSER = Rack::Utils.default_query_parser = QueryParser.new(QueryParser::Params, 65536, 100)
64
66
 
67
+ private
68
+
65
69
  def query_parser
66
70
  QUERY_PARSER
67
71
  end
@@ -18,27 +18,54 @@ class Roda
18
18
  # render('_test')
19
19
  # render('dir/_test')
20
20
  #
21
- # Note that this plugin automatically loads the :render plugin.
21
+ # To render the same template once for each object in an enumerable,
22
+ # you can use the +render_partials+ method:
23
+ #
24
+ # each_partial([1,2,3], :foo) # uses _foo.erb
25
+ #
26
+ # This is basically equivalent to:
27
+ #
28
+ # render_each([1,2,3], "_foo", local: :foo)
29
+ #
30
+ # This plugin depends on the render and render_each plugins.
22
31
  module Partials
23
- # Depend on the render plugin, since this overrides
24
- # some of its methods.
32
+ # Depend on the render plugin, passing received options to it.
33
+ # Also depend on the render_each plugin.
25
34
  def self.load_dependencies(app, opts=OPTS)
26
35
  app.plugin :render, opts
36
+ app.plugin :render_each
27
37
  end
28
38
 
29
39
  module InstanceMethods
40
+ # For each object in the given enumerable, render the given
41
+ # template (prefixing the template filename with an underscore).
42
+ def each_partial(enum, template, opts=OPTS)
43
+ unless opts.has_key?(:local)
44
+ opts = Hash[opts]
45
+ opts[:local] = render_each_default_local(template)
46
+ end
47
+ render_each(enum, partial_template_name(template.to_s), opts)
48
+ end
49
+
30
50
  # Renders the given template without a layout, but
31
51
  # prefixes the template filename to use with an
32
52
  # underscore.
33
53
  def partial(template, opts=OPTS)
34
54
  opts = parse_template_opts(template, opts)
35
55
  if opts[:template]
36
- template = opts[:template].split('/')
37
- template[-1] = "_#{template[-1]}"
38
- opts[:template] = template.join('/')
56
+ opts[:template] = partial_template_name(opts[:template])
39
57
  end
40
58
  render_template(opts)
41
59
  end
60
+
61
+ private
62
+
63
+ # Prefix the template base filename with an underscore.
64
+ def partial_template_name(template)
65
+ segments = template.split('/')
66
+ segments[-1] = "_#{segments[-1]}"
67
+ segments.join('/')
68
+ end
42
69
  end
43
70
  end
44
71
 
@@ -10,7 +10,8 @@ class Roda
10
10
  #
11
11
  # Additionally, you can call the +path+ class method with a class and a block, and it will register
12
12
  # the class. You can then call the +path+ instance method with an instance of that class, and it will
13
- # execute the block in the context of the route block scope with the arguments provided to path.
13
+ # execute the block in the context of the route block scope with the arguments provided to path. You
14
+ # can call the +url+ instance method with the same arguments as the +path+ method to get the full URL.
14
15
  #
15
16
  # Example:
16
17
  #
@@ -46,21 +47,23 @@ class Roda
46
47
  #
47
48
  # r.post 'quux' do
48
49
  # bar = Quux[1]
49
- # r.redirect path(quux, '/bar') # /quux/1/bar
50
+ # r.redirect url(quux, '/bar') # http://example.com/quux/1/bar
50
51
  # end
51
52
  # end
52
53
  #
53
- # The path method accepts the following options when not called with a class:
54
+ # The path class method accepts the following options when not called with a class:
54
55
  #
55
56
  # :add_script_name :: Prefix the path generated with SCRIPT_NAME. This defaults to the app's
56
57
  # :add_script_name option.
57
58
  # :name :: Provide a different name for the method, instead of using <tt>*_path</tt>.
59
+ # :relative :: Generate paths relative to the current request instead of absolute paths by prepending
60
+ # an appropriate prefix. This implies :add_script_name.
58
61
  # :url :: Create a url method in addition to the path method, which will prefix the string generated
59
62
  # with the appropriate scheme, host, and port. If true, creates a <tt>*_url</tt>
60
63
  # method. If a Symbol or String, uses the value as the url method name.
61
64
  # :url_only :: Do not create a path method, just a url method.
62
65
  #
63
- # Note that if :add_script_name, :url, or :url_only is used, the path method will also create a
66
+ # Note that if :add_script_name, :relative, :url, or :url_only is used, the path method will also create a
64
67
  # <tt>_*_path</tt> private method.
65
68
  module Path
66
69
  DEFAULT_PORTS = {'http' => 80, 'https' => 443}.freeze
@@ -121,16 +124,31 @@ class Roda
121
124
 
122
125
  meth = opts[:name] || "#{name}_path"
123
126
  url = opts[:url]
127
+ url_only = opts[:url_only]
128
+ relative = opts[:relative]
124
129
  add_script_name = opts.fetch(:add_script_name, self.opts[:add_script_name])
125
130
 
126
- if add_script_name || url || opts[:url_only]
131
+ if relative
132
+ if (url || url_only)
133
+ raise RodaError, "cannot provide :url or :url_only option if using :relative option"
134
+ end
135
+ add_script_name = true
136
+ plugin :relative_path
137
+ end
138
+
139
+ if add_script_name || url || url_only || relative
127
140
  _meth = "_#{meth}"
128
141
  define_method(_meth, &block)
129
142
  private _meth
130
143
  end
131
144
 
132
- unless opts[:url_only]
133
- if add_script_name
145
+ unless url_only
146
+ if relative
147
+ define_method(meth) do |*a, &blk|
148
+ # Allow calling private _method to get path
149
+ relative_path(request.script_name.to_s + send(_meth, *a, &blk))
150
+ end
151
+ elsif add_script_name
134
152
  define_method(meth) do |*a, &blk|
135
153
  # Allow calling private _method to get path
136
154
  request.script_name.to_s + send(_meth, *a, &blk)
@@ -140,7 +158,7 @@ class Roda
140
158
  end
141
159
  end
142
160
 
143
- if url || opts[:url_only]
161
+ if url || url_only
144
162
  url_meth = if url.is_a?(String) || url.is_a?(Symbol)
145
163
  url
146
164
  else
@@ -148,14 +166,8 @@ class Roda
148
166
  end
149
167
 
150
168
  url_block = lambda do |*a, &blk|
151
- r = request
152
- scheme = r.scheme
153
- port = r.port
154
- uri = ["#{scheme}://#{r.host}#{":#{port}" unless DEFAULT_PORTS[scheme] == port}"]
155
- uri << request.script_name.to_s if add_script_name
156
169
  # Allow calling private _method to get path
157
- uri << send(_meth, *a, &blk)
158
- File.join(uri)
170
+ "#{_base_url}#{request.script_name if add_script_name}#{send(_meth, *a, &blk)}"
159
171
  end
160
172
 
161
173
  define_method(url_meth, &url_block)
@@ -190,6 +202,21 @@ class Roda
190
202
  path = request.script_name.to_s + path if opts[:add_script_name]
191
203
  path
192
204
  end
205
+
206
+ # Similar to #path, but returns a complete URL.
207
+ def url(*args, &block)
208
+ "#{_base_url}#{path(*args, &block)}"
209
+ end
210
+
211
+ private
212
+
213
+ # The string to prepend to the path to make the path a URL.
214
+ def _base_url
215
+ r = @_request
216
+ scheme = r.scheme
217
+ port = r.port
218
+ "#{scheme}://#{r.host}#{":#{port}" unless DEFAULT_PORTS[scheme] == port}"
219
+ end
193
220
  end
194
221
  end
195
222
 
@@ -27,6 +27,8 @@ class Roda
27
27
  end
28
28
 
29
29
  module RequestMethods
30
+ private
31
+
30
32
  def _match_string(str)
31
33
  if str.index(":")
32
34
  consume(self.class.cached_matcher(str){Regexp.escape(str).gsub(/:(\w+)/){|m| _match_symbol_regexp($1)}})
@@ -42,6 +42,8 @@ class Roda
42
42
  # :default_mime :: The default mime type to use if the mime type is not recognized.
43
43
  # :gzip :: Whether to serve already gzipped files with a .gz extension for clients
44
44
  # supporting gzipped transfer encoding.
45
+ # :brotli :: Whether to serve already brotli-compressed files with a .br extension
46
+ # for clients supporting brotli transfer encoding.
45
47
  # :headers :: A hash of headers to use for statically served files
46
48
  # :root :: Use this option for the root of the public directory (default: "public")
47
49
  def self.configure(app, opts={})
@@ -52,6 +54,7 @@ class Roda
52
54
  end
53
55
  app.opts[:public_server] = ::Rack::File.new(app.opts[:public_root], opts[:headers]||{}, opts[:default_mime] || 'text/plain')
54
56
  app.opts[:public_gzip] = opts[:gzip]
57
+ app.opts[:public_brotli] = opts[:brotli]
55
58
  end
56
59
 
57
60
  module RequestMethods
@@ -65,23 +68,8 @@ class Roda
65
68
  server = roda_opts[:public_server]
66
69
  path = ::File.join(server.root, *public_path_segments(path))
67
70
 
68
- if roda_opts[:public_gzip] && env['HTTP_ACCEPT_ENCODING'] =~ /\bgzip\b/
69
- gzip_path = path + '.gz'
70
-
71
- if public_file_readable?(gzip_path)
72
- res = public_serve(server, gzip_path)
73
- headers = res[1]
74
-
75
- unless res[0] == 304
76
- if mime_type = ::Rack::Mime.mime_type(::File.extname(path), 'text/plain')
77
- headers['Content-Type'] = mime_type
78
- end
79
- headers['Content-Encoding'] = 'gzip'
80
- end
81
-
82
- halt res
83
- end
84
- end
71
+ public_serve_compressed(server, path, '.br', 'br') if roda_opts[:public_brotli]
72
+ public_serve_compressed(server, path, '.gz', 'gzip') if roda_opts[:public_gzip]
85
73
 
86
74
  if public_file_readable?(path)
87
75
  halt public_serve(server, path)
@@ -113,6 +101,26 @@ class Roda
113
101
  # :nocov:
114
102
  end
115
103
 
104
+ def public_serve_compressed(server, path, suffix, encoding)
105
+ if env['HTTP_ACCEPT_ENCODING'] =~ /\b#{encoding}\b/
106
+ compressed_path = path + suffix
107
+
108
+ if public_file_readable?(compressed_path)
109
+ res = public_serve(server, compressed_path)
110
+ headers = res[1]
111
+
112
+ unless res[0] == 304
113
+ if mime_type = ::Rack::Mime.mime_type(::File.extname(path), 'text/plain')
114
+ headers['Content-Type'] = mime_type
115
+ end
116
+ headers['Content-Encoding'] = encoding
117
+ end
118
+
119
+ halt res
120
+ end
121
+ end
122
+ end
123
+
116
124
  if ::Rack.release > '2'
117
125
  # Serve the given path using the given Rack::File server.
118
126
  def public_serve(server, path)
@@ -0,0 +1,73 @@
1
+ # frozen-string-literal: true
2
+
3
+ #
4
+ class Roda
5
+ module RodaPlugins
6
+ # The relative_path plugin adds a relative_path method that accepts
7
+ # an absolute path and returns a path relative to the current request
8
+ # by adding an appropriate prefix:
9
+ #
10
+ # plugin :relative_path
11
+ # route do |r|
12
+ # relative_path("/foo")
13
+ # end
14
+ #
15
+ # # GET /
16
+ # "./foo"
17
+ #
18
+ # # GET /bar
19
+ # "./foo"
20
+ #
21
+ # # GET /bar/
22
+ # "../foo"
23
+ #
24
+ # # GET /bar/baz/quux
25
+ # "../../foo"
26
+ #
27
+ # It also offers a relative_prefix method that returns a string that can
28
+ # be prepended to an absolute path. This can be more efficient if you
29
+ # need to convert multiple paths.
30
+ #
31
+ # This plugin is mostly designed for applications using Roda as a static
32
+ # site generator, where the generated site can be hosted at any subpath.
33
+ module RelativePath
34
+ module InstanceMethods
35
+ # Return a relative path for the absolute path based on the current path
36
+ # of the request by adding the appropriate prefix.
37
+ def relative_path(absolute_path)
38
+ relative_prefix + absolute_path
39
+ end
40
+
41
+ # Return a relative prefix to append to an absolute path to a relative path
42
+ # based on the current path of the request.
43
+ def relative_prefix
44
+ env = @_request.env
45
+ script_name = env["SCRIPT_NAME"]
46
+ path_info = env["PATH_INFO"]
47
+
48
+ # Check path begins with slash. All valid paths should, but in case this
49
+ # request is bad, just skip using a relative prefix.
50
+ case script_name.getbyte(0)
51
+ when nil # SCRIPT_NAME empty
52
+ unless path_info.getbyte(0) == 47 # PATH_INFO starts with /
53
+ return ''
54
+ end
55
+ when 47 # SCRIPT_NAME starts with /
56
+ # nothing
57
+ else
58
+ return ''
59
+ end
60
+
61
+ slash_count = script_name.count('/') + path_info.count('/')
62
+ if slash_count > 1
63
+ ("../" * (slash_count - 2)) << ".."
64
+ else
65
+ "."
66
+ end
67
+ end
68
+ end
69
+ end
70
+
71
+ register_plugin(:relative_path, RelativePath)
72
+ end
73
+ end
@@ -241,12 +241,13 @@ class Roda
241
241
  # template file has been modified. This is an internal class and
242
242
  # the API is subject to change at any time.
243
243
  class TemplateMtimeWrapper
244
- def initialize(template_class, path, *template_args)
244
+ def initialize(template_class, path, dependencies, *template_args)
245
245
  @template_class = template_class
246
246
  @path = path
247
247
  @template_args = template_args
248
+ @dependencies = ([path] + Array(dependencies)) if dependencies
248
249
 
249
- @mtime = (File.mtime(path) if File.file?(path))
250
+ @mtime = template_last_modified if File.file?(path)
250
251
  @template = template_class.new(path, *template_args)
251
252
  end
252
253
 
@@ -257,17 +258,28 @@ class Roda
257
258
  @template.render(*args, &block)
258
259
  end
259
260
 
261
+ # Return when the template was last modified. If the template depends on any
262
+ # other files, check the modification times of all dependencies and
263
+ # return the maximum.
264
+ def template_last_modified
265
+ if deps = @dependencies
266
+ deps.map{|f| File.mtime(f)}.max
267
+ else
268
+ File.mtime(@path)
269
+ end
270
+ end
271
+
260
272
  # If the template file has been updated, return true and update
261
273
  # the template object and the modification time. Other return false.
262
274
  def modified?
263
275
  begin
264
- mtime = File.mtime(path = @path)
276
+ mtime = template_last_modified
265
277
  rescue
266
278
  # ignore errors
267
279
  else
268
280
  if mtime != @mtime
269
281
  @mtime = mtime
270
- @template = @template_class.new(path, *@template_args)
282
+ @template = @template_class.new(@path, *@template_args)
271
283
  return true
272
284
  end
273
285
  end
@@ -576,7 +588,7 @@ class Roda
576
588
  !opts[:inline]
577
589
 
578
590
  if render_opts[:check_template_mtime] && !opts[:template_block] && !cache
579
- template = TemplateMtimeWrapper.new(opts[:template_class], opts[:path], 1, template_opts)
591
+ template = TemplateMtimeWrapper.new(opts[:template_class], opts[:path], opts[:dependencies], 1, template_opts)
580
592
 
581
593
  if define_compiled_method
582
594
  method_name = :"_roda_template_#{self.class.object_id}_#{method_cache_key}"
@@ -26,7 +26,9 @@ class Roda
26
26
  #
27
27
  # Will render the +foo+ template, but the local variable used inside
28
28
  # the template will be +bar+. You can use <tt>local: nil</tt> to
29
- # not set a local variable inside the template.
29
+ # not set a local variable inside the template. By default, the
30
+ # local variable name is based on the template name, with any
31
+ # directories and file extensions removed.
30
32
  module RenderEach
31
33
  # Load the render plugin before this plugin, since this plugin
32
34
  # calls the render method.
@@ -45,11 +47,11 @@ class Roda
45
47
  # set a local variable. If not set, uses the template name.
46
48
  def render_each(enum, template, opts=(no_opts = true; optimized_template = _cached_render_each_template_method(template); OPTS))
47
49
  if optimized_template
48
- return _optimized_render_each(enum, optimized_template, template.to_s.to_sym, {})
50
+ return _optimized_render_each(enum, optimized_template, render_each_default_local(template), {})
49
51
  elsif opts.has_key?(:local)
50
52
  as = opts[:local]
51
53
  else
52
- as = template.to_s.to_sym
54
+ as = render_each_default_local(template)
53
55
  if no_opts && optimized_template.nil? && (optimized_template = _optimized_render_method_for_locals(template, (locals = {as=>nil})))
54
56
  return _optimized_render_each(enum, optimized_template, as, locals)
55
57
  end
@@ -77,6 +79,12 @@ class Roda
77
79
 
78
80
  private
79
81
 
82
+ # The default local variable name to use for the template, if the :local option
83
+ # is not used when calling render_each.
84
+ def render_each_default_local(template)
85
+ File.basename(template.to_s).sub(/\..+\z/, '').to_sym
86
+ end
87
+
80
88
  if Render::COMPILED_METHOD_SUPPORT
81
89
  # If compiled method support is enabled in the render plugin, return the
82
90
  # method name to call to render the template. Return false if not given