moxml 0.1.9 → 0.1.10

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4b2faed71c75f32fcfe528621b1d3ed01c603b04f81e75d5b45b6c525d9d1836
4
- data.tar.gz: 68ead788c2e5098b2f1f88653443fe4d8ff1e76b17dab09c08badb64c7ab70a3
3
+ metadata.gz: 07caeff68b2413d8ccaab1e827b151c4b98381f76b0f1feb63cec118be54ae05
4
+ data.tar.gz: 76e3eb23022b6d49e5c265467966d29d6ad12617f2a00af3b34680b32f96b8ac
5
5
  SHA512:
6
- metadata.gz: 8aa59a486d44d101e1c078fc6fde2eea1cad9d4d7690d937dc7faef233a4143206432ffab53da3a2ef01af9245d17f706ddb01bc55b2c3555b358a6a3df10143
7
- data.tar.gz: b5256089daef1f94012046bd7ff0fcbff1827c615b46d416231933de3193f3a262063a6ad761a87c629cdfe15fc51644fc1887d43eaeff5f8710bd16a791ea3c
6
+ metadata.gz: e0a41270e30bca0664d5e4bf32019d82a262cbfe6c2d001879d8e6259bf06e80a24df2372698ecd651835e88b27be2b7f2552b0d26aaf40aa514de7a870bf5e8
7
+ data.tar.gz: 7ade8a4b671b3eb3fe06d2b909783f61bd5172d1645e3c1e9c6344d3e3fb01b5b1c32b8da09b39fedc2b73d04e52edb19956bf00240fbb145798f4be9c598058
data/.rubocop_todo.yml CHANGED
@@ -1,12 +1,12 @@
1
1
  # This configuration was generated by
2
2
  # `rubocop --auto-gen-config`
3
- # on 2025-11-24 13:45:20 UTC using RuboCop version 1.80.0.
3
+ # on 2025-11-26 03:06:16 UTC using RuboCop version 1.80.0.
4
4
  # The point is for the user to remove these configuration records
5
5
  # one by one as the offenses are removed from the code base.
6
6
  # Note that changes in the inspected code, or installation of new
7
7
  # versions of RuboCop, may require this file to be generated again.
8
8
 
9
- # Offense count: 192
9
+ # Offense count: 203
10
10
  # This cop supports safe autocorrection (--autocorrect).
11
11
  # Configuration parameters: Max, AllowHeredoc, AllowURI, AllowQualifiedName, URISchemes, IgnoreCopDirectives, AllowedPatterns, SplitStrings.
12
12
  # URISchemes: http, https
@@ -91,7 +91,7 @@ Metrics/MethodLength:
91
91
  Metrics/ParameterLists:
92
92
  Max: 7
93
93
 
94
- # Offense count: 42
94
+ # Offense count: 43
95
95
  # Configuration parameters: AllowedMethods, AllowedPatterns, Max.
96
96
  Metrics/PerceivedComplexity:
97
97
  Enabled: false
data/README.adoc CHANGED
@@ -26,6 +26,8 @@ Key features:
26
26
 
27
27
  == Supported XML libraries
28
28
 
29
+ === General
30
+
29
31
  Moxml supports the following XML libraries:
30
32
 
31
33
  REXML:: https://github.com/ruby/rexml[REXML], a pure Ruby XML parser
@@ -110,7 +112,9 @@ section.
110
112
  NOTE: HeadedOx provides full XPath 1.0 support via a pure Ruby XPath engine
111
113
  layered on top of Ox's C parser. See HeadedOx documentation for details.
112
114
 
113
- NOTE: Ox/HeadedOx SAX: Only core events supported (start_element, end_element, characters, errors). No separate CDATA, comment, or processing instruction events.
115
+ NOTE: Ox/HeadedOx SAX: Only core events supported (start_element, end_element,
116
+ characters, errors). No separate CDATA, comment, or processing instruction
117
+ events.
114
118
 
115
119
  == Adapter comparison
116
120
 
