Skip to content

Add PPTX best-effort import to slides (parser + UI)#243

Merged
hackerwins merged 4 commits into
mainfrom
feat/slides-pptx-import
May 16, 2026
Merged

Add PPTX best-effort import to slides (parser + UI)#243
hackerwins merged 4 commits into
mainfrom
feat/slides-pptx-import

Conversation

@hackerwins

@hackerwins hackerwins commented May 15, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • New PPTX parser at packages/slides/src/import/pptx/ reuses jszip (shared with the DOCX importer) and the host's DOMParser (Node consumers polyfill via @xmldom/xmldom, same convention as docs). Adds a ./node subpath build so CLI/backend can import without pulling in the editor.
  • Maps OOXML theme/master/layout/slide onto the v0.4 slides model — 13 prst shape kinds land directly on the existing 117-kind registry, <p:cxnSp> straight/curved connectors map to first-class ConnectorElement with 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 docs Inline.style.backgroundColor / href that already exist. Lossy fallbacks (<a:normAutofit> pre-scale, <a:outerShdw> drop) are counted in an ImportReport.
  • Frontend deck list gains an "Import PPTX" item next to "Import DOCX". Picks the file, parses client-side, uploads embedded images through the existing workspace /images endpoint, stashes the parsed deck in a slides-side pending-import registry, and pushes it into the Yorkie root before ensureSlidesRoot runs.
  • Font-size inheritance reads each layout placeholder's <a:lstStyle><a:lvl1pPr><a:defRPr sz> so title runs whose <a:rPr> lacks sz get 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)

  • CLI wafflebase slides import — requires a parallel backend slides-content controller + writer (see docs/tasks/active/20260515-pptx-import-todo.md Task 6).
  • Backend e2e for the CLI flow.
  • Master <p:txStyles> inheritance for placeholders that aren't covered by their layout's <a:lstStyle> (v1.5).

Test plan

  • pnpm verify:fast green on each commit
  • pnpm slides test — 209 files / 926 tests pass (67 new under import/pptx/)
  • pnpm slides build produces dual-entry artifacts (dist/wafflebase-slides.es.js + dist/node.js)
  • pnpm frontend build produces clean chunks, no chunk-gate regression
  • Manual smoke (reporter): import benchmark deck through the deck list UI, confirm 36 slides + title font size matches the source
  • Reviewer manual smoke: import any PPTX through the deck list dropdown; confirm toast reports the right counts and the deck opens
  • Reviewer: check the design doc's new "re-validated gap" subsection in docs/design/slides/slides-themes-layouts-import.md

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Import PPTX files to create Slides documents from PowerPoint, available as "Import PPTX" in the New menu.
    • Preserves text, shapes, images, connectors, tables, themes, and layout ordering with graceful fallbacks; uploads embedded images and shows an import summary toast.
  • Improvements

    • Accurate slide sizing/scaling to a 1920×1080 canvas with fit behavior for differing aspect ratios; improved color/font/highlight handling and clearer fallback reporting.
  • Documentation

    • Updated import design notes and task/lessons pages capturing scope, gaps, and next steps.

Review Change Stack

hackerwins and others added 2 commits May 15, 2026 22:36
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]>
@coderabbitai

coderabbitai Bot commented May 15, 2026

Copy link
Copy Markdown

Warning

Rate limit exceeded

@hackerwins has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 56 minutes and 9 seconds before requesting another review.

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 @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

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 configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 649ad94d-9c4b-4316-944c-075e71827884

📥 Commits

Reviewing files that changed from the base of the PR and between a624e1d and 5e1a618.

📒 Files selected for processing (4)
  • packages/slides/src/import/pptx/group.test.ts
  • packages/slides/src/import/pptx/group.ts
  • packages/slides/src/import/pptx/shape.ts
  • packages/slides/src/import/pptx/slide.ts
📝 Walkthrough

Walkthrough

Adds 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.

Changes

PPTX Import Feature

Layer / File(s) Summary
Task planning & design documentation
docs/design/slides/slides-themes-layouts-import.md, docs/tasks/README.md, docs/tasks/active/20260515-*.md
Design doc updated to derive EMU→px scaling from <p:sldSz>. Added Yorkie deck verification and new tasks/lessons pages describing architecture, phased tasks, and benchmark notes.
Frontend PPTX import UI & staging
packages/frontend/src/app/documents/document-list.tsx, packages/frontend/src/app/slides/pending-imports.ts, packages/frontend/src/app/slides/pptx-actions.ts, packages/frontend/src/app/slides/slides-view.tsx
DocumentList adds Import PPTX menu and handler that picks file, imports via importPptx, creates Slides document, stashes parsed payload in pending-imports Map, shows toast, navigates; SlidesView applies staged payload on mount.
Fixtures
packages/slides/src/import/pptx/__fixtures__/build-minimal-pptx.ts
buildMinimalPptx() builds minimal in-memory PPTX ZIP with required parts for tests.
XML helpers & unzip
packages/slides/src/import/pptx/xml.ts, packages/slides/src/import/pptx/unzip.ts, tests
Namespace-tolerant XML helpers (parseXml, child/children/descendant, attr/attrInt/textOf) and unzipPptx wrapper using JSZip with readText/readBytes/list and Content_Types validation.
Import reporting & rels
packages/slides/src/import/pptx/report.ts, packages/slides/src/import/pptx/rels.ts, tests
ImportReport tracks fallback counters and produces human summary. parseRels -> Map of rId→PptxRel and resolveRelsTarget resolves relative targets vs external URLs.
Theme & master parsing
packages/slides/src/import/pptx/theme.ts, packages/slides/src/import/pptx/master.ts, tests
parseTheme extracts color/font schemes with deterministic fallbacks. parseMaster returns ImportedMaster including clrMap of non-identity mappings and parsed background/placeholder styles.
Layout parsing
packages/slides/src/import/pptx/layout.ts, tests
parseLayout maps OOXML layout types to built-in layout ids, reports unknown types, and extracts placeholder default font sizes into a Map.
Geometry & group transforms
packages/slides/src/import/pptx/geometry.ts, packages/slides/src/import/pptx/group.ts, tests
EMU constants and emuScale derived from <p:sldSz> to 1920×1080; rotEmuToRad, parseXfrm, emuToStrokePx, prst→ShapeKind; GroupTransform compose/apply with pivot/rotation handling and tests.
Color & font utilities
packages/slides/src/import/pptx/color.ts, packages/slides/src/import/pptx/font.ts, tests
Color parsing for srgb/scheme/sys/prst (with tint/shade modifiers), parseHexInContainer, ClrMap routing; parsePrimaryTypeface and containsHangul for Hangul detection and Korean font fallback.
Image & table parsing
packages/slides/src/import/pptx/image.ts, packages/slides/src/import/pptx/table.ts, tests
parsePic resolves blip rels, reads bytes, infers MIME/extension, calls uploadImage, optionally derives normalized crop; parseTable flattens tables into per-cell text + border shapes, reports merges and border approximations.
Text parsing & inline styling
packages/slides/src/import/pptx/text.ts, tests
parseTextBody parses paragraphs/runs, applies normAutofit pre-scaling, extracts paragraph props, list metadata, run styling (font size/family, Hangul fallback, color/highlight), hyperlink sanitization, and emits placeholder inlines for empty paragraphs.
Shape & connector parsing
packages/slides/src/import/pptx/shape.ts, tests
Two-pass parseSpTree with id preassignment (nested groups) then element parsing; builds TextElement/ShapeElement/ConnectorElement, resolves endpoints via idMap or transformed corners, parses adjustments/fill/stroke/arrowheads, and reports dropped effects.
Slide import & entrypoint
packages/slides/src/import/pptx/slide.ts, packages/slides/src/import/pptx/index.ts, tests
parseSlide resolves layout/background/notes and delegates element parsing; importPptx orchestrates unzip, presentation parsing, theme/master/layout loading with dedupe, computes emuScale from <p:sldSz>, parses slides in order, and returns SlidesDocument + ImportReport.
Package exports & build
packages/slides/package.json, packages/slides/src/index.ts, packages/slides/src/node.ts, packages/slides/vite.build.ts
Adds ./node export subpath and jszip dependency; re-exports importPptx/types/report for browser and Node consumers; Vite config updated to multi-entry build for separate node/browser bundles.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

  • wafflebase/wafflebase#190: Frontend SlidesView/Yorkie initialization changes that this PR extends by applying staged PPTX payloads during mount.

🐇 A rabbit peeks, whiskers bright,
I unzip slides into the light,
Shapes and text hop into place,
Themes and colors find their space,
Imported slides — a joyful sight!

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/slides-pptx-import

@github-actions

github-actions Bot commented May 15, 2026

Copy link
Copy Markdown
Contributor

Verification: verify:self

Result: ✅ PASS in 189.9s

Lane Status Duration
sheets:build ✅ pass 13.2s
docs:build ✅ pass 11.6s
slides:build ✅ pass 12.1s
verify:fast ✅ pass 109.4s
frontend:build ✅ pass 16.8s
verify:frontend:chunks ✅ pass 0.3s
backend:build ✅ pass 4.5s
cli:build ✅ pass 1.9s
verify:entropy ✅ pass 20.1s

Verification: verify:integration

Result: ✅ PASS

@codecov

codecov Bot commented May 15, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 win

Import 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 win

Add a regression test ensuring parsed masters do not share mutable style references.

Create two parseMaster(...) results, mutate one placeholderStyles.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 win

Add 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 win

Add a regression test for unsafe hyperlink schemes.

Please add a case like javascript:alert(1) to assert it does not populate inline.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 win

Simplify helper with Array.find for clarity.

The pickRelTarget function manually iterates and returns. Using Array.find would 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 value

Type assertion complexity could be simplified.

The inline type assertion for the empty fallback as Layout[], layoutMap: new Map() as LayoutPathToInfo is 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 win

Duplicate helper function across multiple files.

The relsSiblingFor function appears identically in both slide.ts (lines 146-151) and index.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 win

Consider using a for-of loop for better readability.

The manual iteration over childNodes using an index-based loop could be simplified with a for-of loop 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 win

Consider 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.layouts includes fields like masterId and staticElements that YorkieLayout omits. The cast works because these fields are preserved during JSON serialization and reconstructed by migrateDocument() on read, but the raw as unknown makes the type relationship opaque.

Rather than adjusting YorkieSlidesRoot (which would break the Yorkie storage schema), add a typed conversion function like toYorkieLayout() 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 win

Add a rotated-group test case.

Please add at least one case where <a:xfrm rot="..."> is set on <p:grpSp> and verify transformed child x/y (not only rotation). 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 win

Add a test for uploadImage rejection path.

Please add a case where uploadImage throws and assert parsePic returns undefined with report.skippedImages incremented.

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

📥 Commits

Reviewing files that changed from the base of the PR and between b735297 and f3ab3e1.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (44)
  • docs/design/slides/slides-themes-layouts-import.md
  • docs/tasks/README.md
  • docs/tasks/active/20260515-pptx-import-lessons.md
  • docs/tasks/active/20260515-pptx-import-todo.md
  • packages/frontend/src/app/documents/document-list.tsx
  • packages/frontend/src/app/slides/pending-imports.ts
  • packages/frontend/src/app/slides/pptx-actions.ts
  • packages/frontend/src/app/slides/slides-view.tsx
  • packages/slides/package.json
  • packages/slides/src/import/pptx/__fixtures__/build-minimal-pptx.ts
  • packages/slides/src/import/pptx/color.test.ts
  • packages/slides/src/import/pptx/color.ts
  • packages/slides/src/import/pptx/font.test.ts
  • packages/slides/src/import/pptx/font.ts
  • packages/slides/src/import/pptx/geometry.test.ts
  • packages/slides/src/import/pptx/geometry.ts
  • packages/slides/src/import/pptx/group.test.ts
  • packages/slides/src/import/pptx/group.ts
  • packages/slides/src/import/pptx/image.test.ts
  • packages/slides/src/import/pptx/image.ts
  • packages/slides/src/import/pptx/index.test.ts
  • packages/slides/src/import/pptx/index.ts
  • packages/slides/src/import/pptx/layout.test.ts
  • packages/slides/src/import/pptx/layout.ts
  • packages/slides/src/import/pptx/master.test.ts
  • packages/slides/src/import/pptx/master.ts
  • packages/slides/src/import/pptx/rels.test.ts
  • packages/slides/src/import/pptx/rels.ts
  • packages/slides/src/import/pptx/report.ts
  • packages/slides/src/import/pptx/shape.ts
  • packages/slides/src/import/pptx/slide.ts
  • packages/slides/src/import/pptx/table.test.ts
  • packages/slides/src/import/pptx/table.ts
  • packages/slides/src/import/pptx/text.test.ts
  • packages/slides/src/import/pptx/text.ts
  • packages/slides/src/import/pptx/theme.test.ts
  • packages/slides/src/import/pptx/theme.ts
  • packages/slides/src/import/pptx/unzip.test.ts
  • packages/slides/src/import/pptx/unzip.ts
  • packages/slides/src/import/pptx/xml.test.ts
  • packages/slides/src/import/pptx/xml.ts
  • packages/slides/src/index.ts
  • packages/slides/src/node.ts
  • packages/slides/vite.build.ts

Comment thread packages/frontend/src/app/slides/slides-view.tsx
Comment thread packages/slides/src/import/pptx/color.ts
Comment thread packages/slides/src/import/pptx/font.ts Outdated
Comment thread packages/slides/src/import/pptx/font.ts Outdated
Comment thread packages/slides/src/import/pptx/geometry.ts Outdated
Comment thread packages/slides/src/import/pptx/shape.ts Outdated
Comment on lines +75 to +91
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
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

🧩 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:


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.

Comment thread packages/slides/src/import/pptx/table.ts Outdated
Comment thread packages/slides/src/import/pptx/table.ts Outdated
Comment thread packages/slides/src/import/pptx/text.ts
@hackerwins

Copy link
Copy Markdown
Collaborator Author

Code review

No 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]>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (1)
packages/slides/src/import/pptx/slide.ts (1)

57-59: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Avoid shallow-copying DEFAULT_BACKGROUND in slide parsing.

Line 59 and Line 109 use object spread, which still shares nested fill references. 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

📥 Commits

Reviewing files that changed from the base of the PR and between f3ab3e1 and a624e1d.

📒 Files selected for processing (20)
  • packages/frontend/src/app/slides/slides-view.tsx
  • packages/slides/src/import/pptx/color.test.ts
  • packages/slides/src/import/pptx/color.ts
  • packages/slides/src/import/pptx/font.ts
  • packages/slides/src/import/pptx/geometry.test.ts
  • packages/slides/src/import/pptx/geometry.ts
  • packages/slides/src/import/pptx/group.test.ts
  • packages/slides/src/import/pptx/group.ts
  • packages/slides/src/import/pptx/image.test.ts
  • packages/slides/src/import/pptx/image.ts
  • packages/slides/src/import/pptx/index.ts
  • packages/slides/src/import/pptx/master.test.ts
  • packages/slides/src/import/pptx/master.ts
  • packages/slides/src/import/pptx/report.ts
  • packages/slides/src/import/pptx/shape.ts
  • packages/slides/src/import/pptx/slide.ts
  • packages/slides/src/import/pptx/table.test.ts
  • packages/slides/src/import/pptx/table.ts
  • packages/slides/src/import/pptx/text.test.ts
  • packages/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

Comment thread packages/slides/src/import/pptx/group.ts Outdated
Comment thread packages/slides/src/import/pptx/slide.ts Outdated
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]>
@hackerwins
hackerwins merged commit be0f15f into main May 16, 2026
4 checks passed
@hackerwins
hackerwins deleted the feat/slides-pptx-import branch May 16, 2026 00:50
hackerwins added a commit that referenced this pull request May 16, 2026
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.
hackerwins added a commit that referenced this pull request May 16, 2026
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]>
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