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,516 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
|
|
5
|
+
<meta charset="utf-8">
|
|
6
|
+
<title>ApiServer.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">ApiServer.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 os from 'os'
|
|
50
|
+
import path from 'path'
|
|
51
|
+
import { fileURLToPath } from 'url'
|
|
52
|
+
import { execFile } from 'child_process'
|
|
53
|
+
import get from 'lodash-es/get.js'
|
|
54
|
+
import isstr from 'wsemi/src/isstr.mjs'
|
|
55
|
+
import isestr from 'wsemi/src/isestr.mjs'
|
|
56
|
+
import isbol from 'wsemi/src/isbol.mjs'
|
|
57
|
+
import isobj from 'wsemi/src/isobj.mjs'
|
|
58
|
+
import isp0int from 'wsemi/src/isp0int.mjs'
|
|
59
|
+
import ispnum from 'wsemi/src/ispnum.mjs'
|
|
60
|
+
import cint from 'wsemi/src/cint.mjs'
|
|
61
|
+
import cdbl from 'wsemi/src/cdbl.mjs'
|
|
62
|
+
import fsIsFile from 'wsemi/src/fsIsFile.mjs'
|
|
63
|
+
import fsIsFolder from 'wsemi/src/fsIsFolder.mjs'
|
|
64
|
+
import Hapi from '@hapi/hapi'
|
|
65
|
+
import cvMdTo from './cvMdTo.mjs'
|
|
66
|
+
import { toErrText, checkDocxReady, getQueueSize, cleanWorkDir } from './utils.mjs'
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
//fdSelf (場景B: 內建模板隨模組所在資料夾, 不依啟動時之相對路徑漂移)
|
|
70
|
+
let fdSelf = path.dirname(fileURLToPath(import.meta.url))
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
//getWinwordCount: 取當前 WINWORD 行程數(診斷用)
|
|
74
|
+
//why: 殘留之 WINWORD 行程會鎖檔並使後續轉檔失敗, 健檢時一併呈現供判斷
|
|
75
|
+
function getWinwordCount() {
|
|
76
|
+
return new Promise((resolve) => {
|
|
77
|
+
if (process.platform !== 'win32') {
|
|
78
|
+
resolve(-1)
|
|
79
|
+
return
|
|
80
|
+
}
|
|
81
|
+
execFile('tasklist', ['/FI', 'IMAGENAME eq WINWORD.EXE', '/NH'], { windowsHide: true, timeout: 8000 }, (err, stdout) => {
|
|
82
|
+
if (err) {
|
|
83
|
+
resolve(-1)
|
|
84
|
+
return
|
|
85
|
+
}
|
|
86
|
+
let n = (String(stdout).match(/WINWORD\.EXE/gi) || []).length
|
|
87
|
+
resolve(n)
|
|
88
|
+
})
|
|
89
|
+
})
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
//toContentDisposition: 組附檔標頭(中文檔名以 RFC5987 之 filename* 提供)
|
|
94
|
+
function toContentDisposition(fileName) {
|
|
95
|
+
let ascii = fileName.replace(/[^\x20-\x7E]/g, '_').replace(/"/g, '')
|
|
96
|
+
return `attachment; filename="${ascii}"; filename*=UTF-8''${encodeURIComponent(fileName)}`
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
//isInputError: 判斷是否為輸入面錯誤(回 400), 其餘視為轉檔或環境錯誤(回 500)
|
|
101
|
+
//note: 前綴須與 cvMdTo/utils 之錯誤訊息一致
|
|
102
|
+
function isInputError(msg) {
|
|
103
|
+
return /^(md is empty|asset|templateName)/.test(msg)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* 建立並啟動Markdown轉Html/Docx之API服務
|
|
109
|
+
*
|
|
110
|
+
* 以hapi提供三個端點:GET /api/health(健康檢查與環境狀態)、GET /api/selftest(實跑一次最小轉檔驗證本機Word可用)、POST /api/convert(轉檔主端點,JSON參數同cvMdTo,另可用templateName指定服務端模板或templateBase64夾帶模板;query帶download=1時單一格式直接回傳二進位檔)。
|
|
111
|
+
*
|
|
112
|
+
* @param {Object} [opt={}] 輸入設定物件,預設{}
|
|
113
|
+
* @param {Integer} [opt.port=22000] 輸入服務埠號整數,給0則由系統自動配置(實際埠號見回傳之server.info.port),預設22000
|
|
114
|
+
* @param {String} [opt.host='0.0.0.0'] 輸入監聽位址字串,預設'0.0.0.0'
|
|
115
|
+
* @param {String} [opt.token=''] 輸入權杖字串,非空時所有/api/*須帶x-api-token標頭,預設''
|
|
116
|
+
* @param {Number} [opt.maxMb=256] 輸入請求大小上限數字,單位MB,預設256
|
|
117
|
+
* @param {String} [opt.dirTemplates=''] 輸入docx模板資料夾位置字串,未給則用本套件src/templates,預設''
|
|
118
|
+
* @param {String} [opt.dirWork=''] 輸入工作資料夾位置字串,未給則用系統暫存夾下之w-md2docx,預設''
|
|
119
|
+
* @param {String} [opt.templateDef='temp_tpc.docx'] 輸入預設模板檔名字串,預設'temp_tpc.docx'
|
|
120
|
+
* @param {Boolean} [opt.autoStart=true] 輸入是否立即啟動服務布林值,預設true
|
|
121
|
+
* @returns {Promise} 回傳Promise,resolve回傳{server,settings,getState,stop},其中server為hapi實例,getState為取環境狀態之async函數,stop為停止服務之async函數,reject回傳錯誤訊息
|
|
122
|
+
* @example
|
|
123
|
+
*
|
|
124
|
+
* import ApiServer from 'w-md2docx/src/ApiServer.mjs'
|
|
125
|
+
*
|
|
126
|
+
* let { server, settings } = await ApiServer({
|
|
127
|
+
* port: 22000,
|
|
128
|
+
* token: '',
|
|
129
|
+
* })
|
|
130
|
+
* console.log(`listening on ${server.info.uri}`, settings)
|
|
131
|
+
*
|
|
132
|
+
*/
|
|
133
|
+
async function ApiServer(opt = {}) {
|
|
134
|
+
|
|
135
|
+
//port (0 為由系統自動配置, 供測試等場景避免撞埠; 實際埠號由 server.info.port 取得)
|
|
136
|
+
let port = get(opt, 'port', null)
|
|
137
|
+
if (!isp0int(port)) {
|
|
138
|
+
port = 22000
|
|
139
|
+
}
|
|
140
|
+
port = cint(port)
|
|
141
|
+
|
|
142
|
+
//host
|
|
143
|
+
let host = get(opt, 'host', '')
|
|
144
|
+
if (!isestr(host)) {
|
|
145
|
+
host = '0.0.0.0'
|
|
146
|
+
}
|
|
147
|
+
host = host.trim()
|
|
148
|
+
|
|
149
|
+
//token
|
|
150
|
+
let token = get(opt, 'token', '')
|
|
151
|
+
if (!isstr(token)) {
|
|
152
|
+
token = ''
|
|
153
|
+
}
|
|
154
|
+
token = token.trim()
|
|
155
|
+
|
|
156
|
+
//maxMb
|
|
157
|
+
let maxMb = get(opt, 'maxMb', null)
|
|
158
|
+
if (!ispnum(maxMb)) {
|
|
159
|
+
maxMb = 256
|
|
160
|
+
}
|
|
161
|
+
maxMb = cdbl(maxMb)
|
|
162
|
+
let maxBytes = Math.round(maxMb * 1024 * 1024)
|
|
163
|
+
|
|
164
|
+
//dirTemplates
|
|
165
|
+
let dirTemplates = get(opt, 'dirTemplates', '')
|
|
166
|
+
if (!isestr(dirTemplates)) {
|
|
167
|
+
dirTemplates = path.resolve(fdSelf, 'templates')
|
|
168
|
+
}
|
|
169
|
+
dirTemplates = path.resolve(dirTemplates)
|
|
170
|
+
|
|
171
|
+
//dirWork
|
|
172
|
+
let dirWork = get(opt, 'dirWork', '')
|
|
173
|
+
if (!isestr(dirWork)) {
|
|
174
|
+
dirWork = path.join(os.tmpdir(), 'w-md2docx')
|
|
175
|
+
}
|
|
176
|
+
dirWork = path.resolve(dirWork)
|
|
177
|
+
|
|
178
|
+
//templateDef
|
|
179
|
+
let templateDef = get(opt, 'templateDef', '')
|
|
180
|
+
if (!isestr(templateDef)) {
|
|
181
|
+
templateDef = 'temp_tpc.docx'
|
|
182
|
+
}
|
|
183
|
+
templateDef = templateDef.trim()
|
|
184
|
+
|
|
185
|
+
//autoStart
|
|
186
|
+
let autoStart = get(opt, 'autoStart', true)
|
|
187
|
+
if (!isbol(autoStart)) {
|
|
188
|
+
autoStart = true
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
//listTemplates: 列出可用之 docx 模板檔名
|
|
192
|
+
let listTemplates = () => {
|
|
193
|
+
if (!fsIsFolder(dirTemplates)) {
|
|
194
|
+
return []
|
|
195
|
+
}
|
|
196
|
+
return fs.readdirSync(dirTemplates).filter((v) => /\.docx$/i.test(v))
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
//getFpTemplateDef: 取預設模板位置(不存在則回空字串)
|
|
200
|
+
let getFpTemplateDef = () => {
|
|
201
|
+
let fp = path.resolve(dirTemplates, path.basename(templateDef))
|
|
202
|
+
return fsIsFile(fp) ? fp : ''
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
//pickTemplate: 決定本次使用之模板
|
|
206
|
+
//順序: 請求端夾帶(base64) > 請求指定之服務端模板名 > 服務端預設模板 > 空(交由底層自行決定)
|
|
207
|
+
//回傳 { fpInTemp, from, fpTmpDel } ; fpTmpDel 為須於用畢刪除之暫存模板檔
|
|
208
|
+
let pickTemplate = (inp) => {
|
|
209
|
+
|
|
210
|
+
//請求端夾帶
|
|
211
|
+
let b64 = get(inp, 'templateBase64', '')
|
|
212
|
+
if (!isestr(b64)) {
|
|
213
|
+
b64 = get(inp, 'template.base64', '')
|
|
214
|
+
}
|
|
215
|
+
if (isestr(b64)) {
|
|
216
|
+
b64 = b64.replace(/^data:[^;]+;base64,/, '')
|
|
217
|
+
fs.mkdirSync(dirWork, { recursive: true })
|
|
218
|
+
let fp = path.resolve(dirWork, `tmpl_${Date.now()}_${Math.random().toString(36).slice(2, 8)}.docx`)
|
|
219
|
+
fs.writeFileSync(fp, Buffer.from(b64, 'base64'))
|
|
220
|
+
return { fpInTemp: fp, from: 'request', fpTmpDel: fp }
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
//指定服務端模板名
|
|
224
|
+
let fn = get(inp, 'templateName', '')
|
|
225
|
+
if (isestr(fn)) {
|
|
226
|
+
fn = fn.trim()
|
|
227
|
+
}
|
|
228
|
+
else {
|
|
229
|
+
fn = ''
|
|
230
|
+
}
|
|
231
|
+
if (fn !== '') {
|
|
232
|
+
let fp = path.resolve(dirTemplates, path.basename(fn)) //basename 化, 限定取用 templates 內之檔案
|
|
233
|
+
if (!fsIsFile(fp)) {
|
|
234
|
+
throw new Error(`templateName[${fn}] does not exist (available templates: ${listTemplates().join(', ') || 'none'})`)
|
|
235
|
+
}
|
|
236
|
+
return { fpInTemp: fp, from: 'server', fpTmpDel: '' }
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
//服務端預設模板
|
|
240
|
+
let fpDef = getFpTemplateDef()
|
|
241
|
+
if (fpDef !== '') {
|
|
242
|
+
return { fpInTemp: fpDef, from: 'default', fpTmpDel: '' }
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
return { fpInTemp: '', from: 'none', fpTmpDel: '' }
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
//getState: 取服務環境狀態
|
|
249
|
+
let getState = async () => {
|
|
250
|
+
let rd = checkDocxReady()
|
|
251
|
+
let nWinword = await getWinwordCount()
|
|
252
|
+
return {
|
|
253
|
+
platform: rd.platform,
|
|
254
|
+
node: process.version,
|
|
255
|
+
cwd: rd.cwd,
|
|
256
|
+
exeFound: rd.exeFound,
|
|
257
|
+
exePath: rd.exePath,
|
|
258
|
+
docxReady: rd.ready, //僅代表環境具備條件, 實際 Word 可用性須以 /api/selftest 驗證
|
|
259
|
+
templateDefault: getFpTemplateDef(),
|
|
260
|
+
templates: listTemplates(),
|
|
261
|
+
dirTemplates,
|
|
262
|
+
dirWork,
|
|
263
|
+
queue: getQueueSize(),
|
|
264
|
+
winwordProcesses: nWinword,
|
|
265
|
+
maxBytes,
|
|
266
|
+
tokenRequired: token !== '',
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
//convertCore: 依請求內容轉檔(模板由服務端決定後交由核心處理)
|
|
271
|
+
let convertCore = async (inp) => {
|
|
272
|
+
let rTemp = pickTemplate(inp)
|
|
273
|
+
try {
|
|
274
|
+
let keepTmp = get(inp, 'keepTmp', false)
|
|
275
|
+
let keepWork = get(inp, 'keepWork', false)
|
|
276
|
+
let r = await cvMdTo({
|
|
277
|
+
md: get(inp, 'md', ''),
|
|
278
|
+
mdBase64: get(inp, 'mdBase64', ''),
|
|
279
|
+
name: get(inp, 'name', ''),
|
|
280
|
+
out: get(inp, 'out', ''),
|
|
281
|
+
assets: get(inp, 'assets', []),
|
|
282
|
+
dirWork,
|
|
283
|
+
fpInTemp: rTemp.fpInTemp,
|
|
284
|
+
optMd2html: get(inp, 'optMd2html', {}),
|
|
285
|
+
optHtml2docx: get(inp, 'optHtml2docx', {}),
|
|
286
|
+
keepWork: (keepTmp === true || keepTmp === 'true' || keepWork === true || keepWork === 'true'),
|
|
287
|
+
})
|
|
288
|
+
return {
|
|
289
|
+
success: true,
|
|
290
|
+
...r,
|
|
291
|
+
template: rTemp.from,
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
finally {
|
|
295
|
+
//刪除本次夾帶之暫存模板
|
|
296
|
+
if (rTemp.fpTmpDel !== '' && fsIsFile(rTemp.fpTmpDel)) {
|
|
297
|
+
try {
|
|
298
|
+
fs.unlinkSync(rTemp.fpTmpDel)
|
|
299
|
+
}
|
|
300
|
+
catch (err) {
|
|
301
|
+
//殘檔被鎖, 留待下次清理
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
//清除逾時殘留之作業資料夾
|
|
308
|
+
let nClean = cleanWorkDir(dirWork)
|
|
309
|
+
|
|
310
|
+
//server
|
|
311
|
+
let server = Hapi.server({
|
|
312
|
+
port,
|
|
313
|
+
host,
|
|
314
|
+
routes: {
|
|
315
|
+
cors: true, //內網工具服務, 便於自瀏覽器或他機測試
|
|
316
|
+
payload: {
|
|
317
|
+
maxBytes,
|
|
318
|
+
},
|
|
319
|
+
},
|
|
320
|
+
})
|
|
321
|
+
|
|
322
|
+
//token 驗證(未設定時不啟用)
|
|
323
|
+
server.ext('onRequest', (req, h) => {
|
|
324
|
+
if (token === '') {
|
|
325
|
+
return h.continue
|
|
326
|
+
}
|
|
327
|
+
if (!req.path.startsWith('/api/')) {
|
|
328
|
+
return h.continue
|
|
329
|
+
}
|
|
330
|
+
let t = get(req, 'headers.x-api-token', '')
|
|
331
|
+
if (t !== token) {
|
|
332
|
+
return h.response({ success: false, error: 'unauthorized: a valid x-api-token header is required' }).code(401).takeover()
|
|
333
|
+
}
|
|
334
|
+
return h.continue
|
|
335
|
+
})
|
|
336
|
+
|
|
337
|
+
//apis
|
|
338
|
+
server.route([
|
|
339
|
+
|
|
340
|
+
{
|
|
341
|
+
//服務說明(供瀏覽器直接開啟確認服務存活)
|
|
342
|
+
method: 'GET',
|
|
343
|
+
path: '/',
|
|
344
|
+
handler: () => {
|
|
345
|
+
return {
|
|
346
|
+
service: 'w-md2docx',
|
|
347
|
+
description: 'API service for converting Markdown to HTML/DOCX',
|
|
348
|
+
endpoints: {
|
|
349
|
+
'GET /api/health': 'health check and environment state',
|
|
350
|
+
'GET /api/selftest': 'run a minimal conversion to verify that Microsoft Word is available',
|
|
351
|
+
'POST /api/convert': 'main conversion endpoint (JSON)',
|
|
352
|
+
},
|
|
353
|
+
}
|
|
354
|
+
},
|
|
355
|
+
},
|
|
356
|
+
|
|
357
|
+
{
|
|
358
|
+
method: 'GET',
|
|
359
|
+
path: '/api/health',
|
|
360
|
+
handler: async () => {
|
|
361
|
+
let st = await getState()
|
|
362
|
+
return {
|
|
363
|
+
success: true,
|
|
364
|
+
...st,
|
|
365
|
+
}
|
|
366
|
+
},
|
|
367
|
+
},
|
|
368
|
+
|
|
369
|
+
{
|
|
370
|
+
//實跑一次最小 md -> docx, 驗證本機 Word 確實可用(健檢僅檢查靜態條件, 無法確認 Word 能否被調用)
|
|
371
|
+
method: 'GET',
|
|
372
|
+
path: '/api/selftest',
|
|
373
|
+
handler: async (req, h) => {
|
|
374
|
+
let msStart = Date.now()
|
|
375
|
+
try {
|
|
376
|
+
let r = await convertCore({
|
|
377
|
+
md: '# selftest\n\nThis file is generated by the self test of the conversion service.\n',
|
|
378
|
+
name: 'selftest',
|
|
379
|
+
out: 'both',
|
|
380
|
+
})
|
|
381
|
+
return {
|
|
382
|
+
success: true,
|
|
383
|
+
ms: Date.now() - msStart,
|
|
384
|
+
htmlSize: get(r, 'html.size', 0),
|
|
385
|
+
docxSize: get(r, 'docx.size', 0),
|
|
386
|
+
template: r.template,
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
catch (err) {
|
|
390
|
+
let st = await getState()
|
|
391
|
+
return h.response({
|
|
392
|
+
success: false,
|
|
393
|
+
error: toErrText(err),
|
|
394
|
+
ms: Date.now() - msStart,
|
|
395
|
+
env: st,
|
|
396
|
+
}).code(500)
|
|
397
|
+
}
|
|
398
|
+
},
|
|
399
|
+
},
|
|
400
|
+
|
|
401
|
+
{
|
|
402
|
+
method: 'POST',
|
|
403
|
+
path: '/api/convert',
|
|
404
|
+
options: {
|
|
405
|
+
payload: {
|
|
406
|
+
maxBytes,
|
|
407
|
+
parse: true,
|
|
408
|
+
allow: 'application/json',
|
|
409
|
+
timeout: false, //允許大檔慢速上傳
|
|
410
|
+
},
|
|
411
|
+
timeout: {
|
|
412
|
+
socket: false, //轉檔耗時較長, 不以 socket 逾時中斷
|
|
413
|
+
},
|
|
414
|
+
},
|
|
415
|
+
handler: async (req, h) => {
|
|
416
|
+
|
|
417
|
+
let inp = req.payload
|
|
418
|
+
if (!isobj(inp)) {
|
|
419
|
+
return h.response({ success: false, error: 'parameters must be provided as a JSON object' }).code(400)
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
//download: 單一格式時可直接回傳二進位檔(便於 curl -o 與瀏覽器下載)
|
|
423
|
+
//why: 不合法組合(out='both')於轉檔前即擋下, 避免先跑完耗時之 Word 轉檔才回 400
|
|
424
|
+
let qd = get(req, 'query.download', '')
|
|
425
|
+
let download = (qd === '1' || qd === 'true')
|
|
426
|
+
if (download && get(inp, 'out', '') === 'both') {
|
|
427
|
+
return h.response({ success: false, error: 'download only supports out of html or docx (single format)' }).code(400)
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
try {
|
|
431
|
+
|
|
432
|
+
let r = await convertCore(inp)
|
|
433
|
+
|
|
434
|
+
if (download) {
|
|
435
|
+
let one = null
|
|
436
|
+
if (r.out === 'docx' && isobj(r.docx)) {
|
|
437
|
+
one = r.docx
|
|
438
|
+
}
|
|
439
|
+
else if (r.out === 'html' && isobj(r.html)) {
|
|
440
|
+
one = r.html
|
|
441
|
+
}
|
|
442
|
+
if (one === null) {
|
|
443
|
+
return h.response({ success: false, error: 'download only supports out of html or docx (single format)' }).code(400)
|
|
444
|
+
}
|
|
445
|
+
return h.response(Buffer.from(one.base64, 'base64'))
|
|
446
|
+
.type(one.mime)
|
|
447
|
+
.header('content-disposition', toContentDisposition(one.fileName))
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
return r
|
|
451
|
+
|
|
452
|
+
}
|
|
453
|
+
catch (err) {
|
|
454
|
+
let msg = toErrText(err)
|
|
455
|
+
let code = isInputError(msg) ? 400 : 500
|
|
456
|
+
return h.response({ success: false, error: msg }).code(code)
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
},
|
|
460
|
+
},
|
|
461
|
+
|
|
462
|
+
])
|
|
463
|
+
|
|
464
|
+
//autoStart
|
|
465
|
+
if (autoStart) {
|
|
466
|
+
await server.start()
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
//定期清除逾時殘留之作業資料夾
|
|
470
|
+
let idInterval = setInterval(() => {
|
|
471
|
+
cleanWorkDir(dirWork)
|
|
472
|
+
}, 3600 * 1000)
|
|
473
|
+
idInterval.unref()
|
|
474
|
+
|
|
475
|
+
//stop
|
|
476
|
+
let stop = async () => {
|
|
477
|
+
clearInterval(idInterval)
|
|
478
|
+
await server.stop()
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
return {
|
|
482
|
+
server,
|
|
483
|
+
getState,
|
|
484
|
+
stop,
|
|
485
|
+
settings: { port, host, token, maxMb, maxBytes, dirTemplates, dirWork, templateDef, nClean },
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
|
|
491
|
+
export default ApiServer
|
|
492
|
+
</code></pre>
|
|
493
|
+
</article>
|
|
494
|
+
</section>
|
|
495
|
+
|
|
496
|
+
|
|
497
|
+
|
|
498
|
+
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
</div>
|
|
502
|
+
|
|
503
|
+
<br class="clear">
|
|
504
|
+
|
|
505
|
+
<footer>
|
|
506
|
+
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.
|
|
507
|
+
</footer>
|
|
508
|
+
|
|
509
|
+
<script>prettyPrint();</script>
|
|
510
|
+
<script src="scripts/polyfill.js"></script>
|
|
511
|
+
<script src="scripts/linenumber.js"></script>
|
|
512
|
+
|
|
513
|
+
|
|
514
|
+
|
|
515
|
+
</body>
|
|
516
|
+
</html>
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
|
|
5
|
+
<meta charset="utf-8">
|
|
6
|
+
<title>WMd2docx.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">WMd2docx.mjs</h1>
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
<section>
|
|
47
|
+
<article>
|
|
48
|
+
<pre class="prettyprint source linenums"><code>import cvMdToDocx from './cvMdToDocx.mjs'
|
|
49
|
+
import cvMdTo from './cvMdTo.mjs'
|
|
50
|
+
import ApiServer from './ApiServer.mjs'
|
|
51
|
+
import ApiClient from './ApiClient.mjs'
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Markdown轉Docx工具集
|
|
56
|
+
*
|
|
57
|
+
* 匯整四個入口:cvMdToDocx(md檔轉docx檔)、cvMdTo(md內容轉html/docx之base64)、ApiServer(hapi轉檔服務)、ApiClient(呼叫轉檔服務之用戶端)。
|
|
58
|
+
*
|
|
59
|
+
* @example
|
|
60
|
+
*
|
|
61
|
+
* import WMd2docx from 'w-md2docx/src/WMd2docx.mjs'
|
|
62
|
+
*
|
|
63
|
+
* let r = await WMd2docx.cvMdToDocx('./test/report.md', './test/report.docx')
|
|
64
|
+
* console.log(r)
|
|
65
|
+
* // => { fpOutDocx: '…', sizeDocx: 39856, sizeHtml: 12345, ms: 8342 }
|
|
66
|
+
*
|
|
67
|
+
*/
|
|
68
|
+
let WMd2docx = {
|
|
69
|
+
cvMdToDocx,
|
|
70
|
+
cvMdTo,
|
|
71
|
+
ApiServer,
|
|
72
|
+
ApiClient,
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
export default WMd2docx
|
|
77
|
+
</code></pre>
|
|
78
|
+
</article>
|
|
79
|
+
</section>
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
</div>
|
|
87
|
+
|
|
88
|
+
<br class="clear">
|
|
89
|
+
|
|
90
|
+
<footer>
|
|
91
|
+
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.
|
|
92
|
+
</footer>
|
|
93
|
+
|
|
94
|
+
<script>prettyPrint();</script>
|
|
95
|
+
<script src="scripts/polyfill.js"></script>
|
|
96
|
+
<script src="scripts/linenumber.js"></script>
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
</body>
|
|
101
|
+
</html>
|