@liascript/exporter 3.0.0--1.0.3 → 3.0.2--1.0.3
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/assets/capacitor/{index.bfe7363b.js → index.44a6a9b3.js} +1 -1
- package/dist/assets/capacitor/index.html +1 -1
- package/dist/assets/capacitor/{jszip.min.f6eda75b.js → jszip.min.2f991499.js} +1 -1
- package/dist/assets/capacitor/{trystero-ipfs.min.b27a61d7.js → trystero-ipfs.min.f25fe3e7.js} +1 -1
- package/dist/assets/indexeddb/{index.599a57d6.js → index.4aceca2f.js} +1 -1
- package/dist/assets/indexeddb/index.html +1 -1
- package/dist/assets/pdf/{index.aa124b49.js → index.49b3a4d9.js} +1 -1
- package/dist/assets/pdf/index.html +1 -1
- package/dist/assets/scorm2004/{index.45f7501b.js → index.031764e4.js} +1 -1
- package/dist/assets/scorm2004/index.html +1 -1
- package/dist/assets/{xapi/jszip.min.eaecf580.js → scorm2004/jszip.min.19c66d77.js} +1 -1
- package/dist/assets/web/{index.39a3e3eb.js → index.98993ded.js} +1 -1
- package/dist/assets/web/{index.1062f2c5.js → index.e84d3f3b.js} +1 -1
- package/dist/assets/web/index.html +1 -1
- package/dist/assets/web/{jszip.min.eaecf580.js → jszip.min.19c66d77.js} +1 -1
- package/dist/assets/xapi/{index.018a032a.js → index.f2e89e49.js} +1 -1
- package/dist/assets/xapi/{index.780a0ec3.js → index.f5402f58.js} +1 -1
- package/dist/assets/xapi/index.html +1 -1
- package/dist/assets/{scorm2004/jszip.min.eaecf580.js → xapi/jszip.min.19c66d77.js} +1 -1
- package/dist/index.js +3 -3
- package/dist/server/presets.json +94 -0
- package/dist/server/presets.yaml +120 -0
- package/dist/server/public/app.js +1 -0
- package/dist/server/public/assets/android.svg +38 -0
- package/dist/server/public/assets/cmi.svg +154 -0
- package/dist/server/public/assets/docx.svg +20 -0
- package/dist/server/public/assets/edX.svg +75 -0
- package/dist/server/public/assets/edx.svg +75 -0
- package/dist/server/public/assets/epub.svg +18 -0
- package/dist/server/public/assets/icon.svg +82 -0
- package/dist/server/public/assets/ilias.png +0 -0
- package/dist/server/public/assets/json.svg +4 -0
- package/dist/server/public/assets/learnworlds.png +0 -0
- package/dist/server/public/assets/moodle.svg +190 -0
- package/dist/server/public/assets/opal.png +0 -0
- package/dist/server/public/assets/openolat.png +0 -0
- package/dist/server/public/assets/pdf.svg +4 -0
- package/dist/server/public/assets/rdf.svg +4 -0
- package/dist/server/public/assets/scorm.png +0 -0
- package/dist/server/public/assets/web.png +0 -0
- package/dist/server/public/assets/xapi.png +0 -0
- package/dist/server/public/i18n.js +1 -0
- package/dist/server/public/index.html +1587 -0
- package/dist/server/public/locales/de.json +247 -0
- package/dist/server/public/locales/en.json +247 -0
- package/dist/server/public/status.html +251 -0
- package/dist/server/public/styles.css +712 -0
- package/package.json +5 -1
- package/.parcelrc +0 -3
- package/DESKTOP_APP_README.md +0 -58
- package/DOCKERHUB_DESCRIPTION.md +0 -52
- package/Dockerfile +0 -129
- package/PLAYSTORE_GUIDE.md +0 -172
- package/action.yml +0 -157
- package/custom.css +0 -10
- package/electron-builder.json +0 -149
- package/src/cli.ts +0 -69
- package/src/colorize.ts +0 -115
- package/src/export/android.ts +0 -419
- package/src/export/docx.ts +0 -1025
- package/src/export/epub.ts +0 -1306
- package/src/export/h5p.ts +0 -390
- package/src/export/helper.ts +0 -360
- package/src/export/ims.ts +0 -191
- package/src/export/pdf.ts +0 -406
- package/src/export/presets.ts +0 -220
- package/src/export/project.ts +0 -829
- package/src/export/rdf.ts +0 -551
- package/src/export/scorm12.ts +0 -167
- package/src/export/scorm2004.ts +0 -140
- package/src/export/web.ts +0 -306
- package/src/export/xapi.ts +0 -424
- package/src/exporter.ts +0 -296
- package/src/index.ts +0 -96
- package/src/parser.ts +0 -373
- package/src/presets.yaml +0 -219
- package/src/types.ts +0 -82
- package/tsconfig.json +0 -24
- /package/dist/assets/{pdf → indexeddb}/jszip.min.4fbcc13f.js +0 -0
- /package/dist/assets/{indexeddb → pdf}/jszip.min.63142cc8.js +0 -0
- /package/dist/assets/{xapi → web}/jszip.min.63142cc8.js +0 -0
- /package/dist/assets/{web → xapi}/jszip.min.4fbcc13f.js +0 -0
package/src/export/epub.ts
DELETED
|
@@ -1,1306 +0,0 @@
|
|
|
1
|
-
import * as helper from './helper'
|
|
2
|
-
import * as COLOR from '../colorize'
|
|
3
|
-
import * as path from 'path'
|
|
4
|
-
import puppeteer, { Browser, Page } from 'puppeteer'
|
|
5
|
-
import * as fs from 'fs'
|
|
6
|
-
import { tmpdir } from 'os'
|
|
7
|
-
|
|
8
|
-
// Default EPUB generation settings
|
|
9
|
-
const DEFAULT_TIMEOUT_MS = 15000 // 15 seconds
|
|
10
|
-
const DEFAULT_EPUB_VERSION = 3
|
|
11
|
-
const DEFAULT_LANG = 'en'
|
|
12
|
-
const DEFAULT_TOC_TITLE = 'Table Of Contents'
|
|
13
|
-
const DEFAULT_APPEND_CHAPTER_TITLES = true
|
|
14
|
-
const DEFAULT_HIDE_TOC = false
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Displays help information about EPUB export options and settings.
|
|
18
|
-
*/
|
|
19
|
-
export function help() {
|
|
20
|
-
console.log('')
|
|
21
|
-
console.log(COLOR.heading('EPUB settings:'), '\n')
|
|
22
|
-
|
|
23
|
-
COLOR.info(
|
|
24
|
-
'EPUB export generates e-books from your LiaScript course using Puppeteer to render the content and the @lesjoursfr/html-to-epub library to create the EPUB file. This allows for high-quality e-books compatible with most e-readers.',
|
|
25
|
-
)
|
|
26
|
-
|
|
27
|
-
console.log(
|
|
28
|
-
'\nLearn more: https://www.npmjs.com/package/@lesjoursfr/html-to-epub \n',
|
|
29
|
-
)
|
|
30
|
-
|
|
31
|
-
console.log(COLOR.heading('Required settings:'), '\n')
|
|
32
|
-
COLOR.command(
|
|
33
|
-
null,
|
|
34
|
-
'--epub-title',
|
|
35
|
-
' Title of the book (required)',
|
|
36
|
-
)
|
|
37
|
-
COLOR.command(
|
|
38
|
-
null,
|
|
39
|
-
'--epub-author',
|
|
40
|
-
' Author name(s), semicolon-separated for multiple authors',
|
|
41
|
-
)
|
|
42
|
-
|
|
43
|
-
console.log('')
|
|
44
|
-
console.log(COLOR.heading('Optional settings:'), '\n')
|
|
45
|
-
|
|
46
|
-
COLOR.command(null, '--epub-publisher', ' Publisher name')
|
|
47
|
-
COLOR.command(
|
|
48
|
-
null,
|
|
49
|
-
'--epub-cover',
|
|
50
|
-
' Path to cover image (absolute path or URL)',
|
|
51
|
-
)
|
|
52
|
-
COLOR.command(null, '--epub-description', ' Book description')
|
|
53
|
-
COLOR.command(
|
|
54
|
-
null,
|
|
55
|
-
'--epub-language',
|
|
56
|
-
` Language code in 2 letters (default: ${DEFAULT_LANG})`,
|
|
57
|
-
)
|
|
58
|
-
COLOR.command(
|
|
59
|
-
null,
|
|
60
|
-
'--epub-version',
|
|
61
|
-
` EPUB version: 2 or 3 (default: ${DEFAULT_EPUB_VERSION})`,
|
|
62
|
-
)
|
|
63
|
-
COLOR.command(
|
|
64
|
-
null,
|
|
65
|
-
'--epub-stylesheet',
|
|
66
|
-
' Path to custom CSS file for styling',
|
|
67
|
-
)
|
|
68
|
-
COLOR.command(
|
|
69
|
-
null,
|
|
70
|
-
'--epub-theme',
|
|
71
|
-
' LiaScript theme: default, turquoise, blue, red, yellow',
|
|
72
|
-
)
|
|
73
|
-
COLOR.command(
|
|
74
|
-
null,
|
|
75
|
-
'--epub-toc-title',
|
|
76
|
-
` Title for table of contents (default: "${DEFAULT_TOC_TITLE}")`,
|
|
77
|
-
)
|
|
78
|
-
COLOR.command(
|
|
79
|
-
null,
|
|
80
|
-
'--epub-hide-toc',
|
|
81
|
-
' Hide table of contents in the generated EPUB (default: false)',
|
|
82
|
-
)
|
|
83
|
-
COLOR.command(
|
|
84
|
-
null,
|
|
85
|
-
'--epub-timeout',
|
|
86
|
-
` Additional wait time for rendering in ms (default: ${DEFAULT_TIMEOUT_MS})`,
|
|
87
|
-
)
|
|
88
|
-
COLOR.command(
|
|
89
|
-
null,
|
|
90
|
-
'--epub-fonts',
|
|
91
|
-
' Comma-separated paths to custom font files to embed',
|
|
92
|
-
)
|
|
93
|
-
COLOR.command(
|
|
94
|
-
null,
|
|
95
|
-
'--epub-chapter-title',
|
|
96
|
-
' Custom title for the main chapter (default: course title)',
|
|
97
|
-
)
|
|
98
|
-
COLOR.command(
|
|
99
|
-
null,
|
|
100
|
-
'--epub-preview',
|
|
101
|
-
' Open preview browser for debugging (default: false)',
|
|
102
|
-
)
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
/**
|
|
106
|
-
* Configuration options for EPUB export.
|
|
107
|
-
*/
|
|
108
|
-
export interface EpubExportArguments {
|
|
109
|
-
input: string
|
|
110
|
-
output: string
|
|
111
|
-
|
|
112
|
-
// Required EPUB settings
|
|
113
|
-
'epub-title': string
|
|
114
|
-
'epub-author'?: string
|
|
115
|
-
|
|
116
|
-
// Optional EPUB settings
|
|
117
|
-
'epub-publisher'?: string
|
|
118
|
-
'epub-cover'?: string
|
|
119
|
-
'epub-description'?: string
|
|
120
|
-
'epub-language'?: string
|
|
121
|
-
'epub-version'?: 2 | 3
|
|
122
|
-
'epub-stylesheet'?: string
|
|
123
|
-
'epub-theme'?: string
|
|
124
|
-
'epub-toc-title'?: string
|
|
125
|
-
'epub-hide-toc'?: boolean
|
|
126
|
-
'epub-timeout'?: number
|
|
127
|
-
'epub-fonts'?: string
|
|
128
|
-
'epub-chapter-title'?: string
|
|
129
|
-
'epub-preview'?: boolean
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
export const format = 'epub'
|
|
133
|
-
|
|
134
|
-
/**
|
|
135
|
-
* Exports a LiaScript course to EPUB format using Puppeteer and @lesjoursfr/html-to-epub.
|
|
136
|
-
*
|
|
137
|
-
* This function launches a headless Chrome browser, loads the LiaScript content,
|
|
138
|
-
* applies any custom styling or themes, extracts the rendered HTML DOM,
|
|
139
|
-
* and generates an EPUB file.
|
|
140
|
-
*
|
|
141
|
-
* @param argument - Configuration options for the EPUB export
|
|
142
|
-
* @throws {Error} If browser launch fails, page navigation fails, or EPUB generation fails
|
|
143
|
-
*
|
|
144
|
-
* @example
|
|
145
|
-
* ```typescript
|
|
146
|
-
* await exporter({
|
|
147
|
-
* input: './course.md',
|
|
148
|
-
* output: './output/book',
|
|
149
|
-
* 'epub-title': 'My Course',
|
|
150
|
-
* 'epub-author': 'John Doe',
|
|
151
|
-
* 'epub-language': 'en'
|
|
152
|
-
* })
|
|
153
|
-
* ```
|
|
154
|
-
*/
|
|
155
|
-
export async function exporter(argument: EpubExportArguments, json: any) {
|
|
156
|
-
const dirname = helper.dirname()
|
|
157
|
-
|
|
158
|
-
let url = `file://${dirname}/assets/pdf/index.html?`
|
|
159
|
-
if (helper.isURL(argument.input)) {
|
|
160
|
-
url += argument.input
|
|
161
|
-
} else {
|
|
162
|
-
url += 'file://' + path.resolve(argument.input)
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
let browser: Browser | null = null
|
|
166
|
-
let page: Page | null = null
|
|
167
|
-
|
|
168
|
-
try {
|
|
169
|
-
// Configure browser launch options
|
|
170
|
-
const launchOptions: any = {
|
|
171
|
-
pipe: true,
|
|
172
|
-
args: [
|
|
173
|
-
'--no-sandbox',
|
|
174
|
-
'--disable-web-security',
|
|
175
|
-
'--disable-features=IsolateOrigins',
|
|
176
|
-
'--disable-site-isolation-trials',
|
|
177
|
-
'--unhandled-rejections=strict',
|
|
178
|
-
'--disable-features=BlockInsecurePrivateNetworkRequests',
|
|
179
|
-
'--allow-file-access-from-files',
|
|
180
|
-
'--enable-local-file-accesses',
|
|
181
|
-
'--enable-features=ExperimentalJavaScript',
|
|
182
|
-
],
|
|
183
|
-
headless: !argument['epub-preview'],
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
// Use custom executable path if provided, otherwise use Chrome channel
|
|
187
|
-
if (process.env.PUPPETEER_EXECUTABLE_PATH) {
|
|
188
|
-
launchOptions.executablePath = process.env.PUPPETEER_EXECUTABLE_PATH
|
|
189
|
-
} else {
|
|
190
|
-
launchOptions.channel = 'chrome'
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
try {
|
|
194
|
-
browser = await puppeteer.launch(launchOptions)
|
|
195
|
-
} catch (launchError) {
|
|
196
|
-
throw new Error(
|
|
197
|
-
`Failed to launch browser for EPUB generation. Make sure Chrome is installed. ${launchError}`,
|
|
198
|
-
)
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
page = await browser.newPage()
|
|
202
|
-
console.log(
|
|
203
|
-
'Loading course content... This may take a while for large courses.',
|
|
204
|
-
)
|
|
205
|
-
|
|
206
|
-
page.on('dialog', async (dialog) => {
|
|
207
|
-
await dialog.accept()
|
|
208
|
-
})
|
|
209
|
-
|
|
210
|
-
// Set up render done listener BEFORE navigating to catch the signal
|
|
211
|
-
let renderDoneResolve: () => void
|
|
212
|
-
const renderDonePromise = new Promise<void>((resolve) => {
|
|
213
|
-
renderDoneResolve = resolve
|
|
214
|
-
})
|
|
215
|
-
|
|
216
|
-
page.on('console', (msg) => {
|
|
217
|
-
if (msg.text().startsWith('__RENDER_DONE__')) {
|
|
218
|
-
renderDoneResolve()
|
|
219
|
-
}
|
|
220
|
-
})
|
|
221
|
-
|
|
222
|
-
await page.setExtraHTTPHeaders({
|
|
223
|
-
referer: 'https://liascript.github.io/',
|
|
224
|
-
})
|
|
225
|
-
// Wait for page to load completely
|
|
226
|
-
// Using 'networkidle2' ensures all network requests are complete
|
|
227
|
-
// Timeout set to 0 (unlimited) to handle large courses
|
|
228
|
-
await page.goto(url, {
|
|
229
|
-
waitUntil: 'networkidle2',
|
|
230
|
-
timeout: DEFAULT_TIMEOUT_MS,
|
|
231
|
-
})
|
|
232
|
-
|
|
233
|
-
argument['epub-title'] = argument['epub-title'] || json.lia.str_title
|
|
234
|
-
argument['epub-cover'] = argument['epub-cover'] || json.lia.definition.logo
|
|
235
|
-
argument['epub-author'] = argument['epub-author'] || json.lia.definition.author
|
|
236
|
-
argument['epub-language'] = argument['epub-language'] || json.lia.definition.language
|
|
237
|
-
argument['epub-description'] = argument['epub-description'] || json.lia.definition.comment
|
|
238
|
-
|
|
239
|
-
if (argument['epub-stylesheet']) {
|
|
240
|
-
const href = path.resolve(dirname + '/../', argument['epub-stylesheet'])
|
|
241
|
-
try {
|
|
242
|
-
await page.evaluate(async (href) => {
|
|
243
|
-
const link = document.createElement('link')
|
|
244
|
-
link.rel = 'stylesheet'
|
|
245
|
-
link.href = href
|
|
246
|
-
const promise = new Promise((resolve, reject) => {
|
|
247
|
-
link.onload = resolve
|
|
248
|
-
link.onerror = reject
|
|
249
|
-
})
|
|
250
|
-
document.head.appendChild(link)
|
|
251
|
-
await promise
|
|
252
|
-
}, href)
|
|
253
|
-
} catch (e) {
|
|
254
|
-
throw new Error(
|
|
255
|
-
`Failed to load custom stylesheet from '${argument['epub-stylesheet']}': ${e}`,
|
|
256
|
-
)
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
if (argument['epub-theme']) {
|
|
261
|
-
try {
|
|
262
|
-
await page.evaluate(async (theme) => {
|
|
263
|
-
document.documentElement.classList.remove('lia-theme-default')
|
|
264
|
-
document.documentElement.classList.add('lia-theme-' + theme)
|
|
265
|
-
}, argument['epub-theme'])
|
|
266
|
-
} catch (e) {
|
|
267
|
-
throw new Error(
|
|
268
|
-
`Failed to apply theme '${argument['epub-theme']}': ${e}`,
|
|
269
|
-
)
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
if (!argument['epub-preview']) {
|
|
274
|
-
// Wait for LiaScript to signal rendering is complete
|
|
275
|
-
await renderDonePromise
|
|
276
|
-
|
|
277
|
-
if (argument['epub-timeout']) {
|
|
278
|
-
await helper.sleep(argument['epub-timeout'])
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
await toEPUB(argument, page, dirname)
|
|
282
|
-
} else {
|
|
283
|
-
console.log('Preview mode enabled - browser will remain open')
|
|
284
|
-
}
|
|
285
|
-
} catch (e) {
|
|
286
|
-
const error = e as Error
|
|
287
|
-
console.error('EPUB export failed:', error.message)
|
|
288
|
-
throw new Error(`Failed to export EPUB: ${error.message}`)
|
|
289
|
-
} finally {
|
|
290
|
-
if (argument['epub-preview']) {
|
|
291
|
-
console.log('Browser kept open for preview. Close manually when done.')
|
|
292
|
-
} else {
|
|
293
|
-
if (page) {
|
|
294
|
-
try {
|
|
295
|
-
await page.close()
|
|
296
|
-
} catch (closeError) {
|
|
297
|
-
console.error('Failed to close page:', closeError)
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
if (browser) {
|
|
301
|
-
try {
|
|
302
|
-
await browser.close()
|
|
303
|
-
} catch (closeError) {
|
|
304
|
-
console.error('Failed to close browser:', closeError)
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
/**
|
|
312
|
-
* Generates an EPUB file from a Puppeteer page.
|
|
313
|
-
*
|
|
314
|
-
* Extracts the rendered HTML DOM, processes images and resources,
|
|
315
|
-
* and creates an EPUB file using @lesjoursfr/html-to-epub.
|
|
316
|
-
*
|
|
317
|
-
* @param argument - EPUB export configuration options
|
|
318
|
-
* @param page - Puppeteer page instance containing the rendered content
|
|
319
|
-
* @param dirname - Base directory path
|
|
320
|
-
* @throws {Error} If EPUB generation fails or HTML extraction fails
|
|
321
|
-
*/
|
|
322
|
-
async function toEPUB(
|
|
323
|
-
argument: EpubExportArguments,
|
|
324
|
-
page: Page,
|
|
325
|
-
dirname: string,
|
|
326
|
-
) {
|
|
327
|
-
try {
|
|
328
|
-
console.log('Converting SVG diagrams to PNG images...')
|
|
329
|
-
const svgImages = await screenshotTaggedElements(
|
|
330
|
-
page,
|
|
331
|
-
'figure.lia-figure',
|
|
332
|
-
'data-svg-index',
|
|
333
|
-
'SVG diagram',
|
|
334
|
-
`return !!el.querySelector('.lia-figure__media svg')`,
|
|
335
|
-
)
|
|
336
|
-
console.log(`Converted ${svgImages.size} SVG diagrams to PNG`)
|
|
337
|
-
|
|
338
|
-
console.log('Extracting syntax-highlighted code blocks...')
|
|
339
|
-
const highlightedBlocks = await extractHighlightedCode(page)
|
|
340
|
-
console.log(`Extracted ${highlightedBlocks.size} highlighted code block(s)`)
|
|
341
|
-
|
|
342
|
-
console.log('Screenshotting ABC music notation blocks...')
|
|
343
|
-
const abcImages = await screenshotAbcBlocks(page)
|
|
344
|
-
console.log(`Captured ${abcImages.size} ABC notation block(s)`)
|
|
345
|
-
|
|
346
|
-
console.log('Screenshotting standalone ABC music notation blocks...')
|
|
347
|
-
const standaloneAbcImages = await screenshotStandaloneAbcBlocks(page)
|
|
348
|
-
console.log(
|
|
349
|
-
`Captured ${standaloneAbcImages.size} standalone ABC notation block(s)`,
|
|
350
|
-
)
|
|
351
|
-
|
|
352
|
-
console.log('Screenshotting embedded media (Spotify, SoundCloud, etc.)...')
|
|
353
|
-
const embedImages = await screenshotTaggedElements(
|
|
354
|
-
page,
|
|
355
|
-
'lia-embed',
|
|
356
|
-
'data-embed-index',
|
|
357
|
-
'embed',
|
|
358
|
-
)
|
|
359
|
-
console.log(`Captured ${embedImages.size} embed cover(s)`)
|
|
360
|
-
|
|
361
|
-
console.log('Extracting ECharts diagrams...')
|
|
362
|
-
const chartImages = await extractChartSvgs(page)
|
|
363
|
-
console.log(`Extracted ${chartImages.size} chart(s)`)
|
|
364
|
-
|
|
365
|
-
console.log('Extracting math formulas...')
|
|
366
|
-
const formulaHtml = await extractFormulaHtml(page)
|
|
367
|
-
console.log(`Extracted ${formulaHtml.size} formula(s)`)
|
|
368
|
-
|
|
369
|
-
// Hide formula accessibility/MathML content inside SVG foreignObjects before screenshotting
|
|
370
|
-
await page.evaluate(() => {
|
|
371
|
-
document
|
|
372
|
-
.querySelectorAll('svg foreignObject lia-formula')
|
|
373
|
-
.forEach((formula) => {
|
|
374
|
-
formula.childNodes.forEach((child) => {
|
|
375
|
-
if (child.nodeType === Node.ELEMENT_NODE) {
|
|
376
|
-
;(child as HTMLElement).style.display = 'none'
|
|
377
|
-
}
|
|
378
|
-
})
|
|
379
|
-
const shadow = formula.shadowRoot
|
|
380
|
-
if (shadow) {
|
|
381
|
-
shadow.querySelectorAll('.katex-mathml').forEach((mathml) => {
|
|
382
|
-
;(mathml as HTMLElement).style.display = 'none'
|
|
383
|
-
})
|
|
384
|
-
}
|
|
385
|
-
})
|
|
386
|
-
})
|
|
387
|
-
|
|
388
|
-
console.log('Screenshotting inline SVGs (foreignObject, interactive)...')
|
|
389
|
-
const inlineSvgImages = await screenshotTaggedElements(
|
|
390
|
-
page,
|
|
391
|
-
'svg',
|
|
392
|
-
'data-inline-svg-index',
|
|
393
|
-
'inline SVG',
|
|
394
|
-
// Only content SVGs with viewBox, not inside containers already handled
|
|
395
|
-
`if (!el.hasAttribute('viewBox')) return false;
|
|
396
|
-
const skip = el.closest('.lia-figure, .lia-code, lia-chart, lia-abcjs, lia-embed');
|
|
397
|
-
if (skip) return false;
|
|
398
|
-
// Skip tiny decorative SVGs (icons, spinners, etc.)
|
|
399
|
-
const rect = el.getBoundingClientRect();
|
|
400
|
-
if (rect.width < 50 || rect.height < 50) return false;
|
|
401
|
-
return true;`,
|
|
402
|
-
)
|
|
403
|
-
console.log(`Captured ${inlineSvgImages.size} inline SVG(s)`)
|
|
404
|
-
|
|
405
|
-
const toEntries = (map: Map<number, string>) =>
|
|
406
|
-
Array.from(map.entries()) as [number, string][]
|
|
407
|
-
const payload = {
|
|
408
|
-
svgImages: toEntries(svgImages),
|
|
409
|
-
codeBlocks: toEntries(highlightedBlocks),
|
|
410
|
-
abcImages: toEntries(abcImages),
|
|
411
|
-
standaloneAbcImages: toEntries(standaloneAbcImages),
|
|
412
|
-
embedImages: toEntries(embedImages),
|
|
413
|
-
chartImages: toEntries(chartImages),
|
|
414
|
-
formulas: toEntries(formulaHtml),
|
|
415
|
-
inlineSvgImages: toEntries(inlineSvgImages),
|
|
416
|
-
}
|
|
417
|
-
const chapters = await page.evaluate((data) => {
|
|
418
|
-
const {
|
|
419
|
-
svgImages: svgImagesData,
|
|
420
|
-
codeBlocks,
|
|
421
|
-
abcImages: abcImagesData,
|
|
422
|
-
standaloneAbcImages: standaloneAbcImagesData,
|
|
423
|
-
embedImages: embedImagesData,
|
|
424
|
-
chartImages: chartImagesData,
|
|
425
|
-
formulas: formulasData,
|
|
426
|
-
inlineSvgImages: inlineSvgImagesData,
|
|
427
|
-
} = data
|
|
428
|
-
const bodyClone = document.body.cloneNode(true) as HTMLElement
|
|
429
|
-
|
|
430
|
-
// Remove problematic link tags
|
|
431
|
-
bodyClone
|
|
432
|
-
.querySelectorAll(
|
|
433
|
-
'link[rel="icon"], link[rel="shortcut icon"], link[rel="apple-touch-icon"], ' +
|
|
434
|
-
'link[rel="manifest"], link[rel="preconnect"], link[rel="dns-prefetch"], ' +
|
|
435
|
-
'link[rel="preload"], link[rel="stylesheet"]',
|
|
436
|
-
)
|
|
437
|
-
.forEach((link: any) => link.remove())
|
|
438
|
-
|
|
439
|
-
// Remove the copy-to-clipboard button from code blocks
|
|
440
|
-
bodyClone
|
|
441
|
-
.querySelectorAll('.lia-code__copy, .lia-code__copy--inverted')
|
|
442
|
-
.forEach((btn: Element) => btn.remove())
|
|
443
|
-
|
|
444
|
-
// Remove run/version control toolbar
|
|
445
|
-
bodyClone
|
|
446
|
-
.querySelectorAll('.lia-code-control')
|
|
447
|
-
.forEach((ctrl: Element) => ctrl.remove())
|
|
448
|
-
|
|
449
|
-
// Helper: replace tagged elements with their pre-captured images
|
|
450
|
-
const replaceWithImage = (
|
|
451
|
-
selector: string,
|
|
452
|
-
dataAttr: string,
|
|
453
|
-
images: [number, string][],
|
|
454
|
-
options: {
|
|
455
|
-
alt: string | ((el: Element) => string)
|
|
456
|
-
wrapInFigure?: boolean // true → wrap <img> in a new <figure>
|
|
457
|
-
reuseAsContainer?: boolean // true → clear element innerHTML and append <img> inside it
|
|
458
|
-
imgStyle?: string | ((el: Element) => string)
|
|
459
|
-
figureStyle?: string
|
|
460
|
-
},
|
|
461
|
-
) => {
|
|
462
|
-
const defaultImgStyle =
|
|
463
|
-
'max-width: 100%; height: auto; display: block; margin: 0 auto;'
|
|
464
|
-
bodyClone.querySelectorAll(selector).forEach((el: Element) => {
|
|
465
|
-
try {
|
|
466
|
-
const idx = parseInt(el.getAttribute(dataAttr) || '-1')
|
|
467
|
-
const entry = images.find((item: any) => item[0] === idx)
|
|
468
|
-
if (!entry || !entry[1]) {
|
|
469
|
-
el.remove()
|
|
470
|
-
return
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
const img = document.createElement('img')
|
|
474
|
-
img.src = entry[1]
|
|
475
|
-
img.alt =
|
|
476
|
-
typeof options.alt === 'function' ? options.alt(el) : options.alt
|
|
477
|
-
const style =
|
|
478
|
-
typeof options.imgStyle === 'function'
|
|
479
|
-
? options.imgStyle(el)
|
|
480
|
-
: (options.imgStyle ?? defaultImgStyle)
|
|
481
|
-
img.setAttribute('style', style)
|
|
482
|
-
|
|
483
|
-
if (options.reuseAsContainer) {
|
|
484
|
-
el.innerHTML = ''
|
|
485
|
-
el.appendChild(img)
|
|
486
|
-
if (options.figureStyle)
|
|
487
|
-
el.setAttribute('style', options.figureStyle)
|
|
488
|
-
} else if (options.wrapInFigure) {
|
|
489
|
-
const figure = document.createElement('figure')
|
|
490
|
-
figure.setAttribute(
|
|
491
|
-
'style',
|
|
492
|
-
options.figureStyle ||
|
|
493
|
-
'margin: 1.5em auto; text-align: center; page-break-inside: avoid;',
|
|
494
|
-
)
|
|
495
|
-
figure.appendChild(img)
|
|
496
|
-
el.replaceWith(figure)
|
|
497
|
-
} else {
|
|
498
|
-
el.replaceWith(img)
|
|
499
|
-
}
|
|
500
|
-
} catch (e) {
|
|
501
|
-
console.error(`Error replacing ${selector}:`, e)
|
|
502
|
-
}
|
|
503
|
-
})
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
// Replace <lia-embed> elements with their pre-captured cover screenshots
|
|
507
|
-
replaceWithImage(
|
|
508
|
-
'lia-embed[data-embed-index]',
|
|
509
|
-
'data-embed-index',
|
|
510
|
-
embedImagesData,
|
|
511
|
-
{
|
|
512
|
-
alt: 'Embedded media',
|
|
513
|
-
imgStyle: (el) =>
|
|
514
|
-
el.getAttribute('style') || 'width:250px;height:250px;',
|
|
515
|
-
},
|
|
516
|
-
)
|
|
517
|
-
|
|
518
|
-
// Replace <lia-chart> elements with their extracted SVG images
|
|
519
|
-
replaceWithImage(
|
|
520
|
-
'lia-chart[data-chart-index]',
|
|
521
|
-
'data-chart-index',
|
|
522
|
-
chartImagesData,
|
|
523
|
-
{
|
|
524
|
-
alt: (el) => el.getAttribute('aria-label') || 'Chart',
|
|
525
|
-
wrapInFigure: true,
|
|
526
|
-
},
|
|
527
|
-
)
|
|
528
|
-
|
|
529
|
-
// Replace ABC notation blocks with pre-captured SVG
|
|
530
|
-
replaceWithImage(
|
|
531
|
-
'.lia-code-terminal[data-abc-index]',
|
|
532
|
-
'data-abc-index',
|
|
533
|
-
abcImagesData,
|
|
534
|
-
{
|
|
535
|
-
alt: 'ABC Music Notation',
|
|
536
|
-
wrapInFigure: true,
|
|
537
|
-
},
|
|
538
|
-
)
|
|
539
|
-
|
|
540
|
-
// Replace standalone ABC notation elements (from template macros like @ABCJS.render)
|
|
541
|
-
replaceWithImage(
|
|
542
|
-
'lia-abcjs[data-standalone-abc-index]',
|
|
543
|
-
'data-standalone-abc-index',
|
|
544
|
-
standaloneAbcImagesData,
|
|
545
|
-
{
|
|
546
|
-
alt: 'ABC Music Notation',
|
|
547
|
-
wrapInFigure: true,
|
|
548
|
-
},
|
|
549
|
-
)
|
|
550
|
-
|
|
551
|
-
// Replace SVG diagrams with PNG images for better EPUB compatibility
|
|
552
|
-
replaceWithImage(
|
|
553
|
-
'figure.lia-figure[data-svg-index]',
|
|
554
|
-
'data-svg-index',
|
|
555
|
-
svgImagesData,
|
|
556
|
-
{
|
|
557
|
-
alt: 'ASCII Diagram',
|
|
558
|
-
reuseAsContainer: true,
|
|
559
|
-
figureStyle:
|
|
560
|
-
'margin: 1.5em auto; padding: 1.5em; background-color: #f8f9fa; ' +
|
|
561
|
-
'border: 1px solid #dee2e6; border-radius: 4px; text-align: center; ' +
|
|
562
|
-
'page-break-inside: avoid; max-width: 90%;',
|
|
563
|
-
},
|
|
564
|
-
)
|
|
565
|
-
|
|
566
|
-
// Replace inline SVGs (foreignObject, interactive content) with PNG screenshots
|
|
567
|
-
replaceWithImage(
|
|
568
|
-
'svg[data-inline-svg-index]',
|
|
569
|
-
'data-inline-svg-index',
|
|
570
|
-
inlineSvgImagesData,
|
|
571
|
-
{
|
|
572
|
-
alt: 'SVG Graphic',
|
|
573
|
-
wrapInFigure: true,
|
|
574
|
-
figureStyle:
|
|
575
|
-
'margin: 1.5em auto; text-align: center; page-break-inside: avoid;',
|
|
576
|
-
},
|
|
577
|
-
)
|
|
578
|
-
|
|
579
|
-
// Replace <lia-formula> elements with their pre-extracted MathML/KaTeX HTML
|
|
580
|
-
bodyClone
|
|
581
|
-
.querySelectorAll('lia-formula[data-formula-index]')
|
|
582
|
-
.forEach((formula: Element) => {
|
|
583
|
-
try {
|
|
584
|
-
const idx = parseInt(
|
|
585
|
-
formula.getAttribute('data-formula-index') || '-1',
|
|
586
|
-
)
|
|
587
|
-
const entry = formulasData.find((item: any) => item[0] === idx)
|
|
588
|
-
if (entry && entry[1]) {
|
|
589
|
-
const isBlock = formula.getAttribute('displaymode') === 'true'
|
|
590
|
-
const wrapper = document.createElement(isBlock ? 'div' : 'span')
|
|
591
|
-
wrapper.innerHTML = entry[1]
|
|
592
|
-
|
|
593
|
-
// Set display attribute on <math> element for EPUB3 MathML rendering
|
|
594
|
-
const mathEl = wrapper.querySelector('math')
|
|
595
|
-
if (mathEl) {
|
|
596
|
-
mathEl.setAttribute('display', isBlock ? 'block' : 'inline')
|
|
597
|
-
if (isBlock) {
|
|
598
|
-
wrapper.setAttribute(
|
|
599
|
-
'style',
|
|
600
|
-
'text-align: center; margin: 1em 0;',
|
|
601
|
-
)
|
|
602
|
-
}
|
|
603
|
-
}
|
|
604
|
-
|
|
605
|
-
formula.replaceWith(wrapper)
|
|
606
|
-
}
|
|
607
|
-
} catch (e) {
|
|
608
|
-
console.error('Error replacing formula:', e)
|
|
609
|
-
}
|
|
610
|
-
})
|
|
611
|
-
|
|
612
|
-
// Process terminal output blocks
|
|
613
|
-
bodyClone
|
|
614
|
-
.querySelectorAll('.lia-code-terminal')
|
|
615
|
-
.forEach((terminal: Element) => {
|
|
616
|
-
try {
|
|
617
|
-
// Skip if this is an ABC block (already handled above)
|
|
618
|
-
if (terminal.querySelector('lia-abcjs')) {
|
|
619
|
-
terminal.remove()
|
|
620
|
-
return
|
|
621
|
-
}
|
|
622
|
-
|
|
623
|
-
// If the terminal contains rendered MathML (from @runFormula macro)
|
|
624
|
-
// Extract the formula and replace the terminal with it
|
|
625
|
-
const mathEls = terminal.querySelectorAll('math')
|
|
626
|
-
if (mathEls.length > 0) {
|
|
627
|
-
const container = document.createElement('div')
|
|
628
|
-
container.setAttribute(
|
|
629
|
-
'style',
|
|
630
|
-
'text-align: center; margin: 1em 0; background-color: #1e1e1e; color: #d4d4d4; padding: 1em; border-radius: 4px;',
|
|
631
|
-
)
|
|
632
|
-
mathEls.forEach((m) => {
|
|
633
|
-
m.setAttribute('display', 'block')
|
|
634
|
-
container.appendChild(m.cloneNode(true))
|
|
635
|
-
})
|
|
636
|
-
terminal.replaceWith(container)
|
|
637
|
-
return
|
|
638
|
-
}
|
|
639
|
-
|
|
640
|
-
// If terminal still has an unreplaced <lia-formula> (no data extracted), remove it
|
|
641
|
-
if (terminal.querySelector('lia-formula')) {
|
|
642
|
-
terminal.remove()
|
|
643
|
-
return
|
|
644
|
-
}
|
|
645
|
-
|
|
646
|
-
const terminalOutput = terminal.querySelector('lia-terminal')
|
|
647
|
-
if (!terminalOutput) return
|
|
648
|
-
|
|
649
|
-
const pre = document.createElement('pre')
|
|
650
|
-
const code = document.createElement('code')
|
|
651
|
-
|
|
652
|
-
pre.setAttribute(
|
|
653
|
-
'style',
|
|
654
|
-
'background-color: #1e1e1e; color: #d4d4d4; padding: 1em; ' +
|
|
655
|
-
'border-radius: 4px; font-family: monospace; overflow-x: auto;',
|
|
656
|
-
)
|
|
657
|
-
|
|
658
|
-
const textDivs = terminalOutput.querySelectorAll(
|
|
659
|
-
'div[class^="text-"]',
|
|
660
|
-
)
|
|
661
|
-
if (textDivs.length > 0) {
|
|
662
|
-
textDivs.forEach((div: Element) => {
|
|
663
|
-
const span = document.createElement('span')
|
|
664
|
-
span.textContent = div.textContent || ''
|
|
665
|
-
if (div.classList.contains('text-error')) {
|
|
666
|
-
span.setAttribute('style', 'color: #f48771;')
|
|
667
|
-
} else if (div.classList.contains('text-warning')) {
|
|
668
|
-
span.setAttribute('style', 'color: #dcdcaa;')
|
|
669
|
-
}
|
|
670
|
-
code.appendChild(span)
|
|
671
|
-
code.appendChild(document.createTextNode('\n'))
|
|
672
|
-
})
|
|
673
|
-
} else {
|
|
674
|
-
code.textContent = terminalOutput.textContent || ''
|
|
675
|
-
}
|
|
676
|
-
|
|
677
|
-
pre.appendChild(code)
|
|
678
|
-
terminal.replaceWith(pre)
|
|
679
|
-
} catch (e) {
|
|
680
|
-
console.error('Error processing terminal block:', e)
|
|
681
|
-
}
|
|
682
|
-
})
|
|
683
|
-
|
|
684
|
-
// Replace Ace Editor code blocks with pre-extracted syntax-highlighted HTML
|
|
685
|
-
bodyClone
|
|
686
|
-
.querySelectorAll('.lia-code__input')
|
|
687
|
-
.forEach((codeInput: Element) => {
|
|
688
|
-
try {
|
|
689
|
-
const idx = parseInt(
|
|
690
|
-
codeInput.getAttribute('data-code-index') || '-1',
|
|
691
|
-
)
|
|
692
|
-
const entry = codeBlocks.find((item) => item[0] === idx)
|
|
693
|
-
if (entry && entry[1]) {
|
|
694
|
-
const wrapper = document.createElement('div')
|
|
695
|
-
wrapper.innerHTML = entry[1]
|
|
696
|
-
codeInput.replaceWith(wrapper.firstChild || wrapper)
|
|
697
|
-
}
|
|
698
|
-
} catch (e) {
|
|
699
|
-
console.error('Error injecting highlighted code block:', e)
|
|
700
|
-
}
|
|
701
|
-
})
|
|
702
|
-
|
|
703
|
-
// Extract chapters from <main> elements
|
|
704
|
-
const mainElements = bodyClone.querySelectorAll('main')
|
|
705
|
-
const chapterList: Array<{ title: string; data: string }> = []
|
|
706
|
-
|
|
707
|
-
if (mainElements.length > 0) {
|
|
708
|
-
mainElements.forEach((main: Element, index: number) => {
|
|
709
|
-
let chapterTitle = `Chapter ${index + 1}`
|
|
710
|
-
const headerEl = main.querySelector('header')
|
|
711
|
-
const hTag = headerEl?.querySelector('.h1, .h2, .h3, .h4, .h5, .h6')
|
|
712
|
-
if (hTag?.textContent) {
|
|
713
|
-
chapterTitle = hTag.textContent.trim()
|
|
714
|
-
}
|
|
715
|
-
// Remove the header from DOM to avoid duplicate titles
|
|
716
|
-
if (headerEl) {
|
|
717
|
-
headerEl.remove()
|
|
718
|
-
}
|
|
719
|
-
|
|
720
|
-
main.querySelectorAll('script').forEach((el: Element) => el.remove())
|
|
721
|
-
|
|
722
|
-
const tempDiv = document.createElement('div')
|
|
723
|
-
tempDiv.appendChild(main.cloneNode(true))
|
|
724
|
-
chapterList.push({ title: chapterTitle, data: tempDiv.innerHTML })
|
|
725
|
-
})
|
|
726
|
-
|
|
727
|
-
return chapterList
|
|
728
|
-
} else {
|
|
729
|
-
bodyClone
|
|
730
|
-
.querySelectorAll('script')
|
|
731
|
-
.forEach((el: Element) => el.remove())
|
|
732
|
-
return [{ title: 'Content', data: bodyClone.outerHTML }]
|
|
733
|
-
}
|
|
734
|
-
}, payload)
|
|
735
|
-
|
|
736
|
-
// Convert external/URL-based images to data URIs
|
|
737
|
-
console.log('Fetching external/URL-based images as data URIs...')
|
|
738
|
-
for (const chapter of chapters) {
|
|
739
|
-
chapter.data = await page.evaluate(async (html: string) => {
|
|
740
|
-
const parser = new DOMParser()
|
|
741
|
-
const doc = parser.parseFromString(html, 'text/html')
|
|
742
|
-
|
|
743
|
-
const imgs = Array.from(doc.querySelectorAll('img[src]'))
|
|
744
|
-
await Promise.all(
|
|
745
|
-
imgs.map(async (img) => {
|
|
746
|
-
const src = img.getAttribute('src')!
|
|
747
|
-
// Skip already-inlined data URIs and local file:// paths
|
|
748
|
-
if (src.startsWith('data:') || src.startsWith('file://')) return
|
|
749
|
-
|
|
750
|
-
try {
|
|
751
|
-
const response = await fetch(src)
|
|
752
|
-
if (!response.ok) return
|
|
753
|
-
const blob = await response.blob()
|
|
754
|
-
const dataUri = await new Promise<string>((resolve) => {
|
|
755
|
-
const reader = new FileReader()
|
|
756
|
-
reader.onload = () => resolve(reader.result as string)
|
|
757
|
-
reader.readAsDataURL(blob)
|
|
758
|
-
})
|
|
759
|
-
img.setAttribute('src', dataUri)
|
|
760
|
-
} catch (e) {
|
|
761
|
-
console.warn('Failed to fetch image:', src, e)
|
|
762
|
-
}
|
|
763
|
-
}),
|
|
764
|
-
)
|
|
765
|
-
|
|
766
|
-
return doc.body.innerHTML
|
|
767
|
-
}, chapter.data)
|
|
768
|
-
}
|
|
769
|
-
|
|
770
|
-
// Sanitize chapter HTML for XHTML/XML compatibility
|
|
771
|
-
for (const chapter of chapters) {
|
|
772
|
-
// Strip HTML comments — XML forbids "--" inside comment bodies
|
|
773
|
-
chapter.data = chapter.data.replace(/<!--[\s\S]*?-->/g, '')
|
|
774
|
-
|
|
775
|
-
// Escape namespace-prefixed tags like <jc:trillian.mit.edu> that aren't valid HTML elements
|
|
776
|
-
chapter.data = chapter.data.replace(
|
|
777
|
-
/<((?!\/?\s*(?:svg|math|xlink|xml|xmlns)[:\s>])[a-zA-Z][a-zA-Z0-9]*:[^\s>]+[^>]*)>/g,
|
|
778
|
-
'<$1>',
|
|
779
|
-
)
|
|
780
|
-
}
|
|
781
|
-
|
|
782
|
-
// Read CSS and fonts from the pdf assets folder
|
|
783
|
-
const pdfAssetsPath = path.join(dirname, 'assets', 'pdf')
|
|
784
|
-
const cssFiles = fs
|
|
785
|
-
.readdirSync(pdfAssetsPath)
|
|
786
|
-
.filter((f) => f.endsWith('.css'))
|
|
787
|
-
|
|
788
|
-
let allCSS = ''
|
|
789
|
-
const fontPaths: string[] = []
|
|
790
|
-
|
|
791
|
-
if (cssFiles.length > 0) {
|
|
792
|
-
cssFiles.forEach((cssFile) => {
|
|
793
|
-
allCSS +=
|
|
794
|
-
fs.readFileSync(path.join(pdfAssetsPath, cssFile), 'utf-8') + '\n'
|
|
795
|
-
})
|
|
796
|
-
|
|
797
|
-
const fontMatches = allCSS.match(
|
|
798
|
-
/url\(['"]?([^'")\s]+\.(?:woff2?|ttf|otf|eot))['"]?\)/gi,
|
|
799
|
-
)
|
|
800
|
-
if (fontMatches) {
|
|
801
|
-
const uniqueFonts = new Set<string>()
|
|
802
|
-
fontMatches.forEach((match) => {
|
|
803
|
-
const fontMatch = match.match(/url\(['"]?([^'")\s]+)['"]?\)/)
|
|
804
|
-
if (fontMatch?.[1]) {
|
|
805
|
-
uniqueFonts.add(path.basename(fontMatch[1]))
|
|
806
|
-
}
|
|
807
|
-
})
|
|
808
|
-
uniqueFonts.forEach((fontFilename) => {
|
|
809
|
-
const fontPath = path.join(pdfAssetsPath, fontFilename)
|
|
810
|
-
if (fs.existsSync(fontPath)) {
|
|
811
|
-
fontPaths.push(fontPath)
|
|
812
|
-
} else {
|
|
813
|
-
console.warn(`Warning: Font file not found: ${fontPath}`)
|
|
814
|
-
}
|
|
815
|
-
})
|
|
816
|
-
}
|
|
817
|
-
} else {
|
|
818
|
-
console.warn('Warning: No CSS files found in pdf assets folder')
|
|
819
|
-
}
|
|
820
|
-
|
|
821
|
-
// Include KaTeX font files not already referenced in CSS
|
|
822
|
-
fs.readdirSync(pdfAssetsPath)
|
|
823
|
-
.filter(
|
|
824
|
-
(f) =>
|
|
825
|
-
f.startsWith('KaTeX_') &&
|
|
826
|
-
(f.endsWith('.woff') || f.endsWith('.woff2') || f.endsWith('.ttf')),
|
|
827
|
-
)
|
|
828
|
-
.forEach((fontFile) => {
|
|
829
|
-
const fontPath = path.join(pdfAssetsPath, fontFile)
|
|
830
|
-
if (!fontPaths.includes(fontPath) && fs.existsSync(fontPath)) {
|
|
831
|
-
fontPaths.push(fontPath)
|
|
832
|
-
}
|
|
833
|
-
})
|
|
834
|
-
|
|
835
|
-
console.log(
|
|
836
|
-
`Building EPUB with ${chapters.length} chapter(s) and ${fontPaths.length} font(s)...`,
|
|
837
|
-
)
|
|
838
|
-
|
|
839
|
-
let authors: string | string[] = argument['epub-author'] || 'Unknown'
|
|
840
|
-
if (typeof authors === 'string' && authors.includes(';')) {
|
|
841
|
-
authors = authors.split(';').map((a) => a.trim())
|
|
842
|
-
}
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
let fonts: string[] = [...fontPaths]
|
|
846
|
-
if (argument['epub-fonts']) {
|
|
847
|
-
fonts.push(...argument['epub-fonts'].split(',').map((f) => f.trim()))
|
|
848
|
-
}
|
|
849
|
-
|
|
850
|
-
let customCSS = allCSS
|
|
851
|
-
if (argument['epub-stylesheet']) {
|
|
852
|
-
try {
|
|
853
|
-
customCSS =
|
|
854
|
-
customCSS +
|
|
855
|
-
'\n' +
|
|
856
|
-
fs.readFileSync(path.resolve(argument['epub-stylesheet']), 'utf-8')
|
|
857
|
-
} catch (e) {
|
|
858
|
-
console.warn(`Warning: Could not read custom stylesheet: ${e}`)
|
|
859
|
-
}
|
|
860
|
-
}
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
const epubOptions: any = {
|
|
865
|
-
title: argument['epub-title'] ,
|
|
866
|
-
author: authors,
|
|
867
|
-
lang: argument['epub-language'] || DEFAULT_LANG,
|
|
868
|
-
tocTitle: argument['epub-toc-title'] || DEFAULT_TOC_TITLE,
|
|
869
|
-
appendChapterTitles:
|
|
870
|
-
argument['epub-chapter-title'] === undefined &&
|
|
871
|
-
DEFAULT_APPEND_CHAPTER_TITLES,
|
|
872
|
-
hideToC: argument['epub-hide-toc'] ?? DEFAULT_HIDE_TOC,
|
|
873
|
-
css: customCSS,
|
|
874
|
-
version: (argument['epub-version'] || DEFAULT_EPUB_VERSION) as 2 | 3,
|
|
875
|
-
content: chapters,
|
|
876
|
-
verbose: false,
|
|
877
|
-
tempDir: path.join(tmpdir(), 'liaex-epub-temp'),
|
|
878
|
-
}
|
|
879
|
-
|
|
880
|
-
if (argument['epub-publisher'])
|
|
881
|
-
epubOptions.publisher = argument['epub-publisher']
|
|
882
|
-
if (argument['epub-cover']) epubOptions.cover = argument['epub-cover']
|
|
883
|
-
if (argument['epub-description'])
|
|
884
|
-
epubOptions.description = argument['epub-description']
|
|
885
|
-
if (fonts.length > 0) epubOptions.fonts = fonts
|
|
886
|
-
|
|
887
|
-
const outputPath = argument.output.endsWith('.epub')
|
|
888
|
-
? argument.output
|
|
889
|
-
: argument.output + '.epub'
|
|
890
|
-
|
|
891
|
-
// Use dynamic import with a variable to prevent Parcel from converting it to require()
|
|
892
|
-
// The @lesjoursfr/html-to-epub package is ESM-only and cannot be require()'d
|
|
893
|
-
const epubModuleName = '@lesjoursfr/html-to-epub'
|
|
894
|
-
const epubModule = await (
|
|
895
|
-
new Function('m', 'return import(m)') as (m: string) => Promise<any>
|
|
896
|
-
)(epubModuleName)
|
|
897
|
-
const EPub = epubModule.EPub
|
|
898
|
-
await new EPub(epubOptions, outputPath).render()
|
|
899
|
-
console.log(`EPUB successfully generated: ${outputPath}`)
|
|
900
|
-
} catch (e) {
|
|
901
|
-
const error = e as Error
|
|
902
|
-
throw new Error(`Failed to generate EPUB: ${error.message}`)
|
|
903
|
-
}
|
|
904
|
-
}
|
|
905
|
-
|
|
906
|
-
/**
|
|
907
|
-
* Extracts syntax-highlighted HTML from all Ace Editor code blocks in the live page
|
|
908
|
-
*
|
|
909
|
-
* @param page - Puppeteer page instance
|
|
910
|
-
* @returns Map of block indexes to static highlighted HTML strings
|
|
911
|
-
*/
|
|
912
|
-
async function extractHighlightedCode(
|
|
913
|
-
page: Page,
|
|
914
|
-
): Promise<Map<number, string>> {
|
|
915
|
-
const result = await page.evaluate(() => {
|
|
916
|
-
const escape = (text: string) =>
|
|
917
|
-
text
|
|
918
|
-
.replace(/&/g, '&')
|
|
919
|
-
.replace(/</g, '<')
|
|
920
|
-
.replace(/>/g, '>')
|
|
921
|
-
.replace(/"/g, '"')
|
|
922
|
-
.replace(/'/g, ''')
|
|
923
|
-
|
|
924
|
-
const blocks: [number, string][] = []
|
|
925
|
-
let idx = 0
|
|
926
|
-
|
|
927
|
-
document
|
|
928
|
-
.querySelectorAll('.lia-code__input')
|
|
929
|
-
.forEach((codeInput: Element) => {
|
|
930
|
-
codeInput.setAttribute('data-code-index', idx.toString())
|
|
931
|
-
|
|
932
|
-
try {
|
|
933
|
-
const aceEditor = codeInput.querySelector('.ace_editor')
|
|
934
|
-
const aceContent = aceEditor?.querySelector('.ace_text-layer')
|
|
935
|
-
|
|
936
|
-
if (!aceEditor || !aceContent) {
|
|
937
|
-
blocks.push([idx, ''])
|
|
938
|
-
idx++
|
|
939
|
-
return
|
|
940
|
-
}
|
|
941
|
-
|
|
942
|
-
const gutterCells = aceEditor.querySelectorAll('.ace_gutter-cell')
|
|
943
|
-
const lineNumbers: string[] = []
|
|
944
|
-
gutterCells.forEach((cell: Element) => {
|
|
945
|
-
const n = cell.textContent?.trim()
|
|
946
|
-
if (n && !isNaN(parseInt(n))) lineNumbers.push(n)
|
|
947
|
-
})
|
|
948
|
-
|
|
949
|
-
const bgColor =
|
|
950
|
-
window.getComputedStyle(aceEditor as HTMLElement).backgroundColor ||
|
|
951
|
-
'#f5f5f5'
|
|
952
|
-
|
|
953
|
-
let codeHTML = ''
|
|
954
|
-
let lineIndex = 0
|
|
955
|
-
|
|
956
|
-
// Helper: render a single .ace_line element's content as HTML string
|
|
957
|
-
const renderLineContent = (line: Element): string => {
|
|
958
|
-
let lineHTML = ''
|
|
959
|
-
const hasTokenSpans =
|
|
960
|
-
line.querySelector('span[class*="ace_"]') !== null
|
|
961
|
-
if (hasTokenSpans) {
|
|
962
|
-
line.childNodes.forEach((node: ChildNode) => {
|
|
963
|
-
if (node.nodeType === Node.TEXT_NODE) {
|
|
964
|
-
// Plain text node — raw space/whitespace between tokens
|
|
965
|
-
const text = node.textContent || ''
|
|
966
|
-
// Strip Ace zero-width / invisible control chars but keep real spaces
|
|
967
|
-
const cleaned = text.replace(/[\u200B-\u200D\uFEFF]/g, '')
|
|
968
|
-
if (cleaned) lineHTML += escape(cleaned)
|
|
969
|
-
} else if (node.nodeType === Node.ELEMENT_NODE) {
|
|
970
|
-
const tokenEl = node as HTMLElement
|
|
971
|
-
// Recurse one level: Ace sometimes nests spans (e.g. ace_indent_guide inside ace_indent)
|
|
972
|
-
const text = tokenEl.textContent || ''
|
|
973
|
-
if (!text) return
|
|
974
|
-
const cleaned = text.replace(/[\u200B-\u200D\uFEFF]/g, '')
|
|
975
|
-
if (!cleaned) return
|
|
976
|
-
const color = window.getComputedStyle(tokenEl).color
|
|
977
|
-
const fontWeight = window.getComputedStyle(tokenEl).fontWeight
|
|
978
|
-
const fontStyle = window.getComputedStyle(tokenEl).fontStyle
|
|
979
|
-
const escaped = escape(cleaned)
|
|
980
|
-
let style = ''
|
|
981
|
-
if (
|
|
982
|
-
color &&
|
|
983
|
-
color !== 'rgb(0, 0, 0)' &&
|
|
984
|
-
color !== 'rgba(0, 0, 0, 0)'
|
|
985
|
-
) {
|
|
986
|
-
style += `color:${color};`
|
|
987
|
-
}
|
|
988
|
-
if (fontWeight === 'bold' || parseInt(fontWeight) >= 700)
|
|
989
|
-
style += 'font-weight:bold;'
|
|
990
|
-
if (fontStyle === 'italic') style += 'font-style:italic;'
|
|
991
|
-
lineHTML += style
|
|
992
|
-
? `<span style="${style}">${escaped}</span>`
|
|
993
|
-
: escaped
|
|
994
|
-
}
|
|
995
|
-
})
|
|
996
|
-
} else {
|
|
997
|
-
const cleanText = (line.textContent || '').replace(
|
|
998
|
-
/[\u200B-\u200D\uFEFF\n]/g,
|
|
999
|
-
'',
|
|
1000
|
-
)
|
|
1001
|
-
lineHTML += escape(cleanText)
|
|
1002
|
-
}
|
|
1003
|
-
return lineHTML
|
|
1004
|
-
}
|
|
1005
|
-
|
|
1006
|
-
aceContent
|
|
1007
|
-
.querySelectorAll('.ace_line_group')
|
|
1008
|
-
.forEach((lineGroup: Element) => {
|
|
1009
|
-
// Each .ace_line_group represents ONE logical source line
|
|
1010
|
-
const subLines = lineGroup.querySelectorAll('.ace_line')
|
|
1011
|
-
if (subLines.length === 0) return
|
|
1012
|
-
|
|
1013
|
-
let lineHTML = ''
|
|
1014
|
-
|
|
1015
|
-
// Emit the gutter line number for this logical line
|
|
1016
|
-
if (lineIndex < lineNumbers.length) {
|
|
1017
|
-
lineHTML += `<span style="color:#858585;display:inline-block;width:3em;text-align:right;margin-right:1em;user-select:none;">${lineNumbers[lineIndex]}</span>`
|
|
1018
|
-
lineIndex++
|
|
1019
|
-
}
|
|
1020
|
-
|
|
1021
|
-
// Concatenate content from every sub-line (soft-wrap continuations)
|
|
1022
|
-
subLines.forEach((line: Element) => {
|
|
1023
|
-
lineHTML += renderLineContent(line)
|
|
1024
|
-
})
|
|
1025
|
-
|
|
1026
|
-
codeHTML += `<span style="display:block;">${lineHTML}\n</span>`
|
|
1027
|
-
})
|
|
1028
|
-
|
|
1029
|
-
const html =
|
|
1030
|
-
`<pre style="background-color:${bgColor};padding:1em;border-radius:4px;` +
|
|
1031
|
-
`border-left:3px solid #4caf50;overflow-x:auto;font-family:monospace;` +
|
|
1032
|
-
`white-space:pre;margin:0.5em 0;">` +
|
|
1033
|
-
`<code style="display:block;">${codeHTML}</code></pre>`
|
|
1034
|
-
|
|
1035
|
-
blocks.push([idx, html])
|
|
1036
|
-
} catch (e) {
|
|
1037
|
-
blocks.push([idx, ''])
|
|
1038
|
-
}
|
|
1039
|
-
|
|
1040
|
-
idx++
|
|
1041
|
-
})
|
|
1042
|
-
|
|
1043
|
-
return blocks
|
|
1044
|
-
})
|
|
1045
|
-
|
|
1046
|
-
return new Map(result)
|
|
1047
|
-
}
|
|
1048
|
-
|
|
1049
|
-
/**
|
|
1050
|
-
* Screenshots every <lia-abcjs> music-notation element
|
|
1051
|
-
*
|
|
1052
|
-
* @param page - Puppeteer page instance
|
|
1053
|
-
* @returns Map of ABC block indexes to base64 PNG data URIs
|
|
1054
|
-
*/
|
|
1055
|
-
async function screenshotAbcBlocks(page: Page): Promise<Map<number, string>> {
|
|
1056
|
-
const abcImages = new Map<number, string>()
|
|
1057
|
-
|
|
1058
|
-
// Tag each .lia-code-terminal that wraps a <lia-abcjs> element
|
|
1059
|
-
const abcCount = await page.evaluate(() => {
|
|
1060
|
-
let abcIndex = 0
|
|
1061
|
-
document
|
|
1062
|
-
.querySelectorAll('.lia-code-terminal')
|
|
1063
|
-
.forEach((terminal: Element) => {
|
|
1064
|
-
if (terminal.querySelector('lia-abcjs')) {
|
|
1065
|
-
terminal.setAttribute('data-abc-index', abcIndex.toString())
|
|
1066
|
-
abcIndex++
|
|
1067
|
-
}
|
|
1068
|
-
})
|
|
1069
|
-
return abcIndex
|
|
1070
|
-
})
|
|
1071
|
-
|
|
1072
|
-
// Find each <lia-abcjs> element and capture a screenshot of its rendered SVG content
|
|
1073
|
-
for (let i = 0; i < abcCount; i++) {
|
|
1074
|
-
try {
|
|
1075
|
-
const elements = await page.$$('lia-abcjs')
|
|
1076
|
-
const el = elements[i]
|
|
1077
|
-
if (!el) continue
|
|
1078
|
-
|
|
1079
|
-
const svgDataUri = await page.evaluate((host: Element) => {
|
|
1080
|
-
const svg = host.shadowRoot
|
|
1081
|
-
?.getElementById('paper')
|
|
1082
|
-
?.querySelector('svg')
|
|
1083
|
-
if (!svg) return null
|
|
1084
|
-
const svgString = new XMLSerializer().serializeToString(svg)
|
|
1085
|
-
return 'data:image/svg+xml;base64,' + btoa(svgString)
|
|
1086
|
-
}, el)
|
|
1087
|
-
|
|
1088
|
-
if (svgDataUri) {
|
|
1089
|
-
abcImages.set(i, svgDataUri)
|
|
1090
|
-
}
|
|
1091
|
-
} catch (e) {
|
|
1092
|
-
console.error(`Failed to convert ABC block ${i}:`, e)
|
|
1093
|
-
}
|
|
1094
|
-
}
|
|
1095
|
-
|
|
1096
|
-
return abcImages
|
|
1097
|
-
}
|
|
1098
|
-
|
|
1099
|
-
/**
|
|
1100
|
-
* Extracts SVG from standalone <lia-abcjs> elements (NOT inside .lia-code-terminal).
|
|
1101
|
-
* These come from imported template macros like @ABCJS.render.
|
|
1102
|
-
*
|
|
1103
|
-
* @param page - Puppeteer page instance
|
|
1104
|
-
* @returns Map of standalone ABC indexes to base64 SVG data URIs
|
|
1105
|
-
*/
|
|
1106
|
-
async function screenshotStandaloneAbcBlocks(
|
|
1107
|
-
page: Page,
|
|
1108
|
-
): Promise<Map<number, string>> {
|
|
1109
|
-
const standaloneAbcImages = new Map<number, string>()
|
|
1110
|
-
|
|
1111
|
-
// Tag each <lia-abcjs> that is NOT inside a .lia-code-terminal
|
|
1112
|
-
const count = await page.evaluate(() => {
|
|
1113
|
-
let idx = 0
|
|
1114
|
-
document.querySelectorAll('lia-abcjs').forEach((el: Element) => {
|
|
1115
|
-
if (!el.closest('.lia-code-terminal')) {
|
|
1116
|
-
el.setAttribute('data-standalone-abc-index', idx.toString())
|
|
1117
|
-
idx++
|
|
1118
|
-
}
|
|
1119
|
-
})
|
|
1120
|
-
return idx
|
|
1121
|
-
})
|
|
1122
|
-
|
|
1123
|
-
// Extract SVG from each standalone <lia-abcjs> element's Shadow DOM
|
|
1124
|
-
for (let i = 0; i < count; i++) {
|
|
1125
|
-
try {
|
|
1126
|
-
const el = await page.$(`lia-abcjs[data-standalone-abc-index="${i}"]`)
|
|
1127
|
-
if (!el) continue
|
|
1128
|
-
|
|
1129
|
-
const svgDataUri = await page.evaluate((host: Element) => {
|
|
1130
|
-
const svg = host.shadowRoot
|
|
1131
|
-
?.getElementById('paper')
|
|
1132
|
-
?.querySelector('svg')
|
|
1133
|
-
if (!svg) return null
|
|
1134
|
-
const svgString = new XMLSerializer().serializeToString(svg)
|
|
1135
|
-
return 'data:image/svg+xml;base64,' + btoa(svgString)
|
|
1136
|
-
}, el)
|
|
1137
|
-
|
|
1138
|
-
if (svgDataUri) {
|
|
1139
|
-
standaloneAbcImages.set(i, svgDataUri)
|
|
1140
|
-
}
|
|
1141
|
-
} catch (e) {
|
|
1142
|
-
console.error(`Failed to convert standalone ABC block ${i}:`, e)
|
|
1143
|
-
}
|
|
1144
|
-
}
|
|
1145
|
-
|
|
1146
|
-
return standaloneAbcImages
|
|
1147
|
-
}
|
|
1148
|
-
|
|
1149
|
-
/**
|
|
1150
|
-
* Extracts SVG from every <lia-chart> element (ECharts, open Shadow DOM)
|
|
1151
|
-
*
|
|
1152
|
-
* @param page - Puppeteer page instance
|
|
1153
|
-
* @returns Map of chart indexes to base64 SVG data URIs
|
|
1154
|
-
*/
|
|
1155
|
-
async function extractChartSvgs(page: Page): Promise<Map<number, string>> {
|
|
1156
|
-
const count = await page.evaluate(() => {
|
|
1157
|
-
let idx = 0
|
|
1158
|
-
document.querySelectorAll('lia-chart').forEach((el: Element) => {
|
|
1159
|
-
el.setAttribute('data-chart-index', idx.toString())
|
|
1160
|
-
idx++
|
|
1161
|
-
})
|
|
1162
|
-
return idx
|
|
1163
|
-
})
|
|
1164
|
-
|
|
1165
|
-
const images = new Map<number, string>()
|
|
1166
|
-
const elements = await page.$$('lia-chart')
|
|
1167
|
-
|
|
1168
|
-
for (let i = 0; i < count; i++) {
|
|
1169
|
-
try {
|
|
1170
|
-
const el = elements[i]
|
|
1171
|
-
if (!el) continue
|
|
1172
|
-
|
|
1173
|
-
const svgDataUri = await page.evaluate((host: Element) => {
|
|
1174
|
-
const svg = host.shadowRoot?.querySelector('svg')
|
|
1175
|
-
if (!svg) return null
|
|
1176
|
-
const svgString = new XMLSerializer().serializeToString(svg)
|
|
1177
|
-
return 'data:image/svg+xml;base64,' + btoa(svgString)
|
|
1178
|
-
}, el)
|
|
1179
|
-
|
|
1180
|
-
if (svgDataUri) {
|
|
1181
|
-
images.set(i, svgDataUri)
|
|
1182
|
-
}
|
|
1183
|
-
} catch (e) {
|
|
1184
|
-
console.error(`Failed to extract chart ${i}:`, e)
|
|
1185
|
-
}
|
|
1186
|
-
}
|
|
1187
|
-
|
|
1188
|
-
return images
|
|
1189
|
-
}
|
|
1190
|
-
|
|
1191
|
-
/**
|
|
1192
|
-
* Extracts rendered KaTeX HTML from every <lia-formula> element (open Shadow DOM)
|
|
1193
|
-
*
|
|
1194
|
-
* @param page - Puppeteer page instance
|
|
1195
|
-
* @returns Map of formula indexes to rendered KaTeX HTML strings
|
|
1196
|
-
*/
|
|
1197
|
-
async function extractFormulaHtml(page: Page): Promise<Map<number, string>> {
|
|
1198
|
-
const result = await page.evaluate(() => {
|
|
1199
|
-
const formulas: [number, string][] = []
|
|
1200
|
-
let idx = 0
|
|
1201
|
-
|
|
1202
|
-
document.querySelectorAll('lia-formula').forEach((el: Element) => {
|
|
1203
|
-
el.setAttribute('data-formula-index', idx.toString())
|
|
1204
|
-
|
|
1205
|
-
try {
|
|
1206
|
-
const shadow = el.shadowRoot
|
|
1207
|
-
if (!shadow) {
|
|
1208
|
-
formulas.push([idx, ''])
|
|
1209
|
-
idx++
|
|
1210
|
-
return
|
|
1211
|
-
}
|
|
1212
|
-
|
|
1213
|
-
// Collect all <style> content from the Shadow DOM
|
|
1214
|
-
let styleHTML = ''
|
|
1215
|
-
shadow.querySelectorAll('style').forEach((s: Element) => {
|
|
1216
|
-
styleHTML += `<style>${s.textContent || ''}</style>`
|
|
1217
|
-
})
|
|
1218
|
-
|
|
1219
|
-
// Get the rendered KaTeX span
|
|
1220
|
-
const span = shadow.querySelector('span')
|
|
1221
|
-
if (!span) {
|
|
1222
|
-
formulas.push([idx, ''])
|
|
1223
|
-
idx++
|
|
1224
|
-
return
|
|
1225
|
-
}
|
|
1226
|
-
|
|
1227
|
-
const spanClone = span.cloneNode(true) as HTMLElement
|
|
1228
|
-
const mathml = spanClone.querySelector('.katex-mathml')
|
|
1229
|
-
|
|
1230
|
-
if (mathml) {
|
|
1231
|
-
// Extract the <math> element from inside .katex-mathml
|
|
1232
|
-
const mathEl = mathml.querySelector('math')
|
|
1233
|
-
formulas.push([idx, mathEl ? mathEl.outerHTML : spanClone.outerHTML])
|
|
1234
|
-
} else {
|
|
1235
|
-
// Fallback: strip MathML and use visual HTML if no MathML found
|
|
1236
|
-
spanClone.querySelectorAll('.katex-mathml').forEach((m) => m.remove())
|
|
1237
|
-
formulas.push([idx, styleHTML + spanClone.outerHTML])
|
|
1238
|
-
}
|
|
1239
|
-
} catch (e) {
|
|
1240
|
-
formulas.push([idx, ''])
|
|
1241
|
-
}
|
|
1242
|
-
|
|
1243
|
-
idx++
|
|
1244
|
-
})
|
|
1245
|
-
|
|
1246
|
-
return formulas
|
|
1247
|
-
})
|
|
1248
|
-
|
|
1249
|
-
return new Map(result)
|
|
1250
|
-
}
|
|
1251
|
-
|
|
1252
|
-
/**
|
|
1253
|
-
* Tags matching elements with a data attribute index, then screenshots each one
|
|
1254
|
-
*
|
|
1255
|
-
* @param page - Puppeteer page instance
|
|
1256
|
-
* @param selector - CSS selector to find elements
|
|
1257
|
-
* @param dataAttr - data-attribute name used for indexing (e.g. 'data-svg-index')
|
|
1258
|
-
* @param label - human-readable label for error messages
|
|
1259
|
-
* @param tagFilter - optional filter run inside page.evaluate(); receives each
|
|
1260
|
-
* matched element and returns true to tag it
|
|
1261
|
-
* @returns Map of element indexes to base64 PNG data URIs
|
|
1262
|
-
*/
|
|
1263
|
-
async function screenshotTaggedElements(
|
|
1264
|
-
page: Page,
|
|
1265
|
-
selector: string,
|
|
1266
|
-
dataAttr: string,
|
|
1267
|
-
label: string,
|
|
1268
|
-
tagFilter?: string, // JS function body: (el) => boolean, serialised as string
|
|
1269
|
-
): Promise<Map<number, string>> {
|
|
1270
|
-
const count = await page.evaluate(
|
|
1271
|
-
(sel: string, attr: string, filterBody: string | null) => {
|
|
1272
|
-
let idx = 0
|
|
1273
|
-
const filterFn = filterBody
|
|
1274
|
-
? (new Function('el', filterBody) as (el: Element) => boolean)
|
|
1275
|
-
: null
|
|
1276
|
-
document.querySelectorAll(sel).forEach((el: Element) => {
|
|
1277
|
-
if (!filterFn || filterFn(el)) {
|
|
1278
|
-
el.setAttribute(attr, idx.toString())
|
|
1279
|
-
idx++
|
|
1280
|
-
}
|
|
1281
|
-
})
|
|
1282
|
-
return idx
|
|
1283
|
-
},
|
|
1284
|
-
selector,
|
|
1285
|
-
dataAttr,
|
|
1286
|
-
tagFilter ?? null,
|
|
1287
|
-
)
|
|
1288
|
-
|
|
1289
|
-
const images = new Map<number, string>()
|
|
1290
|
-
for (let i = 0; i < count; i++) {
|
|
1291
|
-
try {
|
|
1292
|
-
const el = await page.$(`${selector}[${dataAttr}="${i}"]`)
|
|
1293
|
-
if (el) {
|
|
1294
|
-
const screenshot = await el.screenshot({ type: 'png' })
|
|
1295
|
-
images.set(
|
|
1296
|
-
i,
|
|
1297
|
-
`data:image/png;base64,${Buffer.from(screenshot).toString('base64')}`,
|
|
1298
|
-
)
|
|
1299
|
-
}
|
|
1300
|
-
} catch (e) {
|
|
1301
|
-
console.error(`Failed to screenshot ${label} ${i}:`, e)
|
|
1302
|
-
}
|
|
1303
|
-
}
|
|
1304
|
-
|
|
1305
|
-
return images
|
|
1306
|
-
}
|