taurus 0.1.0

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 (70) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +3 -0
  3. data/.rubocop.yml +8 -0
  4. data/CHANGELOG.md +518 -0
  5. data/CLAUDE.md +104 -0
  6. data/LICENSE.md +33 -0
  7. data/README.adoc +1529 -0
  8. data/Rakefile +7 -0
  9. data/TODO.impl/01-architecture.md +217 -0
  10. data/TODO.impl/02-ffi-declarations.md +236 -0
  11. data/TODO.impl/03-document-node-element-nodeset.md +382 -0
  12. data/TODO.impl/04-sax-parser.md +203 -0
  13. data/TODO.impl/05-serialize-c14n-memory-specs-css.md +276 -0
  14. data/benchmark/README.md +168 -0
  15. data/benchmark/taurus_vs_nokogiri.rb +105 -0
  16. data/docs/ARCHITECTURE.adoc +559 -0
  17. data/docs/BUILD.md +395 -0
  18. data/docs/ERROR_MESSAGES.md +458 -0
  19. data/docs/FFI_ARCHITECTURE.md +439 -0
  20. data/docs/FUTURE_VISION.md +303 -0
  21. data/docs/GITHUB_ACTIONS.md +293 -0
  22. data/docs/OPTIMIZATIONS_IMPLEMENTED.adoc +459 -0
  23. data/docs/PERFORMANCE.adoc +668 -0
  24. data/docs/PERFORMANCE.md +448 -0
  25. data/docs/RELEASE_NOTES_v1.0.0.md +515 -0
  26. data/docs/XPATH_SPEC_COMPLIANCE.md +298 -0
  27. data/docs/completion/taurus.bash +86 -0
  28. data/docs/completion/taurus.zsh +74 -0
  29. data/docs/man/taurus-format.1 +227 -0
  30. data/docs/man/taurus-parse.1 +178 -0
  31. data/docs/man/taurus-xpath.1 +312 -0
  32. data/docs/man/taurus.1 +160 -0
  33. data/docs/v0.9.0_PERFORMANCE_IMPROVEMENTS.md +217 -0
  34. data/docs/v0.9.0_RELEASE_SUMMARY.md +281 -0
  35. data/docs/v1.0.0_CONTINUATION_PLAN.md +172 -0
  36. data/docs/v1.0.0_CONTINUATION_PROMPT.md +382 -0
  37. data/docs/v1.0.0_SESSION_6_CONTINUATION.md +434 -0
  38. data/docs/v1.0.0_SESSION_6_PROMPT.md +231 -0
  39. data/docs/v1.0.0_STATUS_TRACKER.md +224 -0
  40. data/docs/v1.1.0_CONTINUATION_PLAN.md +299 -0
  41. data/docs/v1.1.0_FINAL_CONTINUATION_PLAN.md +201 -0
  42. data/docs/v1.1.0_SESSION_3_PROMPT.md +223 -0
  43. data/docs/v1.1.0_STATUS_TRACKER.md +355 -0
  44. data/docs/xml-performance.adoc +115 -0
  45. data/docs/xpath-performance.adoc +379 -0
  46. data/lib/taurus/version.rb +5 -0
  47. data/lib/taurus/xml/attr.rb +43 -0
  48. data/lib/taurus/xml/c14n.rb +23 -0
  49. data/lib/taurus/xml/cdata.rb +16 -0
  50. data/lib/taurus/xml/comment.rb +16 -0
  51. data/lib/taurus/xml/css_to_xpath.rb +177 -0
  52. data/lib/taurus/xml/doc_type.rb +54 -0
  53. data/lib/taurus/xml/document.rb +202 -0
  54. data/lib/taurus/xml/document_fragment.rb +42 -0
  55. data/lib/taurus/xml/element.rb +278 -0
  56. data/lib/taurus/xml/ffi.rb +420 -0
  57. data/lib/taurus/xml/namespace.rb +43 -0
  58. data/lib/taurus/xml/node.rb +221 -0
  59. data/lib/taurus/xml/node_set.rb +143 -0
  60. data/lib/taurus/xml/parse_options.rb +19 -0
  61. data/lib/taurus/xml/processing_instruction.rb +26 -0
  62. data/lib/taurus/xml/sax/document.rb +45 -0
  63. data/lib/taurus/xml/sax/parser.rb +148 -0
  64. data/lib/taurus/xml/sax.rb +12 -0
  65. data/lib/taurus/xml/searchable.rb +93 -0
  66. data/lib/taurus/xml/text.rb +16 -0
  67. data/lib/taurus/xml.rb +29 -0
  68. data/lib/taurus.rb +7 -0
  69. data/taurus.gemspec +42 -0
  70. metadata +157 -0
