有志者事竟成。

JS文本换行算法-模拟计算文字换行位置-基于DOM元素自发换行行为和字符分割原理-支持实体编码、不支持标签嵌套和富文本

简介
之前在学习HTML的时候一直很想弄清楚HTML内部换行的逻辑,特别是有时候我们想知道一个字符串放入一个DOM元素之后究竟在哪个字符位发生的换行,然后就可以知道在一个固定宽高且隐藏溢出的容器中当前用户看见的字符到底有多少个,具体是哪几个等。然而,原生的HTML并没有提供这个功能,所以就要自己写算法来实现咯。

下一篇实现小程序文本换行算法

1.原理解析
1.1 常见HTML换行效果
想要模拟HTML的换行的话,首先要对HTML自发换行的原理有一些了解,在我所学的知识中,HTML的换行逻辑是这样的。对于一个标签内文字的渲染如:

<div style="width: 200px;border: 1px solid #000;">
    1 3&nbsp;sad中文       12o3iaslkdjaksldj aslkdj alskdjklsj    这是一段测试文字,没有什么实际含义。 no sense! 
    123  
</div>

渲染出来的效果是这样的:

1.2文本换行相关的因素
简单分析一下html处理换行的逻辑:
首先,截止20190725,HTML中DOM元素内文本换行的逻辑主要和以下几个方面有关:

  1. 容器的宽度width;
  2. 文本的样式,如font-size等;
  3. 容器的空白符处理逻辑,主要是看标签容器的white-space属性;
  4. 容器的单词截断逻辑,主要是看标签容器的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

posted on 2021-04-01 10:34  阿长*长  阅读(764)  评论(0编辑  收藏  举报

导航