roda 2.28.0 → 2.29.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (66) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG +46 -0
  3. data/README.rdoc +25 -7
  4. data/doc/release_notes/2.29.0.txt +156 -0
  5. data/lib/roda.rb +25 -3
  6. data/lib/roda/plugins/_erubis_escaping.rb +2 -0
  7. data/lib/roda/plugins/_symbol_regexp_matchers.rb +22 -0
  8. data/lib/roda/plugins/assets.rb +3 -2
  9. data/lib/roda/plugins/branch_locals.rb +74 -0
  10. data/lib/roda/plugins/caching.rb +15 -7
  11. data/lib/roda/plugins/chunked.rb +10 -7
  12. data/lib/roda/plugins/content_for.rb +4 -1
  13. data/lib/roda/plugins/drop_body.rb +3 -2
  14. data/lib/roda/plugins/error_email.rb +3 -2
  15. data/lib/roda/plugins/error_mail.rb +3 -2
  16. data/lib/roda/plugins/head.rb +2 -1
  17. data/lib/roda/plugins/header_matchers.rb +3 -0
  18. data/lib/roda/plugins/heartbeat.rb +3 -2
  19. data/lib/roda/plugins/json.rb +5 -3
  20. data/lib/roda/plugins/json_parser.rb +3 -2
  21. data/lib/roda/plugins/mailer.rb +3 -3
  22. data/lib/roda/plugins/match_affix.rb +6 -0
  23. data/lib/roda/plugins/multi_route.rb +3 -1
  24. data/lib/roda/plugins/padrino_render.rb +3 -2
  25. data/lib/roda/plugins/params_capturing.rb +3 -3
  26. data/lib/roda/plugins/partials.rb +3 -3
  27. data/lib/roda/plugins/path.rb +4 -2
  28. data/lib/roda/plugins/path_rewriter.rb +2 -2
  29. data/lib/roda/plugins/per_thread_caching.rb +2 -0
  30. data/lib/roda/plugins/placeholder_string_matchers.rb +42 -0
  31. data/lib/roda/plugins/precompile_templates.rb +3 -2
  32. data/lib/roda/plugins/render.rb +86 -37
  33. data/lib/roda/plugins/render_each.rb +2 -1
  34. data/lib/roda/plugins/render_locals.rb +102 -0
  35. data/lib/roda/plugins/run_append_slash.rb +2 -1
  36. data/lib/roda/plugins/run_handler.rb +2 -1
  37. data/lib/roda/plugins/sinatra_helpers.rb +4 -4
  38. data/lib/roda/plugins/static_path_info.rb +2 -0
  39. data/lib/roda/plugins/static_routing.rb +1 -1
  40. data/lib/roda/plugins/streaming.rb +9 -4
  41. data/lib/roda/plugins/symbol_matchers.rb +23 -20
  42. data/lib/roda/plugins/view_options.rb +63 -28
  43. data/lib/roda/plugins/view_subdirs.rb +1 -0
  44. data/lib/roda/plugins/websockets.rb +2 -0
  45. data/lib/roda/version.rb +1 -1
  46. data/spec/composition_spec.rb +2 -2
  47. data/spec/matchers_spec.rb +6 -5
  48. data/spec/plugin/_erubis_escaping_spec.rb +5 -5
  49. data/spec/plugin/backtracking_array_spec.rb +0 -2
  50. data/spec/plugin/branch_locals_spec.rb +88 -0
  51. data/spec/plugin/content_for_spec.rb +8 -2
  52. data/spec/plugin/halt_spec.rb +8 -0
  53. data/spec/plugin/header_matchers_spec.rb +20 -5
  54. data/spec/plugin/multi_route_spec.rb +1 -1
  55. data/spec/plugin/named_templates_spec.rb +2 -2
  56. data/spec/plugin/params_capturing_spec.rb +1 -1
  57. data/spec/plugin/per_thread_caching_spec.rb +1 -1
  58. data/spec/plugin/placeholder_string_matchers_spec.rb +159 -0
  59. data/spec/plugin/render_locals_spec.rb +114 -0
  60. data/spec/plugin/render_spec.rb +83 -8
  61. data/spec/plugin/streaming_spec.rb +104 -4
  62. data/spec/plugin/symbol_matchers_spec.rb +1 -1
  63. data/spec/plugin/view_options_spec.rb +83 -7
  64. data/spec/plugin/websockets_spec.rb +7 -8
  65. data/spec/spec_helper.rb +22 -2
  66. metadata +11 -2
@@ -46,7 +46,7 @@ class Roda
46
46
  # processed before remaining_path rewrites.
47
47
  module PathRewriter
48
48
  OPTS={}.freeze
49
-
49
+ RodaPlugins.deprecate_constant(self, :OPTS)
50
50
  PATH_INFO = 'PATH_INFO'.freeze
51
51
  RodaPlugins.deprecate_constant(self, :PATH_INFO)
52
52
 
@@ -67,7 +67,7 @@ class Roda
67
67
 
68
68
  # Record a path rewrite from path +was+ to path +is+. Options:
69
69
  # :path_info :: Modify PATH_INFO, not just remaining path.
70
- def rewrite_path(was, is = nil, opts=OPTS, &block)
70
+ def rewrite_path(was, is = nil, opts=RodaPlugins::OPTS, &block)
71
71
  if is.is_a? Hash
72
72
  raise RodaError, "cannot provide two hashes to rewrite_path" unless opts.empty?
73
73
  opts = is
@@ -3,6 +3,8 @@
3
3
  #
4
4
  class Roda
5
5
  module RodaPlugins
6
+ warn "The per_thread_caching plugin is deprecated and will be removed in Roda 3. Consider maintaining the plugin as a separate gem if you would like to keep using it."
7
+
6
8
  # The per_thread_caching plugin changes the default cache
7
9
  # from being a shared thread safe cache to a separate cache per
8
10
  # thread. This means getting or setting values no longer
@@ -0,0 +1,42 @@
1
+ # frozen-string-literal: true
2
+
3
+ #
4
+ class Roda
5
+ module RodaPlugins
6
+ # The placeholder_string_matcher plugin exists for backwards compatibility
7
+ # with previous versions of Roda that allowed placeholders inside strings
8
+ # if they were prefixed by colons:
9
+ #
10
+ # plugin :placeholder_string_matchers
11
+ #
12
+ # route do |r|
13
+ # r.is("foo/:bar") |v|
14
+ # # matches foo/baz, yielding "baz"
15
+ # # does not match foo, foo/, or foo/baz/
16
+ # end
17
+ # end
18
+ #
19
+ # It is not recommended to use this in new applications, and it is encouraged
20
+ # to use separate symbol or string class matchers instead:
21
+ #
22
+ # r.is "foo", String
23
+ # r.is "foo", :bar
24
+ module PlaceholderStringMatchers
25
+ def self.load_dependencies(app)
26
+ app.plugin :_symbol_regexp_matchers
27
+ end
28
+
29
+ module RequestMethods
30
+ def _match_string(str)
31
+ if str.index(":")
32
+ consume(self.class.cached_matcher(str){Regexp.escape(str).gsub(/:(\w+)/){|m| _match_symbol_regexp($1)}})
33
+ else
34
+ super
35
+ end
36
+ end
37
+ end
38
+ end
39
+
40
+ register_plugin(:placeholder_string_matchers, PlaceholderStringMatchers)
41
+ end
42
+ end
@@ -47,10 +47,11 @@ class Roda
47
47
  # precompile_templates :inline=>some_template_string
48
48
  module PrecompileTemplates
49
49
  OPTS = {}.freeze
50
+ RodaPlugins.deprecate_constant(self, :OPTS)
50
51
 
51
52
  # Load the render plugin as precompile_templates depends on it.
52
53
  # Default to sorting the locals if the Tilt version is greater than 2.0.1.
53
- def self.load_dependencies(app, opts=OPTS)
54
+ def self.load_dependencies(app, opts=RodaPlugins::OPTS)
54
55
  app.plugin :render
55
56
  app.opts[:precompile_templates_sort] = opts.fetch(:sort_locals, Tilt::VERSION > '2.0.1')
56
57
  end
@@ -58,7 +59,7 @@ class Roda
58
59
  module ClassMethods
59
60
  # Precompile the templates using the given options. See PrecompileTemplates
60
61
  # for details.
61
- def precompile_templates(pattern, opts=OPTS)
62
+ def precompile_templates(pattern, opts=RodaPlugins::OPTS)
62
63
  if pattern.is_a?(Hash)
63
64
  opts = pattern.merge(opts)
64
65
  end
@@ -57,11 +57,6 @@ class Roda
57
57
  # :escape :: Use Roda's Erubis escaping support, which makes <tt><%= %></tt> escape output,
58
58
  # <tt><%== %></tt> not escape output, and handles postfix conditions inside
59
59
  # <tt><%= %></tt> tags. Can have a value of :erubi to use Erubi escaping support.
60
- # :escape_safe_classes :: String subclasses that should not be HTML escaped when used in
61
- # <tt><%= %></tt> tags, when :escape=>true is used. Can be an array for multiple classes.
62
- # :escaper :: Object used for escaping output of <tt><%= %></tt>, when :escape=>true is used,
63
- # overriding the default. If given, object should respond to +escape_xml+ with
64
- # a single argument and return an output string.
65
60
  # :explicit_cache :: Only use the template cache if the :cache option is provided when rendering
66
61
  # (useful for development). Defaults to true if RACK_ENV is development, allowing explicit
67
62
  # caching of specific templates, but not caching by default.
@@ -69,10 +64,7 @@ class Roda
69
64
  # starts subclasses with an empty cache.
70
65
  # :layout :: The base name of the layout file, defaults to 'layout'. This can be provided as a hash
71
66
  # with the :template or :inline options.
72
- # :layout_opts :: The options to use when rendering the layout, if different
73
- # from the default options. To pass local variables to the layout, include a :locals
74
- # option inside :layout_opts. To automatically merge the view template locals into
75
- # the layout template locals, include a :merge_locals option inside :layout_opts.
67
+ # :layout_opts :: The options to use when rendering the layout, if different from the default options.
76
68
  # :template_opts :: The tilt options used when rendering all templates. defaults to:
77
69
  # <tt>{:outvar=>'@_out_buf', :default_encoding=>Encoding.default_external}</tt>.
78
70
  # :engine_opts :: The tilt options to use per template engine. Keys are
@@ -147,15 +139,17 @@ class Roda
147
139
  # plugin option.
148
140
  module Render
149
141
  OPTS={}.freeze
142
+ RodaPlugins.deprecate_constant(self, :OPTS)
150
143
 
151
- def self.load_dependencies(app, opts=OPTS)
144
+ # RODA3: Remove
145
+ def self.load_dependencies(app, opts=RodaPlugins::OPTS)
152
146
  if opts[:escape] && opts[:escape] != :erubi
153
147
  app.plugin :_erubis_escaping
154
148
  end
155
149
  end
156
150
 
157
151
  # Setup default rendering options. See Render for details.
158
- def self.configure(app, opts=OPTS)
152
+ def self.configure(app, opts=RodaPlugins::OPTS)
159
153
  if app.opts[:render]
160
154
  orig_cache = app.opts[:render][:cache]
161
155
  opts = app.opts[:render][:orig_opts].merge(opts)
@@ -164,6 +158,9 @@ class Roda
164
158
  app.opts[:render][:orig_opts] = opts
165
159
 
166
160
  opts = app.opts[:render]
161
+ if opts[:ext] && !opts[:engine]
162
+ RodaPlugins.warn "The :ext render plugin option is deprecated and will be removed in Roda 3. Switch to using the :engine option."
163
+ end
167
164
  opts[:engine] = (opts[:engine] || opts[:ext] || "erb").dup.freeze
168
165
  opts[:views] = app.expand_path(opts[:views]||"views").freeze
169
166
  opts[:allowed_paths] ||= [opts[:views]].freeze
@@ -182,8 +179,25 @@ class Roda
182
179
  opts[:explicit_cache] = ENV['RACK_ENV'] == 'development' unless opts.has_key?(:explicit_cache)
183
180
 
184
181
  opts[:layout_opts] = (opts[:layout_opts] || {}).dup
182
+ if opts[:layout_opts][:views]
183
+ opts[:layout_opts][:views] = app.expand_path(opts[:layout_opts][:views]).freeze
184
+ end
185
+ # RODA3: Remove
185
186
  opts[:layout_opts][:_is_layout] = true
186
187
 
188
+ if opts[:locals]
189
+ RodaPlugins.warn "The :locals render plugin option is deprecated and will be removed in Roda 3. Locals should now be specified on a per-call basis, or you can use the render_locals plugin."
190
+ end
191
+
192
+ if opts[:layout_opts][:locals]
193
+ RodaPlugins.warn "The :layout_opts=>:locals render plugin option is deprecated and will be removed in Roda 3. Locals should now be specified on a per-call basis, or you can use the render_locals plugin."
194
+ end
195
+
196
+ if opts[:layout_opts][:merge_locals]
197
+ RodaPlugins.warn "The :layout_opts=>:merge_locals render plugin option is deprecated and will be removed in Roda 3. You can use the render_locals plugin for merging locals."
198
+ end
199
+
200
+ # RODA3: Remove
187
201
  if opts[:layout_opts][:merge_locals] && opts[:locals]
