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
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Moxml
|
|
4
|
+
module Adapter
|
|
5
|
+
class Leptris
|
|
6
|
+
module Markers
|
|
7
|
+
# Marker presence is a document-level fact: parse records it,
|
|
8
|
+
# the ER builder path flips it, and the split/restore scans
|
|
9
|
+
# consult it. Customized natives only exist as split products
|
|
10
|
+
# of marker-bearing text, so they always report true.
|
|
11
|
+
def entity_bearing?(native)
|
|
12
|
+
case native
|
|
13
|
+
when CustomizedLeptris::Declaration, CustomizedLeptris::Doctype,
|
|
14
|
+
CustomizedLeptris::EntityReference, CustomizedLeptris::TextSegment,
|
|
15
|
+
CustomizedLeptris::DocumentPI
|
|
16
|
+
true
|
|
17
|
+
else
|
|
18
|
+
doc = native.document
|
|
19
|
+
doc.nil? || attachments.get(doc, :entity_markers) != false
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Expand marker-bearing text nodes into the child sequence the
|
|
24
|
+
# moxml contract exposes: text, EntityReference, text, ...
|
|
25
|
+
def split_entity_markers(natives, parent)
|
|
26
|
+
result = []
|
|
27
|
+
natives.each do |child|
|
|
28
|
+
# FFI Text#content returns a fresh unfrozen BINARY string:
|
|
29
|
+
# retag in place (dup would be a throwaway allocation), but
|
|
30
|
+
# before include? — BINARY.include? with the UTF-8 marker raises
|
|
31
|
+
if child.is_a?(::Leptris::XML::Text)
|
|
32
|
+
content = child.content
|
|
33
|
+
content.force_encoding("UTF-8")
|
|
34
|
+
end
|
|
35
|
+
if content&.include?(Entity::MARKER)
|
|
36
|
+
content.scan(/([^#{Entity::MARKER}]*)(?:#{Entity::MARKER}([\w.:-]+);)?/o) do
|
|
37
|
+
text_part = Regexp.last_match(1)
|
|
38
|
+
name = Regexp.last_match(2)
|
|
39
|
+
result << CustomizedLeptris::TextSegment.new(text_part, parent) unless text_part.empty?
|
|
40
|
+
result << CustomizedLeptris::EntityReference.new(name) if name
|
|
41
|
+
end
|
|
42
|
+
else
|
|
43
|
+
result << child
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
result
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Moxml
|
|
4
|
+
module Adapter
|
|
5
|
+
class Leptris
|
|
6
|
+
module Materialize
|
|
7
|
+
BINDING_FFI = ::Leptris::XML::FFI
|
|
8
|
+
private_constant :BINDING_FFI
|
|
9
|
+
|
|
10
|
+
EMPTY_FIELDS = Materializer::EMPTY_ATTRIBUTES
|
|
11
|
+
|
|
12
|
+
NODE_ELEMENT = ::Leptris::XML::FFI::NODE_ELEMENT
|
|
13
|
+
NODE_TEXT = ::Leptris::XML::FFI::NODE_TEXT
|
|
14
|
+
NODE_COMMENT = ::Leptris::XML::FFI::NODE_COMMENT
|
|
15
|
+
NODE_CDATA = ::Leptris::XML::FFI::NODE_CDATA
|
|
16
|
+
NODE_PI = ::Leptris::XML::FFI::NODE_PI
|
|
17
|
+
private_constant :NODE_ELEMENT, :NODE_TEXT, :NODE_COMMENT, :NODE_CDATA,
|
|
18
|
+
:NODE_PI
|
|
19
|
+
|
|
20
|
+
# Reused child-pointer staging buffer capacity for the raw
|
|
21
|
+
# pointer walk. Elements with more children grow the buffer
|
|
22
|
+
# geometrically for the rest of the stream.
|
|
23
|
+
CHILDREN_CAPACITY = 64
|
|
24
|
+
private_constant :CHILDREN_CAPACITY
|
|
25
|
+
|
|
26
|
+
# Bulk materialization (issues #132/#143): a Ruby-side
|
|
27
|
+
# post-order recursion over RAW C pointers — no binding
|
|
28
|
+
# wrapper per node, no per-node FFI callback, no depth memo
|
|
29
|
+
# (depth is a recursion parameter), no record allocation.
|
|
30
|
+
# Child lists arrive through the batch leptris_node_children
|
|
31
|
+
# call; element fields are read straight off the C handles
|
|
32
|
+
# (the binding's own first/next attribute iteration face),
|
|
33
|
+
# skipping the per-attribute wrapper its public API
|
|
34
|
+
# allocates. The unprefixed-attribute namespace read is
|
|
35
|
+
# skipped because the answer is nil by definition
|
|
36
|
+
# (no-namespace attributes).
|
|
37
|
+
def bulk_materialize?
|
|
38
|
+
true
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def materialize_fields(native, buffers, &block)
|
|
42
|
+
return nil unless Leptris::BULK_FIELD_READS
|
|
43
|
+
|
|
44
|
+
doc = native.is_a?(::Leptris::XML::Document) ? native : native.document
|
|
45
|
+
# Marker-bearing text needs the split pipeline (children-level
|
|
46
|
+
# ER expansion); the bulk path has no marker handling.
|
|
47
|
+
return nil if doc.nil? || attachments.get(doc, :entity_markers)
|
|
48
|
+
|
|
49
|
+
root_ptr = if native.is_a?(::Leptris::XML::Document)
|
|
50
|
+
doc.root&.c_ptr
|
|
51
|
+
else
|
|
52
|
+
native.c_ptr
|
|
53
|
+
end
|
|
54
|
+
return nil unless root_ptr
|
|
55
|
+
|
|
56
|
+
walk_fields(root_ptr, 0, buffers,
|
|
57
|
+
::FFI::MemoryPointer.new(:pointer, CHILDREN_CAPACITY),
|
|
58
|
+
&block)
|
|
59
|
+
true
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def walk_fields(ptr, depth, buffers, child_buf, &block)
|
|
63
|
+
f = BINDING_FFI
|
|
64
|
+
capacity = CHILDREN_CAPACITY
|
|
65
|
+
count = f.leptris_node_children(ptr, child_buf, capacity)
|
|
66
|
+
while count == capacity
|
|
67
|
+
capacity *= 4
|
|
68
|
+
child_buf = ::FFI::MemoryPointer.new(:pointer, capacity)
|
|
69
|
+
count = f.leptris_node_children(ptr, child_buf, capacity)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Snapshot before recursing — the recursion reuses this
|
|
73
|
+
# buffer for the next level's child list.
|
|
74
|
+
children = child_buf.read_array_of_pointer(count)
|
|
75
|
+
|
|
76
|
+
children.each do |child|
|
|
77
|
+
case f.leptris_node_get_type(child)
|
|
78
|
+
when NODE_ELEMENT
|
|
79
|
+
walk_fields(child, depth + 1, buffers, child_buf, &block)
|
|
80
|
+
when NODE_TEXT
|
|
81
|
+
yield(:text, nil, nil, nil, EMPTY_FIELDS, EMPTY_FIELDS, f.leptris_text_node_get_content(child), depth + 1)
|
|
82
|
+
when NODE_CDATA
|
|
83
|
+
yield(:cdata, nil, nil, nil, EMPTY_FIELDS, EMPTY_FIELDS, f.leptris_cdata_node_get_content(child), depth + 1)
|
|
84
|
+
when NODE_COMMENT
|
|
85
|
+
yield(:comment, nil, nil, nil, EMPTY_FIELDS, EMPTY_FIELDS, f.leptris_comment_node_get_content(child), depth + 1)
|
|
86
|
+
when NODE_PI
|
|
87
|
+
yield(:processing_instruction, f.leptris_pi_node_get_target(child), nil, nil, EMPTY_FIELDS, EMPTY_FIELDS, f.leptris_pi_node_get_data(child), depth + 1)
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
emit_element_fields(ptr, buffers, depth, &block)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def emit_element_fields(ptr, buffers, depth, &block)
|
|
95
|
+
f = BINDING_FFI
|
|
96
|
+
ns_buf = buffers.namespaces
|
|
97
|
+
attrs_buf = buffers.attributes
|
|
98
|
+
|
|
99
|
+
name = f.leptris_element_name(ptr)
|
|
100
|
+
prefix = f.leptris_element_prefix(ptr)
|
|
101
|
+
uri = f.leptris_element_namespace(ptr)
|
|
102
|
+
uri = nil if uri && uri.empty?
|
|
103
|
+
|
|
104
|
+
# Own declarations only — same shape as the generic path's
|
|
105
|
+
# Element#declared_namespaces (issue #138).
|
|
106
|
+
ns_buf.clear
|
|
107
|
+
i = 0
|
|
108
|
+
ns_count = f.leptris_element_namespace_count(ptr)
|
|
109
|
+
while i < ns_count
|
|
110
|
+
ns_buf << f.leptris_element_namespace_decl_prefix(ptr, i)
|
|
111
|
+
ns_buf << f.leptris_element_namespace_decl_uri(ptr, i)
|
|
112
|
+
i += 1
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
attrs_buf.clear
|
|
116
|
+
attr = f.leptris_element_first_attribute(ptr)
|
|
117
|
+
until attr.nil? || attr.null?
|
|
118
|
+
attr_name = f.leptris_attribute_get_name(attr)
|
|
119
|
+
attr_value = f.leptris_attribute_get_value(ptr, attr)
|
|
120
|
+
colon = attr_name.index(":")
|
|
121
|
+
if colon
|
|
122
|
+
attr_prefix = attr_name[0, colon]
|
|
123
|
+
# Local name + separate prefix, matching the generic
|
|
124
|
+
# path's resolver semantics (moxml's canonical shape).
|
|
125
|
+
attr_name = attr_name[(colon + 1)..]
|
|
126
|
+
attr_uri = f.leptris_attribute_namespace_uri(attr)
|
|
127
|
+
else
|
|
128
|
+
attr_prefix = nil
|
|
129
|
+
attr_uri = nil
|
|
130
|
+
end
|
|
131
|
+
attrs_buf << attr_name << attr_value << attr_uri << attr_prefix
|
|
132
|
+
attr = f.leptris_attribute_next(attr)
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
yield(:element, name, prefix, uri, ns_buf, attrs_buf, nil, depth)
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Moxml
|
|
4
|
+
module Adapter
|
|
5
|
+
class Leptris
|
|
6
|
+
# Bridge between the leptris SAX callbacks and Moxml::SAX::Handler.
|
|
7
|
+
#
|
|
8
|
+
# @private
|
|
9
|
+
class LeptrisSAXBridge < ::Leptris::XML::SAX::Document
|
|
10
|
+
include Moxml::SAX::NamespaceSplitter
|
|
11
|
+
|
|
12
|
+
def initialize(handler)
|
|
13
|
+
@handler = handler
|
|
14
|
+
super()
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def start_document
|
|
18
|
+
@handler.on_start_document
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def end_document
|
|
22
|
+
@handler.on_end_document
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def start_element(name, attrs = [])
|
|
26
|
+
attr_hash, ns_hash = split_attributes_and_namespaces(attrs)
|
|
27
|
+
@handler.on_start_element(name, attr_hash, ns_hash)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def end_element(name)
|
|
31
|
+
@handler.on_end_element(name)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def characters(string)
|
|
35
|
+
@handler.on_characters(string)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def comment(string)
|
|
39
|
+
@handler.on_comment(string)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def cdata_block(string)
|
|
43
|
+
@handler.on_cdata(string)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def processing_instruction(name, content)
|
|
47
|
+
@handler.on_processing_instruction(name, content || "")
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def error(message, _line = 0, _column = 0)
|
|
51
|
+
@handler.on_error(Moxml::ParseError.new(message))
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Moxml
|
|
4
|
+
module Adapter
|
|
5
|
+
class Leptris
|
|
6
|
+
# Wire-format knowledge: the C serializer entry, the
|
|
7
|
+
# canonicalization pass (apostrophes stay literal, optional
|
|
8
|
+
# empty-element expansion), and the literal-region scanner that
|
|
9
|
+
# keeps both from touching CDATA/comment/PI content.
|
|
10
|
+
module Serialize
|
|
11
|
+
# moxml canonical serialization: apostrophes stay literal
|
|
12
|
+
# (only & < > " are escaped) and empty elements expand when the
|
|
13
|
+
# caller asked for it — matching the other adapters' contract.
|
|
14
|
+
# Runs segment-aware: CDATA content is literal and must not be
|
|
15
|
+
# touched.
|
|
16
|
+
# Regions whose content is literal and must never be rewritten.
|
|
17
|
+
# Scanned positionally (String#index), not with a regex: the
|
|
18
|
+
# scan is linear in the input length, so pathological document
|
|
19
|
+
# content cannot blow up the serializer.
|
|
20
|
+
LITERAL_REGIONS = {
|
|
21
|
+
"<!--" => "-->",
|
|
22
|
+
"<![CDATA[" => "]]>",
|
|
23
|
+
"<?" => "?>",
|
|
24
|
+
}.freeze
|
|
25
|
+
|
|
26
|
+
# All quantifiers are possessive and the bare-part class
|
|
27
|
+
# excludes "/" and ">": the possessive groups can never consume
|
|
28
|
+
# the closing delimiter, so no backtracking is possible and the
|
|
29
|
+
# match is linear even on malformed tags.
|
|
30
|
+
EMPTY_ELEMENT_RE = %r{<([A-Za-z_][\w.:-]*+)((?:"[^"]*+"|'[^']*+'|[^<>"'/]++)*+)/>}
|
|
31
|
+
|
|
32
|
+
def serialize(node, options = {})
|
|
33
|
+
# Entity restoration belongs to the wrapper layer
|
|
34
|
+
# (Node#to_xml runs adapter.restore_entities for every
|
|
35
|
+
# adapter); doing it here scanned the output a second time.
|
|
36
|
+
xml = normalize_serialization(raw_serialize(node, options), options)
|
|
37
|
+
# The binding's FFI strings come back binary-tagged; the
|
|
38
|
+
# engine encoded the bytes per this option, so tag them.
|
|
39
|
+
xml.force_encoding(options[:encoding]) if options[:encoding]
|
|
40
|
+
xml
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def raw_serialize(node, options)
|
|
44
|
+
# CDATA must precede Text in this chain: CDATA < Text in the
|
|
45
|
+
# binding, so a Text branch first would swallow CDATA nodes.
|
|
46
|
+
case node
|
|
47
|
+
when CustomizedLeptris::Declaration, CustomizedLeptris::Doctype,
|
|
48
|
+
CustomizedLeptris::EntityReference, CustomizedLeptris::DocumentPI
|
|
49
|
+
return node.to_xml
|
|
50
|
+
when ::Leptris::XML::CDATA
|
|
51
|
+
return XmlEmitter.cdata(node.content)
|
|
52
|
+
when ::Leptris::XML::Comment
|
|
53
|
+
return "<!--#{node.content}-->"
|
|
54
|
+
when ::Leptris::XML::ProcessingInstruction
|
|
55
|
+
content = node.content.to_s
|
|
56
|
+
return content.empty? ? "<?#{node.target}?>" : "<?#{node.target} #{content}?>"
|
|
57
|
+
when ::Leptris::XML::Text, CustomizedLeptris::TextSegment
|
|
58
|
+
return XmlEmitter.escape_text(node.content.to_s)
|
|
59
|
+
when ::Leptris::XML::Document
|
|
60
|
+
return serialize_document(node, options)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
include_decl = options.fetch(:declaration) do
|
|
64
|
+
options[:no_declaration] ? false : document_has_declaration?(node)
|
|
65
|
+
end
|
|
66
|
+
xml = node.to_xml(
|
|
67
|
+
indent: options.fetch(:indent, 0),
|
|
68
|
+
no_decl: !include_decl,
|
|
69
|
+
encoding: options[:encoding],
|
|
70
|
+
)
|
|
71
|
+
# Element output always ends with the close tag — but the
|
|
72
|
+
# engine's serializer appends a stray trailing newline when
|
|
73
|
+
# the element's last text child is non-ASCII.
|
|
74
|
+
xml.sub(/\n+\z/, "")
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def normalize_serialization(xml, options)
|
|
78
|
+
needs_apos = xml.include?("'")
|
|
79
|
+
needs_expand = options[:expand_empty] && xml.include?("/>")
|
|
80
|
+
return xml unless needs_apos || needs_expand
|
|
81
|
+
|
|
82
|
+
out = +""
|
|
83
|
+
pos = 0
|
|
84
|
+
while pos < xml.length
|
|
85
|
+
opener_at, terminator = next_literal_region(xml, pos)
|
|
86
|
+
if opener_at.nil?
|
|
87
|
+
out << normalize_markup(xml[pos..], needs_apos, needs_expand)
|
|
88
|
+
break
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
out << normalize_markup(xml[pos...opener_at], needs_apos, needs_expand)
|
|
92
|
+
search_from = opener_at + opener_at_offset(terminator)
|
|
93
|
+
close = xml.index(terminator, search_from)
|
|
94
|
+
close_end = close.nil? ? xml.length : close + terminator.length
|
|
95
|
+
out << xml[opener_at...close_end]
|
|
96
|
+
pos = close_end
|
|
97
|
+
end
|
|
98
|
+
out
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Nearest literal region at/after from: [position, terminator].
|
|
102
|
+
def next_literal_region(xml, from)
|
|
103
|
+
best = nil
|
|
104
|
+
best_terminator = nil
|
|
105
|
+
LITERAL_REGIONS.each do |opener, terminator|
|
|
106
|
+
idx = xml.index(opener, from)
|
|
107
|
+
next if idx.nil?
|
|
108
|
+
|
|
109
|
+
if best.nil? || idx < best
|
|
110
|
+
best = idx
|
|
111
|
+
best_terminator = terminator
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
best.nil? ? nil : [best, best_terminator]
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Search for a terminator past its opener's overlap-safe offset
|
|
118
|
+
# ("-->" cannot start inside "<!--").
|
|
119
|
+
def opener_at_offset(terminator)
|
|
120
|
+
terminator == "-->" ? 4 : 0
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def normalize_markup(markup, needs_apos, needs_expand)
|
|
124
|
+
markup = markup.gsub("'", "'") if needs_apos
|
|
125
|
+
if needs_expand
|
|
126
|
+
markup = markup.gsub(EMPTY_ELEMENT_RE) do
|
|
127
|
+
"<#{Regexp.last_match(1)}#{Regexp.last_match(2)}></#{Regexp.last_match(1)}>"
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
markup
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|