I've been including a lot of source code examples in my blog lately and needed a good way to paste properly formatted code in blog posts. I write my posts in HTML and wanted a tool that was easy to use, simple to install, produced concise HTML, and worked well with Visual Studio 2008. I was aware of a handful of tools for this purpose but none of them were quite what I was looking for, so I wrote my own one evening. :)
Caveat: ConvertClipboardRtfToHtmlText is a simple utility written for my specific scenario. I'm releasing the tool and code here in case anyone wants to use it, enhance it, or whatever. I have NOT attempted to write a solid, general-purpose RTF-to-HTML converter. Instead, ConvertClipboardRtfToHtmlText assumes its input is in the exact format used by Visual Studio 2008 (and probably VS 2005).
Using ConvertClipboardRtfToHtmlText is simple:
- Copy code (C#, XAML, etc.) to clipboard from Visual Studio 2008
- Run ConvertClipboardRtfToHtmlText to convert the RTF representation of the code on the clipboard to HTML as text (there's no UI because the tool does the conversion and immediately exits)
- Paste the HTML code on the clipboard into your blog post, web page, etc.
I'm the first to acknowledge there's a lot of room for improvement in step #2. :) While the current implementation is good enough for my purposes, I encourage interested parties to consider turning this into a real application - maybe with a notify icon and a system-wide hotkey. If you do so, please let me know because I'd love to check it out!
The compiled tool and code are available in a ZIP file attached to this post. The complete source code is also available below. Naturally, I used ConvertClipboardRtfToHtmlText to post its own source code. :)
Enjoy!
Updated 2008-04-02: Minor changes to code and tool
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Text;
using System.Windows.Forms;
// Convert Visual Studio 2008 RTF clipboard format into HTML by replacing the
// clipboard contents with its HTML representation in text format suitable for
// pasting into a web page or blog.
// USE: Copy to clipboard in VS, run this app (no UI), paste converted text
// NOTE: This is NOT a general-purpose RTF-to-HTML converter! It works well
// enough on the simple input I've tried, but may break for other input.
// TODO: Convert into a real application with a notify icon and hotkey.
namespace ConvertClipboardRtfToHtmlText
{
static class ConvertClipboardRtfToHtmlText
{
private const string colorTbl = "\\colortbl;\r\n";
private const string colorFieldTag = "cf";
private const string tabExpansion = " ";
[STAThread]
static void Main()
{
if (Clipboard.ContainsText(TextDataFormat.Rtf))
{
// Create color table, populate with default color
List<Color> colors = new List<Color>();
Color defaultColor = Color.FromArgb(0, 0, 0);
colors.Add(defaultColor);
// Get RTF
string rtf = Clipboard.GetText(TextDataFormat.Rtf);
// Parse color table
int i = rtf.IndexOf(colorTbl);
if (-1 != i)
{
i += colorTbl.Length;
while ((i < rtf.Length) && ('}' != rtf[i]))
{
// Add color to color table
SkipExpectedText(rtf, ref i, "\\red");
byte red = (byte)ParseNumericField(rtf, ref i);
SkipExpectedText(rtf, ref i, "\\green");
byte green = (byte)ParseNumericField(rtf, ref i);
SkipExpectedText(rtf, ref i, "\\blue");
byte blue = (byte)ParseNumericField(rtf, ref i);
colors.Add(Color.FromArgb(red, green, blue));
SkipExpectedText(rtf, ref i, ";");
}
}
else
{
throw new NotSupportedException("Missing/unknown colorTbl.");
}
// Find start of text and parse
i = rtf.IndexOf("\\fs");
if (-1 != i)
{
// Skip font size tag
while ((i < rtf.Length) && (' ' != rtf[i]))
{
i++;
}
i++;
// Begin building HTML text
StringBuilder sb = new StringBuilder();
sb.AppendFormat("<pre><span style='color:#{0:x2}{1:x2}{2:x2}'>",
defaultColor.R, defaultColor.G, defaultColor.B);
while (i < rtf.Length)
{
if ('\\' == rtf[i])
{
// Parse escape code
i++;
if ((i < rtf.Length) &&
(('{' == rtf[i]) || ('}' == rtf[i]) || ('\\' == rtf[i])))
{
// Escaped '{' or '}' or '\'
sb.Append(rtf[i]);
}
else
{
// Parse tag
int tagEnd = rtf.IndexOf(' ', i);
if (-1 != tagEnd)
{
if (rtf.Substring(i, tagEnd - i).StartsWith(colorFieldTag))
{
// Parse color field tag
i += colorFieldTag.Length;
int colorIndex = ParseNumericField(rtf, ref i);
if ((colorIndex < 0) || (colors.Count <= colorIndex))
{
throw new NotSupportedException("Bad color index.");
}
// Change to new color
sb.AppendFormat(
"</span><span style='color:#{0:x2}{1:x2}{2:x2}'>",
colors[colorIndex].R, colors[colorIndex].G,
colors[colorIndex].B);
}
else if("tab" == rtf.Substring(i, tagEnd-i))
{
sb.Append(tabExpansion);
}
// Skip tag
i = tagEnd;
}
else
{
throw new NotSupportedException("Malformed tag.");
}
}
}
else if ('}' == rtf[i])
{
// Terminal curly; done
break;
}
else
{
// Normal character; HTML-escape '<', '>', and '&'
switch (rtf[i])
{
case '<':
sb.Append("<");
break;
case '>':
sb.Append(">");
break;
case '&':
sb.Append("&");
break;
default:
sb.Append(rtf[i]);
break;
}
}
i++;
}
// Finish building HTML text
sb.Append("</span></pre>");
// Update the clipboard text
Clipboard.SetText(sb.ToString());
}
else
{
throw new NotSupportedException("Missing text section.");
}
}
}
// Skip the specified text
private static void SkipExpectedText(string s, ref int i, string text)
{
foreach (char c in text)
{
if ((s.Length <= i) || (c != s[i]))
{
throw new NotSupportedException("Expected text missing.");
}
i++;
}
}
// Parse a numeric field
private static int ParseNumericField(string s, ref int i)
{
int value = 0;
while ((i < s.Length) && char.IsDigit(s[i]))
{
value *= 10;
value += s[i] - '0';
i++;
}
return value;
}
}
}