From 4b396d5f5801bd47304308d114dbd3adf6a392e1 Mon Sep 17 00:00:00 2001 From: grewn0uille Date: Tue, 20 Aug 2019 17:33:35 +0200 Subject: [PATCH 1/8] Support break-after in table. Related to #209 --- weasyprint/layout/blocks.py | 34 +++++--- weasyprint/layout/tables.py | 98 ++++++++++++++-------- weasyprint/tests/test_layout/test_table.py | 80 ++++++++++++++++++ 3 files changed, 165 insertions(+), 47 deletions(-) diff --git a/weasyprint/layout/blocks.py b/weasyprint/layout/blocks.py index e577f753c..8fe231b23 100644 --- a/weasyprint/layout/blocks.py +++ b/weasyprint/layout/blocks.py @@ -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): diff --git a/weasyprint/layout/tables.py b/weasyprint/layout/tables.py index c81d77b78..006b12074 100644 --- a/weasyprint/layout/tables.py +++ b/weasyprint/layout/tables.py @@ -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 group_layout(group, position_y, max_position_y, 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 group_layout(group, position_y, max_position_y, 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 group_layout(group, position_y, max_position_y, # 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 body_groups_layout(skip_stack, position_y, max_position_y, 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 body_groups_layout(skip_stack, position_y, max_position_y, 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 body_groups_layout(skip_stack, position_y, max_position_y, 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 all_groups_layout(): 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 all_groups_layout(): 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 get_column_cells(table, column): 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 get_column_cells(table, column): 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): diff --git a/weasyprint/tests/test_layout/test_table.py b/weasyprint/tests/test_layout/test_table.py index 66093a8b0..fcf7e473d 100644 --- a/weasyprint/tests/test_layout/test_table.py +++ b/weasyprint/tests/test_layout/test_table.py @@ -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(''' + +

Dummy title

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
row 1
row 2
row 3
row 1
row 2
row 3
row 1
row 2
row 3
row 1
row 2
row 3
row 1
row 2
row 3
+

bla bla

+ ''') + 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), From dd779d15987fb116c6012912924966d9c52c0636 Mon Sep 17 00:00:00 2001 From: grewn0uille Date: Tue, 20 Aug 2019 18:20:15 +0200 Subject: [PATCH 2/8] Support break-before in table. Related to #209 --- weasyprint/layout/blocks.py | 26 +++---- weasyprint/layout/tables.py | 46 ++++++------ weasyprint/tests/test_layout/test_table.py | 81 ++++++++++++++++++++++ 3 files changed, 114 insertions(+), 39 deletions(-) diff --git a/weasyprint/layout/blocks.py b/weasyprint/layout/blocks.py index 8fe231b23..e28d8d987 100644 --- a/weasyprint/layout/blocks.py +++ b/weasyprint/layout/blocks.py @@ -731,20 +731,6 @@ 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. @@ -780,7 +766,17 @@ def block_level_page_break(sibling_before, sibling_after): break box = box.children[0] - return 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_name(sibling_before, sibling_after): diff --git a/weasyprint/layout/tables.py b/weasyprint/layout/tables.py index 006b12074..85fbf83b8 100644 --- a/weasyprint/layout/tables.py +++ b/weasyprint/layout/tables.py @@ -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, combine_break_values + from .blocks import block_container_layout, block_level_page_break column_widths = table.column_widths @@ -62,7 +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} + next_page = {'break': 'any', 'page': None} original_page_is_empty = page_is_empty resolve_percentages(group, containing_block=table) group.position_x = rows_x @@ -80,6 +80,15 @@ def group_layout(group, position_y, max_position_y, assert not skip_stack # No breaks inside rows for now for i, row in enumerate(group.children[skip:]): index_row = i + skip + + if i > 0: + page_break = block_level_page_break( + new_group_children[-1], row) + if page_break in ('page', 'recto', 'verso', 'left', 'right'): + next_page['break'] = page_break + resume_at = (index_row, None) + break + resolve_percentages(row, containing_block=table) row.position_x = rows_x row.position_y = position_y @@ -217,14 +226,6 @@ def group_layout(group, position_y, max_position_y, 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 ( @@ -264,6 +265,15 @@ def body_groups_layout(skip_stack, position_y, max_position_y, next_page = {'break': 'any', 'page': None} for i, group in enumerate(table.children[skip:]): index_group = i + skip + + if i > 0: + page_break = block_level_page_break( + new_table_children[-1], group) + if page_break in ('page', 'recto', 'verso', 'left', 'right'): + next_page['break'] = page_break + resume_at = (index_group, None) + break + if group.is_header or group.is_footer: continue new_group, resume_at, next_page = group_layout( @@ -278,16 +288,8 @@ def body_groups_layout(skip_stack, position_y, max_position_y, position_y += new_group.height + border_spacing_y page_is_empty = False - 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) + if resume_at: + resume_at = (index_group, resume_at) break return new_table_children, resume_at, next_page, position_y @@ -440,10 +442,6 @@ def get_column_cells(table, column): group.width = last.position_x + last.width - first.position_x group.height = columns_height - 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): diff --git a/weasyprint/tests/test_layout/test_table.py b/weasyprint/tests/test_layout/test_table.py index fcf7e473d..8c16c4fd7 100644 --- a/weasyprint/tests/test_layout/test_table.py +++ b/weasyprint/tests/test_layout/test_table.py @@ -2144,6 +2144,87 @@ def test_table_page_break_after(): assert p.element_tag == 'p' +@assert_no_logs +def test_table_page_break_before(): + page1, page2, page3, page4, page5, page6 = render_pages(''' + +

