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