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 __author__ = '$LastChangedBy: cthedot $'
14 __date__ = '$LastChangedDate: 2007-12-27 16:58:12 +0100 (Do, 27 Dez 2007) $'
15 __version__ = '$LastChangedRevision: 743 $'
16
17 import xml.dom
18 import cssutils.stylesheets
19
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 ownerRule
31 of type CSSRule, readonly (NOT IMPLEMENTED YET)
32
33 Inherits properties from stylesheet.StyleSheet
34
35 cssutils
36 --------
37 cssText: string
38 a textual representation of the stylesheet
39 encoding
40 reflects the encoding of an @charset rule or 'utf-8' (default)
41 if set to ``None``
42 prefixes: set
43 A set of declared prefixes via @namespace rules. Each
44 CSSStyleRule is checked if it uses additional prefixes which are
45 not declared. If they do they are "invalidated".
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 type = 'text/css'
56
57 - def __init__(self, href=None, media=None,
58 title=u'', disabled=None,
59 ownerNode=None, parentStyleSheet=None,
60 readonly=False):
72
73 - def _getCssText(self):
75
76 - def _setCssText(self, cssText):
77 """
78 (cssutils)
79 Parses ``cssText`` and overwrites the whole stylesheet.
80
81 DOMException on setting
82
83 - NO_MODIFICATION_ALLOWED_ERR: (self)
84 Raised if the rule is readonly.
85 - SYNTAX_ERR:
86 Raised if the specified CSS string value has a syntax error and
87 is unparsable.
88 - NAMESPACE_ERR:
89 If a namespace prefix is found which is not declared.
90 """
91
92 self._checkReadonly()
93 tokenizer = self._tokenize2(cssText)
94 newseq = []
95
96 new = { 'prefixes': set() }
97
98 def S(expected, seq, token, tokenizer=None):
99
100 if expected == 0:
101 return 1
102 else:
103 return expected
104
105 def charsetrule(expected, seq, token, tokenizer):
106 rule = cssutils.css.CSSCharsetRule()
107 rule.cssText = self._tokensupto2(tokenizer, token)
108 if expected > 0 or len(seq) > 0:
109 self._log.error(
110 u'CSSStylesheet: CSSCharsetRule only allowed at beginning of stylesheet.',
111 token, xml.dom.HierarchyRequestErr)
112 else:
113 if rule.valid:
114 seq.append(rule)
115 return 1
116
117 def importrule(expected, seq, token, tokenizer):
118 rule = cssutils.css.CSSImportRule()
119 rule.cssText = self._tokensupto2(tokenizer, token)
120 if expected > 1:
121 self._log.error(
122 u'CSSStylesheet: CSSImportRule not allowed here.',
123 token, xml.dom.HierarchyRequestErr)
124 else:
125 if rule.valid:
126 seq.append(rule)
127 return 1
128
129 def namespacerule(expected, seq, token, tokenizer):
130 rule = cssutils.css.CSSNamespaceRule()
131 rule.cssText = self._tokensupto2(tokenizer, token)
132 if expected > 2:
133 self._log.error(
134 u'CSSStylesheet: CSSNamespaceRule not allowed here.',
135 token, xml.dom.HierarchyRequestErr)
136 else:
137 if rule.valid:
138 seq.append(rule)
139 new['prefixes'].add(rule.prefix)
140 return 2
141
142 def fontfacerule(expected, seq, token, tokenizer):
143 rule = cssutils.css.CSSFontFaceRule()
144 rule.cssText = self._tokensupto2(tokenizer, token)
145 if rule.valid:
146 seq.append(rule)
147 return 3
148
149 def pagerule(expected, seq, token, tokenizer):
150 rule = cssutils.css.CSSPageRule()
151 rule.cssText = self._tokensupto2(tokenizer, token)
152 if rule.valid:
153 seq.append(rule)
154 return 3
155
156 def mediarule(expected, seq, token, tokenizer):
157 rule = cssutils.css.CSSMediaRule()
158 rule.cssText = self._tokensupto2(tokenizer, token)
159 if rule.valid:
160 seq.append(rule)
161 return 3
162
163 def unknownrule(expected, seq, token, tokenizer):
164 rule = cssutils.css.CSSUnknownRule()
165 rule.cssText = self._tokensupto2(tokenizer, token)
166 if rule.valid:
167 seq.append(rule)
168 return expected
169
170 def ruleset(expected, seq, token, tokenizer):
171 rule = cssutils.css.CSSStyleRule()
172 rule.cssText = self._tokensupto2(tokenizer, token)
173
174
175 notdeclared = set()
176 for selector in rule.selectorList.seq:
177 for prefix in selector.prefixes:
178 if not prefix in new['prefixes']:
179 notdeclared.add(prefix)
180 if notdeclared:
181 rule.valid = False
182 self._log.error(
183 u'CSSStylesheet: CSSStyleRule uses undeclared namespace prefixes: %s.' %
184 u', '.join(notdeclared), error=xml.dom.NamespaceErr)
185
186 if rule.valid:
187 seq.append(rule)
188 return 3
189
190
191
192 valid, expected = self._parse(0, newseq, tokenizer,
193 {'S': S,
194 'CDO': lambda *ignored: None,
195 'CDC': lambda *ignored: None,
196 'CHARSET_SYM': charsetrule,
197 'FONT_FACE_SYM': fontfacerule,
198 'IMPORT_SYM': importrule,
199 'NAMESPACE_SYM': namespacerule,
200 'PAGE_SYM': pagerule,
201 'MEDIA_SYM': mediarule,
202 'ATKEYWORD': unknownrule
203 },
204 default=ruleset)
205
206 del self.cssRules[:]
207 for r in newseq:
208 self.cssRules.append(r)
209 self.prefixes = new['prefixes']
210 for r in self.cssRules:
211 r.parentStyleSheet = self
212
213 cssText = property(_getCssText, _setCssText,
214 "(cssutils) a textual representation of the stylesheet")
215
233
235 "return encoding if @charset rule if given or default of 'utf-8'"
236 try:
237 return self.cssRules[0].encoding
238 except (IndexError, AttributeError):
239 return 'utf-8'
240
241 encoding = property(_getEncoding, _setEncoding,
242 "(cssutils) reflects the encoding of an @charset rule or 'UTF-8' (default) if set to ``None``")
243
245 """
246 Used to delete a rule from the style sheet.
247
248 index
249 of the rule to remove in the StyleSheet's rule list. For an
250 index < 0 **no** INDEX_SIZE_ERR is raised but rules for
251 normal Python lists are used. E.g. ``deleteRule(-1)`` removes
252 the last rule in cssRules.
253
254 DOMException
255
256 - INDEX_SIZE_ERR: (self)
257 Raised if the specified index does not correspond to a rule in
258 the style sheet's rule list.
259 - NO_MODIFICATION_ALLOWED_ERR: (self)
260 Raised if this style sheet is readonly.
261 """
262 self._checkReadonly()
263
264 try:
265 self.cssRules[index].parentStyleSheet = None
266 del self.cssRules[index]
267 except IndexError:
268 raise xml.dom.IndexSizeErr(
269 u'CSSStyleSheet: %s is not a valid index in the rulelist of length %i' % (
270 index, self.cssRules.length))
271
273 """
274 Used to insert a new rule into the style sheet. The new rule now
275 becomes part of the cascade.
276
277 Rule may be a string or a valid CSSRule or a CSSRuleList.
278
279 rule
280 a parsable DOMString
281
282 in cssutils also a CSSRule or a CSSRuleList
283
284 index
285 of the rule before the new rule will be inserted.
286 If the specified index is equal to the length of the
287 StyleSheet's rule collection, the rule will be added to the end
288 of the style sheet.
289 If index is not given or None rule will be appended to rule
290 list.
291
292 returns the index within the stylesheet's rule collection
293
294 DOMException
295
296 - HIERARCHY_REQUEST_ERR: (self)
297 Raised if the rule cannot be inserted at the specified index
298 e.g. if an @import rule is inserted after a standard rule set
299 or other at-rule.
300 - INDEX_SIZE_ERR: (self)
301 Raised if the specified index is not a valid insertion point.
302 - NO_MODIFICATION_ALLOWED_ERR: (self)
303 Raised if this style sheet is readonly.
304 - SYNTAX_ERR: (rule)
305 Raised if the specified rule has a syntax error and is
306 unparsable.
307 """
308 self._checkReadonly()
309
310
311 if index is None:
312 index = len(self.cssRules)
313 elif index < 0 or index > self.cssRules.length:
314 raise xml.dom.IndexSizeErr(
315 u'CSSStyleSheet: Invalid index %s for CSSRuleList with a length of %s.' % (
316 index, self.cssRules.length))
317
318
319 if isinstance(rule, basestring):
320 tempsheet = CSSStyleSheet()
321 tempsheet.cssText = rule
322 if len(tempsheet.cssRules) != 1 or (tempsheet.cssRules and
323 not isinstance(tempsheet.cssRules[0], cssutils.css.CSSRule)):
324 self._log.error(u'CSSStyleSheet: Invalid Rule: %s' % rule)
325 return
326 rule = tempsheet.cssRules[0]
327 elif isinstance(rule, cssutils.css.CSSRuleList):
328 for i, r in enumerate(rule):
329 self.insertRule(r, index + i)
330 return index
331 elif not isinstance(rule, cssutils.css.CSSRule):
332 self._log.error(u'CSSStyleSheet: Not a CSSRule: %s' % rule)
333 return
334
335
336
337 if isinstance(rule, cssutils.css.CSSCharsetRule):
338 if index != 0 or (self.cssRules and
339 isinstance(self.cssRules[0], cssutils.css.CSSCharsetRule)):
340 self._log.error(
341 u'CSSStylesheet: @charset only allowed once at the beginning of a stylesheet.',
342 error=xml.dom.HierarchyRequestErr)
343 return
344 else:
345 self.cssRules.insert(index, rule)
346 rule.parentStyleSheet = self
347
348
349 elif isinstance(rule, cssutils.css.CSSUnknownRule) or \
350 isinstance(rule, cssutils.css.CSSComment):
351 if index == 0 and self.cssRules and \
352 isinstance(self.cssRules[0], cssutils.css.CSSCharsetRule):
353 self._log.error(
354 u'CSSStylesheet: @charset must be the first rule.',
355 error=xml.dom.HierarchyRequestErr)
356 return
357 else:
358 self.cssRules.insert(index, rule)
359 rule.parentStyleSheet = self
360
361
362 elif isinstance(rule, cssutils.css.CSSImportRule):
363
364 if index == 0 and self.cssRules and \
365 isinstance(self.cssRules[0], cssutils.css.CSSCharsetRule):
366 self._log.error(
367 u'CSSStylesheet: Found @charset at index 0.',
368 error=xml.dom.HierarchyRequestErr)
369 return
370
371 for r in self.cssRules[:index]:
372 if isinstance(r, cssutils.css.CSSNamespaceRule) or \
373 isinstance(r, cssutils.css.CSSMediaRule) or \
374 isinstance(r, cssutils.css.CSSPageRule) or \
375 isinstance(r, cssutils.css.CSSStyleRule):
376 self._log.error(
377 u'CSSStylesheet: Cannot insert @import here, found @namespace, @media, @page or CSSStyleRule before index %s.' %
378 index,
379 error=xml.dom.HierarchyRequestErr)
380 return
381 self.cssRules.insert(index, rule)
382 rule.parentStyleSheet = self
383
384
385 elif isinstance(rule, cssutils.css.CSSNamespaceRule):
386
387 for r in self.cssRules[index:]:
388 if isinstance(r, cssutils.css.CSSCharsetRule) or \
389 isinstance(r, cssutils.css.CSSImportRule):
390 self._log.error(
391 u'CSSStylesheet: Cannot insert @namespace here, found @charset or @import after index %s.' %
392 index,
393 error=xml.dom.HierarchyRequestErr)
394 return
395
396 for r in self.cssRules[:index]:
397 if isinstance(r, cssutils.css.CSSMediaRule) or \
398 isinstance(r, cssutils.css.CSSPageRule) or \
399 isinstance(r, cssutils.css.CSSStyleRule):
400 self._log.error(
401 u'CSSStylesheet: Cannot insert @namespace here, found @media, @page or CSSStyleRule before index %s.' %
402 index,
403 error=xml.dom.HierarchyRequestErr)
404 return
405 self.cssRules.insert(index, rule)
406 rule.parentStyleSheet = self
407 self.prefixes.add(rule.prefix)
408
409
410 else:
411 for r in self.cssRules[index:]:
412 if isinstance(r, cssutils.css.CSSCharsetRule) or \
413 isinstance(r, cssutils.css.CSSImportRule) or \
414 isinstance(r, cssutils.css.CSSNamespaceRule):
415 self._log.error(
416 u'CSSStylesheet: Cannot insert rule here, found @charset, @import or @namespace before index %s.' %
417 index,
418 error=xml.dom.HierarchyRequestErr)
419 return
420 self.cssRules.insert(index, rule)
421 rule.parentStyleSheet = self
422
423 return index
424
426 """
427 NOT IMPLEMENTED YET
428
429 CSSOM
430 -----
431 The ownerRule attribute, on getting, must return the CSSImportRule
432 that caused this style sheet to be imported (if any). Otherwise, if
433 no CSSImportRule caused it to be imported the attribute must return
434 null.
435 """
436 raise NotImplementedError()
437
438 ownerRule = property(_getsetOwnerRuleDummy, _getsetOwnerRuleDummy,
439 doc="(DOM attribute) NOT IMPLEMENTED YET")
440
442 """
443 **EXPERIMENTAL**
444
445 Utility method to replace all ``url(urlstring)`` values in
446 ``CSSImportRules`` and ``CSSStyleDeclaration`` objects (properties).
447
448 ``replacer`` must be a function which is called with a single
449 argument ``urlstring`` which is the current value of url()
450 excluding ``url(`` and ``)``. It still may have surrounding
451 single or double quotes though.
452 """
453 for importrule in [
454 r for r in self.cssRules if hasattr(r, 'href')]:
455 importrule.href = replacer(importrule.href)
456
457 def setProperty(v):
458 if v.CSS_PRIMITIVE_VALUE == v.cssValueType and\
459 v.CSS_URI == v.primitiveType:
460 v.setStringValue(v.CSS_URI,
461 replacer(v.getStringValue()))
462
463 def styleDeclarations(base):
464 "recurive function to find all CSSStyleDeclarations"
465 styles = []
466 if hasattr(base, 'cssRules'):
467 for rule in base.cssRules:
468 styles.extend(styleDeclarations(rule))
469 elif hasattr(base, 'style'):
470 styles.append(base.style)
471 return styles
472
473 for style in styleDeclarations(self):
474 for p in style:
475 v = p.cssValue
476 if v.CSS_VALUE_LIST == v.cssValueType:
477 for item in v:
478 setProperty(item)
479 elif v.CSS_PRIMITIVE_VALUE == v.cssValueType:
480 setProperty(v)
481
483 """
484 Sets the global Serializer used for output of all stylesheet
485 output.
486 """
487 if isinstance(cssserializer, cssutils.CSSSerializer):
488 cssutils.ser = cssserializer
489 else:
490 raise ValueError(u'Serializer must be an instance of cssutils.CSSSerializer.')
491
493 """
494 Sets Preference of CSSSerializer used for output of this
495 stylesheet. See cssutils.serialize.Preferences for possible
496 preferences to be set.
497 """
498 cssutils.ser.prefs.__setattr__(pref, value)
499
501 return "cssutils.css.%s(href=%r, title=%r)" % (
502 self.__class__.__name__, self.href, self.title)
503
505 return "<cssutils.css.%s object title=%r href=%r encoding=%r at 0x%x>" % (
506 self.__class__.__name__, self.title, self.href, self.encoding,
507 id(self))
508