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
@@ -0,0 +1,382 @@
1
+ # TODO 3 — Document, Node, Element, NodeSet, Searchable implementation
2
+
3
+ ## Goal
4
+
5
+ Implement the core Nokogiri-compatible Ruby classes backed by FFI
6
+ calls to libtaurus. Each Ruby method = one C function call.
7
+
8
+ ## File layout
9
+
10
+ ```
11
+ lib/taurus.rb # autoload + version
12
+ lib/taurus/xml.rb # XML module + parse entry points
13
+ lib/taurus/xml/ffi.rb # FFI declarations (TODO 2)
14
+ lib/taurus/xml/document.rb # Document class
15
+ lib/taurus/xml/node.rb # Node base class
16
+ lib/taurus/xml/element.rb # Element < Node
17
+ lib/taurus/xml/text.rb # Text < Node
18
+ lib/taurus/xml/comment.rb # Comment < Node
19
+ lib/taurus/xml/cdata.rb # CDATA < Node
20
+ lib/taurus/xml/processing_instruction.rb # ProcessingInstruction < Node
21
+ lib/taurus/xml/attr.rb # Attr class
22
+ lib/taurus/xml/node_set.rb # NodeSet class
23
+ lib/taurus/xml/searchable.rb # Searchable mixin (xpath/css/search)
24
+ lib/taurus/xml/parse_options.rb # ParseOptions class
25
+ ```
26
+
27
+ ## Autoload pattern
28
+
29
+ ```ruby
30
+ # lib/taurus.rb
31
+ module Taurus
32
+ autoload :XML, 'taurus/xml'
33
+ end
34
+
35
+ # lib/taurus/xml.rb
36
+ module Taurus
37
+ module XML
38
+ autoload :FFI, 'taurus/xml/ffi'
39
+ autoload :Document, 'taurus/xml/document'
40
+ autoload :Node, 'taurus/xml/node'
41
+ autoload :Element, 'taurus/xml/element'
42
+ autoload :Text, 'taurus/xml/text'
43
+ autoload :Comment, 'taurus/xml/comment'
44
+ autoload :CDATA, 'taurus/xml/cdata'
45
+ autoload :ProcessingInstruction, 'taurus/xml/processing_instruction'
46
+ autoload :Attr, 'taurus/xml/attr'
47
+ autoload :NodeSet, 'taurus/xml/node_set'
48
+ autoload :Searchable, 'taurus/xml/searchable'
49
+ autoload :ParseOptions, 'taurus/xml/parse_options'
50
+
51
+ def self.parse(string_or_io, options = nil)
52
+ xml = string_or_io.respond_to?(:read) ? string_or_io.read : string_or_io
53
+ status = ::FFI::MemoryPointer.new(:int)
54
+ ptr = FFI.taurus_parse_string(xml, xml.bytesize, status)
55
+ raise ParseError, "taurus_parse_string failed (status=#{status.read_int})" if ptr.nil? || ptr.null?
56
+ Document.wrap(ptr)
57
+ end
58
+ end
59
+ end
60
+ ```
61
+
62
+ ## Document
63
+
64
+ ```ruby
65
+ class Taurus::XML::Document < Taurus::XML::Node
66
+ def self.wrap(c_ptr)
67
+ doc = allocate
68
+ doc.instance_variable_set(:@c_ptr, c_ptr)
69
+ doc
70
+ end
71
+
72
+ def root
73
+ ptr = FFI.taurus_document_root(@c_ptr)
74
+ return nil if ptr.nil? || ptr.null?
75
+ Taurus::XML::Element.wrap(ptr, self)
76
+ end
77
+
78
+ def xpath(expr)
79
+ Taurus::XML::XPath.evaluate(@c_ptr, nil, expr)
80
+ end
81
+
82
+ def at_xpath(expr)
83
+ result = xpath(expr)
84
+ result.is_a?(NodeSet) ? result.first : result
85
+ end
86
+
87
+ def to_xml(options = {})
88
+ opts = SerializeOptions.new
89
+ opts[:indent] = options[:indent] || 0
90
+ opts[:xml_declaration] = options[:no_decl] ? 0 : 1
91
+ ptr = FFI.taurus_serialize_document(@c_ptr, opts.pointer)
92
+ return '' if ptr.nil? || ptr.null?
93
+ str = ptr.read_string
94
+ FFI.taurus_free_string(ptr)
95
+ str
96
+ end
97
+
98
+ def free
99
+ return unless @c_ptr
100
+ FFI.taurus_document_free(@c_ptr)
101
+ @c_ptr = nil
102
+ end
103
+ end
104
+ ```
105
+
106
+ ## Node (base class)
107
+
108
+ ```ruby
109
+ class Taurus::XML::Node
110
+ attr_reader :c_ptr, :document
111
+
112
+ def initialize(c_ptr, document)
113
+ @c_ptr = c_ptr
114
+ @document = document
115
+ end
116
+
117
+ def self.wrap(c_ptr, document)
118
+ type = FFI.taurus_node_get_type(c_ptr)
119
+ case type
120
+ when NODE_ELEMENT then Taurus::XML::Element.new(c_ptr, document)
121
+ when NODE_TEXT then Taurus::XML::Text.new(c_ptr, document)
122
+ when NODE_COMMENT then Taurus::XML::Comment.new(c_ptr, document)
123
+ when NODE_CDATA then Taurus::XML::CDATA.new(c_ptr, document)
124
+ when NODE_PI then Taurus::XML::ProcessingInstruction.new(c_ptr, document)
125
+ else new(c_ptr, document)
126
+ end
127
+ end
128
+
129
+ def name
130
+ raise NotImplementedError
131
+ end
132
+
133
+ def content
134
+ raise NotImplementedError
135
+ end
136
+
137
+ def type
138
+ FFI.taurus_node_get_type(@c_ptr)
139
+ end
140
+
141
+ def element?; type == NODE_ELEMENT; end
142
+ def text?; type == NODE_TEXT; end
143
+ def comment?; type == NODE_COMMENT; end
144
+ def cdata?; type == NODE_CDATA; end
145
+ def processing_instruction?; type == NODE_PI; end
146
+
147
+ def next_sibling
148
+ ptr = FFI.taurus_node_next_sibling(@c_ptr)
149
+ return nil if ptr.null?
150
+ Node.wrap(ptr, @document)
151
+ end
152
+
153
+ def previous_sibling
154
+ ptr = FFI.taurus_node_previous_sibling(@c_ptr)
155
+ return nil if ptr.null?
156
+ Node.wrap(ptr, @document)
157
+ end
158
+
159
+ def parent
160
+ ptr = FFI.taurus_element_parent(@c_ptr) # only works for elements
161
+ return nil if ptr.null?
162
+ Element.wrap(ptr, @document)
163
+ end
164
+
165
+ def children
166
+ NodeSet.new(@document, self)
167
+ end
168
+
169
+ def child
170
+ ptr = FFI.taurus_node_first_child(@c_ptr)
171
+ return nil if ptr.null?
172
+ Node.wrap(ptr, @document)
173
+ end
174
+
175
+ def traverse(&block)
176
+ return enum_for(:traverse) unless block_given?
177
+ block.call(self)
178
+ children.each { |c| c.traverse(&block) }
179
+ end
180
+
181
+ include Searchable
182
+ end
183
+ ```
184
+
185
+ ## Element
186
+
187
+ ```ruby
188
+ class Taurus::XML::Element < Taurus::XML::Node
189
+ def name
190
+ FFI.taurus_element_name(@c_ptr)
191
+ end
192
+
193
+ def name=(n)
194
+ FFI.taurus_element_set_name(@c_ptr, n)
195
+ end
196
+
197
+ def content
198
+ FFI.taurus_element_text(@c_ptr)
199
+ end
200
+ alias_method :text, :content
201
+
202
+ def [](attr_name)
203
+ FFI.taurus_element_attribute(@c_ptr, attr_name, nil)
204
+ end
205
+
206
+ def []=(attr_name, value)
207
+ FFI.taurus_element_set_attribute(@c_ptr, attr_name, value.to_s)
208
+ end
209
+
210
+ def attributes
211
+ # Walk the C attribute list, build a hash of Attr objects
212
+ result = {}
213
+ count = FFI.taurus_element_attribute_count(@c_ptr)
214
+ # ... iterate attribute linked list ...
215
+ result
216
+ end
217
+
218
+ def keys
219
+ attributes.keys
220
+ end
221
+
222
+ def values
223
+ attributes.values.map(&:value)
224
+ end
225
+
226
+ def first_element_child
227
+ ptr = FFI.taurus_element_first_child_any(@c_ptr)
228
+ return nil if ptr.null?
229
+ Element.wrap(ptr, @document)
230
+ end
231
+
232
+ def add_child(node)
233
+ FFI.taurus_element_append_child(@c_ptr, node.c_ptr)
234
+ node
235
+ end
236
+
237
+ def add_class(names) ... end
238
+ def remove_class(names = nil) ... end
239
+ def classes
240
+ (self['class'] || '').split
241
+ end
242
+ end
243
+ ```
244
+
245
+ ## NodeSet
246
+
247
+ ```ruby
248
+ class Taurus::XML::NodeSet
249
+ include Enumerable
250
+ include Searchable
251
+
252
+ def initialize(document, result_ptr = nil)
253
+ @document = document
254
+ @result_ptr = result_ptr # TaurusXPathResult pointer
255
+ end
256
+
257
+ def length
258
+ return @cached_length if @cached_length
259
+ @cached_length =
260
+ @result_ptr ? FFI.taurus_xpath_result_count(@result_ptr) : 0
261
+ end
262
+ alias_method :size, :length
263
+
264
+ def [](index)
265
+ return nil if index < 0 || index >= length
266
+ ptr = FFI.taurus_xpath_result_get(@result_ptr, index)
267
+ return nil if ptr.null?
268
+ Node.wrap(ptr, @document)
269
+ end
270
+
271
+ def first(n = nil)
272
+ return self[0] if n.nil?
273
+ NodeSet.new(@document).tap { |ns| n.times { |i| ns << self[i] } }
274
+ end
275
+
276
+ def last
277
+ self[length - 1]
278
+ end
279
+
280
+ def each
281
+ return enum_for(:each) unless block_given?
282
+ length.times { |i| yield self[i] }
283
+ end
284
+
285
+ def empty?
286
+ length == 0
287
+ end
288
+
289
+ def inner_text
290
+ map(&:content).join
291
+ end
292
+
293
+ def to_xml
294
+ map(&:to_xml).join
295
+ end
296
+
297
+ def free
298
+ return unless @result_ptr
299
+ FFI.taurus_xpath_result_free(@result_ptr)
300
+ @result_ptr = nil
301
+ end
302
+ end
303
+ ```
304
+
305
+ ## Searchable (mixin for xpath/css)
306
+
307
+ ```ruby
308
+ module Taurus::XML::Searchable
309
+ def xpath(*paths)
310
+ expr = paths.join(' | ')
311
+ result_ptr = FFI.taurus_xpath_eval(
312
+ document.c_ptr,
313
+ respond_to?(:c_ptr) ? c_ptr : nil,
314
+ expr
315
+ )
316
+ return nil if result_ptr.null?
317
+
318
+ result_type = FFI.taurus_xpath_result_type(result_ptr)
319
+ case result_type
320
+ when XPATH_NODESET
321
+ NodeSet.new(@document, result_ptr)
322
+ when XPATH_NUMBER
323
+ n = FFI.taurus_xpath_result_number(result_ptr)
324
+ FFI.taurus_xpath_result_free(result_ptr)
325
+ n
326
+ when XPATH_STRING
327
+ s = FFI.taurus_xpath_result_string(result_ptr)
328
+ FFI.taurus_xpath_result_free(result_ptr)
329
+ s
330
+ when XPATH_BOOLEAN
331
+ b = FFI.taurus_xpath_result_boolean(result_ptr)
332
+ FFI.taurus_xpath_result_free(result_ptr)
333
+ b == 1
334
+ end
335
+ end
336
+
337
+ def at_xpath(*paths)
338
+ ns = xpath(*paths)
339
+ ns.is_a?(NodeSet) ? ns.first : ns
340
+ end
341
+
342
+ def search(*args)
343
+ expr = args.first.to_s
344
+ if expr.start_with?('/') || expr.start_with?('//')
345
+ xpath(expr)
346
+ else
347
+ css(expr)
348
+ end
349
+ end
350
+
351
+ def at(*args)
352
+ result = search(*args)
353
+ result.is_a?(NodeSet) ? result.first : result
354
+ end
355
+
356
+ def css(*selectors)
357
+ # Convert CSS to XPath (minimal converter)
358
+ xpath_expr = CssToXPath.convert(selectors.join(', '))
359
+ xpath(xpath_expr)
360
+ end
361
+
362
+ def at_css(*selectors)
363
+ result = css(*selectors)
364
+ result.is_a?(NodeSet) ? result.first : result
365
+ end
366
+ end
367
+ ```
368
+
369
+ ## Implementation notes
370
+
371
+ - **No `require_relative`** anywhere. Use `autoload` defined in the
372
+ immediate parent namespace's file (e.g., `lib/taurus/xml.rb`).
373
+ - **No `instance_variable_set`/`instance_variable_get`** on other
374
+ objects. Use public accessor methods.
375
+ - **No `send`** to call private methods.
376
+ - **No `respond_to?`** for type checks. Use `is_a?`.
377
+ - Wrap C pointers in Ruby objects via `Node.wrap(ptr, doc)` which
378
+ dispatches on the C node type.
379
+ - Node objects are lightweight: just a pointer + document reference.
380
+ No caching of properties (each call goes through FFI).
381
+ - The document is the ONLY object that owns memory. All other objects
382
+ are non-owning handles.
@@ -0,0 +1,203 @@
1
+ # TODO 4 — SAX parser wrapper
2
+
3
+ ## Goal
4
+
5
+ Wrap libtaurus's C SAX parser (`taurus_sax_parse`, `taurus_sax_parser_feed`)
6
+ behind a Nokogiri-compatible Ruby SAX API.
7
+
8
+ ## Nokogiri SAX API (target)
9
+
10
+ ```ruby
11
+ class MyHandler < Nokogiri::XML::SAX::Document
12
+ def start_document; end
13
+ def end_document; end
14
+ def start_element(name, attrs = []); end
15
+ def end_element(name); end
16
+ def characters(string); end
17
+ def comment(string); end
18
+ def cdata_block(string); end
19
+ def processing_instruction(name, content); end
20
+ def warning(msg); end
21
+ def error(msg); end
22
+ end
23
+
24
+ parser = Nokogiri::XML::SAX::Parser.new(MyHandler.new)
25
+ parser.parse(File.read('file.xml'))
26
+ # or streaming:
27
+ parser.parse(io) # reads in chunks
28
+ ```
29
+
30
+ ## Taurus SAX API (source)
31
+
32
+ From `src/include/taurus/sax/sax.h`:
33
+
34
+ ```c
35
+ struct TaurusSAXHandler {
36
+ void (*start_document)(void* user_data);
37
+ void (*end_document)(void* user_data);
38
+ void (*start_element)(void* user_data, const char* name, const char** attrs);
39
+ void (*end_element)(void* user_data, const char* name);
40
+ void (*characters)(void* user_data, const char* text, size_t len);
41
+ void (*comment)(void* user_data, const char* comment);
42
+ void (*cdata)(void* user_data, const char* cdata);
43
+ void (*processing_instruction)(void* user_data, const char* target, const char* data);
44
+ void (*start_prefix_mapping)(void* user_data, const char* prefix, const char* uri);
45
+ void (*end_prefix_mapping)(void* user_data, const char* prefix);
46
+ void (*error)(void* user_data, const char* message, int line, int column);
47
+ };
48
+
49
+ int taurus_sax_parse(const char* xml, size_t len,
50
+ TaurusSAXHandler* handler, void* user_data);
51
+ TaurusSAXParser* taurus_sax_parser_create(TaurusSAXHandler* handler, void* user_data);
52
+ int taurus_sax_parser_feed(TaurusSAXParser* parser, const char* xml,
53
+ size_t len, int is_final);
54
+ void taurus_sax_parser_free(TaurusSAXParser* parser);
55
+ ```
56
+
57
+ ## Implementation
58
+
59
+ ```ruby
60
+ module Taurus
61
+ module XML
62
+ module SAX
63
+ class Document
64
+ def start_document; end
65
+ def end_document; end
66
+ def start_element(name, attrs = []); end
67
+ def end_element(name); end
68
+ def characters(string); end
69
+ def comment(string); end
70
+ def cdata(string); end
71
+ def processing_instruction(name, content); end
72
+ def start_prefix_mapping(prefix, uri); end
73
+ def end_prefix_mapping(prefix); end
74
+ def error(message, line, column); end
75
+ def warning(message); end
76
+ end
77
+
78
+ class Parser
79
+ def initialize(handler = Document.new)
80
+ @handler = handler
81
+ end
82
+
83
+ def parse(io_or_string)
84
+ xml = io_or_string.respond_to?(:read) ? io_or_string.read : io_or_string
85
+
86
+ handler_struct = build_handler_struct(@handler)
87
+ rc = FFI.taurus_sax_parse(xml, xml.bytesize, handler_struct, nil)
88
+ raise ParseError, "SAX parse failed (rc=#{rc})" if rc != 0
89
+ self
90
+ end
91
+
92
+ private
93
+
94
+ def build_handler_struct(handler)
95
+ s = FFI::SAXHandler.new
96
+ s[:start_document] = make_callback(:start_document)
97
+ s[:end_document] = make_callback(:end_document)
98
+ s[:start_element] = make_callback(:start_element)
99
+ s[:end_element] = make_callback(:end_element)
100
+ s[:characters] = make_callback(:characters)
101
+ s[:comment] = make_callback(:comment)
102
+ s[:cdata] = make_callback(:cdata)
103
+ s[:processing_instruction] = make_callback(:processing_instruction)
104
+ s[:start_prefix_mapping] = make_callback(:start_prefix_mapping)
105
+ s[:end_prefix_mapping] = make_callback(:end_prefix_mapping)
106
+ s[:error] = make_callback(:error)
107
+ s
108
+ end
109
+
110
+ def make_callback(event)
111
+ ::FFI::Function.new(:void, [:pointer]) do |_user_data|
112
+ @handler.send(event)
113
+ end
114
+ end
115
+ end
116
+ end
117
+ end
118
+ end
119
+ ```
120
+
121
+ ## Callback signatures (FFI::Function)
122
+
123
+ Each callback needs the right C signature:
124
+
125
+ ```ruby
126
+ # start_document: void(void*)
127
+ s[:start_document] = FFI::Function.new(:void, [:pointer]) do
128
+ @handler.start_document
129
+ end
130
+
131
+ # start_element: void(void*, const char*, const char**)
132
+ # attrs is a NULL-terminated array of name-value pairs
133
+ s[:start_element] = FFI::Function.new(:void, [:pointer, :string, :pointer]) do |_, name, attrs_ptr|
134
+ attrs = []
135
+ unless attrs_ptr.null?
136
+ offset = 0
137
+ loop do
138
+ key_ptr = attrs_ptr.get_pointer(offset)
139
+ break if key_ptr.null?
140
+ val_ptr = attrs_ptr.get_pointer(offset + FFI.type_size(:pointer))
141
+ break if val_ptr.null?
142
+ attrs << key_ptr.read_string
143
+ attrs << val_ptr.read_string
144
+ offset += 2 * FFI.type_size(:pointer)
145
+ end
146
+ end
147
+ @handler.start_element(name, attrs.each_slice(2).to_h)
148
+ end
149
+
150
+ # characters: void(void*, const char*, size_t)
151
+ s[:characters] = FFI::Function.new(:void, [:pointer, :pointer, :size_t]) do |_, text_ptr, len|
152
+ @handler.characters(text_ptr.read_bytes(len).force_encoding('UTF-8'))
153
+ end
154
+
155
+ # error: void(void*, const char*, int, int)
156
+ s[:error] = FFI::Function.new(:void, [:pointer, :string, :int, :int]) do |_, msg, line, col|
157
+ @handler.error(msg, line, col)
158
+ end
159
+ ```
160
+
161
+ ## Streaming (incremental) parsing
162
+
163
+ For large documents, use `taurus_sax_parser_create` + `feed`:
164
+
165
+ ```ruby
166
+ def parse(io)
167
+ return parse(io.read) unless io.respond_to?(:read)
168
+
169
+ handler_struct = build_handler_struct(@handler)
170
+ parser = FFI.taurus_sax_parser_create(handler_struct, nil)
171
+
172
+ io.each_chunk(4096) do |chunk|
173
+ rc = FFI.taurus_sax_parser_feed(parser, chunk, chunk.bytesize, 0)
174
+ break if rc != 0
175
+ end
176
+ # Final flush
177
+ FFI.taurus_sax_parser_feed(parser, '', 0, 1)
178
+ ensure
179
+ FFI.taurus_sax_parser_free(parser) if parser
180
+ end
181
+ ```
182
+
183
+ ## File
184
+
185
+ ```
186
+ lib/taurus/xml/sax.rb
187
+ lib/taurus/xml/sax/parser.rb
188
+ lib/taurus/xml/sax/document.rb
189
+ ```
190
+
191
+ ## Autoload
192
+
193
+ ```ruby
194
+ # lib/taurus/xml/sax.rb
195
+ module Taurus
196
+ module XML
197
+ module SAX
198
+ autoload :Parser, 'taurus/xml/sax/parser'
199
+ autoload :Document, 'taurus/xml/sax/document'
200
+ end
201
+ end
202
+ end
203
+ ```