1 """Selector is a single Selector of a CSSStyleRule SelectorList.
2
3 Partly implements
4 http://www.w3.org/TR/css3-selectors/
5
6 TODO
7 - .contains(selector)
8 - .isSubselector(selector)
9 """
10 __all__ = ['Selector']
11 __docformat__ = 'restructuredtext'
12 __version__ = '$Id: selector.py 1352 2008-07-12 11:01:04Z cthedot $'
13
14 import xml.dom
15 import cssutils
16 from cssutils.util import _SimpleNamespaces
17
19 """
20 (cssutils) a single selector in a SelectorList of a CSSStyleRule
21
22 Properties
23 ==========
24 element
25 Effective element target of this selector
26 parentList: of type SelectorList, readonly
27 The SelectorList that contains this selector or None if this
28 Selector is not attached to a SelectorList.
29 selectorText
30 textual representation of this Selector
31 seq
32 sequence of Selector parts including comments
33 specificity (READONLY)
34 tuple of (a, b, c, d) where:
35
36 a
37 presence of style in document, always 0 if not used on a document
38 b
39 number of ID selectors
40 c
41 number of .class selectors
42 d
43 number of Element (type) selectors
44
45 wellformed
46 if this selector is wellformed regarding the Selector spec
47
48 Format
49 ======
50 ::
51
52 # implemented in SelectorList
53 selectors_group
54 : selector [ COMMA S* selector ]*
55 ;
56
57 selector
58 : simple_selector_sequence [ combinator simple_selector_sequence ]*
59 ;
60
61 combinator
62 /* combinators can be surrounded by white space */
63 : PLUS S* | GREATER S* | TILDE S* | S+
64 ;
65
66 simple_selector_sequence
67 : [ type_selector | universal ]
68 [ HASH | class | attrib | pseudo | negation ]*
69 | [ HASH | class | attrib | pseudo | negation ]+
70 ;
71
72 type_selector
73 : [ namespace_prefix ]? element_name
74 ;
75
76 namespace_prefix
77 : [ IDENT | '*' ]? '|'
78 ;
79
80 element_name
81 : IDENT
82 ;
83
84 universal
85 : [ namespace_prefix ]? '*'
86 ;
87
88 class
89 : '.' IDENT
90 ;
91
92 attrib
93 : '[' S* [ namespace_prefix ]? IDENT S*
94 [ [ PREFIXMATCH |
95 SUFFIXMATCH |
96 SUBSTRINGMATCH |
97 '=' |
98 INCLUDES |
99 DASHMATCH ] S* [ IDENT | STRING ] S*
100 ]? ']'
101 ;
102
103 pseudo
104 /* '::' starts a pseudo-element, ':' a pseudo-class */
105 /* Exceptions: :first-line, :first-letter, :before and :after. */
106 /* Note that pseudo-elements are restricted to one per selector and */
107 /* occur only in the last simple_selector_sequence. */
108 : ':' ':'? [ IDENT | functional_pseudo ]
109 ;
110
111 functional_pseudo
112 : FUNCTION S* expression ')'
113 ;
114
115 expression
116 /* In CSS3, the expressions are identifiers, strings, */
117 /* or of the form "an+b" */
118 : [ [ PLUS | '-' | DIMENSION | NUMBER | STRING | IDENT ] S* ]+
119 ;
120
121 negation
122 : NOT S* negation_arg S* ')'
123 ;
124
125 negation_arg
126 : type_selector | universal | HASH | class | attrib | pseudo
127 ;
128
129 """
130 - def __init__(self, selectorText=None, parentList=None,
131 readonly=False):
132 """
133 :Parameters:
134 selectorText
135 initial value of this selector
136 parentList
137 a SelectorList
138 readonly
139 default to False
140 """
141 super(Selector, self).__init__()
142
143 self.__namespaces = _SimpleNamespaces()
144 self._element = None
145 self._parent = parentList
146 self._specificity = (0, 0, 0, 0)
147
148 if selectorText:
149 self.selectorText = selectorText
150
151 self._readonly = readonly
152
154 "uses own namespaces if not attached to a sheet, else the sheet's ones"
155 try:
156 return self._parent.parentRule.parentStyleSheet.namespaces
157 except AttributeError:
158 return self.__namespaces
159
160 _namespaces = property(__getNamespaces, doc="""if this Selector is attached
161 to a CSSStyleSheet the namespaces of that sheet are mirrored here.
162 While the Selector (or parent SelectorList or parentRule(s) of that are
163 not attached a own dict of {prefix: namespaceURI} is used.""")
164
165
166 element = property(lambda self: self._element,
167 doc=u"Effective element target of this selector.")
168
169 parentList = property(lambda self: self._parent,
170 doc="(DOM) The SelectorList that contains this Selector or\
171 None if this Selector is not attached to a SelectorList.")
172
174 """
175 returns serialized format
176 """
177 return cssutils.ser.do_css_Selector(self)
178
179 - def _setSelectorText(self, selectorText):
180 """
181 :param selectorText:
182 parsable string or a tuple of (selectorText, dict-of-namespaces).
183 Given namespaces are ignored if this object is attached to a
184 CSSStyleSheet!
185
186 :Exceptions:
187 - `NAMESPACE_ERR`: (self)
188 Raised if the specified selector uses an unknown namespace
189 prefix.
190 - `SYNTAX_ERR`: (self)
191 Raised if the specified CSS string value has a syntax error
192 and is unparsable.
193 - `NO_MODIFICATION_ALLOWED_ERR`: (self)
194 Raised if this rule is readonly.
195 """
196 self._checkReadonly()
197
198
199 selectorText, namespaces = self._splitNamespacesOff(selectorText)
200 try:
201
202 namespaces = self.parentList.parentRule.parentStyleSheet.namespaces
203 except AttributeError:
204 pass
205 tokenizer = self._tokenize2(selectorText)
206 if not tokenizer:
207 self._log.error(u'Selector: No selectorText given.')
208 else:
209
210
211
212
213
214
215
216
217 tokens = []
218 for t in tokenizer:
219 typ, val, lin, col = t
220 if val == u':' and tokens and\
221 self._tokenvalue(tokens[-1]) == ':':
222
223 tokens[-1] = (typ, u'::', lin, col)
224
225 elif typ == 'IDENT' and tokens\
226 and self._tokenvalue(tokens[-1]) == u'.':
227
228 tokens[-1] = ('class', u'.'+val, lin, col)
229 elif typ == 'IDENT' and tokens and \
230 self._tokenvalue(tokens[-1]).startswith(u':') and\
231 not self._tokenvalue(tokens[-1]).endswith(u'('):
232
233 if self._tokenvalue(tokens[-1]).startswith(u'::'):
234 t = 'pseudo-element'
235 else:
236 t = 'pseudo-class'
237 tokens[-1] = (t, self._tokenvalue(tokens[-1])+val, lin, col)
238
239 elif typ == 'FUNCTION' and val == u'not(' and tokens and \
240 u':' == self._tokenvalue(tokens[-1]):
241 tokens[-1] = ('negation', u':' + val, lin, tokens[-1][3])
242 elif typ == 'FUNCTION' and tokens\
243 and self._tokenvalue(tokens[-1]).startswith(u':'):
244
245 if self._tokenvalue(tokens[-1]).startswith(u'::'):
246 t = 'pseudo-element'
247 else:
248 t = 'pseudo-class'
249 tokens[-1] = (t, self._tokenvalue(tokens[-1])+val, lin, col)
250
251 elif val == u'*' and tokens and\
252 self._type(tokens[-1]) == 'namespace_prefix' and\
253 self._tokenvalue(tokens[-1]).endswith(u'|'):
254
255 tokens[-1] = ('universal', self._tokenvalue(tokens[-1])+val,
256 lin, col)
257 elif val == u'*':
258
259 tokens.append(('universal', val, lin, col))
260
261 elif val == u'|' and tokens and\
262 self._type(tokens[-1]) in (self._prods.IDENT, 'universal') and\
263 self._tokenvalue(tokens[-1]).find(u'|') == -1:
264
265 tokens[-1] = ('namespace_prefix',
266 self._tokenvalue(tokens[-1])+u'|', lin, col)
267 elif val == u'|':
268
269 tokens.append(('namespace_prefix', val, lin, col))
270
271 else:
272 tokens.append(t)
273
274
275 tokenizer = (t for t in tokens)
276
277
278 new = {'context': [''],
279 'element': None,
280 '_PREFIX': None,
281 'specificity': [0, 0, 0, 0],
282 'wellformed': True
283 }
284
285 S = u' '
286
287 def append(seq, val, typ=None, token=None):
288 """
289 appends to seq
290
291 namespace_prefix, IDENT will be combined to a tuple
292 (prefix, name) where prefix might be None, the empty string
293 or a prefix.
294
295 Saved are also:
296 - specificity definition: style, id, class/att, type
297 - element: the element this Selector is for
298 """
299 context = new['context'][-1]
300 if token:
301 line, col = token[2], token[3]
302 else:
303 line, col = None, None
304
305 if typ == '_PREFIX':
306
307 new['_PREFIX'] = val[:-1]
308
309 return
310
311 if new['_PREFIX'] is not None:
312
313 prefix, new['_PREFIX'] = new['_PREFIX'], None
314 elif typ == 'universal' and '|' in val:
315
316 prefix, val = val.split('|')
317 else:
318 prefix = None
319
320
321 if (typ.endswith('-selector') or typ == 'universal') and not (
322 'attribute-selector' == typ and not prefix):
323
324 if prefix == u'*':
325
326 namespaceURI = cssutils._ANYNS
327 elif prefix is None:
328
329 namespaceURI = namespaces.get(u'', None)
330 elif prefix == u'':
331
332 namespaceURI = u''
333 else:
334
335 try:
336 namespaceURI = namespaces[prefix]
337 except KeyError:
338 new['wellformed'] = False
339 self._log.error(
340 u'Selector: No namespaceURI found for prefix %r' %
341 prefix, token=token, error=xml.dom.NamespaceErr)
342 return
343
344
345 val = (namespaceURI, val)
346
347
348 if not context or context == 'negation':
349 if 'id' == typ:
350 new['specificity'][1] += 1
351 elif 'class' == typ or '[' == val:
352 new['specificity'][2] += 1
353 elif typ in ('type-selector', 'negation-type-selector',
354 'pseudo-element'):
355 new['specificity'][3] += 1
356 if not context and typ in ('type-selector', 'universal'):
357
358 new['element'] = val
359
360 seq.append(val, typ, line=line, col=col)
361
362
363 simple_selector_sequence = 'type_selector universal HASH class attrib pseudo negation '
364 simple_selector_sequence2 = 'HASH class attrib pseudo negation '
365
366 element_name = 'element_name'
367
368 negation_arg = 'type_selector universal HASH class attrib pseudo'
369 negationend = ')'
370
371 attname = 'prefix attribute'
372 attname2 = 'attribute'
373 attcombinator = 'combinator ]'
374 attvalue = 'value'
375 attend = ']'
376
377 expressionstart = 'PLUS - DIMENSION NUMBER STRING IDENT'
378 expression = expressionstart + ' )'
379
380 combinator = ' combinator'
381
382 def _COMMENT(expected, seq, token, tokenizer=None):
383 "special implementation for comment token"
384 append(seq, cssutils.css.CSSComment([token]), 'COMMENT',
385 token=token)
386 return expected
387
388 def _S(expected, seq, token, tokenizer=None):
389
390 context = new['context'][-1]
391 if context.startswith('pseudo-'):
392 if seq and seq[-1].value not in u'+-':
393
394 append(seq, S, 'S', token=token)
395 return expected
396
397 elif context != 'attrib' and 'combinator' in expected:
398 append(seq, S, 'descendant', token=token)
399 return simple_selector_sequence + combinator
400
401 else:
402 return expected
403
404 def _universal(expected, seq, token, tokenizer=None):
405
406 context = new['context'][-1]
407 val = self._tokenvalue(token)
408 if 'universal' in expected:
409 append(seq, val, 'universal', token=token)
410
411 if 'negation' == context:
412 return negationend
413 else:
414 return simple_selector_sequence2 + combinator
415
416 else:
417 new['wellformed'] = False
418 self._log.error(
419 u'Selector: Unexpected universal.', token=token)
420 return expected
421
422 def _namespace_prefix(expected, seq, token, tokenizer=None):
423
424
425 context = new['context'][-1]
426 val = self._tokenvalue(token)
427 if 'attrib' == context and 'prefix' in expected:
428
429 append(seq, val, '_PREFIX', token=token)
430 return attname2
431 elif 'type_selector' in expected:
432
433 append(seq, val, '_PREFIX', token=token)
434 return element_name
435 else:
436 new['wellformed'] = False
437 self._log.error(
438 u'Selector: Unexpected namespace prefix.', token=token)
439 return expected
440
441 def _pseudo(expected, seq, token, tokenizer=None):
442
443 """
444 /* '::' starts a pseudo-element, ':' a pseudo-class */
445 /* Exceptions: :first-line, :first-letter, :before and :after. */
446 /* Note that pseudo-elements are restricted to one per selector and */
447 /* occur only in the last simple_selector_sequence. */
448 """
449 context = new['context'][-1]
450 val, typ = self._tokenvalue(token, normalize=True), self._type(token)
451 if 'pseudo' in expected:
452 if val in (':first-line', ':first-letter', ':before', ':after'):
453
454 typ = 'pseudo-element'
455 append(seq, val, typ, token=token)
456
457 if val.endswith(u'('):
458
459 new['context'].append(typ)
460 return expressionstart
461 elif 'negation' == context:
462 return negationend
463 elif 'pseudo-element' == typ:
464
465 return combinator
466 else:
467 return simple_selector_sequence2 + combinator
468
469 else:
470 new['wellformed'] = False
471 self._log.error(
472 u'Selector: Unexpected start of pseudo.', token=token)
473 return expected
474
475 def _expression(expected, seq, token, tokenizer=None):
476
477 context = new['context'][-1]
478 val, typ = self._tokenvalue(token), self._type(token)
479 if context.startswith('pseudo-'):
480 append(seq, val, typ, token=token)
481 return expression
482 else:
483 new['wellformed'] = False
484 self._log.error(
485 u'Selector: Unexpected %s.' % typ, token=token)
486 return expected
487
488 def _attcombinator(expected, seq, token, tokenizer=None):
489
490
491
492 context = new['context'][-1]
493 val, typ = self._tokenvalue(token), self._type(token)
494 if 'attrib' == context and 'combinator' in expected:
495
496 append(seq, val, typ.lower(), token=token)
497 return attvalue
498 else:
499 new['wellformed'] = False
500 self._log.error(
501 u'Selector: Unexpected %s.' % typ, token=token)
502 return expected
503
504 def _string(expected, seq, token, tokenizer=None):
505
506 context = new['context'][-1]
507 typ, val = self._type(token), self._stringtokenvalue(token)
508
509
510 if 'attrib' == context and 'value' in expected:
511
512 append(seq, val, typ, token=token)
513 return attend
514
515
516 elif context.startswith('pseudo-'):
517
518 append(seq, val, typ, token=token)
519 return expression
520
521 else:
522 new['wellformed'] = False
523 self._log.error(
524 u'Selector: Unexpected STRING.', token=token)
525 return expected
526
527 def _ident(expected, seq, token, tokenizer=None):
528
529 context = new['context'][-1]
530 val, typ = self._tokenvalue(token), self._type(token)
531
532
533 if 'attrib' == context and 'attribute' in expected:
534
535 append(seq, val, 'attribute-selector', token=token)
536 return attcombinator
537
538 elif 'attrib' == context and 'value' in expected:
539
540 append(seq, val, 'attribute-value', token=token)
541 return attend
542
543
544 elif 'negation' == context:
545
546 append(seq, val, 'negation-type-selector', token=token)
547 return negationend
548
549
550 elif context.startswith('pseudo-'):
551
552 append(seq, val, typ, token=token)
553 return expression
554
555 elif 'type_selector' in expected or element_name == expected:
556
557 append(seq, val, 'type-selector', token=token)
558 return simple_selector_sequence2 + combinator
559
560 else:
561 new['wellformed'] = False
562 self._log.error(
563 u'Selector: Unexpected IDENT.',
564 token=token)
565 return expected
566
567 def _class(expected, seq, token, tokenizer=None):
568
569 context = new['context'][-1]
570 val = self._tokenvalue(token)
571 if 'class' in expected:
572 append(seq, val, 'class', token=token)
573
574 if 'negation' == context:
575 return negationend
576 else:
577 return simple_selector_sequence2 + combinator
578
579 else:
580 new['wellformed'] = False
581 self._log.error(
582 u'Selector: Unexpected class.', token=token)
583 return expected
584
585 def _hash(expected, seq, token, tokenizer=None):
586
587 context = new['context'][-1]
588 val = self._tokenvalue(token)
589 if 'HASH' in expected:
590 append(seq, val, 'id', token=token)
591
592 if 'negation' == context:
593 return negationend
594 else:
595 return simple_selector_sequence2 + combinator
596
597 else:
598 new['wellformed'] = False
599 self._log.error(
600 u'Selector: Unexpected HASH.', token=token)
601 return expected
602
603 def _char(expected, seq, token, tokenizer=None):
604
605 context = new['context'][-1]
606 val = self._tokenvalue(token)
607
608
609 if u']' == val and 'attrib' == context and ']' in expected:
610
611 append(seq, val, 'attribute-end', token=token)
612 context = new['context'].pop()
613 context = new['context'][-1]
614 if 'negation' == context:
615 return negationend
616 else:
617 return simple_selector_sequence2 + combinator
618
619 elif u'=' == val and 'attrib' == context and 'combinator' in expected:
620
621 append(seq, val, 'equals', token=token)
622 return attvalue
623
624
625 elif u')' == val and 'negation' == context and u')' in expected:
626
627 append(seq, val, 'negation-end', token=token)
628 new['context'].pop()
629 context = new['context'][-1]
630 return simple_selector_sequence + combinator
631
632
633 elif val in u'+-' and context.startswith('pseudo-'):
634
635 _names = {'+': 'plus', '-': 'minus'}
636 if val == u'+' and seq and seq[-1].value == S:
637 seq.replace(-1, val, _names[val])
638 else:
639 append(seq, val, _names[val],
640 token=token)
641 return expression
642
643 elif u')' == val and context.startswith('pseudo-') and\
644 expression == expected:
645
646 append(seq, val, 'function-end', token=token)
647 new['context'].pop()
648 if 'pseudo-element' == context:
649 return combinator
650 else:
651 return simple_selector_sequence + combinator
652
653
654 elif u'[' == val and 'attrib' in expected:
655
656 append(seq, val, 'attribute-start', token=token)
657 new['context'].append('attrib')
658 return attname
659
660 elif val in u'+>~' and 'combinator' in expected:
661
662 _names = {
663 '>': 'child',
664 '+': 'adjacent-sibling',
665 '~': 'following-sibling'}
666 if seq and seq[-1].value == S:
667 seq.replace(-1, val, _names[val])
668 else:
669 append(seq, val, _names[val], token=token)
670 return simple_selector_sequence
671
672 elif u',' == val:
673
674 new['wellformed'] = False
675 self._log.error(
676 u'Selector: Single selector only.',
677 error=xml.dom.InvalidModificationErr,
678 token=token)
679
680 else:
681 new['wellformed'] = False
682 self._log.error(
683 u'Selector: Unexpected CHAR.', token=token)
684 return expected
685
686 def _negation(expected, seq, token, tokenizer=None):
687
688 context = new['context'][-1]
689 val = self._tokenvalue(token, normalize=True)
690 if 'negation' in expected:
691 new['context'].append('negation')
692 append(seq, val, 'negation-start', token=token)
693 return negation_arg
694 else:
695 new['wellformed'] = False
696 self._log.error(
697 u'Selector: Unexpected negation.', token=token)
698 return expected
699
700
701 newseq = self._tempSeq()
702
703 wellformed, expected = self._parse(expected=simple_selector_sequence,
704 seq=newseq, tokenizer=tokenizer,
705 productions={'CHAR': _char,
706 'class': _class,
707 'HASH': _hash,
708 'STRING': _string,
709 'IDENT': _ident,
710 'namespace_prefix': _namespace_prefix,
711 'negation': _negation,
712 'pseudo-class': _pseudo,
713 'pseudo-element': _pseudo,
714 'universal': _universal,
715
716 'NUMBER': _expression,
717 'DIMENSION': _expression,
718
719 'PREFIXMATCH': _attcombinator,
720 'SUFFIXMATCH': _attcombinator,
721 'SUBSTRINGMATCH': _attcombinator,
722 'DASHMATCH': _attcombinator,
723 'INCLUDES': _attcombinator,
724
725 'S': _S,
726 'COMMENT': _COMMENT})
727 wellformed = wellformed and new['wellformed']
728
729
730 if len(new['context']) > 1 or not newseq:
731 wellformed = False
732 self._log.error(u'Selector: Invalid or incomplete selector: %s' %
733 self._valuestr(selectorText))
734
735 if expected == 'element_name':
736 wellformed = False
737 self._log.error(u'Selector: No element name found: %s' %
738 self._valuestr(selectorText))
739
740 if expected == simple_selector_sequence and newseq:
741 wellformed = False
742 self._log.error(u'Selector: Cannot end with combinator: %s' %
743 self._valuestr(selectorText))
744
745 if newseq and hasattr(newseq[-1].value, 'strip') and \
746 newseq[-1].value.strip() == u'':
747 del newseq[-1]
748
749
750 if wellformed:
751 self.__namespaces = namespaces
752 self._element = new['element']
753 self._specificity = tuple(new['specificity'])
754 self._setSeq(newseq)
755
756 self.__namespaces = self._getUsedNamespaces()
757
758 selectorText = property(_getSelectorText, _setSelectorText,
759 doc="(DOM) The parsable textual representation of the selector.")
760
761
762 specificity = property(lambda self: self._specificity,
763 doc="Specificity of this selector (READONLY).")
764
765 wellformed = property(lambda self: bool(len(self.seq)))
766
774
779
781 "returns list of actually used URIs in this Selector"
782 uris = set()
783 for item in self.seq:
784 type_, val = item.type, item.value
785 if type_.endswith(u'-selector') or type_ == u'universal' and \
786 type(val) == tuple and val[0] not in (None, u'*'):
787 uris.add(val[0])
788 return uris
789
798