moxml 0.1.0 → 0.1.1

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 +400 -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 +314 -0
  14. data/lib/moxml/adapter/oga.rb +309 -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 +61 -99
  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 +110 -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,576 @@
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_html # Get inner XML content
175
+ element.inner_html = xml # Set inner XML content
176
+
177
+ # Attributes
178
+ element[name] # Get attribute value
179
+ element[name] = value # Set attribute value
180
+ element.attributes # Get all attributes
181
+ element.remove_attribute(name) # Remove attribute
182
+
183
+ # Namespace handling
184
+ element.namespace # Get element's namespace
185
+ element.namespace = ns # Set element's namespace
186
+ element.add_namespace(prefix, uri) # Add new namespace
187
+ element.namespaces # Get all namespace definitions
188
+
189
+ # Node structure
190
+ element.parent # Get parent node
191
+ element.children # Get child nodes
192
+ element.add_child(node) # Add child node
193
+ element.remove_child(node) # Remove child node
194
+ element.add_previous_sibling(node) # Add sibling before
195
+ element.add_next_sibling(node) # Add sibling after
196
+ element.replace(node) # Replace with another node
197
+ element.remove # Remove from document
198
+
199
+ # Node type checking
200
+ element.element? # Returns true
201
+ element.text? # Returns false
202
+ element.cdata? # Returns false
203
+ element.comment? # Returns false
204
+ element.processing_instruction? # Returns false
205
+
206
+ # Node querying
207
+ element.xpath(expression) # Find nodes by XPath
208
+ element.at_xpath(expression) # Find first node by XPath
209
+ ----
210
+
211
+ === Text object
212
+
213
+ Text nodes represent character data in the XML document.
295
214
 
296
- # Pretty printing with indentation
297
- formatted_xml = doc.to_xml(
298
- indent: 2,
299
- pretty: true
300
- )
215
+ [source,ruby]
216
+ ----
217
+ # Creating text nodes
218
+ text = doc.create_text("content")
301
219
 
302
- # Controlling XML declaration
303
- with_declaration = doc.to_xml(
304
- xml_declaration: true,
305
- encoding: 'UTF-8',
306
- standalone: 'yes'
307
- )
220
+ # Text properties
221
+ text.content # Get text content
222
+ text.content = "new" # Set text content
308
223
 
309
- # Compact output
310
- minimal_xml = doc.to_xml(
311
- indent: 0,
312
- pretty: false,
313
- xml_declaration: false
314
- )
224
+ # Node type checking
225
+ text.text? # Returns true
315
226
 
316
- # Custom formatting
317
- custom_format = doc.to_xml(
318
- indent: 4,
319
- encoding: 'ISO-8859-1',
320
- xml_declaration: true
321
- )
227
+ # Structure
228
+ text.parent # Get parent node
229
+ text.remove # Remove from document
230
+ text.replace(node) # Replace with another node
322
231
  ----
323
232
 
324
- == Implementation details
233
+ === CDATA object
325
234
 
326
- === Memory management
235
+ CDATA sections contain text that should not be parsed as markup.
327
236
 
328
237
  [source,ruby]
329
238
  ----
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
239
+ # Creating CDATA sections
240
+ cdata = doc.create_cdata("<raw>content</raw>")
340
241
 
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
242
+ # CDATA properties
243
+ cdata.content # Get CDATA content
244
+ cdata.content = "new" # Set CDATA content
348
245
 
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
246
+ # Node type checking
247
+ cdata.cdata? # Returns true
248
+
249
+ # Structure
250
+ cdata.parent # Get parent node
251
+ cdata.remove # Remove from document
252
+ cdata.replace(node) # Replace with another node
359
253
  ----
360
254
 
361
- === Backend-specific optimizations
255
+ === Comment object
256
+
257
+ Comments contain human-readable notes in the XML document.
362
258
 
363
259
  [source,ruby]
364
260
  ----
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
261
+ # Creating comments
262
+ comment = doc.create_comment("Note")
377
263
 
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
264
+ # Comment properties
265
+ comment.content # Get comment content
266
+ comment.content = "new" # Set comment content
391
267
 
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
- })
268
+ # Node type checking
269
+ comment.comment? # Returns true
399
270
 
400
- # Direct access to native methods
401
- nodes = doc.native.xpath('//element')
402
- end
271
+ # Structure
272
+ comment.parent # Get parent node
273
+ comment.remove # Remove from document
274
+ comment.replace(node) # Replace with another node
403
275
  ----
404
276
 
