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
5 changes: 5 additions & 0 deletions features/doc-add-comment.feature
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,8 @@ Feature: Add a comment to a document
And comment.text == "A comment"
And comment.author == "John Doe"
And comment.initials == "JD"

Scenario: Document.add_comment() registers the CommentReference character style
Given a new document without a CommentReference style
When I assign comment = document.add_comment(runs, "A comment", "John Doe", "JD")
Then the document styles include a "CommentReference" character style
18 changes: 18 additions & 0 deletions features/steps/comments.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,12 @@ def given_a_document_having_no_comments_part(context: Context):
context.document = Document(test_docx("doc-default"))


@given("a new document without a CommentReference style")
def given_a_new_document_without_a_comment_reference_style(context: Context):
context.document = Document()
context.document.add_paragraph("Test paragraph for comment anchor.")


# when =====================================================


Expand Down Expand Up @@ -282,3 +288,15 @@ def then_the_result_is_a_comment_object_with_id_2(context: Context):
comment = context.comment
assert type(comment) is Comment, f"expected a Comment object, got {type(comment)}"
assert comment.comment_id == 2, f"expected comment_id `2`, got '{comment.comment_id}'"


@then('the document styles include a "CommentReference" character style')
def then_document_styles_include_comment_reference_style(context: Context):
from docx.enum.style import WD_STYLE_TYPE

styles = context.document.styles
style = styles._element.get_by_id("CommentReference")
assert style is not None, "expected 'CommentReference' style in styles.xml, but it is absent"
assert style.type == WD_STYLE_TYPE.CHARACTER, (
f"expected CHARACTER style, got {style.type}"
)
3 changes: 3 additions & 0 deletions src/docx/document.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,9 @@ def add_comment(
first_run = runs[0]
last_run = runs[-1]

# -- guarantee the 'CommentReference' character style is defined in styles.xml --
self.styles._ensure_comment_reference_style()

# -- Note that comments can only appear in the document part --
comment = self.comments.add_comment(text=text, author=author, initials=initials)

Expand Down
27 changes: 26 additions & 1 deletion src/docx/styles/styles.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

from docx.enum.style import WD_STYLE_TYPE
from docx.oxml.styles import CT_Styles
from docx.shared import ElementProxy
from docx.shared import ElementProxy, Pt
from docx.styles import BabelFish
from docx.styles.latent import LatentStyles
from docx.styles.style import BaseStyle, StyleFactory
Expand Down Expand Up @@ -115,6 +115,31 @@ def _get_by_id(self, style_id: str | None, style_type: WD_STYLE_TYPE):
return self.default(style_type)
return StyleFactory(style)

def _ensure_comment_reference_style(self) -> None:
"""Add the built-in 'CommentReference' character style if not present.

`Document.add_comment()` writes `<w:rStyle w:val="CommentReference"/>` into the
reference run but never adds the corresponding style definition to styles.xml.
Word silently tolerates the dangling reference, but the reference mark then
inherits default run formatting instead of the intended 8-pt "annotation
reference" appearance. This method adds the style once per document, skipping
the work on subsequent calls.
"""
if self._element.get_by_id("CommentReference") is not None:
return
style_elm = self._element.add_style_of_type(
"annotation reference", WD_STYLE_TYPE.CHARACTER, True
)
# The auto-generated styleId from the name would be "annotationreference";
# override it to match the id that the reference run already uses.
style_elm.styleId = "CommentReference"
style_elm.basedOn_val = "DefaultParagraphFont"
style_elm.uiPriority_val = 99
style_elm.semiHidden_val = True
style_elm.unhideWhenUsed_val = True
rPr = style_elm.get_or_add_rPr()
rPr.sz_val = Pt(8)

def _get_style_id_from_name(self, style_name: str, style_type: WD_STYLE_TYPE) -> str | None:
"""Return the id of the style of `style_type` corresponding to `style_name`.

Expand Down