// "Therefore those skilled at the unorthodox // are infinite as heaven and earth, // inexhaustible as the great rivers. // When they come to an end, // they begin again, // like the days and months; // they die and are reborn, // like the four seasons." // // - Sun Tsu, // "The Art of War" using System; using System.Text; using System.Windows.Forms; namespace TheArtOfDev.HtmlRenderer.WinForms.Utilities { /// /// Helper to encode and set HTML fragment to clipboard.
/// See http://theartofdev.wordpress.com/2012/11/11/setting-html-and-plain-text-formatting-to-clipboard/.
/// . ///
/// /// The MIT License (MIT) Copyright (c) 2014 Arthur Teplitzki. /// internal static class ClipboardHelper { #region Fields and Consts /// /// The string contains index references to other spots in the string, so we need placeholders so we can compute the offsets.
/// The _ strings are just placeholders. We'll back-patch them actual values afterwards.
/// The string layout () also ensures that it can't appear in the body of the html because the
/// character must be escaped.
///
private const string Header = @"Version:0.9 StartHTML:<<<<<<<<1 EndHTML:<<<<<<<<2 StartFragment:<<<<<<<<3 EndFragment:<<<<<<<<4 StartSelection:<<<<<<<<3 EndSelection:<<<<<<<<4"; /// /// html comment to point the beginning of html fragment /// public const string StartFragment = ""; /// /// html comment to point the end of html fragment /// public const string EndFragment = @""; /// /// Used to calculate characters byte count in UTF-8 /// private static readonly char[] _byteCount = new char[1]; #endregion /// /// Create with given html and plain-text ready to be used for clipboard or drag and drop.
/// Handle missing ]]> tags, specified start\end segments and Unicode characters. ///
/// /// /// Windows Clipboard works with UTF-8 Unicode encoding while .NET strings use with UTF-16 so for clipboard to correctly /// decode Unicode string added to it from .NET we needs to be re-encoded it using UTF-8 encoding. /// /// /// Builds the CF_HTML header correctly for all possible HTMLs
/// If given html contains start/end fragments then it will use them in the header: /// hello world]]> /// If given html contains html/body tags then it will inject start/end fragments to exclude html/body tags: /// hello world]]> /// If given html doesn't contain html/body tags then it will inject the tags and start/end fragments properly: /// world]]> /// In all cases creating a proper CF_HTML header:
/// /// /// hello world /// ]]> /// /// See format specification here: http://msdn.microsoft.com/library/default.asp?url=/workshop/networking/clipboard/htmlclipboard.asp ///
///
/// a html fragment /// the plain text public static DataObject CreateDataObject(string html, string plainText) { html = html ?? String.Empty; var htmlFragment = GetHtmlDataString(html); // re-encode the string so it will work correctly (fixed in CLR 4.0) if (Environment.Version.Major < 4 && html.Length != Encoding.UTF8.GetByteCount(html)) htmlFragment = Encoding.Default.GetString(Encoding.UTF8.GetBytes(htmlFragment)); var dataObject = new DataObject(); dataObject.SetData(DataFormats.Html, htmlFragment); dataObject.SetData(DataFormats.Text, plainText); dataObject.SetData(DataFormats.UnicodeText, plainText); return dataObject; } /// /// Clears clipboard and sets the given HTML and plain text fragment to the clipboard, providing additional meta-information for HTML.
/// See for HTML fragment details.
///
/// /// ClipboardHelper.CopyToClipboard("Hello World", "Hello World"); /// /// a html fragment /// the plain text public static void CopyToClipboard(string html, string plainText) { var dataObject = CreateDataObject(html, plainText); Clipboard.SetDataObject(dataObject, true); } /// /// Clears clipboard and sets the given plain text fragment to the clipboard.
///
/// the plain text public static void CopyToClipboard(string plainText) { var dataObject = new DataObject(); dataObject.SetData(DataFormats.Text, plainText); dataObject.SetData(DataFormats.UnicodeText, plainText); Clipboard.SetDataObject(dataObject, true); } #region Private/Protected methods /// /// Generate HTML fragment data string with header that is required for the clipboard. /// /// the html to generate for /// the resulted string private static string GetHtmlDataString(string html) { var sb = new StringBuilder(); sb.AppendLine(Header); sb.AppendLine(@""); // if given html already provided the fragments we won't add them int fragmentStart, fragmentEnd; int fragmentStartIdx = html.IndexOf(StartFragment, StringComparison.OrdinalIgnoreCase); int fragmentEndIdx = html.LastIndexOf(EndFragment, StringComparison.OrdinalIgnoreCase); // if html tag is missing add it surrounding the given html (critical) int htmlOpenIdx = html.IndexOf(" -1 ? html.IndexOf('>', htmlOpenIdx) + 1 : -1; int htmlCloseIdx = html.LastIndexOf(" -1 ? html.IndexOf('>', bodyOpenIdx) + 1 : -1; if (htmlOpenEndIdx < 0 && bodyOpenEndIdx < 0) { // the given html doesn't contain html or body tags so we need to add them and place start/end fragments around the given html only sb.Append(""); sb.Append(StartFragment); fragmentStart = GetByteCount(sb); sb.Append(html); fragmentEnd = GetByteCount(sb); sb.Append(EndFragment); sb.Append(""); } else { // insert start/end fragments in the proper place (related to html/body tags if exists) so the paste will work correctly int bodyCloseIdx = html.LastIndexOf(""); else sb.Append(html, 0, htmlOpenEndIdx); if (bodyOpenEndIdx > -1) sb.Append(html, htmlOpenEndIdx > -1 ? htmlOpenEndIdx : 0, bodyOpenEndIdx - (htmlOpenEndIdx > -1 ? htmlOpenEndIdx : 0)); sb.Append(StartFragment); fragmentStart = GetByteCount(sb); var innerHtmlStart = bodyOpenEndIdx > -1 ? bodyOpenEndIdx : (htmlOpenEndIdx > -1 ? htmlOpenEndIdx : 0); var innerHtmlEnd = bodyCloseIdx > -1 ? bodyCloseIdx : (htmlCloseIdx > -1 ? htmlCloseIdx : html.Length); sb.Append(html, innerHtmlStart, innerHtmlEnd - innerHtmlStart); fragmentEnd = GetByteCount(sb); sb.Append(EndFragment); if (innerHtmlEnd < html.Length) sb.Append(html, innerHtmlEnd, html.Length - innerHtmlEnd); if (htmlCloseIdx < 0) sb.Append(""); } } else { // handle html with existing start\end fragments just need to calculate the correct bytes offset (surround with html tag if missing) if (htmlOpenEndIdx < 0) sb.Append(""); int start = GetByteCount(sb); sb.Append(html); fragmentStart = start + GetByteCount(sb, start, start + fragmentStartIdx) + StartFragment.Length; fragmentEnd = start + GetByteCount(sb, start, start + fragmentEndIdx); if (htmlCloseIdx < 0) sb.Append(""); } // Back-patch offsets (scan only the header part for performance) sb.Replace("<<<<<<<<1", Header.Length.ToString("D9"), 0, Header.Length); sb.Replace("<<<<<<<<2", GetByteCount(sb).ToString("D9"), 0, Header.Length); sb.Replace("<<<<<<<<3", fragmentStart.ToString("D9"), 0, Header.Length); sb.Replace("<<<<<<<<4", fragmentEnd.ToString("D9"), 0, Header.Length); return sb.ToString(); } /// /// Calculates the number of bytes produced by encoding the string in the string builder in UTF-8 and not .NET default string encoding. /// /// the string builder to count its string /// optional: the start index to calculate from (default - start of string) /// optional: the end index to calculate to (default - end of string) /// the number of bytes required to encode the string in UTF-8 private static int GetByteCount(StringBuilder sb, int start = 0, int end = -1) { int count = 0; end = end > -1 ? end : sb.Length; for (int i = start; i < end; i++) { _byteCount[0] = sb[i]; count += Encoding.UTF8.GetByteCount(_byteCount); } return count; } #endregion } }