lib/hot_module.rb in hot_module-1.0.0.alpha1 vs lib/hot_module.rb in hot_module-1.0.0.alpha2
- old
+ new
@@ -1,19 +1,29 @@
# frozen_string_literal: true
require_relative "hot_module/version"
-require "nokogiri"
+require "nokolexbor"
# Include this module into your own component class
module HoTModuLe
class Error < StandardError; end
- AttributeBinding = Struct.new(:matcher, :method_name, :method, keyword_init: true) # rubocop:disable Lint/StructNewOverride
+ module JSTemplateLiterals
+ refine Kernel do
+ def `(str)
+ str
+ end
+ end
+ end
+ using JSTemplateLiterals
+
+ AttributeBinding = Struct.new(:matcher, :method_name, :method, :only_for_tag, keyword_init: true) # rubocop:disable Lint/StructNewOverride
+
require_relative "hot_module/fragment"
- require_relative "hot_module/query_selection"
+ # require_relative "hot_module/query_selection"
# @param klass [Class]
# @return [void]
def self.included(klass)
klass.extend ClassMethods
@@ -23,11 +33,15 @@
klass.include ContentMethod
end
# Extends the component class
module ClassMethods
- def html_file_extensions = %w[tmpl.html html].freeze
+ def camelcased(method_symbol)
+ alias_method(method_symbol.to_s.gsub(/(?!^)_[a-z0-9]/) { |match| match[1].upcase }, method_symbol)
+ end
+
+ def html_file_extensions = %w[module.html tmpl.html html].freeze
def processed_css_extension = "css-local"
# @param tag_name [String]
# @param html_module [String] if not provided, a class method called `source_location` must be
# available with the absolute path of the Ruby file
@@ -74,23 +88,24 @@
@shadow_root ||= value
end
# @return [Nokogiri::XML::Element]
def doc
- @doc ||= Nokogiri::HTML5.fragment(
- "<#{tag_name}>#{File.read(html_module)}</#{tag_name}>"
- ).first_element_child
+ @doc ||= Nokolexbor::DocumentFragment.parse(
+ "<#{tag_name}>#{File.read(html_module).strip}</#{tag_name}>"
+ ).children.find(&:element?)
end
def attribute_bindings
@attribute_bindings ||= []
end
- def attribute_binding(matcher, method_name)
+ def attribute_binding(matcher, method_name, only: nil)
attribute_bindings << AttributeBinding.new(
matcher: Regexp.new(matcher),
- method_name: method_name
+ method_name: method_name,
+ only_for_tag: only
)
end
end
module ContentMethod
@@ -108,24 +123,36 @@
# @param attributes [Hash]
# @param content [String, Nokogiri::XML::Element]
# @param return_node [Boolean]
def render_element(attributes: self.attributes, content: self.content, return_node: false) # rubocop:disable Metrics
doc = self.class.doc.clone
+
+ # NOTE: have to fix cloned templates
+ doc.css("template").each do |bad_tmpl|
+ frag = bad_tmpl.children.last
+ new_tmpl = doc.document.create_element("template")
+ bad_tmpl.attributes.each do |k, v|
+ new_tmpl[k] = v
+ end
+ new_tmpl.children[0].children = frag
+ bad_tmpl.swap(new_tmpl)
+ end
+
tmpl_el = doc.css("> template").find { _1.attributes.length.zero? }
unless tmpl_el
tmpl_el = doc.document.create_element("template")
immediate_children = doc.css("> :not(style):not(script)")
- tmpl_el << immediate_children
+ tmpl_el.children[0] << immediate_children
doc.prepend_child(tmpl_el)
end
# Process all the template bits
process_fragment(tmpl_el)
# Set attributes on the custom element
- attributes.each { |k, v| doc[k.to_s.tr("_", "-")] = v }
+ attributes.each { |k, v| doc[k.to_s.tr("_", "-")] = value_to_attribute(v) if v }
# Look for external and internal styles
output_styles = ""
external_styles = doc.css("link[rel=stylesheet]")
external_styles.each do |external_style|
@@ -158,17 +185,17 @@
style_tag.content = output_styles
end
if self.class.shadow_root
# Guess what? We can reuse the same template tag! =)
- tmpl_el["shadowroot"] = "open"
- tmpl_el << style_tag if style_tag
+ tmpl_el["shadowrootmode"] = "open"
+ tmpl_el.children[0] << style_tag if style_tag
doc << content if content
else
- tmpl_el << style_tag if style_tag
- tmpl_el.at_css("slot:not([name])")&.swap(content) if content
- tmpl_el.children.each do |node|
+ tmpl_el.children[0] << style_tag if style_tag
+ tmpl_el.children[0].at_css("slot:not([name])")&.swap(content) if content
+ tmpl_el.children[0].children.each do |node|
doc << node
end
tmpl_el.remove
end
@@ -178,10 +205,25 @@
def call(...)
render_element(...)
end
+ def inspect
+ "#<#{self.class.name} #{attributes}>"
+ end
+
+ def value_to_attribute(val)
+ case val
+ when String, Numeric
+ val
+ when TrueClass
+ ""
+ else
+ val.to_json
+ end
+ end
+
# Override in component if need be, otherwise we'll use the node walker/binding pipeline
#
# @param fragment [Nokogiri::XML::Element]
# @return [void]
def process_fragment(fragment)
@@ -189,9 +231,97 @@
fragment, self.class.attribute_bindings.each { _1.method = method(_1.method_name) },
html_module: self.class.html_module
).process
end
- def inspect
- "#<#{self.class.name} #{attributes}>"
+ def process_list(attribute:, node:, item_node:, for_in:) # rubocop:disable Metrics
+ _context_nodes.push(node)
+
+ lh = for_in[0].strip.delete_prefix("(").delete_suffix(")").split(",").map!(&:strip)
+ rh = for_in[1].strip
+
+ list_items = evaluate_attribute_expression(attribute, rh)
+
+ # TODO: handle object style
+ # https://vuejs.org/guide/essentials/list.html#v-for-with-an-object
+
+ return unless list_items
+
+ _in_context_nodes do |previous_context|
+ list_items.each_with_index do |list_item, index|
+ new_node = item_node.clone
+
+ # NOTE: have to fix cloned templates
+ new_node.css("template").each do |bad_tmpl|
+ frag = bad_tmpl.children.last
+ new_tmpl = item_node.document.create_element("template")
+ bad_tmpl.attributes.each do |k, v|
+ new_tmpl[k] = v
+ end
+ new_tmpl.children[0].children = frag
+ bad_tmpl.swap(new_tmpl)
+ end
+
+ node.parent << new_node
+ new_node["hmod-added"] = ""
+
+ @_context_locals = { **(previous_context || {}) }
+ _context_locals[lh[0]] = list_item
+ _context_locals[lh[1]] = index if lh[1]
+
+ Fragment.new(
+ new_node, self.class.attribute_bindings,
+ html_module: self.class.html_module
+ ).process
+ end
+ end
+ end
+
+ def evaluate_attribute_expression(attribute, eval_code = attribute.value)
+ eval_code = eval_code.gsub(/\${(.*)}/, "\#{\\1}")
+ _context_locals.keys.reverse_each do |name|
+ eval_code = "#{name} = _context_locals[\"#{name}\"];" + eval_code
+ end
+ instance_eval(eval_code, self.class.html_module) # , attribute.line)
+ end
+
+ def class_list_for(obj)
+ case obj
+ when Hash
+ obj.filter { |_k, v| v }.keys
+ when Array
+ # TODO: handle objects inside of an array?
+ obj
+ else
+ Array[obj]
+ end.join(" ")
+ end
+
+ def _context_nodes
+ @_context_nodes ||= []
+ end
+
+ def _context_locals
+ @_context_locals ||= {}
+ end
+
+ def _check_stack(node)
+ node_and_ancestors = [node, *node.ancestors.to_a]
+ stack_misses = 0
+
+ _context_nodes.each do |stack_node|
+ if node_and_ancestors.none? { _1["hmod-added"] } && node_and_ancestors.none? { _1 == stack_node }
+ stack_misses += 1
+ end
+ end
+
+ stack_misses.times { _context_nodes.pop }
+
+ node_and_ancestors.any? { _context_nodes.include?(_1) }
+ end
+
+ def _in_context_nodes
+ previous_context = _context_locals
+ yield previous_context
+ @_context_locals = previous_context
end
end