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