題注:在我寫這篇文章的時候,CSDN還支持MetaBlogAPI,但自從blog改版後,CSDN已經不再支持MetaBlogAPI了,但是下面介紹的這些技巧對於Window Live Writer的插件開發以及博客園等支持MetaBlogAPI的站點依然適用。
背景
經常寫博客的朋友也許經常使用Windows Live Writer,尤其對於有些程序員來說,時常輾轉於CSDN、Windows Live、cnBlogs等衆多社區,使用WLW對博客進行管理和更新會方便很多。我最近在寫一些技術類博客文章的時候,時常需要插入代碼,目前也有許多第三方的代碼格式化插件,如“Insert Code”、“Code Snippet”等,這些插件能讓代碼插入後輸出的HTML中產生語法高亮、行號、隔行陰影等特效,但是與CSDN本身的“插入代碼”功能相比,這些插件輸出的內容中會包含大量的樣式,當代碼量較大時,會造成文章大小急劇地膨脹。
如果要解決內聯樣式造成的HTML膨脹問題,可以採用CSS+JavaScript的解決方案,如著名的的“Syntax Highlighter”項目,但是這需要博客系統和平臺的支持,Wordpress與CSDN都是採用Syntax Hightlighter。因此,我在研究了幾個WLW的插件和Syntax Hightlighter的接口後,決定自己開發一個利用CSDN本身的“插入代碼”功能的插件。
WLW 插件SDK
微軟已經在其MSDN上發佈了Windows Live Writer的SDK,其中關於插件編寫的部分可以參看:Windows Live Writer Plugin API和這篇博客http://www.cnblogs.com/yaoshiyou/archive/2009/11/28/1612746.html。
WLW的插件可以分爲三類:
- 發佈通知插件:這類插件主要用於在博客文章發佈前後調用,可以用來對內容的XHTML進行檢查或者在某些條件下取消發佈。
- 內容插件:這類插件用於讓用戶插入某種形式的內容,並對內容進行格式化處理,例如某些插件可以讓用戶選擇插入本地的一張圖片,然後生成縮略圖上傳並最終返回鏈接地址。
- 頁眉頁腳插件:這類插件主要用於自動在發佈文章首尾添加一些內容,如簽名、鏈接等等。
本次需求就是開發第二類插件,讓用戶輸入源程序代碼,生成符合CSDN樣式的HTML文本。
插件開發步驟
WLW的插件開發的大致步驟如下:
- 創建一個新的.Net類庫項目(framework 1.1以上)。
- 在解決方案資源管理器中右鍵單擊該項目,選擇“添加引用”,然後選擇“瀏覽”的選項,導入對WindowsLive.Writer.Api.dll的引用,此文件位於Window Live Writer執行文件所在文件夾下,如:C:/Program Files/Windows Live/Writer/。
- 創建一個類,繼承自WindowsLive.Writer.Api.ContentSource(如果創建其他類型插件,可能繼承自其他類,可以參看文檔)。
- 對這個類應用WindowsLive.Writer.Api.WriterPluginAttribute屬性,以指明插件的名稱、圖標及唯一ID等信息。
- ContentSource有3個虛方法:CreateContent()、CreateContentFromLiveClipboard()、CreateContentFromUrl(),根據源內容的輸入方式的不同,後續的步驟有一定的不同:
- 對話框輸入:如果希望點擊插件後,彈出一個對話框來讓用戶輸入內容;這時需要爲這個類應用WindowsLive.Writer.Api.InsertableContentSourceAttribute屬性,然後重載
- 剪切板輸入:這種模式是先將內容複製到剪切板中(準確的說應該是Live Clipboard,微軟爲Web開發一種數據交換技術,採用XML描述),然後點擊插件,插件則直接讀取剪切板中的內容作爲自己的源;這時需要爲這個類應用WindowsLive.Writer.Api.LiveClipboardContentSourceAttribute屬性,然後重載CreateContentFromLiveClipboard()方法。
- URL輸入:根據WindowsLiveWriterApplication.BlogThisLink中的URL或者粘貼或拖入到編輯器中的URL作爲數據源輸入;這時需要爲這個類應用WindowsLive.Writer.UrlContentSourceAttribute屬性,然後重載CreateContentFromUrl()方法。
- 當然,有時候插件可能不需要任何輸入源,只需當點擊插件按鈕後輸出信息即可(如個人簽名、當前日期等),則可以以上任選一種方法,在重載函數中忽略輸入的內容即可。
重載方法後,編譯成dll,然後將其放置到Window Live Writer安裝目錄下的Plugins目錄下,重啓WLW,即可以看見新的插件了,也可以在項目的生成後事件添加如下語句,自動複製到插件目錄下進行調試: XCOPY /D /Y /R "$(TargetPath)" "C:/Program Files/Windows Live/Writer/Plugins/",調試時只能先啓動WLW,然後手動附加到進程。
示例詳解
爲了詳解以上步驟中的幾個難點,下面我列出一個最簡單的插件示例。
1: Imports WindowsLive.Writer.Api
2: Imports System.Windows.Forms
3: Imports System.Text
4:
5: <WriterPlugin("4cbc0496-d225-4abb-afe3-58299032fde3", _
6: "CSDN Coding", _
7: Description:="Windows Live Writer Plugin for CSDN", _
8: ImagePath:="blog.bmp", _
9: PublisherUrl:="http://blog.csdn.net/icefireelf")> _
10: <InsertableContentSource("CSDN代碼")> _
11: Public Class PluginAdapter
12: Inherits ContentSource
13: Public Overrides Function CreateContent(ByVal dialogOwner As IWin32Window, ByRef content As String) As System.Windows.Forms.DialogResult
14: Dim ret = MessageBox.Show("是否插入一條問候?", "問候語插件", MessageBoxButtons.OKCancel)
15: If (ret = DialogResult.OK) Then
16: content = "hello"
17: End If
18: Return ret
19: End Function
20: End Class
WriterPluginAttribute
WriterPlugin有2個必填屬性:
- Id:即上面代碼中的構造的第一個參數,此參數用於唯一標識一個插件,必須用GUID,使用項目屬性中程序集的GUID即可。
- Name:即上面代碼中的構造的第二個參數,此參數即WLW的插件欄中顯示的名稱,建議不要太長。
此外還有幾個可選屬性:
- Description:字符串屬性,此參數即WLW的插件詳細信息中顯示的內容,簡單描述插件功能。
- ImagePath: 圖標路徑,此參數決定WLW參見欄名稱前的圖標,如果不填則沒有圖標。圖標須採用16*16的位圖或PNG圖,且必須作爲嵌入的資源(將圖標添加到工程中,在解決方案資源管理器中右鍵點擊圖標,選擇“屬性”,然後將屬性框中的“生成操作”設爲“嵌入的資源”即可)。注意:如果圖標放置在工程目錄下,則直接填圖標名稱即可;如果放置在工程目錄的子級目錄下時,使用“.”作爲目錄分隔符,例如:將圖標放置在工程目錄下的image/目錄中,圖標路徑應寫爲“image.blog.bmp”,而非“image/blog.bmp”。
- PublisherUrl:發表者的URL,如果設置了此屬性,在WLW的插件信息中點擊詳細信息的文字鏈接時,會打開此URL。
- HasEditableOptions: 詳細信息裏面是否有“選項”按鈕(如下圖所示),此參數默認爲False,即不帶“選項”按鈕,當其設置爲True時,則必須爲本類重載EditOptions()方法,在該方法中,可以啓動一個用於配置插件的窗體,獲取參數,然後保存在配置文件或本類對象的成員中,共後續訪問使用。
InsertableContentSourceAttribute
此屬性本用於定義插件在WLW的“Insert”菜單和“Insert”快捷面板的名稱,但新版的WLW已經去除了插入菜單與側邊欄,因此應用此屬性只是爲了保持向前兼容。
ContentSource對象生存期
由上面的代碼可以看出,每個內容插件都對應一個ContentSource派生類的對象。那麼這個對象是如何維護的呢?對於每個Writer的進程而言,所有ContentSource實例都是單例的,即當插件對象創建後,以後每次點擊插件,使用的都是同一個對象。所以,如果該對象擁有任何實例成員,則其在整個進程的上下文中都是有效的,可以用來存儲一些關於插件的全局參數(例如Options屬性)。
ContentSource類繼承自WriterPlugin類,因此也繼承了WriterPlugin.Options屬性與WriterPlugin.Initialize()方法。
- WriterPlugin.Options是一個鍵值對集合對象,一般用於保存與查詢插件的參數記錄(如最後一次的配置)。
- WriterPlugin.Initialize()方法在本對象初始化時調用,可以在子類中可以重載此方法,添加自己的初始化行爲,但一定不要忘記住在重載方法中調用基類的Initialize()實現。
CreateContent方法
ContentContent方法是本插件的入口,在樣例代碼中,我們實現了一個小功能:每次點擊插件,彈出一個對話框,詢問用戶是否插入問候語,如果用戶點擊確認,則輸出“hello”的字符串,否則不輸出。此接口的形式如下:
public virtual DialogResult CreateContent( IWin32Window dialogOwner, ref string content );
本方法有兩個參數:
- dialogOwner:對話框的擁有者。
- content:從ref可以看出這個參數是做爲輸出用,即最後輸出編輯區的內容。
如果輸出了內容,則返回DialogResult.OK,否則,返回DialogResult.Cancel。
注意:前面提到過,由於ContentSource對象是單例的,所以在實現本方法,不應該使用任何實例成員來保存臨時變量,應做到可重入。
Syntax Highlighter
Syntax Highlight支持<pre>與<textarea>兩種元素,下面爲兩個例子:
1: <pre name="code" class="c-sharp:nogutter:collapse">代碼內容</pre>
2: <texterea name="code" class="cpp:nocontrols:firstline[10]" clos="60" rows="10">代碼內容</texterea>
由上可以看出,<pre>與<textarea>的name屬性必須爲"code" ,而class屬性裏可以指定代碼的語言類型,是否顯示行號、是否顯示控制條、是否摺疊、其實行號等屬性,具體如下:
- 語言項 目前支持的語言項有cpp、c-sharp、css、java、javascript、vb、sql、ruby、delphi、python、php、xhtml
- nogutter 如果添加此屬性,將不顯示行號。
- nocontrols 如果添加此屬性,將不會在代碼塊頂部顯示控制器(包含摺疊、打印、複製等命令)。
- collapse 如果添加此屬性,將默認摺疊代碼。
- firstline[value] 如果添加此屬性,行號從value開始計數,默認值是 1。
- showcolumns 如果添加此屬性,將在第一行顯示列號。
其中代碼內容需要用HtmlEncode進行編碼,替換掉'<'、'>'等escape符號。如果查看過CSDN的“插入代碼”輸出的HTML源碼,可以發現其採用的就是<textarea>方案,而且到目前爲止還存在bug,因爲其沒有對插入c++代碼進行Html編碼,所以當代碼中有掉'<'或'>'時,會導致插入代碼錯誤。
My Blog Plugin
在瞭解了Syntax Highlighter的輸出格式後,就可以設置ContentSource的輸出方式了,我的設計是當點擊插件後,彈出以下對話框:
在其中選擇好語言種類和各類配置後,在文本框中輸入代碼,點擊插入後,就按照Syntax Highlighter的格式輸出HTML文本,核心代碼如下:
1: Imports WindowsLive.Writer.Api
2: Imports System.Windows.Forms
3: Imports System.Text
4:
5: <WriterPlugin("4cbc0496-d225-4abb-afe3-58299032fde3", _
6: "CSDN Coding", _
7: Description:="Windows Live Writer Plugin for CSDN", _
8: ImagePath:="blog.bmp", _
9: PublisherUrl:="http://blog.csdn.net/icefireelf")> _
10: <InsertableContentSource("CSDN代碼")> _
11: Public Class PluginAdapter
12: Inherits ContentSource
13:
14: Private Sub SaveSettings(ByVal cf As CodeForm)
15: Options.SetBoolean("CSDN_EnableFolding", cf.EnableFolding)
16: Options.SetBoolean("CSDN_EnableLineNum", cf.EnableLineNum)
17: Options.SetBoolean("CSDN_EnableToolBar", cf.EnableToolBar)
18: Options.SetInt("CSDN_SelectedIndex", cf.SelectedIndex)
19: End Sub
20:
21: Private Sub LoadSettings(ByVal cf As CodeForm)
22: cf.EnableFolding = Options.GetBoolean("CSDN_EnableFolding", False)
23: cf.EnableLineNum = Options.GetBoolean("CSDN_EnableLineNum", True)
24: cf.EnableToolBar = Options.GetBoolean("CSDN_EnableToolBar", True)
25: Dim index As Integer = Options.GetInt("CSDN_SelectedIndex", -1)
26: If (index >= 0) Then
27: cf.SelectedIndex = index
28: End If
29: End Sub
30:
31: Public Function HtmlEncode(ByVal content As String) As String
32: Dim ret As String = HtmlServices.HtmlEncode(content)
33: Return ret
34: End Function
35:
36: Public Overrides Function CreateContent(ByVal dialogOwner As IWin32Window, ByRef content As String) As System.Windows.Forms.DialogResult
37: Try
38: Using cf As New CodeForm
39: LoadSettings(cf)
40: If (cf.ShowDialog(dialogOwner) = DialogResult.No OrElse String.IsNullOrEmpty(cf.GetCode)) Then
41: Return DialogResult.No
42: End If
43: SaveSettings(cf)
44: content = "<pre name=""code"" class=""" & cf.GetLanguage
45: If (cf.EnableFolding) Then
46: content &= ":collapse"
47: End If
48:
49: If (Not cf.EnableLineNum()) Then
50: content &= ":nogutter"
51: Else
52: Dim first As Integer = cf.GetFirstLine()
53: If (first > 1) Then
54: content &= ":firstline[" & first.ToString & "]"
55: End If
56: End If
57:
58: If (Not cf.EnableToolBar) Then
59: content &= ":nocontrols"
60: End If
61: content &= """>" & HtmlEncode(cf.GetCode()) & "</pre>"
62: Return DialogResult.OK
63: End Using
64: Catch ex As Exception
65: MessageBox.Show(ex.Message)
66: Return DialogResult.No
67: End Try
68: End Function
69: End Class
通過代碼可以看出,我選擇了用<pre>標籤,而沒有采用CSDN的<textarea>方案,這是因爲Windows Live Writer目前存在一個Bug:會在編輯模式下自動將<textarea>中的回車都刪除,這就會導致所有代碼被縮成一行(在CSDN網頁上寫好的代碼用WLW下載下來後千萬不要上傳,因爲它會修改<textarea>的格式)。
目前此工程的所有源代碼已經上傳至 http://download.csdn.net/source/3156349 ,有興趣的朋友可以下載參考,或者直接編譯後將生成的dll放入WLW的插件目錄直接使用。