class Marta::XPath::XPathFactory

Here we are creating xpath including arrays of xpaths with one-two-…-x parts that are not known

@note It is believed that no user will use it All xpaths for Marta are constructed out of three parts: granny, pappy, and self. Where self is a xpath for DOM element itself, pappy is for father element, granny is for grandfather. For example //DIV/SPAN/INPUT: //DIV = granny, /SPAN = pappy, /INPUT = self.

We are generating special arrays of hashes at first for each part. And then we are constructing final xpaths

Attributes

granny[RW]
pappy[RW]

Public Class Methods

new(meth, requestor) click to toggle source
# File lib/marta/x_path.rb, line 28
def initialize(meth, requestor)
  @meth = meth
  @granny = @pappy = true
  @requestor = requestor
end

Public Instance Methods

analyze(depth, limit) click to toggle source

We are trying to understand here how much work should we do in order to generate all possible xpaths variants.

depth is suggested amount of unstable xpath parts limit is the maximum amount of xpaths that we want to generate If we can try all the combinations of xpaths with considering depth elements unstable withou reaching the limit of tries, method will return that depth and precreated array_of_hashes If limit will be reached on creation of all xpaths, method is returning last acceptable depth and array_of_hashes

# File lib/marta/x_path.rb, line 44
def analyze(depth, limit)
  hashes = array_of_hashes
  count = 1
  real_depth = 0
  variativity = 0
  hashes.each do |hash|
    variativity += (hash[:empty] - hash[:full]).count
  end
  depth.times do
    count = count * variativity
    if count < limit
      real_depth += 1
    end
  end
  return real_depth, hashes
end
array_of_hashes() click to toggle source

We are parsing stored element data (tag, text and attributes) into the array of hashes

Output looks like: [{full:,empty}, {full:,empty}, {full:[“”],empty[“”, “[@*[contains(.,'x')]]”]}]

# File lib/marta/x_path.rb, line 200
def array_of_hashes
  result = Array.new
  result.push make_hash('//', '//')
  if @granny
    result = result +
             positive_part_of_array_of_hashes('granny') +
             negative_part_of_array_of_hashes('granny')
    result.push make_hash('/', '//')
  end
  if @pappy
    result = result +
             positive_part_of_array_of_hashes('pappy') +
             negative_part_of_array_of_hashes('pappy')
    result.push make_hash('/', '//')
  end
  result = result +
           positive_part_of_array_of_hashes('self') +
           negative_part_of_array_of_hashes('self')
  result
end
generate_xpath() click to toggle source

We can generate straight xpath by all known data

# File lib/marta/x_path.rb, line 148
def generate_xpath
  result = ''
  array_of_hashes.each do |hash|
    result = result + hash[:full][0]
  end
  process_string result, @requestor
end
generate_xpaths(depth, limit = 100000) click to toggle source

Full logic of xpath generating We are understanding can we find all the possible xpath variations We are creating all possible masks (arrays of switches) one by one If we know that we cannot find all variants we are generating some by more random algorithm

# File lib/marta/x_path.rb, line 134
def generate_xpaths(depth, limit = 100000)
  xpaths = Array.new
  real_depth, hashes = analyze(depth, limit)
  masks = get_masks([Array.new(hashes.count, :full)], real_depth)
  masks.each do |mask|
    xpaths = xpaths + xpaths_by_mask(mask, hashes)
  end
  if real_depth != depth
    xpaths = xpaths + monte_carlo(hashes, depth, limit)
  end
  xpaths.uniq.map {|xpath| process_string(xpath, @requestor)}
end
get_masks(masks, depth) click to toggle source

We are generating masks like [:empty,:full,:full,:empty] They are used for more understandable logic of looping xpaths variants In fact they are lists with all the combinations of switches :full:empty. Where the length of switches is the same as length of xpath parts. And amount of :empty switches is depth

# File lib/marta/x_path.rb, line 85
def get_masks(masks, depth)
  result = Array.new
  masks.each do |mask|
    result.push mask
    for i in 0..mask.count-1
      result.push(mask.map { |e| e.dup })
      result.last[i] = :empty
    end
  end
  if depth-1 == 0
    result
  else
    get_masks(result, depth-1)
  end
end
make_hash(full, empty = '') click to toggle source

Creating the smallest possible part of array hash

# File lib/marta/x_path.rb, line 222
def make_hash(full, empty = '')
  {full: [full], empty: empty.class != Array ? [empty] : empty}
end
monte_carlo(hashes, depth, limit) click to toggle source

Generating not more than limit random xpaths variants considering that depth parts of xpath are unstable

# File lib/marta/x_path.rb, line 63
def monte_carlo(hashes, depth, limit)
  xpaths = Array.new
  while xpaths.count < limit do
    mask = Array.new hashes.count, :full
    depth.times do
      mask[rand(mask.count)] = :empty
    end
    final_array = Array.new
    hashes.each_with_index do |hash, index|
      final_array.push(hash[mask[index]].sample)
    end
    xpaths.push final_array.join
    xpaths
  end
  xpaths
end
negative_part_of_array_of_hashes(what) click to toggle source

We are parsing negative part of element data to array of hashes

# File lib/marta/x_path.rb, line 175
def negative_part_of_array_of_hashes(what)
  result = Array.new
  @meth['negative'][what]['tag'].each do |not_tag|
    result.push make_hash("[not(self::#{not_tag})]", '')
  end
  @meth['negative'][what]['text'].each do |not_text|
    result.push make_hash("[not(contains(text(),'#{not_text}'))]", '')
  end
  @meth['negative'][what]['attributes'].each_pair do |attribute, values|
    if (values != []) and (values != ['']) and !values.nil?
      values.each do |value|
        result.push make_hash("[not(contains(@#{attribute},'#{value}'))]")
      end
    end
  end
  result
end
positive_part_of_array_of_hashes(what) click to toggle source

We are parsing positive part of element data to array of hashes

# File lib/marta/x_path.rb, line 157
def positive_part_of_array_of_hashes(what)
  result = Array.new
  result.push make_hash(@meth['positive'][what]['tag'] != [] ? @meth['positive'][what]['tag'][0] : '*', '*')
  if (@meth['positive'][what]['text'] != []) and (@meth['positive'][what]['text'] != [''])
    result.push make_hash("[contains(text(),'#{@meth['positive'][what]['text'][0]}')]")
  end
  @meth['positive'][what]['attributes'].each_pair do |attribute, values|
    if (values != []) and (values != ['']) and !values.nil?
      values.each do |value|
        result.push make_hash("[contains(@#{attribute},'#{value}')]",
                              ["[@*[contains(.,'#{value}')]]", ""])
      end
    end
  end
  result
end
xpaths_by_mask(mask, hashes) click to toggle source

We are forming xpath strings by masks and hashes with data

# File lib/marta/x_path.rb, line 102
def xpaths_by_mask(mask, hashes)
  xpaths = Array.new
  final_array = [[]]
  hashes.each_with_index do |hash, index|
    hash[mask[index]].each_with_index do |hash_value, empty_index|
      if empty_index == 0
        final_array.each do |final_array_item|
          final_array_item.push(hash_value)
        end
      else
        alternative_final_array = []
        final_array.each do |final_array_item|
          alternative_final_array.push final_array_item.dup
        end
        alternative_final_array.each do |a_final_array_item|
          a_final_array_item[-1] = hash_value
        end
        final_array = final_array + alternative_final_array
      end
    end
  end
  final_array.each do |final_array_item|
    xpaths.push final_array_item.join
  end
  xpaths
end