hot_module 1.0.0.alpha1 → 1.0.0.alpha2

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 945191f12e89ec2f10631dd0cbf2292a6f01084f6eb111a2ba3692f2f847314e
4
- data.tar.gz: b3cc68afcc453e60cda43d5eed8dfedda5bc0b33a5a6618a4f999a55a05e3e49
3
+ metadata.gz: 213948e4061c8dfb1da32e9621365caaa4d5594125f03c63802ff3fb53aa3b24
4
+ data.tar.gz: b4077c814479ea1d65d2143eaf6f6b7abba996787408846474c3933ffccf1fa9
5
5
  SHA512:
6
- metadata.gz: cc78a6dab31349eaf65ce29644701766d1a8bfce8ae4cc8e196830068e4df43c78a19db85907d91b265c50abe64555e3d77968450fe8a99941314852dea820bb
7
- data.tar.gz: 8f58833d468c5ad03fdf9e81a020bb36b1ac8c9b7f937bca1e9625302b408477e0e17f8ecbf0792aed5097fda1177bd7fa40b4805c84982beb8e82c0ae59d44a
6
+ metadata.gz: 76b4783da63ac259d17c0fa78c1a30b51f2919fbbbe1a5e007c315d3facb3249ffab2c0538d42322654868faf5b45f342a70b79f4b91c64ee67638644583a283
7
+ data.tar.gz: 00c1db27bd91bc5ea110af3bc26db58dc254b43960b182f327f938ea2f6e1eb440cde6ff4c9e40c2732d215600d671c0e73eeb005178174aa9005e6a3bdfb746
data/Gemfile.lock CHANGED
@@ -1,8 +1,8 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- hot_module (1.0.0.alpha1)
5
- nokogiri (~> 1.13)
4
+ hot_module (1.0.0.alpha2)
5
+ nokolexbor (~> 0.3)
6
6
 
7
7
  GEM
8
8
  remote: https://rubygems.org/
@@ -33,6 +33,7 @@ GEM
33
33
  racc (~> 1.4)
34
34
  nokogiri (1.13.8-x86_64-linux)
35
35
  racc (~> 1.4)
36
+ nokolexbor (0.3.7-arm64-darwin)
36
37
  parallel (1.22.1)
37
38
  parser (3.1.2.1)
38
39
  ast (~> 2.4.1)
data/benchmark.rb ADDED
@@ -0,0 +1,14 @@
1
+ $LOAD_PATH.unshift File.expand_path("lib", __dir__)
2
+ require "hot_module"
3
+
4
+ require_relative "test/fixtures/classes"
5
+
6
+ require "benchmark"
7
+
8
+ Benchmark.bmbm do |x|
9
+ x.report("render") do
10
+ 1000.times do |i|
11
+ Templated.new(name: i.to_s).()
12
+ end
13
+ end
14
+ end
data/hot_module.gemspec CHANGED
@@ -27,7 +27,7 @@ Gem::Specification.new do |spec|
27
27
  end
28
28
  spec.require_paths = ["lib"]
29
29
 
30
- spec.add_dependency "nokogiri", "~> 1.13"
30
+ spec.add_dependency "nokolexbor", "~> 0.3"
31
31
 
32
32
  spec.add_development_dependency "hash_with_dot_access", "~> 1.2"
33
33
  end
@@ -22,15 +22,17 @@ module HoTModuLe
22
22
  end
23
23
  end
24
24
 
25
- def process_attribute_bindings(node)
25
+ def process_attribute_bindings(node) # rubocop:todo Metrics
26
26
  node.attributes.each do |name, attr_node|
27
27
  @attribute_bindings.each do |attribute_binding|
28
+ next if attribute_binding.only_for_tag && node.name != attribute_binding.only_for_tag.to_s
28
29
  next unless attribute_binding.matcher.match?(name)
30
+ next if attribute_binding.method.receiver._check_stack(node)
29
31
 
30
32
  break unless attribute_binding.method.(attribute: attr_node, node: node)
31
33
  end
32
34
  rescue Exception => e # rubocop:disable Lint/RescueException
33
- raise e.class, e.message.lines.first, ["#{@html_module}:#{attr_node.line}", *e.backtrace]
35
+ raise e.class, e.message.lines.first, ["#{@html_module}:#{attr_node}", *e.backtrace]
34
36
  end
35
37
  end
36
38
  end
@@ -2,95 +2,41 @@
2
2
 
3
3
  require "hot_module"
4
4
 
5
- module JSStrings
6
- refine Kernel do
7
- def `(str)
8
- str
9
- end
10
- end
11
- refine String do
12
- def underscore
13
- gsub(/([A-Z]+)(?=[A-Z][a-z])|([a-z\d])(?=[A-Z])/) do
14
- (::Regexp.last_match(1) || ::Regexp.last_match(2)) << "_"
15
- end.tr("-", "_").downcase
16
- end
17
- end
18
- end
19
-
20
5
  module HoTModuLe
21
6
  module Petite
22
- using JSStrings
23
-
24
7
  # @param klass [Class]
25
8
  # @return [void]
26
9
  def self.included(klass)
27
- klass.attribute_binding "v-for", :_for_binding
28
- klass.attribute_binding "v-text", :_text_binding
29
- klass.attribute_binding "v-html", :_html_binding
30
- klass.attribute_binding "v-bind", :_handle_bound_attribute
31
- klass.attribute_binding %r{^:}, :_handle_bound_attribute
10
+ klass.attribute_binding "v-for", :_petite_for_binding, only: :template
11
+ klass.attribute_binding "v-text", :_petite_text_binding
12
+ klass.attribute_binding "v-html", :_petite_html_binding
13
+ klass.attribute_binding "v-bind", :_petite_bound_attribute
14
+ klass.attribute_binding %r{^:}, :_petite_bound_attribute
32
15
  end
33
16
 
34
17
  protected
35
18
 
36
- def evaluate_attribute_expression(attribute, eval_code = attribute.value) # rubocop:disable Metrics/AbcSize
37
- eval_code = eval_code.gsub(/\${(.*)}/, "\#{\\1}")
38
- @_locals ||= {}
39
- @_locals.keys.reverse_each do |name|
40
- eval_code = "#{name} = @_locals[\"#{name}\"];" + eval_code
41
- end
42
- instance_eval(eval_code, self.class.html_module, attribute.line)
43
- rescue NameError => e
44
- bad_name = e.message.match(/`(.*?)'/)[1]
45
- suggestion = bad_name.underscore
46
- eval_code.gsub!(bad_name, suggestion)
47
- instance_eval(eval_code, self.class.html_module, attribute.line)
48
- end
49
-
50
- def _locals_stack
51
- @_locals_stack ||= []
52
- end
53
-
54
- def _check_stack(node) # rubocop:disable Metrics/AbcSize
55
- node_and_ancestors = [node, *node.ancestors.to_a]
56
- stack_misses = 0
57
-
58
- stack_nodes = _locals_stack.map { _1[:node] }
59
- stack_nodes.each do |stack_node|
60
- if node_and_ancestors.none? { _1["v-if"] == "!hydrated" } && node_and_ancestors.none? { _1 == stack_node }
61
- stack_misses += 1
62
- end
63
- end
64
-
65
- stack_misses.times { _locals_stack.pop }
66
-
67
- !((node_and_ancestors & _locals_stack.map { _1[:node] }).empty?) # rubocop:disable Style/RedundantParentheses
68
- end
69
-
70
- def _for_binding(attribute:, node:)
71
- return unless node.name == "template"
72
-
73
- return if _check_stack(node)
19
+ def _petite_for_binding(attribute:, node:)
20
+ delimiter = node["v-for"].include?(" of ") ? " of " : " in "
21
+ expression = node["v-for"].split(delimiter)
74
22
 
75
- @_locals_stack.push({ node: node })
76
- _process_list(attribute: attribute, node: node)
23
+ process_list(
24
+ attribute: attribute,
25
+ node: node,
26
+ item_node: node.children[0].children.find(&:element?),
27
+ for_in: expression
28
+ )
77
29
  end
78
30
 
79
- def _text_binding(attribute:, node:)
80
- return if _check_stack(node)
81
-
82
- node.content = evaluate_attribute_expression(attribute)
31
+ def _petite_text_binding(attribute:, node:)
32
+ node.content = evaluate_attribute_expression(attribute).to_s
83
33
  end
84
34
 
85
- def _html_binding(attribute:, node:)
86
- return if _check_stack(node)
87
-
88
- node.content = evaluate_attribute_expression(attribute)
35
+ def _petite_html_binding(attribute:, node:)
36
+ node.inner_html = evaluate_attribute_expression(attribute).to_s
89
37
  end
90
38
 
91
- def _handle_bound_attribute(attribute:, node:) # rubocop:disable Metrics
92
- return if _check_stack(node)
93
-
39
+ def _petite_bound_attribute(attribute:, node:) # rubocop:disable Metrics
94
40
  return if attribute.name == ":key"
95
41
 
96
42
  real_attribute = if attribute.name.start_with?(":")
@@ -102,60 +48,10 @@ module HoTModuLe
102
48
  obj = evaluate_attribute_expression(attribute)
103
49
 
104
50
  if real_attribute == "class"
105
- class_names = case obj
106
- when Hash
107
- obj.filter { |_k, v| v == true }.keys
108
- when Array
109
- # TODO: handle objects inside of an array
110
- obj
111
- else
112
- Array[obj]
113
- end
114
- node[real_attribute] = class_names.join(" ")
51
+ node[real_attribute] = class_list_for(obj)
115
52
  elsif real_attribute != "style" # style bindings aren't SSRed
116
53
  node[real_attribute] = obj if obj
117
54
  end
118
55
  end
119
-
120
- def _process_list(attribute:, node:) # rubocop:disable Metrics
121
- item_node = node.element_children.first
122
-
123
- delimiter = node["v-for"].include?(" of ") ? " of " : " in "
124
- expression = node["v-for"].split(delimiter)
125
- lh = expression[0].strip.delete_prefix("(").delete_suffix(")").split(",").map!(&:strip)
126
- rh = expression[1].strip
127
-
128
- list_items = evaluate_attribute_expression(attribute, rh)
129
-
130
- # TODO: handle object style
131
- # https://vuejs.org/guide/essentials/list.html#v-for-with-an-object
132
-
133
- return unless list_items
134
-
135
- _in_locals_stack do
136
- list_items.each_with_index do |list_item, index|
137
- new_node = item_node.clone
138
- node.parent << new_node
139
- new_node["v-if"] = "!hydrated"
140
-
141
- local_items = { **(prev_items || {}) }
142
- local_items[lh[0]] = list_item
143
- local_items[lh[1]] = index if lh[1]
144
-
145
- @_locals = local_items
146
-
147
- Fragment.new(
148
- new_node, self.class.attribute_bindings,
149
- html_module: self.class.html_module
150
- ).process
151
- end
152
- end
153
- end
154
-
155
- def _in_locals_stack
156
- prev_items = @_locals
157
- yield
158
- @_locals = prev_items
159
- end
160
56
  end
161
57
  end
@@ -12,5 +12,6 @@ module HoTModuLe
12
12
  def query_selector_all(selector) = css(selector)
13
13
  end
14
14
 
15
- Nokogiri::XML::Node.include QuerySelection unless Nokogiri::XML::Node.instance_methods.include?(:query_selector)
15
+ # TODO: do we need this, or no?
16
+ # Nokogiri::XML::Node.include QuerySelection unless Nokogiri::XML::Node.instance_methods.include?(:query_selector)
16
17
  end
@@ -2,5 +2,5 @@
2
2
 
3
3
  module HoTModuLe
4
4
  # @return [String]
5
- VERSION = "1.0.0.alpha1"
5
+ VERSION = "1.0.0.alpha2"
6
6
  end
data/lib/hot_module.rb CHANGED
@@ -2,16 +2,26 @@
2
2
 
3
3
  require_relative "hot_module/version"
4
4
 
5
- require "nokogiri"
5
+ require "nokolexbor"
6
6
 
7
7
  # Include this module into your own component class
8
8
  module HoTModuLe
9
9
  class Error < StandardError; end
10
10
 
