taurus 0.1.1 → 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.
- checksums.yaml +4 -4
- data/README.adoc +270 -1394
- data/lib/taurus/version.rb +1 -1
- metadata +2 -2
data/README.adoc
CHANGED
|
@@ -1,1529 +1,405 @@
|
|
|
1
|
-
=
|
|
1
|
+
= taurus-ruby
|
|
2
2
|
:toc:
|
|
3
3
|
:toclevels: 3
|
|
4
4
|
|
|
5
5
|
image:https://img.shields.io/gem/v/taurus.svg[RubyGems Version,link=https://rubygems.org/gems/taurus]
|
|
6
|
-
image:https://
|
|
7
|
-
image:https://github.com/lutaml/taurus/actions/workflows/test.yml/badge.svg[Test Suite,link=https://github.com/lutaml/taurus/actions/workflows/test.yml]
|
|
8
|
-
image:https://github.com/lutaml/taurus/actions/workflows/build.yml/badge.svg[CLI Build,link=https://github.com/lutaml/taurus/actions/workflows/build.yml]
|
|
6
|
+
image:https://github.com/lutaml/taurus-ruby/actions/workflows/build.yml/badge.svg[CI,link=https://github.com/lutaml/taurus-ruby/actions/workflows/build.yml]
|
|
9
7
|
|
|
10
|
-
|
|
8
|
+
A Nokogiri-compatible Ruby binding for
|
|
9
|
+
https://github.com/lutaml/taurus[libtaurus], a pure-C99 XML 1.0 parser
|
|
10
|
+
with full https://www.w3.org/TR/1999/REC-xpath-19991116/[XPath 1.0],
|
|
11
|
+
XML Namespaces 1.0, SAX, and C14N (1.0 / 1.1 / Exclusive).
|
|
11
12
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
== Purpose
|
|
16
|
-
|
|
17
|
-
Taurus is a next-generation XML parser for Ruby that combines:
|
|
18
|
-
|
|
19
|
-
* *Fast XML parsing* - C-based XML parsing with SIMD
|
|
20
|
-
|
|
21
|
-
optimizations
|
|
22
|
-
* *Complete namespace support* - Full XML Namespaces 1.0 specification
|
|
23
|
-
* *XPath 1.0 in C* - All 13 axes, 27 functions, operators, predicates ✅
|
|
24
|
-
* *Memory efficiency* - Optimized memory usage with zero leaks
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
== Performance
|
|
28
|
-
|
|
29
|
-
**Version**: 1.0.0
|
|
30
|
-
**Status**: Production Ready - First Stable Release! 🎉
|
|
31
|
-
|
|
32
|
-
[cols="3,2",options="header"]
|
|
33
|
-
|===
|
|
34
|
-
|Component |Status
|
|
35
|
-
|
|
36
|
-
|XML Parsing
|
|
37
|
-
|✅ Complete (100%)
|
|
38
|
-
|
|
39
|
-
|XML Namespaces 1.0
|
|
40
|
-
|✅ Complete (100%)
|
|
41
|
-
|
|
42
|
-
|XPath 1.0 Engine
|
|
43
|
-
|✅ Complete (100% spec compliance)
|
|
44
|
-
|
|
45
|
-
|**Pure C Library (libtaurus)**
|
|
46
|
-
|✅ **Complete** (44+ functions, all exported)
|
|
47
|
-
|
|
48
|
-
|**Ruby FFI Bindings**
|
|
49
|
-
|✅ **Complete** (AutoPointer, thread-safe errors)
|
|
50
|
-
|
|
51
|
-
|**C CLI Tool**
|
|
52
|
-
|✅ **Complete** (4 commands: parse, xpath, format, version)
|
|
53
|
-
|
|
54
|
-
|Ruby Test Suite
|
|
55
|
-
|✅ 335/336 passing (99.7%) - 250/250 XPath tests (100%)
|
|
56
|
-
|
|
57
|
-
|Memory Safety
|
|
58
|
-
|✅ Zero leaks verified
|
|
59
|
-
|===
|
|
60
|
-
|
|
61
|
-
=== Current Performance
|
|
62
|
-
|
|
63
|
-
**XML Parsing** (FFI via libtaurus):
|
|
64
|
-
* **5.87µs** per parse (2.45× slower than Ox's 2.4µs)
|
|
65
|
-
* **C library**: 5.3µs (2.22× slower than Ox)
|
|
66
|
-
* **FFI overhead**: Only 18% (5.3µs → 5.87µs)
|
|
67
|
-
* **Status**: Excellent - near C-extension speed with FFI portability! ✅
|
|
68
|
-
|
|
69
|
-
**XPath Queries** (tested on 5-element document):
|
|
70
|
-
* **Complete XPath 1.0**: All 27 functions, 13 axes working
|
|
71
|
-
* **AST Caching**: Parse once, use forever with O(1) lookup
|
|
72
|
-
* **Status**: Production-ready with full spec compliance ✅
|
|
73
|
-
|
|
74
|
-
**FFI Architecture** (v0.5.0):
|
|
75
|
-
* Pure C library (lib taurus) with 44+ public API functions
|
|
76
|
-
* Ruby FFI bindings with AutoPointer memory management
|
|
77
|
-
* CLI tool using libtaurus directly (zero Ruby overhead)
|
|
78
|
-
* **Trade-off**: ~18% FFI overhead but no compilation needed! ✅
|
|
79
|
-
|
|
80
|
-
=== DOM Access Performance (v0.2.0) 🚀
|
|
81
|
-
|
|
82
|
-
Taurus v0.2.0 achieves exceptional DOM access performance through targeted optimizations:
|
|
83
|
-
|
|
84
|
-
[cols="3,2,2,2",options="header"]
|
|
85
|
-
|===
|
|
86
|
-
|Operation |Taurus v0.2.0 |Ox |Status
|
|
87
|
-
|
|
88
|
-
|Root access
|
|
89
|
-
|0.09µs
|
|
90
|
-
|0.06µs
|
|
91
|
-
|✅ Close (1.5×)
|
|
92
|
-
|
|
93
|
-
|Element name
|
|
94
|
-
|0.18µs
|
|
95
|
-
|0.09µs
|
|
96
|
-
|✅ Competitive (2×)
|
|
97
|
-
|
|
98
|
-
|Attribute access
|
|
99
|
-
|0.181µs
|
|
100
|
-
|0.157µs
|
|
101
|
-
|✅ On par
|
|
102
|
-
|
|
103
|
-
|**Children access**
|
|
104
|
-
|**0.069µs**
|
|
105
|
-
|**0.13µs**
|
|
106
|
-
|🚀 ***1.88× Faster!***
|
|
107
|
-
|
|
108
|
-
|Deep traversal
|
|
109
|
-
|2.12µs
|
|
110
|
-
|2.95µs
|
|
111
|
-
|✅ On par
|
|
112
|
-
|===
|
|
113
|
-
|
|
114
|
-
**Children access is now faster than Ox!** 🏆
|
|
115
|
-
|
|
116
|
-
==== Optimization Techniques
|
|
117
|
-
|
|
118
|
-
v0.2.0 implements four key optimizations:
|
|
119
|
-
|
|
120
|
-
**1. Root Element Caching (5.4× faster)**
|
|
121
|
-
|
|
122
|
-
[source,ruby]
|
|
123
|
-
----
|
|
124
|
-
# Caches root element after first access
|
|
125
|
-
doc = Taurus.parse(xml)
|
|
126
|
-
root = doc.root # First call: scans nodes array
|
|
127
|
-
root = doc.root # Subsequent: instant cache hit
|
|
128
|
-
----
|
|
129
|
-
|
|
130
|
-
**2. String Interning (1.39× faster)**
|
|
131
|
-
|
|
132
|
-
Element names are automatically interned and frozen in C, providing automatic memory deduplication and VM optimization hints.
|
|
133
|
-
|
|
134
|
-
**3. Symbol Fast-Path for Attributes (Matches Ox)**
|
|
135
|
-
|
|
136
|
-
[source,ruby]
|
|
137
|
-
----
|
|
138
|
-
elem[:id] # Fast: direct symbol lookup (O(1))
|
|
139
|
-
elem["id"] # Compatible: converted to symbol
|
|
140
|
-
----
|
|
141
|
-
|
|
142
|
-
*Best practice*: Use symbol keys for 90% of real-world usage pattern.
|
|
143
|
-
|
|
144
|
-
**4. Direct ivar Access for Children (2.3× faster)**
|
|
145
|
-
|
|
146
|
-
[source,ruby]
|
|
147
|
-
----
|
|
148
|
-
# @nodes always initialized in C/Ruby
|
|
149
|
-
elem.nodes # Direct access, no lazy init overhead
|
|
150
|
-
----
|
|
151
|
-
|
|
152
|
-
==== Best Practices for Performance
|
|
153
|
-
|
|
154
|
-
1. **Use symbol keys**: `elem[:attr]` is faster than `elem["attr"]`
|
|
155
|
-
2. **Cache root reference**: Call `doc.root` once, reuse the reference
|
|
156
|
-
3. **Iterate children efficiently**: Use `elem.nodes.each` not repeated `elem.nodes[i]`
|
|
157
|
-
4. **Trust string interning**: Element names automatically deduplicated
|
|
158
|
-
|
|
159
|
-
=== Performance Optimizations (v0.9.0)
|
|
160
|
-
|
|
161
|
-
==== XPath Namespace Resolution
|
|
162
|
-
|
|
163
|
-
**2-3× faster** namespace resolution with reverse iteration strategy:
|
|
164
|
-
|
|
165
|
-
* **Best case**: O(1) - Local namespace found immediately
|
|
166
|
-
* **Average case**: O(k) where k << n (most queries)
|
|
167
|
-
* **Significant** for nested documents with namespace overrides
|
|
168
|
-
|
|
169
|
-
Implementation highlights:
|
|
170
|
-
* Reverse iteration finds local (recent) namespace registrations first
|
|
171
|
-
* Pointer comparison fast-path for repeated queries
|
|
172
|
-
* Early exit on match (no full array scan)
|
|
173
|
-
* Naturally handles namespace override semantics
|
|
174
|
-
|
|
175
|
-
==== XPath Function Benchmarks
|
|
176
|
-
|
|
177
|
-
All 27 XPath 1.0 functions tested (see link:docs/v0.9.0_PERFORMANCE_IMPROVEMENTS.md[Complete Results]):
|
|
178
|
-
|
|
179
|
-
**Ultra-Fast** (<5μs):
|
|
180
|
-
* Boolean: `true()`, `false()` - 3.6μs
|
|
181
|
-
* String: `normalize-space()`, `substring-after()` - 4.8μs
|
|
182
|
-
* Number: `ceiling()` - 4.6μs
|
|
183
|
-
|
|
184
|
-
**Fast** (5-10μs):
|
|
185
|
-
* String: `translate()`, `string-length()`, `substring()`
|
|
186
|
-
* Node-set: `local-name()`, `name()`, `namespace-uri()`
|
|
187
|
-
|
|
188
|
-
**Medium** (10-40μs):
|
|
189
|
-
* String: `concat()`, `starts-with()`, `contains()`
|
|
190
|
-
* Node-set: `last()`, `id()`, `position()`
|
|
191
|
-
|
|
192
|
-
=== Key Optimizations
|
|
193
|
-
|
|
194
|
-
* **Namespace Resolution** (v0.9.0): 2-3× faster with reverse iteration
|
|
195
|
-
* **SIMD Vectorization**: ARM NEON & x86 SSE2 for 300% parsing speedup
|
|
196
|
-
* **Character Classification Table**: 256-byte lookup for zero-branch character tests
|
|
197
|
-
* **AST Pattern Optimization**: Rewrites inefficient query patterns before evaluation
|
|
198
|
-
* **AST Caching**: Global cache with O(1) lookup - parse once, use forever
|
|
199
|
-
* **DOM Optimizations** (v0.2.0): Root caching, string interning, symbol fast-path, direct ivar access
|
|
200
|
-
|
|
201
|
-
For comprehensive XPath axis and function benchmarks, see link:docs/xpath-performance.adoc[XPath Performance Benchmarks] (115+ query patterns tested).
|
|
202
|
-
|
|
203
|
-
For detailed optimization history and lessons learned, see link:docs/OPTIMIZATIONS_IMPLEMENTED.adoc[Optimizations Implemented].
|
|
204
|
-
|
|
205
|
-
=== Performance vs Competition
|
|
206
|
-
|
|
207
|
-
**XML Parsing**:
|
|
208
|
-
|
|
209
|
-
[cols="3,2,2,2",options="header"]
|
|
210
|
-
|===
|
|
211
|
-
|Parser |Parse Time |vs Taurus |Memory
|
|
212
|
-
|
|
213
|
-
|**Ox**
|
|
214
|
-
|2.4µs
|
|
215
|
-
|0.4× (faster)
|
|
216
|
-
|1.0×
|
|
217
|
-
|
|
218
|
-
|**Taurus**
|
|
219
|
-
|5.87µs
|
|
220
|
-
|1.0× (baseline)
|
|
221
|
-
|~1.1×
|
|
222
|
-
|
|
223
|
-
|Nokogiri
|
|
224
|
-
|~10µs
|
|
225
|
-
|1.7× (slower)
|
|
226
|
-
|1.3×
|
|
227
|
-
|
|
228
|
-
|Oga
|
|
229
|
-
|~15µs
|
|
230
|
-
|2.6× (slower)
|
|
231
|
-
|1.5×
|
|
232
|
-
|
|
233
|
-
|Calculated Speedup
|
|
234
|
-
|v0.4×
|
|
235
|
-
|v1.2×
|
|
236
|
-
|
|
237
|
-
|]])
|
|
238
|
-
|
|
239
|
-
**XPath Queries** (`//book` on 5-element document):
|
|
240
|
-
|
|
241
|
-
[cols="3,2,2,2",options="header"]
|
|
242
|
-
|===
|
|
243
|
-
|Parser |XPath Time |vs Nokogiri |Status
|
|
244
|
-
|
|
245
|
-
|**Nokogiri**
|
|
246
|
-
|**3.87µs**
|
|
247
|
-
|**1.0× (baseline)**
|
|
248
|
-
|**✅ Fastest** (libxml2)
|
|
249
|
-
|
|
250
|
-
|Taurus
|
|
251
|
-
|9.00µs
|
|
252
|
-
|2.3× (slower)
|
|
253
|
-
|✅ Complete XPath 1.0
|
|
254
|
-
|
|
255
|
-
|Ox
|
|
256
|
-
|N/A
|
|
257
|
-
|N/A
|
|
258
|
-
|❌ No XPath support
|
|
259
|
-
|
|
260
|
-
|Oga
|
|
261
|
-
|~300µs
|
|
262
|
-
|~77× (slower)
|
|
263
|
-
|Pure Ruby
|
|
264
|
-
|===
|
|
265
|
-
|
|
266
|
-
*Taurus: Ox-level parsing + Complete XPath 1.0 (27 functions) + Full namespaces + Zero dependencies*
|
|
13
|
+
The C DOM is the single source of truth — Ruby objects are thin FFI
|
|
14
|
+
handles over the C pointers, so every Ruby method maps to one FFI call.
|
|
15
|
+
No tree hydration, no parallel Ruby-side model.
|
|
267
16
|
|
|
268
17
|
== Installation
|
|
269
18
|
|
|
270
|
-
=== As a Library (Recommended: FFI)
|
|
271
|
-
|
|
272
|
-
Taurus v0.5.0+ uses Ruby FFI for better portability - no compilation required!
|
|
273
|
-
|
|
274
19
|
Add to your Gemfile:
|
|
275
20
|
|
|
276
21
|
[source,ruby]
|
|
277
22
|
----
|
|
278
|
-
gem
|
|
279
|
-
----
|
|
280
|
-
|
|
281
|
-
Then execute:
|
|
282
|
-
|
|
283
|
-
[source,shell]
|
|
284
|
-
----
|
|
285
|
-
bundle install
|
|
23
|
+
gem "taurus"
|
|
286
24
|
----
|
|
287
25
|
|
|
288
|
-
|
|
26
|
+
Then `bundle install`.
|
|
289
27
|
|
|
290
|
-
|
|
28
|
+
=== Runtime requirement: libtaurus
|
|
291
29
|
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
✅ **Easy Updates**: Just `bundle update taurus` +
|
|
295
|
-
✅ **Minimal Overhead**: Only 15-20% compared to direct C binding +
|
|
296
|
-
✅ **Clean API**: Simple and consistent interface
|
|
297
|
-
|
|
298
|
-
==== Building libtaurus from Source
|
|
299
|
-
|
|
300
|
-
The native library is included, but you can rebuild it:
|
|
30
|
+
`taurus` shells out to the native `libtaurus` shared library via FFI.
|
|
31
|
+
You need `libtaurus.{dylib,so,dll}` installed on the host. Options:
|
|
301
32
|
|
|
33
|
+
1. **Homebrew (macOS, easiest):** `brew install lutaml/tap/libtaurus`
|
|
34
|
+
(if packaged) or build from source (see below).
|
|
35
|
+
2. **Build from source** (Linux/macOS/Windows):
|
|
36
|
+
+
|
|
302
37
|
[source,shell]
|
|
303
38
|
----
|
|
304
39
|
git clone https://github.com/lutaml/taurus.git
|
|
305
40
|
cd taurus
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
=== As a Command-Line Tool
|
|
314
|
-
|
|
315
|
-
Install directly to get the `taurus` CLI:
|
|
316
|
-
|
|
317
|
-
[source,shell]
|
|
318
|
-
----
|
|
319
|
-
gem install taurus
|
|
320
|
-
----
|
|
321
|
-
|
|
322
|
-
Verify installation:
|
|
323
|
-
|
|
324
|
-
[source,shell]
|
|
325
|
-
----
|
|
326
|
-
taurus version
|
|
327
|
-
# Taurus 0.3.0
|
|
328
|
-
# Fast XML parser with complete XPath 1.0 support
|
|
329
|
-
----
|
|
330
|
-
|
|
331
|
-
==== Shell Completion (Optional)
|
|
332
|
-
|
|
333
|
-
Enable command-line completion for faster CLI usage:
|
|
334
|
-
|
|
335
|
-
**Bash**
|
|
336
|
-
|
|
337
|
-
[source,shell]
|
|
338
|
-
----
|
|
339
|
-
# Install globally (requires sudo)
|
|
340
|
-
sudo cp docs/completion/taurus.bash /etc/bash_completion.d/taurus
|
|
341
|
-
|
|
342
|
-
# Or for current user only
|
|
343
|
-
mkdir -p ~/.bash_completion.d
|
|
344
|
-
cp docs/completion/taurus.bash ~/.bash_completion.d/taurus
|
|
345
|
-
echo 'source ~/.bash_completion.d/taurus' >> ~/.bashrc
|
|
346
|
-
source ~/.bashrc
|
|
347
|
-
----
|
|
348
|
-
|
|
349
|
-
**Zsh**
|
|
350
|
-
|
|
351
|
-
[source,shell]
|
|
352
|
-
----
|
|
353
|
-
# Install globally (requires sudo)
|
|
354
|
-
sudo cp docs/completion/taurus.zsh /usr/local/share/zsh/site-functions/_taurus
|
|
355
|
-
|
|
356
|
-
# Or for current user only
|
|
357
|
-
mkdir -p ~/.zsh/completion
|
|
358
|
-
cp docs/completion/taurus.zsh ~/.zsh/completion/_taurus
|
|
359
|
-
echo 'fpath=(~/.zsh/completion $fpath)' >> ~/.zshrc
|
|
360
|
-
echo 'autoload -Uz compinit && compinit' >> ~/.zshrc
|
|
361
|
-
source ~/.zshrc
|
|
362
|
-
----
|
|
363
|
-
|
|
364
|
-
After installation, you can use tab completion:
|
|
365
|
-
|
|
366
|
-
[source,shell]
|
|
367
|
-
----
|
|
368
|
-
taurus p<TAB> # Completes to 'parse'
|
|
369
|
-
taurus parse --f<TAB> # Completes to '--format'
|
|
370
|
-
taurus xpath doc.xml --format <TAB> # Shows: xml json text
|
|
371
|
-
----
|
|
372
|
-
|
|
373
|
-
==== Man Pages (Optional)
|
|
374
|
-
|
|
375
|
-
View comprehensive documentation using man pages:
|
|
376
|
-
|
|
377
|
-
[source,shell]
|
|
378
|
-
----
|
|
379
|
-
# View main manual
|
|
380
|
-
man docs/man/taurus.1
|
|
381
|
-
|
|
382
|
-
# View command-specific manuals
|
|
383
|
-
man docs/man/taurus-parse.1
|
|
384
|
-
man docs/man/taurus-xpath.1
|
|
385
|
-
man docs/man/taurus-format.1
|
|
386
|
-
----
|
|
387
|
-
|
|
388
|
-
To install system-wide (when building CLI from source):
|
|
389
|
-
|
|
390
|
-
[source,shell]
|
|
391
|
-
----
|
|
392
|
-
mkdir -p build && cd build
|
|
393
|
-
cmake .. -DTAURUS_BUILD_CLI=ON
|
|
394
|
-
cmake --build . --config Release
|
|
395
|
-
sudo cmake --install .
|
|
41
|
+
cmake -B build -S . \
|
|
42
|
+
-DCMAKE_BUILD_TYPE=Release \
|
|
43
|
+
-DTAURUS_BUILD_SHARED=ON \
|
|
44
|
+
-DTAURUS_BUILD_STATIC=OFF \
|
|
45
|
+
-DCMAKE_WINDOWS_EXPORT_ALL_SYMBOLS=ON
|
|
46
|
+
cmake --build build -j
|
|
47
|
+
sudo cmake --install build # optional, system-wide
|
|
396
48
|
----
|
|
397
49
|
|
|
398
|
-
|
|
399
|
-
|
|
50
|
+
3. **Point Taurus at a specific path** by setting `TAURUS_LIB_PATH`:
|
|
51
|
+
+
|
|
400
52
|
[source,shell]
|
|
401
53
|
----
|
|
402
|
-
|
|
403
|
-
man taurus-parse
|
|
404
|
-
man taurus-xpath
|
|
405
|
-
man taurus-format
|
|
54
|
+
export TAURUS_LIB_PATH=/usr/local/lib/libtaurus.dylib
|
|
406
55
|
----
|
|
407
56
|
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
=== Enhanced Error Messages (✅ v1.0.0)
|
|
57
|
+
If Taurus can't find the library at startup, every `parse` call raises
|
|
58
|
+
`LoadError`.
|
|
411
59
|
|
|
412
|
-
|
|
60
|
+
== Parsing
|
|
413
61
|
|
|
414
|
-
|
|
415
|
-
* ✅ **Precise location tracking** - Line, column, and byte offset for all errors
|
|
416
|
-
* ✅ **Categorized error codes** - Parse, XPath, evaluation, and generic errors
|
|
417
|
-
* ✅ **Rich error objects** - Full error attributes accessible in Ruby
|
|
418
|
-
* ✅ **Zero-overhead design** - Thread-local error state with minimal impact
|
|
419
|
-
|
|
420
|
-
**Example Error Output**:
|
|
62
|
+
The top-level entry point is `Taurus::XML`. Parse a string or an IO:
|
|
421
63
|
|
|
422
64
|
[source,ruby]
|
|
423
65
|
----
|
|
424
|
-
|
|
425
|
-
Taurus.parse("<>")
|
|
426
|
-
# => Taurus::ParseError: Failed to parse root element at line 1, column 1
|
|
427
|
-
# code: :parse_failed
|
|
428
|
-
# line: 1, column: 1, byte_offset: 0
|
|
429
|
-
#
|
|
430
|
-
# Context:
|
|
431
|
-
# <>
|
|
432
|
-
# ^
|
|
433
|
-
|
|
434
|
-
# XPath error with helpful message
|
|
435
|
-
doc.xpath("//unknown()")
|
|
436
|
-
# => Taurus::XPathError: Unknown function 'unknown' at line 1, column 3
|
|
437
|
-
# code: :xpath_function
|
|
438
|
-
# Suggestion: Did you mean count(), concat(), or contains()?
|
|
439
|
-
----
|
|
440
|
-
|
|
441
|
-
**Error Attributes**:
|
|
442
|
-
|
|
443
|
-
All error exceptions provide full diagnostic information:
|
|
444
|
-
|
|
445
|
-
[source,ruby]
|
|
446
|
-
----
|
|
447
|
-
begin
|
|
448
|
-
Taurus.parse(invalid_xml)
|
|
449
|
-
rescue Taurus::ParseError => e
|
|
450
|
-
puts e.message # Human-readable message
|
|
451
|
-
puts e.code # Symbol error code (:parse_failed, :unclosed_tag, etc.)
|
|
452
|
-
puts e.line # Line number (1-based)
|
|
453
|
-
puts e.column # Column number (1-based)
|
|
454
|
-
puts e.byte_offset # Byte offset in input
|
|
455
|
-
puts e.context # Code snippet showing error location
|
|
456
|
-
end
|
|
457
|
-
----
|
|
458
|
-
|
|
459
|
-
=== XML Parsing (✅ Complete)
|
|
460
|
-
|
|
461
|
-
* ✅ Complete XML 1.0 specification support
|
|
462
|
-
* ✅ Elements, attributes, text, CDATA, comments, processing instructions
|
|
463
|
-
* ✅ Self-closing elements
|
|
464
|
-
* ✅ Robust error handling with Ruby exceptions
|
|
465
|
-
* ✅ Zero-copy parsing techniques
|
|
466
|
-
* ✅ SIMD-optimized hot paths
|
|
467
|
-
|
|
468
|
-
=== XML Namespaces 1.0 (✅ Complete)
|
|
469
|
-
|
|
470
|
-
* ✅ Namespace declaration parsing (`xmlns`, `xmlns:prefix`)
|
|
471
|
-
* ✅ Namespace inheritance with proper scoping
|
|
472
|
-
* ✅ Prefix-to-URI resolution with parent chain traversal
|
|
473
|
-
* ✅ Default namespace handling (nil prefix)
|
|
474
|
-
* ✅ Namespace override in child elements
|
|
475
|
-
|
|
476
|
-
**Rich Namespace API**:
|
|
477
|
-
|
|
478
|
-
* `Element#namespace` - Active namespace for element
|
|
479
|
-
* `Element#namespaces` - Local namespace declarations
|
|
480
|
-
* `Element#namespace_for_prefix(prefix)` - Resolve with inheritance
|
|
481
|
-
* `Element#all_namespaces` - All namespaces including inherited
|
|
482
|
-
|
|
483
|
-
=== XPath 1.0 Engine (✅ Complete - All 27 Functions!)
|
|
484
|
-
|
|
485
|
-
All features implemented in C for maximum performance, with intelligent AST caching.
|
|
486
|
-
|
|
487
|
-
**Performance**: **2.3× slower than Nokogiri** for XPath queries (competitive for v0.1.0 ✅)
|
|
488
|
-
|
|
489
|
-
* Complete XPath 1.0 specification (27/27 functions, 13/13 axes)
|
|
490
|
-
* AST caching eliminates re-parsing overhead
|
|
491
|
-
* O(1) cache lookup with hash table (64 buckets, 256 entries max)
|
|
492
|
-
* ~154KB memory for full cache
|
|
493
|
-
* All 250 XPath tests passing (100%)
|
|
494
|
-
* Zero external dependencies (Nokogiri requires libxml2)
|
|
495
|
-
|
|
496
|
-
==== XPath Axes (13/13) ✅
|
|
497
|
-
|
|
498
|
-
All XPath 1.0 axes fully implemented and tested:
|
|
499
|
-
|
|
500
|
-
* `child` - Direct element children (default)
|
|
501
|
-
* `descendant` - All descendants
|
|
502
|
-
* `descendant-or-self` - Self and descendants (`//`)
|
|
503
|
-
* `parent` - Parent element (`..`)
|
|
504
|
-
* `ancestor` - All ancestors
|
|
505
|
-
* `ancestor-or-self` - Self and ancestors
|
|
506
|
-
* `self` - Context node (`.`)
|
|
507
|
-
* `following-sibling` - Siblings after context
|
|
508
|
-
* `preceding-sibling` - Siblings before context
|
|
509
|
-
* `following` - All following nodes in document order
|
|
510
|
-
* `preceding` - All preceding nodes in document order
|
|
511
|
-
* `attribute` - Element attributes (`@`)
|
|
512
|
-
* `namespace` - Namespace nodes
|
|
513
|
-
|
|
514
|
-
==== XPath Functions (27/27) ✅
|
|
515
|
-
|
|
516
|
-
**String Functions (10/10)**:
|
|
517
|
-
|
|
518
|
-
* `string(object?)` - Convert to string
|
|
519
|
-
* `concat(string, string, ...)` - Concatenate strings
|
|
520
|
-
* `starts-with(string, string)` - Prefix test
|
|
521
|
-
* `contains(string, string)` - Substring test
|
|
522
|
-
* `substring(string, number, number?)` - Extract substring
|
|
523
|
-
* `string-length(string?)` - String length
|
|
524
|
-
* `normalize-space(string?)` - Normalize whitespace
|
|
525
|
-
* `translate(string, string, string)` - Character translation
|
|
526
|
-
* `substring-before(string, string)` - Before delimiter
|
|
527
|
-
* `substring-after(string, string)` - After delimiter
|
|
528
|
-
|
|
529
|
-
**Boolean Functions (5/5)**:
|
|
530
|
-
|
|
531
|
-
* `boolean(object)` - Convert to boolean
|
|
532
|
-
* `not(boolean)` - Logical NOT
|
|
533
|
-
* `true()` - Boolean true
|
|
534
|
-
* `false()` - Boolean false
|
|
535
|
-
* `lang(string)` - Language matching
|
|
536
|
-
|
|
537
|
-
**Number Functions (5/5)**:
|
|
538
|
-
|
|
539
|
-
* `number(object?)` - Convert to number
|
|
540
|
-
* `sum(node-set)` - Sum node values
|
|
541
|
-
* `floor(number)` - Round down
|
|
542
|
-
* `ceiling(number)` - Round up
|
|
543
|
-
* `round(number)` - Round to nearest
|
|
544
|
-
|
|
545
|
-
**Node-set Functions (7/7)**:
|
|
546
|
-
|
|
547
|
-
* `count(node-set)` - Count nodes
|
|
548
|
-
* `id(object)` - Select by ID
|
|
549
|
-
* `last()` - Context size
|
|
550
|
-
* `position()` - Context position
|
|
551
|
-
* `local-name(node-set?)` - Local name
|
|
552
|
-
* `namespace-uri(node-set?)` - Namespace URI
|
|
553
|
-
* `name(node-set?)` - Qualified name
|
|
554
|
-
|
|
555
|
-
==== XPath Operators (15/15) ✅
|
|
556
|
-
|
|
557
|
-
* **Logical**: `or`, `and`
|
|
558
|
-
* **Equality**: `=`, `!=`
|
|
559
|
-
* **Relational**: `<`, `<=`, `>`, `>=`
|
|
560
|
-
* **Arithmetic**: `+`, `-`, `*`, `div`, `mod`
|
|
561
|
-
* **Union**: `|`
|
|
562
|
-
* **Predicate**: `[]`
|
|
563
|
-
|
|
564
|
-
==== XPath Predicates (3/3) ✅
|
|
565
|
-
|
|
566
|
-
* **Position predicates**: `[1]`, `[N]`, `[last()]`
|
|
567
|
-
* **Boolean predicates**: `[@attr]`, `[element]`, `[expression]`
|
|
568
|
-
* **Comparison predicates**: `[@price > 20]`, `[@stock >= 5]` ✅ **NEW in v0.3.1**
|
|
569
|
-
|
|
570
|
-
==== XPath 1.0 Specification Compliance
|
|
571
|
-
|
|
572
|
-
Taurus implements the complete https://www.w3.org/TR/1999/REC-xpath-19991116/[XPath 1.0 W3C Recommendation] with **100% compliance** (250/250 tests passing):
|
|
573
|
-
|
|
574
|
-
* ✅ **All 13 XPath axes** - Full spec compliance with document order maintained
|
|
575
|
-
* ✅ **All 27 XPath functions** - Complete string, boolean, number, and node-set functions
|
|
576
|
-
* ✅ **All 15 operators** - Logical, comparison, arithmetic, and union operators
|
|
577
|
-
* ✅ **Complete predicate support** - Position and boolean predicates with proper sequencing
|
|
578
|
-
* ✅ **Full namespace support** - `namespace-uri()`, `local-name()`, `name()` functions working
|
|
579
|
-
* ✅ **Comprehensive testing** - 250/250 XPath tests passing (100%)
|
|
580
|
-
|
|
581
|
-
**What's implemented**:
|
|
582
|
-
|
|
583
|
-
* All node tests: name tests, wildcards, `text()`, `comment()`, `node()`, `processing-instruction()`
|
|
584
|
-
* All abbreviated syntax: `@attr`, `.`, `..`, `//`, `[N]`
|
|
585
|
-
* Complete type conversion per spec (boolean, number, string, node-set)
|
|
586
|
-
* Proper operator precedence and short-circuit evaluation
|
|
587
|
-
* Document order maintenance across all axes
|
|
588
|
-
* UTF-8 character handling in string functions
|
|
589
|
-
* Complete namespace support in parser and XPath functions
|
|
590
|
-
* ✅ **NEW in v0.6.1**: Absolute path element matching (`/root`, `/root/child`)
|
|
591
|
-
* ✅ **NEW in v1.1.0**: Axis syntax with operator keywords (`ancestor::div`, `child::mod`)
|
|
592
|
-
* ✅ **NEW in v1.1.0**: UTF-8 encoding and substring edge cases
|
|
593
|
-
|
|
594
|
-
**Known Edge Case** (1 test, 0.4% - deferred to v0.7.0):
|
|
595
|
-
|
|
596
|
-
1. **Complex predicates with absolute descendant-or-self** - `//*[function()]` patterns may fail
|
|
597
|
-
* *Example*: `count(//*[local-name() = "item"])` raises error
|
|
598
|
-
* *Workaround*: Use relative path `count(.//*[local-name() = "item"])`
|
|
599
|
-
* *Workaround*: Or use `count(//item)` without predicate
|
|
600
|
-
* Cause: Pre-existing issue with function calls in `//*[...]` predicates
|
|
601
|
-
|
|
602
|
-
This limitation doesn't affect core functionality. Basic XPath queries with `//element` work perfectly, and relative path predicates work correctly.
|
|
603
|
-
|
|
604
|
-
**Planned for v0.7.0+**:
|
|
605
|
-
|
|
606
|
-
* Fix `//*[function()]` predicate evaluation
|
|
607
|
-
* Namespace prefixes in XPath queries (`//ns:book`)
|
|
608
|
-
* XPath 2.0/3.0 features (long-term)
|
|
609
|
-
|
|
610
|
-
==== Edge Cases
|
|
611
|
-
|
|
612
|
-
The implementation correctly handles all XPath 1.0 edge cases (fixed in v1.1.0):
|
|
613
|
-
|
|
614
|
-
* **Negative positions**: `substring("12345", -1, 4)` returns "12" per spec
|
|
615
|
-
* **UTF-8 strings**: Proper character (not byte) counting with correct encoding
|
|
616
|
-
* **Empty delimiters**: `substring-before(str, '')` returns empty string
|
|
617
|
-
* **Operator keywords as names**: Support for `ancestor::div`, `child::mod` etc.
|
|
618
|
-
|
|
619
|
-
For complete compliance details including test coverage by feature, see link:docs/XPATH_SPEC_COMPLIANCE.md[XPath 1.0 Spec Compliance Matrix].
|
|
620
|
-
|
|
621
|
-
=== Performance Features
|
|
622
|
-
|
|
623
|
-
* **AST Caching** (Session 67) - Parse XPath expressions once, use forever
|
|
624
|
-
* **SIMD Optimizations** (Session 48) - ARM NEON & x86 SSE2 vectorization
|
|
625
|
-
* **Character Tables** (Session 58) - Zero-branch character classification
|
|
626
|
-
* **Zero-Copy Parsing** - Minimal memory allocations
|
|
627
|
-
* **Memory Efficient** - ~154KB max for XPath cache, zero leaks
|
|
628
|
-
|
|
629
|
-
=== Command-Line Interface (✅ Complete)
|
|
630
|
-
|
|
631
|
-
Taurus includes a production-ready CLI for XML processing directly from the terminal.
|
|
632
|
-
|
|
633
|
-
**Available Commands**:
|
|
634
|
-
|
|
635
|
-
* `taurus parse FILE` - Parse and validate XML documents
|
|
636
|
-
* `taurus xpath FILE EXPRESSION` - Execute XPath queries
|
|
637
|
-
* `taurus format FILE` - Pretty-print XML
|
|
638
|
-
* `taurus version` - Show version information
|
|
639
|
-
|
|
640
|
-
**Key Features**:
|
|
641
|
-
|
|
642
|
-
* Full XPath 1.0 support from command line
|
|
643
|
-
* Multiple output formats: `xml` (default), `json`, `text`
|
|
644
|
-
* Attribute support in all output formats (✅ v0.5.0)
|
|
645
|
-
* Pretty-printing with customizable indentation
|
|
646
|
-
* Compact mode to remove whitespace
|
|
647
|
-
* Stdin/stdout support for pipelines
|
|
648
|
-
* Quiet and verbose modes
|
|
649
|
-
* Compatible with xmllint exit codes
|
|
650
|
-
|
|
651
|
-
See <<CLI Usage>> section for detailed examples.
|
|
652
|
-
|
|
653
|
-
=== Ox API Compatibility (✅ Complete)
|
|
654
|
-
|
|
655
|
-
* `Element#name`, `#attributes`, `#nodes`
|
|
656
|
-
* `Element#<<`, `#text`, `#replace_text`
|
|
657
|
-
* `Element#[]`, `#[]=` - Dual string/symbol attribute access
|
|
658
|
-
* `Document#root`, `#root=`
|
|
659
|
-
* Parent-child relationships
|
|
660
|
-
* Node addition/removal
|
|
661
|
-
|
|
662
|
-
== Quick Start
|
|
663
|
-
|
|
664
|
-
=== Command-Line Usage
|
|
665
|
-
|
|
666
|
-
==== Parse & Validate
|
|
667
|
-
|
|
668
|
-
Parse and validate XML documents with optional format conversion:
|
|
669
|
-
|
|
670
|
-
[source,shell]
|
|
671
|
-
----
|
|
672
|
-
# Basic parsing (XML output)
|
|
673
|
-
taurus parse document.xml
|
|
674
|
-
|
|
675
|
-
# JSON output with attributes
|
|
676
|
-
taurus parse --format json document.xml
|
|
677
|
-
|
|
678
|
-
# Human-readable tree format
|
|
679
|
-
taurus parse --format text document.xml
|
|
680
|
-
|
|
681
|
-
# Validate without output
|
|
682
|
-
taurus parse --noout document.xml
|
|
683
|
-
|
|
684
|
-
# From stdin
|
|
685
|
-
cat document.xml | taurus parse -
|
|
686
|
-
----
|
|
687
|
-
|
|
688
|
-
[example]
|
|
689
|
-
====
|
|
690
|
-
Given `books.xml`:
|
|
691
|
-
[source,xml]
|
|
692
|
-
----
|
|
693
|
-
<library>
|
|
694
|
-
<book id="1">
|
|
695
|
-
<title>Ruby Guide</title>
|
|
696
|
-
</book>
|
|
697
|
-
</library>
|
|
698
|
-
----
|
|
699
|
-
|
|
700
|
-
JSON output with attributes:
|
|
701
|
-
[source,shell]
|
|
702
|
-
----
|
|
703
|
-
$ taurus parse --format json books.xml
|
|
704
|
-
{"name":"library","children":[{"name":"book","attributes":{"id":"1"},"children":[{"name":"title","text":"Ruby Guide"}]}]}
|
|
705
|
-
----
|
|
706
|
-
|
|
707
|
-
Text tree output with attributes:
|
|
708
|
-
[source,shell]
|
|
709
|
-
----
|
|
710
|
-
$ taurus parse --format text books.xml
|
|
711
|
-
library
|
|
712
|
-
book {id="1"}
|
|
713
|
-
title: Ruby Guide
|
|
714
|
-
----
|
|
715
|
-
====
|
|
716
|
-
|
|
717
|
-
==== XPath Queries
|
|
718
|
-
|
|
719
|
-
Execute XPath queries from the command line:
|
|
720
|
-
|
|
721
|
-
[source,shell]
|
|
722
|
-
----
|
|
723
|
-
# Basic XPath query
|
|
724
|
-
taurus xpath books.xml "//book"
|
|
725
|
-
|
|
726
|
-
# From stdin
|
|
727
|
-
cat books.xml | taurus xpath - "//title"
|
|
728
|
-
|
|
729
|
-
# Count results
|
|
730
|
-
taurus xpath --count books.xml "//book"
|
|
731
|
-
|
|
732
|
-
# Boolean results
|
|
733
|
-
taurus xpath --boolean books.xml "//book[@price > 20]"
|
|
734
|
-
|
|
735
|
-
# With verbose output
|
|
736
|
-
taurus xpath --verbose books.xml "//book"
|
|
737
|
-
----
|
|
66
|
+
require "taurus"
|
|
738
67
|
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
# Custom indentation (4 spaces)
|
|
749
|
-
taurus format --indent 4 books.xml
|
|
750
|
-
|
|
751
|
-
# Save to file
|
|
752
|
-
taurus format --output formatted.xml books.xml
|
|
753
|
-
|
|
754
|
-
# Compact mode (remove whitespace)
|
|
755
|
-
taurus format --compact books.xml
|
|
756
|
-
|
|
757
|
-
# From stdin
|
|
758
|
-
cat books.xml | taurus format -
|
|
759
|
-
----
|
|
760
|
-
|
|
761
|
-
==== Pipeline Examples
|
|
762
|
-
|
|
763
|
-
Combine with standard Unix tools:
|
|
764
|
-
|
|
765
|
-
[source,shell]
|
|
766
|
-
----
|
|
767
|
-
# Count books
|
|
768
|
-
taurus xpath books.xml "//book" | wc -l
|
|
769
|
-
|
|
770
|
-
# Extract and format
|
|
771
|
-
curl https://example.org/feed.xml | taurus xpath - "//entry" | taurus format -
|
|
772
|
-
|
|
773
|
-
# Filter and count
|
|
774
|
-
taurus xpath catalog.xml "//item[@available='true']" --count
|
|
775
|
-
----
|
|
776
|
-
|
|
777
|
-
=== Library Usage
|
|
778
|
-
|
|
779
|
-
==== Basic Parsing
|
|
780
|
-
|
|
781
|
-
[source,ruby]
|
|
782
|
-
----
|
|
783
|
-
require 'taurus'
|
|
784
|
-
|
|
785
|
-
# Parse XML document
|
|
786
|
-
xml = '<root xmlns="http://example.org"><item id="1">content</item></root>'
|
|
787
|
-
doc = Taurus.parse(xml)
|
|
788
|
-
|
|
789
|
-
# Access elements
|
|
790
|
-
root = doc.root
|
|
791
|
-
puts root.name # => "root"
|
|
792
|
-
puts root.namespace # => "http://example.org"
|
|
793
|
-
|
|
794
|
-
# Access children
|
|
795
|
-
item = root.nodes.first
|
|
796
|
-
puts item.name # => "item"
|
|
797
|
-
puts item[:id] # => "1" (symbol or string keys)
|
|
798
|
-
puts item.text # => "content"
|
|
799
|
-
----
|
|
800
|
-
|
|
801
|
-
=== Working with Namespaces
|
|
802
|
-
|
|
803
|
-
[source,ruby]
|
|
804
|
-
----
|
|
805
|
-
xml = <<~XML
|
|
806
|
-
<root xmlns="http://default.org"
|
|
807
|
-
xmlns:ex="http://example.org">
|
|
808
|
-
<item>default namespace</item>
|
|
809
|
-
<ex:item>example namespace</ex:item>
|
|
810
|
-
</root>
|
|
811
|
-
XML
|
|
812
|
-
|
|
813
|
-
doc = Taurus.parse(xml)
|
|
814
|
-
|
|
815
|
-
# Access namespace declarations
|
|
816
|
-
doc.root.namespaces.each do |ns|
|
|
817
|
-
puts "#{ns[:prefix] || 'default'}: #{ns[:href]}"
|
|
818
|
-
end
|
|
819
|
-
|
|
820
|
-
# Resolve with inheritance
|
|
821
|
-
child = doc.root.nodes.first
|
|
822
|
-
puts child.namespace # => "http://default.org" (inherited)
|
|
823
|
-
|
|
824
|
-
# XPath with namespace functions (NEW in v0.6.0)
|
|
825
|
-
uri = doc.xpath('namespace-uri(//item)')
|
|
826
|
-
# => "http://default.org"
|
|
827
|
-
|
|
828
|
-
local = doc.xpath('local-name(//ex:item)')
|
|
829
|
-
# => "item"
|
|
830
|
-
|
|
831
|
-
qualified = doc.xpath('name(//ex:item)')
|
|
832
|
-
# => "ex:item"
|
|
833
|
-
----
|
|
834
|
-
|
|
835
|
-
=== Custom Namespace Support (NEW in v0.9.0)
|
|
836
|
-
|
|
837
|
-
==== Automatic Namespace Detection
|
|
838
|
-
|
|
839
|
-
Taurus automatically detects namespace declarations from your XML documents:
|
|
840
|
-
|
|
841
|
-
[source,ruby]
|
|
842
|
-
----
|
|
843
|
-
xml = <<~XML
|
|
844
|
-
<library xmlns:book="http://books.org">
|
|
845
|
-
<book:title>Ruby Guide</book:title>
|
|
68
|
+
doc = Taurus::XML.parse(<<~XML)
|
|
69
|
+
<library xmlns="http://example.org/ns">
|
|
70
|
+
<book id="b1" lang="en">
|
|
71
|
+
<title>Refactoring</title>
|
|
72
|
+
<author>Martin Fowler</author>
|
|
73
|
+
</book>
|
|
74
|
+
<book id="b2" lang="fr">
|
|
75
|
+
<title>Programmer en Ruby</title>
|
|
76
|
+
</book>
|
|
846
77
|
</library>
|
|
847
78
|
XML
|
|
848
79
|
|
|
849
|
-
doc
|
|
850
|
-
doc.
|
|
80
|
+
doc.root.name # => "library"
|
|
81
|
+
doc.root.children.size # => 5 (2 element children + 3 whitespace text nodes)
|
|
851
82
|
----
|
|
852
83
|
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
For explicit control over namespace mappings, use the `namespaces:` parameter:
|
|
84
|
+
Or a file:
|
|
856
85
|
|
|
857
86
|
[source,ruby]
|
|
858
87
|
----
|
|
859
|
-
|
|
860
|
-
doc.xpath('//ns:book', namespaces: { 'ns' => 'http://books.org' })
|
|
861
|
-
|
|
862
|
-
# Works on elements too
|
|
863
|
-
elem.xpath('.//ns:title', namespaces: { 'ns' => 'http://example.org' })
|
|
88
|
+
doc = Taurus::XML.parse_file("books.xml")
|
|
864
89
|
----
|
|
865
90
|
|
|
866
|
-
|
|
91
|
+
This is the direct Nokogiri equivalent of `Nokogiri::XML(...)`. The
|
|
92
|
+
returned object is a `Taurus::XML::Document`.
|
|
867
93
|
|
|
868
|
-
|
|
94
|
+
== Reading nodes
|
|
869
95
|
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
96
|
+
[horizontal]
|
|
97
|
+
`Document#root` :: root `Element`, or `nil` for an empty document.
|
|
98
|
+
`Node#name` :: element name (e.g. `"book"`).
|
|
99
|
+
`Node#content` (alias `#text`, `#inner_text`) :: all descendant text concatenated.
|
|
100
|
+
`Node#[]` (alias `#attr`, `#get_attribute`) :: attribute value by name.
|
|
101
|
+
`Node#attributes` :: hash of `{name => Attr}`.
|
|
102
|
+
`Node#key?` (alias `#has_attribute?`) :: attribute presence.
|
|
103
|
+
`Node#children` :: `NodeSet` of all children (elements, text, comments, …).
|
|
104
|
+
`Node#element_children` :: `NodeSet` of element children only.
|
|
105
|
+
`Node#first_element_child`, `#last_element_child` :: first/last element child (skip text nodes).
|
|
106
|
+
`Node#next_element`, `#previous_element` :: next/prev sibling element.
|
|
107
|
+
`Node#parent`, `#next_sibling`, `#previous_sibling` :: tree navigation.
|
|
108
|
+
`Node#line` :: 1-based source line number.
|
|
109
|
+
`Node#type` (alias `#node_type`) :: integer type code. Element predicates: `#element?`, `#text?`, `#comment?`, `#cdata?`, `#processing_instruction?`.
|
|
110
|
+
|
|
111
|
+
Example — walk all book titles:
|
|
873
112
|
|
|
874
113
|
[source,ruby]
|
|
875
114
|
----
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
</root>
|
|
883
|
-
XML
|
|
884
|
-
|
|
885
|
-
doc = Taurus.parse(xml)
|
|
886
|
-
|
|
887
|
-
# Direct namespace prefix support
|
|
888
|
-
book_titles = doc.xpath('//book:title')
|
|
889
|
-
# => [<book:title>XPath Guide</book:title>]
|
|
890
|
-
|
|
891
|
-
# Wildcard with namespace prefix
|
|
892
|
-
all_books = doc.xpath('//book:*')
|
|
893
|
-
# => [<book:title>..., <book:isbn>...]
|
|
894
|
-
|
|
895
|
-
# Multiple namespaces
|
|
896
|
-
authors = doc.xpath('//author:name')
|
|
897
|
-
# => [<author:name>John Doe</author:name>]
|
|
115
|
+
doc.root.children.select(&:element?).each do |book|
|
|
116
|
+
title = book.children.find { |c| c.element? && c.name == "title" }
|
|
117
|
+
puts "#{book[:id]}: #{title&.content}"
|
|
118
|
+
end
|
|
119
|
+
# b1: Refactoring
|
|
120
|
+
# b2: Programmer en Ruby
|
|
898
121
|
----
|
|
899
122
|
|
|
900
|
-
|
|
123
|
+
=== Tree iteration
|
|
901
124
|
|
|
902
|
-
|
|
125
|
+
`Node#traverse` walks the subtree in document order via a single C-side
|
|
126
|
+
callback (one FFI call for the whole traversal, not one per node):
|
|
903
127
|
|
|
904
128
|
[source,ruby]
|
|
905
129
|
----
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
doc = Taurus.parse(xml)
|
|
914
|
-
|
|
915
|
-
# Namespace 'product' automatically registered
|
|
916
|
-
items = doc.xpath('//product:item')
|
|
917
|
-
# => Returns both items
|
|
918
|
-
|
|
919
|
-
# Works in predicates
|
|
920
|
-
first = doc.xpath('//product:item[1]')
|
|
921
|
-
# => Returns first item
|
|
922
|
-
----
|
|
923
|
-
|
|
924
|
-
==== Namespace Prefixes in Complex Queries
|
|
925
|
-
|
|
926
|
-
[source,ruby]
|
|
130
|
+
doc.root.traverse do |node|
|
|
131
|
+
case node
|
|
132
|
+
when Taurus::XML::Element then puts "E #{node.name}"
|
|
133
|
+
when Taurus::XML::Text then puts "T #{node.content.inspect}"
|
|
134
|
+
when Taurus::XML::Comment then puts "C #{node.content.inspect}"
|
|
135
|
+
end
|
|
136
|
+
end
|
|
927
137
|
----
|
|
928
|
-
xml = <<~XML
|
|
929
|
-
<catalog xmlns:book="http://books.org">
|
|
930
|
-
<book:publication year="2020">
|
|
931
|
-
<book:title>Learning XPath</book:title>
|
|
932
|
-
<book:author>Jane Smith</book:author>
|
|
933
|
-
</book:publication>
|
|
934
|
-
<book:publication year="2022">
|
|
935
|
-
<book:title>Advanced XPath</book:title>
|
|
936
|
-
</book:publication>
|
|
937
|
-
</catalog>
|
|
938
|
-
XML
|
|
939
|
-
|
|
940
|
-
doc = Taurus.parse(xml)
|
|
941
138
|
|
|
942
|
-
|
|
943
|
-
pub_2020 = doc.xpath('//book:publication[@year="2020"]')
|
|
944
|
-
# => Returns first publication
|
|
139
|
+
== Searching: XPath and CSS
|
|
945
140
|
|
|
946
|
-
|
|
947
|
-
all_titles = doc.xpath('//book:publication/book:title')
|
|
948
|
-
# => Returns both titles
|
|
141
|
+
`Document` and `Element` (via `Taurus::XML::Searchable`) support:
|
|
949
142
|
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
Namespace declarations on any element are automatically discovered:
|
|
143
|
+
[horizontal]
|
|
144
|
+
`#xpath(*exprs)` :: evaluate XPath; returns `NodeSet`, `true`/`false`, `Float`, or `String` depending on the expression.
|
|
145
|
+
`#at_xpath(*exprs)` :: first match (or scalar), like `xpath(*exprs).first`.
|
|
146
|
+
`#css(*selectors)` :: minimal CSS-to-XPath translation, then `xpath`.
|
|
147
|
+
`#at_css(*selectors)` :: first match of `css`.
|
|
148
|
+
`#search(*exprs)` :: dispatches on syntax — `/`-prefixed or `,`-separated → `xpath`, otherwise `css`.
|
|
149
|
+
`#at(*exprs)` :: first match of `search`.
|
|
958
150
|
|
|
959
151
|
[source,ruby]
|
|
960
152
|
----
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
</outer:container>
|
|
967
|
-
</root>
|
|
968
|
-
XML
|
|
153
|
+
doc.xpath("//book") # => NodeSet of both <book>
|
|
154
|
+
doc.xpath("count(//book)") # => 2.0
|
|
155
|
+
doc.xpath("//book[@lang='fr']/title") # => NodeSet[<title>Programmer en Ruby</title>]
|
|
156
|
+
doc.at_xpath("//book[@id='b1']") # => <book id="b1" ...>
|
|
157
|
+
doc.at_xpath("string(//book[1]/@id)") # => "b1"
|
|
969
158
|
|
|
970
|
-
doc
|
|
971
|
-
|
|
972
|
-
#
|
|
973
|
-
inner = doc.xpath('//inner:item') # Finds inner:item
|
|
974
|
-
outer = doc.xpath('//outer:item') # Finds outer:item
|
|
159
|
+
doc.css("book[lang='en'] title") # => NodeSet[<title>Refactoring</title>]
|
|
160
|
+
doc.at_css("book#b1 title") # => <title>Refactoring</title> (id selector)
|
|
161
|
+
doc.css("book:first-child") # first <book>
|
|
975
162
|
----
|
|
976
163
|
|
|
977
|
-
|
|
164
|
+
XPath result type follows XPath 1.0 semantics:
|
|
165
|
+
`count(...)` → `Float`, `boolean(...)` → `true`/`false`,
|
|
166
|
+
`string(...)` → `String`, otherwise a `Taurus::XML::NodeSet`.
|
|
978
167
|
|
|
979
|
-
|
|
168
|
+
=== Supported CSS selectors
|
|
980
169
|
|
|
981
|
-
|
|
982
|
-
----
|
|
983
|
-
xml = <<~XML
|
|
984
|
-
<root xmlns:ns="http://example.org">
|
|
985
|
-
<ns:item>Namespaced</ns:item>
|
|
986
|
-
<item>Not namespaced</item>
|
|
987
|
-
</root>
|
|
988
|
-
XML
|
|
170
|
+
Minimal subset (translated to XPath via `Taurus::XML::CssToXPath`):
|
|
989
171
|
|
|
990
|
-
|
|
172
|
+
* Type/universal: `book`, `*`
|
|
173
|
+
* Class/ID: `.highlight`, `#b1`
|
|
174
|
+
* Attribute presence: `[lang]`
|
|
175
|
+
* Attribute value: `[lang='en']`, `[lang~='en']`, `[lang^='en']`, `[lang$='en']`, `[lang*='en']`
|
|
176
|
+
* Combinators: descendant (space), child (`>`), comma (multi-selector)
|
|
177
|
+
* Pseudo-classes: `:first-child`, `:last-child`, `:only-child`, `:empty`, `:root`, `:not(...)`
|
|
991
178
|
|
|
992
|
-
|
|
993
|
-
all_items = doc.xpath('//item')
|
|
994
|
-
# => Returns BOTH items (matches local name "item")
|
|
179
|
+
For anything more sophisticated, drop down to `xpath`.
|
|
995
180
|
|
|
996
|
-
|
|
997
|
-
ns_items = doc.xpath('//ns:item')
|
|
998
|
-
# => Returns only <ns:item>Namespaced</ns:item>
|
|
999
|
-
----
|
|
181
|
+
== Building and mutating
|
|
1000
182
|
|
|
1001
|
-
|
|
183
|
+
Documents expose factory methods; elements expose mutation methods:
|
|
1002
184
|
|
|
1003
185
|
[source,ruby]
|
|
1004
186
|
----
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
</book>
|
|
1011
|
-
<book id="2">
|
|
1012
|
-
<title>Rails Guide</title>
|
|
1013
|
-
<price>34.99</price>
|
|
1014
|
-
</book>
|
|
1015
|
-
</library>
|
|
1016
|
-
XML
|
|
187
|
+
doc = Taurus::XML.parse("<root/>")
|
|
188
|
+
book = doc.create_element("book")
|
|
189
|
+
book[:id] = "b3"
|
|
190
|
+
book.add_child(doc.create_element("title")).content = "New book"
|
|
191
|
+
doc.root.add_child(book)
|
|
1017
192
|
|
|
1018
|
-
doc
|
|
1019
|
-
|
|
1020
|
-
#
|
|
1021
|
-
books = doc.xpath('//book')
|
|
1022
|
-
puts books.size # => 2
|
|
1023
|
-
|
|
1024
|
-
# Find titles
|
|
1025
|
-
titles = doc.xpath('//book/title')
|
|
1026
|
-
titles.each { |t| puts t.text }
|
|
1027
|
-
# Output:
|
|
1028
|
-
# Ruby Programming
|
|
1029
|
-
# Rails Guide
|
|
1030
|
-
|
|
1031
|
-
# Use predicates
|
|
1032
|
-
first_book = doc.xpath('//book[1]') # Position
|
|
1033
|
-
books_with_id = doc.xpath('//book[@id]') # Boolean
|
|
1034
|
-
|
|
1035
|
-
# Use functions
|
|
1036
|
-
book_count = doc.xpath('count(//book)') # => 2.0
|
|
1037
|
-
all_titles = doc.xpath('string(//book/title)')
|
|
1038
|
-
|
|
1039
|
-
# Navigate with axes
|
|
1040
|
-
parent = doc.xpath('//title/parent::*').first # => <book>
|
|
1041
|
-
siblings = doc.xpath('//title/following-sibling::*')
|
|
193
|
+
puts doc.to_xml
|
|
194
|
+
# <?xml version="1.0"?>
|
|
195
|
+
# <root><book id="b3"><title>New book</title></book></root>
|
|
1042
196
|
----
|
|
1043
197
|
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
198
|
+
[horizontal]
|
|
199
|
+
`Document#create_element(name)` :: detached element owned by the document.
|
|
200
|
+
`Document#create_text_node(str)`, `#create_comment(str)`, `#create_cdata(str)` :: text-class factories.
|
|
201
|
+
`Document#create_processing_instruction(target, data)` :: PI factory.
|
|
202
|
+
`Document#fragment(markup)` :: parse a markup fragment (multiple top-level children allowed).
|
|
203
|
+
`Element#name=`, `#content=` :: rename / replace inner text.
|
|
204
|
+
`Element#[]=` (alias `#set_attribute`) :: add/update an attribute.
|
|
205
|
+
`Element#remove_attribute` (alias `#delete`) :: drop an attribute.
|
|
206
|
+
`Element#add_child(node_or_markup)` (alias `#<<`) :: append a Node, or parse+append a markup String.
|
|
207
|
+
`Element#prepend_child(node)` :: insert as the first child.
|
|
208
|
+
`Element#add_next_sibling(node)`, `#add_previous_sibling(node)` :: sibling insertion.
|
|
209
|
+
`Element#remove_child(node)` :: detach (does not free).
|
|
210
|
+
`Element#children=` :: replace all children.
|
|
211
|
+
`Element#replace(node)` / `#swap(node)` :: replace in parent.
|
|
212
|
+
`Element#wrap(node_or_markup)` :: wrap this element in a new one.
|
|
213
|
+
`Node#unlink` :: detach from the tree.
|
|
214
|
+
|
|
215
|
+
=== Building from scratch (no parse)
|
|
1049
216
|
|
|
1050
217
|
[source,ruby]
|
|
1051
218
|
----
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
XML
|
|
1058
|
-
|
|
1059
|
-
doc = Taurus.parse(xml)
|
|
1060
|
-
|
|
1061
|
-
# Select all id attributes
|
|
1062
|
-
ids = doc.xpath('//@id')
|
|
1063
|
-
# => ["1", "2"]
|
|
1064
|
-
|
|
1065
|
-
# Select specific attributes
|
|
1066
|
-
titles = doc.xpath('//book/@title')
|
|
1067
|
-
# => ["XPath Guide", "Ruby Guide"]
|
|
1068
|
-
|
|
1069
|
-
# Select all attributes of books
|
|
1070
|
-
all_attrs = doc.xpath('//book/@*')
|
|
1071
|
-
# => ["1", "XPath Guide", "2", "Ruby Guide"]
|
|
219
|
+
# Create an empty Document by parsing a sentinel and replacing the root,
|
|
220
|
+
# or build incrementally on a one-element seed.
|
|
221
|
+
doc = Taurus::XML.parse("<root/>")
|
|
222
|
+
doc.root.name = "catalog"
|
|
223
|
+
# ... then create_element / add_child as above.
|
|
1072
224
|
----
|
|
1073
225
|
|
|
1074
|
-
|
|
226
|
+
== Namespaces
|
|
1075
227
|
|
|
1076
|
-
|
|
228
|
+
[horizontal]
|
|
229
|
+
`Element#namespace` :: the element's in-scope namespace as a `Namespace` (or `nil`).
|
|
230
|
+
`Element#namespaces` :: all in-scope namespaces (inherited from ancestors) as a `{prefix_or_xmlns => href}` hash.
|
|
231
|
+
`Element#namespace_definitions` :: only namespaces declared directly on this element.
|
|
232
|
+
`Element#add_namespace_definition(prefix, href)` (alias `#add_namespace`) :: declare `xmlns:prefix="href"` on this element.
|
|
233
|
+
`Element#default_namespace=(href)` :: declare/replace `xmlns="href"`.
|
|
234
|
+
`Element#remove_namespace_definition(prefix)` :: drop a declaration.
|
|
1077
235
|
|
|
1078
236
|
[source,ruby]
|
|
1079
237
|
----
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
#
|
|
1084
|
-
doc.xpath('//book/attribute::id')
|
|
238
|
+
root = doc.root
|
|
239
|
+
root.add_namespace_definition("t", "https://example.org/types")
|
|
240
|
+
puts root.namespaces
|
|
241
|
+
# {"xmlns"=>"http://example.org/ns", "xmlns:t"=>"https://example.org/types"}
|
|
1085
242
|
|
|
1086
|
-
#
|
|
243
|
+
# XPath with prefixes is dispatched straight to libtaurus, which resolves
|
|
244
|
+
# prefixes using the in-scope namespace declarations.
|
|
245
|
+
doc.xpath("//t:title")
|
|
1087
246
|
----
|
|
1088
247
|
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
Use attributes to filter elements:
|
|
248
|
+
== Serialization and canonicalization
|
|
1092
249
|
|
|
1093
|
-
[
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
<book id="3">Free Book</book>
|
|
1100
|
-
</library>
|
|
1101
|
-
XML
|
|
1102
|
-
|
|
1103
|
-
doc = Taurus.parse(xml)
|
|
1104
|
-
|
|
1105
|
-
# Filter by attribute existence
|
|
1106
|
-
books_with_id = doc.xpath('//book[@id]')
|
|
1107
|
-
# => Returns first two books
|
|
1108
|
-
|
|
1109
|
-
# Filter by attribute value
|
|
1110
|
-
book_one = doc.xpath('//book[@id="1"]')
|
|
1111
|
-
# => Returns <book id="1"...>
|
|
1112
|
-
|
|
1113
|
-
# Comparison predicates (NEW in v0.5.2)
|
|
1114
|
-
expensive_books = doc.xpath('//book[@price > 30]')
|
|
1115
|
-
# => Returns <book id="2"...>
|
|
1116
|
-
----
|
|
1117
|
-
|
|
1118
|
-
==== Combining Attributes with Functions
|
|
250
|
+
[horizontal]
|
|
251
|
+
`Document#to_xml(indent: 0, no_decl: false, encoding: nil)` (aliases `#to_s`, `#serialize`) :: serialize the whole document.
|
|
252
|
+
`Element#to_xml(...)` :: serialize a subtree.
|
|
253
|
+
`Document#save(path, **opts)` :: serialize to a file.
|
|
254
|
+
`Document#canonicalize(version, inclusive_ns, with_comments:, exclusive:, mode:)` (alias `#c14n`) :: canonical XML.
|
|
255
|
+
`Element#canonicalize(...)` :: subtree canonicalization.
|
|
1119
256
|
|
|
1120
257
|
[source,ruby]
|
|
1121
258
|
----
|
|
1122
|
-
#
|
|
1123
|
-
|
|
1124
|
-
#
|
|
1125
|
-
|
|
1126
|
-
#
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
# Check if any book has price > 40
|
|
1131
|
-
has_expensive = doc.xpath('boolean(//book[@price > 40])')
|
|
1132
|
-
# => false
|
|
259
|
+
doc.to_xml # one-line, no indent
|
|
260
|
+
doc.to_xml(indent: 2) # pretty-printed
|
|
261
|
+
doc.canonicalize # C14N 1.0
|
|
262
|
+
doc.canonicalize(Taurus::XML::FFI::C14N_1_1) # C14N 1.1
|
|
263
|
+
doc.canonicalize(exclusive: true) # Exclusive C14N
|
|
264
|
+
doc.canonicalize(with_comments: true) # keep comments
|
|
265
|
+
doc.canonicalize(exclusive: true, inclusive_namespaces: ["ds"]) # InclusiveNamespaces
|
|
1133
266
|
----
|
|
1134
267
|
|
|
1135
|
-
==
|
|
1136
|
-
|
|
1137
|
-
Taurus provides detailed error messages with context to help diagnose issues quickly.
|
|
1138
|
-
|
|
1139
|
-
=== Error Types
|
|
1140
|
-
|
|
1141
|
-
==== ParseError
|
|
268
|
+
== SAX parsing
|
|
1142
269
|
|
|
1143
|
-
|
|
270
|
+
For very large documents, use the streaming SAX parser. Subclass
|
|
271
|
+
`Taurus::XML::SAX::Document` and override the events you care about:
|
|
1144
272
|
|
|
1145
273
|
[source,ruby]
|
|
1146
274
|
----
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
275
|
+
class Counter < Taurus::XML::SAX::Document
|
|
276
|
+
attr_reader :elements, :depth
|
|
277
|
+
def initialize
|
|
278
|
+
@elements = 0
|
|
279
|
+
@depth = 0
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
def start_element(name, attrs = [])
|
|
283
|
+
@elements += 1
|
|
284
|
+
@depth += 1
|
|
285
|
+
puts " " * (@depth - 1) + "<#{name}>"
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
def end_element(name)
|
|
289
|
+
@depth -= 1
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
def characters(str)
|
|
293
|
+
puts " " * @depth + "text: #{str.inspect}" unless str.strip.empty?
|
|
294
|
+
end
|
|
1156
295
|
end
|
|
1157
|
-
----
|
|
1158
|
-
|
|
1159
|
-
**Common Parse Errors**:
|
|
1160
|
-
|
|
1161
|
-
* `:null_input` - NULL input provided to parser
|
|
1162
|
-
* `:empty_input` - Empty string provided
|
|
1163
|
-
* `:parse_failed` - Malformed XML structure
|
|
1164
|
-
* `:unclosed_tag` - Missing closing tag
|
|
1165
296
|
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
Raised when XPath evaluation fails:
|
|
1169
|
-
|
|
1170
|
-
[source,ruby]
|
|
297
|
+
parser = Taurus::XML::SAX::Parser.new(Counter.new)
|
|
298
|
+
parser.parse(File.open("huge.xml")) # streams in 4 KB chunks
|
|
1171
299
|
----
|
|
1172
|
-
begin
|
|
1173
|
-
doc.xpath('//item[')
|
|
1174
|
-
rescue Taurus::XPathError => e
|
|
1175
|
-
puts e.message # => "Unexpected token in primary expression: EOF"
|
|
1176
|
-
puts e.code # => :xpath_syntax
|
|
1177
|
-
puts e.line # => 1
|
|
1178
|
-
puts e.column # => 8
|
|
1179
|
-
puts e.context # => "//item[\n ^"
|
|
1180
|
-
end
|
|
1181
|
-
----
|
|
1182
|
-
|
|
1183
|
-
**Common XPath Errors**:
|
|
1184
300
|
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
* `:xpath_evaluation` - Runtime evaluation error
|
|
301
|
+
`SAX::Parser#parse` accepts a `String`, an `IO`, or any object responding
|
|
302
|
+
to `#read`. The handler callbacks are:
|
|
1188
303
|
|
|
1189
|
-
|
|
304
|
+
[horizontal]
|
|
305
|
+
`start_document`, `end_document` :: document boundaries.
|
|
306
|
+
`xmldecl(version, encoding, standalone)` :: XML declaration.
|
|
307
|
+
`start_element(name, attrs)`, `end_element(name)` :: element events; `attrs` is an array of `[name, value]` pairs in source order.
|
|
308
|
+
`characters(str)`, `comment(str)`, `cdata_block(str)` :: text-class events.
|
|
309
|
+
`processing_instruction(name, content)` :: PI event.
|
|
310
|
+
`start_prefix_mapping(prefix, uri)`, `end_prefix_mapping(prefix)` :: namespace events.
|
|
311
|
+
`warning(str)`, `error(msg, line, col)` :: recoverable parser messages.
|
|
312
|
+
|
|
313
|
+
== Memory model
|
|
314
|
+
|
|
315
|
+
`Document` is the only object that owns C memory. Everything else
|
|
316
|
+
(`Element`, `Text`, `Attr`, `NodeSet`, …) is a *borrowed handle* that
|
|
317
|
+
is valid only while its Document is alive.
|
|
318
|
+
|
|
319
|
+
* Free a document explicitly with `Document#free`. After `#free`, any
|
|
320
|
+
further method call on the document or its nodes raises
|
|
321
|
+
`Taurus::XML::UseAfterFreeError`.
|
|
322
|
+
* If you don't call `#free`, GC will — a finalizer captures the raw
|
|
323
|
+
pointer address (not the Ruby wrapper) and calls
|
|
324
|
+
`taurus_document_free` exactly once.
|
|
325
|
+
* `NodeSet`s holding XPath results own their own
|
|
326
|
+
`TaurusXPathResult` and free it on GC.
|
|
327
|
+
* Don't hold a `Node` reference past the lifetime of its `Document`.
|
|
328
|
+
The C memory is gone; using the wrapper is undefined behaviour.
|
|
329
|
+
|
|
330
|
+
== Errors
|
|
331
|
+
|
|
332
|
+
All Taurus errors descend from `Taurus::XML::Error`:
|
|
1190
333
|
|
|
1191
|
-
|
|
334
|
+
[horizontal]
|
|
335
|
+
`ParseError` :: raised by `parse` / `parse_file` / SAX on malformed input.
|
|
336
|
+
`XPathError` :: raised by `xpath` on malformed or unsupported expressions.
|
|
337
|
+
`UseAfterFreeError` :: raised when calling methods on a freed `Document`.
|
|
338
|
+
`Error` :: generic (mutation precondition failures, etc.).
|
|
1192
339
|
|
|
1193
340
|
[source,ruby]
|
|
1194
341
|
----
|
|
1195
342
|
begin
|
|
1196
|
-
|
|
1197
|
-
rescue Taurus::
|
|
1198
|
-
|
|
1199
|
-
puts e.code # => :xpath_function
|
|
1200
|
-
# May include suggestion: "Did you mean count(), concat(), or contains()?"
|
|
343
|
+
Taurus::XML.parse("<unclosed>")
|
|
344
|
+
rescue Taurus::XML::ParseError => e
|
|
345
|
+
warn "parse failed: #{e.message}"
|
|
1201
346
|
end
|
|
1202
347
|
----
|
|
1203
348
|
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
All errors include context snippets showing the exact error location with a position marker (`^`):
|
|
1207
|
-
|
|
1208
|
-
[source,ruby]
|
|
1209
|
-
----
|
|
1210
|
-
# XPath syntax error
|
|
1211
|
-
doc.xpath('//book[@id = invalid]')
|
|
1212
|
-
# XPathError: Unexpected token in primary expression: NCNAME
|
|
1213
|
-
# Line: 1, Column: 14
|
|
1214
|
-
# Context:
|
|
1215
|
-
# //book[@id = invalid]
|
|
1216
|
-
# ^
|
|
1217
|
-
|
|
1218
|
-
# Parse error
|
|
1219
|
-
Taurus.parse('<root><item></root>')
|
|
1220
|
-
# ParseError: Mismatched closing tag at line 1, column 13
|
|
1221
|
-
# Context:
|
|
1222
|
-
# <root><item></root>
|
|
1223
|
-
# ^
|
|
1224
|
-
----
|
|
1225
|
-
|
|
1226
|
-
The position marker precisely indicates where the error occurred, making it easy to locate and fix issues.
|
|
1227
|
-
|
|
1228
|
-
=== Error Object Attributes
|
|
349
|
+
== Migrating from Nokogiri
|
|
1229
350
|
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
[horizontal]
|
|
1233
|
-
`message`:: Human-readable error description
|
|
1234
|
-
`code`:: Symbol error code (`:parse_failed`, `:xpath_syntax`, etc.)
|
|
1235
|
-
`line`:: Line number where error occurred (1-based)
|
|
1236
|
-
`column`:: Column number where error occurred (1-based)
|
|
1237
|
-
`byte_offset`:: Byte offset in the input string
|
|
1238
|
-
`context`:: Code snippet showing error location with `^` marker
|
|
1239
|
-
|
|
1240
|
-
=== Error Codes Reference
|
|
1241
|
-
|
|
1242
|
-
==== Parse Error Codes
|
|
1243
|
-
|
|
1244
|
-
[horizontal]
|
|
1245
|
-
`:null_input`:: NULL input provided to parser
|
|
1246
|
-
`:empty_input`:: Empty string provided to parser
|
|
1247
|
-
`:parse_failed`:: Generic parse failure (malformed XML)
|
|
1248
|
-
`:unclosed_tag`:: XML element not properly closed
|
|
1249
|
-
`:invalid_attribute`:: Invalid attribute syntax
|
|
1250
|
-
|
|
1251
|
-
==== XPath Error Codes
|
|
1252
|
-
|
|
1253
|
-
[horizontal]
|
|
1254
|
-
`:xpath_syntax`:: Invalid XPath expression syntax
|
|
1255
|
-
`:xpath_function`:: Unknown function name or invalid arguments
|
|
1256
|
-
`:xpath_evaluation`:: Runtime evaluation error
|
|
1257
|
-
`:xpath_type_error`:: Type conversion error
|
|
1258
|
-
`:xpath_divide_by_zero`:: Division by zero in arithmetic
|
|
1259
|
-
|
|
1260
|
-
=== Handling Errors Gracefully
|
|
351
|
+
For most read-only XPath use cases the swap is mechanical:
|
|
1261
352
|
|
|
1262
353
|
[source,ruby]
|
|
1263
354
|
----
|
|
1264
|
-
#
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
warn "XML parsing failed: #{e.message}"
|
|
1269
|
-
warn "Error code: #{e.code}"
|
|
1270
|
-
warn "Location: line #{e.line}, column #{e.column}"
|
|
1271
|
-
nil
|
|
1272
|
-
end
|
|
355
|
+
# Nokogiri
|
|
356
|
+
require "nokogiri"
|
|
357
|
+
doc = Nokogiri::XML(File.read("doc.xml"))
|
|
358
|
+
doc.xpath("//item[@id='1']").each { |n| puts n.text }
|
|
1273
359
|
|
|
1274
|
-
#
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
warn "XPath evaluation failed: #{e.message}"
|
|
1279
|
-
warn "Expression: #{expression}"
|
|
1280
|
-
warn "Error at: line #{e.line}, column #{e.column}"
|
|
1281
|
-
[]
|
|
1282
|
-
end
|
|
1283
|
-
|
|
1284
|
-
# Use with error handling
|
|
1285
|
-
doc = parse_safe(user_xml)
|
|
1286
|
-
if doc
|
|
1287
|
-
results = xpath_safe(doc, user_xpath)
|
|
1288
|
-
process_results(results) if results.any?
|
|
1289
|
-
end
|
|
360
|
+
# Taurus
|
|
361
|
+
require "taurus"
|
|
362
|
+
doc = Taurus::XML.parse(File.read("doc.xml"))
|
|
363
|
+
doc.xpath("//item[@id='1']").each { |n| puts n.content }
|
|
1290
364
|
----
|
|
1291
365
|
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
1. **Always handle errors** - Wrap parsing and XPath in begin/rescue blocks
|
|
1295
|
-
2. **Use error codes** - Check `e.code` for specific error types
|
|
1296
|
-
3. **Show context** - Display `e.context` to users for debugging
|
|
1297
|
-
4. **Log full details** - Log all error attributes for troubleshooting
|
|
1298
|
-
5. **Validate input** - Check XML and XPath expressions before processing
|
|
1299
|
-
|
|
1300
|
-
For a complete catalog of all error messages and solutions, see link:docs/ERROR_MESSAGES.md[Error Messages Catalog].
|
|
1301
|
-
|
|
1302
|
-
== Architecture
|
|
1303
|
-
|
|
1304
|
-
=== Modular Design (All files <700 lines)
|
|
1305
|
-
|
|
1306
|
-
**Core Parser**:
|
|
1307
|
-
|
|
1308
|
-
* `taurus.c` (93 lines) - Module initialization
|
|
1309
|
-
* `parse.c` (670 lines) - XML parser with SIMD
|
|
1310
|
-
* `namespace.c` (104 lines) - Namespace management
|
|
1311
|
-
* `element.c` (98 lines) - Element structures
|
|
1312
|
-
* `taurus.h` (103 lines) - Shared declarations
|
|
1313
|
-
|
|
1314
|
-
**XPath Engine** (Modularized in Session 15):
|
|
1315
|
-
|
|
1316
|
-
* `lexer_xpath.c` (538 lines) - Tokenization
|
|
1317
|
-
* `parser_xpath.c` (230 lines) - Parser core
|
|
1318
|
-
* `xpath_parser_expressions.c` (425 lines) - Expression parsing
|
|
1319
|
-
* `xpath_parser_paths.c` (265 lines) - Path parsing
|
|
1320
|
-
* `xpath_parser_node_tests.c` (80 lines) - Node tests
|
|
1321
|
-
* `evaluator_xpath.c` (419 lines) - Evaluator core
|
|
1322
|
-
* `xpath_axes.c` (411 lines) - All 13 axes
|
|
1323
|
-
* `xpath_operators.c` (312 lines) - All operators
|
|
1324
|
-
* `xpath_node_test.c` (99 lines) - Node matching
|
|
1325
|
-
* `xpath_predicates.c` (110 lines) - Predicates
|
|
1326
|
-
* `xpath_functions.c` (189 lines) - Function library
|
|
1327
|
-
* `xpath_ast_cache.c` (173 lines) - AST caching system
|
|
1328
|
-
|
|
1329
|
-
**Performance Optimizations**:
|
|
1330
|
-
|
|
1331
|
-
* `simd_helpers.h` - SIMD utilities (ARM NEON, SSE2, scalar)
|
|
1332
|
-
* `xpath_ast_cache.h` - AST caching API
|
|
1333
|
-
|
|
1334
|
-
**Ruby Layer**:
|
|
1335
|
-
|
|
1336
|
-
* `node.rb` - Base Node class
|
|
1337
|
-
* `element.rb` - Element with full API
|
|
1338
|
-
* `document.rb` - Document container
|
|
1339
|
-
* `node_set.rb` - XPath result sets
|
|
1340
|
-
* `attributes_hash.rb` - Dual-key access
|
|
1341
|
-
|
|
1342
|
-
=== Design Principles
|
|
366
|
+
Notable differences:
|
|
1343
367
|
|
|
1344
|
-
*
|
|
1345
|
-
*
|
|
1346
|
-
|
|
1347
|
-
*
|
|
1348
|
-
|
|
1349
|
-
*
|
|
368
|
+
* `Node#text` exists but the canonical name is `#content` (Nokogiri uses both).
|
|
369
|
+
* `Node#children` includes whitespace text nodes (same as Nokogiri); use
|
|
370
|
+
`#element_children` or `#first_element_child` to skip them.
|
|
371
|
+
* CSS support is intentionally minimal — for advanced selectors, drop to
|
|
372
|
+
`xpath`.
|
|
373
|
+
* No `Nokogiri::HTML` or `Nokogiri::CSS` parser. Taurus is XML-only.
|
|
374
|
+
* No XSLT, no RelaxNG / DTD validation, no schema caching.
|
|
375
|
+
* No built-in JRuby / TruffleRuby support — only CRuby via `ffi`.
|
|
1350
376
|
|
|
1351
|
-
==
|
|
1352
|
-
|
|
1353
|
-
**Overall**: 494/494 tests passing (100%)
|
|
1354
|
-
|
|
1355
|
-
[cols="3,2,2",options="header"]
|
|
1356
|
-
|===
|
|
1357
|
-
|Test Suite |Tests |Status
|
|
1358
|
-
|
|
1359
|
-
|XML Parser (Ruby)
|
|
1360
|
-
|86/86
|
|
1361
|
-
|✅ 100%
|
|
1362
|
-
|
|
1363
|
-
|Namespaces (Ruby)
|
|
1364
|
-
|28/28
|
|
1365
|
-
|✅ 100%
|
|
1366
|
-
|
|
1367
|
-
|XPath Lexer (Ruby)
|
|
1368
|
-
|21/21
|
|
1369
|
-
|✅ 100%
|
|
1370
|
-
|
|
1371
|
-
|XPath Parser (Ruby)
|
|
1372
|
-
|60/60
|
|
1373
|
-
|✅ 100%
|
|
1374
|
-
|
|
1375
|
-
|XPath Engine (Ruby)
|
|
1376
|
-
|250/250
|
|
1377
|
-
|✅ 100%
|
|
1378
|
-
|
|
1379
|
-
|C Parser Tests
|
|
1380
|
-
|25/25
|
|
1381
|
-
|✅ 100%
|
|
1382
|
-
|
|
1383
|
-
|C Evaluator Tests
|
|
1384
|
-
|57/57
|
|
1385
|
-
|✅ 100%
|
|
1386
|
-
|
|
1387
|
-
|Integration Tests
|
|
1388
|
-
|Comprehensive
|
|
1389
|
-
|✅ 100%
|
|
1390
|
-
|===
|
|
1391
|
-
|
|
1392
|
-
**Memory Safety**: Zero leaks verified with valgrind
|
|
1393
|
-
|
|
1394
|
-
== Known Limitations
|
|
1395
|
-
|
|
1396
|
-
None at this time. See link:docs/LIMITATIONS.md[Limitations] for a complete list.
|
|
1397
|
-
|
|
1398
|
-
== Future Enhancements
|
|
1399
|
-
|
|
1400
|
-
1. **XPath 2.0/3.0 features** - Only XPath 1.0 supported (long-term roadmap)
|
|
1401
|
-
2. **Custom namespace registration** - Currently auto-detected only (v0.9.0+)
|
|
1402
|
-
|
|
1403
|
-
== Development
|
|
377
|
+
== Performance
|
|
1404
378
|
|
|
1405
|
-
|
|
379
|
+
On the benchmark suite in `benchmark/taurus_vs_nokogiri.rb` (Ruby 3.3,
|
|
380
|
+
libtaurus v0.13+, macOS arm64), Taurus matches or beats Nokogiri on
|
|
381
|
+
parse, XPath, serialize, and full-tree traverse for the small and
|
|
382
|
+
medium documents that dominate real-world XML workloads. Run the
|
|
383
|
+
benchmark locally for numbers on your hardware:
|
|
1406
384
|
|
|
1407
385
|
[source,shell]
|
|
1408
386
|
----
|
|
1409
|
-
|
|
1410
|
-
git clone https://github.com/lutaml/taurus.git
|
|
1411
|
-
cd taurus
|
|
1412
|
-
|
|
1413
|
-
# Install dependencies
|
|
1414
|
-
bundle install
|
|
1415
|
-
|
|
1416
|
-
# Compile C extension
|
|
1417
|
-
bundle exec rake compile
|
|
1418
|
-
|
|
1419
|
-
# Run tests
|
|
1420
|
-
bundle exec rake spec # Ruby tests
|
|
1421
|
-
bundle exec rake test_c # C unit tests
|
|
1422
|
-
bundle exec rake test # All tests
|
|
387
|
+
bundle exec ruby benchmark/taurus_vs_nokogiri.rb
|
|
1423
388
|
----
|
|
1424
389
|
|
|
1425
|
-
|
|
390
|
+
== Development
|
|
1426
391
|
|
|
1427
392
|
[source,shell]
|
|
1428
393
|
----
|
|
1429
|
-
#
|
|
1430
|
-
bundle exec
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
ruby benchmark/compare_ox.rb
|
|
1434
|
-
|
|
1435
|
-
# XPath profiling
|
|
1436
|
-
ruby benchmark/xpath_profiling.rb
|
|
394
|
+
bundle install # install Ruby deps
|
|
395
|
+
bundle exec rspec # full test suite (176 specs)
|
|
396
|
+
bundle exec rspec spec/xml/xpath_spec.rb:42 # one example by line
|
|
397
|
+
bundle exec rubocop # lint
|
|
1437
398
|
----
|
|
1438
399
|
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
=== API Reference
|
|
1442
|
-
|
|
1443
|
-
**Complete YARD documentation** is available for all public APIs:
|
|
1444
|
-
|
|
1445
|
-
* **HTML Documentation**: link:doc/index.html[View API Docs] (86.12% coverage, 134 methods documented)
|
|
1446
|
-
* **Serve Locally**: Run `yard server` and visit http://localhost:8808
|
|
1447
|
-
|
|
1448
|
-
**Coverage**: All core classes fully documented with examples:
|
|
1449
|
-
* `Taurus` module - Main entry point and parsing
|
|
1450
|
-
* `Taurus::Document` - Document container with root access
|
|
1451
|
-
* `Taurus::Element` - Core element API (50+ methods)
|
|
1452
|
-
* `Taurus::Node` - Base class for all nodes
|
|
1453
|
-
* `Taurus::NodeSet` - XPath result collections
|
|
1454
|
-
* `Taurus::AttributesHash` - Dual string/symbol attribute access
|
|
1455
|
-
* `Taurus::XPath` - XPath utilities (tokenize, parse, evaluate)
|
|
1456
|
-
|
|
1457
|
-
=== Guides & References
|
|
1458
|
-
|
|
1459
|
-
* link:CHANGELOG.md[Changelog] - Version history and release notes
|
|
1460
|
-
* link:docs/XPATH_SPEC_COMPLIANCE.md[XPath 1.0 Spec Compliance] - Complete compliance matrix with test coverage
|
|
1461
|
-
* link:docs/PERFORMANCE.adoc[Performance Guide] - Comprehensive optimization analysis and benchmarking
|
|
1462
|
-
* link:docs/ARCHITECTURE.adoc[Architecture] - System design and component structure
|
|
1463
|
-
* link:docs/FUTURE_VISION.md[Future Vision] - Long-term roadmap and libtaurus vision
|
|
1464
|
-
* link:old-docs/sessions/[Development History] - Historical optimization analyses
|
|
1465
|
-
|
|
1466
|
-
== Contributing
|
|
1467
|
-
|
|
1468
|
-
1. Fork the repository
|
|
1469
|
-
2. Create your feature branch (`git checkout -b feat/amazing-feature`)
|
|
1470
|
-
3. Commit your changes (`git commit -m 'feat: add amazing feature'`)
|
|
1471
|
-
4. Push to the branch (`git push origin feat/amazing-feature`)
|
|
1472
|
-
5. Open a Pull Request
|
|
1473
|
-
|
|
1474
|
-
=== Development Principles
|
|
1475
|
-
|
|
1476
|
-
* **Architecture First** - Prioritize clean design over hacks
|
|
1477
|
-
* **Test Religiously** - 100% pass rate is non-negotiable
|
|
1478
|
-
* **MECE Always** - Mutually Exclusive, Collectively Exhaustive
|
|
1479
|
-
* **Document Thoroughly** - Future developers will thank you
|
|
400
|
+
CI pins libtaurus to a released tag (currently v0.22.0) and builds it
|
|
401
|
+
from source on each runner; see `.github/workflows/build.yml`.
|
|
1480
402
|
|
|
1481
403
|
== License
|
|
1482
404
|
|
|
1483
|
-
MIT
|
|
1484
|
-
|
|
1485
|
-
== Credits
|
|
1486
|
-
|
|
1487
|
-
* **pugixml** - Performance optimization techniques
|
|
1488
|
-
* **StAX** - Memory-efficient streaming patterns
|
|
1489
|
-
* **Ox** - API compatibility inspiration
|
|
1490
|
-
* **Nokogiri** - XPath
|
|
1491
|
-
|
|
1492
|
-
feature completeness inspiration
|
|
1493
|
-
|
|
1494
|
-
== Links
|
|
1495
|
-
|
|
1496
|
-
* **RubyGems**: https://rubygems.org/gems/taurus
|
|
1497
|
-
* **GitHub**: https://github.com/lutaml/taurus
|
|
1498
|
-
* **Issues**: https://github.com/lutaml/taurus/issues
|
|
1499
|
-
* **Discussions**: https://github.com/lutaml/taurus/discussions
|
|
1500
|
-
== C Library
|
|
1501
|
-
|
|
1502
|
-
This gem provides Ruby bindings for the https://github.com/lutaml/taurus[libtaurus] C library.
|
|
1503
|
-
|
|
1504
|
-
The C library provides the core functionality:
|
|
1505
|
-
|
|
1506
|
-
* **High-performance XML parsing** with SIMD optimizations
|
|
1507
|
-
* **Complete XPath 1.0 implementation** (27 functions, 13 axes)
|
|
1508
|
-
* **Full XML Namespaces 1.0** specification support
|
|
1509
|
-
* **Command-line interface** (taurus CLI)
|
|
1510
|
-
* **Zero external dependencies** (no libxml2)
|
|
1511
|
-
|
|
1512
|
-
For C API documentation and CLI usage, see the https://github.com/lutaml/taurus[taurus repository].
|
|
1513
|
-
|
|
1514
|
-
=== Building libtaurus from Source
|
|
1515
|
-
|
|
1516
|
-
If you need to rebuild the C library:
|
|
1517
|
-
|
|
1518
|
-
[source,bash]
|
|
1519
|
-
----
|
|
1520
|
-
git clone https://github.com/lutaml/taurus.git
|
|
1521
|
-
cd taurus
|
|
1522
|
-
mkdir build && cd build
|
|
1523
|
-
cmake ..
|
|
1524
|
-
make
|
|
1525
|
-
sudo make install # Optional: system-wide installation
|
|
1526
|
-
----
|
|
1527
|
-
|
|
1528
|
-
The Ruby gem includes a pre-built copy of `libtaurus.dylib` for convenience.
|
|
1529
|
-
|
|
405
|
+
MIT — see link:LICENSE[LICENSE].
|