Package: lektor-jinja-helpers
Lektor-jinja-helpersThis is aLektor pluginthat adds an assortment of filters, tests,
and globals to Lektor’s Jinja environment.These additions are hopefully useful in Lektor templates.Additionally, anyAnsiblefilter or test plugins that may be
installed in the Python environment will also be made available.
Ansible provides a great variety offiltersandtests, some of which may be useful in Lektor
templates.“Namespacing”To avoid namespace pollution, currently, all of the bits added to the
jinja environment by this plugin are added under thehelpersnamespace. That is, all the names of these filters, tests and globals
start withhelpers.Any filters and tests from ansible plugins are made available under
their fully qualified names (e.g.ansible.builtin.flatten).Jinja Filtershelpers.adjust_heading_levels(html_text, demote=0, normalize=True)This filter expects HTML text as input. It can be used both to
normalize the HTML heading hierarchy in the text, as well as to
“demote” the heading levels by a fixed amount.This is useful when, e.g., amarkdown fieldis to be included on a
page that already has some heading. E.g. the following template would
ensure that the headings withdiv.markdown-bodystart correctly at<h2>.<main>
  <h1>{{ this.title }}</h1>
  <div class="markdown-body">
  {{ this.body | helpers.adjust_heading_levels(demote=1) }}
  <div>
</main>First (by default) it normalizes the HTML heading levels in the text, so that:The first heading is always<h1>.There are no "gaps" in the heading hierarchy: for every heading of
depth greater than<h1>there will exist a parent heading of the
preceding level. E.g. for every<h3>in the normalized text, there
will be an extant<h2>parent preceding it.This normalization may be prevented by passingnormalize=falseto
the filter.Then, optionally, the filterdemotes(increases the level) of each
heading by a fixed amount. The value of thedemoteargument
specifies the number of levels the headings are to be increased.Headings are never demoted below<h6>. This is because<h7>is not
a valid HTML5 element. Instead, anaria-levelattribute is set on
deeply demoted headings: in place of<h7>, an<h6 aria-level="7">is used.helpers.excerpt_html(html_text, min_words=50, cut_mark=r"(?i)\s*more\b")This filter expects HTML text as input and returns a possibly truncated
version of that text. It truncates the input in one of two ways:This function first looks for an HTML comment whose text matches
the regular expressioncut-mark. (The default value matches
comments beginning with the word “more”.) If such a comment is
found, all text after that comment is deleted, and the result is
returned. The truncation is done in an HTML-aware manner. The
result will be a valid HTML fragment: it will not contain dangling
tags, etc.Passingcut_mark=nonewill disable the search for a cut-mark.If no cut-mark is found, then the text is truncated at the first
block element such that there are at leastmin_wordswords in
the preserved (preceding) text.If no suitable truncation point can be found, the original text is returned.This filter provides a thin wrapper around theexcerpt-htmllibrary.helpers.lineage(include_self=True)This filter expects a Lektor DBsource objectas input, and returns
a generator generator that yields first (optionally) the input
object, then the object's parent, and so on up the tree.Theinclude_selfparameter controls whether the input object is
included in the results.As an example, this can be used to determine if any of a pages ancestors is undiscoverable:{% if this | helpers.lineage | rejectattr('is_discoverable') | first is defined -%}
  This page or one of its ancestors is marked undiscoverable!
{% endif -%}helpers.descendants(include_undiscoverable=False, include_hidden=False, include_self=True, depth_first=False)Iterate over descendant pages.This Jinja filter expects a Lektor Page as input and returns a
generator which yields first (optionally) the input page, then the
page's descendants.Theinclude_selfparameter controls whether the input page
is included in the results.Thedepth_firstparameter controls the traversal order.  By
default, the traversal is breadth-first.Theinclude_hiddenandinclude_undiscoverableparameters control
whether hidden and undiscoverable pages are included in the result.
Note that, since hidden pages are always undiscoverable, to include hidden pages,
one must setinclude_undiscoverableas well asinclude_hidden.As an example, here we iterate over all images contained by all discoverable
pages on the site. (This may be a slow operation if there are many pages and/or images.)<ul>
  {% for image in site.root | helpers.descendants | map(attribute="attachments.images") | helpers.flatten -%}
    <li><img src="{{ image | url }}"></li>
  {% endfor -%}
</ul>helpers.flatten(depth=None)Flatten a nested structure of iterables.This filters expects an iterable as input and returns a generator.
Any iterables contained within the input will be flattened to the top level.As an example,[["foo", "bar"], ["baz"]] | helpers.flattenwill return a generator that will yield"foo","bar","baz".Thedepthparameter (if notNone) limits the maximum depth of
flattening performed.  Ifdepthis less than or equal to zero, no flattening
is performed.Strings andMappings (though, technically, they areiterables)
are not flattened.helpers.call(function, *args, **kwargs)This helper can be used to convert a global function to ajinja
filter. This is primarily useful when one would like to use a global
function in amapfilter.As a contrived example{% for r in range(3) | map("helpers.call", range, 4) -%}
  {{ r | join(",") }}
{% endfor -%}will produce0,1,2,3
1,2,3
2,3Jinja Testshelpers.call(function, *args, **kwargs)Helpers.callcan also be used to convert a global function to a
Jinja test.  This is useful when one would like to use a global
function in aselectfilter or one of its
relatives.Another contrived example:{% set isupper = "".__class__.isupper -%}
{{ ["lower", "UPPER"] | select("helpers.call", isupper) | join(",") }}will produce"UPPER".Jinja Globalshelpers.import_module(name, package=none)Importlib.import_modulefrom the Python standard library is made available to Jinja templates ashelpers.import_module. This foot-gun allows access to nearly any python value from within a template.E.g., to access the current date{% set date = helpers.import_module("datetime").date -%}
{{ date.today().isoformat() }}Ansible Filter and Test PluginsIfAnsibleis installed in the Python environment, whatever ansiblefilterandtestplugins that can
be found will be made available to the Jinja environment.Installingansible-core(total install size ~30M) in your virtualenv
will provide access to the filters and tests from theansible.builtinmodule.  Installingansible(total install size
~500M) will provide access to all the standard modules.Ideas / To DoPerhaps thenamespacingof all new features under thehelpers.prefix should be made configurable.It would be nice to avoid the weight ofBeautifulSoup4andhtml5libif possible.  (But note thatexcerpt-htmlcurrently
uses both of those libraries.)Perhaps, instead of thehelpers.descandantsfilter, adescendants_queryglobal, which returns aQueryobject (with.filter,.include_undiscoverable, &c. methods) would present a
more Lektor-uniform APIMake defaults (e.g. forexcerpt_html) configurable via Lektor plugin config file.AuthorJeff Dairikidairiki@dairiki.org