1
1
mirror of https://github.com/Kozea/WeasyPrint.git synced 2024-10-04 07:57:52 +03:00

Handle @page CSS rules.

This commit is contained in:
Simon Sapin 2011-07-01 18:08:24 +02:00
parent 36a62d7174
commit f2ed585bea
6 changed files with 215 additions and 27 deletions

View File

@ -54,6 +54,7 @@ from . import shorthands
from . import inheritance
from . import initial_values
from . import computed_values
from . import page
HTML4_DEFAULT_STYLESHEET = parseFile(os.path.join(os.path.dirname(__file__),
@ -288,6 +289,33 @@ def apply_style_rule(rule, document, origin):
element.applicable_properties.append((precedence, prop))
def apply_page_rule(rule, page_pseudo_elements, origin):
# TODO: support "page names" in page selectors (see CSS3 Paged Media)
pseudo_class = rule.selectorText
page_types = page.PAGE_PSEUDOCLASS_TARGETS.get(pseudo_class, None)
if page_types is not None:
for prop in rule.style:
# In CSS 2.1, only the margin properties apply within
# the page context.
# TODO: add support
if prop.name in page.PAGE_CONTEXT_PROPERTIES:
precedence = (
declaration_precedence(origin, prop.priority),
page.PAGE_PSEUDOCLASS_SPECIFICITY[pseudo_class]
)
for page_type in page_types:
element = page_pseudo_elements[page_type]
element.applicable_properties.append((precedence, prop))
else:
# invalid property here.
# TODO: log/warn that something was ignored
pass
else:
# Invalid/unsupported selector, ignore the whole rule
# TODO: log/warn that something was ignored
pass
def handle_style_attribute(element):
"""
Return the elements ``applicable_properties`` list after adding properties
@ -358,7 +386,7 @@ class StyleDict(dict):
return self.__class__(self)
def assign_properties(element):
def assign_properties(element, page_context=False):
"""
Take the properties left by ``apply_style_rule`` on an element or
pseudo-element and assign computed values with respect to the cascade,
@ -368,16 +396,20 @@ def assign_properties(element):
# stability of Python's sort fulfills rule 4 of the cascade: everything
# else being equal, the latter specified value wins
# http://www.w3.org/TR/CSS21/cascade.html#cascading-order
element.applicable_properties.sort(
# This lambda has one parameter deconstructed as a tuple
key=lambda (precedence, prop): precedence)
def sort_key(applicable_property):
# The list contain (precedence, property) pairs. Sort by precedence
# only, not by property in case of equal precedence.
precedence, _property = applicable_property
return precedence
element.applicable_properties.sort(key=sort_key)
element.style = style = StyleDict()
for precedence, prop in element.applicable_properties:
style[prop.name] = prop.propertyValue
inheritance.handle_inheritance(element)
initial_values.handle_initial_values(element)
computed_values.compute_values(element)
if not page_context:
inheritance.handle_inheritance(element)
initial_values.handle_initial_values(element, page_context=page_context)
computed_values.compute_values(element, page_context=page_context)
class PseudoElement(object):
@ -400,6 +432,9 @@ class PseudoElementDict(dict):
Like a defaultdict, creates PseudoElement objects as needed.
"""
def __init__(self, element):
# Add the element itself in the list of its pseudo-elements
# so that iterating on this dict gives both the element and it
# pseudo-element.
self[''] = element
def __missing__(self, key):
@ -424,7 +459,13 @@ def annotate_document(document, user_stylesheets=None,
for element in document.iter():
element.applicable_properties = []
element.pseudo_elements = PseudoElementDict(element)
author_stylesheets = find_stylesheets(document)
page_pseudo_elements = dict(
(page_type, PseudoElement(None, page_type))
for page_type in page.PAGE_PSEUDOCLASS_TARGETS[''])
document.page_pseudo_elements = page_pseudo_elements
for sheets, origin in ((author_stylesheets, 'author'),
(user_stylesheets or [], 'user'),
(ua_stylesheets or [], 'user agent')):
@ -436,6 +477,8 @@ def annotate_document(document, user_stylesheets=None,
for rule in resolve_import_media(sheet, medium):
if rule.type == rule.STYLE_RULE:
apply_style_rule(rule, document, origin)
elif rule.type == rule.PAGE_RULE:
apply_page_rule(rule, page_pseudo_elements, origin)
# TODO: handle @font-face, @namespace, @page, and @variables
build_lxml_proxy_cache(document)
@ -444,3 +487,6 @@ def annotate_document(document, user_stylesheets=None,
for element_or_pseudo_element in element.pseudo_elements.itervalues():
assign_properties(element_or_pseudo_element)
for pseudo_element in page_pseudo_elements.itervalues():
assign_properties(pseudo_element, page_context=True)

View File

@ -151,7 +151,7 @@ def compute_font_size(element):
style.font_size = font_size
def compute_length(value, font_size):
def compute_length(value, font_size, page_context):
"""
Convert a single length value to pixels.
"""
@ -165,7 +165,10 @@ def compute_length(value, font_size):
factor = LENGTHS_TO_PIXELS[value.dimension]
return DimensionValue(str(value.value * factor) + 'px')
elif value.dimension == 'em':
return DimensionValue(str(value.value * font_size) + 'px')
if page_context:
raise ValueError('The em unit is not allowed in a page context.')
else:
return DimensionValue(str(value.value * font_size) + 'px')
elif value.dimension == 'ex':
# TODO: support ex
raise ValueError('The ex unit is not supported yet.', value.cssText)
@ -173,16 +176,20 @@ def compute_length(value, font_size):
raise ValueError('Unknown length unit', value.value, repr(value.type))
def compute_lengths(element):
def compute_lengths(element, page_context):
"""
Convert other length units to pixels.
"""
style = element.style
font_size = style.font_size
if page_context:
font_size = None
else:
font_size = style.font_size
for name in style:
# PropertyValue objects are not mutable, build a new DummyPropertyValue
style[name] = DummyPropertyValue(
compute_length(value, font_size) for value in style[name])
compute_length(value, font_size, page_context)
for value in style[name])
def compute_line_height(element):
@ -339,20 +346,22 @@ def compute_content(element):
style.content = 'normal'
def compute_values(element):
def compute_values(element, page_context):
"""
Normalize values as much as possible without rendering the document.
"""
# em lengths depend on font-size, compute font-size first
compute_font_size(element)
compute_lengths(element)
compute_line_height(element)
compute_border_width(element)
compute_outline_width(element)
compute_display_float(element)
compute_word_spacing(element)
compute_font_weight(element)
compute_content(element)
if not page_context:
# em lengths depend on font-size, compute font-size first
compute_font_size(element)
compute_lengths(element, page_context)
if not page_context:
compute_line_height(element)
compute_display_float(element)
compute_word_spacing(element)
compute_font_weight(element)
compute_content(element)
compute_border_width(element)
compute_outline_width(element)
# Recent enough cssutils have a .absolute_uri on URIValue objects.
# TODO: percentages for height?
# http://www.w3.org/TR/CSS21/visudet.html#propdef-height

View File

@ -24,6 +24,7 @@
from cssutils.css import PropertyValue, CSSStyleDeclaration
from .shorthands import expand_shorthands_in_declaration
from .page import PAGE_CONTEXT_PROPERTIES
r"""
@ -146,13 +147,15 @@ def is_initial(style, name):
return name not in style or style[name].value == 'initial'
def handle_initial_values(element):
def handle_initial_values(element, page_context):
"""
Properties that do not have a value after inheritance or whose value is the
'initial' keyword (CSS3) get their initial value.
"""
style = element.style
for name, initial in INITIAL_VALUES.iteritems():
if page_context and name not in PAGE_CONTEXT_PROPERTIES:
continue
if is_initial(style, name):
style[name] = initial
@ -162,10 +165,13 @@ def handle_initial_values(element):
for name in ('border-top-color', 'border-right-color',
'border-bottom-color', 'border-left-color'):
if is_initial(style, name):
style[name] = style['color']
if page_context:
style[name] = INITIAL_VALUES['color']
else:
style[name] = style['color']
# text-align: left in left-to-right text, right in right-to-left
if is_initial(style, 'text-align'):
if not page_context and is_initial(style, 'text-align'):
if style.direction == 'rtl':
style['text-align'] = PropertyValue('right')
else:

83
weasy/css/page.py Normal file
View File

@ -0,0 +1,83 @@
# coding: utf8
# WeasyPrint converts web documents (HTML, CSS, ...) to PDF.
# Copyright (C) 2011 Simon Sapin
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
Everything specific to paged media and @page rules.
"""
from . import shorthands
# Selectors for @page rules can have a pseudo-class, one of :first, :left
# or :right. This maps pseudo-classes to lists of "page types" selected.
PAGE_PSEUDOCLASS_TARGETS = {
'': ['left', 'right', 'first_left', 'first_right'], # No pseudo-class
':left': ['left', 'first_left'],
':right': ['right', 'first_right'],
':first': ['first_left', 'first_right'],
}
# Specificity of @page pseudo-classes for the cascade.
PAGE_PSEUDOCLASS_SPECIFICITY = {
'': 0,
':left': 1,
':right': 1,
':first': 10,
}
# Only some properties are allowed in the page context:
# http://www.w3.org/TR/css3-page/#page-properties
# These do not include shorthand properties.
PAGE_CONTEXT_PROPERTIES = set("""
background-attachment
background-color
background-image
background-position
background-repeat
border-bottom-color
border-bottom-style
border-bottom-width
border-left-color
border-left-style
border-left-width
border-right-color
border-right-style
border-right-width
border-top-color
border-top-style
border-top-width
counter-increment
counter-reset
margin-bottom
margin-left
margin-right
margin-top
padding-bottom
padding-left
padding-right
padding-top
size
""".split())

View File

@ -7,7 +7,7 @@
a:after {
content: " [" attr(href) "]"
}
@page :first { margin: 5em }
@page :first { margin-top: 5px }
ul {
border-style: none solid hidden;
border-width: thin thick 4px .25in;

View File

@ -137,3 +137,47 @@ def test_default_stylesheet():
css.annotate_document(document)
assert document.head.style.display == 'none', \
'The HTML4 user-agent stylesheet was not applied'
@suite.test
def test_page():
document = parse_html('doc1.html')
user_sheet = cssutils.parseString(u"""
@page {
margin: 10px;
}
@page :right {
margin-bottom: 12pt;
}
""")
css.annotate_document(document, user_stylesheets=[user_sheet])
style = document.page_pseudo_elements['first_left'].style
assert style.margin_top == 5
assert style.margin_left == 10
assert style.margin_bottom == 10
style = document.page_pseudo_elements['first_right'].style
assert style.margin_top == 5
assert style.margin_left == 10
assert style.margin_bottom == 16
style = document.page_pseudo_elements['left'].style
assert style.margin_top == 10
assert style.margin_left == 10
assert style.margin_bottom == 10
style = document.page_pseudo_elements['right'].style
assert style.margin_top == 10
assert style.margin_left == 10
assert style.margin_bottom == 16
user_sheet_with_em = cssutils.parseString(u"""
@page {
margin: 3em;
}
""")
with attest.raises(ValueError) as exc:
css.annotate_document(document, user_stylesheets=[user_sheet_with_em])
assert exc.args[0] == 'The em unit is not allowed in a page context.'