From ecc794143b6f5d0ae99aed64c6fd04c62491d3a6 Mon Sep 17 00:00:00 2001 From: kygoh Date: Sun, 21 May 2023 08:05:58 +0000 Subject: [PATCH 01/21] Add test case for table with border-collapse in page margins --- tests/layout/test_page.py | 65 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/tests/layout/test_page.py b/tests/layout/test_page.py index 4e9f0d277..72e75cbd7 100644 --- a/tests/layout/test_page.py +++ b/tests/layout/test_page.py @@ -1513,3 +1513,68 @@ def test_running_float(): Hello! ''') + + +def _get_grid(table_wrapper): + table, = table_wrapper.children + return tuple( + [[(style, width, color) if width else None + for _score, (style, width, color) in column] + for column in grid] + for grid in table.collapsed_border_grid) + + +black = (0, 0, 0, 1) +red = (1, 0, 0, 1) +black_3 = ('solid', 3, black) +red_1 = ('solid', 1, red) + + +@assert_no_logs +def test_running_elements_table_border_collapse(): + page0, page1 = render_pages(''' + + + + +
A B
C D
+
page0
+
page1
+ ''') + + page0_html, page0_center = page0.children + page0_table = page0_center.children[0] + page0_vertical_borders, page0_horizontal_borders = _get_grid(page0_table) + assert page0_vertical_borders == [ + [black_3, red_1, black_3], + [black_3, red_1, black_3], + ] + assert page0_horizontal_borders == [ + [black_3, black_3], + [red_1, red_1], + [black_3, black_3], + ] + + page1_html, page1_center = page1.children + page1_table = page1_center.children[0] + page1_vertical_borders, page1_horizontal_borders = _get_grid(page1_table) + assert page1_vertical_borders == [ + [black_3, red_1, black_3], + [black_3, red_1, black_3], + ] + assert page1_horizontal_borders == [ + [black_3, black_3], + [red_1, red_1], + [black_3, black_3], + ] From 93e77a498034240df1f8dc09e9e3cd77627a37e3 Mon Sep 17 00:00:00 2001 From: kygoh Date: Sun, 21 May 2023 08:13:16 +0000 Subject: [PATCH 02/21] Backup border properties before setting to transparent borders --- weasyprint/formatting_structure/build.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/weasyprint/formatting_structure/build.py b/weasyprint/formatting_structure/build.py index efecd35ef..c43e77465 100644 --- a/weasyprint/formatting_structure/build.py +++ b/weasyprint/formatting_structure/build.py @@ -1025,6 +1025,11 @@ def collapse_table_borders(table, grid_width, grid_height): horizontal_borders = [[weak_null_border for x in range(grid_width)] for y in range(grid_height + 1)] + def backup_border_properties(box_style, side): + box_style[f'__border_{side}_style'] = box_style[f'border_{side}_style'] + box_style[f'__border_{side}_width'] = box_style[f'border_{side}_width'] + box_style[f'__border_{side}_color'] = box_style[f'border_{side}_color'] + def set_one_border(border_grid, box_style, side, grid_x, grid_y): from ..draw import get_color @@ -1098,6 +1103,7 @@ def set_borders(box, x, y, w, h): # the correct widths on each box. The actual border grid will be # painted separately. def set_transparent_border(box, side, twice_width): + backup_border_properties(box.style, side) box.style[f'border_{side}_style'] = 'solid' box.style[f'border_{side}_width'] = twice_width / 2 box.style[f'border_{side}_color'] = TRANSPARENT From a9d01910377d32f82275697950b82357edc4f6d9 Mon Sep 17 00:00:00 2001 From: kygoh Date: Sun, 21 May 2023 08:17:46 +0000 Subject: [PATCH 03/21] Restore original border properties before resolving border conflicts in collapsing border model --- weasyprint/formatting_structure/build.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/weasyprint/formatting_structure/build.py b/weasyprint/formatting_structure/build.py index c43e77465..7325bac49 100644 --- a/weasyprint/formatting_structure/build.py +++ b/weasyprint/formatting_structure/build.py @@ -1030,9 +1030,18 @@ def backup_border_properties(box_style, side): box_style[f'__border_{side}_width'] = box_style[f'border_{side}_width'] box_style[f'__border_{side}_color'] = box_style[f'border_{side}_color'] + def restore_border_properties(box_style, side): + if f'__border_{side}_style' in box_style: + box_style[f'border_{side}_style'] = box_style[f'__border_{side}_style'] + if f'__border_{side}_width' in box_style: + box_style[f'border_{side}_width'] = box_style[f'__border_{side}_width'] + if f'__border_{side}_color' in box_style: + box_style[f'border_{side}_color'] = box_style[f'__border_{side}_color'] + def set_one_border(border_grid, box_style, side, grid_x, grid_y): from ..draw import get_color + restore_border_properties(box_style, side) style = box_style[f'border_{side}_style'] width = box_style[f'border_{side}_width'] color = get_color(box_style, f'border_{side}_color') From 345d1f18135709da87519e390cbeca6e484f20cf Mon Sep 17 00:00:00 2001 From: kygoh Date: Sun, 21 May 2023 12:00:01 +0000 Subject: [PATCH 04/21] Coding style fix for lines exceeding 79 characters --- weasyprint/formatting_structure/build.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/weasyprint/formatting_structure/build.py b/weasyprint/formatting_structure/build.py index 7325bac49..77bec6220 100644 --- a/weasyprint/formatting_structure/build.py +++ b/weasyprint/formatting_structure/build.py @@ -1032,11 +1032,14 @@ def backup_border_properties(box_style, side): def restore_border_properties(box_style, side): if f'__border_{side}_style' in box_style: - box_style[f'border_{side}_style'] = box_style[f'__border_{side}_style'] + box_style[f'border_{side}_style'] = \ + box_style[f'__border_{side}_style'] if f'__border_{side}_width' in box_style: - box_style[f'border_{side}_width'] = box_style[f'__border_{side}_width'] + box_style[f'border_{side}_width'] = \ + box_style[f'__border_{side}_width'] if f'__border_{side}_color' in box_style: - box_style[f'border_{side}_color'] = box_style[f'__border_{side}_color'] + box_style[f'border_{side}_color'] = \ + box_style[f'__border_{side}_color'] def set_one_border(border_grid, box_style, side, grid_x, grid_y): from ..draw import get_color From 4427d9fbd62f479d08c1b1ce874f769e2cc741a3 Mon Sep 17 00:00:00 2001 From: kygoh Date: Tue, 23 May 2023 09:42:48 +0000 Subject: [PATCH 05/21] Retain border width only when resolving collapsed borders --- weasyprint/formatting_structure/build.py | 18 ++++-------------- 1 file changed, 4 insertions(+), 14 deletions(-) diff --git a/weasyprint/formatting_structure/build.py b/weasyprint/formatting_structure/build.py index 77bec6220..ad79b72e5 100644 --- a/weasyprint/formatting_structure/build.py +++ b/weasyprint/formatting_structure/build.py @@ -1025,26 +1025,18 @@ def collapse_table_borders(table, grid_width, grid_height): horizontal_borders = [[weak_null_border for x in range(grid_width)] for y in range(grid_height + 1)] - def backup_border_properties(box_style, side): - box_style[f'__border_{side}_style'] = box_style[f'border_{side}_style'] + def backup_border_width(box_style, side): box_style[f'__border_{side}_width'] = box_style[f'border_{side}_width'] - box_style[f'__border_{side}_color'] = box_style[f'border_{side}_color'] - def restore_border_properties(box_style, side): - if f'__border_{side}_style' in box_style: - box_style[f'border_{side}_style'] = \ - box_style[f'__border_{side}_style'] + def restore_border_width(box_style, side): if f'__border_{side}_width' in box_style: box_style[f'border_{side}_width'] = \ box_style[f'__border_{side}_width'] - if f'__border_{side}_color' in box_style: - box_style[f'border_{side}_color'] = \ - box_style[f'__border_{side}_color'] def set_one_border(border_grid, box_style, side, grid_x, grid_y): from ..draw import get_color - restore_border_properties(box_style, side) + restore_border_width(box_style, side) style = box_style[f'border_{side}_style'] width = box_style[f'border_{side}_width'] color = get_color(box_style, f'border_{side}_color') @@ -1115,10 +1107,8 @@ def set_borders(box, x, y, w, h): # the correct widths on each box. The actual border grid will be # painted separately. def set_transparent_border(box, side, twice_width): - backup_border_properties(box.style, side) - box.style[f'border_{side}_style'] = 'solid' + backup_border_width(box.style, side) box.style[f'border_{side}_width'] = twice_width / 2 - box.style[f'border_{side}_color'] = TRANSPARENT def remove_borders(box): set_transparent_border(box, 'top', 0) From 9e59b20f3ad23a55d274b000c211ed59a11fd069 Mon Sep 17 00:00:00 2001 From: kygoh Date: Thu, 25 May 2023 15:01:08 +0000 Subject: [PATCH 06/21] Store harmonized border-{...}-width in computed value collapse_border_{...}_width --- weasyprint/formatting_structure/build.py | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/weasyprint/formatting_structure/build.py b/weasyprint/formatting_structure/build.py index ad79b72e5..c113547f2 100644 --- a/weasyprint/formatting_structure/build.py +++ b/weasyprint/formatting_structure/build.py @@ -1025,18 +1025,9 @@ def collapse_table_borders(table, grid_width, grid_height): horizontal_borders = [[weak_null_border for x in range(grid_width)] for y in range(grid_height + 1)] - def backup_border_width(box_style, side): - box_style[f'__border_{side}_width'] = box_style[f'border_{side}_width'] - - def restore_border_width(box_style, side): - if f'__border_{side}_width' in box_style: - box_style[f'border_{side}_width'] = \ - box_style[f'__border_{side}_width'] - def set_one_border(border_grid, box_style, side, grid_x, grid_y): from ..draw import get_color - restore_border_width(box_style, side) style = box_style[f'border_{side}_style'] width = box_style[f'border_{side}_width'] color = get_color(box_style, f'border_{side}_color') @@ -1107,8 +1098,7 @@ def set_borders(box, x, y, w, h): # the correct widths on each box. The actual border grid will be # painted separately. def set_transparent_border(box, side, twice_width): - backup_border_width(box.style, side) - box.style[f'border_{side}_width'] = twice_width / 2 + box.style[f'collapse_border_{side}_width'] = twice_width / 2 def remove_borders(box): set_transparent_border(box, 'top', 0) From 800a6839d14f4231d5a066b0c17ef83eddebb6a9 Mon Sep 17 00:00:00 2001 From: kygoh Date: Thu, 25 May 2023 15:08:54 +0000 Subject: [PATCH 07/21] Set collapse_border_{...}_width as border_{...}_width used values in resolve_percentages --- weasyprint/layout/percent.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/weasyprint/layout/percent.py b/weasyprint/layout/percent.py index 8577aa1c3..f60e50a14 100644 --- a/weasyprint/layout/percent.py +++ b/weasyprint/layout/percent.py @@ -92,10 +92,14 @@ def resolve_percentages(box, containing_block, main_flex_direction=None): resolve_one_percentage( box, 'max_height', cb_height, main_flex_direction) + collapse = box.style['border_collapse'] == 'collapse' # Used value == computed value for side in ('top', 'right', 'bottom', 'left'): prop = f'border_{side}_width' - setattr(box, prop, box.style[prop]) + if collapse and f'collapse_{prop}' in box.style: + setattr(box, prop, box.style[f'collapse_{prop}']) + else: + setattr(box, prop, box.style[prop]) # Shrink *content* widths and heights according to box-sizing # Thanks heavens and the spec: Our validator rejects negative values From d87034fb0e137f575eff0f4c45e1cf99e729e15b Mon Sep 17 00:00:00 2001 From: kygoh Date: Thu, 25 May 2023 15:14:30 +0000 Subject: [PATCH 08/21] Calculate margin width using collapse_border_{left,right}_width for collapsed-borders mode --- weasyprint/layout/preferred.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/weasyprint/layout/preferred.py b/weasyprint/layout/preferred.py index 917e42705..34ad2425e 100644 --- a/weasyprint/layout/preferred.py +++ b/weasyprint/layout/preferred.py @@ -148,9 +148,15 @@ def margin_width(box, width, left=True, right=True): percentages += style_value.value if left: - width += box.style['border_left_width'] + if box.style['border_collapse'] and 'collapse_border_left_width' in box.style: + width += box.style['collapse_border_left_width'] + else: + width += box.style['border_left_width'] if right: - width += box.style['border_right_width'] + if box.style['border_collapse'] and 'collapse_border_right_width' in box.style: + width += box.style['collapse_border_right_width'] + else: + width += box.style['border_right_width'] if percentages < 100: return width / (1 - percentages / 100) From d4a79bcf5a5d07bdcae0f2fcbe623fab86aebd6c Mon Sep 17 00:00:00 2001 From: kygoh Date: Thu, 25 May 2023 16:08:11 +0000 Subject: [PATCH 09/21] Fix check for collapsed-borders mode in preferred margin width --- weasyprint/layout/preferred.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/weasyprint/layout/preferred.py b/weasyprint/layout/preferred.py index 34ad2425e..34ca1f7a6 100644 --- a/weasyprint/layout/preferred.py +++ b/weasyprint/layout/preferred.py @@ -147,13 +147,14 @@ def margin_width(box, width, left=True, right=True): assert style_value.unit == '%' percentages += style_value.value + collapse = box.style['border_collapse'] == 'collapse' if left: - if box.style['border_collapse'] and 'collapse_border_left_width' in box.style: + if collapse and 'collapse_border_left_width' in box.style: width += box.style['collapse_border_left_width'] else: width += box.style['border_left_width'] if right: - if box.style['border_collapse'] and 'collapse_border_right_width' in box.style: + if collapse and 'collapse_border_right_width' in box.style: width += box.style['collapse_border_right_width'] else: width += box.style['border_right_width'] From fd1dbc7d638537ca323ed3ee4c6f8be79328ec4d Mon Sep 17 00:00:00 2001 From: kygoh Date: Fri, 26 May 2023 03:53:07 +0000 Subject: [PATCH 10/21] Move collapsed-borders conflict resolution logic to layout.table module --- tests/test_boxes.py | 3 +- weasyprint/formatting_structure/build.py | 159 +---------------------- weasyprint/layout/table.py | 159 +++++++++++++++++++++++ 3 files changed, 163 insertions(+), 158 deletions(-) diff --git a/tests/test_boxes.py b/tests/test_boxes.py index 27cd8fec7..6ab4bcf3e 100644 --- a/tests/test_boxes.py +++ b/tests/test_boxes.py @@ -4,6 +4,7 @@ from weasyprint.css import PageType, get_all_computed_styles from weasyprint.formatting_structure import boxes, build from weasyprint.layout.page import set_page_type_computed_styles +from weasyprint.layout.table import collapse_table_borders from .testing_utils import ( FakeHTML, assert_no_logs, assert_tree, capture_logs, parse, parse_all, @@ -19,7 +20,7 @@ def _get_grid(html): [[(style, width, color) if width else None for _score, (style, width, color) in column] for column in grid] - for grid in table.collapsed_border_grid) + for grid in collapse_table_borders(table)) @assert_no_logs diff --git a/weasyprint/formatting_structure/build.py b/weasyprint/formatting_structure/build.py index c113547f2..e3a825e3d 100644 --- a/weasyprint/formatting_structure/build.py +++ b/weasyprint/formatting_structure/build.py @@ -7,8 +7,6 @@ import re import unicodedata -import tinycss2.color3 - from .. import html from ..css import computed_values, properties, targets from ..logger import LOGGER @@ -977,8 +975,8 @@ def wrap_table(box, children): table = box.copy_with_children(row_groups) table.column_groups = tuple(column_groups) if table.style['border_collapse'] == 'collapse': - table.collapsed_border_grid = collapse_table_borders( - table, grid_width, grid_height) + table.grid_width = grid_width + table.grid_height = grid_height if isinstance(box, boxes.InlineTableBox): wrapper_type = boxes.InlineBlockBox @@ -999,159 +997,6 @@ def wrap_table(box, children): return wrapper -TRANSPARENT = tinycss2.color3.parse_color('transparent') - - -def collapse_table_borders(table, grid_width, grid_height): - """Resolve border conflicts for a table in the collapsing border model. - - Take a :class:`TableBox`; set appropriate border widths on the table, - column group, column, row group, row, and cell boxes; and return - a data structure for the resolved collapsed border grid. - - """ - if not (grid_width and grid_height): - # Don’t bother with empty tables - return [], [] - - style_scores = dict((v, i) for i, v in enumerate(reversed([ - 'hidden', 'double', 'solid', 'dashed', 'dotted', 'ridge', - 'outset', 'groove', 'inset', 'none']))) - style_map = {'inset': 'ridge', 'outset': 'groove'} - weak_null_border = ( - (0, 0, style_scores['none']), ('none', 0, TRANSPARENT)) - vertical_borders = [[weak_null_border for x in range(grid_width + 1)] - for y in range(grid_height)] - horizontal_borders = [[weak_null_border for x in range(grid_width)] - for y in range(grid_height + 1)] - - def set_one_border(border_grid, box_style, side, grid_x, grid_y): - from ..draw import get_color - - style = box_style[f'border_{side}_style'] - width = box_style[f'border_{side}_width'] - color = get_color(box_style, f'border_{side}_color') - - # https://www.w3.org/TR/CSS21/tables.html#border-conflict-resolution - score = ((1 if style == 'hidden' else 0), width, style_scores[style]) - - style = style_map.get(style, style) - previous_score, _ = border_grid[grid_y][grid_x] - # Strict < so that the earlier call wins in case of a tie. - if previous_score < score: - border_grid[grid_y][grid_x] = (score, (style, width, color)) - - def set_borders(box, x, y, w, h): - style = box.style - for yy in range(y, y + h): - set_one_border(vertical_borders, style, 'left', x, yy) - set_one_border(vertical_borders, style, 'right', x + w, yy) - for xx in range(x, x + w): - set_one_border(horizontal_borders, style, 'top', xx, y) - set_one_border(horizontal_borders, style, 'bottom', xx, y + h) - - # The order is important here: - # "A style set on a cell wins over one on a row, which wins over a - # row group, column, column group and, lastly, table" - # See https://www.w3.org/TR/CSS21/tables.html#border-conflict-resolution - strong_null_border = ( - (1, 0, style_scores['hidden']), ('hidden', 0, TRANSPARENT)) - grid_y = 0 - for row_group in table.children: - for row in row_group.children: - for cell in row.children: - # No border inside of a cell with rowspan or colspan - for xx in range(cell.grid_x + 1, cell.grid_x + cell.colspan): - for yy in range(grid_y, grid_y + cell.rowspan): - vertical_borders[yy][xx] = strong_null_border - for xx in range(cell.grid_x, cell.grid_x + cell.colspan): - for yy in range(grid_y + 1, grid_y + cell.rowspan): - horizontal_borders[yy][xx] = strong_null_border - # The cell’s own borders - set_borders(cell, x=cell.grid_x, y=grid_y, - w=cell.colspan, h=cell.rowspan) - grid_y += 1 - - grid_y = 0 - for row_group in table.children: - for row in row_group.children: - set_borders(row, x=0, y=grid_y, w=grid_width, h=1) - grid_y += 1 - - grid_y = 0 - for row_group in table.children: - rowspan = len(row_group.children) - set_borders(row_group, x=0, y=grid_y, w=grid_width, h=rowspan) - grid_y += rowspan - - for column_group in table.column_groups: - for column in column_group.children: - set_borders(column, x=column.grid_x, y=0, w=1, h=grid_height) - - for column_group in table.column_groups: - set_borders(column_group, x=column_group.grid_x, y=0, - w=column_group.span, h=grid_height) - - set_borders(table, x=0, y=0, w=grid_width, h=grid_height) - - # Now that all conflicts are resolved, set transparent borders of - # the correct widths on each box. The actual border grid will be - # painted separately. - def set_transparent_border(box, side, twice_width): - box.style[f'collapse_border_{side}_width'] = twice_width / 2 - - def remove_borders(box): - set_transparent_border(box, 'top', 0) - set_transparent_border(box, 'right', 0) - set_transparent_border(box, 'bottom', 0) - set_transparent_border(box, 'left', 0) - - def max_vertical_width(x, y, h): - return max( - width for grid_row in vertical_borders[y:y + h] - for _, (_, width, _) in [grid_row[x]]) - - def max_horizontal_width(x, y, w): - return max( - width for _, (_, width, _) in horizontal_borders[y][x:x + w]) - - grid_y = 0 - for row_group in table.children: - remove_borders(row_group) - for row in row_group.children: - remove_borders(row) - for cell in row.children: - set_transparent_border(cell, 'top', max_horizontal_width( - x=cell.grid_x, y=grid_y, w=cell.colspan)) - set_transparent_border(cell, 'bottom', max_horizontal_width( - x=cell.grid_x, y=grid_y + cell.rowspan, w=cell.colspan)) - set_transparent_border(cell, 'left', max_vertical_width( - x=cell.grid_x, y=grid_y, h=cell.rowspan)) - set_transparent_border(cell, 'right', max_vertical_width( - x=cell.grid_x + cell.colspan, y=grid_y, h=cell.rowspan)) - grid_y += 1 - - for column_group in table.column_groups: - remove_borders(column_group) - for column in column_group.children: - remove_borders(column) - - set_transparent_border(table, 'top', max_horizontal_width( - x=0, y=0, w=grid_width)) - set_transparent_border(table, 'bottom', max_horizontal_width( - x=0, y=grid_height, w=grid_width)) - # "UAs must compute an initial left and right border width for the table - # by examining the first and last cells in the first row of the table." - # https://www.w3.org/TR/CSS21/tables.html#collapsing-borders - # ... so h=1, not grid_height: - set_transparent_border(table, 'left', max_vertical_width( - x=0, y=0, h=1)) - set_transparent_border(table, 'right', max_vertical_width( - x=grid_width, y=0, h=1)) - - return vertical_borders, horizontal_borders - - def flex_boxes(box): """Remove and add boxes according to the flex model. diff --git a/weasyprint/layout/table.py b/weasyprint/layout/table.py index 3a740c22c..7e25a7308 100644 --- a/weasyprint/layout/table.py +++ b/weasyprint/layout/table.py @@ -1,5 +1,7 @@ """Layout for tables and internal table boxes.""" +import tinycss2.color3 + from math import inf from ..formatting_structure import boxes @@ -804,6 +806,9 @@ def auto_table_layout(context, box, containing_block): def table_wrapper_width(context, wrapper, containing_block): """Find the width of each column and derive the wrapper width.""" table = wrapper.get_wrapped_table() + collapse = table.style['border_collapse'] == 'collapse' + if collapse: + table.collapsed_border_grid = collapse_table_borders(table) resolve_percentages(table, containing_block) if table.style['table_layout'] == 'fixed' and table.width != 'auto': @@ -973,3 +978,157 @@ def distribute_excess_width(context, grid, excess_width, column_widths, # Fifth group, part 2, aka abort return excess_width + + +TRANSPARENT = tinycss2.color3.parse_color('transparent') + + +def collapse_table_borders(table): + """Resolve border conflicts for a table in the collapsing border model. + + Take a :class:`TableBox`; set appropriate border widths on the table, + column group, column, row group, row, and cell boxes; and return + a data structure for the resolved collapsed border grid. + + """ + grid_width, grid_height = table.grid_width, table.grid_height + if not (grid_width and grid_height): + # Don’t bother with empty tables + return [], [] + + style_scores = dict((v, i) for i, v in enumerate(reversed([ + 'hidden', 'double', 'solid', 'dashed', 'dotted', 'ridge', + 'outset', 'groove', 'inset', 'none']))) + style_map = {'inset': 'ridge', 'outset': 'groove'} + weak_null_border = ( + (0, 0, style_scores['none']), ('none', 0, TRANSPARENT)) + vertical_borders = [[weak_null_border for x in range(grid_width + 1)] + for y in range(grid_height)] + horizontal_borders = [[weak_null_border for x in range(grid_width)] + for y in range(grid_height + 1)] + + def set_one_border(border_grid, box_style, side, grid_x, grid_y): + from ..draw import get_color + + style = box_style[f'border_{side}_style'] + width = box_style[f'border_{side}_width'] + color = get_color(box_style, f'border_{side}_color') + + # https://www.w3.org/TR/CSS21/tables.html#border-conflict-resolution + score = ((1 if style == 'hidden' else 0), width, style_scores[style]) + + style = style_map.get(style, style) + previous_score, _ = border_grid[grid_y][grid_x] + # Strict < so that the earlier call wins in case of a tie. + if previous_score < score: + border_grid[grid_y][grid_x] = (score, (style, width, color)) + + def set_borders(box, x, y, w, h): + style = box.style + for yy in range(y, y + h): + set_one_border(vertical_borders, style, 'left', x, yy) + set_one_border(vertical_borders, style, 'right', x + w, yy) + for xx in range(x, x + w): + set_one_border(horizontal_borders, style, 'top', xx, y) + set_one_border(horizontal_borders, style, 'bottom', xx, y + h) + + # The order is important here: + # "A style set on a cell wins over one on a row, which wins over a + # row group, column, column group and, lastly, table" + # See https://www.w3.org/TR/CSS21/tables.html#border-conflict-resolution + strong_null_border = ( + (1, 0, style_scores['hidden']), ('hidden', 0, TRANSPARENT)) + grid_y = 0 + for row_group in table.children: + for row in row_group.children: + for cell in row.children: + # No border inside of a cell with rowspan or colspan + for xx in range(cell.grid_x + 1, cell.grid_x + cell.colspan): + for yy in range(grid_y, grid_y + cell.rowspan): + vertical_borders[yy][xx] = strong_null_border + for xx in range(cell.grid_x, cell.grid_x + cell.colspan): + for yy in range(grid_y + 1, grid_y + cell.rowspan): + horizontal_borders[yy][xx] = strong_null_border + # The cell’s own borders + set_borders(cell, x=cell.grid_x, y=grid_y, + w=cell.colspan, h=cell.rowspan) + grid_y += 1 + + grid_y = 0 + for row_group in table.children: + for row in row_group.children: + set_borders(row, x=0, y=grid_y, w=grid_width, h=1) + grid_y += 1 + + grid_y = 0 + for row_group in table.children: + rowspan = len(row_group.children) + set_borders(row_group, x=0, y=grid_y, w=grid_width, h=rowspan) + grid_y += rowspan + + for column_group in table.column_groups: + for column in column_group.children: + set_borders(column, x=column.grid_x, y=0, w=1, h=grid_height) + + for column_group in table.column_groups: + set_borders(column_group, x=column_group.grid_x, y=0, + w=column_group.span, h=grid_height) + + set_borders(table, x=0, y=0, w=grid_width, h=grid_height) + + # Now that all conflicts are resolved, set transparent borders of + # the correct widths on each box. The actual border grid will be + # painted separately. + def set_transparent_border(box, side, twice_width): + box.style[f'collapse_border_{side}_width'] = twice_width / 2 + + def remove_borders(box): + set_transparent_border(box, 'top', 0) + set_transparent_border(box, 'right', 0) + set_transparent_border(box, 'bottom', 0) + set_transparent_border(box, 'left', 0) + + def max_vertical_width(x, y, h): + return max( + width for grid_row in vertical_borders[y:y + h] + for _, (_, width, _) in [grid_row[x]]) + + def max_horizontal_width(x, y, w): + return max( + width for _, (_, width, _) in horizontal_borders[y][x:x + w]) + + grid_y = 0 + for row_group in table.children: + remove_borders(row_group) + for row in row_group.children: + remove_borders(row) + for cell in row.children: + set_transparent_border(cell, 'top', max_horizontal_width( + x=cell.grid_x, y=grid_y, w=cell.colspan)) + set_transparent_border(cell, 'bottom', max_horizontal_width( + x=cell.grid_x, y=grid_y + cell.rowspan, w=cell.colspan)) + set_transparent_border(cell, 'left', max_vertical_width( + x=cell.grid_x, y=grid_y, h=cell.rowspan)) + set_transparent_border(cell, 'right', max_vertical_width( + x=cell.grid_x + cell.colspan, y=grid_y, h=cell.rowspan)) + grid_y += 1 + + for column_group in table.column_groups: + remove_borders(column_group) + for column in column_group.children: + remove_borders(column) + + set_transparent_border(table, 'top', max_horizontal_width( + x=0, y=0, w=grid_width)) + set_transparent_border(table, 'bottom', max_horizontal_width( + x=0, y=grid_height, w=grid_width)) + # "UAs must compute an initial left and right border width for the table + # by examining the first and last cells in the first row of the table." + # https://www.w3.org/TR/CSS21/tables.html#collapsing-borders + # ... so h=1, not grid_height: + set_transparent_border(table, 'left', max_vertical_width( + x=0, y=0, h=1)) + set_transparent_border(table, 'right', max_vertical_width( + x=grid_width, y=0, h=1)) + + return vertical_borders, horizontal_borders From 0c9a6a1ec33c2616aab4cebfbceec7630cab19b7 Mon Sep 17 00:00:00 2001 From: kygoh Date: Fri, 26 May 2023 06:35:40 +0000 Subject: [PATCH 11/21] Resolve border-{...}-width used values during collapsed-borders conflict resolution --- weasyprint/layout/percent.py | 6 +++--- weasyprint/layout/preferred.py | 8 ++++---- weasyprint/layout/table.py | 3 ++- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/weasyprint/layout/percent.py b/weasyprint/layout/percent.py index f60e50a14..c2f405f0d 100644 --- a/weasyprint/layout/percent.py +++ b/weasyprint/layout/percent.py @@ -96,9 +96,9 @@ def resolve_percentages(box, containing_block, main_flex_direction=None): # Used value == computed value for side in ('top', 'right', 'bottom', 'left'): prop = f'border_{side}_width' - if collapse and f'collapse_{prop}' in box.style: - setattr(box, prop, box.style[f'collapse_{prop}']) - else: + # border-{side}-width would have been resolved + # during border conflict resolution for collapsed-borders + if not (collapse and hasattr(box, prop)): setattr(box, prop, box.style[prop]) # Shrink *content* widths and heights according to box-sizing diff --git a/weasyprint/layout/preferred.py b/weasyprint/layout/preferred.py index 34ca1f7a6..58d1a76f3 100644 --- a/weasyprint/layout/preferred.py +++ b/weasyprint/layout/preferred.py @@ -149,13 +149,13 @@ def margin_width(box, width, left=True, right=True): collapse = box.style['border_collapse'] == 'collapse' if left: - if collapse and 'collapse_border_left_width' in box.style: - width += box.style['collapse_border_left_width'] + if collapse and hasattr(box, 'border_left_width'): + width += box.border_left_width else: width += box.style['border_left_width'] if right: - if collapse and 'collapse_border_right_width' in box.style: - width += box.style['collapse_border_right_width'] + if collapse and hasattr(box, 'border_right_width'): + width += box.border_right_width else: width += box.style['border_right_width'] diff --git a/weasyprint/layout/table.py b/weasyprint/layout/table.py index 7e25a7308..abadbf905 100644 --- a/weasyprint/layout/table.py +++ b/weasyprint/layout/table.py @@ -1080,7 +1080,8 @@ def set_borders(box, x, y, w, h): # the correct widths on each box. The actual border grid will be # painted separately. def set_transparent_border(box, side, twice_width): - box.style[f'collapse_border_{side}_width'] = twice_width / 2 + prop = f'border_{side}_width' + setattr(box, prop, twice_width / 2) def remove_borders(box): set_transparent_border(box, 'top', 0) From b3bdff8ae1b5b658753c19370c5c5cc993af8c66 Mon Sep 17 00:00:00 2001 From: kygoh Date: Fri, 26 May 2023 17:37:14 +0000 Subject: [PATCH 12/21] Rename function to set used values for collapsed-borders width --- weasyprint/layout/table.py | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/weasyprint/layout/table.py b/weasyprint/layout/table.py index abadbf905..a33c3dc41 100644 --- a/weasyprint/layout/table.py +++ b/weasyprint/layout/table.py @@ -1079,15 +1079,15 @@ def set_borders(box, x, y, w, h): # Now that all conflicts are resolved, set transparent borders of # the correct widths on each box. The actual border grid will be # painted separately. - def set_transparent_border(box, side, twice_width): + def set_border_used_width(box, side, twice_width): prop = f'border_{side}_width' setattr(box, prop, twice_width / 2) def remove_borders(box): - set_transparent_border(box, 'top', 0) - set_transparent_border(box, 'right', 0) - set_transparent_border(box, 'bottom', 0) - set_transparent_border(box, 'left', 0) + set_border_used_width(box, 'top', 0) + set_border_used_width(box, 'right', 0) + set_border_used_width(box, 'bottom', 0) + set_border_used_width(box, 'left', 0) def max_vertical_width(x, y, h): return max( @@ -1104,13 +1104,13 @@ def max_horizontal_width(x, y, w): for row in row_group.children: remove_borders(row) for cell in row.children: - set_transparent_border(cell, 'top', max_horizontal_width( + set_border_used_width(cell, 'top', max_horizontal_width( x=cell.grid_x, y=grid_y, w=cell.colspan)) - set_transparent_border(cell, 'bottom', max_horizontal_width( + set_border_used_width(cell, 'bottom', max_horizontal_width( x=cell.grid_x, y=grid_y + cell.rowspan, w=cell.colspan)) - set_transparent_border(cell, 'left', max_vertical_width( + set_border_used_width(cell, 'left', max_vertical_width( x=cell.grid_x, y=grid_y, h=cell.rowspan)) - set_transparent_border(cell, 'right', max_vertical_width( + set_border_used_width(cell, 'right', max_vertical_width( x=cell.grid_x + cell.colspan, y=grid_y, h=cell.rowspan)) grid_y += 1 @@ -1119,17 +1119,17 @@ def max_horizontal_width(x, y, w): for column in column_group.children: remove_borders(column) - set_transparent_border(table, 'top', max_horizontal_width( + set_border_used_width(table, 'top', max_horizontal_width( x=0, y=0, w=grid_width)) - set_transparent_border(table, 'bottom', max_horizontal_width( + set_border_used_width(table, 'bottom', max_horizontal_width( x=0, y=grid_height, w=grid_width)) # "UAs must compute an initial left and right border width for the table # by examining the first and last cells in the first row of the table." # https://www.w3.org/TR/CSS21/tables.html#collapsing-borders # ... so h=1, not grid_height: - set_transparent_border(table, 'left', max_vertical_width( + set_border_used_width(table, 'left', max_vertical_width( x=0, y=0, h=1)) - set_transparent_border(table, 'right', max_vertical_width( + set_border_used_width(table, 'right', max_vertical_width( x=grid_width, y=0, h=1)) return vertical_borders, horizontal_borders From bb382b55d7c5986a6018fb7b54c7d6296c0415e4 Mon Sep 17 00:00:00 2001 From: kygoh Date: Wed, 31 May 2023 14:36:11 +0000 Subject: [PATCH 13/21] Add references to computing table measures spec --- weasyprint/layout/preferred.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/weasyprint/layout/preferred.py b/weasyprint/layout/preferred.py index dc28e7aa6..38deffa83 100644 --- a/weasyprint/layout/preferred.py +++ b/weasyprint/layout/preferred.py @@ -135,6 +135,10 @@ def margin_width(box, width, left=True, right=True): """Add box paddings, borders and margins to ``width``.""" percentages = 0 + # See https://drafts.csswg.org/css-tables-3/#cell-intrinsic-offsets + # It is a set of computed values for border-left-width, padding-left, + # padding-right, and border-right-width (along with zero values for + # margin-left and margin-right) for value in ( (['margin_left', 'padding_left'] if left else []) + (['margin_right', 'padding_right'] if right else []) @@ -150,13 +154,20 @@ def margin_width(box, width, left=True, right=True): collapse = box.style['border_collapse'] == 'collapse' if left: if collapse and hasattr(box, 'border_left_width'): + # In collapsed-borders mode: the computed horizontal padding of the + # cell and, for border values, the used border-width values of the + # cell (half the winning border-width) width += box.border_left_width else: + # In separated-borders mode: the computed horizontal padding and + # border of the table-cell width += box.style['border_left_width'] if right: if collapse and hasattr(box, 'border_right_width'): + # [...] the used border-width values of the cell width += box.border_right_width else: + # [...] the computed border of the table-cell width += box.style['border_right_width'] if percentages < 100: From 5976f6d5305c69b610261d19746bf2d43005078f Mon Sep 17 00:00:00 2001 From: kygoh Date: Wed, 31 May 2023 14:53:25 +0000 Subject: [PATCH 14/21] Move collapse border test cases to layout --- tests/layout/test_table.py | 132 ++++++++++++++++++++++++++++++++++++- tests/test_boxes.py | 130 +----------------------------------- 2 files changed, 132 insertions(+), 130 deletions(-) diff --git a/tests/layout/test_table.py b/tests/layout/test_table.py index 12c6cb92c..23e235c68 100644 --- a/tests/layout/test_table.py +++ b/tests/layout/test_table.py @@ -1,8 +1,22 @@ """Tests for layout of tables.""" import pytest +from weasyprint.formatting_structure import boxes +from weasyprint.layout.table import collapse_table_borders -from ..testing_utils import assert_no_logs, capture_logs, render_pages +from ..testing_utils import assert_no_logs, capture_logs, parse_all, render_pages + + +def _get_grid(html): + html = parse_all(html) + body, = html.children + table_wrapper, = body.children + table, = table_wrapper.children + return tuple( + [[(style, width, color) if width else None + for _score, (style, width, color) in column] + for column in grid] + for grid in collapse_table_borders(table)) @assert_no_logs @@ -2912,3 +2926,119 @@ def test_table_different_display(): ''') + + +black = (0, 0, 0, 1) +red = (1, 0, 0, 1) +green = (0, 1, 0, 1) # lime in CSS +blue = (0, 0, 1, 1) +yellow = (1, 1, 0, 1) +black_3 = ('solid', 3, black) +red_1 = ('solid', 1, red) +yellow_5 = ('solid', 5, yellow) +green_5 = ('solid', 5, green) +dashed_blue_5 = ('dashed', 5, blue) + + +@assert_no_logs +def test_border_collapse_1(): + html = parse_all('
') + body, = html.children + table_wrapper, = body.children + table, = table_wrapper.children + assert isinstance(table, boxes.TableBox) + assert not hasattr(table, 'collapsed_border_grid') + + grid = _get_grid('
') + assert grid == ([], []) + + +@assert_no_logs +def test_border_collapse_2(): + vertical_borders, horizontal_borders = _get_grid(''' + + + + +
A B
C D
+ ''') + assert vertical_borders == [ + [black_3, red_1, black_3], + [black_3, red_1, black_3], + ] + assert horizontal_borders == [ + [black_3, black_3], + [red_1, red_1], + [black_3, black_3], + ] + + +@assert_no_logs +def test_border_collapse_3(): + # hidden vs. none + vertical_borders, horizontal_borders = _get_grid(''' + + + + +
A B
C D
+ ''') + assert vertical_borders == [ + [black_3, None, None], + [black_3, black_3, black_3], + ] + assert horizontal_borders == [ + [black_3, None], + [black_3, None], + [black_3, black_3], + ] + + +@assert_no_logs +def test_border_collapse_4(): + vertical_borders, horizontal_borders = _get_grid(''' + + + + + + + + +
+ ''') + assert vertical_borders == [ + [yellow_5, black_3, red_1, yellow_5], + [yellow_5, dashed_blue_5, green_5, green_5], + [yellow_5, black_3, red_1, yellow_5], + [yellow_5, black_3, red_1, yellow_5], + ] + assert horizontal_borders == [ + [yellow_5, yellow_5, yellow_5], + [red_1, dashed_blue_5, green_5], + [red_1, dashed_blue_5, green_5], + [red_1, red_1, red_1], + [yellow_5, yellow_5, yellow_5], + ] + + +@assert_no_logs +def test_border_collapse_5(): + # rowspan and colspan + vertical_borders, horizontal_borders = _get_grid(''' + + + + + +
+ ''') + assert vertical_borders == [ + [black_3, black_3, black_3, black_3], + [black_3, black_3, None, black_3], + ] + assert horizontal_borders == [ + [black_3, black_3, black_3], + [None, black_3, black_3], + [black_3, black_3, black_3], + ] diff --git a/tests/test_boxes.py b/tests/test_boxes.py index 6ab4bcf3e..dbf5f6a66 100644 --- a/tests/test_boxes.py +++ b/tests/test_boxes.py @@ -4,25 +4,13 @@ from weasyprint.css import PageType, get_all_computed_styles from weasyprint.formatting_structure import boxes, build from weasyprint.layout.page import set_page_type_computed_styles -from weasyprint.layout.table import collapse_table_borders + from .testing_utils import ( FakeHTML, assert_no_logs, assert_tree, capture_logs, parse, parse_all, render_pages) -def _get_grid(html): - html = parse_all(html) - body, = html.children - table_wrapper, = body.children - table, = table_wrapper.children - return tuple( - [[(style, width, color) if width else None - for _score, (style, width, color) in column] - for column in grid] - for grid in collapse_table_borders(table)) - - @assert_no_logs def test_box_tree(): assert_tree(parse('

'), [('p', 'Block', [])]) @@ -1116,122 +1104,6 @@ def test_page_counters(): assert text_box.text == 'Page {0} of 3.'.format(page_number) -black = (0, 0, 0, 1) -red = (1, 0, 0, 1) -green = (0, 1, 0, 1) # lime in CSS -blue = (0, 0, 1, 1) -yellow = (1, 1, 0, 1) -black_3 = ('solid', 3, black) -red_1 = ('solid', 1, red) -yellow_5 = ('solid', 5, yellow) -green_5 = ('solid', 5, green) -dashed_blue_5 = ('dashed', 5, blue) - - -@assert_no_logs -def test_border_collapse_1(): - html = parse_all('
') - body, = html.children - table_wrapper, = body.children - table, = table_wrapper.children - assert isinstance(table, boxes.TableBox) - assert not hasattr(table, 'collapsed_border_grid') - - grid = _get_grid('
') - assert grid == ([], []) - - -@assert_no_logs -def test_border_collapse_2(): - vertical_borders, horizontal_borders = _get_grid(''' - - - - -
A B
C D
- ''') - assert vertical_borders == [ - [black_3, red_1, black_3], - [black_3, red_1, black_3], - ] - assert horizontal_borders == [ - [black_3, black_3], - [red_1, red_1], - [black_3, black_3], - ] - - -@assert_no_logs -def test_border_collapse_3(): - # hidden vs. none - vertical_borders, horizontal_borders = _get_grid(''' - - - - -
A B
C D
- ''') - assert vertical_borders == [ - [black_3, None, None], - [black_3, black_3, black_3], - ] - assert horizontal_borders == [ - [black_3, None], - [black_3, None], - [black_3, black_3], - ] - - -@assert_no_logs -def test_border_collapse_4(): - vertical_borders, horizontal_borders = _get_grid(''' - - - - - - - - -
- ''') - assert vertical_borders == [ - [yellow_5, black_3, red_1, yellow_5], - [yellow_5, dashed_blue_5, green_5, green_5], - [yellow_5, black_3, red_1, yellow_5], - [yellow_5, black_3, red_1, yellow_5], - ] - assert horizontal_borders == [ - [yellow_5, yellow_5, yellow_5], - [red_1, dashed_blue_5, green_5], - [red_1, dashed_blue_5, green_5], - [red_1, red_1, red_1], - [yellow_5, yellow_5, yellow_5], - ] - - -@assert_no_logs -def test_border_collapse_5(): - # rowspan and colspan - vertical_borders, horizontal_borders = _get_grid(''' - - - - - -
- ''') - assert vertical_borders == [ - [black_3, black_3, black_3, black_3], - [black_3, black_3, None, black_3], - ] - assert horizontal_borders == [ - [black_3, black_3, black_3], - [None, black_3, black_3], - [black_3, black_3, black_3], - ] - - @assert_no_logs @pytest.mark.parametrize('html', ( '', From af141bac1e677f2e2863150b336278a6be52fb7b Mon Sep 17 00:00:00 2001 From: kygoh Date: Wed, 31 May 2023 15:08:20 +0000 Subject: [PATCH 15/21] Fix coding style --- tests/layout/test_table.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/layout/test_table.py b/tests/layout/test_table.py index 23e235c68..d14a247cc 100644 --- a/tests/layout/test_table.py +++ b/tests/layout/test_table.py @@ -4,7 +4,8 @@ from weasyprint.formatting_structure import boxes from weasyprint.layout.table import collapse_table_borders -from ..testing_utils import assert_no_logs, capture_logs, parse_all, render_pages +from ..testing_utils import ( + assert_no_logs, capture_logs, parse_all, render_pages) def _get_grid(html): From 6fc2eef935764334eb46e51b8d269da56acd8d9d Mon Sep 17 00:00:00 2001 From: Guillaume Ayoub Date: Sun, 10 Sep 2023 22:51:48 +0200 Subject: [PATCH 16/21] Test drawing for border collapse --- tests/draw/test_table.py | 219 +++++++++++++++++++++++++++++++++++++ tests/layout/test_page.py | 65 ----------- tests/layout/test_table.py | 134 +---------------------- 3 files changed, 220 insertions(+), 198 deletions(-) diff --git a/tests/draw/test_table.py b/tests/draw/test_table.py index 95fb01171..ad95d6da5 100644 --- a/tests/draw/test_table.py +++ b/tests/draw/test_table.py @@ -1298,3 +1298,222 @@ def test_tables_23(assert_pixels): ''') + + +@assert_no_logs +def test_running_elements_table_border_collapse(assert_pixels): + assert_pixels(2 * ''' + KK_____________ + KK_____________ + _______________ + _______________ + _______________ + KKKKKKK________ + KRRKRRK________ + KRRKRRK________ + KKKKKKK________ + KRRKRRK________ + KRRKRRK________ + KKKKKKK________ + _______________ + _______________ + _______________ + ''', ''' + +
abcdeabcde
abc abc
+ + +
A B
C D
+

1
+
2
+ ''') + + +@assert_no_logs +def test_running_elements_table_border_collapse_empty(assert_pixels): + assert_pixels(2 * ''' + KK________ + KK________ + __________ + __________ + __________ + __________ + __________ + __________ + __________ + __________ + ''', ''' + +
+
1
+
2
+ ''') + + +@pytest.mark.xfail +@assert_no_logs +def test_running_elements_table_border_collapse_border_style(assert_pixels): + assert_pixels(2 * ''' + KK_____________ + KK_____________ + _______________ + _______________ + _______________ + KKKZ___________ + KRR_RR_________ + KRR_RR_________ + KKKK__Z________ + KRRKRRK________ + KRRKRRK________ + KKKKKKK________ + _______________ + _______________ + _______________ + ''', ''' + + + + +
A B
C D
+
1
+
2
+ ''') + + +@assert_no_logs +def test_running_elements_table_border_collapse_span(assert_pixels): + assert_pixels(2 * ''' + KK_____________ + KK_____________ + _______________ + _______________ + _______________ + KKKKKKKKKK_____ + KRRKRRKRRK_____ + KRRKRRKRRK_____ + K__KKKKKKK_____ + K__KRR___K_____ + K__KRR___K_____ + KKKKKKKKKK_____ + _______________ + _______________ + _______________ + ''', ''' + + + + +
A B C
D
+
1
+
2
+ ''') + + +@assert_no_logs +def test_running_elements_table_border_collapse_margin(assert_pixels): + assert_pixels(2 * ''' + KK_____________ + KK_____________ + _______________ + _______________ + _______________ + _______________ + ____KKKKKKK____ + ____KRRKRRK____ + ____KRRKRRK____ + ____KKKKKKK____ + ____KRRKRRK____ + ____KRRKRRK____ + ____KKKKKKK____ + _______________ + _______________ + ''', ''' + + + + +
A B
C D
+
1
+
2
+ ''') diff --git a/tests/layout/test_page.py b/tests/layout/test_page.py index 8fe8c91c3..04c7aa047 100644 --- a/tests/layout/test_page.py +++ b/tests/layout/test_page.py @@ -1549,68 +1549,3 @@ def test_running_float(): Hello! ''') - - -def _get_grid(table_wrapper): - table, = table_wrapper.children - return tuple( - [[(style, width, color) if width else None - for _score, (style, width, color) in column] - for column in grid] - for grid in table.collapsed_border_grid) - - -black = (0, 0, 0, 1) -red = (1, 0, 0, 1) -black_3 = ('solid', 3, black) -red_1 = ('solid', 1, red) - - -@assert_no_logs -def test_running_elements_table_border_collapse(): - page0, page1 = render_pages(''' - - - - -
A B
C D
-
page0
-
page1
- ''') - - page0_html, page0_center = page0.children - page0_table = page0_center.children[0] - page0_vertical_borders, page0_horizontal_borders = _get_grid(page0_table) - assert page0_vertical_borders == [ - [black_3, red_1, black_3], - [black_3, red_1, black_3], - ] - assert page0_horizontal_borders == [ - [black_3, black_3], - [red_1, red_1], - [black_3, black_3], - ] - - page1_html, page1_center = page1.children - page1_table = page1_center.children[0] - page1_vertical_borders, page1_horizontal_borders = _get_grid(page1_table) - assert page1_vertical_borders == [ - [black_3, red_1, black_3], - [black_3, red_1, black_3], - ] - assert page1_horizontal_borders == [ - [black_3, black_3], - [red_1, red_1], - [black_3, black_3], - ] diff --git a/tests/layout/test_table.py b/tests/layout/test_table.py index a4fc6ff96..2832ef8c1 100644 --- a/tests/layout/test_table.py +++ b/tests/layout/test_table.py @@ -1,23 +1,8 @@ """Tests for layout of tables.""" import pytest -from weasyprint.formatting_structure import boxes -from weasyprint.layout.table import collapse_table_borders -from ..testing_utils import ( - assert_no_logs, capture_logs, parse_all, render_pages) - - -def _get_grid(html): - html = parse_all(html) - body, = html.children - table_wrapper, = body.children - table, = table_wrapper.children - return tuple( - [[(style, width, color) if width else None - for _score, (style, width, color) in column] - for column in grid] - for grid in collapse_table_borders(table)) +from ..testing_utils import assert_no_logs, capture_logs, render_pages @assert_no_logs @@ -2988,120 +2973,3 @@ def test_min_width_with_overflow(): assert table1_td1.min_width == table2_td1.min_width assert table1_td1.width == table2_td1.width - - -black = (0, 0, 0, 1) -red = (1, 0, 0, 1) -green = (0, 1, 0, 1) # lime in CSS -blue = (0, 0, 1, 1) -yellow = (1, 1, 0, 1) -black_3 = ('solid', 3, black) -red_1 = ('solid', 1, red) -yellow_5 = ('solid', 5, yellow) -green_5 = ('solid', 5, green) -dashed_blue_5 = ('dashed', 5, blue) - - -@assert_no_logs -def test_border_collapse_1(): - html = parse_all('
') - body, = html.children - table_wrapper, = body.children - table, = table_wrapper.children - assert isinstance(table, boxes.TableBox) - assert not hasattr(table, 'collapsed_border_grid') - - grid = _get_grid('
') - assert grid == ([], []) - - -@assert_no_logs -def test_border_collapse_2(): - vertical_borders, horizontal_borders = _get_grid(''' - - - - -
A B
C D
- ''') - assert vertical_borders == [ - [black_3, red_1, black_3], - [black_3, red_1, black_3], - ] - assert horizontal_borders == [ - [black_3, black_3], - [red_1, red_1], - [black_3, black_3], - ] - - -@assert_no_logs -def test_border_collapse_3(): - # hidden vs. none - vertical_borders, horizontal_borders = _get_grid(''' - - - - -
A B
C D
- ''') - assert vertical_borders == [ - [black_3, None, None], - [black_3, black_3, black_3], - ] - assert horizontal_borders == [ - [black_3, None], - [black_3, None], - [black_3, black_3], - ] - - -@assert_no_logs -def test_border_collapse_4(): - vertical_borders, horizontal_borders = _get_grid(''' - - - - - - - - -
- ''') - assert vertical_borders == [ - [yellow_5, black_3, red_1, yellow_5], - [yellow_5, dashed_blue_5, green_5, green_5], - [yellow_5, black_3, red_1, yellow_5], - [yellow_5, black_3, red_1, yellow_5], - ] - assert horizontal_borders == [ - [yellow_5, yellow_5, yellow_5], - [red_1, dashed_blue_5, green_5], - [red_1, dashed_blue_5, green_5], - [red_1, red_1, red_1], - [yellow_5, yellow_5, yellow_5], - ] - - -@assert_no_logs -def test_border_collapse_5(): - # rowspan and colspan - vertical_borders, horizontal_borders = _get_grid(''' - - - - - -
- ''') - assert vertical_borders == [ - [black_3, black_3, black_3, black_3], - [black_3, black_3, None, black_3], - ] - assert horizontal_borders == [ - [black_3, black_3, black_3], - [None, black_3, black_3], - [black_3, black_3, black_3], - ] - From 2a3747ac5245442c1d0aac8f23775ce4f457b270 Mon Sep 17 00:00:00 2001 From: Guillaume Ayoub Date: Sun, 10 Sep 2023 23:06:51 +0200 Subject: [PATCH 17/21] Mark extra test as xfail It was failing with previous version too. --- tests/draw/test_table.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/draw/test_table.py b/tests/draw/test_table.py index ad95d6da5..6015ee202 100644 --- a/tests/draw/test_table.py +++ b/tests/draw/test_table.py @@ -1473,6 +1473,7 @@ def test_running_elements_table_border_collapse_span(assert_pixels): ''') +@pytest.mark.xfail @assert_no_logs def test_running_elements_table_border_collapse_margin(assert_pixels): assert_pixels(2 * ''' From ce46dbe5a070f231979cfe2449b1d7221f363ed2 Mon Sep 17 00:00:00 2001 From: kygoh Date: Mon, 11 Sep 2023 11:14:17 +0000 Subject: [PATCH 18/21] Restore collapsed-borders conflict resolution to formatting_structure.build.wrap_table function --- weasyprint/formatting_structure/build.py | 3 +++ weasyprint/layout/table.py | 3 --- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/weasyprint/formatting_structure/build.py b/weasyprint/formatting_structure/build.py index e3a825e3d..b247eb195 100644 --- a/weasyprint/formatting_structure/build.py +++ b/weasyprint/formatting_structure/build.py @@ -11,6 +11,8 @@ from ..css import computed_values, properties, targets from ..logger import LOGGER from . import boxes +from ..layout.table import collapse_table_borders + # Maps values of the ``display`` CSS property to box types. BOX_TYPE_FROM_DISPLAY = { @@ -977,6 +979,7 @@ def wrap_table(box, children): if table.style['border_collapse'] == 'collapse': table.grid_width = grid_width table.grid_height = grid_height + table.collapsed_border_grid = collapse_table_borders(table) if isinstance(box, boxes.InlineTableBox): wrapper_type = boxes.InlineBlockBox diff --git a/weasyprint/layout/table.py b/weasyprint/layout/table.py index 456dbf637..4b08547d0 100644 --- a/weasyprint/layout/table.py +++ b/weasyprint/layout/table.py @@ -812,9 +812,6 @@ def auto_table_layout(context, box, containing_block): def table_wrapper_width(context, wrapper, containing_block): """Find the width of each column and derive the wrapper width.""" table = wrapper.get_wrapped_table() - collapse = table.style['border_collapse'] == 'collapse' - if collapse: - table.collapsed_border_grid = collapse_table_borders(table) resolve_percentages(table, containing_block) if table.style['table_layout'] == 'fixed' and table.width != 'auto': From d09ab65e1fd5dfcd324eacf23bc7fbbbee4843b8 Mon Sep 17 00:00:00 2001 From: kygoh Date: Mon, 18 Sep 2023 01:20:17 +0000 Subject: [PATCH 19/21] Restore unit tests for collapsed borders conflict resolution logic --- tests/layout/test_table.py | 133 ++++++++++++++++++++++++++++++++++++- 1 file changed, 132 insertions(+), 1 deletion(-) diff --git a/tests/layout/test_table.py b/tests/layout/test_table.py index 2832ef8c1..3205d49c4 100644 --- a/tests/layout/test_table.py +++ b/tests/layout/test_table.py @@ -1,8 +1,23 @@ """Tests for layout of tables.""" import pytest +from weasyprint.formatting_structure import boxes +from weasyprint.layout.table import collapse_table_borders -from ..testing_utils import assert_no_logs, capture_logs, render_pages +from ..testing_utils import ( + assert_no_logs, capture_logs, parse_all, render_pages) + + +def _get_grid(html): + html = parse_all(html) + body, = html.children + table_wrapper, = body.children + table, = table_wrapper.children + return tuple( + [[(style, width, color) if width else None + for _score, (style, width, color) in column] + for column in grid] + for grid in collapse_table_borders(table)) @assert_no_logs @@ -2973,3 +2988,119 @@ def test_min_width_with_overflow(): assert table1_td1.min_width == table2_td1.min_width assert table1_td1.width == table2_td1.width + + +black = (0, 0, 0, 1) +red = (1, 0, 0, 1) +green = (0, 1, 0, 1) # lime in CSS +blue = (0, 0, 1, 1) +yellow = (1, 1, 0, 1) +black_3 = ('solid', 3, black) +red_1 = ('solid', 1, red) +yellow_5 = ('solid', 5, yellow) +green_5 = ('solid', 5, green) +dashed_blue_5 = ('dashed', 5, blue) + + +@assert_no_logs +def test_border_collapse_1(): + html = parse_all('
') + body, = html.children + table_wrapper, = body.children + table, = table_wrapper.children + assert isinstance(table, boxes.TableBox) + assert not hasattr(table, 'collapsed_border_grid') + + grid = _get_grid('
') + assert grid == ([], []) + + +@assert_no_logs +def test_border_collapse_2(): + vertical_borders, horizontal_borders = _get_grid(''' + + + + +
A B
C D
+ ''') + assert vertical_borders == [ + [black_3, red_1, black_3], + [black_3, red_1, black_3], + ] + assert horizontal_borders == [ + [black_3, black_3], + [red_1, red_1], + [black_3, black_3], + ] + + +@assert_no_logs +def test_border_collapse_3(): + # hidden vs. none + vertical_borders, horizontal_borders = _get_grid(''' + + + + +
A B
C D
+ ''') + assert vertical_borders == [ + [black_3, None, None], + [black_3, black_3, black_3], + ] + assert horizontal_borders == [ + [black_3, None], + [black_3, None], + [black_3, black_3], + ] + + +@assert_no_logs +def test_border_collapse_4(): + vertical_borders, horizontal_borders = _get_grid(''' + + + + + + + + +
+ ''') + assert vertical_borders == [ + [yellow_5, black_3, red_1, yellow_5], + [yellow_5, dashed_blue_5, green_5, green_5], + [yellow_5, black_3, red_1, yellow_5], + [yellow_5, black_3, red_1, yellow_5], + ] + assert horizontal_borders == [ + [yellow_5, yellow_5, yellow_5], + [red_1, dashed_blue_5, green_5], + [red_1, dashed_blue_5, green_5], + [red_1, red_1, red_1], + [yellow_5, yellow_5, yellow_5], + ] + + +@assert_no_logs +def test_border_collapse_5(): + # rowspan and colspan + vertical_borders, horizontal_borders = _get_grid(''' + + + + + +
+ ''') + assert vertical_borders == [ + [black_3, black_3, black_3, black_3], + [black_3, black_3, None, black_3], + ] + assert horizontal_borders == [ + [black_3, black_3, black_3], + [None, black_3, black_3], + [black_3, black_3, black_3], + ] From ad6351024a1b41032ffa1103a679618fc1c2eb4a Mon Sep 17 00:00:00 2001 From: Guillaume Ayoub Date: Fri, 26 Jan 2024 16:32:02 +0100 Subject: [PATCH 20/21] Clean imports --- tests/test_boxes.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_boxes.py b/tests/test_boxes.py index 7c70be6ba..e0eb30f4c 100644 --- a/tests/test_boxes.py +++ b/tests/test_boxes.py @@ -5,7 +5,6 @@ from weasyprint.formatting_structure import boxes, build from weasyprint.layout.page import set_page_type_computed_styles - from .testing_utils import ( FakeHTML, assert_no_logs, assert_tree, capture_logs, parse, parse_all, render_pages) From f11fcc4c58122e5e16b5b55a4437d77603eef67b Mon Sep 17 00:00:00 2001 From: Guillaume Ayoub Date: Fri, 26 Jan 2024 21:09:15 +0100 Subject: [PATCH 21/21] =?UTF-8?q?Don=E2=80=99t=20store=20extra=20attribute?= =?UTF-8?q?s=20only=20for=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/layout/test_table.py | 19 ++++++++++--------- weasyprint/formatting_structure/build.py | 5 ++--- weasyprint/layout/table.py | 3 +-- 3 files changed, 13 insertions(+), 14 deletions(-) diff --git a/tests/layout/test_table.py b/tests/layout/test_table.py index bc5d05a73..00fbaac19 100644 --- a/tests/layout/test_table.py +++ b/tests/layout/test_table.py @@ -8,16 +8,17 @@ assert_no_logs, capture_logs, parse_all, render_pages) -def _get_grid(html): +def _get_grid(html, grid_width, grid_height): html = parse_all(html) body, = html.children table_wrapper, = body.children table, = table_wrapper.children + border_lists = collapse_table_borders(table, grid_width, grid_height) return tuple( [[(style, width, color) if width else None - for _score, (style, width, color) in column] - for column in grid] - for grid in collapse_table_borders(table)) + for _score, (style, width, color) in border] + for border in border_list] + for border_list in border_lists) @assert_no_logs @@ -3029,7 +3030,7 @@ def test_border_collapse_1(): assert isinstance(table, boxes.TableBox) assert not hasattr(table, 'collapsed_border_grid') - grid = _get_grid('
') + grid = _get_grid('
', 0, 0) assert grid == ([], []) @@ -3041,7 +3042,7 @@ def test_border_collapse_2(): A B C D - ''') + ''', 2, 2) assert vertical_borders == [ [black_3, red_1, black_3], [black_3, red_1, black_3], @@ -3062,7 +3063,7 @@ def test_border_collapse_3(): A B C D - ''') + ''', 2, 2) assert vertical_borders == [ [black_3, None, None], [black_3, black_3, black_3], @@ -3086,7 +3087,7 @@ def test_border_collapse_4(): - ''') + ''', 3, 4) assert vertical_borders == [ [yellow_5, black_3, red_1, yellow_5], [yellow_5, dashed_blue_5, green_5, green_5], @@ -3112,7 +3113,7 @@ def test_border_collapse_5(): - ''') + ''', 3, 2) assert vertical_borders == [ [black_3, black_3, black_3, black_3], [black_3, black_3, None, black_3], diff --git a/weasyprint/formatting_structure/build.py b/weasyprint/formatting_structure/build.py index 2cfd4dcd4..526da1d9f 100644 --- a/weasyprint/formatting_structure/build.py +++ b/weasyprint/formatting_structure/build.py @@ -972,9 +972,8 @@ def wrap_table(box, children): table = box.copy_with_children(row_groups) table.column_groups = tuple(column_groups) if table.style['border_collapse'] == 'collapse': - table.grid_width = grid_width - table.grid_height = grid_height - table.collapsed_border_grid = collapse_table_borders(table) + table.collapsed_border_grid = collapse_table_borders( + table, grid_width, grid_height) if isinstance(box, boxes.InlineTableBox): wrapper_type = boxes.InlineBlockBox diff --git a/weasyprint/layout/table.py b/weasyprint/layout/table.py index 9f9c59ecd..35b3e4cbb 100644 --- a/weasyprint/layout/table.py +++ b/weasyprint/layout/table.py @@ -986,7 +986,7 @@ def distribute_excess_width(context, grid, excess_width, column_widths, TRANSPARENT = tinycss2.color3.parse_color('transparent') -def collapse_table_borders(table): +def collapse_table_borders(table, grid_width, grid_height): """Resolve border conflicts for a table in the collapsing border model. Take a :class:`TableBox`; set appropriate border widths on the table, @@ -994,7 +994,6 @@ def collapse_table_borders(table): a data structure for the resolved collapsed border grid. """ - grid_width, grid_height = table.grid_width, table.grid_height if not (grid_width and grid_height): # Don’t bother with empty tables return [], []