2011-07-22 18:34:32 +04:00
|
|
|
|
# coding: utf8
|
2012-03-22 02:19:27 +04:00
|
|
|
|
"""
|
|
|
|
|
weasyprint.layout.inline
|
|
|
|
|
------------------------
|
2011-07-22 18:34:32 +04:00
|
|
|
|
|
2012-03-22 02:19:27 +04:00
|
|
|
|
Line breaking and layout for inline-level boxes.
|
2011-07-22 18:34:32 +04:00
|
|
|
|
|
2012-03-22 02:19:27 +04:00
|
|
|
|
:copyright: Copyright 2011-2012 Simon Sapin and contributors, see AUTHORS.
|
|
|
|
|
:license: BSD, see LICENSE for details.
|
2011-08-24 12:48:42 +04:00
|
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
2012-02-17 21:49:58 +04:00
|
|
|
|
from __future__ import division, unicode_literals
|
2011-12-12 12:46:34 +04:00
|
|
|
|
|
2012-05-25 14:03:48 +04:00
|
|
|
|
from .absolute import absolute_layout, AbsolutePlaceholder
|
2012-06-21 18:03:29 +04:00
|
|
|
|
from .float import avoid_collisions, float_layout
|
2011-08-22 17:37:10 +04:00
|
|
|
|
from .markers import image_marker_layout
|
2012-06-04 21:22:18 +04:00
|
|
|
|
from .min_max import handle_min_max_width, handle_min_max_height
|
2011-10-21 14:22:19 +04:00
|
|
|
|
from .percentages import resolve_percentages, resolve_one_percentage
|
2012-05-31 01:40:54 +04:00
|
|
|
|
from .preferred import shrink_to_fit, inline_preferred_minimum_width
|
2012-04-12 14:51:21 +04:00
|
|
|
|
from .tables import find_in_flow_baseline, table_wrapper_width
|
2012-06-21 17:10:17 +04:00
|
|
|
|
from ..text import split_first_line
|
2011-08-22 17:37:10 +04:00
|
|
|
|
from ..formatting_structure import boxes
|
2012-06-08 18:10:35 +04:00
|
|
|
|
from ..css.computed_values import strut_layout
|
2011-08-17 16:01:06 +04:00
|
|
|
|
|
|
|
|
|
|
2012-07-12 19:13:21 +04:00
|
|
|
|
def iter_line_boxes(context, box, position_y, skip_stack, containing_block,
|
2012-06-06 14:04:09 +04:00
|
|
|
|
device_size, absolute_boxes, fixed_boxes):
|
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.
|
|
|
|
|
|
|
|
|
|
:param linebox: a non-laid-out :class:`LineBox`
|
|
|
|
|
: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.
|
|
|
|
|
|
|
|
|
|
"""
|
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,
|
2012-06-06 14:04:09 +04:00
|
|
|
|
device_size, absolute_boxes, fixed_boxes)
|
2012-06-26 01:48:21 +04:00
|
|
|
|
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
|
|
|
|
|
|
|
|
|
|
|
2012-07-12 19:13:21 +04:00
|
|
|
|
def get_next_linebox(context, linebox, position_y, skip_stack,
|
2012-06-06 14:04:09 +04:00
|
|
|
|
containing_block, device_size, absolute_boxes,
|
|
|
|
|
fixed_boxes):
|
2012-03-14 22:33:24 +04:00
|
|
|
|
"""Return ``(line, resume_at)``."""
|
2012-06-15 17:14:43 +04:00
|
|
|
|
resolve_percentages(linebox, containing_block)
|
2011-10-21 14:22:19 +04:00
|
|
|
|
if skip_stack is None:
|
|
|
|
|
# text-indent only at the start of the first line
|
|
|
|
|
# Other percentages (margins, width, ...) do not apply.
|
|
|
|
|
resolve_one_percentage(linebox, 'text_indent', containing_block.width)
|
2012-06-15 17:14:43 +04:00
|
|
|
|
else:
|
|
|
|
|
linebox.text_indent = 0
|
2011-10-05 15:01:44 +04:00
|
|
|
|
|
|
|
|
|
skip_stack = skip_first_whitespace(linebox, skip_stack)
|
|
|
|
|
if skip_stack == 'continue':
|
2012-06-28 16:00:27 +04:00
|
|
|
|
return None, None
|
2011-10-05 15:01:44 +04:00
|
|
|
|
|
2012-06-15 17:14:43 +04:00
|
|
|
|
linebox.width = inline_preferred_minimum_width(
|
2012-07-12 19:13:21 +04:00
|
|
|
|
context, linebox, skip_stack=skip_stack, first_line=True)
|
2012-05-10 22:12:47 +04:00
|
|
|
|
|
2012-06-15 17:14:43 +04:00
|
|
|
|
linebox.height, _ = strut_layout(linebox.style)
|
|
|
|
|
linebox.position_y = position_y
|
|
|
|
|
position_x, position_y, available_width = avoid_collisions(
|
2012-07-12 19:13:21 +04:00
|
|
|
|
context, linebox, containing_block, outer=False)
|
2012-06-15 17:14:43 +04:00
|
|
|
|
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
|
|
|
|
|
2012-06-15 17:14:43 +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 = []
|
2012-06-15 17:14:43 +04:00
|
|
|
|
|
2012-06-28 16:00:27 +04:00
|
|
|
|
line, resume_at, preserved_line_break = split_inline_box(
|
2012-07-12 19:13:21 +04:00
|
|
|
|
context, linebox, position_x, max_x, skip_stack,
|
2012-06-28 16:00:27 +04:00
|
|
|
|
containing_block, device_size, line_absolutes,
|
|
|
|
|
line_fixed, line_placeholders, waiting_floats)
|
2012-06-15 17:14:43 +04:00
|
|
|
|
|
2012-07-12 19:13:21 +04:00
|
|
|
|
remove_last_whitespace(context, line)
|
2012-06-15 17:14:43 +04:00
|
|
|
|
|
|
|
|
|
bottom, top = line_box_verticality(line)
|
|
|
|
|
if bottom is None:
|
|
|
|
|
# No children at all
|
|
|
|
|
offset_y = 0
|
|
|
|
|
if preserved_line_break:
|
|
|
|
|
# Only the strut.
|
|
|
|
|
line.baseline = line.margin_top
|
|
|
|
|
line.height += line.margin_top + line.margin_bottom
|
|
|
|
|
else:
|
|
|
|
|
line.height = 0
|
|
|
|
|
line.baseline = 0
|
2011-10-19 17:14:43 +04:00
|
|
|
|
else:
|
2012-06-15 17:14:43 +04:00
|
|
|
|
assert top 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
|
|
|
|
|
|
2012-07-12 19:13:21 +04:00
|
|
|
|
offset_x = text_align(context, line, available_width,
|
2012-06-15 17:14:43 +04:00
|
|
|
|
last=resume_at is None or preserved_line_break)
|
|
|
|
|
if offset_x != 0 or offset_y != 0:
|
|
|
|
|
line.translate(offset_x, offset_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
|
2012-06-15 17:14:43 +04:00
|
|
|
|
position_x, position_y, available_width = avoid_collisions(
|
2012-07-12 19:13:21 +04:00
|
|
|
|
context, line, containing_block, outer=False)
|
2012-06-15 17:14:43 +04:00
|
|
|
|
if (position_x, position_y) == (
|
2012-06-21 18:03:29 +04:00
|
|
|
|
linebox.position_x, linebox.position_y):
|
2012-07-12 19:13:21 +04:00
|
|
|
|
context.excluded_shapes = new_excluded_shapes
|
2012-06-15 17:14:43 +04:00
|
|
|
|
break
|
|
|
|
|
|
|
|
|
|
absolute_boxes.extend(line_absolutes)
|
|
|
|
|
fixed_boxes.extend(line_fixed)
|
2012-05-10 22:12:47 +04:00
|
|
|
|
|
2012-05-25 14:03:48 +04:00
|
|
|
|
for placeholder in line_placeholders:
|
2012-05-25 19:33:43 +04:00
|
|
|
|
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)
|
2012-05-25 19:33:43 +04:00
|
|
|
|
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(
|
2012-07-12 19:13:21 +04:00
|
|
|
|
context, waiting_float, containing_block, absolute_boxes,
|
2012-06-28 16:00:27 +04:00
|
|
|
|
fixed_boxes)
|
|
|
|
|
float_children.append(waiting_float)
|
|
|
|
|
if float_children:
|
|
|
|
|
line = line.copy_with_children(
|
|
|
|
|
line.children + tuple(float_children))
|
|
|
|
|
|
|
|
|
|
return line, resume_at
|
2011-10-05 15:01:44 +04:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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
|
2011-10-08 16:41:12 +04:00
|
|
|
|
white_space = box.style.white_space
|
2011-11-08 16:09:19 +04:00
|
|
|
|
length = len(box.text)
|
2011-10-05 15:01:44 +04:00
|
|
|
|
if index == length:
|
|
|
|
|
# Starting a the end of the TextBox, no text to see: Continue
|
|
|
|
|
return 'continue'
|
|
|
|
|
if white_space in ('normal', 'nowrap', 'pre-line'):
|
2011-11-08 16:09:19 +04:00
|
|
|
|
while index < length and box.text[index] == ' ':
|
2011-10-05 15:01:44 +04:00
|
|
|
|
index += 1
|
2012-06-15 19:59:15 +04:00
|
|
|
|
return (index, None) if index else None
|
2011-10-05 15:01:44 +04:00
|
|
|
|
|
|
|
|
|
if isinstance(box, (boxes.LineBox, boxes.InlineBox)):
|
2011-10-11 14:09:37 +04:00
|
|
|
|
if index == 0 and not box.children:
|
|
|
|
|
return None
|
2011-10-05 15:01:44 +04:00
|
|
|
|
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
|
2011-10-05 15:01:44 +04:00
|
|
|
|
|
|
|
|
|
assert skip_stack is None, 'unexpected skip inside %s' % box
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
2013-03-13 16:49:51 +04:00
|
|
|
|
def trailing_whitespace_size(context, box):
|
|
|
|
|
"""Return the size of the trailing whitespace of ``box``."""
|
|
|
|
|
while isinstance(box, boxes.InlineBox):
|
|
|
|
|
if not box.children:
|
|
|
|
|
return 0
|
|
|
|
|
box = box.children[-1]
|
|
|
|
|
if not (isinstance(box, boxes.TextBox) and
|
|
|
|
|
box.style.white_space in ('normal', 'nowrap', 'pre-line')):
|
|
|
|
|
return 0
|
|
|
|
|
stripped_text = box.text.rstrip(' ')
|
|
|
|
|
if stripped_text:
|
|
|
|
|
if len(stripped_text) == len(box.text):
|
|
|
|
|
return 0
|
|
|
|
|
stripped_box = box.copy_with_text(stripped_text)
|
|
|
|
|
stripped_box, resume, _ = split_text_box(
|
|
|
|
|
context, stripped_box, None, None, 0)
|
|
|
|
|
assert stripped_box is not None
|
|
|
|
|
assert resume is None
|
|
|
|
|
return box.width - stripped_box.width
|
|
|
|
|
else:
|
|
|
|
|
return box.width
|
|
|
|
|
|
|
|
|
|
|
2012-07-12 19:13:21 +04:00
|
|
|
|
def remove_last_whitespace(context, box):
|
2011-10-20 20:37:21 +04:00
|
|
|
|
"""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 = []
|
2011-10-05 15:01:44 +04:00
|
|
|
|
while isinstance(box, (boxes.LineBox, boxes.InlineBox)):
|
2011-10-20 20:37:21 +04:00
|
|
|
|
ancestors.append(box)
|
2011-10-05 15:01:44 +04:00
|
|
|
|
if not box.children:
|
|
|
|
|
return
|
|
|
|
|
box = box.children[-1]
|
2011-10-20 20:37:21 +04:00
|
|
|
|
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(' ')
|
2011-10-26 19:22:49 +04:00
|
|
|
|
if new_text:
|
2011-11-08 16:09:19 +04:00
|
|
|
|
if len(new_text) == len(box.text):
|
2011-10-26 19:22:49 +04:00
|
|
|
|
return
|
2012-12-28 20:45:23 +04:00
|
|
|
|
box.text = new_text
|
2013-03-13 16:49:51 +04:00
|
|
|
|
new_box, resume, _ = split_text_box(context, box, None, None, 0)
|
2011-10-26 19:22:49 +04:00
|
|
|
|
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 = ''
|
2012-01-26 14:33:49 +04:00
|
|
|
|
|
2011-10-20 20:37:21 +04:00
|
|
|
|
for ancestor in ancestors:
|
|
|
|
|
ancestor.width -= space_width
|
2011-08-22 20:20:23 +04:00
|
|
|
|
|
2011-10-06 14:12:39 +04:00
|
|
|
|
# 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
|
|
|
|
|
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)
|
|
|
|
|
"""
|
2012-04-06 21:44:09 +04:00
|
|
|
|
# http://www.w3.org/TR/CSS21/visudet.html#inline-replaced-width
|
2013-03-28 15:44:28 +04:00
|
|
|
|
_, intrinsic_width, intrinsic_height = box.replacement
|
2012-04-06 18:57:13 +04:00
|
|
|
|
# TODO: update this when we have replaced elements that do not
|
|
|
|
|
# always have an intrinsic width. (See commented code below.)
|
|
|
|
|
assert intrinsic_width is not None
|
2013-03-28 15:44:28 +04:00
|
|
|
|
assert intrinsic_height is not None
|
2012-04-06 18:57:13 +04:00
|
|
|
|
|
2011-08-26 17:52:37 +04:00
|
|
|
|
if box.width == 'auto':
|
2013-03-28 15:44:28 +04:00
|
|
|
|
if box.height == 'auto':
|
|
|
|
|
box.width = intrinsic_width
|
|
|
|
|
else:
|
|
|
|
|
intrinsic_ratio = intrinsic_width / intrinsic_height
|
|
|
|
|
box.width = box.height * intrinsic_ratio
|
2012-04-06 18:57:13 +04:00
|
|
|
|
|
|
|
|
|
# Untested code for when we do not always have an intrinsic width.
|
|
|
|
|
# if box.height == 'auto' and box.width == 'auto':
|
|
|
|
|
# if intrinsic_width is not None:
|
|
|
|
|
# box.width = intrinsic_width
|
|
|
|
|
# elif intrinsic_height is not None and intrinsic_ratio is not None:
|
|
|
|
|
# box.width = intrinsic_ratio * intrinsic_height
|
|
|
|
|
# elif box.height != 'auto' and intrinsic_ratio is not None:
|
|
|
|
|
# box.width = intrinsic_ratio * box.height
|
|
|
|
|
# elif intrinsic_ratio is not None:
|
|
|
|
|
# pass
|
|
|
|
|
# # TODO: Intrinsic ratio only: undefined in CSS 2.1.
|
|
|
|
|
# # " 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. "
|
|
|
|
|
|
|
|
|
|
# # Still no value
|
|
|
|
|
# if box.width == 'auto':
|
|
|
|
|
# if intrinsic_width is not None:
|
|
|
|
|
# box.width = intrinsic_width
|
|
|
|
|
# else:
|
|
|
|
|
# # Then the used value of 'width' becomes 300px. If 300px is too
|
|
|
|
|
# # wide to fit the device, UAs should use the width of the largest
|
|
|
|
|
# # rectangle that has a 2:1 ratio and fits the device instead.
|
|
|
|
|
# device_width, _device_height = device_size
|
|
|
|
|
# box.width = min(300, device_width)
|
2011-08-22 17:37:10 +04:00
|
|
|
|
|
2011-08-26 18:16:40 +04:00
|
|
|
|
|
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)
|
|
|
|
|
"""
|
2012-04-06 21:44:09 +04:00
|
|
|
|
# http://www.w3.org/TR/CSS21/visudet.html#inline-replaced-height
|
2012-09-26 18:59:40 +04:00
|
|
|
|
_, intrinsic_width, intrinsic_height = box.replacement
|
2012-04-06 18:57:13 +04:00
|
|
|
|
# TODO: update this when we have replaced elements that do not
|
|
|
|
|
# always have intrinsic dimensions. (See commented code below.)
|
|
|
|
|
assert intrinsic_width is not None
|
|
|
|
|
assert intrinsic_height is not None
|
|
|
|
|
if intrinsic_height == 0:
|
|
|
|
|
# Results in box.height == 0 if used, whatever the used width
|
|
|
|
|
# or intrinsic width.
|
|
|
|
|
intrinsic_ratio = float('inf')
|
|
|
|
|
else:
|
|
|
|
|
intrinsic_ratio = intrinsic_width / intrinsic_height
|
2011-08-22 17:37:10 +04:00
|
|
|
|
|
2012-04-06 18:57:13 +04:00
|
|
|
|
# Test 'auto' on the computed width, not the used width
|
|
|
|
|
if box.style.height == 'auto' and box.style.width == 'auto':
|
2011-08-26 17:52:37 +04:00
|
|
|
|
box.height = intrinsic_height
|
2012-04-06 18:57:13 +04:00
|
|
|
|
elif box.style.height == 'auto':
|
|
|
|
|
box.height = box.width / intrinsic_ratio
|
|
|
|
|
|
|
|
|
|
# Untested code for when we do not always have intrinsic dimensions.
|
|
|
|
|
# if box.style.height == 'auto' and box.style.width == 'auto':
|
|
|
|
|
# if intrinsic_height is not None:
|
|
|
|
|
# box.height = intrinsic_height
|
|
|
|
|
# elif intrinsic_ratio is not None and box.style.height == 'auto':
|
|
|
|
|
# box.height = box.width / intrinsic_ratio
|
|
|
|
|
# elif box.style.height == 'auto' and intrinsic_height is not None:
|
|
|
|
|
# box.height = intrinsic_height
|
|
|
|
|
# elif box.style.height == 'auto':
|
|
|
|
|
# device_width, _device_height = device_size
|
|
|
|
|
# box.height = min(150, device_width / 2)
|
2011-08-22 17:37:10 +04:00
|
|
|
|
|
2011-08-26 18:16:40 +04:00
|
|
|
|
|
2012-04-06 21:44:09 +04:00
|
|
|
|
min_max_replaced_width = handle_min_max_width(replaced_box_width)
|
|
|
|
|
min_max_replaced_height = handle_min_max_height(replaced_box_height)
|
|
|
|
|
|
|
|
|
|
|
2012-05-25 16:08:35 +04:00
|
|
|
|
def inline_replaced_box_layout(box, device_size):
|
2012-04-06 21:44:09 +04:00
|
|
|
|
"""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-04-06 21:44:09 +04:00
|
|
|
|
|
2012-06-04 21:22:18 +04:00
|
|
|
|
|
2012-05-25 16:08:35 +04:00
|
|
|
|
def inline_replaced_box_width_height(box, device_size):
|
2012-04-06 21:44:09 +04:00
|
|
|
|
if box.style.width == 'auto' and box.style.height == 'auto':
|
|
|
|
|
replaced_box_width(box, device_size)
|
|
|
|
|
replaced_box_height(box, device_size)
|
|
|
|
|
min_max_auto_replaced(box)
|
|
|
|
|
else:
|
|
|
|
|
min_max_replaced_width(box, device_size)
|
|
|
|
|
min_max_replaced_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.
|
|
|
|
|
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,
|
2012-06-06 14:04:09 +04:00
|
|
|
|
device_size, absolute_boxes, fixed_boxes):
|
2011-08-24 12:48:42 +04:00
|
|
|
|
"""Compute the width and the height of the atomic ``box``."""
|
2011-08-22 13:38:54 +04:00
|
|
|
|
if isinstance(box, boxes.ReplacedBox):
|
2011-11-08 18:24:31 +04:00
|
|
|
|
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)
|
2012-04-10 14:49:54 +04:00
|
|
|
|
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:
|
2012-06-02 10:28:41 +04:00
|
|
|
|
table_wrapper_width(
|
2012-07-12 19:13:21 +04:00
|
|
|
|
context, box,
|
2012-06-06 14:52:02 +04:00
|
|
|
|
(containing_block.width, containing_block.height))
|
2012-03-23 02:26:09 +04:00
|
|
|
|
box = inline_block_box_layout(
|
2012-07-12 19:13:21 +04:00
|
|
|
|
context, box, position_x, skip_stack, containing_block,
|
2012-06-06 14:04:09 +04:00
|
|
|
|
device_size, absolute_boxes, fixed_boxes)
|
2012-04-02 16:45:44 +04:00
|
|
|
|
else: # pragma: no cover
|
2011-08-22 13:38:54 +04:00
|
|
|
|
raise TypeError('Layout for %s not handled yet' % type(box).__name__)
|
2011-10-19 17:14:43 +04:00
|
|
|
|
return box
|
2011-08-17 16:01:06 +04:00
|
|
|
|
|
|
|
|
|
|
2012-07-12 19:13:21 +04:00
|
|
|
|
def inline_block_box_layout(context, box, position_x, skip_stack,
|
2012-06-06 14:04:09 +04:00
|
|
|
|
containing_block, device_size, absolute_boxes,
|
|
|
|
|
fixed_boxes):
|
2012-03-23 02:26:09 +04:00
|
|
|
|
# Avoid a circular import
|
2012-04-10 13:10:16 +04:00
|
|
|
|
from .blocks import block_container_layout
|
2012-03-23 02:26:09 +04:00
|
|
|
|
|
2012-03-22 21:36:56 +04:00
|
|
|
|
resolve_percentages(box, containing_block)
|
2012-03-23 02:26:09 +04:00
|
|
|
|
|
|
|
|
|
# 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
|
|
|
|
|
|
2012-07-12 19:13:21 +04:00
|
|
|
|
inline_block_width(box, context, containing_block)
|
2012-03-23 02:26:09 +04:00
|
|
|
|
|
2012-03-23 03:45:38 +04:00
|
|
|
|
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,
|
2012-06-06 14:04:09 +04:00
|
|
|
|
absolute_boxes=absolute_boxes, fixed_boxes=fixed_boxes)
|
2012-04-10 14:49:54 +04:00
|
|
|
|
box.baseline = inline_block_baseline(box)
|
2012-03-22 21:36:56 +04:00
|
|
|
|
return box
|
|
|
|
|
|
|
|
|
|
|
2012-04-10 14:49:54 +04:00
|
|
|
|
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.style.overflow == 'visible':
|
|
|
|
|
result = find_in_flow_baseline(box, last=True)
|
|
|
|
|
if result:
|
|
|
|
|
return result
|
|
|
|
|
return box.position_y + box.margin_height()
|
|
|
|
|
|
|
|
|
|
|
2012-04-10 13:43:59 +04:00
|
|
|
|
@handle_min_max_width
|
2012-07-12 19:13:21 +04:00
|
|
|
|
def inline_block_width(box, context, containing_block):
|
2012-04-10 13:43:59 +04:00
|
|
|
|
if box.width == 'auto':
|
2012-07-12 19:13:21 +04:00
|
|
|
|
box.width = shrink_to_fit(context, box, containing_block.width)
|
2012-04-10 13:43:59 +04:00
|
|
|
|
|
|
|
|
|
|
2012-07-12 19:13:21 +04:00
|
|
|
|
def split_inline_level(context, box, position_x, max_x, skip_stack,
|
2012-05-25 14:03:48 +04:00
|
|
|
|
containing_block, device_size, absolute_boxes,
|
2012-06-28 16:00:27 +04:00
|
|
|
|
fixed_boxes, line_placeholders, waiting_floats):
|
2011-09-30 21:04:05 +04:00
|
|
|
|
"""Fit as much content as possible from an inline-level box in a width.
|
2011-08-17 16:01:06 +04:00
|
|
|
|
|
2011-09-30 21:04:05 +04:00
|
|
|
|
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.
|
2011-08-24 20:06:25 +04:00
|
|
|
|
|
2011-09-30 21:04:05 +04:00
|
|
|
|
``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.)
|
2011-08-24 20:06:25 +04:00
|
|
|
|
|
|
|
|
|
"""
|
2011-11-29 13:37:59 +04:00
|
|
|
|
resolve_percentages(box, containing_block)
|
2011-08-24 20:06:25 +04:00
|
|
|
|
if isinstance(box, boxes.TextBox):
|
2011-10-20 20:37:21 +04:00
|
|
|
|
box.position_x = position_x
|
2011-10-05 15:01:44 +04:00
|
|
|
|
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(
|
2012-11-24 19:14:49 +04:00
|
|
|
|
context, box, max_x - position_x, max_x, skip)
|
2011-10-05 15:01:44 +04:00
|
|
|
|
|
|
|
|
|
if skip is None:
|
|
|
|
|
resume_at = None
|
|
|
|
|
else:
|
|
|
|
|
resume_at = (skip, None)
|
2011-08-24 20:06:25 +04:00
|
|
|
|
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
|
2012-06-28 16:00:27 +04:00
|
|
|
|
new_box, resume_at, preserved_line_break = 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)
|
2011-08-24 20:06:25 +04:00
|
|
|
|
elif isinstance(box, boxes.AtomicInlineLevelBox):
|
2012-03-23 03:45:38 +04:00
|
|
|
|
new_box = atomic_box(
|
2012-07-12 19:13:21 +04:00
|
|
|
|
context, box, position_x, skip_stack, containing_block,
|
2012-06-06 14:04:09 +04:00
|
|
|
|
device_size, absolute_boxes, fixed_boxes)
|
2011-10-19 17:14:43 +04:00
|
|
|
|
new_box.position_x = position_x
|
2011-09-30 21:04:05 +04:00
|
|
|
|
resume_at = None
|
2011-10-05 15:01:44 +04:00
|
|
|
|
preserved_line_break = False
|
2011-09-30 21:04:05 +04:00
|
|
|
|
#else: unexpected box type here
|
2011-10-05 15:01:44 +04:00
|
|
|
|
return new_box, resume_at, preserved_line_break
|
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,
|
2012-05-25 14:03:48 +04:00
|
|
|
|
containing_block, device_size, absolute_boxes,
|
2012-06-28 16:00:27 +04:00
|
|
|
|
fixed_boxes, line_placeholders, waiting_floats):
|
2011-09-30 21:04:05 +04:00
|
|
|
|
"""Same behavior as split_inline_level."""
|
2012-06-15 19:59:15 +04:00
|
|
|
|
is_start = skip_stack is None
|
2011-10-21 14:22:19 +04:00
|
|
|
|
initial_position_x = position_x
|
2011-10-19 17:14:43 +04:00
|
|
|
|
assert isinstance(box, (boxes.LineBox, boxes.InlineBox))
|
|
|
|
|
left_spacing = (box.padding_left + box.margin_left +
|
2011-12-30 20:19:02 +04:00
|
|
|
|
box.border_left_width)
|
2011-10-19 17:14:43 +04:00
|
|
|
|
right_spacing = (box.padding_right + box.margin_right +
|
2011-12-30 20:19:02 +04:00
|
|
|
|
box.border_right_width)
|
2012-06-15 19:59:15 +04:00
|
|
|
|
if is_start:
|
|
|
|
|
position_x += left_spacing
|
2011-10-19 17:14:43 +04:00
|
|
|
|
content_box_left = position_x
|
2011-08-17 16:01:06 +04:00
|
|
|
|
|
2011-10-03 20:57:26 +04:00
|
|
|
|
children = []
|
2011-10-05 15:01:44 +04:00
|
|
|
|
preserved_line_break = False
|
2011-08-17 16:01:06 +04:00
|
|
|
|
|
2012-05-31 01:40:54 +04:00
|
|
|
|
if box.style.position == 'relative':
|
|
|
|
|
absolute_boxes = []
|
|
|
|
|
|
2012-05-21 16:22:32 +04:00
|
|
|
|
if is_start:
|
2011-09-30 21:04:05 +04:00
|
|
|
|
skip = 0
|
|
|
|
|
else:
|
|
|
|
|
skip, skip_stack = skip_stack
|
2012-06-21 20:12:17 +04:00
|
|
|
|
|
2011-10-19 17:14:43 +04:00
|
|
|
|
for index, child in box.enumerate_skip(skip):
|
2012-05-25 14:03:48 +04:00
|
|
|
|
child.position_y = box.position_y
|
2012-06-15 19:59:15 +04:00
|
|
|
|
if child.is_absolutely_positioned():
|
2012-05-31 02:22:14 +04:00
|
|
|
|
child.position_x = position_x
|
2012-06-15 19:59:15 +04:00
|
|
|
|
placeholder = AbsolutePlaceholder(child)
|
|
|
|
|
line_placeholders.append(placeholder)
|
|
|
|
|
children.append(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
|
2012-06-21 18:03:29 +04:00
|
|
|
|
elif child.is_floated():
|
|
|
|
|
child.position_x = position_x
|
2012-06-28 14:00:02 +04:00
|
|
|
|
float_width = shrink_to_fit(
|
2012-07-12 19:13:21 +04:00
|
|
|
|
context, child, containing_block.width)
|
2013-03-13 16:49:51 +04:00
|
|
|
|
|
|
|
|
|
# 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()]
|
|
|
|
|
if non_floating_children:
|
|
|
|
|
float_width -= trailing_whitespace_size(
|
|
|
|
|
context, non_floating_children[-1])
|
|
|
|
|
|
2012-06-28 14:00:02 +04:00
|
|
|
|
if float_width > max_x - position_x or waiting_floats:
|
2012-06-26 01:48:21 +04:00
|
|
|
|
# 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(
|
2012-07-12 19:13:21 +04:00
|
|
|
|
context, child, containing_block, absolute_boxes,
|
2012-06-26 01:48:21 +04:00
|
|
|
|
fixed_boxes)
|
|
|
|
|
children.append(child)
|
|
|
|
|
# TODO: use the main text direction of the line
|
|
|
|
|
for old_child in children[:index]:
|
|
|
|
|
if not old_child.is_in_normal_flow():
|
|
|
|
|
continue
|
|
|
|
|
if child.style.float == 'left': # and direction is ltr
|
|
|
|
|
old_child.translate(dx=child.margin_width())
|
|
|
|
|
# elif child.style.float == 'right' and direction is rtl:
|
|
|
|
|
# old_child.translate(dx=-child.margin_width())
|
|
|
|
|
if child.style.float == 'left':
|
|
|
|
|
position_x += child.margin_width()
|
|
|
|
|
elif child.style.float == 'right':
|
|
|
|
|
max_x -= child.margin_width()
|
2012-06-21 18:03:29 +04:00
|
|
|
|
continue
|
2012-05-11 21:31:31 +04:00
|
|
|
|
|
2011-10-05 15:01:44 +04:00
|
|
|
|
new_child, resume_at, preserved = split_inline_level(
|
2012-07-12 19:13:21 +04:00
|
|
|
|
context, child, 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)
|
2011-09-30 21:04:05 +04:00
|
|
|
|
skip_stack = None
|
2011-10-05 15:01:44 +04:00
|
|
|
|
if preserved:
|
|
|
|
|
preserved_line_break = True
|
2011-08-17 16:01:06 +04:00
|
|
|
|
|
2011-08-24 20:06:25 +04:00
|
|
|
|
# TODO: this is non-optimal when last_child is True and
|
2011-08-24 19:47:16 +04:00
|
|
|
|
# width <= remaining_width < width + right_spacing
|
|
|
|
|
# with
|
|
|
|
|
# width = part1.margin_width()
|
2011-08-17 16:01:06 +04:00
|
|
|
|
|
2011-09-30 21:04:05 +04:00
|
|
|
|
# TODO: on the last child, take care of right_spacing
|
2011-08-26 18:21:46 +04:00
|
|
|
|
|
2011-10-05 15:01:44 +04:00
|
|
|
|
if new_child is None:
|
|
|
|
|
# may be None where we would have an empty TextBox
|
|
|
|
|
assert isinstance(child, boxes.TextBox)
|
2011-08-17 16:01:06 +04:00
|
|
|
|
else:
|
2011-10-05 15:01:44 +04:00
|
|
|
|
margin_width = new_child.margin_width()
|
2011-10-17 20:40:18 +04:00
|
|
|
|
new_position_x = position_x + margin_width
|
2011-10-05 15:01:44 +04:00
|
|
|
|
|
2011-10-17 20:40:18 +04:00
|
|
|
|
if (new_position_x > max_x and children):
|
2011-10-05 15:01:44 +04:00
|
|
|
|
# too wide, and the inline is non-empty:
|
|
|
|
|
# put child entirely on the next line.
|
|
|
|
|
resume_at = (index, None)
|
|
|
|
|
break
|
|
|
|
|
else:
|
2011-10-17 20:40:18 +04:00
|
|
|
|
position_x = new_position_x
|
2011-10-05 15:01:44 +04:00
|
|
|
|
children.append(new_child)
|
2011-08-17 16:01:06 +04:00
|
|
|
|
|
2011-09-30 21:04:05 +04:00
|
|
|
|
if resume_at is not None:
|
|
|
|
|
resume_at = (index, resume_at)
|
|
|
|
|
break
|
|
|
|
|
else:
|
|
|
|
|
resume_at = None
|
2011-08-17 16:01:06 +04:00
|
|
|
|
|
2012-05-21 16:22:32 +04:00
|
|
|
|
new_box = box.copy_with_children(
|
|
|
|
|
children, is_start=is_start, is_end=resume_at is None)
|
2012-02-03 03:43:18 +04:00
|
|
|
|
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.
|
|
|
|
|
# This is important for justified text.
|
|
|
|
|
new_box.width = position_x - new_box.position_x
|
|
|
|
|
else:
|
|
|
|
|
new_box.position_x = initial_position_x
|
|
|
|
|
new_box.width = position_x - content_box_left
|
2011-10-19 17:14:43 +04:00
|
|
|
|
|
2012-06-08 18:10:35 +04:00
|
|
|
|
line_height, new_box.baseline = strut_layout(box.style)
|
|
|
|
|
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
|
2011-12-30 20:19:02 +04:00
|
|
|
|
new_box.margin_top = (half_leading - new_box.border_top_width -
|
2011-10-19 17:14:43 +04:00
|
|
|
|
new_box.padding_bottom)
|
2011-12-30 20:19:02 +04:00
|
|
|
|
new_box.margin_bottom = (half_leading - new_box.border_bottom_width -
|
2011-10-19 17:14:43 +04:00
|
|
|
|
new_box.padding_bottom)
|
|
|
|
|
|
2012-05-11 21:31:31 +04:00
|
|
|
|
if new_box.style.position == 'relative':
|
|
|
|
|
for absolute_box in absolute_boxes:
|
2012-07-12 19:13:21 +04:00
|
|
|
|
absolute_layout(context, absolute_box, new_box, fixed_boxes)
|
2012-06-28 16:00:27 +04:00
|
|
|
|
return new_box, resume_at, preserved_line_break
|
2011-08-24 12:48:42 +04:00
|
|
|
|
|
|
|
|
|
|
2012-11-24 19:14:49 +04:00
|
|
|
|
def split_text_box(context, box, available_width, line_width, skip):
|
2011-09-30 21:04:05 +04:00
|
|
|
|
"""Keep as much text as possible from a TextBox in a limitied 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
|
|
|
|
|
2011-10-19 17:14:43 +04:00
|
|
|
|
Return ``(new_box, skip)``. ``skip`` is the number of UTF-8 bytes
|
2011-09-30 21:04:05 +04:00
|
|
|
|
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
|
|
|
|
|
2011-09-30 21:04:05 +04:00
|
|
|
|
Also break an preserved whitespace.
|
2011-08-24 12:48:42 +04:00
|
|
|
|
|
2011-08-17 16:01:06 +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:]
|
2011-12-28 18:34:30 +04:00
|
|
|
|
if font_size == 0 or not text:
|
2011-10-05 15:01:44 +04:00
|
|
|
|
return None, None, False
|
2011-11-08 16:09:19 +04:00
|
|
|
|
# XXX ``resume_at`` is an index in UTF-8 bytes, not unicode codepoints.
|
2012-06-21 17:10:17 +04:00
|
|
|
|
layout, length, resume_at, width, height, baseline = split_first_line(
|
2012-11-24 19:14:49 +04:00
|
|
|
|
text, box.style, context.enable_hinting, available_width, line_width)
|
2011-10-08 20:31:15 +04:00
|
|
|
|
|
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 what’s 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.
|
|
|
|
|
partial_text = text[:resume_at or length]
|
|
|
|
|
utf8_text = partial_text.encode('utf8')
|
|
|
|
|
new_text = utf8_text[:length].decode('utf8')
|
|
|
|
|
new_length = len(new_text)
|
|
|
|
|
if resume_at is not None:
|
2012-12-09 00:43:16 +04:00
|
|
|
|
if length > resume_at:
|
|
|
|
|
# Text has been hyphenated
|
|
|
|
|
new_text += '-'
|
|
|
|
|
between = ''
|
|
|
|
|
else:
|
|
|
|
|
between = utf8_text[length:resume_at].decode('utf8')
|
|
|
|
|
resume_at = new_length + len(between)
|
2011-11-08 16:09:19 +04:00
|
|
|
|
length = new_length
|
|
|
|
|
|
2011-10-08 20:31:15 +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
|
2012-06-21 17:10:17 +04:00
|
|
|
|
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
|
2012-06-08 18:10:35 +04:00
|
|
|
|
line_height, _ = strut_layout(box.style)
|
|
|
|
|
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
|
2012-06-08 18:10:35 +04:00
|
|
|
|
box.baseline += box.margin_top
|
|
|
|
|
assert box.border_top_width == box.padding_top == 0
|
2011-10-08 20:31:15 +04:00
|
|
|
|
else:
|
2011-10-19 17:14:43 +04:00
|
|
|
|
box = None
|
2011-10-08 20:31:15 +04:00
|
|
|
|
|
|
|
|
|
if resume_at is None:
|
|
|
|
|
preserved_line_break = False
|
2011-10-05 15:01:44 +04:00
|
|
|
|
else:
|
2012-12-09 00:43:16 +04:00
|
|
|
|
preserved_line_break = (length != resume_at) and between.strip(' ')
|
2011-10-08 20:31:15 +04:00
|
|
|
|
if preserved_line_break:
|
2012-06-01 11:45:13 +04:00
|
|
|
|
# See http://unicode.org/reports/tr14/
|
|
|
|
|
# TODO: are there others? Find Pango docs on this
|
2012-11-22 23:55:09 +04:00
|
|
|
|
# The space is in this list, as it may have been removed by the
|
|
|
|
|
# Step #2 of split_first_line
|
|
|
|
|
assert between in (' ', '\n', '\u2029'), (
|
2012-06-01 11:45:13 +04:00
|
|
|
|
'Got %r between two lines. '
|
2011-10-08 20:31:15 +04:00
|
|
|
|
'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
|
|
|
|
|
|
|
|
|
|
|
2012-06-08 18:10:35 +04:00
|
|
|
|
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).
|
2012-06-08 18:10:35 +04:00
|
|
|
|
|
|
|
|
|
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 = []
|
|
|
|
|
subtrees_with_min_max = []
|
|
|
|
|
max_y, min_y = aligned_subtree_verticality(
|
|
|
|
|
box, top_bottom_subtrees, baseline_y=0)
|
|
|
|
|
for subtree in top_bottom_subtrees:
|
2012-06-21 18:03:29 +04:00
|
|
|
|
if subtree.is_floated():
|
|
|
|
|
sub_min_y = None
|
|
|
|
|
sub_max_y = None
|
|
|
|
|
else:
|
|
|
|
|
sub_max_y, sub_min_y = aligned_subtree_verticality(
|
|
|
|
|
subtree, top_bottom_subtrees, baseline_y=0)
|
2012-06-08 20:38:19 +04:00
|
|
|
|
subtrees_with_min_max.append(
|
|
|
|
|
(subtree, sub_max_y, sub_min_y))
|
|
|
|
|
|
|
|
|
|
if subtrees_with_min_max:
|
2012-06-21 18:03:29 +04:00
|
|
|
|
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
|
2012-06-21 18:03:29 +04:00
|
|
|
|
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:
|
2012-06-21 18:03:29 +04:00
|
|
|
|
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'
|
|
|
|
|
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'):
|
|
|
|
|
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:
|
2012-06-08 18:10:35 +04:00
|
|
|
|
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
|
|
|
|
|
2012-06-08 18:10:35 +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`.
|
|
|
|
|
|
2011-11-23 21:23:15 +04:00
|
|
|
|
Place all boxes vertically assuming that the baseline of ``box``
|
|
|
|
|
is at `y = baseline_y`.
|
2011-08-17 16:01:06 +04:00
|
|
|
|
|
2011-10-19 17:14:43 +04:00
|
|
|
|
Return ``(max_y, min_y)``, the maximum and minimum vertical position
|
|
|
|
|
of margin boxes.
|
2011-08-17 16:01:06 +04:00
|
|
|
|
|
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
|
|
|
|
|
|
2011-08-19 12:39:31 +04:00
|
|
|
|
for child in box.children:
|
2012-05-10 22:12:47 +04:00
|
|
|
|
if not child.is_in_normal_flow():
|
2012-06-21 18:03:29 +04:00
|
|
|
|
if child.is_floated():
|
|
|
|
|
top_bottom_subtrees.append(child)
|
2012-05-10 22:12:47 +04:00
|
|
|
|
continue
|
2011-11-23 21:23:33 +04:00
|
|
|
|
vertical_align = child.style.vertical_align
|
|
|
|
|
if vertical_align == 'baseline':
|
|
|
|
|
child_baseline_y = baseline_y
|
|
|
|
|
elif vertical_align == 'middle':
|
|
|
|
|
# TODO: find ex from font metrics
|
|
|
|
|
one_ex = box.style.font_size * 0.5
|
|
|
|
|
top = baseline_y - (one_ex + child.margin_height()) / 2.
|
|
|
|
|
child_baseline_y = top + child.baseline
|
2011-11-24 15:40:52 +04:00
|
|
|
|
# TODO: actually implement vertical-align: top and bottom
|
2012-06-08 20:38:19 +04:00
|
|
|
|
elif vertical_align == 'text-top':
|
2011-11-24 14:00:33 +04:00
|
|
|
|
# align top with the top of the parent’s content area
|
|
|
|
|
top = (baseline_y - box.baseline + box.margin_top +
|
2011-12-30 20:19:02 +04:00
|
|
|
|
box.border_top_width + box.padding_top)
|
2011-11-24 14:00:33 +04:00
|
|
|
|
child_baseline_y = top + child.baseline
|
2012-06-08 20:38:19 +04:00
|
|
|
|
elif vertical_align == 'text-bottom':
|
2011-11-24 14:00:33 +04:00
|
|
|
|
# align bottom with the bottom of the parent’s content area
|
|
|
|
|
bottom = (baseline_y - box.baseline + box.margin_top +
|
2011-12-30 20:19:02 +04:00
|
|
|
|
box.border_top_width + box.padding_top + box.height)
|
2011-11-24 14:00:33 +04:00
|
|
|
|
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.
|
2012-10-01 21:30:25 +04:00
|
|
|
|
child_baseline_y = 0
|
2011-11-23 21:23:33 +04:00
|
|
|
|
else:
|
|
|
|
|
# Numeric value: The child’s baseline is `vertical_align` above
|
|
|
|
|
# (lower y) the parent’s baseline.
|
|
|
|
|
child_baseline_y = baseline_y - vertical_align
|
2012-10-01 21:30:25 +04:00
|
|
|
|
|
2011-11-23 21:23:15 +04:00
|
|
|
|
# the child’s `top` is `child.baseline` above (lower y) its baseline.
|
|
|
|
|
top = child_baseline_y - child.baseline
|
2012-04-10 14:49:54 +04:00
|
|
|
|
if isinstance(child, boxes.InlineBlockBox):
|
|
|
|
|
# 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
|
2012-10-01 21:30:25 +04:00
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
2011-11-05 03:32:33 +04:00
|
|
|
|
bottom = top + child.margin_height()
|
2011-11-14 17:42:06 +04:00
|
|
|
|
if min_y is None or top < min_y:
|
|
|
|
|
min_y = top
|
|
|
|
|
if max_y is None or bottom > max_y:
|
|
|
|
|
max_y = bottom
|
2011-08-19 12:39:31 +04:00
|
|
|
|
if isinstance(child, boxes.InlineBox):
|
2011-11-23 21:23:15 +04:00
|
|
|
|
children_max_y, children_min_y = inline_box_verticality(
|
2012-06-08 20:38:19 +04:00
|
|
|
|
child, top_bottom_subtrees, child_baseline_y)
|
2011-11-29 20:19:43 +04:00
|
|
|
|
if children_max_y is None:
|
|
|
|
|
if (
|
|
|
|
|
child.margin_width() == 0
|
|
|
|
|
# Guard against the case where a negative margin
|
|
|
|
|
# compensates something else.
|
|
|
|
|
and child.margin_left == 0
|
|
|
|
|
and child.margin_right == 0
|
|
|
|
|
):
|
|
|
|
|
# No content, ignore this box’s line-height.
|
|
|
|
|
# See http://www.w3.org/TR/CSS21/visuren.html#phantom-line-box
|
|
|
|
|
child.position_y = child_baseline_y
|
|
|
|
|
child.height = 0
|
|
|
|
|
continue
|
|
|
|
|
else:
|
|
|
|
|
assert children_min_y is not None
|
|
|
|
|
if children_min_y < min_y:
|
|
|
|
|
min_y = children_min_y
|
|
|
|
|
if children_max_y > max_y:
|
|
|
|
|
max_y = children_max_y
|
2011-10-19 17:14:43 +04:00
|
|
|
|
return max_y, min_y
|
2011-10-20 20:37:44 +04:00
|
|
|
|
|
|
|
|
|
|
2012-07-12 19:13:21 +04:00
|
|
|
|
def text_align(context, line, available_width, last):
|
2011-10-20 20:37:44 +04:00
|
|
|
|
"""Return how much the line should be moved horizontally according to
|
|
|
|
|
the `text-align` property.
|
|
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
align = line.style.text_align
|
2011-12-16 19:02:49 +04:00
|
|
|
|
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:
|
2012-01-26 14:33:49 +04:00
|
|
|
|
align = 'right' if line.style.direction == 'rtl' else 'left'
|
2011-10-20 20:37:44 +04:00
|
|
|
|
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':
|
2012-07-12 19:13:21 +04:00
|
|
|
|
justify_line(context, line, offset)
|
2012-02-01 19:13:47 +04:00
|
|
|
|
return 0
|
2011-10-20 20:37:44 +04:00
|
|
|
|
if align == 'center':
|
|
|
|
|
offset /= 2.
|
|
|
|
|
else:
|
|
|
|
|
assert align == 'right'
|
|
|
|
|
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):
|
2012-02-01 19:13:47 +04:00
|
|
|
|
nb_spaces = count_spaces(line)
|
|
|
|
|
if nb_spaces == 0:
|
|
|
|
|
# TODO: what should we do with single-word lines?
|
|
|
|
|
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
|
|
|
|
|
|
|
|
|
|
|
2012-07-12 19:13:21 +04:00
|
|
|
|
def add_word_spacing(context, box, extra_word_spacing, x_advance):
|
2012-02-01 19:13:47 +04:00
|
|
|
|
if isinstance(box, boxes.TextBox):
|
|
|
|
|
box.position_x += x_advance
|
|
|
|
|
box.style.word_spacing += extra_word_spacing
|
|
|
|
|
nb_spaces = count_spaces(box)
|
|
|
|
|
if nb_spaces > 0:
|
|
|
|
|
new_box, resume_at, _ = split_text_box(
|
2012-11-24 19:14:49 +04:00
|
|
|
|
context, box, 1e10, None, 0)
|
2012-02-01 19:13:47 +04:00
|
|
|
|
assert new_box is not None
|
|
|
|
|
assert resume_at is None
|
|
|
|
|
# XXX new_box.width - box.width is always 0???
|
|
|
|
|
#x_advance += new_box.width - box.width
|
|
|
|
|
x_advance += extra_word_spacing * nb_spaces
|
|
|
|
|
box.width = new_box.width
|
2012-06-21 17:10:17 +04:00
|
|
|
|
box.pango_layout = new_box.pango_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:
|
|
|
|
|
x_advance = add_word_spacing(
|
2012-07-12 19:13:21 +04:00
|
|
|
|
context, child, extra_word_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
|