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.
 
 
 
 
 

813 lines
32 KiB

"""Selector is a single Selector of a CSSStyleRule SelectorList.
Partly implements http://www.w3.org/TR/css3-selectors/.
TODO
- .contains(selector)
- .isSubselector(selector)
"""
__all__ = ['Selector']
__docformat__ = 'restructuredtext'
__version__ = '$Id$'
from cssutils.helper import Deprecated
from cssutils.util import _SimpleNamespaces
import cssutils
import xml.dom
class Selector(cssutils.util.Base2):
"""
(cssutils) a single selector in a :class:`~cssutils.css.SelectorList`
of a :class:`~cssutils.css.CSSStyleRule`.
Format::
# implemented in SelectorList
selectors_group
: selector [ COMMA S* selector ]*
;
selector
: simple_selector_sequence [ combinator simple_selector_sequence ]*
;
combinator
/* combinators can be surrounded by white space */
: PLUS S* | GREATER S* | TILDE S* | S+
;
simple_selector_sequence
: [ type_selector | universal ]
[ HASH | class | attrib | pseudo | negation ]*
| [ HASH | class | attrib | pseudo | negation ]+
;
type_selector
: [ namespace_prefix ]? element_name
;
namespace_prefix
: [ IDENT | '*' ]? '|'
;
element_name
: IDENT
;
universal
: [ namespace_prefix ]? '*'
;
class
: '.' IDENT
;
attrib
: '[' S* [ namespace_prefix ]? IDENT S*
[ [ PREFIXMATCH |
SUFFIXMATCH |
SUBSTRINGMATCH |
'=' |
INCLUDES |
DASHMATCH ] S* [ IDENT | STRING ] S*
]? ']'
;
pseudo
/* '::' starts a pseudo-element, ':' a pseudo-class */
/* Exceptions: :first-line, :first-letter, :before and :after. */
/* Note that pseudo-elements are restricted to one per selector and */
/* occur only in the last simple_selector_sequence. */
: ':' ':'? [ IDENT | functional_pseudo ]
;
functional_pseudo
: FUNCTION S* expression ')'
;
expression
/* In CSS3, the expressions are identifiers, strings, */
/* or of the form "an+b" */
: [ [ PLUS | '-' | DIMENSION | NUMBER | STRING | IDENT ] S* ]+
;
negation
: NOT S* negation_arg S* ')'
;
negation_arg
: type_selector | universal | HASH | class | attrib | pseudo
;
"""
def __init__(self, selectorText=None, parent=None,
readonly=False):
"""
:Parameters:
selectorText
initial value of this selector
parent
a SelectorList
readonly
default to False
"""
super(Selector, self).__init__()
self.__namespaces = _SimpleNamespaces(log=self._log)
self._element = None
self._parent = parent
self._specificity = (0, 0, 0, 0)
if selectorText:
self.selectorText = selectorText
self._readonly = readonly
def __repr__(self):
if self.__getNamespaces():
st = (self.selectorText, self._getUsedNamespaces())
else:
st = self.selectorText
return u"cssutils.css.%s(selectorText=%r)" % (self.__class__.__name__,
st)
def __str__(self):
return u"<cssutils.css.%s object selectorText=%r specificity=%r" \
u" _namespaces=%r at 0x%x>" % (self.__class__.__name__,
self.selectorText,
self.specificity,
self._getUsedNamespaces(),
id(self))
def _getUsedUris(self):
"Return list of actually used URIs in this Selector."
uris = set()
for item in self.seq:
type_, val = item.type, item.value
if type_.endswith(u'-selector') or type_ == u'universal' and \
isinstance(val, tuple) and val[0] not in (None, u'*'):
uris.add(val[0])
return uris
def _getUsedNamespaces(self):
"Return actually used namespaces only."
useduris = self._getUsedUris()
namespaces = _SimpleNamespaces(log=self._log)
for p, uri in self._namespaces.items():
if uri in useduris:
namespaces[p] = uri
return namespaces
def __getNamespaces(self):
"Use own namespaces if not attached to a sheet, else the sheet's ones."
try:
return self._parent.parentRule.parentStyleSheet.namespaces
except AttributeError:
return self.__namespaces
_namespaces = property(__getNamespaces,
doc=u"If this Selector is attached to a "
u"CSSStyleSheet the namespaces of that sheet "
u"are mirrored here. While the Selector (or "
u"parent SelectorList or parentRule(s) of that "
u"are not attached a own dict of {prefix: "
u"namespaceURI} is used.")
element = property(lambda self: self._element,
doc=u"Effective element target of this selector.")
parent = property(lambda self: self._parent,
doc=u"(DOM) The SelectorList that contains this Selector "
u"or None if this Selector is not attached to a "
u"SelectorList.")
def _getSelectorText(self):
"""Return serialized format."""
return cssutils.ser.do_css_Selector(self)
def _setSelectorText(self, selectorText):
"""
:param selectorText:
parsable string or a tuple of (selectorText, dict-of-namespaces).
Given namespaces are ignored if this object is attached to a
CSSStyleSheet!
:exceptions:
- :exc:`~xml.dom.NamespaceErr`:
Raised if the specified selector uses an unknown namespace
prefix.
- :exc:`~xml.dom.SyntaxErr`:
Raised if the specified CSS string value has a syntax error
and is unparsable.
- :exc:`~xml.dom.NoModificationAllowedErr`:
Raised if this rule is readonly.
"""
self._checkReadonly()
# might be (selectorText, namespaces)
selectorText, namespaces = self._splitNamespacesOff(selectorText)
try:
# uses parent stylesheets namespaces if available,
# otherwise given ones
namespaces = self.parent.parentRule.parentStyleSheet.namespaces
except AttributeError:
pass
tokenizer = self._tokenize2(selectorText)
if not tokenizer:
self._log.error(u'Selector: No selectorText given.')
else:
# prepare tokenlist:
# "*" -> type "universal"
# "*"|IDENT + "|" -> combined to "namespace_prefix"
# "|" -> type "namespace_prefix"
# "." + IDENT -> combined to "class"
# ":" + IDENT, ":" + FUNCTION -> pseudo-class
# FUNCTION "not(" -> negation
# "::" + IDENT, "::" + FUNCTION -> pseudo-element
tokens = []
for t in tokenizer:
typ, val, lin, col = t
if val == u':' and tokens and\
self._tokenvalue(tokens[-1]) == ':':
# combine ":" and ":"
tokens[-1] = (typ, u'::', lin, col)
elif typ == 'IDENT' and tokens\
and self._tokenvalue(tokens[-1]) == u'.':
# class: combine to .IDENT
tokens[-1] = ('class', u'.'+val, lin, col)
elif typ == 'IDENT' and tokens and \
self._tokenvalue(tokens[-1]).startswith(u':') and\
not self._tokenvalue(tokens[-1]).endswith(u'('):
# pseudo-X: combine to :IDENT or ::IDENT but not ":a(" + "b"
if self._tokenvalue(tokens[-1]).startswith(u'::'):
t = 'pseudo-element'
else:
t = 'pseudo-class'
tokens[-1] = (t, self._tokenvalue(tokens[-1])+val, lin, col)
elif typ == 'FUNCTION' and val == u'not(' and tokens and \
u':' == self._tokenvalue(tokens[-1]):
tokens[-1] = ('negation', u':' + val, lin, tokens[-1][3])
elif typ == 'FUNCTION' and tokens\
and self._tokenvalue(tokens[-1]).startswith(u':'):
# pseudo-X: combine to :FUNCTION( or ::FUNCTION(
if self._tokenvalue(tokens[-1]).startswith(u'::'):
t = 'pseudo-element'
else:
t = 'pseudo-class'
tokens[-1] = (t, self._tokenvalue(tokens[-1])+val, lin, col)
elif val == u'*' and tokens and\
self._type(tokens[-1]) == 'namespace_prefix' and\
self._tokenvalue(tokens[-1]).endswith(u'|'):
# combine prefix|*
tokens[-1] = ('universal', self._tokenvalue(tokens[-1])+val,
lin, col)
elif val == u'*':
# universal: "*"
tokens.append(('universal', val, lin, col))
elif val == u'|' and tokens and\
self._type(tokens[-1]) in (self._prods.IDENT, 'universal')\
and self._tokenvalue(tokens[-1]).find(u'|') == -1:
# namespace_prefix: "IDENT|" or "*|"
tokens[-1] = ('namespace_prefix',
self._tokenvalue(tokens[-1])+u'|', lin, col)
elif val == u'|':
# namespace_prefix: "|"
tokens.append(('namespace_prefix', val, lin, col))
else:
tokens.append(t)
tokenizer = iter(tokens)
# for closures: must be a mutable
new = {'context': [''], # stack of: 'attrib', 'negation', 'pseudo'
'element': None,
'_PREFIX': None,
'specificity': [0, 0, 0, 0], # mutable, finally a tuple!
'wellformed': True
}
# used for equality checks and setting of a space combinator
S = u' '
def append(seq, val, typ=None, token=None):
"""
appends to seq
namespace_prefix, IDENT will be combined to a tuple
(prefix, name) where prefix might be None, the empty string
or a prefix.
Saved are also:
- specificity definition: style, id, class/att, type
- element: the element this Selector is for
"""
context = new['context'][-1]
if token:
line, col = token[2], token[3]
else:
line, col = None, None
if typ == '_PREFIX':
# SPECIAL TYPE: save prefix for combination with next
new['_PREFIX'] = val[:-1]
# handle next time
return
if new['_PREFIX'] is not None:
# as saved from before and reset to None
prefix, new['_PREFIX'] = new['_PREFIX'], None
elif typ == 'universal' and '|' in val:
# val == *|* or prefix|*
prefix, val = val.split('|')
else:
prefix = None
# namespace
if (typ.endswith('-selector') or typ == 'universal') and not (
'attribute-selector' == typ and not prefix):
# att **IS NOT** in default ns
if prefix == u'*':
# *|name: in ANY_NS
namespaceURI = cssutils._ANYNS
elif prefix is None:
# e or *: default namespace with prefix u''
# or local-name()
namespaceURI = namespaces.get(u'', None)
elif prefix == u'':
# |name or |*: in no (or the empty) namespace
namespaceURI = u''
else:
# explicit namespace prefix
# does not raise KeyError, see _SimpleNamespaces
namespaceURI = namespaces[prefix]
if namespaceURI is None:
new['wellformed'] = False
self._log.error(u'Selector: No namespaceURI found '
u'for prefix %r' % prefix,
token=token,
error=xml.dom.NamespaceErr)
return
# val is now (namespaceprefix, name) tuple
val = (namespaceURI, val)
# specificity
if not context or context == 'negation':
if 'id' == typ:
new['specificity'][1] += 1
elif 'class' == typ or '[' == val:
new['specificity'][2] += 1
elif typ in ('type-selector', 'negation-type-selector',
'pseudo-element'):
new['specificity'][3] += 1
if not context and typ in ('type-selector', 'universal'):
# define element
new['element'] = val
seq.append(val, typ, line=line, col=col)
# expected constants
simple_selector_sequence = 'type_selector universal HASH class ' \
'attrib pseudo negation '
simple_selector_sequence2 = 'HASH class attrib pseudo negation '
element_name = 'element_name'
negation_arg = 'type_selector universal HASH class attrib pseudo'
negationend = ')'
attname = 'prefix attribute'
attname2 = 'attribute'
attcombinator = 'combinator ]' # optional
attvalue = 'value' # optional
attend = ']'
expressionstart = 'PLUS - DIMENSION NUMBER STRING IDENT'
expression = expressionstart + ' )'
combinator = ' combinator'
def _COMMENT(expected, seq, token, tokenizer=None):
"special implementation for comment token"
append(seq, cssutils.css.CSSComment([token]), 'COMMENT',
token=token)
return expected
def _S(expected, seq, token, tokenizer=None):
# S
context = new['context'][-1]
if context.startswith('pseudo-'):
if seq and seq[-1].value not in u'+-':
# e.g. x:func(a + b)
append(seq, S, 'S', token=token)
return expected
elif context != 'attrib' and 'combinator' in expected:
append(seq, S, 'descendant', token=token)
return simple_selector_sequence + combinator
else:
return expected
def _universal(expected, seq, token, tokenizer=None):
# *|* or prefix|*
context = new['context'][-1]
val = self._tokenvalue(token)
if 'universal' in expected:
append(seq, val, 'universal', token=token)
if 'negation' == context:
return negationend
else:
return simple_selector_sequence2 + combinator
else:
new['wellformed'] = False
self._log.error(
u'Selector: Unexpected universal.', token=token)
return expected
def _namespace_prefix(expected, seq, token, tokenizer=None):
# prefix| => element_name
# or prefix| => attribute_name if attrib
context = new['context'][-1]
val = self._tokenvalue(token)
if 'attrib' == context and 'prefix' in expected:
# [PREFIX|att]
append(seq, val, '_PREFIX', token=token)
return attname2
elif 'type_selector' in expected:
# PREFIX|*
append(seq, val, '_PREFIX', token=token)
return element_name
else:
new['wellformed'] = False
self._log.error(
u'Selector: Unexpected namespace prefix.', token=token)
return expected
def _pseudo(expected, seq, token, tokenizer=None):
# pseudo-class or pseudo-element :a ::a :a( ::a(
"""
/* '::' starts a pseudo-element, ':' a pseudo-class */
/* Exceptions: :first-line, :first-letter, :before and
:after. */
/* Note that pseudo-elements are restricted to one per selector
and */
/* occur only in the last simple_selector_sequence. */
"""
context = new['context'][-1]
val, typ = self._tokenvalue(token, normalize=True),\
self._type(token)
if 'pseudo' in expected:
if val in (':first-line',
':first-letter',
':before',
':after'):
# always pseudo-element ???
typ = 'pseudo-element'
append(seq, val, typ, token=token)
if val.endswith(u'('):
# function
# "pseudo-" "class" or "element"
new['context'].append(typ)
return expressionstart
elif 'negation' == context:
return negationend
elif 'pseudo-element' == typ:
# only one per element, check at ) also!
return combinator
else:
return simple_selector_sequence2 + combinator
else:
new['wellformed'] = False
self._log.error(
u'Selector: Unexpected start of pseudo.', token=token)
return expected
def _expression(expected, seq, token, tokenizer=None):
# [ [ PLUS | '-' | DIMENSION | NUMBER | STRING | IDENT ] S* ]+
context = new['context'][-1]
val, typ = self._tokenvalue(token), self._type(token)
if context.startswith('pseudo-'):
append(seq, val, typ, token=token)
return expression
else:
new['wellformed'] = False
self._log.error(
u'Selector: Unexpected %s.' % typ, token=token)
return expected
def _attcombinator(expected, seq, token, tokenizer=None):
# context: attrib
# PREFIXMATCH | SUFFIXMATCH | SUBSTRINGMATCH | INCLUDES |
# DASHMATCH
context = new['context'][-1]
val, typ = self._tokenvalue(token), self._type(token)
if 'attrib' == context and 'combinator' in expected:
# combinator in attrib
append(seq, val, typ.lower(), token=token)
return attvalue
else:
new['wellformed'] = False
self._log.error(
u'Selector: Unexpected %s.' % typ, token=token)
return expected
def _string(expected, seq, token, tokenizer=None):
# identifier
context = new['context'][-1]
typ, val = self._type(token), self._stringtokenvalue(token)
# context: attrib
if 'attrib' == context and 'value' in expected:
# attrib: [...=VALUE]
append(seq, val, typ, token=token)
return attend
# context: pseudo
elif context.startswith('pseudo-'):
# :func(...)
append(seq, val, typ, token=token)
return expression
else:
new['wellformed'] = False
self._log.error(
u'Selector: Unexpected STRING.', token=token)
return expected
def _ident(expected, seq, token, tokenizer=None):
# identifier
context = new['context'][-1]
val, typ = self._tokenvalue(token), self._type(token)
# context: attrib
if 'attrib' == context and 'attribute' in expected:
# attrib: [...|ATT...]
append(seq, val, 'attribute-selector', token=token)
return attcombinator
elif 'attrib' == context and 'value' in expected:
# attrib: [...=VALUE]
append(seq, val, 'attribute-value', token=token)
return attend
# context: negation
elif 'negation' == context:
# negation: (prefix|IDENT)
append(seq, val, 'negation-type-selector', token=token)
return negationend
# context: pseudo
elif context.startswith('pseudo-'):
# :func(...)
append(seq, val, typ, token=token)
return expression
elif 'type_selector' in expected or element_name == expected:
# element name after ns or complete type_selector
append(seq, val, 'type-selector', token=token)
return simple_selector_sequence2 + combinator
else:
new['wellformed'] = False
self._log.error(u'Selector: Unexpected IDENT.', token=token)
return expected
def _class(expected, seq, token, tokenizer=None):
# .IDENT
context = new['context'][-1]
val = self._tokenvalue(token)
if 'class' in expected:
append(seq, val, 'class', token=token)
if 'negation' == context:
return negationend
else:
return simple_selector_sequence2 + combinator
else:
new['wellformed'] = False
self._log.error(u'Selector: Unexpected class.', token=token)
return expected
def _hash(expected, seq, token, tokenizer=None):
# #IDENT
context = new['context'][-1]
val = self._tokenvalue(token)
if 'HASH' in expected:
append(seq, val, 'id', token=token)
if 'negation' == context:
return negationend
else:
return simple_selector_sequence2 + combinator
else:
new['wellformed'] = False
self._log.error(u'Selector: Unexpected HASH.', token=token)
return expected
def _char(expected, seq, token, tokenizer=None):
# + > ~ ) [ ] + -
context = new['context'][-1]
val = self._tokenvalue(token)
# context: attrib
if u']' == val and 'attrib' == context and ']' in expected:
# end of attrib
append(seq, val, 'attribute-end', token=token)
context = new['context'].pop() # attrib is done
context = new['context'][-1]
if 'negation' == context:
return negationend
else:
return simple_selector_sequence2 + combinator
elif u'=' == val and 'attrib' == context\
and 'combinator' in expected:
# combinator in attrib
append(seq, val, 'equals', token=token)
return attvalue
# context: negation
elif u')' == val and 'negation' == context and u')' in expected:
# not(negation_arg)"
append(seq, val, 'negation-end', token=token)
new['context'].pop() # negation is done
context = new['context'][-1]
return simple_selector_sequence + combinator
# context: pseudo (at least one expression)
elif val in u'+-' and context.startswith('pseudo-'):
# :func(+ -)"
_names = {'+': 'plus', '-': 'minus'}
if val == u'+' and seq and seq[-1].value == S:
seq.replace(-1, val, _names[val])
else:
append(seq, val, _names[val],
token=token)
return expression
elif u')' == val and context.startswith('pseudo-') and\
expression == expected:
# :func(expression)"
append(seq, val, 'function-end', token=token)
new['context'].pop() # pseudo is done
if 'pseudo-element' == context:
return combinator
else:
return simple_selector_sequence + combinator
# context: ROOT
elif u'[' == val and 'attrib' in expected:
# start of [attrib]
append(seq, val, 'attribute-start', token=token)
new['context'].append('attrib')
return attname
elif val in u'+>~' and 'combinator' in expected:
# no other combinator except S may be following
_names = {
'>': 'child',
'+': 'adjacent-sibling',
'~': 'following-sibling'}
if seq and seq[-1].value == S:
seq.replace(-1, val, _names[val])
else:
append(seq, val, _names[val], token=token)
return simple_selector_sequence
elif u',' == val:
# not a selectorlist
new['wellformed'] = False
self._log.error(
u'Selector: Single selector only.',
error=xml.dom.InvalidModificationErr,
token=token)
return expected
else:
new['wellformed'] = False
self._log.error(
u'Selector: Unexpected CHAR.', token=token)
return expected
def _negation(expected, seq, token, tokenizer=None):
# not(
context = new['context'][-1]
val = self._tokenvalue(token, normalize=True)
if 'negation' in expected:
new['context'].append('negation')
append(seq, val, 'negation-start', token=token)
return negation_arg
else:
new['wellformed'] = False
self._log.error(
u'Selector: Unexpected negation.', token=token)
return expected
def _atkeyword(expected, seq, token, tokenizer=None):
"invalidates selector"
new['wellformed'] = False
self._log.error(
u'Selector: Unexpected ATKEYWORD.', token=token)
return expected
# expected: only|not or mediatype, mediatype, feature, and
newseq = self._tempSeq()
wellformed, expected = self._parse(
expected=simple_selector_sequence,
seq=newseq, tokenizer=tokenizer,
productions={'CHAR': _char,
'class': _class,
'HASH': _hash,
'STRING': _string,
'IDENT': _ident,
'namespace_prefix': _namespace_prefix,
'negation': _negation,
'pseudo-class': _pseudo,
'pseudo-element': _pseudo,
'universal': _universal,
# pseudo
'NUMBER': _expression,
'DIMENSION': _expression,
# attribute
'PREFIXMATCH': _attcombinator,
'SUFFIXMATCH': _attcombinator,
'SUBSTRINGMATCH': _attcombinator,
'DASHMATCH': _attcombinator,
'INCLUDES': _attcombinator,
'S': _S,
'COMMENT': _COMMENT,
'ATKEYWORD': _atkeyword})
wellformed = wellformed and new['wellformed']
# post condition
if len(new['context']) > 1 or not newseq:
wellformed = False
self._log.error(u'Selector: Invalid or incomplete selector: %s'
% self._valuestr(selectorText))
if expected == 'element_name':
wellformed = False
self._log.error(u'Selector: No element name found: %s'
% self._valuestr(selectorText))
if expected == simple_selector_sequence and newseq:
wellformed = False
self._log.error(u'Selector: Cannot end with combinator: %s'
% self._valuestr(selectorText))
if newseq and hasattr(newseq[-1].value, 'strip') \
and newseq[-1].value.strip() == u'':
del newseq[-1]
# set
if wellformed:
self.__namespaces = namespaces
self._element = new['element']
self._specificity = tuple(new['specificity'])
self._setSeq(newseq)
# filter that only used ones are kept
self.__namespaces = self._getUsedNamespaces()
selectorText = property(_getSelectorText, _setSelectorText,
doc=u"(DOM) The parsable textual representation of "
u"the selector.")
specificity = property(lambda self: self._specificity,
doc="""Specificity of this selector (READONLY).
Tuple of (a, b, c, d) where:
a
presence of style in document, always 0 if not used on a
document
b
number of ID selectors
c
number of .class selectors
d
number of Element (type) selectors""")
wellformed = property(lambda self: bool(len(self.seq)))
@Deprecated('Use property parent instead')
def _getParentList(self):
return self.parent
parentList = property(_getParentList,
doc="DEPRECATED, see property parent instead")