Skip to content

Commit 99778bb

Browse files
committed
bmk: add BlockItemContainer.end_bookmark()
1 parent 7bc7728 commit 99778bb

6 files changed

Lines changed: 94 additions & 5 deletions

File tree

docx/blkcntnr.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,9 @@ def add_table(self, rows, cols, width):
5454

5555
def end_bookmark(self, bookmark):
5656
"""Return `bookmark` after closing it after last block item in container."""
57-
raise NotImplementedError
57+
if bookmark.is_closed:
58+
raise ValueError("bookmark already closed")
59+
return bookmark.close(self._element.add_bookmarkEnd(bookmark.id))
5860

5961
@property
6062
def paragraphs(self):

docx/bookmark.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,11 +76,24 @@ class _Bookmark(object):
7676
def __init__(self, bookmark_pair):
7777
self._bookmarkStart, self._bookmarkEnd = bookmark_pair
7878

79+
def close(self, bookmarkEnd):
80+
"""Return self after setting end marker to `bookmarkEnd`.
81+
82+
Raises ValueError if this bookmark is already closed or if `id` attribute of
83+
`bookmarkEnd` does not match that of the `bookmarkStart` element.
84+
"""
85+
raise NotImplementedError
86+
7987
@property
8088
def id(self):
8189
"""Provides access to the bookmark id."""
8290
return self._bookmarkStart.id
8391

92+
@property
93+
def is_closed(self):
94+
"""True if this bookmark has both a start and end element."""
95+
raise NotImplementedError
96+
8497
@property
8598
def name(self):
8699
"""Provides access to the bookmark name."""

docx/oxml/document.py

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,6 @@
11
# encoding: utf-8
22

3-
"""
4-
Custom element classes that correspond to the document part, e.g.
5-
<w:document>.
6-
"""
3+
"""Custom element classes that correspond to the document part, e.g. <w:document>."""
74

85
from .xmlchemy import BaseOxmlElement, ZeroOrOne, ZeroOrMore
96

@@ -30,8 +27,20 @@ class CT_Body(BaseOxmlElement):
3027
p = ZeroOrMore("w:p", successors=("w:sectPr",))
3128
tbl = ZeroOrMore("w:tbl", successors=("w:sectPr",))
3229
bookmarkStart = ZeroOrMore("w:bookmarkStart", successors=("w:sectPr",))
30+
bookmarkEnd = ZeroOrMore("w:bookmarkEnd", successors=("w:sectPr",))
3331
sectPr = ZeroOrOne("w:sectPr", successors=())
3432

33+
def add_bookmarkEnd(self, bookmark_id):
34+
"""Return `w:bookmarkEnd` element added at end of document.
35+
36+
The newly added `w:bookmarkEnd` element is linked to it's `w:bookmarkStart`
37+
counterpart by `bookmark_id`. It is the caller's responsibility to determine
38+
`bookmark_id` matches that of the intended `bookmarkStart` element.
39+
"""
40+
bookmarkEnd = self._add_bookmarkEnd()
41+
bookmarkEnd.id = bookmark_id
42+
return bookmarkEnd
43+
3544
def add_bookmarkStart(self, name, bookmark_id):
3645
"""Return `w:bookmarkStart` element added at end of document.
3746

docx/oxml/section.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,18 @@ class CT_HdrFtr(BaseOxmlElement):
2323
p = ZeroOrMore("w:p", successors=())
2424
tbl = ZeroOrMore("w:tbl", successors=())
2525
bookmarkStart = ZeroOrMore("w:bookmarkStart", successors=())
26+
bookmarkEnd = ZeroOrMore("w:bookmarkEnd", successors=())
27+
28+
def add_bookmarkEnd(self, bookmark_id):
29+
"""Return `w:bookmarkEnd` element added at end of this header or footer.
30+
31+
The newly added `w:bookmarkEnd` element is linked to it's `w:bookmarkStart`
32+
counterpart by `bookmark_id`. It is the caller's responsibility to determine
33+
`bookmark_id` matches that of the intended `bookmarkStart` element.
34+
"""
35+
bookmarkEnd = self._add_bookmarkEnd()
36+
bookmarkEnd.id = bookmark_id
37+
return bookmarkEnd
2638

2739
def add_bookmarkStart(self, name, bookmark_id):
2840
"""Return `w:bookmarkStart` element added at the end of this header or footer.

