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-11-24 23:33:21 +0100 (Sa, 24 Nov 2007) $'
     26  __version__ = '$LastChangedRevision: 680 $'
     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(' and tokens and \
    193                       u':' == self._tokenvalue(tokens[-1]):
    194                      tokens[-1] = ('negation', u':' + val, lin, tokens[-1][3])
    195                  elif typ == 'FUNCTION' and tokens\
    196                       and self._tokenvalue(tokens[-1]).startswith(u':'):
    197                      # pseudo-X: combine to :FUNCTION( or ::FUNCTION(
    198                      if self._tokenvalue(tokens[-1]).startswith(u'::'):
    199                          t = 'pseudo-element'
    200                      else:
    201                          t = 'pseudo-class'
    202                      tokens[-1] = (t, self._tokenvalue(tokens[-1])+val, lin, col)
    203
    204                  elif val == u'*' and tokens and\
    205                       self._type(tokens[-1]) == 'namespace_prefix' and\
    206                       self._tokenvalue(tokens[-1]).endswith(u'|'):
    207                      # combine prefix|*
    208                      tokens[-1] = ('universal', self._tokenvalue(tokens[-1])+val,
    209                                    lin, col)
    210                  elif val == u'*':
    211                      # universal: "*"
    212                      tokens.append(('universal', val, lin, col))
    213
    214                  elif val == u'|' and tokens and\
    215                       self._type(tokens[-1]) in ('IDENT', 'universal') and\
    216                       self._tokenvalue(tokens[-1]).find(u'|') == -1:
    217                      # namespace_prefix: "IDENT|" or "*|"
    218                      tokens[-1] = ('namespace_prefix',
    219                                    self._tokenvalue(tokens[-1])+u'|', lin, col)
    220                  elif val == u'|':
    221                      # namespace_prefix: "|"
    222                      tokens.append(('namespace_prefix', val, lin, col))
    223
    224                  else:
    225                      tokens.append(t)
    226
    227              # TODO: back to generator but not elegant at all!
    228              tokenizer = (t for t in tokens)
    229
    230              # for closures: must be a mutable
    231              new = {'valid': True,
    232                     'context': ['ROOT'], # stack of: 'attrib', 'negation', 'pseudo'
    233                     'prefixes': set()
    234                     }
    235              # used for equality checks and setting of a space combinator
    236              S = u' '
    237
    238              def append(seq, val, typ=None):
    239                  "appends to seq, may be expanded later"
    240                  seq.append(val, typ)
    241
    242              # expected constants
    243              simple_selector_sequence = 'type_selector universal HASH class attrib pseudo negation '
    244              simple_selector_sequence2 = 'HASH class attrib pseudo negation '
    245
    246              element_name = 'element_name'
    247
    248              negation_arg = 'type_selector universal HASH class attrib pseudo'
    249              negationend = ')'
    250
    251              attname = 'prefix attribute'
    252              attname2 = 'attribute'
    253              attcombinator = 'combinator ]' # optional
    254              attvalue = 'value'       # optional
    255              attend = ']'
    256
    257              expressionstart = 'PLUS - DIMENSION NUMBER STRING IDENT'
    258              expression = expressionstart + ' )'
    259
    260              combinator = ' combinator'
    261
    262              def _COMMENT(expected, seq, token, tokenizer=None):
    263                  "special implementation for comment token"
    264                  append(seq, cssutils.css.CSSComment([token]))
    265                  return expected
    266
    267              def _S(expected, seq, token, tokenizer=None):
    268                  # S
    269                  context = new['context'][-1]
    270                  val, typ = self._tokenvalue(token), self._type(token)
    271                  if context.startswith('pseudo-'):
    272                      append(seq, S, 'combinator')
    273                      return expected
    274
    275                  elif context != 'attrib' and 'combinator' in expected:
    276                      append(seq, S, 'combinator')
    277                      return simple_selector_sequence + combinator
    278
    279                  else:
    280                      return expected
    281
    282              def _universal(expected, seq, token, tokenizer=None):
    283                  # *|* or prefix|*
    284                  context = new['context'][-1]
    285                  val, typ = self._tokenvalue(token), self._type(token)
    286                  if 'universal' in expected:
    287                      append(seq, val, typ)
    288
    289                      newprefix = val.split(u'|')[0]
    290                      if newprefix and newprefix != u'*':
    291                          new['prefixes'].add(newprefix)
    292
    293                      if 'negation' == context:
    294                          return negationend
    295                      else:
    296                          return simple_selector_sequence2 + combinator
    297
    298                  else:
    299                      self._log.error(
    300                          u'Selector: Unexpected universal.', token=token)
    301                      return expected
    302
    303              def _namespace_prefix(expected, seq, token, tokenizer=None):
    304                  # prefix| => element_name
    305                  # or prefix| => attribute_name if attrib
    306                  context = new['context'][-1]
    307                  val, typ = self._tokenvalue(token), self._type(token)
    308                  if 'attrib' == context and 'prefix' in expected:
    309                      # [PREFIX|att]
    310                      append(seq, val, typ)
    311
    312                      newprefix = val.split(u'|')[0]
    313                      if newprefix and newprefix != u'*':
    314                          new['prefixes'].add(newprefix)
    315
    316                      return attname2
    317                  elif 'type_selector' in expected:
    318                      # PREFIX|*
    319                      append(seq, val, typ)
    320
    321                      newprefix = val.split(u'|')[0]
    322                      if newprefix and newprefix != u'*':
    323                          new['prefixes'].add(newprefix)
    324
    325                      return element_name
    326                  else:
    327                      self._log.error(
    328                          u'Selector: Unexpected namespace prefix.', token=token)
    329                      return expected
    330
    331              def _pseudo(expected, seq, token, tokenizer=None):
    332                  # pseudo-class or pseudo-element :a ::a :a( ::a(
    333                  """
    334                  /* '::' starts a pseudo-element, ':' a pseudo-class */
    335                  /* Exceptions: :first-line, :first-letter, :before and :after. */
    336                  /* Note that pseudo-elements are restricted to one per selector and */
    337                  /* occur only in the last simple_selector_sequence. */
    338                  """
    339                  context = new['context'][-1]
    340                  val, typ = self._tokenvalue(token, normalize=True), self._type(token)
    341                  if 'pseudo' in expected:
    342                      if val in (':first-line', ':first-letter', ':before', ':after'):
    343                          # always pseudo-element ???
    344                          typ = 'pseudo-element'
    345                      append(seq, val, typ)
    346
    347                      if val.endswith(u'('):
    348                          # function
    349                          new['context'].append(typ) # "pseudo-" "class" or "element"
    350                          return expressionstart
    351                      elif 'negation' == context:
    352                          return negationend
    353                      elif 'pseudo-element' == typ:
    354                          # only one per element, check at ) also!
    355                          return combinator
    356                      else:
    357                          return simple_selector_sequence2 + combinator
    358
    359                  else:
    360                      self._log.error(
    361                          u'Selector: Unexpected start of pseudo.', token=token)
    362                      return expected
    363
    364              def _expression(expected, seq, token, tokenizer=None):
    365                  # [ [ PLUS | '-' | DIMENSION | NUMBER | STRING | IDENT ] S* ]+
    366                  context = new['context'][-1]
    367                  val, typ = self._tokenvalue(token), self._type(token)
    368                  if context.startswith('pseudo-'):
    369                      append(seq, val, typ)
    370                      return expression
    371                  else:
    372                      self._log.error(
    373                          u'Selector: Unexpected %s.' % typ, token=token)
    374                      return expected
    375
    376              def _attcombinator(expected, seq, token, tokenizer=None):
    377                  # context: attrib
    378                  # PREFIXMATCH | SUFFIXMATCH | SUBSTRINGMATCH | INCLUDES |
    379                  # DASHMATCH
    380                  context = new['context'][-1]
    381                  val, typ = self._tokenvalue(token), self._type(token)
    382                  if 'attrib' == context and 'combinator' in expected:
    383                      # combinator in attrib
    384                      append(seq, val)
    385                      return attvalue
    386                  else:
    387                      self._log.error(
    388                          u'Selector: Unexpected %s.' % typ, token=token)
    389                      return expected
    390
    391              def _string(expected, seq, token, tokenizer=None):
    392                  # identifier
    393                  context = new['context'][-1]
    394                  val, typ = self._tokenvalue(token), self._type(token)
    395
    396                  # context: attrib
    397                  if 'attrib' == context and 'value' in expected:
    398                      # attrib: [...=VALUE]
    399                      append(seq, val, typ)
    400                      return attend
    401
    402                  # context: pseudo
    403                  elif context.startswith('pseudo-'):
    404                      # :func(...)
    405                      append(seq, val, typ)
    406                      return expression
    407
    408                  else:
    409                      self._log.error(
    410                          u'Selector: Unexpected STRING.', token=token)
    411                      return expected
    412
    413              def _ident(expected, seq, token, tokenizer=None):
    414                  # identifier
    415                  context = new['context'][-1]
    416                  val, typ = self._tokenvalue(token), self._type(token)
    417
    418                  # context: attrib
    419                  if 'attrib' == context and 'attribute' in expected:
    420                      # attrib: [...|ATT...]
    421                      append(seq, val, typ)
    422                      return attcombinator
    423
    424                  elif 'attrib' == context and 'value' in expected:
    425                      # attrib: [...=VALUE]
    426                      append(seq, val, typ)
    427                      return attend
    428
    429                  # context: negation
    430                  elif 'negation' == context:
    431                      # negation: (prefix|IDENT)
    432                      append(seq, val, typ)
    433                      return negationend
    434
    435                  # context: pseudo
    436                  elif context.startswith('pseudo-'):
    437                      # :func(...)
    438                      append(seq, val, typ)
    439                      return expression
    440
    441                  elif 'type_selector' in expected or element_name == expected:
    442                      # element name after ns or complete type_selector
    443                      append(seq, val, typ)
    444                      return simple_selector_sequence2 + combinator
    445
    446                  else:
    447                      self._log.error(
    448                          u'Selector: Unexpected IDENT.', token=token)
    449                      return expected
    450
    451              def _class(expected, seq, token, tokenizer=None):
    452                  # .IDENT
    453                  context = new['context'][-1]
    454                  val, typ = self._tokenvalue(token), self._type(token)
    455                  if 'class' in expected:
    456                      append(seq, val, typ)
    457
    458                      if 'negation' == context:
    459                          return negationend
    460                      else:
    461                          return simple_selector_sequence2 + combinator
    462
    463                  else:
    464                      self._log.error(
    465                          u'Selector: Unexpected class.', token=token)
    466                      return expected
    467
    468              def _hash(expected, seq, token, tokenizer=None):
    469                  # #IDENT
    470                  context = new['context'][-1]
    471                  val, typ = self._tokenvalue(token), self._type(token)
    472                  if 'HASH' in expected:
    473                      append(seq, val, typ)
    474
    475                      if 'negation' == context:
    476                          return negationend
    477                      else:
    478                          return simple_selector_sequence2 + combinator
    479
    480                  else:
    481                      self._log.error(
    482                          u'Selector: Unexpected HASH.', token=token)
    483                      return expected
    484
    485              def _char(expected, seq, token, tokenizer=None):
    486                  # + > ~ S
    487                  context = new['context'][-1]
    488                  val, typ = self._tokenvalue(token), self._type(token)
    489
    490                  # context: attrib
    491                  if u']' == val and 'attrib' == context and ']' in expected:
    492                      # end of attrib
    493                      append(seq, val)
    494                      context = new['context'].pop() # attrib is done
    495                      context = new['context'][-1]
    496                      if 'negation' == context:
    497                          return negationend
    498                      else:
    499                          return simple_selector_sequence2 + combinator
    500
    501                  elif u'=' == val and 'attrib' == context and 'combinator' in expected:
    502                      # combinator in attrib
    503                      append(seq, val)
    504                      return attvalue
    505
    506                  # context: negation
    507                  elif u')' == val and 'negation' == context and u')' in expected:
    508                      # not(negation_arg)"
    509                      append(seq, val)
    510                      new['context'].pop() # negation is done
    511                      context = new['context'][-1]
    512                      return simple_selector_sequence + combinator
    513
    514                  # context: pseudo (at least one expression)
    515                  elif val in u'+-' and context.startswith('pseudo-'):
    516                      # :func(+ -)"
    517                      append(seq, val)
    518                      return expression
    519
    520                  elif u')' == val and context.startswith('pseudo-') and\
    521                       expression == expected:
    522                      # :func(expression)"
    523                      append(seq, val)
    524                      new['context'].pop() # pseudo is done
    525                      if 'pseudo-element' == context:
    526                          return combinator
    527                      else:
    528                          return simple_selector_sequence + combinator
    529
    530                  # context: ROOT
    531                  elif u'[' == val and 'attrib' in expected:
    532                      # start of [attrib]
    533                      new['context'].append('attrib')
    534                      append(seq, val)
    535                      return attname
    536
    537                  elif val in u'+>~' and 'combinator' in expected:
    538                      # no other combinator except S may be following
    539                      if seq and S == seq[-1]:
    540                          seq[-1] = (val, 'combinator')
    541                      else:
    542                          append(seq, val, 'combinator')
    543                      return simple_selector_sequence
    544
    545                  elif u',' == val:
    546                      # not a selectorlist
    547                      self._log.error(
    548                          u'Selector: Single selector only.',
    549                          error=xml.dom.InvalidModificationErr,
    550                          token=token)
    551
    552                  else:
    553                      self._log.error(
    554                          u'Selector: Unexpected CHAR.', token=token)
    555                      return expected
    556
    557              def _negation(expected, seq, token, tokenizer=None):
    558                  # not(
    559                  val = self._tokenvalue(token, normalize=True)
    560                  if 'negation' in expected:
    561                      new['context'].append('negation')
    562                      append(seq, val, 'negation')
    563                      return negation_arg
    564                  else:
    565                      self._log.error(
    566                          u'Selector: Unexpected negation.', token=token)
    567                      return expected
    568
    569              # expected: only|not or mediatype, mediatype, feature, and
    570              newseq = self._newseq()
    571              valid, expected = self._parse(expected=simple_selector_sequence,
    572                  seq=newseq, tokenizer=tokenizer,
    573                  productions={'CHAR': _char,
    574                               'class': _class,
    575                               'HASH': _hash,
    576                               'STRING': _string,
    577                               'IDENT': _ident,
    578                               'namespace_prefix': _namespace_prefix,
    579                               'negation': _negation,
    580                               'pseudo-class': _pseudo,
    581                               'pseudo-element': _pseudo,
    582                               'universal': _universal,
    583                               # pseudo
    584                               'NUMBER': _expression,
    585                               'DIMENSION': _expression,
    586                               # attribute
    587                               'PREFIXMATCH': _attcombinator,
    588                               'SUFFIXMATCH': _attcombinator,
    589                               'SUBSTRINGMATCH': _attcombinator,
    590                               'DASHMATCH': _attcombinator,
    591                               'INCLUDES': _attcombinator,
    592
    593                               'S': _S,
    594                               'COMMENT': _COMMENT})
    595              valid = valid and new['valid']
    596
    597              # post condition
    598              if len(new['context']) > 1:
    599                  valid = False
    600                  self._log.error(u'Selector: Incomplete selector: %s' %
    601                      self._valuestr(selectorText))
    602
    603              if expected == 'element_name':
    604                  valid = False
    605                  self._log.error(u'Selector: No element name found: %s' %
    606                      self._valuestr(selectorText))
    607
    608              if expected == simple_selector_sequence:
    609                  valid = False
    610                  self._log.error(u'Selector: Cannot end with combinator: %s' %
    611                      self._valuestr(selectorText))
    612
    613              if newseq and hasattr(newseq[-1], 'strip') and newseq[-1].strip() == u'':
    614                  del newseq[-1]
    615
    616              # set
    617              if valid:
    618                  self.valid = True
    619                  self.seq = newseq
    620                  self.prefixes = new['prefixes']
    621
    622      selectorText = property(_getSelectorText, _setSelectorText,
    623          doc="(DOM) The parsable textual representation of the selector.")
    624
    625 -    def __repr__(self):
    626          return "cssutils.css.%s(selectorText=%r)" % (
    627                  self.__class__.__name__, self.selectorText)
    628
    629 -    def __str__(self):
    630          return "<cssutils.css.%s object selectorText=%r at 0x%x>" % (
    631                  self.__class__.__name__, self.selectorText, id(self))
    632