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.
Files changed (36) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +9 -0
  3. data/docs/_pages/adapter-protocol.adoc +117 -0
  4. data/docs/_pages/adapters/index.adoc +11 -5
  5. data/docs/_pages/adapters/leptris.adoc +74 -0
  6. data/docs/_pages/adapters/libxml.adoc +1 -1
  7. data/docs/_pages/adapters/nokogiri.adoc +1 -1
  8. data/docs/_pages/conversion-apis.adoc +103 -0
  9. data/docs/_pages/index.adoc +6 -0
  10. data/lib/compat/opal/moxml_boot.rb +1 -0
  11. data/lib/moxml/adapter/base.rb +18 -4
  12. data/lib/moxml/adapter/leptris/document_parts.rb +208 -0
  13. data/lib/moxml/adapter/leptris/markers.rb +51 -0
  14. data/lib/moxml/adapter/leptris/materialize.rb +140 -0
  15. data/lib/moxml/adapter/leptris/sax_bridge.rb +56 -0
  16. data/lib/moxml/adapter/leptris/serialize.rb +135 -0
  17. data/lib/moxml/adapter/leptris.rb +58 -523
  18. data/lib/moxml/adapter/libxml/serialize.rb +497 -0
  19. data/lib/moxml/adapter/libxml.rb +20 -462
  20. data/lib/moxml/adapter/nokogiri.rb +7 -0
  21. data/lib/moxml/context.rb +8 -0
  22. data/lib/moxml/document.rb +18 -0
  23. data/lib/moxml/entity_registry.rb +0 -1
  24. data/lib/moxml/materializer.rb +108 -52
  25. data/lib/moxml/node.rb +8 -0
  26. data/lib/moxml/version.rb +1 -1
  27. data/lib/moxml.rb +3 -0
  28. data/spec/integration/shared_examples/node_wrappers/declaration_behavior.rb +3 -4
  29. data/spec/integration/shared_examples/node_wrappers/document_behavior.rb +4 -0
  30. data/spec/moxml/adapter/leptris_spec.rb +54 -11
  31. data/spec/moxml/adapter/nokogiri_spec.rb +8 -0
  32. data/spec/moxml/builder_spec.rb +8 -1
  33. data/spec/moxml/materializer_spec.rb +51 -0
  34. data/spec/moxml/signature/algorithms_spec.rb +1 -1
  35. data/spec/moxml/xpath/conversion_spec.rb +2 -6
  36. metadata +11 -2
@@ -27,12 +27,38 @@ module Moxml
27
27
  # DTD ATTLIST defaults, matching libxml2/Nokogiri semantics;
28
28
  # ParseOptions::DTDATTR opts in (leptris/leptris#606).
29
29
  DTDATTR_SUPPORTED = ::Leptris::XML::ParseOptions.const_defined?(:DTDATTR)
30
- # leptris-ruby 1.9.32 (#89): traverse is subtree-bounded again —
31
- # earlier 1.9.x walks followed the document chain and swept
32
- # following siblings, so element materialize had to fall back
33
- # to the generic wrapper walk.
34
- TRAVERSE_SUBTREE_BOUNDED =
35
- Gem::Version.new(::Leptris::VERSION) >= Gem::Version.new("1.9.32")
30
+
31
+ # The raw field-read surface for the bulk materializer (issue
32
+ # #143). Bindings without the full set fall back to the generic
33
+ # wrapper walk.
34
+ BULK_FIELD_READS = %i[
35
+ leptris_node_children leptris_node_get_type
36
+ leptris_element_name leptris_element_prefix leptris_element_namespace
37
+ leptris_element_namespace_count leptris_element_namespace_decl_prefix
38
+ leptris_element_namespace_decl_uri leptris_element_first_attribute
39
+ leptris_attribute_get_name leptris_attribute_get_value
40
+ leptris_attribute_next leptris_attribute_namespace_uri
41
+ leptris_text_node_get_content leptris_cdata_node_get_content
42
+ leptris_comment_node_get_content leptris_pi_node_get_target
43
+ leptris_pi_node_get_data
44
+ ].all? { |fn| ::Leptris::XML::FFI.respond_to?(fn) }
45
+
46
+ NO_PARSE_ERRORS = [].freeze
47
+ private_constant :NO_PARSE_ERRORS
48
+
49
+ # Cohesive clusters extracted from this class — the adapter
50
+ # protocol surface is unchanged; the modules hold the document
51
+ # parts assembly, the entity-marker pipeline, and the bulk
52
+ # materializer respectively.
53
+ autoload :DocumentParts, "moxml/adapter/leptris/document_parts"
54
+ autoload :Markers, "moxml/adapter/leptris/markers"
55
+ autoload :Materialize, "moxml/adapter/leptris/materialize"
56
+ autoload :Serialize, "moxml/adapter/leptris/serialize"
57
+ autoload :LeptrisSAXBridge, "moxml/adapter/leptris/sax_bridge"
58
+ extend Serialize
59
+ extend DocumentParts
60
+ extend Markers
61
+ extend Materialize
36
62
 
