# lib/moxml/node_set.rb module Moxml class NodeSet include Enumerable attr_reader :native_nodes def initialize(native_nodes = []) @native_nodes = Array(native_nodes) end def each return enum_for(:each) unless block_given? native_nodes.each { |node| yield Node.wrap(node) } self end def [](index) case index when Integer Node.wrap(native_nodes[index]) when Range NodeSet.new(native_nodes[index]) end end def first Node.wrap(native_nodes.first) end def last Node.wrap(native_nodes.last) end def empty? native_nodes.empty? end def size native_nodes.size end alias length size def to_a map { |node| node } end def filter(selector) NodeSet.new( native_nodes.select { |node| Moxml.adapter.matches?(node, selector) } ) end def remove each(&:remove) self end def text map(&:text).join end def inner_html map(&:inner_html).join end def wrap(html_or_element) each do |node| wrapper = case html_or_element when String Document.parse("
#{html_or_element}
").root.children.first when Element html_or_element.dup else raise ArgumentError, "Expected String or Element" end node.add_previous_sibling(wrapper) wrapper.add_child(node) end self end def add_class(names) each do |node| next unless node.is_a?(Element) current = (node["class"] || "").split(/\s+/) new_classes = names.is_a?(Array) ? names : names.split(/\s+/) node["class"] = (current + new_classes).uniq.join(" ") end self end def remove_class(names) each do |node| next unless node.is_a?(Element) current = (node["class"] || "").split(/\s+/) remove_classes = names.is_a?(Array) ? names : names.split(/\s+/) node["class"] = (current - remove_classes).join(" ") end self end def attr(name, value = nil) if value.nil? first&.[](name) else each { |node| node[name] = value if node.is_a?(Element) } self end end # Collection operations def +(other) NodeSet.new(native_nodes + other.native_nodes) end def -(other) NodeSet.new(native_nodes - other.native_nodes) end def &(other) NodeSet.new(native_nodes & other.native_nodes) end def |(other) NodeSet.new(native_nodes | other.native_nodes) end def uniq NodeSet.new(native_nodes.uniq) end def reverse NodeSet.new(native_nodes.reverse) end # Search and filtering def find_by_id(id) detect { |node| node.is_a?(Element) && node["id"] == id } end def find_by_class(class_name) select { |node| node.is_a?(Element) && (node["class"] || "").split(/\s+/).include?(class_name) } end def find_by_attribute(name, value = nil) select do |node| next unless node.is_a?(Element) if value.nil? node.attributes.key?(name) else node[name] == value end end end def of_type(type) select { |node| node.is_a?(type) } end # DOM Manipulation def before(node_or_nodes) each { |node| node.add_previous_sibling(node_or_nodes) } self end def after(node_or_nodes) each { |node| node.add_next_sibling(node_or_nodes) } self end def replace_with(node_or_nodes) each { |node| node.replace(node_or_nodes) } self end def wrap_all(wrapper) return self if empty? wrapper_node = case wrapper when String Document.parse(wrapper).root when Element wrapper else raise ArgumentError, "Expected String or Element" end first.add_previous_sibling(wrapper_node) wrapper_node.add_child(self) self end # Content manipulation def inner_text=(text) each { |node| node.inner_text = text } self end def inner_html=(html) each { |node| node.inner_html = html } self end # Attribute operations def toggle_class(names) names = names.split(/\s+/) if names.is_a?(String) each do |node| next unless node.is_a?(Element) current = (node["class"] || "").split(/\s+/) names.each do |name| if current.include?(name) current.delete(name) else current << name end end node["class"] = current.uniq.join(" ") end self end def has_class?(name) any? { |node| node.is_a?(Element) && (node["class"] || "").split(/\s+/).include?(name) } end def remove_attr(*attrs) each do |node| next unless node.is_a?(Element) attrs.each { |attr| node.remove_attribute(attr) } end self end # Position and hierarchy def parents NodeSet.new( map { |node| node.parent }.compact.uniq ) end def children NodeSet.new( flat_map { |node| node.children.to_a } ) end def siblings NodeSet.new( flat_map { |node| node.parent ? node.parent.children.reject { |sibling| sibling == node } : [] } ).uniq end def next NodeSet.new( map { |node| node.next_sibling }.compact ) end def previous NodeSet.new( map { |node| node.previous_sibling }.compact ) end end end