Package cssutils :: Module util |
[hide private]
[frames] | no frames]

# Source Code for Module cssutils.util

      1  """base classes for css and stylesheets packages
      2  """
      3  __all__ = []
      4  __docformat__ = 'restructuredtext'
      5  __author__ = '$LastChangedBy: cthedot $'
      6  __date__ = '$LastChangedDate: 2008-02-22 19:03:20 +0100 (Fr, 22 Feb 2008) $'
      7  __version__ = '$LastChangedRevision: 1070 $'
      8
      9  from itertools import ifilter
     10  import re
     11  import types
     12  import xml.dom
     13  import cssutils
     14  from tokenize2 import Tokenizer
     15
     16 -class Base(object):
     17      """
     18      Base class for most CSS and StyleSheets classes
     19
     20      **Superceded by Base2 which is used for new seq handling class.**
     21      See cssutils.util.Base2
     22
     23      Contains helper methods for inheriting classes helping parsing
     24
     25      ``_normalize`` is static as used by Preferences.
     26      """
     27      __tokenizer2 = Tokenizer()
     28
     29      _log = cssutils.log
     30      _prods = cssutils.tokenize2.CSSProductions
     31
     32      # for more on shorthand properties see
     33      # http://www.dustindiaz.com/css-shorthand/
     34      # format: shorthand: [(propname, mandatorycheck?)*]
     35      _SHORTHANDPROPERTIES = {
     36              u'background': [],
     37              u'border': [],
     38              u'border-left': [],
     39              u'border-right': [],
     40              u'border-top': [],
     41              u'border-bottom': [],
     42              u'border-color': [],
     43              u'border-style': [],
     44              u'border-width': [],
     45              u'cue': [],
     46              u'font': [],
     47  #                      [('font-weight', True),
     48  #                      ('font-size', True),
     49  #                      ('line-height', False),
     50  #                      ('font-family', True)],
     51              u'list-style': [],
     52              u'margin': [],
     53              u'outline': [],
     54              u'padding': [],
     55              u'pause': []
     56              }
     57
     58      # simple escapes, all non unicodes
     59      __escapes = re.compile(ur'(\\[^0-9a-fA-F])').sub
     60      # all unicode (see cssproductions "unicode")
     61      __unicodes = re.compile(ur'\\[0-9a-fA-F]{1,6}[\t|\r|\n|\f|\x20]?').sub
     62
     63      @staticmethod
     64 -    def _normalize(x):
     65          """
     66          normalizes x, namely:
     67
     68          - remove any \ before non unicode sequences (0-9a-zA-Z) so for
     69            x=="c\olor\" return "color" (unicode escape sequences should have
     70            been resolved by the tokenizer already)
     71          - lowercase
     72          """
     73          if x:
     74              def removeescape(matchobj):
     75                  return matchobj.group(0)[1:]
     76              x = Base.__escapes(removeescape, x)
     77              return x.lower()
     78          else:
     79              return x
     80
     81 -    def _checkReadonly(self):
     82          "raises xml.dom.NoModificationAllowedErr if rule/... is readonly"
     83          if hasattr(self, '_readonly') and self._readonly:
     84              raise xml.dom.NoModificationAllowedErr(
     85                  u'%s is readonly.' % self.__class__)
     86              return True
     87          return False
     88
     89 -    def _splitNamespacesOff(self, text_namespaces_tuple):
     90          """
     91          returns tuple (text, dict-of-namespaces) or if no namespaces are
     92          in cssText returns (cssText, {})
     93
     94          used in Selector, SelectorList, CSSStyleRule, CSSMediaRule and
     95          CSSStyleSheet
     96          """
     97          if isinstance(text_namespaces_tuple, tuple):
     98              return text_namespaces_tuple[0], _SimpleNamespaces(
     99                                                      text_namespaces_tuple[1])
    100          else:
    101              return text_namespaces_tuple, _SimpleNamespaces()
    102
    103 -    def _tokenize2(self, textortokens):
    104          """
    105          returns tokens of textortokens which may already be tokens in which
    106          case simply returns input
    107          """
    108          if not textortokens:
    109              return None
    110          elif isinstance(textortokens, basestring):
    111              # needs to be tokenized
    112              return self.__tokenizer2.tokenize(
    113                   textortokens)
    114          elif types.GeneratorType == type(textortokens):
    115              # already tokenized
    116              return textortokens
    117          elif isinstance(textortokens, tuple):
    118              # a single token (like a comment)
    119              return [textortokens]
    120          else:
    121              # already tokenized but return generator
    122              return (x for x in textortokens)
    123
    124 -    def _nexttoken(self, tokenizer, default=None):
    125          "returns next token in generator tokenizer or the default value"
    126          try:
    127              return tokenizer.next()
    128          except (StopIteration, AttributeError):
    129              return default
    130
    131 -    def _type(self, token):
    132          "returns type of Tokenizer token"
    133          if token:
    134              return token[0]
    135          else:
    136              return None
    137
    138 -    def _tokenvalue(self, token, normalize=False):
    139          "returns value of Tokenizer token"
    140          if token and normalize:
    141              return Base._normalize(token[1])
    142          elif token:
    143              return token[1]
    144          else:
    145              return None
    146
    147 -    def _stringtokenvalue(self, token):
    148          """
    149          for STRING returns the actual content without surrounding "" or ''
    150          and without respective escapes, e.g.::
    151
    152               "with \" char" => with " char
    153          """
    154          if token:
    155              value = token[1]
    156              return value.replace('\\'+value[0], value[0])[1:-1]
    157          else:
    158              return None
    159
    160 -    def _uritokenvalue(self, token):
    161          """
    162          for URI returns the actual content without surrounding url()
    163          or url(""), url('') and without respective escapes, e.g.::
    164
    165               url("\"") => "
    166          """
    167          if token:
    168              value = token[1][4:-1].strip()
    169              if (value[0] in '\'"') and (value[0] == value[-1]):
    170                  # a string "..." or '...'
    171                  value = value.replace('\\'+value[0], value[0])[1:-1]
    172              return value
    173          else:
    174              return None
    175
    176 -    def _tokensupto2(self,
    177                       tokenizer,
    178                       starttoken=None,
    179                       blockstartonly=False, # {
    180                       blockendonly=False, # }
    181                       mediaendonly=False,
    182                       importmediaqueryendonly=False, # ; or STRING
    183                       mediaqueryendonly=False, # { or STRING
    184                       semicolon=False, # ;
    185                       propertynameendonly=False, # :
    186                       propertyvalueendonly=False, # ! ; }
    187                       propertypriorityendonly=False, # ; }
    188                       selectorattendonly=False, # ]
    189                       funcendonly=False, # )
    190                       listseponly=False, # ,
    191                       separateEnd=False # returns (resulttokens, endtoken)
    192                       ):
    193          """
    194          returns tokens upto end of atrule and end index
    195          end is defined by parameters, might be ; } ) or other
    196
    197          default looks for ending "}" and ";"
    198          """
    199          ends = u';}'
    200          endtypes = ()
    201          brace = bracket = parant = 0 # {}, [], ()
    202
    203          if blockstartonly: # {
    204              ends = u'{'
    205              brace = -1 # set to 0 with first {
    206          elif blockendonly: # }
    207              ends = u'}'
    208              brace = 1
    209          elif mediaendonly: # }
    210              ends = u'}'
    211              brace = 1 # rules } and mediarules }
    212          elif importmediaqueryendonly:
    213              # end of mediaquery which may be ; or STRING
    214              ends = u';'
    215              endtypes = ('STRING',)
    216          elif mediaqueryendonly:
    217              # end of mediaquery which may be { or STRING
    218              # special case, see below
    219              ends = u'{'
    220              brace = -1 # set to 0 with first {
    221              endtypes = ('STRING',)
    222          elif semicolon:
    223              ends = u';'
    224          elif propertynameendonly: # : and ; in case of an error
    225              ends = u':;'
    226          elif propertyvalueendonly: # ; or !important
    227              ends = u';!'
    228          elif propertypriorityendonly: # ;
    229              ends = u';'
    230          elif selectorattendonly: # ]
    231              ends = u']'
    232              if starttoken and self._tokenvalue(starttoken) == u'[':
    233                  bracket = 1
    234          elif funcendonly: # )
    235              ends = u')'
    236              parant = 1
    237          elif listseponly: # ,
    238              ends = u','
    239
    240          resulttokens = []
    241          if starttoken:
    242              resulttokens.append(starttoken)
    243          if tokenizer:
    244              for token in tokenizer:
    245                  typ, val, line, col = token
    246                  if 'EOF' == typ:
    247                      resulttokens.append(token)
    248                      break
    249                  if u'{' == val:
    250                      brace += 1
    251                  elif u'}' == val:
    252                      brace -= 1
    253                  elif u'[' == val:
    254                      bracket += 1
    255                  elif u']' == val:
    256                      bracket -= 1
    257                  # function( or single (
    258                  elif u'(' == val or \
    259                       Base._prods.FUNCTION == typ:
    260                      parant += 1
    261                  elif u')' == val:
    262                      parant -= 1
    263
    264                  resulttokens.append(token)
    265
    266                  if (brace == bracket == parant == 0) and (
    267                      val in ends or typ in endtypes):
    268                      break
    269                  elif mediaqueryendonly and brace == -1 and (
    270                       bracket == parant == 0) and typ in endtypes:
    271                       # mediaqueryendonly with STRING
    272                      break
    273
    274          if separateEnd:
    275              # TODO: use this method as generator, then this makes sense
    276              if resulttokens:
    277                  return resulttokens[:-1], resulttokens[-1]
    278              else:
    279                  return resulttokens, None
    280          else:
    281              return resulttokens
    282
    283 -    def _valuestr(self, t):
    284          """
    285          returns string value of t (t may be a string, a list of token tuples
    286          or a single tuple in format (type, value, line, col).
    287          Mainly used to get a string value of t for error messages.
    288          """
    289          if not t:
    290              return u''
    291          elif isinstance(t, basestring):
    292              return t
    293          else:
    294              return u''.join([x[1] for x in t])
    295
    296 -    def _adddefaultproductions(self, productions, new=None):
    297          """
    298          adds default productions if not already present, used by
    299          _parse only
    300
    301          each production should return the next expected token
    302          normaly a name like "uri" or "EOF"
    303          some have no expectation like S or COMMENT, so simply return
    304          the current value of self.__expected
    305          """
    306          def ATKEYWORD(expected, seq, token, tokenizer=None):
    307              "TODO: add default impl for unexpected @rule?"
    308              return expected
    309
    310          def COMMENT(expected, seq, token, tokenizer=None):
    311              "default implementation for COMMENT token adds CSSCommentRule"
    312              seq.append(cssutils.css.CSSComment([token]))
    313              return expected
    314
    315          def S(expected, seq, token, tokenizer=None):
    316              "default implementation for S token, does nothing"
    317              return expected
    318
    319          def EOF(expected=None, seq=None, token=None, tokenizer=None):
    320              "default implementation for EOF token"
    321              return 'EOF'
    322
    323          p = {'ATKEYWORD': ATKEYWORD,
    324               'COMMENT': COMMENT,
    325               'S': S,
    326               'EOF': EOF # only available if fullsheet
    327               }
    328          p.update(productions)
    329          return p
    330
    331 -    def _parse(self, expected, seq, tokenizer, productions, default=None,
    332                 new=None):
    333          """
    334          puts parsed tokens in seq by calling a production with
    335              (seq, tokenizer, token)
    336
    337          expected
    338              a name what token or value is expected next, e.g. 'uri'
    339          seq
    340              to add rules etc to
    341          tokenizer
    342              call tokenizer.next() to get next token
    343          productions
    344              callbacks {tokentype: callback}
    345          default
    346              default callback if tokentype not in productions
    347          new
    348              used to init default productions
    349
    350          returns (wellformed, expected) which the last prod might have set
    351          """
    352          wellformed = True
    353          if tokenizer:
    354              prods = self._adddefaultproductions(productions, new)
    355              for token in tokenizer:
    356                  p = prods.get(token[0], default)
    357                  if p:
    358                      expected = p(expected, seq, token, tokenizer)
    359                  else:
    360                      wellformed = False
    361                      self._log.error(u'Unexpected token (%s, %s, %s, %s)' % token)
    362
    363          return wellformed, expected
    364
    365
    366 -class Base2(Base):
    367      """
    368      Base class for new seq handling, used by Selector for now only
    369      """
    370 -    def __init__(self):
    371          self._seq = Seq()
    372
    373 -    def _setSeq(self, newseq):
    374          """
    375          sets newseq and makes it readonly
    376          """
    377          newseq._readonly = True
    378          self._seq = newseq
    379
    380      seq = property(lambda self: self._seq, doc="seq for most classes")
    381
    382 -    def _tempSeq(self, readonly=False):
    383          "get a writeable Seq() which is added later"
    384          return Seq(readonly=readonly)
    385
    386 -    def _adddefaultproductions(self, productions, new=None):
    387          """
    388          adds default productions if not already present, used by
    389          _parse only
    390
    391          each production should return the next expected token
    392          normaly a name like "uri" or "EOF"
    393          some have no expectation like S or COMMENT, so simply return
    394          the current value of self.__expected
    395          """
    396          def ATKEYWORD(expected, seq, token, tokenizer=None):
    397              "default impl for unexpected @rule"
    398              if expected != 'EOF':
    399                  # TODO: parentStyleSheet=self
    400                  rule = cssutils.css.CSSUnknownRule()
    401                  rule.cssText = self._tokensupto2(tokenizer, token)
    402                  if rule.wellformed:
    403                      seq.append(rule, cssutils.css.CSSRule.UNKNOWN_RULE,
    404                                 line=token[2], col=token[3])
    405                  return expected
    406              else:
    407                  new['wellformed'] = False
    408                  self._log.error(u'Expected EOF.',
    409                      token=token)
    410                  return expected
    411
    412          def COMMENT(expected, seq, token, tokenizer=None):
    413              "default implementation for COMMENT token adds CSSCommentRule"
    414              seq.append(cssutils.css.CSSComment([token]), 'COMMENT')
    415              return expected
    416
    417          def S(expected, seq, token, tokenizer=None):
    418              "default implementation for S token, does nothing"
    419              return expected
    420
    421          def EOF(expected=None, seq=None, token=None, tokenizer=None):
    422              "default implementation for EOF token"
    423              return 'EOF'
    424
    425          defaultproductions = {'ATKEYWORD': ATKEYWORD,
    426               'COMMENT': COMMENT,
    427               'S': S,
    428               'EOF': EOF # only available if fullsheet
    429               }
    430          defaultproductions.update(productions)
    431          return defaultproductions
    432
    433
    434 -class Seq(object):
    435      """
    436      property seq of Base2 inheriting classes, holds a list of Item objects.
    437
    438      used only by Selector for now
    439
    440      is normally readonly, only writable during parsing
    441      """
    442 -    def __init__(self, readonly=True):
    443          """
    444          only way to write to a Seq is to initialize it with new items
    445          each itemtuple has (value, type, line) where line is optional
    446          """
    447          self._seq = []
    448          self._readonly = readonly
    449
    450 -    def __delitem__(self, i):
    451          del self._seq[i]
    452
    453 -    def __getitem__(self, i):
    454          return self._seq[i]
    455
    456 -    def __setitem__(self, i, (val, typ, line, col)):
    457          self._seq[i] = Item(val, typ, line, col)
    458
    459 -    def __iter__(self):
    460          return iter(self._seq)
    461
    462 -    def __len__(self):
    463          return len(self._seq)
    464
    465 -    def append(self, val, typ, line=None, col=None):
    466          "if not readonly add new Item()"
    467          if self._readonly:
    468              raise AttributeError('Seq is readonly.')
    469          else:
    470              self._seq.append(Item(val, typ, line, col))
    471
    472 -    def replace(self, index=-1, val=None, typ=None, line=None, col=None):
    473          """
    474          if not readonly replace Item at index with new Item or
    475          simply replace value or type
    476          """
    477          if self._readonly:
    478              raise AttributeError('Seq is readonly.')
    479          else:
    480              self._seq[index] = Item(val, typ, line, col)
    481
    482 -    def __repr__(self):
    483          "returns a repr same as a list of tuples of (value, type)"
    484          return u'cssutils.%s.%s([\n    %s])' % (self.__module__,
    485                                            self.__class__.__name__,
    486              u',\n    '.join([u'(%r, %r)' % (item.type, item.value)
    487                            for item in self._seq]
    488              ))
    489 -    def __str__(self):
    490          return "<cssutils.%s.%s object length=%r at 0x%x>" % (
    491                  self.__module__, self.__class__.__name__, len(self), id(self))
    492
    493 -class Item(object):
    494      """
    495      an item in the seq list of classes (successor to tuple items in old seq)
    496
    497      each item has attributes:
    498
    499      type
    500          a sematic type like "element", "attribute"
    501      value
    502          the actual value which may be a string, number etc or an instance
    503          of e.g. a CSSComment
    504      *line*
    505          **NOT IMPLEMENTED YET, may contain the line in the source later**
    506      """
    507 -    def __init__(self, value, type, line=None, col=None):
    508          self.__value = value
    509          self.__type = type
    510          self.__line = line
    511          self.__col = col
    512
    513      type = property(lambda self: self.__type)
    514      value = property(lambda self: self.__value)
    515      line = property(lambda self: self.__line)
    516      col = property(lambda self: self.__col)
    517
    518 -    def __repr__(self):
    519          return "%s.%s(value=%r, type=%r, line=%r, col=%r)" % (
    520                  self.__module__, self.__class__.__name__,
    521                  self.__value, self.__type, self.__line, self.__col)
    522
    523
    524 -class ListSeq(object):
    525      """
    526      (EXPERIMENTAL)
    527      A base class used for list classes like css.SelectorList or
    528      stylesheets.MediaList
    529
    530      adds list like behaviour running on inhering class' property ``seq``
    531
    532      - item in x => bool
    533      - len(x) => integer
    534      - get, set and del x[i]
    535      - for item in x
    536      - append(item)
    537
    538      some methods must be overwritten in inheriting class
    539      """
    540 -    def __init__(self):
    541          self.seq = [] # does not need to use ``Seq`` as simple list only
    542
    543 -    def __contains__(self, item):
    544          return item in self.seq
    545
    546 -    def __delitem__(self, index):
    547          del self.seq[index]
    548
    549 -    def __getitem__(self, index):
    550          return self.seq[index]
    551
    552 -    def __iter__(self):
    553          def gen():
    554              for x in self.seq:
    555                  yield x
    556          return gen()
    557
    558 -    def __len__(self):
    559          return len(self.seq)
    560
    561 -    def __setitem__(self, index, item):
    562          "must be overwritten"
    563          raise NotImplementedError
    564
    565 -    def append(self, item):
    566          "must be overwritten"
    567          raise NotImplementedError
    568
    569
    570 -class Deprecated(object):
    571      """This is a decorator which can be used to mark functions
    572      as deprecated. It will result in a warning being emitted
    573      when the function is used.
    574
    575      It accepts a single paramter ``msg`` which is shown with the warning.
    576      It should contain information which function or method to use instead.
    577      """
    578 -    def __init__(self, msg):
    579          self.msg = msg
    580
    581 -    def __call__(self, func):
    582          def newFunc(*args, **kwargs):
    583              import warnings
    584              warnings.warn("Call to deprecated method %r. %s" %
    585                              (func.__name__, self.msg),
    586                              category=DeprecationWarning,
    587                              stacklevel=2)
    588              return func(*args, **kwargs)
    589          newFunc.__name__ = func.__name__
    590          newFunc.__doc__ = func.__doc__
    591          newFunc.__dict__.update(func.__dict__)
    592          return newFunc
    593
    594
    595 -class _Namespaces(object):
    596      """
    597      A dictionary like wrapper for @namespace rules used in a CSSStyleSheet.
    598      Works on effective namespaces, so e.g. if::
    599
    600          @namespace p1 "uri";
    601          @namespace p2 "uri";
    602
    603      only the second rule is effective and kept.
    604
    605      namespaces
    606          a dictionary {prefix: namespaceURI} containing the effective namespaces
    607          only. These are the latest set in the CSSStyleSheet.
    608      parentStyleSheet
    609          the parent CSSStyleSheet
    610      """
    611 -    def __init__(self, parentStyleSheet, *args):
    612          "no initial values are set, only the relevant sheet is"
    613          self.parentStyleSheet = parentStyleSheet
    614
    615 -    def __contains__(self, prefix):
    616          return prefix in self.namespaces
    617
    618 -    def __delitem__(self, prefix):
    619          """deletes CSSNamespaceRule(s) with rule.prefix == prefix
    620
    621          prefix '' and None are handled the same
    622          """
    623          if not prefix:
    624              prefix = u''
    625          delrule = self.__findrule(prefix)
    626          for i, rule in enumerate(ifilter(lambda r: r.type == r.NAMESPACE_RULE,
    627                              self.parentStyleSheet.cssRules)):
    628              if rule == delrule:
    629                  self.parentStyleSheet.deleteRule(i)
    630                  return
    631
    632          raise xml.dom.NamespaceErr('Prefix %r not found.' % prefix)
    633
    634 -    def __getitem__(self, prefix):
    635          try:
    636              return self.namespaces[prefix]
    637          except KeyError, e:
    638              raise xml.dom.NamespaceErr('Prefix %r not found.' % prefix)
    639
    640 -    def __iter__(self):
    641          return self.namespaces.__iter__()
    642
    643 -    def __len__(self):
    644          return len(self.namespaces)
    645
    646 -    def __setitem__(self, prefix, namespaceURI):
    647          "replaces prefix or sets new rule, may raise NoModificationAllowedErr"
    648          if not prefix:
    649              prefix = u'' # None or ''
    650          rule = self.__findrule(prefix)
    651          if not rule:
    652              self.parentStyleSheet.insertRule(cssutils.css.CSSNamespaceRule(
    653                                                      prefix=prefix,
    654                                                      namespaceURI=namespaceURI),
    655                                    inOrder=True)
    656          else:
    657              if prefix in self.namespaces:
    658                  rule.namespaceURI = namespaceURI # raises NoModificationAllowedErr
    659              if namespaceURI in self.namespaces.values():
    660                  rule.prefix = prefix
    661
    662 -    def __findrule(self, prefix):
    663          # returns namespace rule where prefix == key
    664          for rule in ifilter(lambda r: r.type == r.NAMESPACE_RULE,
    665                              reversed(self.parentStyleSheet.cssRules)):
    666              if rule.prefix == prefix:
    667                  return rule
    668
    669 -    def __getNamespaces(self):
    670          namespaces = {}
    671          for rule in ifilter(lambda r: r.type == r.NAMESPACE_RULE,
    672                              reversed(self.parentStyleSheet.cssRules)):
    673              if rule.namespaceURI not in namespaces.values():
    674                  namespaces[rule.prefix] = rule.namespaceURI
    675          return namespaces
    676
    677      namespaces = property(__getNamespaces,
    678          doc=u'Holds only effective @namespace rules in self.parentStyleSheets'
    679               '@namespace rules.')
    680
    681 -    def get(self, prefix, default):
    682          return self.namespaces.get(prefix, default)
    683
    684 -    def items(self):
    685          return self.namespaces.items()
    686
    687 -    def keys(self):
    688          return self.namespaces.keys()
    689
    690 -    def values(self):
    691          return self.namespaces.values()
    692
    693 -    def prefixForNamespaceURI(self, namespaceURI):
    694          """
    695          returns effective prefix for given namespaceURI or raises IndexError
    696          if this cannot be found"""
    697          for prefix, uri in self.namespaces.items():
    698              if uri == namespaceURI:
    699                  return prefix
    700          raise IndexError(u'NamespaceURI %r not found.' % namespaceURI)
    701
    702 -    def __str__(self):
    703          return u"<cssutils.util.%s object parentStyleSheet=%r namespaces=%r "\
    704                 u"at 0x%x>" % (
    705                  self.__class__.__name__, str(self.parentStyleSheet),
    706                  self.namespaces, id(self))
    707
    708
    709 -class _SimpleNamespaces(_Namespaces):
    710      """
    711      namespaces used in objects like Selector as long as they are not connected
    712      to a CSSStyleSheet
    713      """
    714 -    def __init__(self, *args):
    715          self.__namespaces = dict(*args)
    716
    717 -    def __setitem__(self, prefix, namespaceURI):
    718          self.__namespaces[prefix] = namespaceURI
    719
    720      namespaces = property(lambda self: self.__namespaces,
    721                            doc=u'Dict Wrapper for self.sheets @namespace rules.')
    722
    723 -    def __str__(self):
    724          return u"<cssutils.util.%s object namespaces=%r at 0x%x>" % (
    725                  self.__class__.__name__, self.namespaces, id(self))
    726