专栏名称: 依韵宵音
前端工程师
目录
相关文章推荐
宝鸡市场监管  ·  总局令第98号公布! ·  昨天  
惠安县市场监督管理局  ·  【“3·15”百姓点检日】2025年第1期“ ... ·  昨天  
生物学霸  ·  实验室哪托 VS ... ·  2 天前  
51好读  ›  专栏  ›  依韵宵音

网页中文本朗读功能开发实现分享

依韵宵音  · 掘金  ·  · 2017-12-11 08:06

正文

文本首发我的博客 - https://blog.cdswyda.com/post/2017120914

前几天完成了一个需求,在网页中完成鼠标指向哪里,就用语音读出所指的文本。如果是按钮、链接、文本输入框,则还还要给出是什么的提醒。同时针对大段的文本,不能整段的去读,要按照标点符号进行断句处理。

重点当然就是先获取到当前标签上的文本,再把文本转化成语音即可。

标签朗读

这个很简单了,只用根据当前是什么标签,给出提示即可。

// 标签朗读文本
var tagTextConfig = {
    'a': '链接',
    'input[text]': '文本输入框',
    'input[password]': '密码输入框',
    'button': '按钮',
    'img': '图片'
};

还有需要朗读的标签,继续再添加即可。

然后根据标签,返回前缀文本即可。

/**
 * 获取标签朗读文本
 * @param {HTMLElement} el 要处理的HTMLElement
 * @returns {String}   朗读文本
 */
function getTagText(el) {
    if (!el) return '';

    var tagName = el.tagName.toLowerCase();

    // 处理input等多属性元素
    switch (tagName) {
        case 'input':
            tagName += '[' + el.type + ']';
            break;
        default:
            break;
    }

    // 标签的功能提醒和作用应该有间隔,因此在最后加入一个空格
    return (tagTextConfig[tagName] || '') + ' ';
}

获取完整的朗读文本就更简单了,先取标签的功能提醒,再取标签的文本即可。

文本内容优先取 title 其次 alt 最后 innerText

/**
 * 获取完整朗读文本
 * @param {HTMLElement} el 要处理的HTMLElement
 * @returns {String}   朗读文本
 */
function getText(el) {
    if (!el) return '';

    return getTagText(el) + (el.title || el.alt || el.innerText || '');
}

这样就可以获取到一个标签的功能提醒和内容的全部带朗读文本了。

正文分隔

接下来要处理的就是正文分隔了,在这个过程中,踩了不少坑,走了不少弯路,好好记录一下。

首先准备了正文分隔的配置:

// 正文拆分配置
var splitConfig = {
    // 内容分段标签名称
    unitTag: 'p',
    // 正文中分隔正则表达式
    splitReg: /[,;,;。]/g,
    // 包裹标签名
    wrapTag: 'label',
    // 包裹标签类名
    wrapCls: 'speak-lable',
    // 高亮样式名和样式
    hightlightCls: 'speak-help-hightlight',
    hightStyle: 'background: #000!important; color: #fff!important'
};

最开始想的就是直接按照正文中的分隔标点符号进行分隔就好了呀。

想法如下:

  1. 获取段落全部文本
  2. 使用 split(分隔正则表达式) 方法将正文按照标点符号分隔成小段
  3. 每个小段用标签包裹放回去即可

然而理想很丰满,现实很骨感。

两个大坑如下:

  1. split 方法进行分隔,分隔后分隔字符就丢了,也就是说把原文的一些标点符号给弄丢了。
  2. 如果段落内还存在其他标签,而这个标签内部也正好存在待分隔的标点符号,那包裹分段标签时直接破换了原标签的完整性。

关于第一个问题,丢失标点的符号,考虑过逐个标点来进行和替换 split 分隔方法为逐个字符循环来做。

前者问题是原本一次完成的工作分成了多次,效率太低。第二种感觉效率更低了,分隔本来是很稀疏的,但是却要变成逐个字符出判断处理,更关键的是,分隔标点的位置要插入包裹标签,会导致字符串长度变化,还要处理下标索引。代码是机器跑的,或许不会觉得烦,但是我真的觉得好烦。如果这么干,或许以后哪个AI或者同事看到这样的代码,说不定会说“这真是个傻xxxx”。

