diff --git a/weasy/css/__init__.py b/weasy/css/__init__.py index c34536d2..e60a77fe 100644 --- a/weasy/css/__init__.py +++ b/weasy/css/__init__.py @@ -580,17 +580,3 @@ def get_all_computed_styles(document, medium, element, pseudo_type) return computed_styles - - -def page_style(document, page_number): - """Return the StyleDict for a page box.""" - # First page is a right page. - # TODO: this "should depend on the major writing direction of the - # document". - first_is_right = True - - is_right = (page_number % 2) == (1 if first_is_right else 0) - page_type = 'right_page' if is_right else 'left_page' - if page_number == 1: - page_type = 'first_' + page_type - return document.computed_styles[page_type, None] diff --git a/weasy/css/computed_values.py b/weasy/css/computed_values.py index 781a6490..b7900710 100644 --- a/weasy/css/computed_values.py +++ b/weasy/css/computed_values.py @@ -280,15 +280,12 @@ def border_width(computer, name, value): @register_computer('content') def content(computer, name, values): """Compute the ``content`` property.""" - if computer.pseudo_type in ('before', 'after'): - if values in ('normal', 'none'): - return 'none' - else: - return [('STRING', computer.element.get(value, '')) - if type_ == 'attr' else (type_, value) - for type_, value in values] + if values in ('normal', 'none'): + return values else: - return 'normal' + return [('STRING', computer.element.get(value, '')) + if type_ == 'attr' else (type_, value) + for type_, value in values] @register_computer('display') diff --git a/weasy/draw.py b/weasy/draw.py index e9dce360..d8649d89 100644 --- a/weasy/draw.py +++ b/weasy/draw.py @@ -115,9 +115,9 @@ def draw_page_background(document, context, page): # TODO: more tests for this, see # http://www.w3.org/TR/css3-page/#page-properties draw_background(document, context, page, clip=False) - # Margin boxes come before the content for painting order, - # so the box for the root element is the last child. - root_box = page.children[-1] + # Margin boxes come after the content for painting order, + # so the box for the root element is the first child. + root_box = page.children[0] if has_background(root_box): draw_background(document, context, root_box, clip=False) elif root_box.element_tag.lower() == 'html': diff --git a/weasy/formatting_structure/boxes.py b/weasy/formatting_structure/boxes.py index 0554e029..8193f637 100644 --- a/weasy/formatting_structure/boxes.py +++ b/weasy/formatting_structure/boxes.py @@ -89,7 +89,7 @@ class Box(object): self.style = style.copy() def __repr__(self): - return '<%s %s %i>' % ( + return '<%s %s %s>' % ( type(self).__name__, self.element_tag, self.sourceline) @classmethod @@ -262,32 +262,6 @@ class ParentBox(Box): child.translate(dx, dy) -class PageBox(ParentBox): - """Box for a page. - - Initially the whole document will be in the box for the root element. - During layout a new page box is created after every page break. - - """ - def __init__(self, page_number, style, direction): - self._direction = direction - # starting at 1 for the first page. - self.page_number = page_number - # Page boxes are not linked to any element. - super(PageBox, self).__init__( - element_tag=None, sourceline=None, style=style, children=[]) - - def __repr__(self): - return '<%s %s>' % (type(self).__name__, self.page_number) - - @property - def direction(self): - """ - The direction of the page box (containing block to the root element) - is that of the root element. - """ - return self._direction - class BlockLevelBox(Box): """A box that participates in an block formatting context. @@ -525,3 +499,42 @@ class TableCaptionBox(BlockBox): proper_table_child = True internal_table_or_caption = True proper_parents = (TableBox, InlineTableBox) + + +class PageBox(ParentBox): + """Box for a page. + + Initially the whole document will be in the box for the root element. + During layout a new page box is created after every page break. + + """ + def __init__(self, page_number, style, direction): + self._direction = direction + # starting at 1 for the first page. + self.page_number = page_number + # Page boxes are not linked to any element. + super(PageBox, self).__init__( + element_tag=None, sourceline=None, style=style, children=[]) + + def __repr__(self): + return '<%s %s>' % (type(self).__name__, self.page_number) + + @property + def direction(self): + """ + The direction of the page box (containing block to the root element) + is that of the root element. + """ + return self._direction + + +class MarginBox(BlockContainerBox): + """Box in page margins, as defined in CSS3 Paged Media""" + def __init__(self, at_keyword, style, children=[]): + self.at_keyword = at_keyword + # Margin boxes are not linked to any element. + super(MarginBox, self).__init__( + element_tag=None, sourceline=None, style=style, children=children) + + def __repr__(self): + return '<%s %s>' % (type(self).__name__, self.at_keyword) diff --git a/weasy/formatting_structure/build.py b/weasy/formatting_structure/build.py index 14e7ffb6..d0486230 100644 --- a/weasy/formatting_structure/build.py +++ b/weasy/formatting_structure/build.py @@ -153,7 +153,7 @@ def pseudo_to_box(document, state, element, pseudo_type): # differ from the computer value? display = style.display content = style.content - if 'none' in (display, content): + if 'none' in (display, content) or content == 'normal': return box = BOX_TYPE_FROM_DISPLAY[display]( @@ -164,8 +164,16 @@ def pseudo_to_box(document, state, element, pseudo_type): children = [] if display == 'list-item': children.extend(add_box_marker(document, counter_values, box)) + children.extend(content_to_boxes( + document, style, box, quote_depth, counter_values)) + + yield box.copy_with_children(children) + + +def content_to_boxes(document, style, parent_box, quote_depth, counter_values): + """Takes the value of a ``content`` property and yield boxes.""" texts = [] - for type_, value in content: + for type_, value in style.content: if type_ == 'STRING': texts.append(value) elif type_ == 'URI': @@ -173,10 +181,9 @@ def pseudo_to_box(document, state, element, pseudo_type): if image is not None: text = u''.join(texts) if text: - children.append(boxes.TextBox.anonymous_from(box, text)) + yield boxes.TextBox.anonymous_from(parent_box, text) texts = [] - children.append(boxes.InlineReplacedBox.anonymous_from( - box, image)) + yield boxes.InlineReplacedBox.anonymous_from(parent_box, image) elif type_ == 'counter': counter_name, counter_style = value counter_value = counter_values.get(counter_name, [0])[-1] @@ -200,9 +207,7 @@ def pseudo_to_box(document, state, element, pseudo_type): quote_depth[0] += 1 text = u''.join(texts) if text: - children.append(boxes.TextBox.anonymous_from(box, text)) - - yield box.copy_with_children(children) + yield boxes.TextBox.anonymous_from(parent_box, text) def update_counters(state, style): diff --git a/weasy/layout/inlines.py b/weasy/layout/inlines.py index e5f7bb4e..8429c129 100644 --- a/weasy/layout/inlines.py +++ b/weasy/layout/inlines.py @@ -415,7 +415,7 @@ def split_text_box(document, box, available_width, skip): assert isinstance(box, boxes.TextBox) font_size = box.style.font_size text = box.text[skip:] - if font_size == 0 or available_width <= 0 or not text: + if font_size == 0 or not text: return None, None, False fragment = TextFragment(text, box.style, diff --git a/weasy/layout/pages.py b/weasy/layout/pages.py index 0393d6e5..31d19f7a 100644 --- a/weasy/layout/pages.py +++ b/weasy/layout/pages.py @@ -24,14 +24,81 @@ Layout for page boxes and margin boxes. from __future__ import division -from .. import css -from .blocks import block_level_layout +from .blocks import block_level_layout, block_level_height from .percentages import resolve_percentages -from ..formatting_structure import boxes +from ..formatting_structure import boxes, build -def make_margin_boxes(document, page_number): - return [] +MARGIN_BOXES = ( + # Order recommended on http://dev.w3.org/csswg/css3-page/#painting + # center/middle on top (last in tree order), then corner, ther others + '@top-left', + '@top-right', + '@bottom-left', + '@bottom-right', + '@left-top', + '@right-bottom', + '@right-top', + '@right-bottom' + + '@top-left-corner', + '@top-right-corner', + '@bottom-left-corner', + '@bottom-right-corner', + + '@bottom-center', + '@top-center', + '@left-middle', + '@right-middle', +) + + +def empty_margin_boxes(document, page, page_type): + for at_keyword in MARGIN_BOXES: + style = document.style_for(page_type, at_keyword) + if style is not None and style.content not in ('normal', 'none'): + box = boxes.MarginBox(at_keyword, style) + # TODO: Actual layout + box.position_x = 0 + box.position_y = 0 + containing_block = 0, 0 + resolve_percentages(box, containing_block) + for name in ['width', 'height', 'margin_top', 'margin_bottom', + 'margin_left', 'margin_right']: + if getattr(box, name) == 'auto': + setattr(box, name, 0) + yield box + + +def make_margin_boxes(document, page, page_type): + for box in empty_margin_boxes(document, page, page_type): + # TODO: get actual counter values at the time of the last page break + counter_values = {} + quote_depth = [0] + children = build.content_to_boxes( + document, box.style, page, quote_depth, counter_values) + box = box.copy_with_children(children) + # content_to_boxes() only produces inline-level boxes + box = build.inline_in_block(box) + box, resume_at = 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 page_type_for_number(page_number): + """Return a page type such as ``'first_right_page'`` from a page number.""" + # First page is a right page. + # TODO: this "should depend on the major writing direction of the + # document". + first_is_right = True + + is_right = (page_number % 2) == (1 if first_is_right else 0) + page_type = 'right_page' if is_right else 'left_page' + if page_number == 1: + page_type = 'first_' + page_type + return page_type def make_page(document, page_number, resume_at): @@ -42,15 +109,14 @@ def make_page(document, page_number, resume_at): """ root_box = document.formatting_structure - style = css.page_style(document, page_number) + page_type = page_type_for_number(page_number) + style = document.style_for(page_type) page = boxes.PageBox(page_number, style, root_box.style.direction) device_size = page.style.size page.outer_width, page.outer_height = device_size - sheet = lambda: 1 # dummy object holding attributes. - sheet.width, sheet.height = device_size - resolve_percentages(page, sheet) + resolve_percentages(page, device_size) page.position_x = 0 page.position_y = 0 @@ -70,8 +136,8 @@ def make_page(document, page_number, resume_at): initial_containing_block, device_size, page_is_empty=True) assert root_box - children = make_margin_boxes(document, page_number) - children.append(root_box) + children = [root_box] + children.extend(make_margin_boxes(document, page, page_type)) page = page.copy_with_children(children) diff --git a/weasy/layout/percentages.py b/weasy/layout/percentages.py index 2e555329..54c91030 100644 --- a/weasy/layout/percentages.py +++ b/weasy/layout/percentages.py @@ -61,9 +61,12 @@ def resolve_one_percentage(box, property_name, refer_to, def resolve_percentages(box, containing_block): """Set used values as attributes of the box object.""" - # cb = containing block - cb_width = containing_block.width - cb_height = containing_block.height + if isinstance(containing_block, boxes.Box): + # cb is short for containing block + cb_width = containing_block.width + cb_height = containing_block.height + else: + cb_width, cb_height = containing_block if isinstance(box, boxes.PageBox): maybe_height = cb_height else: diff --git a/weasy/tests/test_boxes.py b/weasy/tests/test_boxes.py index 98050884..46cacf72 100644 --- a/weasy/tests/test_boxes.py +++ b/weasy/tests/test_boxes.py @@ -26,8 +26,9 @@ import contextlib from attest import Tests, assert_hook # pylint: disable=W0611 from .testing_utils import resource_filename, TestPNGDocument, assert_no_logs -from ..css import validation, page_style +from ..css import validation from ..formatting_structure import boxes, build, counters +from ..layout.pages import page_type_for_number SUITE = Tests() @@ -392,7 +393,8 @@ def test_page_style(): def assert_page_margins(page_number, top, right, bottom, left): """Check the page margin values.""" - style = page_style(document, page_number) + page_type = page_type_for_number(page_number) + style = document.style_for(page_type) assert style.margin_top == top assert style.margin_right == right assert style.margin_bottom == bottom @@ -710,6 +712,21 @@ def test_colspan_rowspan(): @SUITE.test def test_before_after(): """Test the :before and :after pseudo-elements.""" + assert_tree(parse_all(''' + +

