Blogging code samples should be easy [Free ConvertClipboardRtfToHtmlText tool and source code!]
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; } } }