import fs from 'fs'
import os from 'os'
import path from 'path'
import get from 'lodash-es/get.js'
import isestr from 'wsemi/src/isestr.mjs'
import isbol from 'wsemi/src/isbol.mjs'
import isobj from 'wsemi/src/isobj.mjs'
import isarr from 'wsemi/src/isarr.mjs'
import fsIsFile from 'wsemi/src/fsIsFile.mjs'
import WMd2html from 'w-md2html/src/WMd2html.mjs'
import cvMdToDocx from './cvMdToDocx.mjs'
import { mimeHtml, mimeDocx, toErrText, retryBusy, toSafeName, writeAssets, transErrAsset } from './utils.mjs'
/**
* Markdown內容轉Html與Docx內容
*
* 與cvMdToDocx之差異:本函數接收Markdown「內容」與其引用之資產(圖片等),於獨立工作資料夾內還原後轉檔,完成後回傳產物之base64內容並清除工作資料夾。適用於服務端等無實體來源檔之情境;有實體md檔者請直接用cvMdToDocx。
*
* @param {Object} [opt={}] 輸入設定物件,預設{}
* @param {String} [opt.md=''] 輸入Markdown內容字串,與mdBase64二擇一
* @param {String} [opt.mdBase64=''] 輸入Markdown內容之base64字串,與md二擇一
* @param {String} [opt.name='output'] 輸入輸出檔名主體字串(不含副檔名),將自動移除路徑成分與非法字元,預設'output'
* @param {String} [opt.out='docx'] 輸入產出類型字串,可為'html'、'docx'、'both',預設'docx'
* @param {Array} [opt.assets=[]] 輸入md內引用之相對路徑資產陣列,格式[{path,base64}],預設[]
* @param {String} [opt.dirWork=''] 輸入工作資料夾根位置字串,未給則用系統暫存夾下之w-md2docx,預設''
* @param {String} [opt.fpInTemp=''] 輸入Docx模板檔位置字串,未給則由w-html2docx使用其內建模板,預設''
* @param {Object} [opt.optMd2html={}] 輸入傳予w-md2html之設定物件,預設{}
* @param {Object} [opt.optHtml2docx={}] 輸入傳予w-html2docx之設定物件,其imgHeightReserveLines未給時由cvMdToDocx預設4(行格線模板之圖片下方預留格數,給null停用),預設{}
* @param {Boolean} [opt.keepWork=false] 輸入是否保留工作資料夾供除錯布林值,預設false
* @param {Boolean|Object} [opt.keepCaption=false] 輸入是否使圖名、表名與其圖、表同頁(true或{labels}),傳予cvMdToDocx,僅於產出docx時有效,預設false
* @param {Boolean|Object} [opt.toc=false] 輸入是否添加章節目錄、圖目錄、表目錄並重編頁碼(true或addDocxToc之設定物件),傳予cvMdToDocx,僅於產出docx時有效,預設false
* @returns {Promise} 回傳Promise,resolve回傳結果物件{name,out,ms,nAssets,[msDocx],[html],[docx],[keepCaption],[toc]},其中html與docx為{fileName,mime,size,base64},keepCaption與toc為cvMdToDocx之同名結果(有要求且產出docx時才有),reject回傳錯誤訊息
* @example
*
* import cvMdTo from 'w-md2docx/src/cvMdTo.mjs'
*
* let r = await cvMdTo({
* md: '# 標題\n\n<img src="pics/圖.png" />',
* name: '報告R00.01',
* out: 'docx',
* assets: [{ path: 'pics/圖.png', base64: '…' }],
* })
* console.log(r)
* // => { name: '報告R00.01', out: 'docx', ms: 8342, nAssets: 1, msDocx: 8300, docx: { fileName: '報告R00.01.docx', mime: '…', size: 39856, base64: '…' } }
*
*/
async function cvMdTo(opt = {}) {
let msStart = Date.now()
//out
let out = get(opt, 'out', '')
if (out !== 'html' && out !== 'docx' && out !== 'both') {
out = 'docx'
}
let needHtml = (out === 'html' || out === 'both')
let needDocx = (out === 'docx' || out === 'both')
//md
let md = ''
let mdIn = get(opt, 'md', '')
let mdBase64 = get(opt, 'mdBase64', '')
if (isestr(mdIn)) {
md = mdIn
}
else if (isestr(mdBase64)) {
md = Buffer.from(mdBase64, 'base64').toString('utf8')
}
if (md.trim() === '') {
return Promise.reject('md is empty: md (string) or mdBase64 is required')
}
//name
let name = toSafeName(get(opt, 'name', ''))
//assets
let assets = get(opt, 'assets', [])
if (!isarr(assets)) {
assets = []
}
//dirWork (未指定則用系統暫存夾)
let dirWork = get(opt, 'dirWork', '')
if (!isestr(dirWork)) {
dirWork = path.join(os.tmpdir(), 'w-md2docx')
}
dirWork = path.resolve(dirWork)
//fpInTemp (存在性由 cvMdToDocx 檢查)
let fpInTemp = get(opt, 'fpInTemp', '')
if (!isestr(fpInTemp)) {
fpInTemp = ''
}
//optMd2html
let optMd2html = get(opt, 'optMd2html', {})
if (!isobj(optMd2html)) {
optMd2html = {}
}
//optHtml2docx
let optHtml2docx = get(opt, 'optHtml2docx', {})
if (!isobj(optHtml2docx)) {
optHtml2docx = {}
}
//keepWork (除錯用, 保留工作資料夾)
let keepWork = get(opt, 'keepWork', false)
if (!isbol(keepWork)) {
keepWork = false
}
//dirJob (每次作業獨立資料夾, 使同名檔案與資產互不干擾)
let idJob = `${Date.now()}_${Math.random().toString(36).slice(2, 8)}`
let dirJob = path.resolve(dirWork, `job_${idJob}`)
fs.mkdirSync(dirJob, { recursive: true })
let rt = {
name,
out,
ms: 0,
nAssets: 0,
}
try {
//寫入 md (資產以 md 所在資料夾為基準解析, 故兩者同置於 dirJob)
let fpMd = path.resolve(dirJob, `${name}.md`)
fs.writeFileSync(fpMd, md, 'utf8')
//寫入資產
try {
rt.nAssets = writeAssets(dirJob, assets)
}
catch (err) {
return Promise.reject(toErrText(err))
}
let fpHtml = path.resolve(dirJob, `${name}.html`)
if (needDocx) {
//md -> html -> docx (中介 html 指定產於作業夾, 供 out 含 html 時一併回傳)
let fpDocx = path.resolve(dirJob, `${name}.docx`)
let errDocx = null
let r = await cvMdToDocx(fpMd, fpDocx, {
fpInTemp,
fpOutHtml: fpHtml,
optMd2html,
optHtml2docx,
keepCaption: get(opt, 'keepCaption', false),
toc: get(opt, 'toc', false),
})
.catch((err) => {
errDocx = transErrAsset(toErrText(err))
})
if (errDocx !== null) {
return Promise.reject(errDocx)
}
let buf = fs.readFileSync(fpDocx)
rt.docx = {
fileName: `${name}.docx`,
mime: mimeDocx,
size: buf.length,
base64: buf.toString('base64'),
}
rt.msDocx = get(r, 'ms', 0)
//圖名表名同頁與目錄之結果(與 cvMdToDocx 一致帶出, 供服務端回應與用戶端得知是否已處理或略過)
if (isobj(get(r, 'keepCaption'))) {
rt.keepCaption = r.keepCaption
}
if (isobj(get(r, 'toc'))) {
rt.toc = r.toc
}
}
else {
//僅需 html, 不啟動 Word
let errHtml = null
await retryBusy(() => WMd2html(fpMd, fpHtml, optMd2html))
.catch((err) => {
errHtml = toErrText(err)
})
if (errHtml !== null) {
return Promise.reject(transErrAsset(`Failed to convert md to html: ${errHtml}`))
}
if (!fsIsFile(fpHtml) || fs.statSync(fpHtml).size === 0) {
return Promise.reject('Failed to convert md to html: html was not generated or is empty')
}
}
if (needHtml) {
let buf = fs.readFileSync(fpHtml)
rt.html = {
fileName: `${name}.html`,
mime: mimeHtml,
size: buf.length,
base64: buf.toString('base64'),
}
}
rt.ms = Date.now() - msStart
return rt
}
finally {
//清除作業資料夾(除錯模式保留)
if (!keepWork) {
try {
fs.rmSync(dirJob, { recursive: true, force: true })
}
catch (err) {
//殘檔被鎖, 留待下次清理
}
}
}
}
export default cvMdTo