Skip to content

feat(pptx): patch-mode saves + IndexedDB-backed source persistence#49

Open
karthikmudunuri wants to merge 2 commits into
mainfrom
karthikmudunuri/patch-mode-and-idb-source
Open

feat(pptx): patch-mode saves + IndexedDB-backed source persistence#49
karthikmudunuri wants to merge 2 commits into
mainfrom
karthikmudunuri/patch-mode-and-idb-source

Conversation

@karthikmudunuri
Copy link
Copy Markdown
Member

The user complaint

After 1.12.1 the user re-uploaded KBC-More_sample_slides.pptx, edited a small text, exported, and the save dropped:

  • 1 / 2 slide masters
  • 1 / 50 slide layouts
  • 1 / 3 themes
  • 18 / 25 media files

Plus the edited text element lost its themed colors and brand fonts — pptxgenjs flattens <a:schemeClr> to inline <a:srgbClr> and drops <a:latin> typeface refs.

Two distinct root causes, two fixes.

1. Patch-mode saves (Univer-inspired)

When the user edits a text or drags a shape, the old path regenerates the entire <p:sp> via pptxgenjs — which only knows a fraction of the OOXML schema, so themed colors, brand fonts, custGeom paths, gradient fills, body padding, effects, autofit hints all drop on the floor.

tryPatchEditedElement now splices the edit into the source XML in place:

  • Text content — splice the new text into <p:txBody>, preserving the first paragraph's <a:pPr> (alignment, bullets, indent) and the first run's <a:rPr> (themed colors, fonts, bold/italic/underline/strike). Multi-line text becomes multi-paragraph; mixed-style runs still fall back to pptxgenjs.
  • Geometry — replace <a:xfrm> (or <p:xfrm> on <p:graphicFrame>) and keep everything else. Works for <p:sp>, <p:pic>, <p:cxnSp>, <p:graphicFrame>.
  • Placeholder-inherited shapes — now registered too. Patch-mode always splices geometry into the patched output, so themed styling on title / body / content placeholders survives text edits.

pptxgenjs remains the fallback for unpatchable cases (newly added elements, color/font picker edits, mixed-style run restyling, shape kind changes). Architecture matches Univer's "edit the source doc tree, never round-trip through a lossy intermediate model."

2. IndexedDB-backed source persistence

Even with patch-mode, the chrome / EMF / slide-bg preservation pipeline still needs the source PPTX bytes at save time. PR #47 introduced an enumerable Deck.sourcePptxId plus a module-level cache so the bytes survive structuredClone and reducer spreads — but the cache was in-memory only, so a full page reload (host loads deck from localStorage, fresh module instance) emptied it.

parsePptx now mirrors source bytes to IndexedDB keyed by sourcePptxId. serializeDeck's source resolution walks: explicit options.source → in-memory cache → IndexedDB → legacy non-enumerable attachment. The IDB layer is best-effort (private mode / quota exceeded gracefully no-op) and falls back to nothing in SSR / Node environments.

Bonus: Export topbar now downloads .pptx

The package's <TopBar.Export> button used to download a .slidewise.json dump when the host didn't register onExport. It now calls serializeDeck directly and downloads a real .pptx, so hosts can verify the full edit → save round trip without wiring onExport at all (and so I can test the dev server locally with one click).

Validation

End-to-end against KBC-More_sample_slides.pptx with parsePptx → structuredClone + spread → serializeDeck(deck) and no source passed:

Source Before this PR After
Slide masters 2 1 2
Slide layouts 50 1 50
Themes 3 1 3
Media 25 18 27 (25 + 2 from pptxgenjs) ✅
Slides with content 6/6 6/6 6/6 ✅

New regression tests in patch-mode.test.ts: edit text on eon-deck slide 10 column 2 → asserts the source <a:schemeClr val="accent1"/> fill AND the <a:schemeClr val="bg1"/> text color both survive verbatim. Geometry drag test asserts the same theme refs survive after an x-coordinate change.

pnpm typecheck clean. pnpm --filter @textcortex/slidewise test → 41/41 passing.

