Files
lektor-groupby-plugin/README.md
2022-03-25 17:21:02 +01:00

5.2 KiB

Lektor Plugin: @groupby

A generic grouping / clustering plugin. Can be used for tagging and similar tasks.

Usage: Simple Example

Lets start with a simple example: adding a tags field to your model. Assuming you have a blog-entry.ini that is used for all children of /blog path.

models/blog-entry.ini

[fields.tags]
label = Tags
type = strings
myvar = true

[fields.body]
label = Content
type = markdown

Notice we introduce a new attribute variable: myvar = true. The name can be anything here, we will come to that later. The only thing that matters is that the value is a boolean and set to true.

Edit your blog entry and add these two new tags:

Awesome
Latest News

Next, we need a plugin to add the groupby event listener.

packages/test/lektor_my_tags_plugin.py

def on_groupby_init(self, groupby, **extra):
    @groupby.watch('/blog', 'myvar', flatten=True, template='myvar.html',
                   slug='tag/{group}/index.html')
    def do_myvar(args):
        page = args.record  # extract additional info from source
        fieldKey, flowIndex, flowKey = args.key  # or get field index directly
        # val = page.get(fieldKey).blocks[flowIndex].get(flowKey)
        value = args.field  # list type since model is 'strings' type
        for tag in value:
            yield slugify(tag), {'val': tag, 'tags_in_page': len(value)}

There are a few important things here:

  1. The first parameter ('/blog') is the root page of the groupby. All results will be placed under this directory, e.g., /blog/tags/clean/. You can also just use /, in which case the same path would be /tags/clean/. Or create multiple listeners, one for /blog/ and another for /projects/, etc.
  2. The second parameter ('myvar') must be the same attribute variable we used in our blog-entry.ini model. The groupby plugin will traverse all models and search for this attribute name.
  3. Flatten determines how Flow elements are processed. If False, the callback function convert_myvar() is called once per Flow element (if the Flow element has the myvar attribute attached). If True (default), the callback is called for all Flow blocks individually.
  4. The template myvar.html is used to render the grouping page. This parameter is optional. If no explicit template is set, the default template groupby-myvar.html would be used. Where myvar is replaced with whatever attribute you chose.
  5. Finally, the slug tag/{group}/index.html is where the result is placed. The default value for this parameter is {attrib}/{group}/index.html. In our case, the default path would resolve to myvar/awesome/index.html. We explicitly chose to replace the default slug with our own, which ignores the attrib path component and instead puts the result pages inside the /tag directory. (PS: you could also use for example t/{group}.html, etc.)

So much for the args parameter. The callback body can produce groupings but does not have to. If you choose to produce an entry, you have to yield a tuple pair of (groupkey, extra-info). groupkey is used to combine & cluster pages and must be URL-safe. The extra-info is passed through to your template file. You can yield more than one entry per source or filter / ignore pages if you don't yield anything. Our simple example will generate the output files tag/awesome/index.html and tag/latest-news/index.html.

Lets take a look at the html next.

templates/myvar.html

<h2>Path: {{ this | url(absolute=True) }}</h2>
<div>This is: {{this}}</div>
<ul>
	{%- for child in this.children %}
	<li>Page: {{ child.record.path }}, Name: {{ child.extra.val }}, Tag count: {{ child.extra.tags_in_page }}</li>
	{%- endfor %}
</ul>

Notice, we can use child.record to access the referenced page of the group cluster. child.extra contains the additional information we previously passed into the template.

The final result of tag/latest-news/index.html:

Path: /tag/latest-news/
This is: <GroupBySource attribute="myvar" group="latest-news" template="myvar.html" slug="tag/latest-news/" children=1>
  -  Page: /blog/barss, Name: Latest News, Tag count: 2

Usage: A slightly more complex example

from lektor.markdown import Markdown
from lektor.types.formats import MarkdownDescriptor
from lektor.utils import slugify
import re
_regex = re.compile(r'{{([^}]{1,32})}}')

def on_groupby_init(self, groupby, **extra):
    @groupby.watch('/', 'inlinetags', slug='tags/{group}/')
    def convert_inlinetags(args):
        arr = args.field if isinstance(args.field, list) else [args.field]
        for obj in arr:
            if isinstance(obj, (Markdown, MarkdownDescriptor)):
                obj = obj.source
            if isinstance(obj, str) and str:
                for match in _regex.finditer(obj):
                    tag = match.group(1)
                    yield slugify(tag), tag

This will find all model fields with attribute inlinetags and search for in-text occurrences of {{Tagname}}, etc. This generic approach does not care what data-type the field value is: strings fields will be expanded and enumerated, Markdown will be unpacked. You can combine this mere tag-detector with text-replacements to point to the actual tags-page.