moxml 0.5.12 → 0.5.13
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/docs/_pages/adapter-protocol.adoc +113 -0
- data/docs/_pages/adapters/index.adoc +11 -5
- data/docs/_pages/adapters/leptris.adoc +69 -0
- data/docs/_pages/adapters/libxml.adoc +1 -1
- data/docs/_pages/adapters/nokogiri.adoc +1 -1
- data/docs/_pages/conversion-apis.adoc +78 -0
- data/docs/_pages/index.adoc +6 -0
- 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 +110 -0
- data/lib/moxml/adapter/leptris/sax_bridge.rb +56 -0
- data/lib/moxml/adapter/leptris/serialize.rb +127 -0
- data/lib/moxml/adapter/leptris.rb +14 -518
- data/lib/moxml/adapter/libxml/serialize.rb +497 -0
- data/lib/moxml/adapter/libxml.rb +20 -462
- data/lib/moxml/materializer.rb +39 -17
- data/lib/moxml/version.rb +1 -1
- data/spec/moxml/adapter/leptris_spec.rb +14 -4
- metadata +11 -2
data/lib/moxml/adapter/libxml.rb
CHANGED
|
@@ -51,6 +51,8 @@ module Moxml
|
|
|
51
51
|
}.freeze
|
|
52
52
|
private_constant :NATIVE_NODE_TYPE_MAP
|
|
53
53
|
|
|
54
|
+
autoload :Serialize, "moxml/adapter/libxml/serialize"
|
|
55
|
+
|
|
54
56
|
WRAPPER_NODE_TYPE_MAP = {
|
|
55
57
|
DoctypeWrapper => :doctype,
|
|
56
58
|
CustomizedLibxml::Declaration => :declaration,
|
|
@@ -63,6 +65,8 @@ module Moxml
|
|
|
63
65
|
}.freeze
|
|
64
66
|
private_constant :WRAPPER_NODE_TYPE_MAP
|
|
65
67
|
|
|
68
|
+
extend Serialize
|
|
69
|
+
|
|
66
70
|
class << self
|
|
67
71
|
def attachments
|
|
68
72
|
@attachments ||= Moxml::NativeAttachment.new
|
|
@@ -312,17 +316,26 @@ module Moxml
|
|
|
312
316
|
native_node = unpatch_node(node)
|
|
313
317
|
return [] unless native_node
|
|
314
318
|
|
|
315
|
-
# Handle Document specially - it doesn't have children? method
|
|
319
|
+
# Handle Document specially - it doesn't have children? method.
|
|
320
|
+
# The binding's document chain lists prolog/epilog PIs and
|
|
321
|
+
# comments around the root (Nokogiri-shaped contract); the
|
|
322
|
+
# DOCTYPE attachment rides along first, mirroring the other
|
|
323
|
+
# adapters.
|
|
316
324
|
if native_node.is_a?(::LibXML::XML::Document)
|
|
317
325
|
result = []
|
|
326
|
+
has_native_doctype = false
|
|
327
|
+
node = native_node.child
|
|
328
|
+
while node
|
|
329
|
+
has_native_doctype ||= node.node_type == ::LibXML::XML::Node::DTD_NODE
|
|
330
|
+
result << patch_node(node)
|
|
331
|
+
node = node.next
|
|
332
|
+
end
|
|
318
333
|
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
return result unless native_node.root
|
|
334
|
+
unless has_native_doctype
|
|
335
|
+
doctype_wrapper = attachments.get(native_node, :doctype)
|
|
336
|
+
result.unshift(doctype_wrapper) if doctype_wrapper
|
|
337
|
+
end
|
|
324
338
|
|
|
325
|
-
result << patch_node(native_node.root)
|
|
326
339
|
return result
|
|
327
340
|
end
|
|
328
341
|
|
|
@@ -952,146 +965,6 @@ module Moxml
|
|
|
952
965
|
results&.first
|
|
953
966
|
end
|
|
954
967
|
|
|
955
|
-
def serialize(node, options = {})
|
|
956
|
-
# FIRST: Check if node is any kind of wrapper with custom to_xml
|
|
957
|
-
if node.is_a?(CustomizedLibxml::Node) || node.is_a?(DoctypeWrapper)
|
|
958
|
-
return node.to_xml
|
|
959
|
-
end
|
|
960
|
-
|
|
961
|
-
native_node = unpatch_node(node)
|
|
962
|
-
return "" unless native_node
|
|
963
|
-
|
|
964
|
-
if native_node.is_a?(::LibXML::XML::Document)
|
|
965
|
-
output = +""
|
|
966
|
-
|
|
967
|
-
# Check if we should include declaration
|
|
968
|
-
# Priority: explicit no_declaration option > default (include)
|
|
969
|
-
should_include_decl = if options.key?(:no_declaration)
|
|
970
|
-
!options[:no_declaration]
|
|
971
|
-
else
|
|
972
|
-
# Default: include declaration
|
|
973
|
-
true
|
|
974
|
-
end
|
|
975
|
-
|
|
976
|
-
if should_include_decl
|
|
977
|
-
# Check if declaration was explicitly managed
|
|
978
|
-
decl = attachments.get(native_node, :declaration)
|
|
979
|
-
if decl
|
|
980
|
-
# Only output declaration if it exists and wasn't removed
|
|
981
|
-
output << decl.to_xml unless decl.removed
|
|
982
|
-
else
|
|
983
|
-
# No declaration stored - create default
|
|
984
|
-
version = native_node.version || "1.0"
|
|
985
|
-
encoding_val = options[:encoding] ||
|
|
986
|
-
encoding_to_string(native_node.encoding) ||
|
|
987
|
-
"UTF-8"
|
|
988
|
-
|
|
989
|
-
# Don't add standalone="yes" by default - only if explicitly set
|
|
990
|
-
decl = CustomizedLibxml::Declaration.new(
|
|
991
|
-
native_node,
|
|
992
|
-
version,
|
|
993
|
-
encoding_val,
|
|
994
|
-
nil, # No standalone by default
|
|
995
|
-
)
|
|
996
|
-
attachments.set(native_node, :declaration, decl)
|
|
997
|
-
output << decl.to_xml
|
|
998
|
-
end
|
|
999
|
-
end
|
|
1000
|
-
|
|
1001
|
-
# Add DOCTYPE if stored on document
|
|
1002
|
-
doctype_wrapper = attachments.get(native_node, :doctype)
|
|
1003
|
-
if doctype_wrapper
|
|
1004
|
-
output << "\n" unless output.empty?
|
|
1005
|
-
output << doctype_wrapper.to_xml
|
|
1006
|
-
end
|
|
1007
|
-
|
|
1008
|
-
# Add document-level processing instructions if stored
|
|
1009
|
-
pis = attachments.get(native_node, :pis)
|
|
1010
|
-
if pis && !pis.empty?
|
|
1011
|
-
pis.each do |pi|
|
|
1012
|
-
output << "\n" unless output.empty?
|
|
1013
|
-
output << pi.to_xml
|
|
1014
|
-
end
|
|
1015
|
-
end
|
|
1016
|
-
|
|
1017
|
-
# Add text nodes if stored (for documents without root)
|
|
1018
|
-
texts = attachments.get(native_node, :texts)
|
|
1019
|
-
if texts && !texts.empty?
|
|
1020
|
-
texts.each do |text|
|
|
1021
|
-
output << "\n" unless output.empty?
|
|
1022
|
-
output << text.to_xml
|
|
1023
|
-
end
|
|
1024
|
-
end
|
|
1025
|
-
|
|
1026
|
-
if native_node.root
|
|
1027
|
-
indent_size = options[:indent].is_a?(Integer) && options[:indent].positive? ? options[:indent] : 0
|
|
1028
|
-
# `eref_active` is computed once here and threaded through the
|
|
1029
|
-
# recursion so that the per-element `attachments.key?` Monitor
|
|
1030
|
-
# sync only fires for docs that actually have entity refs.
|
|
1031
|
-
eref_active = entity_ref_registry(native_node).active?
|
|
1032
|
-
root_output = native_root_output(native_node, indent_size)
|
|
1033
|
-
root_output ||= serialize_element_with_namespaces(
|
|
1034
|
-
native_node.root,
|
|
1035
|
-
true,
|
|
1036
|
-
indent_size,
|
|
1037
|
-
0,
|
|
1038
|
-
eref_active: eref_active,
|
|
1039
|
-
)
|
|
1040
|
-
|
|
1041
|
-
output << "\n" << root_output unless output.empty?
|
|
1042
|
-
output << root_output if output.empty?
|
|
1043
|
-
end
|
|
1044
|
-
|
|
1045
|
-
output
|
|
1046
|
-
else
|
|
1047
|
-
serialize_element_with_namespaces(native_node, true)
|
|
1048
|
-
end
|
|
1049
|
-
end
|
|
1050
|
-
|
|
1051
|
-
# Possessive-quantifier form: no backtracking, so pathological
|
|
1052
|
-
# attribute content cannot blow up the expansion pass.
|
|
1053
|
-
EMPTY_ELEMENT_EXPANSION_RE = %r{
|
|
1054
|
-
<([A-Za-z_][\w.:-]*+)
|
|
1055
|
-
((?:"[^"]*+"|'[^']*+'|[^<>"'/]++)*+)
|
|
1056
|
-
/>
|
|
1057
|
-
}x
|
|
1058
|
-
private_constant :EMPTY_ELEMENT_EXPANSION_RE
|
|
1059
|
-
|
|
1060
|
-
# Serialize the root subtree with libxml's C serializer plus
|
|
1061
|
-
# moxml-canonical corrections — roughly 25x faster and ~2600x
|
|
1062
|
-
# fewer allocations than the Ruby walker. Returns nil whenever
|
|
1063
|
-
# a guard says the walker is still required.
|
|
1064
|
-
def native_root_output(native_doc, indent_size)
|
|
1065
|
-
return nil unless indent_size == 2
|
|
1066
|
-
return nil if entity_ref_registry(native_doc).active?
|
|
1067
|
-
|
|
1068
|
-
output = native_doc.root.to_s
|
|
1069
|
-
|
|
1070
|
-
# The walker strips prefixed names on parsed namespaced
|
|
1071
|
-
# children; that behavior is load-bearing, so namespaced
|
|
1072
|
-
# output keeps the walker.
|
|
1073
|
-
return nil if output.include?("xmlns")
|
|
1074
|
-
|
|
1075
|
-
# The native serializer escapes non-ASCII codepoints in
|
|
1076
|
-
# attribute values as numeric character references
|
|
1077
|
-
# (x="©"); the walker emits literal UTF-8
|
|
1078
|
-
return nil if output.include?("&#x")
|
|
1079
|
-
|
|
1080
|
-
# Layout: pull closing tags onto the last child's line
|
|
1081
|
-
output = output.gsub(/\n[ \t]*(<\/[\w.:-]+>)/, '\1')
|
|
1082
|
-
|
|
1083
|
-
if output.include?("/>")
|
|
1084
|
-
# A literal "/>" inside a comment or CDATA section would be
|
|
1085
|
-
# falsely expanded — the segment-aware walker is correct here
|
|
1086
|
-
return nil if output.include?("<!--") || output.include?("<![CDATA[")
|
|
1087
|
-
|
|
1088
|
-
output = output.gsub(EMPTY_ELEMENT_EXPANSION_RE, '<\1\2></\1>')
|
|
1089
|
-
end
|
|
1090
|
-
|
|
1091
|
-
# Native escapes attribute apostrophes; moxml keeps them literal
|
|
1092
|
-
output.gsub("'", "'")
|
|
1093
|
-
end
|
|
1094
|
-
|
|
1095
968
|
# Shallow duplication: copies the node itself (name, attrs, namespaces)
|
|
1096
969
|
# but NOT its descendants (deep duplication composes this).
|
|
1097
970
|
# walks the source tree and re-adds children one at a time via
|
|
@@ -1198,89 +1071,6 @@ module Moxml
|
|
|
1198
1071
|
|
|
1199
1072
|
private
|
|
1200
1073
|
|
|
1201
|
-
def serialize_element(elem)
|
|
1202
|
-
output = "<#{elem.name}"
|
|
1203
|
-
|
|
1204
|
-
# Add namespace definitions (only on this element, not ancestors)
|
|
1205
|
-
if elem.is_a?(::LibXML::XML::Node)
|
|
1206
|
-
seen_ns = {}
|
|
1207
|
-
elem.namespaces.each do |ns|
|
|
1208
|
-
prefix = ns.prefix
|
|
1209
|
-
uri = ns.href
|
|
1210
|
-
next if seen_ns.key?(prefix)
|
|
1211
|
-
|
|
1212
|
-
seen_ns[prefix] = true
|
|
1213
|
-
output << if prefix.nil? || prefix.empty?
|
|
1214
|
-
" xmlns=\"#{XmlEmitter.escape_attribute(uri)}\""
|
|
1215
|
-
else
|
|
1216
|
-
" xmlns:#{prefix}=\"#{XmlEmitter.escape_attribute(uri)}\""
|
|
1217
|
-
end
|
|
1218
|
-
end
|
|
1219
|
-
end
|
|
1220
|
-
|
|
1221
|
-
# Add attributes
|
|
1222
|
-
if elem.attributes?
|
|
1223
|
-
elem.each_attr do |attr|
|
|
1224
|
-
next if attr.name.start_with?("xmlns")
|
|
1225
|
-
|
|
1226
|
-
# Include namespace prefix if attribute has one
|
|
1227
|
-
attr_name = if attr.ns&.prefix
|
|
1228
|
-
"#{attr.ns.prefix}:#{attr.name}"
|
|
1229
|
-
else
|
|
1230
|
-
attr.name
|
|
1231
|
-
end
|
|
1232
|
-
output << " #{attr_name}=\"#{XmlEmitter.escape_attribute(attr.value)}\""
|
|
1233
|
-
end
|
|
1234
|
-
end
|
|
1235
|
-
|
|
1236
|
-
# Always use verbose format <tag></tag> for consistency with other adapters
|
|
1237
|
-
output << ">"
|
|
1238
|
-
if elem.children?
|
|
1239
|
-
elem.each_child do |child|
|
|
1240
|
-
# Skip whitespace-only text nodes
|
|
1241
|
-
next if blank_text_node?(child)
|
|
1242
|
-
|
|
1243
|
-
output << serialize_node(child)
|
|
1244
|
-
end
|
|
1245
|
-
end
|
|
1246
|
-
|
|
1247
|
-
# Append any EntityReference wrappers stored on the document
|
|
1248
|
-
doc = elem.doc
|
|
1249
|
-
entity_refs = entity_ref_registry(doc).refs_for(elem)
|
|
1250
|
-
entity_refs&.each { |ref| output << ref.to_xml }
|
|
1251
|
-
|
|
1252
|
-
output << "</#{elem.name}>"
|
|
1253
|
-
|
|
1254
|
-
output
|
|
1255
|
-
end
|
|
1256
|
-
|
|
1257
|
-
def serialize_node(node)
|
|
1258
|
-
# Check if node is a wrapper with to_xml method
|
|
1259
|
-
case node
|
|
1260
|
-
when CustomizedLibxml::ProcessingInstruction,
|
|
1261
|
-
CustomizedLibxml::Comment,
|
|
1262
|
-
CustomizedLibxml::Cdata,
|
|
1263
|
-
CustomizedLibxml::Text,
|
|
1264
|
-
CustomizedLibxml::EntityReference
|
|
1265
|
-
return node.to_xml
|
|
1266
|
-
end
|
|
1267
|
-
|
|
1268
|
-
case node.node_type
|
|
1269
|
-
when ::LibXML::XML::Node::ELEMENT_NODE
|
|
1270
|
-
serialize_element(node)
|
|
1271
|
-
when ::LibXML::XML::Node::TEXT_NODE
|
|
1272
|
-
XmlEmitter.escape_text(node.content)
|
|
1273
|
-
when ::LibXML::XML::Node::CDATA_SECTION_NODE
|
|
1274
|
-
"<![CDATA[#{node.content}]]>"
|
|
1275
|
-
when ::LibXML::XML::Node::COMMENT_NODE
|
|
1276
|
-
"<!-- #{node.content} -->"
|
|
1277
|
-
when ::LibXML::XML::Node::PI_NODE
|
|
1278
|
-
"<?#{node.name} #{node.content}?>"
|
|
1279
|
-
else
|
|
1280
|
-
node.to_s
|
|
1281
|
-
end
|
|
1282
|
-
end
|
|
1283
|
-
|
|
1284
1074
|
def import_and_add(doc, element, child)
|
|
1285
1075
|
return unless element && child
|
|
1286
1076
|
|
|
@@ -1346,47 +1136,6 @@ module Moxml
|
|
|
1346
1136
|
end
|
|
1347
1137
|
end
|
|
1348
1138
|
|
|
1349
|
-
def serialize_element_with_namespaces(elem, include_ns = true,
|
|
1350
|
-
indent_size = 0, depth = 0,
|
|
1351
|
-
eref_active: nil)
|
|
1352
|
-
# Cache elem.name — it's a libxml C call we'd otherwise make
|
|
1353
|
-
# twice (open tag + close tag). Concat with `<<` instead of
|
|
1354
|
-
# `"<#{name}"` to avoid the interpolated intermediate string.
|
|
1355
|
-
name = elem.name
|
|
1356
|
-
output = +"<"
|
|
1357
|
-
output << name
|
|
1358
|
-
emit_namespace_definitions(output, elem, include_ns)
|
|
1359
|
-
emit_attributes(output, elem)
|
|
1360
|
-
|
|
1361
|
-
# `eref_active` is precomputed at the top-level `serialize` call
|
|
1362
|
-
# and threaded down — when nil (top-level non-recursive call into
|
|
1363
|
-
# this method), look it up; when false, skip the per-element doc
|
|
1364
|
-
# attachment query that otherwise fires for every element under
|
|
1365
|
-
# Monitor#synchronize.
|
|
1366
|
-
eref_active = doc_eref_active?(elem.doc) if eref_active.nil?
|
|
1367
|
-
entity_refs, child_sequence = if eref_active
|
|
1368
|
-
lookup_entity_ref_serialization(elem)
|
|
1369
|
-
else
|
|
1370
|
-
[
|
|
1371
|
-
nil, nil
|
|
1372
|
-
]
|
|
1373
|
-
end
|
|
1374
|
-
|
|
1375
|
-
# Always use verbose format <tag></tag> for consistency with other adapters
|
|
1376
|
-
output << ">"
|
|
1377
|
-
|
|
1378
|
-
if entity_refs && child_sequence
|
|
1379
|
-
emit_eref_interleaved_children(output, elem, entity_refs, child_sequence,
|
|
1380
|
-
indent_size, depth, eref_active: eref_active)
|
|
1381
|
-
elsif elem.children?
|
|
1382
|
-
emit_children_with_layout(output, elem, indent_size, depth,
|
|
1383
|
-
eref_active: eref_active)
|
|
1384
|
-
end
|
|
1385
|
-
|
|
1386
|
-
output << "</" << name << ">"
|
|
1387
|
-
output
|
|
1388
|
-
end
|
|
1389
|
-
|
|
1390
1139
|
def doc_eref_active?(doc)
|
|
1391
1140
|
entity_ref_registry(doc).active?
|
|
1392
1141
|
end
|
|
@@ -1395,200 +1144,9 @@ module Moxml
|
|
|
1395
1144
|
EntityRefRegistry.new(attachments, doc)
|
|
1396
1145
|
end
|
|
1397
1146
|
|
|
1398
|
-
# Emit `xmlns`/`xmlns:foo` declarations onto `output`. On the root
|
|
1399
|
-
# (`include_ns: true`) we emit ALL definitions; on children we
|
|
1400
|
-
# emit only definitions that OVERRIDE a parent's same-prefix URI.
|
|
1401
|
-
# Skips the whole block when the element has no local definitions,
|
|
1402
|
-
# which is the common case for child elements in unnamespaced docs.
|
|
1403
|
-
def emit_namespace_definitions(output, elem, include_ns)
|
|
1404
|
-
return unless elem.is_a?(::LibXML::XML::Node)
|
|
1405
|
-
|
|
1406
|
-
ns_list = elem.namespaces
|
|
1407
|
-
return unless ns_list.is_a?(::LibXML::XML::Namespaces)
|
|
1408
|
-
|
|
1409
|
-
definitions = ns_list.definitions
|
|
1410
|
-
return if definitions.empty?
|
|
1411
|
-
|
|
1412
|
-
parent_ns_defs = include_ns ? nil : parent_namespace_defs(elem)
|
|
1413
|
-
seen_ns = nil
|
|
1414
|
-
|
|
1415
|
-
definitions.each do |ns|
|
|
1416
|
-
prefix = ns.prefix
|
|
1417
|
-
uri = ns.href
|
|
1418
|
-
next unless include_ns ||
|
|
1419
|
-
(parent_ns_defs&.key?(prefix) && parent_ns_defs[prefix] != uri)
|
|
1420
|
-
|
|
1421
|
-
seen_ns ||= {}
|
|
1422
|
-
next if seen_ns.key?(prefix)
|
|
1423
|
-
|
|
1424
|
-
seen_ns[prefix] = true
|
|
1425
|
-
output << format_ns_declaration(prefix, uri)
|
|
1426
|
-
end
|
|
1427
|
-
end
|
|
1428
|
-
|
|
1429
|
-
def parent_namespace_defs(elem)
|
|
1430
|
-
parent = elem.parent
|
|
1431
|
-
return nil unless parent.is_a?(::LibXML::XML::Node)
|
|
1432
|
-
|
|
1433
|
-
defs = {}
|
|
1434
|
-
parent.namespaces.each { |ns| defs[ns.prefix] = ns.href }
|
|
1435
|
-
defs
|
|
1436
|
-
end
|
|
1437
|
-
|
|
1438
|
-
def format_ns_declaration(prefix, uri)
|
|
1439
|
-
if prefix.nil? || prefix.empty?
|
|
1440
|
-
" xmlns=\"#{XmlEmitter.escape_attribute(uri)}\""
|
|
1441
|
-
else
|
|
1442
|
-
" xmlns:#{prefix}=\"#{XmlEmitter.escape_attribute(uri)}\""
|
|
1443
|
-
end
|
|
1444
|
-
end
|
|
1445
|
-
|
|
1446
|
-
def emit_attributes(output, elem)
|
|
1447
|
-
return unless elem.attributes?
|
|
1448
|
-
|
|
1449
|
-
elem.each_attr do |attr|
|
|
1450
|
-
next if attr.name.start_with?("xmlns")
|
|
1451
|
-
|
|
1452
|
-
attr_name = attr.ns&.prefix ? "#{attr.ns.prefix}:#{attr.name}" : attr.name
|
|
1453
|
-
output << " #{attr_name}=\"#{XmlEmitter.escape_attribute(attr.value)}\""
|
|
1454
|
-
end
|
|
1455
|
-
end
|
|
1456
|
-
|
|
1457
|
-
# Returns [entity_refs, child_sequence] when the element has
|
|
1458
|
-
# interleaved entity references that the serializer needs to
|
|
1459
|
-
# weave back into the native child stream — otherwise [nil, nil].
|
|
1460
|
-
#
|
|
1461
|
-
# The caller is responsible for gating this with `eref_active`
|
|
1462
|
-
# (precomputed once per `serialize` call). When `eref_active` is
|
|
1463
|
-
# false this method is never entered, so the per-element doc
|
|
1464
|
-
# attachment query never fires.
|
|
1465
|
-
def lookup_entity_ref_serialization(elem)
|
|
1466
|
-
doc = elem.doc
|
|
1467
|
-
return [nil, nil] unless doc
|
|
1468
|
-
|
|
1469
|
-
entity_ref_registry(doc).serialization_for(elem)
|
|
1470
|
-
end
|
|
1471
|
-
|
|
1472
|
-
def emit_eref_interleaved_children(output, elem, entity_refs, child_sequence,
|
|
1473
|
-
indent_size, depth, eref_active:)
|
|
1474
|
-
native_children = collect_non_blank_children(elem)
|
|
1475
|
-
child_pad = indent_size.positive? ? " " * (indent_size * (depth + 1)) : nil
|
|
1476
|
-
eref_idx = 0
|
|
1477
|
-
native_idx = 0
|
|
1478
|
-
prev_block = true
|
|
1479
|
-
|
|
1480
|
-
child_sequence.each do |type|
|
|
1481
|
-
case type
|
|
1482
|
-
when :native
|
|
1483
|
-
if native_idx < native_children.size
|
|
1484
|
-
child = native_children[native_idx]
|
|
1485
|
-
is_text_like = child.text? || child.cdata?
|
|
1486
|
-
if prev_block && !is_text_like
|
|
1487
|
-
output << "\n"
|
|
1488
|
-
output << child_pad if child_pad
|
|
1489
|
-
end
|
|
1490
|
-
prev_block = !is_text_like
|
|
1491
|
-
|
|
1492
|
-
output << serialize_child_to_xml(
|
|
1493
|
-
child, indent_size: indent_size, depth: depth,
|
|
1494
|
-
eref_active: eref_active
|
|
1495
|
-
)
|
|
1496
|
-
native_idx += 1
|
|
1497
|
-
end
|
|
1498
|
-
when :eref
|
|
1499
|
-
if eref_idx < entity_refs.size
|
|
1500
|
-
output << entity_refs[eref_idx].to_xml
|
|
1501
|
-
eref_idx += 1
|
|
1502
|
-
prev_block = false
|
|
1503
|
-
end
|
|
1504
|
-
end
|
|
1505
|
-
end
|
|
1506
|
-
end
|
|
1507
|
-
|
|
1508
1147
|
# Regex used in place of `content.to_s.strip.empty?` for whitespace-only
|
|
1509
1148
|
# text detection — `match?` allocates nothing while `.strip` makes a
|
|
1510
1149
|
# throwaway copy of every text node's content on each visit.
|
|
1511
|
-
NON_WHITESPACE_RE = /\S/
|
|
1512
|
-
private_constant :NON_WHITESPACE_RE
|
|
1513
|
-
|
|
1514
|
-
def blank_text_node?(child)
|
|
1515
|
-
child.text? && blank_content?(child.content)
|
|
1516
|
-
end
|
|
1517
|
-
|
|
1518
|
-
def blank_content?(content)
|
|
1519
|
-
content.nil? || !content.match?(NON_WHITESPACE_RE)
|
|
1520
|
-
end
|
|
1521
|
-
|
|
1522
|
-
def collect_non_blank_children(elem)
|
|
1523
|
-
children = []
|
|
1524
|
-
return children unless elem.children?
|
|
1525
|
-
|
|
1526
|
-
elem.each_child do |c|
|
|
1527
|
-
children << c unless blank_text_node?(c)
|
|
1528
|
-
end
|
|
1529
|
-
children
|
|
1530
|
-
end
|
|
1531
|
-
|
|
1532
|
-
# Walk native children once and emit them with the same newline +
|
|
1533
|
-
# indentation layout the old `add_newlines_to_xml` + `indent_xml`
|
|
1534
|
-
# post-passes produced — but in a single recursion with no string
|
|
1535
|
-
# rescanning.
|
|
1536
|
-
#
|
|
1537
|
-
# Newline rule (matching `>(?=<(?!/))` with CDATA-placeholder
|
|
1538
|
-
# protection): emit `\n` + per-level padding before a child iff
|
|
1539
|
-
# the previous emitted sibling was block-level (ended with `>`)
|
|
1540
|
-
# AND the current sibling is block-level. Text and CDATA count
|
|
1541
|
-
# as text-like and suppress the newline on both sides (the
|
|
1542
|
-
# original CDATA placeholder broke the `>...<` adjacency
|
|
1543
|
-
# symmetrically).
|
|
1544
|
-
def emit_children_with_layout(output, elem, indent_size, depth,
|
|
1545
|
-
eref_active:)
|
|
1546
|
-
child_pad = indent_size.positive? ? " " * (indent_size * (depth + 1)) : nil
|
|
1547
|
-
prev_block = true
|
|
1548
|
-
|
|
1549
|
-
elem.each_child do |child|
|
|
1550
|
-
# Cache text? — used twice per child (whitespace skip + is_text_like).
|
|
1551
|
-
# For element children (the common case) both calls return false, so
|
|
1552
|
-
# caching saves a libxml C call.
|
|
1553
|
-
is_text = child.text?
|
|
1554
|
-
next if is_text && blank_content?(child.content)
|
|
1555
|
-
|
|
1556
|
-
is_text_like = is_text || child.cdata?
|
|
1557
|
-
if prev_block && !is_text_like
|
|
1558
|
-
output << "\n"
|
|
1559
|
-
output << child_pad if child_pad
|
|
1560
|
-
end
|
|
1561
|
-
prev_block = !is_text_like
|
|
1562
|
-
|
|
1563
|
-
output << serialize_child_to_xml(child, indent_size: indent_size, depth: depth,
|
|
1564
|
-
eref_active: eref_active)
|
|
1565
|
-
end
|
|
1566
|
-
end
|
|
1567
|
-
|
|
1568
|
-
# Serialize one child node. Elements recurse into the layout-aware
|
|
1569
|
-
# path; non-element wrappers route through their own `to_xml`;
|
|
1570
|
-
# everything else falls through to the per-type serializer.
|
|
1571
|
-
# `indent_size:` and `depth:` are required to force callers to
|
|
1572
|
-
# decide whether the child should inherit the parent's indent
|
|
1573
|
-
# state — the entity-ref interleave path deliberately passes 0/0.
|
|
1574
|
-
#
|
|
1575
|
-
# Element fast-path checked first to avoid allocating a wrapper
|
|
1576
|
-
# we'd immediately throw away (elements always recurse on the
|
|
1577
|
-
# raw native node, not the wrapper). For a typical document this
|
|
1578
|
-
# skips wrapper allocation for the majority of children.
|
|
1579
|
-
def serialize_child_to_xml(child, indent_size:, depth:, eref_active:)
|
|
1580
|
-
if child.element?
|
|
1581
|
-
return serialize_element_with_namespaces(child, false, indent_size, depth + 1,
|
|
1582
|
-
eref_active: eref_active)
|
|
1583
|
-
end
|
|
1584
|
-
|
|
1585
|
-
wrapped_child = patch_node(child)
|
|
1586
|
-
if wrapped_child.is_a?(CustomizedLibxml::Node)
|
|
1587
|
-
wrapped_child.to_xml
|
|
1588
|
-
else
|
|
1589
|
-
serialize_node(child)
|
|
1590
|
-
end
|
|
1591
|
-
end
|
|
1592
1150
|
|
|
1593
1151
|
def collect_namespace_definitions(node)
|
|
1594
1152
|
ns_defs = {}
|
data/lib/moxml/materializer.rb
CHANGED
|
@@ -25,6 +25,39 @@ module Moxml
|
|
|
25
25
|
# adapters' bulk paths reference it too.
|
|
26
26
|
EMPTY_ATTRIBUTES = [].freeze
|
|
27
27
|
|
|
28
|
+
# The one constructor for the record shape — both the generic
|
|
29
|
+
# walk and adapter bulk paths build records through it, so the
|
|
30
|
+
# seven-key contract lives in exactly one place.
|
|
31
|
+
module Record
|
|
32
|
+
module_function
|
|
33
|
+
|
|
34
|
+
def element(qname:, prefix:, namespace_uri:, namespaces:, attributes:, depth:)
|
|
35
|
+
{
|
|
36
|
+
kind: :element,
|
|
37
|
+
qname: qname,
|
|
38
|
+
prefix: prefix,
|
|
39
|
+
namespace_uri: namespace_uri,
|
|
40
|
+
namespaces: namespaces,
|
|
41
|
+
attributes: attributes,
|
|
42
|
+
text: nil,
|
|
43
|
+
depth: depth,
|
|
44
|
+
}
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def text(kind:, text:, depth:)
|
|
48
|
+
{
|
|
49
|
+
kind: kind,
|
|
50
|
+
qname: nil,
|
|
51
|
+
prefix: nil,
|
|
52
|
+
namespace_uri: nil,
|
|
53
|
+
namespaces: EMPTY_ATTRIBUTES,
|
|
54
|
+
attributes: EMPTY_ATTRIBUTES,
|
|
55
|
+
text: text,
|
|
56
|
+
depth: depth,
|
|
57
|
+
}
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
28
61
|
module_function
|
|
29
62
|
|
|
30
63
|
def materialize(node, &block)
|
|
@@ -64,32 +97,21 @@ module Moxml
|
|
|
64
97
|
[attr.name, attr.value, ns&.uri, ns&.prefix]
|
|
65
98
|
end
|
|
66
99
|
ns = element.namespace
|
|
67
|
-
|
|
68
|
-
|
|
100
|
+
# declared_namespaces: the element's OWN declarations ([prefix,
|
|
101
|
+
# uri] pairs; nil prefix = default), not the in-scope set —
|
|
102
|
+
# enough for a consumer to rebuild scope while walking (#138).
|
|
103
|
+
Record.element(
|
|
69
104
|
qname: element.name,
|
|
70
105
|
prefix: element.namespace_prefix,
|
|
71
106
|
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
107
|
namespaces: element.declared_namespaces,
|
|
76
108
|
attributes: attributes,
|
|
77
|
-
text: nil,
|
|
78
109
|
depth: depth,
|
|
79
|
-
|
|
110
|
+
)
|
|
80
111
|
end
|
|
81
112
|
|
|
82
113
|
def text_record(kind, text, depth)
|
|
83
|
-
|
|
84
|
-
kind: kind,
|
|
85
|
-
qname: nil,
|
|
86
|
-
prefix: nil,
|
|
87
|
-
namespace_uri: nil,
|
|
88
|
-
namespaces: EMPTY_ATTRIBUTES,
|
|
89
|
-
attributes: EMPTY_ATTRIBUTES,
|
|
90
|
-
text: text,
|
|
91
|
-
depth: depth,
|
|
92
|
-
}
|
|
114
|
+
Record.text(kind: kind, text: text, depth: depth)
|
|
93
115
|
end
|
|
94
116
|
end
|
|
95
117
|
end
|
data/lib/moxml/version.rb
CHANGED
|
@@ -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
|