diff --git a/tests/draw/test_text.py b/tests/draw/test_text.py index b59757f98..b008b15f3 100644 --- a/tests/draw/test_text.py +++ b/tests/draw/test_text.py @@ -294,6 +294,84 @@ def test_line_clamp_number(assert_pixels): ''') +def test_line_clamp_nested(assert_pixels): + assert_pixels(''' + BBBB__BB__ + BBBB__BB__ + BBBB__BB__ + BBBB__BB__ + BBBBBBBBBB + BBBBBBBBBB + __________ + __________ + __________ + __________ + ''', ''' + + +
+ aa a +

+ bb b + cc c + dddd + eeee + ffff + gggg + hhhh +

+
+ ''') + + +def test_line_clamp_nested_after(assert_pixels): + assert_pixels(''' + 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): assert_pixels(''' diff --git a/weasyprint/layout/__init__.py b/weasyprint/layout/__init__.py index 7f7dd5917..501e1e1d5 100644 --- a/weasyprint/layout/__init__.py +++ b/weasyprint/layout/__init__.py @@ -356,9 +356,9 @@ def _update_footnote_area(self): if self.current_footnote_area.children: footnote_area = build.create_anonymous_boxes( self.current_footnote_area.deepcopy()) - footnote_area, _, _, _, _ = block_level_layout( + footnote_area, _, _, _, _, _ = block_level_layout( self, footnote_area, -inf, None, - self.current_footnote_area.page, True, [], [], [], False) + self.current_footnote_area.page, True, [], [], [], False, None) self.current_footnote_area.height = footnote_area.height self.page_bottom -= footnote_area.margin_height() else: diff --git a/weasyprint/layout/absolute.py b/weasyprint/layout/absolute.py index 6ec567d26..bbee5463c 100644 --- a/weasyprint/layout/absolute.py +++ b/weasyprint/layout/absolute.py @@ -196,10 +196,10 @@ def absolute_block(context, box, containing_block, fixed_boxes, bottom_space, table_wrapper_width(context, box, (cb_width, cb_height)) if isinstance(box, (boxes.BlockBox)): - new_box, resume_at, _, _, _ = block_container_layout( + new_box, resume_at, _, _, _, _ = block_container_layout( context, box, bottom_space, skip_stack, page_is_empty=True, absolute_boxes=absolute_boxes, fixed_boxes=fixed_boxes, - adjoining_margins=None, discard=False) + adjoining_margins=None, discard=False, max_lines=None) else: new_box, resume_at, _, _, _ = flex_layout( context, box, bottom_space, skip_stack, containing_block, diff --git a/weasyprint/layout/block.py b/weasyprint/layout/block.py index 1966b421a..3fcd803a3 100644 --- a/weasyprint/layout/block.py +++ b/weasyprint/layout/block.py @@ -16,7 +16,7 @@ def block_level_layout(context, box, bottom_space, skip_stack, containing_block, page_is_empty, absolute_boxes, - fixed_boxes, adjoining_margins, discard): + fixed_boxes, adjoining_margins, discard, max_lines): """Lay out the block-level ``box``.""" if not isinstance(box, boxes.TableBox): resolve_percentages(box, containing_block) @@ -51,35 +51,38 @@ def block_level_layout(context, box, bottom_space, skip_stack, return block_level_layout_switch( context, box, bottom_space, skip_stack, containing_block, - page_is_empty, absolute_boxes, fixed_boxes, adjoining_margins, discard) + page_is_empty, absolute_boxes, fixed_boxes, adjoining_margins, discard, + max_lines) def block_level_layout_switch(context, box, bottom_space, skip_stack, containing_block, page_is_empty, absolute_boxes, - fixed_boxes, adjoining_margins, discard): + fixed_boxes, adjoining_margins, discard, + max_lines): """Call the layout function corresponding to the ``box`` type.""" if isinstance(box, boxes.TableBox): - return table_layout( + result = table_layout( context, box, bottom_space, skip_stack, containing_block, page_is_empty, absolute_boxes, fixed_boxes) elif isinstance(box, boxes.BlockBox): return block_box_layout( context, box, bottom_space, skip_stack, containing_block, page_is_empty, absolute_boxes, fixed_boxes, adjoining_margins, - discard) + discard, max_lines) elif isinstance(box, boxes.BlockReplacedBox): - return block_replaced_box_layout(context, box, containing_block) + result = block_replaced_box_layout(context, box, containing_block) elif isinstance(box, boxes.FlexBox): - return flex_layout( + result = flex_layout( context, box, bottom_space, skip_stack, containing_block, page_is_empty, absolute_boxes, fixed_boxes) else: # pragma: no cover raise TypeError(f'Layout for {type(box).__name__} not handled yet') + return result + (None,) def block_box_layout(context, box, bottom_space, skip_stack, containing_block, page_is_empty, absolute_boxes, - fixed_boxes, adjoining_margins, discard): + fixed_boxes, adjoining_margins, discard, max_lines): """Lay out the block ``box``.""" if (box.style['column_width'] != 'auto' or box.style['column_count'] != 'auto'): @@ -99,7 +102,7 @@ def block_box_layout(context, box, bottom_space, skip_stack, context, box, bottom_space, skip_stack, containing_block, page_is_empty, absolute_boxes, fixed_boxes, adjoining_margins) - return result + return result + (None,) elif box.is_table_wrapper: table_wrapper_width( context, box, (containing_block.width, containing_block.height)) @@ -107,7 +110,7 @@ def block_box_layout(context, box, bottom_space, skip_stack, result = block_container_layout( context, box, bottom_space, skip_stack, page_is_empty, - absolute_boxes, fixed_boxes, adjoining_margins, discard) + absolute_boxes, fixed_boxes, adjoining_margins, discard, max_lines) new_box = result[0] if new_box and new_box.is_table_wrapper: # Don't collide with floats @@ -290,7 +293,7 @@ def _break_line(context, box, line, new_children, lines_iterator, def _linebox_layout(context, box, index, child, new_children, page_is_empty, absolute_boxes, fixed_boxes, adjoining_margins, bottom_space, position_y, skip_stack, first_letter_style, - draw_bottom_decoration): + draw_bottom_decoration, max_lines): abort = stop = False resume_at = None new_footnotes = [] @@ -304,6 +307,14 @@ def _linebox_layout(context, box, index, child, new_children, page_is_empty, context, child, position_y, bottom_space, skip_stack, new_containing_block, absolute_boxes, fixed_boxes, first_letter_style) for i, (line, resume_at) in enumerate(lines_iterator): + # Break box if we reached max-lines + if max_lines is not None: + if max_lines == 0: + new_children[-1].block_ellipsis = box.style['block_ellipsis'] + break + max_lines -= 1 + + # Update line resume_at and position_y line.resume_at = resume_at new_position_y = line.position_y + line.height @@ -378,23 +389,17 @@ def _linebox_layout(context, box, index, child, new_children, page_is_empty, 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} - return abort, stop, resume_at, position_y, new_footnotes + return abort, stop, resume_at, position_y, new_footnotes, max_lines def _in_flow_layout(context, box, index, child, new_children, page_is_empty, absolute_boxes, fixed_boxes, adjoining_margins, bottom_space, position_y, skip_stack, first_letter_style, draw_bottom_decoration, collapsing_with_children, discard, - next_page): + next_page, max_lines): abort = stop = False last_in_flow_child = find_last_in_flow_child(new_children) @@ -409,7 +414,7 @@ def _in_flow_layout(context, box, index, child, new_children, page_is_empty, stop = True return ( abort, stop, resume_at, position_y, adjoining_margins, - next_page, new_children) + next_page, new_children, max_lines) else: page_break = 'auto' @@ -457,10 +462,10 @@ def _in_flow_layout(context, box, index, child, new_children, page_is_empty, if not getattr(child, 'first_letter_style', None): child.first_letter_style = first_letter_style (new_child, resume_at, next_page, next_adjoining_margins, - collapsing_through) = block_level_layout( + collapsing_through, max_lines) = block_level_layout( context, child, bottom_space, skip_stack, new_containing_block, page_is_empty_with_no_children, absolute_boxes, - fixed_boxes, adjoining_margins, discard) + fixed_boxes, adjoining_margins, discard, max_lines) if new_child is not None: # We need to do this after the child layout to have the @@ -494,10 +499,11 @@ def _in_flow_layout(context, box, index, child, new_children, page_is_empty, bottom_space += ( new_child.padding_bottom + new_child.border_bottom_width) (new_child, resume_at, next_page, next_adjoining_margins, - collapsing_through) = block_level_layout( + collapsing_through, max_lines) = block_level_layout( context, child, bottom_space, skip_stack, new_containing_block, page_is_empty_with_no_children, - absolute_boxes, fixed_boxes, adjoining_margins, discard) + absolute_boxes, fixed_boxes, adjoining_margins, discard, + max_lines) if new_child: position_y = ( new_child.border_box_y() + new_child.border_height()) @@ -524,7 +530,7 @@ def _in_flow_layout(context, box, index, child, new_children, page_is_empty, stop = True return ( abort, stop, resume_at, position_y, adjoining_margins, - next_page, new_children) + next_page, new_children, max_lines) else: # We did not find any page break opportunity if not page_is_empty: @@ -534,7 +540,7 @@ def _in_flow_layout(context, box, index, child, new_children, page_is_empty, abort = True return ( abort, stop, resume_at, position_y, adjoining_margins, - next_page, new_children) + next_page, new_children, max_lines) # else: # ignore this 'avoid' and break anyway. @@ -554,7 +560,7 @@ def _in_flow_layout(context, box, index, child, new_children, page_is_empty, abort = True return ( abort, stop, resume_at, position_y, adjoining_margins, next_page, - new_children) + new_children, max_lines) # index in its non-laid-out parent, not in future new parent # May be used in find_earlier_page_break() @@ -566,12 +572,12 @@ def _in_flow_layout(context, box, index, child, new_children, page_is_empty, return ( abort, stop, resume_at, position_y, adjoining_margins, next_page, - new_children) + new_children, max_lines) def block_container_layout(context, box, bottom_space, skip_stack, page_is_empty, absolute_boxes, fixed_boxes, - adjoining_margins, discard): + adjoining_margins, discard, max_lines): """Set the ``box`` height.""" # TODO: boxes.FlexBox is allowed here because flex_layout calls # block_container_layout, there's probably a better solution. @@ -622,6 +628,9 @@ def block_container_layout(context, box, bottom_space, skip_stack, last_in_flow_child = None + if box.style['max_lines'] != 'none': + max_lines = min(box.style['max_lines'], max_lines or inf) + if is_start: skip = 0 first_letter_style = getattr(box, 'first_letter_style', None) @@ -644,30 +653,49 @@ def block_container_layout(context, box, bottom_space, skip_stack, elif isinstance(child, boxes.LineBox): (abort, stop, resume_at, position_y, - new_footnotes) = _linebox_layout( + new_footnotes, max_lines) = _linebox_layout( context, box, index, child, new_children, page_is_empty, absolute_boxes, fixed_boxes, adjoining_margins, bottom_space, position_y, skip_stack, first_letter_style, - draw_bottom_decoration) + draw_bottom_decoration, max_lines) draw_bottom_decoration |= resume_at is None adjoining_margins = [] all_footnotes += new_footnotes else: (abort, stop, resume_at, position_y, adjoining_margins, - next_page, new_children) = _in_flow_layout( + next_page, new_children, new_max_lines) = _in_flow_layout( context, box, index, child, new_children, page_is_empty, absolute_boxes, fixed_boxes, adjoining_margins, bottom_space, position_y, skip_stack, first_letter_style, draw_bottom_decoration, collapsing_with_children, discard, - next_page) + next_page, max_lines) skip_stack = None + if None not in (new_max_lines, max_lines): + max_lines = new_max_lines + if max_lines <= 0: + stop = True + last_child = (child == box.children[-1]) + if not last_child: + children = new_children + while children: + last_child = children[-1] + if isinstance(last_child, boxes.LineBox): + last_child.block_ellipsis = ( + box.style['block_ellipsis']) + elif isinstance(last_child, boxes.ParentBox): + children = last_child.children + continue + break + if abort: page = child.page_values()[0] for footnote in new_footnotes: context.unlayout_footnote(footnote) - return None, None, {'break': 'any', 'page': page}, [], False + return ( + None, None, {'break': 'any', 'page': page}, [], False, + max_lines) elif stop: adjoining_margins = [] break @@ -685,7 +713,7 @@ def block_container_layout(context, box, bottom_space, skip_stack, for footnote in all_footnotes: context.unlayout_footnote(footnote) return ( - None, None, {'break': 'any', 'page': None}, [], False) + None, None, {'break': 'any', 'page': None}, [], False, max_lines) context.broken_out_of_flow.extend(broken_out_of_flow) @@ -774,7 +802,9 @@ def block_container_layout(context, box, bottom_space, skip_stack, if next_page['page'] is None: next_page['page'] = new_box.page_values()[1] - return new_box, resume_at, next_page, adjoining_margins, collapsing_through + return ( + new_box, resume_at, next_page, adjoining_margins, collapsing_through, + max_lines) def collapse_margin(adjoining_margins): diff --git a/weasyprint/layout/column.py b/weasyprint/layout/column.py index b83ebf3c6..ba985c9ff 100644 --- a/weasyprint/layout/column.py +++ b/weasyprint/layout/column.py @@ -132,11 +132,12 @@ def create_column_box(children): resolve_percentages(block, containing_block) block.position_x = box.content_box_x() block.position_y = current_position_y - new_child, resume_at, next_page, adjoining_margins, _ = ( + new_child, resume_at, next_page, adjoining_margins, _, _ = ( block_level_layout( context, block, original_bottom_space, skip_stack, containing_block, page_is_empty, absolute_boxes, - fixed_boxes, adjoining_margins, discard=False)) + fixed_boxes, adjoining_margins, discard=False, + max_lines=None)) skip_stack = None if new_child is None: forced_end_probing = True @@ -187,12 +188,12 @@ def create_column_box(children): consumed_heights = [] for i in range(count): # Render the column - new_box, resume_at, next_page, _, _ = block_box_layout( + new_box, resume_at, next_page, _, _, _ = block_box_layout( context, column_box, context.page_bottom - current_position_y - height, column_skip_stack, containing_block, page_is_empty or first_probe_run, [], [], [], - discard=False) + discard=False, max_lines=None) if new_box is None: # We didn't render anything, retry. column_skip_stack = {0: None} @@ -211,11 +212,11 @@ def create_column_box(children): empty_space = height - consumed_height # Get the minimum size needed to render the next box - next_box, _, _, _, _ = block_box_layout( + next_box, _, _, _, _, _ = block_box_layout( context, column_box, context.page_bottom - box.content_box_y(), column_skip_stack, containing_block, True, [], [], [], - discard=False) + discard=False, max_lines=None) for child in next_box.children: if child.is_in_normal_flow(): next_box_size = child.margin_height() @@ -300,11 +301,11 @@ def create_column_box(children): box.width - (i + 1) * width - i * style['column_gap']) else: column_box.position_x += i * (width + style['column_gap']) - new_child, column_skip_stack, column_next_page, _, _ = ( + new_child, column_skip_stack, column_next_page, _, _, _ = ( block_box_layout( context, column_box, bottom_space, skip_stack, containing_block, original_page_is_empty, absolute_boxes, - fixed_boxes, None, discard=False)) + fixed_boxes, None, discard=False, max_lines=None)) if new_child is None: break_page = True break diff --git a/weasyprint/layout/flex.py b/weasyprint/layout/flex.py index c4c6c2db4..465dcce12 100644 --- a/weasyprint/layout/flex.py +++ b/weasyprint/layout/flex.py @@ -147,7 +147,7 @@ def flex_layout(context, box, bottom_space, skip_stack, containing_block, new_child.style['max_height'] = Dimension(inf, 'px') new_child = block.block_level_layout( context, new_child, -inf, child_skip_stack, parent_box, - page_is_empty, [], [], [], False)[0] + page_is_empty, [], [], [], False, None)[0] content_size = new_child.height child.min_height = min(specified_size, content_size) @@ -207,7 +207,7 @@ def flex_layout(context, box, bottom_space, skip_stack, containing_block, new_child = block.block_level_layout( context, new_child, -inf, child_skip_stack, parent_box, page_is_empty, absolute_boxes, fixed_boxes, - adjoining_margins=[], discard=False)[0] + adjoining_margins=[], discard=False, max_lines=None)[0] child.flex_base_size = new_child.margin_height() elif child.style[axis] == 'min-content': child.style[axis] = 'auto' @@ -222,7 +222,7 @@ def flex_layout(context, box, bottom_space, skip_stack, containing_block, new_child = block.block_level_layout( context, new_child, -inf, child_skip_stack, parent_box, page_is_empty, absolute_boxes, fixed_boxes, - adjoining_margins=[], discard=False)[0] + adjoining_margins=[], discard=False, max_lines=None)[0] child.flex_base_size = new_child.margin_height() else: assert child.style[axis].unit == 'px' @@ -452,11 +452,11 @@ def flex_layout(context, box, bottom_space, skip_stack, containing_block, else: child_copy = child.copy() block.block_level_width(child_copy, parent_box) - new_child, _, _, adjoining_margins, _ = ( + new_child, _, _, adjoining_margins, _, _ = ( block.block_level_layout_switch( context, child_copy, -inf, child_skip_stack, parent_box, page_is_empty, absolute_boxes, fixed_boxes, - adjoining_margins=[], discard=False)) + [], False, None)) child._baseline = find_in_flow_baseline(new_child) or 0 if cross == 'height': @@ -834,7 +834,7 @@ def flex_layout(context, box, bottom_space, skip_stack, containing_block, new_child, child_resume_at = block.block_level_layout_switch( context, child, bottom_space, child_skip_stack, box, page_is_empty, absolute_boxes, fixed_boxes, - adjoining_margins=[], discard=False)[:2] + adjoining_margins=[], discard=False, max_lines=None)[:2] if new_child is None: if resume_at: index, = resume_at diff --git a/weasyprint/layout/float.py b/weasyprint/layout/float.py index 7787f4fca..e94417460 100644 --- a/weasyprint/layout/float.py +++ b/weasyprint/layout/float.py @@ -56,11 +56,11 @@ def float_layout(context, box, containing_block, absolute_boxes, fixed_boxes, if isinstance(box, boxes.BlockContainerBox): context.create_block_formatting_context() - box, resume_at, _, _, _ = block_container_layout( + box, resume_at, _, _, _, _ = block_container_layout( context, box, bottom_space=bottom_space, skip_stack=skip_stack, page_is_empty=True, absolute_boxes=absolute_boxes, fixed_boxes=fixed_boxes, - adjoining_margins=None, discard=False) + adjoining_margins=None, discard=False, max_lines=None) context.finish_block_formatting_context(box) elif isinstance(box, boxes.FlexContainerBox): box, resume_at, _, _, _ = flex_layout( diff --git a/weasyprint/layout/inline.py b/weasyprint/layout/inline.py index f88c928ca..4f8e62111 100644 --- a/weasyprint/layout/inline.py +++ b/weasyprint/layout/inline.py @@ -397,10 +397,11 @@ def inline_block_box_layout(context, box, position_x, skip_stack, box.position_x = position_x box.position_y = 0 - box, _, _, _, _ = block_container_layout( + box, _, _, _, _, _ = block_container_layout( context, box, bottom_space=-inf, skip_stack=skip_stack, page_is_empty=True, absolute_boxes=absolute_boxes, - fixed_boxes=fixed_boxes, adjoining_margins=None, discard=False) + fixed_boxes=fixed_boxes, adjoining_margins=None, discard=False, + max_lines=None) box.baseline = inline_block_baseline(box) return box diff --git a/weasyprint/layout/page.py b/weasyprint/layout/page.py index a84424c4a..45b2fb01c 100644 --- a/weasyprint/layout/page.py +++ b/weasyprint/layout/page.py @@ -429,10 +429,10 @@ def make_box(at_keyword, containing_block): def margin_box_content_layout(context, page, box): """Layout a margin box’s content once the box has dimensions.""" positioned_boxes = [] - box, resume_at, next_page, _, _ = block_container_layout( + box, resume_at, next_page, _, _, _ = block_container_layout( context, box, bottom_space=-inf, skip_stack=None, page_is_empty=True, absolute_boxes=positioned_boxes, fixed_boxes=positioned_boxes, - adjoining_margins=None, discard=False) + adjoining_margins=None, discard=False, max_lines=None) assert resume_at is None for absolute_box in positioned_boxes: absolute_layout( @@ -555,9 +555,9 @@ def make_page(context, root_box, page_type, resume_at, page_number, context.reported_footnotes = [] reported_footnote_area = build.create_anonymous_boxes( footnote_area.deepcopy()) - reported_footnote_area, _, _, _, _ = block_level_layout( + reported_footnote_area, _, _, _, _, _ = block_level_layout( context, reported_footnote_area, -inf, None, footnote_area.page, - True, [], [], [], False) + True, [], [], [], False, None) footnote_area.height = reported_footnote_area.height context.page_bottom -= reported_footnote_area.margin_height() @@ -582,17 +582,17 @@ def make_page(context, root_box, page_type, resume_at, page_number, broken_out_of_flow.append( (box, containing_block, out_of_flow_resume_at)) context.broken_out_of_flow = broken_out_of_flow - root_box, resume_at, next_page, _, _ = block_level_layout( + root_box, resume_at, next_page, _, _, _ = block_level_layout( context, root_box, 0, resume_at, initial_containing_block, page_is_empty, positioned_boxes, positioned_boxes, adjoining_margins, - discard=False) + False, None) assert root_box root_box.children = out_of_flow_boxes + root_box.children footnote_area = build.create_anonymous_boxes(footnote_area.deepcopy()) - footnote_area, _, _, _, _ = block_level_layout( + footnote_area, _, _, _, _, _ = block_level_layout( context, footnote_area, -inf, None, footnote_area.page, True, - positioned_boxes, positioned_boxes, [], False) + positioned_boxes, positioned_boxes, [], False, None) footnote_area.translate(dy=-footnote_area.margin_height()) page.fixed_boxes = [ diff --git a/weasyprint/layout/table.py b/weasyprint/layout/table.py index e62f497bd..086242577 100644 --- a/weasyprint/layout/table.py +++ b/weasyprint/layout/table.py @@ -159,18 +159,18 @@ def group_layout(group, position_y, bottom_space, page_is_empty, # force to render something if the page is actually empty, or # just draw an empty cell otherwise. See # test_table_break_children_margin. - new_cell, cell_resume_at, _, _, _ = block_container_layout( + new_cell, cell_resume_at, _, _, _, _ = block_container_layout( context, cell, bottom_space, cell_skip_stack, page_is_empty=page_is_empty, absolute_boxes=absolute_boxes, fixed_boxes=fixed_boxes, adjoining_margins=None, - discard=False) + discard=False, max_lines=None) if new_cell is None: cell = cell.copy_with_children([]) - cell, _, _, _, _ = block_container_layout( + cell, _, _, _, _, _ = block_container_layout( context, cell, bottom_space, cell_skip_stack, page_is_empty=True, absolute_boxes=[], fixed_boxes=[], adjoining_margins=None, - discard=False) + discard=False, max_lines=None) cell_resume_at = {0: None} else: cell = new_cell