55
66from __future__ import annotations
77
8- from typing import IO , TYPE_CHECKING , Iterator , List
8+ from typing import IO , TYPE_CHECKING , Iterator , List , Sequence
99
1010from docx .blkcntnr import BlockItemContainer
1111from docx .enum .section import WD_SECTION
1212from docx .enum .text import WD_BREAK
1313from docx .section import Section , Sections
1414from docx .shared import ElementProxy , Emu , Inches , Length
15+ from docx .text .run import Run
1516
1617if TYPE_CHECKING :
1718 import docx .types as t
18- from docx .comments import Comments
19+ from docx .comments import Comment , Comments
1920 from docx .oxml .document import CT_Body , CT_Document
2021 from docx .parts .document import DocumentPart
2122 from docx .settings import Settings
@@ -37,6 +38,51 @@ def __init__(self, element: CT_Document, part: DocumentPart):
3738 self ._part = part
3839 self .__body = None
3940
41+ def add_comment (
42+ self , runs : Run | Sequence [Run ], text : str , author : str = "" , initials : str | None = ""
43+ ) -> Comment :
44+ """Add a comment to the document, anchored to the specified runs.
45+
46+ `runs` can be a single `Run` object or a non-empty sequence of `Run` objects. Only the
47+ first and last run of a sequence are used, it's just more convenient to pass a whole
48+ sequence when that's what you have handy, like `paragraph.runs` for example. When `runs`
49+ contains a single `Run` object, that run serves as both the first and last run.
50+
51+ A comment can be anchored only on an even run boundary, meaning the text the comment
52+ "references" must be a non-zero integer number of consecutive runs. The runs need not be
53+ _contiguous_ per se, like the first can be in one paragraph and the last in the next
54+ paragraph, but all runs between the first and the last will be included in the reference.
55+
56+ The comment reference range is delimited by placing a `w:commentRangeStart` element before
57+ the first run and a `w:commentRangeEnd` element after the last run. This is why only the
58+ first and last run are required and why a single run can serve as both first and last.
59+ Word works out which text to highlight in the UI based on these range markers.
60+
61+ `text` allows the contents of a simple comment to be provided in the call, providing for
62+ the common case where a comment is a single phrase or sentence without special formatting
63+ such as bold or italics. More complex comments can be added using the returned `Comment`
64+ object in much the same way as a `Document` or (table) `Cell` object, using methods like
65+ `.add_paragraph()`, .add_run()`, etc.
66+
67+ The `author` and `initials` parameters allow that metadata to be set for the comment.
68+ `author` is a required attribute on a comment and is the empty string by default.
69+ `initials` is optional on a comment and may be omitted by passing |None|, but Word adds an
70+ `initials` attribute by default and we follow that convention by using the empty string
71+ when no `initials` argument is provided.
72+ """
73+ # -- normalize `runs` to a sequence of runs --
74+ runs = [runs ] if isinstance (runs , Run ) else runs
75+ first_run = runs [0 ]
76+ last_run = runs [- 1 ]
77+
78+ # -- Note that comments can only appear in the document part --
79+ comment = self .comments .add_comment (text = text , author = author , initials = initials )
80+
81+ # -- let the first run orchestrate placement of the comment range start and end --
82+ first_run .mark_comment_range (last_run , comment .comment_id )
83+
84+ return comment
85+
4086 def add_heading (self , text : str = "" , level : int = 1 ):
4187 """Return a heading paragraph newly added to the end of the document.
4288
0 commit comments