diff --git a/features/doc-add-comment.feature b/features/doc-add-comment.feature index 36f46244a..332ea6f7b 100644 --- a/features/doc-add-comment.feature +++ b/features/doc-add-comment.feature @@ -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 diff --git a/features/steps/comments.py b/features/steps/comments.py index 39680f257..2f6aa13e9 100644 --- a/features/steps/comments.py +++ b/features/steps/comments.py @@ -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 ===================================================== @@ -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}" + ) diff --git a/src/docx/document.py b/src/docx/document.py index 73757b46d..108994aa3 100644 --- a/src/docx/document.py +++ b/src/docx/document.py @@ -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) diff --git a/src/docx/styles/styles.py b/src/docx/styles/styles.py index b05b3ebb1..f69bc070d 100644 --- a/src/docx/styles/styles.py +++ b/src/docx/styles/styles.py @@ -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 @@ -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 `` 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`.