ajout fichiers manquant
This commit is contained in:
950
venv/Lib/site-packages/weasyprint/layout/block.py
Normal file
950
venv/Lib/site-packages/weasyprint/layout/block.py
Normal file
@@ -0,0 +1,950 @@
|
||||
"""
|
||||
weasyprint.layout.block
|
||||
-----------------------
|
||||
|
||||
Page breaking and layout for block-level and block-container boxes.
|
||||
|
||||
"""
|
||||
|
||||
from ..formatting_structure import boxes
|
||||
from .absolute import AbsolutePlaceholder, absolute_layout
|
||||
from .column import columns_layout
|
||||
from .flex import flex_layout
|
||||
from .float import avoid_collisions, float_layout, get_clearance
|
||||
from .inline import iter_line_boxes
|
||||
from .min_max import handle_min_max_width
|
||||
from .percent import resolve_percentages, resolve_position_percentages
|
||||
from .replaced import block_replaced_box_layout
|
||||
from .table import table_layout, table_wrapper_width
|
||||
|
||||
|
||||
def block_level_layout(context, box, bottom_space, skip_stack,
|
||||
containing_block, page_is_empty, absolute_boxes,
|
||||
fixed_boxes, adjoining_margins, discard):
|
||||
"""Lay out the block-level ``box``."""
|
||||
if not isinstance(box, boxes.TableBox):
|
||||
resolve_percentages(box, containing_block)
|
||||
|
||||
if box.margin_top == 'auto':
|
||||
box.margin_top = 0
|
||||
if box.margin_bottom == 'auto':
|
||||
box.margin_bottom = 0
|
||||
|
||||
if context.current_page > 1 and page_is_empty:
|
||||
# TODO: this condition is wrong, it only works for blocks whose
|
||||
# parent breaks collapsing margins. It should work for blocks whose
|
||||
# one of the ancestors breaks collapsing margins.
|
||||
# See test_margin_break_clearance.
|
||||
collapse_with_page = (
|
||||
containing_block.is_for_root_element or
|
||||
adjoining_margins)
|
||||
if collapse_with_page:
|
||||
if box.style['margin_break'] == 'discard':
|
||||
box.margin_top = 0
|
||||
elif box.style['margin_break'] == 'auto':
|
||||
if not context.forced_break:
|
||||
box.margin_top = 0
|
||||
|
||||
collapsed_margin = collapse_margin(
|
||||
adjoining_margins + [box.margin_top])
|
||||
box.clearance = get_clearance(context, box, collapsed_margin)
|
||||
if box.clearance is not None:
|
||||
top_border_edge = box.position_y + collapsed_margin + box.clearance
|
||||
box.position_y = top_border_edge - box.margin_top
|
||||
adjoining_margins = []
|
||||
|
||||
return block_level_layout_switch(
|
||||
context, box, bottom_space, skip_stack, containing_block,
|
||||
page_is_empty, absolute_boxes, fixed_boxes, adjoining_margins, discard)
|
||||
|
||||
|
||||
def block_level_layout_switch(context, box, bottom_space, skip_stack,
|
||||
containing_block, page_is_empty, absolute_boxes,
|
||||
fixed_boxes, adjoining_margins, discard):
|
||||
"""Call the layout function corresponding to the ``box`` type."""
|
||||
if isinstance(box, boxes.TableBox):
|
||||
return table_layout(
|
||||
context, box, bottom_space, skip_stack, containing_block,
|
||||
page_is_empty, absolute_boxes, fixed_boxes)
|
||||
elif isinstance(box, boxes.BlockBox):
|
||||
return block_box_layout(
|
||||
context, box, bottom_space, skip_stack, containing_block,
|
||||
page_is_empty, absolute_boxes, fixed_boxes, adjoining_margins,
|
||||
discard)
|
||||
elif isinstance(box, boxes.BlockReplacedBox):
|
||||
return block_replaced_box_layout(context, box, containing_block)
|
||||
elif isinstance(box, boxes.FlexBox):
|
||||
return flex_layout(
|
||||
context, box, bottom_space, skip_stack, containing_block,
|
||||
page_is_empty, absolute_boxes, fixed_boxes)
|
||||
else: # pragma: no cover
|
||||
raise TypeError(f'Layout for {type(box).__name__} not handled yet')
|
||||
|
||||
|
||||
def block_box_layout(context, box, bottom_space, skip_stack,
|
||||
containing_block, page_is_empty, absolute_boxes,
|
||||
fixed_boxes, adjoining_margins, discard):
|
||||
"""Lay out the block ``box``."""
|
||||
if (box.style['column_width'] != 'auto' or
|
||||
box.style['column_count'] != 'auto'):
|
||||
result = columns_layout(
|
||||
context, box, bottom_space, skip_stack, containing_block,
|
||||
page_is_empty, absolute_boxes, fixed_boxes, adjoining_margins)
|
||||
resume_at = result[1]
|
||||
# TODO: this condition and the whole relayout are probably wrong
|
||||
if resume_at is None:
|
||||
new_box = result[0]
|
||||
columns_bottom_space = (
|
||||
new_box.margin_bottom + new_box.padding_bottom +
|
||||
new_box.border_bottom_width)
|
||||
if columns_bottom_space:
|
||||
bottom_space += columns_bottom_space
|
||||
result = columns_layout(
|
||||
context, box, bottom_space, skip_stack,
|
||||
containing_block, page_is_empty, absolute_boxes,
|
||||
fixed_boxes, adjoining_margins)
|
||||
return result
|
||||
elif box.is_table_wrapper:
|
||||
table_wrapper_width(
|
||||
context, box, (containing_block.width, containing_block.height))
|
||||
block_level_width(box, containing_block)
|
||||
|
||||
result = block_container_layout(
|
||||
context, box, bottom_space, skip_stack, page_is_empty,
|
||||
absolute_boxes, fixed_boxes, adjoining_margins, discard)
|
||||
new_box = result[0]
|
||||
if new_box and new_box.is_table_wrapper:
|
||||
# Don't collide with floats
|
||||
# http://www.w3.org/TR/CSS21/visuren.html#floats
|
||||
position_x, position_y, _ = avoid_collisions(
|
||||
context, new_box, containing_block, outer=False)
|
||||
new_box.translate(
|
||||
position_x - new_box.position_x, position_y - new_box.position_y)
|
||||
return result
|
||||
|
||||
|
||||
@handle_min_max_width
|
||||
def block_level_width(box, containing_block):
|
||||
"""Set the ``box`` width."""
|
||||
# 'cb' stands for 'containing block'
|
||||
if isinstance(containing_block, boxes.Box):
|
||||
cb_width = containing_block.width
|
||||
direction = containing_block.style['direction']
|
||||
else:
|
||||
cb_width = containing_block[0]
|
||||
# TODO: what is the real text direction?
|
||||
direction = 'ltr'
|
||||
|
||||
# http://www.w3.org/TR/CSS21/visudet.html#blockwidth
|
||||
|
||||
# These names are waaay too long
|
||||
margin_l = box.margin_left
|
||||
margin_r = box.margin_right
|
||||
padding_l = box.padding_left
|
||||
padding_r = box.padding_right
|
||||
border_l = box.border_left_width
|
||||
border_r = box.border_right_width
|
||||
width = box.width
|
||||
|
||||
# Only margin-left, margin-right and width can be 'auto'.
|
||||
# We want: width of containing block ==
|
||||
# margin-left + border-left-width + padding-left + width
|
||||
# + padding-right + border-right-width + margin-right
|
||||
|
||||
paddings_plus_borders = padding_l + padding_r + border_l + border_r
|
||||
if box.width != 'auto':
|
||||
total = paddings_plus_borders + width
|
||||
if margin_l != 'auto':
|
||||
total += margin_l
|
||||
if margin_r != 'auto':
|
||||
total += margin_r
|
||||
if total > cb_width:
|
||||
if margin_l == 'auto':
|
||||
margin_l = box.margin_left = 0
|
||||
if margin_r == 'auto':
|
||||
margin_r = box.margin_right = 0
|
||||
if width != 'auto' and margin_l != 'auto' and margin_r != 'auto':
|
||||
# The equation is over-constrained.
|
||||
if direction == 'rtl' and not box.is_column:
|
||||
box.position_x += (
|
||||
cb_width - paddings_plus_borders - width - margin_r - margin_l)
|
||||
# Do nothing in ltr.
|
||||
if width == 'auto':
|
||||
if margin_l == 'auto':
|
||||
margin_l = box.margin_left = 0
|
||||
if margin_r == 'auto':
|
||||
margin_r = box.margin_right = 0
|
||||
width = box.width = cb_width - (
|
||||
paddings_plus_borders + margin_l + margin_r)
|
||||
margin_sum = cb_width - paddings_plus_borders - width
|
||||
if margin_l == 'auto' and margin_r == 'auto':
|
||||
box.margin_left = margin_sum / 2.
|
||||
box.margin_right = margin_sum / 2.
|
||||
elif margin_l == 'auto' and margin_r != 'auto':
|
||||
box.margin_left = margin_sum - margin_r
|
||||
elif margin_l != 'auto' and margin_r == 'auto':
|
||||
box.margin_right = margin_sum - margin_l
|
||||
|
||||
|
||||
def relative_positioning(box, containing_block):
|
||||
"""Translate the ``box`` if it is relatively positioned."""
|
||||
if box.style['position'] == 'relative':
|
||||
resolve_position_percentages(box, containing_block)
|
||||
|
||||
if box.left != 'auto' and box.right != 'auto':
|
||||
if box.style['direction'] == 'ltr':
|
||||
translate_x = box.left
|
||||
else:
|
||||
translate_x = -box.right
|
||||
elif box.left != 'auto':
|
||||
translate_x = box.left
|
||||
elif box.right != 'auto':
|
||||
translate_x = -box.right
|
||||
else:
|
||||
translate_x = 0
|
||||
|
||||
if box.top != 'auto':
|
||||
translate_y = box.top
|
||||
elif box.style['bottom'] != 'auto':
|
||||
translate_y = -box.bottom
|
||||
else:
|
||||
translate_y = 0
|
||||
|
||||
box.translate(translate_x, translate_y)
|
||||
|
||||
if isinstance(box, (boxes.InlineBox, boxes.LineBox)):
|
||||
for child in box.children:
|
||||
relative_positioning(child, containing_block)
|
||||
|
||||
|
||||
def _out_of_flow_layout(context, box, index, child, new_children,
|
||||
page_is_empty, absolute_boxes, fixed_boxes,
|
||||
adjoining_margins, bottom_space):
|
||||
stop = False
|
||||
resume_at = None
|
||||
out_of_flow_resume_at = None
|
||||
|
||||
child.position_y += collapse_margin(adjoining_margins)
|
||||
if child.is_absolutely_positioned():
|
||||
placeholder = AbsolutePlaceholder(child)
|
||||
placeholder.index = index
|
||||
new_children.append(placeholder)
|
||||
if child.style['position'] == 'absolute':
|
||||
absolute_boxes.append(placeholder)
|
||||
else:
|
||||
fixed_boxes.append(placeholder)
|
||||
elif child.is_floated():
|
||||
new_child, out_of_flow_resume_at = float_layout(
|
||||
context, child, box, absolute_boxes, fixed_boxes, bottom_space,
|
||||
skip_stack=None)
|
||||
# New page if overflow
|
||||
if (page_is_empty and not new_children) or not (
|
||||
new_child.position_y + new_child.height >
|
||||
context.page_bottom - bottom_space):
|
||||
new_child.index = index
|
||||
new_children.append(new_child)
|
||||
else:
|
||||
last_in_flow_child = find_last_in_flow_child(new_children)
|
||||
page_break = block_level_page_break(last_in_flow_child, child)
|
||||
resume_at = {index: None}
|
||||
if new_children and page_break in ('avoid', 'avoid-page'):
|
||||
result = find_earlier_page_break(
|
||||
new_children, absolute_boxes, fixed_boxes)
|
||||
if result:
|
||||
new_children, resume_at = result
|
||||
stop = True
|
||||
elif child.is_running():
|
||||
running_name = child.style['position'][1]
|
||||
page = context.current_page
|
||||
context.running_elements[running_name][page].append(child)
|
||||
|
||||
return stop, resume_at, out_of_flow_resume_at
|
||||
|
||||
|
||||
def _break_line(box, new_children, lines_iterator, page_is_empty, index,
|
||||
skip_stack, resume_at):
|
||||
over_orphans = len(new_children) - box.style['orphans']
|
||||
if over_orphans < 0 and not page_is_empty:
|
||||
# Reached the bottom of the page before we had
|
||||
# enough lines for orphans, cancel the whole box.
|
||||
return True, False, resume_at
|
||||
# How many lines we need on the next page to satisfy widows
|
||||
# -1 for the current line.
|
||||
needed = box.style['widows'] - 1
|
||||
if needed:
|
||||
for _ in lines_iterator:
|
||||
needed -= 1
|
||||
if needed == 0:
|
||||
break
|
||||
if needed > over_orphans and not page_is_empty:
|
||||
# Total number of lines < orphans + widows
|
||||
return True, False, resume_at
|
||||
if needed and needed <= over_orphans:
|
||||
# Remove lines to keep them for the next page
|
||||
del new_children[-needed:]
|
||||
# Page break here, resume before this line
|
||||
return False, True, {index: skip_stack}
|
||||
|
||||
|
||||
def _linebox_layout(context, box, index, child, new_children, page_is_empty,
|
||||
absolute_boxes, fixed_boxes, adjoining_margins,
|
||||
bottom_space, position_y, skip_stack, first_letter_style,
|
||||
draw_bottom_decoration):
|
||||
abort = stop = False
|
||||
resume_at = None
|
||||
|
||||
assert len(box.children) == 1, 'line box with siblings before layout'
|
||||
|
||||
if adjoining_margins:
|
||||
position_y += collapse_margin(adjoining_margins)
|
||||
new_containing_block = box
|
||||
lines_iterator = iter_line_boxes(
|
||||
context, child, position_y, bottom_space, skip_stack,
|
||||
new_containing_block, absolute_boxes, fixed_boxes, first_letter_style)
|
||||
for i, (line, resume_at) in enumerate(lines_iterator):
|
||||
line.resume_at = resume_at
|
||||
new_position_y = line.position_y + line.height
|
||||
|
||||
# Add bottom padding and border to the bottom position of the
|
||||
# box if needed
|
||||
draw_bottom_decoration |= resume_at is None
|
||||
if draw_bottom_decoration:
|
||||
offset_y = box.border_bottom_width + box.padding_bottom
|
||||
else:
|
||||
offset_y = 0
|
||||
|
||||
# Allow overflow if the first line of the page is higher
|
||||
# than the page itself so that we put *something* on this
|
||||
# page and can advance in the context.
|
||||
overflow = (
|
||||
(new_children or not page_is_empty) and
|
||||
(new_position_y + offset_y > context.page_bottom - bottom_space))
|
||||
if overflow:
|
||||
abort, stop, resume_at = _break_line(
|
||||
box, new_children, lines_iterator, page_is_empty, index,
|
||||
skip_stack, resume_at)
|
||||
break
|
||||
|
||||
# TODO: this is incomplete.
|
||||
# See http://dev.w3.org/csswg/css3-page/#allowed-pg-brk
|
||||
# "When an unforced page break occurs here, both the adjoining
|
||||
# ‘margin-top’ and ‘margin-bottom’ are set to zero."
|
||||
# See https://github.com/Kozea/WeasyPrint/issues/115
|
||||
elif page_is_empty and (
|
||||
new_position_y > context.page_bottom - bottom_space):
|
||||
# Remove the top border when a page is empty and the box is
|
||||
# too high to be drawn in one page
|
||||
new_position_y -= box.margin_top
|
||||
line.translate(0, -box.margin_top)
|
||||
box.margin_top = 0
|
||||
|
||||
if context.footnotes:
|
||||
break_linebox = False
|
||||
footnotes = (
|
||||
descendant.footnote for descendant in line.descendants()
|
||||
if descendant.footnote in context.footnotes)
|
||||
for footnote in footnotes:
|
||||
context.layout_footnote(footnote)
|
||||
overflow = context.reported_footnotes or (
|
||||
new_position_y + offset_y >
|
||||
context.page_bottom - bottom_space)
|
||||
if overflow:
|
||||
context.report_footnote(footnote)
|
||||
if footnote.style['footnote_policy'] == 'line':
|
||||
abort, stop, resume_at = _break_line(
|
||||
box, new_children, lines_iterator, page_is_empty,
|
||||
index, skip_stack, resume_at)
|
||||
break_linebox = True
|
||||
elif footnote.style['footnote_policy'] == 'block':
|
||||
abort = break_linebox = True
|
||||
break
|
||||
if break_linebox:
|
||||
break
|
||||
|
||||
new_children.append(line)
|
||||
position_y = new_position_y
|
||||
skip_stack = resume_at
|
||||
|
||||
# Break box if we reached max-lines
|
||||
if box.style['max_lines'] != 'none':
|
||||
if i >= box.style['max_lines'] - 1:
|
||||
line.block_ellipsis = box.style['block_ellipsis']
|
||||
break
|
||||
|
||||
if new_children:
|
||||
resume_at = {index: new_children[-1].resume_at}
|
||||
|
||||
return abort, stop, resume_at, position_y
|
||||
|
||||
|
||||
def _in_flow_layout(context, box, index, child, new_children, page_is_empty,
|
||||
absolute_boxes, fixed_boxes, adjoining_margins,
|
||||
bottom_space, position_y, skip_stack, first_letter_style,
|
||||
draw_bottom_decoration, collapsing_with_children, discard,
|
||||
next_page):
|
||||
abort = stop = False
|
||||
|
||||
last_in_flow_child = find_last_in_flow_child(new_children)
|
||||
if last_in_flow_child is not None:
|
||||
# Between in-flow siblings
|
||||
page_break = block_level_page_break(last_in_flow_child, child)
|
||||
page_name = block_level_page_name(last_in_flow_child, child)
|
||||
if page_name or page_break in (
|
||||
'page', 'left', 'right', 'recto', 'verso'):
|
||||
page_name = child.page_values()[0]
|
||||
next_page = {'break': page_break, 'page': page_name}
|
||||
resume_at = {index: None}
|
||||
stop = True
|
||||
return (
|
||||
abort, stop, resume_at, position_y, adjoining_margins,
|
||||
next_page, new_children)
|
||||
else:
|
||||
page_break = 'auto'
|
||||
|
||||
new_containing_block = box
|
||||
|
||||
if not new_containing_block.is_table_wrapper:
|
||||
resolve_percentages(child, new_containing_block)
|
||||
if (child.is_in_normal_flow() and last_in_flow_child is None and
|
||||
collapsing_with_children):
|
||||
# TODO: add the adjoining descendants' margin top to
|
||||
# [child.margin_top]
|
||||
old_collapsed_margin = collapse_margin(adjoining_margins)
|
||||
if child.margin_top == 'auto':
|
||||
child_margin_top = 0
|
||||
else:
|
||||
child_margin_top = child.margin_top
|
||||
new_collapsed_margin = collapse_margin(
|
||||
adjoining_margins + [child_margin_top])
|
||||
collapsed_margin_difference = (
|
||||
new_collapsed_margin - old_collapsed_margin)
|
||||
for previous_new_child in new_children:
|
||||
previous_new_child.translate(dy=collapsed_margin_difference)
|
||||
clearance = get_clearance(context, child, new_collapsed_margin)
|
||||
if clearance is not None:
|
||||
for previous_new_child in new_children:
|
||||
previous_new_child.translate(
|
||||
dy=-collapsed_margin_difference)
|
||||
|
||||
collapsed_margin = collapse_margin(adjoining_margins)
|
||||
box.position_y += collapsed_margin - box.margin_top
|
||||
# Count box.margin_top as we emptied adjoining_margins
|
||||
adjoining_margins = []
|
||||
position_y = box.content_box_y()
|
||||
|
||||
if adjoining_margins and box.is_table_wrapper:
|
||||
collapsed_margin = collapse_margin(adjoining_margins)
|
||||
child.position_y += collapsed_margin
|
||||
position_y += collapsed_margin
|
||||
adjoining_margins = []
|
||||
|
||||
page_is_empty_with_no_children = page_is_empty and not any(
|
||||
child for child in new_children
|
||||
if not isinstance(child, AbsolutePlaceholder))
|
||||
|
||||
if not getattr(child, 'first_letter_style', None):
|
||||
child.first_letter_style = first_letter_style
|
||||
(new_child, resume_at, next_page, next_adjoining_margins,
|
||||
collapsing_through) = block_level_layout(
|
||||
context, child, bottom_space, skip_stack,
|
||||
new_containing_block, page_is_empty_with_no_children, absolute_boxes,
|
||||
fixed_boxes, adjoining_margins, discard)
|
||||
|
||||
if new_child is not None:
|
||||
# index in its non-laid-out parent, not in future new parent
|
||||
# May be used in find_earlier_page_break()
|
||||
new_child.index = index
|
||||
|
||||
# We need to do this after the child layout to have the
|
||||
# used value for margin_top (eg. it might be a percentage.)
|
||||
if not isinstance(new_child, (boxes.BlockBox, boxes.TableBox)):
|
||||
adjoining_margins.append(new_child.margin_top)
|
||||
offset_y = (
|
||||
collapse_margin(adjoining_margins) - new_child.margin_top)
|
||||
new_child.translate(0, offset_y)
|
||||
# else: blocks handle that themselves.
|
||||
|
||||
if not collapsing_through:
|
||||
new_content_position_y = (
|
||||
new_child.content_box_y() + new_child.height)
|
||||
new_position_y = (
|
||||
new_child.border_box_y() + new_child.border_height())
|
||||
|
||||
if (new_content_position_y > context.page_bottom - bottom_space and
|
||||
not page_is_empty_with_no_children):
|
||||
# The child content overflows the page area, display it on the
|
||||
# next page.
|
||||
remove_placeholders([new_child], absolute_boxes, fixed_boxes)
|
||||
new_child = None
|
||||
elif (new_position_y > context.page_bottom - bottom_space and
|
||||
not page_is_empty_with_no_children):
|
||||
# The child border/padding overflows the page area, do the
|
||||
# layout again with a higher bottom_space value.
|
||||
remove_placeholders([new_child], absolute_boxes, fixed_boxes)
|
||||
bottom_space += (
|
||||
new_child.padding_bottom + new_child.border_bottom_width)
|
||||
(new_child, resume_at, next_page, next_adjoining_margins,
|
||||
collapsing_through) = block_level_layout(
|
||||
context, child, bottom_space, skip_stack,
|
||||
new_containing_block, page_is_empty_with_no_children,
|
||||
absolute_boxes, fixed_boxes, adjoining_margins, discard)
|
||||
if new_child:
|
||||
position_y = (
|
||||
new_child.border_box_y() + new_child.border_height())
|
||||
else:
|
||||
position_y = new_position_y
|
||||
|
||||
adjoining_margins = next_adjoining_margins
|
||||
if new_child:
|
||||
adjoining_margins.append(new_child.margin_bottom)
|
||||
|
||||
if new_child and new_child.clearance:
|
||||
position_y = new_child.border_box_y() + new_child.border_height()
|
||||
|
||||
skip_stack = None
|
||||
|
||||
if new_child is None:
|
||||
# Nothing fits in the remaining space of this page: break
|
||||
if page_break in ('avoid', 'avoid-page'):
|
||||
# TODO: fill the blank space at the bottom of the page
|
||||
result = find_earlier_page_break(
|
||||
new_children, absolute_boxes, fixed_boxes)
|
||||
if result:
|
||||
new_children, resume_at = result
|
||||
stop = True
|
||||
return (
|
||||
abort, stop, resume_at, position_y, adjoining_margins,
|
||||
next_page, new_children)
|
||||
else:
|
||||
# We did not find any page break opportunity
|
||||
if not page_is_empty:
|
||||
# The page has content *before* this block:
|
||||
# cancel the block and try to find a break
|
||||
# in the parent.
|
||||
abort = True
|
||||
return (
|
||||
abort, stop, resume_at, position_y, adjoining_margins,
|
||||
next_page, new_children)
|
||||
# else:
|
||||
# ignore this 'avoid' and break anyway.
|
||||
|
||||
if all(child.is_absolutely_positioned() for child in new_children):
|
||||
# This box has only rendered absolute children, keep them
|
||||
# for the next page. This is for example useful for list
|
||||
# markers.
|
||||
remove_placeholders(new_children, absolute_boxes, fixed_boxes)
|
||||
new_children = []
|
||||
|
||||
if new_children:
|
||||
resume_at = {index: None}
|
||||
stop = True
|
||||
else:
|
||||
# This was the first child of this box, cancel the box completly
|
||||
abort = True
|
||||
return (
|
||||
abort, stop, resume_at, position_y, adjoining_margins, next_page,
|
||||
new_children)
|
||||
|
||||
new_children.append(new_child)
|
||||
if resume_at is not None:
|
||||
resume_at = {index: resume_at}
|
||||
stop = True
|
||||
|
||||
return (
|
||||
abort, stop, resume_at, position_y, adjoining_margins, next_page,
|
||||
new_children)
|
||||
|
||||
|
||||
def block_container_layout(context, box, bottom_space, skip_stack,
|
||||
page_is_empty, absolute_boxes, fixed_boxes,
|
||||
adjoining_margins, discard):
|
||||
"""Set the ``box`` height."""
|
||||
# TODO: boxes.FlexBox is allowed here because flex_layout calls
|
||||
# block_container_layout, there's probably a better solution.
|
||||
assert isinstance(box, (boxes.BlockContainerBox, boxes.FlexBox))
|
||||
|
||||
if establishes_formatting_context(box):
|
||||
context.create_block_formatting_context()
|
||||
|
||||
is_start = skip_stack is None
|
||||
box.remove_decoration(start=not is_start, end=False)
|
||||
|
||||
discard |= box.style['continue'] == 'discard'
|
||||
draw_bottom_decoration = (
|
||||
discard or box.style['box_decoration_break'] == 'clone')
|
||||
|
||||
if adjoining_margins is None:
|
||||
adjoining_margins = []
|
||||
|
||||
if draw_bottom_decoration:
|
||||
bottom_space += (
|
||||
box.padding_bottom + box.border_bottom_width + box.margin_bottom)
|
||||
|
||||
adjoining_margins.append(box.margin_top)
|
||||
this_box_adjoining_margins = adjoining_margins
|
||||
|
||||
collapsing_with_children = not (
|
||||
box.border_top_width or box.padding_top or box.is_flex_item or
|
||||
establishes_formatting_context(box) or box.is_for_root_element)
|
||||
if collapsing_with_children:
|
||||
# Not counting margins in adjoining_margins, if any
|
||||
# (there are not padding or borders, see above)
|
||||
position_y = box.position_y
|
||||
else:
|
||||
box.position_y += collapse_margin(adjoining_margins) - box.margin_top
|
||||
adjoining_margins = []
|
||||
position_y = box.content_box_y()
|
||||
|
||||
position_x = box.content_box_x()
|
||||
|
||||
if box.style['position'] == 'relative':
|
||||
# New containing block, use a new absolute list
|
||||
absolute_boxes = []
|
||||
|
||||
new_children = []
|
||||
next_page = {'break': 'any', 'page': None}
|
||||
|
||||
last_in_flow_child = None
|
||||
|
||||
if is_start:
|
||||
skip = 0
|
||||
first_letter_style = getattr(box, 'first_letter_style', None)
|
||||
else:
|
||||
# TODO: handle multiple skip stacks
|
||||
(skip, skip_stack), = skip_stack.items()
|
||||
first_letter_style = None
|
||||
for index, child in enumerate(box.children[skip:], start=(skip or 0)):
|
||||
child.position_x = position_x
|
||||
child.position_y = position_y # doesn’t count adjoining_margins
|
||||
|
||||
if not child.is_in_normal_flow():
|
||||
abort = False
|
||||
stop, resume_at, out_of_flow_resume_at = _out_of_flow_layout(
|
||||
context, box, index, child, new_children, page_is_empty,
|
||||
absolute_boxes, fixed_boxes, adjoining_margins,
|
||||
bottom_space)
|
||||
if out_of_flow_resume_at:
|
||||
context.broken_out_of_flow.append(
|
||||
(child, box, out_of_flow_resume_at))
|
||||
|
||||
elif isinstance(child, boxes.LineBox):
|
||||
abort, stop, resume_at, position_y = _linebox_layout(
|
||||
context, box, index, child, new_children, page_is_empty,
|
||||
absolute_boxes, fixed_boxes, adjoining_margins, bottom_space,
|
||||
position_y, skip_stack, first_letter_style,
|
||||
draw_bottom_decoration)
|
||||
draw_bottom_decoration |= resume_at is None
|
||||
adjoining_margins = []
|
||||
|
||||
else:
|
||||
(abort, stop, resume_at, position_y, adjoining_margins,
|
||||
next_page, new_children) = _in_flow_layout(
|
||||
context, box, index, child, new_children, page_is_empty,
|
||||
absolute_boxes, fixed_boxes, adjoining_margins, bottom_space,
|
||||
position_y, skip_stack, first_letter_style,
|
||||
draw_bottom_decoration, collapsing_with_children, discard,
|
||||
next_page)
|
||||
skip_stack = None
|
||||
|
||||
if abort:
|
||||
page = child.page_values()[0]
|
||||
return None, None, {'break': 'any', 'page': page}, [], False
|
||||
elif stop:
|
||||
break
|
||||
|
||||
else:
|
||||
resume_at = None
|
||||
|
||||
box_is_fragmented = resume_at is not None
|
||||
if box.style['continue'] == 'discard':
|
||||
resume_at = None
|
||||
|
||||
if (box_is_fragmented and
|
||||
box.style['break_inside'] in ('avoid', 'avoid-page') and
|
||||
not page_is_empty):
|
||||
return (
|
||||
None, None, {'break': 'any', 'page': None}, [], False)
|
||||
|
||||
if collapsing_with_children:
|
||||
box.position_y += (
|
||||
collapse_margin(this_box_adjoining_margins) - box.margin_top)
|
||||
|
||||
last_in_flow_child = find_last_in_flow_child(new_children)
|
||||
collapsing_through = False
|
||||
if last_in_flow_child is None:
|
||||
collapsed_margin = collapse_margin(adjoining_margins)
|
||||
# Top and bottom margins of this box
|
||||
if (box.height in ('auto', 0) and
|
||||
get_clearance(context, box, collapsed_margin) is None and
|
||||
all(value == 0 for value in [
|
||||
box.min_height, box.border_top_width, box.padding_top,
|
||||
box.border_bottom_width, box.padding_bottom])):
|
||||
collapsing_through = True
|
||||
else:
|
||||
position_y += collapsed_margin
|
||||
adjoining_margins = []
|
||||
else:
|
||||
# Bottom margin of the last child and bottom margin of this box
|
||||
if box.height != 'auto':
|
||||
# Not adjoining (position_y is not used afterwards)
|
||||
adjoining_margins = []
|
||||
|
||||
if (box.border_bottom_width or
|
||||
box.padding_bottom or
|
||||
establishes_formatting_context(box) or
|
||||
box.is_for_root_element or
|
||||
box.is_table_wrapper):
|
||||
position_y += collapse_margin(adjoining_margins)
|
||||
adjoining_margins = []
|
||||
|
||||
# Add block ellipsis
|
||||
if box_is_fragmented and new_children:
|
||||
last_child = new_children[-1]
|
||||
if isinstance(last_child, boxes.LineBox):
|
||||
last_child.block_ellipsis = box.style['block_ellipsis']
|
||||
|
||||
new_box = box.copy_with_children(new_children)
|
||||
new_box.remove_decoration(
|
||||
start=not is_start, end=box_is_fragmented and not discard)
|
||||
|
||||
# TODO: See corner cases in
|
||||
# http://www.w3.org/TR/CSS21/visudet.html#normal-block
|
||||
# TODO: See float.float_layout
|
||||
if new_box.height == 'auto':
|
||||
if context.excluded_shapes and new_box.style['overflow'] != 'visible':
|
||||
max_float_position_y = max(
|
||||
float_box.position_y + float_box.margin_height()
|
||||
for float_box in context.excluded_shapes)
|
||||
position_y = max(max_float_position_y, position_y)
|
||||
new_box.height = position_y - new_box.content_box_y()
|
||||
|
||||
if new_box.style['position'] == 'relative':
|
||||
# New containing block, resolve the layout of the absolute descendants
|
||||
for absolute_box in absolute_boxes:
|
||||
absolute_layout(
|
||||
context, absolute_box, new_box, fixed_boxes, bottom_space,
|
||||
skip_stack=None)
|
||||
|
||||
for child in new_box.children:
|
||||
relative_positioning(child, (new_box.width, new_box.height))
|
||||
|
||||
if establishes_formatting_context(new_box):
|
||||
context.finish_block_formatting_context(new_box)
|
||||
|
||||
if discard or not box_is_fragmented:
|
||||
# After finish_block_formatting_context which may increment
|
||||
# new_box.height
|
||||
new_box.height = max(
|
||||
min(new_box.height, new_box.max_height), new_box.min_height)
|
||||
elif bottom_space > -float('inf'):
|
||||
# Make the box fill the blank space at the bottom of the page
|
||||
# https://www.w3.org/TR/css-break-3/#box-splitting
|
||||
new_box.height = (
|
||||
context.page_bottom - bottom_space - new_box.position_y -
|
||||
(new_box.margin_height() - new_box.height))
|
||||
if draw_bottom_decoration:
|
||||
new_box.height += (
|
||||
box.padding_bottom + box.border_bottom_width +
|
||||
box.margin_bottom)
|
||||
|
||||
if next_page['page'] is None:
|
||||
next_page['page'] = new_box.page_values()[1]
|
||||
|
||||
return new_box, resume_at, next_page, adjoining_margins, collapsing_through
|
||||
|
||||
|
||||
def collapse_margin(adjoining_margins):
|
||||
"""Get the amount of collapsed margin for a list of adjoining margins."""
|
||||
margins = [0] # add 0 to make sure that max/min don’t get an empty list
|
||||
margins.extend(adjoining_margins)
|
||||
positives = (m for m in margins if m >= 0)
|
||||
negatives = (m for m in margins if m <= 0)
|
||||
return max(positives) + min(negatives)
|
||||
|
||||
|
||||
def establishes_formatting_context(box):
|
||||
"""Return whether a box establishes a block formatting context.
|
||||
|
||||
See http://www.w3.org/TR/CSS2/visuren.html#block-formatting
|
||||
|
||||
"""
|
||||
return (
|
||||
box.is_floated()
|
||||
) or (
|
||||
box.is_absolutely_positioned()
|
||||
) or (
|
||||
# TODO: columns shouldn't be block boxes, this condition would then be
|
||||
# useless when this is fixed
|
||||
box.is_column
|
||||
) or (
|
||||
isinstance(box, boxes.BlockContainerBox) and
|
||||
not isinstance(box, boxes.BlockBox)
|
||||
) or (
|
||||
isinstance(box, boxes.BlockBox) and box.style['overflow'] != 'visible'
|
||||
) or (
|
||||
'flow-root' in box.style['display']
|
||||
)
|
||||
|
||||
|
||||
def block_level_page_break(sibling_before, sibling_after):
|
||||
"""Get the correct page break value between siblings.
|
||||
|
||||
Return the value of ``page-break-before`` or ``page-break-after`` that
|
||||
"wins" for boxes that meet at the margin between two sibling boxes.
|
||||
|
||||
For boxes before the margin, the 'page-break-after' value is considered;
|
||||
for boxes after the margin the 'page-break-before' value is considered.
|
||||
|
||||
* 'avoid' takes priority over 'auto'
|
||||
* 'page' takes priority over 'avoid' or 'auto'
|
||||
* 'left' or 'right' take priority over 'always', 'avoid' or 'auto'
|
||||
* Among 'left' and 'right', later values in the tree take priority.
|
||||
|
||||
See http://dev.w3.org/csswg/css3-page/#allowed-pg-brk
|
||||
|
||||
"""
|
||||
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, block_parallel_box_types):
|
||||
values.append(box.style['break_after'])
|
||||
if not (isinstance(box, boxes.ParentBox) and box.children):
|
||||
break
|
||||
box = box.children[-1]
|
||||
values.reverse() # Have them in tree order
|
||||
|
||||
box = sibling_after
|
||||
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
|
||||
|
||||
|
||||
def block_level_page_name(sibling_before, sibling_after):
|
||||
"""Return the next page name when siblings don't have the same names."""
|
||||
before_page = sibling_before.page_values()[1]
|
||||
after_page = sibling_after.page_values()[0]
|
||||
if before_page != after_page:
|
||||
return after_page
|
||||
|
||||
|
||||
def find_earlier_page_break(children, absolute_boxes, fixed_boxes):
|
||||
"""Find the last possible page break in ``children``.
|
||||
|
||||
Because of a `page-break-before: avoid` or a `page-break-after: avoid` we
|
||||
need to find an earlier page break opportunity inside `children`.
|
||||
|
||||
Absolute or fixed placeholders removed from children should also be
|
||||
removed from `absolute_boxes` or `fixed_boxes`.
|
||||
|
||||
Return (new_children, resume_at).
|
||||
|
||||
"""
|
||||
if children and isinstance(children[0], boxes.LineBox):
|
||||
# Normally `orphans` and `widows` apply to the block container, but
|
||||
# line boxes inherit them.
|
||||
orphans = children[0].style['orphans']
|
||||
widows = children[0].style['widows']
|
||||
index = len(children) - widows # how many lines we keep
|
||||
if index < orphans:
|
||||
return None
|
||||
new_children = children[:index]
|
||||
resume_at = {0: new_children[-1].resume_at}
|
||||
remove_placeholders(children[index:], absolute_boxes, fixed_boxes)
|
||||
return new_children, resume_at
|
||||
|
||||
previous_in_flow = None
|
||||
for index, child in reversed_enumerate(children):
|
||||
if isinstance(child, boxes.TableRowGroupBox) and (
|
||||
child.is_header or child.is_footer):
|
||||
# We don’t want to break pages before table headers or footers.
|
||||
continue
|
||||
elif child.is_column:
|
||||
# We don’t want to break pages between columns.
|
||||
continue
|
||||
|
||||
if child.is_in_normal_flow():
|
||||
if previous_in_flow is not None and (
|
||||
block_level_page_break(child, previous_in_flow) not in
|
||||
('avoid', 'avoid-page')):
|
||||
index += 1 # break after child
|
||||
new_children = children[:index]
|
||||
# Get the index in the original parent
|
||||
resume_at = {children[index].index: None}
|
||||
break
|
||||
previous_in_flow = child
|
||||
|
||||
if child.is_in_normal_flow() and (
|
||||
child.style['break_inside'] not in ('avoid', 'avoid-page')):
|
||||
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:
|
||||
new_grand_children, resume_at = result
|
||||
new_child = child.copy_with_children(new_grand_children)
|
||||
new_children = list(children[:index]) + [new_child]
|
||||
|
||||
# Re-add footer at the end of split table
|
||||
# TODO: fix table height and footer position
|
||||
if isinstance(child, boxes.TableRowGroupBox):
|
||||
for next_child in children[index:]:
|
||||
if next_child.is_footer:
|
||||
new_children.append(next_child)
|
||||
|
||||
# Index in the original parent
|
||||
resume_at = {new_child.index: resume_at}
|
||||
index += 1 # Remove placeholders after child
|
||||
break
|
||||
else:
|
||||
return None
|
||||
|
||||
# TODO: don’t remove absolute and fixed placeholders found in table footers
|
||||
remove_placeholders(children[index:], absolute_boxes, fixed_boxes)
|
||||
return new_children, resume_at
|
||||
|
||||
|
||||
def find_last_in_flow_child(children):
|
||||
"""Find and return the last in-flow child of given ``children``."""
|
||||
for child in reversed(children):
|
||||
if child.is_in_normal_flow():
|
||||
return child
|
||||
|
||||
|
||||
def reversed_enumerate(seq):
|
||||
"""Like reversed(list(enumerate(seq))) without copying the whole seq."""
|
||||
return zip(reversed(range(len(seq))), reversed(seq))
|
||||
|
||||
|
||||
def remove_placeholders(box_list, absolute_boxes, fixed_boxes):
|
||||
"""Remove placeholders from absolute and fixed lists.
|
||||
|
||||
For boxes that have been removed in find_earlier_page_break(), remove the
|
||||
matching placeholders in absolute_boxes and fixed_boxes.
|
||||
|
||||
"""
|
||||
for box in box_list:
|
||||
if isinstance(box, boxes.ParentBox):
|
||||
remove_placeholders(box.children, absolute_boxes, fixed_boxes)
|
||||
if box.style['position'] == 'absolute' and box in absolute_boxes:
|
||||
# box is not in absolute_boxes if its parent has position: relative
|
||||
absolute_boxes.remove(box)
|
||||
elif box.style['position'] == 'fixed':
|
||||
fixed_boxes.remove(box)
|
306
venv/Lib/site-packages/weasyprint/layout/column.py
Normal file
306
venv/Lib/site-packages/weasyprint/layout/column.py
Normal file
@@ -0,0 +1,306 @@
|
||||
"""
|
||||
weasyprint.layout.columns
|
||||
-------------------------
|
||||
|
||||
Layout for columns.
|
||||
|
||||
"""
|
||||
|
||||
from math import floor
|
||||
|
||||
from .absolute import absolute_layout
|
||||
from .percent import resolve_percentages
|
||||
|
||||
|
||||
def columns_layout(context, box, bottom_space, skip_stack, containing_block,
|
||||
page_is_empty, absolute_boxes, fixed_boxes,
|
||||
adjoining_margins):
|
||||
"""Lay out a multi-column ``box``."""
|
||||
from .block import (
|
||||
block_box_layout, block_level_layout, block_level_width,
|
||||
collapse_margin)
|
||||
|
||||
# Implementation of the multi-column pseudo-algorithm:
|
||||
# https://www.w3.org/TR/css3-multicol/#pseudo-algorithm
|
||||
width = None
|
||||
style = box.style
|
||||
original_bottom_space = bottom_space
|
||||
|
||||
if box.style['position'] == 'relative':
|
||||
# New containing block, use a new absolute list
|
||||
absolute_boxes = []
|
||||
|
||||
box = box.copy_with_children(box.children)
|
||||
box.position_y += collapse_margin(adjoining_margins) - box.margin_top
|
||||
|
||||
height = box.style['height']
|
||||
if height != 'auto' and height.unit != '%':
|
||||
assert height.unit == 'px'
|
||||
known_height = True
|
||||
bottom_space = max(
|
||||
bottom_space,
|
||||
context.page_bottom - box.content_box_y() - height.value)
|
||||
else:
|
||||
known_height = False
|
||||
|
||||
# TODO: the available width can be unknown if the containing block needs
|
||||
# the size of this block to know its own size.
|
||||
block_level_width(box, containing_block)
|
||||
available_width = box.width
|
||||
if style['column_width'] == 'auto' and style['column_count'] != 'auto':
|
||||
count = style['column_count']
|
||||
width = max(
|
||||
0, available_width - (count - 1) * style['column_gap']) / count
|
||||
elif (style['column_width'] != 'auto' and
|
||||
style['column_count'] == 'auto'):
|
||||
count = max(1, int(floor(
|
||||
(available_width + style['column_gap']) /
|
||||
(style['column_width'] + style['column_gap']))))
|
||||
width = (
|
||||
(available_width + style['column_gap']) / count -
|
||||
style['column_gap'])
|
||||
else:
|
||||
count = min(style['column_count'], int(floor(
|
||||
(available_width + style['column_gap']) /
|
||||
(style['column_width'] + style['column_gap']))))
|
||||
width = (
|
||||
(available_width + style['column_gap']) / count -
|
||||
style['column_gap'])
|
||||
|
||||
def create_column_box(children):
|
||||
column_box = box.anonymous_from(box, children=children)
|
||||
resolve_percentages(column_box, containing_block)
|
||||
column_box.is_column = True
|
||||
column_box.width = width
|
||||
column_box.position_x = box.content_box_x()
|
||||
column_box.position_y = box.content_box_y()
|
||||
return column_box
|
||||
|
||||
# Handle column-span property.
|
||||
# We want to get the following structure:
|
||||
# columns_and_blocks = [
|
||||
# [column_child_1, column_child_2],
|
||||
# spanning_block,
|
||||
# …
|
||||
# ]
|
||||
columns_and_blocks = []
|
||||
column_children = []
|
||||
for child in box.children:
|
||||
if child.style['column_span'] == 'all':
|
||||
if column_children:
|
||||
columns_and_blocks.append(column_children)
|
||||
columns_and_blocks.append(child.copy())
|
||||
column_children = []
|
||||
continue
|
||||
column_children.append(child.copy())
|
||||
if column_children:
|
||||
columns_and_blocks.append(column_children)
|
||||
|
||||
if not box.children:
|
||||
next_page = {'break': 'any', 'page': None}
|
||||
skip_stack = None
|
||||
|
||||
# Balance.
|
||||
#
|
||||
# The current algorithm starts from the ideal height (the total height
|
||||
# divided by the number of columns). We then iterate until the last column
|
||||
# is not the highest one. At the end of each loop, we add the minimal
|
||||
# height needed to make one direct child at the top of one column go to the
|
||||
# end of the previous column.
|
||||
#
|
||||
# We rely on a real rendering for each loop, and with a stupid algorithm
|
||||
# like this it can last minutes…
|
||||
|
||||
adjoining_margins = []
|
||||
current_position_y = box.content_box_y()
|
||||
new_children = []
|
||||
for column_children_or_block in columns_and_blocks:
|
||||
if not isinstance(column_children_or_block, list):
|
||||
# We get a spanning block, we display it like other blocks.
|
||||
block = column_children_or_block
|
||||
resolve_percentages(block, containing_block)
|
||||
block.position_x = box.content_box_x()
|
||||
block.position_y = current_position_y
|
||||
new_child, _, _, adjoining_margins, _ = block_level_layout(
|
||||
context, block, original_bottom_space, skip_stack,
|
||||
containing_block, page_is_empty, absolute_boxes, fixed_boxes,
|
||||
adjoining_margins, discard=False)
|
||||
new_children.append(new_child)
|
||||
current_position_y = (
|
||||
new_child.border_height() + new_child.border_box_y())
|
||||
adjoining_margins.append(new_child.margin_bottom)
|
||||
continue
|
||||
|
||||
excluded_shapes = context.excluded_shapes[:]
|
||||
|
||||
# We have a list of children that we have to balance between columns.
|
||||
column_children = column_children_or_block
|
||||
|
||||
# Find the total height of the content
|
||||
current_position_y += collapse_margin(adjoining_margins)
|
||||
adjoining_margins = []
|
||||
column_box = create_column_box(column_children)
|
||||
column_box.position_y = current_position_y
|
||||
new_child, _, _, _, _ = block_box_layout(
|
||||
context, column_box, -float('inf'), skip_stack, containing_block,
|
||||
page_is_empty, [], [], [], discard=False)
|
||||
height = new_child.margin_height()
|
||||
if style['column_fill'] == 'balance':
|
||||
height /= count
|
||||
|
||||
# Try to render columns until the content fits, increase the column
|
||||
# height step by step.
|
||||
column_skip_stack = skip_stack
|
||||
lost_space = float('inf')
|
||||
while True:
|
||||
# Remove extra excluded shapes introduced during previous loop
|
||||
new_excluded_shapes = (
|
||||
len(context.excluded_shapes) - len(excluded_shapes))
|
||||
for i in range(new_excluded_shapes):
|
||||
context.excluded_shapes.pop()
|
||||
|
||||
for i in range(count):
|
||||
# Render the column
|
||||
new_box, resume_at, next_page, _, _ = block_box_layout(
|
||||
context, column_box,
|
||||
context.page_bottom - current_position_y - height,
|
||||
column_skip_stack, containing_block, page_is_empty,
|
||||
[], [], [], discard=False)
|
||||
if new_box is None:
|
||||
# We didn't render anything. Give up and use the max
|
||||
# content height.
|
||||
height *= count
|
||||
continue
|
||||
column_skip_stack = resume_at
|
||||
|
||||
in_flow_children = [
|
||||
child for child in new_box.children
|
||||
if child.is_in_normal_flow()]
|
||||
|
||||
if in_flow_children:
|
||||
# Get the empty space at the bottom of the column box
|
||||
empty_space = height - (
|
||||
in_flow_children[-1].position_y - box.content_box_y() +
|
||||
in_flow_children[-1].margin_height())
|
||||
|
||||
# Get the minimum size needed to render the next box
|
||||
next_box, _, _, _, _ = block_box_layout(
|
||||
context, column_box,
|
||||
context.page_bottom - box.content_box_y(),
|
||||
column_skip_stack, containing_block, True, [], [], [],
|
||||
discard=False)
|
||||
for child in next_box.children:
|
||||
if child.is_in_normal_flow():
|
||||
next_box_size = child.margin_height()
|
||||
break
|
||||
else:
|
||||
empty_space = next_box_size = 0
|
||||
|
||||
# Append the size needed to render the next box in this
|
||||
# column.
|
||||
#
|
||||
# The next box size may be smaller than the empty space, for
|
||||
# example when the next box can't be separated from its own
|
||||
# next box. In this case we don't try to find the real value
|
||||
# and let the workaround below fix this for us.
|
||||
#
|
||||
# We also want to avoid very small values that may have been
|
||||
# introduced by rounding errors. As the workaround below at
|
||||
# least adds 1 pixel for each loop, we can ignore lost spaces
|
||||
# lower than 1px.
|
||||
if next_box_size - empty_space > 1:
|
||||
lost_space = min(lost_space, next_box_size - empty_space)
|
||||
|
||||
# Stop if we already rendered the whole content
|
||||
if resume_at is None:
|
||||
break
|
||||
|
||||
if column_skip_stack is None:
|
||||
# We rendered the whole content, stop
|
||||
break
|
||||
else:
|
||||
if lost_space == float('inf'):
|
||||
# We didn't find the extra size needed to render a child in
|
||||
# the previous column, increase height by the minimal
|
||||
# value.
|
||||
height += 1
|
||||
else:
|
||||
# Increase the columns heights and render them once again
|
||||
height += lost_space
|
||||
column_skip_stack = skip_stack
|
||||
|
||||
# TODO: check box.style['max']-height
|
||||
bottom_space = max(
|
||||
bottom_space, context.page_bottom - current_position_y - height)
|
||||
|
||||
# Replace the current box children with columns
|
||||
i = 0
|
||||
max_column_height = 0
|
||||
columns = []
|
||||
while True:
|
||||
if i == count - 1:
|
||||
bottom_space = original_bottom_space
|
||||
column_box = create_column_box(column_children)
|
||||
column_box.position_y = current_position_y
|
||||
if style['direction'] == 'rtl':
|
||||
column_box.position_x += (
|
||||
box.width - (i + 1) * width - i * style['column_gap'])
|
||||
else:
|
||||
column_box.position_x += i * (width + style['column_gap'])
|
||||
new_child, column_skip_stack, column_next_page, _, _ = (
|
||||
block_box_layout(
|
||||
context, column_box, bottom_space, skip_stack,
|
||||
containing_block, page_is_empty, absolute_boxes,
|
||||
fixed_boxes, None, discard=False))
|
||||
if new_child is None:
|
||||
break
|
||||
next_page = column_next_page
|
||||
skip_stack = column_skip_stack
|
||||
columns.append(new_child)
|
||||
max_column_height = max(
|
||||
max_column_height, new_child.margin_height())
|
||||
if skip_stack is None:
|
||||
break
|
||||
i += 1
|
||||
if i == count and not known_height:
|
||||
# [If] a declaration that constrains the column height
|
||||
# (e.g., using height or max-height). In this case,
|
||||
# additional column boxes are created in the inline
|
||||
# direction.
|
||||
break
|
||||
|
||||
current_position_y += max_column_height
|
||||
for column in columns:
|
||||
column.height = max_column_height
|
||||
new_children.append(column)
|
||||
|
||||
if box.children and not new_children:
|
||||
# The box has children but none can be drawn, let's skip the whole box
|
||||
return None, (0, None), {'break': 'any', 'page': None}, [], False
|
||||
|
||||
# Set the height of box and the columns
|
||||
box.children = new_children
|
||||
current_position_y += collapse_margin(adjoining_margins)
|
||||
height = current_position_y - box.content_box_y()
|
||||
if box.height == 'auto':
|
||||
box.height = height
|
||||
height_difference = 0
|
||||
else:
|
||||
height_difference = box.height - height
|
||||
if box.min_height != 'auto' and box.min_height > box.height:
|
||||
height_difference += box.min_height - box.height
|
||||
box.height = box.min_height
|
||||
for child in new_children[::-1]:
|
||||
if child.is_column:
|
||||
child.height += height_difference
|
||||
else:
|
||||
break
|
||||
|
||||
if box.style['position'] == 'relative':
|
||||
# New containing block, resolve the layout of the absolute descendants
|
||||
for absolute_box in absolute_boxes:
|
||||
absolute_layout(
|
||||
context, absolute_box, box, fixed_boxes, bottom_space,
|
||||
skip_stack=None)
|
||||
|
||||
return box, skip_stack, next_page, [], False
|
894
venv/Lib/site-packages/weasyprint/layout/flex.py
Normal file
894
venv/Lib/site-packages/weasyprint/layout/flex.py
Normal file
@@ -0,0 +1,894 @@
|
||||
"""
|
||||
weasyprint.layout.flex
|
||||
------------------------
|
||||
|
||||
Layout for flex containers and flex-items.
|
||||
|
||||
"""
|
||||
|
||||
import sys
|
||||
from math import log10
|
||||
|
||||
from ..css.properties import Dimension
|
||||
from ..formatting_structure import boxes
|
||||
from .percent import resolve_one_percentage, resolve_percentages
|
||||
from .preferred import max_content_width, min_content_width
|
||||
from .table import find_in_flow_baseline
|
||||
|
||||
|
||||
class FlexLine(list):
|
||||
pass
|
||||
|
||||
|
||||
def flex_layout(context, box, bottom_space, skip_stack, containing_block,
|
||||
page_is_empty, absolute_boxes, fixed_boxes):
|
||||
from . import block, preferred
|
||||
|
||||
context.create_block_formatting_context()
|
||||
resume_at = None
|
||||
|
||||
# Step 1 is done in formatting_structure.boxes
|
||||
# Step 2
|
||||
if box.style['flex_direction'].startswith('row'):
|
||||
axis, cross = 'width', 'height'
|
||||
else:
|
||||
axis, cross = 'height', 'width'
|
||||
|
||||
margin_left = 0 if box.margin_left == 'auto' else box.margin_left
|
||||
margin_right = 0 if box.margin_right == 'auto' else box.margin_right
|
||||
margin_top = 0 if box.margin_top == 'auto' else box.margin_top
|
||||
margin_bottom = 0 if box.margin_bottom == 'auto' else box.margin_bottom
|
||||
|
||||
if getattr(box, axis) != 'auto':
|
||||
available_main_space = getattr(box, axis)
|
||||
else:
|
||||
if axis == 'width':
|
||||
available_main_space = (
|
||||
containing_block.width -
|
||||
margin_left - margin_right -
|
||||
box.padding_left - box.padding_right -
|
||||
box.border_left_width - box.border_right_width)
|
||||
else:
|
||||
main_space = context.page_bottom - bottom_space - box.position_y
|
||||
if containing_block.height != 'auto':
|
||||
if isinstance(containing_block.height, Dimension):
|
||||
assert containing_block.height.unit == 'px'
|
||||
main_space = min(main_space, containing_block.height.value)
|
||||
else:
|
||||
main_space = min(main_space, containing_block.height)
|
||||
available_main_space = (
|
||||
main_space -
|
||||
margin_top - margin_bottom -
|
||||
box.padding_top - box.padding_bottom -
|
||||
box.border_top_width - box.border_bottom_width)
|
||||
|
||||
if getattr(box, cross) != 'auto':
|
||||
available_cross_space = getattr(box, cross)
|
||||
else:
|
||||
if cross == 'height':
|
||||
main_space = (
|
||||
context.page_bottom - bottom_space - box.content_box_y())
|
||||
if containing_block.height != 'auto':
|
||||
if isinstance(containing_block.height, Dimension):
|
||||
assert containing_block.height.unit == 'px'
|
||||
main_space = min(main_space, containing_block.height.value)
|
||||
else:
|
||||
main_space = min(main_space, containing_block.height)
|
||||
available_cross_space = (
|
||||
main_space -
|
||||
margin_top - margin_bottom -
|
||||
box.padding_top - box.padding_bottom -
|
||||
box.border_top_width - box.border_bottom_width)
|
||||
else:
|
||||
available_cross_space = (
|
||||
containing_block.width -
|
||||
margin_left - margin_right -
|
||||
box.padding_left - box.padding_right -
|
||||
box.border_left_width - box.border_right_width)
|
||||
|
||||
# Step 3
|
||||
children = box.children
|
||||
parent_box = box.copy_with_children(children)
|
||||
resolve_percentages(parent_box, containing_block)
|
||||
# TODO: removing auto margins is OK for this step, but margins should be
|
||||
# calculated later.
|
||||
if parent_box.margin_top == 'auto':
|
||||
box.margin_top = parent_box.margin_top = 0
|
||||
if parent_box.margin_bottom == 'auto':
|
||||
box.margin_bottom = parent_box.margin_bottom = 0
|
||||
if parent_box.margin_left == 'auto':
|
||||
box.margin_left = parent_box.margin_left = 0
|
||||
if parent_box.margin_right == 'auto':
|
||||
box.margin_right = parent_box.margin_right = 0
|
||||
if isinstance(parent_box, boxes.FlexBox):
|
||||
block.block_level_width(parent_box, containing_block)
|
||||
else:
|
||||
parent_box.width = preferred.flex_max_content_width(
|
||||
context, parent_box)
|
||||
original_skip_stack = skip_stack
|
||||
if skip_stack is not None:
|
||||
# TODO: handle multiple skip stacks
|
||||
(skip, skip_stack), = skip_stack.items()
|
||||
if box.style['flex_direction'].endswith('-reverse'):
|
||||
children = children[:skip + 1]
|
||||
else:
|
||||
children = children[skip:]
|
||||
skip_stack = skip_stack
|
||||
else:
|
||||
skip_stack = None
|
||||
child_skip_stack = skip_stack
|
||||
for child in children:
|
||||
if not child.is_flex_item:
|
||||
continue
|
||||
|
||||
# See https://www.w3.org/TR/css-flexbox-1/#min-size-auto
|
||||
if child.style['overflow'] == 'visible':
|
||||
main_flex_direction = axis
|
||||
else:
|
||||
main_flex_direction = None
|
||||
resolve_percentages(child, containing_block, main_flex_direction)
|
||||
child.position_x = parent_box.content_box_x()
|
||||
child.position_y = parent_box.content_box_y()
|
||||
if child.min_width == 'auto':
|
||||
specified_size = (
|
||||
child.width if child.width != 'auto' else float('inf'))
|
||||
if isinstance(child, boxes.ParentBox):
|
||||
new_child = child.copy_with_children(child.children)
|
||||
else:
|
||||
new_child = child.copy()
|
||||
new_child.style = child.style.copy()
|
||||
new_child.style['width'] = 'auto'
|
||||
new_child.style['min_width'] = Dimension(0, 'px')
|
||||
new_child.style['max_width'] = Dimension(float('inf'), 'px')
|
||||
content_size = min_content_width(context, new_child, outer=False)
|
||||
child.min_width = min(specified_size, content_size)
|
||||
elif child.min_height == 'auto':
|
||||
# TODO: find a way to get min-content-height
|
||||
specified_size = (
|
||||
child.height if child.height != 'auto' else float('inf'))
|
||||
if isinstance(child, boxes.ParentBox):
|
||||
new_child = child.copy_with_children(child.children)
|
||||
else:
|
||||
new_child = child.copy()
|
||||
new_child.style = child.style.copy()
|
||||
new_child.style['height'] = 'auto'
|
||||
new_child.style['min_height'] = Dimension(0, 'px')
|
||||
new_child.style['max_height'] = Dimension(float('inf'), 'px')
|
||||
new_child = block.block_level_layout(
|
||||
context, new_child, -float('inf'), child_skip_stack,
|
||||
parent_box, page_is_empty, [], [], [], False)[0]
|
||||
content_size = new_child.height
|
||||
child.min_height = min(specified_size, content_size)
|
||||
|
||||
child.style = child.style.copy()
|
||||
|
||||
if child.style['flex_basis'] == 'content':
|
||||
flex_basis = child.flex_basis = 'content'
|
||||
else:
|
||||
resolve_one_percentage(child, 'flex_basis', available_main_space)
|
||||
flex_basis = child.flex_basis
|
||||
|
||||
# "If a value would resolve to auto for width, it instead resolves
|
||||
# to content for flex-basis." Let's do this for height too.
|
||||
# See https://www.w3.org/TR/css-flexbox-1/#propdef-flex-basis
|
||||
resolve_one_percentage(child, axis, available_main_space)
|
||||
if flex_basis == 'auto':
|
||||
if child.style[axis] == 'auto':
|
||||
flex_basis = 'content'
|
||||
else:
|
||||
if axis == 'width':
|
||||
flex_basis = child.border_width()
|
||||
if child.margin_left != 'auto':
|
||||
flex_basis += child.margin_left
|
||||
if child.margin_right != 'auto':
|
||||
flex_basis += child.margin_right
|
||||
else:
|
||||
flex_basis = child.border_height()
|
||||
if child.margin_top != 'auto':
|
||||
flex_basis += child.margin_top
|
||||
if child.margin_bottom != 'auto':
|
||||
flex_basis += child.margin_bottom
|
||||
|
||||
# Step 3.A
|
||||
if flex_basis != 'content':
|
||||
child.flex_base_size = flex_basis
|
||||
|
||||
# TODO: Step 3.B
|
||||
# TODO: Step 3.C
|
||||
|
||||
# Step 3.D is useless, as we never have infinite sizes on paged media
|
||||
|
||||
# Step 3.E
|
||||
else:
|
||||
child.style[axis] = 'max-content'
|
||||
|
||||
# TODO: don't set style value, support *-content values instead
|
||||
if child.style[axis] == 'max-content':
|
||||
child.style[axis] = 'auto'
|
||||
if axis == 'width':
|
||||
child.flex_base_size = max_content_width(context, child)
|
||||
else:
|
||||
if isinstance(child, boxes.ParentBox):
|
||||
new_child = child.copy_with_children(child.children)
|
||||
else:
|
||||
new_child = child.copy()
|
||||
new_child.width = float('inf')
|
||||
new_child = block.block_level_layout(
|
||||
context, new_child, -float('inf'), child_skip_stack,
|
||||
parent_box, page_is_empty, absolute_boxes, fixed_boxes,
|
||||
adjoining_margins=[], discard=False)[0]
|
||||
child.flex_base_size = new_child.margin_height()
|
||||
elif child.style[axis] == 'min-content':
|
||||
child.style[axis] = 'auto'
|
||||
if axis == 'width':
|
||||
child.flex_base_size = min_content_width(context, child)
|
||||
else:
|
||||
if isinstance(child, boxes.ParentBox):
|
||||
new_child = child.copy_with_children(child.children)
|
||||
else:
|
||||
new_child = child.copy()
|
||||
new_child.width = 0
|
||||
new_child = block.block_level_layout(
|
||||
context, new_child, -float('inf'), child_skip_stack,
|
||||
parent_box, page_is_empty, absolute_boxes, fixed_boxes,
|
||||
adjoining_margins=[], discard=False)[0]
|
||||
child.flex_base_size = new_child.margin_height()
|
||||
else:
|
||||
assert child.style[axis].unit == 'px'
|
||||
# TODO: should we add padding, borders and margins?
|
||||
child.flex_base_size = child.style[axis].value
|
||||
|
||||
child.hypothetical_main_size = max(
|
||||
getattr(child, f'min_{axis}'), min(
|
||||
child.flex_base_size, getattr(child, f'max_{axis}')))
|
||||
|
||||
# Skip stack is only for the first child
|
||||
child_skip_stack = None
|
||||
|
||||
# Step 4
|
||||
# TODO: the whole step has to be fixed
|
||||
if axis == 'width':
|
||||
block.block_level_width(box, containing_block)
|
||||
else:
|
||||
if box.style['height'] != 'auto':
|
||||
box.height = box.style['height'].value
|
||||
else:
|
||||
box.height = 0
|
||||
for i, child in enumerate(children):
|
||||
if not child.is_flex_item:
|
||||
continue
|
||||
child_height = (
|
||||
child.hypothetical_main_size +
|
||||
child.border_top_width + child.border_bottom_width +
|
||||
child.padding_top + child.padding_bottom)
|
||||
if getattr(box, axis) == 'auto' and (
|
||||
child_height + box.height > available_main_space):
|
||||
resume_at = {i: None}
|
||||
children = children[:i + 1]
|
||||
break
|
||||
box.height += child_height
|
||||
|
||||
# Step 5
|
||||
flex_lines = []
|
||||
|
||||
line = []
|
||||
line_size = 0
|
||||
axis_size = getattr(box, axis)
|
||||
for i, child in enumerate(
|
||||
sorted(children, key=lambda item: item.style['order'])):
|
||||
if not child.is_flex_item:
|
||||
continue
|
||||
line_size += child.hypothetical_main_size
|
||||
if box.style['flex_wrap'] != 'nowrap' and line_size > axis_size:
|
||||
if line:
|
||||
flex_lines.append(FlexLine(line))
|
||||
line = [(i, child)]
|
||||
line_size = child.hypothetical_main_size
|
||||
else:
|
||||
line.append((i, child))
|
||||
flex_lines.append(FlexLine(line))
|
||||
line = []
|
||||
line_size = 0
|
||||
else:
|
||||
line.append((i, child))
|
||||
if line:
|
||||
flex_lines.append(FlexLine(line))
|
||||
|
||||
# TODO: handle *-reverse using the terminology from the specification
|
||||
if box.style['flex_wrap'] == 'wrap-reverse':
|
||||
flex_lines.reverse()
|
||||
if box.style['flex_direction'].endswith('-reverse'):
|
||||
for line in flex_lines:
|
||||
line.reverse()
|
||||
|
||||
# Step 6
|
||||
# See https://www.w3.org/TR/css-flexbox-1/#resolve-flexible-lengths
|
||||
for line in flex_lines:
|
||||
# Step 6 - 9.7.1
|
||||
hypothetical_main_size = sum(
|
||||
child.hypothetical_main_size for i, child in line)
|
||||
if hypothetical_main_size < available_main_space:
|
||||
flex_factor_type = 'grow'
|
||||
else:
|
||||
flex_factor_type = 'shrink'
|
||||
|
||||
# Step 6 - 9.7.2
|
||||
for i, child in line:
|
||||
if flex_factor_type == 'grow':
|
||||
child.flex_factor = child.style['flex_grow']
|
||||
else:
|
||||
child.flex_factor = child.style['flex_shrink']
|
||||
if (child.flex_factor == 0 or
|
||||
(flex_factor_type == 'grow' and
|
||||
child.flex_base_size > child.hypothetical_main_size) or
|
||||
(flex_factor_type == 'shrink' and
|
||||
child.flex_base_size < child.hypothetical_main_size)):
|
||||
child.target_main_size = child.hypothetical_main_size
|
||||
child.frozen = True
|
||||
else:
|
||||
child.frozen = False
|
||||
|
||||
# Step 6 - 9.7.3
|
||||
initial_free_space = available_main_space
|
||||
for i, child in line:
|
||||
if child.frozen:
|
||||
initial_free_space -= child.target_main_size
|
||||
else:
|
||||
initial_free_space -= child.flex_base_size
|
||||
|
||||
# Step 6 - 9.7.4
|
||||
while not all(child.frozen for i, child in line):
|
||||
unfrozen_factor_sum = 0
|
||||
remaining_free_space = available_main_space
|
||||
|
||||
# Step 6 - 9.7.4.b
|
||||
for i, child in line:
|
||||
if child.frozen:
|
||||
remaining_free_space -= child.target_main_size
|
||||
else:
|
||||
remaining_free_space -= child.flex_base_size
|
||||
unfrozen_factor_sum += child.flex_factor
|
||||
|
||||
if unfrozen_factor_sum < 1:
|
||||
initial_free_space *= unfrozen_factor_sum
|
||||
|
||||
if initial_free_space == float('inf'):
|
||||
initial_free_space = sys.maxsize
|
||||
if remaining_free_space == float('inf'):
|
||||
remaining_free_space = sys.maxsize
|
||||
|
||||
initial_magnitude = (
|
||||
int(log10(initial_free_space)) if initial_free_space > 0
|
||||
else -float('inf'))
|
||||
remaining_magnitude = (
|
||||
int(log10(remaining_free_space)) if remaining_free_space > 0
|
||||
else -float('inf'))
|
||||
if initial_magnitude < remaining_magnitude:
|
||||
remaining_free_space = initial_free_space
|
||||
|
||||
# Step 6 - 9.7.4.c
|
||||
if remaining_free_space == 0:
|
||||
# "Do nothing", but we at least set the flex_base_size as
|
||||
# target_main_size for next step.
|
||||
for i, child in line:
|
||||
if not child.frozen:
|
||||
child.target_main_size = child.flex_base_size
|
||||
else:
|
||||
scaled_flex_shrink_factors_sum = 0
|
||||
flex_grow_factors_sum = 0
|
||||
for i, child in line:
|
||||
if not child.frozen:
|
||||
child.scaled_flex_shrink_factor = (
|
||||
child.flex_base_size * child.style['flex_shrink'])
|
||||
scaled_flex_shrink_factors_sum += (
|
||||
child.scaled_flex_shrink_factor)
|
||||
flex_grow_factors_sum += child.style['flex_grow']
|
||||
for i, child in line:
|
||||
if not child.frozen:
|
||||
if flex_factor_type == 'grow':
|
||||
ratio = (
|
||||
child.style['flex_grow'] /
|
||||
flex_grow_factors_sum)
|
||||
child.target_main_size = (
|
||||
child.flex_base_size +
|
||||
remaining_free_space * ratio)
|
||||
elif flex_factor_type == 'shrink':
|
||||
if scaled_flex_shrink_factors_sum == 0:
|
||||
child.target_main_size = child.flex_base_size
|
||||
else:
|
||||
ratio = (
|
||||
child.scaled_flex_shrink_factor /
|
||||
scaled_flex_shrink_factors_sum)
|
||||
child.target_main_size = (
|
||||
child.flex_base_size +
|
||||
remaining_free_space * ratio)
|
||||
|
||||
# Step 6 - 9.7.4.d
|
||||
# TODO: First part of this step is useless until 3.E is correct
|
||||
for i, child in line:
|
||||
child.adjustment = 0
|
||||
if not child.frozen and child.target_main_size < 0:
|
||||
child.adjustment = -child.target_main_size
|
||||
child.target_main_size = 0
|
||||
|
||||
# Step 6 - 9.7.4.e
|
||||
adjustments = sum(child.adjustment for i, child in line)
|
||||
for i, child in line:
|
||||
if adjustments == 0:
|
||||
child.frozen = True
|
||||
elif adjustments > 0 and child.adjustment > 0:
|
||||
child.frozen = True
|
||||
elif adjustments < 0 and child.adjustment < 0:
|
||||
child.frozen = True
|
||||
|
||||
# Step 6 - 9.7.5
|
||||
for i, child in line:
|
||||
if axis == 'width':
|
||||
child.width = (
|
||||
child.target_main_size -
|
||||
child.padding_left - child.padding_right -
|
||||
child.border_left_width - child.border_right_width)
|
||||
if child.margin_left != 'auto':
|
||||
child.width -= child.margin_left
|
||||
if child.margin_right != 'auto':
|
||||
child.width -= child.margin_right
|
||||
else:
|
||||
child.height = (
|
||||
child.target_main_size -
|
||||
child.padding_top - child.padding_bottom -
|
||||
child.border_top_width - child.border_top_width)
|
||||
if child.margin_left != 'auto':
|
||||
child.height -= child.margin_left
|
||||
if child.margin_right != 'auto':
|
||||
child.height -= child.margin_right
|
||||
|
||||
# Step 7
|
||||
# TODO: Fix TODO in build.flex_children
|
||||
# TODO: Handle breaks
|
||||
new_flex_lines = []
|
||||
child_skip_stack = skip_stack
|
||||
for line in flex_lines:
|
||||
new_flex_line = FlexLine()
|
||||
for i, child in line:
|
||||
# TODO: Find another way than calling block_level_layout_switch to
|
||||
# get baseline and child.height
|
||||
if child.margin_top == 'auto':
|
||||
child.margin_top = 0
|
||||
if child.margin_bottom == 'auto':
|
||||
child.margin_bottom = 0
|
||||
if isinstance(child, boxes.ParentBox):
|
||||
child_copy = child.copy_with_children(child.children)
|
||||
else:
|
||||
child_copy = child.copy()
|
||||
block.block_level_width(child_copy, parent_box)
|
||||
new_child, _, _, adjoining_margins, _ = (
|
||||
block.block_level_layout_switch(
|
||||
context, child_copy, -float('inf'), child_skip_stack,
|
||||
parent_box, page_is_empty, absolute_boxes, fixed_boxes,
|
||||
adjoining_margins=[], discard=False))
|
||||
|
||||
child._baseline = find_in_flow_baseline(new_child) or 0
|
||||
if cross == 'height':
|
||||
child.height = new_child.height
|
||||
# As flex items margins never collapse (with other flex items
|
||||
# or with the flex container), we can add the adjoining margins
|
||||
# to the child bottom margin.
|
||||
child.margin_bottom += block.collapse_margin(adjoining_margins)
|
||||
else:
|
||||
child.width = min_content_width(context, child, outer=False)
|
||||
|
||||
new_flex_line.append((i, child))
|
||||
|
||||
# Skip stack is only for the first child
|
||||
child_skip_stack = None
|
||||
|
||||
if new_flex_line:
|
||||
new_flex_lines.append(new_flex_line)
|
||||
flex_lines = new_flex_lines
|
||||
|
||||
# Step 8
|
||||
cross_size = getattr(box, cross)
|
||||
if len(flex_lines) == 1 and cross_size != 'auto':
|
||||
flex_lines[0].cross_size = cross_size
|
||||
else:
|
||||
for line in flex_lines:
|
||||
collected_items = []
|
||||
not_collected_items = []
|
||||
for i, child in line:
|
||||
align_self = child.style['align_self']
|
||||
if (box.style['flex_direction'].startswith('row') and
|
||||
align_self == 'baseline' and
|
||||
child.margin_top != 'auto' and
|
||||
child.margin_bottom != 'auto'):
|
||||
collected_items.append(child)
|
||||
else:
|
||||
not_collected_items.append(child)
|
||||
cross_start_distance = 0
|
||||
cross_end_distance = 0
|
||||
for child in collected_items:
|
||||
baseline = child._baseline - child.position_y
|
||||
cross_start_distance = max(cross_start_distance, baseline)
|
||||
cross_end_distance = max(
|
||||
cross_end_distance, child.margin_height() - baseline)
|
||||
collected_cross_size = cross_start_distance + cross_end_distance
|
||||
non_collected_cross_size = 0
|
||||
if not_collected_items:
|
||||
non_collected_cross_size = float('-inf')
|
||||
for child in not_collected_items:
|
||||
if cross == 'height':
|
||||
child_cross_size = child.border_height()
|
||||
if child.margin_top != 'auto':
|
||||
child_cross_size += child.margin_top
|
||||
if child.margin_bottom != 'auto':
|
||||
child_cross_size += child.margin_bottom
|
||||
else:
|
||||
child_cross_size = child.border_width()
|
||||
if child.margin_left != 'auto':
|
||||
child_cross_size += child.margin_left
|
||||
if child.margin_right != 'auto':
|
||||
child_cross_size += child.margin_right
|
||||
non_collected_cross_size = max(
|
||||
child_cross_size, non_collected_cross_size)
|
||||
line.cross_size = max(
|
||||
collected_cross_size, non_collected_cross_size)
|
||||
|
||||
if len(flex_lines) == 1:
|
||||
line, = flex_lines
|
||||
min_cross_size = getattr(box, f'min_{cross}')
|
||||
if min_cross_size == 'auto':
|
||||
min_cross_size = float('-inf')
|
||||
max_cross_size = getattr(box, f'max_{cross}')
|
||||
if max_cross_size == 'auto':
|
||||
max_cross_size = float('inf')
|
||||
line.cross_size = max(
|
||||
min_cross_size, min(line.cross_size, max_cross_size))
|
||||
|
||||
# Step 9
|
||||
if box.style['align_content'] == 'stretch':
|
||||
definite_cross_size = None
|
||||
if cross == 'height' and box.style['height'] != 'auto':
|
||||
definite_cross_size = box.style['height'].value
|
||||
elif cross == 'width':
|
||||
if isinstance(box, boxes.FlexBox):
|
||||
if box.style['width'] == 'auto':
|
||||
definite_cross_size = available_cross_space
|
||||
else:
|
||||
definite_cross_size = box.style['width'].value
|
||||
if definite_cross_size is not None:
|
||||
extra_cross_size = definite_cross_size - sum(
|
||||
line.cross_size for line in flex_lines)
|
||||
if extra_cross_size:
|
||||
for line in flex_lines:
|
||||
line.cross_size += extra_cross_size / len(flex_lines)
|
||||
|
||||
# TODO: Step 10
|
||||
|
||||
# Step 11
|
||||
for line in flex_lines:
|
||||
for i, child in line:
|
||||
align_self = child.style['align_self']
|
||||
if align_self == 'auto':
|
||||
align_self = box.style['align_items']
|
||||
if align_self == 'stretch' and child.style[cross] == 'auto':
|
||||
cross_margins = (
|
||||
(child.margin_top, child.margin_bottom)
|
||||
if cross == 'height'
|
||||
else (child.margin_left, child.margin_right))
|
||||
if child.style[cross] == 'auto':
|
||||
if 'auto' not in cross_margins:
|
||||
cross_size = line.cross_size
|
||||
if cross == 'height':
|
||||
cross_size -= (
|
||||
child.margin_top + child.margin_bottom +
|
||||
child.padding_top + child.padding_bottom +
|
||||
child.border_top_width +
|
||||
child.border_bottom_width)
|
||||
else:
|
||||
cross_size -= (
|
||||
child.margin_left + child.margin_right +
|
||||
child.padding_left + child.padding_right +
|
||||
child.border_left_width +
|
||||
child.border_right_width)
|
||||
setattr(child, cross, cross_size)
|
||||
# TODO: redo layout?
|
||||
# else: Cross size has been set by step 7
|
||||
|
||||
# Step 12
|
||||
original_position_axis = (
|
||||
box.content_box_x() if axis == 'width'
|
||||
else box.content_box_y())
|
||||
justify_content = box.style['justify_content']
|
||||
if box.style['flex_direction'].endswith('-reverse'):
|
||||
if justify_content == 'flex-start':
|
||||
justify_content = 'flex-end'
|
||||
elif justify_content == 'flex-end':
|
||||
justify_content = 'flex-start'
|
||||
|
||||
for line in flex_lines:
|
||||
position_axis = original_position_axis
|
||||
if axis == 'width':
|
||||
free_space = box.width
|
||||
for i, child in line:
|
||||
free_space -= child.border_width()
|
||||
if child.margin_left != 'auto':
|
||||
free_space -= child.margin_left
|
||||
if child.margin_right != 'auto':
|
||||
free_space -= child.margin_right
|
||||
else:
|
||||
free_space = box.height
|
||||
for i, child in line:
|
||||
free_space -= child.border_height()
|
||||
if child.margin_top != 'auto':
|
||||
free_space -= child.margin_top
|
||||
if child.margin_bottom != 'auto':
|
||||
free_space -= child.margin_bottom
|
||||
|
||||
margins = 0
|
||||
for i, child in line:
|
||||
if axis == 'width':
|
||||
if child.margin_left == 'auto':
|
||||
margins += 1
|
||||
if child.margin_right == 'auto':
|
||||
margins += 1
|
||||
else:
|
||||
if child.margin_top == 'auto':
|
||||
margins += 1
|
||||
if child.margin_bottom == 'auto':
|
||||
margins += 1
|
||||
if margins:
|
||||
free_space /= margins
|
||||
for i, child in line:
|
||||
if axis == 'width':
|
||||
if child.margin_left == 'auto':
|
||||
child.margin_left = free_space
|
||||
if child.margin_right == 'auto':
|
||||
child.margin_right = free_space
|
||||
else:
|
||||
if child.margin_top == 'auto':
|
||||
child.margin_top = free_space
|
||||
if child.margin_bottom == 'auto':
|
||||
child.margin_bottom = free_space
|
||||
free_space = 0
|
||||
|
||||
if box.style['direction'] == 'rtl' and axis == 'width':
|
||||
free_space *= -1
|
||||
|
||||
if justify_content == 'flex-end':
|
||||
position_axis += free_space
|
||||
elif justify_content == 'center':
|
||||
position_axis += free_space / 2
|
||||
elif justify_content == 'space-around':
|
||||
position_axis += free_space / len(line) / 2
|
||||
elif justify_content == 'space-evenly':
|
||||
position_axis += free_space / (len(line) + 1)
|
||||
|
||||
for i, child in line:
|
||||
if axis == 'width':
|
||||
child.position_x = position_axis
|
||||
if justify_content == 'stretch':
|
||||
child.width += free_space / len(line)
|
||||
else:
|
||||
child.position_y = position_axis
|
||||
margin_axis = (
|
||||
child.margin_width() if axis == 'width'
|
||||
else child.margin_height())
|
||||
if box.style['direction'] == 'rtl' and axis == 'width':
|
||||
margin_axis *= -1
|
||||
position_axis += margin_axis
|
||||
if justify_content == 'space-around':
|
||||
position_axis += free_space / len(line)
|
||||
elif justify_content == 'space-between':
|
||||
if len(line) > 1:
|
||||
position_axis += free_space / (len(line) - 1)
|
||||
elif justify_content == 'space-evenly':
|
||||
position_axis += free_space / (len(line) + 1)
|
||||
|
||||
# Step 13
|
||||
position_cross = (
|
||||
box.content_box_y() if cross == 'height'
|
||||
else box.content_box_x())
|
||||
for line in flex_lines:
|
||||
line.lower_baseline = 0
|
||||
# TODO: don't duplicate this loop
|
||||
for i, child in line:
|
||||
align_self = child.style['align_self']
|
||||
if align_self == 'auto':
|
||||
align_self = box.style['align_items']
|
||||
if align_self == 'baseline' and axis == 'width':
|
||||
# TODO: handle vertical text
|
||||
child.baseline = child._baseline - position_cross
|
||||
line.lower_baseline = max(line.lower_baseline, child.baseline)
|
||||
for i, child in line:
|
||||
cross_margins = (
|
||||
(child.margin_top, child.margin_bottom) if cross == 'height'
|
||||
else (child.margin_left, child.margin_right))
|
||||
auto_margins = sum([margin == 'auto' for margin in cross_margins])
|
||||
if auto_margins:
|
||||
extra_cross = line.cross_size
|
||||
if cross == 'height':
|
||||
extra_cross -= child.border_height()
|
||||
if child.margin_top != 'auto':
|
||||
extra_cross -= child.margin_top
|
||||
if child.margin_bottom != 'auto':
|
||||
extra_cross -= child.margin_bottom
|
||||
else:
|
||||
extra_cross -= child.border_width()
|
||||
if child.margin_left != 'auto':
|
||||
extra_cross -= child.margin_left
|
||||
if child.margin_right != 'auto':
|
||||
extra_cross -= child.margin_right
|
||||
if extra_cross > 0:
|
||||
extra_cross /= auto_margins
|
||||
if cross == 'height':
|
||||
if child.margin_top == 'auto':
|
||||
child.margin_top = extra_cross
|
||||
if child.margin_bottom == 'auto':
|
||||
child.margin_bottom = extra_cross
|
||||
else:
|
||||
if child.margin_left == 'auto':
|
||||
child.margin_left = extra_cross
|
||||
if child.margin_right == 'auto':
|
||||
child.margin_right = extra_cross
|
||||
else:
|
||||
if cross == 'height':
|
||||
if child.margin_top == 'auto':
|
||||
child.margin_top = 0
|
||||
child.margin_bottom = extra_cross
|
||||
else:
|
||||
if child.margin_left == 'auto':
|
||||
child.margin_left = 0
|
||||
child.margin_right = extra_cross
|
||||
else:
|
||||
# Step 14
|
||||
align_self = child.style['align_self']
|
||||
if align_self == 'auto':
|
||||
align_self = box.style['align_items']
|
||||
position = 'position_y' if cross == 'height' else 'position_x'
|
||||
setattr(child, position, position_cross)
|
||||
if align_self == 'flex-end':
|
||||
if cross == 'height':
|
||||
child.position_y += (
|
||||
line.cross_size - child.margin_height())
|
||||
else:
|
||||
child.position_x += (
|
||||
line.cross_size - child.margin_width())
|
||||
elif align_self == 'center':
|
||||
if cross == 'height':
|
||||
child.position_y += (
|
||||
line.cross_size - child.margin_height()) / 2
|
||||
else:
|
||||
child.position_x += (
|
||||
line.cross_size - child.margin_width()) / 2
|
||||
elif align_self == 'baseline':
|
||||
if cross == 'height':
|
||||
child.position_y += (
|
||||
line.lower_baseline - child.baseline)
|
||||
else:
|
||||
# Handle vertical text
|
||||
pass
|
||||
elif align_self == 'stretch':
|
||||
if child.style[cross] == 'auto':
|
||||
if cross == 'height':
|
||||
margins = child.margin_top + child.margin_bottom
|
||||
else:
|
||||
margins = child.margin_left + child.margin_right
|
||||
if child.style['box_sizing'] == 'content-box':
|
||||
if cross == 'height':
|
||||
margins += (
|
||||
child.border_top_width +
|
||||
child.border_bottom_width +
|
||||
child.padding_top + child.padding_bottom)
|
||||
else:
|
||||
margins += (
|
||||
child.border_left_width +
|
||||
child.border_right_width +
|
||||
child.padding_left + child.padding_right)
|
||||
# TODO: don't set style width, find a way to avoid
|
||||
# width re-calculation after Step 16
|
||||
child.style[cross] = Dimension(
|
||||
line.cross_size - margins, 'px')
|
||||
position_cross += line.cross_size
|
||||
|
||||
# Step 15
|
||||
if getattr(box, cross) == 'auto':
|
||||
# TODO: handle min-max
|
||||
setattr(box, cross, sum(line.cross_size for line in flex_lines))
|
||||
|
||||
# Step 16
|
||||
elif len(flex_lines) > 1:
|
||||
extra_cross_size = getattr(box, cross) - sum(
|
||||
line.cross_size for line in flex_lines)
|
||||
direction = 'position_y' if cross == 'height' else 'position_x'
|
||||
if extra_cross_size > 0:
|
||||
cross_translate = 0
|
||||
for line in flex_lines:
|
||||
for i, child in line:
|
||||
if child.is_flex_item:
|
||||
current_value = getattr(child, direction)
|
||||
current_value += cross_translate
|
||||
setattr(child, direction, current_value)
|
||||
if box.style['align_content'] == 'flex-end':
|
||||
setattr(
|
||||
child, direction,
|
||||
current_value + extra_cross_size)
|
||||
elif box.style['align_content'] == 'center':
|
||||
setattr(
|
||||
child, direction,
|
||||
current_value + extra_cross_size / 2)
|
||||
elif box.style['align_content'] == 'space-around':
|
||||
setattr(
|
||||
child, direction,
|
||||
current_value + extra_cross_size /
|
||||
len(flex_lines) / 2)
|
||||
elif box.style['align_content'] == 'space-evenly':
|
||||
setattr(
|
||||
child, direction,
|
||||
current_value + extra_cross_size /
|
||||
(len(flex_lines) + 1))
|
||||
if box.style['align_content'] == 'space-between':
|
||||
cross_translate += extra_cross_size / (len(flex_lines) - 1)
|
||||
elif box.style['align_content'] == 'space-around':
|
||||
cross_translate += extra_cross_size / len(flex_lines)
|
||||
elif box.style['align_content'] == 'space-evenly':
|
||||
cross_translate += extra_cross_size / (len(flex_lines) + 1)
|
||||
|
||||
# TODO: don't use block_box_layout, see TODOs in Step 14 and
|
||||
# build.flex_children.
|
||||
box = box.copy()
|
||||
box.children = []
|
||||
child_skip_stack = skip_stack
|
||||
for line in flex_lines:
|
||||
for i, child in line:
|
||||
if child.is_flex_item:
|
||||
new_child, child_resume_at = block.block_level_layout_switch(
|
||||
context, child, bottom_space, child_skip_stack, box,
|
||||
page_is_empty, absolute_boxes, fixed_boxes,
|
||||
adjoining_margins=[], discard=False)[:2]
|
||||
if new_child is None:
|
||||
if resume_at:
|
||||
index, = resume_at
|
||||
if index:
|
||||
resume_at = {index + i - 1: None}
|
||||
else:
|
||||
box.children.append(new_child)
|
||||
if child_resume_at is not None:
|
||||
if original_skip_stack:
|
||||
# TODO: handle multiple skip stacks
|
||||
first_level_skip, = original_skip_stack
|
||||
else:
|
||||
first_level_skip = 0
|
||||
if resume_at:
|
||||
index, = resume_at
|
||||
first_level_skip += index
|
||||
resume_at = {first_level_skip + i: child_resume_at}
|
||||
if resume_at:
|
||||
break
|
||||
|
||||
# Skip stack is only for the first child
|
||||
child_skip_stack = None
|
||||
if resume_at:
|
||||
break
|
||||
|
||||
# Set box height
|
||||
# TODO: this is probably useless because of step #15
|
||||
if axis == 'width' and box.height == 'auto':
|
||||
if flex_lines:
|
||||
box.height = sum(line.cross_size for line in flex_lines)
|
||||
else:
|
||||
box.height = 0
|
||||
|
||||
# Set baseline
|
||||
# See https://www.w3.org/TR/css-flexbox-1/#flex-baselines
|
||||
# TODO: use the real algorithm
|
||||
if isinstance(box, boxes.InlineFlexBox):
|
||||
if axis == 'width': # and main text direction is horizontal
|
||||
box.baseline = flex_lines[0].lower_baseline if flex_lines else 0
|
||||
else:
|
||||
box.baseline = ((
|
||||
find_in_flow_baseline(box.children[0])
|
||||
if box.children else 0) or 0)
|
||||
|
||||
context.finish_block_formatting_context(box)
|
||||
|
||||
# TODO: check these returned values
|
||||
return box, resume_at, {'break': 'any', 'page': None}, [], False
|
229
venv/Lib/site-packages/weasyprint/layout/float.py
Normal file
229
venv/Lib/site-packages/weasyprint/layout/float.py
Normal file
@@ -0,0 +1,229 @@
|
||||
"""
|
||||
weasyprint.float
|
||||
----------------
|
||||
|
||||
Layout for floating boxes.
|
||||
|
||||
"""
|
||||
|
||||
from ..formatting_structure import boxes
|
||||
from .min_max import handle_min_max_width
|
||||
from .percent import resolve_percentages, resolve_position_percentages
|
||||
from .preferred import shrink_to_fit
|
||||
from .replaced import inline_replaced_box_width_height
|
||||
from .table import table_wrapper_width
|
||||
|
||||
|
||||
@handle_min_max_width
|
||||
def float_width(box, context, containing_block):
|
||||
# Check that box.width is auto even if the caller does it too, because
|
||||
# the handle_min_max_width decorator can change the value
|
||||
if box.width == 'auto':
|
||||
box.width = shrink_to_fit(context, box, containing_block.width)
|
||||
|
||||
|
||||
def float_layout(context, box, containing_block, absolute_boxes, fixed_boxes,
|
||||
bottom_space, skip_stack):
|
||||
"""Set the width and position of floating ``box``."""
|
||||
from .block import block_container_layout
|
||||
from .flex import flex_layout
|
||||
|
||||
cb_width, cb_height = (containing_block.width, containing_block.height)
|
||||
resolve_percentages(box, (cb_width, cb_height))
|
||||
|
||||
# TODO: This is only handled later in blocks.block_container_layout
|
||||
# http://www.w3.org/TR/CSS21/visudet.html#normal-block
|
||||
if cb_height == 'auto':
|
||||
cb_height = (
|
||||
containing_block.position_y - containing_block.content_box_y())
|
||||
|
||||
resolve_position_percentages(box, (cb_width, cb_height))
|
||||
|
||||
if box.margin_left == 'auto':
|
||||
box.margin_left = 0
|
||||
if box.margin_right == 'auto':
|
||||
box.margin_right = 0
|
||||
if box.margin_top == 'auto':
|
||||
box.margin_top = 0
|
||||
if box.margin_bottom == 'auto':
|
||||
box.margin_bottom = 0
|
||||
|
||||
clearance = get_clearance(context, box)
|
||||
if clearance is not None:
|
||||
box.position_y += clearance
|
||||
|
||||
if isinstance(box, boxes.BlockReplacedBox):
|
||||
inline_replaced_box_width_height(box, containing_block)
|
||||
elif box.width == 'auto':
|
||||
float_width(box, context, containing_block)
|
||||
|
||||
if box.is_table_wrapper:
|
||||
table_wrapper_width(context, box, (cb_width, cb_height))
|
||||
|
||||
if isinstance(box, boxes.BlockContainerBox):
|
||||
context.create_block_formatting_context()
|
||||
box, resume_at, _, _, _ = block_container_layout(
|
||||
context, box, bottom_space=bottom_space,
|
||||
skip_stack=skip_stack, page_is_empty=True,
|
||||
absolute_boxes=absolute_boxes, fixed_boxes=fixed_boxes,
|
||||
adjoining_margins=None, discard=False)
|
||||
context.finish_block_formatting_context(box)
|
||||
elif isinstance(box, boxes.FlexContainerBox):
|
||||
box, resume_at, _, _, _ = flex_layout(
|
||||
context, box, bottom_space=bottom_space,
|
||||
skip_stack=skip_stack, containing_block=containing_block,
|
||||
page_is_empty=True, absolute_boxes=absolute_boxes,
|
||||
fixed_boxes=fixed_boxes)
|
||||
else:
|
||||
assert isinstance(box, boxes.BlockReplacedBox)
|
||||
resume_at = None
|
||||
|
||||
box = find_float_position(context, box, containing_block)
|
||||
|
||||
context.excluded_shapes.append(box)
|
||||
|
||||
return box, resume_at
|
||||
|
||||
|
||||
def find_float_position(context, box, containing_block):
|
||||
"""Get the right position of the float ``box``."""
|
||||
# See http://www.w3.org/TR/CSS2/visuren.html#float-position
|
||||
|
||||
# Point 4 is already handled as box.position_y is set according to the
|
||||
# containing box top position, with collapsing margins handled
|
||||
|
||||
# Points 5 and 6, box.position_y is set to the highest position_y possible
|
||||
if context.excluded_shapes:
|
||||
highest_y = context.excluded_shapes[-1].position_y
|
||||
if box.position_y < highest_y:
|
||||
box.translate(0, highest_y - box.position_y)
|
||||
|
||||
# Points 1 and 2
|
||||
position_x, position_y, available_width = avoid_collisions(
|
||||
context, box, containing_block)
|
||||
|
||||
# Point 9
|
||||
# position_y is set now, let's define position_x
|
||||
# for float: left elements, it's already done!
|
||||
if box.style['float'] == 'right':
|
||||
position_x += available_width - box.margin_width()
|
||||
|
||||
box.translate(position_x - box.position_x, position_y - box.position_y)
|
||||
|
||||
return box
|
||||
|
||||
|
||||
def get_clearance(context, box, collapsed_margin=0):
|
||||
"""Return None if there is no clearance, otherwise the clearance value."""
|
||||
clearance = None
|
||||
hypothetical_position = box.position_y + collapsed_margin
|
||||
# Hypothetical position is the position of the top border edge
|
||||
for excluded_shape in context.excluded_shapes:
|
||||
if box.style['clear'] in (excluded_shape.style['float'], 'both'):
|
||||
y, h = excluded_shape.position_y, excluded_shape.margin_height()
|
||||
if hypothetical_position < y + h:
|
||||
clearance = max(
|
||||
(clearance or 0), y + h - hypothetical_position)
|
||||
return clearance
|
||||
|
||||
|
||||
def avoid_collisions(context, box, containing_block, outer=True):
|
||||
excluded_shapes = context.excluded_shapes
|
||||
position_y = box.position_y if outer else box.border_box_y()
|
||||
|
||||
box_width = box.margin_width() if outer else box.border_width()
|
||||
box_height = box.margin_height() if outer else box.border_height()
|
||||
|
||||
if box.border_height() == 0 and box.is_floated():
|
||||
return 0, 0, containing_block.width
|
||||
|
||||
while True:
|
||||
colliding_shapes = []
|
||||
for shape in excluded_shapes:
|
||||
# Assign locals to avoid slow attribute lookups.
|
||||
shape_position_y = shape.position_y
|
||||
shape_margin_height = shape.margin_height()
|
||||
if ((shape_position_y < position_y <
|
||||
shape_position_y + shape_margin_height) or
|
||||
(shape_position_y < position_y + box_height <
|
||||
shape_position_y + shape_margin_height) or
|
||||
(shape_position_y >= position_y and
|
||||
shape_position_y + shape_margin_height <=
|
||||
position_y + box_height)):
|
||||
colliding_shapes.append(shape)
|
||||
left_bounds = [
|
||||
shape.position_x + shape.margin_width()
|
||||
for shape in colliding_shapes
|
||||
if shape.style['float'] == 'left']
|
||||
right_bounds = [
|
||||
shape.position_x
|
||||
for shape in colliding_shapes
|
||||
if shape.style['float'] == 'right']
|
||||
|
||||
# Set the default maximum bounds
|
||||
max_left_bound = containing_block.content_box_x()
|
||||
max_right_bound = \
|
||||
containing_block.content_box_x() + containing_block.width
|
||||
|
||||
if not outer:
|
||||
max_left_bound += box.margin_left
|
||||
max_right_bound -= box.margin_right
|
||||
|
||||
# Set the real maximum bounds according to sibling float elements
|
||||
if left_bounds or right_bounds:
|
||||
if left_bounds:
|
||||
max_left_bound = max(max(left_bounds), max_left_bound)
|
||||
if right_bounds:
|
||||
max_right_bound = min(min(right_bounds), max_right_bound)
|
||||
|
||||
# Points 3, 7 and 8
|
||||
if box_width > max_right_bound - max_left_bound:
|
||||
# The box does not fit here
|
||||
new_positon_y = min(
|
||||
shape.position_y + shape.margin_height()
|
||||
for shape in colliding_shapes)
|
||||
if new_positon_y > position_y:
|
||||
# We can find a solution with a higher position_y
|
||||
position_y = new_positon_y
|
||||
continue
|
||||
# No solution, we must put the box here
|
||||
break
|
||||
|
||||
# See https://www.w3.org/TR/CSS21/visuren.html#floats
|
||||
# Boxes that can’t collide with floats are:
|
||||
# - floats
|
||||
# - line boxes
|
||||
# - table wrappers
|
||||
# - block-level replaced box
|
||||
# - element establishing new formatting contexts (not handled)
|
||||
assert (
|
||||
(box.style['float'] in ('right', 'left')) or
|
||||
isinstance(box, boxes.LineBox) or
|
||||
box.is_table_wrapper or
|
||||
isinstance(box, boxes.BlockReplacedBox))
|
||||
|
||||
# The x-position of the box depends on its type.
|
||||
position_x = max_left_bound
|
||||
if box.style['float'] == 'none':
|
||||
if containing_block.style['direction'] == 'rtl':
|
||||
if isinstance(box, boxes.LineBox):
|
||||
# The position of the line is the position of the cursor, at
|
||||
# the right bound.
|
||||
position_x = max_right_bound
|
||||
elif box.is_table_wrapper:
|
||||
# The position of the right border of the table is at the right
|
||||
# bound.
|
||||
position_x = max_right_bound - box_width
|
||||
else:
|
||||
# The position of the right border of the replaced box is at
|
||||
# the right bound.
|
||||
assert isinstance(box, boxes.BlockReplacedBox)
|
||||
position_x = max_right_bound - box_width
|
||||
|
||||
available_width = max_right_bound - max_left_bound
|
||||
|
||||
if not outer:
|
||||
position_x -= box.margin_left
|
||||
position_y -= box.margin_top
|
||||
|
||||
return position_x, position_y, available_width
|
1210
venv/Lib/site-packages/weasyprint/layout/inline.py
Normal file
1210
venv/Lib/site-packages/weasyprint/layout/inline.py
Normal file
File diff suppressed because it is too large
Load Diff
81
venv/Lib/site-packages/weasyprint/layout/leader.py
Normal file
81
venv/Lib/site-packages/weasyprint/layout/leader.py
Normal file
@@ -0,0 +1,81 @@
|
||||
"""
|
||||
weasyprint.layout.leader
|
||||
------------------------
|
||||
|
||||
Leader management.
|
||||
|
||||
"""
|
||||
|
||||
from ..formatting_structure import boxes
|
||||
|
||||
|
||||
def leader_index(box):
|
||||
"""Get the index of the first leader box in ``box``."""
|
||||
for i, child in enumerate(box.children):
|
||||
if child.is_leader:
|
||||
return (i, None), child
|
||||
if isinstance(child, boxes.ParentBox):
|
||||
child_leader_index, child_leader = leader_index(child)
|
||||
if child_leader_index is not None:
|
||||
return (i, child_leader_index), child_leader
|
||||
return None, None
|
||||
|
||||
|
||||
def handle_leader(context, line, containing_block):
|
||||
"""Find a leader box in ``line`` and handle its text and its position."""
|
||||
index, leader_box = leader_index(line)
|
||||
extra_width = 0
|
||||
if index is not None and leader_box.children:
|
||||
text_box, = leader_box.children
|
||||
|
||||
# Abort if the leader text has no width
|
||||
if text_box.width <= 0:
|
||||
return
|
||||
|
||||
# Extra width is the additional width taken by the leader box
|
||||
extra_width = containing_block.width - sum(
|
||||
child.width for child in line.children
|
||||
if child.is_in_normal_flow())
|
||||
|
||||
# Take care of excluded shapes
|
||||
for shape in context.excluded_shapes:
|
||||
if shape.position_y + shape.height > line.position_y:
|
||||
extra_width -= shape.width
|
||||
|
||||
# Available width is the width available for the leader box
|
||||
available_width = extra_width + text_box.width
|
||||
line.width = containing_block.width
|
||||
|
||||
# Add text boxes into the leader box
|
||||
number_of_leaders = int(line.width // text_box.width)
|
||||
position_x = line.position_x + line.width
|
||||
children = []
|
||||
for i in range(number_of_leaders):
|
||||
position_x -= text_box.width
|
||||
if position_x < leader_box.position_x:
|
||||
# Don’t add leaders behind the text on the left
|
||||
continue
|
||||
elif (position_x + text_box.width >
|
||||
leader_box.position_x + available_width):
|
||||
# Don’t add leaders behind the text on the right
|
||||
continue
|
||||
text_box = text_box.copy()
|
||||
text_box.position_x = position_x
|
||||
children.append(text_box)
|
||||
leader_box.children = tuple(children)
|
||||
|
||||
if line.style['direction'] == 'rtl':
|
||||
leader_box.translate(dx=-extra_width)
|
||||
|
||||
# Widen leader parent boxes and translate following boxes
|
||||
box = line
|
||||
while index is not None:
|
||||
for child in box.children[index[0] + 1:]:
|
||||
if child.is_in_normal_flow():
|
||||
if line.style['direction'] == 'ltr':
|
||||
child.translate(dx=extra_width)
|
||||
else:
|
||||
child.translate(dx=-extra_width)
|
||||
box = box.children[index[0]]
|
||||
box.width += extra_width
|
||||
index = index[1]
|
45
venv/Lib/site-packages/weasyprint/layout/min_max.py
Normal file
45
venv/Lib/site-packages/weasyprint/layout/min_max.py
Normal file
@@ -0,0 +1,45 @@
|
||||
"""
|
||||
weasyprint.layout.min_max
|
||||
-------------------------
|
||||
|
||||
"""
|
||||
|
||||
import functools
|
||||
|
||||
|
||||
def handle_min_max_width(function):
|
||||
"""Decorate a function setting used width, handling {min,max}-width."""
|
||||
@functools.wraps(function)
|
||||
def wrapper(box, *args):
|
||||
computed_margins = box.margin_left, box.margin_right
|
||||
result = function(box, *args)
|
||||
if box.width > box.max_width:
|
||||
box.width = box.max_width
|
||||
box.margin_left, box.margin_right = computed_margins
|
||||
result = function(box, *args)
|
||||
if box.width < box.min_width:
|
||||
box.width = box.min_width
|
||||
box.margin_left, box.margin_right = computed_margins
|
||||
result = function(box, *args)
|
||||
return result
|
||||
wrapper.without_min_max = function
|
||||
return wrapper
|
||||
|
||||
|
||||
def handle_min_max_height(function):
|
||||
"""Decorate a function setting used height, handling {min,max}-height."""
|
||||
@functools.wraps(function)
|
||||
def wrapper(box, *args):
|
||||
computed_margins = box.margin_top, box.margin_bottom
|
||||
result = function(box, *args)
|
||||
if box.height > box.max_height:
|
||||
box.height = box.max_height
|
||||
box.margin_top, box.margin_bottom = computed_margins
|
||||
result = function(box, *args)
|
||||
if box.height < box.min_height:
|
||||
box.height = box.min_height
|
||||
box.margin_top, box.margin_bottom = computed_margins
|
||||
result = function(box, *args)
|
||||
return result
|
||||
wrapper.without_min_max = function
|
||||
return wrapper
|
867
venv/Lib/site-packages/weasyprint/layout/page.py
Normal file
867
venv/Lib/site-packages/weasyprint/layout/page.py
Normal file
@@ -0,0 +1,867 @@
|
||||
"""
|
||||
weasyprint.layout.pages
|
||||
-----------------------
|
||||
|
||||
Layout for pages and CSS3 margin boxes.
|
||||
|
||||
"""
|
||||
|
||||
import copy
|
||||
|
||||
from ..css import PageType, computed_from_cascaded
|
||||
from ..formatting_structure import boxes, build
|
||||
from ..logger import PROGRESS_LOGGER
|
||||
from .absolute import absolute_box_layout, absolute_layout
|
||||
from .block import block_container_layout, block_level_layout
|
||||
from .float import float_layout
|
||||
from .min_max import handle_min_max_height, handle_min_max_width
|
||||
from .percent import resolve_percentages
|
||||
from .preferred import max_content_width, min_content_width
|
||||
|
||||
|
||||
class OrientedBox:
|
||||
@property
|
||||
def sugar(self):
|
||||
return self.padding_plus_border + self.margin_a + self.margin_b
|
||||
|
||||
@property
|
||||
def outer(self):
|
||||
return self.sugar + self.inner
|
||||
|
||||
@property
|
||||
def outer_min_content_size(self):
|
||||
return self.sugar + (
|
||||
self.min_content_size if self.inner == 'auto' else self.inner)
|
||||
|
||||
@property
|
||||
def outer_max_content_size(self):
|
||||
return self.sugar + (
|
||||
self.max_content_size if self.inner == 'auto' else self.inner)
|
||||
|
||||
def shrink_to_fit(self, available):
|
||||
self.inner = min(
|
||||
max(self.min_content_size, available), self.max_content_size)
|
||||
|
||||
|
||||
class VerticalBox(OrientedBox):
|
||||
def __init__(self, context, box):
|
||||
self.context = context
|
||||
self.box = box
|
||||
# Inner dimension: that of the content area, as opposed to the
|
||||
# outer dimension: that of the margin area.
|
||||
self.inner = box.height
|
||||
self.margin_a = box.margin_top
|
||||
self.margin_b = box.margin_bottom
|
||||
self.padding_plus_border = (
|
||||
box.padding_top + box.padding_bottom +
|
||||
box.border_top_width + box.border_bottom_width)
|
||||
|
||||
def restore_box_attributes(self):
|
||||
box = self.box
|
||||
box.height = self.inner
|
||||
box.margin_top = self.margin_a
|
||||
box.margin_bottom = self.margin_b
|
||||
|
||||
# TODO: Define what are the min-content and max-content heights
|
||||
@property
|
||||
def min_content_size(self):
|
||||
return 0
|
||||
|
||||
@property
|
||||
def max_content_size(self):
|
||||
return 1e6
|
||||
|
||||
|
||||
class HorizontalBox(OrientedBox):
|
||||
def __init__(self, context, box):
|
||||
self.context = context
|
||||
self.box = box
|
||||
self.inner = box.width
|
||||
self.margin_a = box.margin_left
|
||||
self.margin_b = box.margin_right
|
||||
self.padding_plus_border = (
|
||||
box.padding_left + box.padding_right +
|
||||
box.border_left_width + box.border_right_width)
|
||||
self._min_content_size = None
|
||||
self._max_content_size = None
|
||||
|
||||
def restore_box_attributes(self):
|
||||
box = self.box
|
||||
box.width = self.inner
|
||||
box.margin_left = self.margin_a
|
||||
box.margin_right = self.margin_b
|
||||
|
||||
@property
|
||||
def min_content_size(self):
|
||||
if self._min_content_size is None:
|
||||
self._min_content_size = min_content_width(
|
||||
self.context, self.box, outer=False)
|
||||
return self._min_content_size
|
||||
|
||||
@property
|
||||
def max_content_size(self):
|
||||
if self._max_content_size is None:
|
||||
self._max_content_size = max_content_width(
|
||||
self.context, self.box, outer=False)
|
||||
return self._max_content_size
|
||||
|
||||
|
||||
def compute_fixed_dimension(context, box, outer, vertical, top_or_left):
|
||||
"""
|
||||
Compute and set a margin box fixed dimension on ``box``, as described in:
|
||||
http://dev.w3.org/csswg/css3-page/#margin-constraints
|
||||
|
||||
:param box:
|
||||
The margin box to work on
|
||||
:param outer:
|
||||
The target outer dimension (value of a page margin)
|
||||
:param vertical:
|
||||
True to set height, margin-top and margin-bottom; False for width,
|
||||
margin-left and margin-right
|
||||
:param top_or_left:
|
||||
True if the margin box in if the top half (for vertical==True) or
|
||||
left half (for vertical==False) of the page.
|
||||
This determines which margin should be 'auto' if the values are
|
||||
over-constrained. (Rule 3 of the algorithm.)
|
||||
"""
|
||||
box = (VerticalBox if vertical else HorizontalBox)(context, box)
|
||||
|
||||
# Rule 2
|
||||
total = box.padding_plus_border + sum(
|
||||
value for value in [box.margin_a, box.margin_b, box.inner]
|
||||
if value != 'auto')
|
||||
if total > outer:
|
||||
if box.margin_a == 'auto':
|
||||
box.margin_a = 0
|
||||
if box.margin_b == 'auto':
|
||||
box.margin_b = 0
|
||||
if box.inner == 'auto':
|
||||
# XXX this is not in the spec, but without it box.inner
|
||||
# would end up with a negative value.
|
||||
# Instead, this will trigger rule 3 below.
|
||||
# http://lists.w3.org/Archives/Public/www-style/2012Jul/0006.html
|
||||
box.inner = 0
|
||||
# Rule 3
|
||||
if 'auto' not in [box.margin_a, box.margin_b, box.inner]:
|
||||
# Over-constrained
|
||||
if top_or_left:
|
||||
box.margin_a = 'auto'
|
||||
else:
|
||||
box.margin_b = 'auto'
|
||||
# Rule 4
|
||||
if [box.margin_a, box.margin_b, box.inner].count('auto') == 1:
|
||||
if box.inner == 'auto':
|
||||
box.inner = (outer - box.padding_plus_border -
|
||||
box.margin_a - box.margin_b)
|
||||
elif box.margin_a == 'auto':
|
||||
box.margin_a = (outer - box.padding_plus_border -
|
||||
box.margin_b - box.inner)
|
||||
elif box.margin_b == 'auto':
|
||||
box.margin_b = (outer - box.padding_plus_border -
|
||||
box.margin_a - box.inner)
|
||||
# Rule 5
|
||||
if box.inner == 'auto':
|
||||
if box.margin_a == 'auto':
|
||||
box.margin_a = 0
|
||||
if box.margin_b == 'auto':
|
||||
box.margin_b = 0
|
||||
box.inner = (outer - box.padding_plus_border -
|
||||
box.margin_a - box.margin_b)
|
||||
# Rule 6
|
||||
if box.margin_a == 'auto' and box.margin_b == 'auto':
|
||||
box.margin_a = box.margin_b = (
|
||||
outer - box.padding_plus_border - box.inner) / 2
|
||||
|
||||
assert 'auto' not in [box.margin_a, box.margin_b, box.inner]
|
||||
|
||||
box.restore_box_attributes()
|
||||
|
||||
|
||||
def compute_variable_dimension(context, side_boxes, vertical, outer_sum):
|
||||
"""
|
||||
Compute and set a margin box fixed dimension on ``box``, as described in:
|
||||
http://dev.w3.org/csswg/css3-page/#margin-dimension
|
||||
|
||||
:param side_boxes: Three boxes on a same side (as opposed to a corner.)
|
||||
A list of:
|
||||
- A @*-left or @*-top margin box
|
||||
- A @*-center or @*-middle margin box
|
||||
- A @*-right or @*-bottom margin box
|
||||
:param vertical:
|
||||
True to set height, margin-top and margin-bottom; False for width,
|
||||
margin-left and margin-right
|
||||
:param outer_sum:
|
||||
The target total outer dimension (max box width or height)
|
||||
|
||||
"""
|
||||
box_class = VerticalBox if vertical else HorizontalBox
|
||||
side_boxes = [box_class(context, box) for box in side_boxes]
|
||||
box_a, box_b, box_c = side_boxes
|
||||
|
||||
for box in side_boxes:
|
||||
if box.margin_a == 'auto':
|
||||
box.margin_a = 0
|
||||
if box.margin_b == 'auto':
|
||||
box.margin_b = 0
|
||||
|
||||
if box_b.box.is_generated:
|
||||
if box_b.inner == 'auto':
|
||||
ac_max_content_size = 2 * max(
|
||||
box_a.outer_max_content_size, box_c.outer_max_content_size)
|
||||
if outer_sum >= (
|
||||
box_b.outer_max_content_size + ac_max_content_size):
|
||||
box_b.inner = box_b.max_content_size
|
||||
else:
|
||||
ac_min_content_size = 2 * max(
|
||||
box_a.outer_min_content_size,
|
||||
box_c.outer_min_content_size)
|
||||
box_b.inner = box_b.min_content_size
|
||||
available = outer_sum - box_b.outer - ac_min_content_size
|
||||
if available > 0:
|
||||
weight_ac = ac_max_content_size - ac_min_content_size
|
||||
weight_b = (
|
||||
box_b.max_content_size - box_b.min_content_size)
|
||||
weight_sum = weight_ac + weight_b
|
||||
# By definition of max_content_size and min_content_size,
|
||||
# weights can not be negative. weight_sum == 0 implies that
|
||||
# max_content_size == min_content_size for each box, in
|
||||
# which case the sum can not be both <= and > outer_sum
|
||||
# Therefore, one of the last two 'if' statements would not
|
||||
# have lead us here.
|
||||
assert weight_sum > 0
|
||||
box_b.inner += available * weight_b / weight_sum
|
||||
if box_a.inner == 'auto':
|
||||
box_a.shrink_to_fit((outer_sum - box_b.outer) / 2 - box_a.sugar)
|
||||
if box_c.inner == 'auto':
|
||||
box_c.shrink_to_fit((outer_sum - box_b.outer) / 2 - box_c.sugar)
|
||||
else:
|
||||
# Non-generated boxes get zero for every box-model property
|
||||
assert box_b.inner == 0
|
||||
if box_a.inner == box_c.inner == 'auto':
|
||||
if outer_sum >= (
|
||||
box_a.outer_max_content_size +
|
||||
box_c.outer_max_content_size):
|
||||
box_a.inner = box_a.max_content_size
|
||||
box_c.inner = box_c.max_content_size
|
||||
else:
|
||||
box_a.inner = box_a.min_content_size
|
||||
box_c.inner = box_c.min_content_size
|
||||
available = outer_sum - box_a.outer - box_c.outer
|
||||
if available > 0:
|
||||
weight_a = (
|
||||
box_a.max_content_size - box_a.min_content_size)
|
||||
weight_c = (
|
||||
box_c.max_content_size - box_c.min_content_size)
|
||||
weight_sum = weight_a + weight_c
|
||||
# By definition of max_content_size and min_content_size,
|
||||
# weights can not be negative. weight_sum == 0 implies that
|
||||
# max_content_size == min_content_size for each box, in
|
||||
# which case the sum can not be both <= and > outer_sum
|
||||
# Therefore, one of the last two 'if' statements would not
|
||||
# have lead us here.
|
||||
assert weight_sum > 0
|
||||
box_a.inner += available * weight_a / weight_sum
|
||||
box_c.inner += available * weight_c / weight_sum
|
||||
elif box_a.inner == 'auto':
|
||||
box_a.shrink_to_fit(outer_sum - box_c.outer - box_a.sugar)
|
||||
elif box_c.inner == 'auto':
|
||||
box_c.shrink_to_fit(outer_sum - box_a.outer - box_c.sugar)
|
||||
|
||||
# And, we’re done!
|
||||
assert 'auto' not in [box.inner for box in side_boxes]
|
||||
# Set the actual attributes back.
|
||||
for box in side_boxes:
|
||||
box.restore_box_attributes()
|
||||
|
||||
|
||||
def _standardize_page_based_counters(style, pseudo_type):
|
||||
"""Drop 'pages' counter from style in @page and @margin context.
|
||||
|
||||
Ensure `counter-increment: page` for @page context if not otherwise
|
||||
manipulated by the style.
|
||||
|
||||
"""
|
||||
page_counter_touched = False
|
||||
for propname in ('counter_set', 'counter_reset', 'counter_increment'):
|
||||
if style[propname] == 'auto':
|
||||
style[propname] = ()
|
||||
continue
|
||||
justified_values = []
|
||||
for name, value in style[propname]:
|
||||
if name == 'page':
|
||||
page_counter_touched = True
|
||||
if name != 'pages':
|
||||
justified_values.append((name, value))
|
||||
style[propname] = tuple(justified_values)
|
||||
|
||||
if pseudo_type is None and not page_counter_touched:
|
||||
style['counter_increment'] = (
|
||||
('page', 1),) + style['counter_increment']
|
||||
|
||||
|
||||
def make_margin_boxes(context, page, state):
|
||||
"""Yield laid-out margin boxes for this page.
|
||||
|
||||
``state`` is the actual, up-to-date page-state from
|
||||
``context.page_maker[context.current_page]``.
|
||||
|
||||
"""
|
||||
# This is a closure only to make calls shorter
|
||||
def make_box(at_keyword, containing_block):
|
||||
"""Return a margin box with resolved percentages.
|
||||
|
||||
The margin box may still have 'auto' values.
|
||||
|
||||
Return ``None`` if this margin box should not be generated.
|
||||
|
||||
:param at_keyword: which margin box to return, eg. '@top-left'
|
||||
:param containing_block: as expected by :func:`resolve_percentages`.
|
||||
|
||||
"""
|
||||
style = context.style_for(page.page_type, at_keyword)
|
||||
if style is None:
|
||||
# doesn't affect counters
|
||||
style = computed_from_cascaded(
|
||||
element=None, cascaded={}, parent_style=page.style)
|
||||
_standardize_page_based_counters(style, at_keyword)
|
||||
box = boxes.MarginBox(at_keyword, style)
|
||||
# Empty boxes should not be generated, but they may be needed for
|
||||
# the layout of their neighbors.
|
||||
# TODO: should be the computed value.
|
||||
box.is_generated = style['content'] not in (
|
||||
'normal', 'inhibit', 'none')
|
||||
# TODO: get actual counter values at the time of the last page break
|
||||
if box.is_generated:
|
||||
# @margins mustn't manipulate page-context counters
|
||||
margin_state = copy.deepcopy(state)
|
||||
quote_depth, counter_values, counter_scopes = margin_state
|
||||
# TODO: check this, probably useless
|
||||
counter_scopes.append(set())
|
||||
build.update_counters(margin_state, box.style)
|
||||
box.children = build.content_to_boxes(
|
||||
box.style, box, quote_depth, counter_values,
|
||||
context.get_image_from_uri, context.target_collector,
|
||||
context.counter_style, context, page)
|
||||
build.process_whitespace(box)
|
||||
build.process_text_transform(box)
|
||||
box = build.create_anonymous_boxes(box)
|
||||
resolve_percentages(box, containing_block)
|
||||
if not box.is_generated:
|
||||
box.width = box.height = 0
|
||||
for side in ('top', 'right', 'bottom', 'left'):
|
||||
box._reset_spacing(side)
|
||||
return box
|
||||
|
||||
margin_top = page.margin_top
|
||||
margin_bottom = page.margin_bottom
|
||||
margin_left = page.margin_left
|
||||
margin_right = page.margin_right
|
||||
max_box_width = page.border_width()
|
||||
max_box_height = page.border_height()
|
||||
|
||||
# bottom right corner of the border box
|
||||
page_end_x = margin_left + max_box_width
|
||||
page_end_y = margin_top + max_box_height
|
||||
|
||||
# Margin box dimensions, described in
|
||||
# http://dev.w3.org/csswg/css3-page/#margin-box-dimensions
|
||||
generated_boxes = []
|
||||
|
||||
for prefix, vertical, containing_block, position_x, position_y in [
|
||||
('top', False, (max_box_width, margin_top),
|
||||
margin_left, 0),
|
||||
('bottom', False, (max_box_width, margin_bottom),
|
||||
margin_left, page_end_y),
|
||||
('left', True, (margin_left, max_box_height),
|
||||
0, margin_top),
|
||||
('right', True, (margin_right, max_box_height),
|
||||
page_end_x, margin_top),
|
||||
]:
|
||||
if vertical:
|
||||
suffixes = ['top', 'middle', 'bottom']
|
||||
fixed_outer, variable_outer = containing_block
|
||||
else:
|
||||
suffixes = ['left', 'center', 'right']
|
||||
variable_outer, fixed_outer = containing_block
|
||||
side_boxes = [
|
||||
make_box(f'@{prefix}-{suffix}', containing_block)
|
||||
for suffix in suffixes]
|
||||
if not any(box.is_generated for box in side_boxes):
|
||||
continue
|
||||
# We need the three boxes together for the variable dimension:
|
||||
compute_variable_dimension(
|
||||
context, side_boxes, vertical, variable_outer)
|
||||
for box, offset in zip(side_boxes, [0, 0.5, 1]):
|
||||
if not box.is_generated:
|
||||
continue
|
||||
box.position_x = position_x
|
||||
box.position_y = position_y
|
||||
if vertical:
|
||||
box.position_y += offset * (
|
||||
variable_outer - box.margin_height())
|
||||
else:
|
||||
box.position_x += offset * (
|
||||
variable_outer - box.margin_width())
|
||||
compute_fixed_dimension(
|
||||
context, box, fixed_outer, not vertical,
|
||||
prefix in ['top', 'left'])
|
||||
generated_boxes.append(box)
|
||||
|
||||
# Corner boxes
|
||||
|
||||
for at_keyword, cb_width, cb_height, position_x, position_y in [
|
||||
('@top-left-corner', margin_left, margin_top, 0, 0),
|
||||
('@top-right-corner', margin_right, margin_top, page_end_x, 0),
|
||||
('@bottom-left-corner', margin_left, margin_bottom, 0, page_end_y),
|
||||
('@bottom-right-corner', margin_right, margin_bottom,
|
||||
page_end_x, page_end_y),
|
||||
]:
|
||||
box = make_box(at_keyword, (cb_width, cb_height))
|
||||
if not box.is_generated:
|
||||
continue
|
||||
box.position_x = position_x
|
||||
box.position_y = position_y
|
||||
compute_fixed_dimension(
|
||||
context, box, cb_height, True, 'top' in at_keyword)
|
||||
compute_fixed_dimension(
|
||||
context, box, cb_width, False, 'left' in at_keyword)
|
||||
generated_boxes.append(box)
|
||||
|
||||
for box in generated_boxes:
|
||||
yield margin_box_content_layout(context, page, box)
|
||||
|
||||
|
||||
def margin_box_content_layout(context, page, box):
|
||||
"""Layout a margin box’s content once the box has dimensions."""
|
||||
positioned_boxes = []
|
||||
box, resume_at, next_page, _, _ = block_container_layout(
|
||||
context, box, bottom_space=-float('inf'), skip_stack=None,
|
||||
page_is_empty=True, absolute_boxes=positioned_boxes,
|
||||
fixed_boxes=positioned_boxes, adjoining_margins=None, discard=False)
|
||||
assert resume_at is None
|
||||
for absolute_box in positioned_boxes:
|
||||
absolute_layout(
|
||||
context, absolute_box, box, positioned_boxes, bottom_space=0,
|
||||
skip_stack=None)
|
||||
|
||||
vertical_align = box.style['vertical_align']
|
||||
# Every other value is read as 'top', ie. no change.
|
||||
if vertical_align in ('middle', 'bottom') and box.children:
|
||||
first_child = box.children[0]
|
||||
last_child = box.children[-1]
|
||||
top = first_child.position_y
|
||||
# Not always exact because floating point errors
|
||||
# assert top == box.content_box_y()
|
||||
bottom = last_child.position_y + last_child.margin_height()
|
||||
content_height = bottom - top
|
||||
offset = box.height - content_height
|
||||
if vertical_align == 'middle':
|
||||
offset /= 2
|
||||
for child in box.children:
|
||||
child.translate(0, offset)
|
||||
return box
|
||||
|
||||
|
||||
def page_width_or_height(box, containing_block_size):
|
||||
"""Take a :class:`OrientedBox` object and set either width, margin-left
|
||||
and margin-right; or height, margin-top and margin-bottom.
|
||||
|
||||
"The width and horizontal margins of the page box are then calculated
|
||||
exactly as for a non-replaced block element in normal flow. The height
|
||||
and vertical margins of the page box are calculated analogously (instead
|
||||
of using the block height formulas). In both cases if the values are
|
||||
over-constrained, instead of ignoring any margins, the containing block
|
||||
is resized to coincide with the margin edges of the page box."
|
||||
|
||||
http://dev.w3.org/csswg/css3-page/#page-box-page-rule
|
||||
http://www.w3.org/TR/CSS21/visudet.html#blockwidth
|
||||
|
||||
"""
|
||||
remaining = containing_block_size - box.padding_plus_border
|
||||
if box.inner == 'auto':
|
||||
if box.margin_a == 'auto':
|
||||
box.margin_a = 0
|
||||
if box.margin_b == 'auto':
|
||||
box.margin_b = 0
|
||||
box.inner = remaining - box.margin_a - box.margin_b
|
||||
elif box.margin_a == box.margin_b == 'auto':
|
||||
box.margin_a = box.margin_b = (remaining - box.inner) / 2
|
||||
elif box.margin_a == 'auto':
|
||||
box.margin_a = remaining - box.inner - box.margin_b
|
||||
elif box.margin_b == 'auto':
|
||||
box.margin_b = remaining - box.inner - box.margin_a
|
||||
box.restore_box_attributes()
|
||||
|
||||
|
||||
@handle_min_max_width
|
||||
def page_width(box, context, containing_block_width):
|
||||
page_width_or_height(HorizontalBox(context, box), containing_block_width)
|
||||
|
||||
|
||||
@handle_min_max_height
|
||||
def page_height(box, context, containing_block_height):
|
||||
page_width_or_height(VerticalBox(context, box), containing_block_height)
|
||||
|
||||
|
||||
def make_page(context, root_box, page_type, resume_at, page_number,
|
||||
page_state):
|
||||
"""Take just enough content from the beginning to fill one page.
|
||||
|
||||
Return ``(page, finished)``. ``page`` is a laid out PageBox object
|
||||
and ``resume_at`` indicates where in the document to start the next page,
|
||||
or is ``None`` if this was the last page.
|
||||
|
||||
:param page_number: integer, start at 1 for the first page
|
||||
:param resume_at: as returned by ``make_page()`` for the previous page,
|
||||
or ``None`` for the first page.
|
||||
|
||||
"""
|
||||
style = context.style_for(page_type)
|
||||
|
||||
# Propagated from the root or <body>.
|
||||
style['overflow'] = root_box.viewport_overflow
|
||||
page = boxes.PageBox(page_type, style)
|
||||
|
||||
device_size = page.style['size']
|
||||
|
||||
resolve_percentages(page, device_size)
|
||||
|
||||
page.position_x = 0
|
||||
page.position_y = 0
|
||||
cb_width, cb_height = device_size
|
||||
page_width(page, context, cb_width)
|
||||
page_height(page, context, cb_height)
|
||||
|
||||
root_box.position_x = page.content_box_x()
|
||||
root_box.position_y = page.content_box_y()
|
||||
context.page_bottom = root_box.position_y + page.height
|
||||
initial_containing_block = page
|
||||
|
||||
footnote_area_style = context.style_for(page_type, '@footnote')
|
||||
footnote_area = boxes.FootnoteAreaBox(page, footnote_area_style)
|
||||
resolve_percentages(footnote_area, page)
|
||||
footnote_area.position_x = page.content_box_x()
|
||||
footnote_area.position_y = context.page_bottom
|
||||
|
||||
if page_type.blank:
|
||||
previous_resume_at = resume_at
|
||||
root_box = root_box.copy_with_children([])
|
||||
|
||||
# TODO: handle cases where the root element is something else.
|
||||
# See http://www.w3.org/TR/CSS21/visuren.html#dis-pos-flo
|
||||
assert isinstance(root_box, (boxes.BlockBox, boxes.FlexContainerBox))
|
||||
context.create_block_formatting_context()
|
||||
context.current_page = page_number
|
||||
context.current_page_footnotes = context.reported_footnotes.copy()
|
||||
context.current_footnote_area = footnote_area
|
||||
|
||||
if context.reported_footnotes:
|
||||
footnote_area.children = tuple(context.reported_footnotes)
|
||||
context.reported_footnotes = []
|
||||
reported_footnote_area = build.create_anonymous_boxes(
|
||||
footnote_area.deepcopy())
|
||||
reported_footnote_area, _, _, _, _ = block_level_layout(
|
||||
context, reported_footnote_area, -float('inf'), None,
|
||||
footnote_area.page, True, [], [], [], False)
|
||||
footnote_area.height = reported_footnote_area.height
|
||||
context.page_bottom -= reported_footnote_area.margin_height()
|
||||
|
||||
page_is_empty = True
|
||||
adjoining_margins = []
|
||||
positioned_boxes = [] # Mixed absolute and fixed
|
||||
out_of_flow_boxes = []
|
||||
broken_out_of_flow = []
|
||||
for box, containing_block, skip_stack in context.broken_out_of_flow:
|
||||
box.position_y = 0
|
||||
if box.is_floated():
|
||||
out_of_flow_box, out_of_flow_resume_at = float_layout(
|
||||
context, box, containing_block, positioned_boxes,
|
||||
positioned_boxes, 0, skip_stack)
|
||||
else:
|
||||
assert box.is_absolutely_positioned()
|
||||
out_of_flow_box, out_of_flow_resume_at = absolute_box_layout(
|
||||
context, box, containing_block, positioned_boxes, 0,
|
||||
skip_stack)
|
||||
out_of_flow_boxes.append(out_of_flow_box)
|
||||
if out_of_flow_resume_at:
|
||||
broken_out_of_flow.append(
|
||||
(box, containing_block, out_of_flow_resume_at))
|
||||
context.broken_out_of_flow = broken_out_of_flow
|
||||
root_box, resume_at, next_page, _, _ = block_level_layout(
|
||||
context, root_box, 0, resume_at, initial_containing_block,
|
||||
page_is_empty, positioned_boxes, positioned_boxes, adjoining_margins,
|
||||
discard=False)
|
||||
assert root_box
|
||||
root_box.children = tuple(out_of_flow_boxes) + root_box.children
|
||||
|
||||
page.fixed_boxes = [
|
||||
placeholder._box for placeholder in positioned_boxes
|
||||
if placeholder._box.style['position'] == 'fixed']
|
||||
for absolute_box in positioned_boxes:
|
||||
absolute_layout(
|
||||
context, absolute_box, page, positioned_boxes, bottom_space=0,
|
||||
skip_stack=None)
|
||||
|
||||
footnote_area = build.create_anonymous_boxes(footnote_area.deepcopy())
|
||||
footnote_area, _, _, _, _ = block_level_layout(
|
||||
context, footnote_area, -float('inf'), None, footnote_area.page,
|
||||
True, [], [], [], False)
|
||||
footnote_area.translate(dy=-footnote_area.margin_height())
|
||||
|
||||
context.finish_block_formatting_context(root_box)
|
||||
|
||||
page.children = [root_box, footnote_area]
|
||||
descendants = page.descendants()
|
||||
|
||||
# Update page counter values
|
||||
_standardize_page_based_counters(style, None)
|
||||
build.update_counters(page_state, style)
|
||||
page_counter_values = page_state[1]
|
||||
# page_counter_values will be cached in the page_maker
|
||||
|
||||
target_collector = context.target_collector
|
||||
page_maker = context.page_maker
|
||||
|
||||
# remake_state tells the make_all_pages-loop in layout_document()
|
||||
# whether and what to re-make.
|
||||
remake_state = page_maker[page_number - 1][-1]
|
||||
|
||||
# Evaluate and cache page values only once (for the first LineBox)
|
||||
# otherwise we suffer endless loops when the target/pseudo-element
|
||||
# spans across multiple pages
|
||||
cached_anchors = []
|
||||
cached_lookups = []
|
||||
for (_, _, _, _, x_remake_state) in page_maker[:page_number - 1]:
|
||||
cached_anchors.extend(x_remake_state.get('anchors', []))
|
||||
cached_lookups.extend(x_remake_state.get('content_lookups', []))
|
||||
|
||||
for child in descendants:
|
||||
# Cache target's page counters
|
||||
anchor = child.style['anchor']
|
||||
if anchor and anchor not in cached_anchors:
|
||||
remake_state['anchors'].append(anchor)
|
||||
cached_anchors.append(anchor)
|
||||
# Re-make of affected targeting boxes is inclusive
|
||||
target_collector.cache_target_page_counters(
|
||||
anchor, page_counter_values, page_number - 1, page_maker)
|
||||
|
||||
# string-set and bookmark-labels don't create boxes, only `content`
|
||||
# requires another call to make_page. There is maximum one 'content'
|
||||
# item per box.
|
||||
# TODO: remove attribute or set a default value in Box class
|
||||
if hasattr(child, 'missing_link'):
|
||||
# A CounterLookupItem exists for the css-token 'content'
|
||||
counter_lookup = target_collector.counter_lookup_items.get(
|
||||
(child.missing_link, 'content'))
|
||||
else:
|
||||
counter_lookup = None
|
||||
|
||||
# Resolve missing (page based) counters
|
||||
if counter_lookup is not None:
|
||||
call_parse_again = False
|
||||
|
||||
# Prevent endless loops
|
||||
counter_lookup_id = id(counter_lookup)
|
||||
refresh_missing_counters = counter_lookup_id not in cached_lookups
|
||||
if refresh_missing_counters:
|
||||
remake_state['content_lookups'].append(counter_lookup_id)
|
||||
cached_lookups.append(counter_lookup_id)
|
||||
counter_lookup.page_maker_index = page_number - 1
|
||||
|
||||
# Step 1: page based back-references
|
||||
# Marked as pending by target_collector.cache_target_page_counters
|
||||
if counter_lookup.pending:
|
||||
if (page_counter_values !=
|
||||
counter_lookup.cached_page_counter_values):
|
||||
counter_lookup.cached_page_counter_values = copy.deepcopy(
|
||||
page_counter_values)
|
||||
counter_lookup.pending = False
|
||||
call_parse_again = True
|
||||
|
||||
# Step 2: local counters
|
||||
# If the box mixed-in page counters changed, update the content
|
||||
# and cache the new values.
|
||||
missing_counters = counter_lookup.missing_counters
|
||||
if missing_counters:
|
||||
if 'pages' in missing_counters:
|
||||
remake_state['pages_wanted'] = True
|
||||
if refresh_missing_counters and page_counter_values != \
|
||||
counter_lookup.cached_page_counter_values:
|
||||
counter_lookup.cached_page_counter_values = \
|
||||
copy.deepcopy(page_counter_values)
|
||||
for counter_name in missing_counters:
|
||||
counter_value = page_counter_values.get(
|
||||
counter_name, None)
|
||||
if counter_value is not None:
|
||||
call_parse_again = True
|
||||
# no need to loop them all
|
||||
break
|
||||
|
||||
# Step 3: targeted counters
|
||||
target_missing = counter_lookup.missing_target_counters
|
||||
for anchor_name, missed_counters in target_missing.items():
|
||||
if 'pages' not in missed_counters:
|
||||
continue
|
||||
# Adjust 'pages_wanted'
|
||||
item = target_collector.target_lookup_items.get(
|
||||
anchor_name, None)
|
||||
page_maker_index = item.page_maker_index
|
||||
if page_maker_index >= 0 and anchor_name in cached_anchors:
|
||||
page_maker[page_maker_index][-1]['pages_wanted'] = True
|
||||
# 'content_changed' is triggered in
|
||||
# targets.cache_target_page_counters()
|
||||
|
||||
if call_parse_again:
|
||||
remake_state['content_changed'] = True
|
||||
counter_lookup.parse_again(page_counter_values)
|
||||
|
||||
if page_type.blank:
|
||||
resume_at = previous_resume_at
|
||||
|
||||
return page, resume_at, next_page
|
||||
|
||||
|
||||
def set_page_type_computed_styles(page_type, html, style_for):
|
||||
"""Set style for page types and pseudo-types matching ``page_type``."""
|
||||
style_for.add_page_declarations(page_type)
|
||||
|
||||
# Apply style for page
|
||||
style_for.set_computed_styles(
|
||||
page_type,
|
||||
# @page inherits from the root element:
|
||||
# http://lists.w3.org/Archives/Public/www-style/2012Jan/1164.html
|
||||
root=html.etree_element, parent=html.etree_element,
|
||||
base_url=html.base_url)
|
||||
|
||||
# Apply style for page pseudo-elements (margin boxes)
|
||||
for element, pseudo_type in style_for.get_cascaded_styles():
|
||||
if pseudo_type and element == page_type:
|
||||
style_for.set_computed_styles(
|
||||
element, pseudo_type=pseudo_type,
|
||||
# The pseudo-element inherits from the element.
|
||||
root=html.etree_element, parent=element,
|
||||
base_url=html.base_url)
|
||||
|
||||
|
||||
def remake_page(index, context, root_box, html):
|
||||
"""Return one laid out page without margin boxes.
|
||||
|
||||
Start with the initial values from ``context.page_maker[index]``.
|
||||
The resulting values / initial values for the next page are stored in
|
||||
the ``page_maker``.
|
||||
|
||||
As the function's name suggests: the plan is not to make all pages
|
||||
repeatedly when a missing counter was resolved, but rather re-make the
|
||||
single page where the ``content_changed`` happened.
|
||||
|
||||
"""
|
||||
page_maker = context.page_maker
|
||||
(initial_resume_at, initial_next_page, right_page, initial_page_state,
|
||||
remake_state) = page_maker[index]
|
||||
|
||||
# PageType for current page, values for page_maker[index + 1].
|
||||
# Don't modify actual page_maker[index] values!
|
||||
# TODO: should we store (and reuse) page_type in the page_maker?
|
||||
page_state = copy.deepcopy(initial_page_state)
|
||||
next_page_name = initial_next_page['page']
|
||||
first = index == 0
|
||||
if initial_next_page['break'] in ('left', 'right'):
|
||||
next_page_side = initial_next_page['break']
|
||||
elif initial_next_page['break'] in ('recto', 'verso'):
|
||||
direction_ltr = root_box.style['direction'] == 'ltr'
|
||||
break_verso = initial_next_page['break'] == 'verso'
|
||||
next_page_side = 'right' if direction_ltr ^ break_verso else 'left'
|
||||
else:
|
||||
next_page_side = None
|
||||
blank = bool(
|
||||
(next_page_side == 'left' and right_page) or
|
||||
(next_page_side == 'right' and not right_page) or
|
||||
(context.reported_footnotes and initial_resume_at is None))
|
||||
if blank:
|
||||
next_page_name = ''
|
||||
side = 'right' if right_page else 'left'
|
||||
page_type = PageType(side, blank, first, index, name=next_page_name)
|
||||
set_page_type_computed_styles(page_type, html, context.style_for)
|
||||
|
||||
context.forced_break = (
|
||||
initial_next_page['break'] != 'any' or initial_next_page['page'])
|
||||
context.margin_clearance = False
|
||||
|
||||
# make_page wants a page_number of index + 1
|
||||
page_number = index + 1
|
||||
page, resume_at, next_page = make_page(
|
||||
context, root_box, page_type, initial_resume_at, page_number,
|
||||
page_state)
|
||||
assert next_page
|
||||
if blank:
|
||||
next_page['page'] = initial_next_page['page']
|
||||
right_page = not right_page
|
||||
|
||||
# Check whether we need to append or update the next page_maker item
|
||||
if index + 1 >= len(page_maker):
|
||||
# New page
|
||||
page_maker_next_changed = True
|
||||
else:
|
||||
# Check whether something changed
|
||||
# TODO: Find what we need to compare. Is resume_at enough?
|
||||
(next_resume_at, next_next_page, next_right_page,
|
||||
next_page_state, _) = page_maker[index + 1]
|
||||
page_maker_next_changed = (
|
||||
next_resume_at != resume_at or
|
||||
next_next_page != next_page or
|
||||
next_right_page != right_page or
|
||||
next_page_state != page_state)
|
||||
|
||||
if page_maker_next_changed:
|
||||
# Reset remake_state
|
||||
remake_state = {
|
||||
'content_changed': False,
|
||||
'pages_wanted': False,
|
||||
'anchors': [],
|
||||
'content_lookups': [],
|
||||
}
|
||||
# Setting content_changed to True ensures remake.
|
||||
# If resume_at is None (last page) it must be False to prevent endless
|
||||
# loops and list index out of range (see #794).
|
||||
remake_state['content_changed'] = resume_at is not None
|
||||
# page_state is already a deepcopy
|
||||
item = resume_at, next_page, right_page, page_state, remake_state
|
||||
if index + 1 >= len(page_maker):
|
||||
page_maker.append(item)
|
||||
else:
|
||||
page_maker[index + 1] = item
|
||||
|
||||
return page, resume_at
|
||||
|
||||
|
||||
def make_all_pages(context, root_box, html, pages):
|
||||
"""Return a list of laid out pages without margin boxes.
|
||||
|
||||
Re-make pages only if necessary.
|
||||
|
||||
"""
|
||||
i = 0
|
||||
while True:
|
||||
remake_state = context.page_maker[i][-1]
|
||||
if (len(pages) == 0 or
|
||||
remake_state['content_changed'] or
|
||||
remake_state['pages_wanted']):
|
||||
PROGRESS_LOGGER.info('Step 5 - Creating layout - Page %d', i + 1)
|
||||
# Reset remake_state
|
||||
remake_state['content_changed'] = False
|
||||
remake_state['pages_wanted'] = False
|
||||
remake_state['anchors'] = []
|
||||
remake_state['content_lookups'] = []
|
||||
page, resume_at = remake_page(i, context, root_box, html)
|
||||
yield page
|
||||
else:
|
||||
PROGRESS_LOGGER.info(
|
||||
'Step 5 - Creating layout - Page %d (up-to-date)', i + 1)
|
||||
resume_at = context.page_maker[i + 1][0]
|
||||
yield pages[i]
|
||||
|
||||
i += 1
|
||||
if resume_at is None and not context.reported_footnotes:
|
||||
# Throw away obsolete pages
|
||||
context.page_maker = context.page_maker[:i + 1]
|
||||
return
|
975
venv/Lib/site-packages/weasyprint/layout/table.py
Normal file
975
venv/Lib/site-packages/weasyprint/layout/table.py
Normal file
@@ -0,0 +1,975 @@
|
||||
"""
|
||||
weasyprint.layout.tables
|
||||
------------------------
|
||||
|
||||
Layout for tables and internal table boxes.
|
||||
|
||||
"""
|
||||
|
||||
from ..formatting_structure import boxes
|
||||
from ..logger import LOGGER
|
||||
from .percent import resolve_one_percentage, resolve_percentages
|
||||
from .preferred import max_content_width, table_and_columns_preferred_widths
|
||||
|
||||
|
||||
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 (
|
||||
block_container_layout, block_level_page_break,
|
||||
find_earlier_page_break)
|
||||
|
||||
table.remove_decoration(start=skip_stack is not None, end=False)
|
||||
|
||||
column_widths = table.column_widths
|
||||
|
||||
if table.style['border_collapse'] == 'separate':
|
||||
border_spacing_x, border_spacing_y = table.style['border_spacing']
|
||||
else:
|
||||
border_spacing_x = 0
|
||||
border_spacing_y = 0
|
||||
|
||||
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
|
||||
|
||||
if table.style['border_collapse'] == '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:
|
||||
_, 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 rows, 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 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_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)
|
||||
# The computed height is a minimum
|
||||
cell.computed_height = cell.height
|
||||
cell.height = 'auto'
|
||||
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
|
||||
if cell_skip_stack:
|
||||
cell.remove_decoration(start=True, end=False)
|
||||
if table.style['border_collapse'] == 'collapse':
|
||||
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.
|
||||
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)
|
||||
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)
|
||||
cell_resume_at = {0: None}
|
||||
else:
|
||||
cell = new_cell
|
||||
|
||||
cell.remove_decoration(
|
||||
start=cell_skip_stack is not None,
|
||||
end=cell_resume_at is not None)
|
||||
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:
|
||||
if row.style['break_inside'] in ('avoid', 'avoid-page'):
|
||||
resume_at = {index_row: {}}
|
||||
break
|
||||
|
||||
row = row.copy_with_children(new_row_children)
|
||||
|
||||
# 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:
|
||||
extra = row.baseline - cell.baseline
|
||||
if cell.baseline != row.baseline and extra:
|
||||
add_top_padding(cell, extra)
|
||||
|
||||
# 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.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:
|
||||
values, = list(resume_at.values())
|
||||
if len(row.children) == len(values):
|
||||
for cell_resume_at in values.values():
|
||||
if cell_resume_at != {0: None}:
|
||||
break_cell = True
|
||||
break
|
||||
else:
|
||||
# No cell was displayed, give up row
|
||||
next_position_y = float('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.
|
||||
if not page_is_empty and (
|
||||
next_position_y > context.page_bottom - bottom_space):
|
||||
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
|
||||
|
||||
new_group_children.append(row)
|
||||
position_y = next_position_y
|
||||
page_is_empty = False
|
||||
skip_stack = None
|
||||
|
||||
if break_cell and table.style['border_collapse'] == 'collapse':
|
||||
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'
|
||||
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, 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:
|
||||
# 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 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, 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 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)
|
||||
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 for 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 = -float('inf')
|
||||
|
||||
if table.children and table.children[0].is_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 table.children and table.children[-1].is_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 = (
|
||||
group.style['break_inside'] in ('avoid', 'avoid-page'))
|
||||
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):
|
||||
"""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.remove_decoration(
|
||||
start=skip_stack is not None, end=resume_at is not None)
|
||||
if table.style['border_collapse'] == '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 for 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
|
||||
|
||||
if resume_at and not page_is_empty and (
|
||||
table.style['break_inside'] in ('avoid', 'avoid-page')):
|
||||
table = None
|
||||
resume_at = None
|
||||
adjoining_margins = []
|
||||
collapsing_through = False
|
||||
return table, resume_at, next_page, adjoining_margins, collapsing_through
|
||||
|
||||
|
||||
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)
|
||||
|
||||
|
||||
def fixed_table_layout(box):
|
||||
"""Run the fixed table layout and return a list of column widths.
|
||||
|
||||
http://www.w3.org/TR/CSS21/tables.html#fixed-table-layout
|
||||
|
||||
"""
|
||||
table = box.get_wrapped_table()
|
||||
assert table.width != 'auto'
|
||||
|
||||
all_columns = [column for column_group in table.column_groups
|
||||
for column in column_group.children]
|
||||
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 = []
|
||||
num_columns = max(
|
||||
len(all_columns),
|
||||
sum(cell.colspan for cell in first_row_cells)
|
||||
)
|
||||
# ``None`` means not know yet.
|
||||
column_widths = [None] * num_columns
|
||||
|
||||
# `width` on column boxes
|
||||
for i, column in enumerate(all_columns):
|
||||
resolve_one_percentage(column, 'width', table.width)
|
||||
if column.width != 'auto':
|
||||
column_widths[i] = column.width
|
||||
|
||||
if table.style['border_collapse'] == 'separate':
|
||||
border_spacing_x, _ = table.style['border_spacing']
|
||||
else:
|
||||
border_spacing_x = 0
|
||||
|
||||
# `width` on cells of the first row.
|
||||
i = 0
|
||||
for cell in first_row_cells:
|
||||
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. Subtract these
|
||||
# known widths and divide among remaining columns.
|
||||
columns_without_width = [] # and occupied by this cell
|
||||
for j in range(i, i + cell.colspan):
|
||||
if column_widths[j] is None:
|
||||
columns_without_width.append(j)
|
||||
else:
|
||||
width -= column_widths[j]
|
||||
if columns_without_width:
|
||||
width_per_column = width / len(columns_without_width)
|
||||
for j in columns_without_width:
|
||||
column_widths[j] = width_per_column
|
||||
del width
|
||||
i += cell.colspan
|
||||
del i
|
||||
|
||||
# Distribute the remaining space equally on columns that do not have
|
||||
# a width yet.
|
||||
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, w in enumerate(column_widths)
|
||||
if w 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:
|
||||
# XXX this is bad, but we were given a broken table to work with...
|
||||
for i in columns_without_width:
|
||||
column_widths[i] = 0
|
||||
|
||||
# If the sum is less than the table width,
|
||||
# distribute the remaining space equally
|
||||
extra_width = table.width - sum(column_widths) - all_border_spacing
|
||||
if extra_width <= 0:
|
||||
# substract a negative: widen the table
|
||||
table.width -= extra_width
|
||||
elif num_columns:
|
||||
extra_per_column = extra_width / num_columns
|
||||
column_widths = [w + extra_per_column for w in column_widths]
|
||||
|
||||
# Now we have table.width == sum(column_widths) + all_border_spacing
|
||||
# with possible floating point rounding errors.
|
||||
# (unless there is zero column)
|
||||
table.column_widths = column_widths
|
||||
|
||||
|
||||
def auto_table_layout(context, box, containing_block):
|
||||
"""Run the auto table layout and return a list of column widths.
|
||||
|
||||
http://www.w3.org/TR/CSS21/tables.html#auto-table-layout
|
||||
|
||||
"""
|
||||
table = box.get_wrapped_table()
|
||||
(table_min_content_width, table_max_content_width,
|
||||
column_min_content_widths, column_max_content_widths,
|
||||
column_intrinsic_percentages, constrainedness,
|
||||
total_horizontal_border_spacing, grid) = \
|
||||
table_and_columns_preferred_widths(context, box, outer=False)
|
||||
|
||||
margins = 0
|
||||
if box.margin_left != 'auto':
|
||||
margins += box.margin_left
|
||||
if box.margin_right != 'auto':
|
||||
margins += box.margin_right
|
||||
paddings = table.padding_left + table.padding_right
|
||||
borders = table.border_left_width + table.border_right_width
|
||||
|
||||
cb_width, _ = containing_block
|
||||
available_width = cb_width - margins - paddings - borders
|
||||
|
||||
if table.width == 'auto':
|
||||
if available_width <= table_min_content_width:
|
||||
table.width = table_min_content_width
|
||||
elif available_width < table_max_content_width:
|
||||
table.width = available_width
|
||||
else:
|
||||
table.width = table_max_content_width
|
||||
else:
|
||||
if table.width < table_min_content_width:
|
||||
table.width = table_min_content_width
|
||||
|
||||
if not grid:
|
||||
table.column_widths = []
|
||||
return
|
||||
|
||||
assignable_width = table.width - total_horizontal_border_spacing
|
||||
min_content_guess = column_min_content_widths[:]
|
||||
min_content_percentage_guess = column_min_content_widths[:]
|
||||
min_content_specified_guess = column_min_content_widths[:]
|
||||
max_content_guess = column_max_content_widths[:]
|
||||
guesses = (
|
||||
min_content_guess, min_content_percentage_guess,
|
||||
min_content_specified_guess, max_content_guess)
|
||||
for i in range(len(grid)):
|
||||
if column_intrinsic_percentages[i]:
|
||||
min_content_percentage_guess[i] = max(
|
||||
column_intrinsic_percentages[i] / 100 * assignable_width,
|
||||
column_min_content_widths[i])
|
||||
min_content_specified_guess[i] = min_content_percentage_guess[i]
|
||||
max_content_guess[i] = min_content_percentage_guess[i]
|
||||
elif constrainedness[i]:
|
||||
min_content_specified_guess[i] = column_min_content_widths[i]
|
||||
|
||||
if assignable_width <= sum(max_content_guess):
|
||||
# Default values shouldn't be used, but we never know.
|
||||
# See https://github.com/Kozea/WeasyPrint/issues/770
|
||||
lower_guess = guesses[0]
|
||||
upper_guess = guesses[-1]
|
||||
|
||||
# We have to work around floating point rounding errors here.
|
||||
# The 1e-9 value comes from PEP 485.
|
||||
for guess in guesses:
|
||||
if sum(guess) <= assignable_width * (1 + 1e-9):
|
||||
lower_guess = guess
|
||||
else:
|
||||
break
|
||||
for guess in guesses[::-1]:
|
||||
if sum(guess) >= assignable_width * (1 - 1e-9):
|
||||
upper_guess = guess
|
||||
else:
|
||||
break
|
||||
if upper_guess == lower_guess:
|
||||
# TODO: Uncomment the assert when bugs #770 and #628 are closed
|
||||
# Equivalent to "assert assignable_width == sum(upper_guess)"
|
||||
# assert abs(assignable_width - sum(upper_guess)) <= (
|
||||
# assignable_width * 1e-9)
|
||||
table.column_widths = upper_guess
|
||||
else:
|
||||
added_widths = [
|
||||
upper_guess[i] - lower_guess[i] for i in range(len(grid))]
|
||||
available_ratio = (
|
||||
(assignable_width - sum(lower_guess)) / sum(added_widths))
|
||||
table.column_widths = [
|
||||
lower_guess[i] + added_widths[i] * available_ratio
|
||||
for i in range(len(grid))]
|
||||
else:
|
||||
table.column_widths = max_content_guess
|
||||
excess_width = assignable_width - sum(max_content_guess)
|
||||
excess_width = distribute_excess_width(
|
||||
context, grid, excess_width, table.column_widths, constrainedness,
|
||||
column_intrinsic_percentages, column_max_content_widths)
|
||||
if excess_width:
|
||||
if table_min_content_width < table.width - excess_width:
|
||||
# Reduce the width of the size from the excess width that has
|
||||
# not been distributed.
|
||||
table.width -= excess_width
|
||||
else:
|
||||
# Break rules
|
||||
columns = [i for i, column in enumerate(grid) if any(column)]
|
||||
for i in columns:
|
||||
table.column_widths[i] += excess_width / len(columns)
|
||||
|
||||
|
||||
def table_wrapper_width(context, wrapper, containing_block):
|
||||
"""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':
|
||||
fixed_table_layout(wrapper)
|
||||
else:
|
||||
auto_table_layout(context, wrapper, containing_block)
|
||||
|
||||
wrapper.width = table.border_width()
|
||||
|
||||
|
||||
def cell_baseline(cell):
|
||||
"""Return the y position of a cell baseline from the top of its border box.
|
||||
|
||||
See http://www.w3.org/TR/CSS21/tables.html#height-layout
|
||||
|
||||
"""
|
||||
result = find_in_flow_baseline(
|
||||
cell, baseline_types=(boxes.LineBox, boxes.TableRowBox))
|
||||
if result is not None:
|
||||
return result - cell.position_y
|
||||
else:
|
||||
# Default to the bottom of the content area.
|
||||
return cell.border_top_width + cell.padding_top + cell.height
|
||||
|
||||
|
||||
def find_in_flow_baseline(box, last=False, baseline_types=(boxes.LineBox,)):
|
||||
"""Return the absolute y position for the first (or last) in-flow baseline.
|
||||
|
||||
If there’s no in-flow baseline, return None.
|
||||
|
||||
"""
|
||||
# TODO: synthetize baseline when needed
|
||||
# See https://www.w3.org/TR/css-align-3/#synthesize-baseline
|
||||
if isinstance(box, baseline_types):
|
||||
return box.position_y + box.baseline
|
||||
if isinstance(box, boxes.ParentBox) and not isinstance(
|
||||
box, boxes.TableCaptionBox):
|
||||
children = reversed(box.children) if last else box.children
|
||||
for child in children:
|
||||
if child.is_in_normal_flow():
|
||||
result = find_in_flow_baseline(child, last, baseline_types)
|
||||
if result is not None:
|
||||
return result
|
||||
|
||||
|
||||
def distribute_excess_width(context, grid, excess_width, column_widths,
|
||||
constrainedness, column_intrinsic_percentages,
|
||||
column_max_content_widths,
|
||||
column_slice=slice(0, None)):
|
||||
"""Distribute available width to columns.
|
||||
|
||||
Return excess width left when it's impossible without breaking rules.
|
||||
|
||||
See http://dbaron.org/css/intrinsic/#distributetocols
|
||||
|
||||
"""
|
||||
# First group
|
||||
columns = [
|
||||
(i + column_slice.start, column)
|
||||
for i, column in enumerate(grid[column_slice])
|
||||
if not constrainedness[i + column_slice.start] and
|
||||
column_intrinsic_percentages[i + column_slice.start] == 0 and
|
||||
column_max_content_widths[i + column_slice.start] > 0]
|
||||
if columns:
|
||||
current_widths = [column_widths[i] for i, column in columns]
|
||||
differences = [
|
||||
max(0, width[0] - width[1])
|
||||
for width in zip(column_max_content_widths, current_widths)]
|
||||
if sum(differences) > excess_width:
|
||||
differences = [
|
||||
difference / sum(differences) * excess_width
|
||||
for difference in differences]
|
||||
excess_width -= sum(differences)
|
||||
for i, difference in enumerate(differences):
|
||||
column_widths[columns[i][0]] += difference
|
||||
if excess_width <= 0:
|
||||
return
|
||||
|
||||
# Second group
|
||||
columns = [
|
||||
i + column_slice.start for i, column in enumerate(grid[column_slice])
|
||||
if not constrainedness[i + column_slice.start] and
|
||||
column_intrinsic_percentages[i + column_slice.start] == 0]
|
||||
if columns:
|
||||
for i in columns:
|
||||
column_widths[i] += excess_width / len(columns)
|
||||
return
|
||||
|
||||
# Third group
|
||||
columns = [
|
||||
(i + column_slice.start, column)
|
||||
for i, column in enumerate(grid[column_slice])
|
||||
if constrainedness[i + column_slice.start] and
|
||||
column_intrinsic_percentages[i + column_slice.start] == 0 and
|
||||
column_max_content_widths[i + column_slice.start] > 0]
|
||||
if columns:
|
||||
current_widths = [column_widths[i] for i, column in columns]
|
||||
differences = [
|
||||
max(0, width[0] - width[1])
|
||||
for width in zip(column_max_content_widths, current_widths)]
|
||||
if sum(differences) > excess_width:
|
||||
differences = [
|
||||
difference / sum(differences) * excess_width
|
||||
for difference in differences]
|
||||
excess_width -= sum(differences)
|
||||
for i, difference in enumerate(differences):
|
||||
column_widths[columns[i][0]] += difference
|
||||
if excess_width <= 0:
|
||||
return
|
||||
|
||||
# Fourth group
|
||||
columns = [
|
||||
(i + column_slice.start, column)
|
||||
for i, column in enumerate(grid[column_slice])
|
||||
if column_intrinsic_percentages[i + column_slice.start] > 0]
|
||||
if columns:
|
||||
fixed_width = sum(
|
||||
column_widths[j] for j in range(len(grid))
|
||||
if j not in [i for i, column in columns])
|
||||
percentage_width = sum(
|
||||
column_intrinsic_percentages[i]
|
||||
for i, column in columns)
|
||||
if fixed_width and percentage_width >= 100:
|
||||
# Sum of the percentages are greater than 100%
|
||||
ratio = excess_width
|
||||
elif fixed_width == 0:
|
||||
# No fixed width, let's take the whole excess width
|
||||
ratio = excess_width
|
||||
else:
|
||||
ratio = fixed_width / (100 - percentage_width)
|
||||
|
||||
widths = [
|
||||
column_intrinsic_percentages[i] * ratio for i, column in columns]
|
||||
current_widths = [column_widths[i] for i, column in columns]
|
||||
# Allow to reduce the size of the columns to respect the percentage
|
||||
differences = [
|
||||
width[0] - width[1]
|
||||
for width in zip(widths, current_widths)]
|
||||
if sum(differences) > excess_width:
|
||||
differences = [
|
||||
difference / sum(differences) * excess_width
|
||||
for difference in differences]
|
||||
excess_width -= sum(differences)
|
||||
for i, difference in enumerate(differences):
|
||||
column_widths[columns[i][0]] += difference
|
||||
if excess_width <= 0:
|
||||
return
|
||||
|
||||
# Bonus: we've tried our best to distribute the extra size, but we
|
||||
# failed. Instead of blindly distributing the size among all the colums
|
||||
# and breaking all the rules (as said in the draft), let's try to
|
||||
# change the columns with no constraint at all, then resize the table,
|
||||
# and at least break the rules to make the columns fill the table.
|
||||
|
||||
# Fifth group, part 1
|
||||
columns = [
|
||||
i + column_slice.start for i, column in enumerate(grid[column_slice])
|
||||
if any(column) and
|
||||
column_intrinsic_percentages[i + column_slice.start] == 0 and
|
||||
not any(
|
||||
max_content_width(context, cell)
|
||||
for cell in column if cell)]
|
||||
if columns:
|
||||
for i in columns:
|
||||
column_widths[i] += excess_width / len(columns)
|
||||
return
|
||||
|
||||
# Fifth group, part 2, aka abort
|
||||
return excess_width
|
Reference in New Issue
Block a user