hanami-view 2.3.1 → 3.0.0.rc1

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.
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2015-2026 Hanakai team
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
6
+ this software and associated documentation files (the "Software"), to deal in
7
+ the Software without restriction, including without limitation the rights to
8
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
9
+ the Software, and to permit persons to whom the Software is furnished to do so,
10
+ subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
17
+ FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
18
+ COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
19
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
20
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md CHANGED
@@ -1,22 +1,14 @@
1
- # Hanami::View
1
+ <!--- This file is synced from hanakai-rb/repo-sync -->
2
2
 
3
- A view layer for [Hanami](http://hanamirb.org)
3
+ [actions]: https://github.com/hanami/hanami-view/actions
4
+ [chat]: https://discord.gg/naQApPAsZB
5
+ [forum]: https://discourse.hanamirb.org
6
+ [rubygem]: https://rubygems.org/gems/hanami-view
4
7
 
5
- ## Status
8
+ # Hanami View [![Gem Version](https://badge.fury.io/rb/hanami-view.svg)][rubygem] [![CI Status](https://github.com/hanami/hanami-view/workflows/CI/badge.svg)][actions]
6
9
 
7
- [![Gem Version](https://badge.fury.io/rb/hanami-view.svg)](https://badge.fury.io/rb/hanami-view)
8
- [![CI](https://github.com/hanami/view/actions/workflows/ci.yml/badge.svg)](https://github.com/hanami/view/actions?query=workflow%3Aci+branch%3Amain)
9
-
10
- ## Contact
11
-
12
- * Home page: http://hanamirb.org
13
- * Community: http://hanamirb.org/community
14
- * Guides: https://guides.hanamirb.org
15
- * Mailing List: http://hanamirb.org/mailing-list
16
- * API Doc: http://rubydoc.info/gems/hanami-view
17
- * Chat: http://chat.hanamirb.org
18
-
19
- ## Rubies
10
+ [![Forum](https://img.shields.io/badge/Forum-dc360f?logo=discourse&logoColor=white)][forum]
11
+ [![Chat](https://img.shields.io/badge/Chat-717cf8?logo=discord&logoColor=white)][chat]
20
12
 
21
13
  ## Installation
22
14
 
@@ -38,10 +30,6 @@ Or install it yourself as:
38
30
  $ gem install hanami-view
39
31
  ```
40
32
 
41
- ## Versioning
42
-
43
- __Hanami::View__ uses [Semantic Versioning 2.0.0](http://semver.org)
44
-
45
33
  ## Contributing
46
34
 
47
35
  1. Fork it
@@ -50,6 +38,13 @@ __Hanami::View__ uses [Semantic Versioning 2.0.0](http://semver.org)
50
38
  4. Push to the branch (`git push origin my-new-feature`)
51
39
  5. Create new Pull Request
52
40
 
53
- ## Copyright
41
+ ## Links
42
+
43
+ - [User documentation](https://hanamirb.org)
44
+ - [API documentation](http://rubydoc.info/gems/hanami-view)
45
+
46
+
47
+ ## License
48
+
49
+ See `LICENSE` file.
54
50
 
55
- Copyright © 2014–2024 Hanami Team – Released under MIT License
data/hanami-view.gemspec CHANGED
@@ -1,40 +1,40 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- lib = File.expand_path('lib', __dir__)
3
+ # This file is synced from hanakai-rb/repo-sync. To update it, edit repo-sync.yml.
4
+
5
+ lib = File.expand_path("lib", __dir__)
4
6
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
- require 'hanami/view/version'
7
+ require "hanami/view/version"
6
8
 
7
9
  Gem::Specification.new do |spec|
8
- spec.name = 'hanami-view'
10
+ spec.name = "hanami-view"
9
11
  spec.authors = ["Hanakai team"]
10
12
  spec.email = ["info@hanakai.org"]
11
- spec.license = 'MIT'
13
+ spec.license = "MIT"
12
14
  spec.version = Hanami::View::VERSION.dup
13
15
 
14
16
  spec.summary = "A complete, standalone view rendering system that gives you everything you need to write well-factored view code"
15
17
  spec.description = spec.summary
16
- spec.homepage = 'https://hanamirb.org'
18
+ spec.homepage = "https://hanamirb.org"
17
19
  spec.files = Dir["CHANGELOG.md", "LICENSE", "README.md", "hanami-view.gemspec", "lib/**/*"]
18
- spec.bindir = 'bin'
19
- spec.executables = []
20
- spec.require_paths = ['lib']
21
- spec.metadata["rubygems_mfa_required"] = "true"
20
+ spec.bindir = "exe"
21
+ spec.executables = Dir["exe/*"].map { |f| File.basename(f) }
22
+ spec.require_paths = ["lib"]
23
+
24
+ spec.extra_rdoc_files = ["README.md", "CHANGELOG.md", "LICENSE"]
22
25
 
23
- spec.metadata['allowed_push_host'] = 'https://rubygems.org'
24
- spec.metadata['changelog_uri'] = 'https://github.com/hanami/view/blob/main/CHANGELOG.md'
25
- spec.metadata['source_code_uri'] = 'https://github.com/hanami/view'
26
- spec.metadata['bug_tracker_uri'] = 'https://github.com/hanami/view/issues'
26
+ spec.metadata["changelog_uri"] = "https://github.com/hanami/hanami-view/blob/main/CHANGELOG.md"
27
+ spec.metadata["source_code_uri"] = "https://github.com/hanami/hanami-view"
28
+ spec.metadata["bug_tracker_uri"] = "https://github.com/hanami/hanami-view/issues"
29
+ spec.metadata["funding_uri"] = "https://github.com/sponsors/hanami"
27
30
 
28
- spec.required_ruby_version = ">= 3.2"
31
+ spec.required_ruby_version = ">= 3.3"
29
32
 
30
- spec.add_runtime_dependency "dry-configurable", "~> 1.0"
33
+ spec.add_runtime_dependency "dry-configurable", "~> 1.4"
31
34
  spec.add_runtime_dependency "dry-core", "~> 1.0"
32
35
  spec.add_runtime_dependency "dry-inflector", "~> 1.0", "< 2"
33
36
  spec.add_runtime_dependency "temple", "~> 0.10.0", ">= 0.10.2"
34
37
  spec.add_runtime_dependency "tilt", "~> 2.3"
35
38
  spec.add_runtime_dependency "zeitwerk", "~> 2.6"
36
-
37
- spec.add_development_dependency "bundler"
38
- spec.add_development_dependency "rake"
39
- spec.add_development_dependency "rspec"
40
39
  end
40
+
@@ -40,6 +40,17 @@ module Hanami
40
40
  obj.instance_variable_set(:@_rendering, rendering)
41
41
  end
42
42
  end
43
+
44
+ # Returns the name of the template or partial currently being rendered, or nil if no render is
45
+ # in progress.
46
+ #
47
+ # @return [String, nil]
48
+ #
49
+ # @api public
50
+ # @since x.x.x
51
+ def current_template_name
52
+ _rendering.current_template_name
53
+ end
43
54
  end
44
55
  end
45
56
  end
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "set"
4
-
5
3
  module Hanami
6
4
  class View
7
5
  # Decorates attributes in Parts.
@@ -25,7 +25,8 @@ module Hanami
25
25
  content.pop
26
26
  end
27
27
 
28
- [:multi,
28
+ [
29
+ :multi,
29
30
  # Capture the result of the code in a variable. We can't do `[:dynamic, code]` because
30
31
  # it's probably not a complete expression (which is a requirement for Temple).
31
32
  [:code, "#{tmp} = #{code}"],
@@ -22,19 +22,25 @@ module Hanami
22
22
  class Trimming < Temple::Filter
23
23
  define_options trim: true
24
24
 
25
+ # rubocop:disable Metrics/PerceivedComplexity
26
+
25
27
  def on_multi(*exps)
26
- exps = exps.each_with_index.map do |e,i|
27
- if e.first == :static && i > 0 && exps[i-1].first == :code
28
- [:static, e.last.lstrip]
29
- elsif e.first == :static && i < exps.size-1 && exps[i+1].first == :code
30
- [:static, e.last.rstrip]
31
- else
32
- compile(e)
28
+ if options[:trim]
29
+ exps = exps.each_with_index.map do |e, i|
30
+ if e.first == :static && i > 0 && exps[i - 1].first == :code
31
+ [:static, e.last.lstrip]
32
+ elsif e.first == :static && i < exps.size - 1 && exps[i + 1].first == :code
33
+ [:static, e.last.rstrip]
34
+ else
35
+ compile(e)
36
+ end
33
37
  end
34
- end if options[:trim]
38
+ end
35
39
 
36
40
  [:multi, *exps]
37
41
  end
42
+
43
+ # rubocop:enable Metrics/PerceivedComplexity
38
44
  end
39
45
  end
40
46
  end
@@ -86,6 +86,8 @@ module Hanami
86
86
  BLOCK_LINE_RE = /\s*((\s+|\))do|\{)(\s*\|[^|]*\|)?\s*\Z/
87
87
  END_LINE_RE = /\bend\b/
88
88
 
89
+ # rubocop:disable Metrics/AbcSize, Metrics/PerceivedComplexity
90
+
89
91
  def call(input)
90
92
  results = [[:multi]]
91
93
  pos = 0
@@ -153,8 +155,10 @@ module Hanami
153
155
  end
154
156
 
155
157
  # Add any text after the final ERB tag
156
- results.last << [:static, input[pos..-1]]
158
+ results.last << [:static, input[pos..]]
157
159
  end
160
+
161
+ # rubocop:enable Metrics/AbcSize, Metrics/PerceivedComplexity
158
162
  end
159
163
  end
160
164
  end
@@ -85,8 +85,8 @@ module Hanami
85
85
 
86
86
  # @api private
87
87
  # @since 2.1.0
88
- def decorate?
89
- options.fetch(:decorate, true)
88
+ def decorate?(default: false)
89
+ options.fetch(:decorate, default)
90
90
  end
91
91
 
92
92
  # @api private
@@ -122,12 +122,10 @@ module Hanami
122
122
  else
123
123
  object.instance_exec(*args, &proc)
124
124
  end
125
+ elsif proc.is_a?(Method)
126
+ proc.(*args, **keywords)
125
127
  else
126
- if proc.is_a?(Method)
127
- proc.(*args, **keywords)
128
- else
129
- object.instance_exec(*args, **keywords, &proc)
130
- end
128
+ object.instance_exec(*args, **keywords, &proc)
131
129
  end
132
130
  end
133
131
 
@@ -54,13 +54,15 @@ module Hanami
54
54
  # @api private
55
55
  # @since 2.1.0
56
56
  def bind(obj)
57
- bound_exposures = exposures.each_with_object({}) { |(name, exposure), memo|
58
- memo[name] = exposure.bind(obj)
57
+ bound_exposures = exposures.transform_values { |exposure|
58
+ exposure.bind(obj)
59
59
  }
60
60
 
61
61
  self.class.new(bound_exposures)
62
62
  end
63
63
 
64
+ # rubocop:disable Metrics/PerceivedComplexity
65
+
64
66
  # @api private
65
67
  # @since 2.1.0
66
68
  def call(input)
@@ -88,6 +90,8 @@ module Hanami
88
90
  }
89
91
  end
90
92
 
93
+ # rubocop:enable Metrics/PerceivedComplexity
94
+
91
95
  private
92
96
 
93
97
  def tsort_each_node(&block)
@@ -160,7 +160,7 @@ module Hanami
160
160
 
161
161
  return starting_char if name.size == 1
162
162
 
163
- following_chars = name[1..-1]
163
+ following_chars = name[1..]
164
164
  following_chars.gsub!(INVALID_TAG_NAME_FOLLOWING_REGEXP, TAG_NAME_REPLACEMENT_CHAR)
165
165
 
166
166
  starting_char << following_chars
@@ -186,7 +186,8 @@ module Hanami
186
186
 
187
187
  # @api private
188
188
  # @since 2.1.0
189
- TAG_NAME_FOLLOWING_CODEPOINTS = "#{TAG_NAME_START_CODEPOINTS}\\-.0-9\u{B7}\u{0300}-\u{036F}\u{203F}-\u{2040}"
189
+ TAG_NAME_FOLLOWING_CODEPOINTS =
190
+ "#{TAG_NAME_START_CODEPOINTS}\\-.0-9\u{B7}\u{0300}-\u{036F}\u{203F}-\u{2040}".freeze
190
191
  private_constant :TAG_NAME_FOLLOWING_CODEPOINTS
191
192
 
192
193
  # @api private
@@ -87,8 +87,6 @@ module Hanami
87
87
  Formatter.call(number, delimiter: delimiter, separator: separator, precision: precision)
88
88
  end
89
89
 
90
- private
91
-
92
90
  # Formatter
93
91
  #
94
92
  # @since 2.1.0
@@ -135,8 +133,8 @@ module Hanami
135
133
  Float(number)
136
134
  rescue TypeError
137
135
  raise ArgumentError, "failed to convert #{number.inspect} to float"
138
- rescue ArgumentError => e
139
- raise e.class, "failed to convert #{number.inspect} to float"
136
+ rescue ArgumentError => exception
137
+ raise exception.class, "failed to convert #{number.inspect} to float"
140
138
  end
141
139
  end
142
140
  end
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "json"
4
- require "set"
5
4
 
6
5
  module Hanami
7
6
  class View
@@ -16,15 +15,15 @@ module Hanami
16
15
  class TagBuilder
17
16
  # @api private
18
17
  # @since 2.1.0
19
- HTML_VOID_ELEMENTS = %i(
18
+ HTML_VOID_ELEMENTS = %i[
20
19
  area base br col embed hr img input keygen link meta param source track wbr
21
- ).to_set
20
+ ].to_set
22
21
 
23
22
  # @api private
24
23
  # @since 2.1.0
25
- SVG_SELF_CLOSING_ELEMENTS = %i(
24
+ SVG_SELF_CLOSING_ELEMENTS = %i[
26
25
  animate animateMotion animateTransform circle ellipse line path polygon polyline rect set stop use view
27
- ).to_set
26
+ ].to_set
28
27
 
29
28
  # @api private
30
29
  # @since 2.1.0
@@ -32,7 +31,7 @@ module Hanami
32
31
 
33
32
  # @api private
34
33
  # @since 2.1.0
35
- BOOLEAN_ATTRIBUTES = %w(
34
+ BOOLEAN_ATTRIBUTES = %w[
36
35
  allowfullscreen allowpaymentrequest async autofocus
37
36
  autoplay checked compact controls declare default
38
37
  defaultchecked defaultmuted defaultselected defer
@@ -42,7 +41,7 @@ module Hanami
42
41
  pauseonexit playsinline readonly required reversed
43
42
  scoped seamless selected sortable truespeed
44
43
  typemustmatch visible
45
- ).to_set
44
+ ].to_set
46
45
  BOOLEAN_ATTRIBUTES.merge(BOOLEAN_ATTRIBUTES.map(&:to_sym))
47
46
  BOOLEAN_ATTRIBUTES.freeze
48
47
 
@@ -125,6 +124,8 @@ module Hanami
125
124
  "<#{name}#{tag_options}>#{PRE_CONTENT_STRINGS[name]}#{content}</#{name}>".html_safe
126
125
  end
127
126
 
127
+ # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
128
+
128
129
  # @api private
129
130
  # @since 2.1.0
130
131
  def tag_options(**options)
@@ -173,6 +174,8 @@ module Hanami
173
174
  output unless output.empty?
174
175
  end
175
176
 
177
+ # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
178
+
176
179
  # @api private
177
180
  # @since 2.1.0
178
181
  def boolean_tag_option(key)
@@ -187,9 +187,7 @@ module Hanami
187
187
  # @api private
188
188
  # @since 2.1.0
189
189
  def tag_builder
190
- @tag_builder ||= begin
191
- TagBuilder.new(inflector: tag_builder_inflector)
192
- end
190
+ @tag_builder ||= TagBuilder.new(inflector: tag_builder_inflector)
193
191
  end
194
192
 
195
193
  # @api private
@@ -105,5 +105,3 @@ class Numeric
105
105
  true
106
106
  end
107
107
  end
108
-
109
-
@@ -34,7 +34,7 @@ module Hanami
34
34
  # being called. For our needs, `#return_buffer` must be called at all times in order to ensure
35
35
  # the captured string is consistently marked as `.html_safe`.
36
36
  def call(exp)
37
- [preamble, compile(exp), postamble].flatten.compact.join('; ')
37
+ [preamble, compile(exp), postamble].flatten.compact.join("; ")
38
38
  end
39
39
 
40
40
  # Marks the string returned from the captured buffer as HTML safe.
@@ -78,9 +78,9 @@ module Hanami
78
78
  # @api public
79
79
  # @since 2.1.0
80
80
  def initialize(
81
+ value:,
81
82
  rendering: RenderingMissing.new,
82
- name: self.class.part_name(rendering.inflector),
83
- value:
83
+ name: self.class.part_name(rendering.inflector)
84
84
  )
85
85
  @_name = name
86
86
  @_value = value
@@ -119,8 +119,6 @@ module Hanami
119
119
  _rendering.context
120
120
  end
121
121
 
122
- # rubocop:disable Naming/UncommunicativeMethodParamName
123
-
124
122
  # Renders a new partial with the part included in its locals.
125
123
  #
126
124
  # @overload _render(partial_name, as: _name, **locals, &block)
@@ -141,7 +139,6 @@ module Hanami
141
139
  def _render(partial_name, as: _name, **locals, &block)
142
140
  _rendering.partial(partial_name, _rendering.scope({as => self}.merge(locals)), &block)
143
141
  end
144
- # rubocop:enable Naming/UncommunicativeMethodParamName
145
142
 
146
143
  # Builds a new scope with the part included in its locals.
147
144
  #
@@ -18,7 +18,7 @@ module Hanami
18
18
  #
19
19
  # @api public
20
20
  # @since 2.1.0
21
- def call(name, value, as: nil, rendering:)
21
+ def call(name, value, rendering:, as: nil)
22
22
  builder = value.respond_to?(:to_ary) ? :build_collection_part : :build_part
23
23
 
24
24
  send(builder, name: name, value: value, as: as, rendering: rendering)
@@ -32,7 +32,7 @@ module Hanami
32
32
  klass.new(name: name, value: value, rendering: rendering)
33
33
  end
34
34
 
35
- def build_collection_part(name:, value:, as: nil, rendering:)
35
+ def build_collection_part(name:, value:, rendering:, as: nil)
36
36
  item_name, item_as = collection_item_name_as(name, as, inflector: rendering.inflector)
37
37
  item_part_class = part_class(name: item_name, as: item_as, rendering: rendering)
38
38
 
@@ -66,23 +66,22 @@ module Hanami
66
66
  if name.is_a?(Class)
67
67
  name
68
68
  else
69
- View.cache.fetch_or_store(:part_class, name, rendering.config) do
69
+ View.cache.fetch_or_store(:part_class, name, rendering.cache_key) do
70
70
  resolve_part_class(name: name, rendering: rendering)
71
71
  end
72
72
  end
73
73
  end
74
74
 
75
- # rubocop:disable Metrics/PerceivedComplexity
76
75
  def resolve_part_class(name:, rendering:)
77
- namespace = rendering.config.part_namespace
78
- return rendering.config.part_class unless namespace
76
+ namespace = rendering.part_namespace
77
+ return rendering.part_class unless namespace
79
78
 
80
79
  name = rendering.inflector.camelize(name.to_s)
81
80
 
82
81
  # Give autoloaders a chance to act
83
82
  begin
84
83
  klass = namespace.const_get(name)
85
- rescue NameError # rubocop:disable Lint/HandleExceptions
84
+ rescue NameError # rubocop:disable Lint/SuppressedException
86
85
  end
87
86
 
88
87
  if !klass && namespace.const_defined?(name, false)
@@ -92,10 +91,9 @@ module Hanami
92
91
  if klass && klass < Part
93
92
  klass
94
93
  else
95
- rendering.config.part_class
94
+ rendering.part_class
96
95
  end
97
96
  end
98
- # rubocop:enable Metrics/PerceivedComplexity
99
97
  end
100
98
  end
101
99
  end
@@ -1,73 +1,123 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "pathname"
3
4
  require_relative "errors"
4
5
 
5
6
  module Hanami
6
7
  class View
8
+ # Resolves a template by name and renders it through Tilt.
9
+ #
10
+ # Template lookup combines two pieces of state:
11
+ #
12
+ # - **`config.paths`** — the configured view paths, immutable for the lifetime of the
13
+ # renderer. When multiple view paths are configured, earlier ones override later ones.
14
+ # - **`@prefixes`** — a stack of subdirectories within each view path to search, mutated
15
+ # during rendering. It starts at `[CURRENT_PATH_PREFIX]` (the root itself). When a template is
16
+ # rendered, its parent directory (e.g. `"users"` for `"users/index"`) is pushed onto the stack
17
+ # so that a partial referenced by its bare name (e.g. `render("form")` from inside
18
+ # `users/index.html.erb`) can be found alongside the template that renders it. The stack is
19
+ # snapshot-and-restored around each render via `ensure`.
20
+ #
21
+ # `#lookup` tries every combination of a path and a prefix, joining each pair with the
22
+ # requested name to find a matching file. `paths` are checked in configured order; an earlier
23
+ # entry overrides a later one. `prefixes` are checked oldest-first: a partial at the root
24
+ # wins over a same-named partial in a directory pushed onto the stack mid-render. First match
25
+ # wins.
26
+ #
7
27
  # @api private
8
- # @since 2.1.0
9
28
  class Renderer
10
- # @api private
11
- # @since 2.1.0
12
29
  PARTIAL_PREFIX = "_"
13
-
14
- # @api private
15
- # @since 2.1.0
16
30
  PATH_DELIMITER = "/"
17
-
18
- # @api private
19
- # @since 2.1.0
20
31
  CURRENT_PATH_PREFIX = "."
21
32
 
22
- # @api private
23
- # @since 2.1.0
24
- attr_reader :config, :prefixes
33
+ # Matches the `.format.engine` extensions on a template path (e.g. `.html.erb`).
34
+ EXTENSIONS_REGEXP = /\.[^.\/]+\.[^.\/]+\z/
35
+
36
+ # Stack of resolved names for the templates and partials currently being rendered. The top of
37
+ # the stack is the innermost render in progress.
38
+ #
39
+ # @return [Array<String>]
40
+ attr_reader :current_template_names
25
41
 
26
42
  # @api private
27
- # @since 2.1.0
28
- def initialize(config)
29
- @config = config
43
+ def initialize(config_data)
44
+ @config_data = config_data
30
45
  @prefixes = [CURRENT_PATH_PREFIX]
46
+ @current_template_names = []
31
47
  end
32
48
 
33
- # @api private
34
- # @since 2.1.0
35
49
  def template(name, format, scope, &block)
36
50
  old_prefixes = @prefixes.dup
37
51
 
38
- template_path = lookup(name, format)
52
+ result = lookup(name, format)
53
+ raise TemplateNotFoundError.new(name, format, config_data.paths) unless result
39
54
 
40
- raise TemplateNotFoundError.new(name, format, config.paths) unless template_path
55
+ template_path, relative_path = result
41
56
 
42
57
  new_prefix = File.dirname(name)
43
58
  @prefixes << new_prefix unless @prefixes.include?(new_prefix)
59
+ @current_template_names << resolve_template_name(relative_path)
44
60
 
45
61
  render(template_path, scope, &block)
46
62
  ensure
47
63
  @prefixes = old_prefixes
64
+ @current_template_names.pop if result
48
65
  end
49
66
 
50
- # @api private
51
- # @since 2.1.0
52
67
  def partial(name, format, scope, &block)
53
68
  template(name_for_partial(name), format, scope, &block)
54
69
  end
55
70
 
71
+ # Returns the resolved name of the template or partial currently being rendered, or nil if no
72
+ # render is in progress.
73
+ #
74
+ # The name is the file's path relative to the matching view path, with format/engine
75
+ # extensions stripped.
76
+ #
77
+ # @return [String, nil]
78
+ def current_template_name
79
+ @current_template_names.last
80
+ end
81
+
56
82
  private
57
83
 
84
+ attr_reader :config_data, :prefixes
85
+
86
+ # Searches `config.paths` (under each of the `prefixes`) for a template matching `name` and
87
+ # `format`. Returns the template's absolute file path (for rendering via Tilt) together with
88
+ # its path relative to the matching view path.
89
+ #
90
+ # Results are memoized via `View.cache` keyed on `(name, format, config_data, prefixes)`.
91
+ #
92
+ # @return [[String, String], nil]
58
93
  def lookup(name, format)
59
- View.cache.fetch_or_store(:lookup, name, format, config, prefixes) {
94
+ View.cache.fetch_or_store(:lookup, name, format, config_data.object_id, prefixes) {
60
95
  catch :found do
61
- config.paths.reduce(nil) do |_, path|
62
- prefixes.reduce(nil) do |_, prefix|
63
- result = path.lookup(prefix, name, format)
64
- throw :found, result if result
96
+ config_data.paths.each do |path|
97
+ prefixes.each do |prefix|
98
+ file_path = path.lookup(prefix, name, format)
99
+ if file_path
100
+ relative_path = Pathname.new(file_path).relative_path_from(path.dir).to_s
101
+ throw :found, [file_path, relative_path]
102
+ end
65
103
  end
66
104
  end
105
+ nil
67
106
  end
68
107
  }
69
108
  end
70
109
 
110
+ # Derives the rendered template's name from its relative path, suitable for tracking on
111
+ # `@current_template_names` and surfacing via `#current_template_name`.
112
+ #
113
+ # Strips format/engine extensions (e.g. `.html.erb`), so `"posts/_form.html.erb"` becomes
114
+ # `"posts/_form"`.
115
+ #
116
+ # @return [String]
117
+ def resolve_template_name(relative_path)
118
+ relative_path.sub(EXTENSIONS_REGEXP, "")
119
+ end
120
+
71
121
  def name_for_partial(name)
72
122
  segments = name.to_s.split(PATH_DELIMITER)
73
123
  segments[-1] = "#{PARTIAL_PREFIX}#{segments[-1]}"
@@ -79,8 +129,8 @@ module Hanami
79
129
  end
80
130
 
81
131
  def tilt(path)
82
- View.cache.fetch_or_store(:tilt, path, config) {
83
- Hanami::View::Tilt[path, config.renderer_engine_mapping, config.renderer_options]
132
+ View.cache.fetch_or_store(:tilt, path, config_data.object_id) {
133
+ Hanami::View::Tilt[path, config_data.renderer_engine_mapping, config_data.renderer_options]
84
134
  }
85
135
  end
86
136
  end