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,202 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ffi"
4
+
5
+ class Taurus::XML::Document
6
+ attr_reader :c_ptr, :wrapper_cache
7
+
8
+ # @api private
9
+ # Internal flag container shared between the Document instance and its
10
+ # GC finalizer. Using a one-element Array because Procs close over
11
+ # variables by reference — mutating freed[0] is visible from both
12
+ # the explicit `free` path and the finalizer. This eliminates the
13
+ # double-free that FFI::AutoPointer's release proc caused when
14
+ # `Document#free` was called explicitly and then GC ran.
15
+ Freed = Struct.new(:state) # state: :alive | :freed
16
+
17
+ def initialize(c_ptr = nil, freed = Freed.new(:alive))
18
+ @c_ptr = c_ptr
19
+ @freed = freed
20
+ # Per-document weak-ref cache for Node wrappers, keyed on c_ptr
21
+ # address. Eliminates re-allocation when the same node is accessed
22
+ # repeatedly (e.g. via children, siblings, multiple xpath calls).
23
+ # Dies with the Document — no stale entries pointing at freed memory.
24
+ @wrapper_cache = ObjectSpace::WeakMap.new
25
+ end
26
+
27
+ def self.parse(xml_or_io)
28
+ xml = xml_or_io.respond_to?(:read) ? xml_or_io.read : xml_or_io.to_s
29
+ if xml.empty?
30
+ raise Taurus::XML::ParseError, "empty input"
31
+ end
32
+ status_ptr = ::FFI::MemoryPointer.new(:int)
33
+ raw = Taurus::XML::FFI.taurus_parse_string(xml, xml.bytesize, status_ptr)
34
+ if raw.null?
35
+ status = status_ptr.read_int
36
+ raise Taurus::XML::ParseError,
37
+ "taurus_parse_string failed (status=#{status})"
38
+ end
39
+ wrap(raw)
40
+ end
41
+
42
+ def self.parse_file(path)
43
+ status_ptr = ::FFI::MemoryPointer.new(:int)
44
+ raw = Taurus::XML::FFI.taurus_parse_file(path, status_ptr)
45
+ if raw.null?
46
+ status = status_ptr.read_int
47
+ raise Taurus::XML::ParseError,
48
+ "taurus_parse_file failed (status=#{status})"
49
+ end
50
+ wrap(raw)
51
+ end
52
+
53
+ # Convert a raw TaurusDocument pointer into a Ruby Document with safe
54
+ # GC lifetime management. The finalizer captures the raw address
55
+ # integer (not the Document or Pointer object — those would prevent
56
+ # GC) and shares a one-shot flag with the instance so explicit
57
+ # `#free` and the GC finalizer can never both call
58
+ # `taurus_document_free` on the same address.
59
+ def self.wrap(raw_address)
60
+ addr = raw_address.is_a?(::FFI::Pointer) ? raw_address.address : raw_address
61
+ ptr = ::FFI::Pointer.new(addr)
62
+ freed = Freed.new(:alive)
63
+ doc = new(ptr, freed)
64
+ ObjectSpace.define_finalizer(doc, finalizer(addr, freed))
65
+ doc
66
+ end
67
+
68
+ def self.finalizer(address, freed)
69
+ proc do
70
+ next if freed.state == :freed
71
+ freed.state = :freed
72
+ Taurus::XML::FFI.taurus_document_free(::FFI::Pointer.new(address))
73
+ end
74
+ end
75
+ private_class_method :finalizer
76
+
77
+ def root
78
+ raise Taurus::XML::UseAfterFreeError if @freed.state == :freed
79
+ return nil if @c_ptr.nil?
80
+ ptr = Taurus::XML::FFI.taurus_document_root(@c_ptr)
81
+ return nil if ptr.null?
82
+ Taurus::XML::Element.new(ptr, self)
83
+ end
84
+
85
+ def create_element(name)
86
+ ptr = Taurus::XML::FFI.taurus_element_create(@c_ptr, name)
87
+ raise Taurus::XML::Error, "taurus_element_create failed" if ptr.null?
88
+ Taurus::XML::Element.new(ptr, self)
89
+ end
90
+
91
+ def create_text_node(content)
92
+ ptr = Taurus::XML::FFI.taurus_text_node_create(@c_ptr, content.to_s)
93
+ raise Taurus::XML::Error, "taurus_text_node_create failed" if ptr.null?
94
+ Taurus::XML::Text.new(ptr, self)
95
+ end
96
+
97
+ def create_comment(content)
98
+ ptr = Taurus::XML::FFI.taurus_comment_node_create(@c_ptr, content.to_s)
99
+ raise Taurus::XML::Error, "taurus_comment_node_create failed" if ptr.null?
100
+ Taurus::XML::Comment.new(ptr, self)
101
+ end
102
+
103
+ def create_cdata(content)
104
+ ptr = Taurus::XML::FFI.taurus_cdata_node_create(@c_ptr, content.to_s)
105
+ raise Taurus::XML::Error, "taurus_cdata_node_create failed" if ptr.null?
106
+ Taurus::XML::CDATA.new(ptr, self)
107
+ end
108
+
109
+ def create_processing_instruction(target, data = "")
110
+ ptr = Taurus::XML::FFI.taurus_pi_node_create(@c_ptr, target.to_s, data.to_s)
111
+ raise Taurus::XML::Error, "taurus_pi_node_create failed" if ptr.null?
112
+ Taurus::XML::ProcessingInstruction.new(ptr, self)
113
+ end
114
+
115
+ def fragment(markup)
116
+ Taurus::XML::DocumentFragment.parse(markup, self)
117
+ end
118
+
119
+ def dup
120
+ raw = Taurus::XML::FFI.taurus_document_copy(@c_ptr)
121
+ raise Taurus::XML::Error, "taurus_document_copy failed" if raw.null?
122
+ self.class.wrap(raw)
123
+ end
124
+ alias_method :clone, :dup
125
+
126
+ def doctype
127
+ ptr = Taurus::XML::FFI.taurus_document_internal_subset(@c_ptr)
128
+ return nil if ptr.null?
129
+ Taurus::XML::DocType.new(ptr, self)
130
+ end
131
+ alias_method :internal_subset, :doctype
132
+
133
+ def to_xml(indent: 0, no_decl: false, encoding: nil)
134
+ raise Taurus::XML::UseAfterFreeError if @freed.state == :freed
135
+ return "" if @c_ptr.nil?
136
+ opts, enc_ptr = build_serialize_options(indent: indent, no_decl: no_decl, encoding: encoding)
137
+ str_ptr = Taurus::XML::FFI.taurus_document_serialize(@c_ptr, opts.pointer)
138
+ return "" if str_ptr.null?
139
+ str_ptr.read_string.tap { |s| Taurus::XML::FFI.taurus_free_string(str_ptr) }
140
+ end
141
+ alias_method :to_s, :to_xml
142
+ alias_method :serialize, :to_xml
143
+
144
+ def save(path, **opts)
145
+ opts_struct, enc_ptr = build_serialize_options(
146
+ indent: opts.fetch(:indent, 0),
147
+ no_decl: opts.fetch(:no_decl, false),
148
+ encoding: opts[:encoding])
149
+ status = Taurus::XML::FFI.taurus_document_save_file(@c_ptr, path, opts_struct.pointer)
150
+ raise Taurus::XML::Error,
151
+ Taurus::XML::FFI.taurus_status_string(status) unless status == Taurus::XML::FFI::TAURUS_OK
152
+ self
153
+ end
154
+
155
+ def canonicalize(version = Taurus::XML::FFI::C14N_1_0,
156
+ inclusive_namespaces = nil,
157
+ with_comments: false,
158
+ exclusive: false,
159
+ mode: nil)
160
+ raise Taurus::XML::UseAfterFreeError if @freed.state == :freed
161
+ return "" if @c_ptr.nil?
162
+ resolved_mode = mode || (exclusive ? Taurus::XML::FFI::C14N_MODE_EXCLUSIVE
163
+ : Taurus::XML::FFI::C14N_MODE_CANONICAL)
164
+ ns_ptr, _anchor = Taurus::XML.c14n_build_ns_pointer(inclusive_namespaces)
165
+ flags = with_comments ? 1 : 0
166
+ str_ptr = Taurus::XML::FFI.taurus_c14n_canonicalize_ex(
167
+ @c_ptr, version, resolved_mode, ns_ptr, flags)
168
+ return "" if str_ptr.null?
169
+ str_ptr.read_string.tap { Taurus::XML::FFI.taurus_free_string(str_ptr) }
170
+ end
171
+ alias_method :c14n, :canonicalize
172
+
173
+ def free
174
+ return if @freed.state == :freed
175
+ @freed.state = :freed
176
+ Taurus::XML::FFI.taurus_document_free(@c_ptr) unless @c_ptr.nil?
177
+ @c_ptr = nil
178
+ end
179
+
180
+ def name; "document"; end
181
+ def document; self; end
182
+ def encoding
183
+ return nil if @c_ptr.nil?
184
+ Taurus::XML::FFI.taurus_document_encoding(@c_ptr)
185
+ end
186
+
187
+ private
188
+
189
+ def build_serialize_options(indent:, no_decl:, encoding:)
190
+ opts = Taurus::XML::FFI::SerializeOptions.new
191
+ opts[:indent] = indent.to_i
192
+ opts[:xml_declaration] = no_decl ? 0 : 1
193
+ enc_ptr = nil
194
+ if encoding
195
+ enc_ptr = ::FFI::MemoryPointer.from_string(encoding.to_s)
196
+ opts[:encoding] = enc_ptr
197
+ end
198
+ [opts, enc_ptr]
199
+ end
200
+
201
+ include Taurus::XML::Searchable
202
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ffi"
4
+
5
+ # Wraps the synthetic "#document-fragment" element returned by
6
+ # taurus_parse_fragment. Children of this fragment are the parsed nodes;
7
+ # the fragment itself isn't part of any document tree but borrows its
8
+ # document's lifetime.
9
+ class Taurus::XML::DocumentFragment
10
+ attr_reader :document, :c_ptr
11
+
12
+ def initialize(document, c_ptr)
13
+ @document = document
14
+ @c_ptr = c_ptr
15
+ end
16
+
17
+ def self.parse(xml, document)
18
+ status_ptr = ::FFI::MemoryPointer.new(:int)
19
+ raw = Taurus::XML::FFI.taurus_parse_fragment(
20
+ xml.to_s, xml.to_s.bytesize, document.c_ptr, status_ptr)
21
+ if raw.null?
22
+ raise Taurus::XML::ParseError,
23
+ "taurus_parse_fragment failed (status=#{status_ptr.read_int})"
24
+ end
25
+ new(document, raw)
26
+ end
27
+
28
+ def children
29
+ count = Taurus::XML::FFI.taurus_element_child_count(@c_ptr)
30
+ nodes = []
31
+ ptr = Taurus::XML::FFI.taurus_node_first_child(@c_ptr)
32
+ until ptr.nil? || ptr.null?
33
+ nodes << Taurus::XML::Node.wrap(ptr, @document)
34
+ ptr = Taurus::XML::FFI.taurus_node_next_sibling(ptr)
35
+ end
36
+ Taurus::XML::NodeSet.new(@document, nodes)
37
+ end
38
+
39
+ def name
40
+ "#document-fragment"
41
+ end
42
+ end
@@ -0,0 +1,278 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Taurus::XML::Element < Taurus::XML::Node
4
+ def name
5
+ Taurus::XML::FFI.taurus_element_name(@c_ptr)
6
+ end
7
+ alias_method :node_name, :name
8
+
9
+ def name=(new_name)
10
+ status = Taurus::XML::FFI.taurus_element_set_name(@c_ptr, new_name)
11
+ raise Taurus::XML::Error,
12
+ Taurus::XML::FFI.taurus_status_string(status) unless status == Taurus::XML::FFI::TAURUS_OK
13
+ new_name
14
+ end
15
+ alias_method :node_name=, :name=
16
+
17
+ def content
18
+ Taurus::XML::FFI.taurus_element_text(@c_ptr)
19
+ end
20
+
21
+ def content=(new_content)
22
+ status = Taurus::XML::FFI.taurus_element_set_text(@c_ptr, new_content.to_s)
23
+ raise Taurus::XML::Error,
24
+ Taurus::XML::FFI.taurus_status_string(status) unless status == Taurus::XML::FFI::TAURUS_OK
25
+ new_content
26
+ end
27
+
28
+ def [](key)
29
+ Taurus::XML::FFI.taurus_element_attribute(@c_ptr, key.to_s)
30
+ end
31
+ alias_method :attr, :[]
32
+ alias_method :get_attribute, :[]
33
+
34
+ def []=(key, value)
35
+ status = Taurus::XML::FFI.taurus_element_set_attribute(@c_ptr, key.to_s, value.to_s)
36
+ raise Taurus::XML::Error,
37
+ Taurus::XML::FFI.taurus_status_string(status) unless status == Taurus::XML::FFI::TAURUS_OK
38
+ value
39
+ end
40
+ alias_method :set_attribute, :[]=
41
+
42
+ def key?(name)
43
+ !Taurus::XML::FFI.taurus_element_attribute(@c_ptr, name.to_s).nil?
44
+ end
45
+ alias_method :has_attribute?, :key?
46
+
47
+ def remove_attribute(name)
48
+ status = Taurus::XML::FFI.taurus_element_remove_attribute(@c_ptr, name.to_s)
49
+ raise Taurus::XML::Error,
50
+ Taurus::XML::FFI.taurus_status_string(status) unless status == Taurus::XML::FFI::TAURUS_OK
51
+ self
52
+ end
53
+ alias_method :delete, :remove_attribute
54
+
55
+ def keys
56
+ count = Taurus::XML::FFI.taurus_element_attribute_count(@c_ptr)
57
+ count.times.map { |i| Taurus::XML::FFI.taurus_element_attribute_name_at(@c_ptr, i) }
58
+ end
59
+
60
+ def values
61
+ count = Taurus::XML::FFI.taurus_element_attribute_count(@c_ptr)
62
+ count.times.map { |i| Taurus::XML::FFI.taurus_element_attribute_value_at(@c_ptr, i) }
63
+ end
64
+
65
+ def attributes
66
+ count = Taurus::XML::FFI.taurus_element_attribute_count(@c_ptr)
67
+ result = {}
68
+ count.times do |i|
69
+ name = Taurus::XML::FFI.taurus_element_attribute_name_at(@c_ptr, i)
70
+ value = Taurus::XML::FFI.taurus_element_attribute_value_at(@c_ptr, i)
71
+ result[name] = Taurus::XML::Attr.new(name, value, self)
72
+ end
73
+ result
74
+ end
75
+
76
+ def attribute_nodes
77
+ count = Taurus::XML::FFI.taurus_element_attribute_count(@c_ptr)
78
+ count.times.map do |i|
79
+ name = Taurus::XML::FFI.taurus_element_attribute_name_at(@c_ptr, i)
80
+ value = Taurus::XML::FFI.taurus_element_attribute_value_at(@c_ptr, i)
81
+ Taurus::XML::Attr.new(name, value, self)
82
+ end
83
+ end
84
+
85
+ def add_child(node)
86
+ status = Taurus::XML::FFI.taurus_element_append_child(@c_ptr, node.c_ptr)
87
+ raise Taurus::XML::Error,
88
+ Taurus::XML::FFI.taurus_status_string(status) unless status == Taurus::XML::FFI::TAURUS_OK
89
+ node
90
+ end
91
+ alias_method :<<, :add_child
92
+
93
+ def prepend_child(node)
94
+ status = Taurus::XML::FFI.taurus_element_prepend_child(@c_ptr, node.c_ptr)
95
+ raise Taurus::XML::Error,
96
+ Taurus::XML::FFI.taurus_status_string(status) unless status == Taurus::XML::FFI::TAURUS_OK
97
+ node
98
+ end
99
+
100
+ def add_next_sibling(node)
101
+ status = Taurus::XML::FFI.taurus_element_insert_after(@c_ptr, node.c_ptr)
102
+ raise Taurus::XML::Error,
103
+ Taurus::XML::FFI.taurus_status_string(status) unless status == Taurus::XML::FFI::TAURUS_OK
104
+ node
105
+ end
106
+
107
+ def add_previous_sibling(node)
108
+ status = Taurus::XML::FFI.taurus_element_insert_before(@c_ptr, node.c_ptr)
109
+ raise Taurus::XML::Error,
110
+ Taurus::XML::FFI.taurus_status_string(status) unless status == Taurus::XML::FFI::TAURUS_OK
111
+ node
112
+ end
113
+
114
+ def remove_child(node)
115
+ status = Taurus::XML::FFI.taurus_element_remove_child(@c_ptr, node.c_ptr)
116
+ raise Taurus::XML::Error,
117
+ Taurus::XML::FFI.taurus_status_string(status) unless status == Taurus::XML::FFI::TAURUS_OK
118
+ node
119
+ end
120
+
121
+ def children=(node_or_nodes)
122
+ # Remove existing children, then attach the new ones in source order.
123
+ Taurus::XML::FFI.taurus_element_remove_children(@c_ptr)
124
+ Array(node_or_nodes).each { |n| add_child(n) }
125
+ end
126
+
127
+ # Replace this element with +new_node+ in the parent's child list.
128
+ # +new_node+ must belong to the same document. Returns +new_node+.
129
+ def replace(new_node)
130
+ parent = self.parent
131
+ raise Taurus::XML::Error, "cannot replace a node with no parent" unless parent
132
+ add_next_sibling(new_node)
133
+ parent.remove_child(self)
134
+ new_node
135
+ end
136
+
137
+ # Like #replace but returns self for chaining.
138
+ def swap(new_node)
139
+ replace(new_node)
140
+ self
141
+ end
142
+
143
+ # Wrap this element in a new element parsed from +markup+ or a dup of
144
+ # +node+. The wrapper takes this element's place in the tree, and this
145
+ # element becomes its only child. Returns self for chaining.
146
+ def wrap(node_or_markup)
147
+ wrapper =
148
+ case node_or_markup
149
+ when Taurus::XML::Element then node_or_markup.dup
150
+ when String
151
+ frag_doc = Taurus::XML::Document.parse(node_or_markup)
152
+ frag_doc.root or raise Taurus::XML::Error, "wrap markup has no root element"
153
+ else
154
+ raise ArgumentError, "wrap expects a String or Element, got #{node_or_markup.class}"
155
+ end
156
+
157
+ parent = self.parent
158
+ raise Taurus::XML::Error, "cannot wrap a node with no parent" unless parent
159
+
160
+ # Insert wrapper at self's position, then move self into wrapper.
161
+ # add_child moves self (unlinks from old parent first), so no explicit
162
+ # remove_child needed — and trying to remove after the move corrupts
163
+ # the C tree (libtaurus silently handles non-child args badly).
164
+ add_next_sibling(wrapper)
165
+ wrapper.add_child(self)
166
+ self
167
+ end
168
+
169
+ def dup
170
+ copy_ptr = Taurus::XML::FFI.taurus_element_copy(@c_ptr, @document.c_ptr)
171
+ raise Taurus::XML::Error, "taurus_element_copy failed" if copy_ptr.null?
172
+ Taurus::XML::Element.new(copy_ptr, @document)
173
+ end
174
+ alias_method :clone, :dup
175
+
176
+ def add_child(node_or_markup)
177
+ case node_or_markup
178
+ when Taurus::XML::Node
179
+ status = Taurus::XML::FFI.taurus_element_append_child(@c_ptr, node_or_markup.c_ptr)
180
+ raise Taurus::XML::Error,
181
+ Taurus::XML::FFI.taurus_status_string(status) unless status == Taurus::XML::FFI::TAURUS_OK
182
+ node_or_markup
183
+ when String
184
+ frag = Taurus::XML::DocumentFragment.parse(node_or_markup, @document)
185
+ added = []
186
+ frag.children.each do |n|
187
+ status = Taurus::XML::FFI.taurus_element_append_child(@c_ptr, n.c_ptr)
188
+ raise Taurus::XML::Error,
189
+ Taurus::XML::FFI.taurus_status_string(status) unless status == Taurus::XML::FFI::TAURUS_OK
190
+ added << n
191
+ end
192
+ Taurus::XML::NodeSet.new(@document, added)
193
+ else
194
+ raise ArgumentError, "add_child expects a Node or String, got #{node_or_markup.class}"
195
+ end
196
+ end
197
+
198
+ def namespace
199
+ uri = Taurus::XML::FFI.taurus_element_namespace(@c_ptr)
200
+ return nil if uri.nil? || uri.empty?
201
+ Taurus::XML::Namespace.new(self, uri)
202
+ end
203
+
204
+ def namespace_definitions
205
+ count = Taurus::XML::FFI.taurus_element_namespace_count(@c_ptr)
206
+ count.times.map do |i|
207
+ prefix = Taurus::XML::FFI.taurus_element_namespace_decl_prefix(@c_ptr, i)
208
+ uri = Taurus::XML::FFI.taurus_element_namespace_decl_uri(@c_ptr, i)
209
+ Taurus::XML::Namespace.new(self, uri, prefix: prefix)
210
+ end
211
+ end
212
+
213
+ def namespaces
214
+ scopes = {}
215
+ node = self
216
+ while node.is_a?(Taurus::XML::Element)
217
+ node.namespace_definitions.each do |ns|
218
+ key = ns.prefix ? "xmlns:#{ns.prefix}" : "xmlns"
219
+ scopes[key] ||= ns.href
220
+ end
221
+ node = node.parent
222
+ end
223
+ scopes
224
+ end
225
+
226
+ def to_xml(indent: 0, no_decl: false, encoding: nil)
227
+ opts = Taurus::XML::FFI::SerializeOptions.new
228
+ opts[:indent] = indent.to_i
229
+ opts[:xml_declaration] = no_decl ? 0 : 1
230
+ enc_ptr = nil
231
+ if encoding
232
+ enc_ptr = ::FFI::MemoryPointer.from_string(encoding.to_s)
233
+ opts[:encoding] = enc_ptr
234
+ end
235
+ str_ptr = Taurus::XML::FFI.taurus_element_serialize(@c_ptr, opts.pointer)
236
+ return "" if str_ptr.null?
237
+ str_ptr.read_string.tap { Taurus::XML::FFI.taurus_free_string(str_ptr) }
238
+ end
239
+
240
+ def canonicalize(version = Taurus::XML::FFI::C14N_1_0,
241
+ inclusive_namespaces = nil,
242
+ with_comments: false,
243
+ exclusive: false,
244
+ mode: nil)
245
+ resolved_mode = mode || (exclusive ? Taurus::XML::FFI::C14N_MODE_EXCLUSIVE
246
+ : Taurus::XML::FFI::C14N_MODE_CANONICAL)
247
+ ns_ptr, _anchor = Taurus::XML.c14n_build_ns_pointer(inclusive_namespaces)
248
+ flags = with_comments ? 1 : 0
249
+ str_ptr = Taurus::XML::FFI.taurus_c14n_canonicalize_subtree_ex(
250
+ @c_ptr, version, resolved_mode, ns_ptr, flags)
251
+ return "" if str_ptr.null?
252
+ str_ptr.read_string.tap { Taurus::XML::FFI.taurus_free_string(str_ptr) }
253
+ end
254
+ alias_method :c14n, :canonicalize
255
+
256
+ def add_namespace_definition(prefix, href)
257
+ status = Taurus::XML::FFI.taurus_element_add_namespace_definition(
258
+ @c_ptr, prefix.to_s, href.to_s)
259
+ raise Taurus::XML::Error,
260
+ Taurus::XML::FFI.taurus_status_string(status) unless status == Taurus::XML::FFI::TAURUS_OK
261
+ Taurus::XML::Namespace.new(self, href.to_s, prefix: prefix.nil? ? nil : prefix.to_s)
262
+ end
263
+ alias_method :add_namespace, :add_namespace_definition
264
+
265
+ def default_namespace=(href)
266
+ status = Taurus::XML::FFI.taurus_element_set_default_namespace(@c_ptr, href.to_s)
267
+ raise Taurus::XML::Error,
268
+ Taurus::XML::FFI.taurus_status_string(status) unless status == Taurus::XML::FFI::TAURUS_OK
269
+ href
270
+ end
271
+
272
+ def remove_namespace_definition(prefix)
273
+ status = Taurus::XML::FFI.taurus_element_remove_namespace_definition(@c_ptr, prefix.to_s)
274
+ raise Taurus::XML::Error,
275
+ Taurus::XML::FFI.taurus_status_string(status) unless status == Taurus::XML::FFI::TAURUS_OK
276
+ self
277
+ end
278
+ end