A family covering the four run-level revision wrappers: <w:ins> (insertion), <w:del> (deletion), <w:moveFrom> (move source), and <w:moveTo> (move destination).
Library verdicts: python-docx: — docxjs: —
| Feature id | docx/tracked-change-revision--deleted-run |
|---|---|
| Format | docx |
| Category | review |
| Family | docx/tracked-change-revision |
| Axis values | revision=deleted-run |
| Spec | ecma-376-5-part-1 § 17.13.5 w:ins |
| ID / part | Predicate | XPath | python-docx | docxjs |
|---|---|---|---|---|
revision-element-present-deleted-runword/document.xml | existThe fixture must contain at least one w:del element wrapping a run. | //w:del | — | — |
revision-has-id-deleted-runword/document.xml | existThe revision element must carry a @w:id attribute. | //w:del/@w:id | — | — |
revision-has-author-deleted-runword/document.xml | equal = ReviewerThe revision element's @w:author must equal the expected author (Reviewer). | //w:del/@w:author | — | — |
No render assertions declared.
scripts/gen_tracked_change_revision.py
#!/usr/bin/env python3
"""Generate ``fixtures/docx/tracked-change-revision--<kind>.docx`` fixtures.
Each run builds a single paragraph whose (one) run is wrapped in a
run-level revision element selected by ``--kind``:
- ``inserted-run`` — ``<w:ins>``
- ``deleted-run`` — ``<w:del>``
- ``moved-from`` — ``<w:moveFrom>``
- ``moved-to`` — ``<w:moveTo>``
The ``w:ins`` case uses the fork's ``Document.tracked_changes(author, date)``
context manager, which wraps every run added inside the ``with`` in a
``<w:ins>`` with the supplied author/date. The other three kinds are
assembled at the XML layer directly (OxmlElement + QName) because the fork
exposes no high-level ``del`` / ``moveFrom`` / ``moveTo`` writer.
Deletions and move-sources use ``w:delText`` rather than ``w:t`` per the
OOXML schema (17.13.5.4, 17.13.5.15). ``moveFrom`` / ``moveTo`` both carry
an ``@w:name`` so the two halves can be paired up; the fixtures pin
``move-fixture`` for both sides so the value is stable.
Dates are fixed to ``2026-01-01T00:00:00Z`` and the ``doc.save(reproducible=True)``
call ensures the emitted package is byte-identical across runs.
"""
from __future__ import annotations
import argparse
import datetime as dt
from pathlib import Path
from docx import Document
from docx.oxml.ns import qn
from docx.oxml.parser import OxmlElement
_REPO_ROOT = Path(__file__).resolve().parent.parent
_FIXED_DATE = dt.datetime(2026, 1, 1, 0, 0, 0, tzinfo=dt.timezone.utc)
_FIXED_DATE_STR = "2026-01-01T00:00:00Z"
_MOVE_NAME = "move-fixture"
def _build_inserted_run(doc: Document) -> None:
"""Use the fork's tracked_changes() context manager to emit a <w:ins>."""
with doc.tracked_changes(author="Reviewer", date=_FIXED_DATE):
p = doc.add_paragraph()
p.add_run("Inserted text")
def _wrap_run_in_revision(
doc: Document,
tag: str,
text: str,
author: str,
rev_id: int,
use_del_text: bool,
name: str | None = None,
) -> None:
"""Build a paragraph whose run is wrapped in a run-level revision element.
``tag`` is the revision wrapper element, e.g. ``w:del``, ``w:moveFrom``,
``w:moveTo``. When ``use_del_text`` is True the inner run's ``<w:t>`` is
retagged to ``<w:delText>`` (required for w:del and w:moveFrom per the
schema). ``name`` sets ``@w:name`` on the wrapper — used by the paired
move elements.
"""
p = doc.add_paragraph()
r = p.add_run(text)
r_elm = r._r
p_elm = p._p
if use_del_text:
for t in r_elm.findall(qn("w:t")):
t.tag = qn("w:delText")
wrapper = OxmlElement(tag)
wrapper.set(qn("w:id"), str(rev_id))
wrapper.set(qn("w:author"), author)
wrapper.set(qn("w:date"), _FIXED_DATE_STR)
if name is not None:
wrapper.set(qn("w:name"), name)
idx = list(p_elm).index(r_elm)
p_elm.remove(r_elm)
p_elm.insert(idx, wrapper)
wrapper.append(r_elm)
def _build_deleted_run(doc: Document) -> None:
_wrap_run_in_revision(
doc,
tag="w:del",
text="Deleted text",
author="Reviewer",
rev_id=100,
use_del_text=True,
)
def _build_moved_from(doc: Document) -> None:
_wrap_run_in_revision(
doc,
tag="w:moveFrom",
text="Moved source text",
author="Reviewer",
rev_id=200,
use_del_text=True,
name=_MOVE_NAME,
)
def _build_moved_to(doc: Document) -> None:
_wrap_run_in_revision(
doc,
tag="w:moveTo",
text="Moved dest text",
author="Reviewer",
rev_id=201,
use_del_text=False,
name=_MOVE_NAME,
)
_KINDS = {
"inserted-run": _build_inserted_run,
"deleted-run": _build_deleted_run,
"moved-from": _build_moved_from,
"moved-to": _build_moved_to,
}
def _write(kind: str, out: Path) -> None:
if kind not in _KINDS:
raise SystemExit(
"Unknown revision kind %r; expected one of %s"
% (kind, ", ".join(sorted(_KINDS)))
)
doc = Document()
_KINDS[kind](doc)
out.parent.mkdir(parents=True, exist_ok=True)
doc.save(out, reproducible=True)
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument(
"--kind",
default="inserted-run",
choices=sorted(_KINDS),
help="Which revision-wrapper element to emit.",
)
parser.add_argument(
"--out",
default=str(
_REPO_ROOT / "fixtures" / "docx" / "tracked-change-revision.docx"
),
)
args = parser.parse_args()
out_path = Path(args.out)
if not out_path.is_absolute():
out_path = _REPO_ROOT / out_path
_write(args.kind, out_path)
print(out_path)
return 0
if __name__ == "__main__":
raise SystemExit(main())
{
"$schema": "../manifest.schema.json",
"id": "docx/tracked-change-revision--deleted-run",
"kind": "literal",
"title": "Tracked-change revision (ins / del / moveFrom / moveTo)",
"format": "docx",
"category": "review",
"summary": "A family covering the four run-level revision wrappers: <w:ins> (insertion), <w:del> (deletion), <w:moveFrom> (move source), and <w:moveTo> (move destination).",
"spec": {
"source": "ecma-376-5-part-1",
"clause": "17.13.5",
"element": "w:ins",
"rnc_reference": "spec/ecma-376-5/part-1/rnc/WordprocessingML.rnc",
"xsd_reference": "spec/ecma-376-5/part-1/xsd/wml.xsd",
"notes": "Each expanded case produces a paragraph whose run is wrapped in one of the four run-level revision elements. w:ins and w:moveTo carry runs with <w:t> child text; w:del and w:moveFrom carry runs with <w:delText>. All four require @w:id and @w:author. The fork's Document.tracked_changes(author, date) context manager handles the w:ins case; the other three are assembled at the XML level via OxmlElement since the fork has no high-level del / moveFrom / moveTo writer. moveFrom and moveTo additionally set @w:name so the two halves can pair up; the fixtures pin the names 'move-fixture' / 'move-fixture' so the assertion values are stable. Dates are fixed to 2026-01-01T00:00:00Z to keep the fixtures byte-reproducible."
},
"fixtures": {
"machine": "docx/tracked-change-revision--deleted-run",
"office": "docx/tracked-change-revision--deleted-run"
},
"generator": {
"python": "scripts/gen_tracked_change_revision.py",
"arg_template": "--kind {revision.id} --out fixtures/docx/tracked-change-revision--{revision.id}.docx"
},
"_expansion": {
"parent_id": "docx/tracked-change-revision",
"bindings": {
"revision": "deleted-run"
}
},
"assertions": [
{
"id": "revision-element-present-deleted-run",
"part": "word/document.xml",
"namespaces": {
"w": "http://schemas.openxmlformats.org/wordprocessingml/2006/main"
},
"xpath": "//w:del",
"must": "exist",
"description": "The fixture must contain at least one w:del element wrapping a run."
},
{
"id": "revision-has-id-deleted-run",
"part": "word/document.xml",
"namespaces": {
"w": "http://schemas.openxmlformats.org/wordprocessingml/2006/main"
},
"xpath": "//w:del/@w:id",
"must": "exist",
"description": "The revision element must carry a @w:id attribute."
},
{
"id": "revision-has-author-deleted-run",
"part": "word/document.xml",
"namespaces": {
"w": "http://schemas.openxmlformats.org/wordprocessingml/2006/main"
},
"xpath": "//w:del/@w:author",
"must": "equal",
"value": "Reviewer",
"description": "The revision element's @w:author must equal the expected author (Reviewer)."
}
]
}
No rendered reference is available for this case.