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