题注:在我写这篇文章的时候,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的插件目录直接使用。