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/README.adoc CHANGED
@@ -1,770 +1,577 @@
1
- = Moxml: Modular XML processing for Ruby
1
+ = Moxml: Modern XML processing for Ruby
2
+ :toc: macro
3
+ :toclevels: 3
4
+ :toc-title: Contents
5
+ :source-highlighter: highlight.js
2
6
 
3
- Moxml provides a unified API for XML processing in Ruby, supporting multiple XML parsing backends (Nokogiri, Ox, and Oga).
7
+ image:https://github.com/lutaml/moxml/workflows/rake/badge.svg["Build Status", link="https://github.com/lutaml/moxml/actions?workflow=rake"]
4
8
 
5
- Moxml ("mox-em-el") stands for "Modular XML" and aims to provide a consistent
6
- interface for working with XML documents, regardless of the underlying XML
7
- library.
9
+ toc::[]
8
10
 
9
- == Installation
11
+ == Introduction and purpose
10
12
 
11
- [source,ruby]
12
- ----
13
- gem 'moxml'
14
- ----
13
+ Moxml provides a unified, modern XML processing interface for Ruby applications.
14
+ It offers a consistent API that abstracts away the underlying XML implementation
15
+ details while maintaining high performance through efficient node mapping and
16
+ native XPath querying.
17
+
18
+ Key features:
15
19
 
16
- == Basic usage
20
+ * Intuitive, Ruby-idiomatic API for XML manipulation
21
+ * Consistent interface across different XML libraries
22
+ * Efficient node mapping for XPath queries
23
+ * Support for all XML node types and features
24
+ * Easy switching between XML processing engines
25
+ * Clean separation between interface and implementation
17
26
 
18
- === Configuration
27
+ == Getting started
19
28
 
20
- Configure Moxml to use your preferred XML backend:
29
+ Install the gem and at least one supported XML library:
21
30
 
22
31
  [source,ruby]
23
32
  ----
24
- require 'moxml'
25
-
26
- Moxml.configure do |config|
27
- config.backend = :nokogiri # or :ox, :oga
28
- end
33
+ # In your Gemfile
34
+ gem 'moxml'
35
+ gem 'nokogiri' # Or 'ox' or 'oga'
29
36
  ----
30
37
 
31
- === Creating and parsing documents
38
+ === Basic document creation
32
39
 
33
40
  [source,ruby]
34
41
  ----
35
- # Create new empty document
36
- doc = Moxml::Document.new
37
-
38
- # Parse from string
39
- doc = Moxml::Document.parse("<root><child>content</child></root>")
42
+ require 'moxml'
40
43
 
41
- # Parse with encoding
42
- doc = Moxml::Document.parse(xml_string, encoding: 'UTF-8')
43
- ----
44
+ # Create a new XML document
45
+ doc = Moxml.new.create_document
44
46
 
45
- === Document creation patterns
47
+ # Add XML declaration
48
+ doc.add_declaration(version: "1.0", encoding: "UTF-8")
46
49
 
47
- [source,ruby]
48
- ----
49
- # Method 1: Create and build
50
- doc = Moxml::Document.new
51
- root = doc.create_element('root')
50
+ # Create root element with namespace
51
+ root = doc.create_element('book')
52
+ root.add_namespace('dc', 'http://purl.org/dc/elements/1.1/')
52
53
  doc.add_child(root)
53
54
 
54
- # Method 2: Parse from string
55
- doc = Moxml::Document.parse("<root/>")
56
-
57
- # Method 3: Parse with encoding
58
- doc = Moxml::Document.parse(xml_string, encoding: 'UTF-8')
55
+ # Add content
56
+ title = doc.create_element('dc:title')
57
+ title.text = 'XML Processing with Ruby'
58
+ root.add_child(title)
59
59
 
60
- # Method 4: Parse with options
61
- doc = Moxml::Document.parse(xml_string, {
62
- encoding: 'UTF-8',
63
- strict: true
64
- })
60
+ # Output formatted XML
61
+ puts doc.to_xml(indent: 2)
65
62
  ----
66
63
 
67
- === Common XML patterns
64
+ == Working with documents
68
65
 
69
- [source,ruby]
70
- ----
71
- # Working with namespaces
72
- doc = Moxml::Document.new
73
- root = doc.create_element('root')
74
- root['xmlns:custom'] = 'http://example.com/ns'
75
- child = doc.create_element('custom:element')
76
- root.add_child(child)
77
-
78
- # Creating structured data
79
- person = doc.create_element('person')
80
- person['id'] = '123'
81
- name = doc.create_element('name')
82
- name.add_child(doc.create_text('John Doe'))
83
- person.add_child(name)
84
-
85
- # Working with attributes
86
- element = doc.create_element('div')
87
- element['class'] = 'container'
88
- element['data-id'] = '123'
89
- element['style'] = 'color: blue'
90
-
91
- # Handling special characters
92
- text = doc.create_text('Special chars: < > & " \'')
93
- cdata = doc.create_cdata('<script>alert("Hello!");</script>')
94
-
95
- # Processing instructions
96
- pi = doc.create_processing_instruction('xml-stylesheet',
97
- 'type="text/xsl" href="style.xsl"')
98
- doc.add_child(pi)
99
- ----
66
+ === Using the builder pattern
100
67
 
