ruby_wasm_ui 0.8.1
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 +7 -0
- data/.cursor/rules/ruby_comments.mdc +29 -0
- data/.github/workflows/playwright.yml +74 -0
- data/.github/workflows/rspec.yml +33 -0
- data/.node-version +1 -0
- data/CODE_OF_CONDUCT.md +132 -0
- data/LICENSE.txt +21 -0
- data/README.md +218 -0
- data/Rakefile +4 -0
- data/docs/conditional-rendering.md +119 -0
- data/docs/lifecycle-hooks.md +75 -0
- data/docs/list-rendering.md +51 -0
- data/examples/Gemfile +5 -0
- data/examples/Gemfile.lock +41 -0
- data/examples/Makefile +15 -0
- data/examples/npm-packages/runtime/counter/index.html +28 -0
- data/examples/npm-packages/runtime/counter/index.rb +62 -0
- data/examples/npm-packages/runtime/counter/index.spec.js +42 -0
- data/examples/npm-packages/runtime/hello/index.html +28 -0
- data/examples/npm-packages/runtime/hello/index.rb +29 -0
- data/examples/npm-packages/runtime/hello/index.spec.js +53 -0
- data/examples/npm-packages/runtime/input/index.html +28 -0
- data/examples/npm-packages/runtime/input/index.rb +46 -0
- data/examples/npm-packages/runtime/input/index.spec.js +58 -0
- data/examples/npm-packages/runtime/list/index.html +27 -0
- data/examples/npm-packages/runtime/list/index.rb +33 -0
- data/examples/npm-packages/runtime/list/index.spec.js +46 -0
- data/examples/npm-packages/runtime/on_mounted_demo/index.html +40 -0
- data/examples/npm-packages/runtime/on_mounted_demo/index.rb +59 -0
- data/examples/npm-packages/runtime/on_mounted_demo/index.spec.js +50 -0
- data/examples/npm-packages/runtime/r_if_attribute_demo/index.html +34 -0
- data/examples/npm-packages/runtime/r_if_attribute_demo/index.rb +113 -0
- data/examples/npm-packages/runtime/r_if_attribute_demo/index.spec.js +140 -0
- data/examples/npm-packages/runtime/random_cocktail/index.html +27 -0
- data/examples/npm-packages/runtime/random_cocktail/index.rb +69 -0
- data/examples/npm-packages/runtime/random_cocktail/index.spec.js +101 -0
- data/examples/npm-packages/runtime/search_field/index.html +27 -0
- data/examples/npm-packages/runtime/search_field/index.rb +39 -0
- data/examples/npm-packages/runtime/search_field/index.spec.js +59 -0
- data/examples/npm-packages/runtime/todos/index.html +28 -0
- data/examples/npm-packages/runtime/todos/index.rb +239 -0
- data/examples/npm-packages/runtime/todos/index.spec.js +161 -0
- data/examples/npm-packages/runtime/todos/todos_repository.rb +23 -0
- data/examples/package.json +12 -0
- data/examples/src/counter/index.html +23 -0
- data/examples/src/counter/index.rb +60 -0
- data/lib/ruby_wasm_ui +1 -0
- data/lib/ruby_wasm_ui.rb +1 -0
- data/package-lock.json +100 -0
- data/package.json +32 -0
- data/packages/npm-packages/runtime/Gemfile +3 -0
- data/packages/npm-packages/runtime/Gemfile.lock +26 -0
- data/packages/npm-packages/runtime/README.md +5 -0
- data/packages/npm-packages/runtime/eslint.config.mjs +16 -0
- data/packages/npm-packages/runtime/package-lock.json +6668 -0
- data/packages/npm-packages/runtime/package.json +38 -0
- data/packages/npm-packages/runtime/rollup.config.mjs +89 -0
- data/packages/npm-packages/runtime/spec/ruby_wasm_ui/component_spec.rb +416 -0
- data/packages/npm-packages/runtime/spec/ruby_wasm_ui/dom/scheduler_spec.rb +98 -0
- data/packages/npm-packages/runtime/spec/ruby_wasm_ui/nodes_equal_spec.rb +190 -0
- data/packages/npm-packages/runtime/spec/ruby_wasm_ui/template/build_conditional_group_spec.rb +505 -0
- data/packages/npm-packages/runtime/spec/ruby_wasm_ui/template/build_for_group_spec.rb +377 -0
- data/packages/npm-packages/runtime/spec/ruby_wasm_ui/template/build_vdom_spec.rb +573 -0
- data/packages/npm-packages/runtime/spec/ruby_wasm_ui/template/parser_spec.rb +627 -0
- data/packages/npm-packages/runtime/spec/ruby_wasm_ui/utils/arrays_spec.rb +228 -0
- data/packages/npm-packages/runtime/spec/ruby_wasm_ui/utils/objects_spec.rb +127 -0
- data/packages/npm-packages/runtime/spec/ruby_wasm_ui/utils/props_spec.rb +205 -0
- data/packages/npm-packages/runtime/spec/ruby_wasm_ui/utils/strings_spec.rb +107 -0
- data/packages/npm-packages/runtime/spec/spec_helper.rb +16 -0
- data/packages/npm-packages/runtime/src/__tests__/sample.test.js +5 -0
- data/packages/npm-packages/runtime/src/index.js +37 -0
- data/packages/npm-packages/runtime/src/ruby_wasm_ui/app.rb +53 -0
- data/packages/npm-packages/runtime/src/ruby_wasm_ui/component.rb +215 -0
- data/packages/npm-packages/runtime/src/ruby_wasm_ui/dispatcher.rb +46 -0
- data/packages/npm-packages/runtime/src/ruby_wasm_ui/dom/attributes.rb +105 -0
- data/packages/npm-packages/runtime/src/ruby_wasm_ui/dom/destroy_dom.rb +63 -0
- data/packages/npm-packages/runtime/src/ruby_wasm_ui/dom/events.rb +40 -0
- data/packages/npm-packages/runtime/src/ruby_wasm_ui/dom/mount_dom.rb +108 -0
- data/packages/npm-packages/runtime/src/ruby_wasm_ui/dom/patch_dom.rb +237 -0
- data/packages/npm-packages/runtime/src/ruby_wasm_ui/dom/scheduler.rb +51 -0
- data/packages/npm-packages/runtime/src/ruby_wasm_ui/dom.rb +13 -0
- data/packages/npm-packages/runtime/src/ruby_wasm_ui/nodes_equal.rb +45 -0
- data/packages/npm-packages/runtime/src/ruby_wasm_ui/template/build_conditional_group.rb +150 -0
- data/packages/npm-packages/runtime/src/ruby_wasm_ui/template/build_for_group.rb +125 -0
- data/packages/npm-packages/runtime/src/ruby_wasm_ui/template/build_vdom.rb +220 -0
- data/packages/npm-packages/runtime/src/ruby_wasm_ui/template/parser.rb +134 -0
- data/packages/npm-packages/runtime/src/ruby_wasm_ui/template.rb +11 -0
- data/packages/npm-packages/runtime/src/ruby_wasm_ui/utils/arrays.rb +185 -0
- data/packages/npm-packages/runtime/src/ruby_wasm_ui/utils/objects.rb +37 -0
- data/packages/npm-packages/runtime/src/ruby_wasm_ui/utils/props.rb +25 -0
- data/packages/npm-packages/runtime/src/ruby_wasm_ui/utils/strings.rb +19 -0
- data/packages/npm-packages/runtime/src/ruby_wasm_ui/utils.rb +11 -0
- data/packages/npm-packages/runtime/src/ruby_wasm_ui/vdom.rb +84 -0
- data/packages/npm-packages/runtime/src/ruby_wasm_ui/version.rb +5 -0
- data/packages/npm-packages/runtime/src/ruby_wasm_ui.rb +14 -0
- data/packages/npm-packages/runtime/vitest.config.js +8 -0
- data/playwright.config.js +78 -0
- data/sig/ruby_wasm_ui.rbs +4 -0
- metadata +168 -0
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
module RubyWasmUi
|
|
2
|
+
module Dom
|
|
3
|
+
module PatchDom
|
|
4
|
+
# @param old_vdom [RubyWasmUi::Vdom]
|
|
5
|
+
# @param new_vdom [RubyWasmUi::Vdom]
|
|
6
|
+
# @param parent_el [JS::Object]
|
|
7
|
+
# @param host_component [RubyWasmUi::Component, nil]
|
|
8
|
+
# @return [RubyWasmUi::Vdom]
|
|
9
|
+
def execute(old_vdom, new_vdom, parent_el, host_component = nil)
|
|
10
|
+
if !NodesEqual.equal?(old_vdom, new_vdom)
|
|
11
|
+
index = find_index_in_parent(parent_el, old_vdom.el)
|
|
12
|
+
RubyWasmUi::Dom::DestroyDom.execute(old_vdom)
|
|
13
|
+
RubyWasmUi::Dom::MountDom.execute(new_vdom, parent_el, index, host_component)
|
|
14
|
+
|
|
15
|
+
return new_vdom
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# when old_vdom and new_vdom is same type
|
|
19
|
+
new_vdom.el = old_vdom.el
|
|
20
|
+
|
|
21
|
+
case new_vdom.type
|
|
22
|
+
when RubyWasmUi::Vdom::DOM_TYPES[:TEXT]
|
|
23
|
+
patch_text(old_vdom, new_vdom)
|
|
24
|
+
return new_vdom # return early and skip children patch
|
|
25
|
+
when RubyWasmUi::Vdom::DOM_TYPES[:ELEMENT]
|
|
26
|
+
patch_element(old_vdom, new_vdom, host_component)
|
|
27
|
+
when RubyWasmUi::Vdom::DOM_TYPES[:COMPONENT]
|
|
28
|
+
patch_component(old_vdom, new_vdom)
|
|
29
|
+
else
|
|
30
|
+
# noop
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
patch_children(old_vdom, new_vdom, host_component)
|
|
34
|
+
|
|
35
|
+
new_vdom
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
module_function :execute
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
# @param parent_el [JS::Object]
|
|
43
|
+
# @param el [JS::Object]
|
|
44
|
+
# @return [Integer, nil]
|
|
45
|
+
def self.find_index_in_parent(parent_el, el)
|
|
46
|
+
index = parent_el[:childNodes].to_a.index(el)
|
|
47
|
+
return nil if index < 0
|
|
48
|
+
|
|
49
|
+
index
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# @param old_vdom [RubyWasmUi::Vdom]
|
|
53
|
+
# @param new_vdom [RubyWasmUi::Vdom]
|
|
54
|
+
def self.patch_text(old_vdom, new_vdom)
|
|
55
|
+
el = old_vdom.el
|
|
56
|
+
old_text = old_vdom.value
|
|
57
|
+
new_text = new_vdom.value
|
|
58
|
+
|
|
59
|
+
if old_text != new_text
|
|
60
|
+
el.nodeValue = new_text
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# @param old_vdom [RubyWasmUi::Vdom]
|
|
65
|
+
# @param new_vdom [RubyWasmUi::Vdom]
|
|
66
|
+
# @param host_component [RubyWasmUi::Component, nil]
|
|
67
|
+
def self.patch_element(old_vdom, new_vdom, host_component)
|
|
68
|
+
el = old_vdom.el
|
|
69
|
+
|
|
70
|
+
# Extract attributes from oldVdom.props (equivalent to JavaScript destructuring)
|
|
71
|
+
old_props = old_vdom.props || {}
|
|
72
|
+
old_class = old_props[:class]
|
|
73
|
+
old_style = old_props[:style]
|
|
74
|
+
old_events = old_props[:on]
|
|
75
|
+
old_attrs = old_props.reject { |key, _| [:class, :style, :on].include?(key) }
|
|
76
|
+
|
|
77
|
+
# Extract attributes from newVdom.props
|
|
78
|
+
new_props = new_vdom.props || {}
|
|
79
|
+
new_class = new_props[:class]
|
|
80
|
+
new_style = new_props[:style]
|
|
81
|
+
new_events = new_props[:on]
|
|
82
|
+
new_attrs = new_props.reject { |key, _| [:class, :style, :on].include?(key) }
|
|
83
|
+
|
|
84
|
+
# Get listeners from oldVdom
|
|
85
|
+
old_listeners = old_vdom.listeners
|
|
86
|
+
|
|
87
|
+
patch_attrs(el, old_attrs, new_attrs)
|
|
88
|
+
patch_classes(el, old_class, new_class)
|
|
89
|
+
patch_styles(el, old_style, new_style)
|
|
90
|
+
new_vdom.listeners = patch_events(el, old_listeners, old_events, new_events, host_component)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# @param el [JS::Object]
|
|
94
|
+
# @param old_attrs [Hash]
|
|
95
|
+
# @param new_attrs [Hash]
|
|
96
|
+
def self.patch_attrs(el, old_attrs, new_attrs)
|
|
97
|
+
diff = RubyWasmUi::Utils::Objects.diff(old_attrs, new_attrs)
|
|
98
|
+
|
|
99
|
+
diff[:removed].each do |key|
|
|
100
|
+
RubyWasmUi::Dom::Attributes.remove_attribute(el, key)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
(diff[:added] + diff[:updated]).each do |key|
|
|
104
|
+
RubyWasmUi::Dom::Attributes.set_attribute(el, key, new_attrs[key])
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# @param el [JS::Object]
|
|
109
|
+
# @param old_class [String, Array]
|
|
110
|
+
# @param new_class [String, Array]
|
|
111
|
+
def self.patch_classes(el, old_class, new_class)
|
|
112
|
+
old_classes = to_class_list(old_class)
|
|
113
|
+
new_classes = to_class_list(new_class)
|
|
114
|
+
|
|
115
|
+
diff = RubyWasmUi::Utils::Arrays.diff(old_classes, new_classes)
|
|
116
|
+
|
|
117
|
+
diff[:removed].each do |key|
|
|
118
|
+
el[:classList].remove(key)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
diff[:added].each do |key|
|
|
122
|
+
el[:classList].add(key)
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# @param el [JS::Object]
|
|
127
|
+
# @param old_style [Hash, String]
|
|
128
|
+
# @param new_style [Hash, String]
|
|
129
|
+
def self.patch_styles(el, old_style = {}, new_style = {})
|
|
130
|
+
parsed_old_style = RubyWasmUi::Dom::Attributes.parse_style(old_style)
|
|
131
|
+
parsed_new_style = RubyWasmUi::Dom::Attributes.parse_style(new_style)
|
|
132
|
+
diff = RubyWasmUi::Utils::Objects.diff(parsed_old_style || {}, parsed_new_style || {})
|
|
133
|
+
|
|
134
|
+
diff[:removed].each do |key|
|
|
135
|
+
RubyWasmUi::Dom::Attributes.remove_style(el, key)
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
(diff[:added] + diff[:updated]).each do |key|
|
|
139
|
+
RubyWasmUi::Dom::Attributes.set_style(el, key, new_style[key])
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# @param el [JS::Object]
|
|
144
|
+
# @param old_listeners [Hash]
|
|
145
|
+
# @param old_events [Hash]
|
|
146
|
+
# @param new_events [Hash]
|
|
147
|
+
# @return [Hash]
|
|
148
|
+
def self.patch_events(el, old_listeners = {}, old_events = {}, new_events = {}, host_component = nil)
|
|
149
|
+
diff = RubyWasmUi::Utils::Objects.diff(old_events || {}, new_events || {})
|
|
150
|
+
|
|
151
|
+
# Remove old event listeners for removed and updated events
|
|
152
|
+
(diff[:removed] + diff[:updated]).each do |event_name|
|
|
153
|
+
if old_listeners[event_name]
|
|
154
|
+
el.call(:removeEventListener, event_name.to_s, old_listeners[event_name])
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
added_listeners = {}
|
|
159
|
+
|
|
160
|
+
# Add new event listeners for added and updated events
|
|
161
|
+
(diff[:added] + diff[:updated]).each do |event_name|
|
|
162
|
+
listener = RubyWasmUi::Dom::Events.add_event_listener(
|
|
163
|
+
event_name,
|
|
164
|
+
new_events[event_name],
|
|
165
|
+
el,
|
|
166
|
+
host_component
|
|
167
|
+
)
|
|
168
|
+
added_listeners[event_name] = listener
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
added_listeners
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# @param old_vdom [RubyWasmUi::Vdom]
|
|
175
|
+
# @param new_vdom [RubyWasmUi::Vdom]
|
|
176
|
+
# @param host_component [RubyWasmUi::Component, nil]
|
|
177
|
+
# @return [void]
|
|
178
|
+
def self.patch_children(old_vdom, new_vdom, host_component)
|
|
179
|
+
old_children = RubyWasmUi::Vdom.extract_children(old_vdom)
|
|
180
|
+
new_children = RubyWasmUi::Vdom.extract_children(new_vdom)
|
|
181
|
+
parent_el = old_vdom.el
|
|
182
|
+
|
|
183
|
+
equal_proc = ->(a, b) { NodesEqual.equal?(a, b) }
|
|
184
|
+
|
|
185
|
+
diff_seq = RubyWasmUi::Utils::Arrays.diff_sequence(old_children, new_children, equal_proc)
|
|
186
|
+
offset = host_component&.offset || 0
|
|
187
|
+
|
|
188
|
+
diff_seq.each do |operation|
|
|
189
|
+
original_index = operation[:original_index]
|
|
190
|
+
index = operation[:index]
|
|
191
|
+
item = operation[:item]
|
|
192
|
+
|
|
193
|
+
case operation[:op]
|
|
194
|
+
when 'add'
|
|
195
|
+
RubyWasmUi::Dom::MountDom.execute(item, parent_el, index + offset, host_component)
|
|
196
|
+
when 'remove'
|
|
197
|
+
RubyWasmUi::Dom::DestroyDom.execute(item)
|
|
198
|
+
when 'move'
|
|
199
|
+
old_child = old_children[original_index]
|
|
200
|
+
new_child = new_children[index]
|
|
201
|
+
el = old_child.el
|
|
202
|
+
el_at_target_index = parent_el[:childNodes][index + offset]
|
|
203
|
+
|
|
204
|
+
parent_el.insertBefore(el, el_at_target_index)
|
|
205
|
+
RubyWasmUi::Dom::PatchDom.execute(old_child, new_child, parent_el, host_component)
|
|
206
|
+
when 'noop'
|
|
207
|
+
RubyWasmUi::Dom::PatchDom.execute(old_children[original_index], new_children[index], parent_el, host_component)
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
# @param classes [String, Array]
|
|
213
|
+
# @return [Array]
|
|
214
|
+
def self.to_class_list(classes = '')
|
|
215
|
+
if classes.is_a?(Array)
|
|
216
|
+
classes.select { |c| RubyWasmUi::Utils::Strings.is_not_blank_or_empty_string(c) }
|
|
217
|
+
else
|
|
218
|
+
# string case
|
|
219
|
+
classes.to_s.split(/\s+/).select { |c| RubyWasmUi::Utils::Strings.is_not_empty_string(c) }
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
# @param old_vdom [RubyWasmUi::Vdom]
|
|
224
|
+
# @param new_vdom [RubyWasmUi::Vdom]
|
|
225
|
+
# @return [void]
|
|
226
|
+
def self.patch_component(old_vdom, new_vdom)
|
|
227
|
+
component = old_vdom.component
|
|
228
|
+
props = RubyWasmUi::Utils::Props.extract_props_and_events(new_vdom)[:props]
|
|
229
|
+
|
|
230
|
+
component.update_props(props)
|
|
231
|
+
|
|
232
|
+
new_vdom.component = component
|
|
233
|
+
new_vdom.el = component.first_element
|
|
234
|
+
end
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
module RubyWasmUi
|
|
2
|
+
module Dom
|
|
3
|
+
module Scheduler
|
|
4
|
+
class << self
|
|
5
|
+
# @return [Boolean] Flag indicating if a job processing is scheduled
|
|
6
|
+
attr_accessor :scheduled
|
|
7
|
+
|
|
8
|
+
# @return [Array] Array of jobs to be processed
|
|
9
|
+
attr_accessor :jobs
|
|
10
|
+
|
|
11
|
+
# Initialize class variables
|
|
12
|
+
def initialize_scheduler
|
|
13
|
+
@scheduled = false
|
|
14
|
+
@jobs = []
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Enqueues a job to be processed
|
|
18
|
+
# @param job [Proc] The job to be executed
|
|
19
|
+
# @return [void]
|
|
20
|
+
def enqueue_job(job)
|
|
21
|
+
initialize_scheduler if @jobs.nil?
|
|
22
|
+
@jobs.push(job)
|
|
23
|
+
schedule_update
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
# Schedules an update if not already scheduled
|
|
29
|
+
# @return [void]
|
|
30
|
+
def schedule_update
|
|
31
|
+
return if @scheduled
|
|
32
|
+
|
|
33
|
+
@scheduled = true
|
|
34
|
+
# Using JS.global to access queueMicrotask
|
|
35
|
+
JS.global.queueMicrotask(-> { process_jobs })
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Processes all jobs in the queue
|
|
39
|
+
# @return [void]
|
|
40
|
+
def process_jobs
|
|
41
|
+
while @jobs.any?
|
|
42
|
+
job = @jobs.shift
|
|
43
|
+
job.call
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
@scheduled = false
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "dom/attributes"
|
|
4
|
+
require_relative "dom/destroy_dom"
|
|
5
|
+
require_relative "dom/events"
|
|
6
|
+
require_relative "dom/mount_dom"
|
|
7
|
+
require_relative "dom/patch_dom"
|
|
8
|
+
require_relative "dom/scheduler"
|
|
9
|
+
|
|
10
|
+
module RubyWasmUi
|
|
11
|
+
module Dom
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
module RubyWasmUi
|
|
2
|
+
module NodesEqual
|
|
3
|
+
module_function
|
|
4
|
+
|
|
5
|
+
# @param node_one [RubyWasmUi::Vdom]
|
|
6
|
+
# @param node_two [RubyWasmUi::Vdom]
|
|
7
|
+
# @return [Boolean]
|
|
8
|
+
def equal?(node_one, node_two)
|
|
9
|
+
if node_one.type != node_two.type
|
|
10
|
+
return false
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
if node_one.type == RubyWasmUi::Vdom::DOM_TYPES[:ELEMENT]
|
|
14
|
+
tag_one = node_one.tag
|
|
15
|
+
key_one = node_one.props[:key]
|
|
16
|
+
tag_two = node_two.tag
|
|
17
|
+
key_two = node_two.props[:key]
|
|
18
|
+
|
|
19
|
+
return tag_one == tag_two && key_one == key_two
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
if node_one.type == RubyWasmUi::Vdom::DOM_TYPES[:COMPONENT]
|
|
23
|
+
component_one = node_one.tag
|
|
24
|
+
key_one = node_one.props[:key]
|
|
25
|
+
component_two = node_two.tag
|
|
26
|
+
key_two = node_two.props[:key]
|
|
27
|
+
|
|
28
|
+
return component_one == component_two && key_one == key_two
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
if node_one.type == RubyWasmUi::Vdom::DOM_TYPES[:TEXT]
|
|
32
|
+
value_one = node_one.value
|
|
33
|
+
value_two = node_two.value
|
|
34
|
+
|
|
35
|
+
return value_one == value_two
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
if node_one.type == RubyWasmUi::Vdom::DOM_TYPES[:FRAGMENT]
|
|
39
|
+
return true
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
false
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyWasmUi
|
|
4
|
+
module Template
|
|
5
|
+
module BuildConditionalGroup
|
|
6
|
+
module_function
|
|
7
|
+
|
|
8
|
+
# Check if element has conditional attributes (r-if, r-elsif, r-else)
|
|
9
|
+
# @param element [JS.Object]
|
|
10
|
+
# @return [Boolean]
|
|
11
|
+
def has_conditional_attribute?(element)
|
|
12
|
+
return false unless element[:attributes]
|
|
13
|
+
|
|
14
|
+
length = element[:attributes][:length].to_i
|
|
15
|
+
length.times do |i|
|
|
16
|
+
attribute = element[:attributes][i]
|
|
17
|
+
key = attribute[:name].to_s
|
|
18
|
+
return true if %w[r-if r-elsif r-else].include?(key)
|
|
19
|
+
end
|
|
20
|
+
false
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Build conditional group (r-if, r-elsif, r-else)
|
|
24
|
+
# @param elements [JS.Array] Array of elements
|
|
25
|
+
# @param start_index [Integer] Starting index
|
|
26
|
+
# @return [Array] [conditional_code, next_index]
|
|
27
|
+
def build_conditional_group(elements, start_index)
|
|
28
|
+
conditions = []
|
|
29
|
+
current_index = start_index
|
|
30
|
+
elements_length = elements[:length].to_i
|
|
31
|
+
|
|
32
|
+
# Process all consecutive conditional elements
|
|
33
|
+
while current_index < elements_length
|
|
34
|
+
element = elements[current_index]
|
|
35
|
+
# Skip text nodes (whitespace between elements)
|
|
36
|
+
if element[:nodeType] == JS.global[:Node][:TEXT_NODE]
|
|
37
|
+
current_index += 1
|
|
38
|
+
next
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Break if this is not an element with conditional attributes
|
|
42
|
+
unless element[:nodeType] == JS.global[:Node][:ELEMENT_NODE] && has_conditional_attribute?(element)
|
|
43
|
+
break
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
conditional_type = get_conditional_type(element)
|
|
47
|
+
|
|
48
|
+
# If we encounter a new r-if after the first one, break the group
|
|
49
|
+
if conditions.any? && conditional_type == 'r-if'
|
|
50
|
+
break
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
condition = get_conditional_expression(element)
|
|
54
|
+
content = build_single_conditional_content(element)
|
|
55
|
+
|
|
56
|
+
case conditional_type
|
|
57
|
+
when 'r-if'
|
|
58
|
+
conditions << "if #{condition}"
|
|
59
|
+
conditions << " #{content}"
|
|
60
|
+
when 'r-elsif'
|
|
61
|
+
conditions << "elsif #{condition}"
|
|
62
|
+
conditions << " #{content}"
|
|
63
|
+
when 'r-else'
|
|
64
|
+
conditions << "else"
|
|
65
|
+
conditions << " #{content}"
|
|
66
|
+
current_index += 1
|
|
67
|
+
break
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
current_index += 1
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Add final else clause if not present
|
|
74
|
+
unless conditions.any? { |c| c == 'else' }
|
|
75
|
+
conditions << "else"
|
|
76
|
+
conditions << " RubyWasmUi::Vdom.h_fragment([])"
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
conditions << "end"
|
|
80
|
+
|
|
81
|
+
[conditions.join("\n"), current_index]
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Build content for a single conditional element
|
|
85
|
+
# @param element [JS.Object]
|
|
86
|
+
# @return [String]
|
|
87
|
+
def build_single_conditional_content(element)
|
|
88
|
+
tag_name = element[:tagName].to_s.downcase
|
|
89
|
+
filtered_attributes = filter_conditional_attributes(element[:attributes])
|
|
90
|
+
|
|
91
|
+
if tag_name == 'template' || (tag_name == 'div' && BuildVdom.has_data_template_attribute?(element))
|
|
92
|
+
# For template elements, render the content directly
|
|
93
|
+
BuildVdom.build_fragment(element, tag_name)
|
|
94
|
+
elsif BuildVdom.is_component?(tag_name)
|
|
95
|
+
# For components, render the component
|
|
96
|
+
BuildVdom.build_component(element, tag_name, filtered_attributes)
|
|
97
|
+
else
|
|
98
|
+
# For regular HTML elements, render the element
|
|
99
|
+
BuildVdom.build_element(element, tag_name, filtered_attributes)
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Get conditional expression from element attributes
|
|
104
|
+
# @param element [JS.Object]
|
|
105
|
+
# @return [String]
|
|
106
|
+
def get_conditional_expression(element)
|
|
107
|
+
length = element[:attributes][:length].to_i
|
|
108
|
+
length.times do |i|
|
|
109
|
+
attribute = element[:attributes][i]
|
|
110
|
+
key = attribute[:name].to_s
|
|
111
|
+
value = attribute[:value].to_s
|
|
112
|
+
|
|
113
|
+
if %w[r-if r-elsif].include?(key)
|
|
114
|
+
return BuildVdom.embed_script?(value) ? BuildVdom.get_embed_script(value) : value
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
'true' # fallback for r-else
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Get conditional type from element attributes
|
|
121
|
+
# @param element [JS.Object]
|
|
122
|
+
# @return [String]
|
|
123
|
+
def get_conditional_type(element)
|
|
124
|
+
length = element[:attributes][:length].to_i
|
|
125
|
+
length.times do |i|
|
|
126
|
+
attribute = element[:attributes][i]
|
|
127
|
+
key = attribute[:name].to_s
|
|
128
|
+
return key if %w[r-if r-elsif r-else].include?(key)
|
|
129
|
+
end
|
|
130
|
+
'r-if' # fallback
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Filter out conditional attributes from the attributes list
|
|
134
|
+
# @param attributes [JS.Object]
|
|
135
|
+
# @return [Array]
|
|
136
|
+
def filter_conditional_attributes(attributes)
|
|
137
|
+
filtered = []
|
|
138
|
+
length = attributes[:length].to_i
|
|
139
|
+
length.times do |i|
|
|
140
|
+
attribute = attributes[i]
|
|
141
|
+
key = attribute[:name].to_s
|
|
142
|
+
unless %w[r-if r-elsif r-else data-template].include?(key)
|
|
143
|
+
filtered << attribute
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
filtered
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyWasmUi
|
|
4
|
+
module Template
|
|
5
|
+
module BuildForGroup
|
|
6
|
+
module_function
|
|
7
|
+
|
|
8
|
+
# Check if element has r-for attribute
|
|
9
|
+
# @param element [JS.Object]
|
|
10
|
+
# @return [Boolean]
|
|
11
|
+
def has_for_attribute?(element)
|
|
12
|
+
return false unless element[:attributes]
|
|
13
|
+
|
|
14
|
+
length = element[:attributes][:length].to_i
|
|
15
|
+
length.times do |i|
|
|
16
|
+
attribute = element[:attributes][i]
|
|
17
|
+
key = attribute[:name].to_s
|
|
18
|
+
return true if key == 'r-for'
|
|
19
|
+
end
|
|
20
|
+
false
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Build for loop code for r-for attribute
|
|
24
|
+
# @param element [JS.Object]
|
|
25
|
+
# @return [String]
|
|
26
|
+
def build_for_loop(element)
|
|
27
|
+
for_expression = get_for_expression(element)
|
|
28
|
+
return '' unless for_expression
|
|
29
|
+
|
|
30
|
+
# Parse r-for expression like "{todo in todos}"
|
|
31
|
+
# Remove curly braces and parse
|
|
32
|
+
clean_expression = for_expression.gsub(/^\{|\}$/, '').strip
|
|
33
|
+
|
|
34
|
+
# Match pattern: "item in collection"
|
|
35
|
+
if clean_expression.match(/^(\w+)\s+in\s+(.+)$/)
|
|
36
|
+
item_var = $1
|
|
37
|
+
collection_expr = $2
|
|
38
|
+
|
|
39
|
+
# Get the tag name and filtered attributes (excluding r-for)
|
|
40
|
+
tag_name = element[:tagName].to_s.downcase
|
|
41
|
+
filtered_attributes = filter_for_attributes(element[:attributes])
|
|
42
|
+
|
|
43
|
+
# Generate the map code
|
|
44
|
+
if RubyWasmUi::Template::BuildVdom.is_component?(tag_name)
|
|
45
|
+
component_code = build_component_for_item(element, tag_name, filtered_attributes, item_var)
|
|
46
|
+
"#{collection_expr}.map do |#{item_var}|\n #{component_code}\nend"
|
|
47
|
+
else
|
|
48
|
+
element_code = build_element_for_item(element, tag_name, filtered_attributes, item_var)
|
|
49
|
+
"#{collection_expr}.map do |#{item_var}|\n #{element_code}\nend"
|
|
50
|
+
end
|
|
51
|
+
else
|
|
52
|
+
# Fallback for invalid r-for syntax
|
|
53
|
+
''
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Get r-for expression from element attributes
|
|
58
|
+
# @param element [JS.Object]
|
|
59
|
+
# @return [String, nil]
|
|
60
|
+
def get_for_expression(element)
|
|
61
|
+
length = element[:attributes][:length].to_i
|
|
62
|
+
length.times do |i|
|
|
63
|
+
attribute = element[:attributes][i]
|
|
64
|
+
key = attribute[:name].to_s
|
|
65
|
+
value = attribute[:value].to_s
|
|
66
|
+
|
|
67
|
+
if key == 'r-for'
|
|
68
|
+
return RubyWasmUi::Template::BuildVdom.embed_script?(value) ? RubyWasmUi::Template::BuildVdom.get_embed_script(value) : value
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
nil
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Filter out r-for attribute from the attributes list
|
|
75
|
+
# @param attributes [JS.Object]
|
|
76
|
+
# @return [Array]
|
|
77
|
+
def filter_for_attributes(attributes)
|
|
78
|
+
filtered = []
|
|
79
|
+
length = attributes[:length].to_i
|
|
80
|
+
|
|
81
|
+
length.times do |i|
|
|
82
|
+
attribute = attributes[i]
|
|
83
|
+
key = attribute[:name].to_s
|
|
84
|
+
value = attribute[:value].to_s
|
|
85
|
+
|
|
86
|
+
# Skip r-for attribute
|
|
87
|
+
next if key == 'r-for'
|
|
88
|
+
|
|
89
|
+
# Create attribute object in the format expected by parse_attributes
|
|
90
|
+
filtered << { name: key, value: value }
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
filtered
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Build component code for a single item in the loop
|
|
97
|
+
# @param element [JS.Object]
|
|
98
|
+
# @param tag_name [String]
|
|
99
|
+
# @param filtered_attributes [Array]
|
|
100
|
+
# @param item_var [String]
|
|
101
|
+
# @return [String]
|
|
102
|
+
def build_component_for_item(element, tag_name, filtered_attributes, item_var)
|
|
103
|
+
attributes_str = RubyWasmUi::Template::BuildVdom.parse_attributes(filtered_attributes)
|
|
104
|
+
children = RubyWasmUi::Template::BuildVdom.build(element[:childNodes])
|
|
105
|
+
|
|
106
|
+
# Convert kebab-case to PascalCase for component name
|
|
107
|
+
component_name = tag_name.split('-').map(&:capitalize).join
|
|
108
|
+
"RubyWasmUi::Vdom.h(#{component_name}, {#{attributes_str}}, [#{children}])"
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Build element code for a single item in the loop
|
|
112
|
+
# @param element [JS.Object]
|
|
113
|
+
# @param tag_name [String]
|
|
114
|
+
# @param filtered_attributes [Array]
|
|
115
|
+
# @param item_var [String]
|
|
116
|
+
# @return [String]
|
|
117
|
+
def build_element_for_item(element, tag_name, filtered_attributes, item_var)
|
|
118
|
+
attributes_str = RubyWasmUi::Template::BuildVdom.parse_attributes(filtered_attributes)
|
|
119
|
+
children = RubyWasmUi::Template::BuildVdom.build(element[:childNodes])
|
|
120
|
+
|
|
121
|
+
"RubyWasmUi::Vdom.h('#{tag_name}', {#{attributes_str}}, [#{children}])"
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|