w-md2docx 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/.editorconfig +9 -0
  2. package/.eslintignore +3 -0
  3. package/.eslintrc.js +55 -0
  4. package/.jsdoc +25 -0
  5. package/LICENSE +21 -0
  6. package/README.md +83 -0
  7. package/SECURITY.md +5 -0
  8. package/babel.config.js +16 -0
  9. package/dist/w-md2docx.umd.js +7 -0
  10. package/dist/w-md2docx.umd.js.map +1 -0
  11. package/docs/ApiClient.mjs.html +539 -0
  12. package/docs/ApiServer.mjs.html +516 -0
  13. package/docs/WMd2docx.mjs.html +101 -0
  14. package/docs/cvMdTo.mjs.html +288 -0
  15. package/docs/cvMdToDocx.mjs.html +243 -0
  16. package/docs/fonts/Montserrat/Montserrat-Bold.eot +0 -0
  17. package/docs/fonts/Montserrat/Montserrat-Bold.ttf +0 -0
  18. package/docs/fonts/Montserrat/Montserrat-Bold.woff +0 -0
  19. package/docs/fonts/Montserrat/Montserrat-Bold.woff2 +0 -0
  20. package/docs/fonts/Montserrat/Montserrat-Regular.eot +0 -0
  21. package/docs/fonts/Montserrat/Montserrat-Regular.ttf +0 -0
  22. package/docs/fonts/Montserrat/Montserrat-Regular.woff +0 -0
  23. package/docs/fonts/Montserrat/Montserrat-Regular.woff2 +0 -0
  24. package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.eot +0 -0
  25. package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.svg +978 -0
  26. package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.ttf +0 -0
  27. package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.woff +0 -0
  28. package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.woff2 +0 -0
  29. package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.eot +0 -0
  30. package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.svg +1049 -0
  31. package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.ttf +0 -0
  32. package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.woff +0 -0
  33. package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.woff2 +0 -0
  34. package/docs/global.html +5453 -0
  35. package/docs/index.html +84 -0
  36. package/docs/scripts/collapse.js +39 -0
  37. package/docs/scripts/commonNav.js +28 -0
  38. package/docs/scripts/linenumber.js +25 -0
  39. package/docs/scripts/nav.js +12 -0
  40. package/docs/scripts/polyfill.js +4 -0
  41. package/docs/scripts/prettify/Apache-License-2.0.txt +202 -0
  42. package/docs/scripts/prettify/lang-css.js +2 -0
  43. package/docs/scripts/prettify/prettify.js +28 -0
  44. package/docs/scripts/search.js +99 -0
  45. package/docs/styles/jsdoc.css +776 -0
  46. package/docs/styles/prettify.css +80 -0
  47. package/docs/utils.mjs.html +520 -0
  48. package/g.mjs +34 -0
  49. package/package.json +32 -0
  50. package/script.txt +18 -0
  51. package/src/ApiClient.mjs +467 -0
  52. package/src/ApiServer.mjs +444 -0
  53. package/src/WMd2docx.mjs +29 -0
  54. package/src/cvMdTo.mjs +216 -0
  55. package/src/cvMdToDocx.mjs +171 -0
  56. package/src/templates/temp_tpc.docx +0 -0
  57. package/src/utils.mjs +448 -0
  58. package/test/api-ApiClient.test.mjs +221 -0
  59. package/test/api-ApiServer.test.mjs +191 -0
  60. package/test/cocktail.svg +3 -0
  61. package/test/report.md +257 -0
  62. package/test/unit-cvMdTo.test.mjs +132 -0
  63. package/test/unit-cvMdToDocx.test.mjs +185 -0
  64. package/test/unit-utils.test.mjs +370 -0
  65. package/toolg/addVersion.mjs +4 -0
  66. package/toolg/cleanFolder.mjs +5 -0
  67. package/toolg/gDistRollup.mjs +56 -0
  68. package/toolg/gDocsExams.mjs +51 -0
  69. package/toolg/modifyReadme.mjs +4 -0
