ClipboardHelper.cs 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Linq;
  4. using System.Text;
  5. using System.Windows;
  6. namespace com.insanitydesign.MarkdownViewerPlusPlus.Helper
  7. {
  8. /// <summary>
  9. /// Helper to encode and set HTML fragment to clipboard.<br/>
  10. /// See http://theartofdev.com/2014/06/12/setting-htmltext-to-clipboard-revisited/.<br/>
  11. /// <seealso cref="CreateDataObject"/>.
  12. /// </summary>
  13. /// <remarks>
  14. /// The MIT License (MIT) Copyright (c) 2014 Arthur Teplitzki.
  15. /// </remarks>
  16. public static class ClipboardHelper
  17. {
  18. #region Fields and Consts
  19. /// <summary>
  20. /// The string contains index references to other spots in the string, so we need placeholders so we can compute the offsets. <br/>
  21. /// The <![CDATA[<<<<<<<]]>_ strings are just placeholders. We'll back-patch them actual values afterwards. <br/>
  22. /// The string layout (<![CDATA[<<<]]>) also ensures that it can't appear in the body of the html because the <![CDATA[<]]> <br/>
  23. /// character must be escaped. <br/>
  24. /// </summary>
  25. private const string Header = @"Version:0.9
  26. StartHTML:<<<<<<<<1
  27. EndHTML:<<<<<<<<2
  28. StartFragment:<<<<<<<<3
  29. EndFragment:<<<<<<<<4
  30. StartSelection:<<<<<<<<3
  31. EndSelection:<<<<<<<<4";
  32. /// <summary>
  33. /// html comment to point the beginning of html fragment
  34. /// </summary>
  35. public const string StartFragment = "<!--StartFragment-->";
  36. /// <summary>
  37. /// html comment to point the end of html fragment
  38. /// </summary>
  39. public const string EndFragment = @"<!--EndFragment-->";
  40. /// <summary>
  41. /// Used to calculate characters byte count in UTF-8
  42. /// </summary>
  43. private static readonly char[] _byteCount = new char[1];
  44. #endregion
  45. /// <summary>
  46. /// Create <see cref="DataObject"/> with given html and plain-text ready to be used for clipboard or drag and drop.<br/>
  47. /// Handle missing <![CDATA[<html>]]> tags, specified start\end segments and Unicode characters.
  48. /// </summary>
  49. /// <remarks>
  50. /// <para>
  51. /// Windows Clipboard works with UTF-8 Unicode encoding while .NET strings use with UTF-16 so for clipboard to correctly
  52. /// decode Unicode string added to it from .NET we needs to be re-encoded it using UTF-8 encoding.
  53. /// </para>
  54. /// <para>
  55. /// Builds the CF_HTML header correctly for all possible HTMLs<br/>
  56. /// If given html contains start/end fragments then it will use them in the header:
  57. /// <code><![CDATA[<html><body><!--StartFragment-->hello <b>world</b><!--EndFragment--></body></html>]]></code>
  58. /// If given html contains html/body tags then it will inject start/end fragments to exclude html/body tags:
  59. /// <code><![CDATA[<html><body>hello <b>world</b></body></html>]]></code>
  60. /// If given html doesn't contain html/body tags then it will inject the tags and start/end fragments properly:
  61. /// <code><![CDATA[hello <b>world</b>]]></code>
  62. /// In all cases creating a proper CF_HTML header:<br/>
  63. /// <code>
  64. /// <![CDATA[
  65. /// Version:1.0
  66. /// StartHTML:000000177
  67. /// EndHTML:000000329
  68. /// StartFragment:000000277
  69. /// EndFragment:000000295
  70. /// StartSelection:000000277
  71. /// EndSelection:000000277
  72. /// <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
  73. /// <html><body><!--StartFragment-->hello <b>world</b><!--EndFragment--></body></html>
  74. /// ]]>
  75. /// </code>
  76. /// See format specification here: http://msdn.microsoft.com/library/default.asp?url=/workshop/networking/clipboard/htmlclipboard.asp
  77. /// </para>
  78. /// </remarks>
  79. /// <param name="html">a html fragment</param>
  80. /// <param name="plainText">the plain text</param>
  81. public static DataObject CreateDataObject(string html, string plainText)
  82. {
  83. html = html ?? String.Empty;
  84. var htmlFragment = GetHtmlDataString(html);
  85. // re-encode the string so it will work correctly (fixed in CLR 4.0)
  86. if (Environment.Version.Major < 4 && html.Length != Encoding.UTF8.GetByteCount(html))
  87. htmlFragment = Encoding.Default.GetString(Encoding.UTF8.GetBytes(htmlFragment));
  88. var dataObject = new DataObject();
  89. dataObject.SetData(DataFormats.Html, htmlFragment);
  90. dataObject.SetData(DataFormats.Text, plainText);
  91. dataObject.SetData(DataFormats.UnicodeText, plainText);
  92. return dataObject;
  93. }
  94. /// <summary>
  95. /// Clears clipboard and sets the given HTML and plain text fragment to the clipboard, providing additional meta-information for HTML.<br/>
  96. /// See <see cref="CreateDataObject"/> for HTML fragment details.<br/>
  97. /// </summary>
  98. /// <example>
  99. /// ClipboardHelper.CopyToClipboard("Hello <b>World</b>", "Hello World");
  100. /// </example>
  101. /// <param name="html">a html fragment</param>
  102. /// <param name="plainText">the plain text</param>
  103. public static void CopyToClipboard(string html, string plainText)
  104. {
  105. var dataObject = CreateDataObject(html, plainText);
  106. Clipboard.SetDataObject(dataObject);
  107. }
  108. /// <summary>
  109. /// Generate HTML fragment data string with header that is required for the clipboard.
  110. /// </summary>
  111. /// <param name="html">the html to generate for</param>
  112. /// <returns>the resulted string</returns>
  113. private static string GetHtmlDataString(string html)
  114. {
  115. var sb = new StringBuilder();
  116. sb.AppendLine(Header);
  117. sb.AppendLine(@"<!DOCTYPE HTML PUBLIC ""-//W3C//DTD HTML 4.0 Transitional//EN"">");
  118. // if given html already provided the fragments we won't add them
  119. int fragmentStart, fragmentEnd;
  120. int fragmentStartIdx = html.IndexOf(StartFragment, StringComparison.OrdinalIgnoreCase);
  121. int fragmentEndIdx = html.LastIndexOf(EndFragment, StringComparison.OrdinalIgnoreCase);
  122. // if html tag is missing add it surrounding the given html (critical)
  123. int htmlOpenIdx = html.IndexOf("<html", StringComparison.OrdinalIgnoreCase);
  124. int htmlOpenEndIdx = htmlOpenIdx > -1 ? html.IndexOf('>', htmlOpenIdx) + 1 : -1;
  125. int htmlCloseIdx = html.LastIndexOf("</html", StringComparison.OrdinalIgnoreCase);
  126. if (fragmentStartIdx < 0 && fragmentEndIdx < 0)
  127. {
  128. int bodyOpenIdx = html.IndexOf("<body", StringComparison.OrdinalIgnoreCase);
  129. int bodyOpenEndIdx = bodyOpenIdx > -1 ? html.IndexOf('>', bodyOpenIdx) + 1 : -1;
  130. if (htmlOpenEndIdx < 0 && bodyOpenEndIdx < 0)
  131. {
  132. // 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
  133. sb.Append("<html><body>");
  134. sb.Append(StartFragment);
  135. fragmentStart = GetByteCount(sb);
  136. sb.Append(html);
  137. fragmentEnd = GetByteCount(sb);
  138. sb.Append(EndFragment);
  139. sb.Append("</body></html>");
  140. }
  141. else
  142. {
  143. // insert start/end fragments in the proper place (related to html/body tags if exists) so the paste will work correctly
  144. int bodyCloseIdx = html.LastIndexOf("</body", StringComparison.OrdinalIgnoreCase);
  145. if (htmlOpenEndIdx < 0)
  146. sb.Append("<html>");
  147. else
  148. sb.Append(html, 0, htmlOpenEndIdx);
  149. if (bodyOpenEndIdx > -1)
  150. sb.Append(html, htmlOpenEndIdx > -1 ? htmlOpenEndIdx : 0, bodyOpenEndIdx - (htmlOpenEndIdx > -1 ? htmlOpenEndIdx : 0));
  151. sb.Append(StartFragment);
  152. fragmentStart = GetByteCount(sb);
  153. var innerHtmlStart = bodyOpenEndIdx > -1 ? bodyOpenEndIdx : (htmlOpenEndIdx > -1 ? htmlOpenEndIdx : 0);
  154. var innerHtmlEnd = bodyCloseIdx > -1 ? bodyCloseIdx : (htmlCloseIdx > -1 ? htmlCloseIdx : html.Length);
  155. sb.Append(html, innerHtmlStart, innerHtmlEnd - innerHtmlStart);
  156. fragmentEnd = GetByteCount(sb);
  157. sb.Append(EndFragment);
  158. if (innerHtmlEnd < html.Length)
  159. sb.Append(html, innerHtmlEnd, html.Length - innerHtmlEnd);
  160. if (htmlCloseIdx < 0)
  161. sb.Append("</html>");
  162. }
  163. }
  164. else
  165. {
  166. // handle html with existing start\end fragments just need to calculate the correct bytes offset (surround with html tag if missing)
  167. if (htmlOpenEndIdx < 0)
  168. sb.Append("<html>");
  169. int start = GetByteCount(sb);
  170. sb.Append(html);
  171. fragmentStart = start + GetByteCount(sb, start, start + fragmentStartIdx) + StartFragment.Length;
  172. fragmentEnd = start + GetByteCount(sb, start, start + fragmentEndIdx);
  173. if (htmlCloseIdx < 0)
  174. sb.Append("</html>");
  175. }
  176. // Back-patch offsets (scan only the header part for performance)
  177. sb.Replace("<<<<<<<<1", Header.Length.ToString("D9"), 0, Header.Length);
  178. sb.Replace("<<<<<<<<2", GetByteCount(sb).ToString("D9"), 0, Header.Length);
  179. sb.Replace("<<<<<<<<3", fragmentStart.ToString("D9"), 0, Header.Length);
  180. sb.Replace("<<<<<<<<4", fragmentEnd.ToString("D9"), 0, Header.Length);
  181. return sb.ToString();
  182. }
  183. /// <summary>
  184. /// Calculates the number of bytes produced by encoding the string in the string builder in UTF-8 and not .NET default string encoding.
  185. /// </summary>
  186. /// <param name="sb">the string builder to count its string</param>
  187. /// <param name="start">optional: the start index to calculate from (default - start of string)</param>
  188. /// <param name="end">optional: the end index to calculate to (default - end of string)</param>
  189. /// <returns>the number of bytes required to encode the string in UTF-8</returns>
  190. private static int GetByteCount(StringBuilder sb, int start = 0, int end = -1)
  191. {
  192. int count = 0;
  193. end = end > -1 ? end : sb.Length;
  194. for (int i = start; i < end; i++)
  195. {
  196. _byteCount[0] = sb[i];
  197. count += Encoding.UTF8.GetByteCount(_byteCount);
  198. }
  199. return count;
  200. }
  201. }
  202. }