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 - ??? CSS2 gives a special meaning to the comma (,) in selectors.
11 However, since it is not known if the comma may acquire other
12 meanings in future versions of CSS, the whole statement should be
13 ignored if there is an error anywhere in the selector, even though
14 the rest of the selector may look reasonable in CSS2.
15
16 Illegal example(s):
17
18 For example, since the "&" is not a valid token in a CSS2 selector,
19 a CSS2 user agent must ignore the whole second line, and not set
20 the color of H3 to red:
21 """
22 __all__ = ['Selector']
23 __docformat__ = 'restructuredtext'
24 __author__ = '$LastChangedBy: cthedot $'
25 __date__ = '$LastChangedDate: 2008-01-06 01:15:07 +0100 (So, 06 Jan 2008) $'
26 __version__ = '$LastChangedRevision: 821 $'
27
28 import xml.dom
29
30 import cssutils
31
33 """
34 (cssutils) a single selector in a SelectorList of a CSSStyleRule
35
36 Properties
37 ==========
38 selectorText
39 textual representation of this Selector
40 namespaces
41 **TODO:**
42 a dict of {prefix: namespaceURI} mapping, may also be a
43 CSSStyleSheet in which case the namespaces defined there
44 are used. If None cssutils tries to get the namespaces as
45 defined in a possible parent CSSStyleSheet.
46 parentRule: of type CSSRule, readonly
47 The CSS rule that contains this declaration block or None if this
48 CSSStyleDeclaration is not attached to a CSSRule.
49 prefixes
50 a set which prefixes have been used in this selector
51 seq
52 sequence of Selector parts including comments
53 specificity (READONLY)
54 tuple of (a, b, c, d) where:
55
56 a
57 presence of style in document, currently always 0
58 b
59 # of ID selectors
60 c
61 # of .class selectors
62 d
63 # of Element (type) selectors
64
65 wellformed
66 if this selector is wellformed regarding the Selector spec
67
68 Format
69 ======
70 ::
71
72 # implemented in SelectorList
73 selectors_group
74 : selector [ COMMA S* selector ]*
75 ;
76
77 selector
78 : simple_selector_sequence [ combinator simple_selector_sequence ]*
79 ;
80
81 combinator
82 /* combinators can be surrounded by white space */
83 : PLUS S* | GREATER S* | TILDE S* | S+
84 ;
85
86 simple_selector_sequence
87 : [ type_selector | universal ]
88 [ HASH | class | attrib | pseudo | negation ]*
89 | [ HASH | class | attrib | pseudo | negation ]+
90 ;
91
92 type_selector
93 : [ namespace_prefix ]? element_name
94 ;
95
96 namespace_prefix
97 : [ IDENT | '*' ]? '|'
98 ;
99
100 element_name
101 : IDENT
102 ;
103
104 universal
105 : [ namespace_prefix ]? '*'
106 ;
107
108 class
109 : '.' IDENT
110 ;
111
112 attrib
113 : '[' S* [ namespace_prefix ]? IDENT S*
114 [ [ PREFIXMATCH |
115 SUFFIXMATCH |
116 SUBSTRINGMATCH |
117 '=' |
118 INCLUDES |
119 DASHMATCH ] S* [ IDENT | STRING ] S*
120 ]? ']'
121 ;
122
123 pseudo
124 /* '::' starts a pseudo-element, ':' a pseudo-class */
125 /* Exceptions: :first-line, :first-letter, :before and :after. */
126 /* Note that pseudo-elements are restricted to one per selector and */
127 /* occur only in the last simple_selector_sequence. */
128 : ':' ':'? [ IDENT | functional_pseudo ]
129 ;
130
131 functional_pseudo
132 : FUNCTION S* expression ')'
133 ;
134
135 expression
136 /* In CSS3, the expressions are identifiers, strings, */
137 /* or of the form "an+b" */
138 : [ [ PLUS | '-' | DIMENSION | NUMBER | STRING | IDENT ] S* ]+
139 ;
140
141 negation
142 : NOT S* negation_arg S* ')'
143 ;
144
145 negation_arg
146 : type_selector | universal | HASH | class | attrib | pseudo
147 ;
148
149 """
150 - def __init__(self, selectorText=None, namespaces=None,
151 parentRule=None, readonly=False):
152 """
153 selectorText
154 initial value of this selector
155 parentRule
156 a CSSStyleRule
157 readonly
158 default to False
159 """
160 super(Selector, self).__init__()
161
162 self._element = None
163 if not namespaces:
164 namespaces = {}
165 self.namespaces = namespaces
166 self.parentRule = parentRule
167 self.prefixes = set()
168 self._specificity = (0, 0, 0, 0)
169 self.wellformed = False
170 if selectorText:
171 self.selectorText = selectorText
172 self._readonly = readonly
173
174 element = property(lambda self: self._element,
175 doc="Element of this selector (READONLY).")
176
178 return self._parentRule
179
182
183 parentRule = property(_getParentRule, _setParentRule,
184 doc="(DOM) The CSS rule that contains this Selector or\
185 None if this Selector is not attached to a CSSRule.")
186
188 """
189 returns serialized format
190 """
191 return cssutils.ser.do_css_Selector(self)
192
193 - def _setSelectorText(self, selectorText):
194 """
195 sets this selectorText
196
197 TODO:
198 raises xml.dom.Exception
199 """
200 self._checkReadonly()
201 tokenizer = self._tokenize2(selectorText)
202 if not tokenizer:
203 self._log.error(u'Selector: No selectorText given.')
204 else:
205
206
207
208
209
210
211
212
213 tokens = []
214 for t in tokenizer:
215 typ, val, lin, col = t
216 if val == u':' and tokens and\
217 self._tokenvalue(tokens[-1]) == ':':
218
219 tokens[-1] = (typ, u'::', lin, col)
220
221 elif typ == 'IDENT' and tokens\
222 and self._tokenvalue(tokens[-1]) == u'.':
223
224 tokens[-1] = ('class', u'.'+val, lin, col)
225 elif typ == 'IDENT' and tokens and \
226 self._tokenvalue(tokens[-1]).startswith(u':') and\
227 not self._tokenvalue(tokens[-1]).endswith(u'('):
228
229 if self._tokenvalue(tokens[-1]).startswith(u'::'):
230 t = 'pseudo-element'
231 else:
232 t = 'pseudo-class'
233 tokens[-1] = (t, self._tokenvalue(tokens[-1])+val, lin, col)
234
235 elif typ == 'FUNCTION' and val == u'not(' and tokens and \
236 u':' == self._tokenvalue(tokens[-1]):
237 tokens[-1] = ('negation', u':' + val, lin, tokens[-1][3])
238 elif typ == 'FUNCTION' and tokens\
239 and self._tokenvalue(tokens[-1]).startswith(u':'):
240
241 if self._tokenvalue(tokens[-1]).startswith(u'::'):
242 t = 'pseudo-element'
243 else:
244 t = 'pseudo-class'
245 tokens[-1] = (t, self._tokenvalue(tokens[-1])+val, lin, col)
246
247 elif val == u'*' and tokens and\
248 self._type(tokens[-1]) == 'namespace_prefix' and\
249 self._tokenvalue(tokens[-1]).endswith(u'|'):
250
251 tokens[-1] = ('universal', self._tokenvalue(tokens[-1])+val,
252 lin, col)
253 elif val == u'*':
254
255 tokens.append(('universal', val, lin, col))
256
257 elif val == u'|' and tokens and\
258 self._type(tokens[-1]) in ('IDENT', 'universal') and\
259 self._tokenvalue(tokens[-1]).find(u'|') == -1:
260
261 tokens[-1] = ('namespace_prefix',
262 self._tokenvalue(tokens[-1])+u'|', lin, col)
263 elif val == u'|':
264
265 tokens.append(('namespace_prefix', val, lin, col))
266
267 else:
268 tokens.append(t)
269
270
271 tokenizer = (t for t in tokens)
272
273
274 new = {'context': [''],
275 'element': None,
276 '_PREFIX': None,
277 'prefixes': set(),
278 'specificity': [0, 0, 0, 0],
279 'wellformed': True
280 }
281
282 S = u' '
283
284 def append(seq, val, typ=None, line=-1, col=0, context=None):
285 """
286 appends to seq
287
288 namespace_prefix, IDENT will be combined to a tuple
289 (prefix, name) where prefix might be None, the empty string
290 or a prefix.
291
292 Saved are also:
293 - specificity definition: style, id, class, type
294 - element: the element this Selector is for
295 """
296 if typ == '_PREFIX':
297
298 new['_PREFIX'] = val[:-1]
299 return
300
301 if new['_PREFIX'] is not None:
302
303 prefix, new['_PREFIX'] = new['_PREFIX'], None
304 elif typ == 'universal' and '|' in val:
305
306 prefix, val = val.split('|')
307 else:
308 prefix = u''
309
310
311 if typ.endswith('-selector') or typ == 'universal':
312
313 if prefix == u'*':
314 namespaceURI = None
315 elif prefix == u'':
316 namespaceURI = self.namespaces.get(prefix, u'')
317 else:
318 try:
319 namespaceURI = self.namespaces[prefix]
320 except (KeyError,):
321 new['wellformed'] = False
322 self._log.error(u'No namespaceURI found for prefix %r' %
323 prefix, token=(typ, val, line, col),
324 error=xml.dom.NamespaceErr)
325 val = (namespaceURI, val)
326
327 if not context or context == 'negation':
328
329 if 'id' == typ:
330 new['specificity'][1] += 1
331 elif 'class' == typ or '[' == val:
332 new['specificity'][2] += 1
333 elif typ in ('type-selector', 'negation-type-selector',
334 'pseudo-element'):
335 new['specificity'][3] += 1
336 if not context and typ in ('type-selector', 'universal'):
337
338 new['element'] = val
339
340 seq.append(val, typ)
341
342
343 simple_selector_sequence = 'type_selector universal HASH class attrib pseudo negation '
344 simple_selector_sequence2 = 'HASH class attrib pseudo negation '
345
346 element_name = 'element_name'
347
348 negation_arg = 'type_selector universal HASH class attrib pseudo'
349 negationend = ')'
350
351 attname = 'prefix attribute'
352 attname2 = 'attribute'
353 attcombinator = 'combinator ]'
354 attvalue = 'value'
355 attend = ']'
356
357 expressionstart = 'PLUS - DIMENSION NUMBER STRING IDENT'
358 expression = expressionstart + ' )'
359
360 combinator = ' combinator'
361
362 def _COMMENT(expected, seq, token, tokenizer=None):
363 "special implementation for comment token"
364 append(seq, cssutils.css.CSSComment([token]), 'comment')
365 return expected
366
367 def _S(expected, seq, token, tokenizer=None):
368
369 context = new['context'][-1]
370 val, typ = self._tokenvalue(token), self._type(token)
371 if context.startswith('pseudo-'):
372 append(seq, S, 'descendant', context=context)
373 return expected
374
375 elif context != 'attrib' and 'combinator' in expected:
376 append(seq, S, 'descendant', context=context)
377 return simple_selector_sequence + combinator
378
379 else:
380 return expected
381
382 def _universal(expected, seq, token, tokenizer=None):
383
384 context = new['context'][-1]
385 val, typ = self._tokenvalue(token), self._type(token)
386 if 'universal' in expected:
387 append(seq, val, 'universal', context=context)
388
389 newprefix = val.split(u'|')[0]
390 if newprefix and newprefix != u'*':
391 new['prefixes'].add(newprefix)
392
393 if 'negation' == context:
394 return negationend
395 else:
396 return simple_selector_sequence2 + combinator
397
398 else:
399 new['wellformed'] = False
400 self._log.error(
401 u'Selector: Unexpected universal.', token=token)
402 return expected
403
404 def _namespace_prefix(expected, seq, token, tokenizer=None):
405
406
407 context = new['context'][-1]
408 val, typ = self._tokenvalue(token), self._type(token)
409 if 'attrib' == context and 'prefix' in expected:
410
411 append(seq, val, '_PREFIX', context=context)
412
413 newprefix = val.split(u'|')[0]
414 if newprefix and newprefix != u'*':
415 new['prefixes'].add(newprefix)
416
417 return attname2
418 elif 'type_selector' in expected:
419
420 append(seq, val, '_PREFIX', context=context)
421
422 newprefix = val.split(u'|')[0]
423 if newprefix and newprefix != u'*':
424 new['prefixes'].add(newprefix)
425
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._element = new['element']
739 self.prefixes = new['prefixes']
740 self.seq = newseq
741 self._specificity = tuple(new['specificity'])
742 self.wellformed = True
743
744 selectorText = property(_getSelectorText, _setSelectorText,
745 doc="(DOM) The parsable textual representation of the selector.")
746
747 specificity = property(lambda self: self._specificity,
748 doc="specificity of this selector (READONLY).")
749
751 return "cssutils.css.%s(selectorText=%r)" % (
752 self.__class__.__name__, self.selectorText)
753
755 return "<cssutils.css.%s object selectorText=%r specificity=%r at 0x%x>" % (
756 self.__class__.__name__, self.selectorText,
757 self.specificity, id(self))
758