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.
Files changed (124) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +3890 -0
  3. data/CLAUDE.md +115 -0
  4. data/CONTEXT.md +39 -0
  5. data/LICENSE.md +33 -0
  6. data/README.adoc +755 -0
  7. data/Rakefile +407 -0
  8. data/TODO.perf/01-ext-accelerated-default-reads.md +28 -0
  9. data/TODO.perf/02-auto-enable-native-layer.md +19 -0
  10. data/TODO.perf/03-bulk-xpath-result-materialization.md +18 -0
  11. data/TODO.perf/04-ext-bound-serialization.md +15 -0
  12. data/TODO.perf/05-native-surface-completion.md +19 -0
  13. data/TODO.perf/06-perf-battery-native-legs.md +16 -0
  14. data/TODO.perf/07-native-read-floor.md +31 -0
  15. data/TODO.perf/08-version-coherent-native-mutations.md +24 -0
  16. data/TODO.perf/09-adoption-lift-fast-path.md +38 -0
  17. data/TODO.perf/10-bulk-attribute-materialization.md +20 -0
  18. data/TODO.perf/11-moxml-gap-battery.md +18 -0
  19. data/TODO.perf/12-document-lifetime-in-c.md +47 -0
  20. data/TODO.perf/13-structural-memos.md +49 -0
  21. data/TODO.perf/14-insert-family-c-bound.md +31 -0
  22. data/TODO.perf/15-compiled-expression-cache.md +35 -0
  23. data/TODO.perf/16-at-xpath-single-result-seam.md +38 -0
  24. data/TODO.perf/17-precomputed-fast-path-flags.md +35 -0
  25. data/TODO.perf/18-inner-html-one-c-pass.md +32 -0
  26. data/TODO.perf/19-lazy-node-pointer.md +39 -0
  27. data/TODO.perf/20-css-translation-cache.md +24 -0
  28. data/TODO.perf/21-key-memo-consult.md +20 -0
  29. data/TODO.perf/22-eager-nodeset-materialization.md +30 -0
  30. data/TODO.perf/23-cbound-value-mutations.md +27 -0
  31. data/TODO.perf/24-immutable-read-lanes.md +22 -0
  32. data/TODO.perf/25-scope-owned-bulk-path.md +39 -0
  33. data/TODO.perf/26-post-mutation-memo-seeding.md +26 -0
  34. data/TODO.perf/27-c-yield-traversal.md +38 -0
  35. data/TODO.perf/28-address-fills.md +25 -0
  36. data/TODO.perf/29-ns-xpath-compiled.md +23 -0
  37. data/TODO.perf/30-copy-and-element-child-faces.md +31 -0
  38. data/TODO.perf/31-battery-rounds-4-8.md +23 -0
  39. data/TODO.perf/32-clean-host-floor-table.md +29 -0
  40. data/TODO.perf/33-cbound-root-set.md +29 -0
  41. data/TODO.perf/34-fragment-fast-lane.md +41 -0
  42. data/TODO.perf/35-parse-default-c-face.md +24 -0
  43. data/TODO.perf/36-raise-in-c-serializer-encoding.md +34 -0
  44. data/TODO.perf/37-document-lazy-pointer.md +28 -0
  45. data/TODO.perf/38-bulk-walk-and-bench-gate.md +23 -0
  46. data/TODO.restructure/01-constraint-compliance-audit.md +40 -0
  47. data/TODO.restructure/02-deep-copy-seam.md +21 -0
  48. data/TODO.restructure/03-evaluation-context-seam.md +18 -0
  49. data/TODO.restructure/04-spec-mece-restructure.md +20 -0
  50. data/TODO.restructure/05-memory-documentation.md +13 -0
  51. data/TODO.restructure/06-architecture-map-refresh.md +12 -0
  52. data/TODO.restructure/07-external-gates.md +16 -0
  53. data/TODO.restructure/08-new-code-audit-and-ext-memo.md +11 -0
  54. data/TODO.restructure/09-iteration-scope.md +29 -0
  55. data/TODO.restructure/10-cold-attr-read-diet.md +19 -0
  56. data/TODO.restructure/11-construction-factory-diet.md +16 -0
  57. data/TODO.restructure/12-resultattr-merge.md +12 -0
  58. data/TODO.restructure/13-spec-mece-followup.md +9 -0
  59. data/TODO.restructure/14-constraint-audit-refresh.md +15 -0
  60. data/TODO.restructure/15-readme-feature-documentation.md +10 -0
  61. data/TODO.restructure/16-claudemd-map-refresh.md +5 -0
  62. data/TODO.restructure/17-perf-drift-record.md +12 -0
  63. data/TODO.restructure/18-audit-spec-doubles-and-gem-contents.md +12 -0
  64. data/TODO.restructure/19-gitignore-hygiene.md +10 -0
  65. data/TODO.restructure/20-utf8proc-enablement.md +34 -0
  66. data/TODO.restructure/21-typeddata-variant-prototype.md +42 -0
  67. data/TODO.restructure/22-sax-drain-bulk-strip.md +28 -0
  68. data/docs/adr/0001-lockstep-mirror.md +17 -0
  69. data/docs/adr/0002-utf8-at-the-seam.md +19 -0
  70. data/docs/adr/0003-readonly-memoization-pattern.md +34 -0
  71. data/docs/adr/0004-lifetime-guard.md +21 -0
  72. data/docs/adr/0005-autoload-manifest-ordering.md +19 -0
  73. data/docs/adr/0006-ruby-variant-policy.md +25 -0
  74. data/ext/build_windows_native.rb +42 -0
  75. data/ext/leptris/native/extconf.rb +55 -0
  76. data/ext/leptris/native/native.c +2646 -0
  77. data/leptris.gemspec +43 -0
  78. data/lib/leptris/version.rb +5 -0
  79. data/lib/leptris/xml/attr.rb +76 -0
  80. data/lib/leptris/xml/c_string_array.rb +37 -0
  81. data/lib/leptris/xml/cdata.rb +27 -0
  82. data/lib/leptris/xml/comment.rb +27 -0
  83. data/lib/leptris/xml/css_to_xpath.rb +192 -0
  84. data/lib/leptris/xml/descriptor.rb +214 -0
  85. data/lib/leptris/xml/diff.rb +74 -0
  86. data/lib/leptris/xml/doc_type.rb +54 -0
  87. data/lib/leptris/xml/document.rb +716 -0
  88. data/lib/leptris/xml/document_fragment.rb +63 -0
  89. data/lib/leptris/xml/element.rb +771 -0
  90. data/lib/leptris/xml/entity_reference.rb +13 -0
  91. data/lib/leptris/xml/evaluation_context.rb +32 -0
  92. data/lib/leptris/xml/ffi.rb +1527 -0
  93. data/lib/leptris/xml/iteration_scope.rb +56 -0
  94. data/lib/leptris/xml/iterparse.rb +144 -0
  95. data/lib/leptris/xml/namespace.rb +43 -0
  96. data/lib/leptris/xml/native.so +0 -0
  97. data/lib/leptris/xml/native_layer.rb +92 -0
  98. data/lib/leptris/xml/node.rb +686 -0
  99. data/lib/leptris/xml/node_set.rb +212 -0
  100. data/lib/leptris/xml/parse_options.rb +97 -0
  101. data/lib/leptris/xml/plan_value.rb +121 -0
  102. data/lib/leptris/xml/processing_instruction.rb +90 -0
  103. data/lib/leptris/xml/pull.rb +212 -0
  104. data/lib/leptris/xml/relaxng.rb +126 -0
  105. data/lib/leptris/xml/result_attr.rb +39 -0
  106. data/lib/leptris/xml/result_text.rb +36 -0
  107. data/lib/leptris/xml/sax/document.rb +45 -0
  108. data/lib/leptris/xml/sax/dom_dispatch.rb +144 -0
  109. data/lib/leptris/xml/sax/parser.rb +326 -0
  110. data/lib/leptris/xml/sax/recorder.rb +374 -0
  111. data/lib/leptris/xml/sax.rb +14 -0
  112. data/lib/leptris/xml/schematron.rb +88 -0
  113. data/lib/leptris/xml/searchable.rb +361 -0
  114. data/lib/leptris/xml/serialization.rb +180 -0
  115. data/lib/leptris/xml/text.rb +32 -0
  116. data/lib/leptris/xml/xpath.rb +89 -0
  117. data/lib/leptris/xml/xquery.rb +77 -0
  118. data/lib/leptris/xml/xslt.rb +85 -0
  119. data/lib/leptris/xml.rb +108 -0
  120. data/lib/leptris.rb +53 -0
  121. data/lib/libleptris.so +0 -0
  122. data/lib/libutf8proc.so.3 +0 -0
  123. data/scripts/gem_smoke.rb +36 -0
  124. metadata +214 -0
@@ -0,0 +1,771 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Leptris::XML::Element < Leptris::XML::Node
4
+ # TODO.perf/01: with the native bundle loaded, hot reads go
5
+ # through the ext (one C-API dispatch + rb_str_new_cstr — no FFI
6
+ # marshaling; ~3x per read). defined? is near-free when absent.
7
+ # Scope-owned (iterparse-yield) elements stay on the FFI path:
8
+ # their pool addresses recycle across yields and their lifetime
9
+ # rules ride the IterationScope seam — the ext fast path is for
10
+ # document-owned trees.
11
+ def native_fast?
12
+ @native_fast
13
+ end
14
+
15
+ def name
16
+ return @name if @name
17
+ ensure_alive!
18
+ @name = if @addr_reads_fast
19
+ Leptris::XML::Native.fast_name(@c_address)
20
+ else
21
+ Leptris::XML::FFI.leptris_element_name(c_ptr)
22
+ end
23
+ end
24
+ alias_method :node_name, :name
25
+
26
+ def name=(new_name)
27
+ if @native_fast
28
+ Leptris::XML::Native.set_binding_name(
29
+ @document, @c_address, new_name)
30
+ return @name = new_name
31
+ end
32
+ ensure_writable!
33
+ Leptris::XML::FFI.check_status(
34
+ Leptris::XML::FFI.leptris_element_set_name(c_ptr, new_name))
35
+ @name = new_name
36
+ end
37
+ alias_method :node_name=, :name=
38
+
39
+ def content
40
+ return @content if memo_hit?(@content_version)
41
+ ensure_alive!
42
+ result = if @addr_reads_fast
43
+ Leptris::XML::Native.fast_element_text(@c_address)
44
+ else
45
+ Leptris::XML::FFI.leptris_element_text(c_ptr)
46
+ end
47
+ if @document
48
+ @content = result
49
+ @content_version = @document.version
50
+ end
51
+ result
52
+ end
53
+
54
+ def content=(new_content)
55
+ if @native_fast
56
+ Leptris::XML::Native.set_binding_text(
57
+ @document, @c_address, new_content.to_s)
58
+ @content = new_content.to_s
59
+ @content_version = @document.version if @document
60
+ return new_content
61
+ end
62
+ ensure_writable!
63
+ Leptris::XML::FFI.check_status(
64
+ Leptris::XML::FFI.leptris_element_set_text(c_ptr, new_content.to_s))
65
+ @content = new_content.to_s
66
+ @content_version = @document.version if @document
67
+ new_content
68
+ end
69
+
70
+ def [](key)
71
+ # BARE names serve from the versioned attributes hash (written
72
+ # name == the only legal key for a no-namespace attribute);
73
+ # materializes on demand, invalidates through the mutation gate
74
+ # (ADR 0003's 1.9.14 extension); a hash read cannot
75
+ # use-after-free. QUALIFIED names (with a colon) go to the
76
+ # engine: they resolve through in-scope declarations, where the
77
+ # written prefix never matters — the hash cannot answer them.
78
+ #
79
+ # Cold single-attribute reads (ruby#150): the previous path
80
+ # materialised EVERY attribute into two hashes on the first
81
+ # bare read (~17 allocs on a 2-attr element). Now a cold miss
82
+ # pays ONE FFI call and fills only that name; subsequent
83
+ # same-name reads hit the hash; a later attributes/keys call
84
+ # rebuilds the full face. Full-memo (@attributes set) still
85
+ # answers misses as nil without another FFI.
86
+ name = key.to_s
87
+ if @document && !name.include?(":")
88
+ values = @attr_values
89
+ if values && @attributes_version == @document.version
90
+ v = values[name]
91
+ # Present, or a FULL-memo miss (absent). A partial-memo nil
92
+ # is ambiguous (un-queried vs absent) — one idempotent
93
+ # engine call resolves it, then the name is cached.
94
+ return v if !v.nil? || @attributes
95
+ ensure_alive!
96
+ v = if @addr_reads_fast
97
+ Leptris::XML::Native.fast_attribute(@c_address, name)
98
+ else
99
+ Leptris::XML::FFI.leptris_element_attribute(c_ptr, name)
100
+ end
101
+ values[name] = v
102
+ return v
103
+ end
104
+ # Completely cold: one read, start a partial values hash.
105
+ # Drop any full-face memos from a prior version — otherwise a
106
+ # post-mutation cold [] would stamp the new version onto a
107
+ # stale @attributes hash and attributes would serve it.
108
+ ensure_alive!
109
+ v = if @addr_reads_fast
110
+ Leptris::XML::Native.fast_attribute(@c_address, name)
111
+ else
112
+ Leptris::XML::FFI.leptris_element_attribute(c_ptr, name)
113
+ end
114
+ @attr_values = { name => v }
115
+ @attributes = nil
116
+ @attribute_nodes = nil
117
+ @keys = nil
118
+ @values = nil
119
+ @attributes_version = @document.version
120
+ return v
121
+ end
122
+ ensure_alive!
123
+ v = Leptris::XML::FFI.leptris_element_attribute(c_ptr, name)
124
+ # Namespace-aware misses (an undeclared prefix never resolves
125
+ # through in-scope declarations) fall back to the WRITTEN name
126
+ # — the same flat face #attributes exposes, so a value written
127
+ # as el["n:x"] reads back through el["n:x"] (#161).
128
+ return v unless v.nil?
129
+ attributes[name]&.value
130
+ end
131
+ alias_method :attr, :[]
132
+ alias_method :get_attribute, :[]
133
+
134
+ def []=(key, value)
135
+ # C-bound write (TODO.perf/11): gates + version bump + engine
136
+ # set in one dispatch; the bump drops the version-stamped
137
+ # attribute memos on both surfaces.
138
+ if native_fast_children?
139
+ Leptris::XML::Native.set_binding_attribute(
140
+ @document, @c_address, key.to_s, value.to_s)
141
+ seed_attribute_memo(key.to_s, value.to_s)
142
+ return value
143
+ end
144
+ ensure_writable!
145
+ Leptris::XML::FFI.check_status(
146
+ Leptris::XML::FFI.leptris_element_set_attribute(c_ptr, key.to_s, value.to_s))
147
+ seed_attribute_memo(key.to_s, value.to_s)
148
+ value
149
+ end
150
+ alias_method :set_attribute, :[]=
151
+
152
+ # TODO.perf/26: the write knows the new value — seed the
153
+ # version-stamped memo so a read-back skips re-derivation. A
154
+ # valid-and-just-invalidated partial memo extends in place; a
155
+ # full face (or anything older) restarts partial, mirroring the
156
+ # cold-[] rules.
157
+ def seed_attribute_memo(name, written)
158
+ return unless @document
159
+ values = @attr_values
160
+ if values && !@attributes &&
161
+ @attributes_version == @document.version - 1
162
+ values[name] = written
163
+ else
164
+ @attr_values = { name => written }
165
+ @attributes = nil
166
+ @attribute_nodes = nil
167
+ @keys = nil
168
+ @values = nil
169
+ end
170
+ @attributes_version = @document.version
171
+ end
172
+ private :seed_attribute_memo
173
+
174
+ def key?(name)
175
+ ensure_alive!
176
+ # Versioned attribute memo first (TODO.perf/21): a hit proves
177
+ # presence without the engine round-trip; the memo cannot
178
+ # prove absence for a partial fill, so misses fall through.
179
+ n = name.to_s
180
+ values = @attr_values
181
+ if @document && values && @attributes_version == @document.version &&
182
+ values.key?(n)
183
+ return true
184
+ end
185
+ return true if Leptris::XML::FFI.leptris_element_has_attribute(
186
+ c_ptr, n) != 0
187
+ # Same written-name fallback as #[] — the engine lookup is
188
+ # namespace-aware and misses undeclared prefixes (#161).
189
+ attributes.key?(n)
190
+ end
191
+ alias_method :has_attribute?, :key?
192
+
193
+ # Expanded-name attribute access (libleptris 1.8.0): URI + local
194
+ # name with XML Namespaces 1.0 semantics — the written prefix
195
+ # never matters, only what it resolves to through this element's
196
+ # in-scope declarations. uri nil/"" matches no-namespace
197
+ # attributes only; xmlns declarations are invisible.
198
+ def attribute_ns(uri, local)
199
+ ensure_alive!
200
+ Leptris::XML::FFI.leptris_element_attribute_ns(
201
+ c_ptr, uri&.to_s, local.to_s)
202
+ end
203
+
204
+ def has_attribute_ns?(uri, local)
205
+ ensure_alive!
206
+ Leptris::XML::FFI.leptris_element_has_attribute_ns(
207
+ c_ptr, uri&.to_s, local.to_s) != 0
208
+ end
209
+
210
+ def remove_attribute(name)
211
+ ensure_writable!
212
+ Leptris::XML::FFI.check_status(
213
+ Leptris::XML::FFI.leptris_element_remove_attribute(c_ptr, name.to_s))
214
+ self
215
+ end
216
+ alias_method :delete, :remove_attribute
217
+
218
+ # Iterates the element's attributes via the v1.1.0 linked-list face
219
+ # (one FFI call per attribute; the name_at/value_at indexing API
220
+ # re-walks the list per index, making it O(n^2) per element).
221
+ # The C attribute handle rides along so Attr can serve the
222
+ # per-attribute namespace accessors (libleptris 1.8.0).
223
+ def each_attribute
224
+ return enum_for(:each_attribute) unless block_given?
225
+ ensure_alive!
226
+ attr = Leptris::XML::FFI.leptris_element_first_attribute(c_ptr)
227
+ until attr.nil? || attr.null?
228
+ name = Leptris::XML::FFI.leptris_attribute_get_name(attr)
229
+ value = Leptris::XML::FFI.leptris_attribute_get_value(c_ptr, attr)
230
+ yield Leptris::XML::Attr.new(name, value, self, c_handle: attr)
231
+ attr = Leptris::XML::FFI.leptris_attribute_next(attr)
232
+ end
233
+ self
234
+ end
235
+
236
+ def keys
237
+ return @keys if memo_hit?(@keys_version)
238
+ result = each_attribute.to_a.map(&:name)
239
+ if @document
240
+ @keys = result
241
+ @keys_version = @document.version
242
+ end
243
+ result
244
+ end
245
+
246
+ def values
247
+ return @values if memo_hit?(@values_version)
248
+ result = each_attribute.to_a.map(&:value)
249
+ if @document
250
+ @values = result
251
+ @values_version = @document.version
252
+ end
253
+ result
254
+ end
255
+
256
+ def attributes
257
+ # Require the full Attr-hash face — a cold [] may have set the
258
+ # version with only a partial @attr_values (ruby#150).
259
+ return @attributes if @attributes && memo_hit?(@attributes_version)
260
+ # TODO.perf/28: the bulk faces touch no document state —
261
+ # scope-eligible via @addr_reads_fast.
262
+ if @addr_reads_fast
263
+ # TODO.perf/10: both memo faces in one C walk.
264
+ result, values = Leptris::XML::Native.bulk_attr_faces(
265
+ @c_address, self)
266
+ if @document
267
+ @attributes = result
268
+ @attr_values = values
269
+ @attributes_version = @document.version
270
+ end
271
+ return result
272
+ end
273
+ result = {}
274
+ values = {}
275
+ each_attribute do |attr|
276
+ result[attr.name] = attr
277
+ values[attr.name] = attr.value
278
+ end
279
+ if @document
280
+ @attributes = result
281
+ @attr_values = values
282
+ @attributes_version = @document.version
283
+ end
284
+ result
285
+ end
286
+
287
+ def attribute_nodes
288
+ return @attribute_nodes if memo_hit?(@attribute_nodes_version)
289
+ result = each_attribute.to_a
290
+ if @document
291
+ @attribute_nodes = result
292
+ @attribute_nodes_version = @document.version
293
+ end
294
+ result
295
+ end
296
+
297
+ # The element's own namespace prefix (e.g. "foo" for <foo:child/>),
298
+ # or nil when the element has none.
299
+ def prefix
300
+ ensure_alive!
301
+ if native_fast?
302
+ Leptris::XML::Native.fast_prefix(@c_address)
303
+ else
304
+ result = Leptris::XML::FFI.leptris_element_prefix(c_ptr)
305
+ result if result && !result.empty?
306
+ end
307
+ end
308
+
309
+ # The written qualified name — "foo:child" with a prefix, the
310
+ # bare local name without. The explicit contract behind the
311
+ # #name/#prefix split: #name answers the local part, the
312
+ # serialized form uses this spelling (#161).
313
+ def qualified_name
314
+ p = prefix
315
+ p ? "#{p}:#{name}" : name
316
+ end
317
+
318
+ # Cross-document and cross-scope adoption (#178 ask 1): the
319
+ # engine's attach entries move a subtree but do not lift the
320
+ # source's in-scope namespace declarations, so serialized output
321
+ # can carry undeclared prefixes. Re-declares on +node+ everything
322
+ # from its in-scope set that +target_scope+ (the attach target's
323
+ # in-scope map) does not already resolve identically — the
324
+ # declarations ride the moved element, staying local to the
325
+ # adopted subtree. Call BEFORE attaching: the source scope is
326
+ # read through the node's current ancestors. Identical scopes
327
+ # (the common same-document move) add nothing.
328
+ # TODO.perf/09: one dispatch answers whether ANY lift work can
329
+ # apply (no resolved namespace, no own declarations = provable
330
+ # no-op — the common programmatic-build shape). Checked at the
331
+ # mutation sites so the target's in-scope namespaces are never
332
+ # materialized when the answer is no.
333
+ def self.skip_adoption_lift?(node)
334
+ # Non-elements can never need a lift (lift's own first line
335
+ # no-ops them) — skipping also avoids materializing the
336
+ # target's namespaces for the text/comment children every
337
+ # build appends.
338
+ return true unless node.is_a?(Leptris::XML::Element)
339
+ return false unless defined?(Leptris::XML::NATIVE_FAST)
340
+ !Leptris::XML::Native.ns_lift_needed?(node.c_ptr.address)
341
+ end
342
+
343
+ def self.lift_namespaces_for_adoption(node, target_scope)
344
+ return unless node.is_a?(Leptris::XML::Element)
345
+ # Declarations the node already carries (its own definitions —
346
+ # preserved by the C copy and by moves) ride it as-is; lifting
347
+ # must not duplicate them, only what came from ANCESTORS.
348
+ own = node.namespace_definitions.each_with_object({}) do |ns, h|
349
+ h[ns.prefix ? "xmlns:#{ns.prefix}" : "xmlns"] = ns.href
350
+ end
351
+ node.namespaces.each do |key, uri|
352
+ next if target_scope[key] == uri || own[key] == uri
353
+ prefix = key.start_with?("xmlns:") ? key.delete_prefix("xmlns:") : nil
354
+ node.add_namespace_definition(prefix, uri)
355
+ end
356
+ # libxml2 reparent parity (moxml #208 lineage): an OWN
357
+ # declaration the attach target already resolves identically
358
+ # is redundant — prune it so serialized output does not
359
+ # re-declare (libxml2 drops these on reparent). Shadowing
360
+ # declarations (same prefix, different URI) are preserved.
361
+ node.namespace_definitions.each do |ns|
362
+ key = ns.prefix ? "xmlns:#{ns.prefix}" : "xmlns"
363
+ next unless target_scope[key] == ns.href
364
+ node.remove_namespace_definition(ns.prefix)
365
+ end
366
+ nil
367
+ end
368
+
369
+ def prepend_child(node)
370
+ # C-bound insert (TODO.perf/14): gates + predicate + version
371
+ # bump + engine insert in one dispatch; Qnil = the child needs
372
+ # the namespace lift — the full path below handles it.
373
+ if native_fast_children?
374
+ unless Leptris::XML::Native.insert_binding_child(
375
+ @document, @c_address, node.c_ptr.address, 1).nil?
376
+ Leptris::XML::Node.invalidate_cross_document!(node, @document)
377
+ return node
378
+ end
379
+ end
380
+ ensure_writable!
381
+ unless Leptris::XML::Element.skip_adoption_lift?(node)
382
+ Leptris::XML::Element.lift_namespaces_for_adoption(node, namespaces)
383
+ end
384
+ Leptris::XML::FFI.check_status(
385
+ Leptris::XML::FFI.leptris_element_prepend_child(c_ptr, node.c_ptr))
386
+ Leptris::XML::Node.invalidate_cross_document!(node, @document)
387
+ node
388
+ end
389
+
390
+ def add_next_sibling(node)
391
+ # C-bound insert (TODO.perf/14): gates + predicate + version
392
+ # bump + engine insert in one dispatch; Qnil = the child needs
393
+ # the namespace lift — the full path below handles it.
394
+ if native_fast_children?
395
+ unless Leptris::XML::Native.insert_binding_child(
396
+ @document, @c_address, node.c_ptr.address, 2).nil?
397
+ Leptris::XML::Node.invalidate_cross_document!(node, @document)
398
+ return node
399
+ end
400
+ end
401
+ ensure_writable!
402
+ unless Leptris::XML::Element.skip_adoption_lift?(node)
403
+ Leptris::XML::Element.lift_namespaces_for_adoption(node, namespaces)
404
+ end
405
+ Leptris::XML::FFI.check_status(
406
+ Leptris::XML::FFI.leptris_element_insert_after(c_ptr, node.c_ptr))
407
+ Leptris::XML::Node.invalidate_cross_document!(node, @document)
408
+ node
409
+ end
410
+
411
+ def add_previous_sibling(node)
412
+ # C-bound insert (TODO.perf/14): gates + predicate + version
413
+ # bump + engine insert in one dispatch; Qnil = the child needs
414
+ # the namespace lift — the full path below handles it.
415
+ if native_fast_children?
416
+ unless Leptris::XML::Native.insert_binding_child(
417
+ @document, @c_address, node.c_ptr.address, 3).nil?
418
+ Leptris::XML::Node.invalidate_cross_document!(node, @document)
419
+ return node
420
+ end
421
+ end
422
+ ensure_writable!
423
+ unless Leptris::XML::Element.skip_adoption_lift?(node)
424
+ Leptris::XML::Element.lift_namespaces_for_adoption(node, namespaces)
425
+ end
426
+ Leptris::XML::FFI.check_status(
427
+ Leptris::XML::FFI.leptris_element_insert_before(c_ptr, node.c_ptr))
428
+ Leptris::XML::Node.invalidate_cross_document!(node, @document)
429
+ node
430
+ end
431
+
432
+ def remove_child(node)
433
+ ensure_writable!
434
+ Leptris::XML::FFI.check_status(
435
+ Leptris::XML::FFI.leptris_element_remove_child(c_ptr, node.c_ptr))
436
+ node
437
+ end
438
+
439
+ def children=(node_or_nodes)
440
+ ensure_writable!
441
+ # Remove existing children, then attach the new ones in source order.
442
+ Leptris::XML::FFI.check_status(
443
+ Leptris::XML::FFI.leptris_element_remove_children(c_ptr))
444
+ Array(node_or_nodes).each { |n| add_child(n) }
445
+ end
446
+
447
+ # Replace this element with +new_node+ in the parent's child list.
448
+ # +new_node+ must belong to the same document. Returns +new_node+.
449
+ def replace(new_node)
450
+ parent = self.parent
451
+ raise Leptris::XML::Error, "cannot replace a node with no parent" unless parent
452
+ add_next_sibling(new_node)
453
+ parent.remove_child(self)
454
+ new_node
455
+ end
456
+
457
+ # Like #replace but returns self for chaining.
458
+ def swap(new_node)
459
+ replace(new_node)
460
+ self
461
+ end
462
+
463
+ # Wrap this element in a new element parsed from +markup+ or a dup of
464
+ # +node+. The wrapper takes this element's place in the tree, and this
465
+ # element becomes its only child. Returns self for chaining.
466
+ def wrap(node_or_markup)
467
+ wrapper =
468
+ case node_or_markup
469
+ when Leptris::XML::Element then node_or_markup.dup
470
+ when String
471
+ frag_doc = Leptris::XML::Document.parse(node_or_markup)
472
+ frag_doc.root or raise Leptris::XML::Error, "wrap markup has no root element"
473
+ else
474
+ raise ArgumentError, "wrap expects a String or Element, got #{node_or_markup.class}"
475
+ end
476
+
477
+ parent = self.parent
478
+ raise Leptris::XML::Error, "cannot wrap a node with no parent" unless parent
479
+
480
+ # Insert wrapper at self's position, then move self into wrapper.
481
+ # add_child moves self (unlinks from old parent first), so no explicit
482
+ # remove_child needed — and trying to remove after the move corrupts
483
+ # the C tree (libleptris silently handles non-child args badly).
484
+ add_next_sibling(wrapper)
485
+ wrapper.add_child(self)
486
+ self
487
+ end
488
+
489
+ # Deep copy in a NEW document via Document.copy_of (the single
490
+ # copy seam — every child kind and namespace survives,
491
+ # #696/#721/#812). TODO.perf/30: one C dispatch when the ext
492
+ # is loaded — the namespace-lift decision stays here.
493
+ def dup
494
+ ensure_alive!
495
+ if @addr_reads_fast
496
+ doc = Leptris::XML::Native.copy_binding_element(
497
+ @document, @c_address)
498
+ unless doc.nil?
499
+ copied = doc.root
500
+ unless Leptris::XML::Element.skip_adoption_lift?(copied)
501
+ Leptris::XML::Element.lift_namespaces_for_adoption(copied, {})
502
+ end
503
+ return copied
504
+ end
505
+ end
506
+ Leptris::XML::Document.copy_of(self)
507
+ end
508
+ alias_method :clone, :dup
509
+
510
+ # Fused create+append (libleptris 1.9.153): builds a child
511
+ # element of +name+ in this element's document and attaches it
512
+ # in ONE C call — the builder-shape single call
513
+ # (leptris_element_create_child). Tree semantics identical to
514
+ # document.create_element(name) + add_child.
515
+ def create_child(name)
516
+ ensure_writable!
517
+ ptr = Leptris::XML::FFI.leptris_element_create_child(c_ptr, name.to_s)
518
+ raise Leptris::XML::Error,
519
+ "leptris_element_create_child failed for #{name.inspect}" if ptr.null?
520
+ Leptris::XML::Node.wrap_fresh(ptr, @document, Leptris::XML::FFI::NODE_ELEMENT)
521
+ end
522
+
523
+ def add_child(node_or_markup)
524
+ ensure_writable!
525
+ case node_or_markup
526
+ when Leptris::XML::Node
527
+ # C-bound append (TODO.perf/08-09): one dispatch runs the
528
+ # readonly/liveness gates, the provable no-op lift
529
+ # predicate, the version bump, and the engine append. Qnil
530
+ # means the child needs the namespace lift — fall through
531
+ # to the full path.
532
+ if native_fast_children?
533
+ # The face raises on failure (TODO.perf/36); Qnil = the
534
+ # child needs the lift — the full path handles it.
535
+ unless Leptris::XML::Native.append_binding_child(
536
+ @document, @c_address, node_or_markup.c_ptr.address).nil?
537
+ Leptris::XML::Node.invalidate_cross_document!(node_or_markup, @document)
538
+ return node_or_markup
539
+ end
540
+ end
541
+ unless Leptris::XML::Element.skip_adoption_lift?(node_or_markup)
542
+ Leptris::XML::Element.lift_namespaces_for_adoption(node_or_markup, namespaces)
543
+ end
544
+ Leptris::XML::FFI.check_status(
545
+ Leptris::XML::FFI.leptris_element_append_child(c_ptr, node_or_markup.c_ptr))
546
+ Leptris::XML::Node.invalidate_cross_document!(node_or_markup, @document)
547
+ node_or_markup
548
+ when String
549
+ # TODO.perf/34: one dispatch parses the markup and appends
550
+ # every fragment child (single gate + bump). Negative
551
+ # returns: -1 re-runs the FFI path for the exact parse
552
+ # error; -1000-st routes through check_status.
553
+ if native_fast_children?
554
+ r = Leptris::XML::Native.append_markup(
555
+ @document, @c_address, node_or_markup)
556
+ if r != -1
557
+ # Appends raise in C on status failures (TODO.perf/36);
558
+ # -1 alone means the parse failed.
559
+ return Leptris::XML::NodeSet.new(@document, []) if r.zero?
560
+ return Leptris::XML::NodeSet.new(@document, last_added(r))
561
+ end
562
+ # parse failure: reproduce the exact error via the legacy
563
+ # path (it re-fails fast)
564
+ Leptris::XML::DocumentFragment.parse(node_or_markup, @document)
565
+ end
566
+ frag = Leptris::XML::DocumentFragment.parse(node_or_markup, @document)
567
+ added = []
568
+ frag.children.each do |n|
569
+ Leptris::XML::FFI.check_status(
570
+ Leptris::XML::FFI.leptris_element_append_child(c_ptr, n.c_ptr))
571
+ added << n
572
+ end
573
+ Leptris::XML::NodeSet.new(@document, added)
574
+ else
575
+ raise ArgumentError, "add_child expects a Node or String, got #{node_or_markup.class}"
576
+ end
577
+ end
578
+ # The markup-append face appends in fragment order; the added
579
+ # nodes are this element's last +count+ children.
580
+ def last_added(count)
581
+ kids = children.to_a
582
+ kids.last(count)
583
+ end
584
+ private :last_added
585
+
586
+ alias_method :<<, :add_child
587
+
588
+ def namespace
589
+ return @namespace if memo_hit?(@namespace_version)
590
+ ensure_alive!
591
+ uri = Leptris::XML::FFI.leptris_element_namespace(c_ptr)
592
+ result =
593
+ if uri.nil? || uri.empty?
594
+ nil
595
+ else
596
+ # The resolved namespace is reached via this element's own
597
+ # prefix, so carry it through: consumers that distinguish
598
+ # {"p" => "urn:p"} from the default {"nil => "urn:p"} need it.
599
+ prefix = Leptris::XML::FFI.leptris_element_prefix(c_ptr)
600
+ prefix = nil if prefix.nil? || prefix.empty?
601
+ Leptris::XML::Namespace.new(self, uri, prefix: prefix)
602
+ end
603
+ if @document
604
+ @namespace = result
605
+ @namespace_version = @document.version
606
+ end
607
+ result
608
+ end
609
+
610
+ # Nokogiri node.namespace= semantics (libleptris 1.9.76, #817):
611
+ # nil (or "") detaches the namespace — the resolved link and the
612
+ # name's prefix clear, and an xmlns="" undeclaration blocks
613
+ # in-scope defaults from capturing the unqualified name. A URI
614
+ # rebinds to an in-scope declaration carrying it (adopting its
615
+ # prefix); a URI with no in-scope declaration raises — declare
616
+ # it first with #add_namespace_definition.
617
+ def namespace=(uri)
618
+ ensure_writable!
619
+ Leptris::XML::FFI.check_status(
620
+ Leptris::XML::FFI.leptris_element_set_namespace(c_ptr, uri&.to_s))
621
+ uri
622
+ end
623
+
624
+ # The local name, prefix, and resolved namespace URI in ONE
625
+ # call (libleptris 1.9.144) — the adapter fan-out read.
626
+ # Equivalent to #name + #prefix + #namespace, single entry.
627
+ def expanded_name
628
+ ensure_alive!
629
+ local = ::FFI::MemoryPointer.new(:pointer)
630
+ prefix = ::FFI::MemoryPointer.new(:pointer)
631
+ uri = ::FFI::MemoryPointer.new(:pointer)
632
+ Leptris::XML::FFI.leptris_element_expanded_name(
633
+ c_ptr, local, prefix, uri)
634
+ local_ptr = local.read_pointer
635
+ prefix_ptr = prefix.read_pointer
636
+ uri_ptr = uri.read_pointer
637
+ result = {
638
+ local: local_ptr.read_string,
639
+ prefix: prefix_ptr.null? ? nil : prefix_ptr.read_string,
640
+ namespace_uri: uri_ptr.null? ? nil : uri_ptr.read_string,
641
+ }
642
+ local.free
643
+ prefix.free
644
+ uri.free
645
+ result
646
+ end
647
+
648
+ def namespace_definitions
649
+ return @namespace_definitions if memo_hit?(@namespace_definitions_version)
650
+ ensure_alive!
651
+ count = Leptris::XML::FFI.leptris_element_namespace_count(c_ptr)
652
+ result = count.times.map do |i|
653
+ prefix = Leptris::XML::FFI.leptris_element_namespace_decl_prefix(c_ptr, i)
654
+ uri = Leptris::XML::FFI.leptris_element_namespace_decl_uri(c_ptr, i)
655
+ Leptris::XML::Namespace.new(self, uri, prefix: prefix)
656
+ end
657
+ if @document
658
+ @namespace_definitions = result
659
+ @namespace_definitions_version = @document.version
660
+ end
661
+ result
662
+ end
663
+
664
+ def namespaces
665
+ return @namespaces if memo_hit?(@namespaces_version)
666
+ scopes = {}
667
+ node = self
668
+ while node.is_a?(Leptris::XML::Element)
669
+ node.namespace_definitions.each do |ns|
670
+ key = ns.prefix ? "xmlns:#{ns.prefix}" : "xmlns"
671
+ scopes[key] ||= ns.href
672
+ end
673
+ # unstamped: this walk also runs inside the adoption lift,
674
+ # between the version bump and the engine move (see
675
+ # Node#unstamped_parent).
676
+ node = node.unstamped_parent
677
+ end
678
+ if @document
679
+ @namespaces = scopes
680
+ @namespaces_version = @document.version
681
+ end
682
+ scopes
683
+ end
684
+
685
+ # The serialized children (Nokogiri parity): elements serialize
686
+ # through the engine (correct escaping), text is XML-escaped,
687
+ # comments/CDATA/PIs render their literal forms. The complement
688
+ # of #inner_text, which returns the unescaped text content.
689
+ def inner_html
690
+ ensure_alive!
691
+ # One C pass (TODO.perf/18): the child chain, per-kind
692
+ # serialization, and the escape set all in one dispatch —
693
+ # byte-identical to the Ruby loop below.
694
+ return Leptris::XML::Native.fast_inner_xml(@c_address) if
695
+ @native_fast
696
+ children.map do |child|
697
+ case child
698
+ when Leptris::XML::Element
699
+ Leptris::XML::Serialization.element_xml_default(child.c_ptr)
700
+ when Leptris::XML::CDATA # before Text: CDATA subclasses it
701
+ "<![CDATA[#{child.content}]]>"
702
+ when Leptris::XML::Text
703
+ Leptris::XML::Serialization.escape_text(child.content)
704
+ when Leptris::XML::Comment then "<!--#{child.content}-->"
705
+ when Leptris::XML::ProcessingInstruction
706
+ data = child.content
707
+ data.empty? ? "<?#{child.name}?>" : "<?#{child.name} #{data}?>"
708
+ else
709
+ Leptris::XML::Serialization.escape_text(child.content.to_s)
710
+ end
711
+ end.join
712
+ end
713
+
714
+ # indent_text: a STRING is the indent unit with Nokogiri's
715
+ # semantics (leptris-ruby#109; via a C-side copy into a fresh
716
+ # document — no element-level ext entry exists yet). true is NOT
717
+ # supported here: the display form is document-level.
718
+ def to_xml(indent: 0, no_decl: false, encoding: nil, indent_text: false,
719
+ expand_empty: false)
720
+ ensure_alive!
721
+ if indent_text == true
722
+ raise ArgumentError,
723
+ "indent_text: true (display form) is document-level — " \
724
+ "pass the unit string for elements"
725
+ end
726
+ return Leptris::XML::Serialization.to_xml_element_unit(
727
+ self, indent_text, indent: indent) if indent_text.is_a?(String)
728
+ return Leptris::XML::Serialization.element_xml_expand_empty(c_ptr) if expand_empty
729
+ Leptris::XML::Serialization.to_xml(
730
+ Leptris::XML::Serialization::ELEMENT_SERIALIZE_INTO, c_ptr,
731
+ indent: indent, no_decl: no_decl, encoding: encoding)
732
+ end
733
+
734
+ def canonicalize(version = Leptris::XML::FFI::C14N_1_0,
735
+ inclusive_namespaces = nil,
736
+ with_comments: false,
737
+ exclusive: false,
738
+ mode: nil)
739
+ resolved_mode = mode || (exclusive ? Leptris::XML::FFI::C14N_MODE_EXCLUSIVE
740
+ : Leptris::XML::FFI::C14N_MODE_CANONICAL)
741
+ Leptris::XML::Serialization.canonicalize(
742
+ Leptris::XML::FFI.method(:leptris_c14n_canonicalize_subtree_ex), c_ptr,
743
+ version: version, mode: resolved_mode,
744
+ inclusive_namespaces: inclusive_namespaces,
745
+ with_comments: with_comments)
746
+ end
747
+ alias_method :c14n, :canonicalize
748
+
749
+ def add_namespace_definition(prefix, href)
750
+ ensure_writable!
751
+ Leptris::XML::FFI.check_status(
752
+ Leptris::XML::FFI.leptris_element_add_namespace_definition(
753
+ c_ptr, prefix.to_s, href.to_s))
754
+ Leptris::XML::Namespace.new(self, href.to_s, prefix: prefix.nil? ? nil : prefix.to_s)
755
+ end
756
+ alias_method :add_namespace, :add_namespace_definition
757
+
758
+ def default_namespace=(href)
759
+ ensure_writable!
760
+ Leptris::XML::FFI.check_status(
761
+ Leptris::XML::FFI.leptris_element_set_default_namespace(c_ptr, href.to_s))
762
+ href
763
+ end
764
+
765
+ def remove_namespace_definition(prefix)
766
+ ensure_writable!
767
+ Leptris::XML::FFI.check_status(
768
+ Leptris::XML::FFI.leptris_element_remove_namespace_definition(c_ptr, prefix.to_s))
769
+ self
770
+ end
771
+ end