docspec-docx-reader
Streaming DOCX to DocSpec event stream reader.
See the main DocSpec repository for documentation, architecture, and the event protocol.
Supported
- Paragraphs (
<w:p>) and direct text (<w:t>inside<w:r>) - Line breaks (
<w:br>, includingw:type="page"andw:type="column"— all emitLineBreak) - Tabs (
<w:tab>— emitted as aTextevent containing the single character"\t") - Tables (
<w:tbl>,<w:tr>,<w:tc>) — structural events. Horizontal cell merging via<w:gridSpan>is emitted ascolspan. Header rows via<w:trPr><w:tblHeader/></w:trPr>are emitted asStartTableHeader(withscope: Column) for cells in the contiguous header band at the top of the outermost table. Nested tables emit all cells asStartTableCell(header rows in nested tables are not supported). Vertical merging via<w:vMerge>is NOT yet supported — rowspan information is lost. - Run properties (
<w:rPr>):<w:b>(bold),<w:i>(italic),<w:u>(underline — anyw:valother thannone),<w:strike>(strikethrough),<w:dstrike>(double-strike, collapses to strikethrough),<w:vertAlign>(subscriptandsuperscriptonly;baselineresets to neither). These are emitted as deferredStartTextStyle { kind, id: None }/EndTextStylewrapper events around the first run content, not as fields onTextevents. Empty styled runs emit no style wrapper events; multiple<w:t>elements in one styled run share a single wrapper span. - Run color properties (
<w:rPr>):<w:color w:val="HEX">— foreground (text) color. Emitted asStartTextStyle { kind: TextColor(Color::Rgb { r, g, b }) }.w:val="auto"and non-hex values are silently dropped. Black(0,0,0)is preserved by the reader; whether to treat it as "default color" is writer policy.<w:highlight w:val="namedColor">— highlight color using the 17-entry ECMA-376 named palette. Emitted asStartTextStyle { kind: Mark(Color::Rgb { r, g, b }) }.w:val="none"and unknown names are silently dropped.<w:shd w:val="..." w:color="..." w:fill="...">— shading, used as a fallback highlight when<w:highlight>is absent. The visible color is resolved per ECMA-376 §17.3.5 / §17.18.78: forw:valofnilorclear(No Pattern) the visible color isw:fillalone; forw:val="solid"(100% Fill Pattern)w:colorcovers the area (falling back tow:fillwhen absent); for all other patterns (pct10…pct95,*Stripe,*Cross, etc.) the rendered color is approximated byw:fill. A missingw:valis malformed per the XSD and is defensively treated asclear. Emitted asStartTextStyle { kind: Mark(Color::Rgb { r, g, b }) }. An effective color of"auto"(consumer default per spec) or a missing attribute is silently dropped.
- Paragraph properties (
<w:pPr>):<w:jc>(alignment —left/startto Left,right/endto Right,centerto Center,both/distributeto Justify) - Heading-style paragraphs (
<w:pStyle>resolving toTitle→ level 1, or to any style name matchingheading Nfor N ≥ 1) emitStartHeading { level }/EndHeadinginstead ofStartParagraph/EndParagraph. Style inheritance viabasedOnis followed when resolving the style name. - Block-quote-style paragraphs (
<w:pStyle>resolving toQuote,Block Text,Block Quote,Block Quotation, orIntense Quote) emitStartBlockQuote/EndBlockQuoteinstead ofStartParagraph/EndParagraph. - Code-styled paragraphs (paragraph styles whose name resolves to
Source Code,SourceCode, orCodeblock) emitStartPreformatted/EndPreformatted. Consecutive code-styled paragraphs are consolidated into one preformatted block withLineBreakevents between paragraph boundaries. The character styleVerbatim Charis recognized as inline code at the run level only and never promotes a paragraph to preformatted. - Lists (
<w:p>with<w:numPr>): ordered (Decimal/LowerAlpha/UpperAlpha/LowerRoman/UpperRoman) and unordered (Disc) — emitted asStart*ListItem/End*ListItemwithid(numId stringified),level(ilvl),start, andstyle_type. Per-level classification: same numId can mix ordered and unordered levels. Any plain paragraph without<w:numPr>(including empty paragraphs) closes the open list immediately and emits as a top-level paragraph; styled non-numbered paragraphs still emit their dedicatedStartHeading,StartBlockQuote, orStartPreformattedevents instead. Counter continuity within a singlenumId/ilvlis preserved across intervening paragraphs per OOXML §17.9.17: the counter increments for each paragraph at that level "sequential or not". The first item of anumId/ilvlemitsstart: Some(1). Items that immediately follow the previous item at the same level with no intervening non-list content emitstart: None(sequential continuation). Items that resume after any break (table, paragraph, heading, or other non-list content between two items at the same level) emitstart: Some(N)where N is the effective counter value. Two list items with differentnumIds at the same level produce two distinct lists, each with its own counter starting at 1. The list also closes at a heading, block quote, preformatted block, table boundary, table-cell boundary, or end of document. - Empty
<w:rPr/>and<w:pPr/>are treated as no properties (default style / alignment None) - A
<w:rPr>or<w:pPr>that appears after content in the same parent is silently ignored (per the OOXML spec, both must be the first child element) - Hyperlinks (
<w:hyperlink>): resolved viaword/_rels/document.xml.relsand emitted asStartLink/EndLinkevents around inline content. Supports external URL targets (both Strict and Transitional OOXML relationship Type URIs), anchor-only links (w:anchorwithoutr:idemits#fragment), and tooltips (w:tooltip→StartLink.title, XML-decoded). When the relationship cannot be resolved, the link wrapper is dropped and content passes through as plain runs. - Structured Document Tags (
<w:sdt>) — the content of an SDT is emitted normally. The property containers<w:sdtPr>and<w:sdtEndPr>are dropped. - Tracked insertions and moves (
<w:ins>,<w:moveTo>) — the inserted/moved-in content is emitted (accept-changes semantics). - DrawingML images (
<w:drawing>) — emitted asEvent::Image. See Image Support below. - VML images (
<w:pict>containing<v:imagedata>) — emitted asEvent::Image. Alt text precedence:o:titleattribute on<v:imagedata>(non-empty) falls back toaltattribute on parent<v:shape>. Multiple<v:imagedata>in one<w:pict>emit multipleImageevents in document order.<v:group>wrappers are transparent. - Emits:
StartDocument,StartHeading,EndHeading,StartBlockQuote,EndBlockQuote,StartParagraph,StartPreformatted,StartTextStyle,Text,EndTextStyle,LineBreak,EndParagraph,EndPreformatted,StartTable,StartTableRow,StartTableCell,StartTableHeader,EndTableHeader,EndTableCell,EndTableRow,EndTable,StartLink,EndLink,StartOrderedListItem,EndOrderedListItem,StartUnorderedListItem,EndUnorderedListItem,Image,EndDocument - Symbol font character normalization for Wingdings, Wingdings 2, Wingdings 3, Webdings, and Symbol fonts — codepoints are mapped to their Unicode equivalents; unmapped codepoints are dropped
- Compression:
StoredandDeflatedonly - Suppressing empty headings, block quotes, and paragraphs: the reader emits these by design (see
empty_paragraph_still_emits_start_endand sibling tests intests/reader.rs). To hide emptyStartHeading/EndHeading,StartBlockQuote/EndBlockQuote, andStartParagraph/EndParagraphpairs in your pipeline, wrap the reader indocspec_core::SkipEmptyBlocks. ThedocspecCLI and HTTP server apply this filter by default; library consumers opt in by wrapping theirEventSource.
Color and Highlight Precedence
When both <w:highlight> and <w:shd w:fill> appear in the same <w:rPr>, <w:highlight> wins. The <w:shd> fill is ignored for that run.
No-Collapse Rule
Adjacent runs with the same color emit separate StartTextStyle/EndTextStyle pairs. The reader maintains per-run discipline and does not merge consecutive runs, even when their style properties are identical.
Out of Scope (subtree silently dropped)
The XML elements listed below are the reader's denylist — their entire subtree is silently dropped during parsing. Any element NOT listed (whether a known structural tag like <w:p> or an unknown extension) is processed normally; the reader just continues into its children.
<w:rStyle>(run-level style references) — silently dropped<w:pStyle>values other than the heading, block-quote, and code-style names listed in "Supported" — silently dropped; the paragraph emits asStartParagraphwith no style metadata- Run formatting not listed above:
<w:sz>,<w:szCs>,<w:caps>,<w:smallCaps>,<w:position>,<w:spacing>,<w:kern>,<w:lang>,<w:noProof> <w:rFonts>(general font tracking is not exposed as events, except for symbol font resolution (Wingdings, Wingdings 2, Wingdings 3, Webdings, Symbol) which is used internally to normalize codepoints to Unicode)themeColor/themeTint/themeShadeattributes on<w:color>and<w:shd>— silently dropped. The reader does not parsestyles.xmlortheme1.xml, so theme-referenced colors cannot be resolved. Future work.- Revision tracking (
<w:rPrChange>,<w:pPrChange>) - Advanced paragraph layout beyond alignment:
<w:ind>,<w:tabs>,<w:framePr>,<w:sectPr> <w:rPr>nested inside<w:pPr>(paragraph mark / pilcrow run properties)- BiDi-aware logical alignment (
start/endflipping based on paragraph direction is not tracked) - Math (
m:rPr) and DrawingML (a:rPr) namespaces - Vertical cell merging (
<w:vMerge>) — every cell still emits withrowspan: None; covered cells emit as ordinaryStartTableCellevents (visual merge is lost) - Header rows in nested tables — only the outermost table honors
<w:tblHeader> - Table-level property exceptions (
<w:tblPrEx>) — silently ignored (consistent with<w:tblPr>) - Table, row, and cell visual properties (
<w:tblPr>,<w:trPr>visual fields,<w:tcPr>visual fields,<w:tblGrid>) — silently dropped <mc:Fallback>(inside<mc:AlternateContent>) — subtree dropped to deduplicate images that appear in both the modern<w:drawing>(in<mc:Choice>) and the legacy<w:pict>(in<mc:Fallback>).- Comments, footnotes, headers, footers
- Document metadata
- Tracked deletions and moves-from (
<w:del>,<w:moveFrom>) — silently dropped (accept-changes semantics). Their text content uses<w:delText>which is not part of the reader's text-matching set. - Structured document tag properties (
<w:sdtPr>,<w:sdtEndPr>) — metadata containers; subtree dropped. - Field-code hyperlinks (
<w:fldChar>+<w:instrText>HYPERLINK ...): legacy form not currently supported; only the modern<w:hyperlink>element is recognized.
Lists (V1 cuts)
The following list features are intentionally out of scope for V1:
- No
<w:start>element parsing —startcomes from the effective per-numId/ilvlparagraph counter, not explicit OOXML start overrides - No
<w:lvlOverride>resolution — abstractNum's level definitions are authoritative - No picture bullets (
<w:lvlPicBulletId>) — picture-bullet levels emitDisc - No style-linked lists (
<w:numStyleLink>,<w:styleLink>) — fall back toDecimaldefaults <w:multiLevelType>is ignored — per-level<w:numFmt>is authoritative (§17.9.12)- No per-level marker text (
<w:lvlText>) — not parsed - No level-specific font, color, or indent
- No per-level resolution for synthesised phantom levels — when the first authored item appears at
ilvl > 0, intermediate levels inherit the target item's ordered/unordered classification and useDecimal/Discdefaults instead of resolving each phantom level's ownnumFmt
Image Support
<w:drawing> elements are parsed and emit Event::Image. The source field is one of two variants:
- Embedded image (
r:embed):ImageSource::Asset(handle)wherehandle.asset_id()returns"zip://word/media/image1.png". The asset ID is the resolved ZIP entry path with azip://scheme prefix. Callhandle.stream_to(&mut buf)to stream the raw bytes, orhandle.content_type()to get the MIME type. - External image (
r:link):ImageSource::Uri { uri: "<url>" }. The URL is passed through verbatim from the relationship target.
When both r:embed and r:link appear on the same blip, r:embed wins.
A relationship marked TargetMode="External" is honored even when referenced via r:embed — the reader emits ImageSource::Uri in that case, matching Word and LibreOffice behavior.
If the relationship ID cannot be resolved (missing or malformed rels), the reader emits ImageSource::Asset(handle) where handle.asset_id() returns the raw relationship ID with no zip:// prefix. Writers should apply their own missing-asset policy.
wp:docPr/@descr maps to Event::Image.alt. The title field is always None in this release.
VML images (<w:pict> containing <v:imagedata>) are also supported, using the same asset resolution as DrawingML images. V1 limitations: VML positioning attributes (style, w10:wrap, w10:anchorlock), <v:textbox> text content, and inline image bytes via <w:binData> (referenced from <v:imagedata src="wordml://..."/>) are silently dropped.
Asset Handles
Each ImageSource::Asset(handle) carries an Arc<dyn AssetHandle> that holds everything needed to resolve the asset. Call methods directly on the handle:
use ;
use ;
let mut reader = from_path?;
while let Some = reader.next_event?
# Ok::
Streaming Guarantee
Memory use depends on which constructor you call:
from_path: streamsword/document.xmlin constant memory — O(1) regardless of document size. Use this when processing large files or when memory is constrained.from_reader: buffersword/document.xmlinto memory — O(N) in document size. Usefrom_pathwhen constant memory is required.
In both cases, _rels/.rels and word/_rels/document.xml.rels are fully read into
memory at package-open time (typical combined size < 10 KB even for large documents).
The internal event queue remains bounded regardless of document size or hyperlink count.
Quick Start
use ;
let mut reader = from_path?;
while let Some = reader.next_event?
# Ok::
See Also
- MANIFESTO.md — philosophy and values
- ARCHITECTURE.md — pipeline design, event model decisions, and pointers to the in-code event reference
docspec_coreon docs.rs — every event variant, field, and well-formedness rule