def table_layout(context, table, bottom_space, skip_stack, containing_block,
                 page_is_empty, absolute_boxes, fixed_boxes):
    """Layout for a table box."""
    from .block import (  # isort:skip
        avoid_page_break, block_container_layout, block_level_page_break,
        find_earlier_page_break, force_page_break, remove_placeholders)

    # Remove top and bottom decorations for split tables.
    has_header = table.children and table.children[0].is_header
    has_footer = table.children and table.children[-1].is_footer
    collapse = table.style['border_collapse'] == 'collapse'
    remove_start_decoration = skip_stack is not None and not has_header
    table.remove_decoration(remove_start_decoration, end=False)

    # Set border spacings.
    if collapse:
        border_spacing_x = border_spacing_y = 0
    else:
        border_spacing_x, border_spacing_y = table.style['border_spacing']

    # Define column positions.
    column_widths = table.column_widths
    column_positions = table.column_positions = []
    rows_left_x = table.content_box_x() + border_spacing_x
    if table.style['direction'] == 'ltr':
        position_x = table.content_box_x()
        rows_x = position_x + border_spacing_x
        for width in column_widths:
            position_x += border_spacing_x
            column_positions.append(position_x)
            position_x += width
        rows_width = position_x - rows_x
    else:
        position_x = table.content_box_x() + table.width
        rows_x = position_x - border_spacing_x
        for width in column_widths:
            position_x -= border_spacing_x
            position_x -= width
            column_positions.append(position_x)
        rows_width = rows_x - position_x

    # Set border top width on tables with collapsed borders and split cells.
    if collapse:
        table.skip_cell_border_top = False
        table.skip_cell_border_bottom = False
        split_cells = False
        if skip_stack:
            (skipped_groups, group_skip_stack), = skip_stack.items()
            if group_skip_stack:
                (skipped_rows, cells_skip_stack), = group_skip_stack.items()
                if cells_skip_stack:
                    split_cells = True
            else:
                skipped_rows = 0
            for group in table.children[:skipped_groups]:
                skipped_rows += len(group.children)
        else:
            skipped_rows = 0
        if not split_cells and not has_header:
            _, horizontal_borders = table.collapsed_border_grid
            if horizontal_borders:
                table.border_top_width = max(
                    width for _, (_, width, _)
                    in horizontal_borders[skipped_rows]) / 2

    # Make this a sub-function so that many local variables like rows_x
    # don't need to be passed as parameters.
    def group_layout(group, position_y, bottom_space, 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_left_x
        group.position_y = position_y
        group.width = rows_width
        new_group_children = []
        # For each row, cells for which this is the last row (with rowspan).
        ending_cells_by_row = [[] for row in group.children]

        is_group_start = skip_stack is None
        if is_group_start:
            skip = 0
        else:
            (skip, skip_stack), = skip_stack.items()
        for index_row, row in enumerate(group.children[skip:], start=skip):
            row.index = index_row

            if new_group_children:
                page_break = block_level_page_break(
                    new_group_children[-1], row)
                if force_page_break(page_break, context):
                    next_page['break'] = page_break
                    resume_at = {index_row: None}
                    break

            resolve_percentages(row, containing_block=table)
            row.position_x = rows_left_x
            row.position_y = position_y
            row.width = rows_width
            # Place cells at the top of the row and layout their content.
            new_row_children = []
            for index_cell, cell in enumerate(row.children):
                spanned_widths = column_widths[cell.grid_x:][:cell.colspan]
                # In the fixed layout the grid width is set by cells in
                # the first row and column elements.
                # This may be less than the previous value of cell.colspan
                # if that would bring the cell beyond the grid width.
                cell.colspan = len(spanned_widths)
                if cell.colspan == 0:
                    # The cell is entierly beyond the grid width, remove it
                    # entierly. Subsequent cells in the same row have greater
                    # grid_x, so they are beyond too.
                    cell_index = row.children.index(cell)
                    ignored_cells = row.children[cell_index:]
                    LOGGER.warning(
                        'This table row has more columns than the table, '
                        f'ignored {len(ignored_cells)} cells: {ignored_cells}')
                    break
                resolve_percentages(cell, containing_block=table)
                if table.style['direction'] == 'ltr':
                    cell.position_x = column_positions[cell.grid_x]
                else:
                    cell.position_x = column_positions[cell.grid_x + cell.colspan - 1]
                cell.position_y = row.position_y
                cell.margin_top = 0
                cell.margin_left = 0
                cell.width = 0
                borders_plus_padding = cell.border_width()  # with width==0
                # TODO: we should remove the number of columns with no
                # originating cells to cell.colspan, see test_layout_table_auto_49.
                cell.width = (
                    sum(spanned_widths) +
                    border_spacing_x * (cell.colspan - 1) -
                    borders_plus_padding)
                if skip_stack:
                    if index_cell in skip_stack:
                        cell_skip_stack = skip_stack[index_cell]
                    else:
                        cell_skip_stack = {len(cell.children): None}
                else:
                    cell_skip_stack = None

                # Adapt cell and table collapsing borders when a row is split.
                if cell_skip_stack and collapse:
                    if has_header:
                        # We have a header, we have to adapt the position of
                        # the split cell to match the header’s bottom border.
                        header_rows = table.children[0].children
                        if header_rows and header_rows[-1].children:
                            cell.position_y += max(
                                header.border_bottom_width
                                for header in header_rows[-1].children)
                    else:
                        # We don’t have a header, we have to skip the
                        # decoration at the top of the table when it’s drawn.
                        table.skip_cell_border_top = True

                # First try to render content as if there was already something
                # on the page to avoid hitting block_level_layout’s TODO. Then
                # force to render something if the page is actually empty, or
                # just draw an empty cell otherwise. See
                # test_table_break_children_margin.
                # Pretend that height is not set, keeping computed height as a minimum.
                cell.computed_height = cell.height
                cell.height = 'auto'
                original_style = cell.style
                if cell.style['height'] != 'auto':
                    style_copy = cell.style.copy()
                    style_copy['height'] = 'auto'
                    cell.style = style_copy
                new_cell, cell_resume_at, _, _, _, _ = block_container_layout(
                    context, cell, bottom_space, cell_skip_stack,
                    page_is_empty=page_is_empty, absolute_boxes=absolute_boxes,
                    fixed_boxes=fixed_boxes, adjoining_margins=None,
                    discard=False, max_lines=None)
                cell.style = original_style
                if new_cell is None:
                    cell = cell.copy_with_children([])
                    cell, _, _, _, _, _ = block_container_layout(
                        context, cell, bottom_space, cell_skip_stack,
                        page_is_empty=True, absolute_boxes=[], fixed_boxes=[],
                        adjoining_margins=None, discard=False, max_lines=None)
                    cell_resume_at = {0: None}
                else:
                    cell = new_cell

                cell.remove_decoration(start=cell_skip_stack is not None, end=False)
                if cell_resume_at:
                    if resume_at is None:
                        resume_at = {index_row: {}}
                    resume_at[index_row][index_cell] = cell_resume_at
                cell.empty = not any(
                    child.is_floated() or child.is_in_normal_flow()
                    for child in cell.children)
                cell.content_height = cell.height
                if cell.computed_height != 'auto':
                    cell.height = max(cell.height, cell.computed_height)
                new_row_children.append(cell)

            if resume_at and not page_is_empty:
                # Avoid break when "break-inside: avoid" is set on row or any
                # on its cells.
                avoid_break = (
                    avoid_page_break(row.style['break_inside'], context) or any(
                        avoid_page_break(cell.style['break_inside'], context)
                        for cell in row.children))
                if avoid_break:
                    resume_at = {index_row: {}}
                    remove_placeholders(
                        context, new_row_children, absolute_boxes, fixed_boxes)
                    break

            if resume_at:
                # Remove bottom decoration if row is split.
                for cell in new_row_children:
                    cell.remove_decoration(start=False, end=True)

            row = row.copy_with_children(new_row_children)

            # Table height algorithm
            # https://www.w3.org/TR/CSS21/tables.html#height-layout

            # Set row baseline with cells with vertical-align: baseline.
            baseline_cells = []
            for cell in row.children:
                vertical_align = cell.style['vertical_align']
                if vertical_align in ('top', 'middle', 'bottom'):
                    cell.vertical_align = vertical_align
                else:
                    # Assume 'baseline' for any other value
                    cell.vertical_align = 'baseline'
                    cell.baseline = cell_baseline(cell)
                    baseline_cells.append(cell)
            if baseline_cells:
                row.baseline = max(cell.baseline for cell in baseline_cells)
                for cell in baseline_cells:
                    extra = row.baseline - cell.baseline
                    if cell.baseline != row.baseline and extra:
                        add_top_padding(cell, extra)

            # Set row height.
            for cell in row.children:
                ending_cells_by_row[cell.rowspan - 1].append(cell)
            ending_cells = ending_cells_by_row.pop(0)
            if ending_cells:  # in this row
                if row.height == 'auto':
                    row_bottom_y = max(
                        cell.position_y + cell.border_height()
                        for cell in ending_cells)
                    row.height = max(row_bottom_y - row.position_y, 0)
                else:
                    row.height = max(row.height, max(
                        row_cell.border_height() for row_cell in ending_cells))
                    row_bottom_y = row.position_y + row.height
            else:
                row_bottom_y = row.position_y
                row.height = 0

            if not baseline_cells:
                row.baseline = row_bottom_y

            # Add extra padding to make the cells the same height as the row
            # and honor vertical-align.
            for cell in ending_cells:
                cell_bottom_y = cell.position_y + cell.border_height()
                extra = row_bottom_y - cell_bottom_y
                if extra:
                    if cell.vertical_align == 'bottom':
                        add_top_padding(cell, extra)
                    elif cell.vertical_align == 'middle':
                        extra /= 2
                        add_top_padding(cell, extra)
                        cell.padding_bottom += extra
                    else:
                        cell.padding_bottom += extra
                if cell.computed_height != 'auto':
                    vertical_align_shift = 0
                    if cell.vertical_align == 'middle':
                        vertical_align_shift = (
                            cell.computed_height - cell.content_height) / 2
                    elif cell.vertical_align == 'bottom':
                        vertical_align_shift = (
                            cell.computed_height - cell.content_height)
                    if vertical_align_shift > 0:
                        for child in cell.children:
                            child.translate(dy=vertical_align_shift)

            next_position_y = row.position_y + row.height
            if resume_at is None:
                next_position_y += border_spacing_y

            # Break if one cell was broken.
            break_cell = False
            if resume_at:
                if all(child.empty for child in row.children):
                    # No cell was displayed, give up row.
                    next_position_y = inf
                    page_is_empty = False
                    resume_at = None
                else:
                    break_cell = True

            # Break if this row overflows the page, unless there is no
            # other content on the page.
            overflow = context.overflows_page(bottom_space, next_position_y)
            if not page_is_empty and overflow:
                remove_placeholders(context, row.children, absolute_boxes, fixed_boxes)
                if new_group_children:
                    previous_row = new_group_children[-1]
                    page_break = block_level_page_break(previous_row, row)
                    if avoid_page_break(page_break, context):
                        earlier_page_break = find_earlier_page_break(
                            context, 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

            new_group_children.append(row)
            position_y = next_position_y
            page_is_empty = False
            skip_stack = None

            if break_cell and collapse and not has_footer:
                table.skip_cell_border_bottom = True

            if break_cell or resume_at:
                break

        # Do not keep the row group if we made a page break
        # before any of its rows or with 'avoid'.
        abort = (
            resume_at and
            not original_page_is_empty and (
                avoid_page_break(group.style['break_inside'], context) or
                not new_group_children))
        if abort:
            remove_placeholders(
                context, new_group_children, absolute_boxes, fixed_boxes)
            return None, None, next_page

        group = group.copy_with_children(new_group_children)
        group.remove_decoration(start=not is_group_start, end=resume_at is not None)

        # Set missing baselines in a second loop because of rowspan.
        for row in group.children:
            if row.baseline is None:
                if row.children:
                    # Set baseline to lowest bottom content edge.
                    row.baseline = max(
                        cell.content_box_y() + cell.height
                        for cell in row.children) - row.position_y
                else:
                    row.baseline = 0
        group.height = position_y - group.position_y
        if group.children:
            # The last border spacing is outside of the group.
            group.height -= border_spacing_y

        return group, resume_at, next_page

    def body_groups_layout(skip_stack, position_y, bottom_space, page_is_empty):
        if skip_stack is None:
            skip = 0
        else:
            (skip, skip_stack), = skip_stack.items()
        new_table_children = []
        resume_at = None
        next_page = {'break': 'any', 'page': None}

        for i, group in enumerate(table.children[skip:]):
            if group.is_header or group.is_footer:
                continue

            # Index is useless for headers and footers, as we never want to
            # break pages after the header or before the footer.
            index_group = i + skip
            group.index = index_group

            if new_table_children:
                page_break = block_level_page_break(new_table_children[-1], group)
                if force_page_break(page_break, context):
                    next_page['break'] = page_break
                    resume_at = {index_group: None}
                    break

            new_group, resume_at, next_page = group_layout(
                group, position_y, bottom_space, page_is_empty, skip_stack)
            skip_stack = None

            if new_group is None:
                if new_table_children:
                    previous_group = new_table_children[-1]
                    page_break = block_level_page_break(previous_group, group)
                    if avoid_page_break(page_break, context):
                        earlier_page_break = find_earlier_page_break(
                            context, 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)
            position_y += new_group.height + border_spacing_y
            page_is_empty = False

            if resume_at:
                resume_at = {index_group: resume_at}
                break

        return new_table_children, resume_at, next_page, position_y

    # Layout row groups, rows and cells.
    position_y = table.content_box_y()
    if skip_stack is None:
        position_y += border_spacing_y
    initial_position_y = position_y
    table_rows = [
        child for child in table.children
        if not child.is_header and not child.is_footer]

    def all_groups_layout():
        # If the page is not empty, we try to render the header and the footer
        # on it. If the table does not fit on the page, we try to render it on
        # the next page.

        # If the page is empty and the header and footer are too big, there
        # are not rendered. If no row can be rendered because of the header and
        # the footer, the header and/or the footer are not rendered.

        if page_is_empty:
            header_footer_bottom_space = bottom_space
        else:
            header_footer_bottom_space = -inf

        if has_header:
            header = table.children[0]
            header, resume_at, next_page = group_layout(
                header, position_y, header_footer_bottom_space,
                skip_stack=None, page_is_empty=False)
            if header and not resume_at:
                header_height = header.height + border_spacing_y
            else:
                # Header too big for the page.
                header = None
        else:
            header = None

        if has_footer:
            footer = table.children[-1]
            footer, resume_at, next_page = group_layout(
                footer, position_y, header_footer_bottom_space,
                skip_stack=None, page_is_empty=False)
            if footer and not resume_at:
                footer_height = footer.height + border_spacing_y
            else:
                # Footer too big for the page.
                footer = None
        else:
            footer = None

        # Don't remove headers and footers if breaks are avoided in line groups
        if skip_stack:
            skip, = skip_stack
        else:
            skip = 0
        avoid_breaks = False
        for group in table.children[skip:]:
            if not group.is_header and not group.is_footer:
                avoid_breaks = avoid_page_break(group.style['break_inside'], context)
                break

        if header and footer:
            # Try with both the header and footer.
            new_table_children, resume_at, next_page, end_position_y = (
                body_groups_layout(
                    skip_stack, position_y + header_height,
                    bottom_space + footer_height, page_is_empty=avoid_breaks))
            if new_table_children or not table_rows 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,
                    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, next_page, end_position_y = (
                body_groups_layout(
                    skip_stack, position_y + header_height, bottom_space,
                    page_is_empty=avoid_breaks))
            if new_table_children or not table_rows or not page_is_empty:
                return (
                    header, new_table_children, footer, 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, next_page, end_position_y = (
                body_groups_layout(
                    skip_stack, position_y, bottom_space + footer_height,
                    page_is_empty=avoid_breaks))
            if new_table_children or not table_rows 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,
                    next_page)
            else:
                # We could not fit any content, drop the footer.
                footer = None

        assert not (header or footer)
        new_table_children, resume_at, next_page, end_position_y = (
            body_groups_layout(skip_stack, position_y, bottom_space, page_is_empty))
        return header, new_table_children, footer, end_position_y, resume_at, next_page

    def get_column_cells(table, column):
        """Return closure getting the column cells."""
        return lambda: [
            cell
            for row_group in table.children
            for row in row_group.children
            for cell in row.children
            if cell.grid_x == column.grid_x]

    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 +
        ([footer] if footer is not None else []))
    table.column_groups = tuple(
        column_group.deepcopy() for column_group in table.column_groups)
    remove_end_decoration = resume_at is not None and not has_footer
    table.remove_decoration(remove_start_decoration, remove_end_decoration)
    if collapse:
        table.skipped_rows = skipped_rows

    # If the height property has a bigger value, just add blank space
    # below the last row group.
    table.height = max(
        table.height if table.height != 'auto' else 0,
        position_y - table.content_box_y())

    # Layout column groups and columns.
    columns_height = position_y - initial_position_y
    if table.children:
        # The last border spacing is below the columns.
        columns_height -= border_spacing_y
    for group in table.column_groups:
        for column in group.children:
            resolve_percentages(column, containing_block=table)
            if column.grid_x < len(column_positions):
                column.position_x = column_positions[column.grid_x]
                column.position_y = initial_position_y
                column.width = column_widths[column.grid_x]
                column.height = columns_height
            else:
                # Ignore extra empty columns.
                column.position_x = 0
                column.position_y = 0
                column.width = 0
                column.height = 0
            resolve_percentages(group, containing_block=table)
            column.get_cells = get_column_cells(table, column)
        first = group.children[0]
        last = group.children[-1]
        group.position_x = first.position_x
        group.position_y = initial_position_y
        group.width = last.position_x + last.width - first.position_x
        group.height = columns_height

    # Invert columns for drawing.
    if table.style['direction'] == 'rtl':
        column_widths.reverse()
        column_positions.reverse()

    avoid_break = avoid_page_break(table.style['break_inside'], context)
    if resume_at and not page_is_empty and avoid_break:
        remove_placeholders(context, [table], absolute_boxes, fixed_boxes)
        table = None
        resume_at = None
    adjoining_margins = []
    collapsing_through = False

    return table, resume_at, next_page, adjoining_margins, collapsing_through
Layout for a table box.
weasyprint/layout/table.py