-
Notifications
You must be signed in to change notification settings - Fork 1.3k
Expand file tree
/
Copy pathcomments.py
More file actions
124 lines (97 loc) · 4.8 KB
/
comments.py
File metadata and controls
124 lines (97 loc) · 4.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
"""Custom element classes related to document comments."""
from __future__ import annotations
import datetime as dt
from typing import TYPE_CHECKING, Callable, cast
from docx.oxml.ns import nsdecls
from docx.oxml.parser import parse_xml
from docx.oxml.simpletypes import ST_DateTime, ST_DecimalNumber, ST_String
from docx.oxml.xmlchemy import BaseOxmlElement, OptionalAttribute, RequiredAttribute, ZeroOrMore
if TYPE_CHECKING:
from docx.oxml.table import CT_Tbl
from docx.oxml.text.paragraph import CT_P
class CT_Comments(BaseOxmlElement):
"""`w:comments` element, the root element for the comments part.
Simply contains a collection of `w:comment` elements, each representing a single comment. Each
contained comment is identified by a unique `w:id` attribute, used to reference the comment
from the document text. The offset of the comment in this collection is arbitrary; it is
essentially a _set_ implemented as a list.
"""
# -- type-declarations to fill in the gaps for metaclass-added methods --
comment_lst: list[CT_Comment]
comment = ZeroOrMore("w:comment")
def add_comment(self) -> CT_Comment:
"""Return newly added `w:comment` child of this `w:comments`.
The returned `w:comment` element is the minimum valid value, having a `w:id` value unique
within the existing comments and the required `w:author` attribute present but set to the
empty string. It's content is limited to a single run containing the necessary annotation
reference but no text. Content is added by adding runs to this first paragraph and by
adding additional paragraphs as needed.
"""
next_id = self._next_available_comment_id()
comment = cast(
CT_Comment,
parse_xml(
f'<w:comment {nsdecls("w")} w:id="{next_id}" w:author="">'
f" <w:p>"
f" <w:pPr>"
f' <w:pStyle w:val="CommentText"/>'
f" </w:pPr>"
f" <w:r>"
f" <w:rPr>"
f' <w:rStyle w:val="CommentReference"/>'
f" </w:rPr>"
f" <w:annotationRef/>"
f" </w:r>"
f" </w:p>"
f"</w:comment>"
),
)
self.append(comment)
return comment
def get_comment_by_id(self, comment_id: int) -> CT_Comment | None:
"""Return the `w:comment` element identified by `comment_id`, or |None| if not found."""
comment_elms = self.xpath(f"(./w:comment[@w:id='{comment_id}'])[1]")
return comment_elms[0] if comment_elms else None
def _next_available_comment_id(self) -> int:
"""The next available comment id.
According to the schema, this can be any positive integer, as big as you like, and the
default mechanism is to use `max() + 1`. However, if that yields a value larger than will
fit in a 32-bit signed integer, we take a more deliberate approach to use the first
ununsed integer starting from 0.
"""
used_ids = [int(x) for x in self.xpath("./w:comment/@w:id")]
next_id = max(used_ids, default=-1) + 1
if next_id <= 2**31 - 1:
return next_id
# -- fall-back to enumerating all used ids to find the first unused one --
for expected, actual in enumerate(sorted(used_ids)):
if expected != actual:
return expected
return len(used_ids)
class CT_Comment(BaseOxmlElement):
"""`w:comment` element, representing a single comment.
A comment is a so-called "story" and can contain paragraphs and tables much like a table-cell.
While probably most often used for a single sentence or phrase, a comment can contain rich
content, including multiple rich-text paragraphs, hyperlinks, images, and tables.
"""
# -- attributes on `w:comment` --
id: int = RequiredAttribute("w:id", ST_DecimalNumber) # pyright: ignore[reportAssignmentType]
author: str = RequiredAttribute("w:author", ST_String) # pyright: ignore[reportAssignmentType]
initials: str | None = OptionalAttribute( # pyright: ignore[reportAssignmentType]
"w:initials", ST_String
)
date: dt.datetime | None = OptionalAttribute( # pyright: ignore[reportAssignmentType]
"w:date", ST_DateTime
)
# -- children --
p = ZeroOrMore("w:p", successors=())
tbl = ZeroOrMore("w:tbl", successors=())
# -- type-declarations for methods added by metaclass --
add_p: Callable[[], CT_P]
p_lst: list[CT_P]
tbl_lst: list[CT_Tbl]
_insert_tbl: Callable[[CT_Tbl], CT_Tbl]
@property
def inner_content_elements(self) -> list[CT_P | CT_Tbl]:
"""Generate all `w:p` and `w:tbl` elements in this comment."""
return self.xpath("./w:p | ./w:tbl")