Skip to content

feat(file-viewer): open Word, Excel, PowerPoint and OpenDocument files - #3572

Open
LordMelkor wants to merge 2 commits into
block:mainfrom
LordMelkor:clod/file-viewer-office
Open

LordMelkor wants to merge 2 commits into
block:mainfrom
LordMelkor:clod/file-viewer-office

Conversation

@LordMelkor

Copy link
Copy Markdown
Contributor

Opens Word, Excel, PowerPoint and OpenDocument files in the bundled File viewer, a follow-up to #3338 (scope). It ports Media viewer v1.5's Office reading. It was held back from #3338 to keep that PR reviewable (joahg/buzz-app#2), and is now rebased onto main, with Office links and pictures moved onto main's PartMarkdown.

What changes for people

  • Word, OpenDocument text, RTF and old Word files open as documents, with the same parts, outline and Copy text as Markdown. Pictures inside the file are drawn; a linked picture follows the community rule and otherwise stays as its alt text. Old .doc and .ppt files show their text and say so.
  • Excel, OpenDocument and Numbers sheets show one sheet at a time as a grid with sticky headers and a tab per sheet. Each sheet keeps its scroll position. "Copy as TSV" copies the open sheet. Notes say what isn't shown: hidden sheets, rows and columns, sheets past the limit, and formulas with no saved value.
  • PowerPoint and OpenDocument slides are drawn as slides: text, shapes, pictures, tables, charts and speaker notes. A deck with three or more titled slides gets an outline.
  • Charts in sheets and slides are drawn from their saved values: bar, column, line, area, pie, doughnut and scatter, stacked or percent, and combinations of them.
  • Pages and Keynote files show the preview picture saved in the file and say how to open them.
  • A file with macros says they never run. A password-protected, damaged or too-large file says why it can't be opened.
  • The header names the app (Word, Excel, PowerPoint, OpenDocument, Numbers, Pages, Keynote) and shows reading time and words, or the number of sheets or slides.
  • The Office code is a separate chunk that loads only when an Office file is opened (Sigil measured the data layer at 854,011 bytes minified, 244,922 gzipped).

How

  • Data (Sigil, first commit): office/ reads each format into a sanitized hast tree, sheets, slides or a preview; load.ts is the only entry point (openOffice(bytes, name, signal)). The trees pass through a sanitizer schema that allows ids, a few classes and table spans, and no styles.
  • UI (Clod): OfficeView.tsx opens the file and renders each family; office-parts.ts splits a document tree into parts for the existing PartBuffer; SlidesView.tsx and ChartView.tsx draw slides and charts. DocumentView exports Outline and a usePartJump hook, so Office documents reuse the same outline and #link jumps. PartMarkdown exports Picture and FileLink, so pictures and links in Office files follow the same rules as Markdown parts: community media draws and anything else is its alt text; # links move within the file, Buzz and safe links open through MessageLink, and anything else is its text.
  • Formats: formats.ts recognizes Office files by extension, then by MIME type, with a 50 MB limit.
  • New dependency: hast-util-to-jsx-runtime 2.3.6, pinned exactly and listed in NOTICE.md and check-icons. NOTICE.md also lists SheetJS, fflate, rehype-sanitize, the POI shape geometries, the LibreOffice table styles and the legacy test files with their licenses. The lockfile has no registry-specific URLs.

State machines

Audited with the state-machines skill:

  • Open: a typed union, loading | ready | failed, with an AbortController that the unmount aborts. key={target} on Viewer resets it for a new file.
  • Picture URLs: made in one effect when the file shows and all revoked in its cleanup, so none outlives the file.
  • Sheet tabs: the open sheet index, plus a map of scroll positions, written on scroll and read when a sheet opens.
  • Document parts: the existing reduceBuffer from feat(file-viewer): bundle a file viewer for shared PDFs, Markdown, text and CSV #3338, unchanged.

Efficiency

Sigil's Office data layer was reviewed by GPT Astra (High) as an efficiency oracle; its last review found no blocker.

Evidence

At the head of this PR:

  • Biome, tsc, design:check and pnpm lint (including check-icons): clean.
  • Vitest src/bundled/file-viewer and src/features/markdown: 46 files, 1,604 tests passed.
  • Playwright file-viewer.spec.mjs, Chromium and WebKit: 22 passed (11 per engine). New cases:
    • opens Office files in their own chunk, only once one is opened (no OfficeView script loads for notes.md; exactly one loads for foundation.docx)
    • opens budget.xlsx, review.pptx and brochure.pages from their cards
  • The pre-push hook (tsc, the full Vitest suite, design-system, clippy) passed at this head.
  • git diff --check: clean apart from trailing spaces inside the rtf-textutil.rtf fixture, which are part of that file as macOS textutil writes it.

Known limitations

  • Charts and slides are drawn from the values saved in the file. Formulas aren't recalculated, and some rare chart types and slide effects are drawn simply or left out, as the notes say.

🤖 Generated with Claude Code

