A paragraph wrapped in a matched <w:bookmarkStart>/<w:bookmarkEnd> pair under the name 'myBookmark'.
Library verdicts: python-docx: — docxjs: pass
| Feature id | docx/bookmark |
|---|---|
| Format | docx |
| Category | references |
| Spec | ecma-376-5-part-1 § 17.13.6 w:bookmarkStart |
| ID / part | Predicate | XPath | python-docx | docxjs |
|---|---|---|---|---|
bookmark-start-namedword/document.xml | existA <w:bookmarkStart> with @w:name='myBookmark' must be present. | //w:bookmarkStart[@w:name='myBookmark'] | — | — |
bookmark-end-matches-startword/document.xml | existA matching <w:bookmarkEnd> whose @w:id equals the bookmark-start's @w:id must be present. | //w:bookmarkEnd[@w:id=//w:bookmarkStart[@w:name='myBookmark']/@w:id] | — | — |
| ID | Predicate | Selector | python-docx | docxjs |
|---|---|---|---|---|
bookmark-anchor-rendered | css_selector/ existA DOM node carrying the bookmark name (as id, data-bookmark, or <a name>) should exist. Most renderers drop bookmarks today so this often fails — it's a gap to track, not a correctness bug in the authoring library. | .docx-wrapper [id='myBookmark'], .docx-wrapper [data-bookmark='myBookmark'], .docx-wrapper a[name='myBookmark'] | — | pass |
scripts/gen_bookmark.py
#!/usr/bin/env python3
"""Generate ``fixtures/docx/bookmark.docx``.
A minimal document exercising the ``w:bookmarkStart`` / ``w:bookmarkEnd``
pair. One paragraph containing the literal text ``Bookmarked text``,
wrapped in a bookmark named ``myBookmark``.
Uses the fork's ``Paragraph.add_bookmark()`` high-level API
(python-docx >= 2026.05.0). When no ``start_run`` / ``end_run`` is
supplied the bookmark wraps the whole paragraph, which is what we want
for the fixture.
"""
from __future__ import annotations
from pathlib import Path
from docx import Document
_REPO_ROOT = Path(__file__).resolve().parent.parent
_OUT = _REPO_ROOT / "fixtures" / "docx" / "bookmark.docx"
def main() -> int:
doc = Document()
paragraph = doc.add_paragraph("Bookmarked text")
paragraph.add_bookmark("myBookmark")
_OUT.parent.mkdir(parents=True, exist_ok=True)
doc.save(_OUT, reproducible=True)
print(_OUT)
return 0
if __name__ == "__main__":
raise SystemExit(main())
{
"$schema": "../manifest.schema.json",
"id": "docx/bookmark",
"title": "Named bookmark",
"format": "docx",
"category": "references",
"summary": "A paragraph wrapped in a matched <w:bookmarkStart>/<w:bookmarkEnd> pair under the name 'myBookmark'.",
"spec": {
"source": "ecma-376-5-part-1",
"clause": "17.13.6",
"element": "w:bookmarkStart",
"rnc_reference": "spec/ecma-376-5/part-1/rnc/WordprocessingML.rnc",
"xsd_reference": "spec/ecma-376-5/part-1/xsd/wml.xsd",
"notes": "A bookmark is a range marker with no visible rendering — it's purely a target for hyperlinks, cross-references, and Word commands. The start carries @w:id and @w:name; the end carries @w:id only, matched to the start. Both ends must appear in the same body."
},
"fixtures": {
"machine": "docx/bookmark",
"office": "docx/bookmark"
},
"generator": {
"python": "scripts/gen_bookmark.py"
},
"assertions": [
{
"id": "bookmark-start-named",
"part": "word/document.xml",
"namespaces": {
"w": "http://schemas.openxmlformats.org/wordprocessingml/2006/main"
},
"xpath": "//w:bookmarkStart[@w:name='myBookmark']",
"must": "exist",
"description": "A <w:bookmarkStart> with @w:name='myBookmark' must be present."
},
{
"id": "bookmark-end-matches-start",
"part": "word/document.xml",
"namespaces": {
"w": "http://schemas.openxmlformats.org/wordprocessingml/2006/main"
},
"xpath": "//w:bookmarkEnd[@w:id=//w:bookmarkStart[@w:name='myBookmark']/@w:id]",
"must": "exist",
"description": "A matching <w:bookmarkEnd> whose @w:id equals the bookmark-start's @w:id must be present."
}
],
"render_assertions": [
{
"id": "bookmark-anchor-rendered",
"kind": "css_selector",
"selector": ".docx-wrapper [id='myBookmark'], .docx-wrapper [data-bookmark='myBookmark'], .docx-wrapper a[name='myBookmark']",
"must": "exist",
"description": "A DOM node carrying the bookmark name (as id, data-bookmark, or <a name>) should exist. Most renderers drop bookmarks today so this often fails — it's a gap to track, not a correctness bug in the authoring library."
}
]
}
