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



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

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



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

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



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

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



127
128
129
130
131
132
133
134
135
136
137
138
139
# File 'lib/leptris/xml/node.rb', line 127

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



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

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



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

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



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

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.



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

def ensure_alive!
  if @document&.freed?
    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.



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

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

#first_element_childObject



157
158
159
160
161
162
163
164
165
166
# File 'lib/leptris/xml/node.rb', line 157

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



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

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

#last_element_childObject



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

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

#lineObject



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

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



180
181
182
183
184
# File 'lib/leptris/xml/node.rb', line 180

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

#next_siblingObject Also known as: next



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

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



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

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



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

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

#previous_siblingObject Also known as: previous



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

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

Specialized hot path: skips the intermediate NodeSet allocation that Element#children would create, walking via raw FFI calls and wrapping nodes directly. Saves one Array + one NodeSet allocation per parent node. For a tree of N nodes that's ~N fewer allocations on a full traversal.

Still pays ~2 FFI calls per visited node (first_child + next_sibling). Beating Nokogiri on this benchmark needs C-side traverse with a callback (libleptris #273); the per-node FFI cost is the floor.



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

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


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

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