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.
- checksums.yaml +7 -0
- data/.rspec +3 -0
- data/.rubocop.yml +8 -0
- data/CHANGELOG.md +619 -0
- data/CLAUDE.md +104 -0
- data/LICENSE.md +33 -0
- data/README.adoc +405 -0
- data/Rakefile +98 -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/leptris_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/leptris.bash +86 -0
- data/docs/completion/leptris.zsh +74 -0
- data/docs/man/leptris-format.1 +227 -0
- data/docs/man/leptris-parse.1 +178 -0
- data/docs/man/leptris-xpath.1 +312 -0
- data/docs/man/leptris.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/leptris.gemspec +42 -0
- data/lib/leptris/version.rb +5 -0
- data/lib/leptris/xml/attr.rb +41 -0
- data/lib/leptris/xml/c_string_array.rb +37 -0
- data/lib/leptris/xml/cdata.rb +15 -0
- data/lib/leptris/xml/comment.rb +15 -0
- data/lib/leptris/xml/css_to_xpath.rb +177 -0
- data/lib/leptris/xml/doc_type.rb +54 -0
- data/lib/leptris/xml/document.rb +212 -0
- data/lib/leptris/xml/document_fragment.rb +42 -0
- data/lib/leptris/xml/element.rb +259 -0
- data/lib/leptris/xml/ffi.rb +475 -0
- data/lib/leptris/xml/namespace.rb +43 -0
- data/lib/leptris/xml/node.rb +211 -0
- data/lib/leptris/xml/node_set.rb +150 -0
- data/lib/leptris/xml/parse_options.rb +29 -0
- data/lib/leptris/xml/processing_instruction.rb +24 -0
- data/lib/leptris/xml/sax/document.rb +45 -0
- data/lib/leptris/xml/sax/parser.rb +136 -0
- data/lib/leptris/xml/sax.rb +12 -0
- data/lib/leptris/xml/searchable.rb +92 -0
- data/lib/leptris/xml/serialization.rb +41 -0
- data/lib/leptris/xml/text.rb +15 -0
- data/lib/leptris/xml.rb +40 -0
- data/lib/leptris.rb +7 -0
- data/lib/libleptris.so +0 -0
- 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
|