101
- === Working with elements
68
+ The builder pattern provides a clean DSL for creating XML documents:
102
69
 
103
70
  [source,ruby]
104
71
  ----
105
- # Create new element
106
- element = Moxml::Element.new('tagname')
107
-
108
- # Add attributes
109
- element['class'] = 'content'
110
-
111
- # Access attributes
112
- class_attr = element['class']
113
-
114
- # Add child elements
115
- child = element.create_element('child')
116
- element.add_child(child)
72
+ doc = Moxml.new.build do
73
+ declaration version: "1.0", encoding: "UTF-8"
117
74
 
118
- # Access text content
119
- text_content = element.text
75
+ element 'library', xmlns: 'http://example.org/library' do
76
+ element 'book' do
77
+ element 'title' do
78
+ text 'Ruby Programming'
79
+ end
120
80
 
121
- # Add text content
122
- text = element.create_text('content')
123
- element.add_child(text)
81
+ element 'author' do
82
+ text 'Jane Smith'
83
+ end
124
84
 
125
- # Chaining operations
126
- element
127
- .add_child(doc.create_element('child'))
128
- .add_child(doc.create_text('content'))
129
- ['class'] = 'new-class'
85
+ comment 'Publication details'
86
+ element 'published', year: '2024'
130
87
 
131
- # Complex element creation
132
- div = doc.create_element('div')
133
- div['class'] = 'container'
134
- div.add_child(doc.create_element('span'))
135
- .add_child(doc.create_text('Hello'))
136
- div.add_child(doc.create_element('br'))
137
- div.add_child(doc.create_text('World'))
88
+ cdata '<custom>metadata</custom>'
89
+ end
90
+ end
91
+ end
138
92
  ----
139
93
 
140
- === Working with different node types
94
+ === Direct document manipulation
141
95
 
142
96
  [source,ruby]
143
97
  ----
144
- # Text nodes with various content
145
- plain_text = Moxml::Text.new("Simple text")
146
- multiline_text = Moxml::Text.new("Line 1\nLine 2")
147
- special_chars = Moxml::Text.new("Special: & < > \" '")
148
-
149
- # CDATA sections for different content types
150
- script_cdata = Moxml::Cdata.new("function() { alert('Hello!'); }")
151
- xml_cdata = Moxml::Cdata.new("<data><item>value</item></data>")
152
- mixed_cdata = Moxml::Cdata.new("Text with ]]> characters")
153
-
154
- # Comments for documentation
155
- todo_comment = Moxml::Comment.new("TODO: Add validation")
156
- section_comment = Moxml::Comment.new("----- Section Break -----")
157
- debug_comment = Moxml::Comment.new("DEBUG: Remove in production")
158
-
159
- # Processing instructions for various uses
160
- style_pi = Moxml::ProcessingInstruction.new(
161
- "xml-stylesheet",
162
- 'type="text/css" href="style.css"'
163
- )
164
- php_pi = Moxml::ProcessingInstruction.new(
165
- "php",
166
- 'echo "<?php echo $var; ?>>";'
167
- )
168
- custom_pi = Moxml::ProcessingInstruction.new(
169
- "custom-processor",
170
- 'param1="value1" param2="value2"'
171
- )
172
- ----
173
-
174
- === Element manipulation examples
98
+ doc = Moxml.new.create_document
175
99
 
176
- [source,ruby]
177
- ----
178
- # Building complex structures
179
- doc = Moxml::Document.new
180
- root = doc.create_element('html')
100
+ # Add declaration
101
+ doc.add_declaration(version: "1.0", encoding: "UTF-8")
102
+
103
+ # Create root with namespace
104
+ root = doc.create_element('library')
105
+ root.add_namespace(nil, 'http://example.org/library')
181
106
  doc.add_child(root)
182
107
 
183
- # Create head section
184
- head = doc.create_element('head')
185
- root.add_child(head)
108
+ # Add elements with attributes
109
+ book = doc.create_element('book')
110
+ book['id'] = 'b1'
111
+ root.add_child(book)
186
112
 
113
+ # Add mixed content
114
+ book.add_child(doc.create_comment('Book details'))
187
115
  title = doc.create_element('title')
