ExtensionDesign
How would you create a PmWiki extension, and why would anyone want that?
Description
Extensions are a new kind of PmWiki recipes that are easier to install, configure, and maintain (per PmWiki Philosophy#5), without the need to edit PHP config files.
Extensions are managed by ExtensionHub, a control panel where wiki administrators can enable and configure extensions.
Extensions are slightly different from classic PmWiki recipes, as they:
- should be structured in a certain way in order to be recognized and automated by the Hub
- are placed in the
pmwiki/extensions
directory instead of thepmwiki/cookbook
directory.
The main benefit for extensions is the ease of installation and configuration for wiki administrators. This is true for simple recipes, but more so for recipes shipping with additional resources like styles and scripts. Being a wiki administrator myself, I plan to write any new recipes, and eventually convert most of my existing recipes, into the new extension format.
There are also benefits for recipe authors. The Hub includes a number of helper functions that make it easy to access their own local configuration, or inject resources like CSS and JavaScript. Having all extension files grouped in one directory (rather than intermingled in pmwiki/cookbook and pmwiki/pub), is easier to maintain, update, and manage in version control. Being a recipe author myself, ... you get the idea. :-)
Helper functions for extension authors:
- Easier installation for your recipes, with no required editing of config files, and simpler documentation.
- Adding, removing, enabling, disabling configurations is managed by the Hub.
- Extensions can define actions when they need to be included, and priority compared to the processing of
local.php
,stdconfig.php
, and relatively to other extensions. - Extensions can have custom configuration forms so that wiki admins set their preferences. The forms can be annotated as needed, and can have documentation or demonstrations.
- The Hub is managing the storage of the configuration values for the extension.
- Extensions receive their own configuration options for the currently browsed page.
- Extensions can optionally include scripts, styles, and wiki pages (documentation, templates, forms...).
- The extension's base directory is similar to that of a PmWiki skin, with a PHP script, optionally CSS/JS files, pictures, fonts, all in one place. This is easier to manage and update compared to older complex recipes that typically have files and directories intermingled in
cookbook/
andpub/
.
- The extension's base directory is similar to that of a PmWiki skin, with a PHP script, optionally CSS/JS files, pictures, fonts, all in one place. This is easier to manage and update compared to older complex recipes that typically have files and directories intermingled in
- There are helper functions to easily inject scripts and styles (shipped with the extension, or external), and
wikilib.d
pages. - There will be a simple way to define i18n strings for different languages, if the recipe needs this (to do).
There are also some downsides or considerations about using the new format over classic recipes.
- Wiki administrators need to first install ExtensionHub before your extension can be used.
- ExtensionHub requires the latest, or a very recent PmWiki version, and at least PHP 7.0.
- Converting existing recipes to the extension format needs some additional work.
- While I've been working on the Hub for months, the current API is not written in stone and if there is a good reason to change something, early extensions might need to be updated.
While we add new extensions, any classic recipes will continue to work as before, these are not deprecated or removed.
The following sections describe the structure of an extension, as expected by the Hub, and the usage of the helper functions.
Examples
You can review existing extensions and see how they are are organized, see listing at Extensions. Sample extensions are:
- BootstrapIcons BootstrapIcons extension for PmWiki
- CodeHighlight Syntax highlighting for programming languages
- Filterable Search box for instant filtering of long lists and tables
Let me know if you have any questions or difficulties.
Structure
Extensions are placed in their own directories under the pmwiki/extension
directory. All file names will be treated as case-sensitive, including on Windows.
An extension directory structure may look like this:
ExtensionName/ Name of your extension, capitalized, case sensitive
ExtensionName.php Same name as above
LICENSE.txt recommended, software license terms
README optional
styles.css optional resource
script.js optional resource
image.png optional resource
lib/ optional resource directory with js, css, images, fonts
wikilib.d/ optional, additional wiki pages
Site.ExtensionNameForm optional, configuration page
wikiplain.d/ optional, additional wiki pages
Site.ExtensionNameForm optional, configuration page
Only the ExtensionName.php
script is required, the other resources are optional. The directory and the script need to have the same name, case sensitive.
If you plan to publish the extension on the PmWiki.org Cookbook website, it is mandatory to name the directory and php filename exactly as the Cookbook page name, which will ensure it will work with RecipeCheck.
Script file
- Your PHP script file should be named exactly the same as your directory, and end with "
.php
". - If your extension directory is "
MyCoolExtension
", it should contain a file "MyCoolExtension.php
". - In the ExtensionHub page PmWiki will create a link to
Cookbook:MyCoolExtension
unless there is a configuration form page.
An extension script file may look like this:
<?php if (!defined('PmWiki')) exit(); # very strongly recommended /** ExtensionName for PmWiki Written by (c) Petko Yotov 2023-2024 www.pmwiki.org/petko License: MIT, see file LICENSE */ $RecipeInfo['ExtensionName']['Version'] = '2024-02-24'; # default configuration SDVA($MyExtDefaultConf, [ 'var1' => 'value1', 'var2' => 5, ]); # Your code here, for example: function MyExtensionInit($pagename) { global $MyExtDefaultConf; # Configuration from the wiki, for the current page, # merged with the default values $conf = extGetConfig($MyExtDefaultConf); # $conf['var1'] is now either 'value1', # or whatever the admin set in the Hub # do your magic... } MyExtensionInit($pagename); # EOF, no closing ?> marker
You can configure wiki actions when your script should be included, and the priority, see Form page (configuration) below.
A wiki administrator can add separate configurations for different page patterns, for example per wiki group. The helper function extGetConfig()
will return an array of values that apply to the current page and group.
There are additional elements in the array returned by extGetConfig()
:
-
$conf['=dir']
The base directory of your extension, where yourExtensionName.php
file is located.- If the extension is compressed, it will start with "
phar://
" (PHP archive stream). - You can include a separate PHP file from your extension directory with
include_once("{$conf['=dir']}/anotherscript.php");
which should work both with uncompressed and compressed extensions. - To scan and read files from that directory, you can use most PHP filesystem functions, except
glob()
- usescandir()
withMatchNames()
orpreg_grep()
instead. - Your script should not try to write or modify files in that directory : it should be expected to be read-only.
- If the extension is compressed, it will start with "
$conf['=url']
The browser-reachable base URL to your extension base directory.- If the extension is compressed,
hub.php
will serve your files from it on the fly. - For CSS and JS files, call
extAddHeaderResource()
instead, see next section.
- If the extension is compressed,
Resources
Your extension can optionally ship with stylesheets, JavaScript scripts, images, icons, fonts, and other resources.
These can be in the ExtensionName directory, or in a subdirectory. The file names, and the subdirectory names, can be anything, but it is recommended to stick to letters [a-zA-Z]
, digits [0-9]
, minus, dot, and underscore [-._]
(no spaces, parentheses, brackets, special, or international characters).
Users can install an extension by dropping its zip archive in the pmwiki/extension directory
, and optionally uncompress it. So in the wild, your extension may be compressed, or uncompressed. Additionally, if downloaded from version control like GitHub, the zip file name, and the directory name, may have a release suffix. Additionally, the public path to "extensions
" directory may change on different wikis.
We cannot know in advance the public URLs of the resources, that's why a helper function makes it easy to inject these resources in the $HTMLHeaderFmt
or $HTMLFooterFmt
arrays.
To inject resources, use the helper function extAddHeaderResource like this:extAddHeaderResource($space_separated_resources, $attributes);
You can call this function once or multiple times (with different resources).
# add resources to $HTMLHeaderFmt extAddHeaderResource('mystyles.css lib/library.js myscript.js');
This will add to $HTMLHeaderFmt
the necessary <link>
and <script>
tags that will load your resources, with the correct public extension URLs.
To inject resources in $HTMLFooterFmt
, use:
extAddFooterResource('footer_script.js');
If your resources need custom attributes, use the following:
# <script src="...myscript.js" id="extname" data-custom="1"> $attrs["myscript.js"]['id'] = 'extname'; $attrs["myscript.js"]['data-custom'] = '1'; extAddHeaderResource('mystyles.css myscript.js', $attrs); # similar with extAddFooterResource() second argument
By default, the resources are relative to the base directory of your extension. To add resources from elsewhere (like a CDN), use the full URL of the resource:
extAddHeaderResource('https://code.jquery.com/jquery-3.7.1.min.js myscript.js');
Wiki pages (wikilib.d
, wikiplain.d
)
Your extension can optionally ship with wiki pages that will be made available on the wiki after installation. There are helper functions that allow for simpler activation of your pages.
You can have a regular ExtensionName/wikilib.d
directory, where you copy actual wiki page files from your wiki.d
. (Maybe strip them from the page history, see ExpireDiff.)
Alternatively, you can have an ExtensionName/wikiplain.d
directory, where your wiki pages are simple text files with only the text content, without metadata, permissions, or history. This may be convenient if you prefer to edit the text files in your code editor. If you publish the extension to GitHub or other version control system, a plain text format creates better "diffs" that are easier to review than the native PmWiki page format.
Files in both wikilib.d
and wikiplain.d
stay in your extension directory, they are not copied to the core wikilib.d
directory. However, should a wiki user edit one of the pages you ship, the modified page will be copied to "wiki.d
" and subsequently that one will be used when needed. This behavior is similar to how core pages work, for example Main.WikiSandbox
or Site.EditForm
.
The helper functions that add your pages to the wiki are:
# add a regular wikilib.d with native PmWiki page files extAddWikiLibDir('wikilib.d'); # add a directory with plain-text wiki pages extAddWikiPlainDir('wikiplain.d');
If you use these helper functions, the wiki pages will be made available to the wiki. The only exception is a configuration form page (see below), which is only used in the Hub; in this case, and with the default directory names, the page will be included automatically without you calling a helper function.
Form page (configuration)
In the Hub, wiki administrators can configure and enable/disable different "configurations" for different page patterns.
You can ship a form page that will be included by the Hub when a wiki admin enables and configures your extension. The data that is saved by the administrator will be made available to your extension via the function extGetConfig()
.
The form page should be named Site.ExtensionNameForm
, where ExtensionName
is the base name of your extension directory and PHP script.
- The file can be in a subdirectory
wikiplain.d
, in that case this should be plain text file, with only the wiki source text of the page (no metadata, attributes, or history). - The file can be in a subdirectory
wikilib.d
, in that case this should be a native PmWiki page file, that you have edited on your wiki then copied fromwiki.d
into this subdirectory. - The Hub will load the page from
wikiplain.d
if it exists, otherwise fromwikilib.d
if it exists.
This is not a full form, no (:input form:)
, (:input submit:)
or (:input end:)
elements, just input fields like text, number, textarea, select, radio and other, see Forms. You can add annotations before and after the fields to make it easy for the administrator.
The form page can have a >>recipeinfo frame<<
header with a summary, version, your name, and possibly a Cookbook page URL. Then follow the form elements, then optionally a footer content that may be documentation or demonstration.
Here is a sample configuration form from the Filterable extension:
>>frame<< $[Summary]: Search field to filter tables and lists\\ $[Version]: {$ExtVersion}\\ $[Maintainer]: [[(https://www.pmwiki.org/wiki/Profiles/)Petko]]\\ $[Cookbook]: [[(Cookbook:)Filterable]] >><< [[#form]] (:comment Optional custom configuration fields:) Minimum number of items/rows to enable search box (optional):\\ (:input number minsize min=1 step=1:) \\ ''Default: '''5''' '' Query selectors for filterable lists and tables (optional): \\ (:input text selector size=40:) \\ ''Default: '''ul.filterable, ol.filterable, table.filterable''' '' [[#formend]] (:comment Optional footer content, documentation, demo:) |
Minimum number of items/rows to enable search box (optional): Query selectors for filterable lists and tables (optional): |
When editing an extension configuration, if there is a configuration page, the Hub will include:
- the header before the
[[#form]]
anchor at the top of the page, - the part between
[[#form]]...[[#formend]]
inside its own configuration form, - the part after
[[#formend]]
after the configuration form.
Two custom page variables can be used in the form, {$ExtName}
and {$ExtVersion}
. These are only available when the extension is being configured.
When you request the extension configuration, an array will be returned with the same keys you have in your form, and the values that have been saved. In the form with fields:
(:input number minsize min=1 step=1:) (:input text selector size=40:) |
You get the saved configuration like this:
$MyDefaultValues ['minsize'] = 5; $MyDefaultValues ['selector'] = ''; $conf = extGetConfig($MyDefaultValues); # you can now access $conf['minsize'] and $conf['selector']
There is a limited support for configuration arrays, use something like this:
(:input checkbox lang[] PHP "PHP":) (:input checkbox lang[] HTML "HTML":) (:input checkbox theme[defaut] 1 "Default theme":) (:input checkbox theme[mobile] 1 "Mobile theme":) |
When you get the local values:
$MyDefaultValues ['lang'] = []; $MyDefaultValues ['theme'] = []; $conf = extGetConfig($MyDefaultValues); # $conf['lang'] may be [ 'PHP', 'HTML' ] if both were checked # $conf['theme'] may be [ 'default'=>1, 'mobile'=>1 ] if both were checked
The extension SimplePlaylist has a configuration form with an array, check how it is processed.
Your initial/default configuration can include more complex data structures that may not be exposed in a Hub configuration form, but that very advanced users can define in local configuration.
Notes
- To avoid interference with the core and the Hub, custom field names :
- cannot be "n", "action", "pmtoken", "i" or "x",
- cannot start with lowercase "x" followed by an uppercase letter [A-Z] like "xSomething", and
- should be composed of only ASCII letters a-zA-Z, numbers 0-9 and the underscore character "_".
- Field names starting with "
enc_
" or "passwd
" will have their values base64-encoded for storage, then decoded when the recipe requests them, or in the recipe configuration form. - If a field is left empty, it will not be saved, and will not appear in the
$conf
array. You can merge the$conf
array with your default values as shown above. - Numeric values will be cast to Float.
Setting script actions and priority
If your extension needs to be included only for certain wiki actions, you can define them in your configuration form (between the [[#form]]...[[#formend]]
anchors) in hidden input fields:
(:input hidden xAction "copy,move,rename":)
- Only include the extension when someone calls
wiki?action=copy
(ormove
, orrename
). (:input hidden xAction "*,-edit":)
- Do not include the extension when the action is "
edit
" (case sensitive).
The default action is "*
" (any action). You don't need to have a xAction
field if this works for you.
Similarly, and also optionally, you can define the "priority" or order of your extension relative to config.php
, stdconfig.php
and other extensions.
(:input hidden xPriority 120:)
- include the extension before most other extensions.
Extensions that are enabled on the wiki will be ordered by their priority, then included one after another in that order. Notes:
- The default priority for extensions is 150. You don't need to include a
xPriority
element if this works for you. - If the priority is between 0 and 100 inclusive, the extension will be included very early in the processing.
- This is only needed for extensions that do advanced configurations with system and security, and that apply to the whole wiki, for example adding a PageStore class, or an AuthUser backend, or setting global variables like
or$DefaultName
.$UploadPrefixFmt
- Note that the global PmWiki variable
may not be available or reliable at that moment of the processing, and you should not resolve it.$pagename
- Such extensions do not have per-page-pattern configurations.
- This is only needed for extensions that do advanced configurations with system and security, and that apply to the whole wiki, for example adding a PageStore class, or an AuthUser backend, or setting global variables like
- If the priority is greater than 100 and less than or equal to 200, the extension will be included after
local/config.php
,local/Group.Page.php
, andlocal/Group.php
, at the beginning ofscripts/stdconfig.php
.- Most recipes should be safely included here. Your recipe can add or disable Markup rules and core variables.
$pagename
is available, correctly initialized.
- If the priority is greater than 200, the extension will be included after
scripts/stdconfig.php
.- This should only be needed for rare cases, and you should know what you are doing. For example, here markup rules cannot be redefined or disabled (but new ones could be added), and Skin variables cannot be modified.
Notes
To do / some day / maybe
- I18n translations should be added in the future.
Change log / Release notes
- 2024-04-10 Replaced calls to
extAddResource()
withextAddHeaderResource()
andextAddFooterResource()
- 2024-03-02 Page created
See also
- Cookbook /
- ExtensionHub Configuration panel for extensions (Experimental)
- Extensions Recipes compatible with ExtensionHub, see ExtensionDesign.
Contributors
Comments
See discussion at ExtensionDesign-Talk
User notes? : If you use, used or reviewed this recipe, you can add your name. These statistics appear in the Cookbook listings and will help newcomers browsing through the wiki.