Class: Ayril::Selector

Inherits:
Object show all
Defined in:
lib/ayril/selector.rb

Constant Summary collapse

XPath =
{
  :descendant =>   "//*",
  :child =>        "/*",
  :adjacent =>     "/following-sibling::*[1]",
  :laterSibling => '/following-sibling::*',
  :tagName =>      lambda { |m|
    return '' if m[1] == '*'
    "[name()='" + m[1] + "']"
  },
  :className =>    "[contains(concat(' ', @class, ' '), ' \#{1} ')]",
  :id =>           "[@id='\#{1}']",
  :attrPresence => lambda { |m|
    m[1].downcase!
    "[@\#{1}]".interpolate(m)
  },
  :attr => lambda { |m|
    m[1].downcase!
    m[3] = m[5] || m[6]
    Selector::XPath[:operators][m[2]].interpolate(m)
  },
  :pseudo => lambda { |m|
    h = Selector::XPath[:pseudos][m[1]]
    return '' if h.nil?
    return h.call(m) if h.kind_of? Proc
    Selector::XPath[:pseudos][m[1]].interpolate(m)
  },
  :operators => {
    '='  => "[@\#{1}='\#{3}']",
    '!=' => "[@\#{1}!='\#{3}']",
    '^=' => "[starts-with(@\#{1}, '\#{3}')]",
    '$=' => "[substring(@\#{1}, (string-length(@\#{1}) - string-length('\#{3}') + 1))='\#{3}']",
    '*=' => "[contains(@\#{1}, '\#{3}')]",
    '~=' => "[contains(concat(' ', @\#{1}, ' '), ' \#{3} ')]",
    '|=' => "[contains(concat('-', @\#{1}, '-'), '-\#{3}-')]"
  },
  :pseudos => {
    'first-child' => '[not(preceding-sibling::*)]',
    'last-child' =>  '[not(following-sibling::*)]',
    'only-child' =>  '[not(preceding-sibling::* or following-sibling::*)]',
    'empty' =>       "[count(*) = 0 and (count(text()) = 0)]",
    'checked' =>     "[@checked]",
    'disabled' =>    "[(@disabled) and (@type!='hidden')]",
    'enabled' =>     "[not(@disabled) and (@type!='hidden')]",
    'not' => lambda { |m|
      e = m[6]; le = nil; exclusion = []
      while (e != '') and (le != e) and (e =~ /\S/)
        le = e.dup
        Selector::Patterns.each do |pattern|
          n = Selector::XPath[pattern[:name]]
          if m = e.match(pattern[:re])
            v = n.kind_of?(Proc) ? n.call(m) : n.interpolate(m)
            exclusion << ('(' + v[1, v.length - 2] + ')')
            e.gsub! m[0], ''
            break
          end
        end
      end      
      "[not(" + exclusion.join(" and ") + ")]"
    },
    'nth-child' =>      lambda { |m| 
      Selector::XPath[:pseudos]['nth'].call("(count(./preceding-sibling::*) + 1) ", m)
    },
    'nth-last-child' => lambda { |m|
      Selector::XPath[:pseudos]['nth'].call("(count(./following-sibling::*) + 1) ", m)
    },
    'nth-of-type' =>    lambda { |m|
      Selector::XPath[:pseudos]['nth'].call("position() ", m)
    },
    'nth-last-of-type' => lambda { |m|
      Selector::XPath[:pseudos]['nth'].call("(last() + 1 - position()) ", m)
    },
    'first-of-type' =>  lambda { |m| 
      m[6] = "1"; Selector::XPath[:pseudos]['nth-of-type'].call(m)
    },
    'last-of-type' =>   lambda { |m|
      m[6] = "1"; Selector::XPath[:pseudos]['nth-last-of-type'].call(m)
    },
    'only-of-type' =>   lambda { |m|
      p = Selector::XPath[:pseudos]
      p['first-of-type'].call(m) + p['last-of-type'].call(m)
    },
    'nth' => lambda { |fragment, m|
      mm = nil; formula = m[6]; predicate = nil
      formula = '2n+0' if formula == 'even'
      formula = '2n+1' if formula == 'odd'
      return "[#{fragment}= #{mm[1]}]" if mm = formula.match(/^(\d+)$/) # digit only
      if mm = formula.match(/^(-?\d*)?n(([+-])(\d+))?/).to_a # an+b
        mm[1] = -1 if mm[1] == '-'
        # in JS: mm => ['n', undefined, undefined, undefined, undefined]
        # in RB: mm => ['n', '', nil, nil, nil]
        mm[1] = nil if mm[0] == 'n' # edge case
        a = mm[1] ? mm[1].to_i : 1
        b = mm[2] ? mm[2].to_i : 0
        "[((#{fragment} - #{b}) mod #{a} = 0) and ((#{fragment} - #{b}) div #{a} >= 0)]"
      end
    }
  }    
}
Patterns =
[
  # combinators must be listed first
  # (and descendant needs to be last combinator)
  { :name => :laterSibling, :re => %r{^\s*~\s*}  },
  { :name => :child,        :re => %r{^\s*>\s*}  },
  { :name => :adjacent,     :re => %r{^\s*\+\s*}  },
  { :name => :descendant,   :re => %r{^\s}  },

  # selectors follow
  { :name => :tagName,      :re => %r{^\s*(\*|[\w\-]+)(\b|$)?}  },
  { :name => :id,           :re => %r{^#([\w\-\*]+)(\b|$)}  },
  { :name => :className,    :re => %r{^\.([\w\-\*]+)(\b|$)}  },
  { :name => :pseudo,       :re => %r{^:((first|last|nth|nth-last|only)(-child|-of-type)|empty|checked|(en|dis)abled|not)(\((.*?)\))?(\b|$|(?=\s|[:+~>]))}  },
  { :name => :attrPresence, :re => %r{^\[((?:[\w-]+:)?[\w-]+)\]}  },
  { :name => :attr,         :re => %r{\[((?:[\w-]*:)?[\w-]+)\s*(?:([!^$*~|]?=)\s*((['"])([^\4]*?)\4|([^'"][^\]]*?)))?\]} }
]
Assertions =
{
  "tagName" => lambda { |element, matches|
     matches[1].upcase == element.name.upcase
  },

  "className" => lambda { |element, matches|
     element.has_class_name? matches[1]
  },

  "id" => lambda { |element, matches|
     element.read_attribute("id") == matches[1]
  },

  "attrPresence" => lambda { |element, matches|
     element.has_attribute? matches[1]
  },

  "attr" => lambda { |element, matches|
    value = element.read_attribute matches[1]
    value and Selector::Operators[matches[2]].call(value, matches[5] || matches[6])
  }
}
Operators =
{
  '=' =>  lambda { |nv, v| nv == v },
  '!=' => lambda { |nv, v| nv != v },
  '^=' => lambda { |nv, v| nv == v or (nv and nv.start_with? v) },
  '$=' => lambda { |nv, v| nv == v or (nv and nv.end_with? v) },
  '*=' => lambda { |nv, v| nv == v or (nv and nv.include? v) },
  '~=' => lambda { |nv, v| " #{nv} ".include? " #{v} " },
  '|=' => lambda { |nv, v| "-#{(nv || '').upcase}-".include? "-#{(v || '').upcase}-" }    
}
@@cache =
{}

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(expr) ⇒ Selector

Returns a new instance of Selector.



6
7
8
9
# File 'lib/ayril/selector.rb', line 6

def initialize(expr)
  @expr = expr.strip
  self.compile_xpath_matcher
end

Instance Attribute Details

#exprObject (readonly)

Returns the value of attribute expr.



3
4
5
# File 'lib/ayril/selector.rb', line 3

def expr
  @expr
end

#xpathObject (readonly)

Returns the value of attribute xpath.



3
4
5
# File 'lib/ayril/selector.rb', line 3

def xpath
  @xpath
end

Class Method Details

.find_child_elements(element, expressions) ⇒ Object



243
244
245
246
247
# File 'lib/ayril/selector.rb', line 243

def self.find_child_elements(element, expressions)
  Selector::split(expressions.join(',')).map do |expression|
    Selector.new(expression.strip).find_elements(element)
  end.uniq.flatten
end

.find_element(elements, *rest) ⇒ Object



237
238
239
240
241
# File 'lib/ayril/selector.rb', line 237

def self.find_element(elements, *rest)
  expression, index = rest[0], rest[1]
  (expression = nil; index = expression) if expression.kind_of? Integer
  Selector::match_elements(elements, expression || '*')[index || 0]
end

.match_elements(elements, expression) ⇒ Object



233
234
235
# File 'lib/ayril/selector.rb', line 233

def self.match_elements(elements, expression)
  elements & elements[0].rootDocument.select(expression)
end

.split(expression) ⇒ Object



225
226
227
228
229
230
231
# File 'lib/ayril/selector.rb', line 225

def self.split(expression)
  expressions = []
  expression.scan(/(([\w#:.~>+()\s-]+|\*|\[.*?\])+)\s*(,|$)/) do |m|
    expressions << m[1].strip
  end
  expressions
end

Instance Method Details

#compile_xpath_matcherObject



11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# File 'lib/ayril/selector.rb', line 11

def compile_xpath_matcher
  e = @expr.dup; le = nil
  return (@xpath = @@cache[e]) if @@cache.include? e

  @matcher = [".//*"]
  while (e != '') and (le != e) and (e =~ /\S/)
    le = e.dup
    Selector::Patterns.each do |pattern|
      n = Selector::XPath[pattern[:name]]
      if m = e.match(pattern[:re])
        m = m.to_a unless m.nil?
        @matcher << (n.kind_of?(Proc) ? n.call(m) : n.interpolate(m))
        e.sub! m[0], ''
        break
      end
    end
  end

  @xpath = @matcher.join
  @@cache[@expr] = @xpath.gsub! %r{\*?\[name\(\)='([a-zA-Z]+)'\]}, '\1'
end

#find_elements(root) ⇒ Object



33
34
35
# File 'lib/ayril/selector.rb', line 33

def find_elements(root)
  root.select_by_xpath @xpath
end

#inspectObject



71
72
73
# File 'lib/ayril/selector.rb', line 71

def inspect
  "#<Selector:#{@expr.inspect}>"
end

#match?(element) ⇒ Boolean

Returns:

  • (Boolean)


37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
# File 'lib/ayril/selector.rb', line 37

def match?(element)
  @tokens = []

  e = @expr.dup; le = nil
  while (e != '') and (le != e) and (e =~ /\S/)
    le = e.dup
    Selector::Patterns.each do |pattern|
      if m = e.patch(pattern[:re])
        m = m.to_a unless m.nil?
        if Selector::Assertions.include? name
          @tokens << [pattern[:name], m.clone]
          e.sub! m[0], ''
        else # resort to whole document
          return self.find_elements(element.rootDocument).include? element
        end
      end
    end
  end

  match = true
  @tokens.each do |token|
    name, matches = token[0..1]
    if not Selector::Assertions[name].call self, matches
      match = false
      break
    end
  end
  match
end

#to_sObject



67
68
69
# File 'lib/ayril/selector.rb', line 67

def to_s
  @expr
end