11
- AttributeBinding = Struct.new(:matcher, :method_name, :method, keyword_init: true) # rubocop:disable Lint/StructNewOverride
11
+ module JSTemplateLiterals
12
+ refine Kernel do
13
+ def `(str)
14
+ str
15
+ end
16
+ end
17
+ end
18
+
19
+ using JSTemplateLiterals
20
+
21
+ AttributeBinding = Struct.new(:matcher, :method_name, :method, :only_for_tag, keyword_init: true) # rubocop:disable Lint/StructNewOverride
12
22
 
13
23
  require_relative "hot_module/fragment"
14
- require_relative "hot_module/query_selection"
24
+ # require_relative "hot_module/query_selection"
15
25
 
16
26
  # @param klass [Class]
17
27
  # @return [void]
@@ -25,7 +35,11 @@ module HoTModuLe
25
35
 
26
36
  # Extends the component class
27
37
  module ClassMethods
28
- def html_file_extensions = %w[tmpl.html html].freeze
38
+ def camelcased(method_symbol)
39
+ alias_method(method_symbol.to_s.gsub(/(?!^)_[a-z0-9]/) { |match| match[1].upcase }, method_symbol)
40
+ end
41
+
42
+ def html_file_extensions = %w[module.html tmpl.html html].freeze
29
43
  def processed_css_extension = "css-local"
30
44
 
31
45
  # @param tag_name [String]
@@ -76,19 +90,20 @@ module HoTModuLe
76
90
 
77
91
  # @return [Nokogiri::XML::Element]
78
92
  def doc
79
- @doc ||= Nokogiri::HTML5.fragment(
80
- "<#{tag_name}>#{File.read(html_module)}</#{tag_name}>"
81
- ).first_element_child
93
+ @doc ||= Nokolexbor::DocumentFragment.parse(
94
+ "<#{tag_name}>#{File.read(html_module).strip}</#{tag_name}>"
95
+ ).children.find(&:element?)
82
96
  end
83
97
 
84
98
  def attribute_bindings
85
99
  @attribute_bindings ||= []
86
100
  end
87
101
 
88
- def attribute_binding(matcher, method_name)
102
+ def attribute_binding(matcher, method_name, only: nil)
89
103
  attribute_bindings << AttributeBinding.new(
90
104
  matcher: Regexp.new(matcher),
91
- method_name: method_name
105
+ method_name: method_name,
106
+ only_for_tag: only
92
107
  )
93
108
  end
94
109
  end
@@ -110,12 +125,24 @@ module HoTModuLe
110
125
  # @param return_node [Boolean]
111
126
  def render_element(attributes: self.attributes, content: self.content, return_node: false) # rubocop:disable Metrics
112
127
  doc = self.class.doc.clone
128
+
129
+ # NOTE: have to fix cloned templates
130
+ doc.css("template").each do |bad_tmpl|
131
+ frag = bad_tmpl.children.last
132
+ new_tmpl = doc.document.create_element("template")
133
+ bad_tmpl.attributes.each do |k, v|
134
+ new_tmpl[k] = v
135
+ end
136
+ new_tmpl.children[0].children = frag
137
+ bad_tmpl.swap(new_tmpl)
138
+ end
139
+
113
140
  tmpl_el = doc.css("> template").find { _1.attributes.length.zero? }
114
141
 
115
142
  unless tmpl_el
116
143
  tmpl_el = doc.document.create_element("template")
117
144
  immediate_children = doc.css("> :not(style):not(script)")
118
- tmpl_el << immediate_children
145
+ tmpl_el.children[0] << immediate_children
119
146
  doc.prepend_child(tmpl_el)
120
147
  end
121
148
 
@@ -123,7 +150,7 @@ module HoTModuLe
123
150
  process_fragment(tmpl_el)
124
151
 
125
152
  # Set attributes on the custom element
126
- attributes.each { |k, v| doc[k.to_s.tr("_", "-")] = v }
153
+ attributes.each { |k, v| doc[k.to_s.tr("_", "-")] = value_to_attribute(v) if v }
127
154
 
128
155
  # Look for external and internal styles
129
156
  output_styles = ""
@@ -160,13 +187,13 @@ module HoTModuLe
160
187
 
161
188
  if self.class.shadow_root
162
189
  # Guess what? We can reuse the same template tag! =)
163
- tmpl_el["shadowroot"] = "open"
164
- tmpl_el << style_tag if style_tag
190
+ tmpl_el["shadowrootmode"] = "open"
191
+ tmpl_el.children[0] << style_tag if style_tag
165
192
  doc << content if content
166
193
  else
167
- tmpl_el << style_tag if style_tag
168
- tmpl_el.at_css("slot:not([name])")&.swap(content) if content
169
- tmpl_el.children.each do |node|
194
+ tmpl_el.children[0] << style_tag if style_tag
195
+ tmpl_el.children[0].at_css("slot:not([name])")&.swap(content) if content
196
+ tmpl_el.children[0].children.each do |node|
170
197
  doc << node
171
198
  end
172
199
  tmpl_el.remove
@@ -180,6 +207,21 @@ module HoTModuLe
180
207
  render_element(...)
181
208
  end
182
209
 
210
+ def inspect
211
+ "#<#{self.class.name} #{attributes}>"
212
+ end
213
+
214
+ def value_to_attribute(val)
215
+ case val
216
+ when String, Numeric
217
+ val
218
+ when TrueClass
219
+ ""
220
+ else
221
+ val.to_json
222
+ end
223
+ end
224
+
183
225
  # Override in component if need be, otherwise we'll use the node walker/binding pipeline
184
226
  #
185
227
  # @param fragment [Nokogiri::XML::Element]
@@ -191,7 +233,95 @@ module HoTModuLe
191
233
  ).process
192
234
  end
193
235
 
194
- def inspect
195
- "#<#{self.class.name} #{attributes}>"
236
+ def process_list(attribute:, node:, item_node:, for_in:) # rubocop:disable Metrics
237
+ _context_nodes.push(node)
238
+
239
+ lh = for_in[0].strip.delete_prefix("(").delete_suffix(")").split(",").map!(&:strip)
240
+ rh = for_in[1].strip
241
+
242
+ list_items = evaluate_attribute_expression(attribute, rh)
243
+
244
+ # TODO: handle object style
245
+ # https://vuejs.org/guide/essentials/list.html#v-for-with-an-object
246
+
247
+ return unless list_items
248
+
249
+ _in_context_nodes do |previous_context|
250
+ list_items.each_with_index do |list_item, index|
251
+ new_node = item_node.clone
252
+
253
+ # NOTE: have to fix cloned templates
254
+ new_node.css("template").each do |bad_tmpl|
255
+ frag = bad_tmpl.children.last
256
+ new_tmpl = item_node.document.create_element("template")
257
+ bad_tmpl.attributes.each do |k, v|
258
+ new_tmpl[k] = v
259
+ end
260
+ new_tmpl.children[0].children = frag
261
+ bad_tmpl.swap(new_tmpl)
262
+ end
263
+
264
+ node.parent << new_node
265
+ new_node["hmod-added"] = ""
266
+
267
+ @_context_locals = { **(previous_context || {}) }
268
+ _context_locals[lh[0]] = list_item
269
+ _context_locals[lh[1]] = index if lh[1]
270
+
271
+ Fragment.new(
272
+ new_node, self.class.attribute_bindings,
273
+ html_module: self.class.html_module
274
+ ).process
275
+ end
276
+ end
277
+ end
278
+
279
+ def evaluate_attribute_expression(attribute, eval_code = attribute.value)
280
+ eval_code = eval_code.gsub(/\${(.*)}/, "\#{\\1}")
281
+ _context_locals.keys.reverse_each do |name|
282
+ eval_code = "#{name} = _context_locals[\"#{name}\"];" + eval_code
283
+ end
284
+ instance_eval(eval_code, self.class.html_module) # , attribute.line)
285
+ end
286
+
287
+ def class_list_for(obj)
288
+ case obj
289
+ when Hash
290
+ obj.filter { |_k, v| v }.keys
291
+ when Array
292
+ # TODO: handle objects inside of an array?
293
+ obj
294
+ else
295
+ Array[obj]
296
+ end.join(" ")
297
+ end
298
+
299
+ def _context_nodes
300
+ @_context_nodes ||= []
301
+ end
302
+
303
+ def _context_locals
304
+ @_context_locals ||= {}
305
+ end
306
+
307
+ def _check_stack(node)
308
+ node_and_ancestors = [node, *node.ancestors.to_a]
309
+ stack_misses = 0
310
+
311
+ _context_nodes.each do |stack_node|
312
+ if node_and_ancestors.none? { _1["hmod-added"] } && node_and_ancestors.none? { _1 == stack_node }
313
+ stack_misses += 1
314
+ end
315
+ end
316
+
317
+ stack_misses.times { _context_nodes.pop }
318
+
319
+ node_and_ancestors.any? { _context_nodes.include?(_1) }
320
+ end
321
+
322
+ def _in_context_nodes
323
+ previous_context = _context_locals
324
+ yield previous_context
325
+ @_context_locals = previous_context
196
326
  end
197
327
  end
metadata CHANGED
@@ -1,29 +1,29 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: hot_module
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0.alpha1
4
+ version: 1.0.0.alpha2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jared White
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-10-15 00:00:00.000000000 Z
11
+ date: 2023-03-16 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
- name: nokogiri
14
+ name: nokolexbor
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
17
  - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: '1.13'
19
+ version: '0.3'
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
- version: '1.13'
26
+ version: '0.3'
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: hash_with_dot_access
29
29
  requirement: !ruby/object:Gem::Requirement
@@ -53,6 +53,7 @@ files:
53
53
  - LICENSE.txt
54
54
  - README.md
55
55
  - Rakefile
56
+ - benchmark.rb
56
57
  - hot_module.gemspec
57
58
  - lib/hot_module.rb
58
59
  - lib/hot_module/fragment.rb