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
data/lib/moxml/node.rb CHANGED
@@ -1,113 +1,133 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "xml_utils"
4
+ require_relative "node_set"
5
+
1
6
  module Moxml
2
7
  class Node
3
- attr_reader :native
4
-
5
- def initialize(native_node = nil)
6
- @native = native_node || create_native_node
7
- end
8
+ include XmlUtils
8
9
 
9
- def self.wrap(native_node)
10
- return nil if native_node.nil?
10
+ attr_reader :native, :context
11
11
 
12
- klass = case Moxml.adapter.node_type(native_node)
13
- when :element then Element
14
- when :text then Text
15
- when :cdata then Cdata
16
- when :comment then Comment
17
- when :processing_instruction then ProcessingInstruction
18
- when :document then Document
19
- when :attribute then Attribute
20
- when :namespace then Namespace
21
- else
22
- raise Error, "Unknown node type: #{native_node.class}"
23
- end
12
+ def initialize(native, context)
13
+ @native = native
14
+ @context = context
15
+ end
24
16
 
25
- klass.new(native_node)
17
+ def document
18
+ Document.wrap(adapter.document(@native), context)
26
19
  end
27
20
 
28
21
  def parent
29
- wrap_node(adapter.parent(native))
22
+ Node.wrap(adapter.parent(@native), context)
30
23
  end
31
24
 
32
25
  def children
33
- NodeSet.new(adapter.children(native))
26
+ NodeSet.new(adapter.children(@native), context)
34
27
  end
35
28
 
36
29
  def next_sibling
37
- wrap_node(adapter.next_sibling(native))
30
+ Node.wrap(adapter.next_sibling(@native), context)
38
31
  end
39
32
 
40
33
  def previous_sibling
41
- wrap_node(adapter.previous_sibling(native))
34
+ Node.wrap(adapter.previous_sibling(@native), context)
42
35
  end
43
36
 
44
- def remove
45
- adapter.remove(native)
46
- self
47
- end
48
-
49
- def replace(node)
50
- adapter.replace(native, node.native)
37
+ def add_child(node)
38
+ node = prepare_node(node)
39
+ adapter.add_child(@native, node.native)
51
40
  self
52
41
  end
53
42
 
54
43
  def add_previous_sibling(node)
55
- adapter.add_previous_sibling(native, node.native)
44
+ node = prepare_node(node)
45
+ adapter.add_previous_sibling(@native, node.native)
56
46
  self
57
47
  end
58
48
 
59
49
  def add_next_sibling(node)
60
- adapter.add_next_sibling(native, node.native)
50
+ node = prepare_node(node)
51
+ adapter.add_next_sibling(@native, node.native)
61
52
  self
62
53
  end
63
54
 
64
- def text
65
- adapter.text_content(native)
55
+ def remove
56
+ adapter.remove(@native)
57
+ self
66
58
  end
67
59
 
68
- def text=(content)
69
- adapter.set_text_content(native, content)
60
+ def replace(node)
61
+ node = prepare_node(node)
62
+ adapter.replace(@native, node.native)
70
63
  self
71
64
  end
72
65
 
73
- def inner_html
74
- adapter.inner_html(native)
66
+ def to_xml(options = {})
67
+ adapter.serialize(@native, default_options.merge(options))
75
68
  end
76
69
 
77
- def inner_html=(html)
78
- adapter.set_inner_html(native, html)
70
+ def xpath(expression, namespaces = {})
71
+ NodeSet.new(adapter.xpath(@native, expression, namespaces), context)
79
72
  end
80
73
 
81
- def outer_html
82
- adapter.outer_html(native)
74
+ def at_xpath(expression, namespaces = {})
75
+ Node.wrap(adapter.at_xpath(@native, expression, namespaces), context)
83
76
  end
84
77
 
85
- def path
86
- adapter.path(native)
78
+ def ==(other)
79
+ self.class == other.class && @native == other.native
87
80
  end
88
81
 
89
- def line
90
- adapter.line(native)
91
- end
82
+ def self.wrap(node, context)
83
+ return nil if node.nil?
92
84
 
93
- def column
94
- adapter.column(native)
85
+ klass = case adapter(context).node_type(node)
86
+ when :element then Element
87
+ when :text then Text
88
+ when :cdata then Cdata
89
+ when :comment then Comment
90
+ when :processing_instruction then ProcessingInstruction
91
+ when :document then Document
92
+ when :declaration then Declaration
93
+ when :doctype then Doctype
94
+ else self
95
+ end
96
+
97
+ klass.new(node, context)
95
98
  end
