"""CSSStyleSheet implements DOM Level 2 CSS CSSStyleSheet. Partly also: - http://dev.w3.org/csswg/cssom/#the-cssstylesheet - http://www.w3.org/TR/2006/WD-css3-namespace-20060828/ TODO: - ownerRule and ownerNode """ __all__ = ['CSSStyleSheet'] __docformat__ = 'restructuredtext' __version__ = '$Id: cssstylesheet.py 1820 2009-08-01 20:53:08Z cthedot $' from cssutils.helper import Deprecated from cssutils.util import _Namespaces, _SimpleNamespaces, _readUrl import cssutils.stylesheets import xml.dom class CSSStyleSheet(cssutils.stylesheets.StyleSheet): """CSSStyleSheet represents a CSS style sheet. Format:: stylesheet : [ CHARSET_SYM S* STRING S* ';' ]? [S|CDO|CDC]* [ import [S|CDO|CDC]* ]* [ namespace [S|CDO|CDC]* ]* # according to @namespace WD [ [ ruleset | media | page ] [S|CDO|CDC]* ]* ``cssRules`` All Rules in this style sheet, a :class:`~cssutils.css.CSSRuleList`. """ def __init__(self, href=None, media=None, title=u'', disabled=None, ownerNode=None, parentStyleSheet=None, readonly=False, ownerRule=None): """ For parameters see :class:`~cssutils.stylesheets.StyleSheet` """ super(CSSStyleSheet, self).__init__( 'text/css', href, media, title, disabled, ownerNode, parentStyleSheet) self._ownerRule = ownerRule self.cssRules = cssutils.css.CSSRuleList() self._namespaces = _Namespaces(parentStyleSheet=self, log=self._log) self._readonly = readonly # used only during setting cssText by parse*() self.__encodingOverride = None self._fetcher = None def __iter__(self): "Generator which iterates over cssRules." for rule in self._cssRules: yield rule def __repr__(self): if self.media: mediaText = self.media.mediaText else: mediaText = None return "cssutils.css.%s(href=%r, media=%r, title=%r)" % ( self.__class__.__name__, self.href, mediaText, self.title) def __str__(self): if self.media: mediaText = self.media.mediaText else: mediaText = None return "" % ( self.__class__.__name__, self.encoding, self.href, mediaText, self.title, self.namespaces.namespaces, id(self)) def _cleanNamespaces(self): "Remove all namespace rules with same namespaceURI but last one set." rules = self._cssRules namespaceitems = self.namespaces.items() i = 0 while i < len(rules): rule = rules[i] if rule.type == rule.NAMESPACE_RULE and \ (rule.prefix, rule.namespaceURI) not in namespaceitems: self.deleteRule(i) else: i += 1 def _getUsedURIs(self): "Return set of URIs used in the sheet." useduris = set() for r1 in self: if r1.STYLE_RULE == r1.type: useduris.update(r1.selectorList._getUsedUris()) elif r1.MEDIA_RULE == r1.type: for r2 in r1: if r2.type == r2.STYLE_RULE: useduris.update(r2.selectorList._getUsedUris()) return useduris def _setCssRules(self, cssRules): "Set new cssRules and update contained rules refs." cssRules.append = self.insertRule cssRules.extend = self.insertRule cssRules.__delitem__ == self.deleteRule for rule in cssRules: rule._parentStyleSheet = self self._cssRules = cssRules cssRules = property(lambda self: self._cssRules, _setCssRules, "All Rules in this style sheet, a " ":class:`~cssutils.css.CSSRuleList`.") def _getCssText(self): "Textual representation of the stylesheet (a byte string)." return cssutils.ser.do_CSSStyleSheet(self) def _setCssText(self, cssText): """Parse `cssText` and overwrites the whole stylesheet. :param cssText: a parseable string or a tuple of (cssText, dict-of-namespaces) :exceptions: - :exc:`~xml.dom.NamespaceErr`: If a namespace prefix is found which is not declared. - :exc:`~xml.dom.NoModificationAllowedErr`: Raised if the rule is readonly. - :exc:`~xml.dom.SyntaxErr`: Raised if the specified CSS string value has a syntax error and is unparsable. """ self._checkReadonly() cssText, namespaces = self._splitNamespacesOff(cssText) if not namespaces: namespaces = _SimpleNamespaces(log=self._log) tokenizer = self._tokenize2(cssText) newseq = [] #cssutils.css.CSSRuleList() # for closures: must be a mutable new = {'encoding': None, # needed for setting encoding of @import rules 'namespaces': namespaces} def S(expected, seq, token, tokenizer=None): # @charset must be at absolute beginning of style sheet if expected == 0: return 1 else: return expected def COMMENT(expected, seq, token, tokenizer=None): "special: sets parent*" comment = cssutils.css.CSSComment([token], parentStyleSheet=self.parentStyleSheet) seq.append(comment) return expected def charsetrule(expected, seq, token, tokenizer): rule = cssutils.css.CSSCharsetRule(parentStyleSheet=self) rule.cssText = self._tokensupto2(tokenizer, token) if expected > 0 or len(seq) > 0: self._log.error( u'CSSStylesheet: CSSCharsetRule only allowed at beginning of stylesheet.', token, xml.dom.HierarchyRequestErr) else: if rule.wellformed: seq.append(rule) new['encoding'] = rule.encoding return 1 def importrule(expected, seq, token, tokenizer): if new['encoding']: # set temporarily as used by _resolveImport # save newEncoding which have been set by resolveImport self.__newEncoding = new['encoding'] rule = cssutils.css.CSSImportRule(parentStyleSheet=self) rule.cssText = self._tokensupto2(tokenizer, token) if expected > 1: self._log.error( u'CSSStylesheet: CSSImportRule not allowed here.', token, xml.dom.HierarchyRequestErr) else: if rule.wellformed: #del rule._parentEncoding # remove as later it is read from this sheet! seq.append(rule) try: # remove as only used temporarily but may not be set at all del self.__newEncoding except AttributeError, e: pass return 1 def namespacerule(expected, seq, token, tokenizer): rule = cssutils.css.CSSNamespaceRule( cssText=self._tokensupto2(tokenizer, token), parentStyleSheet=self) if expected > 2: self._log.error( u'CSSStylesheet: CSSNamespaceRule not allowed here.', token, xml.dom.HierarchyRequestErr) else: if rule.wellformed: seq.append(rule) # temporary namespaces given to CSSStyleRule and @media new['namespaces'][rule.prefix] = rule.namespaceURI return 2 def fontfacerule(expected, seq, token, tokenizer): rule = cssutils.css.CSSFontFaceRule(parentStyleSheet=self) rule.cssText = self._tokensupto2(tokenizer, token) if rule.wellformed: seq.append(rule) return 3 def mediarule(expected, seq, token, tokenizer): rule = cssutils.css.CSSMediaRule() rule.cssText = (self._tokensupto2(tokenizer, token), new['namespaces']) if rule.wellformed: rule._parentStyleSheet=self for r in rule: r._parentStyleSheet=self seq.append(rule) return 3 def pagerule(expected, seq, token, tokenizer): rule = cssutils.css.CSSPageRule(parentStyleSheet=self) rule.cssText = self._tokensupto2(tokenizer, token) if rule.wellformed: seq.append(rule) return 3 def unknownrule(expected, seq, token, tokenizer): self._log.warn( u'CSSStylesheet: Unknown @rule found.', token, neverraise=True) rule = cssutils.css.CSSUnknownRule(parentStyleSheet=self) rule.cssText = self._tokensupto2(tokenizer, token) if rule.wellformed: seq.append(rule) return expected def ruleset(expected, seq, token, tokenizer): rule = cssutils.css.CSSStyleRule() rule.cssText = (self._tokensupto2(tokenizer, token), new['namespaces']) if rule.wellformed: rule._parentStyleSheet=self seq.append(rule) return 3 # expected: # ['CHARSET', 'IMPORT', 'NAMESPACE', ('PAGE', 'MEDIA', ruleset)] wellformed, expected = self._parse(0, newseq, tokenizer, {'S': S, 'COMMENT': COMMENT, 'CDO': lambda *ignored: None, 'CDC': lambda *ignored: None, 'CHARSET_SYM': charsetrule, 'FONT_FACE_SYM': fontfacerule, 'IMPORT_SYM': importrule, 'NAMESPACE_SYM': namespacerule, 'PAGE_SYM': pagerule, 'MEDIA_SYM': mediarule, 'ATKEYWORD': unknownrule }, default=ruleset) if wellformed: del self._cssRules[:] for rule in newseq: self.insertRule(rule, _clean=False) self._cleanNamespaces() cssText = property(_getCssText, _setCssText, "Textual representation of the stylesheet (a byte string)") def _resolveImport(self, url): """Read (encoding, enctype, decodedContent) from `url` for @import sheets.""" try: # only available during parse of a complete sheet selfAsParentEncoding = self.__newEncoding except AttributeError: try: # explicit @charset selfAsParentEncoding = self._cssRules[0].encoding except (IndexError, AttributeError): # default not UTF-8 but None! selfAsParentEncoding = None return _readUrl(url, fetcher=self._fetcher, overrideEncoding=self.__encodingOverride, parentEncoding=selfAsParentEncoding) def _setCssTextWithEncodingOverride(self, cssText, encodingOverride=None, encoding=None): """Set `cssText` but use `encodingOverride` to overwrite detected encoding. This is used by parse and @import during setting of cssText. If `encoding` is given use this but do not save it as `encodingOverride`.""" if encodingOverride: # encoding during resolving of @import self.__encodingOverride = encodingOverride self.__newEncoding = encoding # save for nested @import self.cssText = cssText if encodingOverride: # set encodingOverride explicit again! self.encoding = self.__encodingOverride # remove? self.__encodingOverride = None elif encoding: # may e.g. be httpEncoding self.encoding = encoding def _setFetcher(self, fetcher=None): """Set @import URL loader, if None the default is used.""" self._fetcher = fetcher def _getEncoding(self): """Encoding set in :class:`~cssutils.css.CSSCharsetRule` or if ``None`` resulting in default ``utf-8`` encoding being used.""" try: return self._cssRules[0].encoding except (IndexError, AttributeError): return 'utf-8' def _setEncoding(self, encoding): """Set `encoding` of charset rule if present in sheet or insert a new :class:`~cssutils.css.CSSCharsetRule` with given `encoding`. If `encoding` is None removes charsetrule if present resulting in default encoding of utf-8. """ try: rule = self._cssRules[0] except IndexError: rule = None if rule and rule.CHARSET_RULE == rule.type: if encoding: rule.encoding = encoding else: self.deleteRule(0) elif encoding: self.insertRule(cssutils.css.CSSCharsetRule(encoding=encoding), 0) encoding = property(_getEncoding, _setEncoding, "(cssutils) Reflect encoding of an @charset rule or 'utf-8' " "(default) if set to ``None``") namespaces = property(lambda self: self._namespaces, doc="All Namespaces used in this CSSStyleSheet.") def add(self, rule): """Add `rule` to style sheet at appropriate position. Same as ``insertRule(rule, inOrder=True)``. """ return self.insertRule(rule, index=None, inOrder=True) def deleteRule(self, index): """Delete rule at `index` from the style sheet. :param index: of the rule to remove in the StyleSheet's rule list. For an `index` < 0 **no** :exc:`~xml.dom.IndexSizeErr` is raised but rules for normal Python lists are used. E.g. ``deleteRule(-1)`` removes the last rule in cssRules. :exceptions: - :exc:`~xml.dom.IndexSizeErr`: Raised if the specified index does not correspond to a rule in the style sheet's rule list. - :exc:`~xml.dom.NamespaceErr`: Raised if removing this rule would result in an invalid StyleSheet - :exc:`~xml.dom.NoModificationAllowedErr`: Raised if this style sheet is readonly. """ self._checkReadonly() try: rule = self._cssRules[index] except IndexError: raise xml.dom.IndexSizeErr( u'CSSStyleSheet: %s is not a valid index in the rulelist of length %i' % ( index, self._cssRules.length)) else: if rule.type == rule.NAMESPACE_RULE: # check all namespacerules if used uris = [r.namespaceURI for r in self if r.type == r.NAMESPACE_RULE] useduris = self._getUsedURIs() if rule.namespaceURI in useduris and\ uris.count(rule.namespaceURI) == 1: raise xml.dom.NoModificationAllowedErr( u'CSSStyleSheet: NamespaceURI defined in this rule is used, cannot remove.') return rule._parentStyleSheet = None # detach del self._cssRules[index] # delete from StyleSheet def insertRule(self, rule, index=None, inOrder=False, _clean=True): """ Used to insert a new rule into the style sheet. The new rule now becomes part of the cascade. :param rule: a parsable DOMString, in cssutils also a :class:`~cssutils.css.CSSRule` or :class:`~cssutils.css.CSSRuleList` :param index: of the rule before the new rule will be inserted. If the specified `index` is equal to the length of the StyleSheet's rule collection, the rule will be added to the end of the style sheet. If `index` is not given or ``None`` rule will be appended to rule list. :param inOrder: if ``True`` the rule will be put to a proper location while ignoring `index` and without raising :exc:`~xml.dom.HierarchyRequestErr`. The resulting index is returned nevertheless. :returns: The index within the style sheet's rule collection :Exceptions: - :exc:`~xml.dom.HierarchyRequestErr`: Raised if the rule cannot be inserted at the specified `index` e.g. if an @import rule is inserted after a standard rule set or other at-rule. - :exc:`~xml.dom.IndexSizeErr`: Raised if the specified `index` is not a valid insertion point. - :exc:`~xml.dom.NoModificationAllowedErr`: Raised if this style sheet is readonly. - :exc:`~xml.dom.SyntaxErr`: Raised if the specified rule has a syntax error and is unparsable. """ self._checkReadonly() # check position if index is None: index = len(self._cssRules) elif index < 0 or index > self._cssRules.length: raise xml.dom.IndexSizeErr( u'CSSStyleSheet: Invalid index %s for CSSRuleList with a length of %s.' % ( index, self._cssRules.length)) return if isinstance(rule, basestring): # init a temp sheet which has the same properties as self tempsheet = CSSStyleSheet(href=self.href, media=self.media, title=self.title, parentStyleSheet=self.parentStyleSheet, ownerRule=self.ownerRule) tempsheet._ownerNode = self.ownerNode tempsheet._fetcher = self._fetcher # prepend encoding if in this sheet to be able to use it in # @import rules encoding resolution # do not add if new rule startswith "@charset" (which is exact!) if not rule.startswith(u'@charset') and (self._cssRules and self._cssRules[0].type == self._cssRules[0].CHARSET_RULE): # rule 0 is @charset! newrulescount, newruleindex = 2, 1 rule = self._cssRules[0].cssText + rule else: newrulescount, newruleindex = 1, 0 # parse the new rule(s) tempsheet.cssText = (rule, self._namespaces) if len(tempsheet.cssRules) != newrulescount or (not isinstance( tempsheet.cssRules[newruleindex], cssutils.css.CSSRule)): self._log.error(u'CSSStyleSheet: Not a CSSRule: %s' % rule) return rule = tempsheet.cssRules[newruleindex] rule._parentStyleSheet = None # done later? # TODO: #tempsheet._namespaces = self._namespaces elif isinstance(rule, cssutils.css.CSSRuleList): # insert all rules for i, r in enumerate(rule): self.insertRule(r, index + i) return index if not rule.wellformed: self._log.error(u'CSSStyleSheet: Invalid rules cannot be added.') return # CHECK HIERARCHY # @charset if rule.type == rule.CHARSET_RULE: if inOrder: index = 0 # always first and only if (self._cssRules and self._cssRules[0].type == rule.CHARSET_RULE): self._cssRules[0].encoding = rule.encoding else: self._cssRules.insert(0, rule) elif index != 0 or (self._cssRules and self._cssRules[0].type == rule.CHARSET_RULE): self._log.error( u'CSSStylesheet: @charset only allowed once at the beginning of a stylesheet.', error=xml.dom.HierarchyRequestErr) return else: self._cssRules.insert(index, rule) # @unknown or comment elif rule.type in (rule.UNKNOWN_RULE, rule.COMMENT) and not inOrder: if index == 0 and self._cssRules and\ self._cssRules[0].type == rule.CHARSET_RULE: self._log.error( u'CSSStylesheet: @charset must be the first rule.', error=xml.dom.HierarchyRequestErr) return else: self._cssRules.insert(index, rule) # @import elif rule.type == rule.IMPORT_RULE: if inOrder: # automatic order if rule.type in (r.type for r in self): # find last of this type for i, r in enumerate(reversed(self._cssRules)): if r.type == rule.type: index = len(self._cssRules) - i break else: # find first point to insert if self._cssRules and self._cssRules[0].type in (rule.CHARSET_RULE, rule.COMMENT): index = 1 else: index = 0 else: # after @charset if index == 0 and self._cssRules and\ self._cssRules[0].type == rule.CHARSET_RULE: self._log.error( u'CSSStylesheet: Found @charset at index 0.', error=xml.dom.HierarchyRequestErr) return # before @namespace, @page, @font-face, @media and stylerule for r in self._cssRules[:index]: if r.type in (r.NAMESPACE_RULE, r.MEDIA_RULE, r.PAGE_RULE, r.STYLE_RULE, r.FONT_FACE_RULE): self._log.error( u'CSSStylesheet: Cannot insert @import here, found @namespace, @media, @page or CSSStyleRule before index %s.' % index, error=xml.dom.HierarchyRequestErr) return self._cssRules.insert(index, rule) # @namespace elif rule.type == rule.NAMESPACE_RULE: if inOrder: if rule.type in (r.type for r in self): # find last of this type for i, r in enumerate(reversed(self._cssRules)): if r.type == rule.type: index = len(self._cssRules) - i break else: # find first point to insert for i, r in enumerate(self._cssRules): if r.type in (r.MEDIA_RULE, r.PAGE_RULE, r.STYLE_RULE, r.FONT_FACE_RULE, r.UNKNOWN_RULE, r.COMMENT): index = i # before these break else: # after @charset and @import for r in self._cssRules[index:]: if r.type in (r.CHARSET_RULE, r.IMPORT_RULE): self._log.error( u'CSSStylesheet: Cannot insert @namespace here, found @charset or @import after index %s.' % index, error=xml.dom.HierarchyRequestErr) return # before @media and stylerule for r in self._cssRules[:index]: if r.type in (r.MEDIA_RULE, r.PAGE_RULE, r.STYLE_RULE, r.FONT_FACE_RULE): self._log.error( u'CSSStylesheet: Cannot insert @namespace here, found @media, @page or CSSStyleRule before index %s.' % index, error=xml.dom.HierarchyRequestErr) return if not (rule.prefix in self.namespaces and self.namespaces[rule.prefix] == rule.namespaceURI): # no doublettes self._cssRules.insert(index, rule) if _clean: self._cleanNamespaces() # all other where order is not important else: if inOrder: # simply add to end as no specific order self._cssRules.append(rule) index = len(self._cssRules) - 1 else: for r in self._cssRules[index:]: if r.type in (r.CHARSET_RULE, r.IMPORT_RULE, r.NAMESPACE_RULE): self._log.error( u'CSSStylesheet: Cannot insert rule here, found @charset, @import or @namespace before index %s.' % index, error=xml.dom.HierarchyRequestErr) return self._cssRules.insert(index, rule) # post settings, TODO: for other rules which contain @rules rule._parentStyleSheet = self if rule.MEDIA_RULE == rule.type: for r in rule: r._parentStyleSheet = self # ? elif rule.IMPORT_RULE == rule.type: rule.href = rule.href # try to reload stylesheet return index ownerRule = property(lambda self: self._ownerRule, doc="A ref to an @import rule if it is imported, else ``None``.") def setSerializer(self, cssserializer): """Set the cssutils global Serializer used for all output.""" if isinstance(cssserializer, cssutils.CSSSerializer): cssutils.ser = cssserializer else: raise ValueError(u'Serializer must be an instance of cssutils.CSSSerializer.') def setSerializerPref(self, pref, value): """Set a Preference of CSSSerializer used for output. See :class:`cssutils.serialize.Preferences` for possible preferences to be set. """ cssutils.ser.prefs.__setattr__(pref, value)