REVELation Documentation
Wrapper Plugin Development
Table of Contents
Overview
This file documents plugin hooks used by the Electron wrapper builder/export pipeline.
Packaging and Manifest
Plugin ZIP installs are strict. The ZIP filename is ignored for plugin identity.
Required ZIP layout:
-
plugin-manifest.jsonat ZIP root -
plugin.jsat ZIP root - any other plugin assets/folders at ZIP root (for example
client.js,index.html,locales/)
Required plugin-manifest.json fields:
-
id: canonical plugin id used as install folder name and runtime plugin key (regex:^[a-z0-9][a-z0-9_-]*$) -
plugin_version: plugin version string -
min_revelation_version: minimum REVELation version string
Installer behavior:
- installs to
plugins/<id>(never derived from ZIP filename) - rejects ZIPs without a valid root manifest
- rejects ZIPs without root
plugin.js - rejects install when
min_revelation_versionis lower than the current app version
Example manifest:
{
"id": "addmedia",
"plugin_version": "0.2.8",
"min_revelation_version": "1.0.1beta"
}
Plugin Bootstrap
Plugins may declare lightweight bootstrap metadata directly on plugin.js.
Supported bootstrap fields:
-
defaultEnabled: trueto include the plugin indefaultConfig.pluginsfor new installs
Example:
module.exports = {
defaultEnabled: true,
priority: 100,
configTemplate: [
{ name: 'enabled', type: 'boolean', default: true }
]
};
Builder Menu Hooks
Browser-side plugin hooks:
-
getContentCreators(context)(legacy) -
getBuilderTemplates(context)(recommended) -
getBuilderExtensions(context)(builder UI extension host)
Template items may provide:
-
labelortitle -
template/markdown/content -
slides/stacks -
onSelect(ctx)orbuild(ctx)
Context fields include:
-
slug,mdFile,dir,origin,insertAt -
insertContent(payload)helper
If a callback calls insertContent(...), builder insertion is considered complete.
Builder Extension Host (getBuilderExtensions)
getBuilderExtensions(context) lets plugins contribute optional UI and behavior to the builder.
Hook context fields:
-
host: BuilderHost API object -
slug: presentation folder name -
mdFile: active markdown file -
dir: web path prefix for the presentation tree
Expected return value:
-
Array<Contribution>(orPromise<Array<Contribution>>) - Return
[]for pages where your plugin has no builder contribution
Contribution kinds:
-
kind: "mode" -
kind: "panel" -
kind: "preview-overlay" -
kind: "toolbar-action" -
kind: "slide-navigator-renderer"
Shared contribution fields:
-
id(required): unique string key for your contribution in this plugin -
label(required formodeandtoolbar-action) -
icon(optional) -
mount/onClick/renderTilecallbacks depending on contribution kind
Mode contribution shape:
-
kind: "mode" -
id: string -
label: string -
icon?: string -
location?: "preview-header" | "left-header"(default:preview-header) -
exclusive?: boolean(reserved for future mode grouping; currently one active mode globally) -
mount(ctx): ModeInstance
Mode mount context:
-
ctx.host -
ctx.id -
ctx.slug -
ctx.mdFile -
ctx.dir
Mode instance hooks (all optional):
-
onActivate() -
onDeactivate() -
onSelectionChanged(payload) -
onDocumentChanged(payload) -
dispose()
Panel contribution shape:
-
kind: "panel" -
id: string -
mount(ctx): (() => void) | { dispose(): void } | void
Panel mount context:
-
ctx.host -
ctx.root(empty container element owned by the host) -
ctx.slug -
ctx.mdFile -
ctx.dir
Preview overlay contribution shape:
-
kind: "preview-overlay" -
id: string -
mount(ctx): (() => void) | { dispose(): void } | void
Preview overlay mount context:
-
ctx.host -
ctx.root(overlay container attached to preview panel) -
ctx.slug -
ctx.mdFile -
ctx.dir
Toolbar action contribution shape:
-
kind: "toolbar-action" -
id: string -
label: string -
icon?: string -
onClick(ctx): void
Toolbar action click context:
-
ctx.host -
ctx.slug -
ctx.mdFile -
ctx.dir
Slide navigator renderer contribution shape:
-
kind: "slide-navigator-renderer" -
id: string -
renderTile(ctx): HTMLElement | null
Slide navigator renderTile context:
-
ctx.slide({ top, body, notes }) -
ctx.h -
ctx.v -
ctx.isActive -
ctx.hasTopMatter
BuilderHost API:
-
version: string(current host contract label) -
apiVersion: number(current integer API version) -
getDocument(): BuilderDocumentSnapshot -
getSelection(): { h: number, v: number } -
getUiState(): { columnMarkdownMode: boolean, previewReady: boolean, dirty: boolean } -
on(eventName, handler): () => voidunsubscribe function -
transact(label, fn): void -
registerMode(...) -
registerPanel(...) -
registerPreviewOverlay(...) -
registerToolbarAction(...) -
registerSlideNavigatorRenderer(renderer) -
getSlideNavigatorRenderer() -
openDialog(spec): Promise<any> -
notify(message, level?)
Event names:
-
selection:changed -
document:changed -
preview:ready -
preview:slidechanged -
mode:changed -
save:before -
save:after
Current event payloads:
-
selection:changed:{ h, v, source } -
document:changed:{ dirty, source }(shape may vary by source) -
preview:ready:{ isOverview } -
preview:slidechanged:{ indices: { h, v }, isOverview } -
mode:changed:{ activeModeId } -
save:before:{ slug, mdFile } -
save:after:{ slug, mdFile, success }
getDocument() snapshot shape:
-
slug,mdFile,dir -
frontmatter(raw YAML frontmatter text) -
noteSeparator -
stacks(Array<Array<{ top, body, notes }>>)
Transaction contract (transact(label, fn)):
-
fn(tx)receives mutation helpers:-
setSelection({ h, v }) -
moveSlide({ h, v }, { h, v }) -
moveColumn(fromH, toH) -
insertSlides({ h, v }, slides) -
replaceColumn(h, slides) -
replaceStacks(stacks)
-
- Core applies post-transaction normalization, marks document dirty, updates selection, and schedules preview refresh.
- Empty slide/column results are sanitized by core.
- Invalid indices are clamped/ignored safely; no exception should be required for normal bounds checks.
Safety and ownership rules:
- Never mutate builder internals directly (
state, DOM nodes outside your root, etc.). - Treat
getDocument()as read-only snapshot data. - Always mutate content through
host.transact(...). - Always unsubscribe listeners and teardown nodes in returned cleanup/dispose functions.
- Handle errors in plugin code; host isolates failures but does not guarantee retries.
Dynamic loading pattern (recommended):
- Keep builder-only code in
builder.js(or equivalent). - In
client.js, lazy-load fromgetBuilderExtensions(...)only whencontext.page === "builder".
Example:
// client.js
window.RevelationPlugins.example = {
init(ctx) {
this.context = ctx;
},
async getBuilderExtensions(ctx) {
if ((this.context?.page || '').toLowerCase() !== 'builder') return [];
const mod = await import('./builder.js');
return mod.getBuilderExtensions(ctx);
}
};
Offline Export Hooks
A plugin may include offline.js with optional hooks:
-
build(context) -
export(context)
export(context) can return:
-
pluginListEntry -
headTags -
bodyTags -
copyentries in{ from, to }format
Example:
module.exports = {
async export(ctx) {
return {
pluginListEntry: {
baseURL: './_resources/plugins/example',
clientHookJS: 'client.js',
priority: 100,
config: {}
},
copy: [
{ from: 'client.js', to: 'plugins/example/client.js' },
{ from: 'dist', to: 'plugins/example/dist' }
]
};
}
};
Plugin UI Localization
Plugin UI text should stay inside each plugin, not in the app-wide translations.json.
Convention:
- Add plugin locale files under
plugins/<pluginName>/locales/translations.json - In plugin pages (
*.html), push that file ontowindow.translationsourcesbefore loading/js/translate.js - Use
data-translatefor static DOM text andtr('...')for runtime JS strings - For builder/runtime hooks (for example
client.jslabels), the plugin can push its own locale source fromctx.baseURLand callloadTranslations()
This keeps plugin translations self-contained and avoids central translation-file bloat.
Plugin-Specific References
Plugin-specific markdown syntaxes are documented in plugin folders:
Documentation Hub
Created with REVELation Snapshot Presenter