hanami-view 2.3.1 → 3.0.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.
@@ -3,54 +3,64 @@
3
3
  module Hanami
4
4
  class View
5
5
  # @api private
6
- # @since 2.1.0
7
6
  class Rendering
8
7
  # @api private
9
- # @since 2.1.0
10
- attr_reader :config, :format
8
+ attr_reader :format
11
9
 
12
- # @api private
13
- # @since 2.1.0
14
10
  attr_reader :inflector, :part_builder, :scope_builder
15
11
 
16
- # @api private
17
- # @since 2.1.0
12
+ attr_reader :part_class, :part_namespace, :scope_class, :scope_namespace
13
+
14
+ # Stable identity for the underlying config snapshot.
15
+ attr_reader :cache_key
16
+
18
17
  attr_reader :context, :renderer
19
18
 
20
- # @api private
21
- # @since 2.1.0
22
- def initialize(config:, format:, context:)
23
- @config = config
19
+ def initialize(config_data:, format:, context:)
24
20
  @format = format
25
21
 
26
- @inflector = config.inflector
27
- @part_builder = config.part_builder
28
- @scope_builder = config.scope_builder
22
+ @inflector = config_data.inflector
23
+ @part_builder = config_data.part_builder
24
+ @scope_builder = config_data.scope_builder
25
+
26
+ @part_class = config_data.part_class
27
+ @part_namespace = config_data.part_namespace
28
+ @scope_class = config_data.scope_class
29
+ @scope_namespace = config_data.scope_namespace
30
+ @cache_key = config_data.object_id
29
31
 
30
32
  @context = context.dup_for_rendering(self)
31
- @renderer = Renderer.new(config)
33
+ @renderer = Renderer.new(config_data)
34
+ end
35
+
36
+ # Returns the resolved name of the template or partial currently being rendered, or nil if
37
+ # no render is in progress.
38
+ #
39
+ # @return [String, nil]
40
+ def current_template_name
41
+ renderer.current_template_name
42
+ end
43
+
44
+ # Returns the stack of resolved names for the templates and partials currently being
45
+ # rendered.
46
+ #
47
+ # @return [Array<String>]
48
+ def current_template_names
49
+ renderer.current_template_names
32
50
  end
33
51
 
34
- # @api private
35
- # @since 2.1.0
36
52
  def template(name, scope, &block)
37
53
  renderer.template(name, format, scope, &block)
38
54
  end
39
55
 
40
- # @api private
41
- # @since 2.1.0
42
56
  def partial(name, scope, &block)
43
57
  renderer.partial(name, format, scope, &block)
44
58
  end
45
59
 
46
- # @api private
47
- # @since 2.1.0
48
60
  def part(name, value, as: nil)
49
61
  part_builder.(name, value, as: as, rendering: self)
50
62
  end
51
63
 
52
- # @api private
53
- # @since 2.1.0
54
64
  def scope(name = nil, locals) # rubocop:disable Style/OptionalArguments
55
65
  scope_builder.(name, locals: locals, rendering: self)
56
66
  end
@@ -8,47 +8,44 @@ module Hanami
8
8
  # @api private
9
9
  # @since 2.1.0
10
10
  class RenderingMissing
11
- # @api private
12
- # @since 2.1.0
13
11
  def format
14
12
  raise RenderingMissingError
15
13
  end
16
14
 
17
- # @api private
18
- # @since 2.1.0
19
15
  def context
20
16
  raise RenderingMissingError
21
17
  end
22
18
 
23
- # @api private
24
- # @since 2.1.0
25
19
  def part(_name, _value, **_options)
26
20
  raise RenderingMissingError
27
21
  end
28
22
 
29
- # @api private
30
- # @since 2.1.0
31
23
  def scope(_name = nil, _locals) # rubocop:disable Style/OptionalArguments
32
24
  raise RenderingMissingError
33
25
  end
34
26
 
35
- # @api private
36
- # @since 2.1.0
37
27
  def template(_name, _scope)
38
28
  raise RenderingMissingError
39
29
  end
40
30
 
41
- # @api private
42
- # @since 2.1.0
43
31
  def partial(_name, _scope)
44
32
  raise RenderingMissingError
45
33
  end
46
34
 
47
- # @api private
48
- # @since 2.1.0
49
35
  def inflector
50
36
  @inflector ||= Dry::Inflector.new
51
37
  end
38
+
39
+ def current_template_name
40
+ nil
41
+ end
42
+
43
+ def current_template_names
44
+ EMPTY_TEMPLATE_NAMES
45
+ end
46
+
47
+ EMPTY_TEMPLATE_NAMES = [].freeze
48
+ private_constant :EMPTY_TEMPLATE_NAMES
52
49
  end