@@ -356,7 +360,7 @@ NOTE: Ox/HeadedOx SAX: Only core events supported (start_element, end_element, c
356
360
  | ✅ Full
357
361
  | ✅ Full
358
362
  | ✅ Full
359
- | ⚠️ Limited^4^
363
+ | Full
360
364
  | ✅ Full
361
365
  | ✅ Full
362
366
 
@@ -400,13 +404,12 @@ NOTE: Ox/HeadedOx SAX: Only core events supported (start_element, end_element, c
400
404
  | ✅ Yes
401
405
  | ✅ Yes
402
406
  |===
403
-
407
+ +
404
408
  ^1^ Ox/HeadedOx: Text node replacement may fail in some cases due to internal node structure +
405
409
  ^2^ Ox: `//book[@id]` works (returns all book elements), but doesn't filter by attribute existence +
406
410
  ^3^ HeadedOx: Full XPath 1.0 with all 27 functions and 6 axes. Pure Ruby XPath engine on Ox's C parser. 99.20% pass rate. See link:docs/headed-ox.adoc[] +
407
411
  ^4^ Ox: Use `.find { |el| el["id"] == "123" }` instead of XPath attribute value predicates +
408
- ^5^ LibXML: DOCTYPE parsing works, serialization is limited (no round-trip preservation) +
409
- ^6^ HeadedOx limitations: Namespace introspection and 7 axes not implemented. See link:docs/HEADED_OX_LIMITATIONS.md[]
412
+ ^5^ HeadedOx limitations: Namespace introspection and 7 axes not implemented. See link:docs/HEADED_OX_LIMITATIONS.md[]
410
413
 
411
414
  === Adapter selection guide
412
415
 
@@ -453,10 +456,13 @@ NOTE: Ox/HeadedOx SAX: Only core events supported (start_element, end_element, c
453
456
  * Need more XPath capabilities than standard Ox provides
454
457
  * Memory efficiency is important but XPath features are required
455
458
 
456
- CAUTION: Ox's custom XPath engine supports common patterns but may not handle
459
+ CAUTION: Ox's custom XPath engine supports common patterns but cannot handle
457
460
  complex XPath expressions. Test thoroughly if your use case requires advanced
458
461
  XPath.
459
462
 
463
+ TODO: We should throw errors when unsupported XPath features are used with Ox
464
+ or HeadedOx to prevent silent failures.
465
+
460
466
 
461
467
  == Getting started
462
468
 
@@ -643,6 +649,23 @@ For complete SAX documentation including all handler types, event methods, adapt
643
649
 
644
650
  For complete node API reference including traversal methods, manipulation, queries, type checking, and node information, see link:docs/_pages/node-api-reference.adoc[Node API Reference].
645
651
 
652
+ === Node identity
653
+
654
+ Moxml provides a consistent `#identifier` method across all node types to safely identify nodes:
655
+
656
+ [source,ruby]
657
+ ----
658
+ element = doc.at_xpath("//book")
659
+ puts element.identifier # => "book"
660
+
661
+ attr = element.attribute("id")
662
+ puts attr.identifier # => "id"
663
+ ----
664
+
665
+ The `#identifier` method returns the primary identifier for each node type (tag name for elements, attribute name for attributes, target for processing instructions, or `nil` for content nodes).
666
+
667
+ IMPORTANT: Always use type-safe patterns when working with mixed node types. See the link:docs/_guides/node-api-consistency.adoc[Node API Consistency Guide] for complete documentation on safe coding patterns, API surface by node type, and migration guidelines.
668
+
646
669
 
647
670
  == Advanced features
648
671
 
@@ -694,7 +717,8 @@ rescue Moxml::Error => e
694
717
  end
695
718
  ----
696
719
 
697
- For complete error class hierarchy, error types, best practices, and debugging techniques, see link:docs/_pages/error-handling.adoc[Error Handling Guide].
720
+ For complete error class hierarchy, error types, best practices, and debugging
721
+ techniques, see link:docs/_pages/error-handling.adoc[Error Handling Guide].
698
722
 
699
723
 
700
724
  == Configuration
@@ -717,22 +741,31 @@ context = Moxml.new do |config|
717
741
  end
718
742
  ----
719
743
 
720
- For all configuration options, adapter selection, serialization options, and environment-based configuration, see link:docs/_pages/configuration.adoc[Configuration Guide].
744
+ For all configuration options, adapter selection, serialization options, and
745
+ environment-based configuration, see
746
+ link:docs/_pages/configuration.adoc[Configuration Guide].
721
747
 
722
748
 
723
749
 
724
750
  == Thread safety
725
751
 
726
- For complete information on thread-safe patterns, context management, and concurrent processing, see the link:docs/_pages/thread-safety.adoc[Thread Safety Guide].
752
+ For complete information on thread-safe patterns, context management, and
753
+ concurrent processing, see the link:docs/_pages/thread-safety.adoc[Thread Safety
754
+ Guide].
727
755
 
728
756
 
729
757
  == Performance considerations
730
758
 
731
- For detailed performance optimization strategies, memory management best practices, and efficient querying patterns, see the link:docs/_pages/performance.adoc[Performance Considerations Guide].
759
+ For detailed performance optimization strategies, memory management best
760
+ practices, and efficient querying patterns, see the
761
+ link:docs/_pages/performance.adoc[Performance Considerations Guide].
732
762
 
733
763
  == Best practices
734
764
 
735
- For comprehensive best practices covering XPath queries, adapter selection, error handling, namespace handling, memory management, thread safety, performance optimization, and testing strategies, see link:docs/_pages/best-practices.adoc[Best Practices Guide].
765
+ For comprehensive best practices covering XPath queries, adapter selection,
766
+ error handling, namespace handling, memory management, thread safety,
767
+ performance optimization, and testing strategies, see
768
+ link:docs/_pages/best-practices.adoc[Best Practices Guide].
736
769
 
737
770
 
738
771
  == Specific adapter limitations
@@ -756,11 +789,13 @@ The Ox adapter provides maximum parsing speed but has XPath limitations.
756
789
  doc.xpath("//book").find { |book| book["id"] == "123" }
757
790
  ----
758
791
 
759
- For complete Ox adapter documentation including all limitations and workarounds, see link:docs/_pages/adapters/ox.adoc[Ox Adapter Guide].
792
+ For complete Ox adapter documentation including all limitations and workarounds,
793
+ see link:docs/_pages/adapters/ox.adoc[Ox Adapter Guide].
760
794
 
761
795
  === HeadedOx adapter
762
796
 
763
- The HeadedOx adapter combines Ox's fast C-based XML parsing with Moxml's comprehensive pure Ruby XPath 1.0 engine.
797
+ The HeadedOx adapter combines Ox's fast C-based XML parsing with Moxml's
798
+ comprehensive pure Ruby XPath 1.0 engine.
764
799
 
765
800
  **Status:** Production-ready v1.2 (99.20% pass rate, 1,992/2,008 tests)
766
801
 
@@ -796,12 +831,6 @@ For complete HeadedOx documentation including architecture, XPath capabilities,
796
831
 
797
832
  ==== LibXML adapter
798
833
 
799
- *DOCTYPE Limitations:*
800
-
801
- * DOCTYPE parsing works
802
- * DOCTYPE round-trip preservation is limited
803
- * DOCTYPE cannot be reliably re-serialized after parsing
804
-
805
834
  *Performance:*
806
835
 
807
836
  * Serialization speed: ~120 ips (slower than target)
@@ -819,7 +848,9 @@ limitations. Use these adapters when you need full XPath and namespace support.
819
848
 
820
849
  == Development and testing
821
850
 
822
- For complete information on development setup, testing strategies, benchmarking, and coverage reporting, see the link:docs/_guides/development-testing.adoc[Development and Testing Guide].
851
+ For complete information on development setup, testing strategies, benchmarking,
852
+ and coverage reporting, see the
853
+ link:docs/_guides/development-testing.adoc[Development and Testing Guide].
823
854
 
824
855
  == Contributing
825
856
 
@@ -10,23 +10,25 @@ Task-oriented guides for common XML processing operations with Moxml.
10
10
  === Document operations
11
11
 
12
12
  link:parsing-xml[Parsing XML]::
13
- Parse XML from strings, files, and IO streams with different adapters.
13
+ Parse XML from strings, files, and IO streams
14
14
 
15
- link:sax-parsing[SAX parsing]::
16
- Memory-efficient event-driven parsing for large XML files and streaming data.
15
+ link:working-with-documents[Working with Documents]::
16
+ Create and manipulate XML documents
17
17
 
18
- link:xml-declaration[XML declaration preservation]::
19
- Understand how Moxml preserves XML declarations for round-trip fidelity and standards compliance.
18
+ link:modifying-xml[Modifying XML]::
19
+ Edit elements, attributes, and content
20
20
 
21
- link:creating-documents[Creating documents]::
22
- Build XML documents from scratch using the builder pattern or direct
23
- manipulation.
21
+ link:sax-parsing[SAX Parsing]::
22
+ Memory-efficient event-driven parsing
24
23
 
25
- link:modifying-xml[Modifying XML]::
26
- Add, remove, and modify elements, attributes, and content in XML documents.
24
+ link:advanced-features[Advanced Features]::
25
+ Namespaces, XPath, and more
26
+
27
+ link:node-api-consistency[Node API Consistency]::
28
+ Understanding node types and their APIs
27
29
 
28
- link:serializing-xml[Serializing XML]::
29
- Convert XML documents to strings with formatting and encoding options.
30
+ link:development-testing[Development & Testing]::
31
+ Contributing to Moxml
30
32
 
31
33
  === Querying and traversal
32
34
 
@@ -0,0 +1,572 @@
1
+ = Node API consistency guide
2
+ :toc:
3
+ :toclevels: 3
4
+
5
+ == Overview
6
+
7
+ This guide documents the API surface of all node types in Moxml, providing clear
8
+ expectations for developers about which methods are available on which node
9
+ types.
10
+
11
+ == Node Type Hierarchy
12
+
13
+ [source]
14
+ ----
15
+ Node (abstract base)
16
+ ├── Document
17
+ ├── Element
18
+ ├── Attribute
19
+ ├── Text
20
+ ├── Cdata
21
+ ├── Comment
22
+ ├── ProcessingInstruction
23
+ ├── Declaration
24
+ └── Doctype
25
+ ----
26
+
27
+ == Common Node Methods
28
+
29
+ All node types inherit these methods from `Moxml::Node`:
30
+
31
+ === Document Navigation
32
+
33
+ [cols="1,3,1"]
34
+ |===
35
+ | Method | Description | Always Available?
36
+
37
+ | `#document`
38
+ | Returns the containing document
39
+ | ✅ Yes
40
+
41
+ | `#parent`
42
+ | Returns the parent node
43
+ | ✅ Yes
44
+
45
+ | `#children`
46
+ | Returns a NodeSet of child nodes
47
+ | ✅ Yes (may be empty)
48
+
49
+ | `#next_sibling`
50
+ | Returns the next sibling node
51
+ | ✅ Yes (may be nil)
52
+
53
+ | `#previous_sibling`
54
+ | Returns the previous sibling node
55
+ | ✅ Yes (may be nil)
56
+ |===
57
+
58
+ === Tree Manipulation
59
+
60
+ [cols="1,3,1"]
61
+ |===
62
+ | Method | Description | Always Available?
63
+
64
+ | `#add_child(node)`
65
+ | Adds a child node
66
+ | ✅ Yes
67
+
68
+ | `#add_previous_sibling(node)`
69
+ | Adds a sibling before this node
70
+ | ✅ Yes
71
+
72
+ | `#add_next_sibling(node)`
73
+ | Adds a sibling after this node
74
+ | ✅ Yes
75
+
76
+ | `#remove`
77
+ | Removes this node from the tree
78
+ | ✅ Yes
79
+
80
+ | `#replace(node)`
81
+ | Replaces this node with another
82
+ | ✅ Yes
83
+ |===
84
+
85
+ === Serialization
86
+
87
+ [cols="1,3,1"]
88
+ |===
89
+ | Method | Description | Always Available?
90
+
91
+ | `#to_xml(options = {})`
92
+ | Serializes node to XML string
93
+ | ✅ Yes
94
+
95
+ | `#clone` / `#dup`
96
+ | Creates a deep copy of the node
97
+ | ✅ Yes
98
+ |===
99
+
100
+ === XPath Queries
101
+
102
+ [cols="1,3,1"]
103
+ |===
104
+ | Method | Description | Always Available?
105
+
106
+ | `#xpath(expression, ns = {})`
107
+ | Returns NodeSet matching XPath
108
+ | ✅ Yes (adapter-dependent)
109
+
110
+ | `#at_xpath(expression, ns = {})`
111
+ | Returns first node matching XPath
112
+ | ✅ Yes (adapter-dependent)
113
+ |===
114
+
115
+ === Type Checking
116
+
117
+ [cols="1,3,1"]
118
+ |===
119
+ | Method | Description | Always Available?
120
+
121
+ | `#element?`
122
+ | Returns true if node is an Element
123
+ | ✅ Yes
124
+
125
+ | `#text?`
126
+ | Returns true if node is a Text node
127
+ | ✅ Yes
128
+
129
+ | `#cdata?`
130
+ | Returns true if node is a CDATA section
131
+ | ✅ Yes
132
+
133
+ | `#comment?`
134
+ | Returns true if node is a Comment
135
+ | ✅ Yes
136
+
137
+ | `#processing_instruction?`
138
+ | Returns true if node is a PI
139
+ | ✅ Yes
140
+
141
+ | `#document?`
142
+ | Returns true if node is a Document
143
+ | ✅ Yes
144
+
145
+ | `#declaration?`
146
+ | Returns true if node is a Declaration
147
+ | ✅ Yes
148
+
149
+ | `#doctype?`
150
+ | Returns true if node is a Doctype
151
+ | ✅ Yes
152
+
153
+ | `#attribute?`
154
+ | Returns true if node is an Attribute
155
+ | ✅ Yes
156
+ |===
157
+
158
+ == Node Type-Specific APIs
159
+
160
+ === Document
161
+
162
+ The root document node.
163
+
164
+ ==== Additional Methods
165
+
166
+ [cols="1,3,1"]
167
+ |===
168
+ | Method | Description | Always Available?
169
+
170
+ | `#root`
171
+ | Returns the root element
172
+ | ✅ Yes
173
+
174
+ | `#root=(element)`
175
+ | Sets the root element
176
+ | ✅ Yes
177
+
178
+ | `#encoding`
179
+ | Returns document encoding
180
+ | ✅ Yes
181
+
182
+ | `#create_element(name, content = nil)`
183
+ | Creates a new element
184
+ | ✅ Yes
185
+
186
+ | `#create_text(content)`
187
+ | Creates a new text node
188
+ | ✅ Yes
189
+
190
+ | `#create_comment(content)`
191
+ | Creates a new comment
192
+ | ✅ Yes
193
+
194
+ | `#create_cdata(content)`
195
+ | Creates a new CDATA section
196
+ | ✅ Yes
197
+
198
+ | `#create_processing_instruction(target, content)`
199
+ | Creates a new processing instruction
200
+ | ✅ Yes
201
+
202
+ | `#create_declaration(version, encoding, standalone)`
203
+ | Creates a new XML declaration
204
+ | ✅ Yes (adapter-dependent)
205
+ |===
206
+
207
+ === Element
208
+
209
+ Elements are the primary structural nodes with tag names, attributes, and children.
210
+
211
+ ==== Identity Methods
212
+
213
+ [cols="1,3,1"]
214
+ |===
215
+ | Method | Description | Always Available?
216
+
217
+ | `#name`
218
+ | Returns the element tag name
219
+ | ✅ Yes
220
+
221
+ | `#name=(value)`
222
+ | Sets the element tag name
223
+ | ✅ Yes
224
+
225
+ | `#identifier`
226
+ | Returns the primary identifier (same as #name)
227
+ | ✅ Yes
228
+ |===
229
+
230
+ ==== Namespace Methods
231
+
232
+ [cols="1,3,1"]
233
+ |===
234
+ | Method | Description | Always Available?
235
+
236
+ | `#namespace`
237
+ | Returns the element's namespace
238
+ | ✅ Yes (may be nil)
239
+
240
+ | `#namespace=(ns_or_hash)`
241
+ | Sets the element's namespace
242
+ | ✅ Yes
243
+
244
+ | `#namespace_prefix`
245
+ | Returns the namespace prefix
246
+ | ✅ Yes (may be nil)
247
+
248
+ | `#namespace_uri`
249
+ | Returns the namespace URI
250
+ | ✅ Yes (may be nil)
251
+
252
+ | `#namespaces`
253
+ | Returns all namespace definitions
254
+ | ✅ Yes
255
+
256
+ | `#add_namespace(prefix, uri)`
257
+ | Adds a namespace definition
258
+ | ✅ Yes
259
+ |===
260
+
261
+ ==== Attribute Methods
262
+
263
+ [cols="1,3,1"]
264
+ |===
265
+ | Method | Description | Always Available?
266
+
267
+ | `#[](name)`
268
+ | Gets attribute value
269
+ | ✅ Yes
270
+
271
+ | `#[]=(name, value)`
272
+ | Sets attribute value
273
+ | ✅ Yes
274
+
275
+ | `#attribute(name)`
276
+ | Returns Attribute object
277
+ | ✅ Yes (may be nil)
278
+
279
+ | `#attributes`
280
+ | Returns array of all attributes
281
+ | ✅ Yes
282
+
283
+ | `#remove_attribute(name)`
284
+ | Removes an attribute
285
+ | ✅ Yes
286
+ |===
287
+
288
+ ==== Content Methods
289
+
290
+ [cols="1,3,1"]
291
+ |===
292
+ | Method | Description | Always Available?
293
+
294
+ | `#text`
295
+ | Returns text content
296
+ | ✅ Yes
297
+
298
+ | `#text=(content)`
299
+ | Sets text content
300
+ | ✅ Yes
301
+
302
+ | `#inner_text`
303
+ | Returns inner text (concatenated)
304
+ | ✅ Yes
305
+
306
+ | `#inner_xml`
307
+ | Returns inner XML as string
308
+ | ✅ Yes
309
+
310
+ | `#inner_xml=(xml)`
311
+ | Sets inner XML from string
312
+ | ✅ Yes
313
+ |===
314
+
315
+ === Attribute
316
+
317
+ Attributes are name-value pairs attached to elements.
318
+
319
+ ==== Identity Methods
320
+
321
+ [cols="1,3,1"]
322
+ |===
323
+ | Method | Description | Always Available?
324
+
325
+ | `#name`
326
+ | Returns the attribute name
327
+ | ✅ Yes
328
+
329
+ | `#name=(new_name)`
330
+ | Sets the attribute name
331
+ | ✅ Yes
332
+
333
+ | `#identifier`
334
+ | Returns the primary identifier (same as #name)
335
+ | ✅ Yes
336
+ |===
337
+
338
+ ==== Value Methods
339
+
340
+ [cols="1,3,1"]
341
+ |===
342
+ | Method | Description | Always Available?
343
+
344
+ | `#value`
345
+ | Returns the attribute value
346
+ | ✅ Yes
347
+
348
+ | `#value=(new_value)`
349
+ | Sets the attribute value
350
+ | ✅ Yes
351
+
352
+ | `#text`
353
+ | Alias for #value (XPath compatibility)
354
+ | ✅ Yes
355
+ |===
356
+
357
+ ==== Relationship Methods
358
+
359
+ [cols="1,3,1"]
360
+ |===
361
+ | Method | Description | Always Available?
362
+
363
+ | `#element`
364
+ | Returns the owning element
365
+ | ✅ Yes
366
+
367
+ | `#namespace`
368
+ | Returns the attribute's namespace
369
+ | ✅ Yes (may be nil)
370
+ |===
371
+
372
+ === Text
373
+
374
+ Text nodes contain character data.
375
+
376
+ ==== Content Methods
377
+
378
+ [cols="1,3,1"]
379
+ |===
380
+ | Method | Description | Always Available?
381
+
382
+ | `#content`
383
+ | Returns the text content
384
+ | ✅ Yes
385
+
386
+ | `#content=(text)`
387
+ | Sets the text content
388
+ | ✅ Yes
389
+
390
+ | `#text`
391
+ | Alias for #content
392
+ | ✅ Yes
393
+ |===
394
+
395
+ ==== Identity Methods
396
+
397
+ [cols="1,3,1"]
398
+ |===
399
+ | Method | Description | Always Available?
400
+
401
+ | `#identifier`
402
+ | Returns nil (content nodes have no identifier)
403
+ | ✅ Yes
404
+ |===
405
+
406
+ === Cdata
407
+
408
+ CDATA sections contain character data that should not be parsed.
409
+
410
+ ==== Content Methods
411
+
412
+ [cols="1,3,1"]
413
+ |===
414
+ | Method | Description | Always Available?
415
+
416
+ | `#content`
417
+ | Returns the CDATA content
418
+ | ✅ Yes
419
+
420
+ | `#content=(text)`
421
+ | Sets the CDATA content
422
+ | ✅ Yes
423
+
424
+ | `#text`
425
+ | Alias for #content
426
+ | ✅ Yes
427
+ |===
428
+
429
+ ==== Identity Methods
430
+
431
+ [cols="1,3,1"]
432
+ |===
433
+ | Method | Description | Always Available?
434
+
435
+ | `#identifier`
436
+ | Returns nil (content nodes have no identifier)
437
+ | ✅ Yes
438
+ |===
439
+
440
+ === Comment
441
+
442
+ Comment nodes contain XML comments.
443
+
444
+ ==== Content Methods
445
+
446
+ [cols="1,3,1"]
447
+ |===
448
+ | Method | Description | Always Available?
449
+
450
+ | `#content`
451
+ | Returns the comment text
452
+ | ✅ Yes
453
+
454
+ | `#content=(text)`
455
+ | Sets the comment text
456
+ | ✅ Yes
457
+
458
+ | `#text`
459
+ | Alias for #content
460
+ | ✅ Yes
461
+ |===
462
+
463
+ ==== Identity Methods
464
+
465
+ [cols="1,3,1"]
466
+ |===
467
+ | Method | Description | Always Available?
468
+
469
+ | `#identifier`
470
+ | Returns nil (content nodes have no identifier)
471
+ | ✅ Yes
472
+ |===
473
+
474
+ === ProcessingInstruction
475
+
476
+ Processing instructions provide directives to applications.
477
+
478
+ ==== Identity Methods
479
+
480
+ [cols="1,3,1"]
481
+ |===
482
+ | Method | Description | Always Available?
483
+
484
+ | `#target`
485
+ | Returns the PI target
486
+ | ✅ Yes
487
+
488
+ | `#target=(new_target)`
489
+ | Sets the PI target
490
+ | ✅ Yes
491
+
492
+ | `#identifier`
493
+ | Returns the primary identifier (same as #target)
494
+ | ✅ Yes
495
+ |===
496
+
497
+ ==== Content Methods
498
+
499
+ [cols="1,3,1"]
500
+ |===
501
+ | Method | Description | Always Available?
502
+
503
+ | `#content`
504
+ | Returns the PI content
505
+ | ✅ Yes
506
+
507
+ | `#content=(new_content)`
508
+ | Sets the PI content
509
+ | ✅ Yes
510
+ |===
511
+
512
+ === Declaration
513
+
514
+ XML declarations specify document metadata.
515
+
516
+ ==== Property Methods
517
+
518
+ [cols="1,3,1"]
519
+ |===
520
+ | Method | Description | Always Available?
521
+
522
+ | `#version`
523
+ | Returns XML version (e.g., "1.0")
524
+ | ✅ Yes
525
+
526
+ | `#version=(new_version)`
527
+ | Sets XML version
528
+ | ✅ Yes
529
+
530
+ | `#encoding`
531
+ | Returns character encoding
532
+ | ✅ Yes (may be nil)
533
+
534
+ | `#encoding=(new_encoding)`
535
+ | Sets character encoding
536
+ | ✅ Yes
537
+
538
+ | `#standalone`
539
+ | Returns standalone status
540
+ | ✅ Yes (may be nil)
541
+
542
+ | `#standalone=(new_standalone)`
543
+ | Sets standalone status
544
+ | ✅ Yes
545
+ |===
546
+
547
+ ==== Identity Methods
548
+
549
+ [cols="1,3,1"]
550
+ |===
551
+ | Method | Description | Always Available?
552
+
553
+ | `#identifier`
554
+ | Returns nil (declarations have no identifier)
555
+ | ✅ Yes
556
+ |===
557
+
558
+ === Doctype
559
+
560
+ Document type declarations specify DTD information.
561
+
562
+ WARNING: Doctype accessor methods are **not fully implemented** across all adapters. The availability of these methods depends on the specific adapter being used.
563
+
564
+ ==== Identity Methods
565
+
566
+ [cols="1,3,1"]
567
+ |===
568
+ |Doctype |`#name` |✅ Yes |Returns DOCTYPE name (root element)
569
+ |Doctype |`#external_id` |✅ Yes |Returns PUBLIC identifier
570
+ |Doctype |`#system_id` |✅ Yes |Returns SYSTEM identifier (DTD URI)
571
+ |Doctype |`#identifier` |✅ Yes |Returns DOCTYPE name (same as `#name`)
572
+ |===
@@ -439,12 +439,12 @@ doc.to_xml # Use automatic preservation
439
439
  Controls whether XML declaration is included in serialized output:
440
440
 
441
441
  * `true`: Always include declaration
442
- * `false`: Never include declaration
442
+ * `false`: Never include declaration
443
443
  * Not specified: Use `has_xml_declaration` value (automatic preservation)
444
444
 
445
445
  == See Also
446
446
 
447
- * link:parsing-xml.html[Parsing XML] - How to parse XML documents
448
- * link:modifying-xml.html[Modifying XML] - Working with parsed documents
449
- * link:../adapters/index.html[Adapters] - Adapter-specific behavior
450
- * link:../best-practices.html[Best Practices] - General XML processing guidelines
447
+ * link:parsing-xml[Parsing XML] - How to parse XML documents
448
+ * link:modifying-xml[Modifying XML] - Working with parsed documents
449
+ * link:../adapters/index[Adapters] - Adapter-specific behavior
450
+ * link:../best-practices[Best Practices] - General XML processing guidelines
@@ -283,7 +283,7 @@ end
283
283
  === References
284
284
 
285
285
  * link:https://github.com/ruby/rexml[REXML on GitHub]
286
- * link:https://ruby-doc.org/stdlib/libdoc/rexml/rdoc/REXML.html[REXML documentation]
286
+ * link:https://ruby-doc.org/stdlib/libdoc/rexml/rdoc/REXML[REXML documentation]
287
287
 
288
288
  === See also
289
289
 
@@ -1,9 +1,77 @@
1
1
  ---
2
- title: Node API reference
3
- nav_order: 5
2
+ title: Node API Reference
3
+ :toc:
4
+ :toclevels: 3
4
5
  ---
5
6
 
6
- == Node API reference
7
+ == Node API Reference
8
+
9
+ This reference documents the API of all node types in Moxml. For a guide on API consistency and safe coding patterns, see the link:../guides/node-api-consistency[Node API Consistency Guide].
10
+
11
+ == Node Identity: The #identifier Method
12
+
13
+ All node types in Moxml support the `#identifier` method, which returns the primary identifier for a node:
14
+
15
+ [cols="1,2,1"]
16
+ |===
17
+ | Node Type | #identifier Returns | Example
18
+
19
+ | Element
20
+ | The tag name
21
+ | `"book"`, `"title"`
22
+
23
+ | Attribute
24
+ | The attribute name
25
+ | `"id"`, `"class"`
26
+
27
+ | ProcessingInstruction
28
+ | The PI target
29
+ | `"xml-stylesheet"`
30
+
31
+ | Text, Comment, Cdata
32
+ | `nil` (no identifier)
33
+ | `nil`
34
+
35
+ | Declaration
36
+ | `nil` (no identifier)
37
+ | `nil`
38
+
39
+ | Document
40
+ | `nil` (no identifier)
41
+ | `nil`
42
+ |===
43
+
44
+ **Example usage:**
45
+
46
+ [source,ruby]
47
+ ----
48
+ element = doc.at_xpath("//book")
49
+ puts element.identifier # => "book"
50
+
51
+ attr = element.attribute("id")
52
+ puts attr.identifier # => "id"
53
+
54
+ pi = doc.children.find { |n| n.processing_instruction? }
55
+ puts pi.identifier # => "xml-stylesheet"
56
+
57
+ text = element.children.find { |n| n.text? }
58
+ puts text.identifier # => nil
59
+ ----
60
+
61
+ **Safe iteration over mixed nodes:**
62
+
63
+ [source,ruby]
64
+ ----
65
+ doc.root.children.each do |node|
66
+ if id = node.identifier
67
+ puts "#{node.class.name.split('::').last}: #{id}"
68
+ else
69
+ puts "#{node.class.name.split('::').last}: (no identifier)"
70
+ end
71
+ end
72
+ ----
73
+
74
+ == Common Node Methods
7
75
 
8
76
  == XML objects and their methods
9
77
 
@@ -47,3 +115,25 @@ See also:
47
115
 
48
116
  * link:../guides/working-with-documents[Working with documents guide]
49
117
  * link:../guides/advanced-features[Advanced features guide]
118
+ === Doctype nodes
119
+
120
+ Doctype nodes represent DOCTYPE declarations in XML documents.
121
+
122
+ [source,ruby]
123
+ ----
124
+ doctype = doc.create_doctype("html", "-//W3C//DTD HTML 4.01//EN",
125
+ "http://www.w3.org/TR/html4/strict.dtd")
126
+ doctype.name # => "html"
127
+ doctype.external_id # => "-//W3C//DTD HTML 4.01//EN"
128
+ doctype.system_id # => "http://www.w3.org/TR/html4/strict.dtd"
129
+ doctype.identifier # => "html"
130
+ ----
131
+
132
+ *Available methods:*
133
+
134
+ * `name` - Returns the DOCTYPE name (root element name)
135
+ * `external_id` - Returns the PUBLIC identifier (or nil)
136
+ * `system_id` - Returns the SYSTEM identifier (DTD URI, or nil)
137
+ * `identifier` - Returns the primary identifier (same as `name`)
138
+
139
+ All Doctype accessor methods are fully implemented across all 6 adapters.
@@ -788,6 +788,20 @@ module Moxml
788
788
  end
789
789
  end
790
790
 
791
+ # Doctype accessor methods
792
+ def doctype_name(native)
793
+ # LibXML uses DoctypeWrapper which stores the values
794
+ native.name
795
+ end
796
+
797
+ def doctype_external_id(native)
798
+ native.external_id
799
+ end
800
+
801
+ def doctype_system_id(native)
802
+ native.system_id
803
+ end
804
+
791
805
  def xpath(node, expression, namespaces = nil)
792
806
  native_node = unpatch_node(node)
793
807
  return [] unless native_node
@@ -1149,7 +1163,7 @@ module Moxml
1149
1163
  # Add namespace definitions (only on this element, not ancestors)
1150
1164
  if elem.respond_to?(:namespaces)
1151
1165
  seen_ns = {}
1152
- elem.namespaces.definitions.each do |ns|
1166
+ elem.namespaces.each do |ns|
1153
1167
  prefix = ns.prefix
1154
1168
  uri = ns.href
1155
1169
  next if seen_ns.key?(prefix)
@@ -321,6 +321,19 @@ module Moxml
321
321
  node.namespace_definitions
322
322
  end
323
323
 
324
+ # Doctype accessor methods
325
+ def doctype_name(native)
326
+ native.name
327
+ end
328
+
329
+ def doctype_external_id(native)
330
+ native.external_id
331
+ end
332
+
333
+ def doctype_system_id(native)
334
+ native.system_id
335
+ end
336
+
324
337
  def xpath(node, expression, namespaces = nil)
325
338
  node.xpath(expression, namespaces).to_a
326
339
  rescue ::Nokogiri::XML::XPath::SyntaxError => e
@@ -365,6 +365,19 @@ module Moxml
365
365
  node.namespaces.values
366
366
  end
367
367
 
368
+ # Doctype accessor methods
369
+ def doctype_name(native)
370
+ native.name
371
+ end
372
+
373
+ def doctype_external_id(native)
374
+ native.public_id
375
+ end
376
+
377
+ def doctype_system_id(native)
378
+ native.system_id
379
+ end
380
+
368
381
  def xpath(node, expression, namespaces = nil)
369
382
  node.xpath(expression, {},
370
383
  namespaces: namespaces&.transform_keys(&:to_s)).to_a
@@ -510,6 +510,31 @@ module Moxml
510
510
  end.values
511
511
  end
512
512
 
513
+ # Doctype accessor methods
514
+ # Ox stores DOCTYPE as a string, so we parse it
515
+ def doctype_name(native)
516
+ # Parse: "name PUBLIC \"external_id\" \"system_id\"" or "name SYSTEM \"system_id\""
517
+ value = native.value.to_s.strip
518
+ # Extract the first word (the name)
519
+ value.split(/\s+/).first
520
+ end
521
+
522
+ def doctype_external_id(native)
523
+ value = native.value.to_s
524
+ # Match PUBLIC "external_id"
525
+ match = value.match(/PUBLIC\s+"([^"]*)"/)
526
+ match ? match[1] : nil
527
+ end
528
+
529
+ def doctype_system_id(native)
530
+ value = native.value.to_s
531
+ # Match the last quoted string (system_id)
532
+ # For PUBLIC: "name PUBLIC \"external_id\" \"system_id\""
533
+ # For SYSTEM: "name SYSTEM \"system_id\""
534
+ matches = value.scan(/"([^"]*)"/)
535
+ matches.last&.first
536
+ end
537
+
513
538
  def xpath(node, expression, namespaces = {})
514
539
  # Translate common XPath patterns to Ox locate() syntax
515
540
  locate_expr = translate_xpath_to_locate(expression, namespaces)
@@ -426,6 +426,19 @@ module Moxml
426
426
  end
427
427
  end
428
428
 
429
+ # Doctype accessor methods
430
+ def doctype_name(native)
431
+ native.name
432
+ end
433
+
434
+ def doctype_external_id(native)
435
+ native.public
436
+ end
437
+
438
+ def doctype_system_id(native)
439
+ native.system
440
+ end
441
+
429
442
  # not used at the moment
430
443
  # but may be useful when the xpath is upgraded to work with namespaces
431
444
  def prepare_xpath_namespaces(node)
@@ -10,6 +10,12 @@ module Moxml
10
10
  adapter.set_attribute_name(@native, new_name)
11
11
  end
12
12
 
13
+ # Returns the primary identifier for this attribute (its name)
14
+ # @return [String] the attribute name
15
+ def identifier
16
+ name
17
+ end
18
+
13
19
  def value
14
20
  @native.value
15
21
  end
data/lib/moxml/doctype.rb CHANGED
@@ -1,17 +1,50 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Moxml
4
+ # Represents an XML DOCTYPE declaration
5
+ #
6
+ # @note Doctype accessor methods are not fully implemented across all adapters.
7
+ # The availability of #name, #external_id, and #system_id depends on whether
8
+ # the specific adapter implements the corresponding adapter methods:
9
+ # - adapter.doctype_name(native)
10
+ # - adapter.doctype_external_id(native)
11
+ # - adapter.doctype_system_id(native)
12
+ #
13
+ # Most adapters do not currently implement these methods. If you need DOCTYPE
14
+ # information, consider using adapter-specific methods or parsing the serialized
15
+ # XML manually.
4
16
  class Doctype < Node
17
+ # Returns the DOCTYPE name (root element name)
18
+ #
19
+ # @return [String, nil] the DOCTYPE name
20
+ # @raise [NotImplementedError] if the adapter doesn't implement doctype_name
5
21
  def name
6
22
  adapter.doctype_name(@native)
7
23
  end
8
24
 
25
+ # Returns the DOCTYPE external ID
26
+ #
27
+ # @return [String, nil] the external ID
28
+ # @raise [NotImplementedError] if the adapter doesn't implement doctype_external_id
9
29
  def external_id
10
30
  adapter.doctype_external_id(@native)
11
31
  end
12
32
 
33
+ # Returns the DOCTYPE system ID
34
+ #
35
+ # @return [String, nil] the system ID
36
+ # @raise [NotImplementedError] if the adapter doesn't implement doctype_system_id
13
37
  def system_id
14
38
  adapter.doctype_system_id(@native)
15
39
  end
40
+
41
+ # Returns the primary identifier for this doctype
42
+ # Since DOCTYPE information is not reliably available across adapters,
43
+ # this returns nil.
44
+ #
45
+ # @return [nil]
46
+ def identifier
47
+ name
48
+ end
16
49
  end
17
50
  end
data/lib/moxml/element.rb CHANGED
@@ -13,6 +13,12 @@ module Moxml
13
13
  adapter.set_node_name(@native, value)
14
14
  end
15
15
 
16
+ # Returns the primary identifier for this element (its tag name)
17
+ # @return [String] the element name
18
+ def identifier
19
+ name
20
+ end
21
+
16
22
  # Returns the expanded name including namespace prefix
17
23
  def expanded_name
18
24
  if namespace_prefix && !namespace_prefix.empty?
data/lib/moxml/error.rb CHANGED
@@ -40,7 +40,7 @@ module Moxml
40
40
  msg = super
41
41
  msg += "\n Expression: #{@expression}" if @expression
42
42
  msg += "\n Adapter: #{@adapter}" if @adapter
43
- msg += "\n Node: <#{@node.name}>" if @node.respond_to?(:name)
43
+ msg += "\n Node: <#{@node.name}>" if @node.is_a?(Element) || @node.is_a?(Attribute)
44
44
  msg += "\n Hint: Verify XPath syntax and ensure the adapter supports the expression"
45
45
  msg
46
46
  end
@@ -60,9 +60,9 @@ module Moxml
60
60
  def to_s
61
61
  msg = super
62
62
  # Only add extra details if any were provided
63
- has_details = @node.respond_to?(:name) || @constraint || @value
63
+ has_details = (@node.is_a?(Element) || @node.is_a?(Attribute)) || @constraint || @value
64
64
  if has_details
65
- msg += "\n Node: <#{@node.name}>" if @node.respond_to?(:name)
65
+ msg += "\n Node: <#{@node.name}>" if @node.is_a?(Element) || @node.is_a?(Attribute)
66
66
  msg += "\n Constraint: #{@constraint}" if @constraint
67
67
  msg += "\n Value: #{@value.inspect}" if @value
68
68
  msg += "\n Hint: Ensure the value meets XML specification requirements"
@@ -119,7 +119,7 @@ module Moxml
119
119
 
120
120
  def to_s
121
121
  msg = super
122
- msg += "\n Node: <#{@node.name}>" if @node.respond_to?(:name)
122
+ msg += "\n Node: <#{@node.name}>" if @node.is_a?(Element) || @node.is_a?(Attribute)
123
123
  msg += "\n Adapter: #{@adapter}" if @adapter
124
124
  msg += "\n Format: #{@format}" if @format
125
125
  msg += "\n Hint: Check that the node structure is valid for serialization"
@@ -160,7 +160,7 @@ module Moxml
160
160
  def to_s
161
161
  msg = super
162
162
  msg += "\n Attribute: #{@attribute_name}" if @attribute_name
163
- msg += "\n Element: <#{@element.name}>" if @element.respond_to?(:name)
163
+ msg += "\n Element: <#{@element.name}>" if @element.is_a?(Element)
164
164
  msg += "\n Value: #{@value.inspect}" if @value
165
165
  msg += "\n Hint: Verify attribute name follows XML naming rules"
166
166
  msg
data/lib/moxml/node.rb CHANGED
@@ -186,6 +186,18 @@ module Moxml
186
186
  end
187
187
  end
188
188
 
189
+ # Returns the primary identifier for this node type
190
+ # For Element: the tag name
191
+ # For Attribute: the attribute name
192
+ # For ProcessingInstruction: the target
193
+ # For content nodes (Text, Comment, Cdata, Declaration): nil (no identifier)
194
+ # For Doctype: nil (not fully implemented across adapters)
195
+ #
196
+ # @return [String, nil] the node's primary identifier or nil
197
+ def identifier
198
+ nil
199
+ end
200
+
189
201
  def self.wrap(node, context)
190
202
  return nil if node.nil?
191
203
 
@@ -10,6 +10,12 @@ module Moxml
10
10
  adapter.set_node_name(@native, new_target.to_s)
11
11
  end
12
12
 
13
+ # Returns the primary identifier for this processing instruction (its target)
14
+ # @return [String] the PI target
15
+ def identifier
16
+ target
17
+ end
18
+
13
19
  def content
14
20
  adapter.processing_instruction_content(@native)
15
21
  end
data/lib/moxml/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Moxml
4
- VERSION = "0.1.9"
4
+ VERSION = "0.1.10"
5
5
  end
@@ -36,7 +36,7 @@ module Moxml
36
36
 
37
37
  def to_s
38
38
  msg = super
39
- msg += "\n Context node: <#{@context_node.name}>" if @context_node.respond_to?(:name)
39
+ msg += "\n Context node: <#{@context_node.name}>" if @context_node.is_a?(Moxml::Element) || @context_node.is_a?(Moxml::Attribute)
40
40
  msg += "\n Step: #{@step}" if @step
41
41
  msg
42
42
  end
@@ -8,7 +8,6 @@ RSpec.describe Moxml::Doctype do
8
8
 
9
9
  describe "#name" do
10
10
  it "returns doctype name" do
11
- skip "Doctype accessor methods not yet implemented in all adapters"
12
11
  doctype = doc.create_doctype("root", nil, "test.dtd")
13
12
  expect(doctype.name).to eq("root")
14
13
  end
@@ -16,15 +15,32 @@ RSpec.describe Moxml::Doctype do
16
15
 
17
16
  describe "#system_id" do
18
17
  it "returns system identifier" do
19
- skip "Doctype accessor methods not yet implemented in all adapters"
20
18
  doctype = doc.create_doctype("root", nil, "test.dtd")
21
19
  expect(doctype.system_id).to eq("test.dtd")
22
20
  end
23
21
  end
24
22
 
23
+ describe "#external_id" do
24
+ it "returns external identifier when present" do
25
+ doctype = doc.create_doctype("html", "-//W3C//DTD HTML 4.01//EN", "http://www.w3.org/TR/html4/strict.dtd")
26
+ expect(doctype.external_id).to eq("-//W3C//DTD HTML 4.01//EN")
27
+ end
28
+
29
+ it "returns nil when not present" do
30
+ doctype = doc.create_doctype("root", nil, "test.dtd")
31
+ expect(doctype.external_id).to be_nil
32
+ end
33
+ end
34
+
35
+ describe "#identifier" do
36
+ it "returns the doctype name" do
37
+ doctype = doc.create_doctype("html", nil, nil)
38
+ expect(doctype.identifier).to eq("html")
39
+ end
40
+ end
41
+
25
42
  describe "creation" do
26
43
  it "creates a doctype" do
27
- skip "Doctype accessor methods not yet implemented in all adapters"
28
44
  doctype = doc.create_doctype("html", nil, nil)
29
45
  expect(doctype).to be_a(described_class)
30
46
  expect(doctype.name).to eq("html")
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: moxml
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.9
4
+ version: 0.1.10
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2025-11-24 00:00:00.000000000 Z
11
+ date: 2025-11-26 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: |
14
14
  Moxml is a unified XML manipulation library that provides a common API
@@ -29,7 +29,6 @@ files:
29
29
  - ".rspec"
30
30
  - ".rubocop.yml"
31
31
  - ".rubocop_todo.yml"
32
- - ".ruby-version"
33
32
  - Gemfile
34
33
  - LICENSE.md
35
34
  - README.adoc
@@ -44,6 +43,7 @@ files:
44
43
  - docs/_guides/development-testing.adoc
45
44
  - docs/_guides/index.adoc
46
45
  - docs/_guides/modifying-xml.adoc
46
+ - docs/_guides/node-api-consistency.adoc
47
47
  - docs/_guides/parsing-xml.adoc
48
48
  - docs/_guides/sax-parsing.adoc
49
49
  - docs/_guides/working-with-documents.adoc
data/.ruby-version DELETED
@@ -1 +0,0 @@
1
- 3.0.7@moxml