96
99
 
97
100
  protected
98
101
 
99
- def wrap_node(native_node)
100
- self.class.wrap(native_node)
102
+ def adapter
103
+ context.config.adapter
104
+ end
105
+
106
+ def self.adapter(context)
107
+ context.config.adapter
101
108
  end
102
109
 
103
110
  private
104
111
 
105
- def adapter
106
- Moxml.adapter
107
- end
108
-
109
- def create_native_node
110
- raise NotImplementedError, "Subclasses must implement create_native_node"
112
+ def prepare_node(node)
113
+ case node
114
+ when String then Text.new(adapter.create_text(node), context)
115
+ when Node then node
116
+ else
117
+ raise ArgumentError, "Invalid node type: #{node.class}"
118
+ end
119
+ end
120
+
121
+ def default_options
122
+ {
123
+ encoding: context.config.default_encoding,
124
+ indent: context.config.default_indent,
125
+ # The short format of empty tags in Oga and Nokogiri isn't configurable
126
+ # Oga: <empty /> (with a space)
127
+ # Nokogiri: <empty/> (without a space)
128
+ # The expanded format is enforced to avoid this conflict
129
+ expand_empty: true
130
+ }
111
131
  end
112
132
  end
113
133
  end
@@ -1,43 +1,46 @@
1
- # lib/moxml/node_set.rb
1
+ # frozen_string_literal: true
2
+
2
3
  module Moxml
3
4
  class NodeSet
4
5
  include Enumerable
5
6
 
6
- attr_reader :native_nodes
7
+ attr_reader :nodes, :context
7
8
 
8
- def initialize(native_nodes = [])
9
- @native_nodes = Array(native_nodes)
9
+ def initialize(nodes, context)
10
+ @nodes = Array(nodes)
11
+ @context = context
10
12
  end
11
13
 
12
14
  def each
13
- return enum_for(:each) unless block_given?
14
- native_nodes.each { |node| yield Node.wrap(node) }
15
+ return to_enum(:each) unless block_given?
16
+
17
+ nodes.each { |node| yield Node.wrap(node, context) }
15
18
  self
16
19
  end
17
20
 
18
21
  def [](index)
19
22
  case index
20
23
  when Integer
21
- Node.wrap(native_nodes[index])
24
+ Node.wrap(nodes[index], context)
22
25
  when Range
23
- NodeSet.new(native_nodes[index])
26
+ NodeSet.new(nodes[index], context)
24
27
  end
25
28
  end
26
29
 
27
30
  def first
28
- Node.wrap(native_nodes.first)
31
+ Node.wrap(nodes.first, context)
29
32
  end
30
33
 
31
34
  def last
32
- Node.wrap(native_nodes.last)
35
+ Node.wrap(nodes.last, context)
33
36
  end
34
37
 
35
38
  def empty?
36
- native_nodes.empty?
39
+ nodes.empty?
37
40
  end
38
41
 
39
42
  def size
40
- native_nodes.size
43
+ nodes.size
41
44
  end
42
45
 
43
46
  alias length size
@@ -46,223 +49,17 @@ module Moxml
46
49
  map { |node| node }
47
50
  end
48
51
 
49
- def filter(selector)
50
- NodeSet.new(
51
- native_nodes.select { |node| Moxml.adapter.matches?(node, selector) }
52
- )
53
- end
54
-
55
- def remove
56
- each(&:remove)
57
- self
52
+ def +(other)
53
+ self.class.new(nodes + other.nodes, context)
58
54
  end
59
55
 
60
56
  def text
61
57
  map(&:text).join
62
58
  end
63
59
 