53
50
  end
54
51
  end
@@ -19,7 +19,7 @@ module Hanami
19
19
  # @since 2.1.0
20
20
  class Scope
21
21
  # @api private
22
- CONVENIENCE_METHODS = %i[format context locals].freeze
22
+ CONVENIENCE_METHODS = %i[format context locals template_name].freeze
23
23
 
24
24
  include Dry::Equalizer(:_name, :_locals, :_rendering)
25
25
 
@@ -35,10 +35,10 @@ module Hanami
35
35
  #
36
36
  # @overload _locals
37
37
  # Returns the locals.
38
+ # @return [Hash{Symbol => Object}]
38
39
  # @overload locals
39
40
  # A convenience alias for `#_locals.` Is available unless there is a local named `locals`
40
- #
41
- # @return [Hash[<Symbol, Object>]
41
+ # @return [Hash{Symbol => Object}]
42
42
  #
43
43
  # @api public
44
44
  # @since 2.1.0
@@ -76,16 +76,16 @@ module Hanami
76
76
  # Renders a partial using the scope.
77
77
  #
78
78
  # @param partial_name [Symbol, String] partial name
79
- # @param locals [Hash<Symbol, Object>] partial locals
79
+ # @param locals [Hash{Symbol => Object}] partial locals
80
80
  # @yieldreturn [String] string content to include where the partial calls `yield`
81
+ # @return [String] the rendered partial output
81
82
  #
82
83
  # @overload render(**locals, &block)
83
84
  # Renders a partial (named after the scope's own name) using the scope.
84
85
  #
85
- # @param locals[Hash<Symbol, Object>] partial locals
86
+ # @param locals [Hash{Symbol => Object}] partial locals
86
87
  # @yieldreturn [String] string content to include where the partial calls `yield`
87
- #
88
- # @return [String] the rendered partial output
88
+ # @return [String] the rendered partial output
89
89
  #
90
90
  # @api public
91
91
  # @since 2.1.0
@@ -120,11 +120,10 @@ module Hanami
120
120
  #
121
121
  # @overload _format
122
122
  # Returns the format.
123
+ # @return [Symbol] format
123
124
  # @overload format
124
- # A convenience alias for `#_format.` Is available unless there is a
125
- # local named `format`
126
- #
127
- # @return [Symbol] format
125
+ # A convenience alias for `#_format.` Is available unless there is a local named `format`.
126
+ # @return [Symbol] format
128
127
  #
129
128
  # @api public
130
129
  # @since 2.1.0
@@ -136,11 +135,10 @@ module Hanami
136
135
  #
137
136
  # @overload _context
138
137
  # Returns the context.
138
+ # @return [Context] context
139
139
  # @overload context
140
- # A convenience alias for `#_context`. Is available unless there is a
141
- # local named `context`.
142
- #
143
- # @return [Context] context
140
+ # A convenience alias for `#_context`. Is available unless there is a local named `context`.
141
+ # @return [Context] context
144
142
  #
145
143
  # @api public
146
144
  # @since 2.1.0
@@ -148,6 +146,22 @@ module Hanami
148
146
  _rendering.context
149
147
  end
150
148
 
149
+ # Returns the name of the template or partial currently being rendered.
150
+ #
151
+ # @overload _template_name
152
+ # Returns the current template name.
153
+ # @return [String, nil]
154
+ # @overload template_name
155
+ # A convenience alias for `#_template_name`. Is available unless there is a local named
156
+ # `template_name`.
157
+ # @return [String, nil]
158
+ #
159
+ # @api public
160
+ # @since 3.0.0
161
+ def _template_name
162
+ _rendering.current_template_name
163
+ end
164
+
151
165
  private
152
166
 
153
167
  # Handles missing methods, according to the following rules:
@@ -17,7 +17,7 @@ module Hanami
17
17
  #
18
18
  # @api public
19
19
  # @since 2.1.0
20
- def call(name = nil, locals:, rendering:) # rubocop:disable Style/OptionalArguments
20
+ def call(name = nil, locals:, rendering:)
21
21
  klass = scope_class(name, rendering: rendering)
22
22
 
23
23
  klass.new(name: name, locals: locals, rendering: rendering)
@@ -27,11 +27,11 @@ module Hanami
27
27
 
28
28
  def scope_class(name = nil, rendering:)
29
29
  if name.nil?
30
- rendering.config.scope_class
30
+ rendering.scope_class
31
31
  elsif name.is_a?(Class)
32
32
  name
