# frozen_string_literal: true require 'nokogiri' module Boxcars class XNode attr_accessor :node, :children, :attributes def initialize(node) @node = node @valid_names = [] @children = {} @attributes = node.attributes.values.to_h { |a| [a.name.to_sym, a.value] } node.children.each do |child| next if child.text? child_node = XNode.new(child) if @children[child.name].nil? @valid_names << child.name.to_sym @children[child.name] = child_node elsif @children[child.name].is_a?(Array) @children[child.name] << child_node else @children[child.name] = [@children[child.name], child_node] end end end def self.from_xml(xml) xml = xml[xml.index("<")..-1] unless xml.start_with?("<") xml = xml[0..xml.rindex(">")] unless xml.end_with?(">") doc = Nokogiri::XML.parse(xml) if doc.errors.any? Boxcars.debug("XML: #{xml}", :yellow) # rubocop:disable Lint/Debugger debugger if ENV.fetch("DEBUG_XML", false) # rubocop:enable Lint/Debugger raise XmlError, "XML is not valid: #{doc.errors.map { |e| "#{e.line}:#{e.column} #{e.message}" }}" end XNode.new(doc.root) end def xml @node.to_xml end def text @node.text end def xpath(path) @node.xpath(path) end def xtext(path) rv = xpath(path)&.text&.gsub(/[[:space:]]+/, " ")&.strip return nil if rv.empty? rv end def stext @stext ||= text.gsub(/[[:space:]]+/, " ").strip # remove extra spaces end def [](key) @children[key.to_s] end def method_missing(name, *args) return @children[name.to_s] if @children.key?(name.to_s) super end def respond_to_missing?(method_name, include_private = false) @valid_names.include?(method) || super end end end