Source code for taika.ext.layouts

r"""
:mod:`taika.ext.layouts` -- Jinja layouts
=========================================

This extension renders documents content trought a the Jinja2 templating engine. It also
renders the content of documents itself if any Jinja block/comment/var is detected.

Event
-----

This extension is subscribed to the :data:`site-post-read` event.

Payload
-------

When the content and the templates are rendered, certain payload is passed and becomes
accessible by both content and templates. This payload has two main keys: ``site`` and
``document``.

Using :code:`document` you can access the document attributes being processed, such as the
:code:`path`, :code:`content`, etc. Check :doc:`/reference/documents` for details.

Inside ``site``, the :class:`taika.Taika` is accessible.

.. note::

    Note that ``site.config`` returns a dictionary with all the sections included, so to access
    which extensions are listed you should use ``site.config.taika.extensions``. This is a long
    "import-like" statement, and probably we will shrink it in the future.

Frontmatter
-----------

.. data:: layout (str)

    The layout that should render the document. Should exist under the :data:`layouts_path`. If None
    the documents is not passed throught the template, but its body is still rendered.

Configuration
-------------

.. note::

    All configuration hangs from a key in the YML configuration named 'layouts'.
    Inside it, you can add the following options:

.. data:: path (list)

    Default: **[./templates/]**

    A list of paths from where the layouts will be loaded.

.. data:: options (dict)

    Default: **{}** (empty-dict)

    A dictionary (key-value) of options to pass to the Jinja environment when created.

.. data:: default (str)

    Default: **index.html**

    The default layout if the document has no :code:`layout` defined.

.. data:: patterns (str)

    Default: **["*"]**

    A list of patterns to match Which files should be renderered. Default to all the files.

Default filters
---------------

.. data:: link

    Link against other documents inside your site using
    :code:`{{ '/posts/2019/my-other-post.md' | link }}`. Relative links not supported, only
    absolute paths will be accepted.

Process
-------

#. (pre-registering) The :class:`JinjaRenderer` is initialized with the configuration. The Jinja
   environment is created and the templates loaded.
#. Checks if the path of the document matches :data:`layouts_pattern`, if not, skips it.
#. Composes the layout using the document itself, so the document metadata is available directly.
#. If the content has any Jinja flag, it is renderered, so you can include Jinja syntax into the
   document text.
#. Then the content (rendered or not) is rendered throught the template :data:`layout`.
#. The document's content is modified.
#. Done!

Classes and Functions
---------------------
"""
import copy
import fnmatch
import logging
import pathlib
import sys
import urllib

from .. import utils

try:
    import jinja2
except ImportError:
    print("This extension needs Jinja2, please install the jinja2 module.")
    sys.exit(1)

LAYOUTS = {"path": ["./templates/"], "default": "index.html", "options": {}, "patterns": ["*"]}

LOGGER = logging.getLogger(__name__)


[docs]class DocumentNotFound(Exception): pass
[docs]class RelativeLinkNotSupported(Exception): pass
[docs]class JinjaRenderer(object): """This class holds the Jinja2 environment, removing the need to create it each time. Attributes ---------- env : :class:`jinja2.Environment` The configured Jinja environment. layouts_patterns : str The list of patterns which will be used to decide if the document should be processed. layouts_default : str The option so the :meth:`JinjaRenderer.render_content` can access it. """ def __init__(self, config): user_config = config.get("layouts", {}) self.config = utils.merge(user_config, copy.deepcopy(LAYOUTS)) layouts_path = self.config["path"] layouts_options = self.config["options"] loader = jinja2.FileSystemLoader(layouts_path) self.env = jinja2.Environment(loader=loader, **layouts_options) self.default = self.config["default"] self.patterns = self.config["patterns"]
[docs] def render_content(self, site): payload = {} payload["site"] = site for document in site.documents: if not any(fnmatch.fnmatch(document["path"], patt) for patt in self.patterns): continue payload["document"] = document content = document["content"] if self._has_block(content) or self._has_comment(content) or self._has_var(content): try: content = self.env.from_string(content).render(**payload) except Exception as err: LOGGER.error(f"Error processing the document '{document['path']}': {err}.") sys.exit(1) document["pre_render_content"] = payload["document"]["content"] = content template_name = document.get("layout", self.default) if template_name is not None: template = self.env.get_template(template_name) content = template.render(**payload) document["content"] = content
def _has_block(self, s): return self.env.block_start_string in s and self.env.block_end_string in s def _has_var(self, s): return self.env.variable_start_string in s and self.env.variable_end_string in s def _has_comment(self, s): return self.env.comment_start_string in s and self.env.comment_end_string in s
[docs]def setup(site): renderer = JinjaRenderer(site.config) renderer.env.filters.update({"link": link}) site.events.register("site-post-read", renderer.render_content)