Class: Leptris::XML::NodeSet

Inherits:
Object
  • Object
show all
Includes:
Enumerable, Searchable
Defined in:
lib/leptris/xml/node_set.rb

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

Constructor Details

#initialize(document, source = nil) ⇒ NodeSet

Two construction modes:

  • eager: pass an Array of Nodes (e.g. Element#children builds one)
  • lazy: pass an FFI::Pointer to a LeptrisXPathResult that this NodeSet will keep alive and free on GC. Each [i] / each call goes through leptris_xpath_result_get instead of pre-materializing.


16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
# File 'lib/leptris/xml/node_set.rb', line 16

def initialize(document, source = nil)
  @document = document
  case source
  when ::FFI::Pointer
    if source.null?
      @result_ptr = nil
      @array = []
    else
      # Wrap in AutoPointer for automatic GC-time cleanup. NodeSet has no
      # explicit #free method (Nokogiri doesn't either), so the AutoPointer
      # double-free risk that Document#free hit doesn't apply here.
      @result_ptr = ::FFI::AutoPointer.new(source, Leptris::XML::FFI.method(:leptris_xpath_result_free))
      @array = nil
    end
  when nil
    @result_ptr = nil
    @array = []
  else
    @result_ptr = nil
    @array = source.to_a
  end
end

Instance Attribute Details

#documentObject (readonly)

Returns the value of attribute document.



9
10
11
# File 'lib/leptris/xml/node_set.rb', line 9

def document
  @document
end

Class Method Details

.from_result(document, result_ptr) ⇒ Object



39
40
41
# File 'lib/leptris/xml/node_set.rb', line 39

def self.from_result(document, result_ptr)
  new(document, result_ptr)
end

Instance Method Details

#[](idx) ⇒ Object

The materialized array (after to_a) is authoritative: every reader consults it first so a materialized set stops paying the batch fetch on each iteration or index.



56
57
58
59
60
61
62
63
64
65
66
67
68
# File 'lib/leptris/xml/node_set.rb', line 56

def [](idx)
  return @array[idx] if @array
  # Negative indexes are Ruby-Array semantics (Nokogiri's NodeSet
  # is slice-like); materialize rather than answer nil
  # inconsistently with an eager set.
  return to_a[idx] if idx.negative?
  if @result_ptr
    return nil if idx < 0 || idx >= length
    ptr = Leptris::XML::FFI.leptris_xpath_result_get_node(@result_ptr, idx)
    return nil if ptr.null?
    Leptris::XML::Node.wrap(ptr, @document)
  end
end

#eachObject



70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
# File 'lib/leptris/xml/node_set.rb', line 70

def each
  return enum_for(:each) unless block_given?
  return @array.each { |n| yield n } if @array
  if @result_ptr
    # Batch-fetch all node pointers through the FFI seam
    # (leptris_xpath_result_get_nodes_ex copies every node kind,
    # not just elements) and wrap each. Wrappers are cached
    # per-Document via Node.wrap, so re-iteration of the same
    # NodeSet hits it.
    n = length
    if n > 0
      pointers = Leptris::XML::FFI.fetch_result_nodes(@result_ptr, n)
      pointers.each do |ptr|
        next if ptr.null?
        yield Leptris::XML::Node.wrap(ptr, @document)
      end
      # The batch accessor under-copies mixed-kind results (upstream
      # leptris#477); fall back to per-index fetch for the remainder.
      (pointers.length...n).each do |i|
        ptr = Leptris::XML::FFI.leptris_xpath_result_get_node(@result_ptr, i)
        next if ptr.null?
        yield Leptris::XML::Node.wrap(ptr, @document)
      end
    end
  else
    @array.each { |n| yield n }
  end
  self
end

#empty?Boolean

Returns:

  • (Boolean)


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

def empty?
  length == 0
end

#first(n = nil) ⇒ Object



110
111
112
113
# File 'lib/leptris/xml/node_set.rb', line 110

def first(n = nil)
  return self[0] if n.nil?
  n.times.map { |i| self[i] }.take_while { |x| !x.nil? }
end

#inner_textObject Also known as: text



123
124
125
# File 'lib/leptris/xml/node_set.rb', line 123

def inner_text
  map(&:content).join
end

#inspectObject



148
149
150
# File 'lib/leptris/xml/node_set.rb', line 148

def inspect
  to_a.inspect
end

#lastObject



115
116
117
118
119
120
121
# File 'lib/leptris/xml/node_set.rb', line 115

def last
  if @result_ptr
    self[length - 1]
  else
    @array.last
  end
end

#lengthObject Also known as: size



43
44
45
46
# File 'lib/leptris/xml/node_set.rb', line 43

def length
  return @array.length if @array
  @result_ptr ? Leptris::XML::FFI.leptris_xpath_result_count(@result_ptr) : 0
end

#to_aObject Also known as: to_ary



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

def to_a
  return @array if @array
  return [] if @result_ptr.nil?
  out = []
  each { |n| out << n }
  @array = out  # memoize so subsequent to_a / each avoids re-fetch
  out
end

#xpath(*paths) ⇒ Object

Union semantics: evaluate the expression against each element member (one C eval per member — the engine has no multi-context eval yet; leptris/leptris ask pending) and accumulate into ONE NodeSet, instead of merging NodeSets per member.

Raises:

  • (ArgumentError)


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

def xpath(*paths)
  handler, _ns, _vars = parse_search_args(paths)
  raise ArgumentError, "custom XPath handlers not supported" if handler
  expr = paths.join(" | ")
  accumulated = []
  each do |node|
    next unless node.is_a?(Leptris::XML::Element)
    result_ptr = Leptris::XML::FFI.leptris_xpath_eval(
      @document.c_ptr, node.c_ptr, expr)
    next if result_ptr.null?
    accumulated.concat(
      Leptris::XML::NodeSet.from_result(@document, result_ptr).to_a)
  end
  Leptris::XML::NodeSet.new(@document, accumulated)
end