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,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,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ffi"
4
+
5
+ module Taurus
6
+ module XML
7
+ module SAX
8
+ autoload :Document, "taurus/xml/sax/document"
9
+ autoload :Parser, "taurus/xml/sax/parser"
10
+ end
11
+ end
12
+ 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
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "taurus/version"
4
+
5
+ module Taurus
6
+ autoload :XML, "taurus/xml"
7
+ end
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