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