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