docx/modify-add-paragraph

Open bold-text.docx (1 paragraph, 1 bold run), append a new paragraph via Document.add_paragraph, save. Assert the new paragraph landed at the end of the body, the original paragraph is untouched (text + bold markers preserved, w14:paraId preserved), and w:sectPr remains the final child of w:body as Word requires.

Library verdicts: python-docx: — docxjs: —

Metadata

Feature iddocx/modify-add-paragraph
Formatdocx
Categorymodify-in-place
Spececma-376-5-part-1 § 17.2.2 w:body

XPath assertions

ID / partPredicateXPathpython-docxdocxjs
new-paragraph-present
word/document.xml
exist
The newly-appended paragraph carrying the mutation marker must be present.
//w:body/w:p/w:r/w:t[normalize-space()='W5-D appended paragraph']
paragraph-count-is-two
word/document.xml
match = ^2(\.0+)?$
Body must contain exactly 2 paragraphs (source had 1, we appended 1).
count(//w:body/w:p)
original-paragraph-preserved
word/document.xml
exist
The original 'Hello bold world' paragraph must still be the first paragraph.
//w:body/w:p[1]/w:r/w:t[normalize-space()='Hello bold world']
original-paragraph-bold-preserved
word/document.xml
exist
The original run's <w:b/> marker must still be present — add_paragraph must not touch sibling runs.
//w:body/w:p[1]/w:r[w:t[normalize-space()='Hello bold world']]/w:rPr/w:b
original-paragraph-bCs-preserved
word/document.xml
exist
The original run's <w:bCs/> complex-script bold marker must also be preserved.
//w:body/w:p[1]/w:r[w:t[normalize-space()='Hello bold world']]/w:rPr/w:bCs
original-paragraph-id-preserved
word/document.xml
equal = ADC5B890
w14:paraId on the original paragraph must be preserved byte-for-byte. Rewriting these IDs breaks Word's comment/tracked-change anchoring.
//w:body/w:p[1]/@w14:paraId
sectPr-is-last-body-child
word/document.xml
equal = sectPr
ECMA-376 requires w:sectPr to be the last child of w:body. add_paragraph must insert BEFORE sectPr, not after.
local-name(//w:body/*[last()])
sectPr-pgSz-preserved
word/document.xml
equal = 12240
Page size attributes (adjacent to the mutation) must be preserved byte-for-byte.
//w:body/w:sectPr/w:pgSz/@w:w
single-sectPr
word/document.xml
match = ^1(\.0+)?$
Exactly one sectPr — add_paragraph must not clone or move section properties.
count(//w:body/w:sectPr)

Render assertions

No render assertions declared.

Generator source

scripts/gen_modify_add_paragraph.py

#!/usr/bin/env python3
"""Generate ``fixtures/docx/bold-text--add-paragraph.docx``.

Thin wrapper around ``scripts.mutations.docx_add_paragraph``: opens the
committed ``fixtures/docx/bold-text.docx``, appends a paragraph, saves.
Satisfies the assertions in ``features/docx/modify-add-paragraph.json``.
"""

from __future__ import annotations

from pathlib import Path

from mutations import docx_add_paragraph

_REPO_ROOT = Path(__file__).resolve().parent.parent
_IN = _REPO_ROOT / "fixtures" / "docx" / "bold-text.docx"
_OUT = _REPO_ROOT / "fixtures" / "docx" / "bold-text--add-paragraph.docx"


def main() -> int:
    docx_add_paragraph(_IN, _OUT)
    print(_OUT)
    return 0


if __name__ == "__main__":
    raise SystemExit(main())

Manifest (expanded)

{
  "$schema": "../manifest.schema.json",
  "id": "docx/modify-add-paragraph",
  "title": "Modify: append paragraph to existing .docx",
  "format": "docx",
  "category": "modify-in-place",
  "summary": "Open bold-text.docx (1 paragraph, 1 bold run), append a new paragraph via Document.add_paragraph, save. Assert the new paragraph landed at the end of the body, the original paragraph is untouched (text + bold markers preserved, w14:paraId preserved), and w:sectPr remains the final child of w:body as Word requires.",
  "roles": [
    "authoring"
  ],
  "spec": {
    "source": "ecma-376-5-part-1",
    "clause": "17.2.2",
    "element": "w:body",
    "rnc_reference": "spec/ecma-376-5/part-1/rnc/WordprocessingML.rnc",
    "xsd_reference": "spec/ecma-376-5/part-1/xsd/wml.xsd",
    "notes": "Per ECMA-376 w:CT_Body, w:sectPr is the final child. Adding a paragraph must insert BEFORE the trailing sectPr, not after — some naive implementations append to the end of the body, producing an invalid document Word then silently repairs on open."
  },
  "fixtures": {
    "machine": "docx/bold-text--add-paragraph"
  },
  "modify": {
    "input_fixture": "fixtures/docx/bold-text.docx",
    "mutation": "docx_add_paragraph",
    "output_fixture": "fixtures/docx/bold-text--add-paragraph.docx"
  },
  "generator": {
    "python": "scripts/gen_modify_add_paragraph.py"
  },
  "assertions": [
    {
      "id": "new-paragraph-present",
      "part": "word/document.xml",
      "namespaces": {
        "w": "http://schemas.openxmlformats.org/wordprocessingml/2006/main"
      },
      "xpath": "//w:body/w:p/w:r/w:t[normalize-space()='W5-D appended paragraph']",
      "must": "exist",
      "description": "The newly-appended paragraph carrying the mutation marker must be present."
    },
    {
      "id": "paragraph-count-is-two",
      "part": "word/document.xml",
      "namespaces": {
        "w": "http://schemas.openxmlformats.org/wordprocessingml/2006/main"
      },
      "xpath": "count(//w:body/w:p)",
      "must": "match",
      "value": "^2(\\.0+)?$",
      "description": "Body must contain exactly 2 paragraphs (source had 1, we appended 1)."
    },
    {
      "id": "original-paragraph-preserved",
      "part": "word/document.xml",
      "namespaces": {
        "w": "http://schemas.openxmlformats.org/wordprocessingml/2006/main"
      },
      "xpath": "//w:body/w:p[1]/w:r/w:t[normalize-space()='Hello bold world']",
      "must": "exist",
      "description": "The original 'Hello bold world' paragraph must still be the first paragraph."
    },
    {
      "id": "original-paragraph-bold-preserved",
      "part": "word/document.xml",
      "namespaces": {
        "w": "http://schemas.openxmlformats.org/wordprocessingml/2006/main"
      },
      "xpath": "//w:body/w:p[1]/w:r[w:t[normalize-space()='Hello bold world']]/w:rPr/w:b",
      "must": "exist",
      "description": "The original run's <w:b/> marker must still be present — add_paragraph must not touch sibling runs."
    },
    {
      "id": "original-paragraph-bCs-preserved",
      "part": "word/document.xml",
      "namespaces": {
        "w": "http://schemas.openxmlformats.org/wordprocessingml/2006/main"
      },
      "xpath": "//w:body/w:p[1]/w:r[w:t[normalize-space()='Hello bold world']]/w:rPr/w:bCs",
      "must": "exist",
      "description": "The original run's <w:bCs/> complex-script bold marker must also be preserved."
    },
    {
      "id": "original-paragraph-id-preserved",
      "part": "word/document.xml",
      "namespaces": {
        "w": "http://schemas.openxmlformats.org/wordprocessingml/2006/main",
        "w14": "http://schemas.microsoft.com/office/word/2010/wordml"
      },
      "xpath": "//w:body/w:p[1]/@w14:paraId",
      "must": "equal",
      "value": "ADC5B890",
      "description": "w14:paraId on the original paragraph must be preserved byte-for-byte. Rewriting these IDs breaks Word's comment/tracked-change anchoring."
    },
    {
      "id": "sectPr-is-last-body-child",
      "part": "word/document.xml",
      "namespaces": {
        "w": "http://schemas.openxmlformats.org/wordprocessingml/2006/main"
      },
      "xpath": "local-name(//w:body/*[last()])",
      "must": "equal",
      "value": "sectPr",
      "description": "ECMA-376 requires w:sectPr to be the last child of w:body. add_paragraph must insert BEFORE sectPr, not after."
    },
    {
      "id": "sectPr-pgSz-preserved",
      "part": "word/document.xml",
      "namespaces": {
        "w": "http://schemas.openxmlformats.org/wordprocessingml/2006/main"
      },
      "xpath": "//w:body/w:sectPr/w:pgSz/@w:w",
      "must": "equal",
      "value": "12240",
      "description": "Page size attributes (adjacent to the mutation) must be preserved byte-for-byte."
    },
    {
      "id": "single-sectPr",
      "part": "word/document.xml",
      "namespaces": {
        "w": "http://schemas.openxmlformats.org/wordprocessingml/2006/main"
      },
      "xpath": "count(//w:body/w:sectPr)",
      "must": "match",
      "value": "^1(\\.0+)?$",
      "description": "Exactly one sectPr — add_paragraph must not clone or move section properties."
    }
  ]
}

Fixture

Download bold-text--add-paragraph.docx (18.6 KB)

Reference preview

No rendered reference is available for this case.

Spec notes

Per ECMA-376 w:CT_Body, w:sectPr is the final child. Adding a paragraph must insert BEFORE the trailing sectPr, not after — some naive implementations append to the end of the body, producing an invalid document Word then silently repairs on open.