leptris 1.9.197.2-arm-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/CHANGELOG.md +3890 -0
- data/CLAUDE.md +115 -0
- data/CONTEXT.md +39 -0
- data/LICENSE.md +33 -0
- data/README.adoc +755 -0
- data/Rakefile +407 -0
- data/TODO.perf/01-ext-accelerated-default-reads.md +28 -0
- data/TODO.perf/02-auto-enable-native-layer.md +19 -0
- data/TODO.perf/03-bulk-xpath-result-materialization.md +18 -0
- data/TODO.perf/04-ext-bound-serialization.md +15 -0
- data/TODO.perf/05-native-surface-completion.md +19 -0
- data/TODO.perf/06-perf-battery-native-legs.md +16 -0
- data/TODO.perf/07-native-read-floor.md +31 -0
- data/TODO.perf/08-version-coherent-native-mutations.md +24 -0
- data/TODO.perf/09-adoption-lift-fast-path.md +38 -0
- data/TODO.perf/10-bulk-attribute-materialization.md +20 -0
- data/TODO.perf/11-moxml-gap-battery.md +18 -0
- data/TODO.perf/12-document-lifetime-in-c.md +47 -0
- data/TODO.perf/13-structural-memos.md +49 -0
- data/TODO.perf/14-insert-family-c-bound.md +31 -0
- data/TODO.perf/15-compiled-expression-cache.md +35 -0
- data/TODO.perf/16-at-xpath-single-result-seam.md +38 -0
- data/TODO.perf/17-precomputed-fast-path-flags.md +35 -0
- data/TODO.perf/18-inner-html-one-c-pass.md +32 -0
- data/TODO.perf/19-lazy-node-pointer.md +39 -0
- data/TODO.perf/20-css-translation-cache.md +24 -0
- data/TODO.perf/21-key-memo-consult.md +20 -0
- data/TODO.perf/22-eager-nodeset-materialization.md +30 -0
- data/TODO.perf/23-cbound-value-mutations.md +27 -0
- data/TODO.perf/24-immutable-read-lanes.md +22 -0
- data/TODO.perf/25-scope-owned-bulk-path.md +39 -0
- data/TODO.perf/26-post-mutation-memo-seeding.md +26 -0
- data/TODO.perf/27-c-yield-traversal.md +38 -0
- data/TODO.perf/28-address-fills.md +25 -0
- data/TODO.perf/29-ns-xpath-compiled.md +23 -0
- data/TODO.perf/30-copy-and-element-child-faces.md +31 -0
- data/TODO.perf/31-battery-rounds-4-8.md +23 -0
- data/TODO.perf/32-clean-host-floor-table.md +29 -0
- data/TODO.perf/33-cbound-root-set.md +29 -0
- data/TODO.perf/34-fragment-fast-lane.md +41 -0
- data/TODO.perf/35-parse-default-c-face.md +24 -0
- data/TODO.perf/36-raise-in-c-serializer-encoding.md +34 -0
- data/TODO.perf/37-document-lazy-pointer.md +28 -0
- data/TODO.perf/38-bulk-walk-and-bench-gate.md +23 -0
- data/TODO.restructure/01-constraint-compliance-audit.md +40 -0
- data/TODO.restructure/02-deep-copy-seam.md +21 -0
- data/TODO.restructure/03-evaluation-context-seam.md +18 -0
- data/TODO.restructure/04-spec-mece-restructure.md +20 -0
- data/TODO.restructure/05-memory-documentation.md +13 -0
- data/TODO.restructure/06-architecture-map-refresh.md +12 -0
- data/TODO.restructure/07-external-gates.md +16 -0
- data/TODO.restructure/08-new-code-audit-and-ext-memo.md +11 -0
- data/TODO.restructure/09-iteration-scope.md +29 -0
- data/TODO.restructure/10-cold-attr-read-diet.md +19 -0
- data/TODO.restructure/11-construction-factory-diet.md +16 -0
- data/TODO.restructure/12-resultattr-merge.md +12 -0
- data/TODO.restructure/13-spec-mece-followup.md +9 -0
- data/TODO.restructure/14-constraint-audit-refresh.md +15 -0
- data/TODO.restructure/15-readme-feature-documentation.md +10 -0
- data/TODO.restructure/16-claudemd-map-refresh.md +5 -0
- data/TODO.restructure/17-perf-drift-record.md +12 -0
- data/TODO.restructure/18-audit-spec-doubles-and-gem-contents.md +12 -0
- data/TODO.restructure/19-gitignore-hygiene.md +10 -0
- data/TODO.restructure/20-utf8proc-enablement.md +34 -0
- data/TODO.restructure/21-typeddata-variant-prototype.md +42 -0
- data/TODO.restructure/22-sax-drain-bulk-strip.md +28 -0
- data/docs/adr/0001-lockstep-mirror.md +17 -0
- data/docs/adr/0002-utf8-at-the-seam.md +19 -0
- data/docs/adr/0003-readonly-memoization-pattern.md +34 -0
- data/docs/adr/0004-lifetime-guard.md +21 -0
- data/docs/adr/0005-autoload-manifest-ordering.md +19 -0
- data/docs/adr/0006-ruby-variant-policy.md +25 -0
- data/ext/build_windows_native.rb +42 -0
- data/ext/leptris/native/extconf.rb +55 -0
- data/ext/leptris/native/native.c +2646 -0
- data/leptris.gemspec +43 -0
- data/lib/leptris/version.rb +5 -0
- data/lib/leptris/xml/attr.rb +76 -0
- data/lib/leptris/xml/c_string_array.rb +37 -0
- data/lib/leptris/xml/cdata.rb +27 -0
- data/lib/leptris/xml/comment.rb +27 -0
- data/lib/leptris/xml/css_to_xpath.rb +192 -0
- data/lib/leptris/xml/descriptor.rb +214 -0
- data/lib/leptris/xml/diff.rb +74 -0
- data/lib/leptris/xml/doc_type.rb +54 -0
- data/lib/leptris/xml/document.rb +716 -0
- data/lib/leptris/xml/document_fragment.rb +63 -0
- data/lib/leptris/xml/element.rb +771 -0
- data/lib/leptris/xml/entity_reference.rb +13 -0
- data/lib/leptris/xml/evaluation_context.rb +32 -0
- data/lib/leptris/xml/ffi.rb +1527 -0
- data/lib/leptris/xml/iteration_scope.rb +56 -0
- data/lib/leptris/xml/iterparse.rb +144 -0
- data/lib/leptris/xml/namespace.rb +43 -0
- data/lib/leptris/xml/native.so +0 -0
- data/lib/leptris/xml/native_layer.rb +92 -0
- data/lib/leptris/xml/node.rb +686 -0
- data/lib/leptris/xml/node_set.rb +212 -0
- data/lib/leptris/xml/parse_options.rb +97 -0
- data/lib/leptris/xml/plan_value.rb +121 -0
- data/lib/leptris/xml/processing_instruction.rb +90 -0
- data/lib/leptris/xml/pull.rb +212 -0
- data/lib/leptris/xml/relaxng.rb +126 -0
- data/lib/leptris/xml/result_attr.rb +39 -0
- data/lib/leptris/xml/result_text.rb +36 -0
- data/lib/leptris/xml/sax/document.rb +45 -0
- data/lib/leptris/xml/sax/dom_dispatch.rb +144 -0
- data/lib/leptris/xml/sax/parser.rb +326 -0
- data/lib/leptris/xml/sax/recorder.rb +374 -0
- data/lib/leptris/xml/sax.rb +14 -0
- data/lib/leptris/xml/schematron.rb +88 -0
- data/lib/leptris/xml/searchable.rb +361 -0
- data/lib/leptris/xml/serialization.rb +180 -0
- data/lib/leptris/xml/text.rb +32 -0
- data/lib/leptris/xml/xpath.rb +89 -0
- data/lib/leptris/xml/xquery.rb +77 -0
- data/lib/leptris/xml/xslt.rb +85 -0
- data/lib/leptris/xml.rb +108 -0
- data/lib/leptris.rb +53 -0
- data/lib/libleptris.so +0 -0
- data/lib/libutf8proc.so.3 +0 -0
- data/scripts/gem_smoke.rb +36 -0
- metadata +214 -0
|
@@ -0,0 +1,716 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ffi"
|
|
4
|
+
|
|
5
|
+
class Leptris::XML::Document
|
|
6
|
+
|
|
7
|
+
# @api private
|
|
8
|
+
# Internal flag container shared between the Document instance and its
|
|
9
|
+
# GC finalizer. Using a one-element Array because Procs close over
|
|
10
|
+
# variables by reference — mutating freed[0] is visible from both
|
|
11
|
+
# the explicit `free` path and the finalizer. This eliminates the
|
|
12
|
+
# double-free that FFI::AutoPointer's release proc caused when
|
|
13
|
+
# `Document#free` was called explicitly and then GC ran.
|
|
14
|
+
Freed = Struct.new(:state) # state: :alive | :freed
|
|
15
|
+
|
|
16
|
+
# Mutation version: advanced by every data mutation (via
|
|
17
|
+
# Node#ensure_writable!, root=, add_pi). Node memos stamp the
|
|
18
|
+
# version they were computed under and recompute after any bump —
|
|
19
|
+
# the invalidation that makes WRITABLE-document memoization sound.
|
|
20
|
+
# Readonly documents never advance it, so their memos are forever
|
|
21
|
+
# valid (ADR 0003 semantics, unchanged).
|
|
22
|
+
def version
|
|
23
|
+
@version
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Called by the mutation gates (Node#ensure_writable!, root=,
|
|
27
|
+
# add_pi) — every memo stamped with an older version discards.
|
|
28
|
+
def advance_version
|
|
29
|
+
@version += 1
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def initialize(c_ptr = nil, freed = Freed.new(:alive))
|
|
33
|
+
@c_ptr = c_ptr
|
|
34
|
+
# Plain-Integer address twin of c_ptr: the native layer reads
|
|
35
|
+
# this ivar directly (no method dispatch), nil'ed exactly when
|
|
36
|
+
# c_ptr is.
|
|
37
|
+
@c_address = c_ptr&.address
|
|
38
|
+
@freed = freed
|
|
39
|
+
@readonly = false
|
|
40
|
+
@version = 0
|
|
41
|
+
# Per-document STRONG cache for Node wrappers, keyed on c_ptr
|
|
42
|
+
# address. Every wrapper is created through Node.wrap, which is the
|
|
43
|
+
# single construction path, so the same C node always yields the
|
|
44
|
+
# same Ruby object. Cleared when the Document is freed — no stale
|
|
45
|
+
# entries.
|
|
46
|
+
#
|
|
47
|
+
# Deliberately NOT ObjectSpace::WeakMap: a weak cache makes wrapper
|
|
48
|
+
# identity a GC race. `doc.root.equal?(doc.root)` failed on the
|
|
49
|
+
# Windows CI matrix (188 examples, the 4 identity specs) because
|
|
50
|
+
# between the two calls the first wrapper was referenced only by
|
|
51
|
+
# the weak map — any GC sweep evicted it and the second call built
|
|
52
|
+
# a fresh object. A strong cache costs at most one wrapper per node
|
|
53
|
+
# actually visited, held until the document dies.
|
|
54
|
+
#
|
|
55
|
+
# Allocated lazily: parse-heavy loops stop paying one Hash per
|
|
56
|
+
# document for trees that are freed before any wrap.
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# TODO.perf/37: the Integer address is canonical; the Pointer
|
|
60
|
+
# materializes only when read (parse/create faces skip it).
|
|
61
|
+
# A freed document answers nil — never re-materialize a stale
|
|
62
|
+
# or absent address.
|
|
63
|
+
def c_ptr
|
|
64
|
+
@c_ptr ||= (@c_address && ::FFI::Pointer.new(@c_address))
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# The Integer twin of #c_ptr.
|
|
68
|
+
def c_address
|
|
69
|
+
@c_address
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def wrapper_cache
|
|
73
|
+
@wrapper_cache ||= {}
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# #231: deterministic scope — yields the document, guarantees
|
|
77
|
+
# teardown at block exit (even on raise). Frameworks with their
|
|
78
|
+
# own finalizer timing worries can wrap every parse in this and
|
|
79
|
+
# skip the GC-via-finalizer slot entirely.
|
|
80
|
+
def self.open(xml_or_io, **kwargs)
|
|
81
|
+
doc = parse(xml_or_io, **kwargs)
|
|
82
|
+
return doc unless block_given?
|
|
83
|
+
begin
|
|
84
|
+
yield doc
|
|
85
|
+
ensure
|
|
86
|
+
doc.free
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# #230: bulk hydration walks — one C pass per subtree, no per-node
|
|
91
|
+
# Ruby wrapper construction. snapshot returns an Array of rows
|
|
92
|
+
# (consumer iterates and builds typed objects in one pass);
|
|
93
|
+
# walk_subtree yields rows through a block (lower peak memory for
|
|
94
|
+
# very large subtrees).
|
|
95
|
+
def snapshot(node)
|
|
96
|
+
if defined?(Leptris::XML::NATIVE_FAST)
|
|
97
|
+
Leptris::XML::Native.snapshot_subtree(
|
|
98
|
+
self, node.respond_to?(:c_address) ? node.c_address : nil)
|
|
99
|
+
else
|
|
100
|
+
rows = []
|
|
101
|
+
walk_subtree(node) { |row| rows << row }
|
|
102
|
+
rows
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def walk_subtree(node, &block)
|
|
107
|
+
return enum_for(:walk_subtree, node) unless block
|
|
108
|
+
if defined?(Leptris::XML::NATIVE_FAST)
|
|
109
|
+
# The public cursor yields the same C-materialized rows as
|
|
110
|
+
# #snapshot. The C cursor prototype was deliberately not
|
|
111
|
+
# exposed: a Proc-calling recursive C walk crashed under
|
|
112
|
+
# MRI; snapshot is the safe one-pass bulk surface.
|
|
113
|
+
snapshot(node).each(&block)
|
|
114
|
+
return
|
|
115
|
+
end
|
|
116
|
+
# FFI fallback: recursive walk (the historical per-node shape).
|
|
117
|
+
if node.is_a?(Leptris::XML::Node)
|
|
118
|
+
yield(element_row(node)) if node.element?
|
|
119
|
+
node.element_children.each { |c| walk_subtree(c, &block) }
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def element_row(el)
|
|
124
|
+
{ kind: "element", name: el.name, prefix: el.namespace&.prefix,
|
|
125
|
+
uri: el.namespace&.href, attrs: el.attributes,
|
|
126
|
+
text: el.element_children.find { |c| c.text? }&.content,
|
|
127
|
+
depth: 0 }
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def self.parse(xml_or_io, options: nil, readonly: false, recover: false)
|
|
131
|
+
xml = xml_or_io.is_a?(String) ? xml_or_io : xml_or_io.read
|
|
132
|
+
if xml.empty?
|
|
133
|
+
raise Leptris::XML::ParseError, "empty input"
|
|
134
|
+
end
|
|
135
|
+
# The default path (no options, no recover) allocates nothing:
|
|
136
|
+
# ParseOptions.new here would only discover flags==0 (#187 —
|
|
137
|
+
# per-call overhead is most of a small-document parse).
|
|
138
|
+
if options.nil? && !recover
|
|
139
|
+
# TODO.perf/35: parse + wrapper + lifetime handle in one C
|
|
140
|
+
# dispatch; failure raises through the shared path below.
|
|
141
|
+
if defined?(Leptris::XML::NATIVE_FAST)
|
|
142
|
+
doc = Leptris::XML::Native.parse_binding_document(xml)
|
|
143
|
+
raise Leptris::XML::ParseError,
|
|
144
|
+
"leptris_parse_string failed: " +
|
|
145
|
+
Leptris::XML::FFI.leptris_last_error.to_s if doc.nil?
|
|
146
|
+
return doc.tap { |d| d.readonly! if readonly }
|
|
147
|
+
end
|
|
148
|
+
raw = Leptris::XML::FFI.leptris_parse_string(xml, xml.bytesize, nil)
|
|
149
|
+
if raw.null?
|
|
150
|
+
raise Leptris::XML::ParseError,
|
|
151
|
+
"leptris_parse_string failed: " +
|
|
152
|
+
Leptris::XML::FFI.leptris_last_error.to_s
|
|
153
|
+
end
|
|
154
|
+
return wrap(raw).tap { |doc| doc.readonly! if readonly }
|
|
155
|
+
end
|
|
156
|
+
if options.nil?
|
|
157
|
+
options = Leptris::XML::ParseOptions.new(recover: recover)
|
|
158
|
+
elsif recover && !options.recover?
|
|
159
|
+
options = options | Leptris::XML::ParseOptions.new(recover: true)
|
|
160
|
+
elsif !options.is_a?(Leptris::XML::ParseOptions)
|
|
161
|
+
raise ArgumentError, "options must be a Leptris::XML::ParseOptions"
|
|
162
|
+
end
|
|
163
|
+
# The status out-param is nullable; the thread-local last error
|
|
164
|
+
# carries failure detail, and skipping the per-parse MemoryPointer
|
|
165
|
+
# is measurable on small documents.
|
|
166
|
+
raw =
|
|
167
|
+
if options.struct_required?
|
|
168
|
+
# Recover is a struct field, not a parse flag — the options
|
|
169
|
+
# struct path (leptris_parse_string_ex) is the only carrier.
|
|
170
|
+
options_struct = options.to_c_struct
|
|
171
|
+
Leptris::XML::FFI.leptris_parse_string_ex(
|
|
172
|
+
xml, xml.bytesize, options_struct.pointer, nil)
|
|
173
|
+
elsif options.flags.zero?
|
|
174
|
+
Leptris::XML::FFI.leptris_parse_string(xml, xml.bytesize, nil)
|
|
175
|
+
else
|
|
176
|
+
Leptris::XML::FFI.leptris_parse_string_flags(
|
|
177
|
+
xml, xml.bytesize, options.flags, nil)
|
|
178
|
+
end
|
|
179
|
+
if raw.null?
|
|
180
|
+
if options.recover?
|
|
181
|
+
# Unreachable in practice: recover returns an empty document
|
|
182
|
+
# rather than NULL; kept so a contract change fails loudly.
|
|
183
|
+
raise Leptris::XML::Error,
|
|
184
|
+
"leptris_parse_string_ex returned NULL under recover"
|
|
185
|
+
end
|
|
186
|
+
raise Leptris::XML::ParseError,
|
|
187
|
+
"leptris_parse_string failed: " +
|
|
188
|
+
Leptris::XML::FFI.leptris_last_error.to_s
|
|
189
|
+
end
|
|
190
|
+
wrap(raw).tap { |doc| doc.readonly! if readonly }
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def self.parse_file(path, readonly: false)
|
|
194
|
+
raw = Leptris::XML::FFI.leptris_parse_file(path, nil)
|
|
195
|
+
if raw.null?
|
|
196
|
+
raise Leptris::XML::ParseError,
|
|
197
|
+
"leptris_parse_file failed: " +
|
|
198
|
+
Leptris::XML::FFI.leptris_last_error.to_s
|
|
199
|
+
end
|
|
200
|
+
wrap(raw).tap { |doc| doc.readonly! if readonly }
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
# Create an empty document (no root element) backed by its own memory
|
|
204
|
+
# pool. Elements for the tree are created against it via
|
|
205
|
+
# #create_element and friends, then attached with #root=.
|
|
206
|
+
def self.create
|
|
207
|
+
# One C dispatch (TODO.perf/12): engine create + wrapper
|
|
208
|
+
# (ivar-seeded) + lifetime handle. The FFI+wrap shape stays
|
|
209
|
+
# for LEPTRIS_NO_NATIVE.
|
|
210
|
+
if defined?(Leptris::XML::NATIVE_FAST)
|
|
211
|
+
doc = Leptris::XML::Native.create_binding_document
|
|
212
|
+
raise Leptris::XML::Error,
|
|
213
|
+
"leptris_document_create failed" if doc.nil?
|
|
214
|
+
return doc
|
|
215
|
+
end
|
|
216
|
+
raw = Leptris::XML::FFI.leptris_document_create
|
|
217
|
+
raise Leptris::XML::Error,
|
|
218
|
+
"leptris_document_create failed" if raw.null?
|
|
219
|
+
wrap(raw)
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
# Deep copy of +node+ in a NEW document — the one authority
|
|
223
|
+
# behind Node#dup / Element#dup and the indent-unit path.
|
|
224
|
+
# Elements deep-copy through the C copier (every child kind and
|
|
225
|
+
# namespace survives, #696/#721/#812); every other node kind
|
|
226
|
+
# rebuilds by value through this document's factories — their
|
|
227
|
+
# entire state is the payload the factory takes (#161). The
|
|
228
|
+
# returned node is detached from the original and fully usable.
|
|
229
|
+
def self.copy_of(node)
|
|
230
|
+
new_doc = create
|
|
231
|
+
return rebuild_node(node, new_doc) unless node.element?
|
|
232
|
+
copy = Leptris::XML::FFI.leptris_element_copy(node.c_ptr, new_doc.c_ptr)
|
|
233
|
+
raise Leptris::XML::Error, "leptris_element_copy failed" if copy.null?
|
|
234
|
+
new_doc.root = Leptris::XML::Node.wrap(copy, new_doc)
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
def self.rebuild_node(node, new_doc)
|
|
238
|
+
case node.type
|
|
239
|
+
when Leptris::XML::FFI::NODE_TEXT
|
|
240
|
+
new_doc.create_text_node(node.content)
|
|
241
|
+
when Leptris::XML::FFI::NODE_CDATA
|
|
242
|
+
new_doc.create_cdata(node.content)
|
|
243
|
+
when Leptris::XML::FFI::NODE_COMMENT
|
|
244
|
+
new_doc.create_comment(node.content)
|
|
245
|
+
when Leptris::XML::FFI::NODE_PI
|
|
246
|
+
new_doc.create_processing_instruction(node.name, node.content)
|
|
247
|
+
else
|
|
248
|
+
raise Leptris::XML::Error,
|
|
249
|
+
"dup is not supported for #{node.class} nodes"
|
|
250
|
+
end
|
|
251
|
+
end
|
|
252
|
+
private_class_method :rebuild_node
|
|
253
|
+
|
|
254
|
+
# Convert a raw LeptrisDocument pointer into a Ruby Document with safe
|
|
255
|
+
# GC lifetime management. The finalizer captures the raw address
|
|
256
|
+
# integer (not the Document or Pointer object — those would prevent
|
|
257
|
+
# GC) and shares a one-shot flag with the instance so explicit
|
|
258
|
+
# `#free` and the GC finalizer can never both call
|
|
259
|
+
# `leptris_document_free` on the same address.
|
|
260
|
+
def self.wrap(raw_address)
|
|
261
|
+
addr = raw_address.is_a?(::FFI::Pointer) ? raw_address.address : raw_address
|
|
262
|
+
ptr = ::FFI::Pointer.new(addr)
|
|
263
|
+
freed = Freed.new(:alive)
|
|
264
|
+
doc = new(ptr, freed)
|
|
265
|
+
if defined?(Leptris::XML::NATIVE_FAST)
|
|
266
|
+
# TypedData dfree owns the release at GC (TODO.perf/12): no
|
|
267
|
+
# Ruby finalizer, no FFI dispatch from finalizer context.
|
|
268
|
+
# Freed stays the shared free-state for #freed?/#free.
|
|
269
|
+
Leptris::XML::Native.doc_handle_attach(doc)
|
|
270
|
+
else
|
|
271
|
+
ObjectSpace.define_finalizer(doc, finalizer(addr, freed))
|
|
272
|
+
end
|
|
273
|
+
doc
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
def self.finalizer(address, freed)
|
|
277
|
+
proc do
|
|
278
|
+
next if freed.state == :freed
|
|
279
|
+
freed.state = :freed
|
|
280
|
+
Leptris::XML::FFI.leptris_document_free(::FFI::Pointer.new(address))
|
|
281
|
+
end
|
|
282
|
+
end
|
|
283
|
+
private_class_method :finalizer
|
|
284
|
+
|
|
285
|
+
def root
|
|
286
|
+
raise Leptris::XML::UseAfterFreeError if @freed.state == :freed
|
|
287
|
+
return nil if c_ptr.nil?
|
|
288
|
+
# Version-stamped memo (TODO.perf/13): root changes only
|
|
289
|
+
# through mutations that advance @version (root=, unlink) or
|
|
290
|
+
# #free (which nils c_ptr above) — the entry point of every
|
|
291
|
+
# pipeline stops paying FFI + wrap per call.
|
|
292
|
+
return @root if @root_version == @version
|
|
293
|
+
ptr = Leptris::XML::FFI.leptris_document_root(c_ptr)
|
|
294
|
+
result = ptr.null? ? nil : Leptris::XML::Node.wrap(ptr, self)
|
|
295
|
+
@root = result
|
|
296
|
+
@root_version = @version
|
|
297
|
+
result
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
# The document node — navigation head over the whole tree chain
|
|
301
|
+
# [prolog comments/PIs, root element, epilog comments/PIs] in
|
|
302
|
+
# document order (libleptris 1.9.7, upstream #580: the libxml2
|
|
303
|
+
# model; XPath /comment() and //processing-instruction() see the
|
|
304
|
+
# document-level nodes). A stable, document-owned singleton —
|
|
305
|
+
# Node.wrap's cache keeps the returned wrapper identical.
|
|
306
|
+
def node
|
|
307
|
+
raise Leptris::XML::UseAfterFreeError if @freed.state == :freed
|
|
308
|
+
@node ||= Leptris::XML::Node.wrap(
|
|
309
|
+
Leptris::XML::FFI.leptris_document_node(c_ptr), self)
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
# The document's children, via the document node: prolog
|
|
313
|
+
# comments/PIs, the root element, epilog comments/PIs, in
|
|
314
|
+
# document order (Nokogiri-parity shape).
|
|
315
|
+
#
|
|
316
|
+
# On programmatically built documents the C document node's chain
|
|
317
|
+
# misses a root attached via #root= until some other document
|
|
318
|
+
# mutation refreshes it (leptris-ruby#91 — libleptris's
|
|
319
|
+
# document_set_root does not register into the chain). The merge
|
|
320
|
+
# below splices the attached root in by document order whenever
|
|
321
|
+
# the chain lacks it, so parsed and built documents read the
|
|
322
|
+
# same. Prolog/epilog placement uses node_compare against the
|
|
323
|
+
# root.
|
|
324
|
+
def children
|
|
325
|
+
doc_node = node
|
|
326
|
+
return Leptris::XML::NodeSet.new(self, []) if doc_node.nil?
|
|
327
|
+
kids = doc_node.children.to_a
|
|
328
|
+
root_ptr = Leptris::XML::FFI.leptris_document_root(c_ptr)
|
|
329
|
+
return kids if root_ptr.null?
|
|
330
|
+
return kids if kids.any? { |child| child.c_ptr == root_ptr }
|
|
331
|
+
root = Leptris::XML::Node.wrap(root_ptr, self)
|
|
332
|
+
# A replaced root stays in the C chain until another document
|
|
333
|
+
# mutation refreshes it — the chain's element slots other than
|
|
334
|
+
# the current root are stale old roots. The new root inherits
|
|
335
|
+
# the first stale slot's position (the prolog/epilog split
|
|
336
|
+
# follows where the old root sat); with no stale element
|
|
337
|
+
# (programmatically built documents) placement falls back to
|
|
338
|
+
# document-order comparison.
|
|
339
|
+
stale = kids.select do |child|
|
|
340
|
+
child.element? && child.c_ptr != root_ptr
|
|
341
|
+
end
|
|
342
|
+
if stale.any?
|
|
343
|
+
kept = kids.reject { |child| stale.include?(child) }
|
|
344
|
+
slot = kids.index(stale.first)
|
|
345
|
+
kept.insert(slot, root)
|
|
346
|
+
Leptris::XML::NodeSet.new(self, kept)
|
|
347
|
+
else
|
|
348
|
+
prolog, epilog = kids.partition do |child|
|
|
349
|
+
Leptris::XML::FFI.leptris_node_compare(child.c_ptr, root_ptr).negative?
|
|
350
|
+
end
|
|
351
|
+
Leptris::XML::NodeSet.new(self, prolog + [root] + epilog)
|
|
352
|
+
end
|
|
353
|
+
end
|
|
354
|
+
|
|
355
|
+
# Attach +element+ as the document's root element. The element must
|
|
356
|
+
# have been created against this document and must not already have
|
|
357
|
+
# a parent. Any previous root is left detached (still owned by the
|
|
358
|
+
# document's pool until #free).
|
|
359
|
+
def root=(element)
|
|
360
|
+
raise Leptris::XML::UseAfterFreeError if @freed.state == :freed
|
|
361
|
+
# Same mutation gate as every other writer — the FFI path
|
|
362
|
+
# historically missed it (TODO.perf/33: the C face exposed
|
|
363
|
+
# the divergence; readonly is one-way, so one ivar read).
|
|
364
|
+
if @readonly
|
|
365
|
+
raise Leptris::XML::ReadOnlyError,
|
|
366
|
+
"document is readonly — root= attempted"
|
|
367
|
+
end
|
|
368
|
+
# A document root has no in-scope declarations of its own —
|
|
369
|
+
# lift everything the element's source scope carried (#178).
|
|
370
|
+
unless Leptris::XML::Element.skip_adoption_lift?(element)
|
|
371
|
+
Leptris::XML::Element.lift_namespaces_for_adoption(element, {})
|
|
372
|
+
end
|
|
373
|
+
# TODO.perf/33: gates + bump + engine set_root in one C
|
|
374
|
+
# dispatch (the FFI call plus the c_ptr materialization fed
|
|
375
|
+
# ~5% of a fresh-doc build).
|
|
376
|
+
if defined?(Leptris::XML::NATIVE_FAST)
|
|
377
|
+
Leptris::XML::Native.set_binding_root(self, element.c_address)
|
|
378
|
+
else
|
|
379
|
+
Leptris::XML::FFI.check_status(
|
|
380
|
+
Leptris::XML::FFI.leptris_document_set_root(c_ptr, element.c_ptr))
|
|
381
|
+
@version += 1
|
|
382
|
+
end
|
|
383
|
+
# Seed the root memo through wrap: a cross-document element
|
|
384
|
+
# must enter THIS document's identity cache with @document
|
|
385
|
+
# pointing here, not ride its source-document wrapper.
|
|
386
|
+
@root = Leptris::XML::Node.wrap(element.c_ptr, self)
|
|
387
|
+
@root_version = @version
|
|
388
|
+
Leptris::XML::Node.invalidate_cross_document!(element, self)
|
|
389
|
+
element
|
|
390
|
+
end
|
|
391
|
+
|
|
392
|
+
def create_element(name)
|
|
393
|
+
# TODO.perf/09: one C call creates + wraps when the ext is
|
|
394
|
+
# loaded — no FFI marshaling, no wrap_fresh frames.
|
|
395
|
+
if defined?(Leptris::XML::NATIVE_FAST)
|
|
396
|
+
node = Leptris::XML::Native.create_binding_element(self, name.to_s)
|
|
397
|
+
raise Leptris::XML::Error, "leptris_element_create failed" if node.nil?
|
|
398
|
+
return node
|
|
399
|
+
end
|
|
400
|
+
ptr = Leptris::XML::FFI.leptris_element_create(c_ptr, name)
|
|
401
|
+
raise Leptris::XML::Error, "leptris_element_create failed" if ptr.null?
|
|
402
|
+
Leptris::XML::Node.wrap_fresh(ptr, self, Leptris::XML::FFI::NODE_ELEMENT)
|
|
403
|
+
end
|
|
404
|
+
|
|
405
|
+
def create_text_node(content)
|
|
406
|
+
if defined?(Leptris::XML::NATIVE_FAST)
|
|
407
|
+
node = Leptris::XML::Native.create_binding_text(self, content.to_s)
|
|
408
|
+
raise Leptris::XML::Error, "leptris_text_node_create failed" if node.nil?
|
|
409
|
+
return node
|
|
410
|
+
end
|
|
411
|
+
ptr = Leptris::XML::FFI.leptris_text_node_create(c_ptr, content.to_s)
|
|
412
|
+
raise Leptris::XML::Error, "leptris_text_node_create failed" if ptr.null?
|
|
413
|
+
Leptris::XML::Node.wrap_fresh(ptr, self, Leptris::XML::FFI::NODE_TEXT)
|
|
414
|
+
end
|
|
415
|
+
|
|
416
|
+
def create_comment(content)
|
|
417
|
+
ptr = Leptris::XML::FFI.leptris_comment_node_create(c_ptr, content.to_s)
|
|
418
|
+
raise Leptris::XML::Error, "leptris_comment_node_create failed" if ptr.null?
|
|
419
|
+
Leptris::XML::Node.wrap_fresh(ptr, self, Leptris::XML::FFI::NODE_COMMENT)
|
|
420
|
+
end
|
|
421
|
+
|
|
422
|
+
def create_cdata(content)
|
|
423
|
+
ptr = Leptris::XML::FFI.leptris_cdata_node_create(c_ptr, content.to_s)
|
|
424
|
+
raise Leptris::XML::Error, "leptris_cdata_node_create failed" if ptr.null?
|
|
425
|
+
Leptris::XML::Node.wrap_fresh(ptr, self, Leptris::XML::FFI::NODE_CDATA)
|
|
426
|
+
end
|
|
427
|
+
|
|
428
|
+
def create_processing_instruction(target, data = "")
|
|
429
|
+
ptr = Leptris::XML::FFI.leptris_pi_node_create(c_ptr, target.to_s, data.to_s)
|
|
430
|
+
raise Leptris::XML::Error, "leptris_pi_node_create failed" if ptr.null?
|
|
431
|
+
Leptris::XML::Node.wrap_fresh(ptr, self, Leptris::XML::FFI::NODE_PI)
|
|
432
|
+
end
|
|
433
|
+
|
|
434
|
+
def fragment(markup)
|
|
435
|
+
Leptris::XML::DocumentFragment.parse(markup, self)
|
|
436
|
+
end
|
|
437
|
+
|
|
438
|
+
def dup
|
|
439
|
+
raw = Leptris::XML::FFI.leptris_document_copy(c_ptr)
|
|
440
|
+
raise Leptris::XML::Error, "leptris_document_copy failed" if raw.null?
|
|
441
|
+
self.class.wrap(raw)
|
|
442
|
+
end
|
|
443
|
+
alias_method :clone, :dup
|
|
444
|
+
|
|
445
|
+
def doctype
|
|
446
|
+
ptr = Leptris::XML::FFI.leptris_document_internal_subset(c_ptr)
|
|
447
|
+
return nil if ptr.null?
|
|
448
|
+
Leptris::XML::DocType.new(ptr, self)
|
|
449
|
+
end
|
|
450
|
+
alias_method :internal_subset, :doctype
|
|
451
|
+
|
|
452
|
+
# indent_text selects the ext-serializer knob (libleptris 1.9.22):
|
|
453
|
+
# a STRING is the indent unit with Nokogiri's semantics — the unit
|
|
454
|
+
# replaces the default spaces, one copy per depth level, standard
|
|
455
|
+
# layout (requires indent > 0); true selects the display form
|
|
456
|
+
# (1.9.9 #129 — text and mixed content indent too; output is
|
|
457
|
+
# display-oriented and not round-trip-guaranteed).
|
|
458
|
+
def to_xml(indent: 0, no_decl: false, encoding: nil, indent_text: false)
|
|
459
|
+
raise Leptris::XML::UseAfterFreeError if @freed.state == :freed
|
|
460
|
+
return "" if c_ptr.nil?
|
|
461
|
+
case indent_text
|
|
462
|
+
when String
|
|
463
|
+
return Leptris::XML::Serialization.to_xml_indent_unit(
|
|
464
|
+
c_ptr, indent_text, indent: indent, no_decl: no_decl,
|
|
465
|
+
encoding: encoding)
|
|
466
|
+
when true
|
|
467
|
+
return Leptris::XML::Serialization.to_xml_display(
|
|
468
|
+
c_ptr, indent: indent, no_decl: no_decl, encoding: encoding)
|
|
469
|
+
end
|
|
470
|
+
Leptris::XML::Serialization.to_xml(
|
|
471
|
+
Leptris::XML::Serialization::DOCUMENT_SERIALIZE_INTO, c_ptr,
|
|
472
|
+
indent: indent, no_decl: no_decl, encoding: encoding)
|
|
473
|
+
end
|
|
474
|
+
alias_method :to_s, :to_xml
|
|
475
|
+
alias_method :serialize, :to_xml
|
|
476
|
+
|
|
477
|
+
def save(path, **opts)
|
|
478
|
+
opts_struct, _encoding_anchor = Leptris::XML::Serialization.build_options(
|
|
479
|
+
indent: opts.fetch(:indent, 0),
|
|
480
|
+
no_decl: opts.fetch(:no_decl, false),
|
|
481
|
+
encoding: opts[:encoding])
|
|
482
|
+
status = Leptris::XML::FFI.leptris_document_save_file(
|
|
483
|
+
c_ptr, path, opts_struct.pointer)
|
|
484
|
+
Leptris::XML::FFI.check_status(status)
|
|
485
|
+
self
|
|
486
|
+
end
|
|
487
|
+
|
|
488
|
+
def canonicalize(version = Leptris::XML::FFI::C14N_1_0,
|
|
489
|
+
inclusive_namespaces = nil,
|
|
490
|
+
with_comments: false,
|
|
491
|
+
exclusive: false,
|
|
492
|
+
mode: nil)
|
|
493
|
+
raise Leptris::XML::UseAfterFreeError if @freed.state == :freed
|
|
494
|
+
return "" if c_ptr.nil?
|
|
495
|
+
resolved_mode = mode || (exclusive ? Leptris::XML::FFI::C14N_MODE_EXCLUSIVE
|
|
496
|
+
: Leptris::XML::FFI::C14N_MODE_CANONICAL)
|
|
497
|
+
Leptris::XML::Serialization.canonicalize(
|
|
498
|
+
Leptris::XML::FFI.method(:leptris_c14n_canonicalize_ex), c_ptr,
|
|
499
|
+
version: version, mode: resolved_mode,
|
|
500
|
+
inclusive_namespaces: inclusive_namespaces,
|
|
501
|
+
with_comments: with_comments)
|
|
502
|
+
end
|
|
503
|
+
alias_method :c14n, :canonicalize
|
|
504
|
+
|
|
505
|
+
def free
|
|
506
|
+
return if @freed.state == :freed
|
|
507
|
+
@freed.state = :freed
|
|
508
|
+
Leptris::XML::FFI.leptris_document_free(c_ptr) unless c_ptr.nil?
|
|
509
|
+
@c_ptr = nil
|
|
510
|
+
@c_address = nil
|
|
511
|
+
@wrapper_cache&.clear
|
|
512
|
+
# Detach the lifetime handle (TODO.perf/12): the memory is
|
|
513
|
+
# already released above — the GC-pass dfree must no-op.
|
|
514
|
+
Leptris::XML::Native.doc_handle_release(self) if defined?(Leptris::XML::NATIVE_FAST)
|
|
515
|
+
end
|
|
516
|
+
|
|
517
|
+
# Enable the first-party EXSLT-style extension pack on this
|
|
518
|
+
# document: str:/set:/math: prefixed functions (replace, tokenize,
|
|
519
|
+
# split, concat, padding; distinct, intersection, difference,
|
|
520
|
+
# leading, trailing; max, min, abs, sqrt, power) as native C
|
|
521
|
+
# handlers. Returns self for chaining.
|
|
522
|
+
def exslt
|
|
523
|
+
Leptris::XML::FFI.check_status(
|
|
524
|
+
Leptris::XML::FFI.leptris_exslt_enable(c_ptr))
|
|
525
|
+
self
|
|
526
|
+
end
|
|
527
|
+
|
|
528
|
+
# Document-level processing instructions (not tree nodes):
|
|
529
|
+
# an array of [target, data] pairs in document order.
|
|
530
|
+
def processing_instructions
|
|
531
|
+
return @processing_instructions if @pi_version == @version
|
|
532
|
+
count = Leptris::XML::FFI.leptris_document_pi_count(c_ptr)
|
|
533
|
+
result = count.times.map do |i|
|
|
534
|
+
[Leptris::XML::FFI.leptris_document_pi_target(c_ptr, i),
|
|
535
|
+
Leptris::XML::FFI.read_pi_data(
|
|
536
|
+
Leptris::XML::FFI.leptris_document_pi_data(c_ptr, i)).to_s]
|
|
537
|
+
end
|
|
538
|
+
@processing_instructions = result
|
|
539
|
+
@pi_version = @version
|
|
540
|
+
result
|
|
541
|
+
end
|
|
542
|
+
|
|
543
|
+
# Removes a document-level processing instruction — by target
|
|
544
|
+
# (String/Symbol) or by 0-based index among the document's PIs
|
|
545
|
+
# (Integer) (libleptris 1.9.9, upstream #612). Returns the removed
|
|
546
|
+
# PI — pool-owned and valid until #free — or nil when nothing
|
|
547
|
+
# matched.
|
|
548
|
+
def remove_pi(target_or_index)
|
|
549
|
+
raise Leptris::XML::UseAfterFreeError if @freed.state == :freed
|
|
550
|
+
if target_or_index.is_a?(Integer)
|
|
551
|
+
ptr = Leptris::XML::FFI.leptris_document_remove_pi(
|
|
552
|
+
c_ptr, nil, target_or_index)
|
|
553
|
+
else
|
|
554
|
+
ptr = Leptris::XML::FFI.leptris_document_remove_pi(
|
|
555
|
+
c_ptr, target_or_index.to_s, 0)
|
|
556
|
+
end
|
|
557
|
+
return nil if ptr.null?
|
|
558
|
+
@version += 1
|
|
559
|
+
Leptris::XML::Node.wrap(ptr, self)
|
|
560
|
+
end
|
|
561
|
+
|
|
562
|
+
# Append a document-level processing instruction. Returns the PI
|
|
563
|
+
# node (epilog anchor — after the root; identity via
|
|
564
|
+
# leptris_document_append_pi, libleptris 1.9.176 / #212).
|
|
565
|
+
def add_pi(target, data = "")
|
|
566
|
+
witness = Leptris::XML::FFI.leptris_document_append_pi(
|
|
567
|
+
c_ptr, target.to_s, data.to_s)
|
|
568
|
+
raise Leptris::XML::Error, "leptris_document_append_pi failed" if witness.null?
|
|
569
|
+
@version += 1
|
|
570
|
+
Leptris::XML::Node.wrap(witness, self)
|
|
571
|
+
end
|
|
572
|
+
|
|
573
|
+
# Remove a document-level PI or comment from the document
|
|
574
|
+
# children chain (libleptris 1.9.176 / #212). The node stays
|
|
575
|
+
# document-owned. Raises when the node is not in the chain.
|
|
576
|
+
def remove_child(node)
|
|
577
|
+
Leptris::XML::FFI.check_status(
|
|
578
|
+
Leptris::XML::FFI.leptris_document_remove_child(c_ptr, node.c_ptr))
|
|
579
|
+
@version += 1
|
|
580
|
+
self
|
|
581
|
+
end
|
|
582
|
+
|
|
583
|
+
# Create an &name; entity reference node (libleptris 1.9.176 /
|
|
584
|
+
# #212): serializes back verbatim once attached.
|
|
585
|
+
def create_entity_reference(name)
|
|
586
|
+
ptr = Leptris::XML::FFI.leptris_entity_ref_node_create(
|
|
587
|
+
c_ptr, name.to_s)
|
|
588
|
+
raise Leptris::XML::Error, "leptris_entity_ref_node_create failed" if ptr.null?
|
|
589
|
+
Leptris::XML::Node.wrap_fresh(ptr, self, Leptris::XML::FFI::NODE_ENTITY_REF)
|
|
590
|
+
end
|
|
591
|
+
|
|
592
|
+
# The XML declaration (libleptris 1.9.176 / #212). `#version`
|
|
593
|
+
# stays the mutation counter — the declaration reads are
|
|
594
|
+
# xml_-prefixed.
|
|
595
|
+
def xml_version
|
|
596
|
+
Leptris::XML::FFI.leptris_document_version(c_ptr)
|
|
597
|
+
end
|
|
598
|
+
|
|
599
|
+
def xml_standalone
|
|
600
|
+
v = Leptris::XML::FFI.leptris_document_standalone(c_ptr)
|
|
601
|
+
v < 0 ? nil : (v == 1)
|
|
602
|
+
end
|
|
603
|
+
|
|
604
|
+
def xml_encoding
|
|
605
|
+
Leptris::XML::FFI.leptris_document_encoding(c_ptr)
|
|
606
|
+
end
|
|
607
|
+
|
|
608
|
+
def xml_version=(value)
|
|
609
|
+
Leptris::XML::FFI.check_status(
|
|
610
|
+
Leptris::XML::FFI.leptris_document_set_version(c_ptr, value.to_s))
|
|
611
|
+
value
|
|
612
|
+
end
|
|
613
|
+
|
|
614
|
+
def xml_encoding=(value)
|
|
615
|
+
Leptris::XML::FFI.check_status(
|
|
616
|
+
Leptris::XML::FFI.leptris_document_set_encoding(c_ptr, value.to_s))
|
|
617
|
+
value
|
|
618
|
+
end
|
|
619
|
+
|
|
620
|
+
def xml_standalone=(value)
|
|
621
|
+
Leptris::XML::FFI.check_status(
|
|
622
|
+
Leptris::XML::FFI.leptris_document_set_standalone(c_ptr,
|
|
623
|
+
value.nil? ? -1 : (value ? 1 : 0)))
|
|
624
|
+
value
|
|
625
|
+
end
|
|
626
|
+
|
|
627
|
+
# Programmatic DOCTYPE (libleptris 1.9.176 / #212): create and
|
|
628
|
+
# set the document's DOCTYPE (root name + optional PUBLIC/SYSTEM
|
|
629
|
+
# ids); serializes in document position. Returns the DocType.
|
|
630
|
+
def set_doctype(name, public_id: nil, system_id: nil)
|
|
631
|
+
dt = Leptris::XML::FFI.leptris_document_set_doctype(
|
|
632
|
+
c_ptr, name.to_s, public_id, system_id)
|
|
633
|
+
raise Leptris::XML::Error, "leptris_document_set_doctype failed" if dt.null?
|
|
634
|
+
@version += 1
|
|
635
|
+
Leptris::XML::DocType.new(dt, self)
|
|
636
|
+
end
|
|
637
|
+
|
|
638
|
+
# Append a document-level comment (epilog position, after the
|
|
639
|
+
# root element) — the add_pi twin (libleptris 1.9.160, #1032;
|
|
640
|
+
# parsed document-level comments round-tripped since 1.9.3/#578,
|
|
641
|
+
# this is the writer). Returns self.
|
|
642
|
+
def add_comment(content)
|
|
643
|
+
witness = Leptris::XML::FFI.leptris_document_add_comment(
|
|
644
|
+
c_ptr, content.to_s)
|
|
645
|
+
raise Leptris::XML::Error,
|
|
646
|
+
"leptris_document_add_comment failed" if witness.null?
|
|
647
|
+
@version += 1
|
|
648
|
+
self
|
|
649
|
+
end
|
|
650
|
+
|
|
651
|
+
# Marks the document read-only: tree mutations raise
|
|
652
|
+
# Leptris::XML::ReadOnlyError, and read paths memoize aggressively
|
|
653
|
+
# (names, content, children, attributes) since they can never go
|
|
654
|
+
# stale. The C document is also frozen (advisory upstream). One-way.
|
|
655
|
+
def readonly!
|
|
656
|
+
Leptris::XML::FFI.leptris_document_freeze(c_ptr)
|
|
657
|
+
@readonly = true
|
|
658
|
+
self
|
|
659
|
+
end
|
|
660
|
+
|
|
661
|
+
def readonly?
|
|
662
|
+
@readonly == true
|
|
663
|
+
end
|
|
664
|
+
|
|
665
|
+
# True once #free has run (or the GC finalizer fired) — borrowed
|
|
666
|
+
# handles check this before dereferencing their c_ptr.
|
|
667
|
+
def freed?
|
|
668
|
+
@freed.state == :freed || c_ptr.nil?
|
|
669
|
+
end
|
|
670
|
+
|
|
671
|
+
# Document-level comments — parsed <!-- ... --> outside the
|
|
672
|
+
# root element, prolog then epilog, in document order (the
|
|
673
|
+
# companion reader to #processing_instructions; libleptris
|
|
674
|
+
# 1.9.3, upstream #578). Version-memoized like the PI list.
|
|
675
|
+
def comments
|
|
676
|
+
return @comments if @comments_version == @version
|
|
677
|
+
count = Leptris::XML::FFI.leptris_document_comment_count(c_ptr)
|
|
678
|
+
result = Array.new(count) do |i|
|
|
679
|
+
Leptris::XML::FFI.leptris_document_comment_content(c_ptr, i)
|
|
680
|
+
end
|
|
681
|
+
@comments = result
|
|
682
|
+
@comments_version = @version
|
|
683
|
+
result
|
|
684
|
+
end
|
|
685
|
+
|
|
686
|
+
# The thread-global last-failure [line, column] (1-based), or nil
|
|
687
|
+
# when no error is recorded — the position companion to
|
|
688
|
+
# Document#last_error; populated by recover parses.
|
|
689
|
+
def last_error_position
|
|
690
|
+
line = ::FFI::MemoryPointer.new(:int)
|
|
691
|
+
column = ::FFI::MemoryPointer.new(:int)
|
|
692
|
+
begin
|
|
693
|
+
Leptris::XML::FFI.leptris_last_error_position(line, column)
|
|
694
|
+
line.read_int.zero? && column.read_int.zero? ? nil :
|
|
695
|
+
[line.read_int, column.read_int]
|
|
696
|
+
ensure
|
|
697
|
+
line.free
|
|
698
|
+
column.free
|
|
699
|
+
end
|
|
700
|
+
end
|
|
701
|
+
|
|
702
|
+
# The most recent error recorded against this document, or nil.
|
|
703
|
+
def last_error
|
|
704
|
+
msg = Leptris::XML::FFI.leptris_document_last_error(c_ptr)
|
|
705
|
+
msg.nil? || msg.empty? ? nil : msg
|
|
706
|
+
end
|
|
707
|
+
|
|
708
|
+
def name; "document"; end
|
|
709
|
+
def document; self; end
|
|
710
|
+
def encoding
|
|
711
|
+
return nil if c_ptr.nil?
|
|
712
|
+
Leptris::XML::FFI.leptris_document_encoding(c_ptr)
|
|
713
|
+
end
|
|
714
|
+
|
|
715
|
+
include Leptris::XML::Searchable
|
|
716
|
+
end
|