pax_global_header 0000666 0000000 0000000 00000000064 13414143200 0014502 g ustar 00root root 0000000 0000000 52 comment=273faed9a61c0b673969c817fafd9e4a703276c3
ruby-xpath-3.2.0/ 0000775 0000000 0000000 00000000000 13414143200 0013607 5 ustar 00root root 0000000 0000000 ruby-xpath-3.2.0/README.md 0000664 0000000 0000000 00000004102 13414143200 0015063 0 ustar 00root root 0000000 0000000 # XPath
XPath is a Ruby DSL around a subset of XPath 1.0. Its primary purpose is to
facilitate writing complex XPath queries from Ruby code.
[](http://badge.fury.io/rb/xpath)
[](http://travis-ci.org/teamcapybara/xpath)
## Generating expressions
To create quick, one-off expressions, `XPath.generate` can be used:
``` ruby
XPath.generate { |x| x.descendant(:ul)[x.attr(:id) == 'foo'] }
```
You can also call expression methods directly on the `XPath` module:
``` ruby
XPath.descendant(:ul)[XPath.attr(:id) == 'foo']
```
However for more complex expressions, it is probably more convenient to include
the `XPath` module into your own class or module:
``` ruby
module MyXPaths
include XPath
def foo_ul
descendant(:ul)[attr(:id) == 'foo']
end
def password_field(id)
descendant(:input)[attr(:type) == 'password'][attr(:id) == id]
end
end
```
Both ways return an `XPath::Expression` instance, which can be further
modified. To convert the expression to a string, just call `#to_s` on it. All
available expressions are defined in `XPath::DSL`.
## String, Hashes and Symbols
When you send a string as an argument to any XPath function, XPath assumes this
to be a string literal. On the other hand if you send in Symbol, XPath assumes
this to be an XPath literal. Thus the following two statements are not
equivalent:
``` ruby
XPath.descendant(:p)[XPath.attr(:id) == 'foo']
XPath.descendant(:p)[XPath.attr(:id) == :foo]
```
These are the XPath expressions that these would be translated to:
```
.//p[@id = 'foo']
.//p[@id = foo]
```
The second expression would match any p tag whose id attribute matches a 'foo'
tag it contains. Most likely this is not what you want.
In fact anything other than a String is treated as a literal. Thus the
following works as expected:
``` ruby
XPath.descendant(:p)[1]
```
Keep in mind that XPath is 1-indexed and not 0-indexed like most other
programming languages, including Ruby.
## License
See [LICENSE](LICENSE).
ruby-xpath-3.2.0/lib/ 0000775 0000000 0000000 00000000000 13414143200 0014355 5 ustar 00root root 0000000 0000000 ruby-xpath-3.2.0/lib/xpath.rb 0000664 0000000 0000000 00000000417 13414143200 0016030 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true
require 'nokogiri'
require 'xpath/dsl'
require 'xpath/expression'
require 'xpath/literal'
require 'xpath/union'
require 'xpath/renderer'
module XPath
extend XPath::DSL
include XPath::DSL
def self.generate
yield(self)
end
end
ruby-xpath-3.2.0/lib/xpath/ 0000775 0000000 0000000 00000000000 13414143200 0015501 5 ustar 00root root 0000000 0000000 ruby-xpath-3.2.0/lib/xpath/dsl.rb 0000664 0000000 0000000 00000010057 13414143200 0016613 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true
module XPath
module DSL
def current
Expression.new(:this_node)
end
def descendant(*expressions)
Expression.new(:descendant, current, expressions)
end
def child(*expressions)
Expression.new(:child, current, expressions)
end
def axis(name, *element_names)
Expression.new(:axis, current, name, element_names)
end
def anywhere(*expressions)
Expression.new(:anywhere, expressions)
end
def attr(expression)
Expression.new(:attribute, current, expression)
end
def text
Expression.new(:text, current)
end
def css(selector)
Expression.new(:css, current, Literal.new(selector))
end
def function(name, *arguments)
Expression.new(:function, name, *arguments)
end
def method(name, *arguments)
Expression.new(:function, name, current, *arguments)
end
def where(expression)
if expression
Expression.new(:where, current, expression)
else
current
end
end
alias_method :[], :where
def is(expression)
Expression.new(:is, current, expression)
end
def binary_operator(name, rhs)
Expression.new(:binary_operator, name, current, rhs)
end
def union(*expressions)
Union.new(*[self, expressions].flatten)
end
alias_method :+, :union
def last
function(:last)
end
def position
function(:position)
end
METHODS = [
# node set
:count, :id, :local_name, :namespace_uri,
# string
:string, :concat, :starts_with, :contains, :substring_before,
:substring_after, :substring, :string_length, :normalize_space,
:translate,
# boolean
:boolean, :not, :true, :false, :lang,
# number
:number, :sum, :floor, :ceiling, :round
].freeze
METHODS.each do |key|
name = key.to_s.tr('_', '-').to_sym
define_method key do |*args|
method(name, *args)
end
end
def qname
method(:name)
end
alias_method :inverse, :not
alias_method :~, :not
alias_method :!, :not
alias_method :normalize, :normalize_space
alias_method :n, :normalize_space
OPERATORS = [
%i[equals = ==],
%i[or or |],
%i[and and &],
%i[not_equals != !=],
%i[lte <= <=],
%i[lt < <],
%i[gte >= >=],
%i[gt > >],
%i[plus +],
%i[minus -],
%i[multiply * *],
%i[divide div /],
%i[mod mod %]
].freeze
OPERATORS.each do |(name, operator, alias_name)|
define_method name do |rhs|
binary_operator(operator, rhs)
end
alias_method alias_name, name if alias_name
end
AXES = %i[
ancestor ancestor_or_self attribute descendant_or_self
following following_sibling namespace parent preceding
preceding_sibling self
].freeze
AXES.each do |key|
name = key.to_s.tr('_', '-').to_sym
define_method key do |*element_names|
axis(name, *element_names)
end
end
alias_method :self_axis, :self
def ends_with(suffix)
function(:substring, current, function(:'string-length', current).minus(function(:'string-length', suffix)).plus(1)) == suffix
end
def contains_word(word)
function(:concat, ' ', current.normalize_space, ' ').contains(" #{word} ")
end
UPPERCASE_LETTERS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖØÙÚÛÜÝÞŸŽŠŒ'
LOWERCASE_LETTERS = 'abcdefghijklmnopqrstuvwxyzàáâãäåæçèéêëìíîïðñòóôõöøùúûüýþÿžšœ'
def lowercase
method(:translate, UPPERCASE_LETTERS, LOWERCASE_LETTERS)
end
def uppercase
method(:translate, LOWERCASE_LETTERS, UPPERCASE_LETTERS)
end
def one_of(*expressions)
expressions.map { |e| current.equals(e) }.reduce(:or)
end
def next_sibling(*expressions)
axis(:"following-sibling")[1].axis(:self, *expressions)
end
def previous_sibling(*expressions)
axis(:"preceding-sibling")[1].axis(:self, *expressions)
end
end
end
ruby-xpath-3.2.0/lib/xpath/expression.rb 0000664 0000000 0000000 00000000610 13414143200 0020222 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true
module XPath
class Expression
attr_accessor :expression, :arguments
include XPath::DSL
def initialize(expression, *arguments)
@expression = expression
@arguments = arguments
end
def current
self
end
def to_xpath(type = nil)
Renderer.render(self, type)
end
alias_method :to_s, :to_xpath
end
end
ruby-xpath-3.2.0/lib/xpath/literal.rb 0000664 0000000 0000000 00000000224 13414143200 0017460 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true
module XPath
class Literal
attr_reader :value
def initialize(value)
@value = value
end
end
end
ruby-xpath-3.2.0/lib/xpath/renderer.rb 0000664 0000000 0000000 00000005317 13414143200 0017642 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true
module XPath
class Renderer
def self.render(node, type)
new(type).render(node)
end
def initialize(type)
@type = type
end
def render(node)
arguments = node.arguments.map { |argument| convert_argument(argument) }
send(node.expression, *arguments)
end
def convert_argument(argument)
case argument
when Expression, Union then render(argument)
when Array then argument.map { |element| convert_argument(element) }
when String then string_literal(argument)
when Literal then argument.value
else argument.to_s
end
end
def string_literal(string)
if string.include?("'")
string = string.split("'", -1).map do |substr|
"'#{substr}'"
end.join(%q(,"'",))
"concat(#{string})"
else
"'#{string}'"
end
end
def this_node
'.'
end
def descendant(current, element_names)
with_element_conditions("#{current}//", element_names)
end
def child(current, element_names)
with_element_conditions("#{current}/", element_names)
end
def axis(current, name, element_names)
with_element_conditions("#{current}/#{name}::", element_names)
end
def anywhere(element_names)
with_element_conditions('//', element_names)
end
def where(on, condition)
"#{on}[#{condition}]"
end
def attribute(current, name)
if valid_xml_name?(name)
"#{current}/@#{name}"
else
"#{current}/attribute::*[local-name(.) = #{string_literal(name)}]"
end
end
def binary_operator(name, left, right)
"(#{left} #{name} #{right})"
end
def is(one, two)
if @type == :exact
binary_operator('=', one, two)
else
function(:contains, one, two)
end
end
def variable(name)
"%{#{name}}"
end
def text(current)
"#{current}/text()"
end
def literal(node)
node
end
def css(current, selector)
paths = Nokogiri::CSS.xpath_for(selector).map do |xpath_selector|
"#{current}#{xpath_selector}"
end
union(paths)
end
def union(*expressions)
expressions.join(' | ')
end
def function(name, *arguments)
"#{name}(#{arguments.join(', ')})"
end
private
def with_element_conditions(expression, element_names)
if element_names.length == 1
"#{expression}#{element_names.first}"
elsif element_names.length > 1
"#{expression}*[#{element_names.map { |e| "self::#{e}" }.join(' | ')}]"
else
"#{expression}*"
end
end
def valid_xml_name?(name)
name =~ /^[a-zA-Z_:][a-zA-Z0-9_:\.\-]*$/
end
end
end
ruby-xpath-3.2.0/lib/xpath/union.rb 0000664 0000000 0000000 00000001152 13414143200 0017155 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true
module XPath
class Union
include Enumerable
attr_reader :expressions
alias_method :arguments, :expressions
def initialize(*expressions)
@expressions = expressions
end
def expression
:union
end
def each(&block)
arguments.each(&block)
end
def method_missing(*args) # rubocop:disable Style/MethodMissingSuper, Style/MissingRespondToMissing
XPath::Union.new(*arguments.map { |e| e.send(*args) })
end
def to_xpath(type = nil)
Renderer.render(self, type)
end
alias_method :to_s, :to_xpath
end
end
ruby-xpath-3.2.0/lib/xpath/version.rb 0000664 0000000 0000000 00000000104 13414143200 0017506 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true
module XPath
VERSION = '3.2.0'
end
ruby-xpath-3.2.0/spec/ 0000775 0000000 0000000 00000000000 13414143200 0014541 5 ustar 00root root 0000000 0000000 ruby-xpath-3.2.0/spec/fixtures/ 0000775 0000000 0000000 00000000000 13414143200 0016412 5 ustar 00root root 0000000 0000000 ruby-xpath-3.2.0/spec/fixtures/form.html 0000664 0000000 0000000 00000030551 13414143200 0020247 0 ustar 00root root 0000000 0000000
Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod
tempor incididunt ut labore
et dolore magna aliqua. Ut enim ad minim veniam,
quis nostrud exercitation ullamco laboris nisi
ut aliquip ex ea commodo consequat.
Duis aute irure dolor in reprehenderit in voluptate velit esse cillum
dolore eu fugiat Redirect pariatur. Excepteur sint occaecat cupidatat non proident,
sunt in culpa qui officia
text with
whitespace
id est laborum.
ruby-xpath-3.2.0/spec/spec_helper.rb 0000664 0000000 0000000 00000000226 13414143200 0017357 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true
require 'xpath'
require 'pry'
RSpec.configure do |config|
config.expect_with(:rspec) { |c| c.syntax = :should }
end
ruby-xpath-3.2.0/spec/union_spec.rb 0000664 0000000 0000000 00000004270 13414143200 0017233 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true
require 'spec_helper'
describe XPath::Union do
let(:template) { File.read(File.expand_path('fixtures/simple.html', File.dirname(__FILE__))) }
let(:doc) { Nokogiri::HTML(template) }
describe '#expressions' do
it 'should return the expressions' do
@expr1 = XPath.generate { |x| x.descendant(:p) }
@expr2 = XPath.generate { |x| x.descendant(:div) }
@collection = XPath::Union.new(@expr1, @expr2)
@collection.expressions.should eq [@expr1, @expr2]
end
end
describe '#each' do
it 'should iterate through the expressions' do
@expr1 = XPath.generate { |x| x.descendant(:p) }
@expr2 = XPath.generate { |x| x.descendant(:div) }
@collection = XPath::Union.new(@expr1, @expr2)
exprs = []
@collection.each { |expr| exprs << expr }
exprs.should eq [@expr1, @expr2]
end
end
describe '#map' do
it 'should map the expressions' do
@expr1 = XPath.generate { |x| x.descendant(:p) }
@expr2 = XPath.generate { |x| x.descendant(:div) }
@collection = XPath::Union.new(@expr1, @expr2)
@collection.map(&:expression).should eq %i[descendant descendant]
end
end
describe '#to_xpath' do
it 'should create a valid xpath expression' do
@expr1 = XPath.generate { |x| x.descendant(:p) }
@expr2 = XPath.generate { |x| x.descendant(:div).where(x.attr(:id) == 'foo') }
@collection = XPath::Union.new(@expr1, @expr2)
@results = doc.xpath(@collection.to_xpath)
@results[0][:title].should eq 'fooDiv'
@results[1].text.should eq 'Blah'
@results[2].text.should eq 'Bax'
end
end
describe '#where and others' do
it 'should be delegated to the individual expressions' do
@expr1 = XPath.generate { |x| x.descendant(:p) }
@expr2 = XPath.generate { |x| x.descendant(:div) }
@collection = XPath::Union.new(@expr1, @expr2)
@xpath1 = @collection.where(XPath.attr(:id) == 'foo').to_xpath
@xpath2 = @collection.where(XPath.attr(:id) == 'fooDiv').to_xpath
@results = doc.xpath(@xpath1)
@results[0][:title].should eq 'fooDiv'
@results = doc.xpath(@xpath2)
@results[0][:id].should eq 'fooDiv'
end
end
end
ruby-xpath-3.2.0/spec/xpath_spec.rb 0000664 0000000 0000000 00000044703 13414143200 0017234 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true
require 'spec_helper'
require 'nokogiri'
class Thingy
include XPath
def foo_div
descendant(:div).where(attr(:id) == 'foo')
end
end
describe XPath do
let(:template) { File.read(File.expand_path('fixtures/simple.html', File.dirname(__FILE__))) }
let(:doc) { Nokogiri::HTML(template) }
def xpath(type = nil, &block)
doc.xpath XPath.generate(&block).to_xpath(type)
end
it 'should work as a mixin' do
xpath = Thingy.new.foo_div.to_xpath
doc.xpath(xpath).first[:title].should eq 'fooDiv'
end
describe '#descendant' do
it 'should find nodes that are nested below the current node' do
@results = xpath { |x| x.descendant(:p) }
@results[0].text.should eq 'Blah'
@results[1].text.should eq 'Bax'
end
it 'should not find nodes outside the context' do
@results = xpath do |x|
foo_div = x.descendant(:div).where(x.attr(:id) == 'foo')
x.descendant(:p).where(x.attr(:id) == foo_div.attr(:title))
end
@results[0].should be_nil
end
it 'should find multiple kinds of nodes' do
@results = xpath { |x| x.descendant(:p, :ul) }
@results[0].text.should eq 'Blah'
@results[3].text.should eq 'A list'
end
it 'should find all nodes when no arguments given' do
@results = xpath { |x| x.descendant[x.attr(:id) == 'foo'].descendant }
@results[0].text.should eq 'Blah'
@results[4].text.should eq 'A list'
end
end
describe '#child' do
it 'should find nodes that are nested directly below the current node' do
@results = xpath { |x| x.descendant(:div).child(:p) }
@results[0].text.should eq 'Blah'
@results[1].text.should eq 'Bax'
end
it 'should not find nodes that are nested further down below the current node' do
@results = xpath { |x| x.child(:p) }
@results[0].should be_nil
end
it 'should find multiple kinds of nodes' do
@results = xpath { |x| x.descendant(:div).child(:p, :ul) }
@results[0].text.should eq 'Blah'
@results[3].text.should eq 'A list'
end
it 'should find all nodes when no arguments given' do
@results = xpath { |x| x.descendant[x.attr(:id) == 'foo'].child }
@results[0].text.should eq 'Blah'
@results[3].text.should eq 'A list'
end
end
describe '#axis' do
it 'should find nodes given the xpath axis' do
@results = xpath { |x| x.axis(:descendant, :p) }
@results[0].text.should eq 'Blah'
end
it 'should find nodes given the xpath axis without a specific tag' do
@results = xpath { |x| x.descendant(:div)[x.attr(:id) == 'foo'].axis(:descendant) }
@results[0][:id].should eq 'fooDiv'
end
end
describe '#next_sibling' do
it 'should find nodes which are immediate siblings of the current node' do
xpath { |x| x.descendant(:p)[x.attr(:id) == 'fooDiv'].next_sibling(:p) }.first.text.should eq 'Bax'
xpath { |x| x.descendant(:p)[x.attr(:id) == 'fooDiv'].next_sibling(:ul, :p) }.first.text.should eq 'Bax'
xpath { |x| x.descendant(:p)[x.attr(:title) == 'monkey'].next_sibling(:ul, :p) }.first.text.should eq 'A list'
xpath { |x| x.descendant(:p)[x.attr(:id) == 'fooDiv'].next_sibling(:ul, :li) }.first.should be_nil
xpath { |x| x.descendant(:p)[x.attr(:id) == 'fooDiv'].next_sibling }.first.text.should eq 'Bax'
end
end
describe '#previous_sibling' do
it 'should find nodes which are exactly preceding the current node' do
xpath { |x| x.descendant(:p)[x.attr(:id) == 'wooDiv'].previous_sibling(:p) }.first.text.should eq 'Bax'
xpath { |x| x.descendant(:p)[x.attr(:id) == 'wooDiv'].previous_sibling(:ul, :p) }.first.text.should eq 'Bax'
xpath { |x| x.descendant(:p)[x.attr(:title) == 'gorilla'].previous_sibling(:ul, :p) }.first.text.should eq 'A list'
xpath { |x| x.descendant(:p)[x.attr(:id) == 'wooDiv'].previous_sibling(:ul, :li) }.first.should be_nil
xpath { |x| x.descendant(:p)[x.attr(:id) == 'wooDiv'].previous_sibling }.first.text.should eq 'Bax'
end
end
describe '#anywhere' do
it 'should find nodes regardless of the context' do
@results = xpath do |x|
foo_div = x.anywhere(:div).where(x.attr(:id) == 'foo')
x.descendant(:p).where(x.attr(:id) == foo_div.attr(:title))
end
@results[0].text.should eq 'Blah'
end
it 'should find multiple kinds of nodes regardless of the context' do
@results = xpath do |x|
context = x.descendant(:div).where(x.attr(:id) == 'woo')
context.anywhere(:p, :ul)
end
@results[0].text.should eq 'Blah'
@results[3].text.should eq 'A list'
@results[4].text.should eq 'A list'
@results[6].text.should eq 'Bax'
end
it 'should find all nodes when no arguments given regardless of the context' do
@results = xpath do |x|
context = x.descendant(:div).where(x.attr(:id) == 'woo')
context.anywhere
end
@results[0].name.should eq 'html'
@results[1].name.should eq 'head'
@results[2].name.should eq 'body'
@results[6].text.should eq 'Blah'
@results[10].text.should eq 'A list'
@results[13].text.should eq 'A list'
@results[15].text.should eq 'Bax'
end
end
describe '#contains' do
it 'should find nodes that contain the given string' do
@results = xpath do |x|
x.descendant(:div).where(x.attr(:title).contains('ooD'))
end
@results[0][:id].should eq 'foo'
end
it 'should find nodes that contain the given expression' do
@results = xpath do |x|
expression = x.anywhere(:div).where(x.attr(:title) == 'fooDiv').attr(:id)
x.descendant(:div).where(x.attr(:title).contains(expression))
end
@results[0][:id].should eq 'foo'
end
end
describe '#contains_word' do
it 'should find nodes that contain the given word in its entirety' do
@results = xpath do |x|
x.descendant.where(x.attr(:class).contains_word('fish'))
end
@results[0].text.should eq 'Bax'
@results[1].text.should eq 'llama'
@results.length.should eq 2
end
end
describe '#starts_with' do
it 'should find nodes that begin with the given string' do
@results = xpath do |x|
x.descendant(:*).where(x.attr(:id).starts_with('foo'))
end
@results.size.should eq 2
@results[0][:id].should eq 'foo'
@results[1][:id].should eq 'fooDiv'
end
it 'should find nodes that contain the given expression' do
@results = xpath do |x|
expression = x.anywhere(:div).where(x.attr(:title) == 'fooDiv').attr(:id)
x.descendant(:div).where(x.attr(:title).starts_with(expression))
end
@results[0][:id].should eq 'foo'
end
end
describe '#ends_with' do
it 'should find nodes that end with the given string' do
@results = xpath do |x|
x.descendant(:*).where(x.attr(:id).ends_with('oof'))
end
@results.size.should eq 2
@results[0][:id].should eq 'oof'
@results[1][:id].should eq 'viDoof'
end
it 'should find nodes that contain the given expression' do
@results = xpath do |x|
expression = x.anywhere(:div).where(x.attr(:title) == 'viDoof').attr(:id)
x.descendant(:div).where(x.attr(:title).ends_with(expression))
end
@results[0][:id].should eq 'oof'
end
end
describe '#uppercase' do
it 'should match uppercased text' do
@results = xpath do |x|
x.descendant(:div).where(x.attr(:title).uppercase == 'VIDOOF')
end
@results[0][:id].should eq 'oof'
end
end
describe '#lowercase' do
it 'should match lowercased text' do
@results = xpath do |x|
x.descendant(:div).where(x.attr(:title).lowercase == 'vidoof')
end
@results[0][:id].should eq 'oof'
end
end
describe '#text' do
it "should select a node's text" do
@results = xpath { |x| x.descendant(:p).where(x.text == 'Bax') }
@results[0].text.should eq 'Bax'
@results[1][:title].should eq 'monkey'
@results = xpath { |x| x.descendant(:div).where(x.descendant(:p).text == 'Bax') }
@results[0][:title].should eq 'fooDiv'
end
end
describe '#substring' do
context 'when called with one argument' do
it 'should select the part of a string after the specified character' do
@results = xpath { |x| x.descendant(:span).where(x.attr(:id) == 'substring').text.substring(7) }
@results.should eq 'there'
end
end
context 'when called with two arguments' do
it 'should select the part of a string after the specified character, up to the given length' do
@results = xpath { |x| x.descendant(:span).where(x.attr(:id) == 'substring').text.substring(2, 4) }
@results.should eq 'ello'
end
end
end
describe '#function' do
it 'should call the given xpath function' do
@results = xpath { |x| x.function(:boolean, x.function(:true) == x.function(:false)) }
@results.should eq false
end
end
describe '#method' do
it 'should call the given xpath function with the current node as the first argument' do
@results = xpath { |x| x.descendant(:span).where(x.attr(:id) == 'string-length').text.method(:"string-length") }
@results.should eq 11
end
end
describe '#string_length' do
it 'should return the length of a string' do
@results = xpath { |x| x.descendant(:span).where(x.attr(:id) == 'string-length').text.string_length }
@results.should eq 11
end
end
describe '#where' do
it 'should limit the expression to find only certain nodes' do
xpath { |x| x.descendant(:div).where(:"@id = 'foo'") }.first[:title].should eq 'fooDiv'
end
it 'should be aliased as []' do
xpath { |x| x.descendant(:div)[:"@id = 'foo'"] }.first[:title].should eq 'fooDiv'
end
it 'should be a no-op when nil condition is passed' do
XPath.descendant(:div).where(nil).to_s.should eq './/div'
end
end
describe '#inverse' do
it 'should invert the expression' do
xpath { |x| x.descendant(:p).where(x.attr(:id).equals('fooDiv').inverse) }.first.text.should eq 'Bax'
end
it 'should be aliased as the unary tilde' do
xpath { |x| x.descendant(:p).where(~x.attr(:id).equals('fooDiv')) }.first.text.should eq 'Bax'
end
it 'should be aliased as the unary bang' do
xpath { |x| x.descendant(:p).where(!x.attr(:id).equals('fooDiv')) }.first.text.should eq 'Bax'
end
end
describe '#equals' do
it 'should limit the expression to find only certain nodes' do
xpath { |x| x.descendant(:div).where(x.attr(:id).equals('foo')) }.first[:title].should eq 'fooDiv'
end
it 'should be aliased as ==' do
xpath { |x| x.descendant(:div).where(x.attr(:id) == 'foo') }.first[:title].should eq 'fooDiv'
end
end
describe '#not_equals' do
it 'should match only when not equal' do
xpath { |x| x.descendant(:div).where(x.attr(:id).not_equals('bar')) }.first[:title].should eq 'fooDiv'
end
it 'should be aliased as !=' do
xpath { |x| x.descendant(:div).where(x.attr(:id) != 'bar') }.first[:title].should eq 'fooDiv'
end
end
describe '#is' do
it 'uses equality when :exact given' do
xpath(:exact) { |x| x.descendant(:div).where(x.attr(:id).is('foo')) }.first[:title].should eq 'fooDiv'
xpath(:exact) { |x| x.descendant(:div).where(x.attr(:id).is('oo')) }.first.should be_nil
end
it 'uses substring matching otherwise' do
xpath { |x| x.descendant(:div).where(x.attr(:id).is('foo')) }.first[:title].should eq 'fooDiv'
xpath { |x| x.descendant(:div).where(x.attr(:id).is('oo')) }.first[:title].should eq 'fooDiv'
end
end
describe '#one_of' do
it 'should return all nodes where the condition matches' do
@results = xpath do |x|
p = x.anywhere(:div).where(x.attr(:id) == 'foo').attr(:title)
x.descendant(:*).where(x.attr(:id).one_of('foo', p, 'baz'))
end
@results[0][:title].should eq 'fooDiv'
@results[1].text.should eq 'Blah'
@results[2][:title].should eq 'bazDiv'
end
end
describe '#and' do
it 'should find all nodes in both expression' do
@results = xpath do |x|
x.descendant(:*).where(x.contains('Bax').and(x.attr(:title).equals('monkey')))
end
@results[0][:title].should eq 'monkey'
end
it 'should be aliased as ampersand (&)' do
@results = xpath do |x|
x.descendant(:*).where(x.contains('Bax') & x.attr(:title).equals('monkey'))
end
@results[0][:title].should eq 'monkey'
end
end
describe '#or' do
it 'should find all nodes in either expression' do
@results = xpath do |x|
x.descendant(:*).where(x.attr(:id).equals('foo').or(x.attr(:id).equals('fooDiv')))
end
@results[0][:title].should eq 'fooDiv'
@results[1].text.should eq 'Blah'
end
it 'should be aliased as pipe (|)' do
@results = xpath do |x|
x.descendant(:*).where(x.attr(:id).equals('foo') | x.attr(:id).equals('fooDiv'))
end
@results[0][:title].should eq 'fooDiv'
@results[1].text.should eq 'Blah'
end
end
describe '#attr' do
it 'should be an attribute' do
@results = xpath { |x| x.descendant(:div).where(x.attr(:id)) }
@results[0][:title].should eq 'barDiv'
@results[1][:title].should eq 'fooDiv'
end
end
describe '#css' do
it 'should find nodes by the given CSS selector' do
@results = xpath { |x| x.css('#preference p') }
@results[0].text.should eq 'allamas'
@results[1].text.should eq 'llama'
end
it 'should respect previous expression' do
@results = xpath { |x| x.descendant[x.attr(:id) == 'moar'].css('p') }
@results[0].text.should eq 'chimp'
@results[1].text.should eq 'flamingo'
end
it 'should be composable' do
@results = xpath { |x| x.css('#moar').descendant(:p) }
@results[0].text.should eq 'chimp'
@results[1].text.should eq 'flamingo'
end
it 'should allow comma separated selectors' do
@results = xpath { |x| x.descendant[x.attr(:id) == 'moar'].css('div, p') }
@results[0].text.should eq 'chimp'
@results[1].text.should eq 'elephant'
@results[2].text.should eq 'flamingo'
end
end
describe '#qname' do
it "should match the node's name" do
xpath { |x| x.descendant(:*).where(x.qname == 'ul') }.first.text.should eq 'A list'
end
end
describe '#union' do
it 'should create a union expression' do
@expr1 = XPath.generate { |x| x.descendant(:p) }
@expr2 = XPath.generate { |x| x.descendant(:div) }
@collection = @expr1.union(@expr2)
@xpath1 = @collection.where(XPath.attr(:id) == 'foo').to_xpath
@xpath2 = @collection.where(XPath.attr(:id) == 'fooDiv').to_xpath
@results = doc.xpath(@xpath1)
@results[0][:title].should eq 'fooDiv'
@results = doc.xpath(@xpath2)
@results[0][:id].should eq 'fooDiv'
end
it 'should be aliased as +' do
@expr1 = XPath.generate { |x| x.descendant(:p) }
@expr2 = XPath.generate { |x| x.descendant(:div) }
@collection = @expr1 + @expr2
@xpath1 = @collection.where(XPath.attr(:id) == 'foo').to_xpath
@xpath2 = @collection.where(XPath.attr(:id) == 'fooDiv').to_xpath
@results = doc.xpath(@xpath1)
@results[0][:title].should eq 'fooDiv'
@results = doc.xpath(@xpath2)
@results[0][:id].should eq 'fooDiv'
end
end
describe '#last' do
it 'returns the number of elements in the context' do
@results = xpath { |x| x.descendant(:p)[XPath.position() == XPath.last()] }
@results[0].text.should eq 'Bax'
@results[1].text.should eq 'Blah'
@results[2].text.should eq 'llama'
end
end
describe '#position' do
it 'returns the position of elements in the context' do
@results = xpath { |x| x.descendant(:p)[XPath.position() == 2] }
@results[0].text.should eq 'Bax'
@results[1].text.should eq 'Bax'
end
end
describe '#count' do
it 'counts the number of occurrences' do
@results = xpath { |x| x.descendant(:div)[x.descendant(:p).count == 2] }
@results[0][:id].should eq 'preference'
end
end
describe '#lte' do
it 'checks lesser than or equal' do
@results = xpath { |x| x.descendant(:p)[XPath.position() <= 2] }
@results[0].text.should eq 'Blah'
@results[1].text.should eq 'Bax'
@results[2][:title].should eq 'gorilla'
@results[3].text.should eq 'Bax'
end
end
describe '#lt' do
it 'checks lesser than' do
@results = xpath { |x| x.descendant(:p)[XPath.position() < 2] }
@results[0].text.should eq 'Blah'
@results[1][:title].should eq 'gorilla'
end
end
describe '#gte' do
it 'checks greater than or equal' do
@results = xpath { |x| x.descendant(:p)[XPath.position() >= 2] }
@results[0].text.should eq 'Bax'
@results[1][:title].should eq 'monkey'
@results[2].text.should eq 'Bax'
@results[3].text.should eq 'Blah'
end
end
describe '#gt' do
it 'checks greater than' do
@results = xpath { |x| x.descendant(:p)[XPath.position() > 2] }
@results[0][:title].should eq 'monkey'
@results[1].text.should eq 'Blah'
end
end
describe '#plus' do
it 'adds stuff' do
@results = xpath { |x| x.descendant(:p)[XPath.position().plus(1) == 2] }
@results[0][:id].should eq 'fooDiv'
@results[1][:title].should eq 'gorilla'
end
end
describe '#minus' do
it 'subtracts stuff' do
@results = xpath { |x| x.descendant(:p)[XPath.position().minus(1) == 0] }
@results[0][:id].should eq 'fooDiv'
@results[1][:title].should eq 'gorilla'
end
end
describe '#multiply' do
it 'multiplies stuff' do
@results = xpath { |x| x.descendant(:p)[XPath.position() * 3 == 3] }
@results[0][:id].should eq 'fooDiv'
@results[1][:title].should eq 'gorilla'
end
end
describe '#divide' do
it 'divides stuff' do
@results = xpath { |x| x.descendant(:p)[XPath.position() / 2 == 1] }
@results[0].text.should eq 'Bax'
@results[1].text.should eq 'Bax'
end
end
describe '#mod' do
it 'take modulo' do
@results = xpath { |x| x.descendant(:p)[XPath.position() % 2 == 1] }
@results[0].text.should eq 'Blah'
@results[1][:title].should eq 'monkey'
@results[2][:title].should eq 'gorilla'
end
end
describe '#ancestor' do
it 'finds ancestor nodes' do
@results = xpath { |x| x.descendant(:p)[1].ancestor }
@results[0].node_name.should eq 'html'
@results[1].node_name.should eq 'body'
@results[2][:id].should eq 'foo'
end
end
end
ruby-xpath-3.2.0/xpath.gemspec 0000664 0000000 0000000 00000004545 13414143200 0016310 0 ustar 00root root 0000000 0000000 #########################################################
# This file has been automatically generated by gem2tgz #
#########################################################
# -*- encoding: utf-8 -*-
# stub: xpath 3.2.0 ruby lib
Gem::Specification.new do |s|
s.name = "xpath".freeze
s.version = "3.2.0"
s.required_rubygems_version = Gem::Requirement.new(">= 0".freeze) if s.respond_to? :required_rubygems_version=
s.require_paths = ["lib".freeze]
s.authors = ["Jonas Nicklas".freeze]
s.cert_chain = ["gem-public_cert.pem".freeze]
s.date = "2018-10-15"
s.description = "XPath is a Ruby DSL for generating XPath expressions".freeze
s.email = ["jonas.nicklas@gmail.com".freeze]
s.files = ["README.md".freeze, "lib/xpath.rb".freeze, "lib/xpath/dsl.rb".freeze, "lib/xpath/expression.rb".freeze, "lib/xpath/literal.rb".freeze, "lib/xpath/renderer.rb".freeze, "lib/xpath/union.rb".freeze, "lib/xpath/version.rb".freeze, "spec/fixtures/form.html".freeze, "spec/fixtures/simple.html".freeze, "spec/fixtures/stuff.html".freeze, "spec/spec_helper.rb".freeze, "spec/union_spec.rb".freeze, "spec/xpath_spec.rb".freeze]
s.homepage = "https://github.com/teamcapybara/xpath".freeze
s.licenses = ["MIT".freeze]
s.required_ruby_version = Gem::Requirement.new(">= 2.3".freeze)
s.rubygems_version = "2.5.2.1".freeze
s.summary = "Generate XPath expressions from Ruby".freeze
if s.respond_to? :specification_version then
s.specification_version = 4
if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
s.add_runtime_dependency(%q.freeze, ["~> 1.8"])
s.add_development_dependency(%q.freeze, [">= 0"])
s.add_development_dependency(%q.freeze, [">= 0"])
s.add_development_dependency(%q.freeze, ["~> 3.0"])
s.add_development_dependency(%q.freeze, [">= 0.5.8"])
else
s.add_dependency(%q.freeze, ["~> 1.8"])
s.add_dependency(%q.freeze, [">= 0"])
s.add_dependency(%q.freeze, [">= 0"])
s.add_dependency(%q.freeze, ["~> 3.0"])
s.add_dependency(%q.freeze, [">= 0.5.8"])
end
else
s.add_dependency(%q.freeze, ["~> 1.8"])
s.add_dependency(%q.freeze, [">= 0"])
s.add_dependency(%q.freeze, [">= 0"])
s.add_dependency(%q.freeze, ["~> 3.0"])
s.add_dependency(%q.freeze, [">= 0.5.8"])
end
end