188
- title.add_child(doc.create_text('Example Page'))
189
- head.add_child(title)
190
-
191
- meta = doc.create_element('meta')
192
- meta['charset'] = 'UTF-8'
193
- head.add_child(meta)
194
-
195
- # Create body section
196
- body = doc.create_element('body')
197
- root.add_child(body)
198
-
199
- div = doc.create_element('div')
200
- div['class'] = 'container'
201
- body.add_child(div)
202
-
203
- # Add multiple paragraphs
204
- 3.times do |i|
205
- p = doc.create_element('p')
206
- p.add_child(doc.create_text("Paragraph #{i + 1}"))
207
- div.add_child(p)
208
- end
209
-
210
- # Working with lists
211
- ul = doc.create_element('ul')
212
- div.add_child(ul)
213
-
214
- ['Item 1', 'Item 2', 'Item 3'].each do |text|
215
- li = doc.create_element('li')
216
- li.add_child(doc.create_text(text))
217
- ul.add_child(li)
218
- end
219
-
220
- # Adding link element
221
- a = doc.create_element('a')
222
- a['href'] = 'https://example.com'
223
- a.add_child(doc.create_text('Visit Example'))
224
- div.add_child(a)
116
+ title.text = 'Ruby Programming'
117
+ book.add_child(title)
225
118
  ----
226
119
 
227
- === Advanced node manipulation
120
+ == XML objects and their methods
228
121
 
229
- [source,ruby]
230
- ----
231
- # Cloning nodes
232
- original = doc.create_element('div')
233
- original['id'] = 'original'
234
- clone = original.clone
235
-
236
- # Moving nodes
237
- target = doc.create_element('target')
238
- source = doc.create_element('source')
239
- source.add_child(doc.create_text('Content'))
240
- target.add_child(source)
241
-
242
- # Replacing nodes
243
- old_node = doc.at_xpath('//old')
244
- new_node = doc.create_element('new')
245
- old_node.replace(new_node)
246
-
247
- # Inserting before/after
248
- reference = doc.create_element('reference')
249
- before = doc.create_element('before')
250
- after = doc.create_element('after')
251
- reference.add_previous_sibling(before)
252
- reference.add_next_sibling(after)
253
-
254
- # Conditional manipulation
255
- element = doc.at_xpath('//conditional')
256
- if element['flag'] == 'true'
257
- element.add_child(doc.create_text('Flag is true'))
258
- else
259
- element.remove
260
- end
261
- ----
122
+ === Document object
262
123
 
263
- === Working with namespaces
124
+ The Document object represents an XML document and serves as the root container
125
+ for all XML nodes.
264
126
 
265
127
  [source,ruby]
266
128
  ----
267
- # Creating namespaced document
268
- doc = Moxml::Document.new
269
- root = doc.create_element('root')
270
- root['xmlns'] = 'http://example.com/default'
271
- root['xmlns:custom'] = 'http://example.com/custom'
272
- doc.add_child(root)
129
+ # Creating a document
130
+ doc = Moxml.new.create_document
131
+ doc = Moxml.new.parse(xml_string)
273
132
 
274
- # Adding namespaced elements
275
- default_elem = doc.create_element('default-elem')
276
- custom_elem = doc.create_element('custom:elem')
133
+ # Document properties and methods
134
+ doc.encoding # Get document encoding
135
+ doc.encoding = "UTF-8" # Set document encoding
136
+ doc.version # Get XML version
137
+ doc.version = "1.1" # Set XML version
138
+ doc.standalone # Get standalone declaration
139
+ doc.standalone = "yes" # Set standalone declaration
277
140
 
278
- root.add_child(default_elem)
279
- root.add_child(custom_elem)
141
+ # Document structure
142
+ doc.root # Get root element
143
+ doc.children # Get all top-level nodes
144
+ doc.add_child(node) # Add a child node
145
+ doc.remove_child(node) # Remove a child node
280
146
 
281
- # Working with attributes in namespaces
282
- custom_elem['custom:attr'] = 'value'
147
+ # Node creation methods
148
+ doc.create_element(name) # Create new element
149
+ doc.create_text(content) # Create text node
150
+ doc.create_cdata(content) # Create CDATA section
151
+ doc.create_comment(content) # Create comment
152
+ doc.create_processing_instruction(target, content) # Create PI
283
153
 
284
- # Accessing namespaced content
285
- ns_elem = doc.at_xpath('//custom:elem')
286
- ns_attr = ns_elem['custom:attr']
154
+ # Document querying
155
+ doc.xpath(expression) # Find nodes by XPath
156
+ doc.at_xpath(expression) # Find first node by XPath
157
+
158
+ # Serialization
159
+ doc.to_xml(options) # Convert to XML string
287
160
  ----
288
161
 
289
- === Document serialization examples
162
+ === Element object
163
+
164
+ Elements are the primary structural components of an XML document, representing
165
+ tags with attributes and content.
290
166
 
291
167
  [source,ruby]
292
168
  ----