64
- def inner_html
65
- map(&:inner_html).join
66
- end
67
-
68
- def wrap(html_or_element)
69
- each do |node|
70
- wrapper = case html_or_element
71
- when String
72
- Document.parse("<div>#{html_or_element}</div>").root.children.first
73
- when Element
74
- html_or_element.dup
75
- else
76
- raise ArgumentError, "Expected String or Element"
77
- end
78
-
79
- node.add_previous_sibling(wrapper)
80
- wrapper.add_child(node)
81
- end
82
- self
83
- end
84
-
85
- def add_class(names)
86
- each do |node|
87
- next unless node.is_a?(Element)
88
- current = (node["class"] || "").split(/\s+/)
89
- new_classes = names.is_a?(Array) ? names : names.split(/\s+/)
90
- node["class"] = (current + new_classes).uniq.join(" ")
91
- end
92
- self
93
- end
94
-
95
- def remove_class(names)
96
- each do |node|
97
- next unless node.is_a?(Element)
98
- current = (node["class"] || "").split(/\s+/)
99
- remove_classes = names.is_a?(Array) ? names : names.split(/\s+/)
100
- node["class"] = (current - remove_classes).join(" ")
101
- end
102
- self
103
- end
104
-
105
- def attr(name, value = nil)
106
- if value.nil?
107
- first&.[](name)
108
- else
109
- each { |node| node[name] = value if node.is_a?(Element) }
110
- self
111
- end
112
- end
113
-
114
- # Collection operations
115
- def +(other)
116
- NodeSet.new(native_nodes + other.native_nodes)
117
- end
118
-
119
- def -(other)
120
- NodeSet.new(native_nodes - other.native_nodes)
121
- end
122
-
123
- def &(other)
124
- NodeSet.new(native_nodes & other.native_nodes)
125
- end
126
-
127
- def |(other)
128
- NodeSet.new(native_nodes | other.native_nodes)
129
- end
130
-
131
- def uniq
132
- NodeSet.new(native_nodes.uniq)
133
- end
134
-
135
- def reverse
136
- NodeSet.new(native_nodes.reverse)
137
- end
138
-
139
- # Search and filtering
140
- def find_by_id(id)
141
- detect { |node| node.is_a?(Element) && node["id"] == id }
142
- end
143
-
144
- def find_by_class(class_name)
145
- select { |node| node.is_a?(Element) && (node["class"] || "").split(/\s+/).include?(class_name) }
146
- end
147
-
148
- def find_by_attribute(name, value = nil)
149
- select do |node|
150
- next unless node.is_a?(Element)
151
- if value.nil?
152
- node.attributes.key?(name)
153
- else
154
- node[name] == value
155
- end
156
- end
157
- end
158
-
159
- def of_type(type)
160
- select { |node| node.is_a?(type) }
161
- end
162
-
163
- # DOM Manipulation
164
- def before(node_or_nodes)
165
- each { |node| node.add_previous_sibling(node_or_nodes) }
166
- self
167
- end
168
-
169
- def after(node_or_nodes)
170
- each { |node| node.add_next_sibling(node_or_nodes) }
171
- self
172
- end
173
-
174
- def replace_with(node_or_nodes)
175
- each { |node| node.replace(node_or_nodes) }
176
- self
177
- end
178
-
179
- def wrap_all(wrapper)
180
- return self if empty?
181
-
182
- wrapper_node = case wrapper
183
- when String
184
- Document.parse(wrapper).root
185
- when Element
186
- wrapper
187
- else
188
- raise ArgumentError, "Expected String or Element"
189
- end
190
-
191
- first.add_previous_sibling(wrapper_node)
192
- wrapper_node.add_child(self)
193
- self
194
- end
195
-
196
- # Content manipulation
197
- def inner_text=(text)
198
- each { |node| node.inner_text = text }
199
- self
200
- end
201
-
202
- def inner_html=(html)
203
- each { |node| node.inner_html = html }
204
- self
205
- end
206
-
207
- # Attribute operations
208
- def toggle_class(names)
209
- names = names.split(/\s+/) if names.is_a?(String)
210
- each do |node|
211
- next unless node.is_a?(Element)
212
- current = (node["class"] || "").split(/\s+/)
213
- names.each do |name|
214
- if current.include?(name)
215
- current.delete(name)
216
- else
217
- current << name
218
- end
219
- end
220
- node["class"] = current.uniq.join(" ")
221
- end
222
- self
223
- end
224
-
225
- def has_class?(name)
226
- any? { |node| node.is_a?(Element) && (node["class"] || "").split(/\s+/).include?(name) }
227
- end
228
-
229
- def remove_attr(*attrs)
230
- each do |node|
231
- next unless node.is_a?(Element)
232
- attrs.each { |attr| node.remove_attribute(attr) }
233
- end
60
+ def remove
61
+ each(&:remove)
234
62
  self
235
63
  end
