diff --git a/assets/Readme.rst b/assets/Readme.rst new file mode 100644 index 0000000..42091cb --- /dev/null +++ b/assets/Readme.rst @@ -0,0 +1,106 @@ +Asset management +---------------- + +This plugin allows you to use the `Webassets`_ module to manage assets such as +CSS and JS files. The module must first be installed:: + + pip install webassets + +The Webassets module allows you to perform a number of useful asset management +functions, including: + +* CSS minifier (``cssmin``, ``yui_css``, ...) +* CSS compiler (``less``, ``sass``, ...) +* JS minifier (``uglifyjs``, ``yui_js``, ``closure``, ...) + +Others filters include CSS URL rewriting, integration of images in CSS via data +URIs, and more. Webassets can also append a version identifier to your asset +URL to convince browsers to download new versions of your assets when you use +far-future expires headers. Please refer to the `Webassets documentation`_ for +more information. + +When used with Pelican, Webassets is configured to process assets in the +``OUTPUT_PATH/theme`` directory. You can use Webassets in your templates by +including one or more template tags. The Jinja variable ``{{ ASSET_URL }}`` can +be used in templates and is relative to the ``theme/`` url. The +``{{ ASSET_URL }}`` variable should be used in conjunction with the +``{{ SITEURL }}`` variable in order to generate URLs properly. For example: + +.. code-block:: jinja + + {% assets filters="cssmin", output="css/style.min.css", "css/inuit.css", "css/pygment-monokai.css", "css/main.css" %} + + {% endassets %} + +... will produce a minified css file with a version identifier that looks like: + +.. code-block:: html + + + +These filters can be combined. Here is an example that uses the SASS compiler +and minifies the output: + +.. code-block:: jinja + + {% assets filters="sass,cssmin", output="css/style.min.css", "css/style.scss" %} + + {% endassets %} + +Another example for Javascript: + +.. code-block:: jinja + + {% assets filters="uglifyjs", output="js/packed.js", "js/jquery.js", "js/base.js", "js/widgets.js" %} + + {% endassets %} + +The above will produce a minified JS file: + +.. code-block:: html + + + +Pelican's debug mode is propagated to Webassets to disable asset packaging +and instead work with the uncompressed assets. + +If you need to create named bundles (for example, if you need to compile SASS +files before minifying with other CSS files), you can use the ``ASSET_BUNDLES`` +variable in your settings file. This is an ordered sequence of 3-tuples, where +the 3-tuple is defined as ``(name, args, kwargs)``. This tuple is passed to the +`environment's register() method`_. The following will compile two SCSS files +into a named bundle, using the ``pyscss`` filter: + +.. code-block:: python + + ASSET_BUNDLES = ( + ('scss', ['colors.scss', 'main.scss'], {'filters': 'pyscss'}), + ) + +Many of Webasset's available compilers have additional configuration options +(i.e. 'Less', 'Sass', 'Stylus', 'Closure_js'). You can pass these options to +Webassets using the ``ASSET_CONFIG`` in your settings file. + +The following will handle Google Closure's compilation level and locate +LessCSS's binary: + +.. code-block:: python + + ASSET_CONFIG = (('closure_compressor_optimization', 'WHITESPACE_ONLY'), + ('less_bin', 'lessc.cmd'), ) + +If you wish to place your assets in locations other than the theme output +directory, you can use ``ASSET_SOURCE_PATHS`` in your settings file to provide +webassets with a list of additional directories to search, relative to the +theme's top-level directory: + +.. code-block:: python + + ASSET_SOURCE_PATHS = [ + 'vendor/css', + 'scss', + ] + +.. _Webassets: https://github.com/miracle2k/webassets +.. _Webassets documentation: http://webassets.readthedocs.org/en/latest/builtin_filters.html +.. _environment's register() method: http://webassets.readthedocs.org/en/latest/environment.html#registering-bundles diff --git a/assets/__init__.py b/assets/__init__.py new file mode 100644 index 0000000..67b75dd --- /dev/null +++ b/assets/__init__.py @@ -0,0 +1 @@ +from .assets import * diff --git a/assets/assets.py b/assets/assets.py new file mode 100644 index 0000000..e204dd6 --- /dev/null +++ b/assets/assets.py @@ -0,0 +1,75 @@ +# -*- coding: utf-8 -*- +""" +Asset management plugin for Pelican +=================================== + +This plugin allows you to use the `webassets`_ module to manage assets such as +CSS and JS files. + +The ASSET_URL is set to a relative url to honor Pelican's RELATIVE_URLS +setting. This requires the use of SITEURL in the templates:: + + + +.. _webassets: https://webassets.readthedocs.org/ + +""" +from __future__ import unicode_literals + +import os +import logging + +from pelican import signals +logger = logging.getLogger(__name__) + +try: + import webassets + from webassets import Environment + from webassets.ext.jinja2 import AssetsExtension +except ImportError: + webassets = None + +def add_jinja2_ext(pelican): + """Add Webassets to Jinja2 extensions in Pelican settings.""" + + if 'JINJA_ENVIRONMENT' in pelican.settings: # pelican 3.7+ + pelican.settings['JINJA_ENVIRONMENT']['extensions'].append(AssetsExtension) + else: + pelican.settings['JINJA_EXTENSIONS'].append(AssetsExtension) + + +def create_assets_env(generator): + """Define the assets environment and pass it to the generator.""" + + theme_static_dir = generator.settings['THEME_STATIC_DIR'] + assets_destination = os.path.join(generator.output_path, theme_static_dir) + generator.env.assets_environment = Environment( + assets_destination, theme_static_dir) + + if 'ASSET_CONFIG' in generator.settings: + for item in generator.settings['ASSET_CONFIG']: + generator.env.assets_environment.config[item[0]] = item[1] + + if 'ASSET_BUNDLES' in generator.settings: + for name, args, kwargs in generator.settings['ASSET_BUNDLES']: + generator.env.assets_environment.register(name, *args, **kwargs) + + if 'ASSET_DEBUG' in generator.settings: + generator.env.assets_environment.debug = generator.settings['ASSET_DEBUG'] + elif logging.getLevelName(logger.getEffectiveLevel()) == "DEBUG": + generator.env.assets_environment.debug = True + + for path in (generator.settings['THEME_STATIC_PATHS'] + + generator.settings.get('ASSET_SOURCE_PATHS', [])): + full_path = os.path.join(generator.theme, path) + generator.env.assets_environment.append_path(full_path) + + +def register(): + """Plugin registration.""" + if webassets: + signals.initialized.connect(add_jinja2_ext) + signals.generator_init.connect(create_assets_env) + else: + logger.warning('`assets` failed to load dependency `webassets`.' + '`assets` plugin not loaded.') diff --git a/assets/requirements.txt b/assets/requirements.txt new file mode 100644 index 0000000..7725d6a --- /dev/null +++ b/assets/requirements.txt @@ -0,0 +1,2 @@ +cssmin +webassets \ No newline at end of file diff --git a/assets/test_assets.py b/assets/test_assets.py new file mode 100644 index 0000000..3001073 --- /dev/null +++ b/assets/test_assets.py @@ -0,0 +1,112 @@ +# -*- coding: utf-8 -*- +# from __future__ import unicode_literals + +import hashlib +import locale +import os +from codecs import open +from tempfile import mkdtemp +from shutil import rmtree +import unittest +import subprocess + +from pelican import Pelican +from pelican.settings import read_settings +from pelican.tests.support import mute, skipIfNoExecutable, module_exists + +CUR_DIR = os.path.dirname(__file__) +THEME_DIR = os.path.join(CUR_DIR, 'test_data') +CSS_REF = open(os.path.join(THEME_DIR, 'static', 'css', + 'style.min.css')).read() +CSS_HASH = hashlib.md5(CSS_REF.encode()).hexdigest()[0:8] + + +@unittest.skipUnless(module_exists('webassets'), "webassets isn't installed") +@skipIfNoExecutable(['sass', '-v']) +@skipIfNoExecutable(['cssmin', '--version']) +class TestWebAssets(unittest.TestCase): + """Base class for testing webassets.""" + + def setUp(self, override=None): + import assets + self.temp_path = mkdtemp(prefix='pelicantests.') + settings = { + 'PATH': os.path.join(os.path.dirname(CUR_DIR), 'test_data', 'content'), + 'OUTPUT_PATH': self.temp_path, + 'PLUGINS': [assets], + 'THEME': THEME_DIR, + 'LOCALE': locale.normalize('en_US'), + 'CACHE_CONTENT': False + } + if override: + settings.update(override) + + self.settings = read_settings(override=settings) + pelican = Pelican(settings=self.settings) + mute(True)(pelican.run)() + + def tearDown(self): + rmtree(self.temp_path) + + def check_link_tag(self, css_file, html_file): + """Check the presence of `css_file` in `html_file`.""" + + link_tag = ('' + .format(css_file=css_file)) + html = open(html_file).read() + self.assertRegexpMatches(html, link_tag) + + +class TestWebAssetsRelativeURLS(TestWebAssets): + """Test pelican with relative urls.""" + + + def setUp(self): + TestWebAssets.setUp(self, override={'RELATIVE_URLS': True}) + + def test_jinja2_ext(self): + # Test that the Jinja2 extension was correctly added. + + from webassets.ext.jinja2 import AssetsExtension + self.assertIn(AssetsExtension, self.settings['JINJA_ENVIRONMENT']['extensions']) + + def test_compilation(self): + # Compare the compiled css with the reference. + + gen_file = os.path.join(self.temp_path, 'theme', 'gen', + 'style.{0}.min.css'.format(CSS_HASH)) + self.assertTrue(os.path.isfile(gen_file)) + + css_new = open(gen_file).read() + self.assertEqual(css_new, CSS_REF) + + def test_template(self): + # Look in the output files for the link tag. + + css_file = './theme/gen/style.{0}.min.css'.format(CSS_HASH) + html_files = ['index.html', 'archives.html', + 'this-is-a-super-article.html'] + for f in html_files: + self.check_link_tag(css_file, os.path.join(self.temp_path, f)) + + self.check_link_tag( + '../theme/gen/style.{0}.min.css'.format(CSS_HASH), + os.path.join(self.temp_path, 'category/yeah.html')) + + +class TestWebAssetsAbsoluteURLS(TestWebAssets): + """Test pelican with absolute urls.""" + + def setUp(self): + TestWebAssets.setUp(self, override={'RELATIVE_URLS': False, + 'SITEURL': 'http://localhost'}) + + def test_absolute_url(self): + # Look in the output files for the link tag with absolute url. + + css_file = ('http://localhost/theme/gen/style.{0}.min.css' + .format(CSS_HASH)) + html_files = ['index.html', 'archives.html', + 'this-is-a-super-article.html'] + for f in html_files: + self.check_link_tag(css_file, os.path.join(self.temp_path, f)) diff --git a/assets/test_data/static/css/style.min.css b/assets/test_data/static/css/style.min.css new file mode 100644 index 0000000..daf9c3c --- /dev/null +++ b/assets/test_data/static/css/style.min.css @@ -0,0 +1 @@ +body{font:14px/1.5 "Droid Sans",sans-serif;background-color:#e4e4e4;color:#242424}a{color:red}a:hover{color:orange} \ No newline at end of file diff --git a/assets/test_data/static/css/style.scss b/assets/test_data/static/css/style.scss new file mode 100644 index 0000000..10cd05b --- /dev/null +++ b/assets/test_data/static/css/style.scss @@ -0,0 +1,19 @@ +/* -*- scss-compile-at-save: nil -*- */ + +$baseFontFamily : "Droid Sans", sans-serif; +$textColor : #242424; +$bodyBackground : #e4e4e4; + +body { + font: 14px/1.5 $baseFontFamily; + background-color: $bodyBackground; + color: $textColor; +} + +a { + color: red; + + &:hover { + color: orange; + } +} diff --git a/assets/test_data/templates/base.html b/assets/test_data/templates/base.html new file mode 100644 index 0000000..05a32d0 --- /dev/null +++ b/assets/test_data/templates/base.html @@ -0,0 +1,7 @@ +{% extends "!simple/base.html" %} + +{% block head %} + {% assets filters="scss,cssmin", output="gen/style.%(version)s.min.css", "css/style.scss" %} + + {% endassets %} +{% endblock %}