之前因為一些需要,需要轉換部分 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
}