Class: Leptris::XML::Node

Inherits:
Object
  • Object
show all
Includes:
Searchable
Defined in:
lib/leptris/xml/node.rb

Direct Known Subclasses

Comment, Element, ProcessingInstruction, Text

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Searchable

#at, #at_css, #at_xpath, #css, #search, wrap_xpath_first_result, wrap_xpath_result, #xpath

Constructor Details

#initialize(c_ptr, document, parent: nil, node_type: nil) ⇒ Node

Returns a new instance of Node.



6
7
8
9
10
11
12
13
# File 'lib/leptris/xml/node.rb', line 6

def initialize(c_ptr, document, parent: nil, node_type: nil)
  @c_ptr = c_ptr
  @document = document
  @parent = parent
  # wrap() already calls leptris_node_get_type for dispatch; reusing
  # the result makes every predicate and #type call FFI-free.
  @node_type = node_type
end

Instance Attribute Details

#c_ptrObject (readonly)

Returns the value of attribute c_ptr.



4
5
6
# File 'lib/leptris/xml/node.rb', line 4

def c_ptr
  @c_ptr
end

#documentObject (readonly)

Returns the value of attribute document.



4
5
6
# File 'lib/leptris/xml/node.rb', line 4

def document
  @document
end

Class Method Details

.wrap(c_ptr, document, parent: nil, node_type: nil) ⇒ Object

node_type: callers holding a batch-fetched kind (the XPath result-set batch fills out_kinds) pass it so the wrap skips the get_type dispatch; nil (the default) dispatches as before.



18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
# File 'lib/leptris/xml/node.rb', line 18

def self.wrap(c_ptr, document, parent: nil, node_type: nil)
  # Per-document weak-ref cache. Returns the existing wrapper when the
  # same c_ptr is wrapped twice (common in children/sibling walks,
  # repeated xpath queries, traverse-then-access patterns). The cache
  # dies with the document so no stale entries. The miss path
  # resolves the cache and address once — a cold walk wraps every
  # node exactly once and pays both only on the store.
  if document
    cache = document.wrapper_cache
    address = c_ptr.address
    if (cached = cache[address])
      return cached
    end
  end

  node_type ||= Leptris::XML::FFI.leptris_node_get_type(c_ptr)
  node =
    case node_type
    when Leptris::XML::FFI::NODE_ELEMENT
      Leptris::XML::Element.new(c_ptr, document, parent: parent, node_type: node_type)
    when Leptris::XML::FFI::NODE_TEXT
      Leptris::XML::Text.new(c_ptr, document, parent: parent, node_type: node_type)
    when Leptris::XML::FFI::NODE_COMMENT
      Leptris::XML::Comment.new(c_ptr, document, parent: parent, node_type: node_type)
    when Leptris::XML::FFI::NODE_CDATA
      Leptris::XML::CDATA.new(c_ptr, document, parent: parent, node_type: node_type)
    when Leptris::XML::FFI::NODE_PI
      Leptris::XML::ProcessingInstruction.new(c_ptr, document, parent: parent, node_type: node_type)
    else
      new(c_ptr, document, parent: parent, node_type: node_type)
    end

  cache[address] = node if document
  node
end

Instance Method Details

#<=>(other) ⇒ Object



143
144
145
146
147
148
# File 'lib/leptris/xml/node.rb', line 143

def <=>(other)
  return nil unless other.is_a?(Leptris::XML::Node)
  return nil unless @document == other.document
  ensure_alive!
  Leptris::XML::FFI.leptris_node_compare(@c_ptr, other.c_ptr)
end

#==(other) ⇒ Object



382
383
384
385
# File 'lib/leptris/xml/node.rb', line 382

def ==(other)
  return false unless other.is_a?(Leptris::XML::Node)
  @c_ptr == other.c_ptr
end

#cdata?Boolean

Returns:

  • (Boolean)


83
# File 'lib/leptris/xml/node.rb', line 83

def cdata?;    type == Leptris::XML::FFI::NODE_CDATA;    end

#childObject



150
151
152
153
154
155
# File 'lib/leptris/xml/node.rb', line 150

def child
  ensure_alive!
  ptr = Leptris::XML::FFI.leptris_node_first_child(@c_ptr)
  return nil if ptr.null?
  Leptris::XML::Node.wrap(ptr, @document, parent: as_element_or_self)
end

#childrenObject



157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
# File 'lib/leptris/xml/node.rb', line 157

