Skip to content

Commit

Permalink
Allow user-defined template engine configuration via the themes confi…
Browse files Browse the repository at this point in the history
…guration.

Currently only implemented configuration is for the Jinja templating
engine, it is now possible to configure Jinja extensions.

Also some improvements to make the template handling code
better and more readable; mostly typing.
  • Loading branch information
aknrdureegaesr committed Feb 20, 2024
1 parent fc7a75f commit 893e4c0
Show file tree
Hide file tree
Showing 10 changed files with 129 additions and 90 deletions.
1 change: 1 addition & 0 deletions CHANGES.txt
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ Features
* Support passing ``--poll`` to ``nikola auto`` to better deal with symlink farms.
* Trace template usage when an environment variable ``NIKOLA_TEMPLATES_TRACE``
is set to any non-empty value.
* Allow configuration of Jinja2 extensions through the theme configuration.

Bugfixes
--------
Expand Down
15 changes: 13 additions & 2 deletions docs/theming.rst
Original file line number Diff line number Diff line change
Expand Up @@ -104,8 +104,8 @@ with the same name as your theme, and a ``.theme`` extension, eg.
.. code:: ini
[Theme]
engine = mako
parent = base
engine = jinja
parent = base-jinja
author = The Nikola Contributors
author_url = https://getnikola.com/
based_on = Bootstrap 3 <https://getbootstrap.com/>
Expand All @@ -120,6 +120,10 @@ with the same name as your theme, and a ``.theme`` extension, eg.
[Nikola]
bootswatch = True
[jinja]
# Good for investigation, but not recommended to leave active in production:
extensions = jinja2.ext.debug
The following keys are currently supported:

* ``Theme`` — contains information about the theme.
Expand Down Expand Up @@ -164,6 +168,13 @@ The following keys are currently supported:
* ``ignored_assets`` — comma-separated list of assets to ignore (relative to
the ``assets/`` directory, eg. ``css/theme.css``)

* ``jinja`` - This section is ignored unless your theme's engine is ``jinja``.

* ``extensions`` - comma-separated list of
`jinja2-extensions <https://jinja.palletsprojects.com/en/3.1.x/extensions/>`_
that you want to be available when rendering your templates.


Templates
---------

Expand Down
9 changes: 2 additions & 7 deletions nikola/log.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,12 +105,7 @@ def configure_logging(logging_mode: LoggingMode = LoggingMode.NORMAL) -> None:
return

handler = logging.StreamHandler()
handler.setFormatter(
ColorfulFormatter(
fmt=_LOGGING_FMT,
datefmt=_LOGGING_DATEFMT,
)
)
handler.setFormatter(ColorfulFormatter(fmt=_LOGGING_FMT, datefmt=_LOGGING_DATEFMT))

