live_cable 0.0.1 → 0.1.2
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/LICENSE +21 -0
- data/README.md +1275 -0
- data/app/assets/javascript/controllers/live_controller.js +79 -44
- data/app/assets/javascript/dom.js +161 -0
- data/app/assets/javascript/live_cable.js +50 -0
- data/app/assets/javascript/live_cable_blessing.js +1 -1
- data/app/assets/javascript/observer.js +74 -0
- data/app/assets/javascript/subscriptions.js +396 -37
- data/app/channels/live_channel.rb +17 -14
- data/app/helpers/live_cable_helper.rb +28 -39
- data/config/importmap.rb +9 -3
- data/lib/generators/live_cable/component/component_generator.rb +58 -0
- data/lib/generators/live_cable/component/templates/component.rb.tt +29 -0
- data/lib/generators/live_cable/component/templates/view.html.live.erb.tt +2 -0
- data/lib/live_cable/component/broadcasting.rb +30 -0
- data/lib/live_cable/component/identification.rb +31 -0
- data/lib/live_cable/component/lifecycle.rb +67 -0
- data/lib/live_cable/component/method_dependency_tracking.rb +22 -0
- data/lib/live_cable/component/reactive_variables.rb +125 -0
- data/lib/live_cable/component/rendering.rb +177 -0
- data/lib/live_cable/component/streaming.rb +43 -0
- data/lib/live_cable/component.rb +21 -236
- data/lib/live_cable/configuration.rb +29 -0
- data/lib/live_cable/connection/broadcasting.rb +33 -0
- data/lib/live_cable/connection/channel_management.rb +13 -0
- data/lib/live_cable/connection/component_management.rb +38 -0
- data/lib/live_cable/connection/error_handling.rb +40 -0
- data/lib/live_cable/connection/messaging.rb +84 -0
- data/lib/live_cable/connection/state_management.rb +56 -0
- data/lib/live_cable/connection.rb +11 -180
- data/lib/live_cable/container.rb +25 -0
- data/lib/live_cable/delegation/array.rb +1 -0
- data/lib/live_cable/delegator.rb +0 -7
- data/lib/live_cable/engine.rb +15 -3
- data/lib/live_cable/observer.rb +5 -1
- data/lib/live_cable/observer_tracking.rb +20 -0
- data/lib/live_cable/render_context.rb +55 -8
- data/lib/live_cable/rendering/compiler.rb +80 -0
- data/lib/live_cable/rendering/dependency_visitor.rb +100 -0
- data/lib/live_cable/rendering/handler.rb +19 -0
- data/lib/live_cable/rendering/method_analyzer.rb +94 -0
- data/lib/live_cable/rendering/method_collector.rb +51 -0
- data/lib/live_cable/rendering/method_dependency_visitor.rb +51 -0
- data/lib/live_cable/rendering/partial.rb +93 -0
- data/lib/live_cable/rendering/partial_renderer.rb +145 -0
- data/lib/live_cable/rendering/render_result.rb +38 -0
- data/lib/live_cable/rendering/renderer.rb +150 -0
- data/lib/live_cable/version.rb +5 -0
- data/lib/live_cable.rb +15 -15
- metadata +124 -4
|
@@ -2,135 +2,29 @@
|
|
|
2
2
|
|
|
3
3
|
module LiveCable
|
|
4
4
|
class Connection
|
|
5
|
-
|
|
5
|
+
include ComponentManagement
|
|
6
|
+
include ChannelManagement
|
|
7
|
+
include StateManagement
|
|
8
|
+
include Messaging
|
|
9
|
+
include Broadcasting
|
|
10
|
+
include ErrorHandling
|
|
6
11
|
|
|
7
12
|
SHARED_CONTAINER = '_shared'
|
|
8
13
|
|
|
9
14
|
def initialize(request)
|
|
10
15
|
@request = request
|
|
11
16
|
@session_id = SecureRandom.uuid
|
|
12
|
-
@containers =
|
|
17
|
+
@containers = Hash.new { |hash, key| hash[key] = Container.new }
|
|
13
18
|
@components = {}
|
|
14
19
|
@channels = {}
|
|
15
20
|
end
|
|
16
21
|
|
|
17
|
-
def get_component(id)
|
|
18
|
-
components[id]
|
|
19
|
-
end
|
|
20
|
-
|
|
21
|
-
def add_component(component)
|
|
22
|
-
component.live_connection = self
|
|
23
|
-
components[component.live_id] = component
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
def remove_component(component)
|
|
27
|
-
components.delete(component.live_id)
|
|
28
|
-
containers.delete(component.live_id)
|
|
29
|
-
end
|
|
30
|
-
|
|
31
|
-
def set_channel(component, channel)
|
|
32
|
-
@channels[component] = channel
|
|
33
|
-
end
|
|
34
|
-
|
|
35
|
-
def get_channel(component)
|
|
36
|
-
@channels[component]
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
def remove_channel(component)
|
|
40
|
-
@channels.delete(component)
|
|
41
|
-
end
|
|
42
|
-
|
|
43
|
-
def get(container_name, component, variable, initial_value)
|
|
44
|
-
containers[container_name] ||= Container.new
|
|
45
|
-
containers[container_name][variable] ||= process_initial_value(component, variable, initial_value)
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
def set(container_name, variable, value)
|
|
49
|
-
dirty(container_name, variable)
|
|
50
|
-
|
|
51
|
-
containers[container_name] ||= Container.new
|
|
52
|
-
containers[container_name][variable] = value
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
def receive(component, data)
|
|
56
|
-
check_csrf_token(data)
|
|
57
|
-
reset_changeset
|
|
58
|
-
|
|
59
|
-
return unless data['messages'].present?
|
|
60
|
-
|
|
61
|
-
data['messages'].each do |message|
|
|
62
|
-
action(component, message)
|
|
63
|
-
end
|
|
64
|
-
|
|
65
|
-
broadcast_changeset
|
|
66
|
-
end
|
|
67
|
-
|
|
68
|
-
def action(component, data)
|
|
69
|
-
params = parse_params(data)
|
|
70
|
-
|
|
71
|
-
if data['_action']
|
|
72
|
-
action = data['_action']&.to_sym
|
|
73
|
-
|
|
74
|
-
if action == :_reactive
|
|
75
|
-
return reactive(component, data)
|
|
76
|
-
end
|
|
77
|
-
|
|
78
|
-
unless component.class.allowed_actions.include?(action)
|
|
79
|
-
raise LiveCable::Error, "Unauthorized action: #{action}"
|
|
80
|
-
end
|
|
81
|
-
|
|
82
|
-
method = component.method(action)
|
|
83
|
-
|
|
84
|
-
if method.arity.positive?
|
|
85
|
-
method.call(params)
|
|
86
|
-
else
|
|
87
|
-
method.call
|
|
88
|
-
end
|
|
89
|
-
end
|
|
90
|
-
rescue StandardError => e
|
|
91
|
-
handle_error(component, e)
|
|
92
|
-
end
|
|
93
|
-
|
|
94
|
-
def reactive(component, data)
|
|
95
|
-
unless component.all_reactive_variables.include?(data['name'].to_sym)
|
|
96
|
-
raise Error, "Invalid reactive variable: #{data['name']}"
|
|
97
|
-
end
|
|
98
|
-
|
|
99
|
-
component.public_send("#{data['name']}=", data['value'])
|
|
100
|
-
rescue StandardError => e
|
|
101
|
-
handle_error(component, e)
|
|
102
|
-
end
|
|
103
|
-
|
|
104
|
-
def channel_name
|
|
105
|
-
"live_#{session_id}"
|
|
106
|
-
end
|
|
107
|
-
|
|
108
|
-
def dirty(container_name, *variables)
|
|
109
|
-
containers[container_name] ||= Container.new
|
|
110
|
-
containers[container_name].mark_dirty(*variables)
|
|
111
|
-
end
|
|
112
|
-
|
|
113
|
-
def broadcast_changeset
|
|
114
|
-
# Use a copy of the components since new ones can get added while rendering
|
|
115
|
-
# and causes an issue here.
|
|
116
|
-
components.values.dup.each do |component|
|
|
117
|
-
container = containers[component.live_id]
|
|
118
|
-
if container&.changed?
|
|
119
|
-
component.render_broadcast
|
|
120
|
-
|
|
121
|
-
next
|
|
122
|
-
end
|
|
123
|
-
|
|
124
|
-
shared_changeset = containers[SHARED_CONTAINER]&.changeset || []
|
|
125
|
-
|
|
126
|
-
if (component.shared_reactive_variables || []).intersect?(shared_changeset)
|
|
127
|
-
component.render_broadcast
|
|
128
|
-
end
|
|
129
|
-
end
|
|
130
|
-
end
|
|
131
|
-
|
|
132
22
|
private
|
|
133
23
|
|
|
24
|
+
# @return [String]
|
|
25
|
+
attr_reader :session_id
|
|
26
|
+
|
|
27
|
+
# @return [ActionDispatch::Request]
|
|
134
28
|
attr_reader :request
|
|
135
29
|
|
|
136
30
|
# @return [Hash<String, Container>]
|
|
@@ -138,68 +32,5 @@ module LiveCable
|
|
|
138
32
|
|
|
139
33
|
# @return [Hash<String, Component>]
|
|
140
34
|
attr_reader :components
|
|
141
|
-
|
|
142
|
-
def check_csrf_token(data)
|
|
143
|
-
session = request.session
|
|
144
|
-
return unless session[:_csrf_token]
|
|
145
|
-
|
|
146
|
-
token = data['_csrf_token']
|
|
147
|
-
unless csrf_checker.valid?(session, token)
|
|
148
|
-
raise LiveCable::Error, 'Invalid CSRF token'
|
|
149
|
-
end
|
|
150
|
-
end
|
|
151
|
-
|
|
152
|
-
def csrf_checker
|
|
153
|
-
@csrf_checker ||= LiveCable::CsrfChecker.new(request)
|
|
154
|
-
end
|
|
155
|
-
|
|
156
|
-
def process_initial_value(component, variable, initial_value)
|
|
157
|
-
case initial_value
|
|
158
|
-
when nil
|
|
159
|
-
nil
|
|
160
|
-
when Proc
|
|
161
|
-
args = []
|
|
162
|
-
args << component if initial_value.arity.positive?
|
|
163
|
-
|
|
164
|
-
initial_value.call(*args)
|
|
165
|
-
else
|
|
166
|
-
raise Error, "Initial value for \":#{variable}\" must be a proc or nil"
|
|
167
|
-
end
|
|
168
|
-
rescue StandardError => e
|
|
169
|
-
handle_error(component, e)
|
|
170
|
-
end
|
|
171
|
-
|
|
172
|
-
def parse_params(data)
|
|
173
|
-
params = data['params'] || ''
|
|
174
|
-
|
|
175
|
-
ActionController::Parameters.new(
|
|
176
|
-
ActionDispatch::ParamBuilder.from_pairs(
|
|
177
|
-
ActionDispatch::QueryParser.each_pair(params)
|
|
178
|
-
)
|
|
179
|
-
)
|
|
180
|
-
end
|
|
181
|
-
|
|
182
|
-
def reset_changeset
|
|
183
|
-
containers.each_value(&:reset_changeset)
|
|
184
|
-
end
|
|
185
|
-
|
|
186
|
-
def handle_error(component, error)
|
|
187
|
-
html = <<~HTML
|
|
188
|
-
<details>
|
|
189
|
-
<summary style="color: #f00; cursor: pointer">
|
|
190
|
-
<strong>#{component.class.name}</strong> - #{error.class.name}: #{error.message}
|
|
191
|
-
</summary>
|
|
192
|
-
<small>
|
|
193
|
-
<ol>
|
|
194
|
-
#{error.backtrace&.map { "<li>#{it}</li>" }&.join("\n")}
|
|
195
|
-
</ol>
|
|
196
|
-
</small>
|
|
197
|
-
</details>
|
|
198
|
-
HTML
|
|
199
|
-
|
|
200
|
-
component.broadcast(_refresh: html)
|
|
201
|
-
ensure
|
|
202
|
-
raise(error)
|
|
203
|
-
end
|
|
204
35
|
end
|
|
205
36
|
end
|
data/lib/live_cable/container.rb
CHANGED
|
@@ -45,6 +45,9 @@ module LiveCable
|
|
|
45
45
|
# container[:tags] = ['ruby'] # Array - wrapped in Delegator
|
|
46
46
|
# container[:user] = User.new # ActiveRecord - wrapped in Delegator
|
|
47
47
|
def []=(key, value)
|
|
48
|
+
# Remove observer from the old value so it stops notifying this container
|
|
49
|
+
self[key].try(:remove_live_cable_observer, observer, key)
|
|
50
|
+
|
|
48
51
|
# ActiveRecord models get observers attached directly
|
|
49
52
|
if value.class < ModelObserver
|
|
50
53
|
value.add_live_cable_observer(observer, key)
|
|
@@ -99,5 +102,27 @@ module LiveCable
|
|
|
99
102
|
def observer
|
|
100
103
|
@observer ||= Observer.new(self)
|
|
101
104
|
end
|
|
105
|
+
|
|
106
|
+
# Clean up all observer references to prevent memory leaks.
|
|
107
|
+
# This breaks the reference chain between delegators, observers, and the container.
|
|
108
|
+
#
|
|
109
|
+
# Should be called when the component is destroyed.
|
|
110
|
+
#
|
|
111
|
+
# @return [void]
|
|
112
|
+
def cleanup
|
|
113
|
+
# Remove this container's observer from all delegated values
|
|
114
|
+
# Note: We only remove this specific observer, not all observers,
|
|
115
|
+
# because the same object might be shared across multiple containers
|
|
116
|
+
each do |variable, value|
|
|
117
|
+
if value.respond_to?(:remove_live_cable_observer)
|
|
118
|
+
value.remove_live_cable_observer(observer, variable)
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Clear the container's data
|
|
123
|
+
clear
|
|
124
|
+
@changeset&.clear
|
|
125
|
+
@observer = nil
|
|
126
|
+
end
|
|
102
127
|
end
|
|
103
128
|
end
|
data/lib/live_cable/delegator.rb
CHANGED
|
@@ -4,13 +4,6 @@ module LiveCable
|
|
|
4
4
|
# Delegation modules for different data types.
|
|
5
5
|
# Each module defines which methods should trigger change notifications.
|
|
6
6
|
module Delegation
|
|
7
|
-
extend ActiveSupport::Autoload
|
|
8
|
-
|
|
9
|
-
autoload :Array
|
|
10
|
-
autoload :Hash
|
|
11
|
-
autoload :Methods
|
|
12
|
-
autoload :Model
|
|
13
|
-
|
|
14
7
|
# Maps Ruby classes to their delegation modules.
|
|
15
8
|
# When a value of one of these types is stored in a container,
|
|
16
9
|
# it will be wrapped in a Delegator and extended with the appropriate module.
|
data/lib/live_cable/engine.rb
CHANGED
|
@@ -4,16 +4,28 @@ module LiveCable
|
|
|
4
4
|
class Engine < ::Rails::Engine
|
|
5
5
|
config.before_configuration do |app|
|
|
6
6
|
# Setup autoloader to use Live namespace for components
|
|
7
|
-
|
|
7
|
+
live_component_dir = app.root.join('app/live')
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
if live_component_dir.directory?
|
|
10
|
+
Rails.autoloaders.main.push_dir(live_component_dir, namespace: Live)
|
|
11
|
+
else
|
|
12
|
+
warn("[LiveCable Warning] #{live_component_dir} does not exist for components.")
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Add LiveCable to importmap (skip when using jsbundling/npm)
|
|
16
|
+
if app.config.respond_to?(:importmap)
|
|
17
|
+
app.config.importmap.paths << root.join('config/importmap.rb')
|
|
18
|
+
end
|
|
11
19
|
end
|
|
12
20
|
|
|
13
21
|
initializer 'live_cable.assets_precompile' do |app|
|
|
14
22
|
app.config.assets.precompile << %w[live_cable/**/*.js]
|
|
15
23
|
end
|
|
16
24
|
|
|
25
|
+
initializer 'live_cable.renderer' do |_app|
|
|
26
|
+
ActionView::Template.register_template_handler(:'live.erb', Rendering::Handler)
|
|
27
|
+
end
|
|
28
|
+
|
|
17
29
|
initializer 'live_cable.active_record' do
|
|
18
30
|
ActiveSupport.on_load :active_record do
|
|
19
31
|
include ModelObserver
|
data/lib/live_cable/observer.rb
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'weakref'
|
|
4
|
+
|
|
3
5
|
module LiveCable
|
|
4
6
|
# Observer pattern implementation for tracking changes to reactive variables.
|
|
5
7
|
#
|
|
@@ -21,7 +23,7 @@ module LiveCable
|
|
|
21
23
|
class Observer
|
|
22
24
|
# @param container [LiveCable::Container] The container to notify of changes
|
|
23
25
|
def initialize(container)
|
|
24
|
-
@container = container
|
|
26
|
+
@container = WeakRef.new(container)
|
|
25
27
|
end
|
|
26
28
|
|
|
27
29
|
# Notify the container that a variable has changed.
|
|
@@ -34,6 +36,8 @@ module LiveCable
|
|
|
34
36
|
# observer.notify(:tags) # Component will re-render because :tags changed
|
|
35
37
|
def notify(variable)
|
|
36
38
|
container.mark_dirty(variable)
|
|
39
|
+
rescue WeakRef::RefError
|
|
40
|
+
# Container was already GC'd — nothing to do
|
|
37
41
|
end
|
|
38
42
|
|
|
39
43
|
private
|
|
@@ -41,6 +41,26 @@ module LiveCable
|
|
|
41
41
|
end
|
|
42
42
|
end
|
|
43
43
|
|
|
44
|
+
# Remove an observer from tracking changes to a specific variable.
|
|
45
|
+
#
|
|
46
|
+
# @param observer [LiveCable::Observer] The observer to remove
|
|
47
|
+
# @param variable [Symbol] The reactive variable name
|
|
48
|
+
# @return [void]
|
|
49
|
+
def remove_live_cable_observer(observer, variable)
|
|
50
|
+
observers = live_cable_observers_for(variable)
|
|
51
|
+
observers.delete(observer)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def initialize_dup(other)
|
|
55
|
+
super
|
|
56
|
+
@live_cable_observers = {}
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def initialize_clone(other)
|
|
60
|
+
super
|
|
61
|
+
@live_cable_observers = {}
|
|
62
|
+
end
|
|
63
|
+
|
|
44
64
|
private
|
|
45
65
|
|
|
46
66
|
# Get the hash of all observers, keyed by variable name.
|
|
@@ -2,28 +2,75 @@
|
|
|
2
2
|
|
|
3
3
|
module LiveCable
|
|
4
4
|
class RenderContext
|
|
5
|
-
def initialize(component)
|
|
5
|
+
def initialize(component, root: nil)
|
|
6
6
|
@component = component
|
|
7
7
|
@children = []
|
|
8
|
+
@root = root
|
|
9
|
+
@render_results = {}
|
|
10
|
+
@children_by_part = {}
|
|
11
|
+
@current_part = nil
|
|
8
12
|
end
|
|
9
13
|
|
|
10
14
|
# @return [Array<LiveCable::Component>]
|
|
11
15
|
attr_reader :children
|
|
12
16
|
|
|
13
|
-
|
|
14
|
-
|
|
17
|
+
# @return [LiveCable::Component]
|
|
18
|
+
attr_reader :component
|
|
19
|
+
|
|
20
|
+
# @return [Hash<String, RenderResult>]
|
|
21
|
+
attr_reader :render_results
|
|
22
|
+
|
|
23
|
+
# @return [Hash<Integer, Array<LiveCable::Component>>]
|
|
24
|
+
attr_reader :children_by_part
|
|
25
|
+
|
|
26
|
+
def render_part(index)
|
|
27
|
+
@current_part = index
|
|
28
|
+
result = yield
|
|
29
|
+
@children_by_part[index] ||= [] unless result.nil?
|
|
30
|
+
result
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Returns children from the previous context that came from parts which were
|
|
34
|
+
# skipped (not rendered) in this render cycle. These children should not be
|
|
35
|
+
# destroyed — their part simply didn't re-evaluate.
|
|
36
|
+
#
|
|
37
|
+
# @param previous_context [RenderContext]
|
|
38
|
+
# @return [Array<LiveCable::Component>]
|
|
39
|
+
def preserved_children_from(previous_context)
|
|
40
|
+
previous_context.children_by_part.each_with_object([]) do |(part, part_children), preserved|
|
|
41
|
+
preserved.concat(part_children) unless @children_by_part.key?(part)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def root?
|
|
46
|
+
root.nil?
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def clear
|
|
50
|
+
@children = nil
|
|
51
|
+
@component = nil
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# @param result [RenderResult]
|
|
55
|
+
def <<(result)
|
|
56
|
+
if root?
|
|
57
|
+
render_results[result.live_id] = result
|
|
58
|
+
else
|
|
59
|
+
root << result
|
|
60
|
+
end
|
|
15
61
|
end
|
|
16
62
|
|
|
17
63
|
def add_component(child)
|
|
18
|
-
return unless
|
|
64
|
+
return unless live_connection
|
|
19
65
|
|
|
20
|
-
|
|
66
|
+
live_connection.add_component(child)
|
|
21
67
|
children << child
|
|
68
|
+
(@children_by_part[@current_part] ||= []) << child
|
|
22
69
|
end
|
|
23
70
|
|
|
24
71
|
# @return [LiveCable::Component, nil]
|
|
25
72
|
def get_component(live_id)
|
|
26
|
-
|
|
73
|
+
live_connection&.get_component(live_id)
|
|
27
74
|
end
|
|
28
75
|
|
|
29
76
|
# @return [LiveCable::Connection]
|
|
@@ -33,7 +80,7 @@ module LiveCable
|
|
|
33
80
|
|
|
34
81
|
private
|
|
35
82
|
|
|
36
|
-
# @return [LiveCable::
|
|
37
|
-
attr_reader :
|
|
83
|
+
# @return [LiveCable::RenderContext, nil]
|
|
84
|
+
attr_reader :root
|
|
38
85
|
end
|
|
39
86
|
end
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module LiveCable
|
|
4
|
+
module Rendering
|
|
5
|
+
class Compiler < ::Herb::Engine::Compiler
|
|
6
|
+
def visit_erb_control_node(node)
|
|
7
|
+
@tokens << [:block_start]
|
|
8
|
+
super
|
|
9
|
+
@tokens << [:block_end]
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def visit_erb_block_node(node)
|
|
13
|
+
@tokens << [:block_start]
|
|
14
|
+
super
|
|
15
|
+
@tokens << [:block_end]
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def generate_output
|
|
19
|
+
tokens = optimize_tokens(@tokens)
|
|
20
|
+
|
|
21
|
+
tokens.map do |type, value, context|
|
|
22
|
+
if type == :block
|
|
23
|
+
value.map { |token| generate_for_token(*token) }
|
|
24
|
+
else
|
|
25
|
+
generate_for_token(type, value, context)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
@engine.send(:finish_method, type)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def generate_for_token(type, value, context)
|
|
33
|
+
case type
|
|
34
|
+
when :text
|
|
35
|
+
@engine.send(:add_text, value)
|
|
36
|
+
when :code
|
|
37
|
+
@engine.send(:add_code, value)
|
|
38
|
+
when :expr
|
|
39
|
+
indicator = @escape ? '==' : '='
|
|
40
|
+
@engine.send(:add_expression, indicator, value)
|
|
41
|
+
when :expr_escaped
|
|
42
|
+
indicator = @escape ? '=' : '=='
|
|
43
|
+
@engine.send(:add_expression, indicator, value)
|
|
44
|
+
when :expr_block
|
|
45
|
+
indicator = @escape ? '==' : '='
|
|
46
|
+
@engine.send(:add_expression_block, indicator, value)
|
|
47
|
+
when :expr_block_escaped
|
|
48
|
+
indicator = @escape ? '=' : '=='
|
|
49
|
+
@engine.send(:add_expression_block, indicator, value)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def optimize_tokens(unoptimized_tokens)
|
|
54
|
+
tokens = super
|
|
55
|
+
optimized_tokens = []
|
|
56
|
+
block_count = 0
|
|
57
|
+
current_block = []
|
|
58
|
+
|
|
59
|
+
tokens.each do |token|
|
|
60
|
+
if token[0] == :block_start
|
|
61
|
+
block_count += 1
|
|
62
|
+
elsif token[0] == :block_end
|
|
63
|
+
block_count -= 1
|
|
64
|
+
|
|
65
|
+
if block_count.zero?
|
|
66
|
+
optimized_tokens << [:block, current_block]
|
|
67
|
+
current_block = []
|
|
68
|
+
end
|
|
69
|
+
elsif block_count.zero?
|
|
70
|
+
optimized_tokens << token
|
|
71
|
+
else
|
|
72
|
+
current_block << token
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
optimized_tokens
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module LiveCable
|
|
4
|
+
module Rendering
|
|
5
|
+
class DependencyVisitor < Prism::Visitor
|
|
6
|
+
# @return [Array<Symbol>]
|
|
7
|
+
attr_reader :local_reads
|
|
8
|
+
|
|
9
|
+
# @return [Array<Symbol>]
|
|
10
|
+
attr_reader :local_writes
|
|
11
|
+
|
|
12
|
+
# @return [Set<Symbol>]
|
|
13
|
+
attr_reader :component_method_calls
|
|
14
|
+
|
|
15
|
+
def initialize
|
|
16
|
+
super
|
|
17
|
+
@local_reads = []
|
|
18
|
+
@local_writes = []
|
|
19
|
+
@component_method_calls = Set.new
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Track all local variable reads
|
|
23
|
+
# @param node [Prism::LocalVariableReadNode]
|
|
24
|
+
# @return [void]
|
|
25
|
+
def visit_local_variable_read_node(node)
|
|
26
|
+
@local_reads |= [node.name]
|
|
27
|
+
super
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Track variable method calls (e.g., `foo` without parens)
|
|
31
|
+
# Also track component.method_name calls
|
|
32
|
+
# @param node [Prism::CallNode]
|
|
33
|
+
# @return [void]
|
|
34
|
+
def visit_call_node(node)
|
|
35
|
+
# Track variable calls (implicit self)
|
|
36
|
+
@local_reads |= [node.name] if node.variable_call?
|
|
37
|
+
|
|
38
|
+
# Track component.method_name calls
|
|
39
|
+
if component_receiver?(node.receiver)
|
|
40
|
+
@component_method_calls << node.name
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
super
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Track local variable writes
|
|
47
|
+
# @param node [Prism::LocalVariableWriteNode]
|
|
48
|
+
# @return [void]
|
|
49
|
+
def visit_local_variable_write_node(node)
|
|
50
|
+
@local_writes |= [node.name]
|
|
51
|
+
super
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Track local variable operator writes (+=, ||=, etc)
|
|
55
|
+
# @param node [Prism::LocalVariableOperatorWriteNode]
|
|
56
|
+
# @return [void]
|
|
57
|
+
def visit_local_variable_operator_write_node(node)
|
|
58
|
+
@local_writes |= [node.name]
|
|
59
|
+
@local_reads |= [node.name] # Reads before writing
|
|
60
|
+
super
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Track local variable and writes (&&=)
|
|
64
|
+
# @param node [Prism::LocalVariableAndWriteNode]
|
|
65
|
+
# @return [void]
|
|
66
|
+
def visit_local_variable_and_write_node(node)
|
|
67
|
+
@local_writes |= [node.name]
|
|
68
|
+
@local_reads |= [node.name]
|
|
69
|
+
super
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Track local variable or writes (||=)
|
|
73
|
+
# @param node [Prism::LocalVariableOrWriteNode]
|
|
74
|
+
# @return [void]
|
|
75
|
+
def visit_local_variable_or_write_node(node)
|
|
76
|
+
@local_writes |= [node.name]
|
|
77
|
+
@local_reads |= [node.name]
|
|
78
|
+
super
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
private
|
|
82
|
+
|
|
83
|
+
# Check if receiver is a reference to 'component'
|
|
84
|
+
# @param receiver [Prism::Node, nil]
|
|
85
|
+
# @return [Boolean]
|
|
86
|
+
def component_receiver?(receiver)
|
|
87
|
+
# No receiver, calling something lke live_id or any component method
|
|
88
|
+
return true if receiver.nil?
|
|
89
|
+
|
|
90
|
+
# Ignore these because they're not component methods, just output methods
|
|
91
|
+
return false if receiver.try(:name) == :@output_buffer
|
|
92
|
+
|
|
93
|
+
# If there is a receiver, it must be a component method call or local variable read
|
|
94
|
+
return false unless receiver.try(:name) == :component
|
|
95
|
+
|
|
96
|
+
receiver.is_a?(Prism::CallNode) || receiver.is_a?(Prism::LocalVariableReadNode)
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module LiveCable
|
|
4
|
+
module Rendering
|
|
5
|
+
class Handler < ActionView::Template::Handlers::ERB
|
|
6
|
+
def call(template, source)
|
|
7
|
+
parse_result = ::Herb.parse(source, track_whitespace: true)
|
|
8
|
+
ast = parse_result.value
|
|
9
|
+
|
|
10
|
+
engine = Renderer.new
|
|
11
|
+
compiler = Compiler.new(engine)
|
|
12
|
+
ast.accept(compiler)
|
|
13
|
+
|
|
14
|
+
compiler.generate_output
|
|
15
|
+
engine.src
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|