1
1
mirror of https://github.com/Kozea/WeasyPrint.git synced 2024-10-04 16:07:57 +03:00

Fix all crashes with W3C test suite

This commit is contained in:
Guillaume Ayoub 2018-02-24 04:41:11 +01:00
parent 1fc10281df
commit 19365e824e
9 changed files with 264 additions and 87 deletions

View File

@ -282,9 +282,10 @@ def break_before_after(computer, name, value):
@register_computer('padding-left')
@register_computer('text-indent')
@register_computer('hyphenate-limit-zone')
@register_computer('flex-basis')
def length(computer, name, value, font_size=None, pixels_only=False):
"""Compute a length ``value``."""
if value == 'auto':
if value in ('auto', 'content'):
return value
if value.value == 0:
return 0 if pixels_only else ZERO_PIXELS

View File

@ -511,8 +511,9 @@ def draw_border(context, box, enable_hinting):
def draw_column_border():
"""Draw column borders."""
columns = (
box.style['column_width'] != 'auto' or
box.style['column_count'] != 'auto')
isinstance(box, boxes.BlockContainerBox) and (
box.style['column_width'] != 'auto' or
box.style['column_count'] != 'auto'))
if columns and box.style['column_rule_width']:
border_widths = (0, 0, 0, box.style['column_rule_width'])
for child in box.children[1:]:
@ -984,7 +985,8 @@ def draw_replacedbox(context, box):
def draw_inline_level(context, page, box, enable_hinting):
if isinstance(box, StackingContext):
stacking_context = box
assert isinstance(stacking_context.box, boxes.InlineBlockBox)
assert isinstance(
stacking_context.box, (boxes.InlineBlockBox, boxes.InlineFlexBox))
draw_stacking_context(context, stacking_context, enable_hinting)
else:
draw_background(context, box.background, enable_hinting)

View File

@ -399,7 +399,14 @@ def wrap_improper(box, children, wrapper_type, test=None):
# Whitespace either fail the test or were removed earlier,
# so there is no need to take special care with the definition
# of "consecutive".
improper.append(child)
if isinstance(box, boxes.FlexContainerBox):
# The display value of a flex item must be "blockified", see
# https://www.w3.org/TR/css-flexbox-1/#flex-items
# TODO: These blocks are currently ignored, we should
# "blockify" them and their children.
pass
else:
improper.append(child)
if improper:
wrapper = wrapper_type.anonymous_from(box, children=[])
# Apply the rules again on the new wrapper

View File

@ -229,6 +229,49 @@ def absolute_block(context, box, containing_block, fixed_boxes):
return new_box
def absolute_flex(context, box, containing_block_sizes, fixed_boxes,
containing_block):
# Avoid a circular import
from .flex import flex_layout
# TODO: this function is really close to absolute_block, we should have
# only one function.
# TODO: having containing_block_sizes and containing_block is stupid.
cb_x, cb_y, cb_width, cb_height = containing_block_sizes
translate_box_width, translate_x = absolute_width(
box, context, containing_block_sizes)
translate_box_height, translate_y = absolute_height(
box, context, containing_block_sizes)
# This box is the containing block for absolute descendants.
absolute_boxes = []
if box.is_table_wrapper:
table_wrapper_width(context, box, (cb_width, cb_height))
# TODO: remove device_size everywhere else
new_box, _, _, _, _ = flex_layout(
context, box, max_position_y=float('inf'), skip_stack=None,
containing_block=containing_block, device_size=None,
page_is_empty=False, absolute_boxes=absolute_boxes,
fixed_boxes=fixed_boxes)
list_marker_layout(context, new_box)
for child_placeholder in absolute_boxes:
absolute_layout(context, child_placeholder, new_box, fixed_boxes)
if translate_box_width:
translate_x -= new_box.width
if translate_box_height:
translate_y -= new_box.height
new_box.translate(translate_x, translate_y)
return new_box
def absolute_layout(context, placeholder, containing_block, fixed_boxes):
"""Set the width of absolute positioned ``box``."""
assert not placeholder._layout_done
@ -260,6 +303,9 @@ def absolute_box_layout(context, box, containing_block, fixed_boxes):
# Absolute tables are wrapped into block boxes
if isinstance(box, boxes.BlockBox):
new_box = absolute_block(context, box, containing_block, fixed_boxes)
elif isinstance(box, boxes.FlexContainerBox):
new_box = absolute_flex(
context, box, containing_block, fixed_boxes, cb)
else:
assert isinstance(box, boxes.BlockReplacedBox)
new_box = absolute_replaced(context, box, containing_block)

View File

