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-02 23:54:52 +0100 (Sa, 02 Feb 2008) $'
14 __version__ = '$LastChangedRevision: 977 $'
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 self.wellformed = False
150
151 if selectorText:
152 self.selectorText = selectorText
153
154 self._readonly = readonly
155
157 "uses own namespaces if not attached to a sheet, else the sheet's ones"
158 try:
159 return self._parent.parentRule.parentStyleSheet.namespaces
160 except AttributeError:
161 return self.__namespaces
162
163 _namespaces = property(__getNamespaces, doc="""if this Selector is attached
164 to a CSSStyleSheet the namespaces of that sheet are mirrored here.
165 While the Selector (or parent SelectorList or parentRule(s) of that are
166 not attached a own dict of {prefix: namespaceURI} is used.""")
167
168
169 element = property(lambda self: self._element,
170 doc=u"Effective element target of this selector.")
171
172 parentList = property(lambda self: self._parent,
173 doc="(DOM) The SelectorList that contains this Selector or\
174 None if this Selector is not attached to a SelectorList.")
175
177 """
178 returns serialized format
179 """
180 return cssutils.ser.do_css_Selector(self)
181
182 - def _setSelectorText(self, selectorText):
183 """
184 :param selectorText:
185 parsable string or a tuple of (selectorText, dict-of-namespaces).
186 Given namespaces are ignored if this object is attached to a
187 CSSStyleSheet!
188
189 :Exceptions:
190 - `NAMESPACE_ERR`: (self)
191 Raised if the specified selector uses an unknown namespace
192 prefix.
193 - `SYNTAX_ERR`: (self)
194 Raised if the specified CSS string value has a syntax error
195 and is unparsable.
196 - `NO_MODIFICATION_ALLOWED_ERR`: (self)
197 Raised if this rule is readonly.
198 """
199 self._checkReadonly()
200
201
202 selectorText, namespaces = self._splitNamespacesOff(selectorText)
203 try:
204
205 namespaces = self.parentList.parentRule.parentStyleSheet.namespaces
206 except AttributeError:
207 pass
208 tokenizer = self._tokenize2(selectorText)
209 if not tokenizer:
210 self._log.error(u'Selector: No selectorText given.')
211 else:
212
213
214
215
216
217
218
219
220 tokens = []
221 for t in tokenizer:
222 typ, val, lin, col = t
223 if val == u':' and tokens and\
224 self._tokenvalue(tokens[-1]) == ':':
225
226 tokens[-1] = (typ, u'::', lin, col)
227
228 elif typ == 'IDENT' and tokens\
229 and self._tokenvalue(tokens[-1]) == u'.':
230
231 tokens[-1] = ('class', u'.'+val, lin, col)
232 elif typ == 'IDENT' and tokens and \
233 self._tokenvalue(tokens[-1]).startswith(u':') and\
234 not self._tokenvalue(tokens[-1]).endswith(u'('):
235
236 if self._tokenvalue(tokens[-1]).startswith(u'::'):
237 t = 'pseudo-element'
238 else:
239 t = 'pseudo-class'
240 tokens[-1] = (t, self._tokenvalue(tokens[-1])+val, lin, col)
241
242 elif typ == 'FUNCTION' and val == u'not(' and tokens and \
243 u':' == self._tokenvalue(tokens[-1]):
244 tokens[-1] = ('negation', u':' + val, lin, tokens[-1][3])
245 elif typ == 'FUNCTION' and tokens\
246 and self._tokenvalue(tokens[-1]).startswith(u':'):
247
248 if self._tokenvalue(tokens[-1]).startswith(u'::'):
249 t = 'pseudo-element'
250 else:
251 t = 'pseudo-class'
252 tokens[-1] = (t, self._tokenvalue(tokens[-1])+val, lin, col)
253
254 elif val == u'*' and tokens and\
255 self._type(tokens[-1]) == 'namespace_prefix' and\
256 self._tokenvalue(tokens[-1]).endswith(u'|'):
257
258 tokens[-1] = ('universal', self._tokenvalue(tokens[-1])+val,
259 lin, col)
260 elif val == u'*':
261
262 tokens.append(('universal', val, lin, col))
263
264 elif val == u'|' and tokens and\
265 self._type(tokens[-1]) in ('IDENT', 'universal') and\
266 self._tokenvalue(tokens[-1]).find(u'|') == -1:
267
268 tokens[-1] = ('namespace_prefix',
269 self._tokenvalue(tokens[-1])+u'|', lin, col)
270 elif val == u'|':
271
272 tokens.append(('namespace_prefix', val, lin, col))
273
274 else:
275 tokens.append(t)
276
277
278 tokenizer = (t for t in tokens)
279
280
281 new = {'context': [''],
282 'element': None,
283 '_PREFIX': None,
284 'specificity': [0, 0, 0, 0],
285 'wellformed': True
286 }
287
288 S = u' '
289
290 def append(seq, val, typ=None, line=-1, col=0, context=None):
291 """
292 appends to seq
293
294 namespace_prefix, IDENT will be combined to a tuple
295 (prefix, name) where prefix might be None, the empty string
296 or a prefix.
297
298 Saved are also:
299 - specificity definition: style, id, class, type
300 - element: the element this Selector is for
301 """
302 if typ == '_PREFIX':
303
304 new['_PREFIX'] = val[:-1]
305 return
306
307 if new['_PREFIX'] is not None:
308
309 prefix = new['_PREFIX']
310 new['_PREFIX'] = None
311 elif typ == 'universal' and '|' in val:
312
313 prefix, val = val.split('|')
314 else:
315 prefix = None
316
317
318 if typ.endswith('-selector') or typ == 'universal':
319
320 if prefix == u'*':
321
322 namespaceURI = cssutils._ANYNS
323 elif prefix is None:
324
325 namespaceURI = namespaces.get(u'', None)
326 elif prefix == u'':
327
328 namespaceURI = namespaces.get(prefix, u'')
329 else:
330 try:
331 namespaceURI = namespaces[prefix]
332 except KeyError:
333 new['wellformed'] = False
334 self._log.error(u'Selector: No namespaceURI found for prefix %r' %
335 prefix, token=(typ, val, line, col),
336 error=xml.dom.NamespaceErr)
337 return
338
339 val = (namespaceURI, val)
340
341 if not context or context == 'negation':
342
343 if 'id' == typ:
344 new['specificity'][1] += 1
345 elif 'class' == typ or '[' == val:
346 new['specificity'][2] += 1
347 elif typ in ('type-selector', 'negation-type-selector',
348 'pseudo-element'):
349 new['specificity'][3] += 1
350 if not context and typ in ('type-selector', 'universal'):
351
352 new['element'] = val
353
354 seq.append(val, typ)
355
356
357 simple_selector_sequence = 'type_selector universal HASH class attrib pseudo negation '
358 simple_selector_sequence2 = 'HASH class attrib pseudo negation '
359
360 element_name = 'element_name'
361
362 negation_arg = 'type_selector universal HASH class attrib pseudo'
363 negationend = ')'
364
365 attname = 'prefix attribute'
366 attname2 = 'attribute'
367 attcombinator = 'combinator ]'
368 attvalue = 'value'
369 attend = ']'
370
371 expressionstart = 'PLUS - DIMENSION NUMBER STRING IDENT'
372 expression = expressionstart + ' )'
373
374 combinator = ' combinator'
375
376 def _COMMENT(expected, seq, token, tokenizer=None):
377 "special implementation for comment token"
378 append(seq, cssutils.css.CSSComment([token]), 'comment')
379 return expected
380
381 def _S(expected, seq, token, tokenizer=None):
382
383 context = new['context'][-1]
384 val, typ = self._tokenvalue(token), self._type(token)
385 if context.startswith('pseudo-'):
386 append(seq, S, 'descendant', context=context)
387 return expected
388
389 elif context != 'attrib' and 'combinator' in expected:
390 append(seq, S, 'descendant', context=context)
391 return simple_selector_sequence + combinator
392
393 else:
394 return expected
395
396 def _universal(expected, seq, token, tokenizer=None):
397
398 context = new['context'][-1]
399 val, typ = self._tokenvalue(token), self._type(token)
400 if 'universal' in expected:
401 append(seq, val, 'universal', context=context)
402
403 if 'negation' == context:
404 return negationend
405 else:
406 return simple_selector_sequence2 + combinator
407
408 else:
409 new['wellformed'] = False
410 self._log.error(
411 u'Selector: Unexpected universal.', token=token)
412 return expected
413
414 def _namespace_prefix(expected, seq, token, tokenizer=None):
415
416
417 context = new['context'][-1]
418 val, typ = self._tokenvalue(token), self._type(token)
419 if 'attrib' == context and 'prefix' in expected:
420
421 append(seq, val, '_PREFIX', context=context)
422 return attname2
423 elif 'type_selector' in expected:
424
425 append(seq, val, '_PREFIX', context=context)
426 return element_name
427 else:
428 new['wellformed'] = False
429 self._log.error(
430 u'Selector: Unexpected namespace prefix.', token=token)
431 return expected
432
433 def _pseudo(expected, seq, token, tokenizer=None):
434
435 """
436 /* '::' starts a pseudo-element, ':' a pseudo-class */
437 /* Exceptions: :first-line, :first-letter, :before and :after. */
438 /* Note that pseudo-elements are restricted to one per selector and */
439 /* occur only in the last simple_selector_sequence. */
440 """
441 context = new['context'][-1]
442 val, typ = self._tokenvalue(token, normalize=True), self._type(token)
443 if 'pseudo' in expected:
444 if val in (':first-line', ':first-letter', ':before', ':after'):
445
446 typ = 'pseudo-element'
447 append(seq, val, typ, context=context)
448
449 if val.endswith(u'('):
450
451 new['context'].append(typ)
452 return expressionstart
453 elif 'negation' == context:
454 return negationend
455 elif 'pseudo-element' == typ:
456
457 return combinator
458 else:
459 return simple_selector_sequence2 + combinator
460
461 else:
462 new['wellformed'] = False
463 self._log.error(
464 u'Selector: Unexpected start of pseudo.', token=token)
465 return expected
466
467 def _expression(expected, seq, token, tokenizer=None):
468
469 context = new['context'][-1]
470 val, typ = self._tokenvalue(token), self._type(token)
471 if context.startswith('pseudo-'):
472 append(seq, val, typ, context=context)
473 return expression
474 else:
475 new['wellformed'] = False
476 self._log.error(
477 u'Selector: Unexpected %s.' % typ, token=token)
478 return expected
479
480 def _attcombinator(expected, seq, token, tokenizer=None):
481
482
483
484 context = new['context'][-1]
485 val, typ = self._tokenvalue(token), self._type(token)
486 if 'attrib' == context and 'combinator' in expected:
487
488 append(seq, val, typ.lower(), context=context)
489 return attvalue
490 else:
491 new['wellformed'] = False
492 self._log.error(
493 u'Selector: Unexpected %s.' % typ, token=token)
494 return expected
495
496 def _string(expected, seq, token, tokenizer=None):
497
498 context = new['context'][-1]
499 val, typ = self._tokenvalue(token), self._type(token)
500
501
502 if 'attrib' == context and 'value' in expected:
503
504 append(seq, val, 'string', context=context)
505 return attend
506
507
508 elif context.startswith('pseudo-'):
509
510 append(seq, val, 'string', context=context)
511 return expression
512
513 else:
514 new['wellformed'] = False
515 self._log.error(
516 u'Selector: Unexpected STRING.', token=token)
517 return expected
518
519 def _ident(expected, seq, token, tokenizer=None):
520
521 context = new['context'][-1]
522 val, typ = self._tokenvalue(token), self._type(token)
523
524
525 if 'attrib' == context and 'attribute' in expected:
526
527 append(seq, val, 'attribute-selector', context=context)
528 return attcombinator
529
530 elif 'attrib' == context and 'value' in expected:
531
532 append(seq, val, 'attribute-value', context=context)
533 return attend
534
535
536 elif 'negation' == context:
537
538 append(seq, val, 'negation-type-selector', context=context)
539 return negationend
540
541
542 elif context.startswith('pseudo-'):
543
544 append(seq, val, typ, context=context)
545 return expression
546
547 elif 'type_selector' in expected or element_name == expected:
548
549 append(seq, val, 'type-selector', context=context)
550 return simple_selector_sequence2 + combinator
551
552 else:
553 new['wellformed'] = False
554 self._log.error(
555 u'Selector: Unexpected IDENT.',
556 token=token)
557 return expected
558
559 def _class(expected, seq, token, tokenizer=None):
560
561 context = new['context'][-1]
562 val, typ = self._tokenvalue(token), self._type(token)
563 if 'class' in expected:
564 append(seq, val, typ, context=context)
565
566 if 'negation' == context:
567 return negationend
568 else:
569 return simple_selector_sequence2 + combinator
570
571 else:
572 new['wellformed'] = False
573 self._log.error(
574 u'Selector: Unexpected class.', token=token)
575 return expected
576
577 def _hash(expected, seq, token, tokenizer=None):
578
579 context = new['context'][-1]
580 val, typ = self._tokenvalue(token), self._type(token)
581 if 'HASH' in expected:
582 append(seq, val, 'id', context=context)
583
584 if 'negation' == context:
585 return negationend
586 else:
587 return simple_selector_sequence2 + combinator
588
589 else:
590 new['wellformed'] = False
591 self._log.error(
592 u'Selector: Unexpected HASH.', token=token)
593 return expected
594
595 def _char(expected, seq, token, tokenizer=None):
596
597 context = new['context'][-1]
598 val, typ = self._tokenvalue(token), self._type(token)
599
600
601 if u']' == val and 'attrib' == context and ']' in expected:
602
603 append(seq, val, 'attribute-end', context=context)
604 context = new['context'].pop()
605 context = new['context'][-1]
606 if 'negation' == context:
607 return negationend
608 else:
609 return simple_selector_sequence2 + combinator
610
611 elif u'=' == val and 'attrib' == context and 'combinator' in expected:
612
613 append(seq, val, 'equals', context=context)
614 return attvalue
615
616
617 elif u')' == val and 'negation' == context and u')' in expected:
618
619 append(seq, val, 'negation-end', context=context)
620 new['context'].pop()
621 context = new['context'][-1]
622 return simple_selector_sequence + combinator
623
624
625 elif val in u'+-' and context.startswith('pseudo-'):
626
627 append(seq, val, {'+': 'plus', '-': 'minus'}[val], context=context)
628 return expression
629
630 elif u')' == val and context.startswith('pseudo-') and\
631 expression == expected:
632
633 append(seq, val, 'function-end', context=context)
634 new['context'].pop()
635 if 'pseudo-element' == context:
636 return combinator
637 else:
638 return simple_selector_sequence + combinator
639
640
641 elif u'[' == val and 'attrib' in expected:
642
643 new['context'].append('attrib')
644 append(seq, val, 'attribute-start', context=context)
645 return attname
646
647 elif val in u'+>~' and 'combinator' in expected:
648
649 _names = {
650 '>': 'child',
651 '+': 'adjacent-sibling',
652 '~': 'following-sibling'}
653 if seq and seq[-1].value == S:
654 seq.replace(-1, val, _names[val])
655 else:
656 append(seq, val, _names[val], context=context)
657 return simple_selector_sequence
658
659 elif u',' == val:
660
661 new['wellformed'] = False
662 self._log.error(
663 u'Selector: Single selector only.',
664 error=xml.dom.InvalidModificationErr,
665 token=token)
666
667 else:
668 new['wellformed'] = False
669 self._log.error(
670 u'Selector: Unexpected CHAR.', token=token)
671 return expected
672
673 def _negation(expected, seq, token, tokenizer=None):
674
675 context = new['context'][-1]
676 val = self._tokenvalue(token, normalize=True)
677 if 'negation' in expected:
678 new['context'].append('negation')
679 append(seq, val, 'negation-start', context=context)
680 return negation_arg
681 else:
682 new['wellformed'] = False
683 self._log.error(
684 u'Selector: Unexpected negation.', token=token)
685 return expected
686
687
688 newseq = self._tempSeq()
689
690 wellformed, expected = self._parse(expected=simple_selector_sequence,
691 seq=newseq, tokenizer=tokenizer,
692 productions={'CHAR': _char,
693 'class': _class,
694 'HASH': _hash,
695 'STRING': _string,
696 'IDENT': _ident,
697 'namespace_prefix': _namespace_prefix,
698 'negation': _negation,
699 'pseudo-class': _pseudo,
700 'pseudo-element': _pseudo,
701 'universal': _universal,
702
703 'NUMBER': _expression,
704 'DIMENSION': _expression,
705
706 'PREFIXMATCH': _attcombinator,
707 'SUFFIXMATCH': _attcombinator,
708 'SUBSTRINGMATCH': _attcombinator,
709 'DASHMATCH': _attcombinator,
710 'INCLUDES': _attcombinator,
711
712 'S': _S,
713 'COMMENT': _COMMENT})
714 wellformed = wellformed and new['wellformed']
715
716
717 if len(new['context']) > 1:
718 wellformed = False
719 self._log.error(u'Selector: Incomplete selector: %s' %
720 self._valuestr(selectorText))
721
722 if expected == 'element_name':
723 wellformed = False
724 self._log.error(u'Selector: No element name found: %s' %
725 self._valuestr(selectorText))
726
727 if expected == simple_selector_sequence:
728 wellformed = False
729 self._log.error(u'Selector: Cannot end with combinator: %s' %
730 self._valuestr(selectorText))
731
732 if newseq and hasattr(newseq[-1].value, 'strip') and \
733 newseq[-1].value.strip() == u'':
734 del newseq[-1]
735
736
737 if wellformed:
738 self.__namespaces = namespaces
739 self._element = new['element']
740 self._specificity = tuple(new['specificity'])
741 self.seq = newseq
742 self.wellformed = True
743
744 self.__namespaces = self._getUsedNamespaces()
745
746 selectorText = property(_getSelectorText, _setSelectorText,
747 doc="(DOM) The parsable textual representation of the selector.")
748
749
750 specificity = property(lambda self: self._specificity,
751 doc="Specificity of this selector (READONLY).")
752
760
765
767 "returns list of actually used URIs in this Selector"
768 uris = set()
769 for item in self.seq:
770 type_, val = item.type, item.value
771 if type_.endswith(u'-selector') or type_ == u'universal' and \
772 type(val) == tuple and val[0] not in (None, u'*'):
773 uris.add(val[0])
774 return uris
775
784