data/Rakefile ADDED
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+ task default: :spec
@@ -0,0 +1,217 @@
1
+ # TODO 1 — Architecture: C-backed Nokogiri-compatible Ruby binding
2
+
3
+ ## Goal
4
+
5
+ Rewrite `taurus-ruby` to be a **thin FFI wrapper** around libtaurus v0.4.2,
6
+ exposing a **Nokogiri-compatible API**. The C DOM is the single source of
7
+ truth — no Ruby-side tree copy.
8
+
9
+ ## Current state (problem)
10
+
11
+ The existing `taurus-ruby` has:
12
+ - A **pure-Ruby** XML tree model (Document < Element, Node, NodeSet)
13
+ - A FFI bridge that does a **one-shot copy** from C to Ruby on parse
14
+ - A **pure-Ruby XPath engine** (lexer, parser, compiler, VM in Ruby)
15
+
16
+ This defeats libtaurus's performance: the C library's optimized bytecode
17
+ VM + element index are never used. XPath goes through the slow Ruby engine.
18
+
19
+ ## Target architecture
20
+
21
+ ```
22
+ User Ruby code
23
+
24
+ Taurus::XML::Document / Node / NodeSet (thin Ruby wrappers)
25
+ ↓ FFI
26
+ libtaurus v0.4.2 (C99: DOM, XPath bytecode VM, element index)
27
+ ```
28
+
29
+ Key principles:
30
+ - **Every Ruby method is a single FFI call** to the C library.
31
+ - **No Ruby-side tree copy.** The C DOM is the truth; Ruby objects are
32
+ handles (wrapping opaque pointers).
33
+ - **XPath goes through C.** `doc.xpath('//book')` calls
34
+ `taurus_xpath_eval` directly. No Ruby XPath engine.
35
+ - **SAX goes through C.** `Taurus::XML::SAX::Parser` wraps
36
+ `taurus_sax_parse` with Ruby callback dispatch.
37
+
38
+ ## Module structure (Nokogiri-compatible)
39
+
40
+ ```ruby
41
+ module Taurus
42
+ module XML
43
+ # Top-level parse entry points
44
+ def self.parse(string_or_io) → Document
45
+ def self.parse_options → ParseOptions
46
+
47
+ class Document < Node
48
+ def root → Element (or nil)
49
+ def create_element(name) → Element
50
+ def create_text_node(text) → Text
51
+ def to_xml(options) → String
52
+ def xpath(expr) → NodeSet | Float | String | Boolean
53
+ def at_xpath(expr) → Node (or nil)
54
+ def search(expr) → NodeSet
55
+ def canonicalize(...) → String
56
+ def free → void (explicit)
57
+ end
58
+
59
+ class Node
60
+ include Searchable
61
+
62
+ def name → String
63
+ def content / text → String
64
+ def [](attr_name) → String (or nil)
65
+ def []=(attr_name, value)
66
+ def attributes → Hash {String => Attr}
67
+ def children → NodeSet
68
+ def child → Node (or nil)
69
+ def first_element_child → Element (or nil)
70
+ def last_element_child → Element (or nil)
71
+ def next_sibling → Node (or nil)
72
+ def previous_sibling → Node (or nil)
73
+ def parent → Node (or nil)
74
+ def document → Document
75
+ def type → Integer (element/text/comment/cdata/pi)
76
+ def element? → Boolean
77
+ def text? → Boolean
78
+ def comment? → Boolean
79
+ def cdata? → Boolean
80
+ def processing_instruction? → Boolean
81
+ def add_child(node) → Node
82
+ def add_next_sibling(node) → Node
83
+ def add_previous_sibling(node) → Node
84
+ def remove → Node
85
+ def replace(node) → Node
86
+ def to_xml(options) → String
87
+ def inner_html → String
88
+ def traverse(&block)
89
+ end
90
+
91
+ class Element < Node
92
+ def add_class(name)
93
+ def remove_class(name)
94
+ def classes → Array<String>
95
+ end
96
+
97
+ class Text < Node; end
98
+ class Comment < Node; end
99
+ class CDATA < Node; end
100
+ class ProcessingInstruction < Node; end
101
+ class Attr
102
+ def name → String
103
+ def value → String
104
+ def value=(val)
105
+ def parent → Element
106
+ def remove
107
+ end
108
+
109
+ class NodeSet
110
+ include Enumerable
111
+ include Searchable
112
+
113
+ def length / size → Integer
114
+ def first(n) → Node | NodeSet
115
+ def last → Node
116
+ def [](index) → Node
117
+ def each(&block)
118
+ def empty? → Boolean
119
+ def xpath(expr) → NodeSet
120
+ def search(expr) → NodeSet
121
+ def to_xml → String
122
+ def inner_text → String
123
+ end
124
+
125
+ module Searchable
126
+ def xpath(*paths) → NodeSet | Float | String | Boolean
127
+ def at_xpath(*paths) → Node (or nil)
128
+ def css(*selectors) → NodeSet (converts CSS to XPath)
129
+ def at_css(*selectors) → Node (or nil)
130
+ def search(*args) → NodeSet (auto-detect CSS/XPath)
131
+ def at(*args) → Node (or nil)
132
+ end
133
+
134
+ class ParseOptions
135
+ DEFAULT_XML = ...
136
+ RECOVER = ...
137
+ NOERROR = ...
138
+ NOWARNING = ...
139
+ NOCDATA = ...
140
+ STRICT = ...
141
+ end
142
+
143
+ module SAX
144
+ class Parser
145
+ def initialize(handler = DocHandler.new)
146
+ def parse(io_or_string)
147
+ end
148
+
149
+ class Document
150
+ def start_element(name, attrs = [])
151
+ def end_element(name)
152
+ def characters(string)
153
+ def start_document
154
+ def end_document
155
+ def comment(string)
156
+ def cdata(string)
157
+ def processing_instruction(name, content)
158
+ def error(message, line, column)
159
+ end
160
+ end
161
+ end
162
+ end
163
+ ```
164
+
165
+ ## Memory model
166
+
167
+ - **Document** owns the C document pool. `Taurus::XML::Document.new`
168
+ calls `taurus_parse_string` → returns a `TaurusDocument` pointer.
169
+ `Document#free` calls `taurus_document_free`. Auto-free via
170
+ `ObjectSpace.define_finalizer` as a safety net (but callers should
171
+ call `#free` explicitly for predictable lifecycle).
172
+ - **Node / Element / Text etc.** are **non-owning handles** wrapping
173
+ a C pointer. The pointer is valid as long as the parent Document
174
+ is alive. Freeing a Node just drops the Ruby wrapper; the C node
175
+ lives until `Document#free`.
176
+ - **NodeSet** wraps a `TaurusXPathResult` pointer from
177
+ `taurus_xpath_eval`. Freeing a NodeSet calls
178
+ `taurus_xpath_result_free`.
179
+ - **Attr** wraps a C attribute pointer (owned by the parent element's
180
+ pool). Non-owning.
181
+
182
+ ## CSS support
183
+
184
+ Nokogiri supports CSS selectors via `css()` and `at_css()`. Taurus
185
+ doesn't have a CSS engine in C, so CSS-to-XPath conversion must be
186
+ done in Ruby. Options:
187
+ 1. Use the `css_parser` gem (depends on `racc`).
188
+ 2. Write a minimal CSS-to-XPath converter in pure Ruby.
189
+
190
+ For v0.4.2 compatibility, option 2 (minimal converter) is recommended.
191
+ Nokogiri's CSS selector support is comprehensive but the common subset
192
+ is small: `tag`, `.class`, `#id`, `> child`, `descendant`,
193
+ `[attr]`, `[attr=value]`, `:first-child`, `:last-child`, `:not(...)`.
194
+
195
+ ## Dependencies
196
+
197
+ ```ruby
198
+ # taurus.gemspec
199
+ spec.add_dependency 'ffi', '~> 1.16'
200
+ ```
201
+
202
+ No other runtime dependencies. No C extension compilation needed —
203
+ just FFI to the pre-built libtaurus shared library.
204
+
205
+ ## Reference material
206
+
207
+ - Nokogiri source: `~/src/external/nokogiri/`
208
+ - `lib/nokogiri/xml/node.rb` — 77 public methods
209
+ - `lib/nokogiri/xml/node_set.rb` — 31 public methods
210
+ - `lib/nokogiri/xml/document.rb` — 22 public methods
211
+ - `lib/nokogiri/xml/searchable.rb` — xpath/css/search module
212
+ - libtaurus public headers: `src/include/taurus/`
213
+ - `types.h` — opaque handle typedefs
214
+ - `dom/document.h`, `dom/element.h`, `dom/serialize.h`
215
+ - `xpath/xpath.h` — XPath eval API
216
+ - `sax/sax.h` — SAX parser API
217
+ - libtaurus v0.4.2: tag `v0.4.2` on `github.com:lutaml/taurus`
@@ -0,0 +1,236 @@
1
+ # TODO 2 — FFI declarations: complete libtaurus v0.4.2 public API
2
+
3
+ ## Goal
4
+
5
+ Create `lib/taurus/xml/ffi.rb` that attaches to EVERY public function
6
+ in libtaurus v0.4.2 via the `ffi` gem. This is the single source of
7
+ truth for the C ↔ Ruby boundary.
8
+
9
+ ## Library loading
10
+
11
+ ```ruby
12
+ module Taurus
13
+ module XML
14
+ module FFI
15
+ extend ::FFI
16
+
17
+ ffi_lib [
18
+ ENV['TAURUS_LIB_PATH'],
19
+ 'taurus',
20
+ '/usr/local/lib/libtaurus.dylib',
21
+ '/usr/local/lib/libtaurus.so',
22
+ File.expand_path('../../../build/src/libtaurus.dylib', __dir__),
23
+ File.expand_path('../../../build/src/libtaurus.so', __dir__),
24
+ ].compact
25
+ end
26
+ end
27
+ end
28
+ ```
29
+
30
+ ## Opaque type declarations
31
+
32
+ ```ruby
33
+ typedef :pointer, :document
34
+ typedef :pointer, :element
35
+ typedef :pointer, :node_ref
36
+ typedef :pointer, :xpath_result
37
+ typedef :pointer, :sax_parser
38
+ typedef :pointer, :attribute
39
+ ```
40
+
41
+ ## Complete function list (attach all of these)
42
+
43
+ Source: `src/include/taurus/types.h`, `src/include/taurus.h`, and
44
+ `src/include/taurus/*.h`.
45
+
46
+ ### Version
47
+ ```ruby
48
+ attach_function :taurus_version, [], :string
49
+ ```
50
+
51
+ ### Document lifecycle
52
+ ```ruby
53
+ attach_function :taurus_parse_string, [:string, :size_t, :pointer], :document
54
+ attach_function :taurus_document_free, [:document], :void
55
+ attach_function :taurus_document_root, [:document], :element
56
+ attach_function :taurus_document_serialize, [:document, :pointer], :pointer
57
+ attach_function :taurus_document_set_strict, [:document, :int], :void
58
+ attach_function :taurus_xinclude_process, [:document, :string], :int
59
+ ```
60
+
61
+ ### Node access
62
+ ```ruby
63
+ attach_function :taurus_node_get_type, [:node_ref], :int
64
+ attach_function :taurus_node_first_child, [:node_ref], :node_ref
65
+ attach_function :taurus_node_next_sibling, [:node_ref], :node_ref
66
+ attach_function :taurus_node_previous_sibling, [:node_ref], :node_ref
67
+ attach_function :taurus_node_child_count, [:node_ref], :size_t
68
+ attach_function :taurus_node_as_element, [:node_ref], :element
69
+ attach_function :taurus_element_as_node, [:element], :node_ref
70
+ attach_function :taurus_element_first_child_any, [:element], :element
71
+ ```
72
+
73
+ ### Element queries
74
+ ```ruby
75
+ attach_function :taurus_element_name, [:element], :string
76
+ attach_function :taurus_element_text, [:element], :string
77
+ attach_function :taurus_element_attribute, [:element, :string, :string], :string
78
+ attach_function :taurus_element_attribute_count, [:element], :size_t
79
+ attach_function :taurus_element_first_attribute, [:element], :pointer
80
+ attach_function :taurus_element_parent, [:element], :element
81
+ attach_function :taurus_element_next_sibling_any, [:element], :element
82
+ attach_function :taurus_element_get_namespace_uri, [:element], :string
83
+ attach_function :taurus_element_get_prefix, [:element], :string
84
+ attach_function :taurus_element_get_name, [:element], :string
85
+ ```
86
+
87
+ ### Element mutation
88
+ ```ruby
89
+ attach_function :taurus_element_set_name, [:element, :string], :void
90
+ attach_function :taurus_element_set_attribute, [:element, :string, :string], :void
91
+ attach_function :taurus_element_remove_attribute, [:element, :string], :void
92
+ attach_function :taurus_element_append_child, [:element, :element], :int
93
+ attach_function :taurus_element_create_child, [:element, :string], :element
94
+ attach_function :taurus_element_set_text, [:element, :string], :void
95
+ attach_function :taurus_element_remove_child, [:element, :element], :void
96
+ ```
97
+
98
+ ### Element creation
99
+ ```ruby
100
+ attach_function :taurus_element_create, [:string], :element
101
+ attach_function :taurus_text_node_create, [:string], :element
102
+ attach_function :taurus_comment_node_create, [:string], :element
103
+ attach_function :taurus_cdata_node_create, [:string], :element
104
+ attach_function :taurus_pi_node_create, [:string, :string], :element
105
+ ```
106
+
107
+ ### Text / Comment / CDATA / PI access
108
+ ```ruby
109
+ attach_function :taurus_text_node_get_content, [:node_ref], :string
110
+ attach_function :taurus_comment_node_get_content, [:node_ref], :string
111
+ attach_function :taurus_cdata_node_get_content, [:node_ref], :string
112
+ attach_function :taurus_pi_node_get_target, [:node_ref], :string
113
+ attach_function :taurus_pi_node_get_data, [:node_ref], :string
114
+ ```
115
+
116
+ ### XPath
117
+ ```ruby
118
+ attach_function :taurus_xpath_eval,
119
+ [:document, :element, :string], :xpath_result
120
+ attach_function :taurus_xpath_eval_with_vars,
121
+ [:document, :string, :pointer], :xpath_result
122
+ attach_function :taurus_xpath_result_type, [:xpath_result], :int
123
+ attach_function :taurus_xpath_result_count, [:xpath_result], :size_t
124
+ attach_function :taurus_xpath_result_get, [:xpath_result, :size_t], :element
125
+ attach_function :taurus_xpath_result_boolean, [:xpath_result], :int
126
+ attach_function :taurus_xpath_result_number, [:xpath_result], :double
127
+ attach_function :taurus_xpath_result_string, [:xpath_result], :pointer
128
+ attach_function :taurus_xpath_result_free, [:xpath_result], :void
129
+ ```
130
+
131
+ ### XPath variables
132
+ ```ruby
133
+ attach_function :taurus_xpath_variable_set_new, [], :pointer
134
+ attach_function :taurus_xpath_variable_set_free, [:pointer], :void
135
+ attach_function :taurus_xpath_variable_set_boolean, [:pointer, :string, :int], :int
136
+ attach_function :taurus_xpath_variable_set_number, [:pointer, :string, :double], :int
137
+ attach_function :taurus_xpath_variable_set_string, [:pointer, :string, :string], :int
138
+ ```
139
+
140
+ ### SAX
141
+ ```ruby
142
+ # TaurusSAXHandler is a struct of function pointers. Use FFI::Struct.
143
+ class SAXHandler < ::FFI::Struct
144
+ layout \
145
+ :start_document, :pointer,
146
+ :end_document, :pointer,
147
+ :start_element, :pointer,
148
+ :end_element, :pointer,
149
+ :characters, :pointer,
150
+ :comment, :pointer,
151
+ :cdata, :pointer,
152
+ :processing_instruction, :pointer,
153
+ :start_prefix_mapping, :pointer,
154
+ :end_prefix_mapping, :pointer,
155
+ :error, :pointer
156
+ end
157
+
158
+ attach_function :taurus_sax_parse,
159
+ [:string, :size_t, SAXHandler.by_pointer, :pointer], :int
160
+ attach_function :taurus_sax_parser_create,
161
+ [SAXHandler.by_pointer, :pointer], :sax_parser
162
+ attach_function :taurus_sax_parser_feed,
163
+ [:sax_parser, :string, :size_t, :int], :int
164
+ attach_function :taurus_sax_parser_free, [:sax_parser], :void
165
+ attach_function :taurus_sax_parser_set_streaming,
166
+ [:sax_parser, :int], :int
167
+ ```
168
+
169
+ ### Serialization
170
+ ```ruby
171
+ # TaurusSerializeOptions struct
172
+ class SerializeOptions < ::FFI::Struct
173
+ layout \
174
+ :indent, :int,
175
+ :xml_declaration, :int,
176
+ :no_empty_tags, :int,
177
+ :preserve_whitespace, :int
178
+ end
179
+
180
+ attach_function :taurus_serialize_document,
181
+ [:document, :pointer], :pointer
182
+ attach_function :taurus_c14n_canonicalize,
183
+ [:document, :int, :int], :pointer
184
+ ```
185
+
186
+ ### Memory
187
+ ```ruby
188
+ attach_function :taurus_free_string, [:pointer], :void
189
+ ```
190
+
191
+ ## Status codes
192
+
193
+ ```ruby
194
+ TAURUS_OK = 0
195
+ TAURUS_ERROR_MEMORY = -1
196
+ TAURUS_ERROR_PARSE = -2
197
+ TAURUS_ERROR_XPATH = -3
198
+ TAURUS_ERROR_NULL_ARG = -4
199
+ TAURUS_ERROR_INVALID_ARG = -5
200
+ TAURUS_ERROR_NOT_FOUND = -6
201
+ TAURUS_ERROR_IO = -7
202
+ ```
203
+
204
+ ## Node type constants
205
+
206
+ ```ruby
207
+ NODE_ELEMENT = 0
208
+ NODE_ATTRIBUTE = 1
209
+ NODE_TEXT = 2
210
+ NODE_COMMENT = 3
211
+ NODE_CDATA = 4
212
+ NODE_PI = 5
213
+ NODE_DOCTYPE = 6
214
+ ```
215
+
216
+ ## XPath result type constants
217
+
218
+ ```ruby
219
+ XPATH_NODESET = 0
220
+ XPATH_BOOLEAN = 1
221
+ XPATH_NUMBER = 2
222
+ XPATH_STRING = 3
223
+ ```
224
+
225
+ ## Notes
226
+
227
+ - Use `Blocking: true` for SAX callbacks (FFI::Function).
228
+ - Use `AutoPointer` for document and xpath_result to get automatic
229
+ cleanup. But ALSO provide explicit `#free` methods since GC timing
230
+ is non-deterministic.
231
+ - The `taurus_element_*` functions that return `:string` return
232
+ document-owned strings (valid until `taurus_document_free`).
233
+ Ruby copies them automatically on FFI return — safe.
234
+ - Functions that return `:pointer` for strings (like
235
+ `taurus_xpath_result_string`, `taurus_serialize_document`) return
236
+ heap-owned strings that the caller must free via `taurus_free_string`.