Update upstream source from tag 'upstream/51'
Update to upstream version '51'
with Debian dir ad9b32e325e40d0ab8a4bf27276fc5655dee6389
Scott Kitterman
4 years ago
0 | 0 | Metadata-Version: 2.1 |
1 | 1 | Name: WeasyPrint |
2 | Version: 50 | |
2 | Version: 51 | |
3 | 3 | Summary: The Awesome Document Factory |
4 | 4 | Home-page: https://weasyprint.org/ |
5 | 5 | Author: Simon Sapin |
46 | 46 | Classifier: Programming Language :: Python :: 3.5 |
47 | 47 | Classifier: Programming Language :: Python :: 3.6 |
48 | 48 | Classifier: Programming Language :: Python :: 3.7 |
49 | Classifier: Programming Language :: Python :: 3.8 | |
49 | 50 | Classifier: Topic :: Internet :: WWW/HTTP |
50 | 51 | Classifier: Topic :: Text Processing :: Markup :: HTML |
51 | 52 | Classifier: Topic :: Multimedia :: Graphics :: Graphics Conversion |
52 | 53 | Classifier: Topic :: Printing |
53 | Requires-Python: >= 3.5 | |
54 | Requires-Python: >=3.5 | |
54 | 55 | Description-Content-Type: text/x-rst |
55 | 56 | Provides-Extra: doc |
56 | 57 | Provides-Extra: test |
0 | 0 | Metadata-Version: 2.1 |
1 | 1 | Name: WeasyPrint |
2 | Version: 50 | |
2 | Version: 51 | |
3 | 3 | Summary: The Awesome Document Factory |
4 | 4 | Home-page: https://weasyprint.org/ |
5 | 5 | Author: Simon Sapin |
46 | 46 | Classifier: Programming Language :: Python :: 3.5 |
47 | 47 | Classifier: Programming Language :: Python :: 3.6 |
48 | 48 | Classifier: Programming Language :: Python :: 3.7 |
49 | Classifier: Programming Language :: Python :: 3.8 | |
49 | 50 | Classifier: Topic :: Internet :: WWW/HTTP |
50 | 51 | Classifier: Topic :: Text Processing :: Markup :: HTML |
51 | 52 | Classifier: Topic :: Multimedia :: Graphics :: Graphics Conversion |
52 | 53 | Classifier: Topic :: Printing |
53 | Requires-Python: >= 3.5 | |
54 | Requires-Python: >=3.5 | |
54 | 55 | Description-Content-Type: text/x-rst |
55 | 56 | Provides-Extra: doc |
56 | 57 | Provides-Extra: test |
4 | 4 | tinycss2>=1.0.0 |
5 | 5 | cssselect2>=0.1 |
6 | 6 | CairoSVG>=2.4.0 |
7 | Pyphen>=0.8 | |
7 | Pyphen>=0.9.1 | |
8 | 8 | |
9 | 9 | [doc] |
10 | 10 | sphinx |
25 | 25 | Programming Language :: Python :: 3.5 |
26 | 26 | Programming Language :: Python :: 3.6 |
27 | 27 | Programming Language :: Python :: 3.7 |
28 | Programming Language :: Python :: 3.8 | |
28 | 29 | Topic :: Internet :: WWW/HTTP |
29 | 30 | Topic :: Text Processing :: Markup :: HTML |
30 | 31 | Topic :: Multimedia :: Graphics :: Graphics Conversion |
47 | 48 | tinycss2>=1.0.0 |
48 | 49 | cssselect2>=0.1 |
49 | 50 | CairoSVG>=2.4.0 |
50 | Pyphen>=0.8 | |
51 | Pyphen>=0.9.1 | |
51 | 52 | tests_require = |
52 | 53 | pytest-runner |
53 | 54 | pytest-cov |
376 | 376 | yield specificity, check_style_attribute( |
377 | 377 | element, 'font-size:%s' % font_sizes[size]) |
378 | 378 | elif element.tag == 'table': |
379 | # TODO: we should support cellpadding | |
380 | 379 | if element.get('cellspacing'): |
381 | 380 | yield specificity, check_style_attribute( |
382 | 381 | element, |
9 | 9 | |
10 | 10 | """ |
11 | 11 | |
12 | from collections import OrderedDict | |
12 | 13 | from urllib.parse import unquote |
13 | 14 | |
14 | 15 | from tinycss2.color3 import parse_color |
16 | 17 | from .. import text |
17 | 18 | from ..logger import LOGGER |
18 | 19 | from ..urls import get_link_attribute |
19 | from .properties import INHERITED, INITIAL_VALUES, Dimension | |
20 | from .properties import ( | |
21 | INHERITED, INITIAL_NOT_COMPUTED, INITIAL_VALUES, Dimension) | |
20 | 22 | from .utils import ( |
21 | 23 | ANGLE_TO_RADIANS, LENGTH_UNITS, LENGTHS_TO_PIXELS, check_var_function, |
22 | 24 | safe_urljoin) |
27 | 29 | # Value in pixels of font-size for <absolute-size> keywords: 12pt (16px) for |
28 | 30 | # medium, and scaling factors given in CSS3 for others: |
29 | 31 | # http://www.w3.org/TR/css3-fonts/#font-size-prop |
30 | # TODO: this will need to be ordered to implement 'smaller' and 'larger' | |
31 | FONT_SIZE_KEYWORDS = dict( | |
32 | FONT_SIZE_KEYWORDS = OrderedDict( | |
32 | 33 | # medium is 16px, others are a ratio of medium |
33 | 34 | (name, INITIAL_VALUES['font_size'] * a / b) |
34 | 35 | for name, a, b in ( |
217 | 218 | |
218 | 219 | value = specified[name] |
219 | 220 | function = getter(name) |
221 | already_computed_value = False | |
220 | 222 | |
221 | 223 | if value and isinstance(value, tuple) and value[0] == 'var()': |
222 | 224 | variable_name, default = value[1] |
242 | 244 | 'for property `%s`.', computed_value, |
243 | 245 | variable_name.replace('_', '-'), name.replace('_', '-')) |
244 | 246 | if name in INHERITED and parent_style: |
247 | already_computed_value = True | |
245 | 248 | value = parent_style[name] |
246 | 249 | else: |
250 | already_computed_value = name not in INITIAL_NOT_COMPUTED | |
247 | 251 | value = INITIAL_VALUES[name] |
248 | 252 | else: |
249 | 253 | value = new_value |
250 | 254 | |
251 | if function is not None: | |
255 | if function is not None and not already_computed_value: | |
252 | 256 | value = function(computer, name, value) |
253 | 257 | # else: same as specified |
254 | 258 | |
496 | 500 | elif value[0] == 'attr()': |
497 | 501 | assert value[1][1] == 'string' |
498 | 502 | computed_value = compute_attr_function(computer, value) |
499 | elif value[0] in ('counter()', 'counters()', 'content()', 'string()'): | |
503 | elif value[0] in ( | |
504 | 'counter()', 'counters()', 'content()', 'element()', | |
505 | 'string()', | |
506 | ): | |
500 | 507 | # Other values need layout context, their computed value cannot be |
501 | 508 | # better than their specified value yet. |
502 | 509 | # See build.compute_content_list. |
591 | 598 | """Compute the ``font-size`` property.""" |
592 | 599 | if value in FONT_SIZE_KEYWORDS: |
593 | 600 | return FONT_SIZE_KEYWORDS[value] |
594 | # TODO: support 'larger' and 'smaller' | |
595 | ||
601 | ||
602 | keyword_values = list(FONT_SIZE_KEYWORDS.values()) | |
596 | 603 | parent_font_size = computer['parent_style']['font_size'] |
597 | if value.unit == '%': | |
604 | ||
605 | if value == 'larger': | |
606 | for i, keyword_value in enumerate(keyword_values): | |
607 | if keyword_value > parent_font_size: | |
608 | return keyword_values[i] | |
609 | else: | |
610 | return parent_font_size * 1.2 | |
611 | elif value == 'smaller': | |
612 | for i, keyword_value in enumerate(keyword_values[::-1]): | |
613 | if keyword_value < parent_font_size: | |
614 | return keyword_values[-i - 1] | |
615 | else: | |
616 | return parent_font_size * 0.8 | |
617 | elif value.unit == '%': | |
598 | 618 | return value.value * parent_font_size / 100. |
599 | 619 | else: |
600 | return length(computer, name, value, pixels_only=True, | |
601 | font_size=parent_font_size) | |
620 | return length( | |
621 | computer, name, value, pixels_only=True, | |
622 | font_size=parent_font_size) | |
602 | 623 | |
603 | 624 | |
604 | 625 | @register_computer('font-weight') |
75 | 75 | border-style: hidden; |
76 | 76 | border-collapse: collapse; |
77 | 77 | } |
78 | table[border] { border-style: outset; } /* only if border is not equivalent to zero */ | |
78 | table[border]:not([border="0"]) { border-style: outset; } | |
79 | 79 | table[frame=void] { border-style: hidden; } |
80 | 80 | table[frame=above] { border-style: outset hidden hidden hidden; } |
81 | 81 | table[frame=below] { border-style: hidden hidden outset hidden; } |
85 | 85 | table[frame=vsides] { border-style: hidden outset; } |
86 | 86 | table[frame=box], table[frame=border] { border-style: outset; } |
87 | 87 | |
88 | table[border] > tr > td, table[border] > tr > th, | |
89 | table[border] > thead > tr > td, table[border] > thead > tr > th, | |
90 | table[border] > tbody > tr > td, table[border] > tbody > tr > th, | |
91 | table[border] > tfoot > tr > td, table[border] > tfoot > tr > th { | |
92 | /* only if border is not equivalent to zero */ | |
88 | table[border]:not([border="0"]) > tr > td, table[border]:not([border="0"]) > tr > th, | |
89 | table[border]:not([border="0"]) > thead > tr > td, table[border]:not([border="0"]) > thead > tr > th, | |
90 | table[border]:not([border="0"]) > tbody > tr > td, table[border]:not([border="0"]) > tbody > tr > th, | |
91 | table[border]:not([border="0"]) > tfoot > tr > td, table[border]:not([border="0"]) > tfoot > tr > th { | |
93 | 92 | border-width: 1px; |
94 | 93 | border-style: inset; |
95 | 94 | } |
480 | 480 | return ('content()', ident.lower_value) |
481 | 481 | |
482 | 482 | |
483 | def check_string_function(token): | |
483 | def check_string_or_element_function(string_or_element, token): | |
484 | 484 | function = parse_function(token) |
485 | 485 | if function is None: |
486 | 486 | return |
487 | 487 | name, args = function |
488 | if name == 'string' and len(args) in (1, 2): | |
488 | if name == string_or_element and len(args) in (1, 2): | |
489 | 489 | custom_ident = args.pop(0) |
490 | 490 | if custom_ident.type != 'ident': |
491 | 491 | return |
500 | 500 | else: |
501 | 501 | ident = 'first' |
502 | 502 | |
503 | return ('string()', (custom_ident, ident)) | |
503 | return ('%s()' % string_or_element, (custom_ident, ident)) | |
504 | 504 | |
505 | 505 | |
506 | 506 | def check_var_function(token): |
530 | 530 | elif token.name == 'content': |
531 | 531 | return check_content_function(token) |
532 | 532 | elif token.name == 'string': |
533 | return check_string_function(token) | |
533 | return check_string_or_element_function('string', token) | |
534 | 534 | |
535 | 535 | |
536 | 536 | def get_length(token, negative=True, percentage=False): |
742 | 742 | elif arg.type == 'string': |
743 | 743 | string = arg.value |
744 | 744 | return ('leader()', ('string', string)) |
745 | elif name == 'element': | |
746 | return check_string_or_element_function('element', token) |
783 | 783 | return length |
784 | 784 | font_size_keyword = get_keyword(token) |
785 | 785 | if font_size_keyword in ('smaller', 'larger'): |
786 | raise InvalidValues('value not supported yet') | |
786 | return font_size_keyword | |
787 | 787 | if font_size_keyword in computed_values.FONT_SIZE_KEYWORDS: |
788 | # or keyword in ('smaller', 'larger') | |
789 | 788 | return font_size_keyword |
790 | 789 | |
791 | 790 | |
967 | 966 | |
968 | 967 | |
969 | 968 | @property() |
970 | @single_keyword | |
971 | def position(keyword): | |
969 | @single_token | |
970 | def position(token): | |
972 | 971 | """``position`` property validation.""" |
973 | return keyword in ('static', 'relative', 'absolute', 'fixed') | |
972 | if token.type == 'function' and token.name == 'running': | |
973 | if len(token.arguments) == 1 and token.arguments[0].type == 'ident': | |
974 | return ('running()', token.arguments[0].value) | |
975 | keyword = get_single_keyword([token]) | |
976 | if keyword in ('static', 'relative', 'absolute', 'fixed'): | |
977 | return keyword | |
974 | 978 | |
975 | 979 | |
976 | 980 | @property() |
1364 | 1368 | if None not in parsed_tokens: |
1365 | 1369 | return (var_name, parsed_tokens) |
1366 | 1370 | elif tokens and get_keyword(tokens[0]) == 'none': |
1367 | return 'none' | |
1371 | return 'none', () | |
1368 | 1372 | |
1369 | 1373 | |
1370 | 1374 | @property() |
1371 | 1375 | def transform(tokens): |
1376 | """Validation for ``transform``.""" | |
1372 | 1377 | if get_single_keyword(tokens) == 'none': |
1373 | 1378 | return () |
1374 | 1379 | else: |
1376 | 1381 | for token in tokens: |
1377 | 1382 | function = parse_function(token) |
1378 | 1383 | if not function: |
1379 | raise InvalidValues | |
1384 | return | |
1380 | 1385 | name, args = function |
1381 | 1386 | |
1382 | 1387 | if len(args) == 1: |
1397 | 1402 | elif name == 'scale' and args[0].type == 'number': |
1398 | 1403 | transforms.append(('scale', (args[0].value,) * 2)) |
1399 | 1404 | else: |
1400 | raise InvalidValues | |
1405 | return | |
1401 | 1406 | elif len(args) == 2: |
1402 | 1407 | if name == 'scale' and all(a.type == 'number' for a in args): |
1403 | 1408 | transforms.append((name, tuple(arg.value for arg in args))) |
1407 | 1412 | if name == 'translate' and all(lengths): |
1408 | 1413 | transforms.append((name, lengths)) |
1409 | 1414 | else: |
1410 | raise InvalidValues | |
1415 | return | |
1411 | 1416 | elif len(args) == 6 and name == 'matrix' and all( |
1412 | 1417 | a.type == 'number' for a in args): |
1413 | 1418 | transforms.append((name, tuple(arg.value for arg in args))) |
1414 | 1419 | else: |
1415 | raise InvalidValues | |
1420 | return | |
1416 | 1421 | return tuple(transforms) |
88 | 88 | def all_children(self): |
89 | 89 | return () |
90 | 90 | |
91 | def __init__(self, element_tag, style): | |
91 | def __init__(self, element_tag, style, element): | |
92 | 92 | self.element_tag = element_tag |
93 | self.element = element | |
93 | 94 | self.style = style |
94 | 95 | |
95 | 96 | def __repr__(self): |
100 | 101 | """Return an anonymous box that inherits from ``parent``.""" |
101 | 102 | style = computed_from_cascaded( |
102 | 103 | cascaded={}, parent_style=parent.style, element=None) |
103 | return cls(parent.element_tag, style, *args, **kwargs) | |
104 | return cls(parent.element_tag, style, parent.element, *args, **kwargs) | |
104 | 105 | |
105 | 106 | def copy(self): |
106 | 107 | """Return shallow copy of the box.""" |
111 | 112 | # Copy attributes |
112 | 113 | new_box.__dict__.update(self.__dict__) |
113 | 114 | return new_box |
115 | ||
116 | def deepcopy(self): | |
117 | """Return a copy of the box with recursive copies of its children.""" | |
118 | return self.copy() | |
114 | 119 | |
115 | 120 | def translate(self, dx=0, dy=0, ignore_floats=False): |
116 | 121 | """Change the box’s position. |
273 | 278 | """Return whether this box is in the absolute positioning scheme.""" |
274 | 279 | return self.style['position'] in ('absolute', 'fixed') |
275 | 280 | |
281 | def is_running(self): | |
282 | """Return whether this box is a running element.""" | |
283 | return self.style['position'][0] == 'running()' | |
284 | ||
276 | 285 | def is_in_normal_flow(self): |
277 | 286 | """Return whether this box is in normal flow.""" |
278 | return not (self.is_floated() or self.is_absolutely_positioned()) | |
287 | return not ( | |
288 | self.is_floated() or self.is_absolutely_positioned() or | |
289 | self.is_running()) | |
279 | 290 | |
280 | 291 | # Start and end page values for named pages |
281 | 292 | |
286 | 297 | |
287 | 298 | class ParentBox(Box): |
288 | 299 | """A box that has children.""" |
289 | def __init__(self, element_tag, style, children): | |
290 | super(ParentBox, self).__init__(element_tag, style) | |
300 | def __init__(self, element_tag, style, element, children): | |
301 | super().__init__(element_tag, style, element) | |
291 | 302 | self.children = tuple(children) |
292 | 303 | |
293 | 304 | def all_children(self): |
332 | 343 | new_box._remove_decoration(not is_start, not is_end) |
333 | 344 | return new_box |
334 | 345 | |
346 | def deepcopy(self): | |
347 | result = self.copy() | |
348 | result.children = tuple(child.deepcopy() for child in self.children) | |
349 | return result | |
350 | ||
335 | 351 | def descendants(self): |
336 | 352 | """A flat generator for a box, its children and descendants.""" |
337 | 353 | yield self |
352 | 368 | raise ValueError('Table wrapper without a table') |
353 | 369 | |
354 | 370 | def page_values(self): |
355 | start_value, end_value = super(ParentBox, self).page_values() | |
371 | start_value, end_value = super().page_values() | |
356 | 372 | if self.children: |
357 | start_box, end_box = self.children[0], self.children[-1] | |
358 | start_value = start_box.page_values()[0] or start_value | |
359 | end_value = end_box.page_values()[1] or end_value | |
373 | if len(self.children) == 1: | |
374 | page_values = self.children[0].page_values() | |
375 | start_value = page_values[0] or start_value | |
376 | end_value = page_values[1] or end_value | |
377 | else: | |
378 | start_box, end_box = self.children[0], self.children[-1] | |
379 | start_value = start_box.page_values()[0] or start_value | |
380 | end_value = end_box.page_values()[1] or end_value | |
360 | 381 | return start_value, end_value |
361 | 382 | |
362 | 383 | |
466 | 487 | ascii_to_wide = dict((i, chr(i + 0xfee0)) for i in range(0x21, 0x7f)) |
467 | 488 | ascii_to_wide.update({0x20: '\u3000', 0x2D: '\u2212'}) |
468 | 489 | |
469 | def __init__(self, element_tag, style, text): | |
490 | def __init__(self, element_tag, style, element, text): | |
470 | 491 | assert text |
471 | super(TextBox, self).__init__(element_tag, style) | |
492 | super().__init__(element_tag, style, element) | |
472 | 493 | text_transform = style['text_transform'] |
473 | 494 | if text_transform != 'none': |
474 | 495 | text = { |
516 | 537 | and is opaque from CSS’s point of view. |
517 | 538 | |
518 | 539 | """ |
519 | def __init__(self, element_tag, style, replacement): | |
520 | super(ReplacedBox, self).__init__(element_tag, style) | |
540 | def __init__(self, element_tag, style, element, replacement): | |
541 | super().__init__(element_tag, style, element) | |
521 | 542 | self.replacement = replacement |
522 | 543 | |
523 | 544 | |
554 | 575 | return |
555 | 576 | self.column_positions = [ |
556 | 577 | position + dx for position in self.column_positions] |
557 | return super(TableBox, self).translate(dx, dy, ignore_floats) | |
578 | return super().translate(dx, dy, ignore_floats) | |
558 | 579 | |
559 | 580 | def page_values(self): |
560 | 581 | return (self.style['page'], self.style['page']) |
666 | 687 | def __init__(self, page_type, style): |
667 | 688 | self.page_type = page_type |
668 | 689 | # Page boxes are not linked to any element. |
669 | super(PageBox, self).__init__( | |
670 | element_tag=None, style=style, children=[]) | |
690 | super().__init__( | |
691 | element_tag=None, style=style, element=None, children=[]) | |
671 | 692 | |
672 | 693 | def __repr__(self): |
673 | 694 | return '<%s %s>' % (type(self).__name__, self.page_type) |
678 | 699 | def __init__(self, at_keyword, style): |
679 | 700 | self.at_keyword = at_keyword |
680 | 701 | # Margin boxes are not linked to any element. |
681 | super(MarginBox, self).__init__( | |
682 | element_tag=None, style=style, children=[]) | |
702 | super().__init__( | |
703 | element_tag=None, style=style, element=None, children=[]) | |
683 | 704 | |
684 | 705 | def __repr__(self): |
685 | 706 | return '<%s %s>' % (type(self).__name__, self.at_keyword) |
79 | 79 | return box |
80 | 80 | |
81 | 81 | |
82 | def make_box(element_tag, style, content): | |
83 | return BOX_TYPE_FROM_DISPLAY[style['display']]( | |
84 | element_tag, style, content) | |
82 | def make_box(element_tag, style, content, element): | |
83 | box = BOX_TYPE_FROM_DISPLAY[style['display']]( | |
84 | element_tag, style, element, content) | |
85 | return box | |
85 | 86 | |
86 | 87 | |
87 | 88 | def element_to_box(element, style_for, get_image_from_uri, base_url, |
121 | 122 | if display == 'none': |
122 | 123 | return [] |
123 | 124 | |
124 | box = make_box(element.tag, style, []) | |
125 | box = make_box(element.tag, style, [], element) | |
125 | 126 | |
126 | 127 | if state is None: |
127 | 128 | # use a list to have a shared mutable object |
226 | 227 | if 'none' in (display, content) or content in ('normal', 'inhibit'): |
227 | 228 | return [] |
228 | 229 | |
229 | box = make_box('%s::%s' % (element.tag, pseudo_type), style, []) | |
230 | box = make_box('%s::%s' % (element.tag, pseudo_type), style, [], element) | |
230 | 231 | |
231 | 232 | quote_depth, counter_values, _counter_scopes = state |
232 | 233 | update_counters(state, style) |
263 | 264 | # `content` where 'normal' computes as 'inhibit' for pseudo elements. |
264 | 265 | quote_depth, counter_values, _counter_scopes = state |
265 | 266 | |
266 | box = make_box('%s::marker' % element.tag, style, children) | |
267 | box = make_box('%s::marker' % element.tag, style, children, element) | |
267 | 268 | |
268 | 269 | if style['display'] == 'none': |
269 | 270 | return |
408 | 409 | counters.format(counter_value, counter_style) |
409 | 410 | for counter_value in counter_values.get(counter_name, [0]))) |
410 | 411 | elif type_ == 'string()': |
411 | if in_page_context: | |
412 | texts.append(context.get_string_set_for(page, *value)) | |
413 | else: | |
412 | if not in_page_context: | |
414 | 413 | # string() is currently only valid in @page context |
415 | 414 | # See https://github.com/Kozea/WeasyPrint/issues/723 |
416 | 415 | LOGGER.warning( |
417 | 416 | '"string(%s)" is only allowed in page margins' % |
418 | 417 | (' '.join(value))) |
418 | continue | |
419 | texts.append(context.get_string_set_for(page, *value) or '') | |
419 | 420 | elif type_ == 'target-counter()': |
420 | 421 | anchor_token, counter_name, counter_style = value |
421 | 422 | lookup_target = target_collector.lookup_target( |
491 | 492 | texts.append(quotes[min(quote_depth[0], len(quotes) - 1)]) |
492 | 493 | if is_open: |
493 | 494 | quote_depth[0] += 1 |
495 | elif type_ == 'element()': | |
496 | if not in_page_context: | |
497 | LOGGER.warning( | |
498 | '"element(%s)" is only allowed in page margins' % | |
499 | (' '.join(value))) | |
500 | continue | |
501 | new_box = context.get_running_element_for(page, *value) | |
502 | if new_box is None: | |
503 | continue | |
504 | new_box = new_box.deepcopy() | |
505 | new_box.style['position'] = 'static' | |
506 | for child in new_box.descendants(): | |
507 | if child.style['content'] in ('normal', 'none'): | |
508 | continue | |
509 | child.children = content_to_boxes( | |
510 | child.style, child, quote_depth, counter_values, | |
511 | get_image_from_uri, target_collector, context=context, | |
512 | page=page) | |
513 | boxlist.append(new_box) | |
494 | 514 | text = ''.join(texts) |
495 | 515 | if text: |
496 | 516 | boxlist.append(boxes.TextBox.anonymous_from(parent_box, text)) |
718 | 738 | See http://www.w3.org/TR/CSS21/tables.html#anonymous-boxes |
719 | 739 | |
720 | 740 | """ |
721 | if not isinstance(box, boxes.ParentBox): | |
741 | if not isinstance(box, boxes.ParentBox) or box.is_running(): | |
722 | 742 | return box |
723 | 743 | |
724 | 744 | # Do recursion. |
1111 | 1131 | See http://www.w3.org/TR/css-flexbox-1/#flex-items |
1112 | 1132 | |
1113 | 1133 | """ |
1114 | if not isinstance(box, boxes.ParentBox): | |
1134 | if not isinstance(box, boxes.ParentBox) or box.is_running(): | |
1115 | 1135 | return box |
1116 | 1136 | |
1117 | 1137 | # Do recursion. |
1195 | 1215 | box.text = text |
1196 | 1216 | return following_collapsible_space |
1197 | 1217 | |
1198 | if isinstance(box, boxes.ParentBox): | |
1218 | if isinstance(box, boxes.ParentBox) and not box.is_running(): | |
1199 | 1219 | for child in box.children: |
1200 | 1220 | if isinstance(child, (boxes.TextBox, boxes.InlineBox)): |
1201 | 1221 | following_collapsible_space = process_whitespace( |
1246 | 1266 | ] |
1247 | 1267 | |
1248 | 1268 | """ |
1249 | if not isinstance(box, boxes.ParentBox): | |
1269 | if not isinstance(box, boxes.ParentBox) or box.is_running(): | |
1250 | 1270 | return box |
1251 | 1271 | |
1252 | 1272 | box_children = list(box.children) |
1381 | 1401 | ] |
1382 | 1402 | |
1383 | 1403 | """ |
1384 | if not isinstance(box, boxes.ParentBox): | |
1404 | if not isinstance(box, boxes.ParentBox) or box.is_running(): | |
1385 | 1405 | return box |
1386 | 1406 | |
1387 | 1407 | new_children = [] |
110 | 110 | else: |
111 | 111 | # TODO: support images with 'display: table-cell'? |
112 | 112 | type_ = boxes.InlineReplacedBox |
113 | new_box = type_(element.tag, box.style, image) | |
113 | new_box = type_(element.tag, box.style, element, image) | |
114 | 114 | # TODO: check other attributes that need to be copied |
115 | 115 | # TODO: find another solution |
116 | 116 | new_box.string_set = box.string_set |
94 | 94 | """ |
95 | 95 | @property |
96 | 96 | def device_units_per_user_units(self): |
97 | scale = super(ScaledSVGSurface, self).device_units_per_user_units | |
97 | scale = super().device_units_per_user_units | |
98 | 98 | return scale / 0.75 |
99 | 99 | |
100 | 100 |
195 | 195 | self._excluded_shapes_lists = [] |
196 | 196 | self.excluded_shapes = None # Not initialized yet |
197 | 197 | self.string_set = defaultdict(lambda: defaultdict(lambda: list())) |
198 | self.running_elements = defaultdict( | |
199 | lambda: defaultdict(lambda: list())) | |
198 | 200 | self.current_page = None |
199 | 201 | self.forced_break = False |
200 | 202 | |
223 | 225 | self.excluded_shapes = None |
224 | 226 | |
225 | 227 | def get_string_set_for(self, page, name, keyword='first'): |
226 | """Resolve value of string function (as set by string set). | |
228 | """Resolve value of string function.""" | |
229 | return self.get_string_or_element_for( | |
230 | self.string_set, page, name, keyword) | |
231 | ||
232 | def get_running_element_for(self, page, name, keyword='first'): | |
233 | """Resolve value of element function.""" | |
234 | return self.get_string_or_element_for( | |
235 | self.running_elements, page, name, keyword) | |
236 | ||
237 | def get_string_or_element_for(self, store, page, name, keyword): | |
238 | """Resolve value of string or element function. | |
227 | 239 | |
228 | 240 | We'll have something like this that represents all assignments on a |
229 | 241 | given page: |
230 | 242 | |
231 | {1: [u'First Header'], 3: [u'Second Header'], | |
232 | 4: [u'Third Header', u'3.5th Header']} | |
243 | {1: ['First Header'], 3: ['Second Header'], | |
244 | 4: ['Third Header', '3.5th Header']} | |
233 | 245 | |
234 | 246 | Value depends on current page. |
235 | 247 | http://dev.w3.org/csswg/css-gcpm/#funcdef-string |
236 | 248 | |
237 | :param name: the name of the named string. | |
238 | :param keyword: indicates which value of the named string to use. | |
239 | Default is the first assignment on the current page | |
240 | else the most recent assignment (entry value) | |
241 | :returns: text | |
249 | :param store: dictionary where the resolved value is stored. | |
250 | :param page: current page. | |
251 | :param name: name of the named string or running element. | |
252 | :param keyword: indicates which value of the named string or running | |
253 | element to use. Default is the first assignment on the | |
254 | current page else the most recent assignment. | |
255 | :returns: text for string set, box for running element | |
242 | 256 | |
243 | 257 | """ |
244 | if self.current_page in self.string_set[name]: | |
258 | if self.current_page in store[name]: | |
245 | 259 | # A value was assigned on this page |
246 | first_string = self.string_set[name][self.current_page][0] | |
247 | last_string = self.string_set[name][self.current_page][-1] | |
260 | first_string = store[name][self.current_page][0] | |
261 | last_string = store[name][self.current_page][-1] | |
248 | 262 | if keyword == 'first': |
249 | 263 | return first_string |
250 | 264 | elif keyword == 'start': |
261 | 275 | break |
262 | 276 | elif keyword == 'last': |
263 | 277 | return last_string |
278 | elif keyword == 'first-except': | |
279 | return | |
264 | 280 | # Search backwards through previous pages |
265 | 281 | for previous_page in range(self.current_page - 1, 0, -1): |
266 | if previous_page in self.string_set[name]: | |
267 | return self.string_set[name][previous_page][-1] | |
268 | return '' | |
282 | if previous_page in store[name]: | |
283 | return store[name][previous_page][-1] |
257 | 257 | # TODO: boxes.FlexBox is allowed here because flex_layout calls |
258 | 258 | # block_container_layout, there's probably a better solution. |
259 | 259 | assert isinstance(box, (boxes.BlockContainerBox, boxes.FlexBox)) |
260 | ||
261 | # TODO: this should make a difference, but that is currently neglected. | |
262 | # See http://www.w3.org/TR/CSS21/visudet.html#normal-block | |
263 | # http://www.w3.org/TR/CSS21/visudet.html#root-height | |
264 | ||
265 | # if box.style['overflow'] != 'visible': | |
266 | # ... | |
267 | 260 | |
268 | 261 | # We have to work around floating point rounding errors here. |
269 | 262 | # The 1e-9 value comes from PEP 485. |
358 | 351 | break |
359 | 352 | resume_at = (index, None) |
360 | 353 | break |
354 | elif child.is_running(): | |
355 | running_name = child.style['position'][1] | |
356 | page = context.current_page | |
357 | context.running_elements[running_name][page].append(child) | |
361 | 358 | continue |
362 | 359 | |
363 | 360 | if isinstance(child, boxes.LineBox): |
277 | 277 | if child.element_tag.endswith('::first-letter'): |
278 | 278 | letter_box = boxes.InlineBox( |
279 | 279 | '%s::first-letter' % box.element_tag, letter_style, |
280 | [child]) | |
280 | box.element, [child]) | |
281 | 281 | box.children = ( |
282 | 282 | (letter_box,) + tuple(box.children[1:])) |
283 | 283 | elif child.text: |
304 | 304 | if first_letter_style['float'] == 'none': |
305 | 305 | letter_box = boxes.InlineBox( |
306 | 306 | '%s::first-letter' % box.element_tag, |
307 | first_letter_style, []) | |
307 | first_letter_style, box.element, []) | |
308 | 308 | text_box = boxes.TextBox( |
309 | 309 | '%s::first-letter' % box.element_tag, letter_style, |
310 | first_letter) | |
310 | box.element, first_letter) | |
311 | 311 | letter_box.children = (text_box,) |
312 | 312 | box.children = (letter_box,) + tuple(box.children) |
313 | 313 | else: |
314 | 314 | letter_box = boxes.BlockBox( |
315 | 315 | '%s::first-letter' % box.element_tag, |
316 | first_letter_style, []) | |
316 | first_letter_style, box.element, []) | |
317 | 317 | letter_box.first_letter_style = None |
318 | 318 | line_box = boxes.LineBox( |
319 | 319 | '%s::first-letter' % box.element_tag, letter_style, |
320 | []) | |
320 | box.element, []) | |
321 | 321 | letter_box.children = (line_box,) |
322 | 322 | text_box = boxes.TextBox( |
323 | 323 | '%s::first-letter' % box.element_tag, letter_style, |
324 | first_letter) | |
324 | box.element, first_letter) | |
325 | 325 | line_box.children = (text_box,) |
326 | 326 | box.children = (letter_box,) + tuple(box.children) |
327 | 327 | if skip_stack and child_skip_stack: |
737 | 737 | old_child.translate(dx=dx) |
738 | 738 | float_resume_at = index + 1 |
739 | 739 | continue |
740 | elif child.is_running(): | |
741 | running_name = child.style['position'][1] | |
742 | page = context.current_page | |
743 | context.running_elements[running_name][page].append(child) | |
744 | continue | |
740 | 745 | |
741 | 746 | last_child = (index == len(box.children) - 1) |
742 | 747 | available_width = max_x |
857 | 862 | # add the original skip stack to the partial |
858 | 863 | # skip stack we get after the new rendering. |
859 | 864 | |
860 | # We have to do: | |
861 | # resume_at + initial_skip_stack | |
862 | # but adding skip stacks is a bit complicated | |
863 | current_skip_stack = initial_skip_stack | |
864 | current_resume_at = (child_index, child_resume_at) | |
865 | # Combining skip stacks is a bit complicated | |
866 | # We have to: | |
867 | # - set `child_index` as the first number | |
868 | # - append the new stack if it's an absolute one | |
869 | # - otherwise append the combined stacks | |
870 | # (resume_at + initial_skip_stack) | |
871 | ||
872 | # extract the initial index | |
873 | if initial_skip_stack is None: | |
874 | current_skip_stack = None | |
875 | initial_index = 0 | |
876 | else: | |
877 | initial_index, current_skip_stack = ( | |
878 | initial_skip_stack) | |
879 | # child_resume_at is an absolute skip stack | |
880 | if child_index > initial_index: | |
881 | resume_at = (child_index, child_resume_at) | |
882 | break | |
883 | ||
884 | # combine the stacks | |
885 | current_resume_at = child_resume_at | |
865 | 886 | stack = [] |
866 | 887 | while current_skip_stack and current_resume_at: |
867 | 888 | skip, current_skip_stack = ( |
874 | 895 | resume_at = current_resume_at |
875 | 896 | while stack: |
876 | 897 | resume_at = (stack.pop(), resume_at) |
898 | # insert the child index | |
899 | resume_at = (child_index, resume_at) | |
877 | 900 | break |
878 | 901 | if break_found: |
879 | 902 | break |
344 | 344 | box.style, box, quote_depth, counter_values, |
345 | 345 | context.get_image_from_uri, context.target_collector, context, |
346 | 346 | page) |
347 | # content_to_boxes() only produces inline-level boxes, no need to | |
348 | # run other post-processors from build.build_formatting_structure() | |
347 | build.process_whitespace(box) | |
348 | box = build.anonymous_table_boxes(box) | |
349 | box = build.flex_boxes(box) | |
349 | 350 | box = build.inline_in_block(box) |
350 | build.process_whitespace(box) | |
351 | box = build.block_in_inline(box) | |
351 | 352 | resolve_percentages(box, containing_block) |
352 | 353 | if not box.is_generated: |
353 | 354 | box.width = box.height = 0 |
181 | 181 | else: |
182 | 182 | row.height = max(row.height, max( |
183 | 183 | row_cell.height for row_cell in ending_cells)) |
184 | row_bottom_y = cell.position_y + row.height | |
184 | row_bottom_y = row.position_y + row.height | |
185 | 185 | else: |
186 | 186 | row_bottom_y = row.position_y |
187 | 187 | row.height = 0 |
326 | 326 | # Layout for row groups, rows and cells |
327 | 327 | position_y = table.content_box_y() + border_spacing_y |
328 | 328 | initial_position_y = position_y |
329 | table_rows = [ | |
330 | child for child in table.children | |
331 | if not child.is_header and not child.is_footer] | |
329 | 332 | |
330 | 333 | def all_groups_layout(): |
334 | # If the page is not empty, we try to render the header and the footer | |
335 | # on it. If the table does not fit on the page, we try to render it on | |
336 | # the next page. | |
337 | ||
338 | # If the page is empty and the header and footer are too big, there | |
339 | # are not rendered. If no row can be rendered because of the header and | |
340 | # the footer, the header and/or the footer are not rendered. | |
341 | ||
342 | if page_is_empty: | |
343 | header_footer_max_position_y = max_position_y | |
344 | else: | |
345 | header_footer_max_position_y = float('inf') | |
346 | ||
331 | 347 | if table.children and table.children[0].is_header: |
332 | 348 | header = table.children[0] |
333 | 349 | header, resume_at, next_page = group_layout( |
334 | header, position_y, max_position_y, | |
350 | header, position_y, header_footer_max_position_y, | |
335 | 351 | skip_stack=None, page_is_empty=False) |
336 | 352 | if header and not resume_at: |
337 | 353 | header_height = header.height + border_spacing_y |
343 | 359 | if table.children and table.children[-1].is_footer: |
344 | 360 | footer = table.children[-1] |
345 | 361 | footer, resume_at, next_page = group_layout( |
346 | footer, position_y, max_position_y, | |
362 | footer, position_y, header_footer_max_position_y, | |
347 | 363 | skip_stack=None, page_is_empty=False) |
348 | 364 | if footer and not resume_at: |
349 | 365 | footer_height = footer.height + border_spacing_y |
369 | 385 | position_y=position_y + header_height, |
370 | 386 | max_position_y=max_position_y - footer_height, |
371 | 387 | page_is_empty=avoid_breaks)) |
372 | if new_table_children or not page_is_empty: | |
388 | if new_table_children or not table_rows or not page_is_empty: | |
373 | 389 | footer.translate(dy=end_position_y - footer.position_y) |
374 | 390 | end_position_y += footer_height |
375 | 391 | return (header, new_table_children, footer, |
386 | 402 | position_y=position_y + header_height, |
387 | 403 | max_position_y=max_position_y, |
388 | 404 | page_is_empty=avoid_breaks)) |
389 | if new_table_children or not page_is_empty: | |
405 | if new_table_children or not table_rows or not page_is_empty: | |
390 | 406 | return (header, new_table_children, footer, |
391 | 407 | end_position_y, resume_at, next_page) |
392 | 408 | else: |
401 | 417 | position_y=position_y, |
402 | 418 | max_position_y=max_position_y - footer_height, |
403 | 419 | page_is_empty=avoid_breaks)) |
404 | if new_table_children or not page_is_empty: | |
420 | if new_table_children or not table_rows or not page_is_empty: | |
405 | 421 | footer.translate(dy=end_position_y - footer.position_y) |
406 | 422 | end_position_y += footer_height |
407 | 423 | return (header, new_table_children, footer, |
71 | 71 | return '({0})'.format(pdf_escape( |
72 | 72 | ('\ufeff' + value).encode('utf-16-be').decode('latin1'))) |
73 | 73 | else: |
74 | return super(PDFFormatter, self).convert_field(value, conversion) | |
74 | return super().convert_field(value, conversion) | |
75 | 75 | |
76 | 76 | def vformat(self, format_string, args, kwargs): |
77 | result = super(PDFFormatter, self).vformat(format_string, args, kwargs) | |
77 | result = super().vformat(format_string, args, kwargs) | |
78 | 78 | return result.encode('latin1') |
79 | 79 | |
80 | 80 |
7 | 7 | :license: BSD, see LICENSE for details. |
8 | 8 | |
9 | 9 | """ |
10 | ||
11 | from math import isclose | |
10 | 12 | |
11 | 13 | import pytest |
12 | 14 | import tinycss2 |
82 | 84 | style_for = get_all_computed_styles( |
83 | 85 | document, user_stylesheets=[CSS(resource_filename('user.css'))]) |
84 | 86 | |
85 | # Element objects behave a lists of their children | |
87 | # Element objects behave as lists of their children | |
86 | 88 | _head, body = document.etree_element |
87 | 89 | h1, p, ul, div = body |
88 | 90 | li_0, _li_1 = ul |
450 | 452 | body, = html.children |
451 | 453 | p, = body.children |
452 | 454 | assert p.margin_left == width |
455 | ||
456 | ||
457 | @assert_no_logs | |
458 | @pytest.mark.parametrize('parent_css, parent_size, child_css, child_size', ( | |
459 | ('10px', 10, '10px', 10), | |
460 | ('x-small', 12, 'xx-large', 32), | |
461 | ('x-large', 24, '2em', 48), | |
462 | ('1em', 16, '1em', 16), | |
463 | ('1em', 16, 'larger', 6 / 5 * 16), | |
464 | ('medium', 16, 'larger', 6 / 5 * 16), | |
465 | ('x-large', 24, 'larger', 32), | |
466 | ('xx-large', 32, 'larger', 1.2 * 32), | |
467 | ('1px', 1, 'larger', 3 / 5 * 16), | |
468 | ('28px', 28, 'larger', 32), | |
469 | ('100px', 100, 'larger', 120), | |
470 | ('xx-small', 3 / 5 * 16, 'larger', 12), | |
471 | ('1em', 16, 'smaller', 8 / 9 * 16), | |
472 | ('medium', 16, 'smaller', 8 / 9 * 16), | |
473 | ('x-large', 24, 'smaller', 6 / 5 * 16), | |
474 | ('xx-large', 32, 'smaller', 24), | |
475 | ('xx-small', 3 / 5 * 16, 'smaller', 0.8 * 3 / 5 * 16), | |
476 | ('1px', 1, 'smaller', 0.8), | |
477 | ('28px', 28, 'smaller', 24), | |
478 | ('100px', 100, 'smaller', 32), | |
479 | )) | |
480 | def test_font_size(parent_css, parent_size, child_css, child_size): | |
481 | document = FakeHTML(string='<p>a<span>b') | |
482 | style_for = get_all_computed_styles(document, user_stylesheets=[CSS( | |
483 | string='p{font-size:%s}span{font-size:%s}' % (parent_css, child_css))]) | |
484 | ||
485 | _head, body = document.etree_element | |
486 | p, = body | |
487 | span, = p | |
488 | assert isclose(style_for(p)['font_size'], parent_size) | |
489 | assert isclose(style_for(span)['font_size'], child_size) |
379 | 379 | assert line2.children[0].children[0].children[0].text == 'yyyyyy yyy' |
380 | 380 | assert line3.children[0].children[0].text == 'ZZZZZZ zzzzz' |
381 | 381 | assert line4.children[0].text == ')x' |
382 | ||
383 | ||
384 | @assert_no_logs | |
385 | def test_breaking_linebox_regression_11(): | |
386 | # Regression test for https://github.com/Kozea/WeasyPrint/issues/953 | |
387 | page, = parse( | |
388 | '<style>@font-face {src: url(AHEM____.TTF); font-family: ahem}</style>' | |
389 | '<p style="width:10em; font-family: ahem">' | |
390 | ' line 1<br><span>123 567 90</span>x' | |
391 | '</p>') | |
392 | html, = page.children | |
393 | body, = html.children | |
394 | p, = body.children | |
395 | line1, line2, line3 = p.children | |
396 | assert line1.children[0].text == 'line 1' | |
397 | assert line2.children[0].children[0].text == '123 567' | |
398 | assert line3.children[0].children[0].text == '90' | |
399 | assert line3.children[1].text == 'x' | |
400 | ||
401 | ||
402 | @assert_no_logs | |
403 | def test_breaking_linebox_regression_12(): | |
404 | # Regression test for https://github.com/Kozea/WeasyPrint/issues/953 | |
405 | page, = parse( | |
406 | '<style>@font-face {src: url(AHEM____.TTF); font-family: ahem}</style>' | |
407 | '<p style="width:10em; font-family: ahem">' | |
408 | ' <br><span>123 567 90</span>x' | |
409 | '</p>') | |
410 | html, = page.children | |
411 | body, = html.children | |
412 | p, = body.children | |
413 | line1, line2, line3 = p.children | |
414 | assert line2.children[0].children[0].text == '123 567' | |
415 | assert line3.children[0].children[0].text == '90' | |
416 | assert line3.children[1].text == 'x' | |
417 | ||
418 | ||
419 | @assert_no_logs | |
420 | def test_breaking_linebox_regression_13(): | |
421 | # Regression test for https://github.com/Kozea/WeasyPrint/issues/953 | |
422 | page, = parse( | |
423 | '<style>@font-face {src: url(AHEM____.TTF); font-family: ahem}</style>' | |
424 | '<p style="width:10em; font-family: ahem">' | |
425 | ' 123 567 90 <span>123 567 90</span>x' | |
426 | '</p>') | |
427 | html, = page.children | |
428 | body, = html.children | |
429 | p, = body.children | |
430 | line1, line2, line3 = p.children | |
431 | assert line1.children[0].text == '123 567 90' | |
432 | assert line2.children[0].children[0].text == '123 567' | |
433 | assert line3.children[0].children[0].text == '90' | |
434 | assert line3.children[1].text == 'x' | |
382 | 435 | |
383 | 436 | |
384 | 437 | @assert_no_logs |
1213 | 1213 | assert line_1.position_y == 3 |
1214 | 1214 | assert line_2.position_y == 43 |
1215 | 1215 | assert line_3.position_y == 83 |
1216 | ||
1217 | ||
1218 | @assert_no_logs | |
1219 | def test_margin_boxes_element(): | |
1220 | pages = render_pages(''' | |
1221 | <style> | |
1222 | footer { | |
1223 | position: running(footer); | |
1224 | } | |
1225 | @page { | |
1226 | margin: 50px; | |
1227 | size: 200px; | |
1228 | @bottom-center { | |
1229 | content: element(footer); | |
1230 | } | |
1231 | } | |
1232 | h1 { | |
1233 | height: 40px; | |
1234 | } | |
1235 | .pages:before { | |
1236 | content: counter(page); | |
1237 | } | |
1238 | .pages:after { | |
1239 | content: counter(pages); | |
1240 | } | |
1241 | </style> | |
1242 | <footer class="pages"> of </footer> | |
1243 | <h1>test1</h1> | |
1244 | <h1>test2</h1> | |
1245 | <h1>test3</h1> | |
1246 | <h1>test4</h1> | |
1247 | <h1>test5</h1> | |
1248 | <h1>test6</h1> | |
1249 | <footer>Static</footer> | |
1250 | ''') | |
1251 | footer1_text = ''.join( | |
1252 | getattr(node, 'text', '') | |
1253 | for node in pages[0].children[1].descendants()) | |
1254 | assert footer1_text == '1 of 3' | |
1255 | ||
1256 | footer2_text = ''.join( | |
1257 | getattr(node, 'text', '') | |
1258 | for node in pages[1].children[1].descendants()) | |
1259 | assert footer2_text == '2 of 3' | |
1260 | ||
1261 | footer3_text = ''.join( | |
1262 | getattr(node, 'text', '') | |
1263 | for node in pages[2].children[1].descendants()) | |
1264 | assert footer3_text == 'Static' | |
1265 | ||
1266 | ||
1267 | @assert_no_logs | |
1268 | @pytest.mark.parametrize('argument, texts', ( | |
1269 | # TODO: start doesn’t work because running elements are removed from the | |
1270 | # original tree, and the current implentation in | |
1271 | # layout.get_running_element_for uses the tree to know if it’s at the | |
1272 | # beginning of the page | |
1273 | ||
1274 | # ('start', ('', '2-first', '2-last', '3-last', '5')), | |
1275 | ||
1276 | ('first', ('', '2-first', '3-first', '3-last', '5')), | |
1277 | ('last', ('', '2-last', '3-last', '3-last', '5')), | |
1278 | ('first-except', ('', '', '', '3-last', '')), | |
1279 | )) | |
1280 | def test_running_elements(argument, texts): | |
1281 | pages = render_pages(''' | |
1282 | <style> | |
1283 | @page { | |
1284 | margin: 50px; | |
1285 | size: 200px; | |
1286 | @bottom-center { content: element(title %s) } | |
1287 | } | |
1288 | article { break-after: page } | |
1289 | h1 { position: running(title) } | |
1290 | </style> | |
1291 | <article> | |
1292 | <div>1</div> | |
1293 | </article> | |
1294 | <article> | |
1295 | <h1>2-first</h1> | |
1296 | <h1>2-last</h1> | |
1297 | </article> | |
1298 | <article> | |
1299 | <p>3</p> | |
1300 | <h1>3-first</h1> | |
1301 | <h1>3-last</h1> | |
1302 | </article> | |
1303 | <article> | |
1304 | </article> | |
1305 | <article> | |
1306 | <h1>5</h1> | |
1307 | </article> | |
1308 | ''' % argument) | |
1309 | assert len(pages) == 5 | |
1310 | for page, text in zip(pages, texts): | |
1311 | html, margin = page.children | |
1312 | if margin.children: | |
1313 | h1, = margin.children | |
1314 | line, = h1.children | |
1315 | textbox, = line.children | |
1316 | assert textbox.text == text | |
1317 | else: | |
1318 | assert not text | |
1319 | ||
1320 | ||
1321 | @assert_no_logs | |
1322 | def test_running_elements_display(): | |
1323 | page, = render_pages(''' | |
1324 | <style> | |
1325 | @page { | |
1326 | margin: 50px; | |
1327 | size: 200px; | |
1328 | @bottom-left { content: element(inline) } | |
1329 | @bottom-center { content: element(block) } | |
1330 | @bottom-right { content: element(table) } | |
1331 | } | |
1332 | table { position: running(table) } | |
1333 | div { position: running(block) } | |
1334 | span { position: running(inline) } | |
1335 | </style> | |
1336 | text | |
1337 | <table><tr><td>table</td></tr></table> | |
1338 | <div>block</div> | |
1339 | <span>inline</span> | |
1340 | ''') | |
1341 | html, left, center, right = page.children | |
1342 | assert ''.join( | |
1343 | getattr(node, 'text', '') for node in left.descendants()) == 'inline' | |
1344 | assert ''.join( | |
1345 | getattr(node, 'text', '') for node in center.descendants()) == 'block' | |
1346 | assert ''.join( | |
1347 | getattr(node, 'text', '') for node in right.descendants()) == 'table' |
2569 | 2569 | assert (tbody.content_box_x(), tbody.content_box_y()) == (20, 50) |
2570 | 2570 | assert (caption.content_box_x(), caption.content_box_y()) == (40, 80) |
2571 | 2571 | assert (h2.content_box_x(), h2.content_box_y()) == (20, 130) |
2572 | ||
2573 | ||
2574 | @assert_no_logs | |
2575 | @pytest.mark.parametrize('rows_expected, thead, tfoot, content', ( | |
2576 | ([[], ['Header', 'Footer']], 45, 45, '<p>content</p>'), | |
2577 | ([[], ['Header', 'Footer']], 85, 5, '<p>content</p>'), | |
2578 | ([['Header', 'Footer']], 30, 30, '<p>content</p>'), | |
2579 | ([[], ['Header']], 30, 110, '<p>content</p>'), | |
2580 | ([[], ['Header', 'Footer']], 30, 60, '<p>content</p>'), | |
2581 | ([[], ['Footer']], 110, 30, '<p>content</p>'), | |
2582 | ||
2583 | # We try to render the header and footer on the same page, but it does not | |
2584 | # fit. So we try to render the header or the footer on the next one, but | |
2585 | # nothing fit either. | |
2586 | ([[], []], 110, 110, '<p>content</p>'), | |
2587 | ||
2588 | ([['Header', 'Footer']], 30, 30, ''), | |
2589 | ([['Header']], 30, 110, ''), | |
2590 | ([['Header', 'Footer']], 30, 60, ''), | |
2591 | ([['Footer']], 110, 30, ''), | |
2592 | ([[]], 110, 110, ''), | |
2593 | )) | |
2594 | def test_table_empty_body(rows_expected, thead, tfoot, content): | |
2595 | html = ''' | |
2596 | <style> | |
2597 | @page { size: 100px } | |
2598 | p { height: 20px } | |
2599 | thead th { height: %spx } | |
2600 | tfoot th { height: %spx } | |
2601 | </style> | |
2602 | %s | |
2603 | <table> | |
2604 | <thead><tr><th>Header</th></tr></thead> | |
2605 | <tfoot><tr><th>Footer</th></tr></tfoot> | |
2606 | </table> | |
2607 | ''' % (thead, tfoot, content) | |
2608 | pages = render_pages(html) | |
2609 | assert len(pages) == len(rows_expected) | |
2610 | for i, page in enumerate(pages): | |
2611 | rows = [] | |
2612 | html, = page.children | |
2613 | body, = html.children | |
2614 | table_wrapper = body.children[-1] | |
2615 | if not table_wrapper.is_table_wrapper: | |
2616 | assert rows == rows_expected[i] | |
2617 | continue | |
2618 | table, = table_wrapper.children | |
2619 | for group in table.children: | |
2620 | for row in group.children: | |
2621 | cell, = row.children | |
2622 | line, = cell.children | |
2623 | text, = line.children | |
2624 | rows.append(text.text) | |
2625 | assert rows == rows_expected[i] |
8 | 8 | |
9 | 9 | """ |
10 | 10 | |
11 | import pytest | |
12 | ||
13 | from ..css.properties import KNOWN_PROPERTIES | |
11 | 14 | from .test_boxes import render_pages as parse |
12 | 15 | |
13 | 16 | |
94 | 97 | body, = html.children |
95 | 98 | paragraph, = body.children |
96 | 99 | assert paragraph.width == 10 |
100 | ||
101 | ||
102 | @pytest.mark.parametrize('prop', KNOWN_PROPERTIES) | |
103 | def test_variable_fallback(prop): | |
104 | parse(''' | |
105 | <style> | |
106 | div { | |
107 | --var: improperValue; | |
108 | %s: var(--var); | |
109 | } | |
110 | </style> | |
111 | <div></div> | |
112 | ''' % prop) |
146 | 146 | void g_type_init (void); |
147 | 147 | |
148 | 148 | void pango_layout_set_width (PangoLayout *layout, int width); |
149 | PangoAttrList * pango_layout_get_attributes(PangoLayout *layout); | |
149 | 150 | void pango_layout_set_attributes ( |
150 | 151 | PangoLayout *layout, PangoAttrList *attrs); |
151 | 152 | void pango_layout_set_text ( |
765 | 766 | if text and (word_spacing != 0 or letter_spacing != 0): |
766 | 767 | letter_spacing = units_from_double(letter_spacing) |
767 | 768 | space_spacing = units_from_double(word_spacing) + letter_spacing |
768 | attr_list = pango.pango_attr_list_new() | |
769 | attr_list = pango.pango_layout_get_attributes(self.layout) | |
770 | if not attr_list: | |
771 | # TODO: list should be freed | |
772 | attr_list = pango.pango_attr_list_new() | |
769 | 773 | |
770 | 774 | def add_attr(start, end, spacing): |
771 | 775 | # TODO: attributes should be freed |
780 | 784 | position = bytestring.find(b' ', position + 1) |
781 | 785 | |
782 | 786 | pango.pango_layout_set_attributes(self.layout, attr_list) |
783 | pango.pango_attr_list_unref(attr_list) | |
784 | 787 | |
785 | 788 | # Tabs width |
786 | 789 | if b'\t' in bytestring: |