{"id":5090,"date":"2010-06-30T11:06:21","date_gmt":"2010-06-30T18:06:21","guid":{"rendered":"http:\/\/4.107"},"modified":"2010-06-30T11:06:21","modified_gmt":"2010-06-30T18:06:21","slug":"tutorial-manipulating-text-through-commands","status":"publish","type":"post","link":"https:\/\/blog.mozilla.org\/labs\/2010\/06\/tutorial-manipulating-text-through-commands\/","title":{"rendered":"Tutorial: Manipulating Text Through Commands"},"content":{"rendered":"<p><em>In this tutorial, we\u2019ll be creating new commands that allow Bespin to work with <a href=\"http:\/\/daringfireball.net\/projects\/markdown\/\">Markdown<\/a> formatted text.<\/em><\/p>\n<p><!--more-->For this tutorial, <strong>you\u2019ll need the <a href=\"http:\/\/ftp.mozilla.org\/pub\/mozilla.org\/labs\/bespin\/markdown_js.js\">markdown_js plugin<\/a><\/strong>.<\/p>\n<h2 id=\"setting_up\">Setting Up<\/h2>\n<p>With this tutorial, we\u2019re starting from the very basics. You\u2019ll need Python (preferably 2.6) to do custom Bespin plugin development. If you don\u2019t have Python already, you can get it <a href=\"http:\/\/python.org\/\">prebuilt for your platform<\/a> with little fuss.<\/p>\n<p>You\u2019ll also need Bespin Embedded, which you can get from the <a href=\"http:\/\/ftp.mozilla.org\/pub\/mozilla.org\/labs\/bespin\/Embedded\/\">releases directory on ftp.mozilla.org<\/a>. This tutorial was written for <a href=\"http:\/\/ftp.mozilla.org\/pub\/mozilla.org\/labs\/bespin\/Embedded\/BespinEmbedded-0.9a1.tar.gz\">Bespin Embedded 0.9a1<\/a> (direct download link).<\/p>\n<p><em>Important note<\/em>: 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.<\/p>\n<p>Make a directory called <code>bespintutorial<\/code>. Uncompress the BespinEmbedded package in there and rename the directory from the tarfile <code>bespin<\/code> (so you\u2019ll have <code>bespintutorial\/bespin\/<\/code>). On a Mac, you can do these things from the Terminal command line:<\/p>\n<pre><code>mkdir bespintutorial\ncd bespintutorial\ncp ~\/Downloads\/BespinEmbedded-VERSION.tar.gz .\ntar xzf BespinEmbedded-VERSION.tar.gz\nmv BespinEmbedded-VERSION bespin\n<\/code>\n<\/pre>\n<h2 id=\"the_manifest\">The Manifest<\/h2>\n<p>Bespin\u2019s build tool, dryice, uses a \u201cmanifest\u201d 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 <code>manifest.json<\/code> in the <code>bespintutorial<\/code> directory. Here\u2019s what will go into the file at this step:<\/p>\n<pre><code>{\n    \"output_dir\": \"..\/build\",\n    \"plugins\": [\"embedded\"],\n    \"search_path\": [\"..\"]\n}<\/code>\n<\/pre>\n<p>We\u2019ll be running the build from the <code>bespin<\/code> directory, so those <code>..<\/code> in the manifest are referring to the <code>bespintutorial<\/code> directory.<\/p>\n<p>Now, we\u2019ll fire up the dryice server. This command assumes at Python is on your path:<\/p>\n<pre><code>cd bespin\npython dryice.py -s 8080 ..\/manifest.json\n<\/code>\n<\/pre>\n<p>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.<\/p>\n<p>Next, place the <code>markdown_js.js<\/code> file which you got at the beginning of this tutorial into the <code>bespintutorial<\/code> directory.<\/p>\n<h2 id=\"getting_our_plugin_going\">Getting Our Plugin Going<\/h2>\n<p>Now, we\u2019ll create a new file called <code>markdown.js<\/code>. This is our Bespin plugin file. Bespin plugins all have a metadata section, so let\u2019s put that at the top of our file to get us going:<\/p>\n<pre><code>\"define metadata\";\n({\n    \"dependencies\": {\n        \"markdown_js\": \"0.1.2\"\n    }\n});\n\"end\";\n<\/code>\n<\/pre>\n<p>So, that\u2019s our whole plugin for now. All we\u2019re saying is that our plugin depends on the <code>markdown_js<\/code> plugin (version 0.1.2). Now we have to tell dryice about our new plugin, so we\u2019ll add it to the plugins line of our manifest.json file:<\/p>\n<pre><code>\"plugins\": [\"embedded\", \"markdown\"],\n<\/code>\n<\/pre>\n<p>You should be able to reload http:\/\/localhost:8080\/ and have things still work.<\/p>\n<h2 id=\"making_our_plugin_do_something\">Making Our Plugin Do Something<\/h2>\n<p>Since we\u2019re going to be making commands, it would be most convenient for us at this point to have a command line. So, we\u2019ll add the <code>command_line<\/code> plugin to our manifest.json file:<\/p>\n<pre><code>\"plugins\": [\"embedded\", \"markdown\", \"command_line\"],\n<\/code>\n<\/pre>\n<p>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\u2019s enable that, too:<\/p>\n<pre><code>\"plugins\": [\"embedded\", \"markdown\", \"command_line\", \"uicommands\"],\n<\/code>\n<\/pre>\n<p>Now we\u2019re ready to make our first command. We\u2019re going to make a command that:<\/p>\n<ol>\n<li>converts the Markdown text we\u2019ve entered to HTML<\/li>\n<li>puts that HTML in a new window so we can preview it.<\/li>\n<\/ol>\n<p>Let\u2019s write a function that does these things.<\/p>\n<pre><code>var env = require('environment').env;\nvar markdown = require(\"markdown_js\");\n\nexports.preview = function(args, request) {\n    var text = env.editor.selectedText;\n    if (!text) {\n        text = env.editor.value;\n    }\n    var popup = window.open(\"\", \"_blank\", \"location=no,menubar=no\");\n    popup.document.body.innerHTML = markdown.toHTML(text);\n    request.done();\n};\n<\/code>\n<\/pre>\n<p>The first two lines import other modules that we\u2019ll be needing. The <code>environment<\/code> plugin is always available in Bespin, and the <code>env<\/code> variable inside of there is very handy, as we\u2019ll soon see. We also import the <code>markdown_js<\/code> module that we declared in our dependencies. We\u2019ll call it <code>markdown<\/code> when we use it in this module for convenience.<\/p>\n<p>If you\u2019re not familiar with <a href=\"http:\/\/commonjs.org\/specs\/modules\/1.0\/\">CommonJS Modules<\/a>, you\u2019ll find that they\u2019re simple to work with. You use the <code>require<\/code> function to import modules and you put anything you want to be available outside of the module on the <code>exports<\/code> object. We\u2019re going to make a function called <code>preview<\/code> available from this module.<\/p>\n<p>Bespin command functions all take two parameters: <code>args<\/code> and <code>request<\/code>. <code>args<\/code> contains the incoming arguments to the command and <code>request<\/code> provides methods for working with this particular request from the user. Of these, the commands we\u2019ll be making here today only make use of <code>request.done()<\/code>, which signals that we have finished all of our processing.<\/p>\n<p>Now, let\u2019s focus on the body of the function:<\/p>\n<pre><code>var text = env.editor.selectedText;\nif (!text) {\n    text = env.editor.value;\n}\nvar popup = window.open(\"\", \"_blank\", \"location=no,menubar=no\");\npopup.document.body.innerHTML = markdown.toHTML(text);\nrequest.done();\n<\/code>\n<\/pre>\n<p>The first line gets us the currently selected text. If the user selected only a portion of the document, we\u2019ll preview that. The next line looks to see if there was any selected text. If there wasn\u2019t, then we just grab all of the text from the editor.<\/p>\n<p>Next, we use the standard window.open call to make a new window. A call to <code>markdown.toHTML<\/code> will give us the HTML version of our text and we drop that into our new window and we\u2019re all set!<\/p>\n<p>We need to register our new command with Bespin.<\/p>\n<p>Since we\u2019re going to create more than one Markdown related command, we\u2019ll plan to create a top-level <code>markdown<\/code> command with subcommands. The command we\u2019re working on now is <code>markdown preview<\/code>. We need to add a new <code>provides<\/code> section to our JSON metadata. Here\u2019s what that looks like:<\/p>\n<pre><code>\"provides\": [\n    {\n        \"ep\": \"command\",\n        \"name\": \"markdown\",\n        \"description\": \"commands for working with markdown files\"\n    },\n    {\n        \"ep\": \"command\",\n        \"name\": \"markdown preview\",\n        \"description\": \"preview the HTML form of this markdown text\",\n        \"pointer\": \"#preview\"\n    }\n]\n<\/code>\n<\/pre>\n<p><code>provides<\/code> gives a list of extensions that are provided by this plugin. Each of those will have an <code>ep<\/code> at a minimum. That\u2019s the \u201cextension point\u201d that we\u2019re 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.<\/p>\n<p>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 <code>pointer<\/code>. The <code>pointer<\/code> tells Bespin where to find the object (in this case, a function) for the extension. <code>#preview<\/code> is equivalent to <code>markdown:index#preview<\/code> which means the <code>preview<\/code> function in the <code>index<\/code> module in the <code>markdown<\/code> plugin.<\/p>\n<p>Your complete file should now look like this:<\/p>\n<pre><code>\"define metadata\";\n({\n    \"dependencies\": {\n        \"markdown_js\": \"0.1.2\"\n    },\n    \"provides\": [\n        {\n            \"ep\": \"command\",\n            \"name\": \"markdown\",\n            \"description\": \"commands for working with markdown files\"\n        },\n        {\n            \"ep\": \"command\",\n            \"name\": \"markdown preview\",\n            \"description\": \"preview the HTML form of this markdown text\",\n            \"pointer\": \"#preview\"\n        }\n    ]\n});\n\"end\";\n\nvar env = require('environment').env;\nvar markdown = require(\"markdown_js\");\n\nexports.preview = function(args, request) {\n    var text = env.editor.selectedText;\n    if (!text) {\n        text = env.editor.value;\n    }\n    var popup = window.open(\"\", \"_blank\", \"location=no,menubar=no\");\n    popup.document.body.innerHTML = markdown.toHTML(text);\n    request.done();\n};\n<\/code>\n<\/pre>\n<p>Let\u2019s come up with some sample Markdown to use. How about this:<\/p>\n<pre><code># Markdown Test #\n\nThis is a *simple* test of Markdown.\n\n* one\n* two\n* three\n<\/code>\n<\/pre>\n<p>Reload your browser, select all of the text and paste in that text. Then, jump down to the command line and run the <code>markdown preview<\/code> command. You should see a new window popup with the HTML version of the text there.<\/p>\n<p>Congratulations! You\u2019ve extended Bespin with a new command.<\/p>\n<h2 id=\"keyboard_shortcut\">Keyboard Shortcut<\/h2>\n<p>Previewing the HTML seems like a very useful feature. Why don\u2019t we add a keyboard shortcut so that we don\u2019t need to go to the command line each time. To do this, we just need to add a <code>key<\/code> to the <code>markdown preview<\/code> metadata, like so:<\/p>\n<pre><code>{\n    \"ep\": \"command\",\n    \"name\": \"markdown preview\",\n    \"description\": \"preview the HTML form of this markdown text\",\n    \"key\": \"ctrl_shift_p\",\n    \"pointer\": \"#preview\"\n}\n<\/code>\n<\/pre>\n<p>Now, cmd-shift-P on the Mac or ctrl-shift-P on Windows\/Linux will run the <code>markdown preview<\/code> command. This also means that we can run the <code>markdown preview<\/code> command even if we don\u2019t have the command line plugin in our build.<\/p>\n<p>An aside about keyboard shortcuts: the <code>key<\/code> defined in command metadata like this is the lowest priority keyboard binding. A keymapping plugin can redefine the keys as can user preferences.<\/p>\n<h2 id=\"replacing_text\">Replacing Text<\/h2>\n<p>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.<\/p>\n<p>We\u2019ll make a <code>markdown convert<\/code> command that converts the text to HTML and then puts the text back into the editor. As before, we\u2019ll start by writing a function that does this conversion and we\u2019ll place it at the bottom of our file:<\/p>\n<pre><code>exports.convert = function(args, request) {\n    var allOrSelection = 'selectedText';\n    var text = env.editor.selectedText;\n    if (!text) {\n        allOrSelection = 'value';\n        text = env.editor.value;\n    }\n    var html = markdown.toHTML(text);\n    env.editor[allOrSelection] = html;\n    request.done();\n};\n<\/code>\n<\/pre>\n<p>In this function, we\u2019re going to do the same kind of thing we did in the <code>preview<\/code> function. We\u2019ll work with the user\u2019s 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\u2019re only replacing the text that the user had selected.<\/p>\n<p>Once we\u2019ve gotten our text, we can use <code>markdown.toHTML<\/code> to do the conversion. Then, we put the text back into the editor (going into either <code>selectedText<\/code> or <code>value<\/code>, depending on where the text came from originally).<\/p>\n<p>We need to tell Bespin about our new command, so we\u2019ll add another object to the <code>provides<\/code> part of our metadata.<\/p>\n<pre><code>{\n    \"ep\": \"command\",\n    \"name\": \"markdown convert\",\n    \"description\": \"convert the selected text to HTML\",\n    \"pointer\": \"#convert\"\n}\n<\/code>\n<\/pre>\n<p>Looks just like the <code>markdown preview<\/code> command, doesn\u2019t it?<\/p>\n<p>Reload the page in your browser, paste in the sample markdown text and then run the <code>markdown convert<\/code> command. You should see the text converted to HTML. Try converting just a section of the text, and you\u2019ll see that just that portion of the file is modified.<\/p>\n<p>That was easy, wasn\u2019t it?<\/p>\n<h2 id=\"undo\">Undo<\/h2>\n<p>Try converting the Markdown to HTML and then pressing cmd\/ctrl-Z.<\/p>\n<h2 id=\"submitting_to_the_bespin_plugin_gallery\">Submitting to the Bespin Plugin Gallery<\/h2>\n<p>Once you\u2019ve 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&#8230; but it&#8217;s coming soon, so you can follow this section to be ready when it&#8217;s here.)<\/p>\n<p>We\u2019ll add version, license and maintainer information:<\/p>\n<pre><code>\"version\": \"1.0.0\",\n\"maintainers\": [\n    {\n        \"name\": \"Kevin Dangoor\",\n        \"email\": \"kid@blazingthings.com\",\n        \"web\": \"http:\/\/blueskyonmars.com\/\"\n    }\n],\n\"licenses\": [\n    {\n        \"type\": \"MPL\",\n        \"url\": \"http:\/\/www.mozilla.org\/MPL\/MPL-1.1.html\"\n    },\n    {\n        \"type\": \"GPL\",\n        \"url\": \"http:\/\/creativecommons.org\/licenses\/GPL\/2.0\/\"\n    },\n    {\n        \"type\": \"LGPL\",\n        \"url\": \"http:\/\/creativecommons.org\/licenses\/LGPL\/2.1\/\"\n    }\n]\n<\/code>\n<\/pre>\n<p>Bespin Plugin metadata is actually a superset of the <a href=\"http:\/\/wiki.commonjs.org\/wiki\/Packages\/1.0\">CommonJS package<\/a> metadata. As specified there, the version numbers should follow the <a href=\"http:\/\/semver.org\/\">Semantic Versioning<\/a> numbering so that useful information about compatibility can be picked up from the version number alone.<\/p>\n<p>I added myself as a maintainer and made this plugin available under the tri-license that Bespin itself is available under.<\/p>\n<p>With all of this metadata in place, the final plugin file looks like this:<\/p>\n<pre><code>\"define metadata\";\n({\n    \"dependencies\": {\n        \"markdown_js\": \"0.1.2\"\n    },\n    \"provides\": [\n        {\n            \"ep\": \"command\",\n            \"name\": \"markdown\",\n            \"description\": \"commands for working with markdown files\"\n        },\n        {\n            \"ep\": \"command\",\n            \"name\": \"markdown preview\",\n            \"description\": \"preview the HTML form of this markdown text\",\n            \"key\": \"ctrl_shift_p\",\n            \"pointer\": \"#preview\"\n        },\n        {\n            \"ep\": \"command\",\n            \"name\": \"markdown convert\",\n            \"description\": \"convert the selected text to HTML\",\n            \"pointer\": \"#convert\"\n        }\n    ],\n    \"version\": \"1.0.0\",\n    \"maintainers\": [\n        {\n            \"name\": \"Kevin Dangoor\",\n            \"email\": \"kid@blazingthings.com\",\n            \"web\": \"http:\/\/blueskyonmars.com\/\"\n        }\n    ],\n    \"licenses\": [\n        {\n            \"type\": \"MPL\",\n            \"url\": \"http:\/\/www.mozilla.org\/MPL\/MPL-1.1.html\"\n        },\n        {\n            \"type\": \"GPL\",\n            \"url\": \"http:\/\/creativecommons.org\/licenses\/GPL\/2.0\/\"\n        },\n        {\n            \"type\": \"LGPL\",\n            \"url\": \"http:\/\/creativecommons.org\/licenses\/LGPL\/2.1\/\"\n        }\n    ]\n});\n\"end\";\n\nvar env = require('environment').env;\nvar markdown = require(\"markdown_js\");\n\nexports.preview = function(args, request) {\n    var text = env.editor.selectedText;\n    if (!text) {\n        text = env.editor.value;\n    }\n    var popup = window.open(\"\", \"_blank\", \"location=no,menubar=no\");\n    popup.document.body.innerHTML = markdown.toHTML(text);\n    request.done();\n};\n\nexports.convert = function(args, request) {\n    var allOrSelection = 'selectedText';\n    var text = env.editor.selectedText;\n    if (!text) {\n        allOrSelection = 'value';\n        text = env.editor.value;\n    }\n    var html = markdown.toHTML(text);\n    env.editor[allOrSelection] = html;\n    request.done();\n};\n<\/code>\n<\/pre>\n<p>We can upload this single .js file to the plugin gallery to share it with others. Once we do, we\u2019ll 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.<\/p>\n<h2 id=\"the_end\">The End<\/h2>\n<p>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:<\/p>\n<ul>\n<li>using dryice server mode to test new plugins<\/li>\n<li>plugin structure and metadata<\/li>\n<li>the mechanics of creating an extension<\/li>\n<li>how to write command extensions<\/li>\n<li>how to manipulate the text in the editor<\/li>\n<\/ul>\n","protected":false},"excerpt":{"rendered":"<p>In this tutorial, we\u2019ll be creating new commands that allow Bespin to work with Markdown formatted text.<\/p>\n","protected":false},"author":456,"featured_media":0,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[1],"tags":[],"_links":{"self":[{"href":"https:\/\/blog.mozilla.org\/labs\/wp-json\/wp\/v2\/posts\/5090"}],"collection":[{"href":"https:\/\/blog.mozilla.org\/labs\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/blog.mozilla.org\/labs\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/blog.mozilla.org\/labs\/wp-json\/wp\/v2\/users\/456"}],"replies":[{"embeddable":true,"href":"https:\/\/blog.mozilla.org\/labs\/wp-json\/wp\/v2\/comments?post=5090"}],"version-history":[{"count":0,"href":"https:\/\/blog.mozilla.org\/labs\/wp-json\/wp\/v2\/posts\/5090\/revisions"}],"wp:attachment":[{"href":"https:\/\/blog.mozilla.org\/labs\/wp-json\/wp\/v2\/media?parent=5090"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/blog.mozilla.org\/labs\/wp-json\/wp\/v2\/categories?post=5090"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/blog.mozilla.org\/labs\/wp-json\/wp\/v2\/tags?post=5090"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}