+
+
+ '''), [ + # No content in pseudo-element, no box generated + ('p', 'Block', []), + ('div', 'Block', []), + ('section', 'Block', [])]) + assert_tree(parse_all(''' +

lorem ipsum + ''') + page_1, page_2 = document.pages + assert page_1.children[0].element_tag == 'html' + assert page_2.children[0].element_tag == 'html' + + margin_boxes_1 = [box.at_keyword for box in page_1.children[1:]] + margin_boxes_2 = [box.at_keyword for box in page_2.children[1:]] + # Order matters, see http://dev.w3.org/csswg/css3-page/#painting + assert margin_boxes_1 == ['@bottom-left', '@bottom-left-corner', + '@top-center'] + assert margin_boxes_2 == ['@top-center'] + + html, top_center = page_2.children + line_box, = top_center.children + text_box, = line_box.children + assert text_box.text == 'Title' diff --git a/weasy/tests/test_layout.py b/weasy/tests/test_layout.py index 05ad360a..527caa96 100644 --- a/weasy/tests/test_layout.py +++ b/weasy/tests/test_layout.py @@ -451,28 +451,30 @@ def test_linebox_text(): @SUITE.test def test_linebox_positions(): """Test the position of line boxes.""" - page = u''' - -

this is test for Weasyprint

