leptris 1.9.199.0-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 +2528 -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,686 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Leptris::XML::Node
|
|
4
|
+
# TODO.perf/19: @c_address is the canonical truth; the Pointer
|
|
5
|
+
# materializes only when actually read (most wrappers' Pointers
|
|
6
|
+
# never are — hot reads are memoized or address-based).
|
|
7
|
+
def c_ptr
|
|
8
|
+
@c_ptr ||= ::FFI::Pointer.new(@c_address)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
# The Integer twin of #c_ptr — reading it never materializes a
|
|
12
|
+
# Pointer (the native faces take addresses).
|
|
13
|
+
def c_address
|
|
14
|
+
@c_address
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Iterparse-yielded elements are owned by an IterationScope (the
|
|
18
|
+
# internal lifetime/memoization authority) — the public #document
|
|
19
|
+
# answers nil for them, per the documented contract.
|
|
20
|
+
# Precomputed at construction (TODO.perf/24): scope ownership
|
|
21
|
+
# is fixed for a node's lifetime — nil for iterparse yields.
|
|
22
|
+
def document
|
|
23
|
+
@pub_document
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def initialize(c_ptr, document, parent: nil, node_type: nil)
|
|
27
|
+
@c_ptr = c_ptr
|
|
28
|
+
@c_address = c_ptr&.address
|
|
29
|
+
@document = document
|
|
30
|
+
@parent = parent
|
|
31
|
+
# Structural-memo stamp (TODO.perf/13): a constructor-seeded
|
|
32
|
+
# @parent is true AS OF the document's current version; any
|
|
33
|
+
# mutation advances the version and forces re-derivation. A
|
|
34
|
+
# NIL constructor parent means UNKNOWN, not "no parent" — the
|
|
35
|
+
# engine may attach during creation (create_child), so an
|
|
36
|
+
# unstamped memo must derive rather than trust nil.
|
|
37
|
+
@parent_version = parent ? document&.version : nil
|
|
38
|
+
# Computed once: structural stamps apply to document-owned
|
|
39
|
+
# nodes only (scope-owned iterparse elements move without
|
|
40
|
+
# their scope's version advancing — see #parent). An ivar
|
|
41
|
+
# keeps the memo-hit path free of method dispatch.
|
|
42
|
+
@structure_memoizable =
|
|
43
|
+
!document.nil? && !document.is_a?(Leptris::XML::IterationScope)
|
|
44
|
+
@pub_document = @structure_memoizable ? document : nil
|
|
45
|
+
# TODO.perf/25: pure address-based native reads touch no
|
|
46
|
+
# cache — safe for scope-owned (iterparse) elements too.
|
|
47
|
+
@addr_reads_fast = !!defined?(Leptris::XML::NATIVE_FAST)
|
|
48
|
+
# NATIVE_FAST availability is fixed at load time before any
|
|
49
|
+
# node exists, so the conjunction with document-ownership is
|
|
50
|
+
# construct-time constant — the hot gates read one ivar.
|
|
51
|
+
@native_fast = @structure_memoizable &&
|
|
52
|
+
defined?(Leptris::XML::NATIVE_FAST) ? true : false
|
|
53
|
+
# wrap() already calls leptris_node_get_type for dispatch; reusing
|
|
54
|
+
# the result makes every predicate and #type call FFI-free.
|
|
55
|
+
@node_type = node_type
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# True when this node's owning context is an IterationScope
|
|
59
|
+
# (iterparse-yielded) rather than a Document.
|
|
60
|
+
def scope_owned?
|
|
61
|
+
@document.is_a?(Leptris::XML::IterationScope)
|
|
62
|
+
end
|
|
63
|
+
private :scope_owned?
|
|
64
|
+
|
|
65
|
+
# node_type: callers holding a batch-fetched kind (the XPath
|
|
66
|
+
# result-set batch fills out_kinds) pass it so the wrap skips the
|
|
67
|
+
# get_type dispatch; nil (the default) dispatches as before.
|
|
68
|
+
# Factory variant: the pointer was JUST created by the engine,
|
|
69
|
+
# so the cache lookup is a guaranteed miss — skip straight to
|
|
70
|
+
# construction and store (identity still survives attach).
|
|
71
|
+
def self.wrap_fresh(c_ptr, document, node_type)
|
|
72
|
+
node = construct(c_ptr, document, nil, node_type, nil)
|
|
73
|
+
if document
|
|
74
|
+
cache = document.wrapper_cache
|
|
75
|
+
cache[c_ptr.address] = node
|
|
76
|
+
end
|
|
77
|
+
node
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def self.wrap(c_ptr, document, parent: nil, node_type: nil, result_value: nil)
|
|
81
|
+
# Per-document weak-ref cache. Returns the existing wrapper when the
|
|
82
|
+
# same c_ptr is wrapped twice (common in children/sibling walks,
|
|
83
|
+
# repeated xpath queries, traverse-then-access patterns). The cache
|
|
84
|
+
# dies with the document so no stale entries. The miss path
|
|
85
|
+
# resolves the cache and address once — a cold walk wraps every
|
|
86
|
+
# node exactly once and pays both only on the store.
|
|
87
|
+
if document
|
|
88
|
+
cache = document.wrapper_cache
|
|
89
|
+
address = c_ptr.address
|
|
90
|
+
if (cached = cache[address])
|
|
91
|
+
# The shared cache also holds NativeNodes (opt-in layer).
|
|
92
|
+
# Only return a hit when it is a binding Node subclass —
|
|
93
|
+
# otherwise fall through and construct the binding wrapper
|
|
94
|
+
# (both views of the same C node can coexist; the native
|
|
95
|
+
# entry stays under its own key-path via NativeNode.from).
|
|
96
|
+
return cached if cached.is_a?(Leptris::XML::Node)
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
node_type ||= Leptris::XML::FFI.leptris_node_get_type(c_ptr)
|
|
101
|
+
node = construct(c_ptr, document, parent, node_type, result_value)
|
|
102
|
+
|
|
103
|
+
cache[address] = node if document
|
|
104
|
+
node
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# The single type-dispatch construction authority shared by wrap
|
|
108
|
+
# (cache-checked) and wrap_fresh (factory path).
|
|
109
|
+
def self.construct(c_ptr, document, parent, node_type, result_value)
|
|
110
|
+
case node_type
|
|
111
|
+
when Leptris::XML::FFI::NODE_ELEMENT
|
|
112
|
+
Leptris::XML::Element.new(c_ptr, document, parent: parent, node_type: node_type)
|
|
113
|
+
when Leptris::XML::FFI::NODE_TEXT
|
|
114
|
+
Leptris::XML::Text.new(c_ptr, document, parent: parent, node_type: node_type)
|
|
115
|
+
when Leptris::XML::FFI::NODE_SYNTHETIC_TEXT
|
|
116
|
+
# Sequence/map/array items: value captured by the NodeSet at
|
|
117
|
+
# materialization (only the result handle can read them).
|
|
118
|
+
Leptris::XML::ResultText.new(
|
|
119
|
+
c_ptr, document, result_value, parent: parent, node_type: node_type)
|
|
120
|
+
when Leptris::XML::FFI::NODE_ATTRIBUTE
|
|
121
|
+
# ruby#153: synthetic attribute result nodes carry their
|
|
122
|
+
# name/value in the result handle — the NodeSet captures
|
|
123
|
+
# them at materialization (result_value is a {name:, value:}
|
|
124
|
+
# hash; the single-node seam captures the same way).
|
|
125
|
+
rv = result_value || {}
|
|
126
|
+
Leptris::XML::ResultAttr.new(
|
|
127
|
+
c_ptr, document, rv[:name], rv[:value],
|
|
128
|
+
parent: parent, node_type: node_type)
|
|
129
|
+
when Leptris::XML::FFI::NODE_COMMENT
|
|
130
|
+
Leptris::XML::Comment.new(c_ptr, document, parent: parent, node_type: node_type)
|
|
131
|
+
when Leptris::XML::FFI::NODE_CDATA
|
|
132
|
+
Leptris::XML::CDATA.new(c_ptr, document, parent: parent, node_type: node_type)
|
|
133
|
+
when Leptris::XML::FFI::NODE_PI
|
|
134
|
+
Leptris::XML::ProcessingInstruction.new(c_ptr, document, parent: parent, node_type: node_type)
|
|
135
|
+
when Leptris::XML::FFI::NODE_ENTITY_REF
|
|
136
|
+
Leptris::XML::EntityReference.new(c_ptr, document, parent: parent, node_type: node_type)
|
|
137
|
+
else
|
|
138
|
+
new(c_ptr, document, parent: parent, node_type: node_type)
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
private_class_method :construct
|
|
142
|
+
|
|
143
|
+
def name
|
|
144
|
+
raise NotImplementedError, "#{self.class}#name not implemented"
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def content
|
|
148
|
+
raise NotImplementedError, "#{self.class}#content not implemented"
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# Dispatching defs, not alias_method: an alias snapshots this base
|
|
152
|
+
# #content (the raise), so subclass overrides would never be seen
|
|
153
|
+
# through the alias. A plain method resolves #content per-call.
|
|
154
|
+
def text
|
|
155
|
+
content
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def inner_text
|
|
159
|
+
content
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def type
|
|
163
|
+
return @node_type if @node_type
|
|
164
|
+
ensure_alive!
|
|
165
|
+
@node_type = Leptris::XML::FFI.leptris_node_get_type(c_ptr)
|
|
166
|
+
end
|
|
167
|
+
alias_method :node_type, :type
|
|
168
|
+
|
|
169
|
+
def element?; type == Leptris::XML::FFI::NODE_ELEMENT; end
|
|
170
|
+
|
|
171
|
+
# The ext bulk path applies to document-owned trees only:
|
|
172
|
+
# scope-owned (iterparse) elements ride the IterationScope seam.
|
|
173
|
+
# Precomputed at construction (TODO.perf/17) — one ivar read.
|
|
174
|
+
def native_fast_children?
|
|
175
|
+
@native_fast
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
# TODO.perf/25: the bulk children faces accept the
|
|
179
|
+
# IterationScope itself (its @wrapper_cache resets per yield —
|
|
180
|
+
# recycled addresses cannot collide — and its @version advances
|
|
181
|
+
# per yield and on mutation), so streaming children ride the
|
|
182
|
+
# one-pass materialization too. The scope-aware stamps live in
|
|
183
|
+
# the C face.
|
|
184
|
+
def native_bulk_children?
|
|
185
|
+
defined?(Leptris::XML::NATIVE_FAST)
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
# Content-defined 64-bit Merkle digest of this subtree
|
|
189
|
+
# (libleptris 1.9.99, #869): element name/prefix/resolved
|
|
190
|
+
# namespace URI, attributes sorted and first-wins-deduplicated,
|
|
191
|
+
# children hashed in document order — no addresses participate,
|
|
192
|
+
# so equal trees hash equal across processes. Equality implies
|
|
193
|
+
# subtree equivalence; inequality implies nothing (descend).
|
|
194
|
+
# +drop_ws+ skips whitespace-only text nodes.
|
|
195
|
+
def digest(drop_ws: false)
|
|
196
|
+
ensure_alive!
|
|
197
|
+
Leptris::XML::FFI.leptris_node_digest(
|
|
198
|
+
c_ptr, drop_ws ? 1 : 0)
|
|
199
|
+
end
|
|
200
|
+
def text?; type == Leptris::XML::FFI::NODE_TEXT; end
|
|
201
|
+
def comment?; type == Leptris::XML::FFI::NODE_COMMENT; end
|
|
202
|
+
def cdata?; type == Leptris::XML::FFI::NODE_CDATA; end
|
|
203
|
+
def processing_instruction?
|
|
204
|
+
type == Leptris::XML::FFI::NODE_PI
|
|
205
|
+
end
|
|
206
|
+
alias_method :pi?, :processing_instruction?
|
|
207
|
+
|
|
208
|
+
# Version-stamped structural memo (TODO.perf/13): derive once,
|
|
209
|
+
# re-derive after any mutation that advances the owning
|
|
210
|
+
# document's version. Fixes the stale seeded @parent after a
|
|
211
|
+
# move (FFI children walks seed it; the move never cleared it).
|
|
212
|
+
# Scope-owned (iterparse) elements never memoize: a scope element
|
|
213
|
+
# adopted into a document moves without its scope's version
|
|
214
|
+
# advancing, so a stamp would lie.
|
|
215
|
+
def parent
|
|
216
|
+
if @structure_memoizable && @parent_version == @document.version
|
|
217
|
+
return @parent
|
|
218
|
+
end
|
|
219
|
+
ensure_alive!
|
|
220
|
+
ptr = Leptris::XML::FFI.leptris_node_parent(c_ptr)
|
|
221
|
+
result = ptr.null? ? nil : Leptris::XML::Node.wrap(ptr, @document)
|
|
222
|
+
if @document
|
|
223
|
+
@parent = result
|
|
224
|
+
@parent_version = @document.version
|
|
225
|
+
end
|
|
226
|
+
result
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
# Derive the parent WITHOUT stamping the structural memo. The
|
|
230
|
+
# adoption lift reads the child's source scope BETWEEN the
|
|
231
|
+
# mutation gate's version bump and the engine move — a stamped
|
|
232
|
+
# #parent there would record post-bump versions carrying
|
|
233
|
+
# pre-move truth. Ancestor walks that run inside mutations use
|
|
234
|
+
# this; #namespaces itself is memoized, so stable trees pay the
|
|
235
|
+
# unstamped derivation at most once per version.
|
|
236
|
+
def unstamped_parent
|
|
237
|
+
ensure_alive!
|
|
238
|
+
ptr = Leptris::XML::FFI.leptris_node_parent(c_ptr)
|
|
239
|
+
ptr.null? ? nil : Leptris::XML::Node.wrap(ptr, @document)
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
# Mutation sites move nodes ACROSS documents: the moved node's
|
|
243
|
+
# stamps reference the source document's version, which the move
|
|
244
|
+
# does not advance. Clear them and advance the source version
|
|
245
|
+
# (its other nodes hold parent/sibling stamps the move invalidates).
|
|
246
|
+
def invalidate_structural_stamps!
|
|
247
|
+
@parent_version = nil
|
|
248
|
+
@next_sibling_version = nil
|
|
249
|
+
@previous_sibling_version = nil
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
# Mutation-site helper: when +node+ moves into +target_document+
|
|
253
|
+
# from a different one, the move invalidates the node's own
|
|
254
|
+
# structural stamps (stamped against the SOURCE version) and the
|
|
255
|
+
# source document's sibling/parent stamps. Scope-owned nodes
|
|
256
|
+
# answer nil from #document and never memoize — nothing to do.
|
|
257
|
+
def self.invalidate_cross_document!(node, target_document)
|
|
258
|
+
source = node.document
|
|
259
|
+
return if source.nil? || source.equal?(target_document)
|
|
260
|
+
node.invalidate_structural_stamps!
|
|
261
|
+
source.advance_version
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
# Borrowed-handle lifetime: every c_ptr dereference is valid only
|
|
265
|
+
# while the owning document lives. Parentless nodes (iterparse
|
|
266
|
+
# yields) cannot validate and are skipped. The guard runs before
|
|
267
|
+
# every uncached FFI dispatch, so it uses the cheapest sufficient
|
|
268
|
+
# check: #free nils the document's c_ptr, and the GC-finalizer
|
|
269
|
+
# path cannot fire while any handle (which strongly references
|
|
270
|
+
# the document) exists. Document#freed? remains the accurate
|
|
271
|
+
# public predicate.
|
|
272
|
+
def ensure_alive!
|
|
273
|
+
if @document && @document.c_ptr.nil?
|
|
274
|
+
raise Leptris::XML::UseAfterFreeError,
|
|
275
|
+
"owning document has been freed — handle used on #{inspect}"
|
|
276
|
+
end
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
# Raises ReadOnlyError when the owning document was marked readonly,
|
|
280
|
+
# UseAfterFreeError when it was freed. Every node-level mutation
|
|
281
|
+
# passes through this gate, so it is where the document's mutation
|
|
282
|
+
# version advances — the invalidation behind writable-document
|
|
283
|
+
# memoization. Bumping before the C call is conservative: a failed
|
|
284
|
+
# mutation merely discards memos.
|
|
285
|
+
def ensure_writable!
|
|
286
|
+
ensure_alive!
|
|
287
|
+
if readonly_document?
|
|
288
|
+
raise Leptris::XML::ReadOnlyError,
|
|
289
|
+
"document is readonly — mutation attempted on #{inspect}"
|
|
290
|
+
end
|
|
291
|
+
@document.advance_version
|
|
292
|
+
nil
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
# Readonly is one-way, so caching TRUE is sound: once observed,
|
|
296
|
+
# the document is readonly forever. FALSE stays uncached (the
|
|
297
|
+
# document may still flip). Saves the document round-trip on
|
|
298
|
+
# per-read guards.
|
|
299
|
+
def readonly_document?
|
|
300
|
+
return true if instance_variable_defined?(:@readonly_document)
|
|
301
|
+
return false unless @document&.readonly?
|
|
302
|
+
@readonly_document = true
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
# A node's source position never changes — first read wins.
|
|
306
|
+
def line
|
|
307
|
+
return @line if defined?(@line)
|
|
308
|
+
ensure_alive!
|
|
309
|
+
@line = Leptris::XML::FFI.leptris_node_line(c_ptr)
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
# Full parser-recorded position (libleptris 1.9.180, #1124):
|
|
313
|
+
# {line:, col_start:, col_end:} in Jing's diagnostic
|
|
314
|
+
# convention; zeros for programmatically-created nodes. First
|
|
315
|
+
# read wins (positions never change).
|
|
316
|
+
def source_position
|
|
317
|
+
return @source_position if defined?(@source_position)
|
|
318
|
+
ensure_alive!
|
|
319
|
+
out = Leptris::XML::FFI::SourcePosition.new
|
|
320
|
+
Leptris::XML::FFI.leptris_node_source_position(c_ptr, out)
|
|
321
|
+
@source_position = { line: out[:line], col_start: out[:col_start],
|
|
322
|
+
col_end: out[:col_end] }
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
# Byte offset of the node's markup in its parse source (the '<'
|
|
326
|
+
# of the tag; libleptris 1.9.162, #1039 — the position descriptor
|
|
327
|
+
# CALLBACK rows echo). 0 when unknown: mutation-created nodes, or
|
|
328
|
+
# documents >= 2 GiB.
|
|
329
|
+
def byte_offset
|
|
330
|
+
return @byte_offset if defined?(@byte_offset)
|
|
331
|
+
ensure_alive!
|
|
332
|
+
@byte_offset = Leptris::XML::FFI.leptris_node_byte_offset(c_ptr)
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
def <=>(other)
|
|
336
|
+
return nil unless other.is_a?(Leptris::XML::Node)
|
|
337
|
+
return nil unless @document == other.document
|
|
338
|
+
ensure_alive!
|
|
339
|
+
Leptris::XML::FFI.leptris_node_compare(c_ptr, other.c_ptr)
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
def child
|
|
343
|
+
ensure_alive!
|
|
344
|
+
ptr = Leptris::XML::FFI.leptris_node_first_child(c_ptr)
|
|
345
|
+
return nil if ptr.null?
|
|
346
|
+
Leptris::XML::Node.wrap(ptr, @document, parent: as_element_or_self)
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
def children
|
|
350
|
+
# Immutable in readonly mode: the batch fetch plus wrapper
|
|
351
|
+
# construction is paid once. The kinds ride the batch
|
|
352
|
+
# (leptris_node_children_ex), so no per-child get_type.
|
|
353
|
+
return @children if memo_hit?(@children_version)
|
|
354
|
+
ensure_alive!
|
|
355
|
+
if native_bulk_children?
|
|
356
|
+
# TODO.perf/01 tail: one C pass constructs every binding
|
|
357
|
+
# wrapper (class dispatch + ivars + identity cache) — the
|
|
358
|
+
# per-child Ruby wrap frames disappear.
|
|
359
|
+
nodes = Leptris::XML::Native.bulk_children(@document, @c_address)
|
|
360
|
+
result = Leptris::XML::NodeSet.new(@document, nodes)
|
|
361
|
+
if @document
|
|
362
|
+
@children = result
|
|
363
|
+
@children_version = @document.version
|
|
364
|
+
end
|
|
365
|
+
return result
|
|
366
|
+
end
|
|
367
|
+
parent = as_element_or_self
|
|
368
|
+
pointers, kinds = Leptris::XML::FFI.fetch_children(c_ptr)
|
|
369
|
+
nodes = Array.new(pointers.size) do |i|
|
|
370
|
+
Leptris::XML::Node.wrap(pointers[i], @document,
|
|
371
|
+
parent: parent, node_type: kinds[i])
|
|
372
|
+
end
|
|
373
|
+
result = Leptris::XML::NodeSet.new(@document, nodes)
|
|
374
|
+
if @document
|
|
375
|
+
@children = result
|
|
376
|
+
@children_version = @document.version
|
|
377
|
+
end
|
|
378
|
+
result
|
|
379
|
+
end
|
|
380
|
+
|
|
381
|
+
def next_sibling
|
|
382
|
+
if @structure_memoizable && @next_sibling_version == @document.version
|
|
383
|
+
return @next_sibling
|
|
384
|
+
end
|
|
385
|
+
ensure_alive!
|
|
386
|
+
ptr = Leptris::XML::FFI.leptris_node_next_sibling(c_ptr)
|
|
387
|
+
result = ptr.null? ? nil : Leptris::XML::Node.wrap(ptr, @document, parent: @parent)
|
|
388
|
+
if @document
|
|
389
|
+
@next_sibling = result
|
|
390
|
+
@next_sibling_version = @document.version
|
|
391
|
+
end
|
|
392
|
+
result
|
|
393
|
+
end
|
|
394
|
+
alias_method :next, :next_sibling
|
|
395
|
+
|
|
396
|
+
def previous_sibling
|
|
397
|
+
if @structure_memoizable && @previous_sibling_version == @document.version
|
|
398
|
+
return @previous_sibling
|
|
399
|
+
end
|
|
400
|
+
ensure_alive!
|
|
401
|
+
ptr = Leptris::XML::FFI.leptris_node_previous_sibling(c_ptr)
|
|
402
|
+
result = ptr.null? ? nil : Leptris::XML::Node.wrap(ptr, @document, parent: @parent)
|
|
403
|
+
if @document
|
|
404
|
+
@previous_sibling = result
|
|
405
|
+
@previous_sibling_version = @document.version
|
|
406
|
+
end
|
|
407
|
+
result
|
|
408
|
+
end
|
|
409
|
+
alias_method :previous, :previous_sibling
|
|
410
|
+
|
|
411
|
+
def first_element_child
|
|
412
|
+
return @first_element_child if memo_hit?(@first_element_child_version)
|
|
413
|
+
ensure_alive!
|
|
414
|
+
# TODO.perf/30: one C walk (the FFI scan paid two calls per
|
|
415
|
+
# skipped non-element sibling).
|
|
416
|
+
if @addr_reads_fast
|
|
417
|
+
result = Leptris::XML::Native.first_element_child(
|
|
418
|
+
@document, @c_address)
|
|
419
|
+
if @document
|
|
420
|
+
@first_element_child = result
|
|
421
|
+
@first_element_child_version = @document.version
|
|
422
|
+
end
|
|
423
|
+
return result
|
|
424
|
+
end
|
|
425
|
+
# Raw pointer scan: non-element siblings are typed with one C
|
|
426
|
+
# call each — never wrapped, never cached — and the found
|
|
427
|
+
# element carries the ELEMENT hint into the wrap.
|
|
428
|
+
ptr = Leptris::XML::FFI.leptris_node_first_child(c_ptr)
|
|
429
|
+
result = nil
|
|
430
|
+
until ptr.nil? || ptr.null?
|
|
431
|
+
if Leptris::XML::FFI.leptris_node_get_type(ptr) ==
|
|
432
|
+
Leptris::XML::FFI::NODE_ELEMENT
|
|
433
|
+
result = Leptris::XML::Node.wrap(
|
|
434
|
+
ptr, @document, parent: as_element_or_self,
|
|
435
|
+
node_type: Leptris::XML::FFI::NODE_ELEMENT)
|
|
436
|
+
break
|
|
437
|
+
end
|
|
438
|
+
ptr = Leptris::XML::FFI.leptris_node_next_sibling(ptr)
|
|
439
|
+
end
|
|
440
|
+
if @document
|
|
441
|
+
@first_element_child = result
|
|
442
|
+
@first_element_child_version = @document.version
|
|
443
|
+
end
|
|
444
|
+
result
|
|
445
|
+
end
|
|
446
|
+
|
|
447
|
+
def last_element_child
|
|
448
|
+
# TODO.perf/30: one C walk — the batch fetch materialized
|
|
449
|
+
# every child pointer to keep one.
|
|
450
|
+
if @addr_reads_fast
|
|
451
|
+
return Leptris::XML::Native.last_element_child(
|
|
452
|
+
@document, @c_address)
|
|
453
|
+
end
|
|
454
|
+
if is_a?(Leptris::XML::Element)
|
|
455
|
+
kids = Leptris::XML::FFI.fetch_element_children(c_ptr)
|
|
456
|
+
return nil if kids.empty?
|
|
457
|
+
return Leptris::XML::Node.wrap(
|
|
458
|
+
kids.last, @document, parent: self,
|
|
459
|
+
node_type: Leptris::XML::FFI::NODE_ELEMENT)
|
|
460
|
+
end
|
|
461
|
+
children.reverse_each.find(&:element?)
|
|
462
|
+
end
|
|
463
|
+
|
|
464
|
+
def element_children
|
|
465
|
+
return @element_children if memo_hit?(@element_children_version)
|
|
466
|
+
ensure_alive!
|
|
467
|
+
# Element receivers ride the element-only batch: text/comment
|
|
468
|
+
# children are never wrapped (nor their get_type paid — the
|
|
469
|
+
# ELEMENT hint rides along). Other nodes keep the filter.
|
|
470
|
+
result =
|
|
471
|
+
if native_bulk_children?
|
|
472
|
+
Leptris::XML::Native.bulk_element_children(@document, @c_address)
|
|
473
|
+
elsif is_a?(Leptris::XML::Element)
|
|
474
|
+
parent = as_element_or_self
|
|
475
|
+
Leptris::XML::FFI.fetch_element_children(c_ptr).map do |ptr|
|
|
476
|
+
Leptris::XML::Node.wrap(ptr, @document, parent: parent,
|
|
477
|
+
node_type: Leptris::XML::FFI::NODE_ELEMENT)
|
|
478
|
+
end
|
|
479
|
+
else
|
|
480
|
+
children.select(&:element?)
|
|
481
|
+
end
|
|
482
|
+
if @document
|
|
483
|
+
@element_children = result
|
|
484
|
+
@element_children_version = @document.version
|
|
485
|
+
end
|
|
486
|
+
result
|
|
487
|
+
end
|
|
488
|
+
alias_method :elements, :element_children
|
|
489
|
+
|
|
490
|
+
def next_element
|
|
491
|
+
sibling = next_sibling
|
|
492
|
+
sibling = sibling.next_sibling until sibling.nil? || sibling.element?
|
|
493
|
+
sibling
|
|
494
|
+
end
|
|
495
|
+
|
|
496
|
+
def previous_element
|
|
497
|
+
sibling = previous_sibling
|
|
498
|
+
sibling = sibling.previous_sibling until sibling.nil? || sibling.element?
|
|
499
|
+
sibling
|
|
500
|
+
end
|
|
501
|
+
|
|
502
|
+
def unlink
|
|
503
|
+
if @native_fast
|
|
504
|
+
Leptris::XML::Native.unlink_binding_node(@document, @c_address)
|
|
505
|
+
@parent = nil
|
|
506
|
+
return self
|
|
507
|
+
end
|
|
508
|
+
ensure_writable!
|
|
509
|
+
Leptris::XML::FFI.check_status(
|
|
510
|
+
Leptris::XML::FFI.leptris_node_unlink(c_ptr))
|
|
511
|
+
@parent = nil
|
|
512
|
+
self
|
|
513
|
+
end
|
|
514
|
+
alias_method :remove, :unlink
|
|
515
|
+
|
|
516
|
+
# Visits the subtree with ONE C call (leptris_node_visit,
|
|
517
|
+
# libleptris 1.9.20 — upstream #645a): elements yield twice —
|
|
518
|
+
# (node, true, depth) before their children, (node, false, depth)
|
|
519
|
+
# after the subtree completes — every other kind once with
|
|
520
|
+
# entering=true; depth counts element levels from the receiver.
|
|
521
|
+
# No NodeSet, pointer array, or children memo per level: the
|
|
522
|
+
# leanest full-subtree iteration the binding offers (a document
|
|
523
|
+
# receiver walks the document child chain). The walk is read-only
|
|
524
|
+
# — mutate only between visits.
|
|
525
|
+
#
|
|
526
|
+
# root.visit { |node, entering, depth| ... }
|
|
527
|
+
def visit(&block)
|
|
528
|
+
return enum_for(:visit) unless block
|
|
529
|
+
ensure_alive!
|
|
530
|
+
# TODO.perf/27: the ext's callback rb_yields directly — no
|
|
531
|
+
# FFI::Function closure per call.
|
|
532
|
+
if @addr_reads_fast
|
|
533
|
+
Leptris::XML::Native.visit_binding(@document, @c_address,
|
|
534
|
+
&block)
|
|
535
|
+
return self
|
|
536
|
+
end
|
|
537
|
+
document = @document
|
|
538
|
+
visitor = ::FFI::Function.new(
|
|
539
|
+
:void, [:pointer, :pointer, :int, :int], blocking: true) do |_, node_ptr, entering, depth|
|
|
540
|
+
block.call(
|
|
541
|
+
Leptris::XML::Node.wrap(node_ptr, document),
|
|
542
|
+
entering == 1, depth)
|
|
543
|
+
end
|
|
544
|
+
Leptris::XML::FFI.leptris_node_visit(c_ptr, visitor, nil)
|
|
545
|
+
self
|
|
546
|
+
end
|
|
547
|
+
|
|
548
|
+
# Walks the subtree in post-order DFS (matches Nokogiri's
|
|
549
|
+
# semantics): the receiver, its descendants, nothing else.
|
|
550
|
+
#
|
|
551
|
+
# One FFI call dispatches the whole walk; the C engine invokes the
|
|
552
|
+
# callback once per visited node (the only per-node cost is the
|
|
553
|
+
# C-to-Ruby callback dispatch, not FFI round-trips).
|
|
554
|
+
#
|
|
555
|
+
# Subtree-bounded by abort-at-self (leptris-ruby#89): the C
|
|
556
|
+
# walker was never bounded — after visiting the receiver it
|
|
557
|
+
# pushes the receiver's NEXT SIBLING and continues to the end of
|
|
558
|
+
# the document chain. In post-order the receiver is the LAST
|
|
559
|
+
# node of its own subtree, so returning non-zero at self stops
|
|
560
|
+
# the walk exactly at the boundary (the C loop honors a non-zero
|
|
561
|
+
# callback return). The self comparison is by address; the
|
|
562
|
+
# receiver's handle is stable for the walk's duration.
|
|
563
|
+
#
|
|
564
|
+
# Exceptions raised by the block are re-raised after the walk
|
|
565
|
+
# (leptris-ruby#90): a rescue inside the callback stashes the
|
|
566
|
+
# exception and returns non-zero, aborting the C walk — without
|
|
567
|
+
# it the FFI dispatch silently swallowed the exception and the
|
|
568
|
+
# walk continued with partially processed data.
|
|
569
|
+
def traverse(&block)
|
|
570
|
+
return enum_for(:traverse) unless block
|
|
571
|
+
ensure_alive!
|
|
572
|
+
# TODO.perf/27: post-order + abort-at-self + stash-abort-raise
|
|
573
|
+
# all preserved in the C callback (walk state on its stack).
|
|
574
|
+
# The block forwards explicitly — rb_yield needs it on the C
|
|
575
|
+
# entry's own frame.
|
|
576
|
+
if @addr_reads_fast
|
|
577
|
+
Leptris::XML::Native.traverse_binding(@document, @c_address,
|
|
578
|
+
&block)
|
|
579
|
+
return self
|
|
580
|
+
end
|
|
581
|
+
error = nil
|
|
582
|
+
self_address = @c_address
|
|
583
|
+
callback = ::FFI::Function.new(:int, [:pointer, :pointer], blocking: true) do |node_ptr, _|
|
|
584
|
+
begin
|
|
585
|
+
yield Leptris::XML::Node.wrap(node_ptr, @document)
|
|
586
|
+
node_ptr.address == self_address ? 1 : 0
|
|
587
|
+
rescue Exception => e # rubocop:disable Lint/RescueException
|
|
588
|
+
error = e
|
|
589
|
+
1
|
|
590
|
+
end
|
|
591
|
+
end
|
|
592
|
+
Leptris::XML::FFI.leptris_node_traverse(
|
|
593
|
+
c_ptr, Leptris::XML::FFI::TRAVERSE_POST_ORDER, callback, nil)
|
|
594
|
+
raise error if error
|
|
595
|
+
self
|
|
596
|
+
end
|
|
597
|
+
|
|
598
|
+
def path
|
|
599
|
+
return @path if memo_hit?(@path_version)
|
|
600
|
+
ensure_alive!
|
|
601
|
+
result = if @addr_reads_fast
|
|
602
|
+
Leptris::XML::Native.fast_path(@c_address)
|
|
603
|
+
else
|
|
604
|
+
str_ptr = Leptris::XML::FFI.leptris_node_get_xpath(c_ptr)
|
|
605
|
+
str_ptr.null? ? nil : Leptris::XML::FFI.read_owned_string(str_ptr)
|
|
606
|
+
end
|
|
607
|
+
if @document
|
|
608
|
+
@path = result
|
|
609
|
+
@path_version = @document.version
|
|
610
|
+
end
|
|
611
|
+
result
|
|
612
|
+
end
|
|
613
|
+
|
|
614
|
+
def css_path
|
|
615
|
+
return @css_path if memo_hit?(@css_path_version)
|
|
616
|
+
result =
|
|
617
|
+
if path.nil?
|
|
618
|
+
nil
|
|
619
|
+
else
|
|
620
|
+
path.split("/").filter_map do |part|
|
|
621
|
+
next nil if part.empty?
|
|
622
|
+
part.gsub(/\[(\d+)\]/, ':nth-of-type(\1)')
|
|
623
|
+
end.join(" > ")
|
|
624
|
+
end
|
|
625
|
+
if @document
|
|
626
|
+
@css_path = result
|
|
627
|
+
@css_path_version = @document.version
|
|
628
|
+
end
|
|
629
|
+
result
|
|
630
|
+
end
|
|
631
|
+
|
|
632
|
+
# Deep copy in a NEW document via Document.copy_of (the single
|
|
633
|
+
# copy seam — comment/PI children and namespaces both survive,
|
|
634
|
+
# #696/#721/#812; the copier is pool-threaded, ~2.3x Nokogiri;
|
|
635
|
+
# non-element kinds rebuild by value — #161).
|
|
636
|
+
def dup
|
|
637
|
+
ensure_alive!
|
|
638
|
+
# TODO.perf/30: elements copy in one C dispatch (engine
|
|
639
|
+
# create + handle + element_copy + rooted wrap). The
|
|
640
|
+
# namespace lift stays a Ruby decision — the copy_of seam's
|
|
641
|
+
# semantics (#696/#721/#812) run below when needed.
|
|
642
|
+
if element? && @addr_reads_fast
|
|
643
|
+
doc = Leptris::XML::Native.copy_binding_element(
|
|
644
|
+
@document, @c_address)
|
|
645
|
+
unless doc.nil?
|
|
646
|
+
# copy_of's contract returns the copied ROOT (the last
|
|
647
|
+
# expression of the root= assignment), not the document.
|
|
648
|
+
copied = doc.root
|
|
649
|
+
unless Leptris::XML::Element.skip_adoption_lift?(copied)
|
|
650
|
+
Leptris::XML::Element.lift_namespaces_for_adoption(copied, {})
|
|
651
|
+
end
|
|
652
|
+
return copied
|
|
653
|
+
end
|
|
654
|
+
end
|
|
655
|
+
Leptris::XML::Document.copy_of(self)
|
|
656
|
+
end
|
|
657
|
+
alias_method :clone, :dup
|
|
658
|
+
|
|
659
|
+
def ==(other)
|
|
660
|
+
return false unless other.is_a?(Leptris::XML::Node)
|
|
661
|
+
c_ptr == other.c_ptr
|
|
662
|
+
end
|
|
663
|
+
|
|
664
|
+
def inspect
|
|
665
|
+
"#<#{self.class.name} ptr=#{c_ptr}>"
|
|
666
|
+
end
|
|
667
|
+
|
|
668
|
+
protected
|
|
669
|
+
|
|
670
|
+
# A memo is valid while the document's mutation version has not
|
|
671
|
+
# advanced since that memo was stored. Each memoized field carries
|
|
672
|
+
# its OWN stamp — a shared node-level stamp would let one field's
|
|
673
|
+
# recompute resurrect another field's stale memo. Readonly
|
|
674
|
+
# documents never advance the version, so their memos are forever
|
|
675
|
+
# valid (ADR 0003 semantics); writable documents gain memos
|
|
676
|
+
# between mutations.
|
|
677
|
+
def memo_hit?(stamp)
|
|
678
|
+
@document && stamp == @document.version
|
|
679
|
+
end
|
|
680
|
+
|
|
681
|
+
def as_element_or_self
|
|
682
|
+
is_a?(Leptris::XML::Element) ? self : nil
|
|
683
|
+
end
|
|
684
|
+
|
|
685
|
+
include Leptris::XML::Searchable
|
|
686
|
+
end
|