原生 JS 实现 HTML 转 Markdown,以及其实现逻辑
之前因为一些需要,需要转换部分 HTML 标签成 markdown 格式,但是不知不觉就完善到一个相对完整的函数。
然后我就封装成了一个文件放在了 github ,也简单做了两个示例网页。
- HTML 转换 -- https://kohunglee.github.io/html2md/example/conversion.html
- 直接就粘贴成 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 的标签内容进行 “保护” ,以方便一会儿恢复。
第五步,就是替换一些简单的标签,什么标题啊,斜体啊,横线啊等等(还有将一些乱七八糟的标签直接删除).....最后依次处理表格和列表。
第六步,按照一定的规范,依次将上面 “保护” 的内容,进行恢复。
第七步,将最头部的空行删去。(我记得中间也曾检查多余的空行删去,不知道为什么没有了),然后转换完毕,将结果返回。
源码如下:
/**
* 把 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(/\<[\s\S]*?\>/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=['"])[\s\S]*?(?=['"])/i)
imgTit = imgHtml.match(/(?<=title=['"])[\s\S]*?(?=['"])/i)
imgAlt = imgHtml.match(/(?<=alt=['"])[\s\S]*?(?=['"])/i)
imgTit = (imgTit != null) ? ` "${imgTit}"` : ' '
imgAlt = (imgAlt != 'null') ? imgAlt : " "
result = `![${imgAlt}](${imgSrc}${imgTit})`
return result
}
// 函数:复原a标签
function aRecover(aData = ''){
let aHref = '' + aData.match(/(?<=href=['"])[\s\S]*?(?=['"])/i)
let aTit = '' + aData.match(/(?<=title=['"])[\s\S]*?(?=['"])/i)
let aText = '' + aData.match(/(?<=\<a\s*[^\>]*?\>)[\s\S]*?(?=<\/a>)/i)
let aImg = aData.match(/<img\s*[^\>]*?\>[^]*?(<\/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=['"])[\s\S]*?(?=['"])/i)
aImgTit = aImg[0].match(/(?<=title=['"])[\s\S]*?(?=['"])/i)
aImgAlt = aImg[0].match(/(?<=alt=['"])[\s\S]*?(?=['"])/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(/<style\s*[^\>]*?\>[^]*?<\/style>/ig,'').replace(/<script\s*[^\>]*?\>[^]*?<\/script>/ig,'')
// 储存pre的内容,并替换<pre>中的内容
preContent = pureHtml.match(/<pre\s*[^\>]*?\>[^]*?<\/pre>/ig)
pureHtml = pureHtml.replace(/(?<=\<pre\s*[^\>]*?\>)[\s\S]*?(?=<\/pre>)/ig,'`#preContent#`')
// 储存code的内容,并替换<code>中的内容
codeContent = pureHtml.match(/(?<=\<code\s*[^\>]*?\>)[\s\S]*?(?=<\/code>)/ig)
pureHtml = pureHtml.replace(/(?<=\<code\s*[^\>]*?\>)[\s\S]*?(?=<\/code>)/ig,'`#codeContent#`')
// 储存a的内容,并替换<a>中的内容
aContent = pureHtml.match(/<a\s*[^\>]*?\>[^]*?<\/a>/ig)
pureHtml = pureHtml.replace(/<a\s*[^\>]*?\>[^]*?<\/a>/ig,'`#aContent#`')
// 储存img的内容,并替换<img>中的内容
imgContent = pureHtml.match(/<img\s*[^\>]*?\>[^]*?(<\/img>)?/ig)
pureHtml = pureHtml.replace(/<img\s*[^\>]*?\>[^]*?(<\/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(/(?<=\<table\s*[^\>]*?\>)[\s\S]*?(?=<\/table>)/ig)
pureHtml = pureHtml.replace(/<table\s*[^\>]*?\>[^]*?<\/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>)[\s\S]*?(?=<\/th?>)/ig))
tbodyContent[i].push(tableContent[i].match(/(?<=\<td>)[\s\S]*?(?=<\/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(/(?<=\<ol\s*[^\>]*?\>)[\s\S]*?(?=<\/ol>)/ig)
pureHtml = pureHtml.replace(/(?<=\<ol\s*[^\>]*?\>)[\s\S]*?(?=<\/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,'\n\n')
// 代码 <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(/^1\n2\n(\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(/\<\;/ig,'<').replace(/\>\;/ig,'>')
// 删去头部的空行
pureHtml = pureHtml.replace(/^\n{1,}/i,'')
return pureHtml
}
原生 JS 实现 HTML 转 Markdown,以及其实现逻辑的更多相关文章
- 原生js实现多组图片切换
这几天一直在练习原生js写效果,需要理清自己的逻辑,做了一个切换多组图片的效果: css样式: * { margin: 0; padding: 0; } body { background: #303 ...
- 用原生js写一个"多动症"的简历
用原生js写一个"多动症"的简历 预览地址源码地址 最近在知乎上看到@方应杭用vue写了一个会动的简历,觉得挺好玩的,研究一下其实现思路,决定试试用原生js来实现. 会动的简历实现 ...
- 原生JS封装Ajax插件(同域&&jsonp跨域)
抛出一个问题,其实所谓的熟悉原生JS,怎样的程度才是熟悉呢? 最近都在做原生JS熟悉的练习... 用原生Js封装了一个Ajax插件,引入一般的项目,传传数据,感觉还是可行的...简单说说思路,如有不正 ...
- 常用原生JS方法总结(兼容性写法)
经常会用到原生JS来写前端...但是原生JS的一些方法在适应各个浏览器的时候写法有的也不怎么一样的... 今天下班有点累... 就来总结一下简单的东西吧…… 备注:一下的方法都是包裹在一个EventU ...
- 原生JS实现"旋转木马"效果的图片轮播插件
一.写在最前面 最近都忙一些杂七杂八的事情,复习软考.研读经典...好像都好久没写过博客了... 我自己写过三个图片轮播,一个是简单的原生JS实现的,没有什么动画效果的,一个是结合JQuery实现的, ...
- 再谈React.js实现原生js拖拽效果
前几天写的那个拖拽,自己留下的疑问...这次在热心博友的提示下又修正了一些小小的bug,也加了拖拽的边缘检测部分...就再聊聊拖拽吧 一.不要直接操作dom元素 react中使用了虚拟dom的概念,目 ...
- React.js实现原生js拖拽效果及思考
一.起因&思路 不知不觉,已经好几天没写博客了...近来除了研究React,还做了公司官网... 一直想写一个原生js拖拽效果,又加上近来学react学得比较嗨.所以就用react来实现这个拖 ...
- 原生JS实现全屏切换以及导航栏滑动隐藏及显示——重构前
思路分析: 向后滚动鼠标滚轮,页面向下全屏切换:向前滚动滚轮,页面向上全屏切换.切换过程为动画效果. 第一屏时,导航栏固定在页面顶部,切换到第二屏时,导航条向左滑动隐藏.切换回第一屏时,导航栏向右滑动 ...
- 原生js实现autocomplete插件
在实际的项目中,能用别人写好的插件实现相关功能是最好不过,为了节约时间成本,因为有的项目比较紧急,没充分时间让你自己来写,即便写了,你还要花大量时间调试兼容性.但是出于学习的目的,你可以利用闲暇时间, ...
随机推荐
- webpack4 mini-css-extract-plugin
在使用webpack的extract-text-webpack-plugin插件提取单独打包css文件时,报错,说是这个插件要依赖webpack3的版本. webpack4得使用mini-css-ex ...
- SQL——with as 临时表
一.WITH AS的含义 WITH AS短语,也叫做子查询部分(subquery factoring),可以让你做很多事情,定义一个SQL片断,该SQL片断会被整个SQL语句所用到.有的时候,是 ...
- Docker仓库--registry与harbor
搭建私有仓库的两种方式: Registry Harbor 一.registry的搭建 docker官方提供了一个搭建私有仓库的镜像registry,只需下载镜像,运行容器并暴露5000端口即可. ...
- windev中使用DateFile和Query作为表格数据源的一个重要区别
表格可以使用DateFile(数据表)和Query(查询表)作为数据源,在列表页面中,因为表格一般都设置为不可编辑,所以这两者在表现使用上,区别都不大.尽管如此,我们还是需要清晰的知道,这两者本质上的 ...
- [gRPC via C#] gRPC本质的探究与实践
鉴于内容过多,先上太长不看版: grpc 就是请求流&响应流特殊一点的 Http 请求,性能和 WebAPI 比起来只快在 Protobuf 上: 附上完整试验代码:GrpcWithOutSD ...
- 项目报错:/uploads: Read-only file system(解决办法)
项目报错:/uploads: Read-only file system(解决办法) 本来以为是service层没加注解,翻到最后才发现问题 原因是项目根目录没有对应的文件夹,在项目根目录创建uplo ...
- 开源绘画应用 Pinta 已移植到GTK 3和.NET 6
Pinta 是一款开源绘画应用,适用于 Linux.Windows 和 macOS.你可以用它来进行自由手绘/素描.你也可以用它在现有的图片上添加箭头.方框.文字等. 年初发布了 Pinta 2.0. ...
- Mybatis学习笔记(详细)
介绍 三层架构:视图层.业务逻辑层.持久层 mybatis框架: 执行数据库操作,完成对数据库的增删改查,封装了jdbc mapper映射,将表中数据转为一个Java对象,一个表对应一个接口 Myba ...
- AHUACM寒假集训I(基础数据结构+串串)
H.超级钢琴 luoguP2048 题目大意: 求出一个长N序列中所有长度在L到R的子序列中序列和最大的K个,并求这K个的和 思路: 暴力的话可以求出所有满足要求的子序列然后排序,然后显然会T. 所以 ...
- Android studio常用快捷键导包的设置
下面是一些快捷键的使用还有快速导包的设置 1. Ctrl+G 同时按下Ctrl+G快捷键弹出快速定位框,在框中输入行数点击OK即可快速切换到对应的行数,如图2.17所示. 2. Ctrl+E 同时按下 ...