moxml 0.5.8 → 0.5.10

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 77a959afbe83b0cc46a158571f792d07888f7bf29f3a65c1ea88be867892ffcc
4
- data.tar.gz: 1a5a9515ba125977cd26e33b878e7ecaaed1a788e450e79f356c54a117c9e6c7
3
+ metadata.gz: 0307f3c9e6060b1ca344c69633788790dc899121f213be74870be3cd2c2600ac
4
+ data.tar.gz: 22135905e79bcb1099a04809bedb69c734b1893ee4fa4749761427641bfcf9b2
5
5
  SHA512:
6
- metadata.gz: ff6e651e53a55b5ed4aed1e68eff9fa40e9a58aed81a3709ce101fb315bddbef928dcd1a8b31317fcf7dd42e65b92158191647365bc7c43fba46677551bd82d6
7
- data.tar.gz: 3c3ad8fac372d42a2dd9d3e4660bc30987015975031fed6b80c1bfeea864f47ed36676d125132384b72d7ba51a3a1ef743877efd5611fd0ecc2cda96efae29e7
6
+ metadata.gz: 50413c5cddd8680fc5c6cf4bd67dcf75e54e0cbf202b45f1ff5b985648ff948286b92c0a4efc9ef725d9fd0af677e0239eefb7fec37f27e43ece9795240a5db7
7
+ data.tar.gz: 696949b3fb51358b4841e2a2fdeb68824659a7ffefdbb6ac4a2006758006d649ddb64127728cd2b5b653cc54dd888e14566d012d5d6b6c382ce012f3b0ea0112
@@ -19,6 +19,11 @@ module Moxml
19
19
  # programmatic DOCTYPE, so those live in CustomizedLeptris value
20
20
  # objects stored through NativeAttachment.
21
21
  class Leptris < Base
22
+ # libleptris 1.9.7 (leptris-ruby 1.9.26): Document#node exposes
23
+ # the libxml2-model document node — prolog/epilog parts in
24
+ # document order. Older bindings keep the flat pre-root PI path.
25
+ DOC_NODE_SUPPORTED = ::Leptris::XML::Document.method_defined?(:node)
26
+
22
27
  class << self
23
28
  def attachments
24
29
  @attachments ||= Moxml::NativeAttachment.new
@@ -30,7 +35,9 @@ module Moxml
30
35
 
31
36
  def parse(xml, options = {}, _context = nil)
32
37
  xml_string = xml.is_a?(IO) || xml.is_a?(StringIO) ? xml.read : xml.to_s
33
- processed = preprocess_entities(xml_string)
38
+ # The marker flag rides preprocess's own `&` scan — no
39
+ # second full-buffer probe (issue #132 parse-side note).
40
+ processed, entity_markers = Entity.preprocess_with_marker_flag(xml_string)
34
41
 
35
42
  # readonly: true (issue #133): the binding memoizes reads and
36
43
  # refuses mutations — the parse-and-read lifecycle for
@@ -49,7 +56,7 @@ module Moxml
49
56
  doc = Document.new(native_doc, ctx)
50
57
 
51
58
  record_source_declaration(native_doc, processed)
52
- attachments.set(native_doc, :entity_markers, processed.include?(Entity::MARKER))
59
+ attachments.set(native_doc, :entity_markers, entity_markers)
53
60
 
54
61
  doc
55
62
  end
@@ -281,6 +288,12 @@ module Moxml
281
288
  qname: node.name,
282
289
  prefix: node.prefix,
283
290
  namespace_uri: ns&.href,
291
+ # Own declarations only — same shape as the generic
292
+ # path's Element#declared_namespaces (issue #138).
293
+ namespaces: begin
294
+ decls = node.namespace_definitions.map { |d| [d.prefix, d.href] }
295
+ decls.empty? ? Materializer::EMPTY_ATTRIBUTES : decls
296
+ end,
284
297
  attributes: attributes,
285
298
  text: nil,
286
299
  depth: material_depth(node, depth_memo),
@@ -293,6 +306,7 @@ module Moxml
293
306
  qname: nil,
294
307
  prefix: nil,
295
308
  namespace_uri: nil,
309
+ namespaces: Materializer::EMPTY_ATTRIBUTES,
296
310
  attributes: Materializer::EMPTY_ATTRIBUTES,
297
311
  text: text,
298
312
  depth: material_depth(node, depth_memo),
@@ -310,17 +324,21 @@ module Moxml
310
324
  end
311
325
 
312
326
  def node_type(node)
327
+ # Frequency-ordered: elements and text dominate every real
328
+ # document, and Node.wrap dispatches here once per cold
329
+ # wrap. CDATA must precede Text (CDATA < Text in the
330
+ # binding).
313
331
  case node
