语雀如何自动将粘贴的内容识别为代码块?
阅读本答案前,需要你先看懂上篇回答
上文说到,操作系统的剪切板可以同时存储多种格式的数据,但往往应用程序之间相互能识别的格式,只有常用的那些,文本、图片、HTML等。
在浏览器里,之前用来实现复制和粘贴的 API 是 copy 和 paste 事件,严格来说,不是实现复制和粘贴,而是在用户执行复制和粘贴操作时,修改复制和粘贴的默认内容:
document.addEventListener("copy", function (e) {
e.clipboardData.setData("text/plain", "text");
e.clipboardData.setData("text/html", "<div>html</div>");
e.clipboardData.setData("foo", "111");
e.clipboardData.setData("bar", "222");
e.preventDefault(); // 忽略用户自己的复制的东西
});
比如上面的代码,当用户按下 CTRL + C 时,不管他复制了什么,都将我们制定的四种格式的数据写入剪切板,前两个 format 是标准的 MIME 类型,分别对应文本和 HTML,后面两个一看就不是 MIME,所以叫做 custom MIME type/custom format,这个时候当你查看系统剪切板时:
会看到有三种格式的数据,其中 public.utf8-plain-text 以及下面的 NSStringPboardType 两种类型存放的就是都是我们存进去的文本,而 public.html 和 Apple HTML pasteboard type 两个存放的是 HTML:
浏览器额外加了个 meta 标签。需要注意的来了,自定义的那两个格式 foo 和 bar,并没有单独存在于剪切板中,而是被浏览器整合到了一个叫 org.chromium.web-custom-data 的格式里:
这里使用的编码格式是 UTF16,所以按照 UTF8 解码看到很多 NULL 字符,其中多种自定义格式的名称和值之间,用某些不可打印字符分割,这样浏览器就能按照自己的规则重新解析回去。比如我们监听页面的 paste 事件:
document.addEventListener('paste', function(e) {
debugger
})
当按下 CTRL + V 时,就能拿到剪切板里的四种格式的数据:
不过你也注意到了,这个格式在剪切板里叫 org.chromium.web-custom-data,好像是 Chrome 私有的。的确是,当你在 Firefox 里执行粘贴时,看不到那两个自定义格式:
这个时候让我们在 Firefox 里也执行一次复制,结果看到:
格式的名称变成了 org.mozilla.custom-clipdata,虽然里面存的值看起来差不多,但也不一定就一模一样。实际上 Safari 也有自己的名字:
而且明显看到存的值也不一样了。这就意味着,三个浏览器的自定义剪切板格式不具有互操作性。
题主问的 VSCode 其实底层是用 Chrome 实现的,包括当你复制代码时,它也是用 JS + copy 事件写入了剪切板数据,比如我们就从这个 TS 代码里复制三行:
看看它到底复制了什么:
除了纯文本,还有带 VSCode 背景颜色和语法高亮颜色的富文本代码:
还有一个自定义格式,看起来像是 JSON 字符串
当你在 Chrome 的网页里用 paste 事件查看时:
会看到三种格式,其中第三种是自定义格式,名称叫做 vscode-editor-data,读出这个格式的值看一下:
其实是一个 JSON 字符串,说明了拷贝的是什么,最有用的就是这个 mode,表明我们复制的是 typescript 代码。不过正如刚才所说,在 Chrome 以外的浏览器里,是读不到这个自定义格式的,除非 VSCode 用原生代码操作剪切板,把三个浏览器的自定义格式都写入一遍。
所以你现在可能认为答案就是:语雀编辑器正是在 paste 事件里使用了 vscode-editor-data,判断出你是复制了代码,然后配合 text/plain 里的纯文本代码,自动给你粘贴成为语雀的代码块。我起初也这么认为,但是当我发现不管复制什么语言的代码,它粘贴的一直都是 plain text 时:
我觉的这个没道理,明明拿到了 typescript,没有理由不自动给用户设置好。于是我开始 debug 他们的代码,这里省略 1000 字调试过程,找到了他们的逻辑其实是只用了 text/html 格式的剪切板内容,把里面的 HTML 解析成了 AST,然后通过一些自己总结的规律,猜测出这个 HTML 是代码片段,比如我们复制的 TS 代码的 HTML 格式是:
<meta charset="utf-8" />
<div
style="
color: #d4d4d4;
background-color: #1e1e1e;
font-family: Menlo, Monaco, 'Courier New', monospace;
font-weight: normal;
font-size: 12px;
line-height: 18px;
white-space: pre;
"
>
<div>
<span style="color: #c586c0">import</span
><span style="color: #d4d4d4"> </span
><span style="color: #ce9178">'./BinaryResourceView.js'</span
><span style="color: #d4d4d4">;</span>
</div>
<div>
<span style="color: #c586c0">import</span
><span style="color: #d4d4d4"> </span
><span style="color: #ce9178">'./BlockedURLsPane.js'</span
><span style="color: #d4d4d4">;</span>
</div>
<div>
<span style="color: #c586c0">import</span
><span style="color: #d4d4d4"> </span
><span style="color: #ce9178">'./EventSourceMessagesView.js'</span
><span style="color: #d4d4d4">;</span>
</div>
</div>
它会看是不是有个 div 节点,这个节点的 style 属性里的 font-family 是不是有包含有 monospace,white-space 是不是 pre,以及这个节点的所有子节点是不是都是 div。这种按照自己总结的经验规律做判断在技术领域叫做 heuristic,启发式算法。
不过正因为语雀没有用 vscode-editor-data,所以这个功能也同样支持 Firefox 和 Safari。不过这还是解释不了语雀在 Chrome 里为什么不额外做一步设置语言类型的事。
题主在题干里说自己用最新的 ClipBoard API 做了尝试,发现 VSCode 只在剪切板里写入了 text/plain 和 text/html 两种格式:
的确是这样的,navigator.clipboard 作为新的 API,直接操控剪切板,不需要用户按下 CTRL+C/V,它并不支持以前 copy+paste 事件支持的自定义类型,因为浏览器之间互不兼容,其他应用程序想要兼容三个浏览器的话读取和写入也停麻烦。所以它有自己的标准实现,就是带 "web " 前缀的 MIME 类型:
const item = new ClipboardItem({
"text/plain": new Blob(["text"], { type: "text/plain" }),
"text/html": new Blob(["<div>html</div>"], { type: "text/html" }),
"web vscode-editor-data": new Blob(['{"mode": "typescript"}'], {
type: "vscode-editor-data",
}),
"web foo/bar": new Blob(["随便"], { type: "foo/bar" }),
});
await navigator.clipboard.write([item]);
后面两个类型的 MIME 都是以 "web" + 空格开头的,这种会被浏览器认为是合法的自定义 MIME,不这样写会报错。在底层实现上,剪切板里用额外一个 map 结构存储了自定义的 MIME 到剪切板里的类型名称的映射:
这样其它浏览器以及其它应用程序就能按照这个标准拿到自定义类型的内容:
Chrome 在 104 里,也就是两个月前刚刚实现了 ClipBoard API 自定义类型的支持,在他们实现过程中,我还反馈了个规范里存在的小 bug https://github.com/w3c/clipboard-apis/issues/178。
总结:
- VSCode 应该用最新的 ClipBoard API 实现写入剪切板自定义类型,如果他们已经更新到 104 内核的话,有人有兴趣可以提 issue 或者 PR。
- 语雀为什么不用 text/plain + vscode-editor-data 来实现代码块的识别和粘贴,高效且体验更好。
- Windows 底层有它自己的剪切板类型名称,但是逻辑是类似的。
吹逼:没有人比我更懂浏览器剪切板: