+ ‘assets/Readme.rst’
+ ‘assets/__init__.py’ + ‘assets/assets.py’ + ‘assets/requirements.txt’ + ‘assets/test_assets.py’ + ‘assets/test_data/static/css/style.min.css’ + ‘assets/test_data/static/css/style.scss’ + ‘assets/test_data/templates/base.html’
This commit is contained in:
parent
ae7f5dcb9c
commit
d3d57adaa3
8 changed files with 323 additions and 0 deletions
106
assets/Readme.rst
Normal file
106
assets/Readme.rst
Normal file
|
|
@ -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" %}
|
||||||
|
<link rel="stylesheet" href="{{ SITEURL }}/{{ ASSET_URL }}">
|
||||||
|
{% endassets %}
|
||||||
|
|
||||||
|
... will produce a minified css file with a version identifier that looks like:
|
||||||
|
|
||||||
|
.. code-block:: html
|
||||||
|
|
||||||
|
<link href="http://{SITEURL}/theme/css/style.min.css?b3a7c807" rel="stylesheet">
|
||||||
|
|
||||||
|
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" %}
|
||||||
|
<link rel="stylesheet" href="{{ SITEURL }}/{{ ASSET_URL }}">
|
||||||
|
{% endassets %}
|
||||||
|
|
||||||
|
Another example for Javascript:
|
||||||
|
|
||||||
|
.. code-block:: jinja
|
||||||
|
|
||||||
|
{% assets filters="uglifyjs", output="js/packed.js", "js/jquery.js", "js/base.js", "js/widgets.js" %}
|
||||||
|
<script src="{{ SITEURL }}/{{ ASSET_URL }}"></script>
|
||||||
|
{% endassets %}
|
||||||
|
|
||||||
|
The above will produce a minified JS file:
|
||||||
|
|
||||||
|
.. code-block:: html
|
||||||
|
|
||||||
|
<script src="http://{SITEURL}/theme/js/packed.js?00703b9d"></script>
|
||||||
|
|
||||||
|
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
|
||||||
1
assets/__init__.py
Normal file
1
assets/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
from .assets import *
|
||||||
75
assets/assets.py
Normal file
75
assets/assets.py
Normal file
|
|
@ -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::
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="{{ SITEURL }}/{{ ASSET_URL }}">
|
||||||
|
|
||||||
|
.. _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.')
|
||||||
2
assets/requirements.txt
Normal file
2
assets/requirements.txt
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
cssmin
|
||||||
|
webassets
|
||||||
112
assets/test_assets.py
Normal file
112
assets/test_assets.py
Normal file
|
|
@ -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 = ('<link rel="stylesheet" href="{css_file}">'
|
||||||
|
.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))
|
||||||
1
assets/test_data/static/css/style.min.css
vendored
Normal file
1
assets/test_data/static/css/style.min.css
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
body{font:14px/1.5 "Droid Sans",sans-serif;background-color:#e4e4e4;color:#242424}a{color:red}a:hover{color:orange}
|
||||||
19
assets/test_data/static/css/style.scss
Normal file
19
assets/test_data/static/css/style.scss
Normal file
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
7
assets/test_data/templates/base.html
Normal file
7
assets/test_data/templates/base.html
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
{% extends "!simple/base.html" %}
|
||||||
|
|
||||||
|
{% block head %}
|
||||||
|
{% assets filters="scss,cssmin", output="gen/style.%(version)s.min.css", "css/style.scss" %}
|
||||||
|
<link rel="stylesheet" href="{{ SITEURL }}/{{ ASSET_URL }}">
|
||||||
|
{% endassets %}
|
||||||
|
{% endblock %}
|
||||||
Loading…
Reference in a new issue