Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 17 additions & 10 deletions docs/ext.rst
Original file line number Diff line number Diff line change
Expand Up @@ -109,30 +109,37 @@ To accept custom parameters in your extra context, add ``*args`` and

.. _ext-filters:

Extending ilters
=================
Extending Filters
==================

Template filters are registered by a
:py:deco:`sphinxnotes.render.filter` function decorator.

The decorated function takes a :py:class:`sphinx.environment.BuildEnvironment`
as argument and returns a filter function.
.. literalinclude:: ../tests/roots/test-filter-example/conf.py
:language: python
:start-after: [literalinclude catify-start]
:end-before: [literalinclude catify-end]

.. note::
.. example::

The decorator is used to **decorate the filter function factory, NOT
the filter function itself**.
.. data.render::

{{ "Hello world" | catify }}

If your filter needs access to the Sphinx build environment
:py:class:`sphinx.environment.BuildEnvironment`
(e.g., to access configuration or document metadata), use ``pass_build_env=True``:

.. literalinclude:: ../tests/roots/test-filter-example/conf.py
:language: python
:start-after: [literalinclude start]
:end-before: [literalinclude end]
:start-after: [literalinclude author-start]
:end-before: [literalinclude author-end]

.. example::

.. data.render::

{{ "Hello world" | catify }}
{{ "author: Hello world" | format_author }}

.. _ext-directives:
.. _ext-roles:
Expand Down
4 changes: 2 additions & 2 deletions docs/tmpl.rst
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,7 @@ The following extra contexts are available:

{%
set m = load_extra('markup')
| jsonify
| jsonify(indent=2)
%}

.. code::
Expand Down Expand Up @@ -295,7 +295,7 @@ __ https://jinja.palletsprojects.com/en/stable/templates/#builtin-filters
{% set text = {'name': 'mimi'} %}

:Strify: ``{{ text }}``
:JSONify: ``{{ text | jsonify | replace('\n', '')}}``
:JSONify: ``{{ text | jsonify }}``

.. seealso:: :ref:`ext-filters`

Expand Down
20 changes: 5 additions & 15 deletions src/sphinxnotes/render/ext/filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,35 +17,25 @@

if TYPE_CHECKING:
from sphinx.application import Sphinx
from sphinx.environment import BuildEnvironment


@filter('roles')
def roles(_: BuildEnvironment):
"""
Converting list of string to list of reStructuredText role.
def roles(value: Iterable[str], role: str) -> Iterable[str]:
"""Converting list of string to list of reStructuredText role.

For example::

{{ ["foo", "bar"] | roles("doc") }}

Produces ``[":doc:`foo`", ":doc:`bar`"]``.
"""

def _filter(value: Iterable[str], role: str) -> Iterable[str]:
return map(lambda x: ':%s:`%s`' % (role, x), value)

return _filter
return map(lambda x, role=role: ':%s:`%s`' % (role, x), value)


@filter('jsonify')
def jsonify(_: BuildEnvironment):
def jsonify(value: Any, indent: str | None = None) -> Any:
"""Converting value to JSON."""

def _filter(value: Any) -> Any:
return json.dumps(value, indent=' ')

return _filter
return json.dumps(value, indent=indent)


def setup(app: Sphinx):
Expand Down
38 changes: 24 additions & 14 deletions src/sphinxnotes/render/jinja.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,28 +31,30 @@ class JinjaRegistry:
rendering environment used by this extension.
"""

_filters: dict[str, Callable[[BuildEnvironment], Callable]]
_filters: dict[str, tuple[Callable, bool]] # (func, pass_build_env)
_extensions: list[str]

def __init__(self) -> None:
self._filters = {}
self._extensions = []

def add_filter(
self, name: str, factory: Callable[[BuildEnvironment], Callable]
self, name: str, func: Callable, pass_build_env: bool = False
) -> None:
"""Register a filter factory.
"""Register a filter.

:param name: The filter name, used in Jinja templates as ``{{ value|name }}``
:param factory: A callable that takes a :py:class:`~sphinx.environment.BuildEnvironment`
and returns a filter callable
:param func: The filter callable
:param pass_build_env: If True, filter receives
:py:class:`sphinx.environment.BuildEnvironment`
as first arg

.. note:: Using the :py:deco:`filter` decorator is recommended for most cases.

"""
if name in self._filters:
raise ValueError(f'Jinja filter "{name}" already registered')
self._filters[name] = factory
self._filters[name] = (func, pass_build_env)

def add_extension(self, extension: str) -> None:
"""Add a Jinja2 extension.
Expand Down Expand Up @@ -111,8 +113,14 @@ class _JinjaEnv(SandboxedEnvironment):

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
for name, factory in REGISTRY._filters.items():
self.filters[name] = factory(self._env)
for name, (func, pass_build_env) in REGISTRY._filters.items():
if pass_build_env:
env = self._env
self.filters[name] = lambda value, *args, _func=func, **kwargs: _func(
env, value, *args, **kwargs
)
else:
self.filters[name] = func

@classmethod
def on_builder_inited(cls, app: Sphinx):
Expand All @@ -132,20 +140,22 @@ def is_safe_attribute(self, obj, attr, value=None):
return super().is_safe_attribute(obj, attr, value)


def filter(name: str):
def filter(name: str, pass_build_env: bool = False):
"""Decorator for adding a filter to the Jinja environment.

Usage::

@filter('my_filter')
def my_filter(env: BuildEnvironment):
def _filter(value):
return value.upper()
return _filter
def my_filter(value):
return value.upper()

@filter('my_filter_with_env', pass_build_env=True)
def my_filter_with_env(env: BuildEnvironment, value):
return value.upper()
"""

def decorator(ff):
REGISTRY.add_filter(name, ff)
REGISTRY.add_filter(name, ff, pass_build_env)
return ff

return decorator
Expand Down
21 changes: 14 additions & 7 deletions tests/roots/test-filter-example/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,27 @@
from sphinx.environment import BuildEnvironment


# [literalinclude start]
# fmt: off
# [literalinclude catify-start]
@filter('catify')
def catify(_: BuildEnvironment):
def catify(value: str) -> str:
"""Speak in a cat-like tone"""
return value + ', meow~'
# [literalinclude catify-end]
# fmt: on

def _filter(value: str) -> str:
return value + ', meow~'

return _filter
# fmt: off
# [literalinclude author-start]
@filter('format_author', pass_build_env=True)
def format_author(env: BuildEnvironment, value: str) -> str:
"""Replace 'author' in value with the Sphinx document author"""
return value.replace('author', env.config.author)
# [literalinclude author-end]
# fmt: on


# [literalinclude end]


extensions = ['sphinxnotes.render.ext']


Expand Down