ayril 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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
+