def children
  # Immutable in readonly mode: the batch fetch plus wrapper
  # construction is paid once. The kinds ride the batch
  # (leptris_node_children_ex), so no per-child get_type.
  return @children if memo_hit?(@children_version)
  ensure_alive!
  parent = as_element_or_self
  pointers, kinds = Leptris::XML::FFI.fetch_children(@c_ptr)
  nodes = Array.new(pointers.size) do |i|
    Leptris::XML::Node.wrap(pointers[i], @document,
                            parent: parent, node_type: kinds[i])
  end
  result = Leptris::XML::NodeSet.new(@document, nodes)
  if @document
    @children = result
    @children_version = @document.version
  end
  result
end

#comment?Boolean

Returns:

  • (Boolean)


82
# File 'lib/leptris/xml/node.rb', line 82

def comment?;  type == Leptris::XML::FFI::NODE_COMMENT;  end

#contentObject

Raises:

  • (NotImplementedError)


58
59
60
# File 'lib/leptris/xml/node.rb', line 58

def content
  raise NotImplementedError, "#{self.class}#content not implemented"
end

#css_pathObject



354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
# File 'lib/leptris/xml/node.rb', line 354

def css_path
  return @css_path if memo_hit?(@css_path_version)
  result =
    if path.nil?
      nil
    else
      path.split("/").filter_map do |part|
        next nil if part.empty?
        part.gsub(/\[(\d+)\]/, ':nth-of-type(\1)')
      end.join(" > ")
    end
  if @document
    @css_path = result
    @css_path_version = @document.version
  end
  result
end

#dupObject Also known as: clone



372
373
374
375
376
377
378
379
# File 'lib/leptris/xml/node.rb', line 372

def dup
  ensure_alive!
  elem_ptr = Leptris::XML::FFI.leptris_node_as_element(@c_ptr)
  raise Leptris::XML::Error, "dup is only supported for element nodes" if elem_ptr.null?
  copy_ptr = Leptris::XML::FFI.leptris_element_copy(elem_ptr, @document.c_ptr)
  raise Leptris::XML::Error, "leptris_element_copy failed" if copy_ptr.null?
  Leptris::XML::Node.wrap(copy_ptr, @document)
end

#element?Boolean

Returns:

  • (Boolean)


80
# File 'lib/leptris/xml/node.rb', line 80

def element?;  type == Leptris::XML::FFI::NODE_ELEMENT;  end

#element_childrenObject Also known as: elements



231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
# File 'lib/leptris/xml/node.rb', line 231

def element_children
  return @element_children if memo_hit?(@element_children_version)
  ensure_alive!
  # Element receivers ride the element-only batch: text/comment
  # children are never wrapped (nor their get_type paid — the
  # ELEMENT hint rides along). Other nodes keep the filter.
  result =
    if is_a?(Leptris::XML::Element)
      parent = as_element_or_self
      Leptris::XML::FFI.fetch_element_children(@c_ptr).map do |ptr|
        Leptris::XML::Node.wrap(ptr, @document, parent: parent,
                                node_type: Leptris::XML::FFI::NODE_ELEMENT)
      end
    else
      children.select(&:element?)
    end
  if @document
    @element_children = result
    @element_children_version = @document.version
  end
  result
end

#ensure_alive!Object

Borrowed-handle lifetime: every c_ptr dereference is valid only while the owning document lives. Parentless nodes (iterparse yields) cannot validate and are skipped. The guard runs before every uncached FFI dispatch, so it uses the cheapest sufficient check: #free nils the document's c_ptr, and the GC-finalizer path cannot fire while any handle (which strongly references the document) exists. Document#freed? remains the accurate public predicate.



105
106
107
108
109
110
# File 'lib/leptris/xml/node.rb', line 105

def ensure_alive!
  if @document && @document.c_ptr.nil?
    raise Leptris::XML::UseAfterFreeError,
      "owning document has been freed — handle used on #{inspect}"
  end
end

#ensure_writable!Object

Raises ReadOnlyError when the owning document was marked readonly, UseAfterFreeError when it was freed. Every node-level mutation passes through this gate, so it is where the document's mutation version advances — the invalidation behind writable-document memoization. Bumping before the C call is conservative: a failed mutation merely discards memos.



118
119
120
121
122
123
124
125
126
# File 'lib/leptris/xml/node.rb', line 118

def ensure_writable!
  ensure_alive!
  if readonly_document?
    raise Leptris::XML::ReadOnlyError,
      "document is readonly — mutation attempted on #{inspect}"
  end
  @document.advance_version
  nil