handlers = [handler]
if logging_mode == LoggingMode.STRICT:
Expand Down Expand Up @@ -152,7 +147,7 @@ def init_template_trace_logging(filename: str) -> None:
As there is lots of other stuff happening on the normal output stream,
this info is also written to a log file.
"""
"""
TEMPLATES_LOGGER.level = logging.DEBUG
formatter = logging.Formatter(
fmt=_LOGGING_FMT,
Expand Down
29 changes: 16 additions & 13 deletions nikola/nikola.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
import pathlib
import sys
import typing
from typing import Any, Dict, Iterable, List, Optional, Set
import mimetypes
from collections import defaultdict
from copy import copy
Expand Down Expand Up @@ -373,7 +374,7 @@ class Nikola(object):
plugin_manager: PluginManager
_template_system: TemplateSystem

def __init__(self, **config):
def __init__(self, **config) -> None:
"""Initialize proper environment for running tasks."""
# Register our own path handlers
self.path_handlers = {
Expand All @@ -395,7 +396,7 @@ def __init__(self, **config):
self.timeline = []
self.pages = []
self._scanned = False
self._template_system: typing.Optional[TemplateSystem] = None
self._template_system: Optional[TemplateSystem] = None
self._THEMES = None
self._MESSAGES = None
self.filters = {}
Expand Down Expand Up @@ -996,13 +997,13 @@ def __init__(self, **config):
# WebP files have no official MIME type yet, but we need to recognize them (Issue #3671)
mimetypes.add_type('image/webp', '.webp')

def _filter_duplicate_plugins(self, plugin_list: typing.Iterable[PluginCandidate]):
def _filter_duplicate_plugins(self, plugin_list: Iterable[PluginCandidate]):
"""Find repeated plugins and discard the less local copy."""
def plugin_position_in_places(plugin: PluginInfo):
# plugin here is a tuple:
# (path to the .plugin file, path to plugin module w/o .py, plugin metadata)
place: pathlib.Path
for i, place in enumerate(self._plugin_places):
place: pathlib.Path
try:
# Path.is_relative_to backport
plugin.source_dir.relative_to(place)
Expand All @@ -1025,7 +1026,7 @@ def plugin_position_in_places(plugin: PluginInfo):
result.append(plugins[-1])
return result

def init_plugins(self, commands_only=False, load_all=False):
def init_plugins(self, commands_only=False, load_all=False) -> None:
"""Load plugins as needed."""
extra_plugins_dirs = self.config['EXTRA_PLUGINS_DIRS']
self._loading_commands_only = commands_only
Expand Down Expand Up @@ -1086,9 +1087,9 @@ def init_plugins(self, commands_only=False, load_all=False):
# Search for compiler plugins which we disabled but shouldn't have
self._activate_plugins_of_category("PostScanner")
if not load_all:
file_extensions = set()
file_extensions: Set[str] = set()
post_scanner: PostScanner
for post_scanner in [p.plugin_object for p in self.plugin_manager.get_plugins_of_category('PostScanner')]:
post_scanner: PostScanner
exts = post_scanner.supported_extensions()
if exts is not None:
file_extensions.update(exts)
Expand Down Expand Up @@ -1126,8 +1127,8 @@ def init_plugins(self, commands_only=False, load_all=False):

self._activate_plugins_of_category("Taxonomy")
self.taxonomy_plugins = {}
taxonomy: Taxonomy
for taxonomy in [p.plugin_object for p in self.plugin_manager.get_plugins_of_category('Taxonomy')]:
taxonomy: Taxonomy
if not taxonomy.is_enabled():
continue
if taxonomy.classification_name in self.taxonomy_plugins:
Expand Down Expand Up @@ -1322,7 +1323,7 @@ def _activate_plugin(self, plugin_info: PluginInfo) -> None:
if candidate.exists() and candidate.is_dir():
self.template_system.inject_directory(str(candidate))

def _activate_plugins_of_category(self, category) -> typing.List[PluginInfo]:
def _activate_plugins_of_category(self, category) -> List[PluginInfo]:
"""Activate all the plugins of a given category and return them."""
# this code duplicated in tests/base.py
plugins = []
Expand Down Expand Up @@ -1390,13 +1391,15 @@ def _get_global_context(self):
def _get_template_system(self):
if self._template_system is None:
# Load template plugin
template_sys_name = utils.get_template_engine(self.THEMES)
template_sys_name, template_sys_user_config = utils.get_template_engine(self.THEMES)
pi = self.plugin_manager.get_plugin_by_name(template_sys_name, "TemplateSystem")
if pi is None:
sys.stderr.write("Error loading {0} template system "
"plugin\n".format(template_sys_name))
sys.exit(1)
self._template_system = typing.cast(TemplateSystem, pi.plugin_object)
if template_sys_user_config is not None:
self._template_system.user_configuration(template_sys_user_config)
lookup_dirs = ['templates'] + [os.path.join(utils.get_theme_path(name), "templates")
for name in self.THEMES]
self._template_system.set_directories(lookup_dirs,
Expand Down Expand Up @@ -1444,7 +1447,7 @@ def get_compiler(self, source_name):

return compiler

def render_template(self, template_name, output_name, context, url_type=None, is_fragment=False):
def render_template(self, template_name: str, output_name: str, context, url_type=None, is_fragment=False):
"""Render a template with the global context.
If ``output_name`` is None, will return a string and all URL
Expand All @@ -1463,7 +1466,7 @@ def render_template(self, template_name, output_name, context, url_type=None, is
utils.TEMPLATES_LOGGER.debug("For %s, template %s builds %s", context["post"].source_path, template_name, output_name)
else:
utils.TEMPLATES_LOGGER.debug("Template %s builds %s", template_name, output_name)
local_context: typing.Dict[str, typing.Any] = {}
local_context: Dict[str, Any] = {}
local_context["template_name"] = template_name
local_context.update(self.GLOBAL_CONTEXT)
local_context.update(context)
Expand Down Expand Up @@ -1699,7 +1702,7 @@ def _register_templated_shortcodes(self):

builtin_sc_dir = utils.pkg_resources_path(
'nikola',
os.path.join('data', 'shortcodes', utils.get_template_engine(self.THEMES)))
os.path.join('data', 'shortcodes', utils.get_template_engine(self.THEMES)[0]))

for sc_dir in [builtin_sc_dir, 'shortcodes']:
if not os.path.isdir(sc_dir):
Expand Down
Loading

0 comments on commit 893e4c0

Please sign in to comment.