@things-factory/integration-headless 7.0.49 → 7.0.54
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/dist-server/engine/task/headless-pdf-capture-board.d.ts +59 -1
- package/dist-server/engine/task/headless-pdf-capture-board.js +29 -250
- package/dist-server/engine/task/headless-pdf-capture-board.js.map +1 -1
- package/dist-server/engine/task/headless-pdf-capture-markdown.d.ts +52 -0
- package/dist-server/engine/task/headless-pdf-capture-markdown.js +38 -0
- package/dist-server/engine/task/headless-pdf-capture-markdown.js.map +1 -0
- package/dist-server/engine/task/headless-pdf-capture.d.ts +52 -1
- package/dist-server/engine/task/headless-pdf-capture.js +13 -196
- package/dist-server/engine/task/headless-pdf-capture.js.map +1 -1
- package/dist-server/engine/task/headless-pdf-open.js +27 -257
- package/dist-server/engine/task/headless-pdf-open.js.map +1 -1
- package/dist-server/engine/task/headless-pdf-save.js +32 -20
- package/dist-server/engine/task/headless-pdf-save.js.map +1 -1
- package/dist-server/engine/task/index.d.ts +1 -0
- package/dist-server/engine/task/index.js +1 -0
- package/dist-server/engine/task/index.js.map +1 -1
- package/dist-server/engine/task/pdf-capture-util.d.ts +96 -0
- package/dist-server/engine/task/pdf-capture-util.js +252 -0
- package/dist-server/engine/task/pdf-capture-util.js.map +1 -0
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -2
- package/server/engine/task/headless-pdf-capture-board.ts +41 -293
- package/server/engine/task/headless-pdf-capture-markdown.ts +41 -0
- package/server/engine/task/headless-pdf-capture.ts +12 -226
- package/server/engine/task/headless-pdf-open.ts +28 -292
- package/server/engine/task/headless-pdf-save.ts +34 -21
- package/server/engine/task/index.ts +1 -0
- package/server/engine/task/pdf-capture-util.ts +319 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@things-factory/integration-headless",
|
|
3
|
-
"version": "7.0.
|
|
3
|
+
"version": "7.0.54",
|
|
4
4
|
"main": "dist-server/index.js",
|
|
5
5
|
"things-factory": true,
|
|
6
6
|
"author": "heartyoh <heartyoh@hatiolab.com>",
|
|
@@ -29,5 +29,5 @@
|
|
|
29
29
|
"ejs": "^3.1.10",
|
|
30
30
|
"pdf-lib": "^1.17.1"
|
|
31
31
|
},
|
|
32
|
-
"gitHead": "
|
|
32
|
+
"gitHead": "14a3320fb61bd0a47c147d4c3ff8310de6172f61"
|
|
33
33
|
}
|
|
@@ -1,9 +1,6 @@
|
|
|
1
|
-
import * as ejs from 'ejs'
|
|
2
1
|
import { TaskRegistry } from '@things-factory/integration-base'
|
|
3
|
-
import {
|
|
4
|
-
import { access } from '@things-factory/utils'
|
|
2
|
+
import { PDFCaptureUtil, getCommonParameterSpec } from './pdf-capture-util'
|
|
5
3
|
import { BoardFunc } from '@things-factory/board-service'
|
|
6
|
-
import { PDFDocument } from 'pdf-lib'
|
|
7
4
|
|
|
8
5
|
const PAGE_FORMATS = {
|
|
9
6
|
A4: { width: 595.28, height: 841.89 },
|
|
@@ -12,250 +9,86 @@ const PAGE_FORMATS = {
|
|
|
12
9
|
Legal: { width: 612, height: 1008 }
|
|
13
10
|
}
|
|
14
11
|
|
|
15
|
-
|
|
16
|
-
var { connection: connectionName, params: stepOptions } = step
|
|
17
|
-
var {
|
|
18
|
-
accessor,
|
|
19
|
-
board,
|
|
20
|
-
draft,
|
|
21
|
-
format = 'A4', // Set default value to A4
|
|
22
|
-
width,
|
|
23
|
-
height,
|
|
24
|
-
marginTop = 0,
|
|
25
|
-
marginBottom = 0,
|
|
26
|
-
marginLeft = 0,
|
|
27
|
-
marginRight = 0,
|
|
28
|
-
scale,
|
|
29
|
-
printBackground,
|
|
30
|
-
landscape,
|
|
31
|
-
preferCSSPageSize
|
|
32
|
-
} = stepOptions || {}
|
|
33
|
-
var { domain, data, user, logger, __headless_pdf } = context
|
|
12
|
+
type PageFormat = keyof typeof PAGE_FORMATS
|
|
34
13
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
if (!board || !board.id) {
|
|
40
|
-
throw new Error('The board property must be set')
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
var { pdfDoc, header, footer, watermark } = __headless_pdf
|
|
44
|
-
|
|
45
|
-
var headlessPool = ConnectionManager.getConnectionInstanceByName(domain, connectionName)
|
|
46
|
-
let browser
|
|
14
|
+
export async function HeadlessPDFCaptureBoard(step, context) {
|
|
15
|
+
const pdfUtil = new PDFCaptureUtil(context)
|
|
16
|
+
await pdfUtil.initBrowser(step.connection)
|
|
47
17
|
|
|
48
18
|
try {
|
|
49
|
-
|
|
50
|
-
const page = await browser.newPage()
|
|
19
|
+
const { board, draft, format = 'A4', landscape, preferCSSPageSize } = step.params
|
|
51
20
|
|
|
52
|
-
|
|
21
|
+
if (!board || !board.id) {
|
|
22
|
+
throw new Error('The board property must be set')
|
|
23
|
+
}
|
|
53
24
|
|
|
54
|
-
|
|
55
|
-
const [fontsToUse, fontStyles] = await BoardFunc.fonts(domain)
|
|
25
|
+
const { model, base } = await BoardFunc.headlessModel({ domain: context.domain, id: board.id }, draft)
|
|
26
|
+
const [fontsToUse, fontStyles] = await BoardFunc.fonts(context.domain)
|
|
56
27
|
|
|
57
28
|
model.fonts = fontsToUse
|
|
58
29
|
model.fontStyles = fontStyles
|
|
59
30
|
|
|
60
|
-
|
|
31
|
+
let width: number | undefined = PAGE_FORMATS[format as PageFormat]?.width
|
|
32
|
+
let height: number | undefined = PAGE_FORMATS[format as PageFormat]?.height
|
|
61
33
|
|
|
62
34
|
if (preferCSSPageSize) {
|
|
63
35
|
width = undefined
|
|
64
36
|
height = undefined
|
|
65
|
-
} else if (PAGE_FORMATS[format]) {
|
|
66
|
-
const pageDimensions = PAGE_FORMATS[format]
|
|
67
|
-
width = pageDimensions.width
|
|
68
|
-
height = pageDimensions.height
|
|
69
37
|
}
|
|
70
38
|
|
|
71
|
-
if (landscape) {
|
|
39
|
+
if (landscape && width && height) {
|
|
72
40
|
;[width, height] = [height, width]
|
|
73
41
|
}
|
|
74
42
|
|
|
75
|
-
const contentWidth = width - marginLeft - marginRight
|
|
76
|
-
const contentHeight = height - marginTop - marginBottom
|
|
77
|
-
|
|
78
|
-
const boardWidthPt = (boardWidthPx * 72) / 96
|
|
79
|
-
const boardHeightPt = (boardHeightPx * 72) / 96
|
|
80
|
-
|
|
81
|
-
const widthRatio = contentWidth / boardWidthPt
|
|
82
|
-
const heightRatio = contentHeight / boardHeightPt
|
|
83
|
-
const ratio = Math.min(widthRatio, heightRatio)
|
|
84
|
-
|
|
85
|
-
const scaledBoardWidthPt = boardWidthPt * ratio
|
|
86
|
-
const scaledBoardHeightPt = boardHeightPt * ratio
|
|
87
|
-
|
|
88
|
-
const offsetX = (contentWidth - scaledBoardWidthPt) / 2 + marginLeft
|
|
89
|
-
const offsetY = (contentHeight - scaledBoardHeightPt) / 2 + marginBottom
|
|
90
|
-
|
|
91
|
-
await page.setViewport({ width: Math.round(boardWidthPx * ratio), height: Math.round(boardHeightPx * ratio) })
|
|
92
|
-
await page.setRequestInterception(true)
|
|
93
|
-
await page.setDefaultTimeout(10000)
|
|
94
|
-
|
|
95
|
-
page.on('console', async msg => {
|
|
96
|
-
console.log(`[browser ${msg.type()}] ${msg.text()}`)
|
|
97
|
-
})
|
|
98
|
-
|
|
99
|
-
page.on('requestfailed', request => {
|
|
100
|
-
console.log('Request failed:', request.url())
|
|
101
|
-
})
|
|
102
|
-
|
|
103
43
|
const protocol = 'http'
|
|
104
44
|
const host = 'localhost'
|
|
105
45
|
const port = process.env.PORT ? `:${process.env.PORT}` : ''
|
|
106
46
|
const path = '/internal-board-service-view'
|
|
107
47
|
const url = `${protocol}://${host}${port}${path}`
|
|
108
48
|
|
|
109
|
-
const
|
|
49
|
+
const page = await pdfUtil.browser!.newPage()
|
|
110
50
|
|
|
111
|
-
page.
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
method: 'POST',
|
|
115
|
-
headers: {
|
|
116
|
-
'Content-Type': 'application/json',
|
|
117
|
-
'x-things-factory-domain': domain?.subdomain,
|
|
118
|
-
Authorization: 'Bearer ' + token
|
|
119
|
-
},
|
|
120
|
-
postData: JSON.stringify({
|
|
121
|
-
model,
|
|
122
|
-
base
|
|
123
|
-
})
|
|
124
|
-
})
|
|
125
|
-
} else if (request.url().startsWith(`${protocol}://${host}${port}`)) {
|
|
126
|
-
request.continue({
|
|
127
|
-
headers: {
|
|
128
|
-
...request.headers(),
|
|
129
|
-
'x-things-factory-domain': domain?.subdomain,
|
|
130
|
-
Authorization: 'Bearer ' + token
|
|
131
|
-
}
|
|
132
|
-
})
|
|
133
|
-
} else {
|
|
134
|
-
request.continue()
|
|
135
|
-
}
|
|
51
|
+
await page.setViewport({
|
|
52
|
+
width: Math.round(width || 800), // Fallback for width if not defined
|
|
53
|
+
height: Math.round(height || 600) // Fallback for height if not defined
|
|
136
54
|
})
|
|
137
55
|
|
|
138
56
|
await page.goto(url)
|
|
139
57
|
|
|
140
|
-
await page.evaluate(
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
const renderTemplateSafely = (template, data) => {
|
|
149
|
-
try {
|
|
150
|
-
return ejs.render(template, data)
|
|
151
|
-
} catch (error) {
|
|
152
|
-
logger.warn(`Template rendering error: ${error.message}`)
|
|
153
|
-
return template
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
header = renderTemplateSafely(header, __headless_pdf)
|
|
158
|
-
footer = renderTemplateSafely(footer, __headless_pdf)
|
|
159
|
-
|
|
160
|
-
// Apply header, footer, and watermark using Puppeteer
|
|
161
|
-
// Apply header, footer, and watermark using Puppeteer
|
|
162
|
-
if (header || footer || watermark) {
|
|
163
|
-
await page.evaluate(
|
|
164
|
-
({ header, footer, watermark, isLandscape }) => {
|
|
165
|
-
const setPositioning = (element, position) => {
|
|
166
|
-
element.style.position = 'fixed'
|
|
167
|
-
element.style.left = '0'
|
|
168
|
-
element.style.width = '100%'
|
|
169
|
-
element.style.textAlign = 'center'
|
|
170
|
-
element.style.fontSize = '12px'
|
|
171
|
-
element.style.margin = '0'
|
|
172
|
-
element.style.padding = '0'
|
|
173
|
-
if (position === 'header') {
|
|
174
|
-
element.style.top = '0'
|
|
175
|
-
} else if (position === 'footer') {
|
|
176
|
-
element.style.bottom = '0'
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
if (header) {
|
|
181
|
-
const headerElement = document.createElement('div')
|
|
182
|
-
headerElement.innerHTML = header
|
|
183
|
-
setPositioning(headerElement, 'header')
|
|
184
|
-
document.body.appendChild(headerElement)
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
if (footer) {
|
|
188
|
-
const footerElement = document.createElement('div')
|
|
189
|
-
footerElement.innerHTML = footer
|
|
190
|
-
setPositioning(footerElement, 'footer')
|
|
191
|
-
document.body.appendChild(footerElement)
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
if (watermark) {
|
|
195
|
-
const watermarkElement = document.createElement('div')
|
|
196
|
-
watermarkElement.innerHTML = watermark
|
|
197
|
-
watermarkElement.style.position = 'fixed'
|
|
198
|
-
watermarkElement.style.top = isLandscape ? '40%' : '50%'
|
|
199
|
-
watermarkElement.style.left = '50%'
|
|
200
|
-
watermarkElement.style.transform = 'translate(-50%, -50%) rotate(-45deg)'
|
|
201
|
-
watermarkElement.style.opacity = '0.2'
|
|
202
|
-
watermarkElement.style.fontSize = '50px'
|
|
203
|
-
watermarkElement.style.color = 'red'
|
|
204
|
-
watermarkElement.style.pointerEvents = 'none'
|
|
205
|
-
watermarkElement.style.zIndex = '9999'
|
|
206
|
-
document.body.appendChild(watermarkElement)
|
|
207
|
-
}
|
|
208
|
-
},
|
|
209
|
-
{ header, footer, watermark, isLandscape: landscape }
|
|
210
|
-
)
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
const pageOptions = {
|
|
214
|
-
format,
|
|
215
|
-
width: `${scaledBoardWidthPt}px`,
|
|
216
|
-
height: `${scaledBoardHeightPt}px`,
|
|
217
|
-
margin: {
|
|
218
|
-
top: `${offsetY}px`,
|
|
219
|
-
right: `${offsetX}px`,
|
|
220
|
-
bottom: `${offsetY}px`,
|
|
221
|
-
left: `${offsetX}px`
|
|
58
|
+
await page.evaluate(
|
|
59
|
+
data => {
|
|
60
|
+
//@ts-ignore
|
|
61
|
+
s.data = data
|
|
62
|
+
return new Promise(resolve => {
|
|
63
|
+
requestAnimationFrame(() => resolve(0))
|
|
64
|
+
})
|
|
222
65
|
},
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
const copiedPages = await pdfDoc.copyPages(contentPdfDoc, contentPdfDoc.getPageIndices())
|
|
233
|
-
|
|
234
|
-
copiedPages.forEach(page => pdfDoc.addPage(page))
|
|
235
|
-
|
|
236
|
-
__headless_pdf.pageCount += copiedPages.length
|
|
66
|
+
{ model, base }
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
// 페이지 옵션 설정
|
|
70
|
+
const pageOptions = pdfUtil.buildPageOptions({
|
|
71
|
+
...step.params,
|
|
72
|
+
width: width ? `${width}px` : undefined,
|
|
73
|
+
height: height ? `${height}px` : undefined
|
|
74
|
+
})
|
|
237
75
|
|
|
238
|
-
|
|
239
|
-
|
|
76
|
+
// PDF 생성 및 추가
|
|
77
|
+
const htmlContent = '' // 보드에서 직접 렌더링되므로 HTML 컨텐츠가 비어있음
|
|
78
|
+
await pdfUtil.processPageAndGeneratePDF(pageOptions, htmlContent) // pageOptions 사용
|
|
240
79
|
|
|
241
80
|
return {
|
|
242
|
-
data: __headless_pdf
|
|
81
|
+
data: context.__headless_pdf
|
|
243
82
|
}
|
|
244
83
|
} catch (error) {
|
|
245
|
-
if (browser) {
|
|
246
|
-
await browser.close()
|
|
247
|
-
}
|
|
248
|
-
|
|
249
84
|
throw error
|
|
85
|
+
} finally {
|
|
86
|
+
await pdfUtil.closeBrowser()
|
|
250
87
|
}
|
|
251
88
|
}
|
|
252
89
|
|
|
253
90
|
HeadlessPDFCaptureBoard.parameterSpec = [
|
|
254
|
-
|
|
255
|
-
type: 'scenario-step-input',
|
|
256
|
-
name: 'accessor',
|
|
257
|
-
label: 'accessor'
|
|
258
|
-
},
|
|
91
|
+
...getCommonParameterSpec(),
|
|
259
92
|
{
|
|
260
93
|
type: 'resource-object',
|
|
261
94
|
name: 'board',
|
|
@@ -270,91 +103,6 @@ HeadlessPDFCaptureBoard.parameterSpec = [
|
|
|
270
103
|
label: 'board-draft',
|
|
271
104
|
defaultValue: false,
|
|
272
105
|
description: 'Set whether to get the current working version or the last released version'
|
|
273
|
-
},
|
|
274
|
-
{
|
|
275
|
-
type: 'select',
|
|
276
|
-
name: 'format',
|
|
277
|
-
label: 'page-format',
|
|
278
|
-
property: {
|
|
279
|
-
options: [
|
|
280
|
-
{ display: '', value: '' },
|
|
281
|
-
{ display: 'A4', value: 'A4' },
|
|
282
|
-
{ display: 'A3', value: 'A3' },
|
|
283
|
-
{ display: 'Letter', value: 'Letter' },
|
|
284
|
-
{ display: 'Legal', value: 'Legal' }
|
|
285
|
-
]
|
|
286
|
-
},
|
|
287
|
-
description: 'Select the paper format for the PDF'
|
|
288
|
-
},
|
|
289
|
-
{
|
|
290
|
-
type: 'string',
|
|
291
|
-
name: 'width',
|
|
292
|
-
label: 'page-width',
|
|
293
|
-
placeholder: '(e.g., "8.5in", "21cm", "600px")',
|
|
294
|
-
description: 'Specify the width of the page (e.g., "8.5in", "21cm", "600px")'
|
|
295
|
-
},
|
|
296
|
-
{
|
|
297
|
-
type: 'string',
|
|
298
|
-
name: 'height',
|
|
299
|
-
label: 'page-height',
|
|
300
|
-
placeholder: '(e.g., "11in", "29.7cm", "800px")',
|
|
301
|
-
description: 'Specify the height of the page (e.g., "11in", "29.7cm", "800px")'
|
|
302
|
-
},
|
|
303
|
-
{
|
|
304
|
-
type: 'string',
|
|
305
|
-
name: 'marginTop',
|
|
306
|
-
label: 'page-margin-top',
|
|
307
|
-
placeholder: '(e.g., "0.5in", "1cm", "100px")',
|
|
308
|
-
description: 'Set the top margin for the page'
|
|
309
|
-
},
|
|
310
|
-
{
|
|
311
|
-
type: 'string',
|
|
312
|
-
name: 'marginBottom',
|
|
313
|
-
label: 'page-margin-bottom',
|
|
314
|
-
placeholder: '(e.g., "0.5in", "1cm", "100px")',
|
|
315
|
-
description: 'Set the bottom margin for the page'
|
|
316
|
-
},
|
|
317
|
-
{
|
|
318
|
-
type: 'string',
|
|
319
|
-
name: 'marginLeft',
|
|
320
|
-
label: 'page-margin-left',
|
|
321
|
-
placeholder: '(e.g., "0.5in", "1cm", "100px")',
|
|
322
|
-
description: 'Set the left margin for the page'
|
|
323
|
-
},
|
|
324
|
-
{
|
|
325
|
-
type: 'string',
|
|
326
|
-
name: 'marginRight',
|
|
327
|
-
label: 'page-margin-right',
|
|
328
|
-
placeholder: '(e.g., "0.5in", "1cm", "100px")',
|
|
329
|
-
description: 'Set the right margin for the page'
|
|
330
|
-
},
|
|
331
|
-
{
|
|
332
|
-
type: 'number',
|
|
333
|
-
name: 'scale',
|
|
334
|
-
label: 'page-scale',
|
|
335
|
-
defaultValue: 1,
|
|
336
|
-
description: 'Set the scale of the page content (default is 1)'
|
|
337
|
-
},
|
|
338
|
-
{
|
|
339
|
-
type: 'boolean',
|
|
340
|
-
name: 'printBackground',
|
|
341
|
-
label: 'print-background',
|
|
342
|
-
defaultValue: true,
|
|
343
|
-
description: 'Include background graphics when printing the page'
|
|
344
|
-
},
|
|
345
|
-
{
|
|
346
|
-
type: 'boolean',
|
|
347
|
-
name: 'landscape',
|
|
348
|
-
label: 'landscape',
|
|
349
|
-
defaultValue: false,
|
|
350
|
-
description: 'Print the PDF in landscape orientation'
|
|
351
|
-
},
|
|
352
|
-
{
|
|
353
|
-
type: 'boolean',
|
|
354
|
-
name: 'preferCSSPageSize',
|
|
355
|
-
label: 'prefer-css-page-size',
|
|
356
|
-
defaultValue: false,
|
|
357
|
-
description: 'Whether to prefer the CSS-defined page size over the given width and height'
|
|
358
106
|
}
|
|
359
107
|
]
|
|
360
108
|
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { TaskRegistry } from '@things-factory/integration-base'
|
|
2
|
+
import { access } from '@things-factory/utils'
|
|
3
|
+
import { marked } from 'marked'
|
|
4
|
+
|
|
5
|
+
import { PDFCaptureUtil, getCommonParameterSpec } from './pdf-capture-util'
|
|
6
|
+
|
|
7
|
+
export async function HeadlessPDFCaptureMarkdown(step, context) {
|
|
8
|
+
const pdfUtil = new PDFCaptureUtil(context)
|
|
9
|
+
await pdfUtil.initBrowser(step.connection)
|
|
10
|
+
|
|
11
|
+
try {
|
|
12
|
+
const { accessor, markdownContent } = step.params
|
|
13
|
+
const templateInput = access(accessor, context.data)
|
|
14
|
+
|
|
15
|
+
const renderedMarkdown = pdfUtil.renderTemplate(markdownContent, templateInput)
|
|
16
|
+
const htmlContent = marked(renderedMarkdown)
|
|
17
|
+
|
|
18
|
+
await pdfUtil.processPageAndGeneratePDF(step.params, htmlContent)
|
|
19
|
+
|
|
20
|
+
return {
|
|
21
|
+
data: context.__headless_pdf
|
|
22
|
+
}
|
|
23
|
+
} catch (error) {
|
|
24
|
+
throw error
|
|
25
|
+
} finally {
|
|
26
|
+
await pdfUtil.closeBrowser()
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
HeadlessPDFCaptureMarkdown.parameterSpec = [
|
|
31
|
+
...getCommonParameterSpec(),
|
|
32
|
+
{
|
|
33
|
+
type: 'textarea',
|
|
34
|
+
name: 'markdownContent',
|
|
35
|
+
label: 'markdown-content'
|
|
36
|
+
}
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
HeadlessPDFCaptureMarkdown.help = 'integration/task/headless-pdf-capture-markdown'
|
|
40
|
+
|
|
41
|
+
TaskRegistry.registerTaskHandler('headless-pdf-capture-markdown', HeadlessPDFCaptureMarkdown)
|