WMd2html.mjs

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