From 2a6bdf05fd676176f4585dc93adca1ae030f9d04 Mon Sep 17 00:00:00 2001 From: relikd Date: Wed, 6 Apr 2022 15:42:02 +0200 Subject: [PATCH] update example readme v0.9.3 --- examples/Makefile | 7 + examples/README.md | 196 ++++++++++++------ examples/configs/advanced.ini | 2 +- examples/configs/groupby.ini | 2 +- .../advanced-example/lektor_advanced.py | 3 +- 5 files changed, 148 insertions(+), 62 deletions(-) create mode 100644 examples/Makefile 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 }}

    + ``` @@ -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]: