Repository: Kozea/WeasyPrint (python)

Issue:
Table headers and footers are not displayed when nothing else is on the page
When I print following webpage, the first table (with header) is not printed, unless I remove <thead> tags. I got the error when I was trying to print irregular tables (some of the tables were connected to look like one table).

http://langust.x10.mx/test_wrongly_printed_header.html


Pull request description:
Fix rendering for header and footer on empty tables
Fix #129, #979 and #1001

Patch:
diff --git a/.travis.yml b/.travis.yml
index f8d22acc..bc07a621 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -11,7 +11,7 @@ matrix:
       python: 3.5
     - os: linux
       python: 3.6
-    - dist: bionic
+    - os: linux
       python: 3.7
     - dist: bionic
       python: 3.8
diff --git a/weasyprint/css/computed_values.py b/weasyprint/css/computed_values.py
index ff47cb4c..76d99215 100644
--- a/weasyprint/css/computed_values.py
+++ b/weasyprint/css/computed_values.py
@@ -18,8 +18,7 @@ from tinycss2.color3 import parse_color
 from .. import text
 from ..logger import LOGGER
 from ..urls import get_link_attribute
-from .properties import (
-    INHERITED, INITIAL_NOT_COMPUTED, INITIAL_VALUES, Dimension)
+from .properties import INHERITED, INITIAL_VALUES, Dimension
 from .utils import (
     ANGLE_TO_RADIANS, LENGTH_UNITS, LENGTHS_TO_PIXELS, check_var_function,
     safe_urljoin)
@@ -219,7 +218,6 @@ def compute(element, pseudo_type, specified, computed, parent_style,
 
         value = specified[name]
         function = getter(name)
-        already_computed_value = False
 
         if value and isinstance(value, tuple) and value[0] == 'var()':
             variable_name, default = value[1]
@@ -245,15 +243,13 @@ def compute(element, pseudo_type, specified, computed, parent_style,
                     'for property `%s`.', computed_value,
                     variable_name.replace('_', '-'), name.replace('_', '-'))
                 if name in INHERITED and parent_style:
-                    already_computed_value = True
                     value = parent_style[name]
                 else:
-                    already_computed_value = name not in INITIAL_NOT_COMPUTED
                     value = INITIAL_VALUES[name]
             else:
                 value = new_value
 
-        if function is not None and not already_computed_value:
+        if function is not None:
             value = function(computer, name, value)
         # else: same as specified
 
diff --git a/weasyprint/css/validation/properties.py b/weasyprint/css/validation/properties.py
index 9aaebbfc..09c40af5 100644
--- a/weasyprint/css/validation/properties.py
+++ b/weasyprint/css/validation/properties.py
@@ -1374,7 +1374,6 @@ def string_set(tokens, base_url):
 
 @property()
 def transform(tokens):
-    """Validation for ``transform``."""
     if get_single_keyword(tokens) == 'none':
         return ()
     else:
@@ -1382,7 +1381,7 @@ def transform(tokens):
         for token in tokens:
             function = parse_function(token)
             if not function:
-                return
+                raise InvalidValues
             name, args = function
 
             if len(args) == 1:
@@ -1403,7 +1402,7 @@ def transform(tokens):
                 elif name == 'scale' and args[0].type == 'number':
                     transforms.append(('scale', (args[0].value,) * 2))
                 else:
-                    return
+                    raise InvalidValues
             elif len(args) == 2:
                 if name == 'scale' and all(a.type == 'number' for a in args):
                     transforms.append((name, tuple(arg.value for arg in args)))
@@ -1413,10 +1412,10 @@ def transform(tokens):
                     if name == 'translate' and all(lengths):
                         transforms.append((name, lengths))
                     else:
-                        return
+                        raise InvalidValues
             elif len(args) == 6 and name == 'matrix' and all(
                     a.type == 'number' for a in args):
                 transforms.append((name, tuple(arg.value for arg in args)))
             else:
-                return
+                raise InvalidValues
         return tuple(transforms)
diff --git a/weasyprint/layout/tables.py b/weasyprint/layout/tables.py
index d24b5807..85190266 100644
--- a/weasyprint/layout/tables.py
+++ b/weasyprint/layout/tables.py
@@ -327,12 +327,28 @@ def table_layout(context, table, max_position_y, skip_stack, containing_block,
     # Layout for row groups, rows and cells
     position_y = table.content_box_y() + border_spacing_y
     initial_position_y = position_y
+    table_rows = [
+        child for child in table.children
+        if not child.is_header and not child.is_footer]
 
     def all_groups_layout():
+        # If the page is not empty, we try to render the header and the footer
+        # on it. If the table does not fit on the page, we try to render it on
+        # the next page.
+
+        # If the page is empty and the header and footer are too big, there
+        # are not rendered. If no row can be rendered because of the header and
+        # the footer, the header and/or the footer are not rendered.
+
+        if page_is_empty:
+            header_footer_max_position_y = max_position_y
+        else:
+            header_footer_max_position_y = float('inf')
+
         if table.children and table.children[0].is_header:
             header = table.children[0]
             header, resume_at, next_page = group_layout(
-                header, position_y, max_position_y,
+                header, position_y, header_footer_max_position_y,
                 skip_stack=None, page_is_empty=False)
             if header and not resume_at:
                 header_height = header.height + border_spacing_y
@@ -344,7 +360,7 @@ def table_layout(context, table, max_position_y, skip_stack, containing_block,
         if table.children and table.children[-1].is_footer:
             footer = table.children[-1]
             footer, resume_at, next_page = group_layout(
-                footer, position_y, max_position_y,
+                footer, position_y, header_footer_max_position_y,
                 skip_stack=None, page_is_empty=False)
             if footer and not resume_at:
                 footer_height = footer.height + border_spacing_y
@@ -370,7 +386,7 @@ def table_layout(context, table, max_position_y, skip_stack, containing_block,
                     position_y=position_y + header_height,
                     max_position_y=max_position_y - footer_height,
                     page_is_empty=avoid_breaks))
-            if new_table_children or not page_is_empty:
+            if new_table_children or not table_rows or not page_is_empty:
                 footer.translate(dy=end_position_y - footer.position_y)
                 end_position_y += footer_height
                 return (header, new_table_children, footer,
@@ -387,7 +403,7 @@ def table_layout(context, table, max_position_y, skip_stack, containing_block,
                     position_y=position_y + header_height,
                     max_position_y=max_position_y,
                     page_is_empty=avoid_breaks))
-            if new_table_children or not page_is_empty:
+            if new_table_children or not table_rows or not page_is_empty:
                 return (header, new_table_children, footer,
                         end_position_y, resume_at, next_page)
             else:
@@ -402,7 +418,7 @@ def table_layout(context, table, max_position_y, skip_stack, containing_block,
                     position_y=position_y,
                     max_position_y=max_position_y - footer_height,
                     page_is_empty=avoid_breaks))
-            if new_table_children or not page_is_empty:
+            if new_table_children or not table_rows or not page_is_empty:
                 footer.translate(dy=end_position_y - footer.position_y)
                 end_position_y += footer_height
                 return (header, new_table_children, footer,


Test patch:
diff --git a/weasyprint/tests/test_layout/test_table.py b/weasyprint/tests/test_layout/test_table.py
index 64c874b2..4d9804a1 100644
--- a/weasyprint/tests/test_layout/test_table.py
+++ b/weasyprint/tests/test_layout/test_table.py
@@ -2570,3 +2570,57 @@ def test_table_caption_margin_bottom():
     assert (tbody.content_box_x(), tbody.content_box_y()) == (20, 50)
     assert (caption.content_box_x(), caption.content_box_y()) == (40, 80)
     assert (h2.content_box_x(), h2.content_box_y()) == (20, 130)
+
+
+@assert_no_logs
+@pytest.mark.parametrize('rows_expected, thead, tfoot, content', (
+    ([[], ['Header', 'Footer']], 45, 45, '<p>content</p>'),
+    ([[], ['Header', 'Footer']], 85, 5, '<p>content</p>'),
+    ([['Header', 'Footer']], 30, 30, '<p>content</p>'),
+    ([[], ['Header']], 30, 110, '<p>content</p>'),
+    ([[], ['Header', 'Footer']], 30, 60, '<p>content</p>'),
+    ([[], ['Footer']], 110, 30, '<p>content</p>'),
+
+    # We try to render the header and footer on the same page, but it does not
+    # fit. So we try to render the header or the footer on the next one, but
+    # nothing fit either.
+    ([[], []], 110, 110, '<p>content</p>'),
+
+    ([['Header', 'Footer']], 30, 30, ''),
+    ([['Header']], 30, 110, ''),
+    ([['Header', 'Footer']], 30, 60, ''),
+    ([['Footer']], 110, 30, ''),
+    ([[]], 110, 110, ''),
+))
+def test_table_empty_body(rows_expected, thead, tfoot, content):
+    html = '''
+      <style>
+        @page { size: 100px }
+        p { height: 20px }
+        thead th { height: %spx }
+        tfoot th { height: %spx }
+      </style>
+      %s
+      <table>
+        <thead><tr><th>Header</th></tr></thead>
+        <tfoot><tr><th>Footer</th></tr></tfoot>
+      </table>
+    ''' % (thead, tfoot, content)
+    pages = render_pages(html)
+    assert len(pages) == len(rows_expected)
+    for i, page in enumerate(pages):
+        rows = []
+        html, = page.children
+        body, = html.children
+        table_wrapper = body.children[-1]
+        if not table_wrapper.is_table_wrapper:
+            assert rows == rows_expected[i]
+            continue
+        table, = table_wrapper.children
+        for group in table.children:
+            for row in group.children:
+                cell, = row.children
+                line, = cell.children
+                text, = line.children
+                rows.append(text.text)
+        assert rows == rows_expected[i]
diff --git a/weasyprint/tests/test_variables.py b/weasyprint/tests/test_variables.py
index 1cc43950..60b89bc4 100644
--- a/weasyprint/tests/test_variables.py
+++ b/weasyprint/tests/test_variables.py
@@ -9,9 +9,6 @@
 
 """
 
-import pytest
-
-from ..css.properties import KNOWN_PROPERTIES
 from .test_boxes import render_pages as parse
 
 
@@ -98,16 +95,3 @@ def test_variable_initial():
     body, = html.children
     paragraph, = body.children
     assert paragraph.width == 10
-
-
-@pytest.mark.parametrize('prop', KNOWN_PROPERTIES)
-def test_variable_fallback(prop):
-    parse('''
-      <style>
-        div {
-          --var: improperValue;
-          %s: var(--var);
-        }
-      </style>
-      <div></div>
-    ''' % prop)