293
- # Basic serialization
294
- xml_string = doc.to_xml
169
+ # Element properties
170
+ element.name # Get element name
171
+ element.name = "new_name" # Set element name
172
+ element.text # Get text content
173
+ element.text = "content" # Set text content
174
+ element.inner_text # Get text content for current node only
175
+ element.inner_html # Get inner XML content
176
+ element.inner_html = xml # Set inner XML content
177
+
178
+ # Attributes
179
+ element[name] # Get attribute value
180
+ element[name] = value # Set attribute value
181
+ element.attributes # Get all attributes
182
+ element.remove_attribute(name) # Remove attribute
183
+
184
+ # Namespace handling
185
+ element.namespace # Get element's namespace
186
+ element.namespace = ns # Set element's namespace
187
+ element.add_namespace(prefix, uri) # Add new namespace
188
+ element.namespaces # Get all namespace definitions
189
+
190
+ # Node structure
191
+ element.parent # Get parent node
192
+ element.children # Get child nodes
193
+ element.add_child(node) # Add child node
194
+ element.remove_child(node) # Remove child node
195
+ element.add_previous_sibling(node) # Add sibling before
196
+ element.add_next_sibling(node) # Add sibling after
197
+ element.replace(node) # Replace with another node
198
+ element.remove # Remove from document
199
+
200
+ # Node type checking
201
+ element.element? # Returns true
202
+ element.text? # Returns false
203
+ element.cdata? # Returns false
204
+ element.comment? # Returns false
205
+ element.processing_instruction? # Returns false
206
+
207
+ # Node querying
208
+ element.xpath(expression) # Find nodes by XPath
209
+ element.at_xpath(expression) # Find first node by XPath
210
+ ----
211
+
212
+ === Text object
213
+
214
+ Text nodes represent character data in the XML document.
295
215
 
296
- # Pretty printing with indentation
297
- formatted_xml = doc.to_xml(
298
- indent: 2,
299
- pretty: true
300
- )
216
+ [source,ruby]
217
+ ----
218
+ # Creating text nodes
219
+ text = doc.create_text("content")
301
220
 
302
- # Controlling XML declaration
303
- with_declaration = doc.to_xml(
304
- xml_declaration: true,
305
- encoding: 'UTF-8',
306
- standalone: 'yes'
307
- )
221
+ # Text properties
222
+ text.content # Get text content
223
+ text.content = "new" # Set text content
308
224
 
309
- # Compact output
310
- minimal_xml = doc.to_xml(
311
- indent: 0,
312
- pretty: false,
313
- xml_declaration: false
314
- )
225
+ # Node type checking
226
+ text.text? # Returns true
315
227
 
316
- # Custom formatting
317
- custom_format = doc.to_xml(
318
- indent: 4,
319
- encoding: 'ISO-8859-1',
320
- xml_declaration: true
321
- )
228
+ # Structure
229
+ text.parent # Get parent node
230
+ text.remove # Remove from document
231
+ text.replace(node) # Replace with another node
322
232
  ----
323
233
 
324
- == Implementation details
234
+ === CDATA object
325
235
 
326
- === Memory management
236
+ CDATA sections contain text that should not be parsed as markup.
327
237
 
328
238
  [source,ruby]
329
239
  ----
330
- # Efficient document handling
331
- doc = Moxml::Document.parse(large_xml)
332
- begin
333
- # Process document
334
- result = process_document(doc)
335
- ensure
336
- # Clear references
337
- doc = nil
338
- GC.start
339
- end
240
+ # Creating CDATA sections
241
+ cdata = doc.create_cdata("<raw>content</raw>")
340
242
 
341
- # Streaming large node sets
342
- doc.xpath('//large-set/*').each do |node|
343
- # Process node
344
- process_node(node)
345
- # Clear reference
346
- node = nil
347
- end
243
+ # CDATA properties
244
+ cdata.content # Get CDATA content
245
+ cdata.content = "new" # Set CDATA content
348
246
 
349
- # Handling large collections
350
- def process_large_nodeset(nodeset)
351
- nodeset.each do |node|
352
- yield node if block_given?
353
- end
354
- ensure
355
- # Clear references
356
- nodeset = nil
357
- GC.start
358
- end
247
+ # Node type checking
248
+ cdata.cdata? # Returns true
249
+
250
+ # Structure
251
+ cdata.parent # Get parent node
252
+ cdata.remove # Remove from document
253
+ cdata.replace(node) # Replace with another node
359
254
  ----
360
255
 
361
- === Backend-specific optimizations
256
+ === Comment object
257
+
258
+ Comments contain human-readable notes in the XML document.
362
259
 
363
260
  [source,ruby]
364
261
  ----
365
- # Nokogiri-specific optimizations
366
- if Moxml.config.backend == :nokogiri
367
- # Use native CSS selectors
368
- nodes = doc.native.css('complex > selector')
369
- nodes.each do |native_node|
370
- node = Moxml::Node.wrap(native_node)
371
- # Process node
372
- end
373
-
374
- # Use native XPath
375
- results = doc.native.xpath('//complex/xpath/expression')
376
- end
262
+ # Creating comments
263
+ comment = doc.create_comment("Note")
377
264
 
