1
1
mirror of https://github.com/Kozea/WeasyPrint.git synced 2024-10-05 16:37:47 +03:00
WeasyPrint/weasyprint/layout/inlines.py

1317 lines
52 KiB
Python
Raw Normal View History

"""
weasyprint.layout.inline
------------------------
Line breaking and layout for inline-level boxes.
2019-03-04 13:04:06 +03:00
:copyright: Copyright 2011-2019 Simon Sapin and contributors, see AUTHORS.
:license: BSD, see LICENSE for details.
2011-08-24 12:48:42 +04:00
"""
import unicodedata
2019-03-10 21:34:16 +03:00
from ..css import computed_from_cascaded
from ..css.computed_values import ex_ratio, strut_layout
from ..formatting_structure import boxes
from ..text import can_break_text, create_layout, split_first_line
2017-03-25 02:33:36 +03:00
from .absolute import AbsolutePlaceholder, absolute_layout
2018-02-24 06:41:11 +03:00
from .flex import flex_layout
from .float import avoid_collisions, float_layout
2017-03-25 02:33:36 +03:00
from .min_max import handle_min_max_height, handle_min_max_width
from .percentages import resolve_one_percentage, resolve_percentages
from .preferred import (
inline_min_content_width, shrink_to_fit, trailing_whitespace_size)
from .replaced import image_marker_layout
2012-04-12 14:51:21 +04:00
from .tables import find_in_flow_baseline, table_wrapper_width
2012-07-12 19:13:21 +04:00
def iter_line_boxes(context, box, position_y, skip_stack, containing_block,
device_size, absolute_boxes, fixed_boxes,
first_letter_style):
2012-03-14 22:33:24 +04:00
"""Return an iterator of ``(line, resume_at)``.
2011-10-17 18:34:33 +04:00
``line`` is a laid-out LineBox with as much content as possible that
fits in the available width.
2018-08-29 15:00:02 +03:00
:param box: a non-laid-out :class:`LineBox`
2011-10-17 18:34:33 +04:00
:param position_y: vertical top position of the line box on the page
:param skip_stack: ``None`` to start at the beginning of ``linebox``,
or a ``resume_at`` value to continue just after an
already laid-out line.
:param containing_block: Containing block of the line box:
a :class:`BlockContainerBox`
:param device_size: ``(width, height)`` of the current page.
"""
resolve_percentages(box, containing_block)
if skip_stack is None:
# TODO: wrong, see https://github.com/Kozea/WeasyPrint/issues/679
resolve_one_percentage(box, 'text_indent', containing_block.width)
else:
box.text_indent = 0
2012-03-14 22:33:24 +04:00
while 1:
2012-06-28 16:00:27 +04:00
line, resume_at = get_next_linebox(
2012-07-12 19:13:21 +04:00
context, box, position_y, skip_stack, containing_block,
device_size, absolute_boxes, fixed_boxes, first_letter_style)
if line:
position_y = line.position_y + line.height
2012-03-14 22:33:24 +04:00
if line is None:
return
yield line, resume_at
if resume_at is None:
return
skip_stack = resume_at
box.text_indent = 0
first_letter_style = None
2012-03-14 22:33:24 +04:00
2012-07-12 19:13:21 +04:00
def get_next_linebox(context, linebox, position_y, skip_stack,
containing_block, device_size, absolute_boxes,
fixed_boxes, first_letter_style):
2012-03-14 22:33:24 +04:00
"""Return ``(line, resume_at)``."""
skip_stack = skip_first_whitespace(linebox, skip_stack)
if skip_stack == 'continue':
2012-06-28 16:00:27 +04:00
return None, None
skip_stack = first_letter_to_box(linebox, skip_stack, first_letter_style)
linebox.position_y = position_y
if context.excluded_shapes:
# Width and height must be calculated to avoid floats
linebox.width = inline_min_content_width(
context, linebox, skip_stack=skip_stack, first_line=True)
linebox.height, _ = strut_layout(linebox.style, context)
else:
# No float, width and height will be set by the lines
linebox.width = linebox.height = 0
position_x, position_y, available_width = avoid_collisions(
2012-07-12 19:13:21 +04:00
context, linebox, containing_block, outer=False)
candidate_height = linebox.height
2012-06-21 20:12:17 +04:00
2012-07-12 19:13:21 +04:00
excluded_shapes = context.excluded_shapes[:]
2012-06-21 20:12:17 +04:00
while 1:
linebox.position_x = position_x
linebox.position_y = position_y
max_x = position_x + available_width
position_x += linebox.text_indent
line_placeholders = []
line_absolutes = []
line_fixed = []
2012-06-28 16:00:27 +04:00
waiting_floats = []
(line, resume_at, preserved_line_break, first_letter,
2018-08-29 14:26:08 +03:00
last_letter, float_width) = split_inline_box(
context, linebox, position_x, max_x, skip_stack, containing_block,
device_size, line_absolutes, line_fixed, line_placeholders,
waiting_floats, line_children=[])
linebox.width, linebox.height = line.width, line.height
2013-04-12 20:51:43 +04:00
if is_phantom_linebox(line) and not preserved_line_break:
line.height = 0
break
2013-04-12 20:51:43 +04:00
remove_last_whitespace(context, line)
new_position_x, _, new_available_width = avoid_collisions(
context, linebox, containing_block, outer=False)
2018-08-29 14:26:08 +03:00
# TODO: handle rtl
new_available_width -= float_width['right']
alignment_available_width = (
new_available_width + new_position_x - linebox.position_x)
offset_x = text_align(
context, line, alignment_available_width,
last=(resume_at is None or preserved_line_break))
bottom, top = line_box_verticality(line)
assert top is not None
assert bottom is not None
line.baseline = -top
line.position_y = top
line.height = bottom - top
offset_y = position_y - top
line.margin_top = 0
line.margin_bottom = 0
line.translate(offset_x, offset_y)
# Avoid floating point errors, as position_y - top + top != position_y
# Removing this line breaks the position == linebox.position test below
# See https://github.com/Kozea/WeasyPrint/issues/583
line.position_y = position_y
if line.height <= candidate_height:
break
candidate_height = line.height
2012-07-12 19:13:21 +04:00
new_excluded_shapes = context.excluded_shapes
context.excluded_shapes = excluded_shapes
position_x, position_y, available_width = avoid_collisions(
2012-07-12 19:13:21 +04:00
context, line, containing_block, outer=False)
if (position_x, position_y) == (
2013-04-11 14:08:53 +04:00
linebox.position_x, linebox.position_y):
2012-07-12 19:13:21 +04:00
context.excluded_shapes = new_excluded_shapes
break
absolute_boxes.extend(line_absolutes)
fixed_boxes.extend(line_fixed)
2012-05-10 22:12:47 +04:00
for placeholder in line_placeholders:
if placeholder.style['_weasy_specified_display'].startswith('inline'):
# Inline-level static position:
2012-05-29 20:50:36 +04:00
placeholder.translate(0, position_y - placeholder.position_y)
else:
# Block-level static position: at the start of the next line
2012-05-29 20:50:36 +04:00
placeholder.translate(
line.position_x - placeholder.position_x,
position_y + line.height - placeholder.position_y)
2012-05-10 22:12:47 +04:00
2012-06-28 16:00:27 +04:00
float_children = []
waiting_floats_y = line.position_y + line.height
for waiting_float in waiting_floats:
waiting_float.position_y = waiting_floats_y
waiting_float = float_layout(
context, waiting_float, containing_block, device_size,
absolute_boxes, fixed_boxes)
2012-06-28 16:00:27 +04:00
float_children.append(waiting_float)
if float_children:
2016-11-01 06:31:15 +03:00
line.children += tuple(float_children)
2012-06-28 16:00:27 +04:00
return line, resume_at
def skip_first_whitespace(box, skip_stack):
"""Return the ``skip_stack`` to start just after the remove spaces
at the beginning of the line.
See http://www.w3.org/TR/CSS21/text.html#white-space-model
"""
if skip_stack is None:
index = 0
next_skip_stack = None
else:
index, next_skip_stack = skip_stack
if isinstance(box, boxes.TextBox):
assert next_skip_stack is None
white_space = box.style['white_space']
2011-11-08 16:09:19 +04:00
length = len(box.text)
2013-04-12 20:51:43 +04:00
if index == length:
# Starting a the end of the TextBox, no text to see: Continue
return 'continue'
2013-04-12 22:09:41 +04:00
if white_space in ('normal', 'nowrap', 'pre-line'):
while index < length and box.text[index] == ' ':
index += 1
2012-06-15 19:59:15 +04:00
return (index, None) if index else None
if isinstance(box, (boxes.LineBox, boxes.InlineBox)):
2011-10-11 14:09:37 +04:00
if index == 0 and not box.children:
return None
result = skip_first_whitespace(box.children[index], next_skip_stack)
if result == 'continue':
index += 1
if index >= len(box.children):
return 'continue'
result = skip_first_whitespace(box.children[index], None)
2012-06-15 19:59:15 +04:00
return (index, result) if (index or result) else None
assert skip_stack is None, 'unexpected skip inside %s' % box
return None
2012-07-12 19:13:21 +04:00
def remove_last_whitespace(context, box):
"""Remove in place space characters at the end of a line.
This also reduces the width of the inline parents of the modified text.
"""
ancestors = []
while isinstance(box, (boxes.LineBox, boxes.InlineBox)):
ancestors.append(box)
if not box.children:
return
box = box.children[-1]
if not (isinstance(box, boxes.TextBox) and
box.style['white_space'] in ('normal', 'nowrap', 'pre-line')):
return
2011-11-08 16:09:19 +04:00
new_text = box.text.rstrip(' ')
if new_text:
2011-11-08 16:09:19 +04:00
if len(new_text) == len(box.text):
return
2012-12-28 20:45:23 +04:00
box.text = new_text
new_box, resume, _ = split_text_box(context, box, None, 0)
assert new_box is not None
assert resume is None
space_width = box.width - new_box.width
box.width = new_box.width
else:
space_width = box.width
box.width = 0
2012-12-28 20:45:23 +04:00
box.text = ''
for ancestor in ancestors:
ancestor.width -= space_width
# TODO: All tabs (U+0009) are rendered as a horizontal shift that
# lines up the start edge of the next glyph with the next tab stop.
# Tab stops occur at points that are multiples of 8 times the width
# of a space (U+0020) rendered in the block's font from the block's
# starting content edge.
# TODO: If spaces (U+0020) or tabs (U+0009) at the end of a line have
# 'white-space' set to 'pre-wrap', UAs may visually collapse them.
2011-09-30 19:24:44 +04:00
def first_letter_to_box(box, skip_stack, first_letter_style):
"""Create a box for the ::first-letter selector."""
if first_letter_style and box.children:
first_letter = ''
child = box.children[0]
if isinstance(child, boxes.TextBox):
2018-01-13 19:41:08 +03:00
letter_style = computed_from_cascaded(
cascaded={}, parent_style=first_letter_style, element=None)
if child.element_tag.endswith('::first-letter'):
letter_box = boxes.InlineBox(
2018-01-13 19:41:08 +03:00
'%s::first-letter' % box.element_tag, letter_style,
[child])
box.children = (
(letter_box,) + tuple(box.children[1:]))
elif child.text:
character_found = False
if skip_stack:
child_skip_stack = skip_stack[1]
if child_skip_stack:
index = child_skip_stack[0]
child.text = child.text[index:]
skip_stack = None
while child.text:
next_letter = child.text[0]
category = unicodedata.category(next_letter)
if category not in ('Ps', 'Pe', 'Pi', 'Pf', 'Po'):
if character_found:
break
character_found = True
first_letter += next_letter
child.text = child.text[1:]
if first_letter.lstrip('\n'):
# "This type of initial letter is similar to an
# inline-level element if its 'float' property is 'none',
# otherwise it is similar to a floated element."
if first_letter_style['float'] == 'none':
letter_box = boxes.InlineBox(
'%s::first-letter' % box.element_tag,
2017-07-01 01:28:14 +03:00
first_letter_style, [])
text_box = boxes.TextBox(
2018-01-13 19:41:08 +03:00
'%s::first-letter' % box.element_tag, letter_style,
first_letter)
letter_box.children = (text_box,)
box.children = (letter_box,) + tuple(box.children)
else:
letter_box = boxes.BlockBox(
'%s::first-letter' % box.element_tag,
2017-07-01 01:28:14 +03:00
first_letter_style, [])
letter_box.first_letter_style = None
line_box = boxes.LineBox(
2018-01-13 19:41:08 +03:00
'%s::first-letter' % box.element_tag, letter_style,
[])
letter_box.children = (line_box,)
text_box = boxes.TextBox(
2018-01-13 19:41:08 +03:00
'%s::first-letter' % box.element_tag, letter_style,
first_letter)
line_box.children = (text_box,)
box.children = (letter_box,) + tuple(box.children)
if skip_stack and child_skip_stack:
skip_stack = (skip_stack[0], (
child_skip_stack[0] + 1, child_skip_stack[1]))
elif isinstance(child, boxes.ParentBox):
if skip_stack:
child_skip_stack = skip_stack[1]
else:
child_skip_stack = None
child_skip_stack = first_letter_to_box(
child, child_skip_stack, first_letter_style)
if skip_stack:
skip_stack = (skip_stack[0], child_skip_stack)
return skip_stack
@handle_min_max_width
2011-10-06 17:36:19 +04:00
def replaced_box_width(box, device_size):
2011-08-26 18:16:40 +04:00
"""
Compute and set the used width for replaced boxes (inline- or block-level)
"""
intrinsic_width, intrinsic_height = box.replacement.get_intrinsic_size(
box.style['image_resolution'], box.style['font_size'])
# This algorithm simply follows the different points of the specification:
# http://www.w3.org/TR/CSS21/visudet.html#inline-replaced-width
if box.height == 'auto' and box.width == 'auto':
if intrinsic_width is not None:
# Point #1
box.width = intrinsic_width
elif box.replacement.intrinsic_ratio is not None:
if intrinsic_height is not None:
# Point #2 first part
box.width = intrinsic_height * box.replacement.intrinsic_ratio
else:
# Point #3
# " It is suggested that, if the containing block's width does
# not itself depend on the replaced element's width, then the
# used value of 'width' is calculated from the constraint
# equation used for block-level, non-replaced elements in
# normal flow. "
# Whaaaaat? Let's not do this and use a value that may work
# well at least with inline blocks.
box.width = (
box.style['font_size'] * box.replacement.intrinsic_ratio)
if box.width == 'auto':
if box.replacement.intrinsic_ratio is not None:
# Point #2 second part
box.width = box.height * box.replacement.intrinsic_ratio
elif intrinsic_width is not None:
# Point #4
box.width = intrinsic_width
else:
# Point #5
device_width, _device_height = device_size
box.width = min(300, device_width)
2011-08-26 18:16:40 +04:00
@handle_min_max_height
2011-10-06 17:36:19 +04:00
def replaced_box_height(box, device_size):
2011-08-26 18:16:40 +04:00
"""
Compute and set the used height for replaced boxes (inline- or block-level)
"""
# http://www.w3.org/TR/CSS21/visudet.html#inline-replaced-height
intrinsic_width, intrinsic_height = box.replacement.get_intrinsic_size(
box.style['image_resolution'], box.style['font_size'])
intrinsic_ratio = box.replacement.intrinsic_ratio
# Test 'auto' on the computed width, not the used width
if box.height == 'auto' and box.width == 'auto':
box.height = intrinsic_height
elif box.height == 'auto' and intrinsic_ratio:
box.height = box.width / intrinsic_ratio
if (box.height == 'auto' and box.width == 'auto' and
intrinsic_height is not None):
box.height = intrinsic_height
elif intrinsic_ratio is not None and box.height == 'auto':
box.height = box.width / intrinsic_ratio
elif box.height == 'auto' and intrinsic_height is not None:
box.height = intrinsic_height
elif box.height == 'auto':
device_width, _device_height = device_size
box.height = min(150, device_width / 2)
2011-08-26 18:16:40 +04:00
2012-05-25 16:08:35 +04:00
def inline_replaced_box_layout(box, device_size):
"""Lay out an inline :class:`boxes.ReplacedBox` ``box``."""
for side in ['top', 'right', 'bottom', 'left']:
if getattr(box, 'margin_' + side) == 'auto':
setattr(box, 'margin_' + side, 0)
2012-05-25 16:08:35 +04:00
inline_replaced_box_width_height(box, device_size)
2012-05-25 16:08:35 +04:00
def inline_replaced_box_width_height(box, device_size):
if box.style['width'] == 'auto' and box.style['height'] == 'auto':
replaced_box_width.without_min_max(box, device_size)
replaced_box_height.without_min_max(box, device_size)
min_max_auto_replaced(box)
else:
replaced_box_width(box, device_size)
replaced_box_height(box, device_size)
def min_max_auto_replaced(box):
"""Resolve {min,max}-{width,height} constraints on replaced elements
that have 'auto' width and heights.
"""
width = box.width
height = box.height
min_width = box.min_width
min_height = box.min_height
max_width = max(min_width, box.max_width)
max_height = max(min_height, box.max_height)
# (violation_width, violation_height)
violations = (
'min' if width < min_width else 'max' if width > max_width else '',
'min' if height < min_height else 'max' if height > max_height else '')
# Work around divisions by zero. These are pathological cases anyway.
# TODO: is there a cleaner way?
if width == 0:
width = 1e-6
if height == 0:
height = 1e-6
# ('', ''): nothing to do
if violations == ('max', ''):
box.width = max_width
box.height = max(max_width * height / width, min_height)
elif violations == ('min', ''):
box.width = min_width
box.height = min(min_width * height / width, max_height)
elif violations == ('', 'max'):
box.width = max(max_height * width / height, min_width)
box.height = max_height
elif violations == ('', 'min'):
box.width = min(min_height * width / height, max_width)
box.height = min_height
elif violations == ('max', 'max'):
if max_width / width <= max_height / height:
box.width = max_width
box.height = max(min_height, max_width * height / width)
else:
box.width = max(min_width, max_height * width / height)
box.height = max_height
elif violations == ('min', 'min'):
if min_width / width <= min_height / height:
box.width = min(max_width, min_height * width / height)
box.height = min_height
else:
box.width = min_width
box.height = min(max_height, min_width * height / width)
elif violations == ('min', 'max'):
box.width = min_width
box.height = max_height
elif violations == ('max', 'min'):
box.width = max_width
box.height = min_height
2012-07-12 19:13:21 +04:00
def atomic_box(context, box, position_x, skip_stack, containing_block,
device_size, absolute_boxes, fixed_boxes):
2011-08-24 12:48:42 +04:00
"""Compute the width and the height of the atomic ``box``."""
if isinstance(box, boxes.ReplacedBox):
box = box.copy()
if getattr(box, 'is_list_marker', False):
image_marker_layout(box)
else:
2012-05-25 16:08:35 +04:00
inline_replaced_box_layout(box, device_size)
box.baseline = box.margin_height()
2012-03-22 21:36:56 +04:00
elif isinstance(box, boxes.InlineBlockBox):
2012-04-12 14:51:21 +04:00
if box.is_table_wrapper:
table_wrapper_width(
2012-07-12 19:13:21 +04:00
context, box,
(containing_block.width, containing_block.height))
box = inline_block_box_layout(
2012-07-12 19:13:21 +04:00
context, box, position_x, skip_stack, containing_block,
device_size, absolute_boxes, fixed_boxes)
2012-04-02 16:45:44 +04:00
else: # pragma: no cover
raise TypeError('Layout for %s not handled yet' % type(box).__name__)
2011-10-19 17:14:43 +04:00
return box
2012-07-12 19:13:21 +04:00
def inline_block_box_layout(context, box, position_x, skip_stack,
containing_block, device_size, absolute_boxes,
fixed_boxes):
# Avoid a circular import
2012-04-10 13:10:16 +04:00
from .blocks import block_container_layout
2012-03-22 21:36:56 +04:00
resolve_percentages(box, containing_block)
# http://www.w3.org/TR/CSS21/visudet.html#inlineblock-width
if box.margin_left == 'auto':
box.margin_left = 0
if box.margin_right == 'auto':
box.margin_right = 0
# http://www.w3.org/TR/CSS21/visudet.html#block-root-margin
if box.margin_top == 'auto':
box.margin_top = 0
if box.margin_bottom == 'auto':
box.margin_bottom = 0
2012-07-12 19:13:21 +04:00
inline_block_width(box, context, containing_block)
box.position_x = position_x
box.position_y = 0
2012-04-10 13:10:16 +04:00
box, _, _, _, _ = block_container_layout(
2012-07-12 19:13:21 +04:00
context, box, max_position_y=float('inf'), skip_stack=skip_stack,
2012-05-09 21:01:32 +04:00
device_size=device_size, page_is_empty=True,
absolute_boxes=absolute_boxes, fixed_boxes=fixed_boxes)
box.baseline = inline_block_baseline(box)
2012-03-22 21:36:56 +04:00
return box
def inline_block_baseline(box):
"""
Return the y position of the baseline for an inline block
from the top of its margin box.
http://www.w3.org/TR/CSS21/visudet.html#propdef-vertical-align
"""
if box.is_table_wrapper:
# Inline table's baseline is its first row's baseline
for child in box.children:
if isinstance(child, boxes.TableBox):
if child.children and child.children[0].children:
first_row = child.children[0].children[0]
return first_row.baseline
elif box.style['overflow'] == 'visible':
result = find_in_flow_baseline(box, last=True)
if result:
return result
return box.position_y + box.margin_height()
@handle_min_max_width
2012-07-12 19:13:21 +04:00
def inline_block_width(box, context, containing_block):
if box.width == 'auto':
2012-07-12 19:13:21 +04:00
box.width = shrink_to_fit(context, box, containing_block.width)
2012-07-12 19:13:21 +04:00
def split_inline_level(context, box, position_x, max_x, skip_stack,
containing_block, device_size, absolute_boxes,
fixed_boxes, line_placeholders, waiting_floats,
line_children):
"""Fit as much content as possible from an inline-level box in a width.
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
is no split is possible.)
"""
resolve_percentages(box, containing_block)
2018-08-29 14:26:08 +03:00
float_widths = {'left': 0, 'right': 0}
if isinstance(box, boxes.TextBox):
box.position_x = position_x
if skip_stack is None:
skip = 0
else:
skip, skip_stack = skip_stack
skip = skip or 0
assert skip_stack is None
new_box, skip, preserved_line_break = split_text_box(
context, box, max_x - position_x, skip)
if skip is None:
resume_at = None
else:
resume_at = (skip, None)
2017-12-11 19:22:54 +03:00
if box.text:
2017-12-11 02:20:49 +03:00
first_letter = box.text[0]
if skip is None:
last_letter = box.text[-1]
else:
last_letter = box.text[skip - 1]
else:
first_letter = last_letter = None
elif isinstance(box, boxes.InlineBox):
2011-08-26 11:58:30 +04:00
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, first_letter,
2018-08-29 14:26:08 +03:00
last_letter, float_widths) = split_inline_box(
2012-07-12 19:13:21 +04:00
context, box, position_x, max_x, skip_stack, containing_block,
2012-06-28 16:00:27 +04:00
device_size, absolute_boxes, fixed_boxes, line_placeholders,
waiting_floats, line_children)
elif isinstance(box, boxes.AtomicInlineLevelBox):
new_box = atomic_box(
2012-07-12 19:13:21 +04:00
context, box, position_x, skip_stack, containing_block,
device_size, absolute_boxes, fixed_boxes)
2011-10-19 17:14:43 +04:00
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'
2018-02-24 06:41:11 +03:00
elif isinstance(box, boxes.InlineFlexBox):
box.position_x = position_x
box.position_y = 0
for side in ['top', 'right', 'bottom', 'left']:
if getattr(box, 'margin_' + side) == 'auto':
setattr(box, 'margin_' + side, 0)
new_box, resume_at, _, _, _ = flex_layout(
context, box, float('inf'), skip_stack, containing_block,
device_size, False, absolute_boxes, fixed_boxes)
preserved_line_break = False
first_letter = '\u2e80'
last_letter = '\u2e80'
else: # pragma: no cover
raise TypeError('Layout for %s not handled yet' % type(box).__name__)
2018-08-29 14:26:08 +03:00
return (
new_box, resume_at, preserved_line_break, first_letter, last_letter,
float_widths)
2011-08-24 12:48:42 +04:00
2012-07-12 19:13:21 +04:00
def split_inline_box(context, box, position_x, max_x, skip_stack,
containing_block, device_size, absolute_boxes,
fixed_boxes, line_placeholders, waiting_floats,
line_children):
"""Same behavior as split_inline_level."""
# In some cases (shrink-to-fit result being the preferred width)
# max_x is coming from Pango itself,
# but floating point errors have accumulated:
# width2 = (width + X) - X # in some cases, width2 < width
# Increase the value a bit to compensate and not introduce
# an unexpected line break. The 1e-9 value comes from PEP 485.
max_x *= 1 + 1e-9
2012-06-15 19:59:15 +04:00
is_start = skip_stack is None
initial_position_x = position_x
initial_skip_stack = skip_stack
2011-10-19 17:14:43 +04:00
assert isinstance(box, (boxes.LineBox, boxes.InlineBox))
left_spacing = (box.padding_left + box.margin_left +
box.border_left_width)
2017-12-10 03:01:55 +03:00
right_spacing = (box.padding_right + box.margin_right +
box.border_right_width)
2011-10-19 17:14:43 +04:00
content_box_left = position_x
2011-10-03 20:57:26 +04:00
children = []
waiting_children = []
preserved_line_break = False
first_letter = last_letter = None
2018-08-29 14:26:08 +03:00
float_widths = {'left': 0, 'right': 0}
float_resume_at = 0
if box.style['position'] == 'relative':
2012-05-31 01:40:54 +04:00
absolute_boxes = []
if is_start:
skip = 0
else:
skip, skip_stack = skip_stack
2012-06-21 20:12:17 +04:00
2017-12-10 03:01:55 +03:00
box_children = list(box.enumerate_skip(skip))
for i, (index, child) in enumerate(box_children):
child.position_y = box.position_y
2012-06-15 19:59:15 +04:00
if child.is_absolutely_positioned():
child.position_x = position_x
2012-06-15 19:59:15 +04:00
placeholder = AbsolutePlaceholder(child)
line_placeholders.append(placeholder)
waiting_children.append((index, placeholder))
if child.style['position'] == 'absolute':
2012-06-06 11:49:56 +04:00
absolute_boxes.append(placeholder)
2012-06-15 19:59:15 +04:00
else:
fixed_boxes.append(placeholder)
2012-05-09 21:01:32 +04:00
continue
elif child.is_floated():
child.position_x = position_x
2019-07-09 18:16:09 +03:00
float_width = shrink_to_fit(context, child, containing_block.width)
# To retrieve the real available space for floats, we must remove
# the trailing whitespaces from the line
non_floating_children = [
2017-11-17 04:39:17 +03:00
child_ for _, child_ in (children + waiting_children)
if not child_.is_floated()]
if non_floating_children:
float_width -= trailing_whitespace_size(
context, non_floating_children[-1])
if float_width > max_x - position_x or waiting_floats:
# TODO: the absolute and fixed boxes in the floats must be
# added here, and not in iter_line_boxes
waiting_floats.append(child)
else:
child = float_layout(
context, child, containing_block, device_size,
absolute_boxes, fixed_boxes)
waiting_children.append((index, child))
2017-12-12 20:20:30 +03:00
# Translate previous line children
2018-08-29 14:26:08 +03:00
dx = max(child.margin_width(), 0)
float_widths[child.style['float']] += dx
if child.style['float'] == 'left':
2017-12-12 20:20:30 +03:00
if isinstance(box, boxes.LineBox):
# The parent is the line, update the current position
# for the next child. When the parent is not the line
# (it is an inline block), the current position of the
2018-08-29 14:26:08 +03:00
# line is updated by the box itself (see next
2017-12-12 20:20:30 +03:00
# split_inline_level call).
position_x += dx
elif child.style['float'] == 'right':
2017-12-12 20:20:30 +03:00
# Update the maximum x position for the next children
2018-08-29 14:26:08 +03:00
max_x -= dx
for _, old_child in line_children:
if not old_child.is_in_normal_flow():
continue
if ((child.style['float'] == 'left' and
box.style['direction'] == 'ltr') or
(child.style['float'] == 'right' and
box.style['direction'] == 'rtl')):
2017-12-12 20:20:30 +03:00
old_child.translate(dx=dx)
float_resume_at = index + 1
continue
2012-05-11 21:31:31 +04:00
2017-12-10 03:01:55 +03:00
last_child = (i == len(box_children) - 1)
available_width = max_x
child_waiting_floats = []
2018-08-29 14:26:08 +03:00
new_child, resume_at, preserved, first, last, new_float_widths = (
split_inline_level(
context, child, position_x, available_width, skip_stack,
containing_block, device_size, absolute_boxes, fixed_boxes,
line_placeholders, child_waiting_floats, line_children))
2017-12-23 03:15:31 +03:00
if last_child and right_spacing and resume_at is None:
# TODO: we should take care of children added into absolute_boxes,
# fixed_boxes and other lists.
if box.style['direction'] == 'rtl':
available_width -= left_spacing
else:
available_width -= right_spacing
2018-08-29 14:26:08 +03:00
new_child, resume_at, preserved, first, last, new_float_widths = (
split_inline_level(
context, child, position_x, available_width, skip_stack,
containing_block, device_size, absolute_boxes, fixed_boxes,
line_placeholders, child_waiting_floats, line_children))
2018-08-29 14:26:08 +03:00
if box.style['direction'] == 'rtl':
max_x -= new_float_widths['left']
else:
max_x -= new_float_widths['right']
2017-12-12 20:20:30 +03:00
skip_stack = None
if preserved:
preserved_line_break = True
can_break = None
if last_letter is True:
last_letter = ' '
2018-02-24 06:41:11 +03:00
elif last_letter is False:
last_letter = ' ' # no-break space
elif box.style['white_space'] in ('pre', 'nowrap'):
can_break = False
if can_break is None:
if None in (last_letter, first):
can_break = False
else:
can_break = can_break_text(
last_letter + first, child.style['lang'])
if can_break:
children.extend(waiting_children)
waiting_children = []
if first_letter is None:
first_letter = first
if child.trailing_collapsible_space:
last_letter = True
else:
last_letter = last
if new_child is None:
# May be None where we have an empty TextBox.
assert isinstance(child, boxes.TextBox)
else:
2017-12-12 20:20:30 +03:00
if isinstance(box, boxes.LineBox):
line_children.append((index, new_child))
2017-12-10 03:01:55 +03:00
# TODO: we should try to find a better condition here.
trailing_whitespace = (
isinstance(new_child, boxes.TextBox) and
not new_child.text.strip())
margin_width = new_child.margin_width()
2017-12-12 20:20:30 +03:00
new_position_x = new_child.position_x + margin_width
2017-12-10 03:01:55 +03:00
if new_position_x > max_x and not trailing_whitespace:
if waiting_children:
# Too wide, let's try to cut inside waiting children,
# starting from the end.
# TODO: we should take care of children added into
# absolute_boxes, fixed_boxes and other lists.
2017-12-11 13:17:31 +03:00
waiting_children_copy = waiting_children[:]
2017-12-10 03:01:55 +03:00
break_found = False
while waiting_children_copy:
child_index, child = waiting_children_copy.pop()
# TODO: should we also accept relative children?
if (child.is_in_normal_flow() and
can_break_inside(child)):
# We break the waiting child at its last possible
# breaking point.
# TODO: The dirty solution chosen here is to
# decrease the actual size by 1 and render the
# waiting child again with this constraint. We may
# find a better way.
max_x = child.position_x + child.margin_width() - 1
2018-08-29 14:26:08 +03:00
child_new_child, child_resume_at, _, _, _, _ = (
2017-12-11 19:22:54 +03:00
split_inline_level(
context, child, child.position_x, max_x,
None, box, device_size,
2017-12-11 19:22:54 +03:00
absolute_boxes, fixed_boxes,
line_placeholders, waiting_floats,
line_children))
# As PangoLayout and PangoLogAttr don't always
# agree, we have to rely on the actual split to
# know whether the child was broken.
# https://github.com/Kozea/WeasyPrint/issues/614
break_found = child_resume_at is not None
if child_resume_at is None:
# PangoLayout decided not to break the child
child_resume_at = (0, None)
# TODO: use this when Pango is always 1.40.13+:
# break_found = True
2017-12-11 19:22:54 +03:00
children = children + waiting_children_copy
if child_new_child is None:
# May be None where we have an empty TextBox.
2017-12-11 19:22:54 +03:00
assert isinstance(child, boxes.TextBox)
else:
children += [(child_index, child_new_child)]
# We have to check whether the child we're breaking
# is the one broken by the initial skip stack.
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
# add the original skip stack to the partial
# skip stack we get after the new rendering.
# We have to do:
# child_resume_at += initial_skip_stack[1]
# but adding skip stacks is a bit complicated
current_skip_stack = initial_skip_stack[1]
current_resume_at = child_resume_at
stack = []
while current_skip_stack and current_resume_at:
skip_stack, current_skip_stack = (
current_skip_stack)
resume_at, current_resume_at = (
current_resume_at)
stack.append(skip_stack + resume_at)
child_resume_at = (
current_skip_stack or current_resume_at)
while stack:
child_resume_at = (
stack.pop(), child_resume_at)
2017-12-11 19:22:54 +03:00
resume_at = (child_index, child_resume_at)
break
2017-12-10 03:01:55 +03:00
if break_found:
break
if children:
# Too wide, can't break waiting children and the inline is
2017-12-10 03:01:55 +03:00
# non-empty: put child entirely on the next line.
resume_at = (children[-1][0] + 1, None)
child_waiting_floats = []
break
position_x = new_position_x
waiting_children.append((index, new_child))
waiting_floats.extend(child_waiting_floats)
if resume_at is not None:
children.extend(waiting_children)
resume_at = (index, resume_at)
break
else:
children.extend(waiting_children)
resume_at = None
is_end = resume_at is None
new_box = box.copy_with_children(
2017-12-11 13:17:31 +03:00
[box_child for index, box_child in children],
is_start=is_start, is_end=is_end)
2012-02-03 03:43:18 +04:00
if isinstance(box, boxes.LineBox):
# We must reset line box width according to its new children
in_flow_children = [
box_child for box_child in new_box.children
if box_child.is_in_normal_flow()]
if in_flow_children:
new_box.width = (
in_flow_children[-1].position_x +
in_flow_children[-1].margin_width() -
new_box.position_x)
else:
new_box.width = 0
2012-02-03 03:43:18 +04:00
else:
new_box.position_x = initial_position_x
if box.style['box_decoration_break'] == 'clone':
translation_needed = True
else:
translation_needed = (
is_start if box.style['direction'] == 'ltr' else is_end)
if translation_needed:
for child in new_box.children:
child.translate(dx=left_spacing)
2012-02-03 03:43:18 +04:00
new_box.width = position_x - content_box_left
2018-08-29 14:26:08 +03:00
new_box.translate(dx=float_widths['left'], ignore_floats=True)
2011-10-19 17:14:43 +04:00
line_height, new_box.baseline = strut_layout(box.style, context)
new_box.height = box.style['font_size']
half_leading = (line_height - new_box.height) / 2.
2011-10-19 17:14:43 +04:00
# Set margins to the half leading but also compensate for borders and
# paddings. We want margin_height() == line_height
new_box.margin_top = (half_leading - new_box.border_top_width -
new_box.padding_top)
new_box.margin_bottom = (half_leading - new_box.border_bottom_width -
2011-10-19 17:14:43 +04:00
new_box.padding_bottom)
if new_box.style['position'] == 'relative':
2012-05-11 21:31:31 +04:00
for absolute_box in absolute_boxes:
2012-07-12 19:13:21 +04:00
absolute_layout(context, absolute_box, new_box, fixed_boxes)
if resume_at is not None:
if resume_at[0] < float_resume_at:
resume_at = (float_resume_at, None)
2018-08-29 14:26:08 +03:00
return (
new_box, resume_at, preserved_line_break, first_letter, last_letter,
float_widths)
2011-08-24 12:48:42 +04:00
def split_text_box(context, box, available_width, skip):
2017-10-21 22:11:34 +03:00
"""Keep as much text as possible from a TextBox in a limited width.
2011-10-19 17:14:43 +04:00
Try not to overflow but always have some text in ``new_box``
2011-08-24 12:48:42 +04:00
Return ``(new_box, skip, preserved_line_break)``. ``skip`` is the number of
UTF-8 bytes to skip form the start of the TextBox for the next line, or
``None`` if all of the text fits.
2011-08-24 12:48:42 +04:00
2017-10-21 22:11:34 +03:00
Also break on preserved line breaks.
2011-08-24 12:48:42 +04:00
"""
2011-10-19 17:14:43 +04:00
assert isinstance(box, boxes.TextBox)
font_size = box.style['font_size']
2011-11-08 16:09:19 +04:00
text = box.text[skip:]
if font_size == 0 or not text:
return None, None, False
layout, length, resume_at, width, height, baseline = split_first_line(
text, box.style, context, available_width, box.justification_spacing)
assert resume_at != 0
2011-11-08 16:09:19 +04:00
# Convert ``length`` and ``resume_at`` from UTF-8 indexes in text
# to Unicode indexes.
# No need to encode whats after resume_at (if set) or length (if
# resume_at is not set). One code point is one or more byte, so
# UTF-8 indexes are always bigger or equal to Unicode indexes.
new_text = layout.text
2017-04-29 15:47:55 +03:00
encoded = text.encode('utf8')
2011-11-08 16:09:19 +04:00
if resume_at is not None:
2017-04-29 15:47:55 +03:00
between = encoded[length:resume_at].decode('utf8')
resume_at = len(encoded[:resume_at].decode('utf8'))
length = len(encoded[:length].decode('utf8'))
2011-11-08 16:09:19 +04:00
if length > 0:
2011-11-08 16:09:19 +04:00
box = box.copy_with_text(new_text)
2011-10-19 17:14:43 +04:00
box.width = width
box.pango_layout = layout
2011-10-19 17:14:43 +04:00
# "The height of the content area should be based on the font,
# but this specification does not specify how."
# http://www.w3.org/TR/CSS21/visudet.html#inline-non-replaced
# We trust Pango and use the height of the LayoutLine.
box.height = height
# "only the 'line-height' is used when calculating the height
# of the line box."
# Set margins so that margin_height() == line_height
line_height, _ = strut_layout(box.style, context)
half_leading = (line_height - height) / 2.
2011-10-19 17:14:43 +04:00
box.margin_top = half_leading
box.margin_bottom = half_leading
# form the top of the content box
box.baseline = baseline
# form the top of the margin box
box.baseline += box.margin_top
else:
2011-10-19 17:14:43 +04:00
box = None
if resume_at is None:
preserved_line_break = False
else:
preserved_line_break = (length != resume_at) and between.strip(' ')
if preserved_line_break:
2012-06-01 11:45:13 +04:00
# See http://unicode.org/reports/tr14/
2017-10-21 22:11:34 +03:00
# \r is already handled by process_whitespace
line_breaks = ('\n', '\t', '\f', '\u0085', '\u2028', '\u2029')
assert between in line_breaks, (
2012-06-01 11:45:13 +04:00
'Got %r between two lines. '
'Expected nothing or a preserved line break' % (between,))
resume_at += skip
2011-10-19 17:14:43 +04:00
return box, resume_at, preserved_line_break
def line_box_verticality(box):
2012-06-08 20:38:19 +04:00
"""Handle ``vertical-align`` within an :class:`LineBox` (or of a
non-align sub-tree).
Place all boxes vertically assuming that the baseline of ``box``
is at `y = 0`.
Return ``(max_y, min_y)``, the maximum and minimum vertical position
of margin boxes.
"""
2012-06-08 20:38:19 +04:00
top_bottom_subtrees = []
max_y, min_y = aligned_subtree_verticality(
box, top_bottom_subtrees, baseline_y=0)
2014-04-27 15:29:55 +04:00
subtrees_with_min_max = [
(subtree, sub_max_y, sub_min_y)
for subtree in top_bottom_subtrees
for sub_max_y, sub_min_y in [
(None, None) if subtree.is_floated()
else aligned_subtree_verticality(
subtree, top_bottom_subtrees, baseline_y=0)
2014-04-27 15:29:55 +04:00
]
]
2012-06-08 20:38:19 +04:00
if subtrees_with_min_max:
sub_positions = [
2012-06-08 20:38:19 +04:00
sub_max_y - sub_min_y
for subtree, sub_max_y, sub_min_y in subtrees_with_min_max
if not subtree.is_floated()]
if sub_positions:
highest_sub = max(sub_positions)
max_y = max(max_y, min_y + highest_sub)
2012-06-08 20:38:19 +04:00
for subtree, sub_max_y, sub_min_y in subtrees_with_min_max:
if subtree.is_floated():
dy = min_y - subtree.position_y
elif subtree.style['vertical_align'] == 'top':
2012-06-08 20:38:19 +04:00
dy = min_y - sub_min_y
else:
assert subtree.style['vertical_align'] == 'bottom'
2012-06-08 20:38:19 +04:00
dy = max_y - sub_max_y
translate_subtree(subtree, dy)
return max_y, min_y
def translate_subtree(box, dy):
if isinstance(box, boxes.InlineBox):
box.position_y += dy
if box.style['vertical_align'] in ('top', 'bottom'):
2012-06-08 20:38:19 +04:00
for child in box.children:
translate_subtree(child, dy)
else:
# Text or atomic boxes
box.translate(dy=dy)
def aligned_subtree_verticality(box, top_bottom_subtrees, baseline_y):
max_y, min_y = inline_box_verticality(box, top_bottom_subtrees, baseline_y)
# Account for the line box itself:
top = baseline_y - box.baseline
bottom = top + box.margin_height()
if min_y is None or top < min_y:
min_y = top
if max_y is None or bottom > max_y:
max_y = bottom
2012-06-08 20:38:19 +04:00
return max_y, min_y
2012-06-08 20:38:19 +04:00
def inline_box_verticality(box, top_bottom_subtrees, baseline_y):
2011-10-19 17:14:43 +04:00
"""Handle ``vertical-align`` within an :class:`InlineBox`.
Place all boxes vertically assuming that the baseline of ``box``
is at `y = baseline_y`.
2011-10-19 17:14:43 +04:00
Return ``(max_y, min_y)``, the maximum and minimum vertical position
of margin boxes.
2011-10-19 17:14:43 +04:00
"""
max_y = None
min_y = None
2012-06-08 20:38:19 +04:00
if not isinstance(box, (boxes.LineBox, boxes.InlineBox)):
return max_y, min_y
for child in box.children:
2012-05-10 22:12:47 +04:00
if not child.is_in_normal_flow():
if child.is_floated():
top_bottom_subtrees.append(child)
2012-05-10 22:12:47 +04:00
continue
vertical_align = child.style['vertical_align']
2011-11-23 21:23:33 +04:00
if vertical_align == 'baseline':
child_baseline_y = baseline_y
elif vertical_align == 'middle':
one_ex = box.style['font_size'] * ex_ratio(box.style)
2011-11-23 21:23:33 +04:00
top = baseline_y - (one_ex + child.margin_height()) / 2.
child_baseline_y = top + child.baseline
# TODO: actually implement vertical-align: top and bottom
2012-06-08 20:38:19 +04:00
elif vertical_align == 'text-top':
# align top with the top of the parents content area
top = (baseline_y - box.baseline + box.margin_top +
box.border_top_width + box.padding_top)
child_baseline_y = top + child.baseline
2012-06-08 20:38:19 +04:00
elif vertical_align == 'text-bottom':
# align bottom with the bottom of the parents content area
bottom = (baseline_y - box.baseline + box.margin_top +
box.border_top_width + box.padding_top + box.height)
child_baseline_y = bottom - child.margin_height() + child.baseline
2012-06-08 20:38:19 +04:00
elif vertical_align in ('top', 'bottom'):
# Later, we will assume for this subtree that its baseline
# is at y=0.
child_baseline_y = 0
2011-11-23 21:23:33 +04:00
else:
# Numeric value: The childs baseline is `vertical_align` above
# (lower y) the parents baseline.
child_baseline_y = baseline_y - vertical_align
# the childs `top` is `child.baseline` above (lower y) its baseline.
top = child_baseline_y - child.baseline
2018-02-25 20:05:04 +03:00
if isinstance(child, (boxes.InlineBlockBox, boxes.InlineFlexBox)):
# This also includes table wrappers for inline tables.
child.translate(dy=top - child.position_y)
else:
child.position_y = top
# grand-children for inline boxes are handled below
if vertical_align in ('top', 'bottom'):
# top or bottom are special, they need to be handled in
# a later pass.
top_bottom_subtrees.append(child)
continue
bottom = top + child.margin_height()
if min_y is None or top < min_y:
min_y = top
if max_y is None or bottom > max_y:
max_y = bottom
if isinstance(child, boxes.InlineBox):
children_max_y, children_min_y = inline_box_verticality(
2012-06-08 20:38:19 +04:00
child, top_bottom_subtrees, child_baseline_y)
2013-04-12 22:09:41 +04:00
if children_min_y is not None and children_min_y < min_y:
2013-04-12 20:51:43 +04:00
min_y = children_min_y
2013-04-12 22:09:41 +04:00
if children_max_y is not None and children_max_y > max_y:
2013-04-12 20:51:43 +04:00
max_y = children_max_y
2011-10-19 17:14:43 +04:00
return max_y, min_y
2012-07-12 19:13:21 +04:00
def text_align(context, line, available_width, last):
"""Return how much the line should be moved horizontally according to
the `text-align` property.
"""
# "When the total width of the inline-level boxes on a line is less than
# the width of the line box containing them, their horizontal distribution
# within the line box is determined by the 'text-align' property."
if line.width >= available_width:
return 0
align = line.style['text_align']
space_collapse = line.style['white_space'] in (
'normal', 'nowrap', 'pre-line')
if align in ('-weasy-start', '-weasy-end'):
if (align == '-weasy-start') ^ (line.style['direction'] == 'rtl'):
align = 'left'
else:
align = 'right'
2012-02-01 19:13:47 +04:00
if align == 'justify' and last:
align = 'right' if line.style['direction'] == 'rtl' else 'left'
if align == 'left':
return 0
2012-05-31 01:40:54 +04:00
offset = available_width - line.width
2012-02-01 19:13:47 +04:00
if align == 'justify':
if space_collapse:
# Justification of texts where white space is not collapsing is
# - forbidden by CSS 2, and
# - not required by CSS 3 Text.
justify_line(context, line, offset)
2012-02-01 19:13:47 +04:00
return 0
if align == 'center':
2018-12-27 18:42:27 +03:00
return offset / 2
else:
assert align == 'right'
2018-12-27 18:42:27 +03:00
return offset
2012-02-01 19:13:47 +04:00
2012-07-12 19:13:21 +04:00
def justify_line(context, line, extra_width):
# TODO: We should use a better alorithm here, see
# https://www.w3.org/TR/css-text-3/#justify-algos
2012-02-01 19:13:47 +04:00
nb_spaces = count_spaces(line)
if nb_spaces == 0:
return
2012-07-12 19:13:21 +04:00
add_word_spacing(context, line, extra_width / nb_spaces, 0)
2012-02-01 19:13:47 +04:00
def count_spaces(box):
if isinstance(box, boxes.TextBox):
# TODO: remove trailing spaces correctly
return box.text.count(' ')
elif isinstance(box, (boxes.LineBox, boxes.InlineBox)):
return sum(count_spaces(child) for child in box.children)
else:
return 0
def add_word_spacing(context, box, justification_spacing, x_advance):
2012-02-01 19:13:47 +04:00
if isinstance(box, boxes.TextBox):
box.justification_spacing = justification_spacing
2012-02-01 19:13:47 +04:00
box.position_x += x_advance
nb_spaces = count_spaces(box)
if nb_spaces > 0:
layout = create_layout(
box.text, box.style, context, float('inf'),
box.justification_spacing)
layout.deactivate()
extra_space = justification_spacing * nb_spaces
x_advance += extra_space
box.width += extra_space
box.pango_layout = layout
2012-02-01 19:13:47 +04:00
elif isinstance(box, (boxes.LineBox, boxes.InlineBox)):
box.position_x += x_advance
previous_x_advance = x_advance
for child in box.children:
if child.is_in_normal_flow():
x_advance = add_word_spacing(
context, child, justification_spacing, x_advance)
2012-02-01 19:13:47 +04:00
box.width += x_advance - previous_x_advance
else:
# Atomic inline-level box
box.translate(x_advance, 0)
return x_advance
2013-04-12 20:51:43 +04:00
def is_phantom_linebox(linebox):
"""http://www.w3.org/TR/CSS21/visuren.html#phantom-line-box"""
for child in linebox.children:
if isinstance(child, boxes.InlineBox):
if not is_phantom_linebox(child):
return False
for side in ('top', 'right', 'bottom', 'left'):
if (getattr(child.style['margin_%s' % side], 'value', None) or
child.style['border_%s_width' % side] or
child.style['padding_%s' % side].value):
return False
elif child.is_in_normal_flow():
return False
return True
def can_break_inside(box):
# See https://www.w3.org/TR/css-text-3/#white-space-property
text_wrap = box.style['white_space'] in ('normal', 'pre-wrap', 'pre-line')
if isinstance(box, boxes.AtomicInlineLevelBox):
return False
elif isinstance(box, boxes.TextBox):
if text_wrap:
return can_break_text(box.text, box.style['lang'])
else:
return False
elif isinstance(box, boxes.ParentBox):
if text_wrap:
return any(can_break_inside(child) for child in box.children)
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