Skip to content

Commit a7c6cb9

Browse files
committed
fix(comments): emit required <a:bodyPr> in <p188:txBody> (maintainer UAT)
Maintainer opened uat_issue25_comments.pptx in PowerPoint: file opened clean (no repair) but ZERO comments displayed. Root cause: the add path builds the body via get_or_add_txBody() (a bare <p188:txBody/>) then sets text via _set_txBody_text, which only PRESERVED a pre-existing <a:bodyPr> and never created one. <p188:txBody> is a DrawingML a:CT_TextBody whose schema REQUIRES <a:bodyPr> as first child; PowerPoint silently drops comments with a bodyPr-less txBody — no repair dialog, the comment is just absent. The entire green trinity + 17/17 UAT could not see this; only maintainer visual review did. Fix: _set_txBody_text now GUARANTEES a leading <a:bodyPr/> (inserts it when absent) for both comments and replies. Added DescribeThreadedCommentBodyPrRegression (2 tests) asserting every txBody carries <a:bodyPr> before <a:p>, so the silent-drop class is now test-caught. uat_issue25.py OUT path moved under ./uat/ per CLAUDE.md §6a update. Trinity: pytest 3753, ruff clean, behave 1062, 0 failed. Refs #25
1 parent 9fa2ff9 commit a7c6cb9

2 files changed

Lines changed: 58 additions & 4 deletions

File tree

src/pptx/oxml/comments.py

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -54,13 +54,19 @@ def _txBody_text(txBody) -> str:
5454
def _set_txBody_text(txBody, value: str) -> None:
5555
"""Replace `txBody`'s body with a single paragraph/run holding `value`.
5656
57-
Existing ``a:p`` children are removed and one fresh
58-
``<a:p><a:r><a:t>value</a:t></a:r></a:p>`` is appended after the
59-
(preserved) ``<a:bodyPr>``. This is the minimal round-trip-safe shape
60-
PowerPoint accepts for a threaded-comment body.
57+
``<p188:txBody>`` is a DrawingML ``a:CT_TextBody`` whose schema REQUIRES
58+
``<a:bodyPr>`` as its first child. A txBody without it is silently
59+
dropped by PowerPoint's threaded-comment reader — the comment never
60+
appears in the Comments pane (no repair dialog, just absent). The
61+
add-path builds the txBody via ``get_or_add_txBody()`` (a bare
62+
``<p188:txBody/>``), so this function must GUARANTEE the leading
63+
``<a:bodyPr/>``, not merely preserve a pre-existing one. A trailing
64+
``<a:p><a:r><a:t>value</a:t></a:r></a:p>`` follows it.
6165
"""
6266
for p in list(txBody.findall(qn("a:p"))):
6367
txBody.remove(p)
68+
if txBody.find(qn("a:bodyPr")) is None:
69+
txBody.insert(0, parse_xml('<a:bodyPr xmlns:a="%s"/>' % _A_NS))
6470
p = parse_xml(
6571
'<a:p xmlns:a="%s"><a:r><a:t>%s</a:t></a:r></a:p>' % (_A_NS, _escape_xml_text(value))
6672
)

tests/test_issue25_comments.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -626,5 +626,53 @@ def it_treats_a_legacy_comment_replies_as_empty_read_only(self):
626626
assert list(legacy.replies) == []
627627

628628

629+
class DescribeThreadedCommentBodyPrRegression:
630+
"""Regression: <p188:txBody> MUST carry <a:bodyPr> (issue #25 silent-drop).
631+
632+
The add-path builds txBody via get_or_add_txBody() (a bare element);
633+
a CT_TextBody without <a:bodyPr> is schema-malformed and PowerPoint
634+
SILENTLY drops the comment (no repair dialog, comment just absent —
635+
caught only by maintainer visual review). Trinity was green while
636+
every comment was invisible. This locks the fix.
637+
"""
638+
639+
def _modern_xml(self):
640+
import io
641+
import zipfile
642+
643+
from pptx import Presentation
644+
645+
prs = Presentation()
646+
s = prs.slides.add_slide(prs.slide_layouts[1])
647+
c = s.comments.add("Body check", author="Rev")
648+
c.replies.add("reply body check", author="Rev2")
649+
buf = io.BytesIO()
650+
prs.save(buf)
651+
buf.seek(0)
652+
z = zipfile.ZipFile(buf)
653+
name = next(n for n in z.namelist() if "modernComment" in n)
654+
return z.read(name).decode()
655+
656+
def it_emits_bodyPr_in_every_comment_txBody(self):
657+
import re
658+
659+
xml = self._modern_xml()
660+
bodies = re.findall(r"<p188:txBody>(.*?)</p188:txBody>", xml, re.S)
661+
assert bodies, "expected at least one <p188:txBody>"
662+
assert all("bodyPr" in b for b in bodies), (
663+
"every threaded-comment/reply txBody must contain <a:bodyPr> "
664+
"or PowerPoint silently drops the comment"
665+
)
666+
667+
def it_places_bodyPr_before_the_paragraph(self):
668+
import re
669+
670+
xml = self._modern_xml()
671+
body = re.search(r"<p188:txBody>(.*?)</p188:txBody>", xml, re.S).group(1)
672+
assert body.index("bodyPr") < body.index("<a:p"), (
673+
"<a:bodyPr> must precede <a:p> (CT_TextBody child order)"
674+
)
675+
676+
629677
if __name__ == "__main__":
630678
raise SystemExit(pytest.main([__file__, "-q"]))

0 commit comments

Comments
 (0)