diff --git a/weasyprint/layout/absolute.py b/weasyprint/layout/absolute.py index b4af187f..0a36b50e 100644 --- a/weasyprint/layout/absolute.py +++ b/weasyprint/layout/absolute.py @@ -17,11 +17,39 @@ from .tables import table_wrapper_width from ..formatting_structure import boxes -def absolute_layout(document, box, containing_block): +class AbsolutePlaceholder(object): + """Left where an absolutely-positioned box was taken out of the flow.""" + def __init__(self, box): + # Work around the overloaded __setattr__ + object.__setattr__(self, '_box', box) + object.__setattr__(self, '_layout_done', False) + + def set_laid_out_box(self, new_box): + object.__setattr__(self, '_box', new_box) + object.__setattr__(self, '_layout_done', True) + + def translate(self, dx=0, dy=0): + if self._layout_done: + self._box.translate(dx, dy) + else: + # Descendants do not have a position yet. + self._box.position_x += dx + self._box.position_y += dy + + # Pretend to be the box itself + def __getattr__(self, name): + return getattr(self._box, name) + + def __setattr__(self, name, value): + setattr(self._box, name, value) + + +def absolute_layout(document, placeholder, containing_block): """Set the width of absolute positioned ``box``.""" # TODO: avoid this (circular import) from .blocks import block_container_layout + box = placeholder._box resolve_percentages(box, containing_block) resolve_position_percentages(box, containing_block) @@ -156,7 +184,7 @@ def absolute_layout(document, box, containing_block): # TODO: handle absolute tables assert isinstance(box, boxes.BlockBox) - # New containing block, use a new absolute list + # This box is the containing block for absolute descendants. absolute_boxes = [] if box.is_table_wrapper: @@ -167,37 +195,16 @@ def absolute_layout(document, box, containing_block): document, box, max_position_y=float('inf'), skip_stack=None, device_size=None, page_is_empty=False, absolute_boxes=absolute_boxes, adjoining_margins=None) - box.__dict__ = new_box.__dict__ - list_marker_layout(document, box) + list_marker_layout(document, new_box) - for absolute_box in absolute_boxes: - absolute_layout(document, absolute_box, new_box) + for child_placeholder in absolute_boxes: + absolute_layout(document, child_placeholder, new_box) if translate_box_width: translate_x -= new_box.width if translate_box_height: translate_y -= new_box.height - translate_except_absolute(new_box, translate_x, translate_y) + new_box.translate(translate_x, translate_y) - -def translate_except_absolute(box, dx=0, dy=0): - """Change the position of the box, except its absolute descandants. - - Also update the children’s positions accordingly. - - """ - box.position_x += dx - box.position_y += dy - - marker = getattr(box, 'outside_list_marker', None) - if marker: - translate_except_absolute(marker, dx, dy) - - if box.style.position in ('static', 'relative'): - if isinstance(box, boxes.ParentBox): - for child in box.children: - translate_except_absolute(child, dx, dy) - if isinstance(box, boxes.TableBox): - for child in box.column_groups: - translate_except_absolute(child, dx, dy) + placeholder.set_laid_out_box(new_box) diff --git a/weasyprint/layout/blocks.py b/weasyprint/layout/blocks.py index 3fddd7e5..a6759e68 100644 --- a/weasyprint/layout/blocks.py +++ b/weasyprint/layout/blocks.py @@ -12,7 +12,7 @@ from __future__ import division, unicode_literals -from .absolute import absolute_layout, translate_except_absolute +from .absolute import absolute_layout, AbsolutePlaceholder from .inlines import (iter_line_boxes, replaced_box_width, replaced_box_height, handle_min_max_width, min_max_replaced_height, min_max_auto_replaced) @@ -174,7 +174,7 @@ def relative_positioning(box, containing_block): else: translate_y = 0 - translate_except_absolute(box, translate_x, translate_y) + box.translate(translate_x, translate_y) if isinstance(box, (boxes.InlineBox, boxes.LineBox)): for child in box.children: @@ -232,13 +232,14 @@ def block_container_layout(document, box, max_position_y, skip_stack, child.position_y = position_y if not child.is_in_normal_flow(): - if child.style.position == 'absolute': + if child.style.position in ('absolute', 'fixed'): child.position_y += collapse_margin(adjoining_margins) - absolute_boxes.append(child) - new_children.append(child) - elif child.style.position == 'fixed': - child.position_y += collapse_margin(adjoining_margins) - document.fixed_boxes.append(child) + placeholder = AbsolutePlaceholder(child) + if child.style.position == 'absolute': + absolute_boxes.append(placeholder) + new_children.append(placeholder) + else: + document.fixed_boxes.append(placeholder) else: # TODO: Floats new_children.append(child) @@ -327,7 +328,7 @@ def block_container_layout(document, box, max_position_y, skip_stack, adjoining_margins.append(new_child.margin_top) offset_y = (collapse_margin(adjoining_margins) - new_child.margin_top) - translate_except_absolute(new_child, 0, offset_y) + new_child.translate(0, offset_y) adjoining_margins = [] #else: blocks handle that themselves. diff --git a/weasyprint/layout/inlines.py b/weasyprint/layout/inlines.py index e0040980..eed3738d 100644 --- a/weasyprint/layout/inlines.py +++ b/weasyprint/layout/inlines.py @@ -15,7 +15,7 @@ import functools import cairo -from .absolute import absolute_layout, translate_except_absolute +from .absolute import absolute_layout, AbsolutePlaceholder from .markers import image_marker_layout from .percentages import resolve_percentages, resolve_one_percentage from .preferred import shrink_to_fit @@ -71,12 +71,12 @@ def get_next_linebox(document, linebox, position_y, skip_stack, if skip_stack == 'continue': return None, None - new_absolute_boxes = [] + line_placeholders = [] resolve_percentages(linebox, containing_block) line, resume_at, preserved_line_break = split_inline_box( document, linebox, position_x, max_x, skip_stack, containing_block, - device_size, new_absolute_boxes) + device_size, absolute_boxes, line_placeholders) remove_last_whitespace(document, line) @@ -104,12 +104,10 @@ def get_next_linebox(document, linebox, position_y, skip_stack, line.margin_bottom = 0 if offset_x != 0 or offset_y != 0: # This also translates children - translate_except_absolute(line, offset_x, offset_y) + line.translate(offset_x, offset_y) - for absolute_box in new_absolute_boxes: - absolute_box.position_y = position_y - - absolute_boxes.extend(new_absolute_boxes) + for placeholder in line_placeholders: + placeholder.position_y = position_y return line, resume_at @@ -456,7 +454,8 @@ def inline_block_width(box, containing_block): def split_inline_level(document, box, position_x, max_x, skip_stack, - containing_block, device_size, absolute_boxes): + containing_block, device_size, absolute_boxes, + line_placeholders): """Fit as much content as possible from an inline-level box in a width. Return ``(new_box, resume_at)``. ``resume_at`` is ``None`` if all of the @@ -492,7 +491,7 @@ def split_inline_level(document, box, position_x, max_x, skip_stack, box.margin_right = 0 new_box, resume_at, preserved_line_break = split_inline_box( document, box, position_x, max_x, skip_stack, containing_block, - device_size, absolute_boxes) + device_size, absolute_boxes, line_placeholders) elif isinstance(box, boxes.AtomicInlineLevelBox): new_box = atomic_box( document, box, position_x, skip_stack, containing_block, @@ -505,7 +504,8 @@ def split_inline_level(document, box, position_x, max_x, skip_stack, def split_inline_box(document, box, position_x, max_x, skip_stack, - containing_block, device_size, absolute_boxes): + containing_block, device_size, absolute_boxes, + line_placeholders): """Same behavior as split_inline_level.""" initial_position_x = position_x assert isinstance(box, (boxes.LineBox, boxes.InlineBox)) @@ -527,27 +527,28 @@ def split_inline_box(document, box, position_x, max_x, skip_stack, if box.style.position == 'relative': absolute_boxes = [] + line_placeholders = [] for index, child in box.enumerate_skip(skip): + child.position_y = box.position_y if not child.is_in_normal_flow(): - if child.style.position == 'absolute': + if child.style.position in ('absolute', 'fixed'): child.position_x = position_x - child.position_y = 0 - absolute_boxes.append(child) - children.append(child) - elif child.style.position == 'fixed': - child.position_x = position_x - child.position_y = 0 - document.fixed_boxes.append(child) + placeholder = AbsolutePlaceholder(child) + line_placeholders.append(placeholder) + if child.style.position == 'absolute': + absolute_boxes.append(placeholder) + children.append(placeholder) + else: + document.fixed_boxes.append(placeholder) else: # TODO: Floats children.append(child) continue - child.position_y = box.position_y new_child, resume_at, preserved = split_inline_level( document, child, position_x, max_x, skip_stack, - containing_block, device_size, absolute_boxes) + containing_block, device_size, absolute_boxes, line_placeholders) skip_stack = None if preserved: preserved_line_break = True diff --git a/weasyprint/layout/preferred.py b/weasyprint/layout/preferred.py index e2b42ad8..e57cfc30 100644 --- a/weasyprint/layout/preferred.py +++ b/weasyprint/layout/preferred.py @@ -74,7 +74,8 @@ def _block_preferred_width(box, function, outer): # though they were the following: width: auto" # http://dbaron.org/css/intrinsic/#outer-intrinsic if box.children: - width = max(function(child, outer=True) for child in box.children) + width = max(function(child, outer=True) for child in box.children + if child.is_in_normal_flow()) else: width = 0 else: @@ -136,6 +137,9 @@ def inline_preferred_minimum_width(box, outer=True): """Return the preferred minimum width for an ``InlineBox``.""" widest_line = 0 for child in box.children: + if not child.is_in_normal_flow(): + continue # Skip + if isinstance(child, boxes.InlineReplacedBox): # Images are on their own line current_line = replaced_preferred_width(child) @@ -156,6 +160,9 @@ def inline_preferred_width(box, outer=True): widest_line = 0 current_line = 0 for child in box.children: + if not child.is_in_normal_flow(): + continue # Skip + if isinstance(child, boxes.InlineReplacedBox): # No line break around images current_line += replaced_preferred_width(child) @@ -313,7 +320,8 @@ def table_and_columns_preferred_widths(box, outer=True, sum(column_preferred_minimum_widths) + total_border_spacing) table_preferred_width = sum(column_preferred_widths) + total_border_spacing - captions = [child for child in box.children if child != table] + captions = [child for child in box.children + if child is not table and child.is_in_normal_flow()] if captions: caption_width = max( diff --git a/weasyprint/tests/test_layout.py b/weasyprint/tests/test_layout.py index 44f35a42..fdc489c2 100644 --- a/weasyprint/tests/test_layout.py +++ b/weasyprint/tests/test_layout.py @@ -39,7 +39,7 @@ def parse_without_layout(html_content): def parse(html_content, return_document=False): """Parse some HTML, apply stylesheets, transform to boxes and lay out.""" - # TODO: remove this patching when asbolute and floats are validated + # TODO: remove this patching when floats are validated with monkeypatch_validation(validate_float): document = TestPNGDocument(html_content, base_url=resource_filename('')) @@ -1503,7 +1503,7 @@ def test_inlinebox_spliting(): while 1: inlinebox.position_y = 0 box, skip, _ = split_inline_box( - document, inlinebox, 0, width, skip, parent, None, []) + document, inlinebox, 0, width, skip, parent, None, [], []) yield box if skip is None: break @@ -1617,7 +1617,7 @@ def test_inlinebox_text_after_spliting(): while 1: inlinebox.position_y = 0 box, skip, _ = split_inline_box( - document, inlinebox, 0, 100, skip, paragraph, None, []) + document, inlinebox, 0, 100, skip, paragraph, None, [], []) parts.append(box) if skip is None: break @@ -3559,6 +3559,7 @@ def test_absolute_positioning(): page, = parse(''' @@ -3567,7 +3568,8 @@ def test_absolute_positioning():

