再议HTML Clipboard Format
起因
在写作编写一个Open Live Writer的VSCode代码插件的彩蛋部分时,写的VSHtmlPaste一直有问题。具体来说,就是VSHtmlPaste产生的Html中的EndHTML、EndFragment、EndSelection比实际的多了6。具体表现就是在复制的代码后面有<!--En这些字符。比如复制pubic,效果如下:
那篇文章已经够长了,就另起了这篇来探讨这个问题。
背景
在VSHtmlPaste中,所复制的Html是由Html是由Productivity Power Tools 2017/2019产生的,通过一个HtmlFragmentExtractor的类提取Html片段。
该类的主要作用是从符合HTML Clipboard Format格式中提取代码片段。该类代码如下:
private static readonly Regex DescriptionRegex = new Regex(@"^([a-zA-Z]+:[a-zA-Z0-9\.]+\r\n)+"); internal static string Extract(string html) { var matches = DescriptionRegex.Matches(html); if (matches.Count == 0) { return string.Empty; } int start = -1; int end = -1; var descriptions = matches[0].Value.Split(new string[] { "\r\n" }, StringSplitOptions.RemoveEmptyEntries); foreach (var description in descriptions) { var pairs = description.Split(':'); var key = pairs[0]; var value = pairs[1]; if (key == "StartFragment") { start = int.Parse(value); continue; } if (key == "EndFragment") { end = int.Parse(value); continue; } } if (start == -1 || end == -1) { return string.Empty; } return html.Substring(start, end - start); }代码很简单,利用正则表达式提取出描述部分,再查找片段的开始和结束,然后提取子串。而这个类的代码已经在VSCodePaste中经过验证,可以正常使用。
查找原因
既然VS的Html是由Productivity Power Tools 2017/2019产生的,那么直接去查看对应的源码好了。下载好了源码,打开工程,定位到CopyAsHtml项目,打开第一个文件:ClipboardSupport,略微一看,发现正是要找的代码。
从图中可以看出,计算EndFragment使用的是字节数,而并不是字符数。
再次前往HTML Clipboard Format,仔细阅读,果然,描述使用的是字节数。而且明确说明了只支持UTF8,在上下文中可以使用其他字符。
造成这个问题的主要原因在于我在C#的世界呆久了,早已忘了当年的MFC了。在阅读Add HTML code to the clipboard by using Visual C++时全是char,就想当然的对应上了C#的char。全然忘了C++的char是1个字节,wchar_t 才是2个字节,而C#的char是2个字节。而在C#中的char代表的只是码点(codepoint),具体一个字符占多少个字节,则是由对应的编码确定。由于HTML Clipboard Format只支持UTF-8,所以占多少个字节是由UTF-8编码确定。
另外一个原因则是使用英文习惯了。在阅读英文资料的时候没有中文的示例,在编写OLW的VSCode代码插件时使用的示例代码中也没有中文,所以没有发现问题。
验证
打开VS,随便复制出一段代码,查看对应的HTML格式数据。
仔细一看,新宋体3个字很是特别。而这个新宋体是VS中的默认字体。所以一复制,样式中就出现了新宋体。而在UTF-8中中文占3个字节。使用GetByteCount函数计算一下对应的字节数。嗯,是9,比起字符数3,确实多了个6。
再次验证
在VSCode中顺便找一行代码中添加注释,注释内容为一大串中文字符,再次复制并通过代码插件插入OLW,果然,报错了。
修改方案
既然问题原因已经找到了,接下来的问题就是修复了。
第一次尝试
修复我的第一反应就是去Encoding类中查看是否有获取字符字节数的重载,然而并没有,只有获取字符数组和字符指针的字节数的重载。
这样也不是不行,可以把String转换成字符数组,然后使用第一个重载,利用二分查找的原理进行统计。代码如下:
internal static string Extract(string html, int fragmentByteStart, int fragmentByteEnd) { int startIndex = fragmentByteStart;//before start usually is ascii int endIndex = Math.Min(fragmentByteEnd - 1, html.Length - 1);//in case of index out of range int target = fragmentByteEnd - fragmentByteStart; char[] array = html.ToCharArray(startIndex, endIndex + 1 - startIndex); int low = 0; int high = array.Length - 1; int middle; while (true) { middle = (low + high) / 2; int byteCount = Encoding.UTF8.GetByteCount(array, 0, middle + 1); if (byteCount == target) { break; } if (byteCount < target) { low = middle + 1; continue; } if (byteCount > target) { high = middle - 1; } } return html.Substring(startIndex, middle + 1); }
只是这样的代码终究不是那么直观,暂时观察,留作后备方案。
第二次尝试
既然没有获取字符字节数的重载,那不妨看看获取字符串字节数的实现。打开Reference Source,找到UTF8Encoding对应的代码。
这么一看,更是复杂,还涉及到Surrogate等概念,毕竟要做到通用,就会复杂一些。但是我们用不到那么多的功能,此方案暂时搁置。
关于编码更多知识可以查看知乎专栏:刨根究底学编程
第三次尝试
Char结构体中是否有获取字节数的函数,虽然基本上不可能,但是万一呢。查看定义,并没有,倒是有一堆Surrogate的函数。
解决方案
既然没有现成的,那就自己动手写一个获取字节数的函数。根据UTF-8编码方案,可以很容易写出代码:
internal static int GetUtf8ByteCount(char c) { int codePoint = c; if (codePoint <= 0x7f)//ascii { return 1; } else if (codePoint <= 0x7ff) { return 2; } else if (codePoint <= 0xffff) { return 3; } else //will not reach,because 0xffff is char.MaxValue { return 4; //Supplementary Multilingual Plane,辅助平面 } }
既然有了函数,剩下的代码就好写了,如下:
internal static string Extract(string html, int fragmentByteStart, int fragmentByteEnd) { int startIndex = fragmentByteStart;//before start usually is ascii int endIndex = -1; int current = fragmentByteStart; for (int i = fragmentByteStart; i < html.Length; i++) { current += GetUtf8ByteCount(html[i]); if (current == fragmentByteEnd) { endIndex = i; break; } } Contract.Assert(endIndex != -1); return html.Substring(startIndex, endIndex + 1 - startIndex); }
经过验证,该方案测试通过。
另一个解决方案
除了上面的方法,还另有一种简单的办法。直接查找<!--StartFragment-->和<!--EndFragment--> 出现的位置,都不用解析描述。
但是<!--StartFragment-->和<!--EndFragment—>貌似不是硬性要求,不过VS和VSCode产生的HTML都采用了该方式,所以在当前场景下也算可用。代码太简单,就不贴上来了。
彩蛋
样式不一致?
在验证一节中,有心细的朋友可能会发现,VS中设置的字体大小是10,但是在复制生成的HTML代码中,font-size却是13px,是不是又有Bug了?
其实这是因为单位不同而导致的,VS设置中的字体大小单位是Point(点、磅、pt),而font-size的单位是pixel(像素、px)。
Point的历史就是孩子没娘,说来话长了,感兴趣的可以通过字号 (印刷)和点 (印刷)来了解。
对于pt和px,只需要记住1pt=1.33px就行了。10*1.33约等于13,这也是font-size为13px的原因。至于原因,是因为72pt=1英寸,而96px=1英寸。更多不同之处可以通过Difference Between Pixel (Px) and Point (Pt) Font Sizes in Email Signatures了解。
汉字数量少VSCodePaste一切正常
在使用VSCodePaste验证时,我发现在代码段中只有一两个汉字注释时,却不会报错。这又是为什么呢?
打开HtmlFragmentParser代码阅读,发现原因在于只有遇到</和>才会处理缓冲区中的文本。而当只有一两个汉字注释时<!--EndFragment-->已经处理了<,却还没有处理到>。因此当文本中汉字少于8个时,都不会报错(<!--EndFragment-->长度为18,而每多1个汉字时,<!--EndFragment-->就会多2个字符被处理。而当18个字符全被处理时,就会处理缓冲区处理,导致校验不通过。所以最多只能多18/2-1个汉字)。
所以在ParseFragment函数结尾应该加上检查缓冲区为空的断言。
参考
编写一个Open Live Writer的VSCode代码插件
Add HTML code to the clipboard by using Visual C++
UTF-8, a transformation format of ISO 10646
Difference Between Pixel (Px) and Point (Pt) Font Sizes in Email Signatures
Difference Between Pixel (Px) and Point (Pt) Font Sizes in Email Signatures
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】