feat(rtf): read an rtf as a text document - #733
Open
andiwand wants to merge 1 commit into
Open
Conversation
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
🤖 Generated with Claude Code
Stage 1 of
src/odr/internal/rtf/PLAN.md. An rtf opens and renders instead of reachingopen_strategy's fallthrough and throwingUnknownFileType.What it decodes
Paragraph structure and text:
\par,\line,\tab,\page,\sect, the literal-character control words, the escapes\\\{\}\~\_\-,\'hhin the run's encoding, and\uN— including surrogate pairs. The encoding resolves from\ansi/\mac/\pc/\pca/\ansicpgNthroughinternal/encoding.The table row moves from "recognised by magic, no decoder" to
DocumentType::textwith{.open, .translate_html, .color_scheme}. No binding work: theFileTypeordinal 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.
\cellrenders as a tab and\rowends a paragraph, so table text stays readable until stage 4.Shape
Following
internal/pdf's parser shape rather thanoldms, as the plan argued: rtf is a text format, so none of the CFB/FIB/piece-table machinery applies, butObjectParser's stream handling does —\binNneeds a raw n-byte read mid-stream and\'hhneeds the same hex helpers. The registry is copied fromoldms/textminus its style index.Leniency is the spec here
The root
AGENTS.mdsays 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\binNrunning past EOF and excessive nesting all throw.Two traps worth calling out, both covered by tests:
\binNis 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.\nonshppictneeds 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
.rtfanywhere undertest/data, and an rtf fragment reads fine in a raw string. They cover the delimiter rules token by token,\binpayloads containing braces, the encoding chain, surrogate pairs,\ucNskipping, and the group leniency.textutilrtf (umlauts,\'80euro, a\uc0surrogate-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/dataholds no rtf, soHtmlOutputTestsgains no case.