File: //home/arjun/projects/env/lib/python3.10/site-packages/weasyprint/formatting_structure/boxes.py
"""Classes for all types of boxes in the CSS formatting structure / box model.
See https://www.w3.org/TR/CSS21/visuren.html
Names are the same as in CSS 2.1 with the exception of ``TextBox``. In
WeasyPrint, any text is in a ``TextBox``. What CSS calls anonymous inline boxes
are text boxes but not all text boxes are anonymous inline boxes.
See https://www.w3.org/TR/CSS21/visuren.html#anonymous
Abstract classes, should not be instantiated:
* Box
* BlockLevelBox
* InlineLevelBox
* BlockContainerBox
* ReplacedBox
* ParentBox
* AtomicInlineLevelBox
Concrete classes:
* PageBox
* BlockBox
* InlineBox
* InlineBlockBox
* BlockReplacedBox
* InlineReplacedBox
* TextBox
* LineBox
* Various table-related Box subclasses
All concrete box classes whose name contains "Inline" or "Block" have one of
the following "outside" behavior:
* Block-level (inherits from :class:`BlockLevelBox`)
* Inline-level (inherits from :class:`InlineLevelBox`)
and one of the following "inside" behavior:
* Block container (inherits from :class:`BlockContainerBox`)
* Inline content (InlineBox and :class:`TextBox`)
* Replaced content (inherits from :class:`ReplacedBox`)
… with various combinasions of both.
See respective docstrings for details.
"""
import itertools
from ..css import computed_from_cascaded
class Box:
"""Abstract base class for all boxes."""
# Definitions for the rules generating anonymous table boxes
# https://www.w3.org/TR/CSS21/tables.html#anonymous-boxes
proper_table_child = False
internal_table_or_caption = False
tabular_container = False
# Keep track of removed collapsing spaces for wrap opportunities.
leading_collapsible_space = False
trailing_collapsible_space = False
# Default, may be overriden on instances.
is_table_wrapper = False
is_flex_item = False
is_for_root_element = False
is_column = False
is_leader = False
# Other properties
transformation_matrix = None
bookmark_label = None
string_set = None
footnote = None
cached_counter_values = None
missing_link = None
# Default, overriden on some subclasses
def all_children(self):
return ()
def __init__(self, element_tag, style, element):
self.element_tag = element_tag
self.element = element
self.style = style
self.remove_decoration_sides = set()
def __repr__(self):
return f'<{type(self).__name__} {self.element_tag}>'
@classmethod
def anonymous_from(cls, parent, *args, **kwargs):
"""Return an anonymous box that inherits from ``parent``."""
style = computed_from_cascaded(
cascaded={}, parent_style=parent.style, element=None)
return cls(parent.element_tag, style, parent.element, *args, **kwargs)
def copy(self):
"""Return shallow copy of the box."""
cls = type(self)
# Create a new instance without calling __init__: parameters are
# different depending on the class.
new_box = cls.__new__(cls)
# Copy attributes
new_box.__dict__.update(self.__dict__)
return new_box
def deepcopy(self):
"""Return a copy of the box with recursive copies of its children."""
return self.copy()
def translate(self, dx=0, dy=0, ignore_floats=False):
"""Change the box’s position.
Also update the children’s positions accordingly.
"""
# Overridden in ParentBox to also translate children, if any.
if dx == dy == 0:
return
self.position_x += dx
self.position_y += dy
for child in self.all_children():
if not (ignore_floats and child.is_floated()):
child.translate(dx, dy, ignore_floats)
# Heights and widths
def padding_width(self):
"""Width of the padding box."""
return self.width + self.padding_left + self.padding_right
def padding_height(self):
"""Height of the padding box."""
return self.height + self.padding_top + self.padding_bottom
def border_width(self):
"""Width of the border box."""
return self.padding_width() + self.border_left_width + \
self.border_right_width
def border_height(self):
"""Height of the border box."""
return self.padding_height() + self.border_top_width + \
self.border_bottom_width
def margin_width(self):
"""Width of the margin box (aka. outer box)."""
return self.border_width() + self.margin_left + self.margin_right
def margin_height(self):
"""Height of the margin box (aka. outer box)."""
return self.border_height() + self.margin_top + self.margin_bottom
# Corners positions
def content_box_x(self):
"""Absolute horizontal position of the content box."""
return self.position_x + self.margin_left + self.padding_left + \
self.border_left_width
def content_box_y(self):
"""Absolute vertical position of the content box."""
return self.position_y + self.margin_top + self.padding_top + \
self.border_top_width
def padding_box_x(self):
"""Absolute horizontal position of the padding box."""
return self.position_x + self.margin_left + self.border_left_width
def padding_box_y(self):
"""Absolute vertical position of the padding box."""
return self.position_y + self.margin_top + self.border_top_width
def border_box_x(self):
"""Absolute horizontal position of the border box."""
return self.position_x + self.margin_left
def border_box_y(self):
"""Absolute vertical position of the border box."""
return self.position_y + self.margin_top
def hit_area(self):
"""Return the (x, y, w, h) rectangle where the box is clickable."""
# "Border area. That's the area that hit-testing is done on."
# https://lists.w3.org/Archives/Public/www-style/2012Jun/0318.html
# TODO: manage the border radii, use outer_border_radii instead
return (self.border_box_x(), self.border_box_y(),
self.border_width(), self.border_height())
def rounded_box(self, bt, br, bb, bl):
"""Position, size and radii of a box inside the outer border box.
bt, br, bb, and bl are distances from the outer border box,
defining a rectangle to be rounded.
"""
tlrx, tlry = self.border_top_left_radius
trrx, trry = self.border_top_right_radius
brrx, brry = self.border_bottom_right_radius
blrx, blry = self.border_bottom_left_radius
tlrx = max(0, tlrx - bl)
tlry = max(0, tlry - bt)
trrx = max(0, trrx - br)
trry = max(0, trry - bt)
brrx = max(0, brrx - br)
brry = max(0, brry - bb)
blrx = max(0, blrx - bl)
blry = max(0, blry - bb)
x = self.border_box_x() + bl
y = self.border_box_y() + bt
width = self.border_width() - bl - br
height = self.border_height() - bt - bb
# Fix overlapping curves
# See https://www.w3.org/TR/css-backgrounds-3/#corner-overlap
ratio = min([1] + [
extent / sum_radii
for extent, sum_radii in (
(width, tlrx + trrx),
(width, blrx + brrx),
(height, tlry + blry),
(height, trry + brry),
)
if sum_radii > 0
])
return (
x, y, width, height,
(tlrx * ratio, tlry * ratio),
(trrx * ratio, trry * ratio),
(brrx * ratio, brry * ratio),
(blrx * ratio, blry * ratio))
def rounded_box_ratio(self, ratio):
return self.rounded_box(
self.border_top_width * ratio,
self.border_right_width * ratio,
self.border_bottom_width * ratio,
self.border_left_width * ratio)
def rounded_padding_box(self):
"""Return the position, size and radii of the rounded padding box."""
return self.rounded_box(
self.border_top_width,
self.border_right_width,
self.border_bottom_width,
self.border_left_width)
def rounded_border_box(self):
"""Return the position, size and radii of the rounded border box."""
return self.rounded_box(0, 0, 0, 0)
def rounded_content_box(self):
"""Return the position, size and radii of the rounded content box."""
return self.rounded_box(
self.border_top_width + self.padding_top,
self.border_right_width + self.padding_right,
self.border_bottom_width + self.padding_bottom,
self.border_left_width + self.padding_left)
# Positioning schemes
def is_floated(self):
"""Return whether this box is floated."""
return self.style['float'] in ('left', 'right')
def is_footnote(self):
"""Return whether this box is a footnote."""
return self.style['float'] == 'footnote'
def is_absolutely_positioned(self):
"""Return whether this box is in the absolute positioning scheme."""
return self.style['position'] in ('absolute', 'fixed')
def is_running(self):
"""Return whether this box is a running element."""
return self.style['position'][0] == 'running()'
def is_in_normal_flow(self):
"""Return whether this box is in normal flow."""
return not (
self.is_floated() or self.is_absolutely_positioned() or
self.is_running() or self.is_footnote())
def is_monolithic(self):
"""Return whether this box is monolithic."""
# https://www.w3.org/TR/css-break-3/#monolithic
return (
isinstance(self, AtomicInlineLevelBox) or
isinstance(self, ReplacedBox) or
self.style['overflow'] in ('auto', 'scroll') or
(self.style['overflow'] == 'hidden' and
self.style['height'] != 'auto'))
# Start and end page values for named pages
def page_values(self):
"""Return start and end page values."""
return (self.style['page'], self.style['page'])
# PDF attachments
def is_attachment(self):
"""Return whether this link should be stored as a PDF attachment."""
from ..html import element_has_link_type
if self.element is not None and self.element.tag == 'a':
return element_has_link_type(self.element, 'attachment')
return False
# Forms
def is_input(self):
"""Return whether this box is a form input."""
# https://html.spec.whatwg.org/multipage/forms.html#category-submit
if self.style['appearance'] == 'auto' and self.element is not None:
if self.element.tag in ('button', 'input', 'select', 'textarea'):
return not isinstance(self, (LineBox, TextBox))
return False
class ParentBox(Box):
"""A box that has children."""
def __init__(self, element_tag, style, element, children):
super().__init__(element_tag, style, element)
self.children = tuple(children)
def all_children(self):
return self.children
def _reset_spacing(self, side):
"""Set to 0 the margin, padding and border of ``side``."""
self.remove_decoration_sides.add(side)
setattr(self, f'margin_{side}', 0)
setattr(self, f'padding_{side}', 0)
setattr(self, f'border_{side}_width', 0)
def remove_decoration(self, start, end):
if self.style['box_decoration_break'] == 'clone':
return
if start:
self._reset_spacing('top')
if end:
self._reset_spacing('bottom')
def copy_with_children(self, new_children):
"""Create a new equivalent box with given ``new_children``."""
new_box = self.copy()
new_box.children = list(new_children)
# Clear and reset removed decorations as we don't want to keep the
# previous data, for example when a box is split between two pages.
self.remove_decoration_sides = set()
return new_box
def deepcopy(self):
result = self.copy()
result.children = tuple(child.deepcopy() for child in self.children)
return result
def descendants(self):
"""A flat generator for a box, its children and descendants."""
yield self
for child in self.children:
if isinstance(child, ParentBox):
for grand_child in child.descendants():
yield grand_child
else:
yield child
def get_wrapped_table(self):
"""Get the table wrapped by the box."""
assert self.is_table_wrapper
for child in self.children:
if isinstance(child, TableBox):
return child
else: # pragma: no cover
raise ValueError('Table wrapper without a table')
def page_values(self):
start_value, end_value = super().page_values()
# TODO: We should find Class A possible page breaks according to
# https://drafts.csswg.org/css-page-3/#propdef-page
# Keep only children in normal flow for now.
children = [
child for child in self.children if child.is_in_normal_flow()]
if children:
if len(children) == 1:
page_values = children[0].page_values()
start_value = page_values[0] or start_value
end_value = page_values[1] or end_value
else:
start_box, end_box = children[0], children[-1]
start_value = start_box.page_values()[0] or start_value
end_value = end_box.page_values()[1] or end_value
return start_value, end_value
class BlockLevelBox(Box):
"""A box that participates in an block formatting context.
An element with a ``display`` value of ``block``, ``list-item`` or
``table`` generates a block-level box.
"""
clearance = None
class BlockContainerBox(ParentBox):
"""A box that contains only block-level boxes or only line boxes.
A box that either contains only block-level boxes or establishes an inline
formatting context and thus contains only line boxes.
A non-replaced element with a ``display`` value of ``block``,
``list-item``, ``inline-block`` or 'table-cell' generates a block container
box.
"""
class BlockBox(BlockContainerBox, BlockLevelBox):
"""A block-level box that is also a block container.
A non-replaced element with a ``display`` value of ``block``, ``list-item``
generates a block box.
"""
class LineBox(ParentBox):
"""A box that represents a line in an inline formatting context.
Can only contain inline-level boxes.
In early stages of building the box tree a single line box contains many
consecutive inline boxes. Later, during layout phase, each line boxes will
be split into multiple line boxes, one for each actual line.
"""
text_overflow = 'clip'
block_ellipsis = 'none'
@classmethod
def anonymous_from(cls, parent, *args, **kwargs):
box = super().anonymous_from(parent, *args, **kwargs)
if parent.style['overflow'] != 'visible':
box.text_overflow = parent.style['text_overflow']
return box
class InlineLevelBox(Box):
"""A box that participates in an inline formatting context.
An inline-level box that is not an inline box is said to be "atomic". Such
boxes are inline blocks, replaced elements and inline tables.
An element with a ``display`` value of ``inline``, ``inline-table``, or
``inline-block`` generates an inline-level box.
"""
def remove_decoration(self, start, end):
if self.style['box_decoration_break'] == 'clone':
return
ltr = self.style['direction'] == 'ltr'
if start:
self._reset_spacing('left' if ltr else 'right')
if end:
self._reset_spacing('right' if ltr else 'left')
class InlineBox(InlineLevelBox, ParentBox):
"""An inline box with inline children.
A box that participates in an inline formatting context and whose content
also participates in that inline formatting context.
A non-replaced element with a ``display`` value of ``inline`` generates an
inline box.
"""
link_annotation = None
def hit_area(self):
"""Return the (x, y, w, h) rectangle where the box is clickable."""
# Use line-height (margin_height) rather than border_height
return (self.border_box_x(), self.position_y,
self.border_width(), self.margin_height())
class TextBox(InlineLevelBox):
"""A box that contains only text and has no box children.
Any text in the document ends up in a text box. What CSS calls "anonymous
inline boxes" are also text boxes.
"""
justification_spacing = 0
def __init__(self, element_tag, style, element, text):
assert text
super().__init__(element_tag, style, element)
self.text = text
def copy_with_text(self, text):
"""Return a new TextBox identical to this one except for the text."""
assert text
new_box = self.copy()
new_box.text = text
return new_box
class AtomicInlineLevelBox(InlineLevelBox):
"""An atomic box in an inline formatting context.
This inline-level box cannot be split for line breaks.
"""
class InlineBlockBox(AtomicInlineLevelBox, BlockContainerBox):
"""A box that is both inline-level and a block container.
It behaves as inline on the outside and as a block on the inside.
A non-replaced element with a 'display' value of 'inline-block' generates
an inline-block box.
"""
class ReplacedBox(Box):
"""A box whose content is replaced.
For example, ``<img>`` are replaced: their content is rendered externally
and is opaque from CSS’s point of view.
"""
def __init__(self, element_tag, style, element, replacement):
super().__init__(element_tag, style, element)
self.replacement = replacement
class BlockReplacedBox(ReplacedBox, BlockLevelBox):
"""A box that is both replaced and block-level.
A replaced element with a ``display`` value of ``block``, ``liste-item`` or
``table`` generates a block-level replaced box.
"""
class InlineReplacedBox(ReplacedBox, AtomicInlineLevelBox):
"""A box that is both replaced and inline-level.
A replaced element with a ``display`` value of ``inline``,
``inline-table``, or ``inline-block`` generates an inline-level replaced
box.
"""
class TableBox(BlockLevelBox, ParentBox):
"""Box for elements with ``display: table``"""
# Definitions for the rules generating anonymous table boxes
# https://www.w3.org/TR/CSS21/tables.html#anonymous-boxes
tabular_container = True
def all_children(self):
return itertools.chain(self.children, self.column_groups)
def translate(self, dx=0, dy=0, ignore_floats=False):
self.column_positions = [
position + dx for position in self.column_positions]
return super().translate(dx, dy, ignore_floats)
def page_values(self):
return (self.style['page'], self.style['page'])
class InlineTableBox(TableBox):
"""Box for elements with ``display: inline-table``"""
class TableRowGroupBox(ParentBox):
"""Box for elements with ``display: table-row-group``"""
proper_table_child = True
internal_table_or_caption = True
tabular_container = True
proper_parents = (TableBox, InlineTableBox)
# Default values. May be overriden on instances.
is_header = False
is_footer = False
class TableRowBox(ParentBox):
"""Box for elements with ``display: table-row``"""
proper_table_child = True
internal_table_or_caption = True
tabular_container = True
proper_parents = (TableBox, InlineTableBox, TableRowGroupBox)
class TableColumnGroupBox(ParentBox):
"""Box for elements with ``display: table-column-group``"""
proper_table_child = True
internal_table_or_caption = True
proper_parents = (TableBox, InlineTableBox)
# Columns groups never have margins or paddings
margin_top = 0
margin_bottom = 0
margin_left = 0
margin_right = 0
padding_top = 0
padding_bottom = 0
padding_left = 0
padding_right = 0
def get_cells(self):
"""Return cells that originate in the group's columns."""
return [
cell for column in self.children for cell in column.get_cells()]
@property
def span(self):
if self.children:
return len(self.children)
else:
try:
return max(int(self.element.get('span', '').strip()), 1)
except ValueError:
return 1
# Not really a parent box, but pretending to be removes some corner cases.
class TableColumnBox(ParentBox):
"""Box for elements with ``display: table-column``"""
proper_table_child = True
internal_table_or_caption = True
proper_parents = (TableBox, InlineTableBox, TableColumnGroupBox)
# Columns never have margins or paddings
margin_top = 0
margin_bottom = 0
margin_left = 0
margin_right = 0
padding_top = 0
padding_bottom = 0
padding_left = 0
padding_right = 0
def get_cells(self):
"""Return cells that originate in the column.
Is set on instances.
"""
raise NotImplementedError
@property
def span(self):
try:
return max(int(self.element.get('span', '').strip()), 1)
except ValueError:
return 1
class TableCellBox(BlockContainerBox):
"""Box for elements with ``display: table-cell``"""
internal_table_or_caption = True
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# HTML 4.01 gives special meaning to colspan=0
# https://www.w3.org/TR/html401/struct/tables.html#adef-rowspan
# but HTML 5 removed it
# https://html.spec.whatwg.org/multipage/tables.html#attr-tdth-colspan
# rowspan=0 is still there though.
try:
self.colspan = max(int(self.element.get('colspan', '').strip()), 1)
except (AttributeError, ValueError):
self.colspan = 1
try:
self.rowspan = max(int(self.element.get('rowspan', '').strip()), 0)
except (AttributeError, ValueError):
self.rowspan = 1
class TableCaptionBox(BlockBox):
"""Box for elements with ``display: table-caption``"""
proper_table_child = True
internal_table_or_caption = True
proper_parents = (TableBox, InlineTableBox)
class PageBox(ParentBox):
"""Box for a page.
Initially the whole document will be in the box for the root element.
During layout a new page box is created after every page break.
"""
def __init__(self, page_type, style):
self.page_type = page_type
# Page boxes are not linked to any element.
super().__init__(
element_tag=None, style=style, element=None, children=[])
def __repr__(self):
return f'<{type(self).__name__} {self.page_type}>'
@property
def bleed(self):
return {
side: self.style[f'bleed_{side}'].value
for side in ('top', 'right', 'bottom', 'left')}
@property
def bleed_area(self):
return (
-self.bleed['left'], -self.bleed['top'],
self.margin_width() + self.bleed['left'] + self.bleed['right'],
self.margin_height() + self.bleed['top'] + self.bleed['bottom'])
class MarginBox(BlockContainerBox):
"""Box in page margins, as defined in CSS3 Paged Media"""
def __init__(self, at_keyword, style):
self.at_keyword = at_keyword
# Margin boxes are not linked to any element.
super().__init__(
element_tag=None, style=style, element=None, children=[])
def __repr__(self):
return f'<{type(self).__name__} {self.at_keyword}>'
class FootnoteAreaBox(BlockBox):
"""Box displaying footnotes, as defined in GCPM."""
def __init__(self, page, style):
self.page = page
# Footnote area boxes are not linked to any element.
super().__init__(
element_tag=None, style=style, element=None, children=[])
def __repr__(self):
return f'<{type(self).__name__} @footnote>'
class FlexContainerBox(ParentBox):
"""A box that contains only flex-items."""
class FlexBox(FlexContainerBox, BlockLevelBox):
"""A box that is both block-level and a flex container.
It behaves as block on the outside and as a flex container on the inside.
"""
class InlineFlexBox(FlexContainerBox, InlineLevelBox):
"""A box that is both inline-level and a flex container.
It behaves as inline on the outside and as a flex container on the inside.
"""