leptris 1.1.2-aarch64-linux

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 (72) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +3 -0
  3. data/.rubocop.yml +8 -0
  4. data/CHANGELOG.md +619 -0
  5. data/CLAUDE.md +104 -0
  6. data/LICENSE.md +33 -0
  7. data/README.adoc +405 -0
  8. data/Rakefile +98 -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/leptris_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/leptris.bash +86 -0
  28. data/docs/completion/leptris.zsh +74 -0
  29. data/docs/man/leptris-format.1 +227 -0
  30. data/docs/man/leptris-parse.1 +178 -0
  31. data/docs/man/leptris-xpath.1 +312 -0
  32. data/docs/man/leptris.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/leptris.gemspec +42 -0
  47. data/lib/leptris/version.rb +5 -0
  48. data/lib/leptris/xml/attr.rb +41 -0
  49. data/lib/leptris/xml/c_string_array.rb +37 -0
  50. data/lib/leptris/xml/cdata.rb +15 -0
  51. data/lib/leptris/xml/comment.rb +15 -0
  52. data/lib/leptris/xml/css_to_xpath.rb +177 -0
  53. data/lib/leptris/xml/doc_type.rb +54 -0
  54. data/lib/leptris/xml/document.rb +212 -0
  55. data/lib/leptris/xml/document_fragment.rb +42 -0
  56. data/lib/leptris/xml/element.rb +259 -0
  57. data/lib/leptris/xml/ffi.rb +475 -0
  58. data/lib/leptris/xml/namespace.rb +43 -0
  59. data/lib/leptris/xml/node.rb +211 -0
  60. data/lib/leptris/xml/node_set.rb +150 -0
  61. data/lib/leptris/xml/parse_options.rb +29 -0
  62. data/lib/leptris/xml/processing_instruction.rb +24 -0
  63. data/lib/leptris/xml/sax/document.rb +45 -0
  64. data/lib/leptris/xml/sax/parser.rb +136 -0
  65. data/lib/leptris/xml/sax.rb +12 -0
  66. data/lib/leptris/xml/searchable.rb +92 -0
  67. data/lib/leptris/xml/serialization.rb +41 -0
  68. data/lib/leptris/xml/text.rb +15 -0
  69. data/lib/leptris/xml.rb +40 -0
  70. data/lib/leptris.rb +7 -0
  71. data/lib/libleptris.so +0 -0
  72. metadata +162 -0
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Leptris::XML::DocType
4
+ attr_reader :c_ptr, :document
5
+
6
+ def initialize(c_ptr, document)
7
+ @c_ptr = c_ptr
8
+ @document = document
9
+ end
10
+
11
+ def name
12
+ Leptris::XML::FFI.leptris_doctype_get_name(@c_ptr)
13
+ end
14
+ alias_method :node_name, :name
15
+
16
+ def root_name
17
+ Leptris::XML::FFI.leptris_doctype_get_root_name(@c_ptr)
18
+ end
19
+
20
+ def public_id
21
+ Leptris::XML::FFI.leptris_doctype_get_public_id(@c_ptr)
22
+ end
23
+
24
+ def system_id
25
+ Leptris::XML::FFI.leptris_doctype_get_system_id(@c_ptr)
26
+ end
27
+
28
+ def internal_subset
29
+ Leptris::XML::FFI.leptris_doctype_get_internal_subset(@c_ptr)
30
+ end
31
+
32
+ def external_id
33
+ pub = public_id
34
+ sys = system_id
35
+ return nil if pub.nil? && sys.nil?
36
+ parts = []
37
+ parts << "PUBLIC \"#{pub}\"" if pub
38
+ parts << "SYSTEM \"#{sys}\"" if sys && pub.nil?
39
+ parts << "\"#{sys}\"" if pub && sys
40
+ parts.join(" ")
41
+ end
42
+
43
+ def to_s
44
+ inner = internal_subset
45
+ parts = ["<!DOCTYPE #{name}"]
46
+ parts << external_id if external_id
47
+ parts << "[#{inner}]" if inner && !inner.empty?
48
+ parts.join(" ") + ">"
49
+ end
50
+
51
+ def inspect
52
+ "#<#{self.class.name} name=#{name.inspect} public=#{public_id.inspect} system=#{system_id.inspect}>"
53
+ end
54
+ end
@@ -0,0 +1,212 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ffi"
4
+
5
+ class Leptris::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 STRONG cache for Node wrappers, keyed on c_ptr
21
+ # address. Every wrapper is created through Node.wrap, which is the
22
+ # single construction path, so the same C node always yields the
23
+ # same Ruby object. Cleared when the Document is freed — no stale
24
+ # entries.
25
+ #
26
+ # Deliberately NOT ObjectSpace::WeakMap: a weak cache makes wrapper
27
+ # identity a GC race. `doc.root.equal?(doc.root)` failed on the
28
+ # Windows CI matrix (188 examples, the 4 identity specs) because
29
+ # between the two calls the first wrapper was referenced only by
30
+ # the weak map — any GC sweep evicted it and the second call built
31
+ # a fresh object. A strong cache costs at most one wrapper per node
32
+ # actually visited, held until the document dies.
33
+ @wrapper_cache = {}
34
+ end
35
+
36
+ def self.parse(xml_or_io, options: nil)
37
+ xml = xml_or_io.respond_to?(:read) ? xml_or_io.read : xml_or_io.to_s
38
+ if xml.empty?
39
+ raise Leptris::XML::ParseError, "empty input"
40
+ end
41
+ flags = resolve_flags(options)
42
+ status_ptr = ::FFI::MemoryPointer.new(:int)
43
+ raw =
44
+ if flags.zero?
45
+ Leptris::XML::FFI.leptris_parse_string(xml, xml.bytesize, status_ptr)
46
+ else
47
+ Leptris::XML::FFI.leptris_parse_string_flags(
48
+ xml, xml.bytesize, flags, status_ptr)
49
+ end
50
+ if raw.null?
51
+ status = status_ptr.read_int
52
+ raise Leptris::XML::ParseError,
53
+ "leptris_parse_string failed (status=#{status})"
54
+ end
55
+ wrap(raw)
56
+ end
57
+
58
+ def self.parse_file(path)
59
+ status_ptr = ::FFI::MemoryPointer.new(:int)
60
+ raw = Leptris::XML::FFI.leptris_parse_file(path, status_ptr)
61
+ if raw.null?
62
+ status = status_ptr.read_int
63
+ raise Leptris::XML::ParseError,
64
+ "leptris_parse_file failed (status=#{status})"
65
+ end
66
+ wrap(raw)
67
+ end
68
+
69
+ # Convert a raw LeptrisDocument pointer into a Ruby Document with safe
70
+ # GC lifetime management. The finalizer captures the raw address
71
+ # integer (not the Document or Pointer object — those would prevent
72
+ # GC) and shares a one-shot flag with the instance so explicit
73
+ # `#free` and the GC finalizer can never both call
74
+ # `leptris_document_free` on the same address.
75
+ def self.wrap(raw_address)
76
+ addr = raw_address.is_a?(::FFI::Pointer) ? raw_address.address : raw_address
77
+ ptr = ::FFI::Pointer.new(addr)
78
+ freed = Freed.new(:alive)
79
+ doc = new(ptr, freed)
80
+ ObjectSpace.define_finalizer(doc, finalizer(addr, freed))
81
+ doc
82
+ end
83
+
84
+ def self.finalizer(address, freed)
85
+ proc do
86
+ next if freed.state == :freed
87
+ freed.state = :freed
88
+ Leptris::XML::FFI.leptris_document_free(::FFI::Pointer.new(address))
89
+ end
90
+ end
91
+ private_class_method :finalizer
92
+
93
+ def self.resolve_flags(options)
94
+ return Leptris::XML::FFI::LEPTRIS_PARSE_DEFAULT if options.nil?
95
+ unless options.is_a?(Leptris::XML::ParseOptions)
96
+ raise ArgumentError, "options must be a Leptris::XML::ParseOptions"
97
+ end
98
+ options.flags
99
+ end
100
+ private_class_method :resolve_flags
101
+
102
+ def root
103
+ raise Leptris::XML::UseAfterFreeError if @freed.state == :freed
104
+ return nil if @c_ptr.nil?
105
+ ptr = Leptris::XML::FFI.leptris_document_root(@c_ptr)
106
+ return nil if ptr.null?
107
+ Leptris::XML::Node.wrap(ptr, self)
108
+ end
109
+
110
+ def create_element(name)
111
+ ptr = Leptris::XML::FFI.leptris_element_create(@c_ptr, name)
112
+ raise Leptris::XML::Error, "leptris_element_create failed" if ptr.null?
113
+ Leptris::XML::Node.wrap(ptr, self)
114
+ end
115
+
116
+ def create_text_node(content)
117
+ ptr = Leptris::XML::FFI.leptris_text_node_create(@c_ptr, content.to_s)
118
+ raise Leptris::XML::Error, "leptris_text_node_create failed" if ptr.null?
119
+ Leptris::XML::Node.wrap(ptr, self)
120
+ end
121
+
122
+ def create_comment(content)
123
+ ptr = Leptris::XML::FFI.leptris_comment_node_create(@c_ptr, content.to_s)
124
+ raise Leptris::XML::Error, "leptris_comment_node_create failed" if ptr.null?
125
+ Leptris::XML::Node.wrap(ptr, self)
126
+ end
127
+
128
+ def create_cdata(content)
129
+ ptr = Leptris::XML::FFI.leptris_cdata_node_create(@c_ptr, content.to_s)
130
+ raise Leptris::XML::Error, "leptris_cdata_node_create failed" if ptr.null?
131
+ Leptris::XML::Node.wrap(ptr, self)
132
+ end
133
+
134
+ def create_processing_instruction(target, data = "")
135
+ ptr = Leptris::XML::FFI.leptris_pi_node_create(@c_ptr, target.to_s, data.to_s)
136
+ raise Leptris::XML::Error, "leptris_pi_node_create failed" if ptr.null?
137
+ Leptris::XML::Node.wrap(ptr, self)
138
+ end
139
+
140
+ def fragment(markup)
141
+ Leptris::XML::DocumentFragment.parse(markup, self)
142
+ end
143
+
144
+ def dup
145
+ raw = Leptris::XML::FFI.leptris_document_copy(@c_ptr)
146
+ raise Leptris::XML::Error, "leptris_document_copy failed" if raw.null?
147
+ self.class.wrap(raw)
148
+ end
149
+ alias_method :clone, :dup
150
+
151
+ def doctype
152
+ ptr = Leptris::XML::FFI.leptris_document_internal_subset(@c_ptr)
153
+ return nil if ptr.null?
154
+ Leptris::XML::DocType.new(ptr, self)
155
+ end
156
+ alias_method :internal_subset, :doctype
157
+
158
+ def to_xml(indent: 0, no_decl: false, encoding: nil)
159
+ raise Leptris::XML::UseAfterFreeError if @freed.state == :freed
160
+ return "" if @c_ptr.nil?
161
+ Leptris::XML::Serialization.to_xml(
162
+ Leptris::XML::FFI.method(:leptris_document_serialize), @c_ptr,
163
+ indent: indent, no_decl: no_decl, encoding: encoding)
164
+ end
165
+ alias_method :to_s, :to_xml
166
+ alias_method :serialize, :to_xml
167
+
168
+ def save(path, **opts)
169
+ opts_struct, _encoding_anchor = Leptris::XML::Serialization.build_options(
170
+ indent: opts.fetch(:indent, 0),
171
+ no_decl: opts.fetch(:no_decl, false),
172
+ encoding: opts[:encoding])
173
+ status = Leptris::XML::FFI.leptris_document_save_file(
174
+ @c_ptr, path, opts_struct.pointer)
175
+ Leptris::XML::FFI.check_status(status)
176
+ self
177
+ end
178
+
179
+ def canonicalize(version = Leptris::XML::FFI::C14N_1_0,
180
+ inclusive_namespaces = nil,
181
+ with_comments: false,
182
+ exclusive: false,
183
+ mode: nil)
184
+ raise Leptris::XML::UseAfterFreeError if @freed.state == :freed
185
+ return "" if @c_ptr.nil?
186
+ resolved_mode = mode || (exclusive ? Leptris::XML::FFI::C14N_MODE_EXCLUSIVE
187
+ : Leptris::XML::FFI::C14N_MODE_CANONICAL)
188
+ Leptris::XML::Serialization.canonicalize(
189
+ Leptris::XML::FFI.method(:leptris_c14n_canonicalize_ex), @c_ptr,
190
+ version: version, mode: resolved_mode,
191
+ inclusive_namespaces: inclusive_namespaces,
192
+ with_comments: with_comments)
193
+ end
194
+ alias_method :c14n, :canonicalize
195
+
196
+ def free
197
+ return if @freed.state == :freed
198
+ @freed.state = :freed
199
+ Leptris::XML::FFI.leptris_document_free(@c_ptr) unless @c_ptr.nil?
200
+ @c_ptr = nil
201
+ @wrapper_cache.clear
202
+ end
203
+
204
+ def name; "document"; end
205
+ def document; self; end
206
+ def encoding
207
+ return nil if @c_ptr.nil?
208
+ Leptris::XML::FFI.leptris_document_encoding(@c_ptr)
209
+ end
210
+
211
+ include Leptris::XML::Searchable
212
+ 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
+ # leptris_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 Leptris::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 = Leptris::XML::FFI.leptris_parse_fragment(
20
+ xml.to_s, xml.to_s.bytesize, document.c_ptr, status_ptr)
21
+ if raw.null?
22
+ raise Leptris::XML::ParseError,
23
+ "leptris_parse_fragment failed (status=#{status_ptr.read_int})"
24
+ end
25
+ new(document, raw)
26
+ end
27
+
28
+ def children
29
+ count = Leptris::XML::FFI.leptris_element_child_count(@c_ptr)
30
+ nodes = []
31
+ ptr = Leptris::XML::FFI.leptris_node_first_child(@c_ptr)
32
+ until ptr.nil? || ptr.null?
33
+ nodes << Leptris::XML::Node.wrap(ptr, @document)
34
+ ptr = Leptris::XML::FFI.leptris_node_next_sibling(ptr)
35
+ end
36
+ Leptris::XML::NodeSet.new(@document, nodes)
37
+ end
38
+
39
+ def name
40
+ "#document-fragment"
41
+ end
42
+ end
@@ -0,0 +1,259 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Leptris::XML::Element < Leptris::XML::Node
4
+ def name
5
+ Leptris::XML::FFI.leptris_element_name(@c_ptr)
6
+ end
7
+ alias_method :node_name, :name
8
+
9
+ def name=(new_name)
10
+ Leptris::XML::FFI.check_status(
11
+ Leptris::XML::FFI.leptris_element_set_name(@c_ptr, new_name))
12
+ new_name
13
+ end
14
+ alias_method :node_name=, :name=
15
+
16
+ def content
17
+ Leptris::XML::FFI.leptris_element_text(@c_ptr)
18
+ end
19
+
20
+ def content=(new_content)
21
+ Leptris::XML::FFI.check_status(
22
+ Leptris::XML::FFI.leptris_element_set_text(@c_ptr, new_content.to_s))
23
+ new_content
24
+ end
25
+
26
+ def [](key)
27
+ Leptris::XML::FFI.leptris_element_attribute(@c_ptr, key.to_s)
28
+ end
29
+ alias_method :attr, :[]
30
+ alias_method :get_attribute, :[]
31
+
32
+ def []=(key, value)
33
+ Leptris::XML::FFI.check_status(
34
+ Leptris::XML::FFI.leptris_element_set_attribute(@c_ptr, key.to_s, value.to_s))
35
+ value
36
+ end
37
+ alias_method :set_attribute, :[]=
38
+
39
+ def key?(name)
40
+ Leptris::XML::FFI.leptris_element_has_attribute(@c_ptr, name.to_s) != 0
41
+ end
42
+ alias_method :has_attribute?, :key?
43
+
44
+ def remove_attribute(name)
45
+ Leptris::XML::FFI.check_status(
46
+ Leptris::XML::FFI.leptris_element_remove_attribute(@c_ptr, name.to_s))
47
+ self
48
+ end
49
+ alias_method :delete, :remove_attribute
50
+
51
+ # Iterates the element's attributes via the v1.1.0 linked-list face
52
+ # (one FFI call per attribute; the name_at/value_at indexing API
53
+ # re-walks the list per index, making it O(n^2) per element).
54
+ def each_attribute
55
+ return enum_for(:each_attribute) unless block_given?
56
+ attr = Leptris::XML::FFI.leptris_element_first_attribute(@c_ptr)
57
+ until attr.nil? || attr.null?
58
+ name = Leptris::XML::FFI.leptris_attribute_get_name(attr)
59
+ value = Leptris::XML::FFI.leptris_attribute_get_value(@c_ptr, attr)
60
+ yield Leptris::XML::Attr.new(name, value, self)
61
+ attr = Leptris::XML::FFI.leptris_attribute_next(attr)
62
+ end
63
+ self
64
+ end
65
+
66
+ def keys
67
+ each_attribute.to_a.map(&:name)
68
+ end
69
+
70
+ def values
71
+ each_attribute.to_a.map(&:value)
72
+ end
73
+
74
+ def attributes
75
+ result = {}
76
+ each_attribute { |attr| result[attr.name] = attr }
77
+ result
78
+ end
79
+
80
+ def attribute_nodes
81
+ each_attribute.to_a
82
+ end
83
+
84
+ # The element's own namespace prefix (e.g. "foo" for <foo:child/>),
85
+ # or nil when the element has none.
86
+ def prefix
87
+ Leptris::XML::FFI.leptris_element_prefix(@c_ptr)
88
+ end
89
+
90
+ def prepend_child(node)
91
+ Leptris::XML::FFI.check_status(
92
+ Leptris::XML::FFI.leptris_element_prepend_child(@c_ptr, node.c_ptr))
93
+ node
94
+ end
95
+
96
+ def add_next_sibling(node)
97
+ Leptris::XML::FFI.check_status(
98
+ Leptris::XML::FFI.leptris_element_insert_after(@c_ptr, node.c_ptr))
99
+ node
100
+ end
101
+
102
+ def add_previous_sibling(node)
103
+ Leptris::XML::FFI.check_status(
104
+ Leptris::XML::FFI.leptris_element_insert_before(@c_ptr, node.c_ptr))
105
+ node
106
+ end
107
+
108
+ def remove_child(node)
109
+ Leptris::XML::FFI.check_status(
110
+ Leptris::XML::FFI.leptris_element_remove_child(@c_ptr, node.c_ptr))
111
+ node
112
+ end
113
+
114
+ def children=(node_or_nodes)
115
+ # Remove existing children, then attach the new ones in source order.
116
+ Leptris::XML::FFI.check_status(
117
+ Leptris::XML::FFI.leptris_element_remove_children(@c_ptr))
118
+ Array(node_or_nodes).each { |n| add_child(n) }
119
+ end
120
+
121
+ # Replace this element with +new_node+ in the parent's child list.
122
+ # +new_node+ must belong to the same document. Returns +new_node+.
123
+ def replace(new_node)
124
+ parent = self.parent
125
+ raise Leptris::XML::Error, "cannot replace a node with no parent" unless parent
126
+ add_next_sibling(new_node)
127
+ parent.remove_child(self)
128
+ new_node
129
+ end
130
+
131
+ # Like #replace but returns self for chaining.
132
+ def swap(new_node)
133
+ replace(new_node)
134
+ self
135
+ end
136
+
137
+ # Wrap this element in a new element parsed from +markup+ or a dup of
138
+ # +node+. The wrapper takes this element's place in the tree, and this
139
+ # element becomes its only child. Returns self for chaining.
140
+ def wrap(node_or_markup)
141
+ wrapper =
142
+ case node_or_markup
143
+ when Leptris::XML::Element then node_or_markup.dup
144
+ when String
145
+ frag_doc = Leptris::XML::Document.parse(node_or_markup)
146
+ frag_doc.root or raise Leptris::XML::Error, "wrap markup has no root element"
147
+ else
148
+ raise ArgumentError, "wrap expects a String or Element, got #{node_or_markup.class}"
149
+ end
150
+
151
+ parent = self.parent
152
+ raise Leptris::XML::Error, "cannot wrap a node with no parent" unless parent
153
+
154
+ # Insert wrapper at self's position, then move self into wrapper.
155
+ # add_child moves self (unlinks from old parent first), so no explicit
156
+ # remove_child needed — and trying to remove after the move corrupts
157
+ # the C tree (libleptris silently handles non-child args badly).
158
+ add_next_sibling(wrapper)
159
+ wrapper.add_child(self)
160
+ self
161
+ end
162
+
163
+ def dup
164
+ copy_ptr = Leptris::XML::FFI.leptris_element_copy(@c_ptr, @document.c_ptr)
165
+ raise Leptris::XML::Error, "leptris_element_copy failed" if copy_ptr.null?
166
+ Leptris::XML::Node.wrap(copy_ptr, @document)
167
+ end
168
+ alias_method :clone, :dup
169
+
170
+ def add_child(node_or_markup)
171
+ case node_or_markup
172
+ when Leptris::XML::Node
173
+ Leptris::XML::FFI.check_status(
174
+ Leptris::XML::FFI.leptris_element_append_child(@c_ptr, node_or_markup.c_ptr))
175
+ node_or_markup
176
+ when String
177
+ frag = Leptris::XML::DocumentFragment.parse(node_or_markup, @document)
178
+ added = []
179
+ frag.children.each do |n|
180
+ Leptris::XML::FFI.check_status(
181
+ Leptris::XML::FFI.leptris_element_append_child(@c_ptr, n.c_ptr))
182
+ added << n
183
+ end
184
+ Leptris::XML::NodeSet.new(@document, added)
185
+ else
186
+ raise ArgumentError, "add_child expects a Node or String, got #{node_or_markup.class}"
187
+ end
188
+ end
189
+ alias_method :<<, :add_child
190
+
191
+ def namespace
192
+ uri = Leptris::XML::FFI.leptris_element_namespace(@c_ptr)
193
+ return nil if uri.nil? || uri.empty?
194
+ Leptris::XML::Namespace.new(self, uri)
195
+ end
196
+
197
+ def namespace_definitions
198
+ count = Leptris::XML::FFI.leptris_element_namespace_count(@c_ptr)
199
+ count.times.map do |i|
200
+ prefix = Leptris::XML::FFI.leptris_element_namespace_decl_prefix(@c_ptr, i)
201
+ uri = Leptris::XML::FFI.leptris_element_namespace_decl_uri(@c_ptr, i)
202
+ Leptris::XML::Namespace.new(self, uri, prefix: prefix)
203
+ end
204
+ end
205
+
206
+ def namespaces
207
+ scopes = {}
208
+ node = self
209
+ while node.is_a?(Leptris::XML::Element)
210
+ node.namespace_definitions.each do |ns|
211
+ key = ns.prefix ? "xmlns:#{ns.prefix}" : "xmlns"
212
+ scopes[key] ||= ns.href
213
+ end
214
+ node = node.parent
215
+ end
216
+ scopes
217
+ end
218
+
219
+ def to_xml(indent: 0, no_decl: false, encoding: nil)
220
+ Leptris::XML::Serialization.to_xml(
221
+ Leptris::XML::FFI.method(:leptris_element_serialize), @c_ptr,
222
+ indent: indent, no_decl: no_decl, encoding: encoding)
223
+ end
224
+
225
+ def canonicalize(version = Leptris::XML::FFI::C14N_1_0,
226
+ inclusive_namespaces = nil,
227
+ with_comments: false,
228
+ exclusive: false,
229
+ mode: nil)
230
+ resolved_mode = mode || (exclusive ? Leptris::XML::FFI::C14N_MODE_EXCLUSIVE
231
+ : Leptris::XML::FFI::C14N_MODE_CANONICAL)
232
+ Leptris::XML::Serialization.canonicalize(
233
+ Leptris::XML::FFI.method(:leptris_c14n_canonicalize_subtree_ex), @c_ptr,
234
+ version: version, mode: resolved_mode,
235
+ inclusive_namespaces: inclusive_namespaces,
236
+ with_comments: with_comments)
237
+ end
238
+ alias_method :c14n, :canonicalize
239
+
240
+ def add_namespace_definition(prefix, href)
241
+ Leptris::XML::FFI.check_status(
242
+ Leptris::XML::FFI.leptris_element_add_namespace_definition(
243
+ @c_ptr, prefix.to_s, href.to_s))
244
+ Leptris::XML::Namespace.new(self, href.to_s, prefix: prefix.nil? ? nil : prefix.to_s)
245
+ end
246
+ alias_method :add_namespace, :add_namespace_definition
247
+
248
+ def default_namespace=(href)
249
+ Leptris::XML::FFI.check_status(
250
+ Leptris::XML::FFI.leptris_element_set_default_namespace(@c_ptr, href.to_s))
251
+ href
252
+ end
253
+
254
+ def remove_namespace_definition(prefix)
255
+ Leptris::XML::FFI.check_status(
256
+ Leptris::XML::FFI.leptris_element_remove_namespace_definition(@c_ptr, prefix.to_s))
257
+ self
258
+ end
259
+ end