1# -*- coding: utf-8 -*- 2# cython: language_level=3, always_allow_keywords=True 3 4## Copyright 1999-2018 by LivingLogic AG, Bayreuth/Germany 5## Copyright 1999-2018 by Walter Dörwald 6## 7## All Rights Reserved 8## 9## See ll/xist/__init__.py for the license 10 11 12""" 13This module contains functions related to the handling of CSS. 14""" 15 16import os, contextlib, operator 17 18try: 19 import cssutils 20 from cssutils import css, stylesheets, codec 21except ImportError: 22 cssutils = None 23else: 24 import logging 25 cssutils.log.setLevel(logging.FATAL) 26 27from ll import misc, url 28from ll.xist import xsc, xfind 29from ll.xist.ns import html 30 31 32__docformat__ = "reStructuredText" 33 34 35def _isstyle(path): 36 if path: 37 node = path[-1] 38 return isinstance(node, (html.style, html.link)) and str(node.attrs.type) == "text/css" 39 return False 40 41 42def replaceurls(stylesheet, replacer): 43 """ 44 Replace all URLs appearing in the :class:`CSSStyleSheet` :obj:`stylesheet`. 45 For each URL the function :obj:`replacer` will be called and the URL will 46 be replaced with the result. 47 """ 48 def newreplacer(u): 49 return str(replacer(url.URL(u))) 50 cssutils.replaceUrls(stylesheet, newreplacer) 51 52 53def geturls(stylesheet): 54 """ 55 Return a list of all URLs appearing in the :class:`CSSStyleSheet` 56 :obj:`stylesheet`. 57 """ 58 return [url.URL(u) for u in cssutils.getUrls(stylesheet)] # This requires cssutils 0.9.5b1 59 60 61def _getmedia(stylesheet): 62 while stylesheet is not None: 63 if stylesheet.media is not None: 64 return {mq.value.mediaType for mq in stylesheet.media} 65 stylesheet = stylesheet.parentStyleSheet 66 return None 67 68 69def _doimport(wantmedia, parentsheet, base): 70 def prependbase(u): 71 if base is not None: 72 u = base/u 73 return u 74 75 havemedia = _getmedia(parentsheet) 76 if wantmedia is None or not havemedia or wantmedia in havemedia: 77 replaceurls(parentsheet, prependbase) 78 for rule in parentsheet.cssRules: 79 if rule.type == css.CSSRule.IMPORT_RULE: 80 href = url.URL(rule.href) 81 if base is not None: 82 href = base/href 83 havemedia = rule.media 84 with contextlib.closing(href.open("rb")) as r: 85 href = r.finalurl() 86 text = r.read() 87 sheet = css.CSSStyleSheet(href=str(href), media=havemedia, parentStyleSheet=parentsheet) 88 sheet.cssText = text 89 yield from _doimport(wantmedia, sheet, href) 90 elif rule.type == css.CSSRule.MEDIA_RULE: 91 if wantmedia in (mq.value.mediaType for mq in rule.media): 92 yield from rule.cssRules 93 elif rule.type == css.CSSRule.STYLE_RULE: 94 yield rule 95 96 97def iterrules(node, base=None, media=None, title=None): 98 """ 99 Return an iterator for all CSS rules defined in the HTML tree :obj:`node`. 100 This will parse the CSS defined in any :class:`html.style` or 101 :class:`html.link` element (and recursively in those stylesheets imported 102 via the ``@import`` rule). The rules will be returned as 103 :class:`CSSStyleRule` objects from the :mod:`cssutils` package (so this 104 requires :mod:`cssutils`). 105 106 The :obj:`base` argument will be used as the base URL for parsing the 107 stylesheet references in the tree (so :const:`None` means the URLs will be 108 used exactly as they appear in the tree). All URLs in the style properties 109 will be resolved. 110 111 If :obj:`media` is given, only rules that apply to this media type will 112 be produced. 113 114 :obj:`title` can be used to specify which stylesheet group should be used. 115 If :obj:`title` is :const:`None` only the persistent and preferred 116 stylesheets will be used. If :obj:`title` is a string only the persistent 117 stylesheets and alternate stylesheets with that style name will be used. 118 119 For a description of "persistent", "preferred" and "alternate" stylesheets 120 see <http://www.w3.org/TR/2002/WD-xhtml2-20020805/mod-styleSheet.html#sec_20.1.2.> 121 """ 122 if base is not None: 123 base = url.URL(base) 124 125 def matchlink(node): 126 if node.attrs.media.hasmedia(media): 127 if title is None: 128 if "title" not in node.attrs and "alternate" not in str(node.attrs.rel).split(): 129 return True 130 elif not node.attrs.title.isfancy() and str(node.attrs.title) == title and "alternate" in str(node.attrs.rel).split(): 131 return True 132 return False 133 134 def matchstyle(node): 135 if node.attrs.media.hasmedia(media): 136 if title is None: 137 if "title" not in node.attrs: 138 return True 139 elif str(node.attrs.title) == title: 140 return True 141 return False 142 143 def doiter(node): 144 for cssnode in node.walknodes(_isstyle): 145 if isinstance(cssnode, html.style): 146 href = str(base) if base is not None else None 147 if matchstyle(cssnode): 148 stylesheet = cssutils.parseString(str(cssnode.content), href=href, media=str(cssnode.attrs.media)) 149 yield from _doimport(media, stylesheet, base) 150 else: # link 151 if "href" in cssnode.attrs: 152 href = cssnode.attrs.href.asURL() 153 if base is not None: 154 href = base/href 155 if matchlink(cssnode): 156 stylesheet = cssutils.parseUrl(str(href), media=str(cssnode.attrs.media)) 157 yield from _doimport(media, stylesheet, href) 158 return misc.Iterator(doiter(node)) 159 160 161def applystylesheets(node, base=None, media=None, title=None): 162 """ 163 :func:`applystylesheets` modifies the XIST tree :obj:`node` by removing all 164 CSS (from :class:`html.link` and :class:`html.style` elements and their 165 ``@import``\ed stylesheets) and putting the resulting style properties into 166 the ``style`` attribute of every affected element instead. 167 168 For the meaning of :obj:`base`, :obj:`media` and :obj:`title` see 169 :func:`iterrules`. 170 """ 171 172 def iterstyles(node, rules): 173 yield from rules 174 # According to CSS 2.1 (http://www.w3.org/TR/CSS21/cascade.html#specificity) 175 # style attributes have the highest weight, so we yield it last 176 # (CSS 3 uses the same weight) 177 if "style" in node.attrs: 178 style = node.attrs.style 179 if not style.isfancy(): 180 yield ( 181 (1, 0, 0, 0), 182 xfind.IsSelector(node), 183 cssutils.parseStyle(str(style)) # parse the style out of the style attribute 184 ) 185 186 rules = [] 187 for rule in iterrules(node, base=base, media=media, title=title): 188 for sel in rule.selectorList: 189 rules.append((sel.specificity, selector(sel), rule.style)) 190 rules.sort(key=operator.itemgetter(0)) 191 count = 0 192 for cursor in node.walk(xsc.Element): 193 del cursor.node[_isstyle] # drop style sheet nodes 194 if cursor.node.Attrs.isdeclared("style"): 195 styles = {} 196 for (spec, sel, style) in iterstyles(cursor.node, rules): 197 if cursor.path in sel: 198 for prop in style: 199 # Properties from later rules overwrite those from earlier ones 200 # We're storing the count so that sorting keeps the order 201 styles[prop.name] = (count, prop.cssText) 202 count += 1 203 style = " ".join(f"{value};" for (count, value) in sorted(styles.values())) 204 if style: 205 cursor.node.attrs.style = style 206 207 208### 209### Selector helper functions 210### 211 212def _is_nth_node(iterator, node, index): 213 # Return whether :obj:`node` is the :obj:`index`'th node in :obj:`iterator` (starting at 1) 214 # :obj:`index` is an int or int string or "even" or "odd" 215 if index == "even": 216 for (i, child) in enumerate(iterator): 217 if child is node: 218 return i % 2 == 1 219 return False 220 elif index == "odd": 221 for (i, child) in enumerate(iterator): 222 if child is node: 223 return i % 2 == 0 224 return False 225 else: 226 if not isinstance(index, int): 227 try: 228 index = int(index) 229 except ValueError: 230 raise ValueError(f"illegal argument {index!r}") 231 else: 232 if index < 1: 233 return False 234 try: 235 return iterator[index-1] is node 236 except IndexError: 237 return False 238 239 240def _is_nth_last_node(iterator, node, index): 241 # Return whether :obj:`node` is the :obj:`index`'th last node in :obj:`iterator` 242 # :obj:`index` is an int or int string or "even" or "odd" 243 if index == "even": 244 pos = None 245 for (i, child) in enumerate(iterator): 246 if child is node: 247 pos = i 248 return pos is None or (i-pos) % 2 == 1 249 elif index == "odd": 250 pos = None 251 for (i, child) in enumerate(iterator): 252 if child is node: 253 pos = i 254 return pos is None or (i-pos) % 2 == 0 255 else: 256 if not isinstance(index, int): 257 try: 258 index = int(index) 259 except ValueError: 260 raise ValueError(f"illegal argument {index!r}") 261 else: 262 if index < 1: 263 return False 264 try: 265 return iterator[-index] is node 266 except IndexError: 267 return False 268 269 270def _children_of_type(node, type): 271 for child in node: 272 if isinstance(child, xsc.Element) and child.xmlname == type: 273 yield child 274 275 276### 277### Selectors 278### 279 280class CSSWeightedSelector(xfind.Selector): 281 """ 282 Base class for all CSS pseudo-class selectors. 283 """ 284 285 286class CSSHasAttributeSelector(CSSWeightedSelector): 287 """ 288 A :class:`CSSHasAttributeSelector` selector selects all element nodes 289 that have an attribute with the specified XML name. 290 """ 291 def __init__(self, attributename): 292 self.attributename = attributename 293 294 def __contains__(self, path): 295 if path: 296 node = path[-1] 297 if isinstance(node, xsc.Element): 298 return node.attrs.has(self.attributename) 299 return False 300 301 def __str__(self): 302 return f"{self.__class___.__qualname__}({self.attributename!r})" 303 304 305class CSSAttributeListSelector(CSSWeightedSelector): 306 """ 307 A :class:`CSSAttributeListSelector` selector selects all element nodes 308 where an attribute with the specified XML name has the specified word 309 among the white space-separated list of words in the attribute value. 310 """ 311 def __init__(self, attributename, attributevalue): 312 self.attributename = attributename 313 self.attributevalue = attributevalue 314 315 def __contains__(self, path): 316 if path: 317 node = path[-1] 318 if isinstance(node, xsc.Element): 319 attr = node.attrs.get(self.attributename) 320 return self.attributevalue in str(attr).split() 321 return False 322 323 def __str__(self): 324 return f"{self.__class__.__qualname__}({self.attributename!r}, {self.attributevalue!r})" 325 326 327class CSSAttributeLangSelector(CSSWeightedSelector): 328 """ 329 A :class:`CSSAttributeLangSelector` selector selects all element nodes 330 where an attribute with the specified XML name either is exactly the 331 specified value or starts with the specified value followed by ``"-"``. 332 """ 333 def __init__(self, attributename, attributevalue): 334 self.attributename = attributename 335 self.attributevalue = attributevalue 336 337 def __contains__(self, path): 338 if path: 339 node = path[-1] 340 if isinstance(node, xsc.Element): 341 attr = node.attrs.get(self.attributename) 342 parts = str(attr).split("-", 1) 343 if parts: 344 return parts[0] == self.attributevalue 345 return False 346 347 def __str__(self): 348 return f"{self.__class__.__qualname__}({self.attributename!r}, {self.attributevalue!r})" 349 350 351class CSSFirstChildSelector(CSSWeightedSelector): 352 """ 353 A :class:`CSSFirstChildSelector` selector selects all element nodes 354 that are the first child of its parent. 355 """ 356 def __contains__(self, path): 357 return len(path) >= 2 and _is_nth_node(path[-2][xsc.Element], path[-1], 1) 358 359 def __str__(self): 360 return "CSSFirstChildSelector()" 361 362 363class CSSLastChildSelector(CSSWeightedSelector): 364 """ 365 A :class:`CSSLastChildSelector` selector selects all element nodes 366 that are the last child of its parent. 367 """ 368 def __contains__(self, path): 369 return len(path) >= 2 and _is_nth_last_node(path[-2][xsc.Element], path[-1], 1) 370 371 def __str__(self): 372 return "CSSLastChildSelector()" 373 374 375class CSSFirstOfTypeSelector(CSSWeightedSelector): 376 """ 377 A :class:`CSSLastChildSelector` selector selects all element nodes 378 that are the first of its type among their siblings. 379 """ 380 def __contains__(self, path): 381 if len(path) >= 2: 382 node = path[-1] 383 return isinstance(node, xsc.Element) and _is_nth_node(misc.Iterator(_children_of_type(path[-2], node.xmlname)), node, 1) 384 return False 385 386 def __str__(self): 387 return "CSSFirstOfTypeSelector()" 388 389 390class CSSLastOfTypeSelector(CSSWeightedSelector): 391 """ 392 A :class:`CSSLastChildSelector` selector selects all element nodes 393 that are the last of its type among their siblings. 394 """ 395 def __contains__(self, path): 396 if len(path) >= 2: 397 node = path[-1] 398 return isinstance(node, xsc.Element) and _is_nth_last_node(misc.Iterator(_children_of_type(path[-2], node.xmlname)), node, 1) 399 return False 400 401 def __str__(self): 402 return "CSSLastOfTypeSelector()" 403 404 405class CSSOnlyChildSelector(CSSWeightedSelector): 406 """ 407 A :class:`CSSOnlyChildSelector` selector selects all element nodes that are 408 the only element among its siblings. 409 """ 410 def __contains__(self, path): 411 if len(path) >= 2: 412 node = path[-1] 413 if isinstance(node, xsc.Element): 414 for child in path[-2][xsc.Element]: 415 if child is not node: 416 return False 417 return True 418 return False 419 420 def __str__(self): 421 return "CSSOnlyChildSelector()" 422 423 424class CSSOnlyOfTypeSelector(CSSWeightedSelector): 425 """ 426 A :class:`CSSOnlyOfTypeSelector` selector selects all element nodes that are 427 the only element of its type among its siblings. 428 """ 429 def __contains__(self, path): 430 if len(path) >= 2: 431 node = path[-1] 432 if isinstance(node, xsc.Element): 433 for child in _children_of_type(path[-2], node.xmlname): 434 if child is not node: 435 return False 436 return True 437 return False 438 439 def __str__(self): 440 return "CSSOnlyOfTypeSelector()" 441 442 443class CSSEmptySelector(CSSWeightedSelector): 444 """ 445 A :class:`CSSEmptySelector` selector selects all element nodes that are 446 empty (i.e. they contain no elements or non-whitespace text). 447 """ 448 def __contains__(self, path): 449 if path: 450 node = path[-1] 451 if isinstance(node, xsc.Element): 452 for child in path[-1].content: 453 if isinstance(child, xsc.Element) or (isinstance(child, xsc.Text) and child): 454 return False 455 return True 456 return False 457 458 def __str__(self): 459 return "CSSEmptySelector()" 460 461 462class CSSRootSelector(CSSWeightedSelector): 463 """ 464 A :class:`CSSRootSelector` selector selects the root element. 465 """ 466 def __contains__(self, path): 467 return len(path) == 1 and isinstance(path[-1], xsc.Element) 468 469 def __str__(self): 470 return "CSSRootSelector()" 471 472 473class CSSLinkSelector(CSSWeightedSelector): 474 """ 475 A :class:`CSSLinkSelector` selector selects all HTML links. 476 """ 477 def __contains__(self, path): 478 if path: 479 node = path[-1] 480 return isinstance(node, xsc.Element) and node.xmlns=="http://www.w3.org/1999/xhtml" and node.xmlname=="a" and "href" in node.attrs 481 return False 482 483 def __str__(self): 484 return f"{self.__class__.__qualname__}()" 485 486 487class CSSInvalidPseudoSelector(CSSWeightedSelector): 488 def __contains__(self, path): 489 return False 490 491 def __str__(self): 492 return f"{self.__class__.__qualname__}()" 493 494 495class CSSHoverSelector(CSSInvalidPseudoSelector): 496 pass 497 498 499class CSSActiveSelector(CSSInvalidPseudoSelector): 500 pass 501 502 503class CSSVisitedSelector(CSSInvalidPseudoSelector): 504 pass 505 506 507class CSSFocusSelector(CSSInvalidPseudoSelector): 508 pass 509 510 511class CSSAfterSelector(CSSInvalidPseudoSelector): 512 pass 513 514 515class CSSBeforeSelector(CSSInvalidPseudoSelector): 516 pass 517 518 519class CSSFunctionSelector(CSSWeightedSelector): 520 """ 521 Base class of all CSS selectors that require an argument. 522 """ 523 def __init__(self, value=None): 524 self.value = value 525 526 def __str__(self): 527 return f"{self.__class__.__qualname__}({self.value!r})" 528 529 530class CSSNthChildSelector(CSSFunctionSelector): 531 """ 532 A :class:`CSSNthChildSelector` selector selects all element nodes that are 533 the n-th element among their siblings. 534 """ 535 def __contains__(self, path): 536 if len(path) >= 2: 537 node = path[-1] 538 if isinstance(node, xsc.Element): 539 return _is_nth_node(path[-2][xsc.Element], node, self.value) 540 return False 541 542 543class CSSNthLastChildSelector(CSSFunctionSelector): 544 """ 545 A :class:`CSSNthLastChildSelector` selector selects all element nodes that are 546 the n-th last element among their siblings. 547 """ 548 def __contains__(self, path): 549 if len(path) >= 2: 550 node = path[-1] 551 if isinstance(node, xsc.Element): 552 return _is_nth_last_node(path[-2][xsc.Element], node, self.value) 553 return False 554 555 556class CSSNthOfTypeSelector(CSSFunctionSelector): 557 """ 558 A :class:`CSSNthOfTypeSelector` selector selects all element nodes that are 559 the n-th of its type among their siblings. 560 """ 561 def __contains__(self, path): 562 if len(path) >= 2: 563 node = path[-1] 564 if isinstance(node, xsc.Element): 565 return _is_nth_node(misc.Iterator(_children_of_type(path[-2], node.xmlname)), node, self.value) 566 return False 567 568 569class CSSNthLastOfTypeSelector(CSSFunctionSelector): 570 """ 571 A :class:`CSSNthOfTypeSelector` selector selects all element nodes that are 572 the n-th last of its type among their siblings. 573 """ 574 def __contains__(self, path): 575 if len(path) >= 2: 576 node = path[-1] 577 if isinstance(node, xsc.Element): 578 return _is_nth_last_node(misc.Iterator(_children_of_type(path[-2], node.xmlname)), node, self.value) 579 return False 580 581 582class CSSTypeSelector(xfind.Selector): 583 def __init__(self, type=None, xmlns=None, *selectors): 584 self.type = type 585 self.xmlns = xsc.nsname(xmlns) 586 self.selectors = [] # id, class, attribute etc. selectors for this node 587 588 def __contains__(self, path): 589 if path: 590 node = path[-1] 591 if self.type is None or node.xmlname == self.type: 592 if self.xmlns is None or node.xmlns == self.xmlns: 593 for selector in self.selectors: 594 if path not in selector: 595 return False 596 return True 597 return False 598 599 def __str__(self): 600 v = [self.__class__.__name__, "("] 601 if self.type is not None or self.xmlns is not None or self.selectors: 602 v.append(repr(self.type)) 603 if self.xmlns is not None or self.selectors: 604 v.append(", ") 605 v.append(repr(self.xmlns)) 606 for selector in self.selectors: 607 v.append(", ") 608 v.append(str(selector)) 609 v.append(")") 610 return "".join(v) 611 612 613class CSSAdjacentSiblingCombinator(xfind.BinaryCombinator): 614 """ 615 A :class:`CSSAdjacentSiblingCombinator` works similar to an 616 :class:`~ll.xist.xfind.AdjacentSiblingCombinator` except that only preceding 617 *elements* are considered. 618 """ 619 620 def __contains__(self, path): 621 if len(path) >= 2 and path in self.right: 622 # Find sibling 623 node = path[-1] 624 sibling = None 625 for child in path[-2][xsc.Element]: 626 if child is node: 627 break 628 sibling = child 629 if sibling is not None: 630 return path[:-1]+[sibling] in self.left 631 return False 632 633 def __str__(self): 634 return f"{self.__class__.__qualname__}({self.left}, {self.right})" 635 636 637class CSSGeneralSiblingCombinator(xfind.BinaryCombinator): 638 """ 639 A :class:`CSSGeneralSiblingCombinator` works similar to an 640 :class:`xfind.GeneralSiblingCombinator` except that only preceding *elements* 641 are considered. 642 """ 643 644 def __contains__(self, path): 645 if len(path) >= 2 and path in self.right: 646 node = path[-1] 647 for child in path[-2][xsc.Element]: 648 if child is node: # no previous element siblings 649 return False 650 if path[:-1]+[child] in self.left: 651 return True 652 return False 653 654 def __str__(self): 655 return f"{self.__class__.__qualname__}({self.left}, {self.right})" 656 657 658_pseudoname2class = { 659 "first-child": CSSFirstChildSelector, 660 "last-child": CSSLastChildSelector, 661 "first-of-type": CSSFirstOfTypeSelector, 662 "last-of-type": CSSLastOfTypeSelector, 663 "only-child": CSSOnlyChildSelector, 664 "only-of-type": CSSOnlyOfTypeSelector, 665 "empty": CSSEmptySelector, 666 "root": CSSRootSelector, 667 "hover": CSSHoverSelector, 668 "focus": CSSFocusSelector, 669 "link": CSSLinkSelector, 670 "visited": CSSVisitedSelector, 671 "active": CSSActiveSelector, 672 "after": CSSAfterSelector, 673 "before": CSSBeforeSelector, 674} 675 676_function2class = { 677 "nth-child": CSSNthChildSelector, 678 "nth-last-child": CSSNthLastChildSelector, 679 "nth-of-type": CSSNthOfTypeSelector, 680 "nth-last-of-type": CSSNthLastOfTypeSelector, 681} 682 683 684def selector(selectors, prefixes=None): 685 """ 686 Create a :class:`xfind.Selector` object that matches all nodes that match 687 the specified CSS selector expression. :obj:`selectors` can be a string or a 688 :class:`cssutils.css.selector.Selector` object. :obj:`prefixes` 689 may be a mapping mapping namespace prefixes to namespace names. 690 """ 691 692 if isinstance(selectors, str): 693 if prefixes is not None: 694 prefixes = dict((key, xsc.nsname(value)) for (key, value) in prefixes.items()) 695 namespaces = "\n".join(f"@namespace {key if key is not None else ''} {value!r};" for (key, value) in prefixes.items()) 696 selectors = f"{namespaces}\n{selectors}{{}}" 697 else: 698 selectors = f"{selectors}{{}}" 699 for rule in cssutils.CSSParser().parseString(selectors).cssRules: 700 if isinstance(rule, css.CSSStyleRule): 701 selectors = rule.selectorList.seq 702 break 703 else: 704 raise ValueError("can't happen") 705 elif isinstance(selectors, css.CSSStyleRule): 706 selectors = selectors.selectorList.seq 707 elif isinstance(selectors, css.Selector): 708 selectors = [selectors] 709 else: 710 raise TypeError(f"can't handle {type(selectors)!r}") 711 orcombinators = [] 712 for selector in selectors: 713 rule = root = CSSTypeSelector() 714 attributename = None 715 attributevalue = None 716 combinator = None 717 for item in selector.seq: 718 t = item.type 719 v = item.value 720 if t == "type-selector": 721 rule.xmlns = v[0] if v[0] != -1 else None 722 rule.type = v[1] 723 if t == "universal": 724 rule.xmlns = v[0] if v[0] != -1 else None 725 rule.type = None 726 elif t == "id": 727 rule.selectors.append(xfind.hasid(v.lstrip("#"))) 728 elif t == "class": 729 rule.selectors.append(xfind.hasclass(v.lstrip("."))) 730 elif t == "attribute-start": 731 combinator = None 732 elif t == "attribute-end": 733 if combinator is None: 734 rule.selectors.append(CSSHasAttributeSelector(attributename)) 735 else: 736 rule.selectors.append(combinator(attributename, attributevalue)) 737 elif t == "attribute-selector": 738 attributename = v 739 elif t == "equals": 740 combinator = xfind.attrhasvalue 741 elif t == "includes": 742 combinator = CSSAttributeListSelector 743 elif t == "dashmatch": 744 combinator = CSSAttributeLangSelector 745 elif t == "prefixmatch": 746 combinator = xfind.attrstartswith 747 elif t == "suffixmatch": 748 combinator = xfind.attrendswith 749 elif t == "substringmatch": 750 combinator = xfind.attrcontains 751 elif t == "pseudo-class": 752 if v.endswith("("): 753 try: 754 rule.selectors.append(_function2class[v.lstrip(":").rstrip("(")]()) 755 except KeyError: 756 raise ValueError(f"unknown function {v}") 757 rule.function = v 758 else: 759 try: 760 rule.selectors.append(_pseudoname2class[v.lstrip(":")]()) 761 except KeyError: 762 raise ValueError(f"unknown pseudo-class {v}") 763 elif t == "NUMBER": 764 # can only appear in a function => set the function value 765 rule.selectors[-1].value = v 766 elif t == "STRING": 767 # can only appear in a attribute selector => set the attribute value 768 attributevalue = v 769 elif t == "child": 770 rule = CSSTypeSelector() 771 root = xfind.ChildCombinator(root, rule) 772 elif t == "descendant": 773 rule = CSSTypeSelector() 774 root = xfind.DescendantCombinator(root, rule) 775 elif t == "adjacent-sibling": 776 rule = CSSTypeSelector() 777 root = CSSAdjacentSiblingCombinator(root, rule) 778 elif t == "following-sibling": 779 rule = CSSTypeSelector() 780 root = CSSGeneralSiblingCombinator(root, rule) 781 orcombinators.append(root) 782 return orcombinators[0] if len(orcombinators) == 1 else xfind.OrCombinator(*orcombinators) 783 784 785def parsestring(data, base=None, encoding=None): 786 """ 787 Parse the string :obj:`data` into a :mod:`cssutils` stylesheet. :obj:`base` 788 is the base URL for the parsing process, :obj:`encoding` can be used to force 789 the parser to use the specified encoding. 790 """ 791 if encoding is None: 792 encoding = "css" 793 if base is not None: 794 base = url.URL(base) 795 href = str(base) 796 else: 797 href = None 798 stylesheet = cssutils.parseString(data.decode(encoding), href=href) 799 if base is not None: 800 def prependbase(u): 801 return base/u 802 replaceurls(stylesheet, prependbase) 803 return stylesheet 804 805 806def parsestream(stream, base=None, encoding=None): 807 """ 808 Parse a :mod:`cssutils` stylesheet from the stream :obj:`stream`. :obj:`base` 809 is the base URL for the parsing process, :obj:`encoding` can be used to force 810 the parser to use the specified encoding. 811 """ 812 return parsestring(stream.read(), base=base, encoding=None) 813 814 815def parsefile(filename, base=None, encoding=None): 816 """ 817 Parse a :mod:`cssutils` stylesheet from the file named :obj:`filename`. 818 :obj:`base` is the base URL for the parsing process (defaulting to the 819 filename itself), :obj:`encoding` can be used to force the parser to use the 820 specified encoding. 821 """ 822 filename = os.path.expanduser(filename) 823 if base is None: 824 base = filename 825 with contextlib.closing(open(filename, "rb")) as stream: 826 return parsestream(stream, base=base, encoding=encoding) 827 828 829def parseurl(name, base=None, encoding=None, *args, **kwargs): 830 """ 831 Parse a :mod:`cssutils` stylesheet from the URL :obj:`name`. :obj:`base` is 832 the base URL for the parsing process (defaulting to the final URL of the 833 response, i.e. including redirects), :obj:`encoding` can be used to force 834 the parser to use the specified encoding. :obj:`arg` and :obj:`kwargs` are 835 passed on to :meth:`URL.openread`, so you can pass POST data and request 836 headers. 837 """ 838 with contextlib.closing(url.URL(name).openread(*args, **kwargs)) as stream: 839 if base is None: 840 base = stream.finalurl() 841 return parsestream(stream, base=base, encoding=encoding) 842 843 844def write(stylesheet, stream, base=None, encoding=None): 845 if base is not None: 846 def reltobase(u): 847 return u.relative(base) 848 replaceurls(stylesheet, reltobase) 849 if encoding is not None: 850 stylesheet.encoding = encoding 851 stream.write(stylesheet.cssText) 852