Editra documentation

Plugin Developer Guide

A practical path from a focused plugin idea to a secure, testable, review-ready Editra extension.

The plugin path

Build one capability. Connect it through a reviewed boundary.

Editra plugins should be small, explicit, and reversible. Choose a loading model, register only the commands you need, clean up every retained resource, test both host types, and publish with validated metadata.

  1. 01Define
  2. 02Build
  3. 03Secure
  4. 04Test
  5. 05Submit

1. Choose a loading model

Both delivery modes use the same public editor API. Select the model that matches your application's deployment needs.

Simple delivery

Single bundle

Load dist/editra.js for the complete built-in experience. Omitting plugins preserves the full editor.

Best for:quick starts and conventional deployments
Focused delivery

Modular core

Load editra-core.js and declare only required plugins. Plugin JavaScript remains lazy until its command is first used.

Best for:performance-sensitive and governed applications
<link rel="stylesheet" href="/editra/dist/editra-core.css">
<div id="editor"></div>
<script src="/editra/dist/editra-core.js"></script>
<script>
  (async function () {
    "use strict";
    const editor = await Editra.init({
      selector: "#editor",
      theme: "Word",
      plugins: ["formatting", "table", "image"]
    });
    globalThis.editraEditor = editor;
  })();
</script>

Watch modular loadingView single-bundle example

2. Build your first reviewed plugin

plugins/example/
plugin.jscommands and lifecycle
plugin.cssoptional scoped styles
manifest.jsonidentity and compatibility
plugin.test.jsbehavior and cleanup

Built-in plugins register a function in window.EditraPlugins. Guard installation with a WeakMap, register commands through the core API, and make cleanup mandatory.

(function (global) {
  "use strict";
  const installations = new WeakMap();

  function install(core) {
    if (installations.has(core)) return installations.get(core);

    const removeCommand = core.registerCommand(
      "exampleCommand",
      () => true,
      { plugin: "example", source: "plugin" }
    );

    const state = { removeCommand };
    core.registerCleanup(() => {
      removeCommand();
      installations.delete(core);
    });
    installations.set(core, state);
    return state;
  }

  function ExamplePlugin(core) {
    install(core);
    return true;
  }

  ExamplePlugin.install = install;
  ExamplePlugin.hydrate = install;
  ExamplePlugin.plugin = Object.freeze({
    name: "example",
    label: "Example",
    command: "exampleCommand"
  });

  (global.EditraPlugins ??= Object.create(null)).example = ExamplePlugin;
})(window);

3. Follow the lifecycle contract

01

Install

Register commands and listeners once for each editor.

02

Hydrate

Reconnect behavior after persisted HTML enters the surface.

03

Execute

Use reviewed commands instead of reaching into private internals.

04

Destroy

Remove listeners, observers, URLs, overlays, and retained state.

Reviewed built-in API

Use supported surfaces

Built-ins have internal access and therefore require maintainer review and release signing.

registerCommandexecuteCommandsanitizeHTMLsecureRequestrecordHistoryemitChangeannounceregisterCleanup

4. Support every editor host

Plugin behavior must be identical whether Editra starts on a <div> or a synchronized <textarea>. Never query, replace, or take ownership of the original host.

5. Use the sandbox for community plugins

Community pluginisolated iframe
validated messages
Capability bridgeallow-listed API
structured results
Editra hostsanitized content

Community code receives no core object. It runs in an iframe with sandbox="allow-scripts" and exchanges structured messages with the host.

CapabilityWhat the plugin receivesBoundary
document.readTextCurrent plain textNo DOM access
document.readHTMLSanitized serialized HTMLNo active script
commands.executeResult from an allowed commandManifest allow-list
ui.notifyBounded host notificationText-only payload
parent.postMessage({
  source: "editra-plugin",
  pluginId: "my-plugin",
  type: "ready"
}, "*");
Never exposed: DOM nodes, editor internals, functions, application credentials, or arbitrary network access.

6. Validate, version, and submit

Before the pull request
  • Metadata validates against the registry schema.
  • Version follows semantic versioning.
  • Compatibility uses >=MAJOR.MINOR.PATCH.
  • Commands and capabilities use the smallest necessary scope.
  • Cleanup, div, textarea, Word, and Classic tests pass.
  • Security-sensitive behavior is documented.

Registry metadata

{
  "name": "Spell Checker",
  "version": "1.0.0",
  "author": "Community Dev",
  "description": "Adds spell checking support",
  "compatibility": ">=2.0.1"
}
Ready for review?

Submit a focused, testable plugin.

Contributions are reviewed for code quality, security boundaries, compatibility, accessibility, lifecycle cleanup, and clear documentation.

Open GitHubRead the API