""" weasyprint.tests.test_boxes --------------------------- Test that the "before layout" box tree is correctly constructed. :copyright: Copyright 2011-2019 Simon Sapin and contributors, see AUTHORS. :license: BSD, see LICENSE for details. """ import functools import pytest from .. import images from ..css import PageType, get_all_computed_styles from ..css.targets import TargetCollector from ..formatting_structure import boxes, build, counters from ..layout.pages import set_page_type_computed_styles from .testing_utils import BASE_URL, FakeHTML, assert_no_logs, capture_logs PROPER_CHILDREN = dict((key, tuple(map(tuple, value))) for key, value in { # Children can be of *any* type in *one* of the lists. boxes.BlockContainerBox: [[boxes.BlockLevelBox], [boxes.LineBox]], boxes.LineBox: [[boxes.InlineLevelBox]], boxes.InlineBox: [[boxes.InlineLevelBox]], boxes.TableBox: [[boxes.TableCaptionBox, boxes.TableColumnGroupBox, boxes.TableColumnBox, boxes.TableRowGroupBox, boxes.TableRowBox]], boxes.InlineTableBox: [[boxes.TableCaptionBox, boxes.TableColumnGroupBox, boxes.TableColumnBox, boxes.TableRowGroupBox, boxes.TableRowBox]], boxes.TableColumnGroupBox: [[boxes.TableColumnBox]], boxes.TableRowGroupBox: [[boxes.TableRowBox]], boxes.TableRowBox: [[boxes.TableCellBox]], }.items()) def serialize(box_list): """Transform a box list into a structure easier to compare for testing.""" return [( box.element_tag, type(box).__name__[:-3], # All concrete boxes are either text, replaced, column or parent. (box.text if isinstance(box, boxes.TextBox) else '' if isinstance(box, boxes.ReplacedBox) else serialize( getattr(box, 'column_groups', ()) + tuple(box.children)))) for box in box_list] def _parse_base(html_content, base_url=BASE_URL): document = FakeHTML(string=html_content, base_url=base_url) style_for = get_all_computed_styles(document) get_image_from_uri = functools.partial( images.get_image_from_uri, {}, document.url_fetcher) target_collector = TargetCollector() return ( document.etree_element, style_for, get_image_from_uri, base_url, target_collector) def parse(html_content): """Parse some HTML, apply stylesheets and transform to boxes.""" box, = build.element_to_box(*_parse_base(html_content)) return box def parse_all(html_content, base_url=BASE_URL): """Like parse() but also run all corrections on boxes.""" box = build.build_formatting_structure(*_parse_base( html_content, base_url)) _sanity_checks(box) return box def render_pages(html_content): """Lay out a document and return a list of PageBox objects.""" return [p._page_box for p in FakeHTML( string=html_content, base_url=BASE_URL ).render(enable_hinting=True).pages] def assert_tree(box, expected): """Check the box tree equality. The obtained result is prettified in the message in case of failure. box: a Box object, starting with and blocks. expected: a list of serialized children as returned by to_lists(). """ assert box.element_tag == 'html' assert isinstance(box, boxes.BlockBox) assert len(box.children) == 1 box = box.children[0] assert isinstance(box, boxes.BlockBox) assert box.element_tag == 'body' assert serialize(box.children) == expected def _sanity_checks(box): """Check that the rules regarding boxes are met. This is not required and only helps debugging. - A block container can contain either only block-level boxes or only line boxes; - Line boxes and inline boxes can only contain inline-level boxes. """ if not isinstance(box, boxes.ParentBox): return acceptable_types_lists = None # raises when iterated for class_ in type(box).mro(): if class_ in PROPER_CHILDREN: acceptable_types_lists = PROPER_CHILDREN[class_] break assert any( all(isinstance(child, acceptable_types) or not child.is_in_normal_flow() for child in box.children) for acceptable_types in acceptable_types_lists ), (box, box.children) for child in box.children: _sanity_checks(child) def _get_grid(html): html = parse_all(html) body, = html.children table_wrapper, = body.children table, = table_wrapper.children return tuple( [[(style, width, color) if width else None for _score, (style, width, color) in column] for column in grid] for grid in table.collapsed_border_grid) @assert_no_logs def test_box_tree(): assert_tree(parse('

'), [('p', 'Block', [])]) assert_tree(parse('''

Hello World L!

'''), [ ('p', 'Block', [ ('p', 'Text', 'Hello '), ('em', 'Inline', [ ('em', 'Text', 'World '), ('img', 'InlineReplaced', ''), ('span', 'InlineBlock', [ ('span', 'Text', 'L')])]), ('p', 'Text', '!')])]) @assert_no_logs def test_html_entities(): for quote in ['"', '"', '"', '"']: assert_tree(parse('

{0}abc{1}'.format(quote, quote)), [ ('p', 'Block', [ ('p', 'Text', '"abc"')])]) @assert_no_logs def test_inline_in_block_1(): source = '

Hello, World!\n

Lipsum.

' expected = [ ('div', 'Block', [ ('div', 'Block', [ ('div', 'Line', [ ('div', 'Text', 'Hello, '), ('em', 'Inline', [ ('em', 'Text', 'World')]), ('div', 'Text', '!\n')])]), ('p', 'Block', [ ('p', 'Line', [ ('p', 'Text', 'Lipsum.')])])])] box = parse(source) box = build.inline_in_block(box) assert_tree(box, expected) @assert_no_logs def test_inline_in_block_2(): source = '

Lipsum.

Hello, World!\n
' expected = [ ('div', 'Block', [ ('p', 'Block', [ ('p', 'Line', [ ('p', 'Text', 'Lipsum.')])]), ('div', 'Block', [ ('div', 'Line', [ ('div', 'Text', 'Hello, '), ('em', 'Inline', [ ('em', 'Text', 'World')]), ('div', 'Text', '!\n')])])])] box = parse(source) box = build.inline_in_block(box) assert_tree(box, expected) @assert_no_logs def test_inline_in_block_3(): # Absolutes are left in the lines to get their static position later. source = '''

Hello World!

''' expected = [ ('p', 'Block', [ ('p', 'Line', [ ('p', 'Text', 'Hello '), ('em', 'Block', [ ('em', 'Line', [ ('em', 'Text', 'World')])]), ('p', 'Text', '!')])])] box = parse(source) box = build.inline_in_block(box) assert_tree(box, expected) box = build.block_in_inline(box) assert_tree(box, expected) @assert_no_logs def test_inline_in_block_4(): # Floats are pull to the top of their containing blocks source = '

Hello World!

' box = parse(source) box = build.inline_in_block(box) box = build.block_in_inline(box) assert_tree(box, [ ('p', 'Block', [ ('p', 'Line', [ ('p', 'Text', 'Hello '), ('em', 'Block', [ ('em', 'Line', [ ('em', 'Text', 'World')])]), ('p', 'Text', '!')])])]) @assert_no_logs def test_block_in_inline(): box = parse('''

Lorem ipsum dolor sit amet,conse''') box = build.inline_in_block(box) assert_tree(box, [ ('body', 'Line', [ ('p', 'InlineBlock', [ ('p', 'Line', [ ('p', 'Text', 'Lorem '), ('em', 'Inline', [ ('em', 'Text', 'ipsum '), ('strong', 'Inline', [ ('strong', 'Text', 'dolor '), ('span', 'Block', [ # This block is "pulled up" ('span', 'Line', [ ('span', 'Text', 'sit')])]), # No whitespace processing here. ('strong', 'Text', '\n '), ('span', 'Block', [ # This block is "pulled up" ('span', 'Line', [ ('span', 'Text', 'amet,')])])]), ('span', 'Block', [ # This block is "pulled up" ('span', 'Line', [ ('em', 'Inline', [ ('em', 'Text', 'conse'), ('i', 'Block', [])])])])])])])])]) box = build.block_in_inline(box) assert_tree(box, [ ('body', 'Line', [ ('p', 'InlineBlock', [ ('p', 'Block', [ ('p', 'Line', [ ('p', 'Text', 'Lorem '), ('em', 'Inline', [ ('em', 'Text', 'ipsum '), ('strong', 'Inline', [ ('strong', 'Text', 'dolor ')])])])]), ('span', 'Block', [ ('span', 'Line', [ ('span', 'Text', 'sit')])]), ('p', 'Block', [ ('p', 'Line', [ ('em', 'Inline', [ ('strong', 'Inline', [ # Whitespace processing not done yet. ('strong', 'Text', '\n ')])])])]), ('span', 'Block', [ ('span', 'Line', [ ('span', 'Text', 'amet,')])]), ('p', 'Block', [ ('p', 'Line', [ ('em', 'Inline', [ ('strong', 'Inline', [])])])]), ('span', 'Block', [ ('span', 'Block', [ ('span', 'Line', [ ('em', 'Inline', [ ('em', 'Text', 'conse')])])]), ('i', 'Block', []), ('span', 'Block', [ ('span', 'Line', [ ('em', 'Inline', [])])])]), ('p', 'Block', [ ('p', 'Line', [ ('em', 'Inline', [])])])])])]) @assert_no_logs def test_styles(): box = parse('''

Lorem ipsum dolor sit amet,consectetur

''') box = build.inline_in_block(box) box = build.block_in_inline(box) descendants = list(box.descendants()) assert len(descendants) == 31 assert descendants[0] == box for child in descendants: # All boxes inherit the color assert child.style['color'] == (0, 0, 1, 1) # blue # Only non-anonymous boxes have margins assert child.style['margin_top'] in ((0, 'px'), (42, 'px')) @assert_no_logs def test_whitespace(): # TODO: test more cases # http://www.w3.org/TR/CSS21/text.html#white-space-model assert_tree(parse_all('''

Lorem \t\r\n ipsum\t dolor sit amet consectetur.

\t  foo\n
\t  foo\n
\t  foo\n
'''), [ ('p', 'Block', [ ('p', 'Line', [ ('p', 'Text', 'Lorem ipsum '), ('strong', 'Inline', [ ('strong', 'Text', 'dolor '), ('img', 'InlineReplaced', ''), ('strong', 'Text', ' sit '), ('span', 'Block', []), ('em', 'Inline', [ ('em', 'Text', 'amet ')]), ('strong', 'Text', 'consectetur')]), ('p', 'Text', '.')])]), ('pre', 'Block', [ ('pre', 'Line', [ # pre ('pre', 'Text', '\t foo\n')])]), ('pre', 'Block', [ ('pre', 'Line', [ # pre-wrap ('pre', 'Text', '\t foo\n')])]), ('pre', 'Block', [ ('pre', 'Line', [ # pre-line ('pre', 'Text', ' foo\n')])])]) @assert_no_logs @pytest.mark.parametrize('page_type, top, right, bottom, left', ( (PageType(side='left', first=True, index=0, blank=None, name=None), 20, 3, 3, 10), (PageType(side='right', first=True, index=0, blank=None, name=None), 20, 10, 3, 3), (PageType(side='left', first=None, index=1, blank=None, name=None), 10, 3, 3, 10), (PageType(side='right', first=None, index=1, blank=None, name=None), 10, 10, 3, 3), (PageType(side='right', first=None, index=1, blank=None, name='name'), 5, 10, 3, 15), (PageType(side='right', first=None, index=2, blank=None, name='name'), 5, 10, 1, 15), (PageType(side='right', first=None, index=8, blank=None, name='name'), 5, 10, 2, 15), )) def test_page_style(page_type, top, right, bottom, left): document = FakeHTML(string=''' ''') style_for = get_all_computed_styles(document) # Force the generation of the style for this page type as it's generally # only done during the rendering. set_page_type_computed_styles(page_type, document, style_for) style = style_for(page_type) assert style['margin_top'] == (top, 'px') assert style['margin_right'] == (right, 'px') assert style['margin_bottom'] == (bottom, 'px') assert style['margin_left'] == (left, 'px') @assert_no_logs def test_images_1(): with capture_logs() as logs: result = parse_all('''

No srcInexistent src

''') assert len(logs) == 1 assert 'ERROR: Failed to load image' in logs[0] assert 'inexistent.jpg' in logs[0] assert_tree(result, [ ('p', 'Block', [ ('p', 'Line', [ ('img', 'InlineReplaced', ''), ('img', 'Inline', [ ('img', 'Text', 'No src')]), ('img', 'Inline', [ ('img', 'Text', 'Inexistent src')])])])]) @assert_no_logs def test_images_2(): with capture_logs() as logs: result = parse_all('

No base_url', base_url=None) assert len(logs) == 1 assert 'ERROR: Relative URI reference without a base URI' in logs[0] assert_tree(result, [ ('p', 'Block', [ ('p', 'Line', [ ('img', 'Inline', [ ('img', 'Text', 'No base_url')])])])]) @assert_no_logs def test_tables_1(): # Rules in http://www.w3.org/TR/CSS21/tables.html#anonymous-boxes # Rule 1.3 # Also table model: http://www.w3.org/TR/CSS21/tables.html#model assert_tree(parse_all(''' foo bar top caption baz '''), [ ('x-table', 'Block', [ ('x-caption', 'TableCaption', [ ('x-caption', 'Line', [ ('x-caption', 'Text', 'top caption')])]), ('x-table', 'Table', [ ('x-table', 'TableColumnGroup', [ ('x-col', 'TableColumn', [])]), ('x-thead', 'TableRowGroup', [ ('x-thead', 'TableRow', [ ('x-th', 'TableCell', [])])]), ('x-table', 'TableRowGroup', [ ('x-tr', 'TableRow', [ ('x-th', 'TableCell', [ ('x-th', 'Line', [ ('x-th', 'Text', 'foo')])]), ('x-th', 'TableCell', [ ('x-th', 'Line', [ ('x-th', 'Text', 'bar')])])])]), ('x-thead', 'TableRowGroup', []), ('x-table', 'TableRowGroup', [ ('x-tr', 'TableRow', [ ('x-td', 'TableCell', [ ('x-td', 'Line', [ ('x-td', 'Text', 'baz')])])])]), ('x-tfoot', 'TableRowGroup', [])]), ('x-caption', 'TableCaption', [])])]) @assert_no_logs def test_tables_2(): # Rules 1.4 and 3.1 assert_tree(parse_all(''' foo bar '''), [ ('body', 'Block', [ ('body', 'Table', [ ('body', 'TableRowGroup', [ ('body', 'TableRow', [ ('span', 'TableCell', [ ('span', 'Line', [ ('span', 'Text', 'foo')])]), ('span', 'TableCell', [ ('span', 'Line', [ ('span', 'Text', 'bar')])])])])])])]) @assert_no_logs def test_tables_3(): # http://www.w3.org/TR/CSS21/tables.html#anonymous-boxes # Rules 1.1 and 1.2 # Rule XXX (not in the spec): column groups have at least one column child assert_tree(parse_all(''' 1 2 3 4 '''), [ ('body', 'Block', [ ('body', 'Table', [ ('span', 'TableColumnGroup', [ ('em', 'TableColumn', [])]), ('ins', 'TableColumnGroup', [ ('ins', 'TableColumn', [])])])])]) @assert_no_logs def test_tables_4(): # Rules 2.1 then 2.3 assert_tree(parse_all('foo

'), [ ('x-table', 'Block', [ ('x-table', 'Table', [ ('x-table', 'TableRowGroup', [ ('x-table', 'TableRow', [ ('x-table', 'TableCell', [ ('x-table', 'Block', [ ('x-table', 'Line', [ ('x-table', 'Text', 'foo ')])]), ('div', 'Block', [])])])])])])]) @assert_no_logs def test_tables_5(): # Rule 2.2 assert_tree(parse_all('' '
'), [ ('body', 'Block', [ ('body', 'Table', [ ('x-thead', 'TableRowGroup', [ ('x-thead', 'TableRow', [ ('x-thead', 'TableCell', [ ('div', 'Block', [])]), ('x-td', 'TableCell', [])])])])])]) @assert_no_logs def test_tables_6(): # Rule 3.2 assert_tree(parse_all(''), [ ('body', 'Line', [ ('span', 'Inline', [ ('span', 'InlineBlock', [ ('span', 'InlineTable', [ ('span', 'TableRowGroup', [ ('x-tr', 'TableRow', [])])])])])])]) @assert_no_logs def test_tables_7(): # Rule 3.1 # Also, rule 1.3 does not apply: whitespace before and after is preserved assert_tree(parse_all(''' '''), [ ('body', 'Line', [ ('span', 'Inline', [ # Whitespace is preserved in table handling, then collapsed # into a single space. ('span', 'Text', ' '), ('span', 'InlineBlock', [ ('span', 'InlineTable', [ ('span', 'TableRowGroup', [ ('span', 'TableRow', [ ('em', 'TableCell', []), ('em', 'TableCell', [])])])])]), ('span', 'Text', ' ')])])]) @assert_no_logs def test_tables_8(): # Rule 3.2 assert_tree(parse_all('\t'), [ ('body', 'Block', [ ('body', 'Table', [ ('body', 'TableRowGroup', [ ('x-tr', 'TableRow', []), ('x-tr', 'TableRow', [])])])])]) @assert_no_logs def test_tables_9(): assert_tree(parse_all('\n'), [ ('body', 'Block', [ ('body', 'Table', [ ('body', 'TableColumnGroup', [ ('x-col', 'TableColumn', [])]), ('x-colgroup', 'TableColumnGroup', [ ('x-colgroup', 'TableColumn', [])])])])]) @assert_no_logs def test_table_style(): html = parse_all('
') body, = html.children wrapper, = body.children table, = wrapper.children assert isinstance(wrapper, boxes.BlockBox) assert isinstance(table, boxes.TableBox) assert wrapper.style['margin_top'] == (1, 'px') assert wrapper.style['padding_top'] == (0, 'px') assert table.style['margin_top'] == (0, 'px') assert table.style['padding_top'] == (2, 'px') @assert_no_logs def test_column_style(): html = parse_all('''
''') body, = html.children wrapper, = body.children table, = wrapper.children colgroup, = table.column_groups widths = [col.style['width'] for col in colgroup.children] assert widths == [(10, 'px'), (10, 'px'), (10, 'px'), 'auto', 'auto'] assert [col.grid_x for col in colgroup.children] == [0, 1, 2, 3, 4] # copies, not the same box object assert colgroup.children[0] is not colgroup.children[1] @assert_no_logs def test_nested_grid_x(): html = parse_all('''
''') body, = html.children wrapper, = body.children table, = wrapper.children grid = [(colgroup.grid_x, [col.grid_x for col in colgroup.children]) for colgroup in table.column_groups] assert grid == [(0, [0, 1]), (2, [2, 3]), (4, [4, 5, 6]), (7, [7])] @assert_no_logs def test_colspan_rowspan_1(): # +---+---+---+ # | A | B | C | X # +---+---+---+ # | D | E | X # +---+---+ +---+ # | F ...| | | <-- overlap # +---+---+---+ + # | H | X X | G | # +---+---+ + + # | I | J | X | | # +---+---+ +---+ # X: empty cells html = parse_all('''
A B C
D E
F G
H
I J
''') body, = html.children wrapper, = body.children table, = wrapper.children group, = table.children assert [[c.grid_x for c in row.children] for row in group.children] == [ [0, 1, 2], [0, 1], [0, 3], [0], [0, 1], ] assert [[c.colspan for c in row.children] for row in group.children] == [ [1, 1, 1], [1, 2], [2, 1], [1], [1, 1], ] assert [[c.rowspan for c in row.children] for row in group.children] == [ [1, 1, 1], [1, 2], [1, 3], [1], [1, 1], ] @assert_no_logs def test_colspan_rowspan_2(): # A cell box cannot extend beyond the last row box of a table. html = parse_all('''
''') body, = html.children wrapper, = body.children table, = wrapper.children group, = table.children assert [[c.grid_x for c in row.children] for row in group.children] == [ [0, 1], [1], ] assert [[c.colspan for c in row.children] for row in group.children] == [ [1, 1], [1], ] assert [[c.rowspan for c in row.children] for row in group.children] == [ [2, 1], # Not 5 [1], ] @assert_no_logs def test_before_after_1(): assert_tree(parse_all('''

'''), [ # No content in pseudo-element, no box generated ('p', 'Block', []), ('div', 'Block', []), ('section', 'Block', [])]) @assert_no_logs def test_before_after_2(): assert_tree(parse_all('''

c

'''), [ ('p', 'Block', [ ('p', 'Line', [ ('p::before', 'Inline', [ ('p::before', 'Text', 'ab')]), ('p', 'Text', ' c '), ('p::after', 'Inline', [ ('p::after', 'Text', 'de')])])])]) @assert_no_logs def test_before_after_3(): assert_tree(parse_all('''

some text

'''), [ ('p', 'Block', [ ('p', 'Line', [ ('a', 'Inline', [ ('a::before', 'Inline', [ ('a::before', 'Text', '[some url] ')]), ('a', 'Text', 'some text')])])])]) @assert_no_logs def test_before_after_4(): assert_tree(parse_all('''

Lorem ipsum dolor sit amet

'''), [ ('p', 'Block', [ ('p', 'Line', [ ('q', 'Inline', [ ('q::before', 'Inline', [ ('q::before', 'Text', '« ')]), ('q', 'Text', 'Lorem ipsum '), ('q', 'Inline', [ ('q::before', 'Inline', [ ('q::before', 'Text', '“ ')]), ('q', 'Text', 'dolor'), ('q::after', 'Inline', [ ('q::after', 'Text', ' ”')])]), ('q', 'Text', ' sit amet'), ('q::after', 'Inline', [ ('q::after', 'Text', ' »')])])])])]) @assert_no_logs def test_before_after_5(): with capture_logs() as logs: assert_tree(parse_all('''

c

'''), [ ('p', 'Block', [ ('p', 'Line', [ ('p::before', 'Inline', [ ('p::before', 'Text', 'a'), ('p::before', 'InlineReplaced', ''), ('p::before', 'Text', 'b')]), ('p', 'Text', 'c')])])]) assert len(logs) == 1 assert 'nested-function(' in logs[0] assert 'invalid value' in logs[0] @assert_no_logs def test_counters_1(): assert_tree(parse_all('''

'''), [ ('p', 'Block', [ ('p', 'Line', [ ('p::before', 'Inline', [ ('p::before', 'Text', counter)])])]) for counter in '0 1 3 2 4 6 -11 -9 -7 44 46 48'.split()]) @assert_no_logs def test_counters_2(): assert_tree(parse_all('''
'''), [ ('ol', 'Block', [ ('li', 'Block', [ ('li', 'Line', [ ('li::marker', 'Inline', [ ('li::marker', 'Text', '1. ')])])]), ('li', 'Block', [ ('li', 'Line', [ ('li::marker', 'Inline', [ ('li::marker', 'Text', '2. ')])])]), ('li', 'Block', [ ('li', 'Line', [ ('li::marker', 'Inline', [ ('li::marker', 'Text', '3. ')])])]), ('li', 'Block', [ ('li', 'Block', [ ('li', 'Line', [ ('li::marker', 'Inline', [ ('li::marker', 'Text', '4. ')])])]), ('ol', 'Block', [ ('li', 'Block', [ ('li', 'Line', [ ('li::marker', 'Inline', [ ('li::marker', 'Text', '1. ')])])]), ('li', 'Block', [ ('li', 'Line', [ ('li::marker', 'Inline', [ ('li::marker', 'Text', '1. ')])])]), ('li', 'Block', [ ('li', 'Line', [ ('li::marker', 'Inline', [ ('li::marker', 'Text', '2. ')])])])])]), ('li', 'Block', [ ('li', 'Line', [ ('li::marker', 'Inline', [ ('li::marker', 'Text', '5. ')])])])])]) @assert_no_logs def test_counters_3(): assert_tree(parse_all('''

'''), [ ('div', 'Block', [ ('p', 'Block', [ ('p', 'Line', [ ('p::marker', 'Inline', [ ('p::marker', 'Text', '1. ')])])]), ('p', 'Block', [ ('p', 'Line', [ ('p::marker', 'Inline', [ ('p::marker', 'Text', '2. ')])])]), ('p', 'Block', [ ('p', 'Line', [ ('p::marker', 'Inline', [ ('p::marker', 'Text', '-55. ')])])])]), ('p', 'Block', [ ('p', 'Line', [ ('p::marker', 'Inline', [ ('p::marker', 'Text', '1. ')])])])]) @assert_no_logs def test_counters_4(): assert_tree(parse_all('''

'''), [ ('section', 'Block', [ ('section', 'Block', [ ('section', 'Line', [ ('section::before', 'Inline', [])])]), ('h1', 'Block', [ ('h1', 'Line', [ ('h1::before', 'Inline', [ ('h1::before', 'Text', '1')])])]), ('h1', 'Block', [ ('h1', 'Line', [ ('h1::before', 'Inline', [ ('h1::before', 'Text', '2')])])]), ('section', 'Block', [ ('section', 'Block', [ ('section', 'Line', [ ('section::before', 'Inline', [])])]), ('h1', 'Block', [ ('h1', 'Line', [ ('h1::before', 'Inline', [ ('h1::before', 'Text', '2.1')])])]), ('h1', 'Block', [ ('h1', 'Line', [ ('h1::before', 'Inline', [ ('h1::before', 'Text', '2.2')])])])]), ('h1', 'Block', [ ('h1', 'Line', [ ('h1::before', 'Inline', [ ('h1::before', 'Text', '3')])])])])]) @assert_no_logs def test_counters_5(): assert_tree(parse_all('''
Scope created now, deleted after the div

'''), [ ('div', 'Block', [ ('div', 'Line', [ ('span', 'Inline', [ ('span', 'Text', 'Scope created now, deleted after the div ')])])]), ('p', 'Block', [ ('p', 'Line', [ ('p::before', 'Inline', [ ('p::before', 'Text', '0')])])])]) @assert_no_logs def test_counters_6(): # counter-increment may interfere with display: list-item assert_tree(parse_all('''

'''), [ ('p', 'Block', [ ('p', 'Line', [ ('p::marker', 'Inline', [ ('p::marker', 'Text', '0. ')])])])]) @assert_no_logs def test_counters_7(): # Test that counters are case-sensitive # See https://github.com/Kozea/WeasyPrint/pull/827 assert_tree(parse_all('''

'''), [ ('p', 'Block', [ ('p', 'Line', [ ('p::before', 'Inline', [ ('p::before', 'Text', counter)])])]) for counter in '2.0 2.3 4.3'.split()]) @assert_no_logs def test_counters_8(): assert_tree(parse_all('''

'''), 2 * [ ('p', 'Block', [ ('p::before', 'Block', [ ('p::marker', 'Block', [ ('p::marker', 'Line', [ ('p::marker', 'Text', '• ')])]), ('p::before', 'Block', [ ('p::before', 'Line', [ ('p::before', 'Text', 'a')])])])])]) @assert_no_logs def test_counter_styles_1(): assert_tree(parse_all('''

'''), [ ('p', 'Block', [ ('p', 'Line', [ ('p::before', 'Inline', [ ('p::before', 'Text', counter)])])]) for counter in '-- • ◦ ▪ -7'.split()]) @assert_no_logs def test_counter_styles_2(): assert_tree(parse_all('''

'''), [ ('p', 'Block', [ ('p', 'Line', [ ('p::before', 'Inline', [ ('p::before', 'Text', counter)])])]) for counter in '''-1986 -1985 -11 -10 -09 -08 -01 00 01 02 09 10 11 99 100 101 4135 4136'''.split()]) @assert_no_logs def test_counter_styles_3(): # Same test as above, but short-circuit HTML and boxes assert [counters.format(value, 'decimal-leading-zero') for value in [ -1986, -1985, -11, -10, -9, -8, -1, 0, 1, 2, 9, 10, 11, 99, 100, 101, 4135, 4136 ]] == ''' -1986 -1985 -11 -10 -09 -08 -01 00 01 02 09 10 11 99 100 101 4135 4136 '''.split() @assert_no_logs def test_counter_styles_4(): # Now that we’re confident that they do the same, use the shorter form. # http://test.csswg.org/suites/css2.1/20110323/html4/content-counter-007.htm assert [counters.format(value, 'lower-roman') for value in [ -1986, -1985, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 49, 50, 389, 390, 3489, 3490, 3491, 4999, 5000, 5001 ]] == ''' -1986 -1985 -1 0 i ii iii iv v vi vii viii ix x xi xii xlix l ccclxxxix cccxc mmmcdlxxxix mmmcdxc mmmcdxci mmmmcmxcix 5000 5001 '''.split() @assert_no_logs def test_counter_styles_5(): # http://test.csswg.org/suites/css2.1/20110323/html4/content-counter-008.htm assert [counters.format(value, 'upper-roman') for value in [ -1986, -1985, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 49, 50, 389, 390, 3489, 3490, 3491, 4999, 5000, 5001 ]] == ''' -1986 -1985 -1 0 I II III IV V VI VII VIII IX X XI XII XLIX L CCCLXXXIX CCCXC MMMCDLXXXIX MMMCDXC MMMCDXCI MMMMCMXCIX 5000 5001 '''.split() @assert_no_logs def test_counter_styles_6(): assert [counters.format(value, 'lower-alpha') for value in [ -1986, -1985, -1, 0, 1, 2, 3, 4, 25, 26, 27, 28, 29, 2002, 2003 ]] == ''' -1986 -1985 -1 0 a b c d y z aa ab ac bxz bya '''.split() @assert_no_logs def test_counter_styles_7(): assert [counters.format(value, 'upper-alpha') for value in [ -1986, -1985, -1, 0, 1, 2, 3, 4, 25, 26, 27, 28, 29, 2002, 2003 ]] == ''' -1986 -1985 -1 0 A B C D Y Z AA AB AC BXZ BYA '''.split() @assert_no_logs def test_counter_styles_8(): assert [counters.format(value, 'lower-latin') for value in [ -1986, -1985, -1, 0, 1, 2, 3, 4, 25, 26, 27, 28, 29, 2002, 2003 ]] == ''' -1986 -1985 -1 0 a b c d y z aa ab ac bxz bya '''.split() @assert_no_logs def test_counter_styles_9(): assert [counters.format(value, 'upper-latin') for value in [ -1986, -1985, -1, 0, 1, 2, 3, 4, 25, 26, 27, 28, 29, 2002, 2003 ]] == ''' -1986 -1985 -1 0 A B C D Y Z AA AB AC BXZ BYA '''.split() @assert_no_logs def test_counter_styles_10(): # http://test.csswg.org/suites/css2.1/20110323/html4/content-counter-009.htm assert [counters.format(value, 'georgian') for value in [ -1986, -1985, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 20, 30, 40, 50, 60, 70, 80, 90, 100, 200, 300, 400, 500, 600, 700, 800, 900, 1000, 2000, 3000, 4000, 5000, 6000, 7000, 8000, 9000, 10000, 19999, 20000, 20001 ]] == ''' -1986 -1985 -1 0 ა ბ გ დ ე ვ ზ ჱ თ ი ია იბ კ ლ მ ნ ჲ ო პ ჟ რ ს ტ ჳ ფ ქ ღ ყ შ ჩ ც ძ წ ჭ ხ ჴ ჯ ჰ ჵ ჵჰშჟთ 20000 20001 '''.split() @assert_no_logs def test_counter_styles_11(): # http://test.csswg.org/suites/css2.1/20110323/html4/content-counter-010.htm assert [counters.format(value, 'armenian') for value in [ -1986, -1985, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 20, 30, 40, 50, 60, 70, 80, 90, 100, 200, 300, 400, 500, 600, 700, 800, 900, 1000, 2000, 3000, 4000, 5000, 6000, 7000, 8000, 9000, 9999, 10000, 10001 ]] == ''' -1986 -1985 -1 0 Ա Բ Գ Դ Ե Զ Է Ը Թ Ժ ԺԱ ԺԲ Ի Լ Խ Ծ Կ Հ Ձ Ղ Ճ Մ Յ Ն Շ Ո Չ Պ Ջ Ռ Ս Վ Տ Ր Ց Ւ Փ Ք ՔՋՂԹ 10000 10001 '''.split() @assert_no_logs def test_margin_boxes(): page_1, page_2 = render_pages('''

lorem ipsum ''') 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:]] assert margin_boxes_1 == ['@top-center', '@bottom-left', '@bottom-left-corner'] 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' @assert_no_logs def test_margin_box_string_set_1(): # Test that both pages get string in the `bottom-center` margin box page_1, page_2 = render_pages('''

first assignment

''') html, bottom_center = page_2.children line_box, = bottom_center.children text_box, = line_box.children assert text_box.text == 'first assignment' html, bottom_center = page_1.children line_box, = bottom_center.children text_box, = line_box.children assert text_box.text == 'first assignment' @assert_no_logs def test_margin_box_string_set_2(): def simple_string_set_test(content_val, extra_style=""): page_1, = render_pages('''

first assignment

''' % dict(content_val=content_val, extra_style=extra_style)) html, top_center = page_1.children line_box, = top_center.children text_box, = line_box.children if content_val in ('before', 'after'): assert text_box.text == 'pseudo' else: assert text_box.text == 'first assignment' # Test each accepted value of `content()` as an arguemnt to `string-set` for value in ('', 'text', 'before', 'after'): if value in ('before', 'after'): extra_style = "p:%s{content: 'pseudo'}" % value simple_string_set_test(value, extra_style) else: simple_string_set_test(value) @assert_no_logs def test_margin_box_string_set_3(): # Test `first` (default value) ie. use the first assignment on the page page_1, = render_pages('''

first assignment

Second assignment

''') html, top_center = page_1.children line_box, = top_center.children text_box, = line_box.children assert text_box.text == 'first assignment' @assert_no_logs def test_margin_box_string_set_4(): # test `first-except` ie. exclude from page on which value is assigned page_1, page_2 = render_pages('''

first_excepted

''') html, top_center = page_1.children assert len(top_center.children) == 0 html, top_center = page_2.children line_box, = top_center.children text_box, = line_box.children assert text_box.text == 'first_excepted' @assert_no_logs def test_margin_box_string_set_5(): # Test `last` ie. use the most-recent assignment page_1, = render_pages('''

String set

Second assignment

''') html, top_center = page_1.children[:2] line_box, = top_center.children text_box, = line_box.children assert text_box.text == 'Second assignment' @assert_no_logs def test_margin_box_string_set_6(): # Test multiple complex string-set values page_1, = render_pages('''
  • first
    • second ''') html, top_center, bottom_center = page_1.children top_line_box, = top_center.children top_text_box, = top_line_box.children assert top_text_box.text == 'before!-first-after!I.1' bottom_line_box, = bottom_center.children bottom_text_box, = bottom_line_box.children assert bottom_text_box.text == 'before!last-secondclass2|1/I' def test_margin_box_string_set_7(): # Test regression: https://github.com/Kozea/WeasyPrint/issues/722 page_1, = render_pages(''' Chocolate Cake ''') html, top_left, top_right = page_1.children left_line_box, = top_left.children left_text_box, = left_line_box.children assert left_text_box.text == '[Chocolate]' right_line_box, = top_right.children right_text_box, = right_line_box.children assert right_text_box.text == '{Cake}' @assert_no_logs def test_margin_box_string_set_8(): # Test regression: https://github.com/Kozea/WeasyPrint/issues/726 page_1, page_2, page_3 = render_pages('''

      Initial

      Empty

      Space

      ''') html, top_left = page_1.children left_line_box, = top_left.children left_text_box, = left_line_box.children assert left_text_box.text == '[initial]' html, top_left = page_2.children left_line_box, = top_left.children left_text_box, = left_line_box.children assert left_text_box.text == '[]' html, top_left = page_3.children left_line_box, = top_left.children left_text_box, = left_line_box.children assert left_text_box.text == '[ ]' @assert_no_logs def test_margin_box_string_set_9(): # Test that named strings are case-sensitive # See https://github.com/Kozea/WeasyPrint/pull/827 page_1, = render_pages('''

      first assignment

      second assignment
      ''') html, top_center = page_1.children line_box, = top_center.children text_box, = line_box.children assert text_box.text == 'first assignment second assignment' @assert_no_logs def test_page_counters(): """Test page-based counters.""" pages = render_pages('''

      lorem ipsum dolor ''') for page_number, page in enumerate(pages, 1): html, bottom_center = page.children line_box, = bottom_center.children text_box, = line_box.children assert text_box.text == 'Page {0} of 3.'.format(page_number) black = (0, 0, 0, 1) red = (1, 0, 0, 1) green = (0, 1, 0, 1) # lime in CSS blue = (0, 0, 1, 1) yellow = (1, 1, 0, 1) black_3 = ('solid', 3, black) red_1 = ('solid', 1, red) yellow_5 = ('solid', 5, yellow) green_5 = ('solid', 5, green) dashed_blue_5 = ('dashed', 5, blue) @assert_no_logs def test_border_collapse_1(): html = parse_all('
      ') body, = html.children table_wrapper, = body.children table, = table_wrapper.children assert isinstance(table, boxes.TableBox) assert not hasattr(table, 'collapsed_border_grid') grid = _get_grid('
      ') assert grid == ([], []) @assert_no_logs def test_border_collapse_2(): vertical_borders, horizontal_borders = _get_grid('''
      A B
      C D
      ''') assert vertical_borders == [ [black_3, red_1, black_3], [black_3, red_1, black_3], ] assert horizontal_borders == [ [black_3, black_3], [red_1, red_1], [black_3, black_3], ] @assert_no_logs def test_border_collapse_3(): # hidden vs. none vertical_borders, horizontal_borders = _get_grid('''
      A B
      C D
      ''') assert vertical_borders == [ [black_3, None, None], [black_3, black_3, black_3], ] assert horizontal_borders == [ [black_3, None], [black_3, None], [black_3, black_3], ] @assert_no_logs def test_border_collapse_4(): vertical_borders, horizontal_borders = _get_grid('''
      ''') assert vertical_borders == [ [yellow_5, black_3, red_1, yellow_5], [yellow_5, dashed_blue_5, green_5, green_5], [yellow_5, black_3, red_1, yellow_5], [yellow_5, black_3, red_1, yellow_5], ] assert horizontal_borders == [ [yellow_5, yellow_5, yellow_5], [red_1, dashed_blue_5, green_5], [red_1, dashed_blue_5, green_5], [red_1, red_1, red_1], [yellow_5, yellow_5, yellow_5], ] @assert_no_logs def test_border_collapse_5(): # rowspan and colspan vertical_borders, horizontal_borders = _get_grid('''
      ''') assert vertical_borders == [ [black_3, black_3, black_3, black_3], [black_3, black_3, None, black_3], ] assert horizontal_borders == [ [black_3, black_3, black_3], [None, black_3, black_3], [black_3, black_3, black_3], ] @assert_no_logs @pytest.mark.parametrize('html', ( '', 'abc', '

      abc', '

      abc', )) def test_display_none_root(html): box = parse_all(html) assert box.style['display'] == 'block' assert not box.children