pax_global_header00006660000000000000000000000064134141432000014502gustar00rootroot0000000000000052 comment=273faed9a61c0b673969c817fafd9e4a703276c3 ruby-xpath-3.2.0/000077500000000000000000000000001341414320000136075ustar00rootroot00000000000000ruby-xpath-3.2.0/README.md000066400000000000000000000041021341414320000150630ustar00rootroot00000000000000# 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. [![Gem Version](https://badge.fury.io/rb/xpath.png)](http://badge.fury.io/rb/xpath) [![Build Status](https://secure.travis-ci.org/teamcapybara/xpath.png?branch=master)](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/000077500000000000000000000000001341414320000143555ustar00rootroot00000000000000ruby-xpath-3.2.0/lib/xpath.rb000066400000000000000000000004171341414320000160300ustar00rootroot00000000000000# 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/000077500000000000000000000000001341414320000155015ustar00rootroot00000000000000ruby-xpath-3.2.0/lib/xpath/dsl.rb000066400000000000000000000100571341414320000166130ustar00rootroot00000000000000# 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.rb000066400000000000000000000006101341414320000202220ustar00rootroot00000000000000# 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.rb000066400000000000000000000002241341414320000174600ustar00rootroot00000000000000# 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.rb000066400000000000000000000053171341414320000176420ustar00rootroot00000000000000# 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.rb000066400000000000000000000011521341414320000171550ustar00rootroot00000000000000# 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.rb000066400000000000000000000001041341414320000175060ustar00rootroot00000000000000# frozen_string_literal: true module XPath VERSION = '3.2.0' end ruby-xpath-3.2.0/spec/000077500000000000000000000000001341414320000145415ustar00rootroot00000000000000ruby-xpath-3.2.0/spec/fixtures/000077500000000000000000000000001341414320000164125ustar00rootroot00000000000000ruby-xpath-3.2.0/spec/fixtures/form.html000066400000000000000000000305511341414320000202470ustar00rootroot00000000000000

Form

An awesome link With id A cool title Alt link A link at a time A link An image that is beautiful An image Href-ed link Wrong Link My whitespaced link An emphatic link with some children

Some Legend
Span Legend
Long legend yo
Long legend
Outer legend
Inner legend

Tables

FirstSecond
Table with caption
FirstSecond
I have nested whitespace I don't

Fields

With id

With name

With placeholder

With referenced label

With parent label

Disabled

ruby-xpath-3.2.0/spec/fixtures/simple.html000066400000000000000000000020611341414320000205700ustar00rootroot00000000000000

Blah

Bax

Bax

Bax

Bax

Blah

allamas

llama

A lot of whitespace

chimp

elephant

flamingo

Hello there Hello there

Blah

ruby-xpath-3.2.0/spec/fixtures/stuff.html000066400000000000000000000041231341414320000204270ustar00rootroot00000000000000

This is a test

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. awesome image

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.

BackToMyself A link came first A link A link with data-method No Href Blank Href Blank Anchor Anchor Anchor on different page Anchor on same page very fine image fine image

Coffee
black hot drink
Milk
white cold drink
ruby-xpath-3.2.0/spec/spec_helper.rb000066400000000000000000000002261341414320000173570ustar00rootroot00000000000000# 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.rb000066400000000000000000000042701341414320000172330ustar00rootroot00000000000000# 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.rb000066400000000000000000000447031341414320000172340ustar00rootroot00000000000000# 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.gemspec000066400000000000000000000045451341414320000163100ustar00rootroot00000000000000######################################################### # 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