In this tutorial, we’ll be creating new commands that allow Bespin to work with Markdown formatted text.
For this tutorial, you’ll need the markdown_js plugin.
Setting Up
With this tutorial, we’re starting from the very basics. You’ll need Python (preferably 2.6) to do custom Bespin plugin development. If you don’t have Python already, you can get it prebuilt for your platform with little fuss.
You’ll also need Bespin Embedded, which you can get from the releases directory on ftp.mozilla.org. This tutorial was written for Bespin Embedded 0.9a1 (direct download link).
Important note: if your copy of Bespin Embedded has a plugins/mine directory, delete that directory before doing this tutorial. That directory was added to the release in error and includes plugins that conflict with the ones needed here.
Make a directory called bespintutorial
. Uncompress the BespinEmbedded package in there and rename the directory from the tarfile bespin
(so you’ll have bespintutorial/bespin/
). On a Mac, you can do these things from the Terminal command line:
mkdir bespintutorial
cd bespintutorial
cp ~/Downloads/BespinEmbedded-VERSION.tar.gz .
tar xzf BespinEmbedded-VERSION.tar.gz
mv BespinEmbedded-VERSION bespin
The Manifest
Bespin’s build tool, dryice, uses a “manifest” file in JSON format to describe what it needs to build and where to find all of the parts. Open up your text editor to create a file called manifest.json
in the bespintutorial
directory. Here’s what will go into the file at this step:
{
"output_dir": "../build",
"plugins": ["embedded"],
"search_path": [".."]
}
We’ll be running the build from the bespin
directory, so those ..
in the manifest are referring to the bespintutorial
directory.
Now, we’ll fire up the dryice server. This command assumes at Python is on your path:
cd bespin
python dryice.py -s 8080 ../manifest.json
After running that command, you can open up your browser to http://localhost:8080/ and you should see the Bespin editor that dryice just built for us.
Next, place the markdown_js.js
file which you got at the beginning of this tutorial into the bespintutorial
directory.
Getting Our Plugin Going
Now, we’ll create a new file called markdown.js
. This is our Bespin plugin file. Bespin plugins all have a metadata section, so let’s put that at the top of our file to get us going:
"define metadata";
({
"dependencies": {
"markdown_js": "0.1.2"
}
});
"end";
So, that’s our whole plugin for now. All we’re saying is that our plugin depends on the markdown_js
plugin (version 0.1.2). Now we have to tell dryice about our new plugin, so we’ll add it to the plugins line of our manifest.json file:
"plugins": ["embedded", "markdown"],
You should be able to reload http://localhost:8080/ and have things still work.
Making Our Plugin Do Something
Since we’re going to be making commands, it would be most convenient for us at this point to have a command line. So, we’ll add the command_line
plugin to our manifest.json file:
"plugins": ["embedded", "markdown", "command_line"],
Reload the page in your browser and you should see the command line at the bottom. I also like being able to press cmd-J (ctrl-J on Windows/Linux) to switch between the command line and the editor. The uicommands plugin gives us that, so let’s enable that, too:
"plugins": ["embedded", "markdown", "command_line", "uicommands"],
Now we’re ready to make our first command. We’re going to make a command that:
- converts the Markdown text we’ve entered to HTML
- puts that HTML in a new window so we can preview it.
Let’s write a function that does these things.
var env = require('environment').env;
var markdown = require("markdown_js");
exports.preview = function(args, request) {
var text = env.editor.selectedText;
if (!text) {
text = env.editor.value;
}
var popup = window.open("", "_blank", "location=no,menubar=no");
popup.document.body.innerHTML = markdown.toHTML(text);
request.done();
};
The first two lines import other modules that we’ll be needing. The environment
plugin is always available in Bespin, and the env
variable inside of there is very handy, as we’ll soon see. We also import the markdown_js
module that we declared in our dependencies. We’ll call it markdown
when we use it in this module for convenience.
If you’re not familiar with CommonJS Modules, you’ll find that they’re simple to work with. You use the require
function to import modules and you put anything you want to be available outside of the module on the exports
object. We’re going to make a function called preview
available from this module.
Bespin command functions all take two parameters: args
and request
. args
contains the incoming arguments to the command and request
provides methods for working with this particular request from the user. Of these, the commands we’ll be making here today only make use of request.done()
, which signals that we have finished all of our processing.
Now, let’s focus on the body of the function:
var text = env.editor.selectedText;
if (!text) {
text = env.editor.value;
}
var popup = window.open("", "_blank", "location=no,menubar=no");
popup.document.body.innerHTML = markdown.toHTML(text);
request.done();
The first line gets us the currently selected text. If the user selected only a portion of the document, we’ll preview that. The next line looks to see if there was any selected text. If there wasn’t, then we just grab all of the text from the editor.
Next, we use the standard window.open call to make a new window. A call to markdown.toHTML
will give us the HTML version of our text and we drop that into our new window and we’re all set!
We need to register our new command with Bespin.
Since we’re going to create more than one Markdown related command, we’ll plan to create a top-level markdown
command with subcommands. The command we’re working on now is markdown preview
. We need to add a new provides
section to our JSON metadata. Here’s what that looks like:
"provides": [
{
"ep": "command",
"name": "markdown",
"description": "commands for working with markdown files"
},
{
"ep": "command",
"name": "markdown preview",
"description": "preview the HTML form of this markdown text",
"pointer": "#preview"
}
]
provides
gives a list of extensions that are provided by this plugin. Each of those will have an ep
at a minimum. That’s the “extension point” that we’re plugging into. Our first extension is a command and we give the command a name (which is what the user types) and a description that will appear in help text.
The second extension is for our preview command. It has a name and description as well. But, since this command is not just a holder for other commands, it also has a pointer
. The pointer
tells Bespin where to find the object (in this case, a function) for the extension. #preview
is equivalent to markdown:index#preview
which means the preview
function in the index
module in the markdown
plugin.
Your complete file should now look like this:
"define metadata";
({
"dependencies": {
"markdown_js": "0.1.2"
},
"provides": [
{
"ep": "command",
"name": "markdown",
"description": "commands for working with markdown files"
},
{
"ep": "command",
"name": "markdown preview",
"description": "preview the HTML form of this markdown text",
"pointer": "#preview"
}
]
});
"end";
var env = require('environment').env;
var markdown = require("markdown_js");
exports.preview = function(args, request) {
var text = env.editor.selectedText;
if (!text) {
text = env.editor.value;
}
var popup = window.open("", "_blank", "location=no,menubar=no");
popup.document.body.innerHTML = markdown.toHTML(text);
request.done();
};
Let’s come up with some sample Markdown to use. How about this:
# Markdown Test #
This is a *simple* test of Markdown.
* one
* two
* three
Reload your browser, select all of the text and paste in that text. Then, jump down to the command line and run the markdown preview
command. You should see a new window popup with the HTML version of the text there.
Congratulations! You’ve extended Bespin with a new command.
Keyboard Shortcut
Previewing the HTML seems like a very useful feature. Why don’t we add a keyboard shortcut so that we don’t need to go to the command line each time. To do this, we just need to add a key
to the markdown preview
metadata, like so:
{
"ep": "command",
"name": "markdown preview",
"description": "preview the HTML form of this markdown text",
"key": "ctrl_shift_p",
"pointer": "#preview"
}
Now, cmd-shift-P on the Mac or ctrl-shift-P on Windows/Linux will run the markdown preview
command. This also means that we can run the markdown preview
command even if we don’t have the command line plugin in our build.
An aside about keyboard shortcuts: the key
defined in command metadata like this is the lowest priority keyboard binding. A keymapping plugin can redefine the keys as can user preferences.
Replacing Text
One more common requirement of commands is that they need to be able to manipulate the text in some fashion and then put the manipulated version back into the editor.
We’ll make a markdown convert
command that converts the text to HTML and then puts the text back into the editor. As before, we’ll start by writing a function that does this conversion and we’ll place it at the bottom of our file:
exports.convert = function(args, request) {
var allOrSelection = 'selectedText';
var text = env.editor.selectedText;
if (!text) {
allOrSelection = 'value';
text = env.editor.value;
}
var html = markdown.toHTML(text);
env.editor[allOrSelection] = html;
request.done();
};
In this function, we’re going to do the same kind of thing we did in the preview
function. We’ll work with the user’s selection, if there is one, and the complete text otherwise. In this case, we need to keep track of which it was so that, when we do the replacement, we’re only replacing the text that the user had selected.
Once we’ve gotten our text, we can use markdown.toHTML
to do the conversion. Then, we put the text back into the editor (going into either selectedText
or value
, depending on where the text came from originally).
We need to tell Bespin about our new command, so we’ll add another object to the provides
part of our metadata.
{
"ep": "command",
"name": "markdown convert",
"description": "convert the selected text to HTML",
"pointer": "#convert"
}
Looks just like the markdown preview
command, doesn’t it?
Reload the page in your browser, paste in the sample markdown text and then run the markdown convert
command. You should see the text converted to HTML. Try converting just a section of the text, and you’ll see that just that portion of the file is modified.
That was easy, wasn’t it?
Undo
Try converting the Markdown to HTML and then pressing cmd/ctrl-Z.
Submitting to the Bespin Plugin Gallery
Once you’ve completed creating a plugin that you want to share with the rest of the world, you should add a little more to the metadata before uploading your plugin to the Bespin Plugin Gallery. (As of this writing, the Bespin Plugin Gallery has not yet been released… but it’s coming soon, so you can follow this section to be ready when it’s here.)
We’ll add version, license and maintainer information:
"version": "1.0.0",
"maintainers": [
{
"name": "Kevin Dangoor",
"email": "kid@blazingthings.com",
"web": "http://blueskyonmars.com/"
}
],
"licenses": [
{
"type": "MPL",
"url": "http://www.mozilla.org/MPL/MPL-1.1.html"
},
{
"type": "GPL",
"url": "http://creativecommons.org/licenses/GPL/2.0/"
},
{
"type": "LGPL",
"url": "http://creativecommons.org/licenses/LGPL/2.1/"
}
]
Bespin Plugin metadata is actually a superset of the CommonJS package metadata. As specified there, the version numbers should follow the Semantic Versioning numbering so that useful information about compatibility can be picked up from the version number alone.
I added myself as a maintainer and made this plugin available under the tri-license that Bespin itself is available under.
With all of this metadata in place, the final plugin file looks like this:
"define metadata";
({
"dependencies": {
"markdown_js": "0.1.2"
},
"provides": [
{
"ep": "command",
"name": "markdown",
"description": "commands for working with markdown files"
},
{
"ep": "command",
"name": "markdown preview",
"description": "preview the HTML form of this markdown text",
"key": "ctrl_shift_p",
"pointer": "#preview"
},
{
"ep": "command",
"name": "markdown convert",
"description": "convert the selected text to HTML",
"pointer": "#convert"
}
],
"version": "1.0.0",
"maintainers": [
{
"name": "Kevin Dangoor",
"email": "kid@blazingthings.com",
"web": "http://blueskyonmars.com/"
}
],
"licenses": [
{
"type": "MPL",
"url": "http://www.mozilla.org/MPL/MPL-1.1.html"
},
{
"type": "GPL",
"url": "http://creativecommons.org/licenses/GPL/2.0/"
},
{
"type": "LGPL",
"url": "http://creativecommons.org/licenses/LGPL/2.1/"
}
]
});
"end";
var env = require('environment').env;
var markdown = require("markdown_js");
exports.preview = function(args, request) {
var text = env.editor.selectedText;
if (!text) {
text = env.editor.value;
}
var popup = window.open("", "_blank", "location=no,menubar=no");
popup.document.body.innerHTML = markdown.toHTML(text);
request.done();
};
exports.convert = function(args, request) {
var allOrSelection = 'selectedText';
var text = env.editor.selectedText;
if (!text) {
allOrSelection = 'value';
text = env.editor.value;
}
var html = markdown.toHTML(text);
env.editor[allOrSelection] = html;
request.done();
};
We can upload this single .js file to the plugin gallery to share it with others. Once we do, we’ll have a change to add images and further description of the plugin if we wish. You can actually add a description to the plugin metadata itself, if you wish.
The End
In this tutorial, we created a brand new plugin that leveraged an existing JavaScript library to do useful text transformation in Bespin. Key concepts covered:
- using dryice server mode to test new plugins
- plugin structure and metadata
- the mechanics of creating an extension
- how to write command extensions
- how to manipulate the text in the editor