diff --git a/docs/ext.rst b/docs/ext.rst index 115725a..363f67e 100644 --- a/docs/ext.rst +++ b/docs/ext.rst @@ -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: diff --git a/docs/tmpl.rst b/docs/tmpl.rst index c9037d4..d6b648a 100644 --- a/docs/tmpl.rst +++ b/docs/tmpl.rst @@ -220,7 +220,7 @@ The following extra contexts are available: {% set m = load_extra('markup') - | jsonify + | jsonify(indent=2) %} .. code:: @@ -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` diff --git a/src/sphinxnotes/render/ext/filters.py b/src/sphinxnotes/render/ext/filters.py index 2da2a8e..9a6a9f2 100644 --- a/src/sphinxnotes/render/ext/filters.py +++ b/src/sphinxnotes/render/ext/filters.py @@ -17,13 +17,11 @@ 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:: @@ -31,21 +29,13 @@ def roles(_: BuildEnvironment): 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): diff --git a/src/sphinxnotes/render/jinja.py b/src/sphinxnotes/render/jinja.py index f3aec4d..2a30f3c 100644 --- a/src/sphinxnotes/render/jinja.py +++ b/src/sphinxnotes/render/jinja.py @@ -31,7 +31,7 @@ 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: @@ -39,20 +39,22 @@ def __init__(self) -> None: 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. @@ -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): @@ -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 diff --git a/tests/roots/test-filter-example/conf.py b/tests/roots/test-filter-example/conf.py index acd0b63..8b2380b 100644 --- a/tests/roots/test-filter-example/conf.py +++ b/tests/roots/test-filter-example/conf.py @@ -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']