@@ -0,0 +1,171 @@
1
+ import fs from 'fs'
2
+ import os from 'os'
3
+ import path from 'path'
4
+ import get from 'lodash-es/get.js'
5
+ import isestr from 'wsemi/src/isestr.mjs'
6
+ import isobj from 'wsemi/src/isobj.mjs'
7
+ import fsIsFile from 'wsemi/src/fsIsFile.mjs'
8
+ import WMd2html from 'w-md2html/src/WMd2html.mjs'
9
+ import WHtml2docx from 'w-html2docx/src/WHtml2docx.mjs'
10
+ import { toErrText, retryBusy, runExclusive, getFpExe } from './utils.mjs'
11
+
12
+
13
+ /**
14
+ * Markdown檔轉Docx檔
15
+ *
16
+ * 內部流程為md -> html -> docx。docx階段係調用本機Microsoft Word(win32com),故僅能於已安裝Word之Windows執行,且同時間僅一份轉檔作業(內部佇列自動排隊)。
17
+ *
18
+ * 注意:底層w-html2docx於Word未安裝或COM呼叫失敗時「仍回傳ok」,本函數一律以產物實體檔之存在與大小驗證成敗,失敗即reject,不會回報假成功。
19
+ *
20
+ * @param {String} fpInMd 輸入來源Markdown檔位置字串
21
+ * @param {String} fpOutDocx 輸入轉出Docx檔位置字串
22
+ * @param {Object} [opt={}] 輸入設定物件,預設{}
23
+ * @param {String} [opt.fpInTemp=''] 輸入Docx模板檔位置字串,未給則由w-html2docx使用其內建模板,預設''
24
+ * @param {String} [opt.fpOutHtml=''] 輸入另存中介Html檔位置字串,未給則中介檔產於系統暫存夾並於轉檔後刪除,預設''
25
+ * @param {Object} [opt.optMd2html={}] 輸入傳予w-md2html之設定物件(如imgWidthMax、fontSizeScale等),預設{}
26
+ * @param {Object} [opt.optHtml2docx={}] 輸入傳予w-html2docx之設定物件(如imgRatioWidthMax、fontFamilies等),預設{}
27
+ * @returns {Promise} 回傳Promise,resolve回傳結果物件{fpOutDocx,sizeDocx,sizeHtml,ms,[fpOutHtml]},reject回傳錯誤訊息
28
+ * @example
29
+ *
30
+ * import cvMdToDocx from 'w-md2docx/src/cvMdToDocx.mjs'
31
+ *
32
+ * let r = await cvMdToDocx('./test/report.md', './test/report.docx', {
33
+ * fpInTemp: './src/templates/temp_tpc.docx',
34
+ * optMd2html: {
35
+ * imgWidthMax: '500px',
36
+ * },
37
+ * })
38
+ * console.log(r)
39
+ * // => { fpOutDocx: '…', sizeDocx: 39856, sizeHtml: 12345, ms: 8342 }
40
+ *
41
+ */
42
+ async function cvMdToDocx(fpInMd, fpOutDocx, opt = {}) {
43
+
44
+ let msStart = Date.now()
45
+
46
+ //check fpInMd
47
+ if (!isestr(fpInMd)) {
48
+ return Promise.reject('fpInMd must be a non-empty string')
49
+ }
50
+ fpInMd = path.resolve(fpInMd)
51
+ if (!fsIsFile(fpInMd)) {
52
+ return Promise.reject(`fpInMd[${fpInMd}] does not exist`)
53
+ }
54
+
55
+ //check fpOutDocx
56
+ if (!isestr(fpOutDocx)) {
57
+ return Promise.reject('fpOutDocx must be a non-empty string')
58
+ }
59
+ fpOutDocx = path.resolve(fpOutDocx)
60
+
61
+ //fpOutHtml (未指定則產於系統暫存夾, 轉檔後刪除)
62
+ let fpOutHtml = get(opt, 'fpOutHtml', '')
63
+ let bKeepHtml = isestr(fpOutHtml)
64
+ if (bKeepHtml) {
65
+ fpOutHtml = path.resolve(fpOutHtml)
66
+ }
67
+ else {
68
+ //檔名唯一, 避免與被鎖定之同名殘檔衝突造成 EBUSY
69
+ fpOutHtml = path.join(os.tmpdir(), `wmd2docx_${process.pid}_${Date.now()}_${Math.random().toString(36).slice(2, 8)}.html`)
70
+ }
71
+
72
+ //optMd2html
73
+ let optMd2html = get(opt, 'optMd2html', {})
74
+ if (!isobj(optMd2html)) {
75
+ optMd2html = {}
76
+ }
77
+
78
+ //optHtml2docx (複製一份, 避免改動呼叫端物件)
79
+ let optHtml2docx = get(opt, 'optHtml2docx', {})
80
+ if (!isobj(optHtml2docx)) {
81
+ optHtml2docx = {}
82
+ }
83
+ optHtml2docx = { ...optHtml2docx }
84
+
85
+ //fpInTemp (未給則交由 w-html2docx 使用內建模板)
86
+ let fpInTemp = get(opt, 'fpInTemp', '')
87
+ if (isestr(fpInTemp)) {
88
+ fpInTemp = path.resolve(fpInTemp)
89
+ if (!fsIsFile(fpInTemp)) {
90
+ return Promise.reject(`fpInTemp[${fpInTemp}] does not exist`)
91
+ }
92
+ optHtml2docx.fpInTemp = fpInTemp
93
+ }
94
+
95
+ try {
96
+
97
+ //md -> html
98
+ //note: w-md2html 係以 md 所在資料夾解析其內引用之相對路徑圖片, 並預設轉為 base64 內嵌,
99
+ // 故中介 html 置於他處亦不影響圖片
100
+ fs.mkdirSync(path.dirname(fpOutHtml), { recursive: true })
101
+ let errHtml = null
102
+ await retryBusy(() => WMd2html(fpInMd, fpOutHtml, optMd2html))
103
+ .catch((err) => {
104
+ errHtml = toErrText(err)
105
+ })
106
+ if (errHtml !== null) {
107
+ return Promise.reject(`Failed to convert md to html: ${errHtml}`)
108
+ }
109
+
110
+ //驗證 html(產物實體檔為準, 不憑回傳值)
111
+ if (!fsIsFile(fpOutHtml) || fs.statSync(fpOutHtml).size === 0) {
112
+ return Promise.reject('Failed to convert md to html: html was not generated or is empty')
113
+ }
114
+ let sizeHtml = fs.statSync(fpOutHtml).size
115
+
116
+ //事前檢查轉檔器(依 cwd 定位, 查無則此 cwd 下必定失敗, 先行明確報錯)
117
+ let fpExe = getFpExe()
118
+ if (fpExe === '') {
119
+ return Promise.reject(`htmlToDocx.exe not found (cwd=${path.resolve()}), please run from the folder that contains node_modules/w-html2docx`)
120
+ }
121
+
122
+ //html -> docx(排隊逐一執行, 避免同時調用本機 Word)
123
+ fs.mkdirSync(path.dirname(fpOutDocx), { recursive: true })
124
+ let errDocx = null
125
+ await runExclusive(() => retryBusy(() => WHtml2docx(fpOutHtml, fpOutDocx, optHtml2docx)))
126
+ .catch((err) => {
127
+ errDocx = toErrText(err)
128
+ })
129
+ if (errDocx !== null) {
130
+ return Promise.reject(`Failed to convert html to docx: ${errDocx}`)
131
+ }
132
+
133
+ //驗證 docx
134
+ //why: w-html2docx 於 Word 未安裝、COM 呼叫失敗等情形「仍回傳 ok」(錯誤僅印於其子程序之輸出),
135
+ // 故一律以產物實體檔之存在與大小為準, 否則將回報假成功
136
+ if (!fsIsFile(fpOutDocx) || fs.statSync(fpOutDocx).size === 0) {
137
+ return Promise.reject('docx was not generated: please make sure Microsoft Word is installed and no stale WINWORD process is locking the files')
138
+ }
139
+ let sizeDocx = fs.statSync(fpOutDocx).size
140
+
141
+ let rt = {
142
+ fpOutDocx,
143
+ sizeDocx,
144
+ sizeHtml,
145
+ ms: Date.now() - msStart,
146
+ }
147
+ if (bKeepHtml) {
148
+ rt.fpOutHtml = fpOutHtml
149
+ }
150
+
151
+ return rt
152
+
153
+ }
154
+ finally {
155
+
156
+ //刪除中介 html(使用者指定保留者不刪; 被鎖就略過, 留待系統暫存清理)
157
+ if (!bKeepHtml && fsIsFile(fpOutHtml)) {
158
+ try {
159
+ fs.unlinkSync(fpOutHtml)
160
+ }
161
+ catch (err) {
162
+ //殘檔被鎖, 略過
163
+ }
164
+ }
165
+
166
+ }
167
+
168
+ }
169
+
170
+
171
+ export default cvMdToDocx
Binary file
package/src/utils.mjs ADDED
@@ -0,0 +1,448 @@
1
+ import fs from 'fs'
2
+ import path from 'path'
3
+ import get from 'lodash-es/get.js'
4
+ import isstr from 'wsemi/src/isstr.mjs'
5
+ import isestr from 'wsemi/src/isestr.mjs'
6
+ import isarr from 'wsemi/src/isarr.mjs'
7
+ import isfun from 'wsemi/src/isfun.mjs'
8
+ import ispint from 'wsemi/src/ispint.mjs'
9
+ import ispnum from 'wsemi/src/ispnum.mjs'
10
+ import cint from 'wsemi/src/cint.mjs'
11
+ import cdbl from 'wsemi/src/cdbl.mjs'
12
+ import fsIsFile from 'wsemi/src/fsIsFile.mjs'
13
+ import fsIsFolder from 'wsemi/src/fsIsFolder.mjs'
14
+
15
+
16
+ //mime
17
+ let mimeHtml = 'text/html; charset=utf-8'
18
+ let mimeDocx = 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
19
+
20
+
21
+ /**
22
+ * 取錯誤訊息文字
23
+ *
24
+ * 底層轉檔器之reject可能為字串或Error物件,統一轉為文字供組合訊息使用。
25
+ *
26
+ * @param {*} err 輸入錯誤資料,可為字串、Error物件或任意資料
27
+ * @returns {String} 回傳錯誤訊息字串
28
+ * @example
29
+ *
30
+ * console.log(toErrText('abc'))
31
+ * // => 'abc'
32
+ *
33
+ * console.log(toErrText(new Error('abc')))
34
+ * // => 'abc'
35
+ *
36
+ */
37
+ function toErrText(err) {
38
+ if (isstr(err)) {
39
+ return err
40
+ }
41
+ if (isestr(get(err, 'message'))) {
42
+ return err.message
43
+ }
44
+ let s = ''
45
+ try {
46
+ s = JSON.stringify(err) //undefined/function/symbol 會回 undefined 而非字串
47
+ }
48
+ catch (e) {
49
+ s = ''
50
+ }
51
+ if (!isstr(s)) {
52
+ s = String(err)
53
+ }
54
+ return s
55
+ }
56
+
57
+
58
+ /**
59
+ * 重試短暫被鎖定之檔案操作
60
+ *
61
+ * Windows下新建檔案常被防毒或索引器即時掃描而短暫鎖定(EBUSY/EPERM),遇此類錯誤時延遲後重試以提高穩定性,其餘錯誤直接拋出。
62
+ *
63
+ * @param {Function} fun 輸入欲執行之函數,可回傳Promise
64
+ * @param {Object} [opt={}] 輸入設定物件,預設{}
65
+ * @param {Integer} [opt.tries=6] 輸入最多嘗試次數整數,預設6
66
+ * @param {Number} [opt.delayMs=400] 輸入每次重試前延遲毫秒數,預設400
67
+ * @returns {Promise} 回傳Promise,resolve回傳fun之結果,reject回傳錯誤
68
+ * @example
69
+ *
70
+ * let r = await retryBusy(() => fs.promises.readFile('./a.txt', 'utf8'), { tries: 3 })
71
+ *
72
+ */
73
+ async function retryBusy(fun, opt = {}) {
74
+
75
+ //check
76
+ if (!isfun(fun)) {
77
+ return Promise.reject('fun must be a function')
78
+ }
79
+
80
+ //tries
81
+ let tries = get(opt, 'tries', null)
82
+ if (!ispint(tries)) {
83
+ tries = 6
84
+ }
85
+ tries = cint(tries)
86
+
87
+ //delayMs
88
+ let delayMs = get(opt, 'delayMs', null)
89
+ if (!ispnum(delayMs)) {
90
+ delayMs = 400
91
+ }
92
+ delayMs = cdbl(delayMs)
93
+
94
+ let errLast = null
95
+ for (let i = 0; i < tries; i++) {
96
+ try {
97
+ return await fun()
98
+ }
99
+ catch (err) {
100
+ errLast = err
101
+ let code = get(err, 'code', '')
102
+ let busy = (code === 'EBUSY' || code === 'EPERM')
103
+ if (busy && i < tries - 1) {
104
+ await new Promise((resolve) => setTimeout(resolve, delayMs))
105
+ continue
106
+ }
107
+ throw err
108
+ }
109
+ }
110
+ throw errLast
111
+ }
112
+
113
+
114
+ //pmChain: docx 轉檔須逐一序列化
115
+ //why: 底層係調用本機 Word(win32com)進行轉檔, 同時執行多份會相互干擾(共用 Word 實例、模板檔遭鎖定),
116
+ // 故不論同時進來幾個請求, docx 階段一律排隊逐一執行; md 轉 html 為純 JS 不受此限
117
+ let pmChain = Promise.resolve()
118
+ let nQueue = 0
119
+
120
+
121
+ /**
122
+ * 將函數排入Word轉檔佇列並逐一執行
123
+ *
124
+ * 底層係調用本機Word(win32com)轉檔,同時執行多份會相互干擾,故docx階段一律排隊逐一執行;前一件不論成敗均續行下一件。
125
+ *
126
+ * @param {Function} fun 輸入欲執行之函數,可回傳Promise
127
+ * @returns {Promise} 回傳Promise,resolve回傳fun之結果,reject回傳錯誤
128
+ * @example
129
+ *
130
+ * let r = await runExclusive(() => WHtml2docx('./a.html', './a.docx'))
131
+ *
132
+ */
133
+ function runExclusive(fun) {
134
+
135
+ //check
136
+ if (!isfun(fun)) {
137
+ return Promise.reject('fun must be a function')
138
+ }
139
+
140
+ nQueue++
141
+ let pm = pmChain.then(fun, fun) //前一件不論成敗均續行
142
+ pmChain = pm.then(() => {
143
+ nQueue--
144
+ }, () => {
145
+ nQueue--
146
+ })
147
+ return pm
148
+ }
149
+
150
+
151
+ /**
152
+ * 取當前排隊中(含執行中)之docx轉檔數
153
+ *
154
+ * @returns {Integer} 回傳排隊數整數
155
+ * @example
156
+ *
157
+ * console.log(getQueueSize())
158
+ * // => 0
159
+ *
160
+ */
161
+ function getQueueSize() {
162
+ return nQueue
163
+ }
164
+
165
+
166
+ /**
167
+ * 取w-html2docx之htmlToDocx.exe位置
168
+ *
169
+ * 注意:w-html2docx內部以「模組載入當下之工作目錄(cwd)」推導exe位置,故此處以同一規則檢查,使事前檢查與實際轉檔行為一致;查無即代表由此cwd執行必定轉檔失敗。
170
+ *
171
+ * @returns {String} 回傳exe位置字串,查無則回傳空字串
172
+ * @example
173
+ *
174
+ * console.log(getFpExe())
175
+ * // => 'C:\\proj\\node_modules\\w-html2docx\\src\\htmlToDocx.exe'
176
+ *
177
+ */
178
+ function getFpExe() {
179
+ let fdSrv = path.resolve() //= process.cwd(), 與 w-html2docx 內部同規則
180
+ let fps = [
181
+ path.resolve(fdSrv, 'src/htmlToDocx.exe'),
182
+ path.resolve(fdSrv, 'node_modules/w-html2docx/src/htmlToDocx.exe'),
183
+ ]
184
+ for (let fp of fps) {
185
+ if (fsIsFile(fp)) {
186
+ return fp
187
+ }
188
+ }
189
+ return ''
190
+ }
191
+
192
+
193
+ /**
194
+ * 檢查本機是否具備docx轉檔條件(Windows且轉檔器存在)
195
+ *
196
+ * 注意:僅代表環境條件成立,不代表Word確實可被調用;後者須實際轉一份小檔驗證。
197
+ *
198
+ * @returns {Object} 回傳狀態物件{platform,isWindows,cwd,exePath,exeFound,ready}
199
+ * @example
200
+ *
201
+ * console.log(checkDocxReady())
202
+ * // => { platform: 'win32', isWindows: true, cwd: '...', exePath: '...', exeFound: true, ready: true }
203
+ *
204
+ */
205
+ function checkDocxReady() {
206
+ let fpExe = getFpExe()
207
+ return {
208
+ platform: process.platform,
209
+ isWindows: process.platform === 'win32',
210
+ cwd: path.resolve(),
211
+ exePath: fpExe,
212
+ exeFound: fpExe !== '',
213
+ ready: process.platform === 'win32' && fpExe !== '',
214
+ }
215
+ }
216
+
217
+
218
+ /**
219
+ * 取安全檔名主體
220
+ *
221
+ * 移除路徑成分、常見副檔名與跨平台非法字元,避免路徑穿越與寫檔失敗;無效輸入回傳'output'。
222
+ *
223
+ * @param {String} name 輸入檔名字串
224
+ * @returns {String} 回傳安全檔名主體字串
225
+ * @example
226
+ *
227
+ * console.log(toSafeName('../report/報告R00.01.md'))
228
+ * // => '報告R00.01'
229
+ *
230
+ * console.log(toSafeName(''))
231
+ * // => 'output'
232
+ *
233
+ */
234
+ function toSafeName(name) {
235
+ let s = ''
236
+ if (isstr(name)) {
237
+ s = name.trim()
238
+ }
239
+ if (s === '') {
240
+ return 'output'
241
+ }
242
+ s = path.basename(s) //去除任何路徑成分
243
+ s = s.replace(/\.(md|markdown|html|htm|docx)$/i, '') //去除常見副檔名
244
+ s = s.replace(/[\\/:*?"<>|\r\n\t]/g, '_') //Windows 非法字元
245
+ s = s.replace(/^\.+/, '') //去除開頭點號(避免隱藏檔與相對路徑)
246
+ s = s.trim()
247
+ if (s === '') {
248
+ return 'output'
249
+ }
250
+ return s.slice(0, 120)
251
+ }
252
+
253
+
254
+ //checkAssetPath: 單一相對路徑之落點檢查
255
+ //why: 路徑可能來自外部輸入, 須以絕對路徑比對且尾端補分隔字元, 避免同前綴之兄弟資料夾被放行
256
+ function checkAssetPath(dirJob, rp, pOri) {
257
+
258
+ //拒絕絕對路徑(含磁碟機代號與 UNC)
259
+ if (/^[a-zA-Z]:/.test(rp) || rp.startsWith('/') || rp.startsWith('//')) {
260
+ throw new Error(`asset.path must be a relative path: ${pOri}`)
261
+ }
262
+
263
+ //落點檢查
264
+ let fp = path.resolve(dirJob, rp)
265
+ let base = path.resolve(dirJob) + path.sep
266
+ if (!fp.startsWith(base)) {
267
+ throw new Error(`asset.path is outside the working folder: ${pOri}`)
268
+ }
269
+
270
+ return fp
271
+ }
272
+
273
+
274
+ /**
275
+ * 解析資產之相對路徑並防護路徑穿越
276
+ *
277
+ * md內之圖片路徑可能為URL編碼(中文或空白),而轉檔器係以md內之原字串組路徑取圖;故原字串與解碼後字串不同時回傳兩者,供呼叫端寫入同一內容確保圖片必能被解析。
278
+ *
279
+ * @param {String} dirJob 輸入工作資料夾位置字串
280
+ * @param {String} p 輸入資產相對路徑字串
281
+ * @returns {Array} 回傳實際寫入之絕對路徑陣列
282
+ * @example
283
+ *
284
+ * console.log(resolveAssetPaths('C:\\job', 'pics/%E5%9C%96.png'))
285
+ * // => [ 'C:\\job\\pics\\%E5%9C%96.png', 'C:\\job\\pics\\圖.png' ]
286
+ *
287
+ */
288
+ function resolveAssetPaths(dirJob, p) {
289
+
290
+ //raw
291
+ let raw = ''
292
+ if (isstr(p)) {
293
+ raw = p.trim().replace(/\\/g, '/')
294
+ }
295
+ if (raw === '') {
296
+ throw new Error('asset.path is empty')
297
+ }
298
+
299
+ //解碼; 非法 percent 序列則視為未編碼
300
+ let dec = raw
301
+ try {
302
+ dec = decodeURIComponent(raw)
303
+ }
304
+ catch (err) {
305
+ dec = raw
306
+ }
307
+
308
+ let rps = (raw === dec) ? [raw] : [raw, dec]
309
+ return rps.map((rp) => checkAssetPath(dirJob, rp, p))
310
+ }
311
+
312
+
313
+ /**
314
+ * 將夾帶之資產(圖片等)還原至工作資料夾
315
+ *
316
+ * 使md內之相對路徑得以解析;base64容許data URI形式。
317
+ *
318
+ * @param {String} dirJob 輸入工作資料夾位置字串
319
+ * @param {Array} assets 輸入資產陣列,格式[{path,base64}],亦接受contentBase64欄位
320
+ * @returns {Integer} 回傳寫入之資產數整數
321
+ * @example
322
+ *
323
+ * let n = writeAssets('C:\\job', [{ path: 'pics/圖.png', base64: '…' }])
324
+ * // => 1
325
+ *
326
+ */
327
+ function writeAssets(dirJob, assets) {
328
+ let n = 0
329
+ if (!isarr(assets)) {
330
+ return n
331
+ }
332
+ for (let v of assets) {
333
+ let p = get(v, 'path', '')
334
+ let fps = resolveAssetPaths(dirJob, p)
335
+ let b64 = get(v, 'base64', '')
336
+ if (!isestr(b64)) {
337
+ b64 = get(v, 'contentBase64', '')
338
+ }
339
+ if (!isestr(b64)) {
340
+ throw new Error(`asset.base64 is empty: ${p}`)
341
+ }
342
+ b64 = b64.replace(/^data:[^;]+;base64,/, '') //容許 data URI 形式
343
+ let buf = Buffer.from(b64, 'base64')
344
+ for (let fp of fps) {
345
+ fs.mkdirSync(path.dirname(fp), { recursive: true })
346
+ fs.writeFileSync(fp, buf)
347
+ }
348
+ n++
349
+ }
350
+ return n
351
+ }
352
+
353
+
354
+ /**
355
+ * 清除工作資料夾內逾時未刪之作業夾(前綴job_)
356
+ *
357
+ * 工作資料夾不存在時自動建立。
358
+ *
359
+ * @param {String} dirWork 輸入工作資料夾位置字串
360
+ * @param {Object} [opt={}] 輸入設定物件,預設{}
361
+ * @param {Number} [opt.msAge=21600000] 輸入視為逾時之毫秒數,預設6小時(21600000)
362
+ * @returns {Integer} 回傳刪除之作業夾數整數
363
+ * @example
364
+ *
365
+ * let n = cleanWorkDir('./tmp', { msAge: 3600 * 1000 })
366
+ * // => 2
367
+ *
368
+ */
369
+ function cleanWorkDir(dirWork, opt = {}) {
370
+
371
+ //check
372
+ if (!isestr(dirWork)) {
373
+ return 0
374
+ }
375
+
376
+ //msAge
377
+ let msAge = get(opt, 'msAge', null)
378
+ if (!ispnum(msAge)) {
379
+ msAge = 6 * 3600 * 1000
380
+ }
381
+ msAge = cdbl(msAge)
382
+
383
+ if (!fsIsFolder(dirWork)) {
384
+ fs.mkdirSync(dirWork, { recursive: true })
385
+ return 0
386
+ }
387
+ let n = 0
388
+ let msNow = Date.now()
389
+ for (let fn of fs.readdirSync(dirWork)) {
390
+ if (!fn.startsWith('job_')) {
391
+ continue
392
+ }
393
+ let fp = path.resolve(dirWork, fn)
394
+ try {
395
+ let st = fs.statSync(fp)
396
+ if (msNow - st.mtimeMs > msAge) {
397
+ fs.rmSync(fp, { recursive: true, force: true })
398
+ n++
399
+ }
400
+ }
401
+ catch (err) {
402
+ //忽略無法處理者
403
+ }
404
+ }
405
+ return n
406
+ }
407
+
408
+
409
+ /**
410
+ * 轉譯資產缺漏之錯誤訊息
411
+ *
412
+ * 底層僅回原始路徑訊息(且含內部工作夾路徑),此處改為可行動之提示並隱去工作路徑;非資產缺漏訊息原樣回傳。
413
+ *
414
+ * @param {String} msg 輸入原始錯誤訊息字串
415
+ * @returns {String} 回傳轉譯後錯誤訊息字串
416
+ * @example
417
+ *
418
+ * console.log(transErrAsset('fp[C:/tmp/job_1/pics/a.png] does not exist'))
419
+ * // => 'Failed to convert md to html: the referenced file (pics/a.png) does not exist, please make sure it is included in assets'
420
+ *
421
+ */
422
+ function transErrAsset(msg) {
423
+ if (!isstr(msg)) {
424
+ return toErrText(msg)
425
+ }
426
+ let mt = msg.match(/job_[^\\/]+[\\/](.+?)\]/)
427
+ if (/does not exist/i.test(msg) && mt) {
428
+ return `Failed to convert md to html: the referenced file (${mt[1].replace(/\\/g, '/')}) does not exist, please make sure it is included in assets`
429
+ }
430
+ return msg
431
+ }
432
+
433
+
434
+ export {
435
+ mimeHtml,
436
+ mimeDocx,
437
+ toErrText,
438
+ retryBusy,
439
+ runExclusive,
440
+ getQueueSize,
441
+ getFpExe,
442
+ checkDocxReady,
443
+ toSafeName,
444
+ resolveAssetPaths,
445
+ writeAssets,
446
+ cleanWorkDir,
447
+ transErrAsset
448
+ }