37
63
  class << self
38
64
  def attachments
@@ -63,9 +89,12 @@ module Moxml
63
89
  rescue ::Leptris::XML::ParseError => e
64
90
  # libleptris has no recovery mode that survives unclosed
65
91
  # tags; non-strict callers get an empty document, matching
66
- # the Libxml adapter's non-strict behavior.
92
+ # the Libxml adapter's non-strict behavior. The fatal
93
+ # error rides the document as parse diagnostics (issue
94
+ # #147) — otherwise nothing says why it came back empty.
67
95
  raise Moxml::ParseError.new(e.message) if options[:strict]
68
96
 
97
+ recover_errors = [e.message]
69
98
  create_document
70
99
  end
71
100
  ctx = _context || Context.new(:leptris)
@@ -73,6 +102,7 @@ module Moxml
73
102
 
74
103
  record_source_declaration(native_doc, processed)
75
104
  attachments.set(native_doc, :entity_markers, entity_markers)
105
+ attachments.set(native_doc, :parse_errors, recover_errors) if recover_errors
76
106
 
77
107
  doc
78
108
  end
@@ -85,20 +115,8 @@ module Moxml
85
115
  ::Leptris::XML::ParseOptions.dtdattr
86
116
  end
87
117
 
88
- # Issue #134: deterministic release of the C tree. The binding
89
- # clears its wrapper cache and raises UseAfterFreeError on
90
- # later access; moxml-side attachments for the document are
91
- # swept too (the context wrapper identity map self-cleans via
92
- # its size valve).
93
- DOCUMENT_ATTACHMENT_KEYS = %i[
94
- entity_markers doc_pi_nodes declaration doctype
95
- had_source_declaration document_text
96
- ].freeze
97
-
98
- def free_document(native)
99
- DOCUMENT_ATTACHMENT_KEYS.each { |key| attachments.delete(native, key) }
100
- native.free
101
- nil
118
+ def parse_errors(native_doc)
119
+ attachments.get(native_doc, :parse_errors) || NO_PARSE_ERRORS
102
120
  end
103
121
 
104
122
  def create_document(_native_doc = nil)
@@ -258,136 +276,6 @@ module Moxml
258
276
  node.target
259
277
  end
260
278
 
261
- # Marker presence is a document-level fact: parse records it,
262
- # the ER builder path flips it, and the split/restore scans
263
- # consult it. Customized natives only exist as split products
264
- # of marker-bearing text, so they always report true.
265
- def entity_bearing?(native)
266
- case native
267
- when CustomizedLeptris::Declaration, CustomizedLeptris::Doctype,
268
- CustomizedLeptris::EntityReference, CustomizedLeptris::TextSegment,
269
- CustomizedLeptris::DocumentPI
270
- true
271
- else
272
- doc = native.document
273
- doc.nil? || attachments.get(doc, :entity_markers) != false
274
- end
275
- end
276
-
277
- # Bulk materialization (issue #132): leptris_node_traverse
278
- # walks the subtree with one FFI call; the only per-node cost
279
- # is the C->Ruby callback and the property reads below. No
280
- # Moxml::Node or Attribute wrapper is allocated.
281
- def bulk_materialize?
282
- true
283
- end
284
-
285
- def materialize_records(native, &block)
286
- doc = native.is_a?(::Leptris::XML::Document) ? native : native.document
287
- # Marker-bearing text needs the split pipeline (children-level
288
- # ER expansion); the bulk path has no marker handling.
289
- return nil if doc.nil? || attachments.get(doc, :entity_markers)
290
-
291
- # On bindings whose traverse follows the document chain
292
- # (leptris 1.9.28–1.9.31), element materialize falls back to
293
- # the generic wrapper walk — only document materialization
294
- # can filter safely there (issue #140).
295
- root = if native.is_a?(::Leptris::XML::Document)
296
- doc.root
297
- else
298
- return nil unless TRAVERSE_SUBTREE_BOUNDED
299
-
300
- native
301
- end
302
- return nil if root.nil?
303
-
304
- depth_memo = {}.compare_by_identity
305
- root.traverse do |node|
306
- depth = material_depth_in_subtree(node, root, depth_memo)
307
- next if depth.nil?
308
-
309
- record = case node
310
- when ::Leptris::XML::Element
311
- element_material_record(node, depth)
312
- when ::Leptris::XML::CDATA
313
- # CDATA < Text in the binding: this arm must come
314
- # first or CDATA content reports as text.
315
- text_material_record(:cdata, node.content, depth)
316
- when ::Leptris::XML::Text
317
- text_material_record(:text, node.content, depth)
318
- when ::Leptris::XML::Comment
319
- text_material_record(:comment, node.content, depth)
320
- when ::Leptris::XML::ProcessingInstruction
321
- text_material_record(:processing_instruction, node.content, depth)
322
- .merge(qname: node.target)
323
- end
324
- yield(record) if record
325
- end
326
- true
327
- end
328
-
329
- def element_material_record(node, depth)
330
- attributes = node.each_attribute.map do |attr|
331
- # Local name + separate prefix, matching the generic
332
- # path's resolver semantics (moxml's canonical shape).
333
- name = attr.name
334
- prefix = attr.prefix
335
- name = name.split(":", 2)[1] || name if prefix
336
- [name, attr.value, attr.namespace_uri, prefix]
337
- end
338
- ns = node.namespace
339
- {
340
- kind: :element,
341
- qname: node.name,
342
- prefix: node.prefix,
343
- namespace_uri: ns&.href,
344
- # Own declarations only — same shape as the generic
345
- # path's Element#declared_namespaces (issue #138).
346
- namespaces: begin
347
- decls = node.namespace_definitions.map { |d| [d.prefix, d.href] }
348
- decls.empty? ? Materializer::EMPTY_ATTRIBUTES : decls
349
- end,
350
- attributes: attributes,
351
- text: nil,
352
- depth: depth,
353
- }
354
- end
355
-
356
- def text_material_record(kind, text, depth)
357
- {
358
- kind: kind,
359
- qname: nil,
360
- prefix: nil,
361
- namespace_uri: nil,
362
- namespaces: Materializer::EMPTY_ATTRIBUTES,
363
- attributes: Materializer::EMPTY_ATTRIBUTES,
364
- text: text,
365
- depth: depth,
366
- }
367
- end
368
-
369
- # Depth relative to the subtree root, or nil when the node
370
- # lies outside it (traverse follows the document chain since
371
- # leptris 1.9.28, so epilog siblings can appear in the
372
- # stream). Binding wrappers are address-stable and #parent is
373
- # memoized, so each edge resolves once through the
374
- # identity-keyed memo.
375
- def material_depth_in_subtree(node, root, memo)
376
- memo[node] ||= if node.equal?(root)
377
- 0
378
- else
379
- parent = node.parent
380
- if parent.nil? || parent.is_a?(::Leptris::XML::Document)
381
- nil
382
- else
383
-
384
- parent_depth = material_depth_in_subtree(parent, root, memo)
385
- parent_depth.nil? ? nil : parent_depth + 1
386
-
387
- end
388
- end
389
- end
390
-
391
279
  def node_type(node)
392
280
  # Frequency-ordered: elements and text dominate every real
393
281
  # document, and Node.wrap dispatches here once per cold
@@ -465,32 +353,6 @@ module Moxml
465
353
  end
466
354
  end
467
355
 
468
- # Expand marker-bearing text nodes into the child sequence the
469
- # moxml contract exposes: text, EntityReference, text, ...
470
- def split_entity_markers(natives, parent)
471
- result = []
472
- natives.each do |child|
473
- # FFI Text#content returns a fresh unfrozen BINARY string:
474
- # retag in place (dup would be a throwaway allocation), but
475
- # before include? — BINARY.include? with the UTF-8 marker raises
476
- if child.is_a?(::Leptris::XML::Text)
477
- content = child.content
478
- content.force_encoding("UTF-8")
479
- end
480
- if content&.include?(Entity::MARKER)
481
- content.scan(/([^#{Entity::MARKER}]*)(?:#{Entity::MARKER}([\w.:-]+);)?/o) do
482
- text_part = Regexp.last_match(1)
483
- name = Regexp.last_match(2)
484
- result << CustomizedLeptris::TextSegment.new(text_part, parent) unless text_part.empty?
485
- result << CustomizedLeptris::EntityReference.new(name) if name
486
- end
487
- else
488
- result << child
489
- end
490
- end
491
- result
492
- end
493
-
494
356
  def parent(node)
495
357
  case node
496
358
  when ::Leptris::XML::Document then nil
@@ -843,6 +705,7 @@ module Moxml
843
705
  ast = XPath::Parser.parse_with_cache(expression)
844
706
  next false if ast_contains_type?(ast, :variable)
845
707
  next false if uses_xmlns_prefix?(ast)
708
+ next false if prefixed_attribute_test?(ast)
846
709
 
847
710
  !selects_attribute_results?(ast)
848
711
  end
@@ -882,6 +745,24 @@ module Moxml
882
745
  end
883
746
  end
884
747
 
748
+ # Some released native engines do not match prefixed attribute
749
+ # tests inside predicates (@p:kind='a'); the Ruby engine does.
750
+ # An attribute test is a :test whose parent axis is
751
+ # "attribute"; bare ones (namespace nil) stay native.
752
+ def prefixed_attribute_test?(ast, parent_axis = nil)
753
+ if ast.type == :test && parent_axis == "attribute"
754
+ ns = ast.value[:namespace]
755
+ return true if ns && !ns.empty? && ns != "xmlns"
756
+ end
757
+
758
+ axis = ast.children.first if ast.type == :axis
759
+
760
+ ast.children.any? do |child|
761
+ child.is_a?(XPath::AST::Node) &&
762
+ prefixed_attribute_test?(child, axis || (child.type == :axis ? nil : parent_axis))
763
+ end
764
+ end
765
+
885
766
  def engine_xpath(node, expression, namespaces = {})
886
767
  unless node.is_a?(Moxml::Node)
887
768
  node = Moxml::Node.wrap(node, Context.new(:leptris))
@@ -909,191 +790,6 @@ module Moxml
909
790
  end
910
791
  end
911
792
 
912
- def serialize(node, options = {})
913
- # Entity restoration belongs to the wrapper layer
914
- # (Node#to_xml runs adapter.restore_entities for every
915
- # adapter); doing it here scanned the output a second time.
916
- normalize_serialization(raw_serialize(node, options), options)
917
- end
918
-
919
- def raw_serialize(node, options)
920
- # CDATA must precede Text in this chain: CDATA < Text in the
921
- # binding, so a Text branch first would swallow CDATA nodes.
922
- case node
923
- when CustomizedLeptris::Declaration, CustomizedLeptris::Doctype,
924
- CustomizedLeptris::EntityReference, CustomizedLeptris::DocumentPI
925
- return node.to_xml
926
- when ::Leptris::XML::CDATA
927
- return XmlEmitter.cdata(node.content)
928
- when ::Leptris::XML::Comment
929
- return "<!--#{node.content}-->"
930
- when ::Leptris::XML::ProcessingInstruction
931
- content = node.content.to_s
932
- return content.empty? ? "<?#{node.target}?>" : "<?#{node.target} #{content}?>"
933
- when ::Leptris::XML::Text, CustomizedLeptris::TextSegment
934
- return XmlEmitter.escape_text(node.content.to_s)
935
- when ::Leptris::XML::Document
936
- return serialize_document(node, options)
937
- end
938
-
939
- include_decl = options.fetch(:declaration) do
940
- options[:no_declaration] ? false : document_has_declaration?(node)
941
- end
942
- node.to_xml(
943
- indent: options.fetch(:indent, 0),
944
- no_decl: !include_decl,
945
- encoding: options[:encoding],
946
- )
947
- end
948
-
949
- # moxml canonical serialization: apostrophes stay literal
950
- # (only & < > " are escaped) and empty elements expand when the
951
- # caller asked for it — matching the other adapters' contract.
952
- # Runs segment-aware: CDATA content is literal and must not be
953
- # touched.
954
- # Regions whose content is literal and must never be rewritten.
955
- # Scanned positionally (String#index), not with a regex: the
956
- # scan is linear in the input length, so pathological document
957
- # content cannot blow up the serializer.
958
- LITERAL_REGIONS = {
959
- "<!--" => "-->",
960
- "<![CDATA[" => "]]>",
961
- "<?" => "?>",
962
- }.freeze
963
-
964
- def normalize_serialization(xml, options)
965
- needs_apos = xml.include?("&apos;")
966
- needs_expand = options[:expand_empty] && xml.include?("/>")
967
- return xml unless needs_apos || needs_expand
968
-
969
- out = +""
970
- pos = 0
971
- while pos < xml.length
972
- opener_at, terminator = next_literal_region(xml, pos)
973
- if opener_at.nil?
974
- out << normalize_markup(xml[pos..], needs_apos, needs_expand)
975
- break
976
- end
977
-
978
- out << normalize_markup(xml[pos...opener_at], needs_apos, needs_expand)
979
- search_from = opener_at + opener_at_offset(terminator)
980
- close = xml.index(terminator, search_from)
981
- close_end = close.nil? ? xml.length : close + terminator.length
982
- out << xml[opener_at...close_end]
983
- pos = close_end
984
- end
985
- out
986
- end
987
-
988
- # Nearest literal region at/after from: [position, terminator].
989
- def next_literal_region(xml, from)
990
- best = nil
991
- best_terminator = nil
992
- LITERAL_REGIONS.each do |opener, terminator|
993
- idx = xml.index(opener, from)
994
- next if idx.nil?
995
-
996
- if best.nil? || idx < best
997
- best = idx
998
- best_terminator = terminator
999
- end
1000
- end
1001
- best.nil? ? nil : [best, best_terminator]
1002
- end
1003
-
1004
- # Search for a terminator past its opener's overlap-safe offset
1005
- # ("-->" cannot start inside "<!--").
1006
- def opener_at_offset(terminator)
1007
- terminator == "-->" ? 4 : 0
1008
- end
1009
-
1010
- # All quantifiers are possessive and the bare-part class
1011
- # excludes "/" and ">": the possessive groups can never consume
1012
- # the closing delimiter, so no backtracking is possible and the
1013
- # match is linear even on malformed tags.
1014
- EMPTY_ELEMENT_RE = %r{<([A-Za-z_][\w.:-]*+)((?:"[^"]*+"|'[^']*+'|[^<>"'/]++)*+)/>}
1015
-
1016
- def normalize_markup(markup, needs_apos, needs_expand)
1017
- markup = markup.gsub("&apos;", "'") if needs_apos
1018
- if needs_expand
1019
- markup = markup.gsub(EMPTY_ELEMENT_RE) do
1020
- "<#{Regexp.last_match(1)}#{Regexp.last_match(2)}></#{Regexp.last_match(1)}>"
1021
- end
1022
- end
1023
- markup
1024
- end
1025
-
1026
- # Documents compose from their parts: the native serializer
1027
- # only walks the root subtree, so declaration, DOCTYPE, PIs and
1028
- # document-level text are assembled around it explicitly.
1029
- def serialize_document(doc, options)
1030
- # Nokogiri's document shape: every top-level part is
1031
- # newline-terminated, at any indent — declaration, DOCTYPE,
1032
- # document PIs, the root element, trailing newline after it.
1033
- # Document-level text is content, not structure: no added
1034
- # newline.
1035
- parts = []
1036
-
1037
- include_decl = !options[:no_declaration] && options.fetch(:declaration) do
1038
- document_has_declaration?(doc)
1039
- end
1040
- if include_decl
1041
- declaration = attachments.get(doc, :declaration)
1042
- parts << (declaration ? declaration.to_xml : default_declaration_xml(doc, options)) << "\n"
1043
- end
1044
-
1045
- doctype = attachments.get(doc, :doctype)
1046
- parts << doctype.to_xml << "\n" if doctype
1047
-
1048
- native = native_doctype_xml(doc)
1049
- parts << native << "\n" if native
1050
-
1051
- if DOC_NODE_SUPPORTED
1052
- # The libxml2-model document node: prolog PIs/comments,
1053
- # the root, epilog PIs/comments — in document order, so
1054
- # epilog parts serialize after the root (issue #130).
1055
- doc_children = document_node_children(doc)
1056
- if doc_children
1057
- doc_children.each { |child| parts << raw_serialize(child, options) << "\n" }
1058
- else
1059
- document_pi_nodes(doc).each { |pi| parts << pi.to_xml << "\n" }
1060
- parts << raw_serialize(doc.root, options) << "\n" if doc.root
1061
- end
1062
- else
1063
- document_pi_nodes(doc).each { |pi| parts << pi.to_xml << "\n" }
1064
-
1065
- parts << raw_serialize(doc.root, options) << "\n" if doc.root
1066
- end
1067
-
1068
- texts = attachments.get(doc, :document_text)
1069
- texts&.each { |text| parts << XmlEmitter.escape_text(text.content.to_s) }
1070
-
1071
- parts.join
1072
- end
1073
-
1074
- def default_declaration_xml(doc, options)
1075
- encoding = options[:encoding] || doc.encoding
1076
- encoding = "UTF-8" if encoding.to_s.empty?
1077
- XmlEmitter.declaration_xml("1.0", encoding, nil)
1078
- end
1079
-
1080
- def marker_text_for(parent, name)
1081
- return nil unless parent.is_a?(::Leptris::XML::Element)
1082
-
1083
- marker = "#{Entity::MARKER}#{name};"
1084
- parent.children.to_a.find do |child|
1085
- child.is_a?(::Leptris::XML::Text) && child.content == marker
1086
- end
1087
- end
1088
-
1089
- def native_doctype_xml(doc)
1090
- dt = doc.doctype
1091
- return nil unless dt
1092
-
1093
- subset = dt.internal_subset if dt.class.method_defined?(:internal_subset)
1094
- XmlEmitter.doctype_xml(dt.root_name, dt.public_id, dt.system_id, subset)
1095
- end
1096
-
1097
793
  def sax_parse(xml, handler)
1098
794
  bridge = LeptrisSAXBridge.new(handler)
1099
795
  xml_string = xml.is_a?(IO) || xml.is_a?(::StringIO) ? xml.read : xml.to_s
@@ -1123,167 +819,6 @@ module Moxml
1123
819
  end
1124
820
  attachments.set(native_doc, :had_source_declaration, had)
1125
821
  end
1126
-
1127
- def document_has_declaration?(native)
1128
- return false unless native.is_a?(::Leptris::XML::Document)
1129
-
1130
- return true if attachments.get(native, :declaration)
1131
-
1132
- attachments.get(native, :had_source_declaration) ? true : false
1133
- end
1134
-
1135
- # Document-level PI pseudo-nodes, materialized once from the C
1136
- # list and cached per document: children and serialization read
1137
- # the same objects, so wrapper mutations round-trip. Build the
1138
- # cache BEFORE appending a PI with add_pi, or the C-side
1139
- # addition would be double-counted.
1140
- # The document node's children, or nil when the node does not
1141
- # reflect reality: parsed documents always list the root
1142
- # element among their children, but programmatically built
1143
- # ones do not (binding gap) — those keep the legacy parts
1144
- # path.
1145
- def document_node_children(doc)
1146
- doc_children = doc.children.to_a
1147
- has_root = doc_children.any?(::Leptris::XML::Element)
1148
- return doc_children if has_root
1149
- return doc_children if doc.root.nil?
1150
-
1151
- nil
1152
- end
1153
-
1154
- def document_pi_nodes(doc)
1155
- attachments.get(doc, :doc_pi_nodes) || begin
1156
- nodes = doc.processing_instructions.map do |(target, data)|
1157
- CustomizedLeptris::DocumentPI.new(target, data, doc)
1158
- end
1159
- attachments.set(doc, :doc_pi_nodes, nodes)
1160
- nodes
1161
- end
1162
- end
1163
-
1164
- def assemble_document_children(doc)
1165
- children = []
1166
-
1167
- native_doctype = doc.doctype
1168
- children << native_doctype if native_doctype
1169
-
1170
- doctype_wrapper = attachments.get(doc, :doctype)
1171
- children << doctype_wrapper if doctype_wrapper
1172
-
1173
- if DOC_NODE_SUPPORTED
1174
- # The libxml2-model document node lists prolog PIs/comments,
1175
- # the root, and epilog PIs/comments in document order —
1176
- # the Nokogiri-shaped contract, epilog anchoring included
1177
- # (issue #130). Built (programmatic) documents are not yet
1178
- # fully reflected by the node (binding gap: an attached
1179
- # root does not appear); document_node_children answers
1180
- # nil there for the legacy parts path.
1181
- doc_children = document_node_children(doc)
1182
- if doc_children
1183
- children.concat(doc_children)
1184
- else
1185
- children.concat(document_pi_nodes(doc))
1186
- children << doc.root if doc.root
1187
- end
1188
- else
1189
- # Legacy path: document-level PIs live outside the element
1190
- # tree in a flat pre-root list (libleptris < 1.9.7 C
1191
- # model); epilog anchoring is not representable there.
1192
- children.concat(document_pi_nodes(doc))
1193
-
1194
- children << doc.root if doc.root
1195
- end
1196
-
1197
- texts = attachments.get(doc, :document_text)
1198
- children.concat(texts) if texts
1199
- children
1200
- end
1201
-
1202
- def add_document_child(doc, child)
1203
- case child
1204
- when CustomizedLeptris::Declaration
1205
- child.parent_doc = doc
1206
- attachments.set(doc, :declaration, child)
1207
- when CustomizedLeptris::Doctype
1208
- child.parent_doc = doc
1209
- attachments.set(doc, :doctype, child)
1210
- when ::Leptris::XML::DocType
1211
- raise Moxml::DocumentStructureError.new(
1212
- "libleptris does not support attaching a native DocType to a document",
1213
- )
1214
- when ::Leptris::XML::Element
1215
- doc.root = child
1216
- when ::Leptris::XML::ProcessingInstruction
1217
- document_pi_nodes(doc)
1218
- doc.add_pi(child.target, child.content.to_s)
1219
- document_pi_nodes(doc) << CustomizedLeptris::DocumentPI.new(
1220
- child.target, child.content.to_s, doc
1221
- )
1222
- when CustomizedLeptris::DocumentPI
1223
- document_pi_nodes(doc)
1224
- doc.add_pi(child.target, child.data)
1225
- document_pi_nodes(doc) << child
1226
- when ::Leptris::XML::Text
1227
- texts = attachments.get(doc, :document_text) || []
1228
- texts << child
1229
- attachments.set(doc, :document_text, texts)
1230
- child
1231
- else
1232
- raise Moxml::DocumentStructureError.new(
1233
- "Unsupported document child: #{child.class}",
1234
- )
1235
- end
1236
- child
1237
- end
1238
- end
1239
-
1240
- # Bridge between the leptris SAX callbacks and Moxml::SAX::Handler.
1241
- #
1242
- # @private
1243
- class LeptrisSAXBridge < ::Leptris::XML::SAX::Document
1244
- include Moxml::SAX::NamespaceSplitter
1245
-
1246
- def initialize(handler)
1247
- @handler = handler
1248
- super()
1249
- end
1250
-
1251
- def start_document
1252
- @handler.on_start_document
1253
- end
1254
-
1255
- def end_document
1256
- @handler.on_end_document
1257
- end
1258
-
1259
- def start_element(name, attrs = [])
1260
- attr_hash, ns_hash = split_attributes_and_namespaces(attrs)
1261
- @handler.on_start_element(name, attr_hash, ns_hash)
1262
- end
1263
-
1264
- def end_element(name)
1265
- @handler.on_end_element(name)
1266
- end
1267
-
1268
- def characters(string)
1269
- @handler.on_characters(string)
1270
- end
1271
-
1272
- def comment(string)
1273
- @handler.on_comment(string)
1274
- end
1275
-
1276
- def cdata_block(string)
1277
- @handler.on_cdata(string)
1278
- end
1279
-
1280
- def processing_instruction(name, content)
1281
- @handler.on_processing_instruction(name, content || "")
1282
- end
1283
-
1284
- def error(message, _line = 0, _column = 0)
1285
- @handler.on_error(Moxml::ParseError.new(message))
1286
- end
1287
822
  end
1288
823
  end
1289
824
  end