普通文本  |  479行  |  14.34 KB

# (c) 2005 Ian Bicking and contributors; written for Paste (http://pythonpaste.org)
# Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php

"""
This module implements a class for handling URLs.
"""
from six.moves.urllib.parse import parse_qsl, quote, unquote, urlencode
import cgi
from paste import request
import six

# Imported lazily from FormEncode:
variabledecode = None

__all__ = ["URL", "Image"]

def html_quote(v):
    if v is None:
        return ''
    return cgi.escape(str(v), 1)

def url_quote(v):
    if v is None:
        return ''
    return quote(str(v))

def js_repr(v):
    if v is None:
        return 'null'
    elif v is False:
        return 'false'
    elif v is True:
        return 'true'
    elif isinstance(v, list):
        return '[%s]' % ', '.join(map(js_repr, v))
    elif isinstance(v, dict):
        return '{%s}' % ', '.join(
            ['%s: %s' % (js_repr(key), js_repr(value))
             for key, value in v])
    elif isinstance(v, str):
        return repr(v)
    elif isinstance(v, unicode):
        # @@: how do you do Unicode literals in Javascript?
        return repr(v.encode('UTF-8'))
    elif isinstance(v, (float, int)):
        return repr(v)
    elif isinstance(v, long):
        return repr(v).lstrip('L')
    elif hasattr(v, '__js_repr__'):
        return v.__js_repr__()
    else:
        raise ValueError(
            "I don't know how to turn %r into a Javascript representation"
            % v)

class URLResource(object):

    """
    This is an abstract superclass for different kinds of URLs
    """

    default_params = {}

    def __init__(self, url, vars=None, attrs=None,
                 params=None):
        self.url = url or '/'
        self.vars = vars or []
        self.attrs = attrs or {}
        self.params = self.default_params.copy()
        self.original_params = params or {}
        if params:
            self.params.update(params)

    #@classmethod
    def from_environ(cls, environ, with_query_string=True,
                     with_path_info=True, script_name=None,
                     path_info=None, querystring=None):
        url = request.construct_url(
            environ, with_query_string=False,
            with_path_info=with_path_info, script_name=script_name,
            path_info=path_info)
        if with_query_string:
            if querystring is None:
                vars = request.parse_querystring(environ)
            else:
                vars = parse_qsl(
                    querystring,
                    keep_blank_values=True,
                    strict_parsing=False)
        else:
            vars = None
        v = cls(url, vars=vars)
        return v

    from_environ = classmethod(from_environ)

    def __call__(self, *args, **kw):
        res = self._add_positional(args)
        res = res._add_vars(kw)
        return res

    def __getitem__(self, item):
        if '=' in item:
            name, value = item.split('=', 1)
            return self._add_vars({unquote(name): unquote(value)})
        return self._add_positional((item,))

    def attr(self, **kw):
        for key in kw.keys():
            if key.endswith('_'):
                kw[key[:-1]] = kw[key]
                del kw[key]
        new_attrs = self.attrs.copy()
        new_attrs.update(kw)
        return self.__class__(self.url, vars=self.vars,
                              attrs=new_attrs,
                              params=self.original_params)

    def param(self, **kw):
        new_params = self.original_params.copy()
        new_params.update(kw)
        return self.__class__(self.url, vars=self.vars,
                              attrs=self.attrs,
                              params=new_params)

    def coerce_vars(self, vars):
        global variabledecode
        need_variable_encode = False
        for key, value in vars.items():
            if isinstance(value, dict):
                need_variable_encode = True
            if key.endswith('_'):
                vars[key[:-1]] = vars[key]
                del vars[key]
        if need_variable_encode:
            if variabledecode is None:
                from formencode import variabledecode
            vars = variabledecode.variable_encode(vars)
        return vars


    def var(self, **kw):
        kw = self.coerce_vars(kw)
        new_vars = self.vars + list(kw.items())
        return self.__class__(self.url, vars=new_vars,
                              attrs=self.attrs,
                              params=self.original_params)

    def setvar(self, **kw):
        """
        Like ``.var(...)``, except overwrites keys, where .var simply
        extends the keys.  Setting a variable to None here will
        effectively delete it.
        """
        kw = self.coerce_vars(kw)
        new_vars = []
        for name, values in self.vars:
            if name in kw:
                continue
            new_vars.append((name, values))
        new_vars.extend(kw.items())
        return self.__class__(self.url, vars=new_vars,
                              attrs=self.attrs,
                              params=self.original_params)

    def setvars(self, **kw):
        """
        Creates a copy of this URL, but with all the variables set/reset
        (like .setvar(), except clears past variables at the same time)
        """
        return self.__class__(self.url, vars=kw.items(),
                              attrs=self.attrs,
                              params=self.original_params)

    def addpath(self, *paths):
        u = self
        for path in paths:
            path = str(path).lstrip('/')
            new_url = u.url
            if not new_url.endswith('/'):
                new_url += '/'
            u = u.__class__(new_url+path, vars=u.vars,
                            attrs=u.attrs,
                            params=u.original_params)
        return u

    if six.PY3:
        __truediv__ = addpath
    else:
        __div__ = addpath

    def become(self, OtherClass):
        return OtherClass(self.url, vars=self.vars,
                          attrs=self.attrs,
                          params=self.original_params)

    def href__get(self):
        s = self.url
        if self.vars:
            s += '?'
            vars = []
            for name, val in self.vars:
                if isinstance(val, (list, tuple)):
                    val = [v for v in val if v is not None]
                elif val is None:
                    continue
                vars.append((name, val))
            s += urlencode(vars, True)
        return s

    href = property(href__get)

    def __repr__(self):
        base = '<%s %s' % (self.__class__.__name__,
                           self.href or "''")
        if self.attrs:
            base += ' attrs(%s)' % (
                ' '.join(['%s="%s"' % (html_quote(n), html_quote(v))
                          for n, v in self.attrs.items()]))
        if self.original_params:
            base += ' params(%s)' % (
                ', '.join(['%s=%r' % (n, v)
                           for n, v in self.attrs.items()]))
        return base + '>'

    def html__get(self):
        if not self.params.get('tag'):
            raise ValueError(
                "You cannot get the HTML of %r until you set the "
                "'tag' param'" % self)
        content = self._get_content()
        tag = '<%s' % self.params.get('tag')
        attrs = ' '.join([
            '%s="%s"' % (html_quote(n), html_quote(v))
            for n, v in self._html_attrs()])
        if attrs:
            tag += ' ' + attrs
        tag += self._html_extra()
        if content is None:
            return tag + ' />'
        else:
            return '%s>%s</%s>' % (tag, content, self.params.get('tag'))

    html = property(html__get)

    def _html_attrs(self):
        return self.attrs.items()

    def _html_extra(self):
        return ''

    def _get_content(self):
        """
        Return the content for a tag (for self.html); return None
        for an empty tag (like ``<img />``)
        """
        raise NotImplementedError

    def _add_vars(self, vars):
        raise NotImplementedError

    def _add_positional(self, args):
        raise NotImplementedError

