hanami-view 2.0.0.alpha5 → 2.0.0.alpha8
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +28 -0
- data/README.md +1 -1
- data/hanami-view.gemspec +2 -1
- data/lib/hanami/view/context.rb +0 -18
- data/lib/hanami/view/errors.rb +5 -0
- data/lib/hanami/view/renderer.rb +1 -1
- data/lib/hanami/view/scope.rb +1 -1
- data/lib/hanami/view/version.rb +1 -1
- data/lib/hanami/view.rb +376 -12
- metadata +5 -7
- data/lib/hanami/view/application_configuration.rb +0 -77
- data/lib/hanami/view/application_context.rb +0 -57
- data/lib/hanami/view/standalone_view.rb +0 -400
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d49c664876b44d1be9f65e7d9c472a3fe50ab40d42002b170a4a2121ddf2f095
|
4
|
+
data.tar.gz: 51508250526dd6af93f2f8d1af2031e37c39109896a0b92fac038b99a65e1512
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e08aff9c75a5c412922f6e7669078e0a88258d8aa6d80b7b56569ce3732a15e703861e7a713381c1e928cbe5e5a67f1ac51e2473a9de85868ca9c895184e838e
|
7
|
+
data.tar.gz: cdad93f677870279dad0fb101dbfc03b4da79d78f78f3f1d6de5fdc50b7e7b0f4e69aef6b9b360049aaa2ff45d2e15beb24b2afe6116a87c07a17eb9eb0219eb
|
data/CHANGELOG.md
CHANGED
@@ -1,6 +1,34 @@
|
|
1
1
|
# Hanami::View
|
2
2
|
View layer for Hanami
|
3
3
|
|
4
|
+
## v2.0.0.alpha8 - 2022-05-19
|
5
|
+
|
6
|
+
### Added
|
7
|
+
- [Tim Riley] Access a hash of template locals via accessing `locals` inside any template
|
8
|
+
|
9
|
+
### Changed
|
10
|
+
- [Tim Riley] Removed automatic integration of `Hanami::View` subclasses with their surrounding Hanami application. View base classes within Hanami apps should inherit from `Hanami::Application::View` instead.
|
11
|
+
|
12
|
+
## v2.0.0.alpha7 - 2022-03-08
|
13
|
+
|
14
|
+
### Added
|
15
|
+
- [Luca Guidi] Automatically inject the app's `settings` and `assets` components (if present) into instances of `Hanami::View::ApplicationContext`
|
16
|
+
- [Luca Guidi] Temporarily added to `Hanami::View::ApplicationContext` the `#content_for`, `#current_path` `#csrf_token` helpers, ported from the hanami-2-application-template. Some of those helpers will be moved to `hanami-helpers` gem in a later release.
|
17
|
+
|
18
|
+
### Changed
|
19
|
+
- [Sean Collins] For views within an Hanami application, changed default location for templates from "web/templates" to "templates"
|
20
|
+
- [Luca Guidi] For views within an Hanami application, the default `part_namespace` is now `"view/parts"` (previously `"views/parts"`)
|
21
|
+
|
22
|
+
## Fixed
|
23
|
+
- [Luca Guidi] Application-level configuration is now applied to `Hanami::View` subclasses, no matter how deep their inheritance chain (e.g. app base view -> slice base view -> slice view)
|
24
|
+
|
25
|
+
## v2.0.0.alpha6 - 2022-02-10
|
26
|
+
### Added
|
27
|
+
- [Luca Guidi] Official support for Ruby: MRI 3.0 and 3.1
|
28
|
+
|
29
|
+
### Changed
|
30
|
+
- [Luca Guidi] Drop support for Ruby: MRI 2.3, 2.4, 2.5, 2.6, and 2.7.
|
31
|
+
|
4
32
|
## v2.0.0.alpha5 - 2022-01-12
|
5
33
|
### Added
|
6
34
|
- [Marc Busqué] Automatically provide access to Hanami application routes helper as `routes` in default application view context (`Hanami::View::ApplicationContext`)
|
data/README.md
CHANGED
data/hanami-view.gemspec
CHANGED
@@ -18,13 +18,14 @@ Gem::Specification.new do |spec|
|
|
18
18
|
spec.bindir = 'bin'
|
19
19
|
spec.executables = []
|
20
20
|
spec.require_paths = ['lib']
|
21
|
+
spec.metadata["rubygems_mfa_required"] = "true"
|
21
22
|
|
22
23
|
spec.metadata['allowed_push_host'] = 'https://rubygems.org'
|
23
24
|
spec.metadata['changelog_uri'] = 'https://github.com/hanami/view/blob/main/CHANGELOG.md'
|
24
25
|
spec.metadata['source_code_uri'] = 'https://github.com/hanami/view'
|
25
26
|
spec.metadata['bug_tracker_uri'] = 'https://github.com/hanami/view/issues'
|
26
27
|
|
27
|
-
spec.required_ruby_version = ">=
|
28
|
+
spec.required_ruby_version = ">= 3.0"
|
28
29
|
|
29
30
|
spec.add_runtime_dependency "concurrent-ruby", "~> 1.0"
|
30
31
|
spec.add_runtime_dependency "dry-configurable", "~> 0.13", ">= 0.13.0"
|
data/lib/hanami/view/context.rb
CHANGED
@@ -1,7 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require "dry/core/equalizer"
|
4
|
-
require_relative "application_context"
|
5
4
|
require_relative "decorated_attributes"
|
6
5
|
|
7
6
|
module Hanami
|
@@ -19,23 +18,6 @@ module Hanami
|
|
19
18
|
|
20
19
|
attr_reader :_render_env, :_options
|
21
20
|
|
22
|
-
def self.inherited(subclass)
|
23
|
-
super
|
24
|
-
|
25
|
-
# When inheriting within an Hanami app, add application context behavior
|
26
|
-
provider = application_provider(subclass)
|
27
|
-
if provider
|
28
|
-
subclass.include ApplicationContext.new(provider)
|
29
|
-
end
|
30
|
-
end
|
31
|
-
|
32
|
-
def self.application_provider(subclass)
|
33
|
-
if Hanami.respond_to?(:application?) && Hanami.application?
|
34
|
-
Hanami.application.component_provider(subclass)
|
35
|
-
end
|
36
|
-
end
|
37
|
-
private_class_method :application_provider
|
38
|
-
|
39
21
|
# Returns a new instance of Context
|
40
22
|
#
|
41
23
|
# In subclasses, you should include an `**options` parameter and pass _all
|
data/lib/hanami/view/errors.rb
CHANGED
data/lib/hanami/view/renderer.rb
CHANGED
data/lib/hanami/view/scope.rb
CHANGED
@@ -35,7 +35,7 @@ module Hanami
|
|
35
35
|
# @overload _locals
|
36
36
|
# Returns the locals
|
37
37
|
# @overload locals
|
38
|
-
# A convenience alias for `#
|
38
|
+
# A convenience alias for `#_locals.` Is available unless there is a
|
39
39
|
# local named `locals`
|
40
40
|
#
|
41
41
|
# @return [Hash[<Symbol, Object>]
|
data/lib/hanami/view/version.rb
CHANGED
data/lib/hanami/view.rb
CHANGED
@@ -15,7 +15,6 @@ require_relative "view/render_environment"
|
|
15
15
|
require_relative "view/rendered"
|
16
16
|
require_relative "view/renderer"
|
17
17
|
require_relative "view/scope_builder"
|
18
|
-
require_relative "view/standalone_view"
|
19
18
|
|
20
19
|
module Hanami
|
21
20
|
# A standalone, template-based view rendering system that offers everything
|
@@ -230,23 +229,388 @@ module Hanami
|
|
230
229
|
|
231
230
|
# @!endgroup
|
232
231
|
|
233
|
-
|
234
|
-
|
235
|
-
def self.inherited(subclass)
|
232
|
+
# @api private
|
233
|
+
def self.inherited(klass)
|
236
234
|
super
|
237
235
|
|
238
|
-
|
239
|
-
|
240
|
-
if subclass.superclass == View && (provider = application_provider(subclass))
|
241
|
-
subclass.include ApplicationView.new(provider)
|
236
|
+
exposures.each do |name, exposure|
|
237
|
+
klass.exposures.import(name, exposure)
|
242
238
|
end
|
243
239
|
end
|
244
240
|
|
245
|
-
|
246
|
-
|
247
|
-
|
241
|
+
# @!group Exposures
|
242
|
+
|
243
|
+
# @!macro [new] exposure_options
|
244
|
+
# @param options [Hash] the exposure's options
|
245
|
+
# @option options [Boolean] :layout expose this value to the layout (defaults to false)
|
246
|
+
# @option options [Boolean] :decorate decorate this value in a matching Part (defaults to
|
247
|
+
# true)
|
248
|
+
# @option options [Symbol, Class] :as an alternative name or class to use when finding a
|
249
|
+
# matching Part
|
250
|
+
|
251
|
+
# @overload expose(name, **options, &block)
|
252
|
+
# Define a value to be passed to the template. The return value of the
|
253
|
+
# block will be decorated by a matching Part and passed to the template.
|
254
|
+
#
|
255
|
+
# The block will be evaluated with the view instance as its `self`. The
|
256
|
+
# block's parameters will determine what it is given:
|
257
|
+
#
|
258
|
+
# - To receive other exposure values, provide positional parameters
|
259
|
+
# matching the exposure names. These exposures will already by decorated
|
260
|
+
# by their Parts.
|
261
|
+
# - To receive the view's input arguments (whatever is passed to
|
262
|
+
# `View#call`), provide matching keyword parameters. You can provide
|
263
|
+
# default values for these parameters to make the corresponding input
|
264
|
+
# keys optional
|
265
|
+
# - To receive the Context object, provide a `context:` keyword parameter
|
266
|
+
# - To receive the view's input arguments in their entirety, provide a
|
267
|
+
# keywords splat parameter (i.e. `**input`)
|
268
|
+
#
|
269
|
+
# @example Accessing input arguments
|
270
|
+
# expose :article do |slug:|
|
271
|
+
# article_repo.find_by_slug(slug)
|
272
|
+
# end
|
273
|
+
#
|
274
|
+
# @example Accessing other exposures
|
275
|
+
# expose :articles do
|
276
|
+
# article_repo.listing
|
277
|
+
# end
|
278
|
+
#
|
279
|
+
# expose :featured_articles do |articles|
|
280
|
+
# articles.select(&:featured?)
|
281
|
+
# end
|
282
|
+
#
|
283
|
+
# @param name [Symbol] name for the exposure
|
284
|
+
# @macro exposure_options
|
285
|
+
#
|
286
|
+
# @overload expose(name, **options)
|
287
|
+
# Define a value to be passed to the template, provided by an instance
|
288
|
+
# method matching the name. The method's return value will be decorated by
|
289
|
+
# a matching Part and passed to the template.
|
290
|
+
#
|
291
|
+
# The method's parameters will determine what it is given:
|
292
|
+
#
|
293
|
+
# - To receive other exposure values, provide positional parameters
|
294
|
+
# matching the exposure names. These exposures will already by decorated
|
295
|
+
# by their Parts.
|
296
|
+
# - To receive the view's input arguments (whatever is passed to
|
297
|
+
# `View#call`), provide matching keyword parameters. You can provide
|
298
|
+
# default values for these parameters to make the corresponding input
|
299
|
+
# keys optional
|
300
|
+
# - To receive the Context object, provide a `context:` keyword parameter
|
301
|
+
# - To receive the view's input arguments in their entirey, provide a
|
302
|
+
# keywords splat parameter (i.e. `**input`)
|
303
|
+
#
|
304
|
+
# @example Accessing input arguments
|
305
|
+
# expose :article
|
306
|
+
#
|
307
|
+
# def article(slug:)
|
308
|
+
# article_repo.find_by_slug(slug)
|
309
|
+
# end
|
310
|
+
#
|
311
|
+
# @example Accessing other exposures
|
312
|
+
# expose :articles
|
313
|
+
# expose :featured_articles
|
314
|
+
#
|
315
|
+
# def articles
|
316
|
+
# article_repo.listing
|
317
|
+
# end
|
318
|
+
#
|
319
|
+
# def featured_articles
|
320
|
+
# articles.select(&:featured?)
|
321
|
+
# end
|
322
|
+
#
|
323
|
+
# @param name [Symbol] name for the exposure
|
324
|
+
# @macro exposure_options
|
325
|
+
#
|
326
|
+
# @overload expose(name, **options)
|
327
|
+
# Define a single value to pass through from the input data (when there is
|
328
|
+
# no instance method matching the `name`). This value will be decorated by
|
329
|
+
# a matching Part and passed to the template.
|
330
|
+
#
|
331
|
+
# @param name [Symbol] name for the exposure
|
332
|
+
# @macro exposure_options
|
333
|
+
# @option options [Boolean] :default a default value to provide if there is no matching
|
334
|
+
# input data
|
335
|
+
#
|
336
|
+
# @overload expose(*names, **options)
|
337
|
+
# Define multiple values to pass through from the input data (when there
|
338
|
+
# is no instance methods matching their names). These values will be
|
339
|
+
# decorated by matching Parts and passed through to the template.
|
340
|
+
#
|
341
|
+
# The provided options will be applied to all the exposures.
|
342
|
+
#
|
343
|
+
# @param names [Symbol] names for the exposures
|
344
|
+
# @macro exposure_options
|
345
|
+
# @option options [Boolean] :default a default value to provide if there is no matching
|
346
|
+
# input data
|
347
|
+
#
|
348
|
+
# @see https://dry-rb.org/gems/dry-view/exposures/
|
349
|
+
#
|
350
|
+
# @api public
|
351
|
+
def self.expose(*names, **options, &block)
|
352
|
+
if names.length == 1
|
353
|
+
exposures.add(names.first, block, **options)
|
354
|
+
else
|
355
|
+
names.each do |name|
|
356
|
+
exposures.add(name, **options)
|
357
|
+
end
|
248
358
|
end
|
249
359
|
end
|
250
|
-
|
360
|
+
|
361
|
+
# @api public
|
362
|
+
def self.private_expose(*names, **options, &block)
|
363
|
+
expose(*names, **options, private: true, &block)
|
364
|
+
end
|
365
|
+
|
366
|
+
# Returns the defined exposures. These are unbound, since bound exposures
|
367
|
+
# are only created when initializing a View instance.
|
368
|
+
#
|
369
|
+
# @return [Exposures]
|
370
|
+
# @api private
|
371
|
+
def self.exposures
|
372
|
+
@exposures ||= Exposures.new
|
373
|
+
end
|
374
|
+
|
375
|
+
# @!endgroup
|
376
|
+
|
377
|
+
# @!group Scope
|
378
|
+
|
379
|
+
# Creates and assigns a scope for the current view.
|
380
|
+
#
|
381
|
+
# The newly created scope is useful to add custom logic that is specific
|
382
|
+
# to the view.
|
383
|
+
#
|
384
|
+
# The scope has access to locals, exposures, and inherited scope (if any)
|
385
|
+
#
|
386
|
+
# If the view already has an explicit scope the newly created scope will
|
387
|
+
# inherit from the explicit scope.
|
388
|
+
#
|
389
|
+
# There are two cases when this may happen:
|
390
|
+
# 1. The scope was explicitly assigned (e.g. `config.scope = MyScope`)
|
391
|
+
# 2. The scope has been inherited by the view superclass
|
392
|
+
#
|
393
|
+
# If the view doesn't have an already existing scope, the newly scope
|
394
|
+
# will inherit from `Hanami::View::Scope` by default.
|
395
|
+
#
|
396
|
+
# However, you can specify any base class for it. This is not
|
397
|
+
# recommended, unless you know what you're doing.
|
398
|
+
#
|
399
|
+
# @param scope [Hanami::View::Scope] the current scope (if any), or the
|
400
|
+
# default base class will be `Hanami::View::Scope`
|
401
|
+
# @param block [Proc] the scope logic definition
|
402
|
+
#
|
403
|
+
# @api public
|
404
|
+
#
|
405
|
+
# @example Basic usage
|
406
|
+
# class MyView < Hanami::View
|
407
|
+
# config.scope = MyScope
|
408
|
+
#
|
409
|
+
# scope do
|
410
|
+
# def greeting
|
411
|
+
# _locals[:message].upcase + "!"
|
412
|
+
# end
|
413
|
+
#
|
414
|
+
# def copyright(time)
|
415
|
+
# "Copy #{time.year}"
|
416
|
+
# end
|
417
|
+
# end
|
418
|
+
# end
|
419
|
+
#
|
420
|
+
# # my_view.html.erb
|
421
|
+
# # <%= greeting %>
|
422
|
+
# # <%= copyright(Time.now.utc) %>
|
423
|
+
#
|
424
|
+
# MyView.new.(message: "Hello") # => "HELLO!"
|
425
|
+
#
|
426
|
+
# @example Inherited scope
|
427
|
+
# class MyScope < Hanami::View::Scope
|
428
|
+
# private
|
429
|
+
#
|
430
|
+
# def shout(string)
|
431
|
+
# string.upcase + "!"
|
432
|
+
# end
|
433
|
+
# end
|
434
|
+
#
|
435
|
+
# class MyView < Hanami::View
|
436
|
+
# config.scope = MyScope
|
437
|
+
#
|
438
|
+
# scope do
|
439
|
+
# def greeting
|
440
|
+
# shout(_locals[:message])
|
441
|
+
# end
|
442
|
+
#
|
443
|
+
# def copyright(time)
|
444
|
+
# "Copy #{time.year}"
|
445
|
+
# end
|
446
|
+
# end
|
447
|
+
# end
|
448
|
+
#
|
449
|
+
# # my_view.html.erb
|
450
|
+
# # <%= greeting %>
|
451
|
+
# # <%= copyright(Time.now.utc) %>
|
452
|
+
#
|
453
|
+
# MyView.new.(message: "Hello") # => "HELLO!"
|
454
|
+
def self.scope(base: config.scope || Hanami::View::Scope, &block)
|
455
|
+
config.scope = Class.new(base, &block)
|
456
|
+
end
|
457
|
+
|
458
|
+
# @!endgroup
|
459
|
+
|
460
|
+
# @!group Render environment
|
461
|
+
|
462
|
+
# Returns a render environment for the view and the given options. This
|
463
|
+
# environment isn't chdir'ed into any particular directory.
|
464
|
+
#
|
465
|
+
# @param format [Symbol] template format to use (defaults to the `default_format` setting)
|
466
|
+
# @param context [Context] context object to use (defaults to the `default_context` setting)
|
467
|
+
#
|
468
|
+
# @see View.template_env render environment for the view's template
|
469
|
+
# @see View.layout_env render environment for the view's layout
|
470
|
+
#
|
471
|
+
# @return [RenderEnvironment]
|
472
|
+
# @api public
|
473
|
+
def self.render_env(format: config.default_format, context: config.default_context)
|
474
|
+
RenderEnvironment.prepare(renderer(format), config, context)
|
475
|
+
end
|
476
|
+
|
477
|
+
# @overload template_env(format: config.default_format, context: config.default_context)
|
478
|
+
# Returns a render environment for the view and the given options,
|
479
|
+
# chdir'ed into the view's template directory. This is the environment
|
480
|
+
# used when rendering the template, and is useful to to fetch
|
481
|
+
# independently when unit testing Parts and Scopes.
|
482
|
+
#
|
483
|
+
# @param format [Symbol] template format to use (defaults to the `default_format` setting)
|
484
|
+
# @param context [Context] context object to use (defaults to the `default_context` setting)
|
485
|
+
#
|
486
|
+
# @return [RenderEnvironment]
|
487
|
+
# @api public
|
488
|
+
def self.template_env(**args)
|
489
|
+
render_env(**args).chdir(config.template)
|
490
|
+
end
|
491
|
+
|
492
|
+
# @overload layout_env(format: config.default_format, context: config.default_context)
|
493
|
+
# Returns a render environment for the view and the given options,
|
494
|
+
# chdir'ed into the view's layout directory. This is the environment used
|
495
|
+
# when rendering the view's layout.
|
496
|
+
#
|
497
|
+
# @param format [Symbol] template format to use (defaults to the `default_format` setting)
|
498
|
+
# @param context [Context] context object to use (defaults to the `default_context` setting)
|
499
|
+
#
|
500
|
+
# @return [RenderEnvironment] @api public
|
501
|
+
def self.layout_env(**args)
|
502
|
+
render_env(**args).chdir(layout_path)
|
503
|
+
end
|
504
|
+
|
505
|
+
# Returns renderer for the view and provided format
|
506
|
+
#
|
507
|
+
# @api private
|
508
|
+
def self.renderer(format)
|
509
|
+
fetch_or_store(:renderer, config, format) {
|
510
|
+
Renderer.new(
|
511
|
+
config.paths,
|
512
|
+
format: format,
|
513
|
+
engine_mapping: config.renderer_engine_mapping,
|
514
|
+
**config.renderer_options
|
515
|
+
)
|
516
|
+
}
|
517
|
+
end
|
518
|
+
|
519
|
+
# @api private
|
520
|
+
def self.layout_path
|
521
|
+
File.join(*[config.layouts_dir, config.layout].compact)
|
522
|
+
end
|
523
|
+
|
524
|
+
# @!endgroup
|
525
|
+
|
526
|
+
# Returns an instance of the view. This binds the defined exposures to the
|
527
|
+
# view instance.
|
528
|
+
#
|
529
|
+
# Subclasses can define their own `#initialize` to accept injected
|
530
|
+
# dependencies, but must call `super()` to ensure the standard view
|
531
|
+
# initialization can proceed.
|
532
|
+
#
|
533
|
+
# @api public
|
534
|
+
def initialize
|
535
|
+
@exposures = self.class.exposures.bind(self)
|
536
|
+
end
|
537
|
+
|
538
|
+
# The view's configuration
|
539
|
+
#
|
540
|
+
# @api private
|
541
|
+
def config
|
542
|
+
self.class.config
|
543
|
+
end
|
544
|
+
|
545
|
+
# The view's bound exposures
|
546
|
+
#
|
547
|
+
# @return [Exposures]
|
548
|
+
# @api private
|
549
|
+
def exposures
|
550
|
+
@exposures
|
551
|
+
end
|
552
|
+
|
553
|
+
# Render the view
|
554
|
+
#
|
555
|
+
# @param format [Symbol] template format to use
|
556
|
+
# @param context [Context] context object to use
|
557
|
+
# @param input input data for preparing exposure values
|
558
|
+
#
|
559
|
+
# @return [Rendered] rendered view object
|
560
|
+
# @api public
|
561
|
+
def call(format: config.default_format, context: config.default_context, **input)
|
562
|
+
ensure_config
|
563
|
+
|
564
|
+
env = self.class.render_env(format: format, context: context)
|
565
|
+
template_env = self.class.template_env(format: format, context: context)
|
566
|
+
|
567
|
+
locals = locals(template_env, input)
|
568
|
+
output = env.template(config.template, template_env.scope(config.scope, locals))
|
569
|
+
|
570
|
+
if layout?
|
571
|
+
layout_env = self.class.layout_env(format: format, context: context)
|
572
|
+
begin
|
573
|
+
output = env.template(
|
574
|
+
self.class.layout_path,
|
575
|
+
layout_env.scope(config.scope, layout_locals(locals))
|
576
|
+
) { output }
|
577
|
+
rescue TemplateNotFoundError
|
578
|
+
raise LayoutNotFoundError.new(config.layout, config.paths)
|
579
|
+
end
|
580
|
+
end
|
581
|
+
|
582
|
+
Rendered.new(output: output, locals: locals)
|
583
|
+
end
|
584
|
+
|
585
|
+
private
|
586
|
+
|
587
|
+
# @api private
|
588
|
+
def ensure_config
|
589
|
+
raise UndefinedConfigError, :paths unless Array(config.paths).any?
|
590
|
+
raise UndefinedConfigError, :template unless config.template
|
591
|
+
end
|
592
|
+
|
593
|
+
# @api private
|
594
|
+
def locals(render_env, input)
|
595
|
+
exposures.(context: render_env.context, **input) do |value, exposure|
|
596
|
+
if exposure.decorate? && value
|
597
|
+
render_env.part(exposure.name, value, **exposure.options)
|
598
|
+
else
|
599
|
+
value
|
600
|
+
end
|
601
|
+
end
|
602
|
+
end
|
603
|
+
|
604
|
+
# @api private
|
605
|
+
def layout_locals(locals)
|
606
|
+
locals.each_with_object({}) do |(key, value), layout_locals|
|
607
|
+
layout_locals[key] = value if exposures[key].for_layout?
|
608
|
+
end
|
609
|
+
end
|
610
|
+
|
611
|
+
# @api private
|
612
|
+
def layout?
|
613
|
+
!!config.layout # rubocop:disable Style/DoubleNegation
|
614
|
+
end
|
251
615
|
end
|
252
616
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: hanami-view
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 2.0.0.
|
4
|
+
version: 2.0.0.alpha8
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Tim Riley
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2022-
|
12
|
+
date: 2022-05-19 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: concurrent-ruby
|
@@ -156,8 +156,6 @@ files:
|
|
156
156
|
- hanami-view.gemspec
|
157
157
|
- lib/hanami-view.rb
|
158
158
|
- lib/hanami/view.rb
|
159
|
-
- lib/hanami/view/application_configuration.rb
|
160
|
-
- lib/hanami/view/application_context.rb
|
161
159
|
- lib/hanami/view/application_view.rb
|
162
160
|
- lib/hanami/view/context.rb
|
163
161
|
- lib/hanami/view/context_helpers/content_helpers.rb
|
@@ -174,7 +172,6 @@ files:
|
|
174
172
|
- lib/hanami/view/renderer.rb
|
175
173
|
- lib/hanami/view/scope.rb
|
176
174
|
- lib/hanami/view/scope_builder.rb
|
177
|
-
- lib/hanami/view/standalone_view.rb
|
178
175
|
- lib/hanami/view/tilt.rb
|
179
176
|
- lib/hanami/view/tilt/erb.rb
|
180
177
|
- lib/hanami/view/tilt/erbse.rb
|
@@ -184,6 +181,7 @@ homepage: https://dry-rb.org/gems/hanami-view
|
|
184
181
|
licenses:
|
185
182
|
- MIT
|
186
183
|
metadata:
|
184
|
+
rubygems_mfa_required: 'true'
|
187
185
|
allowed_push_host: https://rubygems.org
|
188
186
|
changelog_uri: https://github.com/hanami/view/blob/main/CHANGELOG.md
|
189
187
|
source_code_uri: https://github.com/hanami/view
|
@@ -196,14 +194,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
196
194
|
requirements:
|
197
195
|
- - ">="
|
198
196
|
- !ruby/object:Gem::Version
|
199
|
-
version:
|
197
|
+
version: '3.0'
|
200
198
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
201
199
|
requirements:
|
202
200
|
- - ">"
|
203
201
|
- !ruby/object:Gem::Version
|
204
202
|
version: 1.3.1
|
205
203
|
requirements: []
|
206
|
-
rubygems_version: 3.
|
204
|
+
rubygems_version: 3.3.7
|
207
205
|
signing_key:
|
208
206
|
specification_version: 4
|
209
207
|
summary: A complete, standalone view rendering system that gives you everything you
|
@@ -1,77 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require "dry/configurable"
|
4
|
-
require_relative "../view"
|
5
|
-
|
6
|
-
module Hanami
|
7
|
-
class View
|
8
|
-
class ApplicationConfiguration
|
9
|
-
include Dry::Configurable
|
10
|
-
|
11
|
-
setting :parts_path, default: "views/parts"
|
12
|
-
|
13
|
-
def initialize(*)
|
14
|
-
super
|
15
|
-
|
16
|
-
@base_config = View.config.dup
|
17
|
-
|
18
|
-
configure_defaults
|
19
|
-
end
|
20
|
-
|
21
|
-
# Returns the list of available settings
|
22
|
-
#
|
23
|
-
# @return [Set]
|
24
|
-
#
|
25
|
-
# @since 2.0.0
|
26
|
-
# @api private
|
27
|
-
def settings
|
28
|
-
self.class.settings + View.settings - NON_FORWARDABLE_METHODS
|
29
|
-
end
|
30
|
-
|
31
|
-
def finalize!
|
32
|
-
return self if frozen?
|
33
|
-
|
34
|
-
base_config.finalize!
|
35
|
-
|
36
|
-
super
|
37
|
-
end
|
38
|
-
|
39
|
-
private
|
40
|
-
|
41
|
-
attr_reader :base_config
|
42
|
-
|
43
|
-
def configure_defaults
|
44
|
-
self.paths = ["web/templates"]
|
45
|
-
self.template_inference_base = "views"
|
46
|
-
self.layout = "application"
|
47
|
-
end
|
48
|
-
|
49
|
-
# An inflector for views is not configurable via `config.views.inflector` on an
|
50
|
-
# `Hanami::Application`. The application-wide inflector is already configurable
|
51
|
-
# there as `config.inflector` and will be used as the default inflector for views.
|
52
|
-
#
|
53
|
-
# A custom inflector may still be provided in an `Hanami::View` subclass, via
|
54
|
-
# `config.inflector=`.
|
55
|
-
NON_FORWARDABLE_METHODS = [:inflector, :inflector=].freeze
|
56
|
-
private_constant :NON_FORWARDABLE_METHODS
|
57
|
-
|
58
|
-
def method_missing(name, *args, &block)
|
59
|
-
return super if NON_FORWARDABLE_METHODS.include?(name)
|
60
|
-
|
61
|
-
if config.respond_to?(name)
|
62
|
-
config.public_send(name, *args, &block)
|
63
|
-
elsif base_config.respond_to?(name)
|
64
|
-
base_config.public_send(name, *args, &block)
|
65
|
-
else
|
66
|
-
super
|
67
|
-
end
|
68
|
-
end
|
69
|
-
|
70
|
-
def respond_to_missing?(name, _include_all = false)
|
71
|
-
return false if NON_FORWARDABLE_METHODS.include?(name)
|
72
|
-
|
73
|
-
config.respond_to?(name) || base_config.respond_to?(name) || super
|
74
|
-
end
|
75
|
-
end
|
76
|
-
end
|
77
|
-
end
|
@@ -1,57 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Hanami
|
4
|
-
class View
|
5
|
-
class ApplicationContext < Module
|
6
|
-
attr_reader :provider
|
7
|
-
attr_reader :application
|
8
|
-
|
9
|
-
def initialize(provider)
|
10
|
-
@provider = provider
|
11
|
-
@application = provider.respond_to?(:application) ? provider.application : Hanami.application
|
12
|
-
end
|
13
|
-
|
14
|
-
def included(context_class)
|
15
|
-
define_initialize
|
16
|
-
context_class.include(InstanceMethods)
|
17
|
-
end
|
18
|
-
|
19
|
-
private
|
20
|
-
|
21
|
-
def define_initialize
|
22
|
-
inflector = application.inflector
|
23
|
-
routes = application[:routes_helper] if application.key?(:routes_helper)
|
24
|
-
|
25
|
-
define_method :initialize do |**options|
|
26
|
-
@inflector = options[:inflector] || inflector
|
27
|
-
@routes = options[:routes] || routes
|
28
|
-
super(**options)
|
29
|
-
end
|
30
|
-
end
|
31
|
-
|
32
|
-
module InstanceMethods
|
33
|
-
attr_reader :inflector
|
34
|
-
attr_reader :routes
|
35
|
-
|
36
|
-
def request
|
37
|
-
_options.fetch(:request)
|
38
|
-
end
|
39
|
-
|
40
|
-
def session
|
41
|
-
request.session
|
42
|
-
end
|
43
|
-
|
44
|
-
def flash
|
45
|
-
response.flash
|
46
|
-
end
|
47
|
-
|
48
|
-
private
|
49
|
-
|
50
|
-
# TODO: create `Request#flash` so we no longer need the `response`
|
51
|
-
def response
|
52
|
-
_options.fetch(:response)
|
53
|
-
end
|
54
|
-
end
|
55
|
-
end
|
56
|
-
end
|
57
|
-
end
|
@@ -1,400 +0,0 @@
|
|
1
|
-
require_relative "scope"
|
2
|
-
|
3
|
-
module Hanami
|
4
|
-
class View
|
5
|
-
module StandaloneView
|
6
|
-
def self.included(klass)
|
7
|
-
klass.extend ClassMethods
|
8
|
-
klass.include InstanceMethods
|
9
|
-
end
|
10
|
-
|
11
|
-
module ClassMethods
|
12
|
-
# @api private
|
13
|
-
def inherited(klass)
|
14
|
-
super
|
15
|
-
|
16
|
-
exposures.each do |name, exposure|
|
17
|
-
klass.exposures.import(name, exposure)
|
18
|
-
end
|
19
|
-
end
|
20
|
-
|
21
|
-
# @!group Exposures
|
22
|
-
|
23
|
-
# @!macro [new] exposure_options
|
24
|
-
# @param options [Hash] the exposure's options
|
25
|
-
# @option options [Boolean] :layout expose this value to the layout (defaults to false)
|
26
|
-
# @option options [Boolean] :decorate decorate this value in a matching Part (defaults to
|
27
|
-
# true)
|
28
|
-
# @option options [Symbol, Class] :as an alternative name or class to use when finding a
|
29
|
-
# matching Part
|
30
|
-
|
31
|
-
# @overload expose(name, **options, &block)
|
32
|
-
# Define a value to be passed to the template. The return value of the
|
33
|
-
# block will be decorated by a matching Part and passed to the template.
|
34
|
-
#
|
35
|
-
# The block will be evaluated with the view instance as its `self`. The
|
36
|
-
# block's parameters will determine what it is given:
|
37
|
-
#
|
38
|
-
# - To receive other exposure values, provide positional parameters
|
39
|
-
# matching the exposure names. These exposures will already by decorated
|
40
|
-
# by their Parts.
|
41
|
-
# - To receive the view's input arguments (whatever is passed to
|
42
|
-
# `View#call`), provide matching keyword parameters. You can provide
|
43
|
-
# default values for these parameters to make the corresponding input
|
44
|
-
# keys optional
|
45
|
-
# - To receive the Context object, provide a `context:` keyword parameter
|
46
|
-
# - To receive the view's input arguments in their entirety, provide a
|
47
|
-
# keywords splat parameter (i.e. `**input`)
|
48
|
-
#
|
49
|
-
# @example Accessing input arguments
|
50
|
-
# expose :article do |slug:|
|
51
|
-
# article_repo.find_by_slug(slug)
|
52
|
-
# end
|
53
|
-
#
|
54
|
-
# @example Accessing other exposures
|
55
|
-
# expose :articles do
|
56
|
-
# article_repo.listing
|
57
|
-
# end
|
58
|
-
#
|
59
|
-
# expose :featured_articles do |articles|
|
60
|
-
# articles.select(&:featured?)
|
61
|
-
# end
|
62
|
-
#
|
63
|
-
# @param name [Symbol] name for the exposure
|
64
|
-
# @macro exposure_options
|
65
|
-
#
|
66
|
-
# @overload expose(name, **options)
|
67
|
-
# Define a value to be passed to the template, provided by an instance
|
68
|
-
# method matching the name. The method's return value will be decorated by
|
69
|
-
# a matching Part and passed to the template.
|
70
|
-
#
|
71
|
-
# The method's parameters will determine what it is given:
|
72
|
-
#
|
73
|
-
# - To receive other exposure values, provide positional parameters
|
74
|
-
# matching the exposure names. These exposures will already by decorated
|
75
|
-
# by their Parts.
|
76
|
-
# - To receive the view's input arguments (whatever is passed to
|
77
|
-
# `View#call`), provide matching keyword parameters. You can provide
|
78
|
-
# default values for these parameters to make the corresponding input
|
79
|
-
# keys optional
|
80
|
-
# - To receive the Context object, provide a `context:` keyword parameter
|
81
|
-
# - To receive the view's input arguments in their entirey, provide a
|
82
|
-
# keywords splat parameter (i.e. `**input`)
|
83
|
-
#
|
84
|
-
# @example Accessing input arguments
|
85
|
-
# expose :article
|
86
|
-
#
|
87
|
-
# def article(slug:)
|
88
|
-
# article_repo.find_by_slug(slug)
|
89
|
-
# end
|
90
|
-
#
|
91
|
-
# @example Accessing other exposures
|
92
|
-
# expose :articles
|
93
|
-
# expose :featured_articles
|
94
|
-
#
|
95
|
-
# def articles
|
96
|
-
# article_repo.listing
|
97
|
-
# end
|
98
|
-
#
|
99
|
-
# def featured_articles
|
100
|
-
# articles.select(&:featured?)
|
101
|
-
# end
|
102
|
-
#
|
103
|
-
# @param name [Symbol] name for the exposure
|
104
|
-
# @macro exposure_options
|
105
|
-
#
|
106
|
-
# @overload expose(name, **options)
|
107
|
-
# Define a single value to pass through from the input data (when there is
|
108
|
-
# no instance method matching the `name`). This value will be decorated by
|
109
|
-
# a matching Part and passed to the template.
|
110
|
-
#
|
111
|
-
# @param name [Symbol] name for the exposure
|
112
|
-
# @macro exposure_options
|
113
|
-
# @option options [Boolean] :default a default value to provide if there is no matching
|
114
|
-
# input data
|
115
|
-
#
|
116
|
-
# @overload expose(*names, **options)
|
117
|
-
# Define multiple values to pass through from the input data (when there
|
118
|
-
# is no instance methods matching their names). These values will be
|
119
|
-
# decorated by matching Parts and passed through to the template.
|
120
|
-
#
|
121
|
-
# The provided options will be applied to all the exposures.
|
122
|
-
#
|
123
|
-
# @param names [Symbol] names for the exposures
|
124
|
-
# @macro exposure_options
|
125
|
-
# @option options [Boolean] :default a default value to provide if there is no matching
|
126
|
-
# input data
|
127
|
-
#
|
128
|
-
# @see https://dry-rb.org/gems/dry-view/exposures/
|
129
|
-
#
|
130
|
-
# @api public
|
131
|
-
def expose(*names, **options, &block)
|
132
|
-
if names.length == 1
|
133
|
-
exposures.add(names.first, block, **options)
|
134
|
-
else
|
135
|
-
names.each do |name|
|
136
|
-
exposures.add(name, **options)
|
137
|
-
end
|
138
|
-
end
|
139
|
-
end
|
140
|
-
|
141
|
-
# @api public
|
142
|
-
def private_expose(*names, **options, &block)
|
143
|
-
expose(*names, **options, private: true, &block)
|
144
|
-
end
|
145
|
-
|
146
|
-
# Returns the defined exposures. These are unbound, since bound exposures
|
147
|
-
# are only created when initializing a View instance.
|
148
|
-
#
|
149
|
-
# @return [Exposures]
|
150
|
-
# @api private
|
151
|
-
def exposures
|
152
|
-
@exposures ||= Exposures.new
|
153
|
-
end
|
154
|
-
|
155
|
-
# @!endgroup
|
156
|
-
|
157
|
-
# @!group Scope
|
158
|
-
|
159
|
-
# Creates and assigns a scope for the current view.
|
160
|
-
#
|
161
|
-
# The newly created scope is useful to add custom logic that is specific
|
162
|
-
# to the view.
|
163
|
-
#
|
164
|
-
# The scope has access to locals, exposures, and inherited scope (if any)
|
165
|
-
#
|
166
|
-
# If the view already has an explicit scope the newly created scope will
|
167
|
-
# inherit from the explicit scope.
|
168
|
-
#
|
169
|
-
# There are two cases when this may happen:
|
170
|
-
# 1. The scope was explicitly assigned (e.g. `config.scope = MyScope`)
|
171
|
-
# 2. The scope has been inherited by the view superclass
|
172
|
-
#
|
173
|
-
# If the view doesn't have an already existing scope, the newly scope
|
174
|
-
# will inherit from `Hanami::View::Scope` by default.
|
175
|
-
#
|
176
|
-
# However, you can specify any base class for it. This is not
|
177
|
-
# recommended, unless you know what you're doing.
|
178
|
-
#
|
179
|
-
# @param scope [Hanami::View::Scope] the current scope (if any), or the
|
180
|
-
# default base class will be `Hanami::View::Scope`
|
181
|
-
# @param block [Proc] the scope logic definition
|
182
|
-
#
|
183
|
-
# @api public
|
184
|
-
#
|
185
|
-
# @example Basic usage
|
186
|
-
# class MyView < Hanami::View
|
187
|
-
# config.scope = MyScope
|
188
|
-
#
|
189
|
-
# scope do
|
190
|
-
# def greeting
|
191
|
-
# _locals[:message].upcase + "!"
|
192
|
-
# end
|
193
|
-
#
|
194
|
-
# def copyright(time)
|
195
|
-
# "Copy #{time.year}"
|
196
|
-
# end
|
197
|
-
# end
|
198
|
-
# end
|
199
|
-
#
|
200
|
-
# # my_view.html.erb
|
201
|
-
# # <%= greeting %>
|
202
|
-
# # <%= copyright(Time.now.utc) %>
|
203
|
-
#
|
204
|
-
# MyView.new.(message: "Hello") # => "HELLO!"
|
205
|
-
#
|
206
|
-
# @example Inherited scope
|
207
|
-
# class MyScope < Hanami::View::Scope
|
208
|
-
# private
|
209
|
-
#
|
210
|
-
# def shout(string)
|
211
|
-
# string.upcase + "!"
|
212
|
-
# end
|
213
|
-
# end
|
214
|
-
#
|
215
|
-
# class MyView < Hanami::View
|
216
|
-
# config.scope = MyScope
|
217
|
-
#
|
218
|
-
# scope do
|
219
|
-
# def greeting
|
220
|
-
# shout(_locals[:message])
|
221
|
-
# end
|
222
|
-
#
|
223
|
-
# def copyright(time)
|
224
|
-
# "Copy #{time.year}"
|
225
|
-
# end
|
226
|
-
# end
|
227
|
-
# end
|
228
|
-
#
|
229
|
-
# # my_view.html.erb
|
230
|
-
# # <%= greeting %>
|
231
|
-
# # <%= copyright(Time.now.utc) %>
|
232
|
-
#
|
233
|
-
# MyView.new.(message: "Hello") # => "HELLO!"
|
234
|
-
def scope(base: config.scope || Hanami::View::Scope, &block)
|
235
|
-
config.scope = Class.new(base, &block)
|
236
|
-
end
|
237
|
-
|
238
|
-
# @!endgroup
|
239
|
-
|
240
|
-
# @!group Render environment
|
241
|
-
|
242
|
-
# Returns a render environment for the view and the given options. This
|
243
|
-
# environment isn't chdir'ed into any particular directory.
|
244
|
-
#
|
245
|
-
# @param format [Symbol] template format to use (defaults to the `default_format` setting)
|
246
|
-
# @param context [Context] context object to use (defaults to the `default_context` setting)
|
247
|
-
#
|
248
|
-
# @see View.template_env render environment for the view's template
|
249
|
-
# @see View.layout_env render environment for the view's layout
|
250
|
-
#
|
251
|
-
# @return [RenderEnvironment]
|
252
|
-
# @api public
|
253
|
-
def render_env(format: config.default_format, context: config.default_context)
|
254
|
-
RenderEnvironment.prepare(renderer(format), config, context)
|
255
|
-
end
|
256
|
-
|
257
|
-
# @overload template_env(format: config.default_format, context: config.default_context)
|
258
|
-
# Returns a render environment for the view and the given options,
|
259
|
-
# chdir'ed into the view's template directory. This is the environment
|
260
|
-
# used when rendering the template, and is useful to to fetch
|
261
|
-
# independently when unit testing Parts and Scopes.
|
262
|
-
#
|
263
|
-
# @param format [Symbol] template format to use (defaults to the `default_format` setting)
|
264
|
-
# @param context [Context] context object to use (defaults to the `default_context` setting)
|
265
|
-
#
|
266
|
-
# @return [RenderEnvironment]
|
267
|
-
# @api public
|
268
|
-
def template_env(**args)
|
269
|
-
render_env(**args).chdir(config.template)
|
270
|
-
end
|
271
|
-
|
272
|
-
# @overload layout_env(format: config.default_format, context: config.default_context)
|
273
|
-
# Returns a render environment for the view and the given options,
|
274
|
-
# chdir'ed into the view's layout directory. This is the environment used
|
275
|
-
# when rendering the view's layout.
|
276
|
-
#
|
277
|
-
# @param format [Symbol] template format to use (defaults to the `default_format` setting)
|
278
|
-
# @param context [Context] context object to use (defaults to the `default_context` setting)
|
279
|
-
#
|
280
|
-
# @return [RenderEnvironment] @api public
|
281
|
-
def layout_env(**args)
|
282
|
-
render_env(**args).chdir(layout_path)
|
283
|
-
end
|
284
|
-
|
285
|
-
# Returns renderer for the view and provided format
|
286
|
-
#
|
287
|
-
# @api private
|
288
|
-
def renderer(format)
|
289
|
-
fetch_or_store(:renderer, config, format) {
|
290
|
-
Renderer.new(
|
291
|
-
config.paths,
|
292
|
-
format: format,
|
293
|
-
engine_mapping: config.renderer_engine_mapping,
|
294
|
-
**config.renderer_options
|
295
|
-
)
|
296
|
-
}
|
297
|
-
end
|
298
|
-
|
299
|
-
# @api private
|
300
|
-
def layout_path
|
301
|
-
File.join(*[config.layouts_dir, config.layout].compact)
|
302
|
-
end
|
303
|
-
|
304
|
-
# @!endgroup
|
305
|
-
end
|
306
|
-
|
307
|
-
module InstanceMethods
|
308
|
-
# Returns an instance of the view. This binds the defined exposures to the
|
309
|
-
# view instance.
|
310
|
-
#
|
311
|
-
# Subclasses can define their own `#initialize` to accept injected
|
312
|
-
# dependencies, but must call `super()` to ensure the standard view
|
313
|
-
# initialization can proceed.
|
314
|
-
#
|
315
|
-
# @api public
|
316
|
-
def initialize
|
317
|
-
@exposures = self.class.exposures.bind(self)
|
318
|
-
end
|
319
|
-
|
320
|
-
# The view's configuration
|
321
|
-
#
|
322
|
-
# @api private
|
323
|
-
def config
|
324
|
-
self.class.config
|
325
|
-
end
|
326
|
-
|
327
|
-
# The view's bound exposures
|
328
|
-
#
|
329
|
-
# @return [Exposures]
|
330
|
-
# @api private
|
331
|
-
def exposures
|
332
|
-
@exposures
|
333
|
-
end
|
334
|
-
|
335
|
-
# Render the view
|
336
|
-
#
|
337
|
-
# @param format [Symbol] template format to use
|
338
|
-
# @param context [Context] context object to use
|
339
|
-
# @param input input data for preparing exposure values
|
340
|
-
#
|
341
|
-
# @return [Rendered] rendered view object
|
342
|
-
# @api public
|
343
|
-
def call(format: config.default_format, context: config.default_context, **input)
|
344
|
-
ensure_config
|
345
|
-
|
346
|
-
env = self.class.render_env(format: format, context: context)
|
347
|
-
template_env = self.class.template_env(format: format, context: context)
|
348
|
-
|
349
|
-
locals = locals(template_env, input)
|
350
|
-
output = env.template(config.template, template_env.scope(config.scope, locals))
|
351
|
-
|
352
|
-
if layout?
|
353
|
-
layout_env = self.class.layout_env(format: format, context: context)
|
354
|
-
begin
|
355
|
-
output = env.template(
|
356
|
-
self.class.layout_path,
|
357
|
-
layout_env.scope(config.scope, layout_locals(locals))
|
358
|
-
) { output }
|
359
|
-
rescue TemplateNotFoundError
|
360
|
-
raise LayoutNotFoundError.new(config.layout, config.paths)
|
361
|
-
end
|
362
|
-
end
|
363
|
-
|
364
|
-
Rendered.new(output: output, locals: locals)
|
365
|
-
end
|
366
|
-
|
367
|
-
private
|
368
|
-
|
369
|
-
# @api private
|
370
|
-
def ensure_config
|
371
|
-
raise UndefinedConfigError, :paths unless Array(config.paths).any?
|
372
|
-
raise UndefinedConfigError, :template unless config.template
|
373
|
-
end
|
374
|
-
|
375
|
-
# @api private
|
376
|
-
def locals(render_env, input)
|
377
|
-
exposures.(context: render_env.context, **input) do |value, exposure|
|
378
|
-
if exposure.decorate? && value
|
379
|
-
render_env.part(exposure.name, value, **exposure.options)
|
380
|
-
else
|
381
|
-
value
|
382
|
-
end
|
383
|
-
end
|
384
|
-
end
|
385
|
-
|
386
|
-
# @api private
|
387
|
-
def layout_locals(locals)
|
388
|
-
locals.each_with_object({}) do |(key, value), layout_locals|
|
389
|
-
layout_locals[key] = value if exposures[key].for_layout?
|
390
|
-
end
|
391
|
-
end
|
392
|
-
|
393
|
-
# @api private
|
394
|
-
def layout?
|
395
|
-
!!config.layout # rubocop:disable Style/DoubleNegation
|
396
|
-
end
|
397
|
-
end
|
398
|
-
end
|
399
|
-
end
|
400
|
-
end
|