Skip to content

feat(rtf): read an rtf as a text document - #733

Open
andiwand wants to merge 1 commit into
mainfrom
feat/rtf-text
Open

feat(rtf): read an rtf as a text document#733
andiwand wants to merge 1 commit into
mainfrom
feat/rtf-text

Conversation

@andiwand

Copy link
Copy Markdown
Member

🤖 Generated with Claude Code

Stage 1 of src/odr/internal/rtf/PLAN.md. An rtf opens and renders instead of reaching open_strategy's fallthrough and throwing UnknownFileType.

What it decodes

Paragraph structure and text: \par, \line, \tab, \page, \sect, the literal-character control words, the escapes \\ \{ \} \~ \_ \-, \'hh in the run's encoding, and \uN — including surrogate pairs. The encoding resolves from \ansi / \mac / \pc / \pca / \ansicpgN through internal/encoding.

The table row moves from "recognised by magic, no decoder" to DocumentType::text with {.open, .translate_html, .color_scheme}. No binding work: the FileType ordinal was already spent, and everything else arrives through the abstract document model.

Character and paragraph formatting, page layout, tables and pictures are stages 2–5 and are not read yet. \cell renders as a tab and \row ends a paragraph, so table text stays readable until stage 4.

Shape

bytes ─▶ Tokenizer ─▶ TreeBuilder ─▶ ElementRegistry ─▶ Document ─▶ RtfFile

Following internal/pdf's parser shape rather than oldms, as the plan argued: rtf is a text format, so none of the CFB/FIB/piece-table machinery applies, but ObjectParser's stream handling does — \binN needs a raw n-byte read mid-stream and \'hh needs the same hex helpers. The registry is copied from oldms/text minus its style index.

Leniency is the spec here

The root AGENTS.md says throw where the spec dictates what to expect. Here it dictates the opposite: an unknown control word "should be ignored", and {\* exists precisely so a newer writer's output stays readable. So unknown control words and unimplemented {\* destinations are ignored and an unmatched } is dropped — while a group left open at EOF, an invalid hex digit after \', a \binN running past EOF and excessive nesting all throw.

Two traps worth calling out, both covered by tests:

  • \binN is read by the tokenizer, not the parser. Its payload is raw bytes that may contain braces, so a brace-counting scan would desync the group nesting — including while skipping an ignorable destination, which is why that skip still tokenizes.
  • \nonshppict needs an explicit entry in the discard table. It is the unmarked twin of {\*\shppict}, so the \* rule does not hide it, and every image would otherwise appear twice.

Verification

  • 33 new tests, all inline string literals — there is no .rtf anywhere under test/data, and an rtf fragment reads fine in a raw string. They cover the delimiter rules token by token, \bin payloads containing braces, the encoding chain, surrogate pairs, \ucN skipping, and the group leniency.
  • Full suite green: 1018 passed, 0 failed, the same 8 pre-existing skips.
  • clang-tidy: zero findings in the new sources.
  • Real files: a macOS textutil rtf (umlauts, \'80 euro, a \uc0 surrogate-pair emoji, curly quotes) and four LibreOffice conversions including a 2 MB one with images and a table document — correct text throughout, no duplicated images, no leaked font or style-table content.

No reference-output regen is owed: test/data holds no rtf, so HtmlOutputTests gains no case.

Stage 1 of `internal/rtf/PLAN.md`: an rtf opens and renders instead of
reaching `open_strategy`'s fallthrough and throwing `UnknownFileType`.

A pull-based `Tokenizer` over the rtf byte grammar, a `State` group stack,
and a parser building `root -> paragraph -> (text | line break)` so the
generic html renderer and every binding get it for free. Text, its encoding
(`\ansi`, `\mac`, `\ansicpgN`, `\'hh`, `\uN` with signed folding and
surrogate pairs, `\ucN` skipping), paragraphs, line breaks and tabs.

Leniency is the spec here: unknown control words and unimplemented `{\*`
destinations are ignored, an unmatched `}` is dropped. A group left open at
EOF, a bad hex digit, a `\binN` past EOF and excessive nesting throw.

Character and paragraph formatting, page layout, tables and pictures are
stages 2-5 and are not read yet.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N47M1zVGirwkpbtXXctCAb
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