-
-
Notifications
You must be signed in to change notification settings - Fork 696
/
Copy pathpage.py
921 lines (803 loc) · 36.5 KB
/
page.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
"""Layout for pages and CSS3 margin boxes."""
import copy
from math import inf
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
@outer.setter
def outer(self, new_outer_width):
self.inner = min(
max(self.min_content_size, new_outer_width - self.sugar),
self.max_content_size)
@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)
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``.
Described in: https://drafts.csswg.org/css-page-3/#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.
# https://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 == 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, available_size):
"""Compute and set a margin box fixed dimension on ``box``
Described in: https://drafts.csswg.org/css-page-3/#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 available_size:
The distance between the page box’s left right border edges
"""
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 not box_b.box.is_generated:
# Non-generated boxes get zero for every box-model property
assert box_b.inner == 0
if box_a.inner == box_c.inner == 'auto':
# A and C both have 'width: auto'
if available_size > (
box_a.outer_max_content_size +
box_c.outer_max_content_size):
# sum of the outer max-content widths
# is less than the available width
flex_space = (
available_size -
box_a.outer_max_content_size -
box_c.outer_max_content_size)
flex_factor_a = box_a.outer_max_content_size
flex_factor_c = box_c.outer_max_content_size
flex_factor_sum = flex_factor_a + flex_factor_c
if flex_factor_sum == 0:
flex_factor_sum = 1
box_a.outer = box_a.max_content_size + (
flex_space * flex_factor_a / flex_factor_sum)
box_c.outer = box_c.max_content_size + (
flex_space * flex_factor_c / flex_factor_sum)
elif available_size > (
box_a.outer_min_content_size +
box_c.outer_min_content_size):
# sum of the outer min-content widths
# is less than the available width
flex_space = (
available_size -
box_a.outer_min_content_size -
box_c.outer_min_content_size)
flex_factor_a = (
box_a.max_content_size - box_a.min_content_size)
flex_factor_c = (
box_c.max_content_size - box_c.min_content_size)
flex_factor_sum = flex_factor_a + flex_factor_c
if flex_factor_sum == 0:
flex_factor_sum = 1
box_a.outer = box_a.min_content_size + (
flex_space * flex_factor_a / flex_factor_sum)
box_c.outer = box_c.min_content_size + (
flex_space * flex_factor_c / flex_factor_sum)
else:
# otherwise
flex_space = (
available_size -
box_a.outer_min_content_size -
box_c.outer_min_content_size)
flex_factor_a = box_a.min_content_size
flex_factor_c = box_c.min_content_size
flex_factor_sum = flex_factor_a + flex_factor_c
if flex_factor_sum == 0:
flex_factor_sum = 1
box_a.outer = box_a.min_content_size + (
flex_space * flex_factor_a / flex_factor_sum)
box_c.outer = box_c.min_content_size + (
flex_space * flex_factor_c / flex_factor_sum)
else:
# only one box has 'width: auto'
if box_a.inner == 'auto':
box_a.outer = available_size - box_c.outer
elif box_c.inner == 'auto':
box_c.outer = available_size - box_a.outer
else:
if box_b.inner == 'auto':
# resolve any auto width of the middle box (B)
ac_max_content_size = 2 * max(
box_a.outer_max_content_size, box_c.outer_max_content_size)
if available_size > (
box_b.outer_max_content_size + ac_max_content_size):
flex_space = (
available_size -
box_b.outer_max_content_size -
ac_max_content_size)
flex_factor_b = box_b.outer_max_content_size
flex_factor_ac = ac_max_content_size
flex_factor_sum = flex_factor_b + flex_factor_ac
if flex_factor_sum == 0:
flex_factor_sum = 1
box_b.outer = box_b.max_content_size + (
flex_space * flex_factor_b / flex_factor_sum)
else:
ac_min_content_size = 2 * max(
box_a.outer_min_content_size, box_c.outer_min_content_size)
if available_size > (
box_b.outer_min_content_size + ac_min_content_size):
flex_space = (
available_size -
box_b.outer_min_content_size -
ac_min_content_size)
flex_factor_b = (
box_b.max_content_size - box_b.min_content_size)
flex_factor_ac = ac_max_content_size - ac_min_content_size
flex_factor_sum = flex_factor_b + flex_factor_ac
if flex_factor_sum == 0:
flex_factor_sum = 1
box_b.outer = box_b.min_content_size + (
flex_space * flex_factor_b / flex_factor_sum)
else:
flex_space = (
available_size -
box_b.outer_min_content_size -
ac_min_content_size)
flex_factor_b = box_b.min_content_size
flex_factor_ac = ac_min_content_size
flex_factor_sum = flex_factor_b + flex_factor_ac
if flex_factor_sum == 0:
flex_factor_sum = 1
box_b.outer = box_b.min_content_size + (
flex_space * flex_factor_b / flex_factor_sum)
if box_a.inner == 'auto':
box_a.outer = (available_size - box_b.outer) / 2
if box_c.inner == 'auto':
box_c.outer = (available_size - box_b.outer) / 2
# 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, e.g. '@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
# https://drafts.csswg.org/css-page-3/#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=-inf, skip_stack=None, page_is_empty=True,
absolute_boxes=positioned_boxes, fixed_boxes=positioned_boxes,
adjoining_margins=None, discard=False, max_lines=None)
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."
https://drafts.csswg.org/css-page-3/#page-box-page-rule
https://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 int page_number:
Page number, starts 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 https://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.current_footnote_area = footnote_area
reported_footnotes = context.reported_footnotes
context.reported_footnotes = []
for i, reported_footnote in enumerate(reported_footnotes):
context.footnotes.append(reported_footnote)
overflow = context.layout_footnote(reported_footnote)
if overflow and i != 0:
context.report_footnote(reported_footnote)
context.reported_footnotes = reported_footnotes[i:]
break
page_is_empty = True
adjoining_margins = []
positioned_boxes = [] # Mixed absolute and fixed
out_of_flow_boxes = []
broken_out_of_flow = {}
context_out_of_flow = context.broken_out_of_flow.values()
context.broken_out_of_flow = broken_out_of_flow
for box, containing_block, skip_stack in context_out_of_flow:
box.position_y = root_box.content_box_y()
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[out_of_flow_box] = (
box, containing_block, out_of_flow_resume_at)
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)
assert root_box
root_box.children = out_of_flow_boxes + root_box.children
footnote_area = build.create_anonymous_boxes(footnote_area.deepcopy())
footnote_area = block_level_layout(
context, footnote_area, -inf, None, footnote_area.page, True,
positioned_boxes, positioned_boxes)[0]
footnote_area.translate(dy=-footnote_area.margin_height())
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)
context.finish_block_formatting_context(root_box)
page.children = [root_box, footnote_area]
# 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 page.descendants(placeholders=True):
# 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.
if 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
next_page = page_maker[page_number - 1][1]
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:
# https://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)
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))
next_page_name = '' if blank else initial_next_page['page']
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
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
reported_footnotes = None
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)
reported_footnotes = context.reported_footnotes
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]
reported_footnotes = None
yield pages[i]
i += 1
if resume_at is None and not reported_footnotes:
# Throw away obsolete pages and content
context.page_maker = context.page_maker[:i + 1]
context.broken_out_of_flow.clear()
context.reported_footnotes.clear()
return