moxml 0.1.3 → 0.1.6

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 (55) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +0 -1
  3. data/.rubocop_todo.yml +48 -20
  4. data/Gemfile +3 -0
  5. data/LICENSE.md +33 -0
  6. data/README.adoc +95 -23
  7. data/lib/moxml/adapter/base.rb +20 -2
  8. data/lib/moxml/adapter/customized_ox/attribute.rb +29 -0
  9. data/lib/moxml/adapter/customized_ox/namespace.rb +34 -0
  10. data/lib/moxml/adapter/customized_ox/text.rb +12 -0
  11. data/lib/moxml/adapter/customized_rexml/formatter.rb +195 -0
  12. data/lib/moxml/adapter/nokogiri.rb +4 -2
  13. data/lib/moxml/adapter/oga.rb +25 -9
  14. data/lib/moxml/adapter/ox.rb +238 -92
  15. data/lib/moxml/adapter/rexml.rb +462 -0
  16. data/lib/moxml/adapter.rb +1 -1
  17. data/lib/moxml/attribute.rb +2 -2
  18. data/lib/moxml/cdata.rb +0 -4
  19. data/lib/moxml/comment.rb +0 -4
  20. data/lib/moxml/config.rb +1 -1
  21. data/lib/moxml/context.rb +2 -2
  22. data/lib/moxml/doctype.rb +1 -5
  23. data/lib/moxml/document.rb +1 -1
  24. data/lib/moxml/document_builder.rb +14 -18
  25. data/lib/moxml/element.rb +4 -3
  26. data/lib/moxml/namespace.rb +5 -1
  27. data/lib/moxml/node.rb +17 -2
  28. data/lib/moxml/node_set.rb +8 -1
  29. data/lib/moxml/processing_instruction.rb +0 -4
  30. data/lib/moxml/text.rb +0 -4
  31. data/lib/moxml/version.rb +1 -1
  32. data/lib/ox/node.rb +9 -0
  33. data/spec/fixtures/small.xml +1 -0
  34. data/spec/moxml/adapter/rexml_spec.rb +14 -0
  35. data/spec/moxml/all_with_adapters_spec.rb +2 -3
  36. data/spec/support/shared_examples/builder.rb +19 -2
  37. data/spec/support/shared_examples/cdata.rb +7 -5
  38. data/spec/support/shared_examples/declaration.rb +17 -4
  39. data/spec/support/shared_examples/doctype.rb +2 -1
  40. data/spec/support/shared_examples/document.rb +10 -0
  41. data/spec/support/shared_examples/edge_cases.rb +9 -3
  42. data/spec/support/shared_examples/element.rb +5 -1
  43. data/spec/support/shared_examples/examples/benchmark_spec.rb +51 -0
  44. data/spec/support/shared_examples/examples/memory.rb +30 -17
  45. data/spec/support/shared_examples/examples/readme_examples.rb +5 -0
  46. data/spec/support/shared_examples/examples/thread_safety.rb +2 -0
  47. data/spec/support/shared_examples/examples/xpath.rb +34 -3
  48. data/spec/support/shared_examples/integration.rb +6 -2
  49. data/spec/support/shared_examples/namespace.rb +16 -0
  50. data/spec/support/shared_examples/node.rb +4 -0
  51. data/spec/support/shared_examples/node_set.rb +20 -0
  52. data/spec/support/shared_examples/processing_instruction.rb +1 -1
  53. data/spec/support/shared_examples/text.rb +2 -1
  54. data/spec/support/shared_examples/xml_adapter.rb +169 -7
  55. metadata +13 -3
@@ -1,7 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "base"
4
+ # before ox so that all Ox classes inherit the monkey-patched Node
5
+ require_relative "../../ox/node"
4
6
  require "ox"
7
+ require_relative "customized_ox/text"
8
+ require_relative "customized_ox/attribute"
9
+ require_relative "customized_ox/namespace"
5
10
 
6
11
  module Moxml
7
12
  module Adapter
@@ -30,8 +35,9 @@ module Moxml
30
35
  DocumentBuilder.new(Context.new(:ox)).build(native_doc)
31
36
  end
32
37
 
33
- def create_document
34
- ::Ox::Document.new
38
+ def create_document(native_doc = nil)
39
+ attrs = native_doc&.attributes || {}
40
+ ::Ox::Document.new(**attrs)
35
41
  end
36
42
 
37
43
  def create_native_element(name)
@@ -52,63 +58,94 @@ module Moxml
52
58
  ::Ox::Comment.new(content)
53
59
  end
54
60
 
61
+ def create_native_doctype(name, external_id, system_id)
62
+ ::Ox::DocType.new(
63
+ "#{name} PUBLIC \"#{external_id}\" \"#{system_id}\""
64
+ )
65
+ end
66
+
55
67
  def create_native_processing_instruction(target, content)
56
- inst = ::Ox::Instruction.new(target)
57
- inst.value = content
68
+ inst = ::Ox::Instruct.new(target)
69
+ set_processing_instruction_content(inst, content)
58
70
  inst
59
71
  end
60
72
 
61
- # TODO: compare to create_native_declaration
62
- def create_native_declaration2(version, encoding, standalone)
73
+ def create_native_declaration(version, encoding, standalone)
63
74
  inst = ::Ox::Instruct.new("xml")
64
- inst.value = build_declaration_attrs(version, encoding, standalone)
75
+ set_attribute(inst, "version", version)
76
+ set_attribute(inst, "encoding", encoding)
77
+ set_attribute(inst, "standalone", standalone)
65
78
  inst
66
79
  end
67
80
 
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
81
+ def declaration_attribute(declaration, attr_name)
82
+ get_attribute_value(declaration, attr_name)
83
+ end
84
+
85
+ def set_declaration_attribute(declaration, attr_name, value)
86
+ set_attribute(declaration, attr_name, value)
74
87
  end
75
88
 
76
89
  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]
90
+ ns = ::Moxml::Adapter::CustomizedOx::Namespace.new(prefix, uri, element)
91
+ set_attribute(element, ns.expanded_prefix, uri)
92
+ ns
81
93
  end
82
94
 
83
95
  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
96
+ return unless element.respond_to?(:name)
97
+
98
+ prefix = ns.prefix
99
+ # attributes don't have attributes but can have a namespace prefix
100
+ set_attribute(element, ns.expanded_prefix, ns.uri) if element.respond_to?(:attributes)
101
+ element.name = [prefix, element.name.delete_prefix("xmlns:")].compact.join(":")
102
+ namespace(element)
88
103
  end
89
104
 
90
105
  def namespace(element)
91
- return nil unless element.attributes
106
+ prefix =
107
+ if element.respond_to?(:prefix)
108
+ # attribute
109
+ element.prefix
110
+ elsif element.name.include?(":")
111
+ element.name.split(":").first
112
+ end
113
+ attr_name = ["xmlns", prefix].compact.join(":")
92
114
 
93
- xmlns_attr = element.attributes.find { |k, _| k.start_with?("xmlns:") || k == "xmlns" }
94
- return nil unless xmlns_attr
115
+ ([element] + ancestors(element)).each do |node|
116
+ next unless node.respond_to?(:attributes) && node.attributes
95
117
 
96
- prefix = xmlns_attr[0] == "xmlns" ? nil : xmlns_attr[0].sub("xmlns:", "")
97
- [prefix, xmlns_attr[1]]
118
+ if node[attr_name]
119
+ return ::Moxml::Adapter::CustomizedOx::Namespace.new(
120
+ prefix, node[attr_name], element
121
+ )
122
+ end
123
+ end
124
+
125
+ nil
126
+ end
127
+
128
+ def ancestors(node)
129
+ return [] unless (parent = parent(node))
130
+
131
+ [parent] + ancestors(parent)
98
132
  end
99
133
 
100
134
  def processing_instruction_target(node)
101
- node.name
135
+ node.target
102
136
  end
103
137
 
104
138
  def node_type(node)
105
139
  case node
106
140
  when ::Ox::Document then :document
107
- when String then :text
141
+ when ::Moxml::Adapter::CustomizedOx::Text, String then :text
108
142
  when ::Ox::CData then :cdata
109
143
  when ::Ox::Comment then :comment