378
- # Ox-specific optimizations
379
- if Moxml.config.backend == :ox
380
- # Use native parsing options
381
- doc = Moxml::Document.parse(xml, {
382
- mode: :generic,
383
- effort: :tolerant,
384
- smart: true
385
- })
386
-
387
- # Direct element creation
388
- element = Ox::Element.new('name')
389
- wrapped = Moxml::Element.new(element)
390
- end
265
+ # Comment properties
266
+ comment.content # Get comment content
267
+ comment.content = "new" # Set comment content
391
268
 
392
- # Oga-specific optimizations
393
- if Moxml.config.backend == :oga
394
- # Use native parsing features
395
- doc = Moxml::Document.parse(xml, {
396
- encoding: 'UTF-8',
397
- strict: true
398
- })
269
+ # Node type checking
270
+ comment.comment? # Returns true
399
271
 
400
- # Direct access to native methods
401
- nodes = doc.native.xpath('//element')
402
- end
272
+ # Structure
273
+ comment.parent # Get parent node
274
+ comment.remove # Remove from document
275
+ comment.replace(node) # Replace with another node
403
276
  ----
404
277
 
405
- === Threading patterns
278
+ === Processing instruction object
279
+
280
+ Processing instructions provide instructions to applications processing the XML.
406
281
 
407
282
  [source,ruby]
408
283
  ----
409
- # Thread-safe document creation
410
- require 'thread'
284
+ # Creating processing instructions
285
+ pi = doc.create_processing_instruction("xml-stylesheet",
286
+ 'type="text/xsl" href="style.xsl"')
411
287
 
412
- class ThreadSafeXmlProcessor
413
- def initialize
414
- @mutex = Mutex.new
415
- end
288
+ # PI properties
289
+ pi.target # Get PI target
290
+ pi.target = "new" # Set PI target
291
+ pi.content # Get PI content
292
+ pi.content = "new" # Set PI content
416
293
 
417
- def process_document(xml_string)
418
- @mutex.synchronize do
419
- doc = Moxml::Document.parse(xml_string)
420
- # Process document
421
- result = doc.to_xml
422
- doc = nil
423
- result
424
- end
425
- end
426
- end
294
+ # Node type checking
295
+ pi.processing_instruction? # Returns true
427
296
 
428
- # Parallel document processing
429
- def process_documents(xml_strings)
430
- threads = xml_strings.map do |xml|
431
- Thread.new do
432
- doc = Moxml::Document.parse(xml)
433
- # Process document
434
- doc = nil
435
- end
436
- end
437
- threads.each(&:join)
438
- end
439
-
440
- # Thread-local document storage
441
- Thread.new do
442
- Thread.current[:document] = Moxml::Document.new
443
- # Process document
444
- ensure
445
- Thread.current[:document] = nil
446
- end
297
+ # Structure
298
+ pi.parent # Get parent node
299
+ pi.remove # Remove from document
300
+ pi.replace(node) # Replace with another node
447
301
  ----
448
302
 
449
- == Troubleshooting
303
+ === Attribute object
450
304
 
451
- === Common issues and solutions
452
-
453
- ==== Parsing errors
305
+ Attributes represent name-value pairs on elements.
454
306
 
455
307
  [source,ruby]
456
308
  ----
457
- # Handle malformed XML
458
- begin
459
- doc = Moxml::Document.parse(xml_string)
460
- rescue Moxml::ParseError => e
461
- puts "Parse error at line #{e.line}, column #{e.column}: #{e.message}"
462
- # Attempt recovery
463
- xml_string = cleanup_xml(xml_string)
464
- retry
465
- end
309
+ # Attribute properties
310
+ attr.name # Get attribute name
311
+ attr.name = "new" # Set attribute name
312
+ attr.value # Get attribute value
313
+ attr.value = "new" # Set attribute value
466
314
 
467
- # Handle encoding issues
468
- begin
469
- doc = Moxml::Document.parse(xml_string, encoding: 'UTF-8')
470
- rescue Moxml::ParseError => e
471
- if e.message =~ /encoding/
472
- # Try detecting encoding
473
- detected_encoding = detect_encoding(xml_string)
474
- retry if detected_encoding
475
- end
476
- raise
477
- end
315
+ # Namespace handling
316
+ attr.namespace # Get attribute's namespace
317
+ attr.namespace = ns # Set attribute's namespace
318
+
319
+ # Node type checking
320
+ attr.attribute? # Returns true
478
321
  ----
479
322
 
480
- ==== Memory issues
323
+ === Namespace object
324
+
325
+ Namespaces define XML namespaces used in the document.
481
326
 
482
327
  [source,ruby]
483
328
  ----
484
- # Handle large documents
485
- def process_large_document(path)
486
- # Read and process in chunks
487
- File.open(path) do |file|
488
- doc = Moxml::Document.parse(file)
489
- doc.xpath('//chunk').each do |chunk|
490
- process_chunk(chunk)
491
- chunk = nil
492
- end
493
- doc = nil
494
- end
495
- GC.start
496
- end
329
+ # Namespace properties
330
+ ns.prefix # Get namespace prefix
331
+ ns.uri # Get namespace URI
497
332
 
