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