Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions AUTHORS
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ Matt Haggard <[email protected]>
Matt Hanger <[email protected]>
Mattias Wong <[email protected]>
Maxim Bodyansky <[email protected]>
metagriffin <[email protected]>
Michael Elsdörfer <[email protected]>
Michael Mior <[email protected]>
Michael Su <[email protected]>
Expand Down
21 changes: 21 additions & 0 deletions docs/bundles.rst
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,11 @@ arguments:
.. warning::
Currently, using ``depends`` disables caching for a bundle.

* ``renderer`` - Name of the renderer used to render this bundle's
assets in context. Note that the renderer must be either one of the
webassets-provided renderers (currently ``"css"`` and ``"js"``) or a
renderer registered via ``Environment.register_renderer()`` or
``register_global_renderer()`` (see :doc:`/renderer` for more info).

Nested bundles
--------------
Expand Down Expand Up @@ -155,6 +160,22 @@ which allows you do something like this:
{% assets filters="cssmin,datauri", output="gen/packed.css", "common/jquery.css", "site/base.css", "site/widgets.css" %}
...

You can also delegate contextual rendering of asset references to
webassets (here, using Mako):

.. code-block:: mako

% for asset in my_webassets_env['assets'].renderers():
${asset.render()|n}
% endfor

which will correctly render references to the assets via CSS "<link>"
and JavaScript "<script src=...>" HTML elements. You can also inline
the CSS/JS by passing ``inline=True`` to the render call, which will
output self-contained and properly escaped "<style>" and "<script>"
HTML elements. Details of controlling the rendering can be found in
:doc:`/renderer`.


Management command
~~~~~~~~~~~~~~~~~~
Expand Down
201 changes: 201 additions & 0 deletions docs/renderer.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
.. _renderer:

=================
Rendering Control
=================

Webassets primarily deals with how to generate, compile, filter, and
deploy web assets themselves. However, it can also help with rendering
the asset references, such as the "<link>" and "<script>" tags in
HTML. To leverage that, use the `Bundle` class' ``renderers()``
method, which returns a `BundleRenderer` and the `Environment` class'
``register_renderer()`` method to register custom renderers.


Bundle Rendering
================

Bundles have a method ``.renderers()`` that returns a generator of one
or more BundleRenderers that manage the rendering. The primary method
of a BundleRenderer is the ``.render()`` method, which actually
returns the rendered result.

Renderers are inherited by child bundles from parent bundles if their
renderer is set to ``None``. Note that renderers do not propagate from
child bundles to parent (container) bundles.

Both the ``Bundle.renderers()`` and ``BundleRenderer.render()`` methods
take the following optional parameters:

* `inline`: whether or not to render a reference to the asset as or to
to inline the asset directly. Note that some renderers can only do
one or the other.

* `default`: specify a default renderer that is inherited down the
bundle container stack.

If not renderer is defined or inherited, then the default renderer is
used, which simply renders the asset URL (when referenced) or the
asset contents (when inlined).

For example, to render a CSS link reference or inline stylesheet, you
can do the following:

.. code-block:: python

>>> bundle = Bundle('style.css', output='app.css', renderer='css')

# we know that this bundle will only have one renderer...
>>> renderer = list(bundle.renderers())[0]

>>> print renderer.render()
<link rel="stylesheet" type="text/css" href="/app.css"/>

