From 25ea214b4faaa3943563750880fbb318b74b97db Mon Sep 17 00:00:00 2001 From: Guillaume Ayoub Date: Tue, 21 May 2024 16:20:17 +0200 Subject: [PATCH 01/10] Add simple test for grid-auto-flow: column --- tests/layout/test_grid.py | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/tests/layout/test_grid.py b/tests/layout/test_grid.py index 8fcb84caa..01d2d55b1 100644 --- a/tests/layout/test_grid.py +++ b/tests/layout/test_grid.py @@ -50,8 +50,7 @@ def test_grid_rows(): assert div_a.position_x == div_b.position_x == div_c.position_x == 0 assert div_a.position_y < div_b.position_y < div_c.position_y assert div_a.height == div_b.height == div_c.height - assert article.width == html.width - assert div_a.width == div_b.width == div_c.width == html.width + assert div_a.width == div_b.width == div_c.width == html.width == article.width @assert_no_logs @@ -856,3 +855,22 @@ def test_grid_item_margin(): article, = body.children div_a, div_b = article.children # TODO: Test auto margin values. + + +@assert_no_logs +def test_grid_auto_flow_column(): + page, = render_pages(''' +
+
a
+
b
+
c
+
+ ''') + html, = page.children + body, = html.children + article, = body.children + div_a, div_b, div_c = article.children + assert div_a.position_x < div_b.position_x < div_c.position_x + assert div_a.position_y == div_b.position_y == div_c.position_y == 0 + assert div_a.width == div_b.width == div_c.width + assert div_a.height == div_b.height == div_c.height == html.height == article.height From 5d5324abc0653c0b533e1f480118c07caf821334 Mon Sep 17 00:00:00 2001 From: Guillaume Ayoub Date: Tue, 21 May 2024 16:20:51 +0200 Subject: [PATCH 02/10] Handle grid-auto-flow: column in step 1.2 --- weasyprint/layout/grid.py | 91 ++++++++++++++++++++++----------------- 1 file changed, 51 insertions(+), 40 deletions(-) diff --git a/weasyprint/layout/grid.py b/weasyprint/layout/grid.py index 7a2115169..b067692b9 100644 --- a/weasyprint/layout/grid.py +++ b/weasyprint/layout/grid.py @@ -126,48 +126,53 @@ def _get_span(place): return span -def _get_column_placement(row_placement, column_start, column_end, - columns, children_positions, dense): - occupied_columns = set() +def _get_second_placement(first_placement, second_start, second_end, + second_tracks, children_positions, first_flow, dense): + occupied_tracks = set() for x, y, width, height in children_positions.values(): # Test whether cells overlap. - if _intersect(y, height, *row_placement): - for x in range(x, x + width): - occupied_columns.add(x) + if first_flow == 'row': + if _intersect(y, height, *first_placement): + for x in range(x, x + width): + occupied_tracks.add(x) + else: + if _intersect(x, width, *first_placement): + for y in range(y, y + height): + occupied_tracks.add(y) if dense: - for x in count(): - if x in occupied_columns: + for track in count(): + if track in occupied_tracks: continue - if column_start == 'auto': - placement = _get_placement( - (None, x + 1, None), column_end, columns) + if second_start == 'auto': + placement = _get_placement( + (None, track + 1, None), second_end, second_tracks) else: - assert column_start[0] == 'span' + assert second_start[0] == 'span' # If the placement contains two spans, remove the one # contributed by the end grid-placement property. # https://drafts.csswg.org/css-grid/#grid-placement-errors - assert column_start == 'auto' or column_start[0] == 'span' - span = _get_span(column_start) + assert second_start == 'auto' or second_start[0] == 'span' + span = _get_span(second_start) placement = _get_placement( - column_start, (None, x + 1 + span, None), columns) - columns = range(placement[0], placement[0] + placement[1]) - if not set(columns) & occupied_columns: + second_start, (None, track + 1 + span, None), second_tracks) + tracks = range(placement[0], placement[0] + placement[1]) + if not set(tracks) & occupied_tracks: return placement else: - y = max(occupied_columns or [0]) + 1 - if column_start == 'auto': + track = max(occupied_tracks or [0]) + 1 + if second_start == 'auto': return _get_placement( - (None, y + 1, None), column_end, columns) + (None, track + 1, None), second_end, second_tracks) else: - assert column_start[0] == 'span' + assert second_start[0] == 'span' # If the placement contains two spans, remove the one contributed # by the end grid-placement property. # https://drafts.csswg.org/css-grid/#grid-placement-errors - assert column_start == 'auto' or column_start[0] == 'span' - for end_y in count(y + 1): + assert second_start == 'auto' or second_start[0] == 'span' + for end_track in count(track + 1): placement = _get_placement( - column_start, (None, end_y + 1, None), columns) - if placement[0] >= y: + second_start, (None, end_track + 1, None), second_tracks) + if placement[0] >= track: return placement @@ -593,10 +598,6 @@ def grid_layout(context, box, bottom_space, skip_stack, containing_block, refer_to = 0 if box.height == 'auto' else box.height row_gap = percentage(row_gap, refer_to) - # TODO: Support 'column' value in grid-auto-flow. - if 'column' in flow: - LOGGER.warning('"column" is not supported in grid-auto-flow') - if grid_areas == 'none': grid_areas = ((None,),) grid_areas = [list(row) for row in grid_areas] @@ -652,6 +653,9 @@ def grid_layout(context, box, bottom_space, skip_stack, containing_block, # 1. Run the grid placement algorithm. + first_flow = 'column' if 'column' in flow else 'row' + second_flow = 'row' if 'column' in flow else 'column' + # 1.1 Position anything that’s not auto-positioned. children_positions = {} for child in box.children: @@ -669,22 +673,29 @@ def grid_layout(context, box, bottom_space, skip_stack, containing_block, y, height = row_placement children_positions[child] = (x, y, width, height) - # 1.2 Process the items locked to a given row. + # 1.2 Process the items locked to a given row (resp. column). children = sorted(box.children, key=lambda item: item.style['order']) for child in children: if child in children_positions: continue - row_start = child.style['grid_row_start'] - row_end = child.style['grid_row_end'] - row_placement = _get_placement(row_start, row_end, rows[::2]) - if not row_placement: + first_start = child.style[f'grid_{first_flow}_start'] + first_end = child.style[f'grid_{first_flow}_end'] + first_tracks = rows if first_flow == 'row' else columns + first_placement = _get_placement(first_start, first_end, first_tracks[::2]) + if not first_placement: continue - y, height = row_placement - column_start = child.style['grid_column_start'] - column_end = child.style['grid_column_end'] - x, width = _get_column_placement( - row_placement, column_start, column_end, columns, - children_positions, 'dense' in flow) + second_start = child.style[f'grid_{second_flow}_start'] + second_end = child.style[f'grid_{second_flow}_end'] + second_tracks = rows if second_flow == 'row' else columns + second_placement = _get_second_placement( + first_placement, second_start, second_end, second_tracks, + children_positions, first_flow, 'dense' in flow) + if first_flow == 'row': + y, height = first_placement + x, width = second_placement + else: + x, width = first_placement + y, height = second_placement children_positions[child] = (x, y, width, height) # 1.3 Determine the columns in the implicit grid. From 1a10054c2b7b09c94554458b39338651e3e21b43 Mon Sep 17 00:00:00 2001 From: Guillaume Ayoub Date: Tue, 21 May 2024 16:55:26 +0200 Subject: [PATCH 03/10] Handle grid-auto-flow: column in step 1.3 --- weasyprint/layout/grid.py | 73 +++++++++++++++++++++------------------ 1 file changed, 40 insertions(+), 33 deletions(-) diff --git a/weasyprint/layout/grid.py b/weasyprint/layout/grid.py index b067692b9..84cd7c366 100644 --- a/weasyprint/layout/grid.py +++ b/weasyprint/layout/grid.py @@ -653,8 +653,8 @@ def grid_layout(context, box, bottom_space, skip_stack, containing_block, # 1. Run the grid placement algorithm. - first_flow = 'column' if 'column' in flow else 'row' - second_flow = 'row' if 'column' in flow else 'column' + first_flow = 'column' if 'column' in flow else 'row' # auto flow axis + second_flow = 'row' if 'column' in flow else 'column' # other axis # 1.1 Position anything that’s not auto-positioned. children_positions = {} @@ -698,37 +698,44 @@ def grid_layout(context, box, bottom_space, skip_stack, containing_block, y, height = second_placement children_positions[child] = (x, y, width, height) - # 1.3 Determine the columns in the implicit grid. - # 1.3.1 Start with the columns from the explicit grid. - implicit_x1 = 0 - implicit_x2 = len(grid_areas[0]) if grid_areas else 0 - # 1.3.2 Add columns to the beginning and end of the implicit grid. + # 1.3 Determine the columns (resp. rows) in the implicit grid. + # 1.3.1 Start with the columns (resp. rows) from the explicit grid. + implicit_second_1 = 0 + if second_flow == 'column': + implicit_second_2 = len(grid_areas[0]) if grid_areas else 0 + else: + implicit_second_2 = len(grid_areas) + # 1.3.2 Add columns (resp. rows) to the beginning and end of the implicit grid. remaining_grid_items = [] for child in children: if child in children_positions: - x, _, width, _ = children_positions[child] + if second_flow == 'column': + i, _, size, _ = children_positions[child] + else: + _, i, _, size = children_positions[child] else: - column_start = child.style['grid_column_start'] - column_end = child.style['grid_column_end'] - column_placement = _get_placement( - column_start, column_end, columns[::2]) + second_start = child.style[f'grid_{second_flow}_start'] + second_end = child.style[f'grid_{second_flow}_end'] + second_tracks = rows if second_flow == 'row' else columns + second_placement = _get_placement( + second_start, second_end, second_tracks[::2]) remaining_grid_items.append(child) - if column_placement: - x, width = column_placement + if second_placement: + i, size = second_placement else: continue - implicit_x1 = min(x, implicit_x1) - implicit_x2 = max(x + width, implicit_x2) - # 1.3.3 Add columns to accommodate max column span. + implicit_second_1 = min(i, implicit_second_1) + implicit_second_2 = max(i + size, implicit_second_2) + # 1.3.3 Add columns (resp. rows) to accommodate max track span. for child in remaining_grid_items: - column_start = child.style['grid_column_start'] - column_end = child.style['grid_column_end'] + second_start = child.style[f'grid_{second_flow}_start'] + second_end = child.style[f'grid_{second_flow}_end'] span = 1 - if column_start != 'auto' and column_start[0] == 'span': - span = column_start[1] - elif column_end != 'auto' and column_end[0] == 'span': - span = column_end[1] - implicit_x2 = max(implicit_x1 + (span or 1), implicit_x2) + if second_start != 'auto' and second_start[0] == 'span': + span = second_start[1] + elif second_end != 'auto' and second_end[0] == 'span': + span = second_end[1] + implicit_second_2 = max(implicit_second_1 + (span or 1), implicit_second_2) # 1.4 Position the remaining grid items. implicit_y1 = 0 @@ -737,7 +744,7 @@ def grid_layout(context, box, bottom_space, skip_stack, containing_block, _, y, _, height = position implicit_y1 = min(y, implicit_y1) implicit_y2 = max(y + height, implicit_y2) - cursor_x, cursor_y = implicit_x1, implicit_y1 + cursor_x, cursor_y = implicit_second_1, implicit_y1 if 'dense' in flow: for child in remaining_grid_items: column_start = child.style['grid_column_start'] @@ -785,7 +792,7 @@ def grid_layout(context, box, bottom_space, skip_stack, containing_block, children_positions[child] = (x, y, width, height) else: # 1. Set the cursor’s row and column positions. - cursor_x, cursor_y = implicit_x1, implicit_y1 + cursor_x, cursor_y = implicit_second_1, implicit_y1 while True: # 2. Increment the column position of the cursor. y = cursor_y @@ -793,7 +800,7 @@ def grid_layout(context, box, bottom_space, skip_stack, containing_block, row_end = child.style['grid_row_end'] column_start = child.style['grid_column_start'] column_end = child.style['grid_column_end'] - for x in range(cursor_x, implicit_x2): + for x in range(cursor_x, implicit_second_2): if row_start == 'auto': y, height = _get_placement( (None, y + 1, None), row_end, rows[::2]) @@ -836,7 +843,7 @@ def grid_layout(context, box, bottom_space, skip_stack, containing_block, rows.append(next(auto_rows)) rows.append([]) implicit_y2 = cursor_y - cursor_x = implicit_x1 + cursor_x = implicit_second_1 continue break else: @@ -894,7 +901,7 @@ def grid_layout(context, box, bottom_space, skip_stack, containing_block, row_end = child.style['grid_row_end'] column_start = child.style['grid_column_start'] column_end = child.style['grid_column_end'] - for x in range(cursor_x, implicit_x2): + for x in range(cursor_x, implicit_second_2): if row_start == 'auto': y, height = _get_placement( (None, y + 1, None), row_end, rows[::2]) @@ -931,14 +938,14 @@ def grid_layout(context, box, bottom_space, skip_stack, containing_block, rows.append(next(auto_rows)) rows.append([]) implicit_y2 = cursor_y - cursor_x = implicit_x1 + cursor_x = implicit_second_1 continue break - for _ in range(0 - implicit_x1): + for _ in range(0 - implicit_second_1): columns.insert(0, next(auto_columns_back)) columns.insert(0, []) - for _ in range(len(grid_areas[0]) if grid_areas else 0, implicit_x2): + for _ in range(len(grid_areas[0]) if grid_areas else 0, implicit_second_2): columns.append(next(auto_columns)) columns.append([]) for _ in range(0 - implicit_y1): @@ -970,7 +977,7 @@ def grid_layout(context, box, bottom_space, skip_stack, containing_block, # 3.1 Resolve the sizes of the grid columns. columns_sizes = _resolve_tracks_sizes( - column_sizing_functions, box.width, children_positions, implicit_x1, + column_sizing_functions, box.width, children_positions, implicit_second_1, 'x', column_gap, context, box) # 3.2 Resolve the sizes of the grid rows. From 8686885187bc2cbd2501953a4a204fc73f38fbc5 Mon Sep 17 00:00:00 2001 From: Guillaume Ayoub Date: Tue, 21 May 2024 23:11:30 +0200 Subject: [PATCH 04/10] Handle grid-auto-flow: column in step 1.4 --- tests/layout/test_grid.py | 22 +-- weasyprint/layout/grid.py | 320 ++++++++++++++++++++++---------------- 2 files changed, 194 insertions(+), 148 deletions(-) diff --git a/tests/layout/test_grid.py b/tests/layout/test_grid.py index 01d2d55b1..3d3833f5b 100644 --- a/tests/layout/test_grid.py +++ b/tests/layout/test_grid.py @@ -516,9 +516,9 @@ def test_grid_shorthand_auto_flow_columns_none_dense(): display: grid; font-family: weasyprint; font-size: 2px; - grid: none / auto-flow 1fr dense; + grid: none / auto-flow dense 1fr; line-height: 1; - width: 10px; + width: 12px; }
@@ -531,13 +531,13 @@ def test_grid_shorthand_auto_flow_columns_none_dense(): body, = html.children article, = body.children div_a, div_b, div_c = article.children - assert div_a.position_x == div_b.position_x == div_c.position_x == 0 - assert div_a.position_y == 0 - assert div_b.position_y == 2 - assert div_c.position_y == 4 - assert div_a.width == div_b.width == div_c.width == 10 - assert {div.height for div in article.children} == {2} - assert article.width == 10 + assert div_a.position_x == 0 + assert div_b.position_x == 4 + assert div_c.position_x == 8 + assert div_a.position_y == div_b.position_y == div_c.position_y == 0 + assert div_a.height == div_b.height == div_c.height == 2 + assert {div.width for div in article.children} == {4} + assert article.width == 12 @assert_no_logs @@ -862,8 +862,8 @@ def test_grid_auto_flow_column(): page, = render_pages('''
a
-
b
-
c
+
a
+
a
''') html, = page.children diff --git a/weasyprint/layout/grid.py b/weasyprint/layout/grid.py index 84cd7c366..9b1e71126 100644 --- a/weasyprint/layout/grid.py +++ b/weasyprint/layout/grid.py @@ -144,8 +144,8 @@ def _get_second_placement(first_placement, second_start, second_end, if track in occupied_tracks: continue if second_start == 'auto': - placement = _get_placement( - (None, track + 1, None), second_end, second_tracks) + placement = _get_placement( + (None, track + 1, None), second_end, second_tracks) else: assert second_start[0] == 'span' # If the placement contains two spans, remove the one @@ -655,6 +655,9 @@ def grid_layout(context, box, bottom_space, skip_stack, containing_block, first_flow = 'column' if 'column' in flow else 'row' # auto flow axis second_flow = 'row' if 'column' in flow else 'column' # other axis + first_tracks = rows if first_flow == 'row' else columns + second_tracks = rows if second_flow == 'row' else columns + auto_tracks = auto_rows if first_flow == 'row' else auto_columns # 1.1 Position anything that’s not auto-positioned. children_positions = {} @@ -680,13 +683,11 @@ def grid_layout(context, box, bottom_space, skip_stack, containing_block, continue first_start = child.style[f'grid_{first_flow}_start'] first_end = child.style[f'grid_{first_flow}_end'] - first_tracks = rows if first_flow == 'row' else columns first_placement = _get_placement(first_start, first_end, first_tracks[::2]) if not first_placement: continue second_start = child.style[f'grid_{second_flow}_start'] second_end = child.style[f'grid_{second_flow}_end'] - second_tracks = rows if second_flow == 'row' else columns second_placement = _get_second_placement( first_placement, second_start, second_end, second_tracks, children_positions, first_flow, 'dense' in flow) @@ -716,7 +717,6 @@ def grid_layout(context, box, bottom_space, skip_stack, containing_block, else: second_start = child.style[f'grid_{second_flow}_start'] second_end = child.style[f'grid_{second_flow}_end'] - second_tracks = rows if second_flow == 'row' else columns second_placement = _get_placement( second_start, second_end, second_tracks[::2]) remaining_grid_items.append(child) @@ -738,40 +738,52 @@ def grid_layout(context, box, bottom_space, skip_stack, containing_block, implicit_second_2 = max(implicit_second_1 + (span or 1), implicit_second_2) # 1.4 Position the remaining grid items. - implicit_y1 = 0 - implicit_y2 = len(grid_areas) + implicit_first_1 = 0 + if first_flow == 'row': + implicit_first_2 = len(grid_areas) + else: + implicit_first_2 = len(grid_areas[0]) if grid_areas else 0 for position in children_positions.values(): - _, y, _, height = position - implicit_y1 = min(y, implicit_y1) - implicit_y2 = max(y + height, implicit_y2) - cursor_x, cursor_y = implicit_second_1, implicit_y1 + if first_flow == 'row': + _, i, _, size = position + else: + i, _, size, _ = position + implicit_first_1 = min(i, implicit_first_1) + implicit_first_2 = max(i + size, implicit_first_2) + cursor_first, cursor_second = implicit_first_1, implicit_second_1 if 'dense' in flow: for child in remaining_grid_items: - column_start = child.style['grid_column_start'] - column_end = child.style['grid_column_end'] - column_placement = _get_placement( - column_start, column_end, columns[::2]) - if column_placement: - # 1. Set the row position of the cursor. - cursor_y = implicit_y1 - x, width = column_placement - cursor_x = x - # 2. Increment the cursor’s row position. - row_start = child.style['grid_row_start'] - row_end = child.style['grid_row_end'] - for y in count(cursor_y): - if row_start == 'auto': - y, height = _get_placement( - (None, y + 1, None), row_end, rows[::2]) + second_start = child.style[f'grid_{second_flow}_start'] + second_end = child.style[f'grid_{second_flow}_end'] + second_placement = _get_placement( + second_start, second_end, second_tracks[::2]) + if second_placement: + # 1. Set the row (resp. column) position of the cursor. + cursor_first = implicit_first_1 + second_i, second_size = second_placement + cursor_second = second_i + # 2. Increment the cursor’s row (resp. column) position. + first_start = child.style[f'grid_{first_flow}_start'] + first_end = child.style[f'grid_{first_flow}_end'] + for first_i in count(cursor_first): + if first_start == 'auto': + first_i, first_size = _get_placement( + (None, first_i + 1, None), first_end, first_tracks[::2]) else: - assert row_start[0] == 'span' - assert row_start == 'auto' or row_start[0] == 'span' - span = _get_span(row_start) - y, height = _get_placement( - row_start, (None, y + 1 + span, None), rows[::2]) - if y < cursor_y: + assert first_start[0] == 'span' + span = _get_span(first_start) + first_i, first_size = _get_placement( + first_start, (None, first_i + 1 + span, None), + first_tracks[::2]) + if first_i < cursor_first: continue - for row in range(y, y + height): + for _ in range(first_i, first_i + first_size): + if first_flow == 'row': + x, y = second_i, first_i + width, height = second_size, first_size + else: + x, y = first_i, second_i + width, height = first_size, second_size intersect = _intersect_with_children( x, y, width, height, children_positions.values()) if intersect: @@ -782,41 +794,55 @@ def grid_layout(context, box, bottom_space, skip_stack, containing_block, # Child doesn’t intersect with any positioned child on # any row. break - y_diff = y + height - implicit_y2 - if y_diff > 0: - for _ in range(y_diff): - rows.append(next(auto_rows)) - rows.append([]) - implicit_y2 = y + height + first_diff = first_i + first_size - implicit_first_2 + if first_diff > 0: + for _ in range(first_diff): + first_tracks.append(next(auto_tracks)) + first_tracks.append([]) + implicit_first_2 = first_i + first_size # 3. Set the item’s row-start line. + if first_flow == 'row': + x, y = second_i, first_i + width, height = second_size, first_size + else: + x, y = first_i, second_i + width, height = first_size, second_size children_positions[child] = (x, y, width, height) else: # 1. Set the cursor’s row and column positions. - cursor_x, cursor_y = implicit_second_1, implicit_y1 + cursor_first, cursor_second = implicit_first_1, implicit_second_1 while True: - # 2. Increment the column position of the cursor. - y = cursor_y - row_start = child.style['grid_row_start'] - row_end = child.style['grid_row_end'] - column_start = child.style['grid_column_start'] - column_end = child.style['grid_column_end'] - for x in range(cursor_x, implicit_second_2): - if row_start == 'auto': - y, height = _get_placement( - (None, y + 1, None), row_end, rows[::2]) + # 2. Increment the column (resp. row) position of the cursor. + first_i = cursor_first + first_start = child.style[f'grid_{first_flow}_start'] + first_end = child.style[f'grid_{first_flow}_end'] + second_start = child.style[f'grid_{second_flow}_start'] + second_end = child.style[f'grid_{second_flow}_end'] + for second_i in range(cursor_second, implicit_second_2): + if first_start == 'auto': + first_i, first_size = _get_placement( + (None, first_i + 1, None), first_end, first_tracks[::2]) + else: + assert first_start[0] == 'span' + span = _get_span(first_start) + first_i, first_size = _get_placement( + first_start, (None, first_i + 1 + span, None), + first_tracks[::2]) + if second_start == 'auto': + second_i, second_size = _get_placement( + (None, second_i + 1, None), second_end, + second_tracks[::2]) else: - span = _get_span(row_start) - y, height = _get_placement( - row_start, (None, y + 1 + span, None), - rows[::2]) - if column_start == 'auto': - x, width = _get_placement( - (None, x + 1, None), column_end, columns[::2]) + span = _get_span(second_start) + second_i, second_size = _get_placement( + second_start, (None, second_i + 1 + span, None), + second_tracks[::2]) + if first_flow == 'row': + x, y = second_i, first_i + width, height = second_size, first_size else: - span = _get_span(column_start) - x, width = _get_placement( - column_start, (None, x + 1 + span, None), - columns[::2]) + x, y = first_i, second_i + width, height = first_size, second_size intersect = _intersect_with_children( x, y, width, height, children_positions.values()) if intersect: @@ -826,55 +852,62 @@ def grid_layout(context, box, bottom_space, skip_stack, containing_block, # Free place found. # 3. Set the item’s row-/column-start lines. children_positions[child] = (x, y, width, height) - y_diff = cursor_y + height - 1 - implicit_y2 - if y_diff > 0: - for _ in range(y_diff): - rows.append(next(auto_rows)) - rows.append([]) - implicit_y2 = cursor_y + height - 1 + first_diff = ( + cursor_first + first_size - 1 - implicit_first_2) + if first_diff > 0: + for _ in range(first_diff): + first_tracks.append(next(auto_tracks)) + first_tracks.append([]) + implicit_first_2 = cursor_first + first_size - 1 break else: # No room found. # 2. Return to the previous step. - cursor_y += 1 - y_diff = cursor_y + 1 - implicit_y2 - if y_diff > 0: - for _ in range(y_diff): - rows.append(next(auto_rows)) - rows.append([]) - implicit_y2 = cursor_y - cursor_x = implicit_second_1 + cursor_first += 1 + first_diff = cursor_first - implicit_first_2 + if first_diff > 0: + for _ in range(first_diff): + first_tracks.append(next(auto_tracks)) + first_tracks.append([]) + implicit_first_2 = cursor_first + cursor_second = implicit_second_1 continue break else: for child in remaining_grid_items: - column_start = child.style['grid_column_start'] - column_end = child.style['grid_column_end'] - column_placement = _get_placement( - column_start, column_end, columns[::2]) - if column_placement: - # 1. Set the column position of the cursor. - x, width = column_placement - if x < cursor_x: - cursor_y += 1 - cursor_x = x - # 2. Increment the cursor’s row position. - row_start = child.style['grid_row_start'] - row_end = child.style['grid_row_end'] - for cursor_y in count(cursor_y): - if row_start == 'auto': - y, height = _get_placement( - (None, cursor_y + 1, None), row_end, rows[::2]) + second_start = child.style[f'grid_{second_flow}_start'] + second_end = child.style[f'grid_{second_flow}_end'] + second_placement = _get_placement( + second_start, second_end, second_tracks[::2]) + if second_placement: + # 1. Set the column (resp. row) position of the cursor. + second_i, second_size = second_placement + if second_i < cursor_second: + cursor_first += 1 + cursor_second = second_i + # 2. Increment the cursor’s row (resp. column) position. + first_start = child.style[f'grid_{first_flow}_start'] + first_end = child.style[f'grid_{first_flow}_end'] + for cursor_first in count(cursor_first): + if first_start == 'auto': + first_i, first_size = _get_placement( + (None, cursor_first + 1, None), first_end, + first_tracks[::2]) else: - assert row_start[0] == 'span' - assert row_start == 'auto' or row_start[0] == 'span' - span = _get_span(row_start) - y, height = _get_placement( - row_start, (None, cursor_y + 1 + span, None), - rows[::2]) - if y < cursor_y: + assert first_start[0] == 'span' + span = _get_span(first_start) + first_i, first_size = _get_placement( + first_start, (None, first_i + 1 + span, None), + first_tracks[::2]) + if first_i < cursor_first: continue - for row in range(y, y + height): + for row in range(first_i, first_i + first_size): + if first_flow == 'row': + x, y = second_i, first_i + width, height = second_size, first_size + else: + x, y = first_i, second_i + width, height = first_size, second_size intersect = _intersect_with_children( x, y, width, height, children_positions.values()) if intersect: @@ -885,39 +918,46 @@ def grid_layout(context, box, bottom_space, skip_stack, containing_block, # Child doesn’t intersect with any positioned child on # any row. break - y_diff = y + height - implicit_y2 - if y_diff > 0: - for _ in range(y_diff): - rows.append(next(auto_rows)) - rows.append([]) - implicit_y2 = y + height + first_diff = first_i + first_size - implicit_first_2 + if first_diff > 0: + for _ in range(first_diff): + first_tracks.append(next(auto_tracks)) + first_tracks.append([]) + implicit_first_2 = y + height # 3. Set the item’s row-start line. children_positions[child] = (x, y, width, height) else: while True: # 1. Increment the column position of the cursor. - y = cursor_y - row_start = child.style['grid_row_start'] - row_end = child.style['grid_row_end'] - column_start = child.style['grid_column_start'] - column_end = child.style['grid_column_end'] - for x in range(cursor_x, implicit_second_2): - if row_start == 'auto': - y, height = _get_placement( - (None, y + 1, None), row_end, rows[::2]) + first_i = cursor_first + first_start = child.style[f'grid_{first_flow}_start'] + first_end = child.style[f'grid_{first_flow}_end'] + second_start = child.style[f'grid_{second_flow}_start'] + second_end = child.style[f'grid_{second_flow}_end'] + for second_i in range(cursor_second, implicit_second_2): + if first_start == 'auto': + first_i, first_size = _get_placement( + (None, first_i + 1, None), first_end, first_tracks[::2]) else: - span = _get_span(row_start) - y, height = _get_placement( - row_start, (None, y + 1 + span, None), - rows[::2]) - if column_start == 'auto': - x, width = _get_placement( - (None, x + 1, None), column_end, columns[::2]) + span = _get_span(first_start) + first_i, first_size = _get_placement( + first_start, (None, first_i + 1 + span, None), + first_tracks[::2]) + if second_start == 'auto': + second_i, second_size = _get_placement( + (None, second_i + 1, None), second_end, + second_tracks[::2]) else: - span = _get_span(column_start) - x, width = _get_placement( - column_start, (None, x + 1 + span, None), - columns[::2]) + span = _get_span(second_start) + second_i, second_size = _get_placement( + second_start, (None, second_i + 1 + span, None), + second_tracks[::2]) + if first_flow == 'row': + x, y = second_i, first_i + width, height = second_size, first_size + else: + x, y = first_i, second_i + width, height = first_size, second_size intersect = _intersect_with_children( x, y, width, height, children_positions.values()) if intersect: @@ -931,21 +971,27 @@ def grid_layout(context, box, bottom_space, skip_stack, containing_block, else: # No room found. # 2. Return to the previous step. - cursor_y += 1 - y_diff = cursor_y + 1 - implicit_y2 - if y_diff > 0: - for _ in range(y_diff): - rows.append(next(auto_rows)) - rows.append([]) - implicit_y2 = cursor_y - cursor_x = implicit_second_1 + cursor_first += 1 + first_diff = cursor_first + 1 - implicit_first_2 + if first_diff > 0: + for _ in range(first_diff): + first_tracks.append(next(auto_tracks)) + first_tracks.append([]) + implicit_first_2 = cursor_first + cursor_second = implicit_second_1 continue break - for _ in range(0 - implicit_second_1): + if first_flow == 'row': + implicit_x1, implicit_x2 = implicit_second_1, implicit_second_2 + implicit_y1, implicit_y2 = implicit_first_1, implicit_first_2 + else: + implicit_x1, implicit_x2 = implicit_first_1, implicit_first_2 + implicit_y1, implicit_y2 = implicit_second_1, implicit_second_2 + for _ in range(0 - implicit_x1): columns.insert(0, next(auto_columns_back)) columns.insert(0, []) - for _ in range(len(grid_areas[0]) if grid_areas else 0, implicit_second_2): + for _ in range(len(grid_areas[0]) if grid_areas else 0, implicit_x2): columns.append(next(auto_columns)) columns.append([]) for _ in range(0 - implicit_y1): From 40488698ee25692951770bbf4609f0b39fb62915 Mon Sep 17 00:00:00 2001 From: Guillaume Ayoub Date: Wed, 22 May 2024 22:58:04 +0200 Subject: [PATCH 05/10] Add tests for grid-template validation --- tests/css/test_expanders.py | 42 +++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/tests/css/test_expanders.py b/tests/css/test_expanders.py index 56189ded3..edc96517e 100644 --- a/tests/css/test_expanders.py +++ b/tests/css/test_expanders.py @@ -708,6 +708,48 @@ def test_grid_area_invalid(rule): assert_invalid(f'grid-area: {rule}') +@assert_no_logs +@pytest.mark.parametrize('rule, result', ( + ('none', { + 'rows': 'none', 'columns': 'none', 'areas': 'none', + }), + ('subgrid / [outer-edge] 20px [main-start]', { + 'rows': ('subgrid', ()), + 'columns': (('outer-edge',), (20, 'px'), ('main-start',)), + 'areas': 'none', + }), + ('repeat(2, [e] 40px) repeat(5, auto) / subgrid [a] repeat(auto-fill, [b])', { + 'rows': ( + (), ('repeat()', 2, (('e',), (40, 'px'), ())), (), + ('repeat()', 5, ((), 'auto', ())), ()), + 'columns': ('subgrid', (('a',), ('repeat()', 'auto-fill', (('b',),)))), + 'areas': 'none', + }), + # TODO: support last syntax + # ('[a b] "x y y" [c] [d] "x y y" 1fr [e] / auto 2fr auto', { + # 'rows': 'none', 'columns': 'none', 'areas': 'none', + # }), + # ('[a b c] "x x x" 2fr', { + # 'rows': 'none', 'columns': 'none', 'areas': 'none', + # }), +)) +def test_grid_template(rule, result): + assert expand_to_dict(f'grid-template: {rule}') == dict( + (f'grid_template_{key}', value) for key, value in result.items()) + +@assert_no_logs +@pytest.mark.parametrize('rule', ( + 'none none', + 'auto', + 'subgrid / subgrid / subgrid', + '[a] 1px [b] / none /', + '[a] 1px [b] // none', + '[a] 1px [b] none', +)) +def test_grid_template_invalid(rule): + assert_invalid(f'grid-template: {rule}') + + @assert_no_logs @pytest.mark.parametrize('rule, result', ( ('page-break-after: left', {'break_after': 'left'}), From 3359db5cd86293bc9098c5c0d2fa375c0483da30 Mon Sep 17 00:00:00 2001 From: Guillaume Ayoub Date: Thu, 23 May 2024 10:35:24 +0200 Subject: [PATCH 06/10] Test grid shorthand and fix minor bugs --- tests/css/test_expanders.py | 64 ++++++++++++++++++++++++++ weasyprint/css/validation/expanders.py | 9 +++- 2 files changed, 71 insertions(+), 2 deletions(-) diff --git a/tests/css/test_expanders.py b/tests/css/test_expanders.py index edc96517e..e5654e615 100644 --- a/tests/css/test_expanders.py +++ b/tests/css/test_expanders.py @@ -750,6 +750,70 @@ def test_grid_template_invalid(rule): assert_invalid(f'grid-template: {rule}') +@assert_no_logs +@pytest.mark.parametrize('rule, result', ( + ('none', { + 'template_rows': 'none', 'template_columns': 'none', + 'template_areas': 'none', + 'auto_rows': ('auto',), 'auto_columns': ('auto',), + 'auto_flow': ('row',), + }), + ('subgrid / [outer-edge] 20px [main-start]', { + 'template_rows': ('subgrid', ()), + 'template_columns': (('outer-edge',), (20, 'px'), ('main-start',)), + 'template_areas': 'none', + 'auto_rows': ('auto',), 'auto_columns': ('auto',), + 'auto_flow': ('row',), + }), + ('repeat(2, [e] 40px) repeat(5, auto) / subgrid [a] repeat(auto-fill, [b])', { + 'template_rows': ( + (), ('repeat()', 2, (('e',), (40, 'px'), ())), (), + ('repeat()', 5, ((), 'auto', ())), ()), + 'template_columns': ('subgrid', (('a',), ('repeat()', 'auto-fill', (('b',),)))), + 'template_areas': 'none', + 'auto_rows': ('auto',), 'auto_columns': ('auto',), + 'auto_flow': ('row',), + }), + ('auto-flow 1fr / 100px', { + 'template_rows': 'none', 'template_columns': ((), (100, 'px'), ()), + 'template_areas': 'none', + 'auto_rows': ((1, 'fr'),), 'auto_columns': ('auto',), + 'auto_flow': ('row',), + }), + ('none / dense auto-flow 1fr', { + 'template_rows': 'none', 'template_columns': 'none', + 'template_areas': 'none', + 'auto_rows': ('auto',), 'auto_columns': ((1, 'fr'),), + 'auto_flow': ('column', 'dense'), + }), + # TODO: support last grid-template syntax + # ('[a b] "x y y" [c] [d] "x y y" 1fr [e] / auto 2fr auto', { + # }), + # ('[a b c] "x x x" 2fr', { + # }), +)) +def test_grid(rule, result): + assert expand_to_dict(f'grid: {rule}') == dict( + (f'grid_{key}', value) for key, value in result.items()) + + +@assert_no_logs +@pytest.mark.parametrize('rule', ( + 'none none', + 'auto', + 'subgrid / subgrid / subgrid', + '[a] 1px [b] / none /', + '[a] 1px [b] // none', + '[a] 1px [b] none', + 'none / auto-flow 1fr dense', + 'none / dense 1fr auto-flow', + '100px auto-flow / none', + 'dense 100px / auto-flow 1fr' +)) +def test_grid_invalid(rule): + assert_invalid(f'grid: {rule}') + + @assert_no_logs @pytest.mark.parametrize('rule, result', ( ('page-break-after: left', {'break_after': 'left'}), diff --git a/weasyprint/css/validation/expanders.py b/weasyprint/css/validation/expanders.py index a9cb5efb0..51794f629 100644 --- a/weasyprint/css/validation/expanders.py +++ b/weasyprint/css/validation/expanders.py @@ -830,17 +830,22 @@ def expand_grid(tokens, name): templates = {'row': [], 'column': []} iterable = zip(split_tokens, templates.items()) for tokens, (track, track_templates) in iterable: + auto_flow_token = False for token in tokens: if get_keyword(token) == 'dense': if dense or (auto_track and auto_track != track): raise InvalidValues dense = token + auto_track = track elif get_keyword(token) == 'auto-flow': - if auto_track: + if auto_flow_token or (auto_track and auto_track != track): raise InvalidValues + auto_flow_token = True auto_track = track - else: + elif token == tokens[-1]: track_templates.append(token) + else: + raise InvalidValues if not auto_track: raise InvalidValues non_auto_track = 'row' if auto_track == 'column' else 'column' From 6ed418984dc30b3e7d382d3e20cfe45487701c78 Mon Sep 17 00:00:00 2001 From: Guillaume Ayoub Date: Thu, 23 May 2024 12:08:56 +0200 Subject: [PATCH 07/10] Avoid getting grid style attribute multiple times --- weasyprint/layout/grid.py | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/weasyprint/layout/grid.py b/weasyprint/layout/grid.py index 9b1e71126..3e308f71e 100644 --- a/weasyprint/layout/grid.py +++ b/weasyprint/layout/grid.py @@ -753,6 +753,8 @@ def grid_layout(context, box, bottom_space, skip_stack, containing_block, cursor_first, cursor_second = implicit_first_1, implicit_second_1 if 'dense' in flow: for child in remaining_grid_items: + first_start = child.style[f'grid_{first_flow}_start'] + first_end = child.style[f'grid_{first_flow}_end'] second_start = child.style[f'grid_{second_flow}_start'] second_end = child.style[f'grid_{second_flow}_end'] second_placement = _get_placement( @@ -763,8 +765,6 @@ def grid_layout(context, box, bottom_space, skip_stack, containing_block, second_i, second_size = second_placement cursor_second = second_i # 2. Increment the cursor’s row (resp. column) position. - first_start = child.style[f'grid_{first_flow}_start'] - first_end = child.style[f'grid_{first_flow}_end'] for first_i in count(cursor_first): if first_start == 'auto': first_i, first_size = _get_placement( @@ -814,10 +814,6 @@ def grid_layout(context, box, bottom_space, skip_stack, containing_block, while True: # 2. Increment the column (resp. row) position of the cursor. first_i = cursor_first - first_start = child.style[f'grid_{first_flow}_start'] - first_end = child.style[f'grid_{first_flow}_end'] - second_start = child.style[f'grid_{second_flow}_start'] - second_end = child.style[f'grid_{second_flow}_end'] for second_i in range(cursor_second, implicit_second_2): if first_start == 'auto': first_i, first_size = _get_placement( @@ -875,6 +871,8 @@ def grid_layout(context, box, bottom_space, skip_stack, containing_block, break else: for child in remaining_grid_items: + first_start = child.style[f'grid_{first_flow}_start'] + first_end = child.style[f'grid_{first_flow}_end'] second_start = child.style[f'grid_{second_flow}_start'] second_end = child.style[f'grid_{second_flow}_end'] second_placement = _get_placement( @@ -886,8 +884,6 @@ def grid_layout(context, box, bottom_space, skip_stack, containing_block, cursor_first += 1 cursor_second = second_i # 2. Increment the cursor’s row (resp. column) position. - first_start = child.style[f'grid_{first_flow}_start'] - first_end = child.style[f'grid_{first_flow}_end'] for cursor_first in count(cursor_first): if first_start == 'auto': first_i, first_size = _get_placement( @@ -930,10 +926,6 @@ def grid_layout(context, box, bottom_space, skip_stack, containing_block, while True: # 1. Increment the column position of the cursor. first_i = cursor_first - first_start = child.style[f'grid_{first_flow}_start'] - first_end = child.style[f'grid_{first_flow}_end'] - second_start = child.style[f'grid_{second_flow}_start'] - second_end = child.style[f'grid_{second_flow}_end'] for second_i in range(cursor_second, implicit_second_2): if first_start == 'auto': first_i, first_size = _get_placement( From c49b2648af06ad218e3128d2e5fa0b313d113c7e Mon Sep 17 00:00:00 2001 From: Guillaume Ayoub Date: Thu, 23 May 2024 13:20:37 +0200 Subject: [PATCH 08/10] =?UTF-8?q?Don=E2=80=99t=20add=20extra=20rows=20and?= =?UTF-8?q?=20columns=20twice?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- weasyprint/layout/grid.py | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) diff --git a/weasyprint/layout/grid.py b/weasyprint/layout/grid.py index 3e308f71e..c15e46da0 100644 --- a/weasyprint/layout/grid.py +++ b/weasyprint/layout/grid.py @@ -851,21 +851,15 @@ def grid_layout(context, box, bottom_space, skip_stack, containing_block, first_diff = ( cursor_first + first_size - 1 - implicit_first_2) if first_diff > 0: - for _ in range(first_diff): - first_tracks.append(next(auto_tracks)) - first_tracks.append([]) - implicit_first_2 = cursor_first + first_size - 1 + implicit_first_2 += first_diff break else: # No room found. # 2. Return to the previous step. cursor_first += 1 - first_diff = cursor_first - implicit_first_2 + first_diff = cursor_first + 1 - implicit_first_2 if first_diff > 0: - for _ in range(first_diff): - first_tracks.append(next(auto_tracks)) - first_tracks.append([]) - implicit_first_2 = cursor_first + implicit_first_2 += first_diff cursor_second = implicit_second_1 continue break @@ -966,10 +960,7 @@ def grid_layout(context, box, bottom_space, skip_stack, containing_block, cursor_first += 1 first_diff = cursor_first + 1 - implicit_first_2 if first_diff > 0: - for _ in range(first_diff): - first_tracks.append(next(auto_tracks)) - first_tracks.append([]) - implicit_first_2 = cursor_first + implicit_first_2 += first_diff cursor_second = implicit_second_1 continue break From d335034c3d4e06cd2a6ea7e59f617dd58367eb74 Mon Sep 17 00:00:00 2001 From: Guillaume Ayoub Date: Thu, 23 May 2024 13:20:52 +0200 Subject: [PATCH 09/10] Test dense grid with auto columns --- tests/layout/test_grid.py | 49 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/tests/layout/test_grid.py b/tests/layout/test_grid.py index 3d3833f5b..ffe49d8eb 100644 --- a/tests/layout/test_grid.py +++ b/tests/layout/test_grid.py @@ -356,6 +356,7 @@ def test_grid_template_areas_extra_span_dense(): assert div_a.width == div_b.width == div_c.width == div_f.width == 3 assert div_d.width == div_e.width == 6 assert {div.height for div in article.children} == {2} + assert article.height == 6 assert article.width == 9 @@ -874,3 +875,51 @@ def test_grid_auto_flow_column(): assert div_a.position_y == div_b.position_y == div_c.position_y == 0 assert div_a.width == div_b.width == div_c.width assert div_a.height == div_b.height == div_c.height == html.height == article.height + + +@assert_no_logs +def test_grid_template_areas_extra_span_column_dense(): + page, = render_pages(''' + +
+
a
+
b
+
c
+
d
+
e
+
f
+
+ ''') + html, = page.children + body, = html.children + article, = body.children + div_a, div_b, div_c, div_d, div_e, div_f = article.children + assert div_a.position_x == div_c.position_x == 0 + assert div_d.position_x == div_f.position_x == 3 + assert div_b.position_x == 6 + assert div_e.position_x == 9 + assert ( + div_a.position_y == div_b.position_y == + div_e.position_y == div_f.position_y == 0) + assert div_c.position_y == div_d.position_y == 2 + assert ( + div_a.width == div_b.width == div_c.width == + div_e.width == div_f.width == 3) + assert div_d.width == 6 + assert ( + div_a.height == div_b.height == div_c.height == + div_d.height == div_f.height == 2) + assert div_e.height == 4 + assert article.height == 4 + assert article.width == 12 From f29ff1c2f140c63c289a4eede29529c6e03e8f4f Mon Sep 17 00:00:00 2001 From: Guillaume Ayoub Date: Tue, 28 May 2024 22:30:41 +0200 Subject: [PATCH 10/10] =?UTF-8?q?Don=E2=80=99t=20respect=20fr=20units=20fo?= =?UTF-8?q?r=20overconstrained=20sizes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/layout/test_grid.py | 31 +++++++++++++++++++++++++++++++ weasyprint/layout/grid.py | 7 ++++--- 2 files changed, 35 insertions(+), 3 deletions(-) diff --git a/tests/layout/test_grid.py b/tests/layout/test_grid.py index ffe49d8eb..905f32348 100644 --- a/tests/layout/test_grid.py +++ b/tests/layout/test_grid.py @@ -509,6 +509,37 @@ def test_grid_shorthand_auto_flow_rows_fr_size(): assert article.width == 10 +@assert_no_logs +def test_grid_template_fr_too_large(): + page, = render_pages(''' + +
+
a
bbb
+
+ ''') + html, = page.children + body, = html.children + article, = body.children + div_a, div_b = article.children + assert div_a.position_x == 0 + assert div_b.position_x == 4 + assert div_a.position_y == div_b.position_y == 0 + assert div_a.height == div_b.height == 2 + assert div_a.width == 4 + assert div_b.width == 6 + assert article.width == 10 + + def test_grid_shorthand_auto_flow_columns_none_dense(): page, = render_pages('''