w-md2docx 1.0.6 → 1.0.8

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.
@@ -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 14:34:56 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
91
+ Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Fri Sep 18 2026 15:31:24 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
92
92
  </footer>
93
93
 
94
94
  <script>prettyPrint();</script>
@@ -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 14:34:56 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
278
+ Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Fri Sep 18 2026 15:31:24 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
279
279
  </footer>
280
280
 
281
281
  <script>prettyPrint();</script>
@@ -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, getFpExe } from './utils.mjs'
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
- * 注意:底層w-html2docx於Word未安裝或COM呼叫失敗時「仍回傳ok」,本函數一律以產物實體檔之存在與大小驗證成敗,失敗即reject,不會回報假成功。
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
- //why: w-html2docx Word 未安裝、COM 呼叫失敗等情形「仍回傳 ok」(錯誤僅印於其子程序之輸出),
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 14:34:56 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
235
+ Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Fri Sep 18 2026 15:31:24 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轉檔條件(Windows且轉檔器存在)</p>
226
- <p>注意:僅代表環境條件成立,不代表Word確實可被調用;後者須實際轉一份小檔驗證。</p></li></ul></dd>
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#line205">line 205</a>
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: '...', exeFound: true, ready: true }</code></pre>
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#line369">line 369</a>
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 -&gt; html -&gt; docx。docx階段係調用本機Microsoft Word(win32com),故僅能於已安裝Word之Windows執行,且同時間僅一份轉檔作業(內部佇列自動排隊)。</p>
2071
- <p>注意:底層w-html2docx於Word未安裝或COM呼叫失敗時「仍回傳ok」,本函數一律以產物實體檔之存在與大小驗證成敗,失敗即reject,不會回報假成功。</p></li></ul></dd>
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#line42">line 42</a>
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位置</p>
2526
- <p>注意:w-html2docx內部以「模組載入當下之工作目錄(cwd)」推導exe位置,故此處以同一規則檢查,使事前檢查與實際轉檔行為一致;查無即代表由此cwd執行必定轉檔失敗。</p></li></ul></dd>
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#line178">line 178</a>
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位置字串,查無則回傳空字串</p>
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#line288">line 288</a>
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#line234">line 234</a>
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#line422">line 422</a>
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#line327">line 327</a>
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 14:34:56 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
5446
+ Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Fri Sep 18 2026 15:31:24 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 14:34:56 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
74
+ Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Fri Sep 18 2026 15:31:24 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 14:34:56 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
529
+ Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Fri Sep 18 2026 15:31:24 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, //僅代表環境具備條件, 實際 Word 可用性須以 /api/selftest 驗證
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 14:34:56 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
506
+ Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Fri Sep 18 2026 15:31:24 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
507
507
  </footer>
508
508
 
509
509
  <script>prettyPrint();</script>
@@ -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
- * 注意:w-html2docx內部以「模組載入當下之工作目錄(cwd)」推導exe位置,故此處以同一規則檢查,使事前檢查與實際轉檔行為一致;查無即代表由此cwd執行必定轉檔失敗。
216
+ * w-html2docx之查找位置(cwd之src/、cwd之node_modules/w-html2docx/src/)查詢檔案「目前」是否存在,不下載。
217
217
  *
218
- * @returns {String} 回傳exe位置字串,查無則回傳空字串
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轉檔條件(Windows且轉檔器存在)
243
+ * 檢查本機是否具備docx轉檔條件
244
+ *
245
+ * ready僅以作業系統為Windows判定,不以轉檔器是否存在為條件:exeFound為false代表轉檔器目前尚未取得,將於首次轉檔時由w-html2docx自動下載,並非不可用。
242
246
  *
243
- * 注意:僅代表環境條件成立,不代表Word確實可被調用;後者須實際轉一份小檔驗證。
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: '...', exeFound: true, ready: true }
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: process.platform === 'win32',
261
+ isWindows,
257
262
  cwd: path.resolve(),
