CopySourceAsHtml 是個很棒的工具,可用來輔助製作程式文件。它是一個 Visual Studio 的 add-in,安裝好之後,只要在 VS2005 的編輯畫面中點滑鼠右鍵,再選「Copy As HTML...」,就能將選取的程式碼轉成 HTML 並複製到剪貼簿。這對於製作程式文件、在網站上分享程式碼來說,真是很方便的工具。

可惜的是,我使用之後發現中文字無法正確處理;精確的說,應該是無法正確處理 Unicode 字元。因此我自己動手將這個部份的 bugs 修正了(開放原碼萬歲!)。為了示範成果,我將自己的類別庫中的字串工具類別貼上來,附在文後。需要這個修正版的朋友,可以直接下載附件回去,然後照以下步驟進行:

  1. 下載 CopySourceAsHtml 2.0.0 Installer。直接執行裡面的安裝程式即可(安裝前請先關閉 VS2005) 。
  2. 將 本文附件的壓縮檔解開,把裡面的 bin\ 目錄下的 CopySourceAsHtml.dll 和 CopySourceAsHtml.pdb 覆蓋掉原本安裝的檔案。原本這兩個檔案是安裝在本機的 Documents and Settings\<USER>\My Documents\Visual Studio 2005\Addins\ 目錄下。
  3. 完成!開啟 VS2005 測試看看:開啟一個程式檔,選取文字,點右鍵,「Copy As HTML...」,再貼到某個網頁中看看轉出來的結果。

要特別說明的是,在修正 bug 時,主要是處理 RTF 字串的剖析,由於這部分我還是第一次處理,因此是一邊看 RTF spec,一邊寫單元測試來逐一測試各種中文字的編碼情況。不保證 100% 沒問題喔!若您有發現問題,可回應此文,或者您也可以自己動手修改,完整原始碼我都附上了(只要搜尋關鍵字 "Tsai" 就能找到我修改的部份)。

2006-10-24 更新:

使用 Visual Studio 2005 中文版的朋友還必須修正 Connect.cs,可參考 Hikari 提供的修正:

  194             commandBars = (CommandBars)this.application.CommandBars;

  195             editPopup = (CommandBarPopup)commandBars["MenuBar"].Controls["編輯(&E)"];

  196 

  197             copyIndex = FindCopyOnCommandBar("編輯(&E)") + 1;

感謝 Hikari!
註: 這部份的修改若能改成支援多國語言的方式,以自動支援兩種版本的 VS2005,就更好了。若有人有興趣修改,可參考 Connect.cs 中的 "Resource.*" 的程式碼,以及 Resources.resx 的內容。改完的話希望也能寄一份給我,功德無量 :)

============================================================