docx/oxml/table.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -399,6 +399,18 @@ class CT_Tc(BaseOxmlElement):
399399
p = OneOrMore("w:p")
400400
tbl = OneOrMore("w:tbl")
401401
bookmarkStart = ZeroOrMore("w:bookmarkStart", successors=())
402+
bookmarkEnd = ZeroOrMore("w:bookmarkEnd", successors=())
403+
404+
def add_bookmarkEnd(self, bookmark_id):
405+
"""Return `w:bookmarkEnd` element added at end of this cell.
406+
407+
The newly added `w:bookmarkEnd` element is linked to it's `w:bookmarkStart`
408+
counterpart by `bookmark_id`. It is the caller's responsibility to determine
409+
`bookmark_id` matches that of the intended `bookmarkStart` element.
410+
"""
411+
bookmarkEnd = self._add_bookmarkEnd()
412+
bookmarkEnd.id = bookmark_id
413+
return bookmarkEnd
402414

403415
def add_bookmarkStart(self, name, bookmark_id):
404416
"""Return `w:bookmarkStart` element added at the end of this cell.

tests/test_blkcntnr.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,29 @@ def it_can_add_a_table(self, add_table_fixture):
5050
assert table._element.xml == expected_xml
5151
assert table._parent is blkcntnr
5252

53+
def it_can_end_a_bookmark(self, end_bookmark_fixture, bookmark_):
54+
blockContainer, bookmark_id, expected_xml = end_bookmark_fixture
55+
bookmark_.close.return_value = bookmark_
56+
bookmark_.id = bookmark_id
57+
bookmark_.is_closed = False
58+
blkcntnr = BlockItemContainer(blockContainer, None)
59+
60+
bookmark = blkcntnr.end_bookmark(bookmark_)
61+
62+
bookmark_.close.assert_called_once_with(
63+
blockContainer.xpath("w:bookmarkEnd")[-1]
64+
)
65+
assert blkcntnr._element.xml == expected_xml
66+
assert bookmark is bookmark_
67+
68+
def but_it_raises_when_bookmark_is_already_closed(self, bookmark_):
69+
bookmark_.is_closed = True
70+
blkcntnr = BlockItemContainer(None, None)
71+
72+
with pytest.raises(ValueError) as e:
73+
blkcntnr.end_bookmark(bookmark_)
74+
assert "bookmark already closed" in str(e.value)
75+
5376
def it_provides_access_to_the_paragraphs_it_contains(self, paragraphs_fixture):
5477
# ---test len(), iterable, and indexed access---
5578
blkcntnr, expected_count = paragraphs_fixture
@@ -160,6 +183,24 @@ def bookmarks_fixture(self, request):
160183
parent_part_ = instance_mock(request, PartCls)
161184
return parent_part_
162185

186+
@pytest.fixture(
187+
params=[
188+
# ---document body---
189+
("w:body", 0, "w:body/w:bookmarkEnd{w:id=0}"),
190+
# ---table cell---
191+
("w:tc/w:p", 1, "w:tc/(w:p,w:bookmarkEnd{w:id=1})"),
192+
# ---header---
193+
("w:hdr/w:p", 42, "w:hdr/(w:p,w:bookmarkEnd{w:id=42})"),
194+
# ---footer---
195+
("w:ftr/w:p", 24, "w:ftr/(w:p,w:bookmarkEnd{w:id=24})"),
196+
]
197+
)
198+
def end_bookmark_fixture(self, request):
199+
cxml, bookmark_id, expected_cxml = request.param
200+
blockContainer = element(cxml)
201+
expected_xml = xml(expected_cxml)
202+
return blockContainer, bookmark_id, expected_xml
203+
163204
@pytest.fixture(
164205
params=[
165206
("w:body", 0),

0 commit comments

Comments
 (0)