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