Skip to content

Commit

Permalink
Merge pull request #495 from Kozea/namedpages
Browse files Browse the repository at this point in the history
Named pages
  • Loading branch information
liZe authored Aug 9, 2017
2 parents 7e9c22b + 0ac6002 commit 55fab4b
Show file tree
Hide file tree
Showing 14 changed files with 448 additions and 133 deletions.
3 changes: 2 additions & 1 deletion docs/features.rst
Original file line number Diff line number Diff line change
Expand Up @@ -279,7 +279,8 @@ other features are available, including:
selectors;
- the page margin boxes;
- the page-based counters (with known bugs `#91`_, `#93`_, `#289`_);
- the page ``size`` property.
- the page ``size`` property, but **not** the ``bleed`` and ``marks``
properties.

.. _CSS Paged Media Module Level 3: http://dev.w3.org/csswg/css3-page/
.. _named pages: http://dev.w3.org/csswg/css3-page/#using-named-pages
Expand Down
145 changes: 77 additions & 68 deletions weasyprint/css/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
from __future__ import division, unicode_literals

import re
from collections import namedtuple

import cssselect2
import tinycss2
Expand All @@ -39,28 +40,6 @@
# Reject anything not in here:
PSEUDO_ELEMENTS = (None, 'before', 'after', 'first-line', 'first-letter')

# Selectors for @page rules can have a pseudo-class, one of :first, :left,
# :right or :blank. This maps pseudo-classes to lists of "page types" selected.
PAGE_PSEUDOCLASS_TARGETS = {
'first': [
'first_left_page', 'first_right_page',
'first_blank_left_page', 'first_blank_right_page'],
'left': [
'left_page', 'first_left_page',
'blank_left_page', 'first_blank_left_page'],
'right': [
'right_page', 'first_right_page',
'blank_right_page', 'first_blank_right_page'],
'blank': [
'blank_left_page', 'first_blank_left_page',
'blank_right_page', 'first_blank_right_page'],
# no pseudo-class: all pages
None: [
'left_page', 'right_page', 'first_left_page', 'first_right_page',
'blank_left_page', 'blank_right_page',
'first_blank_left_page', 'first_blank_right_page'],
}

# 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(
Expand Down Expand Up @@ -109,6 +88,9 @@ def inherit_from(self):
anonymous = False


PageType = namedtuple('PageType', ['side', 'blank', 'first', 'name'])


def get_child_text(element):
"""Return the text directly in the element, not descendants."""
content = [element.text] if element.text else []
Expand Down Expand Up @@ -452,6 +434,22 @@ def check_style_attribute(element, style_attribute):
'counter-increment:none' % element.get('value'))


def matching_page_types(page_type, names=()):
sides = ['left', 'right', None] if page_type.side is None else [
page_type.side]
blanks = (True, False) if page_type.blank is False else (True,)
firsts = (True, False) if page_type.first is False else (True,)
names = (
tuple(names) + (None,) if page_type.name is None
else (page_type.name,))
for side in sides:
for blank in blanks:
for first in firsts:
for name in names:
yield PageType(
side=side, blank=blank, first=first, name=name)


def evaluate_media_query(query_list, device_media_type):
"""Return the boolean evaluation of `query_list` for the given
`device_media_type`.
Expand Down Expand Up @@ -509,14 +507,20 @@ def set_computed_styles(cascaded_styles, computed_styles, element, parent,
declaration priority (ie. ``!important``) and selector specificity.
"""
parent_style = computed_styles[parent, None] \
if parent is not None else None
# When specified on the font-size property of the root element, the rem
# units refer to the property’s initial value.
root_style = {'font_size': properties.INITIAL_VALUES['font_size']} \
if element is root else computed_styles[root, None]
cascaded = cascaded_styles.get((element, pseudo_type), {})
if element == root:
assert parent is None
parent_style = None
root_style = {
# When specified on the font-size property of the root element, the
# rem units refer to the property’s initial value.
'font_size': properties.INITIAL_VALUES['font_size'],
}
else:
assert parent is not None
parent_style = computed_styles[parent, None]
root_style = computed_styles[root, None]

cascaded = cascaded_styles.get((element, pseudo_type), {})
computed_styles[element, pseudo_type] = computed_from_cascaded(
element, cascaded, parent_style, pseudo_type, root_style, base_url)

