tractatus/pptx-env/lib/python3.12/site-packages/weasyprint/layout/flex.py
TheFlow 2298d36bed fix(submissions): restructure Economist package and fix article display
- Create Economist SubmissionTracking package correctly:
  * mainArticle = full blog post content
  * coverLetter = 216-word SIR— letter
  * Links to blog post via blogPostId
- Archive 'Letter to The Economist' from blog posts (it's the cover letter)
- Fix date display on article cards (use published_at)
- Target publication already displaying via blue badge

Database changes:
- Make blogPostId optional in SubmissionTracking model
- Economist package ID: 68fa85ae49d4900e7f2ecd83
- Le Monde package ID: 68fa2abd2e6acd5691932150

Next: Enhanced modal with tabs, validation, export

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-24 08:47:42 +13:00

952 lines
43 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Layout for flex containers and flex-items."""
import sys
from math import inf, log10
from ..css.properties import Dimension
from ..formatting_structure import boxes
from . import percent
from .absolute import AbsolutePlaceholder, absolute_layout
from .preferred import max_content_width, min_content_width, min_max
from .table import find_in_flow_baseline, table_wrapper_width
class FlexLine(list):
"""Flex container line."""
def flex_layout(context, box, bottom_space, skip_stack, containing_block, page_is_empty,
absolute_boxes, fixed_boxes, discard):
from . import block
# TODO: merge this with block_container_layout.
context.create_flex_formatting_context()
resume_at = None
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'
row_gap, column_gap = box.style['row_gap'], box.style['column_gap']
if draw_bottom_decoration:
bottom_space += box.padding_bottom + box.border_bottom_width + box.margin_bottom
if box.style['position'] == 'relative':
# New containing block, use a new absolute list
absolute_boxes = []
# References are to: https://www.w3.org/TR/css-flexbox-1/#layout-algorithm.
# 1 Initial setup, done in formatting_structure.build.
# 2 Determine the available main and cross space for the flex items.
if box.style['flex_direction'].startswith('row'):
main, cross = 'width', 'height'
else:
main, 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
# Define available main space.
# TODO: min- and max-content not implemented.
if getattr(box, main) != 'auto':
# If that dimension of the flex containers content box is a definite size…
available_main_space = getattr(box, main)
else:
# Otherwise, subtract the flex containers margin, border, and padding…
if main == '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:
available_main_space = inf
# Same as above for available cross space.
# TODO: min- and max-content not implemented.
if getattr(box, cross) != 'auto':
available_cross_space = getattr(box, cross)
else:
if cross == 'width':
available_cross_space = (
containing_block.width -
margin_left - margin_right -
box.padding_left - box.padding_right -
box.border_left_width - box.border_right_width)
else:
available_cross_space = inf
# 3 Determine the flex base size and hypothetical main size of each item.
parent_box = box.copy()
percent.resolve_percentages(parent_box, containing_block)
block.block_level_width(parent_box, containing_block)
children = sorted(box.children, key=lambda item: item.style['order'])
original_skip_stack = skip_stack
if skip_stack is not None:
(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, skip_stack = 0, None
child_skip_stack = skip_stack
if row_gap == 'normal':
row_gap = 0
elif row_gap.unit == '%':
if box.height == 'auto':
row_gap = 0
else:
row_gap = row_gap.value / 100 * box.height
else:
row_gap = row_gap.value
if column_gap == 'normal':
column_gap = 0
elif column_gap.unit == '%':
if box.width == 'auto':
column_gap = 0
else:
column_gap = column_gap.value / 100 * box.width
else:
column_gap = column_gap.value
if main == 'width':
main_gap, cross_gap = column_gap, row_gap
else:
main_gap, cross_gap = row_gap, column_gap
position_x = (
parent_box.position_x + parent_box.border_left_width + parent_box.padding_left)
if parent_box.margin_left != 'auto':
position_x += parent_box.margin_left
position_y = (
parent_box.position_y + parent_box.border_top_width + parent_box.padding_top)
if parent_box.margin_top != 'auto':
position_y += parent_box.margin_top
for index, child in enumerate(children):
if not child.is_flex_item:
# Absolute child layout: create placeholder.
if child.is_absolutely_positioned():
child.position_x = position_x
child.position_y = position_y
new_child = placeholder = AbsolutePlaceholder(child)
placeholder.index = index
children[index] = placeholder
if child.style['position'] == 'absolute':
absolute_boxes.append(placeholder)
else:
fixed_boxes.append(placeholder)
elif child.is_running():
running_name = child.style['position'][1]
page = context.current_page
context.running_elements[running_name][page].append(child)
continue
# See https://www.w3.org/TR/css-flexbox-1/#min-size-auto.
if main == 'width':
child_containing_block = (available_main_space, parent_box.height)
else:
child_containing_block = (parent_box.width, available_main_space)
percent.resolve_percentages(child, child_containing_block)
if child.is_table_wrapper:
table_wrapper_width(context, child, child_containing_block)
child.position_x = position_x
child.position_y = position_y
if child.style['min_width'] == 'auto':
specified_size = child.width
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(inf, 'px')
content_size = min_content_width(context, new_child, outer=False)
transferred_size = None
if isinstance(child, boxes.ReplacedBox):
image = child.replacement
_, intrinsic_height, intrinsic_ratio = image.get_intrinsic_size(
child.style['image_resolution'], child.style['font_size'])
if intrinsic_ratio and intrinsic_height:
transferred_size = intrinsic_height * intrinsic_ratio
content_size = max(
child.min_width, min(child.max_width, content_size))
if specified_size != 'auto':
child.min_width = min(specified_size, content_size)
elif transferred_size is not None:
child.min_width = min(transferred_size, content_size)
else:
child.min_width = content_size
if child.style['min_height'] == 'auto':
# TODO: avoid calling block_level_layout, write min_content_height instead.
specified_size = child.height
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(inf, 'px')
if new_child.style['width'] == 'auto':
new_child_width = max_content_width(context, new_child)
new_child.style['width'] = Dimension(new_child_width, 'px')
new_child = block.block_level_layout(
context, new_child, bottom_space, child_skip_stack, parent_box,
page_is_empty)[0]
content_size = new_child.height if new_child else 0
transferred_size = None
if isinstance(child, boxes.ReplacedBox):
image = child.replacement
intrinsic_width, _, intrinsic_ratio = image.get_intrinsic_size(
child.style['image_resolution'], child.style['font_size'])
if intrinsic_ratio and intrinsic_width:
transferred_size = intrinsic_width / intrinsic_ratio
content_size = max(
child.min_height, min(child.max_height, content_size))
elif not intrinsic_width:
# TODO: wrongly set by block_level_layout, would be OK with
# min_content_height.
content_size = 0
if specified_size != 'auto':
child.min_height = min(specified_size, content_size)
elif transferred_size is not None:
child.min_height = min(transferred_size, content_size)
else:
child.min_height = content_size
if child.style['flex_basis'] == 'content':
flex_basis = 'content'
else:
flex_basis = percent.percentage(
child.style['flex_basis'], available_main_space)
if flex_basis == 'auto':
if (flex_basis := getattr(child, main)) == 'auto':
flex_basis = 'content'
# 3.A If the item has a definite used flex basis…
if flex_basis != 'content':
child.flex_base_size = flex_basis
if main == 'width':
child.main_outer_extra = (
child.border_left_width + child.border_right_width +
child.padding_left + child.padding_right)
if child.margin_left != 'auto':
child.main_outer_extra += child.margin_left
if child.margin_right != 'auto':
child.main_outer_extra += child.margin_right
else:
child.main_outer_extra = (
child.border_top_width + child.border_bottom_width +
child.padding_top + child.padding_bottom)
if child.margin_top != 'auto':
child.main_outer_extra += child.margin_top
if child.margin_bottom != 'auto':
child.main_outer_extra += child.margin_bottom
elif False:
# TODO: 3.B If the flex item has an intrinsic aspect ratio…
# TODO: 3.C If the used flex basis is 'content'…
# TODO: 3.D Otherwise, if the used flex basis is 'content'…
pass
else:
# 3.E Otherwise…
new_child = child.copy()
new_child.style = child.style.copy()
if main == 'width':
# … the items min and max main sizes are ignored.
new_child.style['min_width'] = Dimension(0, 'px')
new_child.style['max_width'] = Dimension(inf, 'px')
child.flex_base_size = max_content_width(
context, new_child, outer=False)
child.main_outer_extra = (
max_content_width(context, child) - child.flex_base_size)
else:
# … the items min and max main sizes are ignored.
new_child.style['min_height'] = Dimension(0, 'px')
new_child.style['max_height'] = Dimension(inf, 'px')
new_child.width = inf
new_child, _, _, adjoining_margins, _, _ = block.block_level_layout(
context, new_child, bottom_space, child_skip_stack, parent_box,
page_is_empty, absolute_boxes, fixed_boxes)
if new_child:
# As flex items margins never collapse (with other flex items or
# with the flex container), we can add the adjoining margins to the
# child height.
new_child.height += block.collapse_margin(adjoining_margins)
child.flex_base_size = new_child.height
child.main_outer_extra = (
new_child.margin_height() - new_child.height)
else:
child.flex_base_size = child.main_outer_extra = 0
if main == 'width':
position_x += child.flex_base_size + child.main_outer_extra
else:
position_y += child.flex_base_size + child.main_outer_extra
min_size = getattr(child, f'min_{main}')
max_size = getattr(child, f'max_{main}')
child.hypothetical_main_size = max(
min_size, min(child.flex_base_size, max_size))
# Skip stack is only for the first child.
child_skip_stack = None
# 4 Determine the main size of the flex container using the rules of the formatting
# context in which it participates.
if main == 'width':
block.block_level_width(box, containing_block)
else:
if box.height == 'auto':
box.height = 0
flex_items = (child for child in children if child.is_flex_item)
for i, child in enumerate(flex_items):
box.height += child.hypothetical_main_size + child.main_outer_extra
if i:
box.height += main_gap
box.height = max(box.min_height, min(box.height, box.max_height))
# 5 If the flex container is single-line, collect all the flex items into a single
# flex line.
flex_lines = []
line = []
line_size = 0
main_size = getattr(box, main)
for i, child in enumerate(children, start=skip):
if not child.is_flex_item:
continue
line_size += child.hypothetical_main_size + child.main_outer_extra
if i > skip:
line_size += main_gap
if box.style['flex_wrap'] != 'nowrap' and line_size > main_size:
if line:
flex_lines.append(FlexLine(line))
line = [(i, child)]
line_size = child.hypothetical_main_size + child.main_outer_extra
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()
# 6 Resolve the flexible lengths of all the flex items to find their used main size.
available_main_space = getattr(box, main)
for line in flex_lines:
# 9.7.1 Determine the used flex factor.
hypothetical_main_size = sum(
child.hypothetical_main_size + child.main_outer_extra
for index, child in line)
if hypothetical_main_size < available_main_space:
flex_factor_type = 'grow'
else:
flex_factor_type = 'shrink'
# 9.7.3 Size inflexible items.
for index, child in line:
if flex_factor_type == 'grow':
child.flex_factor = child.style['flex_grow']
flex_condition = child.flex_base_size > child.hypothetical_main_size
else:
child.flex_factor = child.style['flex_shrink']
flex_condition = child.flex_base_size < child.hypothetical_main_size
if child.flex_factor == 0 or flex_condition:
child.target_main_size = child.hypothetical_main_size
child.frozen = True
else:
child.frozen = False
# 9.7.4 Calculate initial free space.
initial_free_space = available_main_space
for i, (index, child) in enumerate(line):
if child.frozen:
initial_free_space -= child.target_main_size + child.main_outer_extra
else:
initial_free_space -= child.flex_base_size + child.main_outer_extra
if i:
initial_free_space -= main_gap
# 9.7.5.a Check for flexible items.
while not all(child.frozen for index, child in line):
unfrozen_factor_sum = 0
remaining_free_space = available_main_space
# 9.7.5.b Calculate the remaining free space.
for i, (index, child) in enumerate(line):
if child.frozen:
remaining_free_space -= (
child.target_main_size + child.main_outer_extra)
else:
remaining_free_space -= (
child.flex_base_size + child.main_outer_extra)
unfrozen_factor_sum += child.flex_factor
if i:
remaining_free_space -= main_gap
if unfrozen_factor_sum < 1:
initial_free_space *= unfrozen_factor_sum
if initial_free_space == inf:
initial_free_space = sys.maxsize
if remaining_free_space == inf:
remaining_free_space = sys.maxsize
initial_magnitude = (
int(log10(initial_free_space)) if initial_free_space > 0 else -inf)
remaining_magnitude = (
int(log10(remaining_free_space)) if remaining_free_space > 0 else -inf)
if initial_magnitude < remaining_magnitude:
remaining_free_space = initial_free_space
# 9.7.5.c Distribute free space proportional to the flex factors.
if remaining_free_space == 0:
# If the remaining free space is zero: "Do nothing", but we at least set
# the flex_base_size as target_main_size for next step.
for index, 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 index, 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 index, child in line:
if not child.frozen:
# If using the flex grow factor…
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)
# If using the flex shrink factor…
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)
child.target_main_size = min_max(child, child.target_main_size)
# 9.7.5.d Fix min/max violations.
for index, child in line:
child.adjustment = 0
if not child.frozen:
min_size = getattr(child, f'min_{main}')
max_size = getattr(child, f'max_{main}')
min_size = max(min_size , min(child.target_main_size, max_size))
if child.target_main_size < min_size:
child.adjustment = min_size - child.target_main_size
child.target_main_size = min_size
# 9.7.5.e Freeze over-flexed items.
adjustments = sum(child.adjustment for index, child in line)
for index, child in line:
# Zero: Freeze all items.
if adjustments == 0:
child.frozen = True
# Positive: Freeze all the items with min violations.
elif adjustments > 0 and child.adjustment > 0:
child.frozen = True
# Negative: Freeze all the items with max violations.
elif adjustments < 0 and child.adjustment < 0:
child.frozen = True
# 9.7.6 Set each items used main size to its target main size.
for index, child in line:
if main == 'width':
child.width = child.target_main_size
else:
child.height = child.target_main_size
# 7 Determine the hypothetical cross size of each item.
# TODO: Handle breaks.
new_flex_lines = []
child_skip_stack = skip_stack
for line in flex_lines:
new_flex_line = FlexLine()
for index, child in line:
# TODO: Fix this value, see test_flex_item_auto_margin_cross.
if child.margin_top == 'auto':
child.margin_top = 0
if child.margin_bottom == 'auto':
child.margin_bottom = 0
# TODO: Find another way than calling block_level_layout_switch.
new_child = child.copy()
new_child, _, _, adjoining_margins, _, _ = block.block_level_layout_switch(
context, new_child, -inf, child_skip_stack, parent_box, page_is_empty,
absolute_boxes, fixed_boxes, [], discard, None)
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 height.
child.height += block.collapse_margin(adjoining_margins)
else:
if child.width == 'auto':
min_width = min_content_width(context, child, outer=False)
max_width = max_content_width(context, child, outer=False)
child.width = min(max(min_width, new_child.width), max_width)
else:
child.width = new_child.width
new_flex_line.append((index, 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
# 8 Calculate the cross size of each flex line.
cross_size = getattr(box, cross)
if len(flex_lines) == 1 and cross_size != 'auto':
# If the flex container is single-line…
flex_lines[0].cross_size = cross_size
else:
# Otherwise, for each flex line…
# 8.1 Collect all the flex items whose inline-axis is parallel to the main-axis…
for line in flex_lines:
collected_items = []
not_collected_items = []
for index, child in line:
align_self = child.style['align_self']
collect = (
box.style['flex_direction'].startswith('row') and
'baseline' in align_self and
'auto' not in (child.margin_top, child.margin_bottom))
(collected_items if collect else not_collected_items).append(child)
cross_start_distance = 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
# 8.2 Find the largest outer hypothetical cross size.
if not_collected_items:
non_collected_cross_size = -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)
# 8.3 Set the used cross-size of the flex line.
line.cross_size = max(collected_cross_size, non_collected_cross_size)
# 8.3 If the flex container is single-line…
if len(flex_lines) == 1:
line, = flex_lines
min_cross_size = getattr(box, f'min_{cross}')
if min_cross_size == 'auto':
min_cross_size = -inf
max_cross_size = getattr(box, f'max_{cross}')
if max_cross_size == 'auto':
max_cross_size = inf
line.cross_size = max(min_cross_size, min(line.cross_size, max_cross_size))
# 9 Handle 'align-content: stretch'.
align_content = box.style['align_content']
if 'normal' in align_content:
align_content = ('stretch',)
if 'stretch' in align_content:
definite_cross_size = None
if cross == 'height' and box.height != 'auto':
definite_cross_size = box.height
elif cross == 'width':
if isinstance(box, boxes.FlexBox):
if box.width == 'auto':
definite_cross_size = available_cross_space
else:
definite_cross_size = box.width
if definite_cross_size is not None:
extra_cross_size = definite_cross_size
extra_cross_size -= sum(line.cross_size for line in flex_lines)
extra_cross_size -= (len(flex_lines) - 1) * cross_gap
if extra_cross_size:
for line in flex_lines:
line.cross_size += extra_cross_size / len(flex_lines)
# TODO: 10 Collapse 'visibility: collapse' items.
# 11 Determine the used cross size of each flex item.
align_items = box.style['align_items']
if 'normal' in align_items:
align_items = ('stretch',)
for line in flex_lines:
for index, child in line:
align_self = child.style['align_self']
if 'normal' in align_self:
align_self = ('stretch',)
elif 'auto' in align_self:
align_self = align_items
if 'stretch' in align_self and child.style[cross] == 'auto':
cross_margins = (
(child.style['margin_top'], child.style['margin_bottom'])
if cross == 'height' else
(child.style['margin_left'], child.style['margin_right']))
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)
# else: Cross size has been set by step 7.
# 12 Distribute any remaining free space.
original_position_main = (
box.content_box_x() if main == 'width'
else box.content_box_y())
justify_content = box.style['justify_content']
if 'normal' in justify_content:
justify_content = ('flex-start',)
if box.style['flex_direction'].endswith('-reverse'):
if 'flex-start' in justify_content:
justify_content = ('flex-end',)
elif 'flex-end' in justify_content:
justify_content = ('flex-start',)
elif 'start' in justify_content:
justify_content = ('end',)
elif 'end' in justify_content:
justify_content = ('start',)
for line in flex_lines:
position_main = original_position_main
if main == 'width':
free_space = box.width
for index, 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 index, 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
free_space -= (len(line) - 1) * main_gap
# 12.1 If the remaining free space is positive…
margins = 0
for index, child in line:
if main == '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 index, child in line:
if main == '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 main == 'width':
free_space *= -1
# 12.2 Align the items along the main-axis per justify-content.
if {'end', 'flex-end', 'right'} & set(justify_content):
position_main += free_space
elif 'center' in justify_content:
position_main += free_space / 2
elif 'space-around' in justify_content:
position_main += free_space / len(line) / 2
elif 'space-evenly' in justify_content:
position_main += free_space / (len(line) + 1)
growths = sum(child.style['flex_grow'] for child in children)
for i, (index, child) in enumerate(line):
if i:
position_main += main_gap
if main == 'width':
child.position_x = position_main
if 'stretch' in justify_content and growths:
child.width += free_space * child.style['flex_grow'] / growths
else:
child.position_y = position_main
margin_main = (
child.margin_width() if main == 'width' else child.margin_height())
if box.style['direction'] == 'rtl' and main == 'width':
margin_main *= -1
position_main += margin_main
if 'space-around' in justify_content:
position_main += free_space / len(line)
elif 'space-between' in justify_content:
if len(line) > 1:
position_main += free_space / (len(line) - 1)
elif 'space-evenly' in justify_content:
position_main += free_space / (len(line) + 1)
# 13 Resolve cross-axis auto margins.
if cross == 'width':
# Make sure width/margins are no longer "auto", as we did not do it above in
# step 4.
block.block_level_width(box, containing_block)
position_cross = box.content_box_y() if cross == 'height' else box.content_box_x()
for line in flex_lines:
line.lower_baseline = -inf
# TODO: Don't duplicate this loop.
for index, child in line:
align_self = child.style['align_self']
if 'auto' in align_self:
align_self = align_items
if 'baseline' in align_self and main == 'width':
# TODO: handle vertical text.
child.baseline = child._baseline - position_cross
line.lower_baseline = max(line.lower_baseline, child.baseline)
if line.lower_baseline == -inf:
line.lower_baseline = line[0][1]._baseline if line else 0
for index, child in line:
cross_margins = (
(child.style['margin_top'], child.style['margin_bottom'])
if cross == 'height' else
(child.style['margin_left'], child.style['margin_right']))
auto_margins = sum([margin == 'auto' for margin in cross_margins])
# If a flex item has auto cross-axis 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:
# If its outer cross size is less than the cross size…
extra_cross /= auto_margins
if cross == 'height':
if child.style['margin_top'] == 'auto':
child.margin_top = extra_cross
if child.style['margin_bottom'] == 'auto':
child.margin_bottom = extra_cross
else:
if child.style['margin_left'] == 'auto':
child.margin_left = extra_cross
if child.style['margin_right'] == 'auto':
child.margin_right = extra_cross
else:
# Otherwise…
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:
# 14 Align all flex items along the cross-axis.
align_self = child.style['align_self']
if 'normal' in align_self:
align_self = ('stretch',)
elif 'auto' in align_self:
align_self = align_items
position = 'position_y' if cross == 'height' else 'position_x'
setattr(child, position, position_cross)
if {'end', 'self-end', 'flex-end'} & set(align_self):
if cross == 'height':
child.position_y += line.cross_size - child.margin_height()
else:
child.position_x += line.cross_size - child.margin_width()
elif 'center' in align_self:
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 'baseline' in align_self:
if cross == 'height':
child.position_y += line.lower_baseline - child.baseline
else:
# TODO: Handle vertical text.
pass
elif 'stretch' in align_self:
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)
position_cross += line.cross_size
# 15 Determine the flex containers used cross size.
# TODO: Use the updated algorithm.
if getattr(box, cross) == 'auto':
# Otherwise, use the sum of the flex lines' cross sizes…
# TODO: Handle min-max.
# TODO: What about align-content here?
cross_size = sum(line.cross_size for line in flex_lines)
cross_size += (len(flex_lines) - 1) * cross_gap
setattr(box, cross, cross_size)
if len(flex_lines) > 1:
# 15 If the cross size property is a definite size, use that…
extra_cross_size = getattr(box, cross)
extra_cross_size -= sum(line.cross_size for line in flex_lines)
extra_cross_size -= (len(flex_lines) - 1) * cross_gap
# 16 Align all flex lines per align-content.
cross_translate = 0
direction = 'position_y' if cross == 'height' else 'position_x'
for i, line in enumerate(flex_lines):
flex_items = tuple(child for _, child in line if child.is_flex_item)
if i:
cross_translate += cross_gap
for child in flex_items:
current_value = getattr(child, direction) + cross_translate
setattr(child, direction, current_value)
if extra_cross_size == 0:
continue
for child in flex_items:
if {'flex-end', 'end'} & set(align_content):
setattr(child, direction, current_value + extra_cross_size)
elif 'center' in align_content:
setattr(child, direction, current_value + extra_cross_size / 2)
elif 'space-around' in align_content:
setattr(
child, direction,
current_value + extra_cross_size / len(flex_lines) / 2)
elif 'space-evenly' in align_content:
setattr(
child, direction,
current_value + extra_cross_size / (len(flex_lines) + 1))
if 'space-between' in align_content:
cross_translate += extra_cross_size / (len(flex_lines) - 1)
elif 'space-around' in align_content:
cross_translate += extra_cross_size / len(flex_lines)
elif 'space-evenly' in align_content:
cross_translate += extra_cross_size / (len(flex_lines) + 1)
# Now we are no longer in the flex algorithm.
box = box.copy_with_children(
[child for child in children if child.is_absolutely_positioned()])
child_skip_stack = skip_stack
for line in flex_lines:
for index, child in line:
if child.is_flex_item:
# TODO: Don't use block_level_layout_switch.
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=discard,
max_lines=None)[:2]
if new_child is None:
if resume_at:
resume_index, = resume_at
resume_index -= 1
else:
resume_index = 0
resume_at = {resume_index + index: None}
else:
page_is_empty = False
box.children.append(new_child)
if child_resume_at is not None:
if original_skip_stack:
first_level_skip, = original_skip_stack
else:
first_level_skip = 0
if resume_at:
resume_index, = resume_at
first_level_skip += resume_index
resume_at = {first_level_skip + index: child_resume_at}
if resume_at:
break
# Skip stack is only for the first child.
child_skip_stack = None
if resume_at:
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)
# TODO: Use real algorithm, see https://www.w3.org/TR/css-flexbox-1/#flex-baselines.
if isinstance(box, boxes.InlineFlexBox):
if main == 'width': # and main text direction is horizontal
box.baseline = flex_lines[0].lower_baseline if flex_lines else 0
else:
for child in box.children:
if child.is_in_normal_flow():
box.baseline = find_in_flow_baseline(child) or 0
break
else:
box.baseline = 0
box.remove_decoration(start=False, end=resume_at and not discard)
context.finish_flex_formatting_context(box)
# TODO: Check these returned values.
return box, resume_at, {'break': 'any', 'page': None}, [], False