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
# 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.
  if document && (cached = document.wrapper_cache[c_ptr.address])
    return cached
  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

  document.wrapper_cache[c_ptr.address] = node if document
  node
end

Instance Method Details

#<=>(other) ⇒ Object



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

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



296
297
298
299
# File 'lib/leptris/xml/node.rb', line 296

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

#cdata?Boolean

Returns:

  • (Boolean)


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

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

#childObject



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

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



151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
# File 'lib/leptris/xml/node.rb', line 151

def children
  # Immutable in readonly mode: the batch fetch plus wrapper
  # construction is paid once.
  return @children if memo_hit?(@children_version)
  ensure_alive!
  parent = as_element_or_self
  nodes = Leptris::XML::FFI.fetch_children(@c_ptr).map do |ptr|
    Leptris::XML::Node.wrap(ptr, @document, parent: parent)
  end
  result = Leptris::XML::NodeSet.new(@document, nodes)
  if @document
    @children = result
    @children_version = @document.version
  end
  result
end

#comment?Boolean

Returns:

  • (Boolean)


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

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

#contentObject

Raises:

  • (NotImplementedError)


52
53
54
# File 'lib/leptris/xml/node.rb', line 52

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

#css_pathObject



268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
# File 'lib/leptris/xml/node.rb', line 268

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



286
287
288
289
290
291
292
293
# File 'lib/leptris/xml/node.rb', line 286

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)


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

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

#element_childrenObject Also known as: elements



208
209
210
211
212
213
214
215
216
# File 'lib/leptris/xml/node.rb', line 208

def element_children
  return @element_children if memo_hit?(@element_children_version)
  result = children.select(&:element?)
  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.



99
100
101
102
103
104
# File 'lib/leptris/xml/node.rb', line 99

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.



112
113
114
115
116
117
118
119
120
# File 'lib/leptris/xml/node.rb', line 112

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



184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
# File 'lib/leptris/xml/node.rb', line 184

def first_element_child
  return @first_element_child if memo_hit?(@first_element_child_version)
  ensure_alive!
  ptr = Leptris::XML::FFI.leptris_node_first_child(@c_ptr)
  result = nil
  until ptr.nil? || ptr.null?
    node = Leptris::XML::Node.wrap(ptr, @document, parent: as_element_or_self)
    if node.element?
      result = node
      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



63
64
65
# File 'lib/leptris/xml/node.rb', line 63

def inner_text
  content
end

#inspectObject



301
302
303
# File 'lib/leptris/xml/node.rb', line 301

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

#last_element_childObject



204
205
206
# File 'lib/leptris/xml/node.rb', line 204

def last_element_child
  children.reverse_each.find(&:element?)
end

#lineObject



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

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

#nameObject

Raises:

  • (NotImplementedError)


48
49
50
# File 'lib/leptris/xml/node.rb', line 48

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

#next_elementObject



219
220
221
222
223
# File 'lib/leptris/xml/node.rb', line 219

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

#next_siblingObject Also known as: next



168
169
170
171
172
173
# File 'lib/leptris/xml/node.rb', line 168

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



83
84
85
86
87
88
89
# File 'lib/leptris/xml/node.rb', line 83

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



256
257
258
259
260
261
262
263
264
265
266
# File 'lib/leptris/xml/node.rb', line 256

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



225
226
227
228
229
# File 'lib/leptris/xml/node.rb', line 225

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

#previous_siblingObject Also known as: previous



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

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)


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

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)


126
127
128
129
130
# File 'lib/leptris/xml/node.rb', line 126

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.



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

def text
  content
end

#text?Boolean

Returns:

  • (Boolean)


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

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

#traverseObject

Walks the subtree in post-order DFS (matches Nokogiri's semantics).

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).



245
246
247
248
249
250
251
252
253
254
# File 'lib/leptris/xml/node.rb', line 245

def traverse
  return enum_for(:traverse) unless block_given?
  ensure_alive!
  callback = ::FFI::Function.new(:int, [:pointer, :pointer], blocking: true) do |node_ptr, _|
    yield Leptris::XML::Node.wrap(node_ptr, @document)
    0
  end
  Leptris::XML::FFI.leptris_node_traverse(
    @c_ptr, Leptris::XML::FFI::TRAVERSE_POST_ORDER, callback, nil)
end

#typeObject Also known as: node_type



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

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


231
232
233
234
235
236
237
# File 'lib/leptris/xml/node.rb', line 231

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