Dummy title

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
row 1
row 2
row 3
row 1
row 2
row 3
row 1
row 2
row 3
row 1
row 2
row 3
row 1
row 2
row 3
+

bla bla

+ ''') + html, = page1.children + body, = html.children + h1, = body.children + assert h1.element_tag == 'h1' + + 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) == 3 + assert len(table_group2.children) == 1 + + html, = page3.children + body, = html.children + table_wrapper, = body.children + table, = table_wrapper.children + table_group, = table.children + assert len(table_group.children) == 2 + + html, = page4.children + body, = html.children + 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) == 3 + + html, = page5.children + assert not html.children + + html, = page6.children + body, = html.children + table_wrapper, p = body.children + table, = table_wrapper.children + table_group, = table.children + assert len(table_group.children) == 3 + assert p.element_tag == 'p' + + @assert_no_logs @pytest.mark.parametrize('vertical_align, table_position_y', ( ('top', 8), From af6673d2f737aae50ba91f99837d9055aefddb80 Mon Sep 17 00:00:00 2001 From: grewn0uille Date: Tue, 20 Aug 2019 18:35:05 +0200 Subject: [PATCH 3/8] Fix tests --- weasyprint/layout/tables.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/weasyprint/layout/tables.py b/weasyprint/layout/tables.py index 85fbf83b8..b276512ce 100644 --- a/weasyprint/layout/tables.py +++ b/weasyprint/layout/tables.py @@ -81,7 +81,7 @@ def group_layout(group, position_y, max_position_y, for i, row in enumerate(group.children[skip:]): index_row = i + skip - if i > 0: + if new_group_children: page_break = block_level_page_break( new_group_children[-1], row) if page_break in ('page', 'recto', 'verso', 'left', 'right'): @@ -266,7 +266,7 @@ def body_groups_layout(skip_stack, position_y, max_position_y, for i, group in enumerate(table.children[skip:]): index_group = i + skip - if i > 0: + if new_table_children: page_break = block_level_page_break( new_table_children[-1], group) if page_break in ('page', 'recto', 'verso', 'left', 'right'): From fcda62773126d0da48e38f30f0d68e45e76a4ed4 Mon Sep 17 00:00:00 2001 From: grewn0uille Date: Tue, 20 Aug 2019 18:38:37 +0200 Subject: [PATCH 4/8] Break before footer --- weasyprint/layout/tables.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/weasyprint/layout/tables.py b/weasyprint/layout/tables.py index b276512ce..39f5d8a94 100644 --- a/weasyprint/layout/tables.py +++ b/weasyprint/layout/tables.py @@ -266,6 +266,9 @@ def body_groups_layout(skip_stack, position_y, max_position_y, for i, group in enumerate(table.children[skip:]): index_group = i + skip + if group.is_header or group.is_footer: + continue + if new_table_children: page_break = block_level_page_break( new_table_children[-1], group) @@ -274,8 +277,6 @@ def body_groups_layout(skip_stack, position_y, max_position_y, resume_at = (index_group, None) break - if group.is_header or group.is_footer: - continue new_group, resume_at, next_page = group_layout( group, position_y, max_position_y, page_is_empty, skip_stack) skip_stack = None From 6dc59b8dd84702ccdd036fa60e1d899757538f57 Mon Sep 17 00:00:00 2001 From: grewn0uille Date: Wed, 21 Aug 2019 19:00:55 +0200 Subject: [PATCH 5/8] break pages avoid between tr --- weasyprint/layout/tables.py | 17 ++- weasyprint/tests/test_layout/test_table.py | 123 +++++++++++++++++++++ 2 files changed, 139 insertions(+), 1 deletion(-) diff --git a/weasyprint/layout/tables.py b/weasyprint/layout/tables.py index 39f5d8a94..19b0058df 100644 --- a/weasyprint/layout/tables.py +++ b/weasyprint/layout/tables.py @@ -219,7 +219,22 @@ def group_layout(group, position_y, max_position_y, # Break if this row overflows the page, unless there is no # other content on the page. if next_position_y > max_position_y and not page_is_empty: - resume_at = (index_row, None) + index = len(new_group_children) + while index: + page_break = block_level_page_break( + new_group_children[index - 1], row) + if page_break == 'avoid': + index -= 1 + row = new_group_children[index] + else: + resume_at = (index, None) + new_group_children = new_group_children[:index] + break + else: + if original_page_is_empty: + resume_at = (index_row, None) + else: + return None, None, next_page break position_y = next_position_y diff --git a/weasyprint/tests/test_layout/test_table.py b/weasyprint/tests/test_layout/test_table.py index 8c16c4fd7..557bd5891 100644 --- a/weasyprint/tests/test_layout/test_table.py +++ b/weasyprint/tests/test_layout/test_table.py @@ -2225,6 +2225,129 @@ def test_table_page_break_before(): assert p.element_tag == 'p' +@assert_no_logs +@pytest.mark.parametrize('html, rows', ( + (''' + + + + + + + + +
row 0
row 1
row 2
row 3
+ ''', + [1, 3]), + (''' + + + + + + + + +
row 0
row 1
row 2
row 3
+ ''', + [1, 3]), + (''' + + + + + + + + +
row 0
row 1
row 2
row 3
+ ''', + [2, 2]), + (''' + + + + + + + + +
row 0
row 1
row 2
row 3
+ ''', + [3, 1]), + (''' + + + + + + + + +
row 0
row 1
row 2
row 3
+ ''', + [3, 1]), + (''' + +

