diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..7dd95dc --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +examples/** linguist-documentation diff --git a/examples/Examples.lektorproject b/examples/Examples.lektorproject new file mode 100644 index 0000000..9e6763f --- /dev/null +++ b/examples/Examples.lektorproject @@ -0,0 +1,5 @@ +[project] +name = GroupBy Examples + +[packages] +lektor-groupby = 0.9.1 diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 0000000..89e4914 --- /dev/null +++ b/examples/README.md @@ -0,0 +1,209 @@ +# Usage + +Overview: +- the [quick config](#quick-config) example shows how you can use the plugin config to setup a quick and easy tagging system. +- the [simple example](#simple-example) goes into detail how this plugin works. +- the [advanced example](#advanced-example) touches on the potentials of the plugin. + + + +## About + +To use the groupby plugin you have to add an attribute to your model file. +In our case you can refer to the `models/page.ini` model: + +```ini +[fields.tags] +label = Tags +type = strings +testA = true +testB = true + +[fields.body] +label = Body +type = markdown +testC = true +``` + +We did define three custom attributes `testA`, `testB`, and `testC`. +You may add custom attributes to all of the fields. +It is crucial that the value of the custom attribute is set to true. +The attribute name is later used for grouping. + + + +## Quick config + +Relevant files: +``` +configs/groupby.ini +templates/example-config.html +``` + +The easiest way to add tags to your site is by defining the `groupby.ini` config file. + +```ini +[testA] +root = / +slug = config/{group}.html +template = example-config.html +split = ' ' +``` + +The configuration parameter are: + +1. The section title (`testA`) must be one of the attribute variables we defined in our model. +2. The `root` parameter (`/`) is the root page of the groupby. + All results will be placed under this directory, e.g., `/tags/tagname/`. + If you use `root = /blog`, the results path will be `/blog/tags/tagname/`. + The groupby plugin will traverse all sub-pages wich contain the attribute `testA`. +3. The `slug` parameter (`config/{group}.html`) is where the results are placed. + In our case, the path resolves to `config/tagname.html`. + The default value is `{attrib}/{group}/index.html` which would resolve to `testA/tagname/index.html`. +4. The `template`parameter (`example-config.html`) is used to render the results page. + If no explicit template is set, the default template `groupby-testA.html` will be used. + Where `testA` is replaced with whatever attribute you chose. +5. The `split` parameter (`' '`) will be used as string delimiter. + Fields of type `strings` and `checkboxes` are already lists and don't need splitting. + The split is only relevant for fields of type `string` or `text`. + These single-line fields are then expanded to lists as well. + If you do not provide the `split` option, the whole field value will be used as tagname. + + +You can have multiple listeners, e.g., one for `/blog/` and another for `/projects/`. +Just create as many custom attributes as you like, each having its own section. + +In your template file you have access to the children (pages) and their tags. +The emitted `extras` for the child is a list of original tagnames. + +```jinja2 +{%- for child, extras in this.children.items() %} +
  • Page: {{ child.path }}, Tags: {{ extras }}
  • +{%- endfor %} +``` + + + +## Simple example + +Relevant files: +``` +packages/simple-example/lektor_simple.py +templates/example-simple.html +``` + +```python +def on_groupby_after_build_all(self, groupby, builder, **extra): + @groupby.watch('/blog', 'testB', slug='simple/{group}/index.html', + template='example-simple.html', flatten=True) + def convert_simple_example(args): + value = args.field # list, since model is 'strings' type + for tag in value: + yield slugify(tag), {'val': tag, 'tags_in_page': len(value)} + + # page = args.record # extract additional info from source + # fieldKey, flowIndex, flowKey = args.key # or get field index + # if flowIndex is None: + # obj = page[fieldKey] + # else: + # obj = page[fieldKey].blocks[flowIndex].get(flowKey) +``` + +This example is roughly equivalent to the config file example. +The parameters of the `@groupby.watch` function (`root`, `attribute`, `slug`, `template`) correspond to the same config parameters described above. +There is a new `flatten` parameter: + +- Flatten determines how Flow elements are processed. + If `False`, the callback function is called once per Flow element. + If `True` (default), the callback is called for all Flow-Blocks of the Flow individually. + The attribute `testB` can be attached to either the Flow or a Flow-Block regardless. + +The `args` parameter of the `convert_simple_example()` function is a named tuple, it has three attributes: + +1. The `record` points to the `Page` source which contains the tag. +2. The `key` tuple `(field-key, flow-index, flow-key)` tells which field is processed. + For Flow types, `flow-index` and `flow-key` are set, otherwise they are `None`. +3. The `field` value is the content of the processed field. + The field value is reoughly equivalent to the following: + +```python +args.page[fieldKey].blocks[flowIndex].get(flowKey) +``` + +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. + +The template file can access and display the `extra-info`: + +```jinja2 +{%- for child, extras in this.children.items() %} +Page: {{ child.title }} + +{%- endfor %} +``` + + + +## Advanced example + +Relevant files: +``` +configs/advanced.ini +packages/advanced-example/lektor_advanced.py +templates/example-advanced.html +``` + +The following example is similar to the previous one. +Except that it loads a config file and replaces in-text occurrences of `{{Tagname}}` with `Tagname`. + +```python +def on_groupby_before_build_all(self, groupby, builder, **extra): + # load config + regex = re.compile(self.get_config().get('match')) + # since we load and use a config file, we need to track the dependency + @groupby.depends_on(self.config_filename) + @groupby.watch('/', 'testC', slug='advanced/{group}/', + template='example-advanced.html') + def convert_replace_example(args): + # args.field assumed to be Markdown + obj = args.field.source + for match in regex.finditer(obj): + tag = match.group(1) + yield slugify(tag), tag + + def _fn(match: re.Match) -> str: + tag = match.group(1) + return f'{tag}' + args.field.source = regex.sub(_fn, obj) +``` + +One **important** thing to notice is, we use `on_groupby_before_build_all` to register our callback function. +This is required because we would like to modify the source **before** it is written to disk. +If you look back to the [simple example](#simple-example), we used `on_groupby_after_build_all` because we did not care when it is executed. +Generally, it makes little difference which one you use (`on-after` is likely less busy). +Just know that you can process the source before or after it is build. + +For Markdown fields, we can modify the `source` attribute directly. +All other field typed need to be accessed via `args.record` key indirection. + +```ini +match = {{([^}]{1,32})}} +``` + +Lastly, the config file contains a regular expression which matches `{{` + any string less than 32 characters + `}}`. +Notice, the parenthesis (`()`) will match the inner part but the replace function (`re.sub`) will remove the `{{}}` too. + +If the user changes the regex pattern in the config file, we need to rebuild all tags. +For this purpose we need to track changes to the config file. +This is done by calling: + +```python +@groupby.depends_on(file1, file2, ...) +``` diff --git a/examples/configs/advanced.ini b/examples/configs/advanced.ini new file mode 100644 index 0000000..041a182 --- /dev/null +++ b/examples/configs/advanced.ini @@ -0,0 +1 @@ +match = {{([^}]{1,32})}} \ No newline at end of file diff --git a/examples/configs/groupby.ini b/examples/configs/groupby.ini new file mode 100644 index 0000000..bb447ac --- /dev/null +++ b/examples/configs/groupby.ini @@ -0,0 +1,5 @@ +[testA] +root = / +slug = config/{group}.html +template = example-config.html +split = ' ' diff --git a/examples/content/blog/contents.lr b/examples/content/blog/contents.lr new file mode 100644 index 0000000..294ee6a --- /dev/null +++ b/examples/content/blog/contents.lr @@ -0,0 +1,9 @@ +_model: blog +--- +title: Blog +--- +tags: + +Directory +Blog +samegroup diff --git a/examples/content/blog/first-post/contents.lr b/examples/content/blog/first-post/contents.lr new file mode 100644 index 0000000..184db78 --- /dev/null +++ b/examples/content/blog/first-post/contents.lr @@ -0,0 +1,9 @@ +title: Hello Website +--- +body: This is an example blog post. {{Tag}} in {{blog}} +--- +tags: + +Initial +Blog Post +samegroup diff --git a/examples/content/contents.lr b/examples/content/contents.lr new file mode 100644 index 0000000..ac2ad0d --- /dev/null +++ b/examples/content/contents.lr @@ -0,0 +1,8 @@ +title: GroupBy Examples +--- +body: Main body {{tag}} {{Two}} +--- +tags: + +Root +samegroup diff --git a/examples/content/projects/contents.lr b/examples/content/projects/contents.lr new file mode 100644 index 0000000..f3658c6 --- /dev/null +++ b/examples/content/projects/contents.lr @@ -0,0 +1,9 @@ +title: Projects +--- +body: + +This is a list of the projects: + +* Project 1 +* Project 2 +* Project 3 diff --git a/examples/models/blog-post.ini b/examples/models/blog-post.ini new file mode 100644 index 0000000..42b4554 --- /dev/null +++ b/examples/models/blog-post.ini @@ -0,0 +1,4 @@ +[model] +name = Blog Post +inherits = page +hidden = yes diff --git a/examples/models/blog.ini b/examples/models/blog.ini new file mode 100644 index 0000000..7026a3a --- /dev/null +++ b/examples/models/blog.ini @@ -0,0 +1,7 @@ +[model] +name = Blog +inherits = page +hidden = yes + +[children] +model = blog-post diff --git a/examples/models/page.ini b/examples/models/page.ini new file mode 100644 index 0000000..20e7173 --- /dev/null +++ b/examples/models/page.ini @@ -0,0 +1,18 @@ +[model] +name = Page +label = {{ this.title }} + +[fields.title] +label = Title +type = string + +[fields.tags] +label = Tags +type = strings +testA = true +testB = true + +[fields.body] +label = Body +type = markdown +testC = true diff --git a/examples/packages/advanced-example/lektor_advanced.py b/examples/packages/advanced-example/lektor_advanced.py new file mode 100644 index 0000000..5bb2942 --- /dev/null +++ b/examples/packages/advanced-example/lektor_advanced.py @@ -0,0 +1,31 @@ +# -*- coding: utf-8 -*- +from lektor.pluginsystem import Plugin +from lektor.utils import slugify +import re + + +class AdvancedGroupByPlugin(Plugin): + def on_groupby_before_build_all(self, groupby, builder, **extra): + # load config + regex = self.get_config().get('match') + try: + regex = re.compile(regex) + except Exception as e: + print('inlinetags.regex not valid: ' + str(e)) + return + + # since we load and use a config file, we need to track the dependency + @groupby.depends_on(self.config_filename) + @groupby.watch('/', 'testC', slug='advanced/{group}/', + template='example-advanced.html') + def convert_replace_example(args): + # args.field assumed to be Markdown + obj = args.field.source + for match in regex.finditer(obj): + tag = match.group(1) + yield slugify(tag), tag + + def _fn(match: re.Match) -> str: + tag = match.group(1) + return f'{tag}' + args.field.source = regex.sub(_fn, obj) diff --git a/examples/packages/advanced-example/setup.py b/examples/packages/advanced-example/setup.py new file mode 100644 index 0000000..d1b1e46 --- /dev/null +++ b/examples/packages/advanced-example/setup.py @@ -0,0 +1,12 @@ +from setuptools import setup + +setup( + name='lektor-advanced', + py_modules=['lektor_advanced'], + version='1.0', + entry_points={ + 'lektor.plugins': [ + 'advanced = lektor_advanced:AdvancedGroupByPlugin', + ] + } +) diff --git a/examples/packages/simple-example/lektor_simple.py b/examples/packages/simple-example/lektor_simple.py new file mode 100644 index 0000000..61490ae --- /dev/null +++ b/examples/packages/simple-example/lektor_simple.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +from lektor.pluginsystem import Plugin +from lektor.utils import slugify + + +class SimpleGroupByPlugin(Plugin): + def on_groupby_after_build_all(self, groupby, builder, **extra): + @groupby.watch('/blog', 'testB', slug='simple/{group}/index.html', + template='example-simple.html', flatten=True) + def convert_simple_example(args): + # Yield groups + value = args.field # list type since model is 'strings' type + for tag in value: + yield slugify(tag), {'val': tag, 'tags_in_page': len(value)} + # Everything below is just for documentation purposes + page = args.record # extract additional info from source + fieldKey, flowIndex, flowKey = args.key # or get field index + if flowIndex is None: + obj = page[fieldKey] + else: + obj = page[fieldKey].blocks[flowIndex].get(flowKey) + print('page:', page) + print(' obj:', obj) + print() diff --git a/examples/packages/simple-example/setup.py b/examples/packages/simple-example/setup.py new file mode 100644 index 0000000..850c710 --- /dev/null +++ b/examples/packages/simple-example/setup.py @@ -0,0 +1,12 @@ +from setuptools import setup + +setup( + name='lektor-simple', + py_modules=['lektor_simple'], + version='1.0', + entry_points={ + 'lektor.plugins': [ + 'simple = lektor_simple:SimpleGroupByPlugin', + ] + } +) diff --git a/examples/templates/blog-post.html b/examples/templates/blog-post.html new file mode 100644 index 0000000..3109b22 --- /dev/null +++ b/examples/templates/blog-post.html @@ -0,0 +1,4 @@ +{% extends "page.html" %} +{% block body %} + {{ this.body }} +{% endblock %} diff --git a/examples/templates/blog.html b/examples/templates/blog.html new file mode 100644 index 0000000..15eaa7f --- /dev/null +++ b/examples/templates/blog.html @@ -0,0 +1,6 @@ +{% extends "page.html" %} +{% block body %} + {% for child in this.children %} + {{ child.title }} + {% endfor %} +{% endblock %} diff --git a/examples/templates/example-advanced.html b/examples/templates/example-advanced.html new file mode 100644 index 0000000..9804b09 --- /dev/null +++ b/examples/templates/example-advanced.html @@ -0,0 +1,3 @@ +

    Path: {{ this | url(absolute=True) }}

    +
    This is: {{this}}
    +
    Children: {{this.children}}
    diff --git a/examples/templates/example-config.html b/examples/templates/example-config.html new file mode 100644 index 0000000..1deafc2 --- /dev/null +++ b/examples/templates/example-config.html @@ -0,0 +1,7 @@ +

    Path: {{ this | url(absolute=True) }}

    +
    This is: {{this}}
    + \ No newline at end of file diff --git a/examples/templates/example-simple.html b/examples/templates/example-simple.html new file mode 100644 index 0000000..8483d07 --- /dev/null +++ b/examples/templates/example-simple.html @@ -0,0 +1,12 @@ +

    Path: {{ this | url(absolute=True) }}

    +
    This is: {{this}}
    + \ No newline at end of file diff --git a/examples/templates/page.html b/examples/templates/page.html new file mode 100644 index 0000000..6a36472 --- /dev/null +++ b/examples/templates/page.html @@ -0,0 +1,37 @@ + + +{% block title %}Welcome{% endblock %} + + +
    +
    Nav:   + Root   + Blog   + Projects +
    +
    +
    +

    {{ this.title }}

    + {% block body %}{{ this.body }}{% endblock %} +
    + +