1"""
2    weasyprint.tests.layout
3    -----------------------
4
5    Tests for floating boxes layout.
6
7    :copyright: Copyright 2011-2019 Simon Sapin and contributors, see AUTHORS.
8    :license: BSD, see LICENSE for details.
9
10"""
11
12import pytest
13
14from ..formatting_structure import boxes
15from .test_boxes import render_pages
16from .testing_utils import assert_no_logs
17
18
19def outer_area(box):
20    """Return the (x, y, w, h) rectangle for the outer area of a box."""
21    return (box.position_x, box.position_y,
22            box.margin_width(), box.margin_height())
23
24
25@assert_no_logs
26def test_floats_1():
27    # adjacent-floats-001
28    page, = render_pages('''
29      <style>
30        div { float: left }
31        img { width: 100px; vertical-align: top }
32      </style>
33      <div><img src=pattern.png /></div>
34      <div><img src=pattern.png /></div>''')
35    html, = page.children
36    body, = html.children
37    div_1, div_2 = body.children
38    assert outer_area(div_1) == (0, 0, 100, 100)
39    assert outer_area(div_2) == (100, 0, 100, 100)
40
41
42@assert_no_logs
43def test_floats_2():
44    # c414-flt-fit-000
45    page, = render_pages('''
46      <style>
47        body { width: 290px }
48        div { float: left; width: 100px;  }
49        img { width: 60px; vertical-align: top }
50      </style>
51      <div><img src=pattern.png /><!-- 1 --></div>
52      <div><img src=pattern.png /><!-- 2 --></div>
53      <div><img src=pattern.png /><!-- 4 --></div>
54      <img src=pattern.png /><!-- 3
55      --><img src=pattern.png /><!-- 5 -->''')
56    html, = page.children
57    body, = html.children
58    div_1, div_2, div_4, anon_block = body.children
59    line_3, line_5 = anon_block.children
60    img_3, = line_3.children
61    img_5, = line_5.children
62    assert outer_area(div_1) == (0, 0, 100, 60)
63    assert outer_area(div_2) == (100, 0, 100, 60)
64    assert outer_area(img_3) == (200, 0, 60, 60)
65
66    assert outer_area(div_4) == (0, 60, 100, 60)
67    assert outer_area(img_5) == (100, 60, 60, 60)
68
69
70@assert_no_logs
71def test_floats_3():
72    # c414-flt-fit-002
73    page, = render_pages('''
74      <style type="text/css">
75        body { width: 200px }
76        p { width: 70px; height: 20px }
77        .left { float: left }
78        .right { float: right }
79      </style>
80      <p class="left"> ⇦ A 1 </p>
81      <p class="left"> ⇦ B 2 </p>
82      <p class="left"> ⇦ A 3 </p>
83      <p class="right"> B 4 ⇨ </p>
84      <p class="left"> ⇦ A 5 </p>
85      <p class="right"> B 6 ⇨ </p>
86      <p class="right"> B 8 ⇨ </p>
87      <p class="left"> ⇦ A 7 </p>
88      <p class="left"> ⇦ A 9 </p>
89      <p class="left"> ⇦ B 10 </p>
90    ''')
91    html, = page.children
92    body, = html.children
93    positions = [(paragraph.position_x, paragraph.position_y)
94                 for paragraph in body.children]
95    assert positions == [
96        (0, 0), (70, 0), (0, 20), (130, 20), (0, 40), (130, 40),
97        (130, 60), (0, 60), (0, 80), (70, 80), ]
98
99
100@assert_no_logs
101def test_floats_4():
102    # c414-flt-wrap-000 ... more or less
103    page, = render_pages('''
104      <style>
105        body { width: 100px }
106        p { float: left; height: 100px }
107        img { width: 60px; vertical-align: top }
108      </style>
109      <p style="width: 20px"></p>
110      <p style="width: 100%"></p>
111      <img src=pattern.png /><img src=pattern.png />
112    ''')
113    html, = page.children
114    body, = html.children
115    p_1, p_2, anon_block = body.children
116    line_1, line_2 = anon_block.children
117    assert anon_block.position_y == 0
118    assert (line_1.position_x, line_1.position_y) == (20, 0)
119    assert (line_2.position_x, line_2.position_y) == (0, 200)
120
121
122@assert_no_logs
123def test_floats_5():
124    # c414-flt-wrap-000 with text ... more or less
125    page, = render_pages('''
126      <style>
127        body { width: 100px; font: 60px ahem; }
128        p { float: left; height: 100px }
129        img { width: 60px; vertical-align: top }
130      </style>
131      <p style="width: 20px"></p>
132      <p style="width: 100%"></p>
133      A B
134    ''')
135    html, = page.children
136    body, = html.children
137    p_1, p_2, anon_block = body.children
138    line_1, line_2 = anon_block.children
139    assert anon_block.position_y == 0
140    assert (line_1.position_x, line_1.position_y) == (20, 0)
141    assert (line_2.position_x, line_2.position_y) == (0, 200)
142
143
144@assert_no_logs
145def test_floats_6():
146    # floats-placement-vertical-001b
147    page, = render_pages('''
148      <style>
149        body { width: 90px; font-size: 0 }
150        img { vertical-align: top }
151      </style>
152      <body>
153      <span>
154        <img src=pattern.png style="width: 50px" />
155        <img src=pattern.png style="width: 50px" />
156        <img src=pattern.png style="float: left; width: 30px" />
157      </span>
158    ''')
159    html, = page.children
160    body, = html.children
161    line_1, line_2 = body.children
162    span_1, = line_1.children
163    span_2, = line_2.children
164    img_1, = span_1.children
165    img_2, img_3 = span_2.children
166    assert outer_area(img_1) == (0, 0, 50, 50)
167    assert outer_area(img_2) == (30, 50, 50, 50)
168    assert outer_area(img_3) == (0, 50, 30, 30)
169
170
171@assert_no_logs
172def test_floats_7():
173    # Variant of the above: no <span>
174    page, = render_pages('''
175      <style>
176        body { width: 90px; font-size: 0 }
177        img { vertical-align: top }
178      </style>
179      <body>
180      <img src=pattern.png style="width: 50px" />
181      <img src=pattern.png style="width: 50px" />
182      <img src=pattern.png style="float: left; width: 30px" />
183    ''')
184    html, = page.children
185    body, = html.children
186    line_1, line_2 = body.children
187    img_1, = line_1.children
188    img_2, img_3 = line_2.children
189    assert outer_area(img_1) == (0, 0, 50, 50)
190    assert outer_area(img_2) == (30, 50, 50, 50)
191    assert outer_area(img_3) == (0, 50, 30, 30)
192
193
194@assert_no_logs
195def test_floats_8():
196    # Floats do no affect other pages
197    page_1, page_2 = render_pages('''
198      <style>
199        body { width: 90px; font-size: 0 }
200        img { vertical-align: top }
201      </style>
202      <body>
203      <img src=pattern.png style="float: left; width: 30px" />
204      <img src=pattern.png style="width: 50px" />
205      <div style="page-break-before: always"></div>
206      <img src=pattern.png style="width: 50px" />
207    ''')
208    html, = page_1.children
209    body, = html.children
210    float_img, anon_block, = body.children
211    line, = anon_block.children
212    img_1, = line.children
213    assert outer_area(float_img) == (0, 0, 30, 30)
214    assert outer_area(img_1) == (30, 0, 50, 50)
215
216    html, = page_2.children
217    body, = html.children
218    div, anon_block = body.children
219    line, = anon_block.children
220    img_2, = line.children
221
222
223@assert_no_logs
224def test_floats_9():
225    # Regression test
226    # https://github.com/Kozea/WeasyPrint/issues/263
227    page, = render_pages('''<div style="top:100%; float:left">''')
228
229
230@assert_no_logs
231def test_floats_page_breaks_1():
232    # Tests floated images shorter than the page
233    pages = render_pages('''
234      <style>
235        @page { size: 100px; margin: 10px }
236        img { height: 45px; width:70px; float: left;}
237      </style>
238      <body>
239        <img src=pattern.png>
240          <!-- page break should be here !!! -->
241        <img src=pattern.png>
242    ''')
243
244    assert len(pages) == 2
245
246    page_images = []
247    for page in pages:
248        images = [d for d in page.descendants() if d.element_tag == 'img']
249        assert all([img.element_tag == 'img' for img in images])
250        assert all([img.position_x == 10 for img in images])
251        page_images.append(images)
252        del images
253    positions_y = [[img.position_y for img in images]
254                   for images in page_images]
255    assert positions_y == [[10], [10]]
256
257
258@assert_no_logs
259def test_floats_page_breaks_2():
260    # Tests floated images taller than the page
261    pages = render_pages('''
262      <style>
263        @page { size: 100px; margin: 10px }
264        img { height: 81px; width:70px; float: left;}
265      </style>
266      <body>
267        <img src=pattern.png>
268          <!-- page break should be here !!! -->
269        <img src=pattern.png>
270    ''')
271
272    assert len(pages) == 2
273
274    page_images = []
275    for page in pages:
276        images = [d for d in page.descendants() if d.element_tag == 'img']
277        assert all([img.element_tag == 'img' for img in images])
278        assert all([img.position_x == 10 for img in images])
279        page_images.append(images)
280        del images
281    positions_y = [[img.position_y for img in images]
282                   for images in page_images]
283    assert positions_y == [[10], [10]]
284
285
286@assert_no_logs
287def test_floats_page_breaks_3():
288    # Tests floated images shorter than the page
289    pages = render_pages('''
290      <style>
291        @page { size: 100px; margin: 10px }
292        img { height: 30px; width:70px; float: left;}
293      </style>
294      <body>
295        <img src=pattern.png>
296        <img src=pattern.png>
297          <!-- page break should be here !!! -->
298        <img src=pattern.png>
299        <img src=pattern.png>
300          <!-- page break should be here !!! -->
301        <img src=pattern.png>
302    ''')
303
304    assert len(pages) == 3
305
306    page_images = []
307    for page in pages:
308        images = [d for d in page.descendants() if d.element_tag == 'img']
309        assert all([img.element_tag == 'img' for img in images])
310        assert all([img.position_x == 10 for img in images])
311        page_images.append(images)
312        del images
313    positions_y = [[img.position_y for img in images]
314                   for images in page_images]
315    assert positions_y == [[10, 40], [10, 40], [10]]
316
317
318@assert_no_logs
319def test_floats_page_breaks_4():
320    # last float does not fit, pushed to next page
321    pages = render_pages('''
322      <style>
323        @page{
324          size: 110px;
325          margin: 10px;
326          padding: 0;
327        }
328        .large {
329          width: 10px;
330          height: 60px;
331        }
332        .small {
333          width: 10px;
334          height: 20px;
335        }
336      </style>
337      <body>
338        <div class="large"></div>
339        <div class="small"></div>
340        <div class="large"></div>
341    ''')
342
343    assert len(pages) == 2
344    page_divs = []
345    for page in pages:
346        divs = [div for div in page.descendants() if div.element_tag == 'div']
347        assert all([div.element_tag == 'div' for div in divs])
348        page_divs.append(divs)
349        del divs
350
351    positions_y = [[div.position_y for div in divs] for divs in page_divs]
352    assert positions_y == [[10, 70], [10]]
353
354
355@assert_no_logs
356def test_floats_page_breaks_5():
357    # last float does not fit, pushed to next page
358    # center div must not
359    pages = render_pages('''
360      <style>
361        @page{
362          size: 110px;
363          margin: 10px;
364          padding: 0;
365        }
366        .large {
367          width: 10px;
368          height: 60px;
369        }
370        .small {
371          width: 10px;
372          height: 20px;
373          page-break-after: avoid;
374        }
375      </style>
376      <body>
377        <div class="large"></div>
378        <div class="small"></div>
379        <div class="large"></div>
380    ''')
381
382    assert len(pages) == 2
383    page_divs = []
384    for page in pages:
385        divs = [div for div in page.descendants() if div.element_tag == 'div']
386        assert all([div.element_tag == 'div' for div in divs])
387        page_divs.append(divs)
388        del divs
389
390    positions_y = [[div.position_y for div in divs] for divs in page_divs]
391    assert positions_y == [[10], [10, 30]]
392
393
394@assert_no_logs
395def test_floats_page_breaks_6():
396    # center div must be the last element,
397    # but float won't fit and will get pushed anyway
398    pages = render_pages('''
399      <style>
400        @page{
401          size: 110px;
402          margin: 10px;
403          padding: 0;
404        }
405        .large {
406          width: 10px;
407          height: 80px;
408        }
409        .small {
410          width: 10px;
411          height: 20px;
412          page-break-after: avoid;
413        }
414      </style>
415      <body>
416        <div class="large"></div>
417        <div class="small"></div>
418        <div class="large"></div>
419    ''')
420
421    assert len(pages) == 3
422    page_divs = []
423    for page in pages:
424        divs = [div for div in page.descendants() if div.element_tag == 'div']
425        assert all([div.element_tag == 'div' for div in divs])
426        page_divs.append(divs)
427        del divs
428
429    positions_y = [[div.position_y for div in divs] for divs in page_divs]
430    assert positions_y == [[10], [10], [10]]
431
432
433@assert_no_logs
434def test_preferred_widths_1():
435    def get_float_width(body_width):
436        page, = render_pages('''
437          <style>
438            @font-face { src: url(AHEM____.TTF); font-family: ahem }
439          </style>
440          <body style="width: %spx; font-family: ahem">
441          <p style="white-space: pre-line; float: left">
442            Lorem ipsum dolor sit amet,
443              consectetur elit
444          </p>
445                   <!--  ^  No-break space here  -->
446        ''' % body_width)
447        html, = page.children
448        body, = html.children
449        paragraph, = body.children
450        return paragraph.width
451    # Preferred minimum width:
452    assert get_float_width(10) == len('consectetur elit') * 16
453    # Preferred width:
454    assert get_float_width(1000000) == len('Lorem ipsum dolor sit amet,') * 16
455
456
457@assert_no_logs
458def test_preferred_widths_2():
459    # Non-regression test:
460    # Incorrect whitespace handling in preferred width used to cause
461    # unnecessary line break.
462    page, = render_pages('''
463      <p style="float: left">Lorem <em>ipsum</em> dolor.</p>
464    ''')
465    html, = page.children
466    body, = html.children
467    paragraph, = body.children
468    assert len(paragraph.children) == 1
469    assert isinstance(paragraph.children[0], boxes.LineBox)
470
471
472@assert_no_logs
473def test_preferred_widths_3():
474    page, = render_pages('''
475      <style>img { width: 20px }</style>
476      <p style="float: left">
477        <img src=pattern.png><img src=pattern.png><br>
478        <img src=pattern.png></p>
479    ''')
480    html, = page.children
481    body, = html.children
482    paragraph, = body.children
483    assert paragraph.width == 40
484
485
486@assert_no_logs
487def test_preferred_widths_4():
488    page, = render_pages(
489        '<style>'
490        '  @font-face { src: url(AHEM____.TTF); font-family: ahem }'
491        '  p { font: 20px ahem }'
492        '</style>'
493        '<p style="float: left">XX<br>XX<br>X</p>')
494    html, = page.children
495    body, = html.children
496    paragraph, = body.children
497    assert paragraph.width == 40
498
499
500@assert_no_logs
501def test_preferred_widths_5():
502    # The space is the start of the line is collapsed.
503    page, = render_pages(
504        '<style>'
505        '  @font-face { src: url(AHEM____.TTF); font-family: ahem }'
506        '  p { font: 20px ahem }'
507        '</style>'
508        '<p style="float: left">XX<br> XX<br>X</p>')
509    html, = page.children
510    body, = html.children
511    paragraph, = body.children
512    assert paragraph.width == 40
513
514
515@assert_no_logs
516def test_float_in_inline():
517    page, = render_pages('''
518      <style>
519        @font-face { src: url(AHEM____.TTF); font-family: ahem }
520        body {
521          font-family: ahem;
522          font-size: 20px;
523        }
524        p {
525          width: 14em;
526          text-align: justify;
527        }
528        span {
529          float: right;
530        }
531      </style>
532      <p>
533        aa bb <a><span>cc</span> ddd</a> ee ff
534      </p>
535    ''')
536    html, = page.children
537    body, = html.children
538    paragraph, = body.children
539    line1, line2 = paragraph.children
540
541    p1, a, p2 = line1.children
542    assert p1.width == 6 * 20
543    assert p1.text == 'aa bb '
544    assert p1.position_x == 0 * 20
545    assert p2.width == 3 * 20
546    assert p2.text == ' ee'
547    assert p2.position_x == 9 * 20
548    span, a_text = a.children
549    assert a_text.width == 3 * 20  # leading space collapse
550    assert a_text.text == 'ddd'
551    assert a_text.position_x == 6 * 20
552    assert span.width == 2 * 20
553    assert span.children[0].children[0].text == 'cc'
554    assert span.position_x == 12 * 20
555
556    p3, = line2.children
557    assert p3.width == 2 * 20
558
559
560@assert_no_logs
561def test_float_next_line():
562    page, = render_pages('''
563      <style>
564        @font-face { src: url(AHEM____.TTF); font-family: ahem }
565        body {
566          font-family: ahem;
567          font-size: 20px;
568        }
569        p {
570          text-align: justify;
571          width: 13em;
572        }
573        span {
574          float: left;
575        }
576      </style>
577      <p>pp pp pp pp <a><span>ppppp</span> aa</a> pp pp pp pp pp</p>''')
578    html, = page.children
579    body, = html.children
580    paragraph, = body.children
581    line1, line2, line3 = paragraph.children
582    assert len(line1.children) == 1
583    assert len(line3.children) == 1
584    a, p = line2.children
585    span, a_text = a.children
586    assert span.position_x == 0
587    assert span.width == 5 * 20
588    assert a_text.position_x == a.position_x == 5 * 20
589    assert a_text.width == a.width == 2 * 20
590    assert p.position_x == 7 * 20
591
592
593@assert_no_logs
594def test_float_text_indent_1():
595    page, = render_pages('''
596      <style>
597        @font-face { src: url(AHEM____.TTF); font-family: ahem }
598        body {
599          font-family: ahem;
600          font-size: 20px;
601        }
602        p {
603          text-align: justify;
604          text-indent: 1em;
605          width: 14em;
606        }
607        span {
608          float: left;
609        }
610      </style>
611      <p><a>aa <span>float</span> aa</a></p>''')
612    html, = page.children
613    body, = html.children
614    paragraph, = body.children
615    line1, = paragraph.children
616    a, = line1.children
617    a1, span, a2 = a.children
618    span_text, = span.children
619    assert span.position_x == span_text.position_x == 0
620    assert span.width == span_text.width == (
621        (1 + 5) * 20)  # text-indent + span text
622    assert a1.width == 3 * 20
623    assert a1.position_x == (1 + 5 + 1) * 20  # span + a1 text-indent
624    assert a2.width == 2 * 20  # leading space collapse
625    assert a2.position_x == (1 + 5 + 1 + 3) * 20  # span + a1 t-i + a1
626
627
628@assert_no_logs
629def test_float_text_indent_2():
630    page, = render_pages('''
631      <style>
632        @font-face { src: url(AHEM____.TTF); font-family: ahem }
633        body {
634          font-family: ahem;
635          font-size: 20px;
636        }
637        p {
638          text-align: justify;
639          text-indent: 1em;
640          width: 14em;
641        }
642        span {
643          float: left;
644        }
645      </style>
646      <p>
647        oooooooooooo
648        <a>aa <span>float</span> aa</a></p>''')
649    html, = page.children
650    body, = html.children
651    paragraph, = body.children
652    line1, line2 = paragraph.children
653
654    p1, = line1.children
655    assert p1.position_x == 1 * 20  # text-indent
656    assert p1.width == 12 * 20  # p text
657
658    a, = line2.children
659    a1, span, a2 = a.children
660    span_text, = span.children
661    assert span.position_x == span_text.position_x == 0
662    assert span.width == span_text.width == (
663        (1 + 5) * 20)  # text-indent + span text
664    assert a1.width == 3 * 20
665    assert a1.position_x == (1 + 5) * 20  # span
666    assert a2.width == 2 * 20  # leading space collapse
667    assert a2.position_x == (1 + 5 + 3) * 20  # span + a1
668
669
670@assert_no_logs
671def test_float_text_indent_3():
672    page, = render_pages('''
673      <style>
674        @font-face { src: url(AHEM____.TTF); font-family: ahem }
675        body {
676          font-family: ahem;
677          font-size: 20px;
678        }
679        p {
680          text-align: justify;
681          text-indent: 1em;
682          width: 14em;
683        }
684        span {
685          float: right;
686        }
687      </style>
688      <p>
689        oooooooooooo
690        <a>aa <span>float</span> aa</a>
691        oooooooooooo
692      </p>''')
693    html, = page.children
694    body, = html.children
695    paragraph, = body.children
696    line1, line2, line3 = paragraph.children
697
698    p1, = line1.children
699    assert p1.position_x == 1 * 20  # text-indent
700    assert p1.width == 12 * 20  # p text
701
702    a, = line2.children
703    a1, span, a2 = a.children
704    span_text, = span.children
705    assert span.position_x == span_text.position_x == (14 - 5 - 1) * 20
706    assert span.width == span_text.width == (
707        (1 + 5) * 20)  # text-indent + span text
708    assert a1.position_x == 0  # span
709    assert a2.width == 2 * 20  # leading space collapse
710    assert a2.position_x == (14 - 5 - 1 - 2) * 20
711
712    p2, = line3.children
713    assert p2.position_x == 0
714    assert p2.width == 12 * 20  # p text
715
716
717@pytest.mark.xfail
718@assert_no_logs
719def test_float_fail():
720    page, = render_pages('''
721      <style>
722        @font-face { src: url(AHEM____.TTF); font-family: ahem }
723        body {
724          font-family: ahem;
725          font-size: 20px;
726        }
727        p {
728          text-align: justify;
729          width: 12em;
730        }
731        span {
732          float: left;
733          background: red;
734        }
735        a {
736          background: yellow;
737        }
738      </style>
739      <p>bb bb pp bb pp pb <a><span>pp pp</span> apa</a> bb bb</p>''')
740    html, = page.children
741    body, = html.children
742    paragraph, = body.children
743    line1, line2, line3 = paragraph.children
744