@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.
Files changed (72) hide show
  1. package/dist/assets/capacitor/{index.bfe7363b.js → index.a7f021f7.js} +1 -1
  2. package/dist/assets/capacitor/index.html +1 -1
  3. package/dist/assets/capacitor/{jszip.min.f6eda75b.js → jszip.min.43389eb1.js} +1 -1
  4. package/dist/assets/capacitor/{trystero-ipfs.min.b27a61d7.js → trystero-ipfs.min.f25fe3e7.js} +1 -1
  5. package/dist/assets/indexeddb/{index.599a57d6.js → index.4aceca2f.js} +1 -1
  6. package/dist/assets/indexeddb/index.html +1 -1
  7. package/dist/assets/indexeddb/{jszip.min.63142cc8.js → jszip.min.4fbcc13f.js} +1 -1
  8. package/dist/assets/scorm2004/{index.7a5820ab.js → index.33bec53a.js} +1 -1
  9. package/dist/assets/scorm2004/index.html +1 -1
  10. package/dist/assets/scorm2004/{jszip.min.63142cc8.js → jszip.min.4fbcc13f.js} +1 -1
  11. package/dist/assets/xapi/{index.018a032a.js → index.f2e89e49.js} +1 -1
  12. package/dist/assets/xapi/index.html +1 -1
  13. package/dist/assets/xapi/{jszip.min.eaecf580.js → jszip.min.19c66d77.js} +1 -1
  14. package/dist/index.js +47 -47
  15. package/dist/server/presets.json +94 -0
  16. package/dist/server/presets.yaml +120 -0
  17. package/dist/server/public/app.js +1 -0
  18. package/dist/server/public/assets/android.svg +38 -0
  19. package/dist/server/public/assets/cmi.svg +154 -0
  20. package/dist/server/public/assets/docx.svg +20 -0
  21. package/dist/server/public/assets/edX.svg +75 -0
  22. package/dist/server/public/assets/edx.svg +75 -0
  23. package/dist/server/public/assets/epub.svg +18 -0
  24. package/dist/server/public/assets/icon.svg +82 -0
  25. package/dist/server/public/assets/ilias.png +0 -0
  26. package/dist/server/public/assets/json.svg +4 -0
  27. package/dist/server/public/assets/learnworlds.png +0 -0
  28. package/dist/server/public/assets/moodle.svg +190 -0
  29. package/dist/server/public/assets/opal.png +0 -0
  30. package/dist/server/public/assets/openolat.png +0 -0
  31. package/dist/server/public/assets/pdf.svg +4 -0
  32. package/dist/server/public/assets/rdf.svg +4 -0
  33. package/dist/server/public/assets/scorm.png +0 -0
  34. package/dist/server/public/assets/web.png +0 -0
  35. package/dist/server/public/assets/xapi.png +0 -0
  36. package/dist/server/public/i18n.js +1 -0
  37. package/dist/server/public/index.html +1587 -0
  38. package/dist/server/public/locales/de.json +247 -0
  39. package/dist/server/public/locales/en.json +247 -0
  40. package/dist/server/public/status.html +251 -0
  41. package/dist/server/public/styles.css +712 -0
  42. package/package.json +5 -1
  43. package/.parcelrc +0 -3
  44. package/DESKTOP_APP_README.md +0 -58
  45. package/DOCKERHUB_DESCRIPTION.md +0 -52
  46. package/Dockerfile +0 -129
  47. package/PLAYSTORE_GUIDE.md +0 -172
  48. package/action.yml +0 -157
  49. package/custom.css +0 -10
  50. package/electron-builder.json +0 -149
  51. package/src/cli.ts +0 -69
  52. package/src/colorize.ts +0 -115
  53. package/src/export/android.ts +0 -419
  54. package/src/export/docx.ts +0 -1025
  55. package/src/export/epub.ts +0 -1306
  56. package/src/export/h5p.ts +0 -390
  57. package/src/export/helper.ts +0 -360
  58. package/src/export/ims.ts +0 -191
  59. package/src/export/pdf.ts +0 -406
  60. package/src/export/presets.ts +0 -220
  61. package/src/export/project.ts +0 -829
  62. package/src/export/rdf.ts +0 -551
  63. package/src/export/scorm12.ts +0 -167
  64. package/src/export/scorm2004.ts +0 -140
  65. package/src/export/web.ts +0 -306
  66. package/src/export/xapi.ts +0 -424
  67. package/src/exporter.ts +0 -296
  68. package/src/index.ts +0 -96
  69. package/src/parser.ts +0 -373
  70. package/src/presets.yaml +0 -219
  71. package/src/types.ts +0 -82
  72. package/tsconfig.json +0 -24
@@ -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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
725
- .replace(/"/g, '&quot;').replace(/'/g, '&#39;')
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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
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
- }