405
- === Threading patterns
277
+ === Processing instruction object
278
+
279
+ Processing instructions provide instructions to applications processing the XML.
406
280
 
407
281
  [source,ruby]
408
282
  ----
409
- # Thread-safe document creation
410
- require 'thread'
283
+ # Creating processing instructions
284
+ pi = doc.create_processing_instruction("xml-stylesheet",
285
+ 'type="text/xsl" href="style.xsl"')
411
286
 
412
- class ThreadSafeXmlProcessor
413
- def initialize
414
- @mutex = Mutex.new
415
- end
287
+ # PI properties
288
+ pi.target # Get PI target
289
+ pi.target = "new" # Set PI target
290
+ pi.content # Get PI content
291
+ pi.content = "new" # Set PI content
416
292
 
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
293
+ # Node type checking
294
+ pi.processing_instruction? # Returns true
427
295
 
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
296
+ # Structure
297
+ pi.parent # Get parent node
298
+ pi.remove # Remove from document
299
+ pi.replace(node) # Replace with another node
447
300
  ----
448
301
 
449
- == Troubleshooting
302
+ === Attribute object
450
303
 
451
- === Common issues and solutions
452
-
453
- ==== Parsing errors
304
+ Attributes represent name-value pairs on elements.
454
305
 
455
306
  [source,ruby]
456
307
  ----
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
308
+ # Attribute properties
309
+ attr.name # Get attribute name
310
+ attr.name = "new" # Set attribute name
311
+ attr.value # Get attribute value
312
+ attr.value = "new" # Set attribute value
466
313
 
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
314
+ # Namespace handling
315
+ attr.namespace # Get attribute's namespace
316
+ attr.namespace = ns # Set attribute's namespace
317
+
318
+ # Node type checking
319
+ attr.attribute? # Returns true
478
320
  ----
479
321
 
480
- ==== Memory issues
322
+ === Namespace object
323
+
324
+ Namespaces define XML namespaces used in the document.
481
325
 
482
326
  [source,ruby]
483
327
  ----
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
328
+ # Namespace properties
329
+ ns.prefix # Get namespace prefix
330
+ ns.uri # Get namespace URI
497
331
 
498
- # Monitor memory usage
499
- require 'get_process_mem'
332
+ # Formatting
333
+ ns.to_s # Format as xmlns declaration
500
334
 
501
- def memory_safe_processing(xml)
502
- memory = GetProcessMem.new
503
- initial_memory = memory.mb
335
+ # Node type checking
336
+ ns.namespace? # Returns true
337
+ ----
504
338
 
505
- doc = Moxml::Document.parse(xml)
506
- result = process_document(doc)
507
- doc = nil
508
- GC.start
339
+ === Node traversal and inspection
509
340
 
510
- final_memory = memory.mb
511
- puts "Memory usage: #{final_memory - initial_memory}MB"
341
+ Each node type provides methods for traversing the document structure:
512
342
 
513
- result
514
- end
343
+ [source,ruby]
515
344
  ----
345
+ node.parent # Get parent node
346
+ node.children # Get child nodes
347
+ node.next_sibling # Get next sibling
348
+ node.previous_sibling # Get previous sibling
349
+ node.ancestors # Get all ancestor nodes
350
+ node.descendants # Get all descendant nodes
516
351
 
517
- ==== Backend-specific issues
352
+ # Type checking
353
+ node.element? # Is it an element?
354
+ node.text? # Is it a text node?
355
+ node.cdata? # Is it a CDATA section?
356
+ node.comment? # Is it a comment?
357
+ node.processing_instruction? # Is it a PI?
358
+ node.attribute? # Is it an attribute?
359
+ node.namespace? # Is it a namespace?
518
360
 
519
- [source,ruby]
361
+ # Node information
362
+ node.document # Get owning document
363
+ node.path # Get XPath to node
364
+ node.line_number # Get source line number (if available)
520
365
  ----
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
366
 
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
- ----
367
+ == Advanced features
545
368
 
546
- === Performance optimization
369
+ === XPath querying and node mapping
547
370
 
548
- ==== Document creation
371
+ Moxml provides efficient XPath querying by leveraging the native XML library's
372
+ implementation while maintaining consistent node mapping:
549
373
 
550
374
  [source,ruby]
551
375
  ----
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
376
+ # Find all book elements
377
+ books = doc.xpath('//book')
378
+ # Returns Moxml::Element objects mapped to native nodes
564
379
 