end

#first_element_childObject



193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
# File 'lib/leptris/xml/node.rb', line 193

def first_element_child
  return @first_element_child if memo_hit?(@first_element_child_version)
  ensure_alive!
  # Raw pointer scan: non-element siblings are typed with one C
  # call each — never wrapped, never cached — and the found
  # element carries the ELEMENT hint into the wrap.
  ptr = Leptris::XML::FFI.leptris_node_first_child(@c_ptr)
  result = nil
  until ptr.nil? || ptr.null?
    if Leptris::XML::FFI.leptris_node_get_type(ptr) ==
        Leptris::XML::FFI::NODE_ELEMENT
      result = Leptris::XML::Node.wrap(
        ptr, @document, parent: as_element_or_self,
        node_type: Leptris::XML::FFI::NODE_ELEMENT)
      break
    end
    ptr = Leptris::XML::FFI.leptris_node_next_sibling(ptr)
  end
  if @document
    @first_element_child = result
    @first_element_child_version = @document.version
  end
  result
end

#inner_textObject



69
70
71
# File 'lib/leptris/xml/node.rb', line 69

def inner_text
  content
end

#inspectObject



387
388
389
# File 'lib/leptris/xml/node.rb', line 387

def inspect
  "#<#{self.class.name} ptr=#{c_ptr}>"
end

#last_element_childObject



218
219
220
221
222
223
224
225
226
227
228
229
# File 'lib/leptris/xml/node.rb', line 218

def last_element_child
  # Element receivers: the element-only batch fetches pointers
  # without wrapping any text child; only the last is wrapped.
  if is_a?(Leptris::XML::Element)
    kids = Leptris::XML::FFI.fetch_element_children(@c_ptr)
    return nil if kids.empty?
    return Leptris::XML::Node.wrap(
      kids.last, @document, parent: self,
      node_type: Leptris::XML::FFI::NODE_ELEMENT)
  end
  children.reverse_each.find(&:element?)
end

#lineObject



138
139
140
141
# File 'lib/leptris/xml/node.rb', line 138

def line
  ensure_alive!
  Leptris::XML::FFI.leptris_node_line(@c_ptr)
end

#nameObject

Raises:

  • (NotImplementedError)


54
55
56
# File 'lib/leptris/xml/node.rb', line 54

def name
  raise NotImplementedError, "#{self.class}#name not implemented"
end

#next_elementObject



255
256
257
258
259
# File 'lib/leptris/xml/node.rb', line 255

def next_element
  sibling = next_sibling
  sibling = sibling.next_sibling until sibling.nil? || sibling.element?
  sibling
end

#next_siblingObject Also known as: next



177
178
179
180
181
182
# File 'lib/leptris/xml/node.rb', line 177

def next_sibling
  ensure_alive!
  ptr = Leptris::XML::FFI.leptris_node_next_sibling(@c_ptr)
  return nil if ptr.null?
  Leptris::XML::Node.wrap(ptr, @document, parent: @parent)
end

#parentObject



89
90
91
92
93
94
95
# File 'lib/leptris/xml/node.rb', line 89

def parent
  return @parent if @parent
  ensure_alive!
  ptr = Leptris::XML::FFI.leptris_node_parent(@c_ptr)
  return nil if ptr.null?
  Leptris::XML::Node.wrap(ptr, @document)
end

#pathObject



342
343
344
345
346
347
348
349
350
351
352
# File 'lib/leptris/xml/node.rb', line 342

def path
  return @path if memo_hit?(@path_version)
  ensure_alive!
  str_ptr = Leptris::XML::FFI.leptris_node_get_xpath(@c_ptr)
  result = str_ptr.null? ? nil : Leptris::XML::FFI.read_owned_string(str_ptr)
  if @document
    @path = result
    @path_version = @document.version
  end
  result
end

#previous_elementObject



261
262
263
264
265
# File 'lib/leptris/xml/node.rb', line 261

def previous_element
  sibling = previous_sibling
  sibling = sibling.previous_sibling until sibling.nil? || sibling.element?
  sibling
end

#previous_siblingObject Also known as: previous



185
186
187
188
189
190
# File 'lib/leptris/xml/node.rb', line 185

def previous_sibling
  ensure_alive!
  ptr = Leptris::XML::FFI.leptris_node_previous_sibling(@c_ptr)
  return nil if ptr.null?
  Leptris::XML::Node.wrap(ptr, @document, parent: @parent)
end

