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 1429 2008-08-11 19:01:52Z 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(log=self._log)
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
201 try:
202
203 namespaces = self.parentList.parentRule.parentStyleSheet.namespaces
204 except AttributeError:
205 pass
206 tokenizer = self._tokenize2(selectorText)
207 if not tokenizer:
208 self._log.error(u'Selector: No selectorText given.')
209 else:
210
211
212
213
214
215
216
217
218 tokens = []
219 for t in tokenizer:
220 typ, val, lin, col = t
221 if val == u':' and tokens and\
222 self._tokenvalue(tokens[-1]) == ':':
223
224 tokens[-1] = (typ, u'::', lin, col)
225
226 elif typ == 'IDENT' and tokens\
227 and self._tokenvalue(tokens[-1]) == u'.':
228
229 tokens[-1] = ('class', u'.'+val, lin, col)
230 elif typ == 'IDENT' and tokens and \
231 self._tokenvalue(tokens[-1]).startswith(u':') and\
232 not self._tokenvalue(tokens[-1]).endswith(u'('):
233
234 if self._tokenvalue(tokens[-1]).startswith(u'::'):
235 t = 'pseudo-element'
236 else:
237 t = 'pseudo-class'
238 tokens[-1] = (t, self._tokenvalue(tokens[-1])+val, lin, col)
239
240 elif typ == 'FUNCTION' and val == u'not(' and tokens and \
241 u':' == self._tokenvalue(tokens[-1]):
242 tokens[-1] = ('negation', u':' + val, lin, tokens[-1][3])
243 elif typ == 'FUNCTION' and tokens\
244 and self._tokenvalue(tokens[-1]).startswith(u':'):
245
246 if self._tokenvalue(tokens[-1]).startswith(u'::'):
247 t = 'pseudo-element'
248 else:
249 t = 'pseudo-class'
250 tokens[-1] = (t, self._tokenvalue(tokens[-1])+val, lin, col)
251
252 elif val == u'*' and tokens and\
253 self._type(tokens[-1]) == 'namespace_prefix' and\
254 self._tokenvalue(tokens[-1]).endswith(u'|'):
255
256 tokens[-1] = ('universal', self._tokenvalue(tokens[-1])+val,
257 lin, col)
258 elif val == u'*':
259
260 tokens.append(('universal', val, lin, col))
261
262 elif val == u'|' and tokens and\
263 self._type(tokens[-1]) in (self._prods.IDENT, 'universal') and\
264 self._tokenvalue(tokens[-1]).find(u'|') == -1:
265
266 tokens[-1] = ('namespace_prefix',
267 self._tokenvalue(tokens[-1])+u'|', lin, col)
268 elif val == u'|':
269
270 tokens.append(('namespace_prefix', val, lin, col))
271
272 else:
273 tokens.append(t)
274
275
276 tokenizer = (t for t in tokens)
277
278
279 new = {'context': [''],
280 'element': None,
281 '_PREFIX': None,
282 'specificity': [0, 0, 0, 0],
283 'wellformed': True
284 }
285
286 S = u' '
287
288 def append(seq, val, typ=None, token=None):
289 """
290 appends to seq
291
292 namespace_prefix, IDENT will be combined to a tuple
293 (prefix, name) where prefix might be None, the empty string
294 or a prefix.
295
296 Saved are also:
297 - specificity definition: style, id, class/att, type
298 - element: the element this Selector is for
299 """
300 context = new['context'][-1]
301 if token:
302 line, col = token[2], token[3]
303 else:
304 line, col = None, None
305
306 if typ == '_PREFIX':
307
308 new['_PREFIX'] = val[:-1]
309
310 return
311
312 if new['_PREFIX'] is not None:
313
314 prefix, new['_PREFIX'] = new['_PREFIX'], None
315 elif typ == 'universal' and '|' in val:
316
317 prefix, val = val.split('|')
318 else:
319 prefix = None
320
321
322 if (typ.endswith('-selector') or typ == 'universal') and not (
323 'attribute-selector' == typ and not prefix):
324
325 if prefix == u'*':
326
327 namespaceURI = cssutils._ANYNS
328 elif prefix is None:
329
330 namespaceURI = namespaces.get(u'', None)
331 elif prefix == u'':
332
333 namespaceURI = u''
334 else:
335
336
337 namespaceURI = namespaces[prefix]
338
339 if namespaceURI is None:
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 return expected
682
683 else:
684 new['wellformed'] = False
685 self._log.error(
686 u'Selector: Unexpected CHAR.', token=token)
687 return expected
688
689 def _negation(expected, seq, token, tokenizer=None):
690
691 context = new['context'][-1]
692 val = self._tokenvalue(token, normalize=True)
693 if 'negation' in expected:
694 new['context'].append('negation')
695 append(seq, val, 'negation-start', token=token)
696 return negation_arg
697 else:
698 new['wellformed'] = False
699 self._log.error(
700 u'Selector: Unexpected negation.', token=token)
701 return expected
702
703
704 newseq = self._tempSeq()
705
706 wellformed, expected = self._parse(expected=simple_selector_sequence,
707 seq=newseq, tokenizer=tokenizer,
708 productions={'CHAR': _char,
709 'class': _class,
710 'HASH': _hash,
711 'STRING': _string,
712 'IDENT': _ident,
713 'namespace_prefix': _namespace_prefix,
714 'negation': _negation,
715 'pseudo-class': _pseudo,
716 'pseudo-element': _pseudo,
717 'universal': _universal,
718
719 'NUMBER': _expression,
720 'DIMENSION': _expression,
721
722 'PREFIXMATCH': _attcombinator,
723 'SUFFIXMATCH': _attcombinator,
724 'SUBSTRINGMATCH': _attcombinator,
725 'DASHMATCH': _attcombinator,
726 'INCLUDES': _attcombinator,
727
728 'S': _S,
729 'COMMENT': _COMMENT})
730 wellformed = wellformed and new['wellformed']
731
732
733 if len(new['context']) > 1 or not newseq:
734 wellformed = False
735 self._log.error(u'Selector: Invalid or incomplete selector: %s' %
736 self._valuestr(selectorText))
737
738 if expected == 'element_name':
739 wellformed = False
740 self._log.error(u'Selector: No element name found: %s' %
741 self._valuestr(selectorText))
742
743 if expected == simple_selector_sequence and newseq:
744 wellformed = False
745 self._log.error(u'Selector: Cannot end with combinator: %s' %
746 self._valuestr(selectorText))
747
748 if newseq and hasattr(newseq[-1].value, 'strip') and \
749 newseq[-1].value.strip() == u'':
750 del newseq[-1]
751
752
753 if wellformed:
754 self.__namespaces = namespaces
755 self._element = new['element']
756 self._specificity = tuple(new['specificity'])
757 self._setSeq(newseq)
758
759 self.__namespaces = self._getUsedNamespaces()
760
761 selectorText = property(_getSelectorText, _setSelectorText,
762 doc="(DOM) The parsable textual representation of the selector.")
763
764
765 specificity = property(lambda self: self._specificity,
766 doc="Specificity of this selector (READONLY).")
767
768 wellformed = property(lambda self: bool(len(self.seq)))
769
777
782
784 "returns list of actually used URIs in this Selector"
785 uris = set()
786 for item in self.seq:
787 type_, val = item.type, item.value
788 if type_.endswith(u'-selector') or type_ == u'universal' and \
789 type(val) == tuple and val[0] not in (None, u'*'):
790 uris.add(val[0])
791 return uris
792
801