>>> print renderer.render(inline=True)
<style type="text/css"><!--/*--><![CDATA[/*><!--*/
.redish { color: #f30; }
/*]]>*/--></style>


This is most useful when rendering assets of different types in
templates. For example, with the following environment:

.. code-block:: python

# creating a bundle with all assets

all_assets = Bundle(
Bundle('style.css', output='app.css', renderer='css'),
Bundle('script.js', output='app.js', renderer='js'),
)

env.register('app', all_assets)


Then, in a Mako template:

.. code-block:: mako

<html>
<head>
% for asset in my_webassets_env['app'].renderers():
${asset.render()|n}
% endfor
</head>
...
</html>


Would generate something like the following output:

.. code-block::

<html>
<head>
<link rel="stylesheet" type="text/css" href="/app.css"/>
<script type="text/javascript" src="/app.js"></script>
</head>
...
</html>


Renderer Registration
=====================

Webassets provides default renderers for CSS (named ``"css"``),
JavaScript (named ``"js"``), and LESS CSS (named ``"less"``). If you
need to add more renderers, or change the default rendering, this can
be done via renderer registration.

A renderer is either a string in `str.format syntax
<http://docs.python.org/2/library/string.html#formatstrings>`_,
or a callable that receives the following keyword arguments:

* `type`: the renderer type, i.e. the `name`.
* `bundle`: the Bundle object being rendered.
* `url`: the currently being rendered asset URL.
* `content`: the asset content (for inline renderings only).
* `env`: the environment currently in effect for the rendering.

Finally, renderers can also specify whether or not their content can
be merged with another renderer. For example, a "less" file *can* be
merged with a "css" file if and only if the less is being compiled to
css. If in debug mode and "less_run_in_debug" is falsy, then "less"
and "css" cannot be merged. A "merge_checker" is specified as the
third argument in the registration (or via keyword), and must accept
the following keyword arguments:

* `parent`: the container bundle renderer type (e.g. ``"css"``).
* `child`: the contained bundle renderer type (e.g. ``"less"``).
* `env`: the environment currently in effect for the rendering.

The `merge_checker` can return truthy (the contents can be merged),
falsy (the contents cannot be merged), or None (this renderer does not
know). All affected renderers will be queried regarding mergeability,
until a non-None response is given.

An example custom ``svg`` renderer that can handle SVG being
rasterized either client-side or server-side (note that this assumes
that there is a filter that rasterizes SVGs to PNGs running that is
sensitive to the `debug` and `svg_run_in_debug` flags):

.. code-block:: python

def svg_renderer(type, bundle, url):
return '<img src="{url}"/>'

def svg_inline_renderer(type, bundle, url, content):
from base64 import b64encode
dosvgc = not bundle.env.debug or bundle.env.config.get('svg_run_in_debug')
type = 'image/png' if dosvgc else 'image/svg'
return '<img src="data:{type};base64,{content}"/>'.format(type=type, content=content)

def svg_mergeable(parent, child, env):
# (this is a completely bogus implementation -- see
# :func:`webassets.renderer.less_merge_checker` for a
# good example.)
if parent == 'svg' and child == 'xml-comment':
return True
return None


You can register renderers in particular ``Environment`` objects
(recommended) or you can also register renderers globally (only
recommended in rare situations).

To register the renderer in an environment:

.. code-block:: python

env.register_renderer('svg', svg_renderer, svg_inline_renderer, svg_mergeable)


And to register the renderer globally (usually not recommended):

.. code-block:: python

from webassets.renderer import register_global_renderer
register_global_renderer('svg', svg_renderer, svg_inline_renderer, svg_mergeable)


Note that in the above examples, we registered both a referencing
renderer as well as an inline renderer. If we had specified only the
former, then the inline renderer would default to that one as well.

And here an example of registering a simpler string-based renderer
(but which will always render a reference to the image even when
inlining is requested):

.. code-block:: python

env.register_renderer(

# the name of the renderer:
'svg',

# the "by reference" rendering:
'<img src="{url}"/>'

# an "inline" renderer is not specified, so it will
# default to the above "by reference" renderer
)
51 changes: 51 additions & 0 deletions src/webassets/bundle.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ def __init__(self, *contents, **options):
self.depends = options.pop('depends', [])
self.version = options.pop('version', [])
self.extra = options.pop('extra', {})
self.renderer = options.pop('renderer', None)
if options:
raise TypeError("got unexpected keyword argument '%s'" %
list(options.keys())[0])
Expand Down Expand Up @@ -685,6 +686,56 @@ def urls(self, env=None, *args, **kwargs):
urls.extend(bundle._urls(env, extra_filters, *args, **kwargs))
return urls

def renderers(self, types=None, inline=None, default=None, env=None,
*args, **kwargs):
'''
Returns a generator of renderers for this bundle.

This operates almost identically to :meth:`.urls()`, so this
may return one BundleRenderer (usually in production) or
multiple BundleRenderers (usually in debug mode or when the
bundles uses multiple renderer types).

:Parameters:

* `types` : {str, list(str)}, optional, default: null

If specified, restricts the returned renderers to the
selected set. If the selection is a string-type, it is
converted into a list by splitting at commas (","). Then,
each potential renderer is matched against the set, and if
any match, the renderer is included. If the match starts
with a bang ("!"), it is a negative match, and the entire
set becomes an exclusive set instead of inclusive. It is a
syntax error to mix both positive and negative matches.
Examples:

* ``"less,css"``: selects any "less" or "css" renderers
* ``"!js"``: selects any renderers that are not "js"

* `inline` : bool, optional, default: null

If specified (and not ``None``), this will be used as the
default inlining mode of each renderer. Note, however, that
this can be overridden then on a per-renderer basis.

* `default` : str, optional, default: null

If specified (and not ``None``), this will be used as the
value to the `default` parameter to each `render()` call.
Note, however, that this can be overridden then on a
per-renderer basis.

* `env` : object, optional, default: null

Override the default bundle rendering environment with the
specified `env`.
'''
from .renderer import bundle_renderer_iter
return bundle_renderer_iter(
self, types, inline, default, self._get_env(env),
*args, **kwargs)


def pull_external(env, filename):
"""Helper which will pull ``filename`` into
Expand Down
13 changes: 13 additions & 0 deletions src/webassets/env.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from .version import get_versioner, get_manifest
from .updater import get_updater
from .utils import urlparse
from .renderer import prepare_renderer


__all__ = ('Environment', 'RegisterError')
Expand Down Expand Up @@ -402,6 +403,7 @@ def __init__(self, **config):
BundleRegistry.__init__(self)
self._config = self.config_storage_class(self)
self.resolver = self.resolver_class(self)
self.renderers = dict()

# directory, url currently do not have default values
#
Expand Down Expand Up @@ -695,6 +697,17 @@ def _get_url_mapping(self):
modifying this setting directly.
""")

def register_renderer(self, name, renderer,
inline_renderer=None, merge_checker=None):
'''
Registers renderers to be used only for renderings done
within the context of this environment.

For details, and how to register renderers globally, see
:func:`webassets.renderer.register_global_renderer()`.
'''
self.renderers[name] = prepare_renderer(
name, renderer, inline_renderer, merge_checker)

class DictConfigStorage(ConfigStorage):
"""Using a lower-case dict for configuration values.
Expand Down
4 changes: 3 additions & 1 deletion src/webassets/loaders.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,9 @@ def _get_bundle(self, data):
output=data.get('output', None),
debug=data.get('debug', None),
extra=data.get('extra', {}),
depends=data.get('depends', None))
depends=data.get('depends', None),
renderer=data.get('renderer', None),
)
return Bundle(*list(self._yield_bundle_contents(data)), **kwargs)

def _get_bundles(self, obj, known_bundles=None):
Expand Down
Loading