Writing a simple Windows Live Writer plugin

Although I love using Windows Live Writer (WLW) to write blog posts here and at work, one of the problems I've had with it is that, out of the box, it's mostly geared to what you might call "simple" posts. Normal text, bold, italics, underline, bullets, numbered lists, tables, images: no sweat. Words or phrases marked with <code> not at all, and that's something I tend to use a lot of when writing a "technical" blog post. A quotation with <blockquote>? Nah, sorry.

How about special characters? Such as the em dash — something I love using for a parenthetical observation, without having to use parentheses — or the copyright symbol, ©? Nope. You'll have to go into HTML view and fiddle with the entity coding directly. And as for «chevrons», fugeddaboutit.

I've investigated some plugins out there, and have used a couple but there's nothing that's grabbed me and the ones I've used have been a bit of a pain. Then, today, it all came to the fore reading a post by Ned Batchelder where he discusses how he needed some special characters in his posts and added some extra code to Django to display them.

So finally, with my wife watching an episode of The Tudors, I took the time to play around with a couple of ideas.

WLW is extensible in the sense it has an API where you can add content to the text in the editor. (Since MSDN seems to rewrite its URLs every now and then, that link may not last, so search for "Windows Live Writer SDK" instead.) The simplest way is to create a simple Content Source Plugin, and I quote from MSDN:

To create a new Content Source Plugin:

  1. Create a new .NET Class Library project using Visual Studio 2003 (.NET 1.1), Visual Studio 2005 (.NET 2.0), or any other development environment that supports creating .NET assemblies.
  2. Add a reference to the WindowsLive.Writer.Api assembly (located in the directory C:\Program Files\Windows Live\Writer).
  3. Create a new class derived from one of the two content source base classes (ContentSource or SmartContentSource).
  4. Apply the WriterPluginAttribute to the new class. Optionally, if you want an image to appear alongside your plugin within the Writer user interface you should specify the WriterPluginAttribute.ImagePath property as part of this attribute.
  5. Decide which content creation sources your plugin will support and add the required attributes and method overrides:
  6. Add the following command to the Post Build event of your project (this will copy your plugin to the Writer plugins directory after it is built):
    XCOPY /D /Y /R "$(TargetPath)" "C:\Program Files\Windows Live\Writer\Plugins\"
  7. Build your plugin and then run Windows Live Writer to test and debug.