498
- # Monitor memory usage
499
- require 'get_process_mem'
333
+ # Formatting
334
+ ns.to_s # Format as xmlns declaration
500
335
 
501
- def memory_safe_processing(xml)
502
- memory = GetProcessMem.new
503
- initial_memory = memory.mb
336
+ # Node type checking
337
+ ns.namespace? # Returns true
338
+ ----
504
339
 
505
- doc = Moxml::Document.parse(xml)
506
- result = process_document(doc)
507
- doc = nil
508
- GC.start
340
+ === Node traversal and inspection
509
341
 
510
- final_memory = memory.mb
511
- puts "Memory usage: #{final_memory - initial_memory}MB"
342
+ Each node type provides methods for traversing the document structure:
512
343
 
513
- result
514
- end
344
+ [source,ruby]
515
345
  ----
346
+ node.parent # Get parent node
347
+ node.children # Get child nodes
348
+ node.next_sibling # Get next sibling
349
+ node.previous_sibling # Get previous sibling
350
+ node.ancestors # Get all ancestor nodes
351
+ node.descendants # Get all descendant nodes
516
352
 
517
- ==== Backend-specific issues
353
+ # Type checking
354
+ node.element? # Is it an element?
355
+ node.text? # Is it a text node?
356
+ node.cdata? # Is it a CDATA section?
357
+ node.comment? # Is it a comment?
358
+ node.processing_instruction? # Is it a PI?
359
+ node.attribute? # Is it an attribute?
360
+ node.namespace? # Is it a namespace?
518
361
 
519
- [source,ruby]
362
+ # Node information
363
+ node.document # Get owning document
364
+ node.path # Get XPath to node
365
+ node.line_number # Get source line number (if available)
520
366
  ----
521
- # Handle backend limitations
522
- def safe_xpath(doc, xpath)
523
- case Moxml.config.backend
524
- when :nokogiri
525
- doc.xpath(xpath)
526
- when :ox
527
- # Ox has limited XPath support
528
- fallback_xpath_search(doc, xpath)
529
- when :oga
530
- # Handle Oga-specific XPath syntax
531
- modified_xpath = adjust_xpath_for_oga(xpath)
532
- doc.xpath(modified_xpath)
533
- end
534
- end
535
367
 
536
- # Handle backend switching
537
- def with_backend(backend)
538
- original_backend = Moxml.config.backend
539
- Moxml.config.backend = backend
540
- yield
541
- ensure
542
- Moxml.config.backend = original_backend
543
- end
544
- ----
368
+ == Advanced features
545
369
 
546
- === Performance optimization
370
+ === XPath querying and node mapping
547
371
 
548
- ==== Document creation
372
+ Moxml provides efficient XPath querying by leveraging the native XML library's
373
+ implementation while maintaining consistent node mapping:
549
374
 
550
375
  [source,ruby]
551
376
  ----
552
- # Efficient document building
553
- def build_large_document
554
- doc = Moxml::Document.new
555
- root = doc.create_element('root')
556
- doc.add_child(root)
557
-
558
- # Pre-allocate elements
559
- elements = Array.new(1000) do |i|
560
- elem = doc.create_element('item')
561
- elem['id'] = i.to_s
562
- elem
563
- end
377
+ # Find all book elements
378
+ books = doc.xpath('//book')
379
+ # Returns Moxml::Element objects mapped to native nodes
564
380
 
565
- # Batch add elements
566
- elements.each do |elem|
567
- root.add_child(elem)
568
- end
381
+ # Find with namespaces
382
+ titles = doc.xpath('//dc:title',
383
+ 'dc' => 'http://purl.org/dc/elements/1.1/')
384
+
385
+ # Find first matching node
386
+ first_book = doc.at_xpath('//book')
569
387
 
570
- doc
388
+ # Chain queries
389
+ doc.xpath('//book').each do |book|
390
+ # Each book is a mapped Moxml::Element
391
+ title = book.at_xpath('.//title')
392
+ puts "#{book['id']}: #{title.text}"
571
393
  end
394
+ ----
572
395
 
573
- # Memory-efficient processing
574
- def process_large_xml(xml_string)
575
- result = []
576
- doc = Moxml::Document.parse(xml_string)
396
+ === Namespace handling
577
397
 
578
- doc.xpath('//item').each do |item|
579
- # Process and immediately discard
580
- result << process_item(item)
581
- item = nil
582
- end
398
+ [source,ruby]
399
+ ----
400
+ # Add namespace to element
401
+ element.add_namespace('dc', 'http://purl.org/dc/elements/1.1/')
583
402
 
584
- doc = nil
585
- GC.start
403
+ # Create element in namespace
404
+ title = doc.create_element('dc:title')
405
+ title.text = 'Document Title'
586
406
 
