@cyber-dash-tech/revela 0.19.6 → 0.19.7
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/README.md +14 -14
- package/README.zh-CN.md +14 -14
- package/designs/lucent/DESIGN.md +1 -1
- package/designs/lucent/design.css +15 -0
- package/designs/lucent-dark/DESIGN.md +1 -1
- package/designs/lucent-dark/design.css +15 -0
- package/designs/monet/DESIGN.md +40 -127
- package/designs/monet/design.css +15 -0
- package/designs/starter/DESIGN.md +16 -24
- package/designs/starter/design.css +15 -0
- package/designs/summit/DESIGN.md +46 -113
- package/designs/summit/design.css +15 -0
- package/lib/design/designs.ts +1 -1
- package/lib/export/html.ts +14 -0
- package/lib/page-templates/built-in-preview.html +3 -2
- package/lib/page-templates/render.ts +54 -4
- package/lib/page-templates/vocabulary.ts +6 -2
- package/lib/pdf/export.ts +1 -10
- package/lib/pptx/export.ts +164 -15
- package/package.json +2 -1
- package/plugins/revela/.codex-plugin/plugin.json +1 -1
package/lib/pptx/export.ts
CHANGED
|
@@ -22,6 +22,7 @@ import { basename, dirname, extname, join, posix as pathPosix, resolve } from "p
|
|
|
22
22
|
import { randomBytes } from "crypto"
|
|
23
23
|
import { pathToFileURL } from "url"
|
|
24
24
|
import { findChromePath, launchChrome } from "../browser/chrome"
|
|
25
|
+
import { withExportBaseHref } from "../export/html"
|
|
25
26
|
|
|
26
27
|
const CANVAS_W = 1920
|
|
27
28
|
const CANVAS_H = 1080
|
|
@@ -344,6 +345,7 @@ async function preparePage(
|
|
|
344
345
|
document.documentElement.style.scrollSnapType = "none"
|
|
345
346
|
document.documentElement.style.overflow = "visible"
|
|
346
347
|
document.body.style.overflow = "visible"
|
|
348
|
+
document.body.style.margin = "0"
|
|
347
349
|
|
|
348
350
|
const slides = Array.from(document.querySelectorAll(".slide")) as HTMLElement[]
|
|
349
351
|
if (slides.length === 0) {
|
|
@@ -366,7 +368,24 @@ async function preparePage(
|
|
|
366
368
|
exportStyle = document.createElement("style")
|
|
367
369
|
exportStyle.id = "revela-pptx-export-style"
|
|
368
370
|
exportStyle.textContent = `
|
|
371
|
+
html, body {
|
|
372
|
+
scroll-snap-type: none !important;
|
|
373
|
+
overflow: visible !important;
|
|
374
|
+
}
|
|
375
|
+
.slide {
|
|
376
|
+
width: 1920px !important;
|
|
377
|
+
min-width: 1920px !important;
|
|
378
|
+
height: 1080px !important;
|
|
379
|
+
min-height: 1080px !important;
|
|
380
|
+
display: flex !important;
|
|
381
|
+
align-items: center !important;
|
|
382
|
+
justify-content: center !important;
|
|
383
|
+
overflow: hidden !important;
|
|
384
|
+
scroll-snap-align: none !important;
|
|
385
|
+
}
|
|
369
386
|
.slide-canvas {
|
|
387
|
+
width: 1920px !important;
|
|
388
|
+
height: 1080px !important;
|
|
370
389
|
transform: none !important;
|
|
371
390
|
transform-origin: top left !important;
|
|
372
391
|
transition: none !important;
|
|
@@ -466,6 +485,48 @@ async function readSlideMeta(
|
|
|
466
485
|
})
|
|
467
486
|
}
|
|
468
487
|
|
|
488
|
+
async function rasterizeFormulaNodes(page: Page, diagnostics: string[]): Promise<number> {
|
|
489
|
+
const handles = await page.$$(".template-text-panel-formula, .text-panel-formula")
|
|
490
|
+
let rasterized = 0
|
|
491
|
+
|
|
492
|
+
for (const handle of handles) {
|
|
493
|
+
try {
|
|
494
|
+
const box = await handle.boundingBox()
|
|
495
|
+
if (!box || box.width <= 0 || box.height <= 0) continue
|
|
496
|
+
|
|
497
|
+
const base64 = await (handle as any).screenshot({
|
|
498
|
+
encoding: "base64",
|
|
499
|
+
omitBackground: true,
|
|
500
|
+
})
|
|
501
|
+
|
|
502
|
+
await handle.evaluate((node, payload) => {
|
|
503
|
+
const element = node as HTMLElement
|
|
504
|
+
const img = document.createElement("img")
|
|
505
|
+
img.src = `data:image/png;base64,${payload.base64}`
|
|
506
|
+
img.alt = element.getAttribute("data-latex") || "Formula"
|
|
507
|
+
img.setAttribute("data-revela-pptx-formula-raster", "true")
|
|
508
|
+
img.style.width = `${payload.width}px`
|
|
509
|
+
img.style.height = `${payload.height}px`
|
|
510
|
+
img.style.maxWidth = "100%"
|
|
511
|
+
img.style.display = "block"
|
|
512
|
+
img.style.objectFit = "contain"
|
|
513
|
+
element.replaceChildren(img)
|
|
514
|
+
}, {
|
|
515
|
+
base64,
|
|
516
|
+
width: Math.ceil(box.width),
|
|
517
|
+
height: Math.ceil(box.height),
|
|
518
|
+
})
|
|
519
|
+
|
|
520
|
+
rasterized += 1
|
|
521
|
+
} catch (error) {
|
|
522
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
523
|
+
diagnostics.push(`formula rasterize failed: ${message}`)
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
return rasterized
|
|
528
|
+
}
|
|
529
|
+
|
|
469
530
|
async function exportSlidePptx(
|
|
470
531
|
page: Page,
|
|
471
532
|
diagnostics: string[],
|
|
@@ -499,15 +560,47 @@ async function exportSlidePptx(
|
|
|
499
560
|
})
|
|
500
561
|
}
|
|
501
562
|
|
|
502
|
-
target.scrollIntoView({ block: "center", inline: "center" })
|
|
503
563
|
target.style.transform = "none"
|
|
504
564
|
target.style.transformOrigin = "top left"
|
|
505
565
|
target.style.transition = "none"
|
|
506
566
|
target.style.animation = "none"
|
|
507
567
|
await new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve)))
|
|
508
568
|
|
|
569
|
+
const host = document.createElement("div")
|
|
570
|
+
host.id = "revela-pptx-export-root"
|
|
571
|
+
host.style.position = "fixed"
|
|
572
|
+
host.style.left = "0"
|
|
573
|
+
host.style.top = "0"
|
|
574
|
+
host.style.width = "1920px"
|
|
575
|
+
host.style.height = "1080px"
|
|
576
|
+
host.style.overflow = "hidden"
|
|
577
|
+
host.style.zIndex = "2147483647"
|
|
578
|
+
host.style.background = "transparent"
|
|
579
|
+
|
|
580
|
+
const exportTarget = target.cloneNode(true) as HTMLElement
|
|
581
|
+
exportTarget.style.width = "1920px"
|
|
582
|
+
exportTarget.style.height = "1080px"
|
|
583
|
+
exportTarget.style.position = "relative"
|
|
584
|
+
exportTarget.style.left = "0"
|
|
585
|
+
exportTarget.style.top = "0"
|
|
586
|
+
exportTarget.style.transform = "none"
|
|
587
|
+
exportTarget.style.transformOrigin = "top left"
|
|
588
|
+
exportTarget.style.transition = "none"
|
|
589
|
+
exportTarget.style.animation = "none"
|
|
590
|
+
host.appendChild(exportTarget)
|
|
591
|
+
document.body.appendChild(host)
|
|
592
|
+
await new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve)))
|
|
593
|
+
|
|
594
|
+
const rect = exportTarget.getBoundingClientRect()
|
|
595
|
+
if (Math.abs(rect.width - 1920) > 2 || Math.abs(rect.height - 1080) > 2) {
|
|
596
|
+
host.remove()
|
|
597
|
+
throw new Error(
|
|
598
|
+
`Slide ${index + 1} export canvas is ${Math.round(rect.width)}x${Math.round(rect.height)}, expected 1920x1080.`
|
|
599
|
+
)
|
|
600
|
+
}
|
|
601
|
+
|
|
509
602
|
await Promise.all(
|
|
510
|
-
Array.from(
|
|
603
|
+
Array.from(exportTarget.querySelectorAll("img")).map(async (img) => {
|
|
511
604
|
if (img.complete) return
|
|
512
605
|
await new Promise((resolve) => {
|
|
513
606
|
img.addEventListener("load", resolve, { once: true })
|
|
@@ -516,15 +609,19 @@ async function exportSlidePptx(
|
|
|
516
609
|
})
|
|
517
610
|
)
|
|
518
611
|
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
612
|
+
try {
|
|
613
|
+
const blob: Blob = await domToPptx.exportToPptx(exportTarget, {
|
|
614
|
+
fileName: `slide-${index + 1}.pptx`,
|
|
615
|
+
skipDownload: true,
|
|
616
|
+
svgAsVector: false,
|
|
617
|
+
autoEmbedFonts,
|
|
618
|
+
width: 10,
|
|
619
|
+
height: 5.625,
|
|
620
|
+
})
|
|
621
|
+
return Array.from(new Uint8Array(await blob.arrayBuffer()))
|
|
622
|
+
} finally {
|
|
623
|
+
host.remove()
|
|
624
|
+
}
|
|
528
625
|
}, { index: slide.index, autoEmbedFonts: options.autoEmbedFonts })
|
|
529
626
|
|
|
530
627
|
return {
|
|
@@ -717,6 +814,55 @@ function setSpeakerNotes(files: ZipFiles, notesPath: string, notes: string | nul
|
|
|
717
814
|
files[notesPath] = xmlToBytes(doc)
|
|
718
815
|
}
|
|
719
816
|
|
|
817
|
+
function stripSvgBlipFallbacks(pptxBytes: Uint8Array): Uint8Array {
|
|
818
|
+
const files = unzipSync(pptxBytes)
|
|
819
|
+
const orphanedSvgParts = new Set<string>()
|
|
820
|
+
|
|
821
|
+
for (const slidePath of Object.keys(files).filter((path) => /^ppt\/slides\/slide\d+\.xml$/.test(path))) {
|
|
822
|
+
const slideXml = getFileText(files, slidePath)
|
|
823
|
+
if (!slideXml.includes("asvg:svgBlip")) continue
|
|
824
|
+
|
|
825
|
+
const slideDoc = parseXml(slideXml)
|
|
826
|
+
const relsPath = relsPathForPart(slidePath)
|
|
827
|
+
const relsDoc = files[relsPath] ? parseXml(getFileText(files, relsPath)) : null
|
|
828
|
+
const svgRelIds = new Set<string>()
|
|
829
|
+
|
|
830
|
+
for (const svgBlip of Array.from(slideDoc.getElementsByTagName("asvg:svgBlip"))) {
|
|
831
|
+
const relId = svgBlip.getAttribute("r:embed")
|
|
832
|
+
if (relId) svgRelIds.add(relId)
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
for (const blip of Array.from(slideDoc.getElementsByTagName("a:blip"))) {
|
|
836
|
+
for (const extLst of Array.from(blip.getElementsByTagName("a:extLst"))) {
|
|
837
|
+
if (Array.from(extLst.getElementsByTagName("asvg:svgBlip")).length === 0) continue
|
|
838
|
+
blip.removeChild(extLst)
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
if (relsDoc && svgRelIds.size > 0) {
|
|
843
|
+
const relRoot = relsDoc.getElementsByTagName("Relationships")[0]
|
|
844
|
+
for (const rel of Array.from(relsDoc.getElementsByTagName("Relationship"))) {
|
|
845
|
+
const relId = rel.getAttribute("Id")
|
|
846
|
+
if (!relId || !svgRelIds.has(relId)) continue
|
|
847
|
+
const target = rel.getAttribute("Target")
|
|
848
|
+
if (target) orphanedSvgParts.add(resolveRelationshipTarget(slidePath, target))
|
|
849
|
+
relRoot?.removeChild(rel)
|
|
850
|
+
}
|
|
851
|
+
files[relsPath] = xmlToBytes(relsDoc)
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
files[slidePath] = xmlToBytes(slideDoc)
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
for (const partPath of orphanedSvgParts) {
|
|
858
|
+
if (/^ppt\/media\/.+\.svg$/i.test(partPath)) {
|
|
859
|
+
delete files[partPath]
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
return zipSync(files, { level: 0 })
|
|
864
|
+
}
|
|
865
|
+
|
|
720
866
|
export function applySpeakerNotesToPptx(pptxBytes: Uint8Array, notesBySlide: Array<string | null | undefined>): Uint8Array {
|
|
721
867
|
const files = unzipSync(pptxBytes)
|
|
722
868
|
|
|
@@ -724,7 +870,7 @@ export function applySpeakerNotesToPptx(pptxBytes: Uint8Array, notesBySlide: Arr
|
|
|
724
870
|
setSpeakerNotes(files, `ppt/notesSlides/notesSlide${index + 1}.xml`, notes ?? null)
|
|
725
871
|
})
|
|
726
872
|
|
|
727
|
-
return zipSync(files)
|
|
873
|
+
return stripSvgBlipFallbacks(zipSync(files))
|
|
728
874
|
}
|
|
729
875
|
|
|
730
876
|
function updateAppProperties(files: ZipFiles, slideCount: number): void {
|
|
@@ -898,7 +1044,7 @@ function mergeSingleSlidePptx(slides: ExportedSlide[]): Uint8Array {
|
|
|
898
1044
|
mergedFiles["ppt/_rels/presentation.xml.rels"] = xmlToBytes(presentationRelsDoc)
|
|
899
1045
|
updateAppProperties(mergedFiles, slides.length)
|
|
900
1046
|
|
|
901
|
-
return zipSync(mergedFiles, { level: 0 })
|
|
1047
|
+
return stripSvgBlipFallbacks(zipSync(mergedFiles, { level: 0 }))
|
|
902
1048
|
}
|
|
903
1049
|
|
|
904
1050
|
export async function exportToPptx(
|
|
@@ -937,7 +1083,7 @@ export async function exportToPptx(
|
|
|
937
1083
|
const prepareStart = Date.now()
|
|
938
1084
|
const originalHtml = readFileSync(abs, "utf-8")
|
|
939
1085
|
const localized = await localizeExternalImages(originalHtml, tmpDir)
|
|
940
|
-
const patchedHtml = await inlineImageAssets(localized.html, abs)
|
|
1086
|
+
const patchedHtml = withExportBaseHref(await inlineImageAssets(localized.html, abs), abs)
|
|
941
1087
|
tmpHtmlPath = join(tmpDir, "index.html")
|
|
942
1088
|
writeFileSync(tmpHtmlPath, patchedHtml, "utf-8")
|
|
943
1089
|
timingsMs.prepareMs = Date.now() - prepareStart
|
|
@@ -961,6 +1107,7 @@ export async function exportToPptx(
|
|
|
961
1107
|
try {
|
|
962
1108
|
const pageSetupStart = Date.now()
|
|
963
1109
|
const { page, slideCount, diagnostics } = await preparePage(browser, tmpHtmlPath, domToPptxBundlePath)
|
|
1110
|
+
const rasterizedFormulaCount = await rasterizeFormulaNodes(page, diagnostics)
|
|
964
1111
|
timingsMs.pageSetupMs = Date.now() - pageSetupStart
|
|
965
1112
|
const exported: ExportedSlide[] = []
|
|
966
1113
|
const failures: SlideFailure[] = []
|
|
@@ -969,7 +1116,9 @@ export async function exportToPptx(
|
|
|
969
1116
|
const slides = applySpeakerNotesOverride(await readSlideMeta(page, slideCount), options?.speakerNotes)
|
|
970
1117
|
await emitProgress(options, {
|
|
971
1118
|
kind: "stage",
|
|
972
|
-
message:
|
|
1119
|
+
message: rasterizedFormulaCount > 0
|
|
1120
|
+
? `Deck ready. Rasterized ${rasterizedFormulaCount} formula text member(s). Exporting ${slides.length} slide(s) to editable PPTX parts...`
|
|
1121
|
+
: `Deck ready. Exporting ${slides.length} slide(s) to editable PPTX parts...`,
|
|
973
1122
|
})
|
|
974
1123
|
|
|
975
1124
|
const slideExportStart = Date.now()
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cyber-dash-tech/revela",
|
|
3
|
-
"version": "0.19.
|
|
3
|
+
"version": "0.19.7",
|
|
4
4
|
"description": "Codex-first CLI/MCP workspace for trusted narrative artifacts from local sources, research, and evidence",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./index.ts",
|
|
@@ -68,6 +68,7 @@
|
|
|
68
68
|
"dom-to-pptx": "^1.1.6",
|
|
69
69
|
"fflate": "^0.8.2",
|
|
70
70
|
"jimp": "^1.6.1",
|
|
71
|
+
"katex": "^0.17.0",
|
|
71
72
|
"mammoth": "^1.12.0",
|
|
72
73
|
"pdf-lib": "^1.17.1",
|
|
73
74
|
"puppeteer-core": "^24.40.0",
|