188
202
  opts[:layout_opts][:locals] = opts[:locals].merge(opts[:layout_opts][:locals] || {})
189
203
  end
@@ -207,6 +221,7 @@ class Roda
207
221
  if RUBY_VERSION >= "1.9" && !template_opts.has_key?(:default_encoding)
208
222
  template_opts[:default_encoding] = Encoding.default_external
209
223
  end
224
+ # RODA3: Make :escape assume erubi, remove erubis support
210
225
  if opts[:escape] == :erubi
211
226
  require 'tilt/erubi'
212
227
  template_opts[:escape] = true
@@ -259,10 +274,9 @@ class Roda
259
274
 
260
275
  module InstanceMethods
261
276
  # Render the given template. See Render for details.
262
- def render(template, opts = OPTS, &block)
263
- opts = parse_template_opts(template, opts)
264
- merge_render_locals(opts)
265
- retrieve_template(opts).render((opts[:scope]||self), (opts[:locals]||OPTS), &block)
277
+ def render(template, opts = RodaPlugins::OPTS, &block)
278
+ opts = render_template_opts(template, opts)
279
+ retrieve_template(opts).render((opts[:scope]||self), (opts[:locals]||RodaPlugins::OPTS), &block)
266
280
  end
267
281
 
268
282
  # Return the render options for the instance's class. While this
@@ -275,7 +289,7 @@ class Roda
275
289
  # Render the given template. If there is a default layout
276
290
  # for the class, take the result of the template rendering
277
291
  # and render it inside the layout. See Render for details.
278
- def view(template, opts=OPTS)
292
+ def view(template, opts=RodaPlugins::OPTS)
279
293
  opts = parse_template_opts(template, opts)
280
294
  content = opts[:content] || render_template(opts)
281
295
 
@@ -288,6 +302,16 @@ class Roda
288
302
 
289
303
  private
290
304
 
305
+ # Convert template options to single hash when rendering templates using render.
306
+ def render_template_opts(template, opts)
307
+ opts = parse_template_opts(template, opts)
308
+
309
+ # RODA3: Remove
310
+ merge_render_locals(opts) if render_plugin_handle_locals?
311
+
312
+ opts
313
+ end
314
+
291
315
  # Private alias for render. Should be used by other plugins when they want to render a template
292
316
  # without a layout, as plugins can override render to use a layout.
293
317
  alias render_template render
@@ -309,6 +333,9 @@ class Roda
309
333
  # template block, and locals to use for the render in the passed options.
310
334
  def find_template(opts)
311
335
  render_opts = render_opts()
336
+ if opts[:ext] && !opts[:engine]
337
+ RodaPlugins.warn "The :ext render plugin option is deprecated and will be removed in Roda 3. Switch to using the :engine option."
338
+ end
312
339
  engine_override = opts[:engine] ||= opts[:ext]
313
340
  engine = opts[:engine] ||= render_opts[:engine]
314
341
  if content = opts[:inline]
@@ -339,12 +366,14 @@ class Roda
339
366
  else
340
367
  opts.delete(:cache_key)
341
368
  end
369
+ elsif opts[:cache]
370
+ RodaPlugins.warn ":cache render/view method option used when caching explicitly disabled via :cache=>nil/false plugin option. Caching this template will be skipped for backwards compatibility. Starting in Roda 3, the :cache render/view method option will force caching even if the plugin defaults to not caching."
342
371
  end
343
372
 
344
373
  opts
345
374
  end
346
375
 
347
- # Merge any :locals specified in the render_opts into the :locals option given.
376
+ # RODA3: Remove
348
377
  def merge_render_locals(opts)
349
378
  if !opts[:_is_layout] && (r_locals = render_opts[:locals])
350
379
  opts[:locals] = if locals = opts[:locals]
@@ -398,45 +427,65 @@ class Roda
398
427
  # The template path for the given options.
399
428
  def template_path(opts)
400
429
  path = "#{opts[:views]}/#{template_name(opts)}.#{opts[:engine]}"