@ -9,11 +9,13 @@
"""
import sys
from math import log10
from ..css.properties import Dimension
from ..formatting_structure import boxes
from .percentages import resolve_percentages
from .markers import list_marker_layout
from .percentages import resolve_one_percentage, resolve_percentages
from .preferred import max_content_width, min_content_width
from .tables import find_in_flow_baseline
@ -48,8 +50,11 @@ def flex_layout(context, box, max_position_y, skip_stack, containing_block,
else:
main_space = max_position_y - box.position_y
if containing_block.height != 'auto':
assert containing_block.height.unit == 'px'
main_space = min(main_space, containing_block.height.value)
if hasattr(containing_block.height, 'unit'):
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 -
box.margin_top - box.margin_bottom -
@ -62,8 +67,11 @@ def flex_layout(context, box, max_position_y, skip_stack, containing_block,
if cross == 'height':
main_space = max_position_y - box.content_box_y()
if containing_block.height != 'auto':
assert containing_block.height.unit == 'px'
main_space = min(main_space, containing_block.height.value)
if hasattr(containing_block.height, 'unit'):
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 -
box.margin_top - box.margin_bottom -
@ -77,6 +85,8 @@ def flex_layout(context, box, max_position_y, skip_stack, containing_block,
box.border_left_width - box.border_right_width)
# Step 3
resolve_percentages(box, containing_block)
blocks.block_level_width(box, containing_block)
children = box.children
if skip_stack is not None:
assert skip_stack[1] is None
@ -90,9 +100,9 @@ def flex_layout(context, box, max_position_y, skip_stack, containing_block,
child.position_y = box.content_box_y()
child.style = child.style.copy()
resolve_percentages(box, containing_block)
flex_basis = child.style['flex_basis']
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.
@ -101,13 +111,12 @@ def flex_layout(context, box, max_position_y, skip_stack, containing_block,
if child.style[axis] == 'auto':
flex_basis = 'content'
else:
flex_basis = child.style[axis]
resolve_one_percentage(child, axis, available_main_space)
flex_basis = getattr(child, axis)
# Step 3.A
# TODO: handle percentages
if flex_basis != 'content':
assert flex_basis.unit == 'px'
child.flex_base_size = flex_basis.value
child.flex_base_size = flex_basis
# TODO: Step 3.B
# TODO: Step 3.C
@ -119,7 +128,7 @@ def flex_layout(context, box, max_position_y, skip_stack, containing_block,
if flex_basis == 'content':
child.style[axis] = 'max-content'
else:
child.style[axis] = flex_basis
child.style[axis] = Dimension(flex_basis, 'px')
# TODO: don't set style value, support *-content values instead
if child.style[axis] == 'max-content':
@ -129,10 +138,10 @@ def flex_layout(context, box, max_position_y, skip_stack, containing_block,
else:
new_child = child.copy_with_children(child.children)
new_child.width = float('inf')
new_child = blocks.block_container_layout(
new_child = blocks.block_level_layout(
context, new_child, float('inf'), skip_stack,
device_size, page_is_empty, absolute_boxes,
fixed_boxes)[0]
box, device_size, page_is_empty, absolute_boxes,
fixed_boxes, adjoining_margins=[])[0]
child.flex_base_size = new_child.height
elif child.style[axis] == 'min-content':
child.style[axis] = 'auto'
@ -141,10 +150,10 @@ def flex_layout(context, box, max_position_y, skip_stack, containing_block,
else:
new_child = child.copy_with_children(child.children)
new_child.width = 0
new_child = blocks.block_container_layout(
new_child = blocks.block_level_layout(
context, new_child, float('inf'), skip_stack,
device_size, page_is_empty, absolute_boxes,
fixed_boxes)[0]
box, device_size, page_is_empty, absolute_boxes,
fixed_boxes, adjoining_margins=[])[0]
child.flex_base_size = new_child.height
else:
assert child.style[axis].unit == 'px'
@ -259,11 +268,16 @@ def flex_layout(context, box, max_position_y, skip_stack, containing_block,
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 initial_free_space > 0
int(log10(remaining_free_space)) if remaining_free_space > 0
else -float('inf'))
if initial_magnitude < remaining_magnitude:
remaining_free_space = initial_free_space
@ -285,7 +299,9 @@ def flex_layout(context, box, max_position_y, skip_stack, containing_block,
child.scaled_flex_shrink_factor)
for child in line:
if not child.frozen:
if flex_factor_type == 'grow':
if scaled_flex_shrink_factors_sum == 0:
child.target_main_size = child.flex_base_size
elif flex_factor_type == 'grow':
ratio = (
child.style['flex_grow'] /
scaled_flex_shrink_factors_sum)
@ -329,15 +345,35 @@ def flex_layout(context, box, max_position_y, skip_stack, containing_block,
for line in flex_lines:
new_flex_line = FlexLine()
for child in line:
# TODO: Find another way than calling block_container_layout to get
# baseline and child.height
child_copy = child.copy_with_children(child.children)
if child_copy.margin_top == 'auto':
child_copy.margin_top = 0
if child_copy.margin_bottom == 'auto':
child_copy.margin_bottom = 0
blocks.block_level_width(child_copy, box)
new_child = blocks.block_level_layout(
context, child_copy,
available_cross_space + box.content_box_y(), skip_stack,
box, device_size, page_is_empty, absolute_boxes,
fixed_boxes, adjoining_margins=[])[0]
if new_child is None:
# TODO: "If the item does not have a baseline in the
# necessary axis, then one is synthesized from the flex
# items border box."
child._baseline = 0
else:
child._baseline = find_in_flow_baseline(new_child)
if cross == 'height':
child = blocks.block_container_layout(
context, child,
available_cross_space + box.content_box_y(), skip_stack,
device_size, page_is_empty, absolute_boxes, fixed_boxes)[0]
# TODO: check that
child.height = 0 if new_child is None else new_child.height
else:
child.width = min_content_width(context, child, outer=False)
if child is not None:
new_flex_line.append(child)
new_flex_line.append(child)
if new_flex_line:
new_flex_lines.append(new_flex_line)
flex_lines = new_flex_lines
@ -362,21 +398,29 @@ def flex_layout(context, box, max_position_y, skip_stack, containing_block,
cross_start_distance = 0
cross_end_distance = 0
for child in collected_items:
baseline = find_in_flow_baseline(child)
if baseline is None:
baseline = 0
else:
baseline -= child.position_y
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 = max(
child.margin_height() if cross == 'height'
else child.margin_width()
for child in 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)
# TODO: handle min/max height for single-line containers
@ -445,44 +489,56 @@ def flex_layout(context, box, max_position_y, skip_stack, containing_block,
for line in flex_lines:
position_axis = original_position_axis
free_space = getattr(box, axis) - sum(
child.margin_width() if axis == 'width'
else child.margin_height() for child in line)
if axis == 'width':
free_space = box.width
for 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 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
if free_space > 0:
margins = 0
margins = 0
for 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 child in line:
if axis == 'width':
if child.margin_left == 'auto':
margins += 1
child.margin_left = free_space
if child.margin_right == 'auto':
margins += 1
child.margin_right = free_space
else:
if child.margin_top == 'auto':
margins += 1
child.margin_top = free_space
if child.margin_bottom == 'auto':
margins += 1
if margins:
free_space /= margins
for 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
child.margin_bottom = free_space
free_space = 0
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
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
for child in line:
if axis == 'width':
@ -511,14 +567,7 @@ def flex_layout(context, box, max_position_y, skip_stack, containing_block,
align_self = box.style['align_items']
if align_self == 'baseline' and axis == 'width':
# TODO: handle vertical text
baseline = find_in_flow_baseline(child)
if baseline is None:
# TODO: "If the item does not have a baseline in the
# necessary axis, then one is synthesized from the flex
# items border box."
child.baseline = 0
else:
child.baseline = baseline - position_cross
child.baseline = child._baseline - position_cross
lower_baseline = max(lower_baseline, child.baseline)
for child in line:
cross_margins = (
@ -526,10 +575,19 @@ def flex_layout(context, box, max_position_y, skip_stack, containing_block,
else (child.margin_left, child.margin_right))
auto_margins = sum([margin == 'auto' for margin in cross_margins])
if auto_margins:
# TODO: take care of margins insead of using margin_*()
extra_cross = available_cross_space - (
child.margin_height() if cross == 'height'
else child.margin_width())
extra_cross = available_cross_space
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':
@ -636,11 +694,19 @@ def flex_layout(context, box, max_position_y, skip_stack, containing_block,
for line in flex_lines:
for child in line:
if child.is_flex_item:
new_child = blocks.block_container_layout(
context, child, max_position_y, skip_stack, device_size,
page_is_empty, absolute_boxes, fixed_boxes)[0]
new_child = blocks.block_level_layout(
context, child, max_position_y, skip_stack, box,
device_size, page_is_empty, absolute_boxes, fixed_boxes,
adjoining_margins=[])[0]
if new_child is not None:
list_marker_layout(context, new_child)
box.children.append(new_child)
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
# TODO: check these returned values
return box, resume_at, {'break': 'any', 'page': None}, [], False

View File

@ -16,6 +16,7 @@ from ..css.computed_values import ex_ratio, strut_layout
from ..formatting_structure import boxes
from ..text import can_break_text, split_first_line
from .absolute import AbsolutePlaceholder, absolute_layout
from .flex import flex_layout
from .float import avoid_collisions, float_layout
from .min_max import handle_min_max_height, handle_min_max_width
from .percentages import resolve_one_percentage, resolve_percentages
@ -609,7 +610,21 @@ def split_inline_level(context, box, position_x, max_x, skip_stack,
# Atomic inlines behave like ideographic characters.
first_letter = '\u2e80'
last_letter = '\u2e80'
# else: unexpected box type here
elif isinstance(box, boxes.InlineFlexBox):
box.position_x = position_x
box.position_y = 0
box.baseline = 0
for side in ['top', 'right', 'bottom', 'left']:
if getattr(box, 'margin_' + side) == 'auto':
setattr(box, 'margin_' + side, 0)
new_box, resume_at, _, _, _ = flex_layout(
context, box, float('inf'), skip_stack, containing_block,
device_size, False, absolute_boxes, fixed_boxes)
preserved_line_break = False
first_letter = '\u2e80'
last_letter = '\u2e80'
else: # pragma: no cover
raise TypeError('Layout for %s not handled yet' % type(box).__name__)
return new_box, resume_at, preserved_line_break, first_letter, last_letter
@ -741,6 +756,8 @@ def split_inline_box(context, box, position_x, max_x, skip_stack,
can_break = None
if last_letter is True:
last_letter = ' '
elif last_letter is False:
last_letter = ' ' # no-break space
elif box.style['white_space'] in ('pre', 'nowrap'):
can_break = False
if can_break is None:

View File

@ -508,7 +508,7 @@ def make_page(context, root_box, page_type, resume_at, page_number=None):
# 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)
assert isinstance(root_box, (boxes.BlockBox, boxes.FlexContainerBox))
context.create_block_formatting_context()
page_is_empty = True
adjoining_margins = []

View File

@ -56,6 +56,8 @@ def min_content_width(context, box, outer=True):
context, box, outer, is_line_start=True)
elif isinstance(box, boxes.ReplacedBox):
return replaced_min_content_width(box, outer)
elif isinstance(box, boxes.FlexContainerBox):
return flex_min_content_width(context, box, outer)
else:
raise TypeError(
'min-content width for %s not handled yet' %
@ -81,6 +83,8 @@ def max_content_width(context, box, outer=True):
context, box, outer, is_line_start=True)
elif isinstance(box, boxes.ReplacedBox):
return replaced_max_content_width(box, outer)
elif isinstance(box, boxes.FlexContainerBox):
return flex_max_content_width(context, box, outer)
else:
raise TypeError(
'max-content width for %s not handled yet' % type(box).__name__)
@ -650,6 +654,39 @@ def replaced_max_content_width(box, outer=True):
return adjust(box, outer, width)
def flex_min_content_width(context, box, outer=True):
"""Return the min-content width for an ``FlexContainerBox``."""
# TODO: take care of outer
# TODO: use real values, see
# https://www.w3.org/TR/css-flexbox-1/#intrinsic-sizes
min_contents = [
min_content_width(context, child, outer=True)
for child in box.children if child.is_flex_item]
if not min_contents:
return 0
if (box.style['flex_direction'].startswith('row') and
box.style['flex_wrap'] == 'nowrap'):
return sum(min_contents)
else:
return max(min_contents)
def flex_max_content_width(context, box, outer=True):
"""Return the max-content width for an ``FlexContainerBox``."""
# TODO: take care of outer
# TODO: use real values, see
# https://www.w3.org/TR/css-flexbox-1/#intrinsic-sizes
max_contents = [
max_content_width(context, child, outer=True)
for child in box.children if child.is_flex_item]
if not max_contents:
return 0
if box.style['flex_direction'].startswith('row'):
return sum(max_contents)
else:
return max(max_contents)
def trailing_whitespace_size(context, box):
"""Return the size of the trailing whitespace of ``box``."""
from .inlines import split_text_box, split_first_line

View File

@ -101,7 +101,8 @@ class StackingContext(object):
elif box.is_floated():
floats.append(StackingContext.from_box(
box, page, child_contexts))
elif isinstance(box, boxes.InlineBlockBox):
elif isinstance(
box, (boxes.InlineBlockBox, boxes.InlineFlexBox)):
# Have this fake stacking context be part of the "normal"
# box tree, because we need its position in the middle
# of a tree of inline boxes.