root/livinglogic.python.xist/src/ll/xist/css.py @ 3192:ac7e04cf9edc

Revision 3192:ac7e04cf9edc, 20.2 KB (checked in by Walter Doerwald <walter@…>, 12 years ago)

The href of a stylesheet link wasn't applied to the stylesheet itself (only to the imported stylesheets in it).

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