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