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