Skip to content

feat(pptx): applyEdits — lossless surgical-edit API#95

Merged
karthikmudunuri merged 2 commits into
mainfrom
karthikmudunuri/work-on-plan
Jun 12, 2026
Merged

feat(pptx): applyEdits — lossless surgical-edit API#95
karthikmudunuri merged 2 commits into
mainfrom
karthikmudunuri/work-on-plan

Conversation

@karthikmudunuri

Copy link
Copy Markdown
Member

What

Implements applyEdits(source, plan, options?) — a lossless surgical-edit API that patches the original .pptx bytes instead of re-serializing a full deck. Everything not named by an edit comes out byte-identical to the source (masters, layouts, theme, embedded fonts, ppt/tags/*, notes, embeddings, and any untouched element), and the result opens in PowerPoint with no repair.

This removes the lossy round-trip that produced the custGeom / SVG-fallback / dangling-rel fidelity bugs and lets hosts drop their defensive cleanup. serializeDeck stays for the live editor and from-scratch decks; applyEdits is the lossless path for template-derived output.

API

const deck = await parsePptx(source);          // address elements by deck ids
const out: Uint8Array = await applyEdits(source, {
  title: "Q3 Results",
  slides: [
    { source: { slideIndex: 1 }, edits: [{ op: "setText", elementId: titleId, text: "Q3 Results" }] },
    { source: { slideIndex: 3 }, edits: [
        { op: "setChartData", elementId: chartId, categories: ["Jan","Feb","Mar"], series: [{ name: "Revenue", values: [10,20,30] }] },
        { op: "removeElement", elementId: sampleChartId },
    ] },
    { source: { slideIndex: 4 }, edits: [] },   // kept byte-identical
  ],
}, { onWarning: (w) => notifyHost(w.message) });

Elements are addressed by the same stable ids parsePptx returns; slides by 1-based template index. Call applyEdits in the same process as the parsePptx that produced the plan.

Ops supported

setText / clearText (preserve the template box + first-run styling, or rebuild from supplied runs), setChartData (in-place native-chart fill keeping type/colours and regenerating a consistent embedded .xlsx so Edit-Data still works), setTableData, setImage, removeElement, addChart, addDiagram, plus per-slide background and deck title.

How it works

  • New elementLocationRegistry in the parser (getElementLocation) maps every element id → its verbatim source XML block, with no placeholder filtering (the existing elementSourceRegistry skips no-xfrm placeholders, which would break setText on titles).
  • Output slides are built in plan order; reorder is just <p:sldIdLst> order. A repeated source slide is deep-cloned (slide XML + rels + editable deps) from a pristine view, so edits to copies never bleed.
  • Slides the plan drops, plus any parts that become exclusive to them, are reclaimed by a package-wide reachability sweep; then reconcileDanglingRels + content-type pruning run as backstops.
  • Unresolved element ids and unsupported layout-instantiation surface via onWarning instead of throwing.

Scope note: source: { layoutId } (layout-instantiation from scratch) is intentionally not in the lossless patch path — that's serializeDeck's job. applyEdits emits a layout-unresolved warning and continues rather than shipping a wrong slide.

Tests

New apply-edits.test.ts (11 tests) on a synthetic 4-slide template — no branded fixture needed — including the spec's acceptance test: edits text on 3 slides, fills one chart via setChartData, removes one sample chart, leaves slide 4 untouched, then asserts slide 4 + its media are byte-identical, the chart's embedded workbook reflects the new data, the package keeps its root rels / content types, and there are zero dangling rels.

Two real bugs were caught and fixed while writing tests: an rPr capture regex that stopped at a self-closing child element (corrupting run styling), and a GC step that deleted the mandatory _rels/.rels (invalid package, masked because tests only checked surviving rels) — now covered by an explicit assertion.

Verification

  • pnpm typecheck
  • pnpm test ✅ (169 passed, 9 skipped — the skips are branded-fixture-gated)
  • pnpm --filter @textcortex/slidewise build:lib

Note: pnpm lint can't run in this workspace (@eslint/js isn't installed at the repo root — pre-existing), so static checking relied on tsc.

Minor changeset included.

Add applyEdits(source, plan, options?): a patch on the original .pptx bytes
rather than a full re-serialize. Everything not named by an edit comes out
byte-identical to the source (masters, layouts, theme, fonts, tags, notes,
embeddings, untouched elements); the result opens in PowerPoint with no repair.

- New elementLocationRegistry in the parser (getElementLocation) maps every
  element id -> its verbatim source XML block, with no placeholder filtering.
- Ops: setText/clearText, setChartData (in-place chart fill keeping type/colour
  + a regenerated, consistent embedded xlsx so Edit-Data works), setTableData,
  setImage, removeElement, addChart, addDiagram, per-slide background, title.
- Slide subset/reorder/repeat from the template; repeats deep-clone from a
  pristine view so edits never bleed across copies.
- Removed slides + their exclusive parts reclaimed by a reachability sweep,
  then reconcileDanglingRels + content-type pruning.
- Unresolved ids / unsupported layout-instantiation surface via onWarning.

Tests on a synthetic 4-slide template incl. the acceptance test (untouched
slide byte-identical, chart workbook updated, zero dangling rels).
…on-plan

# Conflicts:
#	packages/slidewise/src/index.ts
@karthikmudunuri karthikmudunuri merged commit e56ddd2 into main Jun 12, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant