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: —
| Feature id | docx/modify-add-paragraph |
|---|---|
| Format | docx |
| Category | modify-in-place |
| Spec | ecma-376-5-part-1 § 17.2.2 w:body |
| ID / part | Predicate | XPath | python-docx | docxjs |
|---|---|---|---|---|
new-paragraph-presentword/document.xml | existThe 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-twoword/document.xml | match = ^2(\.0+)?$Body must contain exactly 2 paragraphs (source had 1, we appended 1). | count(//w:body/w:p) | — | — |
original-paragraph-preservedword/document.xml | existThe 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-preservedword/document.xml | existThe 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-preservedword/document.xml | existThe 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-preservedword/document.xml | equal = ADC5B890w14: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-childword/document.xml | equal = sectPrECMA-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-preservedword/document.xml | equal = 12240Page size attributes (adjacent to the mutation) must be preserved byte-for-byte. | //w:body/w:sectPr/w:pgSz/@w:w | — | — |
single-sectPrword/document.xml | match = ^1(\.0+)?$Exactly one sectPr — add_paragraph must not clone or move section properties. | count(//w:body/w:sectPr) | — | — |
No render assertions declared.
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())
{
"$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."
}
]
}
No rendered reference is available for this case.