1
1
mirror of https://github.com/Kozea/WeasyPrint.git synced 2024-10-05 08:27:22 +03:00

Merge branch 'master' into box-decoration-break

This commit is contained in:
Guillaume Ayoub 2019-03-19 17:50:16 +01:00
commit 5dd6e4c2cd
13 changed files with 277 additions and 56 deletions

View File

@ -93,7 +93,7 @@ INITIAL_VALUES = {
# Color 3 (REC): https://www.w3.org/TR/css3-color/
'opacity': 1,
# Multi-column Layout (CR): https://www.w3.org/TR/css3-multicol/
# Multi-column Layout (WD): https://www.w3.org/TR/css-multicol-1/
'column_width': 'auto',
'column_count': 'auto',
'column_gap': Dimension(1, 'em'),
@ -103,7 +103,7 @@ INITIAL_VALUES = {
'column_fill': 'balance',
'column_span': 'none',
# Fonts 3 (CR): https://www.w3.org/TR/css-fonts-3/
# Fonts 3 (REC): https://www.w3.org/TR/css-fonts-3/
'font_family': ('serif',), # depends on user agent
'font_feature_settings': 'normal',
'font_kerning': 'auto',
@ -120,11 +120,12 @@ INITIAL_VALUES = {
'font_variant_position': 'normal',
'font_weight': 400,
# Fragmentation 3 (CR): https://www.w3.org/TR/css-break-3/
# Fragmentation 3/4 (CR/WD): https://www.w3.org/TR/css-break-4/
'box_decoration_break': 'slice',
'break_after': 'auto',
'break_before': 'auto',
'break_inside': 'auto',
'margin_break': 'auto',
'orphans': 2,
'widows': 2,
@ -139,7 +140,7 @@ INITIAL_VALUES = {
'image_resolution': 1, # dppx
'image_rendering': 'auto',
# Paged Media 3 (WD): https://www.w3.org/TR/css3-page/
# Paged Media 3 (WD): https://www.w3.org/TR/css-page-3/
'size': None, # set to A4 in computed_values
'page': 'auto',
'bleed_left': 'auto',
@ -161,11 +162,11 @@ INITIAL_VALUES = {
'white_space': 'normal',
'word_spacing': 0, # computed value for 'normal'
# Transforms 1 (WD): https://www.w3.org/TR/css-transforms-1/
# Transforms 1 (CR): https://www.w3.org/TR/css-transforms-1/
'transform_origin': (Dimension(50, '%'), Dimension(50, '%')),
'transform': (), # computed value for 'none'
# User Interface 3 (CR): https://www.w3.org/TR/css-ui-3/
# User Interface 3 (REC): https://www.w3.org/TR/css-ui-3/
'box_sizing': 'content-box',
'outline_color': 'currentColor', # invert is not supported
'outline_style': 'none',

View File

@ -172,7 +172,7 @@ def comma_separated_list(function):
def get_keyword(token):
"""If ``value`` is a keyword, return its name.
"""If ``token`` is a keyword, return its lowercase name.
Otherwise return ``None``.
@ -181,6 +181,16 @@ def get_keyword(token):
return token.lower_value
def get_custom_ident(token):
"""If ``token`` is a keyword, return its name.
Otherwise return ``None``.
"""
if token.type == 'ident':
return token.value
def get_single_keyword(tokens):
"""If ``values`` is a 1-element list of keywords, return its name.
@ -432,7 +442,7 @@ def check_counter_function(token, allowed_type=None):
ident = args.pop(0)
if ident.type != 'ident':
return
arguments.append(ident.lower_value)
arguments.append(ident.value)
if name == 'counters':
string = args.pop(0)
@ -626,7 +636,7 @@ def get_target(token, base_url):
ident = args.pop(0)
if ident.type != 'ident':
return
values.append(ident.lower_value)
values.append(ident.value)
if name == 'target-counters':
string = get_string(args.pop(0))

View File

@ -17,9 +17,9 @@ from .. import computed_values
from ..properties import KNOWN_PROPERTIES, Dimension
from ..utils import (
InvalidValues, comma_separated_list, get_angle, get_content_list,
get_content_list_token, get_image, get_keyword, get_length, get_resolution,
get_single_keyword, get_url, parse_2d_position, parse_background_position,
parse_function, single_keyword, single_token)
get_content_list_token, get_custom_ident, get_image, get_keyword,
get_length, get_resolution, get_single_keyword, get_url, parse_2d_position,
parse_background_position, parse_function, single_keyword, single_token)
PREFIX = '-weasy-'
PROPRIETARY = set()
@ -308,6 +308,13 @@ def box_decoration_break(keyword):
return keyword in ('slice', 'clone')
@property()
@single_keyword
def margin_break(keyword):
"""``margin-break`` property validation."""
return keyword in ('auto', 'keep', 'discard')
@property(unstable=True)
@single_token
def page(token):
@ -1306,14 +1313,14 @@ def string_set(tokens, base_url):
"""Validation for ``string-set``."""
# Spec asks for strings after custom keywords, but we allow content-lists
if len(tokens) >= 2:
var_name = get_keyword(tokens[0])
var_name = get_custom_ident(tokens[0])
if var_name is None:
return
parsed_tokens = tuple(
get_content_list_token(token, base_url) for token in tokens[1:])
if None not in parsed_tokens:
return (var_name, parsed_tokens)
elif tokens and tokens[0].value == 'none':
elif tokens and get_keyword(tokens[0]) == 'none':
return 'none'

View File

@ -204,6 +204,7 @@ class LayoutContext(object):
self.excluded_shapes = None # Not initialized yet
self.string_set = defaultdict(lambda: defaultdict(lambda: list()))
self.current_page = None
self.forced_break = False
# Cache
self.strut_layouts = {}

View File

@ -41,6 +41,15 @@ def block_level_layout(context, box, max_position_y, skip_stack,
if box.margin_bottom == 'auto':
box.margin_bottom = 0
if (context.current_page > 1 and page_is_empty):
# TODO: we should take care of cases when this box doesn't have
# collapsing margins with the first child of the page, see
# test_margin_break_clearance.
if box.style['margin_break'] == 'discard':
box.margin_top = 0
elif box.style['margin_break'] == 'auto' and context.forced_break:
box.margin_top = 0
collapsed_margin = collapse_margin(
adjoining_margins + [box.margin_top])
box.clearance = get_clearance(context, box, collapsed_margin)
@ -438,11 +447,6 @@ def block_container_layout(context, box, max_position_y, skip_stack,
page_name = block_level_page_name(last_in_flow_child, child)
if page_name or page_break in (
'page', 'left', 'right', 'recto', 'verso'):
if page_break == 'page':
page_break = 'any'
elif page_break not in ('left', 'right', 'recto', 'verso'):
assert page_name
page_break = 'any'
page_name = child.page_values()[0]
next_page = {'break': page_break, 'page': page_name}
resume_at = (index, None)

View File

@ -39,13 +39,18 @@ def flex_layout(context, box, max_position_y, skip_stack, containing_block,
else:
axis, cross = 'height', 'width'
margin_left = 0 if box.margin_left == 'auto' else box.margin_left
margin_right = 0 if box.margin_right == 'auto' else box.margin_right
margin_top = 0 if box.margin_top == 'auto' else box.margin_top
margin_bottom = 0 if box.margin_bottom == 'auto' else box.margin_bottom
if getattr(box, axis) != 'auto':
available_main_space = getattr(box, axis)
else:
if axis == 'width':
available_main_space = (
containing_block.width -
box.margin_left - box.margin_right -
margin_left - margin_right -
box.padding_left - box.padding_right -
box.border_left_width - box.border_right_width)
else:
@ -58,7 +63,7 @@ def flex_layout(context, box, max_position_y, skip_stack, containing_block,
main_space = min(main_space, containing_block.height)
available_main_space = (
main_space -
box.margin_top - box.margin_bottom -
margin_top - margin_bottom -
box.padding_top - box.padding_bottom -
box.border_top_width - box.border_bottom_width)
@ -75,24 +80,28 @@ def flex_layout(context, box, max_position_y, skip_stack, containing_block,
main_space = min(main_space, containing_block.height)
available_cross_space = (
main_space -
box.margin_top - box.margin_bottom -
margin_top - margin_bottom -
box.padding_top - box.padding_bottom -
box.border_top_width - box.border_bottom_width)
else:
available_cross_space = (
containing_block.width -
box.margin_left - box.margin_right -
margin_left - margin_right -
box.padding_left - box.padding_right -
box.border_left_width - box.border_right_width)
# Step 3
children = box.children
resolve_percentages(box, containing_block)
if box.margin_top == 'auto':
box.margin_top = 0
if box.margin_bottom == 'auto':
box.margin_bottom = 0
parent_box = box.copy_with_children(children)
resolve_percentages(parent_box, containing_block)
if parent_box.margin_top == 'auto':
box.margin_top = 0
if parent_box.margin_bottom == 'auto':
box.margin_bottom = 0
if parent_box.margin_left == 'auto':
box.margin_left = 0
if parent_box.margin_right == 'auto':
box.margin_right = 0
if isinstance(parent_box, boxes.FlexBox):
blocks.block_level_width(parent_box, containing_block)
else:
@ -441,7 +450,7 @@ def flex_layout(context, box, max_position_y, skip_stack, containing_block,
parent_box, device_size, page_is_empty, absolute_boxes,
fixed_boxes, adjoining_margins=[]))
child._baseline = find_in_flow_baseline(new_child)
child._baseline = find_in_flow_baseline(new_child) or 0
if cross == 'height':
child.height = new_child.height
# As flex items margins never collapse (with other flex items
@ -844,8 +853,9 @@ def flex_layout(context, box, max_position_y, skip_stack, containing_block,
if axis == 'width': # and main text direction is horizontal
box.baseline = flex_lines[0].lower_baseline if flex_lines else 0
else:
box.baseline = (
find_in_flow_baseline(box.children[0]) if box.children else 0)
box.baseline = ((
find_in_flow_baseline(box.children[0])
if box.children else 0) or 0)
context.finish_block_formatting_context(box)

View File

@ -853,10 +853,9 @@ def split_inline_box(context, box, position_x, max_x, skip_stack,
# We have to check whether the child we're breaking
# is the one broken by the initial skip stack.
broken_child = bool(
initial_skip_stack and
initial_skip_stack[0] == child_index and
initial_skip_stack[1])
broken_child = same_broken_child(
initial_skip_stack,
(child_index, child_resume_at))
if broken_child:
# As this child has already been broken
# following the original skip stack, we have to
@ -1286,3 +1285,14 @@ def can_break_inside(box):
else:
return False
return False
def same_broken_child(skip_stack_1, skip_stack_2):
"""Check that the skip stacks design the same text box."""
while isinstance(skip_stack_1, tuple) and isinstance(skip_stack_2, tuple):
if skip_stack_1[1] is None and skip_stack_2[1] is None:
return True
if skip_stack_1[0] != skip_stack_2[0]:
return False
skip_stack_1, skip_stack_2 = skip_stack_1[1], skip_stack_2[1]
return False

View File

@ -545,6 +545,7 @@ def make_page(context, root_box, page_type, resume_at, page_number,
# See http://www.w3.org/TR/CSS21/visuren.html#dis-pos-flo
assert isinstance(root_box, (boxes.BlockBox, boxes.FlexContainerBox))
context.create_block_formatting_context()
context.current_page = page_number
page_is_empty = True
adjoining_margins = []
positioned_boxes = [] # Mixed absolute and fixed
@ -722,16 +723,20 @@ def remake_page(index, context, root_box, html, cascaded_styles,
page_state = copy.deepcopy(initial_page_state)
next_page_name = initial_next_page['page']
first = index == 0
# TODO: handle recto/verso and add test
blank = ((initial_next_page['break'] == 'left' and right_page) or
(initial_next_page['break'] == 'right' and not right_page))
if blank:
next_page_name = None
side = 'right' if right_page else 'left'
page_type = PageType(
side, blank, first, name=(next_page_name or None))
page_type = PageType(side, blank, first, name=(next_page_name or None))
set_page_type_computed_styles(
page_type, cascaded_styles, computed_styles, html)
context.forced_break = (
initial_next_page['break'] != 'any' or initial_next_page['page'])
context.margin_clearance = False
# make_page wants a page_number of index + 1
page_number = index + 1
page, resume_at, next_page = make_page(

View File

@ -656,6 +656,8 @@ def find_in_flow_baseline(box, last=False, baseline_types=(boxes.LineBox,)):
Return the absolute Y position for the first (or last) in-flow baseline
if any, or None.
"""
# TODO: synthetize baseline when needed
# See https://www.w3.org/TR/css-align-3/#synthesize-baseline
if isinstance(box, baseline_types):
return box.position_y + box.baseline
if isinstance(box, boxes.ParentBox) and not isinstance(

View File

@ -1065,6 +1065,25 @@ def test_counters_6():
('p::marker', 'Text', '0.')])])])
@assert_no_logs
def test_counters_7():
# Test that counters are case-sensitive
# See https://github.com/Kozea/WeasyPrint/pull/827
assert_tree(parse_all('''
<style>
p { counter-increment: p 2 }
p:before { content: counter(p) '.' counter(P); }
</style>
<p></p>
<p style="counter-increment: P 3"></p>
<p></p>'''), [
('p', 'Block', [
('p', 'Line', [
('p::before', 'Inline', [
('p::before', 'Text', counter)])])])
for counter in '2.0 2.3 4.3'.split()])
@assert_no_logs
def test_counter_styles_1():
assert_tree(parse_all('''
@ -1477,7 +1496,7 @@ def test_margin_box_string_set_7():
# Test regression: https://github.com/Kozea/WeasyPrint/issues/722
page_1, = render_pages('''
<style>
img { string-set: left attr(alt) }
img { string-set: left attr(alt) }
img + img { string-set: right attr(alt) }
@page { @top-left { content: '[' string(left) ']' }
@top-right { content: '{' string(right) '}' } }
@ -1495,15 +1514,16 @@ def test_margin_box_string_set_7():
assert right_text_box.text == '{Cake}'
@assert_no_logs
def test_margin_box_string_set_8():
# Test regression: https://github.com/Kozea/WeasyPrint/issues/726
page_1, page_2, page_3 = render_pages('''
<style>
@page { @top-left { content: '[' string(left) ']' } }
p { page-break-before: always }
.initial { -weasy-string-set: left 'initial' }
.empty { -weasy-string-set: left '' }
.space { -weasy-string-set: left ' ' }
.initial { string-set: left 'initial' }
.empty { string-set: left '' }
.space { string-set: left ' ' }
</style>
<p class="initial">Initial</p>
@ -1526,6 +1546,31 @@ def test_margin_box_string_set_8():
assert left_text_box.text == '[ ]'
@assert_no_logs
def test_margin_box_string_set_9():
# Test that named strings are case-sensitive
# See https://github.com/Kozea/WeasyPrint/pull/827
page_1, = render_pages('''
<style>
@page {
@top-center {
content: string(text_header, first)
' ' string(TEXT_header, first)
}
}
p { string-set: text_header content() }
div { string-set: TEXT_header content() }
</style>
<p>first assignment</p>
<div>second assignment</div>
''')
html, top_center = page_1.children
line_box, = top_center.children
text_box, = line_box.children
assert text_box.text == 'first assignment second assignment'
@assert_no_logs
def test_page_counters():
"""Test page-based counters."""

View File

@ -353,3 +353,20 @@ def test_flex_item_min_height():
div_3.height ==
article.height ==
50)
@assert_no_logs
def test_flex_auto_margin():
# Regression test for https://github.com/Kozea/WeasyPrint/issues/800
page, = render_pages('<div style="display: flex; margin: auto">')
page, = render_pages(
'<div style="display: flex; flex-direction: column; margin: auto">')
@assert_no_logs
def test_flex_no_baseline():
# Regression test for https://github.com/Kozea/WeasyPrint/issues/765
page, = render_pages('''
<div class="references" style="display: flex; align-items: baseline;">
<div></div>
</div>''')

View File

@ -238,9 +238,9 @@ def test_breaking_linebox_regression_2():
def test_breaking_linebox_regression_3():
# Regression test #1 for https://github.com/Kozea/WeasyPrint/issues/560
page, = parse(
'<style>@font-face { src: url(AHEM____.TTF); font-family: ahem }</style>'
'<div style="width: 5.5em; font-family: ahem">'
'aaaa aaaa a [<span>aaa</span>]')
'<style>@font-face {src: url(AHEM____.TTF); font-family: ahem}</style>'
'<div style="width: 5.5em; font-family: ahem">'
'aaaa aaaa a [<span>aaa</span>]')
html, = page.children
body, = html.children
div, = body.children
@ -257,9 +257,9 @@ def test_breaking_linebox_regression_3():
def test_breaking_linebox_regression_4():
# Regression test #2 for https://github.com/Kozea/WeasyPrint/issues/560
page, = parse(
'<style>@font-face { src: url(AHEM____.TTF); font-family: ahem }</style>'
'<div style="width: 5.5em; font-family: ahem">'
'aaaa a <span>b c</span>d')
'<style>@font-face {src: url(AHEM____.TTF); font-family: ahem}</style>'
'<div style="width: 5.5em; font-family: ahem">'
'aaaa a <span>b c</span>d')
html, = page.children
body, = html.children
div, = body.children
@ -275,9 +275,9 @@ def test_breaking_linebox_regression_4():
def test_breaking_linebox_regression_5():
# Regression test for https://github.com/Kozea/WeasyPrint/issues/580
page, = parse(
'<style>@font-face { src: url(AHEM____.TTF); font-family: ahem }</style>'
'<div style="width: 5.5em; font-family: ahem">'
'<span>aaaa aaaa a a a</span><span>bc</span>')
'<style>@font-face {src: url(AHEM____.TTF); font-family: ahem}</style>'
'<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
@ -293,9 +293,9 @@ def test_breaking_linebox_regression_5():
def test_breaking_linebox_regression_6():
# Regression test for https://github.com/Kozea/WeasyPrint/issues/586
page, = parse(
'<style>@font-face { src: url(AHEM____.TTF); font-family: ahem }</style>'
'<div style="width: 5.5em; font-family: ahem">'
'a a <span style="white-space: nowrap">/ccc</span>')
'<style>@font-face {src: url(AHEM____.TTF); font-family: ahem}</style>'
'<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
@ -308,9 +308,9 @@ def test_breaking_linebox_regression_6():
def test_breaking_linebox_regression_7():
# Regression test for https://github.com/Kozea/WeasyPrint/issues/660
page, = parse(
'<style>@font-face { src: url(AHEM____.TTF); font-family: ahem }</style>'
'<div style="width: 3.5em; font-family: ahem">'
'<span><span>abc d e</span></span><span>f')
'<style>@font-face {src: url(AHEM____.TTF); font-family: ahem}</style>'
'<div style="width: 3.5em; font-family: ahem">'
'<span><span>abc d e</span></span><span>f')
html, = page.children
body, = html.children
div, = body.children
@ -321,6 +321,46 @@ def test_breaking_linebox_regression_7():
assert line3.children[1].children[0].text == 'f'
@assert_no_logs
def test_breaking_linebox_regression_8():
# Regression test for https://github.com/Kozea/WeasyPrint/issues/783
page, = parse(
'<style>@font-face {src: url(AHEM____.TTF); font-family: ahem}</style>'
'<p style="font-family: ahem"><span>\n'
'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n'
'bbbbbbbbbbb\n'
'<b>cccc</b></span>ddd</p>')
html, = page.children
body, = html.children
p, = body.children
line1, line2 = p.children
assert line1.children[0].children[0].text == (
'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa bbbbbbbbbbb')
assert line2.children[0].children[0].children[0].text == 'cccc'
assert line2.children[1].text == 'ddd'
@pytest.mark.xfail
@assert_no_logs
def test_breaking_linebox_regression_9():
# Regression test for https://github.com/Kozea/WeasyPrint/issues/783
# TODO: inlines.can_break_inside return False for span but we can break
# before the <b> tag. can_break_inside should be fixed.
page, = parse(
'<style>@font-face {src: url(AHEM____.TTF); font-family: ahem}</style>'
'<p style="font-family: ahem"><span>\n'
'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbb\n'
'<b>cccc</b></span>ddd</p>')
html, = page.children
body, = html.children
p, = body.children
line1, line2 = p.children
assert line1.children[0].children[0].text == (
'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbb')
assert line2.children[0].children[0].children[0].text == 'cccc'
assert line2.children[1].text == 'ddd'
@assert_no_logs
def test_linebox_text():
page, = parse('''

View File

@ -428,6 +428,75 @@ def test_page_breaks_complex_8():
assert div_4.height == 20
@assert_no_logs
@pytest.mark.parametrize('break_after, margin_break, margin_top', (
('page', 'auto', 0),
('auto', 'auto', 5),
('page', 'keep', 5),
('auto', 'keep', 5),
('page', 'discard', 0),
('auto', 'discard', 0),
))
def test_margin_break(break_after, margin_break, margin_top):
page_1, page_2 = render_pages('''
<style>
@page { size: 70px; margin: 0 }
div { height: 63px; margin: 5px 0 8px;
break-after: %s; margin-break: %s }
</style>
<section>
<div></div>
</section>
<section>
<div></div>
</section>
''' % (break_after, margin_break))
html, = page_1.children
body, = html.children
section, = body.children
div, = section.children
assert div.margin_top == 5
html, = page_2.children
body, = html.children
section, = body.children
div, = section.children
assert div.margin_top == margin_top
@pytest.mark.xfail
@assert_no_logs
def test_margin_break_clearance():
page_1, page_2 = render_pages('''
<style>
@page { size: 70px; margin: 0 }
div { height: 63px; margin: 5px 0 8px; break-after: page }
</style>
<section>
<div></div>
</section>
<section>
<div style="border-top: 1px solid black">
<div></div>
</div>
</section>
''')
html, = page_1.children
body, = html.children
section, = body.children
div, = section.children
assert div.margin_top == 5
html, = page_2.children
body, = html.children
section, = body.children
div_1, = section.children
assert div_1.margin_top == 0
div_2, = div_1.children
assert div_2.margin_top == 5
assert div_2.content_box_y() == 5
@assert_no_logs
def test_page_names_1():
pages = render_pages('''