debian-weasyprint/weasyprint/tests/test_layout.py

3462 lines
114 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

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: utf-8
"""
weasyprint.tests.layout
-----------------------
Tests for layout, ie. positioning and dimensioning of boxes,
line breaks, page breaks.
:copyright: Copyright 2011-2014 Simon Sapin and contributors, see AUTHORS.
:license: BSD, see LICENSE for details.
"""
from __future__ import division, unicode_literals
import math
import pytest
from ..formatting_structure import boxes
from .test_boxes import render_pages as parse
from .testing_utils import (
FONTS, almost_equal, assert_no_logs, capture_logs, requires)
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
@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_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 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
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']
html_sample = '''
<p style="width: %i.5em; font-family: ahem">ab
<span style="padding-right: 1em; margin-right: 1em">c def</span>g
hi</p>'''
for i in range(15):
page, = parse(html_sample % i)
html, = page.children
body, = html.children
p, = body.children
lines = p.children
if i in (0, 1, 2, 3):
line_1, line_2, line_3, line_4 = lines
textbox_1, = line_1.children
assert textbox_1.text == 'ab'
span_1, = line_2.children
textbox_1, = span_1.children
assert textbox_1.text == 'c'
span_1, textbox_2 = line_3.children
textbox_1, = span_1.children
assert textbox_1.text == 'def'
assert textbox_2.text == 'g'
textbox_1, = line_4.children
assert textbox_1.text == 'hi'
elif i in (4, 5, 6, 7, 8):
line_1, line_2, line_3 = lines
textbox_1, span_1 = line_1.children
assert textbox_1.text == 'ab '
textbox_2, = span_1.children
assert textbox_2.text == 'c'
span_1, textbox_2 = line_2.children
textbox_1, = span_1.children
assert textbox_1.text == 'def'
assert textbox_2.text == 'g'
textbox_1, = line_3.children
assert textbox_1.text == 'hi'
elif i in (9, 10):
line_1, line_2 = lines
textbox_1, span_1 = line_1.children
assert textbox_1.text == 'ab '
textbox_2, = span_1.children
assert textbox_2.text == 'c'
span_1, textbox_2 = line_2.children
textbox_1, = span_1.children
assert textbox_1.text == 'def'
assert textbox_2.text == 'g hi'
elif i in (11, 12, 13):
line_1, line_2 = lines
textbox_1, span_1, textbox_3 = line_1.children
assert textbox_1.text == 'ab '
textbox_2, = span_1.children
assert textbox_2.text == 'c def'
assert textbox_3.text == 'g'
textbox_1, = line_2.children
assert textbox_1.text == 'hi'
elif i in (14, 15):
line_1, = lines
textbox_1, span_1, textbox_3 = line_1.children
assert textbox_1.text == 'ab '
textbox_2, = span_1.children
assert textbox_2.text == 'c def'
assert textbox_3.text == 'g hi'
# Regression test #1 for https://github.com/Kozea/WeasyPrint/issues/560
page, = parse(
'<div style="width: 5.5em; font-family: ahem">'
'aaaa aaaa a [<span>aaa</span>]')
html, = page.children
body, = html.children
div, = body.children
line1, line2, line3, line4 = div.children
assert line1.children[0].text == line2.children[0].text == 'aaaa'
assert line3.children[0].text == 'a'
text1, span, text2 = line4.children
assert text1.text == '['
assert text2.text == ']'
assert span.children[0].text == 'aaa'
# Regression test #2 for https://github.com/Kozea/WeasyPrint/issues/560
page, = parse(
'<div style="width: 5.5em; font-family: ahem">'
'aaaa a <span>b c</span>d')
html, = page.children
body, = html.children
div, = body.children
line1, line2, line3 = div.children
assert line1.children[0].text == 'aaaa'
assert line2.children[0].text == 'a '
assert line2.children[1].children[0].text == 'b'
assert line3.children[0].children[0].text == 'c'
assert line3.children[1].text == 'd'
# Regression test for https://github.com/Kozea/WeasyPrint/issues/580
page, = parse(
'<div style="width: 5.5em; font-family: ahem">'
'<span>aaaa aaaa a a a</span><span>bc</span>')
html, = page.children
body, = html.children
div, = body.children
line1, line2, line3, line4 = div.children
assert line1.children[0].children[0].text == 'aaaa'
assert line2.children[0].children[0].text == 'aaaa'
assert line3.children[0].children[0].text == 'a a'
assert line4.children[0].children[0].text == 'a'
assert line4.children[1].children[0].text == 'bc'
# Regression test for https://github.com/Kozea/WeasyPrint/issues/586
page, = parse(
'<div style="width: 5.5em; font-family: ahem">'
'a a <span style="white-space: nowrap">/ccc</span>')
html, = page.children
body, = html.children
div, = body.children
line1, line2 = div.children
assert line1.children[0].text == 'a a'
assert line2.children[0].children[0].text == '/ccc'
@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)
del 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></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)
del 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)
del 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, _div = 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_page_names():
"""Test the page names."""
pages = parse('''
<style>
@page { size: 100px 100px }
section { page: small }
</style>
<div>
<section>large</section>
</div>
''')
page1, = pages
assert (page1.width, page1.height) == (100, 100)
pages = parse('''
<style>
@page { size: 100px 100px }
@page narrow { margin: 1px }
section { page: small }
</style>
<div>
<section>large</section>
</div>
''')
page1, = pages
assert (page1.width, page1.height) == (100, 100)
pages = parse('''
<style>
@page { margin: 0 }
@page narrow { size: 100px 200px }
@page large { size: 200px 100px }
div { page: narrow }
section { page: large }
</style>
<div>
<section>large</section>
<section>large</section>
<p>narrow</p>
</div>
''')
page1, page2 = pages
assert (page1.width, page1.height) == (200, 100)
html, = page1.children
body, = html.children
div, = body.children
section1, section2 = div.children
assert section1.element_tag == section2.element_tag == 'section'
assert (page2.width, page2.height) == (100, 200)
html, = page2.children
body, = html.children
div, = body.children
p, = div.children
assert p.element_tag == 'p'
pages = parse('''
<style>
@page { size: 200px 200px; margin: 0 }
@page small { size: 100px 100px }
p { page: small }
</style>
<section>normal</section>
<section>normal</section>
<p>small</p>
<section>small</section>
''')
page1, page2 = pages
assert (page1.width, page1.height) == (200, 200)
html, = page1.children
body, = html.children
section1, section2 = body.children
assert section1.element_tag == section2.element_tag == 'section'
assert (page2.width, page2.height) == (100, 100)
html, = page2.children
body, = html.children
p, section = body.children
assert p.element_tag == 'p'
assert section.element_tag == 'section'
pages = parse('''
<style>
@page { size: 200px 200px; margin: 0 }
@page small { size: 100px 100px }
div { page: small }
</style>
<section><p>a</p>b</section>
<section>c<div>d</div></section>
''')
page1, page2 = pages
assert (page1.width, page1.height) == (200, 200)
html, = page1.children
body, = html.children
section1, section2 = body.children
assert section1.element_tag == section2.element_tag == 'section'
p, line = section1.children
line, = section2.children
assert (page2.width, page2.height) == (100, 100)
html, = page2.children
body, = html.children
section2, = body.children
div, = section2.children
pages = parse('''
<style>
@page { margin: 0 }
@page large { size: 200px 200px }
@page small { size: 100px 100px }
section { page: large }
div { page: small }
</style>
<section>a<p>b</p>c</section>
<section>d<div>e</div>f</section>
''')
page1, page2, page3 = pages
assert (page1.width, page1.height) == (200, 200)
html, = page1.children
body, = html.children
section1, section2 = body.children
assert section1.element_tag == section2.element_tag == 'section'
line1, p, line2 = section1.children
line, = section2.children
assert (page2.width, page2.height) == (100, 100)
html, = page2.children
body, = html.children
section2, = body.children
div, = section2.children
assert (page3.width, page3.height) == (200, 200)
html, = page3.children
body, = html.children
section2, = body.children
line, = section2.children
pages = parse('''
<style>
@page { size: 200px 200px; margin: 0 }
@page small { size: 100px 100px }
p { page: small; break-before: right }
</style>
<section>normal</section>
<section>normal</section>
<p>small</p>
<section>small</section>
''')
page1, page2, page3 = pages
assert (page1.width, page1.height) == (200, 200)
html, = page1.children
body, = html.children
section1, section2 = body.children
assert section1.element_tag == section2.element_tag == 'section'
assert (page2.width, page2.height) == (200, 200)
html, = page2.children
assert not html.children
assert (page3.width, page3.height) == (100, 100)
html, = page3.children
body, = html.children
p, section = body.children
assert p.element_tag == 'p'
assert section.element_tag == 'section'
pages = parse('''
<style>
@page small { size: 100px 100px }
section { page: small }
p { line-height: 80px }
</style>
<section>
<p>small</p>
<p>small</p>
</section>
''')
page1, page2 = pages
assert (page1.width, page1.height) == (100, 100)
html, = page1.children
body, = html.children
section, = body.children
p, = section.children
assert section.element_tag == 'section'
assert p.element_tag == 'p'
assert (page2.width, page2.height) == (100, 100)
html, = page2.children
body, = html.children
section, = body.children
p, = section.children
assert section.element_tag == 'section'
assert p.element_tag == 'p'
pages = parse('''
<style>
@page { size: 200px 200px }
@page small { size: 100px 100px }
section { break-after: page; page: small }
article { page: small }
</style>
<section>
<div>big</div>
<div>big</div>
</section>
<article>
<div>small</div>
<div>small</div>
</article>
''')
page1, page2, = pages
assert (page1.width, page1.height) == (100, 100)
html, = page1.children
body, = html.children
section, = body.children
assert section.element_tag == 'section'
assert (page2.width, page2.height) == (100, 100)
html, = page2.children
body, = html.children
article, = body.children
assert article.element_tag == 'article'
@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_inlinebox_splitting():
"""Test the inline boxes splitting."""
# The text is strange to test some corner cases
# See https://github.com/Kozea/WeasyPrint/issues/389
for width in [10000, 100, 10, 0]:
page, = parse('''
<style>p { font-family:%(fonts)s; width: %(width)spx; }</style>
<p><strong>WeasyPrint is a frée softwäre ./ visual rendèring enginè
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 frée softwäre ./ visual '
'rendèring enginè 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,)
# TODO: this test is broken with 1.15.10 and 1.14.12 because of commit b092b63
# now reverted on master and 1.14 branches. Remove the @requires decorator when
# 1.15.12 is released.
@assert_no_logs
@requires('cairo', '1.15.12')
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',
# Explicit SVG, no sniffing
'data:image/svg+xml;base64,'
'R0lGODlhAQABAIABAP///wAAACwAAAAAAQABAAACAkQBADs=',
'really-a-png.svg',
]:
with capture_logs() as logs:
body, img = get_img("<img src='%s' alt='invalid image'>" % url)
assert len(logs) == 1
assert 'ERROR: Failed to load image' in logs[0]
assert isinstance(img, boxes.InlineBox) # not a replaced box
text, = img.children
assert text.text == 'invalid image', url
# Format sniffing
for url in [
# GIF with JPEG mimetype
'data:image/jpeg;base64,'
'R0lGODlhAQABAIABAP///wAAACwAAAAAAQABAAACAkQBADs=',
# GIF with PNG mimetype
'data:image/png;base64,'
'R0lGODlhAQABAIABAP///wAAACwAAAAAAQABAAACAkQBADs=',
# PNG with JPEG mimetype
'data:image/jpeg;base64,'
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC'
'0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=',
# SVG with PNG mimetype
'data:image/png,<svg width="1" height="1"></svg>',
'really-a-svg.png',
]:
with capture_logs() as logs:
body, img = get_img("<img src='%s'>" % url)
assert len(logs) == 0
with capture_logs() as logs:
parse('<img src=nonexistent.png><img src=nonexistent.png>')
# Failures are cached too: only one error
assert len(logs) == 1
assert 'ERROR: Failed to load 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
page, = parse('''
<body style="float: left">
<img style="height: 200px; margin: 10px; display: block" src="
data:image/svg+xml,
<svg width='150' height='100'></svg>
">
''')
html, = page.children
body, = html.children
img, = body.children
assert body.width == 320
assert body.height == 220
assert img.element_tag == 'img'
assert img.width == 300
assert img.height == 200
@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_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_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_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_fixed_positioning():
# TODO:test page-break-before: left/right
page_1, page_2, page_3 = parse('''
a
<div style="page-break-before: always; page-break-after: always">
<p style="position: fixed">b</p>
</div>
c
''')
html, = page_1.children
assert [c.element_tag for c in html.children] == ['body', 'p']
html, = page_2.children
body, = html.children
div, = body.children
assert [c.element_tag for c in div.children] == ['p']
html, = page_3.children
assert [c.element_tag for c in html.children] == ['p', 'body']
@assert_no_logs
def test_font_stretch():
page, = parse('''
<style>
p { float: left; font-family: DejaVu Sans }
</style>
<p>Hello, world!</p>
<p style="font-stretch: condensed">Hello, world!</p>
''')
html, = page.children
body, = html.children
p_1, p_2 = body.children
normal = p_1.width
condensed = p_2.width
assert condensed < normal
@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; font-family: ahem">' + source)
html, = page.children
body, = html.children
lines = body.children
return len(lines)
# Default: no hyphenation
assert line_count('<body>hyphénation') == 1
# lang only: no hyphenation
assert line_count(
'<body lang=fr>hyphénation') == 1
# `hyphens: auto` only: no hyphenation
assert line_count(
'<body style="hyphens: auto">hyphénation') == 1
# lang + `hyphens: auto`: hyphenation
assert line_count(
'<body style="hyphens: auto" lang=fr>hyphénation') > 1
# Hyphenation with soft hyphens
assert line_count('<body>hyp&shy;hénation') == 2
# … unless disabled
assert line_count(
'<body style="hyphens: none">hyp&shy;hénation') == 1
@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, lime, red)',
init=(380, 270, 0, 100), colors=[lime, red])
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))
@assert_no_logs
def test_shrink_to_fit_floating_point_error():
"""Test that no floating point error occurs during shrink to fit.
See bugs #325 and #288, see commit fac5ee9.
"""
for margin_left in range(1, 10):
for font_size in range(1, 10):
page, = parse('''
<style>
@page { size: 100000px 100px }
p { float: left; margin-left: 0.%iin; font-size: 0.%iem;
font-family: "ahem" }
</style>
<p>this parrot is dead</p>
''' % (margin_left, font_size))
html, = page.children
body, = html.children
p, = body.children
assert len(p.children) == 1
letters = 1
for font_size in (1, 5, 10, 50, 100, 1000, 10000):
while True:
page, = parse('''
<style>
@page { size: %i0pt %i0px }
p { font-size: %ipt; font-family: "ahem" }
</style>
<p>mmm <b>%s a</b></p>
''' % (font_size, font_size, font_size, 'i' * letters))
html, = page.children
body, = html.children
p, = body.children
assert len(p.children) in (1, 2)
assert len(p.children[0].children) == 2
text = p.children[0].children[1].children[0].text
assert text
if text.endswith('i'):
letters = 1
break
else:
letters += 1