leptris 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.rspec +3 -0
- data/.rubocop.yml +8 -0
- data/CHANGELOG.md +529 -0
- data/CLAUDE.md +104 -0
- data/LICENSE.md +33 -0
- data/README.adoc +405 -0
- data/Rakefile +7 -0
- data/TODO.impl/01-architecture.md +217 -0
- data/TODO.impl/02-ffi-declarations.md +236 -0
- data/TODO.impl/03-document-node-element-nodeset.md +382 -0
- data/TODO.impl/04-sax-parser.md +203 -0
- data/TODO.impl/05-serialize-c14n-memory-specs-css.md +276 -0
- data/benchmark/README.md +168 -0
- data/benchmark/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 +43 -0
- data/lib/leptris/xml/c14n.rb +23 -0
- data/lib/leptris/xml/cdata.rb +16 -0
- data/lib/leptris/xml/comment.rb +16 -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 +202 -0
- data/lib/leptris/xml/document_fragment.rb +42 -0
- data/lib/leptris/xml/element.rb +278 -0
- data/lib/leptris/xml/ffi.rb +420 -0
- data/lib/leptris/xml/namespace.rb +43 -0
- data/lib/leptris/xml/node.rb +221 -0
- data/lib/leptris/xml/node_set.rb +143 -0
- data/lib/leptris/xml/parse_options.rb +19 -0
- data/lib/leptris/xml/processing_instruction.rb +26 -0
- data/lib/leptris/xml/sax/document.rb +45 -0
- data/lib/leptris/xml/sax/parser.rb +148 -0
- data/lib/leptris/xml/sax.rb +12 -0
- data/lib/leptris/xml/searchable.rb +93 -0
- data/lib/leptris/xml/text.rb +16 -0
- data/lib/leptris/xml.rb +39 -0
- data/lib/leptris.rb +7 -0
- metadata +157 -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,202 @@
|
|
|
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 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 Leptris::XML::ParseError, "empty input"
|
|
31
|
+
end
|
|
32
|
+
status_ptr = ::FFI::MemoryPointer.new(:int)
|
|
33
|
+
raw = Leptris::XML::FFI.leptris_parse_string(xml, xml.bytesize, status_ptr)
|
|
34
|
+
if raw.null?
|
|
35
|
+
status = status_ptr.read_int
|
|
36
|
+
raise Leptris::XML::ParseError,
|
|
37
|
+
"leptris_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 = Leptris::XML::FFI.leptris_parse_file(path, status_ptr)
|
|
45
|
+
if raw.null?
|
|
46
|
+
status = status_ptr.read_int
|
|
47
|
+
raise Leptris::XML::ParseError,
|
|
48
|
+
"leptris_parse_file failed (status=#{status})"
|
|
49
|
+
end
|
|
50
|
+
wrap(raw)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Convert a raw LeptrisDocument 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
|
+
# `leptris_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
|
+
Leptris::XML::FFI.leptris_document_free(::FFI::Pointer.new(address))
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
private_class_method :finalizer
|
|
76
|
+
|
|
77
|
+
def root
|
|
78
|
+
raise Leptris::XML::UseAfterFreeError if @freed.state == :freed
|
|
79
|
+
return nil if @c_ptr.nil?
|
|
80
|
+
ptr = Leptris::XML::FFI.leptris_document_root(@c_ptr)
|
|
81
|
+
return nil if ptr.null?
|
|
82
|
+
Leptris::XML::Element.new(ptr, self)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def create_element(name)
|
|
86
|
+
ptr = Leptris::XML::FFI.leptris_element_create(@c_ptr, name)
|
|
87
|
+
raise Leptris::XML::Error, "leptris_element_create failed" if ptr.null?
|
|
88
|
+
Leptris::XML::Element.new(ptr, self)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def create_text_node(content)
|
|
92
|
+
ptr = Leptris::XML::FFI.leptris_text_node_create(@c_ptr, content.to_s)
|
|
93
|
+
raise Leptris::XML::Error, "leptris_text_node_create failed" if ptr.null?
|
|
94
|
+
Leptris::XML::Text.new(ptr, self)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def create_comment(content)
|
|
98
|
+
ptr = Leptris::XML::FFI.leptris_comment_node_create(@c_ptr, content.to_s)
|
|
99
|
+
raise Leptris::XML::Error, "leptris_comment_node_create failed" if ptr.null?
|
|
100
|
+
Leptris::XML::Comment.new(ptr, self)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def create_cdata(content)
|
|
104
|
+
ptr = Leptris::XML::FFI.leptris_cdata_node_create(@c_ptr, content.to_s)
|
|
105
|
+
raise Leptris::XML::Error, "leptris_cdata_node_create failed" if ptr.null?
|
|
106
|
+
Leptris::XML::CDATA.new(ptr, self)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def create_processing_instruction(target, data = "")
|
|
110
|
+
ptr = Leptris::XML::FFI.leptris_pi_node_create(@c_ptr, target.to_s, data.to_s)
|
|
111
|
+
raise Leptris::XML::Error, "leptris_pi_node_create failed" if ptr.null?
|
|
112
|
+
Leptris::XML::ProcessingInstruction.new(ptr, self)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def fragment(markup)
|
|
116
|
+
Leptris::XML::DocumentFragment.parse(markup, self)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def dup
|
|
120
|
+
raw = Leptris::XML::FFI.leptris_document_copy(@c_ptr)
|
|
121
|
+
raise Leptris::XML::Error, "leptris_document_copy failed" if raw.null?
|
|
122
|
+
self.class.wrap(raw)
|
|
123
|
+
end
|
|
124
|
+
alias_method :clone, :dup
|
|
125
|
+
|
|
126
|
+
def doctype
|
|
127
|
+
ptr = Leptris::XML::FFI.leptris_document_internal_subset(@c_ptr)
|
|
128
|
+
return nil if ptr.null?
|
|
129
|
+
Leptris::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 Leptris::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 = Leptris::XML::FFI.leptris_document_serialize(@c_ptr, opts.pointer)
|
|
138
|
+
return "" if str_ptr.null?
|
|
139
|
+
str_ptr.read_string.tap { |s| Leptris::XML::FFI.leptris_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 = Leptris::XML::FFI.leptris_document_save_file(@c_ptr, path, opts_struct.pointer)
|
|
150
|
+
raise Leptris::XML::Error,
|
|
151
|
+
Leptris::XML::FFI.leptris_status_string(status) unless status == Leptris::XML::FFI::LEPTRIS_OK
|
|
152
|
+
self
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def canonicalize(version = Leptris::XML::FFI::C14N_1_0,
|
|
156
|
+
inclusive_namespaces = nil,
|
|
157
|
+
with_comments: false,
|
|
158
|
+
exclusive: false,
|
|
159
|
+
mode: nil)
|
|
160
|
+
raise Leptris::XML::UseAfterFreeError if @freed.state == :freed
|
|
161
|
+
return "" if @c_ptr.nil?
|
|
162
|
+
resolved_mode = mode || (exclusive ? Leptris::XML::FFI::C14N_MODE_EXCLUSIVE
|
|
163
|
+
: Leptris::XML::FFI::C14N_MODE_CANONICAL)
|
|
164
|
+
ns_ptr, _anchor = Leptris::XML.c14n_build_ns_pointer(inclusive_namespaces)
|
|
165
|
+
flags = with_comments ? 1 : 0
|
|
166
|
+
str_ptr = Leptris::XML::FFI.leptris_c14n_canonicalize_ex(
|
|
167
|
+
@c_ptr, version, resolved_mode, ns_ptr, flags)
|
|
168
|
+
return "" if str_ptr.null?
|
|
169
|
+
str_ptr.read_string.tap { Leptris::XML::FFI.leptris_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
|
+
Leptris::XML::FFI.leptris_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
|
+
Leptris::XML::FFI.leptris_document_encoding(@c_ptr)
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
private
|
|
188
|
+
|
|
189
|
+
def build_serialize_options(indent:, no_decl:, encoding:)
|
|
190
|
+
opts = Leptris::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 Leptris::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
|
+
# 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,278 @@
|
|
|
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
|
+
status = Leptris::XML::FFI.leptris_element_set_name(@c_ptr, new_name)
|
|
11
|
+
raise Leptris::XML::Error,
|
|
12
|
+
Leptris::XML::FFI.leptris_status_string(status) unless status == Leptris::XML::FFI::LEPTRIS_OK
|
|
13
|
+
new_name
|
|
14
|
+
end
|
|
15
|
+
alias_method :node_name=, :name=
|
|
16
|
+
|
|
17
|
+
def content
|
|
18
|
+
Leptris::XML::FFI.leptris_element_text(@c_ptr)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def content=(new_content)
|
|
22
|
+
status = Leptris::XML::FFI.leptris_element_set_text(@c_ptr, new_content.to_s)
|
|
23
|
+
raise Leptris::XML::Error,
|
|
24
|
+
Leptris::XML::FFI.leptris_status_string(status) unless status == Leptris::XML::FFI::LEPTRIS_OK
|
|
25
|
+
new_content
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def [](key)
|
|
29
|
+
Leptris::XML::FFI.leptris_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 = Leptris::XML::FFI.leptris_element_set_attribute(@c_ptr, key.to_s, value.to_s)
|
|
36
|
+
raise Leptris::XML::Error,
|
|
37
|
+
Leptris::XML::FFI.leptris_status_string(status) unless status == Leptris::XML::FFI::LEPTRIS_OK
|
|
38
|
+
value
|
|
39
|
+
end
|
|
40
|
+
alias_method :set_attribute, :[]=
|
|
41
|
+
|
|
42
|
+
def key?(name)
|
|
43
|
+
!Leptris::XML::FFI.leptris_element_attribute(@c_ptr, name.to_s).nil?
|
|
44
|
+
end
|
|
45
|
+
alias_method :has_attribute?, :key?
|
|
46
|
+
|
|
47
|
+
def remove_attribute(name)
|
|
48
|
+
status = Leptris::XML::FFI.leptris_element_remove_attribute(@c_ptr, name.to_s)
|
|
49
|
+
raise Leptris::XML::Error,
|
|
50
|
+
Leptris::XML::FFI.leptris_status_string(status) unless status == Leptris::XML::FFI::LEPTRIS_OK
|
|
51
|
+
self
|
|
52
|
+
end
|
|
53
|
+
alias_method :delete, :remove_attribute
|
|
54
|
+
|
|
55
|
+
def keys
|
|
56
|
+
count = Leptris::XML::FFI.leptris_element_attribute_count(@c_ptr)
|
|
57
|
+
count.times.map { |i| Leptris::XML::FFI.leptris_element_attribute_name_at(@c_ptr, i) }
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def values
|
|
61
|
+
count = Leptris::XML::FFI.leptris_element_attribute_count(@c_ptr)
|
|
62
|
+
count.times.map { |i| Leptris::XML::FFI.leptris_element_attribute_value_at(@c_ptr, i) }
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def attributes
|
|
66
|
+
count = Leptris::XML::FFI.leptris_element_attribute_count(@c_ptr)
|
|
67
|
+
result = {}
|
|
68
|
+
count.times do |i|
|
|
69
|
+
name = Leptris::XML::FFI.leptris_element_attribute_name_at(@c_ptr, i)
|
|
70
|
+
value = Leptris::XML::FFI.leptris_element_attribute_value_at(@c_ptr, i)
|
|
71
|
+
result[name] = Leptris::XML::Attr.new(name, value, self)
|
|
72
|
+
end
|
|
73
|
+
result
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def attribute_nodes
|
|
77
|
+
count = Leptris::XML::FFI.leptris_element_attribute_count(@c_ptr)
|
|
78
|
+
count.times.map do |i|
|
|
79
|
+
name = Leptris::XML::FFI.leptris_element_attribute_name_at(@c_ptr, i)
|
|
80
|
+
value = Leptris::XML::FFI.leptris_element_attribute_value_at(@c_ptr, i)
|
|
81
|
+
Leptris::XML::Attr.new(name, value, self)
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def add_child(node)
|
|
86
|
+
status = Leptris::XML::FFI.leptris_element_append_child(@c_ptr, node.c_ptr)
|
|
87
|
+
raise Leptris::XML::Error,
|
|
88
|
+
Leptris::XML::FFI.leptris_status_string(status) unless status == Leptris::XML::FFI::LEPTRIS_OK
|
|
89
|
+
node
|
|
90
|
+
end
|
|
91
|
+
alias_method :<<, :add_child
|
|
92
|
+
|
|
93
|
+
def prepend_child(node)
|
|
94
|
+
status = Leptris::XML::FFI.leptris_element_prepend_child(@c_ptr, node.c_ptr)
|
|
95
|
+
raise Leptris::XML::Error,
|
|
96
|
+
Leptris::XML::FFI.leptris_status_string(status) unless status == Leptris::XML::FFI::LEPTRIS_OK
|
|
97
|
+
node
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def add_next_sibling(node)
|
|
101
|
+
status = Leptris::XML::FFI.leptris_element_insert_after(@c_ptr, node.c_ptr)
|
|
102
|
+
raise Leptris::XML::Error,
|
|
103
|
+
Leptris::XML::FFI.leptris_status_string(status) unless status == Leptris::XML::FFI::LEPTRIS_OK
|
|
104
|
+
node
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def add_previous_sibling(node)
|
|
108
|
+
status = Leptris::XML::FFI.leptris_element_insert_before(@c_ptr, node.c_ptr)
|
|
109
|
+
raise Leptris::XML::Error,
|
|
110
|
+
Leptris::XML::FFI.leptris_status_string(status) unless status == Leptris::XML::FFI::LEPTRIS_OK
|
|
111
|
+
node
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def remove_child(node)
|
|
115
|
+
status = Leptris::XML::FFI.leptris_element_remove_child(@c_ptr, node.c_ptr)
|
|
116
|
+
raise Leptris::XML::Error,
|
|
117
|
+
Leptris::XML::FFI.leptris_status_string(status) unless status == Leptris::XML::FFI::LEPTRIS_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
|
+
Leptris::XML::FFI.leptris_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 Leptris::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 Leptris::XML::Element then node_or_markup.dup
|
|
150
|
+
when String
|
|
151
|
+
frag_doc = Leptris::XML::Document.parse(node_or_markup)
|
|
152
|
+
frag_doc.root or raise Leptris::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 Leptris::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 (libleptris 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 = Leptris::XML::FFI.leptris_element_copy(@c_ptr, @document.c_ptr)
|
|
171
|
+
raise Leptris::XML::Error, "leptris_element_copy failed" if copy_ptr.null?
|
|
172
|
+
Leptris::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 Leptris::XML::Node
|
|
179
|
+
status = Leptris::XML::FFI.leptris_element_append_child(@c_ptr, node_or_markup.c_ptr)
|
|
180
|
+
raise Leptris::XML::Error,
|
|
181
|
+
Leptris::XML::FFI.leptris_status_string(status) unless status == Leptris::XML::FFI::LEPTRIS_OK
|
|
182
|
+
node_or_markup
|
|
183
|
+
when String
|
|
184
|
+
frag = Leptris::XML::DocumentFragment.parse(node_or_markup, @document)
|
|
185
|
+
added = []
|
|
186
|
+
frag.children.each do |n|
|
|
187
|
+
status = Leptris::XML::FFI.leptris_element_append_child(@c_ptr, n.c_ptr)
|
|
188
|
+
raise Leptris::XML::Error,
|
|
189
|
+
Leptris::XML::FFI.leptris_status_string(status) unless status == Leptris::XML::FFI::LEPTRIS_OK
|
|
190
|
+
added << n
|
|
191
|
+
end
|
|
192
|
+
Leptris::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 = Leptris::XML::FFI.leptris_element_namespace(@c_ptr)
|
|
200
|
+
return nil if uri.nil? || uri.empty?
|
|
201
|
+
Leptris::XML::Namespace.new(self, uri)
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def namespace_definitions
|
|
205
|
+
count = Leptris::XML::FFI.leptris_element_namespace_count(@c_ptr)
|
|
206
|
+
count.times.map do |i|
|
|
207
|
+
prefix = Leptris::XML::FFI.leptris_element_namespace_decl_prefix(@c_ptr, i)
|
|
208
|
+
uri = Leptris::XML::FFI.leptris_element_namespace_decl_uri(@c_ptr, i)
|
|
209
|
+
Leptris::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?(Leptris::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 = Leptris::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 = Leptris::XML::FFI.leptris_element_serialize(@c_ptr, opts.pointer)
|
|
236
|
+
return "" if str_ptr.null?
|
|
237
|
+
str_ptr.read_string.tap { Leptris::XML::FFI.leptris_free_string(str_ptr) }
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
def canonicalize(version = Leptris::XML::FFI::C14N_1_0,
|
|
241
|
+
inclusive_namespaces = nil,
|
|
242
|
+
with_comments: false,
|
|
243
|
+
exclusive: false,
|
|
244
|
+
mode: nil)
|
|
245
|
+
resolved_mode = mode || (exclusive ? Leptris::XML::FFI::C14N_MODE_EXCLUSIVE
|
|
246
|
+
: Leptris::XML::FFI::C14N_MODE_CANONICAL)
|
|
247
|
+
ns_ptr, _anchor = Leptris::XML.c14n_build_ns_pointer(inclusive_namespaces)
|
|
248
|
+
flags = with_comments ? 1 : 0
|
|
249
|
+
str_ptr = Leptris::XML::FFI.leptris_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 { Leptris::XML::FFI.leptris_free_string(str_ptr) }
|
|
253
|
+
end
|
|
254
|
+
alias_method :c14n, :canonicalize
|
|
255
|
+
|
|
256
|
+
def add_namespace_definition(prefix, href)
|
|
257
|
+
status = Leptris::XML::FFI.leptris_element_add_namespace_definition(
|
|
258
|
+
@c_ptr, prefix.to_s, href.to_s)
|
|
259
|
+
raise Leptris::XML::Error,
|
|
260
|
+
Leptris::XML::FFI.leptris_status_string(status) unless status == Leptris::XML::FFI::LEPTRIS_OK
|
|
261
|
+
Leptris::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 = Leptris::XML::FFI.leptris_element_set_default_namespace(@c_ptr, href.to_s)
|
|
267
|
+
raise Leptris::XML::Error,
|
|
268
|
+
Leptris::XML::FFI.leptris_status_string(status) unless status == Leptris::XML::FFI::LEPTRIS_OK
|
|
269
|
+
href
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
def remove_namespace_definition(prefix)
|
|
273
|
+
status = Leptris::XML::FFI.leptris_element_remove_namespace_definition(@c_ptr, prefix.to_s)
|
|
274
|
+
raise Leptris::XML::Error,
|
|
275
|
+
Leptris::XML::FFI.leptris_status_string(status) unless status == Leptris::XML::FFI::LEPTRIS_OK
|
|
276
|
+
self
|
|
277
|
+
end
|
|
278
|
+
end
|