33
33
  else
34
- View.cache.fetch_or_store(name, rendering.config) do
34
+ View.cache.fetch_or_store(name, rendering.cache_key) do
35
35
  resolve_scope_class(name: name, rendering: rendering)
36
36
  end
37
37
  end
@@ -40,12 +40,12 @@ module Hanami
40
40
  def resolve_scope_class(name:, rendering:)
41
41
  name = rendering.inflector.camelize(name.to_s)
42
42
 
43
- namespace = rendering.config.scope_namespace
43
+ namespace = rendering.scope_namespace
44
44
 
45
45
  # Give autoloaders a chance to act
46
46
  begin
47
47
  klass = namespace.const_get(name)
48
- rescue NameError # rubocop:disable Lint/HandleExceptions
48
+ rescue NameError # rubocop:disable Lint/SuppressedException
49
49
  end
50
50
 
51
51
  if !klass && namespace.const_defined?(name, false)
@@ -55,7 +55,7 @@ module Hanami
55
55
  if klass && klass < Scope
56
56
  klass
57
57
  else
58
- rendering.config.scope_class
58
+ rendering.scope_class
59
59
  end
60
60
  end
61
61
  end
@@ -34,7 +34,7 @@ module Hanami
34
34
  Template = Temple::Templates::Tilt(
35
35
  ::Haml::Engine,
36
36
  use_html_safe: true,
37
- capture_generator: HTMLSafeStringBuffer,
37
+ capture_generator: HTMLSafeStringBuffer
38
38
  )
39
39
  end
40
40
  end
@@ -34,7 +34,7 @@ module Hanami
34
34
  Template = Temple::Templates::Tilt(
35
35
  ::Slim::Engine,
36
36
  use_html_safe: true,
37
- capture_generator: HTMLSafeStringBuffer,
37
+ capture_generator: HTMLSafeStringBuffer
38
38
  )
39
39
  end
40
40
  end
@@ -10,14 +10,24 @@ module Hanami
10
10
  # @api private
11
11
  # @since 2.1.0
12
12
  Mapping = ::Tilt.default_mapping.dup.tap { |mapping|
13
- # If "slim" has been required before "hanami/view", unregister Slim's non-lazy registered
14
- # template, so our own template adapter (using register_lazy below) can take precedence.
15
- mapping.unregister "slim"
13
+ # Unregister any existing mappings for the extensions we provide our own engines for.
14
+ #
15
+ # Tilt preferences non-lazy registrations over lazy ones (see `Tilt::Mapping#lookup`). So if
16
+ # "haml" or "slim" has been required before this mapping is built (each of which registers
17
+ # its own non-lazy mapping with Tilt), our own lazy-registered adapters below would be
18
+ # shadowed and never picked up, and templates would render without our specific required
19
+ # behavior.
20
+ #
21
+ # Unregistering first ensures our engines are always used, regardless of load order.
22
+ mapping.unregister "erb", "rhtml", "haml", "slim"
16
23
 
17
24
  # Register our own ERB template.
18
25
  mapping.register_lazy "Hanami::View::ERB::Template", "hanami/view/erb/template", "erb", "rhtml"
19
26
 
20
- # Register ERB templates for Haml and Slim that set the `use_html_safe: true` option.
27
+ # Register templates for Haml and Slim that set the `use_html_safe: true` option.
28
+ #
29
+ # We register these lazily so that the optional "haml" and "slim" gems only need to be
30
+ # installed when their templates are actually used.
21
31
  #
22
32
  # Our template namespaces below have the "Adapter" suffix to work around a bug in Tilt's
23
33
  # `Mapping#const_defined?`, which (if slim was already required) would receive
@@ -3,7 +3,6 @@
3
3
  module Hanami
4
4
  class View
5
5
  # @api public
6
- # @since 0.1.0
7
- VERSION = "2.3.1"
6
+ VERSION = "3.0.0"
8
7
  end
9
8
  end
data/lib/hanami/view.rb CHANGED
@@ -22,7 +22,6 @@ module Hanami
22
22
  # @since 2.1.0
23
23
  class View
24
24
  # @api private
25
- # @since 2.1.0
26
25
  def self.gem_loader
27
26
  @gem_loader ||= Zeitwerk::Loader.new.tap do |loader|
28
27
  root = File.expand_path("..", __dir__)
@@ -32,12 +31,17 @@ module Hanami
32
31
  "#{root}/hanami-view.rb",
33
32
  "#{root}/hanami/view/version.rb",
34
33
  "#{root}/hanami/view/errors.rb",
34
+ # These adapters require the optional "haml" and "slim" gems. They are loaded lazily by
35
+ # Tilt (see Hanami::View::Tilt) only when their respective template engines are used;
36
+ # ignore them here to allow eager loading without those gems installed.
37
+ "#{root}/hanami/view/tilt/haml_adapter.rb",
38
+ "#{root}/hanami/view/tilt/slim_adapter.rb"
35
39
  )
