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.
- checksums.yaml +7 -0
- data/.rspec +3 -0
- data/.rubocop.yml +8 -0
- data/CHANGELOG.md +518 -0
- data/CLAUDE.md +104 -0
- data/LICENSE.md +33 -0
- data/README.adoc +1529 -0
- data/Rakefile +7 -0
- data/TODO.impl/01-architecture.md +217 -0
- data/TODO.impl/02-ffi-declarations.md +236 -0
- data/TODO.impl/03-document-node-element-nodeset.md +382 -0
- data/TODO.impl/04-sax-parser.md +203 -0
- data/TODO.impl/05-serialize-c14n-memory-specs-css.md +276 -0
- data/benchmark/README.md +168 -0
- data/benchmark/taurus_vs_nokogiri.rb +105 -0
- data/docs/ARCHITECTURE.adoc +559 -0
- data/docs/BUILD.md +395 -0
- data/docs/ERROR_MESSAGES.md +458 -0
- data/docs/FFI_ARCHITECTURE.md +439 -0
- data/docs/FUTURE_VISION.md +303 -0
- data/docs/GITHUB_ACTIONS.md +293 -0
- data/docs/OPTIMIZATIONS_IMPLEMENTED.adoc +459 -0
- data/docs/PERFORMANCE.adoc +668 -0
- data/docs/PERFORMANCE.md +448 -0
- data/docs/RELEASE_NOTES_v1.0.0.md +515 -0
- data/docs/XPATH_SPEC_COMPLIANCE.md +298 -0
- data/docs/completion/taurus.bash +86 -0
- data/docs/completion/taurus.zsh +74 -0
- data/docs/man/taurus-format.1 +227 -0
- data/docs/man/taurus-parse.1 +178 -0
- data/docs/man/taurus-xpath.1 +312 -0
- data/docs/man/taurus.1 +160 -0
- data/docs/v0.9.0_PERFORMANCE_IMPROVEMENTS.md +217 -0
- data/docs/v0.9.0_RELEASE_SUMMARY.md +281 -0
- data/docs/v1.0.0_CONTINUATION_PLAN.md +172 -0
- data/docs/v1.0.0_CONTINUATION_PROMPT.md +382 -0
- data/docs/v1.0.0_SESSION_6_CONTINUATION.md +434 -0
- data/docs/v1.0.0_SESSION_6_PROMPT.md +231 -0
- data/docs/v1.0.0_STATUS_TRACKER.md +224 -0
- data/docs/v1.1.0_CONTINUATION_PLAN.md +299 -0
- data/docs/v1.1.0_FINAL_CONTINUATION_PLAN.md +201 -0
- data/docs/v1.1.0_SESSION_3_PROMPT.md +223 -0
- data/docs/v1.1.0_STATUS_TRACKER.md +355 -0
- data/docs/xml-performance.adoc +115 -0
- data/docs/xpath-performance.adoc +379 -0
- data/lib/taurus/version.rb +5 -0
- data/lib/taurus/xml/attr.rb +43 -0
- data/lib/taurus/xml/c14n.rb +23 -0
- data/lib/taurus/xml/cdata.rb +16 -0
- data/lib/taurus/xml/comment.rb +16 -0
- data/lib/taurus/xml/css_to_xpath.rb +177 -0
- data/lib/taurus/xml/doc_type.rb +54 -0
- data/lib/taurus/xml/document.rb +202 -0
- data/lib/taurus/xml/document_fragment.rb +42 -0
- data/lib/taurus/xml/element.rb +278 -0
- data/lib/taurus/xml/ffi.rb +420 -0
- data/lib/taurus/xml/namespace.rb +43 -0
- data/lib/taurus/xml/node.rb +221 -0
- data/lib/taurus/xml/node_set.rb +143 -0
- data/lib/taurus/xml/parse_options.rb +19 -0
- data/lib/taurus/xml/processing_instruction.rb +26 -0
- data/lib/taurus/xml/sax/document.rb +45 -0
- data/lib/taurus/xml/sax/parser.rb +148 -0
- data/lib/taurus/xml/sax.rb +12 -0
- data/lib/taurus/xml/searchable.rb +93 -0
- data/lib/taurus/xml/text.rb +16 -0
- data/lib/taurus/xml.rb +29 -0
- data/lib/taurus.rb +7 -0
- data/taurus.gemspec +42 -0
- 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
|
+
```
|