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