mirror of
https://github.com/Kozea/WeasyPrint.git
synced 2024-10-05 00:21:15 +03:00
Merge branch 'master' into fix_943
This commit is contained in:
commit
7eacb711a7
15
.travis.yml
15
.travis.yml
@ -11,15 +11,19 @@ matrix:
|
||||
python: 3.5
|
||||
- os: linux
|
||||
python: 3.6
|
||||
- dist: xenial
|
||||
- os: linux
|
||||
python: 3.7
|
||||
- dist: bionic
|
||||
python: 3.8
|
||||
- os: osx
|
||||
language: generic
|
||||
env: PYTHON=python3
|
||||
env:
|
||||
- HOMEBREW_NO_AUTO_UPDATE=1
|
||||
- PYTHON=python3
|
||||
- os: windows
|
||||
# Windows doesn't support python or even generic language
|
||||
language: cpp
|
||||
env: PYTHON=/c/Python37/python
|
||||
env: PYTHON=/c/Python38/python
|
||||
allow_failures:
|
||||
- os: windows
|
||||
|
||||
@ -41,10 +45,7 @@ before_install:
|
||||
- if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then mkdir -p ~/.fonts; fi
|
||||
- if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then wget "https://github.com/Kozea/Ahem/blob/master/Ahem.ttf?raw=true" -O ~/.fonts/Ahem.ttf; fi
|
||||
|
||||
- if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then brew tap caskroom/fonts; fi
|
||||
- if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then brew update; fi
|
||||
- if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then brew pin numpy gdal postgis; fi
|
||||
- if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then brew upgrade python; fi
|
||||
- if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then brew tap homebrew/cask-fonts; fi
|
||||
- if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then brew cask install font-dejavu-sans; fi
|
||||
- if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then brew install cairo pango gdk-pixbuf libffi; fi
|
||||
|
||||
|
@ -303,17 +303,18 @@ The `CSS Generated Content for Paged Media Module`_ (GCPM) is a working draft
|
||||
defining "new properties and values, so that authors may bring new techniques
|
||||
(running headers and footers, footnotes, page selection) to paged media".
|
||||
|
||||
`Page selectors`_ are also supported by WeasyPrint. You can select pages
|
||||
according to their position in the document:
|
||||
`Page selectors`_ are supported by WeasyPrint. You can select pages according
|
||||
to their position in the document:
|
||||
|
||||
.. code-block:: css
|
||||
|
||||
@page :nth(3) { background: red } /* Third page */
|
||||
@page :nth(2n+1) { background: green } /* Odd pages */
|
||||
|
||||
You can also use `running elements`_ to put HTML boxes into the page margins.
|
||||
|
||||
The other features of GCPM are **not** implemented:
|
||||
|
||||
- running elements (``running()`` and ``element()``);
|
||||
- footnotes (``float: footnote``, ``footnote-display``, ``footnote`` counter,
|
||||
``::footnote-call``, ``::footnote-marker``, ``@footnote`` rule,
|
||||
``footnote-policy``);
|
||||
@ -321,6 +322,7 @@ The other features of GCPM are **not** implemented:
|
||||
|
||||
.. _CSS Generated Content for Paged Media Module: http://www.w3.org/TR/css-gcpm-3/
|
||||
.. _Page selectors: https://www.w3.org/TR/css-gcpm-3/#document-page-selectors
|
||||
.. _running elements: https://www.w3.org/TR/css-gcpm-3/#running-elements
|
||||
|
||||
|
||||
CSS Generated Content Module Level 3
|
||||
|
@ -13,7 +13,7 @@ WeasyPrint |version| depends on:
|
||||
* tinycss2_ ≥ 1.0.0
|
||||
* cssselect2_ ≥ 0.1
|
||||
* CairoSVG_ ≥ 2.4.0
|
||||
* Pyphen_ ≥ 0.8
|
||||
* Pyphen_ ≥ 0.9.1
|
||||
* GDK-PixBuf_ ≥ 2.25.0 [#]_
|
||||
|
||||
.. _CPython: http://www.python.org/
|
||||
@ -113,12 +113,19 @@ If your favorite system is not listed here but you know the package names,
|
||||
Debian / Ubuntu
|
||||
~~~~~~~~~~~~~~~
|
||||
|
||||
Debian 9.0 Stretch or newer, Ubuntu 16.04 Xenial or newer:
|
||||
WeasyPrint is `packaged for Debian 11 or newer
|
||||
<https://packages.debian.org/search?searchon=names&keywords=weasyprint>`_.
|
||||
|
||||
You can install it with pip on Debian 10 Buster or newer, or on Ubuntu 18.04
|
||||
Bionic Beaver or newer, after installing the following packages:
|
||||
|
||||
.. code-block:: sh
|
||||
|
||||
sudo apt-get install build-essential python3-dev python3-pip python3-setuptools python3-wheel python3-cffi libcairo2 libpango-1.0-0 libpangocairo-1.0-0 libgdk-pixbuf2.0-0 libffi-dev shared-mime-info
|
||||
|
||||
WeasyPrint may work under previous releases of Debian or Ubuntu, but they often
|
||||
provide an old version of Cairo that may limit WeasyPrint's features [1]_.
|
||||
|
||||
Fedora
|
||||
~~~~~~
|
||||
|
||||
|
@ -26,6 +26,7 @@ classifiers =
|
||||
Programming Language :: Python :: 3.5
|
||||
Programming Language :: Python :: 3.6
|
||||
Programming Language :: Python :: 3.7
|
||||
Programming Language :: Python :: 3.8
|
||||
Topic :: Internet :: WWW/HTTP
|
||||
Topic :: Text Processing :: Markup :: HTML
|
||||
Topic :: Multimedia :: Graphics :: Graphics Conversion
|
||||
@ -48,7 +49,7 @@ install_requires =
|
||||
tinycss2>=1.0.0
|
||||
cssselect2>=0.1
|
||||
CairoSVG>=2.4.0
|
||||
Pyphen>=0.8
|
||||
Pyphen>=0.9.1
|
||||
tests_require =
|
||||
pytest-runner
|
||||
pytest-cov
|
||||
|
@ -497,7 +497,10 @@ def _content_list(computer, values):
|
||||
elif value[0] == 'attr()':
|
||||
assert value[1][1] == 'string'
|
||||
computed_value = compute_attr_function(computer, value)
|
||||
elif value[0] in ('counter()', 'counters()', 'content()', 'string()'):
|
||||
elif value[0] in (
|
||||
'counter()', 'counters()', 'content()', 'element()',
|
||||
'string()',
|
||||
):
|
||||
# Other values need layout context, their computed value cannot be
|
||||
# better than their specified value yet.
|
||||
# See build.compute_content_list.
|
||||
|
@ -743,3 +743,7 @@ def get_content_list_token(token, base_url):
|
||||
elif arg.type == 'string':
|
||||
string = arg.value
|
||||
return ('leader()', ('string', string))
|
||||
elif name == 'element':
|
||||
if len(args) != 1 or args[0].type != 'ident':
|
||||
return
|
||||
return ('element()', args[0])
|
||||
|
@ -967,10 +967,15 @@ def text_overflow(keyword):
|
||||
|
||||
|
||||
@property()
|
||||
@single_keyword
|
||||
def position(keyword):
|
||||
@single_token
|
||||
def position(token):
|
||||
"""``position`` property validation."""
|
||||
return keyword in ('static', 'relative', 'absolute', 'fixed')
|
||||
if token.type == 'function' and token.name == 'running':
|
||||
if len(token.arguments) == 1 and token.arguments[0].type == 'ident':
|
||||
return ('running()', token.arguments[0].value)
|
||||
keyword = get_single_keyword([token])
|
||||
if keyword in ('static', 'relative', 'absolute', 'fixed'):
|
||||
return keyword
|
||||
|
||||
|
||||
@property()
|
||||
@ -1364,7 +1369,7 @@ def string_set(tokens, base_url):
|
||||
if None not in parsed_tokens:
|
||||
return (var_name, parsed_tokens)
|
||||
elif tokens and get_keyword(tokens[0]) == 'none':
|
||||
return 'none'
|
||||
return 'none', ()
|
||||
|
||||
|
||||
@property()
|
||||
|
@ -88,8 +88,9 @@ class Box(object):
|
||||
def all_children(self):
|
||||
return ()
|
||||
|
||||
def __init__(self, element_tag, style):
|
||||
def __init__(self, element_tag, style, element):
|
||||
self.element_tag = element_tag
|
||||
self.element = element
|
||||
self.style = style
|
||||
self.remove_decoration_sides = set()
|
||||
|
||||
@ -101,7 +102,7 @@ class Box(object):
|
||||
"""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, *args, **kwargs)
|
||||
return cls(parent.element_tag, style, parent.element, *args, **kwargs)
|
||||
|
||||
def copy(self):
|
||||
"""Return shallow copy of the box."""
|
||||
@ -113,6 +114,10 @@ class Box(object):
|
||||
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.
|
||||
|
||||
@ -274,9 +279,15 @@ class Box(object):
|
||||
"""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())
|
||||
return not (
|
||||
self.is_floated() or self.is_absolutely_positioned() or
|
||||
self.is_running())
|
||||
|
||||
# Start and end page values for named pages
|
||||
|
||||
@ -287,8 +298,8 @@ class Box(object):
|
||||
|
||||
class ParentBox(Box):
|
||||
"""A box that has children."""
|
||||
def __init__(self, element_tag, style, children):
|
||||
super(ParentBox, self).__init__(element_tag, style)
|
||||
def __init__(self, element_tag, style, element, children):
|
||||
super(ParentBox, self).__init__(element_tag, style, element)
|
||||
self.children = tuple(children)
|
||||
|
||||
def all_children(self):
|
||||
@ -320,6 +331,11 @@ class ParentBox(Box):
|
||||
|
||||
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
|
||||
@ -450,9 +466,9 @@ class TextBox(InlineLevelBox):
|
||||
ascii_to_wide = dict((i, chr(i + 0xfee0)) for i in range(0x21, 0x7f))
|
||||
ascii_to_wide.update({0x20: '\u3000', 0x2D: '\u2212'})
|
||||
|
||||
def __init__(self, element_tag, style, text):
|
||||
def __init__(self, element_tag, style, element, text):
|
||||
assert text
|
||||
super(TextBox, self).__init__(element_tag, style)
|
||||
super(TextBox, self).__init__(element_tag, style, element)
|
||||
text_transform = style['text_transform']
|
||||
if text_transform != 'none':
|
||||
text = {
|
||||
@ -500,8 +516,8 @@ class ReplacedBox(Box):
|
||||
and is opaque from CSS’s point of view.
|
||||
|
||||
"""
|
||||
def __init__(self, element_tag, style, replacement):
|
||||
super(ReplacedBox, self).__init__(element_tag, style)
|
||||
def __init__(self, element_tag, style, element, replacement):
|
||||
super(ReplacedBox, self).__init__(element_tag, style, element)
|
||||
self.replacement = replacement
|
||||
|
||||
|
||||
@ -651,7 +667,7 @@ class PageBox(ParentBox):
|
||||
self.page_type = page_type
|
||||
# Page boxes are not linked to any element.
|
||||
super(PageBox, self).__init__(
|
||||
element_tag=None, style=style, children=[])
|
||||
element_tag=None, style=style, element=None, children=[])
|
||||
|
||||
def __repr__(self):
|
||||
return '<%s %s>' % (type(self).__name__, self.page_type)
|
||||
@ -663,7 +679,7 @@ class MarginBox(BlockContainerBox):
|
||||
self.at_keyword = at_keyword
|
||||
# Margin boxes are not linked to any element.
|
||||
super(MarginBox, self).__init__(
|
||||
element_tag=None, style=style, children=[])
|
||||
element_tag=None, style=style, element=None, children=[])
|
||||
|
||||
def __repr__(self):
|
||||
return '<%s %s>' % (type(self).__name__, self.at_keyword)
|
||||
|
@ -80,9 +80,10 @@ def build_formatting_structure(element_tree, style_for, get_image_from_uri,
|
||||
return box
|
||||
|
||||
|
||||
def make_box(element_tag, style, content):
|
||||
return BOX_TYPE_FROM_DISPLAY[style['display']](
|
||||
element_tag, style, content)
|
||||
def make_box(element_tag, style, content, element):
|
||||
box = BOX_TYPE_FROM_DISPLAY[style['display']](
|
||||
element_tag, style, element, content)
|
||||
return box
|
||||
|
||||
|
||||
def element_to_box(element, style_for, get_image_from_uri, base_url,
|
||||
@ -122,7 +123,7 @@ def element_to_box(element, style_for, get_image_from_uri, base_url,
|
||||
if display == 'none':
|
||||
return []
|
||||
|
||||
box = make_box(element.tag, style, [])
|
||||
box = make_box(element.tag, style, [], element)
|
||||
|
||||
if state is None:
|
||||
# use a list to have a shared mutable object
|
||||
@ -227,7 +228,7 @@ def before_after_to_box(element, pseudo_type, state, style_for,
|
||||
if 'none' in (display, content) or content in ('normal', 'inhibit'):
|
||||
return []
|
||||
|
||||
box = make_box('%s::%s' % (element.tag, pseudo_type), style, [])
|
||||
box = make_box('%s::%s' % (element.tag, pseudo_type), style, [], element)
|
||||
|
||||
quote_depth, counter_values, _counter_scopes = state
|
||||
update_counters(state, style)
|
||||
@ -264,7 +265,7 @@ def marker_to_box(element, state, parent_style, style_for, get_image_from_uri,
|
||||
# `content` where 'normal' computes as 'inhibit' for pseudo elements.
|
||||
quote_depth, counter_values, _counter_scopes = state
|
||||
|
||||
box = make_box('%s::marker' % element.tag, style, children)
|
||||
box = make_box('%s::marker' % element.tag, style, children, element)
|
||||
|
||||
if style['display'] == 'none':
|
||||
return
|
||||
@ -492,6 +493,26 @@ def compute_content_list(content_list, parent_box, counter_values, css_token,
|
||||
texts.append(quotes[min(quote_depth[0], len(quotes) - 1)])
|
||||
if is_open:
|
||||
quote_depth[0] += 1
|
||||
elif type_ == 'element()':
|
||||
if value.value not in context.running_elements:
|
||||
# TODO: emit warning
|
||||
continue
|
||||
new_box = None
|
||||
for i in range(context.current_page - 1, -1, -1):
|
||||
if i not in context.running_elements[value.value]:
|
||||
continue
|
||||
running_box = context.running_elements[value.value][i]
|
||||
new_box = running_box.deepcopy()
|
||||
break
|
||||
new_box.style['position'] = 'static'
|
||||
for child in new_box.descendants():
|
||||
if child.style['content'] in ('normal', 'none'):
|
||||
continue
|
||||
child.children = content_to_boxes(
|
||||
child.style, child, quote_depth, counter_values,
|
||||
get_image_from_uri, target_collector, context=context,
|
||||
page=page)
|
||||
boxlist.append(new_box)
|
||||
text = ''.join(texts)
|
||||
if text:
|
||||
boxlist.append(boxes.TextBox.anonymous_from(parent_box, text))
|
||||
@ -1247,7 +1268,7 @@ def inline_in_block(box):
|
||||
]
|
||||
|
||||
"""
|
||||
if not isinstance(box, boxes.ParentBox):
|
||||
if not isinstance(box, boxes.ParentBox) or box.is_running():
|
||||
return box
|
||||
|
||||
box_children = list(box.children)
|
||||
@ -1382,7 +1403,7 @@ def block_in_inline(box):
|
||||
]
|
||||
|
||||
"""
|
||||
if not isinstance(box, boxes.ParentBox):
|
||||
if not isinstance(box, boxes.ParentBox) or box.is_running():
|
||||
return box
|
||||
|
||||
new_children = []
|
||||
|
@ -111,7 +111,7 @@ def make_replaced_box(element, box, image):
|
||||
else:
|
||||
# TODO: support images with 'display: table-cell'?
|
||||
type_ = boxes.InlineReplacedBox
|
||||
new_box = type_(element.tag, box.style, image)
|
||||
new_box = type_(element.tag, box.style, element, image)
|
||||
# TODO: check other attributes that need to be copied
|
||||
# TODO: find another solution
|
||||
new_box.string_set = box.string_set
|
||||
|
@ -196,6 +196,7 @@ class LayoutContext(object):
|
||||
self._excluded_shapes_lists = []
|
||||
self.excluded_shapes = None # Not initialized yet
|
||||
self.string_set = defaultdict(lambda: defaultdict(lambda: list()))
|
||||
self.running_elements = {}
|
||||
self.current_page = None
|
||||
self.forced_break = False
|
||||
|
||||
|
@ -259,13 +259,6 @@ def block_container_layout(context, box, max_position_y, skip_stack,
|
||||
# block_container_layout, there's probably a better solution.
|
||||
assert isinstance(box, (boxes.BlockContainerBox, boxes.FlexBox))
|
||||
|
||||
# TODO: this should make a difference, but that is currently neglected.
|
||||
# See http://www.w3.org/TR/CSS21/visudet.html#normal-block
|
||||
# http://www.w3.org/TR/CSS21/visudet.html#root-height
|
||||
|
||||
# if box.style['overflow'] != 'visible':
|
||||
# ...
|
||||
|
||||
# We have to work around floating point rounding errors here.
|
||||
# The 1e-9 value comes from PEP 485.
|
||||
allowed_max_position_y = max_position_y * (1 + 1e-9)
|
||||
@ -357,6 +350,10 @@ def block_container_layout(context, box, max_position_y, skip_stack,
|
||||
break
|
||||
resume_at = (index, None)
|
||||
break
|
||||
elif child.is_running():
|
||||
context.running_elements.setdefault(
|
||||
child.style['position'][1], {}
|
||||
)[context.current_page - 1] = child
|
||||
continue
|
||||
|
||||
if isinstance(child, boxes.LineBox):
|
||||
|
@ -858,11 +858,27 @@ def split_inline_box(context, box, position_x, max_x, skip_stack,
|
||||
# add the original skip stack to the partial
|
||||
# skip stack we get after the new rendering.
|
||||
|
||||
# We have to do:
|
||||
# resume_at + initial_skip_stack
|
||||
# but adding skip stacks is a bit complicated
|
||||
current_skip_stack = initial_skip_stack
|
||||
current_resume_at = (child_index, child_resume_at)
|
||||
# Combining skip stacks is a bit complicated
|
||||
# We have to:
|
||||
# - set `child_index` as the first number
|
||||
# - append the new stack if it's an absolute one
|
||||
# - otherwise append the combined stacks
|
||||
# (resume_at + initial_skip_stack)
|
||||
|
||||
# extract the initial index
|
||||
if initial_skip_stack is None:
|
||||
current_skip_stack = None
|
||||
initial_index = 0
|
||||
else:
|
||||
initial_index, current_skip_stack = (
|
||||
initial_skip_stack)
|
||||
# child_resume_at is an absolute skip stack
|
||||
if child_index > initial_index:
|
||||
resume_at = (child_index, child_resume_at)
|
||||
break
|
||||
|
||||
# combine the stacks
|
||||
current_resume_at = child_resume_at
|
||||
stack = []
|
||||
while current_skip_stack and current_resume_at:
|
||||
skip, current_skip_stack = (
|
||||
@ -875,6 +891,8 @@ def split_inline_box(context, box, position_x, max_x, skip_stack,
|
||||
resume_at = current_resume_at
|
||||
while stack:
|
||||
resume_at = (stack.pop(), resume_at)
|
||||
# insert the child index
|
||||
resume_at = (child_index, resume_at)
|
||||
break
|
||||
if break_found:
|
||||
break
|
||||
|
@ -345,10 +345,11 @@ def make_margin_boxes(context, page, state):
|
||||
box.style, box, quote_depth, counter_values,
|
||||
context.get_image_from_uri, context.target_collector, context,
|
||||
page)
|
||||
# content_to_boxes() only produces inline-level boxes, no need to
|
||||
# run other post-processors from build.build_formatting_structure()
|
||||
box = build.inline_in_block(box)
|
||||
build.process_whitespace(box)
|
||||
box = build.anonymous_table_boxes(box)
|
||||
box = build.flex_boxes(box)
|
||||
box = build.inline_in_block(box)
|
||||
box = build.block_in_inline(box)
|
||||
resolve_percentages(box, containing_block)
|
||||
if not box.is_generated:
|
||||
box.width = box.height = 0
|
||||
|
@ -182,7 +182,7 @@ def table_layout(context, table, max_position_y, skip_stack, containing_block,
|
||||
else:
|
||||
row.height = max(row.height, max(
|
||||
row_cell.height for row_cell in ending_cells))
|
||||
row_bottom_y = cell.position_y + row.height
|
||||
row_bottom_y = row.position_y + row.height
|
||||
else:
|
||||
row_bottom_y = row.position_y
|
||||
row.height = 0
|
||||
|
@ -382,6 +382,59 @@ def test_breaking_linebox_regression_10():
|
||||
assert line4.children[0].text == ')x'
|
||||
|
||||
|
||||
@assert_no_logs
|
||||
def test_breaking_linebox_regression_11():
|
||||
# Regression test for https://github.com/Kozea/WeasyPrint/issues/953
|
||||
page, = parse(
|
||||
'<style>@font-face {src: url(AHEM____.TTF); font-family: ahem}</style>'
|
||||
'<p style="width:10em; font-family: ahem">'
|
||||
' line 1<br><span>123 567 90</span>x'
|
||||
'</p>')
|
||||
html, = page.children
|
||||
body, = html.children
|
||||
p, = body.children
|
||||
line1, line2, line3 = p.children
|
||||
assert line1.children[0].text == 'line 1'
|
||||
assert line2.children[0].children[0].text == '123 567'
|
||||
assert line3.children[0].children[0].text == '90'
|
||||
assert line3.children[1].text == 'x'
|
||||
|
||||
|
||||
@assert_no_logs
|
||||
def test_breaking_linebox_regression_12():
|
||||
# Regression test for https://github.com/Kozea/WeasyPrint/issues/953
|
||||
page, = parse(
|
||||
'<style>@font-face {src: url(AHEM____.TTF); font-family: ahem}</style>'
|
||||
'<p style="width:10em; font-family: ahem">'
|
||||
' <br><span>123 567 90</span>x'
|
||||
'</p>')
|
||||
html, = page.children
|
||||
body, = html.children
|
||||
p, = body.children
|
||||
line1, line2, line3 = p.children
|
||||
assert line2.children[0].children[0].text == '123 567'
|
||||
assert line3.children[0].children[0].text == '90'
|
||||
assert line3.children[1].text == 'x'
|
||||
|
||||
|
||||
@assert_no_logs
|
||||
def test_breaking_linebox_regression_13():
|
||||
# Regression test for https://github.com/Kozea/WeasyPrint/issues/953
|
||||
page, = parse(
|
||||
'<style>@font-face {src: url(AHEM____.TTF); font-family: ahem}</style>'
|
||||
'<p style="width:10em; font-family: ahem">'
|
||||
' 123 567 90 <span>123 567 90</span>x'
|
||||
'</p>')
|
||||
html, = page.children
|
||||
body, = html.children
|
||||
p, = body.children
|
||||
line1, line2, line3 = p.children
|
||||
assert line1.children[0].text == '123 567 90'
|
||||
assert line2.children[0].children[0].text == '123 567'
|
||||
assert line3.children[0].children[0].text == '90'
|
||||
assert line3.children[1].text == 'x'
|
||||
|
||||
|
||||
@assert_no_logs
|
||||
def test_linebox_text():
|
||||
page, = parse('''
|
||||
|
@ -1214,3 +1214,62 @@ def test_margin_boxes_vertical_align():
|
||||
assert line_1.position_y == 3
|
||||
assert line_2.position_y == 43
|
||||
assert line_3.position_y == 83
|
||||
|
||||
|
||||
@assert_no_logs
|
||||
def test_margin_boxes_element():
|
||||
pages = render_pages('''
|
||||
<html>
|
||||
<head>
|
||||
<style type="text/css">
|
||||
.footer {
|
||||
position: running(footer);
|
||||
}
|
||||
@page {
|
||||
@bottom-center {
|
||||
content: element(footer);
|
||||
}
|
||||
}
|
||||
h1 {
|
||||
margin-bottom: 15cm;
|
||||
}
|
||||
.page:before {
|
||||
content: counter(page);
|
||||
}
|
||||
.pages:after {
|
||||
content: counter(pages);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="footer">
|
||||
<span class="page" /> of <span class="pages" />
|
||||
</div>
|
||||
<h1>test1</h1>
|
||||
<h1>test2</h1>
|
||||
<h1>test3</h1>
|
||||
<h1>test4</h1>
|
||||
<h1>test5</h1>
|
||||
<h1>test6</h1>
|
||||
<div class="footer">
|
||||
Last page will be a static footer
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
''')
|
||||
# first footer
|
||||
footer1_text = ''.join(
|
||||
getattr(node, 'text', '')
|
||||
for node in pages[0].children[1].descendants())
|
||||
assert footer1_text == '1 of 3'
|
||||
|
||||
# second footer
|
||||
footer2_text = ''.join(
|
||||
getattr(node, 'text', '')
|
||||
for node in pages[1].children[1].descendants())
|
||||
assert footer2_text == '2 of 3'
|
||||
# last footer
|
||||
footer3_text = ''.join(
|
||||
getattr(node, 'text', '')
|
||||
for node in pages[2].children[1].descendants())
|
||||
assert footer3_text == 'Last page will be a static footer'
|
||||
|
@ -147,6 +147,7 @@ ffi.cdef('''
|
||||
void g_type_init (void);
|
||||
|
||||
void pango_layout_set_width (PangoLayout *layout, int width);
|
||||
PangoAttrList * pango_layout_get_attributes(PangoLayout *layout);
|
||||
void pango_layout_set_attributes (
|
||||
PangoLayout *layout, PangoAttrList *attrs);
|
||||
void pango_layout_set_text (
|
||||
@ -766,7 +767,10 @@ class Layout(object):
|
||||
if text and (word_spacing != 0 or letter_spacing != 0):
|
||||
letter_spacing = units_from_double(letter_spacing)
|
||||
space_spacing = units_from_double(word_spacing) + letter_spacing
|
||||
attr_list = pango.pango_attr_list_new()
|
||||
attr_list = pango.pango_layout_get_attributes(self.layout)
|
||||
if not attr_list:
|
||||
# TODO: list should be freed
|
||||
attr_list = pango.pango_attr_list_new()
|
||||
|
||||
def add_attr(start, end, spacing):
|
||||
# TODO: attributes should be freed
|
||||
@ -781,7 +785,6 @@ class Layout(object):
|
||||
position = bytestring.find(b' ', position + 1)
|
||||
|
||||
pango.pango_layout_set_attributes(self.layout, attr_list)
|
||||
pango.pango_attr_list_unref(attr_list)
|
||||
|
||||
# Tabs width
|
||||
if b'\t' in bytestring:
|
||||
|
0
weasyprint/tools/renderer.py
Executable file → Normal file
0
weasyprint/tools/renderer.py
Executable file → Normal file
Loading…
Reference in New Issue
Block a user