wow p

+ + + + + + + +
row 0
row 1
row 2
row 3
+ ''', + [1, 3, 1]) +)) +def test_table_page_break_avoid(html, rows): + pages = render_pages(html) + assert len(pages) == len(rows) + rows_per_page = [] + for page in pages: + html, = page.children + body, = html.children + if body.children[0].element_tag == 'p': + rows_per_page.append(len(body.children)) + continue + else: + table_wrapper, = body.children + table, = table_wrapper.children + rows_in_this_page = 0 + for group in table.children: + for row in group.children: + rows_in_this_page += 1 + rows_per_page.append(rows_in_this_page) + + assert rows_per_page == rows + + @assert_no_logs @pytest.mark.parametrize('vertical_align, table_position_y', ( ('top', 8), From 23e2ba1ec6219a666a414bc701b46da34ab96c15 Mon Sep 17 00:00:00 2001 From: grewn0uille Date: Fri, 23 Aug 2019 18:00:52 +0200 Subject: [PATCH 6/8] Break pages avoid between groups --- weasyprint/layout/blocks.py | 6 ++-- weasyprint/layout/tables.py | 55 ++++++++++++++++++++++++++----------- 2 files changed, 42 insertions(+), 19 deletions(-) diff --git a/weasyprint/layout/blocks.py b/weasyprint/layout/blocks.py index 422c4631c..b7cb4eeb5 100644 --- a/weasyprint/layout/blocks.py +++ b/weasyprint/layout/blocks.py @@ -800,7 +800,9 @@ def find_earlier_page_break(children, absolute_boxes, fixed_boxes): previous_in_flow = child if child.is_in_normal_flow() and ( child.style['break_inside'] not in ('avoid', 'avoid-page')): - if isinstance(child, boxes.BlockBox): + breakable_box_types = ( + boxes.BlockBox, boxes.TableBox, boxes.TableRowGroupBox) + if isinstance(child, breakable_box_types): result = find_earlier_page_break( child.children, absolute_boxes, fixed_boxes) if result: @@ -811,8 +813,6 @@ def find_earlier_page_break(children, absolute_boxes, fixed_boxes): resume_at = (new_child.index, resume_at) index += 1 # Remove placeholders after child break - elif isinstance(child, boxes.TableBox): - pass # TODO: find an earlier break between table rows. else: return None diff --git a/weasyprint/layout/tables.py b/weasyprint/layout/tables.py index 2dae7c3e8..5609f0ace 100644 --- a/weasyprint/layout/tables.py +++ b/weasyprint/layout/tables.py @@ -19,7 +19,9 @@ def table_layout(context, table, max_position_y, skip_stack, containing_block, page_is_empty, absolute_boxes, fixed_boxes): """Layout for a table box.""" # Avoid a circular import - from .blocks import block_container_layout, block_level_page_break + from .blocks import ( + block_container_layout, block_level_page_break, + find_earlier_page_break) column_widths = table.column_widths @@ -79,6 +81,7 @@ def group_layout(group, position_y, max_position_y, assert not skip_stack # No breaks inside rows for now for i, row in enumerate(group.children[skip:]): index_row = i + skip + row.index = index_row if new_group_children: page_break = block_level_page_break( @@ -217,22 +220,22 @@ def group_layout(group, position_y, max_position_y, # Break if this row overflows the page, unless there is no # other content on the page. if next_position_y > max_position_y and not page_is_empty: - index = len(new_group_children) - while index: - page_break = block_level_page_break( - new_group_children[index - 1], row) + if new_group_children: + previous_row = new_group_children[-1] + page_break = block_level_page_break(previous_row, row) if page_break == 'avoid': - index -= 1 - row = new_group_children[index] + earlier_page_break = find_earlier_page_break( + new_group_children, absolute_boxes, fixed_boxes) + if earlier_page_break: + new_group_children, resume_at = earlier_page_break + break else: - resume_at = (index, None) - new_group_children = new_group_children[:index] + resume_at = (index_row, None) break + if original_page_is_empty: + resume_at = (index_row, None) else: - if original_page_is_empty: - resume_at = (index_row, None) - else: - return None, None, next_page + return None, None, next_page break position_y = next_position_y @@ -279,6 +282,7 @@ def body_groups_layout(skip_stack, position_y, max_position_y, for i, group in enumerate(table.children[skip:]): index_group = i + skip + group.index = index_group if group.is_header or group.is_footer: continue @@ -296,7 +300,18 @@ def body_groups_layout(skip_stack, position_y, max_position_y, skip_stack = None if new_group is None: - resume_at = (index_group, None) + if new_table_children: + previous_group = new_table_children[-1] + page_break = block_level_page_break(previous_group, group) + if page_break == 'avoid': + earlier_page_break = find_earlier_page_break( + new_table_children, absolute_boxes, fixed_boxes) + if earlier_page_break is not None: + new_table_children, resume_at = earlier_page_break + break + resume_at = (index_group, None) + else: + return None, None, next_page, position_y break new_table_children.append(new_group) @@ -415,6 +430,15 @@ def get_column_cells(table, column): header, new_table_children, footer, position_y, resume_at, next_page = \ all_groups_layout() + + if new_table_children is None: + assert resume_at is None + table = None + adjoining_margins = [] + collapsing_through = False + return ( + table, resume_at, next_page, adjoining_margins, collapsing_through) + table = table.copy_with_children( ([header] if header is not None else []) + new_table_children + @@ -458,8 +482,7 @@ def get_column_cells(table, column): group.height = columns_height if resume_at and not page_is_empty and ( - table.style['break_inside'] in ('avoid', 'avoid-page') or - not new_table_children): + table.style['break_inside'] in ('avoid', 'avoid-page')): table = None resume_at = None adjoining_margins = [] From d01f941ca653020b12ab98ac7dce82ebd76acf52 Mon Sep 17 00:00:00 2001 From: grewn0uille Date: Fri, 30 Aug 2019 11:45:47 +0200 Subject: [PATCH 7/8] Add tests for page breaks between tbodys --- weasyprint/tests/test_layout/test_table.py | 102 ++++++++++++++++++++- 1 file changed, 101 insertions(+), 1 deletion(-) diff --git a/weasyprint/tests/test_layout/test_table.py b/weasyprint/tests/test_layout/test_table.py index 77e3e9fe8..d72a2e3a6 100644 --- a/weasyprint/tests/test_layout/test_table.py +++ b/weasyprint/tests/test_layout/test_table.py @@ -2324,7 +2324,107 @@ def test_table_page_break_before(): ''', - [1, 3, 1]) + [1, 3, 1]), + (''' + + + + + + + + + + + + +
row 0
row 1
row 2
row 0
row 1
row 2
+ ''', + [2, 3, 1]), + (''' + + + + + + + + + + + + +
row 0
row 1
row 2
row 0
row 1
row 2
+ ''', + [2, 3, 1]), + (''' + + + + + + + + + + + + +
row 0
row 1
row 2
row 0
row 1
row 2
+ ''', + [2, 3, 1]), + (''' + + + + + + + + + + + + +
row 0
row 1
row 2
row 0
row 1
row 2
+ ''', + [2, 3, 1]), + (''' + + + + + + + + + + + + +
row 0
row 1
row 2
row 0
row 1
row 2
+ ''', + [3, 3]), )) def test_table_page_break_avoid(html, rows): pages = render_pages(html) From 958140734b8639fc95f5784eea62e77ed4b0d5a9 Mon Sep 17 00:00:00 2001 From: grewn0uille Date: Thu, 12 Sep 2019 18:23:29 +0200 Subject: [PATCH 8/8] Fix same_broken_child The old code assumed that both skip stacks were absolute, but for the second one previous children have already been skipped. We now check that we're in the first child at each level, meaning that we're still breaking the same child. Related to #923. --- weasyprint/layout/inlines.py | 15 ++++++++++----- weasyprint/tests/test_layout/test_inline.py | 21 +++++++++++++++++++++ 2 files changed, 31 insertions(+), 5 deletions(-) diff --git a/weasyprint/layout/inlines.py b/weasyprint/layout/inlines.py index 0fb8da66a..e474e71e0 100644 --- a/weasyprint/layout/inlines.py +++ b/weasyprint/layout/inlines.py @@ -1294,12 +1294,17 @@ def can_break_inside(box): return False -def same_broken_child(skip_stack_1, skip_stack_2): +def same_broken_child(original_skip_stack, relative_skip_stack): """Check that the skip stacks design the same text box.""" - while isinstance(skip_stack_1, tuple) and isinstance(skip_stack_2, tuple): - if skip_stack_1[1] is None and skip_stack_2[1] is None: + while (isinstance(original_skip_stack, tuple) and + isinstance(relative_skip_stack, tuple)): + if original_skip_stack[1] is None and relative_skip_stack[1] is None: + # The last levels of the two skip_stack are the same return True - if skip_stack_1[0] != skip_stack_2[0]: + if relative_skip_stack[0] != 0: + # If at the current level the skip_stack is not 0, it means that + # it is not the first child that has been cut return False - skip_stack_1, skip_stack_2 = skip_stack_1[1], skip_stack_2[1] + original_skip_stack = original_skip_stack[1] + relative_skip_stack = relative_skip_stack[1] return False diff --git a/weasyprint/tests/test_layout/test_inline.py b/weasyprint/tests/test_layout/test_inline.py index 4ba21e3dd..7a0e0abaa 100644 --- a/weasyprint/tests/test_layout/test_inline.py +++ b/weasyprint/tests/test_layout/test_inline.py @@ -361,6 +361,27 @@ def test_breaking_linebox_regression_9(): assert line2.children[1].text == 'ddd' +@assert_no_logs +def test_breaking_linebox_regression_10(): + # Regression test for https://github.com/Kozea/WeasyPrint/issues/923 + page, = parse( + '' + '

' + ' ' + ' xxxxxx YYY yyyyyy yyy' + ' ZZZZZZ zzzzz' + ' )x ' + '

') + html, = page.children + body, = html.children + p, = body.children + line1, line2, line3, line4 = p.children + assert line1.children[0].children[0].children[0].text == 'xxxxxx YYY' + assert line2.children[0].children[0].children[0].text == 'yyyyyy yyy' + assert line3.children[0].children[0].text == 'ZZZZZZ zzzzz' + assert line4.children[0].text == ')x' + + @assert_no_logs def test_linebox_text(): page, = parse('''