332
+ when ::Leptris::XML::Element then :element
333
+ when ::Leptris::XML::CDATA then :cdata
334
+ when ::Leptris::XML::Text, CustomizedLeptris::TextSegment then :text
335
+ when ::Leptris::XML::Attr then :attribute
336
+ when ::Leptris::XML::Comment then :comment
337
+ when ::Leptris::XML::ProcessingInstruction, CustomizedLeptris::DocumentPI then :processing_instruction
314
338
  when ::Leptris::XML::Document then :document
315
339
  when ::Leptris::XML::DocType, CustomizedLeptris::Doctype then :doctype
316
340
  when CustomizedLeptris::Declaration then :declaration
317
341
  when CustomizedLeptris::EntityReference then :entity_reference
318
- when ::Leptris::XML::CDATA then :cdata
319
- when ::Leptris::XML::Comment then :comment
320
- when ::Leptris::XML::ProcessingInstruction, CustomizedLeptris::DocumentPI then :processing_instruction
321
- when ::Leptris::XML::Text, CustomizedLeptris::TextSegment then :text
322
- when ::Leptris::XML::Element then :element
323
- when ::Leptris::XML::Attr then :attribute
324
342
  else :unknown
325
343
  end
326
344
  end
@@ -965,9 +983,22 @@ module Moxml
965
983
  native = native_doctype_xml(doc)
966
984
  parts << native << "\n" if native
967
985
 
968
- document_pi_nodes(doc).each { |pi| parts << pi.to_xml << "\n" }
986
+ if DOC_NODE_SUPPORTED
987
+ # The libxml2-model document node: prolog PIs/comments,
988
+ # the root, epilog PIs/comments — in document order, so
989
+ # epilog parts serialize after the root (issue #130).
990
+ doc_children = document_node_children(doc)
991
+ if doc_children
992
+ doc_children.each { |child| parts << raw_serialize(child, options) << "\n" }
993
+ else
994
+ document_pi_nodes(doc).each { |pi| parts << pi.to_xml << "\n" }
995
+ parts << raw_serialize(doc.root, options) << "\n" if doc.root
996
+ end
997
+ else
998
+ document_pi_nodes(doc).each { |pi| parts << pi.to_xml << "\n" }
969
999
 
970
- parts << raw_serialize(doc.root, options) << "\n" if doc.root
1000
+ parts << raw_serialize(doc.root, options) << "\n" if doc.root
1001
+ end
971
1002
 
972
1003
  texts = attachments.get(doc, :document_text)
973
1004
  texts&.each { |text| parts << XmlEmitter.escape_text(text.content.to_s) }
@@ -1040,6 +1071,20 @@ module Moxml
1040
1071
  # the same objects, so wrapper mutations round-trip. Build the
1041
1072
  # cache BEFORE appending a PI with add_pi, or the C-side
1042
1073
  # addition would be double-counted.
1074
+ # The document node's children, or nil when the node does not
1075
+ # reflect reality: parsed documents always list the root
1076
+ # element among their children, but programmatically built
1077
+ # ones do not (binding gap) — those keep the legacy parts
1078
+ # path.
1079
+ def document_node_children(doc)
1080
+ doc_children = doc.children.to_a
1081
+ has_root = doc_children.any?(::Leptris::XML::Element)
1082
+ return doc_children if has_root
1083
+ return doc_children if doc.root.nil?
1084
+
1085
+ nil
1086
+ end
1087
+
1043
1088
  def document_pi_nodes(doc)
1044
1089
  attachments.get(doc, :doc_pi_nodes) || begin
1045
1090
  nodes = doc.processing_instructions.map do |(target, data)|
@@ -1059,16 +1104,29 @@ module Moxml
1059
1104
  doctype_wrapper = attachments.get(doc, :doctype)
1060
1105
  children << doctype_wrapper if doctype_wrapper
1061
1106
 
1062
- # Document-level PIs live outside the element tree in
1063
- # libleptris (a flat list; its serializer emits them before
1064
- # the root). Listed after the DOCTYPE to match the order
1065
- # serialize_document emits, so children and serialization
1066
- # agree. Epilog anchoring is not representable in the C
1067
- # model — a known divergence from the Nokogiri-shaped
1068
- # contract the other adapters expose.
1069
- children.concat(document_pi_nodes(doc))
1107
+ if DOC_NODE_SUPPORTED
1108
+ # The libxml2-model document node lists prolog PIs/comments,
1109
+ # the root, and epilog PIs/comments in document order —
1110
+ # the Nokogiri-shaped contract, epilog anchoring included
1111
+ # (issue #130). Built (programmatic) documents are not yet
1112
+ # fully reflected by the node (binding gap: an attached
1113
+ # root does not appear); document_node_children answers
1114
+ # nil there for the legacy parts path.
1115
+ doc_children = document_node_children(doc)
1116
+ if doc_children
1117
+ children.concat(doc_children)
1118
+ else
1119
+ children.concat(document_pi_nodes(doc))
1120
+ children << doc.root if doc.root
1121
+ end
1122
+ else
1123
+ # Legacy path: document-level PIs live outside the element
1124
+ # tree in a flat pre-root list (libleptris < 1.9.7 C
1125
+ # model); epilog anchoring is not representable there.
1126
+ children.concat(document_pi_nodes(doc))
1070
1127
 
