Package cssutils :: Package css :: Module selector |
[hide private]
[frames] | no frames]

# Source Code for Module cssutils.css.selector

      1  """Selector is a single Selector of a CSSStyleRule SelectorList.
      2
      3  Partly implements
      4      http://www.w3.org/TR/css3-selectors/
      5
      6  TODO
      7      - .contains(selector)
      8      - .isSubselector(selector)
      9
     10      - ??? CSS2 gives a special meaning to the comma (,) in selectors.
     11          However, since it is not known if the comma may acquire other
     12          meanings in future versions of CSS, the whole statement should be
     13          ignored if there is an error anywhere in the selector, even though
     14          the rest of the selector may look reasonable in CSS2.
     15
     16          Illegal example(s):
     17
     18          For example, since the "&" is not a valid token in a CSS2 selector,
     19          a CSS2 user agent must ignore the whole second line, and not set
     20          the color of H3 to red:
     21  """
     22  __all__ = ['Selector']
     23  __docformat__ = 'restructuredtext'
     24  __author__ = '$LastChangedBy: cthedot $'
     25  __date__ = '$LastChangedDate: 2007-10-18 19:38:00 +0200 (Do, 18 Okt 2007) $'
     26  __version__ = '$LastChangedRevision: 499 $'
     27
     28  import xml.dom
     29
     30  import cssutils
     31
     32 -class Selector(cssutils.util.Base):
     33      """
     34      (cssutils) a single selector in a SelectorList of a CSSStyleRule
     35
     36      Properties
     37      ==========
     38      selectorText
     39          textual representation of this Selector
     40      prefixes
     41          a set which prefixes have been used in this selector
     42      seq
     43          sequence of Selector parts including comments
     44
     45      Format
     46      ======
     47      ::
     48
     49          # implemented in SelectorList
     50          selectors_group
     51            : selector [ COMMA S* selector ]*
     52            ;
     53
     54          selector
     55            : simple_selector_sequence [ combinator simple_selector_sequence ]*
     56            ;
     57
     58          combinator
     59            /* combinators can be surrounded by white space */
     60            : PLUS S* | GREATER S* | TILDE S* | S+
     61            ;
     62
     63          simple_selector_sequence
     64            : [ type_selector | universal ]
     65              [ HASH | class | attrib | pseudo | negation ]*
     66            | [ HASH | class | attrib | pseudo | negation ]+
     67            ;
     68
     69          type_selector
     70            : [ namespace_prefix ]? element_name
     71            ;
     72
     73          namespace_prefix
     74            : [ IDENT | '*' ]? '|'
     75            ;
     76
     77          element_name
     78            : IDENT
     79            ;
     80
     81          universal
     82            : [ namespace_prefix ]? '*'
     83            ;
     84
     85          class
     86            : '.' IDENT
     87            ;
     88
     89          attrib
     90            : '[' S* [ namespace_prefix ]? IDENT S*
     91                  [ [ PREFIXMATCH |
     92                      SUFFIXMATCH |
     93                      SUBSTRINGMATCH |
     94                      '=' |
     95                      INCLUDES |
     96                      DASHMATCH ] S* [ IDENT | STRING ] S*
     97                  ]? ']'
     98            ;
     99
    100          pseudo
    101            /* '::' starts a pseudo-element, ':' a pseudo-class */
    102            /* Exceptions: :first-line, :first-letter, :before and :after. */
    103            /* Note that pseudo-elements are restricted to one per selector and */
    104            /* occur only in the last simple_selector_sequence. */
    105            : ':' ':'? [ IDENT | functional_pseudo ]
    106            ;
    107
    108          functional_pseudo
    109            : FUNCTION S* expression ')'
    110            ;
    111
    112          expression
    113            /* In CSS3, the expressions are identifiers, strings, */
    114            /* or of the form "an+b" */
    115            : [ [ PLUS | '-' | DIMENSION | NUMBER | STRING | IDENT ] S* ]+
    116            ;
    117
    118          negation
    119            : NOT S* negation_arg S* ')'
    120            ;
    121
    122          negation_arg
    123            : type_selector | universal | HASH | class | attrib | pseudo
    124            ;
    125
    126      """
    127 -    def __init__(self, selectorText=None, readonly=False):
    128          """
    129          selectorText
    130              initial value of this selector
    131          readonly
    132              default to False
    133          """
    134          super(Selector, self).__init__()
    135
    136          self.valid = False
    137          self.seq = self._newseq()
    138          self.prefixes = set()
    139          if selectorText:
    140              self.selectorText = selectorText
    141          self._readonly = readonly
    142
    143
    144 -    def _getSelectorText(self):
    145          """
    146          returns serialized format
    147          """
    148          return cssutils.ser.do_css_Selector(self)
    149
    150 -    def _setSelectorText(self, selectorText):
    151          """
    152          sets this selectorText
    153
    154          TODO:
    155          raises xml.dom.Exception
    156          """
    157          self._checkReadonly()
    158          tokenizer = self._tokenize2(selectorText)
    159          if not tokenizer:
    160              self._log.error(u'Selector: No selectorText given.')
    161          else:
    162              # prepare tokenlist:
    163              #     "*" -> type "universal"
    164              #     "*"|IDENT + "|" -> combined to "namespace_prefix"
    165              #     "|" -> type "namespace_prefix"
    166              #     "." + IDENT -> combined to "class"
    167              #     ":" + IDENT, ":" + FUNCTION -> pseudo-class
    168              #     FUNCTION "not(" -> negation
    169              #     "::" + IDENT, "::" + FUNCTION -> pseudo-element
    170              tokens = []
    171              for t in tokenizer:
    172                  typ, val, lin, col = t
    173                  if val == u':' and tokens and\
    174                     self._tokenvalue(tokens[-1]) == ':':
    175                      # combine ":" and ":"
    176                      tokens[-1] = (typ, u'::', lin, col)
    177
    178                  elif typ == 'IDENT' and tokens\
    179                       and self._tokenvalue(tokens[-1]) == u'.':
    180                      # class: combine to .IDENT
    181                      tokens[-1] = ('class', u'.'+val, lin, col)
    182                  elif typ == 'IDENT' and tokens and \
    183                       self._tokenvalue(tokens[-1]).startswith(u':') and\
    184                       not self._tokenvalue(tokens[-1]).endswith(u'('):
    185                      # pseudo-X: combine to :IDENT or ::IDENT but not ":a(" + "b"
    186                      if self._tokenvalue(tokens[-1]).startswith(u'::'):
    187                          t = 'pseudo-element'
    188                      else:
    189                          t = 'pseudo-class'
    190                      tokens[-1] = (t, self._tokenvalue(tokens[-1])+val, lin, col)
    191
    192                  elif typ == 'FUNCTION' and val == u'not(':
    193                      tokens.append(('negation', val, lin, col))
    194                  elif typ == 'FUNCTION' and tokens\
    195                       and self._tokenvalue(tokens[-1]).startswith(u':'):
    196                      # pseudo-X: combine to :FUNCTION( or ::FUNCTION(
    197                      if self._tokenvalue(tokens[-1]).startswith(u'::'):
    198                          t = 'pseudo-element'
    199                      else:
    200                          t = 'pseudo-class'
    201                      tokens[-1] = (t, self._tokenvalue(tokens[-1])+val, lin, col)
    202
    203                  elif val == u'*' and tokens and\
    204                       self._type(tokens[-1]) == 'namespace_prefix' and\
    205                       self._tokenvalue(tokens[-1]).endswith(u'|'):
    206                      # combine prefix|*
    207                      tokens[-1] = ('universal', self._tokenvalue(tokens[-1])+val,
    208                                    lin, col)
    209                  elif val == u'*':
    210                      # universal: "*"
    211                      tokens.append(('universal', val, lin, col))
    212
    213                  elif val == u'|' and tokens and\
    214                       self._type(tokens[-1]) in ('IDENT', 'universal') and\
    215                       self._tokenvalue(tokens[-1]).find(u'|') == -1:
    216                      # namespace_prefix: "IDENT|" or "*|"
    217                      tokens[-1] = ('namespace_prefix',
    218                                    self._tokenvalue(tokens[-1])+u'|', lin, col)
    219                  elif val == u'|':
    220                      # namespace_prefix: "|"
    221                      tokens.append(('namespace_prefix', val, lin, col))
    222
    223                  else:
    224                      tokens.append(t)
    225
    226              # TODO: back to generator but not elegant at all!
    227              tokenizer = (t for t in tokens)
    228
    229              # for closures: must be a mutable
    230              new = {'valid': True,
    231                     'context': ['ROOT'], # stack of: 'attrib', 'negation', 'pseudo'
    232                     'prefixes': set()
    233                     }
    234              # used for equality checks and setting of a space combinator
    235              S = u' '
    236
    237              def append(seq, val, typ=None):
    238                  "appends to seq, may be expanded later"
    239                  seq.append(val, typ)
    240
    241              # expected constants
    242              simple_selector_sequence = 'type_selector universal HASH class attrib pseudo negation '
    243              simple_selector_sequence2 = 'HASH class attrib pseudo negation '
    244
    245              element_name = 'element_name'
    246
    247              negation_arg = 'type_selector universal HASH class attrib pseudo'
    248              negationend = ')'
    249
    250              attname = 'prefix attribute'
    251              attname2 = 'attribute'
    252              attcombinator = 'combinator ]' # optional
    253              attvalue = 'value'       # optional
    254              attend = ']'
    255
    256              expressionstart = 'PLUS - DIMENSION NUMBER STRING IDENT'
    257              expression = expressionstart + ' )'
    258
    259              combinator = ' combinator'
    260
    261              def _COMMENT(expected, seq, token, tokenizer=None):
    262                  "special implementation for comment token"
    263                  append(seq, cssutils.css.CSSComment([token]))
    264                  return expected
    265
    266              def _S(expected, seq, token, tokenizer=None):
    267                  # S
    268                  context = new['context'][-1]
    269                  val, typ = self._tokenvalue(token), self._type(token)
    270                  if context.startswith('pseudo-'):
    271                      append(seq, S, 'combinator')
    272                      return expected
    273
    274                  elif context != 'attrib' and 'combinator' in expected:
    275                      append(seq, S, 'combinator')
    276                      return simple_selector_sequence + combinator
    277
    278                  else:
    279                      return expected
    280
    281              def _universal(expected, seq, token, tokenizer=None):
    282                  # *|* or prefix|*
    283                  context = new['context'][-1]
    284                  val, typ = self._tokenvalue(token), self._type(token)
    285                  if 'universal' in expected:
    286                      append(seq, val, typ)
    287
    288                      newprefix = val.split(u'|')[0]
    289                      if newprefix and newprefix != u'*':
    290                          new['prefixes'].add(newprefix)
    291
    292                      if 'negation' == context:
    293                          return negationend
    294                      else:
    295                          return simple_selector_sequence2 + combinator
    296
    297                  else:
    298                      self._log.error(
    299                          u'Selector: Unexpected universal.', token=token)
    300                      return expected
    301
    302              def _namespace_prefix(expected, seq, token, tokenizer=None):
    303                  # prefix| => element_name
    304                  # or prefix| => attribute_name if attrib
    305                  context = new['context'][-1]
    306                  val, typ = self._tokenvalue(token), self._type(token)
    307                  if 'attrib' == context and 'prefix' in expected:
    308                      # [PREFIX|att]
    309                      append(seq, val, typ)
    310
    311                      newprefix = val.split(u'|')[0]
    312                      if newprefix and newprefix != u'*':
    313                          new['prefixes'].add(newprefix)
    314
    315                      return attname2
    316                  elif 'type_selector' in expected:
    317                      # PREFIX|*
    318                      append(seq, val, typ)
    319
    320                      newprefix = val.split(u'|')[0]
    321                      if newprefix and newprefix != u'*':
    322                          new['prefixes'].add(newprefix)
    323
    324                      return element_name
    325                  else:
    326                      self._log.error(
    327                          u'Selector: Unexpected namespace prefix.', token=token)
    328                      return expected
    329
    330              def _pseudo(expected, seq, token, tokenizer=None):
    331                  # pseudo-class or pseudo-element :a ::a :a( ::a(
    332                  """
    333                  /* '::' starts a pseudo-element, ':' a pseudo-class */
    334                  /* Exceptions: :first-line, :first-letter, :before and :after. */
    335                  /* Note that pseudo-elements are restricted to one per selector and */
    336                  /* occur only in the last simple_selector_sequence. */
    337                  """
    338                  context = new['context'][-1]
    339                  val, typ = self._tokenvalue(token, normalize=True), self._type(token)
    340                  if 'pseudo' in expected:
    341                      if val in (':first-line', ':first-letter', ':before', ':after'):
    342                          # always pseudo-element ???
    343                          typ = 'pseudo-element'
    344                      append(seq, val, typ)
    345
    346                      if val.endswith(u'('):
    347                          # function
    348                          new['context'].append(typ) # "pseudo-" "class" or "element"
    349                          return expressionstart
    350                      elif 'negation' == context:
    351                          return negationend
    352                      elif 'pseudo-element' == typ:
    353                          # only one per element, check at ) also!
    354                          return combinator
    355                      else:
    356                          return simple_selector_sequence2 + combinator
    357
    358                  else:
    359                      self._log.error(
    360                          u'Selector: Unexpected start of pseudo.', token=token)
    361                      return expected
    362
    363              def _expression(expected, seq, token, tokenizer=None):
    364                  # [ [ PLUS | '-' | DIMENSION | NUMBER | STRING | IDENT ] S* ]+
    365                  context = new['context'][-1]
    366                  val, typ = self._tokenvalue(token), self._type(token)
    367                  if context.startswith('pseudo-'):
    368                      append(seq, val, typ)
    369                      return expression
    370                  else:
    371                      self._log.error(
    372                          u'Selector: Unexpected %s.' % typ, token=token)
    373                      return expected
    374
    375              def _attcombinator(expected, seq, token, tokenizer=None):
    376                  # context: attrib
    377                  # PREFIXMATCH | SUFFIXMATCH | SUBSTRINGMATCH | INCLUDES |
    378                  # DASHMATCH
    379                  context = new['context'][-1]
    380                  val, typ = self._tokenvalue(token), self._type(token)
    381                  if 'attrib' == context and 'combinator' in expected:
    382                      # combinator in attrib
    383                      append(seq, val)
    384                      return attvalue
    385                  else:
    386                      self._log.error(
    387                          u'Selector: Unexpected %s.' % typ, token=token)
    388                      return expected
    389
    390              def _string(expected, seq, token, tokenizer=None):
    391                  # identifier
    392                  context = new['context'][-1]
    393                  val, typ = self._tokenvalue(token), self._type(token)
    394
    395                  # context: attrib
    396                  if 'attrib' == context and 'value' in expected:
    397                      # attrib: [...=VALUE]
    398                      append(seq, val, typ)
    399                      return attend
    400
    401                  # context: pseudo
    402                  elif context.startswith('pseudo-'):
    403                      # :func(...)
    404                      append(seq, val, typ)
    405                      return expression
    406
    407                  else:
    408                      self._log.error(
    409                          u'Selector: Unexpected STRING.', token=token)
    410                      return expected
    411
    412              def _ident(expected, seq, token, tokenizer=None):
    413                  # identifier
    414                  context = new['context'][-1]
    415                  val, typ = self._tokenvalue(token), self._type(token)
    416
    417                  # context: attrib
    418                  if 'attrib' == context and 'attribute' in expected:
    419                      # attrib: [...|ATT...]
    420                      append(seq, val, typ)
    421                      return attcombinator
    422
    423                  elif 'attrib' == context and 'value' in expected:
    424                      # attrib: [...=VALUE]
    425                      append(seq, val, typ)
    426                      return attend
    427
    428                  # context: negation
    429                  elif 'negation' == context:
    430                      # negation: (prefix|IDENT)
    431                      append(seq, val, typ)
    432                      return negationend
    433
    434                  # context: pseudo
    435                  elif context.startswith('pseudo-'):
    436                      # :func(...)
    437                      append(seq, val, typ)
    438                      return expression
    439
    440                  elif 'type_selector' in expected or element_name == expected:
    441                      # element name after ns or complete type_selector
    442                      append(seq, val, typ)
    443                      return simple_selector_sequence2 + combinator
    444
    445                  else:
    446                      self._log.error(
    447                          u'Selector: Unexpected IDENT.', token=token)
    448                      return expected
    449
    450              def _class(expected, seq, token, tokenizer=None):
    451                  # .IDENT
    452                  context = new['context'][-1]
    453                  val, typ = self._tokenvalue(token), self._type(token)
    454                  if 'class' in expected:
    455                      append(seq, val, typ)
    456
    457                      if 'negation' == context:
    458                          return negationend
    459                      else:
    460                          return simple_selector_sequence2 + combinator
    461
    462                  else:
    463                      self._log.error(
    464                          u'Selector: Unexpected class.', token=token)
    465                      return expected
    466
    467              def _hash(expected, seq, token, tokenizer=None):
    468                  # #IDENT
    469                  context = new['context'][-1]
    470                  val, typ = self._tokenvalue(token), self._type(token)
    471                  if 'HASH' in expected:
    472                      append(seq, val, typ)
    473
    474                      if 'negation' == context:
    475                          return negationend
    476                      else:
    477                          return simple_selector_sequence2 + combinator
    478
    479                  else:
    480                      self._log.error(
    481                          u'Selector: Unexpected HASH.', token=token)
    482                      return expected
    483
    484              def _char(expected, seq, token, tokenizer=None):
    485                  # + > ~ S
    486                  context = new['context'][-1]
    487                  val, typ = self._tokenvalue(token), self._type(token)
    488
    489                  # context: attrib
    490                  if u']' == val and 'attrib' == context and ']' in expected:
    491                      # end of attrib
    492                      append(seq, val)
    493                      context = new['context'].pop() # attrib is done
    494                      context = new['context'][-1]
    495                      if 'negation' == context:
    496                          return negationend
    497                      else:
    498                          return simple_selector_sequence2 + combinator
    499
    500                  elif u'=' == val and 'attrib' == context and 'combinator' in expected:
    501                      # combinator in attrib
    502                      append(seq, val)
    503                      return attvalue
    504
    505                  # context: negation
    506                  elif u')' == val and 'negation' == context and u')' in expected:
    507                      # not(negation_arg)"
    508                      append(seq, val)
    509                      new['context'].pop() # negation is done
    510                      context = new['context'][-1]
    511                      return simple_selector_sequence + combinator
    512
    513                  # context: pseudo (at least one expression)
    514                  elif val in u'+-' and context.startswith('pseudo-'):
    515                      # :func(+ -)"
    516                      append(seq, val)
    517                      return expression
    518
    519                  elif u')' == val and context.startswith('pseudo-') and\
    520                       expression == expected:
    521                      # :func(expression)"
    522                      append(seq, val)
    523                      new['context'].pop() # pseudo is done
    524                      if 'pseudo-element' == context:
    525                          return combinator
    526                      else:
    527                          return simple_selector_sequence + combinator
    528
    529                  # context: ROOT
    530                  elif u'[' == val and 'attrib' in expected:
    531                      # start of [attrib]
    532                      new['context'].append('attrib')
    533                      append(seq, val)
    534                      return attname
    535
    536                  elif val in u'+>~' and 'combinator' in expected:
    537                      # no other combinator except S may be following
    538                      if seq and S == seq[-1]:
    539                          seq[-1] = (val, 'combinator')
    540                      else:
    541                          append(seq, val, 'combinator')
    542                      return simple_selector_sequence
    543
    544                  elif u',' == val:
    545                      # not a selectorlist
    546                      self._log.error(
    547                          u'Selector: Single selector only.',
    548                          error=xml.dom.InvalidModificationErr,
    549                          token=token)
    550
    551                  else:
    552                      self._log.error(
    553                          u'Selector: Unexpected CHAR.', token=token)
    554                      return expected
    555
    556              def _negation(expected, seq, token, tokenizer=None):
    557                  # not(
    558                  val = self._tokenvalue(token, normalize=True)
    559                  if 'negation' in expected:
    560                      new['context'].append('negation')
    561                      append(seq, val, 'negation')
    562                      return negation_arg
    563                  else:
    564                      self._log.error(
    565                          u'Selector: Unexpected negation.', token=token)
    566                      return expected
    567
    568              # expected: only|not or mediatype, mediatype, feature, and
    569              newseq = self._newseq()
    570              valid, expected = self._parse(expected=simple_selector_sequence,
    571                  seq=newseq, tokenizer=tokenizer,
    572                  productions={'CHAR': _char,
    573                               'class': _class,
    574                               'HASH': _hash,
    575                               'STRING': _string,
    576                               'IDENT': _ident,
    577                               'namespace_prefix': _namespace_prefix,
    578                               'negation': _negation,
    579                               'pseudo-class': _pseudo,
    580                               'pseudo-element': _pseudo,
    581                               'universal': _universal,
    582                               # pseudo
    583                               'NUMBER': _expression,
    584                               'DIMENSION': _expression,
    585                               # attribute
    586                               'PREFIXMATCH': _attcombinator,
    587                               'SUFFIXMATCH': _attcombinator,
    588                               'SUBSTRINGMATCH': _attcombinator,
    589                               'DASHMATCH': _attcombinator,
    590                               'INCLUDES': _attcombinator,
    591
    592                               'S': _S,
    593                               'COMMENT': _COMMENT})
    594              valid = valid and new['valid']
    595
    596              # post condition
    597              if len(new['context']) > 1:
    598                  valid = False
    599                  self._log.error(u'Selector: Incomplete selector: %s' %
    600                      self._valuestr(selectorText))
    601
    602              if expected == 'element_name':
    603                  valid = False
    604                  self._log.error(u'Selector: No element name found: %s' %
    605                      self._valuestr(selectorText))
    606
    607              if expected == simple_selector_sequence:
    608                  valid = False
    609                  self._log.error(u'Selector: Cannot end with combinator: %s' %
    610                      self._valuestr(selectorText))
    611
    612              # set
    613              if valid:
    614                  self.valid = True
    615                  self.seq = newseq
    616                  self.prefixes = new['prefixes']
    617
    618      selectorText = property(_getSelectorText, _setSelectorText,
    619          doc="(DOM) The parsable textual representation of the selector.")
    620
    621 -    def __repr__(self):
    622          return "cssutils.css.%s(selectorText=%r)" % (
    623                  self.__class__.__name__, self.selectorText)
    624
    625 -    def __str__(self):
    626          return "<cssutils.css.%s object selectorText=%r at 0x%x>" % (
    627                  self.__class__.__name__, self.selectorText, id(self))
    628