LordMelkor and others added 2 commits November 15, 2026 08:40
Read Office, OpenDocument, RTF and iWork files through bounded format
admission, XML and archive parsing. Keep the generated document, sheet,
slide and preview models independent of the viewer UI.

Co-authored-by: Sigil <06114f072cf21e50fcd4133ab21346cdb1f74ee16d853a9729995fa6c03a251c@buzz.block.builderlab.xyz>
Signed-off-by: LordMelkor <kray@squareup.com>
Documents show with their pictures, tables and charts, and a Word file
with three or more headings has an Outline. Spreadsheets show a sheet at
a time with a tab for each, and Copy as TSV copies the open sheet.
Presentations show each slide fitted to the tab, with speaker notes.
Numbers files open as sheets too. Older .doc and .ppt files show their
text only, and Pages and Keynote files the preview picture saved in
them. Nothing in a file runs.

Co-authored-by: Clod <c7b21872e9ddb79ca62967d0e21e6a4518a669eb04648e963cdfb2426e62fafb@buzz.block.builderlab.xyz>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: LordMelkor <kray@squareup.com>

@joahg joahg left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

🤖 This account isn't approving UI changes. This PR adds new views to the bundled File viewer (documents, sheets, slides, charts), so it needs a design or core-team reviewer. Because these files arrive as untrusted chat attachments, I focused on security. I found no P0 or P1.

Checked:

  • DOM sinks: none in src/bundled/file-viewer: no dangerouslySetInnerHTML, innerHTML, foreignObject, srcdoc or eval. Parsers build hast directly. Every tree goes through officeSanitizer before toJsxRuntime, and that sanitizer strips script/style/iframe/svg, allows no style attribute and limits src to https.
  • Links and pictures: FileLink only opens # anchors, Buzz links and http(s) without credentials, and only on click; anything else renders as text. Remote and linked pictures, including in slides, go through the same community-media gate as Markdown. Slide <img> also sets no-referrer.
  • Inline styling on slides and charts: colors are hex-validated, font names are restricted to a safe character class, and paths are built from numbers.
  • Hostile files:
    • The zip reader inflates in small pushes with caps per part, in total and per entry count.
    • vbaProject, macros and embeddings are never inflated.
    • XML rejects DOCTYPE and ENTITY.
    • CFB chains are cycle-checked.
    • RTF depth and PPT history cycles are bounded.
    • The 50 MB limit checks both Content-Length and the streamed total.
    • Nothing executes macros.
  • @e965/xlsx 0.20.3: I compared the registry tarball with the official SheetJS 0.20.3 tarball from cdn.sheetjs.com. The library files are byte-identical. Only package.json (name and repository) and README.md differ, and there are no install scripts.

Findings:

  • P2, DOM clobbering via bookmark ids. office-parse.ts sets clobber: [] and clobberPrefix: "", which turns off rehype-sanitize's default user-content- prefix that the Markdown path keeps. Meanwhile office/document.ts (docx bookmarkStart, about line 916) and the ODF bookmark handling emit the file's bookmark names as raw ids. A document with a bookmark named after a global the app or a library reads, such as isTauri, config or a feature-detect name, creates a named element on window while the file is open. In the Tauri build, an own property shadows it for isTauri, so I couldn't turn this into an exploit. It's still fragile. Keeping the default prefix, or prefixing Office ids yourself, closes it, and findId in DocumentView.tsx already looks up the prefixed form.
  • P3, @e965/xlsx is a third-party republish. It has one maintainer. The current version matches upstream, but future bumps won't be checked automatically. Consider using the official tarball URL, or adding a note to NOTICE.md or the README saying the contents were verified against upstream for this version.
  • P3: SheetJS read (sheets.ts) runs synchronously on the main thread, and abort is only checked between phases. An admitted but hostile .xls, .xlsb or .numbers file can still freeze the UI for a while. A Worker would avoid that.
  • P3: the worst-case memory for one file is high: 50 MB in, up to 160 MB inflated plus 64 MB of media plus the DOM. It is bounded, but heavy for a webview.

Not reviewed in depth: I traced the outputs of odp.ts, pptx.ts and document.ts (ids, hrefs, colors, fonts, paths, pictures) to the sinks above, but didn't read every line. I didn't review the tests, the fixtures or the chart rendering math, and I didn't build or run the app.

@kazemek21

Copy link
Copy Markdown
Contributor

Instructions to get the build to run:
buzz://message?channel=5909b3f1-fbe6-4f63-9cd2-06440e58b4a1&id=41538c84fc77a64cc0d124407b31805e3d485d331654a91b5185d62dc9245b05&thread=71a8d710754540d5c562226840389021badb3398752eb76d821d2a5b6c2b16ba

Still working through the community contribution model for UX changes so can't provide a review timeline as of now. Additional context can be found here:
buzz://message?channel=5909b3f1-fbe6-4f63-9cd2-06440e58b4a1&id=8819f5f12686b5cdb17bd7187f32a0824c29dc56fc34afeba9526e9f53ac817a&thread=988d9d10fcedf2b289f815c99d6220033557294d094184d386ec501ab90c6eae

This branch has not been deployed

No deployments
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.

3 participants