miracle2k / django-assets

Django webassets integration.
BSD 2-Clause "Simplified" License
89 stars 79 forks source link

Add convenience templatetags to include css or js #36

Closed ustun closed 10 years ago

ustun commented 10 years ago

Instead of

{% assets filters="jsmin", output="gen/packed.js", "common/jquery.js", "site/base.js", "site/widgets.js" %}
    <script type="text/javascript" src="{{ ASSET_URL }}"></script>
{% endassets %}

it would be better if we could just do

{% assetsjs filters="jsmin", output="gen/packed.js", "common/jquery.js", "site/base.js", "site/widgets.js" %}

It could even detect that it is a js file, so we wouldn't have to have different asset tags like assetscss etc.

Here is a simple implementation that could be improved:

class JsCssAssetsNode(AssetsNode):

    def render(self, context):
        bundle = self.resolve(context)
        result = u""
        for url in bundle.urls(env=get_env()):
            result += self.template.format(ASSET_URL=url)
        return result

class JsAssetsNode(JsCssAssetsNode):
    template = '<script type="text/javascript" src="{ASSET_URL}"></script>'

class CssAssetsNode(JsCssAssetsNode):
    template = '<link rel="stylesheet" type="text/css" href="{ASSET_URL}" media="all" />'

def assets(parser, token, type=""):
    filters = None
    output = None
    debug = None
    files = []

    # parse the arguments
    args = token.split_contents()[1:]
    for arg in args:
        # Handle separating comma; for backwards-compatibility
        # reasons, this is currently optional, but is enforced by
        # the Jinja extension already.
        if arg[-1] == ',':
            arg = arg[:-1]
            if not arg:
                continue

        # determine if keyword or positional argument
        arg = arg.split('=', 1)
        if len(arg) == 1:
            name = None
            value = arg[0]
        else:
            name, value = arg

        # handle known keyword arguments
        if name == 'output':
            output = value
        elif name == 'debug':
            debug = value
        elif name == 'filters':
            filters = value
        elif name == 'filter':
            filters = value
            warnings.warn('The "filter" option of the {% assets %} '
                          'template tag has been renamed to '
                          '"filters" for consistency reasons.',
                            ImminentDeprecationWarning)
        # positional arguments are source files
        elif name is None:
            files.append(value)
        else:
            raise template.TemplateSyntaxError('Unsupported keyword argument "%s"'%name)

    # capture until closing tag
    childnodes = parser.parse(("endassets" + type,))
    parser.delete_first_token()
    if type == "":
        return AssetsNode(filters, output, debug, files, childnodes)
    elif type == "js":
        return JsAssetsNode(filters, output, debug, files, childnodes)
    elif type == "css":
        return CssAssetsNode(filters, output, debug, files, childnodes)

# expose the default Django tag
register.tag('assets', assets)
register.tag('assetsjs', functools.partial(assets, type='js'))
register.tag('assetscss', functools.partial(assets, type='css'))
ustun commented 10 years ago

Here is a bit improved one, since we don't need the closing tags now:

class AssetsNode(template.Node):

    # For testing, to inject a mock bundle
    BundleClass = Bundle

    def __init__(self, filters, output, debug, files, childnodes=None):
        self.childnodes = childnodes
        self.output = output
        self.files = files
        self.filters = filters
        self.debug = debug

    def resolve(self, context={}):
        """We allow variables to be used for all arguments; this function
        resolves all data against a given context;

        This is a separate method as the management command must have
        the ability to check if the tag can be resolved without a context.
        """
        def resolve_var(x):
            if x is None:
                return None
            else:
                try:
                    return template.Variable(x).resolve(context)
                except template.VariableDoesNotExist, e:
                    # Django seems to hide those; we don't want to expose
                    # them either, I guess.
                    raise
        def resolve_bundle(name):
            # If a bundle with that name exists, use it. Otherwise,
            # assume a filename is meant.
            try:
                return get_env()[name]
            except KeyError:
                return name

        return self.BundleClass(
            *[resolve_bundle(resolve_var(f)) for f in self.files],
            **{'output': resolve_var(self.output),
            'filters': resolve_var(self.filters),
            'debug': parse_debug_value(resolve_var(self.debug))})

    def render(self, context):
        bundle = self.resolve(context)

        result = u""
        for url in bundle.urls(env=get_env()):
            context.update({'ASSET_URL': url})
            try:
                result += self.childnodes.render(context)
            finally:
                context.pop()
        return result

class JsCssAssetsNode(AssetsNode):

    def render(self, context):
        bundle = self.resolve(context)
        result = u""
        for url in bundle.urls(env=get_env()):
            result += self.template.format(ASSET_URL=url)
        return result

class JsAssetsNode(JsCssAssetsNode):
    template = '<script type="text/javascript" src="{ASSET_URL}"></script>'

class CssAssetsNode(JsCssAssetsNode):
    template = '<link rel="stylesheet" type="text/css" href="{ASSET_URL}" media="all" />'

def assets(parser, token, type=""):
    filters = None
    output = None
    debug = None
    files = []

    # parse the arguments
    args = token.split_contents()[1:]
    for arg in args:
        # Handle separating comma; for backwards-compatibility
        # reasons, this is currently optional, but is enforced by
        # the Jinja extension already.
        if arg[-1] == ',':
            arg = arg[:-1]
            if not arg:
                continue

        # determine if keyword or positional argument
        arg = arg.split('=', 1)
        if len(arg) == 1:
            name = None
            value = arg[0]
        else:
            name, value = arg

        # handle known keyword arguments
        if name == 'output':
            output = value
        elif name == 'debug':
            debug = value
        elif name == 'filters':
            filters = value
        elif name == 'filter':
            filters = value
            warnings.warn('The "filter" option of the {% assets %} '
                          'template tag has been renamed to '
                          '"filters" for consistency reasons.',
                            ImminentDeprecationWarning)
        # positional arguments are source files
        elif name is None:
            files.append(value)
        else:
            raise template.TemplateSyntaxError('Unsupported keyword argument "%s"'%name)

    if type == "":
        # capture until closing tag
        childnodes = parser.parse(("endassets" + type,))
        parser.delete_first_token()
        return AssetsNode(filters, output, debug, files, childnodes)
    elif type == "js":
        return JsAssetsNode(filters, output, debug, files)
    elif type == "css":
        return CssAssetsNode(filters, output, debug, files)

# If Coffin is installed, expose the Jinja2 extension
try:
    from coffin.template import Library as CoffinLibrary
except ImportError:
    register = template.Library()
else:
    register = CoffinLibrary()
    from webassets.ext.jinja2 import AssetsExtension
    from django_assets.env import get_env
    register.tag(AssetsExtension, environment={'assets_environment': get_env()})

# expose the default Django tag
register.tag('assets', assets)
register.tag('assetsjs', functools.partial(assets, type='js'))
register.tag('assetscss', functools.partial(assets, type='css'))