You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

871 lines
33 KiB

"""Value related classes.
DOM Level 2 CSS CSSValue, CSSPrimitiveValue and CSSValueList are **no longer**
supported and are replaced by these new classes.
"""
__all__ = ['PropertyValue',
'Value',
'ColorValue',
'DimensionValue',
'URIValue',
'CSSFunction',
'CSSVariable',
'MSValue'
]
__docformat__ = 'restructuredtext'
__version__ = '$Id$'
from cssutils.prodparser import *
import cssutils
from cssutils.helper import normalize, pushtoken
import colorsys
import math
import re
import xml.dom
import urlparse
class PropertyValue(cssutils.util._NewBase):
"""
An unstructured list like holder for all values defined for a
:class:`~cssutils.css.Property`. Contains :class:`~cssutils.css.Value`
or subclass objects. Currently there is no access to the combinators of
the defined values which might simply be space or comma or slash.
You may:
- iterate over all contained Value objects (not the separators like ``,``,
``/`` or `` `` though!)
- get a Value item by index or use ``PropertyValue[index]``
- find out the number of values defined (unstructured)
"""
def __init__(self, cssText=None, parent=None, readonly=False):
"""
:param cssText:
the parsable cssText of the value
:param readonly:
defaults to False
"""
super(PropertyValue, self).__init__()
self.parent = parent
self.wellformed = False
if cssText is not None: # may be 0
if isinstance(cssText, (int, float)):
cssText = unicode(cssText) # if it is a number
self.cssText = cssText
self._readonly = readonly
def __len__(self):
return len(list(self.__items()))
def __getitem__(self, index):
try:
return list(self.__items())[index]
except IndexError:
return None
def __iter__(self):
"Generator which iterates over values."
for item in self.__items():
yield item
def __repr__(self):
return u"cssutils.css.%s(%r)" % (self.__class__.__name__,
self.cssText)
def __str__(self):
return u"<cssutils.css.%s object length=%r cssText=%r at "\
u"0x%x>" % (self.__class__.__name__,
self.length, self.cssText, id(self))
def __items(self, seq=None):
"a generator of Value obects only, no , / or ' '"
if seq is None:
seq = self.seq
return (x.value for x in seq if isinstance(x.value, Value))
def _setCssText(self, cssText):
if isinstance(cssText, (int, float)):
cssText = unicode(cssText) # if it is a number
"""
Format::
unary_operator
: '-' | '+'
;
operator
: '/' S* | ',' S* | /* empty */
;
expr
: term [ operator term ]*
;
term
: unary_operator?
[ NUMBER S* | PERCENTAGE S* | LENGTH S* | EMS S* | EXS S* |
ANGLE S* | TIME S* | FREQ S* ]
| STRING S* | IDENT S* | URI S* | hexcolor | function
| UNICODE-RANGE S*
;
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*
;
: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.
- :exc:`~xml.dom.NoModificationAllowedErr`:
Raised if this value is readonly.
"""
self._checkReadonly()
# used as operator is , / or S
nextSor = u',/'
term = Choice(_ColorProd(self, nextSor),
_DimensionProd(self, nextSor),
_URIProd(self, nextSor),
_ValueProd(self, nextSor),
# _CalcValueProd(self, nextSor),
# _Rect(self, nextSor),
# all other functions
_CSSVariableProd(self, nextSor),
_MSValueProd(self, nextSor),
_CSSFunctionProd(self, nextSor)
)
operator = Choice(PreDef.S(toSeq=False),
PreDef.char('comma', ',',
toSeq=lambda t, tokens: ('operator', t[1])),
PreDef.char('slash', '/',
toSeq=lambda t, tokens: ('operator', t[1])),
optional=True)
prods = Sequence(term,
Sequence(# mayEnd this Sequence if whitespace
operator,
# TODO: only when setting via other class
# used by variabledeclaration currently
PreDef.char('END', ';',
stopAndKeep=True,
optional=True),
# TODO: } and !important ends too!
term,
minmax=lambda: (0, None)))
# parse
ok, seq, store, unused = ProdParser().parse(cssText,
u'PropertyValue',
prods)
# must be at least one value!
ok = ok and len(list(self.__items(seq))) > 0
if ok:
self._setSeq(seq)
self.wellformed = True
else:
self._log.error(u'PropertyValue: Unknown syntax or no value: %s' %
self._valuestr(cssText))
cssText = property(lambda self: cssutils.ser.do_css_PropertyValue(self),
_setCssText,
doc="A string representation of the current value.")
def item(self, index):
"""
The value at position `index`. Alternatively simple use
``PropertyValue[index]``.
:param index:
the parsable cssText of the value
:exceptions:
- :exc:`~IndexError`:
Raised if index if out of bounds
"""
return self[index]
length = property(lambda self: len(self),
doc=u"Number of values set.")
value = property(lambda self: cssutils.ser.do_css_PropertyValue(self,
valuesOnly=True),
doc=u"A string representation of the current value "
u"without any comments used for validation.")
class Value(cssutils.util._NewBase):
"""
Represents a single CSS value. For now simple values of
IDENT, STRING, or UNICODE-RANGE values are represented directly
as Value objects. Other values like e.g. FUNCTIONs are represented by
subclasses with an extended API.
"""
IDENT = u'IDENT'
STRING = u'STRING'
UNICODE_RANGE = u'UNICODE-RANGE'
URI = u'URI'
DIMENSION = u'DIMENSION'
NUMBER = u'NUMBER'
PERCENTAGE = u'PERCENTAGE'
COLOR_VALUE = u'COLOR_VALUE'
HASH = u'HASH'
FUNCTION = u'FUNCTION'
VARIABLE = u'VARIABLE'
_type = None
_value = u''
def __init__(self, cssText=None, parent=None, readonly=False):
super(Value, self).__init__()
self.parent = parent
if cssText:
self.cssText = cssText
def __repr__(self):
return u"cssutils.css.%s(%r)" % (self.__class__.__name__,
self.cssText)
def __str__(self):
return u"<cssutils.css.%s object type=%s value=%r cssText=%r at 0x%x>"\
% (self.__class__.__name__,
self.type, self.value, self.cssText,
id(self))
def _setCssText(self, cssText):
self._checkReadonly()
prods = Choice(PreDef.hexcolor(stop=True),
PreDef.ident(stop=True),
PreDef.string(stop=True),
PreDef.unicode_range(stop=True),
)
ok, seq, store, unused = ProdParser().parse(cssText, u'Value', prods)
if ok:
# only 1 value anyway!
self._type = seq[0].type
self._value = seq[0].value
self._setSeq(seq)
self.wellformed = ok
cssText = property(lambda self: cssutils.ser.do_css_Value(self),
_setCssText,
doc=u'String value of this value.')
type = property(lambda self: self._type, #_setType,
doc=u"Type of this value, for now the production type "
u"like e.g. `DIMENSION` or `STRING`. All types are "
u"defined as constants in :class:`~cssutils.css.Value`.")
def _setValue(self, value):
# TODO: check!
self._value = value
value = property(lambda self: self._value, _setValue,
doc=u"Actual value if possible: An int or float or else "
u" a string")
class ColorValue(Value):
"""
A color value like rgb(), rgba(), hsl(), hsla() or #rgb, #rrggbb
TODO: Color Keywords
"""
from colors import COLORS
type = Value.COLOR_VALUE
# hexcolor, FUNCTION?
_colorType = None
_red = 0
_green = 0
_blue = 0
_alpha = 0
def __str__(self):
return u"<cssutils.css.%s object type=%s value=%r colorType=%r "\
u"red=%s blue=%s green=%s alpha=%s at 0x%x>"\
% (self.__class__.__name__,
self.type, self.value,
self.colorType, self.red, self.green, self.blue, self.alpha,
id(self))
def _setCssText(self, cssText):
self._checkReadonly()
types = self._prods # rename!
component = Choice(PreDef.unary(toSeq=lambda t, tokens: (t[0],
DimensionValue(pushtoken(t, tokens),
parent=self)
)),
PreDef.number(toSeq=lambda t, tokens: (t[0],
DimensionValue(pushtoken(t, tokens),
parent=self)
)),
PreDef.percentage(toSeq=lambda t, tokens: (t[0],
DimensionValue(pushtoken(t, tokens),
parent=self)
))
)
noalp = Sequence(Prod(name='FUNCTION',
match=lambda t, v: t == types.FUNCTION and
v in (u'rgb(', u'hsl('),
toSeq=lambda t, tokens: (t[0], normalize(t[1]))),
component,
Sequence(PreDef.comma(),
component,
minmax=lambda: (2, 2)
),
PreDef.funcEnd(stop=True)
)
witha = Sequence(Prod(name='FUNCTION',
match=lambda t, v: t == types.FUNCTION and
v in (u'rgba(', u'hsla('),
toSeq=lambda t, tokens: (t[0],
normalize(t[1]))
),
component,
Sequence(PreDef.comma(),
component,
minmax=lambda: (3, 3)
),
PreDef.funcEnd(stop=True)
)
namedcolor = Prod(name='Named Color',
match=lambda t, v: t == 'IDENT' and (
normalize(v) in self.COLORS.keys()
),
stop=True)
prods = Choice(PreDef.hexcolor(stop=True),
namedcolor,
noalp,
witha)
ok, seq, store, unused = ProdParser().parse(cssText,
self.type,
prods)
if ok:
t, v = seq[0].type, seq[0].value
if u'IDENT' == t:
rgba = self.COLORS[normalize(v)]
if u'HASH' == t:
if len(v) == 4:
# HASH #rgb
rgba = (int(2*v[1], 16),
int(2*v[2], 16),
int(2*v[3], 16),
1.0)
else:
# HASH #rrggbb
rgba = (int(v[1:3], 16),
int(v[3:5], 16),
int(v[5:7], 16),
1.0)
elif u'FUNCTION' == t:
functiontype, raw, check = None, [], u''
HSL = False
for item in seq:
try:
type_ = item.value.type
except AttributeError, e:
# type of function, e.g. rgb(
if item.type == 'FUNCTION':
functiontype = item.value
HSL = functiontype in (u'hsl(', u'hsla(')
continue
# save components
if type_ == Value.NUMBER:
raw.append(item.value.value)
check += u'N'
elif type_ == Value.PERCENTAGE:
if HSL:
# save as percentage fraction
raw.append(item.value.value / 100.0)
else:
# save as real value of percentage of 255
raw.append(int(255 * item.value.value / 100))
check += u'P'
if HSL:
# convert to rgb
# h is 360 based (circle)
h, s, l = raw[0] / 360.0, raw[1], raw[2]
# ORDER h l s !!!
r, g, b = colorsys.hls_to_rgb(h, l, s)
# back to 255 based
rgba = [int(round(r*255)),
int(round(g*255)),
int(round(b*255))]
if len(raw) > 3:
rgba.append(raw[3])
else:
# rgb, rgba
rgba = raw
if len(rgba) < 4:
rgba.append(1.0)
# validate
checks = {u'rgb(': ('NNN', 'PPP'),
u'rgba(': ('NNNN', 'PPPN'),
u'hsl(': ('NPP',),
u'hsla(': ('NPPN',)
}
if check not in checks[functiontype]:
self._log.error(u'ColorValue has invalid %s) parameters: '
u'%s (N=Number, P=Percentage)' %
(functiontype, check))
self._colorType = t
self._red, self._green, self._blue, self._alpha = tuple(rgba)
self._setSeq(seq)
self.wellformed = ok
cssText = property(lambda self: cssutils.ser.do_css_ColorValue(self),
_setCssText,
doc=u"String value of this value.")
value = property(lambda self: cssutils.ser.do_css_CSSFunction(self, True),
doc=u'Same as cssText but without comments.')
type = property(lambda self: Value.COLOR_VALUE,
doc=u"Type is fixed to Value.COLOR_VALUE.")
def _getName(self):
for n, v in self.COLORS.items():
if v == (self.red, self.green, self.blue, self.alpha):
return n
colorType = property(lambda self: self._colorType,
doc=u"IDENT (red), HASH (#f00) or FUNCTION (rgb(255, 0, 0).")
name = property(_getName,
doc=u'Name of the color if known (in ColorValue.COLORS) '
u'else None')
red = property(lambda self: self._red,
doc=u'red part as integer between 0 and 255')
green = property(lambda self: self._green,
doc=u'green part as integer between 0 and 255')
blue = property(lambda self: self._blue,
doc=u'blue part as integer between 0 and 255')
alpha = property(lambda self: self._alpha,
doc=u'alpha part as float between 0.0 and 1.0')
class DimensionValue(Value):
"""
A numerical value with an optional dimenstion like e.g. "px" or "%".
Covers DIMENSION, PERCENTAGE or NUMBER values.
"""
__reNumDim = re.compile(ur'^(\d*\.\d+|\d+)(.*)$', re.I | re.U | re.X)
_dimension = None
_sign = None
def __str__(self):
return u"<cssutils.css.%s object type=%s value=%r dimension=%r cssText=%r at 0x%x>"\
% (self.__class__.__name__,
self.type, self.value, self.dimension, self.cssText,
id(self))
def _setCssText(self, cssText):
self._checkReadonly()
prods = Sequence(PreDef.unary(),
Choice(PreDef.dimension(stop=True),
PreDef.number(stop=True),
PreDef.percentage(stop=True)
)
)
ok, seq, store, unused = ProdParser().parse(cssText,
u'DimensionValue',
prods)
if ok:
sign = val = u''
dim = type_ = None
# find
for item in seq:
if item.value in u'+-':
sign = item.value
else:
type_ = item.type
# number + optional dim
v, d = self.__reNumDim.findall(
normalize(item.value))[0]
if u'.' in v:
val = float(sign + v)
else:
val = int(sign + v)
if d:
dim = d
self._sign = sign
self._value = val
self._dimension = dim
self._type = type_
self._setSeq(seq)
self.wellformed = ok
cssText = property(lambda self: cssutils.ser.do_css_Value(self),
_setCssText,
doc=u"String value of this value including dimension.")
dimension = property(lambda self: self._dimension, #_setValue,
doc=u"Dimension if a DIMENSION or PERCENTAGE value, "
u"else None")
class URIValue(Value):
"""
An URI value like ``url(example.png)``.
"""
_type = Value.URI
_uri = Value._value
def __str__(self):
return u"<cssutils.css.%s object type=%s value=%r uri=%r cssText=%r at 0x%x>"\
% (self.__class__.__name__,
self.type, self.value, self.uri, self.cssText,
id(self))
def _setCssText(self, cssText):
self._checkReadonly()
prods = Sequence(PreDef.uri(stop=True))
ok, seq, store, unused = ProdParser().parse(cssText, u'URIValue', prods)
if ok:
# only 1 value only anyway
self._type = seq[0].type
self._value = seq[0].value
self._setSeq(seq)
self.wellformed = ok
cssText = property(lambda self: cssutils.ser.do_css_Value(self),
_setCssText,
doc=u'String value of this value.')
def _setUri(self, uri):
# TODO: check?
self._value = uri
uri = property(lambda self: self._value, _setUri,
doc=u"Actual URL without delimiters or the empty string")
def absoluteUri(self):
"""Actual URL, made absolute if possible, else same as `uri`."""
# Ancestry: PropertyValue, Property, CSSStyleDeclaration, CSSStyleRule,
# CSSStyleSheet
try:
# TODO: better way?
styleSheet = self.parent.parent.parent.parentRule.parentStyleSheet
except AttributeError, e:
return self.uri
else:
return urlparse.urljoin(styleSheet.href, self.uri)
absoluteUri = property(absoluteUri, doc=absoluteUri.__doc__)
class CSSFunction(Value):
"""
A function value.
"""
_functionName = 'Function'
def _productions(self):
"""Return definition used for parsing."""
types = self._prods # rename!
itemProd = Choice(_ColorProd(self),
_DimensionProd(self),
_URIProd(self),
_ValueProd(self),
#_CalcValueProd(self),
_CSSVariableProd(self),
_CSSFunctionProd(self)
)
funcProds = Sequence(Prod(name='FUNCTION',
match=lambda t, v: t == types.FUNCTION,
toSeq=lambda t, tokens: (t[0],
normalize(t[1]))),
Choice(Sequence(itemProd,
Sequence(PreDef.comma(),
itemProd,
minmax=lambda: (0, None)),
PreDef.funcEnd(stop=True)),
PreDef.funcEnd(stop=True))
)
return funcProds
def _setCssText(self, cssText):
self._checkReadonly()
ok, seq, store, unused = ProdParser().parse(cssText,
self.type,
self._productions())
if ok:
self._setSeq(seq)
self.wellformed = ok
cssText = property(lambda self: cssutils.ser.do_css_CSSFunction(self),
_setCssText,
doc=u"String value of this value.")
value = property(lambda self: cssutils.ser.do_css_CSSFunction(self, True),
doc=u'Same as cssText but without comments.')
type = property(lambda self: Value.FUNCTION,
doc=u"Type is fixed to Value.FUNCTION.")
class MSValue(CSSFunction):
"""An IE specific Microsoft only function value which is much looser
in what is syntactically allowed."""
_functionName = 'MSValue'
def _productions(self):
"""Return definition used for parsing."""
types = self._prods # rename!
func = Prod(name='MSValue-Sub',
match=lambda t, v: t == self._prods.FUNCTION,
toSeq=lambda t, tokens: (MSValue._functionName,
MSValue(pushtoken(t,
tokens
),
parent=self
)
)
)
funcProds = Sequence(Prod(name='FUNCTION',
match=lambda t, v: t == types.FUNCTION,
toSeq=lambda t, tokens: (t[0], t[1])
),
Sequence(Choice(_ColorProd(self),
_DimensionProd(self),
_URIProd(self),
_ValueProd(self),
_MSValueProd(self),
#_CalcValueProd(self),
_CSSVariableProd(self),
func,
#_CSSFunctionProd(self),
Prod(name='MSValuePart',
match=lambda t, v: v != u')',
toSeq=lambda t, tokens: (t[0], t[1])
)
),
minmax=lambda: (0, None)
),
PreDef.funcEnd(stop=True)
)
return funcProds
def _setCssText(self, cssText):
super(MSValue, self)._setCssText(cssText)
cssText = property(lambda self: cssutils.ser.do_css_MSValue(self),
_setCssText,
doc=u"String value of this value.")
class CSSVariable(CSSFunction):
"""The CSSVariable represents a CSS variables like ``var(varname)``.
A variable has a (nonnormalized!) `name` and a `value` which is
tried to be resolved from any available CSSVariablesRule definition.
"""
_functionName = 'CSSVariable'
_name = None
def __str__(self):
return u"<cssutils.css.%s object name=%r value=%r at 0x%x>" % (
self.__class__.__name__, self.name, self.value, id(self))
def _setCssText(self, cssText):
self._checkReadonly()
types = self._prods # rename!
prods = Sequence(Prod(name='var',
match=lambda t, v: t == types.FUNCTION and
normalize(v) == u'var('
),
PreDef.ident(toStore='ident'),
PreDef.funcEnd(stop=True))
# store: name of variable
store = {'ident': None}
ok, seq, store, unused = ProdParser().parse(cssText,
u'CSSVariable',
prods)
if ok:
self._name = store['ident'].value
self._setSeq(seq)
self.wellformed = ok
cssText = property(lambda self: cssutils.ser.do_css_CSSVariable(self),
_setCssText, doc=u"String representation of variable.")
# TODO: writable? check if var (value) available?
name = property(lambda self: self._name,
doc=u"The name identifier of this variable referring to "
u"a value in a "
u":class:`cssutils.css.CSSVariablesDeclaration`.")
type = property(lambda self: Value.VARIABLE,
doc=u"Type is fixed to Value.VARIABLE.")
def _getValue(self):
"Find contained sheet and @variables there"
rel = self
while True:
# find node which has parentRule to get to StyleSheet
if hasattr(rel, 'parent'):
rel = rel.parent
else:
break
try:
variables = rel.parentRule.parentStyleSheet.variables
except AttributeError:
return None
else:
try:
return variables[self.name]
except KeyError:
return None
value = property(_getValue,
doc=u'The resolved actual value or None.')
# helper for productions
def _ValueProd(parent, nextSor=False):
return Prod(name='Value',
match=lambda t, v: t in ('IDENT', 'STRING', 'UNICODE-RANGE'),
nextSor = nextSor,
toSeq=lambda t, tokens: ('Value', Value(
pushtoken(t,
tokens),
parent=parent)
)
)
def _DimensionProd(parent, nextSor=False):
return Prod(name='Dimension',
match=lambda t, v: t in (u'DIMENSION',
u'NUMBER',
u'PERCENTAGE') or v in u'+-',
nextSor = nextSor,
toSeq=lambda t, tokens: (t[0], DimensionValue(
pushtoken(t,
tokens),
parent=parent)
)
)
def _URIProd(parent, nextSor=False):
return Prod(name='URIValue',
match=lambda t, v: t == 'URI',
nextSor = nextSor,
toSeq=lambda t, tokens: ('URIValue', URIValue(
pushtoken(t,
tokens),
parent=parent)
)
)
reHexcolor = re.compile(r'^\#(?:[0-9abcdefABCDEF]{3}|[0-9abcdefABCDEF]{6})$')
def _ColorProd(parent, nextSor=False):
return Prod(name='ColorValue',
match=lambda t, v:
(t == 'HASH' and
reHexcolor.match(v)
) or
(t == 'FUNCTION' and
normalize(v) in (u'rgb(',
u'rgba(',
u'hsl(',
u'hsla(')
) or
(t == 'IDENT' and
normalize(v) in ColorValue.COLORS.keys()
),
nextSor = nextSor,
toSeq=lambda t, tokens: ('ColorValue', ColorValue(
pushtoken(t,
tokens),
parent=parent)
)
)
def _CSSFunctionProd(parent, nextSor=False):
return PreDef.function(nextSor=nextSor,
toSeq=lambda t, tokens: (CSSFunction._functionName,
CSSFunction(
pushtoken(t, tokens),
parent=parent)
)
)
def _CSSVariableProd(parent, nextSor=False):
return PreDef.variable(nextSor=nextSor,
toSeq=lambda t, tokens: (CSSVariable._functionName,
CSSVariable(
pushtoken(t, tokens),
parent=parent)
)
)
def _MSValueProd(parent, nextSor=False):
return Prod(name=MSValue._functionName,
match=lambda t, v: (#t == self._prods.FUNCTION and (
normalize(v) in (u'expression(',
u'alpha(',
u'blur(',
u'chroma(',
u'dropshadow(',
u'fliph(',
u'flipv(',
u'glow(',
u'gray(',
u'invert(',
u'mask(',
u'shadow(',
u'wave(',
u'xray(') or
v.startswith(u'progid:DXImageTransform.Microsoft.')
),
nextSor=nextSor,
toSeq=lambda t, tokens: (MSValue._functionName,
MSValue(pushtoken(t,
tokens
),
parent=parent
)
)
)