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.
Files changed (4) hide show
  1. checksums.yaml +4 -4
  2. data/README.adoc +270 -1394
  3. data/lib/taurus/version.rb +1 -1
  4. metadata +2 -2
data/README.adoc CHANGED
@@ -1,1529 +1,405 @@
1
- = Taurus: High-Performance XML Parser with Complete Namespace & XPath 1.0 Support
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://img.shields.io/github/license/lutaml/taurus.svg[License,link=https://github.com/lutaml/taurus/blob/main/LICENSE]
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
- == Vision
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
- Taurus is a next-generation XML parser for Ruby that combines:
13
- *Taurus delivers Ox-level parsing with complete XPath 1.0 support*: full namespace handling and 27 XPath functions in pure C with zero external dependencies.
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 'taurus'
279
- ----
280
-
281
- Then execute:
282
-
283
- [source,shell]
284
- ----
285
- bundle install
23
+ gem "taurus"
286
24
  ----
287
25
 
288
- **That's it!** The gem automatically uses FFI to call the native C library. No build tools needed.
26
+ Then `bundle install`.
289
27
 
290
- ==== What You Get with FFI
28
+ === Runtime requirement: libtaurus
291
29
 
292
- **No Compilation**: Install on any platform without gcc/make +
293
- **Better Portability**: Works across Ruby versions and platforms +
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
- mkdir build && cd build
307
- cmake ..
308
- make
309
- ----
310
-
311
- This creates `libtaurus.dylib` (macOS) or `libtaurus.so` (Linux).
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
- After installation, man pages are accessible directly:
399
-
50
+ 3. **Point Taurus at a specific path** by setting `TAURUS_LIB_PATH`:
51
+ +
400
52
  [source,shell]
401
53
  ----
402
- man taurus
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
- == Features
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
- Taurus v1.0.0 provides comprehensive error handling with helpful context:
60
+ == Parsing
413
61
 
414
- * ✅ **Context-aware errors** - Show code snippet around error position
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
- # Parse error with context
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
- ==== XML Formatting
740
-
741
- Pretty-print XML documents:
742
-
743
- [source,shell]
744
- ----
745
- # Format with default 2-space indentation
746
- taurus format books.xml
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 = Taurus.parse(xml)
850
- doc.xpath('//book:title') # Automatically uses detected namespaces
80
+ doc.root.name # => "library"
81
+ doc.root.children.size # => 5 (2 element children + 3 whitespace text nodes)
851
82
  ----
852
83
 
853
- ==== Custom Namespace Registration
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
- # Override or supplement auto-detected namespaces
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
- NOTE: The `namespaces:` parameter is optional and backward compatible. By default, Taurus auto-detects namespaces from XML declarations.
91
+ This is the direct Nokogiri equivalent of `Nokogiri::XML(...)`. The
92
+ returned object is a `Taurus::XML::Document`.
867
93
 
868
- === Namespace Prefixes in XPath Queries (v0.8.0)
94
+ == Reading nodes
869
95
 
870
- Taurus v0.8.0 added full support for namespace prefixes directly in XPath queries.
871
-
872
- ==== Basic Usage
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
- xml = <<~XML
877
- <root xmlns:book="http://books.org"
878
- xmlns:author="http://authors.org">
879
- <book:title>XPath Guide</book:title>
880
- <book:isbn>123-456</book:isbn>
881
- <author:name>John Doe</author:name>
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
- ==== Automatic Namespace Detection
123
+ === Tree iteration
901
124
 
902
- Namespace prefixes are automatically detected from the document:
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
- xml = <<~XML
907
- <catalog xmlns:product="http://products.org">
908
- <product:item id="1">Widget</product:item>
909
- <product:item id="2">Gadget</product:item>
910
- </catalog>
911
- XML
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
- # Combine with attribute filters
943
- pub_2020 = doc.xpath('//book:publication[@year="2020"]')
944
- # => Returns first publication
139
+ == Searching: XPath and CSS
945
140
 
946
- # Chain namespace-aware queries
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
- # Use in predicates
951
- has_author = doc.xpath('//book:publication[book:author]')
952
- # => Returns first publication only
953
- ----
954
-
955
- ==== Nested Namespace Declarations
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
- xml = <<~XML
962
- <root xmlns:outer="http://outer.org">
963
- <outer:container xmlns:inner="http://inner.org">
964
- <inner:item>Inner Item</inner:item>
965
- <outer:item>Outer Item</outer:item>
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 = Taurus.parse(xml)
971
-
972
- # Both namespaces work
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
- ==== Backward Compatibility
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
- Queries without prefixes continue to match local names:
168
+ === Supported CSS selectors
980
169
 
