3.9 KiB
Revisions and Comments — XML details
Deep reference for docx_revisions.py and docx_comments.py. Read this
when you need to reason about the raw WordprocessingML, extend the
scripts, or debug an unusual document. Everyday use only needs SKILL.md.
Tracked changes (w:ins / w:del)
Word records run-level tracked changes as wrapper elements inside a
paragraph (w:p), in the w namespace
http://schemas.openxmlformats.org/wordprocessingml/2006/main:
<w:p>
<w:r><w:t>Base </w:t></w:r>
<w:ins w:id="1" w:author="Editor" w:date="2026-01-02T03:04:05Z">
<w:r><w:t>inserted text</w:t></w:r>
</w:ins>
<w:del w:id="2" w:author="Editor" w:date="2026-01-02T03:04:05Z">
<w:r><w:delText>deleted text</w:delText></w:r>
</w:del>
</w:p>
Key facts the script relies on:
- Deleted text lives in
w:delText, notw:t— that is why plain text extraction naturally shows the "accepted" view (insertions visible, deletions hidden). - Resolution semantics:
- accept
w:ins→ unwrap (move child runs up, drop the wrapper) - reject
w:ins→ remove the wrapper and its contents - accept
w:del→ remove the wrapper and its contents - reject
w:del→ rename eachw:delTexttow:t, then unwrap
- accept
- Revisions can appear anywhere block content is allowed: body, table
cells (nested tables too), headers, footers, text boxes. The script
iterates the body root plus every header/footer part root with
root.iter(W+"ins", W+"del"), which finds them at any depth. w:idvalues are unique per revision element, but one logical edit session may produce several elements.accept/reject --idacts on exactly the element(s) carrying that id.
Not handled by the script (detected by docx_read.py --revisions but
left alone): paragraph-mark revisions (w:rPr/w:ins on w:pPr), table
row insertions/deletions (w:trPr/w:ins), format-change records
(w:rPrChange, w:pPrChange), and moves (w:moveFrom/w:moveTo).
Moves are rare from typical editors; if present, treat the file with
Word itself rather than guessing.
Comments
Three cooperating pieces:
word/comments.xml— onew:commentelement per comment, carryingw:id,w:author,w:initials,w:date, and body paragraphs. Related from document.xml via the relationship type.../commentsand content typeapplication/vnd...wordprocessingml.comments+xml(also needs a[Content_Types].xmloverride — python-docx's part machinery adds it when the part is registered).- Range markers in the story —
w:commentRangeStart w:id="N"before the anchored runs,w:commentRangeEnd w:id="N"after them. - The reference run — a
w:rcontainingw:commentReference w:id="N", placed right after the range end; it ties the balloon to the location.
docx_comments.py behavior:
- list / delete always work at the XML level, so they handle files
from any producer.
anchored_textis reconstructed by walking each part root in document order and collectingw:ttext between the start and end markers for each id. - add first isolates the target text into whole runs. If the match
starts or ends mid-run, the run is split at the boundary (the split
copies
w:rPr, so formatting is preserved). Then:- python-docx >= 1.2: the native
document.add_comment(runs, ...)API is used (it creates the comments part, markers, and reference run itself). - older versions or
--xml: the script buildsword/comments.xml, registers the part + relationship through the opc layer, and inserts the markers/reference manually.
- python-docx >= 1.2: the native
- Deleting a comment removes the
w:commentelement and all three marker kinds for that id; the anchored document text is untouched.
Modern Word also writes commentsExtended.xml (threading/resolved
state). The scripts neither read nor produce it: replies and "resolved"
flags are invisible here, and comments added by this skill are plain
top-level comments.