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 +4 -4
- data/lib/moxml/adapter/leptris.rb +77 -19
- data/lib/moxml/element.rb +10 -0
- data/lib/moxml/entity.rb +21 -6
- data/lib/moxml/materializer.rb +5 -0
- data/lib/moxml/version.rb +1 -1
- data/spec/moxml/adapter/leptris_spec.rb +23 -9
- data/spec/moxml/materializer_spec.rb +5 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0307f3c9e6060b1ca344c69633788790dc899121f213be74870be3cd2c2600ac
|
|
4
|
+
data.tar.gz: 22135905e79bcb1099a04809bedb69c734b1893ee4fa4749761427641bfcf9b2
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
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
|
-
|
|
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
|
-
|
|
60
|
-
|
|
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
|
-
|
|
84
|
-
|
|
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
|
data/lib/moxml/materializer.rb
CHANGED
|
@@ -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
|
@@ -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::
|
|
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
|
|
99
|
-
doc = ctx.parse("
|
|
100
|
-
pi = doc.children.to_a
|
|
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[
|
|
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
|
-
|
|
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
|
|
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 *
|
|
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.
|
|
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-
|
|
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
|