Simple enough, indeed. Mind you step 6 requires you run Visual Studio elevated (you're writing to Program Files after all) or it doesn't work. (I'm also using 64-bit Vista, so it's "Program Files (x86)".) I spent a fruitless few minutes searching for a simple way of elevating before running the command, but failed and just copied the DLL manually.

But it's all about showing a dialog (the CreateContent method takes a dialog window handle). Unfortunately, I really didn't want to show a dialog. Using CodeRush and Refactor! Pro a lot, I've come to appreciate the "I want to stay in the editor all the time" philosophy. I did look for some extensibility point where I could get at the post before it was written to whatever blog engine you had — I could then do some search-and-replace type work — but didn't see anything along those lines.

I thought some more and came up with a plan. My idea was to highlight some text and then call the plugin and it would format the text in some way. But, again, I didn't want to show a dialog to allow the user to say how the formatting should work. So I decided to prepend the text with a formatting command. If I wrote cd:SomeIdentifier in WLW, highlighted the text including the "cd:" part, and then selected Insert, my plugin, the code would strip off the cd: command, surround the rest of the text with and and return it. No dialog. The command for blockquote could be bq:, for a character entity, ch:, and so on.

Talking of character entities, this command structure worked very well. All I needed to do was supply a "mnemonic" after the ch: for the character I wanted. So, for example, ch:<< would produce «, ch:-- (two dashes) would produce my favorite em dash whereas one dash an en dash, and so on.

The little language then was easy, command:text, with the command being two characters long. The text could be what ever you wanted, including paragraph breaks, bullet lists, etc. (See the quotation above, in fact: created by a copy and paste from MSDN, prepending with bq:, highlighting it all and then calling the plugin.)

Not so fast, Mr Bucknall. I was running into some bizarre problems where the plugin seemed to work, and then sometimes not. Some debugging later I realized that the text passed into the CreateContent method was HTML, and could include things like paragraph tags (</p> and </p>) especially if I was unfortunate to include the beginning or end of a paragraph. So I had to be careful about stripping out the command (hence commands are always two characters) and the colon from the selected text.

The CreateContent method:

   1: public override DialogResult CreateContent(IWin32Window dialogOwner, ref string content) {
   2:   DialogResult result = DialogResult.Cancel;
   3:   
   4:   if (!string.IsNullOrEmpty(content)) {
   5:     string[] parms = content.Trim().Split(new char[] { ':' }, 2);
   6:     if ((parms.Length == 2) && (parms[0].Length >= 2)) {
   7:       result = DialogResult.OK;
   8:       
   9:       string command = parms[0].Substring(parms[0].Length - 2);
  10:       parms[0] = parms[0].Substring(0, parms[0].Length - 2);
  11:  
  12:       switch (command) {
  13:         case "bq":
  14:           content = Embed(parms[0] + parms[1], "blockquote");
  15:           break;
  16:         case "cd":
  17:           content = Embed(parms[0] + parms[1], "code");
  18:           break;
  19:         case "ch":
  20:           content = ConvertEntity(parms[1]);
  21:           break;
  22:         default:
  23:           content = MakeStrong("Unknown command: " + content);
  24:           break;
  25:       }
  26:     }
  27:   }
  28:   return result;
  29: }

First of all I use the return value to indicate whether the text should be replaced or not. Returning DialogResult.Cancel means leave it alone, DialogResult.OK means replace it, as if a dialog were displayed and the user clicked on the relevant button. I then extract the command and process it. Embed does what you think it does, embeds some text inside of the given start and end tags. ConvertEntity uses a pre-built dictionary of character mnemonics and their HTML entity values to convert a mnemonic to its safe HTML representation.

Things still to do: at the moment, if I need another command I have to edit the source file and rebuild. Ditto for the character entities — so far I've copied Batchelder's set and a couple of others and hard-coded them. So, I'm presuming some kind of XML file to hold the definitions, and some way to tell the plugin to go and search for it over here, rather than over there. But, nevertheless, changing the code every now and then is no real problem at all.

With this plugin, it's certainly a lot easier to write technical posts like this than it ever was before.

Album cover for Motion Picture Now playing:
Yello - Croissant Bleu
(from Motion Picture)

Loading similar posts...   Loading links to posts on similar topics...

3 Responses

 avatar
#1 Joe Cheng [MSFT] said...
02-Jan-09 1:15 PM

Hi Julian,

You can actually do blockquote using either the blockquote button, or hitting Tab/Shift-Tab.

Em-dashes are done automatically when you type "--", and the copyright symbol is done automatically when you type (c). If this doesn't happen for you, make sure you're running the latest WLW 2009 RC and go to Tools | Options | Editing to make sure these features are enabled.

There's also an undocumented/unsupported "feature" in WLW that lets you extend these autoreplace macros by editing a registry key. [Scott Lovegrove has put a UI on this.](http://scottisafooldev.spaces.live.com/blog/cns!FE151030F50B5B37!2048.entry

)

I think you should be able to get the same functionality by doing that, without needing another gesture to invoke a plugin.

Hope that helps!

julian m bucknall avatar
#2 julian m bucknall said...
02-Jan-09 3:17 PM

Joe: Rats! I honestly didn't know about the blockquote button, and looking at it now, it's obvious that's what the icon means. Huh.

As for WLW 2009 RC: not running it I'm afraid. Not yet at any rate.

Thanks for the pointer to the undocumented stuff, I'll take a look once I have the new RC installed.

Cheers, Julian

#3 Delicious bookmarks plug-in for Live Writer said...
29-Jan-09 11:01 AM

Delicious bookmarks plug-in for Live Writer

Leave a response

Note: some MarkDown is allowed, but HTML is not. Expand to show what's available.

  •  Emphasize with italics: surround word with underscores _emphasis_
  •  Emphasize strongly: surround word with double-asterisks **strong**
  •  Link: surround text with square brackets, url with parentheses [text](url)
  •  Inline code: surround text with backticks `IEnumerable`
  •  Unordered list: start each line with an asterisk, space * an item
  •  Ordered list: start each line with a digit, period, space 1. an item
  •  Insert code block: start each line with four spaces
  •  Insert blockquote: start each line with right-angle-bracket, space > Now is the time...
Preview of response