moxml 0.1.26 → 0.3.0
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/.github/workflows/windows.yml +62 -0
- data/Gemfile +3 -1
- data/README.adoc +69 -6
- data/lib/compat/opal/moxml_boot.rb +6 -1
- data/lib/moxml/adapter/base.rb +53 -56
- data/lib/moxml/adapter/customized_leptris/declaration.rb +32 -0
- data/lib/moxml/adapter/customized_leptris/doctype.rb +31 -0
- data/lib/moxml/adapter/customized_leptris/entity_reference.rb +12 -0
- data/lib/moxml/adapter/customized_leptris/text_segment.rb +26 -0
- data/lib/moxml/adapter/customized_leptris.rb +16 -0
- data/lib/moxml/adapter/customized_libxml/cdata.rb +3 -12
- data/lib/moxml/adapter/customized_libxml/comment.rb +2 -9
- data/lib/moxml/adapter/customized_libxml/declaration.rb +1 -10
- data/lib/moxml/adapter/customized_libxml/node.rb +8 -0
- data/lib/moxml/adapter/customized_libxml/processing_instruction.rb +2 -10
- data/lib/moxml/adapter/customized_libxml/text.rb +2 -9
- data/lib/moxml/adapter/customized_oga/entity_decoder.rb +37 -0
- data/lib/moxml/adapter/customized_oga/raw_value_override.rb +52 -0
- data/lib/moxml/adapter/customized_oga/xml_generator.rb +4 -34
- data/lib/moxml/adapter/customized_oga.rb +2 -0
- data/lib/moxml/adapter/customized_ox/entity_reference.rb +1 -17
- data/lib/moxml/adapter/customized_rexml/entity_reference.rb +1 -11
- data/lib/moxml/adapter/headed_ox.rb +9 -128
- data/lib/moxml/adapter/leptris.rb +965 -0
- data/lib/moxml/adapter/libxml.rb +137 -117
- data/lib/moxml/adapter/nokogiri.rb +29 -8
- data/lib/moxml/adapter/oga.rb +139 -55
- data/lib/moxml/adapter/ox.rb +93 -137
- data/lib/moxml/adapter/rexml.rb +64 -51
- data/lib/moxml/adapter.rb +2 -1
- data/lib/moxml/attribute.rb +8 -4
- data/lib/moxml/attribute_resolver.rb +189 -0
- data/lib/moxml/builder.rb +0 -18
- data/lib/moxml/config.rb +29 -5
- data/lib/moxml/context.rb +33 -0
- data/lib/moxml/document.rb +1 -1
- data/lib/moxml/element.rb +29 -12
- data/lib/moxml/entity/reference.rb +28 -0
- data/lib/moxml/entity/restorer.rb +95 -0
- data/lib/moxml/entity.rb +126 -0
- data/lib/moxml/entity_registry.rb +1 -1
- data/lib/moxml/error.rb +0 -42
- data/lib/moxml/node.rb +135 -10
- data/lib/moxml/node_set.rb +1 -1
- data/lib/moxml/sax/namespace_splitter.rb +5 -2
- data/lib/moxml/signature/model/key/dsa_key_value.rb +1 -2
- data/lib/moxml/version.rb +1 -1
- data/lib/moxml/xml_emitter.rb +71 -0
- data/lib/moxml/xpath/compiler.rb +143 -8
- data/lib/moxml/xpath/parser.rb +22 -15
- data/lib/moxml.rb +3 -3
- data/spec/examples/readme_examples_spec.rb +0 -4
- data/spec/examples/xpath_examples_spec.rb +0 -11
- data/spec/integration/all_adapters_spec.rb +18 -1
- data/spec/integration/sax_parity_spec.rb +58 -0
- data/spec/integration/shared_examples/edge_cases.rb +0 -10
- data/spec/integration/shared_examples/high_level/{document_builder_behavior.rb → parse_behavior.rb} +1 -2
- data/spec/integration/shared_examples/integration_workflows.rb +0 -10
- data/spec/integration/shared_examples/node_wrappers/attribute_behavior.rb +98 -0
- data/spec/integration/shared_examples/node_wrappers/namespace_behavior.rb +26 -0
- data/spec/integration/shared_examples/node_wrappers/node_behavior.rb +15 -8
- data/spec/moxml/adapter/leptris_spec.rb +75 -0
- data/spec/moxml/adapter/libxml_internals_spec.rb +1 -1
- data/spec/moxml/adapter/ox_spec.rb +20 -6
- data/spec/moxml/adapter/platform_spec.rb +15 -2
- data/spec/moxml/adapter/shared_examples/adapter_contract.rb +180 -0
- data/spec/moxml/attribute_resolver_spec.rb +108 -0
- data/spec/moxml/config_spec.rb +0 -1
- data/spec/moxml/context_spec.rb +22 -0
- data/spec/moxml/doctype_spec.rb +1 -1
- data/spec/moxml/entity_spec.rb +76 -0
- data/spec/moxml/error_spec.rb +0 -50
- data/spec/moxml/node_spec.rb +104 -0
- data/spec/moxml/sax_entity_parity_spec.rb +358 -0
- data/spec/moxml/xml_emitter_spec.rb +64 -0
- data/spec/moxml/xpath/axes_spec.rb +112 -0
- data/spec/moxml/xpath/parser_spec.rb +6 -0
- data/spec/moxml/xpath_capabilities_spec.rb +5 -3
- data/spec/performance/benchmark_spec.rb +13 -6
- data/spec/performance/thread_safety_spec.rb +0 -4
- data/spec/spec_helper.rb +5 -1
- metadata +23 -6
- data/lib/moxml/adapter/libxml/entity_restorer.rb +0 -94
- data/lib/moxml/document_builder.rb +0 -167
- data/spec/moxml/document_builder_spec.rb +0 -30
|
@@ -0,0 +1,965 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
return if RUBY_ENGINE == "opal"
|
|
4
|
+
|
|
5
|
+
require "stringio"
|
|
6
|
+
require "leptris"
|
|
7
|
+
# leptris-ruby 1.9.0 regression (leptris-ruby#53): the eager ffi load
|
|
8
|
+
# shadows the Leptris::XML autoload manifest, leaving Document/Element
|
|
9
|
+
# unreachable. Loading the manifest explicitly is a no-op when the gem
|
|
10
|
+
# is fixed.
|
|
11
|
+
require "leptris/xml"
|
|
12
|
+
|
|
13
|
+
module Moxml
|
|
14
|
+
module Adapter
|
|
15
|
+
# Adapter over the leptris FFI binding (libleptris C library).
|
|
16
|
+
#
|
|
17
|
+
# libleptris provides DOM parsing, a native XPath 1.0 engine, SAX,
|
|
18
|
+
# and serialization. It has no first-class XML declaration or
|
|
19
|
+
# programmatic DOCTYPE, so those live in CustomizedLeptris value
|
|
20
|
+
# objects stored through NativeAttachment.
|
|
21
|
+
class Leptris < Base
|
|
22
|
+
class << self
|
|
23
|
+
def attachments
|
|
24
|
+
@attachments ||= Moxml::NativeAttachment.new
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def set_root(doc, element)
|
|
28
|
+
doc.root = element
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def parse(xml, options = {}, _context = nil)
|
|
32
|
+
xml_string = xml.is_a?(IO) || xml.is_a?(StringIO) ? xml.read : xml.to_s
|
|
33
|
+
processed = preprocess_entities(xml_string)
|
|
34
|
+
|
|
35
|
+
native_doc = begin
|
|
36
|
+
::Leptris::XML::Document.parse(processed)
|
|
37
|
+
rescue ::Leptris::XML::ParseError => e
|
|
38
|
+
# libleptris has no recovery mode that survives unclosed
|
|
39
|
+
# tags; non-strict callers get an empty document, matching
|
|
40
|
+
# the Libxml adapter's non-strict behavior.
|
|
41
|
+
raise Moxml::ParseError.new(e.message) if options[:strict]
|
|
42
|
+
|
|
43
|
+
create_document
|
|
44
|
+
end
|
|
45
|
+
ctx = _context || Context.new(:leptris)
|
|
46
|
+
doc = Document.new(native_doc, ctx)
|
|
47
|
+
|
|
48
|
+
record_source_declaration(native_doc, processed)
|
|
49
|
+
|
|
50
|
+
doc
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def create_document(_native_doc = nil)
|
|
54
|
+
::Leptris::XML::Document.create
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def create_native_element(name, owner_doc = nil)
|
|
58
|
+
(owner_doc || create_document).create_element(name.to_s)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def create_native_text(content, owner_doc = nil)
|
|
62
|
+
(owner_doc || create_document).create_text_node(content)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def create_native_cdata(content, owner_doc = nil)
|
|
66
|
+
(owner_doc || create_document).create_cdata(content)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def create_native_comment(content, owner_doc = nil)
|
|
70
|
+
(owner_doc || create_document).create_comment(content)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def create_native_processing_instruction(target, content)
|
|
74
|
+
doc = create_document
|
|
75
|
+
doc.create_processing_instruction(target.to_s, content.to_s)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def create_native_doctype(name, external_id, system_id)
|
|
79
|
+
CustomizedLeptris::Doctype.new(name, external_id, system_id)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def create_native_declaration(version, encoding, standalone)
|
|
83
|
+
CustomizedLeptris::Declaration.new(version, encoding, standalone)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def create_native_entity_reference(name)
|
|
87
|
+
CustomizedLeptris::EntityReference.new(name)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def entity_reference_name(node)
|
|
91
|
+
node.name if node.is_a?(CustomizedLeptris::EntityReference)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def declaration_attribute(declaration, attr_name)
|
|
95
|
+
declaration.public_send(attr_name) if attr_matches?(attr_name)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def set_declaration_attribute(declaration, attr_name, value)
|
|
99
|
+
declaration.public_send("#{attr_name}=", value) if attr_matches?(attr_name)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def attr_matches?(attr_name)
|
|
103
|
+
%w[version encoding standalone].include?(attr_name.to_s)
|
|
104
|
+
end
|
|
105
|
+
private :attr_matches?
|
|
106
|
+
|
|
107
|
+
def has_declaration?(native_doc, _wrapper)
|
|
108
|
+
return true if attachments.get(native_doc, :declaration)
|
|
109
|
+
|
|
110
|
+
attachments.key?(native_doc, :had_source_declaration) &&
|
|
111
|
+
attachments.get(native_doc, :had_source_declaration)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def remove_declaration(native_doc)
|
|
115
|
+
attachments.delete(native_doc, :declaration)
|
|
116
|
+
attachments.delete(native_doc, :had_source_declaration)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def create_native_namespace(element, prefix, uri)
|
|
120
|
+
element.add_namespace_definition(prefix, uri)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def set_namespace(node, namespace)
|
|
124
|
+
return set_attribute_namespace(node, namespace) if node.is_a?(::Leptris::XML::Attr)
|
|
125
|
+
|
|
126
|
+
element = node
|
|
127
|
+
prefix = namespace.is_a?(String) ? nil : namespace.prefix
|
|
128
|
+
uri = namespace.is_a?(String) ? namespace : namespace.href
|
|
129
|
+
if prefix.nil? || prefix.empty?
|
|
130
|
+
element.default_namespace = uri.to_s
|
|
131
|
+
# Drop any prefix from the element name: a default namespace
|
|
132
|
+
# never applies to a prefixed name.
|
|
133
|
+
element.name = element.name.split(":", 2)[-1] if element.name.include?(":")
|
|
134
|
+
else
|
|
135
|
+
element.add_namespace_definition(prefix, uri.to_s)
|
|
136
|
+
element.name = "#{prefix}:#{element.name.split(':', 2)[-1]}"
|
|
137
|
+
end
|
|
138
|
+
element
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# Attr is an immutable value object: namespace changes go
|
|
142
|
+
# through the owning element, recreating the attribute with a
|
|
143
|
+
# qualified name, and yield a fresh native for the wrapper.
|
|
144
|
+
def set_attribute_namespace(attr, namespace)
|
|
145
|
+
element = attr.element
|
|
146
|
+
local = attr.name.include?(":") ? attr.name.split(":", 2)[1] : attr.name
|
|
147
|
+
value = attr.value
|
|
148
|
+
element.remove_attribute(attr.name)
|
|
149
|
+
prefix = namespace.is_a?(String) ? nil : namespace.prefix
|
|
150
|
+
uri = namespace.is_a?(String) ? namespace : namespace.href
|
|
151
|
+
qualified = prefix.nil? || prefix.empty? ? local : "#{prefix}:#{local}"
|
|
152
|
+
element[qualified] = value
|
|
153
|
+
if prefix && !prefix.empty? && !uri.to_s.empty? && !resolve_prefix_ns(element, prefix)
|
|
154
|
+
element.add_namespace_definition(prefix, uri.to_s)
|
|
155
|
+
end
|
|
156
|
+
element.attribute_nodes.reverse.find { |candidate| candidate.name == qualified }
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def namespace(node)
|
|
160
|
+
# The binding resolves Attr#namespace to the declaration but
|
|
161
|
+
# without the prefix; when the qualified name carries one,
|
|
162
|
+
# prefer the in-scope declaration that binds it so wrappers
|
|
163
|
+
# keep prefix and uri.
|
|
164
|
+
if node.is_a?(::Leptris::XML::Attr) &&
|
|
165
|
+
(prefix = node.prefix || prefix_part(node.name))
|
|
166
|
+
resolved = resolve_prefix_ns(node.element, prefix)
|
|
167
|
+
return resolved if resolved
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
ns = node.namespace
|
|
171
|
+
return ns unless ns.nil?
|
|
172
|
+
|
|
173
|
+
# Set-side namespaces (created attributes, renamed elements)
|
|
174
|
+
# carry a prefix the native resolver did not bind; resolve
|
|
175
|
+
# through the scope chain so wrapper-level access stays
|
|
176
|
+
# XML-correct.
|
|
177
|
+
owner = node.is_a?(::Leptris::XML::Attr) ? node.element : node
|
|
178
|
+
prefix = if node.is_a?(::Leptris::XML::Attr)
|
|
179
|
+
node.prefix || prefix_part(node.name)
|
|
180
|
+
else
|
|
181
|
+
prefix_part(node.name)
|
|
182
|
+
end
|
|
183
|
+
return nil if prefix.nil?
|
|
184
|
+
|
|
185
|
+
resolve_prefix_ns(owner, prefix)
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
# Nearest in-scope declaration of prefix, walking owner then
|
|
189
|
+
# ancestors. Unbound prefix → nil. Returns the Namespace object
|
|
190
|
+
# so wrappers keep prefix and uri.
|
|
191
|
+
def resolve_prefix_ns(owner, prefix)
|
|
192
|
+
current = owner
|
|
193
|
+
while current.is_a?(::Leptris::XML::Element)
|
|
194
|
+
hit = current.namespace_definitions.find { |ns| ns.prefix == prefix }
|
|
195
|
+
return hit if hit
|
|
196
|
+
|
|
197
|
+
current = current.parent
|
|
198
|
+
end
|
|
199
|
+
nil
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def prefix_part(name)
|
|
203
|
+
name.include?(":") ? name.split(":", 2)[0] : nil
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def processing_instruction_target(node)
|
|
207
|
+
node.target
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def node_type(node)
|
|
211
|
+
case node
|
|
212
|
+
when ::Leptris::XML::Document then :document
|
|
213
|
+
when ::Leptris::XML::DocType, CustomizedLeptris::Doctype then :doctype
|
|
214
|
+
when CustomizedLeptris::Declaration then :declaration
|
|
215
|
+
when CustomizedLeptris::EntityReference then :entity_reference
|
|
216
|
+
when ::Leptris::XML::CDATA then :cdata
|
|
217
|
+
when ::Leptris::XML::Comment then :comment
|
|
218
|
+
when ::Leptris::XML::ProcessingInstruction then :processing_instruction
|
|
219
|
+
when ::Leptris::XML::Text, CustomizedLeptris::TextSegment then :text
|
|
220
|
+
when ::Leptris::XML::Element then :element
|
|
221
|
+
when ::Leptris::XML::Attr then :attribute
|
|
222
|
+
else :unknown
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def node_name(node)
|
|
227
|
+
return node.root_name if node.is_a?(::Leptris::XML::DocType)
|
|
228
|
+
|
|
229
|
+
node.name.to_s.dup.force_encoding("UTF-8")
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
def set_node_name(node, name)
|
|
233
|
+
case node
|
|
234
|
+
when ::Leptris::XML::ProcessingInstruction then node.target = name
|
|
235
|
+
else node.name = name
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
def duplicate_node(node)
|
|
240
|
+
case node
|
|
241
|
+
when CustomizedLeptris::Declaration
|
|
242
|
+
CustomizedLeptris::Declaration.new(node.version, node.encoding, node.standalone)
|
|
243
|
+
when CustomizedLeptris::Doctype
|
|
244
|
+
CustomizedLeptris::Doctype.new(node.name, node.external_id, node.system_id)
|
|
245
|
+
when CustomizedLeptris::EntityReference
|
|
246
|
+
CustomizedLeptris::EntityReference.new(node.name)
|
|
247
|
+
else
|
|
248
|
+
node.dup
|
|
249
|
+
end
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
def children(node)
|
|
253
|
+
case node
|
|
254
|
+
when ::Leptris::XML::Document
|
|
255
|
+
assemble_document_children(node)
|
|
256
|
+
when CustomizedLeptris::Declaration, CustomizedLeptris::Doctype,
|
|
257
|
+
CustomizedLeptris::EntityReference, CustomizedLeptris::TextSegment
|
|
258
|
+
[]
|
|
259
|
+
else
|
|
260
|
+
split_entity_markers(node.children.to_a, node)
|
|
261
|
+
end
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
# Expand marker-bearing text nodes into the child sequence the
|
|
265
|
+
# moxml contract exposes: text, EntityReference, text, ...
|
|
266
|
+
def split_entity_markers(natives, parent)
|
|
267
|
+
result = []
|
|
268
|
+
natives.each do |child|
|
|
269
|
+
content = child.content.to_s.dup.force_encoding("UTF-8") if child.is_a?(::Leptris::XML::Text)
|
|
270
|
+
if content&.include?(Entity::MARKER)
|
|
271
|
+
content.scan(/([^#{Entity::MARKER}]*)(?:#{Entity::MARKER}([\w.:-]+);)?/o) do
|
|
272
|
+
text_part = Regexp.last_match(1)
|
|
273
|
+
name = Regexp.last_match(2)
|
|
274
|
+
result << CustomizedLeptris::TextSegment.new(text_part, parent) unless text_part.empty?
|
|
275
|
+
result << CustomizedLeptris::EntityReference.new(name) if name
|
|
276
|
+
end
|
|
277
|
+
else
|
|
278
|
+
result << child
|
|
279
|
+
end
|
|
280
|
+
end
|
|
281
|
+
result
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
def parent(node)
|
|
285
|
+
case node
|
|
286
|
+
when ::Leptris::XML::Document then nil
|
|
287
|
+
when CustomizedLeptris::Declaration, CustomizedLeptris::Doctype then node.parent_doc
|
|
288
|
+
when CustomizedLeptris::TextSegment, CustomizedLeptris::EntityReference then node.parent
|
|
289
|
+
else
|
|
290
|
+
# The binding reports the root element as parentless; the
|
|
291
|
+
# moxml contract roots at the document.
|
|
292
|
+
node.parent || (node.document&.root == node ? node.document : nil)
|
|
293
|
+
end
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
def next_sibling(node)
|
|
297
|
+
node.next_sibling if node.is_a?(::Leptris::XML::Node)
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
def previous_sibling(node)
|
|
301
|
+
node.previous_sibling if node.is_a?(::Leptris::XML::Node)
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
def document(node)
|
|
305
|
+
case node
|
|
306
|
+
when ::Leptris::XML::Document then node
|
|
307
|
+
when CustomizedLeptris::Declaration, CustomizedLeptris::Doctype then node.parent_doc
|
|
308
|
+
else node.document
|
|
309
|
+
end
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
def root(document)
|
|
313
|
+
document.root
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
def line_number(node)
|
|
317
|
+
return nil unless node.is_a?(::Leptris::XML::Node)
|
|
318
|
+
|
|
319
|
+
line = node.line
|
|
320
|
+
line.zero? ? nil : line
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
def attributes(element)
|
|
324
|
+
element.attribute_nodes
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
def attribute_element(attr)
|
|
328
|
+
attr.element
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
def attribute_name(attr)
|
|
332
|
+
# leptris Attr names are qualified; the wrapper composes the
|
|
333
|
+
# prefix, so expose the local part.
|
|
334
|
+
return attr.name.split(":", 2)[1] if attr.name.include?(":")
|
|
335
|
+
|
|
336
|
+
attr.name.to_s.dup.force_encoding("UTF-8")
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
def set_attribute(element, name, value)
|
|
340
|
+
element[name.to_s] = value.to_s
|
|
341
|
+
end
|
|
342
|
+
|
|
343
|
+
def set_attribute_name(attr, name)
|
|
344
|
+
# Attr is an immutable value object; renames go through the
|
|
345
|
+
# element and yield a fresh native, which the wrapper adopts.
|
|
346
|
+
element = attr.element
|
|
347
|
+
value = attr.value
|
|
348
|
+
element.remove_attribute(attr.name)
|
|
349
|
+
element[name.to_s] = value
|
|
350
|
+
element.attribute_nodes.reverse.find { |candidate| candidate.name == name.to_s }
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
def set_attribute_value(attr, value)
|
|
354
|
+
attr.value = value.to_s
|
|
355
|
+
attr
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
def get_attribute(element, name)
|
|
359
|
+
element.attribute_nodes.find { |attr| attr.name == name.to_s }
|
|
360
|
+
end
|
|
361
|
+
|
|
362
|
+
def get_attribute_value(element, name)
|
|
363
|
+
element[name.to_s]
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
def remove_attribute(element, name)
|
|
367
|
+
element.remove_attribute(name.to_s)
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
def remove_attribute_native(attr)
|
|
371
|
+
attr.element.remove_attribute(attr.name)
|
|
372
|
+
attr
|
|
373
|
+
end
|
|
374
|
+
|
|
375
|
+
def add_child(parent, child)
|
|
376
|
+
case parent
|
|
377
|
+
when ::Leptris::XML::Document then add_document_child(parent, child)
|
|
378
|
+
else
|
|
379
|
+
if child.is_a?(CustomizedLeptris::EntityReference)
|
|
380
|
+
marker = parent.document.create_text_node("#{Entity::MARKER}#{child.name};")
|
|
381
|
+
parent.add_child(marker)
|
|
382
|
+
return child
|
|
383
|
+
end
|
|
384
|
+
child = parent.document.create_text_node(child) if child.is_a?(String)
|
|
385
|
+
parent.add_child(child)
|
|
386
|
+
end
|
|
387
|
+
end
|
|
388
|
+
|
|
389
|
+
def add_previous_sibling(node, new_node)
|
|
390
|
+
# A PI inserted before the root lives at document level in
|
|
391
|
+
# libleptris's model, not in the element tree.
|
|
392
|
+
if new_node.is_a?(::Leptris::XML::ProcessingInstruction) &&
|
|
393
|
+
node.document&.root == node
|
|
394
|
+
node.document.add_pi(new_node.target, new_node.content.to_s)
|
|
395
|
+
return new_node
|
|
396
|
+
end
|
|
397
|
+
node.add_previous_sibling(new_node)
|
|
398
|
+
end
|
|
399
|
+
|
|
400
|
+
def add_next_sibling(node, new_node)
|
|
401
|
+
node.add_next_sibling(new_node)
|
|
402
|
+
end
|
|
403
|
+
|
|
404
|
+
def remove(node)
|
|
405
|
+
case node
|
|
406
|
+
when CustomizedLeptris::Declaration
|
|
407
|
+
remove_declaration(node.parent_doc) if node.parent_doc
|
|
408
|
+
when CustomizedLeptris::Doctype
|
|
409
|
+
attachments.delete(node.parent_doc, :doctype) if node.parent_doc
|
|
410
|
+
when CustomizedLeptris::EntityReference
|
|
411
|
+
marker_text_for(node.parent, node.name)&.unlink
|
|
412
|
+
else
|
|
413
|
+
node.unlink
|
|
414
|
+
end
|
|
415
|
+
end
|
|
416
|
+
|
|
417
|
+
def replace(node, new_node)
|
|
418
|
+
return node.replace(new_node) if node.is_a?(::Leptris::XML::Element)
|
|
419
|
+
|
|
420
|
+
# libleptris only offers element-anchored insertion, so a
|
|
421
|
+
# content node (text/comment/CDATA/PI) is replaced by
|
|
422
|
+
# unlinking and re-inserting: after an element sibling when
|
|
423
|
+
# one exists, else appended (end-of-list) to the parent.
|
|
424
|
+
parent = node.parent
|
|
425
|
+
unless parent
|
|
426
|
+
raise Moxml::DocumentStructureError.new(
|
|
427
|
+
"cannot replace a detached node",
|
|
428
|
+
)
|
|
429
|
+
end
|
|
430
|
+
|
|
431
|
+
prev = node.previous_sibling
|
|
432
|
+
node.unlink
|
|
433
|
+
if prev.is_a?(::Leptris::XML::Element)
|
|
434
|
+
prev.add_next_sibling(new_node)
|
|
435
|
+
else
|
|
436
|
+
parent.add_child(new_node)
|
|
437
|
+
end
|
|
438
|
+
new_node
|
|
439
|
+
end
|
|
440
|
+
|
|
441
|
+
def replace_children(node, new_children)
|
|
442
|
+
node.children = new_children
|
|
443
|
+
end
|
|
444
|
+
|
|
445
|
+
def text_content(node)
|
|
446
|
+
case node
|
|
447
|
+
when ::Leptris::XML::Document then node.root ? node.root.content : ""
|
|
448
|
+
when CustomizedLeptris::Declaration, CustomizedLeptris::Doctype,
|
|
449
|
+
CustomizedLeptris::EntityReference
|
|
450
|
+
""
|
|
451
|
+
when CustomizedLeptris::TextSegment then node.content
|
|
452
|
+
else node.content.to_s
|
|
453
|
+
end
|
|
454
|
+
end
|
|
455
|
+
|
|
456
|
+
def inner_text(node)
|
|
457
|
+
# moxml semantic: direct text children only — no descendant
|
|
458
|
+
# text (that is #text), no comments. Entity references
|
|
459
|
+
# contribute their serialized form.
|
|
460
|
+
children(node).filter_map do |child|
|
|
461
|
+
case child
|
|
462
|
+
when CustomizedLeptris::EntityReference then "&#{child.name};"
|
|
463
|
+
when ::Leptris::XML::CDATA then child.content
|
|
464
|
+
when ::Leptris::XML::Text, CustomizedLeptris::TextSegment
|
|
465
|
+
child.content.to_s.dup.force_encoding("UTF-8")
|
|
466
|
+
end
|
|
467
|
+
end.join
|
|
468
|
+
end
|
|
469
|
+
|
|
470
|
+
def set_text_content(node, content)
|
|
471
|
+
case node
|
|
472
|
+
when ::Leptris::XML::Document
|
|
473
|
+
node.root&.content = content.to_s
|
|
474
|
+
else
|
|
475
|
+
node.content = content.to_s
|
|
476
|
+
end
|
|
477
|
+
end
|
|
478
|
+
|
|
479
|
+
def cdata_content(node)
|
|
480
|
+
node.content
|
|
481
|
+
end
|
|
482
|
+
|
|
483
|
+
def set_cdata_content(node, content)
|
|
484
|
+
node.content = content.to_s
|
|
485
|
+
end
|
|
486
|
+
|
|
487
|
+
def comment_content(node)
|
|
488
|
+
node.content
|
|
489
|
+
end
|
|
490
|
+
|
|
491
|
+
def set_comment_content(node, content)
|
|
492
|
+
node.content = content.to_s
|
|
493
|
+
end
|
|
494
|
+
|
|
495
|
+
def processing_instruction_content(node)
|
|
496
|
+
node.content
|
|
497
|
+
end
|
|
498
|
+
|
|
499
|
+
def set_processing_instruction_content(node, content)
|
|
500
|
+
node.data = content.to_s
|
|
501
|
+
end
|
|
502
|
+
|
|
503
|
+
def namespace_prefix(namespace)
|
|
504
|
+
# Attr#namespace is the bare URI string in leptris 1.9+ (the
|
|
505
|
+
# C model: a namespace handle IS the URI); attributes carry
|
|
506
|
+
# their prefix on the Attr itself.
|
|
507
|
+
return nil if namespace.is_a?(String)
|
|
508
|
+
|
|
509
|
+
namespace.prefix
|
|
510
|
+
end
|
|
511
|
+
|
|
512
|
+
def namespace_uri(namespace)
|
|
513
|
+
return namespace if namespace.is_a?(String)
|
|
514
|
+
|
|
515
|
+
namespace.href
|
|
516
|
+
end
|
|
517
|
+
|
|
518
|
+
def namespace_definitions(element)
|
|
519
|
+
element.namespace_definitions
|
|
520
|
+
end
|
|
521
|
+
|
|
522
|
+
def doctype_name(node)
|
|
523
|
+
node.is_a?(CustomizedLeptris::Doctype) ? node.name : node.root_name
|
|
524
|
+
end
|
|
525
|
+
|
|
526
|
+
def doctype_external_id(node)
|
|
527
|
+
node.is_a?(CustomizedLeptris::Doctype) ? node.external_id : node.public_id
|
|
528
|
+
end
|
|
529
|
+
|
|
530
|
+
def doctype_system_id(node)
|
|
531
|
+
node.system_id
|
|
532
|
+
end
|
|
533
|
+
|
|
534
|
+
# XPath prefers libleptris' native C engine (three orders of
|
|
535
|
+
# magnitude faster than the Ruby engine) for document-context
|
|
536
|
+
# queries, with a conservative gate: the Ruby engine handles
|
|
537
|
+
# element-context evaluation (native relative-context
|
|
538
|
+
# semantics are not contract-verified), variable references,
|
|
539
|
+
# and expressions whose results are attribute nodes (native
|
|
540
|
+
# returns those without name accessors). Both engines resolve
|
|
541
|
+
# expression prefixes against document-declared namespaces.
|
|
542
|
+
NATIVE_XPATH_CACHE = XPath::Cache.new(100)
|
|
543
|
+
|
|
544
|
+
def xpath(node, expression, namespaces = {})
|
|
545
|
+
native = native_xpath(node, expression, namespaces)
|
|
546
|
+
return native unless native.nil?
|
|
547
|
+
|
|
548
|
+
engine_xpath(node, expression, namespaces)
|
|
549
|
+
end
|
|
550
|
+
|
|
551
|
+
def at_xpath(node, expression, namespaces = {})
|
|
552
|
+
native = native_xpath(node, expression, namespaces, first_only: true)
|
|
553
|
+
return native unless native.nil?
|
|
554
|
+
|
|
555
|
+
result = engine_xpath(node, expression, namespaces)
|
|
556
|
+
result.is_a?(Array) ? result.first : result
|
|
557
|
+
end
|
|
558
|
+
|
|
559
|
+
# @return [Array, Object, nil] native results, or nil when the
|
|
560
|
+
# query must run on the Ruby engine
|
|
561
|
+
def native_xpath(node, expression, namespaces, first_only: false)
|
|
562
|
+
return nil unless node.is_a?(::Leptris::XML::Document)
|
|
563
|
+
return nil unless native_expression?(expression)
|
|
564
|
+
|
|
565
|
+
compiled = NATIVE_XPATH_CACHE.get_or_set(expression) do
|
|
566
|
+
::Leptris::XML::XPath.compile(expression)
|
|
567
|
+
end
|
|
568
|
+
result = if namespaces && !namespaces.empty?
|
|
569
|
+
compiled.eval(node, namespaces)
|
|
570
|
+
else
|
|
571
|
+
compiled.eval(node)
|
|
572
|
+
end
|
|
573
|
+
case result
|
|
574
|
+
when ::Leptris::XML::NodeSet
|
|
575
|
+
nodes = result.to_a
|
|
576
|
+
first_only ? nodes.first : nodes
|
|
577
|
+
else
|
|
578
|
+
result
|
|
579
|
+
end
|
|
580
|
+
rescue ::Leptris::XML::XPathError
|
|
581
|
+
# Not supported by the native engine — the Ruby engine is a
|
|
582
|
+
# full XPath 1.0 implementation, including Moxml's syntax
|
|
583
|
+
# errors for invalid expressions.
|
|
584
|
+
nil
|
|
585
|
+
end
|
|
586
|
+
|
|
587
|
+
# Document-context queries without variable references,
|
|
588
|
+
# attribute-node results, or the nokogiri-compat xmlns:
|
|
589
|
+
# reserved prefix convention.
|
|
590
|
+
def native_expression?(expression)
|
|
591
|
+
ast = XPath::Parser.parse_with_cache(expression)
|
|
592
|
+
return false if ast_contains_type?(ast, :variable)
|
|
593
|
+
return false if uses_xmlns_prefix?(ast)
|
|
594
|
+
|
|
595
|
+
!selects_attribute_results?(ast)
|
|
596
|
+
rescue XPath::SyntaxError
|
|
597
|
+
false
|
|
598
|
+
end
|
|
599
|
+
|
|
600
|
+
def ast_contains_type?(ast, type)
|
|
601
|
+
return true if ast.type == type
|
|
602
|
+
|
|
603
|
+
ast.children.any? do |child|
|
|
604
|
+
child.is_a?(XPath::AST::Node) && ast_contains_type?(child, type)
|
|
605
|
+
end
|
|
606
|
+
end
|
|
607
|
+
|
|
608
|
+
# xmlns:name is a nokogiri-compat convention addressing
|
|
609
|
+
# elements in the default namespace; only the Ruby engine
|
|
610
|
+
# implements it.
|
|
611
|
+
def uses_xmlns_prefix?(ast)
|
|
612
|
+
return true if ast.type == :test && ast.value[:namespace] == "xmlns"
|
|
613
|
+
|
|
614
|
+
ast.children.any? do |child|
|
|
615
|
+
child.is_a?(XPath::AST::Node) && uses_xmlns_prefix?(child)
|
|
616
|
+
end
|
|
617
|
+
end
|
|
618
|
+
|
|
619
|
+
def selects_attribute_results?(ast)
|
|
620
|
+
case ast.type
|
|
621
|
+
when :pipe, :union, :filter_expr
|
|
622
|
+
ast.children.any? { |child| selects_attribute_results?(child) }
|
|
623
|
+
when :absolute_path, :relative_path
|
|
624
|
+
step = ast.children.last
|
|
625
|
+
step = step.children.first if step.type == :step_with_predicates
|
|
626
|
+
step.type == :axis && step.children.first == "attribute"
|
|
627
|
+
else
|
|
628
|
+
false
|
|
629
|
+
end
|
|
630
|
+
end
|
|
631
|
+
|
|
632
|
+
def engine_xpath(node, expression, namespaces = {})
|
|
633
|
+
unless node.is_a?(Moxml::Node)
|
|
634
|
+
node = Moxml::Node.wrap(node, Context.new(:leptris))
|
|
635
|
+
end
|
|
636
|
+
|
|
637
|
+
ast = XPath::Parser.parse(expression)
|
|
638
|
+
proc = XPath::Compiler.compile_with_cache(ast, namespaces: namespaces)
|
|
639
|
+
result = proc.call(node)
|
|
640
|
+
|
|
641
|
+
case result
|
|
642
|
+
when Array, NodeSet
|
|
643
|
+
nodes = result.is_a?(NodeSet) ? result.to_a : result
|
|
644
|
+
seen = {}.compare_by_identity
|
|
645
|
+
nodes.map { |n| n.is_a?(Moxml::Node) ? n.native : n }
|
|
646
|
+
.select do |native|
|
|
647
|
+
if seen.key?(native)
|
|
648
|
+
false
|
|
649
|
+
else
|
|
650
|
+
seen[native] = true
|
|
651
|
+
true
|
|
652
|
+
end
|
|
653
|
+
end
|
|
654
|
+
else
|
|
655
|
+
result
|
|
656
|
+
end
|
|
657
|
+
end
|
|
658
|
+
|
|
659
|
+
def serialize(node, options = {})
|
|
660
|
+
xml = raw_serialize(node, options)
|
|
661
|
+
xml = restore_entities(xml)
|
|
662
|
+
normalize_serialization(xml, options)
|
|
663
|
+
end
|
|
664
|
+
|
|
665
|
+
def raw_serialize(node, options)
|
|
666
|
+
# CDATA must precede Text in this chain: CDATA < Text in the
|
|
667
|
+
# binding, so a Text branch first would swallow CDATA nodes.
|
|
668
|
+
case node
|
|
669
|
+
when CustomizedLeptris::Declaration, CustomizedLeptris::Doctype,
|
|
670
|
+
CustomizedLeptris::EntityReference
|
|
671
|
+
return node.to_xml
|
|
672
|
+
when ::Leptris::XML::CDATA
|
|
673
|
+
return XmlEmitter.cdata(node.content)
|
|
674
|
+
when ::Leptris::XML::Comment
|
|
675
|
+
return "<!--#{node.content}-->"
|
|
676
|
+
when ::Leptris::XML::ProcessingInstruction
|
|
677
|
+
content = node.content.to_s
|
|
678
|
+
return content.empty? ? "<?#{node.target}?>" : "<?#{node.target} #{content}?>"
|
|
679
|
+
when ::Leptris::XML::Text, CustomizedLeptris::TextSegment
|
|
680
|
+
return XmlEmitter.escape_text(node.content.to_s)
|
|
681
|
+
when ::Leptris::XML::Document
|
|
682
|
+
return serialize_document(node, options)
|
|
683
|
+
end
|
|
684
|
+
|
|
685
|
+
include_decl = options.fetch(:declaration) do
|
|
686
|
+
options[:no_declaration] ? false : document_has_declaration?(node)
|
|
687
|
+
end
|
|
688
|
+
node.to_xml(
|
|
689
|
+
indent: options.fetch(:indent, 0),
|
|
690
|
+
no_decl: !include_decl,
|
|
691
|
+
encoding: options[:encoding],
|
|
692
|
+
)
|
|
693
|
+
end
|
|
694
|
+
|
|
695
|
+
# moxml canonical serialization: apostrophes stay literal
|
|
696
|
+
# (only & < > " are escaped) and empty elements expand when the
|
|
697
|
+
# caller asked for it — matching the other adapters' contract.
|
|
698
|
+
# Runs segment-aware: CDATA content is literal and must not be
|
|
699
|
+
# touched.
|
|
700
|
+
# Regions whose content is literal and must never be rewritten.
|
|
701
|
+
# Scanned positionally (String#index), not with a regex: the
|
|
702
|
+
# scan is linear in the input length, so pathological document
|
|
703
|
+
# content cannot blow up the serializer.
|
|
704
|
+
LITERAL_REGIONS = {
|
|
705
|
+
"<!--" => "-->",
|
|
706
|
+
"<![CDATA[" => "]]>",
|
|
707
|
+
"<?" => "?>",
|
|
708
|
+
}.freeze
|
|
709
|
+
|
|
710
|
+
def normalize_serialization(xml, options)
|
|
711
|
+
needs_apos = xml.include?("'")
|
|
712
|
+
needs_expand = options[:expand_empty] && xml.include?("/>")
|
|
713
|
+
return xml unless needs_apos || needs_expand
|
|
714
|
+
|
|
715
|
+
out = +""
|
|
716
|
+
pos = 0
|
|
717
|
+
while pos < xml.length
|
|
718
|
+
opener_at, terminator = next_literal_region(xml, pos)
|
|
719
|
+
if opener_at.nil?
|
|
720
|
+
out << normalize_markup(xml[pos..], needs_apos, needs_expand)
|
|
721
|
+
break
|
|
722
|
+
end
|
|
723
|
+
|
|
724
|
+
out << normalize_markup(xml[pos...opener_at], needs_apos, needs_expand)
|
|
725
|
+
search_from = opener_at + opener_at_offset(terminator)
|
|
726
|
+
close = xml.index(terminator, search_from)
|
|
727
|
+
close_end = close.nil? ? xml.length : close + terminator.length
|
|
728
|
+
out << xml[opener_at...close_end]
|
|
729
|
+
pos = close_end
|
|
730
|
+
end
|
|
731
|
+
out
|
|
732
|
+
end
|
|
733
|
+
|
|
734
|
+
# Nearest literal region at/after from: [position, terminator].
|
|
735
|
+
def next_literal_region(xml, from)
|
|
736
|
+
best = nil
|
|
737
|
+
best_terminator = nil
|
|
738
|
+
LITERAL_REGIONS.each do |opener, terminator|
|
|
739
|
+
idx = xml.index(opener, from)
|
|
740
|
+
next if idx.nil?
|
|
741
|
+
|
|
742
|
+
if best.nil? || idx < best
|
|
743
|
+
best = idx
|
|
744
|
+
best_terminator = terminator
|
|
745
|
+
end
|
|
746
|
+
end
|
|
747
|
+
best.nil? ? nil : [best, best_terminator]
|
|
748
|
+
end
|
|
749
|
+
|
|
750
|
+
# Search for a terminator past its opener's overlap-safe offset
|
|
751
|
+
# ("-->" cannot start inside "<!--").
|
|
752
|
+
def opener_at_offset(terminator)
|
|
753
|
+
terminator == "-->" ? 4 : 0
|
|
754
|
+
end
|
|
755
|
+
|
|
756
|
+
# All quantifiers are possessive and the bare-part class
|
|
757
|
+
# excludes "/" and ">": the possessive groups can never consume
|
|
758
|
+
# the closing delimiter, so no backtracking is possible and the
|
|
759
|
+
# match is linear even on malformed tags.
|
|
760
|
+
EMPTY_ELEMENT_RE = %r{<([A-Za-z_][\w.:-]*+)((?:"[^"]*+"|'[^']*+'|[^<>"'/]++)*+)/>}
|
|
761
|
+
|
|
762
|
+
def normalize_markup(markup, needs_apos, needs_expand)
|
|
763
|
+
markup = markup.gsub("'", "'") if needs_apos
|
|
764
|
+
if needs_expand
|
|
765
|
+
markup = markup.gsub(EMPTY_ELEMENT_RE) do
|
|
766
|
+
"<#{Regexp.last_match(1)}#{Regexp.last_match(2)}></#{Regexp.last_match(1)}>"
|
|
767
|
+
end
|
|
768
|
+
end
|
|
769
|
+
markup
|
|
770
|
+
end
|
|
771
|
+
|
|
772
|
+
# Documents compose from their parts: the native serializer
|
|
773
|
+
# only walks the root subtree, so declaration, DOCTYPE, PIs and
|
|
774
|
+
# document-level text are assembled around it explicitly.
|
|
775
|
+
def serialize_document(doc, options)
|
|
776
|
+
parts = []
|
|
777
|
+
|
|
778
|
+
include_decl = !options[:no_declaration] && options.fetch(:declaration) do
|
|
779
|
+
document_has_declaration?(doc)
|
|
780
|
+
end
|
|
781
|
+
if include_decl
|
|
782
|
+
declaration = attachments.get(doc, :declaration)
|
|
783
|
+
parts << (declaration ? declaration.to_xml : default_declaration_xml(doc, options))
|
|
784
|
+
end
|
|
785
|
+
|
|
786
|
+
doctype = attachments.get(doc, :doctype)
|
|
787
|
+
parts << doctype.to_xml if doctype
|
|
788
|
+
|
|
789
|
+
native = native_doctype_xml(doc)
|
|
790
|
+
parts << native if native
|
|
791
|
+
|
|
792
|
+
(doc.processing_instructions || []).each do |(target, data)|
|
|
793
|
+
parts << (data.to_s.empty? ? "<?#{target}?>" : "<?#{target} #{data}?>")
|
|
794
|
+
end
|
|
795
|
+
|
|
796
|
+
parts << raw_serialize(doc.root, options) if doc.root
|
|
797
|
+
|
|
798
|
+
texts = attachments.get(doc, :document_text)
|
|
799
|
+
texts&.each { |text| parts << XmlEmitter.escape_text(text.content.to_s) }
|
|
800
|
+
|
|
801
|
+
parts.join
|
|
802
|
+
end
|
|
803
|
+
|
|
804
|
+
def default_declaration_xml(doc, options)
|
|
805
|
+
encoding = options[:encoding] || doc.encoding
|
|
806
|
+
encoding = "UTF-8" if encoding.to_s.empty?
|
|
807
|
+
XmlEmitter.declaration_xml("1.0", encoding, nil)
|
|
808
|
+
end
|
|
809
|
+
|
|
810
|
+
def marker_text_for(parent, name)
|
|
811
|
+
return nil unless parent.is_a?(::Leptris::XML::Element)
|
|
812
|
+
|
|
813
|
+
marker = "#{Entity::MARKER}#{name};"
|
|
814
|
+
parent.children.to_a.find do |child|
|
|
815
|
+
child.is_a?(::Leptris::XML::Text) && child.content == marker
|
|
816
|
+
end
|
|
817
|
+
end
|
|
818
|
+
|
|
819
|
+
def native_doctype_xml(doc)
|
|
820
|
+
dt = doc.doctype
|
|
821
|
+
return nil unless dt
|
|
822
|
+
|
|
823
|
+
XmlEmitter.doctype_xml(dt.root_name, dt.public_id, dt.system_id)
|
|
824
|
+
end
|
|
825
|
+
|
|
826
|
+
def entity_refs?(element)
|
|
827
|
+
element.is_a?(::Leptris::XML::Element) &&
|
|
828
|
+
!attachments.get(element, :entity_refs).to_a.empty?
|
|
829
|
+
end
|
|
830
|
+
|
|
831
|
+
# Registry-based entity references are not in the native tree,
|
|
832
|
+
# so the native serializer cannot see them; compose the element
|
|
833
|
+
# from the wrapper-visible children instead.
|
|
834
|
+
def serialize_entity_bearing_element(element, options)
|
|
835
|
+
name = element.prefix ? "#{element.prefix}:#{element.name}" : element.name
|
|
836
|
+
attrs = element.attribute_nodes.map do |attr|
|
|
837
|
+
%( #{attr.name}="#{XmlEmitter.escape_attribute(attr.value)}")
|
|
838
|
+
end.join
|
|
839
|
+
decls = element.namespace_definitions.map do |ns|
|
|
840
|
+
ns.prefix ? %( xmlns:#{ns.prefix}="#{ns.href}") : %( xmlns="#{ns.href}")
|
|
841
|
+
end.join
|
|
842
|
+
inner = children(element).map { |child| raw_serialize(child, options) }.join
|
|
843
|
+
"<#{name}#{decls}#{attrs}>#{inner}</#{name}>"
|
|
844
|
+
end
|
|
845
|
+
|
|
846
|
+
def sax_parse(xml, handler)
|
|
847
|
+
bridge = LeptrisSAXBridge.new(handler)
|
|
848
|
+
xml_string = xml.is_a?(IO) || xml.is_a?(::StringIO) ? xml.read : xml.to_s
|
|
849
|
+
::Leptris::XML::SAX::Parser.new(bridge).parse(xml_string)
|
|
850
|
+
rescue ::Leptris::XML::ParseError, ::Leptris::XML::Error => e
|
|
851
|
+
handler.on_error(Moxml::ParseError.new(e.message))
|
|
852
|
+
end
|
|
853
|
+
|
|
854
|
+
private
|
|
855
|
+
|
|
856
|
+
def record_source_declaration(native_doc, xml_string)
|
|
857
|
+
had = xml_string.match?(/\A\s*<\?xml\b/)
|
|
858
|
+
attachments.set(native_doc, :had_source_declaration, had)
|
|
859
|
+
end
|
|
860
|
+
|
|
861
|
+
def document_has_declaration?(native)
|
|
862
|
+
return false unless native.is_a?(::Leptris::XML::Document)
|
|
863
|
+
|
|
864
|
+
return true if attachments.get(native, :declaration)
|
|
865
|
+
|
|
866
|
+
attachments.get(native, :had_source_declaration) ? true : false
|
|
867
|
+
end
|
|
868
|
+
|
|
869
|
+
def assemble_document_children(doc)
|
|
870
|
+
children = []
|
|
871
|
+
|
|
872
|
+
native_doctype = doc.doctype
|
|
873
|
+
children << native_doctype if native_doctype
|
|
874
|
+
|
|
875
|
+
doctype_wrapper = attachments.get(doc, :doctype)
|
|
876
|
+
children << doctype_wrapper if doctype_wrapper
|
|
877
|
+
|
|
878
|
+
children << doc.root if doc.root
|
|
879
|
+
|
|
880
|
+
texts = attachments.get(doc, :document_text)
|
|
881
|
+
children.concat(texts) if texts
|
|
882
|
+
children
|
|
883
|
+
end
|
|
884
|
+
|
|
885
|
+
def add_document_child(doc, child)
|
|
886
|
+
case child
|
|
887
|
+
when CustomizedLeptris::Declaration
|
|
888
|
+
child.parent_doc = doc
|
|
889
|
+
attachments.set(doc, :declaration, child)
|
|
890
|
+
when CustomizedLeptris::Doctype
|
|
891
|
+
child.parent_doc = doc
|
|
892
|
+
attachments.set(doc, :doctype, child)
|
|
893
|
+
when ::Leptris::XML::DocType
|
|
894
|
+
raise Moxml::DocumentStructureError.new(
|
|
895
|
+
"libleptris does not support attaching a native DocType to a document",
|
|
896
|
+
)
|
|
897
|
+
when ::Leptris::XML::Element
|
|
898
|
+
doc.root = child
|
|
899
|
+
when ::Leptris::XML::ProcessingInstruction
|
|
900
|
+
doc.add_pi(child.target, child.content.to_s)
|
|
901
|
+
when ::Leptris::XML::Text
|
|
902
|
+
texts = attachments.get(doc, :document_text) || []
|
|
903
|
+
texts << child
|
|
904
|
+
attachments.set(doc, :document_text, texts)
|
|
905
|
+
child
|
|
906
|
+
else
|
|
907
|
+
raise Moxml::DocumentStructureError.new(
|
|
908
|
+
"Unsupported document child: #{child.class}",
|
|
909
|
+
)
|
|
910
|
+
end
|
|
911
|
+
child
|
|
912
|
+
end
|
|
913
|
+
end
|
|
914
|
+
|
|
915
|
+
# Bridge between the leptris SAX callbacks and Moxml::SAX::Handler.
|
|
916
|
+
#
|
|
917
|
+
# @private
|
|
918
|
+
class LeptrisSAXBridge < ::Leptris::XML::SAX::Document
|
|
919
|
+
include Moxml::SAX::NamespaceSplitter
|
|
920
|
+
|
|
921
|
+
def initialize(handler)
|
|
922
|
+
@handler = handler
|
|
923
|
+
super()
|
|
924
|
+
end
|
|
925
|
+
|
|
926
|
+
def start_document
|
|
927
|
+
@handler.on_start_document
|
|
928
|
+
end
|
|
929
|
+
|
|
930
|
+
def end_document
|
|
931
|
+
@handler.on_end_document
|
|
932
|
+
end
|
|
933
|
+
|
|
934
|
+
def start_element(name, attrs = [])
|
|
935
|
+
attr_hash, ns_hash = split_attributes_and_namespaces(attrs)
|
|
936
|
+
@handler.on_start_element(name, attr_hash, ns_hash)
|
|
937
|
+
end
|
|
938
|
+
|
|
939
|
+
def end_element(name)
|
|
940
|
+
@handler.on_end_element(name)
|
|
941
|
+
end
|
|
942
|
+
|
|
943
|
+
def characters(string)
|
|
944
|
+
@handler.on_characters(string)
|
|
945
|
+
end
|
|
946
|
+
|
|
947
|
+
def comment(string)
|
|
948
|
+
@handler.on_comment(string)
|
|
949
|
+
end
|
|
950
|
+
|
|
951
|
+
def cdata_block(string)
|
|
952
|
+
@handler.on_cdata(string)
|
|
953
|
+
end
|
|
954
|
+
|
|
955
|
+
def processing_instruction(name, content)
|
|
956
|
+
@handler.on_processing_instruction(name, content || "")
|
|
957
|
+
end
|
|
958
|
+
|
|
959
|
+
def error(message, _line = 0, _column = 0)
|
|
960
|
+
@handler.on_error(Moxml::ParseError.new(message))
|
|
961
|
+
end
|
|
962
|
+
end
|
|
963
|
+
end
|
|
964
|
+
end
|
|
965
|
+
end
|