565
- # Batch add elements
566
- elements.each do |elem|
567
- root.add_child(elem)
568
- end
380
+ # Find with namespaces
381
+ titles = doc.xpath('//dc:title',
382
+ 'dc' => 'http://purl.org/dc/elements/1.1/')
383
+
384
+ # Find first matching node
385
+ first_book = doc.at_xpath('//book')
569
386
 
570
- doc
387
+ # Chain queries
388
+ doc.xpath('//book').each do |book|
389
+ # Each book is a mapped Moxml::Element
390
+ title = book.at_xpath('.//title')
391
+ puts "#{book['id']}: #{title.text}"
571
392
  end
393
+ ----
572
394
 
573
- # Memory-efficient processing
574
- def process_large_xml(xml_string)
575
- result = []
576
- doc = Moxml::Document.parse(xml_string)
395
+ === Namespace handling
577
396
 
578
- doc.xpath('//item').each do |item|
579
- # Process and immediately discard
580
- result << process_item(item)
581
- item = nil
582
- end
397
+ [source,ruby]
398
+ ----
399
+ # Add namespace to element
400
+ element.add_namespace('dc', 'http://purl.org/dc/elements/1.1/')
583
401
 
584
- doc = nil
585
- GC.start
402
+ # Create element in namespace
403
+ title = doc.create_element('dc:title')
404
+ title.text = 'Document Title'
586
405
 
587
- result
588
- end
406
+ # Query with namespaces
407
+ doc.xpath('//dc:title',
408
+ 'dc' => 'http://purl.org/dc/elements/1.1/')
589
409
  ----
590
410
 
591
- ==== Query optimization
411
+ === Accessing native implementation
412
+
413
+ While not typically needed, you can access the underlying XML library's nodes:
592
414
 
593
415
  [source,ruby]
594
416
  ----
595
- # Optimize node selection
596
- def efficient_node_selection(doc)
597
- # Cache frequently used nodes
598
- @header_nodes ||= doc.xpath('//header').to_a
417
+ # Get native node
418
+ native_node = element.native
599
419
 
600
- # Use specific selectors
601
- doc.xpath('//specific/path') # Better than '//*[name()="specific"]'
420
+ # Get adapter being used
421
+ adapter = element.context.config.adapter
602
422
 
603
- # Combine queries when possible
604
- doc.xpath('//a | //b') # Better than two separate queries
605
- end
423
+ # Create from native node
424
+ element = Moxml::Element.new(native_node, context)
425
+ ----
606
426
 
607
- # Optimize attribute access
608
- def efficient_attribute_handling(element)
609
- # Cache attribute values
610
- @cached_attrs ||= element.attributes
427
+ == Error handling
611
428
 
612
- # Direct attribute access
613
- value = element['attr'] # Better than element.attributes['attr']
429
+ Moxml provides specific error classes for different types of errors that may
430
+ occur during XML processing:
614
431
 
615
- # Batch attribute updates
616
- attrs = {'id' => '1', 'class' => 'new', 'data' => 'value'}
617
- attrs.each { |k,v| element[k] = v }
432
+ [source,ruby]
433
+ ----
434
+ begin
435
+ doc = context.parse(xml_string)
436
+ rescue Moxml::ParseError => e
437
+ # Handles XML parsing errors
438
+ puts "Parse error at line #{e.line}, column #{e.column}"
439
+ puts "Message: #{e.message}"
440
+ rescue Moxml::ValidationError => e
441
+ # Handles XML validation errors
442
+ puts "Validation error: #{e.message}"
443
+ rescue Moxml::XPathError => e
444
+ # Handles XPath expression errors
445
+ puts "XPath error: #{e.message}"
446
+ rescue Moxml::Error => e
447
+ # Handles other Moxml-specific errors
448
+ puts "Error: #{e.message}"
618
449
  end
619
450
  ----
620
451
 
621
- ==== Serialization optimization
452
+ == Configuration
453
+
454
+ Moxml can be configured globally or per instance:
622
455
 
623
456
  [source,ruby]
624
457
  ----
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
458
+ # Global configuration
459
+ Moxml.configure do |config|
460
+ config.default_adapter = :nokogiri
461
+ config.strict = true
462
+ config.encoding = 'UTF-8'
463
+ end
464
+
465
+ # Instance configuration
466
+ moxml = Moxml.new do |config|
467
+ config.adapter = :ox
468
+ config.strict = false
645
469
  end
646
470
  ----
647
471
 
648
- === Debugging tips
472
+ == Thread safety
649
473
 
650
- ==== Inspection helpers
474
+ Moxml is thread-safe when used properly. Each instance maintains its own state
475
+ and can be used safely in concurrent operations:
651
476
 
