Skip to content

Commit

Permalink
Initial support of multi-column layouts
Browse files Browse the repository at this point in the history
Related to #60.

This commit adds a really simple support of multi-column layouts.
  • Loading branch information
liZe committed Aug 14, 2016
1 parent b945460 commit 33aa5c6
Show file tree
Hide file tree
Showing 5 changed files with 193 additions and 7 deletions.
2 changes: 1 addition & 1 deletion weasyprint/css/__init__.py
Expand Up @@ -67,7 +67,7 @@
# A test function that returns True if the given property name has an
# initial value that is not always the same when computed.
RE_INITIAL_NOT_COMPUTED = re.compile(
'^(display|(border_[a-z]+|outline)_(width|color))$').match
'^(display|column_gap|(border_[a-z]+|outline)_(width|color))$').match


class StyleDict(object):
Expand Down
15 changes: 15 additions & 0 deletions weasyprint/css/computed_values.py
Expand Up @@ -330,6 +330,7 @@ def background_size(computer, name, values):
@register_computer('border-right-width')
@register_computer('border-left-width')
@register_computer('border-bottom-width')
@register_computer('column-rule-width')
@register_computer('outline-width')
def border_width(computer, name, value):
"""Compute the ``border-*-width`` properties."""
Expand All @@ -348,6 +349,12 @@ def border_width(computer, name, value):
return length(computer, name, value, pixels_only=True)


@register_computer('column-width')
def column_width(computer, name, value):
"""Compute the ``column-width`` property."""
return length(computer, name, value, pixels_only=True)


@register_computer('border-top-left-radius')
@register_computer('border-top-right-radius')
@register_computer('border-bottom-left-radius')
Expand All @@ -357,6 +364,14 @@ def border_radius(computer, name, values):
return [length(computer, name, value) for value in values]


@register_computer('column-gap')
def column_gap(computer, name, value):
"""Compute the ``column-gap`` property."""
if value == 'normal':
value = Dimension(1, 'em')
return length(computer, name, value, pixels_only=True)


@register_computer('content')
def content(computer, name, values):
"""Compute the ``content`` property."""
Expand Down
10 changes: 10 additions & 0 deletions weasyprint/css/properties.py
Expand Up @@ -153,6 +153,16 @@
'bookmark_label': [('content', 'text')],
'bookmark_level': 'none',

# CSS Multi-column Layout Module
'column_width': 'auto',
'column_count': 'auto',
'column_gap': Dimension(1, 'em'),
'column_rule_color': 'currentColor',
'column_rule_style': 'none',
'column_rule_width': 'medium',
'column_fill': 'balance',
'column_span': 'none',

# CSS4 Text
# http://dev.w3.org/csswg/css4-text/#hyphenation
'hyphens': 'manual',
Expand Down
72 changes: 70 additions & 2 deletions weasyprint/css/validation.py
Expand Up @@ -241,6 +241,7 @@ def background_attachment(keyword):
@validator('border-right-color')
@validator('border-bottom-color')
@validator('border-left-color')
@validator('column-rule-color')
@single_token
def other_colors(token):
return parse_color(token)
Expand Down Expand Up @@ -578,6 +579,7 @@ def border_corner_radius(tokens):
@validator('border-right-style')
@validator('border-left-style')
@validator('border-bottom-style')
@validator('column-rule-style')
@single_keyword
def border_style(keyword):
"""``border-*-style`` properties validation."""
Expand All @@ -597,10 +599,11 @@ def outline_style(keyword):
@validator('border-right-width')
@validator('border-left-width')
@validator('border-bottom-width')
@validator('column-rule-width')
@validator('outline-width')
@single_token
def border_width(token):
"""``border-*-width`` properties validation."""
"""Border, column rule and outline widths properties validation."""
length = get_length(token, negative=False)
if length:
return length
Expand All @@ -609,6 +612,26 @@ def border_width(token):
return keyword


@validator()
@single_token
def column_width(token):
"""``column-width`` property validation."""
length = get_length(token, negative=False)
if length:
return length
keyword = get_keyword(token)
if keyword == 'auto':
return keyword


@validator()
@single_keyword
def column_span(keyword):
"""``column-span`` property validation."""
# TODO: uncomment this when it is supported
# return keyword in ('all', 'none')


@validator()
@single_keyword
def box_sizing(keyword):
Expand Down Expand Up @@ -787,6 +810,25 @@ def width_height(token):
return 'auto'


@validator()
@single_token
def column_gap(token):
"""Validation for the ``column-gap`` property."""
length = get_length(token, negative=False)
if length:
return length
keyword = get_keyword(token)
if keyword == 'normal':
return keyword


@validator()
@single_keyword
def column_fill(keyword):
"""``column-fill`` property validation."""
return keyword in ('auto', 'balance')


@validator()
@single_keyword
def direction(keyword):
Expand Down Expand Up @@ -966,11 +1008,22 @@ def z_index(token):
@validator('widows')
@single_token
def orphans_widows(token):
"""Validation for the ``orphans`` or ``widows`` properties."""
"""Validation for the ``orphans`` and ``widows`` properties."""
if token.type == 'INTEGER':
value = token.value
if value >= 1:
return value

@validator()
@single_token
def column_count(token):
"""Validation for the ``column-count`` property."""
if token.type == 'INTEGER':
value = token.value
if value >= 1:
return value
if get_keyword(token) == 'auto':
return 'auto'