110
144
  when ::Ox::Instruct then :processing_instruction
111
145
  when ::Ox::Element then :element
146
+ when ::Ox::DocType then :doctype
147
+ when ::Moxml::Adapter::CustomizedOx::Namespace then :banespace
148
+ when ::Moxml::Adapter::CustomizedOx::Attribute then :attribute
112
149
  else :unknown
113
150
  end
114
151
  end
@@ -120,8 +157,40 @@ module Moxml
120
157
  end
121
158
 
122
159
  def set_node_name(node, name)
123
- node.value = name if node.respond_to?(:value=)
124
- node.name = name if node.respond_to?(:name=)
160
+ if node.respond_to?(:name=)
161
+ node.name = name
162
+ elsif node.respond_to?(:value=)
163
+ node.value = name
164
+ end
165
+ end
166
+
167
+ def duplicate_node(node)
168
+ Marshal.load(Marshal.dump(node))
169
+ end
170
+
171
+ def patch_node(node, parent = nil)
172
+ new_node =
173
+ case node
174
+ # it can be either attribute or namespace
175
+ when Array then ::Moxml::Adapter::CustomizedOx::Attribute.new(node.first, node.last)
176
+ when Hash then ::Moxml::Adapter::CustomizedOx::Attribute.new(node.keys.first, node.values.first)
177
+ when String then ::Moxml::Adapter::CustomizedOx::Text.new(node)
178
+ else node
179
+ end
180
+
181
+ new_node.parent = parent if new_node.respond_to?(:parent)
182
+
183
+ new_node
184
+ end
185
+
186
+ def unpatch_node(node)
187
+ case node
188
+ # it can be either attribute or namespace
189
+ when ::Moxml::Adapter::CustomizedOx::Attribute then [node.name, node.value]
190
+ # when ::Moxml::Adapter::CustomizedOx::Attribute then { node.name => node.value }
191
+ when ::Moxml::Adapter::CustomizedOx::Text then node.value
192
+ else node
193
+ end
125
194
  end
126
195
 
127
196
  def children(node)
@@ -135,19 +204,19 @@ module Moxml
135
204
  end
136
205
 
137
206
  def next_sibling(node)
138
- return unless (parent = parent(node))
207
+ return unless (parent = node.parent)
139
208
 
140
209
  siblings = parent.nodes
141
- idx = siblings.index(node)
142
- idx ? siblings[idx + 1] : nil
210
+ idx = siblings.index(unpatch_node(node))
211
+ idx ? patch_node(siblings[idx + 1], parent) : nil
143
212
  end
144
213
 
145
214
  def previous_sibling(node)
146
215
  return unless (parent = parent(node))
147
216
 
148
217
  siblings = parent.nodes
149
- idx = siblings.index(node)
150
- idx&.positive? ? siblings[idx - 1] : nil
218
+ idx = siblings.index(unpatch_node(node))
219
+ idx&.positive? ? patch_node(siblings[idx - 1], parent) : nil
151
220
  end
152
221
 
153
222
  def document(node)
@@ -161,74 +230,153 @@ module Moxml
161
230
  end
162
231
 
163
232
  def attributes(element)
164
- return {} unless element.respond_to?(:attributes) && element.attributes
233
+ return [] unless element.respond_to?(:attributes) && element.attributes
165
234
 
166
- element.attributes.reject { |k, _| k.start_with?("xmlns") }
235
+ element.attributes.map do |name, value|
236
+ next if name.start_with?("xmlns")
237
+
238
+ ::Moxml::Adapter::CustomizedOx::Attribute.new(
239
+ name, value, element
240
+ )
241
+ end.compact
242
+ end
243
+
244
+ def attribute_element(attribute)
245
+ attribute.parent
167
246
  end
168
247
 
169
248
  def set_attribute(element, name, value)
170
249
  element.attributes ||= {}