class URL(URLResource):

    r"""
    >>> u = URL('http://localhost')
    >>> u
    <URL http://localhost>
    >>> u = u['view']
    >>> str(u)
    'http://localhost/view'
    >>> u['//foo'].param(content='view').html
    '<a href="http://localhost/view/foo">view</a>'
    >>> u.param(confirm='Really?', content='goto').html
    '<a href="http://localhost/view" onclick="return confirm(\'Really?\')">goto</a>'
    >>> u(title='See "it"', content='goto').html
    '<a href="http://localhost/view?title=See+%22it%22">goto</a>'
    >>> u('another', var='fuggetaboutit', content='goto').html
    '<a href="http://localhost/view/another?var=fuggetaboutit">goto</a>'
    >>> u.attr(content='goto').html
    Traceback (most recent call last):
        ....
    ValueError: You must give a content param to <URL http://localhost/view attrs(content="goto")> generate anchor tags
    >>> str(u['foo=bar%20stuff'])
    'http://localhost/view?foo=bar+stuff'
    """

    default_params = {'tag': 'a'}

    def __str__(self):
        return self.href

    def _get_content(self):
        if not self.params.get('content'):
            raise ValueError(
                "You must give a content param to %r generate anchor tags"
                % self)
        return self.params['content']

    def _add_vars(self, vars):
        url = self
        for name in ('confirm', 'content'):
            if name in vars:
                url = url.param(**{name: vars.pop(name)})
        if 'target' in vars:
            url = url.attr(target=vars.pop('target'))
        return url.var(**vars)

    def _add_positional(self, args):
        return self.addpath(*args)

    def _html_attrs(self):
        attrs = list(self.attrs.items())
        attrs.insert(0, ('href', self.href))
        if self.params.get('confirm'):
            attrs.append(('onclick', 'return confirm(%s)'
                          % js_repr(self.params['confirm'])))
        return attrs

    def onclick_goto__get(self):
        return 'location.href=%s; return false' % js_repr(self.href)

    onclick_goto = property(onclick_goto__get)

    def button__get(self):
        return self.become(Button)

    button = property(button__get)

    def js_popup__get(self):
        return self.become(JSPopup)

    js_popup = property(js_popup__get)

