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) ⇒ Object



15
16
17
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
# File 'lib/leptris/xml/node.rb', line 15

def self.wrap(c_ptr, document, parent: 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



134
135
136
137
138
139
# File 'lib/leptris/xml/node.rb', line 134

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



293
294
295
296
# File 'lib/leptris/xml/node.rb', line 293

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

#cdata?Boolean

Returns:

  • (Boolean)


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

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

#childObject



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

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



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

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)


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

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

#contentObject

Raises:

  • (NotImplementedError)


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

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

#css_pathObject



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

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



283
284
285
286
287
288
289
290
# File 'lib/leptris/xml/node.rb', line 283

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)


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

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

#element_childrenObject Also known as: elements



205
206
207
208
209
210
211
212
213
# File 'lib/leptris/xml/node.rb', line 205

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.



96
97
98
99
100
101
# File 'lib/leptris/xml/node.rb', line 96

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.



109
110
111
112
113
114
115
116
117
# File 'lib/leptris/xml/node.rb', line 109

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



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

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



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

def inner_text
  content
end

#inspectObject



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

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

#last_element_childObject



201
202
203
# File 'lib/leptris/xml/node.rb', line 201

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

#lineObject



129
130
131
132
# File 'lib/leptris/xml/node.rb', line 129

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

#nameObject

Raises:

  • (NotImplementedError)


45
46
47
# File 'lib/leptris/xml/node.rb', line 45

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

#next_elementObject



216
217
218
219
220
# File 'lib/leptris/xml/node.rb', line 216

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

#next_siblingObject Also known as: next



165
166
167
168
169
170
# File 'lib/leptris/xml/node.rb', line 165

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



80
81
82
83
84
85
86
# File 'lib/leptris/xml/node.rb', line 80

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



253
254
255
256
257
258
259
260
261
262
263
# File 'lib/leptris/xml/node.rb', line 253

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



222
223
224
225
226
# File 'lib/leptris/xml/node.rb', line 222

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

#previous_siblingObject Also known as: previous



173
174
175
176
177
178
# File 'lib/leptris/xml/node.rb', line 173

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)


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

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)


123
124
125
126
127
# File 'lib/leptris/xml/node.rb', line 123

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.



56
57
58
# File 'lib/leptris/xml/node.rb', line 56

def text
  content
end

#text?Boolean

Returns:

  • (Boolean)


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

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



242
243
244
245
246
247
248
249
250
251
# File 'lib/leptris/xml/node.rb', line 242

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



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

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


228
229
230
231
232
233
234
# File 'lib/leptris/xml/node.rb', line 228

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