原生 JS 实现 HTML 转 Markdown,以及其实现逻辑(html2md.js 或 html2markdown.js)

2023-03-21 17:26:32 浏览数 (1)

之前因为一些需要,需要转换部分 HTML 标签成 markdown 格式,但是不知不觉就完善到一个相对完整的函数。

然后我就封装成了一个文件放在了 github ,也简单做了两个示例网页。

  1. HTML 转换 -- https://kohunglee.github.io/html2md/example/conversion.html
  2. 直接就粘贴成 markdown 格式 -- https://kohunglee.github.io/html2md/example/Paste_and_convert.html

代码地址在 html2md

其实这类函数在 github 上有很多,但是或多或少都对 HTML 的还原支持的不够完善,比如 turndown.js 是最热门的,但却不支持表格的恢复,索性就自己做了一个。

其实之间的转换还挺复杂,需要考虑各个标签的优先级,做完又花了两天才完善到一定程度。

(不过需要提醒的是,Safari 和 iOS 上的浏览器不支持这个,因为它们对正则支持的不够完整。不过对于前者,可以使用Chrome,对于后者,又压根无法复制出已封装了 HTML 的内容,所以也不需要考虑。)

代码的实现逻辑如下:

其中,最开始声明了一些数组变量,用于将一些转换过程中的中间产物进行储存。

然后 pureHtml 这个变量就是整个加工过程中的原料,一直到最后。

首先,函数处理的入口是从 112 行 开始的。

第一步,删除 <style><script> 这两个标签及其内容。

第二步,将 pre 里的内容先存到数组里,然后用 ‘#preContent#’ 这个字符替换原来 pre 标签里的内容,我称这个操作为保护。因为后续会有很多复杂的内容,把 pre 保护了,就能保证它的原汁原味,因为 pre 本身就是代码,不能动。

第三步,和 pre 一样的 code ,为什么先 pre 再 code 呢?因为这两样东西有这样的包含关系,一般 pre 里可以有 code ,但 code 却没有 pre ,所以在考虑这样的逻辑后,决定这样储存。

第四步,就是在没有 pre 和 code 的干扰下,放心删除标签中其他没有用的属性,并将 a 和 img 的标签内容进行 “保护” ,以方便一会儿恢复。

第五步,就是替换一些简单的标签,什么标题啊,斜体啊,横线啊等等(还有将一些乱七八糟的标签直接删除).....最后依次处理表格和列表。

第六步,按照一定的规范,依次将上面 “保护” 的内容,进行恢复。

第七步,将最头部的空行删去。(我记得中间也曾检查多余的空行删去,不知道为什么没有了),然后转换完毕,将结果返回。

源码如下:

代码语言:javascript复制
/**
 * 把 html 内容转化为 markdown 格式 V1.0
 * 
 * @author kohunglee
 * @param {string} htmlData 转换前的 html 
 * @return {string} 转化后的 markdown 源码
 */
function html2md(htmlData){
    codeContent     = new Array  // code标签数据
    preContent      = new Array  // pre标签数据
    tableContent    = new Array  // table标签数据
    olContent       = new Array  // ol标签数据
    imgContent      = new Array  // img标签数据
    aContent        = new Array  // a标签数据
    let pureHtml    = htmlData

    // 源代码
    console.log("转换前的源码:"   pureHtml)

    // 函数:删去html标签
    function clearHtmlTag(sourceData = ''){  
        return sourceData.replace(/<[sS]*?>/g,'')
    }

    // 复原ol标签
    function olRecover(olData = ''){  
        let result = olData
        let num = olData.match(/<li>/ig).length
        for(let i = 1; i <= num; i  ){
            let line = '[~wrap]'
            if(i == 1) line = '[~wrap][~wrap]'
            result = result.replace(/<li>/i, line   i   '. ')
        }
        result = result.replace(/</li>/, '')
        return result
    }

    // 函数:复原img标签
    function imgRecover(imgHtml = ''){  
        let imgSrc,imgTit,imgAlt,result
        imgSrc     = imgHtml.match(/(?<=src=['"])[sS]*?(?=['"])/i)
        imgTit     = imgHtml.match(/(?<=title=['"])[sS]*?(?=['"])/i)
        imgAlt     = imgHtml.match(/(?<=alt=['"])[sS]*?(?=['"])/i)

        imgTit = (imgTit != null) ? ` "${imgTit}"` : ' '
        imgAlt = (imgAlt != 'null') ? imgAlt : " "
        result = `![${imgAlt}](${imgSrc}${imgTit})`
        return result
    }

    // 函数:复原a标签
    function aRecover(aData = ''){  
        let aHref = ''   aData.match(/(?<=href=['"])[sS]*?(?=['"])/i)
        let aTit  = ''   aData.match(/(?<=title=['"])[sS]*?(?=['"])/i)
        let aText = ''   aData.match(/(?<=<as*[^>]*?>)[sS]*?(?=</a>)/i)

        let aImg = aData.match(/<imgs*[^>]*?>[^]*?(</img>)?/i)
        let aImgSrc,aImgTit,aImgAlt

        aTit = (aTit != 'null') ? ` "${aTit}"` : ' '
        aText = clearHtmlTag(aText)
        let result = `[${aText}](${aHref}${aTit})`

        if(aImg != null){  // 函数:如果发现图片,则更换为图片显示模式
            aImgSrc     = aImg[0].match(/(?<=src=['"])[sS]*?(?=['"])/i)
            aImgTit     = aImg[0].match(/(?<=title=['"])[sS]*?(?=['"])/i)
            aImgAlt     = aImg[0].match(/(?<=alt=['"])[sS]*?(?=['"])/i)

            aImgTit = (aImgTit != null) ? ` "${aImgTit}"` : ' '
            aImgAlt = (aImgAlt != 'null') ? aImgAlt : " "
            result = `[![${aImgAlt}](${aImgSrc}${aImgTit})](${aHref}${aTit})`
        }
        return result
    }

    // 函数:复原table标签
    function tableRecover(tableData = null){  
        if(tableData[0] == null){  // 如果不存在 th 标签,则默认表格为一层
            let result = ''
            let colNum = tableData[1].length

            for(let i = 0; i < colNum; i  ){
            result  = `|${clearHtmlTag(tableData[1][i])}`
            }
            result  = `|[~wrap]`
            for(let j = 0; j < colNum; j  ){
                result  = `| :------------: `
            }
            result  = `|[~wrap]`
            return result
        }
        let colNum = tableData[0].length  // 如果存在 th 标签,则按 th 的格数来构建整个表格
        let result = ''

        for(let i = 0; i < colNum; i  ){
            result  = `|${clearHtmlTag(tableData[0][i])}`
        }
        result  = `|[~wrap]`
        for(let j = 0; j < colNum; j  ){
            result  = `| :------------: `
        }
        result  = `|[~wrap]`
        for(let k = 0; k < tableData[1].length;){
            for(let z = 0; z < colNum; z  ,k  ){
                result  = `|${clearHtmlTag(tableData[1][k])}`
            }
            result  = `|[~wrap]`
        }
        return result `[~wrap]`
    }
    // 去掉样式和脚本极其内容
    pureHtml = pureHtml.replace(/<styles*[^>]*?>[^]*?</style>/ig,'').replace(/<scripts*[^>]*?>[^]*?</script>/ig,'')

    // 储存pre的内容,并替换<pre>中的内容
    preContent = pureHtml.match(/<pres*[^>]*?>[^]*?</pre>/ig)
    pureHtml = pureHtml.replace(/(?<=<pres*[^>]*?>)[sS]*?(?=</pre>)/ig,'`#preContent#`')

    // 储存code的内容,并替换<code>中的内容
    codeContent = pureHtml.match(/(?<=<codes*[^>]*?>)[sS]*?(?=</code>)/ig)
    pureHtml = pureHtml.replace(/(?<=<codes*[^>]*?>)[sS]*?(?=</code>)/ig,'`#codeContent#`')

    // 储存a的内容,并替换<a>中的内容
    aContent = pureHtml.match(/<as*[^>]*?>[^]*?</a>/ig)
    pureHtml = pureHtml.replace(/<as*[^>]*?>[^]*?</a>/ig,'`#aContent#`')

    // 储存img的内容,并替换<img>中的内容
    imgContent = pureHtml.match(/<imgs*[^>]*?>[^]*?(</img>)?/ig)
    pureHtml = pureHtml.replace(/<imgs*[^>]*?>[^]*?(</img>)?/ig,'`#imgContent#`')

    // 获取纯净(无属性)的 html
    pureHtml = pureHtml.replace(/(?<=<[a-zA-Z0-9]*)s.*?(?=>)/g,'')  

    // 标题:标获取<h1><h2>...数据,并替换
    pureHtml = pureHtml.replace(/<h1>/ig,'[~wrap]# ').replace(/</h1>/ig,'[~wrap][~wrap]')
                        .replace(/<h2>/ig,'[~wrap]## ').replace(/</h2>/ig,'[~wrap][~wrap]')
                        .replace(/<h3>/ig,'[~wrap]### ').replace(/</h3>/ig,'[~wrap][~wrap]')
                        .replace(/<h4>/ig,'[~wrap]#### ').replace(/</h4>/ig,'[~wrap][~wrap]')
                        .replace(/<h5>/ig,'[~wrap]##### ').replace(/</h5>/ig,'[~wrap][~wrap]')
                        .replace(/<h6>/ig,'[~wrap]###### ').replace(/</h6>/ig,'[~wrap][~wrap]')

    // 段落:处理一些常用的结构标签
    pureHtml = pureHtml.replace(/(<br>)/ig,'[~wrap]').replace(/(</p>)|(<br/>)|(</div>)/ig,'[~wrap][~wrap]')
                       .replace(/(<meta>)|(<span>)|(<p>)|(<div>)/ig,'').replace(/</span>/ig,'')

    // 粗体:替换<b><strong>
    pureHtml = pureHtml.replace(/(<b>)|(<strong>)/ig,'**').replace(/(</b>)|(</strong>)/ig,'**')

    // 斜体:替换<i><em><abbr><dfn><cite><address>
    pureHtml = pureHtml.replace(/(<i>)|(<em>)|(<abbr>)|(<dfn>)|(<cite>)|(<address>)/ig,'*').replace(/(</i>)|(</em>)|(</abbr>)|(</dfn>)|(</cite>)|(</address>)/ig,'*')

    // 删除线:替换<del>
    pureHtml = pureHtml.replace(/<del>/ig,'~~').replace(/</del>/ig,'~~')

    // 引用:替换<blockquote>
    pureHtml = pureHtml.replace(/<blockquote>/ig,'[~wrap][~wrap]> ').replace(/</blockquote>/ig,'[~wrap][~wrap]')

    // 水平线:替换<hr>
    pureHtml = pureHtml.replace(/<hr>/ig,'[~wrap][~wrap]------[~wrap][~wrap]')

    // 表格 <table>,得到数据,删除标签,然后逐层分析储存,最终根据结果生成
    tableContent = pureHtml.match(/(?<=<tables*[^>]*?>)[sS]*?(?=</table>)/ig)
    pureHtml = pureHtml.replace(/<tables*[^>]*?>[^]*?</table>/ig,'`#tableContent#`')
    if(tableContent !== null){  // 分析储存
        tbodyContent = new Array
        for(let i = 0; i < tableContent.length; i  ){
            tbodyContent[i] = new Array  // tbodyContent[i]的第一个数据是thead数据,第二个是tbody的数据
            tbodyContent[i].push(tableContent[i].match(/(?<=<th>)[sS]*?(?=</th?>)/ig))
            tbodyContent[i].push(tableContent[i].match(/(?<=<td>)[sS]*?(?=</td?>)/ig))
        }
    }
    if(typeof tbodyContent !== "undefined"){  // 替换
        for(let i = 0; i < tbodyContent.length; i  ){
            let tableText = tableRecover(tbodyContent[i])
            pureHtml = pureHtml.replace(/`#tableContent#`/i,tableText)
        }
    }

    // 有序列表<ol>的<li>,储存ol的内容,并循环恢复ol中的内容
    olContent = pureHtml.match(/(?<=<ols*[^>]*?>)[sS]*?(?=</ol>)/ig)
    pureHtml = pureHtml.replace(/(?<=<ols*[^>]*?>)[sS]*?(?=</ol>)/ig,'`#olContent#`')
    if(olContent !== null){
        for(let k = 0; k < olContent.length; k  ){
            let olText = olRecover(olContent[k])
            pureHtml = pureHtml.replace(/`#olContent#`/i,clearHtmlTag(olText))
        }
    }

    // 无序列表<ul>的<li>,以及<dd>,直接替换
    pureHtml = pureHtml.replace(/(<li>)|(<dd>)/ig,'[~wrap] - ').replace(/(</li>)|(</dd>)/ig,'[~wrap][~wrap]')

    // 处理完列表后,将 <lu>、<lu>、<ol>、<ol> 处理
    pureHtml = pureHtml.replace(/(<ul>)|(<ol>)/ig,'').replace(/(</ul>)|(</ol>)/ig,'[~wrap][~wrap]')

    // 先恢复 img ,再恢复 a
    if(imgContent !== null){
        for(let i = 0; i < imgContent.length; i  ){
            let imgText = imgRecover(imgContent[i])
            pureHtml = pureHtml.replace(/`#imgContent#`/i,imgText)
        }
    }

    // 恢复 a
    if(aContent !== null){
        for(let k = 0; k < aContent.length; k  ){
            let aText = aRecover(aContent[k])
            pureHtml = pureHtml.replace(/`#aContent#`/i,aText)
        }
    }

    // 换行处理,1.替换 [~wrap] 为 ‘n’   2.首行换行删去。   3.将其他过长的换行删去。
    pureHtml = pureHtml.replace(/[~wrap]/ig,'n')
                       .replace(/n{3,}/g,'nn')

    // 代码 <code> ,根据上面的数组恢复code,然后将code替换
    if(codeContent !== null){
        for(let i = 0; i < codeContent.length; i  ){
            pureHtml = pureHtml.replace(/`#codeContent#`/i,clearHtmlTag(codeContent[i]))
        }
    }
    pureHtml = pureHtml.replace(/<code>/ig,' ` ').replace(/</code>/ig,' ` ')

    // 代码 <pre> ,恢复pre,然后将pre替换
    if(preContent !== null){
        for(let k = 0; k < preContent.length; k  ){
            let preLanguage = preContent[k].match(/(?<=language-).*?(?=[s'"])/i)
            let preText = clearHtmlTag(preContent[k])
            preText = preText.replace(/^1n2n(d n)*/,'')  // 去掉行数

            preLanguage = (preLanguage != null && preLanguage[0] != 'undefined') ? preLanguage[0]   'n' : 'n'
            pureHtml = pureHtml.replace(/`#preContent#`/i,preLanguage   preText)
        }
    }
    pureHtml = pureHtml.replace(/<pre>/ig,'```').replace(/</pre>/ig,'n```n')

    // 删去其余的html标签,还原预文本代码中的 '<' 和 '>'
    pureHtml = clearHtmlTag(pureHtml)
    pureHtml = pureHtml.replace(/&lt;/ig,'<').replace(/&gt;/ig,'>')

    // 删去头部的空行
    pureHtml = pureHtml.replace(/^n{1,}/i,'')

    return pureHtml
}

0 人点赞