diff --git a/src/docx/text/paragraph.py b/src/docx/text/paragraph.py index 234ea66cb..3d4d31e6c 100644 --- a/src/docx/text/paragraph.py +++ b/src/docx/text/paragraph.py @@ -5,6 +5,7 @@ from typing import TYPE_CHECKING, Iterator, List, cast from docx.enum.style import WD_STYLE_TYPE +from docx.oxml.ns import qn from docx.oxml.text.run import CT_R from docx.shared import StoryChild from docx.styles.style import ParagraphStyle @@ -159,13 +160,24 @@ def text(self) -> str: the text is mapped to a ```` element and each ``\\n`` or ``\\r`` character is mapped to a line break. Paragraph-level formatting, such as style, is preserved. All run-level formatting, such as bold or italic, is removed. + Comment range markers (`w:commentRangeStart` / `w:commentRangeEnd` and the + comment reference run) are reattached around the replacement run so comments + stay anchored. The range then covers that whole run, not the original + word span. Footnote references and tracked-change markup are still dropped. """ return self._p.text @text.setter def text(self, text: str | None): + comment_ids = [ + int(el.get(qn("w:id"))) + for el in self._p.xpath("./w:commentRangeStart") + if el.get(qn("w:id")) is not None + ] self.clear() - self.add_run(text) + run = self.add_run(text) + for comment_id in comment_ids: + run.mark_comment_range(run, comment_id) def _insert_paragraph_before(self): """Return a newly created paragraph, inserted directly before this paragraph.""" diff --git a/tests/text/test_paragraph.py b/tests/text/test_paragraph.py index 0329b1dd3..6ca9d1c1f 100644 --- a/tests/text/test_paragraph.py +++ b/tests/text/test_paragraph.py @@ -154,6 +154,28 @@ def it_can_replace_the_text_it_contains(self, text_set_fixture): paragraph.text = text assert paragraph.text == expected_text + def it_reattaches_comment_markers_when_text_is_assigned( + self, fake_parent: t.ProvidesStoryPart + ): + p = cast( + CT_P, + element( + 'w:p/(w:commentRangeStart{w:id=42},w:r/w:t"old"' + ",w:commentRangeEnd{w:id=42}" + ",w:r/(w:rPr/w:rStyle{w:val=CommentReference},w:commentReference{w:id=42}))" + ), + ) + paragraph = Paragraph(p, fake_parent) + + paragraph.text = "new" + + assert paragraph.text == "new" + assert p.xml == xml( + 'w:p/(w:commentRangeStart{w:id=42},w:r/w:t"new"' + ",w:commentRangeEnd{w:id=42}" + ",w:r/(w:rPr/w:rStyle{w:val=CommentReference},w:commentReference{w:id=42}))" + ) + def it_knows_its_alignment_value(self, alignment_get_fixture): paragraph, expected_value = alignment_get_fixture assert paragraph.alignment == expected_value