36
40
  loader.inflector = Zeitwerk::GemInflector.new("#{root}/hanami-view.rb")
37
41
  loader.inflector.inflect(
38
42
  "erb" => "ERB",
39
43
  "html" => "HTML",
40
- "html_safe_string_buffer" => "HTMLSafeStringBuffer",
44
+ "html_safe_string_buffer" => "HTMLSafeStringBuffer"
41
45
  )
42
46
  end
43
47
  end
@@ -45,7 +49,6 @@ module Hanami
45
49
  gem_loader.setup
46
50
 
47
51
  # @api private
48
- # @since 2.1.0
49
52
  DEFAULT_RENDERER_OPTIONS = {default_encoding: "utf-8"}.freeze
50
53
 
51
54
  include Dry::Equalizer(:config, :exposures)
@@ -235,6 +238,23 @@ module Hanami
235
238
  # @!scope class
236
239
  setting :inflector, default: Dry::Inflector.new
237
240
 
241
+ # @overload config.decorate_exposures=(value)
242
+ # Controls whether exposures are decorated by default.
243
+ #
244
+ # When set to `true`, all exposures will be decorated with matching Parts unless explicitly
245
+ # marked with `decorate: false`.
246
+ #
247
+ # When set to `false` (the default), exposures will not be decorated unless explicitly marked
248
+ # with `decorate: true`, or declared with `decorate`.
249
+ #
250
+ # Defaults to `false`.
251
+ #
252
+ # @param value [Boolean] whether to decorate exposures by default
253
+ # @api public
254
+ # @since 3.0.0
255
+ # @!scope class
256
+ setting :decorate_exposures, default: false
257
+
238
258
  # @overload config.renderer_options=(options)
239
259
  # A hash of options to pass to the template engine. Template engines are
240
260
  # provided by Tilt; see Tilt's documentation for what options your
@@ -273,7 +293,6 @@ module Hanami
273
293
  # @!endgroup
274
294
 
275
295
  # @api private
276
- # @since 2.1.0
277
296
  def self.inherited(klass)
278
297
  super
279
298
 
@@ -288,13 +307,13 @@ module Hanami
288
307
  # @param options [Hash] the exposure's options
289
308
  # @option options [Boolean] :layout expose this value to the layout (defaults to false)
290
309
  # @option options [Boolean] :decorate decorate this value in a matching Part (defaults to
291
- # true)
310
+ # false, or the value of `config.decorate_exposures`)
292
311
  # @option options [Symbol, Class] :as an alternative name or class to use when finding a
293
312
  # matching Part
294
313
 
295
314
  # @overload expose(name, **options, &block)
296
315
  # Define a value to be passed to the template. The return value of the
297
- # block will be decorated by a matching Part and passed to the template.
316
+ # block will be passed to the template.
298
317
  #
299
318
  # The block will be evaluated with the view instance as its `self`. The
300
319
  # block's parameters will determine what it is given:
@@ -329,8 +348,8 @@ module Hanami
329
348
  #
330
349
  # @overload expose(name, **options)
331
350
  # Define a value to be passed to the template, provided by an instance
332
- # method matching the name. The method's return value will be decorated by
333
- # a matching Part and passed to the template.
351
+ # method matching the name. The method's return value will be passed to
352
+ # the template.
334
353
  #
335
354
  # The method's parameters will determine what it is given:
336
355
  #
@@ -369,8 +388,8 @@ module Hanami
369
388
  #
370
389
  # @overload expose(name, **options)
371
390
  # Define a single value to pass through from the input data (when there is
372
- # no instance method matching the `name`). This value will be decorated by
373
- # a matching Part and passed to the template.
391
+ # no instance method matching the `name`). This value will be passed to
392
+ # the template.
374
393
  #
375
394
  # @param name [Symbol] name for the exposure
376
395
  # @macro exposure_options
@@ -380,7 +399,7 @@ module Hanami
380
399
  # @overload expose(*names, **options)
381
400
  # Define multiple values to pass through from the input data (when there
382
401
  # is no instance methods matching their names). These values will be
383
- # decorated by matching Parts and passed through to the template.
402
+ # passed through to the template.
384
403
  #
385
404
  # The provided options will be applied to all the exposures.
386
405
  #
@@ -411,12 +430,23 @@ module Hanami
411
430
  expose(*names, **options, private: true, &block)
412
431
  end
413
432
 
433
+ # Defines an exposure that will be decorated with a matching Part.
434
+ #
435
+ # This is a shorthand for `expose(..., decorate: true)`.
436
+ #
437
+ # @see expose
438
+ #
439
+ # @api public
440
+ # @since 2.1.0
441
+ def self.decorate(*names, **options, &block)
442
+ expose(*names, **options, decorate: true, &block)
443
+ end
444
+
414
445
  # Returns the defined exposures. These are unbound, since bound exposures
415
446
  # are only created when initializing a View instance.
416
447
  #
417
448
  # @return [Exposures]
418
449
  # @api private
419
- # @since 2.1.0
420
450
  def self.exposures
421
451
  @exposures ||= Exposures.new
422
452
  end
@@ -512,13 +542,6 @@ module Hanami
512
542
  # @!endgroup
513
543
 
514
544
  # @api private
515
- # @since 2.1.0
516
- def self.layout_path(layout)
517
- File.join(*[config.layouts_dir, layout].compact)
518
- end
519
-
520
- # @api private
521
- # @since 2.1.0
522
545
  def self.cache
523
546
  Cache
524
547
  end
@@ -534,6 +557,7 @@ module Hanami
534
557
  self.class.config.finalize!
535
558
  ensure_config
536
559
 
560
+ @config_data = config.to_data
537
561
  @exposures = self.class.exposures.bind(self)
538
562
  end
539
563
 
@@ -550,8 +574,7 @@ module Hanami
550
574
  # @return [Exposures]
551
575
  #
552
576
  # @api private
553
- # @since 2.1.0
554
- def exposures
577
+ def exposures # rubocop:disable Style/TrivialAccessors
555
578
  @exposures
556
579
  end
557
580
 
@@ -566,16 +589,20 @@ module Hanami
566
589
  #
567
590
  # @api public
568
591
  # @since 2.1.0
569
- def call(format: config.default_format, context: config.default_context, layout: config.layout, **input)
592
+ def call(format: config_data.default_format,
593
+ context: config_data.default_context,
594
+ layout: config_data.layout,
595
+ **input)
570
596
  rendering = self.rendering(format: format, context: context)
597
+ scope_class = config_data.scope
571
598
 
572
599
  locals = locals(rendering, input)
573
- output = rendering.template(config.template, rendering.scope(config.scope, locals))
600
+ output = rendering.template(config_data.template, rendering.scope(scope_class, locals))
574
601
 
575
602
  if layout
576
603
  output = rendering.template(
577
- self.class.layout_path(layout),
578
- rendering.scope(config.scope, layout_locals(locals))
604
+ layout_path(layout),
605
+ rendering.scope(scope_class, layout_locals(locals))
579
606
  ) { output }
580
607
  end
581
608
 
@@ -583,13 +610,18 @@ module Hanami
583
610
  end
584
611
 
585
612
  # @api private
586
- # @since 2.1.0
587
- def rendering(format: config.default_format, context: config.default_context)
588
- Rendering.new(config: config, format: format, context: context)
613
+ def rendering(format: config_data.default_format, context: config_data.default_context)
614
+ Rendering.new(config_data:, format:, context:)
589
615
  end
590
616
 
591
617
  private
592
618
 
619
+ # Frozen Data snapshot of the view's resolved configuration values.
620
+ # Used for fast hot-path reads during rendering.
621
+ #
622
+ # @api private
623
+ attr_reader :config_data
624
+
593
625
  def ensure_config
594
626
  raise UndefinedConfigError, :paths unless Array(config.paths).any?
595
627
  raise UndefinedConfigError, :template unless config.template
@@ -597,7 +629,7 @@ module Hanami
597
629
 
598
630
  def locals(rendering, input)
599
631
  exposures.(context: rendering.context, **input) do |value, exposure|
600
- if exposure.decorate? && value
632
+ if exposure.decorate?(default: config_data.decorate_exposures) && value
601
633
  rendering.part(exposure.name, value, as: exposure.options[:as])
602
634
  else
603
635
  value
@@ -605,6 +637,11 @@ module Hanami
605
637
  end
606
638
  end
607
639
 
640
+ # @api private
641
+ def layout_path(layout)
642
+ File.join(*[config_data.layouts_dir, layout].compact)
643
+ end
644
+
608
645
  def layout_locals(locals)
609
646
  locals.each_with_object({}) do |(key, value), layout_locals|
610
647
  layout_locals[key] = value if exposures[key].for_layout?