1 """base classes for css and stylesheets packages
2 """
3 __all__ = []
4 __docformat__ = 'restructuredtext'
5 __author__ = '$LastChangedBy: cthedot $'
6 __date__ = '$LastChangedDate: 2008-02-22 19:03:20 +0100 (Fr, 22 Feb 2008) $'
7 __version__ = '$LastChangedRevision: 1070 $'
8
9 from itertools import ifilter
10 import re
11 import types
12 import xml.dom
13 import cssutils
14 from tokenize2 import Tokenizer
15
16 -class Base(object):
17 """
18 Base class for most CSS and StyleSheets classes
19
20 **Superceded by Base2 which is used for new seq handling class.**
21 See cssutils.util.Base2
22
23 Contains helper methods for inheriting classes helping parsing
24
25 ``_normalize`` is static as used by Preferences.
26 """
27 __tokenizer2 = Tokenizer()
28
29 _log = cssutils.log
30 _prods = cssutils.tokenize2.CSSProductions
31
32
33
34
35 _SHORTHANDPROPERTIES = {
36 u'background': [],
37 u'border': [],
38 u'border-left': [],
39 u'border-right': [],
40 u'border-top': [],
41 u'border-bottom': [],
42 u'border-color': [],
43 u'border-style': [],
44 u'border-width': [],
45 u'cue': [],
46 u'font': [],
47
48
49
50
51 u'list-style': [],
52 u'margin': [],
53 u'outline': [],
54 u'padding': [],
55 u'pause': []
56 }
57
58
59 __escapes = re.compile(ur'(\\[^0-9a-fA-F])').sub
60
61 __unicodes = re.compile(ur'\\[0-9a-fA-F]{1,6}[\t|\r|\n|\f|\x20]?').sub
62
63 @staticmethod
65 """
66 normalizes x, namely:
67
68 - remove any \ before non unicode sequences (0-9a-zA-Z) so for
69 x=="c\olor\" return "color" (unicode escape sequences should have
70 been resolved by the tokenizer already)
71 - lowercase
72 """
73 if x:
74 def removeescape(matchobj):
75 return matchobj.group(0)[1:]
76 x = Base.__escapes(removeescape, x)
77 return x.lower()
78 else:
79 return x
80
82 "raises xml.dom.NoModificationAllowedErr if rule/... is readonly"
83 if hasattr(self, '_readonly') and self._readonly:
84 raise xml.dom.NoModificationAllowedErr(
85 u'%s is readonly.' % self.__class__)
86 return True
87 return False
88
90 """
91 returns tuple (text, dict-of-namespaces) or if no namespaces are
92 in cssText returns (cssText, {})
93
94 used in Selector, SelectorList, CSSStyleRule, CSSMediaRule and
95 CSSStyleSheet
96 """
97 if isinstance(text_namespaces_tuple, tuple):
98 return text_namespaces_tuple[0], _SimpleNamespaces(
99 text_namespaces_tuple[1])
100 else:
101 return text_namespaces_tuple, _SimpleNamespaces()
102
104 """
105 returns tokens of textortokens which may already be tokens in which
106 case simply returns input
107 """
108 if not textortokens:
109 return None
110 elif isinstance(textortokens, basestring):
111
112 return self.__tokenizer2.tokenize(
113 textortokens)
114 elif types.GeneratorType == type(textortokens):
115
116 return textortokens
117 elif isinstance(textortokens, tuple):
118
119 return [textortokens]
120 else:
121
122 return (x for x in textortokens)
123
125 "returns next token in generator tokenizer or the default value"
126 try:
127 return tokenizer.next()
128 except (StopIteration, AttributeError):
129 return default
130
132 "returns type of Tokenizer token"
133 if token:
134 return token[0]
135 else:
136 return None
137
139 "returns value of Tokenizer token"
140 if token and normalize:
141 return Base._normalize(token[1])
142 elif token:
143 return token[1]
144 else:
145 return None
146
148 """
149 for STRING returns the actual content without surrounding "" or ''
150 and without respective escapes, e.g.::
151
152 "with \" char" => with " char
153 """
154 if token:
155 value = token[1]
156 return value.replace('\\'+value[0], value[0])[1:-1]
157 else:
158 return None
159
161 """
162 for URI returns the actual content without surrounding url()
163 or url(""), url('') and without respective escapes, e.g.::
164
165 url("\"") => "
166 """
167 if token:
168 value = token[1][4:-1].strip()
169 if (value[0] in '\'"') and (value[0] == value[-1]):
170
171 value = value.replace('\\'+value[0], value[0])[1:-1]
172 return value
173 else:
174 return None
175
176 - def _tokensupto2(self,
177 tokenizer,
178 starttoken=None,
179 blockstartonly=False,
180 blockendonly=False,
181 mediaendonly=False,
182 importmediaqueryendonly=False,
183 mediaqueryendonly=False,
184 semicolon=False,
185 propertynameendonly=False,
186 propertyvalueendonly=False,
187 propertypriorityendonly=False,
188 selectorattendonly=False,
189 funcendonly=False,
190 listseponly=False,
191 separateEnd=False
192 ):
193 """
194 returns tokens upto end of atrule and end index
195 end is defined by parameters, might be ; } ) or other
196
197 default looks for ending "}" and ";"
198 """
199 ends = u';}'
200 endtypes = ()
201 brace = bracket = parant = 0
202
203 if blockstartonly:
204 ends = u'{'
205 brace = -1
206 elif blockendonly:
207 ends = u'}'
208 brace = 1
209 elif mediaendonly:
210 ends = u'}'
211 brace = 1
212 elif importmediaqueryendonly:
213
214 ends = u';'
215 endtypes = ('STRING',)
216 elif mediaqueryendonly:
217
218
219 ends = u'{'
220 brace = -1
221 endtypes = ('STRING',)
222 elif semicolon:
223 ends = u';'
224 elif propertynameendonly:
225 ends = u':;'
226 elif propertyvalueendonly:
227 ends = u';!'
228 elif propertypriorityendonly:
229 ends = u';'
230 elif selectorattendonly:
231 ends = u']'
232 if starttoken and self._tokenvalue(starttoken) == u'[':
233 bracket = 1
234 elif funcendonly:
235 ends = u')'
236 parant = 1
237 elif listseponly:
238 ends = u','
239
240 resulttokens = []
241 if starttoken:
242 resulttokens.append(starttoken)
243 if tokenizer:
244 for token in tokenizer:
245 typ, val, line, col = token
246 if 'EOF' == typ:
247 resulttokens.append(token)
248 break
249 if u'{' == val:
250 brace += 1
251 elif u'}' == val:
252 brace -= 1
253 elif u'[' == val:
254 bracket += 1
255 elif u']' == val:
256 bracket -= 1
257
258 elif u'(' == val or \
259 Base._prods.FUNCTION == typ:
260 parant += 1
261 elif u')' == val:
262 parant -= 1
263
264 resulttokens.append(token)
265
266 if (brace == bracket == parant == 0) and (
267 val in ends or typ in endtypes):
268 break
269 elif mediaqueryendonly and brace == -1 and (
270 bracket == parant == 0) and typ in endtypes:
271
272 break
273
274 if separateEnd:
275
276 if resulttokens:
277 return resulttokens[:-1], resulttokens[-1]
278 else:
279 return resulttokens, None
280 else:
281 return resulttokens
282
284 """
285 returns string value of t (t may be a string, a list of token tuples
286 or a single tuple in format (type, value, line, col).
287 Mainly used to get a string value of t for error messages.
288 """
289 if not t:
290 return u''
291 elif isinstance(t, basestring):
292 return t
293 else:
294 return u''.join([x[1] for x in t])
295
297 """
298 adds default productions if not already present, used by
299 _parse only
300
301 each production should return the next expected token
302 normaly a name like "uri" or "EOF"
303 some have no expectation like S or COMMENT, so simply return
304 the current value of self.__expected
305 """
306 def ATKEYWORD(expected, seq, token, tokenizer=None):
307 "TODO: add default impl for unexpected @rule?"
308 return expected
309
310 def COMMENT(expected, seq, token, tokenizer=None):
311 "default implementation for COMMENT token adds CSSCommentRule"
312 seq.append(cssutils.css.CSSComment([token]))
313 return expected
314
315 def S(expected, seq, token, tokenizer=None):
316 "default implementation for S token, does nothing"
317 return expected
318
319 def EOF(expected=None, seq=None, token=None, tokenizer=None):
320 "default implementation for EOF token"
321 return 'EOF'
322
323 p = {'ATKEYWORD': ATKEYWORD,
324 'COMMENT': COMMENT,
325 'S': S,
326 'EOF': EOF
327 }
328 p.update(productions)
329 return p
330
331 - def _parse(self, expected, seq, tokenizer, productions, default=None,
332 new=None):
333 """
334 puts parsed tokens in seq by calling a production with
335 (seq, tokenizer, token)
336
337 expected
338 a name what token or value is expected next, e.g. 'uri'
339 seq
340 to add rules etc to
341 tokenizer
342 call tokenizer.next() to get next token
343 productions
344 callbacks {tokentype: callback}
345 default
346 default callback if tokentype not in productions
347 new
348 used to init default productions
349
350 returns (wellformed, expected) which the last prod might have set
351 """
352 wellformed = True
353 if tokenizer:
354 prods = self._adddefaultproductions(productions, new)
355 for token in tokenizer:
356 p = prods.get(token[0], default)
357 if p:
358 expected = p(expected, seq, token, tokenizer)
359 else:
360 wellformed = False
361 self._log.error(u'Unexpected token (%s, %s, %s, %s)' % token)
362
363 return wellformed, expected
364
367 """
368 Base class for new seq handling, used by Selector for now only
369 """
372
374 """
375 sets newseq and makes it readonly
376 """
377 newseq._readonly = True
378 self._seq = newseq
379
380 seq = property(lambda self: self._seq, doc="seq for most classes")
381
383 "get a writeable Seq() which is added later"
384 return Seq(readonly=readonly)
385
387 """
388 adds default productions if not already present, used by
389 _parse only
390
391 each production should return the next expected token
392 normaly a name like "uri" or "EOF"
393 some have no expectation like S or COMMENT, so simply return
394 the current value of self.__expected
395 """
396 def ATKEYWORD(expected, seq, token, tokenizer=None):
397 "default impl for unexpected @rule"
398 if expected != 'EOF':
399
400 rule = cssutils.css.CSSUnknownRule()
401 rule.cssText = self._tokensupto2(tokenizer, token)
402 if rule.wellformed:
403 seq.append(rule, cssutils.css.CSSRule.UNKNOWN_RULE,
404 line=token[2], col=token[3])
405 return expected
406 else:
407 new['wellformed'] = False
408 self._log.error(u'Expected EOF.',
409 token=token)
410 return expected
411
412 def COMMENT(expected, seq, token, tokenizer=None):
413 "default implementation for COMMENT token adds CSSCommentRule"
414 seq.append(cssutils.css.CSSComment([token]), 'COMMENT')
415 return expected
416
417 def S(expected, seq, token, tokenizer=None):
418 "default implementation for S token, does nothing"
419 return expected
420
421 def EOF(expected=None, seq=None, token=None, tokenizer=None):
422 "default implementation for EOF token"
423 return 'EOF'
424
425 defaultproductions = {'ATKEYWORD': ATKEYWORD,
426 'COMMENT': COMMENT,
427 'S': S,
428 'EOF': EOF
429 }
430 defaultproductions.update(productions)
431 return defaultproductions
432
433
434 -class Seq(object):
435 """
436 property seq of Base2 inheriting classes, holds a list of Item objects.
437
438 used only by Selector for now
439
440 is normally readonly, only writable during parsing
441 """
443 """
444 only way to write to a Seq is to initialize it with new items
445 each itemtuple has (value, type, line) where line is optional
446 """
447 self._seq = []
448 self._readonly = readonly
449
452
455
458
460 return iter(self._seq)
461
463 return len(self._seq)
464
465 - def append(self, val, typ, line=None, col=None):
466 "if not readonly add new Item()"
467 if self._readonly:
468 raise AttributeError('Seq is readonly.')
469 else:
470 self._seq.append(Item(val, typ, line, col))
471
472 - def replace(self, index=-1, val=None, typ=None, line=None, col=None):
473 """
474 if not readonly replace Item at index with new Item or
475 simply replace value or type
476 """
477 if self._readonly:
478 raise AttributeError('Seq is readonly.')
479 else:
480 self._seq[index] = Item(val, typ, line, col)
481
483 "returns a repr same as a list of tuples of (value, type)"
484 return u'cssutils.%s.%s([\n %s])' % (self.__module__,
485 self.__class__.__name__,
486 u',\n '.join([u'(%r, %r)' % (item.type, item.value)
487 for item in self._seq]
488 ))
490 return "<cssutils.%s.%s object length=%r at 0x%x>" % (
491 self.__module__, self.__class__.__name__, len(self), id(self))
492
494 """
495 an item in the seq list of classes (successor to tuple items in old seq)
496
497 each item has attributes:
498
499 type
500 a sematic type like "element", "attribute"
501 value
502 the actual value which may be a string, number etc or an instance
503 of e.g. a CSSComment
504 *line*
505 **NOT IMPLEMENTED YET, may contain the line in the source later**
506 """
507 - def __init__(self, value, type, line=None, col=None):
508 self.__value = value
509 self.__type = type
510 self.__line = line
511 self.__col = col
512
513 type = property(lambda self: self.__type)
514 value = property(lambda self: self.__value)
515 line = property(lambda self: self.__line)
516 col = property(lambda self: self.__col)
517
519 return "%s.%s(value=%r, type=%r, line=%r, col=%r)" % (
520 self.__module__, self.__class__.__name__,
521 self.__value, self.__type, self.__line, self.__col)
522
525 """
526 (EXPERIMENTAL)
527 A base class used for list classes like css.SelectorList or
528 stylesheets.MediaList
529
530 adds list like behaviour running on inhering class' property ``seq``
531
532 - item in x => bool
533 - len(x) => integer
534 - get, set and del x[i]
535 - for item in x
536 - append(item)
537
538 some methods must be overwritten in inheriting class
539 """
542
545
548
550 return self.seq[index]
551
553 def gen():
554 for x in self.seq:
555 yield x
556 return gen()
557
560
562 "must be overwritten"
563 raise NotImplementedError
564
566 "must be overwritten"
567 raise NotImplementedError
568
571 """This is a decorator which can be used to mark functions
572 as deprecated. It will result in a warning being emitted
573 when the function is used.
574
575 It accepts a single paramter ``msg`` which is shown with the warning.
576 It should contain information which function or method to use instead.
577 """
580
582 def newFunc(*args, **kwargs):
583 import warnings
584 warnings.warn("Call to deprecated method %r. %s" %
585 (func.__name__, self.msg),
586 category=DeprecationWarning,
587 stacklevel=2)
588 return func(*args, **kwargs)
589 newFunc.__name__ = func.__name__
590 newFunc.__doc__ = func.__doc__
591 newFunc.__dict__.update(func.__dict__)
592 return newFunc
593
596 """
597 A dictionary like wrapper for @namespace rules used in a CSSStyleSheet.
598 Works on effective namespaces, so e.g. if::
599
600 @namespace p1 "uri";
601 @namespace p2 "uri";
602
603 only the second rule is effective and kept.
604
605 namespaces
606 a dictionary {prefix: namespaceURI} containing the effective namespaces
607 only. These are the latest set in the CSSStyleSheet.
608 parentStyleSheet
609 the parent CSSStyleSheet
610 """
611 - def __init__(self, parentStyleSheet, *args):
614
617
633
635 try:
636 return self.namespaces[prefix]
637 except KeyError, e:
638 raise xml.dom.NamespaceErr('Prefix %r not found.' % prefix)
639
642
645
661
668
676
677 namespaces = property(__getNamespaces,
678 doc=u'Holds only effective @namespace rules in self.parentStyleSheets'
679 '@namespace rules.')
680
681 - def get(self, prefix, default):
683
686
689
692
694 """
695 returns effective prefix for given namespaceURI or raises IndexError
696 if this cannot be found"""
697 for prefix, uri in self.namespaces.items():
698 if uri == namespaceURI:
699 return prefix
700 raise IndexError(u'NamespaceURI %r not found.' % namespaceURI)
701
703 return u"<cssutils.util.%s object parentStyleSheet=%r namespaces=%r "\
704 u"at 0x%x>" % (
705 self.__class__.__name__, str(self.parentStyleSheet),
706 self.namespaces, id(self))
707
710 """
711 namespaces used in objects like Selector as long as they are not connected
712 to a CSSStyleSheet
713 """
715 self.__namespaces = dict(*args)
716
719
720 namespaces = property(lambda self: self.__namespaces,
721 doc=u'Dict Wrapper for self.sheets @namespace rules.')
722
724 return u"<cssutils.util.%s object namespaces=%r at 0x%x>" % (
725 self.__class__.__name__, self.namespaces, id(self))
726