root/livinglogic.python.xist/src/ll/xist/css.py @ 3241:b87e8fa740da

Revision 3241:b87e8fa740da, 22.2 KB (checked in by Walter Doerwald <walter@…>, 12 years ago)

Remove CSSWeight class, methods and tests. Use csstils specificity instead.

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