981
- [source,ruby]
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
- doc = Taurus.parse(xml)
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
- # Without prefix: matches local name only
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
- # With prefix: matches namespace + local name
997
- ns_items = doc.xpath('//ns:item')
998
- # => Returns only <ns:item>Namespaced</ns:item>
999
- ----
181
+ == Building and mutating
1000
182
 
1001
- === XPath Queries
183
+ Documents expose factory methods; elements expose mutation methods:
1002
184
 
1003
185
  [source,ruby]
1004
186
  ----
1005
- xml = <<~XML
1006
- <library>
1007
- <book id="1">
1008
- <title>Ruby Programming</title>
1009
- <price>29.99</price>
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 = Taurus.parse(xml)
1019
-
1020
- # Find all books
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
- === Attribute Selection with XPath
1045
-
1046
- Taurus fully supports XPath attribute selection with the attribute axis (`@`), enabling powerful attribute-based queries.
1047
-
1048
- ==== Basic Attribute Selection
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
- xml = <<~XML
1053
- <library>
1054
- <book id="1" title="XPath Guide"/>
1055
- <book id="2" title="Ruby Guide"/>
1056
- </library>
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
- ==== Attribute Axis Syntax
226
+ == Namespaces
1075
227
 
1076
- The attribute axis can be used in two forms:
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
- # Abbreviated syntax (recommended)
1081
- doc.xpath('//book/@id')
1082
-
1083
- # Full axis syntax
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
- # Both return the same results
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
- ==== Attributes in Predicates
1090
-
1091
- Use attributes to filter elements:
248
+ == Serialization and canonicalization
1092
249
 
1093
- [source,ruby]
1094
- ----
1095
- xml = <<~XML
1096
- <library>
1097
- <book id="1" price="29.99">Ruby Programming</book>
1098
- <book id="2" price="34.99">Rails Guide</book>
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
- # Count books with prices
1123
- count = doc.xpath('count(//book[@price])')
1124
- # => 2.0
1125
-
1126
- # Get first book's id
1127
- first_id = doc.xpath('string(//book[1]/@id)')
1128
- # => "1"
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
- == Error Handling
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
- Raised when XML parsing fails due to malformed input:
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
- begin
1148
- doc = Taurus.parse('<unclosed>')
1149
- rescue Taurus::ParseError => e
1150
- puts e.message # => "Failed to parse root element at line 1, column 1"
1151
- puts e.code # => :parse_failed
1152
- puts e.line # => 1
1153
- puts e.column # => 1
1154
- puts e.byte_offset # => 0
1155
- puts e.context # => Shows error location with ^ marker
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
- ==== XPathError
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
- * `:xpath_syntax` - Invalid XPath expression syntax
1186
- * `:xpath_function` - Unknown function name or invalid arguments
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
- ==== EvaluationError
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
- Raised when XPath evaluation encounters runtime issues:
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
- doc.xpath('unknown_func()')
1197
- rescue Taurus::XPathError => e
1198
- puts e.message # => "Unknown function 'unknown_func' at line 1, column 1"
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
- === Error Context and Position Markers
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
- All error exceptions provide comprehensive diagnostic information:
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
- # Validate XML before processing
1265
- def parse_safe(xml)
1266
- Taurus.parse(xml)
1267
- rescue Taurus::ParseError => e
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
- # Validate XPath before execution
1275
- def xpath_safe(doc, expression)
1276
- doc.xpath(expression)
1277
- rescue Taurus::XPathError => e
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
- === Best Practices
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
- * **MECE** - Mutually Exclusive, Collectively Exhaustive
1345
- * **Object-Oriented** - Model-driven architecture
1346
- * **Separation of Concerns** - Clear module boundaries
1347
- * **Open/Closed** - Extensible without modification
1348
- * **Single Responsibility** - Each module has one job
1349
- * **No Code Guards** - Architectural solutions, not `#ifdef`
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
- == Test Coverage
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
- === Building from Source
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
- # Clone repository
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
- === Running Benchmarks
390
+ == Development
1426
391
 
1427
392
  [source,shell]
1428
393
  ----
1429
- # Production benchmark suite (comprehensive)
1430
- bundle exec ruby benchmark/production_suite.rb
1431
-
1432
- # Compare with Ox
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
- == Documentation
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 License - see link:LICENSE[LICENSE] file for details.
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].