hanami-view 2.3.0 → 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.
@@ -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
@@ -3,54 +3,70 @@
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)
32
34
  end
33
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
+ #
41
+ # @api public
42
+ # @since x.x.x
43
+ def current_template_name
44
+ renderer.current_template_name
45
+ end
46
+
47
+ # Returns the stack of resolved names for the templates and partials currently being
48
+ # rendered.
49
+ #
50
+ # @return [Array<String>]
51
+ #
34
52
  # @api private
35
- # @since 2.1.0
53
+ # @since x.x.x
54
+ def current_template_names
55
+ renderer.current_template_names
56
+ end
57
+
36
58
  def template(name, scope, &block)
37
59
  renderer.template(name, format, scope, &block)
38
60
  end
39
61
 
40
- # @api private
41
- # @since 2.1.0
42
62
  def partial(name, scope, &block)
43
63
  renderer.partial(name, format, scope, &block)
44
64
  end
45
65
 
46
- # @api private
47
- # @since 2.1.0
48
66
  def part(name, value, as: nil)
49
67
  part_builder.(name, value, as: as, rendering: self)
50
68
  end
51
69
 
52
- # @api private
53
- # @since 2.1.0
54
70
  def scope(name = nil, locals) # rubocop:disable Style/OptionalArguments
55
71
  scope_builder.(name, locals: locals, rendering: self)
56
72
  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 x.x.x
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
@@ -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.0"
6
+ VERSION = "3.0.0.rc1"
8
7
  end
9
8
  end