zoukankan      html  css  js  c++  java
  • 再议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

  • 相关阅读:
    LockFile文件-解决并发写入日志的问题
    二、Consul Service Mesh
    查看CPU和内存,用机器指令和汇编指令编程
    环境配置过程中的一些小tips
    工具使用指北:GDB
    瞧瞧我发现了什么
    新的目标:Capture The Flag
    python 实现的idw插值方法
    Python 利用 百度接口输入地点名字返回经纬度
    轻松搞定javascript变量(闭包,预解析机制,变量在内存的分配 )
  • 原文地址:https://www.cnblogs.com/yiyan127/p/SupplementOfCF_HTML.html
Copyright © 2011-2022 走看看