587
- result
588
- end
407
+ # Query with namespaces
408
+ doc.xpath('//dc:title',
409
+ 'dc' => 'http://purl.org/dc/elements/1.1/')
589
410
  ----
590
411
 
591
- ==== Query optimization
412
+ === Accessing native implementation
413
+
414
+ While not typically needed, you can access the underlying XML library's nodes:
592
415
 
593
416
  [source,ruby]
594
417
  ----
595
- # Optimize node selection
596
- def efficient_node_selection(doc)
597
- # Cache frequently used nodes
598
- @header_nodes ||= doc.xpath('//header').to_a
418
+ # Get native node
419
+ native_node = element.native
599
420
 
600
- # Use specific selectors
601
- doc.xpath('//specific/path') # Better than '//*[name()="specific"]'
421
+ # Get adapter being used
422
+ adapter = element.context.config.adapter
602
423
 
603
- # Combine queries when possible
604
- doc.xpath('//a | //b') # Better than two separate queries
605
- end
424
+ # Create from native node
425
+ element = Moxml::Element.new(native_node, context)
426
+ ----
606
427
 
607
- # Optimize attribute access
608
- def efficient_attribute_handling(element)
609
- # Cache attribute values
610
- @cached_attrs ||= element.attributes
428
+ == Error handling
611
429
 
612
- # Direct attribute access
613
- value = element['attr'] # Better than element.attributes['attr']
430
+ Moxml provides specific error classes for different types of errors that may
431
+ occur during XML processing:
614
432
 
615
- # Batch attribute updates
616
- attrs = {'id' => '1', 'class' => 'new', 'data' => 'value'}
617
- attrs.each { |k,v| element[k] = v }
433
+ [source,ruby]
434
+ ----
435
+ begin
436
+ doc = context.parse(xml_string)
437
+ rescue Moxml::ParseError => e
438
+ # Handles XML parsing errors
439
+ puts "Parse error at line #{e.line}, column #{e.column}"
440
+ puts "Message: #{e.message}"
441
+ rescue Moxml::ValidationError => e
442
+ # Handles XML validation errors
443
+ puts "Validation error: #{e.message}"
444
+ rescue Moxml::XPathError => e
445
+ # Handles XPath expression errors
446
+ puts "XPath error: #{e.message}"
447
+ rescue Moxml::Error => e
448
+ # Handles other Moxml-specific errors
449
+ puts "Error: #{e.message}"
618
450
  end
619
451
  ----
620
452
 
621
- ==== Serialization optimization
453
+ == Configuration
454
+
455
+ Moxml can be configured globally or per instance:
622
456
 
623
457
  [source,ruby]
624
458
  ----
625
- # Efficient output generation
626
- def optimized_serialization(doc)
627
- # Minimal output
628
- compact = doc.to_xml(
629
- indent: 0,
630
- pretty: false,
631
- xml_declaration: false
632
- )
633
-
634
- # Balanced formatting
635
- readable = doc.to_xml(
636
- indent: 2,
637
- pretty: true,
638
- xml_declaration: true
639
- )
640
-
641
- # Stream large documents
642
- File.open('large.xml', 'w') do |file|
643
- doc.write_to(file, indent: 2)
644
- end
459
+ # Global configuration
460
+ Moxml.configure do |config|
461
+ config.default_adapter = :nokogiri
462
+ config.strict = true
463
+ config.encoding = 'UTF-8'
464
+ end
465
+
466
+ # Instance configuration
467
+ moxml = Moxml.new do |config|
468
+ config.adapter = :ox
469
+ config.strict = false
645
470
  end
646
471
  ----
647
472
 
648
- === Debugging tips
473
+ == Thread safety
649
474
 
650
- ==== Inspection helpers
475
+ Moxml is thread-safe when used properly. Each instance maintains its own state
476
+ and can be used safely in concurrent operations:
651
477
 
652
478
  [source,ruby]
653
479
  ----
654
- # Debug node structure
655
- def inspect_node(node, level = 0)
656
- indent = " " * level
657
- puts "#{indent}#{node.class.name}: #{node.name}"
658
-
659
- if node.respond_to?(:attributes)
660
- node.attributes.each do |name, attr|
661
- puts "#{indent} @#{name}=#{attr.value.inspect}"
662
- end
480
+ class XmlProcessor
481
+ def initialize
482
+ @mutex = Mutex.new
483
+ @context = Moxml.new
663
484
  end
664
485
 
665
- if node.respond_to?(:children)
666
- node.children.each { |child| inspect_node(child, level + 1) }
486
+ def process(xml)
487
+ @mutex.synchronize do
488
+ doc = @context.parse(xml)
489
+ # Modify document
490
+ doc.to_xml
491
+ end
667
492
  end
668
493
  end
494
+ ----
669
495
 
670
- # Track node operations
671
- def debug_node_operations
672
- nodes_created = 0
673
- nodes_removed = 0
496
+ == Performance considerations
674
497
 
