Skip to content

Commit

Permalink
Don't always cut lines between tags
Browse files Browse the repository at this point in the history
Fix #301.
  • Loading branch information
liZe committed Oct 23, 2017
1 parent 1dac7d7 commit d44c738
Showing 1 changed file with 95 additions and 21 deletions.
116 changes: 95 additions & 21 deletions weasyprint/layout/inlines.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@

import unicodedata

import uniseg.linebreak

from ..css.computed_values import ex_ratio, strut_layout
from ..formatting_structure import boxes
from ..text import split_first_line
Expand Down Expand Up @@ -101,7 +103,8 @@ def get_next_linebox(context, linebox, position_y, skip_stack,
line_fixed = []
waiting_floats = []

line, resume_at, preserved_line_break = split_inline_box(
(line, resume_at, preserved_line_break, first_letter,
last_letter) = split_inline_box(
context, linebox, position_x, max_x, skip_stack,
containing_block, device_size, line_absolutes,
line_fixed, line_placeholders, waiting_floats)
Expand Down Expand Up @@ -546,9 +549,10 @@ def split_inline_level(context, box, position_x, max_x, skip_stack,
fixed_boxes, line_placeholders, waiting_floats):
"""Fit as much content as possible from an inline-level box in a width.
Return ``(new_box, resume_at)``. ``resume_at`` is ``None`` if all of the
content fits. Otherwise it can be passed as a ``skip_stack`` parameter
to resume where we left off.
Return ``(new_box, resume_at, preserved_line_break, first_letter,
last_letter)``. ``resume_at`` is ``None`` if all of the content
fits. Otherwise it can be passed as a ``skip_stack`` parameter to resume
where we left off.
``new_box`` is non-empty (unless the box is empty) and as big as possible
while being narrower than ``available_width``, if possible (may overflow
Expand All @@ -572,12 +576,18 @@ def split_inline_level(context, box, position_x, max_x, skip_stack,
resume_at = None
else:
resume_at = (skip, None)
if new_box and new_box.text:
first_letter = new_box.text[0]
last_letter = new_box.text[-1]
else:
first_letter = last_letter = None
elif isinstance(box, boxes.InlineBox):
if box.margin_left == 'auto':
box.margin_left = 0
if box.margin_right == 'auto':
box.margin_right = 0
new_box, resume_at, preserved_line_break = split_inline_box(
(new_box, resume_at, preserved_line_break, first_letter,
last_letter) = split_inline_box(
context, box, position_x, max_x, skip_stack, containing_block,
device_size, absolute_boxes, fixed_boxes, line_placeholders,
waiting_floats)
Expand All @@ -588,8 +598,12 @@ def split_inline_level(context, box, position_x, max_x, skip_stack,
new_box.position_x = position_x
resume_at = None
preserved_line_break = False
# See https://www.w3.org/TR/css-text-3/#line-breaking
# Atomic inlines behave like ideographic characters.
first_letter = '\u2e80'
last_letter = '\u2e80'
# else: unexpected box type here
return new_box, resume_at, preserved_line_break
return new_box, resume_at, preserved_line_break, first_letter, last_letter


def split_inline_box(context, box, position_x, max_x, skip_stack,
Expand All @@ -607,6 +621,7 @@ def split_inline_box(context, box, position_x, max_x, skip_stack,

is_start = skip_stack is None
initial_position_x = position_x
initial_skip_stack = skip_stack
assert isinstance(box, (boxes.LineBox, boxes.InlineBox))
left_spacing = (box.padding_left + box.margin_left +
box.border_left_width)
Expand All @@ -617,7 +632,9 @@ def split_inline_box(context, box, position_x, max_x, skip_stack,
content_box_left = position_x

children = []
waiting_children = []
preserved_line_break = False
first_letter = last_letter = None

if box.style.position == 'relative':
absolute_boxes = []
Expand All @@ -633,7 +650,7 @@ def split_inline_box(context, box, position_x, max_x, skip_stack,
child.position_x = position_x
placeholder = AbsolutePlaceholder(child)
line_placeholders.append(placeholder)
children.append(placeholder)
waiting_children.append((index, placeholder))
if child.style.position == 'absolute':
absolute_boxes.append(placeholder)
else:
Expand All @@ -647,7 +664,7 @@ def split_inline_box(context, box, position_x, max_x, skip_stack,
# To retrieve the real available space for floats, we must remove
# the trailing whitespaces from the line
non_floating_children = [
child_ for child_ in children if not child_.is_floated()]
child_ for _, child_ in children if not child_.is_floated()]
if non_floating_children:
float_width -= trailing_whitespace_size(
context, non_floating_children[-1])
Expand All @@ -660,9 +677,9 @@ def split_inline_box(context, box, position_x, max_x, skip_stack,
child = float_layout(
context, child, containing_block, device_size,
absolute_boxes, fixed_boxes)
children.append(child)
waiting_children.append((index, child))
# TODO: use the main text direction of the line
for old_child in children[:index]:
for _, old_child in children[:index]:
if not old_child.is_in_normal_flow():
continue
if child.style.float == 'left': # and direction is ltr
Expand All @@ -675,14 +692,28 @@ def split_inline_box(context, box, position_x, max_x, skip_stack,
max_x -= max(child.margin_width(), 0)
continue

new_child, resume_at, preserved = split_inline_level(
new_child, resume_at, preserved, first, last = split_inline_level(
context, child, position_x, max_x, skip_stack, containing_block,
device_size, absolute_boxes, fixed_boxes, line_placeholders,
waiting_floats)
skip_stack = None
if preserved:
preserved_line_break = True

if None in (last_letter, first):
can_break = True
else:
can_break = bool(list(uniseg.linebreak.line_break_breakables(
last_letter + first))[1])

if can_break:
children.extend(waiting_children)
waiting_children = []

if first_letter is None:
first_letter = first
last_letter = last

# TODO: this is non-optimal when last_child is True and
# width <= remaining_width < width + right_spacing
# with
Expand All @@ -697,23 +728,54 @@ def split_inline_box(context, box, position_x, max_x, skip_stack,
margin_width = new_child.margin_width()
new_position_x = position_x + margin_width

if (new_position_x > max_x and children):
# too wide, and the inline is non-empty:
# put child entirely on the next line.
resume_at = (index, None)
break
else:
position_x = new_position_x
children.append(new_child)
if new_position_x > max_x:
if children:
# too wide, and the inline is non-empty:
# put child entirely on the next line.
resume_at = (children[-1][0] + 1, None)
break
elif waiting_children:
# too wide, the inline is empty, we tried to add children
# but can't split the line between them: split the last
# child that can be split inside.
# TODO: we should take care of children added into
# absolute_boxes, fixed_boxes and other lists.
for index, child in reversed(waiting_children):
# TODO: what about relative children?
if (child.is_in_normal_flow() and
can_break_inside(child)):
# TODO: replace -1, we use it to cut the last word
# of the line.
answer = split_inline_box(
context, box, initial_position_x,
child.position_x + child.margin_width() - 1,
initial_skip_stack, containing_block,
device_size, absolute_boxes, fixed_boxes,
line_placeholders, waiting_floats)
children = (
waiting_children[:index] +
[(index, answer[0])])
resume_at = answer[1]
break
else:
children = [waiting_children[0]]
resume_at = (waiting_children[0][0] + 1, None)
break

position_x = new_position_x
waiting_children.append((index, new_child))

if resume_at is not None:
children.extend(waiting_children)
resume_at = (index, resume_at)
break
else:
children.extend(waiting_children)
resume_at = None

new_box = box.copy_with_children(
children, is_start=is_start, is_end=resume_at is None)
[child for index, child in children],
is_start=is_start, is_end=resume_at is None)
if isinstance(box, boxes.LineBox):
# Line boxes already have a position_x which may not be the same
# as content_box_left when text-indent is non-zero.
Expand All @@ -736,7 +798,7 @@ def split_inline_box(context, box, position_x, max_x, skip_stack,
if new_box.style.position == 'relative':
for absolute_box in absolute_boxes:
absolute_layout(context, absolute_box, new_box, fixed_boxes)
return new_box, resume_at, preserved_line_break
return new_box, resume_at, preserved_line_break, first_letter, last_letter


def split_text_box(context, box, available_width, skip):
Expand Down Expand Up @@ -1050,3 +1112,15 @@ def is_phantom_linebox(linebox):
elif child.is_in_normal_flow():
return False
return True


def can_break_inside(box):
if isinstance(box, boxes.AtomicInlineLevelBox):
return False
elif isinstance(box, boxes.TextBox):
if box.text:
return any(uniseg.linebreak.line_break_breakables(box.text[1:]))
elif isinstance(box, boxes.ParentBox):
return any(can_break_inside(child) for child in box.children)
else:
return False

0 comments on commit d44c738

Please sign in to comment.