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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +399 -184
- 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 +5 -7
- 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 +62 -30
- metadata +14 -53
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
|
-
|
|
1
|
+
<!--- This file is synced from hanakai-rb/repo-sync -->
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
|
|
8
|
+
# Hanami View [][rubygem] [][actions]
|
|
6
9
|
|
|
7
|
-
[][forum]
|
|
11
|
+
[][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
|
-
##
|
|
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
|
-
|
|
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
|
|
7
|
+
require "hanami/view/version"
|
|
6
8
|
|
|
7
9
|
Gem::Specification.new do |spec|
|
|
8
|
-
spec.name =
|
|
10
|
+
spec.name = "hanami-view"
|
|
9
11
|
spec.authors = ["Hanakai team"]
|
|
10
12
|
spec.email = ["info@hanakai.org"]
|
|
11
|
-
spec.license =
|
|
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 =
|
|
18
|
+
spec.homepage = "https://hanamirb.org"
|
|
17
19
|
spec.files = Dir["CHANGELOG.md", "LICENSE", "README.md", "hanami-view.gemspec", "lib/**/*"]
|
|
18
|
-
spec.bindir =
|
|
19
|
-
spec.executables = []
|
|
20
|
-
spec.require_paths = [
|
|
21
|
-
|
|
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[
|
|
24
|
-
spec.metadata[
|
|
25
|
-
spec.metadata[
|
|
26
|
-
spec.metadata[
|
|
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.
|
|
31
|
+
spec.required_ruby_version = ">= 3.3"
|
|
29
32
|
|
|
30
|
-
spec.add_runtime_dependency "dry-configurable", "~> 1.
|
|
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
|
+
|
data/lib/hanami/view/context.rb
CHANGED
|
@@ -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
|
|
@@ -25,7 +25,8 @@ module Hanami
|
|
|
25
25
|
content.pop
|
|
26
26
|
end
|
|
27
27
|
|
|
28
|
-
[
|
|
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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
|
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
|
|
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
|
data/lib/hanami/view/exposure.rb
CHANGED
|
@@ -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,
|
|
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
|
-
|
|
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.
|
|
58
|
-
|
|
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
|
|
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 =
|
|
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 =>
|
|
139
|
-
raise
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 ||=
|
|
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
|
data/lib/hanami/view/html.rb
CHANGED
|
@@ -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.
|
data/lib/hanami/view/part.rb
CHANGED
|
@@ -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
|
|
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
|
|
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.
|
|
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.
|
|
78
|
-
return rendering.
|
|
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/
|
|
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.
|
|
94
|
+
rendering.part_class
|
|
96
95
|
end
|
|
97
96
|
end
|
|
98
|
-
# rubocop:enable Metrics/PerceivedComplexity
|
|
99
97
|
end
|
|
100
98
|
end
|
|
101
99
|
end
|
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
|