class Image(URLResource):

    r"""
    >>> i = Image('/images')
    >>> i = i / '/foo.png'
    >>> i.html
    '<img src="/images/foo.png" />'
    >>> str(i['alt=foo'])
    '<img src="/images/foo.png" alt="foo" />'
    >>> i.href
    '/images/foo.png'
    """

    default_params = {'tag': 'img'}

    def __str__(self):
        return self.html

    def _get_content(self):
        return None

    def _add_vars(self, vars):
        return self.attr(**vars)

    def _add_positional(self, args):
        return self.addpath(*args)

    def _html_attrs(self):
        attrs = list(self.attrs.items())
        attrs.insert(0, ('src', self.href))
        return attrs

class Button(URLResource):

    r"""
    >>> u = URL('/')
    >>> u = u / 'delete'
    >>> b = u.button['confirm=Sure?'](id=5, content='del')
    >>> str(b)
    '<button onclick="if (confirm(\'Sure?\')) {location.href=\'/delete?id=5\'}; return false">del</button>'
    """

    default_params = {'tag': 'button'}

    def __str__(self):
        return self.html

    def _get_content(self):
        if self.params.get('content'):
            return self.params['content']
        if self.attrs.get('value'):
            return self.attrs['content']
        # @@: Error?
        return None

    def _add_vars(self, vars):
        button = self
        if 'confirm' in vars:
            button = button.param(confirm=vars.pop('confirm'))
        if 'content' in vars:
            button = button.param(content=vars.pop('content'))
        return button.var(**vars)

    def _add_positional(self, args):
        return self.addpath(*args)

    def _html_attrs(self):
        attrs = list(self.attrs.items())
        onclick = 'location.href=%s' % js_repr(self.href)
        if self.params.get('confirm'):
            onclick = 'if (confirm(%s)) {%s}' % (
                js_repr(self.params['confirm']), onclick)
        onclick += '; return false'
        attrs.insert(0, ('onclick', onclick))
        return attrs

class JSPopup(URLResource):

    r"""
    >>> u = URL('/')
    >>> u = u / 'view'
    >>> j = u.js_popup(content='view')
    >>> j.html
    '<a href="/view" onclick="window.open(\'/view\', \'_blank\'); return false" target="_blank">view</a>'
    """

    default_params = {'tag': 'a', 'target': '_blank'}

    def _add_vars(self, vars):
        button = self
        for var in ('width', 'height', 'stripped', 'content'):
            if var in vars:
                button = button.param(**{var: vars.pop(var)})
        return button.var(**vars)

    def _window_args(self):
        p = self.params
        features = []
        if p.get('stripped'):
            p['location'] = p['status'] = p['toolbar'] = '0'
        for param in 'channelmode directories fullscreen location menubar resizable scrollbars status titlebar'.split():
            if param not in p:
                continue
            v = p[param]
            if v not in ('yes', 'no', '1', '0'):
                if v:
                    v = '1'
                else:
                    v = '0'
            features.append('%s=%s' % (param, v))
        for param in 'height left top width':
            if not p.get(param):
                continue
            features.append('%s=%s' % (param, p[param]))
        args = [self.href, p['target']]
        if features:
            args.append(','.join(features))
        return ', '.join(map(js_repr, args))

    def _html_attrs(self):
        attrs = list(self.attrs.items())
        onclick = ('window.open(%s); return false'
                   % self._window_args())
        attrs.insert(0, ('target', self.params['target']))
        attrs.insert(0, ('onclick', onclick))
        attrs.insert(0, ('href', self.href))
        return attrs

    def _get_content(self):
        if not self.params.get('content'):
            raise ValueError(
                "You must give a content param to %r generate anchor tags"
                % self)
        return self.params['content']

    def _add_positional(self, args):
        return self.addpath(*args)

if __name__ == '__main__':
    import doctest
    doctest.testmod()