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