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