w-md2docx 1.0.6 → 1.0.7
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.
- package/README.md +2 -0
- package/dist/w-md2docx.umd.js +2 -2
- package/dist/w-md2docx.umd.js.map +1 -1
- package/docs/WMd2docx.mjs.html +1 -1
- package/docs/cvMdTo.mjs.html +1 -1
- package/docs/cvMdToDocx.mjs.html +14 -12
- package/docs/global.html +20 -17
- package/docs/index.html +1 -1
- package/docs/rmApiClient.mjs.html +1 -1
- package/docs/rmApiServer.mjs.html +3 -3
- package/docs/utils.mjs.html +15 -10
- package/package.json +3 -3
- package/src/cvMdToDocx.mjs +13 -11
- package/src/rmApiServer.mjs +2 -2
- package/src/utils.mjs +14 -9
- package/test/api-rmApiClient.test.mjs +40 -15
- package/test/api-rmApiServer.test.mjs +10 -7
- package/test/unit-cvMdTo.test.mjs +2 -2
- package/test/unit-cvMdToDocx.test.mjs +175 -12
- package/test/unit-utils.test.mjs +20 -3
package/docs/WMd2docx.mjs.html
CHANGED
|
@@ -88,7 +88,7 @@ export default WMd2docx
|
|
|
88
88
|
<br class="clear">
|
|
89
89
|
|
|
90
90
|
<footer>
|
|
91
|
-
Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Sun Sep 13 2026
|
|
91
|
+
Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Sun Sep 13 2026 22:13:36 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
|
|
92
92
|
</footer>
|
|
93
93
|
|
|
94
94
|
<script>prettyPrint();</script>
|
package/docs/cvMdTo.mjs.html
CHANGED
|
@@ -275,7 +275,7 @@ export default cvMdTo
|
|
|
275
275
|
<br class="clear">
|
|
276
276
|
|
|
277
277
|
<footer>
|
|
278
|
-
Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Sun Sep 13 2026
|
|
278
|
+
Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Sun Sep 13 2026 22:13:36 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
|
|
279
279
|
</footer>
|
|
280
280
|
|
|
281
281
|
<script>prettyPrint();</script>
|
package/docs/cvMdToDocx.mjs.html
CHANGED
|
@@ -54,7 +54,7 @@ import isobj from 'wsemi/src/isobj.mjs'
|
|
|
54
54
|
import fsIsFile from 'wsemi/src/fsIsFile.mjs'
|
|
55
55
|
import WMd2html from 'w-md2html/src/WMd2html.mjs'
|
|
56
56
|
import WHtml2docx from 'w-html2docx/src/WHtml2docx.mjs'
|
|
57
|
-
import { toErrText, retryBusy, runExclusive
|
|
57
|
+
import { toErrText, retryBusy, runExclusive } from './utils.mjs'
|
|
58
58
|
|
|
59
59
|
|
|
60
60
|
/**
|
|
@@ -62,7 +62,9 @@ import { toErrText, retryBusy, runExclusive, getFpExe } from './utils.mjs'
|
|
|
62
62
|
*
|
|
63
63
|
* 內部流程為md -> html -> docx。docx階段係調用本機Microsoft Word(win32com),故僅能於已安裝Word之Windows執行,且同時間僅一份轉檔作業(內部佇列自動排隊)。
|
|
64
64
|
*
|
|
65
|
-
*
|
|
65
|
+
* 轉檔器htmlToDocx.exe之定位與補齊由w-html2docx負責:其以當前工作目錄(cwd)查找,缺檔(如npm封鎖安裝腳本致postinstall未執行)時於轉檔當下自動下載,故須於含node_modules/w-html2docx之專案根執行,且首次轉檔須可連網。
|
|
66
|
+
*
|
|
67
|
+
* w-html2docx(≥1.0.29)於轉檔器失敗(Word未安裝、模板無法開啟、另存失敗等)時以離開碼1 reject並帶轉檔器回報之原因,resolve前亦自行確認docx已產生;本函數將其訊息加上前綴原樣傳出,並再以產物實體檔之存在與大小把關一次,不會回報假成功。
|
|
66
68
|
*
|
|
67
69
|
* @param {String} fpInMd 輸入來源Markdown檔位置字串
|
|
68
70
|
* @param {String} fpOutDocx 輸入轉出Docx檔位置字串
|
|
@@ -160,13 +162,9 @@ async function cvMdToDocx(fpInMd, fpOutDocx, opt = {}) {
|
|
|
160
162
|
}
|
|
161
163
|
let sizeHtml = fs.statSync(fpOutHtml).size
|
|
162
164
|
|
|
163
|
-
//事前檢查轉檔器(依 cwd 定位, 查無則此 cwd 下必定失敗, 先行明確報錯)
|
|
164
|
-
let fpExe = getFpExe()
|
|
165
|
-
if (fpExe === '') {
|
|
166
|
-
return Promise.reject(`htmlToDocx.exe not found (cwd=${path.resolve()}), please run from the folder that contains node_modules/w-html2docx`)
|
|
167
|
-
}
|
|
168
|
-
|
|
169
165
|
//html -> docx(排隊逐一執行, 避免同時調用本機 Word)
|
|
166
|
+
//note: 轉檔器之定位與缺檔下載由 WHtml2docx 內部 autoDownloadFiles 負責, 此處不得事前檢查,
|
|
167
|
+
// 否則下層之自動補齊永遠不會被執行
|
|
170
168
|
fs.mkdirSync(path.dirname(fpOutDocx), { recursive: true })
|
|
171
169
|
let errDocx = null
|
|
172
170
|
await runExclusive(() => retryBusy(() => WHtml2docx(fpOutHtml, fpOutDocx, optHtml2docx)))
|
|
@@ -174,12 +172,16 @@ async function cvMdToDocx(fpInMd, fpOutDocx, opt = {}) {
|
|
|
174
172
|
errDocx = toErrText(err)
|
|
175
173
|
})
|
|
176
174
|
if (errDocx !== null) {
|
|
175
|
+
//w-html2docx 以 cwd 查找 htmlToDocx.exe 與預設模板, 查找失敗之訊息含 can not find, 保留原文並補上 cwd 與執行位置提示
|
|
176
|
+
if (errDocx.indexOf('can not find') >= 0) {
|
|
177
|
+
errDocx = `${errDocx} (cwd=${path.resolve()}, w-html2docx resolves htmlToDocx.exe and its default template from cwd and downloads htmlToDocx.exe automatically when absent, please run from the project root that contains node_modules/w-html2docx)`
|
|
178
|
+
}
|
|
177
179
|
return Promise.reject(`Failed to convert html to docx: ${errDocx}`)
|
|
178
180
|
}
|
|
179
181
|
|
|
180
|
-
//驗證 docx
|
|
181
|
-
//
|
|
182
|
-
//
|
|
182
|
+
//驗證 docx(第二道防線)
|
|
183
|
+
//note: w-html2docx ≥1.0.29 於轉檔器失敗時已自行 reject(離開碼1, 帶轉檔器原因), resolve 前亦確認 docx 已產生且非空,
|
|
184
|
+
// 此處再以產物實體檔為準把關一次, 涵蓋其 resolve 後檔案又消失等非預期情形, 不憑回傳值
|
|
183
185
|
if (!fsIsFile(fpOutDocx) || fs.statSync(fpOutDocx).size === 0) {
|
|
184
186
|
return Promise.reject('docx was not generated: please make sure Microsoft Word is installed and no stale WINWORD process is locking the files')
|
|
185
187
|
}
|
|
@@ -230,7 +232,7 @@ export default cvMdToDocx
|
|
|
230
232
|
<br class="clear">
|
|
231
233
|
|
|
232
234
|
<footer>
|
|
233
|
-
Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Sun Sep 13 2026
|
|
235
|
+
Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Sun Sep 13 2026 22:13:36 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
|
|
234
236
|
</footer>
|
|
235
237
|
|
|
236
238
|
<script>prettyPrint();</script>
|
package/docs/global.html
CHANGED
|
@@ -222,14 +222,15 @@ console.log(r)
|
|
|
222
222
|
<dl class="details">
|
|
223
223
|
|
|
224
224
|
<dt class="tag-description">Description:</dt>
|
|
225
|
-
<dd class="tag-description"><ul class="dummy"><li><p>檢查本機是否具備docx
|
|
226
|
-
<p
|
|
225
|
+
<dd class="tag-description"><ul class="dummy"><li><p>檢查本機是否具備docx轉檔條件</p>
|
|
226
|
+
<p>ready僅以作業系統為Windows判定,不以轉檔器是否存在為條件:exeFound為false代表轉檔器目前尚未取得,將於首次轉檔時由w-html2docx自動下載,並非不可用。</p>
|
|
227
|
+
<p>注意:僅代表環境條件成立,不代表轉檔器可下載成功或Word確實可被調用;後二者須實際轉一份小檔驗證。</p></li></ul></dd>
|
|
227
228
|
|
|
228
229
|
|
|
229
230
|
|
|
230
231
|
<dt class="tag-source">Source:</dt>
|
|
231
232
|
<dd class="tag-source"><ul class="dummy"><li>
|
|
232
|
-
<a href="utils.mjs.html">utils.mjs</a>, <a href="utils.mjs.html#
|
|
233
|
+
<a href="utils.mjs.html">utils.mjs</a>, <a href="utils.mjs.html#line209">line 209</a>
|
|
233
234
|
</li></ul></dd>
|
|
234
235
|
|
|
235
236
|
|
|
@@ -277,7 +278,7 @@ console.log(r)
|
|
|
277
278
|
<h5 class="h5-examples">Example</h5>
|
|
278
279
|
|
|
279
280
|
<pre class="prettyprint"><code>console.log(checkDocxReady())
|
|
280
|
-
// => { platform: 'win32', isWindows: true, cwd: '...', exePath: '
|
|
281
|
+
// => { platform: 'win32', isWindows: true, cwd: '...', exePath: '', exeFound: false, ready: true }</code></pre>
|
|
281
282
|
|
|
282
283
|
|
|
283
284
|
|
|
@@ -300,7 +301,7 @@ console.log(r)
|
|
|
300
301
|
|
|
301
302
|
|
|
302
303
|
<div class="param-desc">
|
|
303
|
-
<p>回傳狀態物件{platform,isWindows,cwd,exePath,exeFound,ready}</p>
|
|
304
|
+
<p>回傳狀態物件{platform,isWindows,cwd,exePath,exeFound,ready},exePath與exeFound為轉檔器目前所在位置與是否已存在(診斷用)</p>
|
|
304
305
|
</div>
|
|
305
306
|
|
|
306
307
|
|
|
@@ -344,7 +345,7 @@ console.log(r)
|
|
|
344
345
|
|
|
345
346
|
<dt class="tag-source">Source:</dt>
|
|
346
347
|
<dd class="tag-source"><ul class="dummy"><li>
|
|
347
|
-
<a href="utils.mjs.html">utils.mjs</a>, <a href="utils.mjs.html#
|
|
348
|
+
<a href="utils.mjs.html">utils.mjs</a>, <a href="utils.mjs.html#line374">line 374</a>
|
|
348
349
|
</li></ul></dd>
|
|
349
350
|
|
|
350
351
|
|
|
@@ -2068,13 +2069,14 @@ console.log(r)
|
|
|
2068
2069
|
<dt class="tag-description">Description:</dt>
|
|
2069
2070
|
<dd class="tag-description"><ul class="dummy"><li><p>Markdown檔轉Docx檔</p>
|
|
2070
2071
|
<p>內部流程為md -> html -> docx。docx階段係調用本機Microsoft Word(win32com),故僅能於已安裝Word之Windows執行,且同時間僅一份轉檔作業(內部佇列自動排隊)。</p>
|
|
2071
|
-
<p
|
|
2072
|
+
<p>轉檔器htmlToDocx.exe之定位與補齊由w-html2docx負責:其以當前工作目錄(cwd)查找,缺檔(如npm封鎖安裝腳本致postinstall未執行)時於轉檔當下自動下載,故須於含node_modules/w-html2docx之專案根執行,且首次轉檔須可連網。</p>
|
|
2073
|
+
<p>w-html2docx(≥1.0.29)於轉檔器失敗(Word未安裝、模板無法開啟、另存失敗等)時以離開碼1 reject並帶轉檔器回報之原因,resolve前亦自行確認docx已產生;本函數將其訊息加上前綴原樣傳出,並再以產物實體檔之存在與大小把關一次,不會回報假成功。</p></li></ul></dd>
|
|
2072
2074
|
|
|
2073
2075
|
|
|
2074
2076
|
|
|
2075
2077
|
<dt class="tag-source">Source:</dt>
|
|
2076
2078
|
<dd class="tag-source"><ul class="dummy"><li>
|
|
2077
|
-
<a href="cvMdToDocx.mjs.html">cvMdToDocx.mjs</a>, <a href="cvMdToDocx.mjs.html#
|
|
2079
|
+
<a href="cvMdToDocx.mjs.html">cvMdToDocx.mjs</a>, <a href="cvMdToDocx.mjs.html#line44">line 44</a>
|
|
2078
2080
|
</li></ul></dd>
|
|
2079
2081
|
|
|
2080
2082
|
|
|
@@ -2522,14 +2524,15 @@ console.log(r)
|
|
|
2522
2524
|
<dl class="details">
|
|
2523
2525
|
|
|
2524
2526
|
<dt class="tag-description">Description:</dt>
|
|
2525
|
-
<dd class="tag-description"><ul class="dummy"><li><p>取w-html2docx之htmlToDocx.exe
|
|
2526
|
-
<p
|
|
2527
|
+
<dd class="tag-description"><ul class="dummy"><li><p>取w-html2docx之htmlToDocx.exe目前所在位置(僅供診斷顯示)</p>
|
|
2528
|
+
<p>依w-html2docx之查找位置(cwd之src/、cwd之node_modules/w-html2docx/src/)查詢檔案「目前」是否存在,不下載。</p>
|
|
2529
|
+
<p>注意:不得作為轉檔前之守門。查無不代表無法轉檔——w-html2docx於轉檔當下會自動下載缺少之轉檔器,上層以此攔截會使該機制永遠不被執行。</p></li></ul></dd>
|
|
2527
2530
|
|
|
2528
2531
|
|
|
2529
2532
|
|
|
2530
2533
|
<dt class="tag-source">Source:</dt>
|
|
2531
2534
|
<dd class="tag-source"><ul class="dummy"><li>
|
|
2532
|
-
<a href="utils.mjs.html">utils.mjs</a>, <a href="utils.mjs.html#
|
|
2535
|
+
<a href="utils.mjs.html">utils.mjs</a>, <a href="utils.mjs.html#line180">line 180</a>
|
|
2533
2536
|
</li></ul></dd>
|
|
2534
2537
|
|
|
2535
2538
|
|
|
@@ -2600,7 +2603,7 @@ console.log(r)
|
|
|
2600
2603
|
|
|
2601
2604
|
|
|
2602
2605
|
<div class="param-desc">
|
|
2603
|
-
<p>回傳exe
|
|
2606
|
+
<p>回傳exe位置字串,目前不存在則回傳空字串</p>
|
|
2604
2607
|
</div>
|
|
2605
2608
|
|
|
2606
2609
|
|
|
@@ -3364,7 +3367,7 @@ console.log(r)
|
|
|
3364
3367
|
|
|
3365
3368
|
<dt class="tag-source">Source:</dt>
|
|
3366
3369
|
<dd class="tag-source"><ul class="dummy"><li>
|
|
3367
|
-
<a href="utils.mjs.html">utils.mjs</a>, <a href="utils.mjs.html#
|
|
3370
|
+
<a href="utils.mjs.html">utils.mjs</a>, <a href="utils.mjs.html#line293">line 293</a>
|
|
3368
3371
|
</li></ul></dd>
|
|
3369
3372
|
|
|
3370
3373
|
|
|
@@ -4920,7 +4923,7 @@ console.log(toErrText(new Error('abc')))
|
|
|
4920
4923
|
|
|
4921
4924
|
<dt class="tag-source">Source:</dt>
|
|
4922
4925
|
<dd class="tag-source"><ul class="dummy"><li>
|
|
4923
|
-
<a href="utils.mjs.html">utils.mjs</a>, <a href="utils.mjs.html#
|
|
4926
|
+
<a href="utils.mjs.html">utils.mjs</a>, <a href="utils.mjs.html#line239">line 239</a>
|
|
4924
4927
|
</li></ul></dd>
|
|
4925
4928
|
|
|
4926
4929
|
|
|
@@ -5088,7 +5091,7 @@ console.log(toSafeName(''))
|
|
|
5088
5091
|
|
|
5089
5092
|
<dt class="tag-source">Source:</dt>
|
|
5090
5093
|
<dd class="tag-source"><ul class="dummy"><li>
|
|
5091
|
-
<a href="utils.mjs.html">utils.mjs</a>, <a href="utils.mjs.html#
|
|
5094
|
+
<a href="utils.mjs.html">utils.mjs</a>, <a href="utils.mjs.html#line427">line 427</a>
|
|
5092
5095
|
</li></ul></dd>
|
|
5093
5096
|
|
|
5094
5097
|
|
|
@@ -5253,7 +5256,7 @@ console.log(toSafeName(''))
|
|
|
5253
5256
|
|
|
5254
5257
|
<dt class="tag-source">Source:</dt>
|
|
5255
5258
|
<dd class="tag-source"><ul class="dummy"><li>
|
|
5256
|
-
<a href="utils.mjs.html">utils.mjs</a>, <a href="utils.mjs.html#
|
|
5259
|
+
<a href="utils.mjs.html">utils.mjs</a>, <a href="utils.mjs.html#line332">line 332</a>
|
|
5257
5260
|
</li></ul></dd>
|
|
5258
5261
|
|
|
5259
5262
|
|
|
@@ -5440,7 +5443,7 @@ console.log(toSafeName(''))
|
|
|
5440
5443
|
<br class="clear">
|
|
5441
5444
|
|
|
5442
5445
|
<footer>
|
|
5443
|
-
Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Sun Sep 13 2026
|
|
5446
|
+
Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Sun Sep 13 2026 22:13:36 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
|
|
5444
5447
|
</footer>
|
|
5445
5448
|
|
|
5446
5449
|
<script>prettyPrint();</script>
|
package/docs/index.html
CHANGED
|
@@ -71,7 +71,7 @@
|
|
|
71
71
|
<br class="clear">
|
|
72
72
|
|
|
73
73
|
<footer>
|
|
74
|
-
Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Sun Sep 13 2026
|
|
74
|
+
Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Sun Sep 13 2026 22:13:36 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
|
|
75
75
|
</footer>
|
|
76
76
|
|
|
77
77
|
<script>prettyPrint();</script>
|
|
@@ -526,7 +526,7 @@ export default rmApiClient
|
|
|
526
526
|
<br class="clear">
|
|
527
527
|
|
|
528
528
|
<footer>
|
|
529
|
-
Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Sun Sep 13 2026
|
|
529
|
+
Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Sun Sep 13 2026 22:13:36 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
|
|
530
530
|
</footer>
|
|
531
531
|
|
|
532
532
|
<script>prettyPrint();</script>
|
|
@@ -253,9 +253,9 @@ async function rmApiServer(opt = {}) {
|
|
|
253
253
|
platform: rd.platform,
|
|
254
254
|
node: process.version,
|
|
255
255
|
cwd: rd.cwd,
|
|
256
|
-
exeFound: rd.exeFound,
|
|
256
|
+
exeFound: rd.exeFound, //轉檔器目前是否已存在(診斷用), false 時於首次轉檔由 w-html2docx 自動下載
|
|
257
257
|
exePath: rd.exePath,
|
|
258
|
-
docxReady: rd.ready, //僅代表環境具備條件,
|
|
258
|
+
docxReady: rd.ready, //僅代表環境具備條件, 轉檔器可否下載與 Word 可用性須以 /api/selftest 實轉驗證
|
|
259
259
|
templateDefault: getFpTemplateDef(),
|
|
260
260
|
templates: listTemplates(),
|
|
261
261
|
dirTemplates,
|
|
@@ -503,7 +503,7 @@ export default rmApiServer
|
|
|
503
503
|
<br class="clear">
|
|
504
504
|
|
|
505
505
|
<footer>
|
|
506
|
-
Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Sun Sep 13 2026
|
|
506
|
+
Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Sun Sep 13 2026 22:13:36 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
|
|
507
507
|
</footer>
|
|
508
508
|
|
|
509
509
|
<script>prettyPrint();</script>
|
package/docs/utils.mjs.html
CHANGED
|
@@ -211,11 +211,13 @@ function getQueueSize() {
|
|
|
211
211
|
|
|
212
212
|
|
|
213
213
|
/**
|
|
214
|
-
* 取w-html2docx之htmlToDocx.exe
|
|
214
|
+
* 取w-html2docx之htmlToDocx.exe目前所在位置(僅供診斷顯示)
|
|
215
215
|
*
|
|
216
|
-
*
|
|
216
|
+
* 依w-html2docx之查找位置(cwd之src/、cwd之node_modules/w-html2docx/src/)查詢檔案「目前」是否存在,不下載。
|
|
217
217
|
*
|
|
218
|
-
*
|
|
218
|
+
* 注意:不得作為轉檔前之守門。查無不代表無法轉檔——w-html2docx於轉檔當下會自動下載缺少之轉檔器,上層以此攔截會使該機制永遠不被執行。
|
|
219
|
+
*
|
|
220
|
+
* @returns {String} 回傳exe位置字串,目前不存在則回傳空字串
|
|
219
221
|
* @example
|
|
220
222
|
*
|
|
221
223
|
* console.log(getFpExe())
|
|
@@ -238,26 +240,29 @@ function getFpExe() {
|
|
|
238
240
|
|
|
239
241
|
|
|
240
242
|
/**
|
|
241
|
-
* 檢查本機是否具備docx轉檔條件
|
|
243
|
+
* 檢查本機是否具備docx轉檔條件
|
|
244
|
+
*
|
|
245
|
+
* ready僅以作業系統為Windows判定,不以轉檔器是否存在為條件:exeFound為false代表轉檔器目前尚未取得,將於首次轉檔時由w-html2docx自動下載,並非不可用。
|
|
242
246
|
*
|
|
243
|
-
*
|
|
247
|
+
* 注意:僅代表環境條件成立,不代表轉檔器可下載成功或Word確實可被調用;後二者須實際轉一份小檔驗證。
|
|
244
248
|
*
|
|
245
|
-
* @returns {Object} 回傳狀態物件{platform,isWindows,cwd,exePath,exeFound,ready}
|
|
249
|
+
* @returns {Object} 回傳狀態物件{platform,isWindows,cwd,exePath,exeFound,ready},exePath與exeFound為轉檔器目前所在位置與是否已存在(診斷用)
|
|
246
250
|
* @example
|
|
247
251
|
*
|
|
248
252
|
* console.log(checkDocxReady())
|
|
249
|
-
* // => { platform: 'win32', isWindows: true, cwd: '...', exePath: '
|
|
253
|
+
* // => { platform: 'win32', isWindows: true, cwd: '...', exePath: '', exeFound: false, ready: true }
|
|
250
254
|
*
|
|
251
255
|
*/
|
|
252
256
|
function checkDocxReady() {
|
|
253
257
|
let fpExe = getFpExe()
|
|
258
|
+
let isWindows = process.platform === 'win32'
|
|
254
259
|
return {
|
|
255
260
|
platform: process.platform,
|
|
256
|
-
isWindows
|
|
261
|
+
isWindows,
|
|
257
262
|
cwd: path.resolve(),
|
|
258
263
|
exePath: fpExe,
|
|
259
264
|
exeFound: fpExe !== '',
|
|
260
|
-
ready:
|
|
265
|
+
ready: isWindows,
|
|
261
266
|
}
|
|
262
267
|
}
|
|
263
268
|
|
|
@@ -507,7 +512,7 @@ export {
|
|
|
507
512
|
<br class="clear">
|
|
508
513
|
|
|
509
514
|
<footer>
|
|
510
|
-
Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Sun Sep 13 2026
|
|
515
|
+
Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Sun Sep 13 2026 22:13:36 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
|
|
511
516
|
</footer>
|
|
512
517
|
|
|
513
518
|
<script>prettyPrint();</script>
|
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "w-md2docx",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.7",
|
|
4
4
|
"main": "dist/w-md2docx.umd.js",
|
|
5
5
|
"dependencies": {
|
|
6
6
|
"@hapi/hapi": "^21.4.10",
|
|
7
|
-
"w-html2docx": "^1.0.
|
|
7
|
+
"w-html2docx": "^1.0.29",
|
|
8
8
|
"w-md2html": "^1.0.51",
|
|
9
|
-
"wsemi": "^1.8.
|
|
9
|
+
"wsemi": "^1.8.93"
|
|
10
10
|
},
|
|
11
11
|
"devDependencies": {
|
|
12
12
|
"w-package-tools": "^1.1.16"
|
package/src/cvMdToDocx.mjs
CHANGED
|
@@ -7,7 +7,7 @@ import isobj from 'wsemi/src/isobj.mjs'
|
|
|
7
7
|
import fsIsFile from 'wsemi/src/fsIsFile.mjs'
|
|
8
8
|
import WMd2html from 'w-md2html/src/WMd2html.mjs'
|
|
9
9
|
import WHtml2docx from 'w-html2docx/src/WHtml2docx.mjs'
|
|
10
|
-
import { toErrText, retryBusy, runExclusive
|
|
10
|
+
import { toErrText, retryBusy, runExclusive } from './utils.mjs'
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
/**
|
|
@@ -15,7 +15,9 @@ import { toErrText, retryBusy, runExclusive, getFpExe } from './utils.mjs'
|
|
|
15
15
|
*
|
|
16
16
|
* 內部流程為md -> html -> docx。docx階段係調用本機Microsoft Word(win32com),故僅能於已安裝Word之Windows執行,且同時間僅一份轉檔作業(內部佇列自動排隊)。
|
|
17
17
|
*
|
|
18
|
-
*
|
|
18
|
+
* 轉檔器htmlToDocx.exe之定位與補齊由w-html2docx負責:其以當前工作目錄(cwd)查找,缺檔(如npm封鎖安裝腳本致postinstall未執行)時於轉檔當下自動下載,故須於含node_modules/w-html2docx之專案根執行,且首次轉檔須可連網。
|
|
19
|
+
*
|
|
20
|
+
* w-html2docx(≥1.0.29)於轉檔器失敗(Word未安裝、模板無法開啟、另存失敗等)時以離開碼1 reject並帶轉檔器回報之原因,resolve前亦自行確認docx已產生;本函數將其訊息加上前綴原樣傳出,並再以產物實體檔之存在與大小把關一次,不會回報假成功。
|
|
19
21
|
*
|
|
20
22
|
* @param {String} fpInMd 輸入來源Markdown檔位置字串
|
|
21
23
|
* @param {String} fpOutDocx 輸入轉出Docx檔位置字串
|
|
@@ -113,13 +115,9 @@ async function cvMdToDocx(fpInMd, fpOutDocx, opt = {}) {
|
|
|
113
115
|
}
|
|
114
116
|
let sizeHtml = fs.statSync(fpOutHtml).size
|
|
115
117
|
|
|
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
118
|
//html -> docx(排隊逐一執行, 避免同時調用本機 Word)
|
|
119
|
+
//note: 轉檔器之定位與缺檔下載由 WHtml2docx 內部 autoDownloadFiles 負責, 此處不得事前檢查,
|
|
120
|
+
// 否則下層之自動補齊永遠不會被執行
|
|
123
121
|
fs.mkdirSync(path.dirname(fpOutDocx), { recursive: true })
|
|
124
122
|
let errDocx = null
|
|
125
123
|
await runExclusive(() => retryBusy(() => WHtml2docx(fpOutHtml, fpOutDocx, optHtml2docx)))
|
|
@@ -127,12 +125,16 @@ async function cvMdToDocx(fpInMd, fpOutDocx, opt = {}) {
|
|
|
127
125
|
errDocx = toErrText(err)
|
|
128
126
|
})
|
|
129
127
|
if (errDocx !== null) {
|
|
128
|
+
//w-html2docx 以 cwd 查找 htmlToDocx.exe 與預設模板, 查找失敗之訊息含 can not find, 保留原文並補上 cwd 與執行位置提示
|
|
129
|
+
if (errDocx.indexOf('can not find') >= 0) {
|
|
130
|
+
errDocx = `${errDocx} (cwd=${path.resolve()}, w-html2docx resolves htmlToDocx.exe and its default template from cwd and downloads htmlToDocx.exe automatically when absent, please run from the project root that contains node_modules/w-html2docx)`
|
|
131
|
+
}
|
|
130
132
|
return Promise.reject(`Failed to convert html to docx: ${errDocx}`)
|
|
131
133
|
}
|
|
132
134
|
|
|
133
|
-
//驗證 docx
|
|
134
|
-
//
|
|
135
|
-
//
|
|
135
|
+
//驗證 docx(第二道防線)
|
|
136
|
+
//note: w-html2docx ≥1.0.29 於轉檔器失敗時已自行 reject(離開碼1, 帶轉檔器原因), resolve 前亦確認 docx 已產生且非空,
|
|
137
|
+
// 此處再以產物實體檔為準把關一次, 涵蓋其 resolve 後檔案又消失等非預期情形, 不憑回傳值
|
|
136
138
|
if (!fsIsFile(fpOutDocx) || fs.statSync(fpOutDocx).size === 0) {
|
|
137
139
|
return Promise.reject('docx was not generated: please make sure Microsoft Word is installed and no stale WINWORD process is locking the files')
|
|
138
140
|
}
|
package/src/rmApiServer.mjs
CHANGED
|
@@ -206,9 +206,9 @@ async function rmApiServer(opt = {}) {
|
|
|
206
206
|
platform: rd.platform,
|
|
207
207
|
node: process.version,
|
|
208
208
|
cwd: rd.cwd,
|
|
209
|
-
exeFound: rd.exeFound,
|
|
209
|
+
exeFound: rd.exeFound, //轉檔器目前是否已存在(診斷用), false 時於首次轉檔由 w-html2docx 自動下載
|
|
210
210
|
exePath: rd.exePath,
|
|
211
|
-
docxReady: rd.ready, //僅代表環境具備條件,
|
|
211
|
+
docxReady: rd.ready, //僅代表環境具備條件, 轉檔器可否下載與 Word 可用性須以 /api/selftest 實轉驗證
|
|
212
212
|
templateDefault: getFpTemplateDef(),
|
|
213
213
|
templates: listTemplates(),
|
|
214
214
|
dirTemplates,
|
package/src/utils.mjs
CHANGED
|
@@ -164,11 +164,13 @@ function getQueueSize() {
|
|
|
164
164
|
|
|
165
165
|
|
|
166
166
|
/**
|
|
167
|
-
* 取w-html2docx之htmlToDocx.exe
|
|
167
|
+
* 取w-html2docx之htmlToDocx.exe目前所在位置(僅供診斷顯示)
|
|
168
168
|
*
|
|
169
|
-
*
|
|
169
|
+
* 依w-html2docx之查找位置(cwd之src/、cwd之node_modules/w-html2docx/src/)查詢檔案「目前」是否存在,不下載。
|
|
170
170
|
*
|
|
171
|
-
*
|
|
171
|
+
* 注意:不得作為轉檔前之守門。查無不代表無法轉檔——w-html2docx於轉檔當下會自動下載缺少之轉檔器,上層以此攔截會使該機制永遠不被執行。
|
|
172
|
+
*
|
|
173
|
+
* @returns {String} 回傳exe位置字串,目前不存在則回傳空字串
|
|
172
174
|
* @example
|
|
173
175
|
*
|
|
174
176
|
* console.log(getFpExe())
|
|
@@ -191,26 +193,29 @@ function getFpExe() {
|
|
|
191
193
|
|
|
192
194
|
|
|
193
195
|
/**
|
|
194
|
-
* 檢查本機是否具備docx轉檔條件
|
|
196
|
+
* 檢查本機是否具備docx轉檔條件
|
|
197
|
+
*
|
|
198
|
+
* ready僅以作業系統為Windows判定,不以轉檔器是否存在為條件:exeFound為false代表轉檔器目前尚未取得,將於首次轉檔時由w-html2docx自動下載,並非不可用。
|
|
195
199
|
*
|
|
196
|
-
*
|
|
200
|
+
* 注意:僅代表環境條件成立,不代表轉檔器可下載成功或Word確實可被調用;後二者須實際轉一份小檔驗證。
|
|
197
201
|
*
|
|
198
|
-
* @returns {Object} 回傳狀態物件{platform,isWindows,cwd,exePath,exeFound,ready}
|
|
202
|
+
* @returns {Object} 回傳狀態物件{platform,isWindows,cwd,exePath,exeFound,ready},exePath與exeFound為轉檔器目前所在位置與是否已存在(診斷用)
|
|
199
203
|
* @example
|
|
200
204
|
*
|
|
201
205
|
* console.log(checkDocxReady())
|
|
202
|
-
* // => { platform: 'win32', isWindows: true, cwd: '...', exePath: '
|
|
206
|
+
* // => { platform: 'win32', isWindows: true, cwd: '...', exePath: '', exeFound: false, ready: true }
|
|
203
207
|
*
|
|
204
208
|
*/
|
|
205
209
|
function checkDocxReady() {
|
|
206
210
|
let fpExe = getFpExe()
|
|
211
|
+
let isWindows = process.platform === 'win32'
|
|
207
212
|
return {
|
|
208
213
|
platform: process.platform,
|
|
209
|
-
isWindows
|
|
214
|
+
isWindows,
|
|
210
215
|
cwd: path.resolve(),
|
|
211
216
|
exePath: fpExe,
|
|
212
217
|
exeFound: fpExe !== '',
|
|
213
|
-
ready:
|
|
218
|
+
ready: isWindows,
|
|
214
219
|
}
|
|
215
220
|
}
|
|
216
221
|
|
|
@@ -7,6 +7,32 @@ import rmApiClient, { cvMdTo, health, scanAssetPaths, readAssets } from '../src/
|
|
|
7
7
|
|
|
8
8
|
describe('rmApiClient', function() {
|
|
9
9
|
|
|
10
|
+
let fdTmpRoot = path.resolve('./test/_tmp/api-rmApiClient')
|
|
11
|
+
|
|
12
|
+
after(function() {
|
|
13
|
+
fs.rmSync(fdTmpRoot, { recursive: true, force: true, maxRetries: 10, retryDelay: 300 })
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
//withoutEnv: 暫時移除服務位置之環境變數後執行, 使預設值測試不受執行環境影響
|
|
17
|
+
let withoutEnv = async (fun) => {
|
|
18
|
+
let keys = ['WMD2DOCX_URL', 'WMD2DOCX_HOST', 'WMD2DOCX_PORT']
|
|
19
|
+
let kp = {}
|
|
20
|
+
for (let k of keys) {
|
|
21
|
+
kp[k] = process.env[k]
|
|
22
|
+
delete process.env[k]
|
|
23
|
+
}
|
|
24
|
+
try {
|
|
25
|
+
return await fun()
|
|
26
|
+
}
|
|
27
|
+
finally {
|
|
28
|
+
for (let k of keys) {
|
|
29
|
+
if (kp[k] !== undefined) {
|
|
30
|
+
process.env[k] = kp[k]
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
10
36
|
describe('scanAssetPaths(純函數)', function() {
|
|
11
37
|
|
|
12
38
|
it('涵蓋<img src>與![]() , 去重, 保留原字串', function() {
|
|
@@ -48,7 +74,7 @@ describe('rmApiClient', function() {
|
|
|
48
74
|
|
|
49
75
|
describe('readAssets(讀本機檔)', function() {
|
|
50
76
|
|
|
51
|
-
let fdTmp = path.resolve('
|
|
77
|
+
let fdTmp = path.resolve(fdTmpRoot, 'readAssets')
|
|
52
78
|
|
|
53
79
|
before(function() {
|
|
54
80
|
fs.mkdirSync(path.resolve(fdTmp, 'pics'), { recursive: true })
|
|
@@ -56,10 +82,6 @@ describe('rmApiClient', function() {
|
|
|
56
82
|
fs.writeFileSync(path.resolve(fdTmp, 'pics/a b.png'), 'IMG2')
|
|
57
83
|
})
|
|
58
84
|
|
|
59
|
-
after(function() {
|
|
60
|
-
fs.rmSync(fdTmp, { recursive: true, force: true })
|
|
61
|
-
})
|
|
62
|
-
|
|
63
85
|
it('URL編碼引用可對應解碼後實體檔, path保留原字串', function() {
|
|
64
86
|
let { assets, missing } = readAssets('\n<img src="pics/a%20b.png">', fdTmp)
|
|
65
87
|
assert.strict.deepEqual(missing, [])
|
|
@@ -83,27 +105,25 @@ describe('rmApiClient', function() {
|
|
|
83
105
|
|
|
84
106
|
describe('cvMdTo/health(對接rmApiServer, html路徑不需Word)', function() {
|
|
85
107
|
|
|
86
|
-
let fdTmp = path.resolve('
|
|
108
|
+
let fdTmp = path.resolve(fdTmpRoot, 'service')
|
|
87
109
|
let dirWork = path.resolve(fdTmp, 'work')
|
|
88
110
|
let token = 'tk-client'
|
|
89
111
|
let srv = null
|
|
90
112
|
let host = '127.0.0.1'
|
|
91
|
-
let port =
|
|
92
|
-
let url =
|
|
113
|
+
let port = 8211 //固定埠(8000以上), 與api-rmApiServer(8201, 8202)錯開以免並行撞埠
|
|
114
|
+
let url = `http://${host}:${port}`
|
|
115
|
+
let hostInvalid = 'nonexistent.invalid' //.invalid為保留網域(RFC 2606)必定解析失敗, 使預設埠之驗證不受本機22000埠是否有服務影響
|
|
93
116
|
let fpOutHtml = path.resolve(fdTmp, 'out/report.html')
|
|
94
117
|
|
|
95
118
|
before(async function() {
|
|
96
119
|
fs.mkdirSync(fdTmp, { recursive: true })
|
|
97
|
-
srv = await rmApiServer({ port
|
|
98
|
-
port = srv.server.info.port
|
|
99
|
-
url = `http://${host}:${port}`
|
|
120
|
+
srv = await rmApiServer({ port, host, token, dirWork })
|
|
100
121
|
})
|
|
101
122
|
|
|
102
123
|
after(async function() {
|
|
103
124
|
if (srv) {
|
|
104
125
|
await srv.stop()
|
|
105
126
|
}
|
|
106
|
-
fs.rmSync(fdTmp, { recursive: true, force: true })
|
|
107
127
|
})
|
|
108
128
|
|
|
109
129
|
it('default export匯整四函數', function() {
|
|
@@ -131,12 +151,17 @@ describe('rmApiClient', function() {
|
|
|
131
151
|
}
|
|
132
152
|
})
|
|
133
153
|
|
|
134
|
-
it('host
|
|
135
|
-
await
|
|
154
|
+
it('host與環境變數皆未給時預設127.0.0.1', async function() {
|
|
155
|
+
let r = await withoutEnv(() => health({ port, token })) //僅給port, 須連到127.0.0.1上之測試服務
|
|
156
|
+
assert.strict.equal(r.success, true)
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
it('port與環境變數皆未給時預設22000(與rmApiServer預設埠一致)', async function() {
|
|
160
|
+
await withoutEnv(() => assert.rejects(health({ host: hostInvalid, token, timeoutMs: 3000 }), (e) => String(e).startsWith(`Unable to connect to the conversion service http://${hostInvalid}:22000: `)))
|
|
136
161
|
})
|
|
137
162
|
|
|
138
163
|
it('port非正整數視為未給', async function() {
|
|
139
|
-
await assert.rejects(health({ host, port: 'x', token, timeoutMs: 3000 }), (e) =>
|
|
164
|
+
await withoutEnv(() => assert.rejects(health({ host: hostInvalid, port: 'x', token, timeoutMs: 3000 }), (e) => String(e).startsWith(`Unable to connect to the conversion service http://${hostInvalid}:22000: `)))
|
|
140
165
|
})
|
|
141
166
|
|
|
142
167
|
it('url給予時覆寫host/port', async function() {
|
|
@@ -7,10 +7,12 @@ import rmApiServer from '../src/rmApiServer.mjs'
|
|
|
7
7
|
//rmApiServer: 直打HTTP端點(不經UI); docx相關以html路徵替代, 不需Word
|
|
8
8
|
describe('rmApiServer', function() {
|
|
9
9
|
|
|
10
|
-
let fdTmp = path.resolve('./
|
|
10
|
+
let fdTmp = path.resolve('./test/_tmp/api-rmApiServer')
|
|
11
11
|
let dirWork = path.resolve(fdTmp, 'work')
|
|
12
12
|
let token = 'tk-test'
|
|
13
13
|
let svgB64 = fs.readFileSync('./test/cocktail.svg').toString('base64')
|
|
14
|
+
let portMain = 8201 //固定埠(8000以上), 與api-rmApiClient(8211)錯開以免並行撞埠
|
|
15
|
+
let portStop = 8202
|
|
14
16
|
let srv = null
|
|
15
17
|
let url = ''
|
|
16
18
|
|
|
@@ -24,19 +26,20 @@ describe('rmApiServer', function() {
|
|
|
24
26
|
|
|
25
27
|
before(async function() {
|
|
26
28
|
fs.mkdirSync(fdTmp, { recursive: true })
|
|
27
|
-
srv = await rmApiServer({ port:
|
|
28
|
-
url = `http://127.0.0.1:${
|
|
29
|
+
srv = await rmApiServer({ port: portMain, host: '127.0.0.1', token, dirWork })
|
|
30
|
+
url = `http://127.0.0.1:${portMain}`
|
|
29
31
|
})
|
|
30
32
|
|
|
31
33
|
after(async function() {
|
|
32
34
|
if (srv) {
|
|
33
35
|
await srv.stop()
|
|
34
36
|
}
|
|
35
|
-
fs.rmSync(fdTmp, { recursive: true, force: true })
|
|
37
|
+
fs.rmSync(fdTmp, { recursive: true, force: true, maxRetries: 10, retryDelay: 300 })
|
|
36
38
|
})
|
|
37
39
|
|
|
38
40
|
it('回傳物件與settings', function() {
|
|
39
|
-
assert.strict.equal(
|
|
41
|
+
assert.strict.equal(srv.server.info.port, portMain)
|
|
42
|
+
assert.strict.equal(srv.settings.port, portMain)
|
|
40
43
|
assert.strict.equal(typeof srv.getState, 'function')
|
|
41
44
|
assert.strict.equal(typeof srv.stop, 'function')
|
|
42
45
|
assert.strict.equal(srv.settings.host, '127.0.0.1')
|
|
@@ -180,8 +183,8 @@ describe('rmApiServer', function() {
|
|
|
180
183
|
})
|
|
181
184
|
|
|
182
185
|
it('stop後不再回應', async function() {
|
|
183
|
-
let s3 = await rmApiServer({ port:
|
|
184
|
-
let u3 = `http://127.0.0.1:${
|
|
186
|
+
let s3 = await rmApiServer({ port: portStop, host: '127.0.0.1', dirWork: path.resolve(fdTmp, 'work3') })
|
|
187
|
+
let u3 = `http://127.0.0.1:${portStop}`
|
|
185
188
|
let res = await fetch(`${u3}/api/health`)
|
|
186
189
|
assert.strict.equal(res.status, 200) //無token時不需標頭
|
|
187
190
|
await s3.stop()
|