258
263
  exePath: fpExe,
259
264
  exeFound: fpExe !== '',
260
- ready: process.platform === 'win32' &amp;&amp; fpExe !== '',
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 14:34:56 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
515
+ Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Fri Sep 18 2026 15:31:24 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.6",
3
+ "version": "1.0.8",
4
4
  "main": "dist/w-md2docx.umd.js",
5
5
  "dependencies": {
6
6
  "@hapi/hapi": "^21.4.10",
7
- "w-html2docx": "^1.0.28",
8
- "w-md2html": "^1.0.51",
9
- "wsemi": "^1.8.92"
7
+ "w-html2docx": "^1.0.30",
8
+ "w-md2html": "^1.1.1",
9
+ "wsemi": "^1.9.0"
10
10
  },
11
11
  "devDependencies": {
12
12
  "w-package-tools": "^1.1.16"
@@ -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, getFpExe } from './utils.mjs'
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
- * 注意:底層w-html2docx於Word未安裝或COM呼叫失敗時「仍回傳ok」,本函數一律以產物實體檔之存在與大小驗證成敗,失敗即reject,不會回報假成功。
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
- //why: w-html2docx Word 未安裝、COM 呼叫失敗等情形「仍回傳 ok」(錯誤僅印於其子程序之輸出),
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
  }
@@ -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, //僅代表環境具備條件, 實際 Word 可用性須以 /api/selftest 驗證
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
- * 注意:w-html2docx內部以「模組載入當下之工作目錄(cwd)」推導exe位置,故此處以同一規則檢查,使事前檢查與實際轉檔行為一致;查無即代表由此cwd執行必定轉檔失敗。
169
+ * w-html2docx之查找位置(cwd之src/、cwd之node_modules/w-html2docx/src/)查詢檔案「目前」是否存在,不下載。
170
170
  *
171
- * @returns {String} 回傳exe位置字串,查無則回傳空字串
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轉檔條件(Windows且轉檔器存在)
196
+ * 檢查本機是否具備docx轉檔條件
197
+ *
198
+ * ready僅以作業系統為Windows判定,不以轉檔器是否存在為條件:exeFound為false代表轉檔器目前尚未取得,將於首次轉檔時由w-html2docx自動下載,並非不可用。
195
199
  *
196
- * 注意:僅代表環境條件成立,不代表Word確實可被調用;後者須實際轉一份小檔驗證。
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: '...', exeFound: true, ready: true }
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: process.platform === 'win32',
214
+ isWindows,
210
215
  cwd: path.resolve(),
211
216
  exePath: fpExe,
212
217
  exeFound: fpExe !== '',
213
- ready: process.platform === 'win32' && fpExe !== '',
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('./tmp/zt_rmApiClient_ra')
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('![p](pics/%E5%9C%96.png)\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('./tmp/zt_rmApiClient')
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 = 0
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: 0, host, token, dirWork })
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/port與環境變數皆未給時預設127.0.0.1:22000(與rmApiServer預設埠一致)', async function() {
135
- await assert.rejects(health({ token, timeoutMs: 3000 }), (e) => /^Unable to connect to the conversion service http:\/\/127\.0\.0\.1:22000: /.test(e) || /^Invalid response from the conversion service http:\/\/127\.0\.0\.1:22000/.test(e))
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) => /http:\/\/127\.0\.0\.1:22000/.test(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('./tmp/zt_rmApiServer')
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: 0, host: '127.0.0.1', token, dirWork }) //port 0 由系統配置, 避免並行測試撞埠
28
- url = `http://127.0.0.1:${srv.server.info.port}`
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(typeof srv.server.info.port, 'number')
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: 0, host: '127.0.0.1', dirWork: path.resolve(fdTmp, 'work3') })
184
- let u3 = `http://127.0.0.1:${s3.server.info.port}`
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()