1
1
mirror of https://github.com/Kozea/WeasyPrint.git synced 2024-10-05 00:21:15 +03:00
WeasyPrint/weasyprint/tests/test_layout.py
2013-04-22 17:26:41 +02:00

4758 lines
156 KiB
Python
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# coding: utf8
"""
weasyprint.tests.layout
-----------------------
Tests for layout, ie. positioning and dimensioning of boxes,
line breaks, page breaks.
:copyright: Copyright 2011-2013 Simon Sapin and contributors, see AUTHORS.
:license: BSD, see LICENSE for details.
"""
from __future__ import division, unicode_literals
import math
import pytest
from .testing_utils import FONTS, assert_no_logs, capture_logs, almost_equal
from ..formatting_structure import boxes
from .test_boxes import render_pages as parse
def body_children(page):
"""Take a ``page`` and return its <body>s children."""
html, = page.children
assert html.element_tag == 'html'
body, = html.children
assert body.element_tag == 'body'
return body.children
def outer_area(box):
"""Return the (x, y, w, h) rectangle for the outer area of a box."""
return (box.position_x, box.position_y,
box.margin_width(), box.margin_height())
@assert_no_logs
def test_page_size():
"""Test the layout for ``@page`` properties."""
pages = parse('<p>')
page = pages[0]
assert isinstance(page, boxes.PageBox)
assert int(page.margin_width()) == 793 # A4: 210 mm in pixels
assert int(page.margin_height()) == 1122 # A4: 297 mm in pixels
page, = parse('<style>@page { size: 2in 10in; }</style>')
assert page.margin_width() == 192
assert page.margin_height() == 960
page, = parse('<style>@page { size: 242px; }</style>')
assert page.margin_width() == 242
assert page.margin_height() == 242
page, = parse('<style>@page { size: letter; }</style>')
assert page.margin_width() == 816 # 8.5in
assert page.margin_height() == 1056 # 11in
page, = parse('<style>@page { size: letter portrait; }</style>')
assert page.margin_width() == 816 # 8.5in
assert page.margin_height() == 1056 # 11in
page, = parse('<style>@page { size: letter landscape; }</style>')
assert page.margin_width() == 1056 # 11in
assert page.margin_height() == 816 # 8.5in
page, = parse('<style>@page { size: portrait; }</style>')
assert int(page.margin_width()) == 793 # A4: 210 mm
assert int(page.margin_height()) == 1122 # A4: 297 mm
page, = parse('<style>@page { size: landscape; }</style>')
assert int(page.margin_width()) == 1122 # A4: 297 mm
assert int(page.margin_height()) == 793 # A4: 210 mm
page, = parse('''
<style>@page { size: 200px 300px; margin: 10px 10% 20% 1in }
body { margin: 8px }
</style>
<p style="margin: 0">
''')
assert page.margin_width() == 200
assert page.margin_height() == 300
assert page.position_x == 0
assert page.position_y == 0
assert page.width == 84 # 200px - 10% - 1 inch
assert page.height == 230 # 300px - 10px - 20%
html, = page.children
assert html.element_tag == 'html'
assert html.position_x == 96 # 1in
assert html.position_y == 10 # root elements margins do not collapse
assert html.width == 84
body, = html.children
assert body.element_tag == 'body'
assert body.position_x == 96 # 1in
assert body.position_y == 10
# body has margins in the UA stylesheet
assert body.margin_left == 8
assert body.margin_right == 8
assert body.margin_top == 8
assert body.margin_bottom == 8
assert body.width == 68
paragraph, = body.children
assert paragraph.element_tag == 'p'
assert paragraph.position_x == 104 # 1in + 8px
assert paragraph.position_y == 18 # 10px + 8px
assert paragraph.width == 68
page, = parse('''
<style>
@page { size: 100px; margin: 1px 2px; padding: 4px 8px;
border-width: 16px 32px; border-style: solid }
</style>
<body>
''')
assert page.width == 16 # 100 - 2 * 42
assert page.height == 58 # 100 - 2 * 21
html, = page.children
assert html.element_tag == 'html'
assert html.position_x == 42 # 2 + 8 + 32
assert html.position_y == 21 # 1 + 4 + 16
page, = parse('''<style>@page {
size: 106px 206px; width: 80px; height: 170px;
padding: 1px; border: 2px solid; margin: auto;
}</style>''')
assert page.margin_top == 15 # (206 - 2*1 - 2*2 - 170) / 2
assert page.margin_right == 10 # (106 - 2*1 - 2*2 - 80) / 2
assert page.margin_bottom == 15 # (206 - 2*1 - 2*2 - 170) / 2
assert page.margin_left == 10 # (106 - 2*1 - 2*2 - 80) / 2
page, = parse('''<style>@page {
size: 106px 206px; width: 80px; height: 170px;
padding: 1px; border: 2px solid; margin: 5px 5px auto auto;
}</style>''')
assert page.margin_top == 5
assert page.margin_right == 5
assert page.margin_bottom == 25 # 206 - 2*1 - 2*2 - 170 - 5
assert page.margin_left == 15 # 106 - 2*1 - 2*2 - 80 - 5
# Over-constrained: the containing block is resized
page, = parse('''<style>@page {
size: 4px 10000px; width: 100px; height: 100px;
padding: 1px; border: 2px solid; margin: 3px;
}</style>''')
assert page.margin_width() == 112 # 100 + 2*1 + 2*2 + 2*3
assert page.margin_height() == 112
page, = parse('''<style>@page {
size: 1000px; margin: 100px;
max-width: 500px; min-height: 1500px;
}</style>''')
assert page.margin_width() == 700
assert page.margin_height() == 1700
page, = parse('''<style>@page {
size: 1000px; margin: 100px;
min-width: 1500px; max-height: 500px;
}</style>''')
assert page.margin_width() == 1700
assert page.margin_height() == 700
@assert_no_logs
def test_block_widths():
"""Test the blocks widths."""
page, = parse('''
<style>
@page { margin: 0; size: 120px 2000px }
body { margin: 0 }
div { margin: 10px }
p { padding: 2px; border-width: 1px; border-style: solid }
</style>
<div>
<p></p>
<p style="width: 50px"></p>
</div>
<div style="direction: rtl">
<p style="width: 50px; direction: rtl"></p>
</div>
<div>
<p style="margin: 0 10px 0 20px"></p>
<p style="width: 50px; margin-left: 20px; margin-right: auto"></p>
<p style="width: 50px; margin-left: auto; margin-right: 20px"></p>
<p style="width: 50px; margin: auto"></p>
<p style="margin-left: 20px; margin-right: auto"></p>
<p style="margin-left: auto; margin-right: 20px"></p>
<p style="margin: auto"></p>
<p style="width: 200px; margin: auto"></p>
<p style="min-width: 200px; margin: auto"></p>
<p style="max-width: 50px; margin: auto"></p>
<p style="min-width: 50px; margin: auto"></p>
<p style="width: 70%"></p>
</div>
''')
html, = page.children
assert html.element_tag == 'html'
body, = html.children
assert body.element_tag == 'body'
assert body.width == 120
divs = body.children
paragraphs = []
for div in divs:
assert isinstance(div, boxes.BlockBox)
assert div.element_tag == 'div'
assert div.width == 100
for paragraph in div.children:
assert isinstance(paragraph, boxes.BlockBox)
assert paragraph.element_tag == 'p'
assert paragraph.padding_left == 2
assert paragraph.padding_right == 2
assert paragraph.border_left_width == 1
assert paragraph.border_right_width == 1
paragraphs.append(paragraph)
assert len(paragraphs) == 15
# width is 'auto'
assert paragraphs[0].width == 94
assert paragraphs[0].margin_left == 0
assert paragraphs[0].margin_right == 0
# No 'auto', over-constrained equation with ltr, the initial
# 'margin-right: 0' was ignored.
assert paragraphs[1].width == 50
assert paragraphs[1].margin_left == 0
# No 'auto', over-constrained equation with rtl, the initial
# 'margin-left: 0' was ignored.
assert paragraphs[2].width == 50
assert paragraphs[2].margin_right == 0
# width is 'auto'
assert paragraphs[3].width == 64
assert paragraphs[3].margin_left == 20
# margin-right is 'auto'
assert paragraphs[4].width == 50
assert paragraphs[4].margin_left == 20
# margin-left is 'auto'
assert paragraphs[5].width == 50
assert paragraphs[5].margin_left == 24
# Both margins are 'auto', remaining space is split in half
assert paragraphs[6].width == 50
assert paragraphs[6].margin_left == 22
# width is 'auto', other 'auto' are set to 0
assert paragraphs[7].width == 74
assert paragraphs[7].margin_left == 20
# width is 'auto', other 'auto' are set to 0
assert paragraphs[8].width == 74
assert paragraphs[8].margin_left == 0
# width is 'auto', other 'auto' are set to 0
assert paragraphs[9].width == 94
assert paragraphs[9].margin_left == 0
# sum of non-auto initially is too wide, set auto values to 0
assert paragraphs[10].width == 200
assert paragraphs[10].margin_left == 0
# Constrained by min-width, same as above
assert paragraphs[11].width == 200
assert paragraphs[11].margin_left == 0
# Constrained by max-width, same as paragraphs[6]
assert paragraphs[12].width == 50
assert paragraphs[12].margin_left == 22
# NOT constrained by min-width
assert paragraphs[13].width == 94
assert paragraphs[13].margin_left == 0
# 70%
assert paragraphs[14].width == 70
assert paragraphs[14].margin_left == 0
@assert_no_logs
def test_block_heights():
"""Test the blocks heights."""
page, = parse('''
<style>
@page { margin: 0; size: 100px 20000px }
html, body { margin: 0 }
div { margin: 4px; border-width: 2px; border-style: solid;
padding: 4px }
/* Only use top margins so that margin collapsing does not change
the result: */
p { margin: 16px 0 0; border-width: 4px; border-style: solid;
padding: 8px; height: 50px }
</style>
<div>
<p></p>
<!-- These two are not in normal flow: the do not contribute to
the parents height. -->
<p style="position: absolute"></p>
<p style="float: left"></p>
</div>
<div>
<p></p>
<p></p>
<p></p>
</div>
<div style="height: 20px">
<p></p>
</div>
<div style="height: 120px">
<p></p>
</div>
<div style="max-height: 20px">
<p></p>
</div>
<div style="min-height: 120px">
<p></p>
</div>
<div style="min-height: 20px">
<p></p>
</div>
<div style="max-height: 120px">
<p></p>
</div>
''')
heights = [div.height for div in body_children(page)]
assert heights == [90, 90 * 3, 20, 120, 20, 120, 90, 90]
page, = parse('''
<style>
body { height: 200px; font-size: 0; }
</style>
<div>
<img src=pattern.png style="height: 40px">
</div>
<div style="height: 10%">
<img src=pattern.png style="height: 40px">
</div>
<div style="max-height: 20px">
<img src=pattern.png style="height: 40px">
</div>
<div style="max-height: 10%">
<img src=pattern.png style="height: 40px">
</div>
<div style="min-height: 20px"></div>
<div style="min-height: 10%"></div>
''')
heights = [div.height for div in body_children(page)]
assert heights == [40, 20, 20, 20, 20, 20]
# Same but with no height on body: percentage *-height is ignored
page, = parse('''
<style>
body { font-size: 0; }
</style>
<div>
<img src=pattern.png style="height: 40px">
</div>
<div style="height: 10%">
<img src=pattern.png style="height: 40px">
</div>
<div style="max-height: 20px">
<img src=pattern.png style="height: 40px">
</div>
<div style="max-height: 10%">
<img src=pattern.png style="height: 40px">
</div>
<div style="min-height: 20px"></div>
<div style="min-height: 10%"></div>
''')
heights = [div.height for div in body_children(page)]
assert heights == [40, 40, 20, 40, 20, 0]
@assert_no_logs
def test_block_percentage_heights():
"""Test the blocks heights set in percents."""
page, = parse('''
<style>
html, body { margin: 0 }
body { height: 50% }
</style>
<body>
''')
html, = page.children
assert html.element_tag == 'html'
body, = html.children
assert body.element_tag == 'body'
# Since htmls height depend on bodys, bodys 50% means 'auto'
assert body.height == 0
page, = parse('''
<style>
html, body { margin: 0 }
html { height: 300px }
body { height: 50% }
</style>
<body>
''')
html, = page.children
assert html.element_tag == 'html'
body, = html.children
assert body.element_tag == 'body'
# This time the percentage makes sense
assert body.height == 150
@assert_no_logs
def test_inline_block_sizes():
"""Test the inline-block elements sizes."""
page, = parse('''
<style>
@page { margin: 0; size: 200px 2000px }
body { margin: 0 }
div { display: inline-block; }
</style>
<div> </div>
<div>a</div>
<div style="margin: 10px; height: 100px"></div>
<div style="margin-left: 10px; margin-top: -50px;
padding-right: 20px;"></div>
<div>
Ipsum dolor sit amet,
consectetur adipiscing elit.
Sed sollicitudin nibh
et turpis molestie tristique.
</div>
<div style="width: 100px; height: 100px;
padding-left: 10px; margin-right: 10px;
margin-top: -10px; margin-bottom: 50px"></div>
<div style="font-size: 0">
<div style="min-width: 10px; height: 10px"></div>
<div style="width: 10%">
<div style="width: 10px; height: 10px"></div>
</div>
</div>
<div style="min-width: 185px">foo</div>
<div style="max-width: 10px
">Supercalifragilisticexpialidocious</div>''')
html, = page.children
assert html.element_tag == 'html'
body, = html.children
assert body.element_tag == 'body'
assert body.width == 200
line_1, line_2, line_3, line_4 = body.children
# First line:
# White space in-between divs ends up preserved in TextBoxes
div_1, _, div_2, _, div_3, _, div_4, _ = line_1.children
# First div, one ignored space collapsing with next space
assert div_1.element_tag == 'div'
assert div_1.width == 0
# Second div, one letter
assert div_2.element_tag == 'div'
assert 0 < div_2.width < 20
# Third div, empty with margin
assert div_3.element_tag == 'div'
assert div_3.width == 0
assert div_3.margin_width() == 20
assert div_3.height == 100
# Fourth div, empty with margin and padding
assert div_4.element_tag == 'div'
assert div_4.width == 0
assert div_4.margin_width() == 30
# Second line:
div_5, = line_2.children
# Fifth div, long text, full-width div
assert div_5.element_tag == 'div'
assert len(div_5.children) > 1
assert div_5.width == 200
# Third line:
div_6, _, div_7, _ = line_3.children
# Sixth div, empty div with fixed width and height
assert div_6.element_tag == 'div'
assert div_6.width == 100
assert div_6.margin_width() == 120
assert div_6.height == 100
assert div_6.margin_height() == 140
# Seventh div
assert div_7.element_tag == 'div'
assert div_7.width == 20
child_line, = div_7.children
# Spaces have font-size: 0, they get removed
child_div_1, child_div_2 = child_line.children
assert child_div_1.element_tag == 'div'
assert child_div_1.width == 10
assert child_div_2.element_tag == 'div'
assert child_div_2.width == 2
grandchild, = child_div_2.children
assert grandchild.element_tag == 'div'
assert grandchild.width == 10
div_8, _, div_9 = line_4.children
assert div_8.width == 185
assert div_9.width == 10
# Previously, the hinting for in shrink-to-fit did not match that
# of the layout, which often resulted in a line break just before
# the last word.
page, = parse('''
<p style="display: inline-block">Lorem ipsum dolor sit amet …</p>''')
html, = page.children
body, = html.children
outer_line, = body.children
paragraph, = outer_line.children
inner_lines = paragraph.children
assert len(inner_lines) == 1
text_box, = inner_lines[0].children
assert text_box.text == 'Lorem ipsum dolor sit amet …'
@assert_no_logs
def test_inline_table():
"""Test the inline-table elements sizes."""
page, = parse('''
<table style="display: inline-table; border-spacing: 10px;
margin: 5px">
<tr>
<td><img src=pattern.png style="width: 20px"></td>
<td><img src=pattern.png style="width: 30px"></td>
</tr>
</table>
foo
''')
html, = page.children
body, = html.children
line, = body.children
table_wrapper, text = line.children
table, = table_wrapper.children
row_group, = table.children
row, = row_group.children
td_1, td_2 = row.children
assert table_wrapper.position_x == 0
assert table.position_x == 5 # 0 + margin-left
assert td_1.position_x == 15 # 0 + border-spacing
assert td_1.width == 20
assert td_2.position_x == 45 # 15 + 20 + border-spacing
assert td_2.width == 30
assert table.width == 80 # 20 + 30 + 3 * border-spacing
assert table_wrapper.margin_width() == 90 # 80 + 2 * margin
assert text.position_x == 90
@assert_no_logs
def test_fixed_layout_table():
"""Test the fixed layout table elements sizes."""
page, = parse('''
<table style="table-layout: fixed; border-spacing: 10px;
margin: 5px">
<colgroup>
<col style="width: 20px" />
</colgroup>
<tr>
<td></td>
<td style="width: 40px">a</td>
</tr>
</table>
''')
html, = page.children
body, = html.children
table_wrapper, = body.children
table, = table_wrapper.children
row_group, = table.children
row, = row_group.children
td_1, td_2 = row.children
assert table_wrapper.position_x == 0
assert table.position_x == 5 # 0 + margin-left
assert td_1.position_x == 15 # 5 + border-spacing
assert td_1.width == 20
assert td_2.position_x == 45 # 15 + 20 + border-spacing
assert td_2.width == 40
assert table.width == 90 # 20 + 40 + 3 * border-spacing
page, = parse('''
<table style="table-layout: fixed; border-spacing: 10px;
width: 200px; margin: 5px">
<tr>
<td style="width: 20px">a</td>
<td style="width: 40px"></td>
</tr>
</table>
''')
html, = page.children
body, = html.children
table_wrapper, = body.children
table, = table_wrapper.children
row_group, = table.children
row, = row_group.children
td_1, td_2 = row.children
assert table_wrapper.position_x == 0
assert table.position_x == 5 # 0 + margin-left
assert td_1.position_x == 15 # 5 + border-spacing
assert td_1.width == 75 # 20 + ((200 - 20 - 40 - 3 * border-spacing) / 2)
assert td_2.position_x == 100 # 15 + 75 + border-spacing
assert td_2.width == 95 # 40 + ((200 - 20 - 40 - 3 * border-spacing) / 2)
assert table.width == 200
page, = parse('''
<table style="table-layout: fixed; border-spacing: 10px;
width: 110px; margin: 5px">
<tr>
<td style="width: 40px">a</td>
<td>b</td>
</tr>
<tr>
<td style="width: 50px">a</td>
<td style="width: 30px">b</td>
</tr>
</table>
''')
html, = page.children
body, = html.children
table_wrapper, = body.children
table, = table_wrapper.children
row_group, = table.children
row_1, row_2 = row_group.children
td_1, td_2 = row_1.children
td_3, td_4 = row_2.children
assert table_wrapper.position_x == 0
assert table.position_x == 5 # 0 + margin-left
assert td_1.position_x == 15 # 0 + border-spacing
assert td_3.position_x == 15
assert td_1.width == 40
assert td_2.width == 40
assert td_2.position_x == 65 # 15 + 40 + border-spacing
assert td_4.position_x == 65
assert td_3.width == 40
assert td_4.width == 40
assert table.width == 110 # 20 + 40 + 3 * border-spacing
page, = parse('''
<table style="table-layout: fixed; border-spacing: 0;
width: 100px; margin: 10px">
<colgroup>
<col />
<col style="width: 20px" />
</colgroup>
<tr>
<td></td>
<td style="width: 40px">a</td>
</tr>
</table>
''')
html, = page.children
body, = html.children
table_wrapper, = body.children
table, = table_wrapper.children
row_group, = table.children
row, = row_group.children
td_1, td_2 = row.children
assert table_wrapper.position_x == 0
assert table.position_x == 10 # 0 + margin-left
assert td_1.position_x == 10
assert td_1.width == 80 # 100 - 20
assert td_2.position_x == 90 # 10 + 80
assert td_2.width == 20
assert table.width == 100
# With border-collapse
page, = parse('''
<style>
/* Do not apply: */
colgroup, col, tbody, tr, td { margin: 1000px }
</style>
<table style="table-layout: fixed;
border-collapse: collapse; border: 10px solid;
/* ignored with collapsed borders: */
border-spacing: 10000px; padding: 1000px">
<colgroup>
<col style="width: 30px" />
</colgroup>
<tbody>
<tr>
<td style="padding: 2px"></td>
<td style="width: 34px; padding: 10px; border: 2px solid"></td>
</tr>
</tbody>
</table>
''')
html, = page.children
body, = html.children
table_wrapper, = body.children
table, = table_wrapper.children
row_group, = table.children
row, = row_group.children
td_1, td_2 = row.children
assert table_wrapper.position_x == 0
assert table.position_x == 0
assert table.border_left_width == 5 # half of the collapsed 10px border
assert td_1.position_x == 5 # border-spacing is ignored
assert td_1.margin_width() == 30 # as <col>
assert td_1.width == 20 # 30 - 5 (border-left) - 1 (border-right) - 2*2
assert td_2.position_x == 35
assert td_2.width == 34
assert td_2.margin_width() == 60 # 34 + 2*10 + 5 + 1
assert table.width == 90 # 30 + 60
assert table.margin_width() == 100 # 90 + 2*5 (border)
@assert_no_logs
def test_auto_layout_table():
"""Test the auto layout table elements sizes."""
page, = parse('''
<body style="width: 100px">
<table style="border-spacing: 10px; margin: auto">
<tr>
<td><img src=pattern.png></td>
<td><img src=pattern.png></td>
</tr>
</table>
''')
html, = page.children
body, = html.children
table_wrapper, = body.children
table, = table_wrapper.children
row_group, = table.children
row, = row_group.children
td_1, td_2 = row.children
assert table_wrapper.position_x == 0
assert table_wrapper.width == 38 # Same as table, see below
assert table_wrapper.margin_left == 31 # 0 + margin-left = (100 - 38) / 2
assert table_wrapper.margin_right == 31
assert table.position_x == 31
assert td_1.position_x == 41 # 31 + spacing
assert td_1.width == 4
assert td_2.position_x == 55 # 31 + 4 + spacing
assert td_2.width == 4
assert table.width == 38 # 3 * spacing + 2 * 4
page, = parse('''
<body style="width: 50px">
<table style="border-spacing: 1px; margin: 10%">
<tr>
<td style="border: 3px solid black"><img src=pattern.png></td>
<td style="border: 3px solid black">
<img src=pattern.png><img src=pattern.png>
</td>
</tr>
</table>
''')
html, = page.children
body, = html.children
table_wrapper, = body.children
table, = table_wrapper.children
row_group, = table.children
row, = row_group.children
td_1, td_2 = row.children
assert table_wrapper.position_x == 0
assert table.position_x == 5 # 0 + margin-left
assert td_1.position_x == 6 # 5 + border-spacing
assert td_1.width == 4
assert td_2.position_x == 17 # 6 + 4 + spacing + 2 * border
assert td_2.width == 8
assert table.width == 27 # 3 * spacing + 4 + 8 + 4 * border
page, = parse('''
<table style="border-spacing: 1px; margin: 5px">
<tr>
<td></td>
<td><img src=pattern.png><img src=pattern.png></td>
</tr>
<tr>
<td>
<img src=pattern.png>
<img src=pattern.png>
<img src=pattern.png>
</td>
<td><img src=pattern.png></td>
</tr>
</table>
''')
html, = page.children
body, = html.children
table_wrapper, = body.children
table, = table_wrapper.children
row_group, = table.children
row1, row2 = row_group.children
td_11, td_12 = row1.children
td_21, td_22 = row2.children
assert table_wrapper.position_x == 0
assert table.position_x == 5 # 0 + margin-left
assert td_11.position_x == td_21.position_x == 6 # 5 + spacing
assert td_11.width == td_21.width == 12
assert td_12.position_x == td_22.position_x == 19 # 6 + 12 + spacing
assert td_12.width == td_22.width == 8
assert table.width == 23 # 3 * spacing + 12 + 8
page, = parse('''
<table style="border-spacing: 1px; margin: 5px">
<tr>
<td style="border: 1px solid black"><img src=pattern.png></td>
<td style="border: 2px solid black; padding: 1px">
<img src=pattern.png>
</td>
</tr>
<tr>
<td style="border: 5px solid black"><img src=pattern.png></td>
<td><img src=pattern.png></td>
</tr>
</table>
''')
html, = page.children
body, = html.children
table_wrapper, = body.children
table, = table_wrapper.children
row_group, = table.children
row1, row2 = row_group.children
td_11, td_12 = row1.children
td_21, td_22 = row2.children
assert table_wrapper.position_x == 0
assert table.position_x == 5 # 0 + margin-left
assert td_11.position_x == td_21.position_x == 6 # 5 + spacing
assert td_11.width == 12 # 4 + 2 * 5 - 2 * 1
assert td_21.width == 4
assert td_12.position_x == td_22.position_x == 21 # 6 + 4 + 2 * b1 + sp
assert td_12.width == 4
assert td_22.width == 10 # 4 + 2 * 3
assert table.width == 27 # 3 * spacing + 4 + 4 + 2 * b1 + 2 * b2
page, = parse('''
<style>
@page { size: 100px 1000px; }
</style>
<table style="border-spacing: 1px; margin-right: 79px; font-size: 0">
<tr>
<td><img src=pattern.png></td>
<td>
<img src=pattern.png> <img src=pattern.png>
<img src=pattern.png> <img src=pattern.png>
<img src=pattern.png> <img src=pattern.png>
<img src=pattern.png> <img src=pattern.png>
<img src=pattern.png>
</td>
</tr>
<tr>
<td></td>
</tr>
</table>
''')
# Preferred minimum width is 2 * 4 + 3 * 1 = 11
html, = page.children
body, = html.children
table_wrapper, = body.children
table, = table_wrapper.children
row_group, = table.children
row1, row2 = row_group.children
td_11, td_12 = row1.children
td_21, = row2.children
assert table_wrapper.position_x == 0
assert table.position_x == 0
assert td_11.position_x == td_21.position_x == 1 # spacing
assert td_11.width == td_21.width == 5 # 4 + (width - pmw) * 1 / 10
assert td_12.position_x == 7 # 1 + 5 + sp
assert td_12.width == 13 # 4 + (width - pmw) * 9 / 10
assert table.width == 21
page, = parse('''
<table style="border-spacing: 10px; margin: 5px">
<colgroup>
<col style="width: 20px" />
</colgroup>
<tr>
<td></td>
<td style="width: 40px">a</td>
</tr>
</table>
''')
html, = page.children
body, = html.children
table_wrapper, = body.children
table, = table_wrapper.children
row_group, = table.children
row, = row_group.children
td_1, td_2 = row.children
assert table_wrapper.position_x == 0
assert table.position_x == 5 # 0 + margin-left
assert td_1.position_x == 15 # 0 + border-spacing
assert td_1.width == 20
assert td_2.position_x == 45 # 15 + 20 + border-spacing
assert td_2.width == 40
assert table.width == 90 # 20 + 40 + 3 * border-spacing
page, = parse('''
<table style="border-spacing: 10px; width: 120px; margin: 5px;
font-size: 0">
<tr>
<td style="width: 20px"><img src=pattern.png></td>
<td><img src=pattern.png style="width: 40px"></td>
</tr>
</table>
''')
html, = page.children
body, = html.children
table_wrapper, = body.children
table, = table_wrapper.children
row_group, = table.children
row, = row_group.children
td_1, td_2 = row.children
assert table_wrapper.position_x == 0
assert table.position_x == 5 # 0 + margin-left
assert td_1.position_x == 15 # 5 + border-spacing
assert td_1.width == 30 # 20 + ((120 - 20 - 40 - 3 * sp) * 1 / 3)
assert td_2.position_x == 55 # 15 + 30 + border-spacing
assert td_2.width == 60 # 40 + ((120 - 20 - 40 - 3 * sp) * 2 / 3)
assert table.width == 120
page, = parse('''
<table style="border-spacing: 10px; width: 110px; margin: 5px">
<tr>
<td style="width: 60px"></td>
<td></td>
</tr>
<tr>
<td style="width: 50px"></td>
<td style="width: 30px"></td>
</tr>
</table>
''')
html, = page.children
body, = html.children
table_wrapper, = body.children
table, = table_wrapper.children
row_group, = table.children
row_1, row_2 = row_group.children
td_1, td_2 = row_1.children
td_3, td_4 = row_2.children
assert table_wrapper.position_x == 0
assert table.position_x == 5 # 0 + margin-left
assert td_1.position_x == 15 # 0 + border-spacing
assert td_3.position_x == 15
assert td_1.width == 60
assert td_2.width == 30
assert td_2.position_x == 85 # 15 + 60 + border-spacing
assert td_4.position_x == 85
assert td_3.width == 60
assert td_4.width == 30
assert table.width == 120 # 60 + 30 + 3 * border-spacing
page, = parse('''
<table style="border-spacing: 0; width: 14px; margin: 10px">
<colgroup>
<col />
<col style="width: 6px" />
</colgroup>
<tr>
<td><img src=pattern.png><img src=pattern.png></td>
<td style="width: 8px"></td>
</tr>
</table>
''')
html, = page.children
body, = html.children
table_wrapper, = body.children
table, = table_wrapper.children
row_group, = table.children
row, = row_group.children
td_1, td_2 = row.children
assert table_wrapper.position_x == 0
assert table.position_x == 10 # 0 + margin-left
assert td_1.position_x == 10
assert td_1.width == 5 # 4 + ((14 - 4 - 8) * 8 / 16)
assert td_2.position_x == 15 # 10 + 5
assert td_2.width == 9 # 8 + ((14 - 4 - 8) * 8 / 16)
assert table.width == 14
page, = parse('''
<table style="border-spacing: 0">
<tr>
<td style="width: 10px"></td>
<td colspan="3"></td>
</tr>
<tr>
<td colspan="2" style="width: 22px"></td>
<td style="width: 8px"></td>
<td style="width: 8px"></td>
</tr>
<tr>
<td></td>
<td></td>
<td colspan="2"></td>
</tr>
</table>
''')
html, = page.children
body, = html.children
table_wrapper, = body.children
table, = table_wrapper.children
row_group, = table.children
row1, row2, row3 = row_group.children
td_11, td_12 = row1.children
td_21, td_22, td_23 = row2.children
td_31, td_32, td_33 = row3.children
assert table_wrapper.position_x == 0
assert table.position_x == 0
assert td_11.width == 16 # 10 + (22 - 10) / 2
assert td_12.width == 22 # (0 + (22 - 10) / 2) + 8 + 8
assert td_21.width == 22
assert td_22.width == 8
assert td_23.width == 8
assert td_31.width == 16
assert td_32.width == 6
assert td_33.width == 16
assert table.width == 38
page, = parse('''
<table style="border-spacing: 10px">
<tr>
<td style="width: 10px"></td>
<td colspan="3"></td>
</tr>
<tr>
<td colspan="2" style="width: 32px"></td>
<td style="width: 8px"></td>
<td style="width: 8px"></td>
</tr>
<tr>
<td></td>
<td></td>
<td colspan="2"></td>
</tr>
</table>
''')
html, = page.children
body, = html.children
table_wrapper, = body.children
table, = table_wrapper.children
row_group, = table.children
row1, row2, row3 = row_group.children
td_11, td_12 = row1.children
td_21, td_22, td_23 = row2.children
td_31, td_32, td_33 = row3.children
assert table_wrapper.position_x == 0
assert table.position_x == 0
assert td_11.width == 16 # 10 + (22 - 10) / 2
assert td_12.width == 42 # (0 + (22 - 10) / 2) + 8 + 8
assert td_21.width == 32
assert td_22.width == 8
assert td_23.width == 8
assert td_31.width == 16
assert td_32.width == 6
assert td_33.width == 26
assert table.width == 88
# Regression tests: these used to crash
page, = parse('''
<table style="width: 30px">
<tr>
<td colspan=2></td>
<td></td>
</tr>
</table>
''')
html, = page.children
body, = html.children
table_wrapper, = body.children
table, = table_wrapper.children
row_group, = table.children
row, = row_group.children
td_1, td_2 = row.children
assert td_1.width == 20
assert td_2.width == 10
assert table.width == 30
page, = parse('''
<table style="width: 20px">
<col />
<col />
<tr>
<td></td>
</tr>
</table>
''')
html, = page.children
body, = html.children
table_wrapper, = body.children
table, = table_wrapper.children
row_group, = table.children
row, = row_group.children
td_1, = row.children
assert td_1.width == 10 # TODO: should this be 20?
assert table.width == 20
page, = parse('''
<table style="width: 20px">
<col />
<col />
</table>
''')
html, = page.children
body, = html.children
table_wrapper, = body.children
table, = table_wrapper.children
column_group, = table.column_groups
column_1, column_2 = column_group.children
assert column_1.width == 10
assert column_2.width == 10
# Absolute table
page, = parse('''
<table style="width: 30px; position: absolute">
<tr>
<td colspan=2></td>
<td></td>
</tr>
</table>
''')
html, = page.children
body, = html.children
table_wrapper, = body.children
table, = table_wrapper.children
row_group, = table.children
row, = row_group.children
td_1, td_2 = row.children
assert td_1.width == 20
assert td_2.width == 10
assert table.width == 30
# With border-collapse
page, = parse('''
<style>
/* Do not apply: */
colgroup, col, tbody, tr, td { margin: 1000px }
</style>
<table style="border-collapse: collapse; border: 10px solid;
/* ignored with collapsed borders: */
border-spacing: 10000px; padding: 1000px">
<colgroup>
<col style="width: 30px" />
</colgroup>
<tbody>
<tr>
<td style="padding: 2px"></td>
<td style="width: 34px; padding: 10px; border: 2px solid"></td>
</tr>
</tbody>
</table>
''')
html, = page.children
body, = html.children
table_wrapper, = body.children
table, = table_wrapper.children
row_group, = table.children
row, = row_group.children
td_1, td_2 = row.children
assert table_wrapper.position_x == 0
assert table.position_x == 0
assert table.border_left_width == 5 # half of the collapsed 10px border
assert td_1.position_x == 5 # border-spacing is ignored
assert td_1.margin_width() == 30 # as <col>
assert td_1.width == 20 # 30 - 5 (border-left) - 1 (border-right) - 2*2
assert td_2.position_x == 35
assert td_2.width == 34
assert td_2.margin_width() == 60 # 34 + 2*10 + 5 + 1
assert table.width == 90 # 30 + 60
assert table.margin_width() == 100 # 90 + 2*5 (border)
@assert_no_logs
def test_lists():
"""Test the lists."""
page, = parse('''
<style>
body { margin: 0 }
ul { margin-left: 50px; list-style: inside circle }
</style>
<ul>
<li>abc</li>
</ul>
''')
unordered_list, = body_children(page)
list_item, = unordered_list.children
line, = list_item.children
marker, content = line.children
assert marker.text == ''
assert marker.margin_left == 0
assert marker.margin_right == 8
assert content.text == 'abc'
page, = parse('''
<style>
body { margin: 0 }
ul { margin-left: 50px; }
</style>
<ul>
<li>abc</li>
</ul>
''')
unordered_list, = body_children(page)
list_item, = unordered_list.children
marker = list_item.outside_list_marker
font_size = marker.style.font_size
assert marker.margin_right == 0.5 * font_size # 0.5em
assert marker.position_x == (
list_item.padding_box_x() - marker.width - marker.margin_right)
assert marker.position_y == list_item.position_y
assert marker.text == ''
line, = list_item.children
content, = line.children
assert content.text == 'abc'
@assert_no_logs
def test_empty_linebox():
"""Test lineboxes with no content other than space-like characters."""
page, = parse('<p> </p>')
paragraph, = body_children(page)
assert len(paragraph.children) == 0
assert paragraph.height == 0
# Whitespace removed at the beginning of the line => empty line => no line
page, = parse('''
<style>
p { width: 1px }
</style>
<p><br> </p>
''')
paragraph, = body_children(page)
# TODO: The second line should be removed
pytest.xfail()
assert len(paragraph.children) == 1
@assert_no_logs
def test_breaking_linebox():
"""Test lineboxes breaks with a lot of text and deep nesting."""
page, = parse('''
<style>
p { font-size: 13px;
width: 300px;
font-family: %(fonts)s;
background-color: #393939;
color: #FFFFFF;
line-height: 1;
text-decoration: underline overline line-through;}
</style>
<p><em>Lorem<strong> Ipsum <span>is very</span>simply</strong><em>
dummy</em>text of the printing and. naaaa </em> naaaa naaaa naaaa
naaaa naaaa naaaa naaaa naaaa</p>
''' % {'fonts': FONTS})
html, = page.children
body, = html.children
paragraph, = body.children
assert len(list(paragraph.children)) == 3
lines = paragraph.children
for line in lines:
assert line.style.font_size == 13
assert line.element_tag == 'p'
for child in line.children:
assert child.element_tag in ('em', 'p')
assert child.style.font_size == 13
if isinstance(child, boxes.ParentBox):
for child_child in child.children:
assert child.element_tag in ('em', 'strong', 'span')
assert child.style.font_size == 13
# See http://unicode.org/reports/tr14/
page, = parse('<pre>a\nb\rc\r\nd\u2029e</pre>')
html, = page.children
body, = html.children
pre, = body.children
lines = pre.children
texts = []
for line in lines:
text_box, = line.children
texts.append(text_box.text)
assert texts == ['a', 'b', 'c', 'd', 'e']
@assert_no_logs
def test_linebox_text():
"""Test the creation of line boxes."""
page, = parse('''
<style>
p { width: 165px; font-family:%(fonts)s;}
</style>
<p><em>Lorem Ipsum</em>is very <strong>coool</strong></p>
''' % {'fonts': FONTS})
paragraph, = body_children(page)
lines = list(paragraph.children)
assert len(lines) == 2
text = ' '.join(
(''.join(box.text for box in line.descendants()
if isinstance(box, boxes.TextBox)))
for line in lines)
assert text == 'Lorem Ipsumis very coool'
@assert_no_logs
def test_linebox_positions():
"""Test the position of line boxes."""
for width, expected_lines in [(165, 2), (1, 5), (0, 5)]:
page = '''
<style>
p { width:%(width)spx; font-family:%(fonts)s;
line-height: 20px }
</style>
<p>this is test for <strong>Weasyprint</strong></p>'''
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
@assert_no_logs
def test_forced_line_breaks():
"""Test <pre> and <br>."""
# These lines should be small enough to fit on the default A4 page
# with the default 12pt font-size.
page, = parse('''
<style> pre { line-height: 42px }</style>
<pre>Lorem ipsum dolor sit amet,
consectetur adipiscing elit.
Sed sollicitudin nibh
et turpis molestie tristique.</pre>
''')
pre, = body_children(page)
assert pre.element_tag == 'pre'
lines = pre.children
assert all(isinstance(line, boxes.LineBox) for line in lines)
assert len(lines) == 7
assert [line.height for line in lines] == [42] * 7
page, = parse('''
<style> p { line-height: 42px }</style>
<p>Lorem ipsum dolor sit amet,<br>
consectetur adipiscing elit.<br><br><br>
Sed sollicitudin nibh<br>
<br>
et turpis molestie tristique.</p>
''')
pre, = body_children(page)
assert pre.element_tag == 'p'
lines = pre.children
assert all(isinstance(line, boxes.LineBox) for line in lines)
assert len(lines) == 7
assert [line.height for line in lines] == [42] * 7
@assert_no_logs
def test_page_breaks():
"""Test the page breaks."""
pages = parse('''
<style>
@page { size: 100px; margin: 10px }
body { margin: 0 }
div { height: 30px; font-size: 20px; }
</style>
<div>1</div>
<div>2</div>
<div>3</div>
<div>4</div>
<div>5</div>
''')
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]]
# Same as above, but no content inside each <div>.
# This used to produce no page break.
pages = parse('''
<style>
@page { size: 100px; margin: 10px }
body { margin: 0 }
div { height: 30px }
</style>
<div/><div/><div/><div/><div/>
''')
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]]
pages = parse('''
<style>
@page { size: 100px; margin: 10px }
img { height: 30px; display: block }
</style>
<body>
<img src=pattern.png>
<img src=pattern.png>
<img src=pattern.png>
<img src=pattern.png>
<img src=pattern.png>
''')
page_images = []
for page in pages:
images = body_children(page)
assert all([img.element_tag == 'img' for img in images])
assert all([img.position_x == 10 for img in images])
page_images.append(images)
positions_y = [[img.position_y for img in images]
for images in page_images]
assert positions_y == [[10, 40], [10, 40], [10]]
page_1, page_2, page_3, page_4 = parse('''
<style>
@page { margin: 10px }
@page :left { margin-left: 50px }
@page :right { margin-right: 50px }
html { page-break-before: left }
div { page-break-after: left }
ul { page-break-before: always }
</style>
<div>1</div>
<p>2</p>
<p>3</p>
<article>
<section>
<ul><li>4</li></ul>
</section>
</article>
''')
# 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'
html, = page_2.children
assert page_2.margin_left == 10
assert page_2.margin_right == 50 # right page
assert not html.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
article, = body.children
section, = article.children
ulist, = section.children
assert ulist.element_tag == 'ul'
# Reference for the following test:
# Without any 'avoid', this breaks after the <div>
page_1, page_2 = parse('''
<style>
@page { size: 140px; margin: 0 }
img { height: 25px; vertical-align: top }
p { orphans: 1; widows: 1 }
</style>
<body>
<img src=pattern.png>
<div>
<p><img src=pattern.png><br/><img src=pattern.png><p>
<p><img src=pattern.png><br/><img src=pattern.png><p>
</div><!-- page break here -->
<img src=pattern.png>
''')
html, = page_1.children
body, = html.children
img_1, div = body.children
assert img_1.position_y == 0
assert img_1.height == 25
assert div.position_y == 25
assert div.height == 100
html, = page_2.children
body, = html.children
img_2, = body.children
assert img_2.position_y == 0
assert img_2.height == 25
# Adding a few page-break-*: avoid, the only legal break is
# before the <div>
page_1, page_2 = parse('''
<style>
@page { size: 140px; margin: 0 }
img { height: 25px; vertical-align: top }
p { orphans: 1; widows: 1 }
</style>
<body>
<img src=pattern.png><!-- page break here -->
<div>
<p style="page-break-inside: avoid">
><img src=pattern.png><br/><img src=pattern.png></p>
<p style="page-break-before: avoid; page-break-after: avoid;
widows: 2"
><img src=pattern.png><br/><img src=pattern.png></p>
</div>
<img src=pattern.png>
''')
html, = page_1.children
body, = html.children
img_1, = body.children
assert img_1.position_y == 0
assert img_1.height == 25
html, = page_2.children
body, = html.children
div, img_2 = body.children
assert div.position_y == 0
assert div.height == 100
assert img_2.position_y == 100
assert img_2.height == 25
page_1, page_2 = parse('''
<style>
@page { size: 140px; margin: 0 }
img { height: 25px; vertical-align: top }
p { orphans: 1; widows: 1 }
</style>
<body>
<img src=pattern.png><!-- page break here -->
<div>
<div>
<p style="page-break-inside: avoid">
><img src=pattern.png><br/><img src=pattern.png></p>
<p style="page-break-before: avoid; page-break-after: avoid;
widows: 2"
><img src=pattern.png><br/><img src=pattern.png></p>
</div>
<img src=pattern.png>
</div>
''')
html, = page_1.children
body, = html.children
img_1, = body.children
assert img_1.position_y == 0
assert img_1.height == 25
html, = page_2.children
body, = html.children
outer_div, = body.children
inner_div, img_2 = outer_div.children
assert inner_div.position_y == 0
assert inner_div.height == 100
assert img_2.position_y == 100
assert img_2.height == 25
# Reference for the next test
page_1, page_2, page_3 = parse('''
<style>
@page { size: 100px; margin: 0 }
img { height: 30px; display: block; }
p { orphans: 1; widows: 1 }
</style>
<body>
<div>
<img src=pattern.png style="page-break-after: always">
<section>
<img src=pattern.png>
<img src=pattern.png>
</section>
</div>
<img src=pattern.png><!-- page break here -->
<img src=pattern.png>
''')
html, = page_1.children
body, = html.children
div, = body.children
assert div.height == 30
html, = page_2.children
body, = html.children
div, img_4 = body.children
assert div.height == 60
assert img_4.height == 30
html, = page_3.children
body, = html.children
img_5, = body.children
assert img_5.height == 30
page_1, page_2, page_3 = parse('''
<style>
@page { size: 100px; margin: 0 }
img { height: 30px; display: block; }
p { orphans: 1; widows: 1 }
</style>
<body>
<div>
<img src=pattern.png style="page-break-after: always">
<section>
<img src=pattern.png><!-- page break here -->
<img src=pattern.png style="page-break-after: avoid">
</section>
</div>
<img src=pattern.png style="page-break-after: avoid">
<img src=pattern.png>
''')
html, = page_1.children
body, = html.children
div, = body.children
assert div.height == 30
html, = page_2.children
body, = html.children
div, = body.children
section, = div.children
img_2, = section.children
assert img_2.height == 30
# TODO: currently this is 60: we do not decrease the used height of
# blocks with 'height: auto' when we remove children from them for
# some page-break-*: avoid.
#assert div.height == 30
html, = page_3.children
body, = html.children
div, img_4, img_5, = body.children
assert div.height == 30
assert img_4.height == 30
assert img_5.height == 30
page_1, page_2, page_3 = parse('''
<style>
@page {
@bottom-center { content: counter(page) }
}
@page:blank {
@bottom-center { content: none }
}
</style>
<p style="page-break-after: right">foo</p>
<p>bar</p>
''')
assert len(page_1.children) == 2 # content and @bottom-center
assert len(page_2.children) == 1 # content only
assert len(page_3.children) == 2 # content and @bottom-center
page_1, page_2 = parse('''
<style>
@page { size: 75px; margin: 0 }
div { height: 20px }
</style>
<body>
<div></div>
<section>
<div></div>
<div style="page-break-after: avoid">
<div style="position: absolute"></div>
<div style="position: fixed"></div>
</div>
</section>
<div></div>
''')
html, = page_1.children
body, = html.children
div_1, section = body.children
div_2, = section.children
assert div_1.position_y == 0
assert div_2.position_y == 20
assert div_1.height == 20
assert div_2.height == 20
html, = page_2.children
body, = html.children
section, div_4 = body.children
div_3, = section.children
absolute, fixed = div_3.children
assert div_3.position_y == 0
assert div_4.position_y == 20
assert div_3.height == 20
assert div_4.height == 20
@assert_no_logs
def test_orphans_widows_avoid():
"""Test orphans and widows control."""
def line_distribution(css):
pages = parse('''
<style>
@page { size: 200px }
h1 { height: 120px }
p { line-height: 20px;
width: 1px; /* line break at each word */
%s }
</style>
<h1>Tasty test</h1>
<!-- There is room for 4 lines after h1 on the fist page -->
<p>
one
two
three
four
five
six
seven
</p>
''' % css)
line_counts = []
for i, page in enumerate(pages):
html, = page.children
body, = html.children
if i == 0:
body_children = body.children[1:] # skip h1
else:
body_children = body.children
if body_children:
paragraph, = body_children
line_counts.append(len(paragraph.children))
else:
line_counts.append(0)
return line_counts
assert line_distribution('orphans: 2; widows: 2') == [4, 3]
assert line_distribution('orphans: 5; widows: 2') == [0, 7]
assert line_distribution('orphans: 2; widows: 4') == [3, 4]
assert line_distribution('orphans: 4; widows: 4') == [0, 7]
assert line_distribution(
'orphans: 2; widows: 2; page-break-inside: avoid') == [0, 7]
@assert_no_logs
def test_table_page_breaks():
"""Test the page breaks inside tables."""
def run(html):
pages = parse(html)
rows_per_page = []
rows_position_y = []
for i, page in enumerate(pages):
html, = page.children
body, = html.children
if i == 0:
body_children = body.children[1:] # skip h1
else:
body_children = body.children
if not body_children:
rows_per_page.append(0)
continue
table_wrapper, = body_children
table, = table_wrapper.children
rows_in_this_page = 0
for group in table.children:
assert group.children, 'found an empty table group'
for row in group.children:
rows_in_this_page += 1
rows_position_y.append(row.position_y)
cell, = row.children
line, = cell.children
text, = line.children
assert text.text == 'row %i' % len(rows_position_y)
rows_per_page.append(rows_in_this_page)
return rows_per_page, rows_position_y
rows_per_page, rows_position_y = run('''
<style>
@page { size: 120px }
table { table-layout: fixed; width: 100% }
h1 { height: 30px }
td { height: 40px }
</style>
<h1>Dummy title</h1>
<table>
<tr><td>row 1</td></tr>
<tr><td>row 2</td></tr>
<tr><td>row 3</td></tr>
<tr><td>row 4</td></tr>
<tr><td>row 5</td></tr>
<tr><td style="height: 300px"> <!-- overflow the page -->
row 6</td></tr>
<tr><td>row 7</td></tr>
<tr><td>row 8</td></tr>
</table>
''')
assert rows_per_page == [2, 3, 1, 2]
assert rows_position_y == [30, 70, 0, 40, 80, 0, 0, 40]
rows_per_page, rows_position_y = run('''
<style>
@page { size: 120px }
h1 { height: 30px}
td { height: 40px }
table { table-layout: fixed; width: 100%;
page-break-inside: avoid }
</style>
<h1>Dummy title</h1>
<table>
<tr><td>row 1</td></tr>
<tr><td>row 2</td></tr>
<tr><td>row 3</td></tr>
<tr><td>row 4</td></tr>
</table>
''')
assert rows_per_page == [0, 3, 1]
assert rows_position_y == [0, 40, 80, 0]
rows_per_page, rows_position_y = run('''
<style>
@page { size: 120px }
h1 { height: 30px}
td { height: 40px }
table { table-layout: fixed; width: 100%;
page-break-inside: avoid }
</style>
<h1>Dummy title</h1>
<table>
<tbody>
<tr><td>row 1</td></tr>
<tr><td>row 2</td></tr>
<tr><td>row 3</td></tr>
</tbody>
<tr><td>row 4</td></tr>
</table>
''')
assert rows_per_page == [0, 3, 1]
assert rows_position_y == [0, 40, 80, 0]
rows_per_page, rows_position_y = run('''
<style>
@page { size: 120px }
h1 { height: 30px}
td { height: 40px }
table { table-layout: fixed; width: 100% }
</style>
<h1>Dummy title</h1>
<table>
<tr><td>row 1</td></tr>
<tbody style="page-break-inside: avoid">
<tr><td>row 2</td></tr>
<tr><td>row 3</td></tr>
</tbody>
</table>
''')
assert rows_per_page == [1, 2]
assert rows_position_y == [30, 0, 40]
pages = parse('''
<style>
@page { size: 100px }
</style>
<h1 style="margin: 0; height: 30px">Lipsum</h1>
<!-- Leave 70px on the first page: enough for the header or row1
but not both. -->
<table style="border-spacing: 0; font-size: 5px">
<thead>
<tr><td style="height: 20px">Header</td></tr>
</thead>
<tbody>
<tr><td style="height: 60px">Row 1</td></tr>
<tr><td style="height: 10px">Row 2</td></tr>
<tr><td style="height: 50px">Row 3</td></tr>
<tr><td style="height: 61px">Row 4</td></tr>
<tr><td style="height: 90px">Row 5</td></tr>
</tbody>
<tfoot>
<tr><td style="height: 20px">Footer</td></tr>
</tfoot>
</table>
''')
rows_per_page = []
for i, page in enumerate(pages):
groups = []
html, = page.children
body, = html.children
table_wrapper, = body.children
if i == 0:
assert table_wrapper.element_tag == 'h1'
else:
table, = table_wrapper.children
for group in table.children:
assert group.children, 'found an empty table group'
rows = []
for row in group.children:
cell, = row.children
line, = cell.children
text, = line.children
rows.append(text.text)
groups.append(rows)
rows_per_page.append(groups)
assert rows_per_page == [
[],
[['Header'], ['Row 1'], ['Footer']],
[['Header'], ['Row 2', 'Row 3'], ['Footer']],
[['Header'], ['Row 4']],
[['Row 5']]
]
@assert_no_logs
def test_inlinebox_spliting():
"""Test the inline boxes spliting."""
for width in [10000, 100, 10, 0]:
page, = parse('''
<style>p { font-family:%(fonts)s; width: %(width)spx; }</style>
<p><strong>WeasyPrint is a free software visual rendering engine
for HTML and CSS.</strong></p>
''' % {'fonts': FONTS, 'width': width})
html, = page.children
body, = html.children
paragraph, = body.children
lines = paragraph.children
if width == 10000:
assert len(lines) == 1
else:
assert len(lines) > 1
text_parts = []
for line in lines:
strong, = line.children
text, = strong.children
text_parts.append(text.text)
assert ' '.join(text_parts) == ('WeasyPrint is a free software visual '
'rendering engine for HTML and CSS.')
@assert_no_logs
def test_page_and_linebox_breaking():
"""Test the linebox text after spliting linebox and page."""
# The empty <span/> tests a corner case
# in skip_first_whitespace()
pages = parse('''
<style>
div { font-family:%(fonts)s; font-size:22px}
@page { size: 100px; margin:2px; border:1px solid }
body { margin: 0 }
</style>
<div><span/>1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15</div>
''' % {'fonts': FONTS})
texts = []
for page in pages:
html, = page.children
body, = html.children
div, = body.children
lines = div.children
for line in lines:
line_texts = []
for child in line.descendants():
if isinstance(child, boxes.TextBox):
line_texts.append(child.text)
texts.append(''.join(line_texts))
assert len(pages) == 2
assert ' '.join(texts) == \
'1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15'
@assert_no_logs
def test_whitespace_processing():
"""Test various spaces and tabulations processing."""
for source in ['a', ' a ', ' \n \ta', ' a\t ']:
page, = parse('<p><em>%s</em></p>' % source)
html, = page.children
body, = html.children
p, = body.children
line, = p.children
em, = line.children
text, = em.children
assert text.text == 'a', 'source was %r' % (source,)
page, = parse('<p style="white-space: pre-line">\n\n<em>%s</em></pre>'
% source.replace('\n', ' '))
html, = page.children
body, = html.children
p, = body.children
_line1, _line2, line3 = p.children
em, = line3.children
text, = em.children
assert text.text == 'a', 'source was %r' % (source,)
@assert_no_logs
def test_images():
"""Test that width, height and ratio of images are respected."""
def get_img(html):
page, = parse(html)
html, = page.children
body, = html.children
line, = body.children
img, = line.children
return body, img
# Try a few image formats
for html in [
'<img src="%s">' % url for url in [
'pattern.png', 'pattern.gif', 'blue.jpg', 'pattern.svg',
"data:image/svg+xml,<svg width='4' height='4'></svg>",
"DatA:image/svg+xml,<svg width='4px' height='4px'></svg>",
]
] + [
'<embed src=pattern.png>',
'<embed src=pattern.svg>',
'<embed src=really-a-png.svg type=image/png>',
'<embed src=really-a-svg.png type=image/svg+xml>',
'<object data=pattern.png>',
'<object data=pattern.svg>',
'<object data=really-a-png.svg type=image/png>',
'<object data=really-a-svg.png type=image/svg+xml>',
]:
body, img = get_img(html)
assert img.width == 4
assert img.height == 4
# With physical units
url = "data:image/svg+xml,<svg width='2.54cm' height='0.5in'></svg>"
body, img = get_img('<img src="%s">' % url)
assert img.width == 96
assert img.height == 48
# Invalid images
for url in [
'nonexistent.png',
'unknownprotocol://weasyprint.org/foo.png',
'data:image/unknowntype,Not an image',
# Invalid protocol
'datå:image/svg+xml,<svg width="4" height="4"></svg>',
# zero-byte images
'data:image/png,',
'data:image/jpeg,',
'data:image/svg+xml,',
# Incorrect format
'data:image/png,Not a PNG',
'data:image/jpeg,Not a JPEG',
'data:image/svg+xml,<svg>invalid xml',
'really-a-svg.png',
]:
with capture_logs() as logs:
body, img = get_img("<img src='%s' alt='invalid image'>" % url)
assert len(logs) == 1
assert 'WARNING: Error for image' in logs[0]
assert isinstance(img, boxes.InlineBox) # not a replaced box
text, = img.children
assert text.text == 'invalid image', url
with capture_logs() as logs:
parse('<img src=nonexistent.png><img src=nonexistent.png>')
# Failures are cached too: only one warning
assert len(logs) == 1
assert 'WARNING: Error for image' in logs[0]
# Layout rules try to preserve the ratio, so the height should be 40px too:
body, img = get_img('''<body style="font-size: 0">
<img src="pattern.png" style="width: 40px">''')
assert body.height == 40
assert img.position_y == 0
assert img.width == 40
assert img.height == 40
body, img = get_img('''<body style="font-size: 0">
<img src="pattern.png" style="height: 40px">''')
assert body.height == 40
assert img.position_y == 0
assert img.width == 40
assert img.height == 40
# Same with percentages
body, img = get_img('''<body style="font-size: 0"><p style="width: 200px">
<img src="pattern.png" style="width: 20%">''')
assert body.height == 40
assert img.position_y == 0
assert img.width == 40
assert img.height == 40
body, img = get_img('''<body style="font-size: 0">
<img src="pattern.png" style="min-width: 40px">''')
assert body.height == 40
assert img.position_y == 0
assert img.width == 40
assert img.height == 40
body, img = get_img('<img src="pattern.png" style="max-width: 2px">')
assert img.width == 2
assert img.height == 2
# display: table-cell is ignored. XXX Should it?
page, = parse('''<body style="font-size: 0">
<img src="pattern.png" style="width: 40px">
<img src="pattern.png" style="width: 60px; display: table-cell">
''')
html, = page.children
body, = html.children
line, = body.children
img_1, img_2 = line.children
assert body.height == 60
assert img_1.width == 40
assert img_1.height == 40
assert img_2.width == 60
assert img_2.height == 60
assert img_1.position_y == 20
assert img_2.position_y == 0
# Block-level image:
page, = parse('''
<style>
@page { size: 100px }
img { width: 40px; margin: 10px auto; display: block }
</style>
<body>
<img src="pattern.png">
''')
html, = page.children
body, = html.children
img, = body.children
assert img.element_tag == 'img'
assert img.position_x == 0
assert img.position_y == 0
assert img.width == 40
assert img.height == 40
assert img.content_box_x() == 30 # (100 - 40) / 2 == 30px for margin-left
assert img.content_box_y() == 10
page, = parse('''
<style>
@page { size: 100px }
img { min-width: 40%; margin: 10px auto; display: block }
</style>
<body>
<img src="pattern.png">
''')
html, = page.children
body, = html.children
img, = body.children
assert img.element_tag == 'img'
assert img.position_x == 0
assert img.position_y == 0
assert img.width == 40
assert img.height == 40
assert img.content_box_x() == 30 # (100 - 40) / 2 == 30px for margin-left
assert img.content_box_y() == 10
page, = parse('''
<style>
@page { size: 100px }
img { min-width: 40px; margin: 10px auto; display: block }
</style>
<body>
<img src="pattern.png">
''')
html, = page.children
body, = html.children
img, = body.children
assert img.element_tag == 'img'
assert img.position_x == 0
assert img.position_y == 0
assert img.width == 40
assert img.height == 40
assert img.content_box_x() == 30 # (100 - 40) / 2 == 30px for margin-left
assert img.content_box_y() == 10
page, = parse('''
<style>
@page { size: 100px }
img { min-height: 30px; max-width: 2px;
margin: 10px auto; display: block }
</style>
<body>
<img src="pattern.png">
''')
html, = page.children
body, = html.children
img, = body.children
assert img.element_tag == 'img'
assert img.position_x == 0
assert img.position_y == 0
assert img.width == 2
assert img.height == 30
assert img.content_box_x() == 49 # (100 - 2) / 2 == 49px for margin-left
assert img.content_box_y() == 10
@assert_no_logs
def test_vertical_align():
"""Test various values of vertical-align."""
"""
+-------+ <- position_y = 0
+-----+ |
40px | | | 60px
| | |
+-----+-------+ <- baseline
"""
page, = parse('''
<span>
<img src="pattern.png" style="width: 40px"
><img src="pattern.png" style="width: 60px"
></span>''')
html, = page.children
body, = html.children
line, = body.children
span, = line.children
img_1, img_2 = span.children
assert img_1.height == 40
assert img_2.height == 60
assert img_1.position_y == 20
assert img_2.position_y == 0
# 60px + the descent of the font below the baseline
assert 60 < line.height < 70
assert body.height == line.height
"""
+-------+ <- position_y = 0
35px | |
+-----+ | 60px
40px | | |
| +-------+ <- baseline
+-----+ 15px
"""
page, = parse('''
<span>
<img src="pattern.png" style="width: 40px; vertical-align: -15px"
><img src="pattern.png" style="width: 60px"></span>''')
html, = page.children
body, = html.children
line, = body.children
span, = line.children
img_1, img_2 = span.children
assert img_1.height == 40
assert img_2.height == 60
assert img_1.position_y == 35
assert img_2.position_y == 0
assert line.height == 75
assert body.height == line.height
# Same as previously, but with percentages
page, = parse('''
<span style="line-height: 10px">
<img src="pattern.png" style="width: 40px; vertical-align: -150%"
><img src="pattern.png" style="width: 60px"></span>''')
html, = page.children
body, = html.children
line, = body.children
span, = line.children
img_1, img_2 = span.children
assert img_1.height == 40
assert img_2.height == 60
assert img_1.position_y == 35
assert img_2.position_y == 0
assert line.height == 75
assert body.height == line.height
# Same again, but have the vertical-align on an inline box.
page, = parse('''
<span style="line-height: 10px">
<span style="line-height: 10px; vertical-align: -15px">
<img src="pattern.png" style="width: 40px"></span>
<img src="pattern.png" style="width: 60px"></span>''')
html, = page.children
body, = html.children
line, = body.children
span_1, = line.children
span_2, _whitespace, img_1 = span_1.children
img_1, = span_2.children
assert img_1.height == 40
assert img_2.height == 60
assert img_1.position_y == 35
assert img_2.position_y == 0
assert line.height == 75
assert body.height == line.height
# Same as previously, but with percentages
page, = parse('''
<span style="line-height: 12px; font-size: 12px; font-family: 'ahem'">
<img src="pattern.png" style="width: 40px; vertical-align: middle"
><img src="pattern.png" style="width: 60px"></span>''')
html, = page.children
body, = html.children
line, = body.children
span, = line.children
img_1, img_2 = span.children
assert img_1.height == 40
assert img_2.height == 60
# middle of the image (position_y + 20) is at half the ex-height above
# the baseline of the parent. The ex-height of Ahem is something like 0.8em
assert img_1.position_y == 35.2 # 60 - 0.5 * 0.8 * font-size - 40/2
assert img_2.position_y == 0
assert line.height == 75.2
assert body.height == line.height
# sup and sub currently mean +/- 0.5 em
# With the initial 16px font-size, thats 8px.
page, = parse('''
<span style="line-height: 10px">
<img src="pattern.png" style="width: 60px"
><img src="pattern.png" style="width: 40px; vertical-align: super"
><img src="pattern.png" style="width: 40px; vertical-align: sub"
></span>''')
html, = page.children
body, = html.children
line, = body.children
span, = line.children
img_1, img_2, img_3 = span.children
assert img_1.height == 60
assert img_2.height == 40
assert img_3.height == 40
assert img_1.position_y == 0
assert img_2.position_y == 12 # 20 - 16 * 0.5
assert img_3.position_y == 28 # 20 + 16 * 0.5
assert line.height == 68
assert body.height == line.height
page, = parse('''
<body style="line-height: 10px">
<span>
<img src="pattern.png" style="vertical-align: text-top"
><img src="pattern.png" style="vertical-align: text-bottom"
></span>''')
html, = page.children
body, = html.children
line, = body.children
span, = line.children
img_1, img_2 = span.children
assert img_1.height == 4
assert img_2.height == 4
assert img_1.position_y == 0
assert img_2.position_y == 12 # 16 - 4
assert line.height == 16
assert body.height == line.height
# This case used to cause an exception:
# The second span has no children but should count for line heights
# since it has padding.
page, = parse('''<span style="line-height: 1.5">
<span style="padding: 1px"></span></span>''')
html, = page.children
body, = html.children
line, = body.children
span_1, = line.children
span_2, = span_1.children
assert span_1.height == 16
assert span_2.height == 16
# The lines strut does not has 'line-height: normal' but the result should
# be smaller than 1.5.
assert span_1.margin_height() == 24
assert span_2.margin_height() == 24
assert line.height == 24
page, = parse('''
<span>
<img src="pattern.png" style="width: 40px; vertical-align: -15px"
><img src="pattern.png" style="width: 60px"
></span><div style="display: inline-block; vertical-align: 3px">
<div>
<div style="height: 100px">foo</div>
<div>
<img src="pattern.png" style="
width: 40px; vertical-align: -15px"
><img src="pattern.png" style="width: 60px"
></div>
</div>
</div>''')
html, = page.children
body, = html.children
line, = body.children
span, div_1 = line.children
assert line.height == 178
assert body.height == line.height
# Same as earlier
img_1, img_2 = span.children
assert img_1.height == 40
assert img_2.height == 60
assert img_1.position_y == 138
assert img_2.position_y == 103
div_2, = div_1.children
div_3, div_4 = div_2.children
div_line, = div_4.children
div_img_1, div_img_2 = div_line.children
assert div_1.position_y == 0
assert div_1.height == 175
assert div_3.height == 100
assert div_line.height == 75
assert div_img_1.height == 40
assert div_img_2.height == 60
assert div_img_1.position_y == 135
assert div_img_2.position_y == 100
# The first two images bring the top of the line box 30px above
# the baseline and 10px below.
# Each of the inner span
page, = parse('''
<span style="font-size: 0">
<img src="pattern.png" style="vertical-align: 26px">
<img src="pattern.png" style="vertical-align: -10px">
<span style="vertical-align: top">
<img src="pattern.png" style="vertical-align: -10px">
<span style="vertical-align: -10px">
<img src="pattern.png" style="vertical-align: bottom">
</span>
</span>
<span style="vertical-align: bottom">
<img src="pattern.png" style="vertical-align: 6px">
</span>
</span>''')
html, = page.children
body, = html.children
line, = body.children
span_1, = line.children
img_1, img_2, span_2, span_4 = span_1.children
img_3, span_3 = span_2.children
img_4, = span_3.children
img_5, = span_4.children
assert body.height == line.height
assert line.height == 40
assert img_1.position_y == 0
assert img_2.position_y == 36
assert img_3.position_y == 6
assert img_4.position_y == 36
assert img_5.position_y == 30
page, = parse('''
<span style="font-size: 0">
<img src="pattern.png" style="vertical-align: bottom">
<img src="pattern.png" style="vertical-align: top; height: 100px">
</span>
''')
html, = page.children
body, = html.children
line, = body.children
span, = line.children
img_1, img_2 = span.children
assert img_1.position_y == 96
assert img_2.position_y == 0
# Reference for the next test
page, = parse('''
<span style="font-size: 0; vertical-align: top">
<img src="pattern.png">
</span>
''')
html, = page.children
body, = html.children
line, = body.children
span, = line.children
img_1, = span.children
assert img_1.position_y == 0
# Should be the same as above
page, = parse('''
<span style="font-size: 0; vertical-align: top; display: inline-block">
<img src="pattern.png">
</span>''')
html, = page.children
body, = html.children
line_1, = body.children
span, = line_1.children
line_2, = span.children
img_1, = line_2.children
assert img_1.element_tag == 'img'
assert img_1.position_y == 0
@assert_no_logs
def test_text_align_left():
"""Test the left text alignment."""
"""
<--------------------> page, body
+-----+
+---+ |
| | |
+---+-----+
^ ^ ^ ^
x=0 x=40 x=100 x=200
"""
page, = parse('''
<style>
@page { size: 200px }
</style>
<body>
<img src="pattern.png" style="width: 40px"
><img src="pattern.png" style="width: 60px">''')
html, = page.children
body, = html.children
line, = body.children
img_1, img_2 = line.children
# initial value for text-align: left (in ltr text)
assert img_1.position_x == 0
assert img_2.position_x == 40
@assert_no_logs
def test_text_align_right():
"""Test the right text alignment."""
"""
<--------------------> page, body
+-----+
+---+ |
| | |
+---+-----+
^ ^ ^ ^
x=0 x=100 x=200
x=140
"""
page, = parse('''
<style>
@page { size: 200px }
body { text-align: right }
</style>
<body>
<img src="pattern.png" style="width: 40px"
><img src="pattern.png" style="width: 60px">''')
html, = page.children
body, = html.children
line, = body.children
img_1, img_2 = line.children
assert img_1.position_x == 100 # 200 - 60 - 40
assert img_2.position_x == 140 # 200 - 60
@assert_no_logs
def test_text_align_center():
"""Test the center text alignment."""
"""
<--------------------> page, body
+-----+
+---+ |
| | |
+---+-----+
^ ^ ^ ^
x= x=50 x=150
x=90
"""
page, = parse('''
<style>
@page { size: 200px }
body { text-align: center }
</style>
<body>
<img src="pattern.png" style="width: 40px"
><img src="pattern.png" style="width: 60px">''')
html, = page.children
body, = html.children
line, = body.children
img_1, img_2 = line.children
assert img_1.position_x == 50
assert img_2.position_x == 90
@assert_no_logs
def test_text_align_justify():
"""Test justified text."""
page, = parse('''
<style>
@page { size: 300px 1000px }
body { text-align: justify }
</style>
<p><img src="pattern.png" style="width: 40px"> &#20;
<strong>
<img src="pattern.png" style="width: 60px"> &#20;
<img src="pattern.png" style="width: 10px"> &#20;
<img src="pattern.png" style="width: 100px"
></strong><img src="pattern.png" style="width: 290px"
><!-- Last image will be on its own line. -->''')
html, = page.children
body, = html.children
paragraph, = body.children
line_1, line_2 = paragraph.children
image_1, space_1, strong = line_1.children
image_2, space_2, image_3, space_3, image_4 = strong.children
image_5, = line_2.children
assert space_1.text == ' '
assert space_2.text == ' '
assert space_3.text == ' '
assert image_1.position_x == 0
assert space_1.position_x == 40
assert strong.position_x == 70
assert image_2.position_x == 70
assert space_2.position_x == 130
assert image_3.position_x == 160
assert space_3.position_x == 170
assert image_4.position_x == 200
assert strong.width == 230
assert image_5.position_x == 0
# single-word line (zero spaces)
page, = parse('''
<style>
body { text-align: justify; width: 50px }
</style>
<p>Supercalifragilisticexpialidocious bar</p>
''')
html, = page.children
body, = html.children
paragraph, = body.children
line_1, line_2 = paragraph.children
text, = line_1.children
assert text.position_x == 0
@assert_no_logs
def test_word_spacing():
"""Test word-spacing."""
# keep the empty <style> as a regression test: element.text is None
# (Not a string.)
page, = parse('''
<style></style>
<body><strong>Lorem ipsum dolor<em>sit amet</em></strong></body>
''')
html, = page.children
body, = html.children
line, = body.children
strong_1, = line.children
assert 200 <= strong_1.width <= 250
# TODO: Pango gives only half of word-spacing to a space at the end
# of a TextBox. Is this what we want?
page, = parse('''
<style>strong { word-spacing: 11px }</style>
<body><strong>Lorem ipsum dolor<em>sit amet</em></strong></body>
''')
html, = page.children
body, = html.children
line, = body.children
strong_2, = line.children
assert strong_2.width - strong_1.width == 33
@assert_no_logs
def test_letter_spacing():
"""Test letter-spacing."""
page, = parse('''
<body><strong>Supercalifragilisticexpialidocious</strong></body>
''')
html, = page.children
body, = html.children
line, = body.children
strong_1, = line.children
assert 250 <= strong_1.width <= 300
page, = parse('''
<style>strong { letter-spacing: 11px }</style>
<body><strong>Supercalifragilisticexpialidocious</strong></body>
''')
html, = page.children
body, = html.children
line, = body.children
strong_2, = line.children
assert strong_2.width - strong_1.width == 33 * 11
@assert_no_logs
def test_text_indent():
"""Test the text-indent property."""
for indent in ['12px', '6%']: # 6% of 200px is 12px
page, = parse('''
<style>
@page { size: 220px }
body { margin: 10px; text-indent: %(indent)s }
</style>
<p>Some text that is long enough that it take at least three line,
but maybe more.
''' % {'indent': indent})
html, = page.children
body, = html.children
paragraph, = body.children
lines = paragraph.children
text_1, = lines[0].children
text_2, = lines[1].children
text_3, = lines[2].children
assert text_1.position_x == 22 # 10px margin-left + 12px indent
assert text_2.position_x == 10 # No indent
assert text_3.position_x == 10 # No indent
@assert_no_logs
def test_inline_replaced_auto_margins():
"""Test that auto margins are ignored for inline replaced boxes."""
page, = parse('''
<style>
@page { size: 200px }
img { display: inline; margin: auto; width: 50px }
</style>
<body><img src="pattern.png" />''')
html, = page.children
body, = html.children
line, = body.children
img, = line.children
assert img.margin_top == 0
assert img.margin_right == 0
assert img.margin_bottom == 0
assert img.margin_left == 0
@assert_no_logs
def test_empty_inline_auto_margins():
"""Test that horizontal auto margins are ignored for empty inline boxes."""
page, = parse('''
<style>
@page { size: 200px }
span { margin: auto }
</style>
<body><span></span>''')
html, = page.children
body, = html.children
block, = body.children
span, = block.children
assert span.margin_top != 0
assert span.margin_right == 0
assert span.margin_bottom != 0
assert span.margin_left == 0
@assert_no_logs
def test_box_sizing():
"""Test the box-sizing property.
http://www.w3.org/TR/css3-ui/#box-sizing
"""
page, = parse('''
<style>
@page { size: 100000px }
body { width: 10000px; margin: 0 }
div { width: 10%; height: 1000px;
margin: 100px; padding: 10px; border: 1px solid }
div:nth-child(2) { box-sizing: content-box }
div:nth-child(3) { box-sizing: padding-box }
div:nth-child(4) { box-sizing: border-box }
</style>
<div></div>
<div></div>
<div></div>
<div></div>
''')
html, = page.children
body, = html.children
div_1, div_2, div_3, div_4 = body.children
for div in div_1, div_2:
assert div.style.box_sizing == 'content-box'
assert div.width == 1000
assert div.height == 1000
assert div.padding_width() == 1020
assert div.padding_height() == 1020
assert div.border_width() == 1022
assert div.border_height() == 1022
assert div.margin_height() == 1222
# margin_width() is the width of the containing block
# padding-box
assert div_3.style.box_sizing == 'padding-box'
assert div_3.width == 980 # 1000 - 20
assert div_3.height == 980
assert div_3.padding_width() == 1000
assert div_3.padding_height() == 1000
assert div_3.border_width() == 1002
assert div_3.border_height() == 1002
assert div_3.margin_height() == 1202
# border-box
assert div_4.style.box_sizing == 'border-box'
assert div_4.width == 978 # 1000 - 20 - 2
assert div_4.height == 978
assert div_4.padding_width() == 998
assert div_4.padding_height() == 998
assert div_4.border_width() == 1000
assert div_4.border_height() == 1000
assert div_4.margin_height() == 1200
@assert_no_logs
def test_table_column_width():
source = '''
<style>
body { width: 20000px; margin: 0 }
table {
width: 10000px; margin: 0 auto; border-spacing: 100px 0;
table-layout: fixed
}
td { border: 10px solid; padding: 1px }
</style>
<table>
<col style="width: 10%">
<tr>
<td style="width: 30%" colspan=3>
<td>
</tr>
<tr>
<td>
<td>
<td>
<td>
</tr>
<tr>
<td>
<td colspan=12>This cell will be truncated to grid width
<td>This cell will be removed as it is beyond the grid width
</tr>
</table>
'''
with capture_logs() as logs:
page, = parse(source)
assert len(logs) == 1
assert logs[0] == ('WARNING: This table row has more columns than '
'the table, ignored 1 cells: (<TableCellBox td 25>,)')
html, = page.children
body, = html.children
wrapper, = body.children
table, = wrapper.children
row_group, = table.children
first_row, second_row, third_row = row_group.children
cells = [first_row.children, second_row.children, third_row.children]
assert len(first_row.children) == 2
assert len(second_row.children) == 4
# Third cell here is completly removed
assert len(third_row.children) == 2
assert body.position_x == 0
assert wrapper.position_x == 0
assert wrapper.margin_left == 5000
assert wrapper.content_box_x() == 5000 # auto margin-left
assert wrapper.width == 10000
assert table.position_x == 5000
assert table.width == 10000
assert row_group.position_x == 5100 # 5000 + border_spacing
assert row_group.width == 9800 # 10000 - 2*border-spacing
assert first_row.position_x == row_group.position_x
assert first_row.width == row_group.width
# This cell has colspan=3
assert cells[0][0].position_x == 5100 # 5000 + border-spacing
# `width` on a cell sets the content width
assert cells[0][0].width == 3000 # 30% of 10000px
assert cells[0][0].border_width() == 3022 # 3000 + borders + padding
# Second cell of the first line, but on the fourth and last column
assert cells[0][1].position_x == 8222 # 5100 + 3022 + border-spacing
assert cells[0][1].border_width() == 6678 # 10000 - 3022 - 3*100
assert cells[0][1].width == 6656 # 6678 - borders - padding
assert cells[1][0].position_x == 5100 # 5000 + border-spacing
# `width` on a column sets the border width of cells
assert cells[1][0].border_width() == 1000 # 10% of 10000px
assert cells[1][0].width == 978 # 1000 - borders - padding
assert cells[1][1].position_x == 6200 # 5100 + 1000 + border-spacing
assert cells[1][1].border_width() == 911 # (3022 - 1000 - 2*100) / 2
assert cells[1][1].width == 889 # 911 - borders - padding
assert cells[1][2].position_x == 7211 # 6200 + 911 + border-spacing
assert cells[1][2].border_width() == 911 # (3022 - 1000 - 2*100) / 2
assert cells[1][2].width == 889 # 911 - borders - padding
# Same as cells[0][1]
assert cells[1][3].position_x == 8222 # Also 7211 + 911 + border-spacing
assert cells[1][3].border_width() == 6678
assert cells[1][3].width == 6656
# Same as cells[1][0]
assert cells[2][0].position_x == 5100
assert cells[2][0].border_width() == 1000
assert cells[2][0].width == 978
assert cells[2][1].position_x == 6200 # Same as cells[1][1]
assert cells[2][1].border_width() == 8700 # 1000 - 1000 - 3*border-spacing
assert cells[2][1].width == 8678 # 8700 - borders - padding
assert cells[2][1].colspan == 3 # truncated to grid width
page, = parse('''
<style>
table { width: 1000px; border-spacing: 100px; table-layout: fixed }
</style>
<table>
<tr>
<td style="width: 50%">
<td style="width: 60%">
<td>
</tr>
</table>
''')
html, = page.children
body, = html.children
wrapper, = body.children
table, = wrapper.children
row_group, = table.children
row, = row_group.children
assert row.children[0].width == 500
assert row.children[1].width == 600
assert row.children[2].width == 0
assert table.width == 1500 # 500 + 600 + 4 * border-spacing
# Sum of columns width larger that the table width:
# increase the table width
page, = parse('''
<style>
table { width: 1000px; border-spacing: 100px; table-layout: fixed }
td { width: 60% }
</style>
<table>
<tr>
<td>
<td>
</tr>
</table>
''')
html, = page.children
body, = html.children
wrapper, = body.children
table, = wrapper.children
row_group, = table.children
row, = row_group.children
cell_1, cell_2 = row.children
assert cell_1.width == 600 # 60% of 1000px
assert cell_2.width == 600
assert table.width == 1500 # 600 + 600 + 3*border-spacing
assert wrapper.width == table.width
@assert_no_logs
def test_table_row_height():
page, = parse('''
<table style="width: 1000px; border-spacing: 0 100px;
font: 20px/1em serif; margin: 3px; table-layout: fixed">
<tr>
<td rowspan=0 style="height: 420px; vertical-align: top"></td>
<td>X<br>X<br>X</td>
<td><table style="margin-top: 20px;
border-spacing: 0">X</table></td>
<td style="vertical-align: top">X</td>
<td style="vertical-align: middle">X</td>
<td style="vertical-align: bottom">X</td>
</tr>
<tr>
<!-- cells with no text (no line boxes) is a corner case
in cell baselines -->
<td style="padding: 15px"></td>
<td><div style="height: 10px"></div></td>
</tr>
<tr></tr>
<tr>
<td style="vertical-align: bottom"></td>
</tr>
</table>
''')
html, = page.children
body, = html.children
wrapper, = body.children
table, = wrapper.children
row_group, = table.children
assert wrapper.position_y == 0
assert table.position_y == 3 # 0 + margin-top
assert table.height == 620 # sum of row heigths + 5*border-spacing
assert wrapper.height == table.height
assert row_group.position_y == 103 # 3 + border-spacing
assert row_group.height == 420 # 620 - 2*border-spacing
assert [row.height for row in row_group.children] == [
80, 30, 0, 10]
assert [row.position_y for row in row_group.children] == [
# cumulative sum of previous row heights and border-spacings
103, 283, 413, 513]
assert [[cell.height for cell in row.children]
for row in row_group.children] == [
[420, 60, 40, 20, 20, 20],
[0, 10],
[],
[0]
]
assert [[cell.border_height() for cell in row.children]
for row in row_group.children] == [
[420, 80, 80, 80, 80, 80],
[30, 30],
[],
[10]
]
# The baseline of the first row is at 40px because of the third column.
# The second column thus gets a top padding of 20px pushes the bottom
# to 80px.The middle is at 40px.
assert [[cell.padding_top for cell in row.children]
for row in row_group.children] == [
[0, 20, 0, 0, 30, 60],
[15, 5],
[],
[10]
]
assert [[cell.padding_bottom for cell in row.children]
for row in row_group.children] == [
[0, 0, 40, 60, 30, 0],
[15, 15],
[],
[0]
]
assert [[cell.position_y for cell in row.children]
for row in row_group.children] == [
[103, 103, 103, 103, 103, 103],
[283, 283],
[],
[513]
]
# A cell box cannot extend beyond the last row box of a table.
page, = parse('''
<table style="border-spacing: 0">
<tr style="height: 10px">
<td rowspan=5></td>
<td></td>
</tr>
<tr style="height: 10px">
<td></td>
</tr>
</table>
''')
html, = page.children
body, = html.children
wrapper, = body.children
table, = wrapper.children
row_group, = table.children
@assert_no_logs
def test_table_wrapper():
page, = parse('''
<style>
@page { size: 1000px }
table { width: 600px; height: 500px; table-layout: fixed;
padding: 1px; border: 10px solid; margin: 100px; }
</style>
<table></table>
''')
html, = page.children
body, = html.children
wrapper, = body.children
table, = wrapper.children
assert body.width == 1000
assert wrapper.width == 600 # Not counting borders or padding
assert wrapper.margin_left == 100
assert table.margin_width() == 600
assert table.width == 578 # 600 - 2*10 - 2*1, no margin
# box-sizing in the UA stylesheet makes `height: 500px` set this
assert table.border_height() == 500
assert table.height == 478 # 500 - 2*10 - 2*1
assert table.margin_height() == 500 # no margin
assert wrapper.height == 500
assert wrapper.margin_height() == 700 # 500 + 2*100
# Non-regression test: this used to cause an exception
page, = parse('<html style="display: table">')
@assert_no_logs
def test_margin_boxes_fixed_dimension():
# Corner boxes
page, = parse('''
<style>
@page {
@top-left-corner {
content: 'top_left';
padding: 10px;
}
@top-right-corner {
content: 'top_right';
padding: 10px;
}
@bottom-left-corner {
content: 'bottom_left';
padding: 10px;
}
@bottom-right-corner {
content: 'bottom_right';
padding: 10px;
}
size: 1000px;
margin-top: 10%;
margin-bottom: 40%;
margin-left: 20%;
margin-right: 30%;
}
</style>
''')
html, top_left, top_right, bottom_left, bottom_right = page.children
for margin_box, text in zip(
[top_left, top_right, bottom_left, bottom_right],
['top_left', 'top_right', 'bottom_left', 'bottom_right']):
line, = margin_box.children
text, = line.children
assert text == text
# Check positioning and Rule 1 for fixed dimensions
assert top_left.position_x == 0
assert top_left.position_y == 0
assert top_left.margin_width() == 200 # margin-left
assert top_left.margin_height() == 100 # margin-top
assert top_right.position_x == 700 # size-x - margin-right
assert top_right.position_y == 0
assert top_right.margin_width() == 300 # margin-right
assert top_right.margin_height() == 100 # margin-top
assert bottom_left.position_x == 0
assert bottom_left.position_y == 600 # size-y - margin-bottom
assert bottom_left.margin_width() == 200 # margin-left
assert bottom_left.margin_height() == 400 # margin-bottom
assert bottom_right.position_x == 700 # size-x - margin-right
assert bottom_right.position_y == 600 # size-y - margin-bottom
assert bottom_right.margin_width() == 300 # margin-right
assert bottom_right.margin_height() == 400 # margin-bottom
# Test rules 2 and 3
page, = parse('''
<style>
@page {
margin: 100px 200px;
@bottom-left-corner {
content: "";
margin: 60px
}
}
</style>
''')
html, margin_box = page.children
assert margin_box.margin_width() == 200
assert margin_box.margin_left == 60
assert margin_box.margin_right == 60
assert margin_box.width == 80 # 200 - 60 - 60
assert margin_box.margin_height() == 100
# total was too big, the outside margin was ignored:
assert margin_box.margin_top == 60
assert margin_box.margin_bottom == 40 # Not 60
assert margin_box.height == 0 # But not negative
# Test rule 3 with a non-auto inner dimension
page, = parse('''
<style>
@page {
margin: 100px;
@left-middle {
content: "";
margin: 10px;
width: 130px;
}
}
</style>
''')
html, margin_box = page.children
assert margin_box.margin_width() == 100
assert margin_box.margin_left == -40 # Not 10px
assert margin_box.margin_right == 10
assert margin_box.width == 130 # As specified
# Test rule 4
page, = parse('''
<style>
@page {
margin: 100px;
@left-bottom {
content: "";
margin-left: 10px;
margin-right: auto;
width: 70px;
}
}
</style>
''')
html, margin_box = page.children
assert margin_box.margin_width() == 100
assert margin_box.margin_left == 10 # 10px this time, no over-constrain
assert margin_box.margin_right == 20
assert margin_box.width == 70 # As specified
# Test rules 2, 3 and 4
page, = parse('''
<style>
@page {
margin: 100px;
@right-top {
content: "";
margin-right: 10px;
margin-left: auto;
width: 130px;
}
}
</style>
''')
html, margin_box = page.children
assert margin_box.margin_width() == 100
assert margin_box.margin_left == 0 # rule 2
assert margin_box.margin_right == -30 # rule 3, after rule 2
assert margin_box.width == 130 # As specified
# Test rule 5
page, = parse('''
<style>
@page {
margin: 100px;
@top-left {
content: "";
margin-top: 10px;
margin-bottom: auto;
}
}
</style>
''')
html, margin_box = page.children
assert margin_box.margin_height() == 100
assert margin_box.margin_top == 10
assert margin_box.margin_bottom == 0
assert margin_box.height == 90
# Test rule 5
page, = parse('''
<style>
@page {
margin: 100px;
@top-center {
content: "";
margin: auto 0;
}
}
</style>
''')
html, margin_box = page.children
assert margin_box.margin_height() == 100
assert margin_box.margin_top == 0
assert margin_box.margin_bottom == 0
assert margin_box.height == 100
# Test rule 6
page, = parse('''
<style>
@page {
margin: 100px;
@bottom-right {
content: "";
margin: auto;
height: 70px;
}
}
</style>
''')
html, margin_box = page.children
assert margin_box.margin_height() == 100
assert margin_box.margin_top == 15
assert margin_box.margin_bottom == 15
assert margin_box.height == 70
# Rule 2 inhibits rule 6
page, = parse('''
<style>
@page {
margin: 100px;
@bottom-center {
content: "";
margin: auto 0;
height: 150px;
}
}
</style>
''')
html, margin_box = page.children
assert margin_box.margin_height() == 100
assert margin_box.margin_top == 0
assert margin_box.margin_bottom == -50 # outside
assert margin_box.height == 150
@assert_no_logs
def test_preferred_widths():
"""Unit tests for preferred widths."""
def get_float_width(body_width):
page, = parse('''
<body style="width: %spx">
<p style="white-space: pre-line; float: left">
Lorem ipsum dolor sit amet,
consectetur elit
</p>
<!-- ^ No-break space here -->
''' % body_width)
html, = page.children
body, = html.children
paragraph, = body.children
return paragraph.width
# Not exact, depends on the installed fonts
# Preferred minimum width:
assert 120 < get_float_width(10) < 140
# Preferred width:
assert 220 < get_float_width(10000) < 240
# Non-regression test:
# Incorrect whitespace handling in preferred width used to cause
# unnecessary line break.
page, = parse('''
<p style="float: left">Lorem <em>ipsum</em> dolor.</p>
''')
html, = page.children
body, = html.children
paragraph, = body.children
assert len(paragraph.children) == 1
assert isinstance(paragraph.children[0], boxes.LineBox)
@assert_no_logs
def test_margin_boxes_variable_dimension():
def get_widths(css):
"""Take some CSS to have inside @page
Return margin-widths of the sub-sequence of the three margin boxes
that are generated.
The containing blocks width is 600px. It starts at x = 100 and ends
at x = 700.
"""
expected_at_keywords = [
at_keyword for at_keyword in [
'@top-left', '@top-center', '@top-right']
if at_keyword + ' { content: ' in css]
page, = parse('''
<style>
@page {
size: 800px;
margin: 100px;
padding: 42px;
border: 7px solid;
%s
}
</style>
''' % css)
assert page.children[0].element_tag == 'html'
margin_boxes = page.children[1:]
assert [box.at_keyword for box in margin_boxes] == expected_at_keywords
offsets = {'@top-left': 0, '@top-center': 0.5, '@top-right': 1}
for box in margin_boxes:
assert box.position_x == 100 + offsets[box.at_keyword] * (
600 - box.margin_width())
return [box.margin_width() for box in margin_boxes]
def images(*widths):
return ' '.join(
'url(\'data:image/svg+xml,<svg width="%i" height="10"></svg>\')'
% width for width in widths)
# Use preferred widths if they fit
css = '''
@top-left { content: %s }
@top-center { content: %s }
@top-right { content: %s }
''' % (images(50, 50), images(50, 50), images(50, 50))
assert get_widths(css) == [100, 100, 100]
# 'auto' margins are set to 0
css = '''
@top-left { content: %s; margin: auto }
@top-center { content: %s }
@top-right { content: %s }
''' % (images(50, 50), images(50, 50), images(50, 50))
assert get_widths(css) == [100, 100, 100]
# Use at least minimum widths, even if boxes overlap
css = '''
@top-left { content: %s }
@top-center { content: %s }
@top-right { content: 'foo'; width: 200px }
''' % (images(100, 50), images(300, 150))
# @top-center is 300px wide and centered: this leaves 150 on either side
# There is 50px of overlap with @top-right
assert get_widths(css) == [150, 300, 200]
# In the intermediate case, distribute the remaining space proportionally
css = '''
@top-left { content: %s }
@top-center { content: %s }
@top-right { content: %s }
''' % (images(150, 150), images(150, 150), images(150, 150))
assert get_widths(css) == [200, 200, 200]
css = '''
@top-left { content: %s }
@top-center { content: %s }
@top-right { content: %s }
''' % (images(100, 100, 100), images(100, 100), images(10))
assert get_widths(css) == [220, 160, 10]
css = '''
@top-left { content: %s; width: 205px }
@top-center { content: %s }
@top-right { content: %s }
''' % (images(100, 100, 100), images(100, 100), images(10))
assert get_widths(css) == [205, 190, 10]
# 'width' and other properties have no effect without 'content'
css = '''
@top-left { width: 1000px; margin: 1000px; padding: 1000px;
border: 1000px solid }
@top-center { content: %s }
@top-right { content: %s }
''' % (images(100, 100), images(10))
assert get_widths(css) == [200, 10]
# This leaves 150px for @top-rights shrink-to-fit
css = '''
@top-left { content: ''; width: 200px }
@top-center { content: ''; width: 300px }
@top-right { content: %s }
''' % images(50, 50)
assert get_widths(css) == [200, 300, 100]
css = '''
@top-left { content: ''; width: 200px }
@top-center { content: ''; width: 300px }
@top-right { content: %s }
''' % images(100, 100, 100)
assert get_widths(css) == [200, 300, 150]
css = '''
@top-left { content: ''; width: 200px }
@top-center { content: ''; width: 300px }
@top-right { content: %s }
''' % images(170, 175)
assert get_widths(css) == [200, 300, 175]
css = '''
@top-left { content: ''; width: 200px }
@top-center { content: ''; width: 300px }
@top-right { content: %s }
''' % images(170, 175)
assert get_widths(css) == [200, 300, 175]
##### Without @top-center
css = '''
@top-left { content: ''; width: 200px }
@top-right { content: ''; width: 500px }
'''
assert get_widths(css) == [200, 500]
css = '''
@top-left { content: ''; width: 200px }
@top-right { content: %s }
''' % images(150, 50, 150)
assert get_widths(css) == [200, 350]
css = '''
@top-left { content: ''; width: 200px }
@top-right { content: %s }
''' % images(150, 50, 150, 200)
assert get_widths(css) == [200, 400]
css = '''
@top-left { content: %s }
@top-right { content: ''; width: 200px }
''' % images(150, 50, 450)
assert get_widths(css) == [450, 200]
css = '''
@top-left { content: %s }
@top-right { content: %s }
''' % (images(150, 100), images(10, 120))
assert get_widths(css) == [250, 130]
css = '''
@top-left { content: %s }
@top-right { content: %s }
''' % (images(550, 100), images(10, 120))
assert get_widths(css) == [550, 120]
css = '''
@top-left { content: %s }
@top-right { content: %s }
''' % (images(250, 60), images(250, 180))
# 250 + (100 * 1 / 4), 250 + (100 * 3 / 4)
assert get_widths(css) == [275, 325]
@assert_no_logs
def test_margin_boxes_vertical_align():
"""
3 px -> +-----+
| 1 |
+-----+
43 px -> +-----+
53 px -> | 2 |
+-----+
83 px -> +-----+
| 3 |
103px -> +-----+
"""
page, = parse('''
<style>
@page {
size: 800px;
margin: 106px; /* margin boxes content height is 100px */
@top-left {
content: "foo"; line-height: 20px; border: 3px solid;
vertical-align: top;
}
@top-center {
content: "foo"; line-height: 20px; border: 3px solid;
vertical-align: middle;
}
@top-right {
content: "foo"; line-height: 20px; border: 3px solid;
vertical-align: bottom;
}
}
</style>
''')
html, top_left, top_center, top_right = page.children
line_1, = top_left.children
line_2, = top_center.children
line_3, = top_right.children
assert line_1.position_y == 3
assert line_2.position_y == 43
assert line_3.position_y == 83
@assert_no_logs
def test_margin_collapsing():
"""
The vertical space between to sibling blocks is the max of their margins,
not the sum. But thats only the simplest case...
"""
def assert_collapsing(vertical_space):
assert vertical_space('10px', '15px') == 15 # not 25
# "The maximum of the absolute values of the negative adjoining margins
# is deducted from the maximum of the positive adjoining margins"
assert vertical_space('-10px', '15px') == 5
assert vertical_space('10px', '-15px') == -5
assert vertical_space('-10px', '-15px') == -15
assert vertical_space('10px', 'auto') == 10 # 'auto' is 0
return vertical_space
def assert_NOT_collapsing(vertical_space):
assert vertical_space('10px', '15px') == 25
assert vertical_space('-10px', '15px') == 5
assert vertical_space('10px', '-15px') == -5
assert vertical_space('-10px', '-15px') == -25
assert vertical_space('10px', 'auto') == 10 # 'auto' is 0
return vertical_space
# Siblings
@assert_collapsing
def vertical_space_1(p1_margin_bottom, p2_margin_top):
page, = parse('''
<style>
p { font: 20px/1 serif } /* block height == 20px */
#p1 { margin-bottom: %s }
#p2 { margin-top: %s }
</style>
<p id=p1>Lorem ipsum
<p id=p2>dolor sit amet
''' % (p1_margin_bottom, p2_margin_top))
html, = page.children
body, = html.children
p1, p2 = body.children
p1_bottom = p1.content_box_y() + p1.height
p2_top = p2.content_box_y()
return p2_top - p1_bottom
# Not siblings, first is nested
@assert_collapsing
def vertical_space_2(p1_margin_bottom, p2_margin_top):
page, = parse('''
<style>
p { font: 20px/1 serif } /* block height == 20px */
#p1 { margin-bottom: %s }
#p2 { margin-top: %s }
</style>
<div>
<p id=p1>Lorem ipsum
</div>
<p id=p2>dolor sit amet
''' % (p1_margin_bottom, p2_margin_top))
html, = page.children
body, = html.children
div, p2 = body.children
p1, = div.children
p1_bottom = p1.content_box_y() + p1.height
p2_top = p2.content_box_y()
return p2_top - p1_bottom
# Not siblings, second is nested
@assert_collapsing
def vertical_space_3(p1_margin_bottom, p2_margin_top):
page, = parse('''
<style>
p { font: 20px/1 serif } /* block height == 20px */
#p1 { margin-bottom: %s }
#p2 { margin-top: %s }
</style>
<p id=p1>Lorem ipsum
<div>
<p id=p2>dolor sit amet
</div>
''' % (p1_margin_bottom, p2_margin_top))
html, = page.children
body, = html.children
p1, div = body.children
p2, = div.children
p1_bottom = p1.content_box_y() + p1.height
p2_top = p2.content_box_y()
return p2_top - p1_bottom
# Not siblings, second is doubly nested
@assert_collapsing
def vertical_space_4(p1_margin_bottom, p2_margin_top):
page, = parse('''
<style>
p { font: 20px/1 serif } /* block height == 20px */
#p1 { margin-bottom: %s }
#p2 { margin-top: %s }
</style>
<p id=p1>Lorem ipsum
<div>
<div>
<p id=p2>dolor sit amet
</div>
</div>
''' % (p1_margin_bottom, p2_margin_top))
html, = page.children
body, = html.children
p1, div1 = body.children
div2, = div1.children
p2, = div2.children
p1_bottom = p1.content_box_y() + p1.height
p2_top = p2.content_box_y()
return p2_top - p1_bottom
# Collapsing with children
@assert_collapsing
def vertical_space_5(margin_1, margin_2):
page, = parse('''
<style>
p { font: 20px/1 serif } /* block height == 20px */
#div1 { margin-top: %s }
#div2 { margin-top: %s }
</style>
<p>Lorem ipsum
<div id=div1>
<div id=div2>
<p id=p2>dolor sit amet
</div>
</div>
''' % (margin_1, margin_2))
html, = page.children
body, = html.children
p1, div1 = body.children
div2, = div1.children
p2, = div2.children
p1_bottom = p1.content_box_y() + p1.height
p2_top = p2.content_box_y()
# Parent and element edge are the same:
assert div1.border_box_y() == p2.border_box_y()
assert div2.border_box_y() == p2.border_box_y()
return p2_top - p1_bottom
# Block formatting context: Not collapsing with children
@assert_NOT_collapsing
def vertical_space_6(margin_1, margin_2):
page, = parse('''
<style>
p { font: 20px/1 serif } /* block height == 20px */
#div1 { margin-top: %s; overflow: hidden }
#div2 { margin-top: %s }
</style>
<p>Lorem ipsum
<div id=div1>
<div id=div2>
<p id=p2>dolor sit amet
</div>
</div>
''' % (margin_1, margin_2))
html, = page.children
body, = html.children
p1, div1 = body.children
div2, = div1.children
p2, = div2.children
p1_bottom = p1.content_box_y() + p1.height
p2_top = p2.content_box_y()
return p2_top - p1_bottom
# Collapsing through an empty div
@assert_collapsing
def vertical_space_7(p1_margin_bottom, p2_margin_top):
page, = parse('''
<style>
p { font: 20px/1 serif } /* block height == 20px */
#p1 { margin-bottom: %s }
#p2 { margin-top: %s }
div { margin-bottom: %s; margin-top: %s }
</style>
<p id=p1>Lorem ipsum
<div></div>
<p id=p2>dolor sit amet
''' % (2 * (p1_margin_bottom, p2_margin_top)))
html, = page.children
body, = html.children
p1, div, p2 = body.children
p1_bottom = p1.content_box_y() + p1.height
p2_top = p2.content_box_y()
return p2_top - p1_bottom
# The root element does not collapse
@assert_NOT_collapsing
def vertical_space_8(margin_1, margin_2):
page, = parse('''
<html>
<style>
html { margin-top: %s }
body { margin-top: %s }
</style>
<body>
<p>Lorem ipsum
''' % (margin_1, margin_2))
html, = page.children
body, = html.children
p1, = body.children
p1_top = p1.content_box_y()
# Vertical space from y=0
return p1_top
# <body> DOES collapse
@assert_collapsing
def vertical_space_9(margin_1, margin_2):
page, = parse('''
<html>
<style>
body { margin-top: %s }
div { margin-top: %s }
</style>
<body>
<div>
<p>Lorem ipsum
''' % (margin_1, margin_2))
html, = page.children
body, = html.children
div, = body.children
p1, = div.children
p1_top = p1.content_box_y()
# Vertical space from y=0
return p1_top
@assert_no_logs
def test_relative_positioning():
page, = parse('''
<style>
p { height: 20px }
</style>
<p>1</p>
<div style="position: relative; top: 10px">
<p>2</p>
<p style="position: relative; top: -5px; left: 5px">3</p>
<p>4</p>
<p style="position: relative; bottom: 5px; right: 5px">5</p>
<p style="position: relative">6</p>
<p>7</p>
</div>
<p>8</p>
''')
html, = page.children
body, = html.children
p1, div, p8 = body.children
p2, p3, p4, p5, p6, p7 = div.children
assert (p1.position_x, p1.position_y) == (0, 0)
assert (div.position_x, div.position_y) == (0, 30)
assert (p2.position_x, p2.position_y) == (0, 30)
assert (p3.position_x, p3.position_y) == (5, 45) # (0 + 5, 50 - 5)
assert (p4.position_x, p4.position_y) == (0, 70)
assert (p5.position_x, p5.position_y) == (-5, 85) # (0 - 5, 90 - 5)
assert (p6.position_x, p6.position_y) == (0, 110)
assert (p7.position_x, p7.position_y) == (0, 130)
assert (p8.position_x, p8.position_y) == (0, 140)
assert div.height == 120
page, = parse('''
<style>
img { width: 20px }
body { font-size: 0 } /* Remove spaces */
</style>
<body>
<span><img src=pattern.png></span>
<span style="position: relative; left: 10px">
<img src=pattern.png>
<img src=pattern.png
style="position: relative; left: -5px; top: 5px">
<img src=pattern.png>
<img src=pattern.png
style="position: relative; right: 5px; bottom: 5px">
<img src=pattern.png style="position: relative">
<img src=pattern.png>
</span>
<span><img src=pattern.png></span>
''')
html, = page.children
body, = html.children
line, = body.children
span1, span2, span3 = line.children
img1, = span1.children
img2, img3, img4, img5, img6, img7 = span2.children
img8, = span3.children
assert (img1.position_x, img1.position_y) == (0, 0)
# Don't test the span2.position_y because it depends on fonts
assert span2.position_x == 30
assert (img2.position_x, img2.position_y) == (30, 0)
assert (img3.position_x, img3.position_y) == (45, 5) # (50 - 5, y + 5)
assert (img4.position_x, img4.position_y) == (70, 0)
assert (img5.position_x, img5.position_y) == (85, -5) # (90 - 5, y - 5)
assert (img6.position_x, img6.position_y) == (110, 0)
assert (img7.position_x, img7.position_y) == (130, 0)
assert (img8.position_x, img8.position_y) == (140, 0)
assert span2.width == 120
@assert_no_logs
def test_absolute_positioning():
page, = parse('''
<div style="margin: 3px">
<div style="height: 20px; width: 20px; position: absolute"></div>
<div style="height: 20px; width: 20px; position: absolute;
left: 0"></div>
<div style="height: 20px; width: 20px; position: absolute;
top: 0"></div>
</div>
''')
html, = page.children
body, = html.children
div1, = body.children
div2, div3, div4 = div1.children
assert div1.height == 0
assert (div1.position_x, div1.position_y) == (0, 0)
assert (div2.width, div2.height) == (20, 20)
assert (div2.position_x, div2.position_y) == (3, 3)
assert (div3.width, div3.height) == (20, 20)
assert (div3.position_x, div3.position_y) == (0, 3)
assert (div4.width, div4.height) == (20, 20)
assert (div4.position_x, div4.position_y) == (3, 0)
page, = parse('''
<div style="position: relative; width: 20px">
<div style="height: 20px; width: 20px; position: absolute"></div>
<div style="height: 20px; width: 20px"></div>
</div>
''')
html, = page.children
body, = html.children
div1, = body.children
div2, div3 = div1.children
for div in (div1, div2, div3):
assert (div.position_x, div.position_y) == (0, 0)
assert (div.width, div.height) == (20, 20)
page, = parse('''
<body style="font-size: 0">
<img src=pattern.png>
<span style="position: relative">
<span style="position: absolute">2</span>
<span style="position: absolute">3</span>
<span>4</span>
</span>
''')
html, = page.children
body, = html.children
line, = body.children
img, span1 = line.children
span2, span3, span4 = span1.children
assert span1.position_x == 4
assert (span2.position_x, span2.position_y) == (4, 0)
assert (span3.position_x, span3.position_y) == (4, 0)
assert span4.position_x == 4
page, = parse('''
<style> img { width: 5px; height: 20px} </style>
<body style="font-size: 0">
<img src=pattern.png>
<span style="position: absolute">2</span>
<img src=pattern.png>
''')
html, = page.children
body, = html.children
line, = body.children
img1, span, img2 = line.children
assert (img1.position_x, img1.position_y) == (0, 0)
assert (span.position_x, span.position_y) == (5, 0)
assert (img2.position_x, img2.position_y) == (5, 0)
page, = parse('''
<style> img { width: 5px; height: 20px} </style>
<body style="font-size: 0">
<img src=pattern.png>
<span style="position: absolute; display: block">2</span>
<img src=pattern.png>
''')
html, = page.children
body, = html.children
line, = body.children
img1, span, img2 = line.children
assert (img1.position_x, img1.position_y) == (0, 0)
assert (span.position_x, span.position_y) == (0, 20)
assert (img2.position_x, img2.position_y) == (5, 0)
page, = parse('''
<div style="position: relative; width: 20px; height: 60px;
border: 10px solid; padding-top: 6px; top: 5px; left: 1px">
<div style="height: 20px; width: 20px; position: absolute;
bottom: 50%"></div>
<div style="height: 20px; width: 20px; position: absolute;
top: 13px"></div>
</div>
''')
html, = page.children
body, = html.children
div1, = body.children
div2, div3 = div1.children
assert (div1.position_x, div1.position_y) == (1, 5)
assert (div1.width, div1.height) == (20, 60)
assert (div1.border_width(), div1.border_height()) == (40, 86)
assert (div2.position_x, div2.position_y) == (11, 28)
assert (div2.width, div2.height) == (20, 20)
assert (div3.position_x, div3.position_y) == (11, 28)
assert (div3.width, div3.height) == (20, 20)
page, = parse('''
<style>
@page { size: 1000px 2000px }
html { font-size: 0 }
p { height: 20px }
</style>
<p>1</p>
<div style="width: 100px">
<p>2</p>
<p style="position: absolute; top: -5px; left: 5px">3</p>
<p style="margin: 3px">4</p>
<p style="position: absolute; bottom: 5px; right: 15px;
width: 50px; height: 10%;
padding: 3px; margin: 7px">5
<span>
<img src="pattern.png">
<span style="position: absolute"></span>
<span style="position: absolute; top: -10px; right: 5px;
width: 20px; height: 15px"></span>
</span>
</p>
<p style="margin-top: 8px">6</p>
</div>
<p>7</p>
''')
html, = page.children
body, = html.children
p1, div, p7 = body.children
p2, p3, p4, p5, p6 = div.children
line, = p5.children
span1, = line.children
img, span2, span3 = span1.children
assert (p1.position_x, p1.position_y) == (0, 0)
assert (div.position_x, div.position_y) == (0, 20)
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 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
# = 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)
@assert_no_logs
def test_absolute_images():
page, = parse('''
<style>
img { display: block; position: absolute }
</style>
<div style="margin: 10px">
<img src=pattern.png />
<img src=pattern.png style="left: 15px" />
</div>
''')
html, = page.children
body, = html.children
div, = body.children
img1, img2 = div.children
assert div.height == 0
assert (div.position_x, div.position_y) == (0, 0)
assert (img1.position_x, img1.position_y) == (10, 10)
assert (img1.width, img1.height) == (4, 4)
assert (img2.position_x, img2.position_y) == (15, 10)
assert (img2.width, img2.height) == (4, 4)
# TODO: test the various cases in absolute_replaced()
@assert_no_logs
def test_floats():
# adjacent-floats-001
page, = parse('''
<style>
div { float: left }
img { width: 100px; vertical-align: top }
</style>
<div><img src=pattern.png /></div>
<div><img src=pattern.png /></div>''')
html, = page.children
body, = html.children
div_1, div_2 = body.children
assert outer_area(div_1) == (0, 0, 100, 100)
assert outer_area(div_2) == (100, 0, 100, 100)
# c414-flt-fit-000
page, = parse('''
<style>
body { width: 290px }
div { float: left; width: 100px; }
img { width: 60px; vertical-align: top }
</style>
<div><img src=pattern.png /><!-- 1 --></div>
<div><img src=pattern.png /><!-- 2 --></div>
<div><img src=pattern.png /><!-- 4 --></div>
<img src=pattern.png /><!-- 3
--><img src=pattern.png /><!-- 5 -->''')
html, = page.children
body, = html.children
div_1, div_2, div_4, anon_block = body.children
line_3, line_5 = anon_block.children
img_3, = line_3.children
img_5, = line_5.children
assert outer_area(div_1) == (0, 0, 100, 60)
assert outer_area(div_2) == (100, 0, 100, 60)
assert outer_area(img_3) == (200, 0, 60, 60)
assert outer_area(div_4) == (0, 60, 100, 60)
assert outer_area(img_5) == (100, 60, 60, 60)
# c414-flt-fit-002
page, = parse('''
<style type="text/css">
body { width: 200px }
p { width: 70px; height: 20px }
.left { float: left }
.right { float: right }
</style>
<p class="left"> ⇦ A 1 </p>
<p class="left"> ⇦ B 2 </p>
<p class="left"> ⇦ A 3 </p>
<p class="right"> B 4 ⇨ </p>
<p class="left"> ⇦ A 5 </p>
<p class="right"> B 6 ⇨ </p>
<p class="right"> B 8 ⇨ </p>
<p class="left"> ⇦ A 7 </p>
<p class="left"> ⇦ A 9 </p>
<p class="left"> ⇦ B 10 </p>
''')
html, = page.children
body, = html.children
positions = [(paragraph.position_x, paragraph.position_y)
for paragraph in body.children]
assert positions == [
(0, 0), (70, 0), (0, 20), (130, 20), (0, 40), (130, 40),
(130, 60), (0, 60), (0, 80), (70, 80), ]
# c414-flt-wrap-000 ... more or less
page, = parse('''
<style>
body { width: 100px }
p { float: left; height: 100px }
img { width: 60px; vertical-align: top }
</style>
<p style="width: 20px"></p>
<p style="width: 100%"></p>
<img src=pattern.png /><img src=pattern.png />
''')
html, = page.children
body, = html.children
p_1, p_2, anon_block = body.children
line_1, line_2 = anon_block.children
assert anon_block.position_y == 0
assert (line_1.position_x, line_1.position_y) == (20, 0)
assert (line_2.position_x, line_2.position_y) == (0, 200)
# floats-placement-vertical-001b
page, = parse('''
<style>
body { width: 90px; font-size: 0 }
img { vertical-align: top }
</style>
<body>
<span>
<img src=pattern.png style="width: 50px" />
<img src=pattern.png style="width: 50px" />
<img src=pattern.png style="float: left; width: 30px" />
</span>
''')
html, = page.children
body, = html.children
line_1, line_2 = body.children
span_1, = line_1.children
span_2, = line_2.children
img_1, = span_1.children
img_2, img_3 = span_2.children
assert outer_area(img_1) == (0, 0, 50, 50)
assert outer_area(img_2) == (30, 50, 50, 50)
assert outer_area(img_3) == (0, 50, 30, 30)
# Variant of the above: no <span>
page, = parse('''
<style>
body { width: 90px; font-size: 0 }
img { vertical-align: top }
</style>
<body>
<img src=pattern.png style="width: 50px" />
<img src=pattern.png style="width: 50px" />
<img src=pattern.png style="float: left; width: 30px" />
''')
html, = page.children
body, = html.children
line_1, line_2 = body.children
img_1, = line_1.children
img_2, img_3 = line_2.children
assert outer_area(img_1) == (0, 0, 50, 50)
assert outer_area(img_2) == (30, 50, 50, 50)
assert outer_area(img_3) == (0, 50, 30, 30)
# Floats do no affect other pages
page_1, page_2 = parse('''
<style>
body { width: 90px; font-size: 0 }
img { vertical-align: top }
</style>
<body>
<img src=pattern.png style="float: left; width: 30px" />
<img src=pattern.png style="width: 50px" />
<div style="page-break-before: always"></div>
<img src=pattern.png style="width: 50px" />
''')
html, = page_1.children
body, = html.children
float_img, anon_block, = body.children
line, = anon_block.children
img_1, = line.children
assert outer_area(float_img) == (0, 0, 30, 30)
assert outer_area(img_1) == (30, 0, 50, 50)
html, = page_2.children
body, = html.children
div, anon_block = body.children
line, = anon_block.children
img_2, = line.children
assert outer_area(img_2) == (0, 0, 50, 50)
@assert_no_logs
def test_font_stretch():
page, = parse('''
<style>p { float: left }</style>
<p>Hello, world!</p>
<p style="font-stretch: semi-condensed">Hello, world!</p>
<p style="font-stretch: semi-expanded">Hello, world!</p>
''')
html, = page.children
body, = html.children
p_1, p_2, p_3 = body.children
normal = p_1.width
condensed = p_2.width
assert condensed < normal
# TODO: when @font-face is supported use a font with an expanded variant.
# expanded = p_3.width
# assert normal < expanded
@assert_no_logs
def test_box_decoration_break():
# http://www.w3.org/TR/css3-background/#the-box-decoration-break
# Property not implemented yet, always "slice".
page_1, page_2 = parse('''
<style>
@page { size: 100px }
p { padding: 2px; border: 3px solid; margin: 5px }
img { height: 40px; vertical-align: top }
</style>
<p>
<img src=pattern.png><br>
<img src=pattern.png><br>
<img src=pattern.png><br>
<img src=pattern.png><br>''')
html, = page_1.children
body, = html.children
paragraph, = body.children
line_1, line_2 = paragraph.children
assert paragraph.position_y == 0
assert paragraph.margin_top == 5
assert paragraph.border_top_width == 3
assert paragraph.padding_top == 2
assert paragraph.content_box_y() == 10
assert line_1.position_y == 10
assert line_2.position_y == 50
assert paragraph.height == 80
assert paragraph.margin_bottom == 0
assert paragraph.border_bottom_width == 0
assert paragraph.padding_bottom == 0
assert paragraph.margin_height() == 90
html, = page_2.children
body, = html.children
paragraph, = body.children
line_1, line_2 = paragraph.children
assert paragraph.position_y == 0
assert paragraph.margin_top == 0
assert paragraph.border_top_width == 0
assert paragraph.padding_top == 0
assert paragraph.content_box_y() == 0
assert line_1.position_y == 0
assert line_2.position_y == 40
assert paragraph.height == 80
assert paragraph.padding_bottom == 2
assert paragraph.border_bottom_width == 3
assert paragraph.margin_bottom == 5
assert paragraph.margin_height() == 90
@assert_no_logs
def test_hyphenation():
def line_count(source):
page, = parse('<html style="width: 5em">' + source)
html, = page.children
body, = html.children
lines = body.children
return len(lines)
# Default: no hyphenation
assert line_count('<body>hyphenation') == 1
# lang only: no hyphenation
assert line_count(
'<body lang=en>hyphenation') == 1
# `hyphens: auto` only: no hyphenation
assert line_count(
'<body style="-weasy-hyphens: auto">hyphenation') == 1
# lang + `hyphens: auto`: hyphenation
assert line_count(
'<body style="-weasy-hyphens: auto" lang=en>hyphenation') > 1
# Hyphenation with soft hyphens
assert line_count('<body>hyp&shy;henation') == 2
# … unless disabled
assert line_count(
'<body style="-weasy-hyphens: none">hyp&shy;henation') == 1
@assert_no_logs
def test_hyphenate_character():
page, = parse(
'<html style="width: 5em">'
'<body style="-weasy-hyphens: auto;'
'-weasy-hyphenate-character: \'!\'" lang=en>'
'hyphenation')
html, = page.children
body, = html.children
lines = body.children
assert len(lines) > 1
assert lines[0].children[0].text.endswith('!')
full_text = ''.join(line.children[0].text for line in lines)
assert full_text.replace('!', '') == 'hyphenation'
page, = parse(
'<html style="width: 5em">'
'<body style="-weasy-hyphens: auto;'
'-weasy-hyphenate-character: \'é\'" lang=en>'
'hyphenation')
html, = page.children
body, = html.children
lines = body.children
assert len(lines) > 1
assert lines[0].children[0].text.endswith('é')
full_text = ''.join(line.children[0].text for line in lines)
assert full_text.replace('é', '') == 'hyphenation'
page, = parse(
'<html style="width: 5em">'
'<body style="-weasy-hyphens: auto;'
'-weasy-hyphenate-character: \'ù ù\'" lang=en>'
'hyphenation')
html, = page.children
body, = html.children
lines = body.children
assert len(lines) > 1
assert lines[0].children[0].text.endswith('ù ù')
full_text = ''.join(line.children[0].text for line in lines)
assert full_text.replace(' ', '').replace('ù', '') == 'hyphenation'
page, = parse(
'<html style="width: 5em">'
'<body style="-weasy-hyphens: auto;'
'-weasy-hyphenate-character: \'\'" lang=en>'
'hyphenation')
html, = page.children
body, = html.children
lines = body.children
assert len(lines) > 1
full_text = ''.join(line.children[0].text for line in lines)
assert full_text == 'hyphenation'
# TODO: strange error with some characters
# page, = parse(
# '<html style="width: 5em">'
# '<body style="-weasy-hyphens: auto;'
# '-weasy-hyphenate-character: \'———\'" lang=en>'
# 'hyphenation')
# html, = page.children
# body, = html.children
# lines = body.children
# assert len(lines) > 1
# assert lines[0].children[0].text.endswith('———')
# full_text = ''.join(line.children[0].text for line in lines)
# assert full_text.replace('—', '') == 'hyphenation'
@assert_no_logs
def test_hyphenate_limit_zone():
page, = parse(
'<html style="width: 10em">'
'<body style="-weasy-hyphens: auto;'
'-weasy-hyphenate-limit-zone: 0" lang=en>'
'llllllllll hyphenation')
html, = page.children
body, = html.children
lines = body.children
assert len(lines) == 2
assert lines[0].children[0].text.endswith('')
full_text = ''.join(line.children[0].text for line in lines)
assert full_text.replace('', '') == 'llllllllll hyphenation'
page, = parse(
'<html style="width: 10em">'
'<body style="-weasy-hyphens: auto;'
'-weasy-hyphenate-limit-zone: 9em" lang=en>'
'llllllllll hyphenation')
html, = page.children
body, = html.children
lines = body.children
assert len(lines) > 1
assert lines[0].children[0].text.endswith('ll')
full_text = ''.join(line.children[0].text for line in lines)
assert full_text == 'llllllllllhyphenation'
page, = parse(
'<html style="width: 10em">'
'<body style="-weasy-hyphens: auto;'
'-weasy-hyphenate-limit-zone: 5%" lang=en>'
'llllllllll hyphenation')
html, = page.children
body, = html.children
lines = body.children
assert len(lines) == 2
assert lines[0].children[0].text.endswith('')
full_text = ''.join(line.children[0].text for line in lines)
assert full_text.replace('', '') == 'llllllllll hyphenation'
page, = parse(
'<html style="width: 10em">'
'<body style="-weasy-hyphens: auto;'
'-weasy-hyphenate-limit-zone: 95%" lang=en>'
'llllllllll hyphenation')
html, = page.children
body, = html.children
lines = body.children
assert len(lines) > 1
assert lines[0].children[0].text.endswith('ll')
full_text = ''.join(line.children[0].text for line in lines)
assert full_text == 'llllllllllhyphenation'
@assert_no_logs
def test_hyphenate_limit_chars():
def line_count(limit_chars):
page, = parse((
'<html style="width: 1em">'
'<body style="-weasy-hyphens: auto;'
'-weasy-hyphenate-limit-chars: %s" lang=en>'
'hyphen') % limit_chars)
html, = page.children
body, = html.children
lines = body.children
return len(lines)
assert line_count('auto') == 2
assert line_count('auto auto 0') == 2
assert line_count('0 0 0') == 2
assert line_count('4 4 auto') == 1
assert line_count('6 2 4') == 2
assert line_count('auto 1 auto') == 2
assert line_count('7 auto auto') == 1
assert line_count('6 auto auto') == 2
assert line_count('5 2') == 2
assert line_count('3') == 2
assert line_count('2 4 6') == 1
assert line_count('auto 4') == 1
assert line_count('auto 2') == 2
@assert_no_logs
def test_white_space():
"""Test the white-space property."""
def lines(width, space):
page, = parse('''
<style>
body { font-size: 100px; width: %ipx }
span { white-space: %s }
</style>
<body><span>This \n is text''' % (width, space))
html, = page.children
body, = html.children
return body.children
line1, line2, line3 = lines(1, 'normal')
box1, = line1.children
text1, = box1.children
assert text1.text == 'This'
box2, = line2.children
text2, = box2.children
assert text2.text == 'is'
box3, = line3.children
text3, = box3.children
assert text3.text == 'text'
line1, line2 = lines(1, 'pre')
box1, = line1.children
text1, = box1.children
assert text1.text == 'This\xA0\xA0\xA0\xA0'
box2, = line2.children
text2, = box2.children
assert text2.text == '\xA0\xA0\xA0\xA0is\xA0text'
line1, = lines(1, 'nowrap')
box1, = line1.children
text1, = box1.children
assert text1.text == 'This\xA0is\xA0text'
line1, line2, line3, line4 = lines(1, 'pre-wrap')
box1, = line1.children
text1, = box1.children
assert text1.text == 'This\xA0\xA0\xA0\xA0\u200b'
box2, = line2.children
text2, = box2.children
assert text2.text == '\xA0\xA0\xA0\xA0\u200b'
box3, = line3.children
text3, = box3.children
assert text3.text == 'is\xA0\u200b'
box4, = line4.children
text4, = box4.children
assert text4.text == 'text'
line1, line2, line3 = lines(1, 'pre-line')
box1, = line1.children
text1, = box1.children
assert text1.text == 'This'
box2, = line2.children
text2, = box2.children
assert text2.text == 'is'
box3, = line3.children
text3, = box3.children
assert text3.text == 'text'
line1, = lines(1000000, 'normal')
box1, = line1.children
text1, = box1.children
assert text1.text == 'This is text'
line1, line2 = lines(1000000, 'pre')
box1, = line1.children
text1, = box1.children
assert text1.text == 'This\xA0\xA0\xA0\xA0'
box2, = line2.children
text2, = box2.children
assert text2.text == '\xA0\xA0\xA0\xA0is\xA0text'
line1, = lines(1000000, 'nowrap')
box1, = line1.children
text1, = box1.children
assert text1.text == 'This\xA0is\xA0text'
line1, line2 = lines(1000000, 'pre-wrap')
box1, = line1.children
text1, = box1.children
assert text1.text == 'This\xA0\xA0\xA0\xA0\u200b'
box2, = line2.children
text2, = box2.children
assert text2.text == '\xA0\xA0\xA0\xA0\u200bis\xA0\u200btext'
line1, line2 = lines(1000000, 'pre-line')
box1, = line1.children
text1, = box1.children
assert text1.text == 'This'
box2, = line2.children
text2, = box2.children
assert text2.text == 'is text'
@assert_no_logs
def test_linear_gradient():
red = (1, 0, 0, 1)
lime = (0, 1, 0, 1)
blue = (0, 0, 1, 1)
def layout(gradient_css, type_='linear', init=(),
positions=[0, 1], colors=[blue, lime], scale=(1, 1)):
page, = parse('<style>@page { background: ' + gradient_css)
layer, = page.background.layers
scale_x, scale_y = scale
result = layer.image.layout(
400, 300, lambda dx, dy: (dx * scale_x, dy * scale_y))
expected = 1, type_, init, positions, colors
assert almost_equal(result, expected), (result, expected)
layout('linear-gradient(blue)', 'solid', blue, [], [])
layout('repeating-linear-gradient(blue)', 'solid', blue, [], [])
layout('repeating-linear-gradient(blue, lime 1.5px)',
'solid', (0, .5, .5, 1), [], [])
layout('linear-gradient(blue, lime)', init=(200, 0, 200, 300))
layout('repeating-linear-gradient(blue, lime)', init=(200, 0, 200, 300))
layout('repeating-linear-gradient(blue, lime 20px)', init=(200, 0, 200, 20))
layout('repeating-linear-gradient(blue, lime 20px)',
'solid', (0, .5, .5, 1), [], [], scale=(1/20, 1/20))
layout('linear-gradient(to bottom, blue, lime)', init=(200, 0, 200, 300))
layout('linear-gradient(to top, blue, lime)', init=(200, 300, 200, 0))
layout('linear-gradient(to right, blue, lime)', init=(0, 150, 400, 150))
layout('linear-gradient(to left, blue, lime)', init=(400, 150, 0, 150))
layout('linear-gradient(to top left, blue, lime)',
init=(344, 342, 56, -42))
layout('linear-gradient(to top right, blue, lime)',
init=(56, 342, 344, -42))
layout('linear-gradient(to bottom left, blue, lime)',
init=(344, -42, 56, 342))
layout('linear-gradient(to bottom right, blue, lime)',
init=(56, -42, 344, 342))
layout('linear-gradient(270deg, blue, lime)', init=(400, 150, 0, 150))
layout('linear-gradient(.75turn, blue, lime)', init=(400, 150, 0, 150))
layout('linear-gradient(45deg, blue, lime)', init=(25, 325, 375, -25))
layout('linear-gradient(.125turn, blue, lime)', init=(25, 325, 375, -25))
layout('linear-gradient(.375turn, blue, lime)', init=(25, -25, 375, 325))
layout('linear-gradient(.625turn, blue, lime)', init=(375, -25, 25, 325))
layout('linear-gradient(.875turn, blue, lime)', init=(375, 325, 25, -25))
layout('linear-gradient(blue 2em, lime 20%)', init=(200, 32, 200, 60))
layout('linear-gradient(blue 100px, red, blue, red 160px, lime)',
init=(200, 100, 200, 300), colors=[blue, red, blue, red, lime],
positions=[0, .1, .2, .3, 1])
layout('linear-gradient(blue -100px, blue 0, red -12px, lime 50%)',
init=(200, -100, 200, 150), colors=[blue, blue, red, lime],
positions=[0, .4, .4, 1])
layout('linear-gradient(blue, blue, red, lime -7px)',
init=(200, 0, 200, 100), colors=[blue, blue, red, lime],
positions=[0, 0, 0, 0])
layout('repeating-linear-gradient(blue, blue, lime, lime -7px)',
'solid', (0, .5, .5, 1), [], [])
@assert_no_logs
def test_radial_gradient():
red = (1, 0, 0, 1)
lime = (0, 1, 0, 1)
blue = (0, 0, 1, 1)
def layout(gradient_css, type_='radial', init=(),
positions=[0, 1], colors=[blue, lime], scale_y=1,
ctm_scale=(1, 1)):
if type_ == 'radial':
center_x, center_y, radius0, radius1 = init
init = (center_x, center_y / scale_y, radius0,
center_x, center_y / scale_y, radius1)
page, = parse('<style>@page { background: ' + gradient_css)
layer, = page.background.layers
ctm_scale_x, ctm_scale_y = ctm_scale
result = layer.image.layout(
400, 300, lambda dx, dy: (dx * ctm_scale_x, dy * ctm_scale_y))
expected = scale_y, type_, init, positions, colors
assert almost_equal(result, expected), (result, expected)
layout('radial-gradient(blue)', 'solid', blue, [], [])
layout('repeating-radial-gradient(blue)', 'solid', blue, [], [])
layout('radial-gradient(100px, blue, lime)',
init=(200, 150, 0, 100))
layout('radial-gradient(100px at right 20px bottom 30px, blue, lime)',
init=(380, 270, 0, 100))
layout('radial-gradient(0 0, blue, lime)',
init=(200, 150, 0, 1e-7))
layout('radial-gradient(1px 0, blue, lime)',
init=(200, 150, 0, 1e7), scale_y=1e-14)
layout('radial-gradient(0 1px, blue, lime)',
init=(200, 150, 0, 1e-7), scale_y=1e14)
layout('repeating-radial-gradient(20px 40px, blue, lime)',
init=(200, 150, 0, 20), scale_y=40/20)
layout('repeating-radial-gradient(20px 40px, blue, lime)',
init=(200, 150, 0, 20), scale_y=40/20, ctm_scale=(1/9, 1))
layout('repeating-radial-gradient(20px 40px, blue, lime)',
init=(200, 150, 0, 20), scale_y=40/20, ctm_scale=(1, 1/19))
layout('repeating-radial-gradient(20px 40px, blue, lime)',
'solid', (0, .5, .5, 1), [], [], ctm_scale=(1/11, 1))
layout('repeating-radial-gradient(20px 40px, blue, lime)',
'solid', (0, .5, .5, 1), [], [], ctm_scale=(1, 1/21))
layout('repeating-radial-gradient(42px, blue -20px, lime 10px)',
init=(200, 150, 10, 40))
layout('repeating-radial-gradient(42px, blue -140px, lime -110px)',
init=(200, 150, 10, 40))
layout('radial-gradient(42px, blue -20px, lime -1px)',
'solid', lime, [], [])
layout('radial-gradient(42px, blue -20px, lime 0)',
'solid', lime, [], [])
layout('radial-gradient(42px, blue -20px, lime 20px)',
init=(200, 150, 0, 20), colors=[(0, .5, .5, 1), lime])
layout('radial-gradient(100px 120px, blue, lime)',
init=(200, 150, 0, 100), scale_y=120/100)
layout('radial-gradient(25% 40%, blue, lime)',
init=(200, 150, 0, 100), scale_y=120/100)
layout('radial-gradient(circle closest-side, blue, lime)',
init=(200, 150, 0, 150))
layout('radial-gradient(circle closest-side at 150px 50px, blue, lime)',
init=(150, 50, 0, 50))
layout('radial-gradient(circle closest-side at 45px 50px, blue, lime)',
init=(45, 50, 0, 45))
layout('radial-gradient(circle closest-side at 420px 50px, blue, lime)',
init=(420, 50, 0, 20))
layout('radial-gradient(circle closest-side at 420px 281px, blue, lime)',
init=(420, 281, 0, 19))
layout('radial-gradient(closest-side, blue 20%, lime)',
init=(200, 150, 40, 200), scale_y=150/200)
layout('radial-gradient(closest-side at 300px 20%, blue, lime)',
init=(300, 60, 0, 100), scale_y=60/100)
layout('radial-gradient(closest-side at 10% 230px, blue, lime)',
init=(40, 230, 0, 40), scale_y=70/40)
layout('radial-gradient(circle farthest-side, blue, lime)',
init=(200, 150, 0, 200))
layout('radial-gradient(circle farthest-side at 150px 50px, blue, lime)',
init=(150, 50, 0, 250))
layout('radial-gradient(circle farthest-side at 45px 50px, blue, lime)',
init=(45, 50, 0, 355))
layout('radial-gradient(circle farthest-side at 420px 50px, blue, lime)',
init=(420, 50, 0, 420))
layout('radial-gradient(circle farthest-side at 220px 310px, blue, lime)',
init=(220, 310, 0, 310))
layout('radial-gradient(farthest-side, blue, lime)',
init=(200, 150, 0, 200), scale_y=150/200)
layout('radial-gradient(farthest-side at 300px 20%, blue, lime)',
init=(300, 60, 0, 300), scale_y=240/300)
layout('radial-gradient(farthest-side at 10% 230px, blue, lime)',
init=(40, 230, 0, 360), scale_y=230/360)
layout('radial-gradient(circle closest-corner, blue, lime)',
init=(200, 150, 0, 250))
layout('radial-gradient(circle closest-corner at 340px 80px, blue, lime)',
init=(340, 80, 0, 100))
layout('radial-gradient(circle closest-corner at 0 342px, blue, lime)',
init=(0, 342, 0, 42))
sqrt2 = math.sqrt(2)
layout('radial-gradient(closest-corner, blue, lime)',
init=(200, 150, 0, 200 * sqrt2), scale_y=150/200)
layout('radial-gradient(closest-corner at 450px 100px, blue, lime)',
init=(450, 100, 0, 50 * sqrt2), scale_y=100/50)
layout('radial-gradient(closest-corner at 40px 210px, blue, lime)',
init=(40, 210, 0, 40 * sqrt2), scale_y=90/40)
layout('radial-gradient(circle farthest-corner, blue, lime)',
init=(200, 150, 0, 250))
layout('radial-gradient(circle farthest-corner'
' at 300px -100px, blue, lime)',
init=(300, -100, 0, 500))
layout('radial-gradient(circle farthest-corner at 400px 0, blue, lime)',
init=(400, 0, 0, 500))
layout('radial-gradient(farthest-corner, blue, lime)',
init=(200, 150, 0, 200 * sqrt2), scale_y=150/200)
layout('radial-gradient(farthest-corner at 450px 100px, blue, lime)',
init=(450, 100, 0, 450 * sqrt2), scale_y=200/450)
layout('radial-gradient(farthest-corner at 40px 210px, blue, lime)',
init=(40, 210, 0, 360 * sqrt2), scale_y=210/360)