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