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