ajout fichiers manquant
This commit is contained in:
1115
venv/Lib/site-packages/weasyprint/css/__init__.py
Normal file
1115
venv/Lib/site-packages/weasyprint/css/__init__.py
Normal file
File diff suppressed because it is too large
Load Diff
301
venv/Lib/site-packages/weasyprint/css/counters.py
Normal file
301
venv/Lib/site-packages/weasyprint/css/counters.py
Normal file
@@ -0,0 +1,301 @@
|
||||
"""
|
||||
weasyprint.css.counters
|
||||
-----------------------
|
||||
|
||||
Implement counter styles.
|
||||
|
||||
These are defined in CSS Counter Styles Level 3:
|
||||
https://www.w3.org/TR/css-counter-styles-3/#counter-style-system
|
||||
|
||||
"""
|
||||
|
||||
from copy import deepcopy
|
||||
|
||||
from .utils import remove_whitespace
|
||||
|
||||
|
||||
def symbol(string_or_url):
|
||||
"""Create a string from a symbol."""
|
||||
# TODO: this function should handle images too, and return something else
|
||||
# than strings.
|
||||
type_, value = string_or_url
|
||||
if type_ == 'string':
|
||||
return value
|
||||
return ''
|
||||
|
||||
|
||||
def parse_counter_style_name(tokens, counter_style):
|
||||
tokens = remove_whitespace(tokens)
|
||||
if len(tokens) == 1:
|
||||
token, = tokens
|
||||
if token.type == 'ident':
|
||||
if token.lower_value in ('decimal', 'disc'):
|
||||
if token.lower_value not in counter_style:
|
||||
return token.value
|
||||
elif token.lower_value != 'none':
|
||||
return token.value
|
||||
|
||||
|
||||
class CounterStyle(dict):
|
||||
"""Counter styles dictionary.
|
||||
|
||||
.. versionadded:: 0.52
|
||||
|
||||
Keep a list of counter styles defined by ``@counter-style`` rules, indexed
|
||||
by their names.
|
||||
|
||||
See https://www.w3.org/TR/css-counter-styles-3/.
|
||||
|
||||
"""
|
||||
def resolve_counter(self, counter_name, previous_types=None):
|
||||
if counter_name[0] in ('symbols()', 'string'):
|
||||
counter_type, arguments = counter_name
|
||||
if counter_type == 'string':
|
||||
system = (None, 'cyclic', None)
|
||||
symbols = (('string', arguments),)
|
||||
suffix = ('string', '')
|
||||
elif counter_type == 'symbols()':
|
||||
system = (
|
||||
None, arguments[0], 1 if arguments[0] == 'fixed' else None)
|
||||
symbols = tuple(
|
||||
('string', argument) for argument in arguments[1:])
|
||||
suffix = ('string', ' ')
|
||||
return {
|
||||
'system': system,
|
||||
'negative': (('string', '-'), ('string', '')),
|
||||
'prefix': ('string', ''),
|
||||
'suffix': suffix,
|
||||
'range': 'auto',
|
||||
'pad': (0, ''),
|
||||
'fallback': 'decimal',
|
||||
'symbols': symbols,
|
||||
'additive_symbols': (),
|
||||
}
|
||||
elif counter_name in self:
|
||||
# Avoid circular fallbacks
|
||||
if previous_types is None:
|
||||
previous_types = []
|
||||
elif counter_name in previous_types:
|
||||
return
|
||||
previous_types.append(counter_name)
|
||||
|
||||
counter = self[counter_name].copy()
|
||||
if counter['system']:
|
||||
extends, system, _ = counter['system']
|
||||
else:
|
||||
extends, system = None, 'symbolic'
|
||||
|
||||
# Handle extends
|
||||
while extends:
|
||||
if system in self:
|
||||
extended_counter = self[system]
|
||||
counter['system'] = extended_counter['system']
|
||||
previous_types.append(system)
|
||||
if counter['system']:
|
||||
extends, system, _ = counter['system']
|
||||
else:
|
||||
extends, system = None, 'symbolic'
|
||||
if extends and system in previous_types:
|
||||
extends, system = 'extends', 'decimal'
|
||||
continue
|
||||
for name, value in extended_counter.items():
|
||||
if counter[name] is None and value is not None:
|
||||
counter[name] = value
|
||||
else:
|
||||
return counter
|
||||
|
||||
return counter
|
||||
|
||||
def render_value(self, counter_value, counter_name=None, counter=None,
|
||||
previous_types=None):
|
||||
"""Generate the counter representation.
|
||||
|
||||
See https://www.w3.org/TR/css-counter-styles-3/#generate-a-counter
|
||||
|
||||
"""
|
||||
assert counter or counter_name
|
||||
counter = counter or self.resolve_counter(counter_name, previous_types)
|
||||
if counter is None:
|
||||
if 'decimal' in self:
|
||||
return self.render_value(counter_value, 'decimal')
|
||||
else:
|
||||
# Could happen if the UA stylesheet is not used
|
||||
return ''
|
||||
|
||||
if counter['system']:
|
||||
extends, system, fixed_number = counter['system']
|
||||
else:
|
||||
extends, system, fixed_number = None, 'symbolic', None
|
||||
|
||||
# Avoid circular fallbacks
|
||||
if previous_types is None:
|
||||
previous_types = []
|
||||
elif system in previous_types:
|
||||
return self.render_value(counter_value, 'decimal')
|
||||
previous_types.append(counter_name)
|
||||
|
||||
# Handle extends
|
||||
while extends:
|
||||
if system in self:
|
||||
extended_counter = self[system]
|
||||
counter['system'] = extended_counter['system']
|
||||
if counter['system']:
|
||||
extends, system, fixed_number = counter['system']
|
||||
else:
|
||||
extends, system, fixed_number = None, 'symbolic', None
|
||||
if system in previous_types:
|
||||
return self.render_value(counter_value, 'decimal')
|
||||
previous_types.append(system)
|
||||
for name, value in extended_counter.items():
|
||||
if counter[name] is None and value is not None:
|
||||
counter[name] = value
|
||||
else:
|
||||
return self.render_value(counter_value, 'decimal')
|
||||
|
||||
# Step 2
|
||||
if counter['range'] in ('auto', None):
|
||||
min_range, max_range = -float('inf'), float('inf')
|
||||
if system in ('alphabetic', 'symbolic'):
|
||||
min_range = 1
|
||||
elif system == 'additive':
|
||||
min_range = 0
|
||||
counter_ranges = ((min_range, max_range),)
|
||||
else:
|
||||
counter_ranges = counter['range']
|
||||
for min_range, max_range in counter_ranges:
|
||||
if min_range <= counter_value <= max_range:
|
||||
break
|
||||
else:
|
||||
return self.render_value(
|
||||
counter_value, counter['fallback'] or 'decimal',
|
||||
previous_types=previous_types)
|
||||
|
||||
# Step 3
|
||||
initial = None
|
||||
is_negative = counter_value < 0
|
||||
if is_negative:
|
||||
negative_prefix, negative_suffix = (
|
||||
symbol(character) for character
|
||||
in counter['negative'] or (('string', '-'), ('string', '')))
|
||||
use_negative = (
|
||||
system in
|
||||
('symbolic', 'alphabetic', 'numeric', 'additive'))
|
||||
if use_negative:
|
||||
counter_value = abs(counter_value)
|
||||
|
||||
# TODO: instead of using the decimal fallback when we have the wrong
|
||||
# number of symbols, we should discard the whole counter. The problem
|
||||
# only happens when extending from another style, it is easily refused
|
||||
# during validation otherwise.
|
||||
|
||||
if system == 'cyclic':
|
||||
length = len(counter['symbols'])
|
||||
if length < 1:
|
||||
return self.render_value(counter_value, 'decimal')
|
||||
index = (counter_value - 1) % length
|
||||
initial = symbol(counter['symbols'][index])
|
||||
|
||||
elif system == 'fixed':
|
||||
length = len(counter['symbols'])
|
||||
if length < 1:
|
||||
return self.render_value(counter_value, 'decimal')
|
||||
index = counter_value - fixed_number
|
||||
if 0 <= index < length:
|
||||
initial = symbol(counter['symbols'][index])
|
||||
else:
|
||||
return self.render_value(
|
||||
counter_value, counter['fallback'] or 'decimal',
|
||||
previous_types=previous_types)
|
||||
|
||||
elif system == 'symbolic':
|
||||
length = len(counter['symbols'])
|
||||
if length < 1:
|
||||
return self.render_value(counter_value, 'decimal')
|
||||
index = (counter_value - 1) % length
|
||||
repeat = (counter_value - 1) // length + 1
|
||||
initial = symbol(counter['symbols'][index]) * repeat
|
||||
|
||||
elif system == 'alphabetic':
|
||||
length = len(counter['symbols'])
|
||||
if length < 2:
|
||||
return self.render_value(counter_value, 'decimal')
|
||||
reversed_parts = []
|
||||
while counter_value != 0:
|
||||
counter_value -= 1
|
||||
reversed_parts.append(symbol(
|
||||
counter['symbols'][counter_value % length]))
|
||||
counter_value //= length
|
||||
initial = ''.join(reversed(reversed_parts))
|
||||
|
||||
elif system == 'numeric':
|
||||
if counter_value == 0:
|
||||
initial = symbol(counter['symbols'][0])
|
||||
else:
|
||||
reversed_parts = []
|
||||
length = len(counter['symbols'])
|
||||
if length < 2:
|
||||
return self.render_value(counter_value, 'decimal')
|
||||
counter_value = abs(counter_value)
|
||||
while counter_value != 0:
|
||||
reversed_parts.append(symbol(
|
||||
counter['symbols'][counter_value % length]))
|
||||
counter_value //= length
|
||||
initial = ''.join(reversed(reversed_parts))
|
||||
|
||||
elif system == 'additive':
|
||||
if counter_value == 0:
|
||||
for weight, symbol_string in counter['additive_symbols']:
|
||||
if weight == 0:
|
||||
initial = symbol(symbol_string)
|
||||
else:
|
||||
parts = []
|
||||
if len(counter['additive_symbols']) < 1:
|
||||
return self.render_value(counter_value, 'decimal')
|
||||
for weight, symbol_string in counter['additive_symbols']:
|
||||
repetitions = counter_value // weight
|
||||
parts.extend([symbol(symbol_string)] * repetitions)
|
||||
counter_value -= weight * repetitions
|
||||
if counter_value == 0:
|
||||
initial = ''.join(parts)
|
||||
break
|
||||
if initial is None:
|
||||
return self.render_value(
|
||||
counter_value, counter['fallback'] or 'decimal',
|
||||
previous_types=previous_types)
|
||||
|
||||
assert initial is not None
|
||||
|
||||
# Step 4
|
||||
pad = counter['pad'] or (0, '')
|
||||
pad_difference = pad[0] - len(initial)
|
||||
if is_negative and use_negative:
|
||||
pad_difference -= len(negative_prefix) + len(negative_suffix)
|
||||
if pad_difference > 0:
|
||||
initial = pad_difference * symbol(pad[1]) + initial
|
||||
|
||||
# Step 5
|
||||
if is_negative and use_negative:
|
||||
initial = negative_prefix + initial + negative_suffix
|
||||
|
||||
# Step 6
|
||||
return initial
|
||||
|
||||
def render_marker(self, counter_name, counter_value):
|
||||
"""Generate the content of a ::marker pseudo-element."""
|
||||
counter = self.resolve_counter(counter_name)
|
||||
if counter is None:
|
||||
if 'decimal' in self:
|
||||
return self.render_marker('decimal', counter_value)
|
||||
else:
|
||||
# Could happen if the UA stylesheet is not used
|
||||
return ''
|
||||
|
||||
prefix = symbol(counter['prefix'] or ('string', ''))
|
||||
suffix = symbol(counter['suffix'] or ('string', '. '))
|
||||
|
||||
value = self.render_value(counter_value, counter_name=counter_name)
|
||||
assert value is not None
|
||||
return prefix + value + suffix
|
||||
|
||||
def copy(self):
|
||||
return CounterStyle(deepcopy(self))
|
||||
192
venv/Lib/site-packages/weasyprint/css/html5_ph.css
Normal file
192
venv/Lib/site-packages/weasyprint/css/html5_ph.css
Normal file
@@ -0,0 +1,192 @@
|
||||
/*
|
||||
|
||||
Presentational hints stylsheet for HTML.
|
||||
|
||||
This stylesheet contains all the presentational hints rules that can be
|
||||
expressed as CSS.
|
||||
|
||||
See https://www.w3.org/TR/html5/rendering.html#rendering
|
||||
|
||||
TODO: Attribute values are not case-insensitive, but they should be. We can add
|
||||
a "i" flag when CSS Selectors Level 4 is supported.
|
||||
|
||||
*/
|
||||
|
||||
pre[wrap] { white-space: pre-wrap; }
|
||||
|
||||
br[clear=left] { clear: left; }
|
||||
br[clear=right] { clear: right; }
|
||||
br[clear=all], br[clear=both] { clear: both; }
|
||||
|
||||
ol[type="1"], li[type="1"] { list-style-type: decimal; }
|
||||
ol[type=a], li[type=a] { list-style-type: lower-alpha; }
|
||||
ol[type=A], li[type=A] { list-style-type: upper-alpha; }
|
||||
ol[type=i], li[type=i] { list-style-type: lower-roman; }
|
||||
ol[type=I], li[type=I] { list-style-type: upper-roman; }
|
||||
ul[type=disc], li[type=disc] { list-style-type: disc; }
|
||||
ul[type=circle], li[type=circle] { list-style-type: circle; }
|
||||
ul[type=square], li[type=square] { list-style-type: square; }
|
||||
|
||||
table[align=left] { float: left; }
|
||||
table[align=right] { float: right; }
|
||||
table[align=center] { margin-left: auto; margin-right: auto; }
|
||||
thead[align=absmiddle], tbody[align=absmiddle], tfoot[align=absmiddle],
|
||||
tr[align=absmiddle], td[align=absmiddle], th[align=absmiddle] {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
caption[align=bottom] { caption-side: bottom; }
|
||||
p[align=left], h1[align=left], h2[align=left], h3[align=left],
|
||||
h4[align=left], h5[align=left], h6[align=left] {
|
||||
text-align: left;
|
||||
}
|
||||
p[align=right], h1[align=right], h2[align=right], h3[align=right],
|
||||
h4[align=right], h5[align=right], h6[align=right] {
|
||||
text-align: right;
|
||||
}
|
||||
p[align=center], h1[align=center], h2[align=center], h3[align=center],
|
||||
h4[align=center], h5[align=center], h6[align=center] {
|
||||
text-align: center;
|
||||
}
|
||||
p[align=justify], h1[align=justify], h2[align=justify], h3[align=justify],
|
||||
h4[align=justify], h5[align=justify], h6[align=justify] {
|
||||
text-align: justify;
|
||||
}
|
||||
thead[valign=top], tbody[valign=top], tfoot[valign=top],
|
||||
tr[valign=top], td[valign=top], th[valign=top] {
|
||||
vertical-align: top;
|
||||
}
|
||||
thead[valign=middle], tbody[valign=middle], tfoot[valign=middle],
|
||||
tr[valign=middle], td[valign=middle], th[valign=middle] {
|
||||
vertical-align: middle;
|
||||
}
|
||||
thead[valign=bottom], tbody[valign=bottom], tfoot[valign=bottom],
|
||||
tr[valign=bottom], td[valign=bottom], th[valign=bottom] {
|
||||
vertical-align: bottom;
|
||||
}
|
||||
thead[valign=baseline], tbody[valign=baseline], tfoot[valign=baseline],
|
||||
tr[valign=baseline], td[valign=baseline], th[valign=baseline] {
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
td[nowrap], th[nowrap] { white-space: nowrap; }
|
||||
|
||||
table[rules=none], table[rules=groups], table[rules=rows],
|
||||
table[rules=cols], table[rules=all] {
|
||||
border-style: hidden;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
table[border]:not([border="0"]) { border-style: outset; }
|
||||
table[frame=void] { border-style: hidden; }
|
||||
table[frame=above] { border-style: outset hidden hidden hidden; }
|
||||
table[frame=below] { border-style: hidden hidden outset hidden; }
|
||||
table[frame=hsides] { border-style: outset hidden outset hidden; }
|
||||
table[frame=lhs] { border-style: hidden hidden hidden outset; }
|
||||
table[frame=rhs] { border-style: hidden outset hidden hidden; }
|
||||
table[frame=vsides] { border-style: hidden outset; }
|
||||
table[frame=box], table[frame=border] { border-style: outset; }
|
||||
|
||||
table[border]:not([border="0"]) > tr > td, table[border]:not([border="0"]) > tr > th,
|
||||
table[border]:not([border="0"]) > thead > tr > td, table[border]:not([border="0"]) > thead > tr > th,
|
||||
table[border]:not([border="0"]) > tbody > tr > td, table[border]:not([border="0"]) > tbody > tr > th,
|
||||
table[border]:not([border="0"]) > tfoot > tr > td, table[border]:not([border="0"]) > tfoot > tr > th {
|
||||
border-width: 1px;
|
||||
border-style: inset;
|
||||
}
|
||||
table[rules=none] > tr > td, table[rules=none] > tr > th,
|
||||
table[rules=none] > thead > tr > td, table[rules=none] > thead > tr > th,
|
||||
table[rules=none] > tbody > tr > td, table[rules=none] > tbody > tr > th,
|
||||
table[rules=none] > tfoot > tr > td, table[rules=none] > tfoot > tr > th,
|
||||
table[rules=groups] > tr > td, table[rules=groups] > tr > th,
|
||||
table[rules=groups] > thead > tr > td, table[rules=groups] > thead > tr > th,
|
||||
table[rules=groups] > tbody > tr > td, table[rules=groups] > tbody > tr > th,
|
||||
table[rules=groups] > tfoot > tr > td, table[rules=groups] > tfoot > tr > th,
|
||||
table[rules=rows] > tr > td, table[rules=rows] > tr > th,
|
||||
table[rules=rows] > thead > tr > td, table[rules=rows] > thead > tr > th,
|
||||
table[rules=rows] > tbody > tr > td, table[rules=rows] > tbody > tr > th,
|
||||
table[rules=rows] > tfoot > tr > td, table[rules=rows] > tfoot > tr > th {
|
||||
border-width: 1px;
|
||||
border-style: none;
|
||||
}
|
||||
table[rules=cols] > tr > td, table[rules=cols] > tr > th,
|
||||
table[rules=cols] > thead > tr > td, table[rules=cols] > thead > tr > th,
|
||||
table[rules=cols] > tbody > tr > td, table[rules=cols] > tbody > tr > th,
|
||||
table[rules=cols] > tfoot > tr > td, table[rules=cols] > tfoot > tr > th {
|
||||
border-width: 1px;
|
||||
border-style: none solid;
|
||||
}
|
||||
table[rules=all] > tr > td, table[rules=all] > tr > th,
|
||||
table[rules=all] > thead > tr > td, table[rules=all] > thead > tr > th,
|
||||
table[rules=all] > tbody > tr > td, table[rules=all] > tbody > tr > th,
|
||||
table[rules=all] > tfoot > tr > td, table[rules=all] > tfoot > tr > th {
|
||||
border-width: 1px;
|
||||
border-style: solid;
|
||||
}
|
||||
|
||||
table[rules=groups] > colgroup {
|
||||
border-left-width: 1px;
|
||||
border-left-style: solid;
|
||||
border-right-width: 1px;
|
||||
border-right-style: solid;
|
||||
}
|
||||
table[rules=groups] > thead,
|
||||
table[rules=groups] > tbody,
|
||||
table[rules=groups] > tfoot {
|
||||
border-top-width: 1px;
|
||||
border-top-style: solid;
|
||||
border-bottom-width: 1px;
|
||||
border-bottom-style: solid;
|
||||
}
|
||||
|
||||
table[rules=rows] > tr, table[rules=rows] > thead > tr,
|
||||
table[rules=rows] > tbody > tr, table[rules=rows] > tfoot > tr {
|
||||
border-top-width: 1px;
|
||||
border-top-style: solid;
|
||||
border-bottom-width: 1px;
|
||||
border-bottom-style: solid;
|
||||
}
|
||||
|
||||
hr[align=left] { margin-left: 0; margin-right: auto; }
|
||||
hr[align=right] { margin-left: auto; margin-right: 0; }
|
||||
hr[align=center] { margin-left: auto; margin-right: auto; }
|
||||
hr[color], hr[noshade] { border-style: solid; }
|
||||
|
||||
iframe[frameborder="0"], iframe[frameborder=no] { border: none; }
|
||||
|
||||
applet[align=left], embed[align=left], iframe[align=left],
|
||||
img[align=left], input[type=image][align=left], object[align=left] {
|
||||
float: left;
|
||||
}
|
||||
|
||||
applet[align=right], embed[align=right], iframe[align=right],
|
||||
img[align=right], input[type=image][align=right], object[align=right] {
|
||||
float: right;
|
||||
}
|
||||
|
||||
applet[align=top], embed[align=top], iframe[align=top],
|
||||
img[align=top], input[type=image][align=top], object[align=top] {
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
applet[align=baseline], embed[align=baseline], iframe[align=baseline],
|
||||
img[align=baseline], input[type=image][align=baseline], object[align=baseline] {
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
applet[align=texttop], embed[align=texttop], iframe[align=texttop],
|
||||
img[align=texttop], input[type=image][align=texttop], object[align=texttop] {
|
||||
vertical-align: text-top;
|
||||
}
|
||||
|
||||
applet[align=absmiddle], embed[align=absmiddle], iframe[align=absmiddle],
|
||||
img[align=absmiddle], input[type=image][align=absmiddle], object[align=absmiddle],
|
||||
applet[align=abscenter], embed[align=abscenter], iframe[align=abscenter],
|
||||
img[align=abscenter], input[type=image][align=abscenter], object[align=abscenter] {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
applet[align=bottom], embed[align=bottom], iframe[align=bottom],
|
||||
img[align=bottom], input[type=image][align=bottom],
|
||||
object[align=bottom] {
|
||||
vertical-align: bottom;
|
||||
}
|
||||
826
venv/Lib/site-packages/weasyprint/css/html5_ua.css
Normal file
826
venv/Lib/site-packages/weasyprint/css/html5_ua.css
Normal file
@@ -0,0 +1,826 @@
|
||||
/*
|
||||
|
||||
User agent stylsheet for HTML.
|
||||
|
||||
Contributed by Peter Moulder.
|
||||
Based on suggested styles in the HTML5 specification, CSS 2.1, and
|
||||
what various web browsers use.
|
||||
|
||||
*/
|
||||
|
||||
/* http://www.w3.org/TR/html5/Overview#scroll-to-the-fragment-identifier */
|
||||
*[id] { -weasy-anchor: attr(id); }
|
||||
a[name] { -weasy-anchor: attr(name); }
|
||||
|
||||
*[dir] { /* unicode-bidi: embed; */ }
|
||||
*[hidden] { display: none; }
|
||||
*[dir=ltr] { direction: ltr; }
|
||||
*[dir=rtl] { direction: rtl; }
|
||||
/* :dir(ltr) { direction: ltr; } */
|
||||
/* :dir(rtl) { direction: rtl; } */
|
||||
:root { quotes: '\201c' '\201d' '\2018' '\2019'; }
|
||||
*[lang] { -weasy-lang: attr(lang); }
|
||||
[lang|=af] { quotes: '\201c' '\201d' '\2018' '\201d'; }
|
||||
[lang|=agq] { quotes: '\0027' '\0027' '\201e' '\201d'; }
|
||||
[lang|=ak] { quotes: '\2018' '\2019' '\201c' '\201d'; }
|
||||
[lang|=am] { quotes: '\00ab' '\00bb' '\2039' '\203a'; }
|
||||
[lang|=ar] { quotes: '\201c' '\201d' '\2018' '\2019'; }
|
||||
[lang|=asa] { quotes: '\0027' '\0027' '\201c' '\201d'; }
|
||||
[lang|=az] { quotes: '\201c' '\201d' '\2018' '\2019'; }
|
||||
[lang|=bas] { quotes: '\00ab' '\00bb' '\201c' '\201e'; }
|
||||
[lang|=be] { quotes: '\201e' '\201d' '\00ab' '\00bb'; }
|
||||
[lang|=bem] { quotes: '\0027' '\0027' '\201c' '\201c'; }
|
||||
[lang|=bez] { quotes: '\2018' '\2019' '\201c' '\201d'; }
|
||||
[lang|=bg] { quotes: '\201e' '\201c' '\2018' '\2019'; }
|
||||
[lang|=bm] { quotes: '\00ab' '\00bb' '\201c' '\201d'; }
|
||||
[lang|=bn] { quotes: '\201c' '\201d' '\2018' '\2019'; }
|
||||
[lang|=brx] { quotes: '\0027' '\0027' '\0022' '\0022'; }
|
||||
[lang|=bs] { quotes: '\2018' '\2019' '\201c' '\201d'; }
|
||||
[lang|=ca] { quotes: '\2018' '\2019' '\201c' '\201d'; }
|
||||
[lang|=cgg] { quotes: '\0027' '\0027' '\201c' '\201e'; }
|
||||
[lang|=chr] { quotes: '\201c' '\201d' '\2018' '\2019'; }
|
||||
[lang|=cs] { quotes: '\201e' '\201c' '\201a' '\2018'; }
|
||||
[lang|=cy] { quotes: '\2018' '\2019' '\201c' '\201d'; }
|
||||
[lang|=da] { quotes: '\201d' '\201d' '\201d' '\201d'; }
|
||||
[lang|=dav] { quotes: '\0027' '\0027' '\201c' '\201d'; }
|
||||
[lang|=de] { quotes: '\201e' '\201c' '\201a' '\2018'; }
|
||||
[lang|=de-CH] { quotes: '\00ab' '\00bb' '\2039' '\203a'; }
|
||||
[lang|=dje] { quotes: '\201c' '\201d' '\00ab' '\00bb'; }
|
||||
[lang|=dua] { quotes: '\00ab' '\00bb' '\0027' '\0027'; }
|
||||
[lang|=dyo] { quotes: '\00ab' '\00bb' '\201c' '\201d'; }
|
||||
[lang|=dz] { quotes: '\0022' '\0022' '\0027' '\0027'; }
|
||||
[lang|=ebu] { quotes: '\0027' '\0027' '\201c' '\201d'; }
|
||||
[lang|=ee] { quotes: '\0027' '\0027' '\201c' '\201c'; }
|
||||
[lang|=el] { quotes: '\00ab' '\00bb' '\2018' '\2019'; }
|
||||
[lang|=el-POLYTON] { quotes: '\00ab' '\00bb' '\201b' '\2019'; }
|
||||
[lang|=en] { quotes: '\201c' '\201d' '\2018' '\2019'; }
|
||||
[lang|=en-GB] { quotes: '\2018' '\2019' '\201c' '\201d'; }
|
||||
[lang|=es] { quotes: '\2018' '\2019' '\201c' '\201d'; }
|
||||
[lang|=et] { quotes: '\201e' '\201c' '\201e' '\201c'; }
|
||||
[lang|=eu] { quotes: '\201c' '\201d' '\2018' '\2019'; }
|
||||
[lang|=ewo] { quotes: '\00ab' '\00bb' '\201c' '\201d'; }
|
||||
[lang|=fa] { quotes: '\00ab' '\00bb' '\2039' '\203a'; }
|
||||
[lang|=ff] { quotes: '\201e' '\201d' '\0027' '\0027'; }
|
||||
[lang|=fi] { quotes: '\201d' '\201d' '\2019' '\2019'; }
|
||||
[lang|=fil] { quotes: '\201c' '\201d' '\2018' '\2019'; }
|
||||
[lang|=fo] { quotes: '\201d' '\201d' '\2019' '\2019'; }
|
||||
[lang|=fr] { quotes: '\00ab' '\00bb' '\201c' '\201d'; }
|
||||
[lang|=fr-CH] { quotes: '\00ab' '\00bb' '\2039' '\203a'; }
|
||||
[lang|=fur] { quotes: '\2018' '\2019' '\201c' '\201d'; }
|
||||
[lang|=gaa] { quotes: '\0027' '\0027' '\0022' '\0022'; }
|
||||
[lang|=gd] { quotes: '\201c' '\201d' '\2018' '\2019'; }
|
||||
[lang|=gl] { quotes: '\201c' '\201d' '\2018' '\2019'; }
|
||||
[lang|=gsw] { quotes: '\00ab' '\00bb' '\2039' '\203a'; }
|
||||
[lang|=gu] { quotes: '\0027' '\0027' '\0022' '\0022'; }
|
||||
[lang|=guz] { quotes: '\0027' '\2018' '\201c' '\201c'; }
|
||||
[lang|=ha] { quotes: '\0027' '\0027' '\201c' '\201c'; }
|
||||
[lang|=he] { quotes: '\201c' '\201d' '\0022' '\0022'; }
|
||||
[lang|=hi] { quotes: '\0027' '\0027' '\0022' '\0022'; }
|
||||
[lang|=hr] { quotes: '\2018' '\2019' '\201c' '\201d'; }
|
||||
[lang|=hu] { quotes: '\201e' '\201d' '\201e' '\201d'; }
|
||||
[lang|=ia] { quotes: '\2018' '\2019' '\201c' '\201d'; }
|
||||
[lang|=id] { quotes: '\201c' '\201d' '\2018' '\2019'; }
|
||||
[lang|=ig] { quotes: '\0027' '\0027' '\201c' '\201d'; }
|
||||
[lang|=is] { quotes: '\201e' '\201c' '\201a' '\2018'; }
|
||||
[lang|=it] { quotes: '\2018' '\2019' '\201c' '\201d'; }
|
||||
[lang|=it-CH] { quotes: '\00ab' '\00bb' '\2039' '\203a'; }
|
||||
[lang|=ja] { quotes: '\300c' '\300d' '\300e' '\300f'; }
|
||||
[lang|=jmc] { quotes: '\0027' '\0027' '\201c' '\201c'; }
|
||||
[lang|=ka] { quotes: '\201c' '\201d' '\2018' '\2019'; }
|
||||
[lang|=kab] { quotes: '\00ab' '\00bb' '\201c' '\201d'; }
|
||||
[lang|=kam] { quotes: '\0027' '\0027' '\201c' '\201d'; }
|
||||
[lang|=kde] { quotes: '\0027' '\0027' '\201c' '\201d'; }
|
||||
[lang|=kea] { quotes: '\201c' '\201d' '\00ab' '\00bb'; }
|
||||
[lang|=khq] { quotes: '\201c' '\201d' '\00ab' '\00bb'; }
|
||||
[lang|=ki] { quotes: '\0027' '\0027' '\201c' '\201d'; }
|
||||
[lang|=kl] { quotes: '\00bb' '\00ab' '\203a' '\2039'; }
|
||||
[lang|=kln] { quotes: '\0027' '\0027' '\201c' '\201d'; }
|
||||
[lang|=km] { quotes: '\0027' '\0027' '\0022' '\0022'; }
|
||||
[lang|=kn] { quotes: '\201c' '\201d' '\2018' '\2019'; }
|
||||
[lang|=ko] { quotes: '\2018' '\2019' '\201c' '\201d'; }
|
||||
[lang|=ksb] { quotes: '\0027' '\0027' '\201c' '\201d'; }
|
||||
[lang|=ksf] { quotes: '\0027' '\0027' '\00ab' '\00bb'; }
|
||||
[lang|=ksh] { quotes: '\201e' '\201c' '\201a' '\2018'; }
|
||||
[lang|=lag] { quotes: '\201d' '\201d' '\0027' '\0027'; }
|
||||
[lang|=lg] { quotes: '\0027' '\0027' '\201c' '\201d'; }
|
||||
[lang|=ln] { quotes: '\0027' '\0027' '\201c' '\201d'; }
|
||||
[lang|=lo] { quotes: '\0027' '\0027' '\0022' '\0022'; }
|
||||
[lang|=lt] { quotes: '\201e' '\201c' '\201e' '\201c'; }
|
||||
[lang|=lu] { quotes: '\2018' '\2018' '\201c' '\201c'; }
|
||||
[lang|=luo] { quotes: '\2018' '\2019' '\201c' '\201d'; }
|
||||
[lang|=luy] { quotes: '\0027' '\0027' '\201e' '\201c'; }
|
||||
[lang|=lv] { quotes: '\201c' '\201d' '\2018' '\2019'; }
|
||||
[lang|=mas] { quotes: '\0027' '\0027' '\201d' '\201c'; }
|
||||
[lang|=mer] { quotes: '\0027' '\0027' '\201c' '\201d'; }
|
||||
[lang|=mfe] { quotes: '\201c' '\201d' '\2018' '\2019'; }
|
||||
[lang|=mg] { quotes: '\00ab' '\00bb' '\201c' '\201d'; }
|
||||
[lang|=ml] { quotes: '\201c' '\201d' '\2018' '\2019'; }
|
||||
[lang|=mn] { quotes: '\2018' '\2019' '\201c' '\201d'; }
|
||||
[lang|=mr] { quotes: '\0027' '\0027' '\0022' '\0022'; }
|
||||
[lang|=ms] { quotes: '\201c' '\201d' '\2018' '\2019'; }
|
||||
[lang|=mt] { quotes: '\201c' '\201d' '\2018' '\2019'; }
|
||||
[lang|=mua] { quotes: '\00ab' '\00bb' '\201c' '\201d'; }
|
||||
[lang|=my] { quotes: '\201c' '\201d' '\2018' '\2019'; }
|
||||
[lang|=naq] { quotes: '\0027' '\0027' '\201c' '\201d'; }
|
||||
[lang|=nb] { quotes: '\201c' '\201d' '\2018' '\2019'; }
|
||||
[lang|=nd] { quotes: '\0027' '\0027' '\201c' '\201d'; }
|
||||
[lang|=nds] { quotes: '\201e' '\201c' '\201a' '\2018'; }
|
||||
[lang|=ne] { quotes: '\0027' '\0027' '\0022' '\0022'; }
|
||||
[lang|=nl] { quotes: '\2018' '\2019' '\201c' '\201d'; }
|
||||
[lang|=nmg] { quotes: '\201c' '\201e' '\00ab' '\00bb'; }
|
||||
[lang|=nn] { quotes: '\00ab' '\00bb' '\201c' '\201d'; }
|
||||
[lang|=nr] { quotes: '\2018' '\2019' '\201c' '\201d'; }
|
||||
[lang|=nso] { quotes: '\2018' '\2019' '\201c' '\201d'; }
|
||||
[lang|=nus] { quotes: '\201c' '\201d' '\2018' '\2019'; }
|
||||
[lang|=nyn] { quotes: '\0027' '\0027' '\201c' '\201e'; }
|
||||
[lang|=pa] { quotes: '\0027' '\0027' '\0022' '\0022'; }
|
||||
[lang|=pl] { quotes: '\2018' '\2019' '\201e' '\201d'; }
|
||||
[lang|=pt] { quotes: '\201c' '\201d' '\2018' '\2019'; }
|
||||
[lang|=rm] { quotes: '\00ab' '\00bb' '\2039' '\203a'; }
|
||||
[lang|=rn] { quotes: '\0027' '\0027' '\201d' '\201d'; }
|
||||
[lang|=ro] { quotes: '\201e' '\201d' '\00ab' '\00bb'; }
|
||||
[lang|=rof] { quotes: '\0027' '\0027' '\201c' '\201c'; }
|
||||
[lang|=ru] { quotes: '\00ab' '\00bb' '\201e' '\201c'; }
|
||||
[lang|=rw] { quotes: '\2018' '\2019' '\00ab' '\00bb'; }
|
||||
[lang|=rwk] { quotes: '\0027' '\0027' '\201c' '\201c'; }
|
||||
[lang|=saq] { quotes: '\0027' '\0027' '\201c' '\201d'; }
|
||||
[lang|=sbp] { quotes: '\0027' '\0027' '\201c' '\201d'; }
|
||||
[lang|=se] { quotes: '\201d' '\201d' '\2019' '\2019'; }
|
||||
[lang|=seh] { quotes: '\0027' '\0027' '\0027' '\0027'; }
|
||||
[lang|=ses] { quotes: '\201c' '\201d' '\00ab' '\00bb'; }
|
||||
[lang|=sg] { quotes: '\00ab' '\00bb' '\201c' '\2019'; }
|
||||
[lang|=shi] { quotes: '\00ab' '\00bb' '\201e' '\201d'; }
|
||||
[lang|=shi-Tfng] { quotes: '\00ab' '\00bb' '\201e' '\201d'; }
|
||||
[lang|=sk] { quotes: '\201a' '\2018' '\201e' '\201c'; }
|
||||
[lang|=sl] { quotes: '\00bb' '\00ab' '\201e' '\201c'; }
|
||||
[lang|=sn] { quotes: '\0027' '\0027' '\201d' '\201d'; }
|
||||
[lang|=so] { quotes: '\201c' '\201d' '\2018' '\2019'; }
|
||||
[lang|=sr] { quotes: '\201c' '\201d' '\2018' '\2019'; }
|
||||
[lang|=ss] { quotes: '\2018' '\2019' '\201c' '\201d'; }
|
||||
[lang|=st] { quotes: '\2018' '\2019' '\201c' '\201d'; }
|
||||
[lang|=sv] { quotes: '\201d' '\201d' '\2019' '\2019'; }
|
||||
[lang|=sw] { quotes: '\0027' '\0027' '\0027' '\0027'; }
|
||||
[lang|=swc] { quotes: '\0027' '\0027' '\201d' '\201c'; }
|
||||
[lang|=ta] { quotes: '\201c' '\201d' '\2018' '\2019'; }
|
||||
[lang|=te] { quotes: '\201c' '\201d' '\2018' '\2019'; }
|
||||
[lang|=teo] { quotes: '\0027' '\2019' '\201c' '\201d'; }
|
||||
[lang|=tg] { quotes: '\00ab' '\00bb' '\00ab' '\201e'; }
|
||||
[lang|=th] { quotes: '\201c' '\201d' '\2018' '\2019'; }
|
||||
[lang|=ti-ER] { quotes: '\2018' '\2019' '\201c' '\201d'; }
|
||||
[lang|=tn] { quotes: '\2018' '\2019' '\201c' '\201d'; }
|
||||
[lang|=to] { quotes: '\201c' '\201d' '\00ab' '\00bb'; }
|
||||
[lang|=tr] { quotes: '\201c' '\201d' '\2018' '\2019'; }
|
||||
[lang|=trv] { quotes: '\201c' '\201d' '\2018' '\2019'; }
|
||||
[lang|=ts] { quotes: '\2018' '\2019' '\201c' '\201d'; }
|
||||
[lang|=twq] { quotes: '\201c' '\201d' '\00ab' '\00bb'; }
|
||||
[lang|=tzm] { quotes: '\2018' '\2019' '\201c' '\201d'; }
|
||||
[lang|=uk] { quotes: '\00ab' '\00bb' '\201e' '\201c'; }
|
||||
[lang|=ur] { quotes: '\0022' '\0022' '\0027' '\0027'; }
|
||||
[lang|=vai] { quotes: '\2018' '\2019' '\201c' '\201d'; }
|
||||
[lang|=vai-Latn] { quotes: '\2018' '\2019' '\201c' '\201d'; }
|
||||
[lang|=ve] { quotes: '\2018' '\2019' '\201c' '\201d'; }
|
||||
[lang|=vi] { quotes: '\201c' '\201d' '\2018' '\2019'; }
|
||||
[lang|=vun] { quotes: '\0027' '\0027' '\201c' '\201c'; }
|
||||
[lang|=wae] { quotes: '\00ab' '\00bb' '\2039' '\203a'; }
|
||||
[lang|=xh] { quotes: '\2018' '\2019' '\201c' '\201d'; }
|
||||
[lang|=xog] { quotes: '\0027' '\0027' '\201c' '\201e'; }
|
||||
[lang|=yav] { quotes: '\00ab' '\00bb' '\00ab' '\00bb'; }
|
||||
[lang|=yo] { quotes: '\0027' '\0027' '\0027' '\0027'; }
|
||||
[lang|=zh] { quotes: '\201c' '\201d' '\2018' '\2019'; }
|
||||
[lang|=zh-Hant] { quotes: '\300c' '\300d' '\300e' '\300f'; }
|
||||
[lang|=zu] { quotes: '\2018' '\2019' '\201c' '\201d'; }
|
||||
:link { color: #0000EE; text-decoration: underline; }
|
||||
a[href] { -weasy-link: attr(href); }
|
||||
:visited { color: #551A8B; text-decoration: underline; }
|
||||
a:link[rel~=help] { cursor: help; }
|
||||
a:visited[rel~=help] { cursor: help; }
|
||||
abbr[title] { text-decoration: dotted underline; }
|
||||
acronym[title] { text-decoration: dotted underline; }
|
||||
address { display: block; font-style: italic; /* unicode-bidi: isolate; */ }
|
||||
area { display: none; }
|
||||
area:link[rel~=help] { cursor: help; }
|
||||
area:visited[rel~=help] { cursor: help; }
|
||||
article { display: block; /* unicode-bidi: isolate; */ }
|
||||
aside { display: block; /* unicode-bidi: isolate; */ }
|
||||
b { font-weight: bold; }
|
||||
base { display: none; }
|
||||
basefont { display: none; }
|
||||
bdi { /* unicode-bidi: isolate; */ }
|
||||
bdi[dir] { /* unicode-bidi: isolate; */ }
|
||||
bdo { /* unicode-bidi: bidi-override; */ }
|
||||
bdo[dir] { /* unicode-bidi: bidi-override; */ }
|
||||
big { font-size: larger; }
|
||||
blink { text-decoration: blink; }
|
||||
blockquote { display: block; margin: 1em 40px; /* unicode-bidi: isolate; */ }
|
||||
body { display: block; margin: 8px; }
|
||||
br::before { content: '\A'; white-space: pre-line; }
|
||||
button { display: inline-block; text-align: center; text-indent: 0; }
|
||||
caption { display: table-caption; /* unicode-bidi: isolate; */ }
|
||||
center { display: block; text-align: center; /* unicode-bidi: isolate; */ }
|
||||
cite { font-style: italic; }
|
||||
code { font-family: monospace; }
|
||||
col { display: table-column; /* unicode-bidi: isolate; */ }
|
||||
col[hidden] { display: table-column; /* unicode-bidi: isolate; */ visibility: collapse; }
|
||||
colgroup { display: table-column-group; /* unicode-bidi: isolate; */ }
|
||||
colgroup[hidden] { display: table-column-group; /* unicode-bidi: isolate; */ visibility: collapse; }
|
||||
command { display: none; }
|
||||
datalist { display: none; }
|
||||
|
||||
dd { display: block; margin-left: 40px; /* unicode-bidi: isolate; */ }
|
||||
|
||||
*[dir=ltr] dd { margin-left: 0; margin-right: 40px; }
|
||||
*[dir=rtl] dd { margin-left: 40px; margin-right: 0; }
|
||||
*[dir] *[dir=ltr] dd { margin-left: 0; margin-right: 40px; }
|
||||
*[dir] *[dir=rtl] dd { margin-left: 40px; margin-right: 0; }
|
||||
*[dir] *[dir] *[dir=ltr] dd { margin-left: 0; margin-right: 40px; }
|
||||
*[dir] *[dir] *[dir=rtl] dd { margin-left: 40px; margin-right: 0; }
|
||||
dd[dir=ltr][dir][dir] { margin-left: 0; margin-right: 40px; }
|
||||
dd[dir=rtl][dir][dir] { margin-left: 40px; margin-right: 0; }
|
||||
|
||||
details { display: block; /* unicode-bidi: isolate; */ }
|
||||
del { text-decoration: line-through; }
|
||||
dfn { font-style: italic; }
|
||||
|
||||
dir { display: block; list-style-type: disc; margin-bottom: 1em; margin-top: 1em; padding-left: 40px; /* unicode-bidi: isolate; */ }
|
||||
|
||||
*[dir=rtl] dir { padding-left: 0; padding-right: 40px; }
|
||||
*[dir=ltr] dir { padding-left: 40px; padding-right: 0; }
|
||||
*[dir] *[dir=rtl] dir { padding-left: 0; padding-right: 40px; }
|
||||
*[dir] *[dir=ltr] dir { padding-left: 40px; padding-right: 0; }
|
||||
*[dir] *[dir] *[dir=rtl] dir { padding-left: 0; padding-right: 40px; }
|
||||
*[dir] *[dir] *[dir=ltr] dir { padding-left: 40px; padding-right: 0; }
|
||||
dir[dir=rtl][dir][dir] { padding-left: 0; padding-right: 40px; }
|
||||
dir[dir=ltr][dir][dir] { padding-left: 40px; padding-right: 0; }
|
||||
|
||||
dir dir { list-style-type: circle; margin-bottom: 0; margin-top: 0; }
|
||||
dl dir { list-style-type: circle; margin-bottom: 0; margin-top: 0; }
|
||||
menu dir { list-style-type: circle; margin-bottom: 0; margin-top: 0; }
|
||||
ol dir { list-style-type: circle; margin-bottom: 0; margin-top: 0; }
|
||||
ul dir { list-style-type: circle; margin-bottom: 0; margin-top: 0; }
|
||||
|
||||
div { display: block; /* unicode-bidi: isolate; */ }
|
||||
|
||||
dl { display: block; margin-bottom: 1em; margin-top: 1em; /* unicode-bidi: isolate; */ }
|
||||
|
||||
dir dl { list-style-type: circle; margin-bottom: 0; margin-top: 0; }
|
||||
dl dl { margin-bottom: 0; margin-top: 0; }
|
||||
ol dl { list-style-type: circle; margin-bottom: 0; margin-top: 0; }
|
||||
ul dl { list-style-type: circle; margin-bottom: 0; margin-top: 0; }
|
||||
|
||||
dir dir dl { list-style-type: square; }
|
||||
dir menu dl { list-style-type: square; }
|
||||
dir ol dl { list-style-type: square; }
|
||||
dir ul dl { list-style-type: square; }
|
||||
menu dir dl { list-style-type: square; }
|
||||
menu dl { list-style-type: circle; margin-bottom: 0; margin-top: 0; }
|
||||
menu menu dl { list-style-type: square; }
|
||||
menu ol dl { list-style-type: square; }
|
||||
menu ul dl { list-style-type: square; }
|
||||
ol dir dl { list-style-type: square; }
|
||||
ol menu dl { list-style-type: square; }
|
||||
ol ol dl { list-style-type: square; }
|
||||
ol ul dl { list-style-type: square; }
|
||||
ul dir dl { list-style-type: square; }
|
||||
ul menu dl { list-style-type: square; }
|
||||
ul ol dl { list-style-type: square; }
|
||||
ul ul dl { list-style-type: square; }
|
||||
|
||||
ol, ul { counter-reset: list-item }
|
||||
|
||||
|
||||
dt { display: block; /* unicode-bidi: isolate; */ }
|
||||
em { font-style: italic; }
|
||||
fieldset { display: block; border-style: groove; border-width: 2px; margin-left: 2px; margin-right: 2px; padding: .35em .625em .75em .625em; }
|
||||
figcaption { display: block; /* unicode-bidi: isolate; */ }
|
||||
figure { display: block; margin: 1em 40px; /* unicode-bidi: isolate; */ }
|
||||
footer { display: block; /* unicode-bidi: isolate; */ }
|
||||
form { display: block; /* unicode-bidi: isolate; */ }
|
||||
frame { display: block; }
|
||||
frameset { display: block; }
|
||||
|
||||
h1 { display: block; font-size: 2em; font-weight: bold; hyphens: manual; margin-bottom: .67em; margin-top: .67em; page-break-after: avoid; page-break-inside: avoid; /* unicode-bidi: isolate; */ bookmark-level: 1; bookmark-label: content(text); }
|
||||
section h1 { font-size: 1.50em; margin-bottom: .83em; margin-top: .83em; }
|
||||
section section h1 { font-size: 1.17em; margin-bottom: 1.00em; margin-top: 1.00em; }
|
||||
section section section h1 { font-size: 1.00em; margin-bottom: 1.33em; margin-top: 1.33em; }
|
||||
section section section section h1 { font-size: .83em; margin-bottom: 1.67em; margin-top: 1.67em; }
|
||||
section section section section section h1 { font-size: .67em; margin-bottom: 2.33em; margin-top: 2.33em; }
|
||||
h2 { display: block; font-size: 1.50em; font-weight: bold; hyphens: manual; margin-bottom: .83em; margin-top: .83em; page-break-after: avoid; page-break-inside: avoid; /* unicode-bidi: isolate; */ bookmark-level: 2; bookmark-label: content(text); }
|
||||
h3 { display: block; font-size: 1.17em; font-weight: bold; hyphens: manual; margin-bottom: 1.00em; margin-top: 1.00em; page-break-after: avoid; page-break-inside: avoid; /* unicode-bidi: isolate; */ bookmark-level: 3; bookmark-label: content(text); }
|
||||
h4 { display: block; font-size: 1.00em; font-weight: bold; hyphens: manual; margin-bottom: 1.33em; margin-top: 1.33em; page-break-after: avoid; page-break-inside: avoid; /* unicode-bidi: isolate; */ bookmark-level: 4; bookmark-label: content(text); }
|
||||
h5 { display: block; font-size: .83em; font-weight: bold; hyphens: manual; margin-bottom: 1.67em; margin-top: 1.67em; page-break-after: avoid; /* unicode-bidi: isolate; */ bookmark-level: 5; bookmark-label: content(text); }
|
||||
h6 { display: block; font-size: .67em; font-weight: bold; hyphens: manual; margin-bottom: 2.33em; margin-top: 2.33em; page-break-after: avoid; /* unicode-bidi: isolate; */ bookmark-level: 6; bookmark-label: content(text); }
|
||||
|
||||
head { display: none; }
|
||||
header { display: block; /* unicode-bidi: isolate; */ }
|
||||
hgroup { display: block; /* unicode-bidi: isolate; */ }
|
||||
|
||||
hr { border-style: inset; border-width: 1px; color: gray; display: block; margin-bottom: .5em; margin-left: auto; margin-right: auto; margin-top: .5em; /* unicode-bidi: isolate; */ }
|
||||
html { display: block; }
|
||||
i { font-style: italic; }
|
||||
*[dir=auto] { /* unicode-bidi: isolate; */ }
|
||||
bdo[dir=auto] { /* unicode-bidi: bidi-override isolate; */ }
|
||||
input[type=hidden] { display: none; }
|
||||
menu[type=context] { display: none; }
|
||||
pre[dir=auto] { /* unicode-bidi: plaintext; */ }
|
||||
table[frame=above] { border-color: black; }
|
||||
table[frame=below] { border-color: black; }
|
||||
table[frame=border] { border-color: black; }
|
||||
table[frame=box] { border-color: black; }
|
||||
table[frame=hsides] { border-color: black; }
|
||||
table[frame=lhs] { border-color: black; }
|
||||
table[frame=rhs] { border-color: black; }
|
||||
table[frame=void] { border-color: black; }
|
||||
table[frame=vsides] { border-color: black; }
|
||||
table[rules=all] { border-color: black; }
|
||||
table[rules=cols] { border-color: black; }
|
||||
table[rules=groups] { border-color: black; }
|
||||
table[rules=none] { border-color: black; }
|
||||
table[rules=rows] { border-color: black; }
|
||||
textarea[dir=auto] { /* unicode-bidi: plaintext; */ }
|
||||
iframe { border: 2px inset; }
|
||||
iframe[seamless] { border: none; }
|
||||
input { display: inline-block; text-indent: 0; }
|
||||
ins { text-decoration: underline; }
|
||||
kbd { font-family: monospace; }
|
||||
keygen { display: inline-block; text-indent: 0; }
|
||||
legend { display: block; /* unicode-bidi: isolate; */ }
|
||||
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; */ }
|
||||
|
||||
*[dir=rtl] menu { padding-left: 0; padding-right: 40px; }
|
||||
*[dir=ltr] menu { padding-left: 40px; padding-right: 0; }
|
||||
*[dir] *[dir=rtl] menu { padding-left: 0; padding-right: 40px; }
|
||||
*[dir] *[dir=ltr] menu { padding-left: 40px; padding-right: 0; }
|
||||
*[dir] *[dir] *[dir=rtl] menu { padding-left: 0; padding-right: 40px; }
|
||||
*[dir] *[dir] *[dir=ltr] menu { padding-left: 40px; padding-right: 0; }
|
||||
menu[dir=rtl][dir][dir] { padding-left: 0; padding-right: 40px; }
|
||||
menu[dir=ltr][dir][dir] { padding-left: 40px; padding-right: 0; }
|
||||
|
||||
dir menu { list-style-type: circle; margin-bottom: 0; margin-top: 0; }
|
||||
dl menu { margin-bottom: 0; margin-top: 0; }
|
||||
menu menu { list-style-type: circle; margin-bottom: 0; margin-top: 0; }
|
||||
|
||||
dir dir menu { list-style-type: square; }
|
||||
dir menu menu { list-style-type: square; }
|
||||
dir ol menu { list-style-type: square; }
|
||||
dir ul menu { list-style-type: square; }
|
||||
menu dir menu { list-style-type: square; }
|
||||
menu menu menu { list-style-type: square; }
|
||||
menu ol menu { list-style-type: square; }
|
||||
menu ul menu { list-style-type: square; }
|
||||
|
||||
ol menu { list-style-type: circle; margin-bottom: 0; margin-top: 0; }
|
||||
ol dir menu { list-style-type: square; }
|
||||
ol menu menu { list-style-type: square; }
|
||||
ol ol menu { list-style-type: square; }
|
||||
ol ul menu { list-style-type: square; }
|
||||
ul dir menu { list-style-type: square; }
|
||||
ul menu menu { list-style-type: square; }
|
||||
ul menu { list-style-type: circle; margin-bottom: 0; margin-top: 0; }
|
||||
ul ol menu { list-style-type: square; }
|
||||
ul ul menu { list-style-type: square; }
|
||||
meta { display: none; }
|
||||
nav { display: block; /* unicode-bidi: isolate; */ }
|
||||
nobr { white-space: nowrap; }
|
||||
noembed { display: none; }
|
||||
|
||||
/* The HTML5 spec suggests display:none for the old (now forbidden) noframes element,
|
||||
* but Morp doesn't currently handle frames, so we might as well render it.
|
||||
*/
|
||||
/*noframes { display: none; }*/
|
||||
noframes { display: block; }
|
||||
|
||||
ol { page-break-before: avoid; }
|
||||
ol { display: block; list-style-type: decimal; margin-bottom: 1em; margin-top: 1em; padding-left: 40px; /* unicode-bidi: isolate; */ }
|
||||
|
||||
*[dir=ltr] ol { padding-left: 0; padding-right: 40px; }
|
||||
*[dir=rtl] ol { padding-left: 40px; padding-right: 0; }
|
||||
*[dir] *[dir=ltr] ol { padding-left: 0; padding-right: 40px; }
|
||||
*[dir] *[dir=rtl] ol { padding-left: 40px; padding-right: 0; }
|
||||
*[dir] *[dir] *[dir=ltr] ol { padding-left: 0; padding-right: 40px; }
|
||||
*[dir] *[dir] *[dir=rtl] ol { padding-left: 40px; padding-right: 0; }
|
||||
ol[dir=ltr][dir][dir] { padding-left: 0; padding-right: 40px; }
|
||||
ol[dir=rtl][dir][dir] { padding-left: 40px; padding-right: 0; }
|
||||
|
||||
dir ol { margin-bottom: 0; margin-top: 0; }
|
||||
dl ol { margin-bottom: 0; margin-top: 0; }
|
||||
menu ol { margin-bottom: 0; margin-top: 0; }
|
||||
ol ol { margin-bottom: 0; margin-top: 0; }
|
||||
ul ol { margin-bottom: 0; margin-top: 0; }
|
||||
|
||||
optgroup { text-indent: 0; }
|
||||
option { text-indent: 0; display: none; } /* Don't display the tag, it's replaced content in dynamic browsers */
|
||||
output { /* unicode-bidi: isolate; */ }
|
||||
output[dir] { /* unicode-bidi: isolate; */ }
|
||||
p { display: block; margin-bottom: 1em; margin-top: 1em; /* unicode-bidi: isolate; */ }
|
||||
param { display: none; }
|
||||
plaintext { display: block; font-family: monospace; margin-bottom: 1em; margin-top: 1em; /* unicode-bidi: isolate; */ white-space: pre; }
|
||||
pre { display: block; font-family: monospace; margin-bottom: 1em; margin-top: 1em; /* unicode-bidi: isolate; */ white-space: pre; }
|
||||
q::after { content: close-quote; }
|
||||
q::before { content: open-quote; }
|
||||
rp { display: none; }
|
||||
/* rt { display: ruby-text; } */
|
||||
/* ruby { display: ruby; } */
|
||||
s { text-decoration: line-through; }
|
||||
samp { font-family: monospace; }
|
||||
script { display: none; }
|
||||
section { display: block; /* unicode-bidi: isolate; */ }
|
||||
select { text-indent: 0; }
|
||||
small { font-size: smaller; }
|
||||
source { display: none; }
|
||||
strike { text-decoration: line-through; }
|
||||
strong { font-weight: bolder; }
|
||||
style { display: none; }
|
||||
sub { font-size: smaller; line-height: normal; vertical-align: sub; }
|
||||
summary { display: block; /* unicode-bidi: isolate; */ }
|
||||
sup { font-size: smaller; line-height: normal; vertical-align: super; }
|
||||
|
||||
table { border-collapse: separate; border-color: gray; border-spacing: 2px; display: table; text-indent: 0; /* unicode-bidi: isolate; */ }
|
||||
|
||||
/* The html5 spec doesn't mention the following, though the CSS 2.1 spec does
|
||||
* hint at its use, and a couple of UAs do have this. I haven't looked into
|
||||
* why the HTML5 spec doesn't include this rule.
|
||||
*/
|
||||
table { box-sizing: border-box; }
|
||||
|
||||
tbody { border-color: inherit; display: table-row-group; /* unicode-bidi: isolate; */ vertical-align: middle; }
|
||||
tbody[hidden] { display: table-row-group; /* unicode-bidi: isolate; */ visibility: collapse; }
|
||||
|
||||
td { border-color: gray; display: table-cell; padding: 1px; /* unicode-bidi: isolate; */ vertical-align: inherit; }
|
||||
td[hidden] { display: table-cell; /* unicode-bidi: isolate; */ visibility: collapse; }
|
||||
|
||||
textarea { display: inline-block; text-indent: 0; white-space: pre-wrap; }
|
||||
|
||||
tfoot { border-color: inherit; display: table-footer-group; /* unicode-bidi: isolate; */ vertical-align: middle; }
|
||||
tfoot[hidden] { display: table-footer-group; /* unicode-bidi: isolate; */ visibility: collapse; }
|
||||
|
||||
table[rules=none] > tr > td, table[rules=none] > tr > th, table[rules=groups] > tr > td, table[rules=groups] > tr > th, table[rules=rows] > tr > td, table[rules=rows] > tr > th, table[rules=cols] > tr > td, table[rules=cols] > tr > th, table[rules=all] > tr > td, table[rules=all] > tr > th, table[rules=none] > thead > tr > td, table[rules=none] > thead > tr > th, table[rules=groups] > thead > tr > td, table[rules=groups] > thead > tr > th, table[rules=rows] > thead > tr > td, table[rules=rows] > thead > tr > th, table[rules=cols] > thead > tr > td, table[rules=cols] > thead > tr > th, table[rules=all] > thead > tr > td, table[rules=all] > thead > tr > th, table[rules=none] > tbody > tr > td, table[rules=none] > tbody > tr > th, table[rules=groups] > tbody > tr > td, table[rules=groups] > tbody > tr > th, table[rules=rows] > tbody > tr > td, table[rules=rows] > tbody > tr > th, table[rules=cols] > tbody > tr > td, table[rules=cols] > tbody > tr > th, table[rules=all] > tbody > tr > td, table[rules=all] > tbody > tr > th, table[rules=none] > tfoot > tr > td, table[rules=none] > tfoot > tr > th, table[rules=groups] > tfoot > tr > td, table[rules=groups] > tfoot > tr > th, table[rules=rows] > tfoot > tr > td, table[rules=rows] > tfoot > tr > th, table[rules=cols] > tfoot > tr > td, table[rules=cols] > tfoot > tr > th, table[rules=all] > tfoot > tr > td, table[rules=all] > tfoot > tr > th { border-color: black; }
|
||||
th { border-color: gray; display: table-cell; font-weight: bold; padding: 1px; /* unicode-bidi: isolate; */ vertical-align: inherit; }
|
||||
|
||||
th[hidden] { display: table-cell; /* unicode-bidi: isolate; */ visibility: collapse; }
|
||||
thead { border-color: inherit; display: table-header-group; /* unicode-bidi: isolate; */ vertical-align: middle; }
|
||||
thead[hidden] { display: table-header-group; /* unicode-bidi: isolate; */ visibility: collapse; }
|
||||
table > tr { vertical-align: middle; }
|
||||
tr { border-color: inherit; display: table-row; /* unicode-bidi: isolate; */ vertical-align: inherit; }
|
||||
tr[hidden] { display: table-row; /* unicode-bidi: isolate; */ visibility: collapse; }
|
||||
|
||||
template { display: none; }
|
||||
title { display: none; }
|
||||
track { display: none; }
|
||||
tt { font-family: monospace; }
|
||||
u { text-decoration: underline; }
|
||||
|
||||
::marker { /* unicode-bidi: isolate; */ font-variant-numeric: tabular-nums; }
|
||||
ul { display: block; list-style-type: disc; margin-bottom: 1em; margin-top: 1em; padding-left: 40px; /* unicode-bidi: isolate; */ }
|
||||
|
||||
*[dir=ltr] ul { padding-left: 40px; padding-right: 0; }
|
||||
*[dir=rtl] ul { padding-left: 0; padding-right: 40px; }
|
||||
*[dir] *[dir=ltr] ul { padding-left: 40px; padding-right: 0; }
|
||||
*[dir] *[dir=rtl] ul { padding-left: 0; padding-right: 40px; }
|
||||
*[dir] *[dir] *[dir=ltr] ul { padding-left: 40px; padding-right: 0; }
|
||||
*[dir] *[dir] *[dir=rtl] ul { padding-left: 0; padding-right: 40px; }
|
||||
ul[dir=ltr][dir][dir] { padding-left: 40px; padding-right: 0; }
|
||||
ul[dir=rtl][dir][dir] { padding-left: 0; padding-right: 40px; }
|
||||
|
||||
/* This isn't in the HTML5 spec's suggested styling, and should probably be a
|
||||
* mere hint rather than a demand. It usually is the right thing, though.
|
||||
*/
|
||||
ul { display: block; page-break-before: avoid; }
|
||||
|
||||
dir ul { list-style-type: circle; margin-bottom: 0; margin-top: 0; }
|
||||
dl ul { margin-bottom: 0; margin-top: 0; }
|
||||
menu ul { list-style-type: circle; margin-bottom: 0; margin-top: 0; }
|
||||
ol ul { list-style-type: circle; margin-bottom: 0; margin-top: 0; }
|
||||
ul ul { list-style-type: circle; margin-bottom: 0; margin-top: 0; }
|
||||
|
||||
dir dir ul { list-style-type: square; }
|
||||
dir menu ul { list-style-type: square; }
|
||||
dir ol ul { list-style-type: square; }
|
||||
dir ul ul { list-style-type: square; }
|
||||
menu dir ul { list-style-type: square; }
|
||||
menu menu ul { list-style-type: square; }
|
||||
menu ol ul { list-style-type: square; }
|
||||
menu ul ul { list-style-type: square; }
|
||||
ol dir ul { list-style-type: square; }
|
||||
ol menu ul { list-style-type: square; }
|
||||
ol ol ul { list-style-type: square; }
|
||||
ol ul ul { list-style-type: square; }
|
||||
ul dir ul { list-style-type: square; }
|
||||
ul menu ul { list-style-type: square; }
|
||||
ul ol ul { list-style-type: square; }
|
||||
ul ul ul { list-style-type: square; }
|
||||
|
||||
var { font-style: italic; }
|
||||
video { object-fit: contain; }
|
||||
xmp { display: block; font-family: monospace; margin-bottom: 1em; margin-top: 1em; /* unicode-bidi: isolate; */ white-space: pre; }
|
||||
|
||||
::footnote-call { content: counter(footnote); vertical-align: baseline; font-size: 100%; line-height: inherit; font-variant-position: super; }
|
||||
::footnote-marker { content: counter(footnote) '. '; }
|
||||
|
||||
@page {
|
||||
/* `size: auto` (the initial) is A4 portrait */
|
||||
margin: 75px;
|
||||
@footnote { margin-top: 1em }
|
||||
@top-left-corner { text-align: right; vertical-align: middle }
|
||||
@top-left { text-align: left; vertical-align: middle }
|
||||
@top-center { text-align: center; vertical-align: middle }
|
||||
@top-right { text-align: right; vertical-align: middle }
|
||||
@top-right-corner { text-align: left; vertical-align: middle }
|
||||
@left-top { text-align: center; vertical-align: top }
|
||||
@left-middle { text-align: center; vertical-align: middle }
|
||||
@left-bottom { text-align: center; vertical-align: bottom }
|
||||
@right-top { text-align: center; vertical-align: top }
|
||||
@right-middle { text-align: center; vertical-align: middle }
|
||||
@right-bottom { text-align: center; vertical-align: bottom }
|
||||
@bottom-left-corner { text-align: right; vertical-align: middle }
|
||||
@bottom-left { text-align: left; vertical-align: middle }
|
||||
@bottom-center { text-align: center; vertical-align: middle }
|
||||
@bottom-right { text-align: right; vertical-align: middle }
|
||||
@bottom-right-corner { text-align: left; vertical-align: middle }
|
||||
}
|
||||
|
||||
|
||||
/* Counters: https://www.w3.org/TR/css-counter-styles-3/#predefined-counters */
|
||||
|
||||
@counter-style disc {
|
||||
system: cyclic;
|
||||
symbols: •;
|
||||
suffix: " ";
|
||||
}
|
||||
|
||||
@counter-style circle {
|
||||
system: cyclic;
|
||||
symbols: ◦;
|
||||
suffix: " ";
|
||||
}
|
||||
|
||||
@counter-style square {
|
||||
system: cyclic;
|
||||
symbols: ▪;
|
||||
suffix: " ";
|
||||
}
|
||||
|
||||
@counter-style disclosure-open {
|
||||
system: cyclic;
|
||||
symbols: ▾;
|
||||
suffix: " ";
|
||||
}
|
||||
|
||||
@counter-style disclosure-closed {
|
||||
system: cyclic;
|
||||
/* TODO: handle rtl */
|
||||
symbols: ▸;
|
||||
suffix: " ";
|
||||
}
|
||||
|
||||
@counter-style decimal {
|
||||
system: numeric;
|
||||
symbols: '0' '1' '2' '3' '4' '5' '6' '7' '8' '9';
|
||||
}
|
||||
|
||||
|
||||
@counter-style decimal-leading-zero {
|
||||
system: extends decimal;
|
||||
pad: 2 '0';
|
||||
}
|
||||
|
||||
@counter-style arabic-indic {
|
||||
system: numeric;
|
||||
symbols: ٠ ١ ٢ ٣ ٤ ٥ ٦ ٧ ٨ ٩;
|
||||
}
|
||||
|
||||
@counter-style armenian {
|
||||
system: additive;
|
||||
range: 1 9999;
|
||||
additive-symbols: 9000 Ք, 8000 Փ, 7000 Ւ, 6000 Ց, 5000 Ր, 4000 Տ, 3000 Վ, 2000 Ս, 1000 Ռ, 900 Ջ, 800 Պ, 700 Չ, 600 Ո, 500 Շ, 400 Ն, 300 Յ, 200 Մ, 100 Ճ, 90 Ղ, 80 Ձ, 70 Հ, 60 Կ, 50 Ծ, 40 Խ, 30 Լ, 20 Ի, 10 Ժ, 9 Թ, 8 Ը, 7 Է, 6 Զ, 5 Ե, 4 Դ, 3 Գ, 2 Բ, 1 Ա;
|
||||
}
|
||||
|
||||
@counter-style upper-armenian {
|
||||
system: extends armenian;
|
||||
}
|
||||
|
||||
@counter-style lower-armenian {
|
||||
system: additive;
|
||||
range: 1 9999;
|
||||
additive-symbols: 9000 ք, 8000 փ, 7000 ւ, 6000 ց, 5000 ր, 4000 տ, 3000 վ, 2000 ս, 1000 ռ, 900 ջ, 800 պ, 700 չ, 600 ո, 500 շ, 400 ն, 300 յ, 200 մ, 100 ճ, 90 ղ, 80 ձ, 70 հ, 60 կ, 50 ծ, 40 խ, 30 լ, 20 ի, 10 ժ, 9 թ, 8 ը, 7 է, 6 զ, 5 ե, 4 դ, 3 գ, 2 բ, 1 ա;
|
||||
}
|
||||
|
||||
@counter-style bengali {
|
||||
system: numeric;
|
||||
symbols: ০ ১ ২ ৩ ৪ ৫ ৬ ৭ ৮ ৯;
|
||||
}
|
||||
|
||||
@counter-style cambodian {
|
||||
system: numeric;
|
||||
symbols: ០ ១ ២ ៣ ៤ ៥ ៦ ៧ ៨ ៩;
|
||||
}
|
||||
|
||||
@counter-style khmer {
|
||||
system: extends cambodian;
|
||||
}
|
||||
|
||||
@counter-style cjk-decimal {
|
||||
system: numeric;
|
||||
range: 0 infinite;
|
||||
symbols: 〇 一 二 三 四 五 六 七 八 九;
|
||||
suffix: "、";
|
||||
}
|
||||
|
||||
@counter-style devanagari {
|
||||
system: numeric;
|
||||
symbols: ० १ २ ३ ४ ५ ६ ७ ८ ९;
|
||||
}
|
||||
|
||||
@counter-style georgian {
|
||||
system: additive;
|
||||
range: 1 19999;
|
||||
additive-symbols: 10000 ჵ, 9000 ჰ, 8000 ჯ, 7000 ჴ, 6000 ხ, 5000 ჭ, 4000 წ, 3000 ძ, 2000 ც, 1000 ჩ, 900 შ, 800 ყ, 700 ღ, 600 ქ, 500 ფ, 400 ჳ, 300 ტ, 200 ს, 100 რ, 90 ჟ, 80 პ, 70 ო, 60 ჲ, 50 ნ, 40 მ, 30 ლ, 20 კ, 10 ი, 9 თ, 8 ჱ, 7 ზ, 6 ვ, 5 ე, 4 დ, 3 გ, 2 ბ, 1 ა;
|
||||
}
|
||||
|
||||
@counter-style gujarati {
|
||||
system: numeric;
|
||||
symbols: ૦ ૧ ૨ ૩ ૪ ૫ ૬ ૭ ૮ ૯;
|
||||
}
|
||||
|
||||
@counter-style gurmukhi {
|
||||
system: numeric;
|
||||
symbols: ੦ ੧ ੨ ੩ ੪ ੫ ੬ ੭ ੮ ੯;
|
||||
}
|
||||
|
||||
@counter-style hebrew {
|
||||
system: additive;
|
||||
range: 1 10999;
|
||||
additive-symbols: 10000 י׳, 9000 ט׳, 8000 ח׳, 7000 ז׳, 6000 ו׳, 5000 ה׳, 4000 ד׳, 3000 ג׳, 2000 ב׳, 1000 א׳, 400 ת, 300 ש, 200 ר, 100 ק, 90 צ, 80 פ, 70 ע, 60 ס, 50 נ, 40 מ, 30 ל, 20 כ, 19 יט, 18 יח, 17 יז, 16 טז, 15 טו, 10 י, 9 ט, 8 ח, 7 ז, 6 ו, 5 ה, 4 ד, 3 ג, 2 ב, 1 א;
|
||||
}
|
||||
|
||||
@counter-style kannada {
|
||||
system: numeric;
|
||||
symbols: ೦ ೧ ೨ ೩ ೪ ೫ ೬ ೭ ೮ ೯;
|
||||
}
|
||||
|
||||
@counter-style lao {
|
||||
system: numeric;
|
||||
symbols: ໐ ໑ ໒ ໓ ໔ ໕ ໖ ໗ ໘ ໙;
|
||||
}
|
||||
|
||||
@counter-style malayalam {
|
||||
system: numeric;
|
||||
symbols: ൦ ൧ ൨ ൩ ൪ ൫ ൬ ൭ ൮ ൯;
|
||||
}
|
||||
|
||||
@counter-style mongolian {
|
||||
system: numeric;
|
||||
symbols: ᠐ ᠑ ᠒ ᠓ ᠔ ᠕ ᠖ ᠗ ᠘ ᠙;
|
||||
}
|
||||
|
||||
@counter-style myanmar {
|
||||
system: numeric;
|
||||
symbols: ၀ ၁ ၂ ၃ ၄ ၅ ၆ ၇ ၈ ၉;
|
||||
}
|
||||
|
||||
@counter-style oriya {
|
||||
system: numeric;
|
||||
symbols: ୦ ୧ ୨ ୩ ୪ ୫ ୬ ୭ ୮ ୯;
|
||||
}
|
||||
|
||||
@counter-style persian {
|
||||
system: numeric;
|
||||
symbols: ۰ ۱ ۲ ۳ ۴ ۵ ۶ ۷ ۸ ۹;
|
||||
}
|
||||
|
||||
@counter-style lower-roman {
|
||||
system: additive;
|
||||
range: 1 3999;
|
||||
additive-symbols: 1000 m, 900 cm, 500 d, 400 cd, 100 c, 90 xc, 50 l, 40 xl, 10 x, 9 ix, 5 v, 4 iv, 1 i;
|
||||
}
|
||||
|
||||
@counter-style upper-roman {
|
||||
system: additive;
|
||||
range: 1 3999;
|
||||
additive-symbols: 1000 M, 900 CM, 500 D, 400 CD, 100 C, 90 XC, 50 L, 40 XL, 10 X, 9 IX, 5 V, 4 IV, 1 I;
|
||||
}
|
||||
|
||||
@counter-style tamil {
|
||||
system: numeric;
|
||||
symbols: ௦ ௧ ௨ ௩ ௪ ௫ ௬ ௭ ௮ ௯;
|
||||
}
|
||||
|
||||
@counter-style telugu {
|
||||
system: numeric;
|
||||
symbols: ౦ ౧ ౨ ౩ ౪ ౫ ౬ ౭ ౮ ౯;
|
||||
}
|
||||
|
||||
@counter-style thai {
|
||||
system: numeric;
|
||||
symbols: ๐ ๑ ๒ ๓ ๔ ๕ ๖ ๗ ๘ ๙;
|
||||
}
|
||||
|
||||
@counter-style tibetan {
|
||||
system: numeric;
|
||||
symbols: ༠ ༡ ༢ ༣ ༤ ༥ ༦ ༧ ༨ ༩;
|
||||
}
|
||||
@counter-style lower-alpha {
|
||||
system: alphabetic;
|
||||
symbols: a b c d e f g h i j k l m n o p q r s t u v w x y z;
|
||||
}
|
||||
|
||||
@counter-style lower-latin {
|
||||
system: extends lower-alpha;
|
||||
}
|
||||
|
||||
@counter-style upper-alpha {
|
||||
system: alphabetic;
|
||||
symbols: A B C D E F G H I J K L M N O P Q R S T U V W X Y Z;
|
||||
}
|
||||
|
||||
@counter-style upper-latin {
|
||||
system: extends upper-alpha;
|
||||
}
|
||||
|
||||
@counter-style cjk-earthly-branch {
|
||||
system: alphabetic;
|
||||
symbols: 子 丑 寅 卯 辰 巳 午 未 申 酉 戌 亥;
|
||||
suffix: "、";
|
||||
}
|
||||
|
||||
@counter-style cjk-heavenly-stem {
|
||||
system: alphabetic;
|
||||
symbols: 甲 乙 丙 丁 戊 己 庚 辛 壬 癸;
|
||||
suffix: "、";
|
||||
}
|
||||
|
||||
@counter-style lower-greek {
|
||||
system: alphabetic;
|
||||
symbols: α β γ δ ε ζ η θ ι κ λ μ ν ξ ο π ρ σ τ υ φ χ ψ ω;
|
||||
}
|
||||
|
||||
@counter-style hiragana {
|
||||
system: alphabetic;
|
||||
symbols: あ い う え お か き く け こ さ し す せ そ た ち つ て と な に ぬ ね の は ひ ふ へ ほ ま み む め も や ゆ よ ら り る れ ろ わ ゐ ゑ を ん;
|
||||
suffix: "、";
|
||||
}
|
||||
|
||||
@counter-style hiragana-iroha {
|
||||
system: alphabetic;
|
||||
symbols: い ろ は に ほ へ と ち り ぬ る を わ か よ た れ そ つ ね な ら む う ゐ の お く や ま け ふ こ え て あ さ き ゆ め み し ゑ ひ も せ す;
|
||||
suffix: "、";
|
||||
}
|
||||
|
||||
@counter-style katakana {
|
||||
system: alphabetic;
|
||||
symbols: ア イ ウ エ オ カ キ ク ケ コ サ シ ス セ ソ タ チ ツ テ ト ナ ニ ヌ ネ ノ ハ ヒ フ ヘ ホ マ ミ ム メ モ ヤ ユ ヨ ラ リ ル レ ロ ワ ヰ ヱ ヲ ン;
|
||||
suffix: "、";
|
||||
}
|
||||
|
||||
@counter-style katakana-iroha {
|
||||
system: alphabetic;
|
||||
symbols: イ ロ ハ ニ ホ ヘ ト チ リ ヌ ル ヲ ワ カ ヨ タ レ ソ ツ ネ ナ ラ ム ウ ヰ ノ オ ク ヤ マ ケ フ コ エ テ ア サ キ ユ メ ミ シ ヱ ヒ モ セ ス;
|
||||
suffix: "、";
|
||||
}
|
||||
|
||||
@counter-style japanese-informal {
|
||||
system: additive;
|
||||
range: -9999 9999;
|
||||
additive-symbols: 9000 九千, 8000 八千, 7000 七千, 6000 六千, 5000 五千, 4000 四千, 3000 三千, 2000 二千, 1000 千, 900 九百, 800 八百, 700 七百, 600 六百, 500 五百, 400 四百, 300 三百, 200 二百, 100 百, 90 九十, 80 八十, 70 七十, 60 六十, 50 五十, 40 四十, 30 三十, 20 二十, 10 十, 9 九, 8 八, 7 七, 6 六, 5 五, 4 四, 3 三, 2 二, 1 一, 0 〇;
|
||||
suffix: 、;
|
||||
negative: マイナス;
|
||||
fallback: cjk-decimal;
|
||||
}
|
||||
|
||||
@counter-style japanese-formal {
|
||||
system: additive;
|
||||
range: -9999 9999;
|
||||
additive-symbols: 9000 九阡, 8000 八阡, 7000 七阡, 6000 六阡, 5000 伍阡, 4000 四阡, 3000 参阡, 2000 弐阡, 1000 壱阡, 900 九百, 800 八百, 700 七百, 600 六百, 500 伍百, 400 四百, 300 参百, 200 弐百, 100 壱百, 90 九拾, 80 八拾, 70 七拾, 60 六拾, 50 伍拾, 40 四拾, 30 参拾, 20 弐拾, 10 壱拾, 9 九, 8 八, 7 七, 6 六, 5 伍, 4 四, 3 参, 2 弐, 1 壱, 0 零;
|
||||
suffix: 、;
|
||||
negative: マイナス;
|
||||
fallback: cjk-decimal;
|
||||
}
|
||||
|
||||
@counter-style korean-hangul-formal {
|
||||
system: additive;
|
||||
range: -9999 9999;
|
||||
additive-symbols: 9000 구천, 8000 팔천, 7000 칠천, 6000 육천, 5000 오천, 4000 사천, 3000 삼천, 2000 이천, 1000 일천, 900 구백, 800 팔백, 700 칠백, 600 육백, 500 오백, 400 사백, 300 삼백, 200 이백, 100 일백, 90 구십, 80 팔십, 70 칠십, 60 육십, 50 오십, 40 사십, 30 삼십, 20 이십, 10 일십, 9 구, 8 팔, 7 칠, 6 육, 5 오, 4 사, 3 삼, 2 이, 1 일, 0 영;
|
||||
suffix: ', ';
|
||||
negative: "마이너스 ";
|
||||
}
|
||||
|
||||
@counter-style korean-hanja-informal {
|
||||
system: additive;
|
||||
range: -9999 9999;
|
||||
additive-symbols: 9000 九千, 8000 八千, 7000 七千, 6000 六千, 5000 五千, 4000 四千, 3000 三千, 2000 二千, 1000 千, 900 九百, 800 八百, 700 七百, 600 六百, 500 五百, 400 四百, 300 三百, 200 二百, 100 百, 90 九十, 80 八十, 70 七十, 60 六十, 50 五十, 40 四十, 30 三十, 20 二十, 10 十, 9 九, 8 八, 7 七, 6 六, 5 五, 4 四, 3 三, 2 二, 1 一, 0 零;
|
||||
suffix: ', ';
|
||||
negative: "마이너스 ";
|
||||
}
|
||||
|
||||
@counter-style korean-hanja-formal {
|
||||
system: additive;
|
||||
range: -9999 9999;
|
||||
additive-symbols: 9000 九仟, 8000 八仟, 7000 七仟, 6000 六仟, 5000 五仟, 4000 四仟, 3000 參仟, 2000 貳仟, 1000 壹仟, 900 九百, 800 八百, 700 七百, 600 六百, 500 五百, 400 四百, 300 參百, 200 貳百, 100 壹百, 90 九拾, 80 八拾, 70 七拾, 60 六拾, 50 五拾, 40 四拾, 30 參拾, 20 貳拾, 10 壹拾, 9 九, 8 八, 7 七, 6 六, 5 五, 4 四, 3 參, 2 貳, 1 壹, 0 零;
|
||||
suffix: ', ';
|
||||
negative: "마이너스 ";
|
||||
}
|
||||
229
venv/Lib/site-packages/weasyprint/css/targets.py
Normal file
229
venv/Lib/site-packages/weasyprint/css/targets.py
Normal file
@@ -0,0 +1,229 @@
|
||||
"""
|
||||
weasyprint.formatting_structure.targets
|
||||
---------------------------------------
|
||||
|
||||
Handle target-counter, target-counters and target-text.
|
||||
|
||||
The TargetCollector is a structure providing required targets'
|
||||
counter_values and stuff needed to build pending targets later,
|
||||
when the layout of all targeted anchors has been done.
|
||||
|
||||
"""
|
||||
|
||||
import copy
|
||||
|
||||
from ..logger import LOGGER
|
||||
|
||||
|
||||
class TargetLookupItem:
|
||||
"""Item controlling pending targets and page based target counters.
|
||||
|
||||
Collected in the TargetCollector's ``target_lookup_items``.
|
||||
|
||||
"""
|
||||
def __init__(self, state='pending'):
|
||||
self.state = state
|
||||
|
||||
# Required by target-counter and target-counters to access the
|
||||
# target's .cached_counter_values.
|
||||
# Needed for target-text via TEXT_CONTENT_EXTRACTORS.
|
||||
self.target_box = None
|
||||
|
||||
# Functions that have to been called to check pending targets.
|
||||
# Keys are (source_box, css_token).
|
||||
self.parse_again_functions = {}
|
||||
|
||||
# Anchor position during pagination (page_number - 1)
|
||||
self.page_maker_index = None
|
||||
|
||||
# target_box's page_counters during pagination
|
||||
self.cached_page_counter_values = {}
|
||||
|
||||
|
||||
class CounterLookupItem:
|
||||
"""Item controlling page based counters.
|
||||
|
||||
Collected in the TargetCollector's ``counter_lookup_items``.
|
||||
|
||||
"""
|
||||
def __init__(self, parse_again, missing_counters, missing_target_counters):
|
||||
# Function that have to been called to check pending counter.
|
||||
self.parse_again = parse_again
|
||||
|
||||
# Missing counters and target counters
|
||||
self.missing_counters = missing_counters
|
||||
self.missing_target_counters = missing_target_counters
|
||||
|
||||
# Box position during pagination (page_number - 1)
|
||||
self.page_maker_index = None
|
||||
|
||||
# Marker for remake_page
|
||||
self.pending = False
|
||||
|
||||
# Targeting box's page_counters during pagination
|
||||
self.cached_page_counter_values = {}
|
||||
|
||||
|
||||
class TargetCollector:
|
||||
"""Collector of HTML targets used by CSS content with ``target-*``."""
|
||||
|
||||
def __init__(self):
|
||||
# Lookup items for targets and page counters
|
||||
self.target_lookup_items = {}
|
||||
self.counter_lookup_items = {}
|
||||
|
||||
# When collecting is True, compute_content_list() collects missing
|
||||
# page counters in CounterLookupItems. Otherwise, it mixes in the
|
||||
# TargetLookupItem's cached_page_counter_values.
|
||||
# Is switched to False in check_pending_targets().
|
||||
self.collecting = True
|
||||
|
||||
# had_pending_targets is set to True when a target is needed but has
|
||||
# not been seen yet. check_pending_targets then uses this information
|
||||
# to call the needed parse_again functions.
|
||||
self.had_pending_targets = False
|
||||
|
||||
def anchor_name_from_token(self, anchor_token):
|
||||
"""Get anchor name from string or uri token."""
|
||||
if anchor_token[0] == 'string' and anchor_token[1].startswith('#'):
|
||||
return anchor_token[1][1:]
|
||||
elif anchor_token[0] == 'url' and anchor_token[1][0] == 'internal':
|
||||
return anchor_token[1][1]
|
||||
|
||||
def collect_anchor(self, anchor_name):
|
||||
"""Create a TargetLookupItem for the given `anchor_name``."""
|
||||
if anchor_name and isinstance(anchor_name, str):
|
||||
if self.target_lookup_items.get(anchor_name) is not None:
|
||||
LOGGER.warning('Anchor defined twice: %r', anchor_name)
|
||||
else:
|
||||
self.target_lookup_items.setdefault(
|
||||
anchor_name, TargetLookupItem())
|
||||
|
||||
def lookup_target(self, anchor_token, source_box, css_token, parse_again):
|
||||
"""Get a TargetLookupItem corresponding to ``anchor_token``.
|
||||
|
||||
If it is already filled by a previous anchor-element, the status is
|
||||
'up-to-date'. Otherwise, it is 'pending', we must parse the whole
|
||||
tree again.
|
||||
|
||||
"""
|
||||
anchor_name = self.anchor_name_from_token(anchor_token)
|
||||
item = self.target_lookup_items.get(
|
||||
anchor_name, TargetLookupItem('undefined'))
|
||||
|
||||
if item.state == 'pending':
|
||||
self.had_pending_targets = True
|
||||
item.parse_again_functions.setdefault(
|
||||
(source_box, css_token), parse_again)
|
||||
|
||||
if item.state == 'undefined':
|
||||
LOGGER.error(
|
||||
'Content discarded: target points to undefined anchor %r',
|
||||
anchor_token)
|
||||
|
||||
return item
|
||||
|
||||
def store_target(self, anchor_name, target_counter_values, target_box):
|
||||
"""Store a target called ``anchor_name``.
|
||||
|
||||
If there is a pending TargetLookupItem, it is updated. Only previously
|
||||
collected anchors are stored.
|
||||
|
||||
"""
|
||||
item = self.target_lookup_items.get(anchor_name)
|
||||
if item and item.state == 'pending':
|
||||
item.state = 'up-to-date'
|
||||
item.target_box = target_box
|
||||
# Store the counter_values in the target_box like
|
||||
# compute_content_list does.
|
||||
# TODO: remove attribute or set a default value in Box class
|
||||
if not hasattr(target_box, 'cached_counter_values'):
|
||||
target_box.cached_counter_values = copy.deepcopy(
|
||||
target_counter_values)
|
||||
|
||||
def collect_missing_counters(self, parent_box, css_token,
|
||||
parse_again_function, missing_counters,
|
||||
missing_target_counters):
|
||||
"""Collect missing (probably page-based) counters during formatting.
|
||||
|
||||
The ``missing_counters`` are re-used during pagination.
|
||||
|
||||
The ``missing_link`` attribute added to the parent_box is required to
|
||||
connect the paginated boxes to their originating ``parent_box``.
|
||||
|
||||
"""
|
||||
# No counter collection during pagination
|
||||
if not self.collecting:
|
||||
return
|
||||
|
||||
# No need to add empty miss-lists
|
||||
if missing_counters or missing_target_counters:
|
||||
# TODO: remove attribute or set a default value in Box class
|
||||
if not hasattr(parent_box, 'missing_link'):
|
||||
parent_box.missing_link = parent_box
|
||||
counter_lookup_item = CounterLookupItem(
|
||||
parse_again_function, missing_counters,
|
||||
missing_target_counters)
|
||||
self.counter_lookup_items.setdefault(
|
||||
(parent_box, css_token), counter_lookup_item)
|
||||
|
||||
def check_pending_targets(self):
|
||||
"""Check pending targets if needed."""
|
||||
if self.had_pending_targets:
|
||||
for item in self.target_lookup_items.values():
|
||||
for function in item.parse_again_functions.values():
|
||||
function()
|
||||
self.had_pending_targets = False
|
||||
# Ready for pagination
|
||||
self.collecting = False
|
||||
|
||||
def cache_target_page_counters(self, anchor_name, page_counter_values,
|
||||
page_maker_index, page_maker):
|
||||
"""Store target's current ``page_maker_index`` and page counter values.
|
||||
|
||||
Eventually update associated targeting boxes.
|
||||
|
||||
"""
|
||||
# Only store page counters when paginating
|
||||
if self.collecting:
|
||||
return
|
||||
|
||||
item = self.target_lookup_items.get(anchor_name)
|
||||
if item and item.state == 'up-to-date':
|
||||
item.page_maker_index = page_maker_index
|
||||
if item.cached_page_counter_values != page_counter_values:
|
||||
item.cached_page_counter_values = copy.deepcopy(
|
||||
page_counter_values)
|
||||
|
||||
# Spread the news: update boxes affected by a change in the
|
||||
# anchor's page counter values.
|
||||
for (_, css_token), item in self.counter_lookup_items.items():
|
||||
# Only update items that need counters in their content
|
||||
if css_token != 'content':
|
||||
continue
|
||||
|
||||
# Don't update if item has no missing target counter
|
||||
missing_counters = item.missing_target_counters.get(
|
||||
anchor_name)
|
||||
if missing_counters is None:
|
||||
continue
|
||||
|
||||
# Pending marker for remake_page
|
||||
if (item.page_maker_index is None or
|
||||
item.page_maker_index >= len(page_maker)):
|
||||
item.pending = True
|
||||
continue
|
||||
|
||||
# TODO: Is the item at all interested in the new
|
||||
# page_counter_values? It probably is and this check is a
|
||||
# brake.
|
||||
for counter_name in missing_counters:
|
||||
counter_value = page_counter_values.get(counter_name)
|
||||
if counter_value is not None:
|
||||
remake_state = (
|
||||
page_maker[item.page_maker_index][-1])
|
||||
remake_state['content_changed'] = True
|
||||
item.parse_again(item.cached_page_counter_values)
|
||||
break
|
||||
# Hint: the box's own cached page counters trigger a
|
||||
# separate 'content_changed'.
|
||||
39
venv/Lib/site-packages/weasyprint/css/tests_ua.css
Normal file
39
venv/Lib/site-packages/weasyprint/css/tests_ua.css
Normal file
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
Simplified user-agent stylesheet for HTML5 in tests.
|
||||
*/
|
||||
@page { bleed: 0; @footnote { margin: 0 } }
|
||||
html, body, div, h1, h2, h3, h4, ol, p, ul, hr, pre, section, article
|
||||
{ display: block; }
|
||||
li { display: list-item }
|
||||
head { display: none }
|
||||
pre { white-space: pre }
|
||||
br:before { content: '\A'; white-space: pre-line }
|
||||
ol { list-style-type: decimal }
|
||||
ol, ul { counter-reset: list-item }
|
||||
|
||||
table, x-table { display: table;
|
||||
box-sizing: border-box }
|
||||
tr, x-tr { display: table-row }
|
||||
thead, x-thead { display: table-header-group }
|
||||
tbody, x-tbody { display: table-row-group }
|
||||
tfoot, x-tfoot { display: table-footer-group }
|
||||
col, x-col { display: table-column }
|
||||
colgroup, x-colgroup { display: table-column-group }
|
||||
td, th, x-td, x-th { display: table-cell }
|
||||
caption, x-caption { display: table-caption }
|
||||
|
||||
*[lang] { -weasy-lang: attr(lang); }
|
||||
a[href] { -weasy-link: attr(href); }
|
||||
a[name] { -weasy-anchor: attr(name); }
|
||||
*[id] { -weasy-anchor: attr(id); }
|
||||
h1 { bookmark-level: 1; bookmark-label: content(text); }
|
||||
h2 { bookmark-level: 2; bookmark-label: content(text); }
|
||||
h3 { bookmark-level: 3; bookmark-label: content(text); }
|
||||
h4 { bookmark-level: 4; bookmark-label: content(text); }
|
||||
h5 { bookmark-level: 5; bookmark-label: content(text); }
|
||||
h6 { bookmark-level: 6; bookmark-label: content(text); }
|
||||
|
||||
::marker { unicode-bidi: isolate; font-variant-numeric: tabular-nums; }
|
||||
|
||||
::footnote-call { content: counter(footnote); }
|
||||
::footnote-marker { content: counter(footnote) '.'; }
|
||||
950
venv/Lib/site-packages/weasyprint/layout/block.py
Normal file
950
venv/Lib/site-packages/weasyprint/layout/block.py
Normal file
@@ -0,0 +1,950 @@
|
||||
"""
|
||||
weasyprint.layout.block
|
||||
-----------------------
|
||||
|
||||
Page breaking and layout for block-level and block-container boxes.
|
||||
|
||||
"""
|
||||
|
||||
from ..formatting_structure import boxes
|
||||
from .absolute import AbsolutePlaceholder, absolute_layout
|
||||
from .column import columns_layout
|
||||
from .flex import flex_layout
|
||||
from .float import avoid_collisions, float_layout, get_clearance
|
||||
from .inline import iter_line_boxes
|
||||
from .min_max import handle_min_max_width
|
||||
from .percent import resolve_percentages, resolve_position_percentages
|
||||
from .replaced import block_replaced_box_layout
|
||||
from .table import table_layout, table_wrapper_width
|
||||
|
||||
|
||||
def block_level_layout(context, box, bottom_space, skip_stack,
|
||||
containing_block, page_is_empty, absolute_boxes,
|
||||
fixed_boxes, adjoining_margins, discard):
|
||||
"""Lay out the block-level ``box``."""
|
||||
if not isinstance(box, boxes.TableBox):
|
||||
resolve_percentages(box, containing_block)
|
||||
|
||||
if box.margin_top == 'auto':
|
||||
box.margin_top = 0
|
||||
if box.margin_bottom == 'auto':
|
||||
box.margin_bottom = 0
|
||||
|
||||
if context.current_page > 1 and page_is_empty:
|
||||
# TODO: this condition is wrong, it only works for blocks whose
|
||||
# parent breaks collapsing margins. It should work for blocks whose
|
||||
# one of the ancestors breaks collapsing margins.
|
||||
# See test_margin_break_clearance.
|
||||
collapse_with_page = (
|
||||
containing_block.is_for_root_element or
|
||||
adjoining_margins)
|
||||
if collapse_with_page:
|
||||
if box.style['margin_break'] == 'discard':
|
||||
box.margin_top = 0
|
||||
elif box.style['margin_break'] == 'auto':
|
||||
if not context.forced_break:
|
||||
box.margin_top = 0
|
||||
|
||||
collapsed_margin = collapse_margin(
|
||||
adjoining_margins + [box.margin_top])
|
||||
box.clearance = get_clearance(context, box, collapsed_margin)
|
||||
if box.clearance is not None:
|
||||
top_border_edge = box.position_y + collapsed_margin + box.clearance
|
||||
box.position_y = top_border_edge - box.margin_top
|
||||
adjoining_margins = []
|
||||
|
||||
return block_level_layout_switch(
|
||||
context, box, bottom_space, skip_stack, containing_block,
|
||||
page_is_empty, absolute_boxes, fixed_boxes, adjoining_margins, discard)
|
||||
|
||||
|
||||
def block_level_layout_switch(context, box, bottom_space, skip_stack,
|
||||
containing_block, page_is_empty, absolute_boxes,
|
||||
fixed_boxes, adjoining_margins, discard):
|
||||
"""Call the layout function corresponding to the ``box`` type."""
|
||||
if isinstance(box, boxes.TableBox):
|
||||
return table_layout(
|
||||
context, box, bottom_space, skip_stack, containing_block,
|
||||
page_is_empty, absolute_boxes, fixed_boxes)
|
||||
elif isinstance(box, boxes.BlockBox):
|
||||
return block_box_layout(
|
||||
context, box, bottom_space, skip_stack, containing_block,
|
||||
page_is_empty, absolute_boxes, fixed_boxes, adjoining_margins,
|
||||
discard)
|
||||
elif isinstance(box, boxes.BlockReplacedBox):
|
||||
return block_replaced_box_layout(context, box, containing_block)
|
||||
elif isinstance(box, boxes.FlexBox):
|
||||
return flex_layout(
|
||||
context, box, bottom_space, skip_stack, containing_block,
|
||||
page_is_empty, absolute_boxes, fixed_boxes)
|
||||
else: # pragma: no cover
|
||||
raise TypeError(f'Layout for {type(box).__name__} not handled yet')
|
||||
|
||||
|
||||
def block_box_layout(context, box, bottom_space, skip_stack,
|
||||
containing_block, page_is_empty, absolute_boxes,
|
||||
fixed_boxes, adjoining_margins, discard):
|
||||
"""Lay out the block ``box``."""
|
||||
if (box.style['column_width'] != 'auto' or
|
||||
box.style['column_count'] != 'auto'):
|
||||
result = columns_layout(
|
||||
context, box, bottom_space, skip_stack, containing_block,
|
||||
page_is_empty, absolute_boxes, fixed_boxes, adjoining_margins)
|
||||
resume_at = result[1]
|
||||
# TODO: this condition and the whole relayout are probably wrong
|
||||
if resume_at is None:
|
||||
new_box = result[0]
|
||||
columns_bottom_space = (
|
||||
new_box.margin_bottom + new_box.padding_bottom +
|
||||
new_box.border_bottom_width)
|
||||
if columns_bottom_space:
|
||||
bottom_space += columns_bottom_space
|
||||
result = columns_layout(
|
||||
context, box, bottom_space, skip_stack,
|
||||
containing_block, page_is_empty, absolute_boxes,
|
||||
fixed_boxes, adjoining_margins)
|
||||
return result
|
||||
elif box.is_table_wrapper:
|
||||
table_wrapper_width(
|
||||
context, box, (containing_block.width, containing_block.height))
|
||||
block_level_width(box, containing_block)
|
||||
|
||||
result = block_container_layout(
|
||||
context, box, bottom_space, skip_stack, page_is_empty,
|
||||
absolute_boxes, fixed_boxes, adjoining_margins, discard)
|
||||
new_box = result[0]
|
||||
if new_box and new_box.is_table_wrapper:
|
||||
# Don't collide with floats
|
||||
# http://www.w3.org/TR/CSS21/visuren.html#floats
|
||||
position_x, position_y, _ = avoid_collisions(
|
||||
context, new_box, containing_block, outer=False)
|
||||
new_box.translate(
|
||||
position_x - new_box.position_x, position_y - new_box.position_y)
|
||||
return result
|
||||
|
||||
|
||||
@handle_min_max_width
|
||||
def block_level_width(box, containing_block):
|
||||
"""Set the ``box`` width."""
|
||||
# 'cb' stands for 'containing block'
|
||||
if isinstance(containing_block, boxes.Box):
|
||||
cb_width = containing_block.width
|
||||
direction = containing_block.style['direction']
|
||||
else:
|
||||
cb_width = containing_block[0]
|
||||
# TODO: what is the real text direction?
|
||||
direction = 'ltr'
|
||||
|
||||
# http://www.w3.org/TR/CSS21/visudet.html#blockwidth
|
||||
|
||||
# These names are waaay too long
|
||||
margin_l = box.margin_left
|
||||
margin_r = box.margin_right
|
||||
padding_l = box.padding_left
|
||||
padding_r = box.padding_right
|
||||
border_l = box.border_left_width
|
||||
border_r = box.border_right_width
|
||||
width = box.width
|
||||
|
||||
# Only margin-left, margin-right and width can be 'auto'.
|
||||
# We want: width of containing block ==
|
||||
# margin-left + border-left-width + padding-left + width
|
||||
# + padding-right + border-right-width + margin-right
|
||||
|
||||
paddings_plus_borders = padding_l + padding_r + border_l + border_r
|
||||
if box.width != 'auto':
|
||||
total = paddings_plus_borders + width
|
||||
if margin_l != 'auto':
|
||||
total += margin_l
|
||||
if margin_r != 'auto':
|
||||
total += margin_r
|
||||
if total > cb_width:
|
||||
if margin_l == 'auto':
|
||||
margin_l = box.margin_left = 0
|
||||
if margin_r == 'auto':
|
||||
margin_r = box.margin_right = 0
|
||||
if width != 'auto' and margin_l != 'auto' and margin_r != 'auto':
|
||||
# The equation is over-constrained.
|
||||
if direction == 'rtl' and not box.is_column:
|
||||
box.position_x += (
|
||||
cb_width - paddings_plus_borders - width - margin_r - margin_l)
|
||||
# Do nothing in ltr.
|
||||
if width == 'auto':
|
||||
if margin_l == 'auto':
|
||||
margin_l = box.margin_left = 0
|
||||
if margin_r == 'auto':
|
||||
margin_r = box.margin_right = 0
|
||||
width = box.width = cb_width - (
|
||||
paddings_plus_borders + margin_l + margin_r)
|
||||
margin_sum = cb_width - paddings_plus_borders - width
|
||||
if margin_l == 'auto' and margin_r == 'auto':
|
||||
box.margin_left = margin_sum / 2.
|
||||
box.margin_right = margin_sum / 2.
|
||||
elif margin_l == 'auto' and margin_r != 'auto':
|
||||
box.margin_left = margin_sum - margin_r
|
||||
elif margin_l != 'auto' and margin_r == 'auto':
|
||||
box.margin_right = margin_sum - margin_l
|
||||
|
||||
|
||||
def relative_positioning(box, containing_block):
|
||||
"""Translate the ``box`` if it is relatively positioned."""
|
||||
if box.style['position'] == 'relative':
|
||||
resolve_position_percentages(box, containing_block)
|
||||
|
||||
if box.left != 'auto' and box.right != 'auto':
|
||||
if box.style['direction'] == 'ltr':
|
||||
translate_x = box.left
|
||||
else:
|
||||
translate_x = -box.right
|
||||
elif box.left != 'auto':
|
||||
translate_x = box.left
|
||||
elif box.right != 'auto':
|
||||
translate_x = -box.right
|
||||
else:
|
||||
translate_x = 0
|
||||
|
||||
if box.top != 'auto':
|
||||
translate_y = box.top
|
||||
elif box.style['bottom'] != 'auto':
|
||||
translate_y = -box.bottom
|
||||
else:
|
||||
translate_y = 0
|
||||
|
||||
box.translate(translate_x, translate_y)
|
||||
|
||||
if isinstance(box, (boxes.InlineBox, boxes.LineBox)):
|
||||
for child in box.children:
|
||||
relative_positioning(child, containing_block)
|
||||
|
||||
|
||||
def _out_of_flow_layout(context, box, index, child, new_children,
|
||||
page_is_empty, absolute_boxes, fixed_boxes,
|
||||
adjoining_margins, bottom_space):
|
||||
stop = False
|
||||
resume_at = None
|
||||
out_of_flow_resume_at = None
|
||||
|
||||
child.position_y += collapse_margin(adjoining_margins)
|
||||
if child.is_absolutely_positioned():
|
||||
placeholder = AbsolutePlaceholder(child)
|
||||
placeholder.index = index
|
||||
new_children.append(placeholder)
|
||||
if child.style['position'] == 'absolute':
|
||||
absolute_boxes.append(placeholder)
|
||||
else:
|
||||
fixed_boxes.append(placeholder)
|
||||
elif child.is_floated():
|
||||
new_child, out_of_flow_resume_at = float_layout(
|
||||
context, child, box, absolute_boxes, fixed_boxes, bottom_space,
|
||||
skip_stack=None)
|
||||
# New page if overflow
|
||||
if (page_is_empty and not new_children) or not (
|
||||
new_child.position_y + new_child.height >
|
||||
context.page_bottom - bottom_space):
|
||||
new_child.index = index
|
||||
new_children.append(new_child)
|
||||
else:
|
||||
last_in_flow_child = find_last_in_flow_child(new_children)
|
||||
page_break = block_level_page_break(last_in_flow_child, child)
|
||||
resume_at = {index: None}
|
||||
if new_children and page_break in ('avoid', 'avoid-page'):
|
||||
result = find_earlier_page_break(
|
||||
new_children, absolute_boxes, fixed_boxes)
|
||||
if result:
|
||||
new_children, resume_at = result
|
||||
stop = True
|
||||
elif child.is_running():
|
||||
running_name = child.style['position'][1]
|
||||
page = context.current_page
|
||||
context.running_elements[running_name][page].append(child)
|
||||
|
||||
return stop, resume_at, out_of_flow_resume_at
|
||||
|
||||
|
||||
def _break_line(box, new_children, lines_iterator, page_is_empty, index,
|
||||
skip_stack, resume_at):
|
||||
over_orphans = len(new_children) - box.style['orphans']
|
||||
if over_orphans < 0 and not page_is_empty:
|
||||
# Reached the bottom of the page before we had
|
||||
# enough lines for orphans, cancel the whole box.
|
||||
return True, False, resume_at
|
||||
# How many lines we need on the next page to satisfy widows
|
||||
# -1 for the current line.
|
||||
needed = box.style['widows'] - 1
|
||||
if needed:
|
||||
for _ in lines_iterator:
|
||||
needed -= 1
|
||||
if needed == 0:
|
||||
break
|
||||
if needed > over_orphans and not page_is_empty:
|
||||
# Total number of lines < orphans + widows
|
||||
return True, False, resume_at
|
||||
if needed and needed <= over_orphans:
|
||||
# Remove lines to keep them for the next page
|
||||
del new_children[-needed:]
|
||||
# Page break here, resume before this line
|
||||
return False, True, {index: skip_stack}
|
||||
|
||||
|
||||
def _linebox_layout(context, box, index, child, new_children, page_is_empty,
|
||||
absolute_boxes, fixed_boxes, adjoining_margins,
|
||||
bottom_space, position_y, skip_stack, first_letter_style,
|
||||
draw_bottom_decoration):
|
||||
abort = stop = False
|
||||
resume_at = None
|
||||
|
||||
assert len(box.children) == 1, 'line box with siblings before layout'
|
||||
|
||||
if adjoining_margins:
|
||||
position_y += collapse_margin(adjoining_margins)
|
||||
new_containing_block = box
|
||||
lines_iterator = iter_line_boxes(
|
||||
context, child, position_y, bottom_space, skip_stack,
|
||||
new_containing_block, absolute_boxes, fixed_boxes, first_letter_style)
|
||||
for i, (line, resume_at) in enumerate(lines_iterator):
|
||||
line.resume_at = resume_at
|
||||
new_position_y = line.position_y + line.height
|
||||
|
||||
# Add bottom padding and border to the bottom position of the
|
||||
# box if needed
|
||||
draw_bottom_decoration |= resume_at is None
|
||||
if draw_bottom_decoration:
|
||||
offset_y = box.border_bottom_width + box.padding_bottom
|
||||
else:
|
||||
offset_y = 0
|
||||
|
||||
# Allow overflow if the first line of the page is higher
|
||||
# than the page itself so that we put *something* on this
|
||||
# page and can advance in the context.
|
||||
overflow = (
|
||||
(new_children or not page_is_empty) and
|
||||
(new_position_y + offset_y > context.page_bottom - bottom_space))
|
||||
if overflow:
|
||||
abort, stop, resume_at = _break_line(
|
||||
box, new_children, lines_iterator, page_is_empty, index,
|
||||
skip_stack, resume_at)
|
||||
break
|
||||
|
||||
# TODO: this is incomplete.
|
||||
# See http://dev.w3.org/csswg/css3-page/#allowed-pg-brk
|
||||
# "When an unforced page break occurs here, both the adjoining
|
||||
# ‘margin-top’ and ‘margin-bottom’ are set to zero."
|
||||
# See https://github.com/Kozea/WeasyPrint/issues/115
|
||||
elif page_is_empty and (
|
||||
new_position_y > context.page_bottom - bottom_space):
|
||||
# Remove the top border when a page is empty and the box is
|
||||
# too high to be drawn in one page
|
||||
new_position_y -= box.margin_top
|
||||
line.translate(0, -box.margin_top)
|
||||
box.margin_top = 0
|
||||
|
||||
if context.footnotes:
|
||||
break_linebox = False
|
||||
footnotes = (
|
||||
descendant.footnote for descendant in line.descendants()
|
||||
if descendant.footnote in context.footnotes)
|
||||
for footnote in footnotes:
|
||||
context.layout_footnote(footnote)
|
||||
overflow = context.reported_footnotes or (
|
||||
new_position_y + offset_y >
|
||||
context.page_bottom - bottom_space)
|
||||
if overflow:
|
||||
context.report_footnote(footnote)
|
||||
if footnote.style['footnote_policy'] == 'line':
|
||||
abort, stop, resume_at = _break_line(
|
||||
box, new_children, lines_iterator, page_is_empty,
|
||||
index, skip_stack, resume_at)
|
||||
break_linebox = True
|
||||
elif footnote.style['footnote_policy'] == 'block':
|
||||
abort = break_linebox = True
|
||||
break
|
||||
if break_linebox:
|
||||
break
|
||||
|
||||
new_children.append(line)
|
||||
position_y = new_position_y
|
||||
skip_stack = resume_at
|
||||
|
||||
# Break box if we reached max-lines
|
||||
if box.style['max_lines'] != 'none':
|
||||
if i >= box.style['max_lines'] - 1:
|
||||
line.block_ellipsis = box.style['block_ellipsis']
|
||||
break
|
||||
|
||||
if new_children:
|
||||
resume_at = {index: new_children[-1].resume_at}
|
||||
|
||||
return abort, stop, resume_at, position_y
|
||||
|
||||
|
||||
def _in_flow_layout(context, box, index, child, new_children, page_is_empty,
|
||||
absolute_boxes, fixed_boxes, adjoining_margins,
|
||||
bottom_space, position_y, skip_stack, first_letter_style,
|
||||
draw_bottom_decoration, collapsing_with_children, discard,
|
||||
next_page):
|
||||
abort = stop = False
|
||||
|
||||
last_in_flow_child = find_last_in_flow_child(new_children)
|
||||
if last_in_flow_child is not None:
|
||||
# Between in-flow siblings
|
||||
page_break = block_level_page_break(last_in_flow_child, child)
|
||||
page_name = block_level_page_name(last_in_flow_child, child)
|
||||
if page_name or page_break in (
|
||||
'page', 'left', 'right', 'recto', 'verso'):
|
||||
page_name = child.page_values()[0]
|
||||
next_page = {'break': page_break, 'page': page_name}
|
||||
resume_at = {index: None}
|
||||
stop = True
|
||||
return (
|
||||
abort, stop, resume_at, position_y, adjoining_margins,
|
||||
next_page, new_children)
|
||||
else:
|
||||
page_break = 'auto'
|
||||
|
||||
new_containing_block = box
|
||||
|
||||
if not new_containing_block.is_table_wrapper:
|
||||
resolve_percentages(child, new_containing_block)
|
||||
if (child.is_in_normal_flow() and last_in_flow_child is None and
|
||||
collapsing_with_children):
|
||||
# TODO: add the adjoining descendants' margin top to
|
||||
# [child.margin_top]
|
||||
old_collapsed_margin = collapse_margin(adjoining_margins)
|
||||
if child.margin_top == 'auto':
|
||||
child_margin_top = 0
|
||||
else:
|
||||
child_margin_top = child.margin_top
|
||||
new_collapsed_margin = collapse_margin(
|
||||
adjoining_margins + [child_margin_top])
|
||||
collapsed_margin_difference = (
|
||||
new_collapsed_margin - old_collapsed_margin)
|
||||
for previous_new_child in new_children:
|
||||
previous_new_child.translate(dy=collapsed_margin_difference)
|
||||
clearance = get_clearance(context, child, new_collapsed_margin)
|
||||
if clearance is not None:
|
||||
for previous_new_child in new_children:
|
||||
previous_new_child.translate(
|
||||
dy=-collapsed_margin_difference)
|
||||
|
||||
collapsed_margin = collapse_margin(adjoining_margins)
|
||||
box.position_y += collapsed_margin - box.margin_top
|
||||
# Count box.margin_top as we emptied adjoining_margins
|
||||
adjoining_margins = []
|
||||
position_y = box.content_box_y()
|
||||
|
||||
if adjoining_margins and box.is_table_wrapper:
|
||||
collapsed_margin = collapse_margin(adjoining_margins)
|
||||
child.position_y += collapsed_margin
|
||||
position_y += collapsed_margin
|
||||
adjoining_margins = []
|
||||
|
||||
page_is_empty_with_no_children = page_is_empty and not any(
|
||||
child for child in new_children
|
||||
if not isinstance(child, AbsolutePlaceholder))
|
||||
|
||||
if not getattr(child, 'first_letter_style', None):
|
||||
child.first_letter_style = first_letter_style
|
||||
(new_child, resume_at, next_page, next_adjoining_margins,
|
||||
collapsing_through) = block_level_layout(
|
||||
context, child, bottom_space, skip_stack,
|
||||
new_containing_block, page_is_empty_with_no_children, absolute_boxes,
|
||||
fixed_boxes, adjoining_margins, discard)
|
||||
|
||||
if new_child is not None:
|
||||
# index in its non-laid-out parent, not in future new parent
|
||||
# May be used in find_earlier_page_break()
|
||||
new_child.index = index
|
||||
|
||||
# We need to do this after the child layout to have the
|
||||
# used value for margin_top (eg. it might be a percentage.)
|
||||
if not isinstance(new_child, (boxes.BlockBox, boxes.TableBox)):
|
||||
adjoining_margins.append(new_child.margin_top)
|
||||
offset_y = (
|
||||
collapse_margin(adjoining_margins) - new_child.margin_top)
|
||||
new_child.translate(0, offset_y)
|
||||
# else: blocks handle that themselves.
|
||||
|
||||
if not collapsing_through:
|
||||
new_content_position_y = (
|
||||
new_child.content_box_y() + new_child.height)
|
||||
new_position_y = (
|
||||
new_child.border_box_y() + new_child.border_height())
|
||||
|
||||
if (new_content_position_y > context.page_bottom - bottom_space and
|
||||
not page_is_empty_with_no_children):
|
||||
# The child content overflows the page area, display it on the
|
||||
# next page.
|
||||
remove_placeholders([new_child], absolute_boxes, fixed_boxes)
|
||||
new_child = None
|
||||
elif (new_position_y > context.page_bottom - bottom_space and
|
||||
not page_is_empty_with_no_children):
|
||||
# The child border/padding overflows the page area, do the
|
||||
# layout again with a higher bottom_space value.
|
||||
remove_placeholders([new_child], absolute_boxes, fixed_boxes)
|
||||
bottom_space += (
|
||||
new_child.padding_bottom + new_child.border_bottom_width)
|
||||
(new_child, resume_at, next_page, next_adjoining_margins,
|
||||
collapsing_through) = block_level_layout(
|
||||
context, child, bottom_space, skip_stack,
|
||||
new_containing_block, page_is_empty_with_no_children,
|
||||
absolute_boxes, fixed_boxes, adjoining_margins, discard)
|
||||
if new_child:
|
||||
position_y = (
|
||||
new_child.border_box_y() + new_child.border_height())
|
||||
else:
|
||||
position_y = new_position_y
|
||||
|
||||
adjoining_margins = next_adjoining_margins
|
||||
if new_child:
|
||||
adjoining_margins.append(new_child.margin_bottom)
|
||||
|
||||
if new_child and new_child.clearance:
|
||||
position_y = new_child.border_box_y() + new_child.border_height()
|
||||
|
||||
skip_stack = None
|
||||
|
||||
if new_child is None:
|
||||
# Nothing fits in the remaining space of this page: break
|
||||
if page_break in ('avoid', 'avoid-page'):
|
||||
# TODO: fill the blank space at the bottom of the page
|
||||
result = find_earlier_page_break(
|
||||
new_children, absolute_boxes, fixed_boxes)
|
||||
if result:
|
||||
new_children, resume_at = result
|
||||
stop = True
|
||||
return (
|
||||
abort, stop, resume_at, position_y, adjoining_margins,
|
||||
next_page, new_children)
|
||||
else:
|
||||
# We did not find any page break opportunity
|
||||
if not page_is_empty:
|
||||
# The page has content *before* this block:
|
||||
# cancel the block and try to find a break
|
||||
# in the parent.
|
||||
abort = True
|
||||
return (
|
||||
abort, stop, resume_at, position_y, adjoining_margins,
|
||||
next_page, new_children)
|
||||
# else:
|
||||
# ignore this 'avoid' and break anyway.
|
||||
|
||||
if all(child.is_absolutely_positioned() for child in new_children):
|
||||
# This box has only rendered absolute children, keep them
|
||||
# for the next page. This is for example useful for list
|
||||
# markers.
|
||||
remove_placeholders(new_children, absolute_boxes, fixed_boxes)
|
||||
new_children = []
|
||||
|
||||
if new_children:
|
||||
resume_at = {index: None}
|
||||
stop = True
|
||||
else:
|
||||
# This was the first child of this box, cancel the box completly
|
||||
abort = True
|
||||
return (
|
||||
abort, stop, resume_at, position_y, adjoining_margins, next_page,
|
||||
new_children)
|
||||
|
||||
new_children.append(new_child)
|
||||
if resume_at is not None:
|
||||
resume_at = {index: resume_at}
|
||||
stop = True
|
||||
|
||||
return (
|
||||
abort, stop, resume_at, position_y, adjoining_margins, next_page,
|
||||
new_children)
|
||||
|
||||
|
||||
def block_container_layout(context, box, bottom_space, skip_stack,
|
||||
page_is_empty, absolute_boxes, fixed_boxes,
|
||||
adjoining_margins, discard):
|
||||
"""Set the ``box`` height."""
|
||||
# TODO: boxes.FlexBox is allowed here because flex_layout calls
|
||||
# block_container_layout, there's probably a better solution.
|
||||
assert isinstance(box, (boxes.BlockContainerBox, boxes.FlexBox))
|
||||
|
||||
if establishes_formatting_context(box):
|
||||
context.create_block_formatting_context()
|
||||
|
||||
is_start = skip_stack is None
|
||||
box.remove_decoration(start=not is_start, end=False)
|
||||
|
||||
discard |= box.style['continue'] == 'discard'
|
||||
draw_bottom_decoration = (
|
||||
discard or box.style['box_decoration_break'] == 'clone')
|
||||
|
||||
if adjoining_margins is None:
|
||||
adjoining_margins = []
|
||||
|
||||
if draw_bottom_decoration:
|
||||
bottom_space += (
|
||||
box.padding_bottom + box.border_bottom_width + box.margin_bottom)
|
||||
|
||||
adjoining_margins.append(box.margin_top)
|
||||
this_box_adjoining_margins = adjoining_margins
|
||||
|
||||
collapsing_with_children = not (
|
||||
box.border_top_width or box.padding_top or box.is_flex_item or
|
||||
establishes_formatting_context(box) or box.is_for_root_element)
|
||||
if collapsing_with_children:
|
||||
# Not counting margins in adjoining_margins, if any
|
||||
# (there are not padding or borders, see above)
|
||||
position_y = box.position_y
|
||||
else:
|
||||
box.position_y += collapse_margin(adjoining_margins) - box.margin_top
|
||||
adjoining_margins = []
|
||||
position_y = box.content_box_y()
|
||||
|
||||
position_x = box.content_box_x()
|
||||
|
||||
if box.style['position'] == 'relative':
|
||||
# New containing block, use a new absolute list
|
||||
absolute_boxes = []
|
||||
|
||||
new_children = []
|
||||
next_page = {'break': 'any', 'page': None}
|
||||
|
||||
last_in_flow_child = None
|
||||
|
||||
if is_start:
|
||||
skip = 0
|
||||
first_letter_style = getattr(box, 'first_letter_style', None)
|
||||
else:
|
||||
# TODO: handle multiple skip stacks
|
||||
(skip, skip_stack), = skip_stack.items()
|
||||
first_letter_style = None
|
||||
for index, child in enumerate(box.children[skip:], start=(skip or 0)):
|
||||
child.position_x = position_x
|
||||
child.position_y = position_y # doesn’t count adjoining_margins
|
||||
|
||||
if not child.is_in_normal_flow():
|
||||
abort = False
|
||||
stop, resume_at, out_of_flow_resume_at = _out_of_flow_layout(
|
||||
context, box, index, child, new_children, page_is_empty,
|
||||
absolute_boxes, fixed_boxes, adjoining_margins,
|
||||
bottom_space)
|
||||
if out_of_flow_resume_at:
|
||||
context.broken_out_of_flow.append(
|
||||
(child, box, out_of_flow_resume_at))
|
||||
|
||||
elif isinstance(child, boxes.LineBox):
|
||||
abort, stop, resume_at, position_y = _linebox_layout(
|
||||
context, box, index, child, new_children, page_is_empty,
|
||||
absolute_boxes, fixed_boxes, adjoining_margins, bottom_space,
|
||||
position_y, skip_stack, first_letter_style,
|
||||
draw_bottom_decoration)
|
||||
draw_bottom_decoration |= resume_at is None
|
||||
adjoining_margins = []
|
||||
|
||||
else:
|
||||
(abort, stop, resume_at, position_y, adjoining_margins,
|
||||
next_page, new_children) = _in_flow_layout(
|
||||
context, box, index, child, new_children, page_is_empty,
|
||||
absolute_boxes, fixed_boxes, adjoining_margins, bottom_space,
|
||||
position_y, skip_stack, first_letter_style,
|
||||
draw_bottom_decoration, collapsing_with_children, discard,
|
||||
next_page)
|
||||
skip_stack = None
|
||||
|
||||
if abort:
|
||||
page = child.page_values()[0]
|
||||
return None, None, {'break': 'any', 'page': page}, [], False
|
||||
elif stop:
|
||||
break
|
||||
|
||||
else:
|
||||
resume_at = None
|
||||
|
||||
box_is_fragmented = resume_at is not None
|
||||
if box.style['continue'] == 'discard':
|
||||
resume_at = None
|
||||
|
||||
if (box_is_fragmented and
|
||||
box.style['break_inside'] in ('avoid', 'avoid-page') and
|
||||
not page_is_empty):
|
||||
return (
|
||||
None, None, {'break': 'any', 'page': None}, [], False)
|
||||
|
||||
if collapsing_with_children:
|
||||
box.position_y += (
|
||||
collapse_margin(this_box_adjoining_margins) - box.margin_top)
|
||||
|
||||
last_in_flow_child = find_last_in_flow_child(new_children)
|
||||
collapsing_through = False
|
||||
if last_in_flow_child is None:
|
||||
collapsed_margin = collapse_margin(adjoining_margins)
|
||||
# Top and bottom margins of this box
|
||||
if (box.height in ('auto', 0) and
|
||||
get_clearance(context, box, collapsed_margin) is None and
|
||||
all(value == 0 for value in [
|
||||
box.min_height, box.border_top_width, box.padding_top,
|
||||
box.border_bottom_width, box.padding_bottom])):
|
||||
collapsing_through = True
|
||||
else:
|
||||
position_y += collapsed_margin
|
||||
adjoining_margins = []
|
||||
else:
|
||||
# Bottom margin of the last child and bottom margin of this box
|
||||
if box.height != 'auto':
|
||||
# Not adjoining (position_y is not used afterwards)
|
||||
adjoining_margins = []
|
||||
|
||||
if (box.border_bottom_width or
|
||||
box.padding_bottom or
|
||||
establishes_formatting_context(box) or
|
||||
box.is_for_root_element or
|
||||
box.is_table_wrapper):
|
||||
position_y += collapse_margin(adjoining_margins)
|
||||
adjoining_margins = []
|
||||
|
||||
# Add block ellipsis
|
||||
if box_is_fragmented and new_children:
|
||||
last_child = new_children[-1]
|
||||
if isinstance(last_child, boxes.LineBox):
|
||||
last_child.block_ellipsis = box.style['block_ellipsis']
|
||||
|
||||
new_box = box.copy_with_children(new_children)
|
||||
new_box.remove_decoration(
|
||||
start=not is_start, end=box_is_fragmented and not discard)
|
||||
|
||||
# TODO: See corner cases in
|
||||
# http://www.w3.org/TR/CSS21/visudet.html#normal-block
|
||||
# TODO: See float.float_layout
|
||||
if new_box.height == 'auto':
|
||||
if context.excluded_shapes and new_box.style['overflow'] != 'visible':
|
||||
max_float_position_y = max(
|
||||
float_box.position_y + float_box.margin_height()
|
||||
for float_box in context.excluded_shapes)
|
||||
position_y = max(max_float_position_y, position_y)
|
||||
new_box.height = position_y - new_box.content_box_y()
|
||||
|
||||
if new_box.style['position'] == 'relative':
|
||||
# New containing block, resolve the layout of the absolute descendants
|
||||
for absolute_box in absolute_boxes:
|
||||
absolute_layout(
|
||||
context, absolute_box, new_box, fixed_boxes, bottom_space,
|
||||
skip_stack=None)
|
||||
|
||||
for child in new_box.children:
|
||||
relative_positioning(child, (new_box.width, new_box.height))
|
||||
|
||||
if establishes_formatting_context(new_box):
|
||||
context.finish_block_formatting_context(new_box)
|
||||
|
||||
if discard or not box_is_fragmented:
|
||||
# After finish_block_formatting_context which may increment
|
||||
# new_box.height
|
||||
new_box.height = max(
|
||||
min(new_box.height, new_box.max_height), new_box.min_height)
|
||||
elif bottom_space > -float('inf'):
|
||||
# Make the box fill the blank space at the bottom of the page
|
||||
# https://www.w3.org/TR/css-break-3/#box-splitting
|
||||
new_box.height = (
|
||||
context.page_bottom - bottom_space - new_box.position_y -
|
||||
(new_box.margin_height() - new_box.height))
|
||||
if draw_bottom_decoration:
|
||||
new_box.height += (
|
||||
box.padding_bottom + box.border_bottom_width +
|
||||
box.margin_bottom)
|
||||
|
||||
if next_page['page'] is None:
|
||||
next_page['page'] = new_box.page_values()[1]
|
||||
|
||||
return new_box, resume_at, next_page, adjoining_margins, collapsing_through
|
||||
|
||||
|
||||
def collapse_margin(adjoining_margins):
|
||||
"""Get the amount of collapsed margin for a list of adjoining margins."""
|
||||
margins = [0] # add 0 to make sure that max/min don’t get an empty list
|
||||
margins.extend(adjoining_margins)
|
||||
positives = (m for m in margins if m >= 0)
|
||||
negatives = (m for m in margins if m <= 0)
|
||||
return max(positives) + min(negatives)
|
||||
|
||||
|
||||
def establishes_formatting_context(box):
|
||||
"""Return whether a box establishes a block formatting context.
|
||||
|
||||
See http://www.w3.org/TR/CSS2/visuren.html#block-formatting
|
||||
|
||||
"""
|
||||
return (
|
||||
box.is_floated()
|
||||
) or (
|
||||
box.is_absolutely_positioned()
|
||||
) or (
|
||||
# TODO: columns shouldn't be block boxes, this condition would then be
|
||||
# useless when this is fixed
|
||||
box.is_column
|
||||
) or (
|
||||
isinstance(box, boxes.BlockContainerBox) and
|
||||
not isinstance(box, boxes.BlockBox)
|
||||
) or (
|
||||
isinstance(box, boxes.BlockBox) and box.style['overflow'] != 'visible'
|
||||
) or (
|
||||
'flow-root' in box.style['display']
|
||||
)
|
||||
|
||||
|
||||
def block_level_page_break(sibling_before, sibling_after):
|
||||
"""Get the correct page break value between siblings.
|
||||
|
||||
Return the value of ``page-break-before`` or ``page-break-after`` that
|
||||
"wins" for boxes that meet at the margin between two sibling boxes.
|
||||
|
||||
For boxes before the margin, the 'page-break-after' value is considered;
|
||||
for boxes after the margin the 'page-break-before' value is considered.
|
||||
|
||||
* 'avoid' takes priority over 'auto'
|
||||
* 'page' takes priority over 'avoid' or 'auto'
|
||||
* 'left' or 'right' take priority over 'always', 'avoid' or 'auto'
|
||||
* Among 'left' and 'right', later values in the tree take priority.
|
||||
|
||||
See http://dev.w3.org/csswg/css3-page/#allowed-pg-brk
|
||||
|
||||
"""
|
||||
values = []
|
||||
# https://drafts.csswg.org/css-break-3/#possible-breaks
|
||||
block_parallel_box_types = (
|
||||
boxes.BlockLevelBox, boxes.TableRowGroupBox, boxes.TableRowBox)
|
||||
|
||||
box = sibling_before
|
||||
while isinstance(box, block_parallel_box_types):
|
||||
values.append(box.style['break_after'])
|
||||
if not (isinstance(box, boxes.ParentBox) and box.children):
|
||||
break
|
||||
box = box.children[-1]
|
||||
values.reverse() # Have them in tree order
|
||||
|
||||
box = sibling_after
|
||||
while isinstance(box, block_parallel_box_types):
|
||||
values.append(box.style['break_before'])
|
||||
if not (isinstance(box, boxes.ParentBox) and box.children):
|
||||
break
|
||||
box = box.children[0]
|
||||
|
||||
result = 'auto'
|
||||
for value in values:
|
||||
if value in ('left', 'right', 'recto', 'verso') or (value, result) in (
|
||||
('page', 'auto'),
|
||||
('page', 'avoid'),
|
||||
('avoid', 'auto'),
|
||||
('page', 'avoid-page'),
|
||||
('avoid-page', 'auto')):
|
||||
result = value
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def block_level_page_name(sibling_before, sibling_after):
|
||||
"""Return the next page name when siblings don't have the same names."""
|
||||
before_page = sibling_before.page_values()[1]
|
||||
after_page = sibling_after.page_values()[0]
|
||||
if before_page != after_page:
|
||||
return after_page
|
||||
|
||||
|
||||
def find_earlier_page_break(children, absolute_boxes, fixed_boxes):
|
||||
"""Find the last possible page break in ``children``.
|
||||
|
||||
Because of a `page-break-before: avoid` or a `page-break-after: avoid` we
|
||||
need to find an earlier page break opportunity inside `children`.
|
||||
|
||||
Absolute or fixed placeholders removed from children should also be
|
||||
removed from `absolute_boxes` or `fixed_boxes`.
|
||||
|
||||
Return (new_children, resume_at).
|
||||
|
||||
"""
|
||||
if children and isinstance(children[0], boxes.LineBox):
|
||||
# Normally `orphans` and `widows` apply to the block container, but
|
||||
# line boxes inherit them.
|
||||
orphans = children[0].style['orphans']
|
||||
widows = children[0].style['widows']
|
||||
index = len(children) - widows # how many lines we keep
|
||||
if index < orphans:
|
||||
return None
|
||||
new_children = children[:index]
|
||||
resume_at = {0: new_children[-1].resume_at}
|
||||
remove_placeholders(children[index:], absolute_boxes, fixed_boxes)
|
||||
return new_children, resume_at
|
||||
|
||||
previous_in_flow = None
|
||||
for index, child in reversed_enumerate(children):
|
||||
if isinstance(child, boxes.TableRowGroupBox) and (
|
||||
child.is_header or child.is_footer):
|
||||
# We don’t want to break pages before table headers or footers.
|
||||
continue
|
||||
elif child.is_column:
|
||||
# We don’t want to break pages between columns.
|
||||
continue
|
||||
|
||||
if child.is_in_normal_flow():
|
||||
if previous_in_flow is not None and (
|
||||
block_level_page_break(child, previous_in_flow) not in
|
||||
('avoid', 'avoid-page')):
|
||||
index += 1 # break after child
|
||||
new_children = children[:index]
|
||||
# Get the index in the original parent
|
||||
resume_at = {children[index].index: None}
|
||||
break
|
||||
previous_in_flow = child
|
||||
|
||||
if child.is_in_normal_flow() and (
|
||||
child.style['break_inside'] not in ('avoid', 'avoid-page')):
|
||||
breakable_box_types = (
|
||||
boxes.BlockBox, boxes.TableBox, boxes.TableRowGroupBox)
|
||||
if isinstance(child, breakable_box_types):
|
||||
result = find_earlier_page_break(
|
||||
child.children, absolute_boxes, fixed_boxes)
|
||||
if result:
|
||||
new_grand_children, resume_at = result
|
||||
new_child = child.copy_with_children(new_grand_children)
|
||||
new_children = list(children[:index]) + [new_child]
|
||||
|
||||
# Re-add footer at the end of split table
|
||||
# TODO: fix table height and footer position
|
||||
if isinstance(child, boxes.TableRowGroupBox):
|
||||
for next_child in children[index:]:
|
||||
if next_child.is_footer:
|
||||
new_children.append(next_child)
|
||||
|
||||
# Index in the original parent
|
||||
resume_at = {new_child.index: resume_at}
|
||||
index += 1 # Remove placeholders after child
|
||||
break
|
||||
else:
|
||||
return None
|
||||
|
||||
# TODO: don’t remove absolute and fixed placeholders found in table footers
|
||||
remove_placeholders(children[index:], absolute_boxes, fixed_boxes)
|
||||
return new_children, resume_at
|
||||
|
||||
|
||||
def find_last_in_flow_child(children):
|
||||
"""Find and return the last in-flow child of given ``children``."""
|
||||
for child in reversed(children):
|
||||
if child.is_in_normal_flow():
|
||||
return child
|
||||
|
||||
|
||||
def reversed_enumerate(seq):
|
||||
"""Like reversed(list(enumerate(seq))) without copying the whole seq."""
|
||||
return zip(reversed(range(len(seq))), reversed(seq))
|
||||
|
||||
|
||||
def remove_placeholders(box_list, absolute_boxes, fixed_boxes):
|
||||
"""Remove placeholders from absolute and fixed lists.
|
||||
|
||||
For boxes that have been removed in find_earlier_page_break(), remove the
|
||||
matching placeholders in absolute_boxes and fixed_boxes.
|
||||
|
||||
"""
|
||||
for box in box_list:
|
||||
if isinstance(box, boxes.ParentBox):
|
||||
remove_placeholders(box.children, absolute_boxes, fixed_boxes)
|
||||
if box.style['position'] == 'absolute' and box in absolute_boxes:
|
||||
# box is not in absolute_boxes if its parent has position: relative
|
||||
absolute_boxes.remove(box)
|
||||
elif box.style['position'] == 'fixed':
|
||||
fixed_boxes.remove(box)
|
||||
306
venv/Lib/site-packages/weasyprint/layout/column.py
Normal file
306
venv/Lib/site-packages/weasyprint/layout/column.py
Normal file
@@ -0,0 +1,306 @@
|
||||
"""
|
||||
weasyprint.layout.columns
|
||||
-------------------------
|
||||
|
||||
Layout for columns.
|
||||
|
||||
"""
|
||||
|
||||
from math import floor
|
||||
|
||||
from .absolute import absolute_layout
|
||||
from .percent import resolve_percentages
|
||||
|
||||
|
||||
def columns_layout(context, box, bottom_space, skip_stack, containing_block,
|
||||
page_is_empty, absolute_boxes, fixed_boxes,
|
||||
adjoining_margins):
|
||||
"""Lay out a multi-column ``box``."""
|
||||
from .block import (
|
||||
block_box_layout, block_level_layout, block_level_width,
|
||||
collapse_margin)
|
||||
|
||||
# Implementation of the multi-column pseudo-algorithm:
|
||||
# https://www.w3.org/TR/css3-multicol/#pseudo-algorithm
|
||||
width = None
|
||||
style = box.style
|
||||
original_bottom_space = bottom_space
|
||||
|
||||
if box.style['position'] == 'relative':
|
||||
# New containing block, use a new absolute list
|
||||
absolute_boxes = []
|
||||
|
||||
box = box.copy_with_children(box.children)
|
||||
box.position_y += collapse_margin(adjoining_margins) - box.margin_top
|
||||
|
||||
height = box.style['height']
|
||||
if height != 'auto' and height.unit != '%':
|
||||
assert height.unit == 'px'
|
||||
known_height = True
|
||||
bottom_space = max(
|
||||
bottom_space,
|
||||
context.page_bottom - box.content_box_y() - height.value)
|
||||
else:
|
||||
known_height = False
|
||||
|
||||
# TODO: the available width can be unknown if the containing block needs
|
||||
# the size of this block to know its own size.
|
||||
block_level_width(box, containing_block)
|
||||
available_width = box.width
|
||||
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, int(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'], int(floor(
|
||||
(available_width + style['column_gap']) /
|
||||
(style['column_width'] + style['column_gap']))))
|
||||
width = (
|
||||
(available_width + style['column_gap']) / count -
|
||||
style['column_gap'])
|
||||
|
||||
def create_column_box(children):
|
||||
column_box = box.anonymous_from(box, children=children)
|
||||
resolve_percentages(column_box, containing_block)
|
||||
column_box.is_column = True
|
||||
column_box.width = width
|
||||
column_box.position_x = box.content_box_x()
|
||||
column_box.position_y = box.content_box_y()
|
||||
return column_box
|
||||
|
||||
# Handle column-span property.
|
||||
# We want to get the following structure:
|
||||
# columns_and_blocks = [
|
||||
# [column_child_1, column_child_2],
|
||||
# spanning_block,
|
||||
# …
|
||||
# ]
|
||||
columns_and_blocks = []
|
||||
column_children = []
|
||||
for child in box.children:
|
||||
if child.style['column_span'] == 'all':
|
||||
if column_children:
|
||||
columns_and_blocks.append(column_children)
|
||||
columns_and_blocks.append(child.copy())
|
||||
column_children = []
|
||||
continue
|
||||
column_children.append(child.copy())
|
||||
if column_children:
|
||||
columns_and_blocks.append(column_children)
|
||||
|
||||
if not box.children:
|
||||
next_page = {'break': 'any', 'page': None}
|
||||
skip_stack = None
|
||||
|
||||
# Balance.
|
||||
#
|
||||
# The current algorithm starts from the ideal height (the total height
|
||||
# divided by the number of columns). We then iterate until the last column
|
||||
# is not the highest one. At the end of each loop, we add the minimal
|
||||
# height needed to make one direct child at the top of one column go to the
|
||||
# end of the previous column.
|
||||
#
|
||||
# We rely on a real rendering for each loop, and with a stupid algorithm
|
||||
# like this it can last minutes…
|
||||
|
||||
adjoining_margins = []
|
||||
current_position_y = box.content_box_y()
|
||||
new_children = []
|
||||
for column_children_or_block in columns_and_blocks:
|
||||
if not isinstance(column_children_or_block, list):
|
||||
# We get a spanning block, we display it like other blocks.
|
||||
block = column_children_or_block
|
||||
resolve_percentages(block, containing_block)
|
||||
block.position_x = box.content_box_x()
|
||||
block.position_y = current_position_y
|
||||
new_child, _, _, adjoining_margins, _ = block_level_layout(
|
||||
context, block, original_bottom_space, skip_stack,
|
||||
containing_block, page_is_empty, absolute_boxes, fixed_boxes,
|
||||
adjoining_margins, discard=False)
|
||||
new_children.append(new_child)
|
||||
current_position_y = (
|
||||
new_child.border_height() + new_child.border_box_y())
|
||||
adjoining_margins.append(new_child.margin_bottom)
|
||||
continue
|
||||
|
||||
excluded_shapes = context.excluded_shapes[:]
|
||||
|
||||
# We have a list of children that we have to balance between columns.
|
||||
column_children = column_children_or_block
|
||||
|
||||
# Find the total height of the content
|
||||
current_position_y += collapse_margin(adjoining_margins)
|
||||
adjoining_margins = []
|
||||
column_box = create_column_box(column_children)
|
||||
column_box.position_y = current_position_y
|
||||
new_child, _, _, _, _ = block_box_layout(
|
||||
context, column_box, -float('inf'), skip_stack, containing_block,
|
||||
page_is_empty, [], [], [], discard=False)
|
||||
height = new_child.margin_height()
|
||||
if style['column_fill'] == 'balance':
|
||||
height /= count
|
||||
|
||||
# Try to render columns until the content fits, increase the column
|
||||
# height step by step.
|
||||
column_skip_stack = skip_stack
|
||||
lost_space = float('inf')
|
||||
while True:
|
||||
# Remove extra excluded shapes introduced during previous loop
|
||||
new_excluded_shapes = (
|
||||
len(context.excluded_shapes) - len(excluded_shapes))
|
||||
for i in range(new_excluded_shapes):
|
||||
context.excluded_shapes.pop()
|
||||
|
||||
for i in range(count):
|
||||
# Render the column
|
||||
new_box, resume_at, next_page, _, _ = block_box_layout(
|
||||
context, column_box,
|
||||
context.page_bottom - current_position_y - height,
|
||||
column_skip_stack, containing_block, page_is_empty,
|
||||
[], [], [], discard=False)
|
||||
if new_box is None:
|
||||
# We didn't render anything. Give up and use the max
|
||||
# content height.
|
||||
height *= count
|
||||
continue
|
||||
column_skip_stack = resume_at
|
||||
|
||||
in_flow_children = [
|
||||
child for child in new_box.children
|
||||
if child.is_in_normal_flow()]
|
||||
|
||||
if in_flow_children:
|
||||
# Get the empty space at the bottom of the column box
|
||||
empty_space = height - (
|
||||
in_flow_children[-1].position_y - box.content_box_y() +
|
||||
in_flow_children[-1].margin_height())
|
||||
|
||||
# Get the minimum size needed to render the next box
|
||||
next_box, _, _, _, _ = block_box_layout(
|
||||
context, column_box,
|
||||
context.page_bottom - box.content_box_y(),
|
||||
column_skip_stack, containing_block, True, [], [], [],
|
||||
discard=False)
|
||||
for child in next_box.children:
|
||||
if child.is_in_normal_flow():
|
||||
next_box_size = child.margin_height()
|
||||
break
|
||||
else:
|
||||
empty_space = next_box_size = 0
|
||||
|
||||
# Append the size needed to render the next box in this
|
||||
# column.
|
||||
#
|
||||
# The next box size may be smaller than the empty space, for
|
||||
# example when the next box can't be separated from its own
|
||||
# next box. In this case we don't try to find the real value
|
||||
# and let the workaround below fix this for us.
|
||||
#
|
||||
# We also want to avoid very small values that may have been
|
||||
# introduced by rounding errors. As the workaround below at
|
||||
# least adds 1 pixel for each loop, we can ignore lost spaces
|
||||
# lower than 1px.
|
||||
if next_box_size - empty_space > 1:
|
||||
lost_space = min(lost_space, next_box_size - empty_space)
|
||||
|
||||
# Stop if we already rendered the whole content
|
||||
if resume_at is None:
|
||||
break
|
||||
|
||||
if column_skip_stack is None:
|
||||
# We rendered the whole content, stop
|
||||
break
|
||||
else:
|
||||
if lost_space == float('inf'):
|
||||
# We didn't find the extra size needed to render a child in
|
||||
# the previous column, increase height by the minimal
|
||||
# value.
|
||||
height += 1
|
||||
else:
|
||||
# Increase the columns heights and render them once again
|
||||
height += lost_space
|
||||
column_skip_stack = skip_stack
|
||||
|
||||
# TODO: check box.style['max']-height
|
||||
bottom_space = max(
|
||||
bottom_space, context.page_bottom - current_position_y - height)
|
||||
|
||||
# Replace the current box children with columns
|
||||
i = 0
|
||||
max_column_height = 0
|
||||
columns = []
|
||||
while True:
|
||||
if i == count - 1:
|
||||
bottom_space = original_bottom_space
|
||||
column_box = create_column_box(column_children)
|
||||
column_box.position_y = current_position_y
|
||||
if style['direction'] == 'rtl':
|
||||
column_box.position_x += (
|
||||
box.width - (i + 1) * width - i * style['column_gap'])
|
||||
else:
|
||||
column_box.position_x += i * (width + style['column_gap'])
|
||||
new_child, column_skip_stack, column_next_page, _, _ = (
|
||||
block_box_layout(
|
||||
context, column_box, bottom_space, skip_stack,
|
||||
containing_block, page_is_empty, absolute_boxes,
|
||||
fixed_boxes, None, discard=False))
|
||||
if new_child is None:
|
||||
break
|
||||
next_page = column_next_page
|
||||
skip_stack = column_skip_stack
|
||||
columns.append(new_child)
|
||||
max_column_height = max(
|
||||
max_column_height, new_child.margin_height())
|
||||
if skip_stack is None:
|
||||
break
|
||||
i += 1
|
||||
if i == count and not known_height:
|
||||
# [If] a declaration that constrains the column height
|
||||
# (e.g., using height or max-height). In this case,
|
||||
# additional column boxes are created in the inline
|
||||
# direction.
|
||||
break
|
||||
|
||||
current_position_y += max_column_height
|
||||
for column in columns:
|
||||
column.height = max_column_height
|
||||
new_children.append(column)
|
||||
|
||||
if box.children and not new_children:
|
||||
# The box has children but none can be drawn, let's skip the whole box
|
||||
return None, (0, None), {'break': 'any', 'page': None}, [], False
|
||||
|
||||
# Set the height of box and the columns
|
||||
box.children = new_children
|
||||
current_position_y += collapse_margin(adjoining_margins)
|
||||
height = current_position_y - box.content_box_y()
|
||||
if box.height == 'auto':
|
||||
box.height = height
|
||||
height_difference = 0
|
||||
else:
|
||||
height_difference = box.height - height
|
||||
if box.min_height != 'auto' and box.min_height > box.height:
|
||||
height_difference += box.min_height - box.height
|
||||
box.height = box.min_height
|
||||
for child in new_children[::-1]:
|
||||
if child.is_column:
|
||||
child.height += height_difference
|
||||
else:
|
||||
break
|
||||
|
||||
if box.style['position'] == 'relative':
|
||||
# New containing block, resolve the layout of the absolute descendants
|
||||
for absolute_box in absolute_boxes:
|
||||
absolute_layout(
|
||||
context, absolute_box, box, fixed_boxes, bottom_space,
|
||||
skip_stack=None)
|
||||
|
||||
return box, skip_stack, next_page, [], False
|
||||
894
venv/Lib/site-packages/weasyprint/layout/flex.py
Normal file
894
venv/Lib/site-packages/weasyprint/layout/flex.py
Normal file
@@ -0,0 +1,894 @@
|
||||
"""
|
||||
weasyprint.layout.flex
|
||||
------------------------
|
||||
|
||||
Layout for flex containers and flex-items.
|
||||
|
||||
"""
|
||||
|
||||
import sys
|
||||
from math import log10
|
||||
|
||||
from ..css.properties import Dimension
|
||||
from ..formatting_structure import boxes
|
||||
from .percent import resolve_one_percentage, resolve_percentages
|
||||
from .preferred import max_content_width, min_content_width
|
||||
from .table import find_in_flow_baseline
|
||||
|
||||
|
||||
class FlexLine(list):
|
||||
pass
|
||||
|
||||
|
||||
def flex_layout(context, box, bottom_space, skip_stack, containing_block,
|
||||
page_is_empty, absolute_boxes, fixed_boxes):
|
||||
from . import block, preferred
|
||||
|
||||
context.create_block_formatting_context()
|
||||
resume_at = None
|
||||
|
||||
# Step 1 is done in formatting_structure.boxes
|
||||
# Step 2
|
||||
if box.style['flex_direction'].startswith('row'):
|
||||
axis, cross = 'width', 'height'
|
||||
else:
|
||||
axis, cross = 'height', 'width'
|
||||
|
||||
margin_left = 0 if box.margin_left == 'auto' else box.margin_left
|
||||
margin_right = 0 if box.margin_right == 'auto' else box.margin_right
|
||||
margin_top = 0 if box.margin_top == 'auto' else box.margin_top
|
||||
margin_bottom = 0 if box.margin_bottom == 'auto' else box.margin_bottom
|
||||
|
||||
if getattr(box, axis) != 'auto':
|
||||
available_main_space = getattr(box, axis)
|
||||
else:
|
||||
if axis == 'width':
|
||||
available_main_space = (
|
||||
containing_block.width -
|
||||
margin_left - margin_right -
|
||||
box.padding_left - box.padding_right -
|
||||
box.border_left_width - box.border_right_width)
|
||||
else:
|
||||
main_space = context.page_bottom - bottom_space - box.position_y
|
||||
if containing_block.height != 'auto':
|
||||
if isinstance(containing_block.height, Dimension):
|
||||
assert containing_block.height.unit == 'px'
|
||||
main_space = min(main_space, containing_block.height.value)
|
||||
else:
|
||||
main_space = min(main_space, containing_block.height)
|
||||
available_main_space = (
|
||||
main_space -
|
||||
margin_top - margin_bottom -
|
||||
box.padding_top - box.padding_bottom -
|
||||
box.border_top_width - box.border_bottom_width)
|
||||
|
||||
if getattr(box, cross) != 'auto':
|
||||
available_cross_space = getattr(box, cross)
|
||||
else:
|
||||
if cross == 'height':
|
||||
main_space = (
|
||||
context.page_bottom - bottom_space - box.content_box_y())
|
||||
if containing_block.height != 'auto':
|
||||
if isinstance(containing_block.height, Dimension):
|
||||
assert containing_block.height.unit == 'px'
|
||||
main_space = min(main_space, containing_block.height.value)
|
||||
else:
|
||||
main_space = min(main_space, containing_block.height)
|
||||
available_cross_space = (
|
||||
main_space -
|
||||
margin_top - margin_bottom -
|
||||
box.padding_top - box.padding_bottom -
|
||||
box.border_top_width - box.border_bottom_width)
|
||||
else:
|
||||
available_cross_space = (
|
||||
containing_block.width -
|
||||
margin_left - margin_right -
|
||||
box.padding_left - box.padding_right -
|
||||
box.border_left_width - box.border_right_width)
|
||||
|
||||
# Step 3
|
||||
children = box.children
|
||||
parent_box = box.copy_with_children(children)
|
||||
resolve_percentages(parent_box, containing_block)
|
||||
# TODO: removing auto margins is OK for this step, but margins should be
|
||||
# calculated later.
|
||||
if parent_box.margin_top == 'auto':
|
||||
box.margin_top = parent_box.margin_top = 0
|
||||
if parent_box.margin_bottom == 'auto':
|
||||
box.margin_bottom = parent_box.margin_bottom = 0
|
||||
if parent_box.margin_left == 'auto':
|
||||
box.margin_left = parent_box.margin_left = 0
|
||||
if parent_box.margin_right == 'auto':
|
||||
box.margin_right = parent_box.margin_right = 0
|
||||
if isinstance(parent_box, boxes.FlexBox):
|
||||
block.block_level_width(parent_box, containing_block)
|
||||
else:
|
||||
parent_box.width = preferred.flex_max_content_width(
|
||||
context, parent_box)
|
||||
original_skip_stack = skip_stack
|
||||
if skip_stack is not None:
|
||||
# TODO: handle multiple skip stacks
|
||||
(skip, skip_stack), = skip_stack.items()
|
||||
if box.style['flex_direction'].endswith('-reverse'):
|
||||
children = children[:skip + 1]
|
||||
else:
|
||||
children = children[skip:]
|
||||
skip_stack = skip_stack
|
||||
else:
|
||||
skip_stack = None
|
||||
child_skip_stack = skip_stack
|
||||
for child in children:
|
||||
if not child.is_flex_item:
|
||||
continue
|
||||
|
||||
# See https://www.w3.org/TR/css-flexbox-1/#min-size-auto
|
||||
if child.style['overflow'] == 'visible':
|
||||
main_flex_direction = axis
|
||||
else:
|
||||
main_flex_direction = None
|
||||
resolve_percentages(child, containing_block, main_flex_direction)
|
||||
child.position_x = parent_box.content_box_x()
|
||||
child.position_y = parent_box.content_box_y()
|
||||
if child.min_width == 'auto':
|
||||
specified_size = (
|
||||
child.width if child.width != 'auto' else float('inf'))
|
||||
if isinstance(child, boxes.ParentBox):
|
||||
new_child = child.copy_with_children(child.children)
|
||||
else:
|
||||
new_child = child.copy()
|
||||
new_child.style = child.style.copy()
|
||||
new_child.style['width'] = 'auto'
|
||||
new_child.style['min_width'] = Dimension(0, 'px')
|
||||
new_child.style['max_width'] = Dimension(float('inf'), 'px')
|
||||
content_size = min_content_width(context, new_child, outer=False)
|
||||
child.min_width = min(specified_size, content_size)
|
||||
elif child.min_height == 'auto':
|
||||
# TODO: find a way to get min-content-height
|
||||
specified_size = (
|
||||
child.height if child.height != 'auto' else float('inf'))
|
||||
if isinstance(child, boxes.ParentBox):
|
||||
new_child = child.copy_with_children(child.children)
|
||||
else:
|
||||
new_child = child.copy()
|
||||
new_child.style = child.style.copy()
|
||||
new_child.style['height'] = 'auto'
|
||||
new_child.style['min_height'] = Dimension(0, 'px')
|
||||
new_child.style['max_height'] = Dimension(float('inf'), 'px')
|
||||
new_child = block.block_level_layout(
|
||||
context, new_child, -float('inf'), child_skip_stack,
|
||||
parent_box, page_is_empty, [], [], [], False)[0]
|
||||
content_size = new_child.height
|
||||
child.min_height = min(specified_size, content_size)
|
||||
|
||||
child.style = child.style.copy()
|
||||
|
||||
if child.style['flex_basis'] == 'content':
|
||||
flex_basis = child.flex_basis = 'content'
|
||||
else:
|
||||
resolve_one_percentage(child, 'flex_basis', available_main_space)
|
||||
flex_basis = child.flex_basis
|
||||
|
||||
# "If a value would resolve to auto for width, it instead resolves
|
||||
# to content for flex-basis." Let's do this for height too.
|
||||
# See https://www.w3.org/TR/css-flexbox-1/#propdef-flex-basis
|
||||
resolve_one_percentage(child, axis, available_main_space)
|
||||
if flex_basis == 'auto':
|
||||
if child.style[axis] == 'auto':
|
||||
flex_basis = 'content'
|
||||
else:
|
||||
if axis == 'width':
|
||||
flex_basis = child.border_width()
|
||||
if child.margin_left != 'auto':
|
||||
flex_basis += child.margin_left
|
||||
if child.margin_right != 'auto':
|
||||
flex_basis += child.margin_right
|
||||
else:
|
||||
flex_basis = child.border_height()
|
||||
if child.margin_top != 'auto':
|
||||
flex_basis += child.margin_top
|
||||
if child.margin_bottom != 'auto':
|
||||
flex_basis += child.margin_bottom
|
||||
|
||||
# Step 3.A
|
||||
if flex_basis != 'content':
|
||||
child.flex_base_size = flex_basis
|
||||
|
||||
# TODO: Step 3.B
|
||||
# TODO: Step 3.C
|
||||
|
||||
# Step 3.D is useless, as we never have infinite sizes on paged media
|
||||
|
||||
# Step 3.E
|
||||
else:
|
||||
child.style[axis] = 'max-content'
|
||||
|
||||
# TODO: don't set style value, support *-content values instead
|
||||
if child.style[axis] == 'max-content':
|
||||
child.style[axis] = 'auto'
|
||||
if axis == 'width':
|
||||
child.flex_base_size = max_content_width(context, child)
|
||||
else:
|
||||
if isinstance(child, boxes.ParentBox):
|
||||
new_child = child.copy_with_children(child.children)
|
||||
else:
|
||||
new_child = child.copy()
|
||||
new_child.width = float('inf')
|
||||
new_child = block.block_level_layout(
|
||||
context, new_child, -float('inf'), child_skip_stack,
|
||||
parent_box, page_is_empty, absolute_boxes, fixed_boxes,
|
||||
adjoining_margins=[], discard=False)[0]
|
||||
child.flex_base_size = new_child.margin_height()
|
||||
elif child.style[axis] == 'min-content':
|
||||
child.style[axis] = 'auto'
|
||||
if axis == 'width':
|
||||
child.flex_base_size = min_content_width(context, child)
|
||||
else:
|
||||
if isinstance(child, boxes.ParentBox):
|
||||
new_child = child.copy_with_children(child.children)
|
||||
else:
|
||||
new_child = child.copy()
|
||||
new_child.width = 0
|
||||
new_child = block.block_level_layout(
|
||||
context, new_child, -float('inf'), child_skip_stack,
|
||||
parent_box, page_is_empty, absolute_boxes, fixed_boxes,
|
||||
adjoining_margins=[], discard=False)[0]
|
||||
child.flex_base_size = new_child.margin_height()
|
||||
else:
|
||||
assert child.style[axis].unit == 'px'
|
||||
# TODO: should we add padding, borders and margins?
|
||||
child.flex_base_size = child.style[axis].value
|
||||
|
||||
child.hypothetical_main_size = max(
|
||||
getattr(child, f'min_{axis}'), min(
|
||||
child.flex_base_size, getattr(child, f'max_{axis}')))
|
||||
|
||||
# Skip stack is only for the first child
|
||||
child_skip_stack = None
|
||||
|
||||
# Step 4
|
||||
# TODO: the whole step has to be fixed
|
||||
if axis == 'width':
|
||||
block.block_level_width(box, containing_block)
|
||||
else:
|
||||
if box.style['height'] != 'auto':
|
||||
box.height = box.style['height'].value
|
||||
else:
|
||||
box.height = 0
|
||||
for i, child in enumerate(children):
|
||||
if not child.is_flex_item:
|
||||
continue
|
||||
child_height = (
|
||||
child.hypothetical_main_size +
|
||||
child.border_top_width + child.border_bottom_width +
|
||||
child.padding_top + child.padding_bottom)
|
||||
if getattr(box, axis) == 'auto' and (
|
||||
child_height + box.height > available_main_space):
|
||||
resume_at = {i: None}
|
||||
children = children[:i + 1]
|
||||
break
|
||||
box.height += child_height
|
||||
|
||||
# Step 5
|
||||
flex_lines = []
|
||||
|
||||
line = []
|
||||
line_size = 0
|
||||
axis_size = getattr(box, axis)
|
||||
for i, child in enumerate(
|
||||
sorted(children, key=lambda item: item.style['order'])):
|
||||
if not child.is_flex_item:
|
||||
continue
|
||||
line_size += child.hypothetical_main_size
|
||||
if box.style['flex_wrap'] != 'nowrap' and line_size > axis_size:
|
||||
if line:
|
||||
flex_lines.append(FlexLine(line))
|
||||
line = [(i, child)]
|
||||
line_size = child.hypothetical_main_size
|
||||
else:
|
||||
line.append((i, child))
|
||||
flex_lines.append(FlexLine(line))
|
||||
line = []
|
||||
line_size = 0
|
||||
else:
|
||||
line.append((i, child))
|
||||
if line:
|
||||
flex_lines.append(FlexLine(line))
|
||||
|
||||
# TODO: handle *-reverse using the terminology from the specification
|
||||
if box.style['flex_wrap'] == 'wrap-reverse':
|
||||
flex_lines.reverse()
|
||||
if box.style['flex_direction'].endswith('-reverse'):
|
||||
for line in flex_lines:
|
||||
line.reverse()
|
||||
|
||||
# Step 6
|
||||
# See https://www.w3.org/TR/css-flexbox-1/#resolve-flexible-lengths
|
||||
for line in flex_lines:
|
||||
# Step 6 - 9.7.1
|
||||
hypothetical_main_size = sum(
|
||||
child.hypothetical_main_size for i, child in line)
|
||||
if hypothetical_main_size < available_main_space:
|
||||
flex_factor_type = 'grow'
|
||||
else:
|
||||
flex_factor_type = 'shrink'
|
||||
|
||||
# Step 6 - 9.7.2
|
||||
for i, child in line:
|
||||
if flex_factor_type == 'grow':
|
||||
child.flex_factor = child.style['flex_grow']
|
||||
else:
|
||||
child.flex_factor = child.style['flex_shrink']
|
||||
if (child.flex_factor == 0 or
|
||||
(flex_factor_type == 'grow' and
|
||||
child.flex_base_size > child.hypothetical_main_size) or
|
||||
(flex_factor_type == 'shrink' and
|
||||
child.flex_base_size < child.hypothetical_main_size)):
|
||||
child.target_main_size = child.hypothetical_main_size
|
||||
child.frozen = True
|
||||
else:
|
||||
child.frozen = False
|
||||
|
||||
# Step 6 - 9.7.3
|
||||
initial_free_space = available_main_space
|
||||
for i, child in line:
|
||||
if child.frozen:
|
||||
initial_free_space -= child.target_main_size
|
||||
else:
|
||||
initial_free_space -= child.flex_base_size
|
||||
|
||||
# Step 6 - 9.7.4
|
||||
while not all(child.frozen for i, child in line):
|
||||
unfrozen_factor_sum = 0
|
||||
remaining_free_space = available_main_space
|
||||
|
||||
# Step 6 - 9.7.4.b
|
||||
for i, child in line:
|
||||
if child.frozen:
|
||||
remaining_free_space -= child.target_main_size
|
||||
else:
|
||||
remaining_free_space -= child.flex_base_size
|
||||
unfrozen_factor_sum += child.flex_factor
|
||||
|
||||
if unfrozen_factor_sum < 1:
|
||||
initial_free_space *= unfrozen_factor_sum
|
||||
|
||||
if initial_free_space == float('inf'):
|
||||
initial_free_space = sys.maxsize
|
||||
if remaining_free_space == float('inf'):
|
||||
remaining_free_space = sys.maxsize
|
||||
|
||||
initial_magnitude = (
|
||||
int(log10(initial_free_space)) if initial_free_space > 0
|
||||
else -float('inf'))
|
||||
remaining_magnitude = (
|
||||
int(log10(remaining_free_space)) if remaining_free_space > 0
|
||||
else -float('inf'))
|
||||
if initial_magnitude < remaining_magnitude:
|
||||
remaining_free_space = initial_free_space
|
||||
|
||||
# Step 6 - 9.7.4.c
|
||||
if remaining_free_space == 0:
|
||||
# "Do nothing", but we at least set the flex_base_size as
|
||||
# target_main_size for next step.
|
||||
for i, child in line:
|
||||
if not child.frozen:
|
||||
child.target_main_size = child.flex_base_size
|
||||
else:
|
||||
scaled_flex_shrink_factors_sum = 0
|
||||
flex_grow_factors_sum = 0
|
||||
for i, child in line:
|
||||
if not child.frozen:
|
||||
child.scaled_flex_shrink_factor = (
|
||||
child.flex_base_size * child.style['flex_shrink'])
|
||||
scaled_flex_shrink_factors_sum += (
|
||||
child.scaled_flex_shrink_factor)
|
||||
flex_grow_factors_sum += child.style['flex_grow']
|
||||
for i, child in line:
|
||||
if not child.frozen:
|
||||
if flex_factor_type == 'grow':
|
||||
ratio = (
|
||||
child.style['flex_grow'] /
|
||||
flex_grow_factors_sum)
|
||||
child.target_main_size = (
|
||||
child.flex_base_size +
|
||||
remaining_free_space * ratio)
|
||||
elif flex_factor_type == 'shrink':
|
||||
if scaled_flex_shrink_factors_sum == 0:
|
||||
child.target_main_size = child.flex_base_size
|
||||
else:
|
||||
ratio = (
|
||||
child.scaled_flex_shrink_factor /
|
||||
scaled_flex_shrink_factors_sum)
|
||||
child.target_main_size = (
|
||||
child.flex_base_size +
|
||||
remaining_free_space * ratio)
|
||||
|
||||
# Step 6 - 9.7.4.d
|
||||
# TODO: First part of this step is useless until 3.E is correct
|
||||
for i, child in line:
|
||||
child.adjustment = 0
|
||||
if not child.frozen and child.target_main_size < 0:
|
||||
child.adjustment = -child.target_main_size
|
||||
child.target_main_size = 0
|
||||
|
||||
# Step 6 - 9.7.4.e
|
||||
adjustments = sum(child.adjustment for i, child in line)
|
||||
for i, child in line:
|
||||
if adjustments == 0:
|
||||
child.frozen = True
|
||||
elif adjustments > 0 and child.adjustment > 0:
|
||||
child.frozen = True
|
||||
elif adjustments < 0 and child.adjustment < 0:
|
||||
child.frozen = True
|
||||
|
||||
# Step 6 - 9.7.5
|
||||
for i, child in line:
|
||||
if axis == 'width':
|
||||
child.width = (
|
||||
child.target_main_size -
|
||||
child.padding_left - child.padding_right -
|
||||
child.border_left_width - child.border_right_width)
|
||||
if child.margin_left != 'auto':
|
||||
child.width -= child.margin_left
|
||||
if child.margin_right != 'auto':
|
||||
child.width -= child.margin_right
|
||||
else:
|
||||
child.height = (
|
||||
child.target_main_size -
|
||||
child.padding_top - child.padding_bottom -
|
||||
child.border_top_width - child.border_top_width)
|
||||
if child.margin_left != 'auto':
|
||||
child.height -= child.margin_left
|
||||
if child.margin_right != 'auto':
|
||||
child.height -= child.margin_right
|
||||
|
||||
# Step 7
|
||||
# TODO: Fix TODO in build.flex_children
|
||||
# TODO: Handle breaks
|
||||
new_flex_lines = []
|
||||
child_skip_stack = skip_stack
|
||||
for line in flex_lines:
|
||||
new_flex_line = FlexLine()
|
||||
for i, child in line:
|
||||
# TODO: Find another way than calling block_level_layout_switch to
|
||||
# get baseline and child.height
|
||||
if child.margin_top == 'auto':
|
||||
child.margin_top = 0
|
||||
if child.margin_bottom == 'auto':
|
||||
child.margin_bottom = 0
|
||||
if isinstance(child, boxes.ParentBox):
|
||||
child_copy = child.copy_with_children(child.children)
|
||||
else:
|
||||
child_copy = child.copy()
|
||||
block.block_level_width(child_copy, parent_box)
|
||||
new_child, _, _, adjoining_margins, _ = (
|
||||
block.block_level_layout_switch(
|
||||
context, child_copy, -float('inf'), child_skip_stack,
|
||||
parent_box, page_is_empty, absolute_boxes, fixed_boxes,
|
||||
adjoining_margins=[], discard=False))
|
||||
|
||||
child._baseline = find_in_flow_baseline(new_child) or 0
|
||||
if cross == 'height':
|
||||
child.height = new_child.height
|
||||
# As flex items margins never collapse (with other flex items
|
||||
# or with the flex container), we can add the adjoining margins
|
||||
# to the child bottom margin.
|
||||
child.margin_bottom += block.collapse_margin(adjoining_margins)
|
||||
else:
|
||||
child.width = min_content_width(context, child, outer=False)
|
||||
|
||||
new_flex_line.append((i, child))
|
||||
|
||||
# Skip stack is only for the first child
|
||||
child_skip_stack = None
|
||||
|
||||
if new_flex_line:
|
||||
new_flex_lines.append(new_flex_line)
|
||||
flex_lines = new_flex_lines
|
||||
|
||||
# Step 8
|
||||
cross_size = getattr(box, cross)
|
||||
if len(flex_lines) == 1 and cross_size != 'auto':
|
||||
flex_lines[0].cross_size = cross_size
|
||||
else:
|
||||
for line in flex_lines:
|
||||
collected_items = []
|
||||
not_collected_items = []
|
||||
for i, child in line:
|
||||
align_self = child.style['align_self']
|
||||
if (box.style['flex_direction'].startswith('row') and
|
||||
align_self == 'baseline' and
|
||||
child.margin_top != 'auto' and
|
||||
child.margin_bottom != 'auto'):
|
||||
collected_items.append(child)
|
||||
else:
|
||||
not_collected_items.append(child)
|
||||
cross_start_distance = 0
|
||||
cross_end_distance = 0
|
||||
for child in collected_items:
|
||||
baseline = child._baseline - child.position_y
|
||||
cross_start_distance = max(cross_start_distance, baseline)
|
||||
cross_end_distance = max(
|
||||
cross_end_distance, child.margin_height() - baseline)
|
||||
collected_cross_size = cross_start_distance + cross_end_distance
|
||||
non_collected_cross_size = 0
|
||||
if not_collected_items:
|
||||
non_collected_cross_size = float('-inf')
|
||||
for child in not_collected_items:
|
||||
if cross == 'height':
|
||||
child_cross_size = child.border_height()
|
||||
if child.margin_top != 'auto':
|
||||
child_cross_size += child.margin_top
|
||||
if child.margin_bottom != 'auto':
|
||||
child_cross_size += child.margin_bottom
|
||||
else:
|
||||
child_cross_size = child.border_width()
|
||||
if child.margin_left != 'auto':
|
||||
child_cross_size += child.margin_left
|
||||
if child.margin_right != 'auto':
|
||||
child_cross_size += child.margin_right
|
||||
non_collected_cross_size = max(
|
||||
child_cross_size, non_collected_cross_size)
|
||||
line.cross_size = max(
|
||||
collected_cross_size, non_collected_cross_size)
|
||||
|
||||
if len(flex_lines) == 1:
|
||||
line, = flex_lines
|
||||
min_cross_size = getattr(box, f'min_{cross}')
|
||||
if min_cross_size == 'auto':
|
||||
min_cross_size = float('-inf')
|
||||
max_cross_size = getattr(box, f'max_{cross}')
|
||||
if max_cross_size == 'auto':
|
||||
max_cross_size = float('inf')
|
||||
line.cross_size = max(
|
||||
min_cross_size, min(line.cross_size, max_cross_size))
|
||||
|
||||
# Step 9
|
||||
if box.style['align_content'] == 'stretch':
|
||||
definite_cross_size = None
|
||||
if cross == 'height' and box.style['height'] != 'auto':
|
||||
definite_cross_size = box.style['height'].value
|
||||
elif cross == 'width':
|
||||
if isinstance(box, boxes.FlexBox):
|
||||
if box.style['width'] == 'auto':
|
||||
definite_cross_size = available_cross_space
|
||||
else:
|
||||
definite_cross_size = box.style['width'].value
|
||||
if definite_cross_size is not None:
|
||||
extra_cross_size = definite_cross_size - sum(
|
||||
line.cross_size for line in flex_lines)
|
||||
if extra_cross_size:
|
||||
for line in flex_lines:
|
||||
line.cross_size += extra_cross_size / len(flex_lines)
|
||||
|
||||
# TODO: Step 10
|
||||
|
||||
# Step 11
|
||||
for line in flex_lines:
|
||||
for i, child in line:
|
||||
align_self = child.style['align_self']
|
||||
if align_self == 'auto':
|
||||
align_self = box.style['align_items']
|
||||
if align_self == 'stretch' and child.style[cross] == 'auto':
|
||||
cross_margins = (
|
||||
(child.margin_top, child.margin_bottom)
|
||||
if cross == 'height'
|
||||
else (child.margin_left, child.margin_right))
|
||||
if child.style[cross] == 'auto':
|
||||
if 'auto' not in cross_margins:
|
||||
cross_size = line.cross_size
|
||||
if cross == 'height':
|
||||
cross_size -= (
|
||||
child.margin_top + child.margin_bottom +
|
||||
child.padding_top + child.padding_bottom +
|
||||
child.border_top_width +
|
||||
child.border_bottom_width)
|
||||
else:
|
||||
cross_size -= (
|
||||
child.margin_left + child.margin_right +
|
||||
child.padding_left + child.padding_right +
|
||||
child.border_left_width +
|
||||
child.border_right_width)
|
||||
setattr(child, cross, cross_size)
|
||||
# TODO: redo layout?
|
||||
# else: Cross size has been set by step 7
|
||||
|
||||
# Step 12
|
||||
original_position_axis = (
|
||||
box.content_box_x() if axis == 'width'
|
||||
else box.content_box_y())
|
||||
justify_content = box.style['justify_content']
|
||||
if box.style['flex_direction'].endswith('-reverse'):
|
||||
if justify_content == 'flex-start':
|
||||
justify_content = 'flex-end'
|
||||
elif justify_content == 'flex-end':
|
||||
justify_content = 'flex-start'
|
||||
|
||||
for line in flex_lines:
|
||||
position_axis = original_position_axis
|
||||
if axis == 'width':
|
||||
free_space = box.width
|
||||
for i, child in line:
|
||||
free_space -= child.border_width()
|
||||
if child.margin_left != 'auto':
|
||||
free_space -= child.margin_left
|
||||
if child.margin_right != 'auto':
|
||||
free_space -= child.margin_right
|
||||
else:
|
||||
free_space = box.height
|
||||
for i, child in line:
|
||||
free_space -= child.border_height()
|
||||
if child.margin_top != 'auto':
|
||||
free_space -= child.margin_top
|
||||
if child.margin_bottom != 'auto':
|
||||
free_space -= child.margin_bottom
|
||||
|
||||
margins = 0
|
||||
for i, child in line:
|
||||
if axis == 'width':
|
||||
if child.margin_left == 'auto':
|
||||
margins += 1
|
||||
if child.margin_right == 'auto':
|
||||
margins += 1
|
||||
else:
|
||||
if child.margin_top == 'auto':
|
||||
margins += 1
|
||||
if child.margin_bottom == 'auto':
|
||||
margins += 1
|
||||
if margins:
|
||||
free_space /= margins
|
||||
for i, child in line:
|
||||
if axis == 'width':
|
||||
if child.margin_left == 'auto':
|
||||
child.margin_left = free_space
|
||||
if child.margin_right == 'auto':
|
||||
child.margin_right = free_space
|
||||
else:
|
||||
if child.margin_top == 'auto':
|
||||
child.margin_top = free_space
|
||||
if child.margin_bottom == 'auto':
|
||||
child.margin_bottom = free_space
|
||||
free_space = 0
|
||||
|
||||
if box.style['direction'] == 'rtl' and axis == 'width':
|
||||
free_space *= -1
|
||||
|
||||
if justify_content == 'flex-end':
|
||||
position_axis += free_space
|
||||
elif justify_content == 'center':
|
||||
position_axis += free_space / 2
|
||||
elif justify_content == 'space-around':
|
||||
position_axis += free_space / len(line) / 2
|
||||
elif justify_content == 'space-evenly':
|
||||
position_axis += free_space / (len(line) + 1)
|
||||
|
||||
for i, child in line:
|
||||
if axis == 'width':
|
||||
child.position_x = position_axis
|
||||
if justify_content == 'stretch':
|
||||
child.width += free_space / len(line)
|
||||
else:
|
||||
child.position_y = position_axis
|
||||
margin_axis = (
|
||||
child.margin_width() if axis == 'width'
|
||||
else child.margin_height())
|
||||
if box.style['direction'] == 'rtl' and axis == 'width':
|
||||
margin_axis *= -1
|
||||
position_axis += margin_axis
|
||||
if justify_content == 'space-around':
|
||||
position_axis += free_space / len(line)
|
||||
elif justify_content == 'space-between':
|
||||
if len(line) > 1:
|
||||
position_axis += free_space / (len(line) - 1)
|
||||
elif justify_content == 'space-evenly':
|
||||
position_axis += free_space / (len(line) + 1)
|
||||
|
||||
# Step 13
|
||||
position_cross = (
|
||||
box.content_box_y() if cross == 'height'
|
||||
else box.content_box_x())
|
||||
for line in flex_lines:
|
||||
line.lower_baseline = 0
|
||||
# TODO: don't duplicate this loop
|
||||
for i, child in line:
|
||||
align_self = child.style['align_self']
|
||||
if align_self == 'auto':
|
||||
align_self = box.style['align_items']
|
||||
if align_self == 'baseline' and axis == 'width':
|
||||
# TODO: handle vertical text
|
||||
child.baseline = child._baseline - position_cross
|
||||
line.lower_baseline = max(line.lower_baseline, child.baseline)
|
||||
for i, child in line:
|
||||
cross_margins = (
|
||||
(child.margin_top, child.margin_bottom) if cross == 'height'
|
||||
else (child.margin_left, child.margin_right))
|
||||
auto_margins = sum([margin == 'auto' for margin in cross_margins])
|
||||
if auto_margins:
|
||||
extra_cross = line.cross_size
|
||||
if cross == 'height':
|
||||
extra_cross -= child.border_height()
|
||||
if child.margin_top != 'auto':
|
||||
extra_cross -= child.margin_top
|
||||
if child.margin_bottom != 'auto':
|
||||
extra_cross -= child.margin_bottom
|
||||
else:
|
||||
extra_cross -= child.border_width()
|
||||
if child.margin_left != 'auto':
|
||||
extra_cross -= child.margin_left
|
||||
if child.margin_right != 'auto':
|
||||
extra_cross -= child.margin_right
|
||||
if extra_cross > 0:
|
||||
extra_cross /= auto_margins
|
||||
if cross == 'height':
|
||||
if child.margin_top == 'auto':
|
||||
child.margin_top = extra_cross
|
||||
if child.margin_bottom == 'auto':
|
||||
child.margin_bottom = extra_cross
|
||||
else:
|
||||
if child.margin_left == 'auto':
|
||||
child.margin_left = extra_cross
|
||||
if child.margin_right == 'auto':
|
||||
child.margin_right = extra_cross
|
||||
else:
|
||||
if cross == 'height':
|
||||
if child.margin_top == 'auto':
|
||||
child.margin_top = 0
|
||||
child.margin_bottom = extra_cross
|
||||
else:
|
||||
if child.margin_left == 'auto':
|
||||
child.margin_left = 0
|
||||
child.margin_right = extra_cross
|
||||
else:
|
||||
# Step 14
|
||||
align_self = child.style['align_self']
|
||||
if align_self == 'auto':
|
||||
align_self = box.style['align_items']
|
||||
position = 'position_y' if cross == 'height' else 'position_x'
|
||||
setattr(child, position, position_cross)
|
||||
if align_self == 'flex-end':
|
||||
if cross == 'height':
|
||||
child.position_y += (
|
||||
line.cross_size - child.margin_height())
|
||||
else:
|
||||
child.position_x += (
|
||||
line.cross_size - child.margin_width())
|
||||
elif align_self == 'center':
|
||||
if cross == 'height':
|
||||
child.position_y += (
|
||||
line.cross_size - child.margin_height()) / 2
|
||||
else:
|
||||
child.position_x += (
|
||||
line.cross_size - child.margin_width()) / 2
|
||||
elif align_self == 'baseline':
|
||||
if cross == 'height':
|
||||
child.position_y += (
|
||||
line.lower_baseline - child.baseline)
|
||||
else:
|
||||
# Handle vertical text
|
||||
pass
|
||||
elif align_self == 'stretch':
|
||||
if child.style[cross] == 'auto':
|
||||
if cross == 'height':
|
||||
margins = child.margin_top + child.margin_bottom
|
||||
else:
|
||||
margins = child.margin_left + child.margin_right
|
||||
if child.style['box_sizing'] == 'content-box':
|
||||
if cross == 'height':
|
||||
margins += (
|
||||
child.border_top_width +
|
||||
child.border_bottom_width +
|
||||
child.padding_top + child.padding_bottom)
|
||||
else:
|
||||
margins += (
|
||||
child.border_left_width +
|
||||
child.border_right_width +
|
||||
child.padding_left + child.padding_right)
|
||||
# TODO: don't set style width, find a way to avoid
|
||||
# width re-calculation after Step 16
|
||||
child.style[cross] = Dimension(
|
||||
line.cross_size - margins, 'px')
|
||||
position_cross += line.cross_size
|
||||
|
||||
# Step 15
|
||||
if getattr(box, cross) == 'auto':
|
||||
# TODO: handle min-max
|
||||
setattr(box, cross, sum(line.cross_size for line in flex_lines))
|
||||
|
||||
# Step 16
|
||||
elif len(flex_lines) > 1:
|
||||
extra_cross_size = getattr(box, cross) - sum(
|
||||
line.cross_size for line in flex_lines)
|
||||
direction = 'position_y' if cross == 'height' else 'position_x'
|
||||
if extra_cross_size > 0:
|
||||
cross_translate = 0
|
||||
for line in flex_lines:
|
||||
for i, child in line:
|
||||
if child.is_flex_item:
|
||||
current_value = getattr(child, direction)
|
||||
current_value += cross_translate
|
||||
setattr(child, direction, current_value)
|
||||
if box.style['align_content'] == 'flex-end':
|
||||
setattr(
|
||||
child, direction,
|
||||
current_value + extra_cross_size)
|
||||
elif box.style['align_content'] == 'center':
|
||||
setattr(
|
||||
child, direction,
|
||||
current_value + extra_cross_size / 2)
|
||||
elif box.style['align_content'] == 'space-around':
|
||||
setattr(
|
||||
child, direction,
|
||||
current_value + extra_cross_size /
|
||||
len(flex_lines) / 2)
|
||||
elif box.style['align_content'] == 'space-evenly':
|
||||
setattr(
|
||||
child, direction,
|
||||
current_value + extra_cross_size /
|
||||
(len(flex_lines) + 1))
|
||||
if box.style['align_content'] == 'space-between':
|
||||
cross_translate += extra_cross_size / (len(flex_lines) - 1)
|
||||
elif box.style['align_content'] == 'space-around':
|
||||
cross_translate += extra_cross_size / len(flex_lines)
|
||||
elif box.style['align_content'] == 'space-evenly':
|
||||
cross_translate += extra_cross_size / (len(flex_lines) + 1)
|
||||
|
||||
# TODO: don't use block_box_layout, see TODOs in Step 14 and
|
||||
# build.flex_children.
|
||||
box = box.copy()
|
||||
box.children = []
|
||||
child_skip_stack = skip_stack
|
||||
for line in flex_lines:
|
||||
for i, child in line:
|
||||
if child.is_flex_item:
|
||||
new_child, child_resume_at = block.block_level_layout_switch(
|
||||
context, child, bottom_space, child_skip_stack, box,
|
||||
page_is_empty, absolute_boxes, fixed_boxes,
|
||||
adjoining_margins=[], discard=False)[:2]
|
||||
if new_child is None:
|
||||
if resume_at:
|
||||
index, = resume_at
|
||||
if index:
|
||||
resume_at = {index + i - 1: None}
|
||||
else:
|
||||
box.children.append(new_child)
|
||||
if child_resume_at is not None:
|
||||
if original_skip_stack:
|
||||
# TODO: handle multiple skip stacks
|
||||
first_level_skip, = original_skip_stack
|
||||
else:
|
||||
first_level_skip = 0
|
||||
if resume_at:
|
||||
index, = resume_at
|
||||
first_level_skip += index
|
||||
resume_at = {first_level_skip + i: child_resume_at}
|
||||
if resume_at:
|
||||
break
|
||||
|
||||
# Skip stack is only for the first child
|
||||
child_skip_stack = None
|
||||
if resume_at:
|
||||
break
|
||||
|
||||
# Set box height
|
||||
# TODO: this is probably useless because of step #15
|
||||
if axis == 'width' and box.height == 'auto':
|
||||
if flex_lines:
|
||||
box.height = sum(line.cross_size for line in flex_lines)
|
||||
else:
|
||||
box.height = 0
|
||||
|
||||
# Set baseline
|
||||
# See https://www.w3.org/TR/css-flexbox-1/#flex-baselines
|
||||
# TODO: use the real algorithm
|
||||
if isinstance(box, boxes.InlineFlexBox):
|
||||
if axis == 'width': # and main text direction is horizontal
|
||||
box.baseline = flex_lines[0].lower_baseline if flex_lines else 0
|
||||
else:
|
||||
box.baseline = ((
|
||||
find_in_flow_baseline(box.children[0])
|
||||
if box.children else 0) or 0)
|
||||
|
||||
context.finish_block_formatting_context(box)
|
||||
|
||||
# TODO: check these returned values
|
||||
return box, resume_at, {'break': 'any', 'page': None}, [], False
|
||||
229
venv/Lib/site-packages/weasyprint/layout/float.py
Normal file
229
venv/Lib/site-packages/weasyprint/layout/float.py
Normal file
@@ -0,0 +1,229 @@
|
||||
"""
|
||||
weasyprint.float
|
||||
----------------
|
||||
|
||||
Layout for floating boxes.
|
||||
|
||||
"""
|
||||
|
||||
from ..formatting_structure import boxes
|
||||
from .min_max import handle_min_max_width
|
||||
from .percent import resolve_percentages, resolve_position_percentages
|
||||
from .preferred import shrink_to_fit
|
||||
from .replaced import inline_replaced_box_width_height
|
||||
from .table import table_wrapper_width
|
||||
|
||||
|
||||
@handle_min_max_width
|
||||
def float_width(box, context, containing_block):
|
||||
# Check that box.width is auto even if the caller does it too, because
|
||||
# the handle_min_max_width decorator can change the value
|
||||
if box.width == 'auto':
|
||||
box.width = shrink_to_fit(context, box, containing_block.width)
|
||||
|
||||
|
||||
def float_layout(context, box, containing_block, absolute_boxes, fixed_boxes,
|
||||
bottom_space, skip_stack):
|
||||
"""Set the width and position of floating ``box``."""
|
||||
from .block import block_container_layout
|
||||
from .flex import flex_layout
|
||||
|
||||
cb_width, cb_height = (containing_block.width, containing_block.height)
|
||||
resolve_percentages(box, (cb_width, cb_height))
|
||||
|
||||
# TODO: This is only handled later in blocks.block_container_layout
|
||||
# http://www.w3.org/TR/CSS21/visudet.html#normal-block
|
||||
if cb_height == 'auto':
|
||||
cb_height = (
|
||||
containing_block.position_y - containing_block.content_box_y())
|
||||
|
||||
resolve_position_percentages(box, (cb_width, cb_height))
|
||||
|
||||
if box.margin_left == 'auto':
|
||||
box.margin_left = 0
|
||||
if box.margin_right == 'auto':
|
||||
box.margin_right = 0
|
||||
if box.margin_top == 'auto':
|
||||
box.margin_top = 0
|
||||
if box.margin_bottom == 'auto':
|
||||
box.margin_bottom = 0
|
||||
|
||||
clearance = get_clearance(context, box)
|
||||
if clearance is not None:
|
||||
box.position_y += clearance
|
||||
|
||||
if isinstance(box, boxes.BlockReplacedBox):
|
||||
inline_replaced_box_width_height(box, containing_block)
|
||||
elif box.width == 'auto':
|
||||
float_width(box, context, containing_block)
|
||||
|
||||
if box.is_table_wrapper:
|
||||
table_wrapper_width(context, box, (cb_width, cb_height))
|
||||
|
||||
if isinstance(box, boxes.BlockContainerBox):
|
||||
context.create_block_formatting_context()
|
||||
box, resume_at, _, _, _ = block_container_layout(
|
||||
context, box, bottom_space=bottom_space,
|
||||
skip_stack=skip_stack, page_is_empty=True,
|
||||
absolute_boxes=absolute_boxes, fixed_boxes=fixed_boxes,
|
||||
adjoining_margins=None, discard=False)
|
||||
context.finish_block_formatting_context(box)
|
||||
elif isinstance(box, boxes.FlexContainerBox):
|
||||
box, resume_at, _, _, _ = flex_layout(
|
||||
context, box, bottom_space=bottom_space,
|
||||
skip_stack=skip_stack, containing_block=containing_block,
|
||||
page_is_empty=True, absolute_boxes=absolute_boxes,
|
||||
fixed_boxes=fixed_boxes)
|
||||
else:
|
||||
assert isinstance(box, boxes.BlockReplacedBox)
|
||||
resume_at = None
|
||||
|
||||
box = find_float_position(context, box, containing_block)
|
||||
|
||||
context.excluded_shapes.append(box)
|
||||
|
||||
return box, resume_at
|
||||
|
||||
|
||||
def find_float_position(context, box, containing_block):
|
||||
"""Get the right position of the float ``box``."""
|
||||
# See http://www.w3.org/TR/CSS2/visuren.html#float-position
|
||||
|
||||
# Point 4 is already handled as box.position_y is set according to the
|
||||
# containing box top position, with collapsing margins handled
|
||||
|
||||
# Points 5 and 6, box.position_y is set to the highest position_y possible
|
||||
if context.excluded_shapes:
|
||||
highest_y = context.excluded_shapes[-1].position_y
|
||||
if box.position_y < highest_y:
|
||||
box.translate(0, highest_y - box.position_y)
|
||||
|
||||
# Points 1 and 2
|
||||
position_x, position_y, available_width = avoid_collisions(
|
||||
context, box, containing_block)
|
||||
|
||||
# Point 9
|
||||
# position_y is set now, let's define position_x
|
||||
# for float: left elements, it's already done!
|
||||
if box.style['float'] == 'right':
|
||||
position_x += available_width - box.margin_width()
|
||||
|
||||
box.translate(position_x - box.position_x, position_y - box.position_y)
|
||||
|
||||
return box
|
||||
|
||||
|
||||
def get_clearance(context, box, collapsed_margin=0):
|
||||
"""Return None if there is no clearance, otherwise the clearance value."""
|
||||
clearance = None
|
||||
hypothetical_position = box.position_y + collapsed_margin
|
||||
# Hypothetical position is the position of the top border edge
|
||||
for excluded_shape in context.excluded_shapes:
|
||||
if box.style['clear'] in (excluded_shape.style['float'], 'both'):
|
||||
y, h = excluded_shape.position_y, excluded_shape.margin_height()
|
||||
if hypothetical_position < y + h:
|
||||
clearance = max(
|
||||
(clearance or 0), y + h - hypothetical_position)
|
||||
return clearance
|
||||
|
||||
|
||||
def avoid_collisions(context, box, containing_block, outer=True):
|
||||
excluded_shapes = context.excluded_shapes
|
||||
position_y = box.position_y if outer else box.border_box_y()
|
||||
|
||||
box_width = box.margin_width() if outer else box.border_width()
|
||||
box_height = box.margin_height() if outer else box.border_height()
|
||||
|
||||
if box.border_height() == 0 and box.is_floated():
|
||||
return 0, 0, containing_block.width
|
||||
|
||||
while True:
|
||||
colliding_shapes = []
|
||||
for shape in excluded_shapes:
|
||||
# Assign locals to avoid slow attribute lookups.
|
||||
shape_position_y = shape.position_y
|
||||
shape_margin_height = shape.margin_height()
|
||||
if ((shape_position_y < position_y <
|
||||
shape_position_y + shape_margin_height) or
|
||||
(shape_position_y < position_y + box_height <
|
||||
shape_position_y + shape_margin_height) or
|
||||
(shape_position_y >= position_y and
|
||||
shape_position_y + shape_margin_height <=
|
||||
position_y + box_height)):
|
||||
colliding_shapes.append(shape)
|
||||
left_bounds = [
|
||||
shape.position_x + shape.margin_width()
|
||||
for shape in colliding_shapes
|
||||
if shape.style['float'] == 'left']
|
||||
right_bounds = [
|
||||
shape.position_x
|
||||
for shape in colliding_shapes
|
||||
if shape.style['float'] == 'right']
|
||||
|
||||
# Set the default maximum bounds
|
||||
max_left_bound = containing_block.content_box_x()
|
||||
max_right_bound = \
|
||||
containing_block.content_box_x() + containing_block.width
|
||||
|
||||
if not outer:
|
||||
max_left_bound += box.margin_left
|
||||
max_right_bound -= box.margin_right
|
||||
|
||||
# Set the real maximum bounds according to sibling float elements
|
||||
if left_bounds or right_bounds:
|
||||
if left_bounds:
|
||||
max_left_bound = max(max(left_bounds), max_left_bound)
|
||||
if right_bounds:
|
||||
max_right_bound = min(min(right_bounds), max_right_bound)
|
||||
|
||||
# Points 3, 7 and 8
|
||||
if box_width > max_right_bound - max_left_bound:
|
||||
# The box does not fit here
|
||||
new_positon_y = min(
|
||||
shape.position_y + shape.margin_height()
|
||||
for shape in colliding_shapes)
|
||||
if new_positon_y > position_y:
|
||||
# We can find a solution with a higher position_y
|
||||
position_y = new_positon_y
|
||||
continue
|
||||
# No solution, we must put the box here
|
||||
break
|
||||
|
||||
# See https://www.w3.org/TR/CSS21/visuren.html#floats
|
||||
# Boxes that can’t collide with floats are:
|
||||
# - floats
|
||||
# - line boxes
|
||||
# - table wrappers
|
||||
# - block-level replaced box
|
||||
# - element establishing new formatting contexts (not handled)
|
||||
assert (
|
||||
(box.style['float'] in ('right', 'left')) or
|
||||
isinstance(box, boxes.LineBox) or
|
||||
box.is_table_wrapper or
|
||||
isinstance(box, boxes.BlockReplacedBox))
|
||||
|
||||
# The x-position of the box depends on its type.
|
||||
position_x = max_left_bound
|
||||
if box.style['float'] == 'none':
|
||||
if containing_block.style['direction'] == 'rtl':
|
||||
if isinstance(box, boxes.LineBox):
|
||||
# The position of the line is the position of the cursor, at
|
||||
# the right bound.
|
||||
position_x = max_right_bound
|
||||
elif box.is_table_wrapper:
|
||||
# The position of the right border of the table is at the right
|
||||
# bound.
|
||||
position_x = max_right_bound - box_width
|
||||
else:
|
||||
# The position of the right border of the replaced box is at
|
||||
# the right bound.
|
||||
assert isinstance(box, boxes.BlockReplacedBox)
|
||||
position_x = max_right_bound - box_width
|
||||
|
||||
available_width = max_right_bound - max_left_bound
|
||||
|
||||
if not outer:
|
||||
position_x -= box.margin_left
|
||||
position_y -= box.margin_top
|
||||
|
||||
return position_x, position_y, available_width
|
||||
1210
venv/Lib/site-packages/weasyprint/layout/inline.py
Normal file
1210
venv/Lib/site-packages/weasyprint/layout/inline.py
Normal file
File diff suppressed because it is too large
Load Diff
81
venv/Lib/site-packages/weasyprint/layout/leader.py
Normal file
81
venv/Lib/site-packages/weasyprint/layout/leader.py
Normal file
@@ -0,0 +1,81 @@
|
||||
"""
|
||||
weasyprint.layout.leader
|
||||
------------------------
|
||||
|
||||
Leader management.
|
||||
|
||||
"""
|
||||
|
||||
from ..formatting_structure import boxes
|
||||
|
||||
|
||||
def leader_index(box):
|
||||
"""Get the index of the first leader box in ``box``."""
|
||||
for i, child in enumerate(box.children):
|
||||
if child.is_leader:
|
||||
return (i, None), child
|
||||
if isinstance(child, boxes.ParentBox):
|
||||
child_leader_index, child_leader = leader_index(child)
|
||||
if child_leader_index is not None:
|
||||
return (i, child_leader_index), child_leader
|
||||
return None, None
|
||||
|
||||
|
||||
def handle_leader(context, line, containing_block):
|
||||
"""Find a leader box in ``line`` and handle its text and its position."""
|
||||
index, leader_box = leader_index(line)
|
||||
extra_width = 0
|
||||
if index is not None and leader_box.children:
|
||||
text_box, = leader_box.children
|
||||
|
||||
# Abort if the leader text has no width
|
||||
if text_box.width <= 0:
|
||||
return
|
||||
|
||||
# Extra width is the additional width taken by the leader box
|
||||
extra_width = containing_block.width - sum(
|
||||
child.width for child in line.children
|
||||
if child.is_in_normal_flow())
|
||||
|
||||
# Take care of excluded shapes
|
||||
for shape in context.excluded_shapes:
|
||||
if shape.position_y + shape.height > line.position_y:
|
||||
extra_width -= shape.width
|
||||
|
||||
# Available width is the width available for the leader box
|
||||
available_width = extra_width + text_box.width
|
||||
line.width = containing_block.width
|
||||
|
||||
# Add text boxes into the leader box
|
||||
number_of_leaders = int(line.width // text_box.width)
|
||||
position_x = line.position_x + line.width
|
||||
children = []
|
||||
for i in range(number_of_leaders):
|
||||
position_x -= text_box.width
|
||||
if position_x < leader_box.position_x:
|
||||
# Don’t add leaders behind the text on the left
|
||||
continue
|
||||
elif (position_x + text_box.width >
|
||||
leader_box.position_x + available_width):
|
||||
# Don’t add leaders behind the text on the right
|
||||
continue
|
||||
text_box = text_box.copy()
|
||||
text_box.position_x = position_x
|
||||
children.append(text_box)
|
||||
leader_box.children = tuple(children)
|
||||
|
||||
if line.style['direction'] == 'rtl':
|
||||
leader_box.translate(dx=-extra_width)
|
||||
|
||||
# Widen leader parent boxes and translate following boxes
|
||||
box = line
|
||||
while index is not None:
|
||||
for child in box.children[index[0] + 1:]:
|
||||
if child.is_in_normal_flow():
|
||||
if line.style['direction'] == 'ltr':
|
||||
child.translate(dx=extra_width)
|
||||
else:
|
||||
child.translate(dx=-extra_width)
|
||||
box = box.children[index[0]]
|
||||
box.width += extra_width
|
||||
index = index[1]
|
||||
45
venv/Lib/site-packages/weasyprint/layout/min_max.py
Normal file
45
venv/Lib/site-packages/weasyprint/layout/min_max.py
Normal file
@@ -0,0 +1,45 @@
|
||||
"""
|
||||
weasyprint.layout.min_max
|
||||
-------------------------
|
||||
|
||||
"""
|
||||
|
||||
import functools
|
||||
|
||||
|
||||
def handle_min_max_width(function):
|
||||
"""Decorate a function setting used width, handling {min,max}-width."""
|
||||
@functools.wraps(function)
|
||||
def wrapper(box, *args):
|
||||
computed_margins = box.margin_left, box.margin_right
|
||||
result = function(box, *args)
|
||||
if box.width > box.max_width:
|
||||
box.width = box.max_width
|
||||
box.margin_left, box.margin_right = computed_margins
|
||||
result = function(box, *args)
|
||||
if box.width < box.min_width:
|
||||
box.width = box.min_width
|
||||
box.margin_left, box.margin_right = computed_margins
|
||||
result = function(box, *args)
|
||||
return result
|
||||
wrapper.without_min_max = function
|
||||
return wrapper
|
||||
|
||||
|
||||
def handle_min_max_height(function):
|
||||
"""Decorate a function setting used height, handling {min,max}-height."""
|
||||
@functools.wraps(function)
|
||||
def wrapper(box, *args):
|
||||
computed_margins = box.margin_top, box.margin_bottom
|
||||
result = function(box, *args)
|
||||
if box.height > box.max_height:
|
||||
box.height = box.max_height
|
||||
box.margin_top, box.margin_bottom = computed_margins
|
||||
result = function(box, *args)
|
||||
if box.height < box.min_height:
|
||||
box.height = box.min_height
|
||||
box.margin_top, box.margin_bottom = computed_margins
|
||||
result = function(box, *args)
|
||||
return result
|
||||
wrapper.without_min_max = function
|
||||
return wrapper
|
||||
867
venv/Lib/site-packages/weasyprint/layout/page.py
Normal file
867
venv/Lib/site-packages/weasyprint/layout/page.py
Normal file
@@ -0,0 +1,867 @@
|
||||
"""
|
||||
weasyprint.layout.pages
|
||||
-----------------------
|
||||
|
||||
Layout for pages and CSS3 margin boxes.
|
||||
|
||||
"""
|
||||
|
||||
import copy
|
||||
|
||||
from ..css import PageType, computed_from_cascaded
|
||||
from ..formatting_structure import boxes, build
|
||||
from ..logger import PROGRESS_LOGGER
|
||||
from .absolute import absolute_box_layout, absolute_layout
|
||||
from .block import block_container_layout, block_level_layout
|
||||
from .float import float_layout
|
||||
from .min_max import handle_min_max_height, handle_min_max_width
|
||||
from .percent import resolve_percentages
|
||||
from .preferred import max_content_width, min_content_width
|
||||
|
||||
|
||||
class OrientedBox:
|
||||
@property
|
||||
def sugar(self):
|
||||
return self.padding_plus_border + self.margin_a + self.margin_b
|
||||
|
||||
@property
|
||||
def outer(self):
|
||||
return self.sugar + self.inner
|
||||
|
||||
@property
|
||||
def outer_min_content_size(self):
|
||||
return self.sugar + (
|
||||
self.min_content_size if self.inner == 'auto' else self.inner)
|
||||
|
||||
@property
|
||||
def outer_max_content_size(self):
|
||||
return self.sugar + (
|
||||
self.max_content_size if self.inner == 'auto' else self.inner)
|
||||
|
||||
def shrink_to_fit(self, available):
|
||||
self.inner = min(
|
||||
max(self.min_content_size, available), self.max_content_size)
|
||||
|
||||
|
||||
class VerticalBox(OrientedBox):
|
||||
def __init__(self, context, box):
|
||||
self.context = context
|
||||
self.box = box
|
||||
# Inner dimension: that of the content area, as opposed to the
|
||||
# outer dimension: that of the margin area.
|
||||
self.inner = box.height
|
||||
self.margin_a = box.margin_top
|
||||
self.margin_b = box.margin_bottom
|
||||
self.padding_plus_border = (
|
||||
box.padding_top + box.padding_bottom +
|
||||
box.border_top_width + box.border_bottom_width)
|
||||
|
||||
def restore_box_attributes(self):
|
||||
box = self.box
|
||||
box.height = self.inner
|
||||
box.margin_top = self.margin_a
|
||||
box.margin_bottom = self.margin_b
|
||||
|
||||
# TODO: Define what are the min-content and max-content heights
|
||||
@property
|
||||
def min_content_size(self):
|
||||
return 0
|
||||
|
||||
@property
|
||||
def max_content_size(self):
|
||||
return 1e6
|
||||
|
||||
|
||||
class HorizontalBox(OrientedBox):
|
||||
def __init__(self, context, box):
|
||||
self.context = context
|
||||
self.box = box
|
||||
self.inner = box.width
|
||||
self.margin_a = box.margin_left
|
||||
self.margin_b = box.margin_right
|
||||
self.padding_plus_border = (
|
||||
box.padding_left + box.padding_right +
|
||||
box.border_left_width + box.border_right_width)
|
||||
self._min_content_size = None
|
||||
self._max_content_size = None
|
||||
|
||||
def restore_box_attributes(self):
|
||||
box = self.box
|
||||
box.width = self.inner
|
||||
box.margin_left = self.margin_a
|
||||
box.margin_right = self.margin_b
|
||||
|
||||
@property
|
||||
def min_content_size(self):
|
||||
if self._min_content_size is None:
|
||||
self._min_content_size = min_content_width(
|
||||
self.context, self.box, outer=False)
|
||||
return self._min_content_size
|
||||
|
||||
@property
|
||||
def max_content_size(self):
|
||||
if self._max_content_size is None:
|
||||
self._max_content_size = max_content_width(
|
||||
self.context, self.box, outer=False)
|
||||
return self._max_content_size
|
||||
|
||||
|
||||
def compute_fixed_dimension(context, box, outer, vertical, top_or_left):
|
||||
"""
|
||||
Compute and set a margin box fixed dimension on ``box``, as described in:
|
||||
http://dev.w3.org/csswg/css3-page/#margin-constraints
|
||||
|
||||
:param box:
|
||||
The margin box to work on
|
||||
:param outer:
|
||||
The target outer dimension (value of a page margin)
|
||||
:param vertical:
|
||||
True to set height, margin-top and margin-bottom; False for width,
|
||||
margin-left and margin-right
|
||||
:param top_or_left:
|
||||
True if the margin box in if the top half (for vertical==True) or
|
||||
left half (for vertical==False) of the page.
|
||||
This determines which margin should be 'auto' if the values are
|
||||
over-constrained. (Rule 3 of the algorithm.)
|
||||
"""
|
||||
box = (VerticalBox if vertical else HorizontalBox)(context, box)
|
||||
|
||||
# Rule 2
|
||||
total = box.padding_plus_border + sum(
|
||||
value for value in [box.margin_a, box.margin_b, box.inner]
|
||||
if value != 'auto')
|
||||
if total > outer:
|
||||
if box.margin_a == 'auto':
|
||||
box.margin_a = 0
|
||||
if box.margin_b == 'auto':
|
||||
box.margin_b = 0
|
||||
if box.inner == 'auto':
|
||||
# XXX this is not in the spec, but without it box.inner
|
||||
# would end up with a negative value.
|
||||
# Instead, this will trigger rule 3 below.
|
||||
# http://lists.w3.org/Archives/Public/www-style/2012Jul/0006.html
|
||||
box.inner = 0
|
||||
# Rule 3
|
||||
if 'auto' not in [box.margin_a, box.margin_b, box.inner]:
|
||||
# Over-constrained
|
||||
if top_or_left:
|
||||
box.margin_a = 'auto'
|
||||
else:
|
||||
box.margin_b = 'auto'
|
||||
# Rule 4
|
||||
if [box.margin_a, box.margin_b, box.inner].count('auto') == 1:
|
||||
if box.inner == 'auto':
|
||||
box.inner = (outer - box.padding_plus_border -
|
||||
box.margin_a - box.margin_b)
|
||||
elif box.margin_a == 'auto':
|
||||
box.margin_a = (outer - box.padding_plus_border -
|
||||
box.margin_b - box.inner)
|
||||
elif box.margin_b == 'auto':
|
||||
box.margin_b = (outer - box.padding_plus_border -
|
||||
box.margin_a - box.inner)
|
||||
# Rule 5
|
||||
if box.inner == 'auto':
|
||||
if box.margin_a == 'auto':
|
||||
box.margin_a = 0
|
||||
if box.margin_b == 'auto':
|
||||
box.margin_b = 0
|
||||
box.inner = (outer - box.padding_plus_border -
|
||||
box.margin_a - box.margin_b)
|
||||
# Rule 6
|
||||
if box.margin_a == 'auto' and box.margin_b == 'auto':
|
||||
box.margin_a = box.margin_b = (
|
||||
outer - box.padding_plus_border - box.inner) / 2
|
||||
|
||||
assert 'auto' not in [box.margin_a, box.margin_b, box.inner]
|
||||
|
||||
box.restore_box_attributes()
|
||||
|
||||
|
||||
def compute_variable_dimension(context, side_boxes, vertical, outer_sum):
|
||||
"""
|
||||
Compute and set a margin box fixed dimension on ``box``, as described in:
|
||||
http://dev.w3.org/csswg/css3-page/#margin-dimension
|
||||
|
||||
:param side_boxes: Three boxes on a same side (as opposed to a corner.)
|
||||
A list of:
|
||||
- A @*-left or @*-top margin box
|
||||
- A @*-center or @*-middle margin box
|
||||
- A @*-right or @*-bottom margin box
|
||||
:param vertical:
|
||||
True to set height, margin-top and margin-bottom; False for width,
|
||||
margin-left and margin-right
|
||||
:param outer_sum:
|
||||
The target total outer dimension (max box width or height)
|
||||
|
||||
"""
|
||||
box_class = VerticalBox if vertical else HorizontalBox
|
||||
side_boxes = [box_class(context, box) for box in side_boxes]
|
||||
box_a, box_b, box_c = side_boxes
|
||||
|
||||
for box in side_boxes:
|
||||
if box.margin_a == 'auto':
|
||||
box.margin_a = 0
|
||||
if box.margin_b == 'auto':
|
||||
box.margin_b = 0
|
||||
|
||||
if box_b.box.is_generated:
|
||||
if box_b.inner == 'auto':
|
||||
ac_max_content_size = 2 * max(
|
||||
box_a.outer_max_content_size, box_c.outer_max_content_size)
|
||||
if outer_sum >= (
|
||||
box_b.outer_max_content_size + ac_max_content_size):
|
||||
box_b.inner = box_b.max_content_size
|
||||
else:
|
||||
ac_min_content_size = 2 * max(
|
||||
box_a.outer_min_content_size,
|
||||
box_c.outer_min_content_size)
|
||||
box_b.inner = box_b.min_content_size
|
||||
available = outer_sum - box_b.outer - ac_min_content_size
|
||||
if available > 0:
|
||||
weight_ac = ac_max_content_size - ac_min_content_size
|
||||
weight_b = (
|
||||
box_b.max_content_size - box_b.min_content_size)
|
||||
weight_sum = weight_ac + weight_b
|
||||
# By definition of max_content_size and min_content_size,
|
||||
# weights can not be negative. weight_sum == 0 implies that
|
||||
# max_content_size == min_content_size for each box, in
|
||||
# which case the sum can not be both <= and > outer_sum
|
||||
# Therefore, one of the last two 'if' statements would not
|
||||
# have lead us here.
|
||||
assert weight_sum > 0
|
||||
box_b.inner += available * weight_b / weight_sum
|
||||
if box_a.inner == 'auto':
|
||||
box_a.shrink_to_fit((outer_sum - box_b.outer) / 2 - box_a.sugar)
|
||||
if box_c.inner == 'auto':
|
||||
box_c.shrink_to_fit((outer_sum - box_b.outer) / 2 - box_c.sugar)
|
||||
else:
|
||||
# Non-generated boxes get zero for every box-model property
|
||||
assert box_b.inner == 0
|
||||
if box_a.inner == box_c.inner == 'auto':
|
||||
if outer_sum >= (
|
||||
box_a.outer_max_content_size +
|
||||
box_c.outer_max_content_size):
|
||||
box_a.inner = box_a.max_content_size
|
||||
box_c.inner = box_c.max_content_size
|
||||
else:
|
||||
box_a.inner = box_a.min_content_size
|
||||
box_c.inner = box_c.min_content_size
|
||||
available = outer_sum - box_a.outer - box_c.outer
|
||||
if available > 0:
|
||||
weight_a = (
|
||||
box_a.max_content_size - box_a.min_content_size)
|
||||
weight_c = (
|
||||
box_c.max_content_size - box_c.min_content_size)
|
||||
weight_sum = weight_a + weight_c
|
||||
# By definition of max_content_size and min_content_size,
|
||||
# weights can not be negative. weight_sum == 0 implies that
|
||||
# max_content_size == min_content_size for each box, in
|
||||
# which case the sum can not be both <= and > outer_sum
|
||||
# Therefore, one of the last two 'if' statements would not
|
||||
# have lead us here.
|
||||
assert weight_sum > 0
|
||||
box_a.inner += available * weight_a / weight_sum
|
||||
box_c.inner += available * weight_c / weight_sum
|
||||
elif box_a.inner == 'auto':
|
||||
box_a.shrink_to_fit(outer_sum - box_c.outer - box_a.sugar)
|
||||
elif box_c.inner == 'auto':
|
||||
box_c.shrink_to_fit(outer_sum - box_a.outer - box_c.sugar)
|
||||
|
||||
# And, we’re done!
|
||||
assert 'auto' not in [box.inner for box in side_boxes]
|
||||
# Set the actual attributes back.
|
||||
for box in side_boxes:
|
||||
box.restore_box_attributes()
|
||||
|
||||
|
||||
def _standardize_page_based_counters(style, pseudo_type):
|
||||
"""Drop 'pages' counter from style in @page and @margin context.
|
||||
|
||||
Ensure `counter-increment: page` for @page context if not otherwise
|
||||
manipulated by the style.
|
||||
|
||||
"""
|
||||
page_counter_touched = False
|
||||
for propname in ('counter_set', 'counter_reset', 'counter_increment'):
|
||||
if style[propname] == 'auto':
|
||||
style[propname] = ()
|
||||
continue
|
||||
justified_values = []
|
||||
for name, value in style[propname]:
|
||||
if name == 'page':
|
||||
page_counter_touched = True
|
||||
if name != 'pages':
|
||||
justified_values.append((name, value))
|
||||
style[propname] = tuple(justified_values)
|
||||
|
||||
if pseudo_type is None and not page_counter_touched:
|
||||
style['counter_increment'] = (
|
||||
('page', 1),) + style['counter_increment']
|
||||
|
||||
|
||||
def make_margin_boxes(context, page, state):
|
||||
"""Yield laid-out margin boxes for this page.
|
||||
|
||||
``state`` is the actual, up-to-date page-state from
|
||||
``context.page_maker[context.current_page]``.
|
||||
|
||||
"""
|
||||
# This is a closure only to make calls shorter
|
||||
def make_box(at_keyword, containing_block):
|
||||
"""Return a margin box with resolved percentages.
|
||||
|
||||
The margin box may still have 'auto' values.
|
||||
|
||||
Return ``None`` if this margin box should not be generated.
|
||||
|
||||
:param at_keyword: which margin box to return, eg. '@top-left'
|
||||
:param containing_block: as expected by :func:`resolve_percentages`.
|
||||
|
||||
"""
|
||||
style = context.style_for(page.page_type, at_keyword)
|
||||
if style is None:
|
||||
# doesn't affect counters
|
||||
style = computed_from_cascaded(
|
||||
element=None, cascaded={}, parent_style=page.style)
|
||||
_standardize_page_based_counters(style, at_keyword)
|
||||
box = boxes.MarginBox(at_keyword, style)
|
||||
# Empty boxes should not be generated, but they may be needed for
|
||||
# the layout of their neighbors.
|
||||
# TODO: should be the computed value.
|
||||
box.is_generated = style['content'] not in (
|
||||
'normal', 'inhibit', 'none')
|
||||
# TODO: get actual counter values at the time of the last page break
|
||||
if box.is_generated:
|
||||
# @margins mustn't manipulate page-context counters
|
||||
margin_state = copy.deepcopy(state)
|
||||
quote_depth, counter_values, counter_scopes = margin_state
|
||||
# TODO: check this, probably useless
|
||||
counter_scopes.append(set())
|
||||
build.update_counters(margin_state, box.style)
|
||||
box.children = build.content_to_boxes(
|
||||
box.style, box, quote_depth, counter_values,
|
||||
context.get_image_from_uri, context.target_collector,
|
||||
context.counter_style, context, page)
|
||||
build.process_whitespace(box)
|
||||
build.process_text_transform(box)
|
||||
box = build.create_anonymous_boxes(box)
|
||||
resolve_percentages(box, containing_block)
|
||||
if not box.is_generated:
|
||||
box.width = box.height = 0
|
||||
for side in ('top', 'right', 'bottom', 'left'):
|
||||
box._reset_spacing(side)
|
||||
return box
|
||||
|
||||
margin_top = page.margin_top
|
||||
margin_bottom = page.margin_bottom
|
||||
margin_left = page.margin_left
|
||||
margin_right = page.margin_right
|
||||
max_box_width = page.border_width()
|
||||
max_box_height = page.border_height()
|
||||
|
||||
# bottom right corner of the border box
|
||||
page_end_x = margin_left + max_box_width
|
||||
page_end_y = margin_top + max_box_height
|
||||
|
||||
# Margin box dimensions, described in
|
||||
# http://dev.w3.org/csswg/css3-page/#margin-box-dimensions
|
||||
generated_boxes = []
|
||||
|
||||
for prefix, vertical, containing_block, position_x, position_y in [
|
||||
('top', False, (max_box_width, margin_top),
|
||||
margin_left, 0),
|
||||
('bottom', False, (max_box_width, margin_bottom),
|
||||
margin_left, page_end_y),
|
||||
('left', True, (margin_left, max_box_height),
|
||||
0, margin_top),
|
||||
('right', True, (margin_right, max_box_height),
|
||||
page_end_x, margin_top),
|
||||
]:
|
||||
if vertical:
|
||||
suffixes = ['top', 'middle', 'bottom']
|
||||
fixed_outer, variable_outer = containing_block
|
||||
else:
|
||||
suffixes = ['left', 'center', 'right']
|
||||
variable_outer, fixed_outer = containing_block
|
||||
side_boxes = [
|
||||
make_box(f'@{prefix}-{suffix}', containing_block)
|
||||
for suffix in suffixes]
|
||||
if not any(box.is_generated for box in side_boxes):
|
||||
continue
|
||||
# We need the three boxes together for the variable dimension:
|
||||
compute_variable_dimension(
|
||||
context, side_boxes, vertical, variable_outer)
|
||||
for box, offset in zip(side_boxes, [0, 0.5, 1]):
|
||||
if not box.is_generated:
|
||||
continue
|
||||
box.position_x = position_x
|
||||
box.position_y = position_y
|
||||
if vertical:
|
||||
box.position_y += offset * (
|
||||
variable_outer - box.margin_height())
|
||||
else:
|
||||
box.position_x += offset * (
|
||||
variable_outer - box.margin_width())
|
||||
compute_fixed_dimension(
|
||||
context, box, fixed_outer, not vertical,
|
||||
prefix in ['top', 'left'])
|
||||
generated_boxes.append(box)
|
||||
|
||||
# Corner boxes
|
||||
|
||||
for at_keyword, cb_width, cb_height, position_x, position_y in [
|
||||
('@top-left-corner', margin_left, margin_top, 0, 0),
|
||||
('@top-right-corner', margin_right, margin_top, page_end_x, 0),
|
||||
('@bottom-left-corner', margin_left, margin_bottom, 0, page_end_y),
|
||||
('@bottom-right-corner', margin_right, margin_bottom,
|
||||
page_end_x, page_end_y),
|
||||
]:
|
||||
box = make_box(at_keyword, (cb_width, cb_height))
|
||||
if not box.is_generated:
|
||||
continue
|
||||
box.position_x = position_x
|
||||
box.position_y = position_y
|
||||
compute_fixed_dimension(
|
||||
context, box, cb_height, True, 'top' in at_keyword)
|
||||
compute_fixed_dimension(
|
||||
context, box, cb_width, False, 'left' in at_keyword)
|
||||
generated_boxes.append(box)
|
||||
|
||||
for box in generated_boxes:
|
||||
yield margin_box_content_layout(context, page, box)
|
||||
|
||||
|
||||
def margin_box_content_layout(context, page, box):
|
||||
"""Layout a margin box’s content once the box has dimensions."""
|
||||
positioned_boxes = []
|
||||
box, resume_at, next_page, _, _ = block_container_layout(
|
||||
context, box, bottom_space=-float('inf'), skip_stack=None,
|
||||
page_is_empty=True, absolute_boxes=positioned_boxes,
|
||||
fixed_boxes=positioned_boxes, adjoining_margins=None, discard=False)
|
||||
assert resume_at is None
|
||||
for absolute_box in positioned_boxes:
|
||||
absolute_layout(
|
||||
context, absolute_box, box, positioned_boxes, bottom_space=0,
|
||||
skip_stack=None)
|
||||
|
||||
vertical_align = box.style['vertical_align']
|
||||
# Every other value is read as 'top', ie. no change.
|
||||
if vertical_align in ('middle', 'bottom') and box.children:
|
||||
first_child = box.children[0]
|
||||
last_child = box.children[-1]
|
||||
top = first_child.position_y
|
||||
# Not always exact because floating point errors
|
||||
# assert top == box.content_box_y()
|
||||
bottom = last_child.position_y + last_child.margin_height()
|
||||
content_height = bottom - top
|
||||
offset = box.height - content_height
|
||||
if vertical_align == 'middle':
|
||||
offset /= 2
|
||||
for child in box.children:
|
||||
child.translate(0, offset)
|
||||
return box
|
||||
|
||||
|
||||
def page_width_or_height(box, containing_block_size):
|
||||
"""Take a :class:`OrientedBox` object and set either width, margin-left
|
||||
and margin-right; or height, margin-top and margin-bottom.
|
||||
|
||||
"The width and horizontal margins of the page box are then calculated
|
||||
exactly as for a non-replaced block element in normal flow. The height
|
||||
and vertical margins of the page box are calculated analogously (instead
|
||||
of using the block height formulas). In both cases if the values are
|
||||
over-constrained, instead of ignoring any margins, the containing block
|
||||
is resized to coincide with the margin edges of the page box."
|
||||
|
||||
http://dev.w3.org/csswg/css3-page/#page-box-page-rule
|
||||
http://www.w3.org/TR/CSS21/visudet.html#blockwidth
|
||||
|
||||
"""
|
||||
remaining = containing_block_size - box.padding_plus_border
|
||||
if box.inner == 'auto':
|
||||
if box.margin_a == 'auto':
|
||||
box.margin_a = 0
|
||||
if box.margin_b == 'auto':
|
||||
box.margin_b = 0
|
||||
box.inner = remaining - box.margin_a - box.margin_b
|
||||
elif box.margin_a == box.margin_b == 'auto':
|
||||
box.margin_a = box.margin_b = (remaining - box.inner) / 2
|
||||
elif box.margin_a == 'auto':
|
||||
box.margin_a = remaining - box.inner - box.margin_b
|
||||
elif box.margin_b == 'auto':
|
||||
box.margin_b = remaining - box.inner - box.margin_a
|
||||
box.restore_box_attributes()
|
||||
|
||||
|
||||
@handle_min_max_width
|
||||
def page_width(box, context, containing_block_width):
|
||||
page_width_or_height(HorizontalBox(context, box), containing_block_width)
|
||||
|
||||
|
||||
@handle_min_max_height
|
||||
def page_height(box, context, containing_block_height):
|
||||
page_width_or_height(VerticalBox(context, box), containing_block_height)
|
||||
|
||||
|
||||
def make_page(context, root_box, page_type, resume_at, page_number,
|
||||
page_state):
|
||||
"""Take just enough content from the beginning to fill one page.
|
||||
|
||||
Return ``(page, finished)``. ``page`` is a laid out PageBox object
|
||||
and ``resume_at`` indicates where in the document to start the next page,
|
||||
or is ``None`` if this was the last page.
|
||||
|
||||
:param page_number: integer, start at 1 for the first page
|
||||
:param resume_at: as returned by ``make_page()`` for the previous page,
|
||||
or ``None`` for the first page.
|
||||
|
||||
"""
|
||||
style = context.style_for(page_type)
|
||||
|
||||
# Propagated from the root or <body>.
|
||||
style['overflow'] = root_box.viewport_overflow
|
||||
page = boxes.PageBox(page_type, style)
|
||||
|
||||
device_size = page.style['size']
|
||||
|
||||
resolve_percentages(page, device_size)
|
||||
|
||||
page.position_x = 0
|
||||
page.position_y = 0
|
||||
cb_width, cb_height = device_size
|
||||
page_width(page, context, cb_width)
|
||||
page_height(page, context, cb_height)
|
||||
|
||||
root_box.position_x = page.content_box_x()
|
||||
root_box.position_y = page.content_box_y()
|
||||
context.page_bottom = root_box.position_y + page.height
|
||||
initial_containing_block = page
|
||||
|
||||
footnote_area_style = context.style_for(page_type, '@footnote')
|
||||
footnote_area = boxes.FootnoteAreaBox(page, footnote_area_style)
|
||||
resolve_percentages(footnote_area, page)
|
||||
footnote_area.position_x = page.content_box_x()
|
||||
footnote_area.position_y = context.page_bottom
|
||||
|
||||
if page_type.blank:
|
||||
previous_resume_at = resume_at
|
||||
root_box = root_box.copy_with_children([])
|
||||
|
||||
# TODO: handle cases where the root element is something else.
|
||||
# See http://www.w3.org/TR/CSS21/visuren.html#dis-pos-flo
|
||||
assert isinstance(root_box, (boxes.BlockBox, boxes.FlexContainerBox))
|
||||
context.create_block_formatting_context()
|
||||
context.current_page = page_number
|
||||
context.current_page_footnotes = context.reported_footnotes.copy()
|
||||
context.current_footnote_area = footnote_area
|
||||
|
||||
if context.reported_footnotes:
|
||||
footnote_area.children = tuple(context.reported_footnotes)
|
||||
context.reported_footnotes = []
|
||||
reported_footnote_area = build.create_anonymous_boxes(
|
||||
footnote_area.deepcopy())
|
||||
reported_footnote_area, _, _, _, _ = block_level_layout(
|
||||
context, reported_footnote_area, -float('inf'), None,
|
||||
footnote_area.page, True, [], [], [], False)
|
||||
footnote_area.height = reported_footnote_area.height
|
||||
context.page_bottom -= reported_footnote_area.margin_height()
|
||||
|
||||
page_is_empty = True
|
||||
adjoining_margins = []
|
||||
positioned_boxes = [] # Mixed absolute and fixed
|
||||
out_of_flow_boxes = []
|
||||
broken_out_of_flow = []
|
||||
for box, containing_block, skip_stack in context.broken_out_of_flow:
|
||||
box.position_y = 0
|
||||
if box.is_floated():
|
||||
out_of_flow_box, out_of_flow_resume_at = float_layout(
|
||||
context, box, containing_block, positioned_boxes,
|
||||
positioned_boxes, 0, skip_stack)
|
||||
else:
|
||||
assert box.is_absolutely_positioned()
|
||||
out_of_flow_box, out_of_flow_resume_at = absolute_box_layout(
|
||||
context, box, containing_block, positioned_boxes, 0,
|
||||
skip_stack)
|
||||
out_of_flow_boxes.append(out_of_flow_box)
|
||||
if out_of_flow_resume_at:
|
||||
broken_out_of_flow.append(
|
||||
(box, containing_block, out_of_flow_resume_at))
|
||||
context.broken_out_of_flow = broken_out_of_flow
|
||||
root_box, resume_at, next_page, _, _ = block_level_layout(
|
||||
context, root_box, 0, resume_at, initial_containing_block,
|
||||
page_is_empty, positioned_boxes, positioned_boxes, adjoining_margins,
|
||||
discard=False)
|
||||
assert root_box
|
||||
root_box.children = tuple(out_of_flow_boxes) + root_box.children
|
||||
|
||||
page.fixed_boxes = [
|
||||
placeholder._box for placeholder in positioned_boxes
|
||||
if placeholder._box.style['position'] == 'fixed']
|
||||
for absolute_box in positioned_boxes:
|
||||
absolute_layout(
|
||||
context, absolute_box, page, positioned_boxes, bottom_space=0,
|
||||
skip_stack=None)
|
||||
|
||||
footnote_area = build.create_anonymous_boxes(footnote_area.deepcopy())
|
||||
footnote_area, _, _, _, _ = block_level_layout(
|
||||
context, footnote_area, -float('inf'), None, footnote_area.page,
|
||||
True, [], [], [], False)
|
||||
footnote_area.translate(dy=-footnote_area.margin_height())
|
||||
|
||||
context.finish_block_formatting_context(root_box)
|
||||
|
||||
page.children = [root_box, footnote_area]
|
||||
descendants = page.descendants()
|
||||
|
||||
# Update page counter values
|
||||
_standardize_page_based_counters(style, None)
|
||||
build.update_counters(page_state, style)
|
||||
page_counter_values = page_state[1]
|
||||
# page_counter_values will be cached in the page_maker
|
||||
|
||||
target_collector = context.target_collector
|
||||
page_maker = context.page_maker
|
||||
|
||||
# remake_state tells the make_all_pages-loop in layout_document()
|
||||
# whether and what to re-make.
|
||||
remake_state = page_maker[page_number - 1][-1]
|
||||
|
||||
# Evaluate and cache page values only once (for the first LineBox)
|
||||
# otherwise we suffer endless loops when the target/pseudo-element
|
||||
# spans across multiple pages
|
||||
cached_anchors = []
|
||||
cached_lookups = []
|
||||
for (_, _, _, _, x_remake_state) in page_maker[:page_number - 1]:
|
||||
cached_anchors.extend(x_remake_state.get('anchors', []))
|
||||
cached_lookups.extend(x_remake_state.get('content_lookups', []))
|
||||
|
||||
for child in descendants:
|
||||
# Cache target's page counters
|
||||
anchor = child.style['anchor']
|
||||
if anchor and anchor not in cached_anchors:
|
||||
remake_state['anchors'].append(anchor)
|
||||
cached_anchors.append(anchor)
|
||||
# Re-make of affected targeting boxes is inclusive
|
||||
target_collector.cache_target_page_counters(
|
||||
anchor, page_counter_values, page_number - 1, page_maker)
|
||||
|
||||
# string-set and bookmark-labels don't create boxes, only `content`
|
||||
# requires another call to make_page. There is maximum one 'content'
|
||||
# item per box.
|
||||
# TODO: remove attribute or set a default value in Box class
|
||||
if hasattr(child, 'missing_link'):
|
||||
# A CounterLookupItem exists for the css-token 'content'
|
||||
counter_lookup = target_collector.counter_lookup_items.get(
|
||||
(child.missing_link, 'content'))
|
||||
else:
|
||||
counter_lookup = None
|
||||
|
||||
# Resolve missing (page based) counters
|
||||
if counter_lookup is not None:
|
||||
call_parse_again = False
|
||||
|
||||
# Prevent endless loops
|
||||
counter_lookup_id = id(counter_lookup)
|
||||
refresh_missing_counters = counter_lookup_id not in cached_lookups
|
||||
if refresh_missing_counters:
|
||||
remake_state['content_lookups'].append(counter_lookup_id)
|
||||
cached_lookups.append(counter_lookup_id)
|
||||
counter_lookup.page_maker_index = page_number - 1
|
||||
|
||||
# Step 1: page based back-references
|
||||
# Marked as pending by target_collector.cache_target_page_counters
|
||||
if counter_lookup.pending:
|
||||
if (page_counter_values !=
|
||||
counter_lookup.cached_page_counter_values):
|
||||
counter_lookup.cached_page_counter_values = copy.deepcopy(
|
||||
page_counter_values)
|
||||
counter_lookup.pending = False
|
||||
call_parse_again = True
|
||||
|
||||
# Step 2: local counters
|
||||
# If the box mixed-in page counters changed, update the content
|
||||
# and cache the new values.
|
||||
missing_counters = counter_lookup.missing_counters
|
||||
if missing_counters:
|
||||
if 'pages' in missing_counters:
|
||||
remake_state['pages_wanted'] = True
|
||||
if refresh_missing_counters and page_counter_values != \
|
||||
counter_lookup.cached_page_counter_values:
|
||||
counter_lookup.cached_page_counter_values = \
|
||||
copy.deepcopy(page_counter_values)
|
||||
for counter_name in missing_counters:
|
||||
counter_value = page_counter_values.get(
|
||||
counter_name, None)
|
||||
if counter_value is not None:
|
||||
call_parse_again = True
|
||||
# no need to loop them all
|
||||
break
|
||||
|
||||
# Step 3: targeted counters
|
||||
target_missing = counter_lookup.missing_target_counters
|
||||
for anchor_name, missed_counters in target_missing.items():
|
||||
if 'pages' not in missed_counters:
|
||||
continue
|
||||
# Adjust 'pages_wanted'
|
||||
item = target_collector.target_lookup_items.get(
|
||||
anchor_name, None)
|
||||
page_maker_index = item.page_maker_index
|
||||
if page_maker_index >= 0 and anchor_name in cached_anchors:
|
||||
page_maker[page_maker_index][-1]['pages_wanted'] = True
|
||||
# 'content_changed' is triggered in
|
||||
# targets.cache_target_page_counters()
|
||||
|
||||
if call_parse_again:
|
||||
remake_state['content_changed'] = True
|
||||
counter_lookup.parse_again(page_counter_values)
|
||||
|
||||
if page_type.blank:
|
||||
resume_at = previous_resume_at
|
||||
|
||||
return page, resume_at, next_page
|
||||
|
||||
|
||||
def set_page_type_computed_styles(page_type, html, style_for):
|
||||
"""Set style for page types and pseudo-types matching ``page_type``."""
|
||||
style_for.add_page_declarations(page_type)
|
||||
|
||||
# Apply style for page
|
||||
style_for.set_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)
|
||||
|
||||
# Apply style for page pseudo-elements (margin boxes)
|
||||
for element, pseudo_type in style_for.get_cascaded_styles():
|
||||
if pseudo_type and element == page_type:
|
||||
style_for.set_computed_styles(
|
||||
element, pseudo_type=pseudo_type,
|
||||
# The pseudo-element inherits from the element.
|
||||
root=html.etree_element, parent=element,
|
||||
base_url=html.base_url)
|
||||
|
||||
|
||||
def remake_page(index, context, root_box, html):
|
||||
"""Return one laid out page without margin boxes.
|
||||
|
||||
Start with the initial values from ``context.page_maker[index]``.
|
||||
The resulting values / initial values for the next page are stored in
|
||||
the ``page_maker``.
|
||||
|
||||
As the function's name suggests: the plan is not to make all pages
|
||||
repeatedly when a missing counter was resolved, but rather re-make the
|
||||
single page where the ``content_changed`` happened.
|
||||
|
||||
"""
|
||||
page_maker = context.page_maker
|
||||
(initial_resume_at, initial_next_page, right_page, initial_page_state,
|
||||
remake_state) = page_maker[index]
|
||||
|
||||
# PageType for current page, values for page_maker[index + 1].
|
||||
# Don't modify actual page_maker[index] values!
|
||||
# TODO: should we store (and reuse) page_type in the page_maker?
|
||||
page_state = copy.deepcopy(initial_page_state)
|
||||
next_page_name = initial_next_page['page']
|
||||
first = index == 0
|
||||
if initial_next_page['break'] in ('left', 'right'):
|
||||
next_page_side = initial_next_page['break']
|
||||
elif initial_next_page['break'] in ('recto', 'verso'):
|
||||
direction_ltr = root_box.style['direction'] == 'ltr'
|
||||
break_verso = initial_next_page['break'] == 'verso'
|
||||
next_page_side = 'right' if direction_ltr ^ break_verso else 'left'
|
||||
else:
|
||||
next_page_side = None
|
||||
blank = bool(
|
||||
(next_page_side == 'left' and right_page) or
|
||||
(next_page_side == 'right' and not right_page) or
|
||||
(context.reported_footnotes and initial_resume_at is None))
|
||||
if blank:
|
||||
next_page_name = ''
|
||||
side = 'right' if right_page else 'left'
|
||||
page_type = PageType(side, blank, first, index, name=next_page_name)
|
||||
set_page_type_computed_styles(page_type, html, context.style_for)
|
||||
|
||||
context.forced_break = (
|
||||
initial_next_page['break'] != 'any' or initial_next_page['page'])
|
||||
context.margin_clearance = False
|
||||
|
||||
# make_page wants a page_number of index + 1
|
||||
page_number = index + 1
|
||||
page, resume_at, next_page = make_page(
|
||||
context, root_box, page_type, initial_resume_at, page_number,
|
||||
page_state)
|
||||
assert next_page
|
||||
if blank:
|
||||
next_page['page'] = initial_next_page['page']
|
||||
right_page = not right_page
|
||||
|
||||
# Check whether we need to append or update the next page_maker item
|
||||
if index + 1 >= len(page_maker):
|
||||
# New page
|
||||
page_maker_next_changed = True
|
||||
else:
|
||||
# Check whether something changed
|
||||
# TODO: Find what we need to compare. Is resume_at enough?
|
||||
(next_resume_at, next_next_page, next_right_page,
|
||||
next_page_state, _) = page_maker[index + 1]
|
||||
page_maker_next_changed = (
|
||||
next_resume_at != resume_at or
|
||||
next_next_page != next_page or
|
||||
next_right_page != right_page or
|
||||
next_page_state != page_state)
|
||||
|
||||
if page_maker_next_changed:
|
||||
# Reset remake_state
|
||||
remake_state = {
|
||||
'content_changed': False,
|
||||
'pages_wanted': False,
|
||||
'anchors': [],
|
||||
'content_lookups': [],
|
||||
}
|
||||
# Setting content_changed to True ensures remake.
|
||||
# If resume_at is None (last page) it must be False to prevent endless
|
||||
# loops and list index out of range (see #794).
|
||||
remake_state['content_changed'] = resume_at is not None
|
||||
# page_state is already a deepcopy
|
||||
item = resume_at, next_page, right_page, page_state, remake_state
|
||||
if index + 1 >= len(page_maker):
|
||||
page_maker.append(item)
|
||||
else:
|
||||
page_maker[index + 1] = item
|
||||
|
||||
return page, resume_at
|
||||
|
||||
|
||||
def make_all_pages(context, root_box, html, pages):
|
||||
"""Return a list of laid out pages without margin boxes.
|
||||
|
||||
Re-make pages only if necessary.
|
||||
|
||||
"""
|
||||
i = 0
|
||||
while True:
|
||||
remake_state = context.page_maker[i][-1]
|
||||
if (len(pages) == 0 or
|
||||
remake_state['content_changed'] or
|
||||
remake_state['pages_wanted']):
|
||||
PROGRESS_LOGGER.info('Step 5 - Creating layout - Page %d', i + 1)
|
||||
# Reset remake_state
|
||||
remake_state['content_changed'] = False
|
||||
remake_state['pages_wanted'] = False
|
||||
remake_state['anchors'] = []
|
||||
remake_state['content_lookups'] = []
|
||||
page, resume_at = remake_page(i, context, root_box, html)
|
||||
yield page
|
||||
else:
|
||||
PROGRESS_LOGGER.info(
|
||||
'Step 5 - Creating layout - Page %d (up-to-date)', i + 1)
|
||||
resume_at = context.page_maker[i + 1][0]
|
||||
yield pages[i]
|
||||
|
||||
i += 1
|
||||
if resume_at is None and not context.reported_footnotes:
|
||||
# Throw away obsolete pages
|
||||
context.page_maker = context.page_maker[:i + 1]
|
||||
return
|
||||
975
venv/Lib/site-packages/weasyprint/layout/table.py
Normal file
975
venv/Lib/site-packages/weasyprint/layout/table.py
Normal file
@@ -0,0 +1,975 @@
|
||||
"""
|
||||
weasyprint.layout.tables
|
||||
------------------------
|
||||
|
||||
Layout for tables and internal table boxes.
|
||||
|
||||
"""
|
||||
|
||||
from ..formatting_structure import boxes
|
||||
from ..logger import LOGGER
|
||||
from .percent import resolve_one_percentage, resolve_percentages
|
||||
from .preferred import max_content_width, table_and_columns_preferred_widths
|
||||
|
||||
|
||||
def table_layout(context, table, bottom_space, skip_stack, containing_block,
|
||||
page_is_empty, absolute_boxes, fixed_boxes):
|
||||
"""Layout for a table box."""
|
||||
from .block import (
|
||||
block_container_layout, block_level_page_break,
|
||||
find_earlier_page_break)
|
||||
|
||||
table.remove_decoration(start=skip_stack is not None, end=False)
|
||||
|
||||
column_widths = table.column_widths
|
||||
|
||||
if table.style['border_collapse'] == 'separate':
|
||||
border_spacing_x, border_spacing_y = table.style['border_spacing']
|
||||
else:
|
||||
border_spacing_x = 0
|
||||
border_spacing_y = 0
|
||||
|
||||
column_positions = table.column_positions = []
|
||||
rows_left_x = table.content_box_x() + border_spacing_x
|
||||
if table.style['direction'] == 'ltr':
|
||||
position_x = table.content_box_x()
|
||||
rows_x = position_x + border_spacing_x
|
||||
for width in column_widths:
|
||||
position_x += border_spacing_x
|
||||
column_positions.append(position_x)
|
||||
position_x += width
|
||||
rows_width = position_x - rows_x
|
||||
else:
|
||||
position_x = table.content_box_x() + table.width
|
||||
rows_x = position_x - border_spacing_x
|
||||
for width in column_widths:
|
||||
position_x -= border_spacing_x
|
||||
position_x -= width
|
||||
column_positions.append(position_x)
|
||||
rows_width = rows_x - position_x
|
||||
|
||||
if table.style['border_collapse'] == 'collapse':
|
||||
table.skip_cell_border_top = False
|
||||
table.skip_cell_border_bottom = False
|
||||
split_cells = False
|
||||
if skip_stack:
|
||||
(skipped_groups, group_skip_stack), = skip_stack.items()
|
||||
if group_skip_stack:
|
||||
(skipped_rows, cells_skip_stack), = group_skip_stack.items()
|
||||
if cells_skip_stack:
|
||||
split_cells = True
|
||||
else:
|
||||
skipped_rows = 0
|
||||
for group in table.children[:skipped_groups]:
|
||||
skipped_rows += len(group.children)
|
||||
else:
|
||||
skipped_rows = 0
|
||||
if not split_cells:
|
||||
_, horizontal_borders = table.collapsed_border_grid
|
||||
if horizontal_borders:
|
||||
table.border_top_width = max(
|
||||
width for _, (_, width, _)
|
||||
in horizontal_borders[skipped_rows]) / 2
|
||||
|
||||
# Make this a sub-function so that many local variables like rows_x
|
||||
# don't need to be passed as parameters.
|
||||
def group_layout(group, position_y, bottom_space, page_is_empty,
|
||||
skip_stack):
|
||||
resume_at = None
|
||||
next_page = {'break': 'any', 'page': None}
|
||||
original_page_is_empty = page_is_empty
|
||||
resolve_percentages(group, containing_block=table)
|
||||
group.position_x = rows_left_x
|
||||
group.position_y = position_y
|
||||
group.width = rows_width
|
||||
new_group_children = []
|
||||
# For each rows, cells for which this is the last row (with rowspan)
|
||||
ending_cells_by_row = [[] for row in group.children]
|
||||
|
||||
is_group_start = skip_stack is None
|
||||
if is_group_start:
|
||||
skip = 0
|
||||
else:
|
||||
(skip, skip_stack), = skip_stack.items()
|
||||
for index_row, row in enumerate(group.children[skip:], start=skip):
|
||||
row.index = index_row
|
||||
|
||||
if new_group_children:
|
||||
page_break = block_level_page_break(
|
||||
new_group_children[-1], row)
|
||||
if page_break in ('page', 'recto', 'verso', 'left', 'right'):
|
||||
next_page['break'] = page_break
|
||||
resume_at = {index_row: None}
|
||||
break
|
||||
|
||||
resolve_percentages(row, containing_block=table)
|
||||
row.position_x = rows_left_x
|
||||
row.position_y = position_y
|
||||
row.width = rows_width
|
||||
# Place cells at the top of the row and layout their content
|
||||
new_row_children = []
|
||||
for index_cell, cell in enumerate(row.children):
|
||||
spanned_widths = column_widths[cell.grid_x:][:cell.colspan]
|
||||
# In the fixed layout the grid width is set by cells in
|
||||
# the first row and column elements.
|
||||
# This may be less than the previous value of cell.colspan
|
||||
# if that would bring the cell beyond the grid width.
|
||||
cell.colspan = len(spanned_widths)
|
||||
if cell.colspan == 0:
|
||||
# The cell is entierly beyond the grid width, remove it
|
||||
# entierly. Subsequent cells in the same row have greater
|
||||
# grid_x, so they are beyond too.
|
||||
cell_index = row.children.index(cell)
|
||||
ignored_cells = row.children[cell_index:]
|
||||
LOGGER.warning(
|
||||
'This table row has more columns than the table, '
|
||||
f'ignored {len(ignored_cells)} cells: {ignored_cells}')
|
||||
break
|
||||
resolve_percentages(cell, containing_block=table)
|
||||
if table.style['direction'] == 'ltr':
|
||||
cell.position_x = column_positions[cell.grid_x]
|
||||
else:
|
||||
cell.position_x = column_positions[
|
||||
cell.grid_x + cell.colspan - 1]
|
||||
cell.position_y = row.position_y
|
||||
cell.margin_top = 0
|
||||
cell.margin_left = 0
|
||||
cell.width = 0
|
||||
borders_plus_padding = cell.border_width() # with width==0
|
||||
# TODO: we should remove the number of columns with no
|
||||
# originating cells to cell.colspan, see
|
||||
# test_layout_table_auto_49
|
||||
cell.width = (
|
||||
sum(spanned_widths) +
|
||||
border_spacing_x * (cell.colspan - 1) -
|
||||
borders_plus_padding)
|
||||
# The computed height is a minimum
|
||||
cell.computed_height = cell.height
|
||||
cell.height = 'auto'
|
||||
if skip_stack:
|
||||
if index_cell in skip_stack:
|
||||
cell_skip_stack = skip_stack[index_cell]
|
||||
else:
|
||||
cell_skip_stack = {len(cell.children): None}
|
||||
else:
|
||||
cell_skip_stack = None
|
||||
if cell_skip_stack:
|
||||
cell.remove_decoration(start=True, end=False)
|
||||
if table.style['border_collapse'] == 'collapse':
|
||||
table.skip_cell_border_top = True
|
||||
|
||||
# First try to render content as if there was already something
|
||||
# on the page to avoid hitting block_level_layout’s TODO. Then
|
||||
# force to render something if the page is actually empty, or
|
||||
# just draw an empty cell otherwise. See
|
||||
# test_table_break_children_margin.
|
||||
new_cell, cell_resume_at, _, _, _ = block_container_layout(
|
||||
context, cell, bottom_space, cell_skip_stack,
|
||||
page_is_empty=page_is_empty, absolute_boxes=absolute_boxes,
|
||||
fixed_boxes=fixed_boxes, adjoining_margins=None,
|
||||
discard=False)
|
||||
if new_cell is None:
|
||||
cell = cell.copy_with_children([])
|
||||
cell, _, _, _, _ = block_container_layout(
|
||||
context, cell, bottom_space, cell_skip_stack,
|
||||
page_is_empty=True, absolute_boxes=[],
|
||||
fixed_boxes=[], adjoining_margins=None,
|
||||
discard=False)
|
||||
cell_resume_at = {0: None}
|
||||
else:
|
||||
cell = new_cell
|
||||
|
||||
cell.remove_decoration(
|
||||
start=cell_skip_stack is not None,
|
||||
end=cell_resume_at is not None)
|
||||
if cell_resume_at:
|
||||
if resume_at is None:
|
||||
resume_at = {index_row: {}}
|
||||
resume_at[index_row][index_cell] = cell_resume_at
|
||||
cell.empty = not any(
|
||||
child.is_floated() or child.is_in_normal_flow()
|
||||
for child in cell.children)
|
||||
cell.content_height = cell.height
|
||||
if cell.computed_height != 'auto':
|
||||
cell.height = max(cell.height, cell.computed_height)
|
||||
new_row_children.append(cell)
|
||||
|
||||
if resume_at and not page_is_empty:
|
||||
if row.style['break_inside'] in ('avoid', 'avoid-page'):
|
||||
resume_at = {index_row: {}}
|
||||
break
|
||||
|
||||
row = row.copy_with_children(new_row_children)
|
||||
|
||||
# Table height algorithm
|
||||
# http://www.w3.org/TR/CSS21/tables.html#height-layout
|
||||
|
||||
# cells with vertical-align: baseline
|
||||
baseline_cells = []
|
||||
for cell in row.children:
|
||||
vertical_align = cell.style['vertical_align']
|
||||
if vertical_align in ('top', 'middle', 'bottom'):
|
||||
cell.vertical_align = vertical_align
|
||||
else:
|
||||
# Assume 'baseline' for any other value
|
||||
cell.vertical_align = 'baseline'
|
||||
cell.baseline = cell_baseline(cell)
|
||||
baseline_cells.append(cell)
|
||||
if baseline_cells:
|
||||
row.baseline = max(cell.baseline for cell in baseline_cells)
|
||||
for cell in baseline_cells:
|
||||
extra = row.baseline - cell.baseline
|
||||
if cell.baseline != row.baseline and extra:
|
||||
add_top_padding(cell, extra)
|
||||
|
||||
# row height
|
||||
for cell in row.children:
|
||||
ending_cells_by_row[cell.rowspan - 1].append(cell)
|
||||
ending_cells = ending_cells_by_row.pop(0)
|
||||
if ending_cells: # in this row
|
||||
if row.height == 'auto':
|
||||
row_bottom_y = max(
|
||||
cell.position_y + cell.border_height()
|
||||
for cell in ending_cells)
|
||||
row.height = max(row_bottom_y - row.position_y, 0)
|
||||
else:
|
||||
row.height = max(row.height, max(
|
||||
row_cell.height for row_cell in ending_cells))
|
||||
row_bottom_y = row.position_y + row.height
|
||||
else:
|
||||
row_bottom_y = row.position_y
|
||||
row.height = 0
|
||||
|
||||
if not baseline_cells:
|
||||
row.baseline = row_bottom_y
|
||||
|
||||
# Add extra padding to make the cells the same height as the row
|
||||
# and honor vertical-align
|
||||
for cell in ending_cells:
|
||||
cell_bottom_y = cell.position_y + cell.border_height()
|
||||
extra = row_bottom_y - cell_bottom_y
|
||||
if extra:
|
||||
if cell.vertical_align == 'bottom':
|
||||
add_top_padding(cell, extra)
|
||||
elif cell.vertical_align == 'middle':
|
||||
extra /= 2.
|
||||
add_top_padding(cell, extra)
|
||||
cell.padding_bottom += extra
|
||||
else:
|
||||
cell.padding_bottom += extra
|
||||
if cell.computed_height != 'auto':
|
||||
vertical_align_shift = 0
|
||||
if cell.vertical_align == 'middle':
|
||||
vertical_align_shift = (
|
||||
cell.computed_height - cell.content_height) / 2
|
||||
elif cell.vertical_align == 'bottom':
|
||||
vertical_align_shift = (
|
||||
cell.computed_height - cell.content_height)
|
||||
if vertical_align_shift > 0:
|
||||
for child in cell.children:
|
||||
child.translate(dy=vertical_align_shift)
|
||||
|
||||
next_position_y = row.position_y + row.height
|
||||
if resume_at is None:
|
||||
next_position_y += border_spacing_y
|
||||
|
||||
# Break if one cell was broken
|
||||
break_cell = False
|
||||
if resume_at:
|
||||
values, = list(resume_at.values())
|
||||
if len(row.children) == len(values):
|
||||
for cell_resume_at in values.values():
|
||||
if cell_resume_at != {0: None}:
|
||||
break_cell = True
|
||||
break
|
||||
else:
|
||||
# No cell was displayed, give up row
|
||||
next_position_y = float('inf')
|
||||
page_is_empty = False
|
||||
resume_at = None
|
||||
else:
|
||||
break_cell = True
|
||||
|
||||
# Break if this row overflows the page, unless there is no
|
||||
# other content on the page.
|
||||
if not page_is_empty and (
|
||||
next_position_y > context.page_bottom - bottom_space):
|
||||
if new_group_children:
|
||||
previous_row = new_group_children[-1]
|
||||
page_break = block_level_page_break(previous_row, row)
|
||||
if page_break == 'avoid':
|
||||
earlier_page_break = find_earlier_page_break(
|
||||
new_group_children, absolute_boxes, fixed_boxes)
|
||||
if earlier_page_break:
|
||||
new_group_children, resume_at = earlier_page_break
|
||||
break
|
||||
else:
|
||||
resume_at = {index_row: None}
|
||||
break
|
||||
if original_page_is_empty:
|
||||
resume_at = {index_row: None}
|
||||
else:
|
||||
return None, None, next_page
|
||||
break
|
||||
|
||||
new_group_children.append(row)
|
||||
position_y = next_position_y
|
||||
page_is_empty = False
|
||||
skip_stack = None
|
||||
|
||||
if break_cell and table.style['border_collapse'] == 'collapse':
|
||||
table.skip_cell_border_bottom = True
|
||||
|
||||
if break_cell or resume_at:
|
||||
break
|
||||
|
||||
# Do not keep the row group if we made a page break
|
||||
# before any of its rows or with 'avoid'
|
||||
if resume_at and not original_page_is_empty and (
|
||||
group.style['break_inside'] in ('avoid', 'avoid-page') or
|
||||
not new_group_children):
|
||||
return None, None, next_page
|
||||
|
||||
group = group.copy_with_children(new_group_children)
|
||||
group.remove_decoration(
|
||||
start=not is_group_start, end=resume_at is not None)
|
||||
|
||||
# Set missing baselines in a second loop because of rowspan
|
||||
for row in group.children:
|
||||
if row.baseline is None:
|
||||
if row.children:
|
||||
# lowest bottom content edge
|
||||
row.baseline = max(
|
||||
cell.content_box_y() + cell.height
|
||||
for cell in row.children) - row.position_y
|
||||
else:
|
||||
row.baseline = 0
|
||||
group.height = position_y - group.position_y
|
||||
if group.children:
|
||||
# The last border spacing is outside of the group.
|
||||
group.height -= border_spacing_y
|
||||
|
||||
return group, resume_at, next_page
|
||||
|
||||
def body_groups_layout(skip_stack, position_y, bottom_space,
|
||||
page_is_empty):
|
||||
if skip_stack is None:
|
||||
skip = 0
|
||||
else:
|
||||
(skip, skip_stack), = skip_stack.items()
|
||||
new_table_children = []
|
||||
resume_at = None
|
||||
next_page = {'break': 'any', 'page': None}
|
||||
|
||||
for i, group in enumerate(table.children[skip:]):
|
||||
if group.is_header or group.is_footer:
|
||||
continue
|
||||
|
||||
# Index is useless for headers and footers, as we never want to
|
||||
# break pages after the header or before the footer.
|
||||
index_group = i + skip
|
||||
group.index = index_group
|
||||
|
||||
if new_table_children:
|
||||
page_break = block_level_page_break(
|
||||
new_table_children[-1], group)
|
||||
if page_break in ('page', 'recto', 'verso', 'left', 'right'):
|
||||
next_page['break'] = page_break
|
||||
resume_at = {index_group: None}
|
||||
break
|
||||
|
||||
new_group, resume_at, next_page = group_layout(
|
||||
group, position_y, bottom_space, page_is_empty, skip_stack)
|
||||
skip_stack = None
|
||||
|
||||
if new_group is None:
|
||||
if new_table_children:
|
||||
previous_group = new_table_children[-1]
|
||||
page_break = block_level_page_break(previous_group, group)
|
||||
if page_break == 'avoid':
|
||||
earlier_page_break = find_earlier_page_break(
|
||||
new_table_children, absolute_boxes, fixed_boxes)
|
||||
if earlier_page_break is not None:
|
||||
new_table_children, resume_at = earlier_page_break
|
||||
break
|
||||
resume_at = {index_group: None}
|
||||
else:
|
||||
return None, None, next_page, position_y
|
||||
break
|
||||
|
||||
new_table_children.append(new_group)
|
||||
position_y += new_group.height + border_spacing_y
|
||||
page_is_empty = False
|
||||
|
||||
if resume_at:
|
||||
resume_at = {index_group: resume_at}
|
||||
break
|
||||
|
||||
return new_table_children, resume_at, next_page, position_y
|
||||
|
||||
# Layout for row groups, rows and cells
|
||||
position_y = table.content_box_y()
|
||||
if skip_stack is None:
|
||||
position_y += border_spacing_y
|
||||
initial_position_y = position_y
|
||||
table_rows = [
|
||||
child for child in table.children
|
||||
if not child.is_header and not child.is_footer]
|
||||
|
||||
def all_groups_layout():
|
||||
# If the page is not empty, we try to render the header and the footer
|
||||
# on it. If the table does not fit on the page, we try to render it on
|
||||
# the next page.
|
||||
|
||||
# If the page is empty and the header and footer are too big, there
|
||||
# are not rendered. If no row can be rendered because of the header and
|
||||
# the footer, the header and/or the footer are not rendered.
|
||||
|
||||
if page_is_empty:
|
||||
header_footer_bottom_space = bottom_space
|
||||
else:
|
||||
header_footer_bottom_space = -float('inf')
|
||||
|
||||
if table.children and table.children[0].is_header:
|
||||
header = table.children[0]
|
||||
header, resume_at, next_page = group_layout(
|
||||
header, position_y, header_footer_bottom_space,
|
||||
skip_stack=None, page_is_empty=False)
|
||||
if header and not resume_at:
|
||||
header_height = header.height + border_spacing_y
|
||||
else: # Header too big for the page
|
||||
header = None
|
||||
else:
|
||||
header = None
|
||||
|
||||
if table.children and table.children[-1].is_footer:
|
||||
footer = table.children[-1]
|
||||
footer, resume_at, next_page = group_layout(
|
||||
footer, position_y, header_footer_bottom_space,
|
||||
skip_stack=None, page_is_empty=False)
|
||||
if footer and not resume_at:
|
||||
footer_height = footer.height + border_spacing_y
|
||||
else: # Footer too big for the page
|
||||
footer = None
|
||||
else:
|
||||
footer = None
|
||||
|
||||
# Don't remove headers and footers if breaks are avoided in line groups
|
||||
if skip_stack:
|
||||
skip, = skip_stack
|
||||
else:
|
||||
skip = 0
|
||||
avoid_breaks = False
|
||||
for group in table.children[skip:]:
|
||||
if not group.is_header and not group.is_footer:
|
||||
avoid_breaks = (
|
||||
group.style['break_inside'] in ('avoid', 'avoid-page'))
|
||||
break
|
||||
|
||||
if header and footer:
|
||||
# Try with both the header and footer
|
||||
new_table_children, resume_at, next_page, end_position_y = (
|
||||
body_groups_layout(
|
||||
skip_stack, position_y + header_height,
|
||||
bottom_space + footer_height, page_is_empty=avoid_breaks))
|
||||
if new_table_children or not table_rows or not page_is_empty:
|
||||
footer.translate(dy=end_position_y - footer.position_y)
|
||||
end_position_y += footer_height
|
||||
return (
|
||||
header, new_table_children, footer, end_position_y,
|
||||
resume_at, next_page)
|
||||
else:
|
||||
# We could not fit any content, drop the footer
|
||||
footer = None
|
||||
|
||||
if header and not footer:
|
||||
# Try with just the header
|
||||
new_table_children, resume_at, next_page, end_position_y = (
|
||||
body_groups_layout(
|
||||
skip_stack, position_y + header_height, bottom_space,
|
||||
page_is_empty=avoid_breaks))
|
||||
if new_table_children or not table_rows or not page_is_empty:
|
||||
return (
|
||||
header, new_table_children, footer, end_position_y,
|
||||
resume_at, next_page)
|
||||
else:
|
||||
# We could not fit any content, drop the header
|
||||
header = None
|
||||
|
||||
if footer and not header:
|
||||
# Try with just the footer
|
||||
new_table_children, resume_at, next_page, end_position_y = (
|
||||
body_groups_layout(
|
||||
skip_stack, position_y, bottom_space + footer_height,
|
||||
page_is_empty=avoid_breaks))
|
||||
if new_table_children or not table_rows or not page_is_empty:
|
||||
footer.translate(dy=end_position_y - footer.position_y)
|
||||
end_position_y += footer_height
|
||||
return (
|
||||
header, new_table_children, footer, end_position_y,
|
||||
resume_at, next_page)
|
||||
else:
|
||||
# We could not fit any content, drop the footer
|
||||
footer = None
|
||||
|
||||
assert not (header or footer)
|
||||
new_table_children, resume_at, next_page, end_position_y = (
|
||||
body_groups_layout(
|
||||
skip_stack, position_y, bottom_space, page_is_empty))
|
||||
return (
|
||||
header, new_table_children, footer, end_position_y, resume_at,
|
||||
next_page)
|
||||
|
||||
def get_column_cells(table, column):
|
||||
"""Closure getting the column cells."""
|
||||
return lambda: [
|
||||
cell
|
||||
for row_group in table.children
|
||||
for row in row_group.children
|
||||
for cell in row.children
|
||||
if cell.grid_x == column.grid_x]
|
||||
|
||||
header, new_table_children, footer, position_y, resume_at, next_page = (
|
||||
all_groups_layout())
|
||||
|
||||
if new_table_children is None:
|
||||
assert resume_at is None
|
||||
table = None
|
||||
adjoining_margins = []
|
||||
collapsing_through = False
|
||||
return (
|
||||
table, resume_at, next_page, adjoining_margins, collapsing_through)
|
||||
|
||||
table = table.copy_with_children(
|
||||
([header] if header is not None else []) +
|
||||
new_table_children +
|
||||
([footer] if footer is not None else []))
|
||||
table.remove_decoration(
|
||||
start=skip_stack is not None, end=resume_at is not None)
|
||||
if table.style['border_collapse'] == 'collapse':
|
||||
table.skipped_rows = skipped_rows
|
||||
|
||||
# If the height property has a bigger value, just add blank space
|
||||
# below the last row group.
|
||||
table.height = max(
|
||||
table.height if table.height != 'auto' else 0,
|
||||
position_y - table.content_box_y())
|
||||
|
||||
# Layout for column groups and columns
|
||||
columns_height = position_y - initial_position_y
|
||||
if table.children:
|
||||
# The last border spacing is below the columns.
|
||||
columns_height -= border_spacing_y
|
||||
for group in table.column_groups:
|
||||
for column in group.children:
|
||||
resolve_percentages(column, containing_block=table)
|
||||
if column.grid_x < len(column_positions):
|
||||
column.position_x = column_positions[column.grid_x]
|
||||
column.position_y = initial_position_y
|
||||
column.width = column_widths[column.grid_x]
|
||||
column.height = columns_height
|
||||
else:
|
||||
# Ignore extra empty columns
|
||||
column.position_x = 0
|
||||
column.position_y = 0
|
||||
column.width = 0
|
||||
column.height = 0
|
||||
resolve_percentages(group, containing_block=table)
|
||||
column.get_cells = get_column_cells(table, column)
|
||||
first = group.children[0]
|
||||
last = group.children[-1]
|
||||
group.position_x = first.position_x
|
||||
group.position_y = initial_position_y
|
||||
group.width = last.position_x + last.width - first.position_x
|
||||
group.height = columns_height
|
||||
|
||||
if resume_at and not page_is_empty and (
|
||||
table.style['break_inside'] in ('avoid', 'avoid-page')):
|
||||
table = None
|
||||
resume_at = None
|
||||
adjoining_margins = []
|
||||
collapsing_through = False
|
||||
return table, resume_at, next_page, adjoining_margins, collapsing_through
|
||||
|
||||
|
||||
def add_top_padding(box, extra_padding):
|
||||
"""Increase the top padding of a box.
|
||||
|
||||
This also translates the children.
|
||||
|
||||
"""
|
||||
box.padding_top += extra_padding
|
||||
for child in box.children:
|
||||
child.translate(dy=extra_padding)
|
||||
|
||||
|
||||
def fixed_table_layout(box):
|
||||
"""Run the fixed table layout and return a list of column widths.
|
||||
|
||||
http://www.w3.org/TR/CSS21/tables.html#fixed-table-layout
|
||||
|
||||
"""
|
||||
table = box.get_wrapped_table()
|
||||
assert table.width != 'auto'
|
||||
|
||||
all_columns = [column for column_group in table.column_groups
|
||||
for column in column_group.children]
|
||||
if table.children and table.children[0].children:
|
||||
first_rowgroup = table.children[0]
|
||||
first_row_cells = first_rowgroup.children[0].children
|
||||
else:
|
||||
first_row_cells = []
|
||||
num_columns = max(
|
||||
len(all_columns),
|
||||
sum(cell.colspan for cell in first_row_cells)
|
||||
)
|
||||
# ``None`` means not know yet.
|
||||
column_widths = [None] * num_columns
|
||||
|
||||
# `width` on column boxes
|
||||
for i, column in enumerate(all_columns):
|
||||
resolve_one_percentage(column, 'width', table.width)
|
||||
if column.width != 'auto':
|
||||
column_widths[i] = column.width
|
||||
|
||||
if table.style['border_collapse'] == 'separate':
|
||||
border_spacing_x, _ = table.style['border_spacing']
|
||||
else:
|
||||
border_spacing_x = 0
|
||||
|
||||
# `width` on cells of the first row.
|
||||
i = 0
|
||||
for cell in first_row_cells:
|
||||
resolve_percentages(cell, table)
|
||||
if cell.width != 'auto':
|
||||
width = cell.border_width()
|
||||
width -= border_spacing_x * (cell.colspan - 1)
|
||||
# In the general case, this width affects several columns (through
|
||||
# colspan) some of which already have a width. Subtract these
|
||||
# known widths and divide among remaining columns.
|
||||
columns_without_width = [] # and occupied by this cell
|
||||
for j in range(i, i + cell.colspan):
|
||||
if column_widths[j] is None:
|
||||
columns_without_width.append(j)
|
||||
else:
|
||||
width -= column_widths[j]
|
||||
if columns_without_width:
|
||||
width_per_column = width / len(columns_without_width)
|
||||
for j in columns_without_width:
|
||||
column_widths[j] = width_per_column
|
||||
del width
|
||||
i += cell.colspan
|
||||
del i
|
||||
|
||||
# Distribute the remaining space equally on columns that do not have
|
||||
# a width yet.
|
||||
all_border_spacing = border_spacing_x * (num_columns + 1)
|
||||
min_table_width = (sum(w for w in column_widths if w is not None) +
|
||||
all_border_spacing)
|
||||
columns_without_width = [i for i, w in enumerate(column_widths)
|
||||
if w is None]
|
||||
if columns_without_width and table.width >= min_table_width:
|
||||
remaining_width = table.width - min_table_width
|
||||
width_per_column = remaining_width / len(columns_without_width)
|
||||
for i in columns_without_width:
|
||||
column_widths[i] = width_per_column
|
||||
else:
|
||||
# XXX this is bad, but we were given a broken table to work with...
|
||||
for i in columns_without_width:
|
||||
column_widths[i] = 0
|
||||
|
||||
# If the sum is less than the table width,
|
||||
# distribute the remaining space equally
|
||||
extra_width = table.width - sum(column_widths) - all_border_spacing
|
||||
if extra_width <= 0:
|
||||
# substract a negative: widen the table
|
||||
table.width -= extra_width
|
||||
elif num_columns:
|
||||
extra_per_column = extra_width / num_columns
|
||||
column_widths = [w + extra_per_column for w in column_widths]
|
||||
|
||||
# Now we have table.width == sum(column_widths) + all_border_spacing
|
||||
# with possible floating point rounding errors.
|
||||
# (unless there is zero column)
|
||||
table.column_widths = column_widths
|
||||
|
||||
|
||||
def auto_table_layout(context, box, containing_block):
|
||||
"""Run the auto table layout and return a list of column widths.
|
||||
|
||||
http://www.w3.org/TR/CSS21/tables.html#auto-table-layout
|
||||
|
||||
"""
|
||||
table = box.get_wrapped_table()
|
||||
(table_min_content_width, table_max_content_width,
|
||||
column_min_content_widths, column_max_content_widths,
|
||||
column_intrinsic_percentages, constrainedness,
|
||||
total_horizontal_border_spacing, grid) = \
|
||||
table_and_columns_preferred_widths(context, box, outer=False)
|
||||
|
||||
margins = 0
|
||||
if box.margin_left != 'auto':
|
||||
margins += box.margin_left
|
||||
if box.margin_right != 'auto':
|
||||
margins += box.margin_right
|
||||
paddings = table.padding_left + table.padding_right
|
||||
borders = table.border_left_width + table.border_right_width
|
||||
|
||||
cb_width, _ = containing_block
|
||||
available_width = cb_width - margins - paddings - borders
|
||||
|
||||
if table.width == 'auto':
|
||||
if available_width <= table_min_content_width:
|
||||
table.width = table_min_content_width
|
||||
elif available_width < table_max_content_width:
|
||||
table.width = available_width
|
||||
else:
|
||||
table.width = table_max_content_width
|
||||
else:
|
||||
if table.width < table_min_content_width:
|
||||
table.width = table_min_content_width
|
||||
|
||||
if not grid:
|
||||
table.column_widths = []
|
||||
return
|
||||
|
||||
assignable_width = table.width - total_horizontal_border_spacing
|
||||
min_content_guess = column_min_content_widths[:]
|
||||
min_content_percentage_guess = column_min_content_widths[:]
|
||||
min_content_specified_guess = column_min_content_widths[:]
|
||||
max_content_guess = column_max_content_widths[:]
|
||||
guesses = (
|
||||
min_content_guess, min_content_percentage_guess,
|
||||
min_content_specified_guess, max_content_guess)
|
||||
for i in range(len(grid)):
|
||||
if column_intrinsic_percentages[i]:
|
||||
min_content_percentage_guess[i] = max(
|
||||
column_intrinsic_percentages[i] / 100 * assignable_width,
|
||||
column_min_content_widths[i])
|
||||
min_content_specified_guess[i] = min_content_percentage_guess[i]
|
||||
max_content_guess[i] = min_content_percentage_guess[i]
|
||||
elif constrainedness[i]:
|
||||
min_content_specified_guess[i] = column_min_content_widths[i]
|
||||
|
||||
if assignable_width <= sum(max_content_guess):
|
||||
# Default values shouldn't be used, but we never know.
|
||||
# See https://github.com/Kozea/WeasyPrint/issues/770
|
||||
lower_guess = guesses[0]
|
||||
upper_guess = guesses[-1]
|
||||
|
||||
# We have to work around floating point rounding errors here.
|
||||
# The 1e-9 value comes from PEP 485.
|
||||
for guess in guesses:
|
||||
if sum(guess) <= assignable_width * (1 + 1e-9):
|
||||
lower_guess = guess
|
||||
else:
|
||||
break
|
||||
for guess in guesses[::-1]:
|
||||
if sum(guess) >= assignable_width * (1 - 1e-9):
|
||||
upper_guess = guess
|
||||
else:
|
||||
break
|
||||
if upper_guess == lower_guess:
|
||||
# TODO: Uncomment the assert when bugs #770 and #628 are closed
|
||||
# Equivalent to "assert assignable_width == sum(upper_guess)"
|
||||
# assert abs(assignable_width - sum(upper_guess)) <= (
|
||||
# assignable_width * 1e-9)
|
||||
table.column_widths = upper_guess
|
||||
else:
|
||||
added_widths = [
|
||||
upper_guess[i] - lower_guess[i] for i in range(len(grid))]
|
||||
available_ratio = (
|
||||
(assignable_width - sum(lower_guess)) / sum(added_widths))
|
||||
table.column_widths = [
|
||||
lower_guess[i] + added_widths[i] * available_ratio
|
||||
for i in range(len(grid))]
|
||||
else:
|
||||
table.column_widths = max_content_guess
|
||||
excess_width = assignable_width - sum(max_content_guess)
|
||||
excess_width = distribute_excess_width(
|
||||
context, grid, excess_width, table.column_widths, constrainedness,
|
||||
column_intrinsic_percentages, column_max_content_widths)
|
||||
if excess_width:
|
||||
if table_min_content_width < table.width - excess_width:
|
||||
# Reduce the width of the size from the excess width that has
|
||||
# not been distributed.
|
||||
table.width -= excess_width
|
||||
else:
|
||||
# Break rules
|
||||
columns = [i for i, column in enumerate(grid) if any(column)]
|
||||
for i in columns:
|
||||
table.column_widths[i] += excess_width / len(columns)
|
||||
|
||||
|
||||
def table_wrapper_width(context, wrapper, containing_block):
|
||||
"""Find the width of each column and derive the wrapper width."""
|
||||
table = wrapper.get_wrapped_table()
|
||||
resolve_percentages(table, containing_block)
|
||||
|
||||
if table.style['table_layout'] == 'fixed' and table.width != 'auto':
|
||||
fixed_table_layout(wrapper)
|
||||
else:
|
||||
auto_table_layout(context, wrapper, containing_block)
|
||||
|
||||
wrapper.width = table.border_width()
|
||||
|
||||
|
||||
def cell_baseline(cell):
|
||||
"""Return the y position of a cell baseline from the top of its border box.
|
||||
|
||||
See http://www.w3.org/TR/CSS21/tables.html#height-layout
|
||||
|
||||
"""
|
||||
result = find_in_flow_baseline(
|
||||
cell, baseline_types=(boxes.LineBox, boxes.TableRowBox))
|
||||
if result is not None:
|
||||
return result - cell.position_y
|
||||
else:
|
||||
# Default to the bottom of the content area.
|
||||
return cell.border_top_width + cell.padding_top + cell.height
|
||||
|
||||
|
||||
def find_in_flow_baseline(box, last=False, baseline_types=(boxes.LineBox,)):
|
||||
"""Return the absolute y position for the first (or last) in-flow baseline.
|
||||
|
||||
If there’s no in-flow baseline, return None.
|
||||
|
||||
"""
|
||||
# TODO: synthetize baseline when needed
|
||||
# See https://www.w3.org/TR/css-align-3/#synthesize-baseline
|
||||
if isinstance(box, baseline_types):
|
||||
return box.position_y + box.baseline
|
||||
if isinstance(box, boxes.ParentBox) and not isinstance(
|
||||
box, boxes.TableCaptionBox):
|
||||
children = reversed(box.children) if last else box.children
|
||||
for child in children:
|
||||
if child.is_in_normal_flow():
|
||||
result = find_in_flow_baseline(child, last, baseline_types)
|
||||
if result is not None:
|
||||
return result
|
||||
|
||||
|
||||
def distribute_excess_width(context, grid, excess_width, column_widths,
|
||||
constrainedness, column_intrinsic_percentages,
|
||||
column_max_content_widths,
|
||||
column_slice=slice(0, None)):
|
||||
"""Distribute available width to columns.
|
||||
|
||||
Return excess width left when it's impossible without breaking rules.
|
||||
|
||||
See http://dbaron.org/css/intrinsic/#distributetocols
|
||||
|
||||
"""
|
||||
# First group
|
||||
columns = [
|
||||
(i + column_slice.start, column)
|
||||
for i, column in enumerate(grid[column_slice])
|
||||
if not constrainedness[i + column_slice.start] and
|
||||
column_intrinsic_percentages[i + column_slice.start] == 0 and
|
||||
column_max_content_widths[i + column_slice.start] > 0]
|
||||
if columns:
|
||||
current_widths = [column_widths[i] for i, column in columns]
|
||||
differences = [
|
||||
max(0, width[0] - width[1])
|
||||
for width in zip(column_max_content_widths, current_widths)]
|
||||
if sum(differences) > excess_width:
|
||||
differences = [
|
||||
difference / sum(differences) * excess_width
|
||||
for difference in differences]
|
||||
excess_width -= sum(differences)
|
||||
for i, difference in enumerate(differences):
|
||||
column_widths[columns[i][0]] += difference
|
||||
if excess_width <= 0:
|
||||
return
|
||||
|
||||
# Second group
|
||||
columns = [
|
||||
i + column_slice.start for i, column in enumerate(grid[column_slice])
|
||||
if not constrainedness[i + column_slice.start] and
|
||||
column_intrinsic_percentages[i + column_slice.start] == 0]
|
||||
if columns:
|
||||
for i in columns:
|
||||
column_widths[i] += excess_width / len(columns)
|
||||
return
|
||||
|
||||
# Third group
|
||||
columns = [
|
||||
(i + column_slice.start, column)
|
||||
for i, column in enumerate(grid[column_slice])
|
||||
if constrainedness[i + column_slice.start] and
|
||||
column_intrinsic_percentages[i + column_slice.start] == 0 and
|
||||
column_max_content_widths[i + column_slice.start] > 0]
|
||||
if columns:
|
||||
current_widths = [column_widths[i] for i, column in columns]
|
||||
differences = [
|
||||
max(0, width[0] - width[1])
|
||||
for width in zip(column_max_content_widths, current_widths)]
|
||||
if sum(differences) > excess_width:
|
||||
differences = [
|
||||
difference / sum(differences) * excess_width
|
||||
for difference in differences]
|
||||
excess_width -= sum(differences)
|
||||
for i, difference in enumerate(differences):
|
||||
column_widths[columns[i][0]] += difference
|
||||
if excess_width <= 0:
|
||||
return
|
||||
|
||||
# Fourth group
|
||||
columns = [
|
||||
(i + column_slice.start, column)
|
||||
for i, column in enumerate(grid[column_slice])
|
||||
if column_intrinsic_percentages[i + column_slice.start] > 0]
|
||||
if columns:
|
||||
fixed_width = sum(
|
||||
column_widths[j] for j in range(len(grid))
|
||||
if j not in [i for i, column in columns])
|
||||
percentage_width = sum(
|
||||
column_intrinsic_percentages[i]
|
||||
for i, column in columns)
|
||||
if fixed_width and percentage_width >= 100:
|
||||
# Sum of the percentages are greater than 100%
|
||||
ratio = excess_width
|
||||
elif fixed_width == 0:
|
||||
# No fixed width, let's take the whole excess width
|
||||
ratio = excess_width
|
||||
else:
|
||||
ratio = fixed_width / (100 - percentage_width)
|
||||
|
||||
widths = [
|
||||
column_intrinsic_percentages[i] * ratio for i, column in columns]
|
||||
current_widths = [column_widths[i] for i, column in columns]
|
||||
# Allow to reduce the size of the columns to respect the percentage
|
||||
differences = [
|
||||
width[0] - width[1]
|
||||
for width in zip(widths, current_widths)]
|
||||
if sum(differences) > excess_width:
|
||||
differences = [
|
||||
difference / sum(differences) * excess_width
|
||||
for difference in differences]
|
||||
excess_width -= sum(differences)
|
||||
for i, difference in enumerate(differences):
|
||||
column_widths[columns[i][0]] += difference
|
||||
if excess_width <= 0:
|
||||
return
|
||||
|
||||
# Bonus: we've tried our best to distribute the extra size, but we
|
||||
# failed. Instead of blindly distributing the size among all the colums
|
||||
# and breaking all the rules (as said in the draft), let's try to
|
||||
# change the columns with no constraint at all, then resize the table,
|
||||
# and at least break the rules to make the columns fill the table.
|
||||
|
||||
# Fifth group, part 1
|
||||
columns = [
|
||||
i + column_slice.start for i, column in enumerate(grid[column_slice])
|
||||
if any(column) and
|
||||
column_intrinsic_percentages[i + column_slice.start] == 0 and
|
||||
not any(
|
||||
max_content_width(context, cell)
|
||||
for cell in column if cell)]
|
||||
if columns:
|
||||
for i in columns:
|
||||
column_widths[i] += excess_width / len(columns)
|
||||
return
|
||||
|
||||
# Fifth group, part 2, aka abort
|
||||
return excess_width
|
||||
742
venv/Lib/site-packages/weasyprint/svg/__init__.py
Normal file
742
venv/Lib/site-packages/weasyprint/svg/__init__.py
Normal file
@@ -0,0 +1,742 @@
|
||||
"""
|
||||
weasyprint.svg
|
||||
--------------
|
||||
|
||||
Render SVG images.
|
||||
|
||||
"""
|
||||
|
||||
import re
|
||||
from math import cos, hypot, pi, radians, sin, sqrt
|
||||
from xml.etree import ElementTree
|
||||
|
||||
from cssselect2 import ElementWrapper
|
||||
|
||||
from ..urls import get_url_attribute
|
||||
from .bounding_box import bounding_box, is_valid_bounding_box
|
||||
from .css import parse_declarations, parse_stylesheets
|
||||
from .defs import (
|
||||
apply_filters, clip_path, draw_gradient_or_pattern, paint_mask, use)
|
||||
from .images import image, svg
|
||||
from .path import path
|
||||
from .shapes import circle, ellipse, line, polygon, polyline, rect
|
||||
from .text import text
|
||||
from .utils import (
|
||||
PointError, color, normalize, parse_url, preserve_ratio, size, transform)
|
||||
|
||||
TAGS = {
|
||||
'a': text,
|
||||
'circle': circle,
|
||||
'clipPath': clip_path,
|
||||
'ellipse': ellipse,
|
||||
'image': image,
|
||||
'line': line,
|
||||
'path': path,
|
||||
'polyline': polyline,
|
||||
'polygon': polygon,
|
||||
'rect': rect,
|
||||
'svg': svg,
|
||||
'text': text,
|
||||
'textPath': text,
|
||||
'tspan': text,
|
||||
'use': use,
|
||||
}
|
||||
|
||||
NOT_INHERITED_ATTRIBUTES = frozenset((
|
||||
'clip',
|
||||
'clip-path',
|
||||
'filter',
|
||||
'height',
|
||||
'id',
|
||||
'mask',
|
||||
'opacity',
|
||||
'overflow',
|
||||
'rotate',
|
||||
'stop-color',
|
||||
'stop-opacity',
|
||||
'style',
|
||||
'transform',
|
||||
'transform-origin',
|
||||
'viewBox',
|
||||
'width',
|
||||
'x',
|
||||
'y',
|
||||
'dx',
|
||||
'dy',
|
||||
'{http://www.w3.org/1999/xlink}href',
|
||||
'href',
|
||||
))
|
||||
|
||||
COLOR_ATTRIBUTES = frozenset((
|
||||
'fill',
|
||||
'flood-color',
|
||||
'lighting-color',
|
||||
'stop-color',
|
||||
'stroke',
|
||||
))
|
||||
|
||||
DEF_TYPES = frozenset((
|
||||
'clipPath',
|
||||
'filter',
|
||||
'gradient',
|
||||
'image',
|
||||
'marker',
|
||||
'mask',
|
||||
'path',
|
||||
'pattern',
|
||||
))
|
||||
|
||||
|
||||
class Node:
|
||||
"""An SVG document node."""
|
||||
|
||||
def __init__(self, wrapper, style):
|
||||
self._wrapper = wrapper
|
||||
self._etree_node = wrapper.etree_element
|
||||
self._style = style
|
||||
|
||||
self.attrib = wrapper.etree_element.attrib.copy()
|
||||
|
||||
self.vertices = []
|
||||
self.bounding_box = None
|
||||
|
||||
def copy(self):
|
||||
"""Create a deep copy of the node as it was when first created."""
|
||||
return Node(self._wrapper, self._style)
|
||||
|
||||
def get(self, key, default=None):
|
||||
"""Get attribute."""
|
||||
return self.attrib.get(key, default)
|
||||
|
||||
@property
|
||||
def tag(self):
|
||||
"""XML tag name with no namespace."""
|
||||
return self._etree_node.tag.split('}', 1)[-1]
|
||||
|
||||
@property
|
||||
def text(self):
|
||||
"""XML node text."""
|
||||
return self._etree_node.text
|
||||
|
||||
@property
|
||||
def tail(self):
|
||||
"""Text after the XML node."""
|
||||
return self._etree_node.tail
|
||||
|
||||
def __iter__(self):
|
||||
"""Yield node children, handling cascade."""
|
||||
for wrapper in self._wrapper:
|
||||
child = Node(wrapper, self._style)
|
||||
|
||||
# Cascade
|
||||
for key, value in self.attrib.items():
|
||||
if key not in NOT_INHERITED_ATTRIBUTES:
|
||||
if key not in child.attrib:
|
||||
child.attrib[key] = value
|
||||
|
||||
# Apply style attribute
|
||||
style_attr = child.get('style')
|
||||
if style_attr:
|
||||
normal_attr, important_attr = parse_declarations(style_attr)
|
||||
else:
|
||||
normal_attr, important_attr = [], []
|
||||
normal_matcher, important_matcher = self._style
|
||||
normal = [rule[-1] for rule in normal_matcher.match(wrapper)]
|
||||
important = [rule[-1] for rule in important_matcher.match(wrapper)]
|
||||
declarations_lists = (
|
||||
normal, [normal_attr], important, [important_attr])
|
||||
for declarations_list in declarations_lists:
|
||||
for declarations in declarations_list:
|
||||
for name, value in declarations:
|
||||
child.attrib[name] = value.strip()
|
||||
|
||||
# Replace 'currentColor' value
|
||||
for key in COLOR_ATTRIBUTES:
|
||||
if child.get(key) == 'currentColor':
|
||||
child.attrib[key] = child.get('color', 'black')
|
||||
|
||||
# Handle 'inherit' values
|
||||
for key, value in child.attrib.items():
|
||||
if value == 'inherit':
|
||||
child.attrib[key] = self.get(key)
|
||||
|
||||
# Fix text in text tags
|
||||
if child.tag in ('text', 'textPath', 'a'):
|
||||
children, _ = child.text_children(
|
||||
wrapper, trailing_space=True, text_root=True)
|
||||
child._wrapper.etree_children = [
|
||||
child._etree_node for child in children]
|
||||
|
||||
yield child
|
||||
|
||||
def get_viewbox(self):
|
||||
"""Get node viewBox as a tuple of floats."""
|
||||
viewbox = self.get('viewBox')
|
||||
if viewbox:
|
||||
return tuple(
|
||||
float(number) for number in normalize(viewbox).split())
|
||||
|
||||
def get_href(self, base_url):
|
||||
"""Get the href attribute, with or without a namespace."""
|
||||
for attr_name in ('{http://www.w3.org/1999/xlink}href', 'href'):
|
||||
url = get_url_attribute(self, attr_name, base_url)
|
||||
if url:
|
||||
return url
|
||||
|
||||
@staticmethod
|
||||
def process_whitespace(string, preserve):
|
||||
"""Replace newlines by spaces, and merge spaces if not preserved."""
|
||||
# TODO: should be merged with build.process_whitespace
|
||||
if not string:
|
||||
return ''
|
||||
if preserve:
|
||||
return re.sub('[\n\r\t]', ' ', string)
|
||||
else:
|
||||
string = re.sub('[\n\r]', '', string)
|
||||
string = re.sub('\t', ' ', string)
|
||||
return re.sub(' +', ' ', string)
|
||||
|
||||
def get_child(self, id_):
|
||||
"""Get a child with given id in the whole child tree."""
|
||||
for child in self:
|
||||
if child.get('id') == id_:
|
||||
return child
|
||||
grandchild = child.get_child(id_)
|
||||
if grandchild:
|
||||
return grandchild
|
||||
|
||||
def text_children(self, element, trailing_space, text_root=False):
|
||||
"""Handle text node by fixing whitespaces and flattening tails."""
|
||||
children = []
|
||||
space = '{http://www.w3.org/XML/1998/namespace}space'
|
||||
preserve = self.get(space) == 'preserve'
|
||||
self._etree_node.text = self.process_whitespace(
|
||||
element.etree_element.text, preserve)
|
||||
if trailing_space and not preserve:
|
||||
self._etree_node.text = self.text.lstrip(' ')
|
||||
|
||||
original_rotate = [
|
||||
float(i) for i in
|
||||
normalize(self.get('rotate')).strip().split(' ') if i]
|
||||
rotate = original_rotate.copy()
|
||||
if original_rotate:
|
||||
self.pop_rotation(original_rotate, rotate)
|
||||
if self.text:
|
||||
trailing_space = self.text.endswith(' ')
|
||||
for child_element in element.iter_children():
|
||||
child = child_element.etree_element
|
||||
if child.tag in ('{http://www.w3.org/2000/svg}tref', 'tref'):
|
||||
child_node = Node(child_element, self._style)
|
||||
child_node._etree_node.tag = 'tspan'
|
||||
# Retrieve the referenced node and get its flattened text
|
||||
# and remove the node children.
|
||||
child = child_node._etree_node
|
||||
child._etree_node.text = child.flatten()
|
||||
child_element = ElementWrapper.from_xml_root(child)
|
||||
else:
|
||||
child_node = Node(child_element, self._style)
|
||||
child_preserve = child_node.get(space) == 'preserve'
|
||||
child_node._etree_node.text = self.process_whitespace(
|
||||
child.text, child_preserve)
|
||||
child_node.children, trailing_space = child_node.text_children(
|
||||
child_element, trailing_space)
|
||||
trailing_space = child_node.text.endswith(' ')
|
||||
if original_rotate and 'rotate' not in child_node:
|
||||
child_node.pop_rotation(original_rotate, rotate)
|
||||
children.append(child_node)
|
||||
if child.tail:
|
||||
anonymous_etree = ElementTree.Element(
|
||||
'{http://www.w3.org/2000/svg}tspan')
|
||||
anonymous = Node(
|
||||
ElementWrapper.from_xml_root(anonymous_etree), self._style)
|
||||
anonymous._etree_node.text = self.process_whitespace(
|
||||
child.tail, preserve)
|
||||
if original_rotate:
|
||||
anonymous.pop_rotation(original_rotate, rotate)
|
||||
if trailing_space and not preserve:
|
||||
anonymous._etree_node.text = anonymous.text.lstrip(' ')
|
||||
if anonymous.text:
|
||||
trailing_space = anonymous.text.endswith(' ')
|
||||
children.append(anonymous)
|
||||
|
||||
if text_root and not children and not preserve:
|
||||
self._etree_node.text = self.text.rstrip(' ')
|
||||
|
||||
return children, trailing_space
|
||||
|
||||
def flatten(self):
|
||||
"""Flatten text in node and in its children."""
|
||||
flattened_text = [self.text or '']
|
||||
for child in list(self):
|
||||
flattened_text.append(child.flatten())
|
||||
flattened_text.append(child.tail or '')
|
||||
self.remove(child)
|
||||
return ''.join(flattened_text)
|
||||
|
||||
def pop_rotation(self, original_rotate, rotate):
|
||||
"""Merge nested letter rotations."""
|
||||
self.attrib['rotate'] = ' '.join(
|
||||
str(rotate.pop(0) if rotate else original_rotate[-1])
|
||||
for i in range(len(self.text)))
|
||||
|
||||
|
||||
class SVG:
|
||||
"""An SVG document."""
|
||||
|
||||
def __init__(self, tree, url):
|
||||
wrapper = ElementWrapper.from_xml_root(tree)
|
||||
style = parse_stylesheets(wrapper, url)
|
||||
self.tree = Node(wrapper, style)
|
||||
self.url = url
|
||||
|
||||
self.filters = {}
|
||||
self.gradients = {}
|
||||
self.images = {}
|
||||
self.markers = {}
|
||||
self.masks = {}
|
||||
self.patterns = {}
|
||||
self.paths = {}
|
||||
|
||||
self.use_cache = {}
|
||||
|
||||
self.cursor_position = [0, 0]
|
||||
self.cursor_d_position = [0, 0]
|
||||
self.text_path_width = 0
|
||||
|
||||
self.parse_defs(self.tree)
|
||||
|
||||
def get_intrinsic_size(self, font_size):
|
||||
"""Get intrinsic size of the image."""
|
||||
intrinsic_width = self.tree.get('width', '100%')
|
||||
if '%' in intrinsic_width:
|
||||
intrinsic_width = None
|
||||
else:
|
||||
intrinsic_width = size(intrinsic_width, font_size)
|
||||
|
||||
intrinsic_height = self.tree.get('height', '100%')
|
||||
if '%' in intrinsic_height:
|
||||
intrinsic_height = None
|
||||
else:
|
||||
intrinsic_height = size(intrinsic_height, font_size)
|
||||
|
||||
return intrinsic_width, intrinsic_height
|
||||
|
||||
def get_viewbox(self):
|
||||
"""Get document viewBox as a tuple of floats."""
|
||||
return self.tree.get_viewbox()
|
||||
|
||||
def point(self, x, y, font_size):
|
||||
"""Compute size of an x/y or width/height couple."""
|
||||
return (
|
||||
size(x, font_size, self.inner_width),
|
||||
size(y, font_size, self.inner_height))
|
||||
|
||||
def length(self, length, font_size):
|
||||
"""Compute size of an arbirtary attribute."""
|
||||
return size(length, font_size, self.inner_diagonal)
|
||||
|
||||
def draw(self, stream, concrete_width, concrete_height, base_url,
|
||||
url_fetcher, context):
|
||||
"""Draw image on a stream."""
|
||||
self.stream = stream
|
||||
|
||||
self.concrete_width = concrete_width
|
||||
self.concrete_height = concrete_height
|
||||
self.normalized_diagonal = (
|
||||
hypot(concrete_width, concrete_height) / sqrt(2))
|
||||
|
||||
viewbox = self.get_viewbox()
|
||||
if viewbox:
|
||||
self.inner_width, self.inner_height = viewbox[2], viewbox[3]
|
||||
else:
|
||||
self.inner_width = self.concrete_width
|
||||
self.inner_height = self.concrete_height
|
||||
self.inner_diagonal = (
|
||||
hypot(self.inner_width, self.inner_height) / sqrt(2))
|
||||
|
||||
self.base_url = base_url
|
||||
self.url_fetcher = url_fetcher
|
||||
self.context = context
|
||||
|
||||
self.draw_node(self.tree, size('12pt'))
|
||||
|
||||
def draw_node(self, node, font_size, fill_stroke=True):
|
||||
"""Draw a node."""
|
||||
if node.tag == 'defs':
|
||||
return
|
||||
|
||||
# Update font size
|
||||
font_size = size(node.get('font-size', '1em'), font_size, font_size)
|
||||
|
||||
if fill_stroke:
|
||||
self.stream.push_state()
|
||||
|
||||
# Apply filters
|
||||
filter_ = self.filters.get(parse_url(node.get('filter')).fragment)
|
||||
if filter_:
|
||||
apply_filters(self, node, filter_, font_size)
|
||||
|
||||
# Create substream for opacity
|
||||
opacity = float(node.get('opacity', 1))
|
||||
if fill_stroke and 0 <= opacity < 1:
|
||||
original_stream = self.stream
|
||||
box = self.calculate_bounding_box(node, font_size)
|
||||
if is_valid_bounding_box(box):
|
||||
coords = (box[0], box[1], box[0] + box[2], box[1] + box[3])
|
||||
else:
|
||||
coords = (0, 0, self.concrete_width, self.concrete_height)
|
||||
self.stream = self.stream.add_group(coords)
|
||||
|
||||
# Apply transform attribute
|
||||
self.transform(node.get('transform'), font_size)
|
||||
|
||||
# Clip
|
||||
clip_path = parse_url(node.get('clip-path')).fragment
|
||||
if clip_path and clip_path in self.paths:
|
||||
old_ctm = self.stream.ctm
|
||||
clip_path = self.paths[clip_path]
|
||||
if clip_path.get('clipPathUnits') == 'objectBoundingBox':
|
||||
x, y = self.point(node.get('x'), node.get('y'), font_size)
|
||||
width, height = self.point(
|
||||
node.get('width'), node.get('height'), font_size)
|
||||
self.stream.transform(a=width, d=height, e=x, f=y)
|
||||
clip_path._etree_node.tag = 'g'
|
||||
self.draw_node(clip_path, font_size, fill_stroke=False)
|
||||
# At least set the clipping area to an empty path, so that it’s
|
||||
# totally clipped when the clipping path is empty.
|
||||
self.stream.rectangle(0, 0, 0, 0)
|
||||
self.stream.clip()
|
||||
self.stream.end()
|
||||
new_ctm = self.stream.ctm
|
||||
if new_ctm.determinant:
|
||||
self.stream.transform(*(old_ctm @ new_ctm.invert).values)
|
||||
|
||||
# Manage display and visibility
|
||||
display = node.get('display') != 'none'
|
||||
visible = display and (node.get('visibility') != 'hidden')
|
||||
|
||||
# Draw node
|
||||
if visible and node.tag in TAGS:
|
||||
try:
|
||||
TAGS[node.tag](self, node, font_size)
|
||||
except PointError:
|
||||
pass
|
||||
|
||||
# Draw node children
|
||||
if display and node.tag not in DEF_TYPES:
|
||||
for child in node:
|
||||
self.draw_node(child, font_size, fill_stroke)
|
||||
|
||||
# Apply mask
|
||||
mask = self.masks.get(parse_url(node.get('mask')).fragment)
|
||||
if mask:
|
||||
paint_mask(self, node, mask, opacity)
|
||||
|
||||
# Fill and stroke
|
||||
if fill_stroke:
|
||||
self.fill_stroke(node, font_size)
|
||||
|
||||
# Draw markers
|
||||
self.draw_markers(node, font_size, fill_stroke)
|
||||
|
||||
# Apply opacity stream and restore original stream
|
||||
if fill_stroke and 0 <= opacity < 1:
|
||||
group_id = self.stream.id
|
||||
self.stream = original_stream
|
||||
self.stream.set_alpha(opacity, stroke=True, fill=True)
|
||||
self.stream.draw_x_object(group_id)
|
||||
|
||||
# Clean text tag
|
||||
if node.tag == 'text':
|
||||
self.cursor_position = [0, 0]
|
||||
self.cursor_d_position = [0, 0]
|
||||
self.text_path_width = 0
|
||||
|
||||
if fill_stroke:
|
||||
self.stream.pop_state()
|
||||
|
||||
def draw_markers(self, node, font_size, fill_stroke):
|
||||
"""Draw markers defined in a node."""
|
||||
if not node.vertices:
|
||||
return
|
||||
|
||||
markers = {}
|
||||
common_marker = parse_url(node.get('marker')).fragment
|
||||
for position in ('start', 'mid', 'end'):
|
||||
attribute = f'marker-{position}'
|
||||
if attribute in node.attrib:
|
||||
markers[position] = parse_url(node.attrib[attribute]).fragment
|
||||
else:
|
||||
markers[position] = common_marker
|
||||
|
||||
angle1, angle2 = None, None
|
||||
position = 'start'
|
||||
|
||||
while node.vertices:
|
||||
# Calculate position and angle
|
||||
point = node.vertices.pop(0)
|
||||
angles = node.vertices.pop(0) if node.vertices else None
|
||||
if angles:
|
||||
if position == 'start':
|
||||
angle = pi - angles[0]
|
||||
else:
|
||||
angle = (angle2 + pi - angles[0]) / 2
|
||||
angle1, angle2 = angles
|
||||
else:
|
||||
angle = angle2
|
||||
position = 'end'
|
||||
|
||||
# Draw marker
|
||||
marker = markers[position]
|
||||
if not marker:
|
||||
position = 'mid' if angles else 'start'
|
||||
continue
|
||||
|
||||
marker_node = self.markers.get(marker)
|
||||
|
||||
# Calculate position, scale and clipping
|
||||
if 'viewBox' in node.attrib:
|
||||
marker_width, marker_height = svg.point(
|
||||
marker_node.get('markerWidth', 3),
|
||||
marker_node.get('markerHeight', 3),
|
||||
font_size)
|
||||
scale_x, scale_y, translate_x, translate_y = preserve_ratio(
|
||||
svg, marker_node, font_size, marker_width, marker_height)
|
||||
|
||||
clip_x, clip_y, viewbox_width, viewbox_height = (
|
||||
marker_node.get_viewbox())
|
||||
|
||||
align = marker_node.get(
|
||||
'preserveAspectRatio', 'xMidYMid').split(' ')[0]
|
||||
if align == 'none':
|
||||
x_position = y_position = 'min'
|
||||
else:
|
||||
x_position = align[1:4].lower()
|
||||
y_position = align[5:].lower()
|
||||
|
||||
if x_position == 'mid':
|
||||
clip_x += (viewbox_width - marker_width / scale_x) / 2
|
||||
elif x_position == 'max':
|
||||
clip_x += viewbox_width - marker_width / scale_x
|
||||
|
||||
if y_position == 'mid':
|
||||
clip_y += (
|
||||
viewbox_height - marker_height / scale_y) / 2
|
||||
elif y_position == 'max':
|
||||
clip_y += viewbox_height - marker_height / scale_y
|
||||
|
||||
clip_box = (
|
||||
clip_x, clip_y,
|
||||
marker_width / scale_x, marker_height / scale_y)
|
||||
else:
|
||||
marker_width, marker_height = self.point(
|
||||
marker_node.get('markerWidth', 3),
|
||||
marker_node.get('markerHeight', 3),
|
||||
font_size)
|
||||
box = self.calculate_bounding_box(marker_node, font_size)
|
||||
if is_valid_bounding_box(box):
|
||||
scale_x = scale_y = min(
|
||||
marker_width / box[2], marker_height / box[3])
|
||||
else:
|
||||
scale_x = scale_y = 1
|
||||
translate_x, translate_y = self.point(
|
||||
marker_node.get('refX'), marker_node.get('refY'),
|
||||
font_size)
|
||||
clip_box = None
|
||||
|
||||
# Scale
|
||||
if marker_node.get('markerUnits') != 'userSpaceOnUse':
|
||||
scale = self.length(node.get('stroke-width', 1), font_size)
|
||||
scale_x *= scale
|
||||
scale_y *= scale
|
||||
|
||||
# Override angle
|
||||
node_angle = marker_node.get('orient', 0)
|
||||
if node_angle not in ('auto', 'auto-start-reverse'):
|
||||
angle = radians(float(node_angle))
|
||||
elif node_angle == 'auto-start-reverse' and position == 'start':
|
||||
angle += radians(180)
|
||||
|
||||
# Draw marker path
|
||||
for child in marker_node:
|
||||
self.stream.push_state()
|
||||
|
||||
self.stream.transform(
|
||||
scale_x * cos(angle), scale_x * sin(angle),
|
||||
-scale_y * sin(angle), scale_y * cos(angle),
|
||||
*point)
|
||||
self.stream.transform(e=-translate_x, f=-translate_y)
|
||||
|
||||
overflow = marker_node.get('overflow', 'hidden')
|
||||
if clip_box and overflow in ('hidden', 'scroll'):
|
||||
self.stream.push_state()
|
||||
self.stream.rectangle(*clip_box)
|
||||
self.stream.pop_state()
|
||||
self.stream.clip()
|
||||
|
||||
self.draw_node(child, font_size, fill_stroke)
|
||||
self.stream.pop_state()
|
||||
|
||||
position = 'mid' if angles else 'start'
|
||||
|
||||
@staticmethod
|
||||
def get_paint(value):
|
||||
"""Get paint fill or stroke attribute with a color or a URL."""
|
||||
if not value or value == 'none':
|
||||
return None, None
|
||||
|
||||
value = value.strip()
|
||||
match = re.compile(r'(url\(.+\)) *(.*)').search(value)
|
||||
if match:
|
||||
source = parse_url(match.group(1)).fragment
|
||||
color = match.group(2) or None
|
||||
else:
|
||||
source = None
|
||||
color = value or None
|
||||
|
||||
return source, color
|
||||
|
||||
def fill_stroke(self, node, font_size, text=False):
|
||||
"""Paint fill and stroke for a node."""
|
||||
if node.tag in ('text', 'textPath', 'a') and not text:
|
||||
return
|
||||
|
||||
# Get fill data
|
||||
fill_source, fill_color = self.get_paint(node.get('fill', 'black'))
|
||||
fill_opacity = float(node.get('fill-opacity', 1))
|
||||
fill_drawn = draw_gradient_or_pattern(
|
||||
self, node, fill_source, font_size, fill_opacity, stroke=False)
|
||||
if fill_color and not fill_drawn:
|
||||
red, green, blue, alpha = color(fill_color)
|
||||
self.stream.set_color_rgb(red, green, blue)
|
||||
self.stream.set_alpha(alpha * fill_opacity)
|
||||
fill = fill_color or fill_drawn
|
||||
|
||||
# Get stroke data
|
||||
stroke_source, stroke_color = self.get_paint(node.get('stroke'))
|
||||
stroke_opacity = float(node.get('stroke-opacity', 1))
|
||||
stroke_drawn = draw_gradient_or_pattern(
|
||||
self, node, stroke_source, font_size, stroke_opacity, stroke=True)
|
||||
if stroke_color and not stroke_drawn:
|
||||
red, green, blue, alpha = color(stroke_color)
|
||||
self.stream.set_color_rgb(red, green, blue, stroke=True)
|
||||
self.stream.set_alpha(alpha * stroke_opacity, stroke=True)
|
||||
stroke = stroke_color or stroke_drawn
|
||||
stroke_width = self.length(node.get('stroke-width', '1px'), font_size)
|
||||
if stroke_width:
|
||||
self.stream.set_line_width(stroke_width)
|
||||
else:
|
||||
stroke = None
|
||||
|
||||
# Apply dash array
|
||||
dash_array = tuple(
|
||||
self.length(value, font_size) for value in
|
||||
normalize(node.get('stroke-dasharray')).split() if value != 'none')
|
||||
dash_condition = (
|
||||
dash_array and
|
||||
not all(value == 0 for value in dash_array) and
|
||||
not any(value < 0 for value in dash_array))
|
||||
if dash_condition:
|
||||
offset = self.length(node.get('stroke-dashoffset'), font_size)
|
||||
if offset < 0:
|
||||
sum_dashes = sum(float(value) for value in dash_array)
|
||||
offset = sum_dashes - abs(offset) % sum_dashes
|
||||
self.stream.set_dash(dash_array, offset)
|
||||
|
||||
# Apply line cap
|
||||
line_cap = node.get('stroke-linecap', 'butt')
|
||||
if line_cap == 'round':
|
||||
line_cap = 1
|
||||
elif line_cap == 'square':
|
||||
line_cap = 2
|
||||
else:
|
||||
line_cap = 0
|
||||
self.stream.set_line_cap(line_cap)
|
||||
|
||||
# Apply line join
|
||||
line_join = node.get('stroke-linejoin', 'miter')
|
||||
if line_join == 'round':
|
||||
line_join = 1
|
||||
elif line_join == 'bevel':
|
||||
line_join = 2
|
||||
else:
|
||||
line_join = 0
|
||||
self.stream.set_line_join(line_join)
|
||||
|
||||
# Apply miter limit
|
||||
miter_limit = float(node.get('stroke-miterlimit', 4))
|
||||
if miter_limit < 0:
|
||||
miter_limit = 4
|
||||
self.stream.set_miter_limit(miter_limit)
|
||||
|
||||
# Fill and stroke
|
||||
even_odd = node.get('fill-rule') == 'evenodd'
|
||||
if text:
|
||||
if stroke and fill:
|
||||
text_rendering = 2
|
||||
elif stroke:
|
||||
text_rendering = 1
|
||||
elif fill:
|
||||
text_rendering = 0
|
||||
else:
|
||||
text_rendering = 3
|
||||
self.stream.set_text_rendering(text_rendering)
|
||||
else:
|
||||
if fill and stroke:
|
||||
self.stream.fill_and_stroke(even_odd)
|
||||
elif stroke:
|
||||
self.stream.stroke()
|
||||
elif fill:
|
||||
self.stream.fill(even_odd)
|
||||
else:
|
||||
self.stream.end()
|
||||
|
||||
def transform(self, transform_string, font_size):
|
||||
"""Apply a transformation string to the node."""
|
||||
if not transform_string:
|
||||
return
|
||||
|
||||
matrix = transform(transform_string, font_size, self.inner_diagonal)
|
||||
if matrix.determinant:
|
||||
self.stream.transform(*matrix.values)
|
||||
|
||||
def parse_defs(self, node):
|
||||
"""Parse defs included in a tree."""
|
||||
for def_type in DEF_TYPES:
|
||||
if def_type in node.tag.lower() and 'id' in node.attrib:
|
||||
getattr(self, f'{def_type}s')[node.attrib['id']] = node
|
||||
for child in node:
|
||||
self.parse_defs(child)
|
||||
|
||||
def calculate_bounding_box(self, node, font_size, stroke=True):
|
||||
"""Calculate the bounding box of a node."""
|
||||
if stroke or node.bounding_box is None:
|
||||
box = bounding_box(self, node, font_size, stroke)
|
||||
if is_valid_bounding_box(box) and 0 not in box[2:]:
|
||||
if stroke:
|
||||
return box
|
||||
node.bounding_box = box
|
||||
return node.bounding_box
|
||||
|
||||
|
||||
class Pattern(SVG):
|
||||
"""SVG node applied as a pattern."""
|
||||
def __init__(self, tree, svg):
|
||||
self.svg = svg
|
||||
self.tree = tree
|
||||
self.url = svg.url
|
||||
|
||||
self.filters = {}
|
||||
self.gradients = {}
|
||||
self.images = {}
|
||||
self.markers = {}
|
||||
self.masks = {}
|
||||
self.patterns = {}
|
||||
self.paths = {}
|
||||
|
||||
def draw_node(self, node, font_size, fill_stroke=True):
|
||||
# Store the original tree in self.tree when calling draw(), so that we
|
||||
# can reach defs outside the pattern
|
||||
if node == self.tree:
|
||||
self.tree = self.svg.tree
|
||||
super().draw_node(node, font_size, fill_stroke=True)
|
||||
126
venv/Lib/site-packages/weasyprint/svg/shapes.py
Normal file
126
venv/Lib/site-packages/weasyprint/svg/shapes.py
Normal file
@@ -0,0 +1,126 @@
|
||||
"""
|
||||
weasyprint.svg.shapes
|
||||
---------------------
|
||||
|
||||
Draw simple shapes.
|
||||
|
||||
"""
|
||||
|
||||
from math import atan2, pi, sqrt
|
||||
|
||||
from .utils import normalize, point
|
||||
|
||||
|
||||
def circle(svg, node, font_size):
|
||||
"""Draw circle tag."""
|
||||
r = svg.length(node.get('r'), font_size)
|
||||
if not r:
|
||||
return
|
||||
ratio = r / sqrt(pi)
|
||||
cx, cy = svg.point(node.get('cx'), node.get('cy'), font_size)
|
||||
|
||||
svg.stream.move_to(cx + r, cy)
|
||||
svg.stream.curve_to(cx + r, cy + ratio, cx + ratio, cy + r, cx, cy + r)
|
||||
svg.stream.curve_to(cx - ratio, cy + r, cx - r, cy + ratio, cx - r, cy)
|
||||
svg.stream.curve_to(cx - r, cy - ratio, cx - ratio, cy - r, cx, cy - r)
|
||||
svg.stream.curve_to(cx + ratio, cy - r, cx + r, cy - ratio, cx + r, cy)
|
||||
svg.stream.close()
|
||||
|
||||
|
||||
def ellipse(svg, node, font_size):
|
||||
"""Draw ellipse tag."""
|
||||
rx, ry = svg.point(node.get('rx'), node.get('ry'), font_size)
|
||||
if not rx or not ry:
|
||||
return
|
||||
ratio_x = rx / sqrt(pi)
|
||||
ratio_y = ry / sqrt(pi)
|
||||
cx, cy = svg.point(node.get('cx'), node.get('cy'), font_size)
|
||||
|
||||
svg.stream.move_to(cx + rx, cy)
|
||||
svg.stream.curve_to(
|
||||
cx + rx, cy + ratio_y, cx + ratio_x, cy + ry, cx, cy + ry)
|
||||
svg.stream.curve_to(
|
||||
cx - ratio_x, cy + ry, cx - rx, cy + ratio_y, cx - rx, cy)
|
||||
svg.stream.curve_to(
|
||||
cx - rx, cy - ratio_y, cx - ratio_x, cy - ry, cx, cy - ry)
|
||||
svg.stream.curve_to(
|
||||
cx + ratio_x, cy - ry, cx + rx, cy - ratio_y, cx + rx, cy)
|
||||
svg.stream.close()
|
||||
|
||||
|
||||
def rect(svg, node, font_size):
|
||||
"""Draw rect tag."""
|
||||
width, height = svg.point(node.get('width'), node.get('height'), font_size)
|
||||
if width <= 0 or height <= 0:
|
||||
return
|
||||
|
||||
x, y = svg.point(node.get('x'), node.get('y'), font_size)
|
||||
|
||||
rx = node.get('rx')
|
||||
ry = node.get('ry')
|
||||
if rx and ry is None:
|
||||
ry = rx
|
||||
elif ry and rx is None:
|
||||
rx = ry
|
||||
rx, ry = svg.point(rx, ry, font_size)
|
||||
|
||||
if rx == 0 or ry == 0:
|
||||
svg.stream.rectangle(x, y, width, height)
|
||||
return
|
||||
|
||||
if rx > width / 2:
|
||||
rx = width / 2
|
||||
if ry > height / 2:
|
||||
ry = height / 2
|
||||
|
||||
# Inspired by Cairo Cookbook
|
||||
# http://cairographics.org/cookbook/roundedrectangles/
|
||||
ARC_TO_BEZIER = 4 * (2 ** .5 - 1) / 3
|
||||
c1, c2 = ARC_TO_BEZIER * rx, ARC_TO_BEZIER * ry
|
||||
|
||||
svg.stream.move_to(x + rx, y)
|
||||
svg.stream.line_to(x + width - rx, y)
|
||||
svg.stream.curve_to(
|
||||
x + width - rx + c1, y, x + width, y + c2, x + width, y + ry)
|
||||
svg.stream.line_to(x + width, y + height - ry)
|
||||
svg.stream.curve_to(
|
||||
x + width, y + height - ry + c2, x + width + c1 - rx, y + height,
|
||||
x + width - rx, y + height)
|
||||
svg.stream.line_to(x + rx, y + height)
|
||||
svg.stream.curve_to(
|
||||
x + rx - c1, y + height, x, y + height - c2, x, y + height - ry)
|
||||
svg.stream.line_to(x, y + ry)
|
||||
svg.stream.curve_to(x, y + ry - c2, x + rx - c1, y, x + rx, y)
|
||||
svg.stream.close()
|
||||
|
||||
|
||||
def line(svg, node, font_size):
|
||||
"""Draw line tag."""
|
||||
x1, y1 = svg.point(node.get('x1'), node.get('y1'), font_size)
|
||||
x2, y2 = svg.point(node.get('x2'), node.get('y2'), font_size)
|
||||
svg.stream.move_to(x1, y1)
|
||||
svg.stream.line_to(x2, y2)
|
||||
angle = atan2(y2 - y1, x2 - x1)
|
||||
node.vertices = [(x1, y1), (pi - angle, angle), (x2, y2)]
|
||||
|
||||
|
||||
def polygon(svg, node, font_size):
|
||||
"""Draw polygon tag."""
|
||||
polyline(svg, node, font_size)
|
||||
svg.stream.close()
|
||||
|
||||
|
||||
def polyline(svg, node, font_size):
|
||||
"""Draw polyline tag."""
|
||||
points = normalize(node.get('points'))
|
||||
if points:
|
||||
x, y, points = point(svg, points, font_size)
|
||||
svg.stream.move_to(x, y)
|
||||
node.vertices = [(x, y)]
|
||||
while points:
|
||||
x_old, y_old = x, y
|
||||
x, y, points = point(svg, points, font_size)
|
||||
angle = atan2(x - x_old, y - y_old)
|
||||
node.vertices.append((pi - angle, angle))
|
||||
svg.stream.line_to(x, y)
|
||||
node.vertices.append((x, y))
|
||||
Reference in New Issue
Block a user