652
477
  [source,ruby]
653
478
  ----
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
479
+ class XmlProcessor
480
+ def initialize
481
+ @mutex = Mutex.new
482
+ @context = Moxml.new
663
483
  end
664
484
 
665
- if node.respond_to?(:children)
666
- node.children.each { |child| inspect_node(child, level + 1) }
485
+ def process(xml)
486
+ @mutex.synchronize do
487
+ doc = @context.parse(xml)
488
+ # Modify document
489
+ doc.to_xml
490
+ end
667
491
  end
668
492
  end
493
+ ----
669
494
 
670
- # Track node operations
671
- def debug_node_operations
672
- nodes_created = 0
673
- nodes_removed = 0
495
+ == Performance considerations
674
496
 
675
- yield
676
- ensure
677
- puts "Nodes created: #{nodes_created}"
678
- puts "Nodes removed: #{nodes_removed}"
679
- end
680
- ----
497
+ === Memory management
681
498
 
682
- ==== Backend validation
499
+ Moxml maintains a node registry to ensure consistent object mapping:
683
500
 
684
501
  [source,ruby]
685
502
  ----
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)
503
+ doc = context.parse(large_xml)
504
+ # Process document
505
+ doc = nil # Allow garbage collection of document and registry
506
+ GC.start # Force garbage collection if needed
507
+ ----
693
508
 
694
- # Verify node handling
695
- raise "Node creation failed" unless doc.root
696
- raise "Node type wrong" unless doc.root.is_a?(Moxml::Element)
509
+ === Efficient querying
697
510
 
698
- # Verify serialization
699
- xml = doc.to_xml
700
- raise "Serialization failed" unless xml.include?('<test/>')
511
+ Use specific XPath expressions for better performance:
701
512
 
702
- puts "Backend verification successful"
703
- rescue => e
704
- puts "Backend verification failed: #{e.message}"
705
- end
513
+ [source,ruby]
706
514
  ----
515
+ # More efficient - specific path
516
+ doc.xpath('//book/title')
707
517
 
708
- == Error handling
518
+ # Less efficient - requires full document scan
519
+ doc.xpath('//title')
709
520
 
710
- Moxml provides unified error handling:
521
+ # Most efficient - direct child access
522
+ root.xpath('./title')
523
+ ----
711
524
 
712
- * `Moxml::Error` - Base error class
713
- * `Moxml::ParseError` - XML parsing errors
714
- * `Moxml::ArgumentError` - Invalid argument errors
525
+ == Best practices
715
526
 
716
- === Error handling patterns
527
+ === Document creation
717
528
 
718
529
  [source,ruby]
719
530
  ----
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
531
+ # Preferred - using builder pattern
532
+ doc = Moxml.new.build do
533
+ declaration version: "1.0", encoding: "UTF-8"
534
+ element 'root' do
535
+ element 'child' do
536
+ text 'content'
537
+ end
538
+ end
727
539
  end
728
540
 
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
541
+ # Alternative - direct manipulation
542
+ doc = Moxml.new.create_document
543
+ doc.add_declaration(version: "1.0", encoding: "UTF-8")
544
+ root = doc.create_element('root')
545
+ doc.add_child(root)
546
+ ----
736
547
 
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
548
+ === Node manipulation
549
+
550
+ [source,ruby]
551
+ ----
552
+ # Preferred - chainable operations
553
+ element
554
+ .add_namespace('dc', 'http://purl.org/dc/elements/1.1/')
555
+ .add_child(doc.create_text('content'))
556
+
557
+ # Preferred - clear node type checking
558
+ if node.element?
559
+ node.add_child(doc.create_text('content'))
749
560
  end
750
561
  ----
751
562
 
752
563
  == Contributing
753
564
 
754
- Bug reports and pull requests are welcome on GitHub at
755
- https://github.com/lutaml/moxml.
756
-
757
- === Development guidelines
565
+ 1. Fork the repository
566
+ 2. Create your feature branch (`git checkout -b feature/my-new-feature`)
567
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
568
+ 4. Push to the branch (`git push origin feature/my-new-feature`)
569
+ 5. Create a new Pull Request
758
570
 
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
571
+ == License
765
572
 
766
- == Copyright and license
573
+ Copyright (c) 2024 Ribose Inc.
767
574
 
768
- Copyright Ribose.
575
+ This project is licensed under the BSD-2-Clause License. See the LICENSE file for details.
769
576
 
770
- The gem is available as open source under the terms of the BSD-2-Clause License.