Skip to content

Commit 9329f1b

Browse files
author
Savas Ziplies
committed
Added: ClipboardHelper for formatted Clipboard Copy/Paste
1 parent 01df608 commit 9329f1b

File tree

3 files changed

+231
-10
lines changed

3 files changed

+231
-10
lines changed

MarkdownViewerPlusPlus/Forms/AbstractRenderer.cs

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
using PdfSharp.Pdf;
1919
using System.IO;
2020
using System.Net;
21+
using com.insanitydesign.MarkdownViewerPlusPlus.Helper;
2122

2223
/// <summary>
2324
///
@@ -320,16 +321,7 @@ protected virtual void sendToPrinter_Click(object sender, EventArgs e)
320321
/// <param name="e"></param>
321322
protected void sendToClipboard_Click(object sender, EventArgs e)
322323
{
323-
var dataObject = new DataObject();
324-
string html = BuildHtml(ConvertedText, FileName);
325-
try
326-
{
327-
html = XDocument.Parse(html).ToString();
328-
}
329-
catch { }
330-
//
331-
dataObject.SetData(DataFormats.UnicodeText, html);
332-
Clipboard.SetDataObject(dataObject);
324+
ClipboardHelper.CopyToClipboard(BuildHtml(ConvertedText, FileName), ConvertedText);
333325
}
334326

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

MarkdownViewerPlusPlus/MarkdownViewerPlusPlus.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,7 @@
170170
<Compile Include="Forms\OptionsPanelPDF.Designer.cs">
171171
<DependentUpon>OptionsPanelPDF.cs</DependentUpon>
172172
</Compile>
173+
<Compile Include="Helper\ClipboardHelper.cs" />
173174
<Compile Include="MarkdownViewer.cs" />
174175
<Compile Include="MarkdownViewerConfiguration.cs" />
175176
<Compile Include="MarkdownViewerFormatter.cs" />

0 commit comments

Comments
 (0)