roda 2.28.0 → 2.29.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.
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