Skip to content

Creating a plugin

Benoit Lathiere edited this page Apr 11, 2022 · 49 revisions

Important Note: Before you get started you might want to use development settings else your changes won't appear!

tl;dr

I'm lazy, I don't want to read. Show me a video instead.

A video for how to setup etherpad as a dev environment and to create a plugin.

Create a new ep_plugin with ALL mandatory folders and files.

Copy-paste the content of the gist ep_newplugin.txt into your terminal and change the name of the plugin folder to your plugin name. https://gist.github.com/lisandi/e2ed83af0862e0f013150da58ad160c5

or download ep_new.sh to the node modules folder -> change permissions 0744 -> run the script (recommended) https://gist.github.com/lisandi/4a09acf2be3eb23722f30d49d1b5c8c5

Edit and adjust the content of the files according to your plugin name and functionality.

ep_my_example/           // in last step of the script change the name
    |- locales/en.json
    |- static/
       | js/
       | css/
       | image/
    |- templates/
    + ep.json            // Adjust to your needs
    + LICENSE.md         // Default Apache2 - enter your Real Name
    + package.json       // Adjust to your needs
    + README.md          // Enter Plugin Name and Description etc.

What is a plugin?

Etherpad-Lite allows you to extend its functionality with plugins. A plugin registers functions for certain API hooks (thus certain features of etherpad-lite) to add its own functionality to these.

Tip: If you don't want to copy and paste all these files, you can clone the ep_base repository git clone git@github.com:niklasfi/ep_base

Replace all CAPITALIZED names with whatever you want.

Folder structure

A basic plugin has the following folder structure:

ep_PLUGINNAME/
 + ep.json
 + package.json
 + YOURFILE.js

However, a more advanced plugin should follow this folder structure:

ep_PLUGINNAME/
 | static/
   | js/
   | css/
   | image/
 | templates/
 + ep.json
 + package.json

If your plugin includes client-side hooks, put them in static/js/. If you're adding in CSS or image files, you should put those files in static/css/ and static/image/, respectively. The front end will not load assets from outside of the static/ directory of your plugin.

mkdir ep_123 ep_123/locales ep_123/static ep_123/static/css ep_123/static/js ep_123/static/image ep_123/static/tests ep_123/templates

mv ep_123 ep_my_example

If your plugin adds or modifies the front end HTML (e.g. adding buttons or changing their functions), you should put the necessary HTML code for such operations in templates/, in files of type ".ejs", since Etherpad-Lite uses EJS for HTML templating.

ep.json

This file registers callback functions (hooks), indicates the parts of a plugin and the order of execution. Hooks are events for certain processes in Etherpad Lite; documentation of all available hooks can be found in the docs. Please create your plugin config file ep.json using cat<ep.json ... EOF as listed below and edit the entries to your needs.

cat<<EOF >ep.json
{
  "parts": [
    {
      "name": "main",
      "client_hooks": {
        "aceEditEvent": "ep_PLUGINNAME/static/js/index",
        "postToolbarInit": "ep_PLUGINNAME/static/js/index",
        "aceDomLineProcessLineAttributes": "ep_PLUGINNAME/static/js/index",
        "postAceInit": "ep_PLUGINNAME/static/js/index",
        "aceInitialized": "ep_PLUGINNAME/static/js/index",
        "aceAttribsToClasses": "ep_PLUGINNAME/static/js/index",
        "collectContentPre": "ep_PLUGINNAME/static/js/shared",
        "aceRegisterBlockElements": "ep_PLUGINNAME/static/js/index",
      },
      "hooks": {
        "authorize": "ep_PLUGINNAME/YOURFILE:FUNCTIONNAME1",
        "authenticate": "ep_PLUGINNAME/YOURFILE:FUNCTIONNAME2",
        "expressCreateServer": "ep_PLUGINNAME/YOURFILE:FUNCTIONNAME3",
        "eejsBlock_editbarMenuLeft": "ep_PLUGINNAME/index",
        "collectContentPre": "ep_PLUGINNAME/static/js/shared",
        "collectContentPost": "ep_PLUGINNAME/static/js/shared",
        "padInitToolbar": "ep_PLUGINNAME/index",
        "getLineHTMLForExport": "ep_PLUGINNAME/index",
      }
    }
  ]
}
EOF