以下是我的字串工具類別,有些地方顯示成表情圖案,是因為程式碼剛好與此部落格的表情圖案碼相同

    1 using System;
    2 using System.Text;
    3 using System.Text.RegularExpressions;
    4 using System.Web;
    5 using System.Security.Cryptography;
    6 
    7 namespace Huanlin.Helpers
    8 {
    9     /// <summary>
   10     /// 字串處理工具類別。
   11     /// </summary>
   12     public sealed class StrHelper
   13     {
   14         private StrHelper()
   15         {
   16         }
   17 
   18         /// <summary>
   19         /// Append a slash character '\' to string.
   20         /// </summary>
   21         /// <param name="input">輸入字串。</param>
   22         /// <returns>輸出字串。</returns>
   23         public static string AppendSlash(string input)
   24         {
   25             if (input == null)
   26                     return @"\";
   27             if (input.EndsWith("/") || input.EndsWith("\\"))
   28                 return input;
   29             return input + "\\";
   30         }
   31 
   32         /// <summary>
   33         /// Remove the last slash character.
   34         /// </summary>
   35         /// <param name="input">輸入字串。</param>
   36         /// <returns>輸出字串。</returns>
   37         public static string RemoveLastSlash(string input)
   38         {
   39             if (input == null)
   40                 return "";
   41             if (input.EndsWith("/") || input.EndsWith("\\"))
   42                 return input.Remove(input.Length-1, 1);
   43             return input;
   44         }
   45 
   46         /// <summary>
   47         /// 移除任何一個指定的字元。
   48         /// </summary>
   49         /// <param name="input">輸入字串。</param>
   50         /// <param name="anyOf">要移除的字元。</param>
   51         /// <returns>輸出字串。</returns>
   52         public static string RemoveAny(string input, char[] anyOf)
   53         {
   54             if (String.IsNullOrEmpty(input))
   55                 return input;
   56 
   57             int i;
   58             while (true)
   59             {
   60                 i = input.IndexOfAny(anyOf);
   61                 if (i < 0)
   62                     break;
   63                 input = input.Remove(i, 1);
   64             }
   65             return input;
   66         }
   67 
   68         /// <summary>
   69         /// 移除換行字元(\n) 和(\r)。
   70         /// </summary>
   71         /// <param name="input">輸入字串。</param>
   72         /// <returns>輸出字串。</returns>
   73         public static string RemoveNewLines(string input)
   74         {
   75             return StrHelper.RemoveNewLines(input, false);
   76         }
   77 
   78         /// <summary>
   79         /// 移除換行字元(\n) 和(\r)。
   80         /// </summary>
   81         /// <param name="input">輸入字串。</param>
   82         /// <param name="addSpace">若為true,則把換行符號替換成空白字元。</param>
   83         /// <returns>輸出字串。</returns>
   84         public static string RemoveNewLines(string input,
   85            bool addSpace)
   86         {
   87             string replace = string.Empty;
   88             if (addSpace)
   89                 replace = " ";
   90 
   91             string pattern = @"[\r|\n]";
   92             Regex regEx = new Regex(pattern, RegexOptions.Multiline);
   93 
   94             return regEx.Replace(input, replace);
   95         }
   96 
   97         /// <summary>
   98         /// 移除空白字元(包括全形空白)。
   99         /// </summary>
  100         /// <param name="input">輸入字串。</param>
  101         /// <param name="fullShapeSpaces">是否連全形空白也一併刪除。</param>
  102         /// <returns>輸出字串。</returns>
  103         public static string RemoveSpaces(string input, bool fullShapeSpaces)
  104         {
  105             string replace = string.Empty;
  106             string pattern = @"[ ]";
  107             if (fullShapeSpaces)
  108             {
  109                 pattern = @"[ | ]";
  110             }                
  111 
  112             Regex regEx = new Regex(pattern, RegexOptions.Multiline);
  113 
  114             return regEx.Replace(input, replace);
  115         }
  116 
  117 
  118         /// <summary>
  119         /// 檢驗身分證號碼的正確性。
  120         ///    驗證規則:
  121         ///
  122         ///        身份證統一編號共計有10 位,其中第一位為英文字母,後共有九個數字;
  123         ///        而最後一位數字為檢查碼( Check Digit ) ,表示如下表:
  124         ///
  125         ///        L1 D2 D2 D3 D4 D5 D6 D7 D8 D9 
  126         ///
  127         ///        L1: 英文字母, 代表出生地的縣/市代號
  128         ///        D2: 性別,1=男, 2=女
  129         ///        D9: 檢查碼
  130         ///
  131         ///        L1 對照表:
  132         ///
  133         ///          字母A  B  C  D  E  F  G  H  J  K  L  M  N 
  134         ///          代號10 11 12 13 14 15 16 17 18 19 20 21 22 
  135         ///          -------------------------------------------
  136         ///          字母P  Q  R  S  T  U  V  X  Y  W  Z  I  O
  137         ///          代號23 24 25 26 27 28 29 30 31 32 33 34 35 
  138         ///
  139         ///        令其十位數為X1 ,個位數為X2 ;( 如A:X1=1 , X2=0 )
  140         ///
  141         ///        依其公式計算結果:
  142         ///        Y= X1 + 9*X2 + 8*D1 + 7*D2 + 6*D3 + 5*D4 + 4*D5 + 3*D6 + 2*D7 + 
  143         ///        D8 + D9
  144         ///
  145         ///      假如Y能被10 整除,則表示該身份證號碼為正確,否則為錯誤。即如以10 為
  146         ///        模數,檢查號碼為( 10 - Y- D9 ) / 10 的餘數,如餘數為0 時,則檢查碼
  147         ///        為0 。
  148         /// 
  149         /// </summary>
  150         /// <param name="input"></param>
  151         /// <returns></returns>
  152         public static bool CheckIdno(string idno) 
  153         {
  154             int[] letter_weight = 
  155             {
  156                 // A   B   C   D   E   F   G   H   I   J   K   L   M   N   O   P   Q   R
  157                 10, 11, 12, 13, 14, 15, 16, 17, 34, 18, 19, 20, 21, 22, 35, 23, 24, 25,
  158                 26, 27, 28, 29, 32, 30, 31, 33
  159                 // S   T   U   V   W   X   Y   Z
  160             };
  161 
  162             int  i;
  163             int[] D = new int[9];     // 1..9
  164             int sum;
  165 
  166             if (idno.Length != 10)
  167                 return false;
  168             idno = idno.ToUpper();
  169             if (!Char.IsLetter(idno[0]) || (idno[1] != '1' && idno[1] != '2')) 
  170                 return false;
  171             for (i = 1; i < 10; i++)
  172             {
  173                 if (!Char.IsDigit(idno, i))
  174                     return false;
  175             }
  176 
  177             for (i = 0; i < 9; i++) // 1..9
  178             {
  179                 DIdea = Int32.Parse(idno.Substring(i+1, 1));
  180             }
  181 
  182             i = letter_weight[idno[0]-'A'];
  183             sum = (i / 10) + (i % 10) * 9;
  184             sum = sum + 8 * D[0] + 7 * D[1] + 6 * D[2] + 5 * D[3]
  185                 + 4 * D[4] + 3 * D[5] + 2 * D[Devil + D[7] + D[Music;
  186             return (sum % 10 == 0);
  187         }
  188 
  189         /// <summary>
  190         /// 判斷字串值是否為數字。
  191         /// </summary>
  192         /// <param name="input"></param>
  193         /// <returns></returns>
  194         public static bool IsDigit(string s)
  195         {
  196             try
  197             {
  198                 long num = Convert.ToInt64(s);
  199                 return true;
  200             }
  201             catch
  202             {
  203                 return false;
  204             }
  205         }
  206 
  207         /// <summary>
  208         /// 將字串轉成位元組陣列。
  209         /// </summary>
  210         /// <param name="str"></param>
  211         /// <returns></returns>
  212         public static byte[] StrToByteArray(string str)
  213         {
  214             System.Text.UTF8Encoding encoding = new System.Text.UTF8Encoding();
  215             return encoding.GetBytes(str);
  216         }
  217 
  218         /// <summary>
  219         /// 將字串轉成位元組陣列。
  220         /// </summary>
  221         /// <param name="str">輸入字串。</param>
  222         /// <param name="enc">編碼。</param>
  223         /// <returns></returns>
  224         public static byte[] StrToByteArray(string str, Encoding enc)
  225         {
  226             return enc.GetBytes(str);
  227         }
  228 
  229         /// <summary>
  230         /// Byte 陣列轉成字串。
  231         /// </summary>
  232         /// <param name="bytes"></param>
  233         /// <returns></returns>
  234         public static string ByteArrayToStr(byte[] bytes)
  235         {
  236             System.Text.UTF8Encoding enc = new System.Text.UTF8Encoding();
  237             return enc.GetString(bytes);
  238         }
  239 
  240         /// <summary>
  241         /// 大小寫無關的置換函式。
  242         /// </summary>
  243         /// <param name="input">輸入字串。</param>
  244         /// <param name="newValue">要被置換的字串。</param>
  245         /// <param name="oldValue">置換的新字串。</param>
  246         /// <returns>A string</returns>
  247         public static string CaseInsenstiveReplace(string input,
  248            string oldValue, string newValue)
  249         {
  250             Regex regEx = new Regex(oldValue,
  251                RegexOptions.IgnoreCase | RegexOptions.Multiline);
  252             return regEx.Replace(input, newValue);
  253         }
  254 
  255         /// <summary>
  256         /// 檢查傳入的字串是否有出現任何一個指定的字串。字串比對時有分大小寫。
  257         /// </summary>
  258         /// <param name="input">欲檢查的字串</param>
  259         /// <param name="hasWords">要搜尋的字詞。</param>
  260         /// <returns>有符合的字詞</returns>
  261         public static MatchCollection HasWords(string input,
  262            params string[] hasWords)
  263         {
  264             StringBuilder sb = new StringBuilder(hasWords.Length + 50);
  265             //sb.Append("[");
  266 
  267             foreach (string s in hasWords)
  268             {
  269                 sb.AppendFormat("({0})|",
  270                    HttpUtility.HtmlEncode(s.Trim()));
  271             }
  272 
  273             string pattern = sb.ToString();
  274             pattern = pattern.TrimEnd('|'); // +"]";
  275 
  276             Regex regEx = new Regex(pattern, RegexOptions.Multiline);
  277             return regEx.Matches(input);
  278         }
  279 
  280         /// <summary>
  281         /// 將字串以MD5 編碼。
  282         /// </summary>
  283         /// <param name="input">欲編碼的字串。</param>
  284         /// <returns>編碼過的字串</returns>
  285         public static string MD5Encode(string input)
  286         {
  287             // Create a new instance of the 
  288             // MD5CryptoServiceProvider object.
  289             MD5 md5Hasher = MD5.Create();
  290 
  291             // Convert the input string to a byte 
  292             // array and compute the hash.
  293             byte[] data = md5Hasher.ComputeHash(
  294                Encoding.Default.GetBytes(input));
  295 
  296             // Create a new Stringbuilder to collect the bytes
  297             // and create a string.
  298             StringBuilder sBuilder = new StringBuilder();
  299 
  300             // Loop through each byte of the hashed data 
  301             // and format each one as a hexadecimal string.
  302             for (int i = 0; i < data.Length; i++)
  303             {
  304                 sBuilder.Append(dataIdea.ToString("x2"));
  305             }
  306 
  307             // Return the hexadecimal string.
  308             return sBuilder.ToString();
  309         }
  310 
  311         /// <summary>
  312         /// 刪除空白以及多餘的結束字元('\0')。
  313         /// 某些WinAPI 傳回的字元陣列轉成字串之後,後面會附加結束字元和垃圾資料,便可使用此函式。
  314         /// </summary>
  315         /// <param name="input">輸入字串</param>
  316         /// <returns>輸出字串。</returns>
  317         public static string Trim(string s)
  318         {
  319             s = s.Trim();
  320             int i = s.IndexOf('\0');
  321             if (i >= 0)
  322             {
  323                 return s.Substring(0, i);
  324             }
  325             return s;
  326         }
  327 
  328         /// <summary>
  329         /// 驗證一個字串是否經過MD5 編碼後會與傳入的MD5 字串相符。可用來驗證使用者密碼。
  330         /// </summary>
  331         /// <param name="input">欲比較的字串。</param>
  332         /// <param name="hash">MD5 編碼過的字串。</param>
  333         /// <returns>若相符則傳回True,否則傳回False。</returns>
  334         public static bool MD5Verify(string input, string md5str)
  335         {
  336             // Hash the input.
  337             string hashOfInput = StrHelper.MD5Encode(input);
  338 
  339             // Create a StringComparer an comare the hashes.
  340             StringComparer comparer = StringComparer.OrdinalIgnoreCase;
  341 
  342             if (0 == comparer.Compare(hashOfInput, md5str))
  343             {
  344                 return true;
  345             }
  346             else
  347             {
  348                 return false;
  349             }
  350         }
  351 
  352         /// <summary>
  353         /// 將字串中的所有全形空白轉成半形空白。
  354         /// </summary>
  355         /// <param name="input">輸入字串。</param>
  356         /// <returns>輸出字串。</returns>
  357         public static string FullShapeSpaceToSpace(string input)
  358         {
  359             string replace = " ";
  360             string pattern = @"[ ]";
  361             Regex regEx = new Regex(pattern, RegexOptions.Multiline);
  362             return regEx.Replace(input, replace);
  363         }
  364 
  365         /// <summary>
  366         /// 把所有空白字元轉換成HTML 空白。
  367         /// </summary>
  368         /// <param name="input">輸入字串。</param>
  369         /// <returns>輸出字串。</returns>
  370         public static string SpaceToNbsp(string input)
  371         {
  372             string space = " ";
  373             return input.Replace(" ", space);
  374         }
  375 
  376         /// <summary>
  377         /// 把所有換行符號(\n) 和(\r) 轉成HTML 斷行標籤。
  378         /// </summary>
  379         /// <param name="input">輸入字串。</param>
  380         /// <returns>輸出字串。</returns>
  381         public static string NewLineToBreak(string input)
  382         {
  383             Regex regEx = new Regex(@"[\n|\r]+");
  384             return regEx.Replace(input, "<br />");
  385         }
  386 
  387         /// <summary>
  388         /// 判斷傳入的字串是否為空字串。
  389         /// 注意:空白、全形空白、Tab 字元、和換行符號均視為「空」字元。
  390         /// </summary>
  391         /// <param name="input"></param>
  392         /// <returns></returns>
  393         public static bool IsEmpty(string input)
  394         {
  395             string s = StrHelper.RemoveAny(input, new char[] { ' ', ' ', '\t', '\r', '\n', '\0' });
  396             if (String.IsNullOrEmpty(s))
  397                 return true;
  398             return false;
  399         }
  400 
  401         /// <summary>
  402         /// 把內含16 進位值的字串轉成byte。
  403         /// </summary>
  404         /// <param name="input">內含16 進位值的字串,例如:1F、AE。</param>
  405         /// <returns>byte 值。</returns>
  406         public static byte HexStrToByte(string s)
  407         {
  408             return byte.Parse(s, System.Globalization.NumberStyles.HexNumber);
  409         }
  410 
  411         /// <summary>
  412         /// 字串反轉。
  413         /// </summary>
  414         /// <param name="s">輸入字串。</param>
  415         /// <returns>反轉後的字串。</returns>
  416         public static string Reverse(string s)
  417         {
  418             if (String.IsNullOrEmpty(s))
  419                 return "";
  420             char[] charArray = new char[s.Length];
  421             int len = s.Length - 1;
  422             for (int i = 0; i <= len; i++) {
  423                 charArrayIdea = s[len-i];
  424             }
  425             return new string(charArray);
  426         }
  427     }
  428 }