富文本--格式化数据并保留指定样式实现[原创]

3,154 阅读3分钟

富文本实现格式化,格式化的时候保留图文、文字换行、样式。

开始

有一个需求,粘贴的时候格式化富文本内容,但是保留图文、文字换行、样式。

一开始我做的时候是全部格式化,也就是粘贴的时候拿到html(怎么拿到百度下就有了,一般大家也都是对别人的富文本进行改造。),然后转DOM,然后getInnerText.大功告成。这时候产品说,保留图文和换行哦。

于是修改下。

保留图文和换行

这个比较简单,看下就行了。

  • 流程:获取img并转存 -> 替换img标签 -> 获取到纯文本 -> 添加换行 -> 恢复图片
  • 保存图片的话先把图片下载下来,然后拿到blob再上传到服务器。

不做解析。 (核心代码就这些)

虽然比较简单,但是也有几个地方需要注意的是

  • 图片复制粘贴进来的直接下载会跨域,也就别想绕过去了,弄个代理吧 (1)
  • world复制竟来的别想了拿到图片,纯web做不到,因为拿到的是纯本地路径,可以提醒单图复制粘贴
  • 复制进来图片最好先检验是否图片存在,检验可以新建一个img,然后看是否走入onload
  • 微信公众号的比较特殊,如果用nginx(大神忽略), 返回图片是一个微信警告,可以用nodejs做代理

Koa代理代码

main.js

const error_img_url = 'https://hongqiaojiaoyu.oss-cn-shenzhen.aliyuncs.com/huazhang/imgs/error.png'
const Koa = require('koa');
const app = new Koa();
const get_url_buffer = require('./js/get_url_buffer')
const port = 3024

// 响应
app.use(async (ctx, next) => {
    const path = ctx.request.path
    console.log(path)
    if(path.match(/^\/_proxy\/.*/)){
        var body = await new Promise(function(resolve, reject) {
            get_url_buffer(
                (path.match(/(?:(?<=\/_proxy\/)).*/) || [error_img_url])[0],
                (body) => {
                    resolve(body)
                },
                (uri) => {
                    console.log(uri)
                }
            )
        })
        ctx.set("Access-Control-Allow-Origin", "*")
        ctx.status = 200
        ctx.type = 'jpg'
        ctx.length = Buffer.byteLength(body)
        ctx.body = body
    }
});

app.listen(port);
console.log(`启动完成 端口${port}`)

get_url_buffer.js

var http = require('http')
var https = require('https')
const url = require('url')

/**
 * 获取图片buffer
 * @param {*} _url 要转换的地址
 * @param {*} success 成功 success(buffer)
 * @param {*} fail 失败 fail(uri)
 */
var get_url_buffer = (_url, success, fail) => {
    var uri = url.parse(_url)
    // console.log(uri)
    console.log(_url)
    var option = {
        ...uri,
        method: 'GET',
    }
    var handle_cb = (res) => {
        if(res.statusCode === 301){
            const url = res.headers['location']
            console.log('重定向', url)
            get_url_buffer(url, success, fail)
            return
        }
        var img = []
        var size = 0
        res.on("data", (chunk) => {
            img.push(chunk)
            size += chunk.length
        })
        res.on("end", () => {
            // console.log(size)
            const buffer = Buffer.concat(img, size)
            success(buffer)
        })
    }
    if(uri.protocol === 'https:'){
        return https.request(option, handle_cb).end()
    }
    if(uri.protocol === 'http:'){
        return http.request(option, handle_cb).end()
    }
    console.log('协议异常', uri)
    fail(uri)
}

module.exports = get_url_buffer
  • 下载的时候判断是不是https的
  • 判断是不是重定向了,重定向就拿到定向后的代码继续

到这里,我们实现了第一个功能

但是这时候需求添加了一条,如下

图文、换行,指定样式

这时候我们不好下手,我们需要做一个抉择

  • 样式怎么来,换行怎么计算。
  • 或者用哪个插件
  • 或者要不换个富文本算了。

如果已经有一定修改这时候换富文本不太合适,只能去抄别人富文本的或者去找插件。

但是我遇到了如下两个问题,所以还是决定自己来

  • 看起来抄代码很容易,其实代码绑定性质很强的
  • 插件看了slate-paste-html-plugin,但是用起来一堆报错,就放弃了

思路

1: 要去掉垃圾代码,我们就需要重组html格式
2: 去掉html但是样式不会影响
3: 要计算出换行

