moxml 0.1.0 → 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (77) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/rake.yml +15 -0
  3. data/.github/workflows/release.yml +23 -0
  4. data/.gitignore +3 -0
  5. data/.rubocop.yml +2 -0
  6. data/.rubocop_todo.yml +65 -0
  7. data/.ruby-version +1 -0
  8. data/Gemfile +10 -3
  9. data/README.adoc +401 -594
  10. data/lib/moxml/adapter/base.rb +102 -0
  11. data/lib/moxml/adapter/customized_oga/xml_declaration.rb +18 -0
  12. data/lib/moxml/adapter/customized_oga/xml_generator.rb +104 -0
  13. data/lib/moxml/adapter/nokogiri.rb +319 -0
  14. data/lib/moxml/adapter/oga.rb +318 -0
  15. data/lib/moxml/adapter/ox.rb +325 -0
  16. data/lib/moxml/adapter.rb +26 -170
  17. data/lib/moxml/attribute.rb +47 -14
  18. data/lib/moxml/builder.rb +64 -0
  19. data/lib/moxml/cdata.rb +4 -26
  20. data/lib/moxml/comment.rb +6 -22
  21. data/lib/moxml/config.rb +39 -15
  22. data/lib/moxml/context.rb +29 -0
  23. data/lib/moxml/declaration.rb +16 -26
  24. data/lib/moxml/doctype.rb +9 -0
  25. data/lib/moxml/document.rb +51 -63
  26. data/lib/moxml/document_builder.rb +87 -0
  27. data/lib/moxml/element.rb +63 -97
  28. data/lib/moxml/error.rb +20 -0
  29. data/lib/moxml/namespace.rb +12 -37
  30. data/lib/moxml/node.rb +78 -58
  31. data/lib/moxml/node_set.rb +19 -222
  32. data/lib/moxml/processing_instruction.rb +6 -25
  33. data/lib/moxml/text.rb +4 -26
  34. data/lib/moxml/version.rb +1 -1
  35. data/lib/moxml/xml_utils/encoder.rb +55 -0
  36. data/lib/moxml/xml_utils.rb +80 -0
  37. data/lib/moxml.rb +33 -33
  38. data/moxml.gemspec +1 -1
  39. data/spec/moxml/adapter/nokogiri_spec.rb +14 -0
  40. data/spec/moxml/adapter/oga_spec.rb +14 -0
  41. data/spec/moxml/adapter/ox_spec.rb +49 -0
  42. data/spec/moxml/all_with_adapters_spec.rb +46 -0
  43. data/spec/moxml/config_spec.rb +55 -0
  44. data/spec/moxml/error_spec.rb +71 -0
  45. data/spec/moxml/examples/adapter_spec.rb +27 -0
  46. data/spec/moxml_spec.rb +50 -0
  47. data/spec/spec_helper.rb +32 -0
  48. data/spec/support/shared_examples/attribute.rb +165 -0
  49. data/spec/support/shared_examples/builder.rb +25 -0
  50. data/spec/support/shared_examples/cdata.rb +70 -0
  51. data/spec/support/shared_examples/comment.rb +65 -0
  52. data/spec/support/shared_examples/context.rb +35 -0
  53. data/spec/support/shared_examples/declaration.rb +93 -0
  54. data/spec/support/shared_examples/doctype.rb +25 -0
  55. data/spec/support/shared_examples/document.rb +110 -0
  56. data/spec/support/shared_examples/document_builder.rb +43 -0
  57. data/spec/support/shared_examples/edge_cases.rb +185 -0
  58. data/spec/support/shared_examples/element.rb +130 -0
  59. data/spec/support/shared_examples/examples/attribute.rb +42 -0
  60. data/spec/support/shared_examples/examples/basic_usage.rb +67 -0
  61. data/spec/support/shared_examples/examples/memory.rb +54 -0
  62. data/spec/support/shared_examples/examples/namespace.rb +65 -0
  63. data/spec/support/shared_examples/examples/readme_examples.rb +100 -0
  64. data/spec/support/shared_examples/examples/thread_safety.rb +43 -0
  65. data/spec/support/shared_examples/examples/xpath.rb +39 -0
  66. data/spec/support/shared_examples/integration.rb +135 -0
  67. data/spec/support/shared_examples/namespace.rb +96 -0
  68. data/spec/support/shared_examples/node.rb +110 -0
  69. data/spec/support/shared_examples/node_set.rb +90 -0
  70. data/spec/support/shared_examples/processing_instruction.rb +88 -0
  71. data/spec/support/shared_examples/text.rb +66 -0
  72. data/spec/support/shared_examples/xml_adapter.rb +191 -0
  73. data/spec/support/xml_matchers.rb +27 -0
  74. metadata +55 -6
  75. data/.github/workflows/main.yml +0 -27
  76. data/lib/moxml/error_handler.rb +0 -77
  77. data/lib/moxml/errors.rb +0 -169
@@ -0,0 +1,318 @@
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 inner_text(node)
241
+ if node.respond_to?(:inner_text)
242
+ node.inner_text
243
+ else
244
+ # Oga::XML::Text node for example
245
+ node.text
246
+ end
247
+ end
248
+
249
+ def set_text_content(node, content)
250
+ if node.respond_to?(:inner_text)
251
+ node.inner_text = content
252
+ else
253
+ # Oga::XML::Text node for example
254
+ node.text = content
255
+ end
256
+ end
257
+
258
+ def cdata_content(node)
259
+ node.text
260
+ end
261
+
262
+ def set_cdata_content(node, content)
263
+ node.text = content
264
+ end
265
+
266
+ def comment_content(node)
267
+ node.text
268
+ end
269
+
270
+ def set_comment_content(node, content)
271
+ node.text = content
272
+ end
273
+
274
+ def processing_instruction_content(node)
275
+ node.text
276
+ end
277
+
278
+ def set_processing_instruction_content(node, content)
279
+ node.text = content
280
+ end
281
+
282
+ def namespace_prefix(namespace)
283
+ # nil for the default namespace
284
+ return if namespace.name == ::Oga::XML::Element::XMLNS_PREFIX
285
+
286
+ namespace.name
287
+ end
288
+
289
+ def namespace_uri(namespace)
290
+ namespace.uri
291
+ end
292
+
293
+ def namespace_definitions(node)
294
+ return [] unless node.respond_to?(:namespaces)
295
+
296
+ node.namespaces.values
297
+ end
298
+
299
+ def xpath(node, expression, _namespaces = {})
300
+ node.xpath(expression).to_a
301
+ rescue ::LL::ParserError => e
302
+ raise Moxml::XPathError, e.message
303
+ end
304
+
305
+ def at_xpath(node, expression, _namespaces = {})
306
+ node.at_xpath(expression)
307
+ rescue ::Oga::XPath::Error => e
308
+ raise Moxml::XPathError, e.message
309
+ end
310
+
311
+ def serialize(node, _options = {})
312
+ # Expand empty tags, encode attributes, etc
313
+ ::Moxml::Adapter::CustomizedOga::XmlGenerator.new(node).to_xml
314
+ end
315
+ end
316
+ end
317
+ end
318
+ 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