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