Add PPTX best-effort import to slides (parser + UI)#243
Conversation
This is PR2 of the slides-themes-layouts-import design — accept a .pptx archive and produce a SlidesDocument that the editor can open. The parser is a pure-TS, client-side pipeline under packages/slides/src/import/pptx/. It reuses jszip (also used by the docs DOCX importer) for unzip, and the host's DOMParser for XML parsing — Node consumers (CLI) polyfill via @xmldom/xmldom the same way the docs importer expects. To keep the slides package usable from the CLI / backend / SSR, the build now emits a `./node` subpath entry alongside the browser entry, mirroring @wafflebase/docs. Mapping highlights: - 13 distinct prstGeom kinds in the benchmark deck all land on the existing 117-kind ShapeKind registry; unknown prsts fall back to rect with a counted report entry. - <p:cxnSp> straight/curved connectors map to first-class ConnectorElement with attached endpoints resolved via a per-slide shape-id map. - <p:grpSp> groups are flattened via an affine transform composition (depth-recursive); 48 groups in the benchmark collapse cleanly. - <p:graphicFrame> tables flatten to cell TextElements + a single border rect per cell (uniform stroke approximation; merges counted but not honored in v1). - <a:highlight> and <a:hlinkClick> land on Inline.style.backgroundColor / href (already supported in @wafflebase/docs). - <a:normAutofit fontScale> is applied as a one-shot pre-scale to each run's fontSize — acceptable lossy fallback per the design. - <a:outerShdw> drops with a report bump (shape effects are out of the v1 model). - EMU↔px is derived from the deck's own <p:sldSz> so standard 10″×5.625″ decks (like the Yorkie 캐즘 benchmark) render without aspect distortion. Validated against the 36-slide Yorkie 캐즘 deck: 480 elements imported (text 208, shape 158, connector 51, image 63), 48 groups flattened, 7 tables flattened, 3 unknown shapes folded to rect — all matching the static OOXML analysis. Frontend import UI (Task 5) and CLI `slides import` (Task 6) layer on top of this in follow-up commits; the importer is already exported from both the browser and `./node` entries. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Adds the "Import PPTX" entry to the deck list's "+ New" dropdown and
hooks the just-shipped parser into the docs-mirror import flow:
- `pickAndImportPptx()` opens the file picker, runs `importPptx`,
and uploads embedded images through the same workspace `/images`
endpoint that the DOCX importer uses (with a `Uint8Array → Blob`
adapter).
- A slides-side pending-import registry mirrors the docs one. The
deck list creates an empty backend document, stashes the parsed
deck by id, then navigates to `/p/:id`.
- `SlidesView` consumes the pending entry before `ensureSlidesRoot`
runs, writing meta/themes/masters/layouts/slides directly into the
Yorkie root in a single `doc.update`. On thrown updates the entry
stays put so the next mount can retry.
Also fixes a font-size inheritance bug surfaced by the benchmark deck:
title runs whose `<a:rPr>` carries no `sz` need to fall back to the
layout placeholder's default. The slide parser now reads every layout
placeholder's `<a:lstStyle><a:lvl1pPr><a:defRPr sz>` into a
`Map<"{ooxmlType}:{idx}", fontSize>` and threads it through to the
text parser. The benchmark's slide-1 title now renders at 52pt
(matching the source) instead of collapsing to the docs renderer's
11pt fallback. When the layout map misses a key, a hardcoded
per-PlaceholderType table (title 36, subtitle 24, body 18, caption 14,
big-number 96) keeps non-templated decks usable.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
|
Warning Rate limit exceeded
You’ve run out of usage credits. Purchase more in the billing tab. ⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (4)
📝 WalkthroughWalkthroughAdds a client-side PPTX importer: design/docs and task pages, frontend file picker and staging, JSZip-based unzip and XML helpers, OOXML parsers for theme/master/layout/slide/shape/text/image/table, import reporting, fixtures/tests, and package export/build updates. ChangesPPTX Import Feature
Estimated code review effort🎯 4 (Complex) | ⏱️ ~75 minutes Possibly related PRs
✨ Finishing Touches🧪 Generate unit tests (beta)
|
Verification: verify:selfResult: ✅ PASS in 189.9s
Verification: verify:integrationResult: ✅ PASS |
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
There was a problem hiding this comment.
Actionable comments posted: 16
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
packages/frontend/src/app/documents/document-list.tsx (1)
485-526:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winImport PPTX missing from empty-state dropdown.
The "Import PPTX" option is available in the main "New" dropdown (lines 415-421) but is absent from the empty-state dropdown shown when no documents exist. This creates an inconsistent user experience—users with an empty workspace cannot import PPTX files.
📋 Proposed fix to add Import PPTX to empty state
<DropdownMenuItem disabled={importing} onClick={handleImportDocx} > <FileDown className="mr-2 h-4 w-4 text-blue-500" /> Import DOCX </DropdownMenuItem> + <DropdownMenuItem + disabled={importing} + onClick={handleImportPptx} + > + <FileDown className="mr-2 h-4 w-4 text-orange-500" /> + Import PPTX + </DropdownMenuItem> </DropdownMenuContent>🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/frontend/src/app/documents/document-list.tsx` around lines 485 - 526, Add an "Import PPTX" DropdownMenuItem to the empty-state DropdownMenuContent so the empty-state matches the main "New" dropdown: insert a DropdownMenuItem sibling (like the existing Import DOCX item) with disabled={importing} and onClick={handleImportPptx} that renders the appropriate icon and label "Import PPTX"; ensure the handler name handleImportPptx exists/imported and the disabled prop uses the same importing flag as the Import DOCX item so behavior is consistent with createDocumentMutation calls for other items.
🧹 Nitpick comments (10)
packages/slides/src/import/pptx/master.test.ts (1)
36-40: ⚡ Quick winAdd a regression test ensuring parsed masters do not share mutable style references.
Create two
parseMaster(...)results, mutate oneplaceholderStyles.title.fontSize, and assert the other remains unchanged.As per coding guidelines, "Write tests for critical business logic and edge cases".
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/slides/src/import/pptx/master.test.ts` around lines 36 - 40, Add a regression test that checks parseMaster returns independent style objects: call parseMaster twice (e.g., m1 = parseMaster(MIN_MASTER, 'm', 't') and m2 = parseMaster(MIN_MASTER, 'm', 't')), mutate m1.placeholderStyles.title.fontSize to a new value, and assert that m2.placeholderStyles.title.fontSize remains the original value; use parseMaster and the placeholderStyles.title.fontSize property to locate the code under test and ensure the test name describes the non-sharing guarantee.packages/slides/src/import/pptx/font.test.ts (1)
10-41: ⚡ Quick winAdd edge-case tests for whitespace typeface and extended Hangul blocks.
Current tests miss two parser edges:
typeface=" "and Hangul Jamo Extended-A/B detection. Adding those prevents regression in fallback/font resolution behavior.As per coding guidelines, "Write tests for critical business logic and edge cases".
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/slides/src/import/pptx/font.test.ts` around lines 10 - 41, Add two edge-case tests: for parsePrimaryTypeface (used in parsePrimaryTypeface and fontEl) add a case where the typeface attribute contains only whitespace (e.g. typeface=" ") and assert it returns undefined, and for containsHangul add strings that include Hangul Jamo Extended-A and Extended-B characters (characters from the Hangul Jamo Extended-A/B blocks) and assert they return true; update the existing test suite blocks for parsePrimaryTypeface and containsHangul to include these assertions so the parser treats whitespace-only typeface as empty and the Hangul detector recognizes extended Jamo ranges.packages/slides/src/import/pptx/text.test.ts (1)
46-56: ⚡ Quick winAdd a regression test for unsafe hyperlink schemes.
Please add a case like
javascript:alert(1)to assert it does not populateinline.style.href.As per coding guidelines, "Write tests for critical business logic and edge cases".
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/slides/src/import/pptx/text.test.ts` around lines 46 - 56, Add a regression test that ensures unsafe hyperlink schemes (e.g., "javascript:alert(1)") are ignored: create a test (or extend the existing "resolves hyperlink rids via the per-slide rels map") that supplies a rel entry with target "javascript:alert(1)" (using PptxRel), parse the txBody via parseTextBody (with txBody helper and new ImportReport()), then assert that the resulting inline.style.href is not populated (undefined or falsy) for that inline; reference parseTextBody, txBody, PptxRel, ImportReport, and inline.style.href to locate where to add the check.packages/slides/src/import/pptx/index.ts (2)
158-166: ⚡ Quick winSimplify helper with Array.find for clarity.
The
pickRelTargetfunction manually iterates and returns. UsingArray.findwould be more idiomatic and concise.♻️ Proposed refactor
function pickRelTarget( rels: Map<string, { type: string; target: string }>, type: string, ): string | undefined { - for (const rel of rels.values()) { - if (rel.type === type) return rel.target; - } - return undefined; + return Array.from(rels.values()).find((rel) => rel.type === type)?.target; }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/slides/src/import/pptx/index.ts` around lines 158 - 166, The pickRelTarget helper currently loops over rels.values() manually; simplify it by using Array.prototype.find to locate the first relation whose type matches the provided type and return its target (or undefined). Update the pickRelTarget function to call Array.from(rels.values()).find(r => r.type === type) and then return the found?.target, keeping the same signature and behavior.
86-86: 💤 Low valueType assertion complexity could be simplified.
The inline type assertion for the empty fallback
as Layout[], layoutMap: new Map() as LayoutPathToInfois verbose. Consider defining the return type explicitly or extracting a helper constant.♻️ Proposed simplification
+ const EMPTY_MASTER_RESULT = { + master: undefined, + layouts: [] as Layout[], + layoutMap: new Map() as LayoutPathToInfo + }; + const masterAndLayouts = masterTarget ? await loadMasterAndLayouts( archive, 'ppt/presentation.xml', masterTarget, importedTheme?.id ?? 'default-light', report, ) - : { master: undefined, layouts: [] as Layout[], layoutMap: new Map() as LayoutPathToInfo }; + : EMPTY_MASTER_RESULT;🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/slides/src/import/pptx/index.ts` at line 86, The inline type assertions on the fallback object (layouts: [] as Layout[], layoutMap: new Map() as LayoutPathToInfo) are verbose—replace them by declaring an explicitly typed constant or by annotating the function/variable return type so the compiler infers types without inline casts; e.g., introduce a named constant like emptyLayoutInfo typed to the object shape using the Layout and LayoutPathToInfo types (with layoutMap typed as LayoutPathToInfo and layouts as Layout[]) and use that constant instead of the inline as-casts where the current fallback object is returned/used.packages/slides/src/import/pptx/slide.ts (2)
146-151: ⚡ Quick winDuplicate helper function across multiple files.
The
relsSiblingForfunction appears identically in bothslide.ts(lines 146-151) andindex.ts(lines 219-224). Consider extracting this into a shared utility module to maintain a single source of truth.♻️ Suggested refactor to share the helper
Create a new file
packages/slides/src/import/pptx/path-utils.ts:/** * Compute the sibling `_rels` path for a given PPTX part path. * Example: `ppt/slides/slide1.xml` → `ppt/slides/_rels/slide1.xml.rels` */ export function relsSiblingFor(partPath: string): string { const slash = partPath.lastIndexOf('/'); const dir = slash >= 0 ? partPath.slice(0, slash) : ''; const file = slash >= 0 ? partPath.slice(slash + 1) : partPath; return (dir ? dir + '/' : '') + '_rels/' + file + '.rels'; }Then import from both files:
import { relsSiblingFor } from './path-utils';🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/slides/src/import/pptx/slide.ts` around lines 146 - 151, The helper relsSiblingFor is duplicated in slide.ts and index.ts; extract it into a single shared utility (e.g., create packages/slides/src/import/pptx/path-utils.ts exporting function relsSiblingFor(partPath: string)) and replace the local implementations in both slide.ts and index.ts with an import { relsSiblingFor } from './path-utils'; ensuring the exported function signature and behavior remain identical so callers (relsSiblingFor) continue to work unchanged.
134-142: ⚡ Quick winConsider using a for-of loop for better readability.
The manual iteration over
childNodesusing an index-based loop could be simplified with afor-ofloop combined with type guards, improving readability while maintaining the same logic.♻️ Proposed refactor
- for (let i = 0; i < spTree.childNodes.length; i++) { - const n = spTree.childNodes[i]; - if (n.nodeType !== 1) continue; - const el = n as Element; + for (const node of Array.from(spTree.childNodes)) { + if (node.nodeType !== 1) continue; + const el = node as Element; if (el.localName !== 'sp') continue;🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/slides/src/import/pptx/slide.ts` around lines 134 - 142, Replace the index-based loop over spTree.childNodes with a for-of loop to improve readability: iterate each node in spTree.childNodes, skip non-element nodes (nodeType !== 1), cast to Element (or use a type guard), check el.localName === 'sp', get txBody via child(el, 'txBody'), continue if absent, and then return parseTextBody(txBody, { report }); keep the exact logic and early returns intact and reference the existing symbols spTree, child, txBody, and parseTextBody.packages/frontend/src/app/slides/slides-view.tsx (1)
147-148: ⚡ Quick winConsider a typed conversion helper for pending import assignment to avoid double-casting.
The double-cast pattern
as unknown as YorkieSlidesRoot["layouts"](lines 147-148) reflects a genuine type incompatibility:SlidesDocument.layoutsincludes fields likemasterIdandstaticElementsthatYorkieLayoutomits. The cast works because these fields are preserved during JSON serialization and reconstructed bymigrateDocument()on read, but the rawas unknownmakes the type relationship opaque.Rather than adjusting
YorkieSlidesRoot(which would break the Yorkie storage schema), add a typed conversion function liketoYorkieLayout()that explicitly documents the lossy transformation and ensures type safety at the call site.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/frontend/src/app/slides/slides-view.tsx` around lines 147 - 148, The double-cast on pending.layouts/pending.slides hides a lossy transformation between SlidesDocument.layouts and YorkieSlidesRoot["layouts"]; add a typed conversion helper (e.g., toYorkieLayout(item: SlidesDocumentLayout): YorkieLayout) that explicitly maps/removes fields like masterId and staticElements and documents the loss, call this helper to convert pending.layouts and pending.slides before assigning to r.layouts / r.slides, and keep migrateDocument() noted in a comment as the complementary reconstruction step on read.packages/slides/src/import/pptx/group.test.ts (1)
19-82: ⚡ Quick winAdd a rotated-group test case.
Please add at least one case where
<a:xfrm rot="...">is set on<p:grpSp>and verify transformed childx/y(not onlyrotation). This is a critical geometry edge case.As per coding guidelines, "Write tests for critical business logic and edge cases".
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/slides/src/import/pptx/group.test.ts` around lines 19 - 82, Add a new unit test that covers a rotated group by creating a grpSp XML with <a:xfrm rot="..."> (use grpSp helper), call composeGroupTransform(IDENTITY_TRANSFORM, grp, SCALE) and then applyGroupTransform(local, t) for a child with known local x/y/w/h; assert the resulting world.x and world.y (and optionally w/h and rotation) using toBeCloseTo so you verify the rotation is applied to positions, not just the rotation field. Ensure the test name describes "rotated-group" and follows the existing pattern used in the other tests.packages/slides/src/import/pptx/image.test.ts (1)
28-107: ⚡ Quick winAdd a test for
uploadImagerejection path.Please add a case where
uploadImagethrows and assertparsePicreturnsundefinedwithreport.skippedImagesincremented.As per coding guidelines, "Write tests for critical business logic and edge cases".
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/slides/src/import/pptx/image.test.ts` around lines 28 - 107, Add a test in packages/slides/src/import/pptx/image.test.ts that exercises parsePic’s upload failure path: call parsePic with a valid archive (fakeArchive with 'ppt/media/image1.png'), rels containing rId3, and provide uploadImage as an async function that throws/rejects; assert the returned result is undefined and that the ImportReport instance (report.skippedImages) was incremented by 1. Reference parsePic, uploadImage, ImportReport, fakeArchive and picEl(PIC) when locating where to add the new it(...) block and make uploadImage throw asynchronously to simulate the failure.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@packages/frontend/src/app/slides/slides-view.tsx`:
- Around line 151-153: The catch block that logs "Failed to apply pending PPTX
import" in slides-view.tsx silently swallows import errors; update the error
handling in the catch inside the function that applies the pending PPTX import
(the try/catch around the PPTX import logic in SlidesView) to surface a
user-facing error (e.g., call the app's existing toast/notification API or set
an error state that renders a visible alert/modal) and ensure the UI does not
proceed with an empty slides root without explaining the failure; include the
original error message/details in the notification and consider keeping the
original console.error for debugging while preventing the app from initializing
silently incorrect state.
In `@packages/slides/src/import/pptx/color.ts`:
- Around line 7-10: The code currently assumes identity mapping between scheme
roles and aliases (e.g., bg1/tx1 ↔ dk1/lt1) and ignores the master clrMap;
update the color resolution to consult the master clrMap and apply any remapping
before looking up scheme slots: locate the color resolution logic that
references clrMap or resolves roles (e.g., the function that maps scheme roles
like "bg1","tx1","dk1","lt1") and change it to first check for clrMap entries
for the given role/alias, translate the role via clrMap when present, then
resolve the resulting role to the scheme color; ensure both
WordprocessingML-style aliases and original role names are handled through that
single remapping step so imported text/background/hyperlink colors use the
remapped slots.
In `@packages/slides/src/import/pptx/font.ts`:
- Around line 17-19: The code currently returns the raw value from attr(latin,
'typeface') which allows whitespace-only strings; update the logic around the
face variable to trim whitespace before returning so whitespace-only values are
treated as unset — e.g., call .trim() on the result of attr(latin, 'typeface')
and if the trimmed value is empty return undefined, otherwise return the trimmed
string; reference the face variable and the attr(latin, 'typeface') call to
locate where to change.
- Around line 27-29: The Hangul detection currently checks ranges using the
local variable code but misses Hangul Jamo Extended-A and Extended-B; update the
condition that checks (code >= 0xac00 && code <= 0xd7a3) || (code >= 0x1100 &&
code <= 0x11ff) || (code >= 0x3130 && code <= 0x318f) to also include (code >=
0xA960 && code <= 0xA97F) and (code >= 0xD7B0 && code <= 0xD7FF) so the Korean
fallback covers Jamo Extended-A and Extended-B; locate and modify the boolean
expression that uses the variable named code in
packages/slides/src/import/pptx/font.ts to add those two ranges.
In `@packages/slides/src/import/pptx/geometry.ts`:
- Around line 17-21: Validate slideSizeEmu.cx and slideSizeEmu.cy in emuScale:
if either is 0, NaN, or not finite, avoid computing 1920/0 etc.; either throw a
clear, meaningful Error (e.g., "emuScale: invalid slideSizeEmu.cx/cy — must be
finite non-zero numbers") or substitute slideSizeEmu with DEFAULT_WIDESCREEN_EMU
before computing and then return the EmuScale; update emuScale to perform this
guard and use the validated values when producing sx and sy.
In `@packages/slides/src/import/pptx/group.ts`:
- Around line 89-97: applyGroupTransform currently accumulates rotation in the
returned Frame.rotation but does not rotate the child x/y around the group's
origin, so children are misplaced when the group has rotation; update
applyGroupTransform to first compute the local offset (dx = frame.x -
t.childBaseX, dy = frame.y - t.childBaseY), rotate that vector by t.rotation
(convert to radians if rotations are stored in degrees), then apply scaling and
add t.parentOffX/t.parentOffY to produce world x/y, and keep rotation =
frame.rotation + t.rotation; also consider adjusting w/h by projecting the
rotated/scaled bbox (or at minimum document a TODO) so width/height reflect
rotation-aware bounds.
In `@packages/slides/src/import/pptx/image.ts`:
- Line 70: Wrap the call to ctx.uploadImage(bytes, mime) in a try/catch so an
upload failure becomes a soft skip instead of throwing: on error log/record the
failure into the import report (include the image identifier, mime, and error
message) and return/continue as a skipped image (e.g., leave src null or a
placeholder) so processing continues; ensure you update whatever report object
is used in this module rather than rethrowing the exception and that callers of
the function handle a skipped/null src accordingly.
In `@packages/slides/src/import/pptx/master.ts`:
- Line 21: The background assignment uses a shallow spread so nested objects
like fill remain shared; update the background creation in the master module so
you deep-copy the defaults and parsed result (e.g. clone
DEFAULT_MASTER.background and the object returned from parseBackground) instead
of using { ...DEFAULT_MASTER.background } or shallow spreads; ensure the same
change is applied to the other occurrence noted (the second shallow spread on
line 43) so that nested fill objects are not shared between masters (refer to
background, parseBackground, and DEFAULT_MASTER.background to locate the spots
to fix).
- Line 27: The placeholderStyles property is being shallow-copied from
DEFAULT_MASTER (placeholderStyles: { ...DEFAULT_MASTER.placeholderStyles }),
which leaves nested style objects shared and mutable across masters; change this
to create a deep copy of DEFAULT_MASTER.placeholderStyles (e.g., use
structuredClone or a deep-clone utility or map/clone each nested style object)
so all nested style objects are new instances and mutations won't leak between
masters/imports; update the placeholderStyles assignment in master.ts to use
that deep-clone approach referencing DEFAULT_MASTER.placeholderStyles.
In `@packages/slides/src/import/pptx/report.ts`:
- Around line 20-30: The summary() method in report.ts omits the counters
unknownLayoutTypes, tableMergesIgnored, and tableBordersApproximated so those
metrics never appear in the final report; update summary() to push entries for
these counters (similar to existing lines for tablesFlattened, groupsFlattened,
shadowsDropped, textBoxesPreScaled, unknownShapes, skippedImages) using their
names and pluralized labels (e.g., `${this.unknownLayoutTypes} unknown
layout(s)`, `${this.tableMergesIgnored} table merge(s) ignored`,
`${this.tableBordersApproximated} table border(s) approximated`) before the
parts.length check so all defined counters are included in the returned string.
In `@packages/slides/src/import/pptx/shape.ts`:
- Around line 245-251: The current branch returns early when a prstGeom is
found, causing any accompanying txBody text to be dropped; update the logic in
the function that handles <p:sp> (where prstGeom, spPr, child, buildShapeElement
and parseSpElements are used) to emit both a ShapeElement and a TextElement when
both geometry and txBody exist — e.g., call buildShapeElement for the prstGeom
and also parse the txBody (or change parseSpElements to return [shape, text])
and return/appends both elements (preserving elementId, frame, sp, ctx) instead
of returning only the shape.
- Around line 406-409: The stroke width conversion currently uses a fixed
EMU->px factor (w / 9525) which ignores the slide/deck scaling; update the
conversion in shape.ts where w and width are computed to incorporate the
rendering scale (use the canvas/deck scale from ctx, e.g. ctx.scale or the
deck-level scale variable) so the result becomes Math.max(0, (w / 9525) *
deckScale) when w != null, leaving the fallback of 1 unchanged; adjust any
downstream usage of width (and related variables like ln and solidFill) to rely
on this scaled value so strokes match other scaled geometry.
In `@packages/slides/src/import/pptx/table.test.ts`:
- Around line 75-91: Rename the test case title to accurately reflect the
attributes being exercised: change the it(...) description from 'counts merges
via gridSpan/rowSpan/vMerge' to 'counts merges via gridSpan/hMerge' in the test
in table.test.ts; the assert and XML fixture (using gridSpan and hMerge) as well
as references to parseTable(frame(xml), ctx(report)) and
report.tableMergesIgnored should remain unchanged.
In `@packages/slides/src/import/pptx/table.ts`:
- Line 85: The loop advancing x uses a single colWidth (x += colWidth) which
misplaces cells when a cell has gridSpan > 1; change the increment to advance by
the effective span width for that cell (compute spanWidth as the sum of the
column widths covered by the cell or at minimum colWidth * gridSpan) and do x +=
spanWidth; update the code around the cell layout logic in table.ts where x,
colWidth and gridSpan are used (ensure you read gridSpan from the cell and
compute spanWidth before incrementing x).
- Around line 114-117: The border width conversion currently divides wEmu by
9525 (const wEmu = attrInt(ln, 'w'); const width = wEmu != null ? Math.max(0,
wEmu / 9525) : 1;) which bypasses the deck/shape stroke scaling and causes
inconsistent thickness; replace the hardcoded division with the project’s
canonical EMU→px conversion or deck-derived scale used for shape strokes (i.e.,
call the existing emu-to-px helper or apply the same deck scale/transform used
elsewhere for stroke widths) so width = wEmu != null ? Math.max(0,
emuToPx(wEmu)) : 1 (or equivalent using the deck scale), preserving the existing
per-cell approximation comment and variable names (wEmu, width, attrInt, ln).
In `@packages/slides/src/import/pptx/text.ts`:
- Around line 201-207: The code assigns rel.target directly to style.href (in
the block using hlink, rPr, NS.R, ctx.rels and style.href), which can expose
unsafe URI schemes from untrusted PPTX; update the logic to validate the URI
scheme before assignment by parsing rel.target and allowing only safe schemes
(e.g., http, https, mailto, maybe tel) or explicitly rejecting dangerous schemes
(javascript:, data:, vbscript:), and only set style.href when the scheme passes
validation (otherwise skip or null it); ensure validation handles missing
schemes and relative URLs appropriately and use the existing ctx.rels/rid lookup
flow to locate the value before validating.
---
Outside diff comments:
In `@packages/frontend/src/app/documents/document-list.tsx`:
- Around line 485-526: Add an "Import PPTX" DropdownMenuItem to the empty-state
DropdownMenuContent so the empty-state matches the main "New" dropdown: insert a
DropdownMenuItem sibling (like the existing Import DOCX item) with
disabled={importing} and onClick={handleImportPptx} that renders the appropriate
icon and label "Import PPTX"; ensure the handler name handleImportPptx
exists/imported and the disabled prop uses the same importing flag as the Import
DOCX item so behavior is consistent with createDocumentMutation calls for other
items.
---
Nitpick comments:
In `@packages/frontend/src/app/slides/slides-view.tsx`:
- Around line 147-148: The double-cast on pending.layouts/pending.slides hides a
lossy transformation between SlidesDocument.layouts and
YorkieSlidesRoot["layouts"]; add a typed conversion helper (e.g.,
toYorkieLayout(item: SlidesDocumentLayout): YorkieLayout) that explicitly
maps/removes fields like masterId and staticElements and documents the loss,
call this helper to convert pending.layouts and pending.slides before assigning
to r.layouts / r.slides, and keep migrateDocument() noted in a comment as the
complementary reconstruction step on read.
In `@packages/slides/src/import/pptx/font.test.ts`:
- Around line 10-41: Add two edge-case tests: for parsePrimaryTypeface (used in
parsePrimaryTypeface and fontEl) add a case where the typeface attribute
contains only whitespace (e.g. typeface=" ") and assert it returns undefined,
and for containsHangul add strings that include Hangul Jamo Extended-A and
Extended-B characters (characters from the Hangul Jamo Extended-A/B blocks) and
assert they return true; update the existing test suite blocks for
parsePrimaryTypeface and containsHangul to include these assertions so the
parser treats whitespace-only typeface as empty and the Hangul detector
recognizes extended Jamo ranges.
In `@packages/slides/src/import/pptx/group.test.ts`:
- Around line 19-82: Add a new unit test that covers a rotated group by creating
a grpSp XML with <a:xfrm rot="..."> (use grpSp helper), call
composeGroupTransform(IDENTITY_TRANSFORM, grp, SCALE) and then
applyGroupTransform(local, t) for a child with known local x/y/w/h; assert the
resulting world.x and world.y (and optionally w/h and rotation) using
toBeCloseTo so you verify the rotation is applied to positions, not just the
rotation field. Ensure the test name describes "rotated-group" and follows the
existing pattern used in the other tests.
In `@packages/slides/src/import/pptx/image.test.ts`:
- Around line 28-107: Add a test in
packages/slides/src/import/pptx/image.test.ts that exercises parsePic’s upload
failure path: call parsePic with a valid archive (fakeArchive with
'ppt/media/image1.png'), rels containing rId3, and provide uploadImage as an
async function that throws/rejects; assert the returned result is undefined and
that the ImportReport instance (report.skippedImages) was incremented by 1.
Reference parsePic, uploadImage, ImportReport, fakeArchive and picEl(PIC) when
locating where to add the new it(...) block and make uploadImage throw
asynchronously to simulate the failure.
In `@packages/slides/src/import/pptx/index.ts`:
- Around line 158-166: The pickRelTarget helper currently loops over
rels.values() manually; simplify it by using Array.prototype.find to locate the
first relation whose type matches the provided type and return its target (or
undefined). Update the pickRelTarget function to call
Array.from(rels.values()).find(r => r.type === type) and then return the
found?.target, keeping the same signature and behavior.
- Line 86: The inline type assertions on the fallback object (layouts: [] as
Layout[], layoutMap: new Map() as LayoutPathToInfo) are verbose—replace them by
declaring an explicitly typed constant or by annotating the function/variable
return type so the compiler infers types without inline casts; e.g., introduce a
named constant like emptyLayoutInfo typed to the object shape using the Layout
and LayoutPathToInfo types (with layoutMap typed as LayoutPathToInfo and layouts
as Layout[]) and use that constant instead of the inline as-casts where the
current fallback object is returned/used.
In `@packages/slides/src/import/pptx/master.test.ts`:
- Around line 36-40: Add a regression test that checks parseMaster returns
independent style objects: call parseMaster twice (e.g., m1 =
parseMaster(MIN_MASTER, 'm', 't') and m2 = parseMaster(MIN_MASTER, 'm', 't')),
mutate m1.placeholderStyles.title.fontSize to a new value, and assert that
m2.placeholderStyles.title.fontSize remains the original value; use parseMaster
and the placeholderStyles.title.fontSize property to locate the code under test
and ensure the test name describes the non-sharing guarantee.
In `@packages/slides/src/import/pptx/slide.ts`:
- Around line 146-151: The helper relsSiblingFor is duplicated in slide.ts and
index.ts; extract it into a single shared utility (e.g., create
packages/slides/src/import/pptx/path-utils.ts exporting function
relsSiblingFor(partPath: string)) and replace the local implementations in both
slide.ts and index.ts with an import { relsSiblingFor } from './path-utils';
ensuring the exported function signature and behavior remain identical so
callers (relsSiblingFor) continue to work unchanged.
- Around line 134-142: Replace the index-based loop over spTree.childNodes with
a for-of loop to improve readability: iterate each node in spTree.childNodes,
skip non-element nodes (nodeType !== 1), cast to Element (or use a type guard),
check el.localName === 'sp', get txBody via child(el, 'txBody'), continue if
absent, and then return parseTextBody(txBody, { report }); keep the exact logic
and early returns intact and reference the existing symbols spTree, child,
txBody, and parseTextBody.
In `@packages/slides/src/import/pptx/text.test.ts`:
- Around line 46-56: Add a regression test that ensures unsafe hyperlink schemes
(e.g., "javascript:alert(1)") are ignored: create a test (or extend the existing
"resolves hyperlink rids via the per-slide rels map") that supplies a rel entry
with target "javascript:alert(1)" (using PptxRel), parse the txBody via
parseTextBody (with txBody helper and new ImportReport()), then assert that the
resulting inline.style.href is not populated (undefined or falsy) for that
inline; reference parseTextBody, txBody, PptxRel, ImportReport, and
inline.style.href to locate where to add the check.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: a38b6794-f504-4f7d-8769-ef30ddd2d7f3
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (44)
docs/design/slides/slides-themes-layouts-import.mddocs/tasks/README.mddocs/tasks/active/20260515-pptx-import-lessons.mddocs/tasks/active/20260515-pptx-import-todo.mdpackages/frontend/src/app/documents/document-list.tsxpackages/frontend/src/app/slides/pending-imports.tspackages/frontend/src/app/slides/pptx-actions.tspackages/frontend/src/app/slides/slides-view.tsxpackages/slides/package.jsonpackages/slides/src/import/pptx/__fixtures__/build-minimal-pptx.tspackages/slides/src/import/pptx/color.test.tspackages/slides/src/import/pptx/color.tspackages/slides/src/import/pptx/font.test.tspackages/slides/src/import/pptx/font.tspackages/slides/src/import/pptx/geometry.test.tspackages/slides/src/import/pptx/geometry.tspackages/slides/src/import/pptx/group.test.tspackages/slides/src/import/pptx/group.tspackages/slides/src/import/pptx/image.test.tspackages/slides/src/import/pptx/image.tspackages/slides/src/import/pptx/index.test.tspackages/slides/src/import/pptx/index.tspackages/slides/src/import/pptx/layout.test.tspackages/slides/src/import/pptx/layout.tspackages/slides/src/import/pptx/master.test.tspackages/slides/src/import/pptx/master.tspackages/slides/src/import/pptx/rels.test.tspackages/slides/src/import/pptx/rels.tspackages/slides/src/import/pptx/report.tspackages/slides/src/import/pptx/shape.tspackages/slides/src/import/pptx/slide.tspackages/slides/src/import/pptx/table.test.tspackages/slides/src/import/pptx/table.tspackages/slides/src/import/pptx/text.test.tspackages/slides/src/import/pptx/text.tspackages/slides/src/import/pptx/theme.test.tspackages/slides/src/import/pptx/theme.tspackages/slides/src/import/pptx/unzip.test.tspackages/slides/src/import/pptx/unzip.tspackages/slides/src/import/pptx/xml.test.tspackages/slides/src/import/pptx/xml.tspackages/slides/src/index.tspackages/slides/src/node.tspackages/slides/vite.build.ts
| it('counts merges via gridSpan/rowSpan/vMerge', () => { | ||
| const xml = `<p:graphicFrame> | ||
| <p:xfrm><a:off x="0" y="0"/><a:ext cx="2000000" cy="2000000"/></p:xfrm> | ||
| <a:graphic><a:graphicData uri="http://schemas.openxmlformats.org/drawingml/2006/table"> | ||
| <a:tbl> | ||
| <a:tblGrid><a:gridCol w="1000000"/><a:gridCol w="1000000"/></a:tblGrid> | ||
| <a:tr h="1000000"> | ||
| <a:tc gridSpan="2"><a:txBody><a:bodyPr/><a:p><a:r><a:t>spans</a:t></a:r></a:p></a:txBody></a:tc> | ||
| <a:tc hMerge="1"><a:txBody><a:bodyPr/><a:p/></a:txBody></a:tc> | ||
| </a:tr> | ||
| </a:tbl> | ||
| </a:graphicData></a:graphic> | ||
| </p:graphicFrame>`; | ||
| const report = new ImportReport(); | ||
| parseTable(frame(xml), ctx(report)); | ||
| expect(report.tableMergesIgnored).toBe(2); // one gridSpan + one hMerge | ||
| }); |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
What are the correct OOXML table cell merge attribute names in DrawingML?
💡 Result:
In DrawingML (the OOXML drawing framework), table cell merging is controlled by four specific attributes defined on the table cell element (a:tc) [1][2]. These attributes are: gridSpan: Specifies the number of columns that a merged cell spans [1][2]. rowSpan: Specifies the number of rows that a merged cell spans [1][2]. hMerge: A boolean attribute that, when set to true (or 1), indicates the cell is to be merged with the previous horizontal table cell [1][2]. vMerge: A boolean attribute that, when set to true (or 1), indicates the cell is to be merged with the previous vertical table cell [1][2]. These attributes are used in combination to define merged regions [1][2]. Specifically, gridSpan and rowSpan are typically used on the top-left cell of a merged region to define its dimensions, while hMerge and vMerge are used on subsequent cells to indicate they are part of that merged group [1][3][2]. Note: These DrawingML attributes are distinct from the table cell merge mechanisms used in WordprocessingML (w:tc), which often utilize different elements and attributes such as w:vMerge with values like continue or restart [4][5].
Citations:
- 1: https://ooxml.info/docs/21/21.1/21.1.3/21.1.3.16/
- 2: https://c-rex.net/samples/ooxml/e1/Part4/OOXML_P4_DOCX_tc_topic_ID0EML5LB.html
- 3: https://learn.microsoft.com/en-us/openspecs/office_standards/ms-oe376/6c3a6294-ed13-479a-871a-7e094963289e
- 4: https://ooxml.info/docs/17/17.13/17.13.5/17.13.5.3
- 5: http://officeopenxml.com/WPtableCellProperties.php
Update test name to match the attributes actually tested.
The test at line 75 is named 'counts merges via gridSpan/rowSpan/vMerge' but the XML fixture (lines 82–83) uses only gridSpan and hMerge. According to the OOXML specification, rowSpan and vMerge are valid merge attributes, but they are not tested here. The inline comment at line 90 correctly identifies what is being counted: "one gridSpan + one hMerge". Rename the test to accurately reflect the attributes being tested, such as 'counts merges via gridSpan/hMerge'.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/slides/src/import/pptx/table.test.ts` around lines 75 - 91, Rename
the test case title to accurately reflect the attributes being exercised: change
the it(...) description from 'counts merges via gridSpan/rowSpan/vMerge' to
'counts merges via gridSpan/hMerge' in the test in table.test.ts; the assert and
XML fixture (using gridSpan and hMerge) as well as references to
parseTable(frame(xml), ctx(report)) and report.tableMergesIgnored should remain
unchanged.
Code reviewNo issues found. Checked for bugs and CLAUDE.md compliance. 🤖 Generated with Claude Code - If this code review was useful, please react with 👍. Otherwise, react with 👎. |
CodeRabbit raised 16 actionable items across security, correctness,
and fidelity; this fold-up addresses every Major plus the Minors that
overlap. Key fixes:
- Security: text hyperlinks now route through a `isSafeHref` allowlist
(http/https/mailto/relative) and respect `rel.external`. Drops
`javascript:`/`data:`/internal-slide rels — they are XSS vectors
or non-representable in `Inline.style.href`.
- emuScale guards against `cx=0` / NaN by falling back to widescreen
defaults instead of multiplying every frame by Infinity.
- Image uploads soft-skip on failure: a single broken blob bumps
`report.skippedImages` rather than failing the whole deck.
- Stroke width (shape + table cell) now scales with the deck via a
new `emuToStrokePx` helper instead of a fixed 9525 EMU/px.
- Master parsing deep-clones `DEFAULT_MASTER`'s background and
placeholderStyles so per-deck mutations cannot leak across imports.
- `<p:sp>` with both `prstGeom` and visible `<p:txBody>` now emits
both a `ShapeElement` and a coincident `TextElement` — the typical
"rectangle with a label" pattern was previously losing the text.
- Group rotation rotates child centers around the group pivot
instead of just summing the rotation onto each child.
- Master `<p:clrMap>` parsed and threaded through every slide-level
color resolver, so non-identity swaps (the benchmark deck flips
`bg2`/`tx2`) route schemeClr lookups correctly.
- `ImportReport.summary()` now surfaces `unknownLayoutTypes`,
`tableMergesIgnored`, and `tableBordersApproximated`.
- `font.ts` trims whitespace-only typefaces and detects Hangul Jamo
Extended-A/B ranges.
- Pending-import application failures surface a user-visible toast.
- Connector free endpoints honor `flipH`/`flipV` on the source xfrm.
- Table `gridSpan` cells now advance the column index by the full
span so following cells align correctly.
Benchmark deck (`Yorkie, 캐즘 뛰어넘기.pptx`) post-change: 517
elements (was 480; +37 shape-with-text pairs now preserved), slide 1
title still 52pt, no regression in counts.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
There was a problem hiding this comment.
Actionable comments posted: 2
♻️ Duplicate comments (1)
packages/slides/src/import/pptx/slide.ts (1)
57-59:⚠️ Potential issue | 🟠 Major | ⚡ Quick winAvoid shallow-copying
DEFAULT_BACKGROUNDin slide parsing.Line 59 and Line 109 use object spread, which still shares nested
fillreferences. A later mutation of one slide background can leak into defaults/other slides.💡 Proposed fix
+import { clone } from '../../model/clone'; ... const background = bgEl ? parseSlideBackground(bgEl, opts.clrMap) - : { ...DEFAULT_BACKGROUND }; + : clone(DEFAULT_BACKGROUND); ... - return { ...DEFAULT_BACKGROUND }; + return clone(DEFAULT_BACKGROUND);Also applies to: 109-109
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/slides/src/import/pptx/slide.ts` around lines 57 - 59, The code currently shallow-copies DEFAULT_BACKGROUND into the local background (const background = ... ? parseSlideBackground(...) : { ...DEFAULT_BACKGROUND }), which shares nested objects like fill and allows mutations to leak; update the fallback to produce a deep clone of DEFAULT_BACKGROUND (e.g., use structuredClone(DEFAULT_BACKGROUND) or a dedicated deepClone utility) so each slide gets an independent background object, and apply the same change where DEFAULT_BACKGROUND is spread at the other occurrence; ensure parseSlideBackground usage remains unchanged but that any fallback uses the deep-cloned copy.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@packages/slides/src/import/pptx/group.ts`:
- Around line 79-87: The pivot calculation for nested groups incorrectly ignores
the parent's rotation: replace the current direct translate+scale computation of
groupCenterX/groupCenterY -> pivotX/pivotY (which uses
parent.parentOffX/parent.parentOffY, parent.childBaseX/parent.childBaseY,
parent.scaleX/parent.scaleY) with a proper composition of the parent's affine
transform (translate, scale, then rotate) applied to the group's center; i.e.,
compute the group's center in parent-local coords and then rotate/translate it
via the parent's rotation (or build the parent 2D affine matrix and multiply the
center point) before deriving pivotX/pivotY and the related offsets used later
(also update the analogous logic around lines 90-97 that computes child
offsets).
In `@packages/slides/src/import/pptx/slide.ts`:
- Around line 137-147: The loop in parseNotes currently returns the first <p:sp>
with a <p:txBody> (using spTree, child and parseTextBody) which picks
headers/footers; change it to first check the shape's placeholder: inspect
child(el,'nvSpPr') → child(nvSpPr,'nvPr') → child(nvPr,'ph') and only accept
when ph.getAttribute('type') === 'body'; if none found, parse all txBody shapes
with parseTextBody, filter out empty results and return the largest (by text
length) as a fallback. Ensure you still skip non-element nodes and shapes
without txBody.
---
Duplicate comments:
In `@packages/slides/src/import/pptx/slide.ts`:
- Around line 57-59: The code currently shallow-copies DEFAULT_BACKGROUND into
the local background (const background = ... ? parseSlideBackground(...) : {
...DEFAULT_BACKGROUND }), which shares nested objects like fill and allows
mutations to leak; update the fallback to produce a deep clone of
DEFAULT_BACKGROUND (e.g., use structuredClone(DEFAULT_BACKGROUND) or a dedicated
deepClone utility) so each slide gets an independent background object, and
apply the same change where DEFAULT_BACKGROUND is spread at the other
occurrence; ensure parseSlideBackground usage remains unchanged but that any
fallback uses the deep-cloned copy.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: d27b99c0-54fd-4a01-994d-7befad9dbc56
📒 Files selected for processing (20)
packages/frontend/src/app/slides/slides-view.tsxpackages/slides/src/import/pptx/color.test.tspackages/slides/src/import/pptx/color.tspackages/slides/src/import/pptx/font.tspackages/slides/src/import/pptx/geometry.test.tspackages/slides/src/import/pptx/geometry.tspackages/slides/src/import/pptx/group.test.tspackages/slides/src/import/pptx/group.tspackages/slides/src/import/pptx/image.test.tspackages/slides/src/import/pptx/image.tspackages/slides/src/import/pptx/index.tspackages/slides/src/import/pptx/master.test.tspackages/slides/src/import/pptx/master.tspackages/slides/src/import/pptx/report.tspackages/slides/src/import/pptx/shape.tspackages/slides/src/import/pptx/slide.tspackages/slides/src/import/pptx/table.test.tspackages/slides/src/import/pptx/table.tspackages/slides/src/import/pptx/text.test.tspackages/slides/src/import/pptx/text.ts
🚧 Files skipped from review as they are similar to previous changes (11)
- packages/slides/src/import/pptx/font.ts
- packages/frontend/src/app/slides/slides-view.tsx
- packages/slides/src/import/pptx/color.test.ts
- packages/slides/src/import/pptx/table.test.ts
- packages/slides/src/import/pptx/image.ts
- packages/slides/src/import/pptx/text.test.ts
- packages/slides/src/import/pptx/table.ts
- packages/slides/src/import/pptx/index.ts
- packages/slides/src/import/pptx/geometry.test.ts
- packages/slides/src/import/pptx/color.ts
- packages/slides/src/import/pptx/text.ts
Three more items from CodeRabbit's review of the previous fix-up
commit:
- `group.ts` switches from a translate/scale + pivot-rotation
structure to a full 2x3 affine matrix. Nested rotated groups now
compose via standard matrix multiplication, so an inner group's
pivot is computed in the outer's already-rotated frame. The
cumulative rotation is still tracked separately so each child's
`frame.rotation` (rendered as "rotate around own center") adds on
top of the orbital motion baked into the matrix. New helper
`applyGroupTransformToPoint` keeps `shape.ts` from having to know
the matrix layout. Added a depth-2 nested rotation test.
- `slide.ts::parseNotes` now filters notes-slide shapes by
`<p:ph type="body">` and explicitly skips `sldNum` / `hdr` / `dt`
placeholders, with a longest-text fallback for decks that omit
the body type. Previously it returned the first shape with a
`<p:txBody>`, which could surface a slide number / footer as the
speaker notes content.
- `slide.ts` background fallback now deep-clones `DEFAULT_BACKGROUND`
so mutations on one slide cannot leak into the module-level
singleton or other slides. Matches the `master.ts` fix in the
previous commit.
Benchmark deck unchanged at 517 elements / 36 slides / 52pt title;
slide-1 notes now visibly start with the expected greeting line,
which confirms the body-placeholder filter is choosing correctly.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Implements Task 6 of `docs/tasks/active/20260515-pptx-import-todo.md` — the deferred follow-up to #243. Users can now import a `.pptx` via the CLI in addition to the deck list UI: wafflebase slides import deck.pptx --title "My Deck" Why a parallel writer instead of reusing the frontend's pending- import flow: the CLI never attaches a Yorkie client, so the parsed deck has to be persisted through the backend. The existing docs content endpoint already had this shape (REST in, Yorkie write), so the slides side mirrors it. Backend - `packages/backend/src/yorkie/slides-tree.ts` — `writeSlidesRoot` / `readSlidesRoot` for the slides Yorkie root. Plain JSON bulk- assign (slides body is not a Tree CRDT — that experiment was reverted in Phase 5a), matching `slides-view.tsx`'s `doc.update` pattern. - `packages/backend/src/api/v1/docs-content.controller.ts` — `PUT/GET /documents/:id/content` now dispatches on the persisted document type. Slides → `writeSlidesRoot`; docs → existing Tree CRDT path. A `sniffBodyShape` helper picks the validator arm via `Array.isArray(body.slides|blocks)`; mismatched shape/type returns 400 before any Yorkie attach. - Per-slide + per-element shape validation (`id`, `type`, `frame`) so corrupted decks fail with a useful 400 instead of crashing the renderer later. CLI - `packages/cli/src/slides/{pptx-import,import}.ts` — thin `importPptx` wrapper (base64 inline uploader, matches `docs import`) and orchestrator mirroring `runDocsImport`. A `parser` injection point keeps unit tests deterministic without real `.pptx` bytes. - `packages/cli/src/commands/slides.ts` + `bin.ts` wiring. - HttpClient gets `putSlidesContent` and a widened `createDocument` type union. - The user's `--title` is mirrored onto `deck.meta.title` so the Document row, the deck list, and `GET /content` all agree. Tests + CI - 27 backend controller specs (docs + slides dispatch + validation arms), 3 slides-tree round-trip specs, 13 orchestrator specs (new deck / `--replace` / dry-run / failure paths), 4 wrapper specs. - `slides-pptx-import.e2e-spec.ts` exercises the full CLI → API → Yorkie path. Gated by `RUN_DB_INTEGRATION_TESTS` + `RUN_YORKIE_INTEGRATION_TESTS`. The fixture is pre-generated via `packages/cli/scripts/gen-sample-pptx.mjs` and the resulting binary committed under `packages/backend/test/fixtures/` — same workaround the docs CLI uses to dodge ts-jest's strict CJS interop on JSZip. - `verify-integration` builds `@wafflebase/docs` and `@wafflebase/slides` before invoking the e2e suite, since the integration job is a separate runner that doesn't inherit the `dist/` artifacts from `verify-self`. - The CLI subprocess in the e2e resolves on `'close'` (not `'exit'`) so stdout/stderr are fully flushed before assertions. Known follow-ups - Image upload uses base64 inline `data:` URLs (matches `docs import`). Real `/api/v1/.../images` upload is deferred. - `runSlidesImport` (and `runDocsImport`) does not delete the placeholder document if PUT fails — orphan stays in the workspace. Pre-existing pattern across both CLI import flows; fix should land in a separate PR that touches both.
PR2 (parser + UI, #243) and PR3 / Task 6 (CLI + backend writer, #245) both shipped. The CLI orchestrator and the backend slides-content dispatch were the last open items; check the remaining boxes and let `tasks:archive` move the pair to `docs/tasks/archive/2026/05/`. Image upload (base64 → real `/images` POST) and orphan-deck cleanup on PUT failure are tracked as follow-ups in the merge commit body, not as still-open items in this todo. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Summary
packages/slides/src/import/pptx/reuses jszip (shared with the DOCX importer) and the host'sDOMParser(Node consumers polyfill via@xmldom/xmldom, same convention as docs). Adds a./nodesubpath build so CLI/backend can import without pulling in the editor.<p:cxnSp>straight/curved connectors map to first-classConnectorElementwith attached endpoints via per-slide id map,<p:grpSp>groups flatten through composed affine transforms,<p:graphicFrame>tables flatten to cell text + uniform-stroke border rects,<a:highlight>/<a:hlinkClick>land on the docsInline.style.backgroundColor/hrefthat already exist. Lossy fallbacks (<a:normAutofit>pre-scale,<a:outerShdw>drop) are counted in anImportReport./imagesendpoint, stashes the parsed deck in a slides-side pending-import registry, and pushes it into the Yorkie root beforeensureSlidesRootruns.<a:lstStyle><a:lvl1pPr><a:defRPr sz>so title runs whose<a:rPr>lacksszget the layout default (e.g. 52pt) instead of collapsing to 11pt.Validation
Benchmark deck (
Yorkie, 캐즘 뛰어넘기.pptx, 36 slides) imports through the deck list UI with all counts matching the source: 480 elements (text 208, shape 158, connector 51, image 63), 48 groups flattened, 7 tables flattened, 3 unknown prsts folded to rect, slide-1 title renders at 52pt.Out of scope (follow-up PR)
wafflebase slides import— requires a parallel backendslides-contentcontroller + writer (seedocs/tasks/active/20260515-pptx-import-todo.mdTask 6).<p:txStyles>inheritance for placeholders that aren't covered by their layout's<a:lstStyle>(v1.5).Test plan
pnpm verify:fastgreen on each commitpnpm slides test— 209 files / 926 tests pass (67 new underimport/pptx/)pnpm slides buildproduces dual-entry artifacts (dist/wafflebase-slides.es.js+dist/node.js)pnpm frontend buildproduces clean chunks, no chunk-gate regressiondocs/design/slides/slides-themes-layouts-import.md🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Improvements
Documentation