Skip to content

Commit

Permalink
Merge pull request #2819 from getnikola/filter-registry
Browse files Browse the repository at this point in the history
Allowing to register filters.
  • Loading branch information
Kwpolska committed Jun 4, 2017
2 parents 7b355d3 + a9d46bc commit 6fdaad3
Show file tree
Hide file tree
Showing 5 changed files with 78 additions and 47 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 @@ -1955,15 +1955,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 @@ -1999,65 +1998,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 @@ -2094,7 +2093,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
5 changes: 4 additions & 1 deletion nikola/filters.py
Expand Up @@ -24,7 +24,10 @@
# 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 Nikola.__init__.
"""

from functools import wraps
import os
Expand Down
54 changes: 42 additions & 12 deletions nikola/nikola.py
Expand Up @@ -61,7 +61,7 @@

from .post import Post # NOQA
from .state import Persistor
from . import DEBUG, utils, shortcodes
from . import DEBUG, filters, utils, shortcodes
from .plugin_categories import (
Command,
LateTask,
Expand Down Expand Up @@ -415,6 +415,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 @@ -959,17 +960,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 All @@ -986,6 +976,15 @@ def __init__(self, **config):
# Get search path for themes
self.themes_dirs = ['themes'] + self.config['EXTRA_THEMES_DIRS']

# Register default filters
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('_') or not callable(filter_definition):
continue
# Register all other objects as filters
self.register_filter(filter_name_format.format(filter_name), filter_definition)

self._set_global_context_from_config()
# Read data files only if a site exists (Issue #2708)
if self.configured:
Expand Down Expand Up @@ -1185,8 +1184,28 @@ 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 and configure filters
for actions in self.config['FILTERS'].values():
for i, f in enumerate(actions):
if isinstance(f, str):
# Check whether this denotes a registered filter
_f = self.filters.get(f)
if _f is not None:
f = _f
actions[i] = f
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 @@ -1984,6 +2003,17 @@ 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).
"""
if filter_name in self.filters:
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
11 changes: 4 additions & 7 deletions nikola/plugins/task/posts.py
Expand Up @@ -30,7 +30,7 @@
import os

from nikola.plugin_categories import Task
from nikola import filters, utils
from nikola import utils


def update_deps(post, lang, task):
Expand Down 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
_f = self.site.filters.get(f)
if _f is not None: # A registered filter
flist.append(_f)
else:
flist.append(f)
yield utils.apply_filters(task, {os.path.splitext(dest)[-1]: flist})
Expand Down

0 comments on commit 6fdaad3

Please sign in to comment.