lib/moxml/adapter/ox.rb



# frozen_string_literal: true

require_relative "base"
require "ox"

module Moxml
  module Adapter
    class Ox < Base
      class << self
        def set_root(doc, element)
          replace_children(doc, [element])
        end

        def parse(xml, _options = {})
          native_doc = begin
            result = ::Ox.parse(xml)

            # result can be either Document or Element
            if result.is_a?(::Ox::Document)
              result
            else
              doc = ::Ox::Document.new
              doc << result
              doc
            end
          rescue ::Ox::ParseError => e
            raise Moxml::ParseError, e.message
          end

          DocumentBuilder.new(Context.new(:ox)).build(native_doc)
        end

        def create_document
          ::Ox::Document.new
        end

        def create_native_element(name)
          element = ::Ox::Element.new(name)
          element.instance_variable_set(:@attributes, {})
          element
        end

        def create_native_text(content)
          content
        end

        def create_native_cdata(content)
          ::Ox::CData.new(content)
        end

        def create_native_comment(content)
          ::Ox::Comment.new(content)
        end

        def create_native_processing_instruction(target, content)
          inst = ::Ox::Instruction.new(target)
          inst.value = content
          inst
        end

        # TODO: compare  to create_native_declaration
        def create_native_declaration2(version, encoding, standalone)
          inst = ::Ox::Instruct.new("xml")
          inst.value = build_declaration_attrs(version, encoding, standalone)
          inst
        end

        def create_native_declaration(version, encoding, standalone)
          doc = ::Ox::Document.new
          doc.version = version
          doc.encoding = encoding
          doc.standalone = standalone
          doc
        end

        def create_native_namespace(element, prefix, uri)
          element.attributes ||= {}
          attr_name = prefix ? "xmlns:#{prefix}" : "xmlns"
          element.attributes[attr_name] = uri
          [prefix, uri]
        end

        def set_namespace(element, ns)
          prefix, uri = ns
          element.attributes ||= {}
          attr_name = prefix ? "xmlns:#{prefix}" : "xmlns"
          element.attributes[attr_name] = uri
        end

        def namespace(element)
          return nil unless element.attributes

          xmlns_attr = element.attributes.find { |k, _| k.start_with?("xmlns:") || k == "xmlns" }
          return nil unless xmlns_attr

          prefix = xmlns_attr[0] == "xmlns" ? nil : xmlns_attr[0].sub("xmlns:", "")
          [prefix, xmlns_attr[1]]
        end

        def processing_instruction_target(node)
          node.name
        end

        def node_type(node)
          case node
          when ::Ox::Document then :document
          when String then :text
          when ::Ox::CData then :cdata
          when ::Ox::Comment then :comment
          when ::Ox::Instruct then :processing_instruction
          when ::Ox::Element then :element
          else :unknown
          end
        end

        def node_name(node)
          node.value
        rescue StandardError
          node.name
        end

        def set_node_name(node, name)
          node.value = name if node.respond_to?(:value=)
          node.name = name if node.respond_to?(:name=)
        end

        def children(node)
          return [] unless node.respond_to?(:nodes)

          node.nodes || []
        end

        def parent(node)
          node.parent if node.respond_to?(:parent)
        end

        def next_sibling(node)
          return unless (parent = parent(node))

          siblings = parent.nodes
          idx = siblings.index(node)
          idx ? siblings[idx + 1] : nil
        end

        def previous_sibling(node)
          return unless (parent = parent(node))

          siblings = parent.nodes
          idx = siblings.index(node)
          idx&.positive? ? siblings[idx - 1] : nil
        end

        def document(node)
          current = node
          current = parent(current) while parent(current)
          current
        end

        def root(document)
          document.nodes&.find { |node| node.is_a?(::Ox::Element) }
        end

        def attributes(element)
          return {} unless element.respond_to?(:attributes) && element.attributes

          element.attributes.reject { |k, _| k.start_with?("xmlns") }
        end

        def set_attribute(element, name, value)
          element.attributes ||= {}
          element.attributes[name.to_s] = value.to_s
        end

        def get_attribute(element, name)
          return nil unless element.respond_to?(:attributes) && element.attributes

          element.attributes[name.to_s]
        end

        def remove_attribute(element, name)
          return unless element.respond_to?(:attributes) && element.attributes

          element.attributes.delete(name.to_s)
        end

        def add_child(element, child)
          element.nodes ||= []
          puts "Add child #{child} for #{element.name}: #{element.nodes.count}"
          element.nodes << child
        end

        def add_previous_sibling(node, sibling)
          return unless parent(node)

          idx = node.parent.nodes.index(node)
          node.parent.nodes.insert(idx, sibling) if idx
        end

        def add_next_sibling(node, sibling)
          return unless parent(node)

          idx = node.parent.nodes.index(node)
          node.parent.nodes.insert(idx + 1, sibling) if idx
        end

        def remove(node)
          return unless parent(node)

          node.parent.nodes.delete(node)
        end

        def replace(node, new_node)
          return unless parent(node)

          idx = node.parent.nodes.index(node)
          node.parent.nodes[idx] = new_node if idx
        end

        def replace_children(node, new_children)
          node.remove_children_by_path("*")
          new_children.each { |child| node << child }
          node
        end

        def text_content(node)
          node.is_a?(String) ? node : node.value.to_s
        end

        def set_text_content(node, content)
          if node.is_a?(String)
            node.replace(content.to_s)
          else
            node.value = content.to_s
          end
        end

        def cdata_content(node)
          node.value.to_s
        end

        def set_cdata_content(node, content)
          node.value = content.to_s
        end

        def comment_content(node)
          node.value.to_s
        end

        def set_comment_content(node, content)
          node.value = content.to_s
        end

        def processing_instruction_content(node)
          node.value.to_s
        end

        def set_processing_instruction_content(node, content)
          node.value = content.to_s
        end

        def namespace_definitions(node)
          return [] unless node.respond_to?(:attributes) && node.attributes

          node.attributes.each_with_object([]) do |(name, value), namespaces|
            next unless name.start_with?("xmlns")

            prefix = name == "xmlns" ? nil : name.sub("xmlns:", "")
            namespaces << [prefix, value]
          end
        end

        def xpath(node, expression, namespaces = {})
          # Ox doesn't support XPath, implement basic path matching
          results = []
          traverse(node) do |n|
            results << n if matches_xpath?(n, expression, namespaces)
          end
          results
        end

        def at_xpath(node, expression, namespaces = {})
          traverse(node) do |n|
            return n if matches_xpath?(n, expression, namespaces)
          end
          nil
        end

        def serialize(node, options = {})
          ox_options = {
            indent: options[:indent] || -1,
            with_xml: true,
            with_instructions: true,
            encoding: options[:encoding]
          }
          ::Ox.dump(node, ox_options)
        end

        private

        def traverse(node, &block)
          return unless node

          yield node
          return unless node.respond_to?(:nodes)

          node.nodes&.each { |child| traverse(child, &block) }
        end

        def matches_xpath?(node, expression, _namespaces = {})
          case expression
          when %r{//(\w+)}
            node.is_a?(::Ox::Element) && node.value == ::Regexp.last_match(1)
          when %r{//(\w+)\[@(\w+)='([^']+)'\]}
            node.is_a?(::Ox::Element) &&
              node.value == ::Regexp.last_match(1) &&
              node.attributes &&
              node.attributes[::Regexp.last_match(2)] == ::Regexp.last_match(3)
          else
            false
          end
        end
      end
    end
  end
end