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.
@@ -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
- # Include DOCTYPE if present
320
- doctype_wrapper = attachments.get(native_node, :doctype)
321
- result << doctype_wrapper if doctype_wrapper
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="&#xA9;"); 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("&apos;", "'")
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 = {}
@@ -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
- kind: :element,
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Moxml
4
- VERSION = "0.5.12"
4
+ VERSION = "0.5.13"
5
5
  end
@@ -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 "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.
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
- expect { pi.target = "renamed" }.to raise_error(Leptris::XML::Error)
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