You can omit the FUNCTIONNAME part if the function to register has got the same name as the hook. So "authorize" : "ep_PLGUINNAME/YOURFILE" will call the function exports.authorize in ep_PLUGINRNAME/YOURFILE

Note that there is a property called "parts" that is an array. You can use multiple parts in a plugin, and all of the parts will load. This can be helpful simply for organizational purposes, or for managing dependencies better. Read more about Plugin dependencies.

package.json

This must be an ordinary npm package file, describing the name, version number, author, dependencies, etc. of your plugin. Additionally, it allows you to publish your plugin in the npm registry. Your peerDependencies value should be set to the Etherpad version in which the hooks you leverage are available. Please create your package.json with cat<<EOF >package.json ... EOF as listed below and edit the entries to your needs.

cat <<EOF >package.json
{
  "name": "ep_PLUGINNAME",
  "version": "0.0.1",
  "description": "DESCRIPTION",
  "author": {
    "name": "USERNAME (REAL NAME)",
    "email":  "<MAIL@EXAMPLE.COM>",
  },  
  "contributors": [
    "CONTRIBUTORNAME (REAL NAME)",
  ],  
  "keywords": [
    "etherpad",
    "plugin",
    "ep",
  ], 
  "license": {
    "Apache2"
  }, 
  "repository": {
    "type": "git",
    "url": "https://github.com/ether/myplugin.git",
  },
  "bugs": {
    "url": "https://github.com/ether/myplugin/issues",
  },
  "homepage": {
    "url": "https://github.com/ether/myplugin/README.md",
  },
  "funding": {
    "type": "individual",
    "url": "https://etherpad.org/",
  },
  "dependencies": {
    "MODULE": "0.3.20",
  }, 
  "peerDependencies": {
    "ep_etherpad-lite":">=1.8.6",
  }, 
  "devDependencies": {
    "eslint": "^7.18.0",
    "eslint-config-etherpad": "^1.0.24",
    "eslint-plugin-eslint-comments": "^3.2.0",
    "eslint-plugin-mocha": "^8.0.0",
    "eslint-plugin-node": "^11.1.0",
    "eslint-plugin-prefer-arrow": "^1.2.3",
    "eslint-plugin-promise": "^4.2.1",
    "eslint-plugin-you-dont-need-lodash-underscore": "^6.10.0",
  },
    "eslintConfig": {
    "root": true,
    "extends": "etherpad/plugin",
  },
  "scripts": {
    "lint": "eslint .",
    "lint:fix": "eslint --fix .",
  },
  "engines": { 
    "node": ">= 10.13.0",
  }
}
EOF

YOURFILE.js

A normal javascript file exporting the functions, that you want to register for their corresponding API hooks.

exports.FUNCTIONNAME1 = function(hook_name, args, cb){
  // ...
}

exports.FUNCTIONNAME2 = function(hook_name, args, cb){
 // ...
}

exports.FUNCTIONNAME3 = function(hook_name, args, cb){
 // ...
}

Creating your first plugin

Creating your own plugin in Etherpad Lite is really simple. You will need some basic Javascript understanding.

Step 1.

cd <path/to/etherpad-lite>/plugins-available
mkdir ep_123 ep_123/locales ep_123/static ep_123/static/css ep_123/static/js ep_123/static/image ep_123/static/tests ep_123/templates
mv ep_123 ep_my_example
cd ep_my_example
npm init

You can safely accept all the default settings suggested by npm init.

Step 2.

Please create your package.json with cat<<EOF >package.json ... EOF as listed below and edit the entries to your needs.

