import path from 'path'
import fs from 'fs'
import get from 'lodash-es/get.js'
import each from 'lodash-es/each.js'
import split from 'lodash-es/split.js'
import trim from 'lodash-es/trim.js'
import join from 'lodash-es/join.js'
import drop from 'lodash-es/drop.js'
import size from 'lodash-es/size.js'
import isbol from 'wsemi/src/isbol.mjs'
import isestr from 'wsemi/src/isestr.mjs'
import isfun from 'wsemi/src/isfun.mjs'
import ispm from 'wsemi/src/ispm.mjs'
import replace from 'wsemi/src/replace.mjs'
import html2str from 'wsemi/src/html2str.mjs'
import pmSeries from 'wsemi/src/pmSeries.mjs'
import getFileTrueName from 'wsemi/src/getFileTrueName.mjs'
import getPathParent from 'wsemi/src/getPathParent.mjs'
import fsIsFile from 'wsemi/src/fsIsFile.mjs'
import wi from 'w-image-proc/src/WImageProc.mjs'
// import htmlRemovePInLi from './htmlRemovePInLi.mjs'
import md2html from './md2html.mjs'
import htmlClean from './htmlClean.mjs'
/**
* Markdown檔轉Html檔
*
* @param {String} fpIn 輸入來源Markdown檔位置字串
* @param {String} fpOut 輸入轉出Html檔位置字串
* @param {Object} [opt={}] 輸入設定物件,預設{}
* @param {String} [opt.tableBorderColor='#666'] 輸入表格邊框顏色 (CSS color)
* @param {string[]} [opt.fontFamilies=['Microsoft JhengHei','Avenir','Helvetica','Arial','sans-serif']] 輸入內文字型優先順序列表
* @param {String} [opt.fontSizeUnit='pt'] 輸入字型大小單位字串,可使用例如'pt'、'px'等,預設'pt'
* @param {Number} [opt.fontSizeScale=1] 輸入字型大小縮放倍率數字,預設1
* @param {Number|String} [opt.fontSizeDef] 輸入內文字型大小數字或字串,預設為12,自動轉字型為12*fontSizeScale+fontSizeUnit
* @param {Number|String} [opt.fontSizeH1] 輸入h1字型大小數字或字串,預設為20,自動轉字型為20*fontSizeScale+fontSizeUnit
* @param {Number|String} [opt.fontSizeH2] 輸入h2字型大小數字或字串,預設為16,自動轉字型為16*fontSizeScale+fontSizeUnit
* @param {Number|String} [opt.fontSizeH3] 輸入h3字型大小數字或字串,預設為14,自動轉字型為14*fontSizeScale+fontSizeUnit
* @param {Number|String} [opt.fontSizeH4] 輸入h4字型大小數字或字串,預設為12,自動轉字型為12*fontSizeScale+fontSizeUnit
* @param {Number|String} [opt.fontSizeH5] 輸入h5字型大小數字或字串,預設為12,自動轉字型為12*fontSizeScale+fontSizeUnit
* @param {Number|String} [opt.fontSizeH6] 輸入h6字型大小數字或字串,預設為12,自動轉字型為12*fontSizeScale+fontSizeUnit
* @param {Number|String} [opt.fontSizeP] 輸入p字型大小數字或字串,預設為12,自動轉字型為12*fontSizeScale+fontSizeUnit
* @param {Number|String} [opt.fontSizeTab] 輸入表格文字字型大小數字或字串,預設為11,自動轉字型為11*fontSizeScale+fontSizeUnit
* @param {Number|String} [opt.fontSizeCode] 輸入程式碼區塊字型大小數字或字串,預設為10,自動轉字型為10*fontSizeScale+fontSizeUnit
* @param {String} [opt.textAlignH1='left'] 輸入h1對齊方式,可使用'left'、'center'、'right',預設'left'
* @param {String} [opt.textAlignH2='left'] 輸入h2對齊方式,可使用'left'、'center'、'right',預設'left'
* @param {String} [opt.textAlignH3='left'] 輸入h3對齊方式,可使用'left'、'center'、'right',預設'left'
* @param {String} [opt.textAlignH4='left'] 輸入h4對齊方式,可使用'left'、'center'、'right',預設'left'
* @param {String} [opt.textAlignH5='left'] 輸入h5對齊方式,可使用'left'、'center'、'right',預設'left'
* @param {String} [opt.textAlignH6='left'] 輸入h6對齊方式,可使用'left'、'center'、'right',預設'left'
* @param {Number|String} [opt.imgWidthMax=null] 輸入圖片樣式給予最大寬度,可輸入數字500單位為px,或是字串例如'100%',預設null
* @param {String} [opt.htmlTemp=null] 輸入模板字串,其內須取代{title}與{html},預設null
* @param {Boolean} [opt.imgConvertToBase64=true] 輸入圖片是否自動轉Base64布林值,預設true
* @param {Function} [opt.funProcFpOut=null] 輸入處理輸出檔名函數,函數會傳入{fpOut,pretitle},分別為原始輸出檔位置與提取Markdown內pretitle區文字,預設null
* @returns {Promise} 回傳Promise,resolve回傳成功訊息,reject回傳錯誤訊息
* @example
*
* import w from 'wsemi'
* import WMd2html from './src/WMd2html.mjs'
* //import WMd2html from 'w-md2html/src/WMd2html.mjs'
* //import WMd2html from 'w-md2html'
*
* async function test() {
*
* let fpIn = `./test/report.md`
* let fpOut = `./test/report.html`
* let opt = {
* imgWidthMax: '500px',
* funProcFpOut: (msg) => {
* console.log('msg', msg)
* return msg.fpOut
* },
* }
*
* let r = await WMd2html(fpIn, fpOut, opt)
* console.log(r)
* // => ok
*
* w.fsDeleteFile(fpOut)
*
* }
* test()
* .catch((err) => {
* console.log('catch', err)
* })
*
*/
async function WMd2html(fpIn, fpOut, opt = {}) {
//check
if (!fsIsFile(fpIn)) {
return Promise.reject(`fpIn[${fpIn}] does not exist`)
}
//轉絕對路徑
fpIn = path.resolve(fpIn)
// console.log('fpIn', fpIn)
//fdIn
let fdIn = getPathParent(fpIn)
//htmlTemp
let htmlTemp = get(opt, 'htmlTemp', null)
if (!isestr(htmlTemp)) {
htmlTemp = `
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta http-equiv="Cache-Control" content="no-siteapp"/>
<meta name="renderer" content="webkit" />
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" />
<title>{title}</title>
</head>
<body style="padding:20px; margin:0; overflow-y:auto;">
{html}
</body>
</html>
`
}
//imgConvertToBase64
let imgConvertToBase64 = get(opt, 'imgConvertToBase64', null)
if (!isbol(imgConvertToBase64)) {
imgConvertToBase64 = true
}
//funProcFpOut
let funProcFpOut = get(opt, 'funProcFpOut')
let getPretitle = (md) => {
let s = split(md, '\n')
let pretitle = ''
each(s, (v, k) => {
if (v.indexOf(' pretitle ') >= 0) {
pretitle = get(s, k + 1, '') //取有pretitle的下一列
pretitle = trim(pretitle) //因整行須trim
return false //跳出
}
})
return pretitle
}
let cvItemNum = (md, type) => {
let s = split(md, '\n') //不能用sep避免md內手動分行消失
let i = 0
let rs = []
let kp = {}
each(s, (v) => {
let vv = v
if (v.indexOf(`${type}n`) >= 0) {
i++
//圖表編號
let nn = `${type}${i}`
//更新成圖表編號
vv = replace(v, `${type}n`, nn)
//儲存圖表名對應之圖表編號
let t = v
t = html2str(t)
t = replace(t, `${type}n`, '')
t = trim(t)
t = `{${t}}`
kp[t] = nn
// console.log(t, '-->', kp[t])
}
//儲存圖表編號
rs.push(vv)
})
//取代圖表名為對應圖表編號
let h = join(rs, '\n')
each(kp, (v, k) => {
// h = replace(h, k, v)
h = h.replaceAll(k, v)
})
return h
}
let cvPicB64 = async (fd, md) => {
let gsrc = async (c) => {
//s1s
let s = split(c, 'src="')
if (size(s) !== 2) {
throw new Error(`size(s)!==2`)
}
let s0 = s[0]
let s1 = s[1]
let s1s = split(s1, '"')
//fp
let fnp = s1s[0]
let fp = ''
if (fsIsFile(fnp)) {
fp = fnp
}
else {
fp = `${fd}/${fnp}`
if (!fsIsFile(fp)) {
console.log('c', c)
console.log('fnp', fnp)
throw new Error(`fp[${fp}] does not exist`)
}
}
// console.log('fnp', fnp)
// console.log('fp', fp)
//pb64
// let pb64 = await readPicB64(fp)
let pb64 = await wi.base64(fp, 'png')
// console.log(fp, 'b64', b64)
//drop
s1s = drop(s1s)
//merge
let r = s0 + 'src="' + pb64 + '"' + join(s1s, '"')
return r
}
let tt = []
let s = split(md, '\n')
await pmSeries(s, async (v) => {
let b = v.indexOf('src="') >= 0
if (b) {
v = await gsrc(v)
// console.log(v)
}
tt.push(v)
})
tt = join(tt, '\n')
return tt
}
// //tableBorderColor
// let tableBorderColor = get(opt, 'tableBorderColor', '')
// if (!isestr(tableBorderColor)) {
// tableBorderColor = '#666'
// }
// //fontFamilies
// let fontFamilies = get(opt, 'fontFamilies', [])
// if (!isearr(fontFamilies)) {
// fontFamilies = ['Microsoft JhengHei', 'Avenir', 'Helvetica', 'Arial', 'sans-serif']
// // fontFamilies = ['Times New Roman', '標楷體'] //網頁使用, 因由左往右設定可不覆蓋無字元, 故可先設定Times New Roman再設定標楷體
// }
// fontFamilies = join(map(fontFamilies, (v) => {
// return `'${v}'`
// }), ', ')
// //fontSizeUnit
// let fontSizeUnit = get(opt, 'fontSizeUnit', '')
// if (!isestr(fontSizeUnit)) {
// fontSizeUnit = 'pt'
// }
// //fontSizeScale
// let fontSizeScale = get(opt, 'fontSizeScale', '')
// if (!isnum(fontSizeScale)) {
// fontSizeScale = 1
// }
// fontSizeScale = cdbl(fontSizeScale)
// //fontSizeDef
// let fontSizeDef = get(opt, 'fontSizeDef', '')
// if (!isestr(fontSizeDef)) {
// fontSizeDef = 12 * fontSizeScale + fontSizeUnit
// }
// //fontSizeH1
// let fontSizeH1 = get(opt, 'fontSizeH1', '')
// if (!isestr(fontSizeH1)) {
// fontSizeH1 = 20 * fontSizeScale + fontSizeUnit
// }
// //fontSizeH2
// let fontSizeH2 = get(opt, 'fontSizeH2', '')
// if (!isestr(fontSizeH2)) {
// fontSizeH2 = 16 * fontSizeScale + fontSizeUnit
// }
// //fontSizeH3
// let fontSizeH3 = get(opt, 'fontSizeH3', '')
// if (!isestr(fontSizeH3)) {
// fontSizeH3 = 14 * fontSizeScale + fontSizeUnit
// }
// //fontSizeH4
// let fontSizeH4 = get(opt, 'fontSizeH4', '')
// if (!isestr(fontSizeH4)) {
// fontSizeH4 = 12 * fontSizeScale + fontSizeUnit
// }
// //fontSizeH5
// let fontSizeH5 = get(opt, 'fontSizeH5', '')
// if (!isestr(fontSizeH5)) {
// fontSizeH5 = 12 * fontSizeScale + fontSizeUnit
// }
// //fontSizeH6
// let fontSizeH6 = get(opt, 'fontSizeH6', '')
// if (!isestr(fontSizeH6)) {
// fontSizeH6 = 12 * fontSizeScale + fontSizeUnit
// }
// //fontSizeP
// let fontSizeP = get(opt, 'fontSizeP', '')
// if (!isestr(fontSizeP)) {
// fontSizeP = 12 * fontSizeScale + fontSizeUnit
// }
// //fontSizeTab
// let fontSizeTab = get(opt, 'fontSizeTab', '')
// if (!isestr(fontSizeTab)) {
// fontSizeTab = 11 * fontSizeScale + fontSizeUnit
// }
// //fontSizeCode
// let fontSizeCode = get(opt, 'fontSizeCode', '')
// if (!isestr(fontSizeCode)) {
// fontSizeCode = 10 * fontSizeScale + fontSizeUnit
// }
// //textAlignH1
// let textAlignH1 = get(opt, 'textAlignH1', '')
// if (!isestr(textAlignH1)) {
// textAlignH1 = 'left'
// }
// //textAlignH2
// let textAlignH2 = get(opt, 'textAlignH2', '')
// if (!isestr(textAlignH2)) {
// textAlignH2 = 'left'
// }
// //textAlignH3
// let textAlignH3 = get(opt, 'textAlignH3', '')
// if (!isestr(textAlignH3)) {
// textAlignH3 = 'left'
// }
// //textAlignH4
// let textAlignH4 = get(opt, 'textAlignH4', '')
// if (!isestr(textAlignH4)) {
// textAlignH4 = 'left'
// }
// //textAlignH5
// let textAlignH5 = get(opt, 'textAlignH5', '')
// if (!isestr(textAlignH5)) {
// textAlignH5 = 'left'
// }
// //textAlignH6
// let textAlignH6 = get(opt, 'textAlignH6', '')
// if (!isestr(textAlignH6)) {
// textAlignH6 = 'left'
// }
//title
let title = getFileTrueName(fpIn)
// console.log('title', title)
//fdInMd
let fdInMd = getPathParent(fpIn)
// console.log('fdInMd', fdInMd)
//md
let md = fs.readFileSync(fpIn, 'utf8')
//getPretitle
let pretitle = getPretitle(md)
// console.log(fpIn, 'pretitle', pretitle)
//cvItemNum 圖
md = cvItemNum(md, '圖')
// console.log(md)
//cvItemNum 表
md = cvItemNum(md, '表')
// console.log(md)
//cvPicB64
md = await cvPicB64(fdInMd, md)
//walkTokens
let walkTokens = async (token) => {
if (token.type === 'image') {
if (imgConvertToBase64) {
try {
//fpHref
let fpHref = path.resolve(fdIn, token.href)
// console.log('fpHref', fpHref)
//check
if (fsIsFile(fpHref)) {
//readPicB64
// token.href = await readPicB64(fpHref)
token.href = await wi.base64(fpHref, 'png')
// console.log('hf', hf)
}
else {
console.log(`href[${token.href}] does not exist`)
}
}
catch (err) {
console.log(err)
}
}
}
}
//md2html
let rh = await md2html(md, {
...opt,
funWalkTokens: walkTokens,
mergeStyle: true,
})
//mdh
let mdh = get(rh, 'html', '')
// //check, 可能傳入markdown為空字串, 不能檢測報錯
// if (!isestr(mdh)) {
// throw new Error(`can not convert markdown`)
// }
//h
let h = htmlTemp
//replace, 模板符號
h = replace(h, '{html}', mdh)
h = replace(h, '{title}', title)
// h = replace(h, '{fontFamilies}', fontFamilies)
// h = replace(h, '{fontSizeDef}', fontSizeDef)
// h = replace(h, '{fontSizeH1}', fontSizeH1)
// h = replace(h, '{fontSizeH2}', fontSizeH2)
// h = replace(h, '{fontSizeH3}', fontSizeH3)
// h = replace(h, '{fontSizeH4}', fontSizeH4)
// h = replace(h, '{fontSizeH5}', fontSizeH5)
// h = replace(h, '{fontSizeH6}', fontSizeH6)
// h = replace(h, '{fontSizeP}', fontSizeP)
// h = replace(h, '{fontSizeTab}', fontSizeTab)
// h = replace(h, '{fontSizeCode}', fontSizeCode)
// h = replace(h, '{textAlignH1}', textAlignH1)
// h = replace(h, '{textAlignH2}', textAlignH2)
// h = replace(h, '{textAlignH3}', textAlignH3)
// h = replace(h, '{textAlignH4}', textAlignH4)
// h = replace(h, '{textAlignH5}', textAlignH5)
// h = replace(h, '{textAlignH6}', textAlignH6)
// h = replace(h, '{tableBorderColor}', tableBorderColor)
// console.log('h', h)
//隱藏markedFootnote會自動添加的h2且無法更換Footnotes, 故於style設定強制隱藏, 避免轉docx時仍會出現, 注意不能刪除否則語音閱讀器查不到關聯, 也不能用display:none會於htmlClean被清除
if (true) {
h = replace(h, '<h2 id="footnote-label" class="sr-only">Footnotes</h2>', '<h2 id="footnote-label" class="sr-only" style="height:0px; line-height:0px; min-height:0px; overflow:hidden;">Footnotes</h2>')
}
//li不能於head給予style無法轉docx, 改為個別給予style, 且margin不支援rem須給px
if (true) {
h = h.replaceAll(`<li>`, `<li style="margin:7px 0;">`)
}
//特殊標記<docx_br>, 於html內不顯示, 於docx內為換行符號, 提供docx撐開列距之用
if (true) {
h = h.replaceAll(`<docx_br>`, `
<div style="height:0; overflow:hidden;">
<br>
</div>
`)
}
// //htmlRemovePInLi
// h = htmlRemovePInLi(h)
//htmlClean
h = htmlClean(h)
//funProcFpOut
if (isfun(funProcFpOut)) {
fpOut = funProcFpOut({ fpOut, pretitle })
if (ispm(fpOut)) {
fpOut = await fpOut
}
}
// console.log('fpOut', fpOut)
//writeFileSync
fs.writeFileSync(fpOut, h, 'utf8')
// //writeFileSync
// fs.writeFileSync(fpOutMd, md, 'utf8')
return 'ok'
}
export default WMd2html