moxml 0.1.0

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.
@@ -0,0 +1,169 @@
1
+ # lib/moxml/errors.rb
2
+ module Moxml
3
+ # Base error class for all Moxml errors
4
+ class Error < StandardError; end
5
+
6
+ # Parsing related errors
7
+ class ParseError < Error
8
+ attr_reader :line, :column, :source
9
+
10
+ def initialize(message, line: nil, column: nil, source: nil)
11
+ @line = line
12
+ @column = column
13
+ @source = source
14
+ super(build_message(message))
15
+ end
16
+
17
+ private
18
+
19
+ def build_message(message)
20
+ parts = [message]
21
+ parts << "Line: #{line}" if line
22
+ parts << "Column: #{column}" if column
23
+ parts << "\nSource: #{source}" if source
24
+ parts.join(" | ")
25
+ end
26
+ end
27
+
28
+ # Validation errors
29
+ class ValidationError < Error; end
30
+ class DTDValidationError < ValidationError; end
31
+ class SchemaValidationError < ValidationError; end
32
+ class NamespaceError < ValidationError; end
33
+
34
+ # Structure errors
35
+ class MalformedXMLError < ParseError; end
36
+ class UnbalancedTagError < ParseError; end
37
+ class UndefinedEntityError < ParseError; end
38
+ class DuplicateAttributeError < ParseError; end
39
+
40
+ # Encoding errors
41
+ class EncodingError < Error
42
+ attr_reader :encoding
43
+
44
+ def initialize(message, encoding)
45
+ @encoding = encoding
46
+ super("#{message} (Encoding: #{encoding})")
47
+ end
48
+ end
49
+
50
+ # Security related errors
51
+ class SecurityError < Error; end
52
+ class MaxDepthExceededError < SecurityError; end
53
+ class MaxAttributesExceededError < SecurityError; end
54
+ class MaxNameLengthExceededError < SecurityError; end
55
+ class EntityExpansionError < SecurityError; end
56
+
57
+ # Backend errors
58
+ class BackendError < Error
59
+ attr_reader :backend
60
+
61
+ def initialize(message, backend)
62
+ @backend = backend
63
+ super("#{message} (Backend: #{backend})")
64
+ end
65
+ end
66
+
67
+ class BackendNotFoundError < BackendError; end
68
+ class BackendConfigurationError < BackendError; end
69
+
70
+ # Node manipulation errors
71
+ class NodeError < Error
72
+ attr_reader :node
73
+
74
+ def initialize(message, node)
75
+ @node = node
76
+ super("#{message} (Node: #{node.class})")
77
+ end
78
+ end
79
+
80
+ class InvalidNodeTypeError < NodeError; end
81
+ class InvalidOperationError < NodeError; end
82
+ class NodeNotFoundError < NodeError; end
83
+
84
+ # Visitor pattern errors
85
+ class VisitorError < Error; end
86
+
87
+ class InvalidSelectorError < VisitorError
88
+ attr_reader :selector
89
+
90
+ def initialize(message, selector)
91
+ @selector = selector
92
+ super("#{message} (Selector: #{selector})")
93
+ end
94
+ end
95
+
96
+ class VisitorMethodError < VisitorError
97
+ attr_reader :method_name
98
+
99
+ def initialize(message, method_name)
100
+ @method_name = method_name
101
+ super("#{message} (Method: #{method_name})")
102
+ end
103
+ end
104
+
105
+ # Serialization errors
106
+ class SerializationError < Error; end
107
+
108
+ class InvalidOptionsError < SerializationError
109
+ attr_reader :options
110
+
111
+ def initialize(message, options)
112
+ @options = options
113
+ super("#{message} (Options: #{options})")
114
+ end
115
+ end
116
+
117
+ # IO errors
118
+ class IOError < Error
119
+ attr_reader :path
120
+
121
+ def initialize(message, path)
122
+ @path = path
123
+ super("#{message} (Path: #{path})")
124
+ end
125
+ end
126
+
127
+ class FileNotFoundError < IOError; end
128
+ class WriteError < IOError; end
129
+
130
+ # Memory errors
131
+ class MemoryError < Error
132
+ attr_reader :size
133
+
134
+ def initialize(message, size)
135
+ @size = size
136
+ super("#{message} (Size: #{size} bytes)")
137
+ end
138
+ end
139
+
140
+ class DocumentTooLargeError < MemoryError; end
141
+
142
+ # CDATA related errors
143
+ class CDATAError < Error; end
144
+ class NestedCDATAError < CDATAError; end
145
+ class InvalidCDATAContentError < CDATAError; end
146
+
147
+ # Namespace related errors
148
+ class NamespaceDeclarationError < Error
149
+ attr_reader :prefix, :uri
150
+
151
+ def initialize(message, prefix, uri)
152
+ @prefix = prefix
153
+ @uri = uri
154
+ super("#{message} (Prefix: #{prefix}, URI: #{uri})")
155
+ end
156
+ end
157
+
158
+ # XPath related errors
159
+ class XPathError < Error
160
+ attr_reader :expression
161
+
162
+ def initialize(message, expression)
163
+ @expression = expression
164
+ super("#{message} (Expression: #{expression})")
165
+ end
166
+ end
167
+
168
+ class InvalidXPathError < XPathError; end
169
+ end
@@ -0,0 +1,54 @@
1
+ module Moxml
2
+ class Namespace < Node
3
+ def initialize(prefix_or_native = nil, uri = nil)
4
+ case prefix_or_native
5
+ when String
6
+ super(adapter.create_namespace(nil, prefix_or_native, uri))
7
+ else
8
+ super(prefix_or_native)
9
+ end
10
+ end
11
+
12
+ def prefix
13
+ adapter.namespace_prefix(native)
14
+ end
15
+
16
+ def prefix=(new_prefix)
17
+ adapter.set_namespace_prefix(native, new_prefix)
18
+ self
19
+ end
20
+
21
+ def uri
22
+ adapter.namespace_uri(native)
23
+ end
24
+
25
+ def uri=(new_uri)
26
+ adapter.set_namespace_uri(native, new_uri)
27
+ self
28
+ end
29
+
30
+ def blank?
31
+ uri.nil? || uri.empty?
32
+ end
33
+
34
+ def namespace?
35
+ true
36
+ end
37
+
38
+ def ==(other)
39
+ other.is_a?(Namespace) &&
40
+ other.prefix == prefix &&
41
+ other.uri == uri
42
+ end
43
+
44
+ def to_s
45
+ prefix ? "xmlns:#{prefix}='#{uri}'" : "xmlns='#{uri}'"
46
+ end
47
+
48
+ private
49
+
50
+ def create_native_node
51
+ adapter.create_namespace(nil, "", "")
52
+ end
53
+ end
54
+ end
data/lib/moxml/node.rb ADDED
@@ -0,0 +1,113 @@
1
+ module Moxml
2
+ class Node
3
+ attr_reader :native
4
+
5
+ def initialize(native_node = nil)
6
+ @native = native_node || create_native_node
7
+ end
8
+
9
+ def self.wrap(native_node)
10
+ return nil if native_node.nil?
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
24
+
25
+ klass.new(native_node)
26
+ end
27
+
28
+ def parent
29
+ wrap_node(adapter.parent(native))
30
+ end
31
+
32
+ def children
33
+ NodeSet.new(adapter.children(native))
34
+ end
35
+
36
+ def next_sibling
37
+ wrap_node(adapter.next_sibling(native))
38
+ end
39
+
40
+ def previous_sibling
41
+ wrap_node(adapter.previous_sibling(native))
42
+ end
43
+
44
+ def remove
45
+ adapter.remove(native)
46
+ self
47
+ end
48
+
49
+ def replace(node)
50
+ adapter.replace(native, node.native)
51
+ self
52
+ end
53
+
54
+ def add_previous_sibling(node)
55
+ adapter.add_previous_sibling(native, node.native)
56
+ self
57
+ end
58
+
59
+ def add_next_sibling(node)
60
+ adapter.add_next_sibling(native, node.native)
61
+ self
62
+ end
63
+
64
+ def text
65
+ adapter.text_content(native)
66
+ end
67
+
68
+ def text=(content)
69
+ adapter.set_text_content(native, content)
70
+ self
71
+ end
72
+
73
+ def inner_html
74
+ adapter.inner_html(native)
75
+ end
76
+
77
+ def inner_html=(html)
78
+ adapter.set_inner_html(native, html)
79
+ end
80
+
81
+ def outer_html
82
+ adapter.outer_html(native)
83
+ end
84
+
85
+ def path
86
+ adapter.path(native)
87
+ end
88
+
89
+ def line
90
+ adapter.line(native)
91
+ end
92
+
93
+ def column
94
+ adapter.column(native)
95
+ end
96
+
97
+ protected
98
+
99
+ def wrap_node(native_node)
100
+ self.class.wrap(native_node)
101
+ end
102
+
103
+ private
104
+
105
+ def adapter
106
+ Moxml.adapter
107
+ end
108
+
109
+ def create_native_node
110
+ raise NotImplementedError, "Subclasses must implement create_native_node"
111
+ end
112
+ end
113
+ end
@@ -0,0 +1,268 @@
1
+ # lib/moxml/node_set.rb
2
+ module Moxml
3
+ class NodeSet
4
+ include Enumerable
5
+
6
+ attr_reader :native_nodes
7
+
8
+ def initialize(native_nodes = [])
9
+ @native_nodes = Array(native_nodes)
10
+ end
11
+
12
+ def each
13
+ return enum_for(:each) unless block_given?
14
+ native_nodes.each { |node| yield Node.wrap(node) }
15
+ self
16
+ end
17
+
18
+ def [](index)
19
+ case index
20
+ when Integer
21
+ Node.wrap(native_nodes[index])
22
+ when Range
23
+ NodeSet.new(native_nodes[index])
24
+ end
25
+ end
26
+
27
+ def first
28
+ Node.wrap(native_nodes.first)
29
+ end
30
+
31
+ def last
32
+ Node.wrap(native_nodes.last)
33
+ end
34
+
35
+ def empty?
36
+ native_nodes.empty?
37
+ end
38
+
39
+ def size
40
+ native_nodes.size
41
+ end
42
+
43
+ alias length size
44
+
45
+ def to_a
46
+ map { |node| node }
47
+ end
48
+
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
58
+ end
59
+
60
+ def text
61
+ map(&:text).join
62
+ end
63
+
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
234
+ self
235
+ 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
+ end
268
+ end
@@ -0,0 +1,44 @@
1
+ module Moxml
2
+ 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
+ def target
13
+ adapter.processing_instruction_target(native)
14
+ end
15
+
16
+ def target=(new_target)
17
+ adapter.set_processing_instruction_target(native, new_target)
18
+ self
19
+ end
20
+
21
+ def content
22
+ adapter.processing_instruction_content(native)
23
+ end
24
+
25
+ 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?
32
+ end
33
+
34
+ def processing_instruction?
35
+ true
36
+ end
37
+
38
+ private
39
+
40
+ def create_native_node
41
+ adapter.create_processing_instruction(nil, "", "")
42
+ end
43
+ end
44
+ end
data/lib/moxml/text.rb ADDED
@@ -0,0 +1,39 @@
1
+ module Moxml
2
+ 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
+ def content
13
+ adapter.text_content(native)
14
+ end
15
+
16
+ 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
27
+ end
28
+
29
+ def text?
30
+ true
31
+ end
32
+
33
+ private
34
+
35
+ def create_native_node
36
+ adapter.create_text(nil, "")
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Moxml
4
+ VERSION = "0.1.0"
5
+ end