moxml 0.1.0 → 0.1.2

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.
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