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,539 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+
5
+ <meta charset="utf-8">
6
+ <title>ApiClient.mjs - Documentation</title>
7
+
8
+
9
+ <script src="scripts/prettify/prettify.js"></script>
10
+ <script src="scripts/prettify/lang-css.js"></script>
11
+ <!--[if lt IE 9]>
12
+ <script src="//html5shiv.googlecode.com/svn/trunk/html5.js"></script>
13
+ <![endif]-->
14
+ <link type="text/css" rel="stylesheet" href="styles/prettify.css">
15
+ <link type="text/css" rel="stylesheet" href="styles/jsdoc.css">
16
+ <script src="scripts/nav.js" defer></script>
17
+
18
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
19
+ </head>
20
+ <body>
21
+
22
+ <input type="checkbox" id="nav-trigger" class="nav-trigger" />
23
+ <label for="nav-trigger" class="navicon-button x">
24
+ <div class="navicon"></div>
25
+ </label>
26
+
27
+ <label for="nav-trigger" class="overlay"></label>
28
+
29
+ <nav >
30
+
31
+
32
+ <h2><a href="index.html">Home</a></h2><h3>Global</h3><ul><li><a href="global.html#ApiServer">ApiServer</a></li><li><a href="global.html#WMd2docx">WMd2docx</a></li><li><a href="global.html#checkDocxReady">checkDocxReady</a></li><li><a href="global.html#cleanWorkDir">cleanWorkDir</a></li><li><a href="global.html#cvMdTo">cvMdTo</a></li><li><a href="global.html#cvMdToDocx">cvMdToDocx</a></li><li><a href="global.html#getFpExe">getFpExe</a></li><li><a href="global.html#getQueueSize">getQueueSize</a></li><li><a href="global.html#health">health</a></li><li><a href="global.html#readAssets">readAssets</a></li><li><a href="global.html#resolveAssetPaths">resolveAssetPaths</a></li><li><a href="global.html#retryBusy">retryBusy</a></li><li><a href="global.html#runExclusive">runExclusive</a></li><li><a href="global.html#scanAssetPaths">scanAssetPaths</a></li><li><a href="global.html#toErrText">toErrText</a></li><li><a href="global.html#toSafeName">toSafeName</a></li><li><a href="global.html#transErrAsset">transErrAsset</a></li><li><a href="global.html#writeAssets">writeAssets</a></li></ul>
33
+
34
+ </nav>
35
+
36
+ <div id="main">
37
+
38
+ <h1 class="page-title">ApiClient.mjs</h1>
39
+
40
+
41
+
42
+
43
+
44
+
45
+
46
+ <section>
47
+ <article>
48
+ <pre class="prettyprint source linenums"><code>import fs from 'fs'
49
+ import path from 'path'
50
+ import get from 'lodash-es/get.js'
51
+ import isstr from 'wsemi/src/isstr.mjs'
52
+ import isestr from 'wsemi/src/isestr.mjs'
53
+ import isbol from 'wsemi/src/isbol.mjs'
54
+ import isobj from 'wsemi/src/isobj.mjs'
55
+ import ispnum from 'wsemi/src/ispnum.mjs'
56
+ import ispint from 'wsemi/src/ispint.mjs'
57
+ import isp0int from 'wsemi/src/isp0int.mjs'
58
+ import cint from 'wsemi/src/cint.mjs'
59
+ import cdbl from 'wsemi/src/cdbl.mjs'
60
+ import fsIsFile from 'wsemi/src/fsIsFile.mjs'
61
+ import { toErrText } from './utils.mjs'
62
+
63
+
64
+ //hostDef, portDef: 服務位置預設值(與 ApiServer 預設埠一致)
65
+ let hostDef = '127.0.0.1'
66
+ let portDef = 22000
67
+
68
+
69
+ //getUrl: 取服務位置
70
+ //優先序: opt.url(完整位址, 含協定與可能之路徑前綴) > opt.host + opt.port(可各自獨立給, 缺者依序取環境變數 WMD2DOCX_HOST / WMD2DOCX_PORT 再取預設值)
71
+ function getUrl(opt) {
72
+
73
+ //url
74
+ let url = get(opt, 'url', '')
75
+ if (isestr(url)) {
76
+ return url.trim().replace(/\/+$/, '')
77
+ }
78
+
79
+ //host
80
+ let host = get(opt, 'host', '')
81
+ if (!isestr(host)) {
82
+ host = get(process, 'env.WMD2DOCX_HOST', '')
83
+ }
84
+ if (!isestr(host)) {
85
+ host = hostDef
86
+ }
87
+ host = host.trim()
88
+
89
+ //port
90
+ let port = get(opt, 'port', null)
91
+ if (!ispint(port)) {
92
+ port = get(process, 'env.WMD2DOCX_PORT', null)
93
+ }
94
+ if (!ispint(port)) {
95
+ port = portDef
96
+ }
97
+ port = cint(port)
98
+
99
+ return `http://${host}:${port}`
100
+ }
101
+
102
+
103
+ //getToken: 取權杖(參數 > 環境變數 WMD2DOCX_TOKEN > 空字串)
104
+ function getToken(token) {
105
+ if (!isestr(token)) {
106
+ token = get(process, 'env.WMD2DOCX_TOKEN', '')
107
+ }
108
+ if (!isestr(token)) {
109
+ token = ''
110
+ }
111
+ return token
112
+ }
113
+
114
+
115
+ /**
116
+ * 掃描Markdown內引用之本機相對路徑資產(圖片等)
117
+ *
118
+ * 涵蓋html之&amp;lt;img src="…"&amp;gt;與markdown之![](…);排除網路位址、data協定、mailto、錨點與絕對路徑,並去除錨點與查詢字串。
119
+ *
120
+ * @param {String} md 輸入Markdown內容字串
121
+ * @returns {Array} 回傳相對路徑字串陣列(已去重)
122
+ * @example
123
+ *
124
+ * console.log(scanAssetPaths('![a](pics/a.png)\n&lt;img src="pics/b.png" />\n![c](https://x.com/c.png)'))
125
+ * // => [ 'pics/a.png', 'pics/b.png' ]
126
+ *
127
+ */
128
+ function scanAssetPaths(md) {
129
+ let ps = new Set()
130
+
131
+ if (!isstr(md)) {
132
+ return []
133
+ }
134
+
135
+ let add = (v) => {
136
+ if (!isstr(v)) {
137
+ return
138
+ }
139
+ let p = v.trim().replace(/^&lt;|>$/g, '')
140
+ if (p === '') {
141
+ return
142
+ }
143
+ if (/^(https?:)?\/\//i.test(p) || /^data:/i.test(p) || /^mailto:/i.test(p) || /^#/.test(p)) {
144
+ return //網路位址、內嵌資料與錨點不需夾帶
145
+ }
146
+ if (/^[a-zA-Z]:/.test(p) || p.startsWith('/')) {
147
+ return //絕對路徑不夾帶(服務端一律拒收)
148
+ }
149
+ p = p.split('#')[0].split('?')[0] //去除錨點與查詢字串
150
+ if (p === '') {
151
+ return
152
+ }
153
+ ps.add(p)
154
+ }
155
+
156
+ for (let m of md.matchAll(/&lt;img[^>]*?\ssrc\s*=\s*["']([^"']+)["']/gi)) {
157
+ add(m[1])
158
+ }
159
+ for (let m of md.matchAll(/!\[[^\]]*\]\(\s*([^)\s]+)/g)) {
160
+ add(m[1])
161
+ }
162
+
163
+ return [...ps]
164
+ }
165
+
166
+
167
+ /**
168
+ * 讀取Markdown引用之資產檔並轉base64
169
+ *
170
+ * path一律回傳md內之原字串(服務端據以還原相對路徑);實體檔則原字串與URL解碼後字串皆嘗試。
171
+ *
172
+ * @param {String} md 輸入Markdown內容字串
173
+ * @param {String} dirMd 輸入Markdown所在資料夾位置字串,相對路徑以此為基準
174
+ * @returns {Object} 回傳{assets,missing},assets為[{path,base64}],missing為找不到之相對路徑陣列
175
+ * @example
176
+ *
177
+ * let { assets, missing } = readAssets('![a](pics/a.png)', './report')
178
+ * // => { assets: [ { path: 'pics/a.png', base64: '…' } ], missing: [] }
179
+ *
180
+ */
181
+ function readAssets(md, dirMd) {
182
+ let assets = []
183
+ let missing = []
184
+
185
+ if (!isestr(dirMd)) {
186
+ dirMd = '.'
187
+ }
188
+
189
+ for (let p of scanAssetPaths(md)) {
190
+
191
+ //候選實體路徑(md 內可能為 URL 編碼之中文檔名)
192
+ let cands = [p]
193
+ try {
194
+ let dec = decodeURIComponent(p)
195
+ if (dec !== p) {
196
+ cands.push(dec)
197
+ }
198
+ }
199
+ catch (err) {
200
+ //非法 percent 序列, 略過解碼候選
201
+ }
202
+
203
+ let fpHit = ''
204
+ for (let c of cands) {
205
+ let fp = path.resolve(dirMd, c)
206
+ if (fsIsFile(fp)) {
207
+ fpHit = fp
208
+ break
209
+ }
210
+ }
211
+
212
+ if (fpHit === '') {
213
+ missing.push(p)
214
+ continue
215
+ }
216
+
217
+ assets.push({
218
+ path: p,
219
+ base64: fs.readFileSync(fpHit).toString('base64'),
220
+ })
221
+
222
+ }
223
+ return { assets, missing }
224
+ }
225
+
226
+
227
+ /**
228
+ * 將本機Markdown檔送至轉檔服務,取回並寫出Html與(或)Docx
229
+ *
230
+ * 供無Microsoft Word之機器產製docx:讀取本機md與其引用之圖片,送至ApiServer轉檔後寫回本機。連線層失敗會自動重試;逾時與HTTP錯誤不重試。
231
+ *
232
+ * @param {Object} [opt={}] 輸入設定物件,預設{}
233
+ * @param {String} [opt.host='127.0.0.1'] 輸入服務主機位址(ip或網域)字串,未給則取環境變數WMD2DOCX_HOST,再無則用'127.0.0.1',預設'127.0.0.1'
234
+ * @param {Integer} [opt.port=22000] 輸入服務埠號整數,未給則取環境變數WMD2DOCX_PORT,再無則用22000,預設22000
235
+ * @param {String} [opt.url=''] 輸入服務完整位址字串(如'https://host/prefix'),給予時覆寫host與port,預設''
236
+ * @param {String} opt.fpInMd 輸入來源Markdown檔位置字串
237
+ * @param {String} [opt.fpOutDocx=''] 輸入轉出Docx檔位置字串,與fpOutHtml皆未給時預設於md旁產出同名docx,預設''
238
+ * @param {String} [opt.fpOutHtml=''] 輸入轉出Html檔位置字串,預設''
239
+ * @param {String} [opt.fpInTemp=''] 輸入本機Docx模板檔位置字串,給予時以base64夾帶至服務端,預設''
240
+ * @param {String} [opt.templateName=''] 輸入服務端模板檔名字串,預設''
241
+ * @param {String} [opt.name=''] 輸入輸出檔名主體字串,未給則取md檔名,預設''
242
+ * @param {String} [opt.token=''] 輸入x-api-token字串,未給則取環境變數WMD2DOCX_TOKEN,預設''
243
+ * @param {Object} [opt.optMd2html] 輸入傳予w-md2html之設定物件
244
+ * @param {Object} [opt.optHtml2docx] 輸入傳予w-html2docx之設定物件
245
+ * @param {Boolean} [opt.allowMissingAssets=false] 輸入是否容許md引用之資產檔不存在布林值,預設false
246
+ * @param {Number} [opt.timeoutMs=600000] 輸入逾時毫秒數,轉檔含Word啟動故預設10分鐘,預設600000
247
+ * @param {Integer} [opt.retries=3] 輸入連線層失敗之重試次數整數,預設3
248
+ * @returns {Promise} 回傳Promise,resolve回傳結果物件{ms,nAssets,template,[html],[docx]},其中html與docx為{fp,size},reject回傳錯誤訊息
249
+ * @example
250
+ *
251
+ * import { cvMdTo } from 'w-md2docx/src/ApiClient.mjs'
252
+ *
253
+ * let r = await cvMdTo({
254
+ * host: '127.0.0.1',
255
+ * port: 22000,
256
+ * fpInMd: './test/report.md',
257
+ * fpOutDocx: './test/report.docx',
258
+ * })
259
+ * console.log(r)
260
+ * // => { ms: 8342, nAssets: 1, template: 'default', docx: { fp: '…', size: 39856 } }
261
+ *
262
+ */
263
+ async function cvMdTo(opt = {}) {
264
+
265
+ //url
266
+ let url = getUrl(opt)
267
+
268
+ //fpInMd
269
+ let fpInMd = get(opt, 'fpInMd', '')
270
+ if (!isestr(fpInMd)) {
271
+ return Promise.reject('fpInMd must be a non-empty string')
272
+ }
273
+ fpInMd = path.resolve(fpInMd)
274
+ if (!fsIsFile(fpInMd)) {
275
+ return Promise.reject(`fpInMd[${fpInMd}] does not exist`)
276
+ }
277
+
278
+ //fpOutDocx, fpOutHtml
279
+ let fpOutDocx = get(opt, 'fpOutDocx', '')
280
+ fpOutDocx = isestr(fpOutDocx) ? path.resolve(fpOutDocx) : ''
281
+ let fpOutHtml = get(opt, 'fpOutHtml', '')
282
+ fpOutHtml = isestr(fpOutHtml) ? path.resolve(fpOutHtml) : ''
283
+ if (fpOutDocx === '' &amp;&amp; fpOutHtml === '') {
284
+ //未指定輸出時, 預設於 md 旁產出同名 docx
285
+ fpOutDocx = path.resolve(path.dirname(fpInMd), `${path.basename(fpInMd).replace(/\.md$/i, '')}.docx`)
286
+ }
287
+
288
+ //out (依欲寫出之檔案決定)
289
+ let out = 'docx'
290
+ if (fpOutDocx !== '' &amp;&amp; fpOutHtml !== '') {
291
+ out = 'both'
292
+ }
293
+ else if (fpOutHtml !== '') {
294
+ out = 'html'
295
+ }
296
+
297
+ //allowMissingAssets
298
+ let allowMissingAssets = get(opt, 'allowMissingAssets', false)
299
+ if (!isbol(allowMissingAssets)) {
300
+ allowMissingAssets = false
301
+ }
302
+
303
+ //md 與資產
304
+ let md = fs.readFileSync(fpInMd, 'utf8')
305
+ let dirMd = path.dirname(fpInMd)
306
+ let { assets, missing } = readAssets(md, dirMd)
307
+ if (missing.length > 0 &amp;&amp; !allowMissingAssets) {
308
+ //why: 缺圖不擋下會靜默產出破圖之 docx, 交付後才被發現
309
+ return Promise.reject(`${missing.length} asset file(s) referenced in md do not exist: ${missing.join(', ')}. Set allowMissingAssets:true to skip this check`)
310
+ }
311
+
312
+ //name (輸出檔名主體, 未指定則取 md 檔名)
313
+ let name = get(opt, 'name', '')
314
+ if (!isestr(name)) {
315
+ name = path.basename(fpInMd).replace(/\.md$/i, '')
316
+ }
317
+
318
+ //body
319
+ let body = {
320
+ md,
321
+ name,
322
+ out,
323
+ assets,
324
+ }
325
+ let optMd2html = get(opt, 'optMd2html', null)
326
+ if (isobj(optMd2html)) {
327
+ body.optMd2html = optMd2html
328
+ }
329
+ let optHtml2docx = get(opt, 'optHtml2docx', null)
330
+ if (isobj(optHtml2docx)) {
331
+ body.optHtml2docx = optHtml2docx
332
+ }
333
+ let templateName = get(opt, 'templateName', '')
334
+ if (isestr(templateName)) {
335
+ body.templateName = templateName.trim()
336
+ }
337
+ let fpInTemp = get(opt, 'fpInTemp', '')
338
+ if (isestr(fpInTemp)) {
339
+ fpInTemp = path.resolve(fpInTemp)
340
+ if (!fsIsFile(fpInTemp)) {
341
+ return Promise.reject(`fpInTemp[${fpInTemp}] does not exist`)
342
+ }
343
+ body.templateBase64 = fs.readFileSync(fpInTemp).toString('base64')
344
+ }
345
+
346
+ //headers
347
+ let headers = { 'content-type': 'application/json' }
348
+ let token = getToken(get(opt, 'token', ''))
349
+ if (token !== '') {
350
+ headers['x-api-token'] = token
351
+ }
352
+
353
+ //timeoutMs (轉檔含 Word 啟動, 預設 10 分鐘)
354
+ let timeoutMs = get(opt, 'timeoutMs', null)
355
+ if (!ispnum(timeoutMs)) {
356
+ timeoutMs = 600000
357
+ }
358
+ timeoutMs = cdbl(timeoutMs)
359
+
360
+ //retries (連線層失敗之重試次數)
361
+ let retries = get(opt, 'retries', null)
362
+ if (!isp0int(retries)) {
363
+ retries = 3
364
+ }
365
+ retries = cint(retries)
366
+
367
+ //post
368
+ //why: 連線層失敗(網路瞬斷、服務端重啟、TCP 佇列滿)屬暫時性, 須重試;
369
+ // 逾時與業務錯誤(HTTP 4xx/5xx)則不重試——前者重試只會更久, 後者重試結果相同
370
+ let cBody = JSON.stringify(body)
371
+ let res = null
372
+ let errLast = ''
373
+ for (let i = 0; i &lt;= retries; i++) {
374
+
375
+ if (i > 0) {
376
+ let msWait = [0, 3000, 8000, 15000][i] || 15000
377
+ console.log(`connection failed, retrying in ${msWait / 1000}s (${i}/${retries}): ${errLast}`)
378
+ await new Promise((resolve) => setTimeout(resolve, msWait))
379
+ }
380
+
381
+ try {
382
+ res = await fetch(`${url}/api/convert`, {
383
+ method: 'POST',
384
+ headers,
385
+ body: cBody,
386
+ signal: AbortSignal.timeout(timeoutMs),
387
+ })
388
+ errLast = ''
389
+ break
390
+ }
391
+ catch (err) {
392
+ let nameErr = get(err, 'name', '')
393
+ errLast = toErrText(err) || 'unknown error'
394
+ if (nameErr === 'TimeoutError' || nameErr === 'AbortError') {
395
+ return Promise.reject(`Conversion timed out (no response within ${timeoutMs} ms): ${url}`)
396
+ }
397
+ }
398
+
399
+ }
400
+ if (errLast !== '') {
401
+ return Promise.reject(`Unable to connect to the conversion service ${url} (retried ${retries} times): ${errLast}`)
402
+ }
403
+
404
+ //rt
405
+ let rt = await res.json().catch(() => null)
406
+ if (!res.ok || !isobj(rt) || rt.success !== true) {
407
+ let msg = get(rt, 'error', '')
408
+ if (!isestr(msg)) {
409
+ msg = `HTTP ${res.status}`
410
+ }
411
+ return Promise.reject(`Conversion failed: ${msg}`)
412
+ }
413
+
414
+ //寫出並驗證產物(以實體檔大小為準)
415
+ let rw = { ms: rt.ms, nAssets: rt.nAssets, template: rt.template }
416
+
417
+ let writeOne = (fpOut, one, tag) => {
418
+ if (fpOut === '') {
419
+ return ''
420
+ }
421
+ let b64 = get(one, 'base64', '')
422
+ if (!isestr(b64)) {
423
+ return `the service did not return ${tag}`
424
+ }
425
+ fs.mkdirSync(path.dirname(fpOut), { recursive: true })
426
+ fs.writeFileSync(fpOut, Buffer.from(b64, 'base64'))
427
+ let size = fs.statSync(fpOut).size
428
+ if (size === 0) {
429
+ return `${tag} is empty after being written: ${fpOut}`
430
+ }
431
+ rw[tag] = { fp: fpOut, size }
432
+ return ''
433
+ }
434
+
435
+ let errWrite = writeOne(fpOutHtml, rt.html, 'html')
436
+ if (errWrite !== '') {
437
+ return Promise.reject(errWrite)
438
+ }
439
+ errWrite = writeOne(fpOutDocx, rt.docx, 'docx')
440
+ if (errWrite !== '') {
441
+ return Promise.reject(errWrite)
442
+ }
443
+
444
+ return rw
445
+ }
446
+
447
+
448
+ /**
449
+ * 查詢轉檔服務之健康狀態
450
+ *
451
+ * @param {Object} [opt={}] 輸入設定物件,預設{}
452
+ * @param {String} [opt.host='127.0.0.1'] 輸入服務主機位址(ip或網域)字串,未給則取環境變數WMD2DOCX_HOST,再無則用'127.0.0.1',預設'127.0.0.1'
453
+ * @param {Integer} [opt.port=22000] 輸入服務埠號整數,未給則取環境變數WMD2DOCX_PORT,再無則用22000,預設22000
454
+ * @param {String} [opt.url=''] 輸入服務完整位址字串,給予時覆寫host與port,預設''
455
+ * @param {String} [opt.token=''] 輸入x-api-token字串,未給則取環境變數WMD2DOCX_TOKEN,預設''
456
+ * @param {Number} [opt.timeoutMs=20000] 輸入逾時毫秒數,預設20000
457
+ * @returns {Promise} 回傳Promise,resolve回傳服務端回傳之狀態物件,reject回傳錯誤訊息
458
+ * @example
459
+ *
460
+ * import { health } from 'w-md2docx/src/ApiClient.mjs'
461
+ *
462
+ * let r = await health({ host: '127.0.0.1', port: 22000 })
463
+ * console.log(r)
464
+ * // => { success: true, platform: 'win32', docxReady: true, … }
465
+ *
466
+ */
467
+ async function health(opt = {}) {
468
+
469
+ //url
470
+ let url = getUrl(opt)
471
+
472
+ //headers
473
+ let headers = {}
474
+ let token = getToken(get(opt, 'token', ''))
475
+ if (token !== '') {
476
+ headers['x-api-token'] = token
477
+ }
478
+
479
+ //timeoutMs
480
+ let timeoutMs = get(opt, 'timeoutMs', null)
481
+ if (!ispnum(timeoutMs)) {
482
+ timeoutMs = 20000
483
+ }
484
+ timeoutMs = cdbl(timeoutMs)
485
+
486
+ //fetch
487
+ let res = null
488
+ try {
489
+ res = await fetch(`${url}/api/health`, { headers, signal: AbortSignal.timeout(timeoutMs) })
490
+ }
491
+ catch (err) {
492
+ return Promise.reject(`Unable to connect to the conversion service ${url}: ${toErrText(err)}`)
493
+ }
494
+
495
+ //rt
496
+ let rt = await res.json().catch(() => null)
497
+ if (!isobj(rt)) {
498
+ return Promise.reject(`Invalid response from the conversion service ${url}: HTTP ${res.status}`)
499
+ }
500
+
501
+ return rt
502
+ }
503
+
504
+
505
+ let ApiClient = {
506
+ cvMdTo,
507
+ health,
508
+ scanAssetPaths,
509
+ readAssets,
510
+ }
511
+
512
+
513
+ export { cvMdTo, health, scanAssetPaths, readAssets }
514
+ export default ApiClient
515
+ </code></pre>
516
+ </article>
517
+ </section>
518
+
519
+
520
+
521
+
522
+
523
+
524
+ </div>
525
+
526
+ <br class="clear">
527
+
528
+ <footer>
529
+ Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Tue Sep 01 2026 16:40:03 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
530
+ </footer>
531
+
532
+ <script>prettyPrint();</script>
533
+ <script src="scripts/polyfill.js"></script>
534
+ <script src="scripts/linenumber.js"></script>
535
+
536
+
537
+
538
+ </body>
539
+ </html>