430
+ full_path = self.class.expand_path(path)
401
431
  if opts.fetch(:check_paths){render_opts[:check_paths]}
402
- full_path = self.class.expand_path(path)
403
432
  unless render_opts[:allowed_paths].any?{|f| full_path.start_with?(f)}
404
- raise RodaError, "attempt to render path not in allowed_paths: #{path} (allowed: #{render_opts[:allowed_paths].join(', ')})"
433
+ raise RodaError, "attempt to render path not in allowed_paths: #{full_path} (allowed: #{render_opts[:allowed_paths].join(', ')})"
434
+ end
435
+ elsif !opts.has_key?(:check_paths) && !render_opts.has_key?(:check_paths)
436
+ unless render_opts[:allowed_paths].any?{|f| full_path.start_with?(f)}
437
+ RodaPlugins.warn "The :check_paths render/view method option and :check_paths render plugin option were not specified, and the path used for the template (#{full_path.inspect}) is not in the allowed paths (#{render_opts[:allowed_paths].inspect}). Allowing the template render anyway for backwards compatibility, but an error will be raised starting in Roda 3. Specify the :allowed_paths render plugin option to include the path, or use the :check_paths=>false render plugin option to explicitly disable path checking."
405
438
  end
406
439
  end
407
440
  path
408
441
  end
409
442
 
443
+ # RODA3: Remove
444
+ def render_plugin_handle_locals?
445
+ true
446
+ end
447
+
410
448
  # If a layout should be used, return a hash of options for
411
449
  # rendering the layout template. If a layout should not be
412
450
  # used, return nil.
413
451
  def view_layout_opts(opts)
414
452
  if layout = opts.fetch(:layout, render_opts[:layout])
415
-
416
453
  layout_opts = render_layout_opts
454
+
455
+ # RODA3: Remove
417
456
  merge_locals = layout_opts[:merge_locals]
418
457
 
419
- if method_layout_opts = opts[:layout_opts]
420
- method_layout_locals = method_layout_opts[:locals]
421
- merge_locals = method_layout_opts[:merge_locals] if method_layout_opts.has_key?(:merge_locals)
422
- end
458
+ method_layout_opts = opts[:layout_opts]
423
459
 
424
- locals = {}
425
- if merge_locals && (plugin_locals = render_opts[:locals])
426
- locals.merge!(plugin_locals)
427
- end
428
- if layout_locals = layout_opts[:locals]
429
- locals.merge!(layout_locals)
430
- end
431
- if merge_locals && (method_locals = opts[:locals])
432
- locals.merge!(method_locals)
433
- end
434
- if method_layout_locals
435
- locals.merge!(method_layout_locals)
460
+ # RODA3: Remove
461
+ if render_plugin_handle_locals?
462
+ if method_layout_opts
463
+ method_layout_locals = method_layout_opts[:locals]
464
+ if method_layout_opts.has_key?(:merge_locals)
465
+ RodaPlugins.warn "The :layout_opts=>:merge_locals view option is deprecated and will be removed in Roda 3. You can use the render_locals plugin for merging locals."
466
+ merge_locals = method_layout_opts[:merge_locals]
467
+ end
468
+ end
469
+
470
+ locals = {}
471
+ if merge_locals && (plugin_locals = render_opts[:locals])
472
+ locals.merge!(plugin_locals)
473
+ end
474
+ if layout_locals = layout_opts[:locals]
475
+ locals.merge!(layout_locals)
476
+ end
477
+ if merge_locals && (method_locals = opts[:locals])
478
+ locals.merge!(method_locals)
479
+ end
480
+ if method_layout_locals
481
+ locals.merge!(method_layout_locals)
482
+ end
436
483
  end
437
484
 
438
485
  layout_opts.merge!(method_layout_opts) if method_layout_opts
439
- layout_opts[:locals] = locals unless locals.empty?
486
+
487
+ # RODA3: Remove
488
+ layout_opts[:locals] = locals if render_plugin_handle_locals? && !locals.empty?
440
489
 
441
490
  case layout
442
491
  when Hash
@@ -27,6 +27,7 @@ class Roda
27
27
  # not set a local variable inside the template.
28
28
  module RenderEach
29
29
  OPTS = {}.freeze
30
+ RodaPlugins.deprecate_constant(self, :OPTS)
30
31
 
31
32
  # Load the render plugin before this plugin, since this plugin
32
33
  # calls the render method.
@@ -41,7 +42,7 @@ class Roda
41
42
  # :local :: The local variable to use for the current enum value
42
43
  # inside the template. An explicit +nil+ value does not
43
44
  # set a local variable. If not set, uses the template name.
44
- def render_each(enum, template, opts=OPTS)
45
+ def render_each(enum, template, opts=RodaPlugins::OPTS)
45
46
  if as = opts.has_key?(:local)
46
47
  as = opts[:local]
47
48
  else
@@ -0,0 +1,102 @@
1
+ # frozen-string-literal: true
2
+
3
+ #
4
+ class Roda
5
+ module RodaPlugins
6
+ # The render_locals plugin allows setting default locals for rendering templates.
7
+ #
8
+ # plugin :render_locals, :render=>{:heading=>'Hello'}
9
+ #
10
+ # route do |r|
11
+ # r.get "foo" do
12
+ # view 'foo', :locals=>{:name=>'Foo'} # locals: {:heading=>'Hello', :name=>'Foo'}
13
+ # end
14
+ #
15
+ # r.get "bar" do
16
+ # view 'foo', :locals=>{:heading=>'Bar'} # locals: {:heading=>'Bar'}
17
+ # end
18
+ #
19
+ # view "default" # locals: {:heading=>'Hello'}
20
+ # end
21
+ #
22
+ # The render_locals plugin accepts the following options:
23
+ #
24
+ # render :: The default locals to use for template rendering
25
+ # layout :: The default locals to use for layout rendering
26
+ # merge :: Whether to merge template locals into layout locals
27
+ module RenderLocals
28
+ OPTS = {}.freeze
29
+ RodaPlugins.deprecate_constant(self, :OPTS)
30
+
31
+ def self.load_dependencies(app, opts=RodaPlugins::OPTS)
32
+ app.plugin :render
33
+ end
34
+
35
+ def self.configure(app, opts=RodaPlugins::OPTS)
36
+ app.opts[:render_locals] = (app.opts[:render_locals] || {}).merge(opts[:render]||{}).freeze
37
+ app.opts[:layout_locals] = (app.opts[:layout_locals] || {}).merge(opts[:layout]||{}).freeze
38
+ if opts.has_key?(:merge)
39
+ app.opts[:merge_locals] = opts[:merge]
40
+ app.opts[:layout_locals] = app.opts[:render_locals].merge(app.opts[:layout_locals]).freeze
41
+ end
42
+ end
43
+
44
+ module InstanceMethods
45
+ private
46
+
47
+ def render_locals
48
+ opts[:render_locals]
49
+ end
50
+
51
+ def layout_locals
52
+ opts[:layout_locals]
53
+ end
54
+
55
+ # RODA3: Remove
56
+ def render_plugin_handle_locals?
57
+ false
58
+ end
59
+
60
+ # If this isn't the layout template, then use the plugin's render locals as the default locals.
61
+ def render_template_opts(template, opts)
62
+ opts = super
63
+ return opts if opts[:_is_layout]
64
+
65
+ plugin_locals = render_locals
66
+ if locals = opts[:locals]
67
+ plugin_locals = Hash[plugin_locals].merge!(locals)
68
+ end
69
+ opts[:locals] = plugin_locals
70
+ opts
71
+ end
72
+
73
+ # If using a layout, then use the plugin's layout locals as the default locals.
74
+ def view_layout_opts(opts)
75
+ if layout_opts = super
76
+ merge_locals = layout_opts.has_key?(:merge_locals) ? layout_opts[:merge_locals] : self.opts[:merge_locals]
77
+
78
+ locals = {}
79
+ if merge_locals && (plugin_locals = render_locals)
80
+ locals.merge!(plugin_locals)
81
+ end
82
+ if layout_locals = layout_locals()
83
+ locals.merge!(layout_locals)
84
+ end
85
+ if merge_locals && (method_locals = opts[:locals])
86
+ locals.merge!(method_locals)
87
+ end
88
+ if method_layout_locals = layout_opts[:locals]
89
+ locals.merge!(method_layout_locals)
90
+ end
91
+
92
+ layout_opts[:locals] = locals
93
+ layout_opts[:_is_layout] = true
94
+ layout_opts
95
+ end
96
+ end
97
+ end
98
+ end
99
+
100
+ register_plugin(:render_locals, RenderLocals)
101
+ end
102
+ end