# frozen_string_literal: true require "hot_module" module JSStrings refine Kernel do def `(str) str end end refine String do def underscore gsub(/([A-Z]+)(?=[A-Z][a-z])|([a-z\d])(?=[A-Z])/) do (::Regexp.last_match(1) || ::Regexp.last_match(2)) << "_" end.tr("-", "_").downcase end end end module HoTModuLe module Petite using JSStrings # @param klass [Class] # @return [void] def self.included(klass) klass.attribute_binding "v-for", :_for_binding klass.attribute_binding "v-text", :_text_binding klass.attribute_binding "v-html", :_html_binding klass.attribute_binding "v-bind", :_handle_bound_attribute klass.attribute_binding %r{^:}, :_handle_bound_attribute end protected def evaluate_attribute_expression(attribute, eval_code = attribute.value) # rubocop:disable Metrics/AbcSize eval_code = eval_code.gsub(/\${(.*)}/, "\#{\\1}") @_locals ||= {} @_locals.keys.reverse_each do |name| eval_code = "#{name} = @_locals[\"#{name}\"];" + eval_code end instance_eval(eval_code, self.class.html_module, attribute.line) rescue NameError => e bad_name = e.message.match(/`(.*?)'/)[1] suggestion = bad_name.underscore eval_code.gsub!(bad_name, suggestion) instance_eval(eval_code, self.class.html_module, attribute.line) end def _locals_stack @_locals_stack ||= [] end def _check_stack(node) # rubocop:disable Metrics/AbcSize node_and_ancestors = [node, *node.ancestors.to_a] stack_misses = 0 stack_nodes = _locals_stack.map { _1[:node] } stack_nodes.each do |stack_node| if node_and_ancestors.none? { _1["v-if"] == "!hydrated" } && node_and_ancestors.none? { _1 == stack_node } stack_misses += 1 end end stack_misses.times { _locals_stack.pop } !((node_and_ancestors & _locals_stack.map { _1[:node] }).empty?) # rubocop:disable Style/RedundantParentheses end def _for_binding(attribute:, node:) return unless node.name == "template" return if _check_stack(node) @_locals_stack.push({ node: node }) _process_list(attribute: attribute, node: node) end def _text_binding(attribute:, node:) return if _check_stack(node) node.content = evaluate_attribute_expression(attribute) end def _html_binding(attribute:, node:) return if _check_stack(node) node.content = evaluate_attribute_expression(attribute) end def _handle_bound_attribute(attribute:, node:) # rubocop:disable Metrics return if _check_stack(node) return if attribute.name == ":key" real_attribute = if attribute.name.start_with?(":") attribute.name.delete_prefix(":") elsif attribute.name.start_with?("v-bind:") attribute.name.delete_prefix("v-bind:") end obj = evaluate_attribute_expression(attribute) if real_attribute == "class" class_names = case obj when Hash obj.filter { |_k, v| v == true }.keys when Array # TODO: handle objects inside of an array obj else Array[obj] end node[real_attribute] = class_names.join(" ") elsif real_attribute != "style" # style bindings aren't SSRed node[real_attribute] = obj if obj end end def _process_list(attribute:, node:) # rubocop:disable Metrics item_node = node.element_children.first delimiter = node["v-for"].include?(" of ") ? " of " : " in " expression = node["v-for"].split(delimiter) lh = expression[0].strip.delete_prefix("(").delete_suffix(")").split(",").map!(&:strip) rh = expression[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_locals_stack do list_items.each_with_index do |list_item, index| new_node = item_node.clone node.parent << new_node new_node["v-if"] = "!hydrated" local_items = { **(prev_items || {}) } local_items[lh[0]] = list_item local_items[lh[1]] = index if lh[1] @_locals = local_items Fragment.new( new_node, self.class.attribute_bindings, html_module: self.class.html_module ).process end end end def _in_locals_stack prev_items = @_locals yield @_locals = prev_items end end end