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