JS文本换行算法-模拟计算文字换行位置-基于DOM元素自发换行行为和字符分割原理-支持实体编码、不支持标签嵌套和富文本
简介
之前在学习HTML的时候一直很想弄清楚HTML内部换行的逻辑,特别是有时候我们想知道一个字符串放入一个DOM元素之后究竟在哪个字符位发生的换行,然后就可以知道在一个固定宽高且隐藏溢出的容器中当前用户看见的字符到底有多少个,具体是哪几个等。然而,原生的HTML并没有提供这个功能,所以就要自己写算法来实现咯。
1.原理解析
1.1 常见HTML换行效果
想要模拟HTML的换行的话,首先要对HTML自发换行的原理有一些了解,在我所学的知识中,HTML的换行逻辑是这样的。对于一个标签内文字的渲染如:
<div style="width: 200px;border: 1px solid #000;">
1 3 sad中文 12o3iaslkdjaksldj aslkdj alskdjklsj 这是一段测试文字,没有什么实际含义。 no sense!
123
</div>
渲染出来的效果是这样的:

1.2文本换行相关的因素
简单分析一下html处理换行的逻辑:
首先,截止20190725,HTML中DOM元素内文本换行的逻辑主要和以下几个方面有关:
- 容器的宽度width;
- 文本的样式,如font-size等;
- 容器的空白符处理逻辑,主要是看标签容器的white-space属性;
- 容器的单词截断逻辑,主要是看标签容器的word-wrap(或叫overflow-wrap)和word-break属性;关于,white-space,word-wrap,word-break这三个属性,大家可以参考下面的文字文章,或自己去实验看看。作者:杭电茶娃,链接:https://blog.csdn.net/c11073138/article/details/79534394 ,标题如下:常见样式问题七、word-break、word-wrap、white-space区别
1.3HTML换行原理简单分析
一个html标签,如上面1.1中的div,文本换行的时候其实做了如下的操作:
1.取出标签中间的文字存入字符串,去掉首尾空格;
2.根据white-space的空白符逻辑完成空白符替换;
3.根据word-break和word-wrap进行单字或单词分割。
4.渲染出div的宽高和文字的样式,往容器中填充文字,当文字宽度溢出时进行换行。
5.一些额外的细节处理,这里就不说了,大家可以多去实验观察一下,我也是实验试出来的。
1.4我的解决方案
要求外部传入一个字符串str,和需要模拟的容器的样式styles,自动在模拟换行的位置插入\n。具体操作如下:
1.创建一个div容器,将styles样式设置到div上,将str作为div的innerHTML。
2.获取div的innerText作为空白符处理的结果(innerText是innerHTML解析并按照white-space进行空白符替换的结果)
3.对innerText进行分割,先用正则表达式\b(单词边界)进行初次分割,在根据word-wrap和word-break的设置进行二次分割。最终得到一个数组,如字符串"123你好 ab 2就2d"默认会被分割为[“123”,“你”,“好”," “,“ab”,” ",“2”,“就”,“2d”]。
4.将div容器转变为行容器,即限制高度为1个font-size,尝试往行容器中添加单字或单词,当添加一个单字时行容器的实际高度高于限制高度时,说明产生了换行,则将行容器中已有的字符认为是一行,清空行容器,并从当前单词或单字开始重复进行试添加。
5.在需要换行的地方插入’\n’得到最终结果。
1.5注意点
HTML在进行单词或单字分割的时候,会区分CJK字符与非CJK字符,CJK即Chinese、Japanese、Korean,是指以中日韩统一文字为代表的字符。CJK字符相比起英文等字符的区别在于,CJK字符可以在任意字符处换行,不像英文字符默认情况下只允许在空格处或部分标点符号处换行。
代码中包含CJK字符的正则识别:
2.上代码
//代码风格模仿jQuery,用闭包和对象存储函数
;(function(window,document,$){
//使用示例:dc.getWrapString('asl kaswdasdsadjsk 中文加快速度 12123奥术大师是的撒 asdsds sadfsfdsgf asdsafsdfdsf3',{"font-size":"12px","width":"80px"});
var dc = {
//通过传入的字符串、容器宽度、字体大小样式等进行模拟计算,自动在需要换行的地方拼接上换行符\n,效果类似于html的换行算法,默认将所有空白符当成一个空格处理
getWrapString: function(str,styles){//传入的str需是能被html识别的字符串,暂不支持富文本,函数返回值也是字符串
if(!str||str.length<=0) return '';
/*
解释:此函数是用于模拟html中处理文本换行的逻辑,但未必可以完全模拟,大多时候只用于模拟定宽div标签和pre标签的换行行为
html中控制文本换行主要由white-space、word-break(或word-wrap(别名overflow-wrap))这些属性来控制.
其中word-wrap(overflow-wrap)比较简单,取值只有break-word和normal。控制英文单词是否允许换行时截断
word-break类似于word-wrap但是它有一个取值keep-all比较特殊,这里不做过多说明。 在dom元素中渲染文字换行时,首先根据white-space的取值去替换空白符,再结合word-wrap和word-break去做换行截断。其中CJK字符和非CJK字符在换行时有一些区别。
CJK可简单理解为中国日本韩国文字字符,意义是可以在任意字符处换行的字符,不像英文等字符常规状态下需要把单词当做一个整体,遇到空格才允许换行。 此算法将大部分逻辑交由DOM元素的自发行为去完成,主要的功能在于计算出换行的位置
*/
//1.将str填充到一个不可见的容器中,利用html的空白符逻辑进行初步处理
var $div = $('<div></div>').html(str).appendTo('body').css({
"position": "absolute",
"z-index": -1,
"opacity": 0,
"filter": "alpha(opacity=0)",
"-ms-filter": "alpha(opacity=0)"
});
//styles为容器的样式,需外部传入宽度,另可以传入font-size、white-space等信息
try{$div.css(styles);}catch(e){}
//1.1获取初步处理的字符串,即已完成空白符替换,但还未计算溢出换行。
var rawText = $div.text();
//1.2复用刚才的文本容器作为行容器,增加一些所需样式
var $line = $div.empty().css({
"overflow": "hidden",//原本是为了方便计算水平溢出,但是后面我发现其实用不上
"line-height": "1"//设置行高为1个font-size便于计算是否产生换行
});
//1.3记录word-wrap等设置
var isBreakWord = ($line.css('word-wrap')+$line.css('word-break')).indexOf('break')>=0;
var lineHeight = parseFloat($line.css("font-size"));//记录行高判断是否产生换行
//下面将上面初步处理的字符串进行字符分割。
var words = [],temp = rawText.split(/\b/);//根据单词边界进行初次分割,分割后的non-CJK字符会变成一个整体,但CJK字符需要进行二次分割
//2.对英文单词或一串中文进行二次分割
for(var i=0;i<temp.length;i++){
//将包含CJK字符的项进行单字分割后存入words数字中 //另外,如果设置了break-word则将英文单词分割为单字
if(dc.isCJKCharacter(temp[i])||isBreakWord){
words.push.apply(words,temp[i].split(''));
}else{//不包含CJK字符的项则视为一个整体直接存进words数组
words.push(temp[i]);
}
}
//预留用一个二维数组保存计算结果,如lines=[['12',' ','中'],['asdf','哈']]则表示结果有2行,第一行为'12 中',第二行为'asdf哈'
var lines = [],oneline=[];//oneline是一维数组,保存一行中的单词
//3.遍历求解换行位置,即尝试向行容器中添加字符。
for(var i=0;i<words.length;i++){
var word = words[i];
if(word=='\n'){//如果当前单词是换行符
//则将之前的内容存为1行,并追加保存一个空行
lines.push(oneline,[]);
//清空行容器
$line.empty();
//开始记录新行
oneline = [];
//结束本次循环
continue;
}
$line.append(word);//往行容器中增加内容
if($line.height()<=lineHeight){//如果没有产生换行
//将当前单词或单字追加记录到oneline中
oneline.push(word);
}else{//如果产生了换行
//前面的单词存为一行
lines.push(oneline);
//清空行容器
$line.empty();
//开始记录新行
oneline=[];
//回退,在新行中尝试添加当前单词或单字
i--;
continue;
}
}
//3.1矫正,手动插入最后一行。
lines.push(oneline);
//4.收尾工作
//4.1将二维数组还原为字符串
var string = '';
for(var i=0;i<lines.length;i++){
string+=lines[i].join('')+'\n';
}
string = string.substring(0,string.length-1);
//4.2移除行容器
$line.remove();
//返回计算结果
return string;
},
//传入一个字符串,判断是否包含CJK字符串,CJK即中国日本韩国文字字符。
isCJKCharacter: function(ch){
/*
参考网站:https://blog.csdn.net/iteye_4476/article/details/81652883,来源Unicode官网
CJK字符不同于其他字符,CJK字符可以在任意地方换行,不像英文字符等正常情况必须在单词后换行
截止20190725,根据我查到的资料(Unicode 5.0版),js中的Unicode为2个字节,CJK字符在2个字节的unicode中包括以下范围
//大多数情况下,可以取1.~5.来判断CJK字符
1.标准CJK文字
1.1 [\u3400-\u3db5] => CJK统一表意文字扩展A (发行版3.0)
1.2 [\u4e00-\u9fa5] => CJK统一表意文字 (发行版1.1) (常用来简单判断中文字符)
1.3 [\u9fa6-\u9fbb] => CJK统一表意文字 (发行版4.1)
1.4 [\uf900-\ufa2d] => CJK兼容文字 (发行版1.1)
1.5 [\ufa30-\ufa6a] => CJK兼容文字 (发行版3.2)
1.6 [\ufa70-\ufad9] => CJK兼容文字 (发行版4.1)
(以下两项编码超过两个字节,js中暂时不能用,但是还是写一下(js中无法使用的编码范围前面有双斜杠//标志):
//1.7 [\u20000-\u2a6d6] => CJK统一表意文字扩展B (发行版3.1)
//1.8 [\u2f800-\u2fa1d] => CJK兼容补充 (发行版3.1))
2. [\uff00-\uffef] => 全角中英文标点符号、半宽片假名、半宽平假名、半宽韩文字母
3. [\u2e80-\u2eff] => CJK部首补充
4. [\u3000-\u303f] => CJK标点符号
5. [\u31c0-\u31ef] => CJK笔划
6. [\u2f00-\u2fdf] => 康熙部首
7. [\u2ff0-\u2fff] => 汉字结构描述字符
8. [\u3100-\u312f] => 注音符号
9. [\u31a0-\u31bf] => 注音符号(闽南语客家语扩展)
10. [\u3040-\u309f] => 日文平假名
11. [\u30a0-\u30ff] => 日文片假名
12. [\u31f0-\u31ff] => 日文片假名拼音扩展
13. [\uac00-\ud7af] => 韩文拼音
14. [\u1100-\u11ff] => 韩文字母
15. [\u3130-\u318f] => 韩文兼容字母
//16. [\u1d300-\u1d35f] => 太玄经符号
17. [\u4dc0-\u4dff] => 易经六十四卦象
18. [\ua000-\ua48f] => 彝文音节
19. [\ua490-\ua4cf] => 彝文部首
20. [\u2800-\u28ff] => 盲文符号
21. [\u3200-\u32ff] => CJK字母及月份
22. [\u3300-\u33ff] => CJK特殊符号(日期合并)
23. [\u2700-\u27bf] => 装饰符号(非CJK专用)
24. [\u2600-\u26ff] => 杂项符号(非CJK专用)
25. [\ufe10-\ufe1f] => 中文竖排标点
26. [\ufe30-\ufe4f] => CJK兼容符号(竖排变体、下划线、顿号)
*/
var cjk = {
NO1Unihan: ['\\u3400-\\u3db5','\\u4e00-\\u9fa5','\\u9fa6-\\u9fbb','\\uf900-\\ufa2d','\\ufa30-\\ufa6a','\\ufa70-\\ufad9','',''],
NO2UFF00: ['\\uff00-\\uffef'],
NO3U2E80: ['\\u2e80-\\u2eff'],
NO4U3000: ['\\u3000-\\u303f'],
NO5U31C0: ['\\u31c0-\\u31ef'],
NO6U2F00: ['\\u2f00-\\u2fdf'],
NO7U2FF0: ['\\u2ff0-\u2fff'],
NO8U3100: ['\\u3100-\\u312f'],
NO9U31A0: ['\\u31a0-\\u31bf'],
NO10U3040: ['\\u3040-\\u309f'],
NO11U30A0: ['\\u30a0-\\u30ff'],
NO12U31F0: ['\\u31f0-\\u31ff'],
NO13UAC00: ['\\uac00-\\ud7af'],
NO14U1100: ['\\u1100-\\u11ff'],
NO15U3130: ['\\u3130-\\u318f'],
NO16U1D300: [''],//\\u1d300-\\u1d35f
NO17U4DC0: ['\\u4dc0-\\u4dff'],
NO18UA000: ['\\ua000-\\ua48f'],
NO19UA490: ['\\ua490-\\ua4cf'],
NO20U2800: ['\\u2800-\\u28ff'],
NO21U3200: ['\\u3200-\\u32ff'],
NO22U3300: ['\\u3300-\\u33ff'],
NO23U2700: ['\\u2700-\\u27bf'],
NO24U2600: ['\\u2600-\\u26ff'],
NO25UFE10: ['\\ufe10-\\ufe1f'],
NO26UFE30: ['\\ufe30-\\ufe4f']
};
var reg,str='[';
for(var k in cjk){
str+=cjk[k].join('');
}
str+=']+';
if(str!='[]+'){
reg = new RegExp(str,'m');
return reg.test(ch);
}
return null;
}
};
window.dc = dc;
dc.getWrapString('asl kaswdasdsadjsk 中文加快速度 12123奥术大师是的撒 asdsds sadfsfdsgf asdsafsdfdsf3',{"font-size":"12px","width":"80px"});
})(window,document,jQuery);
3.运行结果

可以看到,代码计算出的换行位置和html的显示效果是一致的,我觉得基本满足我的需求了,就没有继续再往下写了,如果要完全模拟html的文字换行机制从而搬到其他语言的图形界面中,那么就会涉及到标签的渲染和实体编码的替换了,分析起来还是挺麻烦的,就到此为止吧。
4.结语
本文写于20190729,算法其实上周就写好了,不过没有时间整理成博客,整理成博客之后也没有太多时间去雕琢。写出来是方便自己查阅,也让一些和我一样曾经纠结于html的换行机制的小伙伴们有一些启发。最后还是那句话,走过路过点个赞再走呗。被人关注还是有一些开心的。
————————————————
版权声明:本文为CSDN博主「月桦剑士」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/qq_26909801/article/details/97298710
JS文本换行算法-模拟计算文字换行位置-基于DOM元素自发换行行为和字符分割原理-支持实体编码、不支持标签嵌套和富文本的更多相关文章
- d3.js:数据可视化利器之 修改文档:DOM操作符
style: CSS样式操作符 style()操作符用来设置或获取选择集中各DOM元素的CSS样式: selection.style(name[,value[,priority]]) style()操 ...
- C1:DOM 元素的尺寸和位置
DOM元素的尺寸和位置 DOM 元素的尺寸 DOM.offsetWidth/offsetHeight: 包括内容区宽/高,padding,border,不包括margin.如果元素的box-sizei ...
- JS获取HTML DOM元素的方法
JS获取HTML DOM元素有八种方法: 1.根据id获取标签元素: document.getElementById("id名"); 2.根据标签名获取标签列表: document ...
- three.js使用gpu选取物体并计算交点位置
光线投射法 使用three.js自带的光线投射器(Raycaster)选取物体非常简单,代码如下所示: var raycaster = new THREE.Raycaster(); var mouse ...
- 原生js+css实现重力模拟弹跳系统的登录页面
今天小颖把之前保存的js特效视频看了一遍,跟着视频敲了敲嘻嘻,用原生js实现一个炫酷的登录页面.怎么个炫酷法呢,看看下面的图片大家就知道啦. 效果图: 不过在看代码之前呢,大家先和小颖看看css中的o ...
- 【BZOJ4837】LRU算法 [模拟]
LRU算法 Time Limit: 6 Sec Memory Limit: 128 MB[Submit][Status][Discuss] Description 小Q同学在学习操作系统中内存管理的 ...
- js中的各种宽高以及位置总结
在javascript中操作dom节点让其运动的时候,常常会涉及到各种宽高以及位置坐标等概念,如果不能很好地理解这些属性所代表的意义,就不能理解js的运动原理,同时,由于这些属性概念较多,加上浏览器之 ...
- vue.js基础知识篇(3):计算属性、表单控件绑定
第四章:计算属性 为了避免过多的逻辑造成模板的臃肿不堪,可使用计算属性来简化逻辑. 1.什么是计算属性 <!DOCTYPE html><html lang="en" ...
- diff.js 列表对比算法 源码分析
diff.js列表对比算法 源码分析 npm上的代码可以查看 (https://www.npmjs.com/package/list-diff2) 源码如下: /** * * @param {Arra ...
- js数据结构与算法--双向链表的实现
双向链表也叫双链表,是链表的一种,它的每个数据节点中都有两个指针,分别指向直接后继和直接前驱.所以,双向链表中的任意一个节点开始,都可以很方便的访问它的前驱节点和后继节点. 双向链表的实现 linke ...
随机推荐
- 解锁Spring组件扫描的新视角
本文分享自华为云社区<Spring高手之路10--解锁Spring组件扫描的新视角>,作者: 砖业洋__. 首先,我们将探讨一些Spring框架中IOC(Inversion of Cont ...
- 产品代码都给你看了,可别再说不会DDD(一):DDD入门
这是一个讲解DDD落地的文章系列,作者是<实现领域驱动设计>的译者滕云.本文章系列以一个真实的并已成功上线的软件项目--码如云(https://www.mryqr.com)为例,系统性地讲 ...
- HTTP.SYS远程代码执行漏洞验证及其复现(CVE-2015-1635蓝屏洞)
HTTP.SYS远程代码执行漏洞验证及其复现(CVE-2015-1635蓝屏洞) @ 目录 HTTP.SYS远程代码执行漏洞验证及其复现(CVE-2015-1635蓝屏洞) 漏洞概述 http.sys ...
- 对JavaScript中与字符串相关的方法总结
JavaScript中的字符串是由16位码元code unit组成.通常来说,一个字符=16位码元,该类字符也叫做单码元字符.还有一种字符组成策略是代理对,它由两对16位码元组成,即一个字符对应两个1 ...
- ETL之apache hop数据增量同步功能
ETL增量数据抽取CDC 概念:Change Data Capture,变化的数据捕获,也称:[增量数据抽取](名词解释) CDC是一种实现数据的增量抽取解决方案,是实现[ETL整体解决方案]中的一项 ...
- [ABC148F] Playing Tag on Tree
2023-03-04 题目 题目传送门 翻译 翻译 难度&重要性(1~10):5 题目来源 AtCoder 题目算法 最短路 解题思路 考虑到 T 想活得久, A 想尽早追上 T ,所以我们就 ...
- 《Linux基础》09. Shell 编程
@ 目录 1:Shell 简介 2:Shell 脚本 2.1:规则与语法 2.2:执行方式 2.3:第一个 Shell 脚本 3:变量 3.1:系统变量 3.2:用户自定义变量 3.2.1:规则 3. ...
- react移动端上拉加载更多组件
在开发移动端react项目中,遇到了上拉加载更多数据的分页功能,自己封装了一个组件,供大家参考,写的不好还请多多指教! import React, {Component} from 'react'; ...
- Codeforces Round 882 div.2 A
Smiling&Weeping ----总有人间一两风,填我十万八千梦 A. The Man who became a God time limit per test 1 second mem ...
- win11系统无法解决的死结
如果需要使用网上银行.win11一定不能使用. win11已经取消了,对于IE浏览器的支持和安装. 但是大部分网银都是要求IE浏览器.或者IE内核.实际过程当中.虽然所有的浏览器都说兼容IE有IE内核 ...