2011-11-22 16:06:50 +04:00
|
|
|
|
# coding: utf8
|
2012-03-22 02:19:27 +04:00
|
|
|
|
"""
|
|
|
|
|
weasyprint.layout.tables
|
|
|
|
|
------------------------
|
2011-11-22 16:06:50 +04:00
|
|
|
|
|
2012-03-22 02:19:27 +04:00
|
|
|
|
Layout for tables and internal table boxes.
|
2011-11-22 16:06:50 +04:00
|
|
|
|
|
2012-03-22 02:19:27 +04:00
|
|
|
|
:copyright: Copyright 2011-2012 Simon Sapin and contributors, see AUTHORS.
|
|
|
|
|
:license: BSD, see LICENSE for details.
|
2011-11-22 16:06:50 +04:00
|
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
2012-02-17 21:49:58 +04:00
|
|
|
|
from __future__ import division, unicode_literals
|
2011-11-22 16:06:50 +04:00
|
|
|
|
|
2012-02-17 21:49:58 +04:00
|
|
|
|
from ..compat import xrange
|
2012-02-22 20:12:40 +04:00
|
|
|
|
from ..logger import LOGGER
|
2011-11-24 20:56:05 +04:00
|
|
|
|
from ..formatting_structure import boxes
|
2012-04-12 14:51:21 +04:00
|
|
|
|
from ..css.properties import Dimension
|
2011-11-22 16:06:50 +04:00
|
|
|
|
from .percentages import resolve_percentages, resolve_one_percentage
|
2012-04-10 16:37:54 +04:00
|
|
|
|
from .preferred import table_and_columns_preferred_widths
|
2011-11-22 16:06:50 +04:00
|
|
|
|
|
|
|
|
|
|
2012-03-16 22:00:02 +04:00
|
|
|
|
def table_layout(document, table, max_position_y, skip_stack,
|
2012-05-09 21:01:32 +04:00
|
|
|
|
containing_block, device_size, page_is_empty, absolute_boxes):
|
2011-11-22 16:06:50 +04:00
|
|
|
|
"""Layout for a table box.
|
|
|
|
|
|
|
|
|
|
For now only the fixed layout and separate border model are supported.
|
|
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
# Avoid a circular import
|
2012-04-06 18:42:06 +04:00
|
|
|
|
from .blocks import block_container_layout
|
2011-11-22 16:06:50 +04:00
|
|
|
|
|
2011-11-30 17:56:57 +04:00
|
|
|
|
column_widths = table.column_widths
|
2011-11-22 16:06:50 +04:00
|
|
|
|
|
2011-11-24 20:56:05 +04:00
|
|
|
|
border_spacing_x, border_spacing_y = table.style.border_spacing
|
2011-12-01 15:07:45 +04:00
|
|
|
|
# TODO: reverse this for direction: rtl
|
2011-11-24 20:56:05 +04:00
|
|
|
|
column_positions = []
|
2011-11-25 19:50:09 +04:00
|
|
|
|
position_x = table.content_box_x()
|
2011-11-29 14:22:52 +04:00
|
|
|
|
rows_x = position_x + border_spacing_x
|
2011-11-24 20:56:05 +04:00
|
|
|
|
for width in column_widths:
|
|
|
|
|
position_x += border_spacing_x
|
|
|
|
|
column_positions.append(position_x)
|
|
|
|
|
position_x += width
|
2011-11-25 19:50:09 +04:00
|
|
|
|
rows_width = position_x - rows_x
|
2011-11-24 20:56:05 +04:00
|
|
|
|
|
2012-05-04 19:05:16 +04:00
|
|
|
|
# Make this a sub-function so that many local variables like rows_x
|
|
|
|
|
# need not be passed as parameters.
|
|
|
|
|
def group_layout(group, position_y, max_position_y,
|
|
|
|
|
page_is_empty, skip_stack):
|
|
|
|
|
resume_at = None
|
2011-11-29 13:41:25 +04:00
|
|
|
|
resolve_percentages(group, containing_block=table)
|
2011-11-25 19:50:09 +04:00
|
|
|
|
group.position_x = rows_x
|
2011-11-24 20:56:05 +04:00
|
|
|
|
group.position_y = position_y
|
2011-11-25 19:50:09 +04:00
|
|
|
|
group.width = rows_width
|
2011-11-25 20:20:41 +04:00
|
|
|
|
new_group_children = []
|
2011-11-24 20:56:05 +04:00
|
|
|
|
# For each rows, cells for which this is the last row (with rowspan)
|
|
|
|
|
ending_cells_by_row = [[] for row in group.children]
|
2012-03-21 21:00:24 +04:00
|
|
|
|
|
|
|
|
|
if skip_stack is None:
|
|
|
|
|
skip = 0
|
|
|
|
|
else:
|
|
|
|
|
skip, skip_stack = skip_stack
|
|
|
|
|
for index_row, row in group.enumerate_skip(skip):
|
2011-11-29 13:41:25 +04:00
|
|
|
|
resolve_percentages(row, containing_block=table)
|
2011-11-25 19:50:09 +04:00
|
|
|
|
row.position_x = rows_x
|
2011-11-24 20:56:05 +04:00
|
|
|
|
row.position_y = position_y
|
2011-11-25 19:50:09 +04:00
|
|
|
|
row.width = rows_width
|
2011-11-24 20:56:05 +04:00
|
|
|
|
# Place cells at the top of the row and layout their content
|
|
|
|
|
new_row_children = []
|
2011-12-01 15:07:45 +04:00
|
|
|
|
for cell in 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.
|
2011-12-16 15:19:10 +04:00
|
|
|
|
cell_index = row.children.index(cell)
|
|
|
|
|
ignored_cells = row.children[cell_index:]
|
|
|
|
|
LOGGER.warn('This table row has more columns than '
|
|
|
|
|
'the table, ignored %i cells: %r',
|
|
|
|
|
len(ignored_cells), ignored_cells)
|
2011-12-01 15:07:45 +04:00
|
|
|
|
break
|
2011-11-29 13:41:25 +04:00
|
|
|
|
resolve_percentages(cell, containing_block=table)
|
2011-11-24 20:56:05 +04:00
|
|
|
|
cell.position_x = column_positions[cell.grid_x]
|
|
|
|
|
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
|
|
|
|
|
cell.width = (
|
2011-12-01 15:07:45 +04:00
|
|
|
|
sum(spanned_widths)
|
2011-11-24 20:56:05 +04:00
|
|
|
|
+ border_spacing_x * (cell.colspan - 1)
|
|
|
|
|
- borders_plus_padding)
|
|
|
|
|
# The computed height is a minimum
|
|
|
|
|
computed_cell_height = cell.height
|
|
|
|
|
cell.height = 'auto'
|
2012-04-06 18:42:06 +04:00
|
|
|
|
cell, _, _, _, _ = block_container_layout(
|
2011-12-02 18:31:23 +04:00
|
|
|
|
document, cell,
|
2011-11-24 20:56:05 +04:00
|
|
|
|
max_position_y=float('inf'),
|
|
|
|
|
skip_stack=None,
|
|
|
|
|
device_size=device_size,
|
2012-05-09 21:01:32 +04:00
|
|
|
|
page_is_empty=True,
|
|
|
|
|
absolute_boxes=absolute_boxes)
|
2011-11-24 20:56:05 +04:00
|
|
|
|
if computed_cell_height != 'auto':
|
|
|
|
|
cell.height = max(cell.height, computed_cell_height)
|
|
|
|
|
new_row_children.append(cell)
|
2011-11-29 13:41:25 +04:00
|
|
|
|
|
2011-11-25 20:20:41 +04:00
|
|
|
|
row = row.copy_with_children(new_row_children)
|
2011-11-24 20:56:05 +04:00
|
|
|
|
|
|
|
|
|
# Table height algorithm
|
|
|
|
|
# http://www.w3.org/TR/CSS21/tables.html#height-layout
|
|
|
|
|
|
|
|
|
|
# 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:
|
|
|
|
|
if cell.baseline != row.baseline:
|
|
|
|
|
add_top_padding(cell, row.baseline - cell.baseline)
|
|
|
|
|
else:
|
|
|
|
|
row.baseline = None
|
|
|
|
|
|
|
|
|
|
# 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
|
|
|
|
|
row_bottom_y = max(
|
|
|
|
|
cell.position_y + cell.border_height()
|
|
|
|
|
for cell in ending_cells)
|
|
|
|
|
row.height = row_bottom_y - row.position_y
|
|
|
|
|
else:
|
|
|
|
|
row_bottom_y = row.position_y
|
|
|
|
|
row.height = 0
|
|
|
|
|
|
|
|
|
|
# Add extra padding to make the cells the same height as the row
|
|
|
|
|
for cell in ending_cells:
|
|
|
|
|
cell_bottom_y = cell.position_y + cell.border_height()
|
|
|
|
|
extra = row_bottom_y - cell_bottom_y
|
|
|
|
|
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
|
|
|
|
|
|
2012-03-21 21:00:24 +04:00
|
|
|
|
next_position_y = position_y + row.height + border_spacing_y
|
|
|
|
|
# Break if this row overflows the page, unless there is no
|
|
|
|
|
# other content on the page.
|
2012-05-04 19:05:16 +04:00
|
|
|
|
if next_position_y > max_position_y and not page_is_empty:
|
2012-03-21 21:00:24 +04:00
|
|
|
|
resume_at = (index_row, None)
|
|
|
|
|
break
|
2012-03-16 22:00:02 +04:00
|
|
|
|
|
2012-03-21 21:00:24 +04:00
|
|
|
|
position_y = next_position_y
|
|
|
|
|
new_group_children.append(row)
|
2012-05-04 19:05:16 +04:00
|
|
|
|
page_is_empty = False
|
2011-11-24 20:56:05 +04:00
|
|
|
|
|
2012-05-04 19:05:16 +04:00
|
|
|
|
# Do not keep the row group if we made a page break
|
|
|
|
|
# before any of its rows or with 'avoid'
|
|
|
|
|
if resume_at and (group.style.page_break_inside == 'avoid'
|
|
|
|
|
or not new_group_children):
|
|
|
|
|
return None, None
|
|
|
|
|
|
|
|
|
|
group = group.copy_with_children(new_group_children)
|
|
|
|
|
|
|
|
|
|
# Set missing baselines in a second loop because of rowspan
|
|
|
|
|
for row in group.children:
|
|
|
|
|
if row.baseline is None:
|
|
|
|
|
if row.children:
|
|
|
|
|
# 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
|
2012-03-21 21:36:03 +04:00
|
|
|
|
|
2012-05-04 19:05:16 +04:00
|
|
|
|
return group, resume_at
|
2012-03-21 21:36:03 +04:00
|
|
|
|
|
2012-05-04 19:05:16 +04:00
|
|
|
|
def body_groups_layout(skip_stack, position_y, max_position_y,
|
|
|
|
|
page_is_empty):
|
|
|
|
|
if skip_stack is None:
|
|
|
|
|
skip = 0
|
|
|
|
|
else:
|
|
|
|
|
skip, skip_stack = skip_stack
|
|
|
|
|
new_table_children = []
|
|
|
|
|
resume_at = None
|
|
|
|
|
for index_group, group in table.enumerate_skip(skip):
|
|
|
|
|
if group.is_header or group.is_footer:
|
|
|
|
|
continue
|
|
|
|
|
new_group, resume_at = 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)
|
|
|
|
|
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, position_y
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Layout for row groups, rows and cells
|
|
|
|
|
position_y = table.content_box_y() + border_spacing_y
|
|
|
|
|
initial_position_y = 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, position_y, max_position_y,
|
|
|
|
|
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 table.children and table.children[-1].is_footer:
|
|
|
|
|
footer = table.children[-1]
|
|
|
|
|
footer, resume_at = group_layout(
|
|
|
|
|
footer, position_y, max_position_y,
|
|
|
|
|
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
|
|
|
|
|
|
|
|
|
|
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=False)
|
|
|
|
|
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)
|
|
|
|
|
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=False)
|
|
|
|
|
if new_table_children or not page_is_empty:
|
|
|
|
|
return (header, new_table_children, footer,
|
|
|
|
|
end_position_y, resume_at)
|
|
|
|
|
else:
|
|
|
|
|
# We could not fit any content, drop the footer
|
|
|
|
|
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=False)
|
|
|
|
|
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)
|
|
|
|
|
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
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
header, new_table_children, footer, position_y, resume_at = \
|
|
|
|
|
all_groups_layout()
|
|
|
|
|
table = table.copy_with_children(
|
|
|
|
|
([header] if header is not None else []) +
|
|
|
|
|
new_table_children +
|
|
|
|
|
([footer] if footer is not None else []))
|
2011-11-24 20:56:05 +04:00
|
|
|
|
|
2011-11-25 20:06:51 +04:00
|
|
|
|
# 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())
|
2011-11-24 20:56:05 +04:00
|
|
|
|
|
|
|
|
|
# Layout for column groups and columns
|
2011-11-25 19:50:09 +04:00
|
|
|
|
columns_height = position_y - initial_position_y
|
|
|
|
|
if table.children:
|
|
|
|
|
# The last border spacing is below the columns.
|
|
|
|
|
columns_height -= border_spacing_y
|
2011-11-24 20:56:05 +04:00
|
|
|
|
for group in table.column_groups:
|
|
|
|
|
for column in group.children:
|
2011-12-30 20:19:02 +04:00
|
|
|
|
resolve_percentages(column, containing_block=table)
|
2011-11-24 20:56:05 +04:00
|
|
|
|
column.position_x = column_positions[column.grid_x]
|
2011-11-25 19:50:09 +04:00
|
|
|
|
column.position_y = initial_position_y
|
2011-11-24 20:56:05 +04:00
|
|
|
|
column.width = column_widths[column.grid_x]
|
2011-11-25 19:50:09 +04:00
|
|
|
|
column.height = columns_height
|
2011-12-30 20:19:02 +04:00
|
|
|
|
resolve_percentages(group, containing_block=table)
|
2011-11-24 20:56:05 +04:00
|
|
|
|
first = group.children[0]
|
|
|
|
|
last = group.children[-1]
|
|
|
|
|
group.position_x = first.position_x
|
2011-11-25 19:50:09 +04:00
|
|
|
|
group.position_y = initial_position_y
|
2011-11-24 20:56:05 +04:00
|
|
|
|
group.width = last.position_x + last.width - first.position_x
|
2011-11-25 19:50:09 +04:00
|
|
|
|
group.height = columns_height
|
2011-11-24 20:56:05 +04:00
|
|
|
|
|
2012-03-21 21:09:03 +04:00
|
|
|
|
if resume_at and not page_is_empty and (
|
2012-05-04 19:05:16 +04:00
|
|
|
|
table.style.page_break_inside == 'avoid'
|
|
|
|
|
or not new_table_children):
|
2012-03-21 21:09:03 +04:00
|
|
|
|
table = None
|
|
|
|
|
resume_at = None
|
2012-02-28 17:50:35 +04:00
|
|
|
|
next_page = 'any'
|
|
|
|
|
adjoining_margins = []
|
|
|
|
|
collapsing_through = False
|
|
|
|
|
return table, resume_at, next_page, adjoining_margins, collapsing_through
|
2011-11-25 20:20:41 +04:00
|
|
|
|
|
2011-11-24 20:56:05 +04:00
|
|
|
|
|
|
|
|
|
def add_top_padding(box, extra_padding):
|
|
|
|
|
"""Increase the top padding of a box. This also translates the children.
|
|
|
|
|
"""
|
|
|
|
|
box.padding_top += extra_padding
|
|
|
|
|
for child in box.children:
|
|
|
|
|
child.translate(dy=extra_padding)
|
|
|
|
|
|
|
|
|
|
|
2012-05-09 21:01:32 +04:00
|
|
|
|
def fixed_table_layout(box, absolute_boxes):
|
2011-11-24 20:56:05 +04:00
|
|
|
|
"""Run the fixed table layout and return a list of column widths
|
|
|
|
|
|
|
|
|
|
http://www.w3.org/TR/CSS21/tables.html#fixed-table-layout
|
|
|
|
|
|
|
|
|
|
"""
|
2012-04-10 16:37:54 +04:00
|
|
|
|
table = box.get_wrapped_table()
|
2011-11-22 16:06:50 +04:00
|
|
|
|
assert table.width != 'auto'
|
|
|
|
|
|
|
|
|
|
all_columns = [column for column_group in table.column_groups
|
|
|
|
|
for column in column_group.children]
|
2011-11-24 20:56:05 +04:00
|
|
|
|
if table.children and table.children[0].children:
|
|
|
|
|
first_rowgroup = table.children[0]
|
|
|
|
|
first_row_cells = first_rowgroup.children[0].children
|
|
|
|
|
else:
|
|
|
|
|
first_row_cells = []
|
2011-11-22 16:06:50 +04:00
|
|
|
|
num_columns = max(
|
|
|
|
|
len(all_columns),
|
2011-11-24 20:56:05 +04:00
|
|
|
|
sum(cell.colspan for cell in first_row_cells)
|
2011-11-22 16:06:50 +04:00
|
|
|
|
)
|
|
|
|
|
# ``None`` means not know yet.
|
|
|
|
|
column_widths = [None] * num_columns
|
|
|
|
|
|
2011-11-24 20:56:05 +04:00
|
|
|
|
# `width` on column boxes
|
2011-11-22 16:06:50 +04:00
|
|
|
|
for i, column in enumerate(all_columns):
|
2012-04-05 13:21:26 +04:00
|
|
|
|
resolve_one_percentage(column, 'width', table.width)
|
2011-11-22 16:06:50 +04:00
|
|
|
|
if column.width != 'auto':
|
|
|
|
|
column_widths[i] = column.width
|
|
|
|
|
|
2011-11-24 20:56:05 +04:00
|
|
|
|
# `width` on cells of the first row.
|
2011-11-22 16:06:50 +04:00
|
|
|
|
border_spacing_x, border_spacing_y = table.style.border_spacing
|
|
|
|
|
i = 0
|
2011-11-24 20:56:05 +04:00
|
|
|
|
for cell in first_row_cells:
|
2011-11-22 16:06:50 +04:00
|
|
|
|
resolve_percentages(cell, table)
|
|
|
|
|
if cell.width != 'auto':
|
|
|
|
|
width = cell.border_width()
|
|
|
|
|
width -= border_spacing_x * (cell.colspan - 1)
|
|
|
|
|
# In the general case, this width affects several columns (through
|
|
|
|
|
# colspan) some of which already have a width. Subscract these
|
|
|
|
|
# known widths and divide among remaining columns.
|
|
|
|
|
columns_without_width = [] # and occupied by this cell
|
|
|
|
|
for j in xrange(i, i + cell.colspan):
|
|
|
|
|
if column_widths[j] is None:
|
|
|
|
|
columns_without_width.append(j)
|
|
|
|
|
else:
|
|
|
|
|
width -= column_widths[j]
|
2012-04-16 15:48:46 +04:00
|
|
|
|
if columns_without_width:
|
|
|
|
|
width_per_column = width / len(columns_without_width)
|
|
|
|
|
for j in columns_without_width:
|
|
|
|
|
column_widths[j] = width_per_column
|
2011-11-22 16:06:50 +04:00
|
|
|
|
i += cell.colspan
|
|
|
|
|
|
2011-11-24 20:56:05 +04:00
|
|
|
|
# Distribute the remaining space equally on columns that do not have
|
|
|
|
|
# a width yet.
|
2011-11-22 16:06:50 +04:00
|
|
|
|
all_border_spacing = border_spacing_x * (num_columns + 1)
|
|
|
|
|
min_table_width = (sum(w for w in column_widths if w is not None)
|
|
|
|
|
+ all_border_spacing)
|
|
|
|
|
columns_without_width = [i for i, width in enumerate(column_widths)
|
|
|
|
|
if width is None]
|
|
|
|
|
if columns_without_width and table.width >= min_table_width:
|
|
|
|
|
remaining_width = table.width - min_table_width
|
|
|
|
|
width_per_column = remaining_width / len(columns_without_width)
|
|
|
|
|
for i in columns_without_width:
|
|
|
|
|
column_widths[i] = width_per_column
|
|
|
|
|
else:
|
2011-11-24 20:56:05 +04:00
|
|
|
|
# XXX this is bad, but we were given a broken table to work with...
|
2011-11-22 16:06:50 +04:00
|
|
|
|
for i in columns_without_width:
|
|
|
|
|
column_widths[i] = 0
|
|
|
|
|
|
2011-11-24 20:56:05 +04:00
|
|
|
|
# If the sum is less than the table width,
|
|
|
|
|
# distribute the remaining space equally
|
2011-11-22 16:06:50 +04:00
|
|
|
|
extra_width = table.width - sum(column_widths) - all_border_spacing
|
|
|
|
|
if extra_width <= 0:
|
|
|
|
|
# substract a negative: widen the table
|
|
|
|
|
table.width -= extra_width
|
2011-11-29 14:22:52 +04:00
|
|
|
|
elif num_columns:
|
2011-11-22 16:06:50 +04:00
|
|
|
|
extra_per_column = extra_width / num_columns
|
2012-04-10 16:37:54 +04:00
|
|
|
|
column_widths = [width + extra_per_column for width in column_widths]
|
2011-11-22 16:06:50 +04:00
|
|
|
|
|
|
|
|
|
# Now we have table.width == sum(column_widths) + all_border_spacing
|
|
|
|
|
# with possible floating point rounding errors.
|
2011-11-29 14:22:52 +04:00
|
|
|
|
# (unless there is zero column)
|
2011-11-30 17:56:57 +04:00
|
|
|
|
table.column_widths = column_widths
|
2012-03-23 22:31:54 +04:00
|
|
|
|
|
|
|
|
|
|
2012-05-09 21:01:32 +04:00
|
|
|
|
def auto_table_layout(box, containing_block, absolute_boxes):
|
2012-03-23 22:31:54 +04:00
|
|
|
|
"""Run the auto table layout and return a list of column widths.
|
|
|
|
|
|
|
|
|
|
http://www.w3.org/TR/CSS21/tables.html#auto-table-layout
|
|
|
|
|
|
|
|
|
|
"""
|
2012-04-10 16:37:54 +04:00
|
|
|
|
table = box.get_wrapped_table()
|
|
|
|
|
(table_preferred_minimum_width, table_preferred_width,
|
|
|
|
|
column_preferred_minimum_widths, column_preferred_widths) = \
|
2012-04-10 20:29:00 +04:00
|
|
|
|
table_and_columns_preferred_widths(box, resolved_table_width=True)
|
2012-04-07 16:25:30 +04:00
|
|
|
|
|
2012-05-07 20:15:55 +04:00
|
|
|
|
all_border_spacing = (
|
|
|
|
|
table.style.border_spacing[0] * (len(column_preferred_widths) + 1))
|
|
|
|
|
|
|
|
|
|
# TODO: handle percentages
|
|
|
|
|
margins = 0
|
|
|
|
|
if box.style.margin_left.unit != '%':
|
|
|
|
|
margins += box.style.margin_left.value
|
|
|
|
|
if box.style.margin_right.unit != '%':
|
|
|
|
|
margins += box.style.margin_right.value
|
|
|
|
|
|
|
|
|
|
available_width = containing_block.width - margins
|
|
|
|
|
if table.width == 'auto':
|
|
|
|
|
if available_width < table_preferred_minimum_width:
|
|
|
|
|
table.width = table_preferred_minimum_width
|
|
|
|
|
table.column_widths = column_preferred_minimum_widths
|
|
|
|
|
elif available_width < table_preferred_width:
|
|
|
|
|
table.width = available_width
|
|
|
|
|
table.column_widths = column_preferred_minimum_widths
|
|
|
|
|
else:
|
|
|
|
|
table.width = table_preferred_width
|
|
|
|
|
table.column_widths = column_preferred_widths
|
2012-03-23 22:31:54 +04:00
|
|
|
|
else:
|
2012-05-07 20:15:55 +04:00
|
|
|
|
if table.width < table_preferred_minimum_width:
|
|
|
|
|
table.width = table_preferred_minimum_width
|
|
|
|
|
table.column_widths = column_preferred_minimum_widths
|
|
|
|
|
elif table.width < table_preferred_width:
|
|
|
|
|
table.column_widths = column_preferred_minimum_widths
|
|
|
|
|
else:
|
|
|
|
|
table.column_widths = column_preferred_widths
|
|
|
|
|
|
|
|
|
|
lost_width = table.width - sum(table.column_widths) - all_border_spacing
|
|
|
|
|
if lost_width > 0:
|
2012-05-11 18:17:23 +04:00
|
|
|
|
sum_column_preferred_widths = sum(column_preferred_widths)
|
|
|
|
|
if sum_column_preferred_widths:
|
|
|
|
|
table.column_widths = [
|
|
|
|
|
(column_width + lost_width * preferred_column_width /
|
|
|
|
|
sum_column_preferred_widths)
|
|
|
|
|
for (preferred_column_width, column_width)
|
|
|
|
|
in zip(column_preferred_widths, table.column_widths)]
|
|
|
|
|
else:
|
|
|
|
|
table.column_widths = [
|
|
|
|
|
column_width + lost_width / len(table.column_widths)
|
|
|
|
|
for column_width in table.column_widths]
|
2011-11-22 16:06:50 +04:00
|
|
|
|
|
|
|
|
|
|
2012-05-09 21:01:32 +04:00
|
|
|
|
def table_wrapper_width(wrapper, containing_block, absolute_boxes):
|
2012-04-12 14:51:21 +04:00
|
|
|
|
"""Find the width of each column and derive the wrapper width."""
|
|
|
|
|
table = wrapper.get_wrapped_table()
|
|
|
|
|
resolve_percentages(table, containing_block)
|
|
|
|
|
|
|
|
|
|
if table.style.table_layout == 'fixed' and table.width != 'auto':
|
2012-05-09 21:01:32 +04:00
|
|
|
|
fixed_table_layout(wrapper, absolute_boxes)
|
2012-04-12 14:51:21 +04:00
|
|
|
|
else:
|
2012-05-09 21:01:32 +04:00
|
|
|
|
auto_table_layout(wrapper, containing_block, absolute_boxes)
|
2012-04-12 14:51:21 +04:00
|
|
|
|
|
|
|
|
|
wrapper.width = table.border_width()
|
|
|
|
|
wrapper.style.width = Dimension(wrapper.width, 'px')
|
|
|
|
|
|
|
|
|
|
|
2011-11-24 20:56:05 +04:00
|
|
|
|
def cell_baseline(cell):
|
|
|
|
|
"""
|
|
|
|
|
Return the y position of a cell’s baseline from the top of its border box.
|
2011-11-22 16:06:50 +04:00
|
|
|
|
|
2011-11-24 20:56:05 +04:00
|
|
|
|
See http://www.w3.org/TR/CSS21/tables.html#height-layout
|
2011-11-22 16:06:50 +04:00
|
|
|
|
|
2011-11-24 20:56:05 +04:00
|
|
|
|
"""
|
2012-04-10 18:07:32 +04:00
|
|
|
|
result = find_in_flow_baseline(cell,
|
|
|
|
|
baseline_types=(boxes.LineBox, boxes.TableRowBox))
|
2012-04-10 14:49:54 +04:00
|
|
|
|
if result is not None:
|
2012-04-10 17:46:36 +04:00
|
|
|
|
return result - cell.position_y
|
2012-04-10 14:49:54 +04:00
|
|
|
|
else:
|
|
|
|
|
# Default to the bottom of the content area.
|
|
|
|
|
return cell.border_top_width + cell.padding_top + cell.height
|
|
|
|
|
|
|
|
|
|
|
2012-04-10 18:08:53 +04:00
|
|
|
|
def find_in_flow_baseline(box, last=False, baseline_types=(boxes.LineBox,)):
|
2012-04-10 14:49:54 +04:00
|
|
|
|
"""
|
|
|
|
|
Return the absolute Y position for the first (or last) in-flow baseline
|
|
|
|
|
if any, or None.
|
|
|
|
|
"""
|
2012-04-10 18:07:32 +04:00
|
|
|
|
if isinstance(box, baseline_types):
|
2012-04-10 14:49:54 +04:00
|
|
|
|
return box.position_y + box.baseline
|
|
|
|
|
if isinstance(box, boxes.ParentBox):
|
|
|
|
|
children = reversed(box.children) if last else box.children
|
|
|
|
|
for child in children:
|
2012-04-10 18:07:32 +04:00
|
|
|
|
result = find_in_flow_baseline(child, last, baseline_types)
|
2012-04-10 14:49:54 +04:00
|
|
|
|
if result is not None:
|
|
|
|
|
return result
|