moxml 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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