mirror of
https://github.com/Kozea/WeasyPrint.git
synced 2024-10-05 00:21:15 +03:00
Support break-after in table. Related to #209
This commit is contained in:
parent
b8c19a2878
commit
4b396d5f58
@ -731,6 +731,20 @@ def establishes_formatting_context(box):
|
||||
)
|
||||
|
||||
|
||||
def combine_break_values(values):
|
||||
result = 'auto'
|
||||
for value in values:
|
||||
if value in ('left', 'right', 'recto', 'verso') or (value, result) in (
|
||||
('page', 'auto'),
|
||||
('page', 'avoid'),
|
||||
('avoid', 'auto'),
|
||||
('page', 'avoid-page'),
|
||||
('avoid-page', 'auto')):
|
||||
result = value
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def block_level_page_break(sibling_before, sibling_after):
|
||||
"""Return the value of ``page-break-before`` or ``page-break-after``
|
||||
that "wins" for boxes that meet at the margin between two sibling boxes.
|
||||
@ -747,8 +761,12 @@ def block_level_page_break(sibling_before, sibling_after):
|
||||
|
||||
"""
|
||||
values = []
|
||||
# https://drafts.csswg.org/css-break-3/#possible-breaks
|
||||
block_parallel_box_types = (
|
||||
boxes.BlockLevelBox, boxes.TableRowGroupBox, boxes.TableRowBox)
|
||||
|
||||
box = sibling_before
|
||||
while isinstance(box, boxes.BlockLevelBox):
|
||||
while isinstance(box, block_parallel_box_types):
|
||||
values.append(box.style['break_after'])
|
||||
if not (isinstance(box, boxes.ParentBox) and box.children):
|
||||
break
|
||||
@ -756,23 +774,13 @@ def block_level_page_break(sibling_before, sibling_after):
|
||||
values.reverse() # Have them in tree order
|
||||
|
||||
box = sibling_after
|
||||
while isinstance(box, boxes.BlockLevelBox):
|
||||
while isinstance(box, block_parallel_box_types):
|
||||
values.append(box.style['break_before'])
|
||||
if not (isinstance(box, boxes.ParentBox) and box.children):
|
||||
break
|
||||
box = box.children[0]
|
||||
|
||||
result = 'auto'
|
||||
for value in values:
|
||||
if value in ('left', 'right', 'recto', 'verso') or (value, result) in (
|
||||
('page', 'auto'),
|
||||
('page', 'avoid'),
|
||||
('avoid', 'auto'),
|
||||
('page', 'avoid-page'),
|
||||
('avoid-page', 'auto')):
|
||||
result = value
|
||||
|
||||
return result
|
||||
return combine_break_values(values)
|
||||
|
||||
|
||||
def block_level_page_name(sibling_before, sibling_after):
|
||||
|
@ -20,7 +20,7 @@ def table_layout(context, table, max_position_y, skip_stack,
|
||||
fixed_boxes):
|
||||
"""Layout for a table box."""
|
||||
# Avoid a circular import
|
||||
from .blocks import block_container_layout
|
||||
from .blocks import block_container_layout, combine_break_values
|
||||
|
||||
column_widths = table.column_widths
|
||||
|
||||
@ -62,6 +62,7 @@ def table_layout(context, table, max_position_y, skip_stack,
|
||||
def group_layout(group, position_y, max_position_y,
|
||||
page_is_empty, skip_stack):
|
||||
resume_at = None
|
||||
next_page = {'break': None, 'page': None}
|
||||
original_page_is_empty = page_is_empty
|
||||
resolve_percentages(group, containing_block=table)
|
||||
group.position_x = rows_x
|
||||
@ -77,7 +78,8 @@ def table_layout(context, table, max_position_y, skip_stack,
|
||||
else:
|
||||
skip, skip_stack = skip_stack
|
||||
assert not skip_stack # No breaks inside rows for now
|
||||
for index_row, row in group.enumerate_skip(skip):
|
||||
for i, row in enumerate(group.children[skip:]):
|
||||
index_row = i + skip
|
||||
resolve_percentages(row, containing_block=table)
|
||||
row.position_x = rows_x
|
||||
row.position_y = position_y
|
||||
@ -215,12 +217,20 @@ def table_layout(context, table, max_position_y, skip_stack,
|
||||
new_group_children.append(row)
|
||||
page_is_empty = False
|
||||
|
||||
if row.style['break_after'] in (
|
||||
'page', 'recto', 'verso', 'left', 'right'):
|
||||
if index_row < len(group.children) - 1:
|
||||
resume_at = (index_row + 1, None)
|
||||
break
|
||||
else:
|
||||
next_page['break'] = row.style['break_after']
|
||||
|
||||
# Do not keep the row group if we made a page break
|
||||
# before any of its rows or with 'avoid'
|
||||
if resume_at and not original_page_is_empty and (
|
||||
group.style['break_inside'] in ('avoid', 'avoid-page') or
|
||||
not new_group_children):
|
||||
return None, None
|
||||
return None, None, next_page
|
||||
|
||||
group = group.copy_with_children(
|
||||
new_group_children,
|
||||
@ -241,7 +251,7 @@ def table_layout(context, table, max_position_y, skip_stack,
|
||||
# The last border spacing is outside of the group.
|
||||
group.height -= border_spacing_y
|
||||
|
||||
return group, resume_at
|
||||
return group, resume_at, next_page
|
||||
|
||||
def body_groups_layout(skip_stack, position_y, max_position_y,
|
||||
page_is_empty):
|
||||
@ -251,10 +261,12 @@ def table_layout(context, table, max_position_y, skip_stack,
|
||||
skip, skip_stack = skip_stack
|
||||
new_table_children = []
|
||||
resume_at = None
|
||||
for index_group, group in table.enumerate_skip(skip):
|
||||
next_page = {'break': 'any', 'page': None}
|
||||
for i, group in enumerate(table.children[skip:]):
|
||||
index_group = i + skip
|
||||
if group.is_header or group.is_footer:
|
||||
continue
|
||||
new_group, resume_at = group_layout(
|
||||
new_group, resume_at, next_page = group_layout(
|
||||
group, position_y, max_position_y, page_is_empty, skip_stack)
|
||||
skip_stack = None
|
||||
|
||||
@ -266,10 +278,19 @@ def table_layout(context, table, max_position_y, skip_stack,
|
||||
position_y += new_group.height + border_spacing_y
|
||||
page_is_empty = False
|
||||
|
||||
if resume_at:
|
||||
resume_at = (index_group, resume_at)
|
||||
next_page['break'] = combine_break_values(
|
||||
(next_page['break'], group.style['break_after']))
|
||||
|
||||
if resume_at or next_page['break'] in (
|
||||
'page', 'recto', 'verso', 'left', 'right'):
|
||||
if resume_at is None:
|
||||
if index_group < len(table.children) - 1:
|
||||
resume_at = (index_group + 1, None)
|
||||
else:
|
||||
resume_at = (index_group, resume_at)
|
||||
break
|
||||
return new_table_children, resume_at, position_y
|
||||
|
||||
return new_table_children, resume_at, next_page, position_y
|
||||
|
||||
# Layout for row groups, rows and cells
|
||||
position_y = table.content_box_y() + border_spacing_y
|
||||
@ -278,7 +299,7 @@ def table_layout(context, table, max_position_y, skip_stack,
|
||||
def all_groups_layout():
|
||||
if table.children and table.children[0].is_header:
|
||||
header = table.children[0]
|
||||
header, resume_at = group_layout(
|
||||
header, resume_at, next_page = group_layout(
|
||||
header, position_y, max_position_y,
|
||||
skip_stack=None, page_is_empty=False)
|
||||
if header and not resume_at:
|
||||
@ -290,7 +311,7 @@ def table_layout(context, table, max_position_y, skip_stack,
|
||||
|
||||
if table.children and table.children[-1].is_footer:
|
||||
footer = table.children[-1]
|
||||
footer, resume_at = group_layout(
|
||||
footer, resume_at, next_page = group_layout(
|
||||
footer, position_y, max_position_y,
|
||||
skip_stack=None, page_is_empty=False)
|
||||
if footer and not resume_at:
|
||||
@ -311,54 +332,60 @@ def table_layout(context, table, max_position_y, skip_stack,
|
||||
|
||||
if header and footer:
|
||||
# Try with both the header and footer
|
||||
new_table_children, resume_at, end_position_y = body_groups_layout(
|
||||
skip_stack,
|
||||
position_y=position_y + header_height,
|
||||
max_position_y=max_position_y - footer_height,
|
||||
page_is_empty=avoid_breaks)
|
||||
new_table_children, resume_at, next_page, end_position_y = (
|
||||
body_groups_layout(
|
||||
skip_stack,
|
||||
position_y=position_y + header_height,
|
||||
max_position_y=max_position_y - footer_height,
|
||||
page_is_empty=avoid_breaks))
|
||||
if new_table_children or not page_is_empty:
|
||||
footer.translate(dy=end_position_y - footer.position_y)
|
||||
end_position_y += footer_height
|
||||
return (header, new_table_children, footer,
|
||||
end_position_y, resume_at)
|
||||
end_position_y, resume_at, next_page)
|
||||
else:
|
||||
# We could not fit any content, drop the footer
|
||||
footer = None
|
||||
|
||||
if header and not footer:
|
||||
# Try with just the header
|
||||
new_table_children, resume_at, end_position_y = body_groups_layout(
|
||||
skip_stack,
|
||||
position_y=position_y + header_height,
|
||||
max_position_y=max_position_y,
|
||||
page_is_empty=avoid_breaks)
|
||||
new_table_children, resume_at, next_page, end_position_y = (
|
||||
body_groups_layout(
|
||||
skip_stack,
|
||||
position_y=position_y + header_height,
|
||||
max_position_y=max_position_y,
|
||||
page_is_empty=avoid_breaks))
|
||||
if new_table_children or not page_is_empty:
|
||||
return (header, new_table_children, footer,
|
||||
end_position_y, resume_at)
|
||||
end_position_y, resume_at, next_page)
|
||||
else:
|
||||
# We could not fit any content, drop the header
|
||||
header = None
|
||||
|
||||
if footer and not header:
|
||||
# Try with just the footer
|
||||
new_table_children, resume_at, end_position_y = body_groups_layout(
|
||||
skip_stack,
|
||||
position_y=position_y,
|
||||
max_position_y=max_position_y - footer_height,
|
||||
page_is_empty=avoid_breaks)
|
||||
new_table_children, resume_at, next_page, end_position_y = (
|
||||
body_groups_layout(
|
||||
skip_stack,
|
||||
position_y=position_y,
|
||||
max_position_y=max_position_y - footer_height,
|
||||
page_is_empty=avoid_breaks))
|
||||
if new_table_children or not page_is_empty:
|
||||
footer.translate(dy=end_position_y - footer.position_y)
|
||||
end_position_y += footer_height
|
||||
return (header, new_table_children, footer,
|
||||
end_position_y, resume_at)
|
||||
end_position_y, resume_at, next_page)
|
||||
else:
|
||||
# We could not fit any content, drop the footer
|
||||
footer = None
|
||||
|
||||
assert not (header or footer)
|
||||
new_table_children, resume_at, end_position_y = body_groups_layout(
|
||||
skip_stack, position_y, max_position_y, page_is_empty)
|
||||
return header, new_table_children, footer, end_position_y, resume_at
|
||||
new_table_children, resume_at, next_page, end_position_y = (
|
||||
body_groups_layout(
|
||||
skip_stack, position_y, max_position_y, page_is_empty))
|
||||
return (
|
||||
header, new_table_children, footer, end_position_y, resume_at,
|
||||
next_page)
|
||||
|
||||
def get_column_cells(table, column):
|
||||
"""Closure getting the column cells."""
|
||||
@ -369,7 +396,7 @@ def table_layout(context, table, max_position_y, skip_stack,
|
||||
for cell in row.children
|
||||
if cell.grid_x == column.grid_x]
|
||||
|
||||
header, new_table_children, footer, position_y, resume_at = \
|
||||
header, new_table_children, footer, position_y, resume_at, next_page = \
|
||||
all_groups_layout()
|
||||
table = table.copy_with_children(
|
||||
([header] if header is not None else []) +
|
||||
@ -413,7 +440,10 @@ def table_layout(context, table, max_position_y, skip_stack,
|
||||
group.width = last.position_x + last.width - first.position_x
|
||||
group.height = columns_height
|
||||
|
||||
next_page = {'break': 'any', 'page': table.style['page']}
|
||||
next_page['break'] = combine_break_values(
|
||||
(next_page['break'], table.style['break_after']))
|
||||
next_page['page'] = table.style['page']
|
||||
|
||||
if resume_at and not page_is_empty and (
|
||||
table.style['break_inside'] in ('avoid', 'avoid-page') or
|
||||
not new_table_children):
|
||||
|
@ -2064,6 +2064,86 @@ def test_table_page_breaks_complex():
|
||||
]
|
||||
|
||||
|
||||
@assert_no_logs
|
||||
def test_table_page_break_after():
|
||||
page1, page2, page3, page4, page5, page6 = render_pages('''
|
||||
<style>
|
||||
@page { size: 1000px }
|
||||
h1 { height: 30px}
|
||||
td { height: 40px }
|
||||
table { table-layout: fixed; width: 100% }
|
||||
</style>
|
||||
<h1>Dummy title</h1>
|
||||
<table>
|
||||
|
||||
<tbody>
|
||||
<tr><td>row 1</td></tr>
|
||||
<tr><td>row 2</td></tr>
|
||||
<tr><td>row 3</td></tr>
|
||||
</tbody>
|
||||
<tbody>
|
||||
<tr style="break-after: page"><td>row 1</td></tr>
|
||||
<tr><td>row 2</td></tr>
|
||||
<tr><td>row 3</td></tr>
|
||||
</tbody>
|
||||
<tbody>
|
||||
<tr><td>row 1</td></tr>
|
||||
<tr><td>row 2</td></tr>
|
||||
<tr style="break-after: page"><td>row 3</td></tr>
|
||||
</tbody>
|
||||
<tbody style="break-after: right">
|
||||
<tr><td>row 1</td></tr>
|
||||
<tr><td>row 2</td></tr>
|
||||
<tr><td>row 3</td></tr>
|
||||
</tbody>
|
||||
<tbody style="break-after: page">
|
||||
<tr><td>row 1</td></tr>
|
||||
<tr><td>row 2</td></tr>
|
||||
<tr><td>row 3</td></tr>
|
||||
</tbody>
|
||||
|
||||
</table>
|
||||
<p>bla bla</p>
|
||||
''')
|
||||
html, = page1.children
|
||||
body, = html.children
|
||||
h1, table_wrapper = body.children
|
||||
table, = table_wrapper.children
|
||||
table_group1, table_group2 = table.children
|
||||
assert len(table_group1.children) == 3
|
||||
assert len(table_group2.children) == 1
|
||||
|
||||
html, = page2.children
|
||||
body, = html.children
|
||||
table_wrapper, = body.children
|
||||
table, = table_wrapper.children
|
||||
table_group1, table_group2 = table.children
|
||||
assert len(table_group1.children) == 2
|
||||
assert len(table_group2.children) == 3
|
||||
|
||||
html, = page3.children
|
||||
body, = html.children
|
||||
table_wrapper, = body.children
|
||||
table, = table_wrapper.children
|
||||
table_group, = table.children
|
||||
assert len(table_group.children) == 3
|
||||
|
||||
html, = page4.children
|
||||
assert not html.children
|
||||
|
||||
html, = page5.children
|
||||
body, = html.children
|
||||
table_wrapper, = body.children
|
||||
table, = table_wrapper.children
|
||||
table_group, = table.children
|
||||
assert len(table_group.children) == 3
|
||||
|
||||
html, = page6.children
|
||||
body, = html.children
|
||||
p, = body.children
|
||||
assert p.element_tag == 'p'
|
||||
|
||||
|
||||
@assert_no_logs
|
||||
@pytest.mark.parametrize('vertical_align, table_position_y', (
|
||||
('top', 8),
|
||||
|
Loading…
Reference in New Issue
Block a user