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/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"] diff --git a/src/skelmis/docx/text/paragraph.py b/src/skelmis/docx/text/paragraph.py index 17d1755..061ad47 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 @@ -32,6 +33,74 @@ 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, + ) -> 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. + """ + hyperlink = OxmlElement("w:hyperlink") + + hyperlink.set( + qn("w:anchor"), + bookmark_name, + ) + + new_run = OxmlElement("w:r") + new_run.append(OxmlElement("w:rPr")) + new_run.text = display_text + hyperlink.append(new_run) + 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. + + 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 = re.sub(r"\s+", "_", 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(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, bookmark_id): + """Add's a 'bookmarkEnd' entry.""" + end = OxmlElement("w:bookmarkEnd") + end.set(qn("w:id"), bookmark_id) + # noinspection PyTypeChecker + self._p.append(end) + def insert_table_of_contents( self, *, @@ -197,6 +266,7 @@ def insert_table_of_contents( # noinspection PyProtectedMember run._r.append(item) + # noinspection PyTypeChecker def add_external_hyperlink( self, url: str,