@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.
@@ -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(target.querySelectorAll("img")).map(async (img) => {
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
- const blob: Blob = await domToPptx.exportToPptx(target, {
520
- fileName: `slide-${index + 1}.pptx`,
521
- skipDownload: true,
522
- svgAsVector: false,
523
- autoEmbedFonts,
524
- width: 10,
525
- height: 5.625,
526
- })
527
- return Array.from(new Uint8Array(await blob.arrayBuffer()))
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: `Deck ready. Exporting ${slides.length} slide(s) to editable PPTX parts...`,
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.6",
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",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "revela",
3
- "version": "0.19.6",
3
+ "version": "0.19.7",
4
4
  "description": "Use Revela in Codex to specify, research, plan, make, and export trusted narrative decision artifacts.",
5
5
  "author": {
6
6
  "name": "cyber-dash-tech",