236
-
237
- # Position and hierarchy
238
- def parents
239
- NodeSet.new(
240
- map { |node| node.parent }.compact.uniq
241
- )
242
- end
243
-
244
- def children
245
- NodeSet.new(
246
- flat_map { |node| node.children.to_a }
247
- )
248
- end
249
-
250
- def siblings
251
- NodeSet.new(
252
- flat_map { |node| node.parent ? node.parent.children.reject { |sibling| sibling == node } : [] }
253
- ).uniq
254
- end
255
-
256
- def next
257
- NodeSet.new(
258
- map { |node| node.next_sibling }.compact
259
- )
260
- end
261
-
262
- def previous
263
- NodeSet.new(
264
- map { |node| node.previous_sibling }.compact
265
- )
266
- end
267
64
  end
268
65
  end
@@ -1,44 +1,25 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Moxml
2
4
  class ProcessingInstruction < Node
3
- def initialize(target_or_native = nil, content = nil)
4
- case target_or_native
5
- when String
6
- super(adapter.create_processing_instruction(nil, target_or_native, content))
7
- else
8
- super(target_or_native)
9
- end
10
- end
11
-
12
5
  def target
13
- adapter.processing_instruction_target(native)
6
+ adapter.processing_instruction_target(@native)
14
7
  end
15
8
 
16
9
  def target=(new_target)
17
- adapter.set_processing_instruction_target(native, new_target)
18
- self
10
+ adapter.set_node_name(@native, new_target.to_s)
19
11
  end
20
12
 
21
13
  def content
22
- adapter.processing_instruction_content(native)
14
+ adapter.processing_instruction_content(@native)
23
15
  end
24
16
 
25
17
  def content=(new_content)
26
- adapter.set_processing_instruction_content(native, new_content)
27
- self
28
- end
29
-
30
- def blank?
31
- content.strip.empty?
18
+ adapter.set_processing_instruction_content(@native, new_content.to_s)
32
19
  end
33
20
 
34
21
  def processing_instruction?
35
22
  true
36
23
  end
37
-
38
- private
39
-
40
- def create_native_node
41
- adapter.create_processing_instruction(nil, "", "")
42
- end
43
24
  end
44
25
  end
data/lib/moxml/text.rb CHANGED
@@ -1,39 +1,17 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Moxml
2
4
  class Text < Node
3
- def initialize(content_or_native = nil)
4
- case content_or_native
5
- when String
6
- super(adapter.create_text(nil, content_or_native))
7
- else
8
- super(content_or_native)
9
- end
10
- end
11
-
12
5
  def content
13
- adapter.text_content(native)
6
+ adapter.text_content(@native)
14
7
  end
15
8
 
16
9
  def content=(text)
17
- adapter.set_text_content(native, text)
18
- self
19
- end
20
-
21
- def blank?
22
- content.strip.empty?
23
- end
24
-
25
- def cdata?
26
- false
10
+ adapter.set_text_content(@native, normalize_xml_value(text))
27
11
  end
28
12
 
29
13
  def text?
30
14
  true
31
15
  end
32
-
33
- private
34
-
35
- def create_native_node
36
- adapter.create_text(nil, "")
37
- end
38
16
  end
39
17
  end
data/lib/moxml/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Moxml
4
- VERSION = "0.1.0"
4
+ VERSION = "0.1.2"
5
5
  end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Moxml
4
+ module XmlUtils
5
+ class Encoder
6
+ attr_reader :mode
7
+
8
+ MAPPINGS = {
9
+ none: {},
10
+ basic: {
11
+ "<" => "&lt;",
12
+ ">" => "&gt;",
13
+ "&" => "&amp;"
14
+ },
15
+ quotes: {
16
+ "'" => "&apos;",
17
+ '"' => "&quot;"
18
+ },
19
+ full: {
20
+ "<" => "&lt;",
21
+ ">" => "&gt;",
22
+ "'" => "&apos;",
23
+ '"' => "&quot;",
24
+ "&" => "&amp;"
25
+ }
26
+ }.freeze
27
+ MODES = MAPPINGS.keys.freeze
28
+
29
+ def initialize(text, mode = nil)
30
+ @text = text
31
+ @mode = valid_mode(mode)
32
+ end
33
+
34
+ def call
35
+ return @text if mode == :none
36
+
37
+ @text.to_s.gsub(/[#{mapping.keys.join}]/) do |match|
38
+ mapping[match]
39
+ end
40
+ end
41
+
42
+ protected
43
+
44
+ def valid_mode(raw_mode)
45
+ mode_sym = raw_mode.to_s.to_sym
46
+
47
+ MODES.include?(mode_sym) ? mode_sym : MODES.first
48
+ end
49
+
50
+ def mapping
51
+ MAPPINGS[mode] || {}
52
+ end
53
+ end
54
+ end
55
+ end