这个不太好说,我画了一张图

左侧那个是dom树,右侧是修改规则 (但是这个是理想状态,实际获取到的代码不会这么理想,稍后再说怎么解决)

  • 通过左图可以看楚,div里面会有span或者p
  • 当然还有其他,这个p代表的是块元素,span代表的是行内块元素
  • 我们把dom拍扁,这样格式化出来就没有垃圾
  • 排扁后我们需要对元素进行换行操作,这时候需要块元素和行内元素了,我们需要计算这个,计算规则在下一行
  • 右侧图,红色代表寻找路径,绿色代表起点,红色经过的区域如果没有块元素,那么当前起点的dom就算是行内块,反之亦然
  • 为了性能,我们需要先判断他是不是块元素,如果是块元素,就不需要再找路径了

有了思路,就可以实现了

实现

我们需要先递归获取每一个节点,如果是最后一个,那么就计算。 (嗯,很完美
but, 刚刚不是说了,有些不是很符合理想,比如长这样

<p>
    我是文字
    <span>我也是</span>
</p>

按照思路,我们不应该拿这样的dom

所以需要先转下, 代码如下,相信你们可以看懂。

补全span

/**
 * 补全span
 * @param {*} html html片段
 */
function tag_full(html){
    const tag = 'span'
    const reg1 = /(?<=\<(?:\/\w|\w).*>)([^<|>]*)(?=<\w.*>)/g
    const reg2 = /(?<=\<(?:\/\w).*>)([^<|>]*)(?=<\/\w.*>)/g
    const replace_value = (_, p1) => (p1.replace(/\s|\S|t|r/, '') == '' ? '' : `<${tag}>${p1}</${tag}>`)
    return html.replace(reg1, replace_value).replace(reg2, replace_value)
}

转完之后,我们拿最后的节点,但是我们还缺少红色路径。

但是,红色路径怎么拿,看起来有点复杂,我们先拿到当前节点路径再说,就让其他的事情随风吧。

递归获取节点

function traverse_tree(node, path){
    if (!node) return
    if (node.children && node.children.length > 0) {
        for (let i = 0; i < node.children.length; i++) {
            let next_node = node.children[i]
            traverse_tree(next_node, [...path, {node: node, index: i}])
        }
    }else{
        // 其他代码...
    }
}

通过递归,我们遍历的时候顺手拿下节点路径和每个路径下面的节点。

拿到之后,我们和上面一个node节点(这里指的是最后节点)计算分叉位置, 代码如下

获取分叉点

/**
 * 获取分叉点
 * @param {*} old_path [number]
 * @param {*} path [number]
 */
function get_path_diff_index(old_path, path){
    let index = 0
    let k = 0
    for(let item of path){
        old_path[k] === item && index++
        k++
    }
    return index
}

然后计算什么类型就可以了。

这时候,我们就可以再对img、等标签处理了,还有样式。

样式代码送上

设置样式

/**
 * 根据style设置style
 * @param {*} node node节点
 * @param {<filter_styles>} styles ['bold']
 */
function set_style(node, filter_styles){
    let style = {}
    for(let item of filter_styles){
        switch(item){
            case 'bold': {
                let key = 'font-weight'
                let value = get_style(node, key)
                value >= 600 && (style[key] = item)
                break
            }
            case 'color': {
                let value = get_style(node, item)
                console.log(node, value)
                style[item] = value
                break
            }
        }
        ... 其他
    }
    return style
}

然后

合并HTML

/**
 * 合并html
 * @param {*} html_arr 
 */
function merge_html(html_arr){
    function get_style_string(style){
        let style_string = ''
        for(let key in style){
            style_string += `${key}:${style[key]};`
        }
        return style_string ? ` style="${style_string}"` : ''
    }
    const res = html_arr
    .map(e =>
        e.tag === 'img' ? `<p><img src="${e.src}" class="userChoosImg" style="max-width:100%;display: inline-block;" /></p>` :
        block_element_tags.includes(e.tag) ? 
            // 忽略空白文本
            (e.text === '' ? '' : 
                `<${e.tag}${get_style_string(e.style)}>${e.text}</${e.tag}>`) : 
        ``
    )
    .join('')
    console.log('html_format_res: ', res)
    return res
}

然后再循环判断img,把img合并处理,既然走之前的处理图片方法即可。

效果图

尾声

点个赞再走呗

--完--