| 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 |
|
|---|