diff --git a/examples/Makefile b/examples/Makefile
new file mode 100644
index 0000000..9a7864d
--- /dev/null
+++ b/examples/Makefile
@@ -0,0 +1,7 @@
+.PHONY: server clean plugins
+server:
+ lektor server
+clean:
+ lektor clean --yes -v
+plugins:
+ lektor plugins flush-cache && lektor plugins list
diff --git a/examples/README.md b/examples/README.md
index 89e4914..fe853e9 100644
--- a/examples/README.md
+++ b/examples/README.md
@@ -1,9 +1,10 @@
# 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.
+- [quick config example](#quick-config) shows how you can use the plugin config to setup a quick and easy tagging system.
+- [simple example](#simple-example) goes into detail how to use it in your own plugin.
+- [advanced example](#advanced-example) touches on the potentials of the plugin.
+- [Misc](#misc) shows other use-cases.
@@ -45,9 +46,16 @@ The easiest way to add tags to your site is by defining the `groupby.ini` config
```ini
[testA]
root = /
-slug = config/{group}.html
+slug = config/{key}.html
template = example-config.html
split = ' '
+enabled = True
+
+[testA.fields]
+title = "Tagged: " ~ this.group
+
+[testA.key_map]
+Blog = News
```
The configuration parameter are:
@@ -57,9 +65,11 @@ The configuration parameter are:
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.
+3. The `slug` parameter (`config/{key}.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`.
+ The default value is `{attrib}/{key}/index.html` which would resolve to `testA/tagname/index.html`.
+ If this field contains `{key}`, it just replaces the value with the group-key.
+ In all other cases the field value is evaluated in a jinja context.
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.
@@ -68,18 +78,54 @@ The configuration parameter are:
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.
+6. The `enabled` parameter allows you to quickly disable the grouping.
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.
+There are two additional config mappings, `.fields` and `.key_map`.
+Key-value pairs in `.fields` will be added as attributes to your grouping.
+You can access them in your template (e.g., `{{this.title}}`).
+All of the `.fields` values are evaluted in a jinja context, so be cautious when using plain strings.
+
+The built-in field attributes are:
+
+- `group`: returned group name, e.g., "A Title?"
+- `key`: slugified group value, e.g., "a-title"
+- `slug`: url path after root node, e.g. "config/a-title.html" (can be `None`)
+- `record`: parent node, e.g., `Page(path="/")`
+- `children`: dictionary of `{record: extras}` pairs
+- `first_child`: first page
+- `first_extra`: first extra
+- `config`: configuration object (see below)
+
+Without any changes, the `key` value will just be `slugify(group)`.
+However, the other mapping `.key_map` will replace `group` with whatever replacement value is provided in the `.key_map` and then slugified.
+You could, for example, add a `C# = c-sharp` mapping, which would otherwise just be slugified to `c`.
+This is equivalent to `slugify(key_map.get(group))`.
+
+The `config` attribute contains the values that created the group:
+
+- `key`: attribute key, e.g., `TestA`
+- `root`: as provided by init, e.g., `/`
+- `slug`: the raw value, e.g., `config/{key}.html`
+- `template`: as provided by init, e.g., `example-config.html`
+- `enabled`: boolean
+- `dependencies`: path to config file (if initialized from config)
+- `fields`: raw values from `TestA.fields`
+- `key_map`: raw values from `TestA.key_map`
+
+In your template file you have access to the attributes, config, and children (pages):
```jinja2
-{%- for child, extras in this.children.items() %}
-
Page: {{ child.path }}, Tags: {{ extras }}
+{{ this.title }}
+Key: {{ this.key }}, Attribute: {{ this.config.key }}
+
+{%- for child in this.children %}
+- Page: {{ child.path }}
{%- endfor %}
+
```
@@ -93,60 +139,64 @@ 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)}
+def on_groupby_before_build_all(self, groupby, builder, **extra):
+ watcher = groupby.add_watcher('testB', {
+ 'root': '/blog',
+ 'slug': 'simple/{key}/index.html',
+ 'template': 'example-simple.html',
+ })
+ watcher.config.set_key_map({'Foo': 'bar'})
+ watcher.config.set_fields({'date': datetime.now()})
- # 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)
+ @watcher.grouping(flatten=True)
+ def convert_simple_example(args):
+ # Yield groups
+ value = args.field # type: list # since model is 'strings' type
+ for tag in value:
+ yield tag, {'tags_in_page': value}
```
-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:
+This example is roughly equivalent to the config example above – the parameters of the `groupby.add_watcher` function correspond to the same config parameters.
+Additionally, you can set other types in `set_fields` (all strings are evaluated in jinja context!).
-- Flatten determines how Flow elements are processed.
+`@watcher.grouping` sets the callback to generate group keys.
+It has one optional 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.
+1. The `record` points to the `Page` record that 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:
+ The field value is equivalent to the following:
```python
-args.page[fieldKey].blocks[flowIndex].get(flowKey)
+k = args.key
+field = args.record[k.fieldKey].blocks[k.flowIndex].get(k.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.
+If you choose to produce an entry, you have to `yield` a string or tuple pair `(group, extra-info)`.
+`group` is slugified (see above) and then used to combine & cluster pages.
+The `extra-info` (optional) is passed through to your template file.
+You can yield more than one entry per source.
+Or 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 }}
+Custom field date: {{this.date}}
-{%- for extra in extras %}
-- Name: {{ extra.val }}, Tag count: {{ extra.tags_in_page }}
+{%- for child, extras in this.children.items() -%}
+{%- set etxra = (extras|first).tags_in_page %}
+- {{etxra|length}} tags on page "{{child.path}}": {{etxra}}
{%- endfor %}
-{%- endfor %}
```
@@ -166,44 +216,72 @@ Except that it loads a config file and replaces in-text occurrences of `{{Tagnam
```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')
+ config = self.get_config()
+ regex = config.get('testC.pattern.match')
+ try:
+ regex = re.compile(regex)
+ except Exception as e:
+ print('inlinetags.regex not valid: ' + str(e))
+ return
+
+ # load config directly (which also tracks dependency)
+ watcher = groupby.add_watcher('testC', config)
+
+ @watcher.grouping()
def convert_replace_example(args):
# args.field assumed to be Markdown
obj = args.field.source
+ slugify_map = {} # type Dict[str, str]
for match in regex.finditer(obj):
tag = match.group(1)
- yield slugify(tag), tag
+ key = yield tag
+ print('[advanced] slugify:', tag, '->', key)
+ slugify_map[tag] = key
def _fn(match: re.Match) -> str:
tag = match.group(1)
- return f'{tag}'
+ 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.
+Notice, `add_watcher` accepts a config file as parameter which keeps also track of dependencies and rebuilds pages when you edit the config file.
+Further, the `yield` call returns the slugified group-key.
+First, you do not need to slugify it yourself and second, potential replacements from `key_map` are already handled.
For Markdown fields, we can modify the `source` attribute directly.
-All other field typed need to be accessed via `args.record` key indirection.
+All other field types need to be accessed via `args.record` key indirection (see [simple example](#simple-example)).
```ini
+[testC]
+root = /
+slug = "advanced/{}/".format(this.key)
+template = example-advanced.html
+
+[testC.pattern]
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.
+The config file takes the same parameters as the [config example](#quick-config).
+As you can see, `slug` is evaluated in jinja context.
-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:
+We introduced a new config option `testC.pattern.match`.
+This regular expression matches `{{` + any string less than 32 characters + `}}`.
+Notice, the parenthesis (`()`) will match only the inner part but the replace function (`re.sub`) will remove the `{{}}`.
-```python
-@groupby.depends_on(file1, file2, ...)
+
+
+## Misc
+
+It was shortly mentioned above that slugs can be `None` (only if manually set to `slug = None`).
+This is useful if you do not want to create subpages but rather an index page containing all groups.
+This can be done in combination with the next use-case:
+
+```jinja2
+{%- for x in this|vgroups('TestA', 'TestB', recursive=True)|unique|sort %}
+({{ x.group }})
+{%- endfor %}
```
+
+You can query the groups of any parent node (including those without slug).
+The keys (`'TestA', 'TestB'`) can be omitted which will return all groups of all attributes (you can still filter them with `x.config.key == 'TestC'`).
+Refer to `templates/page.html` for usage.
diff --git a/examples/configs/advanced.ini b/examples/configs/advanced.ini
index f2dd274..b620693 100644
--- a/examples/configs/advanced.ini
+++ b/examples/configs/advanced.ini
@@ -1,6 +1,6 @@
[testC]
root = /
-slug = advanced/{key}/
+slug = "advanced/{}/".format(this.key)
template = example-advanced.html
[testC.pattern]
diff --git a/examples/configs/groupby.ini b/examples/configs/groupby.ini
index f7262d4..5871b1b 100644
--- a/examples/configs/groupby.ini
+++ b/examples/configs/groupby.ini
@@ -1,7 +1,7 @@
[testA]
enabled = True
root = /
-slug = "config/{}.html".format(this.key)
+slug = config/{key}.html
template = example-config.html
split = ' '
diff --git a/examples/packages/advanced-example/lektor_advanced.py b/examples/packages/advanced-example/lektor_advanced.py
index 5ba2d21..58ad4de 100644
--- a/examples/packages/advanced-example/lektor_advanced.py
+++ b/examples/packages/advanced-example/lektor_advanced.py
@@ -16,7 +16,8 @@ class AdvancedGroupByPlugin(Plugin):
print('inlinetags.regex not valid: ' + str(e))
return
- watcher = groupby.add_watcher('testC', config) # tracks dependency
+ # load config directly (which also tracks dependency)
+ watcher = groupby.add_watcher('testC', config)
@watcher.grouping()
def _replace(args: GroupByCallbackArgs) -> Generator[str, str, None]: