calibre/src/cssutils/css/cssstylesheet.py

643 lines
26 KiB
Python

"""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 "<cssutils.css.%s object encoding=%r href=%r "\
"media=%r title=%r namespaces=%r at 0x%x>" % (
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)