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:
parent
36a62d7174
commit
f2ed585bea
@ -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 element’s ``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)
|
||||
|
@ -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
|
||||
|
@ -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
83
weasy/css/page.py
Normal 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())
|
@ -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;
|
||||
|
@ -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.'
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user