Commit 935094ee by Steven Cummings

Merge remote-tracking branch 'upstream/master'

parents ee0facf5 e1229053
...@@ -12,23 +12,32 @@ or just made Pipeline more awesome. ...@@ -12,23 +12,32 @@ or just made Pipeline more awesome.
* Ara Anjargolian <ara818@gmail.com> * Ara Anjargolian <ara818@gmail.com>
* Balazs Kossovics <balazs.kossovics@e-loue.com> * Balazs Kossovics <balazs.kossovics@e-loue.com>
* Ben Vinegar <ben@benv.ca> * Ben Vinegar <ben@benv.ca>
* Brant Young <brant.young@gmail.com>
* Bryan Chow <bryan@fullfactor.com> * Bryan Chow <bryan@fullfactor.com>
* Casey Greene <csgreene@princeton.edu> * Casey Greene <csgreene@princeton.edu>
* Chris Reeves <hello@chris.reeves.io>
* Christian Hammond <chipx86@chipx86.com> * Christian Hammond <chipx86@chipx86.com>
* David Charbonnier <d.charbonnier@oxys.net> * David Charbonnier <d.charbonnier@oxys.net>
* David Cramer <dcramer@gmail.com> * David Cramer <dcramer@gmail.com>
* Denis V Seleznyov <code@xy2.ru> * Denis V Seleznyov <code@xy2.ru>
* Florent Messa <florent.messa@gmail.com>
* Idan Zalzberg <idanzalz@gmail.com>
* Kyle MacFarlane <kyle@deletethetrees.com> * Kyle MacFarlane <kyle@deletethetrees.com>
* Luke Yu-Po Chen <nuemail@gmail.com> * Luke Yu-Po Chen <nuemail@gmail.com>
* Matt Dennewitz <mattdennewitz@gmail.com> * Matt Dennewitz <mattdennewitz@gmail.com>
* Max Klymyshyn <klymyshyn@gmail.com> * Max Klymyshyn <klymyshyn@gmail.com>
* Melvin Laplanche <melvin.laplanche+dev@gmail.com>
* Michael Weibel <michael.weibel@gmail.com>
* Patrick Altman <paltman@gmail.com> * Patrick Altman <paltman@gmail.com>
* Peter Baumgartner <pete@lincolnloop.com> * Peter Baumgartner <pete@lincolnloop.com>
* Pierre Drescher <pierre.drescher@gmail.com>
* Remco Wendt <remco@maykinmedia.nl> * Remco Wendt <remco@maykinmedia.nl>
* Sam Thomson <sammthomson@gmail.com> * Sam Thomson <sammthomson@gmail.com>
* Sander Smits <jhmsmits@gmail.com> * Sander Smits <jhmsmits@gmail.com>
* Sander Smits <jsmits@imac.lan>
* Sirex <sirexas@gmail.com> * Sirex <sirexas@gmail.com>
* Steven Cummings <estebistec@gmail.com> * Steven Cummings <estebistec@gmail.com>
* Teo Klestrup Röijezon <teo@nullable.se>
* Timothée Peignier <timothee.peignier@tryphon.org> * Timothée Peignier <timothee.peignier@tryphon.org>
* Trey Smith <trey.smith@nasa.gov> * Trey Smith <trey.smith@nasa.gov>
* Victor Shnayder <victor@mitx.mit.edu>
* Zenobius Jiricek <zenobius.jiricek@gmail.com>
Contribute
==========
#. Check for open issues or open a fresh issue to start a discussion around a
feature idea or a bug. There is a **contribute!** tag for issues that should be
ideal for people who are not very familiar with the codebase yet.
#. Fork the repository on Github to start making your changes on a topic branch.
#. Write a test which shows that the bug was fixed or that the feature works as expected.
#. Send a pull request and bug the maintainer until it gets merged and published.
Make sure to add yourself to *AUTHORS*.
Otherwise, if you simply wants to suggest a feature or report a bug, create an issue :
https://github.com/cyberdelia/django-pipeline/issues
...@@ -3,6 +3,76 @@ ...@@ -3,6 +3,76 @@
History History
======= =======
1.1.20
------
* Ensure yui-compressor can still use YUICompressor.
1.2.19
------
* **BACKWARD INCOMPATIBLE** : Replace python cssmin compressor to run the command (works for python or node implementation)
1.2.18
------
* **BACKWARD INCOMPATIBLE** : Replace yui-compressor by yuglify, check your configuration.
* Use finders in manifest. Thanks to Sjoerd Arendsen.
1.2.17
------
* Fully tested windows compatibility. Thanks to Idan Zalzberg.
1.2.16
------
* Fix manifesto module. Thanks to Zenobius Jiricek.
* Ensure coffee-script compiler don't try to overwrite file. Thanks to Teo Klestrup Röijezon.
1.2.15
------
* Ensure asset url are build with ``posixpath``.
* Deal with storage prefix properly.
1.2.14
------
* Jinja2 support, thanks to Christopher Reeves.
* Add read/save_file method to CompilerBase.
1.2.13
------
* Fix unicode bug in compressor. Thanks to Victor Shnayder.
* Fix outdated detection bug. Thanks to Victor Shnayder and Erwan Ameil.
* Add slimit compressor. Thanks to Brant Young.
1.2.12
------
* Fix IO error when creating new compiled file. Thanks to Melvin Laplanche.
1.2.11
------
* Add a small contribution guide
* Add mimetype settings for sass and scss
* Change compiler interface to let compiler determine if file is outdated
1.2.10
------
* Use ``/usr/bin/env`` by default to find compiler executable. Thanks to Michael Weibel.
* Allow to change embed settings : max size and directory. Thanks to Pierre Drescher.
* Some documentation improvements. Thanks to Florent Messa.
1.2.9
-----
* Don't compile non-outdated files.
* Add non-packing storage.
1.2.8 1.2.8
----- -----
......
Pipeline Pipeline
======== ========
Pipeline is an asset packaging library for Django, providing both CSS and JavaScript concatenation and compression, built-in JavaScript template support, and optional data-URI image and font embedding. Pipeline is an asset packaging library for Django, providing both CSS and
JavaScript concatenation and compression, built-in JavaScript template support,
and optional data-URI image and font embedding.
To install it : :: Installation
------------
To install it, simply: ::
pip install django-pipeline pip install django-pipeline
Documentation
-------------
For documentation, usage, and examples, see : For documentation, usage, and examples, see :
http://django-pipeline.readthedocs.org http://django-pipeline.readthedocs.org
To suggest a feature or report a bug :
https://github.com/cyberdelia/django-pipeline/issues
...@@ -23,7 +23,7 @@ To use it add this to your ``PIPELINE_COMPILERS`` :: ...@@ -23,7 +23,7 @@ To use it add this to your ``PIPELINE_COMPILERS`` ::
Command line to execute for coffee program. Command line to execute for coffee program.
You will most likely change this to the location of coffee on your system. You will most likely change this to the location of coffee on your system.
Defaults to ``'/usr/local/bin/coffee'``. Defaults to ``'/usr/bin/env coffee'``.
``PIPELINE_COFFEE_SCRIPT_ARGUMENTS`` ``PIPELINE_COFFEE_SCRIPT_ARGUMENTS``
------------------------------------ ------------------------------------
...@@ -50,7 +50,7 @@ To use it add this to your ``PIPELINE_COMPILERS`` :: ...@@ -50,7 +50,7 @@ To use it add this to your ``PIPELINE_COMPILERS`` ::
Command line to execute for lessc program. Command line to execute for lessc program.
You will most likely change this to the location of lessc on your system. You will most likely change this to the location of lessc on your system.
Defaults to ``'/usr/local/bin/lessc'``. Defaults to ``'/usr/bin/env lessc'``.
``PIPELINE_LESS_ARGUMENTS`` ``PIPELINE_LESS_ARGUMENTS``
--------------------------- ---------------------------
...@@ -78,7 +78,7 @@ To use it add this to your ``PIPELINE_COMPILERS`` :: ...@@ -78,7 +78,7 @@ To use it add this to your ``PIPELINE_COMPILERS`` ::
Command line to execute for sass program. Command line to execute for sass program.
You will most likely change this to the location of sass on your system. You will most likely change this to the location of sass on your system.
Defaults to ``'/usr/local/bin/sass'``. Defaults to ``'/usr/bin/env sass'``.
``PIPELINE_SASS_ARGUMENTS`` ``PIPELINE_SASS_ARGUMENTS``
--------------------------- ---------------------------
...@@ -107,7 +107,7 @@ To use it add this to your ``PIPELINE_COMPILERS`` :: ...@@ -107,7 +107,7 @@ To use it add this to your ``PIPELINE_COMPILERS`` ::
Command line to execute for stylus program. Command line to execute for stylus program.
You will most likely change this to the location of stylus on your system. You will most likely change this to the location of stylus on your system.
Defaults to ``'/usr/local/bin/stylus'``. Defaults to ``'/usr/bin/env stylus'``.
``PIPELINE_STYLUS_ARGUMENTS`` ``PIPELINE_STYLUS_ARGUMENTS``
----------------------------- -----------------------------
...@@ -142,6 +142,8 @@ A custom compiler for an imaginary compiler called jam :: ...@@ -142,6 +142,8 @@ A custom compiler for an imaginary compiler called jam ::
def match_file(self, filename): def match_file(self, filename):
return filename.endswith('.jam') return filename.endswith('.jam')
def compile_file(self, content, path): def compile_file(self, infile, outfile, outdated=False, force=False):
return jam.compile(content) if not outdated and not force:
return # No need to recompiled file
return jam.compile(infile, outfile)
...@@ -5,10 +5,10 @@ Compressors ...@@ -5,10 +5,10 @@ Compressors
=========== ===========
YUI Compressor compressor YUI compressor
========================= =========================
The YUI compressor uses `yui-compressor <http://developer.yahoo.com/yui/compressor/>`_ The YUI compressor uses `yuglify <http://github.com/yui/yuglify>`_
for compressing javascript and stylesheets. for compressing javascript and stylesheets.
To use it for your stylesheets add this to your ``PIPELINE_CSS_COMPRESSOR`` :: To use it for your stylesheets add this to your ``PIPELINE_CSS_COMPRESSOR`` ::
...@@ -26,11 +26,7 @@ To use it for your javascripts add this to your ``PIPELINE_JS_COMPRESSOR`` :: ...@@ -26,11 +26,7 @@ To use it for your javascripts add this to your ``PIPELINE_JS_COMPRESSOR`` ::
Command line to execute for the YUI program. Command line to execute for the YUI program.
You will most likely change this to the location of yui-compressor on your system. You will most likely change this to the location of yui-compressor on your system.
Defaults to ``'/usr/local/bin/yuicompressor'``. Defaults to ``'/usr/bin/env yuglify'``.
.. warning::
Don't point to ``yuicompressor.jar`` directly, we expect to find a executable script.
``PIPELINE_YUI_CSS_ARGUMENTS`` ``PIPELINE_YUI_CSS_ARGUMENTS``
------------------------------ ------------------------------
...@@ -66,7 +62,7 @@ To use it add this to your ``PIPELINE_JS_COMPRESSOR`` :: ...@@ -66,7 +62,7 @@ To use it add this to your ``PIPELINE_JS_COMPRESSOR`` ::
Command line to execute for the Closure Compiler program. Command line to execute for the Closure Compiler program.
You will most likely change this to the location of closure on your system. You will most likely change this to the location of closure on your system.
Default to ``'/usr/local/bin/closure'`` Default to ``'/usr/bin/env closure'``
.. warning:: .. warning::
Don't point to ``compiler.jar`` directly, we expect to find a executable script. Don't point to ``compiler.jar`` directly, we expect to find a executable script.
...@@ -97,7 +93,7 @@ To use it add this to your ``PIPELINE_JS_COMPRESSOR`` :: ...@@ -97,7 +93,7 @@ To use it add this to your ``PIPELINE_JS_COMPRESSOR`` ::
Command line to execute for the Closure Compiler program. Command line to execute for the Closure Compiler program.
You will most likely change this to the location of closure on your system. You will most likely change this to the location of closure on your system.
Defaults to ``'/usr/local/bin/uglifyjs'``. Defaults to ``'/usr/bin/env uglifyjs'``.
``PIPELINE_UGLIFYJS_ARGUMENTS`` ``PIPELINE_UGLIFYJS_ARGUMENTS``
------------------------------- -------------------------------
...@@ -122,6 +118,21 @@ Install the jsmin library with your favorite Python package manager :: ...@@ -122,6 +118,21 @@ Install the jsmin library with your favorite Python package manager ::
pip install jsmin pip install jsmin
SlimIt compressor
=================
The slimit compressor uses `SlimIt <http://slimit.org/>`_ to
compress javascripts.
To use it add this to your ``PIPELINE_JS_COMPRESSOR`` ::
PIPELINE_JS_COMPRESSOR = 'pipeline.compressors.slimit.SlimItCompressor'
Install the slimit library with your favorite Python package manager ::
pip install slimit
CSSTidy compressor CSSTidy compressor
================== ==================
...@@ -138,7 +149,7 @@ To us it for your stylesheets add this to your ``PIPELINE_CSS_COMPRESSOR`` :: ...@@ -138,7 +149,7 @@ To us it for your stylesheets add this to your ``PIPELINE_CSS_COMPRESSOR`` ::
Command line to execute for csstidy program. Command line to execute for csstidy program.
You will most likely change this to the location of csstidy on your system. You will most likely change this to the location of csstidy on your system.
Defaults to ``'/usr/local/bin/csstidy'`` Defaults to ``'/usr/bin/env csstidy'``
``PIPELINE_CSSTIDY_ARGUMENTS`` ``PIPELINE_CSSTIDY_ARGUMENTS``
------------------------------ ------------------------------
...@@ -147,18 +158,29 @@ To us it for your stylesheets add this to your ``PIPELINE_CSS_COMPRESSOR`` :: ...@@ -147,18 +158,29 @@ To us it for your stylesheets add this to your ``PIPELINE_CSS_COMPRESSOR`` ::
Default to ``'--template=highest'`` Default to ``'--template=highest'``
cssmin compressor CSSMin compressor
================= =================
The cssmin compressor uses the `cssmin <http://pypi.python.org/pypi/cssmin/>`_ The cssmin compressor uses the `cssmin <https://github.com/jbleuzen/node-cssmin>`_
Python library to compress stylesheets. To use it, specify this command to compress stylesheets. To use it, add this to your ``PIPELINE_CSS_COMPRESSOR`` ::
``PIPELINE_CSS_COMPRESSOR`` ::
PIPELINE_CSS_COMPRESSOR = 'pipeline.compressors.cssmin.CSSMinCompressor'
``PIPELINE_CSSMIN_BINARY``
---------------------------
Command line to execute for cssmin program.
You will most likely change this to the location of cssmin on your system.
Defaults to ``'/usr/bin/env cssmin'``
PIPELINE_CSS_COMPRESSOR = 'pipeline.compressors.cssmin.CssminCompressor' ``PIPELINE_CSSMIN_ARGUMENTS``
------------------------------
Install the cssmin library with your favorite Python package manager. E.g. :: Additional arguments to use when cssmin is called.
Default to ``''``
pip install cssmin
Write your own compressor class Write your own compressor class
=============================== ===============================
......
...@@ -51,7 +51,7 @@ copyright = u'2011-2012, Timothée Peignier' ...@@ -51,7 +51,7 @@ copyright = u'2011-2012, Timothée Peignier'
# The short X.Y version. # The short X.Y version.
version = '1.2' version = '1.2'
# The full version, including alpha/beta/rc tags. # The full version, including alpha/beta/rc tags.
release = '1.2.9' release = '1.2.21'
# The language for content autogenerated by Sphinx. Refer to documentation # The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages. # for a list of supported languages.
......
...@@ -188,6 +188,26 @@ Images and fonts are embedded following these rules : ...@@ -188,6 +188,26 @@ Images and fonts are embedded following these rules :
at all in Internet Explorer 8. at all in Internet Explorer 8.
- If asset path contains a directory named "**embed**". - If asset path contains a directory named "**embed**".
Overriding embedding settings
-----------------------------
You can override these rules using the following settings:
``PIPELINE_EMBED_MAX_IMAGE_SIZE``
.................................
Setting that controls the maximum image size (in bytes) to embed in CSS using Data-URIs.
Internet Explorer 8 has issues with assets under 32 kilobytes.
Defaults to ``32700``
``PIPELINE_EMBED_PATH``
.......................
Setting the directory that an asset needs to be in so that it is embedded
Defaults to ``r'[/]?embed/'``
Rewriting CSS urls Rewriting CSS urls
================== ==================
......
...@@ -27,6 +27,15 @@ And if you want versioning use :: ...@@ -27,6 +27,15 @@ And if you want versioning use ::
STATICFILES_STORAGE = 'pipeline.storage.PipelineCachedStorage' STATICFILES_STORAGE = 'pipeline.storage.PipelineCachedStorage'
There is also non-packing storage available, that allows you to run ``collectstatic`` command
without packaging your assets. Useful for production when you don't want to run compressor or compilers ::
STATICFILES_STORAGE = 'pipeline.storage.NonPackagingPipelineStorage'
Also available if you want versioning ::
STATICFILES_STORAGE = 'pipeline.storage.NonPackagingPipelineCachedStorage'
Pipeline is also providing a storage that play nicely with staticfiles app Pipeline is also providing a storage that play nicely with staticfiles app
particularly for development : :: particularly for development : ::
......
...@@ -13,7 +13,7 @@ To use your javascript templates, just add them to your ``PIPELINE_JS`` group :: ...@@ -13,7 +13,7 @@ To use your javascript templates, just add them to your ``PIPELINE_JS`` group ::
'js/application.js', 'js/application.js',
'js/templates/**/*.jst', 'js/templates/**/*.jst',
), ),
'output_filename': 'js/application.r?.js' 'output_filename': 'js/application.js'
} }
} }
......
...@@ -33,7 +33,6 @@ with the name “scripts”, you would use the following code to output them all ...@@ -33,7 +33,6 @@ with the name “scripts”, you would use the following code to output them all
{% compressed_css 'colors' %} {% compressed_css 'colors' %}
{% compressed_js 'stats' %} {% compressed_js 'stats' %}
Collect static Collect static
============== ==============
...@@ -66,3 +65,42 @@ Pipeline provide a way to add your javascripts and stylesheets files to a ...@@ -66,3 +65,42 @@ Pipeline provide a way to add your javascripts and stylesheets files to a
cache-manifest via `Manifesto <http://manifesto.readthedocs.org/>`_. cache-manifest via `Manifesto <http://manifesto.readthedocs.org/>`_.
To do so, you just need to add manifesto app to your ``INSTALLED_APPS``. To do so, you just need to add manifesto app to your ``INSTALLED_APPS``.
Jinja2
======
Pipeline also includes Jinja2 support and is used almost identically to the Django
Template tags implementation.
.. note::
You have to expose the Jinja2 functions provided by pipeline to the Jinja2
environment yourself, Pipeline will not do this for you. There are several implementations
of Jinja2 for Django, like ``django-ninja`` or ``coffin``.
See the vendor documentation for examples on how to expose functions to the Jinja2 environment
and pick a solution that best suites your use case.
For more information on Jinja2 see the documentation at http://jinja.pocoo.org/docs/.
Functions
---------
The functions to expose to the Jinja2 environment are: ::
pipeline.jinja2.ext.compressed_css
pipeline.jinja2.ext.compressed_js
Example
-------
To use in the templates: ::
{{ compressed_css('group_name') }}
{{ compressed_js('group_name') }}
Templates
---------
Unlike the Django template tag implementation the Jinja2 implementation uses different templates, so if you
wish to override them please override ``pipeline/css.jinja`` and ``pipeline/js.jinja``.
...@@ -6,30 +6,33 @@ Sites using Pipeline ...@@ -6,30 +6,33 @@ Sites using Pipeline
The following sites are a partial list of people using Pipeline. The following sites are a partial list of people using Pipeline.
Are you using pipeline and not being in this list ? Drop us a line. Are you using pipeline and not being in this list? Drop us a line.
20 Minutes 20 Minutes
---------- ----------
For their internal tools : http://www.20minutes.fr For their internal tools: http://www.20minutes.fr
Pitchfork
---------
For their main website : http://pitchfork.com
The Molly Project The Molly Project
----------------- -----------------
Molly is a framework for the rapid development of information and service Molly is a framework for the rapid development of information and service
portals targeted at mobile internet devices : http://mollyproject.org portals targeted at mobile internet devices: http://mollyproject.org
It powers the University of Oxford's mobile portal : http://m.ox.ac.uk/ It powers the University of Oxford's mobile portal: http://m.ox.ac.uk/
Croisé dans le Métro Croisé dans le Métro
-------------------- --------------------
For their main and mobile website : For their main and mobile website:
* http://www.croisedanslemetro.com * http://www.croisedanslemetro.com
* http://m.croisedanslemetro.com * http://m.croisedanslemetro.com
Ulule
-----
For their main and forum website:
* http://www.ulule.com
* http://vox.ulule.com
...@@ -24,20 +24,23 @@ class Compiler(object): ...@@ -24,20 +24,23 @@ class Compiler(object):
compilers = property(compilers) compilers = property(compilers)
def compile(self, paths, force=False): def compile(self, paths, force=False):
for index, path in enumerate(paths): for index, input_path in enumerate(paths):
for compiler in self.compilers: for compiler in self.compilers:
compiler = compiler(self.verbose) compiler = compiler(verbose=self.verbose, storage=self.storage)
if compiler.match_file(path): if compiler.match_file(input_path):
new_path = self.output_path(path, compiler.output_extension) output_path = self.output_path(input_path, compiler.output_extension)
paths[index] = new_path paths[index] = output_path
if not force and not self.is_outdated(path, new_path):
continue
try: try:
content = self.read_file(path) infile = finders.find(input_path)
compiled_content = compiler.compile_file(content, finders.find(path)) outfile = finders.find(output_path)
self.save_file(new_path, compiled_content) if outfile is None:
outfile = self.output_path(infile, compiler.output_extension)
outdated = True
else:
outdated = self.is_outdated(input_path, output_path)
compiler.compile_file(infile, outfile, outdated=outdated, force=force)
except CompilerError: except CompilerError:
if not self.storage.exists(new_path) or not settings.PIPELINE: if not self.storage.exists(output_path) or not settings.PIPELINE:
raise raise
return paths return paths
...@@ -45,32 +48,33 @@ class Compiler(object): ...@@ -45,32 +48,33 @@ class Compiler(object):
path = os.path.splitext(path) path = os.path.splitext(path)
return '.'.join((path[0], extension)) return '.'.join((path[0], extension))
def read_file(self, path): def is_outdated(self, infile, outfile):
file = self.storage.open(path, 'rb')
content = file.read()
file.close()
return content
def is_outdated(self, path, new_path):
try: try:
return self.storage.modified_time(path) > self.storage.modified_time(new_path) return self.storage.modified_time(infile) > self.storage.modified_time(outfile)
except (OSError, NotImplementedError): except (OSError, NotImplementedError):
return True return True
def save_file(self, path, content):
return self.storage.save(path, ContentFile(smart_str(content)))
class CompilerBase(object): class CompilerBase(object):
def __init__(self, verbose): def __init__(self, verbose, storage):
self.verbose = verbose self.verbose = verbose
self.storage = storage
def match_file(self, filename): def match_file(self, filename):
raise NotImplementedError raise NotImplementedError
def compile_file(self, content, path): def compile_file(self, infile, outfile, outdated=False, force=False):
raise NotImplementedError raise NotImplementedError
def save_file(self, path, content):
return self.storage.save(path, ContentFile(smart_str(content)))
def read_file(self, path):
file = self.storage.open(path, 'rb')
content = file.read()
file.close()
return content
class CompilerError(Exception): class CompilerError(Exception):
pass pass
......
...@@ -8,9 +8,13 @@ class CoffeeScriptCompiler(SubProcessCompiler): ...@@ -8,9 +8,13 @@ class CoffeeScriptCompiler(SubProcessCompiler):
def match_file(self, path): def match_file(self, path):
return path.endswith('.coffee') return path.endswith('.coffee')
def compile_file(self, content, path): def compile_file(self, infile, outfile, outdated=False, force=False):
command = "%s -sc %s" % ( if not outdated and not force:
return # File doesn't need to be recompiled
command = "%s -cp %s %s > %s" % (
settings.PIPELINE_COFFEE_SCRIPT_BINARY, settings.PIPELINE_COFFEE_SCRIPT_BINARY,
settings.PIPELINE_COFFEE_SCRIPT_ARGUMENTS settings.PIPELINE_COFFEE_SCRIPT_ARGUMENTS,
infile,
outfile
) )
return self.execute_command(command, content) return self.execute_command(command)
import os.path from os.path import dirname
from pipeline.conf import settings from pipeline.conf import settings
from pipeline.compilers import SubProcessCompiler from pipeline.compilers import SubProcessCompiler
...@@ -10,12 +10,11 @@ class LessCompiler(SubProcessCompiler): ...@@ -10,12 +10,11 @@ class LessCompiler(SubProcessCompiler):
def match_file(self, filename): def match_file(self, filename):
return filename.endswith('.less') return filename.endswith('.less')
def compile_file(self, content, path): def compile_file(self, infile, outfile, outdated=False, force=False):
command = '%s %s %s' % ( command = "%s %s %s %s" % (
settings.PIPELINE_LESS_BINARY, settings.PIPELINE_LESS_BINARY,
settings.PIPELINE_LESS_ARGUMENTS, settings.PIPELINE_LESS_ARGUMENTS,
path infile,
outfile
) )
cwd = os.path.dirname(path) return self.execute_command(command, cwd=dirname(infile))
content = self.execute_command(command, cwd=cwd)
return content
import os.path from os.path import dirname
from pipeline.conf import settings from pipeline.conf import settings
from pipeline.compilers import SubProcessCompiler from pipeline.compilers import SubProcessCompiler
...@@ -10,11 +10,11 @@ class SASSCompiler(SubProcessCompiler): ...@@ -10,11 +10,11 @@ class SASSCompiler(SubProcessCompiler):
def match_file(self, filename): def match_file(self, filename):
return filename.endswith(('.scss', '.sass')) return filename.endswith(('.scss', '.sass'))
def compile_file(self, content, path): def compile_file(self, infile, outfile, outdated=False, force=False):
command = "%s --scss %s %s" % ( command = "%s %s --update %s:%s" % (
settings.PIPELINE_SASS_BINARY, settings.PIPELINE_SASS_BINARY,
settings.PIPELINE_SASS_ARGUMENTS, settings.PIPELINE_SASS_ARGUMENTS,
path infile,
outfile
) )
cwd = os.path.dirname(path) return self.execute_command(command, cwd=dirname(infile))
return self.execute_command(command, cwd=cwd)
import os.path from os.path import dirname
from pipeline.conf import settings from pipeline.conf import settings
from pipeline.compilers import SubProcessCompiler from pipeline.compilers import SubProcessCompiler
...@@ -10,10 +10,11 @@ class StylusCompiler(SubProcessCompiler): ...@@ -10,10 +10,11 @@ class StylusCompiler(SubProcessCompiler):
def match_file(self, filename): def match_file(self, filename):
return filename.endswith('.styl') return filename.endswith('.styl')
def compile_file(self, content, path): def compile_file(self, infile, outfile, outdated=False, force=False):
command = "%s %s" % ( command = "%s %s < %s > %s" % (
settings.PIPELINE_STYLUS_BINARY, settings.PIPELINE_STYLUS_BINARY,
settings.PIPELINE_STYLUS_ARGUMENTS, settings.PIPELINE_STYLUS_ARGUMENTS,
infile,
outfile
) )
cwd = os.path.dirname(path) return self.execute_command(command, cwd=dirname(infile))
return self.execute_command(command, content, cwd=cwd)
import base64 import base64
import os import os
import posixpath
import re import re
import subprocess import subprocess
from itertools import takewhile from itertools import takewhile
from django.utils.encoding import smart_str from django.utils.encoding import smart_str, force_unicode
try: try:
from staticfiles import finders from staticfiles import finders
...@@ -13,12 +14,9 @@ except ImportError: ...@@ -13,12 +14,9 @@ except ImportError:
from django.contrib.staticfiles import finders # noqa from django.contrib.staticfiles import finders # noqa
from pipeline.conf import settings from pipeline.conf import settings
from pipeline.utils import to_class, relpath
from pipeline.storage import default_storage from pipeline.storage import default_storage
from pipeline.utils import to_class, relpath
MAX_IMAGE_SIZE = 32700
EMBEDDABLE = r'[/]?embed/'
URL_DETECTOR = r'url\([\'"]?([^\s)]+\.[a-z]+[\?\#\d\w]*)[\'"]?\)' URL_DETECTOR = r'url\([\'"]?([^\s)]+\.[a-z]+[\?\#\d\w]*)[\'"]?\)'
URL_REPLACER = r'url\(__EMBED__(.+?)(\?\d+)?\)' URL_REPLACER = r'url\(__EMBED__(.+?)(\?\d+)?\)'
...@@ -136,7 +134,8 @@ class Compressor(object): ...@@ -136,7 +134,8 @@ class Compressor(object):
output_filename, variant) output_filename, variant)
return "url(%s)" % asset_url return "url(%s)" % asset_url
content = self.read_file(path) content = self.read_file(path)
content = re.sub(URL_DETECTOR, reconstruct, smart_str(content)) # content needs to be unicode to avoid explosions with non-ascii chars
content = re.sub(URL_DETECTOR, reconstruct, force_unicode(content))
stylesheets.append(content) stylesheets.append(content)
return '\n'.join(stylesheets) return '\n'.join(stylesheets)
...@@ -146,10 +145,10 @@ class Compressor(object): ...@@ -146,10 +145,10 @@ class Compressor(object):
def construct_asset_path(self, asset_path, css_path, output_filename, variant=None): def construct_asset_path(self, asset_path, css_path, output_filename, variant=None):
"""Return a rewritten asset URL for a stylesheet""" """Return a rewritten asset URL for a stylesheet"""
public_path = self.absolute_path(asset_path, os.path.dirname(css_path)) public_path = self.absolute_path(asset_path, os.path.dirname(css_path).replace('\\', '/'))
if self.embeddable(public_path, variant): if self.embeddable(public_path, variant):
return "__EMBED__%s" % public_path return "__EMBED__%s" % public_path
if not os.path.isabs(asset_path): if not posixpath.isabs(asset_path):
asset_path = self.relative_path(public_path, output_filename) asset_path = self.relative_path(public_path, output_filename)
return asset_path return asset_path
...@@ -159,11 +158,11 @@ class Compressor(object): ...@@ -159,11 +158,11 @@ class Compressor(object):
font = ext in FONT_EXTS font = ext in FONT_EXTS
if not variant: if not variant:
return False return False
if not (re.search(EMBEDDABLE, path) and self.storage.exists(path)): if not (re.search(settings.PIPELINE_EMBED_PATH, path.replace('\\', '/')) and self.storage.exists(path)):
return False return False
if not ext in EMBED_EXTS: if not ext in EMBED_EXTS:
return False return False
if not (font or len(self.encoded_content(path)) < MAX_IMAGE_SIZE): if not (font or len(self.encoded_content(path)) < settings.PIPELINE_EMBED_MAX_IMAGE_SIZE):
return False return False
return True return True
...@@ -193,16 +192,16 @@ class Compressor(object): ...@@ -193,16 +192,16 @@ class Compressor(object):
Return the absolute public path for an asset, Return the absolute public path for an asset,
given the path of the stylesheet that contains it. given the path of the stylesheet that contains it.
""" """
if os.path.isabs(path): if posixpath.isabs(path):
path = os.path.join(default_storage.location, path) path = posixpath.join(default_storage.location, path)
else: else:
path = os.path.join(start, path) path = posixpath.join(start, path)
return os.path.normpath(path) return posixpath.normpath(path)
def relative_path(self, absolute_path, output_filename): def relative_path(self, absolute_path, output_filename):
"""Rewrite paths relative to the output stylesheet path""" """Rewrite paths relative to the output stylesheet path"""
absolute_path = os.path.join(settings.PIPELINE_ROOT, absolute_path) absolute_path = posixpath.join(settings.PIPELINE_ROOT, absolute_path)
output_path = os.path.join(settings.PIPELINE_ROOT, os.path.dirname(output_filename)) output_path = posixpath.join(settings.PIPELINE_ROOT, posixpath.dirname(output_filename))
return relpath(absolute_path, output_path) return relpath(absolute_path, output_path)
def read_file(self, path): def read_file(self, path):
......
from __future__ import absolute_import from pipeline.conf import settings
from pipeline.compressors import SubProcessCompressor
from pipeline.compressors import CompressorBase
class CSSMinCompressor(SubProcessCompressor):
class CssminCompressor(CompressorBase):
"""
CSS compressor based on the Python library cssmin
(http://pypi.python.org/pypi/cssmin/).
"""
def compress_css(self, css): def compress_css(self, css):
from cssmin import cssmin command = "%s %s" % (settings.PIPELINE_CSSMIN_BINARY, settings.PIPELINE_CSSMIN_ARGUMENTS)
return cssmin(css) return self.execute_command(command, css)
from __future__ import absolute_import
from pipeline.compressors import CompressorBase
class SlimItCompressor(CompressorBase):
"""
JS compressor based on the Python library slimit
(http://pypi.python.org/pypi/slimit/).
"""
def compress_js(self, js):
from slimit import minify
return minify(js)
...@@ -3,8 +3,8 @@ from pipeline.compressors import SubProcessCompressor ...@@ -3,8 +3,8 @@ from pipeline.compressors import SubProcessCompressor
class YUICompressor(SubProcessCompressor): class YUICompressor(SubProcessCompressor):
def compress_common(self, content, type_, arguments): def compress_common(self, content, compress_type, arguments):
command = '%s --type=%s %s' % (settings.PIPELINE_YUI_BINARY, type_, arguments) command = '%s --type=%s %s' % (settings.PIPELINE_YUI_BINARY, compress_type, arguments)
return self.execute_command(command, content) return self.execute_command(command, content)
def compress_js(self, js): def compress_js(self, js):
......
from django.conf import settings from django.conf import settings
PIPELINE = getattr(settings, 'PIPELINE', not settings.DEBUG) PIPELINE = getattr(settings, 'PIPELINE', not settings.DEBUG)
PIPELINE_ROOT = getattr(settings, 'PIPELINE_ROOT', settings.STATIC_ROOT) PIPELINE_ROOT = getattr(settings, 'PIPELINE_ROOT', settings.STATIC_ROOT)
PIPELINE_URL = getattr(settings, 'PIPELINE_URL', settings.STATIC_URL) PIPELINE_URL = getattr(settings, 'PIPELINE_URL', settings.STATIC_URL)
...@@ -25,36 +24,44 @@ PIPELINE_TEMPLATE_FUNC = getattr(settings, 'PIPELINE_TEMPLATE_FUNC', "template") ...@@ -25,36 +24,44 @@ PIPELINE_TEMPLATE_FUNC = getattr(settings, 'PIPELINE_TEMPLATE_FUNC', "template")
PIPELINE_DISABLE_WRAPPER = getattr(settings, 'PIPELINE_DISABLE_WRAPPER', False) PIPELINE_DISABLE_WRAPPER = getattr(settings, 'PIPELINE_DISABLE_WRAPPER', False)
PIPELINE_CSSTIDY_BINARY = getattr(settings, 'PIPELINE_CSSTIDY_BINARY', '/usr/local/bin/csstidy') PIPELINE_CSSTIDY_BINARY = getattr(settings, 'PIPELINE_CSSTIDY_BINARY', '/usr/bin/env csstidy')
PIPELINE_CSSTIDY_ARGUMENTS = getattr(settings, 'PIPELINE_CSSTIDY_ARGUMENTS', '--template=highest') PIPELINE_CSSTIDY_ARGUMENTS = getattr(settings, 'PIPELINE_CSSTIDY_ARGUMENTS', '--template=highest')
PIPELINE_YUI_BINARY = getattr(settings, 'PIPELINE_YUI_BINARY', '/usr/local/bin/yuicompressor') PIPELINE_YUI_BINARY = getattr(settings, 'PIPELINE_YUI_BINARY', '/usr/bin/env yuglify')
PIPELINE_YUI_CSS_ARGUMENTS = getattr(settings, 'PIPELINE_YUI_CSS_ARGUMENTS', '') PIPELINE_YUI_CSS_ARGUMENTS = getattr(settings, 'PIPELINE_YUI_CSS_ARGUMENTS', '--terminal')
PIPELINE_YUI_JS_ARGUMENTS = getattr(settings, 'PIPELINE_YUI_JS_ARGUMENTS', '') PIPELINE_YUI_JS_ARGUMENTS = getattr(settings, 'PIPELINE_YUI_JS_ARGUMENTS', '--terminal')
PIPELINE_CLOSURE_BINARY = getattr(settings, 'PIPELINE_CLOSURE_BINARY', '/usr/local/bin/closure') PIPELINE_CLOSURE_BINARY = getattr(settings, 'PIPELINE_CLOSURE_BINARY', '/usr/bin/env closure')
PIPELINE_CLOSURE_ARGUMENTS = getattr(settings, 'PIPELINE_CLOSURE_ARGUMENTS', '') PIPELINE_CLOSURE_ARGUMENTS = getattr(settings, 'PIPELINE_CLOSURE_ARGUMENTS', '')
PIPELINE_UGLIFYJS_BINARY = getattr(settings, 'PIPELINE_UGLIFYJS_BINARY', '/usr/local/bin/uglifyjs') PIPELINE_UGLIFYJS_BINARY = getattr(settings, 'PIPELINE_UGLIFYJS_BINARY', '/usr/bin/env uglifyjs')
PIPELINE_UGLIFYJS_ARGUMENTS = getattr(settings, 'PIPELINE_UGLIFYJS_ARGUMENTS', '') PIPELINE_UGLIFYJS_ARGUMENTS = getattr(settings, 'PIPELINE_UGLIFYJS_ARGUMENTS', '')
PIPELINE_COFFEE_SCRIPT_BINARY = getattr(settings, 'PIPELINE_COFFEE_SCRIPT_BINARY', '/usr/local/bin/coffee') PIPELINE_CSSMIN_BINARY = getattr(settings, 'PIPELINE_CSSMIN_BINARY', '/usr/bin/env cssmin')
PIPELINE_CSSMIN_ARGUMENTS = getattr(settings, 'PIPELINE_CSSMIN_ARGUMENTS', '')
PIPELINE_COFFEE_SCRIPT_BINARY = getattr(settings, 'PIPELINE_COFFEE_SCRIPT_BINARY', '/usr/bin/env coffee')
PIPELINE_COFFEE_SCRIPT_ARGUMENTS = getattr(settings, 'PIPELINE_COFFEE_SCRIPT_ARGUMENTS', '') PIPELINE_COFFEE_SCRIPT_ARGUMENTS = getattr(settings, 'PIPELINE_COFFEE_SCRIPT_ARGUMENTS', '')
PIPELINE_SASS_BINARY = getattr(settings, 'PIPELINE_SASS_BINARY', '/usr/local/bin/sass') PIPELINE_SASS_BINARY = getattr(settings, 'PIPELINE_SASS_BINARY', '/usr/bin/env sass')
PIPELINE_SASS_ARGUMENTS = getattr(settings, 'PIPELINE_SASS_ARGUMENTS', '') PIPELINE_SASS_ARGUMENTS = getattr(settings, 'PIPELINE_SASS_ARGUMENTS', '')
PIPELINE_STYLUS_BINARY = getattr(settings, 'PIPELINE_STYLUS_BINARY', '/usr/local/bin/stylus') PIPELINE_STYLUS_BINARY = getattr(settings, 'PIPELINE_STYLUS_BINARY', '/usr/bin/env stylus')
PIPELINE_STYLUS_ARGUMENTS = getattr(settings, 'PIPELINE_STYLUS_ARGUMENTS', '') PIPELINE_STYLUS_ARGUMENTS = getattr(settings, 'PIPELINE_STYLUS_ARGUMENTS', '')
PIPELINE_LESS_BINARY = getattr(settings, 'PIPELINE_LESS_BINARY', '/usr/local/bin/lessc') PIPELINE_LESS_BINARY = getattr(settings, 'PIPELINE_LESS_BINARY', '/usr/bin/env lessc')
PIPELINE_LESS_ARGUMENTS = getattr(settings, 'PIPELINE_LESS_ARGUMENTS', '') PIPELINE_LESS_ARGUMENTS = getattr(settings, 'PIPELINE_LESS_ARGUMENTS', '')
PIPELINE_MIMETYPES = getattr(settings, 'PIPELINE_MIMETYPES', ( PIPELINE_MIMETYPES = getattr(settings, 'PIPELINE_MIMETYPES', (
('text/coffeescript', '.coffee'), ('text/coffeescript', '.coffee'),
('text/less', '.less'), ('text/less', '.less'),
('text/javascript', '.js') ('text/javascript', '.js'),
('text/x-sass', '.sass'),
('text/x-scss', '.scss')
)) ))
PIPELINE_EMBED_MAX_IMAGE_SIZE = getattr(settings, 'PIPELINE_EMBED_MAX_IMAGE_SIZE', 32700)
PIPELINE_EMBED_PATH = getattr(settings, 'PIPELINE_EMBED_PATH', r'[/]?embed/')
if PIPELINE_COMPILERS is None: if PIPELINE_COMPILERS is None:
PIPELINE_COMPILERS = [] PIPELINE_COMPILERS = []
import inspect
try:
from staticfiles.storage import staticfiles_storage
except ImportError:
from django.contrib.staticfiles.storage import staticfiles_storage # noqa
from django.conf import settings as django_settings
from jinja2 import Environment, FileSystemLoader
from pipeline.conf import settings as pipeline_settings
from pipeline.packager import Packager, PackageNotFound
from pipeline.utils import guess_type
class Jinja2Compressed(object):
def __init__(self, package_type):
from django.template.loaders import app_directories
if package_type not in ['css', 'js']:
raise PackageNotFound("Package type must be css or js, supplied %s" % package_type)
self.package_type = package_type
self.loader = FileSystemLoader((app_directories.app_template_dirs +
django_settings.TEMPLATE_DIRS))
self.get_pipeline_settings()
def get_pipeline_settings(self):
"""
Because extra Jinja2 functions have to be declared
at creation time the new functions have to be declared before
django settings evaluation so when pipeline tries to import django
settings it will get the default globals rather than user defined
settings. This function attempts to fudge back in user defined
settings into pipeline settings as django.conf.settings is lazy
loaded and pipeline settings are not.
No harm intended :)
I guess a better more robust solution would be to make pipeline
settings lazy loaded also.
"""
members = inspect.getmembers(pipeline_settings)
for setting, val in members:
if setting.startswith('PIPELINE'):
if hasattr(django_settings, setting):
val = getattr(django_settings, setting)
else:
if type(getattr(pipeline_settings, setting)) == str:
val = "'%s'" % val
val = val if val else "''"
expr = "pipeline_settings.%s = %s" % (
setting, val)
exec expr
pipeline_settings.PIPELINE = getattr(django_settings,
'PIPELINE', not django_settings.DEBUG)
self.settings = pipeline_settings
def get_package(self, name):
"""Get the js or css package."""
package = {
'js': self.settings.PIPELINE_JS.get(name, {}),
'css': self.settings.PIPELINE_CSS.get(name, {}),
}[self.package_type]
if package:
package = {name: package}
self.packager = {
'js': Packager(css_packages={}, js_packages=package),
'css': Packager(css_packages=package, js_packages={}),
}[self.package_type]
try:
self.package = self.packager.package_for(self.package_type, name)
except PackageNotFound:
self.package = None
def render(self, path):
"""Render the HTML tag."""
if not self.package.template_name:
template_name = {
'js': 'pipeline/js.jinja',
'css': 'pipeline/css.jinja',
}[self.package_type]
else:
template_name = self.package.template_name
mimetype = {
'js': 'text/javascript',
'css': 'text/css',
}[self.package_type]
context = self.package.extra_context
context.update({
'type': guess_type(path, mimetype),
'url': staticfiles_storage.url(path)
})
env = Environment(loader=self.loader)
tpl = env.get_template(template_name)
return tpl.render(**context)
def html(self, name):
"""Render the HTML Snippet"""
self.get_package(name)
if self.package:
if self.settings.PIPELINE:
return self.render(self.package.output_filename)
else:
paths = self.packager.compile(self.package.paths)
templates = self.packager.pack_templates(self.package)
return {
'css': self.render_individual_css(paths),
'js': self.render_individual_js(paths, templates)
}[self.package_type]
else:
return '' # don't return anything if no package found
def render_individual_css(self, paths):
"""Render individual CSS files"""
tags = [self.render(path) for path in paths]
return '\n'.join(tags)
def render_individual_js(self, paths, templates=None):
"""Render individual JS files"""
tags = [self.render(path) for path in paths]
if templates:
tags.append(self.render_inline_js(self.package, templates))
return '\n'.join(tags)
def render_inline_js(self, package, js):
template_name = (self.package.template_name or
"pipeline/inline_js.jinja")
context = self.package.extra_context
context.update({
'source': js
})
env = Environment(loader=self.loader)
tpl = env.get_template(template_name)
return tpl.render(**context)
def compressed_css(package_name):
compress = Jinja2Compressed('css')
return compress.html(package_name)
def compressed_js(package_name):
compress = Jinja2Compressed('js')
return compress.html(package_name)
import os
try: try:
from staticfiles.finders import DefaultStorageFinder from staticfiles.finders import get_finders
except ImportError: except ImportError:
from django.contrib.staticfiles.storage import DefaultStorageFinder # noqa from django.contrib.staticfiles.finders import get_finders # noqa
from django.conf import settings from pipeline.conf import settings
from manifesto import Manifest from manifesto import Manifest
...@@ -14,7 +16,8 @@ class PipelineManifest(Manifest): ...@@ -14,7 +16,8 @@ class PipelineManifest(Manifest):
def __init__(self): def __init__(self):
self.packager = Packager() self.packager = Packager()
self.packages = self.collect_packages() self.packages = self.collect_packages()
self.finder = DefaultStorageFinder() self.finders = get_finders()
self.package_files = []
def collect_packages(self): def collect_packages(self):
packages = [] packages = []
...@@ -29,12 +32,27 @@ class PipelineManifest(Manifest): ...@@ -29,12 +32,27 @@ class PipelineManifest(Manifest):
return packages return packages
def cache(self): def cache(self):
ignore_patterns = getattr(settings, "STATICFILES_IGNORE_PATTERNS", None)
if settings.PIPELINE: if settings.PIPELINE:
for package in self.packages: for package in self.packages:
self.package_files.append(package.output_filename)
yield str(self.packager.individual_url(package.output_filename)) yield str(self.packager.individual_url(package.output_filename))
else: else:
for package in self.packages: for package in self.packages:
for path in self.packager.compile(package.paths): for path in self.packager.compile(package.paths):
self.package_files.append(path)
yield str(self.packager.individual_url(path)) yield str(self.packager.individual_url(path))
for path in self.finder.list():
yield str(self.packager.individual_url(path)) for finder in self.finders:
for path, storage in finder.list(ignore_patterns):
# Prefix the relative path if the source storage contains it
if getattr(storage, 'prefix', None):
prefixed_path = os.path.join(storage.prefix, path)
else:
prefixed_path = path
# Dont add any doubles
if prefixed_path not in self.package_files:
self.package_files.append(prefixed_path)
yield str(self.packager.individual_url(prefixed_path))
...@@ -88,7 +88,7 @@ class BaseFinderStorage(PipelineStorage): ...@@ -88,7 +88,7 @@ class BaseFinderStorage(PipelineStorage):
return path return path
def exists(self, name): def exists(self, name):
exists = self.finders.find(name) != None exists = self.finders.find(name) is not None
if not exists: if not exists:
return super(BaseFinderStorage, self).exists(name) return super(BaseFinderStorage, self).exists(name)
return exists return exists
...@@ -101,21 +101,31 @@ class BaseFinderStorage(PipelineStorage): ...@@ -101,21 +101,31 @@ class BaseFinderStorage(PipelineStorage):
except OSError: except OSError:
pass pass
def match_location(self, name, path, prefix=None):
if prefix:
prefix = "%s%s" % (prefix, os.sep)
name = name[len(prefix):]
if path == name:
return name
if os.path.splitext(path)[0] == os.path.splitext(name)[0]:
return name
return None
def find_storage(self, name): def find_storage(self, name):
for finder in finders.get_finders(): for finder in finders.get_finders():
for path, storage in finder.list([]): for path, storage in finder.list([]):
if path == name: prefix = getattr(storage, 'prefix', None)
return storage matched_path = self.match_location(name, path, prefix)
if os.path.splitext(path)[0] == os.path.splitext(name)[0]: if matched_path:
return storage return matched_path, storage
raise ValueError("The file '%s' could not be found with %r." % (name, self)) raise ValueError("The file '%s' could not be found with %r." % (name, self))
def _open(self, name, mode="rb"): def _open(self, name, mode="rb"):
storage = self.find_storage(name) name, storage = self.find_storage(name)
return storage._open(name, mode) return storage._open(name, mode)
def _save(self, name, content): def _save(self, name, content):
storage = self.find_storage(name) name, storage = self.find_storage(name)
# Ensure we overwrite file, since we have no control on external storage # Ensure we overwrite file, since we have no control on external storage
if storage.exists(name): if storage.exists(name):
storage.delete(name) storage.delete(name)
......
<link href="{{ url }}" rel="stylesheet" type="{{ type }}"{% if media %} media="{{ media }}"{% endif %}{% if charset %} charset="{{ charset }}"{% endif %} />
<script {% if async %}async{% endif %} {% if defer %}defer{% endif %} type="text/javascript" charset="utf-8">
{{ source|safe }}
</script>
<script {% if async %}async{% endif %} {% if defer %}defer{% endif %} type="{{ type }}" src="{{ url }}" charset="utf-8"></script>
import mimetypes import mimetypes
import os import posixpath
import sys
import urllib import urllib
from django.utils import importlib from django.utils import importlib
...@@ -34,52 +33,18 @@ def guess_type(path, default=None): ...@@ -34,52 +33,18 @@ def guess_type(path, default=None):
return mimetype return mimetype
def _relpath_nt(path, start=os.path.curdir): def relpath(path, start=posixpath.curdir):
"""Return a relative version of a path"""
if not path:
raise ValueError("no path specified")
start_list = os.path.abspath(start).split(os.path.sep)
path_list = os.path.abspath(path).split(os.path.sep)
if start_list[0].lower() != path_list[0].lower():
unc_path, rest = os.path.splitunc(path)
unc_start, rest = os.path.splitunc(start)
if bool(unc_path) ^ bool(unc_start):
raise ValueError("Cannot mix UNC and non-UNC paths (%s and %s)"
% (path, start))
else:
raise ValueError("path is on drive %s, start on drive %s"
% (path_list[0], start_list[0]))
# Work out how much of the filepath is shared by start and path.
for i in range(min(len(start_list), len(path_list))):
if start_list[i].lower() != path_list[i].lower():
break
else:
i += 1
rel_list = [os.path.pardir] * (len(start_list) - i) + path_list[i:]
if not rel_list:
return os.path.curdir
return os.path.join(*rel_list)
def _relpath_posix(path, start=os.path.curdir):
"""Return a relative version of a path""" """Return a relative version of a path"""
if not path: if not path:
raise ValueError("no path specified") raise ValueError("no path specified")
start_list = os.path.abspath(start).split(os.path.sep) start_list = posixpath.abspath(start).split(posixpath.sep)
path_list = os.path.abspath(path).split(os.path.sep) path_list = posixpath.abspath(path).split(posixpath.sep)
# Work out how much of the filepath is shared by start and path. # Work out how much of the filepath is shared by start and path.
i = len(os.path.commonprefix([start_list, path_list])) i = len(posixpath.commonprefix([start_list, path_list]))
rel_list = [os.path.pardir] * (len(start_list) - i) + path_list[i:] rel_list = [posixpath.pardir] * (len(start_list) - i) + path_list[i:]
if not rel_list: if not rel_list:
return os.path.curdir return posixpath.curdir
return os.path.join(*rel_list) return posixpath.join(*rel_list)
if os.path is sys.modules.get('ntpath'):
relpath = _relpath_nt
else:
relpath = _relpath_posix
tox
flake8
\ No newline at end of file
...@@ -4,14 +4,14 @@ from setuptools import setup, find_packages ...@@ -4,14 +4,14 @@ from setuptools import setup, find_packages
setup( setup(
name='django-pipeline', name='django-pipeline',
version='1.2.9', version='1.2.21',
description='Pipeline is an asset packaging library for Django.', description='Pipeline is an asset packaging library for Django.',
long_description=open('README.rst').read() + '\n\n' + long_description=open('README.rst').read() + '\n\n' +
open('HISTORY.rst').read(), open('HISTORY.rst').read(),
author='Timothée Peignier', author='Timothée Peignier',
author_email='timothee.peignier@tryphon.org', author_email='timothee.peignier@tryphon.org',
url='https://github.com/cyberdelia/django-pipeline', url='https://github.com/cyberdelia/django-pipeline',
license=open('LICENSE').read(), license='MIT',
packages=find_packages(), packages=find_packages(),
zip_safe=False, zip_safe=False,
include_package_data=True, include_package_data=True,
......
...@@ -28,7 +28,7 @@ STATICFILES_STORAGE = 'pipeline.storage.PipelineStorage' ...@@ -28,7 +28,7 @@ STATICFILES_STORAGE = 'pipeline.storage.PipelineStorage'
STATIC_ROOT = local_path('static/') STATIC_ROOT = local_path('static/')
STATIC_URL = '/static/' STATIC_URL = '/static/'
STATICFILES_DIRS = ( STATICFILES_DIRS = (
local_path('assets/'), ('pipeline', local_path('assets/')),
local_path('assets2/'), local_path('assets2/'),
) )
STATICFILES_FINDERS = ( STATICFILES_FINDERS = (
...@@ -45,9 +45,9 @@ TEMPLATE_DIRS = ( ...@@ -45,9 +45,9 @@ TEMPLATE_DIRS = (
PIPELINE_CSS = { PIPELINE_CSS = {
'screen': { 'screen': {
'source_filenames': ( 'source_filenames': (
'css/first.css', 'pipeline/css/first.css',
'css/second.css', 'pipeline/css/second.css',
'css/urls.css', 'pipeline/css/urls.css',
), ),
'output_filename': 'screen.css' 'output_filename': 'screen.css'
} }
...@@ -55,10 +55,10 @@ PIPELINE_CSS = { ...@@ -55,10 +55,10 @@ PIPELINE_CSS = {
PIPELINE_JS = { PIPELINE_JS = {
'scripts': { 'scripts': {
'source_filenames': ( 'source_filenames': (
'js/first.js', 'pipeline/js/first.js',
'js/second.js', 'pipeline/js/second.js',
'js/application.js', 'pipeline/js/application.js',
'templates/**/*.jst' 'pipeline/templates/**/*.jst'
), ),
'output_filename': 'scripts.css' 'output_filename': 'scripts.css'
} }
......
{{ compressed_css('screen') }}
{{ compressed_js('scripts') }}
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
from compiler import * from compiler import *
from compressor import * from compressor import *
from glob import * from glob import *
from jinja2 import *
from packager import * from packager import *
from storage import * from storage import *
from utils import * from utils import *
...@@ -3,6 +3,8 @@ from django.test import TestCase ...@@ -3,6 +3,8 @@ from django.test import TestCase
from pipeline.conf import settings from pipeline.conf import settings
from pipeline.compilers import Compiler, CompilerBase from pipeline.compilers import Compiler, CompilerBase
from paths import _
class DummyCompiler(CompilerBase): class DummyCompiler(CompilerBase):
output_extension = 'js' output_extension = 'js'
...@@ -10,8 +12,8 @@ class DummyCompiler(CompilerBase): ...@@ -10,8 +12,8 @@ class DummyCompiler(CompilerBase):
def match_file(self, path): def match_file(self, path):
return path.endswith('.coffee') return path.endswith('.coffee')
def compile_file(self, content, path): def compile_file(self, infile, outfile, outdated=False, force=False):
return content return
class CompilerTest(TestCase): class CompilerTest(TestCase):
...@@ -30,10 +32,10 @@ class CompilerTest(TestCase): ...@@ -30,10 +32,10 @@ class CompilerTest(TestCase):
def test_compile(self): def test_compile(self):
paths = self.compiler.compile([ paths = self.compiler.compile([
'js/dummy.coffee', _('pipeline/js/dummy.coffee'),
'js/application.js', _('pipeline/js/application.js'),
]) ])
self.assertEquals(['js/dummy.js', 'js/application.js'], paths) self.assertEquals([_('pipeline/js/dummy.js'), _('pipeline/js/application.js')], paths)
def tearDown(self): def tearDown(self):
settings.PIPELINE_COMPILERS = self.old_compilers settings.PIPELINE_COMPILERS = self.old_compilers
...@@ -8,9 +8,12 @@ from django.test import TestCase ...@@ -8,9 +8,12 @@ from django.test import TestCase
from pipeline.compressors import Compressor, TEMPLATE_FUNC from pipeline.compressors import Compressor, TEMPLATE_FUNC
from pipeline.compressors.yui import YUICompressor from pipeline.compressors.yui import YUICompressor
from paths import _
class CompressorTest(TestCase): class CompressorTest(TestCase):
def setUp(self): def setUp(self):
self.maxDiff = None
self.compressor = Compressor() self.compressor = Compressor()
def test_js_compressor_class(self): def test_js_compressor_class(self):
...@@ -21,24 +24,24 @@ class CompressorTest(TestCase): ...@@ -21,24 +24,24 @@ class CompressorTest(TestCase):
def test_concatenate_and_rewrite(self): def test_concatenate_and_rewrite(self):
css = self.compressor.concatenate_and_rewrite([ css = self.compressor.concatenate_and_rewrite([
'css/first.css', _('pipeline/css/first.css'),
'css/second.css' _('pipeline/css/second.css')
], 'css/screen.css') ], 'css/screen.css')
self.assertEquals(""".concat {\n display: none;\n}\n\n.concatenate {\n display: block;\n}\n""", css) self.assertEquals(""".concat {\n display: none;\n}\n\n.concatenate {\n display: block;\n}\n""", css)
def test_concatenate(self): def test_concatenate(self):
js = self.compressor.concatenate([ js = self.compressor.concatenate([
'js/first.js', _('pipeline/js/first.js'),
'js/second.js' _('pipeline/js/second.js')
]) ])
self.assertEquals("""function concat() {\n console.log(arguments);\n}\n\nfunction cat() {\n console.log("hello world");\n}\n""", js) self.assertEquals("""function concat() {\n console.log(arguments);\n}\n\nfunction cat() {\n console.log("hello world");\n}\n""", js)
@patch.object(base64, 'b64encode') @patch.object(base64, 'b64encode')
def test_encoded_content(self, mock): def test_encoded_content(self, mock):
self.compressor.encoded_content('images/arrow.png') self.compressor.encoded_content(_('pipeline/images/arrow.png'))
self.assertTrue(mock.called) self.assertTrue(mock.called)
mock.reset_mock() mock.reset_mock()
self.compressor.encoded_content('images/arrow.png') self.compressor.encoded_content(_('pipeline/images/arrow.png'))
self.assertFalse(mock.called) self.assertFalse(mock.called)
def test_relative_path(self): def test_relative_path(self):
...@@ -47,9 +50,9 @@ class CompressorTest(TestCase): ...@@ -47,9 +50,9 @@ class CompressorTest(TestCase):
def test_base_path(self): def test_base_path(self):
base_path = self.compressor.base_path([ base_path = self.compressor.base_path([
'js/templates/form.jst', 'js/templates/field.jst' _('js/templates/form.jst'), _('js/templates/field.jst')
]) ])
self.assertEquals(base_path, 'js/templates') self.assertEquals(base_path, _('js/templates'))
def test_absolute_path(self): def test_absolute_path(self):
absolute_path = self.compressor.absolute_path('../../images/sprite.png', absolute_path = self.compressor.absolute_path('../../images/sprite.png',
...@@ -70,19 +73,19 @@ class CompressorTest(TestCase): ...@@ -70,19 +73,19 @@ class CompressorTest(TestCase):
self.assertEquals(name, 'photo_detail') self.assertEquals(name, 'photo_detail')
def test_compile_templates(self): def test_compile_templates(self):
templates = self.compressor.compile_templates(['templates/photo/list.jst']) templates = self.compressor.compile_templates([_('pipeline/templates/photo/list.jst')])
self.assertEquals(templates, """window.JST = window.JST || {};\n%s\nwindow.JST[\'list\'] = template(\'<div class="photo">\\n <img src="<%%= src %%>" />\\n <div class="caption">\\n <%%= caption %%>\\n </div>\\n</div>\');\n""" % TEMPLATE_FUNC) self.assertEquals(templates, """window.JST = window.JST || {};\n%s\nwindow.JST[\'list\'] = template(\'<div class="photo">\\n <img src="<%%= src %%>" />\\n <div class="caption">\\n <%%= caption %%>\\n </div>\\n</div>\');\n""" % TEMPLATE_FUNC)
templates = self.compressor.compile_templates([ templates = self.compressor.compile_templates([
'templates/video/detail.jst', _('pipeline/templates/video/detail.jst'),
'templates/photo/detail.jst' _('pipeline/templates/photo/detail.jst')
]) ])
self.assertEqual(templates, """window.JST = window.JST || {};\n%s\nwindow.JST[\'video_detail\'] = template(\'<div class="video">\\n <video src="<%%= src %%>" />\\n <div class="caption">\\n <%%= description %%>\\n </div>\\n</div>\');\nwindow.JST[\'photo_detail\'] = template(\'<div class="photo">\\n <img src="<%%= src %%>" />\\n <div class="caption">\\n <%%= caption %%> by <%%= author %%>\\n </div>\\n</div>\');\n""" % TEMPLATE_FUNC) self.assertEqual(templates, """window.JST = window.JST || {};\n%s\nwindow.JST[\'video_detail\'] = template(\'<div class="video">\\n <video src="<%%= src %%>" />\\n <div class="caption">\\n <%%= description %%>\\n </div>\\n</div>\');\nwindow.JST[\'photo_detail\'] = template(\'<div class="photo">\\n <img src="<%%= src %%>" />\\n <div class="caption">\\n <%%= caption %%> by <%%= author %%>\\n </div>\\n</div>\');\n""" % TEMPLATE_FUNC)
def test_embeddable(self): def test_embeddable(self):
self.assertFalse(self.compressor.embeddable('images/sprite.png', None)) self.assertFalse(self.compressor.embeddable(_('pipeline/images/sprite.png'), None))
self.assertFalse(self.compressor.embeddable('images/arrow.png', 'datauri')) self.assertFalse(self.compressor.embeddable(_('pipeline/images/arrow.png'), 'datauri'))
self.assertTrue(self.compressor.embeddable('images/embed/arrow.png', 'datauri')) self.assertTrue(self.compressor.embeddable(_('pipeline/images/embed/arrow.png'), 'datauri'))
self.assertFalse(self.compressor.embeddable('images/arrow.dat', 'datauri')) self.assertFalse(self.compressor.embeddable(_('pipeline/images/arrow.dat'), 'datauri'))
def test_construct_asset_path(self): def test_construct_asset_path(self):
asset_path = self.compressor.construct_asset_path("../../images/sprite.png", asset_path = self.compressor.construct_asset_path("../../images/sprite.png",
...@@ -94,18 +97,18 @@ class CompressorTest(TestCase): ...@@ -94,18 +97,18 @@ class CompressorTest(TestCase):
def test_url_rewrite(self): def test_url_rewrite(self):
output = self.compressor.concatenate_and_rewrite([ output = self.compressor.concatenate_and_rewrite([
'css/urls.css', _('pipeline/css/urls.css'),
], 'css/screen.css') ], 'css/screen.css')
self.assertEquals("""@font-face { self.assertEquals(u"""@font-face {
font-family: 'Pipeline'; font-family: 'Pipeline';
src: url(../fonts/pipeline.eot); src: url(../pipeline/fonts/pipeline.eot);
src: url(../fonts/pipeline.eot?#iefix) format('embedded-opentype'); src: url(../pipeline/fonts/pipeline.eot?#iefix) format('embedded-opentype');
src: local('☺'), url(../fonts/pipeline.woff) format('woff'), url(../fonts/pipeline.ttf) format('truetype'), url(../fonts/pipeline.svg#IyfZbseF) format('svg'); src: local('☺'), url(../pipeline/fonts/pipeline.woff) format('woff'), url(../pipeline/fonts/pipeline.ttf) format('truetype'), url(../pipeline/fonts/pipeline.svg#IyfZbseF) format('svg');
font-weight: normal; font-weight: normal;
font-style: normal; font-style: normal;
} }
.relative-url { .relative-url {
background-image: url(../images/sprite-buttons.png); background-image: url(../pipeline/images/sprite-buttons.png);
} }
.absolute-url { .absolute-url {
background-image: url(/images/sprite-buttons.png); background-image: url(/images/sprite-buttons.png);
......
# -*- coding: utf-8 -*-
from __future__ import absolute_import
from django.conf import settings
from django.test import TestCase
from jinja2 import Environment, FileSystemLoader
from pipeline.packager import PackageNotFound
from pipeline.jinja2.ext import compressed_css, compressed_js, Jinja2Compressed
class Jinja2Test(TestCase):
def setUp(self):
from django.template.loaders import app_directories # has to be here
self.loader = FileSystemLoader((app_directories.app_template_dirs +
settings.TEMPLATE_DIRS))
self.environment = Environment(loader=self.loader)
self.environment.globals['compressed_css'] = compressed_css
self.environment.globals['compressed_js'] = compressed_js
self.maxDiff = None
def test_exception_raised_with_unknown_ftype(self):
try:
Jinja2Compressed('png')
self.fail()
except PackageNotFound:
pass
def test_template_css_function_individual(self):
settings.PIPELINE = False
try:
tpl = self.environment.get_template('css.jinja')
tpl.render()
except:
self.fail('Failed to load individual CSS')
def test_template_css_function_compressed(self):
settings.PIPELINE = True
try:
tpl = self.environment.get_template('css.jinja')
tpl.render()
except:
self.fail('Failed to load compressed CSS')
def test_template_js_function_individual(self):
settings.PIPELINE = False
try:
tpl = self.environment.get_template('js.jinja')
tpl.render()
except:
self.fail('Failed to load individual JS')
def test_template_js_function_compressed(self):
settings.PIPELINE = True
try:
tpl = self.environment.get_template('js.jinja')
tpl.render()
except:
self.fail('Failed to load compressed JS')
...@@ -2,6 +2,8 @@ from django.test import TestCase ...@@ -2,6 +2,8 @@ from django.test import TestCase
from pipeline.packager import Packager, PackageNotFound from pipeline.packager import Packager, PackageNotFound
from paths import _
class PackagerTest(TestCase): class PackagerTest(TestCase):
def test_package_for(self): def test_package_for(self):
...@@ -9,7 +11,7 @@ class PackagerTest(TestCase): ...@@ -9,7 +11,7 @@ class PackagerTest(TestCase):
packager.packages['js'] = packager.create_packages({ packager.packages['js'] = packager.create_packages({
'application': { 'application': {
'source_filenames': ( 'source_filenames': (
'js/application.js', _('pipeline/js/application.js'),
), ),
'output_filename': 'application.js' 'output_filename': 'application.js'
} }
...@@ -29,9 +31,9 @@ class PackagerTest(TestCase): ...@@ -29,9 +31,9 @@ class PackagerTest(TestCase):
packages = packager.create_packages({ packages = packager.create_packages({
'templates': { 'templates': {
'source_filenames': ( 'source_filenames': (
'templates/photo/list.jst', _('pipeline/templates/photo/list.jst'),
), ),
'output_filename': 'templates.js', 'output_filename': 'templates.js',
} }
}) })
self.assertEqual(packages['templates'].templates, ['templates/photo/list.jst']) self.assertEqual(packages['templates'].templates, [_('pipeline/templates/photo/list.jst')])
import os
def _(path):
# Make sure the path contains only the correct separator
return path.replace('/', os.sep).replace('\\', os.sep)
...@@ -3,6 +3,7 @@ from django.utils.datastructures import SortedDict ...@@ -3,6 +3,7 @@ from django.utils.datastructures import SortedDict
from pipeline.conf import settings from pipeline.conf import settings
from pipeline.storage import PipelineStorage from pipeline.storage import PipelineStorage
from paths import _
class StorageTest(TestCase): class StorageTest(TestCase):
...@@ -10,8 +11,8 @@ class StorageTest(TestCase): ...@@ -10,8 +11,8 @@ class StorageTest(TestCase):
settings.PIPELINE_CSS = { settings.PIPELINE_CSS = {
'testing': { 'testing': {
'source_filenames': ( 'source_filenames': (
'css/first.css', _('pipeline/css/first.css'),
'css/third.css', _('css/third.css'),
), ),
'manifest': False, 'manifest': False,
'output_filename': 'testing.css', 'output_filename': 'testing.css',
......
...@@ -19,6 +19,7 @@ deps = ...@@ -19,6 +19,7 @@ deps =
mock mock
django-staticfiles==1.2.1 django-staticfiles==1.2.1
unittest2 unittest2
jinja2
[testenv:py26-1.2.X] [testenv:py26-1.2.X]
basepython = python2.6 basepython = python2.6
...@@ -27,6 +28,7 @@ deps = ...@@ -27,6 +28,7 @@ deps =
mock mock
django-staticfiles==1.2.1 django-staticfiles==1.2.1
unittest2 unittest2
jinja2
[testenv:py27-1.2.X] [testenv:py27-1.2.X]
basepython = python2.7 basepython = python2.7
...@@ -35,6 +37,7 @@ deps = ...@@ -35,6 +37,7 @@ deps =
mock mock
django-staticfiles==1.2.1 django-staticfiles==1.2.1
unittest2 unittest2
jinja2
[testenv:py25-1.3.X] [testenv:py25-1.3.X]
basepython = python2.5 basepython = python2.5
...@@ -43,6 +46,7 @@ deps = ...@@ -43,6 +46,7 @@ deps =
mock mock
django-staticfiles==1.2.1 django-staticfiles==1.2.1
unittest2 unittest2
jinja2
[testenv:py26-1.3.X] [testenv:py26-1.3.X]
basepython = python2.6 basepython = python2.6
...@@ -51,6 +55,7 @@ deps = ...@@ -51,6 +55,7 @@ deps =
mock mock
django-staticfiles==1.2.1 django-staticfiles==1.2.1
unittest2 unittest2
jinja2
[testenv:py27-1.3.X] [testenv:py27-1.3.X]
basepython = python2.7 basepython = python2.7
...@@ -59,6 +64,7 @@ deps = ...@@ -59,6 +64,7 @@ deps =
mock mock
django-staticfiles==1.2.1 django-staticfiles==1.2.1
unittest2 unittest2
jinja2
[testenv:py25] [testenv:py25]
basepython = python2.5 basepython = python2.5
...@@ -67,6 +73,7 @@ deps = ...@@ -67,6 +73,7 @@ deps =
mock mock
django-staticfiles==1.2.1 django-staticfiles==1.2.1
unittest2 unittest2
jinja2
[testenv:py26] [testenv:py26]
basepython = python2.6 basepython = python2.6
...@@ -75,6 +82,7 @@ deps = ...@@ -75,6 +82,7 @@ deps =
mock mock
django-staticfiles==1.2.1 django-staticfiles==1.2.1
unittest2 unittest2
jinja2
[testenv:py27] [testenv:py27]
basepython = python2.7 basepython = python2.7
...@@ -83,6 +91,7 @@ deps = ...@@ -83,6 +91,7 @@ deps =
mock mock
django-staticfiles==1.2.1 django-staticfiles==1.2.1
unittest2 unittest2
jinja2
[testenv:pypy] [testenv:pypy]
basepython = pypy basepython = pypy
...@@ -91,6 +100,7 @@ deps = ...@@ -91,6 +100,7 @@ deps =
mock mock
django-staticfiles==1.2.1 django-staticfiles==1.2.1
unittest2 unittest2
jinja2
[testenv:docs] [testenv:docs]
basepython = python2.7 basepython = python2.7
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment