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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +404 -183
- data/LICENSE +20 -0
- data/README.md +17 -22
- data/hanami-view.gemspec +19 -19
- data/lib/hanami/view/context.rb +11 -0
- data/lib/hanami/view/decorated_attributes.rb +0 -2
- data/lib/hanami/view/erb/filters/block.rb +2 -1
- data/lib/hanami/view/erb/filters/trimming.rb +14 -8
- data/lib/hanami/view/erb/parser.rb +5 -1
- data/lib/hanami/view/exposure.rb +16 -10
- data/lib/hanami/view/exposures.rb +6 -2
- data/lib/hanami/view/helpers/escape_helper.rb +3 -2
- data/lib/hanami/view/helpers/number_formatting_helper.rb +2 -4
- data/lib/hanami/view/helpers/tag_helper/tag_builder.rb +10 -7
- data/lib/hanami/view/helpers/tag_helper.rb +1 -3
- data/lib/hanami/view/html.rb +0 -2
- data/lib/hanami/view/html_safe_string_buffer.rb +1 -1
- data/lib/hanami/view/part.rb +2 -5
- data/lib/hanami/view/part_builder.rb +7 -9
- data/lib/hanami/view/renderer.rb +78 -28
- data/lib/hanami/view/rendering.rb +38 -22
- data/lib/hanami/view/rendering_missing.rb +11 -14
- data/lib/hanami/view/scope.rb +29 -15
- data/lib/hanami/view/scope_builder.rb +6 -6
- data/lib/hanami/view/tilt/haml_adapter.rb +1 -1
- data/lib/hanami/view/tilt/slim_adapter.rb +1 -1
- data/lib/hanami/view/version.rb +1 -2
- data/lib/hanami/view.rb +63 -31
- metadata +14 -53
data/lib/hanami/view/renderer.rb
CHANGED
|
@@ -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
|
-
#
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
|
|
28
|
-
|
|
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
|
-
|
|
52
|
+
result = lookup(name, format)
|
|
53
|
+
raise TemplateNotFoundError.new(name, format, config_data.paths) unless result
|
|
39
54
|
|
|
40
|
-
|
|
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,
|
|
94
|
+
View.cache.fetch_or_store(:lookup, name, format, config_data.object_id, prefixes) {
|
|
60
95
|
catch :found do
|
|
61
|
-
|
|
62
|
-
prefixes.
|
|
63
|
-
|
|
64
|
-
|
|
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,
|
|
83
|
-
Hanami::View::Tilt[path,
|
|
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
|
-
|
|
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
|
-
|
|
17
|
-
|
|
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
|
-
|
|
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 =
|
|
27
|
-
@part_builder =
|
|
28
|
-
@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(
|
|
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
|
|
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
|
data/lib/hanami/view/scope.rb
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
#
|
|
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
|
-
#
|
|
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:)
|
|
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.
|
|
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.
|
|
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.
|
|
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/
|
|
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.
|
|
58
|
+
rendering.scope_class
|
|
59
59
|
end
|
|
60
60
|
end
|
|
61
61
|
end
|