moxml 0.1.26 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/windows.yml +62 -0
  3. data/Gemfile +3 -1
  4. data/README.adoc +69 -6
  5. data/lib/compat/opal/moxml_boot.rb +5 -0
  6. data/lib/moxml/adapter/base.rb +53 -49
  7. data/lib/moxml/adapter/customized_leptris/declaration.rb +32 -0
  8. data/lib/moxml/adapter/customized_leptris/doctype.rb +31 -0
  9. data/lib/moxml/adapter/customized_leptris/entity_reference.rb +12 -0
  10. data/lib/moxml/adapter/customized_leptris/text_segment.rb +26 -0
  11. data/lib/moxml/adapter/customized_leptris.rb +16 -0
  12. data/lib/moxml/adapter/customized_libxml/cdata.rb +3 -12
  13. data/lib/moxml/adapter/customized_libxml/comment.rb +2 -9
  14. data/lib/moxml/adapter/customized_libxml/declaration.rb +1 -10
  15. data/lib/moxml/adapter/customized_libxml/node.rb +8 -0
  16. data/lib/moxml/adapter/customized_libxml/processing_instruction.rb +2 -10
  17. data/lib/moxml/adapter/customized_libxml/text.rb +2 -9
  18. data/lib/moxml/adapter/customized_oga/entity_decoder.rb +37 -0
  19. data/lib/moxml/adapter/customized_oga/raw_value_override.rb +52 -0
  20. data/lib/moxml/adapter/customized_oga/xml_generator.rb +4 -34
  21. data/lib/moxml/adapter/customized_oga.rb +2 -0
  22. data/lib/moxml/adapter/customized_ox/entity_reference.rb +1 -17
  23. data/lib/moxml/adapter/customized_rexml/entity_reference.rb +1 -11
  24. data/lib/moxml/adapter/headed_ox.rb +9 -128
  25. data/lib/moxml/adapter/leptris.rb +965 -0
  26. data/lib/moxml/adapter/libxml.rb +131 -99
  27. data/lib/moxml/adapter/nokogiri.rb +29 -8
  28. data/lib/moxml/adapter/oga.rb +91 -54
  29. data/lib/moxml/adapter/ox.rb +93 -137
  30. data/lib/moxml/adapter/rexml.rb +61 -50
  31. data/lib/moxml/adapter.rb +2 -1
  32. data/lib/moxml/attribute.rb +8 -4
  33. data/lib/moxml/attribute_resolver.rb +189 -0
  34. data/lib/moxml/config.rb +29 -3
  35. data/lib/moxml/context.rb +12 -0
  36. data/lib/moxml/element.rb +29 -12
  37. data/lib/moxml/entity/reference.rb +28 -0
  38. data/lib/moxml/entity.rb +125 -0
  39. data/lib/moxml/node.rb +90 -1
  40. data/lib/moxml/sax/namespace_splitter.rb +5 -2
  41. data/lib/moxml/signature/model/key/dsa_key_value.rb +1 -2
  42. data/lib/moxml/version.rb +1 -1
  43. data/lib/moxml/xml_emitter.rb +71 -0
  44. data/lib/moxml/xpath/compiler.rb +60 -7
  45. data/lib/moxml/xpath/parser.rb +22 -15
  46. data/lib/moxml.rb +3 -0
  47. data/spec/examples/readme_examples_spec.rb +0 -4
  48. data/spec/examples/xpath_examples_spec.rb +0 -11
  49. data/spec/integration/all_adapters_spec.rb +17 -0
  50. data/spec/integration/sax_parity_spec.rb +58 -0
  51. data/spec/integration/shared_examples/edge_cases.rb +0 -10
  52. data/spec/integration/shared_examples/integration_workflows.rb +0 -10
  53. data/spec/integration/shared_examples/node_wrappers/attribute_behavior.rb +98 -0
  54. data/spec/integration/shared_examples/node_wrappers/namespace_behavior.rb +26 -0
  55. data/spec/integration/shared_examples/node_wrappers/node_behavior.rb +0 -8
  56. data/spec/moxml/adapter/leptris_spec.rb +75 -0
  57. data/spec/moxml/adapter/ox_spec.rb +20 -6
  58. data/spec/moxml/adapter/platform_spec.rb +15 -2
  59. data/spec/moxml/adapter/shared_examples/adapter_contract.rb +180 -0
  60. data/spec/moxml/attribute_resolver_spec.rb +108 -0
  61. data/spec/moxml/doctype_spec.rb +1 -1
  62. data/spec/moxml/entity_spec.rb +76 -0
  63. data/spec/moxml/node_spec.rb +104 -0
  64. data/spec/moxml/sax_entity_parity_spec.rb +358 -0
  65. data/spec/moxml/xml_emitter_spec.rb +64 -0
  66. data/spec/moxml/xpath/axes_spec.rb +72 -0
  67. data/spec/moxml/xpath/parser_spec.rb +6 -0
  68. data/spec/moxml/xpath_capabilities_spec.rb +5 -3
  69. data/spec/performance/benchmark_spec.rb +13 -6
  70. data/spec/performance/thread_safety_spec.rb +0 -4
  71. data/spec/spec_helper.rb +5 -1
  72. metadata +21 -2
