The Problem
Office documents are binary containers with inconsistent internal structures. Word, PowerPoint, and Excel files each encode content differently, and converting them to clean Markdown for LLM ingestion usually means running multiple format-specific tools with different output shapes. That fragmentation breaks downstream pipelines.
What This Does
anydoc is a Rust library that parses Word, PowerPoint, Excel, OpenDocument, RTF, EPUB, CSV, and PDF into a shared document model (src/model/), then renders that model through a single Markdown serializer (src/render/markdown/). The result is consistent output regardless of input format.
The repo ships three bindings: a Node.js package (node/), a Python package (python/), and a Rust crate (src/). Each binding exposes the same three operations: to_markdown, to_markdown_bytes, and to_document. A CLI (node/cli.js) wraps the Node binding for shell use.
How It Is Wired
Execution starts at main in examples/convert.rs:11, which calls run and reaches 339 functions. The traced path to external I/O is main -> run -> to_document -> parse, ending at cfb::CompoundFile::open for the filesystem call. That is a short path: one entry point, one function, one external effect.
The format parsers live under src/formats/, one module per format. src/formats/doc/mod.rs is the most connected file: 42 functions, called from 15 files, calling into 23. It defines parse, parse_plc, and parse_clx. The shared XML helpers in src/package/xml.rs (34 functions) serve every format that parses XML, and src/model/link.rs defines is_empty, which is called from 75 places — the widest blast radius in the repo.
The call graph shows 1,297 internal edges with no circular dependencies. The hub is parse, called from 27 places, and insert, called from 37. Changing either ripples broadly. The node/index.js file (686 lines, 233 branch points) is the largest binding and the hardest to modify safely.
How To Use It
# CLI
npx @firecrawl/anydoc report.docx
npx @firecrawl/anydoc slides.pptx -o slides.md
# Node.js
npm install @firecrawl/anydoc
import { toMarkdown } from '@firecrawl/anydoc';
const md = await toMarkdown('report.docx');
# Python
pip install firecrawl-anydoc
import anydoc
md = anydoc.to_markdown("report.docx")
# Rust
cargo add anydoc
let md = anydoc::to_markdown("report.docx")?;
No environment variables or config files are required. The format is auto-detected from content; CSV needs an explicit format argument since it has no signature.
Real-World Use
A document processing pipeline that ingests email attachments — mixed .docx, .xlsx, and .pdf files — converts each to Markdown, then chunks and embeds them for retrieval. The single output shape means one chunking and embedding path handles every format.
import anydoc
for path in incoming_files:
md = anydoc.to_markdown(path)
chunks = chunk_text(md)
embed_and_store(chunks)
Code Health & Issues
Static analysis found 48 findings: 7 high, 41 medium. The high-severity items:
- High - Deep nesting (x33) -
src/formats/doc/lists.rs,src/formats/doc/mod.rs,src/formats/odf/text.rsshow max indentation depth of 8. Flatten with early returns. - High - Duplicated code (x20 blocks across 8 files) -
node/src/document.rsandpython/src/document.rsrepeat similar logic. Extract shared helpers. - High - Oversized files (x6) -
node/index.jsat 686 lines,tests/gen_fixtures.py,src/formats/doc/mod.rs. Split by responsibility.
Medium findings include file handles opened without context managers in bench/convert.py and bench/render_truth.py, and broad exception handling in bench/judge.py.
The CI workflow uses unpinned third-party actions (dtolnay/rust-toolchain@stable), has no Dependabot, no dependency vulnerability scan, and leaves persist-credentials enabled. Fix these before production use.
The Bottom Line
This is a well-structured conversion library with a clean architecture: shared model, single renderer, format parsers isolated. The test suite (126 files) and fuzz targets are strong signals. The main risks are the deep nesting in the .doc and ODF parsers and the CI hygiene gaps. Use it if you need consistent Markdown output across many office formats and can tolerate the binding-layer complexity.