stimulus_reflex 3.4.1 → 3.5.0.pre0
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.
Potentially problematic release.
This version of stimulus_reflex might be problematic. Click here for more details.
- checksums.yaml +4 -4
- data/CHANGELOG.md +587 -495
- data/Gemfile.lock +82 -86
- data/LATEST +1 -0
- data/README.md +10 -10
- data/app/channels/stimulus_reflex/channel.rb +40 -67
- data/lib/generators/USAGE +1 -1
- data/lib/generators/stimulus_reflex/{config_generator.rb → initializer_generator.rb} +3 -3
- data/lib/generators/stimulus_reflex/templates/app/reflexes/%file_name%_reflex.rb.tt +3 -2
- data/lib/generators/stimulus_reflex/templates/app/reflexes/application_reflex.rb.tt +1 -1
- data/lib/generators/stimulus_reflex/templates/config/initializers/stimulus_reflex.rb +6 -1
- data/lib/stimulus_reflex.rb +9 -2
- data/lib/stimulus_reflex/broadcasters/broadcaster.rb +7 -4
- data/lib/stimulus_reflex/broadcasters/page_broadcaster.rb +2 -2
- data/lib/stimulus_reflex/broadcasters/selector_broadcaster.rb +12 -5
- data/lib/stimulus_reflex/cable_ready_channels.rb +6 -2
- data/lib/stimulus_reflex/callbacks.rb +55 -5
- data/lib/stimulus_reflex/concern_enhancer.rb +37 -0
- data/lib/stimulus_reflex/configuration.rb +2 -1
- data/lib/stimulus_reflex/element.rb +31 -7
- data/lib/stimulus_reflex/policies/reflex_invocation_policy.rb +28 -0
- data/lib/stimulus_reflex/reflex.rb +35 -20
- data/lib/stimulus_reflex/reflex_data.rb +79 -0
- data/lib/stimulus_reflex/reflex_factory.rb +31 -0
- data/lib/stimulus_reflex/request_parameters.rb +19 -0
- data/lib/stimulus_reflex/{logger.rb → utils/logger.rb} +0 -2
- data/lib/stimulus_reflex/{sanity_checker.rb → utils/sanity_checker.rb} +58 -10
- data/lib/stimulus_reflex/version.rb +1 -1
- data/lib/tasks/stimulus_reflex/install.rake +6 -4
- data/package.json +6 -5
- data/stimulus_reflex.gemspec +5 -5
- data/test/broadcasters/broadcaster_test_case.rb +1 -1
- data/test/broadcasters/nothing_broadcaster_test.rb +5 -3
- data/test/broadcasters/page_broadcaster_test.rb +8 -4
- data/test/broadcasters/selector_broadcaster_test.rb +171 -55
- data/test/callbacks_test.rb +652 -0
- data/test/concern_enhancer_test.rb +54 -0
- data/test/element_test.rb +181 -0
- data/test/reflex_test.rb +1 -1
- data/test/test_helper.rb +4 -0
- data/test/tmp/app/reflexes/application_reflex.rb +2 -2
- data/test/tmp/app/reflexes/user_reflex.rb +3 -2
- data/yarn.lock +1138 -919
- metadata +39 -28
- data/tags +0 -156
data/lib/generators/USAGE
CHANGED
@@ -3,11 +3,11 @@
|
|
3
3
|
require "rails/generators"
|
4
4
|
|
5
5
|
module StimulusReflex
|
6
|
-
class
|
7
|
-
desc "Creates
|
6
|
+
class InitializerGenerator < Rails::Generators::Base
|
7
|
+
desc "Creates a StimulusReflex initializer in config/initializers"
|
8
8
|
source_root File.expand_path("templates", __dir__)
|
9
9
|
|
10
|
-
def
|
10
|
+
def copy_initializer_file
|
11
11
|
copy_file "config/initializers/stimulus_reflex.rb"
|
12
12
|
end
|
13
13
|
end
|
@@ -17,12 +17,13 @@ class <%= class_name %>Reflex < ApplicationReflex
|
|
17
17
|
# - unsigned - use an unsigned Global ID to map dataset attribute to a model eg. element.unsigned[:foo]
|
18
18
|
# - cable_ready - a special cable_ready that can broadcast to the current visitor (no brackets needed)
|
19
19
|
# - reflex_id - a UUIDv4 that uniquely identies each Reflex
|
20
|
+
# - tab_id - a UUIDv4 that uniquely identifies the browser tab
|
20
21
|
#
|
21
22
|
# Example:
|
22
23
|
#
|
23
24
|
# before_reflex do
|
24
25
|
# # throw :abort # this will prevent the Reflex from continuing
|
25
|
-
# # learn more about callbacks at https://docs.stimulusreflex.com/lifecycle
|
26
|
+
# # learn more about callbacks at https://docs.stimulusreflex.com/rtfm/lifecycle
|
26
27
|
# end
|
27
28
|
#
|
28
29
|
# def example(argument=true)
|
@@ -30,7 +31,7 @@ class <%= class_name %>Reflex < ApplicationReflex
|
|
30
31
|
# # Any declared instance variables will be made available to the Rails controller and view.
|
31
32
|
# end
|
32
33
|
#
|
33
|
-
# Learn more at: https://docs.stimulusreflex.com/
|
34
|
+
# Learn more at: https://docs.stimulusreflex.com/rtfm/reflex-classes
|
34
35
|
|
35
36
|
<% actions.each do |action| -%>
|
36
37
|
def <%= action %>
|
@@ -8,5 +8,5 @@ class ApplicationReflex < StimulusReflex::Reflex
|
|
8
8
|
# # If your ActionCable connection is: `identified_by :current_user`
|
9
9
|
# delegate :current_user, to: :connection
|
10
10
|
#
|
11
|
-
# Learn more at: https://docs.stimulusreflex.com/
|
11
|
+
# Learn more at: https://docs.stimulusreflex.com/rtfm/reflex-classes
|
12
12
|
end
|
@@ -6,6 +6,11 @@ StimulusReflex.configure do |config|
|
|
6
6
|
|
7
7
|
# config.on_failed_sanity_checks = :exit
|
8
8
|
|
9
|
+
# Enable/disable exiting / warning when there's a new StimulusReflex release
|
10
|
+
# `:exit` or `:warn` or `:ignore`
|
11
|
+
|
12
|
+
# config.on_new_version_available = :ignore
|
13
|
+
|
9
14
|
# Override the parent class that the StimulusReflex ActionCable channel inherits from
|
10
15
|
|
11
16
|
# config.parent_channel = "ApplicationCable::Channel"
|
@@ -15,7 +20,7 @@ StimulusReflex.configure do |config|
|
|
15
20
|
# Available colors: red, green, yellow, blue, magenta, cyan, white
|
16
21
|
# You can also use attributes from your ActionCable Connection's identifiers that resolve to valid ActiveRecord models
|
17
22
|
# eg. if your connection is `identified_by :current_user` and your User model has an email attribute, you can access r.email (it will display `-` if the user isn't logged in)
|
18
|
-
# Learn more at: https://docs.stimulusreflex.com/troubleshooting#stimulusreflex-logging
|
23
|
+
# Learn more at: https://docs.stimulusreflex.com/appendices/troubleshooting#stimulusreflex-logging
|
19
24
|
|
20
25
|
# config.logging = proc { "[#{session_id}] #{operation_counter.magenta} #{reflex_info.green} -> #{selector.cyan} via #{mode} Morph (#{operation.yellow})" }
|
21
26
|
|
data/lib/stimulus_reflex.rb
CHANGED
@@ -1,26 +1,33 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require "uri"
|
4
|
+
require "open-uri"
|
4
5
|
require "rack"
|
5
6
|
require "rails/engine"
|
6
7
|
require "active_support/all"
|
7
8
|
require "action_dispatch"
|
8
9
|
require "action_cable"
|
10
|
+
require "action_view"
|
9
11
|
require "nokogiri"
|
10
12
|
require "cable_ready"
|
11
13
|
require "stimulus_reflex/version"
|
12
14
|
require "stimulus_reflex/cable_ready_channels"
|
15
|
+
require "stimulus_reflex/concern_enhancer"
|
13
16
|
require "stimulus_reflex/configuration"
|
14
17
|
require "stimulus_reflex/callbacks"
|
18
|
+
require "stimulus_reflex/request_parameters"
|
15
19
|
require "stimulus_reflex/reflex"
|
20
|
+
require "stimulus_reflex/reflex_data"
|
21
|
+
require "stimulus_reflex/reflex_factory"
|
16
22
|
require "stimulus_reflex/element"
|
17
|
-
require "stimulus_reflex/sanity_checker"
|
18
23
|
require "stimulus_reflex/broadcasters/broadcaster"
|
19
24
|
require "stimulus_reflex/broadcasters/nothing_broadcaster"
|
20
25
|
require "stimulus_reflex/broadcasters/page_broadcaster"
|
21
26
|
require "stimulus_reflex/broadcasters/selector_broadcaster"
|
27
|
+
require "stimulus_reflex/policies/reflex_invocation_policy"
|
22
28
|
require "stimulus_reflex/utils/colorize"
|
23
|
-
require "stimulus_reflex/logger"
|
29
|
+
require "stimulus_reflex/utils/logger"
|
30
|
+
require "stimulus_reflex/utils/sanity_checker"
|
24
31
|
|
25
32
|
module StimulusReflex
|
26
33
|
class Engine < Rails::Engine
|
@@ -3,7 +3,10 @@
|
|
3
3
|
module StimulusReflex
|
4
4
|
class Broadcaster
|
5
5
|
attr_reader :reflex, :logger, :operations
|
6
|
-
delegate :cable_ready, :permanent_attribute_name, to: :reflex
|
6
|
+
delegate :cable_ready, :permanent_attribute_name, :payload, to: :reflex
|
7
|
+
|
8
|
+
DEFAULT_HTML_WITHOUT_FORMAT = Nokogiri::XML::Node::SaveOptions::DEFAULT_HTML &
|
9
|
+
~Nokogiri::XML::Node::SaveOptions::FORMAT
|
7
10
|
|
8
11
|
def initialize(reflex)
|
9
12
|
@reflex = reflex
|
@@ -23,13 +26,13 @@ module StimulusReflex
|
|
23
26
|
false
|
24
27
|
end
|
25
28
|
|
26
|
-
def broadcast_message(subject:,
|
27
|
-
logger.error "\e[31m#{body}\e[0m" if subject == "error"
|
29
|
+
def broadcast_message(subject:, data: {}, error: nil)
|
28
30
|
operations << ["document", :dispatch_event]
|
29
31
|
cable_ready.dispatch_event(
|
30
32
|
name: "stimulus-reflex:server-message",
|
31
33
|
detail: {
|
32
|
-
reflexId: data
|
34
|
+
reflexId: data.delete("reflexId"),
|
35
|
+
payload: payload,
|
33
36
|
stimulus_reflex: data.merge(
|
34
37
|
morph: to_sym,
|
35
38
|
server_message: {subject: subject, body: error&.to_s}
|
@@ -12,10 +12,11 @@ module StimulusReflex
|
|
12
12
|
selectors = selectors.select { |s| document.css(s).present? }
|
13
13
|
selectors.each do |selector|
|
14
14
|
operations << [selector, :morph]
|
15
|
-
html = document.css(selector).inner_html
|
15
|
+
html = document.css(selector).inner_html(save_with: Broadcaster::DEFAULT_HTML_WITHOUT_FORMAT)
|
16
16
|
cable_ready.morph(
|
17
17
|
selector: selector,
|
18
18
|
html: html,
|
19
|
+
payload: payload,
|
19
20
|
children_only: true,
|
20
21
|
permanent_attribute_name: permanent_attribute_name,
|
21
22
|
stimulus_reflex: data.merge({
|
@@ -23,7 +24,6 @@ module StimulusReflex
|
|
23
24
|
})
|
24
25
|
)
|
25
26
|
end
|
26
|
-
|
27
27
|
cable_ready.broadcast
|
28
28
|
end
|
29
29
|
|
@@ -2,19 +2,25 @@
|
|
2
2
|
|
3
3
|
module StimulusReflex
|
4
4
|
class SelectorBroadcaster < Broadcaster
|
5
|
+
include CableReady::Identifiable
|
6
|
+
|
5
7
|
def broadcast(_, data = {})
|
6
8
|
morphs.each do |morph|
|
7
9
|
selectors, html = morph
|
8
|
-
updates = selectors.is_a?(Hash) ? selectors :
|
9
|
-
updates.each do |
|
10
|
-
html =
|
11
|
-
|
10
|
+
updates = selectors.is_a?(Hash) ? selectors : {selectors => html}
|
11
|
+
updates.each do |key, value|
|
12
|
+
html = reflex.render(key) if key.is_a?(ActiveRecord::Base) && value.nil?
|
13
|
+
html = reflex.render_collection(key) if key.is_a?(ActiveRecord::Relation) && value.nil?
|
14
|
+
html ||= value
|
15
|
+
fragment = Nokogiri::HTML.fragment(html.to_s)
|
16
|
+
selector = key.is_a?(ActiveRecord::Base) || key.is_a?(ActiveRecord::Relation) ? dom_id(key) : key.to_s
|
12
17
|
match = fragment.at_css(selector)
|
13
18
|
if match.present?
|
14
19
|
operations << [selector, :morph]
|
15
20
|
cable_ready.morph(
|
16
21
|
selector: selector,
|
17
|
-
html: match.inner_html,
|
22
|
+
html: match.inner_html(save_with: Broadcaster::DEFAULT_HTML_WITHOUT_FORMAT),
|
23
|
+
payload: payload,
|
18
24
|
children_only: true,
|
19
25
|
permanent_attribute_name: permanent_attribute_name,
|
20
26
|
stimulus_reflex: data.merge({
|
@@ -26,6 +32,7 @@ module StimulusReflex
|
|
26
32
|
cable_ready.inner_html(
|
27
33
|
selector: selector,
|
28
34
|
html: fragment.to_html,
|
35
|
+
payload: payload,
|
29
36
|
stimulus_reflex: data.merge({
|
30
37
|
morph: to_sym
|
31
38
|
})
|
@@ -4,8 +4,9 @@ module StimulusReflex
|
|
4
4
|
class CableReadyChannels
|
5
5
|
delegate :[], to: "cable_ready_channels"
|
6
6
|
|
7
|
-
def initialize(stream_name)
|
7
|
+
def initialize(stream_name, reflex_id)
|
8
8
|
@stream_name = stream_name
|
9
|
+
@reflex_id = reflex_id
|
9
10
|
end
|
10
11
|
|
11
12
|
def cable_ready_channels
|
@@ -17,7 +18,10 @@ module StimulusReflex
|
|
17
18
|
end
|
18
19
|
|
19
20
|
def method_missing(name, *args)
|
20
|
-
|
21
|
+
if stimulus_reflex_channel.respond_to?(name)
|
22
|
+
args[0][:reflex_id] = @reflex_id if args.any?
|
23
|
+
return stimulus_reflex_channel.public_send(name, *args)
|
24
|
+
end
|
21
25
|
super
|
22
26
|
end
|
23
27
|
|
@@ -22,24 +22,74 @@ module StimulusReflex
|
|
22
22
|
add_callback(:around, *args, &block)
|
23
23
|
end
|
24
24
|
|
25
|
+
def prepend_before_reflex(*args, &block)
|
26
|
+
prepend_callback(:before, *args, &block)
|
27
|
+
end
|
28
|
+
|
29
|
+
def prepend_after_reflex(*args, &block)
|
30
|
+
prepend_callback(:after, *args, &block)
|
31
|
+
end
|
32
|
+
|
33
|
+
def prepend_around_reflex(*args, &block)
|
34
|
+
prepend_callback(:around, *args, &block)
|
35
|
+
end
|
36
|
+
|
37
|
+
def skip_before_reflex(*args, &block)
|
38
|
+
omit_callback(:before, *args, &block)
|
39
|
+
end
|
40
|
+
|
41
|
+
def skip_after_reflex(*args, &block)
|
42
|
+
omit_callback(:after, *args, &block)
|
43
|
+
end
|
44
|
+
|
45
|
+
def skip_around_reflex(*args, &block)
|
46
|
+
omit_callback(:around, *args, &block)
|
47
|
+
end
|
48
|
+
|
49
|
+
alias_method :append_before_reflex, :before_reflex
|
50
|
+
alias_method :append_around_reflex, :around_reflex
|
51
|
+
alias_method :append_after_reflex, :after_reflex
|
52
|
+
|
25
53
|
private
|
26
54
|
|
27
55
|
def add_callback(kind, *args, &block)
|
28
|
-
|
29
|
-
|
30
|
-
|
56
|
+
insert_callbacks(args, block) do |name, options|
|
57
|
+
set_callback(:process, kind, name, options)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def prepend_callback(kind, *args, &block)
|
62
|
+
insert_callbacks(args, block) do |name, options|
|
63
|
+
set_callback(:process, kind, name, options.merge(prepend: true))
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def omit_callback(kind, *args, &block)
|
68
|
+
insert_callbacks(args) do |name, options|
|
69
|
+
skip_callback(:process, kind, name, options)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def insert_callbacks(callbacks, block = nil)
|
74
|
+
options = callbacks.extract_options!
|
75
|
+
normalize_callback_options!(options)
|
76
|
+
|
77
|
+
callbacks.push(block) if block
|
78
|
+
|
79
|
+
callbacks.each do |callback|
|
80
|
+
yield callback, options
|
81
|
+
end
|
31
82
|
end
|
32
83
|
|
33
84
|
def normalize_callback_options!(options)
|
34
85
|
normalize_callback_option! options, :only, :if
|
35
86
|
normalize_callback_option! options, :except, :unless
|
36
|
-
options
|
37
87
|
end
|
38
88
|
|
39
89
|
def normalize_callback_option!(options, from, to)
|
40
90
|
if (from = options.delete(from))
|
41
91
|
from_set = Array(from).map(&:to_s).to_set
|
42
|
-
from = proc { |reflex| from_set.include? reflex.method_name }
|
92
|
+
from = proc { |reflex| from_set.include? reflex.method_name.to_s }
|
43
93
|
options[to] = Array(options[to]).unshift(from)
|
44
94
|
end
|
45
95
|
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module StimulusReflex
|
2
|
+
module ConcernEnhancer
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
class_methods do
|
6
|
+
def method_missing(name, *args)
|
7
|
+
case ancestors
|
8
|
+
when ->(a) { !(a & [StimulusReflex::Reflex]).empty? }
|
9
|
+
if (ActiveRecord::Base.public_methods + ActionController::Base.public_methods).include? name
|
10
|
+
nil
|
11
|
+
else
|
12
|
+
super
|
13
|
+
end
|
14
|
+
when ->(a) { !(a & [ActiveRecord::Base, ActionController::Base]).empty? }
|
15
|
+
if StimulusReflex::Reflex.public_methods.include? name
|
16
|
+
nil
|
17
|
+
else
|
18
|
+
super
|
19
|
+
end
|
20
|
+
else
|
21
|
+
super
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def respond_to_missing?(name, include_all = false)
|
26
|
+
case ancestors
|
27
|
+
when ->(a) { !(a & [StimulusReflex::Reflex]).empty? }
|
28
|
+
(ActiveRecord::Base.public_methods + ActionController::Base.public_methods).include?(name) || super
|
29
|
+
when ->(a) { !(a & [ActiveRecord::Base, ActionController::Base]).empty? }
|
30
|
+
StimulusReflex::Reflex.public_methods.include?(name) || super
|
31
|
+
else
|
32
|
+
super
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -14,12 +14,13 @@ module StimulusReflex
|
|
14
14
|
end
|
15
15
|
|
16
16
|
class Configuration
|
17
|
-
attr_accessor :on_failed_sanity_checks, :parent_channel, :logging, :middleware
|
17
|
+
attr_accessor :on_failed_sanity_checks, :on_new_version_available, :parent_channel, :logging, :middleware
|
18
18
|
|
19
19
|
DEFAULT_LOGGING = proc { "[#{session_id}] #{operation_counter.magenta} #{reflex_info.green} -> #{selector.cyan} via #{mode} Morph (#{operation.yellow})" }
|
20
20
|
|
21
21
|
def initialize
|
22
22
|
@on_failed_sanity_checks = :exit
|
23
|
+
@on_new_version_available = :ignore
|
23
24
|
@parent_channel = "ApplicationCable::Channel"
|
24
25
|
@logging = DEFAULT_LOGGING
|
25
26
|
@middleware = ActionDispatch::MiddlewareStack.new
|
@@ -1,14 +1,16 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
class StimulusReflex::Element < OpenStruct
|
4
|
-
attr_reader :
|
4
|
+
attr_reader :attrs, :data_attrs
|
5
5
|
|
6
6
|
def initialize(data = {})
|
7
|
-
@
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
7
|
+
@attrs = HashWithIndifferentAccess.new(data["attrs"] || {})
|
8
|
+
datasets = data["dataset"] || {}
|
9
|
+
regular_dataset = datasets["dataset"] || {}
|
10
|
+
@data_attrs = build_data_attrs(regular_dataset, datasets["datasetAll"] || {})
|
11
|
+
all_attributes = @attrs.merge(@data_attrs)
|
12
|
+
super build_underscored(all_attributes)
|
13
|
+
@data_attrs.transform_keys! { |key| key.delete_prefix "data-" }
|
12
14
|
end
|
13
15
|
|
14
16
|
def signed
|
@@ -19,7 +21,29 @@ class StimulusReflex::Element < OpenStruct
|
|
19
21
|
@unsigned ||= ->(accessor) { GlobalID::Locator.locate(dataset[accessor]) }
|
20
22
|
end
|
21
23
|
|
24
|
+
def attributes
|
25
|
+
@attributes ||= OpenStruct.new(build_underscored(attrs))
|
26
|
+
end
|
27
|
+
|
22
28
|
def dataset
|
23
|
-
@dataset ||= OpenStruct.new(
|
29
|
+
@dataset ||= OpenStruct.new(build_underscored(data_attrs))
|
30
|
+
end
|
31
|
+
|
32
|
+
alias_method :data_attributes, :dataset
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def build_data_attrs(dataset, dataset_all)
|
37
|
+
dataset_all.transform_keys! { |key| "data-#{key.delete_prefix("data-").pluralize}" }
|
38
|
+
|
39
|
+
dataset.each { |key, value| dataset_all[key]&.prepend(value) }
|
40
|
+
|
41
|
+
data_attrs = dataset.merge(dataset_all)
|
42
|
+
|
43
|
+
HashWithIndifferentAccess.new(data_attrs || {})
|
44
|
+
end
|
45
|
+
|
46
|
+
def build_underscored(attrs)
|
47
|
+
attrs.merge(attrs.transform_keys(&:underscore))
|
24
48
|
end
|
25
49
|
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module StimulusReflex
|
4
|
+
class ReflexMethodInvocationPolicy
|
5
|
+
attr_reader :arguments, :required_params, :optional_params
|
6
|
+
|
7
|
+
def initialize(method, arguments)
|
8
|
+
@arguments = arguments
|
9
|
+
@required_params = method.parameters.select { |(kind, _)| kind == :req }
|
10
|
+
@optional_params = method.parameters.select { |(kind, _)| kind == :opt }
|
11
|
+
end
|
12
|
+
|
13
|
+
def no_arguments?
|
14
|
+
arguments.size == 0 && required_params.size == 0
|
15
|
+
end
|
16
|
+
|
17
|
+
def arguments?
|
18
|
+
arguments.size >= required_params.size && arguments.size <= required_params.size + optional_params.size
|
19
|
+
end
|
20
|
+
|
21
|
+
def unknown?
|
22
|
+
return false if no_arguments?
|
23
|
+
return false if arguments?
|
24
|
+
|
25
|
+
true
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|