@@ -28,15 +28,7 @@ module Moxml
28
28
  end
29
29
 
30
30
  def to_xml
31
- output = "<!DOCTYPE #{@name}"
32
- if @external_id && !@external_id.empty?
33
- output << " PUBLIC \"#{@external_id}\""
34
- output << " \"#{@system_id}\"" if @system_id
35
- elsif @system_id && !@system_id.empty?
36
- output << " SYSTEM \"#{@system_id}\""
37
- end
38
- output << ">"
39
- output
31
+ XmlEmitter.doctype_xml(@name, @external_id, @system_id)
40
32
  end
41
33
  end
42
34
 
@@ -57,6 +49,7 @@ module Moxml
57
49
 
58
50
  WRAPPER_NODE_TYPE_MAP = {
59
51
  DoctypeWrapper => :doctype,
52
+ CustomizedLibxml::Declaration => :declaration,
60
53
  CustomizedLibxml::Element => :element,
61
54
  CustomizedLibxml::Text => :text,
62
55
  CustomizedLibxml::Cdata => :cdata,
@@ -91,7 +84,7 @@ module Moxml
91
84
  xml_string = preprocess_entities(xml_string)
92
85
 
93
86
  # Extract DOCTYPE before parsing
94
- doctype_match = xml_string.match(/<!DOCTYPE\s+(\S+)(?:\s+PUBLIC\s+"([^"]+)"\s+"([^"]+)"| \s+SYSTEM\s+"([^"]+)")?\s*>/i)
87
+ doctype_match = xml_string.match(/<!DOCTYPE\s+([^\s>]+)(?:\s+PUBLIC\s+"([^"]+)"\s+"([^"]+)"|\s+SYSTEM\s+"([^"]+)")?\s*>/i)
95
88
 
96
89
  native_doc = begin
97
90
  # Handle both string and file inputs
@@ -379,6 +372,11 @@ module Moxml
379
372
  native_doc&.root
380
373
  end
381
374
 
375
+ def line_number(node)
376
+ native = unpatch_node(node)
377
+ native.is_a?(::LibXML::XML::Node) ? native.line_num : nil
378
+ end
379
+
382
380
  def attributes(element)
383
381
  native_elem = unpatch_node(element)
384
382
  return [] unless native_elem
@@ -437,9 +435,13 @@ module Moxml
437
435
  native_elem.attributes.get_attribute(name_str)
438
436
  end
439
437
  else
440
- # Regular attribute without namespace
441
- native_elem[name_str] = value_str
442
- native_elem.attributes.get_attribute(name_str)
438
+ # node[]= is ambiguous when a namespace-bound attribute
439
+ # shares the local name (it clobbers that attribute);
440
+ # create or replace the no-namespace attribute explicitly.
441
+ native_elem.each_attr do |attr|
442
+ attr.remove! if !attr.ns && attr.name == name_str
443
+ end if native_elem.attributes?
444
+ ::LibXML::XML::Attr.new(native_elem, name_str, value_str)
443
445
  end
444
446
  end
445
447
 
@@ -467,30 +469,46 @@ module Moxml
467
469
  native_elem = unpatch_node(element)
468
470
  return nil unless native_elem
469
471
 
470
- # Try to get the attribute with the given name (handles namespaced attrs)
471
- value = native_elem[name.to_s]
472
- return value if value
473
-
474
- # If name contains ':', try to get as namespaced attribute
475
- if name.to_s.include?(":")
476
- prefix, local_name = name.to_s.split(":", 2)
472
+ name_s = name.to_s
473
+ if name_s.include?(":")
474
+ prefix, local_name = name_s.split(":", 2)
477
475
  # Try to find attribute by namespace
478
476
  if native_elem.attributes?
479
477
  native_elem.each_attr do |attr|
480
- if attr.name == local_name || attr.name == name.to_s
478
+ if [local_name, name_s].include?(attr.name)
481
479
  # Check if attribute's namespace matches the prefix
482
480
  if attr.ns && attr.ns.prefix == prefix
483
481
  return attr.value
484
- elsif attr.name == name.to_s
482
+ elsif attr.name == name_s
485
483
  # Fallback: attribute name includes the prefix
486
484
  return attr.value
487
485
  end
488
486
  end
489
487
  end
490
488
  end
489
+
490
+ return nil
491
491
  end
492
492
 
493
- nil
493
+ # Bare name: node[name_s] is ambiguous when a namespaced
494
+ # attribute shares the local name (issue #94), so prefer the
495
+ # namespace-less attribute explicitly.
496
+ if native_elem.attributes?
497
+ namespaced_value = nil
498
+ native_elem.each_attr do |attr|
499
+ next if attr.name.to_s.start_with?("xmlns")
500
+ next unless attr.name == name_s
501
+
502
+ if attr.ns.nil?
503
+ return attr.value
504
+ elsif namespaced_value.nil?
505
+ namespaced_value = attr.value
506
+ end
507
+ end
508
+ return namespaced_value unless namespaced_value.nil?
509
+ end
510
+
511
+ native_elem[name_s]
494
512
  end
495
513
 
496
514
  def remove_attribute(element, name)
@@ -502,6 +520,10 @@ module Moxml
502
520
  attr&.remove!
503
521
  end
504
522
 
523
+ def remove_attribute_native(attr)
524
+ attr&.remove!
525
+ end
526
+
505
527
  def set_attribute_name(attribute, new_name)
506
528
  # LibXML attributes cannot be renamed directly
507
529
  # We must work at the element level
@@ -730,26 +752,52 @@ module Moxml
730
752
 
731
753
  def set_text_content(node, content)
732
754
  native_node = unpatch_node(node)
733
- native_node.content = content.to_s if native_node
755
+ return unless native_node.is_a?(::LibXML::XML::Node)
756
+
757
+ # Text wrapper: swap with new_text to preserve verbatim storage.
758
+ # Element wrapper: let libxml manage the child text node.
759
+ if native_node.text?
760
+ replace_native_verbatim(node, ::LibXML::XML::Node.new_text(content.to_s))
761
+ else
762
+ native_node.content = content.to_s
763
+ end
734
764
  end
735
765
 
736
- def cdata_content(node)
737
- native_node = unpatch_node(node)
738
- content = native_node&.content
739
- # LibXML may HTML-escape CDATA content, un-escape it
740
- return nil unless content
766
+ # Replace `old_native` with `fresh`, preserving position. Both
767
+ # neighbor pointers are set explicitly to match `replace`'s
768
+ # semantics (see #replace above).
769
+ def swap_native_in_place(old_native, fresh)
770
+ parent = old_native.parent
771
+ return if parent.nil?
741
772
 
742
- content.gsub("&quot;", '"')
743
- .gsub("&apos;", "'")
744
- .gsub("&lt;", "<")
745
- .gsub("&gt;", ">")
746
- .gsub("&amp;", "&")
773
+ prev_sibling = old_native.prev
774
+ next_sibling = old_native.next
775
+ old_native.remove!
776
+ prev_sibling.next = fresh if prev_sibling
777
+ next_sibling.prev = fresh if next_sibling
778
+ parent << fresh unless prev_sibling || next_sibling
779
+ end
780
+ private :swap_native_in_place
781
+
782
+ # libxml-ruby's node.content= pre-escapes; building a fresh node via
783
+ # new_<kind> stores content verbatim. Swap in place and re-point the
784
+ # wrapper.
785
+ def replace_native_verbatim(node, fresh)
786
+ native = unpatch_node(node)
787
+ return unless native.is_a?(::LibXML::XML::Node)
788
+
789
+ swap_native_in_place(native, fresh)
790
+ node.replace_native!(fresh) if node.is_a?(CustomizedLibxml::Node)
791
+ end
792
+ private :replace_native_verbatim
793
+
794
+ def cdata_content(node)
795
+ # libxml stores CDATA payload verbatim; no decoding needed.
796
+ unpatch_node(node)&.content
747
797
  end
748
798
 
749
799
  def set_cdata_content(node, content)
750
- native_node = unpatch_node(node)
751
- # CDATA content should NOT be escaped
752
- native_node.content = content.to_s if native_node
800
+ replace_native_verbatim(node, ::LibXML::XML::Node.new_cdata(content.to_s))
753
801
  end
754
802
 
755
803
  def comment_content(node)
@@ -758,8 +806,7 @@ module Moxml
758
806
  end
759
807
 
760
808
  def set_comment_content(node, content)
761
- native_node = unpatch_node(node)
762
- native_node.content = content.to_s if native_node
809
+ replace_native_verbatim(node, ::LibXML::XML::Node.new_comment(content.to_s))
763
810
  end
764
811
 
765
812
  def processing_instruction_target(node)
@@ -768,22 +815,18 @@ module Moxml
768
815
  end
769
816
 
770
817
  def processing_instruction_content(node)
771
- native_node = unpatch_node(node)
772
- content = native_node&.content
773
- # LibXML may HTML-escape the content, un-escape it
774
- return nil unless content
775
-
776
- content.gsub("&quot;", '"')
777
- .gsub("&apos;", "'")
778
- .gsub("&lt;", "<")
779
- .gsub("&gt;", ">")
780
- .gsub("&amp;", "&")
818
+ # XML 1.0 §2.6: PI content is verbatim — no entity resolution.
819
+ unpatch_node(node)&.content
781
820
  end
782
821
 
783
822
  def set_processing_instruction_content(node, content)
784
823
  native_node = unpatch_node(node)
785
- # Store raw content - LibXML will escape it
786
- native_node.content = content.to_s if native_node
824
+ return unless native_node.is_a?(::LibXML::XML::Node)
825
+
826
+ replace_native_verbatim(
827
+ node,
828
+ ::LibXML::XML::Node.new_pi(native_node.name, content.to_s),
829
+ )
787
830
  end
788
831
 
789
832
  def create_native_namespace(element, prefix, uri)
@@ -804,9 +847,10 @@ module Moxml
804
847
 
805
848
  def set_namespace(element, ns)
806
849
  native_elem = unpatch_node(element)
807
- return unless native_elem && ns
850
+ return element unless native_elem
808
851
 
809
- native_elem.namespaces.namespace = ns
852
+ native_elem.namespaces.namespace = ns if ns
853
+ element
810
854
  end
811
855
 
812
856
  def namespace(element)
@@ -867,14 +911,18 @@ module Moxml
867
911
  # LibXML requires ALL prefixes in the XPath to be registered
868
912
  ns_context = build_xpath_namespaces(native_node, namespaces)
869
913
 
870
- results = if ns_context.empty?
871
- native_node.find(expression).to_a
872
- else
873
- native_node.find(expression, ns_context).to_a
874
- end
914
+ found = if ns_context.empty?
915
+ native_node.find(expression)
916
+ else
917
+ native_node.find(expression, ns_context)
918
+ end
919
+
920
+ # find returns an XPath::Object for node results, scalars for
921
+ # count()/string()/boolean functions (adapter contract)
922
+ return found if found.is_a?(Float) || found.is_a?(String) ||
923
+ [true, false].include?(found)
875
924
 
876
- # Wrap results
877
- results.map { |n| patch_node(n) }
925
+ found.to_a.map { |n| patch_node(n) }
878
926
  rescue ::LibXML::XML::Error => e
879
927
  raise Moxml::XPathError.new(
880
928
  e.message,
@@ -1109,9 +1157,9 @@ module Moxml
1109
1157
 
1110
1158
  seen_ns[prefix] = true
1111
1159
  output << if prefix.nil? || prefix.empty?
1112
- " xmlns=\"#{escape_xml(uri)}\""
1160
+ " xmlns=\"#{XmlEmitter.escape_attribute(uri)}\""
1113
1161
  else
1114
- " xmlns:#{prefix}=\"#{escape_xml(uri)}\""
1162
+ " xmlns:#{prefix}=\"#{XmlEmitter.escape_attribute(uri)}\""
1115
1163
  end
1116
1164
  end
1117
1165
  end
@@ -1127,7 +1175,7 @@ module Moxml
1127
1175
  else
1128
1176
  attr.name
1129
1177
  end
1130
- output << " #{attr_name}=\"#{escape_xml(attr.value)}\""
1178
+ output << " #{attr_name}=\"#{XmlEmitter.escape_attribute(attr.value)}\""
1131
1179
  end
1132
1180
  end
1133
1181
 
@@ -1167,7 +1215,7 @@ module Moxml
1167
1215
  when ::LibXML::XML::Node::ELEMENT_NODE
1168
1216
  serialize_element(node)
1169
1217
  when ::LibXML::XML::Node::TEXT_NODE
1170
- escape_text(node.content)
1218
+ XmlEmitter.escape_text(node.content)
1171
1219
  when ::LibXML::XML::Node::CDATA_SECTION_NODE
1172
1220
  "<![CDATA[#{node.content}]]>"
1173
1221
  when ::LibXML::XML::Node::COMMENT_NODE
@@ -1179,28 +1227,6 @@ module Moxml
1179
1227
  end
1180
1228
  end
1181
1229
 
1182
- def escape_text(text)
1183
- text.to_s
1184
- .gsub("&", "&amp;")
1185
- .gsub("<", "&lt;")
1186
- .gsub(">", "&gt;")
1187
- end
1188
-
1189
- ESCAPE_XML_RE = /[&<>"]/
1190
- ESCAPE_XML_MAP = { "&" => "&amp;", "<" => "&lt;", ">" => "&gt;",
1191
- '"' => "&quot;" }.freeze
1192
- private_constant :ESCAPE_XML_RE, :ESCAPE_XML_MAP
1193
-
1194
- def escape_xml(text)
1195
- # One gsub pass with a Hash replacement allocates a single new
1196
- # string. The previous chained gsubs allocated three throwaway
1197
- # strings on every call (very hot for attribute-heavy XML).
1198
- str = text.is_a?(String) ? text : text.to_s
1199
- return str unless str.match?(ESCAPE_XML_RE)
1200
-
1201
- str.gsub(ESCAPE_XML_RE, ESCAPE_XML_MAP)
1202
- end
1203
-
1204
1230
  def import_and_add(doc, element, child)
1205
1231
  return unless element && child
1206
1232
 
@@ -1357,9 +1383,9 @@ module Moxml
1357
1383
 
1358
1384
  def format_ns_declaration(prefix, uri)
1359
1385
  if prefix.nil? || prefix.empty?
1360
- " xmlns=\"#{escape_xml(uri)}\""
1386
+ " xmlns=\"#{XmlEmitter.escape_attribute(uri)}\""
1361
1387
  else
1362
- " xmlns:#{prefix}=\"#{escape_xml(uri)}\""
1388
+ " xmlns:#{prefix}=\"#{XmlEmitter.escape_attribute(uri)}\""
1363
1389
  end
1364
1390
  end
1365
1391
 
@@ -1370,7 +1396,7 @@ module Moxml
1370
1396
  next if attr.name.start_with?("xmlns")
1371
1397
 
1372
1398
  attr_name = attr.ns&.prefix ? "#{attr.ns.prefix}:#{attr.name}" : attr.name
1373
- output << " #{attr_name}=\"#{escape_xml(attr.value)}\""
1399
+ output << " #{attr_name}=\"#{XmlEmitter.escape_attribute(attr.value)}\""
1374
1400
  end
1375
1401
  end
1376
1402
 
@@ -1510,11 +1536,6 @@ module Moxml
1510
1536
  end
1511
1537
  end
1512
1538
 
1513
- def remove_indentation(xml_string)
1514
- # Remove all newlines and extra spaces between tags
1515
- xml_string.gsub(/>\s+</, "><").gsub(/\n\s*/, "")
1516
- end
1517
-
1518
1539
  def collect_namespace_definitions(node)
1519
1540
  ns_defs = {}
1520
1541
 
@@ -1693,13 +1714,24 @@ module Moxml
1693
1714
  @handler.on_end_document
1694
1715
  end
1695
1716
 
1696
- def on_start_element(name, attributes)
1697
- attr_hash, ns_hash = split_attributes_and_namespaces(attributes)
1698
- @handler.on_start_element(name.to_s, attr_hash, ns_hash)
1717
+ # libxml strips prefixes and namespace declarations from the
1718
+ # plain on_start_element callback; the _ns variants carry
1719
+ # them. The namespaces hash uses nil for the default
1720
+ # declaration — the same key convention as every other
1721
+ # bridge's split. Attribute name prefixes are lost by the
1722
+ # binding in both callbacks (documented limitation).
1723
+ def on_start_element_ns(name, attributes, prefix, _uri, namespaces)
1724
+ normalized = (attributes || {}).map { |k, v| [k.to_s, v] }
1725
+ attr_hash, = split_attributes_and_namespaces(normalized) do |v|
1726
+ Moxml::Adapter::Base.decode_entities(v)
1727
+ end
1728
+ qualified = prefix ? "#{prefix}:#{name}" : name.to_s
1729
+ @handler.on_start_element(qualified, attr_hash, namespaces || {})
1699
1730
  end
1700
1731
 
1701
- def on_end_element(name)
1702
- @handler.on_end_element(name.to_s)
1732
+ def on_end_element_ns(name, prefix, _uri)
1733
+ qualified = prefix ? "#{prefix}:#{name}" : name.to_s
1734
+ @handler.on_end_element(qualified)
1703
1735
  end
1704
1736
 
1705
1737
  def on_characters(chars)
@@ -144,6 +144,7 @@ module Moxml
144
144
 
145
145
  def set_namespace(element, ns)
146
146
  element.namespace = ns
147
+ element
147
148
  end
148
149
 
149
150
  def namespace(element)
@@ -216,21 +217,36 @@ module Moxml
216
217
  document.is_a?(::Nokogiri::XML::Document) ? document.root : document.children.first
217
218
  end
218
219
 
219
- def attribute_element(attr)
220
- attr.parent
220
+ def line_number(node)
221
+ node.line
221
222
  end
222
223
 
223
- def attributes(element)
224
- element.attributes.values
224
+ def attribute_element(attr)
225
+ attr.parent
225
226
  end
226
227
 
227
228
  def set_attribute(element, name, value)
228
229
  element[name.to_s] = value.to_s
229
230
  end
230
231
 
232
+ def attributes(element)
233
+ # attribute_nodes, not attributes.values: the attributes Hash
234
+ # is keyed by local name, so xmi:type and type collide and
235
+ # one is silently dropped (issue #94).
236
+ element.attribute_nodes
237
+ end
238
+
231
239
  def get_attribute(element, name)
232
- # attributes keys don't include attribute namespaces
233
- element.attributes[name.to_s]
240
+ name_s = name.to_s
241
+ if name_s.include?(":")
242
+ prefix, local = name_s.split(":", 2)
243
+ href = element.namespace_definitions
244
+ .find { |ns| ns.prefix == prefix }&.href
245
+ element.attribute_with_ns(local, href)
246
+ else
247
+ element.attribute_with_ns(name_s, nil) ||
248
+ element.attributes[name_s]
249
+ end
234
250
  end
235
251
 
236
252
  def get_attribute_value(element, name)
@@ -360,7 +376,10 @@ module Moxml
360
376
  end
361
377
 
362
378
  def xpath(node, expression, namespaces = nil)
363
- node.xpath(expression, namespaces).to_a
379
+ result = node.xpath(expression, namespaces)
380
+ # Adapter contract: Array<native> | scalar (count(),
381
+ # string-length(), boolean functions return scalars).
382
+ result.is_a?(Enumerable) ? result.to_a : result
364
383
  rescue ::Nokogiri::XML::XPath::SyntaxError => e
365
384
  raise Moxml::XPathError.new(
366
385
  e.message,
@@ -471,7 +490,9 @@ module Moxml
471
490
  end
472
491
 
473
492
  def start_element(name, attributes = [])
474
- attr_hash, ns_hash = split_attributes_and_namespaces(attributes)
493
+ attr_hash, ns_hash = split_attributes_and_namespaces(attributes) do |v|
494
+ Moxml::Adapter::Base.decode_entities(v)
495
+ end
475
496
  @handler.on_start_element(name, attr_hash, ns_hash)
476
497
  end
477
498