@liascript/exporter 3.0.0--1.0.3 → 3.0.1--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.a7f021f7.js} +1 -1
- package/dist/assets/capacitor/index.html +1 -1
- package/dist/assets/capacitor/{jszip.min.f6eda75b.js → jszip.min.43389eb1.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/indexeddb/{jszip.min.63142cc8.js → jszip.min.4fbcc13f.js} +1 -1
- package/dist/assets/scorm2004/{index.7a5820ab.js → index.33bec53a.js} +1 -1
- package/dist/assets/scorm2004/index.html +1 -1
- package/dist/assets/scorm2004/{jszip.min.63142cc8.js → jszip.min.4fbcc13f.js} +1 -1
- package/dist/assets/xapi/{index.018a032a.js → index.f2e89e49.js} +1 -1
- package/dist/assets/xapi/index.html +1 -1
- package/dist/assets/xapi/{jszip.min.eaecf580.js → jszip.min.19c66d77.js} +1 -1
- package/dist/index.js +47 -47
- 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/src/export/docx.ts
DELETED
|
@@ -1,1025 +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 HTMLtoDOCX from '@turbodocx/html-to-docx'
|
|
7
|
-
|
|
8
|
-
// Default DOCX generation settings
|
|
9
|
-
const DEFAULT_TIMEOUT_MS = 15000
|
|
10
|
-
const DEFAULT_ORIENTATION = 'portrait'
|
|
11
|
-
const DEFAULT_FONT = 'Arial'
|
|
12
|
-
const DEFAULT_FONT_SIZE = 22 // half-points, equals 11pt
|
|
13
|
-
const DEFAULT_LANG = 'en-US'
|
|
14
|
-
|
|
15
|
-
/** Displays help information about DOCX export options. */
|
|
16
|
-
export function help() {
|
|
17
|
-
console.log('')
|
|
18
|
-
console.log(COLOR.heading('DOCX settings:'), '\n')
|
|
19
|
-
|
|
20
|
-
COLOR.info(
|
|
21
|
-
'DOCX export generates Microsoft Word documents from your LiaScript course using Puppeteer to render the content and the @turbodocx/html-to-docx library to create the DOCX file. The output is compatible with Microsoft Word 2007+, LibreOffice Writer, and Google Docs.',
|
|
22
|
-
)
|
|
23
|
-
|
|
24
|
-
console.log('\nLearn more: https://github.com/TurboDocx/html-to-docx \n')
|
|
25
|
-
|
|
26
|
-
console.log(COLOR.heading('Optional settings:'), '\n')
|
|
27
|
-
|
|
28
|
-
COLOR.command(null, '--docx-title', ' Title of the document')
|
|
29
|
-
COLOR.command(null, '--docx-author', ' Author / creator of the document')
|
|
30
|
-
COLOR.command(null, '--docx-subject', ' Subject of the document')
|
|
31
|
-
COLOR.command(null, '--docx-description', ' Description of the document')
|
|
32
|
-
COLOR.command(null, '--docx-language', ` Language code for spell checker (default: ${DEFAULT_LANG})`)
|
|
33
|
-
COLOR.command(null, '--docx-orientation', ` Page orientation: portrait or landscape (default: ${DEFAULT_ORIENTATION})`)
|
|
34
|
-
COLOR.command(null, '--docx-font', ` Font name (default: ${DEFAULT_FONT})`)
|
|
35
|
-
COLOR.command(null, '--docx-font-size', ` Font size in half-points/HIP (default: ${DEFAULT_FONT_SIZE}, equals 11pt)`)
|
|
36
|
-
COLOR.command(null, '--docx-header', ' Enable header in the document (default: false)')
|
|
37
|
-
COLOR.command(null, '--docx-header-html', ' Custom HTML string for the header')
|
|
38
|
-
COLOR.command(null, '--docx-footer', ' Enable footer in the document (default: false)')
|
|
39
|
-
COLOR.command(null, '--docx-footer-html', ' Custom HTML string for the footer')
|
|
40
|
-
COLOR.command(null, '--docx-page-number', ' Add page numbers to the footer (default: false)')
|
|
41
|
-
COLOR.command(null, '--docx-stylesheet', ' Path to a local CSS file to inject before export')
|
|
42
|
-
COLOR.command(null, '--docx-theme', ' LiaScript theme: default, turquoise, blue, red, yellow')
|
|
43
|
-
COLOR.command(null, '--docx-timeout', ` Additional wait time after rendering in ms (default: ${DEFAULT_TIMEOUT_MS})`)
|
|
44
|
-
COLOR.command(null, '--docx-preview', ' Open preview browser for debugging (default: false)')
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/** Configuration options for DOCX export. */
|
|
48
|
-
export interface DocxExportArguments {
|
|
49
|
-
input: string
|
|
50
|
-
output: string
|
|
51
|
-
'docx-title'?: string
|
|
52
|
-
'docx-author'?: string
|
|
53
|
-
'docx-subject'?: string
|
|
54
|
-
'docx-description'?: string
|
|
55
|
-
'docx-language'?: string
|
|
56
|
-
'docx-orientation'?: 'portrait' | 'landscape'
|
|
57
|
-
'docx-font'?: string
|
|
58
|
-
'docx-font-size'?: number
|
|
59
|
-
'docx-header'?: boolean
|
|
60
|
-
'docx-header-html'?: string
|
|
61
|
-
'docx-footer'?: boolean
|
|
62
|
-
'docx-footer-html'?: string
|
|
63
|
-
'docx-page-number'?: boolean
|
|
64
|
-
'docx-stylesheet'?: string
|
|
65
|
-
'docx-theme'?: string
|
|
66
|
-
'docx-timeout'?: number
|
|
67
|
-
'docx-preview'?: boolean
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
export const format = 'docx'
|
|
71
|
-
|
|
72
|
-
/** Exports a LiaScript course to DOCX format. */
|
|
73
|
-
export async function exporter(argument: DocxExportArguments) {
|
|
74
|
-
const dirname = helper.dirname()
|
|
75
|
-
|
|
76
|
-
let url = `file://${dirname}/assets/pdf/index.html?`
|
|
77
|
-
if (helper.isURL(argument.input)) {
|
|
78
|
-
url += argument.input
|
|
79
|
-
} else {
|
|
80
|
-
url += 'file://' + path.resolve(argument.input)
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
let browser: Browser | null = null
|
|
84
|
-
let page: Page | null = null
|
|
85
|
-
|
|
86
|
-
try {
|
|
87
|
-
const launchOptions: any = {
|
|
88
|
-
pipe: true,
|
|
89
|
-
args: [
|
|
90
|
-
'--no-sandbox',
|
|
91
|
-
'--disable-web-security',
|
|
92
|
-
'--disable-features=IsolateOrigins',
|
|
93
|
-
'--disable-site-isolation-trials',
|
|
94
|
-
'--unhandled-rejections=strict',
|
|
95
|
-
'--disable-features=BlockInsecurePrivateNetworkRequests',
|
|
96
|
-
'--allow-file-access-from-files',
|
|
97
|
-
'--enable-local-file-accesses',
|
|
98
|
-
],
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
if (argument['docx-preview']) {
|
|
102
|
-
launchOptions.headless = false
|
|
103
|
-
launchOptions.devtools = true
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
browser = await puppeteer.launch(launchOptions)
|
|
107
|
-
page = await browser.newPage()
|
|
108
|
-
await page.setViewport({ width: 1200, height: 800 })
|
|
109
|
-
|
|
110
|
-
console.log('Loading course content... This may take a while for large courses.')
|
|
111
|
-
|
|
112
|
-
// Set up render done listener BEFORE navigating
|
|
113
|
-
let renderDoneResolve: () => void
|
|
114
|
-
const renderDonePromise = new Promise<void>((resolve) => {
|
|
115
|
-
renderDoneResolve = resolve
|
|
116
|
-
})
|
|
117
|
-
|
|
118
|
-
page.on('console', (msg) => {
|
|
119
|
-
if (msg.text().startsWith('__RENDER_DONE__')) {
|
|
120
|
-
renderDoneResolve()
|
|
121
|
-
}
|
|
122
|
-
})
|
|
123
|
-
|
|
124
|
-
await page.setExtraHTTPHeaders({
|
|
125
|
-
referer: 'https://liascript.github.io/',
|
|
126
|
-
})
|
|
127
|
-
|
|
128
|
-
await page.goto(url, {
|
|
129
|
-
waitUntil: 'networkidle2',
|
|
130
|
-
timeout: DEFAULT_TIMEOUT_MS,
|
|
131
|
-
})
|
|
132
|
-
|
|
133
|
-
if (argument['docx-stylesheet']) {
|
|
134
|
-
const href = path.resolve(dirname + '/../', argument['docx-stylesheet'])
|
|
135
|
-
try {
|
|
136
|
-
await page.evaluate(async (href) => {
|
|
137
|
-
const link = document.createElement('link')
|
|
138
|
-
link.rel = 'stylesheet'
|
|
139
|
-
link.href = href
|
|
140
|
-
const promise = new Promise((resolve, reject) => {
|
|
141
|
-
link.onload = resolve
|
|
142
|
-
link.onerror = reject
|
|
143
|
-
})
|
|
144
|
-
document.head.appendChild(link)
|
|
145
|
-
await promise
|
|
146
|
-
}, href)
|
|
147
|
-
} catch (e) {
|
|
148
|
-
throw new Error(
|
|
149
|
-
`Failed to load custom stylesheet from '${argument['docx-stylesheet']}': ${e}`,
|
|
150
|
-
)
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
if (argument['docx-theme']) {
|
|
155
|
-
try {
|
|
156
|
-
await page.evaluate(async (theme) => {
|
|
157
|
-
document.documentElement.classList.remove('lia-theme-default')
|
|
158
|
-
document.documentElement.classList.add('lia-theme-' + theme)
|
|
159
|
-
}, argument['docx-theme'])
|
|
160
|
-
} catch (e) {
|
|
161
|
-
throw new Error(
|
|
162
|
-
`Failed to apply theme '${argument['docx-theme']}': ${e}`,
|
|
163
|
-
)
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
if (!argument['docx-preview']) {
|
|
168
|
-
await renderDonePromise
|
|
169
|
-
|
|
170
|
-
if (argument['docx-timeout']) {
|
|
171
|
-
await helper.sleep(argument['docx-timeout'])
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
await toDOCX(argument, page, dirname)
|
|
175
|
-
} else {
|
|
176
|
-
console.log('Preview mode enabled - browser will remain open')
|
|
177
|
-
}
|
|
178
|
-
} catch (e) {
|
|
179
|
-
const error = e as Error
|
|
180
|
-
console.error('DOCX export failed:', error.message)
|
|
181
|
-
throw new Error(`Failed to export DOCX: ${error.message}`)
|
|
182
|
-
} finally {
|
|
183
|
-
if (argument['docx-preview']) {
|
|
184
|
-
console.log('Browser kept open for preview. Close manually when done.')
|
|
185
|
-
} else {
|
|
186
|
-
if (page) {
|
|
187
|
-
try { await page.close() } catch (e) { console.error('Failed to close page:', e) }
|
|
188
|
-
}
|
|
189
|
-
if (browser) {
|
|
190
|
-
try { await browser.close() } catch (e) { console.error('Failed to close browser:', e) }
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
/** Generates a DOCX file from a Puppeteer page. */
|
|
197
|
-
async function toDOCX(
|
|
198
|
-
argument: DocxExportArguments,
|
|
199
|
-
page: Page,
|
|
200
|
-
dirname: string,
|
|
201
|
-
) {
|
|
202
|
-
try {
|
|
203
|
-
console.log('Converting SVG diagrams to PNG images...')
|
|
204
|
-
const svgImages = await screenshotTaggedElements(
|
|
205
|
-
page,
|
|
206
|
-
'figure.lia-figure',
|
|
207
|
-
'data-svg-index',
|
|
208
|
-
'SVG diagram',
|
|
209
|
-
`return !!el.querySelector('.lia-figure__media svg')`,
|
|
210
|
-
)
|
|
211
|
-
console.log(`Converted ${svgImages.size} SVG diagrams to PNG`)
|
|
212
|
-
|
|
213
|
-
console.log('Extracting syntax-highlighted code blocks...')
|
|
214
|
-
const highlightedBlocks = await extractHighlightedCode(page)
|
|
215
|
-
console.log(`Extracted ${highlightedBlocks.size} highlighted code block(s)`)
|
|
216
|
-
|
|
217
|
-
console.log('Extracting terminal output blocks...')
|
|
218
|
-
const terminalOutputs = await extractTerminalOutputs(page)
|
|
219
|
-
console.log(`Extracted ${terminalOutputs.size} terminal output block(s)`)
|
|
220
|
-
|
|
221
|
-
console.log('Screenshotting ABC music notation blocks...')
|
|
222
|
-
const abcImages = await extractAbcSvgs(page, true)
|
|
223
|
-
console.log(`Captured ${abcImages.size} ABC notation block(s)`)
|
|
224
|
-
|
|
225
|
-
console.log('Screenshotting standalone ABC music notation blocks...')
|
|
226
|
-
const standaloneAbcImages = await extractAbcSvgs(page, false)
|
|
227
|
-
console.log(`Captured ${standaloneAbcImages.size} standalone ABC notation block(s)`)
|
|
228
|
-
|
|
229
|
-
console.log('Screenshotting embedded media (Spotify, SoundCloud, etc.)...')
|
|
230
|
-
const embedImages = await screenshotTaggedElements(
|
|
231
|
-
page, 'lia-embed', 'data-embed-index', 'embed',
|
|
232
|
-
)
|
|
233
|
-
console.log(`Captured ${embedImages.size} embed cover(s)`)
|
|
234
|
-
|
|
235
|
-
console.log('Extracting ECharts diagrams...')
|
|
236
|
-
const chartImages = await extractChartSvgs(page)
|
|
237
|
-
console.log(`Extracted ${chartImages.size} chart(s)`)
|
|
238
|
-
|
|
239
|
-
// Inject KaTeX CSS so formula clones render correctly outside shadow DOM
|
|
240
|
-
await page.evaluate(() => {
|
|
241
|
-
const link = document.createElement('link')
|
|
242
|
-
link.rel = 'stylesheet'
|
|
243
|
-
link.href = 'https://cdn.jsdelivr.net/npm/katex@0.16.27/dist/katex.min.css'
|
|
244
|
-
document.head.appendChild(link)
|
|
245
|
-
})
|
|
246
|
-
await page.waitForFunction(() => {
|
|
247
|
-
const sheets = document.styleSheets
|
|
248
|
-
for (let i = 0; i < sheets.length; i++) {
|
|
249
|
-
try {
|
|
250
|
-
if (sheets[i].href?.includes('katex') && sheets[i].cssRules.length > 0) return true
|
|
251
|
-
} catch (e) { /* CORS */ }
|
|
252
|
-
}
|
|
253
|
-
return false
|
|
254
|
-
}, { timeout: 5000 }).catch(() => console.warn('KaTeX CSS load timeout — formulas may not render correctly'))
|
|
255
|
-
|
|
256
|
-
console.log('Screenshotting math formulas...')
|
|
257
|
-
const formulaImages = await screenshotTaggedElements(
|
|
258
|
-
page, 'lia-formula', 'data-formula-index', 'formula',
|
|
259
|
-
)
|
|
260
|
-
console.log(`Captured ${formulaImages.size} formula(s)`)
|
|
261
|
-
|
|
262
|
-
// Hide formula MathML inside SVG foreignObjects before screenshotting
|
|
263
|
-
await page.evaluate(() => {
|
|
264
|
-
document
|
|
265
|
-
.querySelectorAll('svg foreignObject lia-formula')
|
|
266
|
-
.forEach((formula) => {
|
|
267
|
-
formula.childNodes.forEach((child) => {
|
|
268
|
-
if (child.nodeType === Node.ELEMENT_NODE) {
|
|
269
|
-
;(child as HTMLElement).style.display = 'none'
|
|
270
|
-
}
|
|
271
|
-
})
|
|
272
|
-
const shadow = formula.shadowRoot
|
|
273
|
-
if (shadow) {
|
|
274
|
-
shadow.querySelectorAll('.katex-mathml').forEach((mathml) => {
|
|
275
|
-
;(mathml as HTMLElement).style.display = 'none'
|
|
276
|
-
})
|
|
277
|
-
}
|
|
278
|
-
})
|
|
279
|
-
})
|
|
280
|
-
|
|
281
|
-
console.log('Screenshotting inline SVGs (foreignObject, interactive)...')
|
|
282
|
-
const inlineSvgImages = await screenshotTaggedElements(
|
|
283
|
-
page, 'svg', 'data-inline-svg-index', 'inline SVG',
|
|
284
|
-
`if (!el.hasAttribute('viewBox')) return false;
|
|
285
|
-
const skip = el.closest('.lia-figure, .lia-code, lia-chart, lia-abcjs, lia-embed');
|
|
286
|
-
if (skip) return false;
|
|
287
|
-
const rect = el.getBoundingClientRect();
|
|
288
|
-
if (rect.width < 50 || rect.height < 50) return false;
|
|
289
|
-
return true;`,
|
|
290
|
-
)
|
|
291
|
-
console.log(`Captured ${inlineSvgImages.size} inline SVG(s)`)
|
|
292
|
-
|
|
293
|
-
console.log('Screenshotting video/iframe figures...')
|
|
294
|
-
const videoIframeImages = await screenshotTaggedElements(
|
|
295
|
-
page, 'figure.lia-figure', 'data-video-index', 'video/iframe figure',
|
|
296
|
-
`const media = el.querySelector('.lia-figure__media');
|
|
297
|
-
if (!media) return false;
|
|
298
|
-
const type = media.getAttribute('data-media-type');
|
|
299
|
-
return type === 'iframe' || type === 'movie';`,
|
|
300
|
-
)
|
|
301
|
-
console.log(`Captured ${videoIframeImages.size} video/iframe thumbnail(s)`)
|
|
302
|
-
|
|
303
|
-
const toEntries = (map: Map<number, string>) =>
|
|
304
|
-
Array.from(map.entries()) as [number, string][]
|
|
305
|
-
|
|
306
|
-
const payload = {
|
|
307
|
-
svgImages: toEntries(svgImages),
|
|
308
|
-
codeBlocks: toEntries(highlightedBlocks),
|
|
309
|
-
terminalOutputs: toEntries(terminalOutputs),
|
|
310
|
-
abcImages: toEntries(abcImages),
|
|
311
|
-
standaloneAbcImages: toEntries(standaloneAbcImages),
|
|
312
|
-
embedImages: toEntries(embedImages),
|
|
313
|
-
chartImages: toEntries(chartImages),
|
|
314
|
-
formulaImages: toEntries(formulaImages),
|
|
315
|
-
inlineSvgImages: toEntries(inlineSvgImages),
|
|
316
|
-
videoIframeImages: toEntries(videoIframeImages),
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
// Extract and process the full document HTML
|
|
320
|
-
const htmlContent = await page.evaluate((data) => {
|
|
321
|
-
const {
|
|
322
|
-
svgImages: svgImagesData,
|
|
323
|
-
codeBlocks,
|
|
324
|
-
terminalOutputs: terminalOutputsData,
|
|
325
|
-
abcImages: abcImagesData,
|
|
326
|
-
standaloneAbcImages: standaloneAbcImagesData,
|
|
327
|
-
embedImages: embedImagesData,
|
|
328
|
-
chartImages: chartImagesData,
|
|
329
|
-
formulaImages: formulaImagesData,
|
|
330
|
-
inlineSvgImages: inlineSvgImagesData,
|
|
331
|
-
videoIframeImages: videoIframeImagesData,
|
|
332
|
-
} = data
|
|
333
|
-
|
|
334
|
-
const bodyClone = document.body.cloneNode(true) as HTMLElement
|
|
335
|
-
|
|
336
|
-
// Remove UI elements not relevant in a document
|
|
337
|
-
bodyClone
|
|
338
|
-
.querySelectorAll(
|
|
339
|
-
'link[rel="icon"], link[rel="shortcut icon"], link[rel="apple-touch-icon"], ' +
|
|
340
|
-
'link[rel="manifest"], link[rel="preconnect"], link[rel="dns-prefetch"], ' +
|
|
341
|
-
'link[rel="preload"], link[rel="stylesheet"]',
|
|
342
|
-
)
|
|
343
|
-
.forEach((el: any) => el.remove())
|
|
344
|
-
|
|
345
|
-
bodyClone.querySelectorAll('.lia-code__copy, .lia-code__copy--inverted').forEach((el: Element) => el.remove())
|
|
346
|
-
bodyClone.querySelectorAll('.lia-code-control').forEach((el: Element) => el.remove())
|
|
347
|
-
bodyClone.querySelectorAll('.lia-lightbox__clickarea').forEach((el: Element) => el.remove())
|
|
348
|
-
|
|
349
|
-
// Convert gallery flex containers to vertical layout
|
|
350
|
-
bodyClone.querySelectorAll('.lia-gallery').forEach((gallery: Element) => {
|
|
351
|
-
(gallery as HTMLElement).setAttribute('style', 'display: block; margin-bottom: 1em;')
|
|
352
|
-
})
|
|
353
|
-
|
|
354
|
-
// Strip JS attributes from images
|
|
355
|
-
bodyClone.querySelectorAll('img').forEach((img: Element) => {
|
|
356
|
-
img.removeAttribute('onerror')
|
|
357
|
-
img.removeAttribute('onclick')
|
|
358
|
-
img.removeAttribute('loading')
|
|
359
|
-
})
|
|
360
|
-
|
|
361
|
-
// Unwrap images from lia-figure__media divs (html-to-docx needs <img> as direct child of <figure>)
|
|
362
|
-
bodyClone
|
|
363
|
-
.querySelectorAll('figure.lia-figure > .lia-figure__media')
|
|
364
|
-
.forEach((mediaDiv: Element) => {
|
|
365
|
-
const figure = mediaDiv.parentElement!
|
|
366
|
-
if (figure.hasAttribute('data-video-index')) return
|
|
367
|
-
while (mediaDiv.firstChild) {
|
|
368
|
-
figure.insertBefore(mediaDiv.firstChild, mediaDiv)
|
|
369
|
-
}
|
|
370
|
-
mediaDiv.remove()
|
|
371
|
-
})
|
|
372
|
-
|
|
373
|
-
// Convert <figcaption> to styled paragraph after figure
|
|
374
|
-
bodyClone.querySelectorAll('figure > figcaption').forEach((caption: Element) => {
|
|
375
|
-
const figure = caption.parentElement!
|
|
376
|
-
const p = document.createElement('p')
|
|
377
|
-
p.setAttribute('style',
|
|
378
|
-
'text-align: center; font-style: italic; color: #555; font-size: 0.9em; margin-top: 0.3em; margin-bottom: 1em;')
|
|
379
|
-
p.innerHTML = caption.innerHTML
|
|
380
|
-
figure.parentNode?.insertBefore(p, figure.nextSibling)
|
|
381
|
-
caption.remove()
|
|
382
|
-
})
|
|
383
|
-
|
|
384
|
-
// Replace video/iframe figures with screenshot thumbnail + clickable link
|
|
385
|
-
bodyClone
|
|
386
|
-
.querySelectorAll('figure.lia-figure[data-video-index]')
|
|
387
|
-
.forEach((figure: Element) => {
|
|
388
|
-
const idx = parseInt(figure.getAttribute('data-video-index') || '-1')
|
|
389
|
-
const entry = videoIframeImagesData.find((item: [number, string]) => item[0] === idx)
|
|
390
|
-
|
|
391
|
-
const printLink = figure.querySelector('a.lia-print-only')
|
|
392
|
-
const linkUrl = printLink?.getAttribute('href') || ''
|
|
393
|
-
const iframeSrc = figure.querySelector('iframe')?.getAttribute('src') || ''
|
|
394
|
-
const videoSrc =
|
|
395
|
-
figure.querySelector('video')?.getAttribute('src') ||
|
|
396
|
-
figure.querySelector('video source')?.getAttribute('src') || ''
|
|
397
|
-
const mediaUrl = linkUrl || iframeSrc || videoSrc
|
|
398
|
-
|
|
399
|
-
const wrapper = document.createElement('div')
|
|
400
|
-
wrapper.setAttribute('style', 'margin: 1em 0; text-align: center; page-break-inside: avoid;')
|
|
401
|
-
|
|
402
|
-
if (entry && entry[1]) {
|
|
403
|
-
const img = document.createElement('img')
|
|
404
|
-
img.src = entry[1]
|
|
405
|
-
img.alt = printLink?.textContent?.trim() || 'Video thumbnail'
|
|
406
|
-
img.setAttribute('style', 'max-width: 100%; height: auto; display: block; margin: 0 auto;')
|
|
407
|
-
wrapper.appendChild(img)
|
|
408
|
-
} else if (mediaUrl) {
|
|
409
|
-
const ytMatch = mediaUrl.match(/(?:youtube\.com\/(?:watch\?v=|embed\/)|youtu\.be\/)([\w-]{11})/)
|
|
410
|
-
if (ytMatch) {
|
|
411
|
-
const img = document.createElement('img')
|
|
412
|
-
img.src = `https://img.youtube.com/vi/${ytMatch[1]}/hqdefault.jpg`
|
|
413
|
-
img.alt = printLink?.textContent?.trim() || 'YouTube video'
|
|
414
|
-
img.setAttribute('style', 'max-width: 100%; height: auto; display: block; margin: 0 auto;')
|
|
415
|
-
wrapper.appendChild(img)
|
|
416
|
-
}
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
if (mediaUrl) {
|
|
420
|
-
const p = document.createElement('p')
|
|
421
|
-
p.setAttribute('style', 'text-align: center; font-size: 0.9em; margin-top: 0.5em;')
|
|
422
|
-
const a = document.createElement('a')
|
|
423
|
-
a.href = mediaUrl
|
|
424
|
-
a.textContent = '▶ Watch video: ' + mediaUrl
|
|
425
|
-
a.setAttribute('style', 'color: #1a73e8; text-decoration: underline;')
|
|
426
|
-
p.appendChild(a)
|
|
427
|
-
wrapper.appendChild(p)
|
|
428
|
-
} else if (!entry || !entry[1]) {
|
|
429
|
-
const p = document.createElement('p')
|
|
430
|
-
p.setAttribute('style', 'text-align: center; color: #888;')
|
|
431
|
-
p.textContent = '[Video/iframe content — no source available]'
|
|
432
|
-
wrapper.appendChild(p)
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
figure.replaceWith(wrapper)
|
|
436
|
-
})
|
|
437
|
-
|
|
438
|
-
// Replace stray <iframe> and <video> elements with links
|
|
439
|
-
const replaceMediaWithLink = (selector: string, prefix: string) => {
|
|
440
|
-
bodyClone.querySelectorAll(selector).forEach((el: Element) => {
|
|
441
|
-
const src = el.getAttribute('src') ||
|
|
442
|
-
el.querySelector('source')?.getAttribute('src') || ''
|
|
443
|
-
if (src) {
|
|
444
|
-
const p = document.createElement('p')
|
|
445
|
-
p.setAttribute('style', 'text-align: center; font-size: 0.9em; margin: 0.5em 0;')
|
|
446
|
-
const a = document.createElement('a')
|
|
447
|
-
a.href = src
|
|
448
|
-
a.textContent = prefix + src
|
|
449
|
-
a.setAttribute('style', 'color: #1a73e8; text-decoration: underline;')
|
|
450
|
-
p.appendChild(a)
|
|
451
|
-
el.replaceWith(p)
|
|
452
|
-
} else {
|
|
453
|
-
el.remove()
|
|
454
|
-
}
|
|
455
|
-
})
|
|
456
|
-
}
|
|
457
|
-
replaceMediaWithLink('iframe', '🔗 ')
|
|
458
|
-
replaceMediaWithLink('video', '▶ ')
|
|
459
|
-
|
|
460
|
-
// Helper: replace tagged elements with their pre-captured images
|
|
461
|
-
const replaceWithImage = (
|
|
462
|
-
selector: string,
|
|
463
|
-
dataAttr: string,
|
|
464
|
-
images: [number, string][],
|
|
465
|
-
options: {
|
|
466
|
-
alt: string | ((el: Element) => string)
|
|
467
|
-
wrapInFigure?: boolean
|
|
468
|
-
reuseAsContainer?: boolean
|
|
469
|
-
imgStyle?: string | ((el: Element) => string)
|
|
470
|
-
figureStyle?: string
|
|
471
|
-
},
|
|
472
|
-
) => {
|
|
473
|
-
const defaultImgStyle = 'max-width: 100%; height: auto; display: block; margin: 0 auto;'
|
|
474
|
-
bodyClone.querySelectorAll(selector).forEach((el: Element) => {
|
|
475
|
-
try {
|
|
476
|
-
const idx = parseInt(el.getAttribute(dataAttr) || '-1')
|
|
477
|
-
const entry = images.find((item: any) => item[0] === idx)
|
|
478
|
-
if (!entry || !entry[1]) { el.remove(); return }
|
|
479
|
-
|
|
480
|
-
const img = document.createElement('img')
|
|
481
|
-
img.src = entry[1]
|
|
482
|
-
img.alt = typeof options.alt === 'function' ? options.alt(el) : options.alt
|
|
483
|
-
const style = typeof options.imgStyle === 'function'
|
|
484
|
-
? options.imgStyle(el) : (options.imgStyle ?? defaultImgStyle)
|
|
485
|
-
img.setAttribute('style', style)
|
|
486
|
-
|
|
487
|
-
if (options.reuseAsContainer) {
|
|
488
|
-
el.innerHTML = ''
|
|
489
|
-
el.appendChild(img)
|
|
490
|
-
if (options.figureStyle) el.setAttribute('style', options.figureStyle)
|
|
491
|
-
} else if (options.wrapInFigure) {
|
|
492
|
-
const figure = document.createElement('figure')
|
|
493
|
-
figure.setAttribute('style',
|
|
494
|
-
options.figureStyle || 'margin: 1.5em auto; text-align: center; page-break-inside: avoid;')
|
|
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
|
-
replaceWithImage('lia-embed[data-embed-index]', 'data-embed-index', embedImagesData, {
|
|
507
|
-
alt: 'Embedded media',
|
|
508
|
-
imgStyle: (el) => el.getAttribute('style') || 'width:250px;height:250px;',
|
|
509
|
-
})
|
|
510
|
-
|
|
511
|
-
replaceWithImage('lia-chart[data-chart-index]', 'data-chart-index', chartImagesData, {
|
|
512
|
-
alt: (el) => el.getAttribute('aria-label') || 'Chart',
|
|
513
|
-
wrapInFigure: true,
|
|
514
|
-
})
|
|
515
|
-
|
|
516
|
-
replaceWithImage('.lia-code-terminal[data-abc-index]', 'data-abc-index', abcImagesData, {
|
|
517
|
-
alt: 'ABC Music Notation', wrapInFigure: true,
|
|
518
|
-
})
|
|
519
|
-
|
|
520
|
-
replaceWithImage('lia-abcjs[data-standalone-abc-index]', 'data-standalone-abc-index', standaloneAbcImagesData, {
|
|
521
|
-
alt: 'ABC Music Notation', wrapInFigure: true,
|
|
522
|
-
})
|
|
523
|
-
|
|
524
|
-
replaceWithImage('figure.lia-figure[data-svg-index]', 'data-svg-index', svgImagesData, {
|
|
525
|
-
alt: 'ASCII Diagram',
|
|
526
|
-
reuseAsContainer: true,
|
|
527
|
-
figureStyle:
|
|
528
|
-
'margin: 1.5em auto; padding: 1.5em; background-color: #f8f9fa; ' +
|
|
529
|
-
'border: 1px solid #dee2e6; border-radius: 4px; text-align: center; ' +
|
|
530
|
-
'page-break-inside: avoid; max-width: 90%;',
|
|
531
|
-
})
|
|
532
|
-
|
|
533
|
-
replaceWithImage('svg[data-inline-svg-index]', 'data-inline-svg-index', inlineSvgImagesData, {
|
|
534
|
-
alt: 'SVG Graphic',
|
|
535
|
-
wrapInFigure: true,
|
|
536
|
-
figureStyle: 'margin: 1.5em auto; text-align: center; page-break-inside: avoid;',
|
|
537
|
-
})
|
|
538
|
-
|
|
539
|
-
// Replace <lia-formula> with screenshot image (DOCX does not support MathML)
|
|
540
|
-
replaceWithImage('lia-formula[data-formula-index]', 'data-formula-index', formulaImagesData, {
|
|
541
|
-
alt: 'Math Formula',
|
|
542
|
-
wrapInFigure: false,
|
|
543
|
-
})
|
|
544
|
-
|
|
545
|
-
// Process terminal output blocks
|
|
546
|
-
bodyClone.querySelectorAll('.lia-code-terminal').forEach((terminal: Element) => {
|
|
547
|
-
try {
|
|
548
|
-
if (terminal.querySelector('lia-abcjs')) { terminal.remove(); return }
|
|
549
|
-
if (terminal.querySelector('lia-formula')) { terminal.remove(); return }
|
|
550
|
-
|
|
551
|
-
// If terminal contains a formula image (already replaced), unwrap it
|
|
552
|
-
const formulaImg = terminal.querySelector('img[alt="Math Formula"]')
|
|
553
|
-
if (formulaImg) { terminal.replaceWith(formulaImg); return }
|
|
554
|
-
|
|
555
|
-
const idx = parseInt(terminal.getAttribute('data-terminal-index') || '-1')
|
|
556
|
-
const entry = terminalOutputsData.find((item: [number, string]) => item[0] === idx)
|
|
557
|
-
if (entry && entry[1]) {
|
|
558
|
-
const wrapper = document.createElement('div')
|
|
559
|
-
wrapper.innerHTML = entry[1]
|
|
560
|
-
terminal.replaceWith(wrapper.firstChild || wrapper)
|
|
561
|
-
} else {
|
|
562
|
-
// Fallback: read lia-terminal from clone
|
|
563
|
-
const terminalOutput = terminal.querySelector('lia-terminal')
|
|
564
|
-
if (!terminalOutput) { terminal.remove(); return }
|
|
565
|
-
const text = (terminalOutput.textContent || '')
|
|
566
|
-
.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
|
567
|
-
const lines = text.split('\n').map(
|
|
568
|
-
(line: string) => `<span style="color:#d4d4d4;">${line}</span>`,
|
|
569
|
-
)
|
|
570
|
-
const fallbackHTML =
|
|
571
|
-
`<table style="width:100%;border-collapse:collapse;">` +
|
|
572
|
-
`<tr><td style="background-color:#1e1e1e;padding:8px;">` +
|
|
573
|
-
`<pre style="font-family:Courier;color:#d4d4d4;margin:0;white-space:pre;">${lines.join('<br>')}</pre>` +
|
|
574
|
-
`</td></tr></table>`
|
|
575
|
-
const wrapper = document.createElement('div')
|
|
576
|
-
wrapper.innerHTML = fallbackHTML
|
|
577
|
-
terminal.replaceWith(wrapper.firstChild || wrapper)
|
|
578
|
-
}
|
|
579
|
-
} catch (e) {
|
|
580
|
-
console.error('Error processing terminal block:', e)
|
|
581
|
-
}
|
|
582
|
-
})
|
|
583
|
-
|
|
584
|
-
// Replace Ace Editor code blocks with pre-extracted syntax-highlighted HTML
|
|
585
|
-
bodyClone.querySelectorAll('.lia-code__input').forEach((codeInput: Element) => {
|
|
586
|
-
try {
|
|
587
|
-
const idx = parseInt(codeInput.getAttribute('data-code-index') || '-1')
|
|
588
|
-
const entry = codeBlocks.find((item) => item[0] === idx)
|
|
589
|
-
if (entry && entry[1]) {
|
|
590
|
-
const wrapper = document.createElement('div')
|
|
591
|
-
wrapper.innerHTML = entry[1]
|
|
592
|
-
const newNode = wrapper.firstChild || wrapper
|
|
593
|
-
// Insert before .lia-code--block parent to preserve terminal siblings
|
|
594
|
-
const codeBlock = codeInput.parentElement?.classList.contains('lia-code--block')
|
|
595
|
-
? codeInput.parentElement : null
|
|
596
|
-
if (codeBlock) {
|
|
597
|
-
codeBlock.parentElement?.insertBefore(newNode, codeBlock)
|
|
598
|
-
codeInput.remove()
|
|
599
|
-
} else {
|
|
600
|
-
codeInput.replaceWith(newNode)
|
|
601
|
-
}
|
|
602
|
-
} else {
|
|
603
|
-
codeInput.remove()
|
|
604
|
-
}
|
|
605
|
-
} catch (e) {
|
|
606
|
-
console.error('Error injecting highlighted code block:', e)
|
|
607
|
-
}
|
|
608
|
-
})
|
|
609
|
-
|
|
610
|
-
// Build combined HTML from all <main> sections
|
|
611
|
-
const mainElements = bodyClone.querySelectorAll('main')
|
|
612
|
-
let combinedHTML = ''
|
|
613
|
-
|
|
614
|
-
if (mainElements.length > 0) {
|
|
615
|
-
mainElements.forEach((main: Element, index: number) => {
|
|
616
|
-
const headerEl = main.querySelector('header')
|
|
617
|
-
let chapterTitle = `Chapter ${index + 1}`
|
|
618
|
-
const hTag = headerEl?.querySelector('.h1, .h2, .h3, .h4, .h5, .h6')
|
|
619
|
-
if (hTag?.textContent) chapterTitle = hTag.textContent.trim()
|
|
620
|
-
if (headerEl) headerEl.remove()
|
|
621
|
-
main.querySelectorAll('script').forEach((el: Element) => el.remove())
|
|
622
|
-
|
|
623
|
-
const tempDiv = document.createElement('div')
|
|
624
|
-
tempDiv.appendChild(main.cloneNode(true))
|
|
625
|
-
combinedHTML += `<h1>${chapterTitle}</h1>`
|
|
626
|
-
combinedHTML += tempDiv.innerHTML
|
|
627
|
-
})
|
|
628
|
-
} else {
|
|
629
|
-
bodyClone.querySelectorAll('script').forEach((el: Element) => el.remove())
|
|
630
|
-
combinedHTML = bodyClone.outerHTML
|
|
631
|
-
}
|
|
632
|
-
|
|
633
|
-
return combinedHTML
|
|
634
|
-
}, payload)
|
|
635
|
-
|
|
636
|
-
// Convert external images to data URIs
|
|
637
|
-
console.log('Fetching external/URL-based images as data URIs...')
|
|
638
|
-
const processedHTML = await page.evaluate(async (html: string) => {
|
|
639
|
-
const parser = new DOMParser()
|
|
640
|
-
const doc = parser.parseFromString(html, 'text/html')
|
|
641
|
-
|
|
642
|
-
const imgs = Array.from(doc.querySelectorAll('img[src]'))
|
|
643
|
-
let fetched = 0
|
|
644
|
-
let failed = 0
|
|
645
|
-
await Promise.all(
|
|
646
|
-
imgs.map(async (img) => {
|
|
647
|
-
const src = img.getAttribute('src')!
|
|
648
|
-
if (src.startsWith('data:') || src.startsWith('file://')) return
|
|
649
|
-
try {
|
|
650
|
-
const response = await fetch(src)
|
|
651
|
-
if (!response.ok) { failed++; return }
|
|
652
|
-
const blob = await response.blob()
|
|
653
|
-
const dataUri = await new Promise<string>((resolve) => {
|
|
654
|
-
const reader = new FileReader()
|
|
655
|
-
reader.onload = () => resolve(reader.result as string)
|
|
656
|
-
reader.readAsDataURL(blob)
|
|
657
|
-
})
|
|
658
|
-
img.setAttribute('src', dataUri)
|
|
659
|
-
fetched++
|
|
660
|
-
} catch (e) {
|
|
661
|
-
console.warn('Failed to fetch image:', src, e)
|
|
662
|
-
failed++
|
|
663
|
-
}
|
|
664
|
-
}),
|
|
665
|
-
)
|
|
666
|
-
console.log(`Image fetch: ${fetched} succeeded, ${failed} failed`)
|
|
667
|
-
return doc.body.innerHTML
|
|
668
|
-
}, htmlContent)
|
|
669
|
-
|
|
670
|
-
const fullHTML = `<!DOCTYPE html>
|
|
671
|
-
<html lang="${argument['docx-language'] || DEFAULT_LANG}">
|
|
672
|
-
<head>
|
|
673
|
-
<meta charset="UTF-8" />
|
|
674
|
-
<title>${argument['docx-title'] || 'LiaScript Export'}</title>
|
|
675
|
-
</head>
|
|
676
|
-
<body>
|
|
677
|
-
${processedHTML}
|
|
678
|
-
</body>
|
|
679
|
-
</html>`
|
|
680
|
-
|
|
681
|
-
const docOptions: any = {
|
|
682
|
-
orientation: argument['docx-orientation'] || DEFAULT_ORIENTATION,
|
|
683
|
-
title: argument['docx-title'] || 'LiaScript Export',
|
|
684
|
-
creator: argument['docx-author'] || 'LiaScript Exporter',
|
|
685
|
-
subject: argument['docx-subject'],
|
|
686
|
-
description: argument['docx-description'],
|
|
687
|
-
lang: argument['docx-language'] || DEFAULT_LANG,
|
|
688
|
-
font: argument['docx-font'] || DEFAULT_FONT,
|
|
689
|
-
fontSize: argument['docx-font-size'] || DEFAULT_FONT_SIZE,
|
|
690
|
-
table: { row: { cantSplit: false } },
|
|
691
|
-
header: argument['docx-header'] ?? false,
|
|
692
|
-
footer: argument['docx-footer'] ?? false,
|
|
693
|
-
pageNumber: argument['docx-page-number'] ?? false,
|
|
694
|
-
imageProcessing: {
|
|
695
|
-
maxRetries: 3,
|
|
696
|
-
downloadTimeout: 10000,
|
|
697
|
-
maxImageSize: 20 * 1024 * 1024,
|
|
698
|
-
verboseLogging: false,
|
|
699
|
-
},
|
|
700
|
-
}
|
|
701
|
-
|
|
702
|
-
const headerHTML = argument['docx-header-html'] || null
|
|
703
|
-
const footerHTML = argument['docx-footer-html'] || null
|
|
704
|
-
|
|
705
|
-
console.log('Generating DOCX document...')
|
|
706
|
-
const docxBuffer = await HTMLtoDOCX(fullHTML, headerHTML, docOptions, footerHTML)
|
|
707
|
-
|
|
708
|
-
const outputPath = argument.output.endsWith('.docx')
|
|
709
|
-
? argument.output
|
|
710
|
-
: argument.output + '.docx'
|
|
711
|
-
|
|
712
|
-
fs.writeFileSync(outputPath, Buffer.from(docxBuffer as ArrayBuffer))
|
|
713
|
-
console.log(`DOCX successfully generated: ${outputPath}`)
|
|
714
|
-
} catch (e) {
|
|
715
|
-
const error = e as Error
|
|
716
|
-
throw new Error(`Failed to generate DOCX: ${error.message}`)
|
|
717
|
-
}
|
|
718
|
-
}
|
|
719
|
-
|
|
720
|
-
/** Extracts syntax-highlighted HTML from all Ace Editor code blocks. */
|
|
721
|
-
async function extractHighlightedCode(page: Page): Promise<Map<number, string>> {
|
|
722
|
-
const result = await page.evaluate(() => {
|
|
723
|
-
const escape = (text: string) =>
|
|
724
|
-
text.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
|
725
|
-
.replace(/"/g, '"').replace(/'/g, ''')
|
|
726
|
-
|
|
727
|
-
const blocks: [number, string][] = []
|
|
728
|
-
let idx = 0
|
|
729
|
-
|
|
730
|
-
document.querySelectorAll('.lia-code__input').forEach((codeInput: Element) => {
|
|
731
|
-
codeInput.setAttribute('data-code-index', idx.toString())
|
|
732
|
-
|
|
733
|
-
try {
|
|
734
|
-
const aceEditor = codeInput.querySelector('.ace_editor')
|
|
735
|
-
const aceContent = aceEditor?.querySelector('.ace_text-layer')
|
|
736
|
-
|
|
737
|
-
if (!aceEditor || !aceContent) { blocks.push([idx, '']); idx++; return }
|
|
738
|
-
|
|
739
|
-
const bgColor =
|
|
740
|
-
window.getComputedStyle(aceEditor as HTMLElement).backgroundColor || '#f5f5f5'
|
|
741
|
-
|
|
742
|
-
// Render a single .ace_line element's tokens as HTML with syntax colors
|
|
743
|
-
const renderLineContent = (line: Element): string => {
|
|
744
|
-
let lineHTML = ''
|
|
745
|
-
const hasTokenSpans = line.querySelector('span[class*="ace_"]') !== null
|
|
746
|
-
if (hasTokenSpans) {
|
|
747
|
-
line.childNodes.forEach((node: ChildNode) => {
|
|
748
|
-
if (node.nodeType === Node.TEXT_NODE) {
|
|
749
|
-
const cleaned = (node.textContent || '').replace(/[\u200B-\u200D\uFEFF]/g, '')
|
|
750
|
-
if (cleaned) lineHTML += escape(cleaned)
|
|
751
|
-
} else if (node.nodeType === Node.ELEMENT_NODE) {
|
|
752
|
-
const tokenEl = node as HTMLElement
|
|
753
|
-
const cleaned = (tokenEl.textContent || '').replace(/[\u200B-\u200D\uFEFF]/g, '')
|
|
754
|
-
if (!cleaned) return
|
|
755
|
-
const color = window.getComputedStyle(tokenEl).color
|
|
756
|
-
const fontWeight = window.getComputedStyle(tokenEl).fontWeight
|
|
757
|
-
const fontStyle = window.getComputedStyle(tokenEl).fontStyle
|
|
758
|
-
const escaped = escape(cleaned)
|
|
759
|
-
let style = ''
|
|
760
|
-
if (color && color !== 'rgb(0, 0, 0)' && color !== 'rgba(0, 0, 0, 0)') style += `color:${color};`
|
|
761
|
-
if (fontWeight === 'bold' || parseInt(fontWeight) >= 700) style += 'font-weight:bold;'
|
|
762
|
-
if (fontStyle === 'italic') style += 'font-style:italic;'
|
|
763
|
-
lineHTML += style ? `<span style="${style}">${escaped}</span>` : escaped
|
|
764
|
-
}
|
|
765
|
-
})
|
|
766
|
-
} else {
|
|
767
|
-
lineHTML += escape((line.textContent || '').replace(/[\u200B-\u200D\uFEFF\n]/g, ''))
|
|
768
|
-
}
|
|
769
|
-
return lineHTML
|
|
770
|
-
}
|
|
771
|
-
|
|
772
|
-
const lineParas: string[] = []
|
|
773
|
-
const pStyle = 'font-family:Courier;margin:0;padding:0;white-space:pre;'
|
|
774
|
-
|
|
775
|
-
const lineGroups = aceContent.querySelectorAll('.ace_line_group')
|
|
776
|
-
if (lineGroups.length > 0) {
|
|
777
|
-
lineGroups.forEach((lineGroup: Element) => {
|
|
778
|
-
const subLines = lineGroup.querySelectorAll('.ace_line')
|
|
779
|
-
if (subLines.length === 0) { lineParas.push(`<p style="${pStyle}"> </p>`); return }
|
|
780
|
-
let lineHTML = ''
|
|
781
|
-
subLines.forEach((line: Element) => { lineHTML += renderLineContent(line) })
|
|
782
|
-
lineParas.push(`<p style="${pStyle}">${lineHTML || ' '}</p>`)
|
|
783
|
-
})
|
|
784
|
-
} else {
|
|
785
|
-
const rawText = (aceContent.textContent || '').replace(/[\u200B-\u200D\uFEFF]/g, '').trimEnd()
|
|
786
|
-
rawText.split('\n').forEach((line: string) => {
|
|
787
|
-
lineParas.push(`<p style="${pStyle}">${escape(line) || ' '}</p>`)
|
|
788
|
-
})
|
|
789
|
-
}
|
|
790
|
-
|
|
791
|
-
// Single-cell table for reliable background + border in DOCX
|
|
792
|
-
const html =
|
|
793
|
-
`<table style="width:100%;border-collapse:collapse;">` +
|
|
794
|
-
`<tr><td style="background-color:${bgColor};border-left:3px solid #4caf50;padding:8px;">` +
|
|
795
|
-
lineParas.join('') +
|
|
796
|
-
`</td></tr></table>`
|
|
797
|
-
|
|
798
|
-
blocks.push([idx, html])
|
|
799
|
-
} catch (e) {
|
|
800
|
-
blocks.push([idx, ''])
|
|
801
|
-
}
|
|
802
|
-
|
|
803
|
-
idx++
|
|
804
|
-
})
|
|
805
|
-
|
|
806
|
-
return blocks
|
|
807
|
-
})
|
|
808
|
-
|
|
809
|
-
return new Map(result)
|
|
810
|
-
}
|
|
811
|
-
|
|
812
|
-
/** Extracts SVG from <lia-abcjs> shadow DOMs and returns as data URIs. */
|
|
813
|
-
async function extractAbcSvgs(page: Page, insideTerminal: boolean): Promise<Map<number, string>> {
|
|
814
|
-
const dataAttr = insideTerminal ? 'data-abc-index' : 'data-standalone-abc-index'
|
|
815
|
-
|
|
816
|
-
const count = await page.evaluate((inside: boolean, attr: string) => {
|
|
817
|
-
let idx = 0
|
|
818
|
-
if (inside) {
|
|
819
|
-
document.querySelectorAll('.lia-code-terminal').forEach((terminal: Element) => {
|
|
820
|
-
if (terminal.querySelector('lia-abcjs')) {
|
|
821
|
-
terminal.setAttribute(attr, idx.toString())
|
|
822
|
-
idx++
|
|
823
|
-
}
|
|
824
|
-
})
|
|
825
|
-
} else {
|
|
826
|
-
document.querySelectorAll('lia-abcjs').forEach((el: Element) => {
|
|
827
|
-
if (!el.closest('.lia-code-terminal')) {
|
|
828
|
-
el.setAttribute(attr, idx.toString())
|
|
829
|
-
idx++
|
|
830
|
-
}
|
|
831
|
-
})
|
|
832
|
-
}
|
|
833
|
-
return idx
|
|
834
|
-
}, insideTerminal, dataAttr)
|
|
835
|
-
|
|
836
|
-
const images = new Map<number, string>()
|
|
837
|
-
|
|
838
|
-
for (let i = 0; i < count; i++) {
|
|
839
|
-
try {
|
|
840
|
-
// Find the lia-abcjs element (directly or inside a tagged terminal)
|
|
841
|
-
const svgDataUri = await page.evaluate((inside: boolean, attr: string, idx: number) => {
|
|
842
|
-
let abcEl: Element | null = null
|
|
843
|
-
if (inside) {
|
|
844
|
-
const terminal = document.querySelector(`.lia-code-terminal[${attr}="${idx}"]`)
|
|
845
|
-
abcEl = terminal?.querySelector('lia-abcjs') || null
|
|
846
|
-
} else {
|
|
847
|
-
abcEl = document.querySelector(`lia-abcjs[${attr}="${idx}"]`)
|
|
848
|
-
}
|
|
849
|
-
if (!abcEl) return null
|
|
850
|
-
const svg = (abcEl as any).shadowRoot?.getElementById('paper')?.querySelector('svg')
|
|
851
|
-
if (!svg) return null
|
|
852
|
-
return 'data:image/svg+xml;base64,' + btoa(new XMLSerializer().serializeToString(svg))
|
|
853
|
-
}, insideTerminal, dataAttr, i)
|
|
854
|
-
|
|
855
|
-
if (svgDataUri) images.set(i, svgDataUri)
|
|
856
|
-
} catch (e) {
|
|
857
|
-
console.error(`Failed to convert ABC block ${i}:`, e)
|
|
858
|
-
}
|
|
859
|
-
}
|
|
860
|
-
|
|
861
|
-
return images
|
|
862
|
-
}
|
|
863
|
-
|
|
864
|
-
/** Extracts SVG from <lia-chart> elements (ECharts). */
|
|
865
|
-
async function extractChartSvgs(page: Page): Promise<Map<number, string>> {
|
|
866
|
-
const count = await page.evaluate(() => {
|
|
867
|
-
let idx = 0
|
|
868
|
-
document.querySelectorAll('lia-chart').forEach((el: Element) => {
|
|
869
|
-
el.setAttribute('data-chart-index', idx.toString())
|
|
870
|
-
idx++
|
|
871
|
-
})
|
|
872
|
-
return idx
|
|
873
|
-
})
|
|
874
|
-
|
|
875
|
-
const images = new Map<number, string>()
|
|
876
|
-
const elements = await page.$$('lia-chart')
|
|
877
|
-
|
|
878
|
-
for (let i = 0; i < count; i++) {
|
|
879
|
-
try {
|
|
880
|
-
const el = elements[i]
|
|
881
|
-
if (!el) continue
|
|
882
|
-
|
|
883
|
-
const svgDataUri = await page.evaluate((host: Element) => {
|
|
884
|
-
const svg = host.shadowRoot?.querySelector('svg')
|
|
885
|
-
if (!svg) return null
|
|
886
|
-
return 'data:image/svg+xml;base64,' + btoa(new XMLSerializer().serializeToString(svg))
|
|
887
|
-
}, el)
|
|
888
|
-
|
|
889
|
-
if (svgDataUri) images.set(i, svgDataUri)
|
|
890
|
-
} catch (e) {
|
|
891
|
-
console.error(`Failed to extract chart ${i}:`, e)
|
|
892
|
-
}
|
|
893
|
-
}
|
|
894
|
-
|
|
895
|
-
return images
|
|
896
|
-
}
|
|
897
|
-
|
|
898
|
-
/** Tags matching elements with a data attribute index, then screenshots each one. */
|
|
899
|
-
async function screenshotTaggedElements(
|
|
900
|
-
page: Page,
|
|
901
|
-
selector: string,
|
|
902
|
-
dataAttr: string,
|
|
903
|
-
label: string,
|
|
904
|
-
tagFilter?: string,
|
|
905
|
-
): Promise<Map<number, string>> {
|
|
906
|
-
const count = await page.evaluate(
|
|
907
|
-
(sel: string, attr: string, filterBody: string | null) => {
|
|
908
|
-
let idx = 0
|
|
909
|
-
const filterFn = filterBody
|
|
910
|
-
? (new Function('el', filterBody) as (el: Element) => boolean)
|
|
911
|
-
: null
|
|
912
|
-
document.querySelectorAll(sel).forEach((el: Element) => {
|
|
913
|
-
if (!filterFn || filterFn(el)) {
|
|
914
|
-
el.setAttribute(attr, idx.toString())
|
|
915
|
-
idx++
|
|
916
|
-
}
|
|
917
|
-
})
|
|
918
|
-
return idx
|
|
919
|
-
},
|
|
920
|
-
selector, dataAttr, tagFilter ?? null,
|
|
921
|
-
)
|
|
922
|
-
|
|
923
|
-
const images = new Map<number, string>()
|
|
924
|
-
for (let i = 0; i < count; i++) {
|
|
925
|
-
try {
|
|
926
|
-
const el = await page.$(`${selector}[${dataAttr}="${i}"]`)
|
|
927
|
-
if (!el) continue
|
|
928
|
-
|
|
929
|
-
// For lia-formula: clone KaTeX HTML into a regular <div> where the
|
|
930
|
-
// globally-injected KaTeX CDN stylesheet applies (declarative shadow
|
|
931
|
-
// DOM styles don't work in headless Chromium).
|
|
932
|
-
const hasShadow = await page.evaluate((el: Element) => {
|
|
933
|
-
const shadow = (el as any).shadowRoot
|
|
934
|
-
if (!shadow) return false
|
|
935
|
-
|
|
936
|
-
const katex = shadow.querySelector('.katex-display') || shadow.querySelector('.katex')
|
|
937
|
-
if (!katex) return false
|
|
938
|
-
|
|
939
|
-
const clone = katex.cloneNode(true) as HTMLElement
|
|
940
|
-
clone.querySelectorAll('.katex-mathml').forEach((m: Element) => m.remove())
|
|
941
|
-
|
|
942
|
-
const container = document.createElement('div')
|
|
943
|
-
container.setAttribute('data-formula-clone', 'true')
|
|
944
|
-
container.style.cssText =
|
|
945
|
-
'position:absolute;top:0;left:10000px;display:inline-block;background:white;padding:8px;'
|
|
946
|
-
container.appendChild(clone)
|
|
947
|
-
document.body.appendChild(container)
|
|
948
|
-
return true
|
|
949
|
-
}, el)
|
|
950
|
-
|
|
951
|
-
let screenshot: Buffer | Uint8Array
|
|
952
|
-
if (hasShadow) {
|
|
953
|
-
const cloneHandle = await page.$('[data-formula-clone="true"]')
|
|
954
|
-
screenshot = cloneHandle
|
|
955
|
-
? await cloneHandle.screenshot({ type: 'png' })
|
|
956
|
-
: await el.screenshot({ type: 'png' })
|
|
957
|
-
await page.evaluate(() => {
|
|
958
|
-
document.querySelector('[data-formula-clone="true"]')?.remove()
|
|
959
|
-
})
|
|
960
|
-
} else {
|
|
961
|
-
screenshot = await el.screenshot({ type: 'png' })
|
|
962
|
-
}
|
|
963
|
-
|
|
964
|
-
images.set(i, `data:image/png;base64,${Buffer.from(screenshot).toString('base64')}`)
|
|
965
|
-
} catch (e) {
|
|
966
|
-
console.error(`Failed to screenshot ${label} ${i}:`, e)
|
|
967
|
-
}
|
|
968
|
-
}
|
|
969
|
-
|
|
970
|
-
return images
|
|
971
|
-
}
|
|
972
|
-
|
|
973
|
-
/** Extracts terminal output blocks from the live page. */
|
|
974
|
-
async function extractTerminalOutputs(page: Page): Promise<Map<number, string>> {
|
|
975
|
-
const result = await page.evaluate(() => {
|
|
976
|
-
const blocks: [number, string][] = []
|
|
977
|
-
let idx = 0
|
|
978
|
-
const escape = (text: string) =>
|
|
979
|
-
text.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
|
980
|
-
|
|
981
|
-
document.querySelectorAll('.lia-code-terminal').forEach((terminal: Element) => {
|
|
982
|
-
terminal.setAttribute('data-terminal-index', idx.toString())
|
|
983
|
-
|
|
984
|
-
try {
|
|
985
|
-
const terminalOutput = terminal.querySelector('lia-terminal')
|
|
986
|
-
if (!terminalOutput) { blocks.push([idx, '']); idx++; return }
|
|
987
|
-
|
|
988
|
-
const lineSpans: string[] = []
|
|
989
|
-
const textDivs = terminalOutput.querySelectorAll('div[class^="text-"]')
|
|
990
|
-
if (textDivs.length > 0) {
|
|
991
|
-
textDivs.forEach((div: Element) => {
|
|
992
|
-
const text = escape(div.textContent || '')
|
|
993
|
-
if (div.classList.contains('text-error')) {
|
|
994
|
-
lineSpans.push(`<span style="color:#f48771;">${text}</span>`)
|
|
995
|
-
} else if (div.classList.contains('text-warning')) {
|
|
996
|
-
lineSpans.push(`<span style="color:#dcdcaa;">${text}</span>`)
|
|
997
|
-
} else {
|
|
998
|
-
lineSpans.push(`<span style="color:#d4d4d4;">${text}</span>`)
|
|
999
|
-
}
|
|
1000
|
-
})
|
|
1001
|
-
} else {
|
|
1002
|
-
escape(terminalOutput.textContent || '').split('\n').forEach((line) => {
|
|
1003
|
-
lineSpans.push(`<span style="color:#d4d4d4;">${line}</span>`)
|
|
1004
|
-
})
|
|
1005
|
-
}
|
|
1006
|
-
|
|
1007
|
-
const html =
|
|
1008
|
-
`<table style="width:100%;border-collapse:collapse;">` +
|
|
1009
|
-
`<tr><td style="background-color:#1e1e1e;padding:8px;">` +
|
|
1010
|
-
`<pre style="font-family:Courier;color:#d4d4d4;margin:0;white-space:pre;">${lineSpans.join('<br>')}</pre>` +
|
|
1011
|
-
`</td></tr></table>`
|
|
1012
|
-
|
|
1013
|
-
blocks.push([idx, html])
|
|
1014
|
-
} catch (e) {
|
|
1015
|
-
blocks.push([idx, ''])
|
|
1016
|
-
}
|
|
1017
|
-
|
|
1018
|
-
idx++
|
|
1019
|
-
})
|
|
1020
|
-
|
|
1021
|
-
return blocks
|
|
1022
|
-
})
|
|
1023
|
-
|
|
1024
|
-
return new Map(result)
|
|
1025
|
-
}
|