前端模拟终端(三):文本显示与自动换行

目录

前端模拟终端(一):如果我的这款 IOTerm 不是你想要的
前端模拟终端(二):部分可输入而部分不可修改的多行文本域
前端模拟终端(三):文本显示与自动换行
前端模拟终端(四):显示、输入与光标
前端模拟终端(五):看谁用了 rm -rf / 之历史记录
前端模拟终端(六):快捷输入的好助手、终端的灵魂之补全和提示

文本显示与自动换行

  前面都在说 IOTerm 中使用 div 显示文本,但其实一开始选用的是 pre 元素,因为它可以原模原样的显示,而不会少了空格。

  pre 默认使用等宽字体,这也正好符合终端的需要。但是 pre 默认是不会自动换行的,文本会超出元素范围,也就是 white-spacepre。可以将 white-space 改成 pre-wrap,这样就可以既保留空格又能正常换行了。但是因为默认的 world-break 是在单词之间换行,而不会破开整个单词,这就造成在最后一列凹凸不平,不和终端一样。把 world-break 换成 break-all,就可以在单词中断开了。但是效果依然不佳,在都是英文字母,且字体为等宽字体时,还是会出现最后一列不齐的情况。也就是当本应换行处的字母的后面是标签符号时,最后一个字符会被拉到下一行,即标签符号不会出现在一行开头(除了左括号、引号)。有什么好的办法呢?是的,最初,我选择自己写自动换行。不断地测量以确定插入换行符的位置。

  首先需要一个辅助的元素,它的样式要和显示文本的 prediv 一模一样,至少字体、字号等要一样。然后开始不断地尝试与测量吧。最容易想到的当然就是顺序取字符,测量字符串长度。取第一个字符赋值给辅助元素,获取辅助元素的宽度,短了就继续取,长了就插入换行符。取第二个字符与第一个字符组成字符串赋值为辅助元素,获取辅助元素的宽度,进行比较……这样就是一个 O(n)。慢,是肯定的。

  所以,我用了二分法。一行的最大宽度是已知的,记为 maxWidth。设当前测量的字符串为 subText,其宽度为 subTextWidth。当使用的是等宽字体时,就英文来说,每个字符的宽度是一样的,而终端中输入的最多的就是英文了。那么可以通过以下方式估算插入换行符的位置:

endIndex = Math.floor(subText.length * maxWidth / subTextWidth);

  当字符串中有中文等其他文字时,这种估算也只能获取大概的位置,还需要评估其左右。若其宽度 subTextWidth 小于 maxWidth,则往其右再取一个字符,比较宽度,若长了,则 endIndex 为精确的换行位置。若 subTextWidth 大于 maxWidth,则往其左再取一个字符,比较长度,若短了,则 endIndex - 1 为精确的换行位置。

  考虑到实际处理的字符串和显示出来的字符串是可能不一样的,比如存在高亮时,HTML 标签是占长度但不显示的,以及 <、>、& 等转义字符,在代码实现中,将确定插入换行符位置和处理 HTML 标签以及转义字符分为两个步骤。具体代码如下:

private getLineFeedsIndices(text: string) {
    // Find the indices where to insert line feeds into the string `text`.
    //
    // This function returns a object containing `indices`, `numRows` and
    // 'colOffset'. The `indices` is an array recording the indices where to
    // insert line feeds in the string `text`. The `numRows` is the number
    // of rows after the `text` is inserted with line feeds 
.
// The 'colOffset' is the width in pixel of the last line after the // `text` is inserted with line feeds
.
// The `startIndex` is the starting index of the string `subText` in the // string `text`. let startIndex = 0; let maxWidth = this.main.panel.offsetWidth; let indices: number[] = []; let subText: string; let subTextWidth: number; let endIndex: number; let line: string; let lineWidth: number; let preState: number; // In each iteration, find a line whose length is less than `maxWidth`. // The line feed
will not be inserted into the last line even if
// its length is equal to `maxWidth`. while (true) { subText = text.substring(startIndex); this.measurement.innerHTML = escapeText(subText); subTextWidth = this.measurement.offsetWidth; if (subTextWidth <= maxWidth) { break; } // The `endIndex` computed here is a rough index where to insert a // linefeed in string `subText`. And it is also the number of // characters. endIndex = Math.floor(subText.length * maxWidth / subTextWidth); // For an exact index, it is necessary to compare the // relationship between the rough index and its consecutive // indices. preState = 0; while (true) { line = subText.substring(0, endIndex); this.measurement.innerHTML = escapeText(line); lineWidth = this.measurement.offsetWidth; if (lineWidth === maxWidth) { // The `endIndex` here is an exact index where to insert a // line feed into string `subText'. Also, `startIndex` // computed here is an exact index where to insert a // line feed in string `text`. startIndex += endIndex; indices.push(startIndex); break; } else if (lineWidth < maxWidth) { // If a string consists of some narrow characters at // the beginning and wide characters at end, such as // '1234一二三四', the rough index could be smaller. if (preState > 0) { startIndex += endIndex; indices.push(startIndex); break; } else if (preState === 0) { preState = -1; } endIndex++; } else { if (preState < 0) { startIndex += endIndex - 1; indices.push(startIndex); break; } else if (preState === 0) { preState = 1; } endIndex--; } } } return { indices: indices, // Even a empty line '' will be counted as a newline. numRows: 1 + indices.length, colOffset: subTextWidth }; } private autoWrap(html: string) { // Insert HTML tag
as line feed.
// // The `html` is an escaped and highlighted string that excludes line // feed '\n', '\r\n' or HTML tag
. In it, the HTML tag wraps // around the text for highlighting. Note that character '<', '>' and // '&' should be escaped to '<s', '>' and '&' except for HTML // tags. // // This function returns a string composed by `html` and some line // feeds. this.measurement.innerHTML = html; let text = this.measurement.innerText; let lf = this.getLineFeedsIndices(text); let indices = lf.indices; if (indices === []) { return { wrappedHTML: html, numRows: lf.numRows, colOffset: lf.colOffset }; } let wrappedHTML = ''; let i = 0; let length = 0; let isHTML = false; let startIndex = 0; let index = indices.shift(); while (true) { if (!index) { wrappedHTML += html.substring(startIndex); break; } switch (html.charAt(i)) { case '<': isHTML = true; i++; continue; case '>': isHTML = false; i++; continue; case '&': if (['<', '>'].indexOf(html.substr(i, 4)) !== -1) { i += 3; } else if ('&' === html.substr(i, 5)) { i += 4; } break; } if (isHTML) { i++; continue; } length++; i++; if (length === index) { wrappedHTML += html.substring(startIndex, i) + '
'
; startIndex = i; index = indices.shift(); } } return { wrappedHTML, numRows: lf.numRows, colOffset: lf.colOffset }; }
以下才是正文

  没错,上面的方法太麻烦、太长了,不愿看!虽然这比线性查找快多了,但还需要辅助元素进行多次渲染,而且在复制文本时,也会多出一些本来没有的空白符。所以,最终采用的办法是设置样式中的 line-break 属性为 anywhere!是的,你可能没听过这个属性。我在 Bing 和百度中搜索 line break,竟然没看到一篇中文博文介绍该属性的。重要的东西再突出写一下:

line-break: anywhere;

你可能感兴趣的:(前端模拟终端组件,IOTerm,开发记录)