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_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



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

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



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

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



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

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



132
133
134
135
136
137
138
139
140
141
142
143
144
# File 'lib/leptris/xml/node.rb', line 132

def children
  # Immutable in readonly mode: the batch fetch plus wrapper
  # construction is paid once.
  return @children if readonly_cached?(:@children)
  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)
  @children = result if @document&.readonly?
  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



231
232
233
234
235
236
237
238
239
240
241
242
243
244
# File 'lib/leptris/xml/node.rb', line 231

def css_path
  return @css_path if readonly_cached?(:@css_path)
  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
  @css_path = result if @document&.readonly?
  result
end

#dupObject Also known as: clone



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

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



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

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



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

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

#first_element_childObject



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

def first_element_child
  ensure_alive!
  ptr = Leptris::XML::FFI.leptris_node_first_child(@c_ptr)
  until ptr.nil? || ptr.null?
    node = Leptris::XML::Node.wrap(ptr, @document, parent: as_element_or_self)
    return node if node.element?
    ptr = Leptris::XML::FFI.leptris_node_next_sibling(ptr)
  end
  nil
end

#inner_textObject



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

def inner_text
  content
end

#inspectObject



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

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

#last_element_childObject



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

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

#lineObject



113
114
115
116
# File 'lib/leptris/xml/node.rb', line 113

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



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

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

#next_siblingObject Also known as: next



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

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



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

def path
  return @path if readonly_cached?(:@path)
  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)
  @path = result if @document&.readonly?
  result
end

#previous_elementObject



191
192
193
194
195
# File 'lib/leptris/xml/node.rb', line 191

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

#previous_siblingObject Also known as: previous



154
155
156
157
158
159
# File 'lib/leptris/xml/node.rb', line 154

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

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



211
212
213
214
215
216
217
218
219
220
# File 'lib/leptris/xml/node.rb', line 211

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


197
198
199
200
201
202
203
# File 'lib/leptris/xml/node.rb', line 197

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