calibre/src/cssutils/css/property.py
2009-05-12 22:13:29 -07:00

470 lines
17 KiB
Python

"""Property is a single CSS property in a CSSStyleDeclaration."""
__all__ = ['Property']
__docformat__ = 'restructuredtext'
__version__ = '$Id: property.py 1685 2009-03-01 18:26:48Z cthedot $'
from cssutils.helper import Deprecated
from cssvalue import CSSValue
import cssutils
import xml.dom
class Property(cssutils.util.Base):
"""A CSS property in a StyleDeclaration of a CSSStyleRule (cssutils).
Format::
property = name
: IDENT S*
;
expr = value
: term [ operator term ]*
;
term
: unary_operator?
[ NUMBER S* | PERCENTAGE S* | LENGTH S* | EMS S* | EXS S* | ANGLE S* |
TIME S* | FREQ S* | function ]
| STRING S* | IDENT S* | URI S* | hexcolor
;
function
: FUNCTION S* expr ')' S*
;
/*
* There is a constraint on the color that it must
* have either 3 or 6 hex-digits (i.e., [0-9a-fA-F])
* after the "#"; e.g., "#000" is OK, but "#abcd" is not.
*/
hexcolor
: HASH S*
;
prio
: IMPORTANT_SYM S*
;
"""
def __init__(self, name=None, value=None, priority=u'',
_mediaQuery=False, _parent=None):
"""
:param name:
a property name string (will be normalized)
:param value:
a property value string
:param priority:
an optional priority string which currently must be u'',
u'!important' or u'important'
:param _mediaQuery:
if ``True`` value is optional (used by MediaQuery)
:param _parent:
the parent object, normally a
:class:`cssutils.css.CSSStyleDeclaration`
"""
super(Property, self).__init__()
self.seqs = [[], None, []]
self.wellformed = False
self._mediaQuery = _mediaQuery
self._parent = _parent
self.__nametoken = None
self._name = u''
self._literalname = u''
if name:
self.name = name
if value:
self.cssValue = value
else:
self.seqs[1] = CSSValue()
self._priority = u''
self._literalpriority = u''
if priority:
self.priority = priority
def __repr__(self):
return "cssutils.css.%s(name=%r, value=%r, priority=%r)" % (
self.__class__.__name__,
self.literalname, self.cssValue.cssText, self.priority)
def __str__(self):
return "<%s.%s object name=%r value=%r priority=%r valid=%r at 0x%x>" % (
self.__class__.__module__, self.__class__.__name__,
self.name, self.cssValue.cssText, self.priority,
self.valid, id(self))
def _getCssText(self):
"""Return serialized property cssText."""
return cssutils.ser.do_Property(self)
def _setCssText(self, cssText):
"""
:exceptions:
- :exc:`~xml.dom.SyntaxErr`:
Raised if the specified CSS string value has a syntax error and
is unparsable.
- :exc:`~xml.dom.NoModificationAllowedErr`:
Raised if the rule is readonly.
"""
# check and prepare tokenlists for setting
tokenizer = self._tokenize2(cssText)
nametokens = self._tokensupto2(tokenizer, propertynameendonly=True)
if nametokens:
wellformed = True
valuetokens = self._tokensupto2(tokenizer,
propertyvalueendonly=True)
prioritytokens = self._tokensupto2(tokenizer,
propertypriorityendonly=True)
if self._mediaQuery and not valuetokens:
# MediaQuery may consist of name only
self.name = nametokens
self.cssValue = None
self.priority = None
return
# remove colon from nametokens
colontoken = nametokens.pop()
if self._tokenvalue(colontoken) != u':':
wellformed = False
self._log.error(u'Property: No ":" after name found: %r' %
self._valuestr(cssText), colontoken)
elif not nametokens:
wellformed = False
self._log.error(u'Property: No property name found: %r.' %
self._valuestr(cssText), colontoken)
if valuetokens:
if self._tokenvalue(valuetokens[-1]) == u'!':
# priority given, move "!" to prioritytokens
prioritytokens.insert(0, valuetokens.pop(-1))
else:
wellformed = False
self._log.error(u'Property: No property value found: %r.' %
self._valuestr(cssText), colontoken)
if wellformed:
self.wellformed = True
self.name = nametokens
self.cssValue = valuetokens
self.priority = prioritytokens
# also invalid values are set!
self.validate()
else:
self._log.error(u'Property: No property name found: %r.' %
self._valuestr(cssText))
cssText = property(fget=_getCssText, fset=_setCssText,
doc="A parsable textual representation.")
def _setName(self, name):
"""
:exceptions:
- :exc:`~xml.dom.SyntaxErr`:
Raised if the specified name has a syntax error and is
unparsable.
"""
# for closures: must be a mutable
new = {'literalname': None,
'wellformed': True}
def _ident(expected, seq, token, tokenizer=None):
# name
if 'name' == expected:
new['literalname'] = self._tokenvalue(token).lower()
seq.append(new['literalname'])
return 'EOF'
else:
new['wellformed'] = False
self._log.error(u'Property: Unexpected ident.', token)
return expected
newseq = []
wellformed, expected = self._parse(expected='name',
seq=newseq,
tokenizer=self._tokenize2(name),
productions={'IDENT': _ident})
wellformed = wellformed and new['wellformed']
# post conditions
# define a token for error logging
if isinstance(name, list):
token = name[0]
self.__nametoken = token
else:
token = None
if not new['literalname']:
wellformed = False
self._log.error(u'Property: No name found: %r' %
self._valuestr(name), token=token)
if wellformed:
self.wellformed = True
self._literalname = new['literalname']
self._name = self._normalize(self._literalname)
self.seqs[0] = newseq
# # validate
if self._name not in cssutils.profile.knownNames:
# self.valid = False
self._log.warn(u'Property: Unknown Property name.',
token=token, neverraise=True)
else:
pass
# self.valid = True
# if self.cssValue:
# self.cssValue._propertyName = self._name
# #self.valid = self.cssValue.valid
else:
self.wellformed = False
name = property(lambda self: self._name, _setName,
doc="Name of this property.")
literalname = property(lambda self: self._literalname,
doc="Readonly literal (not normalized) name "
"of this property")
def _getCSSValue(self):
return self.seqs[1]
def _setCSSValue(self, cssText):
"""
See css.CSSValue
:exceptions:
- :exc:`~xml.dom.SyntaxErr`:
Raised if the specified CSS string value has a syntax error
(according to the attached property) or is unparsable.
- :exc:`~xml.dom.InvalidModificationErr`:
TODO: Raised if the specified CSS string value represents a different
type of values than the values allowed by the CSS property.
"""
if self._mediaQuery and not cssText:
self.seqs[1] = CSSValue()
else:
if not self.seqs[1]:
self.seqs[1] = CSSValue()
cssvalue = self.seqs[1]
cssvalue.cssText = cssText
if cssvalue.wellformed: #cssvalue._value and
self.seqs[1] = cssvalue
self.wellformed = self.wellformed and cssvalue.wellformed
cssValue = property(_getCSSValue, _setCSSValue,
doc="(cssutils) CSSValue object of this property")
def _getValue(self):
if self.cssValue:
return self.cssValue.cssText # _value # [0]
else:
return u''
def _setValue(self, value):
self.cssValue.cssText = value
# self.valid = self.valid and self.cssValue.valid
self.wellformed = self.wellformed and self.cssValue.wellformed
value = property(_getValue, _setValue,
doc="The textual value of this Properties cssValue.")
def _setPriority(self, priority):
"""
priority
a string, currently either u'', u'!important' or u'important'
Format::
prio
: IMPORTANT_SYM S*
;
"!"{w}"important" {return IMPORTANT_SYM;}
:exceptions:
- :exc:`~xml.dom.SyntaxErr`:
Raised if the specified priority has a syntax error and is
unparsable.
In this case a priority not equal to None, "" or "!{w}important".
As CSSOM defines CSSStyleDeclaration.getPropertyPriority resulting in
u'important' this value is also allowed to set a Properties priority
"""
if self._mediaQuery:
self._priority = u''
self._literalpriority = u''
if priority:
self._log.error(u'Property: No priority in a MediaQuery - ignored.')
return
if isinstance(priority, basestring) and\
u'important' == self._normalize(priority):
priority = u'!%s' % priority
# for closures: must be a mutable
new = {'literalpriority': u'',
'wellformed': True}
def _char(expected, seq, token, tokenizer=None):
# "!"
val = self._tokenvalue(token)
if u'!' == expected == val:
seq.append(val)
return 'important'
else:
new['wellformed'] = False
self._log.error(u'Property: Unexpected char.', token)
return expected
def _ident(expected, seq, token, tokenizer=None):
# "important"
val = self._tokenvalue(token)
if 'important' == expected:
new['literalpriority'] = val
seq.append(val)
return 'EOF'
else:
new['wellformed'] = False
self._log.error(u'Property: Unexpected ident.', token)
return expected
newseq = []
wellformed, expected = self._parse(expected='!',
seq=newseq,
tokenizer=self._tokenize2(priority),
productions={'CHAR': _char,
'IDENT': _ident})
wellformed = wellformed and new['wellformed']
# post conditions
if priority and not new['literalpriority']:
wellformed = False
self._log.info(u'Property: Invalid priority: %r.' %
self._valuestr(priority))
if wellformed:
self.wellformed = self.wellformed and wellformed
self._literalpriority = new['literalpriority']
self._priority = self._normalize(self.literalpriority)
self.seqs[2] = newseq
# validate priority
if self._priority not in (u'', u'important'):
self._log.error(u'Property: No CSS priority value: %r.' %
self._priority)
priority = property(lambda self: self._priority, _setPriority,
doc="Priority of this property.")
literalpriority = property(lambda self: self._literalpriority,
doc="Readonly literal (not normalized) priority of this property")
def validate(self, profiles=None):
"""Validate value against `profiles`.
:param profiles:
A list of profile names used for validating. If no `profiles`
is given ``cssutils.profile.defaultProfiles`` is used
For each of the following cases a message is reported:
- INVALID (so the property is known but not valid)
``ERROR Property: Invalid value for "{PROFILE-1[/PROFILE-2...]"
property: ...``
- VALID but not in given profiles or defaultProfiles
``WARNING Property: Not valid for profile "{PROFILE-X}" but valid
"{PROFILE-Y}" property: ...``
- VALID in current profile
``DEBUG Found valid "{PROFILE-1[/PROFILE-2...]" property...``
- UNKNOWN property
``WARNING Unknown Property name...`` is issued
so for example::
cssutils.log.setLevel(logging.DEBUG)
parser = cssutils.CSSParser()
s = parser.parseString('''body {
unknown-property: x;
color: 4;
color: rgba(1,2,3,4);
color: red
}''')
# Log output:
WARNING Property: Unknown Property name. [2:9: unknown-property]
ERROR Property: Invalid value for "CSS Color Module Level 3/CSS Level 2.1" property: 4 [3:9: color]
DEBUG Property: Found valid "CSS Color Module Level 3" value: rgba(1, 2, 3, 4) [4:9: color]
DEBUG Property: Found valid "CSS Level 2.1" value: red [5:9: color]
and when setting an explicit default profile::
cssutils.profile.defaultProfiles = cssutils.profile.CSS_LEVEL_2
s = parser.parseString('''body {
unknown-property: x;
color: 4;
color: rgba(1,2,3,4);
color: red
}''')
# Log output:
WARNING Property: Unknown Property name. [2:9: unknown-property]
ERROR Property: Invalid value for "CSS Color Module Level 3/CSS Level 2.1" property: 4 [3:9: color]
WARNING Property: Not valid for profile "CSS Level 2.1" but valid "CSS Color Module Level 3" value: rgba(1, 2, 3, 4) [4:9: color]
DEBUG Property: Found valid "CSS Level 2.1" value: red [5:9: color]
"""
valid = False
if self.name and self.value:
if self.name in cssutils.profile.knownNames:
# add valid, matching, validprofiles...
valid, matching, validprofiles = \
cssutils.profile.validateWithProfile(self.name,
self.value,
profiles)
if not valid:
self._log.error(u'Property: Invalid value for '
u'"%s" property: %s'
% (u'/'.join(validprofiles), self.value),
token=self.__nametoken,
neverraise=True)
# TODO: remove logic to profiles!
elif valid and not matching:#(profiles and profiles not in validprofiles):
if not profiles:
notvalidprofiles = u'/'.join(cssutils.profile.defaultProfiles)
else:
notvalidprofiles = profiles
self._log.warn(u'Property: Not valid for profile "%s" '
u'but valid "%s" value: %s '
% (notvalidprofiles, u'/'.join(validprofiles),
self.value),
token = self.__nametoken,
neverraise=True)
valid = False
elif valid:
self._log.debug(u'Property: Found valid "%s" value: %s'
% (u'/'.join(validprofiles), self.value),
token = self.__nametoken,
neverraise=True)
if self._priority not in (u'', u'important'):
valid = False
return valid
valid = property(validate, doc="Check if value of this property is valid "
"in the properties context.")