Package cssutils :: Package css :: Module cssstylesheet
[hide private]
[frames] | no frames]

Source Code for Module cssutils.css.cssstylesheet

  1  """ 
  2  CSSStyleSheet implements DOM Level 2 CSS CSSStyleSheet. 
  3   
  4  Partly also: 
  5      - http://dev.w3.org/csswg/cssom/#the-cssstylesheet 
  6      - http://www.w3.org/TR/2006/WD-css3-namespace-20060828/ 
  7   
  8  TODO: 
  9      - ownerRule and ownerNode 
 10  """ 
 11  __all__ = ['CSSStyleSheet'] 
 12  __docformat__ = 'restructuredtext' 
 13  __version__ = '$Id: cssstylesheet.py 1319 2008-06-29 20:51:55Z cthedot $' 
 14   
 15  import xml.dom 
 16  import cssutils.stylesheets 
 17  from cssutils.util import _Namespaces, _SimpleNamespaces, _readUrl 
 18  from cssutils.helper import Deprecated 
19 20 -class CSSStyleSheet(cssutils.stylesheets.StyleSheet):
21 """ 22 The CSSStyleSheet interface represents a CSS style sheet. 23 24 Properties 25 ========== 26 CSSOM 27 ----- 28 cssRules 29 of type CSSRuleList, (DOM readonly) 30 encoding 31 reflects the encoding of an @charset rule or 'utf-8' (default) 32 if set to ``None`` 33 ownerRule 34 of type CSSRule, readonly. If this sheet is imported this is a ref 35 to the @import rule that imports it. 36 37 Inherits properties from stylesheet.StyleSheet 38 39 cssutils 40 -------- 41 cssText: string 42 a textual representation of the stylesheet 43 namespaces 44 reflects set @namespace rules of this rule. 45 A dict of {prefix: namespaceURI} mapping. 46 47 Format 48 ====== 49 stylesheet 50 : [ CHARSET_SYM S* STRING S* ';' ]? 51 [S|CDO|CDC]* [ import [S|CDO|CDC]* ]* 52 [ namespace [S|CDO|CDC]* ]* # according to @namespace WD 53 [ [ ruleset | media | page ] [S|CDO|CDC]* ]* 54 """
55 - def __init__(self, href=None, media=None, title=u'', disabled=None, 56 ownerNode=None, parentStyleSheet=None, readonly=False, 57 ownerRule=None):
58 """ 59 init parameters are the same as for stylesheets.StyleSheet 60 """ 61 super(CSSStyleSheet, self).__init__( 62 'text/css', href, media, title, disabled, 63 ownerNode, parentStyleSheet) 64 65 self._ownerRule = ownerRule 66 self.cssRules = cssutils.css.CSSRuleList() 67 self.cssRules.append = self.insertRule 68 self.cssRules.extend = self.insertRule 69 self._namespaces = _Namespaces(parentStyleSheet=self) 70 self._readonly = readonly 71 72 # used only during setting cssText by parse*() 73 self.__encodingOverride = None 74 self._fetcher = None
75
76 - def __iter__(self):
77 "generator which iterates over cssRules." 78 for rule in self.cssRules: 79 yield rule
80
81 - def _cleanNamespaces(self):
82 "removes all namespace rules with same namespaceURI but last one set" 83 rules = self.cssRules 84 namespaceitems = self.namespaces.items() 85 i = 0 86 while i < len(rules): 87 rule = rules[i] 88 if rule.type == rule.NAMESPACE_RULE and \ 89 (rule.prefix, rule.namespaceURI) not in namespaceitems: 90 self.deleteRule(i) 91 else: 92 i += 1
93
94 - def _getUsedURIs(self):
95 "returns set of URIs used in the sheet" 96 useduris = set() 97 for r1 in self: 98 if r1.STYLE_RULE == r1.type: 99 useduris.update(r1.selectorList._getUsedUris()) 100 elif r1.MEDIA_RULE == r1.type: 101 for r2 in r1: 102 if r2.type == r2.STYLE_RULE: 103 useduris.update(r2.selectorList._getUsedUris()) 104 return useduris
105
106 - def _getCssText(self):
107 return cssutils.ser.do_CSSStyleSheet(self)
108
109 - def _setCssText(self, cssText):
110 """ 111 (cssutils) 112 Parses ``cssText`` and overwrites the whole stylesheet. 113 114 :param cssText: 115 a parseable string or a tuple of (cssText, dict-of-namespaces) 116 :Exceptions: 117 - `NAMESPACE_ERR`: 118 If a namespace prefix is found which is not declared. 119 - `NO_MODIFICATION_ALLOWED_ERR`: (self) 120 Raised if the rule is readonly. 121 - `SYNTAX_ERR`: 122 Raised if the specified CSS string value has a syntax error and 123 is unparsable. 124 """ 125 self._checkReadonly() 126 127 cssText, namespaces = self._splitNamespacesOff(cssText) 128 if not namespaces: 129 namespaces = _SimpleNamespaces() 130 131 tokenizer = self._tokenize2(cssText) 132 newseq = [] #cssutils.css.CSSRuleList() 133 134 # for closures: must be a mutable 135 new = {'encoding': None, # needed for setting encoding of @import rules 136 'namespaces': namespaces} 137 def S(expected, seq, token, tokenizer=None): 138 # @charset must be at absolute beginning of style sheet 139 if expected == 0: 140 return 1 141 else: 142 return expected
143 144 def COMMENT(expected, seq, token, tokenizer=None): 145 "special: sets parent*" 146 comment = cssutils.css.CSSComment([token], 147 parentStyleSheet=self.parentStyleSheet) 148 seq.append(comment) 149 return expected
150 151 def charsetrule(expected, seq, token, tokenizer): 152 rule = cssutils.css.CSSCharsetRule(parentStyleSheet=self) 153 rule.cssText = self._tokensupto2(tokenizer, token) 154 if expected > 0 or len(seq) > 0: 155 self._log.error( 156 u'CSSStylesheet: CSSCharsetRule only allowed at beginning of stylesheet.', 157 token, xml.dom.HierarchyRequestErr) 158 else: 159 if rule.wellformed: 160 seq.append(rule) 161 new['encoding'] = rule.encoding 162 return 1 163 164 def importrule(expected, seq, token, tokenizer): 165 rule = cssutils.css.CSSImportRule(parentStyleSheet=self) 166 #rule._parentEncoding = new['encoding'] # set temporarily 167 168 # set temporarily as used by _resolveImport 169 self.__newEncoding = new['encoding'] 170 171 rule.cssText = self._tokensupto2(tokenizer, token) 172 if expected > 1: 173 self._log.error( 174 u'CSSStylesheet: CSSImportRule not allowed here.', 175 token, xml.dom.HierarchyRequestErr) 176 else: 177 if rule.wellformed: 178 #del rule._parentEncoding # remove as later it is read from this sheet! 179 seq.append(rule) 180 181 # remove as only used temporarily 182 del self.__newEncoding 183 184 return 1 185 186 def namespacerule(expected, seq, token, tokenizer): 187 rule = cssutils.css.CSSNamespaceRule( 188 cssText=self._tokensupto2(tokenizer, token), 189 parentStyleSheet=self) 190 if expected > 2: 191 self._log.error( 192 u'CSSStylesheet: CSSNamespaceRule not allowed here.', 193 token, xml.dom.HierarchyRequestErr) 194 else: 195 if rule.wellformed: 196 seq.append(rule) 197 # temporary namespaces given to CSSStyleRule and @media 198 new['namespaces'][rule.prefix] = rule.namespaceURI 199 return 2 200 201 def fontfacerule(expected, seq, token, tokenizer): 202 rule = cssutils.css.CSSFontFaceRule(parentStyleSheet=self) 203 rule.cssText = self._tokensupto2(tokenizer, token) 204 if rule.wellformed: 205 seq.append(rule) 206 return 3 207 208 def mediarule(expected, seq, token, tokenizer): 209 rule = cssutils.css.CSSMediaRule() 210 rule.cssText = (self._tokensupto2(tokenizer, token), 211 new['namespaces']) 212 if rule.wellformed: 213 rule._parentStyleSheet=self 214 for r in rule: 215 r._parentStyleSheet=self 216 seq.append(rule) 217 return 3 218 219 def pagerule(expected, seq, token, tokenizer): 220 rule = cssutils.css.CSSPageRule(parentStyleSheet=self) 221 rule.cssText = self._tokensupto2(tokenizer, token) 222 if rule.wellformed: 223 seq.append(rule) 224 return 3 225 226 def unknownrule(expected, seq, token, tokenizer): 227 rule = cssutils.css.CSSUnknownRule(parentStyleSheet=self) 228 rule.cssText = self._tokensupto2(tokenizer, token) 229 if rule.wellformed: 230 seq.append(rule) 231 return expected 232 233 def ruleset(expected, seq, token, tokenizer): 234 rule = cssutils.css.CSSStyleRule() 235 rule.cssText = (self._tokensupto2(tokenizer, token), 236 new['namespaces']) 237 if rule.wellformed: 238 rule._parentStyleSheet=self 239 seq.append(rule) 240 return 3 241 242 # expected: 243 # ['CHARSET', 'IMPORT', 'NAMESPACE', ('PAGE', 'MEDIA', ruleset)] 244 wellformed, expected = self._parse(0, newseq, tokenizer, 245 {'S': S, 246 'COMMENT': COMMENT, 247 'CDO': lambda *ignored: None, 248 'CDC': lambda *ignored: None, 249 'CHARSET_SYM': charsetrule, 250 'FONT_FACE_SYM': fontfacerule, 251 'IMPORT_SYM': importrule, 252 'NAMESPACE_SYM': namespacerule, 253 'PAGE_SYM': pagerule, 254 'MEDIA_SYM': mediarule, 255 'ATKEYWORD': unknownrule 256 }, 257 default=ruleset) 258 259 if wellformed: 260 del self.cssRules[:] 261 for rule in newseq: 262 self.insertRule(rule, _clean=False) 263 self._cleanNamespaces() 264 265 cssText = property(_getCssText, _setCssText, 266 "(cssutils) a textual representation of the stylesheet") 267
268 - def _setCssTextWithEncodingOverride(self, cssText, encodingOverride=None):
269 """Set cssText but use __encodingOverride to overwrite detected 270 encoding. This is only used by @import during setting of cssText. 271 In all other cases __encodingOverride is None""" 272 if encodingOverride: 273 # encoding during @import resolve, is used again during parse! 274 self.__encodingOverride = encodingOverride 275 276 self.cssText = cssText 277 278 if encodingOverride: 279 # set explicit encodingOverride 280 self.encoding = self.__encodingOverride 281 self.__encodingOverride = None
282
283 - def _resolveImport(self, url):
284 """Read (encoding, cssText) from ``url`` for @import sheets""" 285 try: 286 # only available during parse of a complete sheet 287 parentEncoding = self.__newEncoding 288 except AttributeError: 289 # or check if @charset explicitly set 290 try: 291 # explicit cssRules[0] and not the default encoding UTF-8 292 # but in that case None 293 parentEncoding = self.cssRules[0].encoding 294 except (IndexError, AttributeError): 295 parentEncoding = None 296 297 return _readUrl(url, fetcher=self._fetcher, 298 overrideEncoding=self.__encodingOverride, 299 parentEncoding=parentEncoding)
300
301 - def _setFetcher(self, fetcher=None):
302 """sets @import URL loader, if None the default is used""" 303 self._fetcher = fetcher
304
305 - def _setEncoding(self, encoding):
306 """ 307 sets encoding of charset rule if present or inserts new charsetrule 308 with given encoding. If encoding if None removes charsetrule if 309 present. 310 """ 311 try: 312 rule = self.cssRules[0] 313 except IndexError: 314 rule = None 315 if rule and rule.CHARSET_RULE == rule.type: 316 if encoding: 317 rule.encoding = encoding 318 else: 319 self.deleteRule(0) 320 elif encoding: 321 self.insertRule(cssutils.css.CSSCharsetRule(encoding=encoding), 0)
322
323 - def _getEncoding(self):
324 "return encoding if @charset rule if given or default of 'utf-8'" 325 try: 326 return self.cssRules[0].encoding 327 except (IndexError, AttributeError): 328 return 'utf-8'
329 330 encoding = property(_getEncoding, _setEncoding, 331 "(cssutils) reflects the encoding of an @charset rule or 'UTF-8' (default) if set to ``None``") 332 333 namespaces = property(lambda self: self._namespaces, 334 doc="Namespaces used in this CSSStyleSheet.") 335
336 - def add(self, rule):
337 """ 338 Adds rule to stylesheet at appropriate position. 339 Same as ``sheet.insertRule(rule, inOrder=True)``. 340 """ 341 return self.insertRule(rule, index=None, inOrder=True)
342
343 - def deleteRule(self, index):
344 """ 345 Used to delete a rule from the style sheet. 346 347 :param index: 348 of the rule to remove in the StyleSheet's rule list. For an 349 index < 0 **no** INDEX_SIZE_ERR is raised but rules for 350 normal Python lists are used. E.g. ``deleteRule(-1)`` removes 351 the last rule in cssRules. 352 :Exceptions: 353 - `INDEX_SIZE_ERR`: (self) 354 Raised if the specified index does not correspond to a rule in 355 the style sheet's rule list. 356 - `NAMESPACE_ERR`: (self) 357 Raised if removing this rule would result in an invalid StyleSheet 358 - `NO_MODIFICATION_ALLOWED_ERR`: (self) 359 Raised if this style sheet is readonly. 360 """ 361 self._checkReadonly() 362 363 try: 364 rule = self.cssRules[index] 365 except IndexError: 366 raise xml.dom.IndexSizeErr( 367 u'CSSStyleSheet: %s is not a valid index in the rulelist of length %i' % ( 368 index, self.cssRules.length)) 369 else: 370 if rule.type == rule.NAMESPACE_RULE: 371 # check all namespacerules if used 372 uris = [r.namespaceURI for r in self if r.type == r.NAMESPACE_RULE] 373 useduris = self._getUsedURIs() 374 if rule.namespaceURI in useduris and\ 375 uris.count(rule.namespaceURI) == 1: 376 raise xml.dom.NoModificationAllowedErr( 377 u'CSSStyleSheet: NamespaceURI defined in this rule is used, cannot remove.') 378 return 379 380 rule._parentStyleSheet = None # detach 381 del self.cssRules[index] # delete from StyleSheet
382
383 - def insertRule(self, rule, index=None, inOrder=False, _clean=True):
384 """ 385 Used to insert a new rule into the style sheet. The new rule now 386 becomes part of the cascade. 387 388 :Parameters: 389 rule 390 a parsable DOMString, in cssutils also a CSSRule or a 391 CSSRuleList 392 index 393 of the rule before the new rule will be inserted. 394 If the specified index is equal to the length of the 395 StyleSheet's rule collection, the rule will be added to the end 396 of the style sheet. 397 If index is not given or None rule will be appended to rule 398 list. 399 inOrder 400 if True the rule will be put to a proper location while 401 ignoring index but without raising HIERARCHY_REQUEST_ERR. 402 The resulting index is returned nevertheless 403 :returns: the index within the stylesheet's rule collection 404 :Exceptions: 405 - `HIERARCHY_REQUEST_ERR`: (self) 406 Raised if the rule cannot be inserted at the specified index 407 e.g. if an @import rule is inserted after a standard rule set 408 or other at-rule. 409 - `INDEX_SIZE_ERR`: (self) 410 Raised if the specified index is not a valid insertion point. 411 - `NO_MODIFICATION_ALLOWED_ERR`: (self) 412 Raised if this style sheet is readonly. 413 - `SYNTAX_ERR`: (rule) 414 Raised if the specified rule has a syntax error and is 415 unparsable. 416 """ 417 self._checkReadonly() 418 419 # check position 420 if index is None: 421 index = len(self.cssRules) 422 elif index < 0 or index > self.cssRules.length: 423 raise xml.dom.IndexSizeErr( 424 u'CSSStyleSheet: Invalid index %s for CSSRuleList with a length of %s.' % ( 425 index, self.cssRules.length)) 426 return 427 428 if isinstance(rule, basestring): 429 # init a temp sheet which has the same properties as self 430 tempsheet = CSSStyleSheet(href=self.href, 431 media=self.media, 432 title=self.title, 433 parentStyleSheet=self.parentStyleSheet, 434 ownerRule=self.ownerRule) 435 tempsheet._ownerNode = self.ownerNode 436 tempsheet._fetcher = self._fetcher 437 438 # prepend encoding if in this sheet to be able to use it in 439 # @import rules encoding resolution 440 # do not add if new rule startswith "@charset" (which is exact!) 441 if not rule.startswith(u'@charset') and (self.cssRules and 442 self.cssRules[0].type == self.cssRules[0].CHARSET_RULE): 443 # rule 0 is @charset! 444 newrulescount, newruleindex = 2, 1 445 rule = self.cssRules[0].cssText + rule 446 else: 447 newrulescount, newruleindex = 1, 0 448 449 # parse the new rule(s) 450 tempsheet.cssText = (rule, self._namespaces) 451 452 if len(tempsheet.cssRules) != newrulescount or (not isinstance( 453 tempsheet.cssRules[newruleindex], cssutils.css.CSSRule)): 454 self._log.error(u'CSSStyleSheet: Not a CSSRule: %s' % rule) 455 return 456 rule = tempsheet.cssRules[newruleindex] 457 rule._parentStyleSheet = None # done later? 458 459 # TODO: 460 #tempsheet._namespaces = self._namespaces 461 462 463 elif isinstance(rule, cssutils.css.CSSRuleList): 464 # insert all rules 465 for i, r in enumerate(rule): 466 self.insertRule(r, index + i) 467 return index 468 469 if not rule.wellformed: 470 self._log.error(u'CSSStyleSheet: Invalid rules cannot be added.') 471 return 472 473 # CHECK HIERARCHY 474 # @charset 475 if rule.type == rule.CHARSET_RULE: 476 if inOrder: 477 index = 0 478 # always first and only 479 if (self.cssRules and self.cssRules[0].type == rule.CHARSET_RULE): 480 self.cssRules[0].encoding = rule.encoding 481 else: 482 self.cssRules.insert(0, rule) 483 elif index != 0 or (self.cssRules and 484 self.cssRules[0].type == rule.CHARSET_RULE): 485 self._log.error( 486 u'CSSStylesheet: @charset only allowed once at the beginning of a stylesheet.', 487 error=xml.dom.HierarchyRequestErr) 488 return 489 else: 490 self.cssRules.insert(index, rule) 491 492 # @unknown or comment 493 elif rule.type in (rule.UNKNOWN_RULE, rule.COMMENT) and not inOrder: 494 if index == 0 and self.cssRules and\ 495 self.cssRules[0].type == rule.CHARSET_RULE: 496 self._log.error( 497 u'CSSStylesheet: @charset must be the first rule.', 498 error=xml.dom.HierarchyRequestErr) 499 return 500 else: 501 self.cssRules.insert(index, rule) 502 503 # @import 504 elif rule.type == rule.IMPORT_RULE: 505 if inOrder: 506 # automatic order 507 if rule.type in (r.type for r in self): 508 # find last of this type 509 for i, r in enumerate(reversed(self.cssRules)): 510 if r.type == rule.type: 511 index = len(self.cssRules) - i 512 break 513 else: 514 # find first point to insert 515 if self.cssRules and self.cssRules[0].type in (rule.CHARSET_RULE, 516 rule.COMMENT): 517 index = 1 518 else: 519 index = 0 520 else: 521 # after @charset 522 if index == 0 and self.cssRules and\ 523 self.cssRules[0].type == rule.CHARSET_RULE: 524 self._log.error( 525 u'CSSStylesheet: Found @charset at index 0.', 526 error=xml.dom.HierarchyRequestErr) 527 return 528 # before @namespace, @page, @font-face, @media and stylerule 529 for r in self.cssRules[:index]: 530 if r.type in (r.NAMESPACE_RULE, r.MEDIA_RULE, r.PAGE_RULE, 531 r.STYLE_RULE, r.FONT_FACE_RULE): 532 self._log.error( 533 u'CSSStylesheet: Cannot insert @import here, found @namespace, @media, @page or CSSStyleRule before index %s.' % 534 index, 535 error=xml.dom.HierarchyRequestErr) 536 return 537 self.cssRules.insert(index, rule) 538 539 # @namespace 540 elif rule.type == rule.NAMESPACE_RULE: 541 if inOrder: 542 if rule.type in (r.type for r in self): 543 # find last of this type 544 for i, r in enumerate(reversed(self.cssRules)): 545 if r.type == rule.type: 546 index = len(self.cssRules) - i 547 break 548 else: 549 # find first point to insert 550 for i, r in enumerate(self.cssRules): 551 if r.type in (r.MEDIA_RULE, r.PAGE_RULE, r.STYLE_RULE, 552 r.FONT_FACE_RULE, r.UNKNOWN_RULE, r.COMMENT): 553 index = i # before these 554 break 555 else: 556 # after @charset and @import 557 for r in self.cssRules[index:]: 558 if r.type in (r.CHARSET_RULE, r.IMPORT_RULE): 559 self._log.error( 560 u'CSSStylesheet: Cannot insert @namespace here, found @charset or @import after index %s.' % 561 index, 562 error=xml.dom.HierarchyRequestErr) 563 return 564 # before @media and stylerule 565 for r in self.cssRules[:index]: 566 if r.type in (r.MEDIA_RULE, r.PAGE_RULE, r.STYLE_RULE, 567 r.FONT_FACE_RULE): 568 self._log.error( 569 u'CSSStylesheet: Cannot insert @namespace here, found @media, @page or CSSStyleRule before index %s.' % 570 index, 571 error=xml.dom.HierarchyRequestErr) 572 return 573 574 if not (rule.prefix in self.namespaces and 575 self.namespaces[rule.prefix] == rule.namespaceURI): 576 # no doublettes 577 self.cssRules.insert(index, rule) 578 if _clean: 579 self._cleanNamespaces() 580 581 # all other where order is not important 582 else: 583 if inOrder: 584 # simply add to end as no specific order 585 self.cssRules.append(rule) 586 index = len(self.cssRules) - 1 587 else: 588 for r in self.cssRules[index:]: 589 if r.type in (r.CHARSET_RULE, r.IMPORT_RULE, r.NAMESPACE_RULE): 590 self._log.error( 591 u'CSSStylesheet: Cannot insert rule here, found @charset, @import or @namespace before index %s.' % 592 index, 593 error=xml.dom.HierarchyRequestErr) 594 return 595 self.cssRules.insert(index, rule) 596 597 # post settings, TODO: for other rules which contain @rules 598 rule._parentStyleSheet = self 599 if rule.MEDIA_RULE == rule.type: 600 for r in rule: 601 r._parentStyleSheet = self 602 # ? 603 elif rule.IMPORT_RULE == rule.type: 604 rule.href = rule.href # try to reload stylesheet 605 606 return index
607 608 ownerRule = property(lambda self: self._ownerRule, 609 doc="(DOM attribute) NOT IMPLEMENTED YET") 610 611 @Deprecated('Use cssutils.replaceUrls(sheet, replacer) instead.')
612 - def replaceUrls(self, replacer):
613 """ 614 **EXPERIMENTAL** 615 616 Utility method to replace all ``url(urlstring)`` values in 617 ``CSSImportRules`` and ``CSSStyleDeclaration`` objects (properties). 618 619 ``replacer`` must be a function which is called with a single 620 argument ``urlstring`` which is the current value of url() 621 excluding ``url(`` and ``)``. It still may have surrounding 622 single or double quotes though. 623 """ 624 cssutils.replaceUrls(self, replacer)
625
626 - def setSerializer(self, cssserializer):
627 """ 628 Sets the global Serializer used for output of all stylesheet 629 output. 630 """ 631 if isinstance(cssserializer, cssutils.CSSSerializer): 632 cssutils.ser = cssserializer 633 else: 634 raise ValueError(u'Serializer must be an instance of cssutils.CSSSerializer.')
635
636 - def setSerializerPref(self, pref, value):
637 """ 638 Sets Preference of CSSSerializer used for output of this 639 stylesheet. See cssutils.serialize.Preferences for possible 640 preferences to be set. 641 """ 642 cssutils.ser.prefs.__setattr__(pref, value)
643
644 - def __repr__(self):
645 if self.media: 646 mediaText = self.media.mediaText 647 else: 648 mediaText = None 649 return "cssutils.css.%s(href=%r, media=%r, title=%r)" % ( 650 self.__class__.__name__, 651 self.href, mediaText, self.title)
652
653 - def __str__(self):
654 if self.media: 655 mediaText = self.media.mediaText 656 else: 657 mediaText = None 658 return "<cssutils.css.%s object encoding=%r href=%r "\ 659 "media=%r title=%r namespaces=%r at 0x%x>" % ( 660 self.__class__.__name__, self.encoding, self.href, 661 mediaText, self.title, self.namespaces.namespaces, 662 id(self))
663