171
- element.attributes[name.to_s] = value.to_s
250
+ if value.nil?
251
+ # Ox converts all values to strings
252
+ remove_attribute(element, name)
253
+ else
254
+ element.attributes[name.to_s] = value
255
+ end
256
+
257
+ ::Moxml::Adapter::CustomizedOx::Attribute.new(
258
+ name.to_s, value&.to_s, element
259
+ )
260
+ end
261
+
262
+ def set_attribute_name(attribute, name)
263
+ old_name = attribute.name
264
+ attribute.name = name.to_s
265
+ # Ox doesn't change the keys of the attributes hash
266
+ element = attribute.parent
267
+ element.attributes.delete(old_name)
268
+ element.attributes[name] = attribute.value
269
+ end
270
+
271
+ def set_attribute_value(attribute, new_value)
272
+ if new_value.nil?
273
+ # Ox converts all values to strings
274
+ remove_attribute(attribute.parent, attribute.name)
275
+ else
276
+ attribute.value = new_value
277
+ attribute.parent.attributes[attribute.name] = new_value
278
+ end
172
279
  end
173
280
 
174
281
  def get_attribute(element, name)
175
- return nil unless element.respond_to?(:attributes) && element.attributes
282
+ return unless element.respond_to?(:attributes) && element.attributes
283
+ return unless element.attributes.key?(name.to_s) || element.attributes.key?(name.to_s.to_sym)
284
+
285
+ ::Moxml::Adapter::CustomizedOx::Attribute.new(
286
+ name.to_s, element.attributes[name], element
287
+ )
288
+ end
176
289
 
177
- element.attributes[name.to_s]
290
+ def get_attribute_value(element, name)
291
+ element[name]
178
292
  end
179
293
 
180
294
  def remove_attribute(element, name)
181
295
  return unless element.respond_to?(:attributes) && element.attributes
182
296
 
183
297
  element.attributes.delete(name.to_s)
298
+ element.attributes.delete(name.to_s.to_sym)
184
299
  end
185
300
 
186
301
  def add_child(element, child)
302
+ child.parent = element if child.respond_to?(:parent)
187
303
  element.nodes ||= []
188
- puts "Add child #{child} for #{element.name}: #{element.nodes.count}"
189
304
  element.nodes << child
190
305
  end
191
306
 
192
307
  def add_previous_sibling(node, sibling)
193
- return unless parent(node)
308
+ return unless (parent = parent(node))
194
309
 
195
- idx = node.parent.nodes.index(node)
196
- node.parent.nodes.insert(idx, sibling) if idx
310
+ if sibling.respond_to?(:parent)
311
+ sibling.parent&.nodes&.delete(sibling)
312
+ sibling.parent = parent
313
+ end
314
+ idx = parent.nodes.index(node)
315
+ parent.nodes.insert(idx, sibling) if idx
197
316
  end
198
317
 
199
318
  def add_next_sibling(node, sibling)
200
- return unless parent(node)
319
+ return unless (parent = parent(node))
201
320
 
202
- idx = node.parent.nodes.index(node)
203
- node.parent.nodes.insert(idx + 1, sibling) if idx
321
+ if sibling.respond_to?(:parent)
322
+ sibling.parent&.nodes&.delete(sibling)
323
+ sibling.parent = parent
324
+ end
325
+ idx = parent.nodes.index(node)
326
+ parent.nodes.insert(idx + 1, sibling) if idx
204
327
  end
205
328
 
206
329
  def remove(node)
330
+ return node.clear if node.is_a?(String)
331
+
207
332
  return unless parent(node)
208
333
 
209
- node.parent.nodes.delete(node)
334
+ parent(node).nodes.delete(node)
210
335
  end
211
336
 
212
337
  def replace(node, new_node)
213
- return unless parent(node)
338
+ return node.replace(new_node) if node.is_a?(String) && new_node.is_a?(String)
339
+ # There are other cases:
340
+ # when node is a String and new_node isn't
341
+ # when node isn't a String, and new_node is a String
342
+
343
+ return unless (parent = parent(node))
214
344
 
215
- idx = node.parent.nodes.index(node)
216
- node.parent.nodes[idx] = new_node if idx
345
+ new_node.parent = parent if new_node.respond_to?(:parent)
346
+ idx = parent.nodes.index(node)
347
+ parent.nodes[idx] = new_node if idx
217
348
  end
218
349
 
219
350
  def replace_children(node, new_children)
220
351
  node.remove_children_by_path("*")
221
- new_children.each { |child| node << child }
352
+ new_children.each do |child|
353
+ child.parent = node if child.respond_to?(:parent)
354
+ node << child
355
+ end
222
356
  node
223
357
  end
224
358
 
225
359
  def text_content(node)
226
- node.is_a?(String) ? node : node.value.to_s
360
+ case node
361
+ when String then node.to_s
362
+ when ::Moxml::Adapter::CustomizedOx::Text then node.value
363
+ else
364
+ node.nodes.map do |n|
365
+ text_content(n)
366
+ end.join
367
+ end
368
+ end
369
+
370
+ def inner_text(node)
371
+ return "" unless node.respond_to?(:nodes)
372
+
373
+ node.nodes.select { _1.is_a?(String) }.join
227
374
  end
228
375
 
229
376
  def set_text_content(node, content)
230
- if node.is_a?(String)
231
- node.replace(content.to_s)
377
+ case node
378
+ when String then node.replace(content.to_s)
379
+ when ::Ox::Element then node.replace_text(content.to_s)
232
380
  else
233
381
  node.value = content.to_s
234
382
  end
@@ -251,48 +399,60 @@ module Moxml
251
399
  end
252
400
 
253
401
  def processing_instruction_content(node)
254
- node.value.to_s
402
+ node.content.to_s
255
403
  end
256
404
 
257
405
  def set_processing_instruction_content(node, content)
258
- node.value = content.to_s
406
+ node.content = content.to_s
407
+ end
408
+
409
+ def namespace_prefix(namespace)
410
+ namespace.prefix
411
+ end
412
+
413
+ def namespace_uri(namespace)
414
+ namespace.uri
259
415
  end
260
416
 
261
417
  def namespace_definitions(node)
262
- return [] unless node.respond_to?(:attributes) && node.attributes
418
+ ([node] + ancestors(node)).reverse.each_with_object({}) do |n, namespaces|
419
+ next unless n.respond_to?(:attributes) && n.attributes
263
420
 
264
- node.attributes.each_with_object([]) do |(name, value), namespaces|
265
- next unless name.start_with?("xmlns")
421
+ n.attributes.each do |name, value|
422
+ next unless name.to_s.start_with?("xmlns")
266
423
 
267
- prefix = name == "xmlns" ? nil : name.sub("xmlns:", "")
268
- namespaces << [prefix, value]
269
- end
424
+ namespaces[name] = ::Moxml::Adapter::CustomizedOx::Namespace.new(
425
+ name, value, n
426
+ )
427
+ end
428
+ end.values
270
429
  end
271
430
 
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
431
+ def xpath(node, expression, _namespaces = {})
432
+ # locate has a different syntax
433
+ node.locate(expression)
279
434
  end
280
435
 
281
436
  def at_xpath(node, expression, namespaces = {})
282
- traverse(node) do |n|
283
- return n if matches_xpath?(n, expression, namespaces)
284
- end
285
- nil
437
+ xpath(node, expression, namespaces)&.first
286
438
  end
287
439
 
288
440
  def serialize(node, options = {})
441
+ output = ""
442
+ if node.is_a?(::Ox::Document)
443
+ # add declaration
444
+ decl = create_native_declaration(node[:version], node[:encoding], node[:standalone])
445
+ output = ::Ox.dump(::Ox::Document.new << decl).strip
446
+ end
447
+
289
448
  ox_options = {
290
- indent: options[:indent] || -1,
291
- with_xml: true,
449
+ indent: -1, # options[:indent] || -1, # indent is a beast
450
+ # with_xml: true,
292
451
  with_instructions: true,
293
- encoding: options[:encoding]
452
+ encoding: options[:encoding],
453
+ no_empty: options[:expand_empty]
294
454
  }
295
- ::Ox.dump(node, ox_options)
455
+ output + ::Ox.dump(node, ox_options)
296
456
  end
297
457
 
298
458
  private
@@ -305,20 +465,6 @@ module Moxml
305
465
 
306
466
  node.nodes&.each { |child| traverse(child, &block) }
307
467
  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
468
  end
323
469
  end
324
470
  end