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