Skip to content

feat(preview): render RTF and Word documents - #1086

Open
spandan11106 wants to merge 4 commits into
lgse:mainfrom
spandan11106:feat/730-rtf-docx-previews
Open

spandan11106 wants to merge 4 commits into
lgse:mainfrom
spandan11106:feat/730-rtf-docx-previews

Conversation

@spandan11106

Copy link
Copy Markdown
Collaborator

Description

Quick preview renders RTF and Word (DOCX) documents instead of showing raw source or nothing at all. Both feed the paragraph/block document model and virtualized rich-text view already used for Markdown, HTML, CSV/TSV and workbooks, so they inherit its theming, selection, copy, sorting and bounded-parsing behaviour.

  • RTF is converted in process to the bounded HTML subset the rendered view already parses: paragraphs, line breaks, bold, italic, underline, strikethrough, Windows-1252 \'hh escapes and \uN escapes with their \ucN fallback characters. Font tables, stylesheets, pictures, headers, footers and other non-body destination groups are discarded. It keeps its Source view like Markdown and HTML.
  • DOCX is read with docx-rs inside the existing preview sandbox, under the same 20 MiB input limit as workbooks, and returns bounded JSON that the application reparses with the existing HTML parser. Headings, Title/Subtitle, quote styles, inline formatting, tabs, breaks, bulleted and numbered lists with nesting, and tables all render; the first table row becomes the sortable column titles. Being a ZIP container, it has no Source view — the same as XLSX/ODS.
  • Underline (u, ins) is now part of the supported HTML subset. The Pango layer already had the span style but the parser dropped it, so underlined text lost its formatting and raised a misleading "unsupported or active HTML content was omitted" notice.
  • PreviewContent::Workbook is renamed to Rendered, since spreadsheets and Word documents now share it.

The RTF converter is hand written rather than using rtf-parser as the issue proposed. That crate (0.4.3) drops \par entirely, so every paragraph runs together; decodes \'93 to U+0093 instead of a curly quote; and turns 舒? into an unrelated CJK character while leaving the ? fallback in place. DOCX uses docx-rs as planned.

Known limits, documented in docs/document-previews.md: DOCX drops images, hyperlink targets, headers, footers, footnotes and comments, joins each cell's paragraphs into one value, and flattens nested tables into their containing cell; RTF renders paragraphs and inline formatting only, so list markers and table cells become plain text, code pages other than Windows-1252 can mis-decode \'hh, and files over the 1 MiB preview limit fall back to Source.

Visual evidence

Pending — a short video of both previews will be attached in a follow-up comment below.

How to test

  1. Create a folder with a .docx containing headings, bold/italic/underlined text, a bulleted list, a numbered list and a table, plus a .rtf with several paragraphs, styled runs, curly quotes and an em dash. Any Word or WordPad/LibreOffice export works.
  2. Browse to that folder in Strata, select the .docx and press Space.
  3. Click a column title in the table to sort it, then drag across a few cells and press Ctrl+C.
  4. Select the .rtf and press Space, then use View source in the preview header and switch back to the rendered view.
  5. Switch theme while both previews are open.

Expected result: the DOCX preview shows a styled document — headings, inline formatting, nested bullets, numbered items and a sortable table whose first row is the column titles — with no View source control and no omission notice; the copied cells arrive tab separated. The RTF preview shows one block per paragraph with its bold, italic, underlined and struck runs, curly quotes and dashes intact, and toggles between Rendered and Source. Both follow the active theme live. Files that cannot be parsed fall back to the source view or "Preview unavailable" rather than failing the pane.

Related issue

Closes #730

RTF converts in-process to the bounded HTML subset the rendered document
view already parses, keeping its Source view. DOCX is read with docx-rs
inside the preview sandbox, which returns the same HTML as validated
JSON for the application to reparse.

Closes lgse#730
@spandan11106

Copy link
Copy Markdown
Collaborator Author
screenrecording-2026-09-17_14-36-48.mp4

@wmfeht

wmfeht commented Sep 17, 2026

Copy link
Copy Markdown
Collaborator

request-changes on 3317092a (RTF/DOCX previews)

DOCX sandboxing, HTML reparse, the underline subset, and PreviewContent::Rendered look right, and the new tests are focused rather than padded. One production hole in the in-process RTF walker.

Blocker: to_html can panic on \bin.

src/services/rtf.rs walks source.as_bytes() but emits body text with writer.text(&source[index..end], …). That &str slice requires both ends to be UTF-8 character boundaries.

\binN advances by N raw bytes (next.saturating_add(length).min(bytes.len())) and is not gated on destination skip. After that skip, index can sit in the middle of a multi-byte character. The next non-delimiter byte then panics (byte index X is not a char boundary). gio::spawn_blocking catches the unwind, so the pane shows “The preview worker stopped unexpectedly” instead of falling back to Source — still a failed preview, and it runs in the application, not the sandbox.

Minimal shape: {\rtf1\ansi\bin1 é\par} (é is C3 A9; \bin1 skips C3). The same hole hits UTF-8 RTF after a picture \bin, which the comment says this control word exists to skip.

Related: the walker sees from_utf8_lossy output, so \bin lengths are decoded-string bytes, not original file bytes. Picture payloads already desync grouping even when they don’t panic.

skip_fallback already eats UTF-8 continuation bytes. \bin needs the same rule: never slice str off a boundary (decode chunks from &[u8], or snap to is_char_boundary and treat binary as non-text).

A \binN payload advanced the walk by that many source bytes. The preview
source is a lossy decode, so the count could land inside a multi-byte
character and slicing the next run of text panicked the in-process parser.
Skip whole characters instead, and decode emitted text from the byte slice
so no stored length can panic the walk.
@spandan11106

Copy link
Copy Markdown
Collaborator Author

Confirmed and fixed in 3335744. Thanks — the reproduction was exact.

{\rtf1\ansi\bin1 ékept\par} against the previous walker:

panicked at src/services/rtf.rs:162:40:
start byte index 18 is not a char boundary; it is inside 'é' (bytes 17..19 of string)

Both halves of the diagnosis hold. \binN advanced by N source bytes with no boundary rule, and the preview source is String::from_utf8_lossy output from decode_text_sample, so those lengths are decoded-string bytes rather than original file bytes.

Two changes:

  • \bin now skips N characters through a shared next_character helper, which skip_fallback already used for \uN fallbacks. Since a byte that failed to decode has already become exactly one replacement character, counting characters both stays on a boundary and tracks the original payload more closely than counting decoded bytes did.
  • Body text is emitted with String::from_utf8_lossy(&bytes[index..end]) instead of slicing source, so no stored length in any control word can panic the walk, including any arithmetic added later.

Regression test binary_payload_lengths_skip_whole_characters covers your minimal shape and a payload longer than the remaining document. It panics with the same message on the parent commit and passes here.

You are right that a picture payload can still desync grouping — a decoded length is an approximation of a binary one, so a stray { or } inside the payload can still be read as structure. That now costs a garbled or empty rendered view falling back to Source rather than a failed pane, and it is bounded by the same parser budgets. Reading RTF from the original bytes rather than a lossy decode would remove the approximation entirely; that means changing how the preview loader hands documents to the parser, which reaches past this issue, so I would rather do it separately than widen this one.

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.

feat(preview): render RTF and DOCX documents

2 participants