Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion src/docx/text/paragraph.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -159,13 +160,24 @@ def text(self) -> str:
the text is mapped to a ``<w:tab/>`` 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."""
Expand Down
22 changes: 22 additions & 0 deletions tests/text/test_paragraph.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down