Class: Taurus::XML::NodeSet

Inherits:
Object
  • Object
show all
Includes:
Enumerable, Searchable
Defined in:
lib/taurus/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

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 TaurusXPathResult that this NodeSet will keep alive and free on GC. Each [i] / each call goes through taurus_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/taurus/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, Taurus::XML::FFI.method(:taurus_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/taurus/xml/node_set.rb', line 9

def document
  @document
end

Class Method Details

.from_result(document, result_ptr) ⇒ Object



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

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

Instance Method Details

#[](idx) ⇒ Object



52
53
54
55
56
57
58
59
60
61
# File 'lib/taurus/xml/node_set.rb', line 52

def [](idx)
  if @result_ptr
    return nil if idx < 0 || idx >= length
    ptr = Taurus::XML::FFI.taurus_xpath_result_get(@result_ptr, idx)
    return nil if ptr.null?
    Taurus::XML::Node.wrap(ptr, @document)
  else
    @array[idx]
  end
end

#eachObject



63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
# File 'lib/taurus/xml/node_set.rb', line 63

def each
  return enum_for(:each) unless block_given?
  if @result_ptr
    # Batch-fetch all node pointers in one FFI call (taurus_xpath_result_get_nodes,
    # libtaurus v0.11.4) and wrap each. Saves N-1 FFI calls vs the per-index
    # taurus_xpath_result_get loop. Wrappers are still cached per-Document via
    # Node.wrap, so a re-iteration of the same NodeSet hits the cache.
    n = length
    if n > 0
      buf = ::FFI::MemoryPointer.new(:pointer, n)
      begin
        copied = Taurus::XML::FFI.taurus_xpath_result_get_nodes(@result_ptr, buf, n)
        copied.times do |i|
          ptr = buf.get_pointer(i * ::FFI.type_size(:pointer))
          next if ptr.null?
          yield Taurus::XML::Node.wrap(ptr, @document)
        end
      ensure
        buf.free
      end
    end
  else
    @array.each { |n| yield n }
  end
  self
end

#empty?Boolean

Returns:

  • (Boolean)


48
49
50
# File 'lib/taurus/xml/node_set.rb', line 48

def empty?
  length == 0
end

#first(n = nil) ⇒ Object



100
101
102
103
# File 'lib/taurus/xml/node_set.rb', line 100

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



113
114
115
# File 'lib/taurus/xml/node_set.rb', line 113

def inner_text
  map(&:content).join
end

#inspectObject



134
135
136
# File 'lib/taurus/xml/node_set.rb', line 134

def inspect
  to_a.inspect
end

#lastObject



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

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

#lengthObject Also known as: size



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

def length
  @result_ptr ? Taurus::XML::FFI.taurus_xpath_result_count(@result_ptr) : @array.length
end

#to_aObject Also known as: to_ary



90
91
92
93
94
95
96
97
# File 'lib/taurus/xml/node_set.rb', line 90

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

Raises:

  • (ArgumentError)


118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
# File 'lib/taurus/xml/node_set.rb', line 118

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