Package lxml :: Module cssselect |
[hide private]
[frames] | no frames]

# Source Code for Module lxml.cssselect

       1  """CSS Selectors based on XPath.
       2
       3  This module supports selecting XML/HTML tags based on CSS selectors.
       4  See the `CSSSelector` class for details.
       5  """
       6
       7  import re
       8  from lxml import etree
       9
      10  __all__ = ['SelectorSyntaxError', 'ExpressionError',
      11             'CSSSelector']
      12
      13  try:
      14      _basestring = basestring
      15  except NameError:
      16      _basestring = str
      17
      18 -class SelectorSyntaxError(SyntaxError):
      19      pass
      20
      21 -class ExpressionError(RuntimeError):
      22      pass
      23
      24 -class CSSSelector(etree.XPath):
      25      """A CSS selector.
      26
      27      Usage::
      28
      29          >>> from lxml import etree, cssselect
      30          >>> select = cssselect.CSSSelector("a tag > child")
      31
      32          >>> root = etree.XML("<a><b><c/><tag><child>TEXT</child></tag></b></a>")
      33          >>> [ el.tag for el in select(root) ]
      34          ['child']
      35
      36      To use CSS namespaces, you need to pass a prefix-to-namespace
      37      mapping as ``namespaces`` keyword argument::
      38
      39          >>> rdfns = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#'
      40          >>> select_ns = cssselect.CSSSelector('root > rdf|Description',
      41          ...                                   namespaces={'rdf': rdfns})
      42
      43          >>> rdf = etree.XML((
      44          ...     '<root xmlns:rdf="%s">'
      45          ...       '<rdf:Description>blah</rdf:Description>'
      46          ...     '</root>') % rdfns)
      47          >>> [(el.tag, el.text) for el in select_ns(rdf)]
      48          [('{http://www.w3.org/1999/02/22-rdf-syntax-ns#}Description', 'blah')]
      49      """
      50 -    def __init__(self, css, namespaces=None):
      51          path = css_to_xpath(css)
      52          etree.XPath.__init__(self, path, namespaces=namespaces)
      53          self.css = css
      54
      55 -    def __repr__(self):
      56          return '<%s %s for %r>' % (
      57              self.__class__.__name__,
      58              hex(abs(id(self)))[2:],
      59              self.css)
      60
      61  ##############################
      62  ## Token objects:
      63
      64  try:
      65      _unicode = unicode
      66      _unichr = unichr
      67  except NameError:
      68      # Python 3
      69      _unicode = str
      70      _unichr = chr
      71
      72 -class _UniToken(_unicode):
      73 -    def __new__(cls, contents, pos):
      74          obj = _unicode.__new__(cls, contents)
      75          obj.pos = pos
      76          return obj
      77
      78 -    def __repr__(self):
      79          return '%s(%s, %r)' % (
      80              self.__class__.__name__,
      81              _unicode.__repr__(self),
      82              self.pos)
      83
      84 -class Symbol(_UniToken):
      85      pass
      86
      87 -class String(_UniToken):
      88      pass
      89
      90 -class Token(_UniToken):
      91      pass
      92
      93  ############################################################
      94  ## Parsing
      95  ############################################################
      96
      97  ##############################
      98  ## Syntax objects:
      99
     100 -class Class(object):
     101      """
     102      Represents selector.class_name
     103      """
     104
     105 -    def __init__(self, selector, class_name):
     106          self.selector = selector
     107          self.class_name = class_name
     108
     109 -    def __repr__(self):
     110          return '%s[%r.%s]' % (
     111              self.__class__.__name__,
     112              self.selector,
     113              self.class_name)
     114
     115 -    def xpath(self):
     116          sel_xpath = self.selector.xpath()
     117          sel_xpath.add_condition(
     118              "contains(concat(' ', normalize-space(@class), ' '), %s)" % xpath_literal(' '+self.class_name+' '))
     119          return sel_xpath
     120
     121 -class Function(object):
     122      """
     123      Represents selector:name(expr)
     124      """
     125
     126      unsupported = [
     127          'target', 'lang', 'enabled', 'disabled',]
     128
     129 -    def __init__(self, selector, type, name, expr):
     130          self.selector = selector
     131          self.type = type
     132          self.name = name
     133          self.expr = expr
     134
     135 -    def __repr__(self):
     136          return '%s[%r%s%s(%r)]' % (
     137              self.__class__.__name__,
     138              self.selector,
     139              self.type, self.name, self.expr)
     140
     141 -    def xpath(self):
     142          sel_path = self.selector.xpath()
     143          if self.name in self.unsupported:
     144              raise ExpressionError(
     145                  "The pseudo-class %r is not supported" % self.name)
     146          method = '_xpath_' + self.name.replace('-', '_')
     147          if not hasattr(self, method):
     148              raise ExpressionError(
     149                  "The pseudo-class %r is unknown" % self.name)
     150          method = getattr(self, method)
     151          return method(sel_path, self.expr)
     152
     153 -    def _xpath_nth_child(self, xpath, expr, last=False,
     154                           add_name_test=True):
     155          a, b = parse_series(expr)
     156          if not a and not b and not last:
     157              # a=0 means nothing is returned...
     158              xpath.add_condition('false() and position() = 0')
     159              return xpath
     160          if add_name_test:
     161              xpath.add_name_test()
     162          xpath.add_star_prefix()
     163          if a == 0:
     164              if last:
     165                  b = 'last() - %s' % b
     166              xpath.add_condition('position() = %s' % b)
     167              return xpath
     168          if last:
     169              # FIXME: I'm not sure if this is right
     170              a = -a
     171              b = -b
     172          if b > 0:
     173              b_neg = str(-b)
     174          else:
     175              b_neg = '+%s' % (-b)
     176          if a != 1:
     177              expr = ['(position() %s) mod %s = 0' % (b_neg, a)]
     178          else:
     179              expr = []
     180          if b >= 0:
     181              expr.append('position() >= %s' % b)
     182          elif b < 0 and last:
     183              expr.append('position() < (last() %s)' % b)
     184          expr = ' and '.join(expr)
     185          if expr:
     186              xpath.add_condition(expr)
     187          return xpath
     188          # FIXME: handle an+b, odd, even
     189          # an+b means every-a, plus b, e.g., 2n+1 means odd
     190          # 0n+b means b
     191          # n+0 means a=1, i.e., all elements
     192          # an means every a elements, i.e., 2n means even
     193          # -n means -1n
     194          # -1n+6 means elements 6 and previous
     195
     196 -    def _xpath_nth_last_child(self, xpath, expr):
     197          return self._xpath_nth_child(xpath, expr, last=True)
     198
     199 -    def _xpath_nth_of_type(self, xpath, expr):
     200          if xpath.element == '*':
     201              raise NotImplementedError(
     202                  "*:nth-of-type() is not implemented")
     203          return self._xpath_nth_child(xpath, expr, add_name_test=False)
     204
     205 -    def _xpath_nth_last_of_type(self, xpath, expr):
     206          return self._xpath_nth_child(xpath, expr, last=True, add_name_test=False)
     207
     208 -    def _xpath_contains(self, xpath, expr):
     209          # text content, minus tags, must contain expr
     210          if isinstance(expr, Element):
     211              expr = expr._format_element()
     212          xpath.add_condition('contains(css:lower-case(string(.)), %s)'
     213                              % xpath_literal(expr.lower()))
     214          # FIXME: Currently case insensitive matching doesn't seem to be happening
     215          return xpath
     216
     217 -    def _xpath_not(self, xpath, expr):
     218          # everything for which not expr applies
     219          expr = expr.xpath()
     220          cond = expr.condition
     221          # FIXME: should I do something about element_path?
     222          xpath.add_condition('not(%s)' % cond)
     223          return xpath
     224
     225 -def _make_lower_case(context, s):
     226      return s.lower()
     227
     228  ns = etree.FunctionNamespace('http://codespeak.net/lxml/css/')
     229  ns.prefix = 'css'
     230  ns['lower-case'] = _make_lower_case
     231
     232 -class Pseudo(object):
     233      """
     234      Represents selector:ident
     235      """
     236
     237      unsupported = ['indeterminate', 'first-line', 'first-letter',
     238                     'selection', 'before', 'after', 'link', 'visited',
     239                     'active', 'focus', 'hover']
     240
     241 -    def __init__(self, element, type, ident):
     242          self.element = element
     243          assert type in (':', '::')
     244          self.type = type
     245          self.ident = ident
     246
     247 -    def __repr__(self):
     248          return '%s[%r%s%s]' % (
     249              self.__class__.__name__,
     250              self.element,
     251              self.type, self.ident)
     252
     253 -    def xpath(self):
     254          el_xpath = self.element.xpath()
     255          if self.ident in self.unsupported:
     256              raise ExpressionError(
     257                  "The pseudo-class %r is unsupported" % self.ident)
     258          method = '_xpath_' + self.ident.replace('-', '_')
     259          if not hasattr(self, method):
     260              raise ExpressionError(
     261                  "The pseudo-class %r is unknown" % self.ident)
     262          method = getattr(self, method)
     263          el_xpath = method(el_xpath)
     264          return el_xpath
     265
     266 -    def _xpath_checked(self, xpath):
     267          # FIXME: is this really all the elements?
     268          xpath.add_condition("(@selected or @checked) and (name(.) = 'input' or name(.) = 'option')")
     269          return xpath
     270
     271 -    def _xpath_root(self, xpath):
     272          # if this element is the root element
     273          raise NotImplementedError
     274
     275 -    def _xpath_first_child(self, xpath):
     276          xpath.add_star_prefix()
     277          xpath.add_name_test()
     278          xpath.add_condition('position() = 1')
     279          return xpath
     280
     281 -    def _xpath_last_child(self, xpath):
     282          xpath.add_star_prefix()
     283          xpath.add_name_test()
     284          xpath.add_condition('position() = last()')
     285          return xpath
     286
     287 -    def _xpath_first_of_type(self, xpath):
     288          if xpath.element == '*':
     289              raise NotImplementedError(
     290                  "*:first-of-type is not implemented")
     291          xpath.add_star_prefix()
     292          xpath.add_condition('position() = 1')
     293          return xpath
     294
     295 -    def _xpath_last_of_type(self, xpath):
     296          if xpath.element == '*':
     297              raise NotImplementedError(
     298                  "*:last-of-type is not implemented")
     299          xpath.add_star_prefix()
     300          xpath.add_condition('position() = last()')
     301          return xpath
     302
     303 -    def _xpath_only_child(self, xpath):
     304          xpath.add_name_test()
     305          xpath.add_star_prefix()
     306          xpath.add_condition('last() = 1')
     307          return xpath
     308
     309 -    def _xpath_only_of_type(self, xpath):
     310          if xpath.element == '*':
     311              raise NotImplementedError(
     312                  "*:only-of-type is not implemented")
     313          xpath.add_condition('last() = 1')
     314          return xpath
     315
     316 -    def _xpath_empty(self, xpath):
     317          xpath.add_condition("not(*) and not(normalize-space())")
     318          return xpath
     319
     320 -class Attrib(object):
     321      """
     322      Represents selector[namespace|attrib operator value]
     323      """
     324
     325 -    def __init__(self, selector, namespace, attrib, operator, value):
     326          self.selector = selector
     327          self.namespace = namespace
     328          self.attrib = attrib
     329          self.operator = operator
     330          self.value = value
     331
     332 -    def __repr__(self):
     333          if self.operator == 'exists':
     334              return '%s[%r[%s]]' % (
     335                  self.__class__.__name__,
     336                  self.selector,
     337                  self._format_attrib())
     338          else:
     339              return '%s[%r[%s %s %r]]' % (
     340                  self.__class__.__name__,
     341                  self.selector,
     342                  self._format_attrib(),
     343                  self.operator,
     344                  self.value)
     345
     346 -    def _format_attrib(self):
     347          if self.namespace == '*':
     348              return self.attrib
     349          else:
     350              return '%s|%s' % (self.namespace, self.attrib)
     351
     352 -    def _xpath_attrib(self):
     353          # FIXME: if attrib is *?
     354          if self.namespace == '*':
     355              return '@' + self.attrib
     356          else:
     357              return '@%s:%s' % (self.namespace, self.attrib)
     358
     359 -    def xpath(self):
     360          path = self.selector.xpath()
     361          attrib = self._xpath_attrib()
     362          value = self.value
     363          if self.operator == 'exists':
     364              assert not value
     365              path.add_condition(attrib)
     366          elif self.operator == '=':
     367              path.add_condition('%s = %s' % (attrib,
     368                                              xpath_literal(value)))
     369          elif self.operator == '!=':
     370              # FIXME: this seems like a weird hack...
     371              if value:
     372                  path.add_condition('not(%s) or %s != %s'
     373                                     % (attrib, attrib, xpath_literal(value)))
     374              else:
     375                  path.add_condition('%s != %s'
     376                                     % (attrib, xpath_literal(value)))
     377              #path.add_condition('%s != %s' % (attrib, xpath_literal(value)))
     378          elif self.operator == '~=':
     379              path.add_condition("contains(concat(' ', normalize-space(%s), ' '), %s)" % (attrib, xpath_literal(' '+value+' ')))
     380          elif self.operator == '|=':
     381              # Weird, but true...
     382              path.add_condition('%s = %s or starts-with(%s, %s)' % (
     383                  attrib, xpath_literal(value),
     384                  attrib, xpath_literal(value + '-')))
     385          elif self.operator == '^=':
     386              path.add_condition('starts-with(%s, %s)' % (
     387                  attrib, xpath_literal(value)))
     388          elif self.operator == '$=':
     389              # Oddly there is a starts-with in XPath 1.0, but not ends-with
     390              path.add_condition('substring(%s, string-length(%s)-%s) = %s'
     391                                 % (attrib, attrib, len(value)-1, xpath_literal(value)))
     392          elif self.operator == '*=':
     393              # FIXME: case sensitive?
     394              path.add_condition('contains(%s, %s)' % (
     395                  attrib, xpath_literal(value)))
     396          else:
     397              assert 0, ("Unknown operator: %r" % self.operator)
     398          return path
     399
     400 -class Element(object):
     401      """
     402      Represents namespace|element
     403      """
     404
     405 -    def __init__(self, namespace, element):
     406          self.namespace = namespace
     407          self.element = element
     408
     409 -    def __repr__(self):
     410          return '%s[%s]' % (
     411              self.__class__.__name__,
     412              self._format_element())
     413
     414 -    def _format_element(self):
     415          if self.namespace == '*':
     416              return self.element
     417          else:
     418              return '%s|%s' % (self.namespace, self.element)
     419
     420 -    def xpath(self):
     421          if self.namespace == '*':
     422              el = self.element.lower()
     423          else:
     424              # FIXME: Should we lowercase here?
     425              el = '%s:%s' % (self.namespace, self.element)
     426          return XPathExpr(element=el)
     427
     428 -class Hash(object):
     429      """
     430      Represents selector#id
     431      """
     432
     433 -    def __init__(self, selector, id):
     434          self.selector = selector
     435          self.id = id
     436
     437 -    def __repr__(self):
     438          return '%s[%r#%s]' % (
     439              self.__class__.__name__,
     440              self.selector, self.id)
     441
     442 -    def xpath(self):
     443          path = self.selector.xpath()
     444          path.add_condition('@id = %s' % xpath_literal(self.id))
     445          return path
     446
     447 -class Or(object):
     448
     449 -    def __init__(self, items):
     450          self.items = items
     451 -    def __repr__(self):
     452          return '%s(%r)' % (
     453              self.__class__.__name__,
     454              self.items)
     455
     456 -    def xpath(self):
     457          paths = [item.xpath() for item in self.items]
     458          return XPathExprOr(paths)
     459
     460 -class CombinedSelector(object):
     461
     462      _method_mapping = {
     463          ' ': 'descendant',
     464          '>': 'child',
     465          '+': 'direct_adjacent',
     466          '~': 'indirect_adjacent',
     467          }
     468
     469 -    def __init__(self, selector, combinator, subselector):
     470          assert selector is not None
     471          self.selector = selector
     472          self.combinator = combinator
     473          self.subselector = subselector
     474
     475 -    def __repr__(self):
     476          if self.combinator == ' ':
     477              comb = '<followed>'
     478          else:
     479              comb = self.combinator
     480          return '%s[%r %s %r]' % (
     481              self.__class__.__name__,
     482              self.selector,
     483              comb,
     484              self.subselector)
     485
     486 -    def xpath(self):
     487          if self.combinator not in self._method_mapping:
     488              raise ExpressionError(
     489                  "Unknown combinator: %r" % self.combinator)
     490          method = '_xpath_' + self._method_mapping[self.combinator]
     491          method = getattr(self, method)
     492          path = self.selector.xpath()
     493          return method(path, self.subselector)
     494
     495 -    def _xpath_descendant(self, xpath, sub):
     496          # when sub is a descendant in any way of xpath
     497          xpath.join('/descendant::', sub.xpath())
     498          return xpath
     499
     500 -    def _xpath_child(self, xpath, sub):
     501          # when sub is an immediate child of xpath
     502          xpath.join('/', sub.xpath())
     503          return xpath
     504
     505 -    def _xpath_direct_adjacent(self, xpath, sub):
     506          # when sub immediately follows xpath
     507          xpath.join('/following-sibling::', sub.xpath())
     508          xpath.add_name_test()
     509          xpath.add_condition('position() = 1')
     510          return xpath
     511
     512 -    def _xpath_indirect_adjacent(self, xpath, sub):
     513          # when sub comes somewhere after xpath as a sibling
     514          xpath.join('/following-sibling::', sub.xpath())
     515          return xpath
     516
     517  ##############################
     518  ## XPathExpr objects:
     519
     520  _el_re = re.compile(r'^\w+\s*$', re.UNICODE)
     521  _id_re = re.compile(r'^(\w*)#(\w+)\s*$', re.UNICODE)
     522  _class_re = re.compile(r'^(\w*)\.(\w+)\s*$', re.UNICODE)
     523
     524 -def css_to_xpath(css_expr, prefix='descendant-or-self::'):
     525      if isinstance(css_expr, _basestring):
     526          match = _el_re.search(css_expr)
     527          if match is not None:
     528              return '%s%s' % (prefix, match.group(0).strip())
     529          match = _id_re.search(css_expr)
     530          if match is not None:
     531              return "%s%s[@id = '%s']" % (
     532                  prefix, match.group(1) or '*', match.group(2))
     533          match = _class_re.search(css_expr)
     534          if match is not None:
     535              return "%s%s[contains(concat(' ', normalize-space(@class), ' '), ' %s ')]" % (
     536                  prefix, match.group(1) or '*', match.group(2))
     537          css_expr = parse(css_expr)
     538      expr = css_expr.xpath()
     539      assert expr is not None, (
     540          "Got None for xpath expression from %s" % repr(css_expr))
     541      if prefix:
     542          expr.add_prefix(prefix)
     543      return _unicode(expr)
     544
     545 -class XPathExpr(object):
     546
     547 -    def __init__(self, prefix=None, path=None, element='*', condition=None,
     548                   star_prefix=False):
     549          self.prefix = prefix
     550          self.path = path
     551          self.element = element
     552          self.condition = condition
     553          self.star_prefix = star_prefix
     554
     555 -    def __str__(self):
     556          path = ''
     557          if self.prefix is not None:
     558              path += _unicode(self.prefix)
     559          if self.path is not None:
     560              path += _unicode(self.path)
     561          path += _unicode(self.element)
     562          if self.condition:
     563              path += '[%s]' % self.condition
     564          return path
     565
     566 -    def __repr__(self):
     567          return '%s[%s]' % (
     568              self.__class__.__name__, self)
     569
     570 -    def add_condition(self, condition):
     571          if self.condition:
     572              self.condition = '%s and (%s)' % (self.condition, condition)
     573          else:
     574              self.condition = condition
     575
     576 -    def add_path(self, part):
     577          if self.path is None:
     578              self.path = self.element
     579          else:
     580              self.path += self.element
     581          self.element = part
     582
     583 -    def add_prefix(self, prefix):
     584          if self.prefix:
     585              self.prefix = prefix + self.prefix
     586          else:
     587              self.prefix = prefix
     588
     589 -    def add_name_test(self):
     590          if self.element == '*':
     591              # We weren't doing a test anyway
     592              return
     593          self.add_condition("name() = %s" % xpath_literal(self.element))
     594          self.element = '*'
     595
     596 -    def add_star_prefix(self):
     597          """
     598          Adds a /* prefix if there is no prefix.  This is when you need
     599          to keep context's constrained to a single parent.
     600          """
     601          if self.path:
     602              self.path += '*/'
     603          else:
     604              self.path = '*/'
     605          self.star_prefix = True
     606
     607 -    def join(self, combiner, other):
     608          prefix = _unicode(self)
     609          prefix += combiner
     610          path = (other.prefix or '') + (other.path or '')
     611          # We don't need a star prefix if we are joining to this other
     612          # prefix; so we'll get rid of it
     613          if other.star_prefix and path == '*/':
     614              path = ''
     615          self.prefix = prefix
     616          self.path = path
     617          self.element = other.element
     618          self.condition = other.condition
     619
     620 -class XPathExprOr(XPathExpr):
     621      """
     622      Represents |'d expressions.  Note that unfortunately it isn't
     623      the union, it's the sum, so duplicate elements will appear.
     624      """
     625
     626 -    def __init__(self, items, prefix=None):
     627          for item in items:
     628              assert item is not None
     629          self.items = items
     630          self.prefix = prefix
     631
     632 -    def __str__(self):
     633          prefix = self.prefix or ''
     634          return ' | '.join(["%s%s" % (prefix,i) for i in self.items])
     635
     636  split_at_single_quotes = re.compile("('+)").split
     637
     638 -def xpath_literal(s):
     639      if isinstance(s, Element):
     640          # This is probably a symbol that looks like an expression...
     641          s = s._format_element()
     642      else:
     643          s = _unicode(s)
     644      if "'" not in s:
     645          s = "'%s'" % s
     646      elif '"' not in s:
     647          s = '"%s"' % s
     648      else:
     649          s = "concat(%s)" % ','.join([
     650              (("'" in part) and '"%s"' or "'%s'") % part
     651              for part in split_at_single_quotes(s) if part
     652              ])
     653      return s
     654
     655  ##############################
     656  ## Parsing functions
     657
     658 -def parse(string):
     659      stream = TokenStream(tokenize(string))
     660      stream.source = string
     661      try:
     662          return parse_selector_group(stream)
     663      except SelectorSyntaxError:
     664          import sys
     665          e = sys.exc_info()[1]
     666          message = "%s at %s -> %r" % (
     667              e, stream.used, stream.peek())
     668          e.msg = message
     669          if sys.version_info < (2,6):
     670              e.message = message
     671          e.args = tuple([message])
     672          raise
     673
     674 -def parse_selector_group(stream):
     675      result = []
     676      while 1:
     677          result.append(parse_selector(stream))
     678          if stream.peek() == ',':
     679              stream.next()
     680          else:
     681              break
     682      if len(result) == 1:
     683          return result[0]
     684      else:
     685          return Or(result)
     686
     687 -def parse_selector(stream):
     688      result = parse_simple_selector(stream)
     689      while 1:
     690          peek = stream.peek()
     691          if peek == ',' or peek is None:
     692              return result
     693          elif peek in ('+', '>', '~'):
     694              # A combinator
     695              combinator = stream.next()
     696          else:
     697              combinator = ' '
     698          consumed = len(stream.used)
     699          next_selector = parse_simple_selector(stream)
     700          if consumed == len(stream.used):
     701              raise SelectorSyntaxError(
     702                  "Expected selector, got '%s'" % stream.peek())
     703          result = CombinedSelector(result, combinator, next_selector)
     704      return result
     705
     706 -def parse_simple_selector(stream):
     707      peek = stream.peek()
     708      if peek != '*' and not isinstance(peek, Symbol):
     709          element = namespace = '*'
     710      else:
     711          next = stream.next()
     712          if next != '*' and not isinstance(next, Symbol):
     713              raise SelectorSyntaxError(
     714                  "Expected symbol, got '%s'" % next)
     715          if stream.peek() == '|':
     716              namespace = next
     717              stream.next()
     718              element = stream.next()
     719              if element != '*' and not isinstance(next, Symbol):
     720                  raise SelectorSyntaxError(
     721                      "Expected symbol, got '%s'" % next)
     722          else:
     723              namespace = '*'
     724              element = next
     725      result = Element(namespace, element)
     726      has_hash = False
     727      while 1:
     728          peek = stream.peek()
     729          if peek == '#':
     730              if has_hash:
     731                  # You can't have two hashes
     732                  # (FIXME: is there some more general rule I'm missing?)
     733                  break
     734              stream.next()
     735              result = Hash(result, stream.next())
     736              has_hash = True
     737              continue
     738          elif peek == '.':
     739              stream.next()
     740              result = Class(result, stream.next())
     741              continue
     742          elif peek == '[':
     743              stream.next()
     744              result = parse_attrib(result, stream)
     745              next = stream.next()
     746              if not next == ']':
     747                  raise SelectorSyntaxError(
     748                      "] expected, got '%s'" % next)
     749              continue
     750          elif peek == ':' or peek == '::':
     751              type = stream.next()
     752              ident = stream.next()
     753              if not isinstance(ident, Symbol):
     754                  raise SelectorSyntaxError(
     755                      "Expected symbol, got '%s'" % ident)
     756              if stream.peek() == '(':
     757                  stream.next()
     758                  peek = stream.peek()
     759                  if isinstance(peek, String):
     760                      selector = stream.next()
     761                  elif isinstance(peek, Symbol) and is_int(peek):
     762                      selector = int(stream.next())
     763                  else:
     764                      # FIXME: parse_simple_selector, or selector, or...?
     765                      selector = parse_simple_selector(stream)
     766                  next = stream.next()
     767                  if not next == ')':
     768                      raise SelectorSyntaxError(
     769                          "Expected ')', got '%s' and '%s'"
     770                          % (next, selector))
     771                  result = Function(result, type, ident, selector)
     772              else:
     773                  result = Pseudo(result, type, ident)
     774              continue
     775          else:
     776              if peek == ' ':
     777                  stream.next()
     778              break
     779          # FIXME: not sure what "negation" is
     780      return result
     781
     782 -def is_int(v):
     783      try:
     784          int(v)
     785      except ValueError:
     786          return False
     787      else:
     788          return True
     789
     790 -def parse_attrib(selector, stream):
     791      attrib = stream.next()
     792      if stream.peek() == '|':
     793          namespace = attrib
     794          stream.next()
     795          attrib = stream.next()
     796      else:
     797          namespace = '*'
     798      if stream.peek() == ']':
     799          return Attrib(selector, namespace, attrib, 'exists', None)
     800      op = stream.next()
     801      if not op in ('^=', '$=', '*=', '=', '~=', '|=', '!='):
     802          raise SelectorSyntaxError(
     803              "Operator expected, got '%s'" % op)
     804      value = stream.next()
     805      if not isinstance(value, (Symbol, String)):
     806          raise SelectorSyntaxError(
     807              "Expected string or symbol, got '%s'" % value)
     808      return Attrib(selector, namespace, attrib, op, value)
     809
     810 -def parse_series(s):
     811      """
     812      Parses things like '1n+2', or 'an+b' generally, returning (a, b)
     813      """
     814      if isinstance(s, Element):
     815          s = s._format_element()
     816      if not s or s == '*':
     817          # Happens when there's nothing, which the CSS parser thinks of as *
     818          return (0, 0)
     819      if isinstance(s, int):
     820          # Happens when you just get a number
     821          return (0, s)
     822      if s == 'odd':
     823          return (2, 1)
     824      elif s == 'even':
     825          return (2, 0)
     826      elif s == 'n':
     827          return (1, 0)
     828      if 'n' not in s:
     829          # Just a b
     830          return (0, int(s))
     831      a, b = s.split('n', 1)
     832      if not a:
     833          a = 1
     834      elif a == '-' or a == '+':
     835          a = int(a+'1')
     836      else:
     837          a = int(a)
     838      if not b:
     839          b = 0
     840      elif b == '-' or b == '+':
     841          b = int(b+'1')
     842      else:
     843          b = int(b)
     844      return (a, b)
     845
     846
     847  ############################################################
     848  ## Tokenizing
     849  ############################################################
     850
     851  _match_whitespace = re.compile(r'\s+', re.UNICODE).match
     852
     853  _replace_comments = re.compile(r'/\*.*?\*/', re.DOTALL).sub
     854
     855  _match_count_number = re.compile(r'[+-]?\d*n(?:[+-]\d+)?').match
     856
     857 -def tokenize(s):
     858      pos = 0
     859      s = _replace_comments('', s)
     860      while 1:
     861          match = _match_whitespace(s, pos=pos)
     862          if match:
     863              preceding_whitespace_pos = pos
     864              pos = match.end()
     865          else:
     866              preceding_whitespace_pos = 0
     867          if pos >= len(s):
     868              return
     869          match = _match_count_number(s, pos=pos)
     870          if match and match.group() != 'n':
     871              sym = s[pos:match.end()]
     872              yield Symbol(sym, pos)
     873              pos = match.end()
     874              continue
     875          c = s[pos]
     876          c2 = s[pos:pos+2]
     877          if c2 in ('~=', '|=', '^=', '$=', '*=', '::', '!='):
     878              yield Token(c2, pos)
     879              pos += 2
     880              continue
     881          if c in '>+~,.*=[]()|:#':
     882              if c in '.#[' and preceding_whitespace_pos > 0:
     883                  yield Token(' ', preceding_whitespace_pos)
     884              yield Token(c, pos)
     885              pos += 1
     886              continue
     887          if c == '"' or c == "'":
     888              # Quoted string
     889              old_pos = pos
     890              sym, pos = tokenize_escaped_string(s, pos)
     891              yield String(sym, old_pos)
     892              continue
     893          old_pos = pos
     894          sym, pos = tokenize_symbol(s, pos)
     895          yield Symbol(sym, old_pos)
     896          continue
     897
     898  split_at_string_escapes = re.compile(r'(\\(?:%s))'
     899                                       % '|'.join(['[A-Fa-f0-9]{1,6}(?:\r\n|\s)?',
     900                                                   '[^A-Fa-f0-9]'])).split
     901
     902 -def unescape_string_literal(literal):
     903      substrings = []
     904      for substring in split_at_string_escapes(literal):
     905          if not substring:
     906              continue
     907          elif '\\' in substring:
     908              if substring[0] == '\\' and len(substring) > 1:
     909                  substring = substring[1:]
     910                  if substring[0] in '0123456789ABCDEFabcdef':
     911                      # int() correctly ignores the potentially trailing whitespace
     912                      substring = _unichr(int(substring, 16))
     913              else:
     914                  raise SelectorSyntaxError(
     915                      "Invalid escape sequence %r in string %r"
     916                      % (substring.split('\\')[1], literal))
     917          substrings.append(substring)
     918      return ''.join(substrings)
     919
     920 -def tokenize_escaped_string(s, pos):
     921      quote = s[pos]
     922      assert quote in ('"', "'")
     923      pos = pos+1
     924      start = pos
     925      while 1:
     926          next = s.find(quote, pos)
     927          if next == -1:
     928              raise SelectorSyntaxError(
     929                  "Expected closing %s for string in: %r"
     930                  % (quote, s[start:]))
     931          result = s[start:next]
     932          if result.endswith('\\'):
     933              # next quote character is escaped
     934              pos = next+1
     935              continue
     936          if '\\' in result:
     937              result = unescape_string_literal(result)
     938          return result, next+1
     939
     940  _illegal_symbol = re.compile(r'[^\w\\-]', re.UNICODE)
     941
     942 -def tokenize_symbol(s, pos):
     943      start = pos
     944      match = _illegal_symbol.search(s, pos=pos)
     945      if not match:
     946          # Goes to end of s
     947          return s[start:], len(s)
     948      if match.start() == pos:
     949          assert 0, (
     950              "Unexpected symbol: %r at %s" % (s[pos], pos))
     951      if not match:
     952          result = s[start:]
     953          pos = len(s)
     954      else:
     955          result = s[start:match.start()]
     956          pos = match.start()
     957      try:
     958          result = result.encode('ASCII', 'backslashreplace').decode('unicode_escape')
     959      except UnicodeDecodeError:
     960          import sys
     961          e = sys.exc_info()[1]
     962          raise SelectorSyntaxError(
     963              "Bad symbol %r: %s" % (result, e))
     964      return result, pos
     965
     966 -class TokenStream(object):
     967
     968 -    def __init__(self, tokens, source=None):
     969          self.used = []
     970          self.tokens = iter(tokens)
     971          self.source = source
     972          self.peeked = None
     973          self._peeking = False
     974          try:
     975              self.next_token = self.tokens.next
     976          except AttributeError:
     977              # Python 3
     978              self.next_token = self.tokens.__next__
     979
     980 -    def next(self):
     981          if self._peeking:
     982              self._peeking = False
     983              self.used.append(self.peeked)
     984              return self.peeked
     985          else:
     986              try:
     987                  next = self.next_token()
     988                  self.used.append(next)
     989                  return next
     990              except StopIteration:
     991                  return None
     992
     993 -    def __iter__(self):
     994          return iter(self.next, None)
     995
     996 -    def peek(self):
     997          if not self._peeking:
     998              try:
     999                  self.peeked = self.next_token()
    1000              except StopIteration:
    1001                  return None
    1002              self._peeking = True
    1003          return self.peeked
    1004