ayril 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGES +4 -0
- data/Gemfile +3 -0
- data/Gemfile.lock +19 -0
- data/LICENSE +19 -0
- data/README.md +88 -0
- data/Rakefile +40 -0
- data/ayril.gemspec +55 -0
- data/lib/ayril.rb +48 -0
- data/lib/ayril/core_ext/core_ext.rb +96 -0
- data/lib/ayril/selector.rb +250 -0
- data/lib/ayril/version.rb +30 -0
- data/lib/ayril/xml_document.rb +54 -0
- data/lib/ayril/xml_element.rb +75 -0
- data/lib/ayril/xml_element/element_attribute_manipulation.rb +61 -0
- data/lib/ayril/xml_element/element_classname_manipulation.rb +34 -0
- data/lib/ayril/xml_element/element_manipulation.rb +85 -0
- data/lib/ayril/xml_element/element_style_manipulation.rb +11 -0
- data/lib/ayril/xml_element/xml_attribute_hash.rb +60 -0
- data/lib/ayril/xml_element/xml_css_hash.rb +47 -0
- data/lib/ayril/xml_node.rb +118 -0
- data/lib/ayril/xml_node/node_manipulation.rb +73 -0
- data/lib/ayril/xml_node/node_traversal.rb +158 -0
- data/test/invoke_ayril_selector.rb +3 -0
- data/test/invoke_prototype_selector.js +4 -0
- data/test/sanity.xml +124 -0
- data/test/selector.js +521 -0
- data/test/test_sanity.rb +27 -0
- data/test/test_selector.rb +78 -0
- metadata +129 -0
data/CHANGES
ADDED
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
Copyright (C) 2011 by Robert Lowe <rob[!]iblargz.com>
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
|
+
of this software and associated documentation files (the "Software"), to deal
|
5
|
+
in the Software without restriction, including without limitation the rights
|
6
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
7
|
+
copies of the Software, and to permit persons to whom the Software is
|
8
|
+
furnished to do so, subject to the following conditions:
|
9
|
+
|
10
|
+
The above copyright notice and this permission notice shall be included in
|
11
|
+
all copies or substantial portions of the Software.
|
12
|
+
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
15
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
16
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
17
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
18
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
19
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,88 @@
|
|
1
|
+
# Ayril
|
2
|
+
|
3
|
+
Ayril is an XML library in MacRuby that uses Cocoa NSXML classes. It thus has a
|
4
|
+
very specific audience; for now, Cocoa developers on Mac OS X Leopard. This may
|
5
|
+
change in the future as MacRuby expands its targeted platforms.
|
6
|
+
|
7
|
+
Ayril subclasses all NSXML classes (with the exception of NSXMLParser) and
|
8
|
+
enhances them with useful methods and paradigms inspired by the Prototype
|
9
|
+
JavaScript framework and other Ruby XML libraries such as Hpricot and REXML.
|
10
|
+
|
11
|
+
MacRuby developers have had numerous options when it comes to processing XML.
|
12
|
+
One could use REXML, the pure Ruby core library; Hpricot, the mostly-C library;
|
13
|
+
LibXML, a Ruby binding for the libxml2 toolkit written in C; or the Cocoa NSXML
|
14
|
+
classes, written in pure Objective-C and accessible directly through MacRuby.
|
15
|
+
Now, developers have yet another option: Ayril, which takes advantage of the
|
16
|
+
speed of NSXML classes and employs more Ruby-isms than the NSXML classes do.
|
17
|
+
|
18
|
+
## Copyright (C) 2009-2011 by Wilson Lee <kourge[!]gmail.com>, Robert Lowe <rob[!]iblargz.com> - MIT
|
19
|
+
|
20
|
+
* Programming by Wilson Lee on May 03, 2009
|
21
|
+
* Packaging by Robert Lowe on June 16, 2011
|
22
|
+
|
23
|
+
## Tests (not many right now):
|
24
|
+
|
25
|
+
To test sanity:
|
26
|
+
|
27
|
+
Install macruby 0.10
|
28
|
+
|
29
|
+
1. Run in a terminal: `macruby test/test_sanity.rb`
|
30
|
+
|
31
|
+
To test selectors:
|
32
|
+
|
33
|
+
Install rhino
|
34
|
+
Install macruby 0.10
|
35
|
+
|
36
|
+
1. Run in a terminal: `macruby test/test_selector.rb`
|
37
|
+
|
38
|
+
PASS "body"
|
39
|
+
PASS "div"
|
40
|
+
PASS "body div"
|
41
|
+
PASS "div p"
|
42
|
+
PASS "div > p"
|
43
|
+
PASS "div + p"
|
44
|
+
PASS "div ~ p"
|
45
|
+
PASS "div[class^=exa][class$=mple]"
|
46
|
+
PASS "div p a"
|
47
|
+
PASS "div, p, a"
|
48
|
+
PASS ".note"
|
49
|
+
PASS "div.example"
|
50
|
+
PASS "div.dialog.emphatic"
|
51
|
+
PASS "ul .tocline2"
|
52
|
+
PASS "div.example, div.note"
|
53
|
+
PASS "#title"
|
54
|
+
PASS "h1#title"
|
55
|
+
PASS "div #title"
|
56
|
+
PASS "ul.toc li.tocline2"
|
57
|
+
PASS "ul.toc > li.tocline2"
|
58
|
+
PASS "h1#title + div > p"
|
59
|
+
PASS "h1[id]:contains(Selectors)"
|
60
|
+
PASS "a[href][lang][class]"
|
61
|
+
PASS "div[class]"
|
62
|
+
PASS "div[class=example]"
|
63
|
+
PASS "div[class^=exa]"
|
64
|
+
PASS "div[class$=mple]"
|
65
|
+
PASS "div[class*=e]"
|
66
|
+
PASS "div[class|=dialog]"
|
67
|
+
PASS "div[class!=made_up]"
|
68
|
+
PASS "div[class~=example]"
|
69
|
+
PASS "div:not(.example)"
|
70
|
+
PASS "p:contains(selectors)"
|
71
|
+
PASS "p:nth-child(even)"
|
72
|
+
PASS "p:nth-child(2n)"
|
73
|
+
PASS "p:nth-child(odd)"
|
74
|
+
PASS "p:nth-child(2n+1)"
|
75
|
+
PASS "p:nth-child(n)"
|
76
|
+
PASS "p:only-child"
|
77
|
+
PASS "p:last-child"
|
78
|
+
PASS "p:first-child"
|
79
|
+
|
80
|
+
|
81
|
+
===
|
82
|
+
|
83
|
+
TODOS:
|
84
|
+
|
85
|
+
* Passing tests for sets of xml docs (valid, invalid and the ugly.)
|
86
|
+
* Passing tests for utility methods
|
87
|
+
* Handle delegation of errors
|
88
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
require 'rake/clean'
|
2
|
+
require 'rake/testtask'
|
3
|
+
require 'fileutils'
|
4
|
+
require 'date'
|
5
|
+
|
6
|
+
require 'lib/ayril/version.rb'
|
7
|
+
|
8
|
+
# task :default => :test
|
9
|
+
# task :spec => :test
|
10
|
+
|
11
|
+
# PACKAGING ============================================================
|
12
|
+
|
13
|
+
if defined?(Gem)
|
14
|
+
# Load the gemspec using the same limitations as github
|
15
|
+
def spec
|
16
|
+
require 'rubygems' unless defined? Gem::Specification
|
17
|
+
@spec ||= eval(File.read('ayril.gemspec'))
|
18
|
+
end
|
19
|
+
|
20
|
+
def package(ext='')
|
21
|
+
"pkg/ayril-#{spec.version}" + ext
|
22
|
+
end
|
23
|
+
|
24
|
+
desc 'Build packages'
|
25
|
+
task :package => %w[.gem].map {|e| package(e)}
|
26
|
+
|
27
|
+
desc 'Build and install as local gem'
|
28
|
+
task :install => package('.gem') do
|
29
|
+
`gem install #{package('.gem')}`
|
30
|
+
end
|
31
|
+
|
32
|
+
directory 'pkg/'
|
33
|
+
CLOBBER.include('pkg')
|
34
|
+
|
35
|
+
file package('.gem') => %w[pkg/ ayril.gemspec] + spec.files do |f|
|
36
|
+
`gem build ayril.gemspec`
|
37
|
+
mv File.basename(f.name), f.name
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
data/ayril.gemspec
ADDED
@@ -0,0 +1,55 @@
|
|
1
|
+
require 'lib/ayril/version'
|
2
|
+
|
3
|
+
Gem::Specification.new do |s|
|
4
|
+
s.specification_version = 2 if s.respond_to? :specification_version=
|
5
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
6
|
+
|
7
|
+
s.name = %q{ayril}
|
8
|
+
s.version = "#{Ayril::Version::STRING}"
|
9
|
+
s.authors = ["Wilson Lee", "Rob Lowe"]
|
10
|
+
s.date = %q{2011-06-16}
|
11
|
+
s.description = %q{An XML library for MacRuby that is built on top of Cocoa NSXML classes}
|
12
|
+
s.email = ['kourge@gmail.com', 'rob@iblargz.com']
|
13
|
+
s.extra_rdoc_files = [
|
14
|
+
"LICENSE",
|
15
|
+
"CHANGES",
|
16
|
+
"README.md"
|
17
|
+
]
|
18
|
+
s.files = [
|
19
|
+
"Gemfile",
|
20
|
+
"Gemfile.lock",
|
21
|
+
"LICENSE",
|
22
|
+
"README.md",
|
23
|
+
"Rakefile",
|
24
|
+
"ayril.gemspec",
|
25
|
+
"lib/ayril.rb",
|
26
|
+
"lib/ayril/version.rb",
|
27
|
+
"lib/ayril/core_ext/core_ext.rb",
|
28
|
+
"lib/ayril/selector.rb",
|
29
|
+
"lib/ayril/xml_document.rb",
|
30
|
+
"lib/ayril/xml_node.rb",
|
31
|
+
"lib/ayril/xml_node/node_traversal.rb",
|
32
|
+
"lib/ayril/xml_node/node_manipulation.rb",
|
33
|
+
"lib/ayril/xml_element.rb",
|
34
|
+
"lib/ayril/xml_element/element_attribute_manipulation.rb",
|
35
|
+
"lib/ayril/xml_element/element_classname_manipulation.rb",
|
36
|
+
"lib/ayril/xml_element/element_style_manipulation.rb",
|
37
|
+
"lib/ayril/xml_element/element_manipulation.rb",
|
38
|
+
"lib/ayril/xml_element/xml_attribute_hash.rb",
|
39
|
+
"lib/ayril/xml_element/xml_css_hash.rb",
|
40
|
+
"test/test_sanity.rb",
|
41
|
+
"test/sanity.xml",
|
42
|
+
"test/invoke_ayril_selector.rb",
|
43
|
+
"test/invoke_prototype_selector.js",
|
44
|
+
"test/selector.js",
|
45
|
+
"test/test_selector.rb"
|
46
|
+
]
|
47
|
+
s.homepage = %q{http://github.com/RobertLowe/ayril}
|
48
|
+
s.licenses = ["MIT"]
|
49
|
+
s.require_paths = ["lib"]
|
50
|
+
s.rubygems_version = %q{1.4.2}
|
51
|
+
s.summary = %q{An XML library for MacRuby built on top of Cocoa NSXML classes}
|
52
|
+
s.add_development_dependency(%q<rake>, [">= 0"])
|
53
|
+
s.add_development_dependency(%q<bundler>, ["~> 1.0.0"])
|
54
|
+
s.add_development_dependency(%q<MacSpec>, ["~> 0.4.5"])
|
55
|
+
end
|
data/lib/ayril.rb
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
# Copyright (C) 2011 by Wilson Lee <kourge[!]gmail.com>, Robert Lowe <rob[!]iblargz.com>
|
2
|
+
#
|
3
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
|
+
# of this software and associated documentation files (the "Software"), to deal
|
5
|
+
# in the Software without restriction, including without limitation the rights
|
6
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
7
|
+
# copies of the Software, and to permit persons to whom the Software is
|
8
|
+
# furnished to do so, subject to the following conditions:
|
9
|
+
#
|
10
|
+
# The above copyright notice and this permission notice shall be included in
|
11
|
+
# all copies or substantial portions of the Software.
|
12
|
+
#
|
13
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
15
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
16
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
17
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
18
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
19
|
+
# THE SOFTWARE.
|
20
|
+
unless RUBY_ENGINE =~ /macruby/
|
21
|
+
raise NotImplementedError, "Ayril only runs on macruby! ;)"
|
22
|
+
end
|
23
|
+
|
24
|
+
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
|
25
|
+
|
26
|
+
framework 'Cocoa'
|
27
|
+
|
28
|
+
# core extensions to load right away
|
29
|
+
require File.dirname(__FILE__) + '/ayril/core_ext/core_ext'
|
30
|
+
|
31
|
+
module Ayril
|
32
|
+
autoload :Version, 'ayril/version'
|
33
|
+
|
34
|
+
autoload :Selector, 'ayril/selector'
|
35
|
+
|
36
|
+
autoload :XMLNode, 'ayril/xml_node'
|
37
|
+
autoload :XMLElement, 'ayril/xml_element'
|
38
|
+
autoload :XMLDocument, 'ayril/xml_document'
|
39
|
+
|
40
|
+
class XMLDTD < NSXMLDTD
|
41
|
+
include XMLNode::NodeManipulation
|
42
|
+
end
|
43
|
+
|
44
|
+
class XMLDTDNode < NSXMLDTDNode
|
45
|
+
include XMLNode::NodeManipulation
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
@@ -0,0 +1,96 @@
|
|
1
|
+
# Copyright (C) 2011 by Wilson Lee <kourge[!]gmail.com>, Robert Lowe <rob[!]iblargz.com>
|
2
|
+
#
|
3
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
|
+
# of this software and associated documentation files (the "Software"), to deal
|
5
|
+
# in the Software without restriction, including without limitation the rights
|
6
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
7
|
+
# copies of the Software, and to permit persons to whom the Software is
|
8
|
+
# furnished to do so, subject to the following conditions:
|
9
|
+
#
|
10
|
+
# The above copyright notice and this permission notice shall be included in
|
11
|
+
# all copies or substantial portions of the Software.
|
12
|
+
#
|
13
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
15
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
16
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
17
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
18
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
19
|
+
# THE SOFTWARE.
|
20
|
+
|
21
|
+
class NilClass
|
22
|
+
def maybe(*a); self; end if not NilClass.method_defined? :maybe
|
23
|
+
def intern; self; end if not NilClass.method_defined? :intern
|
24
|
+
end
|
25
|
+
|
26
|
+
|
27
|
+
class Object
|
28
|
+
alias :maybe :send
|
29
|
+
end
|
30
|
+
|
31
|
+
class Module
|
32
|
+
# MacRuby still cannot alias some Cocoa methods.
|
33
|
+
def forward(new, old) define_method(new) { |*args| self.send old, *args } end
|
34
|
+
end
|
35
|
+
|
36
|
+
|
37
|
+
class Array
|
38
|
+
def any?; not self.empty? end
|
39
|
+
def invoke(*args) self.map { |item| item.send *args } end
|
40
|
+
def invoke!(*args) self.map! { |item| item.send *args } end
|
41
|
+
|
42
|
+
alias :send_all :invoke
|
43
|
+
alias :send_all! :invoke!
|
44
|
+
alias :pluck :invoke
|
45
|
+
alias :pluck! :invoke!
|
46
|
+
end
|
47
|
+
|
48
|
+
|
49
|
+
class String
|
50
|
+
def interpolate(object, pattern=/(^|.|\r|\n)(#\{(.*?)\})/)
|
51
|
+
self.gsub(pattern) do |match|
|
52
|
+
return '' if object.nil?
|
53
|
+
|
54
|
+
before = $1 || ''
|
55
|
+
return $2 if before == '\\'
|
56
|
+
|
57
|
+
ctx = object; expr = $3
|
58
|
+
pattern = /^([^.\[]+|\[((?:.*?[^\\])?)\])(\.|\[|$)/
|
59
|
+
match = pattern.match(expr)
|
60
|
+
return before if match.nil?
|
61
|
+
|
62
|
+
while not match.nil?
|
63
|
+
comp = match[1].start_with?('[') ? match[2].gsub('\\\\]', ']') : match[1]
|
64
|
+
if ctx.kind_of?(Array) or ctx.kind_of?(MatchData)
|
65
|
+
ctx = ctx[comp.to_i]
|
66
|
+
elsif ctx.kind_of?(Hash)
|
67
|
+
types = ctx.keys.invoke(:class)
|
68
|
+
|
69
|
+
major_type = types.uniq.map do |type|
|
70
|
+
[type, types.find_all { |i| i == type }.length]
|
71
|
+
end.max { |a, b| a[1] <=> b[1] }[0]
|
72
|
+
|
73
|
+
method = {
|
74
|
+
Symbol => :to_sym,
|
75
|
+
String => :to_s
|
76
|
+
}[major_type]
|
77
|
+
|
78
|
+
ctx = ctx[comp.send method]
|
79
|
+
else
|
80
|
+
ctx = ctx[comp]
|
81
|
+
end
|
82
|
+
break if ctx.nil? or match[3] == ''
|
83
|
+
expr = expr[(match[3] == '[' ? match[1].length : match[0].length)..-1]
|
84
|
+
match = pattern.match expr
|
85
|
+
end
|
86
|
+
|
87
|
+
before + ctx.to_s
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def start_with?(string) self.index(string) == 0 end
|
92
|
+
def end_with?(string) self.index(string) == (self.length - string.length) end
|
93
|
+
|
94
|
+
def to_elem; Ayril::XMLElement.new self end
|
95
|
+
end
|
96
|
+
|
@@ -0,0 +1,250 @@
|
|
1
|
+
module Ayril
|
2
|
+
class Selector
|
3
|
+
attr_reader :expr, :xpath
|
4
|
+
@@cache = {}
|
5
|
+
|
6
|
+
def initialize(expr)
|
7
|
+
@expr = expr.strip
|
8
|
+
self.compile_xpath_matcher
|
9
|
+
end
|
10
|
+
|
11
|
+
def compile_xpath_matcher
|
12
|
+
e = @expr.dup; le = nil
|
13
|
+
return (@xpath = @@cache[e]) if @@cache.include? e
|
14
|
+
|
15
|
+
@matcher = [".//*"]
|
16
|
+
while (e != '') and (le != e) and (e =~ /\S/)
|
17
|
+
le = e.dup
|
18
|
+
Selector::Patterns.each do |pattern|
|
19
|
+
n = Selector::XPath[pattern[:name]]
|
20
|
+
if m = e.match(pattern[:re])
|
21
|
+
m = m.to_a unless m.nil?
|
22
|
+
@matcher << (n.kind_of?(Proc) ? n.call(m) : n.interpolate(m))
|
23
|
+
e.sub! m[0], ''
|
24
|
+
break
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
@xpath = @matcher.join
|
30
|
+
@@cache[@expr] = @xpath.gsub! %r{\*?\[name\(\)='([a-zA-Z]+)'\]}, '\1'
|
31
|
+
end
|
32
|
+
|
33
|
+
def find_elements(root)
|
34
|
+
root.select_by_xpath @xpath
|
35
|
+
end
|
36
|
+
|
37
|
+
def match?(element)
|
38
|
+
@tokens = []
|
39
|
+
|
40
|
+
e = @expr.dup; le = nil
|
41
|
+
while (e != '') and (le != e) and (e =~ /\S/)
|
42
|
+
le = e.dup
|
43
|
+
Selector::Patterns.each do |pattern|
|
44
|
+
if m = e.patch(pattern[:re])
|
45
|
+
m = m.to_a unless m.nil?
|
46
|
+
if Selector::Assertions.include? name
|
47
|
+
@tokens << [pattern[:name], m.clone]
|
48
|
+
e.sub! m[0], ''
|
49
|
+
else # resort to whole document
|
50
|
+
return self.find_elements(element.rootDocument).include? element
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
match = true
|
57
|
+
@tokens.each do |token|
|
58
|
+
name, matches = token[0..1]
|
59
|
+
if not Selector::Assertions[name].call self, matches
|
60
|
+
match = false
|
61
|
+
break
|
62
|
+
end
|
63
|
+
end
|
64
|
+
match
|
65
|
+
end
|
66
|
+
|
67
|
+
def to_s
|
68
|
+
@expr
|
69
|
+
end
|
70
|
+
|
71
|
+
def inspect
|
72
|
+
"#<Selector:#{@expr.inspect}>"
|
73
|
+
end
|
74
|
+
|
75
|
+
|
76
|
+
XPath = {
|
77
|
+
:descendant => "//*",
|
78
|
+
:child => "/*",
|
79
|
+
:adjacent => "/following-sibling::*[1]",
|
80
|
+
:laterSibling => '/following-sibling::*',
|
81
|
+
:tagName => lambda { |m|
|
82
|
+
return '' if m[1] == '*'
|
83
|
+
"[name()='" + m[1] + "']"
|
84
|
+
},
|
85
|
+
:className => "[contains(concat(' ', @class, ' '), ' \#{1} ')]",
|
86
|
+
:id => "[@id='\#{1}']",
|
87
|
+
:attrPresence => lambda { |m|
|
88
|
+
m[1].downcase!
|
89
|
+
"[@\#{1}]".interpolate(m)
|
90
|
+
},
|
91
|
+
:attr => lambda { |m|
|
92
|
+
m[1].downcase!
|
93
|
+
m[3] = m[5] || m[6]
|
94
|
+
Selector::XPath[:operators][m[2]].interpolate(m)
|
95
|
+
},
|
96
|
+
:pseudo => lambda { |m|
|
97
|
+
h = Selector::XPath[:pseudos][m[1]]
|
98
|
+
return '' if h.nil?
|
99
|
+
return h.call(m) if h.kind_of? Proc
|
100
|
+
Selector::XPath[:pseudos][m[1]].interpolate(m)
|
101
|
+
},
|
102
|
+
:operators => {
|
103
|
+
'=' => "[@\#{1}='\#{3}']",
|
104
|
+
'!=' => "[@\#{1}!='\#{3}']",
|
105
|
+
'^=' => "[starts-with(@\#{1}, '\#{3}')]",
|
106
|
+
'$=' => "[substring(@\#{1}, (string-length(@\#{1}) - string-length('\#{3}') + 1))='\#{3}']",
|
107
|
+
'*=' => "[contains(@\#{1}, '\#{3}')]",
|
108
|
+
'~=' => "[contains(concat(' ', @\#{1}, ' '), ' \#{3} ')]",
|
109
|
+
'|=' => "[contains(concat('-', @\#{1}, '-'), '-\#{3}-')]"
|
110
|
+
},
|
111
|
+
:pseudos => {
|
112
|
+
'first-child' => '[not(preceding-sibling::*)]',
|
113
|
+
'last-child' => '[not(following-sibling::*)]',
|
114
|
+
'only-child' => '[not(preceding-sibling::* or following-sibling::*)]',
|
115
|
+
'empty' => "[count(*) = 0 and (count(text()) = 0)]",
|
116
|
+
'checked' => "[@checked]",
|
117
|
+
'disabled' => "[(@disabled) and (@type!='hidden')]",
|
118
|
+
'enabled' => "[not(@disabled) and (@type!='hidden')]",
|
119
|
+
'not' => lambda { |m|
|
120
|
+
e = m[6]; le = nil; exclusion = []
|
121
|
+
while (e != '') and (le != e) and (e =~ /\S/)
|
122
|
+
le = e.dup
|
123
|
+
Selector::Patterns.each do |pattern|
|
124
|
+
n = Selector::XPath[pattern[:name]]
|
125
|
+
if m = e.match(pattern[:re])
|
126
|
+
v = n.kind_of?(Proc) ? n.call(m) : n.interpolate(m)
|
127
|
+
exclusion << ('(' + v[1, v.length - 2] + ')')
|
128
|
+
e.gsub! m[0], ''
|
129
|
+
break
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
133
|
+
"[not(" + exclusion.join(" and ") + ")]"
|
134
|
+
},
|
135
|
+
'nth-child' => lambda { |m|
|
136
|
+
Selector::XPath[:pseudos]['nth'].call("(count(./preceding-sibling::*) + 1) ", m)
|
137
|
+
},
|
138
|
+
'nth-last-child' => lambda { |m|
|
139
|
+
Selector::XPath[:pseudos]['nth'].call("(count(./following-sibling::*) + 1) ", m)
|
140
|
+
},
|
141
|
+
'nth-of-type' => lambda { |m|
|
142
|
+
Selector::XPath[:pseudos]['nth'].call("position() ", m)
|
143
|
+
},
|
144
|
+
'nth-last-of-type' => lambda { |m|
|
145
|
+
Selector::XPath[:pseudos]['nth'].call("(last() + 1 - position()) ", m)
|
146
|
+
},
|
147
|
+
'first-of-type' => lambda { |m|
|
148
|
+
m[6] = "1"; Selector::XPath[:pseudos]['nth-of-type'].call(m)
|
149
|
+
},
|
150
|
+
'last-of-type' => lambda { |m|
|
151
|
+
m[6] = "1"; Selector::XPath[:pseudos]['nth-last-of-type'].call(m)
|
152
|
+
},
|
153
|
+
'only-of-type' => lambda { |m|
|
154
|
+
p = Selector::XPath[:pseudos]
|
155
|
+
p['first-of-type'].call(m) + p['last-of-type'].call(m)
|
156
|
+
},
|
157
|
+
'nth' => lambda { |fragment, m|
|
158
|
+
mm = nil; formula = m[6]; predicate = nil
|
159
|
+
formula = '2n+0' if formula == 'even'
|
160
|
+
formula = '2n+1' if formula == 'odd'
|
161
|
+
return "[#{fragment}= #{mm[1]}]" if mm = formula.match(/^(\d+)$/) # digit only
|
162
|
+
if mm = formula.match(/^(-?\d*)?n(([+-])(\d+))?/).to_a # an+b
|
163
|
+
mm[1] = -1 if mm[1] == '-'
|
164
|
+
# in JS: mm => ['n', undefined, undefined, undefined, undefined]
|
165
|
+
# in RB: mm => ['n', '', nil, nil, nil]
|
166
|
+
mm[1] = nil if mm[0] == 'n' # edge case
|
167
|
+
a = mm[1] ? mm[1].to_i : 1
|
168
|
+
b = mm[2] ? mm[2].to_i : 0
|
169
|
+
"[((#{fragment} - #{b}) mod #{a} = 0) and ((#{fragment} - #{b}) div #{a} >= 0)]"
|
170
|
+
end
|
171
|
+
}
|
172
|
+
}
|
173
|
+
}
|
174
|
+
|
175
|
+
Patterns = [
|
176
|
+
# combinators must be listed first
|
177
|
+
# (and descendant needs to be last combinator)
|
178
|
+
{ :name => :laterSibling, :re => %r{^\s*~\s*} },
|
179
|
+
{ :name => :child, :re => %r{^\s*>\s*} },
|
180
|
+
{ :name => :adjacent, :re => %r{^\s*\+\s*} },
|
181
|
+
{ :name => :descendant, :re => %r{^\s} },
|
182
|
+
|
183
|
+
# selectors follow
|
184
|
+
{ :name => :tagName, :re => %r{^\s*(\*|[\w\-]+)(\b|$)?} },
|
185
|
+
{ :name => :id, :re => %r{^#([\w\-\*]+)(\b|$)} },
|
186
|
+
{ :name => :className, :re => %r{^\.([\w\-\*]+)(\b|$)} },
|
187
|
+
{ :name => :pseudo, :re => %r{^:((first|last|nth|nth-last|only)(-child|-of-type)|empty|checked|(en|dis)abled|not)(\((.*?)\))?(\b|$|(?=\s|[:+~>]))} },
|
188
|
+
{ :name => :attrPresence, :re => %r{^\[((?:[\w-]+:)?[\w-]+)\]} },
|
189
|
+
{ :name => :attr, :re => %r{\[((?:[\w-]*:)?[\w-]+)\s*(?:([!^$*~|]?=)\s*((['"])([^\4]*?)\4|([^'"][^\]]*?)))?\]} }
|
190
|
+
]
|
191
|
+
|
192
|
+
Assertions = {
|
193
|
+
"tagName" => lambda { |element, matches|
|
194
|
+
matches[1].upcase == element.name.upcase
|
195
|
+
},
|
196
|
+
|
197
|
+
"className" => lambda { |element, matches|
|
198
|
+
element.has_class_name? matches[1]
|
199
|
+
},
|
200
|
+
|
201
|
+
"id" => lambda { |element, matches|
|
202
|
+
element.read_attribute("id") == matches[1]
|
203
|
+
},
|
204
|
+
|
205
|
+
"attrPresence" => lambda { |element, matches|
|
206
|
+
element.has_attribute? matches[1]
|
207
|
+
},
|
208
|
+
|
209
|
+
"attr" => lambda { |element, matches|
|
210
|
+
value = element.read_attribute matches[1]
|
211
|
+
value and Selector::Operators[matches[2]].call(value, matches[5] || matches[6])
|
212
|
+
}
|
213
|
+
}
|
214
|
+
|
215
|
+
Operators = {
|
216
|
+
'=' => lambda { |nv, v| nv == v },
|
217
|
+
'!=' => lambda { |nv, v| nv != v },
|
218
|
+
'^=' => lambda { |nv, v| nv == v or (nv and nv.start_with? v) },
|
219
|
+
'$=' => lambda { |nv, v| nv == v or (nv and nv.end_with? v) },
|
220
|
+
'*=' => lambda { |nv, v| nv == v or (nv and nv.include? v) },
|
221
|
+
'~=' => lambda { |nv, v| " #{nv} ".include? " #{v} " },
|
222
|
+
'|=' => lambda { |nv, v| "-#{(nv || '').upcase}-".include? "-#{(v || '').upcase}-" }
|
223
|
+
}
|
224
|
+
|
225
|
+
def self.split(expression)
|
226
|
+
expressions = []
|
227
|
+
expression.scan(/(([\w#:.~>+()\s-]+|\*|\[.*?\])+)\s*(,|$)/) do |m|
|
228
|
+
expressions << m[1].strip
|
229
|
+
end
|
230
|
+
expressions
|
231
|
+
end
|
232
|
+
|
233
|
+
def self.match_elements(elements, expression)
|
234
|
+
elements & elements[0].rootDocument.select(expression)
|
235
|
+
end
|
236
|
+
|
237
|
+
def self.find_element(elements, *rest)
|
238
|
+
expression, index = rest[0], rest[1]
|
239
|
+
(expression = nil; index = expression) if expression.kind_of? Integer
|
240
|
+
Selector::match_elements(elements, expression || '*')[index || 0]
|
241
|
+
end
|
242
|
+
|
243
|
+
def self.find_child_elements(element, expressions)
|
244
|
+
Selector::split(expressions.join(',')).map do |expression|
|
245
|
+
Selector.new(expression.strip).find_elements(element)
|
246
|
+
end.uniq.flatten
|
247
|
+
end
|
248
|
+
end
|
249
|
+
end
|
250
|
+
|