render_turbo_stream 4.1.6 → 4.3.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d41d763714ed2d599837c9cd3723d2f618f9ee6df7a095c8a06d0f66291cae7f
4
- data.tar.gz: fac83287dbba160bfbe7a18b505810eab430c0fef43b32472494ee3e3291acd2
3
+ metadata.gz: 28e02fddd9493b46170e9e2767651c82748da1c0a0984940e57044489a24d472
4
+ data.tar.gz: a75224ab07323e2eef5d8f6905776c849a852068074d5e10a356303178b7d30a
5
5
  SHA512:
6
- metadata.gz: 4c6943379cb3411f3d2a79b3ab11559876c6ff25db04f41c5f6b6eeb574f6aa8152398584cc9fc00eb00f107a205bb1852e5ae1141ee48707076ad627c963f39
7
- data.tar.gz: 6c160c3026eac74b5fa1be6288409554a95cad8548b75e615015d99883774e87c3c61d8087588fb5d4e9840b2acd141b90a03e82cf21440ccaf0b221d8b0238e
6
+ metadata.gz: 45161e756cf12ebee2a2fd8941e93471ac001153bd39905597777f3fed48f5bb91e26fb254b41f0f30e340b4f47d1f743c2ec744a969e59e6b21baefdde40d5e
7
+ data.tar.gz: 8e610e79ae595f3af6290b450fb7b61a97c0166e883db6a5cae71bb095dac3481644d2964218478176376c1d0461144235cb6f2526d9af2dba97e21df999e57d
data/README.md CHANGED
@@ -4,6 +4,8 @@
4
4
 
