Pre-development analysis document for docsai. Covers: (1) input/output formats and their real
complexity, (2) the state of the art in comparable open-source projects, (3) candidate extended
Markdown variants as pivot format, (4) evaluation of available Rust libraries, and (5) decisions
taken with their risks.
Analysis date: August 2026.
Scope note: this document analyses flow documents and grids, which is what plan v1
(Phases 0–9) built. Presentations (.pptx/.ppt/.odp) and the agent context-cost axis are
analysed in technical-analysis-presentations.md, which
backs development-plan-v2.md. Nothing here is retracted by it; the
decisions below (IR pivot, custom readers over zip + quick-xml, raw-block hatch, AssetStore
with content hashing, optional LibreOffice fallback) are exactly what makes the presentation
work affordable.
Both are ZIP containers with XML inside. They are the formats with the best public documentation and library support, and therefore anchor phases 1–3 of the plan.
.docx (WordprocessingML) — relevant parts inside the ZIP:
| Part | Content | Relevance for docsai |
|---|---|---|
word/document.xml |
Body: paragraphs (w:p), runs (w:r), tables (w:tbl) |
Core of the conversion |
word/styles.xml |
Style catalog (paragraph, character, table) + basedOn inheritance |
Must be dumped into DocMark front matter |
word/numbering.xml |
List definitions (numbering, bullets, levels) | Critical: lists in OOXML are not syntactically nested; they are rebuilt from numId/ilvl |
word/media/* |
Embedded images (png, jpeg, gif, wmf/emf) | Extract to assets/ |
word/_rels/*.rels |
Relationships (images, hyperlinks) | Reference resolution |
word/header*.xml, word/footer*.xml |
Headers and footers | Dedicated DocMark containers |
docProps/core.xml, docProps/app.xml |
Properties (title, author, dates…) | Front matter |
word/settings.xml, w:sectPr sections |
Page size, margins, columns | Section metadata |
Known hard points of .docx:
- 4-level formatting inheritance: document defaults → paragraph style → character style →
direct formatting (
rPr/pPr). For a faithful conversion you must resolve the cascade but store only the style reference + direct deltas (if everything is flattened, the inverse conversion produces monstrous documents with no reusable styles). - Lists: rebuild the tree from flat
(numId, ilvl)pairs. - Fields (
w:fldSimple,w:instrText): TOC, cross-references, page numbers. Preserved as raw-blocks in v1. - WMF/EMF images: legacy Windows vector formats without simple multiplatform rendering support. Strategy: extract as-is + warning; optional conversion in later phases.
- Revisions (
w:ins,w:del): out of scope for v1; documents with revisions are accepted by taking the "accepted" version and emitting a warning. - Comments (
w:comment*,word/commentsExtended.xml): out of scope for v1, read since plan v2 Phase 16-E into the same IR node a.pptxslide comment uses, with the DocMark syntax of specification §11.3. Written back from Phase 16-F.
.xlsx (SpreadsheetML) — relevant parts: xl/workbook.xml, xl/worksheets/sheet*.xml,
xl/sharedStrings.xml, xl/styles.xml (number formats, fonts, fills, borders — all via cross
indexes), xl/calcChain.xml.
Hard points of .xlsx:
- Cells store cached value + formula (
<c><f>SUM(A1:A3)</f><v>42</v></c>). DocMark must keep both: the formula for bidirectionality, the value for readability. - Shared formulas (
t="shared") and array formulas (t="array"): expand them or preserve their range metadata. - Number formats (
numFmt): the difference between45123and15/07/2023— Excel dates are serial numbers + format. KeepingnumFmtId/format code per cell is mandatory to avoid corrupting data on round-trip. - Merged cells (
mergeCells), column widths/row heights, frozen panes, validations, conditional formatting (the last three: metadata in v1, no semantics).
Also ZIP+XML (content.xml, styles.xml, meta.xml, settings.xml, Pictures/). Conceptual
model very similar to OOXML but with important differences:
- Automatic styles (
office:automatic-styles) represent direct formatting: every fragment with manual formatting generates an anonymous style. They must be "de-automated" on read (map automatic styles to direct-formatting deltas in the IR). .odsformulas use OpenFormula with a namespace prefix (of:=SUM([.A1:.A3])) and a different reference syntax ([.A1]vsA1). For OOXML⇄ODF bidirectionality, formula syntax would need translating; in v1 the formula is kept in its original dialect, annotatingformula-dialecton the cell.- ODF is better specified and more regular than OOXML; the effort of a custom parser with
quick-xmlis manageable and is in fact the plan for.odt(see §4.3).
.xls: solved —calaminereads it natively (values and formulas). Read-only..doc: the project's largest technical risk. It is a binary format on an OLE2/CFB container with complex internal structures (piece table, FIB, FKPs…). No mature Rust crate reads it with styles and images. Options evaluated:
| Option | Effort | Fidelity | Dependencies |
|---|---|---|---|
Custom parser on the cfb crate (MS-DOC spec) |
Very high (months) | High | No external deps |
Fallback to LibreOffice headless (soffice --headless --convert-to docx) and reuse the docx pipeline |
Low | Very high | LibreOffice installed (optional, detected at runtime) |
antiword/wvWare as external process |
Low | Low (loses styles) | External binary |
| Custom plain-text extraction (piece table only) | Medium | Text only | None |
Decision: two-level strategy. (a) LibreOffice headless fallback if installed — maximum
fidelity with minimum effort; (b) native text+basic-structure extractor on cfb as a degraded
mode without dependencies. A full MS-DOC parser is not pursued unless real demand justifies it.
This keeps the principle "single binary with no mandatory external runtime": LibreOffice
improves .doc but is never a requirement.
Images are a first-class project requirement: all input formats carry them, with different geometry models, and conversion must extract them and preserve size, position, anchor, and other properties for round-trip. Model by format:
| Format | Where they live | Geometry/anchor model |
|---|---|---|
.docx |
word/media/* + <w:drawing> (DrawingML) or <w:pict> (legacy VML) |
wp:inline (inline, only wp:extent) or wp:anchor (floating: position relative to page/margin/paragraph/character, EMU offsets, wrap square/tight/through/topAndBottom/none, z-order, behindDoc) |
.xlsx |
xl/media/* + xl/drawings/drawing*.xml (SpreadsheetDrawingML) |
Three anchors: xdr:twoCellAnchor (cell to cell + offsets, stretches with the grid), xdr:oneCellAnchor (origin cell + fixed size), xdr:absoluteAnchor (absolute position in EMU) |
.odt |
Pictures/* + <draw:frame><draw:image> |
text:anchor-type = as-char/char/paragraph/page/frame; svg:width/height/x/y; wrap via graphic style (style:wrap) |
.ods |
Pictures/* + <draw:frame> inside <table:shapes> or cell-anchored |
Cell anchor (table:end-cell-address + offsets) or sheet anchor |
.doc |
Data/Escher stream (OfficeArt) |
Binary Escher; on the LibreOffice-fallback path conversion is free; on the degraded native path BLIPs (images) are extracted without fine geometry |
Properties to preserve in all cases (they define the IR's normalized model; see
architecture.md §3.1): displayed dimensions vs native bitmap dimensions (and thus the scale
factor), DPI, crop (srcRect in OOXML, fo:clip in ODF), rotation and flips, anchor and
position with their offsets, text wrap mode, Z-order, alternative text and title (accessibility,
descr/svg:desc), internal object name and hyperlink on the image.
Specific hard points:
- Heterogeneous coordinates: EMU in OOXML (914,400/inch), cm/in in ODF, twips in
.doc. The IR normalizes everything to EMU with newtypes; DocMark exposes readablepx/pt/cm. - Legacy VML in docx: old documents (or converted from
.doc) use<w:pict>with VML instead of DrawingML. Both must be readable; writing always emits DrawingML. - twoCell anchors in xlsx: real size depends on column widths/row heights; to preserve geometry the symbolic anchor (cells + offsets) must be stored, not the resolved size — and the inverse on write.
- WMF/EMF: extracted as-is (perfect round-trip) but not viewable in Markdown viewers; conversion to PNG/SVG stays in the post-1.0 backlog. The image is still referenced with its full geometry.
- Duplicates: the same bitmap may appear N times with different geometries; the
AssetStorededuplicates by content hash and each appearance keeps its own geometry. - Linked (non-embedded) images: OOXML/ODF allow external
r:link/xlink:href; the URL is preserved with a warning (remote content is not downloaded — security implication).
| Project | Language | What it does | Lessons for docsai |
|---|---|---|---|
| Pandoc | Haskell | Universal conversion via pivot AST; its extended Markdown (attributes {...}, divs :::, front matter) is the most expressive on the market |
The full architectural pattern: readers → AST → writers. Its attribute syntax is the base of DocMark. Known limitation: medium-low fidelity on complex docx (custom styles, text boxes) and no xlsx support |
| MarkItDown (Microsoft) | Python | Office/PDF/HTML → "LLM-ready" Markdown, unidirectional | Validates demand for the MCP/LLM use case; its total loss of styles is exactly the gap docsai fills |
| Docling (IBM) | Python | Documents → Markdown/JSON with its own DoclingDocument model |
Confirms the need for a rich document model as pivot and for exporting both readable MD + structured metadata |
| mammoth (.js/Python) | JS/Python | docx → semantic HTML via a configurable style map (Heading1 ⇒ h1) |
The user-configurable style-map concept is adopted in docsai (--style-map) |
| html2md / turndown, marker, unoconv/unoserver | various | Partial converters | unoserver documents the LibreOffice headless fallback pattern |
| rdocx | Rust | docx read/write + render to PDF/HTML/MD (recent crate, 2026) | Watch as an alternative; too young to anchor the project today |
State-of-the-art conclusion: nobody today combines (1) native binary with no runtime, (2) bidirectionality with styles, (3) spreadsheets with formulas, and (4) an MCP server. Pandoc is the reference ceiling for text; MarkItDown/Docling for LLM integration. docsai positions itself in the empty intersection.
Requirements: readable by humans and standard viewers, arbitrary attributes on inline and block, document metadata, extensible without breaking parsers, and with an ecosystem.
| Candidate | Attributes | Ecosystem | Verdict |
|---|---|---|---|
| Pure CommonMark + GFM | ❌ None | Huge | Insufficient: without attributes there are no styles |
| Pandoc Markdown (attributes + fenced divs + spans + YAML) | ✅ Complete | Large (pandoc consumes it) | Chosen base. Syntax proven for a decade for exactly this problem |
| MyST Markdown | ✅ (directives/roles) | Scientific/Sphinx | More verbose directives; oriented to publishing, not round-trip |
| Djot (Jyrki/MacFarlane) | ✅ native | Small | Technically superior but breaks "looks fine on GitHub" compatibility |
| MDX | JSX | Web/React | Discarded: not readable Markdown for non-programmers |
Decision: DocMark = CommonMark + GFM (tables, strikethrough, task lists) + a subset of
Pandoc extensions (attributes {...} on headings/images/spans/code, fenced divs :::, YAML
front matter) + custom extensions for spreadsheets (cell metadata) documented in
docmark-specification.md. Additional benefit: a DocMark file is processable by Pandoc
directly with acceptable degradation, giving free interoperability with the whole Pandoc
ecosystem (PDF via LaTeX, HTML, EPUB…).
| Crate | Proposed role | Status (2026) | Evaluation notes |
|---|---|---|---|
calamine |
Read .xls, .xlsx, .xlsb, .ods (values and formulas) |
Mature, maintained, widely used | Lazy and fast reading; reads formulas via worksheet_formula(). Does not read styles/number formats in enough detail → complemented with custom reading of xl/styles.xml |
umya-spreadsheet |
Read+write .xlsx with styles |
Maintained | Evaluated in spike R3; not adopted for writing (opaque API / fidelity vs custom OPC) |
rust_xlsxwriter |
Write .xlsx (alternative) |
Very maintained (port of XlsxWriter) | Evaluated in spike R3; not adopted — write-only and weaker style round-trip than a custom writer |
| Custom OPC + SpreadsheetML (chosen) | Read+write .xlsx |
In-tree (docsai-office::xlsx) |
Same approach as docx: full control of styles, shared formulas, drawings. calamine only for .xls. See docs/spikes/R3-xlsx-writer.md |
docx-rs (bokuweb) |
.docx |
Most used (1M+ downloads) | Discarded for reading after spike R1 (docs/spikes/R1-docx-strategy.md, August 2026): handles styles and numbering well, but loses almost the entire image model (wrap, behindDoc, crop, flip, rotation, alt, title, hyperlink), footnotes and w:instr on simple fields, does not preserve unknown elements for raw-blocks, and panics on 23% of 903 measured corrupt inputs. docx reading uses a custom parser on zip + quick-xml. Still a candidate for the Phase 2 writer, an independent decision |
docx-rust |
Alternative .docx reading |
Less activity | More direct XML mapping; keep as reference |
spreadsheet-ods |
Read+write .ods |
Maintained | Covers ODS styles and formulas; avoids writing a custom ODF-spreadsheet writer |
| (none) | .odt |
— | No mature crate for ODT with styles: custom parser/writer on zip + quick-xml (ODF is regular; bounded effort) |
cfb |
OLE2 container for legacy .doc/.xls/.ppt |
Stable | Base of the degraded .doc extractor, and of the degraded .ppt extractor from plan v2 Phase 19 — see §4.6 |
office_oxide |
Candidate reader for legacy .ppt (plan v2 Phase 19) |
Maintained (0.1.9, September 2026) | Evaluated and not adopted — cheap to link but silent on both refusal fixtures, and its .ppt model is text-only. Full argument in §4.6 and kb/99-phase-19-parser-choice.md |
| Crate | Role | Notes |
|---|---|---|
comrak |
Markdown parser for the DocMark→IR pipeline | Full CommonMark+GFM, keeps positions, supports front matter, has limited attribute extension; full {...} attributes and fenced divs ::: are processed in a custom pass over its AST (or pre-lexer) |
markdown-rs / pulldown-cmark |
Alternatives | pulldown-cmark is faster but event-oriented (awkward for transformation); markdown-rs has a pleasant mdast AST but fewer native extensions |
Decision: comrak + custom attributes/divs layer. The DocMark serializer (IR→MD) is
hand-written (not delegated to comrak) to control output byte-by-byte and guarantee round-trip
idempotence.
| Crate | Role |
|---|---|
zip |
OOXML/ODF containers |
quick-xml (+ serde) |
High-performance XML parsing where format crates fall short |
serde / serde_yaml / serde_json |
Front matter, inspect --json, config |
clap (derive) |
CLI |
rmcp (official MCP SDK, stdio transport) |
MCP server; #[tool] macros; implements the 2026-07-28 spec with backward compatibility |
rmcp MSRV |
1.88+ (rmcp 3.x). Workspace rust-version tracks this floor; CI uses stable |
image |
Not used in Phase 1. docsai never re-encodes a bitmap; it only needs to name and measure it, which is done by reading the format header (PNG, JPEG, GIF, BMP, TIFF, WebP, EMF, WMF) in docsai-model::assets, without a heavy dependency. Will be re-evaluated if some phase truly needs re-encoding |
serde_yaml |
Not used. Front matter has a small known schema and the spec requires byte-for-byte determinism, so it is written by hand; the crate is also without active maintenance |
thiserror / anyhow |
Errors |
tracing + tracing-subscriber |
Logs (always to stderr) |
tokio |
Only in docsai-mcp (rmcp requires it); the conversion core is synchronous |
insta |
Not used. Golden files are .expected.dmk.md beside the corpus, as prescribed by AGENTS.md §6: they are reviewed as normal text in the diff and do not depend on a snapshot format |
cargo-fuzz |
Parser fuzzing (Phase 8) |
cargo-dist |
Multiplatform release packaging |
tiktoken-rs |
Token counting for docsai tokens/outline and the corpus token gate (Phase 10) — see §4.4 |
Phase 10 turns document cost into a measured number, so the count has to come from a real
BPE tokenizer, not from bytes / 4. Three constraints come from AGENTS.md: no network at
build or run time, no Python, and no heavy dependency without a written reason.
| Crate | Version seen | Vocabulary | Deps | Verdict |
|---|---|---|---|---|
tiktoken-rs |
0.12 (MIT, MSRV 1.85) | .tiktoken files embedded in the crate |
anyhow, base64, bstr, fancy-regex, regex, rustc-hash |
Chosen |
tiktoken (goliajp) |
3.5 (MIT/Apache-2.0, edition 2024) | zstd-compressed, 11 encodings from 5 vendors | base64, regex, rustc-hash, ruzstd |
Faster and smaller on disk, but young and single-vendor; no reason to bet the CI gate on it |
bpe-openai (github/rust-gems) |
0.3 (MIT) | gzipped, rebuilt by a build.rs |
bpe, either, regex-automata, rmp-serde, serde, unicode-normalization |
Linear-time BPE, but a build script that reconstructs the dictionary on every clean build, for a number we compute once per file |
tokenizers (HuggingFace) |
— | tokenizer.json downloaded |
large | Rejected: needs the network, exactly what the plan forbids |
Decision: tiktoken-rs with the o200k_base encoding, the one modern OpenAI models use
and a fair proxy for any current model's cost. What matters for the CI gate is that the number is
real, deterministic and reproducible offline — not that it matches one vendor's billing exactly.
The encoding is named in the report ("encoding": "o200k_base") so a future change of tokenizer
is a visible change in the golden, never a silent one.
Cost accepted: the embedded vocabulary adds a few MB to the binary. tiktoken-rs loads each
encoding lazily through a singleton, so only the one we ask for is built at runtime.
Phase 11 task 8 asks the MCP server for include_images=none|refs|downscaled|full. Three of
the four rungs are byte slicing and need nothing; the downscaled rung needs a decoder and a
resizer, which is the one place in the project that has to understand an image rather than
carry it.
(The rung was called thumbnails when Phase 11 wrote it and until Phase 16-G, when
render_slides arrived and the word came to name two different pictures: this rung serves
downscaled copies of the pictures a document embeds, render_slides a raster of a whole
slide, which needs no decoder here at all. The old value is refused rather than aliased.
Architecture §6 and kb/81-phase-16-thumbnails.md carry the argument.)
| Option | What it costs | Verdict |
|---|---|---|
image 0.25, default-features = false, features png, jpeg, gif, bmp |
~20 transitive crates (png, zune-jpeg, gif, flate2, bytemuck…), all pure Rust; flate2 is already in the tree via zip |
Chosen |
image with default features |
adds TIFF, WebP, AVIF (ravif/dav1d bindings), EXR, DDS… |
Rejected: C dependencies and codecs no Office package embeds |
| Hand-rolled PNG/JPEG decoding | no new dependency | Rejected: writing image codecs to save a dependency is how a converter grows a memory-safety surface it did not need |
| Shelling out to LibreOffice / ImageMagick | no Rust dependency | Rejected: a system dependency on the main path, which architecture §7 keeps free of one |
Decision: image with the four raster formats Office packages actually embed, in
docsai-mcp only — the format crates, the model and the CLI never link it, so a build that
does not serve MCP does not pay for it. Vector formats (EMF, WMF, SVG) are not decoded and
never will be here: they degrade to a ref with the reason stated on the row, which is the
project's standing rule that a loss is reported rather than hidden.
Cost accepted, and bounded: decoding attacker-supplied images is a denial-of-service surface
(a few kB of PNG can declare 60 000 × 60 000 pixels), so the decoder runs under
image::Limits with a 64 MB allocation ceiling and any failure — bomb, corruption,
unsupported format — degrades that one image to a ref instead of failing the call.
Phase 19 applies the .doc strategy of §1.3 to legacy .ppt, read-only: a LibreOffice route
for fidelity, and a degraded native route for machines without it. The plan requires the
office_oxide crate to be weighed before it is adopted, under §7.4 of AGENTS.md. It was, in a
probe crate outside the repository, against the four corpus/ppt/ fixtures.
| Option | What it costs | Verdict |
|---|---|---|
cfb + our own [MS-PPT] atom walk |
No new crate — cfb 0.14 already carries the .doc reader. ~1 000–1 400 lines of Rust, the same order as the .doc reader (~1 004) and as office_oxide's own .ppt module (902) |
Chosen |
office_oxide 0.1.9 |
+98 KB stripped and +30 s clean release build (measured, LTO, ppt module only); six new crates, a fourth quick-xml (0.41 beside 0.37 and calamine's) and a second zip (8.6 beside 2); a second OLE2 implementation beside cfb |
Rejected — on behaviour, not on cost |
| LibreOffice as the only route | No Rust dependency | Rejected: it is the fidelity route (Phase 19 task 1), but architecture §7 keeps the main path free of a system dependency, so a native fallback is required |
Decision: not adopted. The cost is genuinely small — unlike ooxmlsdk in
docs/spikes/P1-pptx-strategy.md, which cost +28.7 MB — so the rejection rests entirely on what
the crate does with the corpus:
corpus/ppt/encrypted.pptreads back as two ordinary slides.CurrentUserAtom.headerToken(0xF3D1C4DF, [MS-PPT] 2.3.2) is never consulted anywhere in the crate; upstream issue #119 is an open feature request for exactly this. Phase 19's third acceptance criterion — encrypted/protected.pptrejected with a clear error — is therefore unreachable through it. What a caller can do from outside is read the token itself before handing the file to anything, which is what Phase 19-E does (docsai_office::refuse_if_encrypted, called in front of the choice of route, because LibreOffice ignores the token too). That closes the criterion; it does not rescue the crate, because the silent degradation below happens on files no pre-flight can tell apart from sound ones.corpus/ppt/truncated.pptreturnsOkwith one slide of master placeholder prompt text. WhenoffsetToCurrentEditdoes not resolve, the crate falls back to scanning for the highest-offsetUserEditAtom; on a deck missing half its stream that finds the master slide, and the caller cannot tell the result from a real presentation. That isAGENTS.md§7 rule 3, never degrade fidelity silently, on the fixture built to test it. It does not panic — memory safety is not the objection.- Its
.pptmodel is text-only and not extensible.TextRunis{ text_type, text }: no indent level (StyleTextPropAtomis a constant it never parses), no shape identity or geometry, and a slide's paragraphs\r-joined into one string. docsai converts intoPresentation → Slide → Shape, and the losses at that seam could not even be warned about accurately, because from outside the crate there is no way to know whether a given deck had a second bullet level to lose.
What the crate is good at is recorded rather than dismissed: it is MIT/Apache-2.0, actively
released, pure Rust, MSRV 1.88 (this workspace's floor), and its persist-directory walk is the
[MS-PPT] 2.1.2 algorithm done properly — bounds-checked, cycle-guarded, no panic on any fixture.
It is kept as a cross-check on record layout, the role ooxmlsdk was left in by spike P1, and
no code is derived from it. The decision is re-openable if upstream adds encryption detection and
a richer model; the four fixtures are the re-test.
Full evaluation, with the measurements and the verbatim probe output:
kb/99-phase-19-parser-choice.md.
- Mandatory pivot IR (
docsai-model): document tree with two possible roots (TextDocument,Workbook) — detail inarchitecture.md. N formats → 2N converters instead of N². - Styles by reference + delta: the IR stores
style_id+ direct properties, and the full style catalog travels in the front matter. Round-trip thus rebuilds a realstyles.xmland Markdown stays clean. - External assets with manifest: images to
assets/with deterministic names (content hash) so round-trip does not duplicate media. - Structured
ConversionReport: every conversion returns document + list of typed warnings (unsupported element, degradation, raw-block emitted). The CLI shows it; MCP returns it in the tool response. - Optional external fallbacks: LibreOffice headless only for
.doc, detected at runtime (--use-loffice=auto|never|require).
| # | Risk | Prob. | Impact | Mitigation |
|---|---|---|---|---|
| R1 | OOXML style cascade turns out more expensive than expected and delays Phase 1 | High | High | 1-week spike in Phase 0 with real documents; limit v1 to 4-level resolution without conditional tblStyle |
| R2 | No xlsx crate covers style reading in enough detail | Medium | Medium | Already assumed: complementary custom reading of xl/styles.xml with quick-xml (bounded effort, documented format) |
| R3 | Non-idempotent round-trip due to Markdown ambiguities (escapes, spaces) | Medium | High | Custom serializer with deterministic rules + idempotence test in CI from Phase 2 |
| R4 | .doc without LibreOffice disappoints users |
Medium | Low | Clear degraded-mode messages; document in README |
| R5 | OOXML/OpenFormula formula dialect divergence | High | Medium | v1 keeps original dialect + formula-dialect field; automatic translation postponed (Phase 9/backlog) |
| R6 | Third-party crates abandoned mid-project | Low | Medium | The four critical crates are among the most used in the ecosystem; IR design lets a reader be replaced without touching the rest |
| R7 | Binary size grows out of control | Low | Low | cargo bloat in CI, optional features, LTO in release |
| R8 | Diversity of image models (DrawingML vs VML vs Escher vs draw:frame) fragments effort | Medium | Medium | Normalized ImageGeometry model in the IR defined in Phase 0 (architecture §3.1); VML reading limited to model attributes; Escher only BLIPs on the native .doc path |
- Pandoc User's Guide — attribute, div, and front-matter syntax
- calamine (GitHub) and docs.rs/calamine
- umya-spreadsheet (GitHub) and comparison calamine vs umya-spreadsheet
- docx-rs (crates.io) · docx-rust (crates.io) · rdocx (lib.rs)
- Official Rust MCP SDK — rmcp and docs.rs/rmcp
- Converter comparisons: MarkItDown vs Pandoc, Docling vs MarkItDown, Real Python on MarkItDown
- ECMA-376 (OOXML), ISO/IEC 26300 (ODF), [MS-DOC]/[MS-XLS] Microsoft Open Specifications