Skip to content

Commit

Permalink
Allowing to register filters.
Browse files Browse the repository at this point in the history
  • Loading branch information
felixfontein committed Jun 4, 2017
1 parent 0ca3747 commit dfc2333
Show file tree
Hide file tree
Showing 7 changed files with 133 additions and 45 deletions.
2 changes: 2 additions & 0 deletions CHANGES.txt
Expand Up @@ -4,6 +4,8 @@ New in master
Features
--------

* Allowing to register filters from plugins, and allowing to specify default
filters as strings of the form ``filters.<name>`` (part of Issue #2475)
* Support ignoring assets via ``ignore_assets`` theme meta field
(Issue #2812)
* Ignore unused Colorbox locales (Issue #2812)
Expand Down
53 changes: 26 additions & 27 deletions docs/manual.txt
Expand Up @@ -1887,15 +1887,14 @@ To do that, you can use the provided helper adding this in your ``conf.py``:

.. code:: python

from nikola import filters

FILTERS = {
".css": [filters.yui_compressor],
".js": [filters.yui_compressor],
".css": ["filters.yui_compressor"],
".js": ["filters.yui_compressor"],
}

Where ``filters.yui_compressor`` is a helper function provided by Nikola. You can
replace that with strings describing command lines, or arbitrary python functions.
Where ``"filters.yui_compressor"`` points to a helper function provided by Nikola in the
``filters`` module. You can replace that with strings describing command lines, or
arbitrary python functions.

If there's any specific thing you expect to be generally useful as a filter, contact
me and I will add it to the filters library so that more people use it.
Expand Down Expand Up @@ -1931,65 +1930,65 @@ The currently available filters are:
".html": [apply_to_text_file(string.upper)]
}

html_tidy_nowrap
filters.html_tidy_nowrap
Prettify HTML 5 documents with `tidy5 <http://www.html-tidy.org/>`_

html_tidy_wrap
filters.html_tidy_wrap
Prettify HTML 5 documents wrapped at 80 characters with `tidy5 <http://www.html-tidy.org/>`_

html_tidy_wrap_attr
filters.html_tidy_wrap_attr
Prettify HTML 5 documents and wrap lines and attributes with `tidy5 <http://www.html-tidy.org/>`_

html_tidy_mini
filters.html_tidy_mini
Minify HTML 5 into smaller documents with `tidy5 <http://www.html-tidy.org/>`_

html_tidy_withconfig
filters.html_tidy_withconfig
Run `tidy5 <http://www.html-tidy.org/>`_ with ``tidy5.conf`` as the config file (supplied by user)

html5lib_minify
filters.html5lib_minify
Minify HTML5 using html5lib_minify

html5lib_xmllike
filters.html5lib_xmllike
Format using html5lib

typogrify
filters.typogrify
Improve typography using `typogrify <http://static.mintchaos.com/projects/typogrify/>`__

typogrify_sans_widont
filters.typogrify_sans_widont
Same as typogrify without the widont filter

minify_lines
filters.minify_lines
**THIS FILTER HAS BEEN TURNED INTO A NOOP** and currently does nothing.

normalize_html
filters.normalize_html
Pass HTML through LXML to normalize it. For example, it will resolve ``&quot;`` to actual
quotes. Usually not needed.

yui_compressor
filters.yui_compressor
Compress CSS/JavaScript using `YUI compressor <http://yui.github.io/yuicompressor/>`_

closure_compiler
filters.closure_compiler
Compile, compress, and optimize JavaScript `Google Closure Compiler <https://developers.google.com/closure/compiler/>`_

optipng
filters.optipng
Compress PNG files using `optipng <http://optipng.sourceforge.net/>`_

jpegoptim
filters.jpegoptim
Compress JPEG files using `jpegoptim <http://www.kokkonen.net/tjko/projects.html>`_

cssminify
filters.cssminify
Minify CSS using http://cssminifier.com/ (requires Internet access)

jsminify
filters.jsminify
Minify JS using http://javascript-minifier.com/ (requires Internet access)

jsonminify
filters.jsonminify
Minify JSON files (strip whitespace and use minimal separators).

xmlminify
filters.xmlminify
Minify XML files. Suitable for Nikola’s sitemaps and Atom feeds.

add_header_permalinks
filters.add_header_permalinks
Add links next to every header, Sphinx-style. You will need to add styling for the `headerlink` class,
in `custom.css`, for example:

Expand Down Expand Up @@ -2026,7 +2025,7 @@ add_header_permalinks
# HEADER_PERMALINKS_XPATH_LIST = ['*//{hx}']


deduplicate_ids
filters.deduplicate_ids
Prevent duplicated IDs in HTML output. An incrementing counter is added to
offending IDs. If used alongside ``add_header_permalinks``, it will fix
those links (it must run **after** that filter)
Expand Down
6 changes: 5 additions & 1 deletion nikola/filters.py
Expand Up @@ -24,7 +24,11 @@
# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

"""Utility functions to help run filters on files."""
"""Utility functions to help run filters on files.
All filters defined in this module are registered in the
default_filters plugin.
"""

from functools import wraps
import os
Expand Down
47 changes: 36 additions & 11 deletions nikola/nikola.py
Expand Up @@ -414,6 +414,7 @@ def __init__(self, **config):
self._template_system = None
self._THEMES = None
self._MESSAGES = None
self._filters = {}
self.debug = DEBUG
self.loghandlers = utils.STDERR_HANDLER # TODO remove on v8
self.colorful = config.pop('__colorful__', False)
Expand Down Expand Up @@ -957,17 +958,6 @@ def __init__(self, **config):
self._GLOBAL_CONTEXT['subtheme'] = config.get('THEME_REVEAL_CONFIG_SUBTHEME', 'sky')
self._GLOBAL_CONTEXT['transition'] = config.get('THEME_REVEAL_CONFIG_TRANSITION', 'cube')

# Configure filters
for actions in self.config['FILTERS'].values():
for i, f in enumerate(actions):
if hasattr(f, 'configuration_variables'):
args = {}
for arg, config in f.configuration_variables.items():
if config in self.config:
args[arg] = self.config[config]
if args:
actions[i] = functools.partial(f, **args)

# We use one global tzinfo object all over Nikola.
try:
self.tzinfo = dateutil.tz.gettz(self.config['TIMEZONE'])
Expand Down Expand Up @@ -1183,8 +1173,31 @@ def init_plugins(self, commands_only=False, load_all=False):
self.compilers[plugin_info.name] = \
plugin_info.plugin_object

# Load config plugins and register templated shortcodes
self._activate_plugins_of_category("ConfigPlugin")
self._register_templated_shortcodes()

# Check with registered filters
for actions in self.config['FILTERS'].values():
for i, f in list(enumerate(actions)):
if isinstance(f, str):
# Check whether this denotes a registered filter
filter = self._filters.get(f)
if filter is not None:
actions[i] = filter

# Configure filters
for actions in self.config['FILTERS'].values():
for i, f in enumerate(actions):
if hasattr(f, 'configuration_variables'):
args = {}
for arg, config in f.configuration_variables.items():
if config in self.config:
args[arg] = self.config[config]
if args:
actions[i] = functools.partial(f, **args)

# Signal that we are configured
signal('configured').send(self)

def _set_global_context_from_config(self):
Expand Down Expand Up @@ -1982,6 +1995,18 @@ def rel_link(self, src, dst):
url = utils.encodelink(url)
return url

def register_filter(self, filter_name, filter_definition):
"""Register a filter.
filter_name should be a name not confusable with an actual
executable. filter_definition should be a callable accepting
one argument (the filename).
"""
prev_filter = self._filters.get(filter_name)
if prev_filter is not None:
utils.LOGGER.warn('''The filter "{0}" is defined more than once.'''.format(filter_name))
self._filters[filter_name] = filter_definition

def file_exists(self, path, not_empty=False):
"""Check if the file exists. If not_empty is True, it also must not be empty."""
exists = os.path.exists(path)
Expand Down
9 changes: 9 additions & 0 deletions nikola/plugins/misc/default_filters.plugin
@@ -0,0 +1,9 @@
[Core]
Name = default_filters
Module = default_filters

[Documentation]
Author = Roberto Alsina
Version = 1.0
Website = https://getnikola.com/
Description = Register default filters
52 changes: 52 additions & 0 deletions nikola/plugins/misc/default_filters.py
@@ -0,0 +1,52 @@
# -*- coding: utf-8 -*-

# Copyright © 2012-2017 Roberto Alsina and others.

# Permission is hereby granted, free of charge, to any
# person obtaining a copy of this software and associated
# documentation files (the "Software"), to deal in the
# Software without restriction, including without limitation
# the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the
# Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice
# shall be included in all copies or substantial portions of
# the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
# PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
# OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

"""Registers all default filters."""

from __future__ import unicode_literals, print_function

from nikola.plugin_categories import ConfigPlugin
from nikola import filters


class DefaultFilters(ConfigPlugin):
"""Register all default filters."""

name = "default_filters"

def set_site(self, site):
"""Set site, which is a Nikola instance."""
super(DefaultFilters, self).set_site(site)
# Filter names are registered by formatting them by the following string
filter_name_format = 'filters.{0}'
for filter_name, filter_definition in filters.__dict__.items():
# Ignore objects whose name starts with an underscore, or which are not callable
if filter_name.startswith('_'):
continue
if not hasattr(filter_definition, '__call__'):
continue
# Register all other objects as filters
site.register_filter(filter_name_format.format(filter_name), filter_definition)
9 changes: 3 additions & 6 deletions nikola/plugins/task/posts.py
Expand Up @@ -108,12 +108,9 @@ def tl_ch():
for i, f in enumerate(ff):
if not f:
continue
if f.startswith('filters.'): # A function from the filters module
f = f[8:]
try:
flist.append(getattr(filters, f))
except AttributeError:
pass
filter = self.site._filters.get(f)
if filter is not None: # A registered filter
flist.append(filter)
else:
flist.append(f)
yield utils.apply_filters(task, {os.path.splitext(dest)[-1]: flist})
Expand Down

0 comments on commit dfc2333

Please sign in to comment.