ayril 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/CHANGES ADDED
@@ -0,0 +1,4 @@
1
+ v 0.1.0
2
+ ===============================
3
+ * Added a sanity check and fixed a bug where text nodes caused massive fail
4
+ * Gemified source from https://github.com/kourge/ayril
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source :rubygems
2
+
3
+ gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,19 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ ayril (1.0.0)
5
+
6
+ GEM
7
+ remote: http://rubygems.org/
8
+ specs:
9
+ MacSpec (0.4.5)
10
+ rake (0.9.2)
11
+
12
+ PLATFORMS
13
+ ruby
14
+
15
+ DEPENDENCIES
16
+ MacSpec (~> 0.4.5)
17
+ ayril!
18
+ bundler (~> 1.0.0)
19
+ rake
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
+