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-08-20 22:07:12 +0200 (Mo, 20 Aug 2007) $'
     26  __version__ = '$LastChangedRevision: 257 $'
     27
     28  import xml.dom
     29
     30  import cssutils
     31
     32
     33 -class Selector(cssutils.util.Base):
     34      """
     35      (cssutils) a single selector in a SelectorList of a CSSStyleRule
     36
     37      Properties
     38      ==========
     39      selectorText
     40          textual representation of this Selector
     41      namespaces
     42          a set which namespaces have been used in this selector
     43      seq
     44          sequence of Selector parts including comments
     45
     46      Format
     47      ======
     48      combinator
     49        : PLUS S* | GREATER S* | S+ ;
     50      selector
     51        : simple_selector [ combinator simple_selector ]* ;
     52      simple_selector
     53        : element_name [ HASH | class | attrib | pseudo ]*
     54        | [ HASH | class | attrib | pseudo ]+ ;
     55      class
     56        : '.' IDENT ;
     57      element_name
     58        : IDENT | '*' ;
     59      attrib
     60        : '[' S* IDENT S* [ [ '=' | INCLUDES | DASHMATCH ] S*
     61        [ IDENT | STRING ] S* ]? ']'
     62        ;
     63      pseudo
     64        : ':' [ IDENT | FUNCTION S* IDENT? S* ')' ] ;
     65
     66      plus some from http://www.w3.org/TR/css3-selectors/
     67      """
     68 -    def __init__(self, selectorText=None, readonly=False):
     69          """
     70          selectorText
     71              initial value of this selector
     72          readonly
     73              default to False
     74          """
     75          super(Selector, self).__init__()
     76
     77          self.seq = []
     78          self.namespaces = set()
     79          if selectorText:
     80              self.selectorText = selectorText
     81          self._readonly = readonly
     82
     83
     84 -    def _getSelectorText(self):
     85          """
     86          returns serialized format
     87          """
     88          return cssutils.ser.do_css_Selector(self)
     89
     90 -    def _setSelectorText(self, selectorText):
     91          """
     92          DOMException on setting
     93
     94          - INVALID_MODIFICATION_ERR: (self)
     95          - INVALID_MODIFICATION_ERR: (self)
     96            Raised if the specified CSS string value represents a different
     97            type than the current one, e.g. a SelectorList.
     98          - NO_MODIFICATION_ALLOWED_ERR: (self)
     99            Raised if this rule is readonly.
    100          - SYNTAX_ERR: (self)
    101            Raised if the specified CSS string value has a syntax error
    102            and is unparsable.
    103          """
    104          def preprocess(tokens):
    105              """
    106              remove S at start and end,
    107              if around a combinator (ignoring comments)
    108              or in att []
    109              """
    110              # remove starting S
    111              if tokens[0].type == self._ttypes.S:
    112                  del tokens[0]
    113              COMBINATORS_NO_S = u'+>~'
    114              r = []
    115              attlevel = 0
    116              i = 0
    117
    118              i, imax = 0, len(tokens) # imax may be changed later!
    119              while i < imax:
    120                  t = tokens[i]
    121                  if self._ttypes.S == t.type and attlevel > 0:
    122                      # remove all WS in atts 1st level
    123                      i += 1
    124                      continue
    125
    126                  elif t.value in COMBINATORS_NO_S and attlevel == 0:
    127                      # remove preceding S from found except token!=Comment
    128                      before = len(r)
    129                      while before >= 0:
    130                          before -= 1
    131                          if r[before].type == self._ttypes.S:
    132                              del r[before]
    133                          elif r[before].type != self._ttypes.COMMENT:
    134                              break
    135                      # remove following S from tokens except token!=Comment
    136                      after = i+1
    137                      while after < imax:
    138                          if tokens[after].type == self._ttypes.S:
    139                              del tokens[after]
    140                              after -= 1 # BECAUSE DELETED ONE!
    141                              imax = len(tokens) # new imax!
    142                          elif tokens[after].type != self._ttypes.COMMENT:
    143                              break
    144                          after += 1
    145                  elif t.value == u'[':
    146                      attlevel += 1
    147                  elif t.value == u']':
    148                      attlevel -= 1
    149                  r.append(t)
    150                  i += 1
    151
    152              # remove ending S
    153              if r and r[-1].type == self._ttypes.S:
    154                  del r[-1]
    155              return r
    156
    157          def addprefix(seq):
    158              """
    159              checks if last item in seq was a namespace prefix
    160              and if yes adds it to newnamespaces
    161              """
    162              try:
    163                  # may also be comment...
    164                  if isinstance(seq[-1], dict) and \
    165                     seq[-1]['type'] in ('type', 'attributename'):
    166                      newnamespaces.add(seq[-1]['value'])
    167                      seq[-1]['type'] = 'prefix'
    168                  else:
    169                      newnamespaces.add('')
    170              except IndexError:
    171                  pass
    172
    173          def getAttrib(atttokens):
    174              """
    175              atttokens
    176                  a sequence of tokens without any S which are preprocessed
    177
    178              returns seq of selector attribute
    179
    180              Format
    181              ======
    182              attrib
    183                : '[' S* IDENT S* [ [ '=' | INCLUDES | DASHMATCH ] S*
    184                  [ IDENT | STRING ] S* ]? ']' ;
    185
    186              CSS3 additionally:
    187                  ^= PREFIXMATCH
    188                  $= SUFFIXMATCH
    189                  *= SUBSTRINGMATCH
    190              """
    191              attseq = []
    192              expected = "[" # att, comb or ], value, pipe
    193              i, imax = 0, len(atttokens)
    194              while i < imax:
    195                  t = atttokens[i]
    196                  if self._ttypes.COMMENT == t.type: # just add
    197                      attseq.append(cssutils.css.CSSComment(t))
    198
    199                  # [ start
    200                  elif expected == '[' and t.value == u'[':
    201                      attseq.append({'type': 'attribute selector', 'value': t.value})
    202                      expected = 'att'
    203                  elif expected == '[':
    204                      self._log.error(u'Selector: No Attrib start "[" found.', t)
    205                      return
    206
    207                  # ] end
    208                  elif expected in ('comb or ]', ']') and t.value == u']':
    209                      attseq.append({'type': 'attribute selector end', 'value': t.value})
    210                      assert i == len(atttokens) - 1 # should really be end
    211                      break
    212                  elif expected == 'pipe' and t.value == u']':
    213                      self._log.error(u'Selector: No attribute name for namespace start found.', t)
    214                      return
    215
    216                  # * (must be followed by |)
    217                  elif expected == 'att' and t.type == self._ttypes.UNIVERSAL:
    218                      attseq.append({'type': 'type', 'value': t.value})
    219                      expected = 'pipe'
    220
    221                  # | namespace pipe
    222                  elif expected in ('pipe', 'att', 'comb or ]') and \
    223                       t.value == u'|':
    224                      addprefix(attseq)
    225                      attseq.append({'type': 'type', 'value': t.value})
    226                      expected = 'att'
    227
    228                  elif expected == 'att' and t.type == self._ttypes.IDENT:
    229                      attseq.append({'type': 'attributename', 'value': t.value})
    230                      expected = 'comb or ]'
    231                  elif expected == 'att':
    232                      self._log.error(u'Selector: No Attrib found.', t)
    233                      return
    234
    235                  # CSS3 selectors added
    236                  elif expected == 'comb or ]' and t.value in (
    237                       u'=', u'~=', u'|=', u'^=', u'$=', u'*='):
    238                      attseq.append({'type': 'combinator', 'value': t.value})
    239                      expected = 'val'
    240                  elif expected == 'comb or ]':
    241                      self._log.error(u'Selector: No Attrib combinator found.', t)
    242                      return
    243
    244                  elif expected == 'val' and t.type in (
    245                       self._ttypes.IDENT, self._ttypes.STRING):
    246                      attseq.append({'type': 'attributevalue', 'value': t.value})
    247                      expected = ']'
    248                  elif expected == 'val':
    249                      self._log.error(u'Selector: No Attrib value found.', t)
    250                      return
    251
    252                  else:
    253                      self._log.error(u'Selector: Unknown Attrib syntax.', t)
    254                      return
    255
    256                  i += 1
    257
    258              else: # in case of ] not found
    259                  self._log.error(u'Selector: Incomplete Attrib.', t)
    260                  return
    261
    262              return attseq
    263
    264          def getFunc(functokens):
    265              """
    266              functokens
    267                  a sequence of tokens
    268
    269              returns seq of selector functional_pseudo
    270
    271              Format
    272              ======
    273              functional_pseudo
    274                : FUNCTION S* expression ')'
    275                ;
    276              """
    277              funcseq = [{'type': 'function', 'value': functokens[0].value}]
    278              expected = ")" # expr or )
    279              i, imax = 1, len(functokens)
    280              while i < imax:
    281                  t = functokens[i]
    282
    283                  if self._ttypes.COMMENT == t.type: # just add
    284                      funcseq.append(cssutils.css.CSSComment(t))
    285
    286                  elif t.value == u')':
    287                      funcseq.append(
    288                          {'type': 'function end', 'value': t.value})
    289                      expected = None
    290                      assert i == len(functokens) - 1 # should really be end
    291                      break
    292
    293                  else:
    294                      funcseq.append({'type': 'functionvalue', 'value': t.value})
    295
    296                  i += 1
    297
    298              if expected:
    299                  self._log.error(
    300                      u'Selector: Incomplete functional pseudo "%s".' %
    301                      self._valuestr(functokens))
    302                  return
    303
    304              return funcseq
    305
    306          self._checkReadonly()
    307
    308          tokens = self._tokenize(selectorText)
    309          if tokens:
    310              COMBINATORS = u' >+~'
    311              tokens = preprocess(tokens)
    312
    313              newseq = []
    314              newnamespaces = set()
    315              found1 = False
    316              # simple_selector | combinator: u' >+~' |
    317              # pseudoclass after : | pseudoelement after ::
    318              expected = 'simple_selector'
    319              i, imax = 0, len(tokens)
    320              while i < imax:
    321                  t = tokens[i]
    322                  ##print t.value, t.type, expected
    323                  # , invalid SelectorList
    324                  if self._ttypes.COMMA == t.type:
    325                      self._log.error(u'Selector: Possible SelectorList found.', t,
    326                                error=xml.dom.InvalidModificationErr)
    327                      return
    328
    329                  # /* comments */
    330                  elif self._ttypes.COMMENT == t.type and expected not in (
    331                       'classname', 'pseudoclass', 'pseudoelement'):
    332                      # just add
    333                      newseq.append(cssutils.css.CSSComment(t))
    334
    335                  # . class
    336                  elif expected.startswith('simple_selector') and \
    337                       t.type == self._ttypes.CLASS:
    338                      newseq.append({'value': t.value, 'type': 'class'})
    339                      found1 = True
    340                      expected = 'classname'
    341                  elif expected == 'classname' and t.type == self._ttypes.IDENT:
    342                      newseq.append({'value': t.value, 'type': 'classname'})
    343                      expected = 'simple_selector or combinator'
    344
    345                  # pseudoclass ":" or pseudoelement "::"
    346                  elif expected.startswith('simple_selector') and \
    347                       t.value == u':' or t.value == u'::':
    348                      expected = {
    349                          ':': 'pseudoclass', '::': 'pseudoelement'}[t.value]
    350                      newseq.append({'value': t.value, 'type': expected})
    351                      found1 = True
    352                  elif expected.startswith('pseudo') and \
    353                       t.type == self._ttypes.IDENT:
    354                      newseq.append({'value': t.value, 'type': 'pseudoname'})
    355                      expected = 'simple_selector or combinator'
    356                  elif expected == 'pseudoclass' and \
    357                       t.type == self._ttypes.FUNCTION:
    358                      # pseudo "function" like lang(...)
    359                      functokens, endi = self._tokensupto(
    360                          tokens[i:], funcendonly=True)
    361                      i += endi
    362                      funcseq = getFunc(functokens)
    363                      if not funcseq:
    364                          self._log.error(
    365                              u'Selector: Invalid functional pseudo.', t)
    366                          return
    367                      newseq.extend(funcseq)
    368                      expected = 'simple_selector or combinator'
    369                  elif expected == 'pseudoclass':
    370                      self._log.error(u'Selector: Pseudoclass name expected.', t)
    371                      return
    372                  elif expected == 'pseudoelement':
    373                      self._log.error(u'Selector: Pseudoelement name expected.', t)
    374                      return
    375
    376                  # #hash
    377                  elif expected.startswith('simple_selector') and \
    378                       t.type == self._ttypes.HASH:
    379                      newseq.append({'value': t.value, 'type': 'id'})
    380                      found1 = True
    381                      expected = 'simple_selector or combinator'
    382
    383                  # | namespace pipe
    384                  elif expected.startswith('simple_selector') and \
    385                       t.value == u'|':
    386                      addprefix(newseq)
    387                      newseq.append({'type': 'pipe', 'value': t.value})
    388                      found1 = True
    389                      expected = 'namespaced element'
    390
    391                  # element, *
    392                  elif (expected.startswith('simple_selector') or
    393                        expected == 'namespaced element') and \
    394                       t.type in (self._ttypes.IDENT, self._ttypes.UNIVERSAL):
    395                      newseq.append({'value': t.value, 'type': 'type'})
    396                      found1 = True
    397                      expected = 'simple_selector or combinator'
    398                  # element, *
    399                  elif expected == 'namespaced element' and \
    400                       t.type not in (self._ttypes.IDENT, self._ttypes.UNIVERSAL):
    401                      self._log.error(u'Selector: Namespaced element expected.', t)
    402                      return
    403
    404                  # attribute selector
    405                  elif expected.startswith('simple_selector') and t.value == u'[':
    406                      atttokens, endi = self._tokensupto(
    407                          tokens[i:], selectorattendonly=True)
    408                      i += endi
    409                      attseq = getAttrib(atttokens)
    410                      if not attseq:
    411                          self._log.warn(
    412                              u'Selector: Empty attribute selector.', t)
    413                          return
    414                      newseq.extend(attseq)
    415                      found1 = True
    416                      expected = 'simple_selector or combinator'
    417
    418                  # CSS3 combinator ~
    419                  elif expected == 'simple_selector or combinator' and \
    420                       t.value in COMBINATORS or t.type == self._ttypes.S:
    421                      if t.type == self._ttypes.S:
    422                          newseq.append({'value': t.normalvalue, 'type': 'combinator'}) # single u' '
    423                      else:
    424                          newseq.append({'value': t.value, 'type': 'combinator'})
    425                      expected = 'simple_selector' # or [
    426                  elif expected == 'simple_selector or combinator':
    427                      self._log.error(u'Selector: No combinator found.', t)
    428                      return
    429
    430                  else:
    431                      self._log.error(u'Selector: Expected simple selector.', t)
    432                      return
    433
    434                  i += 1
    435
    436              if expected == 'classname':
    437                  self._log.error(u'Selector: Class name expected: %s.' %
    438                            selectorText)
    439                  return
    440              elif expected == 'namespaced element':
    441                  self._log.error(u'Selector: No element found for namespace start: %s.' %
    442                            selectorText)
    443                  return
    444              elif expected == 'pseudoclass':
    445                  self._log.error(u'Selector: Pseudoclass name expected: %s.' %
    446                            selectorText)
    447                  return
    448              elif expected == 'pseudoelement':
    449                  self._log.error(u'Selector: Pseudoelement name expected: %s.' %
    450                            selectorText)
    451                  return
    452              elif newseq and ((
    453                      isinstance(newseq[-1], basestring) and \
    454                      newseq[-1] in COMBINATORS
    455                      ) or ( # may be a dict
    456                      isinstance(newseq[-1], dict) and
    457                      isinstance(newseq[-1]['value'], basestring) and \
    458                      newseq[-1]['value'] in COMBINATORS)):
    459                  self._log.error(u'Selector: Cannot end with combinator: %s.' %
    460                      selectorText)
    461                  return
    462
    463              if found1:
    464                  self.seq = newseq
    465                  self.namespaces = newnamespaces
    466  ##                import pprint;pprint.pprint(newseq)
    467  ##                print self.namespaces
    468              else:
    469                  self._log.error(
    470                      u'Selector: No selector found: %s.' % selectorText)
    471          else:
    472              self._log.error(u'Selector: Empty selector.')
    473
    474      selectorText = property(_getSelectorText, _setSelectorText,
    475          doc="(DOM) The parsable textual representation of the selector.")
    476
    477 -    def __repr__(self):
    478          return "cssutils.css.%s(selectorText=%r)" % (
    479                  self.__class__.__name__, self.selectorText)
    480
    481 -    def __str__(self):
    482          return "<cssutils.css.%s object selectorText=%r at 0x%x>" % (
    483                  self.__class__.__name__, self.selectorText, id(self))
    484
    485
    486  if __name__ == '__main__':
    487      s = Selector()
    488      s.selectorText = u'n1|a[n2|x]' #u'::a:b()'
    489      print s.selectorText
    490