root/trunk/proj/w3c/cssParser.py

Revision 404, 51.2 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 parser, with CSS 3 enhanced page parsing (compatible with CSS-2.1).
11
12 The CSS 2.1 Specification from which this parser was derived can be found at
13 http://www.w3.org/TR/CSS21/
14
15 The CSS 3 Paged Media Module from which enhancements to this parser were
16 derived can be found at http://www.w3.org/TR/css3-page/
17
18 Primary Classes:
19     * CSSParser
20         Parses CSS source forms into results using a Builder Pattern.  Must
21         provide concrete implemenation of CSSBuilderAbstract.
22
23     * CSSBuilderAbstract
24         Outlines the interface between CSSParser and it's rule-builder.
25         Compose CSSParser with a concrete implementation of the builder to get
26         usable results from the CSS parser.
27
28 Dependencies:
29     python 2.3 (or greater)
30     re
31
32 Credits:
33     * 2005.07.12 Andre Soereng
34         http://www.w3.org/TR/CSS21/syndata.html#parsing-errors   
35    
36     * 2005.03.23 Gary Poster
37         Host of bugfixes and testing of the engine.  He also added support for
38         page margins from http://www.w3.org/TR/css3-page/
39
40     * 2005.04.06 Fred Drake
41         Bugfix and test for ticket:43 and ticket:44
42        
43     * 2005.04.15 Henning Ramm
44         Lots of good documentation for the CSSParser various parse functions,
45         which inspired more thorough documentation of the interfaces.
46 """
47
48 #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
49 #~ Imports
50 #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
51
52 import re
53
54 try:
55     set
56 except NameError:
57     from sets import Set as set
58
59 #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
60 #~ Definitions
61 #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
62
63 class CSSSelectorAbstract(object):
64     """Outlines the interface between CSSParser and its rule-builder for selectors.
65
66     CSSBuilderAbstract.selector and CSSBuilderAbstract.combineSelectors must
67     return concrete implementations of this abstract.
68
69     See css.CSSMutableSelector for an example implementation.
70     """
71
72     def addHashId(self, hashId):
73         """Modify the selector to respond to hashId. 
74        
75         #hashId {...}
76
77         HashId is a string."""
78         raise NotImplementedError('Subclass Responsibility: %r' % (self,))
79     def addClass(self, className):
80         """Modify the selector to respond to className classes.
81        
82         .className {...}
83
84         className is a string."""
85         raise NotImplementedError('Subclass Responsibility: %r' % (self,))
86     def addAttribute(self, attrName):
87         """Modify the selector to respond to objects with an attrName attribute.
88        
89         [attrName] {...}
90
91         attrName is a string."""
92         raise NotImplementedError('Subclass Responsibility: %r' % (self,))
93     def addAttributeOperation(self, attrName, attrOp, attrValue):
94         """Modify the selector to respond to objects with an attrName attribute
95         related to attrValue by attrOp.
96        
97         [attrName] {...}
98
99         attrName and attrValue are strings.
100         attrOp is one of '=', '~=', '|=', '&=', '^=', '!=', '<>'
101         """
102         raise NotImplementedError('Subclass Responsibility: %r' % (self,))
103     def addPseudo(self, pseudo):
104         """Modify the selector to respond to pseudo-classes.
105
106         :pseudo {...}
107
108         pseudo is a string."""
109         raise NotImplementedError('Subclass Responsibility: %r' % (self,))
110     def addPseudoFunction(self, pseudoFn, value):
111         """Modify the selector to respond to pseudo-class functions.
112
113         :pseudoFn(value) {...}
114
115         pseudoFn and value are strings."""
116         raise NotImplementedError('Subclass Responsibility: %r' % (self,))
117
118 class CSSBuilderAbstract(object):
119     """Outlines the interface between CSSParser and its rule-builder. 
120    
121     Compose CSSParser with a concrete implementation of the builder to get
122     usable results from the CSS parser.
123
124     See css.CSSBuilder for an example implementation
125     """
126
127     #~ css results ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
128
129     def beginStylesheet(self, context):
130         """Called at the beginning of a full stylesheet parse.
131        
132         Context is simply passed through the parser to the builder"""
133         raise NotImplementedError('Subclass Responsibility: %r' % (self,))
134     def stylesheet(self, rulesets, imports):
135         """Should return a stylesheet suitable for the subclass. 
136
137         Rulesets is a list of results from ruleset() and @directives from at*()
138         methods.  Imports is a list of imports returned from atImport()."""
139         raise NotImplementedError('Subclass Responsibility: %r' % (self,))
140     def endStylesheet(self, context):
141         """Called at the end of a full stylesheet parse.
142        
143         Context is simply passed through the parser to the builder"""
144         raise NotImplementedError('Subclass Responsibility: %r' % (self,))
145
146     def beginInline(self, context):
147         """Called at the beginning of an inline stylesheet parse.
148
149         Context is simply passed through the parser to the builder"""
150         raise NotImplementedError('Subclass Responsibility: %r' % (self,))
151     def inline(self, declarations):
152         """Should return an inline declaration result suitable for the subclass. 
153
154         Declarations is a list of properties returned by property()."""
155         raise NotImplementedError('Subclass Responsibility: %r' % (self,))
156     def endInline(self, context):
157         """Called at the end of an inline stylesheet parse.
158
159         Context is simply passed through the parser to the builder"""
160         raise NotImplementedError('Subclass Responsibility: %r' % (self,))
161
162     def ruleset(self, selectors, declarations):
163         """Should return the ruleset suitable for the subclass. 
164        
165         Selectors is a list of selectors returned by either selector() or
166         combineSelectors().  Delcarations is a list of properties returned by
167         property().
168         """
169         raise NotImplementedError('Subclass Responsibility: %r' % (self,))
170
171     #~ css namespaces ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
172
173     def resolveNamespacePrefix(self, nsPrefix, name):
174         """Should return a single name correlating to the namespace prefix and
175         the name.  This affects the name passed to termIdent(), selector(),
176         and CSSSelectorAbstract's addAttribute() and addAttributeOperation().
177
178         See atNamespace and the CSS spec.
179         """
180         raise NotImplementedError('Subclass Responsibility: %r' % (self,))
181
182     #~ css @ directives ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
183
184     def atCharset(self, charset):
185         """Charset is a string from the @charset directive"""
186         raise NotImplementedError('Subclass Responsibility: %r' % (self,))
187     def atImport(self, import_, filterMediums, cssParser):
188         """Should return the result of importing 'import_' reference string if
189         the current medium matches the filterMediums.  An implementation may
190         choose to return a callback for this method instead.  The list of
191         results from this method are passed to stylehseet()
192        
193         cssParser is an instance implementation of CSSParser"""
194         raise NotImplementedError('Subclass Responsibility: %r' % (self,))
195     def atNamespace(self, nsPrefix, uri):
196         """Called for each @namespace directive to inform the builder of nsPrefix's associated url."""
197         raise NotImplementedError('Subclass Responsibility: %r' % (self,))
198     def atMedia(self, filterMediums, rulesets):
199         """Should return rulesets if current medium matches the filterMediums.
200         rulesets is a list of results from ruleset()"""
201         raise NotImplementedError('Subclass Responsibility: %r' % (self,))
202     def atPage(self, page, pseudoPage, declarations, margins):
203         """Should return a list of rulesets (possibly empty) to be passed to stylesheet().
204
205         Page and PseudoPage are strings.  Declarations is a list of properties.
206         Margins is a list of results from atPageMargin(). 
207
208         See atPageMargin() and the extended `CSS 3 candidate recommendation`__
209
210         .. __: http://www.w3.org/TR/css3-page/
211         """
212         raise NotImplementedError('Subclass Responsibility: %r' % (self,))
213     def atPageMargin(self, page, pseudoPage, marginName, declarations):
214         """Should return a margin result suitable for atPage()'s margins argument.
215
216         Page, PseudoPage, and MarginName are strings.  Declarations is a
217         list of properties.
218
219         See atPage() and the extended `CSS 3 candidate recommendation`__
220
221         .. __: http://www.w3.org/TR/css3-page/
222         """
223         raise NotImplementedError('Subclass Responsibility: %r' % (self,))
224     def atFontFace(self, declarations):
225         """Parses an @font-face directive.  Should return a list of rulesets to
226         be passed to stylesheet()."""
227         raise NotImplementedError('Subclass Responsibility: %r' % (self,))
228
229     #~ css selectors ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
230
231     def combineSelectors(self, selectorA, combiner, selectorB):
232         """Should combine a selector suitable to the subclass implementation.
233         Combiner is typically one of " ", "+", or ">".  Please see the CSS spec
234         for definition of these combiners. 
235        
236         The result must implement CSSSelectorAbstract"""
237         raise NotImplementedError('Subclass Responsibility: %r' % (self,))
238     def selector(self, name):
239         """Should return a selector suitable to the subclass implementation.
240
241         The result must implement CSSSelectorAbstract"""
242         raise NotImplementedError('Subclass Responsibility: %r' % (self,))
243
244     #~ css declarations ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
245
246     def property(self, name, value, important=False):
247         """Should return what the subclass defines as a property binding of
248         name, value and importance.
249        
250         Name is a string, value is the result of either a term*() or
251         combineTerms() methods."""
252         raise NotImplementedError('Subclass Responsibility: %r' % (self,))
253     def combineTerms(self, termA, combiner, termB):
254         """Needs to return the appropriate combination result of termA and
255         termB using combiner.  Combiner is usually one of '/', '+', ',' and the
256         terms are results from the term*() methods provided here. """
257         raise NotImplementedError('Subclass Responsibility: %r' % (self,))
258     def termIdent(self, value):
259         """Should return what the subclass defines as an ident terminal.
260         Value is a string."""
261         raise NotImplementedError('Subclass Responsibility: %r' % (self,))
262     def termNumber(self, value, units=None):
263         """Should return what the subclass defines as a number terminal
264         Value and unites are strings."""
265         raise NotImplementedError('Subclass Responsibility: %r' % (self,))
266     def termColor(self, value):
267         """Should return what the subclass defines as a color terminal
268         Value is a string."""
269         raise NotImplementedError('Subclass Responsibility: %r' % (self,))
270     def termURI(self, value):
271         """Should return what the subclass defines as a URI terminal
272         Value is a string."""
273         raise NotImplementedError('Subclass Responsibility: %r' % (self,))
274     def termString(self, value):
275         """Should return what the subclass defines as a string terminal
276         Value is a string."""
277         raise NotImplementedError('Subclass Responsibility: %r' % (self,))
278     def termUnicodeRange(self, value):
279         """Should return what the subclass defines as a unicode range terminal
280         Value is a string."""
281         raise NotImplementedError('Subclass Responsibility: %r' % (self,))
282     def termFunction(self, name, value):
283         """Should return what the subclass defines as a function terminal
284         Name and value are strings."""
285         raise NotImplementedError('Subclass Responsibility: %r' % (self,))
286     def termUnknown(self, src):
287         """Should return what the subclass decides to do with an unknown terminal
288         Src is a string."""
289         raise NotImplementedError('Subclass Responsibility: %r' % (self,))
290
291 #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
292 #~ CSS Parser
293 #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
294
295 class CSSParseError(Exception):
296     src = None
297     fullsrc = None
298     inline = False
299     _baseSrcRef = ('<unknown>', 1)
300
301     def __init__(self, msg, src):
302         Exception.__init__(self, msg)
303         self.src = src
304
305     def __str__(self):
306         return '%s (%s)' % (Exception.__str__(self), self.getSrcRefString())
307
308     def setFullCSSSource(self, fullsrc, inline=False):
309         self.fullsrc = fullsrc
310         if inline:
311             self.inline = inline
312
313     def _getFullSrcIndex(self):
314         return len(self.fullsrc) - len(self.src)
315
316     def getLineSrc(self):
317         lineIdx = 1 + self.fullsrc.rfind('\n', 0, self._getFullSrcIndex())
318         lineSrc = self.fullsrc[lineIdx:].split('\n', 1)[0]
319         return lineSrc
320
321     def getLine(self):
322         return self._getLineOffset() + self.fullsrc.count('\n', 0, self._getFullSrcIndex())
323
324     def getColumn(self):
325         linesrc = self.getLineSrc()
326         return linesrc.index(self.src.split('\n', 1)[0]) + 1
327
328     def getSrcRef(self):
329         return (self.getFilename(), self.getLine())
330     def getBaseSrcRef(self, srcRef):
331         return self._baseSrcRef
332     def setBaseSrcRef(self, srcRef):
333         self._baseSrcRef = srcRef
334
335     def getFilename(self):
336         return self._baseSrcRef[0]
337     def setFilename(self, filename):
338         self._baseSrcRef = (filename, 1)
339     def _getLineOffset(self):
340         return self._baseSrcRef[1]
341
342     def getSrcRefString(self):
343         return '\"%s\" line: %d col: %d' % (self.getFilename(), self.getLine(), self.getColumn())
344
345     def raiseFromSrc(self, srcref=None):
346         from TG.introspection.stack import traceSrcrefExec
347         if srcref is not None:
348             self.setBaseSrcRef(srcref)
349         traceSrcrefExec(self.getSrcRef(), 'raise cssError', cssError=self)
350
351 #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
352
353 class CSSParser(object):
354     """CSS-2.1 parser dependent only upon the re module.
355    
356     Implemented directly from http://www.w3.org/TR/CSS21/grammar.html
357     Tested with some existing CSS stylesheets for portability.
358    
359     CSS Parsing API:
360         * setCSSBuilder()
361             To set a concrete instance implementing CSSBuilderAbstract
362
363         * parseFile()
364             Use to parse external stylesheets using a file-like object
365
366             >>> cssFile = open('test.css', 'r')
367             >>> stylesheets = myCSSParser.parseFile(cssFile)
368
369         * parse()
370             Use to parse embedded stylesheets using source string
371
372             >>> cssSrc = '''
373             ... body,body.body {
374             ...     font: 110%, "Times New Roman", Arial, Verdana, Helvetica, serif;
375             ...     background: White;
376             ...     color: Black;
377             ... }
378             ... a {text-decoration: underline;}
379             ... '''
380             >>> stylesheets = myCSSParser.parse(cssSrc)
381
382         * parseInline()
383             Use to parse inline stylesheets using attribute source string
384
385             >>> style = 'font: 110%, "Times New Roman", Arial, Verdana, Helvetica, serif; background: White; color: Black'
386             >>> stylesheets = myCSSParser.parseInline(style)
387
388         * parseAttributes()
389             Use to parse attribute string values into inline stylesheets
390
391             >>> stylesheets = myCSSParser.parseAttributes(
392             ...     font='110%, "Times New Roman", Arial, Verdana, Helvetica, serif',
393             ...     background='White',
394             ...     color='Black')
395
396         * parseSingleAttr()
397             Use to parse a single string value into a CSS expression
398
399             >>> fontValue = myCSSParser.parseSingleAttr('110%, "Times New Roman", Arial, Verdana, Helvetica, serif')
400     """
401
402     #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
403     #~ Constants / Variables / Etc.
404     #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
405
406     ParseError = CSSParseError
407     bParseStrict = False
408
409     AttributeOperators = set(('=', '~=', '|=', '&=', '^=', '!=', '<>'))
410     SelectorQualifiers = set(('#', '.', '[', ':'))
411     SelectorCombiners = set(('+', '>'))
412     ExpressionOperators = set(('/', '+', ','))
413     DeclarationSetters = set((':', '='))
414     DeclarationBoundry = set(('', ',', '{','}', '[',']','(',')'))
415
416     # atKeywordHandlers is a class-level dictionary to enable extending
417     # @-directives in a standard way.  See _parseAtKeyword for details.
418     atKeywordHandlers = {}
419
420     #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
421     #~ Regular expressions
422     #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
423
424     if True: # makes the following code foldable
425         _orRule = lambda *args: '|'.join(args)
426         _reflags = re.I | re.M | re.U
427         i_hex = '[0-9a-fA-F]'
428         i_nonascii = u'[\200-\377]'
429         i_unicode = '\\\\(?:%s){1,6}\s?' % i_hex
430         i_escape = _orRule(i_unicode, u'\\\\[ -~\200-\377]')
431         i_nmstart = _orRule('[-A-Za-z_]', i_nonascii, i_escape)
432         i_nmchar = _orRule('[-0-9A-Za-z_]', i_nonascii, i_escape)
433         i_ident = '((?:%s)(?:%s)*)' % (i_nmstart,i_nmchar)
434         re_ident = re.compile(i_ident, _reflags)
435         i_element_name = '((?:%s)|\*)' % (i_ident[1:-1],)
436         re_element_name = re.compile(i_element_name, _reflags)
437         i_namespace_selector = '((?:%s)|\*|)\|(?!=)' % (i_ident[1:-1],)
438         re_namespace_selector = re.compile(i_namespace_selector, _reflags)
439         i_class = '\\.' + i_ident
440         re_class = re.compile(i_class, _reflags)
441         i_hash = '#((?:%s)+)' % i_nmchar
442         re_hash = re.compile(i_hash, _reflags)
443         i_rgbcolor = '(#%s{6}|#%s{3})' % (i_hex, i_hex)
444         re_rgbcolor = re.compile(i_rgbcolor, _reflags)
445         i_nl = u'\n|\r\n|\r|\f'
446         i_escape_nl = u'\\\\(?:%s)' % i_nl
447         i_string_content = _orRule(u'[\t !#$%&(-~]', i_escape_nl, i_nonascii, i_escape)
448         i_string1 = u'\"((?:%s|\')*)\"' % i_string_content
449         i_string2 = u'\'((?:%s|\")*)\'' % i_string_content
450         i_string = _orRule(i_string1, i_string2)
451         re_string = re.compile(i_string, _reflags)
452
453         i_string1_unexpectedEnd = i_string1[:-1]
454         i_string2_unexpectedEnd = i_string2[:-1]
455         i_string_unexpectedEnd = _orRule(i_string1_unexpectedEnd, i_string2_unexpectedEnd)
456         re_string_unexpectedEnd = re.compile(i_string_unexpectedEnd, _reflags)
457
458         i_uri = (u'url\\(\s*(?:(?:%s)|((?:%s)+))\s*\\)'
459                  % (i_string, _orRule('[!#$%&*-~]', i_nonascii, i_escape)))
460         re_uri = re.compile(i_uri, _reflags)
461         i_num = u'([-+]?[0-9]+(?:\\.[0-9]+)?)|([-+]?\\.[0-9]+)'
462         re_num = re.compile(i_num, _reflags)
463         i_unit = '(%%|%s)?' % i_ident
464         re_unit = re.compile(i_unit, _reflags)
465         i_function = i_ident + '\\('
466         re_function = re.compile(i_function, _reflags)
467         i_functionterm = u'[-+]?' + i_function
468         re_functionterm = re.compile(i_functionterm, _reflags)
469         i_unicoderange1 = "(?:U\\+%s{1,6}-%s{1,6})" % (i_hex, i_hex)
470         i_unicoderange2 = "(?:U\\+\?{1,6}|{h}(\?{0,5}|{h}(\?{0,4}|{h}(\?{0,3}|{h}(\?{0,2}|{h}(\??|{h}))))))"
471         i_unicoderange = i_unicoderange1 # u'(%s|%s)' % (i_unicoderange1, i_unicoderange2)
472         re_unicoderange = re.compile(i_unicoderange, _reflags)
473         i_important = u'!\s*(important)'
474         re_important = re.compile(i_important, _reflags)
475
476         i_comment = u'\\s*(?:\\s*\\/\\*[^*]*\\*+([^/*][^*]*\\*+)*\\/\\s*)*'
477         re_comment = re.compile(i_comment, _reflags)
478
479         i_declarationError = u'((?:[^;{}]*(?:{[^}]*})?)*)'
480         re_declarationError = re.compile(i_declarationError, _reflags)
481
482         i_atKeywordErrorStart = u'([^;{]*[;{])'
483         re_atKeywordErrorStart = re.compile(i_atKeywordErrorStart, _reflags)
484         i_atKeywordErrorGroup = u'([^{}]*[{}])'
485         re_atKeywordErrorGroup = re.compile(i_atKeywordErrorGroup, _reflags)
486
487         del _orRule
488
489     #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
490     #~ Public
491     #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
492
493     def __init__(self, cssBuilder=None):
494         self.setCSSBuilder(cssBuilder)
495        
496     #~ CSS Builder to delegate to ~~~~~~~~~~~~~~~~~~~~~~~~
497
498     def getCSSBuilder(self):
499         """A concrete instance implementing CSSBuilderAbstract"""
500         return self._cssBuilder
501     def setCSSBuilder(self, cssBuilder):
502         """A concrete instance implementing CSSBuilderAbstract"""
503         self._cssBuilder = cssBuilder
504     cssBuilder = property(getCSSBuilder, setCSSBuilder)
505
506     #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
507     #~ Public CSS Parsing API
508     #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
509
510     def parseFile(self, srcFile, context=None, closeFile=False):
511         """Parses CSS file-like objects using the current cssBuilder.
512         Use for external stylesheets.
513
514             >>> cssFile = open('test.css', 'r')
515             >>> stylesheets = myCSSParser.parseFile(cssFile)
516
517         Context is a pass-through variable to the CSSBuilder.
518         """
519
520         try:
521             result = self.parse(srcFile.read(), context)
522         finally:
523             if closeFile:
524                 srcFile.close()
525         return result
526
527     def parse(self, src, context=None):
528         """Parses CSS string source using the current cssBuilder. 
529         Use for embedded stylesheets.
530
531             >>> cssSrc = '''
532                 body,body.body {
533                     font: 110%, "Times New Roman", Arial, Verdana, Helvetica, serif;
534                     background: White;
535                     color: Black;
536                 }
537                 a {text-decoration: underline;}
538             '''
539             >>> stylesheets = myCSSParser.parse(cssSrc)
540        
541         Context is just a pass-through variable to the CSSBuilder.
542         """
543
544         self.cssBuilder.beginStylesheet(context)
545         try:
546             try:
547                 src, stylesheet = self._parseStylesheet(src)
548             except self.ParseError, err:
549                 err.setFullCSSSource(src)
550                 raise
551         finally:
552             self.cssBuilder.endStylesheet(context)
553         return stylesheet
554
555     def parseInline(self, src, context=None):
556         """Parses CSS inline source string using the current cssBuilder.
557         Use to parse inline stylesheets using attribute source string
558         Use to parse a tag's 'sytle'-like attribute.
559
560             >>> style = 'font: 110%, "Times New Roman", Arial, Verdana, Helvetica, serif; background: White; color: Black'
561             >>> stylesheets = myCSSParser.parseInline(style)
562
563         Context is just a pass-through variable to the CSSBuilder.
564         """
565
566         self.cssBuilder.beginInline(context)
567         try:
568             try:
569                 src, declarations = self._parseDeclarationGroup(self._stripCSS(src), braces=False)
570             except self.ParseError, err:
571                 err.setFullCSSSource(src, inline=True)
572                 raise
573
574             result = self.cssBuilder.inline(declarations)
575         finally:
576             self.cssBuilder.endInline(context)
577         return result
578
579     def parseAttributes(self, attributes={}, context=None, **kwAttributes):
580         """Parses CSS attribute source strings and return an inline stylesheet.
581         Use to parse a tag's highly CSS-based attributes like 'font'.
582
583         Use to parse attribute string values into inline stylesheets
584
585             >>> stylesheets = myCSSParser.parseAttributes(
586                     font='110%, "Times New Roman", Arial, Verdana, Helvetica, serif',
587                     background='White',
588                     color='Black')
589
590         Context is just a pass-through variable to the CSSBuilder.
591
592         See also: parseSingleAttr
593         """
594         if attributes:
595             kwAttributes.update(attributes)
596
597         self.cssBuilder.beginInline(context)
598         try:
599             properties = []
600             try:
601                 for propertyName, src in kwAttributes.iteritems():
602                     src, property = self._parseDeclarationProperty(self._stripCSS(src), propertyName)
603                     if property is not None:
604                         properties.append(property)
605
606             except self.ParseError, err:
607                 err.setFullCSSSource(src, inline=True)
608                 raise
609
610             result = self.cssBuilder.inline(properties)
611         finally:
612             self.cssBuilder.endInline(context)
613         return result
614
615     def parseSingleAttr(self, attrValue):
616         """Parse a single CSS attribute source string and return the built CSS expression.
617         Use to parse a tag's highly CSS-based attributes like 'font'.
618         Use to parse a single string value into a CSS expression
619
620             >>> fontValue = myCSSParser.parseSingleAttr('110%, "Times New Roman", Arial, Verdana, Helvetica, serif')
621
622
623         See also: parseAttributes
624         """
625
626         attributes = self.parseAttributes(singleAttr=attrValue)
627         return attributes['singleAttr']
628
629     #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
630     #~ Internal _parse methods
631     #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
632
633     def _parseStylesheet(self, src):
634         """Parses a CSS stylesheet into imports and rulesets, returning the
635         result of cssBuilder.stylesheet()
636        
637         ::
638             stylesheet
639                 : [ CHARSET_SYM S* STRING S* ';' ]?
640                     [S|CDO|CDC]* [ import [S|CDO|CDC]* ]*
641                     [ [ ruleset | media | page | font_face ] [S|CDO|CDC]* ]*
642                 ;
643         """
644         # [ CHARSET_SYM S* STRING S* ';' ]?
645         src = self._parseAtCharset(src)
646
647         # [S|CDO|CDC]*
648         src = self._parseSCDOCDC(src)
649         #  [ import [S|CDO|CDC]* ]*
650         src, imports = self._parseAtImports(src)
651
652         # [ namespace [S|CDO|CDC]* ]*
653         src = self._parseAtNamespace(src)
654
655         rulesets = []
656
657         # [ [ ruleset | atkeywords ] [S|CDO|CDC]* ]*
658         while src: # due to ending with ]*
659             if src.startswith('@'):
660                 # @media, @page, @font-face
661                 src, atResults = self._parseAtKeyword(src)
662                 if atResults is not None:
663                     rulesets.extend(atResults)
664             else:
665                 # ruleset
666                 src, ruleset = self._parseRuleset(src)
667                 rulesets.append(ruleset)
668
669             # [S|CDO|CDC]*
670             src = self._parseSCDOCDC(src)
671
672         stylesheet = self.cssBuilder.stylesheet(rulesets, imports)
673         return src, stylesheet
674
675     def _parseSCDOCDC(self, src):
676         """[S|CDO|CDC]*"""
677         while 1:
678             src = self._stripCSS(src)
679             if src.startswith('<!--'):
680                 src = src[4:]
681             elif src.startswith('-->'):
682                 src = src[3:]
683             else:
684                 break
685         return src
686
687     #~ CSS @ directives ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
688
689     def _parseAtCharset(self, src):
690         """Parses @charset directives.
691
692         ::
693             [ CHARSET_SYM S* STRING S* ';' ]?
694         """
695         if src.startswith('@charset '):
696             src = self._stripCSS(src[9:])
697             src, charset = self._getString(src)
698             src = self._stripCSS(src)
699             if src[:1] != ';':
700                 raise self.ParseError('@charset expected a terminating \';\'', src)
701             src = self._stripCSS(src[1:])
702
703             self.cssBuilder.atCharset(charset)
704         return src
705
706     def _parseAtImports(self, src):
707         """Returns a list of imports returned by cssBuilder.atImport().
708
709         ::
710             [ import [S|CDO|CDC]* ]*"""
711         result = []
712         while src.startswith('@import '):
713             src = self._stripCSS(src[8:])
714
715             src, import_ = self._getStringOrURI(src)
716             if import_ is None:
717                 raise self.ParseError('Import expecting string or url', src)
718
719             filterMediums = []
720             src, medium = self._getIdent(self._stripCSS(src))
721             while medium is not None:
722                 filterMediums.append(medium)
723                 if src[:1] == ',':
724                     src = self._stripCSS(src[1:])
725                     src, medium = self._getIdent(src)
726                 else:
727                     break
728
729             if src[:1] != ';':
730                 raise self.ParseError('@import expected a terminating \';\'', src)
731             src = self._stripCSS(src[1:])
732
733             stylesheet = self.cssBuilder.atImport(import_, filterMediums, self)
734             if stylesheet is not None:
735                 result.append(stylesheet)
736
737             src = self._parseSCDOCDC(src)
738         return src, result
739
740     def _parseAtNamespace(self, src):
741         """Parses @namespace directives.
742
743         Calls cssBuilder.atNamespace for each directive.
744
745         ::
746             namespace :
747             @namespace S* [IDENT S*]? [STRING|URI] S* ';' S*
748         """
749        
750         src = self._parseSCDOCDC(src)
751         while src.startswith('@namespace'):
752             src = self._stripCSS(src[len('@namespace'):])
753
754             src, namespace = self._getStringOrURI(src)
755             if namespace is None:
756                 src, nsPrefix = self._getIdent(src)
757                 if nsPrefix is None:
758                     raise self.ParseError('@namespace expected an identifier or a URI', src)
759                 src, namespace = self._getStringOrURI(self._stripCSS(src))
760                 if namespace is None:
761                     raise self.ParseError('@namespace expected a URI', src)
762             else:
763                 nsPrefix = None
764
765             src = self._stripCSS(src)
766             if src[:1] != ';':
767                 raise self.ParseError('@namespace expected a terminating \';\'', src)
768             src = self._stripCSS(src[1:])
769
770             self.cssBuilder.atNamespace(nsPrefix, namespace)
771
772             src = self._parseSCDOCDC(src)
773         return src
774
775     def _parseAtKeyword(self, src):
776         """[media | page | font_face | unknown_keyword]"""
777         if src.startswith('@'):
778             src = src[1:]
779         else:
780             raise self.ParseError('atKeyword missing @ sign', src)
781
782         src, atDirective = self._getIdent(src)
783         src = self._stripCSS(src)
784
785         directiveHandler = self.atKeywordHandlers.get(atDirective,
786                                     self.__class__._parseAtUnknownHandler)
787         return directiveHandler(self, atDirective, src)
788
789     def _parseAtUnknownHandler(self, atDirective, src):
790         if self.bParseStrict:
791             raise self.ParseError('Unknown @-Directive \"%s\"' % atDirective, src)
792
793         src, content = self._getMatchResult(self.re_atKeywordErrorStart, src)
794         src = self._stripCSS(src)
795
796         content = content.lstrip()
797         if content[0] == '{': n = 1
798         elif content[-1] == '{': n = 2
799         else: n = 0
800
801         while n:
802             src, content = self._getMatchResult(self.re_atKeywordErrorGroup, src)
803             src = self._stripCSS(src)
804             n += {'{':1, '}':-1}.get(content[-1], 0)
805
806         return src, []
807
808     def _parseAtMedia(self, atDirective, src):
809         """media
810         : MEDIA_SYM S* medium [ ',' S* medium ]* '{' S* ruleset* '}' S*
811         ;
812         """
813         filterMediums = []
814         while src and src[0] != '{':
815             src, medium = self._getIdent(src)
816             if medium is None:
817                 raise self.ParseError('@media rule expected media identifier', src)
818             filterMediums.append(medium)
819             if src[0] == ',':
820                 src = self._stripCSS(src[1:])
821             else:
822                 src = self._stripCSS(src)
823
824         if not src.startswith('{'):
825             raise self.ParseError('Ruleset opening \'{\' not found', src)
826         src = self._stripCSS(src[1:])
827        
828         rulesets = []
829         while src and not src.startswith('}'):
830             src, ruleset = self._parseRuleset(src)
831             rulesets.append(ruleset)
832             src = self._stripCSS(src)
833
834         if not src.startswith('}'):
835             if self.bParseStrict or src:
836                 raise self.ParseError('Ruleset closing \'}\' not found', src)
837         elif src:
838             src = self._stripCSS(src[1:])
839
840         result = self.cssBuilder.atMedia(filterMediums, rulesets)
841         if result is None:
842             result = []
843         return src, result
844     atKeywordHandlers['media'] = _parseAtMedia
845
846     def _parseAtPage(self, atDirective, src):
847         """@page directive.  Returns result from cssBuilder.atPage
848        
849         Supports extended CSS 3 candidate recommendation
850         http://www.w3.org/TR/css3-page/
851        
852         ::
853             @page
854                 : PAGE_SYM S* IDENT? pseudo_page? S*
855                 '{' S* [ declaration | @margin ] [ ';'
856                     S* [ declaration | @margin ]? ]* '}' S*
857                 ;
858         """
859         if not src.startswith('{'):
860             src, page = self._getIdent(src)
861         else: page = ''
862
863         if src[:1] == ':':
864             src, pseudoPage = self._getIdent(src[1:])
865             src = src[1:]
866         else: pseudoPage = ''
867         src = self._stripCSS(src)
868
869         if not src.startswith('{'):
870             raise self.ParseError('Ruleset opening \'{\' not found', src)
871         src = self._stripCSS(src[1:])
872
873         declarations, margins = [], []
874         while src[:1] not in self.DeclarationBoundry:
875             # declaration group while loop.
876             if src.startswith('@'):
877                 # @ specific margin
878                 src, margin = self._parseAtPageMargin(src[1:], page, pseudoPage)
879                 margins.append(margin)
880             else:
881                 # declaration
882                 src, property = self._parseDeclaration(src)
883                 if property is not None:
884                     declarations.append(property)
885
886                 if src.startswith(';'):
887                     src = self._stripCSS(src[1:])
888
889             # [S|CDO|CDC]*
890             src = self._parseSCDOCDC(src)
891
892         if not src.startswith('}'):
893             raise self.ParseError('Ruleset closing \'}\' not found', src)
894         else:
895             src = self._stripCSS(src[1:])
896
897         result = self.cssBuilder.atPage(page, pseudoPage, declarations, margins)
898         if result is None:
899             result = []
900         return self._stripCSS(src), result
901     atKeywordHandlers['page'] = _parseAtPage
902
903     def _parseAtPageMargin(self, src, page, pseudoPage):
904         """@page margin directive.  Returns result from cssBuilder.atPageMargin
905        
906         Supports extended CSS 3 candidate recommendation
907         http://www.w3.org/TR/css3-page/
908        
909         ::
910             page margin
911                 : margin_sym S* '{' declaration [ ';' S* declaration? ]* '}' S*
912                 ;
913         ;
914
915         See _parseAtPage()
916         """
917         src, margin = self._getIdent(src)
918         if margin is None:
919             raise self.ParseError('At-margin rule received an unknown margin', src)
920         src, declarations = self._parseDeclarationGroup(self._stripCSS(src))
921         result = self.cssBuilder.atPageMargin(page, pseudoPage, margin.lower(), declarations)
922         return src, result
923
924     def _parseAtFontFace(self, atDirective, src):
925         src, declarations = self._parseDeclarationGroup(src)
926         result = self.cssBuilder.atFontFace(declarations)
927         if result is None:
928             result = []
929         return src, result
930     atKeywordHandlers['font-face'] = _parseAtFontFace
931
932     #~ ruleset - see selector and declaration groups ~~~~
933
934     def _parseRuleset(self, src):
935         """Parses a CSS ruleset by parsing the selectors and declarations.
936         Returns the result of cssBuilder.ruleset() from the list of selectors
937         and list of declarations.
938        
939         ::
940             ruleset
941                 : selector [ ',' S* selector ]*
942                     '{' S* declaration [ ';' S* declaration ]* '}' S*
943                 ;
944         """
945         src, selectors = self._parseSelectorGroup(src)
946         src, declarations = self._parseDeclarationGroup(self._stripCSS(src))
947         result = self.cssBuilder.ruleset(selectors, declarations)
948         return src, result
949
950     #~ selector parsing ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
951
952     def _parseSelectorGroup(self, src):
953         """Returns a list of selectors, complex or simple, returned from cssBuilder.selector().
954
955         Each element must implement CSSSelectorAbstract."""
956         selectors = []
957         while src[:1] not in ('{','}', ']','(',')', ';', ''):
958             src, selector = self._parseSelector(src)
959             if selector is None:
960                 break
961             selectors.append(selector)
962             if src.startswith(','):
963                 src = self._stripCSS(src[1:])
964         return src, selectors
965
966     def _parseSelector(self, src):
967         """Parses a complex selector.
968
969         Selectors are combined using cssBuilder.combineSelectors() as necessary.
970         Returns a modified selector from cssBuilder.selector() which must implement
971         CSSSelectorAbstract. 
972
973         ::
974             selector
975                 : simple_selector [ combinator simple_selector ]*
976                 ;
977         """
978         src, selector = self._parseSimpleSelector(src)
979         while src[:1] not in ('', ',', ';', '{','}', '[',']','(',')'):
980             for combiner in self.SelectorCombiners:
981                 if src.startswith(combiner):
982                     src = self._stripCSS(src[len(combiner):])
983                     break
984             else:
985                 combiner = ' '
986             src, selectorB = self._parseSimpleSelector(src)
987             selector = self.cssBuilder.combineSelectors(selector, combiner, selectorB)
988         return self._stripCSS(src), selector
989
990     def _parseSimpleSelector(self, src):
991         """Parses a single selector. 
992        
993         Complex selectors are handled by _parseSelector.  Returns a modified
994         selector from cssBuilder.selector() which must implement
995         CSSSelectorAbstract.
996
997         ::
998             simple_selector
999                 : [ namespace_selector ]? element_name?
1000                 [ HASH | class | attrib | pseudo ]* S*
1001                 ;
1002         """
1003         src = self._stripCSS(src)
1004         src, nsPrefix = self._getMatchResult(self.re_namespace_selector, src)
1005         src = self._stripCSS(src)
1006         src, name = self._getMatchResult(self.re_element_name, src)
1007         src = self._stripCSS(src)
1008         if name:
1009             pass # already *successfully* assigned
1010         elif src[:1] in self.SelectorQualifiers:
1011             name = '*'
1012         else:
1013             raise self.ParseError('Selector name or qualifier expected', src)
1014
1015         name = self.cssBuilder.resolveNamespacePrefix(nsPrefix, name)
1016         selector = self.cssBuilder.selector(name)
1017
1018         while src and src[:1] in self.SelectorQualifiers:
1019             src, hash_ = self._getMatchResult(self.re_hash, src)
1020             src = self._stripCSS(src)
1021             if hash_ is not None:
1022                 selector.addHashId(hash_)
1023                 continue
1024
1025             src, class_ = self._getMatchResult(self.re_class, src)
1026             src = self._stripCSS(src)
1027             if class_ is not None:
1028                 selector.addClass(class_)
1029                 continue
1030
1031             if src.startswith('['):
1032                 src, selector = self._parseSelectorAttribute(src, selector)
1033             elif src.startswith(':'):
1034                 src, selector = self._parseSelectorPseudo(src, selector)
1035             else:
1036                 break
1037
1038         return self._stripCSS(src), selector
1039
1040     def _parseSelectorAttribute(self, src, selector):
1041         """Parses a attribute selector. 
1042
1043         Selector argument must implement CSSSelectorAbstract.
1044         Please see CSS spec for definition.
1045        
1046         ::
1047             attrib
1048                 : '[' S* [ namespace_selector ]? IDENT S*
1049                     [ [ '=' | INCLUDES | DASHMATCH ] S* [ IDENT | STRING ] S* ]? ']'
1050                 ;
1051         """
1052         if not src.startswith('['):
1053             raise self.ParseError('Selector Attribute opening \'[\' not found', src)
1054         src = self._stripCSS(src[1:])
1055
1056         src, nsPrefix = self._getMatchResult(self.re_namespace_selector, src)
1057         src = self._stripCSS(src)
1058         src, attrName = self._getIdent(src)
1059
1060         if attrName is None:
1061             raise self.ParseError('Expected a selector attribute name', src)
1062         if nsPrefix is not None:
1063             attrName = self.cssBuilder.resolveNamespacePrefix(nsPrefix, attrName)
1064
1065         for attrOp in self.AttributeOperators:
1066             if src.startswith(attrOp):
1067                 break
1068         else:
1069             attrOp = ''
1070         src = self._stripCSS(src[len(attrOp):])
1071
1072         if attrOp:
1073             src, attrValue = self._getIdent(src)
1074             if attrValue is None:
1075                 src, attrValue = self._getString(src)
1076                 if attrValue is None:
1077                     raise self.ParseError('Expected a selector attribute value', src)
1078         else:
1079             attrValue = None
1080
1081         if not src.startswith(']'):
1082             raise self.ParseError('Selector Attribute closing \']\' not found', src)
1083         else:
1084             src = src[1:]
1085
1086         if attrOp:
1087             selector.addAttributeOperation(attrName, attrOp, attrValue)
1088         else:
1089             selector.addAttribute(attrName)
1090         return src, selector
1091
1092     def _parseSelectorPseudo(self, src, selector):
1093         """Parses a pseudo selector. 
1094
1095         Selector argument must implement CSSSelectorAbstract.
1096         Please see CSS spec for definition.
1097
1098         ::
1099             pseudo
1100                 : ':' [ IDENT | function ]
1101                 ;
1102         """
1103         if not src.startswith(':'):
1104             raise self.ParseError('Selector Pseudo \':\' not found', src)
1105         src = src[1:]
1106
1107         src, name = self._getIdent(src)
1108         if not name:
1109             raise self.ParseError('Selector Pseudo identifier not found', src)
1110
1111         if src.startswith('('):
1112             # function
1113             src = self._stripCSS(src[1:])
1114             src, term = self._parseExpression(src, True)
1115             if not src.startswith(')'):
1116                 raise self.ParseError('Selector Pseudo Function closing \')\' not found', src)
1117             src = src[1:]
1118             selector.addPseudoFunction(name, term)
1119         else:
1120             selector.addPseudo(name)
1121
1122         return src, selector
1123
1124     #~ declaration and expression parsing ~~~~~~~~~~~~~~~
1125
1126     def _parseDeclarationGroup(self, src, braces=True):
1127         """Returns a list of properties returned from cssBuilder.property"""
1128         if src.startswith('{'):
1129             src, braces = src[1:], True
1130         elif braces:
1131             raise self.ParseError('Declaration group opening \'{\' not found', src)
1132        
1133         properties = []
1134         src = self._stripCSS(src)
1135         while src[:1] not in self.DeclarationBoundry:
1136             src, property = self._parseDeclaration(src)
1137             if property is not None:
1138                 properties.append(property)
1139             if src.startswith(';'):
1140                 src = self._stripCSS(src[1:])
1141
1142         if braces:
1143             if not src.startswith('}'):
1144                 if self.bParseStrict or src:
1145                     raise self.ParseError('Declaration group closing \'}\' not found', src)
1146             src = src[1:]
1147
1148         return self._stripCSS(src), properties
1149
1150     def _parseDeclaration(self, src):
1151         """Returns a property or None. 
1152        
1153         Parses only the property name and the declaration setter.
1154         _parseDeclarationProperty completes the property by parsing the
1155         expression.
1156        
1157         ::
1158             declaration
1159                 : ident S* ':' S* expr prio?
1160                 | /* empty */
1161         ;
1162         """
1163         # property
1164         src, propertyName = self._getIdent(src)
1165         property = None
1166
1167         if propertyName is not None:
1168             src = self._stripCSS(src)
1169             # S* : S*
1170             if self.bParseStrict:
1171                 if src[:1] != ':':
1172                     raise self.ParseError('Malformed declaration missing ":" before the value', src)
1173                 src, property = self._parseDeclarationProperty(self._stripCSS(src[1:]), propertyName)
1174
1175             elif src[:1] in self.DeclarationSetters:
1176                 # Note: we are being fairly flexible here...  technically, the
1177                 # ":" is *required*, but in the name of flexibility we support
1178                 # an "=" transition
1179                 src, property = self._parseDeclarationProperty(self._stripCSS(src[1:]), propertyName)
1180
1181             else:
1182                 # dump characters to next ; or }
1183                 src, dumpText = self._getMatchResult(self.re_declarationError, src)
1184                 src = self._stripCSS(src)
1185         elif self.bParseStrict:
1186             raise self.ParseError('Property name not present', src)
1187
1188         return src, property
1189
1190     def _parseDeclarationProperty(self, src, propertyName):
1191         """Returns the result from cssBuilder.property(), combining name and value for the declaration"""
1192         # expr
1193         src, expr = self._parseExpression(src)
1194         if expr is NotImplemented:
1195             return src, None
1196
1197         # prio?
1198         src, important = self._getMatchResult(self.re_important, src)
1199         src = self._stripCSS(src)
1200
1201         property = self.cssBuilder.property(propertyName, expr, important)
1202         return src, property
1203
1204     def _parseExpression(self, src, returnList=False):
1205         """Returns the terms (combined if necessary) for the property's value expression.
1206
1207         ::
1208             expr
1209             : term [ operator term ]*
1210             ;
1211         """
1212         src, term = self._parseExpressionTerm(src)
1213
1214         if term is NotImplemented:
1215             return src, term
1216
1217         operator = None
1218         while src[:1] not in ('', ';', '{','}', '[',']', ')'):
1219             for operator in self.ExpressionOperators:
1220                 if src.startswith(operator):
1221                     src = src[len(operator):]
1222                     break
1223             else:
1224                 operator = ' '
1225             src, term2 = self._parseExpressionTerm(self._stripCSS(src))
1226             if term2 is NotImplemented:
1227                 break
1228             else:
1229                 term = self.cssBuilder.combineTerms(term, operator, term2)
1230
1231         if operator is None and returnList:
1232             term = self.cssBuilder.combineTerms(term, None, None)
1233             return src, term
1234         else:
1235             return src, term
1236
1237     def _parseExpressionTerm(self, src):
1238         """Returns the result from the applicable cssBuilder.term*() method.
1239
1240         ::
1241             term
1242             : unary_operator?  [ NUMBER S* | PERCENTAGE S* | LENGTH S* | EMS S*
1243                 | EXS S* | ANGLE S* | TIME S* | FREQ S* | function ] | STRING S*
1244                 | IDENT S* | URI S* | RGB S* | UNICODERANGE S* | hexcolor
1245         ;
1246         """
1247         src, result = self._getMatchResult(self.re_num, src)
1248         if result is not None:
1249             src, units = self._getMatchResult(self.re_unit, src)
1250             term = self.cssBuilder.termNumber(result, units)
1251             return self._stripCSS(src), term
1252
1253         src, result = self._getString(src, self.re_uri)
1254         if result is not None:
1255             term = self.cssBuilder.termURI(result)
1256             return self._stripCSS(src), term
1257
1258         src, result = self._getString(src)
1259         if result is not None:
1260             term = self.cssBuilder.termString(result)
1261             return self._stripCSS(src), term
1262
1263         src, result = self._getMatchResult(self.re_functionterm, src)
1264         if result is not None:
1265             src, params = self._parseExpression(src, True)
1266             if src[0] != ')':
1267                 raise self.ParseError('Terminal function expression expected closing \')\'', src)
1268             src = self._stripCSS(src[1:])
1269             term = self.cssBuilder.termFunction(result, params)
1270             return src, term
1271
1272         src, result = self._getMatchResult(self.re_rgbcolor, src)
1273         if result is not None:
1274             term = self.cssBuilder.termColor(result)
1275             return self._stripCSS(src), term
1276
1277         src, result = self._getMatchResult(self.re_unicoderange, src)
1278         if result is not None:
1279             term = self.cssBuilder.termUnicodeRange(result)
1280             return self._stripCSS(src), term
1281
1282         src, nsPrefix = self._getMatchResult(self.re_namespace_selector, src)
1283         src, result = self._getIdent(src)
1284         if result is not None:
1285             if nsPrefix is not None:
1286                 result = self.cssBuilder.resolveNamespacePrefix(nsPrefix, result)
1287             term = self.cssBuilder.termIdent(result)
1288             return self._stripCSS(src), term
1289
1290         src2, result = self._getString(src, self.re_string_unexpectedEnd)
1291         if result:
1292             if self.bParseStrict:
1293                 raise self.ParseError('Unexpected end of string literal', src)
1294
1295             src2 = self._stripCSS(src2)
1296             if not src2:
1297                 # Special case where the CSS file was truncated, and we
1298                 # should actually return the unterminated string
1299                 term = self.cssBuilder.termString(result)
1300             else:
1301                 # Per section 4.2 of the CSS21 spec, unterminated strings
1302                 # should be ignored
1303                 term = NotImplemented
1304             return src2, term
1305
1306         if self.bParseStrict:
1307             raise self.ParseError('Malformed declaration missing value', src)
1308         else:
1309             return self.cssBuilder.termUnknown(src)
1310
1311     #~ utility methods ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1312    
1313     def _getIdent(self, src, default=None):
1314         return self._getMatchResult(self.re_ident, src, default)
1315
1316     def _getString(self, src, rexpression=None, default=None):
1317         if rexpression is None:
1318             rexpression = self.re_string
1319         result = rexpression.match(src)
1320
1321         if result:
1322             strres = filter(None, result.groups())
1323             if strres:
1324                 strres = strres[0]
1325             else:
1326                 strres = ''
1327             return src[result.end():], strres
1328         else:
1329             return src, default
1330
1331     def _getStringOrURI(self, src):
1332         src, result = self._getString(src, self.re_uri)
1333         if result is None:
1334             src, result = self._getString(src)
1335         return src, result
1336
1337     def _getMatchResult(self, rexpression, src, default=None, group=1):
1338         result = rexpression.match(src)
1339         if result:
1340             return src[result.end():], result.group(group)
1341         else:
1342             return src, default
1343
1344     def _stripCSS(self, src):
1345         # Get rid of the comments
1346         match = self.re_comment.match(src)
1347         return src[match.end():]
1348
Note: See TracBrowser for help on using the browser.