view_component 3.0.0.rc2 → 3.0.0.rc3
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of view_component might be problematic. Click here for more details.
- checksums.yaml +4 -4
- data/app/controllers/view_components_system_test_controller.rb +22 -1
- data/docs/CHANGELOG.md +28 -0
- data/lib/view_component/base.rb +5 -0
- data/lib/view_component/compiler.rb +49 -11
- data/lib/view_component/inline_template.rb +55 -0
- data/lib/view_component/system_test_helpers.rb +5 -5
- data/lib/view_component/test_helpers.rb +26 -15
- data/lib/view_component/translatable.rb +33 -23
- data/lib/view_component/version.rb +1 -1
- data/lib/view_component.rb +1 -0
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f88375c08b20eb604992d315362fbdf6e0654fd87d7766d1ac404daecb1b57c9
|
4
|
+
data.tar.gz: 5f37ce63b609f5ff542a7e7c4629c952e3a02fc4e2ffc83df28aee86ff5d40cd
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 425107ab124527b5199b4174437c17a0f2dae4447b77a9393f68dd8c7a88517e25a3100fa0ed4cf6190d7e3b38390c6fd5cf48718753a53169f72dfe50ae5d12
|
7
|
+
data.tar.gz: b1642a727d8a586d8dec7a2e7075b570f5ccb20cee0b3c41037ac0b119adc6261ca1d1fb1dea0558f4bb40a0a45ba73b111ce4816a85be6798e3fb2bfaf76daf
|
@@ -1,7 +1,28 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
class ViewComponentsSystemTestController < ActionController::Base # :nodoc:
|
4
|
+
TEMP_DIR = FileUtils.mkdir_p("./tmp/view_components/").first
|
5
|
+
|
6
|
+
before_action :validate_test_env
|
7
|
+
before_action :validate_file_path
|
8
|
+
|
4
9
|
def system_test_entrypoint
|
5
|
-
render file:
|
10
|
+
render file: @path
|
11
|
+
end
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
def validate_test_env
|
16
|
+
raise "ViewComponentsSystemTestController must only be called in a test environment" unless Rails.env.test?
|
17
|
+
end
|
18
|
+
|
19
|
+
# Ensure that the file path is valid and doesn't target files outside
|
20
|
+
# the expected directory (e.g. via a path traversal or symlink attack)
|
21
|
+
def validate_file_path
|
22
|
+
base_path = ::File.realpath(TEMP_DIR)
|
23
|
+
@path = ::File.realpath(params.permit(:file)[:file], base_path)
|
24
|
+
unless @path.start_with?(base_path)
|
25
|
+
raise ArgumentError, "Invalid file path"
|
26
|
+
end
|
6
27
|
end
|
7
28
|
end
|
data/docs/CHANGELOG.md
CHANGED
@@ -10,6 +10,34 @@ nav_order: 5
|
|
10
10
|
|
11
11
|
## main
|
12
12
|
|
13
|
+
## v3.0.0.rc3
|
14
|
+
|
15
|
+
Run into an issue with this release candidate? [Let us know](https://github.com/ViewComponent/view_component/issues/1629).
|
16
|
+
|
17
|
+
* Fix typos in generator docs.
|
18
|
+
|
19
|
+
*Sascha Karnatz*
|
20
|
+
|
21
|
+
* Add `TestHelpers#vc_test_controller`.
|
22
|
+
|
23
|
+
*Joel Hawksley*
|
24
|
+
|
25
|
+
* Document `config.view_component.capture_compatibility_patch_enabled` as option for the known incompatibilities with Rails form helpers.
|
26
|
+
|
27
|
+
*Tobias L. Maier*
|
28
|
+
|
29
|
+
* Add support for experimental inline templates.
|
30
|
+
|
31
|
+
*Blake Williams*
|
32
|
+
|
33
|
+
* Expose `translate` and `t` I18n methods on component classes.
|
34
|
+
|
35
|
+
*Elia Schito*
|
36
|
+
|
37
|
+
* Protect against Arbitrary File Read edge case in `ViewComponentsSystemTestController`.
|
38
|
+
|
39
|
+
*Nick Malcolm*
|
40
|
+
|
13
41
|
## v3.0.0.rc2
|
14
42
|
|
15
43
|
Run into an issue with this release? [Let us know](https://github.com/ViewComponent/view_component/issues/1629).
|
data/lib/view_component/base.rb
CHANGED
@@ -481,6 +481,11 @@ module ViewComponent
|
|
481
481
|
compiler.compiled?
|
482
482
|
end
|
483
483
|
|
484
|
+
# @private
|
485
|
+
def ensure_compiled
|
486
|
+
compile unless compiled?
|
487
|
+
end
|
488
|
+
|
484
489
|
# Compile templates to instance methods, assuming they haven't been compiled already.
|
485
490
|
#
|
486
491
|
# Do as much work as possible in this step, as doing so reduces the amount
|
@@ -51,24 +51,46 @@ module ViewComponent
|
|
51
51
|
component_class.validate_collection_parameter!
|
52
52
|
end
|
53
53
|
|
54
|
-
|
55
|
-
|
56
|
-
# as Ruby warns when redefining a method.
|
57
|
-
method_name = call_method_name(template[:variant])
|
54
|
+
if has_inline_template?
|
55
|
+
template = component_class.inline_template
|
58
56
|
|
59
57
|
redefinition_lock.synchronize do
|
60
|
-
component_class.silence_redefinition_of_method(
|
58
|
+
component_class.silence_redefinition_of_method("call")
|
61
59
|
# rubocop:disable Style/EvalWithLocation
|
62
|
-
component_class.class_eval <<-RUBY, template
|
63
|
-
def
|
64
|
-
#{
|
60
|
+
component_class.class_eval <<-RUBY, template.path, template.lineno
|
61
|
+
def call
|
62
|
+
#{compiled_inline_template(template)}
|
65
63
|
end
|
66
64
|
RUBY
|
67
65
|
# rubocop:enable Style/EvalWithLocation
|
66
|
+
|
67
|
+
component_class.silence_redefinition_of_method("render_template_for")
|
68
|
+
component_class.class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
69
|
+
def render_template_for(variant = nil)
|
70
|
+
call
|
71
|
+
end
|
72
|
+
RUBY
|
73
|
+
end
|
74
|
+
else
|
75
|
+
templates.each do |template|
|
76
|
+
# Remove existing compiled template methods,
|
77
|
+
# as Ruby warns when redefining a method.
|
78
|
+
method_name = call_method_name(template[:variant])
|
79
|
+
|
80
|
+
redefinition_lock.synchronize do
|
81
|
+
component_class.silence_redefinition_of_method(method_name)
|
82
|
+
# rubocop:disable Style/EvalWithLocation
|
83
|
+
component_class.class_eval <<-RUBY, template[:path], 0
|
84
|
+
def #{method_name}
|
85
|
+
#{compiled_template(template[:path])}
|
86
|
+
end
|
87
|
+
RUBY
|
88
|
+
# rubocop:enable Style/EvalWithLocation
|
89
|
+
end
|
68
90
|
end
|
69
|
-
end
|
70
91
|
|
71
|
-
|
92
|
+
define_render_template_for
|
93
|
+
end
|
72
94
|
|
73
95
|
component_class.build_i18n_backend
|
74
96
|
|
@@ -103,12 +125,16 @@ module ViewComponent
|
|
103
125
|
end
|
104
126
|
end
|
105
127
|
|
128
|
+
def has_inline_template?
|
129
|
+
component_class.respond_to?(:inline_template) && component_class.inline_template.present?
|
130
|
+
end
|
131
|
+
|
106
132
|
def template_errors
|
107
133
|
@__vc_template_errors ||=
|
108
134
|
begin
|
109
135
|
errors = []
|
110
136
|
|
111
|
-
if (templates + inline_calls).empty?
|
137
|
+
if (templates + inline_calls).empty? && !has_inline_template?
|
112
138
|
errors << "Couldn't find a template file or inline render method for #{component_class}."
|
113
139
|
end
|
114
140
|
|
@@ -216,9 +242,21 @@ module ViewComponent
|
|
216
242
|
end
|
217
243
|
end
|
218
244
|
|
245
|
+
def compiled_inline_template(template)
|
246
|
+
handler = ActionView::Template.handler_for_extension(template.language)
|
247
|
+
template.rstrip! if component_class.strip_trailing_whitespace?
|
248
|
+
|
249
|
+
compile_template(template.source, handler)
|
250
|
+
end
|
251
|
+
|
219
252
|
def compiled_template(file_path)
|
220
253
|
handler = ActionView::Template.handler_for_extension(File.extname(file_path).delete("."))
|
221
254
|
template = File.read(file_path)
|
255
|
+
|
256
|
+
compile_template(template, handler)
|
257
|
+
end
|
258
|
+
|
259
|
+
def compile_template(template, handler)
|
222
260
|
template.rstrip! if component_class.strip_trailing_whitespace?
|
223
261
|
|
224
262
|
if handler.method(:call).parameters.length > 1
|
@@ -0,0 +1,55 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ViewComponent # :nodoc:
|
4
|
+
module InlineTemplate
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
Template = Struct.new(:source, :language, :path, :lineno)
|
7
|
+
|
8
|
+
class_methods do
|
9
|
+
def method_missing(method, *args)
|
10
|
+
return super if !method.end_with?("_template")
|
11
|
+
|
12
|
+
if defined?(@__vc_inline_template_defined) && @__vc_inline_template_defined
|
13
|
+
raise ViewComponent::ComponentError, "inline templates can only be defined once per-component"
|
14
|
+
end
|
15
|
+
|
16
|
+
if args.size != 1
|
17
|
+
raise ArgumentError, "wrong number of arguments (given #{args.size}, expected 1)"
|
18
|
+
end
|
19
|
+
|
20
|
+
ext = method.to_s.gsub("_template", "")
|
21
|
+
template = args.first
|
22
|
+
|
23
|
+
@__vc_inline_template_language = ext
|
24
|
+
|
25
|
+
caller = caller_locations(1..1)[0]
|
26
|
+
@__vc_inline_template = Template.new(
|
27
|
+
template,
|
28
|
+
ext,
|
29
|
+
caller.absolute_path || caller.path,
|
30
|
+
caller.lineno
|
31
|
+
)
|
32
|
+
|
33
|
+
@__vc_inline_template_defined = true
|
34
|
+
end
|
35
|
+
ruby2_keywords(:method_missing) if respond_to?(:ruby2_keywords, true)
|
36
|
+
|
37
|
+
def respond_to_missing?(method, include_all = false)
|
38
|
+
method.end_with?("_template") || super
|
39
|
+
end
|
40
|
+
|
41
|
+
def inline_template
|
42
|
+
@__vc_inline_template
|
43
|
+
end
|
44
|
+
|
45
|
+
def inline_template_language
|
46
|
+
@__vc_inline_template_language if defined?(@__vc_inline_template_language)
|
47
|
+
end
|
48
|
+
|
49
|
+
def inherited(subclass)
|
50
|
+
super
|
51
|
+
subclass.instance_variable_set(:@__vc_inline_template_language, inline_template_language)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -10,12 +10,12 @@ module ViewComponent
|
|
10
10
|
# @param layout [String] The (optional) layout to use.
|
11
11
|
# @return [Proc] A block that can be used to visit the path of the inline rendered component.
|
12
12
|
def with_rendered_component_path(fragment, layout: false, &block)
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
13
|
+
file = Tempfile.new(
|
14
|
+
["rendered_#{fragment.class.name}", ".html"],
|
15
|
+
ViewComponentsSystemTestController::TEMP_DIR
|
16
|
+
)
|
17
17
|
begin
|
18
|
-
file.write(
|
18
|
+
file.write(vc_test_controller.render_to_string(html: fragment.to_html.html_safe, layout: layout))
|
19
19
|
file.rewind
|
20
20
|
|
21
21
|
block.call("/_system_test_entrypoint?file=#{file.path.split("/").last}")
|
@@ -47,9 +47,9 @@ module ViewComponent
|
|
47
47
|
@page = nil
|
48
48
|
@rendered_content =
|
49
49
|
if Rails.version.to_f >= 6.1
|
50
|
-
|
50
|
+
vc_test_controller.view_context.render(component, args, &block)
|
51
51
|
else
|
52
|
-
|
52
|
+
vc_test_controller.view_context.render_component(component, &block)
|
53
53
|
end
|
54
54
|
|
55
55
|
Nokogiri::HTML.fragment(@rendered_content)
|
@@ -105,7 +105,7 @@ module ViewComponent
|
|
105
105
|
# ```
|
106
106
|
def render_in_view_context(*args, &block)
|
107
107
|
@page = nil
|
108
|
-
@rendered_content =
|
108
|
+
@rendered_content = vc_test_controller.view_context.instance_exec(*args, &block)
|
109
109
|
Nokogiri::HTML.fragment(@rendered_content)
|
110
110
|
end
|
111
111
|
ruby2_keywords(:render_in_view_context) if respond_to?(:ruby2_keywords, true)
|
@@ -120,12 +120,12 @@ module ViewComponent
|
|
120
120
|
#
|
121
121
|
# @param variant [Symbol] The variant to be set for the provided block.
|
122
122
|
def with_variant(variant)
|
123
|
-
old_variants =
|
123
|
+
old_variants = vc_test_controller.view_context.lookup_context.variants
|
124
124
|
|
125
|
-
|
125
|
+
vc_test_controller.view_context.lookup_context.variants = variant
|
126
126
|
yield
|
127
127
|
ensure
|
128
|
-
|
128
|
+
vc_test_controller.view_context.lookup_context.variants = old_variants
|
129
129
|
end
|
130
130
|
|
131
131
|
# Set the controller to be used while executing the given block,
|
@@ -139,12 +139,12 @@ module ViewComponent
|
|
139
139
|
#
|
140
140
|
# @param klass [ActionController::Base] The controller to be used.
|
141
141
|
def with_controller_class(klass)
|
142
|
-
old_controller = defined?(@
|
142
|
+
old_controller = defined?(@vc_test_controller) && @vc_test_controller
|
143
143
|
|
144
|
-
@
|
144
|
+
@vc_test_controller = __vc_test_helpers_build_controller(klass)
|
145
145
|
yield
|
146
146
|
ensure
|
147
|
-
@
|
147
|
+
@vc_test_controller = old_controller
|
148
148
|
end
|
149
149
|
|
150
150
|
# Set the URL of the current request (such as when using request-dependent path helpers):
|
@@ -161,7 +161,7 @@ module ViewComponent
|
|
161
161
|
old_request_path_parameters = __vc_test_helpers_request.path_parameters
|
162
162
|
old_request_query_parameters = __vc_test_helpers_request.query_parameters
|
163
163
|
old_request_query_string = __vc_test_helpers_request.query_string
|
164
|
-
old_controller = defined?(@
|
164
|
+
old_controller = defined?(@vc_test_controller) && @vc_test_controller
|
165
165
|
|
166
166
|
path, query = path.split("?", 2)
|
167
167
|
__vc_test_helpers_request.path_info = path
|
@@ -174,16 +174,27 @@ module ViewComponent
|
|
174
174
|
__vc_test_helpers_request.path_parameters = old_request_path_parameters
|
175
175
|
__vc_test_helpers_request.set_header("action_dispatch.request.query_parameters", old_request_query_parameters)
|
176
176
|
__vc_test_helpers_request.set_header(Rack::QUERY_STRING, old_request_query_string)
|
177
|
-
@
|
177
|
+
@vc_test_controller = old_controller
|
178
|
+
end
|
179
|
+
|
180
|
+
# Access the controller used by `render_inline`:
|
181
|
+
#
|
182
|
+
# ```ruby
|
183
|
+
# test "logged out user sees login link" do
|
184
|
+
# vc_test_controller.expects(:logged_in?).at_least_once.returns(false)
|
185
|
+
# render_inline(LoginComponent.new)
|
186
|
+
# assert_selector("[aria-label='You must be signed in']")
|
187
|
+
# end
|
188
|
+
# ```
|
189
|
+
#
|
190
|
+
# @return [ActionController::Base]
|
191
|
+
def vc_test_controller
|
192
|
+
@vc_test_controller ||= __vc_test_helpers_build_controller(Base.test_controller.constantize)
|
178
193
|
end
|
179
194
|
|
180
195
|
# Note: We prefix private methods here to prevent collisions in consumer's tests.
|
181
196
|
private
|
182
197
|
|
183
|
-
def __vc_test_helpers_controller
|
184
|
-
@__vc_test_helpers_controller ||= __vc_test_helpers_build_controller(Base.test_controller.constantize)
|
185
|
-
end
|
186
|
-
|
187
198
|
def __vc_test_helpers_request
|
188
199
|
@__vc_test_helpers_request ||=
|
189
200
|
begin
|
@@ -21,7 +21,7 @@ module ViewComponent
|
|
21
21
|
end
|
22
22
|
|
23
23
|
def build_i18n_backend
|
24
|
-
return if
|
24
|
+
return if compiled?
|
25
25
|
|
26
26
|
self.i18n_backend = if (translation_files = sidecar_files(%w[yml yaml])).any?
|
27
27
|
# Returning nil cleans up if translations file has been removed since the last compilation
|
@@ -32,6 +32,27 @@ module ViewComponent
|
|
32
32
|
)
|
33
33
|
end
|
34
34
|
end
|
35
|
+
|
36
|
+
def i18n_key(key, scope = nil)
|
37
|
+
scope = scope.join(".") if scope.is_a? Array
|
38
|
+
key = key&.to_s unless key.is_a?(String)
|
39
|
+
key = "#{scope}.#{key}" if scope
|
40
|
+
key = "#{i18n_scope}#{key}" if key.start_with?(".")
|
41
|
+
key
|
42
|
+
end
|
43
|
+
|
44
|
+
def translate(key = nil, **options)
|
45
|
+
return key.map { |k| translate(k, **options) } if key.is_a?(Array)
|
46
|
+
|
47
|
+
ensure_compiled
|
48
|
+
|
49
|
+
locale = options.delete(:locale) || ::I18n.locale
|
50
|
+
key = i18n_key(key, options.delete(:scope))
|
51
|
+
|
52
|
+
i18n_backend.translate(locale, key, options)
|
53
|
+
end
|
54
|
+
|
55
|
+
alias_method :t, :translate
|
35
56
|
end
|
36
57
|
|
37
58
|
class I18nBackend < ::I18n::Backend::Simple
|
@@ -64,15 +85,10 @@ module ViewComponent
|
|
64
85
|
return key.map { |k| translate(k, **options) } if key.is_a?(Array)
|
65
86
|
|
66
87
|
locale = options.delete(:locale) || ::I18n.locale
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
key = "#{i18n_scope}#{key}" if key.start_with?(".")
|
72
|
-
|
73
|
-
if HTML_SAFE_TRANSLATION_KEY.match?(key)
|
74
|
-
html_escape_translation_options!(options)
|
75
|
-
end
|
88
|
+
key = self.class.i18n_key(key, options.delete(:scope))
|
89
|
+
as_html = HTML_SAFE_TRANSLATION_KEY.match?(key)
|
90
|
+
|
91
|
+
html_escape_translation_options!(options) if as_html
|
76
92
|
|
77
93
|
if key.start_with?(i18n_scope + ".")
|
78
94
|
translated =
|
@@ -85,10 +101,7 @@ module ViewComponent
|
|
85
101
|
return super(key, locale: locale, **options)
|
86
102
|
end
|
87
103
|
|
88
|
-
|
89
|
-
translated = html_safe_translation(translated)
|
90
|
-
end
|
91
|
-
|
104
|
+
translated = html_safe_translation(translated) if as_html
|
92
105
|
translated
|
93
106
|
else
|
94
107
|
super(key, locale: locale, **options)
|
@@ -101,6 +114,8 @@ module ViewComponent
|
|
101
114
|
self.class.i18n_scope
|
102
115
|
end
|
103
116
|
|
117
|
+
private
|
118
|
+
|
104
119
|
def html_safe_translation(translation)
|
105
120
|
if translation.respond_to?(:map)
|
106
121
|
translation.map { |element| html_safe_translation(element) }
|
@@ -112,18 +127,13 @@ module ViewComponent
|
|
112
127
|
end
|
113
128
|
end
|
114
129
|
|
115
|
-
private
|
116
|
-
|
117
130
|
def html_escape_translation_options!(options)
|
118
131
|
options.each do |name, value|
|
119
|
-
|
120
|
-
|
121
|
-
end
|
122
|
-
end
|
123
|
-
end
|
132
|
+
next if ::I18n.reserved_keys_pattern.match?(name)
|
133
|
+
next if name == :count && value.is_a?(Numeric)
|
124
134
|
|
125
|
-
|
126
|
-
|
135
|
+
options[name] = ERB::Util.html_escape(value.to_s)
|
136
|
+
end
|
127
137
|
end
|
128
138
|
end
|
129
139
|
end
|
data/lib/view_component.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: view_component
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 3.0.0.
|
4
|
+
version: 3.0.0.rc3
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- ViewComponent Team
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2023-
|
11
|
+
date: 2023-03-08 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activesupport
|
@@ -363,6 +363,7 @@ files:
|
|
363
363
|
- lib/view_component/docs_builder_component.html.erb
|
364
364
|
- lib/view_component/docs_builder_component.rb
|
365
365
|
- lib/view_component/engine.rb
|
366
|
+
- lib/view_component/inline_template.rb
|
366
367
|
- lib/view_component/instrumentation.rb
|
367
368
|
- lib/view_component/preview.rb
|
368
369
|
- lib/view_component/preview_template_error.rb
|