Module: Canon::XmlParsing

Defined in:
lib/canon/xml_parsing.rb

Overview

Backend-agnostic XML parsing, serialization, and type dispatch.

Responsibilities (MECE):

  • Engine actions: parse selects the XML engine chosen by Canon::XmlBackend (raw Nokogiri or moxml with its resolved adapter — leptris when installed, nokogiri otherwise).
  • Node type dispatch: type queries and traversal answer for ANY recognized node (Nokogiri or moxml) regardless of the active engine — callers may hand us nodes from either library (user input, format detection). Dispatch is by node type, never by backend.

defined?(Nokogiri) guards keep Nokogiri constants unresolved under Opal (no NameError at runtime).

OCP: adding a new engine means extending Canon::XmlBackend detection plus the node-type union here; all comparator/formatter code stays untouched because it goes through this module.

Class Method Summary collapse

Class Method Details

.attribute_value(node, attr_name) ⇒ Object



190
191
192
193
194
195
196
# File 'lib/canon/xml_parsing.rb', line 190

def attribute_value(node, attr_name)
  if defined?(Nokogiri) && node.is_a?(Nokogiri::XML::Element)
    node[attr_name.to_s]
  elsif node.is_a?(Moxml::Element)
    node[attr_name.to_s]
  end
end

.attributes(node) ⇒ Object



180
181
182
183
184
185
186
187
188
# File 'lib/canon/xml_parsing.rb', line 180

def attributes(node)
  if defined?(Nokogiri) && node.is_a?(Nokogiri::XML::Element)
    node.attributes.values
  elsif node.is_a?(Moxml::Element)
    node.attributes
  else
    []
  end
end

.cdata?(node) ⇒ Boolean

Returns:

  • (Boolean)


125
126
127
128
129
# File 'lib/canon/xml_parsing.rb', line 125

def cdata?(node)
  return true if defined?(Nokogiri) && node.is_a?(Nokogiri::XML::CDATA)

  node.is_a?(Moxml::Cdata)
end

.children(node) ⇒ Object

--- Node traversal ---



147
148
149
150
151
152
153
154
155
# File 'lib/canon/xml_parsing.rb', line 147

def children(node)
  if defined?(Nokogiri) && node.is_a?(Nokogiri::XML::Node)
    node.children.to_a
  elsif node.is_a?(Moxml::Node)
    node.children.to_a
  else
    []
  end
end

.comment?(node) ⇒ Boolean

Returns:

  • (Boolean)


119
120
121
122
123
# File 'lib/canon/xml_parsing.rb', line 119

def comment?(node)
  return true if defined?(Nokogiri) && node.is_a?(Nokogiri::XML::Comment)

  node.is_a?(Moxml::Comment)
end

.document?(obj) ⇒ Boolean

--- Type checks (any recognized engine node) ---

Returns:

  • (Boolean)


95
96
97
98
99
# File 'lib/canon/xml_parsing.rb', line 95

def document?(obj)
  return true if defined?(Nokogiri) && obj.is_a?(Nokogiri::XML::Document)

  obj.is_a?(Moxml::Document)
end

.document_fragment?(obj) ⇒ Boolean

Returns:

  • (Boolean)


137
138
139
# File 'lib/canon/xml_parsing.rb', line 137

def document_fragment?(obj)
  defined?(Nokogiri) && obj.is_a?(Nokogiri::XML::DocumentFragment)
end

.dtd?(node) ⇒ Boolean

Returns:

  • (Boolean)


141
142
143
# File 'lib/canon/xml_parsing.rb', line 141

def dtd?(node)
  defined?(Nokogiri) && node.is_a?(Nokogiri::XML::DTD)
end

.element?(node) ⇒ Boolean

Returns:

  • (Boolean)


107
108
109
110
111
# File 'lib/canon/xml_parsing.rb', line 107

def element?(node)
  return true if defined?(Nokogiri) && node.is_a?(Nokogiri::XML::Element)

  node.is_a?(Moxml::Element)
end

.moxml_adapter_nameObject

The adapter moxml resolved on this runtime. This is the single source of truth for engine capability: moxml owns the preference order, canon never probes gems itself.



33
34
35
# File 'lib/canon/xml_parsing.rb', line 33

def moxml_adapter_name
  moxml_context.config.adapter_name
end

.moxml_contextObject



23
24
25
26
27
28
# File 'lib/canon/xml_parsing.rb', line 23

def moxml_context
  # Opal needs the explicit rexml adapter: stock oga requires its
  # C extension there. CRuby defers to moxml's preferred adapter
  # (leptris when installed and capable, nokogiri otherwise).
  @moxml_context ||= Moxml.new(RUBY_ENGINE == "opal" ? :rexml : nil)
end

.moxml_node_address(wrapper) ⇒ Object



83
84
85
86
87
88
89
90
91
# File 'lib/canon/xml_parsing.rb', line 83

def moxml_node_address(wrapper)
  native = wrapper.native
  case native
  when ::Leptris::XML::NativeNode then native.address
  when ::Leptris::XML::Node then native.c_ptr.address
  end
rescue StandardError
  nil
end

.name(node) ⇒ Object



157
158
159
160
161
162
163
# File 'lib/canon/xml_parsing.rb', line 157

def name(node)
  if defined?(Nokogiri) && node.is_a?(Nokogiri::XML::Node)
    node.name
  elsif node.is_a?(Moxml::Node)
    node.name
  end
end

.namespace_definitions(node) ⇒ Object



198
199
200
201
202
203
204
205
206
# File 'lib/canon/xml_parsing.rb', line 198

def namespace_definitions(node)
  if defined?(Nokogiri) && node.is_a?(Nokogiri::XML::Element)
    node.namespace_definitions
  elsif node.is_a?(Moxml::Element)
    node.namespace_definitions
  else
    []
  end
end

.namespace_uri(node) ⇒ Object



216
217
218
219
220
221
222
# File 'lib/canon/xml_parsing.rb', line 216

def namespace_uri(node)
  if defined?(Nokogiri) && node.is_a?(Nokogiri::XML::Element)
    node.namespace&.href
  elsif node.is_a?(Moxml::Element)
    node.namespace_uri
  end
end

.node_type(node) ⇒ Object

Returns a symbol for all engines (:element, :text, :comment, etc.) or nil for unrecognised nodes.



226
227
228
229
230
231
232
# File 'lib/canon/xml_parsing.rb', line 226

def node_type(node)
  if defined?(Nokogiri) && node.is_a?(Nokogiri::XML::Node)
    nokogiri_node_type(node)
  elsif node.is_a?(Moxml::Node)
    moxml_node_type(node)
  end
end

.parent(node) ⇒ Object



208
209
210
211
212
213
214
# File 'lib/canon/xml_parsing.rb', line 208

def parent(node)
  return nil unless xml_node?(node)
  # Document nodes have no parent
  return nil if document?(node)

  node.parent
end

.parse(xml_string, options = {}) ⇒ Object

--- Parsing ---



39
40
41
42
43
44
45
# File 'lib/canon/xml_parsing.rb', line 39

def parse(xml_string, options = {})
  if XmlBackend.nokogiri?
    nokogiri_parse(xml_string, options)
  else
    moxml_parse(xml_string, options)
  end
end

.parse_fragment(xml_string) ⇒ Object



47
48
49
50
51
52
53
54
# File 'lib/canon/xml_parsing.rb', line 47

def parse_fragment(xml_string)
  if XmlBackend.nokogiri?
    Nokogiri::XML.fragment(xml_string).children.to_a
  else
    doc = moxml_context.parse("<__frag__>#{xml_string}</__frag__>", readonly: true)
    doc.root.children.to_a
  end
end

.processing_instruction?(node) ⇒ Boolean

Returns:

  • (Boolean)


131
132
133
134
135
# File 'lib/canon/xml_parsing.rb', line 131

def processing_instruction?(node)
  return true if defined?(Nokogiri) && node.is_a?(Nokogiri::XML::ProcessingInstruction)

  node.is_a?(Moxml::ProcessingInstruction)
end

.same_engine_node?(left, right) ⇒ Boolean

Same underlying engine node? Wrapper identity first (correct everywhere wrappers are unified). Fallback: moxml 0.5.36-0.5.39 wrap Document#root and Document#children entries in DIFFERENT wrapper classes for one node (NativeNode vs the FFI Element — moxml#216), so identity fails exactly where consumers exclude the document element from children; the shared C pointer address is the stable identity. Inert once wrappers unify.

Returns:

  • (Boolean)


73
74
75
76
77
78
79
80
81
# File 'lib/canon/xml_parsing.rb', line 73

def same_engine_node?(left, right)
  return true if left.equal?(right)
  return false unless defined?(::Moxml::Node) &&
    left.is_a?(::Moxml::Node) && right.is_a?(::Moxml::Node)

  address_left = moxml_node_address(left)
  address_right = moxml_node_address(right)
  !address_left.nil? && address_left == address_right
end

.serialize(node) ⇒ Object

--- Serialization ---



58
59
60
61
62
63
64
# File 'lib/canon/xml_parsing.rb', line 58

def serialize(node)
  if defined?(Nokogiri) && node.is_a?(Nokogiri::XML::Document)
    node.to_xml(encoding: "UTF-8")
  else
    node.to_xml
  end
end

.text_content(node) ⇒ Object



165
166
167
168
169
170
171
172
173
174
175
176
177
178
# File 'lib/canon/xml_parsing.rb', line 165

def text_content(node)
  if defined?(Nokogiri) && node.is_a?(Nokogiri::XML::Node)
    node.content
  else
    case node
    when Moxml::Text, Moxml::Cdata, Moxml::Comment
      node.content.to_s
    when Moxml::Node
      node.text.to_s
    else
      node.to_s
    end
  end
end

.text_node?(node) ⇒ Boolean

Returns:

  • (Boolean)


113
114
115
116
117
# File 'lib/canon/xml_parsing.rb', line 113

def text_node?(node)
  return true if defined?(Nokogiri) && node.is_a?(Nokogiri::XML::Text)

  node.is_a?(Moxml::Text)
end

.xml_node?(obj) ⇒ Boolean

Returns:

  • (Boolean)


101
102
103
104
105
# File 'lib/canon/xml_parsing.rb', line 101

def xml_node?(obj)
  return true if defined?(Nokogiri) && obj.is_a?(Nokogiri::XML::Node)

  obj.is_a?(Moxml::Node)
end