DOCX SDT Handling#
Structured Document Tags (w:sdt) are OOXML content controls used by Word to wrap metadata fields, table-of-contents blocks, citation references, checkboxes, and other authored content. Docling's DOCX backend (MsWordDocumentBackend in docling/backend/msword_backend.py) handles them at two distinct levels:
| Level | OOXML location | Handler |
|---|---|---|
| Block-level | Direct child of w:body or w:sdtContent | _walk_linear() dispatch on tag_name == "sdt" |
| Inline (run-level) | w:sdt inside a w:p paragraph | _iter_paragraph_content() dispatch on tag_name == "sdt" |
Block-level SDT (_walk_linear)#
When _walk_linear() encounters a top-level sdt element, it:
- Finds the
./w:sdtContentchild (direct child XPath, not deep search). - Calls
self._walk_linear(sdt_content, doc)recursively, which re-enters the full element dispatch loop on the SDT's children.
This means tables, paragraphs, nested textboxes, and further SDT containers inside an SDT all flow through the same extraction paths as top-level body elements. Prior to PR #3657 (merged 2026-06-19), block-level SDTs used a shallow iteration over w:p children only; tables wrapped in w:sdtContent were silently flattened to text. The recursive _walk_linear call fixed this.
Common DOCX patterns that rely on this path: table-of-contents blocks, tracked-change containers, and any document section where Word wraps a table or multi-paragraph region in a content control.
Inline SDT (_iter_paragraph_content)#
When _iter_paragraph_content() sees a w:sdt child inside a paragraph element, it:
- Collects all
w:ttext nodes underw:sdtContentvia XPath.//w:sdtContent//w:t/text(), joining them into a single string. - If the text is empty, skips the element entirely.
- Reads the first
w:rrun insidew:sdtContentand calls_get_format_from_run()to get aFormattingobject (bold, italic, etc.). Falls back toNoneif there are no runs. - Appends
(text, fmt, None)to the content list — the same tuple shape used for regular runs and hyperlinks.
This means inline SDT text participates in run grouping in _get_paragraph_elements(), is included in _get_paragraph_text(), and flows through to TextItem nodes in the DoclingDocument exactly like a normal run.
Note on formatting: Only the format of the first run inside the SDT is used — multi-run SDTs with mixed formatting are flattened to the formatting of run 0.
Prior to PR #3280 (merged 2026-04-13), inline w:sdt elements were silently dropped from paragraph output. This commonly affected citation references and Word template metadata fields embedded as inline content controls.
Transparent wrapper stripping#
_iter_paragraph_content() also recursively unwraps smartTag, customXml, ins, and fldSimple elements before dispatching — so SDTs nested inside those wrappers are still reached.
Checkbox SDT (special case)#
Word 2010+ w14:checkbox elements, which appear inside w:sdt, are detected via dedicated helpers _has_checkbox(), _is_checkbox_checked(), and _get_checkbox_label(). These return DocItemLabel.CHECKBOX_SELECTED or DocItemLabel.CHECKBOX_UNSELECTED, and _clean_checkbox_symbols() strips any Unicode checkbox glyph (☐ ☑ ☒ □ ■ ▪ ▫) from the text. Checkbox detection was added in PR #3349 (merged 2026-04-28).
Related articles & primary sources#
| Resource | Notes |
|---|---|
msword_backend.py | Full backend implementation |
| PR #3280 | Inline SDT fix — _iter_paragraph_content changes |
| PR #3657 | Block SDT recursive walk fix |
| DOCX Text Formatting KB | _iter_paragraph_content run grouping context |