2

3

4

-

5 @@ -3576,7 +3578,7 @@ def test_absolute_positioning(): width: 20px; height: 15px">

-

6

+

6

7

''') @@ -3592,22 +3594,25 @@ def test_absolute_positioning(): assert (p2.position_x, p2.position_y) == (0, 20) assert (p3.position_x, p3.position_y) == (5, -5) assert (p4.position_x, p4.position_y) == (0, 40) - # p5 y = p4 y + p4 margin height - p5 bottom - margin collapsing - # = 40 + 26 - 5 - 3 - # = 58 - assert (p5.position_x, p5.position_y) == (5, 58) - assert (img.position_x, img.position_y) == (15, 68) - assert (span2.position_x, span2.position_y) == (19, 68) - # span3 x = p5 right - p5 right margin - span width - span right - # = 105 - 7 - 20 - 5 - # = 73 + # p5 x = page width - right - margin/padding/border - width + # = 1000 - 15 - 2 * 10 - 50 + # = 915 + # p5 y = page height - bottom - margin/padding/border - height + # = 2000 - 5 - 2 * 10 - 200 + # = 1775 + assert (p5.position_x, p5.position_y) == (915, 1775) + assert (img.position_x, img.position_y) == (925, 1785) + assert (span2.position_x, span2.position_y) == (929, 1785) + # span3 x = p5 right - p5 margin - span width - span right + # = 985 - 7 - 20 - 5 + # = 953 # span3 y = p5 y + p5 margin top + span top - # = 58 + 7 + -10 - # = 55 - assert (span3.position_x, span3.position_y) == (73, 55) - # p6 y = p4 y + p4 margin height + p5 margin height - margin collapsing - # = 40 + 26 + 40 - 3 - # = 103 - assert (p6.position_x, p6.position_y) == (0, 103) - assert (p7.position_x, p7.position_y) == (0, 123) - assert div.height == 103 + # = 1775 + 7 + -10 + # = 1772 + assert (span3.position_x, span3.position_y) == (953, 1772) + # p6 y = p4 y + p4 margin height - margin collapsing + # = 40 + 26 - 3 + # = 63 + assert (p6.position_x, p6.position_y) == (0, 63) + assert div.height == 71 # 20*3 + 2*3 + 8 - 3 + assert (p7.position_x, p7.position_y) == (0, 91)