第二个问题想过很多办法来补救,如先使用正则匹配捕获内容中成对的标签,对标签内部的分隔先处理一遍,然后再处理整个的。

想不明白问题二的,可参考一下待分隔的段落:

<p>这是一段测试文本,这里有个链接。<a>您好,可以点击此处进行跳转</a>还有其他内容其他内容容其他内容容其他内容,容其他内容。</p>

如先使用 /<((\w+?)>)(.+?)<\/\2(?=>)/g 正则,依次捕获段落内被标签包裹的内容,对标签内部的内容先处理。

但是问题又来了,这么处理的都是字符串,在js中都是基本类型,这些操作进行的时候都是在复制的基础上进行的,要修改到原字符串里去,还得记录下原本的开始结束位置,再将新的插进去。繁,还是繁,但是已经比之前逐个字符去遍历的好,正则捕获中本来就有了匹配的索引,直接用即可,还能接受。

但是这只是处理了段落内部标签的问题,段落内肯定还有很多文本是没有处理呢,怎么办?

正则匹配到了只是段落内标签的结果啊,外面的没有啊。哦,对,有匹配到的索引,上次匹配到的位置加上上次处理的长度,就是一段直接文本的开始。下一次匹配到的索引-1就是这段直接文本的结束。这只是匹配过程中的,还有首尾要单独处理。又回到烦的老路上去了。。。

这么烦,一个段落分隔能这么繁琐,我不信!

突然想到了,有文本节点这么个东西,删繁就简嘛,正则先到边上去,直接处理段落的所有节点不就行了。

文本节点则分隔直接包裹,标签节点则对内容进行包裹,这种情况下处理的直接是dom,更省事。

文本节点里放标签?这是在开玩笑么,是也不是。文本节点里确实只能放文本,但是我把标签直接放进去,它会自动转义,那最后再替换出来不就行了。

好了,方案终于有了,而且这个方案逻辑多简单,代码逻辑自然也不会烦。

/**
 * 正文内容分段处理
 * @param {jQueryObject/HTMLElement/String}  $content 要处理的正文jQ对象或HTMLElement或其对应选择器
 */
function splitConent($content) {
    $content = $($content);

    $content.find(splitConfig.unitTag).each(function (index, item) {
        var $item = $(item),
            text = $.trim($item.text());
        if (!text) return;

        var nodes = $item[0].childNodes;

        $.each(nodes, function (i, node) {
            switch (node.nodeType) {
                case 3:
                    // text 节点
                    // 由于是文本节点,标签被转义了,后续再转回来
                    node.data = '<' + splitConfig.wrapTag + '>' +
                        node.data.replace(splitConfig.splitReg, '</' + splitConfig.wrapTag + '>$&<' + splitConfig.wrapTag + '>') +
                        '</' + splitConfig.wrapTag + '>';
                    break;
                case 1:
                    // 元素节点
                    var innerHtml = node.innerHTML,
                        start = '',
                        end = '';
                    // 如果内部还有直接标签,先去掉
                    var startResult = /^<\w+?>/.exec(innerHtml);
                    if (startResult) {
                        start = startResult[0];
                        innerHtml = innerHtml.substr(start.length);
                    }
                    var endResult = /<\/\w+?>$/.exec(innerHtml);
                    if (endResult) {
                        end = endResult[0];
                        innerHtml = innerHtml.substring(0, endResult.index);
                    }
                    // 更新内部内容
                    node.innerHTML = start +
                        '<' + splitConfig.wrapTag + '>' +
                        innerHtml.replace(splitConfig.splitReg, '</' + splitConfig.wrapTag + '>$&<' + splitConfig.wrapTag + '>') +
                        '</' + splitConfig.wrapTag + '>' +
                        end;
                    break;
                default:
                    break;
            }
        });

        // 处理文本节点中被转义的html标签
        $item[0].innerHTML = $item[0].innerHTML






请到「今天看啥」查看全文