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
|
@@ -34,6 +34,20 @@ module Moxml
|
|
|
34
34
|
TRAVERSE_SUBTREE_BOUNDED =
|
|
35
35
|
Gem::Version.new(::Leptris::VERSION) >= Gem::Version.new("1.9.32")
|
|
36
36
|
|
|
37
|
+
# Cohesive clusters extracted from this class — the adapter
|
|
38
|
+
# protocol surface is unchanged; the modules hold the document
|
|
39
|
+
# parts assembly, the entity-marker pipeline, and the bulk
|
|
40
|
+
# materializer respectively.
|
|
41
|
+
autoload :DocumentParts, "moxml/adapter/leptris/document_parts"
|
|
42
|
+
autoload :Markers, "moxml/adapter/leptris/markers"
|
|
43
|
+
autoload :Materialize, "moxml/adapter/leptris/materialize"
|
|
44
|
+
autoload :Serialize, "moxml/adapter/leptris/serialize"
|
|
45
|
+
autoload :LeptrisSAXBridge, "moxml/adapter/leptris/sax_bridge"
|
|
46
|
+
extend Serialize
|
|
47
|
+
extend DocumentParts
|
|
48
|
+
extend Markers
|
|
49
|
+
extend Materialize
|
|
50
|
+
|
|
37
51
|
class << self
|
|
38
52
|
def attachments
|
|
39
53
|
@attachments ||= Moxml::NativeAttachment.new
|
|
@@ -85,22 +99,6 @@ module Moxml
|
|
|
85
99
|
::Leptris::XML::ParseOptions.dtdattr
|
|
86
100
|
end
|
|
87
101
|
|
|
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
|
|
102
|
-
end
|
|
103
|
-
|
|
104
102
|
def create_document(_native_doc = nil)
|
|
105
103
|
::Leptris::XML::Document.create
|
|
106
104
|
end
|
|
@@ -258,136 +256,6 @@ module Moxml
|
|
|
258
256
|
node.target
|
|
259
257
|
end
|
|
260
258
|
|
|
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
259
|
def node_type(node)
|
|
392
260
|
# Frequency-ordered: elements and text dominate every real
|
|
393
261
|
# document, and Node.wrap dispatches here once per cold
|
|
@@ -465,32 +333,6 @@ module Moxml
|
|
|
465
333
|
end
|
|
466
334
|
end
|
|
467
335
|
|
|
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
336
|
def parent(node)
|
|
495
337
|
case node
|
|
496
338
|
when ::Leptris::XML::Document then nil
|
|
@@ -909,191 +751,6 @@ module Moxml
|
|
|
909
751
|
end
|
|
910
752
|
end
|
|
911
753
|
|
|
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?("'")
|
|
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("'", "'") 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
754
|
def sax_parse(xml, handler)
|
|
1098
755
|
bridge = LeptrisSAXBridge.new(handler)
|
|
1099
756
|
xml_string = xml.is_a?(IO) || xml.is_a?(::StringIO) ? xml.read : xml.to_s
|
|
@@ -1123,167 +780,6 @@ module Moxml
|
|
|
1123
780
|
end
|
|
1124
781
|
attachments.set(native_doc, :had_source_declaration, had)
|
|
1125
782
|
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
783
|
end
|
|
1288
784
|
end
|
|
1289
785
|
end
|