#processing_instruction?Boolean Also known as: pi?

Returns:

  • (Boolean)


84
85
86
# File 'lib/leptris/xml/node.rb', line 84

def processing_instruction?
  type == Leptris::XML::FFI::NODE_PI
end

#readonly_document?Boolean

Readonly is one-way, so caching TRUE is sound: once observed, the document is readonly forever. FALSE stays uncached (the document may still flip). Saves the document round-trip on per-read guards.

Returns:

  • (Boolean)


132
133
134
135
136
# File 'lib/leptris/xml/node.rb', line 132

def readonly_document?
  return true if instance_variable_defined?(:@readonly_document)
  return false unless @document&.readonly?
  @readonly_document = true
end

#textObject

Dispatching defs, not alias_method: an alias snapshots this base #content (the raise), so subclass overrides would never be seen through the alias. A plain method resolves #content per-call.



65
66
67
# File 'lib/leptris/xml/node.rb', line 65

def text
  content
end

#text?Boolean

Returns:

  • (Boolean)


81
# File 'lib/leptris/xml/node.rb', line 81

def text?;     type == Leptris::XML::FFI::NODE_TEXT;     end

#traverseObject

Walks the subtree in post-order DFS (matches Nokogiri's semantics): the receiver, its descendants, nothing else.

One FFI call dispatches the whole walk; the C engine invokes the callback once per visited node (the only per-node cost is the C-to-Ruby callback dispatch, not FFI round-trips).

Subtree-bounded by abort-at-self (leptris-ruby#89): the C walker was never bounded — after visiting the receiver it pushes the receiver's NEXT SIBLING and continues to the end of the document chain. In post-order the receiver is the LAST node of its own subtree, so returning non-zero at self stops the walk exactly at the boundary (the C loop honors a non-zero callback return). The self comparison is by address; the receiver's handle is stable for the walk's duration.

Exceptions raised by the block are re-raised after the walk (leptris-ruby#90): a rescue inside the callback stashes the exception and returns non-zero, aborting the C walk — without it the FFI dispatch silently swallowed the exception and the walk continued with partially processed data.



322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
# File 'lib/leptris/xml/node.rb', line 322

def traverse
  return enum_for(:traverse) unless block_given?
  ensure_alive!
  error = nil
  self_address = @c_ptr.address
  callback = ::FFI::Function.new(:int, [:pointer, :pointer], blocking: true) do |node_ptr, _|
    begin
      yield Leptris::XML::Node.wrap(node_ptr, @document)
      node_ptr.address == self_address ? 1 : 0
    rescue Exception => e # rubocop:disable Lint/RescueException
      error = e
      1
    end
  end
  Leptris::XML::FFI.leptris_node_traverse(
    @c_ptr, Leptris::XML::FFI::TRAVERSE_POST_ORDER, callback, nil)
  raise error if error
  self
end

#typeObject Also known as: node_type



73
74
75
76
77
# File 'lib/leptris/xml/node.rb', line 73

def type
  return @node_type if @node_type
  ensure_alive!
  @node_type = Leptris::XML::FFI.leptris_node_get_type(@c_ptr)
end


267
268
269
270
271
272
273
# File 'lib/leptris/xml/node.rb', line 267

def unlink
  ensure_writable!
  Leptris::XML::FFI.check_status(
    Leptris::XML::FFI.leptris_node_unlink(@c_ptr))
  @parent = nil
  self
end

#visit(&block) ⇒ Object

Visits the subtree with ONE C call (leptris_node_visit, libleptris 1.9.20 — upstream #645a): elements yield twice — (node, true, depth) before their children, (node, false, depth) after the subtree completes — every other kind once with entering=true; depth counts element levels from the receiver. No NodeSet, pointer array, or children memo per level: the leanest full-subtree iteration the binding offers (a document receiver walks the document child chain). The walk is read-only — mutate only between visits.

root.visit { |node, entering, depth| ... }


287
288
289
290
291
292
293
294
295
296
297
298
299
# File 'lib/leptris/xml/node.rb', line 287

def visit(&block)
  return enum_for(:visit) unless block
  ensure_alive!
  document = @document
  visitor = ::FFI::Function.new(
    :void, [:pointer, :pointer, :int, :int], blocking: true) do |_, node_ptr, entering, depth|
    block.call(
      Leptris::XML::Node.wrap(node_ptr, document),
      entering == 1, depth)
  end
  Leptris::XML::FFI.leptris_node_visit(@c_ptr, visitor, nil)
  self
end