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

# Source Code for Module cssutils.serialize

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