2015-12-26 15:11:45 +01:00
|
|
|
title: How To
|
2015-12-19 14:52:17 +01:00
|
|
|
---
|
2015-12-22 00:28:10 +01:00
|
|
|
summary: Covers the most common things plugins would do and how to implement it.
|
2015-12-19 14:52:17 +01:00
|
|
|
---
|
|
|
|
sort_key: 30
|
|
|
|
---
|
|
|
|
body:
|
|
|
|
|
|
|
|
So you want to build a plugin but it's not quite clear how you would structure
|
|
|
|
a plugin to accomplish this? This part of the documentation should help you
|
|
|
|
find answers to those questions.
|
|
|
|
|
|
|
|
## Expose Functionality
|
|
|
|
|
|
|
|
Most plugins will provide functionality of sorts. There are two places where
|
|
|
|
functionality is typically needed: in templates or for other plugins to use.
|
|
|
|
Plugins can import from each other just like this, but functionality exposed
|
|
|
|
into templates should follow some basic rules:
|
|
|
|
|
|
|
|
* modify `jinja_env.globals` or `jinja_env.filters` and do not use
|
|
|
|
`process-template-context` unless you absolutely know what you are doing.
|
|
|
|
* expose as few global variables as possible. If you have a lot you want to
|
|
|
|
provide then consider registering an object with the templates and to
|
|
|
|
attach multiple attributes on it.
|
|
|
|
* use clear names for template globals. This is a shared space that all
|
|
|
|
plugins and Lektor itself modify so do not provide a function called
|
|
|
|
`get_script` but for instance call it `get_my_plugin_script`.
|
|
|
|
|
|
|
|
A simple example of a plugin that implements [Gravatar
|
|
|
|
:ext](https://en.gravatar.com/) support:
|
|
|
|
|
|
|
|
```python
|
|
|
|
from hashlib import md5
|
|
|
|
from werkzeug.urls import url_encode
|
|
|
|
from lektor.pluginsystem import Plugin
|
|
|
|
|
|
|
|
BASE_URL = 'https://secure.gravatar.com/avatar/'
|
|
|
|
|
|
|
|
def get_gravatar(email, **options):
|
|
|
|
fn = md5(email.lower().strip().encode('utf-8')).hexdigest()
|
|
|
|
return '%s/%s?%s' % (BASE_URL, fn, url_encode(options))
|
|
|
|
|
|
|
|
class GravatarPlugin(Plugin):
|
|
|
|
name = 'Gravatar'
|
|
|
|
def on_setup_env(self, **extra):
|
2016-02-03 21:58:33 +01:00
|
|
|
self.env.jinja_env.filters['gravatar'] = get_gravatar
|
2015-12-19 14:52:17 +01:00
|
|
|
```
|
|
|
|
|
|
|
|
## Configure Plugins
|
|
|
|
|
|
|
|
Plugins can come with their own config files and it's encouraged that plugins
|
|
|
|
take advantage of this. Each plugin has exactly one INI file called
|
|
|
|
`<plugin-id>.ini` inside the `configs` folder.
|
|
|
|
|
|
|
|
To get access to the plugin the `get_config` function can be used which
|
|
|
|
returns a dict like object to access the ini file.
|
|
|
|
|
|
|
|
```python
|
|
|
|
config = self.get_config()
|
|
|
|
value = config.get('section.key', 'default_value')
|
|
|
|
```
|
|
|
|
|
|
|
|
This would correspond to this config in `configs/my-plugin.ini`:
|
|
|
|
|
|
|
|
```ini
|
|
|
|
[section]
|
|
|
|
key = the value
|
|
|
|
```
|
|
|
|
|
|
|
|
## Dependency Tracking
|
|
|
|
|
|
|
|
While a lot of dependencies are tracked automatically, when you develop a
|
|
|
|
plugin you probably will discover that sometimes you need to track your own
|
|
|
|
ones. There are different ways in which dependency tracking can work and
|
|
|
|
depending on the situation you might have to use two different mechanisms.
|
|
|
|
|
|
|
|
1. **Dependency tracking by association**: while a build of a source object
|
|
|
|
into an artifact is active more dependencies for that artifact can be
|
|
|
|
registered with the [record_dependency :ref](../../api/build/context/record-dependency/)
|
|
|
|
method of the context. It takes a filename that should be recorded as
|
|
|
|
additional dependency for the current artifact
|
|
|
|
2. **Dependency tracking as artifact source**: if you build your own artifact
|
|
|
|
you need to define the source files that make up the artifact (if you have
|
|
|
|
such a thing). For instance if you build a thumbnail you will need to
|
|
|
|
track those source files that are the source images. This can be done
|
|
|
|
through the [sub_artifact :ref](../../api/build/context/sub-artifact/)
|
|
|
|
method which declares a new artifact.
|
|
|
|
|
|
|
|
Here examples for both of those in one:
|
|
|
|
|
|
|
|
```python
|
|
|
|
import os
|
|
|
|
from flask import json
|
|
|
|
from lektor.pluginsystem import Plugin
|
|
|
|
|
|
|
|
def dump_exif(image):
|
|
|
|
ctx = get_ctx()
|
|
|
|
path = posixpath.join(image.path, '-exif.json')
|
|
|
|
@ctx.sub_artifact(path, sources=[image.source_filename])
|
|
|
|
def include_file(artifact):
|
|
|
|
ctx.record_dependency(__file__)
|
|
|
|
with artifact.open('wb') as f:
|
|
|
|
json.dump(image.exif.to_dict(), f)
|
|
|
|
return path
|
|
|
|
|
|
|
|
class ExifDumpPlugin(Plugin):
|
|
|
|
def setup_env(self, **extra):
|
|
|
|
self.env.jinja_env.globals['dump_exif'] = dump_exif
|
|
|
|
```
|
|
|
|
|
|
|
|
This dumps out the EXIF data into a JSON file and returns the artifact name.
|
|
|
|
The source image is tracked as direct source for the artifact and within the
|
|
|
|
function we also track the plugin's filename to rebuild if the plugin changes.
|
2016-01-02 18:38:47 +01:00
|
|
|
|
|
|
|
## Adding New Field Types
|
|
|
|
|
|
|
|
Let's say you want to add a "datetime" type to the set of
|
|
|
|
[field types :ref](../../api/db/types/) used in your models, because the
|
|
|
|
built-in "date" type has no time component. You might update `blog-post.ini`
|
|
|
|
from the [blog guide :ref](../../guides/blog) like so:
|
|
|
|
|
|
|
|
```ini
|
|
|
|
[fields.pub_date]
|
|
|
|
label = Publication date
|
|
|
|
type = datetime # Custom "datetime" type.
|
|
|
|
```
|
|
|
|
|
|
|
|
In a blog post's `contents.lr`, specify its publication date like:
|
|
|
|
|
|
|
|
```
|
|
|
|
pub_date: 2016-01-02 12:34:56
|
|
|
|
```
|
|
|
|
|
|
|
|
You can add your "datetime" type to Lektor with a plugin:
|
|
|
|
|
|
|
|
```python
|
|
|
|
from datetime import datetime
|
|
|
|
from lektor.pluginsystem import Plugin
|
|
|
|
from lektor.types import Type
|
|
|
|
|
|
|
|
class DatetimeType(Type):
|
|
|
|
def value_from_raw(self, raw):
|
|
|
|
return datetime.strptime(raw.value, '%Y-%m-%d %H:%M:%S')
|
|
|
|
|
|
|
|
def strftime_filter(dt, fmt='%B %-d, %Y'):
|
|
|
|
return dt.strftime(fmt)
|
|
|
|
|
|
|
|
class DatetimeTypePlugin(Plugin):
|
|
|
|
name = u'datetime type'
|
|
|
|
description = u'Adds a new field type, "datetime".'
|
|
|
|
|
|
|
|
def on_setup_env(self, **extra):
|
|
|
|
self.env.types['datetime'] = DatetimeType
|
|
|
|
self.env.jinja_env.filters['strftime'] = strftime_filter
|
|
|
|
```
|
|
|
|
|
|
|
|
This plugin also adds a `strftime` filter to Jinja.
|
|
|
|
Render a datetime with the default format in a template like:
|
|
|
|
|
|
|
|
```html+jinja
|
|
|
|
Published on {{ this.pub_date|strftime }}
|
|
|
|
```
|
|
|
|
|
|
|
|
Or a custom format:
|
|
|
|
|
|
|
|
```html+jinja
|
|
|
|
Published on {{ this.pub_date|strftime('%Y-%m-%d') }}
|
|
|
|
```
|
|
|
|
|
|
|
|
Python datetime objects implement the standard comparison protocol, so
|
|
|
|
ordering still works without modifying `blog.ini`:
|
|
|
|
|
|
|
|
```ini
|
|
|
|
[children]
|
|
|
|
model = blog-post
|
|
|
|
order_by = -pub_date, title
|
|
|
|
```
|