Expand All @@ -530,6 +534,8 @@ def computed_from_cascaded(element, cascaded, parent_style, pseudo_type=None,
computed = dict(properties.INITIAL_VALUES)
for name in properties.INHERITED:
computed[name] = parent_style[name]
# page is not inherited but taken from the ancestor if 'auto'
computed['page'] = parent_style['page']
# border-*-style is none, so border-width computes to zero.
# Other than that, properties that would need computing are
# border-*-color, but they do not apply.
Expand Down Expand Up @@ -567,6 +573,14 @@ def computed_from_cascaded(element, cascaded, parent_style, pseudo_type=None,

specified[name] = value

if specified['page'] == 'auto':
# The page property does not inherit. However, if the page value on
# an element is auto, then its used value is the value specified on
# its nearest ancestor with a non-auto value. When specified on the
# root element, the used value for auto is the empty string.
computed['page'] = specified['page'] = (
'' if parent_style is None else parent_style['page'])

return StyleDict(computed_values.compute(
element, pseudo_type, specified, computed, parent_style, root_style,
base_url))
Expand Down Expand Up @@ -642,39 +656,45 @@ def preprocess_stylesheet(device_media_type, base_url, stylesheet_rules,

elif rule.type == 'at-rule' and rule.at_keyword == 'page':
tokens = remove_whitespace(rule.prelude)
# TODO: support named pages (see CSS3 Paged Media)
types = {
'side': None, 'blank': False, 'first': False, 'name': None}
# TODO: Specificity is probably wrong, should clean and test that.
if not tokens:
pseudo_class = None
specificity = (0, 0)
specificity = (0, 0, 0)
elif (len(tokens) == 2 and
tokens[0].type == 'literal' and
tokens[0].value == ':' and
tokens[1].type == 'ident'):
pseudo_class = tokens[1].lower_value
specificity = {
'first': (1, 0), 'blank': (1, 0),
'left': (0, 1), 'right': (0, 1),
}.get(pseudo_class)
if not specificity:
if pseudo_class in ('left', 'right'):
types['side'] = pseudo_class
specificity = (0, 0, 1)
elif pseudo_class in ('blank', 'first'):
types[pseudo_class] = True
specificity = (0, 1, 0)
else:
LOGGER.warning('Unknown @page pseudo-class "%s", '
'the whole @page rule was ignored '
'at %s:%s.',
pseudo_class,
rule.source_line, rule.source_column)
continue
elif len(tokens) == 1 and tokens[0].type == 'ident':
types['name'] = tokens[0].value
specificity = (1, 0, 0)
else:
LOGGER.warning('Unsupported @page selector "%s", '
'the whole @page rule was ignored at %s:%s.',
tinycss2.serialize(rule.prelude),
rule.source_line, rule.source_column)
continue
page_type = PageType(**types)
# Use a double lambda to have a closure that holds page_types
match = (lambda page_type: lambda page_names: list(
matching_page_types(page_type, names=page_names)))(page_type)
content = tinycss2.parse_declaration_list(rule.content)
declarations = list(preprocess_declarations(base_url, content))

# Use a double lambda to have a closure that holds page_types
match = (lambda page_types: lambda _document: page_types)(
PAGE_PSEUDOCLASS_TARGETS[pseudo_class])

if declarations:
selector_list = [(specificity, None, match)]
page_rules.append((rule, selector_list, declarations))
Expand Down Expand Up @@ -737,7 +757,6 @@ def get_all_computed_styles(html, user_stylesheets=None,
pseudo-element type, and return a StyleDict object.
"""

# List stylesheets. Order here is not important ('origin' is).
sheets = []
for sheet in (html._ua_stylesheets() or []):
Expand Down Expand Up @@ -774,20 +793,6 @@ def get_all_computed_styles(html, user_stylesheets=None,
weight = (precedence, specificity)
add_declaration(cascaded_styles, name, values, weight, element)

for sheet, origin, sheet_specificity in sheets:
# Add declarations for page elements
for _rule, selector_list, declarations in sheet.page_rules:
for selector in selector_list:
specificity, pseudo_type, match = selector
specificity = sheet_specificity or specificity
for element in match(html):
for name, values, importance in declarations:
precedence = declaration_precedence(origin, importance)
weight = (precedence, specificity)
add_declaration(
cascaded_styles, name, values, weight, element,
pseudo_type)

# keys: (element, pseudo_element_type), like cascaded_styles
# values: StyleDict objects:
# keys: property name as a string
Expand Down Expand Up @@ -817,17 +822,21 @@ def get_all_computed_styles(html, user_stylesheets=None,
parent=(element.parent.etree_element if element.parent else None),
base_url=html.base_url)

# Then computed styles for @page.
page_names = set(style['page'] for style in computed_styles.values())

# Iterate on all possible page types, even if there is no cascaded style
# for them.
for page_type in PAGE_PSEUDOCLASS_TARGETS[None]:
set_computed_styles(
cascaded_styles, computed_styles, page_type,
# @page inherits from the root element:
# http://lists.w3.org/Archives/Public/www-style/2012Jan/1164.html
root=html.etree_element, parent=html.etree_element,
base_url=html.base_url)
for sheet, origin, sheet_specificity in sheets:
# Add declarations for page elements
for _rule, selector_list, declarations in sheet.page_rules:
for selector in selector_list:
specificity, pseudo_type, match = selector
specificity = sheet_specificity or specificity
for page_type in match(page_names):
for name, values, importance in declarations:
precedence = declaration_precedence(origin, importance)
weight = (precedence, specificity)
add_declaration(
cascaded_styles, name, values, weight, page_type,
pseudo_type)

# Then computed styles for pseudo elements, in any order.
# Pseudo-elements inherit from their associated element so they come
Expand All @@ -838,7 +847,7 @@ def get_all_computed_styles(html, user_stylesheets=None,
# Only iterate on pseudo-elements that have cascaded styles. (Others
# might as well not exist.)
for element, pseudo_type in cascaded_styles:
if pseudo_type:
if pseudo_type and not isinstance(element, PageType):
set_computed_styles(
cascaded_styles, computed_styles, element,
pseudo_type=pseudo_type,
Expand Down Expand Up @@ -868,4 +877,4 @@ def style_for(element, pseudo_type=None, __get=computed_styles.get):

return style

return style_for
return style_for, cascaded_styles, computed_styles
1 change: 1 addition & 0 deletions weasyprint/css/html5_ua.css
Original file line number Diff line number Diff line change
Expand Up @@ -347,6 +347,7 @@ li { display: list-item; unicode-bidi: isolate; }
link { display: none; }
listing { display: block; font-family: monospace; margin-bottom: 1em; margin-top: 1em; unicode-bidi: isolate; white-space: pre; }
mark { background: yellow; color: black; }
main { display: block; unicode-bidi: isolate; }

menu { display: block; list-style-type: disc; margin-bottom: 1em; margin-top: 1em; padding-left: 40px; unicode-bidi: isolate; }

Expand Down
3 changes: 2 additions & 1 deletion weasyprint/css/properties.py
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,8 @@
'image_rendering': 'auto',

# Paged Media 3: https://www.w3.org/TR/css3-page/
'size': None, # XXX set to A4 in computed_values
'size': None, # set to A4 in computed_values
'page': 'auto',

# Text 3/4: https://www.w3.org/TR/css-text-4/
'hyphenate_character': '‐', # computed value chosen by the user agent
Expand Down
8 changes: 8 additions & 0 deletions weasyprint/css/validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -609,6 +609,14 @@ def break_inside(keyword):
return keyword in ('auto', 'avoid', 'avoid-page', 'avoid-column')


@validator()
@single_token
def page(token):
"""``page`` property validation."""
if token.type == 'ident':
return 'auto' if token.lower_value == 'auto' else token.value


@validator('outline-style')
@single_keyword
def outline_style(keyword):
Expand Down
4 changes: 2 additions & 2 deletions weasyprint/document.py
Original file line number Diff line number Diff line change
Expand Up @@ -301,7 +301,7 @@ def _render(cls, html, stylesheets, enable_hinting,
presentational_hints=False):
font_config = FontConfiguration()
page_rules = []
style_for = get_all_computed_styles(
style_for, cascaded_styles, computed_styles = get_all_computed_styles(
html, presentational_hints=presentational_hints, user_stylesheets=[
css if hasattr(css, 'matcher')
else CSS(guess=css, media_type=html.media_type)
Expand All @@ -315,7 +315,7 @@ def _render(cls, html, stylesheets, enable_hinting,
build_formatting_structure(
html.etree_element, style_for, get_image_from_uri,
html.base_url),
font_config)
font_config, html, cascaded_styles, computed_styles)
rendering = cls(
[Page(p, enable_hinting) for p in page_boxes],
DocumentMetadata(**html._get_metadata()), html.url_fetcher)
Expand Down
17 changes: 17 additions & 0 deletions weasyprint/formatting_structure/boxes.py
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,12 @@ def is_in_normal_flow(self):
"""Return whether this box is in normal flow."""
return not (self.is_floated() or self.is_absolutely_positioned())

# 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'])


class ParentBox(Box):
"""A box that has children."""
Expand Down Expand Up @@ -331,6 +337,14 @@ def get_wrapped_table(self):
else: # pragma: no cover
raise ValueError('Table wrapper without a table')

def page_values(self):
start_value, end_value = super(ParentBox, self).page_values()
if self.children:
start_box, end_box = self.children[0], self.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.
Expand Down Expand Up @@ -521,6 +535,9 @@ def translate(self, dx=0, dy=0):
position + dx for position in self.column_positions]
return super(TableBox, self).translate(dx, dy)

def page_values(self):
return (self.style['page'], self.style['page'])


class InlineTableBox(TableBox):
"""Box for elements with ``display: inline-table``"""
Expand Down
5 changes: 3 additions & 2 deletions weasyprint/layout/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ def layout_fixed_boxes(context, pages):


def layout_document(enable_hinting, style_for, get_image_from_uri, root_box,
font_config):
font_config, html, cascaded_styles, computed_styles):
"""Lay out the whole document.
This includes line breaks, page breaks, absolute size and position for all
Expand All @@ -50,7 +50,8 @@ def layout_document(enable_hinting, style_for, get_image_from_uri, root_box,
"""
context = LayoutContext(
enable_hinting, style_for, get_image_from_uri, font_config)
pages = list(make_all_pages(context, root_box))
pages = list(make_all_pages(
context, root_box, html, cascaded_styles, computed_styles))
page_counter = [1]
counter_values = {'page': page_counter, 'pages': [len(pages)]}
for i, page in enumerate(pages):
Expand Down
Loading

0 comments on commit 55fab4b

Please sign in to comment.