glue 0.17.0 → 0.18.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/CHANGELOG CHANGED
@@ -1,3 +1,29 @@
1
+ 31-05-2005 George Moschovitis <gm@navel.gr>
2
+
3
+ * test/*: fixes to pass again.
4
+
5
+ 27-05-2005 George Moschovitis <gm@navel.gr>
6
+
7
+ * lib/glue/sanitize.rb: added.
8
+
9
+ 25-05-2005 George Moschovitis <gm@navel.gr>
10
+
11
+ * Rakefile: added the dependency of facets here.
12
+
13
+ 24-05-2005 George Moschovitis <gm@navel.gr>
14
+
15
+ * lib/glue/inflector.rb: removed (now in facets).
16
+
17
+ * lib/glue/dynamic_include.rb: removed (now in facets).
18
+
19
+ * lib/glue/cache.rb: removed (now in facets)
20
+
21
+ * vendor: removed.
22
+
23
+ 22-05-2005 George Moschovitis <gm@navel.gr>
24
+
25
+ * lib/glue.rb (pp_exception): added some cleanup code.
26
+
1
27
  15-05-2005 George Moschovitis <gm@navel.gr>
2
28
 
3
29
  * doc/RELEASES: updated.
data/Rakefile CHANGED
@@ -59,7 +59,7 @@ spec = Gem::Specification.new do |s|
59
59
  end
60
60
  s.summary = 'Glue utilities'
61
61
  s.description = 'A collection of utilities and useful classes'
62
- s.add_dependency 'facets', '>= 0.7.1'
62
+ s.add_dependency 'facets', '>= 0.7.2'
63
63
  #s.add_dependency 'flexmock', '>= 0.0.3'
64
64
 
65
65
  s.required_ruby_version = '>= 1.8.2'
@@ -1,4 +1,9 @@
1
- == Version 0.17.0
1
+ == Version 0.18.0
2
+
3
+ Deprecated many methods. Facet methods are used instead.
4
+
5
+
6
+ == Version 0.17.0 was released on 16/05/2005.
2
7
 
3
8
  A lot of fucntionality is deprecated. The great Facets library
4
9
  will gradually replace the general utility methods found in
@@ -51,7 +51,7 @@ module Kernel
51
51
  # the pretty printed string
52
52
 
53
53
  def pp_exception(ex)
54
- return %{#{ex.message}\n\tBACKTRACE:\n\t#{ex.backtrace.join("\n\t")}\n\tLOGGED FROM:\n\t#{caller[0]}}
54
+ return %{#{ex.message}\n #{ex.backtrace.join("\n ")}\n LOGGED FROM: #{caller[0].gsub("#{Nitro::LibPath}/", '')}}
55
55
  end
56
56
 
57
57
  end
@@ -60,7 +60,7 @@ module Glue
60
60
 
61
61
  # The version.
62
62
 
63
- Version = '0.17.0'
63
+ Version = '0.18.0'
64
64
 
65
65
  # Library path.
66
66
 
@@ -1,7 +1,5 @@
1
- # * George Moschovitis <gm@navel.gr>
2
1
  # Original code from Rails distribution.
3
2
  # http://www.rubyonrails.com
4
- # $Id: attribute.rb 1 2005-04-11 11:04:30Z gmosx $
5
3
 
6
4
  #--
7
5
  # Extends the module object with module and instance accessors
@@ -76,3 +74,5 @@ class Module # :nodoc:
76
74
  alias_method :cattr_accessor, :mattr_accessor
77
75
 
78
76
  end
77
+
78
+ # * George Moschovitis <gm@navel.gr>
@@ -1,7 +1,3 @@
1
- # * George Moschovitis <gm@navel.gr>
2
- # (c) 2004-2005 Navel, all rights reserved.
3
- # $Id: logger.rb 1 2005-04-11 11:04:30Z gmosx $
4
-
5
1
  require 'logger'
6
2
 
7
3
  # A simple wrapper arround the Ruby logger. Mainly for
@@ -47,7 +43,7 @@ class Logger
47
43
  unless expr.respond_to? :to_str
48
44
  warn "trace: Can't evaluate the given value: #{caller.first}"
49
45
  else
50
- require 'extensions/binding'
46
+ require 'facet/binding-of-caller'
51
47
 
52
48
  Binding.of_caller do |b|
53
49
  value = b.eval(expr.to_str)
@@ -115,10 +111,10 @@ class Logger
115
111
  unless expr.respond_to? :to_str
116
112
  warn "trace: Can't evaluate the given value: #{caller.first}"
117
113
  else
118
- require 'extensions/binding'
114
+ require 'facet/binding-of-caller'
119
115
 
120
116
  Binding.of_caller do |b|
121
- value = b.eval(expr.to_str)
117
+ value = eval(expr.to_str, b)
122
118
  formatter = TRACE_STYLES[style] || :inspect
123
119
  case formatter
124
120
  when :pp then require 'pp'
@@ -170,3 +166,5 @@ module Logging
170
166
  end
171
167
 
172
168
  end
169
+
170
+ # * George Moschovitis <gm@navel.gr>
@@ -1,6 +1,6 @@
1
1
  # * George Moschovitis <gm@navel.gr>
2
2
  # (c) 2004-2005 Navel, all rights reserved.
3
- # $Id: number.rb 35 2005-05-04 07:49:35Z gmosx $
3
+ # $Id: number.rb 74 2005-05-24 07:29:50Z gmosx $
4
4
 
5
5
  module Glue;
6
6
 
@@ -14,7 +14,7 @@ module NumberUtils
14
14
  # Returns the multiple ceil of a number
15
15
 
16
16
  def self.ceil_multiple(num, multiple)
17
- # gmosx: to_f is needed!s
17
+ # gmosx: to_f is needed!
18
18
  # gmosx: IS THERE a more optimized way to do this?
19
19
  return ((num.to_f/multiple).ceil*multiple)
20
20
  end
@@ -0,0 +1,48 @@
1
+ # Code from rubyonrails project (http://www.rubyonrails.com)
2
+ # Temporarily here.
3
+
4
+ require 'html/tokenizer'
5
+ require 'html/node'
6
+
7
+ VERBOTEN_TAGS = %w(form script) unless defined?(VERBOTEN_TAGS)
8
+ VERBOTEN_ATTRS = /^on/i unless defined?(VERBOTEN_ATTRS)
9
+
10
+ class String
11
+ # Sanitizes the given HTML by making form and script tags into regular
12
+ # text, and removing all "onxxx" attributes (so that arbitrary Javascript
13
+ # cannot be executed). Also removes href attributes that start with
14
+ # "javascript:".
15
+ #
16
+ # Returns the sanitized text.
17
+ def self.sanitize(html)
18
+ # only do this if absolutely necessary
19
+ if html.index("<")
20
+ tokenizer = HTML::Tokenizer.new(html)
21
+ new_text = ""
22
+
23
+ while token = tokenizer.next
24
+ node = HTML::Node.parse(nil, 0, 0, token, false)
25
+ new_text << case node
26
+ when HTML::Tag
27
+ if VERBOTEN_TAGS.include?(node.name)
28
+ node.to_s.gsub(/</, "&lt;")
29
+ else
30
+ if node.closing != :close
31
+ node.attributes.delete_if { |attr,v| attr =~ VERBOTEN_ATTRS }
32
+ if node.attributes["href"] =~ /^javascript:/i
33
+ node.attributes.delete "href"
34
+ end
35
+ end
36
+ node.to_s
37
+ end
38
+ else
39
+ node.to_s.gsub(/</, "&lt;")
40
+ end
41
+ end
42
+
43
+ html = new_text
44
+ end
45
+
46
+ html
47
+ end
48
+ end
@@ -0,0 +1,63 @@
1
+ require 'html/tokenizer'
2
+ require 'html/node'
3
+
4
+ module HTML#:nodoc:
5
+
6
+ # A top-level HTMl document. You give it a body of text, and it will parse that
7
+ # text into a tree of nodes.
8
+ class Document #:nodoc:
9
+
10
+ # The root of the parsed document.
11
+ attr_reader :root
12
+
13
+ # Create a new Document from the given text.
14
+ def initialize(text)
15
+ tokenizer = Tokenizer.new(text)
16
+ @root = Node.new(nil)
17
+ node_stack = [ @root ]
18
+ while token = tokenizer.next
19
+ node = Node.parse(node_stack.last, tokenizer.line, tokenizer.position, token)
20
+
21
+ node_stack.last.children << node unless node.tag? && node.closing == :close
22
+ if node.tag? && !node.childless?
23
+ if node_stack.length > 1 && node.closing == :close
24
+ if node_stack.last.name == node.name
25
+ node_stack.pop
26
+ else
27
+ open_start = node_stack.last.position - 20
28
+ open_start = 0 if open_start < 0
29
+ close_start = node.position - 20
30
+ close_start = 0 if close_start < 0
31
+ warn <<EOF.strip
32
+ ignoring attempt to close #{node_stack.last.name} with #{node.name}
33
+ opened at byte #{node_stack.last.position}, line #{node_stack.last.line}
34
+ closed at byte #{node.position}, line #{node.line}
35
+ attributes at open: #{node_stack.last.attributes.inspect}
36
+ text around open: #{text[open_start,40].inspect}
37
+ text around close: #{text[close_start,40].inspect}
38
+ EOF
39
+ end
40
+ elsif node.closing != :close
41
+ node_stack.push node
42
+ end
43
+ end
44
+ end
45
+ end
46
+
47
+ # Search the tree for (and return) the first node that matches the given
48
+ # conditions. The conditions are interpreted differently for different node
49
+ # types, see HTML::Text#find and HTML::Tag#find.
50
+ def find(conditions)
51
+ @root.find(conditions)
52
+ end
53
+
54
+ # Search the tree for (and return) all nodes that match the given
55
+ # conditions. The conditions are interpreted differently for different node
56
+ # types, see HTML::Text#find and HTML::Tag#find.
57
+ def find_all(conditions)
58
+ @root.find_all(conditions)
59
+ end
60
+
61
+ end
62
+
63
+ end
@@ -0,0 +1,480 @@
1
+ require 'strscan'
2
+
3
+ module HTML#:nodoc:
4
+
5
+ class Conditions < Hash#:nodoc:
6
+ def initialize(hash)
7
+ super()
8
+ hash = { :content => hash } unless Hash === hash
9
+ hash = keys_to_symbols(hash)
10
+ hash.each do |k,v|
11
+ case k
12
+ when :tag, :content then
13
+ # keys are valid, and require no further processing
14
+ when :attributes then
15
+ hash[k] = keys_to_strings(v)
16
+ when :parent, :child, :ancestor, :descendant, :sibling, :before,
17
+ :after
18
+ hash[k] = Conditions.new(v)
19
+ when :children
20
+ hash[k] = v = keys_to_symbols(v)
21
+ v.each do |k,v2|
22
+ case k
23
+ when :count, :greater_than, :less_than
24
+ # keys are valid, and require no further processing
25
+ when :only
26
+ v[k] = Conditions.new(v2)
27
+ else
28
+ raise "illegal key #{k.inspect} => #{v2.inspect}"
29
+ end
30
+ end
31
+ else
32
+ raise "illegal key #{k.inspect} => #{v.inspect}"
33
+ end
34
+ end
35
+ update hash
36
+ end
37
+
38
+ private
39
+
40
+ def keys_to_strings(hash)
41
+ hash.keys.inject({}) do |h,k|
42
+ h[k.to_s] = hash[k]
43
+ h
44
+ end
45
+ end
46
+
47
+ def keys_to_symbols(hash)
48
+ hash.keys.inject({}) do |h,k|
49
+ raise "illegal key #{k.inspect}" unless k.respond_to?(:to_sym)
50
+ h[k.to_sym] = hash[k]
51
+ h
52
+ end
53
+ end
54
+ end
55
+
56
+ # The base class of all nodes, textual and otherwise, in an HTML document.
57
+ class Node#:nodoc:
58
+ # The array of children of this node. Not all nodes have children.
59
+ attr_reader :children
60
+
61
+ # The parent node of this node. All nodes have a parent, except for the
62
+ # root node.
63
+ attr_reader :parent
64
+
65
+ # The line number of the input where this node was begun
66
+ attr_reader :line
67
+
68
+ # The byte position in the input where this node was begun
69
+ attr_reader :position
70
+
71
+ # Create a new node as a child of the given parent.
72
+ def initialize(parent, line=0, pos=0)
73
+ @parent = parent
74
+ @children = []
75
+ @line, @position = line, pos
76
+ end
77
+
78
+ # Return a textual representation of the node.
79
+ def to_s
80
+ s = ""
81
+ @children.each { |child| s << child.to_s }
82
+ s
83
+ end
84
+
85
+ # Return false (subclasses must override this to provide specific matching
86
+ # behavior.) +conditions+ may be of any type.
87
+ def match(conditions)
88
+ false
89
+ end
90
+
91
+ # Search the children of this node for the first node for which #find
92
+ # returns non +nil+. Returns the result of the #find call that succeeded.
93
+ def find(conditions)
94
+ @children.each do |child|
95
+ node = child.find(conditions)
96
+ return node if node
97
+ end
98
+ nil
99
+ end
100
+
101
+ # Search for all nodes that match the given conditions, and return them
102
+ # as an array.
103
+ def find_all(conditions)
104
+ matches = []
105
+ matches << self if match(conditions)
106
+ @children.each do |child|
107
+ matches.concat child.find_all(conditions)
108
+ end
109
+ matches
110
+ end
111
+
112
+ # Returns +false+. Subclasses may override this if they define a kind of
113
+ # tag.
114
+ def tag?
115
+ false
116
+ end
117
+
118
+ def validate_conditions(conditions)
119
+ Conditions === conditions ? conditions : Conditions.new(conditions)
120
+ end
121
+
122
+ class <<self
123
+ def parse(parent, line, pos, content, strict=true)
124
+ if content !~ /^<\S/
125
+ Text.new(parent, line, pos, content)
126
+ else
127
+ scanner = StringScanner.new(content)
128
+
129
+ unless scanner.skip(/</)
130
+ if strict
131
+ raise "expected <"
132
+ else
133
+ return Text.new(parent, line, pos, content)
134
+ end
135
+ end
136
+
137
+ closing = ( scanner.scan(/\//) ? :close : nil )
138
+ return Text.new(parent, line, pos, content) unless name = scanner.scan(/[\w:]+/)
139
+ name.downcase!
140
+
141
+ unless closing
142
+ scanner.skip(/\s*/)
143
+ attributes = {}
144
+ while attr = scanner.scan(/[-\w:]+/)
145
+ value = true
146
+ if scanner.scan(/\s*=\s*/)
147
+ if delim = scanner.scan(/['"]/)
148
+ value = ""
149
+ while text = scanner.scan(/[^#{delim}\\]+|./)
150
+ case text
151
+ when "\\" then
152
+ value << text
153
+ value << scanner.getch
154
+ when delim
155
+ break
156
+ else value << text
157
+ end
158
+ end
159
+ else
160
+ value = scanner.scan(/[^\s>\/]+/)
161
+ end
162
+ end
163
+ attributes[attr.downcase] = value
164
+ scanner.skip(/\s*/)
165
+ end
166
+
167
+ closing = ( scanner.scan(/\//) ? :self : nil )
168
+ end
169
+
170
+ unless scanner.scan(/\s*>/)
171
+ if strict
172
+ raise "expected > (got #{scanner.rest.inspect} for #{content}, #{attributes.inspect})"
173
+ else
174
+ # throw away all text until we find what we're looking for
175
+ scanner.skip_until(/>/) or scanner.terminate
176
+ end
177
+ end
178
+
179
+ Tag.new(parent, line, pos, name, attributes, closing)
180
+ end
181
+ end
182
+ end
183
+ end
184
+
185
+ # A node that represents text, rather than markup.
186
+ class Text < Node#:nodoc:
187
+
188
+ attr_reader :content
189
+
190
+ # Creates a new text node as a child of the given parent, with the given
191
+ # content.
192
+ def initialize(parent, line, pos, content)
193
+ super(parent, line, pos)
194
+ @content = content
195
+ end
196
+
197
+ # Returns the content of this node.
198
+ def to_s
199
+ @content
200
+ end
201
+
202
+ # Returns +self+ if this node meets the given conditions. Text nodes support
203
+ # conditions of the following kinds:
204
+ #
205
+ # * if +conditions+ is a string, it must be a substring of the node's
206
+ # content
207
+ # * if +conditions+ is a regular expression, it must match the node's
208
+ # content
209
+ # * if +conditions+ is a hash, it must contain a <tt>:content</tt> key that
210
+ # is either a string or a regexp, and which is interpreted as described
211
+ # above.
212
+ def find(conditions)
213
+ match(conditions) && self
214
+ end
215
+
216
+ # Returns non-+nil+ if this node meets the given conditions, or +nil+
217
+ # otherwise. See the discussion of #find for the valid conditions.
218
+ def match(conditions)
219
+ case conditions
220
+ when String
221
+ @content.index(conditions)
222
+ when Regexp
223
+ @content =~ conditions
224
+ when Hash
225
+ conditions = validate_conditions(conditions)
226
+
227
+ # Text nodes only have :content, :parent, :ancestor
228
+ unless (conditions.keys - [:content, :parent, :ancestor]).empty?
229
+ return false
230
+ end
231
+
232
+ match(conditions[:content])
233
+ else
234
+ nil
235
+ end
236
+ end
237
+ end
238
+
239
+ # A Tag is any node that represents markup. It may be an opening tag, a
240
+ # closing tag, or a self-closing tag. It has a name, and may have a hash of
241
+ # attributes.
242
+ class Tag < Node#:nodoc:
243
+
244
+ # Either +nil+, <tt>:close</tt>, or <tt>:self</tt>
245
+ attr_reader :closing
246
+
247
+ # Either +nil+, or a hash of attributes for this node.
248
+ attr_reader :attributes
249
+
250
+ # The name of this tag.
251
+ attr_reader :name
252
+
253
+ # Create a new node as a child of the given parent, using the given content
254
+ # to describe the node. It will be parsed and the node name, attributes and
255
+ # closing status extracted.
256
+ def initialize(parent, line, pos, name, attributes, closing)
257
+ super(parent, line, pos)
258
+ @name = name
259
+ @attributes = attributes
260
+ @closing = closing
261
+ end
262
+
263
+ # A convenience for obtaining an attribute of the node. Returns +nil+ if
264
+ # the node has no attributes.
265
+ def [](attr)
266
+ @attributes ? @attributes[attr] : nil
267
+ end
268
+
269
+ # Returns non-+nil+ if this tag can contain child nodes.
270
+ def childless?
271
+ @name =~ /^(img|br|hr|link|meta|area|base|basefont|col|frame|input|isindex|param)$/o
272
+ end
273
+
274
+ # Returns a textual representation of the node
275
+ def to_s
276
+ if @closing == :close
277
+ "</#{@name}>"
278
+ else
279
+ s = "<#{@name}"
280
+ @attributes.each do |k,v|
281
+ s << " #{k}"
282
+ s << "='#{v.gsub(/'/,"\\\\'")}'" if String === v
283
+ end
284
+ s << " /" if @closing == :self
285
+ s << ">"
286
+ @children.each { |child| s << child.to_s }
287
+ s
288
+ end
289
+ end
290
+
291
+ # If either the node or any of its children meet the given conditions, the
292
+ # matching node is returned. Otherwise, +nil+ is returned. (See the
293
+ # description of the valid conditions in the +match+ method.)
294
+ def find(conditions)
295
+ match(conditions) && self || super
296
+ end
297
+
298
+ # Returns +true+, indicating that this node represents an HTML tag.
299
+ def tag?
300
+ true
301
+ end
302
+
303
+ # Returns +true+ if the node meets any of the given conditions. The
304
+ # +conditions+ parameter must be a hash of any of the following keys
305
+ # (all are optional):
306
+ #
307
+ # * <tt>:tag</tt>: the node name must match the corresponding value
308
+ # * <tt>:attributes</tt>: a hash. The node's values must match the
309
+ # corresponding values in the hash.
310
+ # * <tt>:parent</tt>: a hash. The node's parent must match the
311
+ # corresponding hash.
312
+ # * <tt>:child</tt>: a hash. At least one of the node's immediate children
313
+ # must meet the criteria described by the hash.
314
+ # * <tt>:ancestor</tt>: a hash. At least one of the node's ancestors must
315
+ # meet the criteria described by the hash.
316
+ # * <tt>:descendant</tt>: a hash. At least one of the node's descendants
317
+ # must meet the criteria described by the hash.
318
+ # * <tt>:sibling</tt>: a hash. At least one of the node's siblings must
319
+ # meet the criteria described by the hash.
320
+ # * <tt>:after</tt>: a hash. The node must be after any sibling meeting
321
+ # the criteria described by the hash, and at least one sibling must match.
322
+ # * <tt>:before</tt>: a hash. The node must be before any sibling meeting
323
+ # the criteria described by the hash, and at least one sibling must match.
324
+ # * <tt>:children</tt>: a hash, for counting children of a node. Accepts the
325
+ # keys:
326
+ # ** <tt>:count</tt>: either a number or a range which must equal (or
327
+ # include) the number of children that match.
328
+ # ** <tt>:less_than</tt>: the number of matching children must be less than
329
+ # this number.
330
+ # ** <tt>:greater_than</tt>: the number of matching children must be
331
+ # greater than this number.
332
+ # ** <tt>:only</tt>: another hash consisting of the keys to use
333
+ # to match on the children, and only matching children will be
334
+ # counted.
335
+ #
336
+ # Conditions are matched using the following algorithm:
337
+ #
338
+ # * if the condition is a string, it must be a substring of the value.
339
+ # * if the condition is a regexp, it must match the value.
340
+ # * if the condition is a number, the value must match number.to_s.
341
+ # * if the condition is +true+, the value must not be +nil+.
342
+ # * if the condition is +false+ or +nil+, the value must be +nil+.
343
+ #
344
+ # Usage:
345
+ #
346
+ # # test if the node is a "span" tag
347
+ # node.match :tag => "span"
348
+ #
349
+ # # test if the node's parent is a "div"
350
+ # node.match :parent => { :tag => "div" }
351
+ #
352
+ # # test if any of the node's ancestors are "table" tags
353
+ # node.match :ancestor => { :tag => "table" }
354
+ #
355
+ # # test if any of the node's immediate children are "em" tags
356
+ # node.match :child => { :tag => "em" }
357
+ #
358
+ # # test if any of the node's descendants are "strong" tags
359
+ # node.match :descendant => { :tag => "strong" }
360
+ #
361
+ # # test if the node has between 2 and 4 span tags as immediate children
362
+ # node.match :children => { :count => 2..4, :only => { :tag => "span" } }
363
+ #
364
+ # # get funky: test to see if the node is a "div", has a "ul" ancestor
365
+ # # and an "li" parent (with "class" = "enum"), and whether or not it has
366
+ # # a "span" descendant that contains # text matching /hello world/:
367
+ # node.match :tag => "div",
368
+ # :ancestor => { :tag => "ul" },
369
+ # :parent => { :tag => "li",
370
+ # :attributes => { :class => "enum" } },
371
+ # :descendant => { :tag => "span",
372
+ # :child => /hello world/ }
373
+ def match(conditions)
374
+ conditions = validate_conditions(conditions)
375
+
376
+ # only Text nodes have content
377
+ return false if conditions[:content]
378
+
379
+ # test the name
380
+ return false unless match_condition(@name, conditions[:tag]) if conditions[:tag]
381
+
382
+ # test attributes
383
+ (conditions[:attributes] || {}).each do |key, value|
384
+ return false unless match_condition(self[key], value)
385
+ end
386
+
387
+ # test parent
388
+ return false unless parent.match(conditions[:parent]) if conditions[:parent]
389
+
390
+ # test children
391
+ return false unless children.find { |child| child.match(conditions[:child]) } if conditions[:child]
392
+
393
+ # test ancestors
394
+ if conditions[:ancestor]
395
+ return false unless catch :found do
396
+ p = self
397
+ throw :found, true if p.match(conditions[:ancestor]) while p = p.parent
398
+ end
399
+ end
400
+
401
+ # test descendants
402
+ if conditions[:descendant]
403
+ return false unless children.find do |child|
404
+ # test the child
405
+ child.match(conditions[:descendant]) ||
406
+ # test the child's descendants
407
+ child.match(:descendant => conditions[:descendant])
408
+ end
409
+ end
410
+
411
+ # count children
412
+ if opts = conditions[:children]
413
+ matches = children
414
+ matches = matches.select { |c| c.match(opts[:only]) } if opts[:only]
415
+ opts.each do |key, value|
416
+ next if key == :only
417
+ case key
418
+ when :count
419
+ if Integer === value
420
+ return false if matches.length != value
421
+ else
422
+ return false unless value.include?(matches.length)
423
+ end
424
+ when :less_than
425
+ return false unless matches.length < value
426
+ when :greater_than
427
+ return false unless matches.length > value
428
+ else raise "unknown count condition #{key}"
429
+ end
430
+ end
431
+ end
432
+
433
+ # test siblings
434
+ if conditions[:sibling] || conditions[:before] || conditions[:after]
435
+ siblings = parent ? parent.children : []
436
+ self_index = siblings.index(self)
437
+
438
+ if conditions[:sibling]
439
+ return false unless siblings.detect do |s|
440
+ s != self && s.match(conditions[:sibling])
441
+ end
442
+ end
443
+
444
+ if conditions[:before]
445
+ return false unless siblings[self_index+1..-1].detect do |s|
446
+ s != self && s.match(conditions[:before])
447
+ end
448
+ end
449
+
450
+ if conditions[:after]
451
+ return false unless siblings[0,self_index].detect do |s|
452
+ s != self && s.match(conditions[:after])
453
+ end
454
+ end
455
+ end
456
+
457
+ true
458
+ end
459
+
460
+ private
461
+
462
+ # Match the given value to the given condition.
463
+ def match_condition(value, condition)
464
+ case condition
465
+ when String
466
+ value && value.index(condition)
467
+ when Regexp
468
+ value && value.match(condition)
469
+ when Numeric
470
+ value == condition.to_s
471
+ when true
472
+ !value.nil?
473
+ when false, nil
474
+ value.nil?
475
+ else
476
+ false
477
+ end
478
+ end
479
+ end
480
+ end