5
5
  This gem has a second [README Turbo::StreamsChannel](https://gitlab.com/sedl/renderturbostream/-/blob/main/README-channels.md). Starting point is here. And I recommend that you download my [Quick-and-dirty test project](https://gitlab.com/sedl/renderturbostream_railsapp), set it up, see all the tests succeed, and then read through this README.
6
6
 
7
+ As of v4.3, locals inside partials should work as expected. If you are working with turbo but without this gem, please read [readme-locals](https://gitlab.com/sedl/renderturbostream/-/blob/main/readme-locals.md) to avoid tedious details.
8
+
7
9
  Defining templates like `(create|update).turbo_stream.haml` is a heavy mix of logic and view. This gem separates logic and view so that `*.turbo_stream.*` templates are no longer necessary and the logic can stay on the ruby side.
8
10
 
9
11
  For `:replace` actions, responses must be wrapped inside a `turbo_frame_tag` with a matching `target_id`. The gem will regex the content and wrap it by a `turbo_frame_tag` if necessary but not present. This way the matching `target_id` is only defined in one place. This check only happens on the first call after restarting the application in production and on every call if precompile assets is set to true in configs.
@@ -17,7 +19,7 @@ Has a testing strategy.
17
19
  An overview of how we design a rails-7 application with turbo
18
20
  is [published on dev.to](https://dev.to/chmich/rails-7-vite-wrapping-up-1pia).
19
21
 
20
- I am happy if it can help.
22
+ Hope it helps :)
21
23
 
22
24
  **Chris**
23
25
 
@@ -170,6 +172,34 @@ If this config is set to true, Turbo::StreamsChannel is installed and a current
170
172
 
171
173
  If an `if_success_redirect_to` argument is provided and the save action was successful, `turbo_stream_save` would send the partials by channel.
172
174
 
175
+ **target-ID: Avoid the same definition in multiple places**
176
+
177
+ Without this gem a turbo action would be wrapped within two frames, for example:
178
+
179
+ ```haml
180
+ = turbo_stream.replace 'target-id' do
181
+ = render 'a partial'
182
+
183
+ ```
184
+
185
+ and within a partial:
186
+
187
+ ```haml
188
+ = turbo_frame_tag 'target-id' do
189
+ ... content
190
+ ```
191
+
192
+ The `turbo_frame_tag` would stay inside a partial in most cases. The reason is: On the first load, the target-id must be delivered up front, so that if the tag needs to be replaced later, turbo knows which part to replace.
193
+
194
+ This means that the target id must be defined in several places: inside a partial and at the place where the turbo action is defined.
195
+
196
+ In order to avoid this kind of tedious coding, the gem has a kind of fallback built in: If the argument `partial` is given, but the attribute `target_id` is not, the gem will get the target_id from the partial. The process is:
197
+
198
+ 1. Render the partial with the provided locals
199
+ 2. Grabs into the partial by Nokogiri and looks for a turbo_frame_tag and fetches the target_id.
200
+ 3. If all that not is found it raises a exception
201
+ 4. wraps the partial within the `turbo_stream.*` and sends this to the front.
202
+
173
203
  # Request Testing
174
204
 
175
205
  To test if the whole system works together, including javascript actions, so that finally a part reaches the surface, there is **Capybara** system testing. But it is a good practice to **break tests into smaller pieces**. So, there are helpers for **enabling the much faster request tests that are much easier to maintain**.
@@ -222,10 +252,19 @@ config.x.render_turbo_stream.first_argument_is_html_id = %[replace append prepen
222
252
 
223
253
  This setting is relevant for testing helpers.
224
254
 
255
+ # Conclusion
256
+
257
+ The World Wide Web, founded around 1990 by [Tim Berners-Lee](https://en.wikipedia.org/wiki/Tim_Berners-Lee), was an html response from the server.
258
+
259
+ Frameworks like Angular, Ember, React, Vue brought a much better user experience, so called "single page applications". But they changed something: Now Javascript was the processor of HTML, which is far from the [Progressive Enhancement] (https://developer.mozilla.org/en-US/docs/Glossary/Progressive_Enhancement)
260
+
261
+ The Rails core team has now achieved a milestone by turbo, for bringing a user experience like a single page app, but by reducing javascript. Somehow modern, back to the roots. Thank you.
262
+
263
+ This gem is an attempt to make this approach more convenient for developers.
225
264
 
226
265
  # Contributing
227
266
 
228
- Contribution welcome.
267
+ Contributors welcome.
229
268
 
230
269
  # License
231
270
 
@@ -0,0 +1,4 @@
1
+ module RenderTurboStream
2
+ class RenderController < ActionController::Base
3
+ end
4
+ end
@@ -14,22 +14,29 @@
14
14
  <% else %>
15
15
  <% ctl = { partial: args[:partial], target_id: args[:target_id], action: args[:action] } %>
16
16
 
17
+
18
+ <% locals = args[:locals]&.symbolize_keys %>
19
+ <% partial = args[:partial] %>
20
+ <% rendered_html = render(partial: partial, locals: locals, formats: [:html]) %>
21
+ <% unless args[:target].present? %>
22
+ <% args[:target] = RenderTurboStream::Libs.fetch_arguments_from_rendered_string(rendered_html)[:target] %>
23
+ <% unless args[:target].present? %>
24
+ <% raise 'No target specified by arguments and no target found inside the rendered partial' %>
25
+ <% end %>
26
+ <% end %>
27
+
28
+
17
29
  <% target_id = RenderTurboStream::Libs.target_to_target_id(args[:target]) %>
18
30
 
19
31
  <% info = { target_id: args[:target_id], partial: args[:partial], locals: args[:locals] } %>
20
32
 
21
33
 
22
-
23
34
  <% if args[:action].present? %>
24
35
  <% Rails.logger.debug(" • render-turbo-stream #{args[:action].upcase} => #{info}") %>
25
36
  <%= turbo_stream.send args[:action].to_sym, target_id do %>
26
- <% if RenderTurboStream::CheckTemplate.new(partial: args[:partial], action: args[:action]).add_turbo_frame_tag? %>
27
- <%= turbo_frame_tag RenderTurboStream::Libs.target_to_target_id(args[:target]) do %>
28
- <%= render args[:partial], locals: args[:locals]&.symbolize_keys %>
29
- <% end %>
30
- <% else %>
31
- <%= render args[:partial], locals: args[:locals]&.symbolize_keys %>
32
- <% end %>
37
+
38
+ <%= rendered_html %>
39
+
33
40
  <% end %>
34
41
 
35
42
  <% else %>
@@ -10,15 +10,22 @@
10
10
  <% rendered_partials.push(h) %>
11
11
  <% else %>
12
12
 
13
- <% wrap = RenderTurboStream::CheckTemplate.new(partial: s[:partial], action: s[:action]).add_turbo_frame_tag? %>
14
13
 
15
- <% if wrap %>
16
- <% html = turbo_frame_tag RenderTurboStream::Libs.target_to_target_id(s[:target]) do %>
17
- <%= (render s[:partial], locals: s[:locals]&.symbolize_keys, formats: [:html]) %>
14
+ <% locals = s[:locals]&.symbolize_keys %>
15
+ <% partial = s[:partial] %>
16
+ <% html = (render partial: partial, locals: locals, formats: [:html]) %>
17
+
18
+
19
+
20
+ <% unless s[:target].present? %>
21
+ <% s[:target] = RenderTurboStream::Libs.fetch_arguments_from_rendered_string(html)[:target] %>
22
+ <% unless s[:target].present? %>
23
+ <% raise 'No target specified by arguments and no target found inside the rendered partial' %>
18
24
  <% end %>
19
- <% else %>
20
- <% html = (render s[:partial], locals: s[:locals]&.symbolize_keys, formats: [:html]) %>
21
25
  <% end %>
26
+
27
+
28
+
22
29
  <% s[:target] = s[:target] %>
23
30
  <% s.delete(:id) %>
24
31
  <% rendered_partials.push({ html_response: html, type: 'stream-partial' }.merge(s)) %>
@@ -7,9 +7,10 @@ module RenderTurboStream
7
7
 
8
8
  def render_to_channel(channel, target, action, controller_self, partial: nil, template: nil, locals: nil)
9
9
 
10
+ _locals = (locals ? locals : {})
11
+
10
12
  # instance variables
11
13
 
12
- target_id = RenderTurboStream::Libs.target_to_target_id(target)
13
14
  check_template = RenderTurboStream::CheckTemplate.new(
14
15
  partial: partial,
15
16
  template: template,
@@ -19,35 +20,40 @@ module RenderTurboStream
19
20
 
20
21
  # add instance_variables to locals
21
22
 
22
- locals = {} unless locals # prevent error missing keys for nil
23
- if check_template.add_turbo_frame_tag?
24
- _layout = 'layouts/add_turbo_frame_tag'
25
- _locals = locals.merge(
26
- {
27
- render_turbo_stream_partial: partial,
28
- render_turbo_stream_target_id: target_id
29
- }
30
- )
31
- else
32
- _locals = locals
33
- _layout = false
34
- end
35
23
  check_template.templates_instance_variables.each do |v|
36
24
  _locals[:"render_turbo_stream_instance_variable_#{v}"] = controller_self.instance_variable_get(v.to_s)
37
25
  end
38
26
 
27
+ # fetch target-id
28
+
29
+ target_id = RenderTurboStream::Libs.target_to_target_id(target)
30
+ if !target.present? || Rails.env.test?
31
+ if partial.present?
32
+ rendered_html = RenderController.render(partial: partial, locals: _locals, layout: false)
33
+ elsif template.present?
34
+ rendered_html = RenderController.render(template: template, locals: _locals, layout: false)
35
+ end
36
+ if !target.present?
37
+ r = RenderTurboStream::Libs.fetch_arguments_from_rendered_string(rendered_html)
38
+ target_id = r[:target_id]
39
+ target = r[:target]
40
+ end
41
+ end
42
+
43
+ unless target.present? && target_id.present?
44
+ raise 'No target specified by arguments and no target found inside the rendered partial'
45
+ end
46
+
39
47
  # add headers for test
40
48
 
41
49
  if Rails.env.test?
42
- html = RenderTurboStreamRenderController.render(partial: partial, locals: _locals, layout: _layout) if partial
43
- html = RenderTurboStreamRenderController.render(template: template, locals: _locals, layout: _layout) if template
44
50
  props = {
45
51
  target: target,
46
52
  action: action,
47
53
  type: 'channel-partial',
48
54
  locals: locals,
49
55
  channel: channel,
50
- html_response: html.to_s
56
+ html_response: rendered_html.to_s
51
57
  }
52
58
 
53
59
  props[:partial] = partial if partial
@@ -66,23 +72,23 @@ module RenderTurboStream
66
72
 
67
73
  # send
68
74
 
69
- if partial
75
+ if partial.present?
70
76
  Turbo::StreamsChannel.send(
71
77
  "broadcast_#{action}_to",
72
78
  channel.to_s,
73
79
  target: target_id,
74
80
  partial: partial,
75
81
  locals: _locals&.symbolize_keys,
76
- layout: _layout
82
+ layout: false
77
83
  )
78
- elsif template
84
+ elsif template.present?
79
85
  Turbo::StreamsChannel.send(
80
86
  "broadcast_#{action}_to",
81
87
  channel.to_s,
82
88
  target: target_id,
83
89
  template: template,
84
- locals: locals&.symbolize_keys,
85
- layout: _layout
90
+ locals: _locals&.symbolize_keys,
91
+ layout: false
86
92
  )
87
93
  end
88
94
  end
@@ -114,7 +120,7 @@ module RenderTurboStream
114
120
  end
115
121
  end
116
122
 
117
- content = RenderTurboStreamRenderController.render(template: 'render_turbo_stream_command', layout: false, locals: { command: command, arguments: arguments })
123
+ content = RenderController.render(template: 'render_turbo_stream_command', layout: false, locals: { command: command, arguments: arguments })
118
124
 
119
125
  Turbo::StreamsChannel.broadcast_stream_to(
120
126
  channel,
@@ -23,12 +23,12 @@ module RenderTurboStream
23
23
  if $render_turbo_stream_check_templates[@key]
24
24
  @result = $render_turbo_stream_check_templates[@key]
25
25
  else
26
- @result = { instance_variables: [], add_turbo_frame_tag: false }
26
+ @result = { instance_variables: [] }
27
27
  fetch_nested_partials(1, partial: @partial_path, template: @template_path)
28
28
  $render_turbo_stream_check_templates[@key] ||= @result
29
29
  end
30
30
  else
31
- @result = { instance_variables: [], add_turbo_frame_tag: false }
31
+ @result = { instance_variables: [] }
32
32
  fetch_nested_partials(1, partial: @partial_path, template: @template_path)
33
33
  end
34
34
  end
@@ -37,10 +37,6 @@ module RenderTurboStream
37
37
  @result[:instance_variables]
38
38
  end
39
39
 
40
- def add_turbo_frame_tag?
41
- @result[:add_turbo_frame_tag]
42
- end
43
-
44
40
  private
45
41
 
46
42
  def production?
@@ -67,13 +63,6 @@ module RenderTurboStream
67
63
  end
68
64
  end
69
65
 
70
- conf = Rails.configuration.x.render_turbo_stream.auto_wrap_for_actions
71
- if stack_level == 1 && conf.present? && conf.include?(@action.to_sym)
72
- unless _code.match(/(turbo_frame_tag|turbo-frame)/)
73
- @result[:add_turbo_frame_tag] = true
74
- end
75
- end
76
-
77
66
  loop do
78
67
  _matched_partial = _code.match((/\brender[ ](partial:|())(|[ ])("|')[a-z_\/]+("|')/))
79
68
  _code = _code[(_code.index(_matched_partial.to_s) + 5)..-1]
@@ -1,7 +1,7 @@
1
1
  module RenderTurboStream
2
2
  module ControllerChannelHelpers
3
3
 
4
- def render_to_all(target_id, action = :replace, partial: nil, template: nil, locals: nil)
4
+ def render_to_all(target_id = nil, action = :replace, partial: nil, template: nil, locals: nil)
5
5
  evaluate_instance_variables
6
6
  render_to_channel(
7
7
  'all',
@@ -13,7 +13,7 @@ module RenderTurboStream
13
13
  )
14
14
  end
15
15
 
16
- def render_to_me(target_id, action = :replace, partial: nil, locals: nil)
16
+ def render_to_me(target_id = nil, action = :replace, partial: nil, locals: nil)
17
17
  begin
18
18
  u_id = helpers.current_user&.id
19
19
  unless u_id.present?
@@ -34,7 +34,7 @@ module RenderTurboStream
34
34
  )
35
35
  end
36
36
 
37
- def render_to_authenticated_group(group, target_id, action = :replace, partial: nil, locals: nil)
37
+ def render_to_authenticated_group(group, target_id = nil, action = :replace, partial: nil, locals: nil)
38
38
  begin
39
39
  u_id = helpers.current_user&.id
40
40
  unless u_id.present?
@@ -55,7 +55,7 @@ module RenderTurboStream
55
55
  )
56
56
  end
57
57
 
58
- def render_to_channel(channel, target_id, action, partial: nil, template: nil, locals: nil, instance_variables: true)
58
+ def render_to_channel(channel, target_id = nil, action = :replace, partial: nil, template: nil, locals: nil, instance_variables: true)
59
59
 
60
60
  raise 'Arguments partial and template cannot both be specified' if partial && template
61
61
 
@@ -21,7 +21,7 @@ module RenderTurboStream
21
21
  if_error_add: nil, # additional partials that should be rendered if save_action failed
22
22
  add: [], # additional streams
23
23
 
24
- if_success_notices: nil, # array of strings, override default generated flash generation in the case of success
24
+ if_success_notices: nil, # array of strings, or string, override default generated flash generation in the case of success
25
25
  if_error_alerts: nil,
26
26
  add_notices: nil, # array of strings
27
27
  add_alerts: nil,
@@ -132,11 +132,11 @@ module RenderTurboStream
132
132
  else
133
133
  r[:target] = props[:target]
134
134
  end
135
- raise "Missing attribute :target in #{props}" if !props[:target].present?
136
135
  r.delete(:target_id)
137
136
  r[:action] = (props[:action].present? ? props[:action] : :replace)
138
137
  r[:partial] = RenderTurboStream::Libs.partial_path(props[:target], props[:target_id], controller_path, props[:partial])
139
138
  r[:type] = 'stream-partial'
139
+
140
140
  ary.push(r)
141
141
  elsif pr.is_a?(Array)
142
142
  raise "array has to contain at least one element: #{pr}" unless pr.first.present?
@@ -157,7 +157,7 @@ module RenderTurboStream
157
157
 
158
158
  # renders a partial to turbo_stream
159
159
 
160
- def stream_partial(target_id, partial: nil, action: :replace, locals: {})
160
+ def stream_partial(target_id = nil, partial: nil, action: :replace, locals: {})
161
161
  render_turbo_stream(
162
162
  [
163
163
  {
@@ -5,7 +5,7 @@ module RenderTurboStream
5
5
  @save_action = save_action
6
6
  end
7
7
 
8
- def generate_flash( model_name, controller_action, if_success_notices, if_error_alerts, add_notices, add_alerts )
8
+ def generate_flash(model_name, controller_action, if_success_notices, if_error_alerts, add_notices, add_alerts)
9
9
 
10
10
  target_id = (Rails.configuration.x.render_turbo_stream.flash_target_id rescue nil)
11
11
  raise "Missing configuration: config.x.render_turbo_stream.flash_target_id" unless target_id
@@ -14,26 +14,33 @@ module RenderTurboStream
14
14
  turbo_action = Rails.configuration.x.render_turbo_stream.flash_turbo_action
15
15
  raise "Missing configuration: configuration.x.render_turbo_stream.flash_turbo_action" unless turbo_action
16
16
 
17
-
18
17
  if @save_action
19
18
  notices = if if_success_notices
20
- if_success_notices
21
- else
22
- str = I18n.t(
23
- "activerecord.render_turbo_stream_success.#{controller_action}"
24
- )
25
- [format(str, model_name: model_name)]
26
- end
19
+ if if_success_notices.is_a?(String)
20
+ [if_success_notices]
21
+ else
22
+ if_success_notices
23
+ end
24
+ else
25
+ str = I18n.t(
26
+ "activerecord.render_turbo_stream_success.#{controller_action}"
27
+ )
28
+ [format(str, model_name: model_name)]
29
+ end
27
30
  alerts = []
28
31
  else
29
32
  alerts = if if_error_alerts
30
- if_error_alerts
31
- else
32
- str = I18n.t(
33
- "activerecord.render_turbo_stream_errors.#{controller_action}"
34
- )
35
- [format(str, model_name: model_name)]
36
- end
33
+ if if_error_alerts.is_a?(String)
34
+ [if_error_alerts]
35
+ else
36
+ if_error_alerts
37
+ end
38
+ else
39
+ str = I18n.t(
40
+ "activerecord.render_turbo_stream_errors.#{controller_action}"
41
+ )
42
+ [format(str, model_name: model_name)]
43
+ end
37
44
  notices = []
38
45
  end
39
46
 
@@ -63,13 +70,13 @@ module RenderTurboStream
63
70
  { turbo_actions: turbo_actions, alerts: alerts, notices: notices }
64
71
  end
65
72
 
66
- def additional_actions( if_success_add, if_error_add, add )
73
+ def additional_actions(if_success_add, if_error_add, add)
67
74
 
68
75
  (@save_action ? make_actions(if_success_add) : make_actions(if_error_add)) + make_actions(add)
69
76
 
70
77
  end
71
78
 
72
- def generate_action( controller_path, target_id, action, partial, locals )
79
+ def generate_action(controller_path, target_id, action, partial, locals)
73
80
  libs = RenderTurboStream::Libs
74
81
  target = libs.target_id_to_target(target_id)
75
82
  _partial = libs.partial_path(nil, target_id, controller_path, partial)
@@ -29,5 +29,18 @@ module RenderTurboStream
29
29
  end
30
30
  end
31
31
 
32
+ def self.fetch_arguments_from_rendered_string(rendered_string)
33
+ noko = Nokogiri::HTML(rendered_string)
34
+ frame = noko.at_css('turbo-frame')
35
+ if frame.present?
36
+ {
37
+ target_id: frame[:id],
38
+ target: target_id_to_target(frame[:id])
39
+ }
40
+ else
41
+ {}
42
+ end
43
+ end
44
+
32
45
  end
33
46
  end
@@ -1,3 +1,3 @@
1
1
  module RenderTurboStream
2
- VERSION = "4.1.6"
2
+ VERSION = "4.3.0"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: render_turbo_stream
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.1.6
4
+ version: 4.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - christian
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-05-17 00:00:00.000000000 Z
11
+ date: 2023-05-25 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -27,10 +27,10 @@ dependencies:
27
27
  description: 'A set of helpers that allow a UNIFIED WORKFLOW for TurboStream and TurboStreams::Channel.
28
28
  To avoid a heavy mix of view and logic, partials and templates can be rendered directly
29
29
  from the controller. There is no need to write *.turbo_stream.* templates anymore.
30
- Logic can stay on the Ruby side, in the controller. Javascript actions can also
31
- be executed directly from the controller. TESTING: To reduce the amount of hard-to-maintain
32
- system tests, this gem allows detailed request tests. A DEMO PROJECT with all this
33
- built in, along with system and request tests, is linked in the README.'
30
+ Logic can stay on in the controller. Javascript actions can also be executed directly
31
+ from the controller. TESTING: To reduce the amount of hard-to-maintain system tests,
32
+ this gem allows detailed request tests. A DEMO PROJECT with all this built in, along
33
+ with system and request tests, is linked in the README.'
34
34
  email:
35
35
  - christian@sedlmair.ch
36
36
  executables: []
@@ -40,7 +40,7 @@ files:
40
40
  - README.md
41
41
  - Rakefile
42
42
  - app/controllers/render_turbo_stream/application_controller.rb
43
- - app/controllers/render_turbo_stream/render_turbo_stream_render_controller.rb
43
+ - app/controllers/render_turbo_stream/render_controller.rb
44
44
  - app/views/layouts/_add_turbo_frame_tag.html.erb
45
45
  - app/views/render_turbo_stream.turbo_stream.erb
46
46
  - app/views/render_turbo_stream_command.html.erb
@@ -86,5 +86,5 @@ requirements: []
86
86
  rubygems_version: 3.4.12
87
87
  signing_key:
88
88
  specification_version: 4
89
- summary: Complete workflow for turbo-rails with a testing strategy.
89
+ summary: Make working with Turbo fun!
90
90
  test_files: []
@@ -1,4 +0,0 @@
1
- module RenderTurboStream
2
- class RenderTurboStreamRenderController < ActionController::Base
3
- end
4
- end