root/trunk/proj/w3c/css.py

Revision 404, 34.5 kB (checked in by sholloway, 5 years ago)

Significant refactoring for CSSParser and CSSCascadeStrategy acquisition from SkinModel? instead of xmlSkinner.
Transitioned use of "href" to simply "ref"
Tested the wx skinning demos, fixing some
All tests pass.

Line 
1 #!/usr/bin/env python
2 ##~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
3 ##~ Copyright (C) 2002-2004  TechGame Networks, LLC.
4 ##~
5 ##~ This library is free software; you can redistribute it and/or
6 ##~ modify it under the terms of the BSD style License as found in the
7 ##~ LICENSE file included with this distribution.
8 ##~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
9
10 """CSS-2.1 engine
11
12 Primary classes:
13     * CSSElementInterfaceAbstract
14         Provide a concrete implementation for the XML element model used.
15
16     * CSSCascadeStrategy
17         Implements the CSS-2.1 engine's attribute lookup rules.
18
19     * CSSParser
20         Parses CSS source forms into usable results using CSSBuilder and
21         CSSMutableSelector.  You may want to override parseExternal()
22
23     * CSSBuilder (and CSSMutableSelector)
24         A concrete implementation for cssParser.CSSBuilderAbstract (and
25         cssParser.CSSSelectorAbstract) to provide usable results to
26         CSSParser requests.
27
28 Dependencies:
29     python 2.3 (or greater)
30         sets
31     cssParser
32         re
33
34 Credits:
35     * 2005.07.12 Andre Soereng
36         Asked for increased stylesheet merging support to handle multiple
37         stylesheet links.  He also noticed that the CSS Cascading rules were
38         not correct.  (In fact, they were exactly backwards ;)  A new tests was
39         added to verify the cascading order.
40
41     * 2005.03.23 Gary Poster
42         Host of bugfixes and testing of the engine.  He also added support for
43         page margins from http://www.w3.org/TR/css3-page/
44
45 """
46
47 #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
48 #~ Imports
49 #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
50
51 import os
52 import itertools
53
54 try:
55     set
56 except NameError:
57     from sets import Set as set
58
59 import cssParser
60
61 #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
62 #~ Constants / Variables / Etc.
63 #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
64
65 CSSParseError = cssParser.CSSParseError
66
67 #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
68 #~ Definitions
69 #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
70
71 class CSSElementInterfaceAbstract(object):
72     def asCSSElement(self):
73         return self
74     def getAttr(self, name, default=NotImplemented):
75         raise NotImplementedError('Subclass Responsibility: %r' % (self,))
76     def getIdAttr(self):
77         return self.getAttr('id', '')
78     def getClassAttr(self):
79         return self.getAttr('class', '')
80     def getClasses(self):
81         return self.getClassAttr().replace(',', ' ').split()
82
83     def getInlineStyle(self):
84         raise NotImplementedError('Subclass Responsibility: %r' % (self,))
85     def setInlineStyle(self, style):
86         raise NotImplementedError('Subclass Responsibility: %r' % (self,))
87     def getInlineStyleEx(self, fnParseInline=None):
88         style = self.getInlineStyle()
89         if fnParseInline and style:
90             style = fnParseInline(self.getStyleAttr() or '')
91             self.setInlineStyle(style)
92         return style
93
94     def getInlineStyleEx(self, fnParseInline=None):
95         return self.getInlineStyle()
96
97     def matchesNode(self):
98         raise NotImplementedError('Subclass Responsibility: %r' % (self,))
99
100     def inPseudoState(self, name, params=()):
101         raise NotImplementedError('Subclass Responsibility: %r' % (self,))
102
103     def iterXMLParents(self):
104         """Results must be compatible with CSSElementInterfaceAbstract"""
105         raise NotImplementedError('Subclass Responsibility: %r' % (self,))
106
107     def getPreviousSibling(self):
108         raise NotImplementedError('Subclass Responsibility: %r' % (self,))
109
110 class CSSElementInterfaceBase(CSSElementInterfaceAbstract):
111     def getStyleAttr(self):
112         return self.getAttr('style', '')
113
114     _inlineStyle = None
115     def getInlineStyle(self):
116         return self._inlineStyle
117     def setInlineStyle(self, style):
118         self._inlineStyle = style
119     def delInlineStyle(self):
120         del self._inlineStyle
121
122     def getInlineStyleEx(self, fnParseInline=None):
123         style = self.getInlineStyle()
124         if fnParseInline and style:
125             style = fnParseInline(self.getStyleAttr() or '')
126             self.setInlineStyle(style)
127         return style
128
129 #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
130
131 class CSSCascadeStrategy(object):
132     author = None
133     user = None
134     userAgent = None
135     _parser = None
136
137     def __init__(self, author=None, user=None, userAgent=None):
138         if author is not None:
139             self.author = author
140         if user is not None:
141             self.user = user
142         if userAgent is not None:
143             self.userAgent = userAgent
144
145     #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
146
147     def getParser(self):
148         return self._parser
149     def setParser(self, cssParser):
150         self._parser = cssParser
151
152     def getInlineParser(self):
153         parser = self.getParser()
154         if parser:
155             return parser.parseInline
156
157     #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
158
159     def copy(self):
160         return self.__class__(
161                 self.author and self.author.copy(),
162                 self.user and self.user.copy(),
163                 self.userAgent and self.userAgent.copy())
164
165     def merge(self, author=None, user=None, userAgent=None):
166         if self.author:
167             self.author.merge(author)
168         elif author: self.author = author
169
170         if self.user:
171             self.user.merge(user)
172         elif user: self.user = user
173
174         if self.userAgent:
175             self.userAgent.merge(userAgent)
176         elif userAgent: self.userAgent = userAgent
177
178     #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
179
180     def iterCSSRulesets(self, inline=None):
181         """Rulesets, in order of importance specified by the CSS spec.
182
183         .. [1] http://www.w3.org/TR/CSS21/cascade.html#cascading-order
184         """
185         if self.user is not None:
186             yield self.user.important
187
188         if inline:
189             yield inline.important
190             yield inline.normal
191
192         if self.author is not None:
193             yield self.author.important
194             yield self.author.normal
195
196         if self.user is not None:
197             yield self.user.normal
198
199         if self.userAgent is not None:
200             yield self.userAgent.important
201             yield self.userAgent.normal
202
203     #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
204
205     def findStyleFor(self, element, attrName, default=NotImplemented):
206         """Attempts to find the style setting for attrName in the CSSRulesets.
207
208         Note: This method does not attempt to resolve rules that return
209         "inherited", "default", or values that have units (including "%").
210         This is left up to the client app to re-query the CSS in order to
211         implement these semantics.
212         """
213         attrName = self._normalizeAttrNames(attrName)
214         element = self._normalizeCSSElement(element)
215
216         rule = self.findCSSRulesFor(element, attrName)
217         return self._extractStyleForRule(rule, attrName, default)
218
219     def findStylesForEach(self, element, attrNames, default=NotImplemented):
220         """Attempts to find the style setting for attrName in the CSSRulesets.
221
222         Note: This method does not attempt to resolve rules that return
223         "inherited", "default", or values that have units (including "%").
224         This is left up to the client app to re-query the CSS in order to
225         implement these semantics.
226         """
227         attrNames = self._normalizeAttrNames(attrNames)
228         element = self._normalizeCSSElement(element)
229
230         rules = self.findCSSRulesForEach(element, attrNames)
231         return [(attrName, self._extractStyleForRule(rule, attrName, default))
232                 for attrName, rule in rules.iteritems()]
233            
234     def findAllCSSRulesFor(self, element, bSorted=False):
235         result = self._findAllCSSRulesFor(element)
236         if bSorted:
237             result = sorted(result, reverse=True)
238         return result
239        
240     def _findAllCSSRulesFor(self, element):
241         element = self._normalizeCSSElement(element)
242         inline = element.getInlineStyleEx(self.getInlineParser())
243         for ruleset in self.iterCSSRulesets(inline):
244             for rule in ruleset.findAllCSSRulesFor(element):
245                 yield rule
246
247     def findCSSRulesFor(self, element, attrName):
248         element = self._normalizeCSSElement(element)
249         attrName = self._normalizeAttrNames(attrName)
250
251         rules = []
252         inline = element.getInlineStyleEx(self.getInlineParser())
253         for ruleset in self.iterCSSRulesets(inline):
254             rules = ruleset.findCSSRuleFor(element, attrName)
255             if rules:
256                 break
257         return rules
258
259     def findCSSRulesForEach(self, element, attrNames):
260         element = self._normalizeCSSElement(element)
261         attrNames = self._normalizeAttrNames(attrNames)
262
263         allRules = dict([(name, []) for name in attrNames])
264         attrNameSet = set(attrNames)
265
266         inline = element.getInlineStyleEx(self.getInlineParser())
267         for ruleset in self.iterCSSRulesets(inline):
268             for attrName in attrNameSet:
269                 rules = ruleset.findCSSRuleFor(element, attrName)
270                 if rules:
271                     allRules[attrName] = rules
272                     attrNameSet.remove(attrName)
273
274             if not attrNameSet:
275                 break
276
277         return allRules
278
279     #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
280
281     def _normalizeCSSElement(self, element):
282         return element.asCSSElement()
283
284     def _normalizeAttrNames(self, attrNames):
285         return attrNames
286
287     def _extractStyleForRule(self, rule, attrName, default=NotImplemented):
288         if rule:
289             # rule is packed in a list to differentiate from "no rule" vs "rule
290             # whose value evalutates as False"
291             style = rule[-1][1]
292             return style[attrName]
293         elif default is not NotImplemented:
294             return default
295         else:
296             raise LookupError("Could not find style for '%s' in %r" % (attrName, rule))
297
298 #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
299 #~ CSS Selectors
300 #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
301
302 class CSSSelectorBase(object):
303     qualifiers = ()
304     inline = False
305     _hash = None
306     _specificity = None
307
308     def __init__(self, completeName='*', qualifiers=qualifiers):
309         if not isinstance(completeName, tuple):
310             completeName = (None, '*', completeName)
311         self.completeName = completeName
312         self.qualifiers = qualifiers
313
314     def _updateHash(self):
315         self._hash = hash((self.fullName, self.specificity(), self.qualifiers))
316     def __hash__(self):
317         if self._hash is None:
318             return object.__hash__(self)
319         else:
320             return self._hash
321
322     def getNSPrefix(self):
323         return self.completeName[0]
324     nsPrefix = property(getNSPrefix)
325
326     def getName(self):
327         return self.completeName[2]
328     name = property(getName)
329
330     def getNamespace(self):
331         return self.completeName[1]
332     namespace = property(getNamespace)
333
334     def getFullName(self):
335         return self.completeName[1:3]
336     fullName = property(getFullName)
337
338     def __repr__(self):
339         strArgs = (self.__class__.__name__,)+self.specificity()+(self.asString(),)
340         return '<%s %d:%d:%d:%d %s >' % strArgs
341
342     def __str__(self):
343         return self.asString()
344
345     def __cmp__(self, other):
346         result = cmp(self.specificity(), other.specificity())
347         if result != 0:
348             return result
349         result = cmp(self.fullName, other.fullName)
350         if result != 0:
351             return result
352         result = cmp(self.qualifiers, other.qualifiers)
353         return result
354
355     def specificity(self):
356         if self._specificity is None:
357             self._specificity = self._calcSpecificity()
358         return self._specificity
359
360     def _calcSpecificity(self):
361         """from http://www.w3.org/TR/CSS21/cascade.html#specificity"""
362         hashCount = 0
363         qualifierCount = 0
364         elementCount = int(self.name != '*')
365         for q in self.qualifiers:
366             if q.isHash(): hashCount += 1
367             elif q.isClass(): qualifierCount += 1
368             elif q.isAttr(): qualifierCount += 1
369             elif q.isPseudo(): elementCount += 1
370             elif q.isCombiner():
371                 i,h,q,e = q.selector.specificity()
372                 hashCount += h
373                 qualifierCount += q
374                 elementCount += e
375         return self.inline, hashCount, qualifierCount, elementCount
376
377     def matches(self, element=None):
378         if element is None:
379             return False
380
381         if not element.matchesNode(self.fullName):
382             return False
383
384         for qualifier in self.qualifiers:
385             if not qualifier.matches(element):
386                 return False
387         else:
388             return True
389
390     def asString(self):
391         result = []
392         if self.nsPrefix is not None:
393             result.append('%s|%s' % (self.nsPrefix, self.name))
394         else: result.append(self.name)
395
396         for q in self.qualifiers:
397             if q.isCombiner():
398                 result.insert(0, q.asString())
399             else:
400                 result.append(q.asString())
401         return ''.join(result)
402
403 #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
404
405 class CSSInlineSelector(CSSSelectorBase):
406     qualifiers = ()
407     inline = True
408
409 #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
410
411 class CSSMutableSelector(CSSSelectorBase, cssParser.CSSSelectorAbstract):
412     qualifiers = [] # note this is lazyily initialized in _addQualifier
413
414     def asImmutable(self):
415         return CSSImmutableSelector(self.completeName, [q.asImmutable() for q in self.qualifiers])
416
417     def combineSelectors(klass, selectorA, op, selectorB):
418         selectorB.addCombination(op, selectorA)
419         return selectorB
420     combineSelectors = classmethod(combineSelectors)
421
422     def addCombination(self, op, other):
423         self._addQualifier(CSSSelectorCombinationQualifier(op, other))
424     def addHashId(self, hashId):
425         self._addQualifier(CSSSelectorHashQualifier(hashId))
426     def addClass(self, class_):
427         self._addQualifier(CSSSelectorClassQualifier(class_))
428     def addAttribute(self, attrName):
429         self._addQualifier(CSSSelectorAttributeQualifier(attrName))
430     def addAttributeOperation(self, attrName, op, attrValue):
431         self._addQualifier(CSSSelectorAttributeQualifier(attrName, op, attrValue))
432     def addPseudo(self, name):
433         self._addQualifier(CSSSelectorPseudoQualifier(name))
434     def addPseudoFunction(self, name, params):
435         self._addQualifier(CSSSelectorPseudoQualifier(name, params))
436
437     #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
438
439     def _addQualifier(self, qualifier):
440         if self.qualifiers:
441             self.qualifiers.append(qualifier)
442         else:
443             self.qualifiers = [qualifier]
444
445 #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
446
447 class CSSImmutableSelector(CSSSelectorBase):
448     qualifiers = ()
449
450     def __init__(self, completeName='*', qualifiers=qualifiers):
451         CSSSelectorBase.__init__(self, completeName, tuple(qualifiers))
452         self._updateHash()
453
454     def fromSelector(klass, selector):
455         return klass(selector.completeName, selector.qualifiers)
456     fromSelector = classmethod(fromSelector)
457
458 #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
459 #~ CSS Selector Qualifiers -- see CSSImmutableSelector
460 #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
461
462 class CSSSelectorQualifierBase(object):
463     def isHash(self):
464         return False
465     def isClass(self):
466         return False
467     def isAttr(self):
468         return False
469     def isPseudo(self):
470         return False
471     def isCombiner(self):
472         return False
473     def asImmutable(self):
474         return self
475     def __str__(self):
476         return self.asString()
477
478 class CSSSelectorHashQualifier(CSSSelectorQualifierBase):
479     def __init__(self, hashId):
480         self.hashId = hashId
481     def isHash(self):
482         return True
483     def __hash__(self):
484         return hash((self.hashId,))
485     def asString(self):
486         return '#'+self.hashId
487     def matches(self, element):
488         return element.getIdAttr() == self.hashId
489
490 class CSSSelectorClassQualifier(CSSSelectorQualifierBase):
491     def __init__(self, classId):
492         self.classId = classId
493     def isClass(self):
494         return True
495     def __hash__(self):
496         return hash((self.classId,))
497     def asString(self):
498         return '.'+self.classId
499     def matches(self, element):
500         return self.classId in element.getClasses()
501
502 class CSSSelectorAttributeQualifier(CSSSelectorQualifierBase):
503     name, op, value = None, None, NotImplemented
504
505     def __init__(self, attrName, op=None, attrValue=NotImplemented):
506         self.name = attrName
507         if op is not self.op:
508             self.op = op
509         if attrValue is not self.value:
510             self.value = attrValue
511     def isAttr(self):
512         return True
513     def __hash__(self):
514         return hash((self.name, self.op, self.value))
515     def asString(self):
516         if self.value is NotImplemented:
517             return '[%s]' % (self.name,)
518         else: return '[%s%s%s]' % (self.name, self.op, self.value)
519     def matches(self, element):
520         op = self.op
521         if op is None:
522             return element.getAttr(self.name, NotImplemented) != NotImplemented
523         elif op == '=':
524             return self.value == element.getAttr(self.name, NotImplemented)
525         elif op == '~=':
526             return self.value in element.getAttr(self.name, '').split()
527         elif op == '|=':
528             return self.value in element.getAttr(self.name, '').split('-')
529         else:
530             raise RuntimeError("Unknown operator %r for %r" % (self.op, self))
531
532 class CSSSelectorPseudoQualifier(CSSSelectorQualifierBase):
533     def __init__(self, attrName, params=()):
534         self.name = attrName
535         self.params = tuple(params)
536     def isPseudo(self):
537         return True
538     def __hash__(self):
539         return hash((self.name, self.params))
540     def asString(self):
541         if self.params:
542             return ':'+self.name
543         else:
544             return ':%s(%s)' % (self.name, self.params)
545     def matches(self, element):
546         return element.inPseudoState(self.name, self.params)
547
548 class CSSSelectorCombinationQualifier(CSSSelectorQualifierBase):
549     def __init__(self, op, selector):
550         self.op = op
551         self.selector = selector
552     def isCombiner(self):
553         return True
554     def __hash__(self):
555         return hash((self.op, self.selector))
556     def asImmutable(self):
557         return self.__class__(self.op, self.selector.asImmutable())
558     def asString(self):
559         return '%s%s' % (self.selector.asString(), self.op)
560     def matches(self, element):
561         op, selector = self.op, self.selector
562         if op == ' ':
563             for parent in element.iterXMLParents():
564                 if selector.matches(parent):
565                     return True
566             else:
567                 return False
568         elif op == '>':
569             for parent in element.iterXMLParents():
570                 if selector.matches(parent):
571                     return True
572                 else:
573                     return False
574         elif op == '+':
575             return selector.matches(element.getPreviousSibling())
576
577 #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
578 #~ CSS Misc
579 #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
580
581 class CSSTerminalFunction(object):
582     def __init__(self, name, params):
583         self.name = name
584         self.params = params
585
586     def __repr__(self):
587         return '<CSS function: %s(%s)>' % (self.name, ', '.join(self.params))
588
589 class CSSTerminalOperator(tuple):
590     def __new__(klass, *args):
591         return tuple.__new__(klass, args)
592
593     def __repr__(self):
594         return 'op' + tuple.__repr__(self)
595
596 #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
597 #~ CSS Objects
598 #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
599
600 class CSSDeclarations(dict):
601     def copy(self):
602         return self.__class__(self.iteritems())
603
604 #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
605
606 class CSSRuleset(dict):
607     def isRuleset(self):
608         return True
609     def isDualRuleset(self):
610         return False
611
612     def copy(self):
613         result = self.__class__()
614         for nodeFilter, declarations in self.iteritems():
615             result[nodeFilter] = declarations.copy()
616
617     def findAllCSSRulesFor(self, element):
618         for nodeFilter, declarations in self.iteritems():
619             if (nodeFilter.matches(element)):
620                 yield (nodeFilter, declarations)
621
622     def findCSSRulesFor(self, element, attrName):
623         ruleResults = []
624         for nodeFilter, declarations in self.iteritems():
625             if (attrName in declarations) and (nodeFilter.matches(element)):
626                 ruleResults.append((nodeFilter, declarations))
627         ruleResults.sort()
628         return ruleResults
629
630     def findCSSRuleFor(self, element, attrName):
631         # rule is packed in a list to differentiate from "no rule" vs "rule
632         # whose value evalutates as False"
633         return self.findCSSRulesFor(element, attrName)[-1:]
634
635     def merge(self, ruleset):
636         for rhsNodeFilter, rhsDelcarations in ruleset.iteritems():
637             declarations = self.setdefault(rhsNodeFilter, rhsDelcarations)
638             if declarations is not rhsDelcarations:
639                 declarations.update(rhsDelcarations)
640
641 class CSSInlineRuleset(CSSRuleset, CSSDeclarations):
642     def copy(self):
643         return self.__class__(self.iteritems())
644
645     def findAllCSSRulesFor(self, element):
646         yield (CSSInlineSelector(), self)
647
648     def findCSSRulesFor(self, element, attrName):
649         if attrName in self:
650             return [(CSSInlineSelector(), self)]
651         else: return []
652
653     def findCSSRuleFor(self, element, attrName):
654         # rule is packed in a list to differentiate from "no rule" vs "rule
655         # whose value evalutates as False"
656         return self.findCSSRulesFor(*args, **kw)[-1:]
657
658 #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
659
660 class CSSDualRuleset(object):
661     """Combines important and non-important styles into a single ruleset"""
662     RulesetFactory = CSSRuleset
663     normal = None
664     important = None
665
666     def __init__(self, normalRuleset=None, importantRuleset=None):
667         self.setRulesets(normalRuleset, importantRuleset)
668
669     def __repr__(self):
670         strArgs = (self.__class__.__name__, self.important, self.normal)
671         return '<%s important:%r normal:%r>' % strArgs
672
673     def __len__(self):
674         return len(self.important) + len(self.normal)
675     def copy(self):
676         self.__class__(self.normal.copy(), self.important.copy())
677
678     def values(self):
679         return list(self.itervalues())
680     def keys(self):
681         return list(self.iterkeys())
682     def items(self):
683         return list(self.iteritems())
684     def iterkeys(self):
685         return itertools.chain(self.important.iterkeys(), self.normal.iterkeys())
686     def itervalues(self):
687         return itertools.chain(self.important.itervalues(), self.normal.itervalues())
688     def iteritems(self):
689         return itertools.chain(self.important.iteritems(), self.normal.iteritems())
690
691     def __getitem__(self, key):
692         if key in self.important:
693             return self.important[key]
694         else:
695             return self.normal[key]
696
697     def isRuleset(self):
698         return False
699     def isDualRuleset(self):
700         return True
701
702     def getRulesets(self):
703         return self.normal, self.important
704     def setRulesets(self, normalRuleset=None, importantRuleset=None):
705         if normalRuleset is None:
706             normalRuleset = self.RulesetFactory()
707         elif not normalRuleset.isRuleset():
708             raise ValueError("normalRuleset (%r) does not report to be a vaild ruleset" % (normalRuleset,))
709         self.normal = normalRuleset
710
711         if importantRuleset is None:
712             importantRuleset = self.RulesetFactory()
713         elif not importantRuleset.isRuleset():
714             raise ValueError("importantRuleset (%r) does not report to be a vaild ruleset" % (importantRuleset,))
715         self.important = importantRuleset
716
717     def merge(self, dualRulesetOrNormal, orImportant=None):
718         if dualRulesetOrNormal:
719             if isinstance(dualRulesetOrNormal, dict):
720                 self.normal.merge(dualRulesetOrNormal)
721             elif dualRulesetOrNormal.isDualRuleset():
722                 self.normal.merge(dualRulesetOrNormal.normal)
723                 self.important.merge(dualRulesetOrNormal.important)
724             else:
725                 self.normal.merge(dualRulesetOrNormal)
726
727         if orImportant:
728             self.important.merge(orImportant)
729
730     #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
731
732     def findAllCSSRulesFor(self, element):
733         for rule in self.important.findAllCSSRulesFor(element):
734             yield rule
735         for rule in self.normal.findAllCSSRulesFor(element):
736             yield rule
737
738     def findCSSRulesFor(self, element, attrName):
739         ruleResults = self.important.findCSSRulesFor(element, attrName)
740         ruleResults += self.normal.findCSSRulesFor(element, attrName)
741         return ruleResults
742
743     def findCSSRuleFor(self, element, attrName):
744         return (self.important.findCSSRuleFor(element, attrName) or
745                     self.normal.findCSSRuleFor(element, attrName))
746
747 #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
748 #~ CSS Builder
749 #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
750
751 class CSSBuilder(cssParser.CSSBuilderAbstract):
752     RulesetFactory = CSSRuleset
753     DualRulesetFactory = CSSDualRuleset
754     InlineRulesetFactory = CSSInlineRuleset
755     SelectorFactory = CSSMutableSelector
756     MediumSetFactory = set
757     DeclarationsFactory = CSSDeclarations
758     TermFunctionFactory = CSSTerminalFunction
759     TermOperatorFactory = CSSTerminalOperator
760     xmlnsSynonyms = {}
761     mediumSet = set()
762     charset = None
763
764     def __init__(self, mediumSet=mediumSet):
765         self.setMediumSet(mediumSet)
766
767     def isValidMedium(self, filterMediums, default=True):
768         if not filterMediums:
769             return default
770         if 'all' in filterMediums:
771             return True
772
773         filterMediums = self.MediumSetFactory(filterMediums)
774         return bool(self.getMediumSet().intersection(filterMediums))
775
776     def getMediumSet(self):
777         return self.mediumSet
778     def setMediumSet(self, mediumSet):
779         self.mediumSet = self.MediumSetFactory(mediumSet)
780     def updateMediumSet(self, mediumSet):
781         self.getMediumSet().update(mediumSet)
782
783     #~ helpers ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
784
785     def _pushState(self):
786         _restoreState = self.__dict__
787         self.__dict__ = self.__dict__.copy()
788         self._restoreState = _restoreState
789         self.namespaces = {}
790     def _popState(self):
791         self.__dict__ = self._restoreState
792
793     def _declarations(self, declarations, DeclarationsFactory=None):
794         DeclarationsFactory = DeclarationsFactory or self.DeclarationsFactory
795
796         normal, important = [], []
797         for d in declarations:
798             if d[-1]:
799                 important.append(d[:-1])
800             else: normal.append(d[:-1])
801         return DeclarationsFactory(normal), DeclarationsFactory(important)
802
803     def _xmlnsGetSynonym(self, uri):
804         # Don't forget to substitute our namespace synonyms!
805         return self.xmlnsSynonyms.get(uri or None, uri) or None
806
807     #~ css results ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
808
809     def beginStylesheet(self, context):
810         self._pushState()
811
812     def endStylesheet(self, context):
813         self._popState()
814
815     def stylesheet(self, stylesheetElements, stylesheetImports):
816         result = self.DualRulesetFactory()
817
818         for stylesheet in stylesheetImports:
819             result.merge(stylesheet)
820         for stylesheet in stylesheetElements:
821             result.merge(stylesheet)
822         return result
823
824     def beginInline(self, context):
825         self._pushState()
826
827     def endInline(self, context):
828         self._popState()
829
830     def inline(self, declarations):
831         normal, important = self._declarations(declarations, self.InlineRulesetFactory)
832         normal.merge(important)
833         return normal
834
835     def ruleset(self, selectors, declarations):
836         # Make sure to save copies of the declarations everywhere to prepare
837         # for when the values are merged in self.stylesheet
838
839         normalDecl, importantDecl = self._declarations(declarations)
840         result = self.DualRulesetFactory()
841
842         for s in selectors:
843             s = s.asImmutable()
844             if normalDecl:
845                 result.normal[s] = normalDecl.copy()
846             if importantDecl:
847                 result.important[s] = importantDecl.copy()
848         return result
849
850     #~ css namespaces ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
851
852     def resolveNamespacePrefix(self, nsPrefix, name):
853         if nsPrefix == '*':
854             return (nsPrefix, '*', name)
855         xmlns = self.namespaces.get(nsPrefix, None)
856         xmlns = self._xmlnsGetSynonym(xmlns)
857         return (nsPrefix, xmlns, name)
858
859     #~ css @ directives ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
860
861     def atCharset(self, charset):
862         self.charset = charset
863
864     def atImport(self, import_, filterMediums, cssParser):
865         if self.isValidMedium(filterMediums, True):
866             return cssParser.parseExternal(import_, self)
867         return None
868
869     def atNamespace(self, nsprefix, uri):
870         self.namespaces[nsprefix] = uri
871
872     def atMedia(self, filterMediums, rulesets):
873         if self.isValidMedium(filterMediums, False):
874             return rulesets
875         else:
876             return None
877
878     def atPage(self, page, pseudoPage, properties, margins):
879         return [self.ruleset([self.selector('*')], properties)]
880
881     def atPageMargin(self, page, pseudoPage, marginName, properties):
882         return (marginName, self.ruleset([self.selector('*')], properties))
883
884     def atFontFace(self, declarations):
885         return [self.ruleset([self.selector('*')], declarations)]
886
887     #~ css selectors ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
888
889     def selector(self, name):
890         return self.SelectorFactory(name)
891
892     def combineSelectors(self, selectorA, op, selectorB):
893         return self.SelectorFactory.combineSelectors(selectorA, op, selectorB)
894
895     #~ css declarations ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
896
897     def property(self, name, value, important=False):
898         if value is not NotImplemented:
899             return (name, value, important)
900
901     def combineTerms(self, termA, op, termB):
902         if op in (',', ' '):
903             if isinstance(termA, list):
904                 termA.append(termB)
905                 return termA
906             else:
907                 return [termA, termB]
908         elif op is None and termB is None:
909             return [termA]
910         else:
911             if isinstance(termA, list):
912                 # Bind these "closer" than the list operators -- i.e. work on
913                 # the (recursively) last element of the list
914                 termA[-1] = self.combineTerms(termA[-1], op, termB)
915                 return termA
916             else:
917                 return self.TermOperatorFactory(termA, op, termB)
918
919     def termIdent(self, value):
920         return value
921
922     def termNumber(self, value, units=None):
923         if units:
924             return value, units
925         else:
926             return value
927
928     def termColor(self, value):
929         return value
930
931     def termURI(self, value):
932         return value
933
934     def termString(self, value):
935         return value
936
937     def termUnicodeRange(self, value):
938         return value
939
940     def termFunction(self, name, value):
941         return self.TermFunctionFactory(name, value)
942
943     def termUnknown(self, src):
944         return src, NotImplemented
945
946 #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
947 #~ CSS Parser -- finally!
948 #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
949
950 class CSSParser(cssParser.CSSParser):
951     CSSBuilderFactory = CSSBuilder
952
953     def __init__(self, cssBuilder=None, create=True, **kw):
954         if not cssBuilder and create:
955             assert cssBuilder is None
956             cssBuilder = self.createCSSBuilder(**kw)
957         cssParser.CSSParser.__init__(self, cssBuilder)
958
959     def createCSSBuilder(self, **kw):
960         return self.CSSBuilderFactory(**kw)
961
962     def parseExternal(self, cssResourceName, cssBuilder):
963         if os.path.isfile(cssResourceName):
964             cssFile = file(cssResourceName, 'r')
965             return self.parseFile(cssFile, True)
966         else:
967             raise RuntimeError("Cannot resolve external CSS file: \"%s\"" % cssResourceName)
968
969 #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
970 #~ Convience functions
971 #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
972
973 class _CSSNamespace(object):
974     CSSParserFactory = CSSParser
975     _defaultParser = None
976
977     def _getDefaultParser(klass):
978         if klass._defaultParser is None:
979             klass._defaultParser = klass.CSSParserFactory()
980         return klass._defaultParser
981     _getDefaultParser = classmethod(_getDefaultParser)
982
983     def updateNamespace(klass, ns):
984         ns['parse'] = klass.parse
985         ns['parseFile'] = klass.parseFile
986         ns['parseInline'] = klass.parseInline
987         ns['parseAttributes'] = klass.parseAttributes
988         ns['parseSingleAttr'] = klass.parseSingleAttr
989     updateNamespace = classmethod(updateNamespace)
990
991     def parse(klass, src, context=None):
992         return klass._getDefaultParser().parse(src, context)
993     parse = classmethod(parse)
994
995     def parseFile(klass, srcFile, context=None, closeFile=False):
996         return klass._getDefaultParser().parseFile(srcFile, context, closeFile)
997     parseFile = classmethod(parseFile)
998
999     def parseInline(klass, src, context=None):
1000         return klass._getDefaultParser().parseInline(src, context)
1001     parseInline = classmethod(parseInline)
1002
1003     def parseAttributes(klass, attributes={}, context=None, **kwAttributes):
1004         return klass._getDefaultParser().parseAttributes(attributes, context, **kwAttributes)
1005     parseAttributes = classmethod(parseAttributes)
1006
1007     def parseSingleAttr(klass, attrValue):
1008         return klass._getDefaultParser().parseSingleAttr(attrValue)
1009     parseSingleAttr = classmethod(parseSingleAttr)
1010
1011 _CSSNamespace.updateNamespace(locals())
1012
Note: See TracBrowser for help on using the browser.