''' - page, = parse(page % {'fonts': FONTS, 'width': 165}) - paragraph, = body_children(page) - lines = list(paragraph.children) - assert len(lines) == 2 + for width, expected_lines in [(165, 2), (1, 5), (0, 5)]: + page = u''' + +

this is test for Weasyprint

''' + page, = parse(page % {'fonts': FONTS, 'width': width}) + paragraph, = body_children(page) + lines = list(paragraph.children) + assert len(lines) == expected_lines - ref_position_y = lines[0].position_y - ref_position_x = lines[0].position_x - for line in lines: - assert ref_position_y == line.position_y - assert ref_position_x == line.position_x - for box in line.children: - assert ref_position_x == box.position_x - ref_position_x += box.width - assert ref_position_y == box.position_y - assert ref_position_x - line.position_x <= line.width - ref_position_x = line.position_x - ref_position_y += line.height + ref_position_y = lines[0].position_y + ref_position_x = lines[0].position_x + for line in lines: + assert ref_position_y == line.position_y + assert ref_position_x == line.position_x + for box in line.children: + assert ref_position_x == box.position_x + ref_position_x += box.width + assert ref_position_y == box.position_y + assert ref_position_x - line.position_x <= line.width + ref_position_x = line.position_x + ref_position_y += line.height @SUITE.test @@ -605,6 +607,11 @@ def test_inlinebox_spliting(): assert len(parts) > 1 assert original_text == get_joined_text(parts) + # test with width = 0 + parts = list(get_parts(document, inlinebox, 0, parent)) + assert len(parts) > 1 + assert original_text == get_joined_text(parts) + # with margin-border-padding content = ''' WeasyPrint is a free software visual rendering engine