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 1340 2008-07-09 20:05:20Z 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 rule = cssutils.css.CSSImportRule(parentStyleSheet=self)
166
167
168
169 self.__newEncoding = new['encoding']
170
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
182 del self.__newEncoding
183
184 return 1
185
186 def namespacerule(expected, seq, token, tokenizer):
187 rule = cssutils.css.CSSNamespaceRule(
188 cssText=self._tokensupto2(tokenizer, token),
189 parentStyleSheet=self)
190 if expected > 2:
191 self._log.error(
192 u'CSSStylesheet: CSSNamespaceRule not allowed here.',
193 token, xml.dom.HierarchyRequestErr)
194 else:
195 if rule.wellformed:
196 seq.append(rule)
197
198 new['namespaces'][rule.prefix] = rule.namespaceURI
199 return 2
200
201 def fontfacerule(expected, seq, token, tokenizer):
202 rule = cssutils.css.CSSFontFaceRule(parentStyleSheet=self)
203 rule.cssText = self._tokensupto2(tokenizer, token)
204 if rule.wellformed:
205 seq.append(rule)
206 return 3
207
208 def mediarule(expected, seq, token, tokenizer):
209 rule = cssutils.css.CSSMediaRule()
210 rule.cssText = (self._tokensupto2(tokenizer, token),
211 new['namespaces'])
212 if rule.wellformed:
213 rule._parentStyleSheet=self
214 for r in rule:
215 r._parentStyleSheet=self
216 seq.append(rule)
217 return 3
218
219 def pagerule(expected, seq, token, tokenizer):
220 rule = cssutils.css.CSSPageRule(parentStyleSheet=self)
221 rule.cssText = self._tokensupto2(tokenizer, token)
222 if rule.wellformed:
223 seq.append(rule)
224 return 3
225
226 def unknownrule(expected, seq, token, tokenizer):
227 self._log.warn(
228 u'CSSStylesheet: Unknown @rule found.',
229 token, neverraise=True)
230 rule = cssutils.css.CSSUnknownRule(parentStyleSheet=self)
231 rule.cssText = self._tokensupto2(tokenizer, token)
232 if rule.wellformed:
233 seq.append(rule)
234 return expected
235
236 def ruleset(expected, seq, token, tokenizer):
237 rule = cssutils.css.CSSStyleRule()
238 rule.cssText = (self._tokensupto2(tokenizer, token),
239 new['namespaces'])
240 if rule.wellformed:
241 rule._parentStyleSheet=self
242 seq.append(rule)
243 return 3
244
245
246
247 wellformed, expected = self._parse(0, newseq, tokenizer,
248 {'S': S,
249 'COMMENT': COMMENT,
250 'CDO': lambda *ignored: None,
251 'CDC': lambda *ignored: None,
252 'CHARSET_SYM': charsetrule,
253 'FONT_FACE_SYM': fontfacerule,
254 'IMPORT_SYM': importrule,
255 'NAMESPACE_SYM': namespacerule,
256 'PAGE_SYM': pagerule,
257 'MEDIA_SYM': mediarule,
258 'ATKEYWORD': unknownrule
259 },
260 default=ruleset)
261
262 if wellformed:
263 del self.cssRules[:]
264 for rule in newseq:
265 self.insertRule(rule, _clean=False)
266 self._cleanNamespaces()
267
268 cssText = property(_getCssText, _setCssText,
269 "(cssutils) a textual representation of the stylesheet")
270
271 - def _setCssTextWithEncodingOverride(self, cssText, encodingOverride=None):
272 """Set cssText but use __encodingOverride to overwrite detected
273 encoding. This is only used by @import during setting of cssText.
274 In all other cases __encodingOverride is None"""
275 if encodingOverride:
276
277 self.__encodingOverride = encodingOverride
278
279 self.cssText = cssText
280
281 if encodingOverride:
282
283 self.encoding = self.__encodingOverride
284 self.__encodingOverride = None
285
287 """Read (encoding, cssText) from ``url`` for @import sheets"""
288 try:
289
290 parentEncoding = self.__newEncoding
291 except AttributeError:
292
293 try:
294
295
296 parentEncoding = self.cssRules[0].encoding
297 except (IndexError, AttributeError):
298 parentEncoding = None
299
300 return _readUrl(url, fetcher=self._fetcher,
301 overrideEncoding=self.__encodingOverride,
302 parentEncoding=parentEncoding)
303
305 """sets @import URL loader, if None the default is used"""
306 self._fetcher = fetcher
307
325
327 "return encoding if @charset rule if given or default of 'utf-8'"
328 try:
329 return self.cssRules[0].encoding
330 except (IndexError, AttributeError):
331 return 'utf-8'
332
333 encoding = property(_getEncoding, _setEncoding,
334 "(cssutils) reflects the encoding of an @charset rule or 'UTF-8' (default) if set to ``None``")
335
336 namespaces = property(lambda self: self._namespaces,
337 doc="Namespaces used in this CSSStyleSheet.")
338
339 - def add(self, rule):
340 """
341 Adds rule to stylesheet at appropriate position.
342 Same as ``sheet.insertRule(rule, inOrder=True)``.
343 """
344 return self.insertRule(rule, index=None, inOrder=True)
345
347 """
348 Used to delete a rule from the style sheet.
349
350 :param index:
351 of the rule to remove in the StyleSheet's rule list. For an
352 index < 0 **no** INDEX_SIZE_ERR is raised but rules for
353 normal Python lists are used. E.g. ``deleteRule(-1)`` removes
354 the last rule in cssRules.
355 :Exceptions:
356 - `INDEX_SIZE_ERR`: (self)
357 Raised if the specified index does not correspond to a rule in
358 the style sheet's rule list.
359 - `NAMESPACE_ERR`: (self)
360 Raised if removing this rule would result in an invalid StyleSheet
361 - `NO_MODIFICATION_ALLOWED_ERR`: (self)
362 Raised if this style sheet is readonly.
363 """
364 self._checkReadonly()
365
366 try:
367 rule = self.cssRules[index]
368 except IndexError:
369 raise xml.dom.IndexSizeErr(
370 u'CSSStyleSheet: %s is not a valid index in the rulelist of length %i' % (
371 index, self.cssRules.length))
372 else:
373 if rule.type == rule.NAMESPACE_RULE:
374
375 uris = [r.namespaceURI for r in self if r.type == r.NAMESPACE_RULE]
376 useduris = self._getUsedURIs()
377 if rule.namespaceURI in useduris and\
378 uris.count(rule.namespaceURI) == 1:
379 raise xml.dom.NoModificationAllowedErr(
380 u'CSSStyleSheet: NamespaceURI defined in this rule is used, cannot remove.')
381 return
382
383 rule._parentStyleSheet = None
384 del self.cssRules[index]
385
386 - def insertRule(self, rule, index=None, inOrder=False, _clean=True):
387 """
388 Used to insert a new rule into the style sheet. The new rule now
389 becomes part of the cascade.
390
391 :Parameters:
392 rule
393 a parsable DOMString, in cssutils also a CSSRule or a
394 CSSRuleList
395 index
396 of the rule before the new rule will be inserted.
397 If the specified index is equal to the length of the
398 StyleSheet's rule collection, the rule will be added to the end
399 of the style sheet.
400 If index is not given or None rule will be appended to rule
401 list.
402 inOrder
403 if True the rule will be put to a proper location while
404 ignoring index but without raising HIERARCHY_REQUEST_ERR.
405 The resulting index is returned nevertheless
406 :returns: the index within the stylesheet's rule collection
407 :Exceptions:
408 - `HIERARCHY_REQUEST_ERR`: (self)
409 Raised if the rule cannot be inserted at the specified index
410 e.g. if an @import rule is inserted after a standard rule set
411 or other at-rule.
412 - `INDEX_SIZE_ERR`: (self)
413 Raised if the specified index is not a valid insertion point.
414 - `NO_MODIFICATION_ALLOWED_ERR`: (self)
415 Raised if this style sheet is readonly.
416 - `SYNTAX_ERR`: (rule)
417 Raised if the specified rule has a syntax error and is
418 unparsable.
419 """
420 self._checkReadonly()
421
422
423 if index is None:
424 index = len(self.cssRules)
425 elif index < 0 or index > self.cssRules.length:
426 raise xml.dom.IndexSizeErr(
427 u'CSSStyleSheet: Invalid index %s for CSSRuleList with a length of %s.' % (
428 index, self.cssRules.length))
429 return
430
431 if isinstance(rule, basestring):
432
433 tempsheet = CSSStyleSheet(href=self.href,
434 media=self.media,
435 title=self.title,
436 parentStyleSheet=self.parentStyleSheet,
437 ownerRule=self.ownerRule)
438 tempsheet._ownerNode = self.ownerNode
439 tempsheet._fetcher = self._fetcher
440
441
442
443
444 if not rule.startswith(u'@charset') and (self.cssRules and
445 self.cssRules[0].type == self.cssRules[0].CHARSET_RULE):
446
447 newrulescount, newruleindex = 2, 1
448 rule = self.cssRules[0].cssText + rule
449 else:
450 newrulescount, newruleindex = 1, 0
451
452
453 tempsheet.cssText = (rule, self._namespaces)
454
455 if len(tempsheet.cssRules) != newrulescount or (not isinstance(
456 tempsheet.cssRules[newruleindex], cssutils.css.CSSRule)):
457 self._log.error(u'CSSStyleSheet: Not a CSSRule: %s' % rule)
458 return
459 rule = tempsheet.cssRules[newruleindex]
460 rule._parentStyleSheet = None
461
462
463
464
465
466 elif isinstance(rule, cssutils.css.CSSRuleList):
467
468 for i, r in enumerate(rule):
469 self.insertRule(r, index + i)
470 return index
471
472 if not rule.wellformed:
473 self._log.error(u'CSSStyleSheet: Invalid rules cannot be added.')
474 return
475
476
477
478 if rule.type == rule.CHARSET_RULE:
479 if inOrder:
480 index = 0
481
482 if (self.cssRules and self.cssRules[0].type == rule.CHARSET_RULE):
483 self.cssRules[0].encoding = rule.encoding
484 else:
485 self.cssRules.insert(0, rule)
486 elif index != 0 or (self.cssRules and
487 self.cssRules[0].type == rule.CHARSET_RULE):
488 self._log.error(
489 u'CSSStylesheet: @charset only allowed once at the beginning of a stylesheet.',
490 error=xml.dom.HierarchyRequestErr)
491 return
492 else:
493 self.cssRules.insert(index, rule)
494
495
496 elif rule.type in (rule.UNKNOWN_RULE, rule.COMMENT) and not inOrder:
497 if index == 0 and self.cssRules and\
498 self.cssRules[0].type == rule.CHARSET_RULE:
499 self._log.error(
500 u'CSSStylesheet: @charset must be the first rule.',
501 error=xml.dom.HierarchyRequestErr)
502 return
503 else:
504 self.cssRules.insert(index, rule)
505
506
507 elif rule.type == rule.IMPORT_RULE:
508 if inOrder:
509
510 if rule.type in (r.type for r in self):
511
512 for i, r in enumerate(reversed(self.cssRules)):
513 if r.type == rule.type:
514 index = len(self.cssRules) - i
515 break
516 else:
517
518 if self.cssRules and self.cssRules[0].type in (rule.CHARSET_RULE,
519 rule.COMMENT):
520 index = 1
521 else:
522 index = 0
523 else:
524
525 if index == 0 and self.cssRules and\
526 self.cssRules[0].type == rule.CHARSET_RULE:
527 self._log.error(
528 u'CSSStylesheet: Found @charset at index 0.',
529 error=xml.dom.HierarchyRequestErr)
530 return
531
532 for r in self.cssRules[:index]:
533 if r.type in (r.NAMESPACE_RULE, r.MEDIA_RULE, r.PAGE_RULE,
534 r.STYLE_RULE, r.FONT_FACE_RULE):
535 self._log.error(
536 u'CSSStylesheet: Cannot insert @import here, found @namespace, @media, @page or CSSStyleRule before index %s.' %
537 index,
538 error=xml.dom.HierarchyRequestErr)
539 return
540 self.cssRules.insert(index, rule)
541
542
543 elif rule.type == rule.NAMESPACE_RULE:
544 if inOrder:
545 if rule.type in (r.type for r in self):
546
547 for i, r in enumerate(reversed(self.cssRules)):
548 if r.type == rule.type:
549 index = len(self.cssRules) - i
550 break
551 else:
552
553 for i, r in enumerate(self.cssRules):
554 if r.type in (r.MEDIA_RULE, r.PAGE_RULE, r.STYLE_RULE,
555 r.FONT_FACE_RULE, r.UNKNOWN_RULE, r.COMMENT):
556 index = i
557 break
558 else:
559
560 for r in self.cssRules[index:]:
561 if r.type in (r.CHARSET_RULE, r.IMPORT_RULE):
562 self._log.error(
563 u'CSSStylesheet: Cannot insert @namespace here, found @charset or @import after index %s.' %
564 index,
565 error=xml.dom.HierarchyRequestErr)
566 return
567
568 for r in self.cssRules[:index]:
569 if r.type in (r.MEDIA_RULE, r.PAGE_RULE, r.STYLE_RULE,
570 r.FONT_FACE_RULE):
571 self._log.error(
572 u'CSSStylesheet: Cannot insert @namespace here, found @media, @page or CSSStyleRule before index %s.' %
573 index,
574 error=xml.dom.HierarchyRequestErr)
575 return
576
577 if not (rule.prefix in self.namespaces and
578 self.namespaces[rule.prefix] == rule.namespaceURI):
579
580 self.cssRules.insert(index, rule)
581 if _clean:
582 self._cleanNamespaces()
583
584
585 else:
586 if inOrder:
587
588 self.cssRules.append(rule)
589 index = len(self.cssRules) - 1
590 else:
591 for r in self.cssRules[index:]:
592 if r.type in (r.CHARSET_RULE, r.IMPORT_RULE, r.NAMESPACE_RULE):
593 self._log.error(
594 u'CSSStylesheet: Cannot insert rule here, found @charset, @import or @namespace before index %s.' %
595 index,
596 error=xml.dom.HierarchyRequestErr)
597 return
598 self.cssRules.insert(index, rule)
599
600
601 rule._parentStyleSheet = self
602 if rule.MEDIA_RULE == rule.type:
603 for r in rule:
604 r._parentStyleSheet = self
605
606 elif rule.IMPORT_RULE == rule.type:
607 rule.href = rule.href
608
609 return index
610
611 ownerRule = property(lambda self: self._ownerRule,
612 doc="(DOM attribute) NOT IMPLEMENTED YET")
613
614 @Deprecated('Use cssutils.replaceUrls(sheet, replacer) instead.')
616 """
617 **EXPERIMENTAL**
618
619 Utility method to replace all ``url(urlstring)`` values in
620 ``CSSImportRules`` and ``CSSStyleDeclaration`` objects (properties).
621
622 ``replacer`` must be a function which is called with a single
623 argument ``urlstring`` which is the current value of url()
624 excluding ``url(`` and ``)``. It still may have surrounding
625 single or double quotes though.
626 """
627 cssutils.replaceUrls(self, replacer)
628
630 """
631 Sets the global Serializer used for output of all stylesheet
632 output.
633 """
634 if isinstance(cssserializer, cssutils.CSSSerializer):
635 cssutils.ser = cssserializer
636 else:
637 raise ValueError(u'Serializer must be an instance of cssutils.CSSSerializer.')
638
640 """
641 Sets Preference of CSSSerializer used for output of this
642 stylesheet. See cssutils.serialize.Preferences for possible
643 preferences to be set.
644 """
645 cssutils.ser.prefs.__setattr__(pref, value)
646
655
666