1071
- children << doc.root if doc.root
1128
+ children << doc.root if doc.root
1129
+ end
1072
1130
 
1073
1131
  texts = attachments.get(doc, :document_text)
1074
1132
  children.concat(texts) if texts
data/lib/moxml/element.rb CHANGED
@@ -119,6 +119,16 @@ module Moxml
119
119
  end
120
120
  alias namespace_definitions namespaces
121
121
 
122
+ # The element's OWN namespace declarations as [prefix, uri]
123
+ # pairs (nil prefix = default namespace) — not the inherited
124
+ # scope. The shape materialize records carry (issue #138).
125
+ def declared_namespaces
126
+ adapter.namespace_definitions(@native).map do |ns|
127
+ wrapper = Namespace.new(ns, context)
128
+ [wrapper.prefix, wrapper.uri]
129
+ end
130
+ end
131
+
122
132
  # Returns all namespaces in scope for this element,
123
133
  # including those inherited from ancestor elements.
124
134
  def in_scope_namespaces
data/lib/moxml/entity.rb CHANGED
@@ -55,9 +55,12 @@ module Moxml
55
55
  module_function
56
56
 
57
57
  # Replace non-standard entity references with markers before
58
- # parsing. Always returns a UTF-8 encoded string.
59
- def preprocess_entities(xml)
60
- return "" if xml.nil?
58
+ # parsing. Always returns a UTF-8 encoded string, and reports
59
+ # whether the result can contain markers: the flag rides the
60
+ # same `&` scan, so callers needing it (leptris parse) avoid a
61
+ # second full-buffer multibyte probe.
62
+ def preprocess_with_marker_flag(xml)
63
+ return ["", false] if xml.nil?
61
64
 
62
65
  str = if xml.encoding == Encoding::BINARY
63
66
  # Binary strings are assumed to be UTF-8. If the bytes are
@@ -78,11 +81,23 @@ module Moxml
78
81
  # Fast path: no `&` means no entity references to mark — skip
79
82
  # the regex scan and string allocation entirely. The vast
80
83
  # majority of XML payloads contain no entity references.
81
- return str unless str.include?("&")
84
+ return [str, false] unless str.include?("&")
82
85
 
83
- str.gsub(NAME_RE) do |match|
84
- STANDARD_ENTITIES.include?(::Regexp.last_match(1)) ? match : "#{MARKER}#{::Regexp.last_match(1)};"
86
+ marked = false
87
+ processed = str.gsub(NAME_RE) do |match|
88
+ name = ::Regexp.last_match(1)
89
+ if STANDARD_ENTITIES.include?(name)
90
+ match
91
+ else
92
+ marked = true
93
+ "#{MARKER}#{name};"
94
+ end
85
95
  end
96
+ [processed, marked]
97
+ end
98
+
99
+ def preprocess_entities(xml)
100
+ preprocess_with_marker_flag(xml)[0]
86
101
  end
87
102
 
88
103
  # Resolve numeric (&#NN; / &#xNN;) and the five standard named
@@ -69,6 +69,10 @@ module Moxml
69
69
  qname: element.name,
70
70
  prefix: element.namespace_prefix,
71
71
  namespace_uri: ns&.uri,
72
+ # The element's OWN namespace declarations ([prefix, uri]
73
+ # pairs; nil prefix = default), not the in-scope set — enough
74
+ # for a consumer to rebuild scope while walking (issue #138).
75
+ namespaces: element.declared_namespaces,
72
76
  attributes: attributes,
73
77
  text: nil,
74
78
  depth: depth,
@@ -81,6 +85,7 @@ module Moxml
81
85
  qname: nil,
82
86
  prefix: nil,
83
87
  namespace_uri: nil,
88
+ namespaces: EMPTY_ATTRIBUTES,
84
89
  attributes: EMPTY_ATTRIBUTES,
85
90
  text: text,
86
91
  depth: depth,
data/lib/moxml/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Moxml
4
- VERSION = "0.5.8"
4
+ VERSION = "0.5.10"
5
5
  end
@@ -85,37 +85,51 @@ RSpec.describe Moxml::Adapter::Leptris do
85
85
  let(:ctx) { Moxml.new(:leptris) }
86
86
 
87
87
  it "lists document PIs as children with the root" do
