1 """
2 CSSStyleSheet implements DOM Level 2 CSS CSSStyleSheet.
3
4 Partly also:
5 - http://dev.w3.org/csswg/cssom/#the-cssstylesheet
6 - http://www.w3.org/TR/2006/WD-css3-namespace-20060828/
7
8 TODO:
9 - ownerRule and ownerNode
10 """
11 __all__ = ['CSSStyleSheet']
12 __docformat__ = 'restructuredtext'
13 __version__ = '$Id: cssstylesheet.py 1395 2008-07-27 13:30:13Z cthedot $'
14
15 import xml.dom
16 import cssutils.stylesheets
17 from cssutils.util import _Namespaces, _SimpleNamespaces, _readUrl
18 from cssutils.helper import Deprecated
21 """
22 The CSSStyleSheet interface represents a CSS style sheet.
23
24 Properties
25 ==========
26 CSSOM
27 -----
28 cssRules
29 of type CSSRuleList, (DOM readonly)
30 encoding
31 reflects the encoding of an @charset rule or 'utf-8' (default)
32 if set to ``None``
33 ownerRule
34 of type CSSRule, readonly. If this sheet is imported this is a ref
35 to the @import rule that imports it.
36
37 Inherits properties from stylesheet.StyleSheet
38
39 cssutils
40 --------
41 cssText: string
42 a textual representation of the stylesheet
43 namespaces
44 reflects set @namespace rules of this rule.
45 A dict of {prefix: namespaceURI} mapping.
46
47 Format
48 ======
49 stylesheet
50 : [ CHARSET_SYM S* STRING S* ';' ]?
51 [S|CDO|CDC]* [ import [S|CDO|CDC]* ]*
52 [ namespace [S|CDO|CDC]* ]* # according to @namespace WD
53 [ [ ruleset | media | page ] [S|CDO|CDC]* ]*
54 """
55 - def __init__(self, href=None, media=None, title=u'', disabled=None,
56 ownerNode=None, parentStyleSheet=None, readonly=False,
57 ownerRule=None):
75
77 "generator which iterates over cssRules."
78 for rule in self.cssRules:
79 yield rule
80
82 "removes all namespace rules with same namespaceURI but last one set"
83 rules = self.cssRules
84 namespaceitems = self.namespaces.items()
85 i = 0
86 while i < len(rules):
87 rule = rules[i]
88 if rule.type == rule.NAMESPACE_RULE and \
89 (rule.prefix, rule.namespaceURI) not in namespaceitems:
90 self.deleteRule(i)
91 else:
92 i += 1
93
105
106 - def _getCssText(self):
108
109 - def _setCssText(self, cssText):
110 """
111 (cssutils)
112 Parses ``cssText`` and overwrites the whole stylesheet.
113
114 :param cssText:
115 a parseable string or a tuple of (cssText, dict-of-namespaces)
116 :Exceptions:
117 - `NAMESPACE_ERR`:
118 If a namespace prefix is found which is not declared.
119 - `NO_MODIFICATION_ALLOWED_ERR`: (self)
120 Raised if the rule is readonly.
121 - `SYNTAX_ERR`:
122 Raised if the specified CSS string value has a syntax error and
123 is unparsable.
124 """
125 self._checkReadonly()
126
127 cssText, namespaces = self._splitNamespacesOff(cssText)
128 if not namespaces:
129 namespaces = _SimpleNamespaces()
130
131 tokenizer = self._tokenize2(cssText)
132 newseq = []
133
134
135 new = {'encoding': None,
136 'namespaces': namespaces}
137 def S(expected, seq, token, tokenizer=None):
138
139 if expected == 0:
140 return 1
141 else:
142 return expected
143
144 def COMMENT(expected, seq, token, tokenizer=None):
145 "special: sets parent*"
146 comment = cssutils.css.CSSComment([token],
147 parentStyleSheet=self.parentStyleSheet)
148 seq.append(comment)
149 return expected
150
151 def charsetrule(expected, seq, token, tokenizer):
152 rule = cssutils.css.CSSCharsetRule(parentStyleSheet=self)
153 rule.cssText = self._tokensupto2(tokenizer, token)
154 if expected > 0 or len(seq) > 0:
155 self._log.error(
156 u'CSSStylesheet: CSSCharsetRule only allowed at beginning of stylesheet.',
157 token, xml.dom.HierarchyRequestErr)
158 else:
159 if rule.wellformed:
160 seq.append(rule)
161 new['encoding'] = rule.encoding
162 return 1
163
164 def importrule(expected, seq, token, tokenizer):
165 if new['encoding']:
166
167
168 self.__newEncoding = new['encoding']
169
170 rule = cssutils.css.CSSImportRule(parentStyleSheet=self)
171 rule.cssText = self._tokensupto2(tokenizer, token)
172 if expected > 1:
173 self._log.error(
174 u'CSSStylesheet: CSSImportRule not allowed here.',
175 token, xml.dom.HierarchyRequestErr)
176 else:
177 if rule.wellformed:
178
179 seq.append(rule)
180
181 try:
182
183 del self.__newEncoding
184 except AttributeError, e:
185 pass
186
187 return 1
188
189 def namespacerule(expected, seq, token, tokenizer):
190 rule = cssutils.css.CSSNamespaceRule(
191 cssText=self._tokensupto2(tokenizer, token),
192 parentStyleSheet=self)
193 if expected > 2:
194 self._log.error(
195 u'CSSStylesheet: CSSNamespaceRule not allowed here.',
196 token, xml.dom.HierarchyRequestErr)
197 else:
198 if rule.wellformed:
199 seq.append(rule)
200
201 new['namespaces'][rule.prefix] = rule.namespaceURI
202 return 2
203
204 def fontfacerule(expected, seq, token, tokenizer):
205 rule = cssutils.css.CSSFontFaceRule(parentStyleSheet=self)
206 rule.cssText = self._tokensupto2(tokenizer, token)
207 if rule.wellformed:
208 seq.append(rule)
209 return 3
210
211 def mediarule(expected, seq, token, tokenizer):
212 rule = cssutils.css.CSSMediaRule()
213 rule.cssText = (self._tokensupto2(tokenizer, token),
214 new['namespaces'])
215 if rule.wellformed:
216 rule._parentStyleSheet=self
217 for r in rule:
218 r._parentStyleSheet=self
219 seq.append(rule)
220 return 3
221
222 def pagerule(expected, seq, token, tokenizer):
223 rule = cssutils.css.CSSPageRule(parentStyleSheet=self)
224 rule.cssText = self._tokensupto2(tokenizer, token)
225 if rule.wellformed:
226 seq.append(rule)
227 return 3
228
229 def unknownrule(expected, seq, token, tokenizer):
230 self._log.warn(
231 u'CSSStylesheet: Unknown @rule found.',
232 token, neverraise=True)
233 rule = cssutils.css.CSSUnknownRule(parentStyleSheet=self)
234 rule.cssText = self._tokensupto2(tokenizer, token)
235 if rule.wellformed:
236 seq.append(rule)
237 return expected
238
239 def ruleset(expected, seq, token, tokenizer):
240 rule = cssutils.css.CSSStyleRule()
241 rule.cssText = (self._tokensupto2(tokenizer, token),
242 new['namespaces'])
243 if rule.wellformed:
244 rule._parentStyleSheet=self
245 seq.append(rule)
246 return 3
247
248
249
250 wellformed, expected = self._parse(0, newseq, tokenizer,
251 {'S': S,
252 'COMMENT': COMMENT,
253 'CDO': lambda *ignored: None,
254 'CDC': lambda *ignored: None,
255 'CHARSET_SYM': charsetrule,
256 'FONT_FACE_SYM': fontfacerule,
257 'IMPORT_SYM': importrule,
258 'NAMESPACE_SYM': namespacerule,
259 'PAGE_SYM': pagerule,
260 'MEDIA_SYM': mediarule,
261 'ATKEYWORD': unknownrule
262 },
263 default=ruleset)
264
265 if wellformed:
266 del self.cssRules[:]
267 for rule in newseq:
268 self.insertRule(rule, _clean=False)
269 self._cleanNamespaces()
270
271 cssText = property(_getCssText, _setCssText,
272 "(cssutils) a textual representation of the stylesheet")
273
275 """Read (encoding, enctype, decodedContent) from ``url`` for @import
276 sheets."""
277 try:
278
279 selfAsParentEncoding = self.__newEncoding
280 except AttributeError:
281 try:
282
283 selfAsParentEncoding = self.cssRules[0].encoding
284 except (IndexError, AttributeError):
285
286 selfAsParentEncoding = None
287
288 return _readUrl(url, fetcher=self._fetcher,
289 overrideEncoding=self.__encodingOverride,
290 parentEncoding=selfAsParentEncoding)
291
292 - def _setCssTextWithEncodingOverride(self, cssText, encodingOverride=None,
293 encoding=None):
294 """Set cssText but use ``encodingOverride`` to overwrite detected
295 encoding. This is used by parse and @import during setting of cssText.
296
297 If ``encoding`` is given use this but do not save it as encodingOverride"""
298 if encodingOverride:
299
300 self.__encodingOverride = encodingOverride
301
302 self.__newEncoding = encoding
303 self.cssText = cssText
304
305 if encodingOverride:
306
307 self.encoding = self.__encodingOverride
308
309 self.__encodingOverride = None
310 elif encoding:
311
312 self.encoding = encoding
313
315 """sets @import URL loader, if None the default is used"""
316 self._fetcher = fetcher
317
335
337 "return encoding if @charset rule if given or default of 'utf-8'"
338 try:
339 return self.cssRules[0].encoding
340 except (IndexError, AttributeError):
341 return 'utf-8'
342
343 encoding = property(_getEncoding, _setEncoding,
344 "(cssutils) reflects the encoding of an @charset rule or 'UTF-8' (default) if set to ``None``")
345
346 namespaces = property(lambda self: self._namespaces,
347 doc="Namespaces used in this CSSStyleSheet.")
348
349 - def add(self, rule):
350 """
351 Adds rule to stylesheet at appropriate position.
352 Same as ``sheet.insertRule(rule, inOrder=True)``.
353 """
354 return self.insertRule(rule, index=None, inOrder=True)
355
357 """
358 Used to delete a rule from the style sheet.
359
360 :param index:
361 of the rule to remove in the StyleSheet's rule list. For an
362 index < 0 **no** INDEX_SIZE_ERR is raised but rules for
363 normal Python lists are used. E.g. ``deleteRule(-1)`` removes
364 the last rule in cssRules.
365 :Exceptions:
366 - `INDEX_SIZE_ERR`: (self)
367 Raised if the specified index does not correspond to a rule in
368 the style sheet's rule list.
369 - `NAMESPACE_ERR`: (self)
370 Raised if removing this rule would result in an invalid StyleSheet
371 - `NO_MODIFICATION_ALLOWED_ERR`: (self)
372 Raised if this style sheet is readonly.
373 """
374 self._checkReadonly()
375
376 try:
377 rule = self.cssRules[index]
378 except IndexError:
379 raise xml.dom.IndexSizeErr(
380 u'CSSStyleSheet: %s is not a valid index in the rulelist of length %i' % (
381 index, self.cssRules.length))
382 else:
383 if rule.type == rule.NAMESPACE_RULE:
384
385 uris = [r.namespaceURI for r in self if r.type == r.NAMESPACE_RULE]
386 useduris = self._getUsedURIs()
387 if rule.namespaceURI in useduris and\
388 uris.count(rule.namespaceURI) == 1:
389 raise xml.dom.NoModificationAllowedErr(
390 u'CSSStyleSheet: NamespaceURI defined in this rule is used, cannot remove.')
391 return
392
393 rule._parentStyleSheet = None
394 del self.cssRules[index]
395
396 - def insertRule(self, rule, index=None, inOrder=False, _clean=True):
397 """
398 Used to insert a new rule into the style sheet. The new rule now
399 becomes part of the cascade.
400
401 :Parameters:
402 rule
403 a parsable DOMString, in cssutils also a CSSRule or a
404 CSSRuleList
405 index
406 of the rule before the new rule will be inserted.
407 If the specified index is equal to the length of the
408 StyleSheet's rule collection, the rule will be added to the end
409 of the style sheet.
410 If index is not given or None rule will be appended to rule
411 list.
412 inOrder
413 if True the rule will be put to a proper location while
414 ignoring index but without raising HIERARCHY_REQUEST_ERR.
415 The resulting index is returned nevertheless
416 :returns: the index within the stylesheet's rule collection
417 :Exceptions:
418 - `HIERARCHY_REQUEST_ERR`: (self)
419 Raised if the rule cannot be inserted at the specified index
420 e.g. if an @import rule is inserted after a standard rule set
421 or other at-rule.
422 - `INDEX_SIZE_ERR`: (self)
423 Raised if the specified index is not a valid insertion point.
424 - `NO_MODIFICATION_ALLOWED_ERR`: (self)
425 Raised if this style sheet is readonly.
426 - `SYNTAX_ERR`: (rule)
427 Raised if the specified rule has a syntax error and is
428 unparsable.
429 """
430 self._checkReadonly()
431
432
433 if index is None:
434 index = len(self.cssRules)
435 elif index < 0 or index > self.cssRules.length:
436 raise xml.dom.IndexSizeErr(
437 u'CSSStyleSheet: Invalid index %s for CSSRuleList with a length of %s.' % (
438 index, self.cssRules.length))
439 return
440
441 if isinstance(rule, basestring):
442
443 tempsheet = CSSStyleSheet(href=self.href,
444 media=self.media,
445 title=self.title,
446 parentStyleSheet=self.parentStyleSheet,
447 ownerRule=self.ownerRule)
448 tempsheet._ownerNode = self.ownerNode
449 tempsheet._fetcher = self._fetcher
450
451
452
453
454 if not rule.startswith(u'@charset') and (self.cssRules and
455 self.cssRules[0].type == self.cssRules[0].CHARSET_RULE):
456
457 newrulescount, newruleindex = 2, 1
458 rule = self.cssRules[0].cssText + rule
459 else:
460 newrulescount, newruleindex = 1, 0
461
462
463 tempsheet.cssText = (rule, self._namespaces)
464
465 if len(tempsheet.cssRules) != newrulescount or (not isinstance(
466 tempsheet.cssRules[newruleindex], cssutils.css.CSSRule)):
467 self._log.error(u'CSSStyleSheet: Not a CSSRule: %s' % rule)
468 return
469 rule = tempsheet.cssRules[newruleindex]
470 rule._parentStyleSheet = None
471
472
473
474
475 elif isinstance(rule, cssutils.css.CSSRuleList):
476
477 for i, r in enumerate(rule):
478 self.insertRule(r, index + i)
479 return index
480
481 if not rule.wellformed:
482 self._log.error(u'CSSStyleSheet: Invalid rules cannot be added.')
483 return
484
485
486
487 if rule.type == rule.CHARSET_RULE:
488 if inOrder:
489 index = 0
490
491 if (self.cssRules and self.cssRules[0].type == rule.CHARSET_RULE):
492 self.cssRules[0].encoding = rule.encoding
493 else:
494 self.cssRules.insert(0, rule)
495 elif index != 0 or (self.cssRules and
496 self.cssRules[0].type == rule.CHARSET_RULE):
497 self._log.error(
498 u'CSSStylesheet: @charset only allowed once at the beginning of a stylesheet.',
499 error=xml.dom.HierarchyRequestErr)
500 return
501 else:
502 self.cssRules.insert(index, rule)
503
504
505 elif rule.type in (rule.UNKNOWN_RULE, rule.COMMENT) and not inOrder:
506 if index == 0 and self.cssRules and\
507 self.cssRules[0].type == rule.CHARSET_RULE:
508 self._log.error(
509 u'CSSStylesheet: @charset must be the first rule.',
510 error=xml.dom.HierarchyRequestErr)
511 return
512 else:
513 self.cssRules.insert(index, rule)
514
515
516 elif rule.type == rule.IMPORT_RULE:
517 if inOrder:
518
519 if rule.type in (r.type for r in self):
520
521 for i, r in enumerate(reversed(self.cssRules)):
522 if r.type == rule.type:
523 index = len(self.cssRules) - i
524 break
525 else:
526
527 if self.cssRules and self.cssRules[0].type in (rule.CHARSET_RULE,
528 rule.COMMENT):
529 index = 1
530 else:
531 index = 0
532 else:
533
534 if index == 0 and self.cssRules and\
535 self.cssRules[0].type == rule.CHARSET_RULE:
536 self._log.error(
537 u'CSSStylesheet: Found @charset at index 0.',
538 error=xml.dom.HierarchyRequestErr)
539 return
540
541 for r in self.cssRules[:index]:
542 if r.type in (r.NAMESPACE_RULE, r.MEDIA_RULE, r.PAGE_RULE,
543 r.STYLE_RULE, r.FONT_FACE_RULE):
544 self._log.error(
545 u'CSSStylesheet: Cannot insert @import here, found @namespace, @media, @page or CSSStyleRule before index %s.' %
546 index,
547 error=xml.dom.HierarchyRequestErr)
548 return
549 self.cssRules.insert(index, rule)
550
551
552 elif rule.type == rule.NAMESPACE_RULE:
553 if inOrder:
554 if rule.type in (r.type for r in self):
555
556 for i, r in enumerate(reversed(self.cssRules)):
557 if r.type == rule.type:
558 index = len(self.cssRules) - i
559 break
560 else:
561
562 for i, r in enumerate(self.cssRules):
563 if r.type in (r.MEDIA_RULE, r.PAGE_RULE, r.STYLE_RULE,
564 r.FONT_FACE_RULE, r.UNKNOWN_RULE, r.COMMENT):
565 index = i
566 break
567 else:
568
569 for r in self.cssRules[index:]:
570 if r.type in (r.CHARSET_RULE, r.IMPORT_RULE):
571 self._log.error(
572 u'CSSStylesheet: Cannot insert @namespace here, found @charset or @import after index %s.' %
573 index,
574 error=xml.dom.HierarchyRequestErr)
575 return
576
577 for r in self.cssRules[:index]:
578 if r.type in (r.MEDIA_RULE, r.PAGE_RULE, r.STYLE_RULE,
579 r.FONT_FACE_RULE):
580 self._log.error(
581 u'CSSStylesheet: Cannot insert @namespace here, found @media, @page or CSSStyleRule before index %s.' %
582 index,
583 error=xml.dom.HierarchyRequestErr)
584 return
585
586 if not (rule.prefix in self.namespaces and
587 self.namespaces[rule.prefix] == rule.namespaceURI):
588
589 self.cssRules.insert(index, rule)
590 if _clean:
591 self._cleanNamespaces()
592
593
594 else:
595 if inOrder:
596
597 self.cssRules.append(rule)
598 index = len(self.cssRules) - 1
599 else:
600 for r in self.cssRules[index:]:
601 if r.type in (r.CHARSET_RULE, r.IMPORT_RULE, r.NAMESPACE_RULE):
602 self._log.error(
603 u'CSSStylesheet: Cannot insert rule here, found @charset, @import or @namespace before index %s.' %
604 index,
605 error=xml.dom.HierarchyRequestErr)
606 return
607 self.cssRules.insert(index, rule)
608
609
610 rule._parentStyleSheet = self
611 if rule.MEDIA_RULE == rule.type:
612 for r in rule:
613 r._parentStyleSheet = self
614
615 elif rule.IMPORT_RULE == rule.type:
616 rule.href = rule.href
617
618 return index
619
620 ownerRule = property(lambda self: self._ownerRule,
621 doc="(DOM attribute) NOT IMPLEMENTED YET")
622
623 @Deprecated('Use cssutils.replaceUrls(sheet, replacer) instead.')
625 """
626 **EXPERIMENTAL**
627
628 Utility method to replace all ``url(urlstring)`` values in
629 ``CSSImportRules`` and ``CSSStyleDeclaration`` objects (properties).
630
631 ``replacer`` must be a function which is called with a single
632 argument ``urlstring`` which is the current value of url()
633 excluding ``url(`` and ``)``. It still may have surrounding
634 single or double quotes though.
635 """
636 cssutils.replaceUrls(self, replacer)
637
639 """
640 Sets the global Serializer used for output of all stylesheet
641 output.
642 """
643 if isinstance(cssserializer, cssutils.CSSSerializer):
644 cssutils.ser = cssserializer
645 else:
646 raise ValueError(u'Serializer must be an instance of cssutils.CSSSerializer.')
647
649 """
650 Sets Preference of CSSSerializer used for output of this
651 stylesheet. See cssutils.serialize.Preferences for possible
652 preferences to be set.
653 """
654 cssutils.ser.prefs.__setattr__(pref, value)
655
664
675