Test plan

  • Typecheck + tests green locally
  • End-to-end script on KBC reproduces full chrome preservation
  • Regression test for theme refs on text edit (eon-deck)
  • Regression test for theme refs on geometry edit (eon-deck)
  • Reviewer: open KBC-More_sample_slides.pptx → edit a text → export → verify in PowerPoint that the 50 layouts and 3 themes are intact

Changeset

Minor bump on @textcortex/slidewise (1.12.1 → 1.13.0). New optional Deck.sourcePptxId field is additive; patch-mode kicks in automatically, no API changes.

Known not-yet-patched

  • Multi-run text edits with mixed styling — still fall through to pptxgenjs. The contentEditable surface collapses heterogeneous runs to a flat runs array which the patch path can't yet faithfully serialize. Follow-up: rebuild <p:txBody> from el.runs while preserving paragraph-level <a:pPr>.
  • Color / font / weight picker changes — not yet a patch category. They go through pptxgenjs. Adding color and fontFamily to the patch field set is straightforward; will follow once the contentEditable run handling is sorted.
  • 4:3 source chrome preservation — still skipped by the aspect-ratio guard. Needs the deeper inverse-fit refactor.

Two changes that make edits feel like editing the real PowerPoint, not
regenerating from a stripped model.

1. Patch-mode saves: edited elements get their source <p:sp> spliced in
   place instead of regenerated via pptxgenjs, so themed colors / brand
   fonts / gradient fills / custGeom / effects / autofit / body padding
   on the unchanged parts of the element survive verbatim. Covers text
   content edits (splice <p:txBody> preserving first paragraph's pPr
   and first run's rPr) and geometry edits (splice <a:xfrm> or <p:xfrm>
   for <p:graphicFrame>). pptxgenjs remains the fallback for
   unpatchable cases. Placeholder-inherited shapes (no explicit xfrm in
   source) are now registered too — patch-mode always splices geometry
   into the patched output for them.

2. IndexedDB-backed source persistence: parsePptx mirrors source bytes
   to IndexedDB keyed by deck.sourcePptxId; serializeDeck reads through
   in-memory cache → IndexedDB → non-enumerable attachment → explicit
   options.source. The chrome / EMF / slide-bg preservation pipeline
   now survives page reloads on its own — host apps that persist deck
   JSON to localStorage no longer need to re-attach source bytes.

Also: Export topbar button falls back to a real .pptx download (via
serializeDeck) instead of a .slidewise.json dump when the host doesn't
register an onExport callback. Makes local round-trip testing trivial.

Validated end-to-end on KBC-More_sample_slides.pptx: after
parsePptx → structuredClone + spread → serializeDeck(deck) with no
options.source, the saved zip retains all 2 masters, 50 layouts, and 3
themes versus the 1/1/1 the previous build produced. New regression
tests in patch-mode.test.ts and slide10-bg.test.ts cover the theme-ref
and geometry-patch paths.
@karthikmudunuri karthikmudunuri force-pushed the karthikmudunuri/patch-mode-and-idb-source branch from 4e621f3 to 255f2f7 Compare May 15, 2026 09:50
Three things were dropping shapes in the editor's saved output after the
patch-mode landing:

1. Self-closing <p:spPr/> bug. The geometry-injection regex
   `<p:spPr\b[^>]*>` matched both `<p:spPr>` AND `<p:spPr/>` (the `/`
   sits inside `[^>]*`), so for placeholder-only shapes whose source
   spPr was empty/self-closing, we emitted `<p:spPr/><a:xfrm…/>` —
   xfrm OUTSIDE the spPr container, invalid OOXML, and PowerPoint
   silently dropped the shape. Reorder the regex chain so the
   self-closing form is matched FIRST and converted to an open/close
   pair with xfrm INSIDE.

2. Add a final structural sanity check after any patch — count opening
   vs closing tags for p:sp / p:pic / p:graphicFrame / p:spPr /
   p:txBody / a:p / a:r / a:t / a:xfrm. If they don't balance, or the
   shape container count drifts from the source, fall back to
   pptxgenjs's lossy emitter instead of shipping broken XML.

3. Regression test edits every text element on eon-deck slide 10
   (placeholder-only spPrs across the board) and asserts the saved
   slide has no `<p:spPr/><a:xfrm` pattern and that p:spPr opening /
   closing counts balance.
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