diff --git a/docs/features.rst b/docs/features.rst
index 5138e7a9f..2a949ce7b 100644
--- a/docs/features.rst
+++ b/docs/features.rst
@@ -494,22 +494,36 @@ The ``image-orientation`` property is **not** supported.
.. _Image Values and Replaced Content Module Level 4: http://www.w3.org/TR/css4-images/
-CSS Basic User Interface Module Level 3
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+CSS Box Sizing Module Level 3
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+The `CSS Box Sizing Module Level 3`_ is a candidate recommendation extending
+"the CSS sizing properties with keywords that represent content-based
+'intrinsic' sizes and context-based 'extrinsic' sizes."
+
+The new property defined in this document is implemented in WeasyPrint:
+``box-sizing``.
+
+The ``min-content``, ``max-content`` and ``fit-content()`` sizing values are
+**not** supported.
+
+.. _CSS Box Sizing Module Level 3: https://www.w3.org/TR/css-sizing-3/
-The `CSS Basic User Interface Module Level 3`_ also known as CSS3 UI is a
-candidate recommendation describing "CSS properties which enable authors to
-style user interface related properties and values."
-Two new properties defined in this document are implemented in WeasyPrint:
-the ``box-sizing`` and ``text-overflow`` properties.
+CSS Overflow Module Level 3
+~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+The `CSS Overflow Module Level 3`_ is a working draft containing "the features
+of CSS relating to scrollable overflow handling in visual media."
-Some of the properties do not apply for WeasyPrint: ``cursor``, ``resize``,
-``caret-color``, ``nav-(up|right|down|left)``.
+The ``overflow`` property is supported, as defined in CSS2. ``overflow-x``,
+``overflow-y``, ``overflow-clip-margin``, ``overflow-inline`` and
+``overflow-block`` are **not** supported.
-The ``outline-offset`` property is **not** implemented.
+The ``text-overflow``, ``block-ellipsis``, ``line-clamp``, ``max-lines`` and
+``continue`` properties are supported.
-.. _CSS Basic User Interface Module Level 3: http://www.w3.org/TR/css-ui-3/
+.. _CSS Overflow Module Level 3: https://www.w3.org/TR/2020/WD-css-overflow-3-20200603/
CSS Values and Units Module Level 3
diff --git a/tests/test_draw/test_text.py b/tests/test_draw/test_text.py
index 14a87f265..aa8c17f52 100644
--- a/tests/test_draw/test_text.py
+++ b/tests/test_draw/test_text.py
@@ -6,6 +6,8 @@
"""
+import pytest
+
from . import assert_pixels
@@ -118,6 +120,157 @@ def test_text_align_rtl_trailing_whitespace():
''')
+def test_max_lines_ellipsis():
+ assert_pixels('max_lines_ellipsis', 10, 10, '''
+ BBBBBBBB__
+ BBBBBBBB__
+ BBBBBBBBBB
+ BBBBBBBBBB
+ __________
+ __________
+ __________
+ __________
+ __________
+ __________
+ ''', '''
+
+
+ abcd efgh ijkl
+
+ ''')
+
+
+@pytest.mark.xfail
+def test_max_lines_nested():
+ assert_pixels('max_lines_nested', 10, 12, '''
+ BBBBBBBBBB
+ BBBBBBBBBB
+ BBBBBBBBBB
+ BBBBBBBBBB
+ rrrrrrrrrr
+ rrrrrrrrrr
+ rrrrrrrrrr
+ rrrrrrrrrr
+ BBBBBBBBBB
+ BBBBBBBBBB
+ __________
+ __________
+ ''', '''
+
+
+ aaaaa
+ aaaaa
+
+ bbbbb
+ bbbbb
+ bbbbb
+ bbbbb
+
+ aaaaa
+ aaaaa
+
+ ''')
+
+
+def test_line_clamp():
+ assert_pixels('line_clamp', 10, 10, '''
+ BBBB__BB__
+ BBBB__BB__
+ BBBB__BB__
+ BBBB__BB__
+ BBBBBBBBBB
+ BBBBBBBBBB
+ __________
+ __________
+ __________
+ __________
+ ''', '''
+
+
+
+ aa a
+ bb b
+ cc c
+ dddd
+ eeee
+ ffff
+ gggg
+ hhhh
+
+ ''')
+
+
+@pytest.mark.xfail
+def test_ellipsis_nested():
+ assert_pixels('ellipsis_nested', 10, 10, '''
+ BBBBBB____
+ BBBBBB____
+ BBBBBB____
+ BBBBBB____
+ BBBBBB____
+ BBBBBB____
+ BBBBBB____
+ BBBBBB____
+ BBBBBBBB__
+ BBBBBBBB__
+ ''', '''
+
+
+
aaa
+
aaa
+
aaa
+
aaa
+
aaa
+
aaa
+
+ ''')
+
+
def test_text_align_right():
assert_pixels('text_align_right', 9, 6, '''
_________
diff --git a/tests/test_layout/test_block.py b/tests/test_layout/test_block.py
index 253f14cbf..8c35417d8 100644
--- a/tests/test_layout/test_block.py
+++ b/tests/test_layout/test_block.py
@@ -775,3 +775,62 @@ def test_box_margin_top_repagination():
div, h1 = body.children
assert div.margin_top == 0
assert div.padding_box_y() == 0
+
+
+@assert_no_logs
+def test_continue_discard():
+ page_1, = parse('''
+
+
+ a
+ b
+ c
+ d
+ e
+ f
+ ''')
+ html, = page_1.children
+ body, = html.children
+ article, = body.children
+ assert article.height == 3 * 25
+ div_1, div_2, div_3 = article.children
+ assert div_1.position_y == 1
+ assert div_2.position_y == 1 + 25
+ assert div_3.position_y == 1 + 25 * 2
+ assert article.border_bottom_width == 1
+
+
+@assert_no_logs
+def test_continue_discard_children():
+ page_1, = parse('''
+
+
+
+ a
+ b
+ c
+ d
+ e
+ f
+
+ ''')
+ html, = page_1.children
+ body, = html.children
+ article, = body.children
+ assert article.height == 2 + 3 * 25
+ section, = article.children
+ assert section.height == 3 * 25
+ div_1, div_2, div_3 = section.children
+ assert div_1.position_y == 2
+ assert div_2.position_y == 2 + 25
+ assert div_3.position_y == 2 + 25 * 2
+ assert article.border_bottom_width == 1
diff --git a/tests/test_text.py b/tests/test_text.py
index 1730e382b..a07dd1d6d 100644
--- a/tests/test_text.py
+++ b/tests/test_text.py
@@ -1050,3 +1050,59 @@ def test_text_floating_pre_line():
This is
oh this end
''')
+
+
+@pytest.mark.xfail
+@assert_no_logs
+def test_max_lines():
+ page, = render_pages('''
+
+
+ abcd efgh ijkl
+
+ ''')
+ html, = page.children
+ body, = html.children
+ p1, p2 = body.children
+ line1, line2 = p1.children
+ line3, = p2.children
+ text1, = line1.children
+ text2, = line2.children
+ text3, = line3.children
+ assert text1.text == 'abcd'
+ assert text2.text == 'efgh'
+ assert text3.text == 'ijkl'
+
+
+@assert_no_logs
+def test_continue():
+ page, = render_pages('''
+
+
+ abcd efgh ijkl
+
+ ''')
+ html, = page.children
+ body, = html.children
+ p, = body.children
+ line1, line2 = p.children
+ text1, = line1.children
+ text2, = line2.children
+ assert text1.text == 'abcd'
+ assert text2.text == 'efgh'
diff --git a/weasyprint/css/properties.py b/weasyprint/css/properties.py
index 19af719be..c529b2bdd 100644
--- a/weasyprint/css/properties.py
+++ b/weasyprint/css/properties.py
@@ -184,6 +184,9 @@
'text_decoration_style': 'solid',
# Overflow Module 3 (WD): https://www.w3.org/TR/css-overflow-3/
+ 'block_ellipsis': 'none',
+ 'continue': 'auto',
+ 'max_lines': 'none',
'overflow': 'visible',
'text_overflow': 'clip',
@@ -218,6 +221,7 @@
# link: click events normally bubble up to link ancestors
# See http://lists.w3.org/Archives/Public/www-style/2012Jun/0315.html
INHERITED = {
+ 'block_ellipsis',
'border_collapse',
'border_spacing',
'caption_side',
diff --git a/weasyprint/css/validation/expanders.py b/weasyprint/css/validation/expanders.py
index c98d370aa..dd4e2fcdc 100644
--- a/weasyprint/css/validation/expanders.py
+++ b/weasyprint/css/validation/expanders.py
@@ -16,11 +16,12 @@
from .descriptors import expand_font_variant
from .properties import (
background_attachment, background_image, background_position,
- background_repeat, background_size, border_style, border_width, box,
- column_count, column_width, flex_basis, flex_direction, flex_grow_shrink,
- flex_wrap, font_family, font_size, font_stretch, font_style, font_weight,
- line_height, list_style_image, list_style_position, list_style_type,
- other_colors, overflow_wrap, validate_non_shorthand)
+ background_repeat, background_size, block_ellipsis, border_style,
+ border_width, box, column_count, column_width, flex_basis, flex_direction,
+ flex_grow_shrink, flex_wrap, font_family, font_size, font_stretch,
+ font_style, font_weight, line_height, list_style_image,
+ list_style_position, list_style_type, other_colors, overflow_wrap,
+ validate_non_shorthand)
EXPANDERS = {}
@@ -607,3 +608,26 @@ def expand_flex_flow(base_url, name, tokens):
raise InvalidValues
else:
raise InvalidValues
+
+
+@expander('line-clamp')
+def expand_line_clamp(base_url, name, tokens):
+ """Expand the ``line-clamp`` property."""
+ if len(tokens) == 1:
+ keyword = get_single_keyword(tokens)
+ if keyword == 'none':
+ yield 'max_lines', 'none'
+ yield 'continue', 'auto'
+ yield 'block-ellipsis', 'none'
+ elif tokens[0].type == 'number' and tokens[0].int_value is not None:
+ yield 'max_lines', tokens[0].int_value
+ yield 'continue', 'discard'
+ yield 'block-ellipsis', 'auto'
+ elif len(tokens) == 2:
+ if tokens[0].type == 'number':
+ max_lines = tokens[0].int_value
+ ellipsis = block_ellipsis([tokens[1]])
+ if max_lines and ellipsis is not None:
+ yield 'max_lines', tokens[0].value
+ yield 'continue', 'discard'
+ yield 'block-ellipsis', ellipsis
diff --git a/weasyprint/css/validation/properties.py b/weasyprint/css/validation/properties.py
index a9e0fc557..5437a33ed 100644
--- a/weasyprint/css/validation/properties.py
+++ b/weasyprint/css/validation/properties.py
@@ -326,6 +326,36 @@ def box_decoration_break(keyword):
return keyword in ('slice', 'clone')
+@property()
+@single_token
+def block_ellipsis(token):
+ """``box-ellipsis`` property validation."""
+ if token.type == 'string':
+ return ('string', token.value)
+ else:
+ keyword = get_keyword(token)
+ if keyword in ('none', 'auto'):
+ return keyword
+
+
+@property('continue', unstable=True)
+@single_keyword
+def continue_(keyword):
+ """``continue`` property validation."""
+ return keyword in ('auto', 'discard')
+
+
+@property(unstable=True)
+@single_token
+def max_lines(token):
+ if token.type == 'number' and token.int_value is not None:
+ if token.int_value >= 1:
+ return token.int_value
+ keyword = get_keyword(token)
+ if keyword == 'none':
+ return keyword
+
+
@property(unstable=True)
@single_keyword
def margin_break(keyword):
diff --git a/weasyprint/draw.py b/weasyprint/draw.py
index ee73f1640..37fa532ab 100644
--- a/weasyprint/draw.py
+++ b/weasyprint/draw.py
@@ -15,6 +15,7 @@
from .layout.backgrounds import BackgroundLayer
from .stacking import StackingContext
from .text.ffi import ffi, harfbuzz, pango, units_from_double, units_to_double
+from .text.line_break import get_last_word_end
SIDES = ('top', 'right', 'bottom', 'left')
CROP = '''
@@ -991,7 +992,8 @@ def draw_replacedbox(context, box):
context, draw_width, draw_height, box.style['image_rendering'])
-def draw_inline_level(context, page, box, offset_x=0, text_overflow='clip'):
+def draw_inline_level(context, page, box, offset_x=0, text_overflow='clip',
+ block_ellipsis='none'):
if isinstance(box, StackingContext):
stacking_context = box
assert isinstance(
@@ -1003,8 +1005,13 @@ def draw_inline_level(context, page, box, offset_x=0, text_overflow='clip'):
if isinstance(box, (boxes.InlineBox, boxes.LineBox)):
if isinstance(box, boxes.LineBox):
text_overflow = box.text_overflow
+ block_ellipsis = box.block_ellipsis
in_text = False
- for child in box.children:
+ ellipsis = 'none'
+ for i, child in enumerate(box.children):
+ if i == len(box.children) - 1:
+ # Last child
+ ellipsis = block_ellipsis
if isinstance(child, StackingContext):
child_offset_x = offset_x
else:
@@ -1014,13 +1021,16 @@ def draw_inline_level(context, page, box, offset_x=0, text_overflow='clip'):
if not in_text:
context.begin_text()
in_text = True
- draw_text(context, child, child_offset_x, text_overflow)
+ draw_text(
+ context, child, child_offset_x, text_overflow,
+ ellipsis)
else:
if in_text:
in_text = False
context.end_text()
draw_inline_level(
- context, page, child, child_offset_x, text_overflow)
+ context, page, child, child_offset_x, text_overflow,
+ ellipsis)
if in_text:
context.end_text()
elif isinstance(box, boxes.InlineReplacedBox):
@@ -1033,7 +1043,7 @@ def draw_inline_level(context, page, box, offset_x=0, text_overflow='clip'):
context.end_text()
-def draw_text(context, textbox, offset_x, text_overflow):
+def draw_text(context, textbox, offset_x, text_overflow, block_ellipsis):
"""Draw a textbox to a pydyf stream."""
# Pango crashes with font-size: 0
assert textbox.style['font_size']
@@ -1046,7 +1056,7 @@ def draw_text(context, textbox, offset_x, text_overflow):
context.set_alpha(textbox.style['color'][3])
textbox.pango_layout.reactivate(textbox.style)
- draw_first_line(context, textbox, text_overflow, x, y)
+ draw_first_line(context, textbox, text_overflow, block_ellipsis, x, y)
# Draw text decoration
values = textbox.style['text_decoration_line']
@@ -1073,20 +1083,40 @@ def draw_text(context, textbox, offset_x, text_overflow):
textbox.pango_layout.deactivate()
-def draw_first_line(context, textbox, text_overflow, x, y):
+def draw_first_line(context, textbox, text_overflow, block_ellipsis, x, y):
"""Draw the given ``textbox`` line to the document ``context``."""
pango.pango_layout_set_single_paragraph_mode(
textbox.pango_layout.layout, True)
- if text_overflow == 'ellipsis':
+ if text_overflow == 'ellipsis' or block_ellipsis != 'none':
assert textbox.pango_layout.max_width is not None
max_width = textbox.pango_layout.max_width
pango.pango_layout_set_width(
textbox.pango_layout.layout, units_from_double(max_width))
- pango.pango_layout_set_ellipsize(
- textbox.pango_layout.layout, pango.PANGO_ELLIPSIZE_END)
-
- first_line, _ = textbox.pango_layout.get_first_line()
+ if text_overflow == 'ellipsis':
+ pango.pango_layout_set_ellipsize(
+ textbox.pango_layout.layout, pango.PANGO_ELLIPSIZE_END)
+ else:
+ if block_ellipsis == 'auto':
+ ellipsis = '…'
+ else:
+ assert block_ellipsis[0] == 'string'
+ ellipsis = block_ellipsis[1]
+ textbox.pango_layout.set_text(textbox.pango_layout.text + ellipsis)
+
+ first_line, second_line = textbox.pango_layout.get_first_line()
+
+ if block_ellipsis != 'none':
+ while second_line:
+ last_word_end = get_last_word_end(
+ textbox.pango_layout.text[:-len(ellipsis)],
+ textbox.style['lang'])
+ if last_word_end is None:
+ break
+ text = textbox.pango_layout.text
+ new_text = text.encode('utf-8')[:last_word_end].decode('utf-8')
+ textbox.pango_layout.set_text(new_text + ellipsis)
+ first_line, second_line = textbox.pango_layout.get_first_line()
font_size = textbox.style['font_size']
utf8_text = textbox.text.encode('utf-8')
diff --git a/weasyprint/formatting_structure/boxes.py b/weasyprint/formatting_structure/boxes.py
index 13b935012..fe8d97528 100644
--- a/weasyprint/formatting_structure/boxes.py
+++ b/weasyprint/formatting_structure/boxes.py
@@ -411,6 +411,7 @@ class LineBox(ParentBox):
"""
text_overflow = 'clip'
+ block_ellipsis = 'none'
@classmethod
def anonymous_from(cls, parent, *args, **kwargs):
diff --git a/weasyprint/layout/absolute.py b/weasyprint/layout/absolute.py
index 84e52eb76..a5062b443 100644
--- a/weasyprint/layout/absolute.py
+++ b/weasyprint/layout/absolute.py
@@ -206,7 +206,7 @@ def absolute_block(context, box, containing_block, fixed_boxes):
new_box, _, _, _, _ = block_container_layout(
context, box, max_position_y=float('inf'), skip_stack=None,
page_is_empty=False, absolute_boxes=absolute_boxes,
- fixed_boxes=fixed_boxes, adjoining_margins=None)
+ fixed_boxes=fixed_boxes, adjoining_margins=None, discard=False)
for child_placeholder in absolute_boxes:
absolute_layout(context, child_placeholder, new_box, fixed_boxes)
diff --git a/weasyprint/layout/blocks.py b/weasyprint/layout/blocks.py
index b487bec4f..1250fc92a 100644
--- a/weasyprint/layout/blocks.py
+++ b/weasyprint/layout/blocks.py
@@ -21,7 +21,7 @@
def block_level_layout(context, box, max_position_y, skip_stack,
containing_block, page_is_empty, absolute_boxes,
- fixed_boxes, adjoining_margins):
+ fixed_boxes, adjoining_margins, discard):
"""Lay out the block-level ``box``.
:param max_position_y: the absolute vertical position (as in
@@ -57,12 +57,12 @@ def block_level_layout(context, box, max_position_y, skip_stack,
return block_level_layout_switch(
context, box, max_position_y, skip_stack, containing_block,
- page_is_empty, absolute_boxes, fixed_boxes, adjoining_margins)
+ page_is_empty, absolute_boxes, fixed_boxes, adjoining_margins, discard)
def block_level_layout_switch(context, box, max_position_y, skip_stack,
containing_block, page_is_empty, absolute_boxes,
- fixed_boxes, adjoining_margins):
+ fixed_boxes, adjoining_margins, discard):
"""Call the layout function corresponding to the ``box`` type."""
if isinstance(box, boxes.TableBox):
return table_layout(
@@ -71,7 +71,8 @@ def block_level_layout_switch(context, box, max_position_y, skip_stack,
elif isinstance(box, boxes.BlockBox):
return block_box_layout(
context, box, max_position_y, skip_stack, containing_block,
- page_is_empty, absolute_boxes, fixed_boxes, adjoining_margins)
+ page_is_empty, absolute_boxes, fixed_boxes, adjoining_margins,
+ discard)
elif isinstance(box, boxes.BlockReplacedBox):
box = block_replaced_box_layout(box, containing_block)
# Don't collide with floats
@@ -93,7 +94,7 @@ def block_level_layout_switch(context, box, max_position_y, skip_stack,
def block_box_layout(context, box, max_position_y, skip_stack,
containing_block, page_is_empty, absolute_boxes,
- fixed_boxes, adjoining_margins):
+ fixed_boxes, adjoining_margins, discard):
"""Lay out the block ``box``."""
if (box.style['column_width'] != 'auto' or
box.style['column_count'] != 'auto'):
@@ -124,7 +125,7 @@ def block_box_layout(context, box, max_position_y, skip_stack,
new_box, resume_at, next_page, adjoining_margins, collapsing_through = \
block_container_layout(
context, box, max_position_y, skip_stack, page_is_empty,
- absolute_boxes, fixed_boxes, adjoining_margins)
+ absolute_boxes, fixed_boxes, adjoining_margins, discard)
if new_box and new_box.is_table_wrapper:
# Don't collide with floats
# http://www.w3.org/TR/CSS21/visuren.html#floats
@@ -256,7 +257,7 @@ def relative_positioning(box, containing_block):
def block_container_layout(context, box, max_position_y, skip_stack,
page_is_empty, absolute_boxes, fixed_boxes,
- adjoining_margins=None):
+ adjoining_margins, discard):
"""Set the ``box`` height."""
# TODO: boxes.FlexBox is allowed here because flex_layout calls
# block_container_layout, there's probably a better solution.
@@ -273,10 +274,14 @@ def block_container_layout(context, box, max_position_y, skip_stack,
is_start = skip_stack is None
box.remove_decoration(start=not is_start, end=False)
+ discard |= box.style['continue'] == 'discard'
+ draw_bottom_decoration = (
+ discard or box.style['box_decoration_break'] == 'clone')
+
if adjoining_margins is None:
adjoining_margins = []
- if box.style['box_decoration_break'] == 'clone':
+ if draw_bottom_decoration:
max_position_y -= (
box.padding_bottom + box.border_bottom_width +
box.margin_bottom)
@@ -371,14 +376,14 @@ def block_container_layout(context, box, max_position_y, skip_stack,
new_containing_block, absolute_boxes, fixed_boxes,
first_letter_style)
is_page_break = False
- for line, resume_at in lines_iterator:
+ for i, (line, resume_at) in enumerate(lines_iterator):
line.resume_at = resume_at
new_position_y = line.position_y + line.height
# Add bottom padding and border to the bottom position of the
# box if needed
- if resume_at is None or (
- box.style['box_decoration_break'] == 'clone'):
+ draw_bottom_decoration |= resume_at is None
+ if draw_bottom_decoration:
offset_y = box.border_bottom_width + box.padding_bottom
else:
offset_y = 0
@@ -431,6 +436,13 @@ def block_container_layout(context, box, max_position_y, skip_stack,
new_children.append(line)
position_y = new_position_y
skip_stack = resume_at
+
+ # Break box if we reached max-lines
+ if box.style['max_lines'] != 'none':
+ if i >= box.style['max_lines'] - 1:
+ line.block_ellipsis = box.style['block_ellipsis']
+ break
+
if new_children:
resume_at = (index, new_children[-1].resume_at)
if is_page_break:
@@ -505,7 +517,7 @@ def block_container_layout(context, box, max_position_y, skip_stack,
collapsing_through) = block_level_layout(
context, child, max_position_y, skip_stack,
new_containing_block, page_is_empty_with_no_children,
- absolute_boxes, fixed_boxes, adjoining_margins)
+ absolute_boxes, fixed_boxes, adjoining_margins, discard)
skip_stack = None
if new_child is not None:
@@ -595,7 +607,11 @@ def block_container_layout(context, box, max_position_y, skip_stack,
else:
resume_at = None
- if (resume_at is not None and
+ box_is_fragmented = resume_at is not None
+ if box.style['continue'] == 'discard':
+ resume_at = None
+
+ if (box_is_fragmented and
box.style['break_inside'] in ('avoid', 'avoid-page') and
not page_is_empty):
return (
@@ -638,8 +654,15 @@ def block_container_layout(context, box, max_position_y, skip_stack,
position_y += collapse_margin(adjoining_margins)
adjoining_margins = []
+ # Add block ellipsis
+ if box_is_fragmented and new_children:
+ last_child = new_children[-1]
+ if isinstance(last_child, boxes.LineBox):
+ last_child.block_ellipsis = box.style['block_ellipsis']
+
new_box = box.copy_with_children(new_children)
- new_box.remove_decoration(start=not is_start, end=resume_at is not None)
+ new_box.remove_decoration(
+ start=not is_start, end=box_is_fragmented and not discard)
# TODO: See corner cases in
# http://www.w3.org/TR/CSS21/visudet.html#normal-block
@@ -663,19 +686,19 @@ def block_container_layout(context, box, max_position_y, skip_stack,
if not isinstance(new_box, boxes.BlockBox):
context.finish_block_formatting_context(new_box)
- if resume_at is None:
+ if discard or not box_is_fragmented:
# After finish_block_formatting_context which may increment
# new_box.height
new_box.height = max(
min(new_box.height, new_box.max_height),
new_box.min_height)
- else:
+ elif max_position_y < float('inf'):
# Make the box fill the blank space at the bottom of the page
# https://www.w3.org/TR/css-break-3/#box-splitting
new_box.height = (
max_position_y - new_box.position_y -
(new_box.margin_height() - new_box.height))
- if box.style['box_decoration_break'] == 'clone':
+ if draw_bottom_decoration:
new_box.height += (
box.padding_bottom + box.border_bottom_width +
box.margin_bottom)
diff --git a/weasyprint/layout/columns.py b/weasyprint/layout/columns.py
index 618ffdf83..458e8563c 100644
--- a/weasyprint/layout/columns.py
+++ b/weasyprint/layout/columns.py
@@ -124,7 +124,7 @@ def create_column_box(children):
new_child, _, _, adjoining_margins, _ = block_level_layout(
context, block, original_max_position_y, skip_stack,
containing_block, page_is_empty, absolute_boxes, fixed_boxes,
- adjoining_margins)
+ adjoining_margins, discard=False)
new_children.append(new_child)
current_position_y = (
new_child.border_height() + new_child.border_box_y())
@@ -142,7 +142,7 @@ def create_column_box(children):
column_box = create_column_box(column_children)
new_child, _, _, _, _ = block_box_layout(
context, column_box, float('inf'), skip_stack, containing_block,
- page_is_empty, [], [], [])
+ page_is_empty, [], [], [], discard=False)
height = new_child.margin_height()
if style['column_fill'] == 'balance':
height /= count
@@ -163,7 +163,7 @@ def create_column_box(children):
new_box, resume_at, next_page, _, _ = block_box_layout(
context, column_box, box.content_box_y() + height,
column_skip_stack, containing_block, page_is_empty,
- [], [], [])
+ [], [], [], discard=False)
if new_box is None:
# We didn't render anything. Give up and use the max
# content height.
@@ -184,7 +184,8 @@ def create_column_box(children):
# Get the minimum size needed to render the next box
next_box, _, _, _, _ = block_box_layout(
context, column_box, box.content_box_y(),
- column_skip_stack, containing_block, True, [], [], [])
+ column_skip_stack, containing_block, True, [], [], [],
+ discard=False)
for child in next_box.children:
if child.is_in_normal_flow():
next_box_size = child.margin_height()
@@ -246,7 +247,7 @@ def create_column_box(children):
block_box_layout(
context, column_box, max_position_y, skip_stack,
containing_block, page_is_empty, absolute_boxes,
- fixed_boxes, None))
+ fixed_boxes, None, discard=False))
if new_child is None:
break
next_page = column_next_page
diff --git a/weasyprint/layout/flex.py b/weasyprint/layout/flex.py
index 029d6e6f4..c6d99d747 100644
--- a/weasyprint/layout/flex.py
+++ b/weasyprint/layout/flex.py
@@ -154,7 +154,7 @@ def flex_layout(context, box, max_position_y, skip_stack, containing_block,
new_child.style['max_height'] = Dimension(float('inf'), 'px')
new_child = blocks.block_level_layout(
context, new_child, float('inf'), child_skip_stack,
- parent_box, page_is_empty, [], [], [])[0]
+ parent_box, page_is_empty, [], [], [], False)[0]
content_size = new_child.height
child.min_height = min(specified_size, content_size)
@@ -214,7 +214,7 @@ def flex_layout(context, box, max_position_y, skip_stack, containing_block,
new_child = blocks.block_level_layout(
context, new_child, float('inf'), child_skip_stack,
parent_box, page_is_empty, absolute_boxes, fixed_boxes,
- adjoining_margins=[])[0]
+ adjoining_margins=[], discard=False)[0]
child.flex_base_size = new_child.margin_height()
elif child.style[axis] == 'min-content':
child.style[axis] = 'auto'
@@ -229,7 +229,7 @@ def flex_layout(context, box, max_position_y, skip_stack, containing_block,
new_child = blocks.block_level_layout(
context, new_child, float('inf'), child_skip_stack,
parent_box, page_is_empty, absolute_boxes, fixed_boxes,
- adjoining_margins=[])[0]
+ adjoining_margins=[], discard=False)[0]
child.flex_base_size = new_child.margin_height()
else:
assert child.style[axis].unit == 'px'
@@ -463,7 +463,7 @@ def flex_layout(context, box, max_position_y, skip_stack, containing_block,
blocks.block_level_layout_switch(
context, child_copy, float('inf'), child_skip_stack,
parent_box, page_is_empty, absolute_boxes, fixed_boxes,
- adjoining_margins=[]))
+ adjoining_margins=[], discard=False))
child._baseline = find_in_flow_baseline(new_child) or 0
if cross == 'height':
@@ -842,7 +842,7 @@ def flex_layout(context, box, max_position_y, skip_stack, containing_block,
new_child, child_resume_at = blocks.block_level_layout_switch(
context, child, max_position_y, child_skip_stack, box,
page_is_empty, absolute_boxes, fixed_boxes,
- adjoining_margins=[])[:2]
+ adjoining_margins=[], discard=False)[:2]
if new_child is None:
if resume_at and resume_at[0]:
resume_at = (resume_at[0] + i - 1, None)
diff --git a/weasyprint/layout/float.py b/weasyprint/layout/float.py
index 23b8bfcc1..4eed8af30 100644
--- a/weasyprint/layout/float.py
+++ b/weasyprint/layout/float.py
@@ -66,7 +66,7 @@ def float_layout(context, box, containing_block, absolute_boxes, fixed_boxes):
context, box, max_position_y=float('inf'),
skip_stack=None, page_is_empty=False,
absolute_boxes=absolute_boxes, fixed_boxes=fixed_boxes,
- adjoining_margins=None)
+ adjoining_margins=None, discard=False)
context.finish_block_formatting_context(box)
elif isinstance(box, boxes.FlexContainerBox):
box, _, _, _, _ = flex_layout(
diff --git a/weasyprint/layout/inlines.py b/weasyprint/layout/inlines.py
index 3090ee13d..711de4b9a 100644
--- a/weasyprint/layout/inlines.py
+++ b/weasyprint/layout/inlines.py
@@ -535,7 +535,7 @@ def inline_block_box_layout(context, box, position_x, skip_stack,
box, _, _, _, _ = block_container_layout(
context, box, max_position_y=float('inf'), skip_stack=skip_stack,
page_is_empty=True, absolute_boxes=absolute_boxes,
- fixed_boxes=fixed_boxes)
+ fixed_boxes=fixed_boxes, adjoining_margins=None, discard=False)
box.baseline = inline_block_baseline(box)
return box
diff --git a/weasyprint/layout/pages.py b/weasyprint/layout/pages.py
index 01483a8e6..f8f4b4dba 100644
--- a/weasyprint/layout/pages.py
+++ b/weasyprint/layout/pages.py
@@ -437,7 +437,8 @@ def margin_box_content_layout(context, page, box):
box, resume_at, next_page, _, _ = block_container_layout(
context, box,
max_position_y=float('inf'), skip_stack=None,
- page_is_empty=True, absolute_boxes=[], fixed_boxes=[])
+ page_is_empty=True, absolute_boxes=[], fixed_boxes=[],
+ adjoining_margins=None, discard=False)
assert resume_at is None
vertical_align = box.style['vertical_align']
@@ -548,7 +549,7 @@ def make_page(context, root_box, page_type, resume_at, page_number,
root_box, resume_at, next_page, _, _ = block_level_layout(
context, root_box, page_content_bottom, resume_at,
initial_containing_block, page_is_empty, positioned_boxes,
- positioned_boxes, adjoining_margins)
+ positioned_boxes, adjoining_margins, discard=False)
assert root_box
page.fixed_boxes = [
diff --git a/weasyprint/layout/tables.py b/weasyprint/layout/tables.py
index cbb7bd8ff..5e9c0ca4b 100644
--- a/weasyprint/layout/tables.py
+++ b/weasyprint/layout/tables.py
@@ -142,12 +142,11 @@ def group_layout(group, position_y, max_position_y,
cell.computed_height = cell.height
cell.height = 'auto'
cell, _, _, _, _ = block_container_layout(
- context, cell,
- max_position_y=float('inf'),
- skip_stack=None,
- page_is_empty=False,
+ context, cell, max_position_y=float('inf'),
+ skip_stack=None, page_is_empty=False,
absolute_boxes=absolute_boxes,
- fixed_boxes=fixed_boxes)
+ fixed_boxes=fixed_boxes, adjoining_margins=None,
+ discard=False)
cell.empty = not any(
child.is_floated() or child.is_in_normal_flow()
for child in cell.children)
diff --git a/weasyprint/text/line_break.py b/weasyprint/text/line_break.py
index 08db90da2..d6af045b9 100644
--- a/weasyprint/text/line_break.py
+++ b/weasyprint/text/line_break.py
@@ -599,3 +599,12 @@ def get_next_word_boundaries(text, lang):
else:
return None
return word_start, word_end
+
+
+def get_last_word_end(text, lang):
+ if not text or len(text) < 2:
+ return None
+ bytestring, log_attrs = get_log_attrs(text, lang)
+ for i, attr in enumerate(list(log_attrs)[::-1]):
+ if i and attr.is_word_end:
+ return len(bytestring) - i