1from __future__ import unicode_literals, division, absolute_import, print_function 2import xml.dom 3import css_parser 4from . import cssrule 5from .marginrule import MarginRule 6from .cssstyledeclaration import CSSStyleDeclaration 7from itertools import chain 8"""CSSPageRule implements DOM Level 2 CSS CSSPageRule.""" 9 10__all__ = ['CSSPageRule'] 11__docformat__ = 'restructuredtext' 12__version__ = '$Id$' 13 14import sys 15if sys.version_info[0] >= 3: 16 string_type = str 17else: 18 string_type = basestring 19 20 21def as_list(p): 22 if isinstance(p, list): 23 return p 24 return list(p) 25 26 27class CSSPageRule(cssrule.CSSRuleRules): 28 """ 29 The CSSPageRule interface represents a @page rule within a CSS style 30 sheet. The @page rule is used to specify the dimensions, orientation, 31 margins, etc. of a page box for paged media. 32 33 Format:: 34 35 page : 36 PAGE_SYM S* IDENT? pseudo_page? S* 37 '{' S* [ declaration | margin ]? [ ';' S* [ declaration | margin ]? ]* '}' S* 38 ; 39 40 pseudo_page : 41 ':' [ "left" | "right" | "first" ] 42 ; 43 44 margin : 45 margin_sym S* '{' declaration [ ';' S* declaration? ]* '}' S* 46 ; 47 48 margin_sym : 49 TOPLEFTCORNER_SYM | 50 TOPLEFT_SYM | 51 TOPCENTER_SYM | 52 TOPRIGHT_SYM | 53 TOPRIGHTCORNER_SYM | 54 BOTTOMLEFTCORNER_SYM | 55 BOTTOMLEFT_SYM | 56 BOTTOMCENTER_SYM | 57 BOTTOMRIGHT_SYM | 58 BOTTOMRIGHTCORNER_SYM | 59 LEFTTOP_SYM | 60 LEFTMIDDLE_SYM | 61 LEFTBOTTOM_SYM | 62 RIGHTTOP_SYM | 63 RIGHTMIDDLE_SYM | 64 RIGHTBOTTOM_SYM 65 ; 66 67 `cssRules` contains a list of `MarginRule` objects. 68 """ 69 70 def __init__(self, selectorText=None, style=None, parentRule=None, 71 parentStyleSheet=None, readonly=False): 72 """ 73 If readonly allows setting of properties in constructor only. 74 75 :param selectorText: 76 type string 77 :param style: 78 CSSStyleDeclaration for this CSSStyleRule 79 """ 80 super(CSSPageRule, self).__init__(parentRule=parentRule, 81 parentStyleSheet=parentStyleSheet) 82 self._atkeyword = '@page' 83 self._specificity = (0, 0, 0) 84 85 tempseq = self._tempSeq() 86 87 if selectorText: 88 self.selectorText = selectorText 89 tempseq.append(self.selectorText, 'selectorText') 90 else: 91 self._selectorText = self._tempSeq() 92 93 if style: 94 self.style = style 95 else: 96 self.style = CSSStyleDeclaration() 97 98 tempseq.append(self.style, 'style') 99 100 self._setSeq(tempseq) 101 self._readonly = readonly 102 103 def __repr__(self): 104 return "css_parser.css.%s(selectorText=%r, style=%r)" % ( 105 self.__class__.__name__, 106 self.selectorText, 107 self.style.cssText) 108 109 def __str__(self): 110 return ("<css_parser.css.%s object selectorText=%r specificity=%r " + 111 "style=%r cssRules=%r at 0x%x>") % ( 112 self.__class__.__name__, 113 self.selectorText, 114 self.specificity, 115 self.style.cssText, 116 len(self.cssRules), 117 id(self)) 118 119 def __contains__(self, margin): 120 """Check if margin is set in the rule.""" 121 return margin in as_list(self.keys()) 122 123 def keys(self): 124 "Return list of all set margins (MarginRule)." 125 return as_list(r.margin for r in self.cssRules) 126 127 def __getitem__(self, margin): 128 """Retrieve the style (of MarginRule) 129 for `margin` (which must be normalized). 130 """ 131 for r in self.cssRules: 132 if r.margin == margin: 133 return r.style 134 135 def __setitem__(self, margin, style): 136 """Set the style (of MarginRule) 137 for `margin` (which must be normalized). 138 """ 139 for i, r in enumerate(self.cssRules): 140 if r.margin == margin: 141 r.style = style 142 return i 143 else: 144 return self.add(MarginRule(margin, style)) 145 146 def __delitem__(self, margin): 147 """Delete the style (the MarginRule) 148 for `margin` (which must be normalized). 149 """ 150 for r in self.cssRules: 151 if r.margin == margin: 152 self.deleteRule(r) 153 154 def __parseSelectorText(self, selectorText): 155 """ 156 Parse `selectorText` which may also be a list of tokens 157 and returns (selectorText, seq). 158 159 see _setSelectorText for details 160 """ 161 # for closures: must be a mutable 162 new = {'wellformed': True, 'last-S': False, 163 'name': 0, 'first': 0, 'lr': 0} 164 165 def _char(expected, seq, token, tokenizer=None): 166 # pseudo_page, :left, :right or :first 167 val = self._tokenvalue(token) 168 if not new['last-S'] and expected in ['page', ': or EOF']\ 169 and ':' == val: 170 try: 171 identtoken = next(tokenizer) 172 except StopIteration: 173 self._log.error( 174 'CSSPageRule selectorText: No IDENT found.', token) 175 else: 176 ival, ityp = self._tokenvalue(identtoken),\ 177 self._type(identtoken) 178 if self._prods.IDENT != ityp: 179 self._log.error('CSSPageRule selectorText: Expected ' 180 'IDENT but found: %r' % ival, token) 181 else: 182 if ival not in ('first', 'left', 'right'): 183 self._log.warn('CSSPageRule: Unknown @page ' 184 'selector: %r' 185 % (':'+ival,), neverraise=True) 186 if ival == 'first': 187 new['first'] = 1 188 else: 189 new['lr'] = 1 190 seq.append(val + ival, 'pseudo') 191 return 'EOF' 192 return expected 193 else: 194 new['wellformed'] = False 195 self._log.error('CSSPageRule selectorText: Unexpected CHAR: %r' 196 % val, token) 197 return expected 198 199 def S(expected, seq, token, tokenizer=None): 200 "Does not raise if EOF is found." 201 if expected == ': or EOF': 202 # pseudo must directly follow IDENT if given 203 new['last-S'] = True 204 return expected 205 206 def IDENT(expected, seq, token, tokenizer=None): 207 "" 208 val = self._tokenvalue(token) 209 if 'page' == expected: 210 if self._normalize(val) == 'auto': 211 self._log.error('CSSPageRule selectorText: Invalid pagename.', 212 token) 213 else: 214 new['name'] = 1 215 seq.append(val, 'IDENT') 216 217 return ': or EOF' 218 else: 219 new['wellformed'] = False 220 self._log.error('CSSPageRule selectorText: Unexpected IDENT: ' 221 '%r' % val, token) 222 return expected 223 224 def COMMENT(expected, seq, token, tokenizer=None): 225 "Does not raise if EOF is found." 226 seq.append(css_parser.css.CSSComment([token]), 'COMMENT') 227 return expected 228 229 newseq = self._tempSeq() 230 wellformed, expected = self._parse(expected='page', 231 seq=newseq, tokenizer=self._tokenize2(selectorText), 232 productions={'CHAR': _char, 233 'IDENT': IDENT, 234 'COMMENT': COMMENT, 235 'S': S}, 236 new=new) 237 wellformed = wellformed and new['wellformed'] 238 239 # post conditions 240 if expected == 'ident': 241 self._log.error( 242 'CSSPageRule selectorText: No valid selector: %r' % 243 self._valuestr(selectorText)) 244 245 return wellformed, newseq, (new['name'], new['first'], new['lr']) 246 247 def __parseMarginAndStyle(self, tokens): 248 "tokens is a list, no generator (yet)" 249 g = iter(tokens) 250 styletokens = [] 251 252 # new rules until parse done 253 cssRules = [] 254 255 for token in g: 256 if token[0] == 'ATKEYWORD' and \ 257 self._normalize(token[1]) in MarginRule.margins: 258 259 # MarginRule 260 m = MarginRule(parentRule=self, 261 parentStyleSheet=self.parentStyleSheet) 262 m.cssText = chain([token], g) 263 264 # merge if margin set more than once 265 for r in cssRules: 266 if r.margin == m.margin: 267 for p in m.style: 268 r.style.setProperty(p, replace=False) 269 break 270 else: 271 cssRules.append(m) 272 273 continue 274 275 # TODO: Properties? 276 styletokens.append(token) 277 278 return cssRules, styletokens 279 280 def _getCssText(self): 281 """Return serialized property cssText.""" 282 return css_parser.ser.do_CSSPageRule(self) 283 284 def _setCssText(self, cssText): 285 """ 286 :exceptions: 287 - :exc:`~xml.dom.SyntaxErr`: 288 Raised if the specified CSS string value has a syntax error and 289 is unparsable. 290 - :exc:`~xml.dom.InvalidModificationErr`: 291 Raised if the specified CSS string value represents a different 292 type of rule than the current one. 293 - :exc:`~xml.dom.HierarchyRequestErr`: 294 Raised if the rule cannot be inserted at this point in the 295 style sheet. 296 - :exc:`~xml.dom.NoModificationAllowedErr`: 297 Raised if the rule is readonly. 298 """ 299 super(CSSPageRule, self)._setCssText(cssText) 300 301 tokenizer = self._tokenize2(cssText) 302 if self._type(self._nexttoken(tokenizer)) != self._prods.PAGE_SYM: 303 self._log.error('CSSPageRule: No CSSPageRule found: %s' % 304 self._valuestr(cssText), 305 error=xml.dom.InvalidModificationErr) 306 else: 307 newStyle = CSSStyleDeclaration(parentRule=self) 308 ok = True 309 310 selectortokens, startbrace = self._tokensupto2(tokenizer, 311 blockstartonly=True, 312 separateEnd=True) 313 styletokens, braceorEOFtoken = self._tokensupto2(tokenizer, 314 blockendonly=True, 315 separateEnd=True) 316 nonetoken = self._nexttoken(tokenizer) 317 if self._tokenvalue(startbrace) != '{': 318 ok = False 319 self._log.error('CSSPageRule: No start { of style declaration ' 320 'found: %r' % 321 self._valuestr(cssText), startbrace) 322 elif nonetoken: 323 ok = False 324 self._log.error('CSSPageRule: Trailing content found.', 325 token=nonetoken) 326 327 selok, newselseq, specificity = self.__parseSelectorText(selectortokens) 328 ok = ok and selok 329 330 val, type_ = self._tokenvalue(braceorEOFtoken),\ 331 self._type(braceorEOFtoken) 332 333 if val != '}' and type_ != 'EOF': 334 ok = False 335 self._log.error( 336 'CSSPageRule: No "}" after style declaration found: %r' % 337 self._valuestr(cssText)) 338 else: 339 if 'EOF' == type_: 340 # add again as style needs it 341 styletokens.append(braceorEOFtoken) 342 343 # filter pagemargin rules out first 344 cssRules, styletokens = self.__parseMarginAndStyle(styletokens) 345 346 # SET, may raise: 347 newStyle.cssText = styletokens 348 349 if ok: 350 self._selectorText = newselseq 351 self._specificity = specificity 352 self.style = newStyle 353 self.cssRules = css_parser.css.CSSRuleList() 354 for r in cssRules: 355 self.cssRules.append(r) 356 357 cssText = property(_getCssText, _setCssText, 358 doc="(DOM) The parsable textual representation of this rule.") 359 360 def _getSelectorText(self): 361 """Wrapper for css_parser Selector object.""" 362 return css_parser.ser.do_CSSPageRuleSelector(self._selectorText) 363 364 def _setSelectorText(self, selectorText): 365 """Wrapper for css_parser Selector object. 366 367 :param selectorText: 368 DOM String, in CSS 2.1 one of 369 370 - :first 371 - :left 372 - :right 373 - empty 374 375 :exceptions: 376 - :exc:`~xml.dom.SyntaxErr`: 377 Raised if the specified CSS string value has a syntax error 378 and is unparsable. 379 - :exc:`~xml.dom.NoModificationAllowedErr`: 380 Raised if this rule is readonly. 381 """ 382 self._checkReadonly() 383 384 # may raise SYNTAX_ERR 385 wellformed, newseq, specificity = self.__parseSelectorText(selectorText) 386 if wellformed: 387 self._selectorText = newseq 388 self._specificity = specificity 389 390 selectorText = property(_getSelectorText, _setSelectorText, 391 doc="(DOM) The parsable textual representation of " 392 "the page selector for the rule.") 393 394 def _setStyle(self, style): 395 """ 396 :param style: 397 a CSSStyleDeclaration or string 398 """ 399 self._checkReadonly() 400 # Under Python2.x this was basestring but given unicode literals ... 401 if isinstance(style, string_type): 402 self._style = CSSStyleDeclaration(cssText=style, parentRule=self) 403 else: 404 style._parentRule = self 405 self._style = style 406 407 style = property(lambda self: self._style, _setStyle, 408 doc="(DOM) The declaration-block of this rule set, " 409 "a :class:`~css_parser.css.CSSStyleDeclaration`.") 410 411 def insertRule(self, rule, index=None): 412 """Implements base ``insertRule``.""" 413 rule, index = self._prepareInsertRule(rule, index) 414 415 if rule is False or rule is True: 416 # done or error 417 return 418 419 # check hierarchy 420 if isinstance(rule, css_parser.css.CSSCharsetRule) or \ 421 isinstance(rule, css_parser.css.CSSFontFaceRule) or \ 422 isinstance(rule, css_parser.css.CSSImportRule) or \ 423 isinstance(rule, css_parser.css.CSSNamespaceRule) or \ 424 isinstance(rule, CSSPageRule) or \ 425 isinstance(rule, css_parser.css.CSSMediaRule): 426 self._log.error('%s: This type of rule is not allowed here: %s' 427 % (self.__class__.__name__, rule.cssText), 428 error=xml.dom.HierarchyRequestErr) 429 return 430 431 return self._finishInsertRule(rule, index) 432 433 specificity = property(lambda self: self._specificity, 434 doc="""Specificity of this page rule (READONLY). 435Tuple of (f, g, h) where: 436 437 - if the page selector has a named page, f=1; else f=0 438 - if the page selector has a ':first' pseudo-class, g=1; else g=0 439 - if the page selector has a ':left' or ':right' pseudo-class, h=1; else h=0 440""") 441 442 type = property(lambda self: self.PAGE_RULE, 443 doc="The type of this rule, as defined by a CSSRule " 444 "type constant.") 445 446 # constant but needed: 447 wellformed = property(lambda self: True) 448