@validator()
Expand Down Expand Up @@ -1572,6 +1625,7 @@ def expand_border(base_url, name, tokens):
@expander('border-right')
@expander('border-bottom')
@expander('border-left')
@expander('column-rule')
@expander('outline')
@generic_expander('-width', '-color', '-style')
def expand_border_side(name, tokens):
Expand Down Expand Up @@ -1689,6 +1743,20 @@ def add(name, value):
yield 'background-color', color


@expander('columns')
@generic_expander('column-width', 'column-count')
def expand_columns(name, tokens):
"""Expand the ``columns`` shorthand property."""
for token in tokens:
if column_width([token]) is not None:
name = 'column-width'
elif column_count([token]) is not None:
name = 'column-count'
else:
raise InvalidValues
yield name, [token]


@expander('font')
@generic_expander('-style', '-variant', '-weight', '-stretch', '-size',
'line-height', '-family') # line-height is not a suffix
Expand Down
101 changes: 97 additions & 4 deletions weasyprint/layout/blocks.py
Expand Up @@ -12,6 +12,8 @@

from __future__ import division, unicode_literals

from math import floor

from .absolute import absolute_layout, AbsolutePlaceholder
from .float import float_layout, get_clearance, avoid_collisions
from .inlines import (iter_line_boxes, replaced_box_width, replaced_box_height,
Expand All @@ -20,6 +22,7 @@
from .min_max import handle_min_max_width
from .tables import table_layout, table_wrapper_width
from .percentages import resolve_percentages, resolve_position_percentages
from .preferred import shrink_to_fit
from ..formatting_structure import boxes
from ..compat import xrange, izip

Expand Down Expand Up @@ -54,10 +57,16 @@ def block_level_layout(context, box, max_position_y, skip_stack,
adjoining_margins = []

if isinstance(box, boxes.BlockBox):
return block_box_layout(
context, box, max_position_y, skip_stack,
containing_block, device_size, page_is_empty,
absolute_boxes, fixed_boxes, adjoining_margins)
style = box.style
if style.column_width != 'auto' or style.column_count != 'auto':
return columns_layout(
context, box, max_position_y, skip_stack, containing_block,
device_size, page_is_empty, absolute_boxes, fixed_boxes)
else:
return block_box_layout(
context, box, max_position_y, skip_stack, containing_block,
device_size, page_is_empty, absolute_boxes, fixed_boxes,
adjoining_margins)
elif isinstance(box, boxes.BlockReplacedBox):
box = block_replaced_box_layout(box, containing_block, device_size)
# Don't collide with floats
Expand Down Expand Up @@ -97,6 +106,90 @@ def block_box_layout(context, box, max_position_y, skip_stack,
return new_box, resume_at, next_page, adjoining_margins, collapsing_through


def columns_layout(context, box, max_position_y, skip_stack, containing_block,
device_size, page_is_empty, absolute_boxes, fixed_boxes):
"""Lay out a multi-column ``box``."""
# Implementation of the multi-column pseudo-algorithm:
# https://www.w3.org/TR/css3-multicol/#pseudo-algorithm
count = None
width = None
style = box.style
available_width = containing_block.width
if available_width == 'auto':
if style.column_count == 'auto':
count = 1
width = containing_block.content_width()
elif style.column_width != 'auto':
count = style.column_count
width = style.column_width
else:
# TODO: replace with real shrink-to-fit
available_width = shrink_to_fit(context, box, float('inf'))
if count is None:
if style.column_width == 'auto' and style.column_count != 'auto':
count = style.column_count
width = max(
0, available_width - (count - 1) * style.column_gap) / count
elif style.column_width != 'auto' and style.column_count == 'auto':
count = max(1, floor(
(available_width + style.column_gap) /
(style.column_width + style.column_gap)))
width = (
(available_width + style.column_gap) / count -
style.column_gap)
else:
count = min(style.column_count, floor(
(available_width + style.column_gap) /
(style.column_width + style.column_gap)))
width = (
(available_width + style.column_gap) / count -
style.column_gap)

block_level_width(box, containing_block)

# Really stupid balance algorithm
original_max_position_y = max_position_y
if style.column_fill == 'balance':
# Find the total height of the content
column_box = box.copy()
column_box.width = width
new_child, _, _, _, _ = block_box_layout(
context, column_box, float('inf'), skip_stack, containing_block,
device_size, page_is_empty, absolute_boxes, fixed_boxes, [])
# TODO: We add a 1em extra size to the maximum height of each column,
# we should change this later.
max_position_y = min(
max_position_y,
box.position_y + new_child.height / count + style.font_size)

# Replace the current box children with columns
old_box = box.copy()
old_box.children = []
for i in range(count):
if i == count - 1:
max_position_y = original_max_position_y
column_box = box.copy()
column_box.width = width
column_box.position_x = box.position_x + i * width
(new_child, skip_stack, next_page, next_adjoining_margins,
collapsing_through) = block_box_layout(
context, column_box, max_position_y, skip_stack, containing_block,
device_size, page_is_empty, absolute_boxes, fixed_boxes, [])
if new_child:
old_box.children.append(new_child)
if skip_stack is None:
break

if old_box.children:
old_box.height = max(child.height for child in old_box.children)
else:
old_box.height = 0

return (old_box, skip_stack, next_page, next_adjoining_margins,
collapsing_through)



@handle_min_max_width
def block_replaced_width(box, containing_block, device_size):
# http://www.w3.org/TR/CSS21/visudet.html#block-replaced-width
Expand Down

0 comments on commit 33aa5c6

Please sign in to comment.