lektor-jinja-helpers


Namelektor-jinja-helpers JSON
Version 0.1a1 PyPI version JSON
download
home_page
SummaryAn assortment of Jinja filters, tests, and globals for Lektor
upload_time2023-08-23 20:33:17
maintainer
docs_urlNone
author
requires_python>=3.8
licenseBSD-3-Clause
keywords lektor plugin jinja filters jinja tests jinja globals
VCS
bugtrack_url
requirements No requirements were recorded.
Travis-CI No Travis.
coveralls test coverage No coveralls.
            # Lektor-jinja-helpers

[![Test status badge](https://img.shields.io/github/actions/workflow/status/dairiki/lektor-jinja-helpers/ci.yml?label=tests)](https://github.com/dairiki/lektor-jinja-helpers/actions/workflows/ci.yml)

This is a [Lektor plugin] that adds an assortment of filters, tests,
and globals to Lektor’s Jinja environment.

These additions are hopefully useful in Lektor templates.

Additionally, any [Ansible] filter or test plugins that may be
installed in the Python environment will also be made available.
Ansible provides a great variety of [filters][ansible filters] and
[tests][ansible tests], 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 the `helpers`
namespace. That is, all the names of these filters, tests and globals
start with `helpers.`

Any filters and tests from ansible plugins are made available under
their fully qualified names (e.g. `ansible.builtin.flatten`).

## Jinja Filters

### helpers.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., a [markdown field] is to be included on a
page that already has some heading. E.g. the following template would
ensure that the headings with `div.markdown-body` start correctly at
`<h2>`.

```j2
<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 passing `normalize=false` to
the filter.

Then, optionally, the filter *demotes* (increases the level) of each
heading by a fixed amount. The value of the `demote` argument
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, an [aria-level] attribute 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:

1. This function first looks for an HTML comment whose text matches
   the regular expression **cut-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.

   Passing `cut_mark=none` will disable the search for a cut-mark.

2. If no cut-mark is found, then the text is truncated at the first
   block element such that there are at least ``min_words`` words 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 the [excerpt-html] library.


### helpers.lineage(include\_self=True)

This filter expects a Lektor DB *source object* as input, and returns
a generator generator that yields first (optionally) the input
object, then the object's parent, and so on up the tree.

The `include_self` parameter 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:

```j2
{% 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.

The `include_self` parameter controls whether the input page
is included in the results.

The `depth_first` parameter controls the traversal order.  By
default, the traversal is breadth-first.

The `include_hidden` and `include_undiscoverable` parameters 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 set `include_undiscoverable` as well as `include_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.)

```j2
<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.flatten

will return a generator that will yield ``"foo"``, ``"bar"``, ``"baz"``.

The ``depth`` parameter (if not ``None``) limits the maximum depth of
flattening performed.  If ``depth`` is less than or equal to zero, no flattening
is performed.

Strings and ``Mapping``s (though, technically, they are *iterables*)
are not flattened.

### helpers.call(function, \*args, \*\*kwargs)

This helper can be used to convert a global function to a [jinja
filter]. This is primarily useful when one would like to use a global
function in a [`map`][map filter] filter.

As a contrived example

```j2
{% for r in range(3) | map("helpers.call", range, 4) -%}
  {{ r | join(",") }}
{% endfor -%}
```

will produce

```
0,1,2,3
1,2,3
2,3
```

## Jinja Tests

### helpers.call(function, \*args, \*\*kwargs)

`Helpers.call` can 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 a [`select`][select filter] filter or one of its
relatives.

Another contrived example:

```j2
{% set isupper = "".__class__.isupper -%}
{{ ["lower", "UPPER"] | select("helpers.call", isupper) | join(",") }}
```

will produce `"UPPER"`.

## Jinja Globals

### helpers.import\_module(name, package=none)

[`Importlib.import_module`][import-module] from the Python standard library is made available to Jinja templates as `helpers.import_module`. This foot-gun allows access to nearly any python value from within a template.

E.g., to access the current date

```j2
{% set date = helpers.import_module("datetime").date -%}
{{ date.today().isoformat() }}
```

## Ansible Filter and Test Plugins

If [Ansible] is installed in the Python environment, whatever ansible
[filter][ansible filters] and [test][ansible tests] plugins that can
be found will be made available to the Jinja environment.

Installing [ansible-core] (total install size ~30M) in your virtualenv
will provide access to the filters and tests from the
`ansible.builtin` module.  Installing [ansible] (total install size
~500M) will provide access to all the standard modules.

## Ideas / To Do

- Perhaps the *namespacing* of all new features under the `helpers.`
  prefix should be made configurable.

- It would be nice to avoid the weight of [BeautifulSoup4] and
  [html5lib] if possible.  (But note that [excerpt-html] currently
  uses both of those libraries.)

- Perhaps, instead of the `helpers.descandants` filter, a
  `descendants_query` global, which returns a `Query` object (with
  `.filter`, `.include_undiscoverable`, &c. methods) would present a
  more Lektor-uniform API

## Author

Jeff Dairiki <dairiki@dairiki.org>


[Lektor]: <https://pypi.org/project/lektor/>
[Jinja]: <https://jinja.palletsprojects.com/en/3.1.x/>
[Lektor plugin]: <https://www.getlektor.com/docs/plugins/>
[markdown field]: <https://www.getlektor.com/docs/api/db/types/markdown/>
[aria-level]: <https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-level>
[html5lib]: <https://pypi.org/project/html5lib/>
[BeautifulSoup4]: <https://pypi.org/project/beautifulsoup4/>
[excerpt-html]: <https://pypi.org/project/excerpt-html/> (Excerpt-html at PyPI)
[jinja filter]: <https://jinja.palletsprojects.com/en/3.1.x/templates/#id11>
[map filter]: <https://jinja.palletsprojects.com/en/3.1.x/templates/#jinja-filters.map>
[select filter]: <https://jinja.palletsprojects.com/en/3.1.x/templates/#jinja-filters.select>
[import-module]: <https://docs.python.org/3/library/importlib.html#importlib.import_module>

[Ansible]: <https://pypi.org/project/ansible/>
[ansible-core]: <https://pypi.org/project/ansible-core/>
[ansible filters]: <https://docs.ansible.com/ansible/latest/collections/index_filter.html>
[ansible tests]: <https://docs.ansible.com/ansible/latest/collections/index_test.html>

            

Raw data

            {
    "_id": null,
    "home_page": "",
    "name": "lektor-jinja-helpers",
    "maintainer": "",
    "docs_url": null,
    "requires_python": ">=3.8",
    "maintainer_email": "",
    "keywords": "lektor plugin jinja filters jinja tests jinja globals",
    "author": "",
    "author_email": "Jeff Dairiki <dairiki@dairiki.org>",
    "download_url": "https://files.pythonhosted.org/packages/97/2e/9541eb42e4beecf5c8a54921a480f3e71e22094c273b14e6e64c14cda4b8/lektor_jinja_helpers-0.1a1.tar.gz",
    "platform": null,
    "description": "# Lektor-jinja-helpers\n\n[![Test status badge](https://img.shields.io/github/actions/workflow/status/dairiki/lektor-jinja-helpers/ci.yml?label=tests)](https://github.com/dairiki/lektor-jinja-helpers/actions/workflows/ci.yml)\n\nThis is a [Lektor plugin] that adds an assortment of filters, tests,\nand globals to Lektor\u2019s Jinja environment.\n\nThese additions are hopefully useful in Lektor templates.\n\nAdditionally, any [Ansible] filter or test plugins that may be\ninstalled in the Python environment will also be made available.\nAnsible provides a great variety of [filters][ansible filters] and\n[tests][ansible tests], some of which may be useful in Lektor\ntemplates.\n\n## \u201cNamespacing\u201d\n\nTo avoid namespace pollution, currently, all of the bits added to the\njinja environment by this plugin are added under the `helpers`\nnamespace. That is, all the names of these filters, tests and globals\nstart with `helpers.`\n\nAny filters and tests from ansible plugins are made available under\ntheir fully qualified names (e.g. `ansible.builtin.flatten`).\n\n## Jinja Filters\n\n### helpers.adjust\\_heading\\_levels(html\\_text, demote=0, normalize=True)\n\nThis filter expects HTML text as input. It can be used both to\nnormalize the HTML heading hierarchy in the text, as well as to\n\u201cdemote\u201d the heading levels by a fixed amount.\n\nThis is useful when, e.g., a [markdown field] is to be included on a\npage that already has some heading. E.g. the following template would\nensure that the headings with `div.markdown-body` start correctly at\n`<h2>`.\n\n```j2\n<main>\n  <h1>{{ this.title }}</h1>\n  <div class=\"markdown-body\">\n  {{ this.body | helpers.adjust_heading_levels(demote=1) }}\n  <div>\n</main>\n```\n\nFirst (by default) it normalizes the HTML heading levels in the text, so that:\n\n- The first heading is always `<h1>`.\n\n- There are no \"gaps\" in the heading hierarchy: for every heading of\n  depth greater than `<h1>` there will exist a parent heading of the\n  preceding level. E.g. for every `<h3>` in the normalized text, there\n  will be an extant `<h2>` parent preceding it.\n\nThis normalization may be prevented by passing `normalize=false` to\nthe filter.\n\nThen, optionally, the filter *demotes* (increases the level) of each\nheading by a fixed amount. The value of the `demote` argument\nspecifies the number of levels the headings are to be increased.\n\nHeadings are never demoted below `<h6>`. This is because `<h7>` is not\na valid HTML5 element. Instead, an [aria-level] attribute is set on\ndeeply demoted headings: in place of `<h7>`, an `<h6 aria-level=\"7\">`\nis used.\n\n### helpers.excerpt\\_html(html\\_text, min\\_words=50, cut\\_mark=r\"(?i)\\s*more\\b\")\n\nThis filter expects HTML text as input and returns a possibly truncated\nversion of that text. It truncates the input in one of two ways:\n\n1. This function first looks for an HTML comment whose text matches\n   the regular expression **cut-mark**. (The default value matches\n   comments beginning with the word \u201cmore\u201d.) If such a comment is\n   found, all text after that comment is deleted, and the result is\n   returned. The truncation is done in an HTML-aware manner. The\n   result will be a valid HTML fragment: it will not contain dangling\n   tags, etc.\n\n   Passing `cut_mark=none` will disable the search for a cut-mark.\n\n2. If no cut-mark is found, then the text is truncated at the first\n   block element such that there are at least ``min_words`` words in\n   the preserved (preceding) text.\n\nIf no suitable truncation point can be found, the original text is returned.\n\nThis filter provides a thin wrapper around the [excerpt-html] library.\n\n\n### helpers.lineage(include\\_self=True)\n\nThis filter expects a Lektor DB *source object* as input, and returns\na generator generator that yields first (optionally) the input\nobject, then the object's parent, and so on up the tree.\n\nThe `include_self` parameter controls whether the input object is\nincluded in the results.\n\nAs an example, this can be used to determine if any of a pages ancestors is undiscoverable:\n\n```j2\n{% if this | helpers.lineage | rejectattr('is_discoverable') | first is defined -%}\n  This page or one of its ancestors is marked undiscoverable!\n{% endif -%}\n```\n\n### helpers.descendants(include\\_undiscoverable=False, include\\_hidden=False, include\\_self=True, depth\\_first=False)\n\nIterate over descendant pages.\n\nThis Jinja filter expects a Lektor Page as input and returns a\ngenerator which yields first (optionally) the input page, then the\npage's descendants.\n\nThe `include_self` parameter controls whether the input page\nis included in the results.\n\nThe `depth_first` parameter controls the traversal order.  By\ndefault, the traversal is breadth-first.\n\nThe `include_hidden` and `include_undiscoverable` parameters control\nwhether hidden and undiscoverable pages are included in the result.\nNote that, since hidden pages are always undiscoverable, to include hidden pages,\none must set `include_undiscoverable` as well as `include_hidden`.\n\nAs an example, here we iterate over all images contained by all discoverable\npages on the site. (This may be a slow operation if there are many pages and/or images.)\n\n```j2\n<ul>\n  {% for image in site.root | helpers.descendants | map(attribute=\"attachments.images\") | helpers.flatten -%}\n    <li><img src=\"{{ image | url }}\"></li>\n  {% endfor -%}\n</ul>\n```\n\n### helpers.flatten(depth=None)\n\nFlatten a nested structure of iterables.\n\nThis filters expects an iterable as input and returns a generator.\nAny iterables contained within the input will be flattened to the top level.\n\nAs an example,\n\n    [[\"foo\", \"bar\"], [\"baz\"]] | helpers.flatten\n\nwill return a generator that will yield ``\"foo\"``, ``\"bar\"``, ``\"baz\"``.\n\nThe ``depth`` parameter (if not ``None``) limits the maximum depth of\nflattening performed.  If ``depth`` is less than or equal to zero, no flattening\nis performed.\n\nStrings and ``Mapping``s (though, technically, they are *iterables*)\nare not flattened.\n\n### helpers.call(function, \\*args, \\*\\*kwargs)\n\nThis helper can be used to convert a global function to a [jinja\nfilter]. This is primarily useful when one would like to use a global\nfunction in a [`map`][map filter] filter.\n\nAs a contrived example\n\n```j2\n{% for r in range(3) | map(\"helpers.call\", range, 4) -%}\n  {{ r | join(\",\") }}\n{% endfor -%}\n```\n\nwill produce\n\n```\n0,1,2,3\n1,2,3\n2,3\n```\n\n## Jinja Tests\n\n### helpers.call(function, \\*args, \\*\\*kwargs)\n\n`Helpers.call` can also be used to convert a global function to a\nJinja test.  This is useful when one would like to use a global\nfunction in a [`select`][select filter] filter or one of its\nrelatives.\n\nAnother contrived example:\n\n```j2\n{% set isupper = \"\".__class__.isupper -%}\n{{ [\"lower\", \"UPPER\"] | select(\"helpers.call\", isupper) | join(\",\") }}\n```\n\nwill produce `\"UPPER\"`.\n\n## Jinja Globals\n\n### helpers.import\\_module(name, package=none)\n\n[`Importlib.import_module`][import-module] from the Python standard library is made available to Jinja templates as `helpers.import_module`. This foot-gun allows access to nearly any python value from within a template.\n\nE.g., to access the current date\n\n```j2\n{% set date = helpers.import_module(\"datetime\").date -%}\n{{ date.today().isoformat() }}\n```\n\n## Ansible Filter and Test Plugins\n\nIf [Ansible] is installed in the Python environment, whatever ansible\n[filter][ansible filters] and [test][ansible tests] plugins that can\nbe found will be made available to the Jinja environment.\n\nInstalling [ansible-core] (total install size ~30M) in your virtualenv\nwill provide access to the filters and tests from the\n`ansible.builtin` module.  Installing [ansible] (total install size\n~500M) will provide access to all the standard modules.\n\n## Ideas / To Do\n\n- Perhaps the *namespacing* of all new features under the `helpers.`\n  prefix should be made configurable.\n\n- It would be nice to avoid the weight of [BeautifulSoup4] and\n  [html5lib] if possible.  (But note that [excerpt-html] currently\n  uses both of those libraries.)\n\n- Perhaps, instead of the `helpers.descandants` filter, a\n  `descendants_query` global, which returns a `Query` object (with\n  `.filter`, `.include_undiscoverable`, &c. methods) would present a\n  more Lektor-uniform API\n\n## Author\n\nJeff Dairiki <dairiki@dairiki.org>\n\n\n[Lektor]: <https://pypi.org/project/lektor/>\n[Jinja]: <https://jinja.palletsprojects.com/en/3.1.x/>\n[Lektor plugin]: <https://www.getlektor.com/docs/plugins/>\n[markdown field]: <https://www.getlektor.com/docs/api/db/types/markdown/>\n[aria-level]: <https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-level>\n[html5lib]: <https://pypi.org/project/html5lib/>\n[BeautifulSoup4]: <https://pypi.org/project/beautifulsoup4/>\n[excerpt-html]: <https://pypi.org/project/excerpt-html/> (Excerpt-html at PyPI)\n[jinja filter]: <https://jinja.palletsprojects.com/en/3.1.x/templates/#id11>\n[map filter]: <https://jinja.palletsprojects.com/en/3.1.x/templates/#jinja-filters.map>\n[select filter]: <https://jinja.palletsprojects.com/en/3.1.x/templates/#jinja-filters.select>\n[import-module]: <https://docs.python.org/3/library/importlib.html#importlib.import_module>\n\n[Ansible]: <https://pypi.org/project/ansible/>\n[ansible-core]: <https://pypi.org/project/ansible-core/>\n[ansible filters]: <https://docs.ansible.com/ansible/latest/collections/index_filter.html>\n[ansible tests]: <https://docs.ansible.com/ansible/latest/collections/index_test.html>\n",
    "bugtrack_url": null,
    "license": "BSD-3-Clause",
    "summary": "An assortment of Jinja filters, tests, and globals for Lektor",
    "version": "0.1a1",
    "project_urls": {
        "Homepage": "https://github.com/dairiki/lektor-jinja-helpers"
    },
    "split_keywords": [
        "lektor",
        "plugin",
        "jinja",
        "filters",
        "jinja",
        "tests",
        "jinja",
        "globals"
    ],
    "urls": [
        {
            "comment_text": "",
            "digests": {
                "blake2b_256": "e44cbeb380dde39c41795135bfe061b1bf5784831e528761c9c4642387a68433",
                "md5": "c9a00316a78fa0b0bbc4bf671051f0f6",
                "sha256": "2f32376a2ec00da7658493bd5b25255e1d08314e6d36d5ed3e73c6268dc0973b"
            },
            "downloads": -1,
            "filename": "lektor_jinja_helpers-0.1a1-py3-none-any.whl",
            "has_sig": false,
            "md5_digest": "c9a00316a78fa0b0bbc4bf671051f0f6",
            "packagetype": "bdist_wheel",
            "python_version": "py3",
            "requires_python": ">=3.8",
            "size": 12132,
            "upload_time": "2023-08-23T20:33:16",
            "upload_time_iso_8601": "2023-08-23T20:33:16.775834Z",
            "url": "https://files.pythonhosted.org/packages/e4/4c/beb380dde39c41795135bfe061b1bf5784831e528761c9c4642387a68433/lektor_jinja_helpers-0.1a1-py3-none-any.whl",
            "yanked": false,
            "yanked_reason": null
        },
        {
            "comment_text": "",
            "digests": {
                "blake2b_256": "972e9541eb42e4beecf5c8a54921a480f3e71e22094c273b14e6e64c14cda4b8",
                "md5": "e163c3c22dfdf2cf51d6dcc0420286b3",
                "sha256": "c060d7f6d0e1cc3d8c9ef30c6052bff27ff0b75bbe34b9e5399423acc277d59f"
            },
            "downloads": -1,
            "filename": "lektor_jinja_helpers-0.1a1.tar.gz",
            "has_sig": false,
            "md5_digest": "e163c3c22dfdf2cf51d6dcc0420286b3",
            "packagetype": "sdist",
            "python_version": "source",
            "requires_python": ">=3.8",
            "size": 16846,
            "upload_time": "2023-08-23T20:33:17",
            "upload_time_iso_8601": "2023-08-23T20:33:17.998113Z",
            "url": "https://files.pythonhosted.org/packages/97/2e/9541eb42e4beecf5c8a54921a480f3e71e22094c273b14e6e64c14cda4b8/lektor_jinja_helpers-0.1a1.tar.gz",
            "yanked": false,
            "yanked_reason": null
        }
    ],
    "upload_time": "2023-08-23 20:33:17",
    "github": true,
    "gitlab": false,
    "bitbucket": false,
    "codeberg": false,
    "github_user": "dairiki",
    "github_project": "lektor-jinja-helpers",
    "travis_ci": false,
    "coveralls": false,
    "github_actions": true,
    "tox": true,
    "lcname": "lektor-jinja-helpers"
}
        
Elapsed time: 0.10648s