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