cat <<EOF >package.json
{
  "name": "ep_PLUGINNAME",
  "version": "0.0.1",
  "description": "DESCRIPTION",
  "author": {
    "name": "USERNAME (REAL NAME)",
    "email":  "<MAIL@EXAMPLE.COM>",
  },  
  "contributors": [
    "CONTRIBUTORNAME (REAL NAME)",
  ],  
  "keywords": [
    "etherpad",
    "plugin",
    "ep",
  ], 
  "license": {
    "Apache2"
  }, 
  "repository": {
    "type": "git",
    "url": "https://github.com/ether/myplugin.git",
  },
  "bugs": {
    "url": "https://github.com/ether/myplugin/issues",
  },
  "homepage": {
    "url": "https://github.com/ether/myplugin/README.md",
  },
  "funding": {
    "type": "individual",
    "url": "https://etherpad.org/",
  },
  "dependencies": {
    "MODULE": "0.3.20",
  }, 
  "peerDependencies": {
    "ep_etherpad-lite":">=1.8.6",
  }, 
  "devDependencies": {
    "eslint": "^7.18.0",
    "eslint-config-etherpad": "^1.0.24",
    "eslint-plugin-eslint-comments": "^3.2.0",
    "eslint-plugin-mocha": "^8.0.0",
    "eslint-plugin-node": "^11.1.0",
    "eslint-plugin-prefer-arrow": "^1.2.3",
    "eslint-plugin-promise": "^4.2.1",
    "eslint-plugin-you-dont-need-lodash-underscore": "^6.10.0",
  },
    "eslintConfig": {
    "root": true,
    "extends": "etherpad/plugin",
  },
  "scripts": {
    "lint": "eslint .",
    "lint:fix": "eslint --fix .",
  },
  "engines": { 
    "node": ">= 10.13.0",
  }
}
EOF

Step 3

Edit your plugin config file package.json to your needs. Please keep the order of its structure and add new entries if necessary at the end

{
  "name": "ep_my_example", // Your plugin name must begin with ep_ 
  "version": "0.0.1",
  "description": "Adds an Apple response on /apples",
  "author": {
    "name": "RedHog (Egil Moeller)",
    "email":  "<egil.moller@freecode.no>",
  },  
  "contributors": [
    "johnyma22 (John McLear) <john@mclear.co.uk>",
  ],  
  "keywords": [
    "etherpad",
    "plugin",
    "ep",
  ], 
  "license": {
    "Apache2"
  }, 
  "repository": {
    "type": "git",
    "url": "https://github.com/ether/myplugin.git",
  },
  "bugs": {
    "url": "https://github.com/ether/myplugin/issues",
  },
  "homepage": {
    "url": "https://github.com/ether/myplugin/README.md",
  },
  "funding": {
    "type": "individual",
    "url": "https://etherpad.org/",
  },
  "dependencies": {
    "MODULE": "0.3.20",
  }, 
  "peerDependencies": {
    "ep_etherpad-lite":">=1.8.6",
  }, 
  "devDependencies": {
    "eslint": "^7.18.0",
    "eslint-config-etherpad": "^1.0.24",
    "eslint-plugin-eslint-comments": "^3.2.0",
    "eslint-plugin-mocha": "^8.0.0",
    "eslint-plugin-node": "^11.1.0",
    "eslint-plugin-prefer-arrow": "^1.2.3",
    "eslint-plugin-promise": "^4.2.1",
    "eslint-plugin-you-dont-need-lodash-underscore": "^6.10.0",
  },
    "eslintConfig": {
    "root": true,
    "extends": "etherpad/plugin",
  },
  "scripts": {
    "lint": "eslint .",
    "lint:fix": "eslint --fix .",
  },
  "engines": { 
    "node": ">= 0.4.1 < 0.7.0",
  }
}

Step 4.

Please create your plugin config file ep.json using cat<ep.json ... EOF and then edit the file to your needs by keeping its base structure as is.

cat<<EOF >ep.json
{
  "parts": [
    {
      "name": "main",
      "client_hooks": {
        "aceEditEvent": "ep_PLUGINNAME/static/js/index",
        "postToolbarInit": "ep_PLUGINNAME/static/js/index",
        "aceDomLineProcessLineAttributes": "ep_PLUGINNAME/static/js/index",
        "postAceInit": "ep_PLUGINNAME/static/js/index",
        "aceInitialized": "ep_PLUGINNAME/static/js/index",
        "aceAttribsToClasses": "ep_PLUGINNAME/static/js/index",
        "collectContentPre": "ep_PLUGINNAME/static/js/shared",
        "aceRegisterBlockElements": "ep_PLUGINNAME/static/js/index",
      },
      "hooks": {
        "authorize": "ep_PLUGINNAME/YOURFILE:FUNCTIONNAME1",
        "authenticate": "ep_PLUGINNAME/YOURFILE:FUNCTIONNAME2",
        "expressCreateServer": "ep_PLUGINNAME/YOURFILE:FUNCTIONNAME3",
        "eejsBlock_editbarMenuLeft": "ep_PLUGINNAME/index",
        "collectContentPre": "ep_PLUGINNAME/static/js/shared",
        "collectContentPost": "ep_PLUGINNAME/static/js/shared",
        "padInitToolbar": "ep_PLUGINNAME/index",
        "getLineHTMLForExport": "ep_PLUGINNAME/index",
      }
    }
  ]
}
EOF

Step 5.

Edit your plugin config file ep.json to your needs by keeping its base structure as is.

{
  "parts": [
    {
      "name": "apples",
      "hooks": {
        "expressCreateServer": "ep_my_example/apples:expressCreateServer"
      }
    }
  ]
}

Step 6.

Create your README.md file if it is not existing already and add edit it to reflect your plugin name and plugin description with a minimum of 50 letters!

cat<<EOF >README.md
# My Example Plugin Name

![Publish Status](https://github.com/ether/ep_my_example/workflows/Node.js%20Package/badge.svg) ![Publish Status](https://github.com/ether/ep_my_example/workflows/Node.js%20Package/badge.svg)

![Screenshot](https://user-images.githubusercontent.com/220864/107214131-5c3dd600-6a01-11eb-82d9-b2d67ec8ae93.png)

## What is this?
An Etherpad Plugin to apply the my_example_plugin functionality (describe what it is) in a pad.  

Currently supports:
* My example functionality 1
* My example functionality 2
* My example functionality 3
* My example functionality 4

## Export support
Experimental.  As a special attribute on Etherpad, some weirdness may be experienced.

## History
I decided to copy/paste the ep_headings2 plugin over ep_align to fix a load of issues.

## License
Apache 2

## Development
Development did for free to help support continued learning due to disruption by the coronavirus.  26LLC supported.

## Author
My Example Plugin by XYZ

## Funding
Place your funding information here.

EOF

Step 7.

Create your LICENSE.md file and add your license (default is Apache2 for all created plugins with NO LICENSE mentioned)

cat<<EOF >LICENSE.md
Copyright 2021 [ENTER YOUR FULL REAL NAME]

Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
EOF

Step 8.

Create your locales/en.json etc. files and add the language data entries to your templates and js

data-l10n-id="ep_my_example.toolbar.left.title" - Please check how the translation data entries have been added to other ep_plugins

cat<<EOF >locales/en.json
{
  "ep_my_example.toolbar.left.title" : "Left"
}
EOF

Step 9.

Create your actual plugin Javascript code.

exports.expressCreateServer = function (hook_name, args, cb) {
  args.app.get('/apples', function(req, res) {
    res.send("<em>Abra cadabra</em>");
  });
}

Step 10.

Check again and revise if necessary your README.md, package.json, and ep.json as well as the language file and its tags. Does everything reflect the functionality of your plugin?

Step 11.

Install your plugin:

npm install ep_my_example

Step 12.

Start your server and test:

On linux, run:

$ ../../bin/run.sh

On windows:

..\..\start.bat

Now open http://localhost:9001/apples in your browser.

Respects some rules for styling your plugin

Basic template for a button icon in toolbar

<li class="separator"></li> <!-- Optional -->
<li class="ep_XXX-button">
  <a title="">
    <span class="buttonicon buttonicon-NAME_OF_THE_ICON"></span>
  </a>
</li>

See How to add a new icon to etherpad

Basic template for a popup

<div class="popup toolbar-popup ep_XXXX-popup" id="my-popup-id">
  <div class="popup-content">
    <!-- YOUR CONTENT -->
  </div>
</div>

Insert this template with eejsBlock_editorContainerBox hook.

to open/close it from javascript

$('#my-popup-id').addClass('popup-show'); // show
$('#my-popup-id').removeClass('popup-show'); // hide

For positioning the popup, you can either use javascript

$('#my-popup-id').css('left', $('.ep_XXX-button').offset().left);

Or simply positioning the popup or the left with

.ep_XXXX-popup {
  left: 30px;
  right: auto;
}

Check how it is done in ep_hyper_link for a simple example

Basic template for a button

<button class="btn btn-primary ep_XXXX-btn">My Button</button>
<button class="btn btn-default ep_XXXX-btn">My Button</button>

btn-primary is a colored button quite visible, use it for actions like save, submit... btn-default is a more discreet button, use it for action like cancel, go back...

Insert vertical containers (such as table of content, comments...)

use eejsBlock_editorContainerBox hook, have a look how it is done on ep_table_of_contents for a simple example

General rules

  • Do not change font property (font-family and font-size) inside your plugin, just use inherit one
  • Do not change the color or size of the text, use inherited values. If you want bigger text, use headings h1 or h2
  • Try to not use absolute positioning everywhere, flexboxes are often much more efficient
  • Do not add a property to the main DOM object like the #editorcontainerbox, or #outerdocbody iframe

Writing and running front-end tests for your plugin

Etherpad allows you to easily create front-end tests for plugins.

  1. Create a new folder
%your_plugin%/static/tests/frontend/specs
  1. Put your spec file in here (Example spec files are visible in %etherpad_root_folder%/tests/frontend/specs)

  2. Visit http://yourserver.com/tests/frontend/ your front-end tests will run.

Tip to ease your life

Move all the files from %etherpad_root_folder%/tests/frontend/specs folder elsewhere.
Do the same for the plugins that have a test file in their %your_plugin%/static/tests/frontend/specs folder (after the step before, just run the test to see what plugins have one).
That way, the time you do the tests on your front-end test, you will only have your that runs.
Once your test is good, don't forget to put back all the files.

Using the correct paths

Never use ../ in your require paths, this will break your plugins functionality in windows.. Instead you should use require('ep_etherpad-lite/node/utils/Settings'); as an example..

Publishing your plugin

To publish your plugin so it is available in /admin/plugins type:

npm adduser
npm publish

Providing settings from settings.json in your plugin

So you want to provide an API key or something? You have two ways to pass settings from the server to the client.

Option a) Render the setting inline with the eejs block

var settings = require('ep_etherpad-lite/node/utils/Settings');
var pluginSettings = settings.ep_MY_PLUGIN_NAME;
var checkFrequency = pluginSettings.settingOne || 60000;
var staleTime = pluginSettings.settingTwo || 300000;

exports.eejsBlock_editbarMenuRight = function (hook_name, args, cb) {
  args.content = args.content + "<script>alert('"+pluginSettings.settingOne+"');</script>";
  return cb();
};

Option b) Pass the setting as a clientVar -- This is often cleaner.

