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



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

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

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)


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



274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
# File 'lib/leptris/xml/node.rb', line 274

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



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

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



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

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.



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



190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
# File 'lib/leptris/xml/node.rb', line 190

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



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

def inner_text
  content
end

#inspectObject



307
308
309
# File 'lib/leptris/xml/node.rb', line 307

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

#last_element_childObject



210
211
212
# File 'lib/leptris/xml/node.rb', line 210

def last_element_child
  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



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

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

#next_siblingObject Also known as: next



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

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



262
263
264
265
266
267
268
269
270
271
272
# File 'lib/leptris/xml/node.rb', line 262

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



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

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

#previous_siblingObject Also known as: previous



182
183
184
185
186
187
# File 'lib/leptris/xml/node.rb', line 182

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

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



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

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



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


237
238
239
240
241
242
243
# File 'lib/leptris/xml/node.rb', line 237

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