88
- doc = ctx.parse('<?xml version="1.0"?><?pi-prolog before?><root/><?pi-epilog after?>')
88
+ doc = ctx.parse('<?xml version="1.0"?><?pi-prolog before?><root/><?pi-epilog after?><!-- tail -->')
89
89
  kids = doc.children.to_a
90
90
 
91
91
  expect(kids.map(&:class)).to eq(
92
- [Moxml::ProcessingInstruction, Moxml::ProcessingInstruction, Moxml::Element],
92
+ [Moxml::ProcessingInstruction, Moxml::Element,
93
+ Moxml::ProcessingInstruction, Moxml::Comment],
93
94
  )
94
95
  expect(kids.select(&:processing_instruction?).map(&:target)).to eq(%w[pi-prolog pi-epilog])
95
96
  expect(kids[0].content).to eq("before")
97
+ expect(doc.to_xml.index("pi-epilog")).to be > doc.to_xml.index("</root>")
98
+ expect(doc.to_xml.index("<!-- tail -->")).to be > doc.to_xml.index("</root>")
96
99
  end
97
100
 
98
- it "round-trips mutations and additions through serialization" do
99
- doc = ctx.parse("<?pi-original x?><root/>")
100
- pi = doc.children.to_a[0]
101
+ it "round-trips tree-level PI mutations through serialization" do
102
+ doc = ctx.parse("<root><a><?pi-original x?></a></root>")
103
+ pi = doc.at_xpath("//a").children.to_a.find(&:processing_instruction?)
101
104
  pi.target = "renamed"
102
105
  pi.content = "changed"
103
106
  expect(doc.to_xml).to include("<?renamed changed?>")
107
+ end
104
108
 
109
+ it "adds document PIs and lists them as children" do
110
+ doc = ctx.parse("<root/>")
105
111
  doc.add_child(doc.create_processing_instruction("added", "now"))
106
112
  expect(doc.to_xml).to include("<?added now?>")
107
- expect(doc.children.to_a.select(&:processing_instruction?).map(&:target)).to eq(%w[renamed added])
113
+ expect(doc.children.to_a.select(&:processing_instruction?).map(&:target)).to eq(%w[added])
114
+ end
115
+
116
+ it "reports document-level PI mutation as unsupported on the native path" do
117
+ # libleptris 1.9.7 exposes document PIs read-only: target=/data=
118
+ # and unlink are rejected for nodes outside the element tree.
119
+ doc = ctx.parse("<?pi x?><root/>")
120
+ pi = doc.children.to_a[0]
121
+
122
+ expect { pi.target = "renamed" }.to raise_error(Leptris::XML::Error)
108
123
  end
109
124
 
110
125
  it "serializes children and document output in agreement" do
111
126
  # libleptris stores document PIs as one flat pre-root list (no
112
127
  # epilog anchoring); children and to_xml must at least agree.
113
128
  doc = ctx.parse('<?xml version="1.0"?><?pi-a 1?><root/><?pi-b 2?>')
114
- from_children = doc.children.to_a.select(&:processing_instruction?)
115
- .map(&:to_xml).join("\n")
129
+ parts = doc.children.to_a.map { |c| "#{c.to_xml}\n" }.join
116
130
  from_document = doc.to_xml.sub(%r{\A<\?xml[^>]*\?>\n}, "")
117
131
 
118
- expect(from_document).to start_with(from_children)
132
+ expect(from_document).to eq(parts)
119
133
  end
120
134
 
121
135
  it "matches raw Nokogiri byte-for-byte for pretty-printing (issue #129)" do
@@ -36,6 +36,10 @@ RSpec.describe Moxml::Materializer do
36
36
  expect(book[:namespace_uri]).to eq("urn:c")
37
37
  expect(book[:text]).to be_nil
38
38
 
39
+ catalog = records.last
40
+ expect(catalog[:namespaces]).to eq([[nil, "urn:c"], ["p", "urn:p"]])
41
+ expect(book[:namespaces]).to eq([])
42
+
39
43
  title_text = records.find { |r| r[:kind] == :text && r[:text] == "T" }
40
44
  expect(title_text[:attributes]).to eq([])
41
45
  end
@@ -77,6 +81,6 @@ RSpec.describe Moxml::Materializer do
77
81
 
78
82
  # Records themselves allocate (hash + FFI strings); the wrapper
79
83
  # tree does not: far fewer allocations than 2 wrappers per node.
80
- expect(allocated).to be < node_count * 20
84
+ expect(allocated).to be < node_count * 24
81
85
  end
82
86
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: moxml
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.8
4
+ version: 0.5.10
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-08-27 00:00:00.000000000 Z
11
+ date: 2026-08-28 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: |
14
14
  Moxml is a unified XML manipulation library that provides a common API