Skip to content

docx_plus.bookmarks.registry

Per-document registries for the two bookmark namespaces.

BookmarkIdRegistry tracks issued w:id values, which live in their own uniqueness namespace separate from SDT, comment, and note ids. Body-side <w:bookmarkStart> / <w:bookmarkEnd> elements both carry the id on a direct @w:id attribute, so the seeder uses the attribute-form collector inherited from _IdRegistryBase.

BookmarkNameRegistry (v0.5) tracks bookmark names. Bookmarks are the one thing in the format addressed by name, and nothing stops a document carrying two with the same w:name — a duplicate makes a REF ambiguous and makes delete_bookmark remove both. It also mints hidden Word-style anchors with next_ref_name(), in the _Ref + 9-digit form Word itself uses for auto-generated cross-reference targets. The leading underscore is load-bearing: Word omits underscore-prefixed bookmarks from its Bookmark dialog, so machine-generated caption anchors stay out of the user's list.

Both classes live in docx_plus.core.ids as of v0.5 and are re-exported here. The move was forced by SPEC §9.1 — publishing has to bookmark a caption to make it referenceable and cannot import from a sibling capability to do it.

docx_plus.bookmarks.registry

Bookmark id and name registries.

Bookmark w:id is its own uniqueness namespace, separate from SDT, comment, and note ids. Bookmarks are also the one thing in the format addressed by name, which is a second namespace needing its own allocator.

Both classes moved to :mod:docx_plus.core.ids in v0.5 and are re-exported here, so from docx_plus.bookmarks import BookmarkIdRegistry is unchanged. The move was forced by SPEC §9.1: publishing has to bookmark a caption to make it referenceable — a REF field can only point at a bookmark, never at the caption's own SEQ field — and it cannot import from a sibling capability to do it.

This module imports only from docx_plus.core (SPEC §9.1).

BookmarkIdRegistry

BookmarkIdRegistry(doc: Document)

Bases: _IdRegistryBase

Tracks issued bookmark w:id values for one document-edit session.

The body-side <w:bookmarkStart> / <w:bookmarkEnd> elements both carry the id on a direct @w:id attribute, not as a <w:id w:val=...> child the way SDTs do.

Source code in docx_plus/core/ids.py
def __init__(self, doc: Document) -> None:
    """Scan ``doc`` for IDs already issued in this namespace.

    Args:
        doc: A python-docx :class:`~docx.document.Document`.
    """
    self._issued: set[int] = set()
    self._seed_from_document(doc)

BookmarkNameRegistry

BookmarkNameRegistry(doc: Document)

Tracks bookmark names in use, and mints Word-style hidden ones.

Bookmarks are the one thing in the format addressed by name rather than by id, and nothing prevents a document from carrying two with the same w:name. That is worth guarding: a duplicate makes a REF ambiguous and makes :func:~docx_plus.bookmarks.delete_bookmark remove both.

Deliberately not an :class:_IdRegistryBase subclass — the keys are strings, and most of them (sec_1, intro) have no numeric part to allocate from at all.

Lifecycle: one instance per document-edit session, like every other registry here (SPEC §9.4).

Scan doc for bookmark names already in use.

Parameters:

Name Type Description Default
doc Document

A python-docx :class:~docx.document.Document.

required
Source code in docx_plus/core/ids.py
def __init__(self, doc: Document) -> None:
    """Scan ``doc`` for bookmark names already in use.

    Args:
        doc: A python-docx :class:`~docx.document.Document`.
    """
    self._names: set[str] = set()
    for start in xpath(doc.element.body, ".//w:bookmarkStart"):
        name = start.get(qn("w:name"))
        if name is not None:
            self._names.add(str(name))

__contains__

__contains__(name: str) -> bool

Whether name is already in use.

Source code in docx_plus/core/ids.py
def __contains__(self, name: str) -> bool:
    """Whether ``name`` is already in use."""
    return name in self._names

reserve

reserve(name: str) -> str

Claim name, asserting it is not already taken.

Parameters:

Name Type Description Default
name str

The bookmark name to claim.

required

Returns:

Type Description
str

name (echoed so the call composes inline).

Raises:

Type Description
DuplicateBookmarkNameError

If name is already in use.

Source code in docx_plus/core/ids.py
def reserve(self, name: str) -> str:
    """Claim ``name``, asserting it is not already taken.

    Args:
        name: The bookmark name to claim.

    Returns:
        ``name`` (echoed so the call composes inline).

    Raises:
        DuplicateBookmarkNameError: If ``name`` is already in use.
    """
    if name in self._names:
        raise DuplicateBookmarkNameError(f"bookmark name {name!r} is already in use")
    self._names.add(name)
    return name

next_ref_name

next_ref_name() -> str

Mint an unused hidden name in Word's _Ref + 9-digit form.

Use this for anchors the user never names themselves — a caption being made referenceable, for instance. The leading underscore keeps it out of Word's Bookmark dialog.

Returns:

Type Description
str

A fresh name such as "_Ref418320715".

Raises:

Type Description
RegistryExhaustedError

If the space is exhausted (effectively impossible — included for completeness).

Source code in docx_plus/core/ids.py
def next_ref_name(self) -> str:
    """Mint an unused hidden name in Word's ``_Ref`` + 9-digit form.

    Use this for anchors the user never names themselves — a caption
    being made referenceable, for instance. The leading underscore
    keeps it out of Word's Bookmark dialog.

    Returns:
        A fresh name such as ``"_Ref418320715"``.

    Raises:
        RegistryExhaustedError: If the space is exhausted (effectively
            impossible — included for completeness).
    """
    for _ in range(64):
        candidate = f"_Ref{secrets.randbelow(_MAX_REF_SUFFIX) + 1:0{_REF_NAME_DIGITS}d}"
        if candidate not in self._names:
            self._names.add(candidate)
            return candidate
    for suffix in range(1, _MAX_REF_SUFFIX + 1):  # pragma: no cover - unreachable
        candidate = f"_Ref{suffix:0{_REF_NAME_DIGITS}d}"
        if candidate not in self._names:
            self._names.add(candidate)
            return candidate
    raise RegistryExhaustedError("bookmark name registry exhausted the _Ref space")

issued

issued() -> frozenset[str]

Return an immutable snapshot of every name in use.

Source code in docx_plus/core/ids.py
def issued(self) -> frozenset[str]:
    """Return an immutable snapshot of every name in use."""
    return frozenset(self._names)

DuplicateBookmarkNameError

Bases: DocxPlusError, ValueError

Raised when a bookmark name is already in use in the document.

Subclasses ValueError so existing except ValueError: clauses still catch it; also subclasses :class:DocxPlusError per SPEC §9.7.