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: 2007-08-20 22:07:12 +0200 (Mo, 20 Aug 2007) $'
26 __version__ = '$LastChangedRevision: 257 $'
27
28 import xml.dom
29
30 import cssutils
31
32
34 """
35 (cssutils) a single selector in a SelectorList of a CSSStyleRule
36
37 Properties
38 ==========
39 selectorText
40 textual representation of this Selector
41 namespaces
42 a set which namespaces have been used in this selector
43 seq
44 sequence of Selector parts including comments
45
46 Format
47 ======
48 combinator
49 : PLUS S* | GREATER S* | S+ ;
50 selector
51 : simple_selector [ combinator simple_selector ]* ;
52 simple_selector
53 : element_name [ HASH | class | attrib | pseudo ]*
54 | [ HASH | class | attrib | pseudo ]+ ;
55 class
56 : '.' IDENT ;
57 element_name
58 : IDENT | '*' ;
59 attrib
60 : '[' S* IDENT S* [ [ '=' | INCLUDES | DASHMATCH ] S*
61 [ IDENT | STRING ] S* ]? ']'
62 ;
63 pseudo
64 : ':' [ IDENT | FUNCTION S* IDENT? S* ')' ] ;
65
66 plus some from http://www.w3.org/TR/css3-selectors/
67 """
68 - def __init__(self, selectorText=None, readonly=False):
82
83
85 """
86 returns serialized format
87 """
88 return cssutils.ser.do_css_Selector(self)
89
90 - def _setSelectorText(self, selectorText):
91 """
92 DOMException on setting
93
94 - INVALID_MODIFICATION_ERR: (self)
95 - INVALID_MODIFICATION_ERR: (self)
96 Raised if the specified CSS string value represents a different
97 type than the current one, e.g. a SelectorList.
98 - NO_MODIFICATION_ALLOWED_ERR: (self)
99 Raised if this rule is readonly.
100 - SYNTAX_ERR: (self)
101 Raised if the specified CSS string value has a syntax error
102 and is unparsable.
103 """
104 def preprocess(tokens):
105 """
106 remove S at start and end,
107 if around a combinator (ignoring comments)
108 or in att []
109 """
110
111 if tokens[0].type == self._ttypes.S:
112 del tokens[0]
113 COMBINATORS_NO_S = u'+>~'
114 r = []
115 attlevel = 0
116 i = 0
117
118 i, imax = 0, len(tokens)
119 while i < imax:
120 t = tokens[i]
121 if self._ttypes.S == t.type and attlevel > 0:
122
123 i += 1
124 continue
125
126 elif t.value in COMBINATORS_NO_S and attlevel == 0:
127
128 before = len(r)
129 while before >= 0:
130 before -= 1
131 if r[before].type == self._ttypes.S:
132 del r[before]
133 elif r[before].type != self._ttypes.COMMENT:
134 break
135
136 after = i+1
137 while after < imax:
138 if tokens[after].type == self._ttypes.S:
139 del tokens[after]
140 after -= 1
141 imax = len(tokens)
142 elif tokens[after].type != self._ttypes.COMMENT:
143 break
144 after += 1
145 elif t.value == u'[':
146 attlevel += 1
147 elif t.value == u']':
148 attlevel -= 1
149 r.append(t)
150 i += 1
151
152
153 if r and r[-1].type == self._ttypes.S:
154 del r[-1]
155 return r
156
157 def addprefix(seq):
158 """
159 checks if last item in seq was a namespace prefix
160 and if yes adds it to newnamespaces
161 """
162 try:
163
164 if isinstance(seq[-1], dict) and \
165 seq[-1]['type'] in ('type', 'attributename'):
166 newnamespaces.add(seq[-1]['value'])
167 seq[-1]['type'] = 'prefix'
168 else:
169 newnamespaces.add('')
170 except IndexError:
171 pass
172
173 def getAttrib(atttokens):
174 """
175 atttokens
176 a sequence of tokens without any S which are preprocessed
177
178 returns seq of selector attribute
179
180 Format
181 ======
182 attrib
183 : '[' S* IDENT S* [ [ '=' | INCLUDES | DASHMATCH ] S*
184 [ IDENT | STRING ] S* ]? ']' ;
185
186 CSS3 additionally:
187 ^= PREFIXMATCH
188 $= SUFFIXMATCH
189 *= SUBSTRINGMATCH
190 """
191 attseq = []
192 expected = "["
193 i, imax = 0, len(atttokens)
194 while i < imax:
195 t = atttokens[i]
196 if self._ttypes.COMMENT == t.type:
197 attseq.append(cssutils.css.CSSComment(t))
198
199
200 elif expected == '[' and t.value == u'[':
201 attseq.append({'type': 'attribute selector', 'value': t.value})
202 expected = 'att'
203 elif expected == '[':
204 self._log.error(u'Selector: No Attrib start "[" found.', t)
205 return
206
207
208 elif expected in ('comb or ]', ']') and t.value == u']':
209 attseq.append({'type': 'attribute selector end', 'value': t.value})
210 assert i == len(atttokens) - 1
211 break
212 elif expected == 'pipe' and t.value == u']':
213 self._log.error(u'Selector: No attribute name for namespace start found.', t)
214 return
215
216
217 elif expected == 'att' and t.type == self._ttypes.UNIVERSAL:
218 attseq.append({'type': 'type', 'value': t.value})
219 expected = 'pipe'
220
221
222 elif expected in ('pipe', 'att', 'comb or ]') and \
223 t.value == u'|':
224 addprefix(attseq)
225 attseq.append({'type': 'type', 'value': t.value})
226 expected = 'att'
227
228 elif expected == 'att' and t.type == self._ttypes.IDENT:
229 attseq.append({'type': 'attributename', 'value': t.value})
230 expected = 'comb or ]'
231 elif expected == 'att':
232 self._log.error(u'Selector: No Attrib found.', t)
233 return
234
235
236 elif expected == 'comb or ]' and t.value in (
237 u'=', u'~=', u'|=', u'^=', u'$=', u'*='):
238 attseq.append({'type': 'combinator', 'value': t.value})
239 expected = 'val'
240 elif expected == 'comb or ]':
241 self._log.error(u'Selector: No Attrib combinator found.', t)
242 return
243
244 elif expected == 'val' and t.type in (
245 self._ttypes.IDENT, self._ttypes.STRING):
246 attseq.append({'type': 'attributevalue', 'value': t.value})
247 expected = ']'
248 elif expected == 'val':
249 self._log.error(u'Selector: No Attrib value found.', t)
250 return
251
252 else:
253 self._log.error(u'Selector: Unknown Attrib syntax.', t)
254 return
255
256 i += 1
257
258 else:
259 self._log.error(u'Selector: Incomplete Attrib.', t)
260 return
261
262 return attseq
263
264 def getFunc(functokens):
265 """
266 functokens
267 a sequence of tokens
268
269 returns seq of selector functional_pseudo
270
271 Format
272 ======
273 functional_pseudo
274 : FUNCTION S* expression ')'
275 ;
276 """
277 funcseq = [{'type': 'function', 'value': functokens[0].value}]
278 expected = ")"
279 i, imax = 1, len(functokens)
280 while i < imax:
281 t = functokens[i]
282
283 if self._ttypes.COMMENT == t.type:
284 funcseq.append(cssutils.css.CSSComment(t))
285
286 elif t.value == u')':
287 funcseq.append(
288 {'type': 'function end', 'value': t.value})
289 expected = None
290 assert i == len(functokens) - 1
291 break
292
293 else:
294 funcseq.append({'type': 'functionvalue', 'value': t.value})
295
296 i += 1
297
298 if expected:
299 self._log.error(
300 u'Selector: Incomplete functional pseudo "%s".' %
301 self._valuestr(functokens))
302 return
303
304 return funcseq
305
306 self._checkReadonly()
307
308 tokens = self._tokenize(selectorText)
309 if tokens:
310 COMBINATORS = u' >+~'
311 tokens = preprocess(tokens)
312
313 newseq = []
314 newnamespaces = set()
315 found1 = False
316
317
318 expected = 'simple_selector'
319 i, imax = 0, len(tokens)
320 while i < imax:
321 t = tokens[i]
322
323
324 if self._ttypes.COMMA == t.type:
325 self._log.error(u'Selector: Possible SelectorList found.', t,
326 error=xml.dom.InvalidModificationErr)
327 return
328
329
330 elif self._ttypes.COMMENT == t.type and expected not in (
331 'classname', 'pseudoclass', 'pseudoelement'):
332
333 newseq.append(cssutils.css.CSSComment(t))
334
335
336 elif expected.startswith('simple_selector') and \
337 t.type == self._ttypes.CLASS:
338 newseq.append({'value': t.value, 'type': 'class'})
339 found1 = True
340 expected = 'classname'
341 elif expected == 'classname' and t.type == self._ttypes.IDENT:
342 newseq.append({'value': t.value, 'type': 'classname'})
343 expected = 'simple_selector or combinator'
344
345
346 elif expected.startswith('simple_selector') and \
347 t.value == u':' or t.value == u'::':
348 expected = {
349 ':': 'pseudoclass', '::': 'pseudoelement'}[t.value]
350 newseq.append({'value': t.value, 'type': expected})
351 found1 = True
352 elif expected.startswith('pseudo') and \
353 t.type == self._ttypes.IDENT:
354 newseq.append({'value': t.value, 'type': 'pseudoname'})
355 expected = 'simple_selector or combinator'
356 elif expected == 'pseudoclass' and \
357 t.type == self._ttypes.FUNCTION:
358
359 functokens, endi = self._tokensupto(
360 tokens[i:], funcendonly=True)
361 i += endi
362 funcseq = getFunc(functokens)
363 if not funcseq:
364 self._log.error(
365 u'Selector: Invalid functional pseudo.', t)
366 return
367 newseq.extend(funcseq)
368 expected = 'simple_selector or combinator'
369 elif expected == 'pseudoclass':
370 self._log.error(u'Selector: Pseudoclass name expected.', t)
371 return
372 elif expected == 'pseudoelement':
373 self._log.error(u'Selector: Pseudoelement name expected.', t)
374 return
375
376
377 elif expected.startswith('simple_selector') and \
378 t.type == self._ttypes.HASH:
379 newseq.append({'value': t.value, 'type': 'id'})
380 found1 = True
381 expected = 'simple_selector or combinator'
382
383
384 elif expected.startswith('simple_selector') and \
385 t.value == u'|':
386 addprefix(newseq)
387 newseq.append({'type': 'pipe', 'value': t.value})
388 found1 = True
389 expected = 'namespaced element'
390
391
392 elif (expected.startswith('simple_selector') or
393 expected == 'namespaced element') and \
394 t.type in (self._ttypes.IDENT, self._ttypes.UNIVERSAL):
395 newseq.append({'value': t.value, 'type': 'type'})
396 found1 = True
397 expected = 'simple_selector or combinator'
398
399 elif expected == 'namespaced element' and \
400 t.type not in (self._ttypes.IDENT, self._ttypes.UNIVERSAL):
401 self._log.error(u'Selector: Namespaced element expected.', t)
402 return
403
404
405 elif expected.startswith('simple_selector') and t.value == u'[':
406 atttokens, endi = self._tokensupto(
407 tokens[i:], selectorattendonly=True)
408 i += endi
409 attseq = getAttrib(atttokens)
410 if not attseq:
411 self._log.warn(
412 u'Selector: Empty attribute selector.', t)
413 return
414 newseq.extend(attseq)
415 found1 = True
416 expected = 'simple_selector or combinator'
417
418
419 elif expected == 'simple_selector or combinator' and \
420 t.value in COMBINATORS or t.type == self._ttypes.S:
421 if t.type == self._ttypes.S:
422 newseq.append({'value': t.normalvalue, 'type': 'combinator'})
423 else:
424 newseq.append({'value': t.value, 'type': 'combinator'})
425 expected = 'simple_selector'
426 elif expected == 'simple_selector or combinator':
427 self._log.error(u'Selector: No combinator found.', t)
428 return
429
430 else:
431 self._log.error(u'Selector: Expected simple selector.', t)
432 return
433
434 i += 1
435
436 if expected == 'classname':
437 self._log.error(u'Selector: Class name expected: %s.' %
438 selectorText)
439 return
440 elif expected == 'namespaced element':
441 self._log.error(u'Selector: No element found for namespace start: %s.' %
442 selectorText)
443 return
444 elif expected == 'pseudoclass':
445 self._log.error(u'Selector: Pseudoclass name expected: %s.' %
446 selectorText)
447 return
448 elif expected == 'pseudoelement':
449 self._log.error(u'Selector: Pseudoelement name expected: %s.' %
450 selectorText)
451 return
452 elif newseq and ((
453 isinstance(newseq[-1], basestring) and \
454 newseq[-1] in COMBINATORS
455 ) or (
456 isinstance(newseq[-1], dict) and
457 isinstance(newseq[-1]['value'], basestring) and \
458 newseq[-1]['value'] in COMBINATORS)):
459 self._log.error(u'Selector: Cannot end with combinator: %s.' %
460 selectorText)
461 return
462
463 if found1:
464 self.seq = newseq
465 self.namespaces = newnamespaces
466
467
468 else:
469 self._log.error(
470 u'Selector: No selector found: %s.' % selectorText)
471 else:
472 self._log.error(u'Selector: Empty selector.')
473
474 selectorText = property(_getSelectorText, _setSelectorText,
475 doc="(DOM) The parsable textual representation of the selector.")
476
478 return "cssutils.css.%s(selectorText=%r)" % (
479 self.__class__.__name__, self.selectorText)
480
482 return "<cssutils.css.%s object selectorText=%r at 0x%x>" % (
483 self.__class__.__name__, self.selectorText, id(self))
484
485
486 if __name__ == '__main__':
487 s = Selector()
488 s.selectorText = u'n1|a[n2|x]'
489 print s.selectorText
490