2012-03-22 02:19:27 +04:00
|
|
|
"""
|
|
|
|
weasyprint.layout.preferred
|
|
|
|
---------------------------
|
2012-01-03 13:56:02 +04:00
|
|
|
|
2016-01-09 17:16:29 +03:00
|
|
|
Preferred and minimum preferred width, aka. max-content and min-content
|
|
|
|
width, aka. the shrink-to-fit algorithm.
|
2012-01-03 13:56:02 +04:00
|
|
|
|
2016-01-09 17:16:29 +03:00
|
|
|
Terms used (max-content width, min-content width) are defined in David
|
|
|
|
Baron's unofficial draft (http://dbaron.org/css/intrinsic/).
|
|
|
|
|
2018-09-20 19:41:37 +03:00
|
|
|
:copyright: Copyright 2011-2018 Simon Sapin and contributors, see AUTHORS.
|
2012-03-22 02:19:27 +04:00
|
|
|
:license: BSD, see LICENSE for details.
|
2012-01-03 13:56:02 +04:00
|
|
|
|
|
|
|
"""
|
|
|
|
|
2016-04-12 01:56:36 +03:00
|
|
|
import sys
|
2012-09-13 13:46:13 +04:00
|
|
|
|
2012-06-21 17:10:17 +04:00
|
|
|
from .. import text
|
2017-03-25 02:33:36 +03:00
|
|
|
from ..formatting_structure import boxes
|
2014-05-23 16:50:39 +04:00
|
|
|
from .replaced import default_image_sizing
|
2012-03-23 20:33:59 +04:00
|
|
|
|
|
|
|
|
2012-07-12 19:13:21 +04:00
|
|
|
def shrink_to_fit(context, box, available_width):
|
2012-03-23 22:31:54 +04:00
|
|
|
"""Return the shrink-to-fit width of ``box``.
|
|
|
|
|
2012-05-30 21:06:09 +04:00
|
|
|
*Warning:* both available_outer_width and the return value are
|
|
|
|
for width of the *content area*, not margin area.
|
|
|
|
|
2012-03-23 22:31:54 +04:00
|
|
|
http://www.w3.org/TR/CSS21/visudet.html#float-width
|
|
|
|
|
|
|
|
"""
|
|
|
|
return min(
|
2012-06-02 10:28:41 +04:00
|
|
|
max(
|
2016-01-09 17:16:29 +03:00
|
|
|
min_content_width(context, box, outer=False),
|
2012-06-02 10:28:41 +04:00
|
|
|
available_width),
|
2016-01-09 17:16:29 +03:00
|
|
|
max_content_width(context, box, outer=False))
|
2012-03-22 21:36:56 +04:00
|
|
|
|
2012-03-22 22:32:59 +04:00
|
|
|
|
2016-01-09 17:16:29 +03:00
|
|
|
def min_content_width(context, box, outer=True):
|
|
|
|
"""Return the min-content width for ``box``.
|
2012-01-03 13:56:02 +04:00
|
|
|
|
|
|
|
This is the width by breaking at every line-break opportunity.
|
|
|
|
|
2012-03-22 22:32:59 +04:00
|
|
|
"""
|
2017-12-23 02:19:32 +03:00
|
|
|
if isinstance(box, (
|
|
|
|
boxes.BlockContainerBox, boxes.TableColumnBox, boxes.FlexBox)):
|
2012-04-10 16:37:54 +04:00
|
|
|
if box.is_table_wrapper:
|
2016-01-29 20:50:51 +03:00
|
|
|
return table_and_columns_preferred_widths(context, box, outer)[0]
|
2012-04-10 16:37:54 +04:00
|
|
|
else:
|
2016-01-09 17:16:29 +03:00
|
|
|
return block_min_content_width(context, box, outer)
|
2016-11-02 01:55:21 +03:00
|
|
|
elif isinstance(box, boxes.TableColumnGroupBox):
|
|
|
|
return column_group_content_width(context, box)
|
2012-03-23 02:26:09 +04:00
|
|
|
elif isinstance(box, (boxes.InlineBox, boxes.LineBox)):
|
2016-01-09 17:16:29 +03:00
|
|
|
return inline_min_content_width(
|
2013-04-22 19:26:41 +04:00
|
|
|
context, box, outer, is_line_start=True)
|
2012-05-25 16:08:35 +04:00
|
|
|
elif isinstance(box, boxes.ReplacedBox):
|
2016-01-11 06:30:38 +03:00
|
|
|
return replaced_min_content_width(box, outer)
|
2018-02-24 06:41:11 +03:00
|
|
|
elif isinstance(box, boxes.FlexContainerBox):
|
|
|
|
return flex_min_content_width(context, box, outer)
|
2012-03-22 22:32:59 +04:00
|
|
|
else:
|
|
|
|
raise TypeError(
|
2016-01-09 17:16:29 +03:00
|
|
|
'min-content width for %s not handled yet' %
|
2012-03-22 22:32:59 +04:00
|
|
|
type(box).__name__)
|
|
|
|
|
|
|
|
|
2016-01-09 17:16:29 +03:00
|
|
|
def max_content_width(context, box, outer=True):
|
|
|
|
"""Return the max-content width for ``box``.
|
2012-03-22 22:32:59 +04:00
|
|
|
|
|
|
|
This is the width by only breaking at forced line breaks.
|
|
|
|
|
|
|
|
"""
|
2017-12-23 02:19:32 +03:00
|
|
|
if isinstance(box, (
|
|
|
|
boxes.BlockContainerBox, boxes.TableColumnBox, boxes.FlexBox)):
|
2012-04-10 16:37:54 +04:00
|
|
|
if box.is_table_wrapper:
|
2016-01-29 02:39:51 +03:00
|
|
|
return table_and_columns_preferred_widths(context, box, outer)[1]
|
2012-04-10 16:37:54 +04:00
|
|
|
else:
|
2016-01-09 17:16:29 +03:00
|
|
|
return block_max_content_width(context, box, outer)
|
2016-11-02 01:55:21 +03:00
|
|
|
elif isinstance(box, boxes.TableColumnGroupBox):
|
|
|
|
return column_group_content_width(context, box)
|
2012-03-23 02:26:09 +04:00
|
|
|
elif isinstance(box, (boxes.InlineBox, boxes.LineBox)):
|
2016-01-09 17:16:29 +03:00
|
|
|
return inline_max_content_width(
|
|
|
|
context, box, outer, is_line_start=True)
|
2012-05-25 16:08:35 +04:00
|
|
|
elif isinstance(box, boxes.ReplacedBox):
|
2018-02-16 02:54:34 +03:00
|
|
|
return replaced_max_content_width(box, outer)
|
2018-02-24 06:41:11 +03:00
|
|
|
elif isinstance(box, boxes.FlexContainerBox):
|
|
|
|
return flex_max_content_width(context, box, outer)
|
2012-03-22 22:32:59 +04:00
|
|
|
else:
|
|
|
|
raise TypeError(
|
2016-01-09 17:16:29 +03:00
|
|
|
'max-content width for %s not handled yet' % type(box).__name__)
|
2012-03-22 22:32:59 +04:00
|
|
|
|
|
|
|
|
2016-01-09 17:16:29 +03:00
|
|
|
def _block_content_width(context, box, function, outer):
|
|
|
|
"""Helper to create ``block_*_content_width.``"""
|
2018-01-13 19:05:23 +03:00
|
|
|
width = box.style['width']
|
2012-04-05 15:16:17 +04:00
|
|
|
if width == 'auto' or width.unit == '%':
|
|
|
|
# "percentages on the following properties are treated instead as
|
2016-01-29 02:39:51 +03:00
|
|
|
# though they were the following: width: auto"
|
2012-04-05 15:16:17 +04:00
|
|
|
# http://dbaron.org/css/intrinsic/#outer-intrinsic
|
2012-05-25 17:57:13 +04:00
|
|
|
children_widths = [
|
2012-07-12 19:13:21 +04:00
|
|
|
function(context, child, outer=True) for child in box.children
|
2012-05-31 23:05:10 +04:00
|
|
|
if not child.is_absolutely_positioned()]
|
2012-05-25 17:57:13 +04:00
|
|
|
width = max(children_widths) if children_widths else 0
|
2012-03-23 20:33:59 +04:00
|
|
|
else:
|
2012-04-05 15:16:17 +04:00
|
|
|
assert width.unit == 'px'
|
|
|
|
width = width.value
|
|
|
|
|
|
|
|
return adjust(box, outer, width)
|
|
|
|
|
2012-04-04 16:25:13 +04:00
|
|
|
|
2016-04-01 20:28:19 +03:00
|
|
|
def min_max(box, width):
|
|
|
|
"""Get box width from given width and box min- and max-widths."""
|
2018-01-13 19:05:23 +03:00
|
|
|
min_width = box.style['min_width']
|
|
|
|
max_width = box.style['max_width']
|
2018-03-06 03:25:47 +03:00
|
|
|
if min_width == 'auto' or min_width.unit == '%':
|
2018-03-06 03:00:45 +03:00
|
|
|
min_width = 0
|
|
|
|
else:
|
|
|
|
min_width = min_width.value
|
2018-03-06 03:25:47 +03:00
|
|
|
if max_width == 'auto' or max_width.unit == '%':
|
2018-03-06 03:00:45 +03:00
|
|
|
max_width = float('inf')
|
|
|
|
else:
|
|
|
|
max_width = max_width.value
|
2016-04-01 20:28:19 +03:00
|
|
|
return max(min_width, min(width, max_width))
|
2012-04-10 14:00:31 +04:00
|
|
|
|
2016-01-21 15:47:16 +03:00
|
|
|
|
2016-04-01 20:28:19 +03:00
|
|
|
def margin_width(box, width, left=True, right=True):
|
|
|
|
"""Add box paddings, borders and margins to ``width``."""
|
2012-04-10 14:00:31 +04:00
|
|
|
percentages = 0
|
|
|
|
|
2013-04-23 20:49:44 +04:00
|
|
|
for value in (
|
|
|
|
(['margin_left', 'padding_left'] if left else []) +
|
|
|
|
(['margin_right', 'padding_right'] if right else [])
|
|
|
|
):
|
2012-04-10 14:00:31 +04:00
|
|
|
style_value = box.style[value]
|
|
|
|
if style_value != 'auto':
|
|
|
|
if style_value.unit == 'px':
|
2016-04-01 20:28:19 +03:00
|
|
|
width += style_value.value
|
2012-04-10 14:00:31 +04:00
|
|
|
else:
|
|
|
|
assert style_value.unit == '%'
|
|
|
|
percentages += style_value.value
|
|
|
|
|
2013-04-23 20:49:44 +04:00
|
|
|
if left:
|
2018-01-13 19:05:23 +03:00
|
|
|
width += box.style['border_left_width']
|
2013-04-23 20:49:44 +04:00
|
|
|
if right:
|
2018-01-13 19:05:23 +03:00
|
|
|
width += box.style['border_right_width']
|
2012-04-10 21:00:56 +04:00
|
|
|
|
2012-04-12 17:11:33 +04:00
|
|
|
if percentages < 100:
|
2016-04-01 20:28:19 +03:00
|
|
|
return width / (1 - percentages / 100.)
|
2012-03-23 02:26:09 +04:00
|
|
|
else:
|
2012-04-10 14:00:31 +04:00
|
|
|
# Pathological case, ignore
|
2012-03-23 20:33:59 +04:00
|
|
|
return 0
|
|
|
|
|
|
|
|
|
2016-04-01 20:28:19 +03:00
|
|
|
def adjust(box, outer, width, left=True, right=True):
|
|
|
|
"""Respect min/max and adjust width depending on ``outer``.
|
|
|
|
|
|
|
|
If ``outer`` is set to ``True``, return margin width, else return content
|
|
|
|
width.
|
|
|
|
|
|
|
|
"""
|
|
|
|
fixed = min_max(box, width)
|
|
|
|
|
|
|
|
if outer:
|
|
|
|
return margin_width(box, fixed, left, right)
|
|
|
|
else:
|
|
|
|
return fixed
|
|
|
|
|
|
|
|
|
2016-01-09 17:16:29 +03:00
|
|
|
def block_min_content_width(context, box, outer=True):
|
|
|
|
"""Return the min-content width for a ``BlockBox``."""
|
|
|
|
return _block_content_width(
|
|
|
|
context, box, min_content_width, outer)
|
2012-03-22 22:32:59 +04:00
|
|
|
|
|
|
|
|
2016-01-09 17:16:29 +03:00
|
|
|
def block_max_content_width(context, box, outer=True):
|
|
|
|
"""Return the max-content width for a ``BlockBox``."""
|
|
|
|
return _block_content_width(context, box, max_content_width, outer)
|
2012-03-22 22:32:59 +04:00
|
|
|
|
|
|
|
|
2016-01-09 17:16:29 +03:00
|
|
|
def inline_min_content_width(context, box, outer=True, skip_stack=None,
|
|
|
|
first_line=False, is_line_start=False):
|
|
|
|
"""Return the min-content width for an ``InlineBox``.
|
2012-05-31 04:05:35 +04:00
|
|
|
|
|
|
|
The width is calculated from the lines from ``skip_stack``. If
|
|
|
|
``first_line`` is ``True``, only the first line minimum width is
|
|
|
|
calculated.
|
|
|
|
|
|
|
|
"""
|
2017-07-07 15:26:38 +03:00
|
|
|
widths = inline_line_widths(
|
2013-04-26 17:42:15 +04:00
|
|
|
context, box, outer, is_line_start, minimum=True,
|
2017-07-24 00:11:55 +03:00
|
|
|
skip_stack=skip_stack, first_line=first_line)
|
2013-04-26 17:42:15 +04:00
|
|
|
|
2017-07-07 15:26:38 +03:00
|
|
|
if first_line:
|
|
|
|
widths = [next(widths)]
|
2013-04-26 17:42:15 +04:00
|
|
|
else:
|
2017-07-07 15:26:38 +03:00
|
|
|
widths = list(widths)
|
2013-04-26 17:42:15 +04:00
|
|
|
widths[-1] -= trailing_whitespace_size(context, box)
|
|
|
|
return adjust(box, outer, max(widths))
|
2012-01-03 13:56:02 +04:00
|
|
|
|
|
|
|
|
2016-01-09 17:16:29 +03:00
|
|
|
def inline_max_content_width(context, box, outer=True, is_line_start=False):
|
|
|
|
"""Return the max-content width for an ``InlineBox``."""
|
2013-04-26 17:42:15 +04:00
|
|
|
widths = list(
|
|
|
|
inline_line_widths(context, box, outer, is_line_start, minimum=False))
|
|
|
|
widths[-1] -= trailing_whitespace_size(context, box)
|
|
|
|
return adjust(box, outer, max(widths))
|
2013-04-23 20:49:44 +04:00
|
|
|
|
|
|
|
|
2016-11-02 01:55:21 +03:00
|
|
|
def column_group_content_width(context, box):
|
|
|
|
"""Return the *-content width for an ``TableColumnGroupBox``."""
|
2018-01-13 19:05:23 +03:00
|
|
|
width = box.style['width']
|
2016-11-02 01:55:21 +03:00
|
|
|
if width == 'auto' or width.unit == '%':
|
|
|
|
width = 0
|
|
|
|
else:
|
|
|
|
assert width.unit == 'px'
|
|
|
|
width = width.value
|
|
|
|
|
|
|
|
return adjust(box, False, width)
|
|
|
|
|
|
|
|
|
2013-04-26 17:00:21 +04:00
|
|
|
def inline_line_widths(context, box, outer, is_line_start, minimum,
|
2017-07-24 00:11:55 +03:00
|
|
|
skip_stack=None, first_line=False):
|
2018-08-29 17:36:10 +03:00
|
|
|
if box.style['text_indent'].unit == '%':
|
|
|
|
# TODO: this is wrong, text-indent percentages should be resolved
|
|
|
|
# before calling this function.
|
|
|
|
text_indent = 0
|
|
|
|
else:
|
|
|
|
text_indent = box.style['text_indent'].value
|
2012-01-03 13:56:02 +04:00
|
|
|
current_line = 0
|
2013-04-23 21:55:21 +04:00
|
|
|
if skip_stack is None:
|
|
|
|
skip = 0
|
|
|
|
else:
|
|
|
|
skip, skip_stack = skip_stack
|
|
|
|
for index, child in box.enumerate_skip(skip):
|
2012-06-28 20:46:54 +04:00
|
|
|
if child.is_absolutely_positioned():
|
2012-05-25 14:03:48 +04:00
|
|
|
continue # Skip
|
|
|
|
|
2012-06-28 21:07:26 +04:00
|
|
|
if isinstance(child, boxes.InlineBox):
|
2017-07-24 00:11:55 +03:00
|
|
|
lines = inline_line_widths(
|
2018-02-13 00:56:03 +03:00
|
|
|
context, child, outer, is_line_start, minimum, skip_stack,
|
|
|
|
first_line)
|
2017-07-24 00:11:55 +03:00
|
|
|
if first_line:
|
|
|
|
lines = [next(lines)]
|
|
|
|
else:
|
|
|
|
lines = list(lines)
|
2013-04-23 20:49:44 +04:00
|
|
|
if len(lines) == 1:
|
|
|
|
lines[0] = adjust(child, outer, lines[0])
|
|
|
|
else:
|
|
|
|
lines[0] = adjust(child, outer, lines[0], right=False)
|
|
|
|
lines[-1] = adjust(child, outer, lines[-1], left=False)
|
2012-06-28 21:07:26 +04:00
|
|
|
elif isinstance(child, boxes.TextBox):
|
2018-01-13 19:05:23 +03:00
|
|
|
space_collapse = child.style['white_space'] in (
|
2016-05-13 20:54:55 +03:00
|
|
|
'normal', 'nowrap', 'pre-line')
|
2013-04-23 21:55:21 +04:00
|
|
|
if skip_stack is None:
|
|
|
|
skip = 0
|
|
|
|
else:
|
|
|
|
skip, skip_stack = skip_stack
|
|
|
|
assert skip_stack is None
|
|
|
|
child_text = child.text[(skip or 0):]
|
2016-05-13 20:54:55 +03:00
|
|
|
if is_line_start and space_collapse:
|
2013-04-23 21:55:21 +04:00
|
|
|
child_text = child_text.lstrip(' ')
|
2013-04-26 17:00:21 +04:00
|
|
|
if minimum and child_text == ' ':
|
2013-04-23 21:55:21 +04:00
|
|
|
lines = [0, 0]
|
|
|
|
else:
|
2017-10-21 17:56:25 +03:00
|
|
|
max_width = 0 if minimum else None
|
|
|
|
lines = []
|
|
|
|
resume_at = new_resume_at = 0
|
|
|
|
while new_resume_at is not None:
|
|
|
|
resume_at += new_resume_at
|
|
|
|
_, _, new_resume_at, width, _, _ = (
|
|
|
|
text.split_first_line(
|
|
|
|
child_text[resume_at:], child.style, context,
|
2018-03-05 03:23:32 +03:00
|
|
|
max_width, child.justification_spacing,
|
|
|
|
minimum=True))
|
2017-10-21 17:56:25 +03:00
|
|
|
lines.append(width)
|
|
|
|
if first_line:
|
|
|
|
break
|
2017-12-23 03:11:16 +03:00
|
|
|
if first_line and new_resume_at:
|
2017-12-24 01:48:13 +03:00
|
|
|
current_line += lines[0]
|
2017-12-23 03:11:16 +03:00
|
|
|
break
|
2012-06-28 21:07:26 +04:00
|
|
|
else:
|
2013-04-26 17:00:21 +04:00
|
|
|
# http://www.w3.org/TR/css3-text/#line-break-details
|
|
|
|
# "The line breaking behavior of a replaced element
|
|
|
|
# or other atomic inline is equivalent to that
|
|
|
|
# of the Object Replacement Character (U+FFFC)."
|
|
|
|
# http://www.unicode.org/reports/tr14/#DescriptionOfProperties
|
|
|
|
# "By default, there is a break opportunity
|
|
|
|
# both before and after any inline object."
|
|
|
|
if minimum:
|
2016-01-09 17:16:29 +03:00
|
|
|
lines = [0, max_content_width(context, child), 0]
|
2013-04-26 17:00:21 +04:00
|
|
|
else:
|
2016-01-09 17:16:29 +03:00
|
|
|
lines = [max_content_width(context, child)]
|
2013-04-23 20:49:44 +04:00
|
|
|
# The first text line goes on the current line
|
|
|
|
current_line += lines[0]
|
|
|
|
if len(lines) > 1:
|
|
|
|
# Forced line break
|
2018-08-29 17:36:10 +03:00
|
|
|
yield current_line + text_indent
|
|
|
|
text_indent = 0
|
2013-04-23 20:49:44 +04:00
|
|
|
if len(lines) > 2:
|
2013-04-26 17:42:15 +04:00
|
|
|
for line in lines[1:-1]:
|
|
|
|
yield line
|
2013-04-23 20:49:44 +04:00
|
|
|
current_line = lines[-1]
|
|
|
|
is_line_start = lines[-1] == 0
|
2013-04-23 21:55:21 +04:00
|
|
|
skip_stack = None
|
2018-08-29 17:36:10 +03:00
|
|
|
yield current_line + text_indent
|
2012-01-03 13:56:02 +04:00
|
|
|
|
|
|
|
|
2016-01-11 06:30:38 +03:00
|
|
|
def _percentage_contribution(box):
|
|
|
|
"""Return the percentage contribution of a cell, column or column group.
|
|
|
|
|
|
|
|
http://dbaron.org/css/intrinsic/#pct-contrib
|
|
|
|
|
|
|
|
"""
|
|
|
|
min_width = (
|
2018-01-13 19:05:23 +03:00
|
|
|
box.style['min_width'].value if box.style['min_width'] != 'auto' and
|
|
|
|
box.style['min_width'].unit == '%' else 0)
|
2016-01-11 06:30:38 +03:00
|
|
|
max_width = (
|
2018-01-13 19:05:23 +03:00
|
|
|
box.style['max_width'].value if box.style['max_width'] != 'auto' and
|
|
|
|
box.style['max_width'].unit == '%' else float('inf'))
|
2016-01-11 06:30:38 +03:00
|
|
|
width = (
|
2018-01-13 19:05:23 +03:00
|
|
|
box.style['width'].value if box.style['width'] != 'auto' and
|
|
|
|
box.style['width'].unit == '%' else 0)
|
2016-01-11 06:30:38 +03:00
|
|
|
return max(min_width, min(width, max_width))
|
2012-04-10 16:37:54 +04:00
|
|
|
|
2016-01-11 06:30:38 +03:00
|
|
|
|
|
|
|
def table_and_columns_preferred_widths(context, box, outer=True):
|
|
|
|
"""Return content widths for the auto layout table and its columns.
|
2012-04-10 20:29:00 +04:00
|
|
|
|
2012-04-10 16:37:54 +04:00
|
|
|
The tuple returned is
|
2016-01-09 17:16:29 +03:00
|
|
|
``(table_min_content_width, table_max_content_width,
|
2016-01-11 06:30:38 +03:00
|
|
|
column_min_content_widths, column_max_content_widths,
|
2016-01-24 19:06:33 +03:00
|
|
|
column_intrinsic_percentages, constrainedness,
|
|
|
|
total_horizontal_border_spacing, grid)``
|
2012-04-10 16:37:54 +04:00
|
|
|
|
2016-01-11 06:30:38 +03:00
|
|
|
http://dbaron.org/css/intrinsic/
|
2013-08-02 20:58:56 +04:00
|
|
|
|
2012-04-10 16:37:54 +04:00
|
|
|
"""
|
2018-09-19 17:23:19 +03:00
|
|
|
# Avoid a circular import
|
|
|
|
from .tables import distribute_excess_width
|
|
|
|
|
2012-04-10 16:37:54 +04:00
|
|
|
table = box.get_wrapped_table()
|
2018-03-25 17:23:59 +03:00
|
|
|
result = context.tables.get(table)
|
2012-09-13 13:46:13 +04:00
|
|
|
if result:
|
2016-01-29 20:50:51 +03:00
|
|
|
return result[outer]
|
2012-09-13 13:46:13 +04:00
|
|
|
|
2016-01-11 06:30:38 +03:00
|
|
|
# Create the grid
|
|
|
|
grid_width, grid_height = 0, 0
|
|
|
|
row_number = 0
|
|
|
|
for row_group in table.children:
|
|
|
|
for row in row_group.children:
|
|
|
|
for cell in row.children:
|
2016-01-24 19:06:33 +03:00
|
|
|
grid_width = max(cell.grid_x + cell.colspan, grid_width)
|
2016-01-11 06:30:38 +03:00
|
|
|
grid_height = max(row_number + cell.rowspan, grid_height)
|
|
|
|
row_number += 1
|
|
|
|
grid = [[None] * grid_width for i in range(grid_height)]
|
|
|
|
row_number = 0
|
|
|
|
for row_group in table.children:
|
|
|
|
for row in row_group.children:
|
|
|
|
for cell in row.children:
|
|
|
|
grid[row_number][cell.grid_x] = cell
|
|
|
|
row_number += 1
|
|
|
|
|
2016-01-24 19:06:33 +03:00
|
|
|
zipped_grid = list(zip(*grid))
|
|
|
|
|
|
|
|
# Define the total horizontal border spacing
|
2018-01-13 19:05:23 +03:00
|
|
|
if table.style['border_collapse'] == 'separate' and grid_width > 0:
|
2016-01-24 19:06:33 +03:00
|
|
|
total_horizontal_border_spacing = (
|
2018-01-13 19:05:23 +03:00
|
|
|
table.style['border_spacing'][0] *
|
2016-01-24 19:06:33 +03:00
|
|
|
(1 + len([column for column in zipped_grid if any(column)])))
|
|
|
|
else:
|
|
|
|
total_horizontal_border_spacing = 0
|
|
|
|
|
2016-01-11 06:30:38 +03:00
|
|
|
if grid_width == 0 or grid_height == 0:
|
2016-01-24 16:35:01 +03:00
|
|
|
table.children = []
|
2016-01-29 20:50:51 +03:00
|
|
|
min_width = block_min_content_width(context, table, outer=False)
|
|
|
|
max_width = block_max_content_width(context, table, outer=False)
|
|
|
|
outer_min_width = adjust(
|
|
|
|
box, outer=True, width=block_min_content_width(
|
|
|
|
context, table, outer=True))
|
|
|
|
outer_max_width = adjust(
|
|
|
|
box, outer=True, width=block_max_content_width(
|
|
|
|
context, table, outer=True))
|
|
|
|
result = ([], [], [], [], total_horizontal_border_spacing, [])
|
2018-03-25 17:23:59 +03:00
|
|
|
context.tables[table] = result = {
|
2016-01-29 20:50:51 +03:00
|
|
|
False: (min_width, max_width) + result,
|
|
|
|
True: (outer_min_width, outer_max_width) + result,
|
|
|
|
}
|
|
|
|
return result[outer]
|
2012-04-10 16:37:54 +04:00
|
|
|
|
2016-01-11 06:30:38 +03:00
|
|
|
column_groups = [None] * grid_width
|
|
|
|
columns = [None] * grid_width
|
2016-01-11 12:54:23 +03:00
|
|
|
column_number = 0
|
2016-01-11 06:30:38 +03:00
|
|
|
for column_group in table.column_groups:
|
|
|
|
for column in column_group.children:
|
2016-01-11 12:54:23 +03:00
|
|
|
column_groups[column_number] = column_group
|
|
|
|
columns[column_number] = column
|
|
|
|
column_number += 1
|
|
|
|
if column_number == grid_width:
|
2016-01-11 06:30:38 +03:00
|
|
|
break
|
|
|
|
else:
|
|
|
|
continue
|
|
|
|
break
|
|
|
|
|
2018-09-19 17:23:19 +03:00
|
|
|
colspan_cells = []
|
|
|
|
|
2016-01-11 06:30:38 +03:00
|
|
|
# Define the intermediate content widths
|
|
|
|
min_content_widths = [0 for i in range(grid_width)]
|
|
|
|
max_content_widths = [0 for i in range(grid_width)]
|
|
|
|
intrinsic_percentages = [0 for i in range(grid_width)]
|
|
|
|
|
|
|
|
# Intermediate content widths for span 1
|
|
|
|
for i in range(grid_width):
|
|
|
|
for groups in (column_groups, columns):
|
|
|
|
if groups[i]:
|
|
|
|
min_content_widths[i] = max(
|
|
|
|
min_content_widths[i],
|
|
|
|
min_content_width(context, groups[i]))
|
|
|
|
max_content_widths[i] = max(
|
|
|
|
max_content_widths[i],
|
|
|
|
max_content_width(context, groups[i]))
|
|
|
|
intrinsic_percentages[i] = max(
|
|
|
|
intrinsic_percentages[i],
|
|
|
|
_percentage_contribution(groups[i]))
|
|
|
|
for cell in zipped_grid[i]:
|
2018-09-19 17:23:19 +03:00
|
|
|
if cell:
|
|
|
|
if cell.colspan == 1:
|
|
|
|
min_content_widths[i] = max(
|
|
|
|
min_content_widths[i],
|
|
|
|
min_content_width(context, cell))
|
|
|
|
max_content_widths[i] = max(
|
|
|
|
max_content_widths[i],
|
|
|
|
max_content_width(context, cell))
|
|
|
|
intrinsic_percentages[i] = max(
|
|
|
|
intrinsic_percentages[i],
|
|
|
|
_percentage_contribution(cell))
|
|
|
|
else:
|
|
|
|
colspan_cells.append(cell)
|
2016-01-11 06:30:38 +03:00
|
|
|
|
2018-09-19 17:23:19 +03:00
|
|
|
# Intermediate content widths for span > 1 is wrong in the 4.1 section, as
|
|
|
|
# explained in its third issue. Min- and max-content widths are handled by
|
|
|
|
# the excess width distribution method, and percentages do not distribute
|
|
|
|
# widths to columns that have originating cells.
|
|
|
|
|
|
|
|
# Intermediate intrinsic percentage widths for span > 1
|
2016-01-11 06:30:38 +03:00
|
|
|
for span in range(1, grid_width):
|
|
|
|
percentage_contributions = []
|
2016-01-12 06:53:59 +03:00
|
|
|
for i in range(grid_width):
|
2016-01-11 06:30:38 +03:00
|
|
|
percentage_contribution = intrinsic_percentages[i]
|
|
|
|
for j, cell in enumerate(zipped_grid[i]):
|
2016-01-12 06:53:59 +03:00
|
|
|
indexes = [k for k in range(i + 1) if grid[j][k]]
|
|
|
|
if not indexes:
|
|
|
|
continue
|
|
|
|
origin = max(indexes)
|
|
|
|
origin_cell = grid[j][origin]
|
|
|
|
if origin_cell.colspan - 1 != span:
|
|
|
|
continue
|
|
|
|
cell_slice = slice(origin, origin + origin_cell.colspan)
|
2018-09-18 16:32:42 +03:00
|
|
|
baseline_percentage = sum(intrinsic_percentages[cell_slice])
|
2016-01-12 06:53:59 +03:00
|
|
|
|
2018-08-09 18:59:47 +03:00
|
|
|
# Cell contribution to intrinsic percentage width
|
2016-01-12 06:53:59 +03:00
|
|
|
if intrinsic_percentages[i] == 0:
|
|
|
|
diff = max(
|
2016-01-11 06:30:38 +03:00
|
|
|
0,
|
2016-01-12 06:53:59 +03:00
|
|
|
_percentage_contribution(origin_cell) -
|
|
|
|
baseline_percentage)
|
|
|
|
other_columns_contributions = [
|
|
|
|
max_content_widths[j]
|
|
|
|
for j in range(
|
|
|
|
origin, origin + origin_cell.colspan)
|
|
|
|
if intrinsic_percentages[j] == 0]
|
|
|
|
other_columns_contributions_sum = sum(
|
|
|
|
other_columns_contributions)
|
|
|
|
if other_columns_contributions_sum == 0:
|
|
|
|
ratio = 1 / len(other_columns_contributions)
|
2016-01-11 06:30:38 +03:00
|
|
|
else:
|
2016-01-12 06:53:59 +03:00
|
|
|
ratio = (
|
|
|
|
max_content_widths[i] /
|
|
|
|
other_columns_contributions_sum)
|
2018-08-10 19:02:59 +03:00
|
|
|
percentage_contribution = max(
|
|
|
|
percentage_contribution,
|
|
|
|
diff * ratio)
|
2016-01-11 06:30:38 +03:00
|
|
|
|
|
|
|
percentage_contributions.append(percentage_contribution)
|
|
|
|
|
2016-01-12 06:53:59 +03:00
|
|
|
intrinsic_percentages = percentage_contributions
|
2016-01-11 06:30:38 +03:00
|
|
|
|
2018-09-19 17:23:19 +03:00
|
|
|
# Define constrainedness
|
|
|
|
constrainedness = [False for i in range(grid_width)]
|
|
|
|
for i in range(grid_width):
|
|
|
|
if (column_groups[i] and column_groups[i].style['width'] != 'auto' and
|
|
|
|
column_groups[i].style['width'].unit != '%'):
|
|
|
|
constrainedness[i] = True
|
|
|
|
continue
|
|
|
|
if (columns[i] and columns[i].style['width'] != 'auto' and
|
|
|
|
columns[i].style['width'].unit != '%'):
|
|
|
|
constrainedness[i] = True
|
|
|
|
continue
|
|
|
|
for cell in zipped_grid[i]:
|
|
|
|
if (cell and cell.colspan == 1 and
|
|
|
|
cell.style['width'] != 'auto' and
|
|
|
|
cell.style['width'].unit != '%'):
|
|
|
|
constrainedness[i] = True
|
|
|
|
break
|
|
|
|
|
2016-01-11 06:30:38 +03:00
|
|
|
intrinsic_percentages = [
|
|
|
|
min(percentage, 100 - sum(intrinsic_percentages[:i]))
|
|
|
|
for i, percentage in enumerate(intrinsic_percentages)]
|
|
|
|
|
2018-09-19 17:23:19 +03:00
|
|
|
# Max- and min-content widths for span > 1
|
|
|
|
for cell in colspan_cells:
|
|
|
|
min_content = min_content_width(context, cell)
|
|
|
|
max_content = max_content_width(context, cell)
|
|
|
|
column_slice = slice(cell.grid_x, cell.grid_x + cell.colspan)
|
|
|
|
columns_min_content = sum(min_content_widths[column_slice])
|
|
|
|
columns_max_content = sum(max_content_widths[column_slice])
|
|
|
|
if table.style['border_collapse'] == 'separate':
|
|
|
|
spacing = (cell.colspan - 1) * table.style['border_spacing'][0]
|
|
|
|
else:
|
|
|
|
spacing = 0
|
|
|
|
|
|
|
|
if min_content > columns_min_content + spacing:
|
|
|
|
excess_width = min_content - (columns_min_content + spacing)
|
|
|
|
distribute_excess_width(
|
|
|
|
context, zipped_grid, excess_width, min_content_widths,
|
2018-10-29 18:58:54 +03:00
|
|
|
constrainedness, intrinsic_percentages, max_content_widths,
|
|
|
|
column_slice)
|
2018-09-19 17:23:19 +03:00
|
|
|
|
|
|
|
if max_content > columns_max_content + spacing:
|
|
|
|
excess_width = max_content - (columns_max_content + spacing)
|
|
|
|
distribute_excess_width(
|
|
|
|
context, zipped_grid, excess_width, max_content_widths,
|
2018-10-29 18:58:54 +03:00
|
|
|
constrainedness, intrinsic_percentages, max_content_widths,
|
|
|
|
column_slice)
|
2018-09-19 17:23:19 +03:00
|
|
|
|
2016-01-11 06:30:38 +03:00
|
|
|
# Calculate the max- and min-content widths of table and columns
|
2016-01-12 06:53:59 +03:00
|
|
|
small_percentage_contributions = [
|
2018-08-10 19:02:15 +03:00
|
|
|
max_content_widths[i] / (intrinsic_percentages[i] / 100.)
|
2016-01-11 06:30:38 +03:00
|
|
|
for i in range(grid_width)
|
2016-01-12 06:53:59 +03:00
|
|
|
if intrinsic_percentages[i]]
|
2016-01-11 06:30:38 +03:00
|
|
|
large_percentage_contribution_numerator = sum(
|
|
|
|
max_content_widths[i] for i in range(grid_width)
|
|
|
|
if intrinsic_percentages[i] == 0)
|
|
|
|
large_percentage_contribution_denominator = (
|
|
|
|
(100 - sum(intrinsic_percentages)) / 100.)
|
|
|
|
if large_percentage_contribution_denominator == 0:
|
|
|
|
if large_percentage_contribution_numerator == 0:
|
|
|
|
large_percentage_contribution = 0
|
2012-04-12 15:20:41 +04:00
|
|
|
else:
|
2016-04-12 01:56:36 +03:00
|
|
|
# "the large percentage contribution of the table [is] an
|
|
|
|
# infinitely large number if the numerator is nonzero [and] the
|
|
|
|
# denominator of that ratio is 0."
|
|
|
|
#
|
|
|
|
# http://dbaron.org/css/intrinsic/#autotableintrinsic
|
|
|
|
#
|
|
|
|
# Please note that "an infinitely large number" is not "infinite",
|
|
|
|
# and that's probably not a coincindence: putting 'inf' here breaks
|
|
|
|
# some cases (see #305).
|
|
|
|
large_percentage_contribution = sys.maxsize
|
2016-01-11 06:30:38 +03:00
|
|
|
else:
|
|
|
|
large_percentage_contribution = (
|
|
|
|
large_percentage_contribution_numerator /
|
|
|
|
large_percentage_contribution_denominator)
|
2012-04-10 19:56:23 +04:00
|
|
|
|
2016-01-11 06:30:38 +03:00
|
|
|
table_min_content_width = (
|
|
|
|
total_horizontal_border_spacing + sum(min_content_widths))
|
|
|
|
table_max_content_width = (
|
|
|
|
total_horizontal_border_spacing + max(
|
2016-01-12 06:53:59 +03:00
|
|
|
[sum(max_content_widths), large_percentage_contribution] +
|
|
|
|
small_percentage_contributions))
|
2016-01-11 06:30:38 +03:00
|
|
|
|
2018-01-13 19:05:23 +03:00
|
|
|
if table.style['width'] != 'auto' and table.style['width'].unit == 'px':
|
2016-01-29 02:39:51 +03:00
|
|
|
# "percentages on the following properties are treated instead as
|
|
|
|
# though they were the following: width: auto"
|
|
|
|
# http://dbaron.org/css/intrinsic/#outer-intrinsic
|
2018-01-13 19:05:23 +03:00
|
|
|
table_min_width = table_max_width = table.style['width'].value
|
2016-01-29 02:39:51 +03:00
|
|
|
else:
|
|
|
|
table_min_width = table_min_content_width
|
|
|
|
table_max_width = table_max_content_width
|
2016-01-29 20:50:51 +03:00
|
|
|
|
2016-04-01 20:28:19 +03:00
|
|
|
table_min_content_width = max(
|
|
|
|
table_min_content_width, adjust(
|
|
|
|
table, outer=False, width=table_min_width))
|
|
|
|
table_max_content_width = max(
|
|
|
|
table_max_content_width, adjust(
|
|
|
|
table, outer=False, width=table_max_width))
|
2016-04-25 03:10:22 +03:00
|
|
|
table_outer_min_content_width = margin_width(
|
|
|
|
table, margin_width(box, table_min_content_width))
|
|
|
|
table_outer_max_content_width = margin_width(
|
|
|
|
table, margin_width(box, table_max_content_width))
|
2016-01-29 02:39:51 +03:00
|
|
|
|
2012-09-13 13:46:13 +04:00
|
|
|
result = (
|
2016-01-11 06:30:38 +03:00
|
|
|
min_content_widths, max_content_widths, intrinsic_percentages,
|
2016-01-24 19:06:33 +03:00
|
|
|
constrainedness, total_horizontal_border_spacing, zipped_grid)
|
2018-03-25 17:23:59 +03:00
|
|
|
context.tables[table] = result = {
|
2016-01-29 20:50:51 +03:00
|
|
|
False: (table_min_content_width, table_max_content_width) + result,
|
|
|
|
True: (
|
|
|
|
(table_outer_min_content_width, table_outer_max_content_width) +
|
|
|
|
result),
|
|
|
|
}
|
|
|
|
return result[outer]
|
2012-04-10 16:37:54 +04:00
|
|
|
|
|
|
|
|
2016-01-11 06:30:38 +03:00
|
|
|
def replaced_min_content_width(box, outer=True):
|
2016-01-09 17:16:29 +03:00
|
|
|
"""Return the min-content width for an ``InlineReplacedBox``."""
|
2018-01-13 19:05:23 +03:00
|
|
|
width = box.style['width']
|
2017-10-11 12:11:57 +03:00
|
|
|
if width == 'auto':
|
2018-01-13 19:05:23 +03:00
|
|
|
height = box.style['height']
|
2014-05-23 16:50:39 +04:00
|
|
|
if height == 'auto' or height.unit == '%':
|
|
|
|
height = 'auto'
|
|
|
|
else:
|
|
|
|
assert height.unit == 'px'
|
|
|
|
height = height.value
|
2018-01-13 19:05:23 +03:00
|
|
|
if (box.style['max_width'] != 'auto' and
|
|
|
|
box.style['max_width'].unit == '%'):
|
2017-10-11 12:11:57 +03:00
|
|
|
# See https://drafts.csswg.org/css-sizing/#intrinsic-contribution
|
|
|
|
width = 0
|
|
|
|
else:
|
|
|
|
image = box.replacement
|
|
|
|
iwidth, iheight = image.get_intrinsic_size(
|
2018-01-13 19:05:23 +03:00
|
|
|
box.style['image_resolution'], box.style['font_size'])
|
2017-10-11 12:11:57 +03:00
|
|
|
width, _ = default_image_sizing(
|
|
|
|
iwidth, iheight, image.intrinsic_ratio, 'auto', height,
|
|
|
|
default_width=300, default_height=150)
|
2018-01-13 19:05:23 +03:00
|
|
|
elif box.style['width'].unit == '%':
|
2017-10-11 12:11:57 +03:00
|
|
|
# See https://drafts.csswg.org/css-sizing/#intrinsic-contribution
|
|
|
|
width = 0
|
2012-04-04 16:25:13 +04:00
|
|
|
else:
|
2012-04-10 14:00:31 +04:00
|
|
|
assert width.unit == 'px'
|
|
|
|
width = width.value
|
|
|
|
return adjust(box, outer, width)
|
2013-04-26 17:42:15 +04:00
|
|
|
|
|
|
|
|
2018-02-16 02:54:34 +03:00
|
|
|
def replaced_max_content_width(box, outer=True):
|
|
|
|
"""Return the max-content width for an ``InlineReplacedBox``."""
|
|
|
|
width = box.style['width']
|
|
|
|
if width == 'auto':
|
|
|
|
height = box.style['height']
|
|
|
|
if height == 'auto' or height.unit == '%':
|
|
|
|
height = 'auto'
|
|
|
|
else:
|
|
|
|
assert height.unit == 'px'
|
|
|
|
height = height.value
|
|
|
|
image = box.replacement
|
|
|
|
iwidth, iheight = image.get_intrinsic_size(
|
|
|
|
box.style['image_resolution'], box.style['font_size'])
|
|
|
|
width, _ = default_image_sizing(
|
|
|
|
iwidth, iheight, image.intrinsic_ratio, 'auto', height,
|
|
|
|
default_width=300, default_height=150)
|
2018-02-22 20:28:38 +03:00
|
|
|
elif box.style['width'].unit == '%':
|
|
|
|
# See https://drafts.csswg.org/css-sizing/#intrinsic-contribution
|
|
|
|
width = 0
|
2018-02-16 02:54:34 +03:00
|
|
|
else:
|
|
|
|
assert width.unit == 'px'
|
|
|
|
width = width.value
|
|
|
|
return adjust(box, outer, width)
|
|
|
|
|
|
|
|
|
2018-02-24 06:41:11 +03:00
|
|
|
def flex_min_content_width(context, box, outer=True):
|
|
|
|
"""Return the min-content width for an ``FlexContainerBox``."""
|
|
|
|
# TODO: use real values, see
|
|
|
|
# https://www.w3.org/TR/css-flexbox-1/#intrinsic-sizes
|
|
|
|
min_contents = [
|
|
|
|
min_content_width(context, child, outer=True)
|
|
|
|
for child in box.children if child.is_flex_item]
|
|
|
|
if not min_contents:
|
2018-08-22 12:26:46 +03:00
|
|
|
return adjust(box, outer, 0)
|
2018-02-24 06:41:11 +03:00
|
|
|
if (box.style['flex_direction'].startswith('row') and
|
|
|
|
box.style['flex_wrap'] == 'nowrap'):
|
2018-08-22 12:26:46 +03:00
|
|
|
return adjust(box, outer, sum(min_contents))
|
2018-02-24 06:41:11 +03:00
|
|
|
else:
|
2018-08-22 12:26:46 +03:00
|
|
|
return adjust(box, outer, max(min_contents))
|
2018-02-24 06:41:11 +03:00
|
|
|
|
|
|
|
|
|
|
|
def flex_max_content_width(context, box, outer=True):
|
|
|
|
"""Return the max-content width for an ``FlexContainerBox``."""
|
|
|
|
# TODO: use real values, see
|
|
|
|
# https://www.w3.org/TR/css-flexbox-1/#intrinsic-sizes
|
|
|
|
max_contents = [
|
|
|
|
max_content_width(context, child, outer=True)
|
|
|
|
for child in box.children if child.is_flex_item]
|
|
|
|
if not max_contents:
|
2018-08-22 12:26:46 +03:00
|
|
|
return adjust(box, outer, 0)
|
2018-02-24 06:41:11 +03:00
|
|
|
if box.style['flex_direction'].startswith('row'):
|
2018-08-22 12:26:46 +03:00
|
|
|
return adjust(box, outer, sum(max_contents))
|
2018-02-24 06:41:11 +03:00
|
|
|
else:
|
2018-08-22 12:26:46 +03:00
|
|
|
return adjust(box, outer, max(max_contents))
|
2018-02-24 06:41:11 +03:00
|
|
|
|
|
|
|
|
2013-04-26 17:42:15 +04:00
|
|
|
def trailing_whitespace_size(context, box):
|
|
|
|
"""Return the size of the trailing whitespace of ``box``."""
|
|
|
|
from .inlines import split_text_box, split_first_line
|
|
|
|
|
|
|
|
while isinstance(box, (boxes.InlineBox, boxes.LineBox)):
|
|
|
|
if not box.children:
|
|
|
|
return 0
|
|
|
|
box = box.children[-1]
|
|
|
|
if not (isinstance(box, boxes.TextBox) and box.text and
|
2018-01-13 19:05:23 +03:00
|
|
|
box.style['white_space'] in ('normal', 'nowrap', 'pre-line')):
|
2013-04-26 17:42:15 +04:00
|
|
|
return 0
|
|
|
|
stripped_text = box.text.rstrip(' ')
|
2018-01-13 19:05:23 +03:00
|
|
|
if box.style['font_size'] == 0 or len(stripped_text) == len(box.text):
|
2013-04-26 17:42:15 +04:00
|
|
|
return 0
|
|
|
|
if stripped_text:
|
2017-10-21 17:56:25 +03:00
|
|
|
old_box, _, _ = split_text_box(context, box, None, 0)
|
2013-04-26 17:42:15 +04:00
|
|
|
assert old_box
|
|
|
|
stripped_box = box.copy_with_text(stripped_text)
|
|
|
|
stripped_box, resume, _ = split_text_box(
|
2017-10-21 17:56:25 +03:00
|
|
|
context, stripped_box, None, 0)
|
2013-04-26 17:42:15 +04:00
|
|
|
assert stripped_box is not None
|
|
|
|
assert resume is None
|
|
|
|
return old_box.width - stripped_box.width
|
|
|
|
else:
|
|
|
|
_, _, _, width, _, _ = split_first_line(
|
2017-10-21 17:56:25 +03:00
|
|
|
box.text, box.style, context, None, box.justification_spacing)
|
2013-04-26 17:42:15 +04:00
|
|
|
return width
|