再议HTML Clipboard Format

起因

在写作编写一个Open Live Writer的VSCode代码插件的彩蛋部分时,写的VSHtmlPaste一直有问题。具体来说,就是VSHtmlPaste产生的Html中的EndHTML、EndFragment、EndSelection比实际的多了6。具体表现就是在复制的代码后面有<!--En这些字符。比如复制pubic,效果如下:

VSHtmlPaste效果图

那篇文章已经够长了,就另起了这篇来探讨这个问题。

背景

在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.]+

)+");

        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[] { "

" }, 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,略微一看,发现正是要找的代码。

ClipboardSupport

从图中可以看出,计算EndFragment使用的是字节数,而并不是字符数。

再次前往HTML Clipboard Format,仔细阅读,果然,描述使用的是字节数。而且明确说明了只支持UTF8,在上下文中可以使用其他字符。

HTMLDescription

SupportedCharSet

造成这个问题的主要原因在于我在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格式数据。

VSHTML

仔细一看,新宋体3个字很是特别。而这个新宋体是VS中的默认字体。所以一复制,样式中就出现了新宋体。而在UTF-8中中文占3个字节。使用GetByteCount函数计算一下对应的字节数。嗯,是9,比起字符数3,确实多了个6。

VS默认字体设置

再次验证

在VSCode中顺便找一行代码中添加注释,注释内容为一大串中文字符,再次复制并通过代码插件插入OLW,果然,报错了。

VSCode代码插件报错

修改方案

既然问题原因已经找到了,接下来的问题就是修复了。

第一次尝试

修复我的第一反应就是去Encoding类中查看是否有获取字符字节数的重载,然而并没有,只有获取字符数组和字符指针的字节数的重载。

GetByteCount

这样也不是不行,可以把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--> 出现的位置,都不用解析描述。

Fragment

但是<!--StartFragment-->和<!--EndFragment—>貌似不是硬性要求,不过VS和VSCode产生的HTML都采用了该方式,所以在当前场景下也算可用。代码太简单,就不贴上来了。

彩蛋

样式不一致?

在验证一节中,有心细的朋友可能会发现,VS中设置的字体大小是10,但是在复制生成的HTML代码中,font-size却是13px,是不是又有Bug了?

VS默认字体设置

VSHTML

其实这是因为单位不同而导致的,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代码插件

HTML Clipboard Format

GitHub - microsoft/VS-PPT: Productivity Power Tools - a set of Visual Studio extensions improving developer productivity.

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

原文地址:https://www.cnblogs.com/yiyan127/p/SupplementOfCF_HTML.html