// On the server
var settings = require('ep_etherpad-lite/node/utils/Settings');
exports.clientVars = function(hook, context, callback)
{
  // return the setting to the clientVars, sending the value
  return callback({ "settingOne": settings.ep_MY_PLUGIN_NAME.settingOne });
};

// Also add in your file ep.json
..
  "hooks": {
    ...
    "clientVars": "ep_MY_PLUGIN_NAME/client:clientVars"
  },
..

// On the client
exports.postAceInit = function(hook, context){ // Once the editor has initialized
  alert(clientVars.settingOne); // clientVars is available globally..
}

With both options admins have to add some JSON to the end of their settings.json. Admins can accomplish this by modifying the file in their favourite editor or by visiting /admin/settings. We strongly recommend you use /admin/settings as this will validate your JSON and restart Etherpad for you.

Example setting block

"ep_MY_PLUGIN_NAME" : {
  settingOne: 6000,
  settingTwo: { 
    host: "127.0.0.1"
  }
}

The only gotcha here is that admins need to be aware that JSON requires a , between objects so need to prefix "ep_MY_PLUGIN_NAME" with a "," resulting in ",ep_MY_PLUGIN_NAME". We need a perma solution for this that makes it easier for plugins to manage settings and apply settings.

My plugin hasn't shown up in Etherpad and I published it with an ep_ prefix.

Due to an NPM bug we have to maintain the Etherpad plugin list.. This list is updated every hour and your plugin should be there after the next update. If it is still missing after hours please contact John - contact@etherpad.org

General

Resources

For Developers

How to's

Set up

Advanced steps

Integrating Etherpad in your web app

for Developers

Clone this wiki locally