moxml 0.1.0 → 0.1.1
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/rake.yml +15 -0
- data/.github/workflows/release.yml +23 -0
- data/.gitignore +3 -0
- data/.rubocop.yml +2 -0
- data/.rubocop_todo.yml +65 -0
- data/.ruby-version +1 -0
- data/Gemfile +10 -3
- data/README.adoc +400 -594
- data/lib/moxml/adapter/base.rb +102 -0
- data/lib/moxml/adapter/customized_oga/xml_declaration.rb +18 -0
- data/lib/moxml/adapter/customized_oga/xml_generator.rb +104 -0
- data/lib/moxml/adapter/nokogiri.rb +314 -0
- data/lib/moxml/adapter/oga.rb +309 -0
- data/lib/moxml/adapter/ox.rb +325 -0
- data/lib/moxml/adapter.rb +26 -170
- data/lib/moxml/attribute.rb +47 -14
- data/lib/moxml/builder.rb +64 -0
- data/lib/moxml/cdata.rb +4 -26
- data/lib/moxml/comment.rb +6 -22
- data/lib/moxml/config.rb +39 -15
- data/lib/moxml/context.rb +29 -0
- data/lib/moxml/declaration.rb +16 -26
- data/lib/moxml/doctype.rb +9 -0
- data/lib/moxml/document.rb +51 -63
- data/lib/moxml/document_builder.rb +87 -0
- data/lib/moxml/element.rb +61 -99
- data/lib/moxml/error.rb +20 -0
- data/lib/moxml/namespace.rb +12 -37
- data/lib/moxml/node.rb +78 -58
- data/lib/moxml/node_set.rb +19 -222
- data/lib/moxml/processing_instruction.rb +6 -25
- data/lib/moxml/text.rb +4 -26
- data/lib/moxml/version.rb +1 -1
- data/lib/moxml/xml_utils/encoder.rb +55 -0
- data/lib/moxml/xml_utils.rb +80 -0
- data/lib/moxml.rb +33 -33
- data/moxml.gemspec +1 -1
- data/spec/moxml/adapter/nokogiri_spec.rb +14 -0
- data/spec/moxml/adapter/oga_spec.rb +14 -0
- data/spec/moxml/adapter/ox_spec.rb +49 -0
- data/spec/moxml/all_with_adapters_spec.rb +46 -0
- data/spec/moxml/config_spec.rb +55 -0
- data/spec/moxml/error_spec.rb +71 -0
- data/spec/moxml/examples/adapter_spec.rb +27 -0
- data/spec/moxml_spec.rb +50 -0
- data/spec/spec_helper.rb +32 -0
- data/spec/support/shared_examples/attribute.rb +165 -0
- data/spec/support/shared_examples/builder.rb +25 -0
- data/spec/support/shared_examples/cdata.rb +70 -0
- data/spec/support/shared_examples/comment.rb +65 -0
- data/spec/support/shared_examples/context.rb +35 -0
- data/spec/support/shared_examples/declaration.rb +93 -0
- data/spec/support/shared_examples/doctype.rb +25 -0
- data/spec/support/shared_examples/document.rb +110 -0
- data/spec/support/shared_examples/document_builder.rb +43 -0
- data/spec/support/shared_examples/edge_cases.rb +185 -0
- data/spec/support/shared_examples/element.rb +110 -0
- data/spec/support/shared_examples/examples/attribute.rb +42 -0
- data/spec/support/shared_examples/examples/basic_usage.rb +67 -0
- data/spec/support/shared_examples/examples/memory.rb +54 -0
- data/spec/support/shared_examples/examples/namespace.rb +65 -0
- data/spec/support/shared_examples/examples/readme_examples.rb +100 -0
- data/spec/support/shared_examples/examples/thread_safety.rb +43 -0
- data/spec/support/shared_examples/examples/xpath.rb +39 -0
- data/spec/support/shared_examples/integration.rb +135 -0
- data/spec/support/shared_examples/namespace.rb +96 -0
- data/spec/support/shared_examples/node.rb +110 -0
- data/spec/support/shared_examples/node_set.rb +90 -0
- data/spec/support/shared_examples/processing_instruction.rb +88 -0
- data/spec/support/shared_examples/text.rb +66 -0
- data/spec/support/shared_examples/xml_adapter.rb +191 -0
- data/spec/support/xml_matchers.rb +27 -0
- metadata +55 -6
- data/.github/workflows/main.yml +0 -27
- data/lib/moxml/error_handler.rb +0 -77
- data/lib/moxml/errors.rb +0 -169
@@ -0,0 +1,309 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "base"
|
4
|
+
require_relative "customized_oga/xml_generator"
|
5
|
+
require_relative "customized_oga/xml_declaration"
|
6
|
+
require "oga"
|
7
|
+
|
8
|
+
module Moxml
|
9
|
+
module Adapter
|
10
|
+
class Oga < Base
|
11
|
+
class << self
|
12
|
+
def set_root(doc, element)
|
13
|
+
doc.children.clear # Clear any existing children
|
14
|
+
doc.children << element
|
15
|
+
end
|
16
|
+
|
17
|
+
def parse(xml, options = {})
|
18
|
+
native_doc = begin
|
19
|
+
::Oga.parse_xml(xml, strict: options[:strict])
|
20
|
+
rescue LL::ParserError => e
|
21
|
+
raise Moxml::ParseError, e.message
|
22
|
+
end
|
23
|
+
|
24
|
+
DocumentBuilder.new(Context.new(:oga)).build(native_doc)
|
25
|
+
end
|
26
|
+
|
27
|
+
def create_document
|
28
|
+
::Oga::XML::Document.new
|
29
|
+
end
|
30
|
+
|
31
|
+
def create_native_element(name)
|
32
|
+
::Oga::XML::Element.new(name: name)
|
33
|
+
end
|
34
|
+
|
35
|
+
def create_native_text(content)
|
36
|
+
::Oga::XML::Text.new(text: content)
|
37
|
+
end
|
38
|
+
|
39
|
+
def create_native_cdata(content)
|
40
|
+
::Oga::XML::Cdata.new(text: content)
|
41
|
+
end
|
42
|
+
|
43
|
+
def create_native_comment(content)
|
44
|
+
::Oga::XML::Comment.new(text: content)
|
45
|
+
end
|
46
|
+
|
47
|
+
def create_native_doctype(name, external_id, system_id)
|
48
|
+
::Oga::XML::Doctype.new(
|
49
|
+
name: name, public_id: external_id, system_id: system_id, type: "PUBLIC"
|
50
|
+
)
|
51
|
+
end
|
52
|
+
|
53
|
+
def create_native_processing_instruction(target, content)
|
54
|
+
::Oga::XML::ProcessingInstruction.new(name: target, text: content)
|
55
|
+
end
|
56
|
+
|
57
|
+
def create_native_declaration(version, encoding, standalone)
|
58
|
+
attrs = {
|
59
|
+
version: version,
|
60
|
+
encoding: encoding,
|
61
|
+
standalone: standalone
|
62
|
+
}.compact
|
63
|
+
::Moxml::Adapter::CustomizedOga::XmlDeclaration.new(attrs)
|
64
|
+
end
|
65
|
+
|
66
|
+
def declaration_attribute(declaration, attr_name)
|
67
|
+
return unless ::Moxml::Declaration::ALLOWED_ATTRIBUTES.include?(attr_name.to_s)
|
68
|
+
|
69
|
+
declaration.public_send(attr_name)
|
70
|
+
end
|
71
|
+
|
72
|
+
def set_declaration_attribute(declaration, attr_name, value)
|
73
|
+
return unless ::Moxml::Declaration::ALLOWED_ATTRIBUTES.include?(attr_name.to_s)
|
74
|
+
|
75
|
+
declaration.public_send("#{attr_name}=", value)
|
76
|
+
end
|
77
|
+
|
78
|
+
def create_native_namespace(element, prefix, uri)
|
79
|
+
ns = element.available_namespaces[prefix]
|
80
|
+
return ns unless ns.nil?
|
81
|
+
|
82
|
+
# Oga creates an attribute and registers a namespace
|
83
|
+
set_attribute(element, [::Oga::XML::Element::XMLNS_PREFIX, prefix].compact.join(":"), uri)
|
84
|
+
element.register_namespace(prefix, uri)
|
85
|
+
::Oga::XML::Namespace.new(name: prefix, uri: uri)
|
86
|
+
end
|
87
|
+
|
88
|
+
def set_namespace(element, ns_or_string)
|
89
|
+
element.namespace_name = ns_or_string.to_s
|
90
|
+
end
|
91
|
+
|
92
|
+
def namespace(element)
|
93
|
+
if element.respond_to?(:namespace)
|
94
|
+
element.namespace
|
95
|
+
elsif element.respond_to?(:namespaces)
|
96
|
+
element.namespaces.values.last
|
97
|
+
end
|
98
|
+
rescue NoMethodError
|
99
|
+
# Oga attributes fail with NoMethodError:
|
100
|
+
# undefined method `available_namespaces' for nil:NilClass
|
101
|
+
nil
|
102
|
+
end
|
103
|
+
|
104
|
+
def processing_instruction_target(node)
|
105
|
+
node.name
|
106
|
+
end
|
107
|
+
|
108
|
+
def node_type(node)
|
109
|
+
case node
|
110
|
+
when ::Oga::XML::Element then :element
|
111
|
+
when ::Oga::XML::Text then :text
|
112
|
+
when ::Oga::XML::Cdata then :cdata
|
113
|
+
when ::Oga::XML::Comment then :comment
|
114
|
+
when ::Oga::XML::ProcessingInstruction then :processing_instruction
|
115
|
+
when ::Oga::XML::Document then :document
|
116
|
+
when ::Oga::XML::Doctype then :doctype
|
117
|
+
else :unknown
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
def node_name(node)
|
122
|
+
node.name
|
123
|
+
end
|
124
|
+
|
125
|
+
def set_node_name(node, name)
|
126
|
+
node.name = name
|
127
|
+
end
|
128
|
+
|
129
|
+
def children(node)
|
130
|
+
all_children = []
|
131
|
+
|
132
|
+
all_children += [node.xml_declaration, node.doctype].compact if node.is_a?(::Oga::XML::Document)
|
133
|
+
|
134
|
+
return all_children unless node.respond_to?(:children)
|
135
|
+
|
136
|
+
all_children + node.children.reject do |child|
|
137
|
+
child.is_a?(::Oga::XML::Text) &&
|
138
|
+
child.text.strip.empty? &&
|
139
|
+
!(child.previous.nil? && child.next.nil?)
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
def parent(node)
|
144
|
+
node.parent if node.respond_to?(:parent)
|
145
|
+
end
|
146
|
+
|
147
|
+
def next_sibling(node)
|
148
|
+
node.next
|
149
|
+
end
|
150
|
+
|
151
|
+
def previous_sibling(node)
|
152
|
+
node.previous
|
153
|
+
end
|
154
|
+
|
155
|
+
def document(node)
|
156
|
+
current = node
|
157
|
+
current = current.parent while parent(current)
|
158
|
+
|
159
|
+
current
|
160
|
+
end
|
161
|
+
|
162
|
+
def root(document)
|
163
|
+
document.children.find { |node| node.is_a?(::Oga::XML::Element) }
|
164
|
+
end
|
165
|
+
|
166
|
+
def attribute_element(attr)
|
167
|
+
attr.element
|
168
|
+
end
|
169
|
+
|
170
|
+
def attributes(element)
|
171
|
+
return [] unless element.respond_to?(:attributes)
|
172
|
+
|
173
|
+
# remove attributes-namespaces
|
174
|
+
element.attributes.reject do |attr|
|
175
|
+
attr.name == ::Oga::XML::Element::XMLNS_PREFIX || attr.namespace_name == ::Oga::XML::Element::XMLNS_PREFIX
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
def set_attribute(element, name, value)
|
180
|
+
namespace_name = nil
|
181
|
+
namespace_name, name = name.to_s.split(":", 2) if name.to_s.include?(":")
|
182
|
+
|
183
|
+
attr = ::Oga::XML::Attribute.new(
|
184
|
+
name: name.to_s,
|
185
|
+
namespace_name: namespace_name,
|
186
|
+
value: value.to_s
|
187
|
+
)
|
188
|
+
element.add_attribute(attr)
|
189
|
+
end
|
190
|
+
|
191
|
+
def get_attribute(element, name)
|
192
|
+
element.attribute(name.to_s)
|
193
|
+
end
|
194
|
+
|
195
|
+
def get_attribute_value(element, name)
|
196
|
+
element[name.to_s]
|
197
|
+
end
|
198
|
+
|
199
|
+
def remove_attribute(element, name)
|
200
|
+
attr = element.attribute(name.to_s)
|
201
|
+
element.attributes.delete(attr) if attr
|
202
|
+
end
|
203
|
+
|
204
|
+
def add_child(element, child_or_text)
|
205
|
+
child =
|
206
|
+
if child_or_text.is_a?(String)
|
207
|
+
create_native_text(child_or_text)
|
208
|
+
else
|
209
|
+
child_or_text
|
210
|
+
end
|
211
|
+
|
212
|
+
element.children << child
|
213
|
+
end
|
214
|
+
|
215
|
+
def add_previous_sibling(node, sibling)
|
216
|
+
node.before(sibling)
|
217
|
+
end
|
218
|
+
|
219
|
+
def add_next_sibling(node, sibling)
|
220
|
+
node.after(sibling)
|
221
|
+
end
|
222
|
+
|
223
|
+
def remove(node)
|
224
|
+
node.remove
|
225
|
+
end
|
226
|
+
|
227
|
+
def replace(node, new_node)
|
228
|
+
node.replace(new_node)
|
229
|
+
end
|
230
|
+
|
231
|
+
def replace_children(node, new_children)
|
232
|
+
node.inner_text = ""
|
233
|
+
new_children.each { |child| add_child(node, child) }
|
234
|
+
end
|
235
|
+
|
236
|
+
def text_content(node)
|
237
|
+
node.text
|
238
|
+
end
|
239
|
+
|
240
|
+
def set_text_content(node, content)
|
241
|
+
if node.respond_to?(:inner_text)
|
242
|
+
node.inner_text = content
|
243
|
+
else
|
244
|
+
# Oga::XML::Text node for example
|
245
|
+
node.text = content
|
246
|
+
end
|
247
|
+
end
|
248
|
+
|
249
|
+
def cdata_content(node)
|
250
|
+
node.text
|
251
|
+
end
|
252
|
+
|
253
|
+
def set_cdata_content(node, content)
|
254
|
+
node.text = content
|
255
|
+
end
|
256
|
+
|
257
|
+
def comment_content(node)
|
258
|
+
node.text
|
259
|
+
end
|
260
|
+
|
261
|
+
def set_comment_content(node, content)
|
262
|
+
node.text = content
|
263
|
+
end
|
264
|
+
|
265
|
+
def processing_instruction_content(node)
|
266
|
+
node.text
|
267
|
+
end
|
268
|
+
|
269
|
+
def set_processing_instruction_content(node, content)
|
270
|
+
node.text = content
|
271
|
+
end
|
272
|
+
|
273
|
+
def namespace_prefix(namespace)
|
274
|
+
# nil for the default namespace
|
275
|
+
return if namespace.name == ::Oga::XML::Element::XMLNS_PREFIX
|
276
|
+
|
277
|
+
namespace.name
|
278
|
+
end
|
279
|
+
|
280
|
+
def namespace_uri(namespace)
|
281
|
+
namespace.uri
|
282
|
+
end
|
283
|
+
|
284
|
+
def namespace_definitions(node)
|
285
|
+
return [] unless node.respond_to?(:namespaces)
|
286
|
+
|
287
|
+
node.namespaces.values
|
288
|
+
end
|
289
|
+
|
290
|
+
def xpath(node, expression, _namespaces = {})
|
291
|
+
node.xpath(expression).to_a
|
292
|
+
rescue ::LL::ParserError => e
|
293
|
+
raise Moxml::XPathError, e.message
|
294
|
+
end
|
295
|
+
|
296
|
+
def at_xpath(node, expression, _namespaces = {})
|
297
|
+
node.at_xpath(expression)
|
298
|
+
rescue ::Oga::XPath::Error => e
|
299
|
+
raise Moxml::XPathError, e.message
|
300
|
+
end
|
301
|
+
|
302
|
+
def serialize(node, _options = {})
|
303
|
+
# Expand empty tags, encode attributes, etc
|
304
|
+
::Moxml::Adapter::CustomizedOga::XmlGenerator.new(node).to_xml
|
305
|
+
end
|
306
|
+
end
|
307
|
+
end
|
308
|
+
end
|
309
|
+
end
|
@@ -0,0 +1,325 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "base"
|
4
|
+
require "ox"
|
5
|
+
|
6
|
+
module Moxml
|
7
|
+
module Adapter
|
8
|
+
class Ox < Base
|
9
|
+
class << self
|
10
|
+
def set_root(doc, element)
|
11
|
+
replace_children(doc, [element])
|
12
|
+
end
|
13
|
+
|
14
|
+
def parse(xml, _options = {})
|
15
|
+
native_doc = begin
|
16
|
+
result = ::Ox.parse(xml)
|
17
|
+
|
18
|
+
# result can be either Document or Element
|
19
|
+
if result.is_a?(::Ox::Document)
|
20
|
+
result
|
21
|
+
else
|
22
|
+
doc = ::Ox::Document.new
|
23
|
+
doc << result
|
24
|
+
doc
|
25
|
+
end
|
26
|
+
rescue ::Ox::ParseError => e
|
27
|
+
raise Moxml::ParseError, e.message
|
28
|
+
end
|
29
|
+
|
30
|
+
DocumentBuilder.new(Context.new(:ox)).build(native_doc)
|
31
|
+
end
|
32
|
+
|
33
|
+
def create_document
|
34
|
+
::Ox::Document.new
|
35
|
+
end
|
36
|
+
|
37
|
+
def create_native_element(name)
|
38
|
+
element = ::Ox::Element.new(name)
|
39
|
+
element.instance_variable_set(:@attributes, {})
|
40
|
+
element
|
41
|
+
end
|
42
|
+
|
43
|
+
def create_native_text(content)
|
44
|
+
content
|
45
|
+
end
|
46
|
+
|
47
|
+
def create_native_cdata(content)
|
48
|
+
::Ox::CData.new(content)
|
49
|
+
end
|
50
|
+
|
51
|
+
def create_native_comment(content)
|
52
|
+
::Ox::Comment.new(content)
|
53
|
+
end
|
54
|
+
|
55
|
+
def create_native_processing_instruction(target, content)
|
56
|
+
inst = ::Ox::Instruction.new(target)
|
57
|
+
inst.value = content
|
58
|
+
inst
|
59
|
+
end
|
60
|
+
|
61
|
+
# TODO: compare to create_native_declaration
|
62
|
+
def create_native_declaration2(version, encoding, standalone)
|
63
|
+
inst = ::Ox::Instruct.new("xml")
|
64
|
+
inst.value = build_declaration_attrs(version, encoding, standalone)
|
65
|
+
inst
|
66
|
+
end
|
67
|
+
|
68
|
+
def create_native_declaration(version, encoding, standalone)
|
69
|
+
doc = ::Ox::Document.new
|
70
|
+
doc.version = version
|
71
|
+
doc.encoding = encoding
|
72
|
+
doc.standalone = standalone
|
73
|
+
doc
|
74
|
+
end
|
75
|
+
|
76
|
+
def create_native_namespace(element, prefix, uri)
|
77
|
+
element.attributes ||= {}
|
78
|
+
attr_name = prefix ? "xmlns:#{prefix}" : "xmlns"
|
79
|
+
element.attributes[attr_name] = uri
|
80
|
+
[prefix, uri]
|
81
|
+
end
|
82
|
+
|
83
|
+
def set_namespace(element, ns)
|
84
|
+
prefix, uri = ns
|
85
|
+
element.attributes ||= {}
|
86
|
+
attr_name = prefix ? "xmlns:#{prefix}" : "xmlns"
|
87
|
+
element.attributes[attr_name] = uri
|
88
|
+
end
|
89
|
+
|
90
|
+
def namespace(element)
|
91
|
+
return nil unless element.attributes
|
92
|
+
|
93
|
+
xmlns_attr = element.attributes.find { |k, _| k.start_with?("xmlns:") || k == "xmlns" }
|
94
|
+
return nil unless xmlns_attr
|
95
|
+
|
96
|
+
prefix = xmlns_attr[0] == "xmlns" ? nil : xmlns_attr[0].sub("xmlns:", "")
|
97
|
+
[prefix, xmlns_attr[1]]
|
98
|
+
end
|
99
|
+
|
100
|
+
def processing_instruction_target(node)
|
101
|
+
node.name
|
102
|
+
end
|
103
|
+
|
104
|
+
def node_type(node)
|
105
|
+
case node
|
106
|
+
when ::Ox::Document then :document
|
107
|
+
when String then :text
|
108
|
+
when ::Ox::CData then :cdata
|
109
|
+
when ::Ox::Comment then :comment
|
110
|
+
when ::Ox::Instruct then :processing_instruction
|
111
|
+
when ::Ox::Element then :element
|
112
|
+
else :unknown
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
def node_name(node)
|
117
|
+
node.value
|
118
|
+
rescue StandardError
|
119
|
+
node.name
|
120
|
+
end
|
121
|
+
|
122
|
+
def set_node_name(node, name)
|
123
|
+
node.value = name if node.respond_to?(:value=)
|
124
|
+
node.name = name if node.respond_to?(:name=)
|
125
|
+
end
|
126
|
+
|
127
|
+
def children(node)
|
128
|
+
return [] unless node.respond_to?(:nodes)
|
129
|
+
|
130
|
+
node.nodes || []
|
131
|
+
end
|
132
|
+
|
133
|
+
def parent(node)
|
134
|
+
node.parent if node.respond_to?(:parent)
|
135
|
+
end
|
136
|
+
|
137
|
+
def next_sibling(node)
|
138
|
+
return unless (parent = parent(node))
|
139
|
+
|
140
|
+
siblings = parent.nodes
|
141
|
+
idx = siblings.index(node)
|
142
|
+
idx ? siblings[idx + 1] : nil
|
143
|
+
end
|
144
|
+
|
145
|
+
def previous_sibling(node)
|
146
|
+
return unless (parent = parent(node))
|
147
|
+
|
148
|
+
siblings = parent.nodes
|
149
|
+
idx = siblings.index(node)
|
150
|
+
idx&.positive? ? siblings[idx - 1] : nil
|
151
|
+
end
|
152
|
+
|
153
|
+
def document(node)
|
154
|
+
current = node
|
155
|
+
current = parent(current) while parent(current)
|
156
|
+
current
|
157
|
+
end
|
158
|
+
|
159
|
+
def root(document)
|
160
|
+
document.nodes&.find { |node| node.is_a?(::Ox::Element) }
|
161
|
+
end
|
162
|
+
|
163
|
+
def attributes(element)
|
164
|
+
return {} unless element.respond_to?(:attributes) && element.attributes
|
165
|
+
|
166
|
+
element.attributes.reject { |k, _| k.start_with?("xmlns") }
|
167
|
+
end
|
168
|
+
|
169
|
+
def set_attribute(element, name, value)
|
170
|
+
element.attributes ||= {}
|
171
|
+
element.attributes[name.to_s] = value.to_s
|
172
|
+
end
|
173
|
+
|
174
|
+
def get_attribute(element, name)
|
175
|
+
return nil unless element.respond_to?(:attributes) && element.attributes
|
176
|
+
|
177
|
+
element.attributes[name.to_s]
|
178
|
+
end
|
179
|
+
|
180
|
+
def remove_attribute(element, name)
|
181
|
+
return unless element.respond_to?(:attributes) && element.attributes
|
182
|
+
|
183
|
+
element.attributes.delete(name.to_s)
|
184
|
+
end
|
185
|
+
|
186
|
+
def add_child(element, child)
|
187
|
+
element.nodes ||= []
|
188
|
+
puts "Add child #{child} for #{element.name}: #{element.nodes.count}"
|
189
|
+
element.nodes << child
|
190
|
+
end
|
191
|
+
|
192
|
+
def add_previous_sibling(node, sibling)
|
193
|
+
return unless parent(node)
|
194
|
+
|
195
|
+
idx = node.parent.nodes.index(node)
|
196
|
+
node.parent.nodes.insert(idx, sibling) if idx
|
197
|
+
end
|
198
|
+
|
199
|
+
def add_next_sibling(node, sibling)
|
200
|
+
return unless parent(node)
|
201
|
+
|
202
|
+
idx = node.parent.nodes.index(node)
|
203
|
+
node.parent.nodes.insert(idx + 1, sibling) if idx
|
204
|
+
end
|
205
|
+
|
206
|
+
def remove(node)
|
207
|
+
return unless parent(node)
|
208
|
+
|
209
|
+
node.parent.nodes.delete(node)
|
210
|
+
end
|
211
|
+
|
212
|
+
def replace(node, new_node)
|
213
|
+
return unless parent(node)
|
214
|
+
|
215
|
+
idx = node.parent.nodes.index(node)
|
216
|
+
node.parent.nodes[idx] = new_node if idx
|
217
|
+
end
|
218
|
+
|
219
|
+
def replace_children(node, new_children)
|
220
|
+
node.remove_children_by_path("*")
|
221
|
+
new_children.each { |child| node << child }
|
222
|
+
node
|
223
|
+
end
|
224
|
+
|
225
|
+
def text_content(node)
|
226
|
+
node.is_a?(String) ? node : node.value.to_s
|
227
|
+
end
|
228
|
+
|
229
|
+
def set_text_content(node, content)
|
230
|
+
if node.is_a?(String)
|
231
|
+
node.replace(content.to_s)
|
232
|
+
else
|
233
|
+
node.value = content.to_s
|
234
|
+
end
|
235
|
+
end
|
236
|
+
|
237
|
+
def cdata_content(node)
|
238
|
+
node.value.to_s
|
239
|
+
end
|
240
|
+
|
241
|
+
def set_cdata_content(node, content)
|
242
|
+
node.value = content.to_s
|
243
|
+
end
|
244
|
+
|
245
|
+
def comment_content(node)
|
246
|
+
node.value.to_s
|
247
|
+
end
|
248
|
+
|
249
|
+
def set_comment_content(node, content)
|
250
|
+
node.value = content.to_s
|
251
|
+
end
|
252
|
+
|
253
|
+
def processing_instruction_content(node)
|
254
|
+
node.value.to_s
|
255
|
+
end
|
256
|
+
|
257
|
+
def set_processing_instruction_content(node, content)
|
258
|
+
node.value = content.to_s
|
259
|
+
end
|
260
|
+
|
261
|
+
def namespace_definitions(node)
|
262
|
+
return [] unless node.respond_to?(:attributes) && node.attributes
|
263
|
+
|
264
|
+
node.attributes.each_with_object([]) do |(name, value), namespaces|
|
265
|
+
next unless name.start_with?("xmlns")
|
266
|
+
|
267
|
+
prefix = name == "xmlns" ? nil : name.sub("xmlns:", "")
|
268
|
+
namespaces << [prefix, value]
|
269
|
+
end
|
270
|
+
end
|
271
|
+
|
272
|
+
def xpath(node, expression, namespaces = {})
|
273
|
+
# Ox doesn't support XPath, implement basic path matching
|
274
|
+
results = []
|
275
|
+
traverse(node) do |n|
|
276
|
+
results << n if matches_xpath?(n, expression, namespaces)
|
277
|
+
end
|
278
|
+
results
|
279
|
+
end
|
280
|
+
|
281
|
+
def at_xpath(node, expression, namespaces = {})
|
282
|
+
traverse(node) do |n|
|
283
|
+
return n if matches_xpath?(n, expression, namespaces)
|
284
|
+
end
|
285
|
+
nil
|
286
|
+
end
|
287
|
+
|
288
|
+
def serialize(node, options = {})
|
289
|
+
ox_options = {
|
290
|
+
indent: options[:indent] || -1,
|
291
|
+
with_xml: true,
|
292
|
+
with_instructions: true,
|
293
|
+
encoding: options[:encoding]
|
294
|
+
}
|
295
|
+
::Ox.dump(node, ox_options)
|
296
|
+
end
|
297
|
+
|
298
|
+
private
|
299
|
+
|
300
|
+
def traverse(node, &block)
|
301
|
+
return unless node
|
302
|
+
|
303
|
+
yield node
|
304
|
+
return unless node.respond_to?(:nodes)
|
305
|
+
|
306
|
+
node.nodes&.each { |child| traverse(child, &block) }
|
307
|
+
end
|
308
|
+
|
309
|
+
def matches_xpath?(node, expression, _namespaces = {})
|
310
|
+
case expression
|
311
|
+
when %r{//(\w+)}
|
312
|
+
node.is_a?(::Ox::Element) && node.value == ::Regexp.last_match(1)
|
313
|
+
when %r{//(\w+)\[@(\w+)='([^']+)'\]}
|
314
|
+
node.is_a?(::Ox::Element) &&
|
315
|
+
node.value == ::Regexp.last_match(1) &&
|
316
|
+
node.attributes &&
|
317
|
+
node.attributes[::Regexp.last_match(2)] == ::Regexp.last_match(3)
|
318
|
+
else
|
319
|
+
false
|
320
|
+
end
|
321
|
+
end
|
322
|
+
end
|
323
|
+
end
|
324
|
+
end
|
325
|
+
end
|