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

# Source Code for Module cssutils.serialize

      1  #!/usr/bin/env python
      2  # -*- coding: utf-8 -*-
      3  """serializer classes for CSS classes
      4
      5  """
      6  __all__ = ['CSSSerializer']
      7  __docformat__ = 'restructuredtext'
      8  __author__ = '$LastChangedBy: cthedot $'
      9  __date__ = '$LastChangedDate: 2007-12-29 17:13:39 +0100 (Sa, 29 Dez 2007) $'
     10  __version__ = '$LastChangedRevision: 760 $'
     11
     12  import codecs
     13  import re
     14  import cssutils
     15  import util
     16
     17 -def _escapecss(e):
     18      """
     19      Escapes characters not allowed in the current encoding the CSS way
     20      with a backslash followed by a uppercase hex code point
     21
     22      E.g. the german umlaut 'ä' is escaped as \E4
     23      """
     24      s = e.args[1][e.start:e.end]
     25      return u''.join([ur'\%s ' % str(hex(ord(x)))[2:] # remove 0x from hex
     26                       .upper() for x in s]), e.end
     27
     28  codecs.register_error('escapecss', _escapecss)
     29
     30
     31 -class Preferences(object):
     32      """
     33      controls output of CSSSerializer
     34
     35      defaultAtKeyword = True
     36          Should the literal @keyword from src CSS be used or the default
     37          form, e.g. if ``True``: ``@import`` else: ``@i\mport``
     38      defaultPropertyName = True
     39          Should the normalized propertyname be used or the one given in
     40          the src file, e.g. if ``True``: ``color`` else: ``c\olor``
     41
     42          Only used if ``keepAllProperties==False``.
     43
     44      importHrefFormat = None
     45          Uses hreftype if ``None`` or explicit ``'string'`` or ``'uri'``
     46      indent = 4 * ' '
     47          Indentation of e.g Properties inside a CSSStyleDeclaration
     48
     49      keepAllProperties = True
     50          If ``True`` all properties set in the original CSSStylesheet
     51          are kept meaning even properties set twice with the exact same
     52          same name are kept!
     53
     54      defaultAtKeyword=%r,
     55      defaultPropertyName=%r,
     56      importHrefFormat=%r,
     57      indent=%r,
     58      keepAllProperties=%r,
     59      keepComments=%r,
     60      keepEmptyRules=%r,
     61      lineNumbers=%r,
     62      lineSeparator=%r,
     63      listItemSpacer=%r,
     64      omitLastSemicolon=%r,
     65      paranthesisSpacer=%r,
     66      propertyNameSpacer=%r,
     67      validOnly=%r,
     68      wellformedOnly=%r,
     69
     70
     71      keepComments = True
     72          If ``False`` removes all CSSComments
     73      keepEmptyRules = False
     74          defines if empty rules like e.g. ``a {}`` are kept in the resulting
     75          serialized sheet
     76
     77      lineNumbers = False
     78          Only used if a complete CSSStyleSheet is serialized.
     79      lineSeparator = u'\\n'
     80          How to end a line. This may be set to e.g. u'' for serializing of
     81          CSSStyleDeclarations usable in HTML style attribute.
     82      listItemSpacer = u' '
     83          string which is used in ``css.SelectorList``, ``css.CSSValue`` and
     84          ``stylesheets.MediaList`` after the comma
     85      omitLastSemicolon = True
     86          If ``True`` omits ; after last property of CSSStyleDeclaration
     87      paranthesisSpacer = u' '
     88          string which is used before an opening paranthesis like in a
     89          ``css.CSSMediaRule`` or ``css.CSSStyleRule``
     90      propertyNameSpacer = u' '
     91          string which is used after a Property name colon
     92
     93      validOnly = False (**not anywhere used yet**)
     94          if True only valid (Properties or Rules) are kept
     95
     96          A Property is valid if it is a known Property with a valid value.
     97          Currently CSS 2.1 values as defined in cssproperties.py would be
     98          valid.
     99
    100      wellformedOnly = True (**not anywhere used yet**)
    101          only wellformed properties and rules are kept
    102
    103      **DEPRECATED**: removeInvalid = True
    104          Omits invalid rules, replaced by ``validOnly`` which will be used
    105          more cases
    106
    107      """
    108 -    def __init__(self, **initials):
    109          """
    110          Always use named instead of positional parameters
    111          """
    112          self.useDefaults()
    113
    114          for key, value in initials.items():
    115              if value:
    116                  self.__setattr__(key, value)
    117
    118 -    def useDefaults(self):
    119          "reset all preference options to the default value"
    120          self.defaultAtKeyword = True
    121          self.defaultPropertyName = True
    122          self.importHrefFormat = None
    123          self.indent = 4 * u' '
    124          self.keepAllProperties = True
    125          self.keepComments = True
    126          self.keepEmptyRules = False
    127          self.lineNumbers = False
    128          self.lineSeparator = u'\n'
    129          self.listItemSpacer = u' '
    130          self.omitLastSemicolon = True
    131          self.paranthesisSpacer = u' '
    132          self.propertyNameSpacer = u' '
    133          self.validOnly = False
    134          self.wellformedOnly = True
    135          # DEPRECATED
    136          self.removeInvalid = True
    137
    138 -    def useMinified(self):
    139          """
    140          sets options to achive a minified stylesheet
    141
    142          you may want to set preferences with this convinience method
    143          and set settings you want adjusted afterwards
    144          """
    145          self.importHrefFormat = 'string'
    146          self.indent = u''
    147          self.keepComments = False
    148          self.keepEmptyRules = False
    149          self.lineNumbers = False
    150          self.lineSeparator = u''
    151          self.listItemSpacer = u''
    152          self.omitLastSemicolon = True
    153          self.paranthesisSpacer = u''
    154          self.propertyNameSpacer = u''
    155          self.validOnly = False
    156          self.wellformedOnly = True
    157
    158 -    def __repr__(self):
    159          return u"cssutils.css.%s(%s)" % (self.__class__.__name__,
    160              u', '.join(['\n    %s=%r' % (p, self.__getattribute__(p)) for p in self.__dict__]
    161                  ))
    162
    163 -    def __str__(self):
    164          return u"<cssutils.css.%s object %s at 0x%x" % (self.__class__.__name__,
    165              u' '.join(['%s=%r' % (p, self.__getattribute__(p)) for p in self.__dict__]
    166                  ),
    167                  id(self))
    168
    169
    170 -class CSSSerializer(object):
    171      """
    172      Methods to serialize a CSSStylesheet and its parts
    173
    174      To use your own serializing method the easiest is to subclass CSS
    175      Serializer and overwrite the methods you like to customize.
    176      """
    177      __notinurimatcher = re.compile(ur'''.*?[\)\s\;]''', re.U).match
    178      # chars not in URI without quotes around
    179
    180 -    def __init__(self, prefs=None):
    181          """
    182          prefs
    183              instance of Preferences
    184          """
    185          if not prefs:
    186              prefs = Preferences()
    187          self.prefs = prefs
    188          self._level = 0 # current nesting level
    189
    190 -    def _serialize(self, text):
    191          if self.prefs.lineNumbers:
    192              pad = len(str(text.count(self.prefs.lineSeparator)+1))
    193              out = []
    194              for i, line in enumerate(text.split(self.prefs.lineSeparator)):
    195                  out.append((u'%*i: %s') % (pad, i+1, line))
    196              text = self.prefs.lineSeparator.join(out)
    197          return text
    198
    199 -    def _noinvalids(self, x):
    200          # DEPRECATED: REMOVE!
    201          if self.prefs.removeInvalid and \
    202             hasattr(x, 'valid') and not x.valid:
    203              return True
    204          else:
    205              return False
    206
    207 -    def _valid(self, x):
    208          """
    209          checks if valid items only and if yes it item is valid
    210          """
    211          return not self.prefs.validOnly or (self.prefs.validOnly and
    212                                              hasattr(x, 'valid') and
    213                                              x.valid)
    214
    215 -    def _wellformed(self, x):
    216          """
    217          checks if wellformed items only and if yes it item is wellformed
    218          """
    219          return self.prefs.wellformedOnly and hasattr(x, 'wellformed') and\
    220                 x.wellformed
    221
    222 -    def _escapestring(self, s, delim=u'"'):
    223          """
    224          escapes delim charaters in string s with \delim
    225          """
    226          return s.replace(delim, u'\\%s' % delim)
    227
    228 -    def _getatkeyword(self, rule, default):
    229          """
    230          used by all @rules to get the keyword used
    231          dependent on prefs setting defaultAtKeyword
    232          """
    233          if self.prefs.defaultAtKeyword:
    234              return default
    235          else:
    236              return rule.atkeyword
    237
    238 -    def _getpropertyname(self, property, actual):
    239          """
    240          used by all styledeclarations to get the propertyname used
    241          dependent on prefs setting defaultPropertyName
    242          """
    243          if self.prefs.defaultPropertyName and \
    244             not self.prefs.keepAllProperties:
    245              return property.normalname
    246          else:
    247              return actual
    248
    249 -    def _indentblock(self, text, level):
    250          """
    251          indent a block like a CSSStyleDeclaration to the given level
    252          which may be higher than self._level (e.g. for CSSStyleDeclaration)
    253          """
    254          if not self.prefs.lineSeparator:
    255              return text
    256          return self.prefs.lineSeparator.join(
    257              [u'%s%s' % (level * self.prefs.indent, line)
    258                  for line in text.split(self.prefs.lineSeparator)]
    259          )
    260
    261 -    def _uri(self, uri):
    262          """
    263          returns uri enclosed in " if necessary
    264          """
    265          if CSSSerializer.__notinurimatcher(uri):
    266              return '"%s"' % uri
    267          else:
    268              return uri
    269
    270 -    def do_stylesheets_mediaquery(self, mediaquery):
    271          """
    272          a single media used in medialist
    273          """
    274          if len(mediaquery.seq) == 0:
    275              return u''
    276          else:
    277              out = []
    278              for part in mediaquery.seq:
    279                  if isinstance(part, cssutils.css.Property): # Property
    280                      out.append(u'(%s)' % part.cssText)
    281                  elif hasattr(part, 'cssText'): # comments
    282                      out.append(part.cssText)
    283                  else:
    284                      # TODO: media queries!
    285                      out.append(part)
    286              return u' '.join(out)
    287
    288 -    def do_stylesheets_medialist(self, medialist):
    289          """
    290          comma-separated list of media, default is 'all'
    291
    292          If "all" is in the list, every other media *except* "handheld" will
    293          be stripped. This is because how Opera handles CSS for PDAs.
    294          """
    295          if len(medialist) == 0:
    296              return u'all'
    297          else:
    298              sep = u',%s' % self.prefs.listItemSpacer
    299              return sep.join(
    300                          (mq.mediaText for mq in medialist))
    301
    302 -    def do_CSSStyleSheet(self, stylesheet):
    303          out = []
    304          for rule in stylesheet.cssRules:
    305              cssText = rule.cssText
    306              if cssText:
    307                  out.append(cssText)
    308          text = self._serialize(self.prefs.lineSeparator.join(out))
    309
    310          # get encoding of sheet, defaults to UTF-8
    311          try:
    312              encoding = stylesheet.cssRules[0].encoding
    313          except (IndexError, AttributeError):
    314              encoding = 'UTF-8'
    315
    316          return text.encode(encoding, 'escapecss')
    317
    318 -    def do_CSSComment(self, rule):
    319          """
    320          serializes CSSComment which consists only of commentText
    321          """
    322          if self.prefs.keepComments and rule._cssText:
    323              return rule._cssText
    324          else:
    325              return u''
    326
    327 -    def do_CSSCharsetRule(self, rule):
    328          """
    329          serializes CSSCharsetRule
    330          encoding: string
    331
    332          always @charset "encoding";
    333          no comments or other things allowed!
    334          """
    335          if not rule.encoding or self._noinvalids(rule):
    336              return u''
    337          return u'@charset "%s";' % self._escapestring(rule.encoding)
    338
    339 -    def do_CSSFontFaceRule(self, rule):
    340          """
    341          serializes CSSFontFaceRule
    342
    343          style
    344              CSSStyleDeclaration
    345
    346          + CSSComments
    347          """
    348          self._level += 1
    349          try:
    350              styleText = self.do_css_CSSStyleDeclaration(rule.style)
    351          finally:
    352              self._level -= 1
    353
    354          if not styleText or self._noinvalids(rule):
    355              return u''
    356
    357          before = []
    358          for x in rule.seq:
    359              if hasattr(x, 'cssText'):
    360                  before.append(x.cssText)
    361              else:
    362                  # TODO: resolve
    363                  raise SyntaxErr('serializing CSSFontFaceRule: unexpected %r' % x)
    364          if before:
    365              before = u' '.join(before).strip()
    366              if before:
    367                  before = u' %s' % before
    368          else:
    369              before = u''
    370
    371          return u'%s%s {%s%s%s%s}' % (
    372              self._getatkeyword(rule, u'@font-face'),
    373              before,
    374              self.prefs.lineSeparator,
    375              self._indentblock(styleText, self._level + 1),
    376              self.prefs.lineSeparator,
    377              (self._level + 1) * self.prefs.indent
    378              )
    379
    380 -    def do_CSSImportRule(self, rule):
    381          """
    382          serializes CSSImportRule
    383
    384          href
    385              string
    386          hreftype
    387              'uri' or 'string'
    388          media
    389              cssutils.stylesheets.medialist.MediaList
    390
    391          + CSSComments
    392          """
    393          if not rule.href or self._noinvalids(rule):
    394              return u''
    395          out = [u'%s ' % self._getatkeyword(rule, u'@import')]
    396          for part in rule.seq:
    397              if rule.href == part:
    398                  if self.prefs.importHrefFormat == 'uri':
    399                      out.append(u'url(%s)' % self._uri(part))
    400                  elif self.prefs.importHrefFormat == 'string' or \
    401                     rule.hreftype == 'string':
    402                      out.append(u'"%s"' % self._escapestring(part))
    403                  else:
    404                      out.append(u'url(%s)' % self._uri(part))
    405              elif isinstance(
    406                    part, cssutils.stylesheets.medialist.MediaList):
    407                  mediaText = self.do_stylesheets_medialist(part)#.strip()
    408                  if mediaText and not mediaText == u'all':
    409                      out.append(u' %s' % mediaText)
    410              elif hasattr(part, 'cssText'): # comments
    411                  out.append(part.cssText)
    412          return u'%s;' % u''.join(out)
    413
    414 -    def do_CSSNamespaceRule(self, rule):
    415          """
    416          serializes CSSNamespaceRule
    417
    418          uri
    419              string
    420          prefix
    421              string
    422
    423          + CSSComments
    424          """
    425          if not rule.namespaceURI or self._noinvalids(rule):
    426              return u''
    427
    428          out = [u'%s' % self._getatkeyword(rule, u'@namespace')]
    429          for part in rule.seq:
    430              if rule.prefix == part and part != u'':
    431                  out.append(u' %s' % part)
    432              elif rule.namespaceURI == part:
    433                  out.append(u' "%s"' % self._escapestring(part))
    434              elif hasattr(part, 'cssText'): # comments
    435                  out.append(part.cssText)
    436          return u'%s;' % u''.join(out)
    437
    438 -    def do_CSSMediaRule(self, rule):
    439          """
    440          serializes CSSMediaRule
    441
    442          + CSSComments
    443          """
    444          rulesout = []
    445          for r in rule.cssRules:
    446              rtext = r.cssText
    447              if rtext:
    448                  # indent each line of cssText
    449                  rulesout.append(self._indentblock(rtext, self._level + 1))
    450                  rulesout.append(self.prefs.lineSeparator)
    451
    452          if not self.prefs.keepEmptyRules and not u''.join(rulesout).strip() or\
    453             self._noinvalids(rule.media):
    454              return u''
    455
    456  #        if len(rule.cssRules) == 0 and not self.prefs.keepEmptyRules or\
    457  #           self._noinvalids(rule.media):
    458  #            return u''
    459          mediaText = self.do_stylesheets_medialist(rule.media)#.strip()
    460          out = [u'%s %s%s{%s' % (self._getatkeyword(rule, u'@media'),
    461                                 mediaText,
    462                                 self.prefs.paranthesisSpacer,
    463                                 self.prefs.lineSeparator)]
    464          out.extend(rulesout)
    465          return u'%s%s}' % (u''.join(out),
    466                             (self._level + 1) * self.prefs.indent)
    467
    468 -    def do_CSSPageRule(self, rule):
    469          """
    470          serializes CSSPageRule
    471
    472          selectorText
    473              string
    474          style
    475              CSSStyleDeclaration
    476
    477          + CSSComments
    478          """
    479          self._level += 1
    480          try:
    481              styleText = self.do_css_CSSStyleDeclaration(rule.style)
    482          finally:
    483              self._level -= 1
    484
    485          if not styleText or self._noinvalids(rule):
    486              return u''
    487
    488          return u'%s%s {%s%s%s%s}' % (
    489              self._getatkeyword(rule, u'@page'),
    490              self.do_pageselector(rule.seq),
    491              self.prefs.lineSeparator,
    492              self._indentblock(styleText, self._level + 1),
    493              self.prefs.lineSeparator,
    494              (self._level + 1) * self.prefs.indent
    495              )
    496
    497 -    def do_pageselector(self, seq):
    498          """
    499          a selector of a CSSPageRule including comments
    500          """
    501          if len(seq) == 0 or self._noinvalids(seq):
    502              return u''
    503          else:
    504              out = []
    505              for part in seq:
    506                  if hasattr(part, 'cssText'):
    507                      out.append(part.cssText)
    508                  else:
    509                      out.append(part)
    510              return u' %s' % u''.join(out)
    511
    512 -    def do_CSSUnknownRule(self, rule):
    513          """
    514          serializes CSSUnknownRule
    515          anything until ";" or "{...}"
    516          + CSSComments
    517          """
    518          if rule.atkeyword and not self._noinvalids(rule):
    519              out = [u'%s' % rule.atkeyword]
    520              for part in rule.seq:
    521                  if isinstance(part, cssutils.css.csscomment.CSSComment):
    522                      if self.prefs.keepComments:
    523                          out.append(part.cssText)
    524                  else:
    525                      out.append(part)
    526              if not (out[-1].endswith(u';') or out[-1].endswith(u'}')):
    527                  out.append(u';')
    528              return u''.join(out)
    529          else:
    530              return u''
    531
    532 -    def do_CSSStyleRule(self, rule):
    533          """
    534          serializes CSSStyleRule
    535
    536          selectorList
    537          style
    538
    539          + CSSComments
    540          """
    541          selectorText = self.do_css_SelectorList(rule.selectorList)
    542          if not selectorText or self._noinvalids(rule):
    543              return u''
    544          self._level += 1
    545          styleText = u''
    546          try:
    547              styleText = self.do_css_CSSStyleDeclaration(rule.style)
    548          finally:
    549              self._level -= 1
    550          if not styleText:
    551                  if self.prefs.keepEmptyRules:
    552                      return u'%s%s{}' % (selectorText,
    553                                          self.prefs.paranthesisSpacer)
    554          else:
    555              return u'%s%s{%s%s%s%s}' % (
    556                  selectorText,
    557                  self.prefs.paranthesisSpacer,
    558                  self.prefs.lineSeparator,
    559                  self._indentblock(styleText, self._level + 1),
    560                  self.prefs.lineSeparator,
    561                  (self._level + 1) * self.prefs.indent)
    562
    563 -    def do_css_SelectorList(self, selectorlist):
    564          """
    565          comma-separated list of Selectors
    566          """
    567          if selectorlist.seq and self._wellformed(selectorlist) and\
    568                                  self._valid(selectorlist):
    569              out = []
    570              sep = u',%s' % self.prefs.listItemSpacer
    571              for part in selectorlist.seq:
    572                  if hasattr(part, 'cssText'):
    573                      out.append(part.cssText)
    574  #                elif u',' == part:
    575  #                    out.append(sep)
    576                  elif isinstance(part, cssutils.css.Selector):
    577                      out.append(self.do_css_Selector(part))
    578                  else:
    579                      out.append(part) # ?
    580              return sep.join(out)
    581          else:
    582              return u''
    583
    584
    585 -    def do_css_Selector(self, selector):
    586          """
    587          a single selector including comments
    588
    589          TODO: style combinators like + >
    590          """
    591          if selector.seq and self._wellformed(selector) and\
    592                                  self._valid(selector):
    593              out = []
    594              for part in selector.seq:
    595                  if hasattr(part, 'cssText'):
    596                      out.append(part.cssText)
    597                  else:
    598                      if type(part) == dict:
    599                          out.append(part['value'])
    600                      else:
    601                          out.append(part)
    602              return u''.join(out)
    603          else:
    604              return u''
    605
    606 -    def do_css_CSSStyleDeclaration(self, style, separator=None):
    607          """
    608          Style declaration of CSSStyleRule
    609          """
    610          if len(style.seq) > 0 and self._wellformed(style) and\
    611                                  self._valid(style):
    612              if separator is None:
    613                  separator = self.prefs.lineSeparator
    614
    615              if self.prefs.keepAllProperties:
    616                  parts = style.seq
    617              else:
    618                  # find distinct names
    619                  nnames = set()
    620                  for x in style.seq:
    621                      if isinstance(x, cssutils.css.Property):
    622                          nnames.add(x.normalname)
    623                  # filter list
    624                  parts = []
    625                  for x in reversed(style.seq):
    626                      if isinstance(x, cssutils.css.Property):
    627                          if x.normalname in nnames:
    628                              parts.append(x)
    629                              nnames.remove(x.normalname)
    630                      else:
    631                          parts.append(x)
    632                  parts.reverse()
    633
    634              out = []
    635              for (i, part) in enumerate(parts):
    636                  if isinstance(part, cssutils.css.CSSComment):
    637                      # CSSComment
    638                      if self.prefs.keepComments:
    639                          out.append(part.cssText)
    640                          out.append(separator)
    641                  elif isinstance(part, cssutils.css.Property):
    642                      # PropertySimilarNameList
    643                      out.append(self.do_Property(part))
    644                      if not (self.prefs.omitLastSemicolon and i==len(parts)-1):
    645                          out.append(u';')
    646                      out.append(separator)
    647                  else:
    648                      # other?
    649                      out.append(part)
    650
    651              if out and out[-1] == separator:
    652                  del out[-1]
    653
    654              return u''.join(out)
    655
    656          else:
    657              return u''
    658
    659 -    def do_Property(self, property):
    660          """
    661          Style declaration of CSSStyleRule
    662
    663          Property has a seqs attribute which contains seq lists for
    664          name, a CSSvalue and a seq list for priority
    665          """
    666          out = []
    667          if property.seqs[0] and self._wellformed(property) and\
    668                                  self._valid(property):
    669              nameseq, cssvalue, priorityseq = property.seqs
    670
    671              #name
    672              for part in nameseq:
    673                  if hasattr(part, 'cssText'):
    674                      out.append(part.cssText)
    675                  elif property.name == part:
    676                      out.append(self._getpropertyname(property, part))
    677                  else:
    678                      out.append(part)
    679
    680              if out and (not property._mediaQuery or
    681                          property._mediaQuery and cssvalue.cssText):
    682                  # MediaQuery may consist of name only
    683                  out.append(u':')
    684                  out.append(self.prefs.propertyNameSpacer)
    685
    686              # value
    687              out.append(cssvalue.cssText)
    688
    689              # priority
    690              if out and priorityseq:
    691                  out.append(u' ')
    692                  for part in priorityseq:
    693                      if hasattr(part, 'cssText'): # comments
    694                          out.append(part.cssText)
    695                      else:
    696                          out.append(part)
    697
    698          return u''.join(out)
    699
    700 -    def do_Property_priority(self, priorityseq):
    701          """
    702          a Properties priority "!" S* "important"
    703          """
    704          out = []
    705          for part in priorityseq:
    706              if hasattr(part, 'cssText'): # comments
    707                  out.append(u' ')
    708                  out.append(part.cssText)
    709                  out.append(u' ')
    710              else:
    711                  out.append(part)
    712          return u''.join(out).strip()
    713
    714 -    def do_css_CSSValue(self, cssvalue):
    715          """
    716          serializes a CSSValue
    717          """
    718          if not cssvalue:
    719              return u''
    720          else:
    721              sep = u',%s' % self.prefs.listItemSpacer
    722              out = []
    723              for part in cssvalue.seq:
    724                  if hasattr(part, 'cssText'):
    725                      # comments or CSSValue if a CSSValueList
    726                      out.append(part.cssText)
    727                  elif isinstance(part, basestring) and part == u',':
    728                      out.append(sep)
    729                  else:
    730                      out.append(part)
    731              return (u''.join(out)).strip()
    732