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