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