hanami-view 2.0.0.alpha8 → 2.1.0.beta2
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 +36 -0
- data/README.md +15 -3
- data/hanami-view.gemspec +5 -3
- data/lib/hanami/view/cache.rb +16 -0
- data/lib/hanami/view/context.rb +15 -55
- data/lib/hanami/view/context_helpers/content_helpers.rb +5 -5
- data/lib/hanami/view/decorated_attributes.rb +2 -2
- data/lib/hanami/view/erb/engine.rb +27 -0
- data/lib/hanami/view/erb/filters/block.rb +44 -0
- data/lib/hanami/view/erb/filters/trimming.rb +42 -0
- data/lib/hanami/view/erb/parser.rb +161 -0
- data/lib/hanami/view/erb/template.rb +30 -0
- data/lib/hanami/view/errors.rb +8 -2
- data/lib/hanami/view/exposure.rb +23 -17
- data/lib/hanami/view/exposures.rb +22 -13
- data/lib/hanami/view/helpers/escape_helper.rb +221 -0
- data/lib/hanami/view/helpers/number_formatting_helper.rb +182 -0
- data/lib/hanami/view/helpers/tag_helper/tag_builder.rb +230 -0
- data/lib/hanami/view/helpers/tag_helper.rb +210 -0
- data/lib/hanami/view/html.rb +104 -0
- data/lib/hanami/view/html_safe_string_buffer.rb +46 -0
- data/lib/hanami/view/part.rb +13 -15
- data/lib/hanami/view/part_builder.rb +68 -108
- data/lib/hanami/view/path.rb +4 -31
- data/lib/hanami/view/rendered.rb +31 -0
- data/lib/hanami/view/renderer.rb +36 -44
- data/lib/hanami/view/rendering.rb +42 -0
- data/lib/hanami/view/{render_environment_missing.rb → rendering_missing.rb} +8 -13
- data/lib/hanami/view/scope.rb +14 -15
- data/lib/hanami/view/scope_builder.rb +42 -78
- data/lib/hanami/view/tilt/haml_adapter.rb +40 -0
- data/lib/hanami/view/tilt/slim_adapter.rb +40 -0
- data/lib/hanami/view/tilt.rb +22 -46
- data/lib/hanami/view/version.rb +1 -1
- data/lib/hanami/view.rb +58 -99
- metadata +64 -26
- data/LICENSE +0 -20
- data/lib/hanami/view/render_environment.rb +0 -62
- data/lib/hanami/view/tilt/erb.rb +0 -26
- data/lib/hanami/view/tilt/erbse.rb +0 -21
- data/lib/hanami/view/tilt/haml.rb +0 -26
@@ -0,0 +1,210 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Based on ActionView::Helpers::TagHelper, also released under the MIT licence.
|
4
|
+
#
|
5
|
+
# Copyright (c) David Heinemeier Hansson
|
6
|
+
|
7
|
+
module Hanami
|
8
|
+
class View
|
9
|
+
module Helpers
|
10
|
+
# Helper methods for generating HTML tags.
|
11
|
+
#
|
12
|
+
# When using full Hanami apps, these helpers will be automatically available in your view
|
13
|
+
# templates, part classes and scope classes.
|
14
|
+
#
|
15
|
+
# When using hanami-view standalone, include this module directly in your base part and scope
|
16
|
+
# classes, or in specific classes as required.
|
17
|
+
#
|
18
|
+
# @example Standalone usage
|
19
|
+
# class BasePart < Hanami::View::Part
|
20
|
+
# include Hanami::View::Helpers::TagHelper
|
21
|
+
# end
|
22
|
+
#
|
23
|
+
# class BaseScope < Hanami::View::Scope
|
24
|
+
# include Hanami::View::Helpers::TagHelper
|
25
|
+
# end
|
26
|
+
#
|
27
|
+
# class BaseView < Hanami::View
|
28
|
+
# config.part_class = BasePart
|
29
|
+
# config.scope_class = BaseScope
|
30
|
+
# end
|
31
|
+
#
|
32
|
+
# @api public
|
33
|
+
# @since 2.0.0
|
34
|
+
module TagHelper
|
35
|
+
module_function
|
36
|
+
|
37
|
+
# Returns a tag builder for building HTML tag strings.
|
38
|
+
#
|
39
|
+
# @example General usage
|
40
|
+
# tag.div # => <div></div>
|
41
|
+
# tag.img # => <img>
|
42
|
+
#
|
43
|
+
# tag.div("hello") # => <div>hello</div>
|
44
|
+
# tag.div { "hello" } # => <div>hello</div>
|
45
|
+
# tag.div(tag.p("hello")) # => <div><p>hello</p></div>
|
46
|
+
#
|
47
|
+
# tag.div(class: ["a", "b"]) # => <div class="a b"></div>
|
48
|
+
# tag.div(class: {"a": true, "b": false}) # => <div class="a"></div>
|
49
|
+
#
|
50
|
+
# tag.div(id: "el", data: {x: "y"}) # => <div id="el" data-x="y"></div>
|
51
|
+
# tag.div(id: "el", aria: {x: "y"}) # => <div id="el" aria-x="y"></div>
|
52
|
+
#
|
53
|
+
# tag.custom_tag("hello") # => <custom-tag>hello</custom-tag>
|
54
|
+
#
|
55
|
+
# @example Escaping
|
56
|
+
# tag.p("<script>alert()</script>") # => <p><script>alert()</script></p>
|
57
|
+
# tag.p(class: "<script>alert()</script>") # => <p class="<script>alert()</script>"></p>
|
58
|
+
# tag.p("<em>safe content</em>".html_safe) # => <p><em>safe content</em></p>
|
59
|
+
#
|
60
|
+
# @example Within templates
|
61
|
+
# <%= tag.div(id: "el") do %>
|
62
|
+
# <p>Template content can be mixed in.</p>
|
63
|
+
# <%= tag.p("Also nested tag builders.") %>
|
64
|
+
# <% end %>
|
65
|
+
#
|
66
|
+
# @api public
|
67
|
+
# @since 2.0.0
|
68
|
+
def tag
|
69
|
+
tag_builder
|
70
|
+
end
|
71
|
+
|
72
|
+
# Returns an anchor tag for the given contents and URL.
|
73
|
+
#
|
74
|
+
# The tag's contents are automatically escaped (unless marked as HTML safe).
|
75
|
+
#
|
76
|
+
# Uses the {#tag} builder to prepare the tag, so all tag builder options are also used.
|
77
|
+
#
|
78
|
+
# @overload link_to(content, url, **attributes)
|
79
|
+
# Returns a tag using a given string as the contents.
|
80
|
+
#
|
81
|
+
# @param content [String] content used in the a tag
|
82
|
+
# @param url [String] URL to be used in the `href` attribute
|
83
|
+
# @param attributes [Hash] HTML attributes to include in the tag
|
84
|
+
#
|
85
|
+
# @overload link_to(url, **attributes, &block)
|
86
|
+
# Returns a tag using the given block's return value as the contents.
|
87
|
+
#
|
88
|
+
# @param url [String] URL to be used in the `href` attribute
|
89
|
+
# @param attributes [Hash] HTML attributes to include in the tag
|
90
|
+
# @param block [Proc] block that returns the contents of the tag
|
91
|
+
#
|
92
|
+
# @return [String] HTML markup for the anchor tag
|
93
|
+
#
|
94
|
+
# @example
|
95
|
+
# link_to("Home", "/")
|
96
|
+
# # => <a href="/">Home</a>
|
97
|
+
#
|
98
|
+
# link_to("/") { "Home" }
|
99
|
+
# # => <a href="/">Home</a>
|
100
|
+
#
|
101
|
+
# link_to("Home", "/", class: "button") %>
|
102
|
+
# # => <a href="/" class="button">Home</a>
|
103
|
+
#
|
104
|
+
# @example Escaping
|
105
|
+
# link_to("<script>alert('xss')</script>", "/")
|
106
|
+
# # => <a href="/"><script>alert('xss')</script></a>
|
107
|
+
#
|
108
|
+
# link_to("/") { "<script>alert('xss')</script>" }
|
109
|
+
# # => <a href="/"><script>alert('xss')</script></a>
|
110
|
+
#
|
111
|
+
# @see #tag
|
112
|
+
#
|
113
|
+
# @api public
|
114
|
+
# @since 2.0.0
|
115
|
+
def link_to(content, url = nil, **attributes, &block)
|
116
|
+
if block
|
117
|
+
raise ArgumentError if url && content
|
118
|
+
|
119
|
+
url = content
|
120
|
+
content = nil
|
121
|
+
end
|
122
|
+
|
123
|
+
attributes[:href] = url or raise ArgumentError
|
124
|
+
|
125
|
+
tag.a(content, **attributes, &block)
|
126
|
+
end
|
127
|
+
|
128
|
+
# Returns a string of space-separated tokens from a range of given arguments.
|
129
|
+
#
|
130
|
+
# This is intended for building an HTML tag attribute value, such as a list of class names.
|
131
|
+
#
|
132
|
+
# @return [String]
|
133
|
+
#
|
134
|
+
# @example
|
135
|
+
# token_list("foo", "bar")
|
136
|
+
# # => "foo bar"
|
137
|
+
#
|
138
|
+
# token_list("foo", "foo bar")
|
139
|
+
# # => "foo bar"
|
140
|
+
#
|
141
|
+
# token_list({ foo: true, bar: false })
|
142
|
+
# # => "foo"
|
143
|
+
#
|
144
|
+
# token_list(nil, false, 123, "", "foo", { bar: true })
|
145
|
+
# # => "123 foo bar"
|
146
|
+
#
|
147
|
+
# @api public
|
148
|
+
# @since 2.0.0
|
149
|
+
def token_list(*args)
|
150
|
+
tokens = build_tag_values(*args).flat_map { |value|
|
151
|
+
safe = value.html_safe?
|
152
|
+
value.split(/\s+/).map { |s| safe ? s.html_safe : s }
|
153
|
+
}
|
154
|
+
|
155
|
+
EscapeHelper.escape_join(tokens, " ")
|
156
|
+
end
|
157
|
+
|
158
|
+
# @see #token_list
|
159
|
+
#
|
160
|
+
# @api public
|
161
|
+
# @since 2.0.0
|
162
|
+
def class_names(...)
|
163
|
+
token_list(...)
|
164
|
+
end
|
165
|
+
|
166
|
+
# @api private
|
167
|
+
# @since 2.0.0
|
168
|
+
def build_tag_values(*args)
|
169
|
+
tag_values = []
|
170
|
+
|
171
|
+
args.each do |tag_value|
|
172
|
+
case tag_value
|
173
|
+
when Hash
|
174
|
+
tag_value.each do |key, val|
|
175
|
+
tag_values << key.to_s if val && !key.to_s.empty?
|
176
|
+
end
|
177
|
+
when Array
|
178
|
+
tag_values.concat build_tag_values(*tag_value)
|
179
|
+
else
|
180
|
+
tag_values << tag_value.to_s unless tag_value.to_s.empty?
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
tag_values
|
185
|
+
end
|
186
|
+
|
187
|
+
# @api private
|
188
|
+
# @since 2.0.0
|
189
|
+
def tag_builder
|
190
|
+
@tag_builder ||= begin
|
191
|
+
TagBuilder.new(inflector: tag_builder_inflector)
|
192
|
+
end
|
193
|
+
end
|
194
|
+
|
195
|
+
# @api private
|
196
|
+
# @since 2.0.0
|
197
|
+
def tag_builder_inflector
|
198
|
+
if respond_to?(:_context)
|
199
|
+
return _context.inflector
|
200
|
+
end
|
201
|
+
|
202
|
+
# TODO: When hanami-view moves to Zeitwerk (and the only external require is for
|
203
|
+
# "dry/view"), remove this.
|
204
|
+
require "dry/inflector"
|
205
|
+
Dry::Inflector.new
|
206
|
+
end
|
207
|
+
end
|
208
|
+
end
|
209
|
+
end
|
210
|
+
end
|
@@ -0,0 +1,104 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Hanami
|
4
|
+
class View
|
5
|
+
module HTML
|
6
|
+
# A string that has been marked as "HTML safe", ensuring that it is not automatically escaped
|
7
|
+
# when used in HTML view templates.
|
8
|
+
#
|
9
|
+
# A SafeString is frozen when initialized to ensure it cannot be mutated after being marked as
|
10
|
+
# safe, which could be an avenue for injection of unsafe content.
|
11
|
+
#
|
12
|
+
# @see String#html_safe
|
13
|
+
#
|
14
|
+
# @api public
|
15
|
+
# @since 2.0.0
|
16
|
+
class SafeString < String
|
17
|
+
# @api public
|
18
|
+
# @since 2.0.0
|
19
|
+
def initialize(string)
|
20
|
+
super(string)
|
21
|
+
freeze
|
22
|
+
end
|
23
|
+
|
24
|
+
# @api private
|
25
|
+
private def initialize_copy(other)
|
26
|
+
super
|
27
|
+
freeze
|
28
|
+
end
|
29
|
+
|
30
|
+
# @return [true]
|
31
|
+
#
|
32
|
+
# @api public
|
33
|
+
# @since 2.0.0
|
34
|
+
def html_safe?
|
35
|
+
true
|
36
|
+
end
|
37
|
+
|
38
|
+
# @return [self]
|
39
|
+
#
|
40
|
+
# @api public
|
41
|
+
# @since 2.0.0
|
42
|
+
def html_safe
|
43
|
+
self
|
44
|
+
end
|
45
|
+
|
46
|
+
# @return [self]
|
47
|
+
#
|
48
|
+
# @api public
|
49
|
+
# @since 2.0.0
|
50
|
+
def to_s
|
51
|
+
self
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
# @api private
|
56
|
+
# @since 2.0.0
|
57
|
+
module StringExtensions
|
58
|
+
# Returns the string as a {Hanami::View::HTML::SafeString}, ensuring the string is not
|
59
|
+
# automatically escaped when used in HTML view templates.
|
60
|
+
#
|
61
|
+
# @return [Hanami::View::HTML::SafeString]
|
62
|
+
#
|
63
|
+
# @api public
|
64
|
+
# @since 2.0.0
|
65
|
+
def html_safe
|
66
|
+
Hanami::View::HTML::SafeString.new(self)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
class String
|
74
|
+
# Prepend our `#html_safe` method so that it takes precedence over Active Support's. When both
|
75
|
+
# methods are loaded, the more likely scenario is that the user will want Hanami's, since in the
|
76
|
+
# context of a Hanami app, Active Support is more likely to be loaded incidentally, as a
|
77
|
+
# transitive dependency of another gem.
|
78
|
+
#
|
79
|
+
# Having our `#html_safe` available via this module also means that a user can also choose to
|
80
|
+
# _undefine_ this method within the module if they'd rather use Active Support's.
|
81
|
+
prepend Hanami::View::HTML::StringExtensions
|
82
|
+
end
|
83
|
+
|
84
|
+
class Object
|
85
|
+
# @return [false]
|
86
|
+
#
|
87
|
+
# @api public
|
88
|
+
# @since 2.0.0
|
89
|
+
def html_safe?
|
90
|
+
false
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
class Numeric
|
95
|
+
# @return [true]
|
96
|
+
#
|
97
|
+
# @api public
|
98
|
+
# @since 2.0.0
|
99
|
+
def html_safe?
|
100
|
+
true
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
|
@@ -0,0 +1,46 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "temple"
|
4
|
+
|
5
|
+
module Hanami
|
6
|
+
class View
|
7
|
+
# Specialized Temple buffer class that marks block-captured strings as HTML safe.
|
8
|
+
#
|
9
|
+
# This is important for any scope or part methods that receive a string from a yielded block
|
10
|
+
# and then determine whether to escape that string based on its `.html_safe?` value.
|
11
|
+
#
|
12
|
+
# In this case, since blocks captured templates _intentionally_ contain HTML (this is the
|
13
|
+
# purpose of the template after all), it makes sense to mark the entire captured block string as
|
14
|
+
# HTML safe.
|
15
|
+
#
|
16
|
+
# This is compatible with escaping of values interpolated into the template, since those will
|
17
|
+
# have already been automatically escaped by the template engine when they are evaluated, before
|
18
|
+
# the overall block is captured.
|
19
|
+
#
|
20
|
+
# This filter is included in all three of our supported HTML template engines (ERB, Haml and
|
21
|
+
# Slim) to provide consistent behavior across all.
|
22
|
+
#
|
23
|
+
# @see Hanami::View::ERB::Engine
|
24
|
+
# @see Hanami::View::HamlAdapter::Template
|
25
|
+
# @see Hanami::View::SlimAdapter::Template
|
26
|
+
#
|
27
|
+
# @api private
|
28
|
+
# @since 2.0.0
|
29
|
+
class HTMLSafeStringBuffer < Temple::Generators::StringBuffer
|
30
|
+
# Replace `Temple::Generator::ArrayBuffer#call` (which is used via the superclass of
|
31
|
+
# `StringBuffer`) with the standard implementation from the base `Temple::Generator`.
|
32
|
+
#
|
33
|
+
# This avoids certain specialisations in `ArrayBuffer#call` that prevent `#return_buffer` from
|
34
|
+
# being called. For our needs, `#return_buffer` must be called at all times in order to ensure
|
35
|
+
# the captured string is consistently marked as `.html_safe`.
|
36
|
+
def call(exp)
|
37
|
+
[preamble, compile(exp), postamble].flatten.compact.join('; ')
|
38
|
+
end
|
39
|
+
|
40
|
+
# Marks the string returned from the captured buffer as HTML safe.
|
41
|
+
def return_buffer
|
42
|
+
"#{buffer}.html_safe"
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
data/lib/hanami/view/part.rb
CHANGED
@@ -1,8 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require "dry/core/equalizer"
|
4
|
-
require_relative "decorated_attributes"
|
5
|
-
require_relative "render_environment_missing"
|
6
4
|
|
7
5
|
module Hanami
|
8
6
|
class View
|
@@ -25,7 +23,7 @@ module Hanami
|
|
25
23
|
value
|
26
24
|
].freeze
|
27
25
|
|
28
|
-
include Dry::Equalizer(:_name, :_value, :
|
26
|
+
include Dry::Equalizer(:_name, :_value, :_rendering)
|
29
27
|
include DecoratedAttributes
|
30
28
|
|
31
29
|
# The part's name. This comes from the exposure supplying the value.
|
@@ -48,12 +46,12 @@ module Hanami
|
|
48
46
|
# @api public
|
49
47
|
attr_reader :_value
|
50
48
|
|
51
|
-
# The current
|
49
|
+
# The current rendering
|
52
50
|
#
|
53
|
-
# @return [
|
51
|
+
# @return [Rendering]
|
54
52
|
#
|
55
53
|
# @api private
|
56
|
-
attr_reader :
|
54
|
+
attr_reader :_rendering
|
57
55
|
|
58
56
|
# Determins a part name (when initialized without one). Intended for use
|
59
57
|
# only while unit testing Parts.
|
@@ -67,17 +65,17 @@ module Hanami
|
|
67
65
|
#
|
68
66
|
# @param name [Symbol] part name
|
69
67
|
# @param value [Object] the value to decorate
|
70
|
-
# @param
|
68
|
+
# @param rendering [Rendering] the current rendering
|
71
69
|
#
|
72
70
|
# @api public
|
73
71
|
def initialize(
|
74
|
-
|
75
|
-
name: self.class.part_name(
|
72
|
+
rendering: RenderingMissing.new,
|
73
|
+
name: self.class.part_name(rendering.inflector),
|
76
74
|
value:
|
77
75
|
)
|
78
76
|
@_name = name
|
79
77
|
@_value = value
|
80
|
-
@
|
78
|
+
@_rendering = rendering
|
81
79
|
end
|
82
80
|
|
83
81
|
# The template format for the current render environment.
|
@@ -92,7 +90,7 @@ module Hanami
|
|
92
90
|
#
|
93
91
|
# @api public
|
94
92
|
def _format
|
95
|
-
|
93
|
+
_rendering.format
|
96
94
|
end
|
97
95
|
|
98
96
|
# The context object for the current render environment
|
@@ -107,7 +105,7 @@ module Hanami
|
|
107
105
|
#
|
108
106
|
# @api public
|
109
107
|
def _context
|
110
|
-
|
108
|
+
_rendering.context
|
111
109
|
end
|
112
110
|
|
113
111
|
# Renders a new partial with the part included in its locals.
|
@@ -128,7 +126,7 @@ module Hanami
|
|
128
126
|
# @api public
|
129
127
|
# rubocop:disable Naming/UncommunicativeMethodParamName
|
130
128
|
def _render(partial_name, as: _name, **locals, &block)
|
131
|
-
|
129
|
+
_rendering.partial(partial_name, _rendering.scope({as => self}.merge(locals)), &block)
|
132
130
|
end
|
133
131
|
# rubocop:enable Naming/UncommunicativeMethodParamName
|
134
132
|
|
@@ -148,7 +146,7 @@ module Hanami
|
|
148
146
|
#
|
149
147
|
# @api public
|
150
148
|
def _scope(scope_name = nil, **locals)
|
151
|
-
|
149
|
+
_rendering.scope(scope_name, {_name => self}.merge(locals))
|
152
150
|
end
|
153
151
|
|
154
152
|
# Returns a string representation of the value
|
@@ -180,7 +178,7 @@ module Hanami
|
|
180
178
|
klass.new(
|
181
179
|
name: name,
|
182
180
|
value: value,
|
183
|
-
|
181
|
+
rendering: _rendering,
|
184
182
|
**options
|
185
183
|
)
|
186
184
|
end
|
@@ -1,139 +1,99 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require "dry/core/cache"
|
4
|
-
require "dry/core/equalizer"
|
5
|
-
require_relative "part"
|
6
|
-
|
7
3
|
module Hanami
|
8
4
|
class View
|
9
5
|
# Decorates exposure values with matching parts
|
10
6
|
#
|
11
7
|
# @api private
|
12
8
|
class PartBuilder
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
def for_render_env(render_env)
|
29
|
-
return self if render_env == self.render_env
|
30
|
-
|
31
|
-
self.class.new(namespace: namespace, render_env: render_env)
|
32
|
-
end
|
33
|
-
|
34
|
-
# Decorates an exposure value
|
35
|
-
#
|
36
|
-
# @param name [Symbol] exposure name
|
37
|
-
# @param value [Object] exposure value
|
38
|
-
# @param options [Hash] exposure options
|
39
|
-
#
|
40
|
-
# @return [Hanami::View::Part] decorated value
|
41
|
-
#
|
42
|
-
# @api private
|
43
|
-
def call(name, value, **options)
|
44
|
-
builder = value.respond_to?(:to_ary) ? :build_collection_part : :build_part
|
45
|
-
|
46
|
-
send(builder, name, value, **options)
|
47
|
-
end
|
48
|
-
|
49
|
-
private
|
50
|
-
|
51
|
-
def build_part(name, value, **options)
|
52
|
-
klass = part_class(name: name, **options)
|
9
|
+
class << self
|
10
|
+
# Decorates an exposure value
|
11
|
+
#
|
12
|
+
# @param name [Symbol] exposure name
|
13
|
+
# @param value [Object] exposure value
|
14
|
+
# @param as [Symbol, nil] alternative name to use for part class resolution
|
15
|
+
#
|
16
|
+
# @return [Hanami::View::Part] decorated value
|
17
|
+
#
|
18
|
+
# @api private
|
19
|
+
def call(name, value, as: nil, rendering:)
|
20
|
+
builder = value.respond_to?(:to_ary) ? :build_collection_part : :build_part
|
21
|
+
|
22
|
+
send(builder, name: name, value: value, as: as, rendering: rendering)
|
23
|
+
end
|
53
24
|
|
54
|
-
|
55
|
-
name: name,
|
56
|
-
value: value,
|
57
|
-
render_env: render_env
|
58
|
-
)
|
59
|
-
end
|
25
|
+
private
|
60
26
|
|
61
|
-
|
62
|
-
|
63
|
-
item_name, item_as = collection_item_options(name: name, **options).values_at(:name, :as)
|
27
|
+
def build_part(name:, value:, as:, rendering:)
|
28
|
+
klass = part_class(name: name, as: as, rendering: rendering)
|
64
29
|
|
65
|
-
|
66
|
-
|
67
|
-
}
|
30
|
+
klass.new(name: name, value: value, rendering: rendering)
|
31
|
+
end
|
68
32
|
|
69
|
-
|
70
|
-
|
33
|
+
def build_collection_part(name:, value:, as: nil, rendering:)
|
34
|
+
item_name, item_as = collection_item_name_as(name, as, inflector: rendering.inflector)
|
35
|
+
item_part_class = part_class(name: item_name, as: item_as, rendering: rendering)
|
71
36
|
|
72
|
-
|
73
|
-
|
74
|
-
|
37
|
+
arr = value.to_ary.map { |item|
|
38
|
+
item_part_class.new(name: item_name, value: item, rendering: rendering)
|
39
|
+
}
|
75
40
|
|
76
|
-
|
77
|
-
|
78
|
-
|
41
|
+
collection_as = as.is_a?(Array) ? as.first : nil
|
42
|
+
build_part(name: name, value: arr, as: collection_as, rendering: rendering)
|
43
|
+
end
|
79
44
|
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
45
|
+
def collection_item_name_as(name, as, inflector:)
|
46
|
+
singular_name = inflector.singularize(name).to_sym
|
47
|
+
singular_as =
|
48
|
+
if as.is_a?(Array)
|
49
|
+
as.last if as.length > 1
|
50
|
+
else
|
51
|
+
as
|
52
|
+
end
|
53
|
+
|
54
|
+
if singular_as && !singular_as.is_a?(Class)
|
55
|
+
singular_as = inflector.singularize(singular_as.to_s)
|
87
56
|
end
|
88
57
|
|
89
|
-
|
90
|
-
singular_as = inflector.singularize(singular_as.to_s)
|
58
|
+
[singular_name, singular_as]
|
91
59
|
end
|
92
60
|
|
93
|
-
|
94
|
-
name
|
95
|
-
as: singular_as
|
96
|
-
)
|
97
|
-
end
|
98
|
-
|
99
|
-
def part_class(name:, fallback_class: Part, **options)
|
100
|
-
name = options[:as] || name
|
61
|
+
def part_class(name:, as:, rendering:)
|
62
|
+
name = as || name
|
101
63
|
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
64
|
+
if name.is_a?(Class)
|
65
|
+
name
|
66
|
+
else
|
67
|
+
View.cache.fetch_or_store(:part_class, name, rendering.config) do
|
68
|
+
resolve_part_class(name: name, rendering: rendering)
|
69
|
+
end
|
107
70
|
end
|
108
71
|
end
|
109
|
-
end
|
110
72
|
|
111
|
-
|
112
|
-
|
113
|
-
|
73
|
+
# rubocop:disable Metrics/PerceivedComplexity
|
74
|
+
def resolve_part_class(name:, rendering:)
|
75
|
+
namespace = rendering.config.part_namespace
|
76
|
+
return rendering.config.part_class unless namespace
|
114
77
|
|
115
|
-
|
78
|
+
name = rendering.inflector.camelize(name.to_s)
|
116
79
|
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
80
|
+
# Give autoloaders a chance to act
|
81
|
+
begin
|
82
|
+
klass = namespace.const_get(name)
|
83
|
+
rescue NameError # rubocop:disable Lint/HandleExceptions
|
84
|
+
end
|
122
85
|
|
123
|
-
|
124
|
-
|
125
|
-
|
86
|
+
if !klass && namespace.const_defined?(name, false)
|
87
|
+
klass = namespace.const_get(name)
|
88
|
+
end
|
126
89
|
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
90
|
+
if klass && klass < Part
|
91
|
+
klass
|
92
|
+
else
|
93
|
+
rendering.config.part_class
|
94
|
+
end
|
131
95
|
end
|
132
|
-
|
133
|
-
# rubocop:enable Metrics/PerceivedComplexity
|
134
|
-
|
135
|
-
def inflector
|
136
|
-
render_env.inflector
|
96
|
+
# rubocop:enable Metrics/PerceivedComplexity
|
137
97
|
end
|
138
98
|
end
|
139
99
|
end
|