docx/tracked-change-revision--moved-from

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: —

Metadata

Feature iddocx/tracked-change-revision--moved-from
Formatdocx
Categoryreview
Familydocx/tracked-change-revision
Axis valuesrevision=moved-from
Spececma-376-5-part-1 § 17.13.5 w:ins

XPath assertions

ID / partPredicateXPathpython-docxdocxjs
revision-element-present-moved-from
word/document.xml
exist
The fixture must contain at least one w:moveFrom element wrapping a run.
//w:moveFrom
revision-has-id-moved-from
word/document.xml
exist
The revision element must carry a @w:id attribute.
//w:moveFrom/@w:id
revision-has-author-moved-from
word/document.xml
equal = Reviewer
The revision element's @w:author must equal the expected author (Reviewer).
//w:moveFrom/@w:author

Render assertions

No render assertions declared.

Generator source

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())

Manifest (expanded)

{
  "$schema": "../manifest.schema.json",
  "id": "docx/tracked-change-revision--moved-from",
  "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--moved-from",
    "office": "docx/tracked-change-revision--moved-from"
  },
  "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": "moved-from"
    }
  },
  "assertions": [
    {
      "id": "revision-element-present-moved-from",
      "part": "word/document.xml",
      "namespaces": {
        "w": "http://schemas.openxmlformats.org/wordprocessingml/2006/main"
      },
      "xpath": "//w:moveFrom",
      "must": "exist",
      "description": "The fixture must contain at least one w:moveFrom element wrapping a run."
    },
    {
      "id": "revision-has-id-moved-from",
      "part": "word/document.xml",
      "namespaces": {
        "w": "http://schemas.openxmlformats.org/wordprocessingml/2006/main"
      },
      "xpath": "//w:moveFrom/@w:id",
      "must": "exist",
      "description": "The revision element must carry a @w:id attribute."
    },
    {
      "id": "revision-has-author-moved-from",
      "part": "word/document.xml",
      "namespaces": {
        "w": "http://schemas.openxmlformats.org/wordprocessingml/2006/main"
      },
      "xpath": "//w:moveFrom/@w:author",
      "must": "equal",
      "value": "Reviewer",
      "description": "The revision element's @w:author must equal the expected author (Reviewer)."
    }
  ]
}

Fixture

Download tracked-change-revision--moved-from.docx (18.6 KB)

Reference preview

No rendered reference is available for this case.

Spec 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.