diff --git a/weasyprint/layout/blocks.py b/weasyprint/layout/blocks.py index e7ea66eb7..b7cb4eeb5 100644 --- a/weasyprint/layout/blocks.py +++ b/weasyprint/layout/blocks.py @@ -723,8 +723,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 @@ -732,7 +736,7 @@ 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 @@ -796,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: @@ -807,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/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/layout/tables.py b/weasyprint/layout/tables.py index 7d0b7891d..23a9f1095 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 + from .blocks import ( + block_container_layout, block_level_page_break, + find_earlier_page_break) column_widths = table.column_widths @@ -61,6 +63,7 @@ def table_layout(context, table, max_position_y, skip_stack, containing_block, def group_layout(group, position_y, max_position_y, page_is_empty, skip_stack): resume_at = 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 @@ -78,6 +81,16 @@ 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( + 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 @@ -207,7 +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: - resume_at = (index_row, None) + if new_group_children: + previous_row = new_group_children[-1] + page_break = block_level_page_break(previous_row, row) + if page_break == 'avoid': + 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_row, None) + break + if original_page_is_empty: + resume_at = (index_row, None) + else: + return None, None, next_page break position_y = next_position_y @@ -219,7 +247,7 @@ def group_layout(group, position_y, max_position_y, 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, @@ -240,7 +268,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): @@ -250,16 +278,40 @@ def body_groups_layout(skip_stack, position_y, max_position_y, skip, skip_stack = skip_stack new_table_children = [] resume_at = None + next_page = {'break': 'any', 'page': None} + 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 - new_group, resume_at = group_layout( + + if new_table_children: + 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 + + new_group, resume_at, next_page = group_layout( group, position_y, max_position_y, page_is_empty, skip_stack) 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) @@ -269,7 +321,8 @@ def body_groups_layout(skip_stack, position_y, max_position_y, if resume_at: 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 +331,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 +343,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 +364,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,8 +428,17 @@ 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() + + 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 + @@ -413,10 +481,8 @@ 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']} 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 = [] 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( + '<style>@font-face {src: url(AHEM____.TTF); font-family: ahem}</style>' + '<p style="width:195px; font-family: ahem">' + ' <span>' + ' <span>xxxxxx YYY yyyyyy yyy</span>' + ' ZZZZZZ zzzzz' + ' </span> )x ' + '</p>') + 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(''' diff --git a/weasyprint/tests/test_layout/test_table.py b/weasyprint/tests/test_layout/test_table.py index 6b2b8786e..64c874b2c 100644 --- a/weasyprint/tests/test_layout/test_table.py +++ b/weasyprint/tests/test_layout/test_table.py @@ -2088,6 +2088,390 @@ 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 +def test_table_page_break_before(): + 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 style="break-before: 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 style="break-before: page"><td>row 2</td></tr> + <tr><td>row 3</td></tr> + </tbody> + <tbody> + <tr style="break-before: 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><td>row 3</td></tr> + </tbody> + <tbody style="break-before: left"> + <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, = 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('html, rows', ( + (''' + <style> + @page { size: 100px } + table { table-layout: fixed; width: 100% } + tr { height: 26px } + </style> + <table> + <tbody> + <tr><td>row 0</td></tr> + <tr><td>row 1</td></tr> + <tr style="break-before: avoid"><td>row 2</td></tr> + <tr style="break-before: avoid"><td>row 3</td></tr> + </tbody> + </table> + ''', + [1, 3]), + (''' + <style> + @page { size: 100px } + table { table-layout: fixed; width: 100% } + tr { height: 26px } + </style> + <table> + <tbody> + <tr><td>row 0</td></tr> + <tr style="break-after: avoid"><td>row 1</td></tr> + <tr><td>row 2</td></tr> + <tr style="break-before: avoid"><td>row 3</td></tr> + </tbody> + </table> + ''', + [1, 3]), + (''' + <style> + @page { size: 100px } + table { table-layout: fixed; width: 100% } + tr { height: 26px } + </style> + <table> + <tbody> + <tr><td>row 0</td></tr> + <tr><td>row 1</td></tr> + <tr style="break-after: avoid"><td>row 2</td></tr> + <tr><td>row 3</td></tr> + </tbody> + </table> + ''', + [2, 2]), + (''' + <style> + @page { size: 100px } + table { table-layout: fixed; width: 100% } + tr { height: 26px } + </style> + <table> + <tbody> + <tr style="break-before: avoid"><td>row 0</td></tr> + <tr style="break-before: avoid"><td>row 1</td></tr> + <tr style="break-before: avoid"><td>row 2</td></tr> + <tr style="break-before: avoid"><td>row 3</td></tr> + </tbody> + </table> + ''', + [3, 1]), + (''' + <style> + @page { size: 100px } + table { table-layout: fixed; width: 100% } + tr { height: 26px } + </style> + <table> + <tbody> + <tr style="break-after: avoid"><td>row 0</td></tr> + <tr style="break-after: avoid"><td>row 1</td></tr> + <tr style="break-after: avoid"><td>row 2</td></tr> + <tr style="break-after: avoid"><td>row 3</td></tr> + </tbody> + </table> + ''', + [3, 1]), + (''' + <style> + @page { size: 100px } + table { table-layout: fixed; width: 100% } + tr { height: 26px } + p { height: 26px } + </style> + <p>wow p</p> + <table> + <tbody> + <tr style="break-after: avoid"><td>row 0</td></tr> + <tr style="break-after: avoid"><td>row 1</td></tr> + <tr style="break-after: avoid"><td>row 2</td></tr> + <tr style="break-after: avoid"><td>row 3</td></tr> + </tbody> + </table> + ''', + [1, 3, 1]), + (''' + <style> + @page { size: 100px } + table { table-layout: fixed; width: 100% } + tr { height: 30px } + </style> + <table> + <tbody style="break-after: avoid"> + <tr><td>row 0</td></tr> + <tr><td>row 1</td></tr> + <tr><td>row 2</td></tr> + </tbody> + <tbody> + <tr><td>row 0</td></tr> + <tr><td>row 1</td></tr> + <tr><td>row 2</td></tr> + </tbody> + </table> + ''', + [2, 3, 1]), + (''' + <style> + @page { size: 100px } + table { table-layout: fixed; width: 100% } + tr { height: 30px } + </style> + <table> + <tbody> + <tr><td>row 0</td></tr> + <tr><td>row 1</td></tr> + <tr><td>row 2</td></tr> + </tbody> + <tbody style="break-before: avoid"> + <tr><td>row 0</td></tr> + <tr><td>row 1</td></tr> + <tr><td>row 2</td></tr> + </tbody> + </table> + ''', + [2, 3, 1]), + (''' + <style> + @page { size: 100px } + table { table-layout: fixed; width: 100% } + tr { height: 30px } + </style> + <table> + <tbody> + <tr><td>row 0</td></tr> + <tr><td>row 1</td></tr> + <tr><td>row 2</td></tr> + </tbody> + <tbody> + <tr style="break-before: avoid"><td>row 0</td></tr> + <tr><td>row 1</td></tr> + <tr><td>row 2</td></tr> + </tbody> + </table> + ''', + [2, 3, 1]), + (''' + <style> + @page { size: 100px } + table { table-layout: fixed; width: 100% } + tr { height: 30px } + </style> + <table> + <tbody> + <tr><td>row 0</td></tr> + <tr><td>row 1</td></tr> + <tr style="break-after: avoid"><td>row 2</td></tr> + </tbody> + <tbody> + <tr><td>row 0</td></tr> + <tr><td>row 1</td></tr> + <tr><td>row 2</td></tr> + </tbody> + </table> + ''', + [2, 3, 1]), + (''' + <style> + @page { size: 100px } + table { table-layout: fixed; width: 100% } + tr { height: 30px } + </style> + <table> + <tbody style="break-after: avoid"> + <tr><td>row 0</td></tr> + <tr><td>row 1</td></tr> + <tr style="break-after: page"><td>row 2</td></tr> + </tbody> + <tbody> + <tr><td>row 0</td></tr> + <tr><td>row 1</td></tr> + <tr><td>row 2</td></tr> + </tbody> + </table> + ''', + [3, 3]), +)) +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),