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,143 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ffi"
|
|
4
|
+
|
|
5
|
+
class Taurus::XML::NodeSet
|
|
6
|
+
include Enumerable
|
|
7
|
+
include Taurus::XML::Searchable
|
|
8
|
+
|
|
9
|
+
attr_reader :document
|
|
10
|
+
|
|
11
|
+
# Two construction modes:
|
|
12
|
+
# - eager: pass an Array of Nodes (e.g. Element#children builds one)
|
|
13
|
+
# - lazy: pass an FFI::Pointer to a TaurusXPathResult that this NodeSet
|
|
14
|
+
# will keep alive and free on GC. Each [i] / each call goes
|
|
15
|
+
# through taurus_xpath_result_get instead of pre-materializing.
|
|
16
|
+
def initialize(document, source = nil)
|
|
17
|
+
@document = document
|
|
18
|
+
case source
|
|
19
|
+
when ::FFI::Pointer
|
|
20
|
+
if source.null?
|
|
21
|
+
@result_ptr = nil
|
|
22
|
+
@array = []
|
|
23
|
+
else
|
|
24
|
+
# Wrap in AutoPointer for automatic GC-time cleanup. NodeSet has no
|
|
25
|
+
# explicit #free method (Nokogiri doesn't either), so the AutoPointer
|
|
26
|
+
# double-free risk that Document#free hit doesn't apply here.
|
|
27
|
+
@result_ptr = ::FFI::AutoPointer.new(source, Taurus::XML::FFI.method(:taurus_xpath_result_free))
|
|
28
|
+
@array = nil
|
|
29
|
+
end
|
|
30
|
+
when nil
|
|
31
|
+
@result_ptr = nil
|
|
32
|
+
@array = []
|
|
33
|
+
else
|
|
34
|
+
@result_ptr = nil
|
|
35
|
+
@array = source.to_a
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def self.from_result(document, result_ptr)
|
|
40
|
+
new(document, result_ptr)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def length
|
|
44
|
+
@result_ptr ? Taurus::XML::FFI.taurus_xpath_result_count(@result_ptr) : @array.length
|
|
45
|
+
end
|
|
46
|
+
alias_method :size, :length
|
|
47
|
+
|
|
48
|
+
def empty?
|
|
49
|
+
length == 0
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def [](idx)
|
|
53
|
+
if @result_ptr
|
|
54
|
+
return nil if idx < 0 || idx >= length
|
|
55
|
+
ptr = Taurus::XML::FFI.taurus_xpath_result_get(@result_ptr, idx)
|
|
56
|
+
return nil if ptr.null?
|
|
57
|
+
Taurus::XML::Node.wrap(ptr, @document)
|
|
58
|
+
else
|
|
59
|
+
@array[idx]
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def each
|
|
64
|
+
return enum_for(:each) unless block_given?
|
|
65
|
+
if @result_ptr
|
|
66
|
+
# Batch-fetch all node pointers in one FFI call (taurus_xpath_result_get_nodes,
|
|
67
|
+
# libtaurus v0.11.4) and wrap each. Saves N-1 FFI calls vs the per-index
|
|
68
|
+
# taurus_xpath_result_get loop. Wrappers are still cached per-Document via
|
|
69
|
+
# Node.wrap, so a re-iteration of the same NodeSet hits the cache.
|
|
70
|
+
n = length
|
|
71
|
+
if n > 0
|
|
72
|
+
buf = ::FFI::MemoryPointer.new(:pointer, n)
|
|
73
|
+
begin
|
|
74
|
+
copied = Taurus::XML::FFI.taurus_xpath_result_get_nodes(@result_ptr, buf, n)
|
|
75
|
+
copied.times do |i|
|
|
76
|
+
ptr = buf.get_pointer(i * ::FFI.type_size(:pointer))
|
|
77
|
+
next if ptr.null?
|
|
78
|
+
yield Taurus::XML::Node.wrap(ptr, @document)
|
|
79
|
+
end
|
|
80
|
+
ensure
|
|
81
|
+
buf.free
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
else
|
|
85
|
+
@array.each { |n| yield n }
|
|
86
|
+
end
|
|
87
|
+
self
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def to_a
|
|
91
|
+
return @array if @array
|
|
92
|
+
return [] if @result_ptr.nil?
|
|
93
|
+
out = []
|
|
94
|
+
each { |n| out << n }
|
|
95
|
+
@array = out # memoize so subsequent to_a / each avoids re-fetch
|
|
96
|
+
out
|
|
97
|
+
end
|
|
98
|
+
alias_method :to_ary, :to_a
|
|
99
|
+
|
|
100
|
+
def first(n = nil)
|
|
101
|
+
return self[0] if n.nil?
|
|
102
|
+
n.times.map { |i| self[i] }.take_while { |x| !x.nil? }
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def last
|
|
106
|
+
if @result_ptr
|
|
107
|
+
self[length - 1]
|
|
108
|
+
else
|
|
109
|
+
@array.last
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def inner_text
|
|
114
|
+
map(&:content).join
|
|
115
|
+
end
|
|
116
|
+
alias_method :text, :inner_text
|
|
117
|
+
|
|
118
|
+
def xpath(*paths)
|
|
119
|
+
handler, _ns, _vars = parse_search_args(paths)
|
|
120
|
+
raise ArgumentError, "custom XPath handlers not supported" if handler
|
|
121
|
+
expr = paths.join(" | ")
|
|
122
|
+
accumulated = Taurus::XML::NodeSet.new(@document)
|
|
123
|
+
each do |node|
|
|
124
|
+
next unless node.is_a?(Taurus::XML::Element)
|
|
125
|
+
result_ptr = Taurus::XML::FFI.taurus_xpath_eval(
|
|
126
|
+
@document.c_ptr, node.c_ptr, expr)
|
|
127
|
+
next if result_ptr.null?
|
|
128
|
+
sub = Taurus::XML::NodeSet.send(:from_result, @document, result_ptr)
|
|
129
|
+
accumulated = merge_node_sets(accumulated, sub)
|
|
130
|
+
end
|
|
131
|
+
accumulated
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def inspect
|
|
135
|
+
to_a.inspect
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
private
|
|
139
|
+
|
|
140
|
+
def merge_node_sets(a, b)
|
|
141
|
+
Taurus::XML::NodeSet.new(@document, a.to_a + b.to_a)
|
|
142
|
+
end
|
|
143
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Taurus::XML::ParseOptions
|
|
4
|
+
DEFAULT_XML = 0
|
|
5
|
+
RECOVER = 1 << 0
|
|
6
|
+
NOERROR = 1 << 5
|
|
7
|
+
NOWARNING = 1 << 6
|
|
8
|
+
NOCDATA = 1 << 8
|
|
9
|
+
STRICT = 1 << 18
|
|
10
|
+
|
|
11
|
+
attr_reader :options
|
|
12
|
+
|
|
13
|
+
def initialize(options = DEFAULT_XML)
|
|
14
|
+
@options = options.to_i
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def strict?; !@options.zero?; end
|
|
18
|
+
def recover?; @options & RECOVER != 0; end
|
|
19
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Taurus::XML::ProcessingInstruction < Taurus::XML::Node
|
|
4
|
+
def name
|
|
5
|
+
Taurus::XML::FFI.taurus_pi_node_get_target(@c_ptr)
|
|
6
|
+
end
|
|
7
|
+
alias_method :target, :name
|
|
8
|
+
|
|
9
|
+
def content
|
|
10
|
+
Taurus::XML::FFI.taurus_pi_node_get_data(@c_ptr).to_s
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def target=(new_target)
|
|
14
|
+
status = Taurus::XML::FFI.taurus_pi_node_set_target(@c_ptr, new_target.to_s)
|
|
15
|
+
raise Taurus::XML::Error,
|
|
16
|
+
Taurus::XML::FFI.taurus_status_string(status) unless status == Taurus::XML::FFI::TAURUS_OK
|
|
17
|
+
new_target
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def data=(new_data)
|
|
21
|
+
status = Taurus::XML::FFI.taurus_pi_node_set_data(@c_ptr, new_data.to_s)
|
|
22
|
+
raise Taurus::XML::Error,
|
|
23
|
+
Taurus::XML::FFI.taurus_status_string(status) unless status == Taurus::XML::FFI::TAURUS_OK
|
|
24
|
+
new_data
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Taurus::XML::SAX::Document
|
|
4
|
+
# Called when an XML declaration is parsed.
|
|
5
|
+
def xmldecl(version, encoding, standalone)
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def start_document
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def end_document
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# Called at the beginning of an element.
|
|
15
|
+
# +attrs+ is an Array of [name, value] pairs in source order.
|
|
16
|
+
def start_element(name, attrs = [])
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def end_element(name)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def characters(string)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def comment(string)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def cdata_block(string)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def processing_instruction(name, content)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def start_prefix_mapping(prefix, uri)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def end_prefix_mapping(prefix)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def warning(string)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def error(message, line = 0, column = 0)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ffi"
|
|
4
|
+
|
|
5
|
+
class Taurus::XML::SAX::Parser
|
|
6
|
+
CHUNK_SIZE = 4096
|
|
7
|
+
private_constant :CHUNK_SIZE
|
|
8
|
+
|
|
9
|
+
attr_accessor :document, :encoding
|
|
10
|
+
|
|
11
|
+
def initialize(handler = Taurus::XML::SAX::Document.new, encoding = nil)
|
|
12
|
+
@document = handler
|
|
13
|
+
@encoding = encoding
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Parse a string, IO, or file path. Dispatches to parse_memory /
|
|
17
|
+
# parse_io / parse_file based on the argument type.
|
|
18
|
+
def parse(input)
|
|
19
|
+
case input
|
|
20
|
+
when String then parse_memory(input)
|
|
21
|
+
when ->(x) { x.respond_to?(:read) } then parse_io(input)
|
|
22
|
+
else
|
|
23
|
+
raise ArgumentError, "SAX parser expects a String or IO, got #{input.class}"
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def parse_memory(string)
|
|
28
|
+
string = string.dup.force_encoding("UTF-8")
|
|
29
|
+
handler_struct = build_handler_struct
|
|
30
|
+
rc = Taurus::XML::FFI.taurus_sax_parse(
|
|
31
|
+
string, string.bytesize, handler_struct.pointer, nil)
|
|
32
|
+
if rc != 0
|
|
33
|
+
raise Taurus::XML::ParseError,
|
|
34
|
+
"taurus_sax_parse failed (rc=#{rc})"
|
|
35
|
+
end
|
|
36
|
+
self
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def parse_io(io)
|
|
40
|
+
handler_struct = build_handler_struct
|
|
41
|
+
parser_ptr = Taurus::XML::FFI.taurus_sax_parser_create(
|
|
42
|
+
handler_struct.pointer, nil)
|
|
43
|
+
if parser_ptr.null?
|
|
44
|
+
raise Taurus::XML::Error, "taurus_sax_parser_create failed"
|
|
45
|
+
end
|
|
46
|
+
begin
|
|
47
|
+
while (chunk = io.read(CHUNK_SIZE))
|
|
48
|
+
rc = Taurus::XML::FFI.taurus_sax_parser_feed(
|
|
49
|
+
parser_ptr, chunk, chunk.bytesize, 0)
|
|
50
|
+
if rc != 0
|
|
51
|
+
raise Taurus::XML::ParseError, "taurus_sax_parser_feed failed (rc=#{rc})"
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
# Final flush
|
|
55
|
+
rc = Taurus::XML::FFI.taurus_sax_parser_feed(parser_ptr, "", 0, 1)
|
|
56
|
+
if rc != 0
|
|
57
|
+
raise Taurus::XML::ParseError, "taurus_sax_parser_feed (final) failed (rc=#{rc})"
|
|
58
|
+
end
|
|
59
|
+
ensure
|
|
60
|
+
Taurus::XML::FFI.taurus_sax_parser_free(parser_ptr)
|
|
61
|
+
end
|
|
62
|
+
self
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def parse_file(path)
|
|
66
|
+
File.open(path, "r") { |f| parse_io(f) }
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
private
|
|
70
|
+
|
|
71
|
+
# Build a TaurusSAXHandler struct populated with FFI::Function callbacks
|
|
72
|
+
# that dispatch to the Ruby handler. The struct (and its callbacks) are
|
|
73
|
+
# anchored against GC via the local variable for the duration of the
|
|
74
|
+
# synchronous parse call.
|
|
75
|
+
def build_handler_struct
|
|
76
|
+
s = Taurus::XML::FFI::SAXHandler.new
|
|
77
|
+
handler = @document # capture in closures
|
|
78
|
+
|
|
79
|
+
s[:start_document] = callback(:void, [:pointer]) do |_|
|
|
80
|
+
handler.start_document
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
s[:end_document] = callback(:void, [:pointer]) do |_|
|
|
84
|
+
handler.end_document
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
s[:start_element] = callback(:void, [:pointer, :string, :pointer]) do |_, name, attrs_ptr|
|
|
88
|
+
attrs = walk_attr_array(attrs_ptr)
|
|
89
|
+
handler.start_element(name, attrs)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
s[:end_element] = callback(:void, [:pointer, :string]) do |_, name|
|
|
93
|
+
handler.end_element(name)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
s[:characters] = callback(:void, [:pointer, :pointer, :size_t]) do |_, text_ptr, len|
|
|
97
|
+
handler.characters(text_ptr.read_bytes(len).force_encoding("UTF-8"))
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
s[:comment] = callback(:void, [:pointer, :string]) do |_, comment|
|
|
101
|
+
handler.comment(comment)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
s[:cdata] = callback(:void, [:pointer, :string]) do |_, cdata|
|
|
105
|
+
handler.cdata_block(cdata)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
s[:processing_instruction] = callback(:void, [:pointer, :string, :string]) do |_, target, data|
|
|
109
|
+
handler.processing_instruction(target, data)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
s[:start_prefix_mapping] = callback(:void, [:pointer, :string, :string]) do |_, prefix, uri|
|
|
113
|
+
handler.start_prefix_mapping(prefix, uri)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
s[:end_prefix_mapping] = callback(:void, [:pointer, :string]) do |_, prefix|
|
|
117
|
+
handler.end_prefix_mapping(prefix)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
s[:error] = callback(:void, [:pointer, :string, :int, :int]) do |_, msg, line, col|
|
|
121
|
+
handler.error(msg, line, col)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
s
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def callback(return_type, params, blocking: true, &block)
|
|
128
|
+
::FFI::Function.new(return_type, params, blocking: blocking, &block)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# The C `const char** attrs` is a NULL-terminated flat array of name/value
|
|
132
|
+
# pairs: [name1, value1, name2, value2, ..., NULL]. Walk into pairs.
|
|
133
|
+
def walk_attr_array(attrs_ptr)
|
|
134
|
+
return [] if attrs_ptr.null?
|
|
135
|
+
ptr_size = ::FFI.type_size(:pointer)
|
|
136
|
+
result = []
|
|
137
|
+
offset = 0
|
|
138
|
+
loop do
|
|
139
|
+
name_ptr = attrs_ptr.get_pointer(offset)
|
|
140
|
+
break if name_ptr.null?
|
|
141
|
+
value_ptr = attrs_ptr.get_pointer(offset + ptr_size)
|
|
142
|
+
break if value_ptr.null?
|
|
143
|
+
result << [name_ptr.read_string, value_ptr.read_string]
|
|
144
|
+
offset += 2 * ptr_size
|
|
145
|
+
end
|
|
146
|
+
result
|
|
147
|
+
end
|
|
148
|
+
end
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Taurus::XML::Searchable
|
|
4
|
+
def xpath(*paths)
|
|
5
|
+
handler, _ns, _vars = parse_search_args(paths)
|
|
6
|
+
raise ArgumentError, "custom XPath handlers not supported" if handler
|
|
7
|
+
expr = paths.join(" | ")
|
|
8
|
+
|
|
9
|
+
doc_ptr = is_a?(Taurus::XML::Document) ? c_ptr : document.c_ptr
|
|
10
|
+
context_ptr = is_a?(Taurus::XML::Document) ? nil : c_ptr
|
|
11
|
+
|
|
12
|
+
result_ptr = Taurus::XML::FFI.taurus_xpath_eval(doc_ptr, context_ptr, expr)
|
|
13
|
+
if result_ptr.null?
|
|
14
|
+
raise Taurus::XML::XPathError,
|
|
15
|
+
Taurus::XML::FFI.taurus_status_string(Taurus::XML::FFI::TAURUS_ERROR_XPATH)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
wrap_xpath_result(result_ptr)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def at_xpath(*paths)
|
|
22
|
+
result = xpath(*paths)
|
|
23
|
+
result.is_a?(Taurus::XML::NodeSet) ? result.first : result
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def search(*args)
|
|
27
|
+
paths = args.first.is_a?(Array) ? args.first : [args.first]
|
|
28
|
+
paths.map(&:to_s).all? { |p| looks_like_xpath?(p) } ? xpath(*paths) : css(*paths)
|
|
29
|
+
end
|
|
30
|
+
alias_method :/, :search
|
|
31
|
+
|
|
32
|
+
def at(*args)
|
|
33
|
+
result = search(*args)
|
|
34
|
+
result.is_a?(Taurus::XML::NodeSet) ? result.first : result
|
|
35
|
+
end
|
|
36
|
+
alias_method :%, :at
|
|
37
|
+
|
|
38
|
+
def css(*args)
|
|
39
|
+
handler, ns, _ = parse_search_args(args)
|
|
40
|
+
raise ArgumentError, "namespace bindings not supported in css" if ns && !ns.empty?
|
|
41
|
+
raise ArgumentError, "custom CSS handlers not supported" if handler
|
|
42
|
+
expr = args.map { |r| Taurus::XML::CssToXPath.convert(r) }.join(" | ")
|
|
43
|
+
xpath(expr)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def at_css(*args)
|
|
47
|
+
result = css(*args)
|
|
48
|
+
result.is_a?(Taurus::XML::NodeSet) ? result.first : result
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
protected
|
|
52
|
+
|
|
53
|
+
def parse_search_args(args)
|
|
54
|
+
handler = args.find { |a| !a.is_a?(String) && !a.is_a?(Hash) && !a.is_a?(Symbol) }
|
|
55
|
+
args = args - [handler] if handler
|
|
56
|
+
hashes = []
|
|
57
|
+
while args.last.is_a?(Hash) || args.last.nil?
|
|
58
|
+
hashes << args.pop
|
|
59
|
+
break if args.empty?
|
|
60
|
+
end
|
|
61
|
+
ns, vars = hashes.reverse
|
|
62
|
+
[handler, ns, vars]
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def looks_like_xpath?(str)
|
|
66
|
+
%r{\A(\./|/|\.\.|\.)}.match?(str)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def wrap_xpath_result(result_ptr)
|
|
70
|
+
type = Taurus::XML::FFI.taurus_xpath_result_type(result_ptr)
|
|
71
|
+
case type
|
|
72
|
+
when Taurus::XML::FFI::XPATH_NODESET
|
|
73
|
+
Taurus::XML::NodeSet.send(:from_result, document, result_ptr)
|
|
74
|
+
when Taurus::XML::FFI::XPATH_BOOLEAN
|
|
75
|
+
v = Taurus::XML::FFI.taurus_xpath_result_boolean(result_ptr) != 0
|
|
76
|
+
Taurus::XML::FFI.taurus_xpath_result_free(result_ptr)
|
|
77
|
+
v
|
|
78
|
+
when Taurus::XML::FFI::XPATH_NUMBER
|
|
79
|
+
v = Taurus::XML::FFI.taurus_xpath_result_number(result_ptr)
|
|
80
|
+
Taurus::XML::FFI.taurus_xpath_result_free(result_ptr)
|
|
81
|
+
v
|
|
82
|
+
when Taurus::XML::FFI::XPATH_STRING
|
|
83
|
+
str_ptr = Taurus::XML::FFI.taurus_xpath_result_string(result_ptr)
|
|
84
|
+
v = str_ptr.null? ? "" : str_ptr.read_string
|
|
85
|
+
Taurus::XML::FFI.taurus_free_string(str_ptr) unless str_ptr.null?
|
|
86
|
+
Taurus::XML::FFI.taurus_xpath_result_free(result_ptr)
|
|
87
|
+
v
|
|
88
|
+
else
|
|
89
|
+
Taurus::XML::FFI.taurus_xpath_result_free(result_ptr)
|
|
90
|
+
raise Taurus::XML::XPathError, "unknown xpath result type #{type}"
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Taurus::XML::Text < Taurus::XML::Node
|
|
4
|
+
def name; "text"; end
|
|
5
|
+
|
|
6
|
+
def content
|
|
7
|
+
Taurus::XML::FFI.taurus_text_node_get_content(@c_ptr)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def content=(new_content)
|
|
11
|
+
status = Taurus::XML::FFI.taurus_text_node_set_content(@c_ptr, new_content.to_s)
|
|
12
|
+
raise Taurus::XML::Error,
|
|
13
|
+
Taurus::XML::FFI.taurus_status_string(status) unless status == Taurus::XML::FFI::TAURUS_OK
|
|
14
|
+
new_content
|
|
15
|
+
end
|
|
16
|
+
end
|
data/lib/taurus/xml.rb
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Taurus
|
|
4
|
+
module XML
|
|
5
|
+
autoload :FFI, "taurus/xml/ffi"
|
|
6
|
+
autoload :Node, "taurus/xml/node"
|
|
7
|
+
autoload :Element, "taurus/xml/element"
|
|
8
|
+
autoload :Text, "taurus/xml/text"
|
|
9
|
+
autoload :Comment, "taurus/xml/comment"
|
|
10
|
+
autoload :CDATA, "taurus/xml/cdata"
|
|
11
|
+
autoload :ProcessingInstruction, "taurus/xml/processing_instruction"
|
|
12
|
+
autoload :Attr, "taurus/xml/attr"
|
|
13
|
+
autoload :Namespace, "taurus/xml/namespace"
|
|
14
|
+
autoload :Document, "taurus/xml/document"
|
|
15
|
+
autoload :DocumentFragment, "taurus/xml/document_fragment"
|
|
16
|
+
autoload :DocType, "taurus/xml/doc_type"
|
|
17
|
+
autoload :NodeSet, "taurus/xml/node_set"
|
|
18
|
+
autoload :Searchable, "taurus/xml/searchable"
|
|
19
|
+
autoload :ParseOptions, "taurus/xml/parse_options"
|
|
20
|
+
autoload :CssToXPath, "taurus/xml/css_to_xpath"
|
|
21
|
+
autoload :SAX, "taurus/xml/sax"
|
|
22
|
+
require "taurus/xml/c14n" # module-level helper, eager load
|
|
23
|
+
|
|
24
|
+
class Error < StandardError; end
|
|
25
|
+
class ParseError < Error; end
|
|
26
|
+
class XPathError < Error; end
|
|
27
|
+
class UseAfterFreeError < Error; end
|
|
28
|
+
end
|
|
29
|
+
end
|
data/lib/taurus.rb
ADDED
data/taurus.gemspec
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "lib/taurus/version"
|
|
4
|
+
|
|
5
|
+
Gem::Specification.new do |spec|
|
|
6
|
+
spec.name = "taurus"
|
|
7
|
+
spec.version = Taurus::VERSION
|
|
8
|
+
spec.authors = ["Ribose Inc."]
|
|
9
|
+
spec.email = ["open.source@ribose.com"]
|
|
10
|
+
|
|
11
|
+
spec.summary = "Nokogiri-compatible Ruby binding for libtaurus (XML 1.0, XPath 1.0, SAX)"
|
|
12
|
+
spec.description = <<~DESC
|
|
13
|
+
Taurus is a Nokogiri-compatible Ruby binding for libtaurus, a pure-C99 XML
|
|
14
|
+
1.0 parser with full XPath 1.0 and SAX support. The C DOM is the single
|
|
15
|
+
source of truth; Ruby objects are thin FFI wrappers (one Ruby method =
|
|
16
|
+
one FFI call).
|
|
17
|
+
DESC
|
|
18
|
+
spec.homepage = "https://github.com/lutaml/taurus-ruby"
|
|
19
|
+
spec.license = "MIT"
|
|
20
|
+
spec.required_ruby_version = ">= 3.0.0"
|
|
21
|
+
|
|
22
|
+
spec.metadata = {
|
|
23
|
+
"homepage_uri" => spec.homepage,
|
|
24
|
+
"source_code_uri" => "https://github.com/lutaml/taurus-ruby",
|
|
25
|
+
"changelog_uri" => "https://github.com/lutaml/taurus-ruby/blob/main/CHANGELOG.md",
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
spec.files = Dir.chdir(__dir__) do
|
|
29
|
+
`git ls-files -z`.split("\x0").reject do |f|
|
|
30
|
+
(File.expand_path(f) == __FILE__) ||
|
|
31
|
+
f.start_with?(*%w[bin/ test/ spec/ features/ .git .github appveyor Gemfile])
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
spec.bindir = "bin"
|
|
35
|
+
spec.executables = []
|
|
36
|
+
spec.require_paths = ["lib"]
|
|
37
|
+
|
|
38
|
+
spec.add_dependency "ffi", "~> 1.15"
|
|
39
|
+
|
|
40
|
+
spec.add_development_dependency "rake"
|
|
41
|
+
spec.add_development_dependency "rspec"
|
|
42
|
+
end
|