Skip to content

Commit e3a321d

Browse files
committed
comments: add Run.mark_comment_range()
1 parent af3b973 commit e3a321d

File tree

3 files changed

+51
-2
lines changed

3 files changed

+51
-2
lines changed

src/docx/oxml/text/run.py

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,11 @@
22

33
from __future__ import annotations
44

5-
from typing import TYPE_CHECKING, Callable, Iterator, List
5+
from typing import TYPE_CHECKING, Callable, Iterator, List, cast
66

77
from docx.oxml.drawing import CT_Drawing
88
from docx.oxml.ns import qn
9+
from docx.oxml.parser import OxmlElement
910
from docx.oxml.simpletypes import ST_BrClear, ST_BrType
1011
from docx.oxml.text.font import CT_RPr
1112
from docx.oxml.xmlchemy import BaseOxmlElement, OptionalAttribute, ZeroOrMore, ZeroOrOne
@@ -87,6 +88,19 @@ def iter_items() -> Iterator[str | CT_Drawing | CT_LastRenderedPageBreak]:
8788

8889
return list(iter_items())
8990

91+
def insert_comment_range_end_and_reference_below(self, comment_id: int) -> None:
92+
"""Insert a `w:commentRangeEnd` and `w:commentReference` element after this run.
93+
94+
The `w:commentRangeEnd` element is the immediate sibling of this `w:r` and is followed by
95+
a `w:r` containing the `w:commentReference` element.
96+
"""
97+
self.addnext(self._new_comment_reference_run(comment_id))
98+
self.addnext(OxmlElement("w:commentRangeEnd", attrs={qn("w:id"): str(comment_id)}))
99+
100+
def insert_comment_range_start_above(self, comment_id: int) -> None:
101+
"""Insert a `w:commentRangeStart` element with `comment_id` before this run."""
102+
self.addprevious(OxmlElement("w:commentRangeStart", attrs={qn("w:id"): str(comment_id)}))
103+
90104
@property
91105
def lastRenderedPageBreaks(self) -> List[CT_LastRenderedPageBreak]:
92106
"""All `w:lastRenderedPageBreaks` descendants of this run."""
@@ -132,6 +146,23 @@ def _insert_rPr(self, rPr: CT_RPr) -> CT_RPr:
132146
self.insert(0, rPr)
133147
return rPr
134148

149+
def _new_comment_reference_run(self, comment_id: int) -> CT_R:
150+
"""Return a new `w:r` element with `w:commentReference` referencing `comment_id`.
151+
152+
Should look like this:
153+
154+
<w:r>
155+
<w:rPr><w:rStyle w:val="CommentReference"/></w:rPr>
156+
<w:commentReference w:id="0"/>
157+
</w:r>
158+
159+
"""
160+
r = cast(CT_R, OxmlElement("w:r"))
161+
rPr = r.get_or_add_rPr()
162+
rPr.style = "CommentReference"
163+
r.append(OxmlElement("w:commentReference", attrs={qn("w:id"): str(comment_id)}))
164+
return r
165+
135166

136167
# ------------------------------------------------------------------------------------
137168
# Run inner-content elements

src/docx/text/run.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -178,7 +178,12 @@ def mark_comment_range(self, last_run: Run, comment_id: int) -> None:
178178
179179
`comment_id` identfies the comment that references this range.
180180
"""
181-
raise NotImplementedError
181+
# -- insert `w:commentRangeStart` with `comment_id` before this (first) run --
182+
self._r.insert_comment_range_start_above(comment_id)
183+
184+
# -- insert `w:commentRangeEnd` and `w:commentReference` run with `comment_id` after
185+
# -- `last_run`
186+
last_run._r.insert_comment_range_end_and_reference_below(comment_id)
182187

183188
@property
184189
def style(self) -> CharacterStyle:

tests/text/test_run.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from docx import types as t
1212
from docx.enum.style import WD_STYLE_TYPE
1313
from docx.enum.text import WD_BREAK, WD_UNDERLINE
14+
from docx.oxml.text.paragraph import CT_P
1415
from docx.oxml.text.run import CT_R
1516
from docx.parts.document import DocumentPart
1617
from docx.shape import InlineShape
@@ -122,6 +123,18 @@ def it_can_iterate_its_inner_content_items(
122123
actual = [type(item).__name__ for item in inner_content]
123124
assert actual == expected, f"expected: {expected}, got: {actual}"
124125

126+
def it_can_mark_a_comment_reference_range(self, paragraph_: Mock):
127+
p = cast(CT_P, element('w:p/w:r/w:t"referenced text"'))
128+
run = last_run = Run(p.r_lst[0], paragraph_)
129+
130+
run.mark_comment_range(last_run, comment_id=42)
131+
132+
assert p.xml == xml(
133+
'w:p/(w:commentRangeStart{w:id=42},w:r/w:t"referenced text"'
134+
",w:commentRangeEnd{w:id=42}"
135+
",w:r/(w:rPr/w:rStyle{w:val=CommentReference},w:commentReference{w:id=42}))"
136+
)
137+
125138
def it_knows_its_character_style(
126139
self, part_prop_: Mock, document_part_: Mock, paragraph_: Mock
127140
):

0 commit comments

Comments
 (0)