From 461ab52bd5d1b388283c57460d29d8d8f7ae48cf Mon Sep 17 00:00:00 2001 From: Simon Sapin Date: Fri, 20 Jan 2012 11:55:06 +0100 Subject: [PATCH] Implement page-break-before and page-break-after (without 'avoid'). --- CHANGES | 2 + weasy/css/validation.py | 24 ++++++++++++ weasy/layout/blocks.py | 31 ++++++++++++--- weasy/layout/pages.py | 55 +++++++++++++++++++-------- weasy/layout/tables.py | 6 +-- weasy/tests/test_layout.py | 77 ++++++++++++++++++++++++++++++++++++-- 6 files changed, 167 insertions(+), 28 deletions(-) diff --git a/CHANGES b/CHANGES index edb583d8..74f891dc 100644 --- a/CHANGES +++ b/CHANGES @@ -5,6 +5,8 @@ Version 0.4, released on 201X-XX-XX page-based counters. * All CSS 2.1 border styles * Fix SVG images with non-pixel units. Requires CairoSVG 0.3 +* Support for ``page-break-before`` and ``page-break-after``, except for + the value ``avoid``. Version 0.3.1, released on 2011-12-14 diff --git a/weasy/css/validation.py b/weasy/css/validation.py index 168f69df..6ca10252 100644 --- a/weasy/css/validation.py +++ b/weasy/css/validation.py @@ -510,6 +510,30 @@ def length_or_precentage(value): return value +@validator('page-break-before') +@validator('page-break-after') +@single_keyword +def page_break(keyword): + """Validation for the ``page-break-before`` and ``page-break-after`` + properties. + + """ + if keyword == 'avoid': + raise InvalidValues('value not supported yet') + return keyword in ('auto', 'always', 'left', 'right') + + +# Not very useful, might as well ignore the property anyway. +# Keep it for completeness. +@validator() +@single_keyword +def page_break_inside(keyword): + """Validation for the ``page-break-inside`` property.""" + if keyword == 'avoid': + raise InvalidValues('value not supported yet') + return keyword ('auto',) + + @validator() @single_keyword def position(keyword): diff --git a/weasy/layout/blocks.py b/weasy/layout/blocks.py index 89ed939d..6ad5e01a 100644 --- a/weasy/layout/blocks.py +++ b/weasy/layout/blocks.py @@ -50,8 +50,9 @@ def block_level_layout(document, box, max_position_y, skip_stack, return block_box_layout(document, box, max_position_y, skip_stack, containing_block, device_size, page_is_empty) elif isinstance(box, boxes.BlockReplacedBox): - return block_replaced_box_layout( - box, containing_block, device_size), None + box = block_replaced_box_layout( + box, containing_block, device_size) + return box, None, 'any' else: raise TypeError('Layout for %s not handled yet' % type(box).__name__) @@ -142,6 +143,7 @@ def block_level_width(box, containing_block): box.margin_right = margin_sum - margin_l +# TODO: rename this to block_container_something def block_level_height(document, box, max_position_y, skip_stack, device_size, page_is_empty): """Set the ``box`` height.""" @@ -160,6 +162,7 @@ def block_level_height(document, box, max_position_y, skip_stack, initial_position_y = position_y new_children = [] + next_page = 'any' if skip_stack is None: skip = 0 @@ -191,7 +194,7 @@ def block_level_height(document, box, max_position_y, skip_stack, if new_position_y > max_position_y and not page_is_empty: if not new_children: # Page break before any content, cancel the whole box. - return None, None + return None, None, 'any' # Page break here, resume before this line resume_at = (index, skip_stack) is_page_break = True @@ -205,8 +208,17 @@ def block_level_height(document, box, max_position_y, skip_stack, if is_page_break: break else: + page_break = child.style.page_break_before + if page_break in ('always', 'left', 'right'): + next_page = 'any' if page_break == 'always' else page_break + resume_at = (index, None) + # Force break only once + # TODO: refactor to avoid doing this? + child.style.page_break_before = 'auto' + break + new_containing_block = box - new_child, resume_at = block_level_layout( + new_child, resume_at, next_page = block_level_layout( document, child, max_position_y, skip_stack, new_containing_block, device_size, page_is_empty) skip_stack = None @@ -217,7 +229,7 @@ def block_level_height(document, box, max_position_y, skip_stack, else: # This was the first child of this box, cancel the box # completly - return None, None + return None, None, 'any' new_position_y = position_y + new_child.margin_height() # Bottom borders may overflow here # TODO: back-track somehow when all lines fit but not borders @@ -227,6 +239,13 @@ def block_level_height(document, box, max_position_y, skip_stack, if resume_at is not None: resume_at = (index, resume_at) break + + page_break = child.style.page_break_after + if page_break in ('always', 'left', 'right'): + next_page = 'any' if page_break == 'always' else page_break + # Resume after this + resume_at = (index + 1, None) + break else: resume_at = None @@ -242,7 +261,7 @@ def block_level_height(document, box, max_position_y, skip_stack, box.outside_list_marker = None box.reset_spacing('top') new_box.reset_spacing('bottom') - return new_box, resume_at + return new_box, resume_at, next_page def block_table_wrapper(document, wrapper, max_position_y, skip_stack, diff --git a/weasy/layout/pages.py b/weasy/layout/pages.py index e996f980..09250e1b 100644 --- a/weasy/layout/pages.py +++ b/weasy/layout/pages.py @@ -430,13 +430,30 @@ def make_margin_boxes(document, page, counter_values): # content_to_boxes() only produces inline-level boxes, no need to # run other post-processors from build.build_formatting_structure() box = build.inline_in_block(box) - box, resume_at = block_level_height(document, box, + box, resume_at, next_page = block_level_height(document, box, max_position_y=float('inf'), skip_stack=None, device_size=page.style.size, page_is_empty=True) assert resume_at is None yield box +def make_empty_page(document, page_type): + root_box = document.formatting_structure + style = document.style_for(page_type) + page = boxes.PageBox(page_type, style, root_box.style.direction) + + device_size = page.style.size + page.outer_width, page.outer_height = device_size + + resolve_percentages(page, device_size) + + page.position_x = 0 + page.position_y = 0 + page.width = page.outer_width - page.horizontal_surroundings() + page.height = page.outer_height - page.vertical_surroundings() + return page + + def make_page(document, page_type, resume_at): """Take just enough content from the beginning to fill one page. @@ -450,19 +467,9 @@ def make_page(document, page_type, resume_at): or ``None`` for the first page. """ + page = make_empty_page(document, page_type) root_box = document.formatting_structure - style = document.style_for(page_type) - page = boxes.PageBox(page_type, style, root_box.style.direction) - device_size = page.style.size - page.outer_width, page.outer_height = device_size - - resolve_percentages(page, device_size) - - page.position_x = 0 - page.position_y = 0 - page.width = page.outer_width - page.horizontal_surroundings() - page.height = page.outer_height - page.vertical_surroundings() root_box.position_x = page.content_box_x() root_box.position_y = page.content_box_y() @@ -472,25 +479,41 @@ def make_page(document, page_type, resume_at): # 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) - root_box, resume_at = block_level_layout( + root_box, resume_at, next_page = block_level_layout( document, root_box, page_content_bottom, resume_at, initial_containing_block, device_size, page_is_empty=True) assert root_box page = page.copy_with_children([root_box]) - return page, resume_at + return page, resume_at, next_page def make_all_pages(document): """Return a list of laid out pages without margin boxes.""" root_box = document.formatting_structure prefix = 'first_' - right_page = root_box.style.direction == 'ltr' + + # Special case the root box + page_break = root_box.style.page_break_before + if page_break == 'right': + right_page = True + if page_break == 'left': + right_page = False + else: + right_page = root_box.style.direction == 'ltr' + resume_at = None + next_page = 'any' while True: page_type = prefix + ('right_page' if right_page else 'left_page') - page, resume_at = make_page(document, page_type, resume_at) + if ((next_page == 'left' and right_page) or + (next_page == 'right' and not right_page)): + page = make_empty_page(document, page_type) + else: + page, resume_at, next_page = make_page( + document, page_type, resume_at) + assert next_page yield page if resume_at is None: return diff --git a/weasy/layout/tables.py b/weasy/layout/tables.py index c30b916d..aa1ba8df 100644 --- a/weasy/layout/tables.py +++ b/weasy/layout/tables.py @@ -104,7 +104,7 @@ def table_layout(document, table, max_position_y, containing_block, # The computed height is a minimum computed_cell_height = cell.height cell.height = 'auto' - cell, _ = block_level_height( + cell, _, _ = block_level_height( document, cell, max_position_y=float('inf'), skip_stack=None, @@ -216,9 +216,9 @@ def table_layout(document, table, max_position_y, containing_block, and not page_is_empty): # If the table does not fit, put it on the next page. # (No page break inside tables yet.) - return None, None + return None, None, 'any' else: - return table, None + return table, None, 'any' def add_top_padding(box, extra_padding): diff --git a/weasy/tests/test_layout.py b/weasy/tests/test_layout.py index d94fdccc..b7ffc4c7 100644 --- a/weasy/tests/test_layout.py +++ b/weasy/tests/test_layout.py @@ -521,16 +521,20 @@ def test_forced_line_breaks(): assert [line.height for line in lines] == [42] * 7 -#@SUITE.test +@SUITE.test def test_page_breaks(): """Test the page breaks.""" pages = parse(''' -
+
1
+
2
+
3
+
4
+
5
''') page_divs = [] for page in pages: @@ -542,6 +546,73 @@ def test_page_breaks(): positions_y = [[div.position_y for div in divs] for divs in page_divs] assert positions_y == [[10, 40], [10, 40], [10]] + # Same as above, but no content inside each
. + # TODO: This currently gives no page break. Should it? +# pages = parse(''' +# +#
+# ''') +# page_divs = [] +# for page in pages: +# divs = body_children(page) +# assert all([div.element_tag == 'div' for div in divs]) +# assert all([div.position_x == 10 for div in divs]) +# page_divs.append(divs) + +# positions_y = [[div.position_y for div in divs] for divs in page_divs] +# assert positions_y == [[10, 40], [10, 40], [10]] + + page_1, page_2, page_3, page_4 = parse(''' + +
1
+

2

+

3

+
  • 4
+ ''') + + # The first page is a right page on rtl, but not here because of + # page-break-before on the root element. + assert page_1.margin_left == 50 # left page + assert page_1.margin_right == 10 + html, = page_1.children + body, = html.children + div, = body.children + line, = div.children + text, = line.children + assert div.element_tag == 'div' + assert text.text == '1' + + assert page_2.margin_left == 10 + assert page_2.margin_right == 50 # right page + assert not page_2.children # empty page to get to a left page + + assert page_3.margin_left == 50 # left page + assert page_3.margin_right == 10 + html, = page_3.children + body, = html.children + p_1, p_2 = body.children + assert p_1.element_tag == 'p' + assert p_2.element_tag == 'p' + + assert page_4.margin_left == 10 + assert page_4.margin_right == 50 # right page + html, = page_4.children + body, = html.children + ulist, = body.children + assert ulist.element_tag == 'ul' + @SUITE.test def test_inlinebox_spliting():