moxml 0.5.9 → 0.5.11
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/base.rb +8 -0
- data/lib/moxml/adapter/leptris.rb +138 -30
- data/lib/moxml/document.rb +11 -0
- data/lib/moxml/element.rb +10 -0
- data/lib/moxml/materializer.rb +5 -0
- data/lib/moxml/version.rb +1 -1
- data/lib/moxml/xml_emitter.rb +6 -2
- data/spec/moxml/adapter/leptris_spec.rb +48 -9
- data/spec/moxml/document_free_spec.rb +22 -0
- data/spec/moxml/materializer_spec.rb +16 -1
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 15a0dd834bc1c9637a8d2284173d4994b1717b1bd2d8d9461686a56c679942e6
|
|
4
|
+
data.tar.gz: 4635e66b998d8fa371dd1617d0f960fc31b8a886627095061f945aa8b29cae41
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6eace14b6adb980bc4c14c6f628b231961f47fab55e05a0bf809591b4203a6581f0cb333dc358ec3bebeb1c239f7ecd65fc68ca34ee70fb1bf71fbf8c00f278c
|
|
7
|
+
data.tar.gz: f03e39569fd299ac6a7e8dc266d97a5499fea6f89b166f64e46944649286df8a6ceb4fd4732b6a67cac160f2318b0ed4c79fc8d782184b3a090b9c3feb65be1e
|
data/lib/moxml/adapter/base.rb
CHANGED
|
@@ -198,6 +198,14 @@ namespace_validation_mode: :strict)
|
|
|
198
198
|
false
|
|
199
199
|
end
|
|
200
200
|
|
|
201
|
+
# Deterministic native-memory release for adapters backed by
|
|
202
|
+
# C trees (issue #134). GC-managed engines no-op; released
|
|
203
|
+
# documents raise the engine's use-after-free error on
|
|
204
|
+
# further access.
|
|
205
|
+
def free_document(_native)
|
|
206
|
+
nil
|
|
207
|
+
end
|
|
208
|
+
|
|
201
209
|
# Check if the native document has an XML declaration
|
|
202
210
|
# @param native_doc the native document object
|
|
203
211
|
# @param wrapper [Moxml::Document] the wrapper with has_xml_declaration flag
|
|
@@ -19,6 +19,15 @@ 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
|
+
# libleptris 1.9.8 (leptris-ruby 1.9.30): plain parse excludes
|
|
27
|
+
# DTD ATTLIST defaults, matching libxml2/Nokogiri semantics;
|
|
28
|
+
# ParseOptions::DTDATTR opts in (leptris/leptris#606).
|
|
29
|
+
DTDATTR_SUPPORTED = ::Leptris::XML::ParseOptions.const_defined?(:DTDATTR)
|
|
30
|
+
|
|
22
31
|
class << self
|
|
23
32
|
def attachments
|
|
24
33
|
@attachments ||= Moxml::NativeAttachment.new
|
|
@@ -37,8 +46,14 @@ module Moxml
|
|
|
37
46
|
# readonly: true (issue #133): the binding memoizes reads and
|
|
38
47
|
# refuses mutations — the parse-and-read lifecycle for
|
|
39
48
|
# multi-pass consumers (comparison, diff, signature).
|
|
49
|
+
# dtdattr: true opts into DTD ATTLIST default materialization
|
|
50
|
+
# (off by default since libleptris 1.9.8, matching libxml2).
|
|
40
51
|
native_doc = begin
|
|
41
|
-
::Leptris::XML::Document.parse(
|
|
52
|
+
::Leptris::XML::Document.parse(
|
|
53
|
+
processed,
|
|
54
|
+
readonly: options[:readonly] == true,
|
|
55
|
+
options: dtdattr_parse_options(options),
|
|
56
|
+
)
|
|
42
57
|
rescue ::Leptris::XML::ParseError => e
|
|
43
58
|
# libleptris has no recovery mode that survives unclosed
|
|
44
59
|
# tags; non-strict callers get an empty document, matching
|
|
@@ -56,6 +71,30 @@ module Moxml
|
|
|
56
71
|
doc
|
|
57
72
|
end
|
|
58
73
|
|
|
74
|
+
# nil when DTDATTR is unsupported or not requested — the
|
|
75
|
+
# binding treats a nil options hash as plain defaults.
|
|
76
|
+
def dtdattr_parse_options(options)
|
|
77
|
+
return nil unless DTDATTR_SUPPORTED && options[:dtdattr] == true
|
|
78
|
+
|
|
79
|
+
::Leptris::XML::ParseOptions.dtdattr
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Issue #134: deterministic release of the C tree. The binding
|
|
83
|
+
# clears its wrapper cache and raises UseAfterFreeError on
|
|
84
|
+
# later access; moxml-side attachments for the document are
|
|
85
|
+
# swept too (the context wrapper identity map self-cleans via
|
|
86
|
+
# its size valve).
|
|
87
|
+
DOCUMENT_ATTACHMENT_KEYS = %i[
|
|
88
|
+
entity_markers doc_pi_nodes declaration doctype
|
|
89
|
+
had_source_declaration document_text
|
|
90
|
+
].freeze
|
|
91
|
+
|
|
92
|
+
def free_document(native)
|
|
93
|
+
DOCUMENT_ATTACHMENT_KEYS.each { |key| attachments.delete(native, key) }
|
|
94
|
+
native.free
|
|
95
|
+
nil
|
|
96
|
+
end
|
|
97
|
+
|
|
59
98
|
def create_document(_native_doc = nil)
|
|
60
99
|
::Leptris::XML::Document.create
|
|
61
100
|
end
|
|
@@ -243,24 +282,33 @@ module Moxml
|
|
|
243
282
|
# ER expansion); the bulk path has no marker handling.
|
|
244
283
|
return nil if doc.nil? || attachments.get(doc, :entity_markers)
|
|
245
284
|
|
|
246
|
-
|
|
285
|
+
# leptris 1.9.28+'s traverse follows the document chain; from
|
|
286
|
+
# an arbitrary element it can visit following siblings. Use
|
|
287
|
+
# the bulk path only for document materialization, and filter
|
|
288
|
+
# the stream to the document element's subtree (issue #140).
|
|
289
|
+
return nil unless native.is_a?(::Leptris::XML::Document)
|
|
290
|
+
|
|
291
|
+
root = doc.root
|
|
247
292
|
return nil if root.nil?
|
|
248
293
|
|
|
249
294
|
depth_memo = {}.compare_by_identity
|
|
250
295
|
root.traverse do |node|
|
|
296
|
+
depth = material_depth_in_subtree(node, root, depth_memo)
|
|
297
|
+
next if depth.nil?
|
|
298
|
+
|
|
251
299
|
record = case node
|
|
252
300
|
when ::Leptris::XML::Element
|
|
253
|
-
element_material_record(node,
|
|
301
|
+
element_material_record(node, depth)
|
|
254
302
|
when ::Leptris::XML::CDATA
|
|
255
303
|
# CDATA < Text in the binding: this arm must come
|
|
256
304
|
# first or CDATA content reports as text.
|
|
257
|
-
text_material_record(:cdata, node.content,
|
|
305
|
+
text_material_record(:cdata, node.content, depth)
|
|
258
306
|
when ::Leptris::XML::Text
|
|
259
|
-
text_material_record(:text, node.content,
|
|
307
|
+
text_material_record(:text, node.content, depth)
|
|
260
308
|
when ::Leptris::XML::Comment
|
|
261
|
-
text_material_record(:comment, node.content,
|
|
309
|
+
text_material_record(:comment, node.content, depth)
|
|
262
310
|
when ::Leptris::XML::ProcessingInstruction
|
|
263
|
-
text_material_record(:processing_instruction, node.content,
|
|
311
|
+
text_material_record(:processing_instruction, node.content, depth)
|
|
264
312
|
.merge(qname: node.target)
|
|
265
313
|
end
|
|
266
314
|
yield(record) if record
|
|
@@ -268,7 +316,7 @@ module Moxml
|
|
|
268
316
|
true
|
|
269
317
|
end
|
|
270
318
|
|
|
271
|
-
def element_material_record(node,
|
|
319
|
+
def element_material_record(node, depth)
|
|
272
320
|
attributes = node.each_attribute.map do |attr|
|
|
273
321
|
# Local name + separate prefix, matching the generic
|
|
274
322
|
# path's resolver semantics (moxml's canonical shape).
|
|
@@ -283,32 +331,51 @@ module Moxml
|
|
|
283
331
|
qname: node.name,
|
|
284
332
|
prefix: node.prefix,
|
|
285
333
|
namespace_uri: ns&.href,
|
|
334
|
+
# Own declarations only — same shape as the generic
|
|
335
|
+
# path's Element#declared_namespaces (issue #138).
|
|
336
|
+
namespaces: begin
|
|
337
|
+
decls = node.namespace_definitions.map { |d| [d.prefix, d.href] }
|
|
338
|
+
decls.empty? ? Materializer::EMPTY_ATTRIBUTES : decls
|
|
339
|
+
end,
|
|
286
340
|
attributes: attributes,
|
|
287
341
|
text: nil,
|
|
288
|
-
depth:
|
|
342
|
+
depth: depth,
|
|
289
343
|
}
|
|
290
344
|
end
|
|
291
345
|
|
|
292
|
-
def text_material_record(kind, text,
|
|
346
|
+
def text_material_record(kind, text, depth)
|
|
293
347
|
{
|
|
294
348
|
kind: kind,
|
|
295
349
|
qname: nil,
|
|
296
350
|
prefix: nil,
|
|
297
351
|
namespace_uri: nil,
|
|
352
|
+
namespaces: Materializer::EMPTY_ATTRIBUTES,
|
|
298
353
|
attributes: Materializer::EMPTY_ATTRIBUTES,
|
|
299
354
|
text: text,
|
|
300
|
-
depth:
|
|
355
|
+
depth: depth,
|
|
301
356
|
}
|
|
302
357
|
end
|
|
303
358
|
|
|
304
|
-
#
|
|
305
|
-
#
|
|
359
|
+
# Depth relative to the subtree root, or nil when the node
|
|
360
|
+
# lies outside it (traverse follows the document chain since
|
|
361
|
+
# leptris 1.9.28, so epilog siblings can appear in the
|
|
362
|
+
# stream). Binding wrappers are address-stable and #parent is
|
|
363
|
+
# memoized, so each edge resolves once through the
|
|
306
364
|
# identity-keyed memo.
|
|
307
|
-
def
|
|
308
|
-
memo[node] ||=
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
365
|
+
def material_depth_in_subtree(node, root, memo)
|
|
366
|
+
memo[node] ||= if node.equal?(root)
|
|
367
|
+
0
|
|
368
|
+
else
|
|
369
|
+
parent = node.parent
|
|
370
|
+
if parent.nil? || parent.is_a?(::Leptris::XML::Document)
|
|
371
|
+
nil
|
|
372
|
+
else
|
|
373
|
+
|
|
374
|
+
parent_depth = material_depth_in_subtree(parent, root, memo)
|
|
375
|
+
parent_depth.nil? ? nil : parent_depth + 1
|
|
376
|
+
|
|
377
|
+
end
|
|
378
|
+
end
|
|
312
379
|
end
|
|
313
380
|
|
|
314
381
|
def node_type(node)
|
|
@@ -971,9 +1038,22 @@ module Moxml
|
|
|
971
1038
|
native = native_doctype_xml(doc)
|
|
972
1039
|
parts << native << "\n" if native
|
|
973
1040
|
|
|
974
|
-
|
|
1041
|
+
if DOC_NODE_SUPPORTED
|
|
1042
|
+
# The libxml2-model document node: prolog PIs/comments,
|
|
1043
|
+
# the root, epilog PIs/comments — in document order, so
|
|
1044
|
+
# epilog parts serialize after the root (issue #130).
|
|
1045
|
+
doc_children = document_node_children(doc)
|
|
1046
|
+
if doc_children
|
|
1047
|
+
doc_children.each { |child| parts << raw_serialize(child, options) << "\n" }
|
|
1048
|
+
else
|
|
1049
|
+
document_pi_nodes(doc).each { |pi| parts << pi.to_xml << "\n" }
|
|
1050
|
+
parts << raw_serialize(doc.root, options) << "\n" if doc.root
|
|
1051
|
+
end
|
|
1052
|
+
else
|
|
1053
|
+
document_pi_nodes(doc).each { |pi| parts << pi.to_xml << "\n" }
|
|
975
1054
|
|
|
976
|
-
|
|
1055
|
+
parts << raw_serialize(doc.root, options) << "\n" if doc.root
|
|
1056
|
+
end
|
|
977
1057
|
|
|
978
1058
|
texts = attachments.get(doc, :document_text)
|
|
979
1059
|
texts&.each { |text| parts << XmlEmitter.escape_text(text.content.to_s) }
|
|
@@ -1000,7 +1080,8 @@ module Moxml
|
|
|
1000
1080
|
dt = doc.doctype
|
|
1001
1081
|
return nil unless dt
|
|
1002
1082
|
|
|
1003
|
-
|
|
1083
|
+
subset = dt.internal_subset if dt.class.method_defined?(:internal_subset)
|
|
1084
|
+
XmlEmitter.doctype_xml(dt.root_name, dt.public_id, dt.system_id, subset)
|
|
1004
1085
|
end
|
|
1005
1086
|
|
|
1006
1087
|
def sax_parse(xml, handler)
|
|
@@ -1046,6 +1127,20 @@ module Moxml
|
|
|
1046
1127
|
# the same objects, so wrapper mutations round-trip. Build the
|
|
1047
1128
|
# cache BEFORE appending a PI with add_pi, or the C-side
|
|
1048
1129
|
# addition would be double-counted.
|
|
1130
|
+
# The document node's children, or nil when the node does not
|
|
1131
|
+
# reflect reality: parsed documents always list the root
|
|
1132
|
+
# element among their children, but programmatically built
|
|
1133
|
+
# ones do not (binding gap) — those keep the legacy parts
|
|
1134
|
+
# path.
|
|
1135
|
+
def document_node_children(doc)
|
|
1136
|
+
doc_children = doc.children.to_a
|
|
1137
|
+
has_root = doc_children.any?(::Leptris::XML::Element)
|
|
1138
|
+
return doc_children if has_root
|
|
1139
|
+
return doc_children if doc.root.nil?
|
|
1140
|
+
|
|
1141
|
+
nil
|
|
1142
|
+
end
|
|
1143
|
+
|
|
1049
1144
|
def document_pi_nodes(doc)
|
|
1050
1145
|
attachments.get(doc, :doc_pi_nodes) || begin
|
|
1051
1146
|
nodes = doc.processing_instructions.map do |(target, data)|
|
|
@@ -1065,16 +1160,29 @@ module Moxml
|
|
|
1065
1160
|
doctype_wrapper = attachments.get(doc, :doctype)
|
|
1066
1161
|
children << doctype_wrapper if doctype_wrapper
|
|
1067
1162
|
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1163
|
+
if DOC_NODE_SUPPORTED
|
|
1164
|
+
# The libxml2-model document node lists prolog PIs/comments,
|
|
1165
|
+
# the root, and epilog PIs/comments in document order —
|
|
1166
|
+
# the Nokogiri-shaped contract, epilog anchoring included
|
|
1167
|
+
# (issue #130). Built (programmatic) documents are not yet
|
|
1168
|
+
# fully reflected by the node (binding gap: an attached
|
|
1169
|
+
# root does not appear); document_node_children answers
|
|
1170
|
+
# nil there for the legacy parts path.
|
|
1171
|
+
doc_children = document_node_children(doc)
|
|
1172
|
+
if doc_children
|
|
1173
|
+
children.concat(doc_children)
|
|
1174
|
+
else
|
|
1175
|
+
children.concat(document_pi_nodes(doc))
|
|
1176
|
+
children << doc.root if doc.root
|
|
1177
|
+
end
|
|
1178
|
+
else
|
|
1179
|
+
# Legacy path: document-level PIs live outside the element
|
|
1180
|
+
# tree in a flat pre-root list (libleptris < 1.9.7 C
|
|
1181
|
+
# model); epilog anchoring is not representable there.
|
|
1182
|
+
children.concat(document_pi_nodes(doc))
|
|
1076
1183
|
|
|
1077
|
-
|
|
1184
|
+
children << doc.root if doc.root
|
|
1185
|
+
end
|
|
1078
1186
|
|
|
1079
1187
|
texts = attachments.get(doc, :document_text)
|
|
1080
1188
|
children.concat(texts) if texts
|
data/lib/moxml/document.rb
CHANGED
|
@@ -31,6 +31,17 @@ module Moxml
|
|
|
31
31
|
root&.materialize(&block)
|
|
32
32
|
end
|
|
33
33
|
|
|
34
|
+
# Deterministically release the adapter's native memory for this
|
|
35
|
+
# document (issue #134) — batch workloads parsing thousands of
|
|
36
|
+
# documents otherwise hold C trees until GC finalizers run.
|
|
37
|
+
# GC-managed engines no-op. Further access raises the engine's
|
|
38
|
+
# use-after-free error; ordinary garbage-collected documents keep
|
|
39
|
+
# working via the finalizer either way.
|
|
40
|
+
def free
|
|
41
|
+
adapter.free_document(@native)
|
|
42
|
+
nil
|
|
43
|
+
end
|
|
44
|
+
|
|
34
45
|
def create_element(name)
|
|
35
46
|
Element.new(adapter.create_element(name, owner_doc: @native), context)
|
|
36
47
|
end
|
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/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
data/lib/moxml/xml_emitter.rb
CHANGED
|
@@ -56,8 +56,9 @@ module Moxml
|
|
|
56
56
|
output << "?>"
|
|
57
57
|
end
|
|
58
58
|
|
|
59
|
-
# @return [String] a DOCTYPE with its external identifier
|
|
60
|
-
|
|
59
|
+
# @return [String] a DOCTYPE with its external identifier and
|
|
60
|
+
# optional internal subset
|
|
61
|
+
def doctype_xml(name, public_id, system_id, internal_subset = nil)
|
|
61
62
|
output = "<!DOCTYPE #{name}"
|
|
62
63
|
if public_id && !public_id.to_s.empty?
|
|
63
64
|
output << %( PUBLIC "#{public_id}")
|
|
@@ -65,6 +66,9 @@ module Moxml
|
|
|
65
66
|
elsif system_id && !system_id.to_s.empty?
|
|
66
67
|
output << %( SYSTEM "#{system_id}")
|
|
67
68
|
end
|
|
69
|
+
if internal_subset && !internal_subset.empty?
|
|
70
|
+
output << " [" << internal_subset << "]"
|
|
71
|
+
end
|
|
68
72
|
output << ">"
|
|
69
73
|
end
|
|
70
74
|
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::
|
|
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
|
|
@@ -139,6 +153,31 @@ RSpec.describe Moxml::Adapter::Leptris do
|
|
|
139
153
|
end
|
|
140
154
|
end
|
|
141
155
|
|
|
156
|
+
describe "DTD ATTLIST defaults" do
|
|
157
|
+
# libleptris 1.9.8: plain parse excludes ATTLIST defaults,
|
|
158
|
+
# matching libxml2/Nokogiri/REXML; dtdattr: true opts in.
|
|
159
|
+
let(:ctx) { Moxml.new(:leptris) }
|
|
160
|
+
let(:dtd_xml) do
|
|
161
|
+
%(<?xml version="1.0"?><!DOCTYPE doc [<!ATTLIST e9 attr CDATA "default">]><doc><e9/></doc>)
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
it "excludes ATTLIST defaults on plain parse" do
|
|
165
|
+
skip "requires leptris with no-DTDATTR semantics" unless Moxml::Adapter::Leptris::DTDATTR_SUPPORTED
|
|
166
|
+
|
|
167
|
+
doc = ctx.parse(dtd_xml)
|
|
168
|
+
expect(doc.at_xpath("//e9")["attr"]).to be_nil
|
|
169
|
+
expect(doc.to_xml).not_to include("attr=")
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
it "materializes ATTLIST defaults with dtdattr: true" do
|
|
173
|
+
skip "requires leptris with ParseOptions::DTDATTR" unless Moxml::Adapter::Leptris::DTDATTR_SUPPORTED
|
|
174
|
+
|
|
175
|
+
doc = ctx.parse(dtd_xml, dtdattr: true)
|
|
176
|
+
expect(doc.at_xpath("//e9")["attr"]).to eq("default")
|
|
177
|
+
expect(doc.to_xml).to include(%(attr="default"))
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
|
|
142
181
|
describe "entity-marker tracking" do
|
|
143
182
|
let(:ctx) { Moxml.new(:leptris) }
|
|
144
183
|
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
|
|
5
|
+
RSpec.describe "Document#free" do
|
|
6
|
+
let(:xml) { '<r><a x="1">t</a></r>' }
|
|
7
|
+
|
|
8
|
+
it "frees native memory deterministically (issue #134)" do
|
|
9
|
+
doc = Moxml.new(:leptris).parse(xml)
|
|
10
|
+
expect(doc.root.name).to eq("r")
|
|
11
|
+
|
|
12
|
+
expect(doc.free).to be_nil
|
|
13
|
+
expect { doc.root }.to raise_error(Leptris::XML::UseAfterFreeError)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
it "is a no-op on GC-managed engines" do
|
|
17
|
+
doc = Moxml.new(:nokogiri).parse(xml)
|
|
18
|
+
|
|
19
|
+
expect(doc.free).to be_nil
|
|
20
|
+
expect(doc.root.name).to eq("r")
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -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
|
|
@@ -56,6 +60,17 @@ RSpec.describe Moxml::Materializer do
|
|
|
56
60
|
)
|
|
57
61
|
end
|
|
58
62
|
|
|
63
|
+
it "scopes the document stream to the root subtree (issue #140)" do
|
|
64
|
+
scoped = %(<?xml version="1.0"?><!-- prolog c --><?pi b?><root>t</root><!-- epilog c --><?pi2 a?>)
|
|
65
|
+
leptris = Moxml.new(:leptris).materialize(scoped).to_a
|
|
66
|
+
nokogiri = Moxml.new(:nokogiri).materialize(scoped).to_a
|
|
67
|
+
|
|
68
|
+
expect(leptris).to eq(nokogiri)
|
|
69
|
+
expect(leptris.map { |r| [r[:kind], r[:depth]] }).to eq(
|
|
70
|
+
[[:text, 1], [:element, 0]],
|
|
71
|
+
)
|
|
72
|
+
end
|
|
73
|
+
|
|
59
74
|
it "materializes from any element subtree" do
|
|
60
75
|
doc = Moxml.new(:nokogiri).parse(xml)
|
|
61
76
|
title = doc.at_xpath("//*[local-name()='title']")
|
|
@@ -77,6 +92,6 @@ RSpec.describe Moxml::Materializer do
|
|
|
77
92
|
|
|
78
93
|
# Records themselves allocate (hash + FFI strings); the wrapper
|
|
79
94
|
# tree does not: far fewer allocations than 2 wrappers per node.
|
|
80
|
-
expect(allocated).to be < node_count *
|
|
95
|
+
expect(allocated).to be < node_count * 24
|
|
81
96
|
end
|
|
82
97
|
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.11
|
|
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
|
|
@@ -447,6 +447,7 @@ files:
|
|
|
447
447
|
- spec/moxml/declaration_spec.rb
|
|
448
448
|
- spec/moxml/doctype_spec.rb
|
|
449
449
|
- spec/moxml/document_builder_spec.rb
|
|
450
|
+
- spec/moxml/document_free_spec.rb
|
|
450
451
|
- spec/moxml/document_spec.rb
|
|
451
452
|
- spec/moxml/element_spec.rb
|
|
452
453
|
- spec/moxml/entity_preservation_spec.rb
|