Skip to content

Commit cd09fab

Browse files
author
Steve Canny
committed
tbl: add CT_Tc._move_content_to()
1 parent 25f1519 commit cd09fab

File tree

2 files changed

+76
-3
lines changed

2 files changed

+76
-3
lines changed

docx/oxml/table.py

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
from . import parse_xml
1010
from ..exceptions import InvalidSpanError
11-
from .ns import nsdecls
11+
from .ns import nsdecls, qn
1212
from ..shared import Emu, Twips
1313
from .simpletypes import (
1414
ST_Merge, ST_TblLayoutType, ST_TblWidth, ST_TwipsMeasure, XsdInt
@@ -254,6 +254,16 @@ def grid_span(self):
254254
return 1
255255
return tcPr.grid_span
256256

257+
def iter_block_items(self):
258+
"""
259+
Generate a reference to each of the block-level content elements in
260+
this cell, in the order they appear.
261+
"""
262+
block_item_tags = (qn('w:p'), qn('w:tbl'), qn('w:sdt'))
263+
for child in self:
264+
if child.tag in block_item_tags:
265+
yield child
266+
257267
@property
258268
def left(self):
259269
"""
@@ -372,16 +382,52 @@ def _insert_tcPr(self, tcPr):
372382
self.insert(0, tcPr)
373383
return tcPr
374384

385+
@property
386+
def _is_empty(self):
387+
"""
388+
True if this cell contains only a single empty ``<w:p>`` element.
389+
"""
390+
block_items = list(self.iter_block_items())
391+
if len(block_items) > 1:
392+
return False
393+
p = block_items[0] # cell must include at least one <w:p> element
394+
if len(p.r_lst) == 0:
395+
return True
396+
return False
397+
375398
def _move_content_to(self, other_tc):
376399
"""
377400
Append the content of this cell to *other_tc*, leaving this cell with
378401
a single empty ``<w:p>`` element.
379402
"""
380-
raise NotImplementedError
403+
if other_tc is self:
404+
return
405+
if self._is_empty:
406+
return
407+
other_tc._remove_trailing_empty_p()
408+
# appending moves each element from self to other_tc
409+
for block_element in self.iter_block_items():
410+
other_tc.append(block_element)
411+
# add back the required minimum single empty <w:p> element
412+
self.append(self._new_p())
381413

382414
def _new_tbl(self):
383415
return CT_Tbl.new()
384416

417+
def _remove_trailing_empty_p(self):
418+
"""
419+
Remove the last content element from this cell if it is an empty
420+
``<w:p>`` element.
421+
"""
422+
block_items = list(self.iter_block_items())
423+
last_content_elm = block_items[-1]
424+
if last_content_elm.tag != qn('w:p'):
425+
return
426+
p = last_content_elm
427+
if len(p.r_lst) > 0:
428+
return
429+
self.remove(p)
430+
385431
def _span_dimensions(self, other_tc):
386432
"""
387433
Return a (top, left, height, width) 4-tuple specifying the extents of

tests/oxml/test_table.py

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
from docx.oxml import parse_xml
1515
from docx.oxml.table import CT_Row, CT_Tc
1616

17-
from ..unitutil.cxml import element
17+
from ..unitutil.cxml import element, xml
1818
from ..unitutil.file import snippet_seq
1919
from ..unitutil.mock import call, instance_mock, method_mock, property_mock
2020

@@ -73,6 +73,12 @@ def it_can_extend_its_horz_span_to_help_merge(self, span_width_fixture):
7373
assert tc._swallow_next_tc.call_args_list == expected_calls
7474
assert tc.vMerge == vMerge
7575

76+
def it_can_move_its_content_to_help_merge(self, move_fixture):
77+
tc, tc_2, expected_tc_xml, expected_tc_2_xml = move_fixture
78+
tc._move_content_to(tc_2)
79+
assert tc.xml == expected_tc_xml
80+
assert tc_2.xml == expected_tc_2_xml
81+
7682
def it_raises_on_tr_above(self, tr_above_raise_fixture):
7783
tc = tr_above_raise_fixture
7884
with pytest.raises(ValueError):
@@ -128,6 +134,27 @@ def merge_fixture(
128134
tr_.tc_at_grid_col.return_value = top_tc_
129135
return tc, other_tc, tr_, top_tc_, left, height, width
130136

137+
@pytest.fixture(params=[
138+
('w:tc/w:p', 'w:tc/w:p',
139+
'w:tc/w:p', 'w:tc/w:p'),
140+
('w:tc/w:p', 'w:tc/w:p/w:r',
141+
'w:tc/w:p', 'w:tc/w:p/w:r'),
142+
('w:tc/w:p/w:r', 'w:tc/w:p',
143+
'w:tc/w:p', 'w:tc/w:p/w:r'),
144+
('w:tc/(w:p/w:r,w:sdt)', 'w:tc/w:p',
145+
'w:tc/w:p', 'w:tc/(w:p/w:r,w:sdt)'),
146+
('w:tc/(w:p/w:r,w:sdt)', 'w:tc/(w:tbl,w:p)',
147+
'w:tc/w:p', 'w:tc/(w:tbl,w:p/w:r,w:sdt)'),
148+
])
149+
def move_fixture(self, request):
150+
tc_cxml, tc_2_cxml, expected_tc_cxml, expected_tc_2_cxml = (
151+
request.param
152+
)
153+
tc, tc_2 = element(tc_cxml), element(tc_2_cxml)
154+
expected_tc_xml = xml(expected_tc_cxml)
155+
expected_tc_2_xml = xml(expected_tc_2_cxml)
156+
return tc, tc_2, expected_tc_xml, expected_tc_2_xml
157+
131158
@pytest.fixture(params=[
132159
(0, 0, 0, 0, 1, (0, 0, 1, 2)),
133160
(0, 0, 1, 2, 1, (0, 1, 3, 1)),

0 commit comments

Comments
 (0)