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.
- checksums.yaml +4 -4
- data/.rubocop.yml +9 -0
- data/docs/_pages/adapter-protocol.adoc +117 -0
- data/docs/_pages/adapters/index.adoc +11 -5
- data/docs/_pages/adapters/leptris.adoc +74 -0
- data/docs/_pages/adapters/libxml.adoc +1 -1
- data/docs/_pages/adapters/nokogiri.adoc +1 -1
- data/docs/_pages/conversion-apis.adoc +103 -0
- data/docs/_pages/index.adoc +6 -0
- data/lib/compat/opal/moxml_boot.rb +1 -0
- data/lib/moxml/adapter/base.rb +18 -4
- 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 +140 -0
- data/lib/moxml/adapter/leptris/sax_bridge.rb +56 -0
- data/lib/moxml/adapter/leptris/serialize.rb +135 -0
- data/lib/moxml/adapter/leptris.rb +58 -523
- data/lib/moxml/adapter/libxml/serialize.rb +497 -0
- data/lib/moxml/adapter/libxml.rb +20 -462
- data/lib/moxml/adapter/nokogiri.rb +7 -0
- data/lib/moxml/context.rb +8 -0
- data/lib/moxml/document.rb +18 -0
- data/lib/moxml/entity_registry.rb +0 -1
- data/lib/moxml/materializer.rb +108 -52
- data/lib/moxml/node.rb +8 -0
- data/lib/moxml/version.rb +1 -1
- data/lib/moxml.rb +3 -0
- data/spec/integration/shared_examples/node_wrappers/declaration_behavior.rb +3 -4
- data/spec/integration/shared_examples/node_wrappers/document_behavior.rb +4 -0
- data/spec/moxml/adapter/leptris_spec.rb +54 -11
- data/spec/moxml/adapter/nokogiri_spec.rb +8 -0
- data/spec/moxml/builder_spec.rb +8 -1
- data/spec/moxml/materializer_spec.rb +51 -0
- data/spec/moxml/signature/algorithms_spec.rb +1 -1
- data/spec/moxml/xpath/conversion_spec.rb +2 -6
- 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
|
-
|
|
31
|
-
#
|
|
32
|
-
#
|
|
33
|
-
#
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
89
|
-
|
|
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?("'")
|
|
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
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
|