moxml 0.5.12 → 0.5.14
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/.rubocop.yml +9 -0
- data/docs/_pages/adapter-protocol.adoc +117 -0
- data/docs/_pages/adapters/index.adoc +11 -5
- data/docs/_pages/adapters/leptris.adoc +74 -0
- data/docs/_pages/adapters/libxml.adoc +1 -1
- data/docs/_pages/adapters/nokogiri.adoc +1 -1
- data/docs/_pages/conversion-apis.adoc +103 -0
- data/docs/_pages/index.adoc +6 -0
- data/lib/compat/opal/moxml_boot.rb +1 -0
- data/lib/moxml/adapter/base.rb +18 -4
- data/lib/moxml/adapter/leptris/document_parts.rb +208 -0
- data/lib/moxml/adapter/leptris/markers.rb +51 -0
- data/lib/moxml/adapter/leptris/materialize.rb +140 -0
- data/lib/moxml/adapter/leptris/sax_bridge.rb +56 -0
- data/lib/moxml/adapter/leptris/serialize.rb +135 -0
- data/lib/moxml/adapter/leptris.rb +58 -523
- data/lib/moxml/adapter/libxml/serialize.rb +497 -0
- data/lib/moxml/adapter/libxml.rb +20 -462
- data/lib/moxml/adapter/nokogiri.rb +7 -0
- data/lib/moxml/context.rb +8 -0
- data/lib/moxml/document.rb +18 -0
- data/lib/moxml/entity_registry.rb +0 -1
- data/lib/moxml/materializer.rb +108 -52
- data/lib/moxml/node.rb +8 -0
- data/lib/moxml/version.rb +1 -1
- data/lib/moxml.rb +3 -0
- data/spec/integration/shared_examples/node_wrappers/declaration_behavior.rb +3 -4
- data/spec/integration/shared_examples/node_wrappers/document_behavior.rb +4 -0
- data/spec/moxml/adapter/leptris_spec.rb +54 -11
- data/spec/moxml/adapter/nokogiri_spec.rb +8 -0
- data/spec/moxml/builder_spec.rb +8 -1
- data/spec/moxml/materializer_spec.rb +51 -0
- data/spec/moxml/signature/algorithms_spec.rb +1 -1
- data/spec/moxml/xpath/conversion_spec.rb +2 -6
- metadata +11 -2
data/lib/moxml/materializer.rb
CHANGED
|
@@ -7,89 +7,145 @@ module Moxml
|
|
|
7
7
|
# consumer tree needs without allocating Moxml::Node or
|
|
8
8
|
# Moxml::Attribute wrappers.
|
|
9
9
|
#
|
|
10
|
-
#
|
|
11
|
-
# document.materialize.each { |r| ... } # Enumerator
|
|
10
|
+
# Two emission forms share one walk (issue #143):
|
|
12
11
|
#
|
|
13
|
-
#
|
|
14
|
-
#
|
|
15
|
-
#
|
|
16
|
-
#
|
|
17
|
-
#
|
|
18
|
-
#
|
|
12
|
+
# document.materialize { |record| ... } # Hash snapshot per node
|
|
13
|
+
# document.materialize.to_a # Enumerator of snapshots
|
|
14
|
+
# document.materialize_fields { |kind, qname, prefix, namespace_uri,
|
|
15
|
+
# namespaces, attributes, text, depth| }
|
|
16
|
+
#
|
|
17
|
+
# materialize_fields is the conversion hot path: the two list
|
|
18
|
+
# arguments are flat reused buffers — attributes stride 4 (name,
|
|
19
|
+
# value, namespace_uri, prefix), namespaces stride 2 (prefix, uri;
|
|
20
|
+
# nil prefix = default) — valid only inside the block. Copy anything
|
|
21
|
+
# you intend to keep, or use materialize, whose records are
|
|
22
|
+
# independent Hash snapshots.
|
|
23
|
+
#
|
|
24
|
+
# Record shape (both forms):
|
|
25
|
+
# kind: :element|:text|:cdata|:comment|:processing_instruction,
|
|
26
|
+
# qname: "tag"|"pi-target"|nil, prefix: "p"|nil,
|
|
27
|
+
# namespace_uri: "urn:x"|nil,
|
|
28
|
+
# namespaces: [[prefix, uri], ...], # element's OWN declarations
|
|
29
|
+
# attributes: [[name, value, namespace_uri, prefix], ...],
|
|
30
|
+
# text: String|nil, depth: Integer
|
|
19
31
|
#
|
|
20
32
|
# Adapters with a bulk path (leptris: one leptris_node_traverse FFI
|
|
21
|
-
# call for the whole subtree
|
|
22
|
-
# walk the wrapper
|
|
33
|
+
# call for the whole subtree, reading properties straight off the C
|
|
34
|
+
# handles) answer bulk_materialize?; the others walk the wrapper
|
|
35
|
+
# tree generically. Both emit identical streams — spec-pinned.
|
|
23
36
|
module Materializer
|
|
24
|
-
# Shared frozen empty
|
|
25
|
-
#
|
|
37
|
+
# Shared frozen empty list for records with no list fields;
|
|
38
|
+
# adapter bulk paths yield it too.
|
|
26
39
|
EMPTY_ATTRIBUTES = [].freeze
|
|
27
40
|
|
|
41
|
+
# The two flat, reused field buffers a materialize_fields block
|
|
42
|
+
# receives (issue #143). One instance streams an entire subtree;
|
|
43
|
+
# both arrays are cleared and refilled per element record.
|
|
44
|
+
class Buffers
|
|
45
|
+
attr_reader :attributes, :namespaces
|
|
46
|
+
|
|
47
|
+
def initialize
|
|
48
|
+
@attributes = []
|
|
49
|
+
@namespaces = []
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Builds the Hash snapshot form from the field stream — the one
|
|
54
|
+
# place the eight-key record shape is materialized.
|
|
55
|
+
module Record
|
|
56
|
+
module_function
|
|
57
|
+
|
|
58
|
+
def from_fields(kind, qname, prefix, namespace_uri, namespaces,
|
|
59
|
+
attributes, text, depth)
|
|
60
|
+
{
|
|
61
|
+
kind: kind,
|
|
62
|
+
qname: qname,
|
|
63
|
+
prefix: prefix,
|
|
64
|
+
namespace_uri: namespace_uri,
|
|
65
|
+
namespaces: group_flat(namespaces, 2),
|
|
66
|
+
attributes: group_flat(attributes, 4),
|
|
67
|
+
text: text,
|
|
68
|
+
depth: depth,
|
|
69
|
+
}
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def group_flat(flat, stride)
|
|
73
|
+
return EMPTY_ATTRIBUTES if flat.empty?
|
|
74
|
+
|
|
75
|
+
flat.each_slice(stride).to_a
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
28
79
|
module_function
|
|
29
80
|
|
|
81
|
+
# Hash-snapshot form: every record is an independent Hash.
|
|
30
82
|
def materialize(node, &block)
|
|
31
|
-
adapter = node.context.config.adapter
|
|
32
83
|
return enum_for(:materialize, node) unless block
|
|
33
84
|
|
|
34
|
-
|
|
85
|
+
materialize_fields(node) do |kind, qname, prefix, namespace_uri, namespaces, attributes, text, depth|
|
|
86
|
+
yield(Record.from_fields(kind, qname, prefix, namespace_uri,
|
|
87
|
+
namespaces, attributes, text, depth))
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Zero-allocation streaming form — see the module docs. Requires
|
|
92
|
+
# a block: the reused buffers are only valid inside it.
|
|
93
|
+
def materialize_fields(node, buffers = Buffers.new, &block)
|
|
94
|
+
raise ArgumentError, "materialize_fields requires a block" unless block
|
|
95
|
+
|
|
96
|
+
adapter = node.context.config.adapter
|
|
97
|
+
if adapter.bulk_materialize? &&
|
|
98
|
+
adapter.materialize_fields(node.native, buffers, &block)
|
|
35
99
|
return
|
|
36
100
|
end
|
|
37
101
|
|
|
38
|
-
|
|
102
|
+
walk_fields(node, 0, buffers, &block)
|
|
39
103
|
end
|
|
40
104
|
|
|
41
105
|
# Generic post-order walk over the wrapper tree. Works on every
|
|
42
106
|
# adapter; the fast bulk path exists where the engine offers one.
|
|
43
|
-
def
|
|
107
|
+
def walk_fields(node, depth, buffers, &block)
|
|
44
108
|
case node
|
|
45
109
|
when Element
|
|
46
|
-
node.children.each { |child|
|
|
47
|
-
|
|
110
|
+
node.children.each { |child| walk_fields(child, depth + 1, buffers, &block) }
|
|
111
|
+
fill_element_buffers(node, buffers)
|
|
112
|
+
ns = node.namespace
|
|
113
|
+
yield(:element, node.name, node.namespace_prefix, ns&.uri,
|
|
114
|
+
buffers.namespaces, buffers.attributes, nil, depth)
|
|
48
115
|
when Text
|
|
49
|
-
yield(
|
|
116
|
+
yield(:text, nil, nil, nil, EMPTY_ATTRIBUTES, EMPTY_ATTRIBUTES,
|
|
117
|
+
node.content, depth)
|
|
50
118
|
when Cdata
|
|
51
|
-
yield(
|
|
119
|
+
yield(:cdata, nil, nil, nil, EMPTY_ATTRIBUTES, EMPTY_ATTRIBUTES,
|
|
120
|
+
node.content, depth)
|
|
52
121
|
when Comment
|
|
53
|
-
yield(
|
|
122
|
+
yield(:comment, nil, nil, nil, EMPTY_ATTRIBUTES, EMPTY_ATTRIBUTES,
|
|
123
|
+
node.content, depth)
|
|
54
124
|
when ProcessingInstruction
|
|
55
|
-
yield(
|
|
125
|
+
yield(:processing_instruction, node.target, nil, nil,
|
|
126
|
+
EMPTY_ATTRIBUTES, EMPTY_ATTRIBUTES, node.content, depth)
|
|
56
127
|
when EntityReference
|
|
57
|
-
yield(
|
|
128
|
+
yield(:entity_reference, node.name, nil, nil,
|
|
129
|
+
EMPTY_ATTRIBUTES, EMPTY_ATTRIBUTES, "&#{node.name};", depth)
|
|
58
130
|
end
|
|
59
131
|
end
|
|
60
132
|
|
|
61
|
-
def
|
|
62
|
-
|
|
133
|
+
def fill_element_buffers(element, buffers)
|
|
134
|
+
attrs = buffers.attributes
|
|
135
|
+
attrs.clear
|
|
136
|
+
element.attributes.each do |attr|
|
|
63
137
|
ns = attr.namespace
|
|
64
|
-
|
|
138
|
+
attrs << attr.name << attr.value << ns&.uri << ns&.prefix
|
|
65
139
|
end
|
|
66
|
-
ns = element.namespace
|
|
67
|
-
{
|
|
68
|
-
kind: :element,
|
|
69
|
-
qname: element.name,
|
|
70
|
-
prefix: element.namespace_prefix,
|
|
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,
|
|
76
|
-
attributes: attributes,
|
|
77
|
-
text: nil,
|
|
78
|
-
depth: depth,
|
|
79
|
-
}
|
|
80
|
-
end
|
|
81
140
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
text: text,
|
|
91
|
-
depth: depth,
|
|
92
|
-
}
|
|
141
|
+
ns_buf = buffers.namespaces
|
|
142
|
+
ns_buf.clear
|
|
143
|
+
# declared_namespaces: the element's OWN declarations ([prefix,
|
|
144
|
+
# uri] pairs; nil prefix = default), not the in-scope set —
|
|
145
|
+
# enough for a consumer to rebuild scope while walking (#138).
|
|
146
|
+
element.declared_namespaces.each do |prefix, uri|
|
|
147
|
+
ns_buf << prefix << uri
|
|
148
|
+
end
|
|
93
149
|
end
|
|
94
150
|
end
|
|
95
151
|
end
|
data/lib/moxml/node.rb
CHANGED
|
@@ -131,6 +131,14 @@ module Moxml
|
|
|
131
131
|
Materializer.materialize(self, &block)
|
|
132
132
|
end
|
|
133
133
|
|
|
134
|
+
# Zero-allocation streaming form — flat reused buffers valid only
|
|
135
|
+
# inside the block (issue #143). See Moxml::Materializer.
|
|
136
|
+
def materialize_fields(&block)
|
|
137
|
+
raise ArgumentError, "materialize_fields requires a block" unless block
|
|
138
|
+
|
|
139
|
+
Materializer.materialize_fields(self, &block)
|
|
140
|
+
end
|
|
141
|
+
|
|
134
142
|
def at_xpath(expression, namespaces = {})
|
|
135
143
|
Moxml::Node.wrap(adapter.at_xpath(@native, expression, namespaces),
|
|
136
144
|
context)
|
data/lib/moxml/version.rb
CHANGED
data/lib/moxml.rb
CHANGED
|
@@ -84,6 +84,9 @@ module Moxml
|
|
|
84
84
|
autoload :Builder, "moxml/builder"
|
|
85
85
|
autoload :Entity, "moxml/entity"
|
|
86
86
|
autoload :EntityRegistry, "moxml/entity_registry"
|
|
87
|
+
# Opal has no autoload; the compat boot file requires this eagerly
|
|
88
|
+
# alongside the registry.
|
|
89
|
+
autoload :EntityRegistryOpalData, "moxml/entity_registry_opal_data"
|
|
87
90
|
autoload :NativeAttachment, "moxml/native_attachment"
|
|
88
91
|
autoload :XmlUtils, "moxml/xml_utils"
|
|
89
92
|
autoload :XmlEmitter, "moxml/xml_emitter"
|
|
@@ -36,11 +36,10 @@ RSpec.shared_examples "Moxml::Declaration" do
|
|
|
36
36
|
end
|
|
37
37
|
|
|
38
38
|
it "normalizes encoding" do
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
end
|
|
39
|
+
# Encoding names are case-insensitive (XML 1.0 §4.3.3); some
|
|
40
|
+
# engine/REXML builds upcase the stored value.
|
|
42
41
|
declaration.encoding = "utf-8"
|
|
43
|
-
expect(declaration.encoding).to eq("utf-8")
|
|
42
|
+
expect(declaration.encoding.downcase).to eq("utf-8")
|
|
44
43
|
end
|
|
45
44
|
end
|
|
46
45
|
|
|
@@ -42,6 +42,10 @@ RSpec.shared_examples "Moxml::Document" do
|
|
|
42
42
|
context.config.strict_parsing = true
|
|
43
43
|
expect { context.parse("<invalid>") }.to raise_error(Moxml::ParseError)
|
|
44
44
|
end
|
|
45
|
+
|
|
46
|
+
it "answers no parse errors on clean documents" do
|
|
47
|
+
expect(doc.parse_errors).to eq([])
|
|
48
|
+
end
|
|
45
49
|
end
|
|
46
50
|
|
|
47
51
|
describe "node creation" do
|
|
@@ -113,13 +113,23 @@ RSpec.describe Moxml::Adapter::Leptris do
|
|
|
113
113
|
expect(doc.children.to_a.select(&:processing_instruction?).map(&:target)).to eq(%w[added])
|
|
114
114
|
end
|
|
115
115
|
|
|
116
|
-
it "
|
|
117
|
-
# libleptris 1.9.
|
|
118
|
-
#
|
|
116
|
+
it "keeps the document coherent across document-level PI mutation attempts" do
|
|
117
|
+
# Divergent builds: libleptris 1.9.8 (released 1.9.32 platform
|
|
118
|
+
# gems) accepts target= on parse-created document PIs; newer C
|
|
119
|
+
# builds raise the descriptive contract error (leptris-ruby#92).
|
|
120
|
+
# Pin the stable part — the document stays coherent either way.
|
|
119
121
|
doc = ctx.parse("<?pi x?><root/>")
|
|
120
122
|
pi = doc.children.to_a[0]
|
|
121
123
|
|
|
122
|
-
|
|
124
|
+
begin
|
|
125
|
+
pi.target = "renamed"
|
|
126
|
+
rescue Leptris::XML::Error
|
|
127
|
+
# rejected on this build
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
expect(doc.root.name).to eq("root")
|
|
131
|
+
expect(doc.children.to_a.first.processing_instruction?).to be(true)
|
|
132
|
+
expect(doc.to_xml).to match(/<root\s*\/?>|<root><\/root>/)
|
|
123
133
|
end
|
|
124
134
|
|
|
125
135
|
it "serializes children and document output in agreement" do
|
|
@@ -133,13 +143,27 @@ RSpec.describe Moxml::Adapter::Leptris do
|
|
|
133
143
|
end
|
|
134
144
|
|
|
135
145
|
it "matches raw Nokogiri byte-for-byte for pretty-printing (issue #129)" do
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
146
|
+
cases = {
|
|
147
|
+
minimal: %(<root><a/><b>x</b></root>),
|
|
148
|
+
namespaces: %(<root xmlns="urn:a" xmlns:p="urn:p"><p:child p:attr="v" plain="w"/><other>x & y</other></root>),
|
|
149
|
+
attributes: %(<r a="1" b="two <three>" c="apos 'here'"><e/></r>),
|
|
150
|
+
mixed: %(<r>text <b>bold</b> tail<!-- c --></r>),
|
|
151
|
+
cdata: %(<r><![CDATA[raw <stuff> & things]]></r>),
|
|
152
|
+
deep: %(<l1><l2><l3><l4><leaf/></l4></l3></l2></l1>),
|
|
153
|
+
unicode: %(<r name="Ünïcödé">日本語テキスト & more</r>),
|
|
154
|
+
unicode_nested: %(<r>a<b>日本</b>c</r>),
|
|
155
|
+
longtext: %(<r>#{'word ' * 30}</r>),
|
|
156
|
+
empty_root: %(<r/>),
|
|
157
|
+
selfclosing: %(<r><a/><b/><c>t</c><d/></r>),
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
cases.each do |name, source|
|
|
161
|
+
target = Nokogiri::XML(source).to_xml(indent: 2, encoding: "UTF-8")
|
|
162
|
+
output = ctx.parse(source).to_xml(
|
|
163
|
+
indent: 2, declaration: true, expand_empty: false, encoding: "UTF-8",
|
|
164
|
+
)
|
|
165
|
+
expect(output).to eq(target), "byte-parity failed for #{name}"
|
|
166
|
+
end
|
|
143
167
|
end
|
|
144
168
|
|
|
145
169
|
it "reports the tracked native for a re-added document PI" do
|
|
@@ -178,6 +202,25 @@ RSpec.describe Moxml::Adapter::Leptris do
|
|
|
178
202
|
end
|
|
179
203
|
end
|
|
180
204
|
|
|
205
|
+
describe "recover-path parse diagnostics" do
|
|
206
|
+
let(:ctx) { Moxml.new(:leptris) }
|
|
207
|
+
|
|
208
|
+
it "records the fatal error on non-strict parses" do
|
|
209
|
+
doc = ctx.parse("<root><unclosed>", strict: false)
|
|
210
|
+
expect(doc.root).to be_nil
|
|
211
|
+
expect(doc.parse_errors).not_to be_empty
|
|
212
|
+
expect(doc.parse_errors).to all(be_a(String))
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
it "answers [] on clean parses" do
|
|
216
|
+
expect(ctx.parse("<root/>").parse_errors).to eq([])
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
it "still raises on strict parses" do
|
|
220
|
+
expect { ctx.parse("<root><unclosed>") }.to raise_error(Moxml::ParseError)
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
|
|
181
224
|
describe "entity-marker tracking" do
|
|
182
225
|
let(:ctx) { Moxml.new(:leptris) }
|
|
183
226
|
|
|
@@ -11,4 +11,12 @@ RSpec.describe Moxml::Adapter::Nokogiri do
|
|
|
11
11
|
end
|
|
12
12
|
|
|
13
13
|
it_behaves_like "xml adapter"
|
|
14
|
+
|
|
15
|
+
describe "recover-mode error channel" do
|
|
16
|
+
it "exposes recoverable syntax errors as parse_errors" do
|
|
17
|
+
doc = Moxml.new(:nokogiri).parse("<root><unclosed>", strict: false)
|
|
18
|
+
expect(doc.parse_errors).not_to be_empty
|
|
19
|
+
expect(doc.parse_errors).to all(be_a(String))
|
|
20
|
+
end
|
|
21
|
+
end
|
|
14
22
|
end
|
data/spec/moxml/builder_spec.rb
CHANGED
|
@@ -298,7 +298,14 @@ RSpec.describe Moxml::Builder do
|
|
|
298
298
|
end
|
|
299
299
|
end.to raise_error(RuntimeError, "test error")
|
|
300
300
|
|
|
301
|
-
|
|
301
|
+
# Restoration contract, observed behaviorally: the cursor is
|
|
302
|
+
# fully unwound to the document — an empty build on the same
|
|
303
|
+
# builder completes against the document and returns it, and
|
|
304
|
+
# the root built before the failure is intact. (The partially
|
|
305
|
+
# built subtree stays; restoration moves the cursor, it does
|
|
306
|
+
# not roll back the tree.)
|
|
307
|
+
expect(builder.build { nil }).to equal(builder.document)
|
|
308
|
+
expect(builder.document.root.name).to eq("root")
|
|
302
309
|
end
|
|
303
310
|
end
|
|
304
311
|
end
|
|
@@ -94,4 +94,55 @@ RSpec.describe Moxml::Materializer do
|
|
|
94
94
|
# tree does not: far fewer allocations than 2 wrappers per node.
|
|
95
95
|
expect(allocated).to be < node_count * 24
|
|
96
96
|
end
|
|
97
|
+
|
|
98
|
+
describe "materialize_fields (issue #143)" do
|
|
99
|
+
it "streams the same data as the Hash snapshot form on every adapter" do
|
|
100
|
+
%i[leptris nokogiri rexml oga ox].each do |adapter|
|
|
101
|
+
skip "adapter not installed" unless Moxml::Adapter.available?(adapter)
|
|
102
|
+
|
|
103
|
+
ctx = Moxml.new(adapter)
|
|
104
|
+
snapshot = ctx.materialize(xml).to_a.map do |r|
|
|
105
|
+
[r[:kind], r[:qname], r[:prefix], r[:namespace_uri],
|
|
106
|
+
r[:namespaces], r[:attributes], r[:text], r[:depth]]
|
|
107
|
+
end
|
|
108
|
+
fields = []
|
|
109
|
+
ctx.materialize_fields(xml) do |kind, qname, prefix, uri, namespaces, attributes, text, depth|
|
|
110
|
+
# Copy out of the reused buffers before they are refilled.
|
|
111
|
+
fields << [
|
|
112
|
+
kind, qname, prefix, uri,
|
|
113
|
+
namespaces.each_slice(2).to_a,
|
|
114
|
+
attributes.each_slice(4).to_a,
|
|
115
|
+
text, depth
|
|
116
|
+
]
|
|
117
|
+
end
|
|
118
|
+
expect(fields).to eq(snapshot)
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
it "reuses one buffer pair across the element stream" do
|
|
123
|
+
skip "leptris not installed" unless Moxml::Adapter.available?(:leptris)
|
|
124
|
+
|
|
125
|
+
element_buffers = []
|
|
126
|
+
Moxml.new(:leptris).parse(xml).root.materialize_fields do |kind, _qname, _p, _u, _ns, attrs, _t, _d|
|
|
127
|
+
element_buffers << attrs if kind == :element
|
|
128
|
+
end
|
|
129
|
+
expect(element_buffers.map(&:object_id).uniq.size).to eq(1)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
it "builds independent snapshots in the Hash form" do
|
|
133
|
+
ctx = Moxml.new(:nokogiri)
|
|
134
|
+
first = ctx.materialize(xml).to_a
|
|
135
|
+
first.each { |r| r[:attributes] = nil }
|
|
136
|
+
second = ctx.materialize(xml).to_a
|
|
137
|
+
expect(second.find { |r| r[:qname] == "book" }[:attributes])
|
|
138
|
+
.to include(["id", "b1", nil, nil])
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
it "requires a block" do
|
|
142
|
+
expect { Moxml.new(:nokogiri).materialize_fields(xml) }
|
|
143
|
+
.to raise_error(ArgumentError)
|
|
144
|
+
expect { Moxml.new(:nokogiri).parse(xml).materialize_fields }
|
|
145
|
+
.to raise_error(ArgumentError)
|
|
146
|
+
end
|
|
147
|
+
end
|
|
97
148
|
end
|
|
@@ -56,7 +56,7 @@ RSpec.describe Moxml::Signature::Algorithms do
|
|
|
56
56
|
|
|
57
57
|
describe "custom algorithm registration" do
|
|
58
58
|
after do
|
|
59
|
-
described_class.
|
|
59
|
+
described_class.registry[:digest]
|
|
60
60
|
&.delete("http://test.example/custom-digest")
|
|
61
61
|
end
|
|
62
62
|
|
|
@@ -317,12 +317,8 @@ RSpec.describe Moxml::XPath::Conversion do
|
|
|
317
317
|
expect(described_class.first_node_text(nodes)).to eq("first")
|
|
318
318
|
end
|
|
319
319
|
|
|
320
|
-
it "returns empty string
|
|
321
|
-
|
|
322
|
-
node_set = [double("node")]
|
|
323
|
-
allow(node_set).to receive(:[]).with(0).and_return(node_set[0])
|
|
324
|
-
|
|
325
|
-
expect(described_class.first_node_text(node_set)).to eq("")
|
|
320
|
+
it "returns empty string when the set is empty" do
|
|
321
|
+
expect(described_class.first_node_text([])).to eq("")
|
|
326
322
|
end
|
|
327
323
|
|
|
328
324
|
it "handles nodes with empty text" do
|
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.14
|
|
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-29 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
|
|
@@ -56,8 +56,10 @@ files:
|
|
|
56
56
|
- docs/_guides/working-with-documents.adoc
|
|
57
57
|
- docs/_guides/xml-declaration.adoc
|
|
58
58
|
- docs/_pages/adapter-compatibility.adoc
|
|
59
|
+
- docs/_pages/adapter-protocol.adoc
|
|
59
60
|
- docs/_pages/adapters/headed-ox.adoc
|
|
60
61
|
- docs/_pages/adapters/index.adoc
|
|
62
|
+
- docs/_pages/adapters/leptris.adoc
|
|
61
63
|
- docs/_pages/adapters/libxml.adoc
|
|
62
64
|
- docs/_pages/adapters/nokogiri.adoc
|
|
63
65
|
- docs/_pages/adapters/oga.adoc
|
|
@@ -66,6 +68,7 @@ files:
|
|
|
66
68
|
- docs/_pages/best-practices.adoc
|
|
67
69
|
- docs/_pages/compatibility.adoc
|
|
68
70
|
- docs/_pages/configuration.adoc
|
|
71
|
+
- docs/_pages/conversion-apis.adoc
|
|
69
72
|
- docs/_pages/error-handling.adoc
|
|
70
73
|
- docs/_pages/headed-ox-limitations.adoc
|
|
71
74
|
- docs/_pages/headed-ox.adoc
|
|
@@ -150,8 +153,14 @@ files:
|
|
|
150
153
|
- lib/moxml/adapter/customized_rexml/formatter.rb
|
|
151
154
|
- lib/moxml/adapter/headed_ox.rb
|
|
152
155
|
- lib/moxml/adapter/leptris.rb
|
|
156
|
+
- lib/moxml/adapter/leptris/document_parts.rb
|
|
157
|
+
- lib/moxml/adapter/leptris/markers.rb
|
|
158
|
+
- lib/moxml/adapter/leptris/materialize.rb
|
|
159
|
+
- lib/moxml/adapter/leptris/sax_bridge.rb
|
|
160
|
+
- lib/moxml/adapter/leptris/serialize.rb
|
|
153
161
|
- lib/moxml/adapter/libxml.rb
|
|
154
162
|
- lib/moxml/adapter/libxml/entity_ref_registry.rb
|
|
163
|
+
- lib/moxml/adapter/libxml/serialize.rb
|
|
155
164
|
- lib/moxml/adapter/nokogiri.rb
|
|
156
165
|
- lib/moxml/adapter/oga.rb
|
|
157
166
|
- lib/moxml/adapter/ox.rb
|