675
- yield
676
- ensure
677
- puts "Nodes created: #{nodes_created}"
678
- puts "Nodes removed: #{nodes_removed}"
679
- end
680
- ----
498
+ === Memory management
681
499
 
682
- ==== Backend validation
500
+ Moxml maintains a node registry to ensure consistent object mapping:
683
501
 
684
502
  [source,ruby]
685
503
  ----
686
- # Verify backend behavior
687
- def verify_backend_compatibility
688
- doc = Moxml::Document.new
689
-
690
- # Test basic operations
691
- element = doc.create_element('test')
692
- doc.add_child(element)
504
+ doc = context.parse(large_xml)
505
+ # Process document
506
+ doc = nil # Allow garbage collection of document and registry
507
+ GC.start # Force garbage collection if needed
508
+ ----
693
509
 
694
- # Verify node handling
695
- raise "Node creation failed" unless doc.root
696
- raise "Node type wrong" unless doc.root.is_a?(Moxml::Element)
510
+ === Efficient querying
697
511
 
698
- # Verify serialization
699
- xml = doc.to_xml
700
- raise "Serialization failed" unless xml.include?('<test/>')
512
+ Use specific XPath expressions for better performance:
701
513
 
702
- puts "Backend verification successful"
703
- rescue => e
704
- puts "Backend verification failed: #{e.message}"
705
- end
514
+ [source,ruby]
706
515
  ----
516
+ # More efficient - specific path
517
+ doc.xpath('//book/title')
707
518
 
708
- == Error handling
519
+ # Less efficient - requires full document scan
520
+ doc.xpath('//title')
709
521
 
710
- Moxml provides unified error handling:
522
+ # Most efficient - direct child access
523
+ root.xpath('./title')
524
+ ----
711
525
 
712
- * `Moxml::Error` - Base error class
713
- * `Moxml::ParseError` - XML parsing errors
714
- * `Moxml::ArgumentError` - Invalid argument errors
526
+ == Best practices
715
527
 
716
- === Error handling patterns
528
+ === Document creation
717
529
 
718
530
  [source,ruby]
719
531
  ----
720
- # Handle parsing errors
721
- begin
722
- doc = Moxml::Document.parse(xml_string)
723
- rescue Moxml::ParseError => e
724
- logger.error "Parse error: #{e.message}"
725
- logger.error "At line #{e.line}, column #{e.column}"
726
- raise
532
+ # Preferred - using builder pattern
533
+ doc = Moxml.new.build do
534
+ declaration version: "1.0", encoding: "UTF-8"
535
+ element 'root' do
536
+ element 'child' do
537
+ text 'content'
538
+ end
539
+ end
727
540
  end
728
541
 
729
- # Handle invalid operations
730
- begin
731
- element['invalid/name'] = 'value'
732
- rescue Moxml::ArgumentError => e
733
- logger.warn "Invalid operation: #{e.message}"
734
- # Use alternative approach
735
- end
542
+ # Alternative - direct manipulation
543
+ doc = Moxml.new.create_document
544
+ doc.add_declaration(version: "1.0", encoding: "UTF-8")
545
+ root = doc.create_element('root')
546
+ doc.add_child(root)
547
+ ----
736
548
 
737
- # Custom error handling
738
- class XmlProcessor
739
- def process(xml)
740
- doc = Moxml::Document.parse(xml)
741
- yield doc
742
- rescue Moxml::Error => e
743
- handle_moxml_error(e)
744
- rescue StandardError => e
745
- handle_standard_error(e)
746
- ensure
747
- doc = nil
748
- end
549
+ === Node manipulation
550
+
551
+ [source,ruby]
552
+ ----
553
+ # Preferred - chainable operations
554
+ element
555
+ .add_namespace('dc', 'http://purl.org/dc/elements/1.1/')
556
+ .add_child(doc.create_text('content'))
557
+
558
+ # Preferred - clear node type checking
559
+ if node.element?
560
+ node.add_child(doc.create_text('content'))
749
561
  end
750
562
  ----
751
563
 
752
564
  == Contributing
753
565
 
754
- Bug reports and pull requests are welcome on GitHub at
755
- https://github.com/lutaml/moxml.
756
-
757
- === Development guidelines
566
+ 1. Fork the repository
567
+ 2. Create your feature branch (`git checkout -b feature/my-new-feature`)
568
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
569
+ 4. Push to the branch (`git push origin feature/my-new-feature`)
570
+ 5. Create a new Pull Request
758
571
 
759
- * Follow Ruby style guide
760
- * Add tests for new features
761
- * Update documentation
762
- * Ensure backwards compatibility
763
- * Consider performance implications
764
- * Test with all supported backends
572
+ == License
765
573
 
766
- == Copyright and license
574
+ Copyright (c) 2024 Ribose Inc.
767
575
 
768
- Copyright Ribose.
576
+ This project is licensed under the BSD-2-Clause License. See the LICENSE file for details.
769
577
 
770
- The gem is available as open source under the terms of the BSD-2-Clause License.