Blame

36c0a2 Ralph Thesen 2026-08-30 20:08:01
Add plugins page documenting installation and hooks
1
# Plugins
2
3
An Otter Wiki can be extended with plugins. It uses
4
[pluggy](https://pluggy.readthedocs.io/en/stable/) to expose a set of function
5
hooks that a plugin implements to add or change behaviour, from rewriting
6
markdown before it is rendered to injecting HTML into the sidebar or reacting
7
to page changes.
8
9
> [!NOTE]
10
> The plugin API is experimental and still limited. If you want to write a
11
> plugin and cannot find a matching hook, please open an
12
> [issue](https://github.com/redimp/otterwiki/issues).
13
14
## What a plugin is
15
16
A plugin is a normal, pip-installable Python package that registers one or more
17
hook implementations. In practice that means three things:
18
19
- a class whose methods are decorated with `@hookimpl`,
20
- a call to `plugin_manager.register(...)` so the wiki finds it,
21
- an `otterwiki` entry point in the package metadata.
22
23
The smallest complete example, the `noemojis` plugin, strips emojis from every
24
page before it is rendered:
25
26
```python
27
from otterwiki.plugins import hookimpl, plugin_manager
28
29
class NoEmojiPlugin:
30
@hookimpl
31
def renderer_markdown_preprocess(self, md):
32
return self.emojis.sub('', md)
33
34
# needed so the plugin_manager finds the plugin
35
plugin_manager.register(NoEmojiPlugin())
36
```
37
38
with a `pyproject.toml` that declares the entry point:
39
40
```toml
41
[project.entry-points.otterwiki]
42
noemojis = "otterwiki_noemojis"
43
```
44
45
A plugin that surfaces `info()` and `help()` also appears in the in-app plugin
46
help at [/-/help/plugins](/-/help/plugins), next to the built-in embeddings.
47
48
## Installing a plugin
49
50
A plugin must be installed into the same (virtual) environment that runs the
51
flask app.
52
53
### In the docker image
54
55
On container start `entrypoint.sh` installs every plugin directory it finds
56
under `/app-data/plugins/` and `/plugins/` with `pip install -U .`. So there
57
are two ways to deploy a plugin:
58
59
- drop the plugin directory into `plugins/` inside the volume that already
60
holds your `app-data` (alongside `db.sqlite` and `repository`), or
61
- keep your plugins in a separate directory and mount it into the container at
62
`/plugins`.
63
64
For example, mounting a plugins directory next to the usual app-data volume:
65
66
```yaml
67
services:
68
otterwiki:
69
image: redimp/otterwiki:2
70
volumes:
71
- ./app-data:/app-data
72
- ./plugins:/plugins
73
```
74
75
> [!NOTE]
76
> A plugin with dependencies that are not already in the image will fail to
77
> install. In that case build a custom image that bundles the plugin and its
78
> requirements. In environments with `SELINUX=enforcing` the bind mounts need
79
> adjusting, see the [[FAQ|FAQ#environments-with-selinux]].
80
81
### From a source install
82
83
Activate the virtual environment that runs the app and install the plugin
84
directory into it:
85
86
```bash
87
venv/bin/pip install .
88
```
89
90
### Uninstalling
91
92
From a source install run `pip uninstall <plugin-name>`. For the docker image,
93
remove the plugin directory and recreate the container for a clean environment.
94
95
## Developing Plugins
96
97
TODO
98
99
### Available hooks
100
101
The authoritative list of hooks, with full signatures and documentation, is the
102
`OtterWikiPluginSpec` class in
103
[otterwiki/plugins.py](https://github.com/redimp/otterwiki/blob/main/otterwiki/plugins.py).
104
A plugin implements only the hooks it needs. Grouped by purpose:
105
106
**Setup**
107
108
- `setup(app, db, storage)` receives the Flask app, database and git storage to initialise the plugin.
109
110
**Rendering (pre/post processing)**
111
112
- `renderer_markdown_preprocess(md)` transforms the raw markdown before rendering (chained across plugins).
113
- `renderer_html_postprocess(html)` transforms the HTML after the page has been rendered.
114
- `renderer_javascript()` adds JavaScript to the rendered page.
115
- `page_view_htmlcontent_postprocess(html, page)` transforms a page's rendered content just before display.
116
- `page_render_context(page, preview)` receives the page currently being rendered, and whether it is a preview.
117
118
**Embeddings**
119
120
- `embedding_parse(embedding, options, args)` parses an embedding and returns its HTML.
121
- `embedding_render(embedding, args)` renders a `{{name ...}}` embedding to HTML.
122
123
**Template injection points**
124
125
- `template_html_head_inject(page)` injects HTML into the `<head>`.
126
- `template_html_body_inject(page)` injects HTML before the closing `</body>`.
127
- `template_html_sidebar_left_inject(page)` appends HTML to the left sidebar (menu and page index).
128
- `template_html_sidebar_right_inject(page)` appends HTML to the right sidebar (the "On this page" block).
129
130
**Per-element rendering**
131
132
- `renderer_process_link(...)` modifies each rendered markdown link.
133
- `renderer_process_image(...)` modifies each rendered image.
134
- `renderer_process_heading(...)` modifies each rendered heading.
135
- `renderer_process_wikilink(...)` modifies each rendered WikiLink.
136
137
**Repository and page events**
138
139
- `repository_changed(changed_files)` reacts to any repository change, including the git web server and automatic pulls (read-only).
140
- `page_saved(pagepath, content, author, message)` runs after a page's content changed.
141
- `page_deleted(pagepath, author, message)` runs after a page was deleted.
142
- `page_renamed(old_pagepath, new_pagepath, author, message)` runs after a page was renamed.
143
144
**Info and help**
145
146
- `info()` returns `(name, description, category)` used to group the plugin in the user help.
147
- `help(plugin)` returns the plugin's documentation shown under `/-/help`.
148
- `help_category_prelude(category)` returns introductory text for a help category.
149
150
**Static CSS**
151
152
- `static_css()` returns CSS added to every page via the layout template.
153
154
**URL routes**
155
156
- `url_request(plugin, extra, method, values)` handles requests to `/-/plugin/<name>/<extra>`.
157
- `url_admin_request(plugin, extra, method, values)` handles admin requests to `/-/admin/plugin/<name>/<extra>`.
158
159
**Sidebar page index**
160
161
- `sidebar_page_index_filter_entries(entries, mode)` filters the sidebar page index entries in place.
162
- `sidebar_page_index_sort_entries(entries, mode)` sorts the sidebar page index entries in place.
163
164
## Example plugins
165
166
Six worked examples live in
167
[docs/plugin_examples](https://github.com/redimp/otterwiki/tree/main/docs/plugin_examples)
168
in the source repository. Each is a self-contained package you can copy as a
169
starting point, and they come with a `docker-compose.yaml` for trying them out
170
and a test suite.
171
172
- [noemojis](https://github.com/redimp/otterwiki/tree/main/docs/plugin_examples/plugin_noemojis) removes all emojis from pages.
173
- [htmlinjection](https://github.com/redimp/otterwiki/tree/main/docs/plugin_examples/plugin_htmlinjection) demonstrates the HTML injection and per-element rendering hooks.
174
- [referencingpages](https://github.com/redimp/otterwiki/tree/main/docs/plugin_examples/plugin_referencingpages) shows which pages reference the current page via WikiLinks.
175
- [sidebarpageindex](https://github.com/redimp/otterwiki/tree/main/docs/plugin_examples/plugin_sidebarpageindex) demonstrates the sidebar page index filter and sort hooks.
176
- [authorsignature](https://github.com/redimp/otterwiki/tree/main/docs/plugin_examples/plugin_authorsignature) adds a footer with the original author and last editor.
177
- [redlinks](https://github.com/redimp/otterwiki/tree/main/docs/plugin_examples/plugin_redlinks) marks WikiLinks to non-existent pages in red, like MediaWiki's redlinks.