From 9982f851fea36c96f82cae3600bd254626dab81f Mon Sep 17 00:00:00 2001 From: skelmis Date: Sun, 15 Jun 2025 22:08:43 +1200 Subject: [PATCH 1/4] feat: implement bookmarks and internal hyperlinks --- HISTORY.rst | 6 +++ README.md | 2 + src/skelmis/docx/text/paragraph.py | 77 ++++++++++++++++++++++++++++++ 3 files changed, 85 insertions(+) diff --git a/HISTORY.rst b/HISTORY.rst index 8c92ccf..70e3c82 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -3,6 +3,12 @@ Release History --------------- +2.3.0 (2025-06-15) +++++++++++++++++++ + +* Implement support to add bookmarks in a paragraph +* Implement support for creating internal hyperlinks to bookmarks + 2.2.2 (2025-06-15) ++++++++++++++++++ diff --git a/README.md b/README.md index 124a6f1..9198016 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,8 @@ Key differences at a glance: - Supporting the ability to transform word documents into PDF's ([1](https://skelmis-docx.readthedocs.io/en/latest/api/utility.html#skelmis.docx.utility.document_to_pdf)) - Horizontal rules + paragraph bounding boxes / borders ([1](https://skelmis-docx.readthedocs.io/en/latest/api/text.html#skelmis.docx.text.paragraph.Paragraph.insert_horizontal_rule), [2](https://skelmis-docx.readthedocs.io/en/latest/api/text.html#skelmis.docx.text.paragraph.Paragraph.draw_paragraph_border)) - External hyperlinks ([1](https://skelmis-docx.readthedocs.io/en/latest/api/text.html#skelmis.docx.text.paragraph.Paragraph.add_external_hyperlink)) +- Internal hyperlinks (Linking to bookmarks) ([1](https://skelmis-docx.readthedocs.io/en/latest/api/text.html#skelmis.docx.text.paragraph.Paragraph.add_internal_hyperlink)) +- Creating bookmarks ([1](https://skelmis-docx.readthedocs.io/en/latest/api/text.html#skelmis.docx.text.paragraph.Paragraph.add_bookmark)) - The ability to insert a customisable Table of Contents (ToC) ([1](https://skelmis-docx.readthedocs.io/en/latest/api/text.html#skelmis.docx.text.paragraph.Paragraph.insert_table_of_contents)) ## Installation diff --git a/src/skelmis/docx/text/paragraph.py b/src/skelmis/docx/text/paragraph.py index 17d1755..d6c673d 100644 --- a/src/skelmis/docx/text/paragraph.py +++ b/src/skelmis/docx/text/paragraph.py @@ -32,6 +32,83 @@ def __init__(self, p: CT_P, parent: t.ProvidesStoryPart): super(Paragraph, self).__init__(parent) self._p = self._element = p + def add_internal_hyperlink( + self, + bookmark_name: str, + display_text: str, + tool_tip: str | None = None, + ): + """Add an internal hyperlink to a bookmark within the document. + + :param bookmark_name: The name of the bookmark as provided to ``add_bookmark``. + :param display_text: The display text to put in the document and associate with this link. + :param tool_tip: The text to show on hover. If not set, defaults to ``#bookmark_name`` + """ + hyperlink = OxmlElement("w:hyperlink") + + hyperlink.set( + qn("w:anchor"), + bookmark_name, + ) + + if tool_tip is not None: + hyperlink.set( + qn("w:tooltip"), + tool_tip, + ) + + new_run = OxmlElement("w:r") + r_pr = OxmlElement("w:rPr") + new_run.append(r_pr) + new_run.text = display_text + hyperlink.append(new_run) + # noinspection PyTypeChecker + self._p.append(hyperlink) + + def add_bookmark(self, name: str, display_text: str | None = None, *, bookmark_id=None): + """Add a bookmark to the document for later linking to. + + Note that this method does not support bookmarking against various cells in a table. + Currently only paragraph based textual bookmarks are supported. + + :param name: The name of the bookmark. Unless ``bookmark_id`` is also set + this name should be unique across the document. + :param display_text: The text to display; and associate with; alongside the bookmark. + :param bookmark_id: If you don't want the internal bookmark ID to be + set to the ``name`` parameter, set this. + """ + if bookmark_id is None: + # ID should be unique, + # so we make an assumption name also is and call it a day + bookmark_id = name + + self._start_bookmark(name, bookmark_id) + + if display_text is not None: + text = OxmlElement("w:r") + text.text = display_text + # noinspection PyTypeChecker + self._p.append(text) + + self._end_bookmark(name, bookmark_id) + + def _start_bookmark(self, name: str, bookmark_id): + """Add's a 'bookmarkStart' entry.""" + # http://officeopenxml.com/WPbookmark.php + start = OxmlElement("w:bookmarkStart") + start.set(qn("w:id"), bookmark_id) + start.set(qn("w:name"), name) + # noinspection PyTypeChecker + self._p.append(start) + + def _end_bookmark(self, name: str, bookmark_id): + """Add's a 'bookmarkEnd' entry.""" + end = OxmlElement("w:bookmarkEnd") + end.set(qn("w:id"), bookmark_id) + end.set(qn("w:name"), name) + # noinspection PyTypeChecker + self._p.append(end) + def insert_table_of_contents( self, *, From a9ab4c2c84fe0ad0efe7aa0da8b53ef1d570673b Mon Sep 17 00:00:00 2001 From: skelmis Date: Sun, 15 Jun 2025 22:09:04 +1200 Subject: [PATCH 2/4] chore: bump version --- pyproject.toml | 2 +- src/skelmis/docx/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 77746b0..3504100 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "skelmis-docx" -version = "2.2.2" +version = "2.3.0" description = "Create, read, and update Microsoft Word .docx files." authors = [{ name = "Skelmis", email = "skelmis.craft@gmail.com" }] requires-python = ">=3.10" diff --git a/src/skelmis/docx/__init__.py b/src/skelmis/docx/__init__.py index dda41a5..2718331 100644 --- a/src/skelmis/docx/__init__.py +++ b/src/skelmis/docx/__init__.py @@ -13,7 +13,7 @@ if TYPE_CHECKING: from skelmis.docx.opc.part import Part -__version__ = "2.2.2" +__version__ = "2.3.0" __all__ = ["Document"] From 803ebb1eddd018294aeb2ed9d09f23858f879bb6 Mon Sep 17 00:00:00 2001 From: skelmis Date: Sun, 15 Jun 2025 22:44:18 +1200 Subject: [PATCH 3/4] fix bookmark end mis spec --- src/skelmis/docx/text/paragraph.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/skelmis/docx/text/paragraph.py b/src/skelmis/docx/text/paragraph.py index d6c673d..960608e 100644 --- a/src/skelmis/docx/text/paragraph.py +++ b/src/skelmis/docx/text/paragraph.py @@ -2,6 +2,7 @@ from __future__ import annotations +import re from typing import TYPE_CHECKING, Iterator, List, cast from skelmis.docx.enum.style import WD_STYLE_TYPE @@ -37,7 +38,7 @@ def add_internal_hyperlink( bookmark_name: str, display_text: str, tool_tip: str | None = None, - ): + ) -> Hyperlink: """Add an internal hyperlink to a bookmark within the document. :param bookmark_name: The name of the bookmark as provided to ``add_bookmark``. @@ -64,6 +65,7 @@ def add_internal_hyperlink( hyperlink.append(new_run) # noinspection PyTypeChecker self._p.append(hyperlink) + return Hyperlink(hyperlink, self) def add_bookmark(self, name: str, display_text: str | None = None, *, bookmark_id=None): """Add a bookmark to the document for later linking to. @@ -80,7 +82,7 @@ def add_bookmark(self, name: str, display_text: str | None = None, *, bookmark_i if bookmark_id is None: # ID should be unique, # so we make an assumption name also is and call it a day - bookmark_id = name + bookmark_id = re.sub(r"\s+", "_", name) self._start_bookmark(name, bookmark_id) @@ -90,7 +92,7 @@ def add_bookmark(self, name: str, display_text: str | None = None, *, bookmark_i # noinspection PyTypeChecker self._p.append(text) - self._end_bookmark(name, bookmark_id) + self._end_bookmark(bookmark_id) def _start_bookmark(self, name: str, bookmark_id): """Add's a 'bookmarkStart' entry.""" @@ -101,11 +103,10 @@ def _start_bookmark(self, name: str, bookmark_id): # noinspection PyTypeChecker self._p.append(start) - def _end_bookmark(self, name: str, bookmark_id): + def _end_bookmark(self, bookmark_id): """Add's a 'bookmarkEnd' entry.""" end = OxmlElement("w:bookmarkEnd") end.set(qn("w:id"), bookmark_id) - end.set(qn("w:name"), name) # noinspection PyTypeChecker self._p.append(end) From b861b7e145febe68d66e87011a21828cf76479cc Mon Sep 17 00:00:00 2001 From: skelmis Date: Sun, 15 Jun 2025 22:54:28 +1200 Subject: [PATCH 4/4] Remove tooltip because apparently libre doesnt support it.. --- src/skelmis/docx/text/paragraph.py | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/src/skelmis/docx/text/paragraph.py b/src/skelmis/docx/text/paragraph.py index 960608e..061ad47 100644 --- a/src/skelmis/docx/text/paragraph.py +++ b/src/skelmis/docx/text/paragraph.py @@ -33,17 +33,16 @@ def __init__(self, p: CT_P, parent: t.ProvidesStoryPart): super(Paragraph, self).__init__(parent) self._p = self._element = p + # noinspection PyTypeChecker def add_internal_hyperlink( self, bookmark_name: str, display_text: str, - tool_tip: str | None = None, ) -> Hyperlink: """Add an internal hyperlink to a bookmark within the document. :param bookmark_name: The name of the bookmark as provided to ``add_bookmark``. :param display_text: The display text to put in the document and associate with this link. - :param tool_tip: The text to show on hover. If not set, defaults to ``#bookmark_name`` """ hyperlink = OxmlElement("w:hyperlink") @@ -52,18 +51,10 @@ def add_internal_hyperlink( bookmark_name, ) - if tool_tip is not None: - hyperlink.set( - qn("w:tooltip"), - tool_tip, - ) - new_run = OxmlElement("w:r") - r_pr = OxmlElement("w:rPr") - new_run.append(r_pr) + new_run.append(OxmlElement("w:rPr")) new_run.text = display_text hyperlink.append(new_run) - # noinspection PyTypeChecker self._p.append(hyperlink) return Hyperlink(hyperlink, self) @@ -275,6 +266,7 @@ def insert_table_of_contents( # noinspection PyProtectedMember run._r.append(item) + # noinspection PyTypeChecker def add_external_hyperlink( self, url: str,