等宽字符的精确宽度

Precise width of a monospace character

我正在开发一个带有语法高亮显示的代码编辑器,我需要知道等宽字符的精确宽度。我使用这个值来计算一个字符在一行中的位置,我需要知道这个位置以便我可以放置各种 GUI 元素,例如文本光标(可以有多个)、选择矩形、警告工具提示等。到现在为止我一直在使用以下功能:

    function getCharacterWidth(char, fontFamily, fontSize) {
        let span = document.createElement("span");
        span.style.fontFamily = fontFamily;
        span.style.fontSize = fontSize;
        span.style.position = "absolute";
        span.style.visibility = "hidden";
        span.style.width = "auto";
        span.style.whiteSpace = "nowrap";
        span.style.padding = "0";
        span.style.margin = "0";
        span.style.letterSpacing = "0px";
        span.style.wordSpacing = "0px";
        span.innerText = char;
        document.body.appendChild(span);
        
        let width = span.getBoundingClientRect().width;
        span.remove();
        
        return width;
    }

它一直运行良好,但后来我注意到 Google Chrome 上有问题。当我的文本编辑器渲染包含数千个字符的大行时,由于舍入问题,字符位置无法正确计算。似乎在 Google Chrome 上,getBoundingClientRect() 返回的 width 的精度最多为小数点后 5 位,这对于我的用例来说并不理想。在 Firefox 上,精度似乎高得多,最高可达小数点后 15 位,这就是为什么我在那里从未遇到过这个问题。

经过一番挖掘,我听说了根据包含数千个字符的跨度宽度来计算字符宽度的想法 ()。因此,在我原来的函数中,我用 span.innerText = char.repeat(10000) 替换了 span.innerText = char 并返回了 width / 10000。它有所帮助,但当我处理大行时,计算仍然明显不对。

所以我来了。如何在其他浏览器中像 Firefox 一样高精度地计算字符的宽度?

这不是一个干净的解决方案,但我怀疑没有真正干净的解决方案。

您可以保留一个“列映射”,基于您已经掌握的语法格式<span>。说你的荧光笔给你:

<div class="line">
var long_line_of_variables = <span class="number">123</span>;
</div>

你可以测量这个跨度的左偏移+它出现的列(=它之前兄弟姐妹的textContent的长度= 29)并到达

colums = {
  '29': 290.456
}

现在您可以插入第 14 列在 290.456*14/29=140.22 像素处。
我们添加的跨度越多,我们就能猜得越好:

colums = {
  '29': 290.456,
  '2900': 28997.000 // whoops, not what we would have calculated!
}

此方法是启发式的,因此您需要找到一种策略,该策略适用于各种浏览器、zoom/font-scale 设置等,包括

  • 向这个“地图”添加越来越多的跨度
  • 但也许偶尔“清理”一次它?
  • 保留全球地图或每行一个?
  • 智能插值:选择最近的地图条目(条目之间的最佳间隔)
  • 添加更多“跨度探针”
    • 将没有任何格式的长行拆分为 N 个字符的块,将它们包装在跨度中,或者在中间插入空跨度
    • 也许只是每行末尾的一个探测跨度?

在研究过类似的问题和启发式方法后,我的建议是:不要。 :)
它涉及大量的调整和测试,并且可能是跨平台的噩梦(例如比较 Win 上 Firefox 与 Linux 与 MacOS 与 iOS 的字体渲染和舍入)。相反,尝试将 anything 附加到本地化范围。我知道这可能更难——很多文本编辑器都在为排长队而苦苦挣扎,尤其是。当涉及到 MB 大小的编译 JS 时...

我提出了一个解决方案,该解决方案在我的用例 中运行良好。这个想法是根据我的编辑器的最大行来计算字符的宽度。我的线条元素看起来像这样:

<div class="line"><span class="keyword">let</span><span class="space"> </span><span class="identifier">foo</span></div>

如果我们想根据那条线计算一个字符的宽度,我们会这样做:

let lineElement = document.querySelector(".line");
let lineWidth = lineElement.getBoundingClientRect().width;
let charWidth = lineWidth / lineElement.textContent.length;

我会跟踪我在编辑器中呈现的最大行。每当我呈现新的最大行时,我都会根据该新行更新 charWidth

这使我可以通过简单的乘法 column * charWidth 来计算一行中任何给定列中字符的位置。到目前为止,它一直运行良好,即使对于包含 100000 多个字符的行也是如此。

但是,我还必须做最后一件事来处理更大的行。 Google Chrome 上似乎有一个错误,当您尝试呈现带有巨大文本节点的行时,例如<span>many many characters...</span>,它不会为您提供元素的正确宽度(参见 Inaccurate width of large element on Chrome)。为了克服这个问题,每当我必须渲染一个巨大的文本时,我将它分成多个跨度,例如<span>many many </span><span>characters...</span>.

现在我可以毫无问题地渲染 500000 个字符的长行,字体大小为 48。除此之外,事情又开始变得奇怪了,出现计算错误和其他奇怪的浏览器行为。所以我决定设置一行中呈现 500000 个字符的硬性限制。超过 500000n 个字符的所有内容都对用户隐藏。